apsis-rs/crates/ops/apsis-scheduler/tests/planner.rs

432 lines
15 KiB
Rust

//! 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, 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)
.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_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)]);
}