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

393 lines
12 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! Tests for tasks and the set that holds them.
//!
//! 任务及保存它们的集合的测试。
use apsis_resource::ResourceId;
use apsis_task::{Requirement, Task, TaskErrorKind, TaskId, TaskSet, TaskState, task_order};
/// A sixty second task with no window and no requirements.
///
/// 一个六十秒、无窗口也无需求的任务。
fn simple() -> Task {
Task::new(TaskId::new(0), 60.0)
}
/// A task requiring two resources for sixty seconds, in a window from 100 to
/// 1 000 seconds.
///
/// 一个在 100 至 1 000 秒窗口内、需要两个资源六十秒的任务。
fn observation() -> Task {
simple()
.with_window(100.0, 1_000.0)
.with_priority(5.0)
.try_require(ResourceId::new(0), 1.0)
.expect("the resource is new")
.try_require(ResourceId::new(1), 1.0)
.expect("the resource is new")
}
#[test]
fn a_new_task_has_the_given_duration_and_nothing_else() {
let task = simple();
assert_eq!(task.duration, 60.0);
assert_eq!(task.priority, 0.0);
assert!(!task.mandatory);
assert!(task.requirements.is_empty());
assert_eq!(task.requirement_count(), 0);
// A task with no window is unbounded at both ends.
// 无窗口的任务在两端都无界。
assert!(task.earliest_start.is_infinite() && task.earliest_start < 0.0);
assert!(task.latest_end.is_infinite() && task.latest_end > 0.0);
assert!(!task.window_is_too_short());
}
#[test]
fn a_window_restricts_the_task() {
let task = observation();
assert_eq!(task.earliest_start, 100.0);
assert_eq!(task.latest_end, 1_000.0);
assert_eq!(task.window_length(), 900.0);
assert!(task.fits_window());
assert_eq!(task.earliest_possible_end(), 160.0);
}
#[test]
fn a_window_shorter_than_the_task_is_detected() {
let task = simple().with_window(0.0, 30.0);
assert!(task.window_is_too_short());
assert!(!task.fits_window());
assert_eq!(
task.validate().expect_err("the window is too short").kind(),
TaskErrorKind::WindowTooShort
);
assert!(!task.is_valid());
}
#[test]
fn a_window_exactly_the_length_of_the_task_is_accepted() {
// The task must fit entirely inside the window, and a window of exactly
// the duration holds it.
// 任务必须完全落在窗口内,而恰好等于时长的窗口能容纳它。
let task = simple().with_window(0.0, 60.0);
assert!(!task.window_is_too_short());
assert!(task.is_valid());
}
#[test]
fn requiring_a_resource_records_the_demand() {
let task = observation();
assert_eq!(task.requirement_count(), 2);
assert_eq!(task.demand_for(ResourceId::new(0)), Some(1.0));
assert_eq!(task.demand_for(ResourceId::new(1)), Some(1.0));
assert_eq!(task.demand_for(ResourceId::new(9)), None);
assert!(task.requires(ResourceId::new(0)));
assert!(!task.requires(ResourceId::new(9)));
assert_eq!(
task.resources(),
vec![ResourceId::new(0), ResourceId::new(1)]
);
}
#[test]
fn a_duplicate_requirement_is_reported_immediately() {
let task = simple()
.try_require(ResourceId::new(0), 1.0)
.expect("the resource is new");
let error = task
.try_require(ResourceId::new(0), 2.0)
.expect_err("the resource is already required");
assert_eq!(error.kind(), TaskErrorKind::DuplicateRequirement);
}
#[test]
fn a_duplicate_requirement_built_by_chaining_is_rejected_on_validation() {
// The plain builder cannot report an error, so the duplicate survives
// until validation.
// 普通构建器无法报告错误,故重复项存留到校验时。
let task = simple()
.require(ResourceId::new(0), 1.0)
.require(ResourceId::new(0), 1.0);
assert!(task.has_duplicate_requirements());
assert_eq!(
task.validate().expect_err("duplicate").kind(),
TaskErrorKind::DuplicateRequirement
);
}
#[test]
fn an_invalid_demand_is_rejected() {
for demand in [0.0, -1.0, f64::NAN, f64::INFINITY] {
let error = simple()
.try_require(ResourceId::new(0), demand)
.expect_err("the demand is not usable");
assert_eq!(
error.kind(),
TaskErrorKind::InvalidDemand,
"demand {demand}"
);
}
}
#[test]
fn an_invalid_duration_is_rejected() {
for duration in [0.0, -1.0, f64::NAN, f64::INFINITY] {
let task = Task::new(TaskId::new(0), duration);
assert!(!task.is_valid(), "duration {duration}");
assert_eq!(
task.validate().expect_err("invalid duration").kind(),
TaskErrorKind::InvalidDuration
);
}
}
#[test]
fn a_reversed_window_is_rejected() {
let task = simple().with_window(100.0, 50.0);
assert_eq!(
task.validate().expect_err("reversed").kind(),
TaskErrorKind::InvalidWindow
);
}
#[test]
fn a_non_finite_window_is_rejected() {
assert_eq!(
simple()
.with_window(f64::NAN, 100.0)
.validate()
.expect_err("nan")
.kind(),
TaskErrorKind::InvalidWindow
);
assert_eq!(
simple()
.with_window(0.0, f64::NAN)
.validate()
.expect_err("nan")
.kind(),
TaskErrorKind::InvalidWindow
);
}
#[test]
fn a_non_finite_priority_is_rejected() {
let task = simple().with_priority(f64::NAN);
assert_eq!(
task.validate().expect_err("nan").kind(),
TaskErrorKind::InvalidWeight
);
}
#[test]
fn the_weighted_value_combines_priority_and_duration() {
let task = simple().with_priority(5.0);
assert_eq!(task.weighted_value(), 300.0);
// A task with no priority is worth nothing.
// 无优先级的任务不值任何东西。
assert_eq!(simple().weighted_value(), 0.0);
}
#[test]
fn the_mandatory_flag_is_recorded() {
assert!(!simple().mandatory);
assert!(simple().with_mandatory(true).mandatory);
}
#[test]
fn the_builder_does_not_modify_the_original() {
// A builder chain returns a new value, so a base task can be shared.
// 构建器链返回新值,故基础任务可被共享。
let base = simple();
let first = base.clone().with_priority(1.0);
let second = base.clone().with_priority(2.0);
assert_eq!(base.priority, 0.0);
assert_eq!(first.priority, 1.0);
assert_eq!(second.priority, 2.0);
}
#[test]
fn a_task_displays_its_shape() {
let text = observation().to_string();
assert!(text.contains("task 0"), "{text}");
assert!(text.contains("60 s"), "{text}");
assert!(text.contains("2 resource"), "{text}");
}
#[test]
fn task_states_report_their_placement() {
let pending = TaskState::Pending;
assert!(!pending.is_scheduled());
assert_eq!(pending.start(), None);
assert_eq!(pending.name(), "pending");
let scheduled = TaskState::Scheduled { start: 100.0 };
assert!(scheduled.is_scheduled());
assert_eq!(scheduled.start(), Some(100.0));
assert_eq!(scheduled.end(60.0), Some(160.0));
assert_eq!(scheduled.name(), "scheduled");
let deferred = TaskState::Deferred;
assert!(!deferred.is_scheduled());
assert_eq!(deferred.name(), "deferred");
let infeasible = TaskState::Infeasible {
reason: "window too short".to_string(),
};
assert!(!infeasible.is_scheduled());
assert_eq!(infeasible.name(), "infeasible");
}
#[test]
fn a_set_holds_tasks_by_identifier() {
let mut set = TaskSet::new();
assert!(set.is_empty());
set.insert(simple()).expect("the identifier is new");
set.insert(Task::new(TaskId::new(1), 30.0))
.expect("the identifier is new");
assert_eq!(set.len(), 2);
assert_eq!(set.total_duration(), 90.0);
assert!(set.get(TaskId::new(0)).is_some());
assert!(set.get(TaskId::new(9)).is_none());
}
#[test]
fn a_duplicate_task_identifier_is_rejected() {
let mut set = TaskSet::new();
set.insert(simple()).expect("the identifier is new");
let error = set
.insert(Task::new(TaskId::new(0), 30.0))
.expect_err("the identifier is taken");
assert_eq!(error.kind(), TaskErrorKind::DuplicateTask);
assert_eq!(set.len(), 1);
}
#[test]
fn an_invalid_task_is_rejected_by_the_set() {
let mut set = TaskSet::new();
let error = set
.insert(Task::new(TaskId::new(0), -1.0))
.expect_err("the duration is invalid");
assert_eq!(error.kind(), TaskErrorKind::InvalidDuration);
assert!(set.is_empty());
}
#[test]
fn a_set_can_be_built_from_a_list() {
let set = TaskSet::from_tasks(vec![simple(), Task::new(TaskId::new(1), 30.0)])
.expect("the tasks are usable");
assert_eq!(set.len(), 2);
assert_eq!(
TaskSet::from_tasks(vec![simple(), simple()])
.expect_err("the identifiers collide")
.kind(),
TaskErrorKind::DuplicateTask
);
}
#[test]
fn a_task_can_be_removed() {
let mut set = TaskSet::new();
set.insert(simple()).expect("new");
let removed = set.remove(TaskId::new(0)).expect("the task exists");
assert_eq!(removed.id, TaskId::new(0));
assert!(set.is_empty());
assert_eq!(
set.remove(TaskId::new(0)).expect_err("gone").kind(),
TaskErrorKind::NoSuchTask
);
}
#[test]
fn the_set_reports_queries() {
let set = TaskSet::from_tasks(vec![
observation(),
Task::new(TaskId::new(1), 30.0).with_mandatory(true),
Task::new(TaskId::new(2), 10.0)
.with_priority(2.0)
.try_require(ResourceId::new(0), 0.5)
.expect("the resource is new"),
])
.expect("the tasks are usable");
assert_eq!(
set.tasks_requiring(ResourceId::new(0)),
vec![TaskId::new(0), TaskId::new(2)]
);
assert_eq!(
set.tasks_requiring(ResourceId::new(9)),
Vec::<TaskId>::new()
);
assert_eq!(set.mandatory(), vec![TaskId::new(1)]);
// The weighted value is the priority times the duration.
// 加权值是优先级乘以时长。
assert_eq!(set.total_weighted_value(), 300.0 + 0.0 + 20.0);
}
#[test]
fn the_task_order_places_the_most_valuable_first() {
let set = TaskSet::from_tasks(vec![
Task::new(TaskId::new(0), 10.0).with_priority(1.0),
Task::new(TaskId::new(1), 100.0).with_priority(5.0),
Task::new(TaskId::new(2), 50.0).with_priority(2.0),
])
.expect("the tasks are usable");
let order = task_order(set.tasks());
// The weighted values are 10, 500, and 100, so the order is 1, 2, 0.
// 加权值为 10、500 与 100,故次序是 1、2、0。
assert_eq!(order, vec![TaskId::new(1), TaskId::new(2), TaskId::new(0)]);
}
#[test]
fn the_task_order_breaks_ties_by_deadline_then_identifier() {
let set = TaskSet::from_tasks(vec![
// The same value and no deadline.
// 价值相同且无截止时间。
Task::new(TaskId::new(0), 10.0).with_priority(1.0),
Task::new(TaskId::new(1), 10.0).with_priority(1.0),
// The same value but an earlier deadline.
// 价值相同但截止时间更早。
Task::new(TaskId::new(2), 10.0)
.with_priority(1.0)
.with_window(0.0, 100.0),
Task::new(TaskId::new(3), 10.0).with_priority(1.0),
])
.expect("the tasks are usable");
let order = task_order(set.tasks());
// The task with the deadline comes first, then the others by identifier.
// 有截止时间的任务最先,其余按标识符。
assert_eq!(
order,
vec![
TaskId::new(2),
TaskId::new(0),
TaskId::new(1),
TaskId::new(3)
]
);
}
#[test]
fn the_task_order_of_an_empty_set_is_empty() {
assert!(task_order(&[]).is_empty());
}
#[test]
fn a_requirement_reports_validity() {
assert!(Requirement::new(ResourceId::new(0), 1.0).is_valid());
assert!(!Requirement::new(ResourceId::new(0), 0.0).is_valid());
assert!(!Requirement::new(ResourceId::new(0), f64::NAN).is_valid());
}
#[test]
fn task_identifiers_are_ordered() {
let mut ids = [TaskId::new(2), TaskId::new(0), TaskId::new(1)];
ids.sort();
assert_eq!(ids, [TaskId::new(0), TaskId::new(1), TaskId::new(2)]);
assert_eq!(TaskId::new(3).to_string(), "task 3");
assert_eq!(TaskId::new(3).index(), 3);
}