use std::path::{Path, PathBuf};
use std::process::Command;
fn bin() -> &'static str {
env!("CARGO_BIN_EXE_kshana")
}
fn sha256_hex(bytes: &[u8]) -> String {
use sha2::{Digest, Sha256};
let mut h = Sha256::new();
h.update(bytes);
hex::encode(h.finalize())
}
fn temp_workdir(label: &str) -> PathBuf {
static SEQ: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
let seq = SEQ.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let dir = std::env::temp_dir().join(format!("kshana-{label}-{}-{seq}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
dir
}
fn run_in(dir: &Path, args: &[String]) {
let o = Command::new(bin())
.args(args)
.current_dir(dir)
.output()
.expect("run kshana binary");
assert!(
o.status.success(),
"kshana {args:?} failed in {}: {}",
dir.display(),
String::from_utf8_lossy(&o.stderr)
);
}
fn reproduce(scenario: &str, extra: &[&str], extra_files: &[(&str, &str)]) -> serde_json::Value {
let root = Path::new(env!("CARGO_MANIFEST_DIR"));
let file = Path::new(scenario)
.file_name()
.unwrap()
.to_str()
.unwrap()
.to_string();
let stem = file.trim_end_matches(".toml").to_string();
let bytes = std::fs::read(root.join(scenario)).unwrap();
let a = temp_workdir("report-a");
std::fs::write(a.join(&file), &bytes).unwrap();
for (src, name) in extra_files {
std::fs::copy(root.join(src), a.join(name)).unwrap();
}
let mut args = vec![file.clone()];
args.extend(extra.iter().map(|s| s.to_string()));
run_in(&a, &args);
let result_a = std::fs::read(a.join(format!("{stem}.result.json"))).unwrap();
let report_a = std::fs::read(a.join(format!("{stem}.report.json"))).unwrap();
let html_a = std::fs::read(a.join(format!("{stem}.report.html"))).unwrap();
let rep: serde_json::Value = serde_json::from_slice(&report_a).unwrap();
let rec = &rep["reproducibility"];
assert_eq!(
rec["scenario_sha256"],
sha256_hex(&bytes),
"{scenario}: scenario digest"
);
assert_eq!(
rec["result_sha256"],
sha256_hex(&result_a),
"{scenario}: result digest"
);
assert_eq!(rec["scenario_file"], file.as_str());
let argv: Vec<String> = rec["argv"]
.as_array()
.expect("argv")
.iter()
.map(|v| v.as_str().unwrap().to_string())
.collect();
assert_eq!(argv[0], "kshana");
let b = temp_workdir("report-b");
std::fs::write(b.join(&file), &bytes).unwrap();
assert_eq!(
sha256_hex(&std::fs::read(b.join(&file)).unwrap()),
rec["scenario_sha256"].as_str().unwrap(),
"the copied scenario must match the recorded digest before it is run"
);
for f in rec["input_files"].as_array().unwrap() {
let name = f["path"].as_str().unwrap();
std::fs::copy(a.join(name), b.join(name)).unwrap();
assert_eq!(
sha256_hex(&std::fs::read(b.join(name)).unwrap()),
f["sha256"].as_str().unwrap()
);
}
run_in(&b, &argv[1..]);
let result_b = std::fs::read(b.join(format!("{stem}.result.json"))).unwrap();
assert!(
result_a == result_b,
"{scenario}: the recorded command did not reproduce result.json byte for byte"
);
assert_eq!(
sha256_hex(&result_b),
rec["result_sha256"].as_str().unwrap()
);
let report_b = std::fs::read(b.join(format!("{stem}.report.json"))).unwrap();
assert!(
report_a == report_b,
"{scenario}: report.json differs on reproduction"
);
let html_b = std::fs::read(b.join(format!("{stem}.report.html"))).unwrap();
assert!(
html_a == html_b,
"{scenario}: report.html differs on reproduction"
);
std::fs::remove_dir_all(&a).ok();
std::fs::remove_dir_all(&b).ok();
rep
}
#[test]
fn the_recorded_command_reproduces_a_clock_run() {
let rep = reproduce("scenarios/clock-holdover.toml", &[], &[]);
assert_eq!(rep["reproducibility"]["seed"], 42);
assert_eq!(
rep["reproducibility"]["command"],
"kshana clock-holdover.toml"
);
assert!(rep["reproducibility"].get("generated_utc").is_none());
}
#[test]
fn the_recorded_command_reproduces_a_chained_campaign() {
let rep = reproduce(
"scenarios/campaign-jam-spoof-holdover-integrity.toml",
&[],
&[],
);
assert_eq!(rep["aggregation"]["mode"], "chain");
}
#[test]
fn the_recorded_command_reproduces_a_sweep_campaign() {
let rep = reproduce("scenarios/campaign-sweep-jammer-power.toml", &[], &[]);
assert_eq!(rep["aggregation"]["mode"], "sweep");
assert_eq!(
rep["aggregation"]["tables"][0]["rows"]
.as_array()
.unwrap()
.len(),
13
);
}
#[test]
fn the_recorded_command_carries_an_earth_orientation_file_and_its_digest() {
let rep = reproduce(
"scenarios/orbit-sgp4-gps.toml",
&["--eop", "finals.txt"],
&[(
"tests/fixtures/agency/eop/finals2000A_2022001.txt",
"finals.txt",
)],
);
let rec = &rep["reproducibility"];
assert_eq!(
rec["command"],
"kshana orbit-sgp4-gps.toml --eop finals.txt"
);
let files = rec["input_files"].as_array().unwrap();
assert_eq!(files.len(), 1);
let fixture = std::fs::read(
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures/agency/eop/finals2000A_2022001.txt"),
)
.unwrap();
assert_eq!(files[0]["sha256"], sha256_hex(&fixture));
}
#[test]
fn a_study_name_run_records_its_flag_and_says_its_stamp_is_the_one_difference() {
let dir = temp_workdir("report-study");
std::fs::write(
dir.join("s.toml"),
include_str!("../scenarios/clock-holdover.toml"),
)
.unwrap();
run_in(
&dir,
&[
"s.toml".to_string(),
"--study-name".to_string(),
"My Study".to_string(),
],
);
let rep: serde_json::Value =
serde_json::from_slice(&std::fs::read(dir.join("my-study.report.json")).unwrap()).unwrap();
let rec = &rep["reproducibility"];
assert_eq!(rec["command"], "kshana s.toml --study-name 'My Study'");
assert!(rec["generated_utc"]
.as_str()
.is_some_and(|s| s.ends_with('Z')));
assert!(rec["determinism"]
.as_str()
.unwrap()
.contains("generated_utc"));
let html = std::fs::read_to_string(dir.join("my-study.report.html")).unwrap();
assert!(html.contains("<title>My Study \u{2014} Kshana</title>"));
std::fs::remove_dir_all(&dir).ok();
}