#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum BaseSelection {
ExplicitCommit(String),
MergeBaseWith(String),
}
pub(crate) fn select_base(
explicit_base: Option<&str>,
base_ref_flag: Option<&str>,
env_github_base_ref: Option<&str>,
default_branch: &str,
) -> BaseSelection {
if let Some(commit) = explicit_base {
return BaseSelection::ExplicitCommit(commit.to_string());
}
if let Some(r) = base_ref_flag {
return BaseSelection::MergeBaseWith(r.to_string());
}
if let Some(r) = env_github_base_ref.filter(|s| !s.is_empty()) {
return BaseSelection::MergeBaseWith(format!("origin/{r}"));
}
BaseSelection::MergeBaseWith(default_branch.to_string())
}
use anyhow::{Context, Result};
fn short_hash(repo: &gix::Repository, id: gix::ObjectId) -> String {
let head_oid = repo.head_id().ok().map(|i| i.detach());
let dirty = head_oid
.as_ref()
.and_then(|_| ktstr::test_support::repo_is_dirty(repo))
.unwrap_or(false);
let short = id.to_hex_with_len(7).to_string();
if dirty && head_oid == Some(id) {
format!("{short}-dirty")
} else {
short
}
}
fn rev_parse_commit(repo: &gix::Repository, spec: &str) -> Result<gix::ObjectId> {
let parsed = repo
.rev_parse(spec)
.with_context(|| format!("resolve revision '{spec}'"))?;
match parsed.detach() {
gix::revision::plumbing::Spec::Include(id)
| gix::revision::plumbing::Spec::ExcludeParents(id) => Ok(id),
other => anyhow::bail!("'{spec}' did not resolve to a single commit ({other:?})"),
}
}
pub(crate) fn resolve_baseline(
repo: &gix::Repository,
sel: &BaseSelection,
) -> Result<gix::ObjectId> {
match sel {
BaseSelection::ExplicitCommit(c) => rev_parse_commit(repo, c),
BaseSelection::MergeBaseWith(r) => {
let head = repo.head_id().context("resolve HEAD")?.detach();
let base = rev_parse_commit(repo, r)?;
let mb = repo
.merge_base(head, base)
.with_context(|| format!("compute merge-base(HEAD, {r})"))?;
Ok(mb.detach())
}
}
}
use ktstr::flock::{FlockMode, try_flock};
use std::path::{Path, PathBuf};
use std::process::Command;
fn checkout_dir(temp_root: &Path, baseline_short: &str) -> PathBuf {
temp_root.join(format!("ktstr-perf-delta-co-{baseline_short}"))
}
fn checkout_lock_path(wt_dir: &Path) -> PathBuf {
let mut s = wt_dir.as_os_str().to_owned();
s.push(".lock");
PathBuf::from(s)
}
fn acquire_baseline_checkout_lock(
wt_dir: &Path,
baseline_short: &str,
) -> Result<std::os::fd::OwnedFd> {
let lock_path = checkout_lock_path(wt_dir);
try_flock(&lock_path, FlockMode::Exclusive)
.with_context(|| format!("lock baseline checkout {}", lock_path.display()))?
.ok_or_else(|| {
anyhow::anyhow!(
"another `cargo ktstr perf-delta` run is using the baseline checkout \
for {baseline_short}; retry once it finishes"
)
})
}
fn baseline_child_env(
sidecar_leaf_abs: &Path,
project_commit: &str,
) -> Vec<(&'static str, String)> {
vec![
(ktstr::KTSTR_PERF_ONLY_ENV, "1".to_string()),
(
ktstr::KTSTR_SIDECAR_DIR_ENV,
sidecar_leaf_abs.to_string_lossy().into_owned(),
),
(ktstr::KTSTR_PROJECT_COMMIT_ENV, project_commit.to_string()),
]
}
fn sweep_stale_checkouts(temp_root: &Path) {
let Ok(entries) = std::fs::read_dir(temp_root) else {
return;
};
for entry in entries.flatten() {
let name = entry.file_name();
let Some(name) = name.to_str() else { continue };
if !name.starts_with("ktstr-perf-delta-co-") || name.ends_with(".lock") {
continue;
}
let wt_dir = entry.path();
if !wt_dir.is_dir() {
continue;
}
let held = match try_flock(checkout_lock_path(&wt_dir), FlockMode::Exclusive) {
Ok(Some(fd)) => fd,
_ => continue,
};
let _ = std::fs::remove_dir_all(&wt_dir);
let _ = std::fs::remove_file(checkout_lock_path(&wt_dir));
drop(held); }
}
fn finish_or_reraise(guard: crate::interrupt::InterruptGuard, run_res: Result<()>) -> Result<()> {
let caught = guard.interrupted();
drop(guard);
if let Some(sig) = caught {
crate::interrupt::reraise(sig);
}
run_res
}
fn perf_test_argv(
kernel: &str,
filter: Option<&str>,
profile: Option<&str>,
nextest_profile: Option<&str>,
passthrough: &[String],
) -> Vec<String> {
let mut v = vec![
"ktstr".to_string(),
"test".to_string(),
"--kernel".to_string(),
kernel.to_string(),
];
if let Some(p) = profile {
v.push("--profile".to_string());
v.push(p.to_string());
}
if let Some(np) = nextest_profile {
v.push("--nextest-profile".to_string());
v.push(np.to_string());
}
if let Some(f) = filter {
v.push("-E".to_string());
v.push(f.to_string());
}
v.extend(passthrough.iter().cloned());
v
}
struct CheckoutGuard {
wt_dir: PathBuf,
}
impl Drop for CheckoutGuard {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.wt_dir);
}
}
fn create_checkout_from_tree(
repo: &gix::Repository,
wt_dir: &Path,
tree_oid: gix::ObjectId,
label: &str,
) -> Result<()> {
let _ = std::fs::remove_dir_all(wt_dir);
std::fs::create_dir_all(wt_dir)
.with_context(|| format!("create {label} checkout dir {}", wt_dir.display()))?;
let mut index = repo
.index_from_tree(&tree_oid)
.with_context(|| format!("build index from {label} tree"))?;
let mut opts = repo
.checkout_options(gix::worktree::stack::state::attributes::Source::IdMapping)
.context("build checkout options")?;
opts.destination_is_initially_empty = true;
opts.keep_going = false;
let objects = repo
.objects
.clone()
.into_arc()
.context("build thread-safe object handle for checkout")?;
gix::worktree::state::checkout(
&mut index,
wt_dir,
objects,
&gix::progress::Discard,
&gix::progress::Discard,
&crate::interrupt::INTERRUPTED,
opts,
)
.with_context(|| format!("check out {label} into {}", wt_dir.display()))?;
Ok(())
}
fn build_head_snapshot_tree(
repo: &gix::Repository,
head_tree_oid: gix::ObjectId,
dirty: bool,
) -> Result<gix::ObjectId> {
if !dirty {
return Ok(head_tree_oid);
}
use gix::bstr::{BString, ByteSlice};
use gix::objs::tree::EntryKind;
use std::os::unix::fs::PermissionsExt;
let workdir = repo
.workdir()
.context("cannot snapshot a dirty HEAD without a working tree")?
.to_path_buf();
let mut editor = repo
.edit_tree(head_tree_oid)
.context("open a tree editor on HEAD for the dirty snapshot")?;
let status = repo
.status(gix::progress::Discard)
.context("configure working-tree status for the snapshot")?
.untracked_files(gix::status::UntrackedFiles::Files)
.index_worktree_rewrites(None)
.tree_index_track_renames(gix::status::tree_index::TrackRenames::Disabled);
let mut changed = 0usize;
for item in status
.into_iter(Vec::new())
.context("start the working-tree status walk for the snapshot")?
{
let item = item.context("read a working-tree status entry")?;
let rela: BString = item.location().to_owned();
let abs = workdir.join(gix::path::from_bstr(rela.as_bstr()).as_ref());
match std::fs::symlink_metadata(&abs) {
Ok(md) if md.file_type().is_symlink() => {
let target = std::fs::read_link(&abs)
.with_context(|| format!("read symlink {abs:?} for snapshot"))?;
let blob = repo
.write_blob(gix::path::into_bstr(target).as_ref())
.with_context(|| format!("write symlink blob for {rela}"))?
.detach();
editor
.upsert(rela.as_bstr(), EntryKind::Link, blob)
.with_context(|| format!("snapshot symlink {rela}"))?;
changed += 1;
}
Ok(md) if md.is_file() => {
let bytes =
std::fs::read(&abs).with_context(|| format!("read {abs:?} for snapshot"))?;
let blob = repo
.write_blob(&bytes)
.with_context(|| format!("write blob for {rela}"))?
.detach();
let kind = if md.permissions().mode() & 0o111 != 0 {
EntryKind::BlobExecutable
} else {
EntryKind::Blob
};
editor
.upsert(rela.as_bstr(), kind, blob)
.with_context(|| format!("snapshot {rela}"))?;
changed += 1;
}
Ok(md) if md.is_dir() => continue,
_ => {
editor
.remove(rela.as_bstr())
.with_context(|| format!("drop {rela} from snapshot"))?;
changed += 1;
}
}
}
let tree = editor
.write()
.context("write the dirty-HEAD snapshot tree")?
.detach();
eprintln!(
"perf-delta: HEAD is dirty; snapshotting {changed} uncommitted change(s) \
(incl. untracked non-ignored files) so the comparison is hermetic"
);
Ok(tree)
}
fn resolve_run_trees(
repo: &gix::Repository,
baseline_oid: gix::ObjectId,
) -> Result<(gix::ObjectId, gix::ObjectId)> {
let baseline_tree = repo
.find_commit(baseline_oid)
.context("find baseline commit")?
.tree_id()
.context("resolve baseline tree")?
.detach();
let head_oid = repo.head_id().context("resolve HEAD")?.detach();
let head_tree = repo
.find_commit(head_oid)
.context("find HEAD commit")?
.tree_id()
.context("resolve HEAD tree")?
.detach();
let dirty = ktstr::test_support::repo_is_dirty(repo) == Some(true);
let head_snapshot_tree = build_head_snapshot_tree(repo, head_tree, dirty)?;
Ok((baseline_tree, head_snapshot_tree))
}
fn resolve_kernel_for_children(repo_root: &Path, kernel: &str) -> String {
let p = Path::new(kernel);
if p.is_relative() && repo_root.join(p).exists() {
repo_root.join(p).to_string_lossy().into_owned()
} else {
kernel.to_string()
}
}
#[allow(clippy::too_many_arguments)]
fn noise_dual_run(
repo: &gix::Repository,
repo_root: &Path,
baseline_tree: gix::ObjectId,
head_tree: gix::ObjectId,
baseline_short: &str,
head_short: &str,
kernel: &str,
filter: Option<&str>,
profile: Option<&str>,
nextest_profile: Option<&str>,
passthrough: &[String],
runs: usize,
) -> Result<()> {
let runs_root_abs = repo_root.join(ktstr::test_support::runs_root());
let wt_dir = checkout_dir(&std::env::temp_dir(), baseline_short);
let head_wt_dir = checkout_dir(&std::env::temp_dir(), head_short);
let kernel_arg = resolve_kernel_for_children(repo_root, kernel);
let _baseline_lock = acquire_baseline_checkout_lock(&wt_dir, baseline_short)?;
let _head_lock = acquire_baseline_checkout_lock(&head_wt_dir, head_short)?;
create_checkout_from_tree(repo, &wt_dir, baseline_tree, baseline_short)?;
let _baseline_guard = CheckoutGuard {
wt_dir: wt_dir.clone(),
};
create_checkout_from_tree(repo, &head_wt_dir, head_tree, head_short)?;
let _head_guard = CheckoutGuard {
wt_dir: head_wt_dir.clone(),
};
if crate::interrupt::caught().is_some() {
return Ok(());
}
for i in 0..runs {
let base_leaf = noise_run_leaf(&runs_root_abs, "base", baseline_short, i);
let head_leaf = noise_run_leaf(&runs_root_abs, "head", head_short, i);
println!("perf-delta --noise-adjust: run {}/{runs}", i + 1);
let bs = Command::new("cargo")
.current_dir(&wt_dir)
.args(perf_test_argv(
&kernel_arg,
filter,
profile,
nextest_profile,
passthrough,
))
.envs(baseline_child_env(&base_leaf, baseline_short))
.status()
.with_context(|| format!("spawn baseline `cargo ktstr test` (noise run {i})"))?;
if !bs.success() {
eprintln!("perf-delta --noise-adjust: warning: baseline run {i} exited {bs}");
}
if crate::interrupt::caught().is_some() {
break;
}
let hs = Command::new("cargo")
.current_dir(&head_wt_dir)
.args(perf_test_argv(
&kernel_arg,
filter,
profile,
nextest_profile,
passthrough,
))
.env(ktstr::KTSTR_PERF_ONLY_ENV, "1")
.env(ktstr::KTSTR_SIDECAR_DIR_ENV, &head_leaf)
.env(ktstr::KTSTR_PROJECT_COMMIT_ENV, head_short)
.status()
.with_context(|| format!("spawn HEAD `cargo ktstr test` (noise run {i})"))?;
if !hs.success() {
eprintln!("perf-delta --noise-adjust: warning: HEAD run {i} exited {hs}");
}
if crate::interrupt::caught().is_some() {
break;
}
}
Ok(())
}
fn noise_run_leaf(runs_root_abs: &Path, side: &str, short: &str, i: usize) -> PathBuf {
runs_root_abs.join(format!("perf-delta-noise-{side}-{short}-{i}"))
}
fn count_sidecars(dir: &Path) -> usize {
std::fs::read_dir(dir)
.into_iter()
.flatten()
.flatten()
.filter(|e| {
e.file_name()
.to_str()
.is_some_and(|n| n.ends_with(".ktstr.json"))
})
.count()
}
pub(crate) struct PerfDeltaArgs<'a> {
pub base: Option<&'a str>,
pub base_ref: Option<&'a str>,
pub filter: Option<&'a str>,
pub relevant: bool,
pub default_branch: &'a str,
pub kernel: Option<&'a str>,
pub threshold: Option<f64>,
pub policy: Option<&'a std::path::Path>,
pub phase_display: cli::PhaseDisplayOptions,
pub all_metrics: bool,
pub fail_threshold: Option<usize>,
pub must_fail: Option<&'a str>,
pub noise_adjust: Option<usize>,
pub noise_spread_threshold: Option<f64>,
pub profile: Option<&'a str>,
pub nextest_profile: Option<&'a str>,
pub passthrough: &'a [String],
}
pub(crate) fn run(args: &PerfDeltaArgs<'_>) -> Result<i32> {
let policy = cli::ComparisonPolicy::from_cli_flags(args.threshold, args.policy)
.context("resolve --threshold / --policy")?;
let gate = cli::GateOptions {
fail_threshold: args.fail_threshold,
must_fail: parse_must_fail(args.must_fail, args.noise_adjust.is_some())?,
show_all: args.all_metrics,
};
let runs_tests = args.noise_adjust.is_some();
if !runs_tests
&& (!args.passthrough.is_empty()
|| args.profile.is_some()
|| args.nextest_profile.is_some())
{
anyhow::bail!(
"build-shaping flags (cargo/nextest passthrough {:?}, --profile {:?}, \
--nextest-profile {:?}) are only applied by the run-producing path \
(--noise-adjust N); this invocation compares already-pooled sidecars \
and runs no tests, so they would be ignored. Add --noise-adjust N, \
or drop the flags.",
args.passthrough,
args.profile,
args.nextest_profile,
);
}
let cwd = std::env::current_dir().context("get cwd")?;
let repo = gix::discover(&cwd).context("discover git repository")?;
let env_base = std::env::var("GITHUB_BASE_REF").ok();
let sel = select_base(
args.base,
args.base_ref,
env_base.as_deref(),
args.default_branch,
);
let baseline_oid = resolve_baseline(&repo, &sel)?;
let baseline = short_hash(&repo, baseline_oid);
let head = short_hash(&repo, repo.head_id().context("resolve HEAD")?.detach());
if baseline == head {
anyhow::bail!(
"baseline ({baseline}) resolves to HEAD — nothing to compare; \
choose a different --base / --base-ref"
);
}
println!("perf-delta: candidate HEAD {head} vs baseline {baseline}");
match &sel {
BaseSelection::ExplicitCommit(c) => println!(" baseline: explicit --base {c}"),
BaseSelection::MergeBaseWith(r) => println!(" baseline: merge-base(HEAD, {r})"),
}
let relevant_expr = if args.relevant {
crate::affected::relevant_test_filter(args.base, args.base_ref, args.default_branch)
.context("compute --relevant perf-delta test set")?
} else {
None
};
let effective_filter: Option<String> = match (relevant_expr.as_deref(), args.filter) {
(Some(rel), Some(user)) => Some(format!("({rel}) & ({user})")),
(Some(rel), None) => Some(rel.to_string()),
(None, user) => user.map(str::to_string),
};
let effective_filter = effective_filter.as_deref();
println!(
" perf tests: {}",
effective_filter.unwrap_or("all performance_mode tests")
);
if let Some(n) = args.noise_adjust {
let kernel = args
.kernel
.context("--noise-adjust requires --kernel (the kernel both perf runs boot)")?;
let (baseline_tree, head_snapshot_tree) = resolve_run_trees(&repo, baseline_oid)?;
sweep_stale_checkouts(&std::env::temp_dir());
let interrupt_guard = crate::interrupt::InterruptGuard::install();
finish_or_reraise(
interrupt_guard,
noise_dual_run(
&repo,
&cwd,
baseline_tree,
head_snapshot_tree,
&baseline,
&head,
kernel,
effective_filter,
args.profile,
args.nextest_profile,
args.passthrough,
n,
),
)?;
let runs_root_abs = cwd.join(ktstr::test_support::runs_root());
let baseline_sidecars: usize = (0..n)
.map(|i| count_sidecars(&noise_run_leaf(&runs_root_abs, "base", &baseline, i)))
.sum();
if baseline_sidecars == 0 {
println!(
"perf-delta: no performance_mode sidecars produced at baseline {baseline} \
— nothing to compare (define #[ktstr_test(performance_mode)] tests, or widen \
the -E filter, to enable the delta)"
);
return Ok(0);
}
let threshold = args.noise_spread_threshold.unwrap_or(5.0);
let build = crate::stats::BuildCompareFilters {
a_project_commit: vec![baseline.clone()],
b_project_commit: vec![head.clone()],
..Default::default()
};
let (filter_a, filter_b) = build.build();
return cli::compare_partitions_noise(
&filter_a,
&filter_b,
None,
threshold,
&args.phase_display,
&gate,
);
}
let build = crate::stats::BuildCompareFilters {
a_project_commit: vec![baseline],
b_project_commit: vec![head],
..Default::default()
};
let (filter_a, filter_b) = build.build();
cli::compare_partitions(&filter_a, &filter_b, None, &policy, None, &gate)
}
fn parse_must_fail(csv: Option<&str>, noise_adjust: bool) -> Result<Vec<String>> {
let Some(csv) = csv else {
return Ok(Vec::new());
};
let mut names = Vec::new();
for raw in csv.split(',') {
let name = raw.trim();
if name.is_empty() {
continue;
}
let Some(def) = cli::metric_def(name) else {
anyhow::bail!(
"--must-fail: unknown metric {name:?} \
(see `cargo ktstr stats list-metrics`)"
);
};
if cli::is_render_suppressed_component(name) {
anyhow::bail!(
"--must-fail: {name:?} is an internal rate COMPONENT that never \
surfaces as a regression — gate on its user-facing rate metric instead"
);
}
if !def.gates_aggregate(noise_adjust) {
let hint = if def.gates_aggregate(true) {
" — it only reports per-run; add --noise-adjust to gate on it"
} else {
" — it is a per-phase-only metric that never produces an aggregate \
regression; assert it per-phase in the test's Verdict DSL instead"
};
anyhow::bail!("--must-fail: {name:?} never reaches the aggregate comparison{hint}");
}
if !noise_adjust && def.classify_direction().is_none() {
anyhow::bail!(
"--must-fail: {name:?} is an informational metric with no regression \
direction — it can never fail the default comparison. Add \
--noise-adjust (a per-test direction override can then make it gate), \
or gate on a directional metric"
);
}
names.push(name.to_string());
}
Ok(names)
}
use ktstr::cli;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_must_fail_trims_skips_empties_and_rejects_bad_names() {
let ok = parse_must_fail(Some(" worst_spread , ,avg_dsq_depth,"), false)
.expect("known directional metrics parse");
assert_eq!(
ok,
vec!["worst_spread".to_string(), "avg_dsq_depth".to_string()]
);
assert!(parse_must_fail(None, false).unwrap().is_empty());
assert!(parse_must_fail(None, true).unwrap().is_empty());
assert!(parse_must_fail(Some("definitely_not_a_metric"), false).is_err());
assert!(
parse_must_fail(Some("total_taobench_ops"), false).is_err(),
"a render-suppressed rate component must be rejected on the scalar path"
);
assert!(
parse_must_fail(Some("total_taobench_ops"), true).is_err(),
"a render-suppressed rate component must be rejected under --noise-adjust too"
);
assert!(
parse_must_fail(Some("total_ttwu_count"), false).is_err(),
"an informational metric must be rejected on the scalar path"
);
assert_eq!(
parse_must_fail(Some("total_ttwu_count"), true)
.expect("informational metric accepted under --noise-adjust"),
vec!["total_ttwu_count".to_string()],
);
assert!(
parse_must_fail(Some("wakeup_p99_latency_us"), false).is_err(),
"a per-phase-only metric must be rejected on the scalar path"
);
assert!(
parse_must_fail(Some("wakeup_p99_latency_us"), true).is_err(),
"a per-phase-only metric must be rejected under --noise-adjust too"
);
assert!(
parse_must_fail(Some("wakeup_p99_latency_us_whole"), false).is_err(),
"a whole-run distribution metric cannot gate the scalar compare"
);
assert_eq!(
parse_must_fail(Some("wakeup_p99_latency_us_whole"), true)
.expect("whole-run distribution metric gates under --noise-adjust"),
vec!["wakeup_p99_latency_us_whole".to_string()],
);
}
#[test]
fn explicit_base_wins_and_skips_merge_base() {
assert_eq!(
select_base(Some("abc123"), Some("main"), Some("release"), "main"),
BaseSelection::ExplicitCommit("abc123".to_string()),
"--base must take precedence over every ref source and skip merge-base",
);
}
#[test]
fn base_ref_flag_beats_env_and_default() {
assert_eq!(
select_base(None, Some("topic"), Some("release"), "main"),
BaseSelection::MergeBaseWith("topic".to_string()),
);
}
#[test]
fn github_base_ref_resolves_to_origin_tracking_ref() {
assert_eq!(
select_base(None, None, Some("release-1.2"), "main"),
BaseSelection::MergeBaseWith("origin/release-1.2".to_string()),
"a PR target branch is the fetched remote-tracking ref",
);
}
#[test]
fn empty_github_base_ref_is_unset_falls_back_to_default() {
assert_eq!(
select_base(None, None, Some(""), "main"),
BaseSelection::MergeBaseWith("main".to_string()),
"empty GITHUB_BASE_REF (non-PR run) must not select an empty ref",
);
}
#[test]
fn no_inputs_falls_back_to_default_branch() {
assert_eq!(
select_base(None, None, None, "main"),
BaseSelection::MergeBaseWith("main".to_string()),
);
}
#[test]
fn checkout_dir_under_temp_root() {
assert_eq!(
checkout_dir(Path::new("/tmp"), "abc1234"),
Path::new("/tmp/ktstr-perf-delta-co-abc1234"),
);
}
#[test]
fn noise_run_leaf_names_per_side_per_run() {
let root = Path::new("/work/target/ktstr");
assert_eq!(
noise_run_leaf(root, "base", "abc1234", 0),
Path::new("/work/target/ktstr/perf-delta-noise-base-abc1234-0"),
);
assert_eq!(
noise_run_leaf(root, "head", "def5678", 2),
Path::new("/work/target/ktstr/perf-delta-noise-head-def5678-2"),
);
}
#[test]
fn count_sidecars_counts_ktstr_json_and_zero_for_missing() {
let base = std::env::temp_dir().join(format!("ktstr-pd-count-{}", std::process::id()));
assert_eq!(count_sidecars(&base.join("absent")), 0);
std::fs::create_dir_all(&base).expect("mk tempdir");
std::fs::write(base.join("a.ktstr.json"), "{}").expect("write a");
std::fs::write(base.join("b.ktstr.json"), "{}").expect("write b");
std::fs::write(base.join("notes.txt"), "x").expect("write txt");
assert_eq!(
count_sidecars(&base),
2,
"only *.ktstr.json sidecars count, not sibling files",
);
std::fs::remove_dir_all(&base).ok();
}
#[test]
fn baseline_child_env_sets_perf_only_sidecar_dir_and_project_commit() {
let leaf = Path::new("/work/target/ktstr/perf-delta-baseline-abc1234");
let env = baseline_child_env(leaf, "abc1234");
assert_eq!(
env,
vec![
("KTSTR_PERF_ONLY", "1".to_string()),
(
"KTSTR_SIDECAR_DIR",
"/work/target/ktstr/perf-delta-baseline-abc1234".to_string(),
),
("KTSTR_PROJECT_COMMIT", "abc1234".to_string()),
],
);
assert_eq!(env[0].0, ktstr::KTSTR_PERF_ONLY_ENV);
assert_eq!(env[1].0, ktstr::KTSTR_SIDECAR_DIR_ENV);
assert_eq!(env[2].0, ktstr::KTSTR_PROJECT_COMMIT_ENV);
}
#[test]
fn sweep_reclaims_unlocked_but_keeps_locked_checkouts() {
let base = std::env::temp_dir().join(format!("ktstr-pd-sweep-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&base);
std::fs::create_dir_all(&base).expect("mk temp root");
let unlocked = base.join("ktstr-perf-delta-co-aaaaaaa");
let locked = base.join("ktstr-perf-delta-co-bbbbbbb");
std::fs::create_dir(&unlocked).expect("mk unlocked checkout");
std::fs::create_dir(&locked).expect("mk locked checkout");
let held = try_flock(checkout_lock_path(&locked), FlockMode::Exclusive)
.expect("flock call")
.expect("lock free initially");
sweep_stale_checkouts(&base);
assert!(
!unlocked.exists(),
"an unlocked orphan checkout is reclaimed"
);
assert!(
!checkout_lock_path(&unlocked).exists(),
"the reclaimed orphan's sibling .lock is removed too (no lock debris)"
);
assert!(locked.exists(), "a locked (live) checkout is never removed");
assert!(
checkout_lock_path(&locked).exists(),
"a live checkout's held .lock is left intact"
);
drop(held);
let _ = std::fs::remove_dir_all(&base);
}
#[test]
fn finish_or_reraise_passes_through_when_not_interrupted() {
let g = crate::interrupt::InterruptGuard::install();
assert!(g.interrupted().is_none(), "no signal delivered in-test");
assert!(finish_or_reraise(g, Ok(())).is_ok(), "Ok passes through");
let g2 = crate::interrupt::InterruptGuard::install();
assert!(
finish_or_reraise(g2, Err(anyhow::anyhow!("boom"))).is_err(),
"Err propagates unchanged"
);
}
#[test]
fn checkout_guard_drop_removes_the_checkout_dir() {
let base = std::env::temp_dir().join(format!("ktstr-pd-guard-{}", std::process::id()));
let wt = base.join("wt");
std::fs::create_dir_all(wt.join("scx-ktstr")).expect("mk checkout subtree");
std::fs::write(wt.join("scx-ktstr/build.rs"), "x").expect("write checkout file");
assert!(wt.exists(), "checkout present before drop");
{
let _guard = CheckoutGuard { wt_dir: wt.clone() };
}
assert!(!wt.exists(), "CheckoutGuard::drop removes the checkout dir");
{
let _guard = CheckoutGuard { wt_dir: wt.clone() };
}
assert!(!wt.exists(), "drop on an absent checkout stays a no-op");
std::fs::remove_dir_all(&base).ok();
}
#[test]
fn create_checkout_from_tree_materializes_the_tree() {
let base = std::env::temp_dir().join(format!("ktstr-pd-gixco-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&base);
let repo_dir = base.join("repo");
std::fs::create_dir_all(&repo_dir).expect("mk repo dir");
let repo = gix::init(&repo_dir).expect("gix::init");
let blob = repo.write_blob(b"baseline\n").expect("write blob").detach();
let tree = gix::objs::Tree {
entries: vec![gix::objs::tree::Entry {
mode: gix::objs::tree::EntryKind::Blob.into(),
filename: "file.txt".into(),
oid: blob,
}],
};
let tree_id: gix::ObjectId = repo.write_object(&tree).expect("write tree").detach();
let wt = base.join("checkout");
create_checkout_from_tree(&repo, &wt, tree_id, "baseline").expect("gix checkout");
assert_eq!(
std::fs::read_to_string(wt.join("file.txt")).expect("checked-out file present"),
"baseline\n",
"the tree's file must be materialized into the plain checkout",
);
assert!(
!wt.join(".git").exists(),
"the checkout is a PLAIN dir — no .git linkage, so cleanup is a plain remove_dir_all",
);
std::fs::write(wt.join("file.txt"), b"stale\n").expect("dirty the leftover");
std::fs::write(wt.join("untracked.tmp"), b"junk\n").expect("leftover untracked");
create_checkout_from_tree(&repo, &wt, tree_id, "baseline").expect("re-checkout self-heals");
assert_eq!(
std::fs::read_to_string(wt.join("file.txt")).expect("file present after re-checkout"),
"baseline\n",
"re-checkout must overwrite a stale leftover with the tree content",
);
assert!(
!wt.join("untracked.tmp").exists(),
"pre-checkout self-heal (remove_dir_all) clears a leftover's untracked debris",
);
std::fs::remove_dir_all(&base).ok();
}
fn init_committed_repo(
dir: &std::path::Path,
files: &[(&str, &[u8])],
) -> (gix::Repository, gix::ObjectId) {
std::fs::create_dir_all(dir).expect("mk repo dir");
let mut repo = gix::init(dir).expect("gix::init");
let _ = repo
.committer_or_set_generic_fallback()
.expect("committer fallback");
{
use gix::config::tree::gitoxide;
let mut cfg = gix::config::File::new(gix::config::file::Metadata::api());
cfg.set_raw_value(gitoxide::Author::NAME_FALLBACK, "ktstr-test")
.expect("author name fallback");
cfg.set_raw_value(
gitoxide::Author::EMAIL_FALLBACK,
"ktstr-test@example.invalid",
)
.expect("author email fallback");
repo.config_snapshot_mut().append(cfg);
}
let mut entries: Vec<gix::objs::tree::Entry> = files
.iter()
.map(|(name, content)| {
let oid = repo.write_blob(content).expect("write blob").detach();
gix::objs::tree::Entry {
mode: gix::objs::tree::EntryKind::Blob.into(),
filename: (*name).into(),
oid,
}
})
.collect();
entries.sort_by(|a, b| a.filename.cmp(&b.filename));
let tree = gix::objs::Tree { entries };
let tree_id: gix::ObjectId = repo.write_object(&tree).expect("write tree").detach();
repo.commit("HEAD", "init", tree_id, std::iter::empty::<gix::ObjectId>())
.expect("commit");
let mut idx = repo.index_from_tree(&tree_id).expect("index_from_tree");
idx.write(gix::index::write::Options::default())
.expect("write index");
for (name, content) in files {
std::fs::write(dir.join(name), content).expect("write worktree file");
}
(repo, tree_id)
}
#[test]
fn dirty_head_snapshot_captures_worktree_bytes_not_head() {
let base = std::env::temp_dir().join(format!("ktstr-pd-snap-dirty-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&base);
let repo_dir = base.join("repo");
let (repo, head_tree) = init_committed_repo(&repo_dir, &[("file.txt", b"v1\n")]);
std::fs::write(repo_dir.join("file.txt"), b"v2\n").expect("dirty the worktree");
let snap = build_head_snapshot_tree(&repo, head_tree, true).expect("build snapshot");
assert_ne!(
snap, head_tree,
"a dirty snapshot must differ from HEAD's tree"
);
let wt = base.join("checkout");
create_checkout_from_tree(&repo, &wt, snap, "head").expect("checkout snapshot");
assert_eq!(
std::fs::read_to_string(wt.join("file.txt")).expect("snapshot file"),
"v2\n",
"the snapshot holds the WORKTREE bytes (v2), not HEAD's (v1)",
);
std::fs::remove_dir_all(&base).ok();
}
#[test]
fn dirty_head_snapshot_worktree_wins_over_staged() {
let base = std::env::temp_dir().join(format!("ktstr-pd-snap-stg-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&base);
let repo_dir = base.join("repo");
let (repo, head_tree) = init_committed_repo(&repo_dir, &[("file.txt", b"v1\n")]);
let blob_v2 = repo.write_blob(b"v2\n").expect("write v2 blob").detach();
let tree_v2 = gix::objs::Tree {
entries: vec![gix::objs::tree::Entry {
mode: gix::objs::tree::EntryKind::Blob.into(),
filename: "file.txt".into(),
oid: blob_v2,
}],
};
let tree_v2_id: gix::ObjectId =
repo.write_object(&tree_v2).expect("write v2 tree").detach();
let mut idx = repo
.index_from_tree(&tree_v2_id)
.expect("index_from_tree v2");
idx.write(gix::index::write::Options::default())
.expect("persist v2 index");
std::fs::write(repo_dir.join("file.txt"), b"v1\n").expect("revert worktree to v1");
let snap = build_head_snapshot_tree(&repo, head_tree, true).expect("build snapshot");
assert_eq!(
snap, head_tree,
"worktree wins: a staged-then-reverted file snapshots as its worktree \
content (v1 == HEAD), ignoring the staged v2",
);
std::fs::remove_dir_all(&base).ok();
}
#[test]
fn clean_head_snapshot_is_head_tree_unchanged() {
let base = std::env::temp_dir().join(format!("ktstr-pd-snap-clean-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&base);
let repo_dir = base.join("repo");
let (repo, head_tree) = init_committed_repo(&repo_dir, &[("file.txt", b"v1\n")]);
let snap = build_head_snapshot_tree(&repo, head_tree, false).expect("clean snapshot");
assert_eq!(
snap, head_tree,
"a clean HEAD snapshot is HEAD's tree unchanged (no editor, no writes)",
);
std::fs::remove_dir_all(&base).ok();
}
#[test]
fn dirty_head_snapshot_includes_untracked_but_excludes_ignored() {
let base = std::env::temp_dir().join(format!("ktstr-pd-snap-untr-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&base);
let repo_dir = base.join("repo");
let (repo, head_tree) = init_committed_repo(
&repo_dir,
&[("file.txt", b"v1\n"), (".gitignore", b"ignore.me\n")],
);
std::fs::write(repo_dir.join("new.rs"), b"fn wip() {}\n").expect("untracked WIP");
std::fs::write(repo_dir.join("ignore.me"), b"scratch\n").expect("ignored scratch");
let snap = build_head_snapshot_tree(&repo, head_tree, true).expect("build snapshot");
let wt = base.join("checkout");
create_checkout_from_tree(&repo, &wt, snap, "head").expect("checkout snapshot");
assert!(
wt.join("new.rs").exists(),
"an untracked non-ignored WIP file is included in the snapshot",
);
assert!(
!wt.join("ignore.me").exists(),
"an ignored file is excluded from the snapshot",
);
std::fs::remove_dir_all(&base).ok();
}
#[test]
fn dirty_head_snapshot_drops_a_deleted_file() {
let base = std::env::temp_dir().join(format!("ktstr-pd-snap-del-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&base);
let repo_dir = base.join("repo");
let (repo, head_tree) = init_committed_repo(
&repo_dir,
&[("keep.txt", b"keep\n"), ("gone.txt", b"gone\n")],
);
std::fs::remove_file(repo_dir.join("gone.txt")).expect("delete gone.txt");
let snap = build_head_snapshot_tree(&repo, head_tree, true).expect("build snapshot");
let wt = base.join("checkout");
create_checkout_from_tree(&repo, &wt, snap, "head").expect("checkout snapshot");
assert!(wt.join("keep.txt").exists(), "the kept file survives");
assert!(
!wt.join("gone.txt").exists(),
"a worktree-deleted file is dropped from the snapshot",
);
std::fs::remove_dir_all(&base).ok();
}
#[test]
fn dirty_head_snapshot_preserves_the_worktree_exec_bit() {
use std::os::unix::fs::PermissionsExt;
let base = std::env::temp_dir().join(format!("ktstr-pd-snap-exec-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&base);
let repo_dir = base.join("repo");
let (repo, head_tree) = init_committed_repo(&repo_dir, &[("run.sh", b"#!/bin/sh\n")]);
std::fs::write(repo_dir.join("run.sh"), b"#!/bin/sh\necho hi\n").expect("edit");
std::fs::set_permissions(
repo_dir.join("run.sh"),
std::fs::Permissions::from_mode(0o755),
)
.expect("chmod +x");
let snap = build_head_snapshot_tree(&repo, head_tree, true).expect("build snapshot");
let wt = base.join("checkout");
create_checkout_from_tree(&repo, &wt, snap, "head").expect("checkout snapshot");
assert!(
std::fs::metadata(wt.join("run.sh"))
.expect("run.sh present")
.permissions()
.mode()
& 0o111
!= 0,
"the snapshot preserves the worktree's exec bit (BlobExecutable)",
);
std::fs::remove_dir_all(&base).ok();
}
#[test]
fn baseline_checkout_lock_blocks_a_concurrent_same_baseline_run() {
let wt = std::env::temp_dir().join(format!("ktstr-pd-locktest-wt-{}", std::process::id()));
assert_eq!(
checkout_lock_path(&wt),
PathBuf::from(format!("{}.lock", wt.display())),
"lock is a sibling <wt_dir>.lock, outside the checkout cleanup removes",
);
let lock_path = checkout_lock_path(&wt);
let _ = std::fs::remove_file(&lock_path);
let held =
acquire_baseline_checkout_lock(&wt, "abc1234").expect("first run acquires the lock");
assert!(
acquire_baseline_checkout_lock(&wt, "abc1234").is_err(),
"a concurrent same-baseline run must fail to acquire, not clobber the live checkout",
);
drop(held);
let after = acquire_baseline_checkout_lock(&wt, "abc1234")
.expect("lock frees for the next run once released");
drop(after);
let _ = std::fs::remove_file(&lock_path);
}
#[test]
fn perf_test_argv_appends_filter_and_passthrough() {
let expect = |v: &[&str]| v.iter().map(|s| s.to_string()).collect::<Vec<String>>();
let feat = ["--features".to_string(), "integration,wprof".to_string()];
assert_eq!(
perf_test_argv("6.14", None, None, None, &[]),
expect(&["ktstr", "test", "--kernel", "6.14"]),
);
assert_eq!(
perf_test_argv("6.14", Some("test(perf_smoke)"), None, None, &[]),
expect(&[
"ktstr",
"test",
"--kernel",
"6.14",
"-E",
"test(perf_smoke)"
]),
);
assert_eq!(
perf_test_argv("6.14", Some("test(perf_smoke)"), None, None, &feat),
expect(&[
"ktstr",
"test",
"--kernel",
"6.14",
"-E",
"test(perf_smoke)",
"--features",
"integration,wprof",
]),
);
assert_eq!(
perf_test_argv("6.14", None, None, None, &feat),
expect(&[
"ktstr",
"test",
"--kernel",
"6.14",
"--features",
"integration,wprof",
]),
);
}
#[test]
fn perf_test_argv_emits_profiles_before_passthrough() {
let expect = |v: &[&str]| v.iter().map(|s| s.to_string()).collect::<Vec<String>>();
let feat = ["--features".to_string(), "integration".to_string()];
assert_eq!(
perf_test_argv("6.14", Some("test(perf)"), Some("dev"), Some("ci"), &feat),
expect(&[
"ktstr",
"test",
"--kernel",
"6.14",
"--profile",
"dev",
"--nextest-profile",
"ci",
"-E",
"test(perf)",
"--features",
"integration",
]),
);
assert_eq!(
perf_test_argv("6.14", None, Some("dev"), None, &[]),
expect(&["ktstr", "test", "--kernel", "6.14", "--profile", "dev"]),
);
assert_eq!(
perf_test_argv("6.14", None, None, Some("ci"), &[]),
expect(&[
"ktstr",
"test",
"--kernel",
"6.14",
"--nextest-profile",
"ci",
]),
);
}
#[test]
fn resolve_kernel_absolutizes_relative_existing_path() {
let base = std::env::temp_dir().join(format!("ktstr-pd-kresolve-{}", std::process::id()));
std::fs::create_dir_all(base.join("linux")).expect("mk linux dir");
assert_eq!(
resolve_kernel_for_children(&base, "linux"),
base.join("linux").to_string_lossy(),
);
std::fs::remove_dir_all(&base).ok();
}
#[test]
fn resolve_kernel_passes_version_and_absolute_and_missing_through() {
let repo = Path::new("/no/such/repo-xyz");
assert_eq!(resolve_kernel_for_children(repo, "6.14"), "6.14");
assert_eq!(
resolve_kernel_for_children(repo, "/abs/linux"),
"/abs/linux",
);
assert_eq!(
resolve_kernel_for_children(repo, "../nonexistent-xyz"),
"../nonexistent-xyz",
);
}
#[test]
fn gix_resolution_against_a_temp_repo() {
use std::process::Command;
let dir = std::env::temp_dir().join(format!("ktstr-pd-gitfix-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).expect("mk tempdir");
let run = |args: &[&str]| {
let ok = Command::new("git")
.current_dir(&dir)
.args([
"-c",
"user.email=t@example.invalid",
"-c",
"user.name=t",
"-c",
"commit.gpgsign=false",
])
.args(args)
.status()
.map(|s| s.success())
.unwrap_or(false);
assert!(ok, "git {args:?} failed");
};
run(&["init", "-q"]);
std::fs::write(dir.join("f"), "1").expect("write f");
run(&["add", "."]);
run(&["commit", "-q", "-m", "first"]);
std::fs::write(dir.join("f"), "2").expect("write f");
run(&["add", "."]);
run(&["commit", "-q", "-m", "second"]);
let repo = gix::discover(&dir).expect("discover temp repo");
let head = rev_parse_commit(&repo, "HEAD").expect("rev-parse HEAD");
let first = rev_parse_commit(&repo, "HEAD~1").expect("rev-parse HEAD~1");
assert_ne!(head, first, "HEAD and HEAD~1 are distinct commits");
assert_eq!(
resolve_baseline(&repo, &BaseSelection::ExplicitCommit("HEAD~1".to_string())).unwrap(),
first,
);
assert_eq!(
resolve_baseline(&repo, &BaseSelection::MergeBaseWith("HEAD~1".to_string())).unwrap(),
first,
"merge-base(HEAD, HEAD~1) is HEAD~1",
);
let sh = short_hash(&repo, first);
assert_eq!(sh.len(), 7, "short hash is 7 hex chars: {sh}");
assert!(sh.bytes().all(|b| b.is_ascii_hexdigit()), "hex only: {sh}");
std::fs::remove_dir_all(&dir).ok();
}
}