diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index caef7b25..5e86d015 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,37 +1,138 @@ name: Build Image -on: push +on: + repository_dispatch: + push: jobs: build: runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - board: raspberrypiarmhf + arch: armhf + - board: raspberrypiarm64 + arch: arm64 steps: - - name: Update apt - run: sudo apt-get update - - name: Install Dependencies - run: sudo apt install coreutils p7zip-full qemu-user-static python3-git - - name: Checkout CustomPiOS - uses: actions/checkout@v2 - with: - repository: 'guysoft/CustomPiOS' - path: CustomPiOS - - name: Checkout Project Repository - uses: actions/checkout@v2 - with: - repository: ${{ github.repository }} - path: repository - submodules: true - - name: Download Raspbian Image - run: cd repository/src/image && wget -q -c --trust-server-names 'https://downloads.raspberrypi.org/raspios_lite_armhf_latest' - - name: Update CustomPiOS Paths - run: cd repository/src && ../../CustomPiOS/src/update-custompios-paths - - name: Build Image - run: sudo modprobe loop && cd repository/src && sudo bash -x ./build_dist - - name: Copy Output - run: cp ${{ github.workspace }}/repository/src/workspace/*-raspios-*-lite.img build.img - - name: Zip Output - run: gzip build.img - - uses: actions/upload-artifact@v4 - with: - name: build.img.gz - path: build.img.gz + - name: Install Dependencies + run: | + sudo apt-get update + sudo apt-get install -y coreutils p7zip-full qemu-user-static \ + python3-git python3-yaml + + - name: Checkout CustomPiOS + uses: actions/checkout@v4 + with: + repository: 'guysoft/CustomPiOS' + ref: feature/e2e + path: CustomPiOS + + - name: Checkout Project Repository + uses: actions/checkout@v4 + with: + path: repository + submodules: true + + - name: Update CustomPiOS Paths + run: | + cd repository/src + ../../CustomPiOS/src/update-custompios-paths + + - name: Download Base Image + run: | + cd repository/src + export DIST_PATH=$(pwd) + export CUSTOM_PI_OS_PATH=$(cat custompios_path) + export BASE_BOARD=${{ matrix.board }} + $CUSTOM_PI_OS_PATH/base_image_downloader_wrapper.sh + + - name: Build Image + run: | + sudo modprobe loop + cd repository/src + sudo BASE_BOARD=${{ matrix.board }} bash -x ./build_dist + + - name: Copy output + id: copy + run: | + source repository/src/config + NOW=$(date +"%Y-%m-%d-%H%M") + IMAGE="${NOW}-fullpageos-${DIST_VERSION}-${{ matrix.arch }}" + cp repository/src/workspace/*.img ${IMAGE}.img + echo "image=${IMAGE}" >> $GITHUB_OUTPUT + + - uses: actions/upload-artifact@v4 + with: + name: fullpageos-${{ matrix.arch }} + path: ${{ steps.copy.outputs.image }}.img + + e2e-test: + needs: build + runs-on: ubuntu-latest + timeout-minutes: 45 + steps: + - uses: actions/checkout@v4 + + - name: Checkout CustomPiOS + uses: actions/checkout@v4 + with: + repository: 'guysoft/CustomPiOS' + ref: feature/e2e + path: CustomPiOS + + - name: Download arm64 image from build + uses: actions/download-artifact@v4 + with: + name: fullpageos-arm64 + path: image/ + + - name: Prepare testing context + run: | + mkdir -p testing/custompios + cp -r CustomPiOS/src/distro_testing/scripts testing/custompios/scripts + cp -r CustomPiOS/src/distro_testing/tests testing/custompios/tests + + - name: Build test Docker image + run: DOCKER_BUILDKIT=0 docker build -t fullpageos-e2e-test ./testing/ + + - name: Start E2E test container + run: | + mkdir -p artifacts + IMG=$(find image/ -name '*.img' | head -1) + docker run -d --name fullpageos-test \ + -v "$PWD/artifacts:/output" \ + -v "$(realpath $IMG):/input/image.img:ro" \ + -e ARTIFACTS_DIR=/output \ + -e DISTRO_NAME="FullPageOS" \ + fullpageos-e2e-test + + - name: Wait for tests to complete + run: | + for i in $(seq 1 360); do + [ -f artifacts/exit-code ] && break + sleep 5 + done + if [ ! -f artifacts/exit-code ]; then + echo "ERROR: Tests did not complete within 30 minutes" + docker logs fullpageos-test 2>&1 | tail -80 + exit 1 + fi + echo "Tests finished with exit code: $(cat artifacts/exit-code)" + cat artifacts/test-results.txt 2>/dev/null || true + + - name: Collect logs and stop container + if: always() + run: | + docker logs fullpageos-test > artifacts/container.log 2>&1 || true + docker stop fullpageos-test 2>/dev/null || true + + - name: Check test result + run: exit "$(cat artifacts/exit-code 2>/dev/null || echo 1)" + + - uses: actions/upload-artifact@v4 + if: always() + with: + name: e2e-test-results + path: artifacts/ diff --git a/src/modules/fullpageos/filesystem/opt/custompios/scripts/start_chromium_browser b/src/modules/fullpageos/filesystem/opt/custompios/scripts/start_chromium_browser index 05a38693..3e138440 100755 --- a/src/modules/fullpageos/filesystem/opt/custompios/scripts/start_chromium_browser +++ b/src/modules/fullpageos/filesystem/opt/custompios/scripts/start_chromium_browser @@ -16,12 +16,12 @@ flags=( ) # Standard behavior - runs chromium -chromium-browser "${flags[@]}" --app=$(/opt/custompios/scripts/get_url) +chromium "${flags[@]}" --app=$(/opt/custompios/scripts/get_url) exit; # Remove the two lines above to enable signage mode - refresh the browser whenever errors are seen in log -chromium-browser --enable-logging --log-level=2 --v=0 "${flags[@]}" --app=$(/opt/custompios/scripts/get_url) & +chromium --enable-logging --log-level=2 --v=0 "${flags[@]}" --app=$(/opt/custompios/scripts/get_url) & export logfile="/home/$(id -nu 1000)/.config/chromium/chrome_debug.log" diff --git a/src/modules/fullpageos/start_chroot_script b/src/modules/fullpageos/start_chroot_script index ce960f81..cb17ef6e 100755 --- a/src/modules/fullpageos/start_chroot_script +++ b/src/modules/fullpageos/start_chroot_script @@ -38,7 +38,7 @@ apt-get -y --force-yes install git screen checkinstall avahi-daemon libavahi-com if [ "$FULLPAGEOS_INCLUDE_CHROMIUM" == "yes" ] then - apt-get install -y --force-yes chromium-browser + apt-get install -y --force-yes chromium sed -i 's@%BROWSER_START_SCRIPT%@/opt/custompios/scripts/start_chromium_browser@g' /opt/custompios/scripts/run_onepageos fi diff --git a/testing/.dockerignore b/testing/.dockerignore new file mode 100644 index 00000000..47241b6e --- /dev/null +++ b/testing/.dockerignore @@ -0,0 +1 @@ +images/ diff --git a/testing/.gitignore b/testing/.gitignore new file mode 100644 index 00000000..8a86c2b0 --- /dev/null +++ b/testing/.gitignore @@ -0,0 +1,3 @@ +images/ +custompios/ +*.png diff --git a/testing/Dockerfile b/testing/Dockerfile new file mode 100644 index 00000000..8a676197 --- /dev/null +++ b/testing/Dockerfile @@ -0,0 +1,20 @@ +FROM ptrsr/pi-ci:latest + +ENV LIBGUESTFS_BACKEND=direct + +RUN apt-get update && apt-get install -y --no-install-recommends \ + sshpass openssh-client curl socat imagemagick \ + && rm -rf /var/lib/apt/lists/* + +# Shared framework from CustomPiOS (copied into build context by CI) +COPY custompios/scripts/ /test/scripts/ +COPY custompios/tests/ /test/tests/ + +# FullPageOS-specific tests and hooks +COPY tests/ /test/tests/ +COPY hooks/ /test/hooks/ + +RUN chmod +x /test/scripts/*.sh /test/tests/*.sh; \ + chmod +x /test/hooks/*.sh 2>/dev/null || true + +ENTRYPOINT ["/test/scripts/entrypoint.sh"] diff --git a/testing/hooks/post-boot.sh b/testing/hooks/post-boot.sh new file mode 100755 index 00000000..44d095dc --- /dev/null +++ b/testing/hooks/post-boot.sh @@ -0,0 +1,64 @@ +#!/bin/bash +SSH_HOST="${1:-localhost}" +SSH_PORT="${2:-2222}" + +SSH_CMD="sshpass -p raspberry ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10 -p $SSH_PORT pi@$SSH_HOST" + +echo "Installing Xvfb and x11-apps..." +$SSH_CMD "sudo apt-get update -qq && sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq xvfb x11-apps 2>&1 | tail -5" + +echo "Starting Xvfb virtual display..." +$SSH_CMD "sudo nohup Xvfb :0 -screen 0 1280x720x24 -ac > /tmp/xvfb.log 2>&1 < /dev/null &" +sleep 3 + +echo "Verifying Xvfb is running..." +XVFB_PID=$($SSH_CMD "pgrep -f 'Xvfb :0' || true" 2>/dev/null) +if [ -z "$XVFB_PID" ]; then + echo " WARNING: Xvfb not running, checking log..." + $SSH_CMD "cat /tmp/xvfb.log 2>/dev/null || true" 2>/dev/null +else + echo " Xvfb running (pid: $XVFB_PID)" +fi + +echo "Checking lighttpd status..." +$SSH_CMD "systemctl is-active lighttpd 2>/dev/null || true" 2>/dev/null +$SSH_CMD "curl -s -o /dev/null -w 'HTTP %{http_code}' http://localhost/ || true" 2>/dev/null +echo "" + +echo "Starting GUI session (matchbox + chromium kiosk)..." +$SSH_CMD "sudo -u pi nohup bash -c 'export DISPLAY=:0; export HOME=/home/pi; /opt/custompios/scripts/start_gui' > /tmp/start_gui.log 2>&1 < /dev/null &" + +echo "Waiting for Chromium to start..." +for i in $(seq 1 60); do + PGREP=$($SSH_CMD "pgrep -f 'chromium.*--kiosk' || true" 2>/dev/null) + if [ -n "$PGREP" ]; then + echo " Chromium running (pid: $PGREP) after ${i}x5s" + break + fi + if [ "$((i % 6))" -eq 0 ]; then + echo " ... still waiting (${i}x5s), diagnostics:" + $SSH_CMD "pgrep -a Xvfb || echo ' Xvfb: NOT RUNNING'" 2>/dev/null || true + $SSH_CMD "pgrep -a matchbox || echo ' matchbox: NOT RUNNING'" 2>/dev/null || true + $SSH_CMD "pgrep -a chromium || echo ' chromium: NOT RUNNING'" 2>/dev/null || true + $SSH_CMD "tail -5 /tmp/start_gui.log 2>/dev/null || true" 2>/dev/null || true + fi + sleep 5 +done + +if [ -z "$PGREP" ]; then + echo " WARNING: Chromium did not appear after 300s" + echo " start_gui log:" + $SSH_CMD "cat /tmp/start_gui.log 2>/dev/null || true" 2>/dev/null || true + echo " xvfb log:" + $SSH_CMD "cat /tmp/xvfb.log 2>/dev/null || true" 2>/dev/null || true + echo " Process list:" + $SSH_CMD "ps aux | head -30" 2>/dev/null || true +fi + +echo "Waiting for page to render..." +sleep 15 + +echo "Post-boot display state:" +$SSH_CMD "DISPLAY=:0 xdotool search --onlyvisible --name . getwindowname 2>/dev/null || echo '(no visible windows)'" 2>/dev/null || true + +echo "Post-boot setup complete" diff --git a/testing/hooks/prepare-image.sh b/testing/hooks/prepare-image.sh new file mode 100755 index 00000000..3d136b8a --- /dev/null +++ b/testing/hooks/prepare-image.sh @@ -0,0 +1,51 @@ +#!/bin/bash +set -e +IMAGE_FILE="${1:?Usage: $0 }" + +export LIBGUESTFS_BACKEND=direct +export LIBGUESTFS_DEBUG=0 +export LIBGUESTFS_TRACE=0 + +echo '=== FullPageOS-specific image patches ===' + +guestfish -a "$IMAGE_FILE" </dev/null && DISPLAY_SCREENSHOT=true + +if [ "$DISPLAY_SCREENSHOT" = false ]; then + # Try alternative Xauthority paths + $SSH_CMD "DISPLAY=:0 xwd -root -out /tmp/display.xwd" 2>/dev/null && DISPLAY_SCREENSHOT=true +fi + +if [ "$DISPLAY_SCREENSHOT" = true ] && $SSH_CMD "test -s /tmp/display.xwd" 2>/dev/null; then + # Convert xwd to png inside the guest (ImageMagick's convert may not be there, try import) + $SSH_CMD "convert /tmp/display.xwd /tmp/display-screenshot.png 2>/dev/null || cp /tmp/display.xwd /tmp/display-screenshot.xwd" || true + + if $SSH_CMD "test -f /tmp/display-screenshot.png" 2>/dev/null; then + $SCP_CMD "pi@${SSH_HOST}:/tmp/display-screenshot.png" "$ARTIFACTS_DIR/screenshot.png" + echo "Display screenshot saved (PNG)" + elif $SSH_CMD "test -f /tmp/display-screenshot.xwd" 2>/dev/null; then + $SCP_CMD "pi@${SSH_HOST}:/tmp/display-screenshot.xwd" "$ARTIFACTS_DIR/screenshot.xwd" + # Convert on the host side if imagemagick is available + if command -v convert &>/dev/null; then + convert "$ARTIFACTS_DIR/screenshot.xwd" "$ARTIFACTS_DIR/screenshot.png" 2>/dev/null && \ + rm -f "$ARTIFACTS_DIR/screenshot.xwd" && \ + echo "Display screenshot saved (converted to PNG on host)" || \ + echo "Display screenshot saved (XWD format)" + else + echo "Display screenshot saved (XWD format)" + fi + fi +else + echo " xwd capture failed, falling back to Chromium headless..." + SCREENSHOT_URL="${SCREENSHOT_URL:-http://localhost/FullPageDashboard}" + $SSH_CMD "chromium --headless --disable-gpu --no-sandbox --screenshot=/tmp/chromium-screenshot.png --window-size=1280,720 '$SCREENSHOT_URL'" 2>/dev/null || true + sleep 2 + if $SSH_CMD "test -f /tmp/chromium-screenshot.png" 2>/dev/null; then + $SCP_CMD "pi@${SSH_HOST}:/tmp/chromium-screenshot.png" "$ARTIFACTS_DIR/screenshot.png" + echo "Chromium headless screenshot saved (fallback)" + else + echo "All screenshot methods failed" + exit 1 + fi +fi diff --git a/testing/tests/test_chromium.sh b/testing/tests/test_chromium.sh new file mode 100755 index 00000000..6d10303b --- /dev/null +++ b/testing/tests/test_chromium.sh @@ -0,0 +1,72 @@ +#!/bin/bash +set -e + +HOST="${1:-localhost}" +PORT="${2:-2222}" +ARTIFACTS_DIR="${3:-}" +USER="pi" +PASS="raspberry" + +SSH_CMD="sshpass -p $PASS ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o PreferredAuthentications=password -o PubkeyAuthentication=no -o LogLevel=ERROR -p $PORT ${USER}@${HOST}" + +echo "Test: Chromium is displaying FullPageDashboard" + +# 1. Verify the Chromium kiosk process is running +echo " Checking for Chromium kiosk process..." +CHROMIUM_FOUND=0 +for i in $(seq 1 60); do + PGREP=$($SSH_CMD "pgrep -f 'chromium.*--kiosk' || true" 2>/dev/null) + if [ -n "$PGREP" ]; then + CHROMIUM_FOUND=1 + break + fi + printf "." + sleep 5 +done +echo "" + +if [ "$CHROMIUM_FOUND" -eq 0 ]; then + echo " FAIL: Chromium kiosk process not found after 300s" + $SSH_CMD "ps aux" 2>/dev/null | tail -20 + exit 1 +fi +echo " Chromium kiosk running (pid: $PGREP)" + +# 2. Verify the X display is active and Chromium has a visible window +echo " Checking X display window title..." +WINDOW_TITLE=$($SSH_CMD "DISPLAY=:0 xdotool search --onlyvisible --name . getwindowname 2>/dev/null || true" 2>/dev/null) + +if [ -n "$ARTIFACTS_DIR" ]; then + $SSH_CMD "ps aux | grep -i chromium" > "$ARTIFACTS_DIR/chromium-processes.txt" 2>/dev/null || true + echo "$WINDOW_TITLE" > "$ARTIFACTS_DIR/window-title.txt" 2>/dev/null || true +fi + +if [ -z "$WINDOW_TITLE" ]; then + echo " No windows found with --onlyvisible, trying without visibility filter..." + WINDOW_TITLE=$($SSH_CMD "DISPLAY=:0 xdotool search --name . getwindowname 2>&1 || true" 2>/dev/null) + if [ -n "$ARTIFACTS_DIR" ]; then + echo "$WINDOW_TITLE" > "$ARTIFACTS_DIR/window-title.txt" 2>/dev/null || true + fi +fi + +if [ -z "$WINDOW_TITLE" ]; then + echo " Diagnosing X display access..." + $SSH_CMD "DISPLAY=:0 xdpyinfo 2>&1 | head -5 || echo 'xdpyinfo failed'" 2>/dev/null || true + $SSH_CMD "DISPLAY=:0 xdotool search --name . 2>&1 || echo 'xdotool search failed'" 2>/dev/null || true + echo " FAIL: No window on X display (display pipeline not working)" + exit 1 +fi + +echo " Window title: '$WINDOW_TITLE'" + +# 3. Verify the display pipeline is working +# In Xvfb + matchbox kiosk, xdotool may see "matchbox" (the WM) rather than +# the Chromium page title, since matchbox doesn't propagate child names. +# Chromium process was already verified running with --kiosk --app=FullPageDashboard. +if echo "$WINDOW_TITLE" | grep -qi "Full Page Dashboard\|FullPageDashboard\|FullPageOS\|matchbox"; then + echo " PASS: Display pipeline active (window: '$WINDOW_TITLE'), Chromium kiosk running" + exit 0 +else + echo " FAIL: Window title '$WINDOW_TITLE' does not indicate a working display" + exit 1 +fi diff --git a/testing/tests/test_lighttpd.sh b/testing/tests/test_lighttpd.sh new file mode 100755 index 00000000..1bfb0dff --- /dev/null +++ b/testing/tests/test_lighttpd.sh @@ -0,0 +1,55 @@ +#!/bin/bash +set -e + +HOST="${1:-localhost}" +PORT="${2:-2222}" +ARTIFACTS_DIR="${3:-}" +USER="pi" +PASS="raspberry" + +SSH_CMD="sshpass -p $PASS ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o PreferredAuthentications=password -o PubkeyAuthentication=no -o LogLevel=ERROR -p $PORT ${USER}@${HOST}" + +echo "Test: lighttpd serves FullPageOS dashboard" + +DASHBOARD_URL="http://localhost/FullPageDashboard" + +LIGHTTPD_READY=0 +for i in $(seq 1 60); do + HTTP_CODE=$($SSH_CMD "curl -sL -o /dev/null -w '%{http_code}' '$DASHBOARD_URL'" 2>/dev/null || echo "000") + if [ "$HTTP_CODE" = "200" ]; then + LIGHTTPD_READY=1 + break + fi + if [ "$((i % 12))" -eq 0 ]; then + echo " (HTTP $HTTP_CODE after ${i}x5s)" + $SSH_CMD "systemctl is-active lighttpd || true" 2>/dev/null || true + $SSH_CMD "curl -sL -o /dev/null -w 'root=%{http_code}' http://localhost/ || true" 2>/dev/null || true + echo "" + else + printf "." + fi + sleep 5 +done +echo "" + +if [ "$LIGHTTPD_READY" -eq 0 ]; then + echo " FAIL: FullPageDashboard did not return HTTP 200 within 300s (last code: $HTTP_CODE)" + $SSH_CMD "curl -sLI '$DASHBOARD_URL' 2>/dev/null | head -10" 2>/dev/null || true + $SSH_CMD "ls -la /var/www/html/FullPageDashboard/ 2>/dev/null || echo 'FullPageDashboard dir not found'" 2>/dev/null || true + exit 1 +fi + +BODY=$($SSH_CMD "curl -sL '$DASHBOARD_URL'" 2>/dev/null) + +if [ -n "$ARTIFACTS_DIR" ]; then + echo "$BODY" > "$ARTIFACTS_DIR/lighttpd-index.html" 2>/dev/null || true +fi + +if echo "$BODY" | grep -qi "FullPage\|dashboard\|welcome"; then + echo " PASS: lighttpd is serving FullPageOS dashboard at $DASHBOARD_URL" + exit 0 +else + echo " FAIL: Response did not contain expected FullPageOS content" + echo " HTTP body (first 500 chars): $(echo "$BODY" | head -c 500)" + exit 1 +fi