Skip to content

Commit 9c4ee86

Browse files
committed
Avoid iterating files when --exact is passed in
This fixes a quadratic performance issue in which the test directories are iterated for every test case when run under nextest.
1 parent 2c6057a commit 9c4ee86

File tree

4 files changed

+65
-9
lines changed

4 files changed

+65
-9
lines changed

src/runner.rs

Lines changed: 58 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,7 @@ use libtest_mimic::{Arguments, Trial};
1111
pub fn runner(requirements: &[Requirements]) -> ExitCode {
1212
let args = Arguments::from_args();
1313

14-
let mut tests: Vec<_> = requirements.iter().flat_map(|req| req.expand()).collect();
15-
tests.sort_unstable_by(|a, b| a.name().cmp(b.name()));
14+
let tests = find_tests(&args, requirements);
1615

1716
let conclusion = libtest_mimic::run(&args, tests);
1817

@@ -25,6 +24,43 @@ pub fn runner(requirements: &[Requirements]) -> ExitCode {
2524
conclusion.exit_code()
2625
}
2726

27+
fn find_tests(args: &Arguments, requirements: &[Requirements]) -> Vec<Trial> {
28+
let tests: Vec<_> = if let Some(exact_filter) = exact_filter(args) {
29+
let exact_tests: Vec<_> = requirements
30+
.iter()
31+
.flat_map(|req| req.exact(exact_filter))
32+
.collect();
33+
34+
match exact_tests.len() {
35+
0 if is_nextest() => {
36+
panic!("Failed to find exact match for filter {exact_filter}");
37+
}
38+
len @ (2..) if is_nextest() => {
39+
panic!("Only expected one but found {len} exact matches for filter {exact_filter}");
40+
}
41+
_ => {}
42+
}
43+
exact_tests
44+
} else {
45+
let mut tests: Vec<_> = requirements.iter().flat_map(|req| req.expand()).collect();
46+
tests.sort_unstable_by(|a, b| a.name().cmp(b.name()));
47+
tests
48+
};
49+
tests
50+
}
51+
52+
fn is_nextest() -> bool {
53+
std::env::var_os("NEXTEST").is_some_and(|val| val.eq("1"))
54+
}
55+
56+
fn exact_filter(args: &Arguments) -> Option<&str> {
57+
if args.exact && args.skip.is_empty() {
58+
args.filter.as_deref()
59+
} else {
60+
None
61+
}
62+
}
63+
2864
#[doc(hidden)]
2965
pub struct Requirements {
3066
test: TestFn,
@@ -49,6 +85,25 @@ impl Requirements {
4985
}
5086
}
5187

88+
fn trial(&self, path: Utf8PathBuf) -> Trial {
89+
let testfn = self.test;
90+
let name = utils::derive_test_name(&self.root, &path, &self.test_name);
91+
Trial::test(name, move || {
92+
testfn
93+
.call(&path)
94+
.map_err(|err| format!("{:?}", err).into())
95+
})
96+
}
97+
98+
fn exact(&self, filter: &str) -> Option<Trial> {
99+
let path = utils::derive_test_path(&self.root, filter, &self.test_name)?;
100+
if path.exists() {
101+
Some(self.trial(path))
102+
} else {
103+
None
104+
}
105+
}
106+
52107
/// Scans all files in a given directory, finds matching ones and generates a test descriptor
53108
/// for each of them.
54109
fn expand(&self) -> Vec<Trial> {
@@ -66,13 +121,7 @@ impl Requirements {
66121
error
67122
)
68123
}) {
69-
let testfn = self.test;
70-
let name = utils::derive_test_name(&self.root, &path, &self.test_name);
71-
Some(Trial::test(name, move || {
72-
testfn
73-
.call(&path)
74-
.map_err(|err| format!("{:?}", err).into())
75-
}))
124+
Some(self.trial(path))
76125
} else {
77126
None
78127
}

src/utils.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,8 @@ pub fn derive_test_name(root: &Utf8Path, path: &Utf8Path, test_name: &str) -> St
3333

3434
format!("{}::{}", test_name, relative)
3535
}
36+
37+
pub fn derive_test_path(root: &Utf8Path, filter: &str, test_name: &str) -> Option<Utf8PathBuf> {
38+
let relative = filter.strip_prefix(test_name)?.strip_prefix("::")?;
39+
Some(root.join(relative))
40+
}

tests/files/::colon::dir/::.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
floop

tests/files/::colon::dir/a.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
flarp

0 commit comments

Comments
 (0)