diff --git a/Cargo.lock b/Cargo.lock index 948f81f..530e812 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -427,6 +427,7 @@ version = "0.0.2" dependencies = [ "cph-diag", "serde", + "tempfile", "toml", ] diff --git a/crates/cph-model/Cargo.toml b/crates/cph-model/Cargo.toml index 145ca5c..3f4fc64 100644 --- a/crates/cph-model/Cargo.toml +++ b/crates/cph-model/Cargo.toml @@ -10,3 +10,6 @@ serde = { workspace = true } # `preserve_order` keeps `[targets.*]` tables in TOML document order, which is # the declared order this crate exposes via `Lesson.targets` (ADR-0009). toml = { workspace = true, features = ["preserve_order"] } + +[dev-dependencies] +tempfile = "3" diff --git a/crates/cph-model/tests/load.rs b/crates/cph-model/tests/load.rs index de5aae6..378d00a 100644 --- a/crates/cph-model/tests/load.rs +++ b/crates/cph-model/tests/load.rs @@ -279,16 +279,15 @@ fn malformed_target_config_is_non_fatal_with_schema_violations() { /// match exactly (ADR-0016's MVP rule). const CPH_VERSION: &str = env!("CARGO_PKG_VERSION"); -/// Write a one-part lesson into a temp dir, optionally with a `.cph-version` -/// file. Returns the temp dir so the caller runs `load`. -fn tmp_lesson_with_version(version: Option<&str>) -> PathBuf { - let mut p = std::env::temp_dir(); - let nanos = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_nanos(); - p.push(format!("cph-version-test-{nanos}")); - std::fs::create_dir_all(&p).unwrap(); +/// Write a one-part lesson into a uniquely-created temp dir, optionally with a +/// `.cph-version` file. `TempDir` owns cleanup so parallel tests cannot delete +/// one another's fixture after a timestamp collision. +fn tmp_lesson_with_version(version: Option<&str>) -> tempfile::TempDir { + let tmp = tempfile::Builder::new() + .prefix("cph-version-test-") + .tempdir() + .unwrap(); + let p = tmp.path(); std::fs::write( p.join("manifest.toml"), "[project]\nid = \"v\"\nname = \"v\"\n[info]\ntitle = \"v\"\n[[parts]]\nkind = \"segment\"\npath = \"segments/a\"\n", @@ -301,14 +300,13 @@ fn tmp_lesson_with_version(version: Option<&str>) -> PathBuf { if let Some(v) = version { std::fs::write(p.join(".cph-version"), v).unwrap(); } - p + tmp } #[test] fn cph_version_matching_is_not_a_diagnostic() { let tmp = tmp_lesson_with_version(Some(CPH_VERSION)); - let (_lesson, diags) = load(&tmp); - let _ = std::fs::remove_dir_all(&tmp); + let (_lesson, diags) = load(tmp.path()); assert!( diags.iter().all(|d| d.code != DiagCode::CphVersionMismatch), "a matching .cph-version must not warn, got {diags:?}" @@ -318,8 +316,7 @@ fn cph_version_matching_is_not_a_diagnostic() { #[test] fn cph_version_mismatch_is_an_error_diagnostic() { let tmp = tmp_lesson_with_version(Some("99.99.99")); - let (lesson, diags) = load(&tmp); - let _ = std::fs::remove_dir_all(&tmp); + let (lesson, diags) = load(tmp.path()); // The lesson still loads (so other defects could surface), but the // mismatch is an error diagnostic — which alone makes it illegal. assert!(lesson.is_some(), "a mismatch should not halt loading"); @@ -339,8 +336,7 @@ fn cph_version_mismatch_is_an_error_diagnostic() { fn cph_version_missing_is_skipped() { // No `.cph-version` file at all → skipped (ADR-0016 migration period; OPEN). let tmp = tmp_lesson_with_version(None); - let (_lesson, diags) = load(&tmp); - let _ = std::fs::remove_dir_all(&tmp); + let (_lesson, diags) = load(tmp.path()); assert!( diags.iter().all(|d| d.code != DiagCode::CphVersionMismatch), "a missing .cph-version must be skipped, got {diags:?}" @@ -350,8 +346,7 @@ fn cph_version_missing_is_skipped() { #[test] fn cph_version_empty_is_an_error() { let tmp = tmp_lesson_with_version(Some(" \n")); - let (_lesson, diags) = load(&tmp); - let _ = std::fs::remove_dir_all(&tmp); + let (_lesson, diags) = load(tmp.path()); assert!( diags .iter()