From 47fecd5066d424609b545fe797fec312ec6270a7 Mon Sep 17 00:00:00 2001 From: Miguel Angel Ajo Pelayo Date: Wed, 16 Oct 2024 11:12:10 +0200 Subject: [PATCH] Create a soc-pytest example and add jumpstarter pytest class --- examples/soc-pytest/README.md | 176 ++++++++++++++++++ .../exporter.yaml | 24 +++ .../image/Makefile | 56 ++++++ .../image/README.md | 76 ++++++++ .../image/images/.gitignore | 1 + .../image/images/.gitkeep | 0 .../image/scripts/download-latest-raspbian | 22 +++ .../image/scripts/prepare-latest-raw | 32 ++++ .../test_booted_ok.jpeg | Bin 0 -> 38177 bytes .../test_booting_empty_ok.jpeg | Bin 0 -> 49017 bytes .../test_booting_rainbow_ok.jpeg | Bin 0 -> 66179 bytes .../test_booting_raspberries_ok.jpeg | Bin 0 -> 43165 bytes .../test_on_rpi4.py | 119 ++++++++++++ examples/soc-pytest/pyproject.toml | 28 +++ jumpstarter/testing/__init__.py | 0 jumpstarter/testing/pytest.py | 61 ++++++ jumpstarter/testing/utils.py | 22 +++ uv.lock | 30 ++- 18 files changed, 640 insertions(+), 7 deletions(-) create mode 100644 examples/soc-pytest/README.md create mode 100644 examples/soc-pytest/jumpstarter_example_soc_pytest/exporter.yaml create mode 100644 examples/soc-pytest/jumpstarter_example_soc_pytest/image/Makefile create mode 100644 examples/soc-pytest/jumpstarter_example_soc_pytest/image/README.md create mode 100644 examples/soc-pytest/jumpstarter_example_soc_pytest/image/images/.gitignore create mode 100644 examples/soc-pytest/jumpstarter_example_soc_pytest/image/images/.gitkeep create mode 100755 examples/soc-pytest/jumpstarter_example_soc_pytest/image/scripts/download-latest-raspbian create mode 100755 examples/soc-pytest/jumpstarter_example_soc_pytest/image/scripts/prepare-latest-raw create mode 100644 examples/soc-pytest/jumpstarter_example_soc_pytest/test_booted_ok.jpeg create mode 100644 examples/soc-pytest/jumpstarter_example_soc_pytest/test_booting_empty_ok.jpeg create mode 100644 examples/soc-pytest/jumpstarter_example_soc_pytest/test_booting_rainbow_ok.jpeg create mode 100644 examples/soc-pytest/jumpstarter_example_soc_pytest/test_booting_raspberries_ok.jpeg create mode 100644 examples/soc-pytest/jumpstarter_example_soc_pytest/test_on_rpi4.py create mode 100644 examples/soc-pytest/pyproject.toml create mode 100644 jumpstarter/testing/__init__.py create mode 100644 jumpstarter/testing/pytest.py create mode 100644 jumpstarter/testing/utils.py diff --git a/examples/soc-pytest/README.md b/examples/soc-pytest/README.md new file mode 100644 index 000000000..bf0bc6874 --- /dev/null +++ b/examples/soc-pytest/README.md @@ -0,0 +1,176 @@ +# Jumpstarter SOC testing with pytest example + +This example aims to demonstrate Jumpstarter in an a simple +SOC testing scenario using pytest. + +The following drivers will be utilized: +- DUTLink: for power, storage and console control of the target +- UStreamer: with an hdmi capture card plus a webcam for video snapshits + +This example requires the following hardware: +- 1x Raspberry Pi 4 +- 1x dutlink (DUTLink could be replaced by a composite set of power, storage mux and serial port interface) +- 1x HDMI Capture card +- 1x Webcam + +# Running the example (distributed env) + +1) Setup an environment with the required hardware, and customize the exporter.yaml +2) Setup the exporter to be run from a container (TODO: link) +3) Label the exporter in k8s with the `board=rpi4` label +4) Prepare the images by running `make` in the `image` directory +5) Run the tests in this directory by running: +```bash +$ cd jumpstarter_example_soc_pytest +$ uv run pytest -s +================================================================== test session starts =================================================================== +platform linux -- Python 3.12.3, pytest-8.3.3, pluggy-1.5.0 +rootdir: /home/majopela/jumpstarter/examples/soc-pytest +configfile: pyproject.toml +plugins: anyio-4.6.2.post1, cov-5.0.0 +collected 6 items + +test_on_rpi4.py::TestResource::test_setup_device +--------------------------------------------------------------------- live log setup --------------------------------------------------------------------- +INFO jumpstarter.client.lease:lease.py:35 Leasing Exporter matching labels {'board': 'rpi4'} for seconds: 1800 + +INFO jumpstarter.client.lease:lease.py:42 Lease c33b74ff-ad92-42a6-aa88-2c8a944a297c created +INFO jumpstarter.client.lease:lease.py:46 Polling Lease c33b74ff-ad92-42a6-aa88-2c8a944a297c +INFO jumpstarter.client.lease:lease.py:51 Lease c33b74ff-ad92-42a6-aa88-2c8a944a297c acquired +INFO jumpstarter.client.lease:lease.py:73 Connecting to Lease with name c33b74ff-ad92-42a6-aa88-2c8a944a297c +--------------------------------------------------------------------- live log call ---------------------------------------------------------------------- +INFO /home/majopela/jumpstarter/examples/soc-pytest/jumpstarter_example_soc_pytest/test_on_rpi4.py:test_on_rpi4.py:51 Setting up device +read: 2.45GB [00:49, 52.8MB/s] +INFO jumpstarter.testing.utils:utils.py:15 Waiting for login prompt + +RPi: BOOTLOADER release VERSION:817717 DATE: 2023/01/11 TIME: 17:40:52 +BOOTMODE: 0x06 partition 1 build-ts BUILD_TIMESTAMP=1673458852 serial c3656a7d boardrev d03114 stc 608563 +.. +Starting start4.elf @ 0xfeb00200 partition 1 ++ +[ 0.000000] Booting Linux on physical CPU 0x0000000000 [0x410fd083] +... +... + +Raspbian GNU/Linux 12 rpitest ttyS0 + +rpitest login: root +Password: +Linux rpitest 6.6.31+rpt-rpi-v8 #1 SMP PREEMPT Debian 1:6.6.31-1+rpt1 (2024-05-29) aarch64 + +The programs included with the Debian GNU/Linux system are free software; +the exact distribution terms for each program are described in the +individual files in /usr/share/doc/*/copyright. + +Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent +permitted by applicable law. +root@rpitest:~#INFO jumpstarter.testing.utils:utils.py:21 Logged in +INFO /home/majopela/jumpstarter/examples/soc-pytest/jumpstarter_example_soc_pytest/test_on_rpi4.py:test_on_rpi4.py:112 Attempting a soft power off + poweroff +root@rpitest:~# Stopping session-1.scope - Session 1 of User root... +... +[ 28.964752] reboot: Power down +PASSED +test_on_rpi4.py::TestResource::test_tpm2_device +--------------------------------------------------------------------- live log setup --------------------------------------------------------------------- +INFO jumpstarter.testing.utils:utils.py:15 Waiting for login prompt +INFO jumpstarter.testing.utils:utils.py:21 Logged in +--------------------------------------------------------------------- live log call ---------------------------------------------------------------------- +INFO /home/majopela/jumpstarter/examples/soc-pytest/jumpstarter_example_soc_pytest/test_on_rpi4.py:test_on_rpi4.py:78 Running command: apt-get install -y tpm2-tools +apt-get install -y tpm2-tools +... +root@rpitest:~# INFO /home/majopela/jumpstarter/examples/soc-pytest/jumpstarter_example_soc_pytest/test_on_rpi4.py:test_on_rpi4.py:78 Running command: tpm2_createprimary -C e -c primary.ctx +tpm2_createprimary -C e -c primary.ctx +name-alg: + value: sha256 + raw: 0xb +attributes: + value: fixedtpm|fixedparent|sensitivedataorigin|userwithauth|restricted|decrypt + raw: 0x30072 +type: + value: rsa + raw: 0x1 +exponent: 65537 +bits: 2048 +scheme: + value: null + raw: 0x10 +scheme-halg: + value: (null) + raw: 0x0 +sym-alg: + value: aes + raw: 0x6 +sym-mode: + value: cfb + raw: 0x43 +sym-keybits: 128 +rsa: efe8d8387679d50d7cea501f4302834eebd4c4b3ec7f7b6a40128c63f3e9fb6e9203429dba4e1221d4d40039ff757dc3cbec638c79e11fe5cb4cc159a5e15a3d785b179f3081ada24f6370bad9b81ad2ddcba2e137bb62a454069d37da7cd1e3a06cb7fe03fc8386b055746b5396ee3b44aa1e40dae4e6257c763a53f7eb60a29df18ee14bce38d376434d89e9c95a79d1563833a48db8016c130f6246f24e023b8874e6f2f8bb1fbfe8ad9a1a0ef71b7fc0ed412056a40a225b6f352ea32aa9564c56bef09df7107b871db136aa530ae479b0b09256373479716416bc18fc7544df8c5de99383c37193f5e016bca7ab39231a69c6d4255d93aed66527bb261d +root@rpitest:~# INFO /home/majopela/jumpstarter/examples/soc-pytest/jumpstarter_example_soc_pytest/test_on_rpi4.py:test_on_rpi4.py:78 Running command: tpm2_create -G rsa -u key.pub -r key.priv -C primary.ctx +tpm2_create -G rsa -u key.pub -r key.priv -C primary.ctx +name-alg: + value: sha256 + raw: 0xb +attributes: + value: fixedtpm|fixedparent|sensitivedataorigin|userwithauth|decrypt|sign + raw: 0x60072 +type: + value: rsa + raw: 0x1 +exponent: 65537 +bits: 2048 +scheme: + value: null + raw: 0x10 +scheme-halg: + value: (null) + raw: 0x0 +sym-alg: + value: null + raw: 0x10 +sym-mode: + value: (null) + raw: 0x0 +sym-keybits: 0 +rsa: c8cebe46344bbed17c39a497c3e5c53406be142ce741697641d940b77a835b3956c4ce0c5949688ff44a5d8ef847097e1870589ff4afcd401d2b7814b9a57ecc1f750b8a759b4e4f59915d8dda68c5463c8392870a59e21a02481e4d9b8d7ad27dd915850a587b6ff1a87fa98c578a0188e74c2731e39456c4e2e7f3158a878a294f82105a6ead9e397c15cd80c8b587c9a3f47513680cbe5f5fb5a0a41830566e5b70f312fa5e28fc780f45e72d4c8aa42fc2ea9d19e1068815493e2acda90cd6f7dabede223b494f916bd0c67682d4d5b4073b80954c0bab0ac612ae243f92c1d85ab3a7840d1d4aa7390f6155edb3341f229fbc015a8637d16230da03920f +root@rpitest:~# INFO /home/majopela/jumpstarter/examples/soc-pytest/jumpstarter_example_soc_pytest/test_on_rpi4.py:test_on_rpi4.py:78 Running command: tpm2_load -C primary.ctx -u key.pub -r key.priv -c key.ctx +tpm2_load -C primary.ctx -u key.pub -r key.priv -c key.ctx +name: 000b0395380f392a3ef0773853ed245ed1a2ba94d26261d846268146f2f4de148cf0 +root@rpitest:~# INFO /home/majopela/jumpstarter/examples/soc-pytest/jumpstarter_example_soc_pytest/test_on_rpi4.py:test_on_rpi4.py:78 Running command: echo my message > message.dat +echo my message > message.dat +root@rpitest:~# INFO /home/majopela/jumpstarter/examples/soc-pytest/jumpstarter_example_soc_pytest/test_on_rpi4.py:test_on_rpi4.py:78 Running command: tpm2_sign -c key.ctx -g sha256 -o sig.rssa message.dat +tpm2_sign -c key.ctx -g sha256 -o sig.rssa message.dat +root@rpitest:~# INFO /home/majopela/jumpstarter/examples/soc-pytest/jumpstarter_example_soc_pytest/test_on_rpi4.py:test_on_rpi4.py:78 Running command: tpm2_verifysignature -c key.ctx -g sha256 -s sig.rssa -m message.dat +.dat_verifysignature -c key.ctx -g sha256 -s sig.rssa -m message +root@rpitest:~# echo result: $? +result: 0 +PASSED +------------------------------------------------------------------- live log teardown -------------------------------------------------------------------- +INFO /home/majopela/jumpstarter/examples/soc-pytest/jumpstarter_example_soc_pytest/test_on_rpi4.py:test_on_rpi4.py:112 Attempting a soft power off +poweroff +root@rpitest:~# poweroff +... +[ 80.068761] reboot: Power down + +test_on_rpi4.py::TestResource::test_power_off_camera PASSED +test_on_rpi4.py::TestResource::test_power_on_camera PASSED +test_on_rpi4.py::TestResource::test_power_on_hdmi +--------------------------------------------------------------------- live log call ---------------------------------------------------------------------- +INFO imagehash:imagehash.py:79 video comparing snapshot test_booting_empty_ok.jpeg: snapshot f0f0f0f0f0f0f0f0, ref f0f0f0f0f0f0f0f0, diff: 0 +INFO imagehash:imagehash.py:79 video comparing snapshot test_booting_rainbow_ok.jpeg: snapshot 3c3c3c1c1c1c1c1c, ref 3c3c3c1c1c1c1c1c, diff: 0 +INFO imagehash:imagehash.py:79 video comparing snapshot test_booting_raspberries_ok.jpeg: snapshot c000000000000000, ref c000000000000000, diff: 0 +PASSED +test_on_rpi4.py::TestResource::test_login_console_hdmi +--------------------------------------------------------------------- live log setup --------------------------------------------------------------------- +INFO jumpstarter.testing.utils:utils.py:15 Waiting for login prompt +INFO jumpstarter.testing.utils:utils.py:21 Logged in +--------------------------------------------------------------------- live log call ---------------------------------------------------------------------- +INFO imagehash:imagehash.py:79 video comparing snapshot test_booted_ok.jpeg: snapshot c0c0000000000000, ref c0c0000000000000, diff: 0 +PASSED +------------------------------------------------------------------- live log teardown -------------------------------------------------------------------- +INFO /home/majopela/jumpstarter/examples/soc-pytest/jumpstarter_example_soc_pytest/test_on_rpi4.py:test_on_rpi4.py:112 Attempting a soft power off +INFO jumpstarter.client.lease:lease.py:63 Releasing Lease c33b74ff-ad92-42a6-aa88-2c8a944a297c + + +============================================================= 6 passed in 303.59s (0:05:03) ============================================================== +``` diff --git a/examples/soc-pytest/jumpstarter_example_soc_pytest/exporter.yaml b/examples/soc-pytest/jumpstarter_example_soc_pytest/exporter.yaml new file mode 100644 index 000000000..6aafb3fd6 --- /dev/null +++ b/examples/soc-pytest/jumpstarter_example_soc_pytest/exporter.yaml @@ -0,0 +1,24 @@ +apiVersion: jumpstarter.dev/v1alpha1 +kind: ExporterConfig +endpoint: grpc.jumpstarter.example.com:443 +token: xxxxx +export: + # a DUTLink interface to the DUT + dutlink: + type: jumpstarter_driver_dutlink.driver.Dutlink + config: + storage_device: "/dev/disk/by-id/usb-SanDisk_3.2_Gen_1_5B4C0AB025C0-0:0" + # an HDMI to USB capture card + video: + type: jumpstarter_driver_ustreamer.driver.UStreamer + config: + args: + device: '/dev/v4l/by-path/pci-0000:00:14.0-usbv2-0:3:1.0-video-index0' + resolution: 1920x1080 + # a USB camera pointing to the DUT + camera: + type: jumpstarter_driver_ustreamer.driver.UStreamer + config: + args: + device: '/dev/v4l/by-path/pci-0000:00:14.0-usbv2-0:4:1.0-video-index0' + resolution: 1280x720 \ No newline at end of file diff --git a/examples/soc-pytest/jumpstarter_example_soc_pytest/image/Makefile b/examples/soc-pytest/jumpstarter_example_soc_pytest/image/Makefile new file mode 100644 index 000000000..f3414940a --- /dev/null +++ b/examples/soc-pytest/jumpstarter_example_soc_pytest/image/Makefile @@ -0,0 +1,56 @@ +all: prepare-image + +############################################################################### +# Image preparation targets +############################################################################### + +download-image: + scripts/download-latest-raspbian + +prepare-image: images/latest.raw mount + scripts/prepare-latest-raw + touch images/.prepared + umount mnt + +images/.prepared: + make prepare-image + +images/latest.raw.xz: + make download-image + +images/latest.raw: images/latest.raw.xz + xz -d -f -v -T0 -k $^ + touch images/latest.raw + rm -f images/.prepared + +clean-image: + rm -f images/.prepared + rm -f images/latest.raw + +clean-images: clean-image + rm -rf images/download.raspberrypi.org + rm -rf images/latest.raw.xz + +clean: clean-image clean-images + +############################################################################### +# Image manipulation targets +############################################################################### + +mnt: + mkdir -p $@ + +umount: + umount mnt || true + +mount: umount images/latest.raw mnt + guestmount -a images/latest.raw -m /dev/sda2 -m /dev/sda1:/boot/firmware -o allow_other --rw mnt + + +############################################################################### +# phony targets are targets which don't produce files, just for utility +############################################################################### + + +.PHONY: download-image prepare-image +.PHONY: mount umount diff --git a/examples/soc-pytest/jumpstarter_example_soc_pytest/image/README.md b/examples/soc-pytest/jumpstarter_example_soc_pytest/image/README.md new file mode 100644 index 000000000..e65006975 --- /dev/null +++ b/examples/soc-pytest/jumpstarter_example_soc_pytest/image/README.md @@ -0,0 +1,76 @@ +# Image preparation scripts + +This directory contains scripts to prepare the image for the example SOC test, +running `make` should: + +* Download a minimal raspbian image +* Inject the settings for the test to be performed (enable UART, setup basic password, tpm dtb) + +You will need guestmount installed, sudo permissions. + +fuse must be configured to enable `user_allow_other` in `/etc/fuse.conf`. + + +```bash +$ make +make download-image +make[1]: Entering directory '/home/majopela/jumpstarter/examples/soc-pytest/jumpstarter_example_soc_pytest/image' +scripts/download-latest-raspbian +https://downloads.raspberrypi.org/raspios_lite_armhf/images/raspios_lite_armhf-2024-07-04/2024-07-04-raspios-bookworm-armhf-lite.img.xz +--2024-10-17 12:12:43-- https://downloads.raspberrypi.org/raspios_lite_armhf/images/raspios_lite_armhf-2024-07-04/2024-07-04-raspios-bookworm-armhf-lite.img.xz +Resolving downloads.raspberrypi.org (downloads.raspberrypi.org)... 2a00:1098:80:56::2:1, 2a00:1098:80:56::1:1, 2a00:1098:82:47::1, ... +Connecting to downloads.raspberrypi.org (downloads.raspberrypi.org)|2a00:1098:80:56::2:1|:443... connected. +HTTP request sent, awaiting response... 200 OK +Length: 523828628 (500M) [application/x-xz] +Saving to: ‘./images/downloads.raspberrypi.org/raspios_lite_armhf/images/raspios_lite_armhf-2024-07-04/2024-07-04-raspios-bookworm-armhf-lite.img.xz’ + +downloads.raspberrypi.org/raspios_lite_armhf/ima 100%[=======================================================================================================>] 499.56M 77.9MB/s in 6.6s + +2024-10-17 12:12:53 (75.3 MB/s) - ‘./images/downloads.raspberrypi.org/raspios_lite_armhf/images/raspios_lite_armhf-2024-07-04/2024-07-04-raspios-bookworm-armhf-lite.img.xz’ saved [523828628/523828628] + +FINISHED --2024-10-17 12:12:53-- +Total wall clock time: 10s +Downloaded: 1 files, 500M in 6.6s (75.3 MB/s) +Latest image: ./images/downloads.raspberrypi.org/raspios_lite_armhf/images/raspios_lite_armhf-2024-07-04/2024-07-04-raspios-bookworm-armhf-lite.img.xz +Updating link from latest.raw.xz -> ./images/downloads.raspberrypi.org/raspios_lite_armhf/images/raspios_lite_armhf-2024-07-04/2024-07-04-raspios-bookworm-armhf-lite.img.xz +make[1]: Leaving directory '/home/majopela/jumpstarter/examples/soc-pytest/jumpstarter_example_soc_pytest/image' +xz -d -f -v -T0 -k images/latest.raw.xz +images/latest.raw.xz (1/1) + 100 % 499.6 MiB / 2,512.0 MiB = 0.199 102 MiB/s 0:24 +touch images/latest.raw +rm -f images/.prepared +umount mnt || true +umount: /home/majopela/jumpstarter/examples/soc-pytest/jumpstarter_example_soc_pytest/image/mnt: not mounted. +guestmount -a images/latest.raw -m /dev/sda2 -m /dev/sda1:/boot/firmware -o allow_other --rw mnt +scripts/prepare-latest-raw ++ sudo sed -i 's/console=serial0,115200 console=tty1/console=serial0,115200/g' mnt/boot/firmware/cmdline.txt ++ cat mnt/boot/firmware/cmdline.txt +console=serial0,115200 root=PARTUUID=d28ec40f-02 rootfstype=ext4 fsck.repair=yes rootwait quiet init=/usr/lib/raspberrypi-sys-mods/firstboot ++ cat ++ sudo tee mnt/boot/firmware/custom.toml +# Raspberry Pi First Boot Setup +[system] +hostname = "rpitest" + +[user] +name = "root" +password = "changeme" +password_encrypted = false + +[ssh] +enabled = false + +[wlan] +country = "es" + +[locale] +keymap = "es" +timezone = "Europe/Madrid" ++ cat ++ sudo tee -a mnt/boot/firmware/config.txt +dtparam=spi=on +dtoverlay=tpm-slb9670 +enable_uart=1 +touch images/.prepared +umount mnt +``` \ No newline at end of file diff --git a/examples/soc-pytest/jumpstarter_example_soc_pytest/image/images/.gitignore b/examples/soc-pytest/jumpstarter_example_soc_pytest/image/images/.gitignore new file mode 100644 index 000000000..72e8ffc0d --- /dev/null +++ b/examples/soc-pytest/jumpstarter_example_soc_pytest/image/images/.gitignore @@ -0,0 +1 @@ +* diff --git a/examples/soc-pytest/jumpstarter_example_soc_pytest/image/images/.gitkeep b/examples/soc-pytest/jumpstarter_example_soc_pytest/image/images/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/examples/soc-pytest/jumpstarter_example_soc_pytest/image/scripts/download-latest-raspbian b/examples/soc-pytest/jumpstarter_example_soc_pytest/image/scripts/download-latest-raspbian new file mode 100755 index 000000000..7bd18469e --- /dev/null +++ b/examples/soc-pytest/jumpstarter_example_soc_pytest/image/scripts/download-latest-raspbian @@ -0,0 +1,22 @@ +#!/bin/bash +set -e +#LATEST_URL=$(wget -O /dev/null -o - --max-redirect=0 https://downloads.raspberrypi.org/raspios_lite_armhf_latest 2>/dev/null| sed -n "s/^Location: \(.*\) \[following\]$/\1/p") +LATEST_URL=$(curl https://downloads.raspberrypi.org/raspios_lite_armhf_latest -v 2>&1 | sed -n "s/< location: \(.*\)\r$/\1/p") +echo $LATEST_URL +CACHE="./images" +wget "${LATEST_URL}" -np -m -A '*img.xz' -c -P "${CACHE}" +# use the latest compose image +LATEST_IMG=$(ls -Art "${CACHE}/downloads.raspberrypi.org/raspios_lite_armhf/images"/*/*.img.xz | tail -n 1) + +echo "Latest image: ${LATEST_IMG}" + +# calculate full path to LATEST_IMG +LATEST_IMG_FULLPATH=$(readlink -f ${LATEST_IMG}) +EXISTING_LINK=$(readlink "${CACHE}/latest.raw.xz" || true ) +# if the link has changed, update the link +if [[ "${LATEST_IMG_FULLPATH}" != "${EXISTING_LINK}" ]]; then + echo "Updating link from latest.raw.xz -> ${LATEST_IMG}" + ln -fs "${LATEST_IMG_FULLPATH}" "${CACHE}/latest.raw.xz" +else + echo "We are up-to-date." +fi diff --git a/examples/soc-pytest/jumpstarter_example_soc_pytest/image/scripts/prepare-latest-raw b/examples/soc-pytest/jumpstarter_example_soc_pytest/image/scripts/prepare-latest-raw new file mode 100755 index 000000000..566c2b7b8 --- /dev/null +++ b/examples/soc-pytest/jumpstarter_example_soc_pytest/image/scripts/prepare-latest-raw @@ -0,0 +1,32 @@ +#!/bin/sh +set -x +# all output to serial port +sudo sed -i 's/console=serial0,115200 console=tty1/console=serial0,115200/g' mnt/boot/firmware/cmdline.txt +cat mnt/boot/firmware/cmdline.txt + +cat << EOF | sudo tee mnt/boot/firmware/custom.toml +# Raspberry Pi First Boot Setup +[system] +hostname = "rpitest" + +[user] +name = "root" +password = "changeme" +password_encrypted = false + +[ssh] +enabled = false + +[wlan] +country = "es" + +[locale] +keymap = "es" +timezone = "Europe/Madrid" +EOF + +cat << EOF | sudo tee -a mnt/boot/firmware/config.txt +dtparam=spi=on +dtoverlay=tpm-slb9670 +enable_uart=1 +EOF \ No newline at end of file diff --git a/examples/soc-pytest/jumpstarter_example_soc_pytest/test_booted_ok.jpeg b/examples/soc-pytest/jumpstarter_example_soc_pytest/test_booted_ok.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..b68d7231af12aa809fc5fafd1694e4f7a6c1c982 GIT binary patch literal 38177 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 SxWtS5$VklN7`dTm)BgpWXp9{I literal 0 HcmV?d00001 diff --git a/examples/soc-pytest/jumpstarter_example_soc_pytest/test_booting_empty_ok.jpeg b/examples/soc-pytest/jumpstarter_example_soc_pytest/test_booting_empty_ok.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..0539b92ab785d43360ebf245a204a77c4017e24d GIT binary patch literal 49017 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 Grt*KgN*hK1 literal 0 HcmV?d00001 diff --git a/examples/soc-pytest/jumpstarter_example_soc_pytest/test_booting_rainbow_ok.jpeg b/examples/soc-pytest/jumpstarter_example_soc_pytest/test_booting_rainbow_ok.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..488701c6ec6e6b8f05c040885a83a3097808adcf GIT binary patch literal 66179 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 literal 0 HcmV?d00001 diff --git a/examples/soc-pytest/jumpstarter_example_soc_pytest/test_booting_raspberries_ok.jpeg b/examples/soc-pytest/jumpstarter_example_soc_pytest/test_booting_raspberries_ok.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..1ce11069222df9a5da7c8ea3a24430d197ac52bf GIT binary patch literal 43165 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 literal 0 HcmV?d00001 diff --git a/examples/soc-pytest/jumpstarter_example_soc_pytest/test_on_rpi4.py b/examples/soc-pytest/jumpstarter_example_soc_pytest/test_on_rpi4.py new file mode 100644 index 000000000..1cf6e6ff6 --- /dev/null +++ b/examples/soc-pytest/jumpstarter_example_soc_pytest/test_on_rpi4.py @@ -0,0 +1,119 @@ +import logging +import os +import sys +import time + +import opendal +import pexpect +import pytest +from jumpstarter_imagehash import ImageHash + +from jumpstarter.client.adapters import PexpectAdapter +from jumpstarter.testing.pytest import JumpstarterTest +from jumpstarter.testing.utils import wait_and_login + +log = logging.getLogger(__file__) + +class TestResource(JumpstarterTest): + filter_labels = {"board":"rpi4"} + + @classmethod + def teardown_class(cls): + try: + cls._client.dutlink.power.off() + except Exception as e: + log.error(f"Failed to power off the device: {e}") + + # call parent teardown + super().teardown_class() + + @pytest.fixture() + def console(self, client): + with PexpectAdapter(client=client.dutlink.console) as console: + if os.environ.get("DEBUG_CONSOLE") == "1": + console.logfile_read = sys.stdout.buffer + yield console + + @pytest.fixture() + def video(self, client): + return ImageHash(client.video) + + @pytest.fixture() + def shell(self, client, console): + client.dutlink.power.off() + time.sleep(1) + client.dutlink.power.on() + yield wait_and_login(console, "root", "changeme", "@rpitest:~#") + _power_off(client, console) + + def test_setup_device(self, client, console): + client.dutlink.power.off() + log.info("Setting up device") + try: + client.dutlink.storage.write_local_file("image/images/latest.raw") + except opendal.exceptions.NotFound: + pytest.exit("No image found, please enter the image directory and run `make`, " + "more details in the README.md") + return + client.dutlink.storage.dut() + client.dutlink.power.on() + console.logfile_read = sys.stdout.buffer + # first boot on raspbian will take some time, we wait for the login + wait_and_login(console, "root", "changeme", "@rpitest:~#") + # then power off the device + _power_off(client, console) + + def test_tpm2_device(self, shell): + shell.logfile_read = sys.stdout.buffer + + lines = ["apt-get install -y tpm2-tools", + "tpm2_createprimary -C e -c primary.ctx", + "tpm2_create -G rsa -u key.pub -r key.priv -C primary.ctx", + "tpm2_load -C primary.ctx -u key.pub -r key.priv -c key.ctx", + "echo my message > message.dat", + "tpm2_sign -c key.ctx -g sha256 -o sig.rssa message.dat", + "tpm2_verifysignature -c key.ctx -g sha256 -s sig.rssa -m message.dat"] + + for line in lines: + log.info(f"Running command: {line}") + shell.sendline(line) + shell.expect("@rpitest:~#", timeout=200) + + shell.sendline("echo result: $?") + shell.expect(r"result: \d.", timeout=200) + assert shell.after.decode().strip() == "result: 0" + + def test_power_off_camera(self, client): + client.dutlink.power.off() + client.camera.snapshot().save("camera_off.jpeg") + + def test_power_on_camera(self, client): + client.dutlink.power.on() + time.sleep(1) + client.camera.snapshot().save("camera_on.jpeg") + client.dutlink.power.off() + + def test_power_on_hdmi(self, client, video): + # check all the image snapshots through the rpi4 boot process + client.dutlink.power.on() + time.sleep(1) + video.assert_snapshot("test_booting_empty_ok.jpeg") + time.sleep(6) + video.assert_snapshot("test_booting_rainbow_ok.jpeg") + time.sleep(4) + video.assert_snapshot("test_booting_raspberries_ok.jpeg") + client.dutlink.power.off() + + def test_login_console_hdmi(self, shell, video): + video.assert_snapshot("test_booted_ok.jpeg") + + +def _power_off(client, console): + log.info("Attempting a soft power off") + try: + console.sendline("poweroff") + console.expect("Power down.") + except pexpect.TIMEOUT: + log.error("Timeout waiting for power down, continuing with hard power off") + finally: + client.dutlink.power.off() diff --git a/examples/soc-pytest/pyproject.toml b/examples/soc-pytest/pyproject.toml new file mode 100644 index 000000000..681fd3ad5 --- /dev/null +++ b/examples/soc-pytest/pyproject.toml @@ -0,0 +1,28 @@ +[project] +name = "jumpstarter-example-soc-pytest" +version = "0.1.0" +description = "" +authors = [ + { name = "Miguel Angel Ajo Pelayo", email = "majopela@redhat.com" }, + { name = "Kirk Brauer", email = "kbrauer@hatci.com" }, + { name = "Nick Cao", email = "ncao@redhat.com" }, +] +readme = "README.md" +requires-python = ">=3.11" +dependencies = [ + "jumpstarter", + "pytest>=8.3.2", + # TODO: figure this out + #"jumsptarter_imagehash", + #"jumpstarter_driver_dutlink", +] + +[tool.uv.sources] +jumpstarter = { workspace = true } +#jumpstarter-imagehash = { workspace = true } +#jumpstarter-driver-dutlink = { workspace = true } + +[tool.pytest.ini_options] +addopts = "-s --ignore examples/pytest/test_on_rpi4.py" +log_cli = 1 +log_cli_level = "INFO" \ No newline at end of file diff --git a/jumpstarter/testing/__init__.py b/jumpstarter/testing/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/jumpstarter/testing/pytest.py b/jumpstarter/testing/pytest.py new file mode 100644 index 000000000..f6993d06b --- /dev/null +++ b/jumpstarter/testing/pytest.py @@ -0,0 +1,61 @@ +import time + +import pytest + +from jumpstarter.common import MetadataFilter +from jumpstarter.common.utils import env +from jumpstarter.config.client import ClientConfigV1Alpha1 + + +class JumpstarterTest: + """ + Base class for Jumpstarter test cases in pytest + + This class provides a client fixture that can be used to interact with + Jumpstarter services in test cases. + + Looks for the JUMPSTARTER_HOST environment variable to connect to a + established Jumpstarter shell, otherwise it will try to acquire a lease + for a single exporter using the filter_labels annotation. + i.e.: + ``` + class TestResource(JumpstarterTest): + filter_labels = {"board":"rpi4"} + + @pytest.fixture() + def console(self, client): + with PexpectAdapter(client=client.dutlink.console) as console: + yield console + + def test_setup_device(self, client, console): + client.dutlink.power.off() + log.info("Setting up device") + client.dutlink.storage.write_local_file("2024-07-04-raspios-bookworm-arm64-lite.img") + client.dutlink.storage.dut() + client.dutlink.power.on() + ``` + """ + @classmethod + def setup_class(cls): + try: + cls.__client = env() + cls._client = cls.__client.__enter__() + except RuntimeError: + labels = getattr(cls, "filter_labels", {}) + cls._lease = ClientConfigV1Alpha1.load("default").lease(metadata_filter=MetadataFilter(labels=labels)) + cls.__client = cls._lease.__enter__().connect() + cls._client = cls.__client.__enter__() + + @classmethod + def teardown_class(cls): + cls.__client.__exit__(None, None, None) + if hasattr(cls, "_lease"): + cls._lease.__exit__(None, None, None) + + # BUG workaround: make sure that grpc servers get the client/lease release properly + time.sleep(1) + + @pytest.fixture() + def client(self): + return self._client + diff --git a/jumpstarter/testing/utils.py b/jumpstarter/testing/utils.py new file mode 100644 index 000000000..19c8b7d3e --- /dev/null +++ b/jumpstarter/testing/utils.py @@ -0,0 +1,22 @@ +import logging + +log = logging.getLogger(__name__) + +def wait_and_login(pexpect_console, username, password, prompt, timeout=240): + """ + Wait for login prompt and login + + :param pexpect_console: pexpect console object + :type pexpect_console: pexpect.spawn + + :return: pexpect console object + :rtype: pexpect.spawn + """ + log.info("Waiting for login prompt") + pexpect_console.expect("login:", timeout=timeout) + pexpect_console.sendline(username) + pexpect_console.expect("Password:") + pexpect_console.sendline(password) + pexpect_console.expect(prompt, timeout=60) + log.info("Logged in") + return pexpect_console diff --git a/uv.lock b/uv.lock index 0a326d385..2e1c2122d 100644 --- a/uv.lock +++ b/uv.lock @@ -14,6 +14,7 @@ members = [ "jumpstarter-driver-sdwire", "jumpstarter-driver-ustreamer", "jumpstarter-example-automotive", + "jumpstarter-example-soc-pytest", "jumpstarter-imagehash", ] @@ -734,7 +735,7 @@ wheels = [ [[package]] name = "jumpstarter" -version = "0.0.3.dev44+gb4ed30f.d20241016" +version = "0.0.3.dev49+g43aabe2.d20241016" source = { editable = "." } dependencies = [ { name = "aiohttp" }, @@ -811,7 +812,7 @@ dev = [ [[package]] name = "jumpstarter-driver-can" -version = "0.0.3.dev44+gfd0d3ba" +version = "0.0.3.dev49+g43aabe2.d20241016" source = { editable = "contrib/drivers/can" } dependencies = [ { name = "can-isotp" }, @@ -840,7 +841,7 @@ dev = [ [[package]] name = "jumpstarter-driver-dutlink" -version = "0.0.3.dev44+gfd0d3ba" +version = "0.0.3.dev49+g43aabe2.d20241016" source = { editable = "contrib/drivers/dutlink" } dependencies = [ { name = "jumpstarter" }, @@ -869,7 +870,7 @@ dev = [ [[package]] name = "jumpstarter-driver-raspberrypi" -version = "0.0.3.dev44+gfd0d3ba" +version = "0.0.3.dev49+g43aabe2.d20241016" source = { editable = "contrib/drivers/raspberrypi" } dependencies = [ { name = "gpiozero" }, @@ -896,7 +897,7 @@ dev = [ [[package]] name = "jumpstarter-driver-sdwire" -version = "0.0.3.dev44+gfd0d3ba" +version = "0.0.3.dev49+g43aabe2.d20241016" source = { editable = "contrib/drivers/sdwire" } dependencies = [ { name = "jumpstarter" }, @@ -925,7 +926,7 @@ dev = [ [[package]] name = "jumpstarter-driver-ustreamer" -version = "0.0.3.dev44+gfd0d3ba" +version = "0.0.3.dev49+g43aabe2.d20241016" source = { editable = "contrib/drivers/ustreamer" } dependencies = [ { name = "jumpstarter" }, @@ -961,9 +962,24 @@ dependencies = [ [package.metadata] requires-dist = [{ name = "jumpstarter", editable = "." }] +[[package]] +name = "jumpstarter-example-soc-pytest" +version = "0.1.0" +source = { virtual = "examples/soc-pytest" } +dependencies = [ + { name = "jumpstarter" }, + { name = "pytest" }, +] + +[package.metadata] +requires-dist = [ + { name = "jumpstarter", editable = "." }, + { name = "pytest", specifier = ">=8.3.2" }, +] + [[package]] name = "jumpstarter-imagehash" -version = "0.0.3.dev44+gb4ed30f.d20241016" +version = "0.0.3.dev49+g43aabe2.d20241016" source = { editable = "contrib/libs/imagehash" } dependencies = [ { name = "imagehash" },