From 7f52b5a58687ee176d82ac6c37ff2882853d71bb Mon Sep 17 00:00:00 2001 From: Guy Sheffer Date: Fri, 20 Feb 2026 23:18:39 +0200 Subject: [PATCH 1/6] 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 2/6] 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 3/6] 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 4/6] 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 5/6] 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 6/6] 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