use std::collections::BTreeSet;
use std::ffi::OsString;
use std::path::{Path, PathBuf};
use testing_conventions::colocated_test::{missing_unit_tests, Language};
use testing_conventions::run;
fn fixture(name: &str) -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures/colocated_test")
.join(name)
}
fn relative_orphans(root: &Path, language: Language) -> Vec<String> {
orphans_with(root, language, &BTreeSet::new())
}
fn orphans_with(root: &Path, language: Language, exempt: &BTreeSet<String>) -> Vec<String> {
missing_unit_tests(root, language, exempt)
.expect("walking a readable tree should succeed")
.iter()
.map(|path| {
path.strip_prefix(root)
.expect("an orphan must live under the scanned root")
.to_string_lossy()
.replace('\\', "/")
})
.collect()
}
fn exempt(paths: &[&str]) -> BTreeSet<String> {
paths.iter().map(|p| p.to_string()).collect()
}
fn unit_colocated_test_run(fixture_name: &str, language: &str) -> anyhow::Result<i32> {
let dir = fixture(fixture_name);
let config = dir.join("testing-conventions.toml");
let argv: Vec<OsString> = vec![
"testing-conventions".into(),
"unit".into(),
"colocated-test".into(),
"--language".into(),
language.into(),
"--config".into(),
config.into_os_string(),
dir.into_os_string(),
];
run(argv)
}
fn unit_colocated_test_exit(fixture_name: &str, language: &str) -> i32 {
unit_colocated_test_run(fixture_name, language).expect("a readable tree should not error")
}
#[test]
fn python_clean_tree_reports_no_orphans() {
assert!(
relative_orphans(&fixture("clean"), Language::Python).is_empty(),
"every source in the clean tree has a colocated _test.py"
);
}
#[test]
fn python_red_tree_reports_every_missing_twin() {
assert_eq!(
relative_orphans(&fixture("red"), Language::Python),
vec!["lonely.py", "pkg/orphan.py"],
);
}
#[test]
fn python_missing_root_is_an_error() {
assert!(
missing_unit_tests(
fixture("does_not_exist"),
Language::Python,
&BTreeSet::new()
)
.is_err(),
"an unreadable root must be an error"
);
}
#[test]
fn python_subcommand_exits_zero_on_a_clean_tree() {
assert_eq!(unit_colocated_test_exit("clean", "python"), 0);
}
#[test]
fn python_subcommand_exits_nonzero_on_a_red_tree() {
assert_eq!(unit_colocated_test_exit("red", "python"), 1);
}
#[test]
fn typescript_clean_tree_reports_no_orphans() {
assert!(
relative_orphans(&fixture("typescript/clean"), Language::TypeScript).is_empty(),
"every .ts/.tsx/.mts/.cts source is paired; declaration files are ignored"
);
}
#[test]
fn typescript_red_tree_reports_every_missing_twin() {
assert_eq!(
relative_orphans(&fixture("typescript/red"), Language::TypeScript),
vec!["bridge.cts", "lonely.ts", "pkg/orphan.ts", "solo.mts"],
);
}
#[test]
fn typescript_subcommand_exits_zero_on_a_clean_tree() {
assert_eq!(
unit_colocated_test_exit("typescript/clean", "typescript"),
0
);
}
#[test]
fn typescript_subcommand_exits_nonzero_on_a_red_tree() {
assert_eq!(unit_colocated_test_exit("typescript/red", "typescript"), 1);
}
#[test]
fn empty_init_is_a_non_subject_but_content_and_shims_are_orphans() {
assert_eq!(
relative_orphans(&fixture("python_exempt"), Language::Python),
vec!["cli.py", "pkg/__init__.py"],
);
}
#[test]
fn config_exemptions_lift_listed_files() {
assert!(orphans_with(
&fixture("python_exempt"),
Language::Python,
&exempt(&["cli.py", "pkg/__init__.py"]),
)
.is_empty());
}
#[test]
fn python_subcommand_exits_zero_with_config_exemptions() {
assert_eq!(unit_colocated_test_exit("python_exempt", "python"), 0);
}
#[test]
fn a_typescript_barrel_is_an_orphan_until_explicitly_exempted() {
assert_eq!(
relative_orphans(&fixture("typescript/exempt"), Language::TypeScript),
vec!["index.ts"],
);
assert_eq!(
unit_colocated_test_exit("typescript/exempt", "typescript"),
0
);
}
#[test]
fn a_stale_exempt_entry_is_an_error() {
assert!(
unit_colocated_test_run("stale_exempt", "python").is_err(),
"an exempt entry pointing at a missing file must error"
);
}
fn run_cli(args: &[&str]) -> anyhow::Result<i32> {
let argv: Vec<OsString> = std::iter::once(OsString::from("testing-conventions"))
.chain(args.iter().copied().map(OsString::from))
.collect();
run(argv)
}
#[test]
fn unit_colocated_test_requires_language() {
let err = run_cli(&["unit", "colocated-test", "src"]).expect_err("--language is required");
let clap_err = err
.downcast_ref::<clap::Error>()
.expect("a missing required flag should surface as a clap::Error");
assert_eq!(
clap_err.kind(),
clap::error::ErrorKind::MissingRequiredArgument
);
}
#[test]
fn the_flat_unit_location_subcommand_is_gone() {
let err = run_cli(&["unit-location", "src"]).expect_err("the flat subcommand was removed");
let clap_err = err
.downcast_ref::<clap::Error>()
.expect("an unknown subcommand should surface as a clap::Error");
assert_eq!(clap_err.kind(), clap::error::ErrorKind::InvalidSubcommand);
}
#[test]
fn the_old_unit_location_subcommand_is_renamed() {
let err = run_cli(&["unit", "location", "src"])
.expect_err("`unit location` was renamed to `unit colocated-test`");
let clap_err = err
.downcast_ref::<clap::Error>()
.expect("an unknown subcommand should surface as a clap::Error");
assert_eq!(clap_err.kind(), clap::error::ErrorKind::InvalidSubcommand);
}