use fluxbench_core::BenchmarkDef;
use std::collections::{HashMap, HashSet, VecDeque};
pub struct ExecutionPlan {
pub benchmarks: Vec<&'static BenchmarkDef>,
}
pub fn build_plan(
benchmarks: impl IntoIterator<Item = &'static BenchmarkDef>,
filter: Option<®ex::Regex>,
group: Option<&str>,
tag: Option<&str>,
skip_tag: Option<&str>,
) -> ExecutionPlan {
let mut selected: Vec<_> = benchmarks
.into_iter()
.filter(|b| {
if let Some(re) = filter {
if !re.is_match(b.id) {
return false;
}
}
if let Some(g) = group {
if b.group != g {
return false;
}
}
if let Some(t) = tag {
if !b.tags.contains(&t) {
return false;
}
}
if let Some(st) = skip_tag {
if b.tags.contains(&st) {
return false;
}
}
true
})
.collect();
selected.sort_by_key(|b| b.id);
let has_deps = selected.iter().any(|b| !b.depends_on.is_empty());
if has_deps {
selected = topological_sort(selected);
}
ExecutionPlan {
benchmarks: selected,
}
}
fn topological_sort(benchmarks: Vec<&'static BenchmarkDef>) -> Vec<&'static BenchmarkDef> {
let id_set: HashSet<&str> = benchmarks.iter().map(|b| b.id).collect();
let index_map: HashMap<&str, usize> = benchmarks
.iter()
.enumerate()
.map(|(i, b)| (b.id, i))
.collect();
let n = benchmarks.len();
let mut in_degree = vec![0usize; n];
let mut dependents: Vec<Vec<usize>> = vec![Vec::new(); n];
for (i, b) in benchmarks.iter().enumerate() {
for &dep_id in b.depends_on {
if !id_set.contains(dep_id) {
eprintln!(
"Warning: benchmark '{}' depends on '{}' which is not in the filtered set; skipping dependency",
b.id, dep_id
);
continue;
}
let dep_idx = index_map[dep_id];
in_degree[i] += 1;
dependents[dep_idx].push(i);
}
}
let mut queue: VecDeque<usize> = (0..n).filter(|&i| in_degree[i] == 0).collect();
let mut result = Vec::with_capacity(n);
while let Some(idx) = queue.pop_front() {
result.push(benchmarks[idx]);
let mut newly_ready = Vec::new();
for &dep_idx in &dependents[idx] {
in_degree[dep_idx] -= 1;
if in_degree[dep_idx] == 0 {
newly_ready.push(dep_idx);
}
}
newly_ready.sort_by_key(|&i| benchmarks[i].id);
queue.extend(newly_ready);
}
if result.len() != n {
let in_cycle: Vec<&str> = (0..n)
.filter(|&i| in_degree[i] > 0)
.map(|i| benchmarks[i].id)
.collect();
eprintln!(
"Error: dependency cycle detected among benchmarks: {}. Running in alphabetical order.",
in_cycle.join(", ")
);
return benchmarks;
}
result
}
#[cfg(test)]
mod tests {
use super::*;
use fluxbench_core::Severity;
fn make_bench(
id: &'static str,
group: &'static str,
tags: &'static [&'static str],
) -> BenchmarkDef {
make_bench_with_deps(id, group, tags, &[])
}
fn make_bench_with_deps(
id: &'static str,
group: &'static str,
tags: &'static [&'static str],
depends_on: &'static [&'static str],
) -> BenchmarkDef {
BenchmarkDef {
id,
name: id,
group,
severity: Severity::Info,
threshold: 0.0,
budget_ns: None,
tags,
runner_fn: |_| {},
file: "",
line: 0,
module_path: "",
warmup_ns: None,
measurement_ns: None,
samples: None,
min_iterations: None,
max_iterations: None,
depends_on,
}
}
#[test]
fn test_no_filter() {
let benches = [
make_bench("c_bench", "default", &[]),
make_bench("a_bench", "default", &[]),
make_bench("b_bench", "default", &[]),
];
let refs: Vec<_> = benches
.iter()
.map(|b| unsafe { &*(b as *const _) })
.collect();
let plan = build_plan(refs, None, None, None, None);
assert_eq!(plan.benchmarks.len(), 3);
assert_eq!(plan.benchmarks[0].id, "a_bench");
assert_eq!(plan.benchmarks[1].id, "b_bench");
assert_eq!(plan.benchmarks[2].id, "c_bench");
}
#[test]
fn test_group_filter() {
let benches = [
make_bench("bench1", "group_a", &[]),
make_bench("bench2", "group_b", &[]),
make_bench("bench3", "group_a", &[]),
];
let refs: Vec<_> = benches
.iter()
.map(|b| unsafe { &*(b as *const _) })
.collect();
let plan = build_plan(refs, None, Some("group_a"), None, None);
assert_eq!(plan.benchmarks.len(), 2);
assert!(plan.benchmarks.iter().all(|b| b.group == "group_a"));
}
#[test]
fn test_tag_filter() {
let benches = [
make_bench("bench1", "default", &["fast"]),
make_bench("bench2", "default", &["slow"]),
make_bench("bench3", "default", &["fast", "important"]),
];
let refs: Vec<_> = benches
.iter()
.map(|b| unsafe { &*(b as *const _) })
.collect();
let plan = build_plan(refs, None, None, Some("fast"), None);
assert_eq!(plan.benchmarks.len(), 2);
assert!(plan.benchmarks.iter().all(|b| b.tags.contains(&"fast")));
}
#[test]
fn test_dependency_ordering() {
let benches = [
make_bench_with_deps("c_bench", "default", &[], &["a_bench"]),
make_bench("a_bench", "default", &[]),
make_bench_with_deps("b_bench", "default", &[], &["a_bench"]),
];
let refs: Vec<_> = benches
.iter()
.map(|b| unsafe { &*(b as *const _) })
.collect();
let plan = build_plan(refs, None, None, None, None);
assert_eq!(plan.benchmarks.len(), 3);
assert_eq!(plan.benchmarks[0].id, "a_bench");
let remaining: Vec<&str> = plan.benchmarks[1..].iter().map(|b| b.id).collect();
assert!(remaining.contains(&"b_bench"));
assert!(remaining.contains(&"c_bench"));
}
#[test]
fn test_dependency_chain() {
let benches = [
make_bench_with_deps("c_bench", "default", &[], &["b_bench"]),
make_bench("a_bench", "default", &[]),
make_bench_with_deps("b_bench", "default", &[], &["a_bench"]),
];
let refs: Vec<_> = benches
.iter()
.map(|b| unsafe { &*(b as *const _) })
.collect();
let plan = build_plan(refs, None, None, None, None);
assert_eq!(plan.benchmarks.len(), 3);
assert_eq!(plan.benchmarks[0].id, "a_bench");
assert_eq!(plan.benchmarks[1].id, "b_bench");
assert_eq!(plan.benchmarks[2].id, "c_bench");
}
#[test]
fn test_dependency_cycle_fallback() {
let benches = [
make_bench_with_deps("a_bench", "default", &[], &["c_bench"]),
make_bench_with_deps("b_bench", "default", &[], &["a_bench"]),
make_bench_with_deps("c_bench", "default", &[], &["b_bench"]),
];
let refs: Vec<_> = benches
.iter()
.map(|b| unsafe { &*(b as *const _) })
.collect();
let plan = build_plan(refs, None, None, None, None);
assert_eq!(plan.benchmarks.len(), 3);
assert_eq!(plan.benchmarks[0].id, "a_bench");
assert_eq!(plan.benchmarks[1].id, "b_bench");
assert_eq!(plan.benchmarks[2].id, "c_bench");
}
#[test]
fn test_skip_tag() {
let benches = [
make_bench("bench1", "default", &["fast"]),
make_bench("bench2", "default", &["slow"]),
make_bench("bench3", "default", &["fast", "skip_ci"]),
];
let refs: Vec<_> = benches
.iter()
.map(|b| unsafe { &*(b as *const _) })
.collect();
let plan = build_plan(refs, None, None, None, Some("skip_ci"));
assert_eq!(plan.benchmarks.len(), 2);
assert!(plan.benchmarks.iter().all(|b| !b.tags.contains(&"skip_ci")));
}
}