Skip to content

Commit 3edc7fb

Browse files
committed
[cargo-miri] support nextest
Add the ability to list and run nextest commands. Running tests out of archives is currently broken, as the comment in run-test.py explains. But the list and run commands work fine.
1 parent 9e37c48 commit 3edc7fb

File tree

3 files changed

+85
-9
lines changed

3 files changed

+85
-9
lines changed

.github/workflows/ci.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,9 @@ jobs:
7777
./rustup-toolchain "" --host ${{ matrix.host_target }}
7878
fi
7979
80+
- name: Install nextest
81+
uses: taiki-e/install-action@nextest
82+
8083
- name: Show Rust version
8184
run: |
8285
rustup show

cargo-miri/bin.rs

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ Usage:
2525
Subcommands:
2626
run, r Run binaries
2727
test, t Run tests
28+
nextest Run tests with nextest (requires cargo-nextest installed)
2829
setup Only perform automatic setup, but without asking questions (for getting a proper libstd)
2930
3031
The cargo options are exactly the same as for `cargo run` and `cargo test`, respectively.
@@ -34,10 +35,11 @@ Examples:
3435
cargo miri test -- test-suite-filter
3536
"#;
3637

37-
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
38+
#[derive(Clone, Debug, PartialEq, Eq)]
3839
enum MiriCommand {
3940
Run,
4041
Test,
42+
Nextest(String),
4143
Setup,
4244
}
4345

@@ -575,18 +577,29 @@ fn phase_cargo_miri(mut args: env::Args) {
575577
// so we cannot detect subcommands later.
576578
let subcommand = match args.next().as_deref() {
577579
Some("test" | "t") => MiriCommand::Test,
580+
Some("nextest") => {
581+
// The next argument is the nextest subcommand.
582+
let nextest_command = match args.next() {
583+
Some(nextest_command) => nextest_command,
584+
None =>
585+
show_error(format!(
586+
"`cargo miri nextest` requires a nextest subcommand as its next argument."
587+
)),
588+
};
589+
MiriCommand::Nextest(nextest_command)
590+
}
578591
Some("run" | "r") => MiriCommand::Run,
579592
Some("setup") => MiriCommand::Setup,
580593
// Invalid command.
581594
_ =>
582595
show_error(format!(
583-
"`cargo miri` supports the following subcommands: `run`, `test`, and `setup`."
596+
"`cargo miri` supports the following subcommands: `run`, `test`, `nextest`, and `setup`."
584597
)),
585598
};
586599
let verbose = has_arg_flag("-v");
587600

588601
// We always setup.
589-
setup(subcommand);
602+
setup(subcommand.clone());
590603

591604
// Invoke actual cargo for the job, but with different flags.
592605
// We re-use `cargo test` and `cargo run`, which makes target and binary handling very easy but
@@ -595,13 +608,14 @@ fn phase_cargo_miri(mut args: env::Args) {
595608
// approach that uses `cargo check`, making that part easier but target and binary handling
596609
// harder.
597610
let cargo_miri_path = std::env::current_exe().expect("current executable path invalid");
598-
let cargo_cmd = match subcommand {
599-
MiriCommand::Test => "test",
600-
MiriCommand::Run => "run",
611+
let cargo_cmd: Vec<&str> = match &subcommand {
612+
MiriCommand::Test => vec!["test"],
613+
MiriCommand::Nextest(nextest_command) => vec!["nextest", nextest_command],
614+
MiriCommand::Run => vec!["run"],
601615
MiriCommand::Setup => return, // `cargo miri setup` stops here.
602616
};
603617
let mut cmd = cargo();
604-
cmd.arg(cargo_cmd);
618+
cmd.args(cargo_cmd);
605619

606620
// Make sure we know the build target, and cargo does, too.
607621
// This is needed to make the `CARGO_TARGET_*_RUNNER` env var do something,

test-cargo-miri/run-test.py

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
and the working directory to contain the cargo-miri-test project.
66
'''
77

8-
import sys, subprocess, os, re
8+
import shutil, sys, subprocess, os, re, typing
99

1010
CGREEN = '\33[32m'
1111
CBOLD = '\33[1m'
@@ -23,6 +23,12 @@ def cargo_miri(cmd, quiet = True):
2323
args += ["--target", os.environ['MIRI_TEST_TARGET']]
2424
return args
2525

26+
def cargo_miri_nextest(cmd, quiet = True):
27+
args = ["cargo", "miri", "nextest", cmd]
28+
if 'MIRI_TEST_TARGET' in os.environ:
29+
args += ["--target", os.environ['MIRI_TEST_TARGET']]
30+
return args
31+
2632
def normalize_stdout(str):
2733
str = str.replace("src\\", "src/") # normalize paths across platforms
2834
return re.sub("finished in \d+\.\d\ds", "finished in $TIME", str)
@@ -55,6 +61,35 @@ def test(name, cmd, stdout_ref, stderr_ref, stdin=b'', env={}):
5561
print("--- END test stderr ---")
5662
fail("exit code was {}".format(p.returncode))
5763

64+
def test_nextest(name, cmd: typing.List[str], stdin=b'', env={}) -> typing.Tuple[str, str]:
65+
print("Testing {}...".format(name))
66+
67+
## Call `cargo miri`, capture all output
68+
p_env = os.environ.copy()
69+
p_env.update(env)
70+
p = subprocess.Popen(
71+
cmd,
72+
stdin=subprocess.PIPE,
73+
stdout=subprocess.PIPE,
74+
stderr=subprocess.PIPE,
75+
env=p_env,
76+
)
77+
(stdout, stderr) = p.communicate(input=stdin)
78+
stdout = stdout.decode("UTF-8")
79+
stderr = stderr.decode("UTF-8")
80+
81+
if p.returncode == 0:
82+
return (stdout, stderr)
83+
# Show output
84+
print("Nextest did not exit with 0!")
85+
print("--- BEGIN test stdout ---")
86+
print(stdout, end="")
87+
print("--- END test stdout ---")
88+
print("--- BEGIN test stderr ---")
89+
print(stderr, end="")
90+
print("--- END test stderr ---")
91+
fail("exit code was {}".format(p.returncode))
92+
5893
def test_no_rebuild(name, cmd, env={}):
5994
print("Testing {}...".format(name))
6095
p_env = os.environ.copy()
@@ -159,6 +194,26 @@ def test_cargo_miri_test():
159194
env={'MIRIFLAGS': "-Zmiri-permissive-provenance"},
160195
)
161196

197+
def test_cargo_miri_nextest():
198+
if shutil.which("cargo-nextest") is None:
199+
print("Skipping `cargo miri nextest` (no cargo-nextest)")
200+
return
201+
# main() in src/main.rs is a custom test harness that doesn't implement the API required by
202+
# nextest -- this means that we can't test it. However, all the other tests are regular ones.
203+
nextest_filter = "!(package(cargo-miri-test) & binary(main))"
204+
# These tests just check that the exit code is 0.
205+
# TODO: maybe inspect stdout/stderr, especially for list output?
206+
test_nextest("`cargo miri nextest list`",
207+
cargo_miri_nextest("list") + ["-E", nextest_filter]
208+
)
209+
test_nextest("`cargo miri nextest run`",
210+
cargo_miri_nextest("run") + ["-E", nextest_filter],
211+
)
212+
# Running nextest archives is currently not supported.
213+
# See https://github.com/nextest-rs/nextest/issues/370 for one issue -- also note that cargo
214+
# miri passes in cargo-related options to nextest, which nextest rejects when running tests
215+
# from an archive.
216+
162217
os.chdir(os.path.dirname(os.path.realpath(__file__)))
163218
os.environ["CARGO_TARGET_DIR"] = "target" # this affects the location of the target directory that we need to check
164219
os.environ["RUST_TEST_NOCAPTURE"] = "0" # this affects test output, so make sure it is not set
@@ -173,9 +228,13 @@ def test_cargo_miri_test():
173228
subprocess.run(cargo_miri("setup"), check=True)
174229
test_cargo_miri_run()
175230
test_cargo_miri_test()
231+
test_cargo_miri_nextest()
176232
# Ensure we did not create anything outside the expected target dir.
177233
for target_dir in ["target", "custom-run", "custom-test", "config-cli"]:
178-
if os.listdir(target_dir) != ["miri"]:
234+
listdir = os.listdir(target_dir)
235+
if "nextest" in listdir:
236+
listdir.remove("nextest")
237+
if listdir != ["miri"]:
179238
fail(f"`{target_dir}` contains unexpected files")
180239
# Ensure something exists inside that target dir.
181240
os.access(os.path.join(target_dir, "miri", "debug", "deps"), os.F_OK)

0 commit comments

Comments
 (0)