use super::{BacklogItem, ItemId};
use std::collections::{HashMap, HashSet};
#[must_use]
pub fn parent_creates_cycle(items: &[BacklogItem], child: &ItemId, parent: &ItemId) -> bool {
if child == parent {
return true;
}
let parents: HashMap<&ItemId, &ItemId> = items
.iter()
.filter_map(|it| it.parent.as_ref().map(|p| (&it.id, p)))
.collect();
let mut current = Some(parent);
let mut seen = HashSet::new();
while let Some(id) = current {
if id == child {
return true;
}
if !seen.insert(id) {
break;
}
current = parents.get(id).copied();
}
false
}
#[must_use]
pub fn dependency_creates_cycle(items: &[BacklogItem], item: &ItemId, dep: &ItemId) -> bool {
if item == dep {
return true;
}
let deps: HashMap<&ItemId, &[ItemId]> = items
.iter()
.map(|it| (&it.id, it.depends_on.as_slice()))
.collect();
let mut stack = vec![dep];
let mut seen = HashSet::new();
while let Some(id) = stack.pop() {
if id == item {
return true;
}
if !seen.insert(id) {
continue;
}
if let Some(edges) = deps.get(id) {
stack.extend(edges.iter());
}
}
false
}
#[cfg(test)]
mod tests {
use super::*;
use crate::backlog::Status;
use crate::rank::Rank;
use chrono::DateTime;
fn item(n: u32) -> BacklogItem {
BacklogItem::new(
ItemId::new("T", n),
format!("Item {n}"),
Status::new("todo"),
Rank::between(None, None).expect("open bounds produce a rank"),
DateTime::from_timestamp(0, 0).expect("valid epoch"),
)
.expect("valid item")
}
fn item_with_parent(n: u32, parent: u32) -> BacklogItem {
let mut it = item(n);
it.parent = Some(ItemId::new("T", parent));
it
}
fn item_with_deps(n: u32, deps: &[u32]) -> BacklogItem {
let mut it = item(n);
it.depends_on = deps.iter().map(|&d| ItemId::new("T", d)).collect();
it
}
#[test]
fn parent_self_reference_is_a_cycle() {
let items = [item(1)];
assert!(parent_creates_cycle(
&items,
&ItemId::new("T", 1),
&ItemId::new("T", 1)
));
}
#[test]
fn parent_to_unrelated_item_is_acyclic() {
let items = [item(1), item(2)];
assert!(!parent_creates_cycle(
&items,
&ItemId::new("T", 1),
&ItemId::new("T", 2)
));
}
#[test]
fn parent_to_descendant_is_a_cycle() {
let items = [item(1), item_with_parent(2, 1)];
assert!(parent_creates_cycle(
&items,
&ItemId::new("T", 1),
&ItemId::new("T", 2)
));
}
#[test]
fn parent_to_transitive_descendant_is_a_cycle() {
let items = [item(1), item_with_parent(2, 1), item_with_parent(3, 2)];
assert!(parent_creates_cycle(
&items,
&ItemId::new("T", 1),
&ItemId::new("T", 3)
));
}
#[test]
fn parent_to_sibling_subtree_is_acyclic() {
let items = [item(1), item_with_parent(2, 1), item_with_parent(3, 1)];
assert!(!parent_creates_cycle(
&items,
&ItemId::new("T", 3),
&ItemId::new("T", 2)
));
}
#[test]
fn parent_traversal_stops_on_an_existing_cycle() {
let items = [item_with_parent(1, 2), item_with_parent(2, 1)];
assert!(!parent_creates_cycle(
&items,
&ItemId::new("T", 3),
&ItemId::new("T", 1)
));
}
#[test]
fn dependency_self_reference_is_a_cycle() {
let items = [item(1)];
assert!(dependency_creates_cycle(
&items,
&ItemId::new("T", 1),
&ItemId::new("T", 1)
));
}
#[test]
fn dependency_on_independent_item_is_acyclic() {
let items = [item(1), item(2)];
assert!(!dependency_creates_cycle(
&items,
&ItemId::new("T", 1),
&ItemId::new("T", 2)
));
}
#[test]
fn dependency_back_edge_is_a_cycle() {
let items = [item(1), item_with_deps(2, &[1])];
assert!(dependency_creates_cycle(
&items,
&ItemId::new("T", 1),
&ItemId::new("T", 2)
));
}
#[test]
fn dependency_transitive_back_edge_is_a_cycle() {
let items = [item(1), item_with_deps(2, &[1]), item_with_deps(3, &[2])];
assert!(dependency_creates_cycle(
&items,
&ItemId::new("T", 1),
&ItemId::new("T", 3)
));
}
#[test]
fn dependency_shared_diamond_is_acyclic() {
let items = [
item(1),
item_with_deps(2, &[1]),
item_with_deps(3, &[1]),
item_with_deps(4, &[2, 3]),
];
assert!(!dependency_creates_cycle(
&items,
&ItemId::new("T", 4),
&ItemId::new("T", 1)
));
}
#[test]
fn dependency_traversal_ignores_an_existing_cycle_not_reaching_the_item() {
let items = [item_with_deps(1, &[2]), item_with_deps(2, &[1])];
assert!(!dependency_creates_cycle(
&items,
&ItemId::new("T", 3),
&ItemId::new("T", 1)
));
}
}