diff --git a/nixos/lib/test-driver/.coveragerc b/nixos/lib/test-driver/.coveragerc new file mode 100644 index 0000000000000..ac895dbef65d0 --- /dev/null +++ b/nixos/lib/test-driver/.coveragerc @@ -0,0 +1,5 @@ +[paths] +source = + ./test_driver/ + /build/test_driver/ + **/site-packages/test_driver/ diff --git a/nixos/lib/test-driver/.gitignore b/nixos/lib/test-driver/.gitignore new file mode 100644 index 0000000000000..350f588cb33fc --- /dev/null +++ b/nixos/lib/test-driver/.gitignore @@ -0,0 +1,3 @@ +.mutmut-cache +.coverage +coverage.xml diff --git a/nixos/lib/test-driver/default.nix b/nixos/lib/test-driver/default.nix index 09d80deb85467..12f20debfdef1 100644 --- a/nixos/lib/test-driver/default.nix +++ b/nixos/lib/test-driver/default.nix @@ -46,4 +46,6 @@ python3Packages.buildPythonApplication { echo -e "\x1b[32m## run black\x1b[0m" black --check --diff . ''; + + passthru.tests = nixosTests.nixos-test-driver.driver; } diff --git a/nixos/lib/test-driver/mutmut_config.py b/nixos/lib/test-driver/mutmut_config.py new file mode 100644 index 0000000000000..d84a0a0461a96 --- /dev/null +++ b/nixos/lib/test-driver/mutmut_config.py @@ -0,0 +1,4 @@ +def pre_mutation(context): + line = context.current_source_line.strip() + if line.startswith("self.log") or line.startswith("raise") or ".nested" in line: + context.skip = True diff --git a/nixos/lib/test-driver/run_mutmut.sh b/nixos/lib/test-driver/run_mutmut.sh new file mode 100755 index 0000000000000..3621799e7f2e0 --- /dev/null +++ b/nixos/lib/test-driver/run_mutmut.sh @@ -0,0 +1,12 @@ +#! /usr/bin/env nix-shell +#! nix-shell -I nixpkgs=../../.. -p black mutmut python3Packages.coverage -i bash + +set -euxo pipefail + +coverage combine --keep $(nix-build ../../.. -A nixosTests.nixos-test-driver) + +mutmut run \ + --runner "nix build -f ../../.. nixosTests.nixos-test-driver" \ + --paths-to-mutate "./test_driver/machine.py" \ + --use-coverage \ + --post-mutation "black test_driver -q" diff --git a/nixos/lib/test-driver/test_driver/driver.py b/nixos/lib/test-driver/test_driver/driver.py index 786821b0cc0d6..556d5b314c49f 100644 --- a/nixos/lib/test-driver/test_driver/driver.py +++ b/nixos/lib/test-driver/test_driver/driver.py @@ -3,9 +3,10 @@ import signal import tempfile import threading -from contextlib import contextmanager +from collections.abc import Callable, Iterator +from contextlib import AbstractContextManager, contextmanager from pathlib import Path -from typing import Any, Callable, ContextManager, Dict, Iterator, List, Optional, Union +from typing import Any from test_driver.logger import rootlog from test_driver.machine import Machine, NixStartScript, retry @@ -35,21 +36,47 @@ def pythonize_name(name: str) -> str: return re.sub(r"^[^A-z_]|[^A-z0-9_]", "_", name) +@contextmanager +def must_raise( + regex: str, + exception: type[BaseException] = Exception, +) -> Iterator[None]: + """Context manager that enforces that an exception is raised. + If no exception is raised, or the wrong kind of exception is raised, + or the exception string does not match the regex, an exception is raised. + """ + short_desc = f"{exception.__name__} {regex!r}" + msg = f"Expected {short_desc}" + + with rootlog.nested(f"Waiting for {short_desc}"): + try: + yield + except exception as e: + if re.search(regex, str(e)): + return + raise Exception(f"{msg}, but string {str(e)!r} did not match") from e + except Exception as e: + raise Exception( + f"{msg}, but {e!r} is not an instance of {exception!r}" + ) from e + raise Exception(f"{msg}, but no exception was raised") + + class Driver: """A handle to the driver that sets up the environment and runs the tests""" tests: str - vlans: List[VLan] - machines: List[Machine] - polling_conditions: List[PollingCondition] + vlans: list[VLan] + machines: list[Machine] + polling_conditions: list[PollingCondition] global_timeout: int race_timer: threading.Timer def __init__( self, - start_scripts: List[str], - vlans: List[int], + start_scripts: list[str], + vlans: list[int], tests: str, out_dir: Path, keep_vm_state: bool = False, @@ -66,7 +93,7 @@ def __init__( vlans = list(set(vlans)) self.vlans = [VLan(nr, tmp_dir) for nr in vlans] - def cmd(scripts: List[str]) -> Iterator[NixStartScript]: + def cmd(scripts: list[str]) -> Iterator[NixStartScript]: for s in scripts: yield NixStartScript(s) @@ -103,7 +130,7 @@ def subtest(self, name: str) -> Iterator[None]: rootlog.error(f'Test "{name}" failed with error: "{e}"') raise e - def test_symbols(self) -> Dict[str, Any]: + def test_symbols(self) -> dict[str, Any]: @contextmanager def subtest(name: str) -> Iterator[None]: return self.subtest(name) @@ -125,6 +152,7 @@ def subtest(name: str) -> Iterator[None]: serial_stdout_on=self.serial_stdout_on, polling_condition=self.polling_condition, Machine=Machine, # for typing + must_raise=must_raise, ) machine_symbols = {pythonize_name(m.name): m for m in self.machines} # If there's exactly one machine, make it available under the name @@ -187,7 +215,7 @@ def terminate_test(self) -> None: # to swallow them and prevent itself from terminating. os.kill(os.getpid(), signal.SIGTERM) - def create_machine(self, args: Dict[str, Any]) -> Machine: + def create_machine(self, args: dict[str, Any]) -> Machine: tmp_dir = get_tmp_dir() if args.get("startCommand"): @@ -218,11 +246,11 @@ def check_polling_conditions(self) -> None: def polling_condition( self, - fun_: Optional[Callable] = None, + fun_: Callable | None = None, *, seconds_interval: float = 2.0, - description: Optional[str] = None, - ) -> Union[Callable[[Callable], ContextManager], ContextManager]: + description: str | None = None, + ) -> Callable[[Callable], AbstractContextManager] | AbstractContextManager: driver = self class Poll: diff --git a/nixos/lib/test-driver/test_driver/machine.py b/nixos/lib/test-driver/test_driver/machine.py index 529de41d892a9..125180bf691bb 100644 --- a/nixos/lib/test-driver/test_driver/machine.py +++ b/nixos/lib/test-driver/test_driver/machine.py @@ -84,6 +84,8 @@ ")": "shift-0x0B", } +DEFAULT_TIMEOUT = 900 + def make_command(args: list) -> str: return " ".join(map(shlex.quote, (map(str, args)))) @@ -120,7 +122,7 @@ def _perform_ocr_on_screenshot( return model_results -def retry(fn: Callable, timeout: int = 900) -> None: +def retry(fn: Callable, timeout: int = DEFAULT_TIMEOUT) -> None: """Call the given function repeatedly, with 1 second intervals, until it returns True or a timeout is reached. """ @@ -547,7 +549,7 @@ def execute( command: str, check_return: bool = True, check_output: bool = True, - timeout: Optional[int] = 900, + timeout: Optional[int] = DEFAULT_TIMEOUT, ) -> Tuple[int, str]: """ Execute a shell command, returning a list `(status, stdout)`. @@ -693,7 +695,7 @@ def fail(self, *commands: str, timeout: Optional[int] = None) -> str: output += out return output - def wait_until_succeeds(self, command: str, timeout: int = 900) -> str: + def wait_until_succeeds(self, command: str, timeout: int = DEFAULT_TIMEOUT) -> str: """ Repeat a shell command with 1-second intervals until it succeeds. Has a default timeout of 900 seconds which can be modified, e.g. @@ -712,7 +714,7 @@ def check_success(_: Any) -> bool: retry(check_success, timeout) return output - def wait_until_fails(self, command: str, timeout: int = 900) -> str: + def wait_until_fails(self, command: str, timeout: int = DEFAULT_TIMEOUT) -> str: """ Like `wait_until_succeeds`, but repeating the command until it fails. """ @@ -747,7 +749,9 @@ def get_tty_text(self, tty: str) -> str: ) return output - def wait_until_tty_matches(self, tty: str, regexp: str, timeout: int = 900) -> None: + def wait_until_tty_matches( + self, tty: str, regexp: str, timeout: int = DEFAULT_TIMEOUT + ) -> None: """Wait until the visible output on the chosen TTY matches regular expression. Throws an exception on timeout. """ @@ -775,7 +779,7 @@ def send_chars(self, chars: str, delay: Optional[float] = 0.01) -> None: for char in chars: self.send_key(char, delay, log=False) - def wait_for_file(self, filename: str, timeout: int = 900) -> None: + def wait_for_file(self, filename: str, timeout: int = DEFAULT_TIMEOUT) -> None: """ Waits until the file exists in the machine's file system. """ @@ -788,7 +792,7 @@ def check_file(_: Any) -> bool: retry(check_file, timeout) def wait_for_open_port( - self, port: int, addr: str = "localhost", timeout: int = 900 + self, port: int, addr: str = "localhost", timeout: int = DEFAULT_TIMEOUT ) -> None: """ Wait until a process is listening on the given TCP port and IP address @@ -1014,7 +1018,7 @@ def get_screen_text(self) -> str: """ return self._get_screen_text_variants([2])[0] - def wait_for_text(self, regex: str, timeout: int = 900) -> None: + def wait_for_text(self, regex: str, timeout: int = DEFAULT_TIMEOUT) -> None: """ Wait until the supplied regular expressions matches the textual contents of the screen by using optical character recognition (see @@ -1214,7 +1218,7 @@ def get_window_names(self) -> List[str]: r"xwininfo -root -tree | sed 's/.*0x[0-9a-f]* \"\([^\"]*\)\".*/\1/; t; d'" ).splitlines() - def wait_for_window(self, regexp: str, timeout: int = 900) -> None: + def wait_for_window(self, regexp: str, timeout: int = DEFAULT_TIMEOUT) -> None: """ Wait until an X11 window has appeared whose name matches the given regular expression, e.g., `wait_for_window("Terminal")`. diff --git a/nixos/lib/test-script-prepend.py b/nixos/lib/test-script-prepend.py index 15e59ce01047d..f89448ca11421 100644 --- a/nixos/lib/test-script-prepend.py +++ b/nixos/lib/test-script-prepend.py @@ -8,6 +8,7 @@ from typing import Callable, Iterator, ContextManager, Optional, List, Dict, Any, Union from typing_extensions import Protocol from pathlib import Path +import os class RetryProtocol(Protocol): @@ -25,6 +26,13 @@ def __call__( ) -> Union[Callable[[Callable], ContextManager], ContextManager]: raise Exception("This is just type information for the Nix test driver") +class MustRaiseProtocol(Protocol): + def __call__( + self, + message: str, + exception: type[BaseException] = Exception + ) -> ContextManager[None]: + raise Exception("This is just type information for the Nix test driver") start_all: Callable[[], None] subtest: Callable[[str], ContextManager[None]] @@ -39,4 +47,5 @@ def __call__( join_all: Callable[[], None] serial_stdout_off: Callable[[], None] serial_stdout_on: Callable[[], None] +must_raise: MustRaiseProtocol polling_condition: PollingConditionProtocol diff --git a/nixos/lib/testing-python.nix b/nixos/lib/testing-python.nix index f5222351518b5..0ab4e9cb693fa 100644 --- a/nixos/lib/testing-python.nix +++ b/nixos/lib/testing-python.nix @@ -49,6 +49,7 @@ rec { , skipLint ? false , passthru ? {} , meta ? {} + , makeTestDriver ? (x: x) , # For meta.position pos ? # position used in error messages and for meta.position (if meta.description or null != null diff --git a/nixos/lib/testing/driver.nix b/nixos/lib/testing/driver.nix index b6f01c38191d2..5c147ee7ba0d0 100644 --- a/nixos/lib/testing/driver.nix +++ b/nixos/lib/testing/driver.nix @@ -4,13 +4,13 @@ let # Reifies and correctly wraps the python test driver for # the respective qemu version and with or without ocr support - testDriver = hostPkgs.callPackage ../test-driver { + initialTestDriver = hostPkgs.callPackage ../test-driver { inherit (config) enableOCR extraPythonPackages; qemu_pkg = config.qemu.package; imagemagick_light = hostPkgs.imagemagick_light.override { inherit (hostPkgs) libtiff; }; tesseract4 = hostPkgs.tesseract4.override { enableLanguages = [ "eng" ]; }; }; - + testDriver = config.makeTestDriver initialTestDriver; vlans = map (m: ( m.virtualisation.vlans ++ @@ -109,6 +109,13 @@ in defaultText = literalMD "set by the test framework"; }; + makeTestDriver = mkOption { + description = mdDoc "Function that returns the package containing the test driver that will perform the testing instrumentation"; + type = types.functionTo types.package; + defaultText = literalMD "set by the test framework"; + internal = true; + }; + hostPkgs = mkOption { description = mdDoc "Nixpkgs attrset used outside the nodes."; type = types.raw; @@ -196,6 +203,7 @@ in }; driver = withChecks driver; + makeTestDriver = lib.id; # make available on the test runner passthru.driver = config.driver; diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 2f6d5a8dae889..daed9f2fcdcae 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -98,6 +98,7 @@ in { [[ 143 = $(cat $failed/testBuildFailure.exit) ]] touch $out ''; + driver = handleTest ./nixos-test-driver {}; }; # NixOS vm tests and non-vm unit tests diff --git a/nixos/tests/nixos-test-driver/common.py b/nixos/tests/nixos-test-driver/common.py new file mode 100644 index 0000000000000..aec551a14fccb --- /dev/null +++ b/nixos/tests/nixos-test-driver/common.py @@ -0,0 +1,29 @@ +from subprocess import check_call +from contextlib import contextmanager +from pathlib import Path + +exampleDir = Path("/tmp/exampleDir") +exampleDir.mkdir(parents=True, exist_ok=True) + +exampleFile = exampleDir / "exampleFile" +exampleFile.write_bytes(bytes(range(256))) +assert exampleFile.exists() + + +def host_files_same(path1, path2): + return check_call(["diff", path1, path2]) + + +@contextmanager +def no_sleep(): + import time + + old_sleep = time.sleep + time.sleep = lambda x: None + yield + time.sleep = old_sleep + + +out = Path(os.environ.get("out", ".")).resolve() + +start_all() diff --git a/nixos/tests/nixos-test-driver/coverage-driver.nix b/nixos/tests/nixos-test-driver/coverage-driver.nix new file mode 100644 index 0000000000000..8356808e8e3b5 --- /dev/null +++ b/nixos/tests/nixos-test-driver/coverage-driver.nix @@ -0,0 +1,39 @@ +pkgs: driver: +let + coverage-rc = pkgs.writeText "coverage.rc" '' + [run] + include = + **/site-packages/test_driver/* + ''; + + coverage = "${pkgs.python3Packages.coverage}/bin/coverage"; + + pythonButItsCoverage = pkgs.writeScript "python-coverage" '' + #!${pkgs.runtimeShell} + set -euxo pipefail + COVERAGE_RCFILE=${coverage-rc} ${coverage} run -- "$@" + RETURN_CODE=$? + + if whoami | grep "nixbld"; then + cp .coverage $out + fi + + exit $RETURN_CODE + ''; + + coverageDriver = pkgs.runCommand "test-driver-coverage" { } '' + mkdir -p $out/bin + cat \ + <(echo "#!${pythonButItsCoverage}") \ + ${driver}/bin/.nixos-test-driver-wrapped \ + > $out/bin/.nixos-test-driver-wrapped + sed "s@${driver}@$out@" ${driver}/bin/nixos-test-driver > $out/bin/nixos-test-driver + chmod +x $out/bin/nixos-test-driver + chmod +x $out/bin/.nixos-test-driver-wrapped + # exit 1 + ''; +in +pkgs.symlinkJoin { + name = "${driver.name}-coverage"; + paths = [ coverageDriver driver ]; +} diff --git a/nixos/tests/nixos-test-driver/default.nix b/nixos/tests/nixos-test-driver/default.nix new file mode 100644 index 0000000000000..379ba13aa9faf --- /dev/null +++ b/nixos/tests/nixos-test-driver/default.nix @@ -0,0 +1,76 @@ +# All tests that don't need a specific config go here. + +{ pkgs, ... }: + +let + lib = pkgs.lib; + machines = import ./machines.nix; + mkTest = + name: + { script + , nodes ? { inherit (machines) basic; } + , enableOCR ? false + , skipTypeCheck ? false + }: + pkgs.testers.runNixOSTest + ({ pkgs, ... }: { + inherit name nodes enableOCR; + + testScript = builtins.readFile (pkgs.runCommand "testScript.py" + { preferLocalBuild = true; } + '' + cat ${./common.py} ${script} > $out + ''); + + makeTestDriver = lib.mkForce (import ./coverage-driver.nix pkgs); + inherit skipTypeCheck; + }); + + combineCoverage = ps: pkgs.runCommand "coverage" + { + nativeBuildInputs = [ pkgs.python3Packages.coverage ]; + coverageFiles = map (p: "${p}/.coverage") (lib.attrValues ps); + passthru = ps; + preferLocalBuild = true; + } + '' + COVERAGE_RCFILE=${../../lib/test-driver/.coveragerc} coverage combine --keep $coverageFiles + cp .coverage $out + ''; + + tests = lib.mapAttrs mkTest { + low-level = { script = ./low_level.py; }; + systemd = { script = ./systemd.py; }; + files = { script = ./files.py; }; + power = { script = ./power.py; }; + # TODO: typing the polling condition file is left as an exercise for people + # who want to have fun with typing assertions to force mypy to learn about + # the evolution of the types. + polling-condition = { script = ./polling_condition.py; skipTypeCheck = true; }; + driver = { + nodes = { }; + script = ./driver.py; + }; + tty = { + nodes = { inherit (machines) tty; }; + script = ./tty.py; + }; + ocr = { + nodes = { inherit (machines) x11; }; + script = ./ocr.py; + enableOCR = true; + }; + networking = { + nodes = { inherit (machines) client server; }; + script = ./networking.py; + }; + x11 = { + nodes = { inherit (machines) x11; }; + script = ./x11.py; + }; + }; + + combined = combineCoverage tests; +in + +combined diff --git a/nixos/tests/nixos-test-driver/driver.py b/nixos/tests/nixos-test-driver/driver.py new file mode 100644 index 0000000000000..161d58041feaf --- /dev/null +++ b/nixos/tests/nixos-test-driver/driver.py @@ -0,0 +1,39 @@ +# Basically everything that doesn't need a machine object to work. + +from test_driver import logger +import string + +with subtest("subtest"): + with must_raise("", SystemExit): + # Logger.error uses sys.exit. TODO: is this what we want? + with subtest("foobar"): + raise Exception("Oops.") + +with subtest("Logger"): + with subtest("sanitise"): + # No-op when no special characters are present + assert logger.rootlog.sanitise("foobar") == "foobar" + + # Remove all non-printable characters + assert all( + c in string.printable + for c in map(chr, range(128)) + if c == logger.rootlog.sanitise(c) + ) + +with subtest("must_raise"): + with subtest("success"), must_raise("Oops."): + raise Exception("Oops.") + + with subtest("failures"): + with must_raise("string .* did not match"): + with must_raise("Something"): + raise Exception("Another thing") + + with must_raise(".* is not an instance of .*"): + with must_raise("Oops.", ValueError): + raise IndexError("Oops.") + + with must_raise("no exception was raised"): + with must_raise("Oops."): + pass diff --git a/nixos/tests/nixos-test-driver/files.py b/nixos/tests/nixos-test-driver/files.py new file mode 100644 index 0000000000000..bd8f6900f2309 --- /dev/null +++ b/nixos/tests/nixos-test-driver/files.py @@ -0,0 +1,38 @@ +from tempfile import TemporaryDirectory + +with subtest("File copy methods"): + for method in [machine.copy_from_host, machine.copy_from_host_via_shell]: + with subtest( + f"{method.__name__}/copy_from_vm (file)" + ), TemporaryDirectory() as tmpdir: + method(str(exampleFile), "/tmp/file") + machine.copy_from_vm("/tmp/file", tmpdir) + host_files_same(exampleFile, Path(tmpdir, "file")) + machine.succeed("rm /tmp/file") + + with subtest("copy_from_host/copy_from_vm (dir)"), TemporaryDirectory() as tmpdir: + machine.copy_from_host(str(exampleDir), "/tmp/dir") + machine.copy_from_vm("/tmp/dir", tmpdir) + host_files_same(exampleFile, Path(tmpdir, "dir", "exampleFile")) + machine.succeed("rm -r /tmp/dir") + + with subtest("copy_from_vm"): + machine.succeed("touch /tmp/file") + machine.copy_from_vm("/tmp/file") + assert (out / "file").exists() + (out / "file").unlink() + machine.succeed("rm /tmp/file") + + with subtest("wait_for_file"): + machine.succeed("(sleep 5 && echo hello > /tmp/file) &") + machine.wait_for_file("/tmp/file") + machine.succeed("rm /tmp/file") + +with subtest("wait_for_file"): + with subtest("success"): + machine.succeed("(sleep 5 && echo hello > /tmp/file) >&2 &") + machine.wait_for_file("/tmp/file") + machine.succeed("rm /tmp/file") + + with subtest("failure"), no_sleep(), must_raise("action timed out"): + machine.wait_for_file("/will/never/exist", timeout=10) diff --git a/nixos/tests/nixos-test-driver/low_level.py b/nixos/tests/nixos-test-driver/low_level.py new file mode 100644 index 0000000000000..36aa6418b37ee --- /dev/null +++ b/nixos/tests/nixos-test-driver/low_level.py @@ -0,0 +1,49 @@ +from test_driver.machine import retry + +with subtest("various"): + # Test that "machine" alias is properly exported: + assert machine is basic + assert isinstance(machine, Machine) + +with subtest("retry"): + # Should not throw, because we are saving it in the last go. + with must_raise("action timed out"), no_sleep(): + retry(lambda x: False) + with no_sleep(): + retry(lambda x: x) + +with subtest("low-level commands"): + with subtest("execute"): + assert machine.execute("true") == (0, "") + assert machine.execute("false", check_return=False) == (-1, "") + + with subtest("succeed"): + r = machine.succeed("echo hello") + assert r == "hello\n" + machine.wait_until_succeeds("true") + with must_raise("command `false` failed"): + machine.succeed("false") + + r2 = machine.succeed("seq 10000").splitlines() + assert r2 == [str(i + 1) for i in range(10000)] + with subtest("fail"): + machine.fail("false") + machine.wait_until_fails("false") + with must_raise("command `true` unexpectedly succeeded"): + machine.fail("true") + + assert machine.is_up() + machine.sleep(2) + +# Not sure where this should go - we explicitly need OCR off so it can't go in +# the OCR test.: +with must_raise("OCR requested but enableOCR is false"): + machine.wait_for_text("foobar") + +with subtest("send_monitor_command"): + r = machine.send_monitor_command("help") + + # for both of these to succeed we need to grab >10kb of data, + # which ensures chunking is working as expected + assert "balloon" in r # The first output + assert "(qemu)" in r # The last output diff --git a/nixos/tests/nixos-test-driver/machines.nix b/nixos/tests/nixos-test-driver/machines.nix new file mode 100644 index 0000000000000..426cfdd5427f9 --- /dev/null +++ b/nixos/tests/nixos-test-driver/machines.nix @@ -0,0 +1,22 @@ +rec { + basic = { ... }: { + imports = [ ../../modules/profiles/minimal.nix ]; + }; + tty = { ... }: { + services.getty.autologinUser = "root"; + }; + server = { pkgs, ... }: { + environment.systemPackages = [ pkgs.python3 ]; + networking.firewall.allowedTCPPorts = [ 8080 ]; + }; + client = { pkgs, ... }: { + environment.systemPackages = [ pkgs.curl ]; + }; + x11 = { pkgs, ... }: { + imports = [ + ../common/x11.nix + ../common/user-account.nix + ../common/auto.nix + ]; + }; +} diff --git a/nixos/tests/nixos-test-driver/networking.py b/nixos/tests/nixos-test-driver/networking.py new file mode 100644 index 0000000000000..5ea57430b192e --- /dev/null +++ b/nixos/tests/nixos-test-driver/networking.py @@ -0,0 +1,30 @@ +from urllib.request import urlopen + +client.wait_for_unit("multi-user.target") +server.wait_for_unit("multi-user.target") + +with subtest("wait_for_closed_port"): + server.wait_for_closed_port(8080) + +# Prepare a server +server.succeed("echo Hello > file") +server.succeed("python -m http.server 8080 >&2 &") + +with subtest("wait_for_open_port"): + server.wait_for_open_port(8080) + +with subtest("simple"): + client.wait_until_succeeds("curl http://server:8080/file | grep Hello") + +with subtest("block"): + server.block() + client.fail("curl --max-time 5 http://server:8080/file") + +with subtest("unblock"): + server.unblock() + client.succeed("curl http://server:8080/file") + +with subtest("forward_port"): + server.forward_port(8888, 8080) + response = urlopen("http://localhost:8888/file").read() + assert response == b"Hello\n", f"unexpected response {response!r}" diff --git a/nixos/tests/nixos-test-driver/ocr.py b/nixos/tests/nixos-test-driver/ocr.py new file mode 100644 index 0000000000000..dc2bd15bc5da3 --- /dev/null +++ b/nixos/tests/nixos-test-driver/ocr.py @@ -0,0 +1,10 @@ +machine.wait_for_x() +machine.succeed("xterm >&2 &") + +with subtest("wait_for_text"): + with subtest("success"): + machine.wait_for_text("root@", timeout=10) + with subtest("failure"), no_sleep(), must_raise("timed out"): + machine.wait_for_text("It never comes", timeout=10) + +assert any("root@" in t for t in machine.get_screen_text_variants()) diff --git a/nixos/tests/nixos-test-driver/polling_condition.py b/nixos/tests/nixos-test-driver/polling_condition.py new file mode 100644 index 0000000000000..7ac83a0d9de3e --- /dev/null +++ b/nixos/tests/nixos-test-driver/polling_condition.py @@ -0,0 +1,58 @@ +with subtest("polling_condition"): + + with subtest("Failures"): + + @polling_condition + def polling_condition_always_false(): + return False + + @polling_condition + def polling_condition_raises(): + machine.succeed("false") + + for condition in [polling_condition_always_false, polling_condition_raises]: + with must_raise( + f"Polling condition failed: {condition.condition.description}" + ): + with condition: + machine.succeed("true") + + with subtest("Successes"): + + @polling_condition + def polling_condition_simple(): + machine.succeed("true") + + @polling_condition + def polling_condition_always_true(): + return True + + genericDescription = "This is a description" + + @polling_condition(description=genericDescription) + def polling_condition_explicit_description(): + pass + + @polling_condition + def polling_condition_docstring_description(): + "This is a description" + pass + + assert ( + polling_condition_explicit_description.condition.description + == genericDescription + ) + + assert ( + polling_condition_docstring_description.condition.description + == genericDescription + ) + + for condition in [ + polling_condition_simple, + polling_condition_always_true, + polling_condition_explicit_description, + polling_condition_docstring_description, + ]: + with condition: + machine.succeed("true") diff --git a/nixos/tests/nixos-test-driver/power.py b/nixos/tests/nixos-test-driver/power.py new file mode 100644 index 0000000000000..e2bdcf784a159 --- /dev/null +++ b/nixos/tests/nixos-test-driver/power.py @@ -0,0 +1,11 @@ +with subtest("power off related"): + for method in [machine.shutdown, machine.crash]: + with subtest(method.__name__): + machine.wait_for_unit("multi-user.target") + method() + machine.wait_for_shutdown() + assert not machine.booted + # No-op, but shouldn't throw an error: + method() + join_all() + # Re-start the machine: diff --git a/nixos/tests/nixos-test-driver/systemd.py b/nixos/tests/nixos-test-driver/systemd.py new file mode 100644 index 0000000000000..93370ae5ca1de --- /dev/null +++ b/nixos/tests/nixos-test-driver/systemd.py @@ -0,0 +1,7 @@ +machine.wait_for_unit("multi-user.target") + +with subtest("require_unit_state"): + with subtest("success"): + machine.require_unit_state("multi-user.target", "active") + with subtest("failure"), must_raise("to to be in state 'borked'"): + machine.require_unit_state("multi-user.target", "borked") diff --git a/nixos/tests/nixos-test-driver/tty.py b/nixos/tests/nixos-test-driver/tty.py new file mode 100644 index 0000000000000..5227b32013763 --- /dev/null +++ b/nixos/tests/nixos-test-driver/tty.py @@ -0,0 +1,24 @@ +import string +import re + +test_string = "".join(c for c in string.printable if c not in string.whitespace) + +with subtest("wait_until_tty_matches"): + with subtest("success"): + machine.wait_until_tty_matches("1", "root@") + + with subtest("failure"), no_sleep(), must_raise("action timed out"): + machine.wait_until_tty_matches("1", "asdfasdf", timeout=10) + +with subtest("send_chars"): + machine.send_chars("cat\n") + machine.sleep(2) + machine.send_chars(test_string + "\n") + machine.wait_until_tty_matches("1", re.escape(test_string), timeout=10) + machine.send_key("ctrl-d") + +with subtest("wait_for_console_text"): + with subtest("failure"), no_sleep(), must_raise("action timed out"): + machine.wait_for_console_text("foobar", timeout=10) + + # TODO: find a good way to test success here. diff --git a/nixos/tests/nixos-test-driver/x11.py b/nixos/tests/nixos-test-driver/x11.py new file mode 100644 index 0000000000000..9aaa1c610d9cc --- /dev/null +++ b/nixos/tests/nixos-test-driver/x11.py @@ -0,0 +1,23 @@ +with subtest("wait_for_x"): + machine.wait_for_x() + machine.sleep(3) + + +with subtest("wait_for_window"): + with subtest("failure"), no_sleep(), must_raise("action timed out"): + machine.wait_for_window("foobar", timeout=10) + + with subtest("success"): + machine.succeed("xterm >&2 &") + machine.sleep(3) + machine.wait_for_window("xterm") + +with subtest("get_window_names"): + assert "xterm" in machine.get_window_names() + + +with subtest("screenshot"): + machine.screenshot("foo") + assert (out / "foo.png").exists() + machine.screenshot("/tmp/bar.png") + assert Path("/tmp/bar.png").exists() diff --git a/pkgs/by-name/ci/circom/package.nix b/pkgs/by-name/ci/circom/package.nix new file mode 100644 index 0000000000000..6d74c5e268e3b --- /dev/null +++ b/pkgs/by-name/ci/circom/package.nix @@ -0,0 +1,27 @@ +{ lib +, rustPlatform +, fetchFromGitHub +}: + +rustPlatform.buildRustPackage rec { + pname = "circom"; + version = "2.1.6"; + + src = fetchFromGitHub { + owner = "iden3"; + repo = "circom"; + rev = "v${version}"; + hash = "sha256-2YusBWAYDrTvFHYIjKpALphhmtsec7jjKHb1sc9lt3Q="; + }; + + cargoHash = "sha256-G6z+DxIhmm1Kzv8EQCqvfGAhQn5Vrx9LXrl+bWBVKaM="; + doCheck = false; + + meta = with lib; { + description = "zkSnark circuit compiler"; + homepage = "https://github.com/iden3/circom"; + changelog = "https://github.com/iden3/circom/blob/${src.rev}/RELEASES.md"; + license = licenses.gpl3Only; + maintainers = with maintainers; [ raitobezarius ]; + }; +}