//! Tests for the greedy planner. use apsis_resource::{Resource, ResourceId, ResourceSet, Window}; use apsis_schedule::Schedule; use apsis_scheduler::{ Planner, demand_at, earliest_feasible_start, feasible_at, infeasible_tasks, resources_free_at, resources_have_capacity, }; use apsis_task::{Task, TaskId, TaskState}; /// 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_single_task_is_placed_at_the_opening_of_its_window() { let resources = ResourceSet::from_resources(vec![resource(0, 1.0)]).expect("one"); let tasks = [task(0, 60.0, &[0])]; let result = Planner::new().schedule(&tasks, &resources); assert_eq!(result.schedule.len(), 1); assert!(result.is_complete()); let placement = result .schedule .placement_of(TaskId::new(0)) .expect("placed"); // The resource is available from zero, so the task starts there. assert_eq!(placement.start, 0.0); } #[test] fn tasks_on_one_exclusive_resource_are_placed_back_to_back() { let resources = ResourceSet::from_resources(vec![resource(0, 1.0)]).expect("one"); // Equal value, so the order falls back to the earlier deadline and then // the identifier. let tasks = [task(0, 60.0, &[0]), task(1, 30.0, &[0])]; let result = Planner::new().schedule(&tasks, &resources); assert!(result.is_complete()); let first = result .schedule .placement_of(TaskId::new(0)) .expect("placed"); let second = result .schedule .placement_of(TaskId::new(1)) .expect("placed"); // The two must not overlap: the second starts at or after the end of the // first. assert!(second.start >= first.end(), "{second:?} against {first:?}"); assert!(!result.schedule.has_overlapping_placements()); } #[test] fn a_task_that_does_not_fit_is_deferred() { // The resource is available for only sixty seconds, so a task of ninety // cannot be placed. let mut limited = Resource::new(ResourceId::new(0), 1.0); limited .set_windows(&[Window::new(0.0, 60.0)]) .expect("the window is usable"); let resources = ResourceSet::from_resources(vec![limited]).expect("one"); let tasks = [task(0, 90.0, &[0])]; let result = Planner::new().schedule(&tasks, &resources); assert!(!result.is_complete()); assert_eq!(result.deferred(), vec![TaskId::new(0)]); assert_eq!( result.outcome_of(TaskId::new(0)).expect("reported").state, TaskState::Deferred ); assert!(result.schedule.is_empty()); } #[test] fn a_task_demanding_more_than_the_capacity_is_infeasible() { let resources = ResourceSet::from_resources(vec![resource(0, 1.0)]).expect("one"); let tasks = [Task::new(TaskId::new(0), 60.0) .try_require(ResourceId::new(0), 2.0) .expect("the resource is new")]; let result = Planner::new().schedule(&tasks, &resources); assert_eq!(result.infeasible(), vec![TaskId::new(0)]); assert!(result.schedule.is_empty()); assert!(!result.is_complete()); assert!( result .outcome_of(TaskId::new(0)) .expect("reported") .is_infeasible() ); } #[test] fn a_task_requiring_a_missing_resource_is_infeasible() { let resources = ResourceSet::from_resources(vec![resource(0, 1.0)]).expect("one"); let tasks = [task(0, 60.0, &[9])]; let result = Planner::new().schedule(&tasks, &resources); assert_eq!(result.infeasible(), vec![TaskId::new(0)]); assert!(result.schedule.is_empty()); } #[test] fn a_task_with_a_window_too_short_is_infeasible() { let resources = ResourceSet::from_resources(vec![resource(0, 1.0)]).expect("one"); let tasks = [task(0, 90.0, &[0]).with_window(0.0, 60.0)]; let result = Planner::new().schedule(&tasks, &resources); assert_eq!(result.infeasible(), vec![TaskId::new(0)]); } #[test] fn a_mandatory_task_is_placed_before_a_more_valuable_optional_one() { let resources = ResourceSet::from_resources(vec![resource(0, 1.0)]).expect("one"); // The optional task is worth far more, but it can use whatever time is // left, while the mandatory task has only one window. 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 result = Planner::new().schedule(&tasks, &resources); assert!(result.honours_mandatory(&tasks)); let mandatory_placement = result .schedule .placement_of(TaskId::new(1)) .expect("the mandatory task is placed"); assert_eq!(mandatory_placement.start, 0.0); // With the rule turned off, and the improvement pass disabled so that // only the ordering is under test, the optional task goes first and the // mandatory one is placed after it, using the rest of its window. let without = Planner::new() .with_mandatory_first(false) .with_improvement(apsis_scheduler::ImprovementSettings::disabled()) .schedule(&tasks, &resources); let optional_placement = without .schedule .placement_of(TaskId::new(0)) .expect("the optional task is placed"); assert_eq!(optional_placement.start, 0.0); } #[test] fn the_improvement_pass_recovers_a_plan_that_the_greedy_order_missed() { // The optional task is worth more, so it is placed first and occupies // the resource from zero to sixty. The mandatory task has a window from // zero to a hundred, so it cannot fit after the optional one and would // be lost. The improvement pass moves the optional task aside and places // both. 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 greedy = Planner::new() .with_mandatory_first(false) .with_improvement(apsis_scheduler::ImprovementSettings::disabled()) .schedule(&tasks, &resources); // Without the pass the mandatory task is lost. assert!(!greedy.honours_mandatory(&tasks)); assert_eq!(greedy.schedule.len(), 1); let improved = Planner::new() .with_mandatory_first(false) .schedule(&tasks, &resources); // With the pass both are placed. assert!(improved.is_complete(), "{:?}", improved.outcomes); assert!(improved.honours_mandatory(&tasks)); assert!(!improved.improved.is_empty()); } #[test] fn the_value_ordering_places_the_most_valuable_task_first() { let resources = ResourceSet::from_resources(vec![resource(0, 1.0)]).expect("one"); let tasks = [ task(0, 10.0, &[0]).with_priority(1.0), task(1, 100.0, &[0]).with_priority(5.0), task(2, 50.0, &[0]).with_priority(2.0), ]; let result = Planner::new().schedule(&tasks, &resources); assert!(result.is_complete()); // The weighted values are 10, 500, and 100, so task 1 goes first. assert_eq!( result .schedule .placement_of(TaskId::new(1)) .expect("placed") .start, 0.0 ); } #[test] fn two_tasks_on_a_shared_resource_can_overlap() { let resources = ResourceSet::from_resources(vec![resource(0, 2.0)]).expect("one"); let tasks = [task(0, 60.0, &[0]), task(1, 60.0, &[0])]; let result = Planner::new().schedule(&tasks, &resources); assert!(result.is_complete()); let first = result .schedule .placement_of(TaskId::new(0)) .expect("placed"); let second = result .schedule .placement_of(TaskId::new(1)) .expect("placed"); // Both start as early as possible, because the capacity holds them both. assert_eq!(first.start, 0.0); assert_eq!(second.start, 0.0); assert!(result.schedule.has_overlapping_placements()); } #[test] fn a_task_is_placed_after_the_other_frees_its_resource() { let resources = ResourceSet::from_resources(vec![resource(0, 1.0)]).expect("one"); // The first task occupies the resource from 100 to 200, so the second, // which can start no earlier than 100, must wait. let first = task(0, 100.0, &[0]).with_window(100.0, 1_000.0); let second = task(1, 50.0, &[0]).with_window(100.0, 1_000.0); let tasks = [first, second]; let result = Planner::new().schedule(&tasks, &resources); assert!(result.is_complete()); let first_placement = result .schedule .placement_of(TaskId::new(0)) .expect("placed"); let second_placement = result .schedule .placement_of(TaskId::new(1)) .expect("placed"); assert!(second_placement.start >= first_placement.end()); } #[test] fn the_earliest_start_is_the_false_of_the_window_when_nothing_blocks() { let resources = ResourceSet::from_resources(vec![resource(0, 1.0)]).expect("one"); let schedule = Schedule::new(); let tasks = [task(0, 60.0, &[0]).with_window(300.0, 1_000.0)]; let start = earliest_feasible_start(&tasks[0], &tasks, &schedule, &resources) .expect("a placement exists"); assert_eq!(start, 300.0); } #[test] fn the_earliest_start_is_none_when_the_window_is_too_short() { let resources = ResourceSet::from_resources(vec![resource(0, 1.0)]).expect("one"); let schedule = Schedule::new(); let tasks = [task(0, 90.0, &[0]).with_window(0.0, 60.0)]; assert_eq!( earliest_feasible_start(&tasks[0], &tasks, &schedule, &resources), None ); } #[test] fn the_earliest_start_accounts_for_a_blocking_placement() { let resources = ResourceSet::from_resources(vec![resource(0, 1.0)]).expect("one"); let blocker = task(0, 100.0, &[0]); let mut schedule = Schedule::new(); schedule.place(&blocker, 0.0).expect("fits"); let candidate = task(1, 50.0, &[0]); let tasks = [blocker, candidate.clone()]; let start = earliest_feasible_start(&candidate, &tasks, &schedule, &resources) .expect("a placement exists"); // The resource is held until a hundred, so the earliest start is there. assert_eq!(start, 100.0); } #[test] fn feasibility_reports_the_capacity_limit() { let resources = ResourceSet::from_resources(vec![resource(0, 1.0)]).expect("one"); let existing = task(0, 100.0, &[0]); let mut schedule = Schedule::new(); schedule.place(&existing, 0.0).expect("fits"); let candidate = task(1, 50.0, &[0]); let tasks = [existing, candidate.clone()]; // Starting at fifty would overlap the existing placement, which the // capacity of one does not allow. assert!(!feasible_at( &candidate, &tasks, &schedule, &resources, 50.0 )); // Starting at a hundred is free. assert!(feasible_at( &candidate, &tasks, &schedule, &resources, 100.0 )); } #[test] fn feasibility_reports_the_availability_limit() { let mut limited = Resource::new(ResourceId::new(0), 1.0); limited .set_windows(&[Window::new(0.0, 100.0)]) .expect("the window is usable"); let resources = ResourceSet::from_resources(vec![limited]).expect("one"); let schedule = Schedule::new(); let tasks = [task(0, 60.0, &[0])]; assert!(feasible_at(&tasks[0], &tasks, &schedule, &resources, 0.0)); assert!(feasible_at(&tasks[0], &tasks, &schedule, &resources, 40.0)); // Starting at fifty would end at one hundred and ten, past the window. assert!(!feasible_at(&tasks[0], &tasks, &schedule, &resources, 50.0)); } #[test] fn the_capacity_check_covers_a_multiple_resource_task() { let resources = ResourceSet::from_resources(vec![resource(0, 1.0), resource(1, 1.0)]).expect("distinct"); // The task needs both resources at once. let tasks = [task(0, 60.0, &[0, 1])]; assert!(resources_have_capacity(&tasks[0], &resources)); // The same task with one resource missing is infeasible. let missing = [task(0, 60.0, &[0, 9])]; assert!(!resources_have_capacity(&missing[0], &resources)); assert_eq!(infeasible_tasks(&missing, &resources), vec![TaskId::new(0)]); } #[test] fn the_demand_at_an_instant_is_reported_per_resource() { let resources = ResourceSet::from_resources(vec![resource(0, 2.0), resource(1, 1.0)]).expect("distinct"); let tasks = [task(0, 100.0, &[0]), task(1, 100.0, &[1])]; let mut schedule = Schedule::new(); schedule.place(&tasks[0], 0.0).expect("fits"); schedule.place(&tasks[1], 50.0).expect("fits"); let demand = demand_at(&schedule, &tasks, &resources, 75.0); assert_eq!(demand[0], (ResourceId::new(0), 1.0)); assert_eq!(demand[1], (ResourceId::new(1), 1.0)); // Before either placement the demand is zero. let demand = demand_at(&schedule, &tasks, &resources, -1.0); assert_eq!(demand[0].1, 0.0); } #[test] fn the_free_resources_at_an_instant_exclude_the_busy_ones() { let resources = ResourceSet::from_resources(vec![resource(0, 1.0), resource(1, 1.0)]).expect("distinct"); let tasks = [task(0, 100.0, &[0])]; let mut schedule = Schedule::new(); schedule.place(&tasks[0], 0.0).expect("fits"); // At fifty seconds resource zero is full and resource one is free. assert_eq!( resources_free_at(&schedule, &tasks, &resources, 50.0), vec![ResourceId::new(1)] ); // At a hundred and fifty both are free. assert_eq!( resources_free_at(&schedule, &tasks, &resources, 150.0), vec![ResourceId::new(0), ResourceId::new(1)] ); } #[test] fn the_planner_order_puts_mandatory_tasks_first() { let tasks = [ task(0, 10.0, &[0]).with_priority(10.0), task(1, 10.0, &[0]).with_priority(1.0).with_mandatory(true), ]; let order = Planner::new().order(&tasks); assert_eq!(order[0], TaskId::new(1)); let without = Planner::new().with_mandatory_first(false).order(&tasks); assert_eq!(without[0], TaskId::new(0)); } #[test] fn the_result_reports_every_task() { let resources = ResourceSet::from_resources(vec![resource(0, 1.0)]).expect("one"); let tasks = [ task(0, 60.0, &[0]), task(1, 90.0, &[0]).with_window(0.0, 60.0), Task::new(TaskId::new(2), 60.0) .try_require(ResourceId::new(0), 5.0) .expect("the resource is new"), ]; let result = Planner::new().schedule(&tasks, &resources); assert_eq!(result.outcomes.len(), 3); for task in &tasks { assert!(result.outcome_of(task.id).is_some()); } // Only the first can be placed. assert_eq!(result.scheduled(), vec![TaskId::new(0)]); assert!(!result.is_complete()); assert!(result.honours_mandatory(&tasks)); } #[test] fn a_mandatory_task_that_cannot_be_placed_is_reported() { let resources = ResourceSet::from_resources(vec![resource(0, 1.0)]).expect("one"); let tasks = [task(0, 90.0, &[0]) .with_window(0.0, 60.0) .with_mandatory(true)]; let result = Planner::new().schedule(&tasks, &resources); // The plan does not honour the mandatory task, which the caller can // detect. assert!(!result.honours_mandatory(&tasks)); assert!(!result.is_complete()); } #[test] fn planning_an_empty_task_list_gives_an_empty_schedule() { let resources = ResourceSet::from_resources(vec![resource(0, 1.0)]).expect("one"); let result = Planner::new().schedule(&[], &resources); assert!(result.schedule.is_empty()); assert!(result.outcomes.is_empty()); assert!(result.is_complete()); assert!(result.honours_mandatory(&[])); } #[test] fn the_planner_default_places_mandatory_tasks_first() { assert_eq!(Planner::default(), Planner::new()); assert!(Planner::new().mandatory_first); } #[test] fn the_infeasible_task_list_excludes_the_merely_deferred() { let resources = ResourceSet::from_resources(vec![resource(0, 1.0)]).expect("one"); let tasks = [ // Can be placed. task(0, 60.0, &[0]), // Demands too much. Task::new(TaskId::new(1), 60.0) .try_require(ResourceId::new(0), 5.0) .expect("the resource is new"), // Names a resource that does not exist. task(2, 60.0, &[9]), ]; let infeasible = infeasible_tasks(&tasks, &resources); assert_eq!(infeasible, vec![TaskId::new(1), TaskId::new(2)]); }