From 7f52b5a58687ee176d82ac6c37ff2882853d71bb Mon Sep 17 00:00:00 2001 From: Guy Sheffer Date: Fri, 20 Feb 2026 23:18:39 +0200 Subject: [PATCH 01/22] Add arm64 build and E2E test system - Update build.yml to build both armhf and arm64 using CustomPiOS v2 board system (BASE_BOARD matrix) with base_image_downloader - Add testing/ directory with Docker+QEMU-based E2E test harness that boots an OctoPi arm64 image, verifies SSH access, and checks OctoPrint web server availability - Add e2e-test.yml workflow triggered by build completion that runs E2E tests on the arm64 artifact and uploads screenshot + logs --- .github/workflows/build.yml | 100 +++++++++++++------------ .github/workflows/e2e-test.yml | 112 ++++++++++++++++++++++++++++ testing/.dockerignore | 1 + testing/.gitignore | 1 + testing/Dockerfile | 11 +++ testing/run-test.sh | 82 ++++++++++++++++++++ testing/scripts/boot-qemu.sh | 29 +++++++ testing/scripts/entrypoint.sh | 112 ++++++++++++++++++++++++++++ testing/scripts/prepare-image.sh | 93 +++++++++++++++++++++++ testing/scripts/wait-for-ssh.sh | 44 +++++++++++ testing/tests/test_boot.sh | 24 ++++++ testing/tests/test_octoprint_web.sh | 46 ++++++++++++ 12 files changed, 607 insertions(+), 48 deletions(-) create mode 100644 .github/workflows/e2e-test.yml create mode 100644 testing/.dockerignore create mode 100644 testing/.gitignore create mode 100644 testing/Dockerfile create mode 100755 testing/run-test.sh create mode 100755 testing/scripts/boot-qemu.sh create mode 100755 testing/scripts/entrypoint.sh create mode 100755 testing/scripts/prepare-image.sh create mode 100755 testing/scripts/wait-for-ssh.sh create mode 100755 testing/tests/test_boot.sh create mode 100755 testing/tests/test_octoprint_web.sh diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8deaf620..7ddb7632 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -3,64 +3,68 @@ name: Build Image on: repository_dispatch: push: - schedule: + schedule: - cron: '0 0 * * *' jobs: build: runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - board: raspberrypiarmhf + arch: armhf + - board: raspberrypiarm64 + arch: arm64 steps: - - name: Install Dependencies - run: | - sudo apt update - sudo apt install coreutils p7zip-full qemu-user-static python3-git + - 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@v2 - with: - repository: 'guysoft/CustomPiOS' - path: CustomPiOS + - name: Checkout CustomPiOS + uses: actions/checkout@v4 + with: + repository: 'guysoft/CustomPiOS' + path: CustomPiOS - - name: Checkout Project Repository - uses: actions/checkout@v2 - with: - path: repository - submodules: true + - name: Checkout Project Repository + uses: actions/checkout@v4 + with: + path: repository + submodules: true - - name: Download Raspbian Image - run: | - cd repository/src/image - wget -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: Update CustomPiOS Paths - run: | - cd repository/src - ../../CustomPiOS/src/update-custompios-paths - - # - name: Force apt mirror to work around intermittent mirror hiccups - # run: | - # echo "OCTOPI_APTMIRROR=http://mirror.us.leaseweb.net/raspbian/raspbian" > repository/src/config.local + - 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 bash -x ./build_dist + - 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-octopi-$DIST_VERSION + - name: Copy output + id: copy + run: | + source repository/src/config + NOW=$(date +"%Y-%m-%d-%H%M") + IMAGE="${NOW}-octopi-${DIST_VERSION}-${{ matrix.arch }}" + cp repository/src/workspace/*.img ${IMAGE}.img + echo "image=${IMAGE}" >> $GITHUB_OUTPUT - cp repository/src/workspace/*.img $IMAGE.img - - echo "::set-output name=image::$IMAGE" - - # artifact upload will take care of zipping for us - - uses: actions/upload-artifact@v4 - if: github.event_name == 'schedule' - with: - name: ${{ steps.copy.outputs.image }} - path: ${{ steps.copy.outputs.image }}.img + - uses: actions/upload-artifact@v4 + with: + name: octopi-${{ matrix.arch }} + path: ${{ steps.copy.outputs.image }}.img diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml new file mode 100644 index 00000000..1448cd26 --- /dev/null +++ b/.github/workflows/e2e-test.yml @@ -0,0 +1,112 @@ +name: E2E Test + +on: + workflow_run: + workflows: ["Build Image"] + types: [completed] + workflow_dispatch: + inputs: + image_url: + description: "OctoPi image zip URL (arm64). Leave empty to use stable 1.1.0." + required: false + +jobs: + e2e-test: + runs-on: ubuntu-latest + timeout-minutes: 30 + if: > + github.event_name == 'workflow_dispatch' || + github.event.workflow_run.conclusion == 'success' + steps: + - uses: actions/checkout@v4 + + - name: Download arm64 image from build + if: github.event_name == 'workflow_run' + uses: actions/download-artifact@v4 + with: + name: octopi-arm64 + path: image/ + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Download arm64 image from URL + if: github.event_name == 'workflow_dispatch' + run: | + URL="${{ github.event.inputs.image_url }}" + if [ -z "$URL" ]; then + URL="https://unofficialpi.org/Distros/OctoPi/octopi-bookworm-arm64-lite-1.1.0.zip" + fi + wget -q --show-progress -O octopi.zip "$URL" + mkdir -p image && unzip octopi.zip '*.img' -d image/ + + - name: Build test Docker image + run: DOCKER_BUILDKIT=0 docker build -t octopi-e2e-test ./testing/ + + - name: Start E2E test container + run: | + mkdir -p artifacts + IMG=$(find image/ -name '*.img' | head -1) + docker run -d --name octopi-test \ + -p 9980:9980 \ + -v "$PWD/artifacts:/output" \ + -v "$(realpath $IMG):/input/image.img:ro" \ + -e ARTIFACTS_DIR=/output \ + -e KEEP_ALIVE=true \ + octopi-e2e-test + + - name: Wait for tests to complete + run: | + for i in $(seq 1 180); do + [ -f artifacts/exit-code ] && break + sleep 5 + done + if [ ! -f artifacts/exit-code ]; then + echo "ERROR: Tests did not complete within 15 minutes" + docker logs octopi-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: Wait for OctoPrint web server + run: | + for i in $(seq 1 60); do + HTTP=$(curl -4 -s -o /dev/null -w '%{http_code}' \ + --connect-timeout 5 http://127.0.0.1:9980 2>/dev/null || true) + [ "$HTTP" = "200" ] && break + sleep 5 + done + + - name: Take OctoPrint screenshot + run: | + npx --yes puppeteer browsers install chrome + node -e " + const puppeteer = require('puppeteer'); + (async () => { + const browser = await puppeteer.launch({ + headless: 'new', args: ['--no-sandbox'] + }); + const page = await browser.newPage(); + await page.setViewport({width: 1280, height: 900}); + await page.goto('http://127.0.0.1:9980', { + waitUntil: 'networkidle2', timeout: 60000 + }); + await page.screenshot({path: 'artifacts/octoprint-screenshot.png'}); + await browser.close(); + })(); + " + + - name: Collect logs and stop container + if: always() + run: | + docker logs octopi-test > artifacts/container.log 2>&1 || true + docker stop octopi-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/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..47241b6e --- /dev/null +++ b/testing/.gitignore @@ -0,0 +1 @@ +images/ diff --git a/testing/Dockerfile b/testing/Dockerfile new file mode 100644 index 00000000..248d7882 --- /dev/null +++ b/testing/Dockerfile @@ -0,0 +1,11 @@ +FROM ptrsr/pi-ci:latest + +ENV LIBGUESTFS_BACKEND=direct + +RUN apt-get update && apt-get install -y --no-install-recommends sshpass openssh-client curl && rm -rf /var/lib/apt/lists/* + +COPY scripts/ /test/scripts/ +COPY tests/ /test/tests/ +RUN chmod +x /test/scripts/*.sh /test/tests/*.sh + +ENTRYPOINT ["/test/scripts/entrypoint.sh"] diff --git a/testing/run-test.sh b/testing/run-test.sh new file mode 100755 index 00000000..5f943ed1 --- /dev/null +++ b/testing/run-test.sh @@ -0,0 +1,82 @@ +#!/bin/bash +set -e + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +IMAGE_DIR="${SCRIPT_DIR}/images" + +OCTOPI_URL="${IMAGE_URL:-https://unofficialpi.org/Distros/OctoPi/octopi-bookworm-arm64-lite-1.1.0.zip}" +OCTOPI_ZIP="octopi-bookworm-arm64-lite-1.1.0.zip" +OCTOPI_MD5="74cfd8e6c5b6ff9d8443aaa357201bcd" +DOCKER_IMAGE="octopi-e2e-test" +HTTP_PORT=9980 + +mkdir -p "$IMAGE_DIR" + +if [ -n "$IMAGE_PATH" ]; then + IMG_FILE="$(readlink -f "$IMAGE_PATH")" + echo "Using provided image: $IMG_FILE" +else + ZIP_PATH="${IMAGE_DIR}/${OCTOPI_ZIP}" + + if [ ! -f "$ZIP_PATH" ]; then + echo "Downloading OctoPi arm64 image from $OCTOPI_URL..." + wget -q --show-progress -O "$ZIP_PATH" "$OCTOPI_URL" + else + echo "Using cached download: $ZIP_PATH" + fi + + if [ "$OCTOPI_URL" = "https://unofficialpi.org/Distros/OctoPi/octopi-bookworm-arm64-lite-1.1.0.zip" ]; then + echo "Verifying checksum..." + ACTUAL_MD5=$(md5sum "$ZIP_PATH" | awk '{print $1}') + if [ "$ACTUAL_MD5" != "$OCTOPI_MD5" ]; then + echo "ERROR: MD5 mismatch! Expected: $OCTOPI_MD5 Got: $ACTUAL_MD5" + rm -f "$ZIP_PATH" + exit 1 + fi + echo "Checksum OK." + fi + + IMG_NAME=$(unzip -Z1 "$ZIP_PATH" | grep '\.img$' | head -1) + if [ -z "$IMG_NAME" ]; then + echo "ERROR: No .img file found inside $ZIP_PATH" + exit 1 + fi + + IMG_FILE="${IMAGE_DIR}/${IMG_NAME}" + + if [ ! -f "$IMG_FILE" ]; then + echo "Extracting $IMG_NAME..." + unzip -o "$ZIP_PATH" "$IMG_NAME" -d "$IMAGE_DIR" + else + echo "Using cached image: $IMG_FILE" + fi +fi + +if [ ! -f "$IMG_FILE" ]; then + echo "ERROR: Image file not found: $IMG_FILE" + exit 1 +fi + +echo "" +echo "Image: $IMG_FILE" +echo "Size: $(du -h "$IMG_FILE" | awk '{print $1}')" +echo "" + +echo "Building Docker image..." +DOCKER_BUILDKIT=0 docker build -t "$DOCKER_IMAGE" "$SCRIPT_DIR" + +DOCKER_RUN_ARGS="docker run --rm" +if [ -n "$KEEP_ALIVE" ]; then + DOCKER_RUN_ARGS+=" -p ${HTTP_PORT}:${HTTP_PORT}" + DOCKER_RUN_ARGS+=" -e KEEP_ALIVE=true" +fi +if [ -n "$ARTIFACTS_DIR" ]; then + DOCKER_RUN_ARGS+=" -v $(realpath "$ARTIFACTS_DIR"):/output" + DOCKER_RUN_ARGS+=" -e ARTIFACTS_DIR=/output" +fi + +echo "" +echo "Running E2E test..." +$DOCKER_RUN_ARGS \ + -v "${IMG_FILE}:/input/image.img:ro" \ + "$DOCKER_IMAGE" diff --git a/testing/scripts/boot-qemu.sh b/testing/scripts/boot-qemu.sh new file mode 100755 index 00000000..c83facfc --- /dev/null +++ b/testing/scripts/boot-qemu.sh @@ -0,0 +1,29 @@ +#!/bin/bash +set -e + +IMAGE_FILE="${1:?Usage: $0 }" +KERNEL="${2:-/base/kernel.img}" +SSH_PORT="${3:-2222}" +LOG_FILE="${4:-/tmp/qemu-serial.log}" +HTTP_PORT="${5:-8080}" + +echo "=== Starting QEMU (aarch64, -M virt) ===" +echo " Image: $IMAGE_FILE" +echo " Kernel: $KERNEL" +echo " SSH: port $SSH_PORT -> guest:22" +echo " HTTP: port $HTTP_PORT -> guest:80" + +qemu-system-aarch64 \ + -machine virt \ + -cpu cortex-a72 \ + -m 2G \ + -smp 4 \ + -kernel "$KERNEL" \ + -append "rw console=ttyAMA0 root=/dev/vda2 rootfstype=ext4 rootdelay=1 loglevel=2" \ + -drive "file=$IMAGE_FILE,format=qcow2,id=hd0,if=none,cache=writeback" \ + -device virtio-blk,drive=hd0,bootindex=0 \ + -netdev "user,id=mynet,hostfwd=tcp::${SSH_PORT}-:22,hostfwd=tcp::${HTTP_PORT}-:80" \ + -device virtio-net-pci,netdev=mynet \ + -nographic \ + -no-reboot \ + 2>&1 | tee "$LOG_FILE" diff --git a/testing/scripts/entrypoint.sh b/testing/scripts/entrypoint.sh new file mode 100755 index 00000000..f1f511ad --- /dev/null +++ b/testing/scripts/entrypoint.sh @@ -0,0 +1,112 @@ +#!/bin/bash +set -e + +INPUT_IMAGE="/input/image.img" +WORK_DIR="/work" +IMAGE_FILE="${WORK_DIR}/distro.qcow2" +KERNEL="/base/kernel.img" +SSH_PORT=2222 +SSH_TIMEOUT="${SSH_TIMEOUT:-600}" +LOG_FILE="/tmp/qemu-serial.log" +HTTP_PORT="${HTTP_PORT:-9980}" + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +TEST_DIR="$(dirname "$SCRIPT_DIR")/tests" + +echo "============================================" +echo " OctoPi E2E Test" +echo "============================================" + +if [ ! -f "$INPUT_IMAGE" ]; then + echo "ERROR: No image found at $INPUT_IMAGE" + echo "Mount an OctoPi .img file with: -v /path/to/image.img:/input/image.img:ro" + exit 1 +fi + +if [ ! -f "$KERNEL" ]; then + echo "ERROR: No kernel found at $KERNEL" + exit 1 +fi + +cleanup() { + if [ -n "$QEMU_PID" ] && kill -0 "$QEMU_PID" 2>/dev/null; then + echo "Stopping QEMU (pid $QEMU_PID)..." + kill "$QEMU_PID" 2>/dev/null || true + wait "$QEMU_PID" 2>/dev/null || true + fi +} +trap cleanup EXIT + +echo "" +echo "--- Step 1: Prepare image ---" +"$SCRIPT_DIR/prepare-image.sh" "$INPUT_IMAGE" "$IMAGE_FILE" + +echo "" +echo "--- Step 2: Boot QEMU ---" +"$SCRIPT_DIR/boot-qemu.sh" "$IMAGE_FILE" "$KERNEL" "$SSH_PORT" "$LOG_FILE" "$HTTP_PORT" & +QEMU_PID=$! +echo "QEMU started (pid $QEMU_PID)" + +echo "" +echo "--- Step 3: Wait for SSH ---" +set +e +"$SCRIPT_DIR/wait-for-ssh.sh" localhost "$SSH_PORT" "$SSH_TIMEOUT" +SSH_WAIT_RC=$? +set -e +if [ "$SSH_WAIT_RC" -ne 0 ]; then + echo "SSH wait failed. QEMU log tail:" + tail -50 "$LOG_FILE" 2>/dev/null || true + if [ -n "$ARTIFACTS_DIR" ]; then + cp "$LOG_FILE" "$ARTIFACTS_DIR/qemu-boot.log" 2>/dev/null || true + echo "1" > "$ARTIFACTS_DIR/exit-code" + fi + exit 1 +fi + +echo "" +echo "--- Step 4: Run tests ---" +TEST_RESULT=0 +for test_script in "$TEST_DIR"/test_*.sh; do + if [ -x "$test_script" ]; then + echo "Running $(basename "$test_script")..." + if [ -n "$ARTIFACTS_DIR" ]; then + if "$test_script" localhost "$SSH_PORT" "$ARTIFACTS_DIR"; then + echo " -> PASSED" + else + echo " -> FAILED" + TEST_RESULT=1 + fi + else + if "$test_script" localhost "$SSH_PORT"; then + echo " -> PASSED" + else + echo " -> FAILED" + TEST_RESULT=1 + fi + fi + fi +done + +echo "" +echo "============================================" +if [ "$TEST_RESULT" -eq 0 ]; then + echo " ALL TESTS PASSED" +else + echo " SOME TESTS FAILED" +fi +echo "============================================" + +if [ -n "$ARTIFACTS_DIR" ]; then + echo "Collecting artifacts to $ARTIFACTS_DIR..." + cp "$LOG_FILE" "$ARTIFACTS_DIR/qemu-boot.log" 2>/dev/null || true + echo "$TEST_RESULT" > "$ARTIFACTS_DIR/exit-code" + echo "TEST_RESULT=$TEST_RESULT" > "$ARTIFACTS_DIR/test-results.txt" +fi + +if [ -n "$KEEP_ALIVE" ]; then + echo "Keeping container alive (KEEP_ALIVE set)..." + trap - EXIT + sleep infinity +else + exit "$TEST_RESULT" +fi diff --git a/testing/scripts/prepare-image.sh b/testing/scripts/prepare-image.sh new file mode 100755 index 00000000..dde90806 --- /dev/null +++ b/testing/scripts/prepare-image.sh @@ -0,0 +1,93 @@ +#!/bin/bash +set -e +INPUT_IMAGE="${1:?Usage: $0 }" +OUTPUT_IMAGE="${2:?Usage: $0 }" +PIPASS=$(openssl passwd -6 raspberry) + +echo '=== Preparing image ===' +mkdir -p /work +echo 'Converting to qcow2...' +qemu-img convert -f raw -O qcow2 "$INPUT_IMAGE" "$OUTPUT_IMAGE" +echo 'Patching image (rootfs)...' +export LIBGUESTFS_BACKEND=direct +export LIBGUESTFS_DEBUG=0 +export LIBGUESTFS_TRACE=0 +guestfish -a "$OUTPUT_IMAGE" < /dev/tcp/"$HOST"/"$PORT") 2>/dev/null; then + RESULT=$(sshpass -p "$PASS" ssh $SSH_OPTS -p "$PORT" "${USER}@${HOST}" true 2>&1) + RC=$? + if [ "$RC" -eq 0 ]; then + echo "" + echo "SSH is ready (took ${ELAPSED}s)" + exit 0 + fi + if [ $(( ATTEMPT % 6 )) -eq 0 ]; then + echo "" + echo "[${ELAPSED}s] Port open, sshpass rc=$RC output: $RESULT" + echo "[${ELAPSED}s] Trying verbose SSH..." + sshpass -p "$PASS" ssh -v $SSH_OPTS -p "$PORT" "${USER}@${HOST}" true 2>&1 | tail -20 + else + printf "x" + fi + else + printf "." + fi + sleep 5 +done diff --git a/testing/tests/test_boot.sh b/testing/tests/test_boot.sh new file mode 100755 index 00000000..ad4e624f --- /dev/null +++ b/testing/tests/test_boot.sh @@ -0,0 +1,24 @@ +#!/bin/bash +set -e + +HOST="${1:-localhost}" +PORT="${2:-2222}" +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: SSH login and run 'echo hello world'" + +OUTPUT=$($SSH_CMD 'echo hello world' 2>/dev/null) + +if [ "$OUTPUT" = "hello world" ]; then + echo " Output: '$OUTPUT'" + echo " PASS: Got expected output" + exit 0 +else + echo " Expected: 'hello world'" + echo " Got: '$OUTPUT'" + echo " FAIL: Unexpected output" + exit 1 +fi diff --git a/testing/tests/test_octoprint_web.sh b/testing/tests/test_octoprint_web.sh new file mode 100755 index 00000000..14c68011 --- /dev/null +++ b/testing/tests/test_octoprint_web.sh @@ -0,0 +1,46 @@ +#!/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: OctoPrint web server is accessible" + +OCTOPRINT_READY=0 +for i in $(seq 1 24); do + HTTP_CODE=$($SSH_CMD "curl -s -o /dev/null -w '%{http_code}' http://localhost" 2>/dev/null || echo "000") + if [ "$HTTP_CODE" = "200" ]; then + OCTOPRINT_READY=1 + break + fi + printf "W" + sleep 5 +done +echo "" + +if [ "$OCTOPRINT_READY" -eq 0 ]; then + echo " FAIL: OctoPrint web server not reachable after 120s" + exit 1 +fi + +echo " OctoPrint web server is ready (HTTP 200)" + +FULL_HTML=$($SSH_CMD "curl -s http://localhost" 2>/dev/null) + +if [ -n "$ARTIFACTS_DIR" ]; then + echo "$FULL_HTML" > "$ARTIFACTS_DIR/octoprint.html" + echo " Saved HTML to $ARTIFACTS_DIR/octoprint.html" +fi + +if echo "$FULL_HTML" | grep -q "OctoPrint"; then + echo " PASS: OctoPrint web UI returned expected content" + exit 0 +else + echo " FAIL: Response did not contain 'OctoPrint'" + exit 1 +fi From 2a9064d0d9105fd942eef47d0fa2470c51e70098 Mon Sep 17 00:00:00 2001 From: Guy Sheffer Date: Sat, 21 Feb 2026 23:04:15 +0200 Subject: [PATCH 02/22] Move E2E test into build.yml, enable e2e-test on feature branch - Add e2e-test job to build.yml with needs: build, downloads the octopi-arm64 artifact and runs the QEMU test + screenshot - Change e2e-test.yml to trigger on push to feature/e2e and devel using a stable arm64 image (workflow_run doesn't work from non-default branches) --- .github/workflows/build.yml | 85 ++++++++++++++++++++++++++++++++++ .github/workflows/e2e-test.yml | 30 ++---------- 2 files changed, 89 insertions(+), 26 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7ddb7632..7244b4ca 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -68,3 +68,88 @@ jobs: with: name: octopi-${{ matrix.arch }} path: ${{ steps.copy.outputs.image }}.img + + e2e-test: + needs: build + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v4 + + - name: Download arm64 image from build + uses: actions/download-artifact@v4 + with: + name: octopi-arm64 + path: image/ + + - name: Build test Docker image + run: DOCKER_BUILDKIT=0 docker build -t octopi-e2e-test ./testing/ + + - name: Start E2E test container + run: | + mkdir -p artifacts + IMG=$(find image/ -name '*.img' | head -1) + docker run -d --name octopi-test \ + -p 9980:9980 \ + -v "$PWD/artifacts:/output" \ + -v "$(realpath $IMG):/input/image.img:ro" \ + -e ARTIFACTS_DIR=/output \ + -e KEEP_ALIVE=true \ + octopi-e2e-test + + - name: Wait for tests to complete + run: | + for i in $(seq 1 180); do + [ -f artifacts/exit-code ] && break + sleep 5 + done + if [ ! -f artifacts/exit-code ]; then + echo "ERROR: Tests did not complete within 15 minutes" + docker logs octopi-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: Wait for OctoPrint web server + run: | + for i in $(seq 1 60); do + HTTP=$(curl -4 -s -o /dev/null -w '%{http_code}' \ + --connect-timeout 5 http://127.0.0.1:9980 2>/dev/null || true) + [ "$HTTP" = "200" ] && echo "OctoPrint ready" && break + sleep 5 + done + + - name: Take OctoPrint screenshot + run: | + npx --yes puppeteer browsers install chrome + node -e " + const puppeteer = require('puppeteer'); + (async () => { + const browser = await puppeteer.launch({ + headless: 'new', args: ['--no-sandbox'] + }); + const page = await browser.newPage(); + await page.setViewport({width: 1280, height: 900}); + await page.goto('http://127.0.0.1:9980', { + waitUntil: 'networkidle2', timeout: 60000 + }); + await page.screenshot({path: 'artifacts/octoprint-screenshot.png'}); + await browser.close(); + })(); + " + + - name: Collect logs and stop container + if: always() + run: | + docker logs octopi-test > artifacts/container.log 2>&1 || true + docker stop octopi-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/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml index 1448cd26..ca3b8c34 100644 --- a/.github/workflows/e2e-test.yml +++ b/.github/workflows/e2e-test.yml @@ -1,41 +1,19 @@ name: E2E Test on: - workflow_run: - workflows: ["Build Image"] - types: [completed] - workflow_dispatch: - inputs: - image_url: - description: "OctoPi image zip URL (arm64). Leave empty to use stable 1.1.0." - required: false + push: + branches: [feature/e2e, devel] jobs: e2e-test: runs-on: ubuntu-latest timeout-minutes: 30 - if: > - github.event_name == 'workflow_dispatch' || - github.event.workflow_run.conclusion == 'success' steps: - uses: actions/checkout@v4 - - name: Download arm64 image from build - if: github.event_name == 'workflow_run' - uses: actions/download-artifact@v4 - with: - name: octopi-arm64 - path: image/ - run-id: ${{ github.event.workflow_run.id }} - github-token: ${{ secrets.GITHUB_TOKEN }} - - - name: Download arm64 image from URL - if: github.event_name == 'workflow_dispatch' + - name: Download stable arm64 image run: | - URL="${{ github.event.inputs.image_url }}" - if [ -z "$URL" ]; then - URL="https://unofficialpi.org/Distros/OctoPi/octopi-bookworm-arm64-lite-1.1.0.zip" - fi + URL="https://unofficialpi.org/Distros/OctoPi/octopi-bookworm-arm64-lite-1.1.0.zip" wget -q --show-progress -O octopi.zip "$URL" mkdir -p image && unzip octopi.zip '*.img' -d image/ From daf0122b3d2be57fcf25827a092c0421d90039ef Mon Sep 17 00:00:00 2001 From: Guy Sheffer Date: Sun, 22 Feb 2026 00:04:29 +0200 Subject: [PATCH 03/22] Remove standalone e2e-test.yml E2E testing lives in build.yml as a job that tests the built arm64 image. No need for a separate workflow against a stable image. --- .github/workflows/e2e-test.yml | 90 ---------------------------------- 1 file changed, 90 deletions(-) delete mode 100644 .github/workflows/e2e-test.yml diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml deleted file mode 100644 index ca3b8c34..00000000 --- a/.github/workflows/e2e-test.yml +++ /dev/null @@ -1,90 +0,0 @@ -name: E2E Test - -on: - push: - branches: [feature/e2e, devel] - -jobs: - e2e-test: - runs-on: ubuntu-latest - timeout-minutes: 30 - steps: - - uses: actions/checkout@v4 - - - name: Download stable arm64 image - run: | - URL="https://unofficialpi.org/Distros/OctoPi/octopi-bookworm-arm64-lite-1.1.0.zip" - wget -q --show-progress -O octopi.zip "$URL" - mkdir -p image && unzip octopi.zip '*.img' -d image/ - - - name: Build test Docker image - run: DOCKER_BUILDKIT=0 docker build -t octopi-e2e-test ./testing/ - - - name: Start E2E test container - run: | - mkdir -p artifacts - IMG=$(find image/ -name '*.img' | head -1) - docker run -d --name octopi-test \ - -p 9980:9980 \ - -v "$PWD/artifacts:/output" \ - -v "$(realpath $IMG):/input/image.img:ro" \ - -e ARTIFACTS_DIR=/output \ - -e KEEP_ALIVE=true \ - octopi-e2e-test - - - name: Wait for tests to complete - run: | - for i in $(seq 1 180); do - [ -f artifacts/exit-code ] && break - sleep 5 - done - if [ ! -f artifacts/exit-code ]; then - echo "ERROR: Tests did not complete within 15 minutes" - docker logs octopi-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: Wait for OctoPrint web server - run: | - for i in $(seq 1 60); do - HTTP=$(curl -4 -s -o /dev/null -w '%{http_code}' \ - --connect-timeout 5 http://127.0.0.1:9980 2>/dev/null || true) - [ "$HTTP" = "200" ] && break - sleep 5 - done - - - name: Take OctoPrint screenshot - run: | - npx --yes puppeteer browsers install chrome - node -e " - const puppeteer = require('puppeteer'); - (async () => { - const browser = await puppeteer.launch({ - headless: 'new', args: ['--no-sandbox'] - }); - const page = await browser.newPage(); - await page.setViewport({width: 1280, height: 900}); - await page.goto('http://127.0.0.1:9980', { - waitUntil: 'networkidle2', timeout: 60000 - }); - await page.screenshot({path: 'artifacts/octoprint-screenshot.png'}); - await browser.close(); - })(); - " - - - name: Collect logs and stop container - if: always() - run: | - docker logs octopi-test > artifacts/container.log 2>&1 || true - docker stop octopi-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/ From 896303c1b02979ec461c556ee0f616ec4144872a Mon Sep 17 00:00:00 2001 From: Guy Sheffer Date: Sun, 22 Feb 2026 13:57:03 +0200 Subject: [PATCH 04/22] Fix puppeteer screenshot: npm install instead of npx browsers install --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7244b4ca..ef7c6af8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -122,7 +122,7 @@ jobs: - name: Take OctoPrint screenshot run: | - npx --yes puppeteer browsers install chrome + npm install puppeteer node -e " const puppeteer = require('puppeteer'); (async () => { From caf03f9d30cf02d53562c50696d8527b09e8a8e3 Mon Sep 17 00:00:00 2001 From: Guy Sheffer Date: Sun, 22 Feb 2026 22:44:16 +0200 Subject: [PATCH 05/22] Wait for OctoPrint Setup Wizard before taking screenshot The previous screenshot captured OctoPrint's "starting up" loading screen instead of the actual UI. Now both the CI workflow and the E2E test wait for OctoPrint to fully finish its startup phase by checking for CONFIG_WIZARD in the page HTML, and puppeteer waits for the #wizard_dialog to become visible before capturing. --- .github/workflows/build.yml | 46 ++++++++++++++++++++++++----- testing/tests/test_octoprint_web.sh | 23 ++++++++++----- 2 files changed, 54 insertions(+), 15 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ef7c6af8..9b66ed73 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -111,14 +111,23 @@ jobs: echo "Tests finished with exit code: $(cat artifacts/exit-code)" cat artifacts/test-results.txt 2>/dev/null || true - - name: Wait for OctoPrint web server + - name: Wait for OctoPrint to fully start run: | - for i in $(seq 1 60); do - HTTP=$(curl -4 -s -o /dev/null -w '%{http_code}' \ - --connect-timeout 5 http://127.0.0.1:9980 2>/dev/null || true) - [ "$HTTP" = "200" ] && echo "OctoPrint ready" && break + echo "Waiting for OctoPrint to finish startup..." + for i in $(seq 1 90); do + BODY=$(curl -4 -s --connect-timeout 5 http://127.0.0.1:9980 2>/dev/null || true) + if echo "$BODY" | grep -q "CONFIG_WIZARD"; then + echo "OctoPrint fully started (Setup Wizard ready)" + exit 0 + elif echo "$BODY" | grep -q "starting up"; then + printf "S" + else + printf "." + fi sleep 5 done + echo "" + echo "WARNING: OctoPrint may not be fully started yet, proceeding anyway" - name: Take OctoPrint screenshot run: | @@ -131,10 +140,31 @@ jobs: }); const page = await browser.newPage(); await page.setViewport({width: 1280, height: 900}); - await page.goto('http://127.0.0.1:9980', { - waitUntil: 'networkidle2', timeout: 60000 - }); + + // Retry loading until OctoPrint finishes its startup phase + for (let attempt = 0; attempt < 30; attempt++) { + await page.goto('http://127.0.0.1:9980', { + waitUntil: 'networkidle2', timeout: 60000 + }); + const html = await page.content(); + if (html.includes('CONFIG_WIZARD') || html.includes('id=\"login\"')) break; + console.log('OctoPrint still starting up, retrying in 10s... (attempt ' + (attempt+1) + ')'); + await new Promise(r => setTimeout(r, 10000)); + } + + // Wait for the Setup Wizard dialog to appear + try { + await page.waitForSelector('#wizard_dialog', { visible: true, timeout: 120000 }); + } catch (e) { + console.log('Wizard did not appear, taking screenshot of current state'); + } + + // Dismiss notification popovers by clicking the wizard body + try { await page.click('#wizard_dialog .modal-body'); } catch(e) {} + await new Promise(r => setTimeout(r, 2000)); + await page.screenshot({path: 'artifacts/octoprint-screenshot.png'}); + console.log('Screenshot captured'); await browser.close(); })(); " diff --git a/testing/tests/test_octoprint_web.sh b/testing/tests/test_octoprint_web.sh index 14c68011..adcfa3d7 100755 --- a/testing/tests/test_octoprint_web.sh +++ b/testing/tests/test_octoprint_web.sh @@ -9,26 +9,35 @@ 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: OctoPrint web server is accessible" +echo "Test: OctoPrint web server is accessible and fully started" OCTOPRINT_READY=0 -for i in $(seq 1 24); do +for i in $(seq 1 60); do + BODY=$($SSH_CMD "curl -s http://localhost" 2>/dev/null || echo "") HTTP_CODE=$($SSH_CMD "curl -s -o /dev/null -w '%{http_code}' http://localhost" 2>/dev/null || echo "000") + if [ "$HTTP_CODE" = "200" ]; then - OCTOPRINT_READY=1 - break + if echo "$BODY" | grep -q "starting up"; then + printf "S" + elif echo "$BODY" | grep -q "CONFIG_WIZARD\|OctoPrint"; then + OCTOPRINT_READY=1 + break + else + printf "W" + fi + else + printf "." fi - printf "W" sleep 5 done echo "" if [ "$OCTOPRINT_READY" -eq 0 ]; then - echo " FAIL: OctoPrint web server not reachable after 120s" + echo " FAIL: OctoPrint did not fully start within 300s" exit 1 fi -echo " OctoPrint web server is ready (HTTP 200)" +echo " OctoPrint web server is fully started (HTTP 200, UI loaded)" FULL_HTML=$($SSH_CMD "curl -s http://localhost" 2>/dev/null) From b547ccf3b55599bb7027b3cf587359b71d895984 Mon Sep 17 00:00:00 2001 From: Guy Sheffer Date: Wed, 11 Mar 2026 01:16:59 +0200 Subject: [PATCH 06/22] Refactor e2e tests to use shared CustomPiOS distro_testing framework Replace standalone testing scripts with the shared framework from CustomPiOS/src/distro_testing/. The CI workflow now checks out CustomPiOS and copies the shared scripts into the build context. OctoPi-specific logic moves into hooks/ (haproxy IPv4 patching, headless browser screenshot). Removes run-test.sh and testing/scripts/ (now provided by the framework). --- .github/workflows/build.yml | 15 +++++ testing/.gitignore | 2 + testing/Dockerfile | 15 ++++- testing/hooks/prepare-image.sh | 30 +++++++++ testing/hooks/screenshot.sh | 36 ++++++++++ testing/run-test.sh | 82 ---------------------- testing/scripts/boot-qemu.sh | 29 -------- testing/scripts/entrypoint.sh | 112 ------------------------------- testing/scripts/prepare-image.sh | 93 ------------------------- testing/scripts/wait-for-ssh.sh | 44 ------------ testing/tests/test_boot.sh | 24 ------- 11 files changed, 95 insertions(+), 387 deletions(-) create mode 100755 testing/hooks/prepare-image.sh create mode 100755 testing/hooks/screenshot.sh delete mode 100755 testing/run-test.sh delete mode 100755 testing/scripts/boot-qemu.sh delete mode 100755 testing/scripts/entrypoint.sh delete mode 100755 testing/scripts/prepare-image.sh delete mode 100755 testing/scripts/wait-for-ssh.sh delete mode 100755 testing/tests/test_boot.sh diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9b66ed73..db71e165 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -28,6 +28,7 @@ jobs: uses: actions/checkout@v4 with: repository: 'guysoft/CustomPiOS' + ref: feature/e2e path: CustomPiOS - name: Checkout Project Repository @@ -76,12 +77,25 @@ jobs: 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: octopi-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 octopi-e2e-test ./testing/ @@ -94,6 +108,7 @@ jobs: -v "$PWD/artifacts:/output" \ -v "$(realpath $IMG):/input/image.img:ro" \ -e ARTIFACTS_DIR=/output \ + -e DISTRO_NAME="OctoPi" \ -e KEEP_ALIVE=true \ octopi-e2e-test diff --git a/testing/.gitignore b/testing/.gitignore index 47241b6e..8a86c2b0 100644 --- a/testing/.gitignore +++ b/testing/.gitignore @@ -1 +1,3 @@ images/ +custompios/ +*.png diff --git a/testing/Dockerfile b/testing/Dockerfile index 248d7882..ab56a399 100644 --- a/testing/Dockerfile +++ b/testing/Dockerfile @@ -2,10 +2,19 @@ FROM ptrsr/pi-ci:latest ENV LIBGUESTFS_BACKEND=direct -RUN apt-get update && apt-get install -y --no-install-recommends sshpass openssh-client curl && rm -rf /var/lib/apt/lists/* +RUN apt-get update && apt-get install -y --no-install-recommends \ + sshpass openssh-client curl socat imagemagick \ + && rm -rf /var/lib/apt/lists/* -COPY scripts/ /test/scripts/ +# Shared framework from CustomPiOS (copied into build context by CI) +COPY custompios/scripts/ /test/scripts/ +COPY custompios/tests/ /test/tests/ + +# OctoPi-specific tests and hooks COPY tests/ /test/tests/ -RUN chmod +x /test/scripts/*.sh /test/tests/*.sh +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/prepare-image.sh b/testing/hooks/prepare-image.sh new file mode 100755 index 00000000..66ca08a9 --- /dev/null +++ b/testing/hooks/prepare-image.sh @@ -0,0 +1,30 @@ +#!/bin/bash +set -e +IMAGE_FILE="${1:?Usage: $0 }" + +export LIBGUESTFS_BACKEND=direct +export LIBGUESTFS_DEBUG=0 +export LIBGUESTFS_TRACE=0 + +echo '=== OctoPi-specific image patches ===' + +echo 'Downloading haproxy config for IPv4 patching...' +guestfish -a "$IMAGE_FILE" </dev/null || echo "") + +if [ -n "$BODY" ]; then + echo "$BODY" > "$ARTIFACTS_DIR/octoprint-ui.html" + echo " Saved OctoPrint HTML to artifacts" +fi + +# Attempt headless screenshot from inside the container if a browser is available +HTTP_PORT="${QEMU_HTTP_PORT:-8080}" +for BROWSER in chromium chromium-browser google-chrome; do + if command -v "$BROWSER" &>/dev/null; then + echo " Using $BROWSER for headless screenshot..." + "$BROWSER" --headless --disable-gpu --no-sandbox \ + --virtual-time-budget=10000 \ + --screenshot="$ARTIFACTS_DIR/screenshot.png" \ + --window-size=1280,720 \ + "http://localhost:${HTTP_PORT}" 2>/dev/null || true + if [ -f "$ARTIFACTS_DIR/screenshot.png" ]; then + echo " Browser screenshot saved" + exit 0 + fi + fi +done + +echo " No headless browser available in container (HTML artifact saved instead)" diff --git a/testing/run-test.sh b/testing/run-test.sh deleted file mode 100755 index 5f943ed1..00000000 --- a/testing/run-test.sh +++ /dev/null @@ -1,82 +0,0 @@ -#!/bin/bash -set -e - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -IMAGE_DIR="${SCRIPT_DIR}/images" - -OCTOPI_URL="${IMAGE_URL:-https://unofficialpi.org/Distros/OctoPi/octopi-bookworm-arm64-lite-1.1.0.zip}" -OCTOPI_ZIP="octopi-bookworm-arm64-lite-1.1.0.zip" -OCTOPI_MD5="74cfd8e6c5b6ff9d8443aaa357201bcd" -DOCKER_IMAGE="octopi-e2e-test" -HTTP_PORT=9980 - -mkdir -p "$IMAGE_DIR" - -if [ -n "$IMAGE_PATH" ]; then - IMG_FILE="$(readlink -f "$IMAGE_PATH")" - echo "Using provided image: $IMG_FILE" -else - ZIP_PATH="${IMAGE_DIR}/${OCTOPI_ZIP}" - - if [ ! -f "$ZIP_PATH" ]; then - echo "Downloading OctoPi arm64 image from $OCTOPI_URL..." - wget -q --show-progress -O "$ZIP_PATH" "$OCTOPI_URL" - else - echo "Using cached download: $ZIP_PATH" - fi - - if [ "$OCTOPI_URL" = "https://unofficialpi.org/Distros/OctoPi/octopi-bookworm-arm64-lite-1.1.0.zip" ]; then - echo "Verifying checksum..." - ACTUAL_MD5=$(md5sum "$ZIP_PATH" | awk '{print $1}') - if [ "$ACTUAL_MD5" != "$OCTOPI_MD5" ]; then - echo "ERROR: MD5 mismatch! Expected: $OCTOPI_MD5 Got: $ACTUAL_MD5" - rm -f "$ZIP_PATH" - exit 1 - fi - echo "Checksum OK." - fi - - IMG_NAME=$(unzip -Z1 "$ZIP_PATH" | grep '\.img$' | head -1) - if [ -z "$IMG_NAME" ]; then - echo "ERROR: No .img file found inside $ZIP_PATH" - exit 1 - fi - - IMG_FILE="${IMAGE_DIR}/${IMG_NAME}" - - if [ ! -f "$IMG_FILE" ]; then - echo "Extracting $IMG_NAME..." - unzip -o "$ZIP_PATH" "$IMG_NAME" -d "$IMAGE_DIR" - else - echo "Using cached image: $IMG_FILE" - fi -fi - -if [ ! -f "$IMG_FILE" ]; then - echo "ERROR: Image file not found: $IMG_FILE" - exit 1 -fi - -echo "" -echo "Image: $IMG_FILE" -echo "Size: $(du -h "$IMG_FILE" | awk '{print $1}')" -echo "" - -echo "Building Docker image..." -DOCKER_BUILDKIT=0 docker build -t "$DOCKER_IMAGE" "$SCRIPT_DIR" - -DOCKER_RUN_ARGS="docker run --rm" -if [ -n "$KEEP_ALIVE" ]; then - DOCKER_RUN_ARGS+=" -p ${HTTP_PORT}:${HTTP_PORT}" - DOCKER_RUN_ARGS+=" -e KEEP_ALIVE=true" -fi -if [ -n "$ARTIFACTS_DIR" ]; then - DOCKER_RUN_ARGS+=" -v $(realpath "$ARTIFACTS_DIR"):/output" - DOCKER_RUN_ARGS+=" -e ARTIFACTS_DIR=/output" -fi - -echo "" -echo "Running E2E test..." -$DOCKER_RUN_ARGS \ - -v "${IMG_FILE}:/input/image.img:ro" \ - "$DOCKER_IMAGE" diff --git a/testing/scripts/boot-qemu.sh b/testing/scripts/boot-qemu.sh deleted file mode 100755 index c83facfc..00000000 --- a/testing/scripts/boot-qemu.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/bin/bash -set -e - -IMAGE_FILE="${1:?Usage: $0 }" -KERNEL="${2:-/base/kernel.img}" -SSH_PORT="${3:-2222}" -LOG_FILE="${4:-/tmp/qemu-serial.log}" -HTTP_PORT="${5:-8080}" - -echo "=== Starting QEMU (aarch64, -M virt) ===" -echo " Image: $IMAGE_FILE" -echo " Kernel: $KERNEL" -echo " SSH: port $SSH_PORT -> guest:22" -echo " HTTP: port $HTTP_PORT -> guest:80" - -qemu-system-aarch64 \ - -machine virt \ - -cpu cortex-a72 \ - -m 2G \ - -smp 4 \ - -kernel "$KERNEL" \ - -append "rw console=ttyAMA0 root=/dev/vda2 rootfstype=ext4 rootdelay=1 loglevel=2" \ - -drive "file=$IMAGE_FILE,format=qcow2,id=hd0,if=none,cache=writeback" \ - -device virtio-blk,drive=hd0,bootindex=0 \ - -netdev "user,id=mynet,hostfwd=tcp::${SSH_PORT}-:22,hostfwd=tcp::${HTTP_PORT}-:80" \ - -device virtio-net-pci,netdev=mynet \ - -nographic \ - -no-reboot \ - 2>&1 | tee "$LOG_FILE" diff --git a/testing/scripts/entrypoint.sh b/testing/scripts/entrypoint.sh deleted file mode 100755 index f1f511ad..00000000 --- a/testing/scripts/entrypoint.sh +++ /dev/null @@ -1,112 +0,0 @@ -#!/bin/bash -set -e - -INPUT_IMAGE="/input/image.img" -WORK_DIR="/work" -IMAGE_FILE="${WORK_DIR}/distro.qcow2" -KERNEL="/base/kernel.img" -SSH_PORT=2222 -SSH_TIMEOUT="${SSH_TIMEOUT:-600}" -LOG_FILE="/tmp/qemu-serial.log" -HTTP_PORT="${HTTP_PORT:-9980}" - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -TEST_DIR="$(dirname "$SCRIPT_DIR")/tests" - -echo "============================================" -echo " OctoPi E2E Test" -echo "============================================" - -if [ ! -f "$INPUT_IMAGE" ]; then - echo "ERROR: No image found at $INPUT_IMAGE" - echo "Mount an OctoPi .img file with: -v /path/to/image.img:/input/image.img:ro" - exit 1 -fi - -if [ ! -f "$KERNEL" ]; then - echo "ERROR: No kernel found at $KERNEL" - exit 1 -fi - -cleanup() { - if [ -n "$QEMU_PID" ] && kill -0 "$QEMU_PID" 2>/dev/null; then - echo "Stopping QEMU (pid $QEMU_PID)..." - kill "$QEMU_PID" 2>/dev/null || true - wait "$QEMU_PID" 2>/dev/null || true - fi -} -trap cleanup EXIT - -echo "" -echo "--- Step 1: Prepare image ---" -"$SCRIPT_DIR/prepare-image.sh" "$INPUT_IMAGE" "$IMAGE_FILE" - -echo "" -echo "--- Step 2: Boot QEMU ---" -"$SCRIPT_DIR/boot-qemu.sh" "$IMAGE_FILE" "$KERNEL" "$SSH_PORT" "$LOG_FILE" "$HTTP_PORT" & -QEMU_PID=$! -echo "QEMU started (pid $QEMU_PID)" - -echo "" -echo "--- Step 3: Wait for SSH ---" -set +e -"$SCRIPT_DIR/wait-for-ssh.sh" localhost "$SSH_PORT" "$SSH_TIMEOUT" -SSH_WAIT_RC=$? -set -e -if [ "$SSH_WAIT_RC" -ne 0 ]; then - echo "SSH wait failed. QEMU log tail:" - tail -50 "$LOG_FILE" 2>/dev/null || true - if [ -n "$ARTIFACTS_DIR" ]; then - cp "$LOG_FILE" "$ARTIFACTS_DIR/qemu-boot.log" 2>/dev/null || true - echo "1" > "$ARTIFACTS_DIR/exit-code" - fi - exit 1 -fi - -echo "" -echo "--- Step 4: Run tests ---" -TEST_RESULT=0 -for test_script in "$TEST_DIR"/test_*.sh; do - if [ -x "$test_script" ]; then - echo "Running $(basename "$test_script")..." - if [ -n "$ARTIFACTS_DIR" ]; then - if "$test_script" localhost "$SSH_PORT" "$ARTIFACTS_DIR"; then - echo " -> PASSED" - else - echo " -> FAILED" - TEST_RESULT=1 - fi - else - if "$test_script" localhost "$SSH_PORT"; then - echo " -> PASSED" - else - echo " -> FAILED" - TEST_RESULT=1 - fi - fi - fi -done - -echo "" -echo "============================================" -if [ "$TEST_RESULT" -eq 0 ]; then - echo " ALL TESTS PASSED" -else - echo " SOME TESTS FAILED" -fi -echo "============================================" - -if [ -n "$ARTIFACTS_DIR" ]; then - echo "Collecting artifacts to $ARTIFACTS_DIR..." - cp "$LOG_FILE" "$ARTIFACTS_DIR/qemu-boot.log" 2>/dev/null || true - echo "$TEST_RESULT" > "$ARTIFACTS_DIR/exit-code" - echo "TEST_RESULT=$TEST_RESULT" > "$ARTIFACTS_DIR/test-results.txt" -fi - -if [ -n "$KEEP_ALIVE" ]; then - echo "Keeping container alive (KEEP_ALIVE set)..." - trap - EXIT - sleep infinity -else - exit "$TEST_RESULT" -fi diff --git a/testing/scripts/prepare-image.sh b/testing/scripts/prepare-image.sh deleted file mode 100755 index dde90806..00000000 --- a/testing/scripts/prepare-image.sh +++ /dev/null @@ -1,93 +0,0 @@ -#!/bin/bash -set -e -INPUT_IMAGE="${1:?Usage: $0 }" -OUTPUT_IMAGE="${2:?Usage: $0 }" -PIPASS=$(openssl passwd -6 raspberry) - -echo '=== Preparing image ===' -mkdir -p /work -echo 'Converting to qcow2...' -qemu-img convert -f raw -O qcow2 "$INPUT_IMAGE" "$OUTPUT_IMAGE" -echo 'Patching image (rootfs)...' -export LIBGUESTFS_BACKEND=direct -export LIBGUESTFS_DEBUG=0 -export LIBGUESTFS_TRACE=0 -guestfish -a "$OUTPUT_IMAGE" < /dev/tcp/"$HOST"/"$PORT") 2>/dev/null; then - RESULT=$(sshpass -p "$PASS" ssh $SSH_OPTS -p "$PORT" "${USER}@${HOST}" true 2>&1) - RC=$? - if [ "$RC" -eq 0 ]; then - echo "" - echo "SSH is ready (took ${ELAPSED}s)" - exit 0 - fi - if [ $(( ATTEMPT % 6 )) -eq 0 ]; then - echo "" - echo "[${ELAPSED}s] Port open, sshpass rc=$RC output: $RESULT" - echo "[${ELAPSED}s] Trying verbose SSH..." - sshpass -p "$PASS" ssh -v $SSH_OPTS -p "$PORT" "${USER}@${HOST}" true 2>&1 | tail -20 - else - printf "x" - fi - else - printf "." - fi - sleep 5 -done diff --git a/testing/tests/test_boot.sh b/testing/tests/test_boot.sh deleted file mode 100755 index ad4e624f..00000000 --- a/testing/tests/test_boot.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/bin/bash -set -e - -HOST="${1:-localhost}" -PORT="${2:-2222}" -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: SSH login and run 'echo hello world'" - -OUTPUT=$($SSH_CMD 'echo hello world' 2>/dev/null) - -if [ "$OUTPUT" = "hello world" ]; then - echo " Output: '$OUTPUT'" - echo " PASS: Got expected output" - exit 0 -else - echo " Expected: 'hello world'" - echo " Got: '$OUTPUT'" - echo " FAIL: Unexpected output" - exit 1 -fi From 8ea7d14beb851ee1a36b32b062972ee925321614 Mon Sep 17 00:00:00 2001 From: Guy Sheffer Date: Thu, 12 Mar 2026 14:05:12 +0200 Subject: [PATCH 07/22] Fix e2e CI: remove old puppeteer screenshot step and port forward The puppeteer-based screenshot was from the pre-framework approach and fails because the container stops the QEMU port forward after tests complete. Screenshots are now handled inside the container via the hooks/screenshot.sh mechanism. Also remove KEEP_ALIVE and increase the wait timeout to 25 minutes. --- .github/workflows/build.yml | 64 ++----------------------------------- 1 file changed, 2 insertions(+), 62 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index db71e165..d7a1a5fe 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -104,86 +104,26 @@ jobs: mkdir -p artifacts IMG=$(find image/ -name '*.img' | head -1) docker run -d --name octopi-test \ - -p 9980:9980 \ -v "$PWD/artifacts:/output" \ -v "$(realpath $IMG):/input/image.img:ro" \ -e ARTIFACTS_DIR=/output \ -e DISTRO_NAME="OctoPi" \ - -e KEEP_ALIVE=true \ octopi-e2e-test - name: Wait for tests to complete run: | - for i in $(seq 1 180); do + for i in $(seq 1 300); do [ -f artifacts/exit-code ] && break sleep 5 done if [ ! -f artifacts/exit-code ]; then - echo "ERROR: Tests did not complete within 15 minutes" + echo "ERROR: Tests did not complete within 25 minutes" docker logs octopi-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: Wait for OctoPrint to fully start - run: | - echo "Waiting for OctoPrint to finish startup..." - for i in $(seq 1 90); do - BODY=$(curl -4 -s --connect-timeout 5 http://127.0.0.1:9980 2>/dev/null || true) - if echo "$BODY" | grep -q "CONFIG_WIZARD"; then - echo "OctoPrint fully started (Setup Wizard ready)" - exit 0 - elif echo "$BODY" | grep -q "starting up"; then - printf "S" - else - printf "." - fi - sleep 5 - done - echo "" - echo "WARNING: OctoPrint may not be fully started yet, proceeding anyway" - - - name: Take OctoPrint screenshot - run: | - npm install puppeteer - node -e " - const puppeteer = require('puppeteer'); - (async () => { - const browser = await puppeteer.launch({ - headless: 'new', args: ['--no-sandbox'] - }); - const page = await browser.newPage(); - await page.setViewport({width: 1280, height: 900}); - - // Retry loading until OctoPrint finishes its startup phase - for (let attempt = 0; attempt < 30; attempt++) { - await page.goto('http://127.0.0.1:9980', { - waitUntil: 'networkidle2', timeout: 60000 - }); - const html = await page.content(); - if (html.includes('CONFIG_WIZARD') || html.includes('id=\"login\"')) break; - console.log('OctoPrint still starting up, retrying in 10s... (attempt ' + (attempt+1) + ')'); - await new Promise(r => setTimeout(r, 10000)); - } - - // Wait for the Setup Wizard dialog to appear - try { - await page.waitForSelector('#wizard_dialog', { visible: true, timeout: 120000 }); - } catch (e) { - console.log('Wizard did not appear, taking screenshot of current state'); - } - - // Dismiss notification popovers by clicking the wizard body - try { await page.click('#wizard_dialog .modal-body'); } catch(e) {} - await new Promise(r => setTimeout(r, 2000)); - - await page.screenshot({path: 'artifacts/octoprint-screenshot.png'}); - console.log('Screenshot captured'); - await browser.close(); - })(); - " - - name: Collect logs and stop container if: always() run: | From 45480fad5b6b7d53f5f8d36021ab1345e0668dbd Mon Sep 17 00:00:00 2001 From: Guy Sheffer Date: Wed, 25 Mar 2026 11:31:04 +0200 Subject: [PATCH 08/22] Fix e2e: wait for CONFIG_WIZARD before capturing OctoPrint artifacts The test and screenshot hook both matched "OctoPrint" in the starting page title instead of waiting for the actual wizard. Now both poll specifically for CONFIG_WIZARD in the response. The test saves HTML from the matching response (not a second curl). Add chromium to the Docker container for headless screenshots. Increase poll timeout to 600s for slower CI runners. --- testing/Dockerfile | 2 +- testing/hooks/screenshot.sh | 28 +++++++++++++++++++-------- testing/tests/test_octoprint_web.sh | 30 ++++++++++++++--------------- 3 files changed, 36 insertions(+), 24 deletions(-) diff --git a/testing/Dockerfile b/testing/Dockerfile index ab56a399..d71c2cd3 100644 --- a/testing/Dockerfile +++ b/testing/Dockerfile @@ -3,7 +3,7 @@ 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 \ + sshpass openssh-client curl socat imagemagick chromium \ && rm -rf /var/lib/apt/lists/* # Shared framework from CustomPiOS (copied into build context by CI) diff --git a/testing/hooks/screenshot.sh b/testing/hooks/screenshot.sh index b0d7c8d0..fb3e7b40 100755 --- a/testing/hooks/screenshot.sh +++ b/testing/hooks/screenshot.sh @@ -6,23 +6,35 @@ ARTIFACTS_DIR="${3:-/output}" SSH_CMD="sshpass -p raspberry ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o PreferredAuthentications=password -o PubkeyAuthentication=no -o LogLevel=ERROR -p $SSH_PORT ${SSH_HOST}" -echo "Capturing OctoPrint web UI artifacts..." +echo "Waiting for OctoPrint wizard page before capturing..." -# Save rendered HTML via curl through SSH (inside the guest, hitting localhost:80) -BODY=$(${SSH_CMD} -l pi "curl -s http://localhost" 2>/dev/null || echo "") +WIZARD_READY=0 +for i in $(seq 1 24); do + BODY=$(${SSH_CMD} -l pi "curl -s http://localhost" 2>/dev/null || echo "") + if echo "$BODY" | grep -q "CONFIG_WIZARD"; then + WIZARD_READY=1 + echo "$BODY" > "$ARTIFACTS_DIR/octoprint-ui.html" + echo " Saved OctoPrint wizard HTML to artifacts (after ${i}x5s)" + break + fi + printf "." + sleep 5 +done +echo "" -if [ -n "$BODY" ]; then - echo "$BODY" > "$ARTIFACTS_DIR/octoprint-ui.html" - echo " Saved OctoPrint HTML to artifacts" +if [ "$WIZARD_READY" -eq 0 ]; then + echo " WARNING: CONFIG_WIZARD not found after 120s, saving current page" + if [ -n "$BODY" ]; then + echo "$BODY" > "$ARTIFACTS_DIR/octoprint-ui.html" + fi fi -# Attempt headless screenshot from inside the container if a browser is available HTTP_PORT="${QEMU_HTTP_PORT:-8080}" for BROWSER in chromium chromium-browser google-chrome; do if command -v "$BROWSER" &>/dev/null; then echo " Using $BROWSER for headless screenshot..." "$BROWSER" --headless --disable-gpu --no-sandbox \ - --virtual-time-budget=10000 \ + --virtual-time-budget=15000 \ --screenshot="$ARTIFACTS_DIR/screenshot.png" \ --window-size=1280,720 \ "http://localhost:${HTTP_PORT}" 2>/dev/null || true diff --git a/testing/tests/test_octoprint_web.sh b/testing/tests/test_octoprint_web.sh index adcfa3d7..bc54fcc6 100755 --- a/testing/tests/test_octoprint_web.sh +++ b/testing/tests/test_octoprint_web.sh @@ -9,21 +9,21 @@ 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: OctoPrint web server is accessible and fully started" +echo "Test: OctoPrint web server is accessible with CONFIG_WIZARD" OCTOPRINT_READY=0 -for i in $(seq 1 60); do +for i in $(seq 1 120); do BODY=$($SSH_CMD "curl -s http://localhost" 2>/dev/null || echo "") HTTP_CODE=$($SSH_CMD "curl -s -o /dev/null -w '%{http_code}' http://localhost" 2>/dev/null || echo "000") if [ "$HTTP_CODE" = "200" ]; then - if echo "$BODY" | grep -q "starting up"; then - printf "S" - elif echo "$BODY" | grep -q "CONFIG_WIZARD\|OctoPrint"; then + if echo "$BODY" | grep -q "CONFIG_WIZARD"; then OCTOPRINT_READY=1 break + elif echo "$BODY" | grep -q "starting up\|still starting"; then + printf "S" else - printf "W" + printf "?" fi else printf "." @@ -33,23 +33,23 @@ done echo "" if [ "$OCTOPRINT_READY" -eq 0 ]; then - echo " FAIL: OctoPrint did not fully start within 300s" + echo " FAIL: OctoPrint CONFIG_WIZARD did not appear within 600s" + echo " Last HTTP code: $HTTP_CODE" + echo " Last body (first 200 chars): $(echo "$BODY" | head -c 200)" exit 1 fi -echo " OctoPrint web server is fully started (HTTP 200, UI loaded)" - -FULL_HTML=$($SSH_CMD "curl -s http://localhost" 2>/dev/null) +echo " OctoPrint CONFIG_WIZARD is loaded (HTTP 200)" if [ -n "$ARTIFACTS_DIR" ]; then - echo "$FULL_HTML" > "$ARTIFACTS_DIR/octoprint.html" - echo " Saved HTML to $ARTIFACTS_DIR/octoprint.html" + echo "$BODY" > "$ARTIFACTS_DIR/octoprint.html" + echo " Saved wizard HTML to $ARTIFACTS_DIR/octoprint.html" fi -if echo "$FULL_HTML" | grep -q "OctoPrint"; then - echo " PASS: OctoPrint web UI returned expected content" +if echo "$BODY" | grep -q "OctoPrint"; then + echo " PASS: OctoPrint wizard page returned expected content" exit 0 else - echo " FAIL: Response did not contain 'OctoPrint'" + echo " FAIL: Wizard page did not contain 'OctoPrint'" exit 1 fi From 68321d2d1ea768255e47ee84f362b9fbbc0b4e2e Mon Sep 17 00:00:00 2001 From: Guy Sheffer Date: Wed, 25 Mar 2026 14:38:53 +0200 Subject: [PATCH 09/22] Fix e2e: show headless Chromium errors, add missing deps for screenshot The headless screenshot silently failed (2>/dev/null hid the error). Now show stderr, use --headless=new, --disable-dev-shm-usage, and add missing shared libraries (libnss3, libgbm1, etc.) needed for Chromium headless in the container environment. --- testing/Dockerfile | 4 +++- testing/hooks/screenshot.sh | 27 ++++++++++++++++++++------- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/testing/Dockerfile b/testing/Dockerfile index d71c2cd3..8d5a8837 100644 --- a/testing/Dockerfile +++ b/testing/Dockerfile @@ -3,7 +3,9 @@ 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 chromium \ + sshpass openssh-client curl socat imagemagick \ + chromium libnss3 libatk1.0-0 libatk-bridge2.0-0 libgbm1 \ + libasound2t64 libxcomposite1 libxdamage1 libxrandr2 fonts-liberation \ && rm -rf /var/lib/apt/lists/* # Shared framework from CustomPiOS (copied into build context by CI) diff --git a/testing/hooks/screenshot.sh b/testing/hooks/screenshot.sh index fb3e7b40..a0a36f11 100755 --- a/testing/hooks/screenshot.sh +++ b/testing/hooks/screenshot.sh @@ -30,19 +30,32 @@ if [ "$WIZARD_READY" -eq 0 ]; then fi HTTP_PORT="${QEMU_HTTP_PORT:-8080}" -for BROWSER in chromium chromium-browser google-chrome; do - if command -v "$BROWSER" &>/dev/null; then - echo " Using $BROWSER for headless screenshot..." - "$BROWSER" --headless --disable-gpu --no-sandbox \ + +echo " Checking forwarded port accessibility..." +curl -s -o /dev/null -w " Port $HTTP_PORT -> HTTP %{http_code}\n" "http://localhost:${HTTP_PORT}" || echo " Port $HTTP_PORT not reachable" + +for BROWSER in chromium chromium-browser; do + BROWSER_PATH=$(command -v "$BROWSER" 2>/dev/null || true) + if [ -n "$BROWSER_PATH" ]; then + echo " Taking headless screenshot with $BROWSER_PATH ..." + "$BROWSER_PATH" \ + --headless=new \ + --no-sandbox \ + --disable-gpu \ + --disable-software-rasterizer \ + --disable-dev-shm-usage \ --virtual-time-budget=15000 \ --screenshot="$ARTIFACTS_DIR/screenshot.png" \ --window-size=1280,720 \ - "http://localhost:${HTTP_PORT}" 2>/dev/null || true + "http://localhost:${HTTP_PORT}" 2>&1 | tail -5 || true if [ -f "$ARTIFACTS_DIR/screenshot.png" ]; then - echo " Browser screenshot saved" + echo " Screenshot saved to $ARTIFACTS_DIR/screenshot.png" + ls -la "$ARTIFACTS_DIR/screenshot.png" exit 0 + else + echo " Screenshot file was NOT created by $BROWSER" fi fi done -echo " No headless browser available in container (HTML artifact saved instead)" +echo " No screenshot produced (headless browser may have failed)" From c2cc14fa071bc9bebd09fa3a4f5de8d6c2564bb0 Mon Sep 17 00:00:00 2001 From: Guy Sheffer Date: Wed, 25 Mar 2026 17:51:40 +0200 Subject: [PATCH 10/22] Fix e2e screenshot: skip snap stub, find real chromium binary The base image has /usr/bin/chromium-browser as a snap wrapper stub that prints "requires the chromium snap". Now search for the actual chromium binary at /usr/lib/chromium/chromium first, skipping any snap stubs. Add diagnostic output if no working binary is found. --- testing/hooks/screenshot.sh | 52 +++++++++++++++++++++---------------- 1 file changed, 30 insertions(+), 22 deletions(-) diff --git a/testing/hooks/screenshot.sh b/testing/hooks/screenshot.sh index a0a36f11..e5d397c2 100755 --- a/testing/hooks/screenshot.sh +++ b/testing/hooks/screenshot.sh @@ -34,28 +34,36 @@ HTTP_PORT="${QEMU_HTTP_PORT:-8080}" echo " Checking forwarded port accessibility..." curl -s -o /dev/null -w " Port $HTTP_PORT -> HTTP %{http_code}\n" "http://localhost:${HTTP_PORT}" || echo " Port $HTTP_PORT not reachable" -for BROWSER in chromium chromium-browser; do - BROWSER_PATH=$(command -v "$BROWSER" 2>/dev/null || true) - if [ -n "$BROWSER_PATH" ]; then - echo " Taking headless screenshot with $BROWSER_PATH ..." - "$BROWSER_PATH" \ - --headless=new \ - --no-sandbox \ - --disable-gpu \ - --disable-software-rasterizer \ - --disable-dev-shm-usage \ - --virtual-time-budget=15000 \ - --screenshot="$ARTIFACTS_DIR/screenshot.png" \ - --window-size=1280,720 \ - "http://localhost:${HTTP_PORT}" 2>&1 | tail -5 || true - if [ -f "$ARTIFACTS_DIR/screenshot.png" ]; then - echo " Screenshot saved to $ARTIFACTS_DIR/screenshot.png" - ls -la "$ARTIFACTS_DIR/screenshot.png" - exit 0 - else - echo " Screenshot file was NOT created by $BROWSER" - fi +# Find real chromium binary (skip snap stubs that print "requires the chromium snap") +BROWSER_PATH="" +for candidate in /usr/lib/chromium/chromium /usr/bin/chromium /snap/bin/chromium; do + if [ -x "$candidate" ] && ! "$candidate" --version 2>&1 | grep -q "snap"; then + BROWSER_PATH="$candidate" + break fi done -echo " No screenshot produced (headless browser may have failed)" +if [ -n "$BROWSER_PATH" ]; then + echo " Taking headless screenshot with $BROWSER_PATH ..." + "$BROWSER_PATH" \ + --headless=new \ + --no-sandbox \ + --disable-gpu \ + --disable-software-rasterizer \ + --disable-dev-shm-usage \ + --virtual-time-budget=15000 \ + --screenshot="$ARTIFACTS_DIR/screenshot.png" \ + --window-size=1280,720 \ + "http://localhost:${HTTP_PORT}" 2>&1 | tail -10 || true + if [ -f "$ARTIFACTS_DIR/screenshot.png" ]; then + echo " Screenshot saved to $ARTIFACTS_DIR/screenshot.png" + ls -la "$ARTIFACTS_DIR/screenshot.png" + else + echo " Screenshot file was NOT created" + fi +else + echo " No working chromium binary found. Checking available:" + ls -la /usr/bin/chromium* /usr/lib/chromium/chromium 2>&1 || true + echo " dpkg -l chromium:" + dpkg -l chromium 2>&1 | tail -3 || true +fi From 0fb920045c0e40385077a491ea2ea90363a8508f Mon Sep 17 00:00:00 2001 From: Guy Sheffer Date: Thu, 26 Mar 2026 23:52:21 +0200 Subject: [PATCH 11/22] Fix e2e screenshot: use google-chrome-stable, add wizard validation The Ubuntu-based ptrsr/pi-ci image only has a snap stub for chromium. Install google-chrome-stable from Google's apt repo instead, which provides a real headless browser binary. The test now takes a screenshot right when CONFIG_WIZARD is confirmed and validates the PNG is >10KB (a real rendered page, not blank). The screenshot hook also uses google-chrome-stable as a redundant capture. --- testing/Dockerfile | 7 +++--- testing/hooks/screenshot.sh | 33 ++++++----------------------- testing/tests/test_octoprint_web.sh | 29 +++++++++++++++++++++---- 3 files changed, 36 insertions(+), 33 deletions(-) diff --git a/testing/Dockerfile b/testing/Dockerfile index 8d5a8837..f1815965 100644 --- a/testing/Dockerfile +++ b/testing/Dockerfile @@ -3,9 +3,10 @@ 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 \ - chromium libnss3 libatk1.0-0 libatk-bridge2.0-0 libgbm1 \ - libasound2t64 libxcomposite1 libxdamage1 libxrandr2 fonts-liberation \ + sshpass openssh-client curl socat imagemagick wget gnupg \ + && wget -q -O - https://dl.google.com/linux/linux_signing_key.pub | gpg --dearmor -o /usr/share/keyrings/google-chrome.gpg \ + && echo "deb [arch=amd64 signed-by=/usr/share/keyrings/google-chrome.gpg] http://dl.google.com/linux/chrome/deb/ stable main" > /etc/apt/sources.list.d/google-chrome.list \ + && apt-get update && apt-get install -y --no-install-recommends google-chrome-stable \ && rm -rf /var/lib/apt/lists/* # Shared framework from CustomPiOS (copied into build context by CI) diff --git a/testing/hooks/screenshot.sh b/testing/hooks/screenshot.sh index e5d397c2..10c23206 100755 --- a/testing/hooks/screenshot.sh +++ b/testing/hooks/screenshot.sh @@ -30,40 +30,21 @@ if [ "$WIZARD_READY" -eq 0 ]; then fi HTTP_PORT="${QEMU_HTTP_PORT:-8080}" - -echo " Checking forwarded port accessibility..." -curl -s -o /dev/null -w " Port $HTTP_PORT -> HTTP %{http_code}\n" "http://localhost:${HTTP_PORT}" || echo " Port $HTTP_PORT not reachable" - -# Find real chromium binary (skip snap stubs that print "requires the chromium snap") -BROWSER_PATH="" -for candidate in /usr/lib/chromium/chromium /usr/bin/chromium /snap/bin/chromium; do - if [ -x "$candidate" ] && ! "$candidate" --version 2>&1 | grep -q "snap"; then - BROWSER_PATH="$candidate" - break - fi -done +BROWSER_PATH=$(command -v google-chrome-stable 2>/dev/null || true) if [ -n "$BROWSER_PATH" ]; then echo " Taking headless screenshot with $BROWSER_PATH ..." - "$BROWSER_PATH" \ - --headless=new \ - --no-sandbox \ - --disable-gpu \ - --disable-software-rasterizer \ - --disable-dev-shm-usage \ - --virtual-time-budget=15000 \ + "$BROWSER_PATH" --headless=new --no-sandbox --disable-gpu \ + --disable-dev-shm-usage --virtual-time-budget=15000 \ --screenshot="$ARTIFACTS_DIR/screenshot.png" \ --window-size=1280,720 \ - "http://localhost:${HTTP_PORT}" 2>&1 | tail -10 || true + "http://localhost:${HTTP_PORT}" 2>&1 | tail -5 || true if [ -f "$ARTIFACTS_DIR/screenshot.png" ]; then - echo " Screenshot saved to $ARTIFACTS_DIR/screenshot.png" - ls -la "$ARTIFACTS_DIR/screenshot.png" + SIZE=$(stat -c%s "$ARTIFACTS_DIR/screenshot.png" 2>/dev/null || echo "0") + echo " Screenshot saved (${SIZE} bytes)" else echo " Screenshot file was NOT created" fi else - echo " No working chromium binary found. Checking available:" - ls -la /usr/bin/chromium* /usr/lib/chromium/chromium 2>&1 || true - echo " dpkg -l chromium:" - dpkg -l chromium 2>&1 | tail -3 || true + echo " No google-chrome-stable found in container" fi diff --git a/testing/tests/test_octoprint_web.sh b/testing/tests/test_octoprint_web.sh index bc54fcc6..b9435847 100755 --- a/testing/tests/test_octoprint_web.sh +++ b/testing/tests/test_octoprint_web.sh @@ -46,10 +46,31 @@ if [ -n "$ARTIFACTS_DIR" ]; then echo " Saved wizard HTML to $ARTIFACTS_DIR/octoprint.html" fi -if echo "$BODY" | grep -q "OctoPrint"; then - echo " PASS: OctoPrint wizard page returned expected content" - exit 0 -else +if ! echo "$BODY" | grep -q "OctoPrint"; then echo " FAIL: Wizard page did not contain 'OctoPrint'" exit 1 fi + +HTTP_PORT="${QEMU_HTTP_PORT:-8080}" +BROWSER=$(command -v google-chrome-stable 2>/dev/null || true) +if [ -n "$BROWSER" ] && [ -n "$ARTIFACTS_DIR" ]; then + echo " Taking wizard screenshot via headless Chrome..." + "$BROWSER" --headless=new --no-sandbox --disable-gpu \ + --disable-dev-shm-usage --virtual-time-budget=15000 \ + --screenshot="$ARTIFACTS_DIR/screenshot.png" \ + --window-size=1280,720 \ + "http://localhost:${HTTP_PORT}" 2>/dev/null || true + if [ -f "$ARTIFACTS_DIR/screenshot.png" ]; then + SIZE=$(stat -c%s "$ARTIFACTS_DIR/screenshot.png" 2>/dev/null || echo "0") + if [ "$SIZE" -gt 10000 ]; then + echo " Screenshot saved (${SIZE} bytes) -- wizard page captured" + else + echo " WARNING: Screenshot is only ${SIZE} bytes (may be blank)" + fi + else + echo " WARNING: Screenshot was not created" + fi +fi + +echo " PASS: OctoPrint wizard page verified" +exit 0 From 91c0b83d8d2878c7e69c386433bc8605121a5215 Mon Sep 17 00:00:00 2001 From: Guy Sheffer Date: Mon, 30 Mar 2026 16:34:24 +0300 Subject: [PATCH 12/22] Fix e2e timeout: add 30s kill timer to Chrome, increase wait to 35min Chrome headless can hang indefinitely if the page doesn't load. Wrap all Chrome calls with `timeout 30` to kill after 30 seconds. Increase the workflow wait loop from 25 to 35 min and job timeout from 30 to 45 min, since OctoPrint can take 10+ min to start on slower CI runners. --- .github/workflows/build.yml | 6 +++--- testing/hooks/screenshot.sh | 2 +- testing/tests/test_octoprint_web.sh | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d7a1a5fe..51d9ea68 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -73,7 +73,7 @@ jobs: e2e-test: needs: build runs-on: ubuntu-latest - timeout-minutes: 30 + timeout-minutes: 45 steps: - uses: actions/checkout@v4 @@ -112,12 +112,12 @@ jobs: - name: Wait for tests to complete run: | - for i in $(seq 1 300); do + for i in $(seq 1 420); do [ -f artifacts/exit-code ] && break sleep 5 done if [ ! -f artifacts/exit-code ]; then - echo "ERROR: Tests did not complete within 25 minutes" + echo "ERROR: Tests did not complete within 35 minutes" docker logs octopi-test 2>&1 | tail -80 exit 1 fi diff --git a/testing/hooks/screenshot.sh b/testing/hooks/screenshot.sh index 10c23206..e0dd2247 100755 --- a/testing/hooks/screenshot.sh +++ b/testing/hooks/screenshot.sh @@ -34,7 +34,7 @@ BROWSER_PATH=$(command -v google-chrome-stable 2>/dev/null || true) if [ -n "$BROWSER_PATH" ]; then echo " Taking headless screenshot with $BROWSER_PATH ..." - "$BROWSER_PATH" --headless=new --no-sandbox --disable-gpu \ + timeout 30 "$BROWSER_PATH" --headless=new --no-sandbox --disable-gpu \ --disable-dev-shm-usage --virtual-time-budget=15000 \ --screenshot="$ARTIFACTS_DIR/screenshot.png" \ --window-size=1280,720 \ diff --git a/testing/tests/test_octoprint_web.sh b/testing/tests/test_octoprint_web.sh index b9435847..7fec1a26 100755 --- a/testing/tests/test_octoprint_web.sh +++ b/testing/tests/test_octoprint_web.sh @@ -55,7 +55,7 @@ HTTP_PORT="${QEMU_HTTP_PORT:-8080}" BROWSER=$(command -v google-chrome-stable 2>/dev/null || true) if [ -n "$BROWSER" ] && [ -n "$ARTIFACTS_DIR" ]; then echo " Taking wizard screenshot via headless Chrome..." - "$BROWSER" --headless=new --no-sandbox --disable-gpu \ + timeout 30 "$BROWSER" --headless=new --no-sandbox --disable-gpu \ --disable-dev-shm-usage --virtual-time-budget=15000 \ --screenshot="$ARTIFACTS_DIR/screenshot.png" \ --window-size=1280,720 \ From 4ce3dfdf0222bc0a5313fce487435f1aa5d7b98b Mon Sep 17 00:00:00 2001 From: Guy Sheffer Date: Tue, 31 Mar 2026 22:19:35 +0300 Subject: [PATCH 13/22] Add chromium fallback browser for e2e screenshot capture Install chromium in the Docker container alongside google-chrome-stable. Update screenshot.sh to try google-chrome-stable, chromium, and chromium-browser in order, with screenshot size validation. --- testing/Dockerfile | 2 +- testing/hooks/screenshot.sh | 29 +++++++++++++++++++++-------- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/testing/Dockerfile b/testing/Dockerfile index f1815965..aaaf9baf 100644 --- a/testing/Dockerfile +++ b/testing/Dockerfile @@ -3,7 +3,7 @@ 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 wget gnupg \ + sshpass openssh-client curl socat imagemagick wget gnupg chromium \ && wget -q -O - https://dl.google.com/linux/linux_signing_key.pub | gpg --dearmor -o /usr/share/keyrings/google-chrome.gpg \ && echo "deb [arch=amd64 signed-by=/usr/share/keyrings/google-chrome.gpg] http://dl.google.com/linux/chrome/deb/ stable main" > /etc/apt/sources.list.d/google-chrome.list \ && apt-get update && apt-get install -y --no-install-recommends google-chrome-stable \ diff --git a/testing/hooks/screenshot.sh b/testing/hooks/screenshot.sh index e0dd2247..ff93de83 100755 --- a/testing/hooks/screenshot.sh +++ b/testing/hooks/screenshot.sh @@ -30,21 +30,34 @@ if [ "$WIZARD_READY" -eq 0 ]; then fi HTTP_PORT="${QEMU_HTTP_PORT:-8080}" -BROWSER_PATH=$(command -v google-chrome-stable 2>/dev/null || true) +SCREENSHOT_TAKEN=0 + +for BROWSER in google-chrome-stable chromium chromium-browser; do + BROWSER_PATH=$(command -v "$BROWSER" 2>/dev/null || true) + [ -n "$BROWSER_PATH" ] || continue -if [ -n "$BROWSER_PATH" ]; then echo " Taking headless screenshot with $BROWSER_PATH ..." - timeout 30 "$BROWSER_PATH" --headless=new --no-sandbox --disable-gpu \ + timeout 30 "$BROWSER_PATH" --headless --no-sandbox --disable-gpu \ --disable-dev-shm-usage --virtual-time-budget=15000 \ --screenshot="$ARTIFACTS_DIR/screenshot.png" \ --window-size=1280,720 \ - "http://localhost:${HTTP_PORT}" 2>&1 | tail -5 || true + "http://localhost:${HTTP_PORT}" 2>/dev/null || true + if [ -f "$ARTIFACTS_DIR/screenshot.png" ]; then SIZE=$(stat -c%s "$ARTIFACTS_DIR/screenshot.png" 2>/dev/null || echo "0") - echo " Screenshot saved (${SIZE} bytes)" + if [ "$SIZE" -gt 10000 ]; then + echo " Screenshot saved (${SIZE} bytes) -- wizard page captured" + SCREENSHOT_TAKEN=1 + break + else + echo " WARNING: Screenshot is only ${SIZE} bytes (may be blank), trying next browser" + rm -f "$ARTIFACTS_DIR/screenshot.png" + fi else - echo " Screenshot file was NOT created" + echo " Screenshot file was NOT created by $BROWSER, trying next browser" fi -else - echo " No google-chrome-stable found in container" +done + +if [ "$SCREENSHOT_TAKEN" -eq 0 ]; then + echo " WARNING: No browser produced a valid screenshot" fi From 3ca0db4045a54b8cc75f5f2a4c765be1e3ef08c1 Mon Sep 17 00:00:00 2001 From: Guy Sheffer Date: Sat, 4 Apr 2026 22:22:59 +0300 Subject: [PATCH 14/22] Fix e2e screenshot: capture browser stderr, add container-friendly flags Both headless browsers silently fail to produce screenshot.png because stderr is suppressed with 2>/dev/null. Redirect stderr to a log file (browser-screenshot.log) included in artifacts for diagnosis. Also add --headless=new (consistent), --single-process, --disable-software-rasterizer, --disable-extensions, and --disable-background-networking for better container compatibility. --- testing/hooks/screenshot.sh | 12 +++++++++--- testing/tests/test_octoprint_web.sh | 10 ++++++++-- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/testing/hooks/screenshot.sh b/testing/hooks/screenshot.sh index ff93de83..f9953f04 100755 --- a/testing/hooks/screenshot.sh +++ b/testing/hooks/screenshot.sh @@ -37,11 +37,13 @@ for BROWSER in google-chrome-stable chromium chromium-browser; do [ -n "$BROWSER_PATH" ] || continue echo " Taking headless screenshot with $BROWSER_PATH ..." - timeout 30 "$BROWSER_PATH" --headless --no-sandbox --disable-gpu \ - --disable-dev-shm-usage --virtual-time-budget=15000 \ + timeout 30 "$BROWSER_PATH" --headless=new --no-sandbox --disable-gpu \ + --disable-dev-shm-usage --disable-software-rasterizer \ + --disable-extensions --disable-background-networking \ + --single-process --virtual-time-budget=15000 \ --screenshot="$ARTIFACTS_DIR/screenshot.png" \ --window-size=1280,720 \ - "http://localhost:${HTTP_PORT}" 2>/dev/null || true + "http://localhost:${HTTP_PORT}" 2>"$ARTIFACTS_DIR/browser-screenshot.log" || true if [ -f "$ARTIFACTS_DIR/screenshot.png" ]; then SIZE=$(stat -c%s "$ARTIFACTS_DIR/screenshot.png" 2>/dev/null || echo "0") @@ -51,10 +53,14 @@ for BROWSER in google-chrome-stable chromium chromium-browser; do break else echo " WARNING: Screenshot is only ${SIZE} bytes (may be blank), trying next browser" + echo " Browser stderr:" + cat "$ARTIFACTS_DIR/browser-screenshot.log" 2>/dev/null | head -20 || true rm -f "$ARTIFACTS_DIR/screenshot.png" fi else echo " Screenshot file was NOT created by $BROWSER, trying next browser" + echo " Browser stderr:" + cat "$ARTIFACTS_DIR/browser-screenshot.log" 2>/dev/null | head -20 || true fi done diff --git a/testing/tests/test_octoprint_web.sh b/testing/tests/test_octoprint_web.sh index 7fec1a26..2afca9c3 100755 --- a/testing/tests/test_octoprint_web.sh +++ b/testing/tests/test_octoprint_web.sh @@ -56,19 +56,25 @@ BROWSER=$(command -v google-chrome-stable 2>/dev/null || true) if [ -n "$BROWSER" ] && [ -n "$ARTIFACTS_DIR" ]; then echo " Taking wizard screenshot via headless Chrome..." timeout 30 "$BROWSER" --headless=new --no-sandbox --disable-gpu \ - --disable-dev-shm-usage --virtual-time-budget=15000 \ + --disable-dev-shm-usage --disable-software-rasterizer \ + --disable-extensions --disable-background-networking \ + --single-process --virtual-time-budget=15000 \ --screenshot="$ARTIFACTS_DIR/screenshot.png" \ --window-size=1280,720 \ - "http://localhost:${HTTP_PORT}" 2>/dev/null || true + "http://localhost:${HTTP_PORT}" 2>"$ARTIFACTS_DIR/browser-screenshot.log" || true if [ -f "$ARTIFACTS_DIR/screenshot.png" ]; then SIZE=$(stat -c%s "$ARTIFACTS_DIR/screenshot.png" 2>/dev/null || echo "0") if [ "$SIZE" -gt 10000 ]; then echo " Screenshot saved (${SIZE} bytes) -- wizard page captured" else echo " WARNING: Screenshot is only ${SIZE} bytes (may be blank)" + echo " Browser stderr:" + cat "$ARTIFACTS_DIR/browser-screenshot.log" 2>/dev/null | head -20 || true fi else echo " WARNING: Screenshot was not created" + echo " Browser stderr:" + cat "$ARTIFACTS_DIR/browser-screenshot.log" 2>/dev/null | head -20 || true fi fi From 5549c1daae7cd2f2e244ba31af3f1d81af46d1d7 Mon Sep 17 00:00:00 2001 From: Guy Sheffer Date: Sun, 5 Apr 2026 10:38:11 +0300 Subject: [PATCH 15/22] Fix e2e screenshot: add dbus, skip snap stubs, drop --single-process Root cause from browser-screenshot.log: - google-chrome-stable fails with "Failed to connect to the bus" (no dbus) - chromium-browser is a snap stub, not a real binary Fixes: - Install dbus in Dockerfile and start dbus-daemon before headless browser - Detect and skip snap stub browsers in screenshot.sh - Remove --single-process flag which can interfere with screenshot pipeline --- testing/Dockerfile | 2 +- testing/hooks/screenshot.sh | 12 +++++++++++- testing/tests/test_octoprint_web.sh | 7 ++++++- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/testing/Dockerfile b/testing/Dockerfile index aaaf9baf..8bcf0272 100644 --- a/testing/Dockerfile +++ b/testing/Dockerfile @@ -3,7 +3,7 @@ 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 wget gnupg chromium \ + sshpass openssh-client curl socat imagemagick wget gnupg dbus chromium \ && wget -q -O - https://dl.google.com/linux/linux_signing_key.pub | gpg --dearmor -o /usr/share/keyrings/google-chrome.gpg \ && echo "deb [arch=amd64 signed-by=/usr/share/keyrings/google-chrome.gpg] http://dl.google.com/linux/chrome/deb/ stable main" > /etc/apt/sources.list.d/google-chrome.list \ && apt-get update && apt-get install -y --no-install-recommends google-chrome-stable \ diff --git a/testing/hooks/screenshot.sh b/testing/hooks/screenshot.sh index f9953f04..e6682d79 100755 --- a/testing/hooks/screenshot.sh +++ b/testing/hooks/screenshot.sh @@ -32,15 +32,25 @@ fi HTTP_PORT="${QEMU_HTTP_PORT:-8080}" SCREENSHOT_TAKEN=0 +if command -v dbus-daemon &>/dev/null && [ ! -S /run/dbus/system_bus_socket ]; then + mkdir -p /run/dbus + dbus-daemon --system --fork 2>/dev/null || true +fi + for BROWSER in google-chrome-stable chromium chromium-browser; do BROWSER_PATH=$(command -v "$BROWSER" 2>/dev/null || true) [ -n "$BROWSER_PATH" ] || continue + if "$BROWSER_PATH" --version 2>&1 | grep -qi "snap"; then + echo " Skipping $BROWSER_PATH (snap stub)" + continue + fi + echo " Taking headless screenshot with $BROWSER_PATH ..." timeout 30 "$BROWSER_PATH" --headless=new --no-sandbox --disable-gpu \ --disable-dev-shm-usage --disable-software-rasterizer \ --disable-extensions --disable-background-networking \ - --single-process --virtual-time-budget=15000 \ + --virtual-time-budget=15000 \ --screenshot="$ARTIFACTS_DIR/screenshot.png" \ --window-size=1280,720 \ "http://localhost:${HTTP_PORT}" 2>"$ARTIFACTS_DIR/browser-screenshot.log" || true diff --git a/testing/tests/test_octoprint_web.sh b/testing/tests/test_octoprint_web.sh index 2afca9c3..7689217a 100755 --- a/testing/tests/test_octoprint_web.sh +++ b/testing/tests/test_octoprint_web.sh @@ -54,11 +54,16 @@ fi HTTP_PORT="${QEMU_HTTP_PORT:-8080}" BROWSER=$(command -v google-chrome-stable 2>/dev/null || true) if [ -n "$BROWSER" ] && [ -n "$ARTIFACTS_DIR" ]; then + if command -v dbus-daemon &>/dev/null && [ ! -S /run/dbus/system_bus_socket ]; then + mkdir -p /run/dbus + dbus-daemon --system --fork 2>/dev/null || true + fi + echo " Taking wizard screenshot via headless Chrome..." timeout 30 "$BROWSER" --headless=new --no-sandbox --disable-gpu \ --disable-dev-shm-usage --disable-software-rasterizer \ --disable-extensions --disable-background-networking \ - --single-process --virtual-time-budget=15000 \ + --virtual-time-budget=15000 \ --screenshot="$ARTIFACTS_DIR/screenshot.png" \ --window-size=1280,720 \ "http://localhost:${HTTP_PORT}" 2>"$ARTIFACTS_DIR/browser-screenshot.log" || true From d5c0cea87df1790f816b939b0151de398f19ac7f Mon Sep 17 00:00:00 2001 From: Guy Sheffer Date: Sun, 5 Apr 2026 13:46:00 +0300 Subject: [PATCH 16/22] Fix e2e screenshot: use dbus-run-session, add fonts-liberation Chrome needs a session D-Bus (not system), previous dbus-daemon --system produced "Could not parse server address" errors. Use dbus-run-session to wrap Chrome with a proper session bus. Also add dbus-x11 (provides dbus-run-session) and fonts-liberation (required for Chrome headless rendering) to the Dockerfile. Drop the chromium apt package (snap stub on Ubuntu, not a real browser). --- testing/Dockerfile | 3 ++- testing/hooks/screenshot.sh | 8 ++++---- testing/tests/test_octoprint_web.sh | 8 ++++---- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/testing/Dockerfile b/testing/Dockerfile index 8bcf0272..ea0aa0a1 100644 --- a/testing/Dockerfile +++ b/testing/Dockerfile @@ -3,7 +3,8 @@ 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 wget gnupg dbus chromium \ + sshpass openssh-client curl socat imagemagick wget gnupg \ + dbus dbus-x11 fonts-liberation \ && wget -q -O - https://dl.google.com/linux/linux_signing_key.pub | gpg --dearmor -o /usr/share/keyrings/google-chrome.gpg \ && echo "deb [arch=amd64 signed-by=/usr/share/keyrings/google-chrome.gpg] http://dl.google.com/linux/chrome/deb/ stable main" > /etc/apt/sources.list.d/google-chrome.list \ && apt-get update && apt-get install -y --no-install-recommends google-chrome-stable \ diff --git a/testing/hooks/screenshot.sh b/testing/hooks/screenshot.sh index e6682d79..a08cb66f 100755 --- a/testing/hooks/screenshot.sh +++ b/testing/hooks/screenshot.sh @@ -32,9 +32,9 @@ fi HTTP_PORT="${QEMU_HTTP_PORT:-8080}" SCREENSHOT_TAKEN=0 -if command -v dbus-daemon &>/dev/null && [ ! -S /run/dbus/system_bus_socket ]; then - mkdir -p /run/dbus - dbus-daemon --system --fork 2>/dev/null || true +DBUS_PREFIX="" +if command -v dbus-run-session &>/dev/null; then + DBUS_PREFIX="dbus-run-session --" fi for BROWSER in google-chrome-stable chromium chromium-browser; do @@ -47,7 +47,7 @@ for BROWSER in google-chrome-stable chromium chromium-browser; do fi echo " Taking headless screenshot with $BROWSER_PATH ..." - timeout 30 "$BROWSER_PATH" --headless=new --no-sandbox --disable-gpu \ + timeout 30 $DBUS_PREFIX "$BROWSER_PATH" --headless=new --no-sandbox --disable-gpu \ --disable-dev-shm-usage --disable-software-rasterizer \ --disable-extensions --disable-background-networking \ --virtual-time-budget=15000 \ diff --git a/testing/tests/test_octoprint_web.sh b/testing/tests/test_octoprint_web.sh index 7689217a..b6751a1f 100755 --- a/testing/tests/test_octoprint_web.sh +++ b/testing/tests/test_octoprint_web.sh @@ -54,13 +54,13 @@ fi HTTP_PORT="${QEMU_HTTP_PORT:-8080}" BROWSER=$(command -v google-chrome-stable 2>/dev/null || true) if [ -n "$BROWSER" ] && [ -n "$ARTIFACTS_DIR" ]; then - if command -v dbus-daemon &>/dev/null && [ ! -S /run/dbus/system_bus_socket ]; then - mkdir -p /run/dbus - dbus-daemon --system --fork 2>/dev/null || true + DBUS_PREFIX="" + if command -v dbus-run-session &>/dev/null; then + DBUS_PREFIX="dbus-run-session --" fi echo " Taking wizard screenshot via headless Chrome..." - timeout 30 "$BROWSER" --headless=new --no-sandbox --disable-gpu \ + timeout 30 $DBUS_PREFIX "$BROWSER" --headless=new --no-sandbox --disable-gpu \ --disable-dev-shm-usage --disable-software-rasterizer \ --disable-extensions --disable-background-networking \ --virtual-time-budget=15000 \ From 63e82b23e3f0bf8006e70d35f9ddc3cce9636fdd Mon Sep 17 00:00:00 2001 From: Guy Sheffer Date: Sun, 5 Apr 2026 16:48:50 +0300 Subject: [PATCH 17/22] Fix e2e screenshot: use legacy --headless mode, write to /tmp first --headless=new has known bugs in Docker containers causing screenshot failures (Chrome starts but never writes the file). Switch to legacy --headless mode which is stable for CLI --screenshot. Other changes: - Write screenshot to /tmp first, then copy to artifacts (rules out volume mount issues) - Add HTTP connectivity check before launching Chrome - Log Chrome version for easier debugging - Increase timeout from 30s to 45s - Add --disable-setuid-sandbox and --hide-scrollbars - Remove --virtual-time-budget (can cause issues in old headless mode) --- testing/hooks/screenshot.sh | 25 +++++++++++++++---------- testing/tests/test_octoprint_web.sh | 15 ++++++++------- 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/testing/hooks/screenshot.sh b/testing/hooks/screenshot.sh index a08cb66f..2e5ca79c 100755 --- a/testing/hooks/screenshot.sh +++ b/testing/hooks/screenshot.sh @@ -32,6 +32,10 @@ fi HTTP_PORT="${QEMU_HTTP_PORT:-8080}" SCREENSHOT_TAKEN=0 +echo " Verifying localhost:${HTTP_PORT} is reachable from container..." +HTTP_CHECK=$(curl -s -o /dev/null -w '%{http_code}' "http://localhost:${HTTP_PORT}" 2>/dev/null || echo "000") +echo " HTTP status from container: ${HTTP_CHECK}" + DBUS_PREFIX="" if command -v dbus-run-session &>/dev/null; then DBUS_PREFIX="dbus-run-session --" @@ -46,26 +50,27 @@ for BROWSER in google-chrome-stable chromium chromium-browser; do continue fi - echo " Taking headless screenshot with $BROWSER_PATH ..." - timeout 30 $DBUS_PREFIX "$BROWSER_PATH" --headless=new --no-sandbox --disable-gpu \ - --disable-dev-shm-usage --disable-software-rasterizer \ - --disable-extensions --disable-background-networking \ - --virtual-time-budget=15000 \ - --screenshot="$ARTIFACTS_DIR/screenshot.png" \ + echo " Taking headless screenshot with $BROWSER_PATH ($(${BROWSER_PATH} --version 2>/dev/null || echo 'unknown version'))..." + timeout 45 $DBUS_PREFIX "$BROWSER_PATH" --headless --no-sandbox --disable-gpu \ + --disable-dev-shm-usage --disable-setuid-sandbox \ + --disable-software-rasterizer --hide-scrollbars \ + --screenshot="/tmp/screenshot.png" \ --window-size=1280,720 \ "http://localhost:${HTTP_PORT}" 2>"$ARTIFACTS_DIR/browser-screenshot.log" || true - if [ -f "$ARTIFACTS_DIR/screenshot.png" ]; then - SIZE=$(stat -c%s "$ARTIFACTS_DIR/screenshot.png" 2>/dev/null || echo "0") + if [ -f /tmp/screenshot.png ]; then + SIZE=$(stat -c%s /tmp/screenshot.png 2>/dev/null || echo "0") if [ "$SIZE" -gt 10000 ]; then - echo " Screenshot saved (${SIZE} bytes) -- wizard page captured" + cp /tmp/screenshot.png "$ARTIFACTS_DIR/screenshot.png" + echo " Screenshot saved (${SIZE} bytes)" SCREENSHOT_TAKEN=1 + rm -f /tmp/screenshot.png break else echo " WARNING: Screenshot is only ${SIZE} bytes (may be blank), trying next browser" echo " Browser stderr:" cat "$ARTIFACTS_DIR/browser-screenshot.log" 2>/dev/null | head -20 || true - rm -f "$ARTIFACTS_DIR/screenshot.png" + rm -f /tmp/screenshot.png fi else echo " Screenshot file was NOT created by $BROWSER, trying next browser" diff --git a/testing/tests/test_octoprint_web.sh b/testing/tests/test_octoprint_web.sh index b6751a1f..4ce45ff3 100755 --- a/testing/tests/test_octoprint_web.sh +++ b/testing/tests/test_octoprint_web.sh @@ -60,22 +60,23 @@ if [ -n "$BROWSER" ] && [ -n "$ARTIFACTS_DIR" ]; then fi echo " Taking wizard screenshot via headless Chrome..." - timeout 30 $DBUS_PREFIX "$BROWSER" --headless=new --no-sandbox --disable-gpu \ - --disable-dev-shm-usage --disable-software-rasterizer \ - --disable-extensions --disable-background-networking \ - --virtual-time-budget=15000 \ - --screenshot="$ARTIFACTS_DIR/screenshot.png" \ + timeout 45 $DBUS_PREFIX "$BROWSER" --headless --no-sandbox --disable-gpu \ + --disable-dev-shm-usage --disable-setuid-sandbox \ + --disable-software-rasterizer --hide-scrollbars \ + --screenshot="/tmp/screenshot.png" \ --window-size=1280,720 \ "http://localhost:${HTTP_PORT}" 2>"$ARTIFACTS_DIR/browser-screenshot.log" || true - if [ -f "$ARTIFACTS_DIR/screenshot.png" ]; then - SIZE=$(stat -c%s "$ARTIFACTS_DIR/screenshot.png" 2>/dev/null || echo "0") + if [ -f /tmp/screenshot.png ]; then + SIZE=$(stat -c%s /tmp/screenshot.png 2>/dev/null || echo "0") if [ "$SIZE" -gt 10000 ]; then + cp /tmp/screenshot.png "$ARTIFACTS_DIR/screenshot.png" echo " Screenshot saved (${SIZE} bytes) -- wizard page captured" else echo " WARNING: Screenshot is only ${SIZE} bytes (may be blank)" echo " Browser stderr:" cat "$ARTIFACTS_DIR/browser-screenshot.log" 2>/dev/null | head -20 || true fi + rm -f /tmp/screenshot.png else echo " WARNING: Screenshot was not created" echo " Browser stderr:" From 4703512bcf3d10e9c7f87620cc0393e01666f79a Mon Sep 17 00:00:00 2001 From: Guy Sheffer Date: Sun, 5 Apr 2026 22:34:37 +0300 Subject: [PATCH 18/22] Add OCR-based retry loop to wait for wizard in screenshot The screenshot was capturing "Connecting to OctoPrint's server..." because Chrome rendered before the JS frontend finished its websocket handshake. Now both screenshot scripts retry up to 12 times (120s), running tesseract OCR on each capture to detect wizard keywords (wizard, access control, setup) before accepting the screenshot. Changes: - Add tesseract-ocr to Dockerfile - Wrap screenshot capture in OCR retry loop in both scripts - Save OCR text to screenshot-ocr.txt artifact for debugging - Keep last screenshot as fallback if wizard never detected --- testing/Dockerfile | 2 +- testing/hooks/screenshot.sh | 92 +++++++++++++++++++---------- testing/tests/test_octoprint_web.sh | 69 ++++++++++++++++------ 3 files changed, 113 insertions(+), 50 deletions(-) diff --git a/testing/Dockerfile b/testing/Dockerfile index ea0aa0a1..380aca62 100644 --- a/testing/Dockerfile +++ b/testing/Dockerfile @@ -4,7 +4,7 @@ ENV LIBGUESTFS_BACKEND=direct RUN apt-get update && apt-get install -y --no-install-recommends \ sshpass openssh-client curl socat imagemagick wget gnupg \ - dbus dbus-x11 fonts-liberation \ + dbus dbus-x11 fonts-liberation tesseract-ocr \ && wget -q -O - https://dl.google.com/linux/linux_signing_key.pub | gpg --dearmor -o /usr/share/keyrings/google-chrome.gpg \ && echo "deb [arch=amd64 signed-by=/usr/share/keyrings/google-chrome.gpg] http://dl.google.com/linux/chrome/deb/ stable main" > /etc/apt/sources.list.d/google-chrome.list \ && apt-get update && apt-get install -y --no-install-recommends google-chrome-stable \ diff --git a/testing/hooks/screenshot.sh b/testing/hooks/screenshot.sh index 2e5ca79c..c1d2ba03 100755 --- a/testing/hooks/screenshot.sh +++ b/testing/hooks/screenshot.sh @@ -30,7 +30,6 @@ if [ "$WIZARD_READY" -eq 0 ]; then fi HTTP_PORT="${QEMU_HTTP_PORT:-8080}" -SCREENSHOT_TAKEN=0 echo " Verifying localhost:${HTTP_PORT} is reachable from container..." HTTP_CHECK=$(curl -s -o /dev/null -w '%{http_code}' "http://localhost:${HTTP_PORT}" 2>/dev/null || echo "000") @@ -41,44 +40,77 @@ if command -v dbus-run-session &>/dev/null; then DBUS_PREFIX="dbus-run-session --" fi +BROWSER_PATH="" for BROWSER in google-chrome-stable chromium chromium-browser; do - BROWSER_PATH=$(command -v "$BROWSER" 2>/dev/null || true) - [ -n "$BROWSER_PATH" ] || continue - - if "$BROWSER_PATH" --version 2>&1 | grep -qi "snap"; then - echo " Skipping $BROWSER_PATH (snap stub)" + B=$(command -v "$BROWSER" 2>/dev/null || true) + [ -n "$B" ] || continue + if "$B" --version 2>&1 | grep -qi "snap"; then + echo " Skipping $B (snap stub)" continue fi + BROWSER_PATH="$B" + break +done + +if [ -z "$BROWSER_PATH" ]; then + echo " WARNING: No usable browser found for screenshot" +else + echo " Using $BROWSER_PATH ($(${BROWSER_PATH} --version 2>/dev/null || echo 'unknown'))" - echo " Taking headless screenshot with $BROWSER_PATH ($(${BROWSER_PATH} --version 2>/dev/null || echo 'unknown version'))..." - timeout 45 $DBUS_PREFIX "$BROWSER_PATH" --headless --no-sandbox --disable-gpu \ - --disable-dev-shm-usage --disable-setuid-sandbox \ - --disable-software-rasterizer --hide-scrollbars \ - --screenshot="/tmp/screenshot.png" \ - --window-size=1280,720 \ - "http://localhost:${HTTP_PORT}" 2>"$ARTIFACTS_DIR/browser-screenshot.log" || true + WIZARD_VISIBLE=0 + for attempt in $(seq 1 12); do + rm -f /tmp/screenshot.png + timeout 45 $DBUS_PREFIX "$BROWSER_PATH" --headless --no-sandbox --disable-gpu \ + --disable-dev-shm-usage --disable-setuid-sandbox \ + --disable-software-rasterizer --hide-scrollbars \ + --screenshot="/tmp/screenshot.png" \ + --window-size=1280,720 \ + "http://localhost:${HTTP_PORT}" 2>"$ARTIFACTS_DIR/browser-screenshot.log" || true + + if [ ! -f /tmp/screenshot.png ]; then + echo " Attempt $attempt: screenshot not created" + cat "$ARTIFACTS_DIR/browser-screenshot.log" 2>/dev/null | head -10 || true + sleep 10 + continue + fi - if [ -f /tmp/screenshot.png ]; then SIZE=$(stat -c%s /tmp/screenshot.png 2>/dev/null || echo "0") - if [ "$SIZE" -gt 10000 ]; then - cp /tmp/screenshot.png "$ARTIFACTS_DIR/screenshot.png" - echo " Screenshot saved (${SIZE} bytes)" - SCREENSHOT_TAKEN=1 + if [ "$SIZE" -le 10000 ]; then + echo " Attempt $attempt: screenshot too small (${SIZE} bytes)" rm -f /tmp/screenshot.png - break - else - echo " WARNING: Screenshot is only ${SIZE} bytes (may be blank), trying next browser" - echo " Browser stderr:" - cat "$ARTIFACTS_DIR/browser-screenshot.log" 2>/dev/null | head -20 || true + sleep 10 + continue + fi + + cp /tmp/screenshot.png "$ARTIFACTS_DIR/screenshot.png" + + OCR_TEXT="" + if command -v tesseract &>/dev/null; then + OCR_TEXT=$(tesseract /tmp/screenshot.png stdout 2>/dev/null || echo "") + fi + + if echo "$OCR_TEXT" | grep -qi "wizard\|access.control\|setup"; then + echo " Attempt $attempt: wizard detected via OCR (${SIZE} bytes)" + WIZARD_VISIBLE=1 rm -f /tmp/screenshot.png + break fi - else - echo " Screenshot file was NOT created by $BROWSER, trying next browser" - echo " Browser stderr:" - cat "$ARTIFACTS_DIR/browser-screenshot.log" 2>/dev/null | head -20 || true + + OCR_FIRST_LINE=$(echo "$OCR_TEXT" | head -c 80) + echo " Attempt $attempt: wizard not visible yet, OCR: ${OCR_FIRST_LINE:-}" + rm -f /tmp/screenshot.png + sleep 10 + done + + if [ -n "$OCR_TEXT" ]; then + echo "$OCR_TEXT" > "$ARTIFACTS_DIR/screenshot-ocr.txt" fi -done -if [ "$SCREENSHOT_TAKEN" -eq 0 ]; then - echo " WARNING: No browser produced a valid screenshot" + if [ "$WIZARD_VISIBLE" -eq 1 ]; then + echo " Screenshot captured with wizard visible" + elif [ -f "$ARTIFACTS_DIR/screenshot.png" ]; then + echo " WARNING: Wizard not detected via OCR after 12 attempts, keeping last screenshot" + else + echo " WARNING: No valid screenshot produced" + fi fi diff --git a/testing/tests/test_octoprint_web.sh b/testing/tests/test_octoprint_web.sh index 4ce45ff3..80978288 100755 --- a/testing/tests/test_octoprint_web.sh +++ b/testing/tests/test_octoprint_web.sh @@ -59,28 +59,59 @@ if [ -n "$BROWSER" ] && [ -n "$ARTIFACTS_DIR" ]; then DBUS_PREFIX="dbus-run-session --" fi - echo " Taking wizard screenshot via headless Chrome..." - timeout 45 $DBUS_PREFIX "$BROWSER" --headless --no-sandbox --disable-gpu \ - --disable-dev-shm-usage --disable-setuid-sandbox \ - --disable-software-rasterizer --hide-scrollbars \ - --screenshot="/tmp/screenshot.png" \ - --window-size=1280,720 \ - "http://localhost:${HTTP_PORT}" 2>"$ARTIFACTS_DIR/browser-screenshot.log" || true - if [ -f /tmp/screenshot.png ]; then + echo " Taking wizard screenshot via headless Chrome (with OCR retry)..." + WIZARD_VISIBLE=0 + for attempt in $(seq 1 12); do + rm -f /tmp/screenshot.png + timeout 45 $DBUS_PREFIX "$BROWSER" --headless --no-sandbox --disable-gpu \ + --disable-dev-shm-usage --disable-setuid-sandbox \ + --disable-software-rasterizer --hide-scrollbars \ + --screenshot="/tmp/screenshot.png" \ + --window-size=1280,720 \ + "http://localhost:${HTTP_PORT}" 2>"$ARTIFACTS_DIR/browser-screenshot.log" || true + + if [ ! -f /tmp/screenshot.png ]; then + echo " Attempt $attempt: screenshot not created" + sleep 10 + continue + fi + SIZE=$(stat -c%s /tmp/screenshot.png 2>/dev/null || echo "0") - if [ "$SIZE" -gt 10000 ]; then - cp /tmp/screenshot.png "$ARTIFACTS_DIR/screenshot.png" - echo " Screenshot saved (${SIZE} bytes) -- wizard page captured" - else - echo " WARNING: Screenshot is only ${SIZE} bytes (may be blank)" - echo " Browser stderr:" - cat "$ARTIFACTS_DIR/browser-screenshot.log" 2>/dev/null | head -20 || true + if [ "$SIZE" -le 10000 ]; then + echo " Attempt $attempt: screenshot too small (${SIZE} bytes)" + rm -f /tmp/screenshot.png + sleep 10 + continue + fi + + cp /tmp/screenshot.png "$ARTIFACTS_DIR/screenshot.png" + + OCR_TEXT="" + if command -v tesseract &>/dev/null; then + OCR_TEXT=$(tesseract /tmp/screenshot.png stdout 2>/dev/null || echo "") fi + + if echo "$OCR_TEXT" | grep -qi "wizard\|access.control\|setup"; then + echo " Attempt $attempt: wizard detected via OCR (${SIZE} bytes)" + WIZARD_VISIBLE=1 + rm -f /tmp/screenshot.png + break + fi + + OCR_FIRST_LINE=$(echo "$OCR_TEXT" | head -c 80) + echo " Attempt $attempt: wizard not visible yet, OCR: ${OCR_FIRST_LINE:-}" rm -f /tmp/screenshot.png - else - echo " WARNING: Screenshot was not created" - echo " Browser stderr:" - cat "$ARTIFACTS_DIR/browser-screenshot.log" 2>/dev/null | head -20 || true + sleep 10 + done + + if [ -n "$OCR_TEXT" ]; then + echo "$OCR_TEXT" > "$ARTIFACTS_DIR/screenshot-ocr.txt" + fi + + if [ "$WIZARD_VISIBLE" -eq 1 ]; then + echo " Screenshot captured with wizard visible" + elif [ -f "$ARTIFACTS_DIR/screenshot.png" ]; then + echo " WARNING: Wizard not detected via OCR after 12 attempts, keeping last screenshot" fi fi From b462da309bde6f9374a511929cb33d5e714069b6 Mon Sep 17 00:00:00 2001 From: Guy Sheffer Date: Mon, 6 Apr 2026 01:46:38 +0300 Subject: [PATCH 19/22] Add --virtual-time-budget=30000 to give Chrome time for websocket Chrome captures immediately after HTML load, before OctoPrint's JS frontend completes its websocket handshake. OCR retry confirmed all 12 attempts showed "Connecting to OctoPrint's server..." because Chrome exits too quickly. Add --virtual-time-budget=30000 to let the page run for 30 virtual seconds before capturing. Increase timeout to 60s. --- testing/hooks/screenshot.sh | 3 ++- testing/tests/test_octoprint_web.sh | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/testing/hooks/screenshot.sh b/testing/hooks/screenshot.sh index c1d2ba03..12fdc6ff 100755 --- a/testing/hooks/screenshot.sh +++ b/testing/hooks/screenshot.sh @@ -60,9 +60,10 @@ else WIZARD_VISIBLE=0 for attempt in $(seq 1 12); do rm -f /tmp/screenshot.png - timeout 45 $DBUS_PREFIX "$BROWSER_PATH" --headless --no-sandbox --disable-gpu \ + timeout 60 $DBUS_PREFIX "$BROWSER_PATH" --headless --no-sandbox --disable-gpu \ --disable-dev-shm-usage --disable-setuid-sandbox \ --disable-software-rasterizer --hide-scrollbars \ + --virtual-time-budget=30000 \ --screenshot="/tmp/screenshot.png" \ --window-size=1280,720 \ "http://localhost:${HTTP_PORT}" 2>"$ARTIFACTS_DIR/browser-screenshot.log" || true diff --git a/testing/tests/test_octoprint_web.sh b/testing/tests/test_octoprint_web.sh index 80978288..2f4cc186 100755 --- a/testing/tests/test_octoprint_web.sh +++ b/testing/tests/test_octoprint_web.sh @@ -63,9 +63,10 @@ if [ -n "$BROWSER" ] && [ -n "$ARTIFACTS_DIR" ]; then WIZARD_VISIBLE=0 for attempt in $(seq 1 12); do rm -f /tmp/screenshot.png - timeout 45 $DBUS_PREFIX "$BROWSER" --headless --no-sandbox --disable-gpu \ + timeout 60 $DBUS_PREFIX "$BROWSER" --headless --no-sandbox --disable-gpu \ --disable-dev-shm-usage --disable-setuid-sandbox \ --disable-software-rasterizer --hide-scrollbars \ + --virtual-time-budget=30000 \ --screenshot="/tmp/screenshot.png" \ --window-size=1280,720 \ "http://localhost:${HTTP_PORT}" 2>"$ARTIFACTS_DIR/browser-screenshot.log" || true From 88373682c94c4ffa8e196e845204835df05c4cdf Mon Sep 17 00:00:00 2001 From: Guy Sheffer Date: Tue, 14 Apr 2026 21:27:22 +0300 Subject: [PATCH 20/22] Refactor E2E to use CustomPiOS multi-stage Docker build and reusable workflow - Dockerfile: multi-stage build pulls shared scripts from ghcr.io/guysoft/custompios container instead of cp-r from checkout - Workflow: build job uses CustomPiOS devel, e2e-test job calls reusable workflow from CustomPiOS (no more inline 70-line job) - Test scripts: source shared ssh-helpers.sh instead of inline SSH_CMD - Remove custompios/ from .gitignore (no longer needed) - Update E2E_HANDOFF.md to reflect new architecture --- .github/workflows/build.yml | 73 +----------- testing/.gitignore | 1 - testing/Dockerfile | 9 +- testing/E2E_HANDOFF.md | 175 ++++++++++++++++++++++++++++ testing/hooks/screenshot.sh | 9 +- testing/tests/test_octoprint_web.sh | 13 +-- 6 files changed, 195 insertions(+), 85 deletions(-) create mode 100644 testing/E2E_HANDOFF.md diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 51d9ea68..b34febb8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -28,7 +28,7 @@ jobs: uses: actions/checkout@v4 with: repository: 'guysoft/CustomPiOS' - ref: feature/e2e + ref: devel path: CustomPiOS - name: Checkout Project Repository @@ -72,69 +72,8 @@ jobs: 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: octopi-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 octopi-e2e-test ./testing/ - - - name: Start E2E test container - run: | - mkdir -p artifacts - IMG=$(find image/ -name '*.img' | head -1) - docker run -d --name octopi-test \ - -v "$PWD/artifacts:/output" \ - -v "$(realpath $IMG):/input/image.img:ro" \ - -e ARTIFACTS_DIR=/output \ - -e DISTRO_NAME="OctoPi" \ - octopi-e2e-test - - - name: Wait for tests to complete - run: | - for i in $(seq 1 420); do - [ -f artifacts/exit-code ] && break - sleep 5 - done - if [ ! -f artifacts/exit-code ]; then - echo "ERROR: Tests did not complete within 35 minutes" - docker logs octopi-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 octopi-test > artifacts/container.log 2>&1 || true - docker stop octopi-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/ + uses: guysoft/CustomPiOS/.github/workflows/e2e-test.yml@feature/e2e + with: + image-artifact-name: octopi-arm64 + distro-name: OctoPi + timeout-minutes: 45 diff --git a/testing/.gitignore b/testing/.gitignore index 8a86c2b0..2d4732fd 100644 --- a/testing/.gitignore +++ b/testing/.gitignore @@ -1,3 +1,2 @@ images/ -custompios/ *.png diff --git a/testing/Dockerfile b/testing/Dockerfile index 380aca62..8196b85f 100644 --- a/testing/Dockerfile +++ b/testing/Dockerfile @@ -1,3 +1,6 @@ +ARG CUSTOMPIOS_TAG=devel +FROM ghcr.io/guysoft/custompios:${CUSTOMPIOS_TAG} AS custompios + FROM ptrsr/pi-ci:latest ENV LIBGUESTFS_BACKEND=direct @@ -10,11 +13,9 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ && apt-get update && apt-get install -y --no-install-recommends google-chrome-stable \ && 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/ +COPY --from=custompios /CustomPiOS/distro_testing/scripts/ /test/scripts/ +COPY --from=custompios /CustomPiOS/distro_testing/tests/ /test/tests/ -# OctoPi-specific tests and hooks COPY tests/ /test/tests/ COPY hooks/ /test/hooks/ diff --git a/testing/E2E_HANDOFF.md b/testing/E2E_HANDOFF.md new file mode 100644 index 00000000..daca32b7 --- /dev/null +++ b/testing/E2E_HANDOFF.md @@ -0,0 +1,175 @@ +# OctoPi E2E Test Infrastructure -- Handoff Document + +**Date:** 2026-04-14 (updated) +**OctoPi branch:** `feature/e2e` +**CustomPiOS:** shared scripts come from `ghcr.io/guysoft/custompios:devel` container via Docker multi-stage build + +--- + +## What We Did + +We built and iteratively fixed an end-to-end testing pipeline for OctoPi that: + +1. Builds the OctoPi Raspberry Pi image from source (armhf + arm64) +2. Boots the arm64 image in QEMU inside a Docker container +3. Waits for SSH, then runs automated tests +4. Captures a PNG screenshot of the OctoPrint Setup Wizard UI +5. Uploads all artifacts (logs, HTML, screenshot, OCR text) to GitHub Actions + +The screenshot work alone required 10+ iterations because headless Chrome in Docker +is notoriously fragile. The problems discovered and fixed (in order): + +| Problem | Fix | Commit | +|---------|-----|--------| +| `chromium-browser` is a snap stub on Ubuntu, not a real binary | Skip snap stubs, find real binary | `c2cc14f`, `0fb9200` | +| Chrome stderr suppressed by `2>/dev/null`, hiding all errors | Redirect stderr to `browser-screenshot.log` artifact | `3ca0db4` | +| Missing D-Bus in container -- Chrome fails silently | Install `dbus`, `dbus-x11`, wrap Chrome with `dbus-run-session` | `5549c1d`, `d5c0cea` | +| `--headless=new` broken in Docker containers (known Chrome bug) | Switch to legacy `--headless` mode | `63e82b2` | +| Missing fonts for Chrome rendering | Install `fonts-liberation` | `d5c0cea` | +| Screenshot shows "Connecting to OctoPrint's server..." spinner, not wizard | Add tesseract OCR retry loop + `--virtual-time-budget=30000` | `4703512`, `b462da3` | + +--- + +## Current Architecture + +``` +GitHub Actions workflow (.github/workflows/build.yml) + | + +-- Job: build (matrix: armhf, arm64) + | Checks out CustomPiOS at ref: devel for build tools + | Builds OctoPi .img using CustomPiOS framework + | Uploads octopi-armhf and octopi-arm64 artifacts + | + +-- Job: e2e-test (needs: build) + Calls reusable workflow: guysoft/CustomPiOS/.github/workflows/e2e-test.yml + Inputs: image-artifact-name=octopi-arm64, distro-name=OctoPi + The reusable workflow: + - Downloads the arm64 artifact + - docker build ./testing/ (multi-stage: pulls shared scripts from custompios container) + - docker run with /input/image.img + /output artifacts + - Polls for exit-code, collects logs, uploads artifacts +``` + +### How Shared Scripts Are Obtained + +The `testing/Dockerfile` uses a Docker multi-stage build: + +```dockerfile +ARG CUSTOMPIOS_TAG=devel +FROM ghcr.io/guysoft/custompios:${CUSTOMPIOS_TAG} AS custompios +FROM ptrsr/pi-ci:latest +... +COPY --from=custompios /CustomPiOS/distro_testing/scripts/ /test/scripts/ +COPY --from=custompios /CustomPiOS/distro_testing/tests/ /test/tests/ +``` + +No CustomPiOS checkout or file-copy step needed. The scripts track the `custompios:devel` container tag automatically. Override `CUSTOMPIOS_TAG` to use a different branch during development. + +### Inside the Docker Container + +The entrypoint (`/test/scripts/entrypoint.sh`, from CustomPiOS `src/distro_testing/`) runs: + +1. **Step 1 -- Prepare image:** Convert .img to qcow2, patch with guestfish (fstab, SSH, systemd units, password). OctoPi hook `hooks/prepare-image.sh` patches haproxy for IPv4-only. +2. **Step 2 -- Boot QEMU:** `qemu-system-aarch64 -M virt`, port forwards `2222->22` (SSH) and `8080->80` (HTTP). +3. **Step 3 -- Wait for SSH:** Up to 600s polling SSH on port 2222. +4. **Step 4 -- Run tests:** Glob-ordered `test_*.sh` scripts: + - `test_boot.sh` (shared) -- SSH echo smoke test + - `test_octoprint_web.sh` (OctoPi) -- Wait for CONFIG_WIZARD via SSH+curl, optional headless Chrome screenshot with OCR retry +5. **Step 5 -- Screenshot:** QEMU screendump attempt (fails, no console), then `hooks/screenshot.sh` -- headless Chrome OCR retry loop +6. **Cleanup:** Write `exit-code`, `test-results.txt`, `qemu-boot.log` to artifacts + +### Key Files + +| File | Purpose | +|------|---------| +| `.github/workflows/build.yml` | CI workflow: build + e2e-test (reusable workflow call) | +| `testing/Dockerfile` | Multi-stage: custompios container + ptrsr/pi-ci + Chrome + tesseract + dbus | +| `testing/tests/test_octoprint_web.sh` | Main OctoPi test: wait for CONFIG_WIZARD, screenshot | +| `testing/hooks/screenshot.sh` | Post-test screenshot hook: OCR retry loop for wizard | +| `testing/hooks/prepare-image.sh` | OctoPi-specific: patch haproxy for IPv4 | + +Shared scripts (from CustomPiOS container at `/test/scripts/`): +- `entrypoint.sh` -- Test orchestrator +- `boot-qemu.sh` -- QEMU boot with port forwarding +- `prepare-image.sh` -- Image prep (qcow2, guestfish) +- `wait-for-ssh.sh` -- SSH readiness polling +- `ssh-helpers.sh` -- Shared `ssh_cmd`/`scp_cmd` functions (sourced by all test scripts) + +### Artifacts Produced (in `e2e-test-results`) + +| File | Content | +|------|---------| +| `screenshot.png` | PNG 1280x720 of OctoPrint Setup Wizard (or loading page as fallback) | +| `screenshot-ocr.txt` | Tesseract OCR text from the final screenshot | +| `browser-screenshot.log` | Chrome stderr from the last screenshot attempt | +| `octoprint.html` | Raw HTML from the CONFIG_WIZARD page (via curl from guest) | +| `octoprint-ui.html` | Same, from the screenshot hook's curl check | +| `container.log` | Full Docker container stdout/stderr | +| `qemu-boot.log` | QEMU serial console log | +| `exit-code` | "0" if all tests passed | +| `test-results.txt` | `TEST_RESULT=0` or `TEST_RESULT=1` | + +--- + +## How the Screenshot Works (Current State) + +Both `test_octoprint_web.sh` and `screenshot.sh` use the same pattern: + +1. **Find a real browser:** Loop through `google-chrome-stable`, `chromium`, `chromium-browser`. Skip snap stubs (detected by `--version` output containing "snap"). +2. **Retry loop (up to 12 attempts, ~120s):** + a. Launch Chrome: `dbus-run-session -- google-chrome-stable --headless --no-sandbox --disable-gpu --disable-dev-shm-usage --disable-setuid-sandbox --disable-software-rasterizer --hide-scrollbars --virtual-time-budget=30000 --screenshot=/tmp/screenshot.png --window-size=1280,720 http://localhost:8080` + b. Check if screenshot file was created and is > 10KB + c. Run `tesseract /tmp/screenshot.png stdout` for OCR + d. If OCR text contains `wizard`, `access control`, or `setup` --> accept screenshot + e. Otherwise, log OCR text first line, sleep 10s, retry +3. **Save:** Copy to `$ARTIFACTS_DIR/screenshot.png`, write OCR text to `screenshot-ocr.txt` +4. **Fallback:** If wizard never detected after 12 attempts, keep whatever last screenshot was captured + +--- + +## Known Issues and Quirks + +### `--virtual-time-budget=30000` is unreliable in legacy `--headless` mode + +In the last CI run (#24012191477): +- `test_octoprint_web.sh` (Step 4): All 12 screenshot attempts produced NO file. Chrome with `--virtual-time-budget` in legacy headless mode sometimes fails to write the screenshot at all. The **test itself still passes** because it validates via curl/HTML, not the screenshot. +- `screenshot.sh` (Step 5): First attempt succeeded immediately (wizard visible, 50KB PNG). By this point, enough real time had elapsed (~14 min from Step 4's retries) that OctoPrint's websocket connected quickly. + +### `--headless=new` does not work in this Docker container + +Chrome's new headless mode (`--headless=new`) has known bugs in Docker that prevent screenshot file creation. We MUST use legacy `--headless`. See: https://github.com/GoogleChromeLabs/chrome-for-testing/issues/82 + +### D-Bus is required + +Chrome needs a D-Bus session bus even in headless mode. Without `dbus-run-session`, Chrome logs D-Bus errors and fails to produce screenshots. The Dockerfile installs `dbus` + `dbus-x11` (provides `dbus-run-session`). + +### QEMU screendump always fails + +QEMU runs with `-nographic` (no display device), so `screendump` via the monitor socket always returns "Error: There is no console to take a screendump from". The QEMU screenshot path in the entrypoint is dead code for this setup. + +### Build times + +- armhf build: ~2h +- arm64 build: ~2.5-3h +- e2e-test: ~8-15 min (depends on screenshot retry duration) +- Total pipeline: ~3-3.5h + +--- + +## Repo Relationships + +- **OctoPi** (`guysoft/OctoPi`): The distro. Contains `testing/` with OctoPi-specific tests, hooks, and Dockerfile. +- **CustomPiOS** (`guysoft/CustomPiOS`): The build framework. Contains `src/distro_testing/` with shared test scripts. These are pulled into the E2E Docker image via multi-stage `COPY --from=custompios` (no checkout or file copy needed). +- **Reusable workflow:** The e2e-test job calls `guysoft/CustomPiOS/.github/workflows/e2e-test.yml` which handles the docker build/run/poll/artifact cycle. + +The OctoPi workflow checks out CustomPiOS `devel` only for the **build** job (to get build tools). The **e2e-test** job gets shared scripts through the Docker container image. + +--- + +## Next Steps / Potential Improvements + +1. **Remove `--virtual-time-budget` from `test_octoprint_web.sh`** -- It causes all 12 screenshot attempts to fail (no file created). The test doesn't need screenshots; let the hook handle it. + +2. **Faster screenshot cycle** -- Instead of 12 retries x (60s Chrome timeout + 10s sleep), the screenshot hook could first wait for the test script to complete (which already burned enough time), then take a single screenshot. The wizard is usually ready by Step 5. + +3. **`--shm-size` for Docker** -- The workflow doesn't set `--shm-size` on `docker run`. Adding `--shm-size=1g` might improve Chrome reliability. Currently mitigated by `--disable-dev-shm-usage`. diff --git a/testing/hooks/screenshot.sh b/testing/hooks/screenshot.sh index 12fdc6ff..0cbac145 100755 --- a/testing/hooks/screenshot.sh +++ b/testing/hooks/screenshot.sh @@ -1,16 +1,15 @@ #!/bin/bash set -e -SSH_HOST="${1:-localhost}" -SSH_PORT="${2:-2222}" +export E2E_SSH_HOST="${1:-localhost}" +export E2E_SSH_PORT="${2:-2222}" ARTIFACTS_DIR="${3:-/output}" - -SSH_CMD="sshpass -p raspberry ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o PreferredAuthentications=password -o PubkeyAuthentication=no -o LogLevel=ERROR -p $SSH_PORT ${SSH_HOST}" +source /test/scripts/ssh-helpers.sh echo "Waiting for OctoPrint wizard page before capturing..." WIZARD_READY=0 for i in $(seq 1 24); do - BODY=$(${SSH_CMD} -l pi "curl -s http://localhost" 2>/dev/null || echo "") + BODY=$(ssh_cmd "curl -s http://localhost" 2>/dev/null || echo "") if echo "$BODY" | grep -q "CONFIG_WIZARD"; then WIZARD_READY=1 echo "$BODY" > "$ARTIFACTS_DIR/octoprint-ui.html" diff --git a/testing/tests/test_octoprint_web.sh b/testing/tests/test_octoprint_web.sh index 2f4cc186..84116ece 100755 --- a/testing/tests/test_octoprint_web.sh +++ b/testing/tests/test_octoprint_web.sh @@ -1,20 +1,17 @@ #!/bin/bash set -e -HOST="${1:-localhost}" -PORT="${2:-2222}" +export E2E_SSH_HOST="${1:-localhost}" +export E2E_SSH_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}" +source /test/scripts/ssh-helpers.sh echo "Test: OctoPrint web server is accessible with CONFIG_WIZARD" OCTOPRINT_READY=0 for i in $(seq 1 120); do - BODY=$($SSH_CMD "curl -s http://localhost" 2>/dev/null || echo "") - HTTP_CODE=$($SSH_CMD "curl -s -o /dev/null -w '%{http_code}' http://localhost" 2>/dev/null || echo "000") + BODY=$(ssh_cmd "curl -s http://localhost" 2>/dev/null || echo "") + HTTP_CODE=$(ssh_cmd "curl -s -o /dev/null -w '%{http_code}' http://localhost" 2>/dev/null || echo "000") if [ "$HTTP_CODE" = "200" ]; then if echo "$BODY" | grep -q "CONFIG_WIZARD"; then From 6e476a2e1aa1e791de05cf1f58654fddf4b7f653 Mon Sep 17 00:00:00 2001 From: Guy Sheffer Date: Thu, 16 Apr 2026 17:31:09 +0300 Subject: [PATCH 21/22] Remove internal E2E_HANDOFF.md development file --- testing/E2E_HANDOFF.md | 175 ----------------------------------------- 1 file changed, 175 deletions(-) delete mode 100644 testing/E2E_HANDOFF.md diff --git a/testing/E2E_HANDOFF.md b/testing/E2E_HANDOFF.md deleted file mode 100644 index daca32b7..00000000 --- a/testing/E2E_HANDOFF.md +++ /dev/null @@ -1,175 +0,0 @@ -# OctoPi E2E Test Infrastructure -- Handoff Document - -**Date:** 2026-04-14 (updated) -**OctoPi branch:** `feature/e2e` -**CustomPiOS:** shared scripts come from `ghcr.io/guysoft/custompios:devel` container via Docker multi-stage build - ---- - -## What We Did - -We built and iteratively fixed an end-to-end testing pipeline for OctoPi that: - -1. Builds the OctoPi Raspberry Pi image from source (armhf + arm64) -2. Boots the arm64 image in QEMU inside a Docker container -3. Waits for SSH, then runs automated tests -4. Captures a PNG screenshot of the OctoPrint Setup Wizard UI -5. Uploads all artifacts (logs, HTML, screenshot, OCR text) to GitHub Actions - -The screenshot work alone required 10+ iterations because headless Chrome in Docker -is notoriously fragile. The problems discovered and fixed (in order): - -| Problem | Fix | Commit | -|---------|-----|--------| -| `chromium-browser` is a snap stub on Ubuntu, not a real binary | Skip snap stubs, find real binary | `c2cc14f`, `0fb9200` | -| Chrome stderr suppressed by `2>/dev/null`, hiding all errors | Redirect stderr to `browser-screenshot.log` artifact | `3ca0db4` | -| Missing D-Bus in container -- Chrome fails silently | Install `dbus`, `dbus-x11`, wrap Chrome with `dbus-run-session` | `5549c1d`, `d5c0cea` | -| `--headless=new` broken in Docker containers (known Chrome bug) | Switch to legacy `--headless` mode | `63e82b2` | -| Missing fonts for Chrome rendering | Install `fonts-liberation` | `d5c0cea` | -| Screenshot shows "Connecting to OctoPrint's server..." spinner, not wizard | Add tesseract OCR retry loop + `--virtual-time-budget=30000` | `4703512`, `b462da3` | - ---- - -## Current Architecture - -``` -GitHub Actions workflow (.github/workflows/build.yml) - | - +-- Job: build (matrix: armhf, arm64) - | Checks out CustomPiOS at ref: devel for build tools - | Builds OctoPi .img using CustomPiOS framework - | Uploads octopi-armhf and octopi-arm64 artifacts - | - +-- Job: e2e-test (needs: build) - Calls reusable workflow: guysoft/CustomPiOS/.github/workflows/e2e-test.yml - Inputs: image-artifact-name=octopi-arm64, distro-name=OctoPi - The reusable workflow: - - Downloads the arm64 artifact - - docker build ./testing/ (multi-stage: pulls shared scripts from custompios container) - - docker run with /input/image.img + /output artifacts - - Polls for exit-code, collects logs, uploads artifacts -``` - -### How Shared Scripts Are Obtained - -The `testing/Dockerfile` uses a Docker multi-stage build: - -```dockerfile -ARG CUSTOMPIOS_TAG=devel -FROM ghcr.io/guysoft/custompios:${CUSTOMPIOS_TAG} AS custompios -FROM ptrsr/pi-ci:latest -... -COPY --from=custompios /CustomPiOS/distro_testing/scripts/ /test/scripts/ -COPY --from=custompios /CustomPiOS/distro_testing/tests/ /test/tests/ -``` - -No CustomPiOS checkout or file-copy step needed. The scripts track the `custompios:devel` container tag automatically. Override `CUSTOMPIOS_TAG` to use a different branch during development. - -### Inside the Docker Container - -The entrypoint (`/test/scripts/entrypoint.sh`, from CustomPiOS `src/distro_testing/`) runs: - -1. **Step 1 -- Prepare image:** Convert .img to qcow2, patch with guestfish (fstab, SSH, systemd units, password). OctoPi hook `hooks/prepare-image.sh` patches haproxy for IPv4-only. -2. **Step 2 -- Boot QEMU:** `qemu-system-aarch64 -M virt`, port forwards `2222->22` (SSH) and `8080->80` (HTTP). -3. **Step 3 -- Wait for SSH:** Up to 600s polling SSH on port 2222. -4. **Step 4 -- Run tests:** Glob-ordered `test_*.sh` scripts: - - `test_boot.sh` (shared) -- SSH echo smoke test - - `test_octoprint_web.sh` (OctoPi) -- Wait for CONFIG_WIZARD via SSH+curl, optional headless Chrome screenshot with OCR retry -5. **Step 5 -- Screenshot:** QEMU screendump attempt (fails, no console), then `hooks/screenshot.sh` -- headless Chrome OCR retry loop -6. **Cleanup:** Write `exit-code`, `test-results.txt`, `qemu-boot.log` to artifacts - -### Key Files - -| File | Purpose | -|------|---------| -| `.github/workflows/build.yml` | CI workflow: build + e2e-test (reusable workflow call) | -| `testing/Dockerfile` | Multi-stage: custompios container + ptrsr/pi-ci + Chrome + tesseract + dbus | -| `testing/tests/test_octoprint_web.sh` | Main OctoPi test: wait for CONFIG_WIZARD, screenshot | -| `testing/hooks/screenshot.sh` | Post-test screenshot hook: OCR retry loop for wizard | -| `testing/hooks/prepare-image.sh` | OctoPi-specific: patch haproxy for IPv4 | - -Shared scripts (from CustomPiOS container at `/test/scripts/`): -- `entrypoint.sh` -- Test orchestrator -- `boot-qemu.sh` -- QEMU boot with port forwarding -- `prepare-image.sh` -- Image prep (qcow2, guestfish) -- `wait-for-ssh.sh` -- SSH readiness polling -- `ssh-helpers.sh` -- Shared `ssh_cmd`/`scp_cmd` functions (sourced by all test scripts) - -### Artifacts Produced (in `e2e-test-results`) - -| File | Content | -|------|---------| -| `screenshot.png` | PNG 1280x720 of OctoPrint Setup Wizard (or loading page as fallback) | -| `screenshot-ocr.txt` | Tesseract OCR text from the final screenshot | -| `browser-screenshot.log` | Chrome stderr from the last screenshot attempt | -| `octoprint.html` | Raw HTML from the CONFIG_WIZARD page (via curl from guest) | -| `octoprint-ui.html` | Same, from the screenshot hook's curl check | -| `container.log` | Full Docker container stdout/stderr | -| `qemu-boot.log` | QEMU serial console log | -| `exit-code` | "0" if all tests passed | -| `test-results.txt` | `TEST_RESULT=0` or `TEST_RESULT=1` | - ---- - -## How the Screenshot Works (Current State) - -Both `test_octoprint_web.sh` and `screenshot.sh` use the same pattern: - -1. **Find a real browser:** Loop through `google-chrome-stable`, `chromium`, `chromium-browser`. Skip snap stubs (detected by `--version` output containing "snap"). -2. **Retry loop (up to 12 attempts, ~120s):** - a. Launch Chrome: `dbus-run-session -- google-chrome-stable --headless --no-sandbox --disable-gpu --disable-dev-shm-usage --disable-setuid-sandbox --disable-software-rasterizer --hide-scrollbars --virtual-time-budget=30000 --screenshot=/tmp/screenshot.png --window-size=1280,720 http://localhost:8080` - b. Check if screenshot file was created and is > 10KB - c. Run `tesseract /tmp/screenshot.png stdout` for OCR - d. If OCR text contains `wizard`, `access control`, or `setup` --> accept screenshot - e. Otherwise, log OCR text first line, sleep 10s, retry -3. **Save:** Copy to `$ARTIFACTS_DIR/screenshot.png`, write OCR text to `screenshot-ocr.txt` -4. **Fallback:** If wizard never detected after 12 attempts, keep whatever last screenshot was captured - ---- - -## Known Issues and Quirks - -### `--virtual-time-budget=30000` is unreliable in legacy `--headless` mode - -In the last CI run (#24012191477): -- `test_octoprint_web.sh` (Step 4): All 12 screenshot attempts produced NO file. Chrome with `--virtual-time-budget` in legacy headless mode sometimes fails to write the screenshot at all. The **test itself still passes** because it validates via curl/HTML, not the screenshot. -- `screenshot.sh` (Step 5): First attempt succeeded immediately (wizard visible, 50KB PNG). By this point, enough real time had elapsed (~14 min from Step 4's retries) that OctoPrint's websocket connected quickly. - -### `--headless=new` does not work in this Docker container - -Chrome's new headless mode (`--headless=new`) has known bugs in Docker that prevent screenshot file creation. We MUST use legacy `--headless`. See: https://github.com/GoogleChromeLabs/chrome-for-testing/issues/82 - -### D-Bus is required - -Chrome needs a D-Bus session bus even in headless mode. Without `dbus-run-session`, Chrome logs D-Bus errors and fails to produce screenshots. The Dockerfile installs `dbus` + `dbus-x11` (provides `dbus-run-session`). - -### QEMU screendump always fails - -QEMU runs with `-nographic` (no display device), so `screendump` via the monitor socket always returns "Error: There is no console to take a screendump from". The QEMU screenshot path in the entrypoint is dead code for this setup. - -### Build times - -- armhf build: ~2h -- arm64 build: ~2.5-3h -- e2e-test: ~8-15 min (depends on screenshot retry duration) -- Total pipeline: ~3-3.5h - ---- - -## Repo Relationships - -- **OctoPi** (`guysoft/OctoPi`): The distro. Contains `testing/` with OctoPi-specific tests, hooks, and Dockerfile. -- **CustomPiOS** (`guysoft/CustomPiOS`): The build framework. Contains `src/distro_testing/` with shared test scripts. These are pulled into the E2E Docker image via multi-stage `COPY --from=custompios` (no checkout or file copy needed). -- **Reusable workflow:** The e2e-test job calls `guysoft/CustomPiOS/.github/workflows/e2e-test.yml` which handles the docker build/run/poll/artifact cycle. - -The OctoPi workflow checks out CustomPiOS `devel` only for the **build** job (to get build tools). The **e2e-test** job gets shared scripts through the Docker container image. - ---- - -## Next Steps / Potential Improvements - -1. **Remove `--virtual-time-budget` from `test_octoprint_web.sh`** -- It causes all 12 screenshot attempts to fail (no file created). The test doesn't need screenshots; let the hook handle it. - -2. **Faster screenshot cycle** -- Instead of 12 retries x (60s Chrome timeout + 10s sleep), the screenshot hook could first wait for the test script to complete (which already burned enough time), then take a single screenshot. The wizard is usually ready by Step 5. - -3. **`--shm-size` for Docker** -- The workflow doesn't set `--shm-size` on `docker run`. Adding `--shm-size=1g` might improve Chrome reliability. Currently mitigated by `--disable-dev-shm-usage`. From 8e2368155eccc441b135c7f13aa594a8b953a900 Mon Sep 17 00:00:00 2001 From: Guy Sheffer Date: Wed, 22 Apr 2026 00:49:07 +0300 Subject: [PATCH 22/22] Use CustomPiOS feature-e2e container with nologin fix The shared prepare-image.sh now sets the pi user's shell to /bin/bash (instead of the Trixie default /usr/sbin/nologin), fixing SSH sessions that failed with "This account is currently not available." Temporarily points at feature-e2e tag; flip back to devel after merge. --- testing/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/Dockerfile b/testing/Dockerfile index 8196b85f..aa1f416b 100644 --- a/testing/Dockerfile +++ b/testing/Dockerfile @@ -1,4 +1,4 @@ -ARG CUSTOMPIOS_TAG=devel +ARG CUSTOMPIOS_TAG=feature-e2e FROM ghcr.io/guysoft/custompios:${CUSTOMPIOS_TAG} AS custompios FROM ptrsr/pi-ci:latest