234 lines
7.3 KiB
Rust
234 lines
7.3 KiB
Rust
//! Tests for the precedence graph.
|
|
|
|
use apsis_task::{PrecedenceGraph, TaskErrorKind, TaskId};
|
|
|
|
/// The identifiers used by the tests.
|
|
fn id(index: usize) -> TaskId {
|
|
TaskId::new(index)
|
|
}
|
|
|
|
#[test]
|
|
fn an_empty_graph_has_no_edges() {
|
|
let graph = PrecedenceGraph::new();
|
|
assert!(graph.is_empty());
|
|
assert_eq!(graph.edge_count(), 0);
|
|
assert_eq!(graph.task_count(), 0);
|
|
assert!(!graph.has_constraint(id(0), id(1)));
|
|
assert!(graph.predecessors_of(id(0)).is_empty());
|
|
assert!(graph.successors_of(id(0)).is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn a_constraint_is_recorded_both_ways() {
|
|
let mut graph = PrecedenceGraph::new();
|
|
graph.add(id(0), id(1)).expect("no cycle");
|
|
|
|
assert!(graph.has_constraint(id(0), id(1)));
|
|
// The constraint is directed, so the reverse does not exist.
|
|
assert!(!graph.has_constraint(id(1), id(0)));
|
|
assert_eq!(graph.successors_of(id(0)), vec![id(1)]);
|
|
assert_eq!(graph.predecessors_of(id(1)), vec![id(0)]);
|
|
assert_eq!(graph.edge_count(), 1);
|
|
assert_eq!(graph.task_count(), 2);
|
|
}
|
|
|
|
#[test]
|
|
fn a_self_constraint_is_rejected() {
|
|
let mut graph = PrecedenceGraph::new();
|
|
let error = graph
|
|
.add(id(0), id(0))
|
|
.expect_err("a task cannot precede itself");
|
|
assert_eq!(error.kind(), TaskErrorKind::PrecedenceCycle);
|
|
assert!(graph.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn a_duplicate_constraint_is_accepted_and_ignored() {
|
|
let mut graph = PrecedenceGraph::new();
|
|
graph.add(id(0), id(1)).expect("no cycle");
|
|
graph.add(id(0), id(1)).expect("already present");
|
|
// The edge is recorded once.
|
|
assert_eq!(graph.edge_count(), 1);
|
|
assert_eq!(graph.successors_of(id(0)), vec![id(1)]);
|
|
}
|
|
|
|
#[test]
|
|
fn a_cycle_is_rejected() {
|
|
let mut graph = PrecedenceGraph::new();
|
|
graph.add(id(0), id(1)).expect("no cycle");
|
|
graph.add(id(1), id(2)).expect("no cycle");
|
|
// Closing the cycle is refused.
|
|
let error = graph.add(id(2), id(0)).expect_err("this closes a cycle");
|
|
assert_eq!(error.kind(), TaskErrorKind::PrecedenceCycle);
|
|
// The graph is unchanged.
|
|
assert!(!graph.has_constraint(id(2), id(0)));
|
|
assert_eq!(graph.edge_count(), 2);
|
|
}
|
|
|
|
#[test]
|
|
fn a_longer_cycle_is_rejected() {
|
|
let mut graph = PrecedenceGraph::new();
|
|
for edge in [(0, 1), (1, 2), (2, 3), (3, 4)] {
|
|
graph.add(id(edge.0), id(edge.1)).expect("no cycle");
|
|
}
|
|
assert_eq!(
|
|
graph
|
|
.add(id(4), id(1))
|
|
.expect_err("this closes a cycle")
|
|
.kind(),
|
|
TaskErrorKind::PrecedenceCycle
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn reachability_follows_the_direction() {
|
|
let mut graph = PrecedenceGraph::new();
|
|
graph.add(id(0), id(1)).expect("no cycle");
|
|
graph.add(id(1), id(2)).expect("no cycle");
|
|
|
|
// The transitive relation holds forward but not backward.
|
|
assert!(graph.is_reachable(id(0), id(1)));
|
|
assert!(graph.is_reachable(id(0), id(2)));
|
|
assert!(graph.is_reachable(id(1), id(2)));
|
|
assert!(!graph.is_reachable(id(2), id(0)));
|
|
// A task reaches itself by a path of no edges.
|
|
assert!(graph.is_reachable(id(0), id(0)));
|
|
}
|
|
|
|
#[test]
|
|
fn roots_and_leaves_are_reported() {
|
|
let mut graph = PrecedenceGraph::new();
|
|
// A diamond: 0 before 1 and 2, and both before 3.
|
|
graph.add(id(0), id(1)).expect("no cycle");
|
|
graph.add(id(0), id(2)).expect("no cycle");
|
|
graph.add(id(1), id(3)).expect("no cycle");
|
|
graph.add(id(2), id(3)).expect("no cycle");
|
|
|
|
assert_eq!(graph.roots(), vec![id(0)]);
|
|
assert_eq!(graph.leaves(), vec![id(3)]);
|
|
}
|
|
|
|
#[test]
|
|
fn a_chain_has_one_root_and_one_leaf() {
|
|
let mut graph = PrecedenceGraph::new();
|
|
graph.add(id(0), id(1)).expect("no cycle");
|
|
graph.add(id(1), id(2)).expect("no cycle");
|
|
assert_eq!(graph.roots(), vec![id(0)]);
|
|
assert_eq!(graph.leaves(), vec![id(2)]);
|
|
}
|
|
|
|
#[test]
|
|
fn the_topological_order_respects_the_constraints() {
|
|
let mut graph = PrecedenceGraph::new();
|
|
graph.add(id(0), id(1)).expect("no cycle");
|
|
graph.add(id(1), id(2)).expect("no cycle");
|
|
|
|
let tasks = [id(2), id(1), id(0)];
|
|
let order = graph
|
|
.topological_order(&tasks)
|
|
.expect("the graph is acyclic");
|
|
// The constraints force 0 before 1 before 2, whatever the input order.
|
|
assert_eq!(order, vec![id(0), id(1), id(2)]);
|
|
}
|
|
|
|
#[test]
|
|
fn the_topological_order_breaks_ties_by_position() {
|
|
let mut graph = PrecedenceGraph::new();
|
|
// Only 0 is constrained, before 1. Tasks 2 and 3 are free.
|
|
graph.add(id(0), id(1)).expect("no cycle");
|
|
|
|
// The preference order is 3, 2, 0, 1, so among the free tasks 3 comes
|
|
// first.
|
|
let tasks = [id(3), id(2), id(0), id(1)];
|
|
let order = graph
|
|
.topological_order(&tasks)
|
|
.expect("the graph is acyclic");
|
|
assert_eq!(order, vec![id(3), id(2), id(0), id(1)]);
|
|
}
|
|
|
|
#[test]
|
|
fn the_topological_order_honours_the_diamond() {
|
|
let mut graph = PrecedenceGraph::new();
|
|
graph.add(id(0), id(1)).expect("no cycle");
|
|
graph.add(id(0), id(2)).expect("no cycle");
|
|
graph.add(id(1), id(3)).expect("no cycle");
|
|
graph.add(id(2), id(3)).expect("no cycle");
|
|
|
|
let tasks = [id(0), id(1), id(2), id(3)];
|
|
let order = graph
|
|
.topological_order(&tasks)
|
|
.expect("the graph is acyclic");
|
|
assert_eq!(order, vec![id(0), id(1), id(2), id(3)]);
|
|
|
|
// The position of each task in the result relative to its constraints.
|
|
let position_of = |task: TaskId| {
|
|
order
|
|
.iter()
|
|
.position(|candidate| *candidate == task)
|
|
.expect("present")
|
|
};
|
|
assert!(position_of(id(0)) < position_of(id(1)));
|
|
assert!(position_of(id(0)) < position_of(id(2)));
|
|
assert!(position_of(id(1)) < position_of(id(3)));
|
|
assert!(position_of(id(2)) < position_of(id(3)));
|
|
}
|
|
|
|
#[test]
|
|
fn the_topological_order_includes_unconstrained_tasks() {
|
|
let mut graph = PrecedenceGraph::new();
|
|
graph.add(id(0), id(1)).expect("no cycle");
|
|
// Task 9 has no constraints at all.
|
|
let tasks = [id(0), id(1), id(9)];
|
|
let order = graph
|
|
.topological_order(&tasks)
|
|
.expect("the graph is acyclic");
|
|
assert_eq!(order.len(), 3);
|
|
assert!(order.contains(&id(9)));
|
|
}
|
|
|
|
#[test]
|
|
fn the_topological_order_ignores_predecessors_outside_the_list() {
|
|
// A constraint whose predecessor is not in the list cannot be waited
|
|
// for, so it is ignored rather than blocking the successor.
|
|
let mut graph = PrecedenceGraph::new();
|
|
graph.add(id(5), id(1)).expect("no cycle");
|
|
let tasks = [id(1)];
|
|
assert_eq!(
|
|
graph
|
|
.topological_order(&tasks)
|
|
.expect("the graph is acyclic"),
|
|
vec![id(1)]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn the_topological_order_of_an_empty_list_is_empty() {
|
|
let graph = PrecedenceGraph::new();
|
|
assert!(
|
|
graph
|
|
.topological_order(&[])
|
|
.expect("the graph is acyclic")
|
|
.is_empty()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn a_graph_with_two_independent_chains_orders_both() {
|
|
let mut graph = PrecedenceGraph::new();
|
|
graph.add(id(0), id(1)).expect("no cycle");
|
|
graph.add(id(2), id(3)).expect("no cycle");
|
|
|
|
let tasks = [id(0), id(1), id(2), id(3)];
|
|
let order = graph
|
|
.topological_order(&tasks)
|
|
.expect("the graph is acyclic");
|
|
let position_of = |task: TaskId| {
|
|
order
|
|
.iter()
|
|
.position(|candidate| *candidate == task)
|
|
.expect("present")
|
|
};
|
|
assert!(position_of(id(0)) < position_of(id(1)));
|
|
assert!(position_of(id(2)) < position_of(id(3)));
|
|
}
|