A shared e2e testing framework for distros built with CustomPiOS. It boots a built image in QEMU inside a Docker container, waits for SSH, runs test scripts, and captures a QEMU screenshot.
This directory (src/distro_testing/) provides the generic infrastructure. Each distro adds its own testing/ directory with distro-specific tests and hooks.
┌─────────────────────────────────────────────────┐
│ Docker container (ptrsr/pi-ci + test tools) │
│ │
│ 1. prepare-image.sh → convert & patch image │
│ 2. boot-qemu.sh → start QEMU -M virt │
│ 3. wait-for-ssh.sh → poll until SSH ready │
│ 4. test_*.sh → run all tests via SSH │
│ 5. screendump → QEMU monitor capture │
└─────────────────────────────────────────────────┘
src/distro_testing/
├── Dockerfile.base # Reference base image (ptrsr/pi-ci + tools)
├── README.md # This file
├── scripts/
│ ├── prepare-image.sh # Generic image prep (qcow2, fstab, SSH, etc.)
│ ├── boot-qemu.sh # QEMU boot with configurable ports
│ ├── wait-for-ssh.sh # SSH readiness poller
│ └── entrypoint.sh # Test orchestrator
└── tests/
└── test_boot.sh # Generic SSH boot test
testing/
├── Dockerfile # Extends base, copies both shared + distro files
├── tests/
│ └── test_myservice.sh # Distro-specific tests
└── hooks/
└── prepare-image.sh # (optional) Distro-specific image patches
Your distro's testing/Dockerfile copies the shared framework (placed in custompios/ by CI) and your distro-specific tests:
FROM ptrsr/pi-ci:latest
ENV LIBGUESTFS_BACKEND=direct
RUN apt-get update && apt-get install -y --no-install-recommends \
sshpass openssh-client curl socat imagemagick \
&& rm -rf /var/lib/apt/lists/*
# Shared framework from CustomPiOS (copied into build context by CI)
COPY custompios/scripts/ /test/scripts/
COPY custompios/tests/ /test/tests/
# Distro-specific tests and hooks
COPY tests/ /test/tests/
COPY hooks/ /test/hooks/
RUN chmod +x /test/scripts/*.sh /test/tests/*.sh; \
chmod +x /test/hooks/*.sh 2>/dev/null || true
ENTRYPOINT ["/test/scripts/entrypoint.sh"]Test scripts live in testing/tests/ and follow this convention:
#!/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}"
# Your test logic here -- use $SSH_CMD to run commands on the guest
OUTPUT=$($SSH_CMD 'systemctl is-active myservice' 2>/dev/null)
if [ "$OUTPUT" = "active" ]; then
echo " PASS: myservice is running"
exit 0
else
echo " FAIL: myservice is not running (status: $OUTPUT)"
exit 1
fiConventions:
- Script name must start with
test_(e.g.test_myservice.sh) - Arguments:
$1= host,$2= SSH port,$3= artifacts directory (optional) - Exit 0 for pass, non-zero for fail
- Use the
SSH_CMDpattern shown above for guest commands
If your distro needs image patches beyond the generic ones (e.g. fixing configs for QEMU), create testing/hooks/prepare-image.sh:
#!/bin/bash
set -e
IMAGE_FILE="${1:?Usage: $0 <image.qcow2>}"
export LIBGUESTFS_BACKEND=direct
# Example: patch a config file inside the image
guestfish -a "$IMAGE_FILE" <<EOF
run
mount /dev/sda2 /
# your guestfish commands here
umount /
EOF
echo 'Distro-specific patches applied'The hook receives the qcow2 image path as $1 and is called after the generic preparation completes.
Configure the test environment via Docker -e flags or in your workflow:
| Variable | Default | Description |
|---|---|---|
DISTRO_NAME |
CustomPiOS Distro |
Name shown in test output banner |
QEMU_SSH_PORT |
2222 |
Host port forwarded to guest SSH (22) |
QEMU_HTTP_PORT |
8080 |
Host port forwarded to guest HTTP (80) |
QEMU_EXTRA_PORTS |
(empty) | Additional hostfwd entries, e.g. hostfwd=tcp::5900-:5900 |
QEMU_EXTRA_ARGS |
(empty) | Extra QEMU flags, e.g. -device virtio-gpu-pci |
QEMU_MONITOR_SOCK |
/tmp/qemu-monitor.sock |
Path to QEMU monitor socket for screendump |
SSH_TIMEOUT |
600 |
Seconds to wait for SSH before giving up |
ARTIFACTS_DIR |
(empty) | Directory to write test results, logs, screenshots |
KEEP_ALIVE |
(empty) | If set, container stays alive after tests (for debugging) |
Add an e2e-test job to your workflow. The key steps are:
- Build the image (your existing build job)
- Download the built artifact
- Checkout CustomPiOS and copy
src/distro_testing/into your Docker build context - Build and run the test container
e2e-test:
needs: build
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- name: Checkout CustomPiOS
uses: actions/checkout@v4
with:
repository: 'guysoft/CustomPiOS'
path: CustomPiOS
- name: Download image from build
uses: actions/download-artifact@v4
with:
name: build-image
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 e2e-test ./testing/
- name: Start E2E test container
run: |
mkdir -p artifacts
IMG=$(find image/ -name '*.img' | head -1)
docker run -d --name e2e-test \
-v "$PWD/artifacts:/output" \
-v "$(realpath $IMG):/input/image.img:ro" \
-e ARTIFACTS_DIR=/output \
-e DISTRO_NAME="My Distro" \
-e KEEP_ALIVE=true \
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 e2e-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
if: always()
run: |
docker logs e2e-test > artifacts/container.log 2>&1 || true
docker stop e2e-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/The entrypoint automatically attempts a QEMU monitor screendump after tests complete. For distros with a GUI (e.g. FullPageOS), add a virtual GPU:
-e QEMU_EXTRA_ARGS="-device virtio-gpu-pci"The screenshot is captured via:
echo "screendump /tmp/screenshot.ppm" | socat - unix-connect:/tmp/qemu-monitor.sock
This is purely QEMU-internal -- no guest-side VNC or screenshot tools are needed. The resulting image is saved to $ARTIFACTS_DIR/screenshot.png.
Note: The -nographic flag is always set for serial console output. The screendump captures the virtual GPU framebuffer, which is separate from the serial console. If no GPU device is added, the screendump will be empty or unavailable.
# From your distro's repo root
cd testing
# Copy the shared framework
mkdir -p custompios
cp -r /path/to/CustomPiOS/src/distro_testing/scripts custompios/scripts
cp -r /path/to/CustomPiOS/src/distro_testing/tests custompios/tests
# Build the Docker image
DOCKER_BUILDKIT=0 docker build -t my-distro-e2e .
# Run tests
mkdir -p artifacts
docker run --rm \
-v "$PWD/artifacts:/output" \
-v "/path/to/my-distro.img:/input/image.img:ro" \
-e ARTIFACTS_DIR=/output \
-e DISTRO_NAME="My Distro" \
my-distro-e2eAdd KEEP_ALIVE=true to keep the container running after tests:
docker run -d --name debug-test \
-v "$PWD/artifacts:/output" \
-v "/path/to/image.img:/input/image.img:ro" \
-e ARTIFACTS_DIR=/output \
-e KEEP_ALIVE=true \
my-distro-e2e
# Watch logs
docker logs -f debug-test
# SSH into the running guest (from inside the container)
docker exec -it debug-test sshpass -p raspberry ssh \
-o StrictHostKeyChecking=no -p 2222 pi@localhost
# Check QEMU serial log
docker exec -it debug-test cat /tmp/qemu-serial.log