use std::path::{Path, PathBuf};
use crate::cli::Cli;
use crate::config::{self, Config, Suite};
use crate::discovery;
use crate::dylib;
use crate::error::{Error, Outcome};
use crate::exit::ExitCode;
use crate::freshness::FreshnessSnapshot;
use crate::lock;
use crate::manifest::{self, Manifest};
use crate::normalize::NormalizationContext;
use crate::suite_workspace;
use crate::toolchain::{self, Toolchain};
use crate::util;
use crate::verdict::FixtureResult;
use crate::worker::{self, WorkerContext};
#[derive(Debug)]
pub struct Report {
pub results: Vec<FixtureResult>,
pub cleanup_residue: bool,
pub wall_ms: u64,
pub suites_run: Vec<String>,
}
impl Report {
pub fn exit_code(&self) -> ExitCode {
let mut code = ExitCode::Ok;
for r in &self.results {
code = code.merge(r.verdict.exit_code());
}
if self.cleanup_residue {
code = code.merge(ExitCode::CleanupResidue);
}
code
}
}
pub fn run(cli: Cli) -> Result<Report, Error> {
cli.validate_mode_consistency()?;
let manifest_path = resolve_manifest_path(&cli)?;
let crate_root = derive_crate_root(&manifest_path);
let config = config::load(&manifest_path)?;
let toolchain = toolchain::capture()?;
let selected_indexes = select_suites_by_cli(&config.suites, &cli.suite)?;
let multi_suite = selected_indexes.len() > 1;
if cli.list {
for &idx in &selected_indexes {
let suite = &config.suites[idx];
if multi_suite {
eprintln!("# suite \"{}\"", suite.name);
}
let fixtures = discovery::collect(suite, &crate_root, &cli.filter)?;
for f in &fixtures {
println!("{}", f.relative_path);
}
}
return Ok(Report {
results: Vec::new(),
cleanup_residue: false,
wall_ms: 0,
suites_run: selected_indexes
.iter()
.map(|&i| config.suites[i].name.clone())
.collect(),
});
}
let workspace_target = dylib::workspace_target_dir(&manifest_path);
let _session_lock = lock::SessionLock::acquire(&workspace_target)?;
if cli.no_cache {
for suite in &config.suites {
let manifest_dest = manifest::manifest_path_for_suite(&workspace_target, &suite.name);
util::remove_path_race_free(&manifest_dest, "prior session cache manifest")?;
let build_dir = dylib::build_dir_for_suite(&workspace_target, &suite.name);
util::remove_path_race_free(&build_dir, "prior session cache build dir")?;
}
if !cli.quiet {
let n = config.suites.len();
let plural = if n == 1 { "" } else { "s" };
eprintln!(
"lihaaf: --no-cache: removed any prior manifest + lihaaf-build dir across {n} suite{plural}"
);
}
}
let session_temp = create_session_temp_dir(&workspace_target)?;
let session_temp_path = session_temp.path().to_path_buf();
let total_dispatch_start = std::time::Instant::now();
let mut all_results: Vec<FixtureResult> = Vec::new();
let mut suites_run: Vec<String> = Vec::with_capacity(selected_indexes.len());
for &idx in &selected_indexes {
let suite = &config.suites[idx];
if multi_suite && !cli.quiet {
eprintln!("lihaaf: === suite \"{}\" ===", suite.name);
}
let suite_results = run_one_suite(SuiteRunInput {
suite,
config: &config,
cli: &cli,
crate_root: &crate_root,
manifest_path: &manifest_path,
workspace_target: &workspace_target,
toolchain: &toolchain,
session_temp: &session_temp_path,
})?;
if multi_suite {
print_per_suite_aggregate(suite, &suite_results);
}
all_results.extend(suite_results);
suites_run.push(suite.name.clone());
}
let wall_ms = total_dispatch_start.elapsed().as_millis() as u64;
let cleanup_residue = all_results.iter().any(|r| r.cleanup_failure.is_some());
print_aggregate(&all_results, wall_ms, cleanup_residue);
if cli.keep_output {
eprintln!(
"lihaaf: --keep-output set; per-fixture workdirs preserved under {}",
session_temp_path.display()
);
std::mem::forget(session_temp);
} else if cleanup_residue {
eprintln!(
"lihaaf: CLEANUP_RESIDUE detected; session-temp parent preserved at {}",
session_temp_path.display()
);
std::mem::forget(session_temp);
}
Ok(Report {
results: all_results,
cleanup_residue,
wall_ms,
suites_run,
})
}
fn create_session_temp_dir(workspace_target: &std::path::Path) -> Result<tempfile::TempDir, Error> {
std::fs::create_dir_all(workspace_target).map_err(|e| {
Error::io(
e,
"creating session temp parent dir",
Some(workspace_target.to_path_buf()),
)
})?;
tempfile::Builder::new()
.prefix("lihaaf-session-")
.tempdir_in(workspace_target)
.map_err(|e| {
Error::io(
e,
"creating session temp dir",
Some(workspace_target.to_path_buf()),
)
})
}
#[cfg(test)]
mod session_temp_parent_tests {
use super::create_session_temp_dir;
#[test]
fn creates_missing_target_parent_before_session_tempdir() {
let tmp = tempfile::tempdir().unwrap();
let target = tmp.path().join("fixture-crate").join("target");
let session_temp = create_session_temp_dir(&target).unwrap();
assert!(target.is_dir());
assert!(session_temp.path().starts_with(&target));
}
}
struct SuiteRunInput<'a> {
suite: &'a Suite,
config: &'a Config,
cli: &'a Cli,
crate_root: &'a Path,
manifest_path: &'a Path,
workspace_target: &'a Path,
toolchain: &'a Toolchain,
session_temp: &'a Path,
}
fn run_one_suite(input: SuiteRunInput<'_>) -> Result<Vec<FixtureResult>, Error> {
let SuiteRunInput {
suite,
config,
cli,
crate_root,
manifest_path,
workspace_target,
toolchain,
session_temp,
} = input;
let lihaaf_build_dir = dylib::build_dir_for_suite(workspace_target, &suite.name);
let build_started = std::time::Instant::now();
let build_out = if suite.build_targets.is_empty() {
dylib::build(&dylib::BuildParams {
crate_name: &config.dylib_crate,
features: &suite.features,
manifest_path,
target_dir: &lihaaf_build_dir,
toolchain,
})?
} else {
suite_workspace::build(&suite_workspace::BuildParams {
dylib_crate: &config.dylib_crate,
suite,
metadata_manifest_path: manifest_path,
target_dir: &lihaaf_build_dir,
toolchain,
})?
};
if !cli.quiet {
let secs = build_started.elapsed().as_secs_f64();
eprintln!("lihaaf: built {} dylib in {:.1}s", config.dylib_crate, secs);
}
let managed_path = dylib::managed_dylib_path(workspace_target, &build_out.cargo_dylib_path);
if cli.use_symlink {
dylib::symlink_dylib(&build_out.cargo_dylib_path, &managed_path)?;
} else {
dylib::copy_dylib(&build_out.cargo_dylib_path, &managed_path)?;
}
let dylib_sha = util::sha256_file(&managed_path)?;
let dylib_mtime = dylib::mtime_unix_secs(&managed_path)?;
let metadata_snapshot = toml_value_to_json(&config.raw_metadata);
let manifest_obj = Manifest {
lihaaf_version: crate::VERSION.to_string(),
suite_name: suite.name.clone(),
rustc_release: toolchain.release_line.clone(),
rustc_commit_hash: toolchain.commit_hash.clone(),
host_triple: toolchain.host.clone(),
sysroot: toolchain.sysroot.clone(),
dylib_crate: config.dylib_crate.clone(),
cargo_dylib_path: build_out.cargo_dylib_path.clone(),
managed_dylib_path: managed_path.clone(),
dylib_sha256: dylib_sha.clone(),
dylib_mtime_unix_secs: dylib_mtime,
use_symlink: cli.use_symlink,
features: suite.features.clone(),
extern_crates: suite.extern_crates.clone(),
edition: suite.edition.clone(),
metadata_snapshot,
};
let manifest_dest = manifest::manifest_path_for_suite(workspace_target, &suite.name);
manifest_obj.write(&manifest_dest)?;
let freshness_snapshot = FreshnessSnapshot {
managed_dylib_path: managed_path.clone(),
original_mtime_unix_secs: dylib_mtime,
original_sha256: dylib_sha.clone(),
original_toolchain: toolchain.clone(),
};
let fixtures = discovery::collect(suite, crate_root, &cli.filter)?;
if !cli.quiet {
let cf = fixtures
.iter()
.filter(|f| matches!(f.kind, discovery::FixtureKind::CompileFail))
.count();
let cp = fixtures.len() - cf;
eprintln!(
"lihaaf: {} fixtures discovered (compile_fail: {cf}, compile_pass: {cp})",
fixtures.len()
);
}
let parallelism = compute_parallelism(cli, suite);
if !cli.quiet {
eprintln!("lihaaf: parallelism = {parallelism}");
}
let normalization_root = normalization_root_for_suite(suite, manifest_path, crate_root)?;
let norm_ctx = NormalizationContext::new(normalization_root, toolchain.sysroot.clone())
.with_compat_short_cargo(cli.inner_compat_normalize);
let mut worker_ctx = WorkerContext::new(
crate_root.to_path_buf(),
managed_path.clone(),
build_out.deps_dir.clone(),
&config.dylib_crate,
suite,
cli.effective_bless(),
cli.verbose,
cli.keep_output,
session_temp.to_path_buf(),
norm_ctx,
&toolchain.sysroot,
freshness_snapshot,
);
let mut extra_names = worker_ctx.extra_extern_crates.clone();
extra_names.extend(worker_ctx.dev_deps.iter().cloned());
worker_ctx.extern_paths = worker::resolve_extern_paths(&build_out.deps_dir, &extra_names)?;
let post_capture = toolchain::capture()?;
if !toolchain::matches(toolchain, &post_capture) {
return Err(Error::Session(Outcome::ToolchainDrift {
original: toolchain::format_drift_key(toolchain),
current: toolchain::format_drift_key(&post_capture),
}));
}
let progress_quiet = cli.quiet;
let dispatch_outcome = worker::dispatch_pool(&fixtures, &worker_ctx, parallelism, move |r| {
if progress_quiet {
if !r.verdict.is_pass() {
eprintln!("lihaaf: {} {}", r.verdict.label(), r.relative_path);
}
} else {
eprintln!(
"lihaaf: {:>26} {} ({} ms)",
r.verdict.label(),
r.relative_path,
r.wall_ms
);
}
emit_fixture_warnings(r);
});
if let Some(failure) = dispatch_outcome.freshness_failure {
return Err(Error::Session(Outcome::FreshnessDrift {
invariant: failure.invariant_label().to_string(),
detail: failure.detail(),
}));
}
Ok(dispatch_outcome.results)
}
pub fn select_suites_by_cli(
suites: &[Suite],
cli_selection: &[String],
) -> Result<Vec<usize>, Error> {
if cli_selection.is_empty() {
return Ok((0..suites.len()).collect());
}
let mut requested: std::collections::BTreeSet<&str> = std::collections::BTreeSet::new();
for name in cli_selection {
if !suites.iter().any(|s| s.name == *name) {
let known: Vec<String> = suites.iter().map(|s| format!("\"{}\"", s.name)).collect();
return Err(Error::Session(Outcome::ConfigInvalid {
message: format!(
"--suite \"{name}\" does not match any suite in [package.metadata.lihaaf]. Known suites: [{}].\nWhy this matters: lihaaf only runs suites that are declared in metadata; --suite cannot create a new one.",
known.join(", ")
),
}));
}
requested.insert(name.as_str());
}
Ok(suites
.iter()
.enumerate()
.filter(|(_, s)| requested.contains(s.name.as_str()))
.map(|(i, _)| i)
.collect())
}
fn print_aggregate(results: &[FixtureResult], wall_ms: u64, cleanup_residue: bool) {
use crate::verdict::Verdict;
let mut counts: std::collections::BTreeMap<&'static str, usize> =
std::collections::BTreeMap::new();
for r in results {
*counts.entry(r.verdict.label()).or_insert(0) += 1;
}
let mut summary = String::new();
for (label, n) in &counts {
if !summary.is_empty() {
summary.push_str(", ");
}
summary.push_str(&format!("{n} {label}"));
}
if results.is_empty() {
summary.push_str("0 results");
}
eprintln!("lihaaf: {summary}");
let aggregate = aggregate_counts(results);
eprintln!(
"lihaaf: {} ok, {} failed, {} timeout, {} memory_exhausted",
aggregate.ok, aggregate.failed, aggregate.timeout, aggregate.memory_exhausted
);
eprintln!("lihaaf: total wall-clock: {:.1}s", wall_ms as f64 / 1000.0);
if cleanup_residue {
eprintln!("lihaaf: CLEANUP_RESIDUE — one or more workdirs could not be removed:");
for r in results {
if let Some(c) = &r.cleanup_failure {
eprintln!(
" {} (path={}, error={})",
r.relative_path,
c.path.display(),
c.message
);
}
}
}
for r in results {
match &r.verdict {
Verdict::SnapshotDiff { diff } => {
eprintln!("\n=== {} (SNAPSHOT_DIFF) ===\n{diff}", r.relative_path);
}
Verdict::SnapshotMissing { actual } => {
eprintln!(
"\n=== {} (SNAPSHOT_MISSING) ===\n--- actual normalized stderr ---\n{actual}",
r.relative_path
);
}
Verdict::ExpectedFailButPassed => {
eprintln!(
"\n=== {} (EXPECTED_FAIL_BUT_PASSED) ===\nfixture compiled successfully but is in a compile_fail dir",
r.relative_path
);
}
Verdict::ExpectedPassButFailed { stderr } => {
eprintln!(
"\n=== {} (EXPECTED_PASS_BUT_FAILED) ===\n{stderr}",
r.relative_path
);
}
Verdict::SnapshotDiffTooLarge {
actual_lines,
expected_lines,
actual_head,
expected_head,
} => {
eprintln!(
"\n=== {} (SNAPSHOT_DIFF_TOO_LARGE) ===\nactual: {actual_lines} lines, expected: {expected_lines} lines",
r.relative_path
);
eprintln!("--- actual head ---\n{actual_head}");
eprintln!("--- expected head ---\n{expected_head}");
}
_ => {}
}
}
}
fn print_per_suite_aggregate(suite: &Suite, results: &[FixtureResult]) {
let agg = aggregate_counts(results);
eprintln!(
"lihaaf: suite \"{}\": {} ok, {} failed, {} timeout, {} memory_exhausted",
suite.name, agg.ok, agg.failed, agg.timeout, agg.memory_exhausted
);
}
fn emit_fixture_warnings(r: &FixtureResult) {
use crate::verdict::FixtureWarning;
if let Some(w) = &r.warning {
match w {
FixtureWarning::LargeSnapshot {
expected_lines,
actual_lines,
} => {
eprintln!(
"lihaaf: LARGE_SNAPSHOT {} ({}/{} lines)",
r.relative_path, expected_lines, actual_lines
);
}
}
}
}
#[derive(Debug, Default, Clone, Copy)]
struct AggregateCounts {
ok: usize,
failed: usize,
timeout: usize,
memory_exhausted: usize,
}
fn aggregate_counts(results: &[FixtureResult]) -> AggregateCounts {
use crate::verdict::Verdict;
let mut a = AggregateCounts::default();
for r in results {
match &r.verdict {
Verdict::Ok | Verdict::Blessed { .. } => a.ok += 1,
Verdict::Timeout => a.timeout += 1,
Verdict::MemoryExhausted => a.memory_exhausted += 1,
_ => a.failed += 1,
}
}
a
}
fn compute_parallelism(cli: &Cli, suite: &Suite) -> usize {
let cpu_cap: usize = std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(1);
let ram_cap: usize = match util::total_ram_mb() {
Some(total) => {
let cap = total / suite.per_fixture_memory_mb as u64;
cap.max(1) as usize
}
None => cpu_cap, };
let cli_cap: usize = cli.jobs.map(|n| n as usize).unwrap_or(cpu_cap);
cli_cap.min(ram_cap)
}
fn resolve_manifest_path(cli: &Cli) -> Result<PathBuf, Error> {
if let Some(p) = &cli.manifest_path {
if !p.is_file() {
return Err(Error::Session(Outcome::ConfigInvalid {
message: format!(
"--manifest-path={} does not point at an existing file.\nWhy this matters: lihaaf needs the consumer's Cargo.toml.",
p.display()
),
}));
}
return absolutize_manifest_path(p);
}
let mut dir =
std::env::current_dir().map_err(|e| Error::io(e, "reading current directory", None))?;
loop {
let candidate = dir.join("Cargo.toml");
if candidate.is_file() {
return Ok(candidate);
}
if !dir.pop() {
return Err(Error::Session(Outcome::ConfigInvalid {
message:
"no Cargo.toml found in the current directory or any parent.\nWhy this matters: lihaaf reads `[package.metadata.lihaaf]` from the consumer crate.\nFix: cd into a crate directory or pass --manifest-path."
.into(),
}));
}
}
}
fn absolutize_manifest_path(p: &Path) -> Result<PathBuf, Error> {
if p.is_absolute() {
return Ok(p.to_path_buf());
}
let cwd =
std::env::current_dir().map_err(|e| Error::io(e, "reading current directory", None))?;
Ok(cwd.join(p))
}
fn derive_crate_root(manifest_path: &Path) -> PathBuf {
match manifest_path.parent() {
Some(p) if !p.as_os_str().is_empty() => p.to_path_buf(),
_ => PathBuf::from("."),
}
}
fn normalization_root_for_suite(
suite: &Suite,
manifest_path: &Path,
crate_root: &Path,
) -> Result<PathBuf, Error> {
if suite.build_targets.is_empty() {
return Ok(crate_root.to_path_buf());
}
staged_suite_normalization_root(manifest_path, crate_root)
}
fn staged_suite_normalization_root(
manifest_path: &Path,
crate_root: &Path,
) -> Result<PathBuf, Error> {
for dir in crate_root.ancestors() {
let candidate = dir.join("Cargo.toml");
if !candidate.is_file() {
continue;
}
let text = std::fs::read_to_string(&candidate).map_err(|e| {
Error::io(
e,
"reading ancestor Cargo.toml for staged-suite normalization root",
Some(candidate.clone()),
)
})?;
let value: toml::Value =
toml::from_str(&text).map_err(|e: toml::de::Error| Error::TomlParse {
path: candidate.clone(),
message: format!(
"{e}\nWhy this matters: manifest `{}` belongs to a suite with \
build_targets, so lihaaf needs the ancestor workspace root to normalize \
sibling-member diagnostics.",
manifest_path.display()
),
})?;
if value
.as_table()
.is_some_and(|table| table.contains_key("workspace"))
{
return Ok(dir.to_path_buf());
}
}
Ok(crate_root.to_path_buf())
}
fn toml_value_to_json(v: &toml::Value) -> serde_json::Value {
match v {
toml::Value::String(s) => serde_json::Value::String(s.clone()),
toml::Value::Integer(i) => serde_json::Value::Number((*i).into()),
toml::Value::Float(f) => serde_json::Number::from_f64(*f)
.map(serde_json::Value::Number)
.unwrap_or(serde_json::Value::Null),
toml::Value::Boolean(b) => serde_json::Value::Bool(*b),
toml::Value::Datetime(d) => serde_json::Value::String(d.to_string()),
toml::Value::Array(a) => {
serde_json::Value::Array(a.iter().map(toml_value_to_json).collect())
}
toml::Value::Table(t) => {
let mut map = serde_json::Map::new();
for (k, v) in t {
map.insert(k.clone(), toml_value_to_json(v));
}
serde_json::Value::Object(map)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::DEFAULT_SUITE_NAME;
fn suite(name: &str, per_fixture_mb: u32) -> Suite {
Suite {
name: name.into(),
extern_crates: vec!["x".into()],
fixture_dirs: vec![],
features: vec![],
allow_lints: vec![],
edition: "2021".into(),
dev_deps: vec![],
build_targets: Default::default(),
compile_fail_marker: "compile_fail".into(),
fixture_timeout_secs: 90,
per_fixture_memory_mb: per_fixture_mb,
extra_substitutions: vec![],
strip_lines: vec![],
strip_line_prefixes: vec![],
}
}
fn default_test_cli() -> Cli {
Cli {
bless: false,
compat: false,
compat_cargo_test_argv: None,
compat_commit: None,
compat_filter: vec![],
compat_manifest: None,
compat_package: None,
compat_report: None,
compat_root: None,
compat_trybuild_macro: vec![],
filter: vec![],
jobs: None,
suite: vec![],
no_cache: false,
manifest_path: None,
list: false,
quiet: true,
verbose: false,
use_symlink: false,
keep_output: false,
inner_compat_normalize: false,
}
}
#[test]
fn parallelism_respects_explicit_jobs() {
let mut cli = default_test_cli();
cli.jobs = Some(2);
let p = compute_parallelism(&cli, &suite(DEFAULT_SUITE_NAME, 1024));
assert!(p <= 2);
cli.jobs = Some(1);
let p2 = compute_parallelism(&cli, &suite(DEFAULT_SUITE_NAME, 1024));
assert_eq!(p2, 1);
}
#[test]
fn parallelism_is_at_least_one() {
let cli = default_test_cli();
let p = compute_parallelism(&cli, &suite(DEFAULT_SUITE_NAME, u32::MAX / 2));
assert!(p >= 1);
}
#[test]
fn fixture_warnings_default_to_none_on_construction() {
use crate::verdict::{FixtureResult, Verdict};
let r = FixtureResult {
relative_path: "x".into(),
verdict: Verdict::Ok,
cleanup_failure: None,
wall_ms: 0,
warning: None,
};
assert!(r.warning.is_none());
}
#[test]
fn aggregate_counts_buckets_per_section_3_3() {
use crate::verdict::{FixtureResult, Verdict};
let r = |label: &str, v: Verdict| FixtureResult {
relative_path: label.to_string(),
verdict: v,
cleanup_failure: None,
wall_ms: 0,
warning: None,
};
let results = vec![
r("a", Verdict::Ok),
r(
"b",
Verdict::Blessed {
snapshot_path: PathBuf::from("/tmp/x.stderr"),
},
),
r("c", Verdict::Timeout),
r("d", Verdict::Timeout),
r("e", Verdict::MemoryExhausted),
r(
"f",
Verdict::SnapshotDiff {
diff: "diff".into(),
},
),
r("g", Verdict::ExpectedFailButPassed),
r(
"h",
Verdict::WorkerCrashed {
cause: "signal: 11".into(),
},
),
];
let agg = aggregate_counts(&results);
assert_eq!(agg.ok, 2);
assert_eq!(agg.failed, 3);
assert_eq!(agg.timeout, 2);
assert_eq!(agg.memory_exhausted, 1);
}
fn assert_derive_crate_root_equals(input: &str, expected: &str) {
let root = derive_crate_root(&PathBuf::from(input));
assert_eq!(root, PathBuf::from(expected));
assert!(
!root.as_os_str().is_empty(),
"crate root must never be the empty path"
);
}
#[test]
fn derive_crate_root_handles_bare_cargo_toml() {
assert_derive_crate_root_equals("Cargo.toml", ".");
}
#[test]
fn derive_crate_root_falls_back_for_parentless_input() {
assert_derive_crate_root_equals("", ".");
}
#[test]
fn derive_crate_root_returns_parent_of_absolute_manifest() {
#[cfg(unix)]
assert_derive_crate_root_equals("/abs/pkg/Cargo.toml", "/abs/pkg");
#[cfg(windows)]
assert_derive_crate_root_equals(r"C:\abs\pkg\Cargo.toml", r"C:\abs\pkg");
}
#[test]
fn derive_crate_root_returns_parent_of_relative_workspace_member() {
assert_derive_crate_root_equals("member/Cargo.toml", "member");
}
#[test]
fn default_suite_normalization_root_stays_at_crate_root_under_workspace() {
let tmp = tempfile::tempdir().unwrap();
let workspace_root = tmp.path();
let member_dir = workspace_root.join("axum-macros");
std::fs::create_dir_all(&member_dir).unwrap();
std::fs::write(
workspace_root.join("Cargo.toml"),
"[workspace]\nmembers = [\"axum-macros\"]\n",
)
.unwrap();
let member_manifest = member_dir.join("Cargo.toml");
std::fs::write(&member_manifest, "[package]\nname = \"axum-macros\"\n").unwrap();
let crate_root = derive_crate_root(&member_manifest);
let root = normalization_root_for_suite(
&suite(DEFAULT_SUITE_NAME, 1024),
&member_manifest,
&crate_root,
)
.unwrap();
assert_eq!(root, member_dir);
}
#[test]
fn staged_suite_normalization_root_uses_ancestor_workspace_root() {
let tmp = tempfile::tempdir().unwrap();
let workspace_root = tmp.path();
let member_dir = workspace_root.join("axum-macros");
let sibling_dir = workspace_root.join("axum").join("src");
std::fs::create_dir_all(&member_dir).unwrap();
std::fs::create_dir_all(&sibling_dir).unwrap();
std::fs::write(
workspace_root.join("Cargo.toml"),
"[workspace]\nmembers = [\"axum-macros\", \"axum\"]\n",
)
.unwrap();
let member_manifest = member_dir.join("Cargo.toml");
std::fs::write(&member_manifest, "[package]\nname = \"axum-macros\"\n").unwrap();
let crate_root = derive_crate_root(&member_manifest);
let mut staged = suite("from_request", 1024);
staged.build_targets = crate::config::BuildTargets::try_from(vec!["tests".into()]).unwrap();
staged.dev_deps = vec!["axum".into()];
let root = normalization_root_for_suite(&staged, &member_manifest, &crate_root).unwrap();
let ctx = NormalizationContext::new(root.clone(), PathBuf::from("/rust"));
let actual = crate::normalize::normalize(
&format!(
"note: required by a bound\n --> {}/method_routing.rs:167:16\n",
sibling_dir.display()
),
&ctx,
&member_dir.join("tests/from_request/fail"),
);
assert_eq!(root, workspace_root);
assert!(actual.contains("$WORKSPACE/axum/src/method_routing.rs:167:16"));
assert!(
!actual.contains(workspace_root.to_string_lossy().as_ref()),
"local absolute workspace path leaked through: {actual}"
);
}
#[test]
fn absolutize_manifest_path_promotes_bare_cargo_toml_to_absolute() {
let abs = absolutize_manifest_path(&PathBuf::from("Cargo.toml"))
.expect("current_dir is readable in this test");
assert!(
abs.is_absolute(),
"absolutized manifest path must be absolute, got {abs:?}"
);
let root = derive_crate_root(&abs);
assert!(
root.is_absolute(),
"derived crate root must be absolute, got {root:?}"
);
assert!(
!root.as_os_str().is_empty(),
"derived crate root must never be empty"
);
let cwd = std::env::current_dir().unwrap();
assert_eq!(root, cwd);
}
#[test]
fn absolutize_manifest_path_preserves_absolute_input() {
#[cfg(unix)]
let input = PathBuf::from("/etc/Cargo.toml");
#[cfg(windows)]
let input = PathBuf::from(r"C:\etc\Cargo.toml");
let out = absolutize_manifest_path(&input).unwrap();
assert_eq!(out, input);
}
#[test]
fn toml_to_json_round_trips_table_shape() {
let toml_text = r#"
a = 1
b = "two"
c = [3, 4]
[d]
e = true
"#;
let v: toml::Value = toml::from_str(toml_text).unwrap();
let j = toml_value_to_json(&v);
assert_eq!(j["a"], serde_json::json!(1));
assert_eq!(j["b"], serde_json::json!("two"));
assert_eq!(j["c"], serde_json::json!([3, 4]));
assert_eq!(j["d"]["e"], serde_json::json!(true));
}
fn suites_named(names: &[&str]) -> Vec<Suite> {
names.iter().map(|n| suite(n, 1024)).collect()
}
#[test]
fn select_suites_empty_cli_returns_all_in_declared_order() {
let suites = suites_named(&["default", "spatial", "extra"]);
let idx = select_suites_by_cli(&suites, &[]).unwrap();
assert_eq!(idx, vec![0, 1, 2]);
}
#[test]
fn select_suites_filters_to_named_subset_in_declared_order() {
let suites = suites_named(&["default", "spatial", "extra"]);
let idx =
select_suites_by_cli(&suites, &["extra".to_string(), "spatial".to_string()]).unwrap();
assert_eq!(idx, vec![1, 2]);
}
#[test]
fn select_suites_dedups_repeated_cli_names() {
let suites = suites_named(&["default", "spatial"]);
let idx = select_suites_by_cli(
&suites,
&[
"spatial".to_string(),
"spatial".to_string(),
"default".to_string(),
],
)
.unwrap();
assert_eq!(idx, vec![0, 1]);
}
#[test]
fn select_suites_unknown_name_rejected_with_known_list() {
let suites = suites_named(&["default", "spatial"]);
let err = select_suites_by_cli(&suites, &["unknown".to_string()]).unwrap_err();
match err {
Error::Session(Outcome::ConfigInvalid { message }) => {
assert!(message.contains("\"unknown\""));
assert!(message.contains("\"default\""));
assert!(message.contains("\"spatial\""));
}
other => panic!("expected ConfigInvalid, got {other:?}"),
}
}
#[test]
fn select_suites_default_is_addressable_by_cli_name() {
let suites = suites_named(&["default", "spatial"]);
let idx = select_suites_by_cli(&suites, &["default".to_string()]).unwrap();
assert_eq!(idx, vec![0]);
}
}