From 87dc8066855f1503ad4872357ed23e08a9eb9748 Mon Sep 17 00:00:00 2001 From: Kelvin Zhang Date: Wed, 6 May 2026 14:36:04 -0700 Subject: [PATCH] [ci] scripts: rewrite unittest to use expect Rewrite the python-based unittest script into two distinct expect scripts: one for testing UEFI integration and another for executing general unittests. This allows FAT tests to accurately evaluate hardware dependencies by keeping virtio block devices out of the general unittest environment. Updates GitHub CI configurations to depend on the 'expect' utility and invoke both shell scripts instead of python. --- .github/workflows/github-ci-clang.yml | 9 ++- .github/workflows/github-ci-rust.yml | 9 ++- scripts/uefi_unittest.exp | 78 ++++++++++++++++++++ scripts/unittest.exp | 80 +++++++++++++++++++++ scripts/unittest.py | 100 -------------------------- 5 files changed, 172 insertions(+), 104 deletions(-) create mode 100755 scripts/uefi_unittest.exp create mode 100755 scripts/unittest.exp delete mode 100644 scripts/unittest.py diff --git a/.github/workflows/github-ci-clang.yml b/.github/workflows/github-ci-clang.yml index c7778d22f5..8a79abd337 100644 --- a/.github/workflows/github-ci-clang.yml +++ b/.github/workflows/github-ci-clang.yml @@ -81,11 +81,16 @@ jobs: shell: bash run: | env -i DEBIAN_FRONTEND=noninteractive sudo apt-get update - env -i DEBIAN_FRONTEND=noninteractive sudo apt-get install -y qemu-system-arm + env -i DEBIAN_FRONTEND=noninteractive sudo apt-get install -y qemu-system-arm expect + - name: uefi unittest + if: ${{ matrix.project == 'qemu-virt-arm64-test' }} + shell: bash + run: | + ./scripts/uefi_unittest.exp - name: unittest if: ${{ matrix.project == 'qemu-virt-arm64-test' }} shell: bash run: | - python3 scripts/unittest.py + ./scripts/unittest.exp # vim: ts=2 sw=2 expandtab diff --git a/.github/workflows/github-ci-rust.yml b/.github/workflows/github-ci-rust.yml index 6c2b98cb94..0ae5952652 100644 --- a/.github/workflows/github-ci-rust.yml +++ b/.github/workflows/github-ci-rust.yml @@ -77,11 +77,16 @@ jobs: shell: bash run: | env -i DEBIAN_FRONTEND=noninteractive sudo apt-get update - env -i DEBIAN_FRONTEND=noninteractive sudo apt-get install -y qemu-system-arm + env -i DEBIAN_FRONTEND=noninteractive sudo apt-get install -y qemu-system-arm expect + - name: uefi unittest + if: ${{ matrix.project == 'qemu-virt-arm64-test' }} + shell: bash + run: | + ./scripts/uefi_unittest.exp - name: unittest if: ${{ matrix.project == 'qemu-virt-arm64-test' }} shell: bash run: | - python3 scripts/unittest.py + ./scripts/unittest.exp # vim: ts=2 sw=2 expandtab diff --git a/scripts/uefi_unittest.exp b/scripts/uefi_unittest.exp new file mode 100755 index 0000000000..52054dfd86 --- /dev/null +++ b/scripts/uefi_unittest.exp @@ -0,0 +1,78 @@ +#!/usr/bin/expect -f + +# Disable pty echo and newline translation to prevent double carriage returns +set stty_init "-onlcr -echo" + +proc kill_qemu {} { + global qemu_pid + catch {exec kill -TERM $qemu_pid} + after 1000 + catch {exec kill -KILL $qemu_pid} + catch {wait} +} + +proc shutdown_qemu {status} { + kill_qemu + exit $status +} + +# Set timeout for the first stage: waiting for the shell +set timeout 10 + +# Spawn the QEMU process +spawn qemu-system-aarch64 \ + -cpu max \ + -m 512 \ + -smp 1 \ + -machine virt,highmem=off \ + -kernel build-qemu-virt-arm64-test/lk.elf \ + -net none \ + -nographic \ + -drive if=none,file=lib/uefi/helloworld_aa64.efi,id=blk,format=raw \ + -device virtio-blk-device,drive=blk +set qemu_pid [exp_pid] + +# Wait for the app shell to start +expect { + "starting app shell" { + # Send the command to load the UEFI app + send "uefi_load virtio0\r" + } + timeout { + puts "Timeout waiting for 'starting app shell'" + kill_qemu + exit 1 + } + eof { + puts "QEMU exited unexpectedly before shell" + exit 1 + } +} + +# Set timeout for the second stage: waiting for Hello World! +set timeout 5 + +# Wait for the expected output from the UEFI app +expect { + "Hello World!" { + # Successfully saw the output, now shut down + shutdown_qemu 0 + } + "panic (caller" { + puts "LK panic while running the UEFI unittest" + shutdown_qemu 1 + } + "CRASH: starting debug shell" { + puts "LK crashed and entered the debug shell while running the UEFI unittest" + shutdown_qemu 1 + } + timeout { + puts "Timeout waiting for 'Hello World!'" + # Try to shut down anyway + shutdown_qemu 1 + } + eof { + puts "QEMU exited unexpectedly before Hello World!" + exit 1 + } +} diff --git a/scripts/unittest.exp b/scripts/unittest.exp new file mode 100755 index 0000000000..c30b62322d --- /dev/null +++ b/scripts/unittest.exp @@ -0,0 +1,80 @@ +#!/usr/bin/expect -f + +# Disable pty echo and newline translation to prevent double carriage returns +set stty_init "-onlcr -echo" + +proc kill_qemu {} { + global qemu_pid + catch {exec kill -TERM $qemu_pid} + after 1000 + catch {exec kill -KILL $qemu_pid} + catch {wait} +} + +proc shutdown_qemu {status} { + kill_qemu + exit $status +} + +# Set timeout for the first stage: waiting for the shell +set timeout 10 + +# Spawn the QEMU process +spawn qemu-system-aarch64 \ + -cpu max \ + -m 512 \ + -smp 1 \ + -machine virt,highmem=off \ + -kernel build-qemu-virt-arm64-test/lk.elf \ + -net none \ + -nographic +set qemu_pid [exp_pid] + +# Wait for the app shell to start +expect { + "starting app shell" { + # Send the command to run unittests + send "ut all\r" + } + timeout { + puts "Timeout waiting for 'starting app shell'" + kill_qemu + exit 1 + } + eof { + puts "QEMU exited unexpectedly before shell" + exit 1 + } +} + +# Set timeout for the second stage: waiting for tests to finish +set timeout 30 + +# Wait for the expected output from the unittests +expect { + "SUCCESS! All test cases passed" { + # Successfully saw the output, now shut down + shutdown_qemu 0 + } + "FAILURE! Some test cases failed" { + puts "Some test cases failed!" + shutdown_qemu 1 + } + "panic (caller" { + puts "LK panic while running unittests" + shutdown_qemu 1 + } + "CRASH: starting debug shell" { + puts "LK crashed and entered the debug shell while running unittests" + shutdown_qemu 1 + } + timeout { + puts "Timeout waiting for unittests to complete" + # Try to shut down anyway + shutdown_qemu 1 + } + eof { + puts "QEMU exited unexpectedly before unittests completed" + exit 1 + } +} diff --git a/scripts/unittest.py b/scripts/unittest.py deleted file mode 100644 index 3403cfe8a0..0000000000 --- a/scripts/unittest.py +++ /dev/null @@ -1,100 +0,0 @@ -import os -import sys -import signal -import subprocess -from subprocess import PIPE, STDOUT -import io -import time -import select - - -def wait_for_output(fp: io.TextIOBase, predicate, timeout: float): - start_time = time.time() - end_time = start_time + timeout - poll_obj = select.poll() - poll_obj.register(fp, select.POLLIN) - output = [] - cur_line = "" - while time.time() < end_time: - poll_result = poll_obj.poll(max(end_time-time.time(), 0.001)) - if poll_result: - data = fp.read() - if "\n" in data: - output.append(cur_line + data[:data.index("\n") + 1]) - cur_line = data[data.index("\n") + 1:] - if predicate(output[-1]): - return True, output - else: - cur_line += data - - return False, output - - -def shutdown_little_kernel(p: subprocess.Popen): - try: - ret = p.poll() - if ret: - print("LittleKernel already exited with code", ret) - return - status_path = "/proc/{}/status".format(p.pid) - if os.path.exists(status_path): - with open(status_path) as fp: - lines = fp.readlines() - state_line = [l for l in lines if "State:" in l] - if state_line: - print("LittleKernel process state after test:",state_line[0].rstrip()) - else: - print(status_path, "does not exists") - p.stdin.write("poweroff\n") - p.stdin.flush() - p.wait(0.3) - p.send_signal(signal.SIGINT) - p.wait(1) - except subprocess.TimeoutExpired: - pass - finally: - p.kill() - p.wait() - - -def main(): - # Test relies on reading subprocess output, so set bufsize=0 - # to ensure that we get real-time output. - p = subprocess.Popen(['qemu-system-aarch64', - '-cpu', - 'max', - '-m', - '512', - '-smp', - '1', - '-machine', - 'virt,highmem=off', - '-kernel', - 'build-qemu-virt-arm64-test/lk.elf', - '-net', - 'none', - '-nographic', - '-drive', - 'if=none,file=lib/uefi/helloworld_aa64.efi,id=blk,format=raw', - '-device', - 'virtio-blk-device,drive=blk'], stdout=PIPE, stdin=PIPE, stderr=STDOUT, text=True, bufsize=0) - try: - os.set_blocking(p.stdout.fileno(), False) - condition_met, output = wait_for_output( - p.stdout, lambda l: "starting app shell" in l, 5) - assert condition_met, "Did not see 'starting app shell', stdout: {}".format( - "".join(output)) - p.stdin.write("uefi_load virtio0\n") - p.stdin.flush() - condition_met, output = wait_for_output( - p.stdout, lambda l: "Hello World!" in l, 0.5) - print("".join(output)) - if condition_met: - return - - finally: - shutdown_little_kernel(p) - - -if __name__ == "__main__": - main()