tests: allow multiple integration tests in check for forgotton test files

The previous implementation of `assert_no_forgotten_test_files`
hard-coded the name of the `runner` integration test and required all
other source files to appear in matching `mod` declarations. Thus, this
approach cannot handle multiple integration tests.

However, additional integration tests may be desirable
- to support tests using a custom test harness (see upcoming commits)
- to balance the trade-off between test run time and compile time as
  the test suite grows in the future.

The new implementation first uses `taplo` to parse the `[[test]]`
sections of the manifest to identify integration test main modules,
and then searches in those for `mod` declarations. This is then compared
to the list of source files in the tests directory. Like the previous
implementation, the new one does not attempt to recurse into submodules
or to handle directory-style modules; just like before it only treats
source files without a module declaration as an error and relies on the
compiler to complain about the other way around.

When `taplo` is not installed, the check is skipped unless it is running
in CI where we require `taplo` to be available.
This commit is contained in:
Jonas Greitemann 2025-04-10 10:54:19 +02:00 committed by Jonas Greitemann
parent 69cf7b38fc
commit 8882f0016d

View File

@ -13,6 +13,7 @@
// limitations under the License.
use std::collections::HashMap;
use std::collections::HashSet;
use std::env;
use std::fs;
use std::fs::OpenOptions;
@ -20,8 +21,11 @@ use std::io::Read as _;
use std::io::Write as _;
use std::path::Path;
use std::path::PathBuf;
use std::process::Command;
use std::process::Stdio;
use std::sync::Arc;
use bstr::ByteSlice as _;
use itertools::Itertools as _;
use jj_lib::backend;
use jj_lib::backend::Backend;
@ -621,21 +625,80 @@ pub fn assert_abandoned_with_parent(
}
pub fn assert_no_forgotten_test_files(test_dir: &Path) {
let runner_path = test_dir.join("runner.rs");
let runner = fs::read_to_string(&runner_path).unwrap();
let entries = fs::read_dir(test_dir).unwrap();
for entry in entries {
let path = entry.unwrap().path();
if let Some(ext) = path.extension() {
let name = path.file_stem().unwrap();
if ext == "rs" && name != "runner" {
let search = format!("mod {};", name.to_str().unwrap());
assert!(
runner.contains(&search),
"missing `{search}` declaration in {}",
runner_path.display()
);
}
}
// We require `taplo` for this check; if it's not installed, that's ok unless
// we're running in CI.
if Command::new("taplo")
.arg("--version")
.stdout(Stdio::null())
.status()
.is_err()
{
ensure_running_outside_ci("`taplo` must be in the PATH");
eprintln!(
"Skipping check for forgotten test files because taplo is not installed on the system"
);
return;
}
// Use taplo to find all the test executable main modules listed in `[[test]]`.
let taplo_output = Command::new("taplo")
.args(["get", "test[*].name", "-f", "Cargo.toml"])
.current_dir(test_dir.parent().unwrap())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
.unwrap();
assert!(
taplo_output.status.success(),
"taplo returned with status {}: {}",
taplo_output.status,
taplo_output.stderr.to_str_lossy(),
);
let test_bin_mods = taplo_output
.stdout
.to_str()
.unwrap()
.trim()
.lines()
.map(ToString::to_string)
.collect_vec();
// Add to that all submodules which are declared in the main test modules via
// `mod`.
let mut test_mods: HashSet<_> = test_bin_mods
.iter()
.flat_map(|test_mod| {
let test_mod_path = test_dir.join(test_mod).with_extension("rs");
let test_mod_contents = fs::read_to_string(&test_mod_path).unwrap();
test_mod_contents
.lines()
.map(|line| line.trim_start_matches("pub "))
.filter_map(|line| line.strip_prefix("mod"))
.filter_map(|line| line.strip_suffix(";"))
.map(|line| line.trim().to_string())
.collect_vec()
})
.collect();
test_mods.extend(test_bin_mods);
// Gather list of Rust source files in test directory for comparison.
let test_mod_files: HashSet<_> = fs::read_dir(test_dir)
.unwrap()
.map(|entry| entry.unwrap().path())
.filter(|path| path.extension().is_some_and(|ext| ext == "rs"))
.filter_map(|path| {
path.file_stem()
.and_then(|stem| stem.to_os_string().into_string().ok())
})
.collect();
assert!(
test_mod_files.is_subset(&test_mods),
"the following test source files are not declared as integration tests nor included as \
submodules of one: {}",
test_mod_files
.difference(&test_mods)
.map(|mod_stem| format!("{mod_stem}.rs"))
.join(", "),
);
}