Skip to content

Avoid iterating files when --exact is passed in #49

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Apr 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,7 @@ walkdir = "2.5.0"
[[test]]
name = "example"
harness = false

[[test]]
name = "run_example"
harness = true
70 changes: 61 additions & 9 deletions src/runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@
pub fn runner(requirements: &[Requirements]) -> ExitCode {
let args = Arguments::from_args();

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

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

Expand All @@ -25,6 +24,50 @@
conclusion.exit_code()
}

fn find_tests(args: &Arguments, requirements: &[Requirements]) -> Vec<Trial> {
let tests: Vec<_> = if let Some(exact_filter) = exact_filter(args) {
let exact_tests: Vec<_> = requirements
.iter()
.flat_map(|req| req.exact(exact_filter))
.collect();

if is_nextest() {
if exact_tests.is_empty() {
panic!("Failed to find exact match for filter {exact_filter}");

Check warning on line 36 in src/runner.rs

View check run for this annotation

Codecov / codecov/patch

src/runner.rs#L36

Added line #L36 was not covered by tests
} else if exact_tests.len() > 1 {
panic!(
"Only expected one but found {} exact matches for filter {exact_filter}",
exact_tests.len()
);

Check warning on line 41 in src/runner.rs

View check run for this annotation

Codecov / codecov/patch

src/runner.rs#L38-L41

Added lines #L38 - L41 were not covered by tests
}
}

Check warning on line 43 in src/runner.rs

View check run for this annotation

Codecov / codecov/patch

src/runner.rs#L43

Added line #L43 was not covered by tests
exact_tests
} else if is_full_scan_forbidden(args) {
panic!("Exact filter was expected to be used");

Check warning on line 46 in src/runner.rs

View check run for this annotation

Codecov / codecov/patch

src/runner.rs#L46

Added line #L46 was not covered by tests
} else {
let mut tests: Vec<_> = requirements.iter().flat_map(|req| req.expand()).collect();
tests.sort_unstable_by(|a, b| a.name().cmp(b.name()));
tests
};
tests
}

fn is_nextest() -> bool {
std::env::var("NEXTEST").as_deref() == Ok("1")
}

fn is_full_scan_forbidden(args: &Arguments) -> bool {
!args.list && std::env::var("__DATATEST_FULL_SCAN_FORBIDDEN").as_deref() == Ok("1")
}

fn exact_filter(args: &Arguments) -> Option<&str> {
if args.exact && args.skip.is_empty() {
args.filter.as_deref()
} else {
None
}
}

#[doc(hidden)]
pub struct Requirements {
test: TestFn,
Expand All @@ -49,6 +92,21 @@
}
}

fn trial(&self, path: Utf8PathBuf) -> Trial {
let testfn = self.test;
let name = utils::derive_test_name(&self.root, &path, &self.test_name);
Trial::test(name, move || {
testfn
.call(&path)
.map_err(|err| format!("{:?}", err).into())
})
}

fn exact(&self, filter: &str) -> Option<Trial> {
let path = utils::derive_test_path(&self.root, filter, &self.test_name)?;
path.exists().then(|| self.trial(path))
}

/// Scans all files in a given directory, finds matching ones and generates a test descriptor
/// for each of them.
fn expand(&self) -> Vec<Trial> {
Expand All @@ -66,13 +124,7 @@
error
)
}) {
let testfn = self.test;
let name = utils::derive_test_name(&self.root, &path, &self.test_name);
Some(Trial::test(name, move || {
testfn
.call(&path)
.map_err(|err| format!("{:?}", err).into())
}))
Some(self.trial(path))
} else {
None
}
Expand Down
73 changes: 73 additions & 0 deletions src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,76 @@ pub fn derive_test_name(root: &Utf8Path, path: &Utf8Path, test_name: &str) -> St

format!("{}::{}", test_name, relative)
}

pub fn derive_test_path(root: &Utf8Path, filter: &str, test_name: &str) -> Option<Utf8PathBuf> {
let relative = filter.strip_prefix(test_name)?.strip_prefix("::")?;
Some(root.join(relative))
}

#[cfg(test)]
mod tests {
use super::*;

mod derive_path {
use super::*;

#[test]
fn missing_test_name() {
assert_eq!(derive_test_path("root".into(), "file", "test_name"), None);
}

#[test]
fn missing_colons() {
assert_eq!(
derive_test_path("root".into(), "test_name", "test_name"),
None
);
}

#[test]
fn is_relative_to_root() {
assert_eq!(
derive_test_path("root".into(), "test_name::file", "test_name"),
Some("root/file".into())
);
assert_eq!(
derive_test_path("root2".into(), "test_name::file", "test_name"),
Some("root2/file".into())
);
}

#[test]
fn nested_dirs() {
assert_eq!(
derive_test_path("root".into(), "test_name::dir/dir2/file", "test_name"),
Some("root/dir/dir2/file".into())
);
}

#[test]
fn subsequent_module_separators_remain() {
assert_eq!(
derive_test_path("root".into(), "test_name::mod::file", "test_name"),
Some("root/mod::file".into())
);
}

#[test]
fn inverse_of_derive_test_name() {
let root: Utf8PathBuf = "root".into();
for (path, test_name) in [
(root.join("foo/bar.txt"), "test_name"),
(root.join("foo::bar.txt"), "test_name"),
(root.join("foo/bar/baz"), "test_name"),
(root.join("foo"), "test_name::mod"),
(root.join("🦀"), "🚀::🚀"),
] {
let derived_test_name = derive_test_name(&root, &path, test_name);
assert_eq!(
derive_test_path(&root, &derived_test_name, test_name),
Some(path)
);
}
}
}
}
1 change: 1 addition & 0 deletions tests/files/::colon::dir/::.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
floop
1 change: 1 addition & 0 deletions tests/files/::colon::dir/a.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
flarp
42 changes: 42 additions & 0 deletions tests/run_example.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Copyright (c) The datatest-stable Contributors
// SPDX-License-Identifier: MIT OR Apache-2.0

#[test]
fn run_example() {
let output = std::process::Command::new("cargo")
.args(["nextest", "run", "--test=example", "--color=never"])
.env("__DATATEST_FULL_SCAN_FORBIDDEN", "1")
.output()
.expect("Failed to run `cargo nextest`");

// It's a pain to make assertions on byte slices (even a subslice check isn't easy)
// and it's also difficult to print nice error messages. So we just assume all
// nextest output will be utf8 and convert it.
let stderr = std::str::from_utf8(&output.stderr).expect("cargo nextest stderr should be utf-8");

assert!(
output.status.success(),
"Command failed (exit status: {}, stderr: {stderr})",
output.status
);

let lines: &[&str] = &[
"datatest-stable::example test_artifact::::colon::dir/::.txt",
"datatest-stable::example test_artifact::::colon::dir/a.txt",
"datatest-stable::example test_artifact::a.txt",
"datatest-stable::example test_artifact_utf8::::colon::dir/::.txt",
"datatest-stable::example test_artifact::b.txt",
"datatest-stable::example test_artifact_utf8::::colon::dir/a.txt",
"datatest-stable::example test_artifact_utf8::a.txt",
"datatest-stable::example test_artifact_utf8::c.skip.txt",
"datatest-stable::example test_artifact_utf8::b.txt",
"9 tests run: 9 passed, 0 skipped",
];

for line in lines {
assert!(
stderr.contains(line),
"Expected to find substring\n {line}\nin stderr\n {stderr}",
);
}
}