From 7d14378255ab38b952fc33ff24580c0e0ccd92fd Mon Sep 17 00:00:00 2001 From: Pedro Henrique Penna Date: Wed, 11 Nov 2026 17:40:03 -0800 Subject: [PATCH] tests: check the CLI's behavior instead of its old output (SL3.11a) Two of the checks that run the nvx-rs that NVX_RS names still compared its output with the words that it inherited from nvx.py. The refusal of an egress policy that is not an object had to read "egress policy must be an object", the message of the Python code that nvx-rs replaced in #597. The help of nvx-rs run had to state "alpine 128" and the other guests' defaults, as nvx.py's help did before #902. Neither wording is a behavior: a reworded message or help failed the checks, while a refusal with another code, or a run that booted a guest with another memory default, passed them. The egress check now reads the refusal through nvx_rs as the scenarios do: its stable code, unsupported, and a message of one line without that code. The memory check now runs nvx-rs run --dry-run for each guest, through nvx_rs.command_line, on placeholder artifacts, and reads the --memory of the OpenVMM command that a run would launch: the memory that guests.py gives test-microvm. nvx_rs.query's docstring no longer says that nvx.py prints the message of the Python code that nvx-rs replaced. On Windows, with nvx-rs built from this tree, both checks passed. With the message and the help reworded, the old checks failed and the new ones passed; with the refusal's code changed to io and Alpine's default memory to 256 MiB, the old checks passed and the new ones failed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- scripts/nvx_tools/nvx_rs.py | 4 +-- scripts/test_nvx_tools.py | 65 +++++++++++++++++++++++++++---------- 2 files changed, 50 insertions(+), 19 deletions(-) diff --git a/scripts/nvx_tools/nvx_rs.py b/scripts/nvx_tools/nvx_rs.py index d0e5dfd89..83eff6caf 100644 --- a/scripts/nvx_tools/nvx_rs.py +++ b/scripts/nvx_tools/nvx_rs.py @@ -389,8 +389,8 @@ def query(arguments: Sequence[str]) -> object: """Run `nvx-rs --root ROOT ARGUMENTS`, and return the JSON that it prints. A failure raises NvxRsError with the code and the message of the error - that nvx-rs reported, which nvx.py prints after `error: ` as it printed the - message of the Python code that nvx-rs replaced. + that nvx-rs reported, which nvx.py prints after `error: `. Callers tell + failures apart by the code, since the message may change. """ command = command_line(arguments) try: diff --git a/scripts/test_nvx_tools.py b/scripts/test_nvx_tools.py index 5cb67b1ac..168fa4c5b 100644 --- a/scripts/test_nvx_tools.py +++ b/scripts/test_nvx_tools.py @@ -2452,21 +2452,49 @@ def test_usage_documents_every_option_of_nvx_rs(self): @unittest.skipUnless( os.environ.get("NVX_RS"), "set NVX_RS to an nvx-rs executable to run" ) - def test_run_help_states_each_guest_memory_default(self): + def test_run_boots_each_guest_with_its_default_memory(self): # nvx-rs run boots each guest with the memory that guests.py gives - # test-microvm, and its help says so. - completed = subprocess.run( - [os.environ["NVX_RS"], "run", "--help"], - capture_output=True, - text=True, - timeout=60, - check=True, - ) - help_text = " ".join(completed.stdout.split()) - for name in guests.GUEST_NAMES: - default = guests.guest_descriptor(name).default_memory_mib - with self.subTest(guest=name): - self.assertIn(f"{name} {default}", help_text) + # test-microvm, as the OpenVMM command of its dry run shows. A dry run + # launches nothing, so placeholders stand in for the artifacts. + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary).resolve() + build_dir = BuildConstants.BUILD_DIR.relative_to(BuildConstants.REPO_ROOT) + for relative in ( + common.openvmm_binary_path().relative_to(BuildConstants.REPO_ROOT), + build_dir / KernelBuildConstants.BINARY_NAME, + *( + build_dir / guests.guest_descriptor(name).initramfs_name + for name in guests.GUEST_NAMES + ), + ): + (root / relative).parent.mkdir(parents=True, exist_ok=True) + (root / relative).write_bytes(b"placeholder\n") + with patch.dict(os.environ): + os.environ.pop("NVX_OPENVMM", None) + for name in guests.GUEST_NAMES: + default = guests.guest_descriptor(name).default_memory_mib + with self.subTest(guest=name): + completed = subprocess.run( + nvx_rs.command_line( + ("run", "--guest", name, "--dry-run"), root=root + ), + stdin=subprocess.DEVNULL, + capture_output=True, + text=True, + timeout=60, + check=True, + ) + command = completed.stdout.splitlines()[0] + self.assertTrue(command.startswith(">> "), command) + words = command.split() + self.assertEqual( + [ + value + for option, value in itertools.pairwise(words) + if option == "--memory" + ], + [f"{default}M"], + ) def test_egress_policies_compile_to_the_rules_that_nvx_rs_prints(self): with patch.object( @@ -2521,11 +2549,14 @@ def test_the_real_nvx_rs_prints_what_nvx_py_reads(self): allow=("192.0.2.0/25",), deny=("198.51.100.0/24:icmp",) ), ) + # nvx-rs refuses a policy that is not an object with the stable + # code of its error, and nvx.py reads the message, one line + # without that code, whatever its wording. policy.write_text("[]", encoding="utf-8") - with self.assertRaisesRegex( - common.ScriptError, "^egress policy must be an object$" - ): + with self.assertRaises(nvx_rs.NvxRsError) as refused: nvx_rs.compile_egress_policy(policy) + self.assertEqual(refused.exception.code, "unsupported") + self.assertRegex(str(refused.exception), r"\A(?!error\[)[^\r\n]+\Z") # A caller may pass a path as a command line gave it, which may # be relative and start with "-". (root / "-policy.json").write_text(