use std::collections::HashMap;
use crate::core::Isolation;
pub struct GroupInput<'a> {
pub eval_id: &'a str,
pub isolation: Option<Isolation>,
pub fixtures: &'a [(String, String)],
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Group {
pub id: String,
pub eval_ids: Vec<String>,
pub rationale: String,
}
pub fn compute_groups(evals: &[GroupInput]) -> Vec<Group> {
struct Building {
id: String,
eval_ids: Vec<String>,
claims: HashMap<String, (String, String)>,
sealed: bool,
rationale: String,
}
fn claims_of(ev: &GroupInput) -> HashMap<String, (String, String)> {
ev.fixtures
.iter()
.map(|(dest, source)| (dest.clone(), (source.clone(), ev.eval_id.to_string())))
.collect()
}
let mut groups: Vec<Building> = Vec::new();
for ev in evals {
if ev.isolation == Some(Isolation::Isolated) {
let id = format!("g{}", groups.len() + 1);
groups.push(Building {
id,
eval_ids: vec![ev.eval_id.to_string()],
claims: claims_of(ev),
sealed: true,
rationale: "isolation: isolated".to_string(),
});
continue;
}
let mut joined = false;
let mut conflict_note: Option<String> = None;
for g in groups.iter_mut().filter(|g| !g.sealed) {
let mut conflict: Option<(String, String)> = None;
for (dest, source) in ev.fixtures {
if let Some((prev_source, prev_eval)) = g.claims.get(dest)
&& prev_source != source
{
conflict = Some((dest.clone(), prev_eval.clone()));
break;
}
}
match conflict {
None => {
for (dest, source) in ev.fixtures {
g.claims
.entry(dest.clone())
.or_insert_with(|| (source.clone(), ev.eval_id.to_string()));
}
g.eval_ids.push(ev.eval_id.to_string());
joined = true;
break;
}
Some((dest, other_eval)) => {
conflict_note.get_or_insert_with(|| {
format!("fixture-conflict: {} vs {other_eval} at {dest}", ev.eval_id)
});
}
}
}
if !joined {
let id = format!("g{}", groups.len() + 1);
groups.push(Building {
id,
eval_ids: vec![ev.eval_id.to_string()],
claims: claims_of(ev),
sealed: false,
rationale: conflict_note.unwrap_or_else(|| "default".to_string()),
});
}
}
groups
.into_iter()
.map(|b| Group {
id: b.id,
eval_ids: b.eval_ids,
rationale: b.rationale,
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
fn input<'a>(
id: &'a str,
isolation: Option<Isolation>,
fixtures: &'a [(String, String)],
) -> GroupInput<'a> {
GroupInput {
eval_id: id,
isolation,
fixtures,
}
}
fn pair(dest: &str, source: &str) -> (String, String) {
(dest.to_string(), source.to_string())
}
#[test]
fn single_group_when_no_conflicts_or_hints() {
let f1 = [pair("a.txt", "/s/a.txt")];
let f2 = [pair("b.txt", "/s/b.txt")];
let evals = [
input("e1", None, &f1),
input("e2", None, &f2),
input("e3", None, &[]),
];
let groups = compute_groups(&evals);
assert_eq!(groups.len(), 1);
assert_eq!(groups[0].id, "g1");
assert_eq!(groups[0].eval_ids, vec!["e1", "e2", "e3"]);
assert_eq!(groups[0].rationale, "default");
}
#[test]
fn conflicting_fixtures_split_into_two_groups() {
let f1 = [pair("config.json", "/a/config.json")];
let f2 = [pair("config.json", "/b/config.json")];
let evals = [input("e1", None, &f1), input("e2", None, &f2)];
let groups = compute_groups(&evals);
assert_eq!(groups.len(), 2);
assert_eq!(groups[0].eval_ids, vec!["e1"]);
assert_eq!(groups[1].id, "g2");
assert_eq!(groups[1].eval_ids, vec!["e2"]);
assert!(
groups[1].rationale.contains("fixture-conflict"),
"rationale: {}",
groups[1].rationale
);
assert!(
groups[1].rationale.contains("config.json"),
"rationale: {}",
groups[1].rationale
);
}
#[test]
fn idempotent_same_source_share_stays_one_group() {
let f = [pair("config.json", "/a/config.json")];
let evals = [input("e1", None, &f), input("e2", None, &f)];
let groups = compute_groups(&evals);
assert_eq!(groups.len(), 1);
assert_eq!(groups[0].eval_ids, vec!["e1", "e2"]);
}
#[test]
fn isolated_hint_forces_singleton_and_seals_it() {
let evals = [
input("e1", Some(Isolation::Isolated), &[]),
input("e2", None, &[]),
input("e3", None, &[]),
];
let groups = compute_groups(&evals);
assert_eq!(groups.len(), 2);
assert_eq!(groups[0].eval_ids, vec!["e1"]);
assert_eq!(groups[0].rationale, "isolation: isolated");
assert_eq!(groups[1].eval_ids, vec!["e2", "e3"]);
assert_eq!(groups[1].rationale, "default");
}
#[test]
fn isolated_eval_with_fixtures_is_still_a_singleton() {
let f = [pair("x.txt", "/s/x.txt")];
let evals = [input("e1", Some(Isolation::Isolated), &f)];
let groups = compute_groups(&evals);
assert_eq!(groups.len(), 1);
assert_eq!(groups[0].eval_ids, vec!["e1"]);
assert_eq!(groups[0].rationale, "isolation: isolated");
}
#[test]
fn ids_are_deterministic_in_creation_order() {
let f1 = [pair("c.json", "/a/c.json")];
let f2 = [pair("c.json", "/b/c.json")];
let f3 = [pair("c.json", "/d/c.json")];
let evals = [
input("e1", None, &f1),
input("e2", None, &f2),
input("e3", None, &f3),
];
let groups = compute_groups(&evals);
assert_eq!(
groups.iter().map(|g| g.id.as_str()).collect::<Vec<_>>(),
vec!["g1", "g2", "g3"]
);
}
#[test]
fn eval_joins_first_non_conflicting_group() {
let f1 = [pair("a", "/s1/a")];
let f2 = [pair("a", "/s2/a")];
let f3 = [pair("a", "/s1/a")];
let evals = [
input("e1", None, &f1),
input("e2", None, &f2),
input("e3", None, &f3),
];
let groups = compute_groups(&evals);
assert_eq!(groups.len(), 2);
assert_eq!(groups[0].eval_ids, vec!["e1", "e3"]);
assert_eq!(groups[1].eval_ids, vec!["e2"]);
}
}