Skip to content

chore: bump actions/checkout from 4 to 6 #49

chore: bump actions/checkout from 4 to 6

chore: bump actions/checkout from 4 to 6 #49

Workflow file for this run

name: E2E Tests
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
E2E_DIR: tests/e2e
jobs:
e2e-test:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install Eneru
run: pip install ".[notifications]"
- name: Install system dependencies
run: |
sudo apt-get update
sudo apt-get install -y nut-client openssh-client
- name: Generate SSH keys for E2E tests
run: |
ssh-keygen -t ed25519 -f /tmp/e2e-ssh-key -N "" -C "eneru-e2e-test"
mkdir -p ${{ env.E2E_DIR }}/ssh-target
cp /tmp/e2e-ssh-key.pub ${{ env.E2E_DIR }}/ssh-target/authorized_keys
- name: Build and start E2E environment
run: |
cd ${{ env.E2E_DIR }}
# Build and start services (authorized_keys is bind-mounted from ssh-target/)
docker compose up -d --build
docker compose ps
- name: Wait for services to be ready
run: |
echo "Waiting for NUT server..."
for i in {1..30}; do
if upsc TestUPS@localhost:3493 2>/dev/null | grep -q "ups.status"; then
echo "NUT server ready!"
break
fi
echo " Attempt $i/30..."
sleep 2
done
echo ""
echo "Waiting for SSH server..."
for i in {1..30}; do
if nc -z localhost 2222 2>/dev/null; then
echo "SSH server ready!"
break
fi
echo " Attempt $i/30..."
sleep 2
done
echo ""
echo "=== E2E Environment Ready ==="
upsc TestUPS@localhost:3493 2>/dev/null | head -15
- name: Create tmpfs mount for unmount testing
run: |
sudo mkdir -p /tmp/eneru-e2e-tmpfs
sudo mount -t tmpfs -o size=1M tmpfs /tmp/eneru-e2e-tmpfs
echo "test data" | sudo tee /tmp/eneru-e2e-tmpfs/testfile
mount | grep eneru-e2e-tmpfs
- name: Test SSH connectivity
run: |
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
-i /tmp/e2e-ssh-key -p 2222 testuser@localhost \
"echo 'SSH connection successful!'"
# ========================================
# TEST 1: Validate config against real NUT
# ========================================
- name: "Test 1: Validate E2E config"
run: |
echo "=== Test 1: Config Validation ==="
eneru validate --config ${{ env.E2E_DIR }}/config-e2e.yaml
# ========================================
# TEST 2: Monitor normal (online) state
# ========================================
- name: "Test 2: Monitor normal state (no shutdown triggered)"
run: |
echo "=== Test 2: Normal State Monitoring ==="
# Ensure UPS is in online state
cp ${{ env.E2E_DIR }}/scenarios/online-charging.dev ${{ env.E2E_DIR }}/scenarios/apply.dev
sleep 3
# Run Eneru for 5 seconds - should NOT trigger shutdown
timeout 5 eneru run --config ${{ env.E2E_DIR }}/config-e2e-dry-run.yaml 2>&1 | tee /tmp/test2.log || true
# Verify no shutdown was triggered
if grep -q "SHUTDOWN SEQUENCE" /tmp/test2.log; then
echo "FAIL: Shutdown was triggered during normal operation!"
exit 1
fi
echo "PASS: No shutdown triggered during normal operation"
# ========================================
# TEST 3: Detect power failure (dry-run)
# ========================================
- name: "Test 3: Detect power failure (dry-run)"
run: |
echo "=== Test 3: Power Failure Detection ==="
# Clean up any previous shutdown flags
rm -f /tmp/eneru-e2e-shutdown-flag
# Switch to low battery scenario
cp ${{ env.E2E_DIR }}/scenarios/low-battery.dev ${{ env.E2E_DIR }}/scenarios/apply.dev
sleep 3
# Run Eneru in dry-run mode
eneru run --config ${{ env.E2E_DIR }}/config-e2e-dry-run.yaml --exit-after-shutdown 2>&1 | tee /tmp/test3.log || true
# Verify shutdown was triggered (in dry-run)
if ! grep -q "SHUTDOWN SEQUENCE" /tmp/test3.log; then
echo "FAIL: Shutdown was NOT triggered for low battery!"
cat /tmp/test3.log
exit 1
fi
if ! grep -q "DRY-RUN" /tmp/test3.log; then
echo "FAIL: Dry-run mode not indicated!"
exit 1
fi
echo "PASS: Low battery correctly triggered shutdown (dry-run)"
# ========================================
# TEST 4: SSH remote shutdown (real execution)
# ========================================
# NOTE: Container shutdown is tested in dry-run (Test 3). Real container shutdown
# with shutdown_all_remaining_containers=true would stop the NUT server container,
# causing connection loss and repeated shutdown loops. So Test 4 focuses on SSH.
- name: "Test 4: SSH remote shutdown"
run: |
echo "=== Test 4: SSH Remote Shutdown ==="
cd ${{ env.E2E_DIR }}
# Reset SSH target state
docker compose exec -T ssh-target sh -c "rm -f /var/run/shutdown-triggered && touch /var/run/server-alive"
# Clean up shutdown flag
rm -f /tmp/eneru-e2e-shutdown-flag
# Switch to low battery scenario
cp scenarios/low-battery.dev scenarios/apply.dev
sleep 3
# Run Eneru briefly - will trigger shutdown and send SSH command
eneru run --config config-e2e.yaml --exit-after-shutdown 2>&1 | tee /tmp/test4.log || true
echo ""
echo "=== Verifying SSH shutdown ==="
# Verify SSH shutdown command was sent
if docker compose exec -T ssh-target test -f /var/run/shutdown-triggered; then
echo "PASS: SSH shutdown command was received"
else
echo "FAIL: SSH shutdown command was NOT received"
docker compose logs ssh-target
exit 1
fi
# Check the shutdown log
echo "SSH target shutdown log:"
docker compose exec -T ssh-target cat /var/log/shutdown.log || true
echo ""
echo "=== Test 4 PASSED: SSH remote shutdown executed successfully ==="
# ========================================
# TEST 5: FSD (Forced Shutdown) trigger
# ========================================
- name: "Test 5: FSD flag triggers immediate shutdown"
run: |
echo "=== Test 5: FSD Trigger ==="
# Clean up
rm -f /tmp/eneru-e2e-shutdown-flag
# Switch to FSD scenario
cp ${{ env.E2E_DIR }}/scenarios/fsd.dev ${{ env.E2E_DIR }}/scenarios/apply.dev
sleep 3
# Run Eneru in dry-run mode
eneru run --config ${{ env.E2E_DIR }}/config-e2e-dry-run.yaml --exit-after-shutdown 2>&1 | tee /tmp/test5.log || true
# Verify FSD triggered shutdown
if ! grep -q "FSD" /tmp/test5.log; then
echo "FAIL: FSD was not detected!"
cat /tmp/test5.log
exit 1
fi
echo "PASS: FSD correctly triggered shutdown"
# ========================================
# TEST 6: Voltage events (brownout, AVR)
# ========================================
- name: "Test 6: Voltage event detection"
run: |
echo "=== Test 6: Voltage Events ==="
# Clean up
rm -f /tmp/eneru-e2e-shutdown-flag
# Start with normal state
cp ${{ env.E2E_DIR }}/scenarios/online-charging.dev ${{ env.E2E_DIR }}/scenarios/apply.dev
sleep 2
# Switch to brownout scenario
cp ${{ env.E2E_DIR }}/scenarios/brownout.dev ${{ env.E2E_DIR }}/scenarios/apply.dev
sleep 2
# Run briefly to detect brownout
timeout 8 eneru run --config ${{ env.E2E_DIR }}/config-e2e-dry-run.yaml 2>&1 | tee /tmp/test6.log || true
# Verify brownout was detected (should log a voltage warning)
if grep -q -i "voltage\|brownout" /tmp/test6.log; then
echo "PASS: Voltage event detected"
else
echo "Note: Voltage event may not have been logged in short window"
fi
# ========================================
# TEST 7: Notification test (if secret available)
# ========================================
- name: "Test 7: Notification delivery"
env:
E2E_NOTIFICATION_URL: ${{ secrets.E2E_NOTIFICATION_URL }}
run: |
echo "=== Test 7: Notification Delivery ==="
if [ -z "$E2E_NOTIFICATION_URL" ]; then
echo "SKIP: E2E_NOTIFICATION_URL secret not configured"
exit 0
fi
# Create config with notification URL substituted
sed "s|\${E2E_NOTIFICATION_URL}|${E2E_NOTIFICATION_URL}|g" \
${{ env.E2E_DIR }}/config-e2e-notifications.yaml > /tmp/config-notif.yaml
# Test notification delivery
eneru test-notifications --config /tmp/config-notif.yaml
echo "PASS: Notification sent successfully"
# ========================================
# TEST 8: Multi-UPS config validation
# ========================================
- name: "Test 8: Multi-UPS config validation"
run: |
echo "=== Test 8: Multi-UPS Config Validation ==="
# Verify both UPSes are reachable
upsc UPS1@localhost:3493 2>/dev/null | grep -q "ups.status" || { echo "FAIL: UPS1 not reachable"; exit 1; }
upsc UPS2@localhost:3493 2>/dev/null | grep -q "ups.status" || { echo "FAIL: UPS2 not reachable"; exit 1; }
echo "Both UPS1 and UPS2 reachable on NUT server"
# Validate multi-UPS config
eneru validate --config ${{ env.E2E_DIR }}/config-e2e-multi-ups.yaml
echo "PASS: Multi-UPS config validates against real NUT server"
# ========================================
# TEST 9: Multi-UPS isolation - UPS1 fails, UPS2 unaffected
# ========================================
- name: "Test 9: Multi-UPS isolation (UPS1 fails, UPS2 normal)"
run: |
echo "=== Test 9: Multi-UPS Isolation ==="
# Clean up
rm -f /tmp/eneru-e2e-shutdown-flag*
# Ensure both UPSes start online
cp ${{ env.E2E_DIR }}/scenarios/online-charging.dev ${{ env.E2E_DIR }}/scenarios/apply-UPS1.dev
cp ${{ env.E2E_DIR }}/scenarios/online-charging.dev ${{ env.E2E_DIR }}/scenarios/apply-UPS2.dev
sleep 3
# Now fail UPS1 (low battery) while UPS2 stays online
cp ${{ env.E2E_DIR }}/scenarios/low-battery.dev ${{ env.E2E_DIR }}/scenarios/apply-UPS1.dev
sleep 3
# Verify UPS1 is on battery / low
UPS1_STATUS=$(upsc UPS1@localhost:3493 ups.status 2>/dev/null)
UPS2_STATUS=$(upsc UPS2@localhost:3493 ups.status 2>/dev/null)
echo "UPS1 status: $UPS1_STATUS"
echo "UPS2 status: $UPS2_STATUS"
if echo "$UPS1_STATUS" | grep -q "OB"; then
echo "PASS: UPS1 correctly shows on battery"
else
echo "FAIL: UPS1 should be on battery, got: $UPS1_STATUS"
exit 1
fi
if echo "$UPS2_STATUS" | grep -q "OL"; then
echo "PASS: UPS2 correctly shows online (unaffected)"
else
echo "FAIL: UPS2 should still be online, got: $UPS2_STATUS"
exit 1
fi
# Run multi-UPS Eneru briefly -- UPS1 should trigger shutdown for its group
eneru run --config ${{ env.E2E_DIR }}/config-e2e-multi-ups.yaml --exit-after-shutdown 2>&1 | tee /tmp/test9.log || true
# Verify UPS1 triggered shutdown
if grep -q "SHUTDOWN SEQUENCE\|SHUTDOWN INITIATED\|Triggering immediate shutdown" /tmp/test9.log; then
echo "PASS: UPS1 low battery triggered shutdown"
else
echo "FAIL: No shutdown triggered for UPS1"
cat /tmp/test9.log
exit 1
fi
# Verify the log shows UPS1 context (prefixed with display name or UPS name)
if grep -q "E2E UPS1\|UPS1@localhost" /tmp/test9.log; then
echo "PASS: Shutdown log correctly identifies UPS1"
else
echo "Note: UPS identification in logs not verified"
fi
echo "PASS: Multi-UPS isolation working correctly"
# ========================================
# TEST 10: Multi-UPS both online (no false triggers)
# ========================================
- name: "Test 10: Multi-UPS both online (no false triggers)"
run: |
echo "=== Test 10: Multi-UPS Normal Operation ==="
# Clean up
rm -f /tmp/eneru-e2e-shutdown-flag*
# Both UPSes online
cp ${{ env.E2E_DIR }}/scenarios/online-charging.dev ${{ env.E2E_DIR }}/scenarios/apply-UPS1.dev
cp ${{ env.E2E_DIR }}/scenarios/online-charging.dev ${{ env.E2E_DIR }}/scenarios/apply-UPS2.dev
sleep 3
# Run briefly -- should NOT trigger any shutdown
timeout 5 eneru run --config ${{ env.E2E_DIR }}/config-e2e-multi-ups.yaml 2>&1 | tee /tmp/test10.log || true
if grep -q "SHUTDOWN SEQUENCE\|SHUTDOWN INITIATED" /tmp/test10.log; then
echo "FAIL: Shutdown triggered during normal multi-UPS operation!"
exit 1
fi
echo "PASS: No false shutdown triggers with both UPSes online"
# ========================================
# TEST 11: Ownership validation (non-local with containers)
# ========================================
- name: "Test 11: Ownership validation rejects non-local containers"
run: |
echo "=== Test 11: Ownership Validation ==="
cat > /tmp/config-bad-ownership.yaml <<YAML
ups:
- name: "UPS1@localhost:3493"
is_local: true
- name: "UPS2@localhost:3493"
containers:
enabled: true
YAML
# validate should report ERROR for non-local group with containers
OUTPUT=$(eneru validate --config /tmp/config-bad-ownership.yaml 2>&1) || true
if echo "$OUTPUT" | grep -q "ERROR.*containers"; then
echo "PASS: Ownership violation correctly detected"
else
echo "FAIL: Ownership violation not detected"
echo "$OUTPUT"
exit 1
fi
# ========================================
# TEST 12: CLI safety (bare eneru shows help)
# ========================================
- name: "Test 12: CLI safety - bare eneru shows help"
run: |
echo "=== Test 12: CLI Safety ==="
OUTPUT=$(eneru 2>&1) || true
EXIT_CODE=$?
if echo "$OUTPUT" | grep -q "run\|validate\|monitor"; then
echo "PASS: Bare 'eneru' shows help with subcommands"
else
echo "FAIL: Bare 'eneru' did not show help"
echo "$OUTPUT"
exit 1
fi
# ========================================
# TEST 13: TUI --once snapshot
# ========================================
- name: "Test 13: TUI --once snapshot"
run: |
echo "=== Test 13: TUI --once ==="
OUTPUT=$(eneru monitor --config ${{ env.E2E_DIR }}/config-e2e-dry-run.yaml --once 2>&1)
if echo "$OUTPUT" | grep -q "TestUPS@localhost\|Eneru v"; then
echo "PASS: TUI --once outputs UPS status"
else
echo "FAIL: TUI --once did not produce expected output"
echo "$OUTPUT"
exit 1
fi
# ========================================
# TEST 14: Multi-UPS concurrent failure
# ========================================
- name: "Test 14: Multi-UPS concurrent failure (both UPSes fail)"
run: |
echo "=== Test 14: Concurrent UPS Failure ==="
# Clean up
rm -f /tmp/eneru-e2e-shutdown-flag*
# Both UPSes go low-battery simultaneously
cp ${{ env.E2E_DIR }}/scenarios/low-battery.dev ${{ env.E2E_DIR }}/scenarios/apply-UPS1.dev
cp ${{ env.E2E_DIR }}/scenarios/low-battery.dev ${{ env.E2E_DIR }}/scenarios/apply-UPS2.dev
sleep 3
# Run multi-UPS Eneru -- both groups should trigger shutdown
eneru run --config ${{ env.E2E_DIR }}/config-e2e-multi-ups.yaml --exit-after-shutdown 2>&1 | tee /tmp/test14.log || true
# Verify shutdown was triggered
if ! grep -q "SHUTDOWN SEQUENCE\|SHUTDOWN INITIATED\|Triggering immediate shutdown" /tmp/test14.log; then
echo "FAIL: No shutdown triggered during concurrent failure"
cat /tmp/test14.log
exit 1
fi
# Verify both UPSes are referenced in the log
if grep -q "E2E UPS1\|UPS1@localhost" /tmp/test14.log; then
echo "PASS: UPS1 shutdown logged"
else
echo "Note: UPS1 identification not verified in logs"
fi
if grep -q "E2E UPS2\|UPS2@localhost" /tmp/test14.log; then
echo "PASS: UPS2 shutdown logged"
else
echo "Note: UPS2 identification not verified in logs"
fi
echo "PASS: Concurrent failure handled correctly"
# ========================================
# TEST 15: Non-local failure (UPS2 fails, UPS1 unaffected)
# ========================================
- name: "Test 15: Non-local failure (UPS2 fails, UPS1 unaffected)"
run: |
echo "=== Test 15: Non-Local UPS Failure ==="
# Clean up
rm -f /tmp/eneru-e2e-shutdown-flag*
# UPS1 stays online, UPS2 goes low-battery
cp ${{ env.E2E_DIR }}/scenarios/online-charging.dev ${{ env.E2E_DIR }}/scenarios/apply-UPS1.dev
cp ${{ env.E2E_DIR }}/scenarios/low-battery.dev ${{ env.E2E_DIR }}/scenarios/apply-UPS2.dev
sleep 3
# Verify UPS states
UPS1_STATUS=$(upsc UPS1@localhost:3493 ups.status 2>/dev/null)
UPS2_STATUS=$(upsc UPS2@localhost:3493 ups.status 2>/dev/null)
echo "UPS1 status: $UPS1_STATUS (should be OL)"
echo "UPS2 status: $UPS2_STATUS (should be OB)"
# Run multi-UPS Eneru -- UPS2 (non-local) should trigger, UPS1 unaffected
eneru run --config ${{ env.E2E_DIR }}/config-e2e-multi-ups.yaml --exit-after-shutdown 2>&1 | tee /tmp/test15.log || true
# Verify UPS2 triggered shutdown
if ! grep -q "SHUTDOWN SEQUENCE\|SHUTDOWN INITIATED\|Triggering immediate shutdown" /tmp/test15.log; then
echo "FAIL: No shutdown triggered for UPS2"
cat /tmp/test15.log
exit 1
fi
# Verify UPS2 is identified in shutdown context
if grep -q "E2E UPS2\|UPS2@localhost" /tmp/test15.log; then
echo "PASS: UPS2 correctly triggered shutdown"
else
echo "Note: UPS2 identification not verified in logs"
fi
echo "PASS: Non-local failure correctly handled"
# ========================================
# TEST 16: Local drain (drain_on_local_shutdown=true)
# ========================================
- name: "Test 16: Local drain (drain_on_local_shutdown=true)"
run: |
echo "=== Test 16: Local Drain ==="
# Clean up
rm -f /tmp/eneru-e2e-shutdown-flag*
# UPS1 (is_local) goes low-battery, UPS2 stays online
cp ${{ env.E2E_DIR }}/scenarios/low-battery.dev ${{ env.E2E_DIR }}/scenarios/apply-UPS1.dev
cp ${{ env.E2E_DIR }}/scenarios/online-charging.dev ${{ env.E2E_DIR }}/scenarios/apply-UPS2.dev
sleep 3
# Run with drain config
eneru run --config ${{ env.E2E_DIR }}/config-e2e-multi-ups-drain.yaml --exit-after-shutdown 2>&1 | tee /tmp/test16.log || true
# Verify shutdown was triggered
if ! grep -q "SHUTDOWN SEQUENCE\|SHUTDOWN INITIATED\|Triggering immediate shutdown" /tmp/test16.log; then
echo "FAIL: No shutdown triggered"
cat /tmp/test16.log
exit 1
fi
# Verify drain message appears
if grep -qi "drain" /tmp/test16.log; then
echo "PASS: Drain message logged"
else
echo "FAIL: Drain message not found in logs"
cat /tmp/test16.log
exit 1
fi
echo "PASS: Local drain correctly executed"
# ========================================
# TEST 17: Local no-drain (drain_on_local_shutdown=false)
# ========================================
- name: "Test 17: Local no-drain (drain_on_local_shutdown=false)"
run: |
echo "=== Test 17: Local No-Drain ==="
# Clean up
rm -f /tmp/eneru-e2e-shutdown-flag*
# UPS1 (is_local) goes low-battery, UPS2 stays online
cp ${{ env.E2E_DIR }}/scenarios/low-battery.dev ${{ env.E2E_DIR }}/scenarios/apply-UPS1.dev
cp ${{ env.E2E_DIR }}/scenarios/online-charging.dev ${{ env.E2E_DIR }}/scenarios/apply-UPS2.dev
sleep 3
# Run with default multi-UPS config (drain=false)
eneru run --config ${{ env.E2E_DIR }}/config-e2e-multi-ups.yaml --exit-after-shutdown 2>&1 | tee /tmp/test17.log || true
# Verify UPS1 shutdown triggered
if ! grep -q "SHUTDOWN SEQUENCE\|SHUTDOWN INITIATED\|Triggering immediate shutdown" /tmp/test17.log; then
echo "FAIL: No shutdown triggered for UPS1"
cat /tmp/test17.log
exit 1
fi
# Verify NO drain message
if grep -qi "drain" /tmp/test17.log; then
echo "FAIL: Drain should NOT occur with drain_on_local_shutdown=false"
exit 1
fi
echo "PASS: No-drain correctly skipped drain step"
# ========================================
# TEST 18: Recovery (OB then power restored)
# ========================================
- name: "Test 18: Recovery - power restored before shutdown"
run: |
echo "=== Test 18: Power Recovery ==="
# Clean up
rm -f /tmp/eneru-e2e-shutdown-flag*
# Start with on-battery (above thresholds -- no shutdown trigger)
cp ${{ env.E2E_DIR }}/scenarios/on-battery.dev ${{ env.E2E_DIR }}/scenarios/apply.dev
sleep 3
# Run Eneru in background
timeout 12 eneru run --config ${{ env.E2E_DIR }}/config-e2e-dry-run.yaml 2>&1 | tee /tmp/test18.log &
ENERU_PID=$!
# Wait for Eneru to detect on-battery state
sleep 4
# Restore power
cp ${{ env.E2E_DIR }}/scenarios/online-charging.dev ${{ env.E2E_DIR }}/scenarios/apply.dev
sleep 3
# Wait for Eneru to detect recovery
wait $ENERU_PID 2>/dev/null || true
# Verify POWER_RESTORED was logged
if grep -qi "POWER_RESTORED\|power.*restored\|Power restored" /tmp/test18.log; then
echo "PASS: Power restoration detected"
else
echo "FAIL: POWER_RESTORED not logged"
cat /tmp/test18.log
exit 1
fi
# Verify NO shutdown was triggered
if grep -q "SHUTDOWN SEQUENCE" /tmp/test18.log; then
echo "FAIL: Shutdown should NOT have been triggered during recovery"
exit 1
fi
echo "PASS: Recovery correctly handled - no shutdown, power restored logged"
- name: Collect logs on failure
if: failure()
run: |
echo "=== Docker Compose Logs ==="
cd ${{ env.E2E_DIR }}
docker compose logs
echo ""
echo "=== Eneru Test Logs ==="
cat /tmp/test*.log 2>/dev/null || echo "No test logs found"
- name: Cleanup
if: always()
run: |
cd ${{ env.E2E_DIR }}
docker compose down -v --remove-orphans
sudo umount /tmp/eneru-e2e-tmpfs 2>/dev/null || true