diff --git a/ci/qemu_guest_test.py b/ci/qemu_guest_test.py new file mode 100644 index 0000000..7720873 --- /dev/null +++ b/ci/qemu_guest_test.py @@ -0,0 +1,87 @@ +"""qemu-based tests that are copied into the guest and run there""" + +# Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. +# SPDX-License-Identifier: BSD-3-Clause + +# These tests are run inside the qemu guest as root using its own pytest runner +# invocation. + +import subprocess +import tempfile + +import pytest + +# Mark this module so that the main test runner can skip it when running from +# the host. However, the guest test runner does not use this mark but instead +# explicitly calls this file. Marks require test collection, and the guest test +# runner isn't going to have dependencies installed that are only needed for +# host tests, causing guest test collection to fail otherwise. +pytestmark = pytest.mark.guest + + +def test_empty(): + # The empty test. This is nevertheless useful as its presence ensures that + # the host is calling the guest test suite in this module correctly. + pass + + +# To keep tests fast for developer iteration, just install all the test +# dependencies together once for all tests defined here. It is likely that our +# tests here are not going to interact. If they do, then we can decide whether +# to compromise on this at that time. +@pytest.fixture(scope="module") +def apt_dependencies(): + # To speed things up, we deliberately skip the apt-get update here on the + # assumption that it was arranged by whatever is running the test. This is + # arranged from qemu_test.py::test_using_guest_tests() instead. + subprocess.run( + ["apt-get", "install", "-y", "--no-install-recommends", "sudo", "gdb"], + check=True, + ) + + +def test_sudo_no_fqdn(apt_dependencies): + """sudo should not call FQDN lookup functions + + See: https://github.com/qualcomm-linux/qcom-deb-images/issues/193 + """ + with tempfile.NamedTemporaryFile( + mode="w", delete_on_close=False + ) as gdb_commands_file: + print( + "catch load", + "run", + "del 1", + sep="\n", + file=gdb_commands_file, + ) + for fn_name in [ + "gethostbyaddr", + "getnameinfo", + "getaddrinfo", + "gethostbyname", + ]: + print( + f"break {fn_name}", + f"commands", + f' print "{fn_name} called\\n"', + f" quit 1", + f"end", + sep="\n", + file=gdb_commands_file, + ) + print("continue", file=gdb_commands_file) + gdb_commands_file.close() + + subprocess.run( + [ + "gdb", + "--batch", + "-x", + gdb_commands_file.name, + "--args", + "sudo", + "true", + ], + check=True, + ) diff --git a/ci/qemu_test.py b/ci/qemu_test.py index c03fbde..7df77c2 100644 --- a/ci/qemu_test.py +++ b/ci/qemu_test.py @@ -8,15 +8,28 @@ import subprocess import sys import tempfile +import types import pexpect import pytest +# Since the first test checks for the mandatory password reset functionality +# that also prepares the VM for shell-based access, we make the additional +# optimisation that the fixture for a logged in VM re-uses that VM, so the +# ordering of [plain VM fixture, password reset test, logged-in VM fixture] +# matters here. -@pytest.fixture + +@pytest.fixture(scope="module") def vm(): """A pexpect.spawn object attached to the serial console of a VM freshly booting with a CoW base of disk-ufs.img""" + # Since qemu booting is slow and we want fast developer iteration, we make the + # optimisation compromise that we will not reset the qemu test fixture from a + # fresh image for every test. Most tests should not collide with each other. If + # we think a new test will do that and we want to make the compromise of giving + # it an isolated environment for a slower test suite, we can deal with that + # then. with tempfile.TemporaryDirectory() as tmpdir: qcow_path = os.path.join(tmpdir, "disk1.qcow") subprocess.run( @@ -33,7 +46,7 @@ def vm(): ], check=True, ) - child = pexpect.spawn( + spawn = pexpect.spawn( "qemu-system-aarch64", [ "-cpu", @@ -51,17 +64,21 @@ def vm(): "-nographic", "-bios", "/usr/share/AAVMF/AAVMF_CODE.fd", + "-fsdev", + f"local,id=fsdev0,path={os.getcwd()},security_model=none", + "-device", + "virtio-9p-pci,fsdev=fsdev0,mount_tag=qcom-deb-images", ], ) - child.logfile = sys.stdout.buffer - yield child + spawn.logfile = sys.stdout.buffer + yield types.SimpleNamespace(spawn=spawn, logged_in=False) # No need to be nice; that would take time - child.kill(signal.SIGKILL) + spawn.kill(signal.SIGKILL) # If this blocks then we have a problem. Better to hang than build up # excess qemu processes that won't die. - child.wait() + spawn.wait() def test_password_reset_required(vm): @@ -69,16 +86,55 @@ def test_password_reset_required(vm): # https://github.com/qualcomm-linux/qcom-deb-images/issues/69 # This takes a minute or two on a ThinkPad T14s Gen 6 Snapdragon - vm.expect_exact("debian login:", timeout=240) - - vm.send("debian\r\n") - vm.expect_exact("Password:") - vm.send("debian\r\n") - vm.expect_exact("You are required to change your password immediately") - vm.expect_exact("Current password:") - vm.send("debian\r\n") - vm.expect_exact("New password:") - vm.send("new password\r\n") - vm.expect_exact("Retype new password:") - vm.send("new password\r\n") - vm.expect_exact("debian@debian:~$") + vm.spawn.expect_exact("debian login:", timeout=240) + + vm.spawn.send("debian\r\n") + vm.spawn.expect_exact("Password:") + vm.spawn.send("debian\r\n") + vm.spawn.expect_exact("You are required to change your password immediately") + vm.spawn.expect_exact("Current password:") + vm.spawn.send("debian\r\n") + vm.spawn.expect_exact("New password:") + vm.spawn.send("new password\r\n") + vm.spawn.expect_exact("Retype new password:") + vm.spawn.send("new password\r\n") + vm.spawn.expect_exact("debian@debian:~$") + + vm.logged_in = True + + +@pytest.fixture(scope="module") +def logged_in_vm(vm): + if not vm.logged_in: + pytest.skip("Password reset test did not run or failed") + return vm + + +def test_using_guest_tests(logged_in_vm): + """Run the tests in qemu_guest_test.py inside the qemu guest""" + # Statement of test success and failure that are unlikely to appear by + # accident + SUCCESS_NOTICE = "All ci/qemu_guest_test.py tests passed" + FAILURE_NOTICE = "Some ci/qemu_guest_test.py tests failed" + # We use apt-get -U here and the apt_dependencies fixture in + # qemu_guest_test.py relies on this. + SCRIPT = f"""sudo -i sh </etc/sudoers.d/90-debos ) + # See: https://github.com/qualcomm-linux/qcom-deb-images/issues/193 + - action: run + description: Configure sudo to use !fqdn + command: | + set -eux + echo "Defaults !fqdn" > ${ROOTDIR}/etc/sudoers.d/disable-fqdn + # NB: Recommends pull in way too many packages, and we don't need to follow # Recommends reaching outside of this Priority level - action: apt diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..34c9047 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,7 @@ +[tool.pytest.ini_options] +# See ci/qemu_test.py and ci/qemu_test_guest.py for details on arrangements for +# guest tests. +addopts = "-m 'not guest'" +markers = [ + "guest: Tests that run from inside a built image" +]