//! Tests for precedence honoured by the planner, and for the improvement //! pass. use apsis_resource::{Resource, ResourceId, ResourceSet, Window}; use apsis_scheduler::{ImprovementSettings, Planner}; use apsis_task::{PrecedenceGraph, Task, TaskId}; /// A resource available from 0 to 1 000 seconds. fn resource(id: usize, capacity: f64) -> Resource { let mut resource = Resource::new(ResourceId::new(id), capacity); resource .set_windows(&[Window::new(0.0, 1_000.0)]) .expect("the window is usable"); resource } /// A task requiring one unit of each named resource. fn task(id: usize, duration: f64, resources: &[usize]) -> Task { let mut task = Task::new(TaskId::new(id), duration); for resource in resources { task = task .try_require(ResourceId::new(*resource), 1.0) .expect("the resource is new"); } task } #[test] fn a_chain_of_tasks_is_placed_in_order() { // Three tasks that must run one after another on the same resource. let resources = ResourceSet::from_resources(vec![resource(0, 1.0)]).expect("one"); let tasks = [ task(0, 60.0, &[0]), task(1, 30.0, &[0]), task(2, 90.0, &[0]), ]; let mut graph = PrecedenceGraph::new(); graph.add(TaskId::new(0), TaskId::new(1)).expect("no cycle"); graph.add(TaskId::new(1), TaskId::new(2)).expect("no cycle"); let result = Planner::new().schedule_with_precedence(&tasks, &resources, &graph); assert!(result.is_complete(), "{:?}", result.outcomes); assert!(result.honours_precedence(&graph)); let first = result .schedule .placement_of(TaskId::new(0)) .expect("placed"); let second = result .schedule .placement_of(TaskId::new(1)) .expect("placed"); let third = result .schedule .placement_of(TaskId::new(2)) .expect("placed"); assert_eq!(first.start, 0.0); assert_eq!(second.start, 60.0); assert_eq!(third.start, 90.0); } #[test] fn a_precedence_floor_delays_a_task_that_could_start_earlier() { // The second task could start at zero on its own, but must wait for the // first, which runs on a different resource. let resources = ResourceSet::from_resources(vec![resource(0, 1.0), resource(1, 1.0)]).expect("distinct"); let tasks = [task(0, 100.0, &[0]), task(1, 60.0, &[1])]; let mut graph = PrecedenceGraph::new(); graph.add(TaskId::new(0), TaskId::new(1)).expect("no cycle"); let result = Planner::new().schedule_with_precedence(&tasks, &resources, &graph); assert!(result.is_complete()); let second = result .schedule .placement_of(TaskId::new(1)) .expect("placed"); // The second resource was free from the start, but the constraint delays // the task to the end of the first. assert_eq!(second.start, 100.0); assert!(result.honours_precedence(&graph)); } #[test] fn a_diamond_precedence_is_honoured() { let resources = ResourceSet::from_resources(vec![resource(0, 1.0), resource(1, 1.0)]).expect("distinct"); // The first task feeds two parallel tasks, which both feed a last one. let tasks = [ task(0, 100.0, &[0]), task(1, 60.0, &[1]), task(2, 60.0, &[1]), task(3, 30.0, &[0]), ]; let mut graph = PrecedenceGraph::new(); graph.add(TaskId::new(0), TaskId::new(1)).expect("no cycle"); graph.add(TaskId::new(0), TaskId::new(2)).expect("no cycle"); graph.add(TaskId::new(1), TaskId::new(3)).expect("no cycle"); graph.add(TaskId::new(2), TaskId::new(3)).expect("no cycle"); let result = Planner::new().schedule_with_precedence(&tasks, &resources, &graph); assert!(result.is_complete(), "{:?}", result.outcomes); assert!(result.honours_precedence(&graph)); // The last task must follow both of the parallel ones. let last = result .schedule .placement_of(TaskId::new(3)) .expect("placed"); for parallel in [1_usize, 2] { let placement = result .schedule .placement_of(TaskId::new(parallel)) .expect("placed"); assert!( placement.end() <= last.start, "{placement:?} against {last:?}" ); } } #[test] fn a_sequential_observation_and_downlink_is_expressed_as_two_tasks() { // An observation uses the payload and a downlink uses the transmitter, // and the downlink must follow the observation. This is the pattern for // a task that needs two resources in sequence. let resources = ResourceSet::from_resources(vec![resource(0, 1.0), resource(1, 1.0)]).expect("distinct"); let observation = task(0, 120.0, &[0]).with_window(50.0, 500.0); let downlink = task(1, 90.0, &[1]).with_window(0.0, 1_000.0); let tasks = [observation, downlink]; let mut graph = PrecedenceGraph::new(); graph.add(TaskId::new(0), TaskId::new(1)).expect("no cycle"); let result = Planner::new().schedule_with_precedence(&tasks, &resources, &graph); assert!(result.is_complete()); assert!(result.honours_precedence(&graph)); let collection = result .schedule .placement_of(TaskId::new(0)) .expect("placed"); let transmission = result .schedule .placement_of(TaskId::new(1)) .expect("placed"); assert_eq!(collection.start, 50.0); // The downlink starts when the collection ends. assert_eq!(transmission.start, 170.0); // The two use different resources, so they may be placed back to back // without a conflict. assert_eq!(collection.end(), transmission.start); } #[test] fn a_successor_of_an_unplaced_task_is_deferred() { // The first task cannot be placed, so the constraint cannot be met and // the second is deferred rather than placed early. let resources = ResourceSet::from_resources(vec![resource(0, 1.0)]).expect("one"); let impossible = task(0, 90.0, &[0]).with_window(0.0, 60.0); let successor = task(1, 30.0, &[0]); let tasks = [impossible, successor]; let mut graph = PrecedenceGraph::new(); graph.add(TaskId::new(0), TaskId::new(1)).expect("no cycle"); let result = Planner::new().schedule_with_precedence(&tasks, &resources, &graph); assert!(!result.schedule.is_placed(TaskId::new(1))); assert!(result.deferred().contains(&TaskId::new(1))); // The constraints are trivially satisfied, because nothing was promised. assert!(result.honours_precedence(&graph)); } #[test] fn the_precedence_aware_order_is_topological() { let tasks = [ Task::new(TaskId::new(0), 60.0), Task::new(TaskId::new(1), 60.0), Task::new(TaskId::new(2), 60.0), ]; let mut graph = PrecedenceGraph::new(); graph.add(TaskId::new(2), TaskId::new(0)).expect("no cycle"); let order = Planner::new().order_with_precedence(&tasks, Some(&graph)); let position = |id: TaskId| { order .iter() .position(|candidate| *candidate == id) .expect("present") }; assert!(position(TaskId::new(2)) < position(TaskId::new(0))); } #[test] fn a_cycle_in_the_graph_falls_back_to_the_value_order() { // The graph refuses a cycle, so a cycle can only exist if the caller // builds the graph from one task set and orders another. The planner // then falls back to the value order rather than failing. let tasks = [ Task::new(TaskId::new(0), 60.0), Task::new(TaskId::new(1), 60.0), ]; let graph = PrecedenceGraph::new(); let order = Planner::new().order_with_precedence(&tasks, Some(&graph)); // An empty graph gives the value order, which for equal values is by // identifier. assert_eq!(order, vec![TaskId::new(0), TaskId::new(1)]); } #[test] fn the_improvement_pass_is_skipped_when_precedence_is_used() { let resources = ResourceSet::from_resources(vec![resource(0, 1.0)]).expect("one"); let tasks = [task(0, 60.0, &[0]), task(1, 60.0, &[0])]; let graph = PrecedenceGraph::new(); let result = Planner::new().schedule_with_precedence(&tasks, &resources, &graph); // The pass does not run, so nothing is reported as improved. assert!(result.improved.is_empty()); } #[test] fn the_improvement_pass_can_be_disabled() { let resources = ResourceSet::from_resources(vec![resource(0, 1.0)]).expect("one"); let optional = task(0, 60.0, &[0]) .with_priority(100.0) .with_window(0.0, 1_000.0); let mandatory = task(1, 60.0, &[0]) .with_priority(1.0) .with_window(0.0, 100.0) .with_mandatory(true); let tasks = [optional, mandatory]; let planner = Planner::new() .with_mandatory_first(false) .with_improvement(ImprovementSettings::disabled()); let result = planner.schedule(&tasks, &resources); assert!(result.improved.is_empty()); assert_eq!(result.schedule.len(), 1); } #[test] fn the_improvement_pass_preserves_feasibility() { // Whatever the pass decides, the result must remain conflict free. let resources = ResourceSet::from_resources(vec![resource(0, 1.0), resource(1, 2.0)]).expect("distinct"); let tasks: Vec = (0..10) .map(|index| { task(index, 40.0 + f64::from(index as u32) * 5.0, &[index % 2]) .with_priority(1.0 + f64::from(index as u32)) }) .collect(); let result = Planner::new().schedule(&tasks, &resources); // The schedule must not place two tasks beyond the capacity of a // resource at the same time. let report = apsis_conflict::detect(&result.schedule, &tasks, &resources); assert!( report.is_clear(), "the improvement pass broke feasibility: {:?}", report.conflicts() ); } #[test] fn the_improvement_pass_never_loses_a_placement() { // Every task that the greedy pass placed must still be placed // afterwards, because the pass only adds placements or moves them. let resources = ResourceSet::from_resources(vec![resource(0, 1.0)]).expect("one"); let tasks: Vec = (0..6) .map(|index| task(index, 60.0, &[0]).with_priority(1.0 + f64::from(index as u32))) .collect(); let with_pass = Planner::new().schedule(&tasks, &resources); let without_pass = Planner::new() .with_improvement(ImprovementSettings::disabled()) .schedule(&tasks, &resources); assert!( with_pass.schedule.len() >= without_pass.schedule.len(), "{} against {}", with_pass.schedule.len(), without_pass.schedule.len() ); } #[test] fn the_improvement_settings_are_validated_by_use() { let resources = ResourceSet::from_resources(vec![resource(0, 1.0)]).expect("one"); let tasks = [task(0, 60.0, &[0])]; // A bound of zero disables the move search but leaves the pass enabled, // which behaves as if the pass were off. let planner = Planner::new().with_improvement(ImprovementSettings::new(0)); let result = planner.schedule(&tasks, &resources); assert!(result.is_complete()); assert!(result.improved.is_empty()); }