use std::collections::{BTreeMap, BTreeSet};
use anyhow::bail;
use crate::model::types::{Bead, Edge};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Link {
pub bead: usize,
pub edge: Edge,
pub first: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Placed {
pub bead: usize,
pub depth: u16,
pub edge: Option<Edge>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Assembled {
pub beads: Vec<Bead>,
pub children: Vec<Vec<Link>>,
pub dangling: Vec<String>,
pub cycles: Vec<String>,
}
pub struct Nesting<'a> {
by_id: BTreeMap<&'a str, &'a Bead>,
children: BTreeMap<&'a str, Vec<&'a str>>,
waiting_on_the_absent: BTreeSet<&'a str>,
lost_their_place: BTreeSet<&'a str>,
}
#[cfg(test)]
thread_local! {
static NESTINGS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
}
#[cfg(test)]
pub(crate) fn nestings_on_this_thread() -> usize {
NESTINGS.with(std::cell::Cell::get)
}
impl<'a> Nesting<'a> {
pub fn of(beads: &'a [Bead]) -> Self {
#[cfg(test)]
NESTINGS.with(|count| count.set(count.get() + 1));
let by_id: BTreeMap<&str, &Bead> = beads.iter().map(|b| (b.id.as_str(), b)).collect();
let mut children: BTreeMap<&str, BTreeSet<&str>> = BTreeMap::new();
let mut waiting_on_the_absent = BTreeSet::new();
let mut lost_their_place = BTreeSet::new();
for &bead in by_id.values() {
for edge in &bead.dependencies {
let nests: Option<(&'a str, &'a str)> = match edge.edge {
Edge::ParentChild => Some((&edge.on, &bead.id)),
Edge::Blocks => Some((&bead.id, &edge.on)),
Edge::Other(_) => None,
};
if by_id.contains_key(edge.on.as_str()) {
if let Some((over, under)) = nests {
children.entry(over).or_default().insert(under);
}
continue;
}
waiting_on_the_absent.insert(bead.id.as_str());
if let Some((_, under)) = nests.filter(|(over, _)| *over == edge.on) {
lost_their_place.insert(under);
}
}
}
let children = children
.into_iter()
.map(|(parent, kids)| {
let mut kids: Vec<&str> = kids.into_iter().collect();
kids.sort_by(|a, b| {
let (a, b) = (by_id[a], by_id[b]);
a.status
.rank()
.cmp(&b.status.rank())
.then(a.priority.cmp(&b.priority))
.then_with(|| a.id.cmp(&b.id))
});
(parent, kids)
})
.collect();
Nesting {
by_id,
children,
waiting_on_the_absent,
lost_their_place,
}
}
pub fn assemble(&self, root: &str) -> anyhow::Result<Assembled> {
let Some((&root, _)) = self.by_id.get_key_value(root) else {
bail!("bd's answer holds no bead {root} to draw a tree from");
};
let Reached {
order,
children,
looped,
} = reach(root, &self.children, &self.by_id);
let dangling: Vec<String> = order
.iter()
.filter(|id| self.waiting_on_the_absent.contains(*id))
.map(|id| id.to_string())
.collect();
let cycles = if looped {
cuts(&children)
.into_iter()
.map(|bead| order[bead].to_string())
.collect()
} else {
Vec::new()
};
let beads = order.iter().map(|id| self.by_id[id].clone()).collect();
Ok(Assembled {
beads,
children,
dangling,
cycles,
})
}
pub fn adrift(&self) -> Vec<String> {
self.lost_their_place
.iter()
.map(|id| id.to_string())
.collect()
}
pub fn top_of(&self, id: &str) -> Vec<String> {
let mut over: BTreeMap<&str, Vec<&str>> = BTreeMap::new();
for (parent, kids) in &self.children {
for kid in kids {
over.entry(kid).or_default().push(parent);
}
}
let mut above: BTreeSet<&str> = BTreeSet::new();
let mut climbing: Vec<&str> = vec![id];
while let Some(reached) = climbing.pop() {
if !above.insert(reached) {
continue;
}
climbing.extend(over.get(reached).into_iter().flatten());
}
let mut tops: BTreeSet<&str> = above
.iter()
.copied()
.filter(|reached| !over.contains_key(reached))
.collect();
loop {
let drawn = under(tops.iter().copied(), &self.children);
let Some(&left) = above.iter().find(|reached| !drawn.contains(*reached)) else {
break;
};
tops.insert(left);
}
for top in tops.clone() {
let without: BTreeSet<&str> =
tops.iter().copied().filter(|kept| *kept != top).collect();
let drawn = under(without.iter().copied(), &self.children);
if above.iter().all(|reached| drawn.contains(reached)) {
tops.remove(top);
}
}
tops.into_iter().map(str::to_string).collect()
}
}
fn under<'a>(
from: impl IntoIterator<Item = &'a str>,
children: &BTreeMap<&'a str, Vec<&'a str>>,
) -> BTreeSet<&'a str> {
let mut reached: BTreeSet<&str> = BTreeSet::new();
let mut going: Vec<&str> = from.into_iter().collect();
while let Some(bead) = going.pop() {
if !reached.insert(bead) {
continue;
}
going.extend(children.get(bead).into_iter().flatten().copied());
}
reached
}
struct Reached<'a> {
order: Vec<&'a str>,
children: Vec<Vec<Link>>,
looped: bool,
}
fn reach<'a>(
root: &'a str,
ordered: &BTreeMap<&'a str, Vec<&'a str>>,
by_id: &BTreeMap<&'a str, &'a Bead>,
) -> Reached<'a> {
let mut found = Reached {
order: vec![root],
children: vec![Vec::new()],
looped: false,
};
let mut placed: BTreeMap<&str, usize> = BTreeMap::from([(root, 0)]);
descend(
root,
0,
ordered,
by_id,
&mut placed,
&mut Vec::new(),
&mut found,
);
found
}
fn descend<'a>(
id: &'a str,
at: usize,
ordered: &BTreeMap<&'a str, Vec<&'a str>>,
by_id: &BTreeMap<&'a str, &'a Bead>,
placed: &mut BTreeMap<&'a str, usize>,
above: &mut Vec<&'a str>,
found: &mut Reached<'a>,
) {
above.push(id);
for &child in ordered.get(id).into_iter().flatten() {
let edge = if by_id[child]
.dependencies
.iter()
.any(|d| d.on == id && d.edge == Edge::ParentChild)
{
Edge::ParentChild
} else {
Edge::Blocks
};
found.looped |= above.contains(&child);
let (bead, first) = match placed.get(child) {
Some(&bead) => (bead, false),
None => {
let bead = found.order.len();
found.order.push(child);
found.children.push(Vec::new());
placed.insert(child, bead);
(bead, true)
}
};
found.children[at].push(Link { bead, edge, first });
if first {
descend(child, bead, ordered, by_id, placed, above, found);
}
}
above.pop();
}
fn cuts(children: &[Vec<Link>]) -> BTreeSet<usize> {
let reach: Vec<BTreeSet<usize>> = (0..children.len())
.map(|from| {
let mut reached = BTreeSet::new();
let mut going = vec![from];
while let Some(bead) = going.pop() {
for link in &children[bead] {
if reached.insert(link.bead) {
going.push(link.bead);
}
}
}
reached
})
.collect();
cuts_from(0, &mut Vec::new(), children, &reach, &mut BTreeMap::new())
}
fn cuts_from(
at: usize,
above: &mut Vec<usize>,
children: &[Vec<Link>],
reach: &[BTreeSet<usize>],
answered: &mut BTreeMap<(usize, Vec<usize>), BTreeSet<usize>>,
) -> BTreeSet<usize> {
let seen: Vec<usize> = above
.iter()
.copied()
.filter(|bead| reach[at].contains(bead))
.collect();
if let Some(cut) = answered.get(&(at, seen.clone())) {
return cut.clone();
}
let mut cut = BTreeSet::new();
above.push(at);
for link in &children[at] {
if above.contains(&link.bead) {
cut.insert(link.bead);
} else {
cut.extend(cuts_from(link.bead, above, children, reach, answered));
}
}
above.pop();
answered.insert((at, seen), cut.clone());
cut
}
pub fn links_from<'a>(children: &'a [Vec<Link>], at: usize, above: &[usize]) -> Vec<&'a Link> {
children[at]
.iter()
.filter(|link| link.bead != at && !above.contains(&link.bead))
.collect()
}
pub fn beneath(children: &[Vec<Link>], at: usize, above: &[usize]) -> Vec<usize> {
let mut reached: Vec<bool> = vec![false; children.len()];
reached[at] = true;
for bead in above {
reached[*bead] = true;
}
let mut found = Vec::new();
let mut going = vec![at];
while let Some(bead) = going.pop() {
for link in &children[bead] {
if !reached[link.bead] {
reached[link.bead] = true;
found.push(link.bead);
going.push(link.bead);
}
}
}
found
}
pub fn unroll(children: &[Vec<Link>]) -> Vec<Placed> {
let mut rows = Vec::new();
if !children.is_empty() {
unroll_from(0, 0, None, children, &mut Vec::new(), &mut rows);
}
rows
}
fn unroll_from(
at: usize,
depth: u16,
edge: Option<Edge>,
children: &[Vec<Link>],
above: &mut Vec<usize>,
out: &mut Vec<Placed>,
) {
out.push(Placed {
bead: at,
depth,
edge,
});
let links = links_from(children, at, above);
above.push(at);
for link in links {
unroll_from(
link.bead,
depth + 1,
Some(link.edge.clone()),
children,
above,
out,
);
}
above.pop();
}
#[cfg(test)]
mod tests {
use super::*;
const ROOT: &str = "r";
use crate::collect::bd::parse_beads;
use crate::model::types::Edge;
const FIXTURE: &str = include_str!("../../tests/fixtures/bd_list.json");
const FIXTURE_ROOT: &str = "bdi-2bb";
fn assembled(json: &str, root: &str) -> Assembled {
let beads = parse_beads(json).expect("the rows parse");
Nesting::of(&beads)
.assemble(root)
.expect("the rows assemble")
}
fn rows(a: &Assembled) -> Vec<(&str, u16, Option<Edge>)> {
unroll(&a.children)
.into_iter()
.map(|p| (a.beads[p.bead].id.as_str(), p.depth, p.edge))
.collect()
}
fn ids(a: &Assembled) -> Vec<&str> {
rows(a).into_iter().map(|(id, _, _)| id).collect()
}
fn drawn(a: &Assembled, id: &str) -> usize {
ids(a).into_iter().filter(|drawn| *drawn == id).count()
}
fn depth_of(a: &Assembled, id: &str) -> u16 {
rows(a)
.into_iter()
.find(|(drawn, _, _)| *drawn == id)
.unwrap_or_else(|| panic!("{id} is in the rows"))
.1
}
fn index_of(a: &Assembled, id: &str) -> usize {
a.beads
.iter()
.position(|b| b.id == id)
.unwrap_or_else(|| panic!("{id} is among the beads"))
}
fn dep(on: &str, kind: &str) -> String {
format!(r#"{{"depends_on_id":"{on}","type":"{kind}"}}"#)
}
fn bead(id: &str, status: &str, deps: &[String]) -> String {
format!(
r#"{{"id":"{id}","title":"{id}","status":"{status}","dependencies":[{}]}}"#,
deps.join(",")
)
}
fn tracker(beads: &[String]) -> String {
format!("[{}]", beads.join(","))
}
fn parent_of(a: &Assembled, id: &str) -> Option<String> {
let rows = rows(a);
let at = rows.iter().position(|(drawn, _, _)| *drawn == id)?;
let depth = rows[at].1;
rows[..at]
.iter()
.rev()
.find(|(_, above, _)| *above < depth)
.map(|(parent, _, _)| (*parent).to_string())
}
fn parents_of(a: &Assembled, id: &str) -> Vec<String> {
let rows = rows(a);
rows.iter()
.enumerate()
.filter(|(_, (drawn, _, _))| *drawn == id)
.map(|(at, (_, depth, _))| {
rows[..at]
.iter()
.rev()
.find(|(_, above, _)| above < depth)
.map(|(parent, _, _)| (*parent).to_string())
.expect("every row but the root hangs under one")
})
.collect()
}
#[test]
fn a_blocker_is_drawn_beneath_the_bead_it_blocks() {
let json = tracker(&[
bead("r", "open", &[]),
bead(
"late",
"open",
&[dep("r", "parent-child"), dep("early", "blocks")],
),
bead("early", "closed", &[dep("r", "parent-child")]),
]);
let a = assembled(&json, ROOT);
assert_eq!(parent_of(&a, "early").as_deref(), Some("late"));
}
#[test]
fn a_child_stays_beneath_its_own_parent() {
let json = tracker(&[
bead("r", "open", &[]),
bead("r.1", "open", &[dep("r", "parent-child")]),
]);
let a = assembled(&json, ROOT);
assert_eq!(parent_of(&a, "r.1").as_deref(), Some("r"));
}
#[test]
fn a_blocker_of_several_beads_is_drawn_under_each_of_them() {
let json = tracker(&[
bead("r", "open", &[]),
bead(
"a",
"open",
&[dep("r", "parent-child"), dep("done", "blocks")],
),
bead(
"b",
"open",
&[dep("r", "parent-child"), dep("done", "blocks")],
),
bead("done", "closed", &[dep("r", "parent-child")]),
]);
let a = assembled(&json, ROOT);
assert_eq!(
drawn(&a, "done"),
3,
"once under the root, once under each waiter"
);
let under = parents_of(&a, "done");
assert!(under.contains(&"a".to_string()));
assert!(under.contains(&"b".to_string()));
}
#[test]
fn a_bead_drawn_several_ways_is_held_once_and_each_way_down_points_at_it() {
let json = tracker(&[
bead("r", "open", &[]),
bead(
"a",
"open",
&[dep("r", "parent-child"), dep("done", "blocks")],
),
bead(
"b",
"open",
&[dep("r", "parent-child"), dep("done", "blocks")],
),
bead("done", "closed", &[dep("r", "parent-child")]),
]);
let a = assembled(&json, ROOT);
let held: Vec<&str> = a.beads.iter().map(|b| b.id.as_str()).collect();
assert_eq!(
held,
vec!["r", "a", "done", "b"],
"once each, as first reached"
);
let done = index_of(&a, "done");
let linked_from: Vec<&str> = a
.children
.iter()
.enumerate()
.filter(|(_, links)| links.iter().any(|link| link.bead == done))
.map(|(from, _)| a.beads[from].id.as_str())
.collect();
assert_eq!(linked_from, vec!["r", "a", "b"]);
}
#[test]
fn a_closed_blocker_is_a_leaf_rather_than_a_branch_over_what_waited_on_it() {
let json = tracker(&[
bead("r", "open", &[]),
bead(
"waited",
"open",
&[dep("r", "parent-child"), dep("done", "blocks")],
),
bead("done", "closed", &[dep("r", "parent-child")]),
]);
let a = assembled(&json, ROOT);
assert!(
a.children[index_of(&a, "done")].is_empty(),
"a closed blocker has nothing beneath it"
);
}
#[test]
fn a_bead_reached_two_ways_is_drawn_once_for_each() {
let json = tracker(&[
bead("r", "open", &[]),
bead(
"r.1",
"open",
&[dep("r", "parent-child"), dep("r.2", "blocks")],
),
bead("r.2", "open", &[dep("r", "parent-child")]),
]);
let a = assembled(&json, ROOT);
assert_eq!(
drawn(&a, "r.2"),
2,
"once as the root's child, once as what r.1 waits on"
);
assert_eq!(a.beads.len(), 3, "and held once");
}
#[test]
fn the_first_link_to_a_bead_is_the_way_the_walk_first_reached_it() {
let json = tracker(&[
bead("r", "open", &[]),
bead(
"r.1",
"open",
&[dep("r", "parent-child"), dep("r.2", "blocks")],
),
bead("r.2", "open", &[dep("r", "parent-child")]),
]);
let a = assembled(&json, ROOT);
let (r_1, r_2) = (index_of(&a, "r.1"), index_of(&a, "r.2"));
let firsts: Vec<(usize, usize, bool)> = a
.children
.iter()
.enumerate()
.flat_map(|(from, links)| links.iter().map(move |l| (from, l.bead, l.first)))
.collect();
assert_eq!(
firsts,
vec![(0, r_1, true), (0, r_2, false), (r_1, r_2, true)]
);
assert_eq!(
rows(&a)[..3]
.iter()
.map(|(id, depth, _)| (*id, *depth))
.collect::<Vec<_>>(),
vec![("r", 0), ("r.1", 1), ("r.2", 2)],
"which is the way down its first row takes"
);
}
#[test]
fn a_link_carries_the_kind_of_edge_that_hangs_the_bead_there() {
let json = tracker(&[
bead("r", "open", &[]),
bead(
"r.1",
"open",
&[dep("r", "parent-child"), dep("r.2", "blocks")],
),
bead("r.2", "open", &[dep("r", "parent-child")]),
]);
let a = assembled(&json, ROOT);
let r_1 = index_of(&a, "r.1");
assert_eq!(a.children[0][0].edge, Edge::ParentChild);
assert_eq!(a.children[r_1][0].edge, Edge::Blocks);
assert_eq!(
rows(&a),
vec![
("r", 0, None),
("r.1", 1, Some(Edge::ParentChild)),
("r.2", 2, Some(Edge::Blocks)),
("r.2", 1, Some(Edge::ParentChild)),
]
);
}
#[test]
fn a_bead_blocked_by_one_of_its_own_forebears_is_reported_and_its_beads_kept() {
let json = tracker(&[
bead("r", "open", &[]),
bead(
"r.1",
"open",
&[dep("r", "parent-child"), dep("r", "blocks")],
),
]);
let a = assembled(&json, ROOT);
assert_eq!(a.cycles, vec!["r".to_string()]);
assert_eq!(ids(&a), vec!["r", "r.1"], "both are kept");
}
#[test]
fn a_loop_is_cut_where_the_way_down_comes_back_on_itself() {
let json = tracker(&[
bead("r", "open", &[]),
bead(
"r.1",
"open",
&[dep("r", "parent-child"), dep("r", "blocks")],
),
]);
let a = assembled(&json, ROOT);
let r_1 = index_of(&a, "r.1");
assert_eq!(a.children[r_1].len(), 1, "the way back up is held");
assert!(links_from(&a.children, r_1, &[0]).is_empty());
assert_eq!(beneath(&a.children, r_1, &[0]), Vec::<usize>::new());
assert_eq!(beneath(&a.children, 0, &[]), vec![r_1]);
}
fn every_way_down(children: &[Vec<Link>]) -> BTreeSet<usize> {
fn walk(
at: usize,
above: &mut Vec<usize>,
children: &[Vec<Link>],
cut: &mut BTreeSet<usize>,
) {
above.push(at);
for link in &children[at] {
if above.contains(&link.bead) {
cut.insert(link.bead);
} else {
walk(link.bead, above, children, cut);
}
}
above.pop();
}
let mut cut = BTreeSet::new();
walk(0, &mut Vec::new(), children, &mut cut);
cut
}
#[test]
fn a_loop_is_reported_at_the_beads_a_walk_over_every_way_down_would_cut_it_at() {
let met_from_above = tracker(&[
bead("r", "open", &[]),
bead("x", "open", &[dep("r", "parent-child"), dep("c", "blocks")]),
bead("c", "open", &[dep("x", "blocks")]),
]);
let both_ways_round = tracker(&[
bead("r", "open", &[]),
bead("a", "open", &[dep("r", "parent-child"), dep("b", "blocks")]),
bead("b", "open", &[dep("r", "parent-child"), dep("a", "blocks")]),
]);
let shared_above = tracker(&[
bead("r", "open", &[]),
bead("p", "open", &[dep("r", "parent-child"), dep("m", "blocks")]),
bead("q", "open", &[dep("r", "parent-child"), dep("m", "blocks")]),
bead("m", "open", &[dep("n", "blocks")]),
bead("n", "open", &[dep("m", "blocks")]),
]);
let under_itself = tracker(&[
bead("r", "open", &[]),
bead("s", "open", &[dep("r", "parent-child"), dep("s", "blocks")]),
]);
for (json, expected) in [
(met_from_above, vec!["x"]),
(both_ways_round, vec!["a", "b"]),
(shared_above, vec!["m"]),
(under_itself, vec!["s"]),
] {
let a = assembled(&json, ROOT);
let reference: Vec<&str> = every_way_down(&a.children)
.into_iter()
.map(|bead| a.beads[bead].id.as_str())
.collect();
assert_eq!(a.cycles, expected, "{json}");
assert_eq!(a.cycles, reference, "{json}");
}
}
#[test]
fn a_tree_with_no_loop_reports_none() {
assert!(assembled(FIXTURE, FIXTURE_ROOT).cycles.is_empty());
}
#[test]
fn an_edge_kind_bdi_does_not_know_nests_nothing() {
let json = tracker(&[
bead("r", "open", &[]),
bead(
"r.1",
"open",
&[dep("r", "parent-child"), dep("r.2", "discovered-by")],
),
bead("r.2", "open", &[dep("r", "parent-child")]),
]);
let a = assembled(&json, ROOT);
assert_eq!(parent_of(&a, "r.2").as_deref(), Some("r"));
assert_eq!(drawn(&a, "r.2"), 1);
}
#[test]
fn the_root_leads_the_rows_at_depth_zero() {
let a = assembled(FIXTURE, FIXTURE_ROOT);
assert_eq!(a.beads[0].id, FIXTURE_ROOT);
assert_eq!(
rows(&a)[0],
(FIXTURE_ROOT, 0, None),
"nothing reached the root"
);
}
#[test]
fn a_descendant_follows_its_own_forebear_rather_than_the_next_sibling() {
let a = assembled(FIXTURE, FIXTURE_ROOT);
let rows = rows(&a);
let at = rows
.iter()
.position(|(id, _, _)| *id == "bdi-2bb.5")
.expect("the waiting bead is drawn");
assert_eq!(
rows[at + 1],
("bdi-2bb.3", rows[at].1 + 1, Some(Edge::Blocks))
);
}
#[test]
fn the_tracker_draws_a_blocker_under_each_bead_that_waits_on_it() {
let a = assembled(FIXTURE, FIXTURE_ROOT);
assert_eq!(drawn(&a, "bdi-2bb.3"), 3);
}
#[test]
fn depth_counts_the_parent_chain_rather_than_what_bd_reported() {
let json = r#"[
{"id":"r","title":"root","status":"open","depth":7},
{"id":"r.1","title":"child","status":"open",
"dependencies":[{"depends_on_id":"r","type":"parent-child"}],"depth":7},
{"id":"r.1.1","title":"grandchild","status":"open",
"dependencies":[{"depends_on_id":"r.1","type":"parent-child"}],"depth":0}
]"#;
let a = assembled(json, ROOT);
assert_eq!(depth_of(&a, "r"), 0);
assert_eq!(depth_of(&a, "r.1"), 1);
assert_eq!(depth_of(&a, "r.1.1"), 2);
}
#[test]
fn a_root_depending_on_nothing_is_not_reported_as_a_dangling_parent() {
let json = r#"[
{"id":"r","title":"root","status":"open"},
{"id":"r.1","title":"child","status":"open",
"dependencies":[{"depends_on_id":"r","type":"parent-child"}]}
]"#;
let a = assembled(json, ROOT);
assert_eq!(a.beads[0].id, "r");
assert!(a.dangling.is_empty(), "the root is not a dangling parent");
}
#[test]
fn siblings_order_by_state_then_priority_then_id() {
let json = r#"[
{"id":"t","title":"root","status":"open"},
{"id":"t.a","title":"a","status":"open","priority":1,
"dependencies":[{"depends_on_id":"t","type":"parent-child"}]},
{"id":"t.b","title":"b","status":"open","priority":1,
"dependencies":[{"depends_on_id":"t","type":"parent-child"}]},
{"id":"t.c","title":"c","status":"open","priority":0,
"dependencies":[{"depends_on_id":"t","type":"parent-child"}]},
{"id":"t.d","title":"d","status":"in_progress","priority":9,
"dependencies":[{"depends_on_id":"t","type":"parent-child"}]},
{"id":"t.e","title":"e","status":"closed","priority":0,
"dependencies":[{"depends_on_id":"t","type":"parent-child"}]},
{"id":"t.f","title":"f","status":"blocked","priority":5,
"dependencies":[{"depends_on_id":"t","type":"parent-child"}]}
]"#;
let a = assembled(json, "t");
assert_eq!(ids(&a), vec!["t", "t.d", "t.f", "t.c", "t.a", "t.b", "t.e"]);
}
#[test]
fn real_siblings_from_the_tracker_come_back_in_flight_first() {
let a = assembled(FIXTURE, FIXTURE_ROOT);
assert_eq!(ids(&a)[1], "bdi-r5l", "the bead in flight leads");
}
#[test]
fn a_bead_whose_parent_is_absent_is_reported_and_kept() {
let json = r#"[
{"id":"r","title":"root","status":"open"},
{"id":"r.9","title":"orphan","status":"open",
"dependencies":[{"depends_on_id":"r.404","type":"parent-child"}]}
]"#;
let its_own = assembled(json, "r.9");
assert_eq!(
ids(&its_own),
vec!["r.9"],
"the orphan is kept, not dropped"
);
assert_eq!(its_own.dangling, vec!["r.9".to_string()]);
assert_eq!(depth_of(&its_own, "r.9"), 0);
assert!(its_own.cycles.is_empty());
let elsewhere = assembled(json, ROOT);
assert_eq!(ids(&elsewhere), vec!["r"]);
assert!(elsewhere.dangling.is_empty());
}
#[test]
fn an_orphan_does_not_join_a_tree_that_never_named_it() {
let json = r#"[
{"id":"one","title":"one","status":"open"},
{"id":"two","title":"two","status":"open"},
{"id":"lost","title":"its parent was deleted","status":"closed",
"dependencies":[{"depends_on_id":"gone","type":"parent-child"}]}
]"#;
assert_eq!(ids(&assembled(json, "one")), vec!["one"]);
assert_eq!(ids(&assembled(json, "two")), vec!["two"]);
}
#[test]
fn a_root_the_answer_does_not_hold_is_a_loud_failure() {
let json = r#"[
{"id":"one","title":"one","status":"open"},
{"id":"two","title":"two","status":"open"}
]"#;
let beads = parse_beads(json).unwrap();
let err = Nesting::of(&beads)
.assemble("three")
.expect_err("no bead three to draw from")
.to_string();
assert!(err.contains("three"), "names the root asked for: {err}");
assert!(
Nesting::of(&[]).assemble("one").is_err(),
"an empty answer holds no root"
);
}
#[test]
fn a_bead_the_root_does_not_reach_belongs_to_another_tree_and_is_not_drawn() {
let json = r#"[
{"id":"one","title":"one","status":"open"},
{"id":"one.1","title":"child","status":"open",
"dependencies":[{"depends_on_id":"one","type":"parent-child"}]},
{"id":"two","title":"two","status":"open"},
{"id":"two.1","title":"child","status":"open",
"dependencies":[{"depends_on_id":"two","type":"parent-child"}]}
]"#;
let a = assembled(json, "one");
assert_eq!(ids(&a), vec!["one", "one.1"]);
}
#[test]
fn nothing_is_unrolled_from_no_tree() {
assert!(unroll(&[]).is_empty());
}
}