Repository navigation
nvx-rs: add the scenario tests that replace the CLI transcripts - #825
Conversation
There was a problem hiding this comment.
🟡 Changes recommended
The mapping guard can accept missing replacement cases, and stale-runtime assertion failures can leak the fake OpenVMM process.
2 open issues
What changed in this PR
This PR adds structured CLI scenario tests for nvx-rs, preparing to retire recorded transcripts under #728 while retaining the replay and leaving production behavior unchanged.
Changes:
- Adds a shared harness and table-driven checks for CLI output, validation, launch arguments, and sandbox state.
- Adds configuration-format fixtures and mappings from recorded scenarios to replacement tests.
- Updates test documentation and enables Windows command-line parsing support.
| File | Description |
|---|---|
nvx-rs/tests/fixtures/state/format-7.json |
Current export-plan configuration fixture. |
nvx-rs/tests/fixtures/state/format-6.json |
Legacy file-share configuration fixture. |
nvx-rs/tests/fixtures/state/format-5.json |
Legacy share-policy configuration fixture. |
nvx-rs/tests/fixtures/state/format-4.json |
Legacy multiple-share configuration fixture. |
nvx-rs/tests/fixtures/state/format-3.json |
Legacy caller-owned share fixture. |
nvx-rs/tests/fixtures/state/format-2.json |
Legacy read-only share fixture. |
nvx-rs/tests/fixtures/state/format-1.json |
Supported basic configuration fixture. |
nvx-rs/tests/fixtures/state/format-1-nvx-py.json |
Python configuration fixture without image bindings. |
nvx-rs/tests/fixtures/state/format-1-network.json |
Supported network configuration fixture. |
nvx-rs/tests/cli/validation.rs |
Checks validation failures leave the root unchanged. |
nvx-rs/tests/cli/usage.rs |
Checks parser errors and help output. |
nvx-rs/tests/cli/openvmm.rs |
Defines launch-argument and report assertions. |
nvx-rs/tests/cli/one_shot.rs |
Checks launches, output, exit statuses, and reports. |
nvx-rs/tests/cli/harness.rs |
Shares roots, snapshots, process cleanup, and output parsing. |
nvx-rs/tests/cli/formats.rs |
Checks supported and rejected configuration formats. |
nvx-rs/tests/cli/egress.rs |
Checks policy compilation and refusals. |
nvx-rs/tests/cli/artifacts.rs |
Checks artifact paths and guest ordering. |
nvx-rs/tests/cli.rs |
Registers scenario modules and shares the harness with existing tests. |
nvx-rs/README.md |
Documents scenario tests and transcript retirement. |
nvx-rs/Cargo.toml |
Enables Windows shell parsing for tests. |
doc/project-structure.md |
Updates the test-layout description. |
Review effort: Balanced
927430e to
7c36bfd
Compare
7c36bfd to
20cc199
Compare
There was a problem hiding this comment.
🟡 Changes recommended
The #513 mapping claims regression coverage from a CLI test that cannot exercise the affected asynchronous collection path.
2 open issues
2 resolved since last review
Review effort: Balanced
20cc199 to
7456b6d
Compare
There was a problem hiding this comment.
🟢 Approval recommended
No blocking findings remain in the review, and these test-focused changes retain the existing transcript replay and interruption coverage without changing production behavior.
3 resolved since last review
Review effort: Balanced
tests/cli.rs built its artifact roots for itself: the replay of tests/fixtures/cli.json populated each root from the tree that the fixture records, and populated_root() gave the other tests a root with the same tree. The scenario tests that are to replace the replay (#728) need such roots in modules of their own, without the fixture that they retire. tests/cli/harness.rs now builds them. Root::new() writes the build provenance, layers, scratch image, shares, and report directory of a test root, and installs the fake OpenVMM as its OpenVMM, and Root::empty() returns an empty root for the replay to populate. Either ends, when it drops, the fake OpenVMM processes that the runtime records and launch journals below it name. Root::command() runs nvx-rs --root ROOT in the root, without the variables that change its output or its OpenVMM and without standard input, and Root::snapshot() records each path below the root with its kind, its POSIX mode, and a digest of its content. The replay checks that the tree that its fixture records is the one that Root::new() builds, apart from the fake OpenVMM that both install, so that the two cannot drift apart while both exist, and the tests that called populated_root() call Root::new(). fixture_roots_link_the_built_executable is now roots_link_the_built_fake_openvmm. A root's path must hold no comma, which --layer and --mount use as a separator; only the replay, whose recordings quote no part of the root, still needs a path without spaces or other special characters. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The replay of tests/fixtures/cli.json checks every error of nvx-rs against a transcript that Python recorded, byte for byte: the usage text, the message, and the files that each step leaves. A change of a message breaks those transcripts, as the error codes of #724 did, which #812 added to them by hand, while what callers rely on, the exit status, the code of the error, the option or value that it names, and that a refused command changes nothing, stays implicit in them (#728). tests/cli/usage.rs checks from a table that each of 61 command lines that the parser of nvx-rs sandbox or nvx-rs run refuses exits with status 2, writes nothing to standard output, and writes the usage of its parser and one error, argument-unrecognized, argument-missing, or argument-invalid, that names the option, and the value if any, at fault. Help prints the usage and the options of either command and exits with status 0, after an operation or options, and before an invalid value too. tests/cli/validation.rs checks from a table of 129 cases each check that refuses a command line before anything changes: options outside their operation, environments and environment files, egress policy files, network options, layers, scratch images, distro manifests, state directories, shares and their policies, owners, and targets, kernel command-line tokens and the share of the command line's budget that shares take, restores and memory, the artifacts that nvx-rs run needs, before and at launch, and which of several errors a command line reports first. Each case builds the root that it needs, then must exit with status 1, write nothing to standard output and one error with the case's code, such as unsupported, artifact-missing, or io, whose message holds the case's fragments, and leave every file below the root as it found it. Cases for Windows alone cover shares that the caller owns. The harness's Outcome holds how a run ended and checks a success, an error, or a usage error by its status, its streams, the code of its error, and stable fragments of its message, such as option names, values, and paths: callers tell errors apart by their codes and such fragments, since the wording of a message may change. Root::nvx_rs() runs nvx-rs with the placeholders {root}, {root/PATH}, and {exe} of its arguments replaced, and in_parallel() checks the cases of a table on four threads. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
A dry run prints the OpenVMM command that a run would launch, after ">> ", and launches nothing. The replay compares that line with the one that Python's shlex.join or subprocess.list2cmdline rendered for the recorded command, character for character, so it pins how each argument is quoted rather than that the line splits back into the arguments that OpenVMM would receive (#728). tests/cli/dry_run.rs checks 72 dry runs of nvx-rs sandbox run and nvx-rs run from a table. Each must succeed, leave the root as it found it, and print a line that splits, as the host splits a command line, into OpenVMM's program and options, each with its values in order, which must be those that the case derives from the minimal one-shot run, the cold boot, or the restore that OpenVMM's documentation describes. The cases cover layers, scratch images, workloads, environments, shared directories and files with their access and policies, egress policies, network options, kernel command lines, memory, processors, guests, snapshots, and the paths that nvx-rs run shows relative to the root. A property test runs nvx-rs run --dry-run with 64 arbitrary kernel command lines, with quotes, backslashes, and, on POSIX hosts, newlines, and checks that the line splits back into the --cmdline value that it was given. tests/cli/openvmm.rs parses OpenVMM command lines into their options, and builds the options of the minimal one-shot run, the cold boot, and the restore, the kernel command-line tokens of the sandbox profile and its default workload, and the nvx_share= tokens and child names of shared directories and files. The harness splits the >> line as a POSIX shell splits words, and on Windows with CommandLineToArgvW, for which the Windows dev-dependency on windows-sys enables Win32_UI_Shell. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
A one-shot run prints its OpenVMM command, runs OpenVMM on the terminal, and exits with OpenVMM's status, which a one-shot sandbox's OpenVMM takes from its workload. The replay compared what each run printed with Python's transcript, so what OpenVMM received was implicit in the >> line, and the images that a launch handed it went unchecked (#728). tests/cli/one_shot.rs runs 20 launches of nvx-rs sandbox run and nvx-rs run with the fake OpenVMM from a table. Each case checks the exit status, a workload's or OpenVMM's; the output after the >> line, byte for byte, and standard error, exactly; that the >> line splits into the very arguments that the fake OpenVMM recorded when it accepted them, with the options that the case expects; that the sandbox's layer and scratch image reach OpenVMM through files that the launch checked, and that OpenVMM read them whole; and that an outcome report that the case asks for follows OpenVMM's schema, with the network policy that the case expects, its host loopback among it, and with every step of the teardown that callers require, successful. The launches of nvx-rs run cover cold boots, every boot option, restores, missing snapshots, outcome reports, shares, an OpenVMM that refuses its options, and a relative NVX_OPENVMM. tests/cli/openvmm.rs now checks the sandbox blocks of a launch, the images that the fake OpenVMM read through them, and outcome reports, and the harness reads the arguments that the fake OpenVMM records and other JSON documents below the root. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
7456b6d to
630cfa1
Compare
630cfa1 to
baa8c97
Compare
|
Windows mutation results, as the Gates section promised. Run 40038822590 ran the replay alone, and run 40038833994 every other test of
The head's tests differ from 630cfa1's only in the mapping test, which exercises no production code, and a comment. |
There was a problem hiding this comment.
🟡 Changes recommended
The #373 regression mapping points to a shell case that cannot detect incorrect inherited PWD in a non-shell workload.
1 open issue
1 resolved since last review
Review effort: Balanced
The replay ran each lifecycle of a managed sandbox as recorded steps, and compared their output and the files that they left after it replaced process IDs, devices, file IDs, times, and random identifiers with placeholders, so it pinned the layout of every record without checking what the record says (#728). tests/cli/lifecycle.rs provisions, starts, executes in, stops, and deprovisions a sandbox of each configuration of a table: minimal, with one share, a share that the caller owns on POSIX hosts, several shares, share policies, a shared file, network and launch options, a proxy-only network, and relative and nested state directories. It checks the entries and POSIX modes of the state directory; the configuration that provision records, with bindings that verify against the root's distro layer and scratch image; the runtime record of a start, whose process runs and whose runtime identity holds the digests of the root's artifacts and the versions of nvx::contract; the control capability; the options with which the start launched OpenVMM and the mappings that its guest received; an execution; the outcome report of the stop; and that a second start and stop behave as the first did. A table of executions checks that exec exits with the workload's status and passes its output through: environments, working directories, among them a shell's PWD, timeouts, among them that of a workload that writes until its timeout, missing programs, and outcome reports. A refused execution fails with the code of its refusal, such as exec-refused or outcome-report-exists, changes nothing but the fake OpenVMM's log of sessions, and a marker in guest memory shows that its workload never ran. A table of refusals covers, each by its code, operations without a sandbox, those that a running sandbox refuses, a failed start, a start without OpenVMM, a malformed configuration, a state path that is not a directory or holds a foreign file, dry runs of lifecycle operations (#373), a recycled process ID (#369), and concurrent starts (#464). The check of a recycled process ID holds the OpenVMM whose record exec clears in a guard that kills it when the check fails, since a root ends only the processes that the records below it name. tests/cli/openvmm.rs builds the options of a managed start and checks its control endpoint, and the harness lists directories, finds the records of the fake OpenVMM, reads POSIX modes, and replaces the placeholders of JSON documents. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The replay started only the sandboxes that the build under test had just provisioned, so it read only the configuration formats that the build writes. start reads formats 1 and 7, and must refuse formats 2 to 6 of earlier builds, and the configurations that nvx.py wrote without bindings, before it launches OpenVMM (#728). tests/fixtures/state/ holds a hand-written config.json of each format: 1, which this build writes for a sandbox without shares, with launch options and with a network; 7, with an export plan for a shared directory with policies and a shared file; 2 to 6, in the shapes of the builds that wrote them; and the format 1 of nvx.py. tests/cli/formats.rs provisions a sandbox in a root, replaces its configuration with each fixture, whose placeholders name the paths of the root, the hypervisor of this host, and the bindings of the root's distro layer and scratch image, and checks that formats 1 and 7 start with the options that they configure, that OpenVMM received the root's images through checked files, and that exec runs in them, and that the others fail with format-unsupported and the error that their format earns, and leave the root as it was. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
baa8c97 to
cfcc540
Compare
nvx-rs egress compile prints the native rules into which the library lowers an egress policy file, and nvx-rs artifacts the paths of a root's artifacts. The replay compared both with what Python's functions printed when they were recorded, so a deliberate change of the lowering, or of the guests that the library knows, needed a new recording by hand (#728). tests/cli/egress.rs runs egress compile on nine policy files, empty, allowing, allowing and denying, invalid JSON, an invalid rule, an unknown field, a missing file, a directory, and a scoped IPv6 range (#462), and compares what it prints with the JSON of the rules of nvx::host::network::policy::compile_policy_file, and its errors with the code and the message of the library's refusal, as well as with the code that the case expects. tests/cli/artifacts.rs checks that artifacts prints, for a checkout and for an NVX_OPENVMM override, the root, OpenVMM, the guest kernel, and every guest in the order of nvx::host::artifacts::Guest::ALL, with its initramfs, package manifest, and distro layer and manifest, or null, in the exact layout of json.dumps(value, indent=2, sort_keys=True), which escapes every character outside printable ASCII, such as those of a root below a non-ASCII temporary directory. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
#728 retires tests/fixtures/cli.json and its replay once the scenario tests check what each scenario checked. tests/cli/scenarios.rs makes that claim checkable: it maps each of the 315 scenarios of the fixture's POSIX and Windows sections to the tests and table cases that check what it checked, and each regression that #722 traces, #369, #373, #462, #464, and #513, to the tests that guard against it. every_recorded_scenario_maps_to_a_test fails for a scenario that maps to no test, for a mapping of a scenario that the fixture lacks, for a scenario that maps twice, for a test that does not exist, and for a case that the table of its test lacks or names twice. It reads the cases from the tables themselves, so a case cannot pass for one whose name only appears elsewhere in a test's source: the tables of the scenario tests, and the names of their cases, are visible to it. The mapping retires with the replay. The PWD that #373 left at / for a workload in a working directory is not the CLI's to check: a shell repairs it, and the fake OpenVMM emulates the guest agent that sets it. For it, the mapping names the unit test of the sandbox agent's launch, which plans the environment of /bin/echo in a working directory, and the check of the managed-exec-config microVM scenario, which runs /usr/bin/env there in a real guest. For #513, it names the test of the asynchronous API of aci_edge_sandboxes. The published package of nvx lacks the sources of those last two, so the mapping test reads them at run time, in a checkout, which SOURCE-MANIFEST.json marks at its root, and fails there if it cannot; from the package, the CLI tests still compile, and the mapping test checks the other mappings. The README describes the scenario tests in its Testing section, the mapping at the end of Golden scenarios, and tests/cli/ and the configurations of tests/fixtures/state/ in its layout, as doc/project-structure.md does. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
cfcc540 to
e7be5d7
Compare
tests/fixtures/cli.json held the 315 scenarios that scripts recorded from the Python implementations of nvx.py sandbox and nvx.py run, and nvx_rs_reproduces_the_recorded_scenarios replayed them byte for byte once placeholders replaced the root and volatile values. The scenario tests of tests/cli/ check what callers rely on in their stead (#728): every scenario, and every regression that #722 traces, maps to them; on Linux and Windows, the replay reaches no line or branch of production code beyond the tests outside its family; and cargo-mutants finds no mutant of src/host/cli that the replay catches and the other tests of the CLI miss. The fixture goes, with the replay, its normalization and population helpers, and the mapping of tests/cli/scenarios.rs, whose tables of cases are private again, as is Root::empty(), which only Root::new() calls now. .gitattributes no longer marks the fixture as generated, and the test of the guest binaries' source identity names another file of tests/fixtures, root-tokens.json, which holds the cases of a root's boot tokens rather than behavior recorded from Python, so that no retirement of #722 removes it. The README describes the scenario tests in a section of their own, which replaces Golden scenarios and records how they replaced the transcripts, and its Python parity and Testing sections point to it. #828, which merged after #825 added the scenario tests, changed two messages that they check, and recorded the change in the transcripts alone: an unknown field of an egress policy is quoted as Rust quotes it, "other" where Python wrote 'other', and a policy file that cannot be read is named as it was given, ./ included, where Python normalized its path. The tests of egress compile and of the refusals of run expect these messages, which the transcripts no longer check. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Retire the Python-backed scalar compatibility layer of #730 in favor of native sandbox values while preserving the existing wire contracts. - Remove the public `Int` and `IntError` exports; use `Option<u64>` for the memory limit and `Option<u32>` for the process limit, including the bound that leaves room for the guest's init process. - Share ASCII-digit-guarded `FromStr` parsing for UID and GID, the CLI resource limits, and the limits that sandbox configurations persist, which they keep writing as JSON integers. - Use the canonical native `Uuid` parser while preserving tokens and all valid plan and configuration wire encodings. - Reject malformed limits at the CLI usage stage with clear native errors: `--memory-max` and `--pids-max` fail with `argument-invalid` and exit status 2, naming the accepted ASCII decimal range, instead of Python's `invalid int value` and the later `--pids-max must be positive` refusal with exit status 1. - Quote layer and persisted-plan values with Rust debug formatting. - Remove the now-unused Python integer arithmetic, keep only the test-only historical Python UUID oracle, and narrow the visibility of the remaining private integer helpers. - Update the scenario tests that replaced the CLI transcripts (#825, #830) to match: the usage table expects the double-quoted layer UUID and roles and the native `--memory-max` message, and the `--pids-max 0` case moves from the validation table, whose cases exit with status 1, to the usage table. - Document the input contract and the API change in the README, whose Python parity section no longer lists the sandbox, nor UUIDs among the values that it checks as Python does; #828 already removed the egress policy compiler from that section. Validation on Windows, rebased on dev: 702 library tests and 51 CLI tests pass with all features, and `cargo fmt --all --check` passes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Retire the Python JSON and surrogate behavior from the sandbox metadata checks for #730. Parse bounded environment files directly as Vec<String> with serde_json and validate ordinary UTF-8 KEY=VALUE strings. Read distro manifests as strict JSON, require the integer format 1, and refuse non-finite numbers and unpaired surrogate escapes, even in unused fields. Preserve the file, entry, and byte limits, the binding of the artifact name and digest, the file seals, and the independent refusal of systemd. Quote sandbox values and paths with Rust debug formatting, and keep the native JSON and I/O causes in diagnostics. Move the now-unused Python JSON lookup and UTF-8 encode-oracle helpers out of production code; the unrelated historical Python family keeps its test-only oracles. Update the focused checks of tests/cli.rs that name these values and paths; the transcripts of tests/fixtures/cli.json, which #830 retired, no longer record them. Document the accepted-input and diagnostic changes. Update the scenario tests that replaced the transcripts (#825, #830) to match. The validation table expects the double-quoted --mount modes and targets, and the serde_json causes of environment files that hold an object or a non-string entry. Its environment-file cases name the file as nvx-rs now quotes it: Root::expand, in the shared harness, gains the {root:?} and {root/RELATIVE:?} placeholders, which quote a path as Rust's Debug quotes it, so that the fragments match the escaped backslashes of Windows paths as well as POSIX paths. Validation on Windows, rebased on dev: 703 library tests and 51 CLI tests pass with all features, `cargo fmt --all --check` passes, and Clippy passes with warnings denied for all targets with all features and for the library without default features. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Complete the kernel-token part of #730 with the existing protocol::cmdline Linux lexer rather than Python's whitespace splitting. Refuse control characters, Unicode whitespace, quotes, and the bytes that Linux treats as whitespace, including the 0xa0 inside a UTF-8 sequence, in entrypoints, workload arguments, and guest targets, including the target of the single directory share of run. Check reserved names after Linux's quote splitting and its aliasing of hyphens and underscores, and also in the view of the guest's shell bootstrap, which splits the expanded command line on IFS whitespace without interpreting quotes, so that a quoted value such as extra="ignored nvx_memory_max=max ignored" cannot override a configured entrypoint, memory limit, process limit, or share; ordinary quoted caller values remain accepted. Trim the caller's command line once, before both reservation checks and rendering, so that the checks see exactly the text that is rendered and leading Unicode whitespace, such as U+2003 EM SPACE, cannot hide a reserved name. Confirm that Linux can still see every generated root parameter, so that -- and unmatched quotes cannot hide the appended boot settings. Keep the guest root parser and the exact 1024-byte budgets for both share-delivery modes and adapter plans. Broaden the properties to quoted and dashed reserved inputs, leading Unicode whitespace, and the known separator bytes, add explicit hidden-root and quoted-override regressions for both share-delivery modes, and make the host-path unit tests native-only. Update the documentation, and the validation table of the scenario tests that replaced the CLI transcripts (#825, #830): a sandbox argument with a space is now refused for its whitespace rather than its spaces. Build the Python compatibility code that only the CLI and the historical tests use for them alone, so that the default-feature library has no dead code, and add `cargo clippy --lib -- -D warnings` to check-nvx-rs so that it stays so. Gate the Python split and strip oracles to tests, and the integer, codec, and Unicode-digit helpers to the cli feature and tests. Since #828 moved the egress policy compiler to serde_json, and the previous commit the sandbox metadata readers, only the CLI's state files use the Python JSON reader and Python strings, so gate the json and text modules whole, as their dumps, shell, and value siblings are. The README's Python parity section now credits the nvx-rs CLI, their only remaining user, with the whitespace, int, and json.loads semantics, and the module documentation of the JSON reader no longer cites the retired egress and distro-manifest readers. Validation on Windows, rebased on dev: 706 library tests and 51 CLI tests pass with all features, and 630 library tests with default features; `cargo fmt --all --check`, Clippy with warnings denied for all targets with all features, for the default-feature library, and for the library without default features, rustdoc with warnings denied, and the Markdown link check pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Retire the Python-backed scalar compatibility layer of #730 in favor of native sandbox values while preserving the existing wire contracts. - Remove the public `Int` and `IntError` exports; use `Option<u64>` for the memory limit and `Option<u32>` for the process limit, including the bound that leaves room for the guest's init process. - Share ASCII-digit-guarded `FromStr` parsing for UID and GID, the CLI resource limits, and the limits that sandbox configurations persist, which they keep writing as JSON integers. - Use the canonical native `Uuid` parser while preserving tokens and all valid plan and configuration wire encodings. - Reject malformed limits at the CLI usage stage with clear native errors: `--memory-max` and `--pids-max` fail with `argument-invalid` and exit status 2, naming the accepted ASCII decimal range, instead of Python's `invalid int value` and the later `--pids-max must be positive` refusal with exit status 1. - Quote layer and persisted-plan values with Rust debug formatting. - Remove the now-unused Python integer arithmetic, keep only the test-only historical Python UUID oracle, and narrow the visibility of the remaining private integer helpers. - Update the scenario tests that replaced the CLI transcripts (#825, #830) to match: the usage table expects the double-quoted layer UUID and roles and the native `--memory-max` message, and the `--pids-max 0` case moves from the validation table, whose cases exit with status 1, to the usage table. - Document the input contract and the API change in the README, whose Python parity section no longer lists the sandbox, nor UUIDs among the values that it checks as Python does; #828 already removed the egress policy compiler from that section. Validation on Windows, rebased on dev: 702 library tests and 51 CLI tests pass with all features, and `cargo fmt --all --check` passes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Retire the Python JSON and surrogate behavior from the sandbox metadata checks for #730. Parse bounded environment files directly as Vec<String> with serde_json and validate ordinary UTF-8 KEY=VALUE strings. Read distro manifests as strict JSON, require the integer format 1, and refuse non-finite numbers and unpaired surrogate escapes, even in unused fields. Preserve the file, entry, and byte limits, the binding of the artifact name and digest, the file seals, and the independent refusal of systemd. Read distro manifests with the strict JSON reader of egress policy files that #828 added, which host::network::policy::json now shares as read_document, with the name that its errors give the root of the document. serde_json's own Value depends on the features of serde_json, which another crate of a build may enable: with arbitrary_precision, it reads an object that holds a number as that number, so that a format of {"$serde_json::private::Number": "1"} would pass the integer check, and it accepts numbers beyond the range of a double in unused fields. The shared reader reads both as it does without the feature. It also refuses an object that repeats a field, as `manifest has duplicate field "format"` or `packages[0] has duplicate field "name"`, and arrays and objects nested more than MAX_JSON_DEPTH, 32, deep, and a manifest that is not UTF-8 now fails as such. Quote sandbox values and paths with Rust debug formatting, and keep the native JSON and I/O causes in diagnostics. Move the now-unused Python JSON lookup and UTF-8 encode-oracle helpers out of production code; the unrelated historical Python family keeps its test-only oracles. Update the focused checks of tests/cli.rs that name these values and paths; the transcripts of tests/fixtures/cli.json, which #830 retired, no longer record them. Document the accepted-input and diagnostic changes. Update the scenario tests that replaced the transcripts (#825, #830) to match. The validation table expects the double-quoted --mount modes and targets, and the serde_json causes of environment files that hold an object or a non-string entry. Its environment-file cases name the file as nvx-rs now quotes it: Root::expand, in the shared harness, gains the {root:?} and {root/RELATIVE:?} placeholders, which quote a path as Rust's Debug quotes it, so that the fragments match the escaped backslashes of Windows paths as well as POSIX paths. The manifest tests refuse that object-valued format as a mismatch, and refuse numbers beyond a double, repeated fields at the root and in a package, nesting beyond the limit, and a manifest that is not UTF-8. A test of the shared reader simulates arbitrary_precision, as its other tests do, and checks that the object stays an object there, and another that read_document names the root of a document in its errors. Validation on Windows, rebased on dev: 705 library tests and 51 CLI tests pass with all features, `cargo fmt --all --check` passes, Clippy passes with warnings denied for all targets with all features, and the Markdown link check passes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Complete the kernel-token part of #730 with the existing protocol::cmdline Linux lexer rather than Python's whitespace splitting. Refuse control characters, Unicode whitespace, quotes, and the bytes that Linux treats as whitespace, including the 0xa0 inside a UTF-8 sequence, in entrypoints, workload arguments, and guest targets, including the target of the single directory share of run. Check reserved names after Linux's quote splitting and its aliasing of hyphens and underscores, and also in the view of the guest's shell bootstrap, which splits the expanded command line on IFS whitespace without interpreting quotes, so that a quoted value such as extra="ignored nvx_memory_max=max ignored" cannot override a configured entrypoint, memory limit, process limit, or share; ordinary quoted caller values remain accepted. Trim the caller's command line once, before both reservation checks and rendering, so that the checks see exactly the text that is rendered and leading Unicode whitespace, such as U+2003 EM SPACE, cannot hide a reserved name. Confirm that Linux can still see every generated root parameter, so that -- and unmatched quotes cannot hide the appended boot settings. Keep the guest root parser and the exact 1024-byte budgets for both share-delivery modes and adapter plans. Broaden the properties to quoted and dashed reserved inputs, leading Unicode whitespace, and the known separator bytes, add explicit hidden-root and quoted-override regressions for both share-delivery modes, and make the host-path unit tests native-only. Update the documentation, and the validation table of the scenario tests that replaced the CLI transcripts (#825, #830): a sandbox argument with a space is now refused for its whitespace rather than its spaces. Build the Python compatibility code that only the CLI and the historical tests use for them alone, so that the default-feature library has no dead code, and add `cargo clippy --lib -- -D warnings` to check-nvx-rs so that it stays so. Gate the Python split and strip oracles to tests, and the integer, codec, and Unicode-digit helpers to the cli feature and tests. Since #828 moved the egress policy compiler to serde_json, and the previous commit the sandbox metadata readers, only the CLI's state files use the Python JSON reader and Python strings, so gate the json and text modules whole, as their dumps, shell, and value siblings are. The README's Python parity section now credits the nvx-rs CLI, their only remaining user, with the whitespace, int, and json.loads semantics, and the module documentation of the JSON reader no longer cites the retired egress and distro-manifest readers. Validation on Windows, rebased on dev: 708 library tests and 51 CLI tests pass with all features, and 632 library tests with default features; `cargo fmt --all --check`, Clippy with warnings denied for all targets with all features, for the default-feature library, and for the library without default features, rustdoc with warnings denied, and the Markdown link check pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>


Part of #728: the first of its two pull requests, which adds the scenario tests. The second, stacked on this one, retires the transcripts.
nvx-rs/tests/fixtures/cli.jsonholds 315 recorded scenarios of the CLI, 313 for POSIX hosts and 312 for Windows, whichnvx_rs_reproduces_the_recorded_scenariosreplays byte for byte: each step's status, standard output and error, and files, once placeholders replace the root and volatile values. A change of a message rewrites transcripts by hand, as the error codes of #724 did in #812, while what callers rely on stays implicit in them. This pull request adds table-driven scenario tests innvx-rs/tests/cli/that check that contract directly: exit statuses; errors by their codes and stable fragments of their messages; the entries, records, and POSIX modes of state directories; and the arguments that OpenVMM receives. It also maps every recorded scenario, and every regression that #722 traces, to the tests that replace or guard it. The CLI does not change.Changes
tests/cli/harness.rs): fresh artifact roots,Root::new()andRoot::empty(), that end leftover fake OpenVMM processes when they drop;Outcomechecks of successes, errors, and usage errors, each by its status, streams, exact code, and message fragments; snapshots of the tree below a root; and the splitting of>>lines with a POSIX shell-word splitter, orCommandLineToArgvWon Windows, for which the Windows dev-dependency onwindows-sysenablesWin32_UI_Shell. The replay and the other tests oftests/cli.rsuse the same roots, and the replay checks that its fixture's tree is the harness's.usage.rs: 61 usage errors of both parsers, each with status 2, itsargument-*code, and the option and value at fault, and help.validation.rs: 129 refusals before any side effect, each with status 1, its code, its fragments, and the root unchanged, including which of several errors a command line reports first.dry_run.rs: 72 dry runs, whose>>line must split into the options that OpenVMM's documentation prescribes for the case, and a property test that arbitrary--cmdlinevalues split back out of the line.one_shot.rs: 20 launches with the fake OpenVMM, checking exit statuses, output byte for byte, that the>>line splits into the argv that the fake recorded, the checked images, and outcome reports: the network policy that the case expects, host loopback and status code included, and the seven teardown steps that callers require, each successful.lifecycle.rs: provision, start, exec, stop, and deprovision for 9 configurations, checking the configuration with its bindings, the runtime record and identity, the capability, POSIX modes, OpenVMM's options, and the guest's mappings; managed executions; and 10 refusals of lifecycle operations, each by its code. Together they cover the regressions of Track nvx.py managed sandbox bugs: PID-only liveness check wedges stop, deprovision, and start #369 and Track managed sandbox lifecycle bugs: on Windows, provision waits out its timeout for a lock file that refuses access for good #464, and the dry runs of Track managed sandbox bugs: --dry-run performs managed operations, and layered exec --cwd leaves PWD at / #373.formats.rs, with hand-written configurations intests/fixtures/state/: formats 1 and 7 start and exec, and formats 2 to 6 and the format 1 ofnvx.pyfail withformat-unsupportedbefore OpenVMM launches.egress.rs: 9 policy files, including Track egress policy compiler bugs: scoped IPv6 validation, range fragmentation #462's scoped IPv6 range, againstcompile_policy_file's rules, or its refusal's code and message;artifacts.rs: a checkout and anNVX_OPENVMMoverride, in the order ofGuest::ALLand the byte layout ofjson.dumps, whoseensure_asciiescapes it reproduces.tests/cli/scenarios.rs): each of the 315 scenarios maps to tests and table cases, and each regression that nvx-rs: retire the Python-parity golden tests and make Rust the reference #722 traces maps to the tests that guard against it. ThePWDthat Track managed sandbox bugs: --dry-run performs managed operations, and layered exec --cwd leaves PWD at / #373 left at/maps tosandbox_launch_runs_the_workload_itself_through_its_entry, a unit test of the guest agent's launch, and to the/usr/bin/env --cwd /tmpcheck that themanaged-exec-configmicroVM scenario runs in a real guest. Track Rust sandbox exec bugs: async output collection bypasses timeouts #513 maps toasync_wait_with_output_honors_the_workload_timeoutofaci_edge_sandboxes. The crate's package lacks the sources of those last two, so the mapping test reads them at run time in a checkout.every_recorded_scenario_maps_to_a_testreads the cases from the tables themselves, and fails for an unmapped scenario, a mapping of a scenario that the fixture lacks, a duplicate, a test that does not exist, or a case that its test's table lacks or names twice. The mapping retires with the replay.doc/project-structure.md.The eight commits are self-contained: each builds, lints, and passes its tests on Windows and Linux, and adds the helpers of the harness with their first users.
Differences from the issue
nvx-rs run.startreads configuration formats 1 and 7 alone since nvx-rs: host cold integration for image-backed sandboxes #622, so the format tests check that those start and that formats 2 to 6 are refused. Writing only the newest format belongs to the second pull request, with the retirement.aci_edge_sandboxes, and Track managed sandbox bugs: --dry-run performs managed operations, and layered exec --cwd leaves PWD at / #373'sPWDin the guest agent, which the fake OpenVMM only emulates and a shell would override. The mapping names the tests that guard against them where they lie.Gates
CLI scenariosfamily ofscripts/nvx_rs_coverage.py, reaches no line or branch of production code beyond the tests outside the families, on Linux or Windows. What only it reaches among all the tests, 6 lines and 2 branches on Linux and 1 branch on Windows, depends on timing and random inputs: busy-wait loops of the guest's unit tests insrc/guest/process.rsandsrc/guest/sys/process.rs, and a bound insrc/protocol/exec.rsthat the protocol's property tests reach by chance. Earlier runs attribute such lines to other families.--test cli, at 7c36bfd, onsrc/host/cli/**/*.rs): both runs evaluate the same 791 mutants. The replay catches or times out on 483 of them, and the other tests on 538, every one of the 483 among them. One shard of the second run lost its runner, in this run and in an earlier one: mutants ofParser::parseandState::consume_optionmakenvx-rsloop and allocate without bound, which exhausts a 16 GB runner before cargo-mutants' timeout. That shard's 50 mutants ran locally with cargo-mutants 27.1.0, four test threads as on the runner, and 1 GiB of address space per process, so that such a process aborts instead.--test cli, at 630cfa1, on the files with platform-specific code,src/host/cli/{mod,output,session}.rsandsrc/host/cli/sandbox/*.rs): both runs evaluate the same 505 mutants, 72 of them unviable in both. The replay catches or times out on 268 of them, and the other tests on 309, every one of the 268 among them. 43 that the replay catches time out with the other tests instead: in each such run, at least 4 tests had already failed, and onlystart_hands_openvmm_the_images_that_it_checkedora_run_passes_openvmm_no_handle_of_its_caller, both older than this pull request, were still waiting when cargo-mutants' timeout ended it.The head's tests differ from those of 7c36bfd only by the review fixes, which add checks and drop none, and from those of 630cfa1 only by the mapping test, which exercises no production code, and a comment, so the mutants that they catch can only be more.
Validation
-D warningsfor all targets and features and for the library without default features; and theclitests. At the head: the full test suite with all features, andcargo +1.89 check --all-targets --all-features.git archiveof the commit: rustfmt, both Clippy configurations, and theclitests. At the head: the full test suite with all features, and the Rust 1.89 check.cargo packagepacks, unpacked outside the repository, passescargo check --all-targets --features cli,testingand theclitests on Windows.artifactstest passes withTMPorTMPDIRbelownvx728-José-😀, on Windows and Linux.validate-nvxaction. On this machine, 18 subtests oftest_setup_azure.pyand 3 of a host-loopback script test fail through the local Git Bash; they run shell scripts that this change does not touch.