Skip to content

Commit 5bd8e08

Browse files
authored
feat(harness): Add argfile support (#98)
2 parents f5af618 + cf95a0b commit 5bd8e08

8 files changed

Lines changed: 438 additions & 4 deletions

File tree

DESIGN.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,3 +156,15 @@ Unsure which of those would be slower and how the different characteristics matc
156156

157157
Rather than build into every harness shuffle, sharding, and any other specific logic like that,
158158
we can instead give the user direct control over the test order by the order they are specified on the command line.
159+
160+
### Decision: argfile support
161+
162+
Similar to filters changing the order of tests,
163+
argfile support allows for passing a large list of arguments to a test binary.
164+
165+
The syntax and semantics match rustc:
166+
- Expanded before parsing, independent of any other syntax
167+
- Arguments are delimited by newlines; no shell escaping
168+
- rustc has unstable support for `@shell:<path>`
169+
- Lines are read literal, empty lines are empty arguments and no comments
170+
- Non-recursive

crates/libtest2-harness/src/harness.rs

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,19 @@ use libtest_lexarg::OutputFormat;
33
use crate::{cli, notify, Case, RunError, RunMode, State};
44

55
pub struct Harness {
6-
raw: Vec<std::ffi::OsString>,
6+
raw: std::io::Result<Vec<std::ffi::OsString>>,
77
cases: Vec<Box<dyn Case>>,
88
}
99

1010
impl Harness {
1111
pub fn with_args(args: impl IntoIterator<Item = impl Into<std::ffi::OsString>>) -> Self {
12-
let raw = args.into_iter().map(|s| s.into()).collect::<Vec<_>>();
12+
let raw = expand_args(args);
1313
Self { raw, cases: vec![] }
1414
}
1515

1616
pub fn with_env() -> Self {
17-
let raw = std::env::args_os().collect::<Vec<_>>();
17+
let raw = std::env::args_os();
18+
let raw = expand_args(raw);
1819
Self { raw, cases: vec![] }
1920
}
2021

@@ -31,7 +32,14 @@ impl Harness {
3132
}
3233

3334
pub fn main(mut self) -> ! {
34-
let mut parser = cli::Parser::new(&self.raw);
35+
let raw = match self.raw {
36+
Ok(raw) => raw,
37+
Err(err) => {
38+
eprintln!("{err}");
39+
std::process::exit(1)
40+
}
41+
};
42+
let mut parser = cli::Parser::new(&raw);
3543
let opts = parse(&mut parser).unwrap_or_else(|err| {
3644
eprintln!("{err}");
3745
std::process::exit(1)
@@ -131,6 +139,27 @@ fn parse<'p>(
131139
Ok(opts)
132140
}
133141

142+
fn expand_args(
143+
args: impl IntoIterator<Item = impl Into<std::ffi::OsString>>,
144+
) -> std::io::Result<Vec<std::ffi::OsString>> {
145+
let mut expanded = Vec::new();
146+
for arg in args {
147+
let arg = arg.into();
148+
if let Some(argfile) = arg.to_str().and_then(|s| s.strip_prefix("@")) {
149+
expanded.extend(parse_argfile(std::path::Path::new(argfile))?);
150+
} else {
151+
expanded.push(arg);
152+
}
153+
}
154+
Ok(expanded)
155+
}
156+
157+
fn parse_argfile(path: &std::path::Path) -> std::io::Result<Vec<std::ffi::OsString>> {
158+
// Logic taken from rust-lang/rust's `compiler/rustc_driver_impl/src/args.rs`
159+
let content = std::fs::read_to_string(path)?;
160+
Ok(content.lines().map(|s| s.into()).collect())
161+
}
162+
134163
fn notifier(opts: &libtest_lexarg::TestOpts) -> std::io::Result<Box<dyn notify::Notifier>> {
135164
#[cfg(feature = "color")]
136165
let stdout = anstream::stdout();
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
use snapbox::prelude::*;
2+
use snapbox::str;
3+
4+
fn test_cmd() -> snapbox::cmd::Command {
5+
static BIN: once_cell_polyfill::sync::OnceLock<(std::path::PathBuf, std::path::PathBuf)> =
6+
once_cell_polyfill::sync::OnceLock::new();
7+
let (bin, current_dir) = BIN.get_or_init(|| {
8+
let package_root = crate::util::new_test(
9+
r#"
10+
fn main() {
11+
use libtest2_mimic::Trial;
12+
use libtest2_mimic::RunError;
13+
libtest2_mimic::Harness::with_env()
14+
.cases(vec![
15+
Trial::test("one", |_| Ok(())),
16+
Trial::test("two", |_| Ok(())),
17+
Trial::test("three", |_| Ok(())),
18+
Trial::test("one_two", |_| Ok(())),
19+
])
20+
.main();
21+
}
22+
"#,
23+
false,
24+
);
25+
let bin = crate::util::compile_test(&package_root);
26+
(bin, package_root)
27+
});
28+
snapbox::cmd::Command::new(bin).current_dir(current_dir)
29+
}
30+
31+
fn check(
32+
args: &[&str],
33+
argfile: &std::path::Path,
34+
code: i32,
35+
single: impl IntoData,
36+
parallel: impl IntoData,
37+
) {
38+
test_cmd()
39+
.arg(format!("@{}", argfile.to_str().unwrap()))
40+
.args(args)
41+
.args(["--test-threads", "1"])
42+
.assert()
43+
.code(code)
44+
.stdout_eq(single);
45+
test_cmd()
46+
.arg(format!("@{}", argfile.to_str().unwrap()))
47+
.args(args)
48+
.assert()
49+
.code(code)
50+
.stdout_eq(parallel);
51+
}
52+
53+
#[test]
54+
fn empty() {
55+
let argfile = crate::util::new_file("argfile-", ".txt", "");
56+
check(
57+
&[],
58+
&argfile,
59+
0,
60+
str![[r#"
61+
62+
running 4 tests
63+
test one ... ok
64+
test one_two ... ok
65+
test three ... ok
66+
test two ... ok
67+
68+
test result: ok. 4 passed; 0 failed; 0 ignored; 0 filtered out; finished in [..]s
69+
70+
71+
"#]],
72+
str![[r#"
73+
74+
running 4 tests
75+
...
76+
77+
test result: ok. 4 passed; 0 failed; 0 ignored; 0 filtered out; finished in [..]s
78+
79+
80+
"#]],
81+
);
82+
}
83+
84+
#[test]
85+
fn list() {
86+
let argfile = crate::util::new_file("argfile-", ".txt", "--list");
87+
check(
88+
&[],
89+
&argfile,
90+
0,
91+
str![[r#"
92+
one: test
93+
one_two: test
94+
three: test
95+
two: test
96+
97+
4 tests
98+
99+
100+
"#]],
101+
str![[r#"
102+
one: test
103+
one_two: test
104+
...
105+
106+
4 tests
107+
108+
109+
"#]],
110+
);
111+
}
112+
113+
#[test]
114+
fn multiline() {
115+
let argfile = crate::util::new_file(
116+
"argfile-",
117+
".txt",
118+
"one
119+
two
120+
--exact",
121+
);
122+
check(
123+
&[],
124+
&argfile,
125+
0,
126+
str![[r#"
127+
128+
running 2 tests
129+
test one ... ok
130+
test two ... ok
131+
132+
test result: ok. 2 passed; 0 failed; 0 ignored; 2 filtered out; finished in [..]s
133+
134+
135+
"#]],
136+
str![[r#"
137+
138+
running 2 tests
139+
...
140+
141+
test result: ok. 2 passed; 0 failed; 0 ignored; 2 filtered out; finished in [..]s
142+
143+
144+
"#]],
145+
);
146+
}
147+
148+
#[test]
149+
fn mixed() {
150+
let argfile = crate::util::new_file(
151+
"argfile-", ".txt", "one
152+
two",
153+
);
154+
check(
155+
&["--exact"],
156+
&argfile,
157+
0,
158+
str![[r#"
159+
160+
running 2 tests
161+
test one ... ok
162+
test two ... ok
163+
164+
test result: ok. 2 passed; 0 failed; 0 ignored; 2 filtered out; finished in [..]s
165+
166+
167+
"#]],
168+
str![[r#"
169+
170+
running 2 tests
171+
...
172+
173+
test result: ok. 2 passed; 0 failed; 0 ignored; 2 filtered out; finished in [..]s
174+
175+
176+
"#]],
177+
);
178+
}
179+
180+
#[test]
181+
fn invalid() {
182+
let argfile = std::path::Path::new("highly-improbably-non-existent-file.txt");
183+
check(&[], argfile, 1, str![""], str![""]);
184+
}

crates/libtest2-mimic/tests/testsuite/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
mod all_passing;
2+
mod argfile;
23
mod main_thread;
34
mod mixed_bag;
45
mod panic;

crates/libtest2-mimic/tests/testsuite/util.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,15 @@ harness = {harness}
4040
package_root
4141
}
4242

43+
pub fn new_file(name_prefix: &str, name_suffix: &str, content: &str) -> std::path::PathBuf {
44+
static SUFFIX: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
45+
let suffix = SUFFIX.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
46+
let name = format!("{name_prefix}{suffix}{name_suffix}");
47+
let path = tempdir().join(name);
48+
std::fs::write(&path, content).unwrap();
49+
path
50+
}
51+
4352
pub fn compile_test(package_root: &std::path::Path) -> std::path::PathBuf {
4453
let manifest_path = package_root.join("Cargo.toml");
4554
let target_name = package_root.file_name().unwrap().to_str().unwrap();

0 commit comments

Comments
 (0)