apsis-rs/crates/ops/apsis-schedule/tests/schedule.rs

523 lines
19 KiB
Rust

//! Tests for the schedule representation and its evaluation.
//!
//! 调度表示及其评估的测试。
use apsis_resource::{Resource, ResourceId, ResourceSet, Window};
use apsis_schedule::{Evaluation, Placement, Schedule, ScheduleErrorKind};
use apsis_task::{Task, TaskId};
/// A resource with the given identifier available from 0 to 1 000 seconds.
///
/// 具有给定标识符、自 0 到 1 000 秒可用的资源。
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 the given resources.
///
/// 一个需要各给定资源一单位的任务。
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_new_schedule_is_empty() {
let schedule = Schedule::new();
assert!(schedule.is_empty());
assert_eq!(schedule.len(), 0);
assert!(schedule.placements().is_empty());
assert_eq!(schedule.earliest_start(), None);
assert_eq!(schedule.latest_end(), None);
assert_eq!(schedule.scheduled_seconds(), 0.0);
}
#[test]
fn placing_a_task_records_it() {
let mut schedule = Schedule::new();
let task = task(0, 60.0, &[0]);
schedule.place(&task, 100.0).expect("the task fits");
assert_eq!(schedule.len(), 1);
assert!(schedule.is_placed(TaskId::new(0)));
let placement = schedule.placement_of(TaskId::new(0)).expect("placed");
assert_eq!(placement.task, TaskId::new(0));
assert_eq!(placement.start, 100.0);
assert_eq!(placement.duration, 60.0);
assert_eq!(placement.end(), 160.0);
assert_eq!(schedule.scheduled_seconds(), 60.0);
assert_eq!(schedule.earliest_start(), Some(100.0));
assert_eq!(schedule.latest_end(), Some(160.0));
}
#[test]
fn a_task_can_be_placed_only_once() {
let mut schedule = Schedule::new();
let task = task(0, 60.0, &[0]);
schedule.place(&task, 100.0).expect("the task fits");
let error = schedule
.place(&task, 200.0)
.expect_err("the task is already placed");
assert_eq!(error.kind(), ScheduleErrorKind::AlreadyPlaced);
assert_eq!(schedule.len(), 1);
}
#[test]
fn a_placement_before_the_window_is_rejected() {
let mut schedule = Schedule::new();
let task = task(0, 60.0, &[0]).with_window(100.0, 1_000.0);
let error = schedule
.place(&task, 50.0)
.expect_err("the placement is too early");
assert_eq!(error.kind(), ScheduleErrorKind::BeforeWindow);
assert!(schedule.is_empty());
}
#[test]
fn a_placement_after_the_window_is_rejected() {
let mut schedule = Schedule::new();
let task = task(0, 60.0, &[0]).with_window(0.0, 100.0);
// Starting at fifty would end at one hundred and ten, past the window.
// 自五十开始会在百一十结束,越过窗口。
let error = schedule
.place(&task, 50.0)
.expect_err("the placement is too late");
assert_eq!(error.kind(), ScheduleErrorKind::AfterWindow);
// Starting at forty ends exactly at the window close, which is allowed.
// 自四十开始恰在窗口关闭处结束,这是允许的。
assert!(schedule.place(&task, 40.0).is_ok());
}
#[test]
fn a_non_finite_start_is_rejected() {
let mut schedule = Schedule::new();
let task = task(0, 60.0, &[0]);
for start in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
let error = schedule
.place(&task, start)
.expect_err("the start is not finite");
assert_eq!(
error.kind(),
ScheduleErrorKind::InvalidStart,
"start {start}"
);
}
assert!(schedule.is_empty());
}
#[test]
fn a_task_can_be_unplaced() {
let mut schedule = Schedule::new();
let first = task(0, 60.0, &[0]);
let second = task(1, 30.0, &[0]);
schedule.place(&first, 0.0).expect("fits");
schedule.place(&second, 100.0).expect("fits");
assert_eq!(schedule.len(), 2);
let removed = schedule.unplace(TaskId::new(0)).expect("placed");
assert_eq!(removed.task, TaskId::new(0));
assert_eq!(schedule.len(), 1);
assert!(!schedule.is_placed(TaskId::new(0)));
// The remaining placement is still found, so the index was rebuilt.
// 剩余的放置仍被找到,故索引被重建。
assert!(schedule.is_placed(TaskId::new(1)));
assert_eq!(
schedule
.placement_of(TaskId::new(1))
.expect("still placed")
.start,
100.0
);
assert_eq!(
schedule.unplace(TaskId::new(0)).expect_err("gone").kind(),
ScheduleErrorKind::NotPlaced
);
}
#[test]
fn the_index_survives_several_removals() {
let mut schedule = Schedule::new();
let tasks: Vec<Task> = (0..5).map(|index| task(index, 10.0, &[index])).collect();
for (index, task) in tasks.iter().enumerate() {
schedule
.place(task, index as f64 * 100.0)
.expect("the task fits");
}
// Remove from the middle, which shifts every later placement.
// 从中间移除,这会移动其后每处放置。
schedule.unplace(TaskId::new(2)).expect("placed");
for index in [0_usize, 1, 3, 4] {
let placement = schedule
.placement_of(TaskId::new(index))
.unwrap_or_else(|| panic!("task {index} should still be placed"));
assert_eq!(placement.start, index as f64 * 100.0);
}
assert_eq!(schedule.len(), 4);
}
#[test]
fn placements_on_a_resource_are_found_in_time_order() {
let resources = ResourceSet::from_resources(vec![resource(0, 1.0), resource(1, 1.0)])
.expect("the identifiers are distinct");
let tasks = vec![
task(0, 60.0, &[0]),
task(1, 30.0, &[1]),
task(2, 20.0, &[0]),
];
let mut schedule = Schedule::new();
// Placed out of time order.
// 按非时间次序放置。
schedule.place(&tasks[2], 200.0).expect("fits");
schedule.place(&tasks[0], 0.0).expect("fits");
schedule.place(&tasks[1], 100.0).expect("fits");
let on_zero = schedule.placements_on(ResourceId::new(0), &tasks);
assert_eq!(on_zero.len(), 2);
assert_eq!(on_zero[0].task, TaskId::new(0));
assert_eq!(on_zero[1].task, TaskId::new(2));
let on_one = schedule.placements_on(ResourceId::new(1), &tasks);
assert_eq!(on_one.len(), 1);
assert_eq!(on_one[0].task, TaskId::new(1));
assert!(
schedule
.placements_on(ResourceId::new(9), &tasks)
.is_empty()
);
let _ = resources;
}
#[test]
fn overlapping_placements_are_detected() {
let mut schedule = Schedule::new();
let first = task(0, 60.0, &[0]);
let second = task(1, 60.0, &[1]);
schedule.place(&first, 0.0).expect("fits");
schedule.place(&second, 30.0).expect("fits");
// The two overlap in time, although they use different resources, so the
// schedule reports an overlap and the capacity check allows it.
// 二者在时间上重叠,尽管使用不同资源,故调度报告一次重叠而容量检查允许它。
assert!(schedule.has_overlapping_placements());
}
#[test]
fn back_to_back_placements_do_not_overlap() {
let mut schedule = Schedule::new();
let first = task(0, 60.0, &[0]);
let second = task(1, 60.0, &[0]);
schedule.place(&first, 0.0).expect("fits");
schedule.place(&second, 60.0).expect("fits");
assert!(!schedule.has_overlapping_placements());
}
#[test]
fn the_capacity_check_accepts_an_exclusive_resource_used_once() {
let resources = ResourceSet::from_resources(vec![resource(0, 1.0)]).expect("one");
let tasks = vec![task(0, 60.0, &[0]), task(1, 60.0, &[0])];
let mut schedule = Schedule::new();
schedule.place(&tasks[0], 0.0).expect("fits");
schedule.place(&tasks[1], 60.0).expect("fits");
assert!(schedule.validate_capacity(&tasks, &resources).is_ok());
}
#[test]
fn the_capacity_check_rejects_an_exclusive_resource_used_twice() {
let resources = ResourceSet::from_resources(vec![resource(0, 1.0)]).expect("one");
let tasks = vec![task(0, 60.0, &[0]), task(1, 60.0, &[0])];
let mut schedule = Schedule::new();
schedule.place(&tasks[0], 0.0).expect("fits");
schedule.place(&tasks[1], 30.0).expect("fits");
let error = schedule
.validate_capacity(&tasks, &resources)
.expect_err("the resource is overbooked");
assert_eq!(error.kind(), ScheduleErrorKind::CapacityExceeded);
}
#[test]
fn a_shared_resource_allows_two_tasks_within_the_capacity() {
// A resource of capacity two holds two unit tasks at once.
// 容量为二的资源同时容纳两个单位任务。
let resources = ResourceSet::from_resources(vec![resource(0, 2.0)]).expect("one");
let tasks = vec![task(0, 60.0, &[0]), task(1, 60.0, &[0])];
let mut schedule = Schedule::new();
schedule.place(&tasks[0], 0.0).expect("fits");
schedule.place(&tasks[1], 30.0).expect("fits");
assert!(schedule.validate_capacity(&tasks, &resources).is_ok());
// A third would exceed it.
// 第三个会超过它。
let third = task(2, 60.0, &[0]);
schedule.place(&third, 45.0).expect("fits");
let mut all = tasks.clone();
all.push(third);
assert_eq!(
schedule
.validate_capacity(&all, &resources)
.expect_err("three exceeds two")
.kind(),
ScheduleErrorKind::CapacityExceeded
);
}
#[test]
fn fractional_demands_share_a_resource() {
// Four tasks of a quarter unit each fill a resource of capacity one.
// 四个各四分之一单位的任务填满容量为一的资源。
let resources = ResourceSet::from_resources(vec![resource(0, 1.0)]).expect("one");
let tasks: Vec<Task> = (0..4)
.map(|index| {
Task::new(TaskId::new(index), 60.0)
.try_require(ResourceId::new(0), 0.25)
.expect("the resource is new")
})
.collect();
let mut schedule = Schedule::new();
for task in &tasks {
schedule.place(task, 0.0).expect("fits");
}
assert!(schedule.validate_capacity(&tasks, &resources).is_ok());
let fifth = Task::new(TaskId::new(9), 60.0)
.try_require(ResourceId::new(0), 0.25)
.expect("the resource is new");
schedule.place(&fifth, 0.0).expect("fits");
let mut all = tasks.clone();
all.push(fifth);
assert_eq!(
schedule
.validate_capacity(&all, &resources)
.expect_err("five quarters exceeds one")
.kind(),
ScheduleErrorKind::CapacityExceeded
);
}
#[test]
fn the_capacity_check_rejects_a_placement_without_its_task() {
let resources = ResourceSet::from_resources(vec![resource(0, 1.0)]).expect("one");
let tasks = [task(0, 60.0, &[0])];
let mut schedule = Schedule::new();
schedule.place(&tasks[0], 0.0).expect("fits");
// The task list does not contain the placed task.
// 任务列表不含所放置的任务。
assert_eq!(
schedule
.validate_capacity(&[], &resources)
.expect_err("the task is missing")
.kind(),
ScheduleErrorKind::RequirementMismatch
);
}
#[test]
fn the_capacity_check_rejects_a_duration_mismatch() {
let resources = ResourceSet::from_resources(vec![resource(0, 1.0)]).expect("one");
let original = task(0, 60.0, &[0]);
let mut schedule = Schedule::new();
schedule.place(&original, 0.0).expect("fits");
// A task with the same identifier but a different duration.
// 一个标识符相同但时长不同的任务。
let different = task(0, 90.0, &[0]);
assert_eq!(
schedule
.validate_capacity(&[different], &resources)
.expect_err("the durations differ")
.kind(),
ScheduleErrorKind::RequirementMismatch
);
}
#[test]
fn the_evaluation_reports_the_counts_and_the_makespan() {
let resources = ResourceSet::from_resources(vec![resource(0, 1.0)]).expect("one");
let tasks = vec![task(0, 60.0, &[0]), task(1, 30.0, &[0])];
let mut schedule = Schedule::new();
schedule.place(&tasks[0], 0.0).expect("fits");
schedule.place(&tasks[1], 100.0).expect("fits");
let evaluation = schedule.evaluate(&tasks, &resources);
assert_eq!(evaluation.placed_count, 2);
assert_eq!(evaluation.offered_count, 2);
assert_eq!(evaluation.scheduled_seconds, 90.0);
assert_eq!(evaluation.makespan_start, 0.0);
assert_eq!(evaluation.makespan_end, 130.0);
assert_eq!(evaluation.makespan(), 130.0);
assert_eq!(evaluation.fill_rate(), Some(1.0));
assert!(evaluation.is_complete());
}
#[test]
fn the_evaluation_reports_resource_usage() {
let resources =
ResourceSet::from_resources(vec![resource(0, 2.0), resource(1, 1.0)]).expect("distinct");
let tasks = vec![
task(0, 60.0, &[0]),
task(1, 60.0, &[0]),
task(2, 30.0, &[1]),
];
let mut schedule = Schedule::new();
// The two on resource zero overlap, so the peak demand is two.
// 资源零上的二者重叠,故峰值需求为二。
schedule.place(&tasks[0], 0.0).expect("fits");
schedule.place(&tasks[1], 30.0).expect("fits");
schedule.place(&tasks[2], 0.0).expect("fits");
let evaluation = schedule.evaluate(&tasks, &resources);
let zero = evaluation
.usage_of(ResourceId::new(0))
.expect("the resource is used");
assert_eq!(zero.peak_demand, 2.0);
// The two overlap for thirty seconds, so the busy time is ninety.
// 二者重叠三十秒,故繁忙时间为九十。
assert_eq!(zero.busy_seconds, 90.0);
assert_eq!(zero.placement_count, 2);
assert_eq!(zero.utilisation(180.0), Some(0.5));
let one = evaluation
.usage_of(ResourceId::new(1))
.expect("the resource is used");
assert_eq!(one.peak_demand, 1.0);
assert_eq!(one.busy_seconds, 30.0);
assert_eq!(one.placement_count, 1);
assert!(evaluation.usage_of(ResourceId::new(9)).is_none());
}
#[test]
fn the_evaluation_of_an_empty_schedule_is_zero() {
let resources = ResourceSet::from_resources(vec![resource(0, 1.0)]).expect("one");
let schedule = Schedule::new();
let evaluation = schedule.evaluate(&[], &resources);
assert_eq!(evaluation.placed_count, 0);
assert_eq!(evaluation.offered_count, 0);
assert_eq!(evaluation.makespan(), 0.0);
// With nothing offered and nothing placed, the schedule is vacuously
// complete, but there is no fill rate to report.
// 没有提供、也没有放置任何东西时,调度空洞地完整,但没有可报告的填充率。
assert_eq!(evaluation.fill_rate(), None);
assert!(evaluation.is_complete());
let zero = evaluation.usage_of(ResourceId::new(0)).expect("present");
assert_eq!(zero.peak_demand, 0.0);
assert_eq!(zero.busy_seconds, 0.0);
assert_eq!(zero.utilisation(0.0), None);
}
#[test]
fn the_fill_rate_reports_unplaced_tasks() {
let resources = ResourceSet::from_resources(vec![resource(0, 1.0)]).expect("one");
let tasks = vec![task(0, 60.0, &[0]), task(1, 60.0, &[0])];
let mut schedule = Schedule::new();
schedule.place(&tasks[0], 0.0).expect("fits");
let evaluation = schedule.evaluate(&tasks, &resources);
assert_eq!(evaluation.placed_count, 1);
assert_eq!(evaluation.offered_count, 2);
assert_eq!(evaluation.fill_rate(), Some(0.5));
assert!(!evaluation.is_complete());
}
#[test]
fn the_time_order_differs_from_the_insertion_order() {
let mut schedule = Schedule::new();
let tasks = [task(0, 60.0, &[0]), task(1, 60.0, &[0])];
schedule.place(&tasks[0], 200.0).expect("fits");
schedule.place(&tasks[1], 0.0).expect("fits");
// The insertion order is 0 then 1.
// 插入次序是 0 然后 1。
assert_eq!(schedule.placements()[0].task, TaskId::new(0));
// The time order is 1 then 0.
// 时间次序是 1 然后 0。
let ordered = schedule.in_time_order();
assert_eq!(ordered[0].task, TaskId::new(1));
assert_eq!(ordered[1].task, TaskId::new(0));
// The original is unchanged.
// 原件不变。
assert_eq!(schedule.placements()[0].task, TaskId::new(0));
}
#[test]
fn the_resource_busy_until_reports_the_next_free_time() {
let tasks = vec![task(0, 60.0, &[0]), task(1, 60.0, &[1])];
let mut schedule = Schedule::new();
schedule.place(&tasks[0], 0.0).expect("fits");
schedule.place(&tasks[1], 30.0).expect("fits");
assert_eq!(
schedule.resource_busy_until(ResourceId::new(0), &tasks, 30.0),
Some(60.0)
);
// Resource zero is idle at a hundred seconds.
// 资源零在百秒处空闲。
assert_eq!(
schedule.resource_busy_until(ResourceId::new(0), &tasks, 100.0),
None
);
// Resource one is busy at thirty seconds and free at ninety.
// 资源一在三十秒处繁忙、在九十秒处空闲。
assert_eq!(
schedule.resource_busy_until(ResourceId::new(1), &tasks, 50.0),
Some(90.0)
);
}
#[test]
fn a_placement_reports_what_it_covers() {
let placement = Placement::new(TaskId::new(0), 100.0, 60.0);
assert_eq!(placement.end(), 160.0);
assert!(placement.covers(100.0));
assert!(placement.covers(159.999));
// The end is not covered, so back to back placements do not overlap.
// 终点不被覆盖,故前后相接的放置不重叠。
assert!(!placement.covers(160.0));
assert!(!placement.covers(99.999));
assert!(placement.is_valid());
}
#[test]
fn placements_overlap_only_when_they_share_time() {
let first = Placement::new(TaskId::new(0), 0.0, 60.0);
let second = Placement::new(TaskId::new(1), 60.0, 60.0);
assert!(!first.overlaps(&second));
let overlapping = Placement::new(TaskId::new(2), 30.0, 60.0);
assert!(first.overlaps(&overlapping));
assert!(overlapping.overlaps(&first));
}
#[test]
fn an_invalid_placement_is_reported() {
assert!(!Placement::new(TaskId::new(0), f64::NAN, 60.0).is_valid());
assert!(!Placement::new(TaskId::new(0), 0.0, -1.0).is_valid());
// A zero duration placement is valid, for an instantaneous task.
// 零时长放置有效,用于瞬时任务。
assert!(Placement::new(TaskId::new(0), 0.0, 0.0).is_valid());
}
#[test]
fn the_evaluation_type_is_nameable() {
// The type must be usable by a caller that stores the results, and an
// empty schedule against one resource must report itself as empty.
// 该类型必须能被存储结果的调用方使用,且针对一个资源的空调度必须报告自身为空。
fn takes(_evaluation: &Evaluation) {}
let resources = ResourceSet::from_resources(vec![resource(0, 1.0)]).expect("one");
let schedule = Schedule::new();
let evaluation = schedule.evaluate(&[], &resources);
takes(&evaluation);
assert_eq!(evaluation.placed_count, 0);
assert_eq!(evaluation.offered_count, 0);
assert_eq!(evaluation.scheduled_seconds, 0.0);
assert_eq!(evaluation.resource_usage.len(), 1);
}