use std::fmt::Write as _;
use std::path::Path;
use super::fixture::{self, Fixture, LinkError, Linker, Log, Status, TargetEnv};
pub fn thread_counts() -> Vec<usize> {
let n = std::thread::available_parallelism()
.map_or(4, |n| n.get())
.max(4);
vec![1, 2, n]
}
const BACKINGS: [&str; 3] = ["write", "mmap", "memory"];
struct Variant {
label: String,
extra: Vec<String>,
vars: Vec<(String, String)>,
compare_with: Option<&'static str>,
}
impl Variant {
fn new(label: String, extra: &[&str]) -> Self {
Self {
label,
extra: extra.iter().map(|s| (*s).to_string()).collect(),
vars: Vec::new(),
compare_with: None,
}
}
fn var(mut self, backing: &str) -> Self {
self.vars
.push(("QLD_OUTPUT_BACKING".to_string(), backing.to_string()));
self
}
fn compare_with(mut self, label: &'static str) -> Self {
self.compare_with = Some(label);
self
}
}
pub fn compare_outputs(first: &Path, second: &Path, outputs: &[String]) -> Result<(), String> {
let mut problems = String::new();
for output in outputs {
let read = |dir: &Path| {
std::fs::read(dir.join(output))
.map_err(|e| format!("{}: {e}", dir.join(output).display()))
};
let (a, b) = (read(first)?, read(second)?);
if a != b {
let offset = a
.iter()
.zip(&b)
.position(|(x, y)| x != y)
.unwrap_or(a.len().min(b.len()));
let _ = writeln!(
problems,
"{output}: {} is {} bytes, {} is {} bytes, first difference at offset {offset:#x}",
first.display(),
a.len(),
second.display(),
b.len()
);
}
}
if problems.is_empty() {
Ok(())
} else {
Err(problems)
}
}
pub fn check(
fixture: &Fixture,
env: &TargetEnv,
linker: &Linker,
pristine: &Path,
reference: &Path,
scratch: &Path,
log: &mut Log,
) -> Result<String, Status> {
let outputs = fixture.link_outputs();
let mut variants: Vec<Variant> = if linker.supports_threads() {
thread_counts()
.into_iter()
.map(|n| Variant::new(format!("threads-{n}"), &[&format!("--threads={n}")]))
.collect()
} else {
vec![Variant::new("relink".to_string(), &[])]
};
if *linker == Linker::Qld {
for backing in BACKINGS {
variants.push(Variant::new(format!("backing-{backing}"), &[]).var(backing));
}
for backing in BACKINGS {
variants.push(
Variant::new(format!("build-id-{backing}"), &["--build-id=sha1"])
.var(backing)
.compare_with("build-id-write"),
);
}
}
let mut problems = String::new();
for variant in &variants {
let Variant {
label,
extra,
vars,
compare_with,
} = variant;
let dir = scratch.join(label);
fixture::fresh_dir(&dir)?;
fixture::copy_dir(pristine, &dir)
.map_err(|e| Status::Fail(format!("cannot copy {}: {e}", pristine.display())))?;
let reference = match compare_with {
Some(other) => scratch.join(other),
None => reference.to_path_buf(),
};
let reference = reference.as_path();
match fixture::link_with_vars(fixture, env, linker, &dir, extra, vars, log) {
Ok(()) => {}
Err(LinkError::Unimplemented(message)) => return Err(Status::skip(message)),
Err(LinkError::Failed(message)) => {
return Err(Status::Fail(format!(
"determinism relink ({label}) failed: {message}"
)));
}
Err(LinkError::Status(status)) => return Err(status),
}
if reference == dir {
continue;
}
if let Err(message) = compare_outputs(reference, &dir, &outputs) {
let _ = writeln!(problems, "output differs with {label}:\n{message}");
}
}
if problems.is_empty() {
let labels: Vec<&str> = variants.iter().map(|v| v.label.as_str()).collect();
Ok(format!("deterministic across {}", labels.join(", ")))
} else {
Err(Status::Fail(format!(
"non-deterministic output:\n{problems}"
)))
}
}