From 3a28fefe7d4e7d842304ff4eee42c76593194b0a Mon Sep 17 00:00:00 2001 From: Jacek Galowicz Date: Fri, 6 Sep 2019 09:25:22 +0200 Subject: [PATCH 01/16] nixos/test: Port test driver to python Thanks @blitz and @jtraue for help with implementing machine methods --- nixos/lib/test-driver/test-driver.py | 762 +++++++++++++++++++++++++++ nixos/lib/testing-python.nix | 279 ++++++++++ nixos/tests/make-test-python.nix | 9 + 3 files changed, 1050 insertions(+) create mode 100644 nixos/lib/test-driver/test-driver.py create mode 100644 nixos/lib/testing-python.nix create mode 100644 nixos/tests/make-test-python.nix diff --git a/nixos/lib/test-driver/test-driver.py b/nixos/lib/test-driver/test-driver.py new file mode 100644 index 00000000000..16d4b0b907d --- /dev/null +++ b/nixos/lib/test-driver/test-driver.py @@ -0,0 +1,762 @@ +#! /somewhere/python3 + +from contextlib import contextmanager +from xml.sax.saxutils import XMLGenerator +import _thread +import atexit +import os +import pty +import queue +import re +import shutil +import socket +import subprocess +import sys +import tempfile +import time +import unicodedata + +CHAR_TO_KEY = { + "A": "shift-a", + "N": "shift-n", + "-": "0x0C", + "_": "shift-0x0C", + "B": "shift-b", + "O": "shift-o", + "=": "0x0D", + "+": "shift-0x0D", + "C": "shift-c", + "P": "shift-p", + "[": "0x1A", + "{": "shift-0x1A", + "D": "shift-d", + "Q": "shift-q", + "]": "0x1B", + "}": "shift-0x1B", + "E": "shift-e", + "R": "shift-r", + ";": "0x27", + ":": "shift-0x27", + "F": "shift-f", + "S": "shift-s", + "'": "0x28", + '"': "shift-0x28", + "G": "shift-g", + "T": "shift-t", + "`": "0x29", + "~": "shift-0x29", + "H": "shift-h", + "U": "shift-u", + "\\": "0x2B", + "|": "shift-0x2B", + "I": "shift-i", + "V": "shift-v", + ",": "0x33", + "<": "shift-0x33", + "J": "shift-j", + "W": "shift-w", + ".": "0x34", + ">": "shift-0x34", + "K": "shift-k", + "X": "shift-x", + "/": "0x35", + "?": "shift-0x35", + "L": "shift-l", + "Y": "shift-y", + " ": "spc", + "M": "shift-m", + "Z": "shift-z", + "\n": "ret", + "!": "shift-0x02", + "@": "shift-0x03", + "#": "shift-0x04", + "$": "shift-0x05", + "%": "shift-0x06", + "^": "shift-0x07", + "&": "shift-0x08", + "*": "shift-0x09", + "(": "shift-0x0A", + ")": "shift-0x0B", +} + + +def eprint(*args, **kwargs): + print(*args, file=sys.stderr, **kwargs) + + +def create_vlan(vlan_nr): + global log + log.log("starting VDE switch for network {}".format(vlan_nr)) + vde_socket = os.path.abspath("./vde{}.ctl".format(vlan_nr)) + pty_master, pty_slave = pty.openpty() + vde_process = subprocess.Popen( + ["vde_switch", "-s", vde_socket, "--dirmode", "0777"], + bufsize=1, + stdin=pty_slave, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + shell=False, + ) + fd = os.fdopen(pty_master, "w") + fd.write("version\n") + # TODO: perl version checks if this can be read from + # an if not, dies. we could hang here forever. Fix it. + vde_process.stdout.readline() + if not os.path.exists(os.path.join(vde_socket, "ctl")): + raise Exception("cannot start vde_switch") + + return (vlan_nr, vde_socket, vde_process, fd) + + +def retry(fn): + """Call the given function repeatedly, with 1 second intervals, + until it returns True or a timeout is reached. + """ + + for _ in range(900): + if fn(False): + return + time.sleep(1) + + if not fn(True): + raise Exception("action timed out") + + +class Logger: + def __init__(self): + self.logfile = os.environ.get("LOGFILE", "/dev/null") + self.logfile_handle = open(self.logfile, "wb") + self.xml = XMLGenerator(self.logfile_handle, encoding="utf-8") + self.queue = queue.Queue(1000) + + self.xml.startDocument() + self.xml.startElement("logfile", attrs={}) + + def close(self): + self.xml.endElement("logfile") + self.xml.endDocument() + self.logfile_handle.close() + + def sanitise(self, message): + return "".join(ch for ch in message if unicodedata.category(ch)[0] != "C") + + def maybe_prefix(self, message, attributes): + if "machine" in attributes: + return "{}: {}".format(attributes["machine"], message) + return message + + def log_line(self, message, attributes): + self.xml.startElement("line", attributes) + self.xml.characters(message) + self.xml.endElement("line") + + def log(self, message, attributes={}): + eprint(self.maybe_prefix(message, attributes)) + self.drain_log_queue() + self.log_line(message, attributes) + + def enqueue(self, message): + self.queue.put(message) + + def drain_log_queue(self): + try: + while True: + item = self.queue.get_nowait() + attributes = {"machine": item["machine"], "type": "serial"} + self.log_line(self.sanitise(item["msg"]), attributes) + except queue.Empty: + pass + + @contextmanager + def nested(self, message, attributes={}): + eprint(self.maybe_prefix(message, attributes)) + + self.xml.startElement("nest", attrs={}) + self.xml.startElement("head", attributes) + self.xml.characters(message) + self.xml.endElement("head") + + tic = time.time() + self.drain_log_queue() + yield + self.drain_log_queue() + toc = time.time() + self.log("({:.2f} seconds)".format(toc - tic)) + + self.xml.endElement("nest") + + +class Machine: + def __init__(self, args): + if "name" in args: + self.name = args["name"] + else: + self.name = "machine" + try: + cmd = args["startCommand"] + self.name = re.search("run-(.+)-vm$", cmd).group(1) + except KeyError: + pass + except AttributeError: + pass + + self.script = args.get("startCommand", self.create_startcommand(args)) + + tmp_dir = os.environ.get("TMPDIR", tempfile.gettempdir()) + + def create_dir(name): + path = os.path.join(tmp_dir, name) + os.makedirs(path, mode=0o700, exist_ok=True) + return path + + self.state_dir = create_dir("vm-state-{}".format(self.name)) + self.shared_dir = create_dir("xchg-shared") + + self.booted = False + self.connected = False + self.pid = None + self.socket = None + self.monitor = None + self.logger = args["log"] + self.allow_reboot = args.get("allowReboot", False) + + @staticmethod + def create_startcommand(args): + net_backend = "-netdev user,id=net0" + net_frontend = "-device virtio-net-pci,netdev=net0" + + if "netBackendArgs" in args: + net_backend += "," + args["netBackendArgs"] + + if "netFrontendArgs" in args: + net_frontend += "," + args["netFrontendArgs"] + + start_command = ( + "qemu-kvm -m 384 " + net_backend + " " + net_frontend + " $QEMU_OPTS " + ) + + if "hda" in args: + hda_path = os.path.abspath(args["hda"]) + if args.get("hdaInterface", "") == "scsi": + start_command += ( + "-drive id=hda,file=" + + hda_path + + ",werror=report,if=none " + + "-device scsi-hd,drive=hda " + ) + else: + start_command += ( + "-drive file=" + + hda_path + + ",if=" + + args["hdaInterface"] + + ",werror=report " + ) + + if "cdrom" in args: + start_command += "-cdrom " + args["cdrom"] + " " + + if "usb" in args: + start_command += ( + "-device piix3-usb-uhci -drive " + + "id=usbdisk,file=" + + args["usb"] + + ",if=none,readonly " + + "-device usb-storage,drive=usbdisk " + ) + if "bios" in args: + start_command += "-bios " + args["bios"] + " " + + start_command += args.get("qemuFlags", "") + + return start_command + + def is_up(self): + return self.booted and self.connected + + def log(self, msg): + self.logger.log(msg, {"machine": self.name}) + + def nested(self, msg, attrs={}): + my_attrs = {"machine": self.name} + my_attrs.update(attrs) + return self.logger.nested(msg, my_attrs) + + def wait_for_monitor_prompt(self): + while True: + answer = self.monitor.recv(1024).decode() + if answer.endswith("(qemu) "): + return answer + + def send_monitor_command(self, command): + message = ("{}\n".format(command)).encode() + self.log("sending monitor command: {}".format(command)) + self.monitor.send(message) + return self.wait_for_monitor_prompt() + + def wait_for_unit(self, unit, user=None): + while True: + info = self.get_unit_info(unit, user) + state = info["ActiveState"] + if state == "failed": + raise Exception('unit "{}" reached state "{}"'.format(unit, state)) + + if state == "inactive": + status, jobs = self.systemctl("list-jobs --full 2>&1", user) + if "No jobs" in jobs: + info = self.get_unit_info(unit) + if info["ActiveState"] == state: + raise Exception( + ( + 'unit "{}" is inactive and there ' "are no pending jobs" + ).format(unit) + ) + if state == "active": + return True + + def get_unit_info(self, unit, user=None): + status, lines = self.systemctl('--no-pager show "{}"'.format(unit), user) + if status != 0: + return None + + line_pattern = re.compile(r"^([^=]+)=(.*)$") + + def tuple_from_line(line): + match = line_pattern.match(line) + return match[1], match[2] + + return dict( + tuple_from_line(line) + for line in lines.split("\n") + if line_pattern.match(line) + ) + + def systemctl(self, q, user=None): + if user is not None: + q = q.replace("'", "\\'") + return self.execute( + ( + "su -l {} -c " + "$'XDG_RUNTIME_DIR=/run/user/`id -u` " + "systemctl --user {}'" + ).format(user, q) + ) + return self.execute("systemctl {}".format(q)) + + def execute(self, command): + self.connect() + + out_command = "( {} ); echo '|!EOF' $?\n".format(command) + self.shell.send(out_command.encode()) + + output = "" + status_code_pattern = re.compile(r"(.*)\|\!EOF\s+(\d+)") + + while True: + chunk = self.shell.recv(4096).decode() + match = status_code_pattern.match(chunk) + if match: + output += match[1] + status_code = int(match[2]) + return (status_code, output) + output += chunk + + def succeed(self, *commands): + """Execute each command and check that it succeeds.""" + for command in commands: + with self.nested("must succeed: {}".format(command)): + status, output = self.execute(command) + if status != 0: + self.log("output: {}".format(output)) + raise Exception( + "command `{}` failed (exit code {})".format(command, status) + ) + return output + + def fail(self, *commands): + """Execute each command and check that it fails.""" + for command in commands: + with self.nested("must fail: {}".format(command)): + status, output = self.execute(command) + if status == 0: + raise Exception( + "command `{}` unexpectedly succeeded".format(command) + ) + + def wait_until_succeeds(self, command): + with self.nested("waiting for success: {}".format(command)): + while True: + status, output = self.execute(command) + if status == 0: + return output + + def wait_until_fails(self, command): + with self.nested("waiting for failure: {}".format(command)): + while True: + status, output = self.execute(command) + if status != 0: + return output + + def wait_for_shutdown(self): + if not self.booted: + return + + with self.nested("waiting for the VM to power off"): + sys.stdout.flush() + self.process.wait() + + self.pid = None + self.booted = False + self.connected = False + + def get_tty_text(self, tty): + status, output = self.execute( + "fold -w$(stty -F /dev/tty{0} size | " + "awk '{{print $2}}') /dev/vcs{0}".format(tty) + ) + return output + + def wait_until_tty_matches(self, tty, regexp): + matcher = re.compile(regexp) + with self.nested("waiting for {} to appear on tty {}".format(regexp, tty)): + while True: + text = self.get_tty_text(tty) + if len(matcher.findall(text)) > 0: + return True + + def send_chars(self, chars): + with self.nested("sending keys ‘{}‘".format(chars)): + for char in chars: + self.send_key(char) + + def wait_for_file(self, filename): + with self.nested("waiting for file ‘{}‘".format(filename)): + while True: + status, _ = self.execute("test -e {}".format(filename)) + if status == 0: + return True + + def wait_for_open_port(self, port): + def port_is_open(_): + status, _ = self.execute("nc -z localhost {}".format(port)) + return status == 0 + + with self.nested("waiting for TCP port {}".format(port)): + retry(port_is_open) + + def wait_for_closed_port(self, port): + def port_is_closed(_): + status, _ = self.execute("nc -z localhost {}".format(port)) + return status != 0 + + retry(port_is_closed) + + def start_job(self, jobname, user=None): + return self.systemctl("start {}".format(jobname), user) + + def stop_job(self, jobname, user=None): + return self.systemctl("stop {}".format(jobname), user) + + def wait_for_job(self, jobname): + return self.wait_for_unit(jobname) + + def connect(self): + if self.connected: + return + + with self.nested("waiting for the VM to finish booting"): + self.start() + + tic = time.time() + self.shell.recv(1024) + # TODO: Timeout + toc = time.time() + + self.log("connected to guest root shell") + self.log("(connecting took {:.2f} seconds)".format(toc - tic)) + self.connected = True + + def screenshot(self, filename): + out_dir = os.environ.get("out", os.getcwd()) + word_pattern = re.compile(r"^\w+$") + if word_pattern.match(filename): + filename = os.path.join(out_dir, "{}.png".format(filename)) + tmp = "{}.ppm".format(filename) + + with self.nested( + "making screenshot {}".format(filename), + {"image": os.path.basename(filename)}, + ): + self.send_monitor_command("screendump {}".format(tmp)) + ret = subprocess.run("pnmtopng {} > {}".format(tmp, filename), shell=True) + os.unlink(tmp) + if ret.returncode != 0: + raise Exception("Cannot convert screenshot") + + def get_screen_text(self): + if shutil.which("tesseract") is None: + raise Exception("get_screen_text used but enableOCR is false") + + magick_args = ( + "-filter Catrom -density 72 -resample 300 " + + "-contrast -normalize -despeckle -type grayscale " + + "-sharpen 1 -posterize 3 -negate -gamma 100 " + + "-blur 1x65535" + ) + + tess_args = "-c debug_file=/dev/null --psm 11 --oem 2" + + with self.nested("performing optical character recognition"): + with tempfile.NamedTemporaryFile() as tmpin: + self.send_monitor_command("screendump {}".format(tmpin.name)) + + cmd = "convert {} {} tiff:- | tesseract - - {}".format( + magick_args, tmpin.name, tess_args + ) + ret = subprocess.run(cmd, shell=True, capture_output=True) + if ret.returncode != 0: + raise Exception( + "OCR failed with exit code {}".format(ret.returncode) + ) + + return ret.stdout.decode("utf-8") + + def wait_for_text(self, regex): + def screen_matches(last): + text = self.get_screen_text() + m = re.search(regex, text) + + if last and not m: + self.log("Last OCR attempt failed. Text was: {}".format(text)) + + return m + + with self.nested("waiting for {} to appear on screen".format(regex)): + retry(screen_matches) + + def send_key(self, key): + key = CHAR_TO_KEY.get(key, key) + self.send_monitor_command("sendkey {}".format(key)) + + def start(self): + if self.booted: + return + + self.log("starting vm") + + def create_socket(path): + if os.path.exists(path): + os.unlink(path) + s = socket.socket(family=socket.AF_UNIX, type=socket.SOCK_STREAM) + s.bind(path) + s.listen(1) + return s + + monitor_path = os.path.join(self.state_dir, "monitor") + self.monitor_socket = create_socket(monitor_path) + + shell_path = os.path.join(self.state_dir, "shell") + self.shell_socket = create_socket(shell_path) + + qemu_options = ( + " ".join( + [ + "" if self.allow_reboot else "-no-reboot", + "-monitor unix:{}".format(monitor_path), + "-chardev socket,id=shell,path={}".format(shell_path), + "-device virtio-serial", + "-device virtconsole,chardev=shell", + "-device virtio-rng-pci", + "-serial stdio" if "DISPLAY" in os.environ else "-nographic", + ] + ) + + " " + + os.environ.get("QEMU_OPTS", "") + ) + + environment = { + "QEMU_OPTS": qemu_options, + "SHARED_DIR": self.shared_dir, + "USE_TMPDIR": "1", + } + environment.update(dict(os.environ)) + + self.process = subprocess.Popen( + self.script, + bufsize=1, + stdin=subprocess.DEVNULL, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + shell=False, + cwd=self.state_dir, + env=environment, + ) + self.monitor, _ = self.monitor_socket.accept() + self.shell, _ = self.shell_socket.accept() + + def process_serial_output(): + for line in self.process.stdout: + line = line.decode().replace("\r", "").rstrip() + eprint("{} # {}".format(self.name, line)) + self.logger.enqueue({"msg": line, "machine": self.name}) + + _thread.start_new_thread(process_serial_output, ()) + + self.wait_for_monitor_prompt() + + self.pid = self.process.pid + self.booted = True + + self.log("QEMU running (pid {})".format(self.pid)) + + def shutdown(self): + if self.booted: + return + + self.shell.send("poweroff\n".encode()) + self.wait_for_shutdown() + + def crash(self): + if self.booted: + return + + self.log("forced crash") + self.send_monitor_command("quit") + self.wait_for_shutdown() + + def wait_for_x(self): + """Wait until it is possible to connect to the X server. Note that + testing the existence of /tmp/.X11-unix/X0 is insufficient. + """ + with self.nested("waiting for the X11 server"): + while True: + cmd = ( + "journalctl -b SYSLOG_IDENTIFIER=systemd | " + + 'grep "Reached target Current graphical"' + ) + status, _ = self.execute(cmd) + if status != 0: + continue + status, _ = self.execute("[ -e /tmp/.X11-unix/X0 ]") + if status == 0: + return + + def sleep(self, secs): + time.sleep(secs) + + def block(self): + """Make the machine unreachable by shutting down eth1 (the multicast + interface used to talk to the other VMs). We keep eth0 up so that + the test driver can continue to talk to the machine. + """ + self.send_monitor_command("set_link virtio-net-pci.1 off") + + def unblock(self): + """Make the machine reachable. + """ + self.send_monitor_command("set_link virtio-net-pci.1 on") + + +def create_machine(args): + global log + args["log"] = log + args["redirectSerial"] = os.environ.get("USE_SERIAL", "0") == "1" + return Machine(args) + + +def start_all(): + with log.nested("starting all VMs"): + for machine in machines: + machine.start() + + +def join_all(): + with log.nested("waiting for all VMs to finish"): + for machine in machines: + machine.wait_for_shutdown() + + +def test_script(): + exec(os.environ["testScript"]) + + +def run_tests(): + tests = os.environ.get("tests", None) + if tests is not None: + with log.nested("running the VM test script"): + try: + exec(tests) + except Exception as e: + eprint("error: {}".format(str(e))) + sys.exit(1) + else: + while True: + try: + value = input("> ") + exec(value) + except EOFError: + break + + # TODO: Collect coverage data + + for machine in machines: + if machine.is_up(): + machine.execute("sync") + + if nr_tests != 0: + log.log("{} out of {} tests succeeded".format(nr_succeeded, nr_tests)) + + +@contextmanager +def subtest(name): + global nr_tests + global nr_succeeded + + with log.nested(name): + nr_tests += 1 + try: + yield + nr_succeeded += 1 + return True + except Exception as e: + log.log("error: {}".format(str(e))) + + return False + + +if __name__ == "__main__": + global log + log = Logger() + + vlan_nrs = list(dict.fromkeys(os.environ["VLANS"].split())) + vde_sockets = [create_vlan(v) for v in vlan_nrs] + for nr, vde_socket, _, _ in vde_sockets: + os.environ["QEMU_VDE_SOCKET_{}".format(nr)] = vde_socket + + vm_scripts = sys.argv[1:] + machines = [create_machine({"startCommand": s}) for s in vm_scripts] + machine_eval = [ + "{0} = machines[{1}]".format(m.name, idx) for idx, m in enumerate(machines) + ] + exec("\n".join(machine_eval)) + + nr_tests = 0 + nr_succeeded = 0 + + @atexit.register + def clean_up(): + with log.nested("cleaning up"): + for machine in machines: + if machine.pid is None: + continue + log.log("killing {} (pid {})".format(machine.name, machine.pid)) + machine.process.kill() + + for _, _, process, _ in vde_sockets: + process.kill() + log.close() + + tic = time.time() + run_tests() + toc = time.time() + print("test script finished in {:.2f}s".format(toc - tic)) diff --git a/nixos/lib/testing-python.nix b/nixos/lib/testing-python.nix new file mode 100644 index 00000000000..5240cba116f --- /dev/null +++ b/nixos/lib/testing-python.nix @@ -0,0 +1,279 @@ +{ system +, pkgs ? import ../.. { inherit system config; } + # Use a minimal kernel? +, minimal ? false + # Ignored +, config ? {} + # Modules to add to each VM +, extraConfigurations ? [] }: + +with import ./build-vms.nix { inherit system pkgs minimal extraConfigurations; }; +with pkgs; + +let + jquery-ui = callPackage ./testing/jquery-ui.nix { }; + jquery = callPackage ./testing/jquery.nix { }; + +in rec { + + inherit pkgs; + + + testDriver = let + testDriverScript = ./test-driver/test-driver.py; + in stdenv.mkDerivation { + name = "nixos-test-driver"; + + nativeBuildInputs = [ makeWrapper ]; + buildInputs = [ python3 ]; + checkInputs = with python3Packages; [ pylint black ]; + + dontUnpack = true; + + preferLocalBuild = true; + + doCheck = true; + checkPhase = '' + pylint --errors-only ${testDriverScript} + black --check --diff ${testDriverScript} + ''; + + installPhase = + '' + mkdir -p $out/bin + cp ${testDriverScript} $out/bin/nixos-test-driver + chmod u+x $out/bin/nixos-test-driver + # TODO: copy user script part into this file (append) + + wrapProgram $out/bin/nixos-test-driver \ + --prefix PATH : "${lib.makeBinPath [ qemu_test vde2 netpbm coreutils ]}" \ + ''; + }; + + + # Run an automated test suite in the given virtual network. + # `driver' is the script that runs the network. + runTests = driver: + stdenv.mkDerivation { + name = "vm-test-run-${driver.testName}"; + + requiredSystemFeatures = [ "kvm" "nixos-test" ]; + + buildInputs = [ libxslt ]; + + buildCommand = + '' + mkdir -p $out/nix-support + + LOGFILE=$out/log.xml tests='exec(os.environ["testScript"])' ${driver}/bin/nixos-test-driver + + # Generate a pretty-printed log. + xsltproc --output $out/log.html ${./test-driver/log2html.xsl} $out/log.xml + ln -s ${./test-driver/logfile.css} $out/logfile.css + ln -s ${./test-driver/treebits.js} $out/treebits.js + ln -s ${jquery}/js/jquery.min.js $out/ + ln -s ${jquery}/js/jquery.js $out/ + ln -s ${jquery-ui}/js/jquery-ui.min.js $out/ + ln -s ${jquery-ui}/js/jquery-ui.js $out/ + + touch $out/nix-support/hydra-build-products + echo "report testlog $out log.html" >> $out/nix-support/hydra-build-products + + for i in */xchg/coverage-data; do + mkdir -p $out/coverage-data + mv $i $out/coverage-data/$(dirname $(dirname $i)) + done + ''; + }; + + + makeTest = + { testScript + , makeCoverageReport ? false + , enableOCR ? false + , name ? "unnamed" + , ... + } @ t: + + let + # A standard store path to the vm monitor is built like this: + # /tmp/nix-build-vm-test-run-$name.drv-0/vm-state-machine/monitor + # The max filename length of a unix domain socket is 108 bytes. + # This means $name can at most be 50 bytes long. + maxTestNameLen = 50; + testNameLen = builtins.stringLength name; + + testDriverName = with builtins; + if testNameLen > maxTestNameLen then + abort ("The name of the test '${name}' must not be longer than ${toString maxTestNameLen} " + + "it's currently ${toString testNameLen} characters long.") + else + "nixos-test-driver-${name}"; + + nodes = buildVirtualNetwork ( + t.nodes or (if t ? machine then { machine = t.machine; } else { })); + + testScript' = + # Call the test script with the computed nodes. + if lib.isFunction testScript + then testScript { inherit nodes; } + else testScript; + + vlans = map (m: m.config.virtualisation.vlans) (lib.attrValues nodes); + + vms = map (m: m.config.system.build.vm) (lib.attrValues nodes); + + ocrProg = tesseract4.override { enableLanguages = [ "eng" ]; }; + + imagemagick_tiff = imagemagick_light.override { inherit libtiff; }; + + # Generate onvenience wrappers for running the test driver + # interactively with the specified network, and for starting the + # VMs from the command line. + driver = runCommand testDriverName + { buildInputs = [ makeWrapper]; + testScript = testScript'; + preferLocalBuild = true; + testName = name; + } + '' + mkdir -p $out/bin + + echo -n "$testScript" > $out/test-script + ${python3Packages.black}/bin/black --check --diff $out/test-script + + ln -s ${testDriver}/bin/nixos-test-driver $out/bin/ + vms=($(for i in ${toString vms}; do echo $i/bin/run-*-vm; done)) + wrapProgram $out/bin/nixos-test-driver \ + --add-flags "''${vms[*]}" \ + ${lib.optionalString enableOCR + "--prefix PATH : '${ocrProg}/bin:${imagemagick_tiff}/bin'"} \ + --run "export testScript=\"\$(cat $out/test-script)\"" \ + --set VLANS '${toString vlans}' + ln -s ${testDriver}/bin/nixos-test-driver $out/bin/nixos-run-vms + wrapProgram $out/bin/nixos-run-vms \ + --add-flags "''${vms[*]}" \ + ${lib.optionalString enableOCR "--prefix PATH : '${ocrProg}/bin'"} \ + --set tests 'start_all(); join_all();' \ + --set VLANS '${toString vlans}' \ + ${lib.optionalString (builtins.length vms == 1) "--set USE_SERIAL 1"} + ''; # " + + passMeta = drv: drv // lib.optionalAttrs (t ? meta) { + meta = (drv.meta or {}) // t.meta; + }; + + test = passMeta (runTests driver); + report = passMeta (releaseTools.gcovReport { coverageRuns = [ test ]; }); + + nodeNames = builtins.attrNames nodes; + invalidNodeNames = lib.filter + (node: builtins.match "^[A-z_][A-z0-9_]+$" node == null) nodeNames; + + in + if lib.length invalidNodeNames > 0 then + throw '' + Cannot create machines out of (${lib.concatStringsSep ", " invalidNodeNames})! + All machines are referenced as perl variables in the testing framework which will break the + script when special characters are used. + + Please stick to alphanumeric chars and underscores as separation. + '' + else + (if makeCoverageReport then report else test) // { + inherit nodes driver test; + }; + + runInMachine = + { drv + , machine + , preBuild ? "" + , postBuild ? "" + , ... # ??? + }: + let + vm = buildVM { } + [ machine + { key = "run-in-machine"; + networking.hostName = "client"; + nix.readOnlyStore = false; + virtualisation.writableStore = false; + } + ]; + + buildrunner = writeText "vm-build" '' + source $1 + + ${coreutils}/bin/mkdir -p $TMPDIR + cd $TMPDIR + + exec $origBuilder $origArgs + ''; + + testScript = '' + startAll; + $client->waitForUnit("multi-user.target"); + ${preBuild} + $client->succeed("env -i ${bash}/bin/bash ${buildrunner} /tmp/xchg/saved-env >&2"); + ${postBuild} + $client->succeed("sync"); # flush all data before pulling the plug + ''; + + vmRunCommand = writeText "vm-run" '' + xchg=vm-state-client/xchg + ${coreutils}/bin/mkdir $out + ${coreutils}/bin/mkdir -p $xchg + + for i in $passAsFile; do + i2=''${i}Path + _basename=$(${coreutils}/bin/basename ''${!i2}) + ${coreutils}/bin/cp ''${!i2} $xchg/$_basename + eval $i2=/tmp/xchg/$_basename + ${coreutils}/bin/ls -la $xchg + done + + unset i i2 _basename + export | ${gnugrep}/bin/grep -v '^xchg=' > $xchg/saved-env + unset xchg + + export tests='${testScript}' + ${testDriver}/bin/nixos-test-driver ${vm.config.system.build.vm}/bin/run-*-vm + ''; # */ + + in + lib.overrideDerivation drv (attrs: { + requiredSystemFeatures = [ "kvm" ]; + builder = "${bash}/bin/sh"; + args = ["-e" vmRunCommand]; + origArgs = attrs.args; + origBuilder = attrs.builder; + }); + + + runInMachineWithX = { require ? [], ... } @ args: + let + client = + { ... }: + { + inherit require; + virtualisation.memorySize = 1024; + services.xserver.enable = true; + services.xserver.displayManager.slim.enable = false; + services.xserver.displayManager.auto.enable = true; + services.xserver.windowManager.default = "icewm"; + services.xserver.windowManager.icewm.enable = true; + services.xserver.desktopManager.default = "none"; + }; + in + runInMachine ({ + machine = client; + preBuild = + '' + $client->waitForX; + ''; + } // args); + + + simpleTest = as: (makeTest as).test; + +} diff --git a/nixos/tests/make-test-python.nix b/nixos/tests/make-test-python.nix new file mode 100644 index 00000000000..89897fe7e61 --- /dev/null +++ b/nixos/tests/make-test-python.nix @@ -0,0 +1,9 @@ +f: { + system ? builtins.currentSystem, + pkgs ? import ../.. { inherit system; config = {}; }, + ... +} @ args: + +with import ../lib/testing-python.nix { inherit system pkgs; }; + +makeTest (if pkgs.lib.isFunction f then f (args // { inherit pkgs; inherit (pkgs) lib; }) else f) From ac97edf013368c10c0c0978780d89b60bc520900 Mon Sep 17 00:00:00 2001 From: Florian Klink Date: Mon, 4 Nov 2019 22:49:45 +0100 Subject: [PATCH 02/16] nixos/test: use ptpython as repl --- nixos/lib/test-driver/test-driver.py | 8 ++------ nixos/lib/testing-python.nix | 2 +- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/nixos/lib/test-driver/test-driver.py b/nixos/lib/test-driver/test-driver.py index 16d4b0b907d..45b7e229a5c 100644 --- a/nixos/lib/test-driver/test-driver.py +++ b/nixos/lib/test-driver/test-driver.py @@ -15,6 +15,7 @@ import sys import tempfile import time import unicodedata +import ptpython.repl CHAR_TO_KEY = { "A": "shift-a", @@ -690,12 +691,7 @@ def run_tests(): eprint("error: {}".format(str(e))) sys.exit(1) else: - while True: - try: - value = input("> ") - exec(value) - except EOFError: - break + ptpython.repl.embed(locals(), globals()) # TODO: Collect coverage data diff --git a/nixos/lib/testing-python.nix b/nixos/lib/testing-python.nix index 5240cba116f..21f6172e967 100644 --- a/nixos/lib/testing-python.nix +++ b/nixos/lib/testing-python.nix @@ -25,7 +25,7 @@ in rec { name = "nixos-test-driver"; nativeBuildInputs = [ makeWrapper ]; - buildInputs = [ python3 ]; + buildInputs = [ (python3.withPackages (p: [ p.ptpython ])) ]; checkInputs = with python3Packages; [ pylint black ]; dontUnpack = true; From 7d19c5aaa76a43f8c63c665c09601a6ad3cdbc01 Mon Sep 17 00:00:00 2001 From: Jacek Galowicz Date: Mon, 4 Nov 2019 23:50:50 +0100 Subject: [PATCH 03/16] nixos/test: Pythonify documentation --- .../running-nixos-tests-interactively.xml | 12 +-- .../development/writing-nixos-tests.xml | 87 ++++++++++--------- 2 files changed, 50 insertions(+), 49 deletions(-) diff --git a/nixos/doc/manual/development/running-nixos-tests-interactively.xml b/nixos/doc/manual/development/running-nixos-tests-interactively.xml index e390d62fde2..ea3ba0e4bf7 100644 --- a/nixos/doc/manual/development/running-nixos-tests-interactively.xml +++ b/nixos/doc/manual/development/running-nixos-tests-interactively.xml @@ -14,14 +14,14 @@ starting VDE switch for network 1 > - You can then take any Perl statement, e.g. + You can then take any Python statement, e.g. -> startAll -> testScript -> $machine->succeed("touch /tmp/foo") -> print($machine->succeed("pwd")) # Show stdout of command +> start_all() +> test_script() +> machine.succeed("touch /tmp/foo") +> print(machine.succeed("pwd")) # Show stdout of command - The function testScript executes the entire test script + The function test_script executes the entire test script and drops you back into the test driver command line upon its completion. This allows you to inspect the state of the VMs after the test (e.g. to debug the test script). diff --git a/nixos/doc/manual/development/writing-nixos-tests.xml b/nixos/doc/manual/development/writing-nixos-tests.xml index 6be2d0a4d23..24efd2e3273 100644 --- a/nixos/doc/manual/development/writing-nixos-tests.xml +++ b/nixos/doc/manual/development/writing-nixos-tests.xml @@ -8,7 +8,7 @@ A NixOS test is a Nix expression that has the following structure: -import ./make-test.nix { +import ./make-test-python.nix { # Either the configuration of a single machine: machine = @@ -27,11 +27,11 @@ import ./make-test.nix { testScript = '' - Perl code… + Python code… ''; } - The attribute testScript is a bit of Perl code that + The attribute testScript is a bit of Python code that executes the test (described below). During the test, it will start one or more virtual machines, the configuration of which is described by the attribute machine (if you need only one machine in your @@ -96,26 +96,27 @@ xlink:href="https://github.com/NixOS/nixpkgs/blob/master/nixos/modules/virtualis - The test script is a sequence of Perl statements that perform various + The test script is a sequence of Python statements that perform various actions, such as starting VMs, executing commands in the VMs, and so on. Each virtual machine is represented as an object stored in the variable - $name, where - name is the identifier of the machine (which is - just machine if you didn’t specify multiple machines - using the nodes attribute). For instance, the following - starts the machine, waits until it has finished booting, then executes a - command and checks that the output is more-or-less correct: + name if this is also the + identifier of the machine in the declarative config. + If you didn't specify multiple machines using the nodes + attribute, it is just machine. + The following example starts the machine, waits until it has finished booting, + then executes a command and checks that the output is more-or-less correct: -$machine->start; -$machine->waitForUnit("default.target"); -$machine->succeed("uname") =~ /Linux/ or die; +machine.start() +machine.wait_for_unit("default.target") +if not "Linux" in machine.succeed("uname"): + raise Exception("Wrong OS") The first line is actually unnecessary; machines are implicitly started when - you first execute an action on them (such as waitForUnit + you first execute an action on them (such as wait_for_unit or succeed). If you have multiple machines, you can speed up the test by starting them in parallel: -startAll; +start_all() @@ -187,7 +188,7 @@ startAll; - getScreenText + get_screen_text @@ -204,7 +205,7 @@ startAll; - sendMonitorCommand + send_monitor_command @@ -215,23 +216,23 @@ startAll; - sendKeys + send_keys Simulate pressing keys on the virtual keyboard, e.g., - sendKeys("ctrl-alt-delete"). + send_keys("ctrl-alt-delete"). - sendChars + send_chars Simulate typing a sequence of characters on the virtual keyboard, e.g., - sendKeys("foobar\n") will type the string + send_keys("foobar\n") will type the string foobar followed by the Enter key. @@ -272,7 +273,7 @@ startAll; - waitUntilSucceeds + wait_until_succeeds @@ -282,7 +283,7 @@ startAll; - waitUntilFails + wait_until_fails @@ -292,7 +293,7 @@ startAll; - waitForUnit + wait_for_unit @@ -302,7 +303,7 @@ startAll; - waitForFile + wait_for_file @@ -312,7 +313,7 @@ startAll; - waitForOpenPort + wait_for_open_port @@ -323,7 +324,7 @@ startAll; - waitForClosedPort + wait_for_closed_port @@ -333,7 +334,7 @@ startAll; - waitForX + wait_for_x @@ -343,13 +344,13 @@ startAll; - waitForText + wait_for_text Wait until the supplied regular expressions matches the textual contents of the screen by using optical character recognition (see - getScreenText). + get_screen_text). @@ -361,23 +362,23 @@ startAll; - waitForWindow + wait_for_window Wait until an X11 window has appeared whose name matches the given - regular expression, e.g., waitForWindow(qr/Terminal/). + regular expression, e.g., wait_for_window("Terminal"). - copyFileFromHost + copy_file_from_host Copies a file from host to machine, e.g., - copyFileFromHost("myfile", "/etc/my/important/file"). + copy_file_from_host("myfile", "/etc/my/important/file"). The first argument is the file on the host. The file needs to be @@ -397,8 +398,8 @@ startAll; -$machine->systemctl("list-jobs --no-pager"); // runs `systemctl list-jobs --no-pager` -$machine->systemctl("list-jobs --no-pager", "any-user"); // spawns a shell for `any-user` and runs `systemctl --user list-jobs --no-pager` +machine.systemctl("list-jobs --no-pager") # runs `systemctl list-jobs --no-pager` +machine.systemctl("list-jobs --no-pager", "any-user") # spawns a shell for `any-user` and runs `systemctl --user list-jobs --no-pager` @@ -408,14 +409,14 @@ $machine->systemctl("list-jobs --no-pager", "any-user"); // spawns a shell for ` To test user units declared by systemd.user.services the - optional $user argument can be used: + optional user argument can be used: -$machine->start; -$machine->waitForX; -$machine->waitForUnit("xautolock.service", "x-session-user"); +machine.start() +machine.wait_for_x() +machine.wait_for_unit("xautolock.service", "x-session-user") - This applies to systemctl, getUnitInfo, - waitForUnit, startJob and - stopJob. + This applies to systemctl, get_unit_info, + wait_for_unit, start_job and + stop_job. From be48c5c5714146debbb13038d7c125fcba1b5e38 Mon Sep 17 00:00:00 2001 From: Jacek Galowicz Date: Tue, 10 Sep 2019 16:00:04 +0200 Subject: [PATCH 04/16] nixos/login: Port test to python --- nixos/tests/login.nix | 92 ++++++++++++++++++------------------------- 1 file changed, 39 insertions(+), 53 deletions(-) diff --git a/nixos/tests/login.nix b/nixos/tests/login.nix index bd8ed23a7b8..d36c1a91be4 100644 --- a/nixos/tests/login.nix +++ b/nixos/tests/login.nix @@ -1,4 +1,4 @@ -import ./make-test.nix ({ pkgs, latestKernel ? false, ... }: +import ./make-test-python.nix ({ pkgs, latestKernel ? false, ... }: { name = "login"; @@ -12,62 +12,48 @@ import ./make-test.nix ({ pkgs, latestKernel ? false, ... }: sound.enable = true; # needed for the factl test, /dev/snd/* exists without them but udev doesn't care then }; - testScript = - '' - $machine->waitForUnit('multi-user.target'); - $machine->waitUntilSucceeds("pgrep -f 'agetty.*tty1'"); - $machine->screenshot("postboot"); + testScript = '' + machine.wait_for_unit("multi-user.target") + machine.wait_until_succeeds("pgrep -f 'agetty.*tty1'") + machine.screenshot("postboot") - subtest "create user", sub { - $machine->succeed("useradd -m alice"); - $machine->succeed("(echo foobar; echo foobar) | passwd alice"); - }; + with subtest("create user"): + machine.succeed("useradd -m alice") + machine.succeed("(echo foobar; echo foobar) | passwd alice") - # Check whether switching VTs works. - subtest "virtual console switching", sub { - $machine->fail("pgrep -f 'agetty.*tty2'"); - $machine->sendKeys("alt-f2"); - $machine->waitUntilSucceeds("[ \$(fgconsole) = 2 ]"); - $machine->waitForUnit('getty@tty2.service'); - $machine->waitUntilSucceeds("pgrep -f 'agetty.*tty2'"); - }; + with subtest("Check whether switching VTs works"): + machine.fail("pgrep -f 'agetty.*tty2'") + machine.send_key("alt-f2") + machine.wait_until_succeeds("[ $(fgconsole) = 2 ]") + machine.wait_for_unit("getty@tty2.service") + machine.wait_until_succeeds("pgrep -f 'agetty.*tty2'") - # Log in as alice on a virtual console. - subtest "virtual console login", sub { - $machine->waitUntilTTYMatches(2, "login: "); - $machine->sendChars("alice\n"); - $machine->waitUntilTTYMatches(2, "login: alice"); - $machine->waitUntilSucceeds("pgrep login"); - $machine->waitUntilTTYMatches(2, "Password: "); - $machine->sendChars("foobar\n"); - $machine->waitUntilSucceeds("pgrep -u alice bash"); - $machine->sendChars("touch done\n"); - $machine->waitForFile("/home/alice/done"); - }; + with subtest("Log in as alice on a virtual console"): + machine.wait_until_tty_matches(2, "login: ") + machine.send_chars("alice\n") + machine.wait_until_tty_matches(2, "login: alice") + machine.wait_until_succeeds("pgrep login") + machine.wait_until_tty_matches(2, "Password: ") + machine.send_chars("foobar\n") + machine.wait_until_succeeds("pgrep -u alice bash") + machine.send_chars("touch done\n") + machine.wait_for_file("/home/alice/done") - # Check whether systemd gives and removes device ownership as - # needed. - subtest "device permissions", sub { - $machine->succeed("getfacl -p /dev/snd/timer | grep -q alice"); - $machine->sendKeys("alt-f1"); - $machine->waitUntilSucceeds("[ \$(fgconsole) = 1 ]"); - $machine->fail("getfacl -p /dev/snd/timer | grep -q alice"); - $machine->succeed("chvt 2"); - $machine->waitUntilSucceeds("getfacl -p /dev/snd/timer | grep -q alice"); - }; + with subtest("Systemd gives and removes device ownership as needed"): + machine.succeed("getfacl /dev/snd/timer | grep -q alice") + machine.send_key("alt-f1") + machine.wait_until_succeeds("[ $(fgconsole) = 1 ]") + machine.fail("getfacl /dev/snd/timer | grep -q alice") + machine.succeed("chvt 2") + machine.wait_until_succeeds("getfacl /dev/snd/timer | grep -q alice") - # Log out. - subtest "virtual console logout", sub { - $machine->sendChars("exit\n"); - $machine->waitUntilFails("pgrep -u alice bash"); - $machine->screenshot("mingetty"); - }; - - # Check whether ctrl-alt-delete works. - subtest "ctrl-alt-delete", sub { - $machine->sendKeys("ctrl-alt-delete"); - $machine->waitForShutdown; - }; - ''; + with subtest("Virtual console logout"): + machine.send_chars("exit\n") + machine.wait_until_fails("pgrep -u alice bash") + machine.screenshot("mingetty") + with subtest("Check whether ctrl-alt-delete works"): + machine.send_key("ctrl-alt-delete") + machine.wait_for_shutdown() + ''; }) From 75d3e810d68418b27d3e888bafe5c033c11735c7 Mon Sep 17 00:00:00 2001 From: Jacek Galowicz Date: Tue, 10 Sep 2019 15:59:50 +0200 Subject: [PATCH 05/16] nixos/bittorrent: Port test to python --- nixos/tests/bittorrent.nix | 58 +++++++++++++++++++++++--------------- 1 file changed, 36 insertions(+), 22 deletions(-) diff --git a/nixos/tests/bittorrent.nix b/nixos/tests/bittorrent.nix index 3b1169a1b7f..e5be652c711 100644 --- a/nixos/tests/bittorrent.nix +++ b/nixos/tests/bittorrent.nix @@ -6,7 +6,7 @@ # which only works if the first client successfully uses the UPnP-IGD # protocol to poke a hole in the NAT. -import ./make-test.nix ({ pkgs, ... }: +import ./make-test-python.nix ({ pkgs, ... }: let @@ -108,42 +108,56 @@ in testScript = { nodes, ... }: '' - startAll; + start_all() # Wait for network and miniupnpd. - $router->waitForUnit("network-online.target"); - $router->waitForUnit("miniupnpd"); + router.wait_for_unit("network-online.target") + router.wait_for_unit("miniupnpd") # Create the torrent. - $tracker->succeed("mkdir /tmp/data"); - $tracker->succeed("cp ${file} /tmp/data/test.tar.bz2"); - $tracker->succeed("transmission-create /tmp/data/test.tar.bz2 --private --tracker http://${externalTrackerAddress}:6969/announce --outfile /tmp/test.torrent"); - $tracker->succeed("chmod 644 /tmp/test.torrent"); + tracker.succeed("mkdir /tmp/data") + tracker.succeed( + "cp ${file} /tmp/data/test.tar.bz2" + ) + tracker.succeed( + "transmission-create /tmp/data/test.tar.bz2 --private --tracker http://${externalTrackerAddress}:6969/announce --outfile /tmp/test.torrent" + ) + tracker.succeed("chmod 644 /tmp/test.torrent") # Start the tracker. !!! use a less crappy tracker - $tracker->waitForUnit("network-online.target"); - $tracker->waitForUnit("opentracker.service"); - $tracker->waitForOpenPort(6969); + tracker.wait_for_unit("network-online.target") + tracker.wait_for_unit("opentracker.service") + tracker.wait_for_open_port(6969) # Start the initial seeder. - $tracker->succeed("transmission-remote --add /tmp/test.torrent --no-portmap --no-dht --download-dir /tmp/data"); + tracker.succeed( + "transmission-remote --add /tmp/test.torrent --no-portmap --no-dht --download-dir /tmp/data" + ) # Now we should be able to download from the client behind the NAT. - $tracker->waitForUnit("httpd"); - $client1->waitForUnit("network-online.target"); - $client1->succeed("transmission-remote --add http://${externalTrackerAddress}/test.torrent --download-dir /tmp >&2 &"); - $client1->waitForFile("/tmp/test.tar.bz2"); - $client1->succeed("cmp /tmp/test.tar.bz2 ${file}"); + tracker.wait_for_unit("httpd") + client1.wait_for_unit("network-online.target") + client1.succeed( + "transmission-remote --add http://${externalTrackerAddress}/test.torrent --download-dir /tmp >&2 &" + ) + client1.wait_for_file("/tmp/test.tar.bz2") + client1.succeed( + "cmp /tmp/test.tar.bz2 ${file}" + ) # Bring down the initial seeder. - # $tracker->stopJob("transmission"); + # tracker.stop_job("transmission") # Now download from the second client. This can only succeed if # the first client created a NAT hole in the router. - $client2->waitForUnit("network-online.target"); - $client2->succeed("transmission-remote --add http://${externalTrackerAddress}/test.torrent --no-portmap --no-dht --download-dir /tmp >&2 &"); - $client2->waitForFile("/tmp/test.tar.bz2"); - $client2->succeed("cmp /tmp/test.tar.bz2 ${file}"); + client2.wait_for_unit("network-online.target") + client2.succeed( + "transmission-remote --add http://${externalTrackerAddress}/test.torrent --no-portmap --no-dht --download-dir /tmp >&2 &" + ) + client2.wait_for_file("/tmp/test.tar.bz2") + client2.succeed( + "cmp /tmp/test.tar.bz2 ${file}" + ) ''; }) From 8eead58520f45f55cec40b7d7f0ca47e458356a8 Mon Sep 17 00:00:00 2001 From: Jacek Galowicz Date: Tue, 10 Sep 2019 19:55:35 +0200 Subject: [PATCH 06/16] nixos/postgres: Port test to python --- nixos/tests/postgresql.nix | 44 +++++++++++++++++++++----------------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/nixos/tests/postgresql.nix b/nixos/tests/postgresql.nix index ae5d6d095ea..e71c3888288 100644 --- a/nixos/tests/postgresql.nix +++ b/nixos/tests/postgresql.nix @@ -3,7 +3,7 @@ pkgs ? import ../.. { inherit system config; } }: -with import ../lib/testing.nix { inherit system pkgs; }; +with import ../lib/testing-python.nix { inherit system pkgs; }; with pkgs.lib; let @@ -40,29 +40,33 @@ let backupName = if backup-all then "all" else "postgres"; backupService = if backup-all then "postgresqlBackup" else "postgresqlBackup-postgres"; in '' - sub check_count { - my ($select, $nlines) = @_; - return 'test $(sudo -u postgres psql postgres -tAc "' . $select . '"|wc -l) -eq ' . $nlines; - } + def check_count(statement, lines): + return 'test $(sudo -u postgres psql postgres -tAc "{}"|wc -l) -eq {}'.format( + statement, lines + ) + + + machine.start() + machine.wait_for_unit("postgresql") - $machine->start; - $machine->waitForUnit("postgresql"); # postgresql should be available just after unit start - $machine->succeed("cat ${test-sql} | sudo -u postgres psql"); - $machine->shutdown; # make sure that postgresql survive restart (bug #1735) - sleep(2); - $machine->start; - $machine->waitForUnit("postgresql"); - $machine->fail(check_count("SELECT * FROM sth;", 3)); - $machine->succeed(check_count("SELECT * FROM sth;", 5)); - $machine->fail(check_count("SELECT * FROM sth;", 4)); - $machine->succeed(check_count("SELECT xpath(\'/test/text()\', doc) FROM xmltest;", 1)); + machine.succeed( + "cat ${test-sql} | sudo -u postgres psql" + ) + machine.shutdown() # make sure that postgresql survive restart (bug #1735) + time.sleep(2) + machine.start() + machine.wait_for_unit("postgresql") + machine.fail(check_count("SELECT * FROM sth;", 3)) + machine.succeed(check_count("SELECT * FROM sth;", 5)) + machine.fail(check_count("SELECT * FROM sth;", 4)) + machine.succeed(check_count("SELECT xpath('/test/text()', doc) FROM xmltest;", 1)) # Check backup service - $machine->succeed("systemctl start ${backupService}.service"); - $machine->succeed("zcat /var/backup/postgresql/${backupName}.sql.gz | grep 'ok'"); - $machine->succeed("stat -c '%a' /var/backup/postgresql/${backupName}.sql.gz | grep 600"); - $machine->shutdown; + machine.succeed("systemctl start ${backupService}.service") + machine.succeed("zcat /var/backup/postgresql/${backupName}.sql.gz | grep 'ok'") + machine.succeed("stat -c '%a' /var/backup/postgresql/${backupName}.sql.gz | grep 600") + machine.shutdown() ''; }; From 9b33b5d38c3f2f93ff03e195b090ed1b25f6d51c Mon Sep 17 00:00:00 2001 From: Jana Traue Date: Sun, 27 Oct 2019 11:24:26 +0100 Subject: [PATCH 07/16] nixos/gitea: convert test to python --- nixos/tests/gitea.nix | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/nixos/tests/gitea.nix b/nixos/tests/gitea.nix index b8ab6dabc8c..ffbc07cfbb2 100644 --- a/nixos/tests/gitea.nix +++ b/nixos/tests/gitea.nix @@ -3,7 +3,7 @@ pkgs ? import ../.. { inherit system config; } }: -with import ../lib/testing.nix { inherit system pkgs; }; +with import ../lib/testing-python.nix { inherit system pkgs; }; with pkgs.lib; { @@ -18,11 +18,11 @@ with pkgs.lib; }; testScript = '' - startAll; + start_all() - $machine->waitForUnit('gitea.service'); - $machine->waitForOpenPort('3000'); - $machine->succeed("curl --fail http://localhost:3000/"); + machine.wait_for_unit("gitea.service") + machine.wait_for_open_port(3000) + machine.succeed("curl --fail http://localhost:3000/") ''; }; @@ -37,11 +37,11 @@ with pkgs.lib; }; testScript = '' - startAll; + start_all() - $machine->waitForUnit('gitea.service'); - $machine->waitForOpenPort('3000'); - $machine->succeed("curl --fail http://localhost:3000/"); + machine.wait_for_unit("gitea.service") + machine.wait_for_open_port(3000) + machine.succeed("curl --fail http://localhost:3000/") ''; }; @@ -56,12 +56,14 @@ with pkgs.lib; }; testScript = '' - startAll; + start_all() - $machine->waitForUnit('gitea.service'); - $machine->waitForOpenPort('3000'); - $machine->succeed("curl --fail http://localhost:3000/"); - $machine->succeed("curl --fail http://localhost:3000/user/sign_up | grep 'Registration is disabled. Please contact your site administrator.'"); + machine.wait_for_unit("gitea.service") + machine.wait_for_open_port(3000) + machine.succeed("curl --fail http://localhost:3000/") + machine.succeed( + "curl --fail http://localhost:3000/user/sign_up | grep 'Registration is disabled. Please contact your site administrator.'" + ) ''; }; } From 2af2d59fd611ae6ee3ed8e7867243bdd6945f020 Mon Sep 17 00:00:00 2001 From: Julian Stecklina Date: Sun, 27 Oct 2019 11:44:06 +0100 Subject: [PATCH 08/16] nixos/emacs-daemon: convert test to python --- nixos/tests/emacs-daemon.nix | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/nixos/tests/emacs-daemon.nix b/nixos/tests/emacs-daemon.nix index 3594e35e343..b89d9b1bde6 100644 --- a/nixos/tests/emacs-daemon.nix +++ b/nixos/tests/emacs-daemon.nix @@ -1,4 +1,4 @@ -import ./make-test.nix ({ pkgs, ...} : { +import ./make-test-python.nix ({ pkgs, ...} : { name = "emacs-daemon"; meta = with pkgs.stdenv.lib.maintainers; { maintainers = [ ]; @@ -21,25 +21,28 @@ import ./make-test.nix ({ pkgs, ...} : { environment.variables.TEST_SYSTEM_VARIABLE = "system variable"; }; - testScript = - '' - $machine->waitForUnit("multi-user.target"); + testScript = '' + machine.wait_for_unit("multi-user.target") # checks that the EDITOR environment variable is set - $machine->succeed("test \$(basename \"\$EDITOR\") = emacseditor"); + machine.succeed('test $(basename "$EDITOR") = emacseditor') # waits for the emacs service to be ready - $machine->waitUntilSucceeds("systemctl --user status emacs.service | grep 'Active: active'"); + machine.wait_until_succeeds( + "systemctl --user status emacs.service | grep 'Active: active'" + ) # connects to the daemon - $machine->succeed("emacsclient --create-frame \$EDITOR &"); + machine.succeed("emacsclient --create-frame $EDITOR &") # checks that Emacs shows the edited filename - $machine->waitForText("emacseditor"); + machine.wait_for_text("emacseditor") # makes sure environment variables are accessible from Emacs - $machine->succeed("emacsclient --eval '(getenv \"TEST_SYSTEM_VARIABLE\")'") =~ /system variable/ or die; + machine.succeed( + "emacsclient --eval '(getenv \"TEST_SYSTEM_VARIABLE\")' | grep -q 'system variable'" + ) - $machine->screenshot("emacsclient"); + machine.screenshot("emacsclient") ''; }) From 8b12f0ddcc02ed2fd5fa5fc8d465cae765668f04 Mon Sep 17 00:00:00 2001 From: Jana Traue Date: Sun, 27 Oct 2019 12:08:55 +0100 Subject: [PATCH 09/16] nixos/quake3: convert test to python --- nixos/tests/quake3.nix | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/nixos/tests/quake3.nix b/nixos/tests/quake3.nix index 4253ce4a867..4d57e219790 100644 --- a/nixos/tests/quake3.nix +++ b/nixos/tests/quake3.nix @@ -1,4 +1,4 @@ -import ./make-test.nix ({ pkgs, ...} : +import ./make-test-python.nix ({ pkgs, ...} : let @@ -59,37 +59,37 @@ rec { testScript = '' - startAll; + start_all() - $server->waitForUnit("quake3-server"); - $client1->waitForX; - $client2->waitForX; + server.wait_for_unit("quake3-server") + client1.wait_for_x() + client2.wait_for_x() - $client1->execute("quake3 +set r_fullscreen 0 +set name Foo +connect server &"); - $client2->execute("quake3 +set r_fullscreen 0 +set name Bar +connect server &"); + client1.execute("quake3 +set r_fullscreen 0 +set name Foo +connect server &") + client2.execute("quake3 +set r_fullscreen 0 +set name Bar +connect server &") - $server->waitUntilSucceeds("grep -q 'Foo.*entered the game' /tmp/log"); - $server->waitUntilSucceeds("grep -q 'Bar.*entered the game' /tmp/log"); + server.wait_until_succeeds("grep -q 'Foo.*entered the game' /tmp/log") + server.wait_until_succeeds("grep -q 'Bar.*entered the game' /tmp/log") - $server->sleep(10); # wait for a while to get a nice screenshot + server.sleep(10) # wait for a while to get a nice screenshot - $client1->block(); + client1.block() - $server->sleep(20); + server.sleep(20) - $client1->screenshot("screen1"); - $client2->screenshot("screen2"); + client1.screenshot("screen1") + client2.screenshot("screen2") - $client1->unblock(); + client1.unblock() - $server->sleep(10); + server.sleep(10) - $client1->screenshot("screen3"); - $client2->screenshot("screen4"); + client1.screenshot("screen3") + client2.screenshot("screen4") - $client1->shutdown(); - $client2->shutdown(); - $server->stopJob("quake3-server"); + client1.shutdown() + client2.shutdown() + server.stop_job("quake3-server") ''; }) From baec88a119949d5a4875a9063c0a199901c0415f Mon Sep 17 00:00:00 2001 From: Jana Traue Date: Sun, 27 Oct 2019 12:23:36 +0100 Subject: [PATCH 10/16] nixos/wireguard: convert default test to python --- nixos/tests/wireguard/default.nix | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/nixos/tests/wireguard/default.nix b/nixos/tests/wireguard/default.nix index b0797b96323..8206823a918 100644 --- a/nixos/tests/wireguard/default.nix +++ b/nixos/tests/wireguard/default.nix @@ -2,7 +2,7 @@ let wg-snakeoil-keys = import ./snakeoil-keys.nix; in -import ../make-test.nix ({ pkgs, ...} : { +import ../make-test-python.nix ({ pkgs, ...} : { name = "wireguard"; meta = with pkgs.stdenv.lib.maintainers; { maintainers = [ ma27 ]; @@ -86,12 +86,12 @@ import ../make-test.nix ({ pkgs, ...} : { }; testScript = '' - startAll; + start_all() - $peer0->waitForUnit("wireguard-wg0.service"); - $peer1->waitForUnit("wireguard-wg0.service"); + peer0.wait_for_unit("wireguard-wg0.service") + peer1.wait_for_unit("wireguard-wg0.service") - $peer1->succeed("ping -c5 fc00::1"); - $peer1->succeed("ping -c5 10.23.42.1") + peer1.succeed("ping -c5 fc00::1") + peer1.succeed("ping -c5 10.23.42.1") ''; }) From cdd4d4fadf0d2d0b8ce9c92fabf2a808d8a97e07 Mon Sep 17 00:00:00 2001 From: Jana Traue Date: Sun, 27 Oct 2019 12:35:15 +0100 Subject: [PATCH 11/16] nixos/wireguard: convert generated test to python --- nixos/tests/wireguard/generated.nix | 44 ++++++++++++++++------------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/nixos/tests/wireguard/generated.nix b/nixos/tests/wireguard/generated.nix index 897feafe3ff..a29afd2d466 100644 --- a/nixos/tests/wireguard/generated.nix +++ b/nixos/tests/wireguard/generated.nix @@ -1,4 +1,4 @@ -import ../make-test.nix ({ pkgs, ...} : { +import ../make-test-python.nix ({ pkgs, ...} : { name = "wireguard-generated"; meta = with pkgs.stdenv.lib.maintainers; { maintainers = [ ma27 grahamc ]; @@ -28,30 +28,34 @@ import ../make-test.nix ({ pkgs, ...} : { }; testScript = '' - startAll; + start_all() - $peer1->waitForUnit("wireguard-wg0.service"); - $peer2->waitForUnit("wireguard-wg0.service"); + peer1.wait_for_unit("wireguard-wg0.service") + peer2.wait_for_unit("wireguard-wg0.service") - my ($retcode, $peer1pubkey) = $peer1->execute("wg pubkey < /etc/wireguard/private"); - $peer1pubkey =~ s/\s+$//; - if ($retcode != 0) { - die "Could not read public key from peer1"; - } + retcode, peer1pubkey = peer1.execute("wg pubkey < /etc/wireguard/private") + if retcode != 0: + raise Exception("Could not read public key from peer1") - my ($retcode, $peer2pubkey) = $peer2->execute("wg pubkey < /etc/wireguard/private"); - $peer2pubkey =~ s/\s+$//; - if ($retcode != 0) { - die "Could not read public key from peer2"; - } + retcode, peer2pubkey = peer2.execute("wg pubkey < /etc/wireguard/private") + if retcode != 0: + raise Exception("Could not read public key from peer2") - $peer1->succeed("wg set wg0 peer $peer2pubkey allowed-ips 10.10.10.2/32 endpoint 192.168.1.2:12345 persistent-keepalive 1"); - $peer1->succeed("ip route replace 10.10.10.2/32 dev wg0 table main"); + peer1.succeed( + "wg set wg0 peer {} allowed-ips 10.10.10.2/32 endpoint 192.168.1.2:12345 persistent-keepalive 1".format( + peer2pubkey.strip() + ) + ) + peer1.succeed("ip route replace 10.10.10.2/32 dev wg0 table main") - $peer2->succeed("wg set wg0 peer $peer1pubkey allowed-ips 10.10.10.1/32 endpoint 192.168.1.1:12345 persistent-keepalive 1"); - $peer2->succeed("ip route replace 10.10.10.1/32 dev wg0 table main"); + peer2.succeed( + "wg set wg0 peer {} allowed-ips 10.10.10.1/32 endpoint 192.168.1.1:12345 persistent-keepalive 1".format( + peer1pubkey.strip() + ) + ) + peer2.succeed("ip route replace 10.10.10.1/32 dev wg0 table main") - $peer1->succeed("ping -c1 10.10.10.2"); - $peer2->succeed("ping -c1 10.10.10.1"); + peer1.succeed("ping -c1 10.10.10.2") + peer2.succeed("ping -c1 10.10.10.1") ''; }) From d4a5ea5219c548287d0c4f253e9b43b159199244 Mon Sep 17 00:00:00 2001 From: Jacek Galowicz Date: Sun, 27 Oct 2019 13:22:46 +0100 Subject: [PATCH 12/16] nixos/boot: Port test to python --- nixos/tests/boot.nix | 35 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/nixos/tests/boot.nix b/nixos/tests/boot.nix index 57d8006d7ac..c5040f3b31f 100644 --- a/nixos/tests/boot.nix +++ b/nixos/tests/boot.nix @@ -3,7 +3,7 @@ pkgs ? import ../.. { inherit system config; } }: -with import ../lib/testing.nix { inherit system pkgs; }; +with import ../lib/testing-python.nix { inherit system pkgs; }; with pkgs.lib; let @@ -17,11 +17,11 @@ let ]; }).config.system.build.isoImage; - perlAttrs = params: "{ ${concatStringsSep ", " (mapAttrsToList (name: param: "${name} => ${builtins.toJSON param}") params)} }"; + pythonDict = params: "\n {\n ${concatStringsSep ",\n " (mapAttrsToList (name: param: "\"${name}\": \"${param}\"") params)},\n }\n"; makeBootTest = name: extraConfig: let - machineConfig = perlAttrs ({ qemuFlags = "-m 768"; } // extraConfig); + machineConfig = pythonDict ({ qemuFlags = "-m 768"; } // extraConfig); in makeTest { inherit iso; @@ -29,16 +29,16 @@ let nodes = { }; testScript = '' - my $machine = createMachine(${machineConfig}); - $machine->start; - $machine->waitForUnit("multi-user.target"); - $machine->succeed("nix verify -r --no-trust /run/current-system"); + machine = create_machine(${machineConfig}) + machine.start() + machine.wait_for_unit("multi-user.target") + machine.succeed("nix verify -r --no-trust /run/current-system") - # Test whether the channel got installed correctly. - $machine->succeed("nix-instantiate --dry-run '' -A hello"); - $machine->succeed("nix-env --dry-run -iA nixos.procps"); + with subtest("Check whether the channel got installed correctly"): + machine.succeed("nix-instantiate --dry-run '' -A hello") + machine.succeed("nix-env --dry-run -iA nixos.procps") - $machine->shutdown; + machine.shutdown() ''; }; @@ -60,7 +60,7 @@ let config.system.build.netbootIpxeScript ]; }; - machineConfig = perlAttrs ({ + machineConfig = pythonDict ({ qemuFlags = "-boot order=n -m 2000"; netBackendArgs = "tftp=${ipxeBootDir},bootfile=netboot.ipxe"; } // extraConfig); @@ -68,12 +68,11 @@ let makeTest { name = "boot-netboot-" + name; nodes = { }; - testScript = - '' - my $machine = createMachine(${machineConfig}); - $machine->start; - $machine->waitForUnit("multi-user.target"); - $machine->shutdown; + testScript = '' + machine = create_machine(${machineConfig}) + machine.start() + machine.wait_for_unit("multi-user.target") + machine.shutdown() ''; }; in { From e5ee59626309a3af57fe1befbff75081f942f5e3 Mon Sep 17 00:00:00 2001 From: Jacek Galowicz Date: Sun, 27 Oct 2019 13:53:55 +0100 Subject: [PATCH 13/16] nixos/acme: Port test to python --- nixos/tests/acme.nix | 53 ++++++++++++++++++++++++-------------------- 1 file changed, 29 insertions(+), 24 deletions(-) diff --git a/nixos/tests/acme.nix b/nixos/tests/acme.nix index 206d97849f0..6bd315ff1ea 100644 --- a/nixos/tests/acme.nix +++ b/nixos/tests/acme.nix @@ -1,6 +1,6 @@ let commonConfig = ./common/letsencrypt/common.nix; -in import ./make-test.nix { +in import ./make-test-python.nix { name = "acme"; nodes = rec { @@ -90,39 +90,44 @@ in import ./make-test.nix { newServerSystem = nodes.webserver2.config.system.build.toplevel; switchToNewServer = "${newServerSystem}/bin/switch-to-configuration test"; in - # Note, waitForUnit does not work for oneshot services that do not have RemainAfterExit=true, + # Note, wait_for_unit does not work for oneshot services that do not have RemainAfterExit=true, # this is because a oneshot goes from inactive => activating => inactive, and never # reaches the active state. To work around this, we create some mock target units which # get pulled in by the oneshot units. The target units linger after activation, and hence we # can use them to probe that a oneshot fired. It is a bit ugly, but it is the best we can do '' - $client->start; - $letsencrypt->start; - $acmeStandalone->start; + client.start() + letsencrypt.start() + acmeStandalone.start() - $letsencrypt->waitForUnit("default.target"); - $letsencrypt->waitForUnit("pebble.service"); + letsencrypt.wait_for_unit("default.target") + letsencrypt.wait_for_unit("pebble.service") - subtest "can request certificate with HTTPS-01 challenge", sub { - $acmeStandalone->waitForUnit("default.target"); - $acmeStandalone->succeed("systemctl start acme-standalone.com.service"); - $acmeStandalone->waitForUnit("acme-finished-standalone.com.target"); - }; + with subtest("can request certificate with HTTPS-01 challenge"): + acmeStandalone.wait_for_unit("default.target") + acmeStandalone.succeed("systemctl start acme-standalone.com.service") + acmeStandalone.wait_for_unit("acme-finished-standalone.com.target") - $client->waitForUnit("default.target"); + client.wait_for_unit("default.target") - $client->succeed('curl https://acme-v02.api.letsencrypt.org:15000/roots/0 > /tmp/ca.crt'); - $client->succeed('curl https://acme-v02.api.letsencrypt.org:15000/intermediate-keys/0 >> /tmp/ca.crt'); + client.succeed("curl https://acme-v02.api.letsencrypt.org:15000/roots/0 > /tmp/ca.crt") + client.succeed( + "curl https://acme-v02.api.letsencrypt.org:15000/intermediate-keys/0 >> /tmp/ca.crt" + ) - subtest "Can request certificate for nginx service", sub { - $webserver->waitForUnit("acme-finished-a.example.com.target"); - $client->succeed('curl --cacert /tmp/ca.crt https://a.example.com/ | grep -qF "hello world"'); - }; + with subtest("Can request certificate for nginx service"): + webserver.wait_for_unit("acme-finished-a.example.com.target") + client.succeed( + "curl --cacert /tmp/ca.crt https://a.example.com/ | grep -qF 'hello world'" + ) - subtest "Can add another certificate for nginx service", sub { - $webserver->succeed("/run/current-system/fine-tune/child-1/bin/switch-to-configuration test"); - $webserver->waitForUnit("acme-finished-b.example.com.target"); - $client->succeed('curl --cacert /tmp/ca.crt https://b.example.com/ | grep -qF "hello world"'); - }; + with subtest("Can add another certificate for nginx service"): + webserver.succeed( + "/run/current-system/fine-tune/child-1/bin/switch-to-configuration test" + ) + webserver.wait_for_unit("acme-finished-b.example.com.target") + client.succeed( + "curl --cacert /tmp/ca.crt https://b.example.com/ | grep -qF 'hello world'" + ) ''; } From 986a1eb32b9e9f8c099de6153e13492be96820f4 Mon Sep 17 00:00:00 2001 From: Jacek Galowicz Date: Sun, 27 Oct 2019 13:56:33 +0100 Subject: [PATCH 14/16] nixos/ammonite: Port test to python --- nixos/tests/ammonite.nix | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nixos/tests/ammonite.nix b/nixos/tests/ammonite.nix index fedfde233e8..1955e42be5f 100644 --- a/nixos/tests/ammonite.nix +++ b/nixos/tests/ammonite.nix @@ -1,4 +1,4 @@ -import ./make-test.nix ({ pkgs, ...} : { +import ./make-test-python.nix ({ pkgs, ...} : { name = "ammonite"; meta = with pkgs.stdenv.lib.maintainers; { maintainers = [ nequissimus ]; @@ -13,8 +13,8 @@ import ./make-test.nix ({ pkgs, ...} : { }; testScript = '' - startAll; + start_all() - $amm->succeed("amm -c 'val foo = 21; println(foo * 2)' | grep 42") + amm.succeed("amm -c 'val foo = 21; println(foo * 2)' | grep 42") ''; }) From 48508da01e754f3ed3a7d2656bbc695ab66036bc Mon Sep 17 00:00:00 2001 From: Jacek Galowicz Date: Sun, 27 Oct 2019 14:20:06 +0100 Subject: [PATCH 15/16] nixos/automysqlackup: Port test to python --- nixos/tests/automysqlbackup.nix | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/nixos/tests/automysqlbackup.nix b/nixos/tests/automysqlbackup.nix index ada104a34de..224b93862fb 100644 --- a/nixos/tests/automysqlbackup.nix +++ b/nixos/tests/automysqlbackup.nix @@ -1,4 +1,4 @@ -import ./make-test.nix ({ pkgs, lib, ... }: +import ./make-test-python.nix ({ pkgs, lib, ... }: { name = "automysqlbackup"; @@ -15,20 +15,24 @@ import ./make-test.nix ({ pkgs, lib, ... }: }; testScript = '' - startAll; + start_all() # Need to have mysql started so that it can be populated with data. - $machine->waitForUnit("mysql.service"); + machine.wait_for_unit("mysql.service") - # Wait for testdb to be fully populated (5 rows). - $machine->waitUntilSucceeds("mysql -u root -D testdb -N -B -e 'select count(id) from tests' | grep -q 5"); + with subtest("Wait for testdb to be fully populated (5 rows)."): + machine.wait_until_succeeds( + "mysql -u root -D testdb -N -B -e 'select count(id) from tests' | grep -q 5" + ) - # Do a backup and wait for it to start - $machine->startJob("automysqlbackup.service"); - $machine->waitForJob("automysqlbackup.service"); + with subtest("Do a backup and wait for it to start"): + machine.start_job("automysqlbackup.service") + machine.wait_for_job("automysqlbackup.service") - # wait for backup file and check that data appears in backup - $machine->waitForFile("/var/backup/mysql/daily/testdb"); - $machine->succeed("${pkgs.gzip}/bin/zcat /var/backup/mysql/daily/testdb/daily_testdb_*.sql.gz | grep hello"); + with subtest("wait for backup file and check that data appears in backup"): + machine.wait_for_file("/var/backup/mysql/daily/testdb") + machine.succeed( + "${pkgs.gzip}/bin/zcat /var/backup/mysql/daily/testdb/daily_testdb_*.sql.gz | grep hello" + ) ''; }) From 40396a7e12419fc772063d2a3fa87ec7d14a46d9 Mon Sep 17 00:00:00 2001 From: Julian Stecklina Date: Sun, 27 Oct 2019 13:53:36 +0100 Subject: [PATCH 16/16] nixos/zfs: convert test to python --- nixos/tests/zfs.nix | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/nixos/tests/zfs.nix b/nixos/tests/zfs.nix index d7a08268e98..8f844aca416 100644 --- a/nixos/tests/zfs.nix +++ b/nixos/tests/zfs.nix @@ -7,7 +7,7 @@ with import ../lib/testing.nix { inherit system pkgs; }; let - makeTest = import ./make-test.nix; + makeTest = import ./make-test-python.nix; makeZfsTest = name: { kernelPackage ? pkgs.linuxPackages_latest @@ -34,12 +34,12 @@ let }; testScript = '' - $machine->succeed("modprobe zfs"); - $machine->succeed("zpool status"); + machine.succeed("modprobe zfs") + machine.succeed("zpool status") - $machine->succeed("ls /dev"); + machine.succeed("ls /dev") - $machine->succeed( + machine.succeed( "mkdir /tmp/mnt", "udevadm settle", @@ -55,9 +55,7 @@ let "umount /tmp/mnt", "zpool destroy rpool", "udevadm settle" - - ); - + ) '' + extraTest; }; @@ -70,8 +68,8 @@ in { unstable = makeZfsTest "unstable" { enableUnstable = true; extraTest = '' - $machine->succeed( - "echo password | zpool create -o altroot='/tmp/mnt' -O encryption=aes-256-gcm -O keyformat=passphrase rpool /dev/vdb1", + machine.succeed( + "echo password | zpool create -o altroot=\"/tmp/mnt\" -O encryption=aes-256-gcm -O keyformat=passphrase rpool /dev/vdb1", "zfs create -o mountpoint=legacy rpool/root", "mount -t zfs rpool/root /tmp/mnt", "udevadm settle", @@ -79,7 +77,7 @@ in { "umount /tmp/mnt", "zpool destroy rpool", "udevadm settle" - ); + ) ''; };