diff --git a/tests/test-driver/test-driver.py b/tests/test-driver/test-driver.py index bcb9564..b3b5cc5 100644 --- a/tests/test-driver/test-driver.py +++ b/tests/test-driver/test-driver.py @@ -216,10 +216,10 @@ class Machine: self.name = "machine" cmd = args.get("startCommand", None) if cmd: - match = re.search("bin/run-(.+)-vm$", cmd) + match = re.search("run-(.+)-vm$", cmd) if match: self.name = match.group(1) - + self.logger = args["log"] self.script = args.get("startCommand", self.create_startcommand(args)) tmp_dir = os.environ.get("TMPDIR", tempfile.gettempdir()) @@ -229,7 +229,10 @@ class Machine: os.makedirs(path, mode=0o700, exist_ok=True) return path - self.state_dir = create_dir("vm-state-{}".format(self.name)) + self.state_dir = os.path.join(tmp_dir, f"vm-state-{self.name}") + if not args.get("keepVmState", False): + self.cleanup_statedir() + os.makedirs(self.state_dir, mode=0o700, exist_ok=True) self.shared_dir = create_dir("shared-xchg") self.booted = False @@ -237,7 +240,6 @@ class Machine: self.pid: Optional[int] = None self.socket = None self.monitor: Optional[socket.socket] = None - self.logger: Logger = args["log"] self.serialQueue: "Queue[str]" = Queue() self.allow_reboot = args.get("allowReboot", False) @@ -428,15 +430,18 @@ class Machine: output += out return output - def fail(self, *commands: str) -> None: + def fail(self, *commands: str) -> str: """Execute each command and check that it fails.""" + output = "" for command in commands: with self.nested("must fail: {}".format(command)): - status, output = self.execute(command) + (status, out) = self.execute(command) if status == 0: raise Exception( "command `{}` unexpectedly succeeded".format(command) ) + output += out + return output def wait_until_succeeds(self, command: str) -> str: """Wait until a command returns success and return its output. @@ -627,11 +632,8 @@ class Machine: shutil.copytree(host_src, host_intermediate) else: shutil.copy(host_src, host_intermediate) - self.succeed("sync") self.succeed(make_command(["mkdir", "-p", vm_target.parent])) self.succeed(make_command(["cp", "-r", vm_intermediate, vm_target])) - # Make sure the cleanup is synced into VM - self.succeed("sync") def copy_from_vm(self, source: str, target_dir: str = "") -> None: """Copy a file from the VM (specified by an in-VM source path) to a path @@ -649,7 +651,6 @@ class Machine: # Copy the file to the shared directory inside VM self.succeed(make_command(["mkdir", "-p", vm_shared_temp])) self.succeed(make_command(["cp", "-r", vm_src, vm_intermediate])) - self.succeed("sync") abs_target = out_dir / target_dir / vm_src.name abs_target.parent.mkdir(exist_ok=True, parents=True) # Copy the file from the shared directory outside VM @@ -657,8 +658,6 @@ class Machine: shutil.copytree(intermediate, abs_target) else: shutil.copy(intermediate, abs_target) - # Make sure the cleanup is synced into VM - self.succeed("sync") def dump_tty_contents(self, tty: str) -> None: """Debugging: Dump the contents of the TTY @@ -706,6 +705,22 @@ class Machine: with self.nested("waiting for {} to appear on screen".format(regex)): retry(screen_matches) + def wait_for_console_text(self, regex: str) -> None: + self.log("waiting for {} to appear on console".format(regex)) + # Buffer the console output, this is needed + # to match multiline regexes. + console = io.StringIO() + while True: + try: + console.write(self.last_lines.get()) + except queue.Empty: + self.sleep(1) + continue + console.seek(0) + matches = re.search(regex, console.read()) + if matches is not None: + return + def send_key(self, key: str) -> None: key = CHAR_TO_KEY.get(key, key) self.send_monitor_command("sendkey {}".format(key)) @@ -769,11 +784,16 @@ class Machine: self.monitor, _ = self.monitor_socket.accept() self.shell, _ = self.shell_socket.accept() + # Store last serial console lines for use + # of wait_for_console_text + self.last_lines: Queue = Queue() + def process_serial_output() -> None: assert self.process.stdout is not None for _line in self.process.stdout: # Ignore undecodable bytes that may occur in boot menus line = _line.decode(errors="ignore").replace("\r", "").rstrip() + self.last_lines.put(line) eprint("{} # {}".format(self.name, line)) self.logger.enqueue({"msg": line, "machine": self.name}) self.serialQueue.put(line) @@ -787,6 +807,12 @@ class Machine: self.log("QEMU running (pid {})".format(self.pid)) + def cleanup_statedir(self) -> None: + if os.path.isdir(self.state_dir): + shutil.rmtree(self.state_dir) + self.logger.log(f"deleting VM state directory {self.state_dir}") + self.logger.log("if you want to keep the VM state, pass --keep-vm-state") + def shutdown(self) -> None: if not self.booted: return @@ -843,7 +869,8 @@ class Machine: retry(window_is_visible) def sleep(self, secs: int) -> None: - time.sleep(secs) + # We want to sleep in *guest* time, not *host* time. + self.succeed(f"sleep {secs}") def forward_port(self, host_port: int = 8080, guest_port: int = 80) -> None: """Forward a TCP port on the host to a TCP port on the guest. @@ -899,7 +926,8 @@ def run_tests() -> None: try: exec(tests, globals()) except Exception as e: - eprint("error: {}".format(str(e))) + eprint("error: ") + traceback.print_exc() sys.exit(1) else: ptpython.repl.embed(locals(), globals()) @@ -958,7 +986,8 @@ if __name__ == "__main__": continue log.log("killing {} (pid {})".format(machine.name, machine.pid)) machine.process.kill() - + for _, _, process, _ in vde_sockets: + process.terminate() log.close() tic = time.time()