use std::collections::{BTreeMap, BTreeSet, VecDeque};
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use anyhow::{Context, Result, anyhow};
mod dep_info;
mod diff;
struct SchedulerInfo {
name: String,
package: Option<String>,
test_count: usize,
}
#[derive(Debug, PartialEq)]
enum AffectedOutcome {
RunAll,
Empty,
Subset(Vec<String>),
}
pub(crate) fn run(base: Option<&str>, base_ref: Option<&str>, default_branch: &str) -> Result<()> {
let schedulers = enumerate_schedulers().context("enumerate declared schedulers")?;
let (outcome, testable) = compute_outcome(base, base_ref, default_branch, false, &schedulers)?;
let names = match outcome {
AffectedOutcome::RunAll => testable,
AffectedOutcome::Empty => Vec::new(),
AffectedOutcome::Subset(pkgs) => pkgs,
};
println!(
"{}",
serde_json::to_string(&names).context("serialize affected JSON")?
);
Ok(())
}
fn package_schedulers(schedulers: &[SchedulerInfo]) -> Vec<String> {
let mut v: Vec<String> = schedulers
.iter()
.filter(|s| s.test_count > 0)
.filter_map(|s| s.package.clone())
.collect();
v.sort();
v.dedup();
v
}
fn enumerate_schedulers() -> Result<Vec<SchedulerInfo>> {
let per_binary: Vec<Vec<ktstr::test_support::SchedulerListEntry>> = crate::misc::probe_collect(
None,
false,
|bin| {
let mut c = Command::new(bin);
c.arg("--ktstr-list-schedulers");
c
},
|_bin, out| {
serde_json::from_slice::<Vec<ktstr::test_support::SchedulerListEntry>>(&out.stdout)
.map_err(|e| format!("parse --ktstr-list-schedulers output: {e}"))
},
)
.map_err(|e| anyhow!("probe test binaries for declared schedulers: {e:?}"))?;
let mut by_name: BTreeMap<String, SchedulerInfo> = BTreeMap::new();
for entry in per_binary.into_iter().flatten() {
let name = entry.scheduler.name.clone();
let package = match entry.scheduler.binary_kind {
ktstr::test_support::BinaryKindJson::Discover(pkg) => Some(pkg),
_ => None,
};
by_name
.entry(name.clone())
.and_modify(|si| si.test_count += entry.test_count)
.or_insert(SchedulerInfo {
name,
package,
test_count: entry.test_count,
});
}
Ok(by_name.into_values().collect())
}
fn enumerate_scheduler_tests() -> Result<Vec<ktstr::test_support::SchedulerTestJson>> {
let per_binary: Vec<Vec<ktstr::test_support::SchedulerTestJson>> = crate::misc::probe_collect(
None,
false,
|bin| {
let mut c = Command::new(bin);
c.arg("--ktstr-list-scheduler-tests");
c
},
|_bin, out| {
serde_json::from_slice::<Vec<ktstr::test_support::SchedulerTestJson>>(&out.stdout)
.map_err(|e| format!("parse --ktstr-list-scheduler-tests output: {e}"))
},
)
.map_err(|e| anyhow!("probe test binaries for declared tests: {e:?}"))?;
let mut by_name: BTreeMap<String, ktstr::test_support::SchedulerTestJson> = BTreeMap::new();
for entry in per_binary.into_iter().flatten() {
by_name.entry(entry.test.clone()).or_insert(entry);
}
Ok(by_name.into_values().collect())
}
fn compute_outcome(
base: Option<&str>,
base_ref: Option<&str>,
default_branch: &str,
include_worktree: bool,
schedulers: &[SchedulerInfo],
) -> Result<(AffectedOutcome, Vec<String>)> {
let cargo = std::env::var_os("CARGO")
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from("cargo"));
let mut meta_cmd = cargo_metadata::MetadataCommand::new();
meta_cmd.cargo_path(&cargo);
let meta = match meta_cmd.exec() {
Ok(m) => m,
Err(_) => return Ok((AffectedOutcome::RunAll, package_schedulers(schedulers))),
};
let ws_root = PathBuf::from(meta.workspace_root.as_str());
let repo_root = std::fs::canonicalize(&ws_root).unwrap_or(ws_root);
let ws = WorkspaceGraph::build(&meta, &repo_root);
let mut testable_pkgs: Vec<String> = Vec::new();
for s in schedulers {
if s.test_count == 0 {
continue;
}
let Some(pkg) = s.package.as_deref() else {
continue; };
if ws.is_member(pkg) {
testable_pkgs.push(pkg.to_string());
} else {
eprintln!(
"ktstr affected: declared scheduler package `{pkg}` (with tests) \
is not a workspace member; excluded from the matrix"
);
}
}
testable_pkgs.sort();
testable_pkgs.dedup();
let cwd = std::env::current_dir().context("resolve current dir")?;
let repo = match gix::discover(&cwd) {
Ok(r) => r,
Err(_) => return Ok((AffectedOutcome::RunAll, testable_pkgs)),
};
let workdir_matches_root = repo
.workdir()
.and_then(|w| std::fs::canonicalize(w).ok())
.is_some_and(|w| w == repo_root);
if !workdir_matches_root {
return Ok((AffectedOutcome::RunAll, testable_pkgs));
}
let sel = crate::perf_delta::select_base(
base,
base_ref,
std::env::var("GITHUB_BASE_REF").ok().as_deref(),
default_branch,
);
let base_oid = match crate::perf_delta::resolve_baseline(&repo, &sel) {
Ok(o) => o,
Err(_) => return Ok((AffectedOutcome::RunAll, testable_pkgs)),
};
let head_oid = match repo.head_id() {
Ok(h) => h.detach(),
Err(_) => return Ok((AffectedOutcome::RunAll, testable_pkgs)),
};
let mut changed = match diff::changed_paths_committed(&repo, base_oid, head_oid) {
Ok(c) => c,
Err(_) => return Ok((AffectedOutcome::RunAll, testable_pkgs)),
};
if include_worktree {
match diff::changed_paths_worktree(&repo) {
Ok(w) => changed.extend(w),
Err(_) => return Ok((AffectedOutcome::RunAll, testable_pkgs)),
}
}
if changed.is_empty() {
return Ok((AffectedOutcome::Empty, testable_pkgs));
}
if changed.iter().all(|p| is_docs_only(p)) {
return Ok((AffectedOutcome::Empty, testable_pkgs));
}
if changed.iter().any(|p| is_infra_path(p)) {
return Ok((AffectedOutcome::RunAll, testable_pkgs));
}
let need_dot_d = changed
.iter()
.any(|p| is_native_source(p) || ws.owning_crate(p).is_none());
let mut input_sets: BTreeMap<String, Option<BTreeSet<String>>> = BTreeMap::new();
if need_dot_d {
let id_to_name: BTreeMap<String, String> = meta
.packages
.iter()
.map(|p| (p.id.repr.clone(), p.name.to_string()))
.collect();
let artifacts = build_schedulers(&cargo, &testable_pkgs, &id_to_name);
for pkg in &testable_pkgs {
let set = artifacts
.get(pkg)
.and_then(|a| scheduler_input_set_from_artifact(pkg, a, &repo_root, &ws).ok());
input_sets.insert(pkg.clone(), set);
}
}
let outcome = attribute(&changed, &testable_pkgs, &input_sets, &ws);
Ok((outcome, testable_pkgs))
}
pub(crate) fn relevant_test_filter(
base: Option<&str>,
base_ref: Option<&str>,
default_branch: &str,
) -> Result<Option<String>> {
let schedulers = enumerate_schedulers().context("enumerate declared schedulers")?;
let (outcome, _testable) = compute_outcome(base, base_ref, default_branch, true, &schedulers)?;
if !matches!(outcome, AffectedOutcome::Subset(_)) {
return Ok(select_relevant_tests(&outcome, &BTreeMap::new(), &[]));
}
let pkg_of: BTreeMap<&str, Option<&str>> = schedulers
.iter()
.map(|s| (s.name.as_str(), s.package.as_deref()))
.collect();
let tests = enumerate_scheduler_tests().context("enumerate declared tests")?;
Ok(select_relevant_tests(&outcome, &pkg_of, &tests))
}
fn select_relevant_tests(
outcome: &AffectedOutcome,
pkg_of: &BTreeMap<&str, Option<&str>>,
tests: &[ktstr::test_support::SchedulerTestJson],
) -> Option<String> {
let affected: BTreeSet<&str> = match outcome {
AffectedOutcome::RunAll => return None,
AffectedOutcome::Empty => return Some("none()".to_string()),
AffectedOutcome::Subset(pkgs) => pkgs.iter().map(String::as_str).collect(),
};
let selected: BTreeSet<&str> = tests
.iter()
.filter(|t| match pkg_of.get(t.scheduler.as_str()).copied() {
Some(Some(pkg)) => affected.contains(pkg),
_ => true,
})
.map(|t| t.test.as_str())
.collect();
if selected.is_empty() {
None
} else {
Some(crate::replay::build_nextest_filter(&selected))
}
}
fn attribute(
changed: &BTreeSet<String>,
testable_pkgs: &[String],
input_sets: &BTreeMap<String, Option<BTreeSet<String>>>,
ws: &WorkspaceGraph,
) -> AffectedOutcome {
let mut affected: BTreeSet<String> = BTreeSet::new();
let mut unattributed = false;
for path in changed {
if is_docs_only(path) {
continue;
}
let mut attributed = false;
if let Some(owner) = ws.owning_crate(path) {
attributed = true;
for pkg in testable_pkgs {
if ws.closure_contains(pkg, owner) {
affected.insert(pkg.clone());
}
}
}
for (pkg, set) in input_sets {
if set.as_ref().is_some_and(|s| s.contains(path)) {
attributed = true;
affected.insert(pkg.clone());
}
}
if !attributed {
unattributed = true;
}
}
for (pkg, set) in input_sets {
if set.is_none() {
affected.insert(pkg.clone());
}
}
if unattributed {
return AffectedOutcome::RunAll;
}
let affected: Vec<String> = affected.into_iter().collect();
if affected.is_empty() {
AffectedOutcome::Empty
} else {
AffectedOutcome::Subset(affected)
}
}
fn scheduler_input_set_from_artifact(
pkg: &str,
artifact: &Path,
repo_root: &Path,
ws: &WorkspaceGraph,
) -> Result<BTreeSet<String>> {
let (manifest_rel, build_rs_rel) = ws.scheduler_manifest_inputs(pkg, repo_root)?;
let dot_d = artifact.with_extension("d");
let contents = std::fs::read_to_string(&dot_d)
.with_context(|| format!("read dep-info {}", dot_d.display()))?;
let mut set: BTreeSet<String> = dep_info::parse_dep_info(&contents)
.iter()
.filter_map(|p| dep_info::normalize_to_repo_relative(p, repo_root))
.collect();
set.insert(manifest_rel);
if let Some(b) = build_rs_rel {
set.insert(b);
}
Ok(set)
}
fn build_schedulers(
cargo: &Path,
pkgs: &[String],
id_to_name: &BTreeMap<String, String>,
) -> BTreeMap<String, PathBuf> {
if pkgs.is_empty() {
return BTreeMap::new();
}
let profile = ktstr::scheduler_profile_name();
let mut cmd = Command::new(cargo);
cmd.args([
"build",
"--message-format=json",
"--profile",
profile.as_str(),
]);
for pkg in pkgs {
cmd.args(["-p", pkg]);
}
let output = match cmd.stdout(Stdio::piped()).stderr(Stdio::inherit()).output() {
Ok(o) => o,
Err(_) => return BTreeMap::new(),
};
map_artifacts_to_bins(std::io::Cursor::new(output.stdout), id_to_name)
}
fn map_artifacts_to_bins<R: std::io::BufRead>(
stream: R,
id_to_name: &BTreeMap<String, String>,
) -> BTreeMap<String, PathBuf> {
let mut map: BTreeMap<String, PathBuf> = BTreeMap::new();
for msg in cargo_metadata::Message::parse_stream(stream).flatten() {
let cargo_metadata::Message::CompilerArtifact(art) = msg else {
continue;
};
if art.target.is_bin()
&& !art.profile.test
&& let Some(name) = id_to_name.get(&art.package_id.repr)
&& let Some(exe) = art.executable
{
let path = PathBuf::from(exe.as_str());
if let Some(prev) = map.insert(name.clone(), path) {
eprintln!(
"ktstr affected: scheduler package `{name}` emitted multiple \
[[bin]] artifacts; using the last, ignoring {} (its `.d` \
inputs are not considered)",
prev.display()
);
}
}
}
map
}
struct WorkspaceGraph {
crate_dirs: BTreeMap<String, String>,
closures: BTreeMap<String, BTreeSet<String>>,
manifest_paths: BTreeMap<String, PathBuf>,
member_names: BTreeSet<String>,
}
impl WorkspaceGraph {
fn build(meta: &cargo_metadata::Metadata, repo_root: &Path) -> Self {
let member_ids: BTreeSet<&cargo_metadata::PackageId> =
meta.workspace_members.iter().collect();
let mut member_names = BTreeSet::new();
let mut crate_dirs = BTreeMap::new();
let mut manifest_paths = BTreeMap::new();
let mut id_to_name: BTreeMap<&cargo_metadata::PackageId, String> = BTreeMap::new();
for p in &meta.packages {
let name = p.name.to_string();
if member_ids.contains(&p.id) {
member_names.insert(name.clone());
let manifest = PathBuf::from(p.manifest_path.as_str());
if let Some(dir) = manifest.parent() {
let canon = std::fs::canonicalize(dir).unwrap_or_else(|_| dir.to_path_buf());
if let Ok(rel) = canon.strip_prefix(repo_root) {
let rel = rel.to_string_lossy().into_owned();
if !rel.is_empty() {
crate_dirs.insert(name.clone(), format!("{rel}/"));
}
}
}
manifest_paths.insert(name.clone(), manifest);
}
id_to_name.insert(&p.id, name);
}
let mut edges: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
if let Some(resolve) = &meta.resolve {
for node in &resolve.nodes {
let Some(from) = id_to_name
.get(&node.id)
.filter(|n| member_names.contains(*n))
else {
continue;
};
for dep in &node.deps {
let Some(to) = id_to_name
.get(&dep.pkg)
.filter(|n| member_names.contains(*n))
else {
continue;
};
let linked = dep.dep_kinds.is_empty()
|| dep.dep_kinds.iter().any(|dk| {
matches!(
dk.kind,
cargo_metadata::DependencyKind::Normal
| cargo_metadata::DependencyKind::Build
)
});
if linked {
edges
.entry((*from).clone())
.or_default()
.insert((*to).clone());
}
}
}
}
let closures = member_names
.iter()
.map(|name| (name.clone(), transitive_closure(name, &edges)))
.collect();
Self {
crate_dirs,
closures,
manifest_paths,
member_names,
}
}
fn is_member(&self, pkg: &str) -> bool {
self.member_names.contains(pkg)
}
fn owning_crate(&self, path: &str) -> Option<&str> {
self.crate_dirs
.iter()
.filter(|(_, dir)| path.starts_with(dir.as_str()))
.max_by_key(|(_, dir)| dir.len())
.map(|(name, _)| name.as_str())
}
fn closure_contains(&self, scheduler: &str, crate_name: &str) -> bool {
self.closures
.get(scheduler)
.is_some_and(|c| c.contains(crate_name))
}
fn scheduler_manifest_inputs(
&self,
pkg: &str,
repo_root: &Path,
) -> Result<(String, Option<String>)> {
let manifest = self
.manifest_paths
.get(pkg)
.ok_or_else(|| anyhow!("{pkg} not in workspace metadata"))?;
let dir = manifest
.parent()
.ok_or_else(|| anyhow!("{pkg} manifest has no parent dir"))?;
let build_rs_path = dir.join("build.rs");
let manifest_rel = dep_info::normalize_to_repo_relative(manifest, repo_root)
.ok_or_else(|| anyhow!("{pkg} manifest is outside the repo"))?;
let build_rs_rel = if build_rs_path.exists() {
dep_info::normalize_to_repo_relative(&build_rs_path, repo_root)
} else {
None
};
Ok((manifest_rel, build_rs_rel))
}
}
fn transitive_closure(start: &str, edges: &BTreeMap<String, BTreeSet<String>>) -> BTreeSet<String> {
let mut seen = BTreeSet::new();
let mut queue = VecDeque::new();
queue.push_back(start.to_string());
while let Some(n) = queue.pop_front() {
if !seen.insert(n.clone()) {
continue;
}
if let Some(next) = edges.get(&n) {
for m in next {
if !seen.contains(m) {
queue.push_back(m.clone());
}
}
}
}
seen
}
fn is_docs_only(path: &str) -> bool {
path.ends_with(".md") || matches!(path.split('/').next(), Some("doc" | "docs"))
}
fn is_infra_path(path: &str) -> bool {
matches!(
path,
"Cargo.toml" | "Cargo.lock" | "rust-toolchain.toml" | "rust-toolchain" | "ktstr.kconfig"
) || path.starts_with(".cargo/")
}
fn is_native_source(path: &str) -> bool {
path.ends_with(".c") || path.ends_with(".h")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn docs_only_classification() {
assert!(is_docs_only("README.md"));
assert!(is_docs_only("docs/guide/x.md"));
assert!(is_docs_only("doc/notes.txt"));
assert!(!is_docs_only("src/docs_render.rs"));
assert!(!is_docs_only("scheds/rust/scx_x/src/main.rs"));
}
#[test]
fn infra_classification() {
assert!(is_infra_path("Cargo.lock"));
assert!(is_infra_path("Cargo.toml"));
assert!(is_infra_path("rust-toolchain.toml"));
assert!(is_infra_path("ktstr.kconfig"));
assert!(is_infra_path(".cargo/config.toml"));
assert!(!is_infra_path("scheds/rust/scx_x/Cargo.toml"));
}
#[test]
fn native_source_classification() {
assert!(is_native_source("scheds/rust/scx_x/src/bpf/main.bpf.c"));
assert!(is_native_source("scheds/include/scx/common.bpf.h"));
assert!(!is_native_source("scheds/rust/scx_x/src/main.rs"));
}
fn graph() -> WorkspaceGraph {
let mut crate_dirs = BTreeMap::new();
crate_dirs.insert("scx_a".to_string(), "scheds/rust/scx_a/".to_string());
crate_dirs.insert("scx_b".to_string(), "scheds/rust/scx_b/".to_string());
crate_dirs.insert("scx_common".to_string(), "rust/scx_common/".to_string());
let mut closures = BTreeMap::new();
closures.insert(
"scx_a".to_string(),
["scx_a", "scx_common"]
.iter()
.map(|s| s.to_string())
.collect(),
);
closures.insert(
"scx_b".to_string(),
["scx_b"].iter().map(|s| s.to_string()).collect(),
);
closures.insert(
"scx_common".to_string(),
["scx_common"].iter().map(|s| s.to_string()).collect(),
);
WorkspaceGraph {
crate_dirs,
closures,
manifest_paths: BTreeMap::new(),
member_names: ["scx_a", "scx_b", "scx_common"]
.iter()
.map(|s| s.to_string())
.collect(),
}
}
#[test]
fn owning_crate_longest_prefix() {
let g = graph();
assert_eq!(
g.owning_crate("scheds/rust/scx_a/src/main.rs"),
Some("scx_a")
);
assert_eq!(
g.owning_crate("rust/scx_common/src/lib.rs"),
Some("scx_common")
);
assert_eq!(g.owning_crate("scheds/include/scx/common.bpf.h"), None);
}
#[test]
fn closure_reflects_dependency_edges() {
let g = graph();
assert!(g.closure_contains("scx_a", "scx_common"));
assert!(!g.closure_contains("scx_b", "scx_common"));
assert!(g.closure_contains("scx_a", "scx_a"));
}
#[test]
fn transitive_closure_follows_chain() {
let mut edges: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
edges.insert("a".into(), ["b"].iter().map(|s| s.to_string()).collect());
edges.insert("b".into(), ["c"].iter().map(|s| s.to_string()).collect());
let c = transitive_closure("a", &edges);
assert_eq!(
c,
["a", "b", "c"]
.iter()
.map(|s| s.to_string())
.collect::<BTreeSet<_>>()
);
}
fn changed(paths: &[&str]) -> BTreeSet<String> {
paths.iter().map(|s| s.to_string()).collect()
}
#[test]
fn attribute_docs_only_is_empty() {
let g = graph();
let pkgs = vec!["scx_a".to_string()];
let out = attribute(
&changed(&["README.md", "docs/x.md"]),
&pkgs,
&BTreeMap::new(),
&g,
);
assert_eq!(out, AffectedOutcome::Empty);
}
#[test]
fn attribute_layer2_closure_hit() {
let g = graph();
let pkgs = vec!["scx_a".to_string(), "scx_b".to_string()];
let out = attribute(
&changed(&["rust/scx_common/src/lib.rs"]),
&pkgs,
&BTreeMap::new(),
&g,
);
assert_eq!(out, AffectedOutcome::Subset(vec!["scx_a".to_string()]));
}
#[test]
fn attribute_layer1_shared_header_via_dot_d() {
let g = graph();
let pkgs = vec!["scx_a".to_string(), "scx_b".to_string()];
let mut input_sets = BTreeMap::new();
input_sets.insert(
"scx_a".to_string(),
Some(changed(&["scheds/include/scx/common.bpf.h"])),
);
input_sets.insert("scx_b".to_string(), Some(BTreeSet::new()));
let out = attribute(
&changed(&["scheds/include/scx/common.bpf.h"]),
&pkgs,
&input_sets,
&g,
);
assert_eq!(out, AffectedOutcome::Subset(vec!["scx_a".to_string()]));
}
#[test]
fn attribute_unattributed_non_docs_is_runall() {
let g = graph();
let pkgs = vec!["scx_a".to_string()];
let mut input_sets = BTreeMap::new();
input_sets.insert("scx_a".to_string(), Some(BTreeSet::new()));
let out = attribute(
&changed(&["tools/random_script.sh"]),
&pkgs,
&input_sets,
&g,
);
assert_eq!(out, AffectedOutcome::RunAll);
}
#[test]
fn attribute_uncomputable_dot_d_is_conservatively_affected() {
let g = graph();
let pkgs = vec!["scx_a".to_string(), "scx_b".to_string()];
let mut input_sets = BTreeMap::new();
input_sets.insert("scx_a".to_string(), None); input_sets.insert("scx_b".to_string(), Some(BTreeSet::new()));
let out = attribute(
&changed(&["scheds/rust/scx_b/src/bpf/main.bpf.c"]),
&pkgs,
&input_sets,
&g,
);
assert_eq!(
out,
AffectedOutcome::Subset(vec!["scx_a".to_string(), "scx_b".to_string()])
);
}
#[test]
fn changed_paths_committed_captures_rename_source_and_dest() {
use std::process::Command;
let dir = std::env::temp_dir().join(format!("ktstr-aff-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"]);
run(&["config", "diff.renames", "true"]);
std::fs::write(dir.join("a.rs"), "fn a() {}\n").expect("write a.rs");
run(&["add", "."]);
run(&["commit", "-q", "-m", "first"]);
run(&["mv", "a.rs", "b.rs"]);
run(&["commit", "-q", "-am", "rename a.rs -> b.rs"]);
let repo = gix::discover(&dir).expect("discover temp repo");
let oid = |spec: &str| -> gix::ObjectId {
match repo.rev_parse(spec).expect("rev-parse").detach() {
gix::revision::plumbing::Spec::Include(id)
| gix::revision::plumbing::Spec::ExcludeParents(id) => id,
other => panic!("{spec} did not resolve to a commit: {other:?}"),
}
};
let changed = diff::changed_paths_committed(&repo, oid("HEAD~1"), oid("HEAD"))
.expect("changed paths");
assert!(
changed.contains("a.rs"),
"rename source captured: {changed:?}"
);
assert!(
changed.contains("b.rs"),
"rename dest captured: {changed:?}"
);
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn workspace_graph_from_metadata() {
let dir = std::env::temp_dir().join(format!("ktstr-aff-wsfix-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(dir.join("dep/src")).unwrap();
std::fs::create_dir_all(dir.join("user/src")).unwrap();
std::fs::write(
dir.join("Cargo.toml"),
"[workspace]\nmembers = [\"dep\", \"user\"]\nresolver = \"2\"\n",
)
.unwrap();
std::fs::write(
dir.join("dep/Cargo.toml"),
"[package]\nname = \"dep\"\nversion = \"0.1.0\"\nedition = \"2021\"\n",
)
.unwrap();
std::fs::write(dir.join("dep/src/lib.rs"), "").unwrap();
std::fs::write(
dir.join("user/Cargo.toml"),
"[package]\nname = \"user\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\
[dependencies]\ndep = { path = \"../dep\" }\n",
)
.unwrap();
std::fs::write(dir.join("user/src/lib.rs"), "").unwrap();
let repo_root = std::fs::canonicalize(&dir).unwrap();
let meta = cargo_metadata::MetadataCommand::new()
.manifest_path(dir.join("Cargo.toml"))
.exec()
.expect("cargo metadata");
let ws = WorkspaceGraph::build(&meta, &repo_root);
assert!(ws.is_member("dep") && ws.is_member("user"));
assert_eq!(ws.owning_crate("dep/src/lib.rs"), Some("dep"));
assert_eq!(ws.owning_crate("user/src/lib.rs"), Some("user"));
assert_eq!(ws.owning_crate("README.md"), None);
assert!(ws.closure_contains("user", "dep"));
assert!(!ws.closure_contains("dep", "user"));
std::fs::remove_dir_all(&dir).ok();
}
fn sched_test(test: &str, scheduler: &str) -> ktstr::test_support::SchedulerTestJson {
ktstr::test_support::SchedulerTestJson {
test: test.to_string(),
scheduler: scheduler.to_string(),
}
}
#[test]
fn select_relevant_runall_is_none() {
assert_eq!(
select_relevant_tests(&AffectedOutcome::RunAll, &BTreeMap::new(), &[]),
None
);
}
#[test]
fn select_relevant_empty_is_none_filterset() {
assert_eq!(
select_relevant_tests(&AffectedOutcome::Empty, &BTreeMap::new(), &[]),
Some("none()".to_string())
);
}
#[test]
fn select_relevant_subset_maps_tests_to_affected_packages() {
let mut pkg_of: BTreeMap<&str, Option<&str>> = BTreeMap::new();
pkg_of.insert("scx_a_sched", Some("scx_a"));
pkg_of.insert("scx_b_sched", Some("scx_b"));
pkg_of.insert("builtin", None); let tests = vec![
sched_test("t_a", "scx_a_sched"), sched_test("t_b", "scx_b_sched"), sched_test("t_builtin", "builtin"), sched_test("t_unknown", "ghost"), ];
let outcome = AffectedOutcome::Subset(vec!["scx_a".to_string()]);
let expr = select_relevant_tests(&outcome, &pkg_of, &tests).expect("Subset selects tests");
assert!(
expr.contains("t_a$"),
"affected Discover pkg test kept: {expr}"
);
assert!(
expr.contains("t_builtin$"),
"package-less test kept: {expr}"
);
assert!(
expr.contains("t_unknown$"),
"unenumerable-scheduler test kept: {expr}"
);
assert!(
!expr.contains("t_b$"),
"unaffected Discover pkg test dropped: {expr}"
);
}
#[test]
fn select_relevant_subset_empty_selection_is_none_failsafe() {
let mut pkg_of: BTreeMap<&str, Option<&str>> = BTreeMap::new();
pkg_of.insert("scx_b_sched", Some("scx_b"));
let tests = vec![sched_test("t_b", "scx_b_sched")];
let outcome = AffectedOutcome::Subset(vec!["scx_a".to_string()]);
assert_eq!(select_relevant_tests(&outcome, &pkg_of, &tests), None);
}
#[test]
fn map_artifacts_to_bins_filters_and_attributes() {
let mut id_to_name: BTreeMap<String, String> = BTreeMap::new();
id_to_name.insert(
"scx_a 1.0.0 (path+file:///w/scx_a)".to_string(),
"scx_a".to_string(),
);
id_to_name.insert(
"scx_b 1.0.0 (path+file:///w/scx_b)".to_string(),
"scx_b".to_string(),
);
let stream = concat!(
r#"{"reason":"compiler-artifact","package_id":"scx_a 1.0.0 (path+file:///w/scx_a)","target":{"name":"scx_a","kind":["bin"],"src_path":"/w/scx_a/src/main.rs"},"profile":{"opt_level":"3","debug_assertions":false,"overflow_checks":false,"test":false},"features":[],"filenames":["/w/target/release/scx_a"],"executable":"/w/target/release/scx_a","fresh":false}"#,
"\n",
r#"{"reason":"compiler-artifact","package_id":"scx_a 1.0.0 (path+file:///w/scx_a)","target":{"name":"scx_a","kind":["lib"],"src_path":"/w/scx_a/src/lib.rs"},"profile":{"opt_level":"3","debug_assertions":false,"overflow_checks":false,"test":false},"features":[],"filenames":["/w/target/release/libscx_a.rlib"],"executable":null,"fresh":false}"#,
"\n",
r#"{"reason":"compiler-artifact","package_id":"scx_a 1.0.0 (path+file:///w/scx_a)","target":{"name":"scx_a","kind":["bin"],"src_path":"/w/scx_a/src/main.rs"},"profile":{"opt_level":"3","debug_assertions":false,"overflow_checks":false,"test":true},"features":[],"filenames":["/w/target/release/deps/scx_a-abc123"],"executable":"/w/target/release/deps/scx_a-abc123","fresh":false}"#,
"\n",
r#"{"reason":"compiler-artifact","package_id":"scx_b 1.0.0 (path+file:///w/scx_b)","target":{"name":"scx_b","kind":["bin"],"src_path":"/w/scx_b/src/main.rs"},"profile":{"opt_level":"3","debug_assertions":false,"overflow_checks":false,"test":false},"features":[],"filenames":[],"executable":null,"fresh":false}"#,
"\n",
r#"{"reason":"compiler-artifact","package_id":"ghost 1.0.0 (path+file:///w/ghost)","target":{"name":"ghost","kind":["bin"],"src_path":"/w/ghost/src/main.rs"},"profile":{"opt_level":"3","debug_assertions":false,"overflow_checks":false,"test":false},"features":[],"filenames":["/w/target/release/ghost"],"executable":"/w/target/release/ghost","fresh":false}"#,
"\n",
r#"{"reason":"build-finished","success":true}"#,
"\n",
);
let map = map_artifacts_to_bins(std::io::Cursor::new(stream), &id_to_name);
assert_eq!(map.len(), 1, "only the known non-test bin maps: {map:?}");
assert_eq!(
map.get("scx_a").map(|p| p.to_string_lossy().into_owned()),
Some("/w/target/release/scx_a".to_string()),
"the real (non-test) bin is mapped, not the test-profile bin",
);
assert!(
!map.contains_key("scx_b"),
"a bin with a null executable is not mapped: {map:?}",
);
}
}