pub(crate) mod baseline;
pub(crate) mod cleanup;
pub(crate) mod cli;
pub(crate) mod discovery;
pub(crate) mod fixture_convert;
pub(crate) mod gate;
pub(crate) mod overlay;
pub(crate) mod report;
pub(crate) mod rustup;
use std::path::{Path, PathBuf};
use std::time::Instant;
use crate::error::Error;
pub fn run(args: cli::CompatArgs) -> Result<(), Error> {
let started = Instant::now();
cleanup::install_panic_hook();
let compat_report = args.compat_report.clone();
let dual_root = resolve_dual_root(&args)?;
let compat_root = dual_root.workspace_root.clone();
let member_root = dual_root.member_root.clone();
let upstream_manifest = dual_root.member_manifest.clone();
let guard = cleanup::CleanupGuard::new(args.inner_cli.keep_output);
let converted_fixtures_root = member_root.join("target").join("lihaaf-compat-converted");
let abs_compile_pass = crate::util::to_forward_slash(
&converted_fixtures_root
.join("compile_pass")
.to_string_lossy(),
);
let abs_compile_fail = crate::util::to_forward_slash(
&converted_fixtures_root
.join("compile_fail")
.to_string_lossy(),
);
let overlay_plan = overlay::materialize_overlay_with_workspace_member_context(
&upstream_manifest,
|upstream_name| {
let name = upstream_name
.map(str::to_string)
.unwrap_or_else(|| basename_fallback(&member_root));
overlay::compat_default_synthetic_metadata(
&name,
vec![abs_compile_pass.clone(), abs_compile_fail.clone()],
)
},
dual_root.workspace_member_context.as_ref(),
)?;
let crate_name = overlay_plan
.upstream_crate_name
.clone()
.unwrap_or_else(|| basename_fallback(&member_root));
guard.track(overlay_plan.sibling_manifest.clone(), &compat_root);
let discovery_output = discovery::discover(&member_root, &args.compat_trybuild_macro)?;
let baseline_sidecar = compat_root
.join("target")
.join("lihaaf-compat-baseline.json");
let recognized: Vec<baseline::FixtureId> = discovery_output
.fixtures
.iter()
.map(|f| baseline::FixtureId {
repo_relative_path: PathBuf::from(f.relative_path.clone()),
})
.collect();
let baseline_result = baseline::run_baseline_with_recognized_fixtures(
&args.compat_cargo_test_argv,
&compat_root,
&baseline_sidecar,
&recognized,
)?;
guard.track(baseline_sidecar.clone(), &compat_root);
let converted =
fixture_convert::convert_fixtures(&member_root, &discovery_output.fixtures, &guard)?;
let lihaaf_started = Instant::now();
let inner_cli = build_inner_cli(&args, &overlay_plan.sibling_manifest);
let inner_result = crate::session::run(inner_cli);
let lihaaf_dur_ms = u64::try_from(lihaaf_started.elapsed().as_millis()).unwrap_or(u64::MAX);
let (lihaaf_pass, lihaaf_fail, lihaaf_exit_code, inner_session_error) = match inner_result {
Ok(report) => {
let pass = u32::try_from(
report
.results
.iter()
.filter(|r| matches!(r.verdict, crate::verdict::Verdict::Ok))
.count(),
)
.unwrap_or(u32::MAX);
let fail = u32::try_from(
report
.results
.iter()
.filter(|r| !matches!(r.verdict, crate::verdict::Verdict::Ok))
.count(),
)
.unwrap_or(u32::MAX);
let exit_code = if fail == 0 { 0 } else { 1 };
(pass, fail, exit_code, None)
}
Err(e) => {
let exit_code = inner_error_exit_code(&e);
(0u32, 0u32, exit_code, Some(format!("{e}")))
}
};
let (toolchain, toolchain_capture_error) = match rustup::capture_active_toolchain(&compat_root)
{
Ok(s) => (s, None),
Err(e) => (String::new(), Some(format!("{e}"))),
};
let mismatch_examples = build_mismatch_examples(&baseline_result, &converted);
let mismatch_count = u32::try_from(mismatch_examples.len()).unwrap_or(u32::MAX);
let mut envelope_errors = assemble_diagnostic_errors(
toolchain_capture_error,
&discovery_output.unrecognized,
inner_session_error,
&compat_root,
);
let generated_paths_envelope = match guard.finalize() {
Ok(entries) => entries
.iter()
.map(|e| report::generated_path_from_cleanup(e, &compat_root))
.collect::<Vec<_>>(),
Err(e) => {
envelope_errors.push(report::EnvelopeError {
error_type: "cleanup_failed".into(),
fixture: None,
file: String::new(),
line: 0,
detail: format!("{e}"),
});
Vec::new()
}
};
let baseline_pass = baseline_result.pass.unwrap_or(0);
let baseline_fail = baseline_result.fail.unwrap_or(0);
let baseline_dur_ms = baseline_result.dur_ms;
let mut envelope = report::CompatEnvelope {
schema_version: 1,
mode: "compat".into(),
crate_name,
commit: args.compat_commit.clone().unwrap_or_default(),
commands: report::Commands {
baseline: render_argv(&baseline_result.argv),
lihaaf: render_inner_command(&args, &overlay_plan.sibling_manifest, &compat_root),
},
results: report::Results {
baseline: report::BaselineCounts {
pass: baseline_pass,
fail: baseline_fail,
unknown_count: baseline_result.unknown_count,
exit_code: baseline_result.exit_code,
dur_ms: baseline_dur_ms,
},
lihaaf: report::LihaafCounts {
pass: lihaaf_pass,
fail: lihaaf_fail,
exit_code: lihaaf_exit_code,
dur_ms: lihaaf_dur_ms,
toolchain: toolchain.clone(),
},
mismatch_count,
},
mismatch_examples,
errors: envelope_errors,
excluded_fixtures: Vec::new(),
generated_paths: generated_paths_envelope,
overlay: report::OverlayMetadata {
generated: true,
dropped_comments: overlay_plan.dropped_comments,
upstream_already_has_dylib: overlay_plan.upstream_already_has_dylib,
},
toolchain,
};
report::normalize_error_detail_paths(&mut envelope, &compat_root);
report::write_envelope(&mut envelope, &compat_report)?;
let _ = started;
Ok(())
}
fn resolve_dual_root(args: &cli::CompatArgs) -> Result<overlay::DualRoot, Error> {
let workspace_root = args.compat_root.clone();
if let Some(m) = &args.compat_manifest {
let member_root = m
.parent()
.map(Path::to_path_buf)
.unwrap_or_else(|| PathBuf::from("."));
return Ok(overlay::DualRoot {
workspace_root: member_root.clone(),
workspace_root_manifest: m.clone(),
member_root,
member_manifest: m.clone(),
workspace_member_context: None,
});
}
let default_manifest = workspace_root.join("Cargo.toml");
let Some(pkg) = &args.compat_package else {
return Ok(overlay::DualRoot {
workspace_root: workspace_root.clone(),
workspace_root_manifest: default_manifest.clone(),
member_root: workspace_root,
member_manifest: default_manifest,
workspace_member_context: None,
});
};
let (member_manifest, workspace_root_value) =
overlay::resolve_workspace_member_manifest(&default_manifest, pkg)?;
let member_root = member_manifest
.parent()
.map(Path::to_path_buf)
.unwrap_or_else(|| PathBuf::from("."));
Ok(overlay::DualRoot {
workspace_root,
workspace_root_manifest: default_manifest.clone(),
member_root,
member_manifest,
workspace_member_context: Some(overlay::WorkspaceMemberContext {
workspace_root_manifest: default_manifest,
workspace_root_value,
}),
})
}
fn basename_fallback(compat_root: &Path) -> String {
compat_root
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_else(|| "unknown".to_string())
}
fn build_inner_cli(args: &cli::CompatArgs, overlay_manifest: &Path) -> crate::cli::Cli {
crate::cli::Cli {
bless: args.inner_cli.bless,
compat: false,
compat_cargo_test_argv: None,
compat_commit: None,
compat_filter: Vec::new(),
compat_manifest: None,
compat_package: None,
compat_report: None,
compat_root: None,
compat_trybuild_macro: Vec::new(),
filter: args.compat_filter.clone(),
jobs: args.inner_cli.jobs,
suite: args.inner_cli.suite.clone(),
no_cache: args.inner_cli.no_cache,
manifest_path: Some(overlay_manifest.to_path_buf()),
list: args.inner_cli.list,
quiet: args.inner_cli.quiet,
verbose: args.inner_cli.verbose,
use_symlink: args.inner_cli.use_symlink,
keep_output: args.inner_cli.keep_output,
inner_compat_normalize: true,
}
}
fn render_inner_command(
args: &cli::CompatArgs,
overlay_manifest: &Path,
compat_root: &Path,
) -> String {
let rel_manifest = overlay_manifest
.strip_prefix(compat_root)
.unwrap_or_else(|_| {
panic!(
"render_inner_command: overlay_manifest `{}` is not under compat_root `{}`; \
this is a compat-driver bug — the overlay must always be staged under \
compat_root",
overlay_manifest.display(),
compat_root.display()
)
});
let rel_manifest_str = crate::util::to_forward_slash(&rel_manifest.to_string_lossy());
let mut parts: Vec<String> = vec![
"cargo".into(),
"lihaaf".into(),
"--manifest-path".into(),
rel_manifest_str,
];
if args.inner_cli.bless {
parts.push("--bless".into());
}
if args.inner_cli.no_cache {
parts.push("--no-cache".into());
}
if args.inner_cli.list {
parts.push("--list".into());
}
if args.inner_cli.quiet {
parts.push("--quiet".into());
}
if args.inner_cli.verbose {
parts.push("--verbose".into());
}
if args.inner_cli.use_symlink {
parts.push("--use-symlink".into());
}
if args.inner_cli.keep_output {
parts.push("--keep-output".into());
}
if let Some(j) = args.inner_cli.jobs {
parts.push("--jobs".into());
parts.push(j.to_string());
}
for s in &args.inner_cli.suite {
parts.push("--suite".into());
parts.push(s.clone());
}
for f in &args.compat_filter {
parts.push("--filter".into());
parts.push(f.clone());
}
parts.join(" ")
}
fn render_argv(argv: &[String]) -> String {
argv.join(" ")
}
fn build_mismatch_examples(
baseline_result: &baseline::BaselineResult,
_converted: &[fixture_convert::ConvertedFixture],
) -> Vec<report::MismatchExample> {
baseline_result
.mismatch_entries
.iter()
.map(|m| {
let mismatch_type = match m.baseline_verdict {
baseline::BaselineVerdict::Pass => "baseline_only_pass",
baseline::BaselineVerdict::Fail => "baseline_only_fail",
};
report::MismatchExample {
fixture: m.fixture.clone(),
mismatch_type: mismatch_type.into(),
notes: String::new(),
}
})
.collect()
}
fn inner_error_exit_code(e: &Error) -> i32 {
match e {
Error::Session(outcome) => i32::from(outcome.exit_code() as u8),
_ => i32::from(crate::exit::ExitCode::ConfigInvalid as u8),
}
}
fn assemble_diagnostic_errors(
toolchain_capture_error: Option<String>,
discovery_unrecognized: &[discovery::DiscoveryUnrecognized],
inner_session_error: Option<String>,
compat_root: &Path,
) -> Vec<report::EnvelopeError> {
let mut errors: Vec<report::EnvelopeError> = Vec::new();
if let Some(detail) = toolchain_capture_error {
errors.push(report::EnvelopeError {
error_type: "toolchain_capture_failed".into(),
fixture: None,
file: String::new(),
line: 0,
detail,
});
}
for unrecog in discovery_unrecognized {
errors.push(report::EnvelopeError {
error_type: "discovery_unrecognized".into(),
fixture: None,
file: crate::util::relative_to(&unrecog.file, compat_root)
.unwrap_or_else(|err| err.non_absolute_path()),
line: u32::try_from(unrecog.line).unwrap_or(u32::MAX),
detail: unrecog.detail.clone(),
});
}
if let Some(detail) = inner_session_error {
errors.push(report::EnvelopeError {
error_type: "lihaaf_session_failed".into(),
fixture: None,
file: String::new(),
line: 0,
detail,
});
}
errors
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cli::Cli;
fn neutral_compat_args() -> cli::CompatArgs {
cli::CompatArgs {
compat_root: PathBuf::from("/tmp/lihaaf-build-inner-cli-test-root"),
compat_report: PathBuf::from("/tmp/lihaaf-build-inner-cli-test-report.json"),
compat_cargo_test_argv: vec!["cargo".to_string(), "test".to_string()],
compat_manifest: None,
compat_package: None,
compat_commit: None,
compat_filter: Vec::new(),
compat_trybuild_macro: Vec::new(),
inner_cli: Cli {
bless: false,
compat: true,
compat_cargo_test_argv: None,
compat_commit: None,
compat_filter: Vec::new(),
compat_manifest: None,
compat_package: None,
compat_report: Some(PathBuf::from(
"/tmp/lihaaf-build-inner-cli-test-report.json",
)),
compat_root: Some(PathBuf::from("/tmp/lihaaf-build-inner-cli-test-root")),
compat_trybuild_macro: Vec::new(),
filter: Vec::new(),
jobs: None,
suite: Vec::new(),
no_cache: false,
manifest_path: None,
list: false,
quiet: false,
verbose: false,
use_symlink: false,
keep_output: false,
inner_compat_normalize: false,
},
}
}
#[test]
fn build_inner_cli_sets_inner_compat_normalize_true() {
let args = neutral_compat_args();
let overlay_manifest =
PathBuf::from("/tmp/lihaaf-build-inner-cli-test/target/lihaaf-overlay/Cargo.toml");
let inner = build_inner_cli(&args, &overlay_manifest);
assert!(
inner.inner_compat_normalize,
"compat driver must set inner_compat_normalize=true so the inner session's \
NormalizationContext.compat_short_cargo is true",
);
assert!(
!inner.compat,
"inner Cli must have `compat: false` so the inner session does NOT recurse into \
the compat driver",
);
}
#[test]
fn normalization_context_reads_inner_compat_normalize() {
let ctx_compat =
crate::normalize::NormalizationContext::new(PathBuf::from("/p"), PathBuf::from("/r"))
.with_compat_short_cargo(true);
assert!(
ctx_compat.compat_short_cargo,
"with_compat_short_cargo(true) must set the flag",
);
let ctx_non_compat =
crate::normalize::NormalizationContext::new(PathBuf::from("/p"), PathBuf::from("/r"))
.with_compat_short_cargo(false);
assert!(
!ctx_non_compat.compat_short_cargo,
"with_compat_short_cargo(false) must clear the flag (mirrors the default)",
);
}
#[test]
fn assemble_diagnostic_errors_ignores_baseline_unknown_count() {
let errors = assemble_diagnostic_errors(
None, &[], None, Path::new("/tmp/compat-root"), );
assert!(
errors.is_empty(),
"with no toolchain / discovery / session errors, errors[] must be \
empty regardless of how many baseline `unknown_count` lines the \
libtest parser saw; got {errors:?}",
);
assert!(
!errors.iter().any(|e| e.error_type == "baseline_unknown"),
"no `baseline_unknown` entry may appear in errors[]; the field \
lives in results.baseline.unknown_count as a diagnostic counter"
);
}
#[test]
fn render_inner_command_manifest_path_is_repo_relative() {
let compat_root_a = PathBuf::from("/home/runner/work/my-crate");
let compat_root_b = PathBuf::from("/workspace/local/my-crate");
let overlay_a = compat_root_a
.join("target")
.join("lihaaf-overlay")
.join("Cargo.toml");
let overlay_b = compat_root_b
.join("target")
.join("lihaaf-overlay")
.join("Cargo.toml");
let args = neutral_compat_args();
let cmd_a = render_inner_command(&args, &overlay_a, &compat_root_a);
let cmd_b = render_inner_command(&args, &overlay_b, &compat_root_b);
assert_eq!(
cmd_a, cmd_b,
"commands.lihaaf must be runner-independent; \
runner A: `{cmd_a}`\nrunner B: `{cmd_b}`"
);
assert!(
!cmd_a.contains("/home/"),
"commands.lihaaf must not contain an absolute path; got `{cmd_a}`"
);
assert!(
cmd_a.contains("target/lihaaf-overlay/Cargo.toml"),
"commands.lihaaf must contain the repo-relative manifest path; got `{cmd_a}`"
);
}
#[test]
fn assemble_diagnostic_errors_orders_kinds_by_source_seam() {
let unrec = discovery::DiscoveryUnrecognized {
file: PathBuf::from("/tmp/compat-root/tests/ui.rs"),
line: 7,
detail: "non-literal arg".into(),
};
let errors = assemble_diagnostic_errors(
Some("rustup spawn failed".into()),
std::slice::from_ref(&unrec),
Some("inner panicked".into()),
Path::new("/tmp/compat-root"),
);
let types: Vec<&str> = errors.iter().map(|e| e.error_type.as_str()).collect();
assert_eq!(
types,
vec![
"toolchain_capture_failed",
"discovery_unrecognized",
"lihaaf_session_failed",
],
"in-memory order is toolchain → discovery → session; got {types:?}"
);
}
}