docs: bilingual comments for the ops tests

This commit is contained in:
ApsisRS contributors 2026-10-02 02:00:32 +08:00
parent 4ffe215860
commit 2ba298e213
9 changed files with 206 additions and 0 deletions

View File

@ -1,10 +1,14 @@
//! Tests for conflict detection. //! Tests for conflict detection.
//!
//! 冲突检测的测试。
use apsis_conflict::{Conflict, ConflictKind, Precedence, Schedule, detect, detect_precedence}; use apsis_conflict::{Conflict, ConflictKind, Precedence, Schedule, detect, detect_precedence};
use apsis_resource::{Resource, ResourceId, ResourceSet, Window}; use apsis_resource::{Resource, ResourceId, ResourceSet, Window};
use apsis_task::{Task, TaskId}; use apsis_task::{Task, TaskId};
/// A resource available from 0 to 1 000 seconds. /// A resource available from 0 to 1 000 seconds.
///
/// 一个自 0 到 1 000 秒可用的资源。
fn resource(id: usize, capacity: f64) -> Resource { fn resource(id: usize, capacity: f64) -> Resource {
let mut resource = Resource::new(ResourceId::new(id), capacity); let mut resource = Resource::new(ResourceId::new(id), capacity);
resource resource
@ -14,6 +18,8 @@ fn resource(id: usize, capacity: f64) -> Resource {
} }
/// A task requiring one unit of each named resource. /// A task requiring one unit of each named resource.
///
/// 一个需要每个指名资源一单位的任务。
fn task(id: usize, duration: f64, resources: &[usize]) -> Task { fn task(id: usize, duration: f64, resources: &[usize]) -> Task {
let mut task = Task::new(TaskId::new(id), duration); let mut task = Task::new(TaskId::new(id), duration);
for resource in resources { for resource in resources {
@ -25,6 +31,8 @@ fn task(id: usize, duration: f64, resources: &[usize]) -> Task {
} }
/// The standard resource set used by the tests. /// The standard resource set used by the tests.
///
/// 测试所用的标准资源集。
fn resources() -> ResourceSet { fn resources() -> ResourceSet {
ResourceSet::from_resources(vec![resource(0, 1.0), resource(1, 1.0)]) ResourceSet::from_resources(vec![resource(0, 1.0), resource(1, 1.0)])
.expect("the identifiers are distinct") .expect("the identifiers are distinct")
@ -63,8 +71,10 @@ fn a_capacity_conflict_is_found() {
let conflict = report.of_kind(ConflictKind::Capacity)[0]; let conflict = report.of_kind(ConflictKind::Capacity)[0];
assert_eq!(conflict.resource, Some(ResourceId::new(0))); assert_eq!(conflict.resource, Some(ResourceId::new(0)));
// The conflict names both tasks, so a repair pass knows the pair. // The conflict names both tasks, so a repair pass knows the pair.
// 该冲突指名两个任务,故修复遍可知道这一对。
assert!(conflict.other.is_some()); assert!(conflict.other.is_some());
// The instant reported is the start of the second placement. // The instant reported is the start of the second placement.
// 所报告的瞬时是第二处放置的开始。
assert_eq!(conflict.at, 30.0); assert_eq!(conflict.at, 30.0);
assert!(conflict.is_movable()); assert!(conflict.is_movable());
} }
@ -86,8 +96,10 @@ fn a_window_conflict_is_found_at_the_start() {
let mut schedule = Schedule::new(); let mut schedule = Schedule::new();
// The schedule refuses a placement before the window, so the conflict is // The schedule refuses a placement before the window, so the conflict is
// constructed directly to test the detector. // constructed directly to test the detector.
// 调度拒绝窗口之前的放置,故直接构建该冲突以测试检测器。
schedule.place(&tasks[0], 100.0).expect("fits"); schedule.place(&tasks[0], 100.0).expect("fits");
// A task list whose window has moved since the schedule was built. // A task list whose window has moved since the schedule was built.
// 一个自调度构建以来窗口已移动的任务列表。
let moved = vec![task(0, 60.0, &[0]).with_window(200.0, 1_000.0)]; let moved = vec![task(0, 60.0, &[0]).with_window(200.0, 1_000.0)];
let report = detect(&schedule, &moved, &resources); let report = detect(&schedule, &moved, &resources);
@ -104,12 +116,14 @@ fn a_window_conflict_is_found_at_the_end() {
let mut schedule = Schedule::new(); let mut schedule = Schedule::new();
schedule.place(&tasks[0], 100.0).expect("fits"); schedule.place(&tasks[0], 100.0).expect("fits");
// A task list whose window closes earlier than the placement ends. // A task list whose window closes earlier than the placement ends.
// 一个窗口比放置结束更早关闭的任务列表。
let shortened = vec![task(0, 60.0, &[0]).with_window(0.0, 120.0)]; let shortened = vec![task(0, 60.0, &[0]).with_window(0.0, 120.0)];
let report = detect(&schedule, &shortened, &resources); let report = detect(&schedule, &shortened, &resources);
assert_eq!(report.count_of(ConflictKind::Window), 1); assert_eq!(report.count_of(ConflictKind::Window), 1);
let conflict = report.of_kind(ConflictKind::Window)[0]; let conflict = report.of_kind(ConflictKind::Window)[0];
// The instant reported is the end of the placement. // The instant reported is the end of the placement.
// 所报告的瞬时是该放置的结束。
assert_eq!(conflict.at, 160.0); assert_eq!(conflict.at, 160.0);
} }
@ -121,6 +135,7 @@ fn a_requirement_conflict_is_found_for_a_missing_task() {
schedule.place(&tasks[0], 0.0).expect("fits"); schedule.place(&tasks[0], 0.0).expect("fits");
// The task list is empty, so the placement has no task. // The task list is empty, so the placement has no task.
// 任务列表为空,故该放置没有任务。
let report = detect(&schedule, &[], &resources); let report = detect(&schedule, &[], &resources);
assert_eq!(report.count_of(ConflictKind::Requirement), 1); assert_eq!(report.count_of(ConflictKind::Requirement), 1);
assert_eq!(report.placed_tasks(), 1); assert_eq!(report.placed_tasks(), 1);
@ -156,6 +171,7 @@ fn a_requirement_conflict_is_found_for_a_missing_resource() {
fn a_requirement_conflict_is_found_for_an_impossible_demand() { fn a_requirement_conflict_is_found_for_an_impossible_demand() {
let resources = resources(); let resources = resources();
// A demand of two on a resource of capacity one can never be met. // A demand of two on a resource of capacity one can never be met.
// 容量为一的资源上需求为二永远无法满足。
let tasks = vec![ let tasks = vec![
Task::new(TaskId::new(0), 60.0) Task::new(TaskId::new(0), 60.0)
.try_require(ResourceId::new(0), 2.0) .try_require(ResourceId::new(0), 2.0)
@ -173,6 +189,7 @@ fn a_requirement_conflict_is_found_for_an_impossible_demand() {
#[test] #[test]
fn an_availability_conflict_is_found_outside_the_windows() { fn an_availability_conflict_is_found_outside_the_windows() {
// The resource is available only from 200 to 300 seconds. // The resource is available only from 200 to 300 seconds.
// 该资源只在 200 至 300 秒可用。
let mut limited = Resource::new(ResourceId::new(0), 1.0); let mut limited = Resource::new(ResourceId::new(0), 1.0);
limited limited
.set_windows(&[Window::new(200.0, 300.0)]) .set_windows(&[Window::new(200.0, 300.0)])
@ -194,6 +211,7 @@ fn an_availability_conflict_is_found_outside_the_windows() {
fn an_availability_conflict_is_found_for_a_placement_that_overruns_a_window() { fn an_availability_conflict_is_found_for_a_placement_that_overruns_a_window() {
// The resource is available only from 0 to 120 seconds, so a placement // The resource is available only from 0 to 120 seconds, so a placement
// from 100 for 60 seconds overruns it. // from 100 for 60 seconds overruns it.
// 该资源只在 0 至 120 秒可用,故自 100 起持续 60 秒的放置会超出它。
let mut limited = Resource::new(ResourceId::new(0), 1.0); let mut limited = Resource::new(ResourceId::new(0), 1.0);
limited limited
.set_windows(&[Window::new(0.0, 120.0)]) .set_windows(&[Window::new(0.0, 120.0)])
@ -221,6 +239,7 @@ fn a_placement_inside_the_availability_has_no_conflict() {
#[test] #[test]
fn several_conflicts_are_reported_together() { fn several_conflicts_are_reported_together() {
// The detector reports every problem, not just the first. // The detector reports every problem, not just the first.
// 检测器报告每个问题,而不只是第一个。
let resources = resources(); let resources = resources();
let tasks = [task(0, 60.0, &[0]), task(1, 60.0, &[0])]; let tasks = [task(0, 60.0, &[0]), task(1, 60.0, &[0])];
let mut schedule = Schedule::new(); let mut schedule = Schedule::new();
@ -228,6 +247,7 @@ fn several_conflicts_are_reported_together() {
schedule.place(&tasks[1], 30.0).expect("fits"); schedule.place(&tasks[1], 30.0).expect("fits");
// A task list that also moves the window of the first task. // A task list that also moves the window of the first task.
// 一个还移动了第一个任务窗口的任务列表。
let changed = vec![ let changed = vec![
task(0, 60.0, &[0]).with_window(100.0, 1_000.0), task(0, 60.0, &[0]).with_window(100.0, 1_000.0),
task(1, 60.0, &[0]), task(1, 60.0, &[0]),
@ -245,6 +265,8 @@ fn the_report_is_ordered_by_time() {
// changed so that both placements fall before the new window opens. The // changed so that both placements fall before the new window opens. The
// schedule refuses such a placement, which is why the changed list is // schedule refuses such a placement, which is why the changed list is
// used for the check. // used for the check.
// 各放置在一个宽窗口内作出,然后任务列表被改变,使两处放置都落在新窗口打开之前。
// 调度拒绝这样的放置,故检查使用改变后的列表。
let placed = [task(0, 60.0, &[0]), task(1, 60.0, &[1])]; let placed = [task(0, 60.0, &[0]), task(1, 60.0, &[1])];
let mut schedule = Schedule::new(); let mut schedule = Schedule::new();
schedule.place(&placed[0], 100.0).expect("fits"); schedule.place(&placed[0], 100.0).expect("fits");
@ -273,6 +295,7 @@ fn the_report_lists_the_conflicting_tasks_once_each() {
let report = detect(&schedule, &tasks, &resources); let report = detect(&schedule, &tasks, &resources);
let tasks_involved = report.conflicting_tasks(); let tasks_involved = report.conflicting_tasks();
// Both tasks are involved, and each appears once. // Both tasks are involved, and each appears once.
// 两个任务都涉及,且各自出现一次。
assert!(tasks_involved.contains(&TaskId::new(0))); assert!(tasks_involved.contains(&TaskId::new(0)));
assert!(tasks_involved.contains(&TaskId::new(1))); assert!(tasks_involved.contains(&TaskId::new(1)));
assert_eq!(tasks_involved.len(), 2); assert_eq!(tasks_involved.len(), 2);
@ -301,6 +324,7 @@ fn a_precedence_violation_is_found() {
let mut schedule = Schedule::new(); let mut schedule = Schedule::new();
schedule.place(&tasks[0], 100.0).expect("fits"); schedule.place(&tasks[0], 100.0).expect("fits");
// The second task must follow the first, but starts inside it. // The second task must follow the first, but starts inside it.
// 第二个任务必须跟随第一个,但起始于其中。
schedule.place(&tasks[1], 120.0).expect("fits"); schedule.place(&tasks[1], 120.0).expect("fits");
let conflicts = detect_precedence( let conflicts = detect_precedence(
@ -322,6 +346,7 @@ fn a_satisfied_precedence_has_no_conflict() {
schedule.place(&tasks[0], 100.0).expect("fits"); schedule.place(&tasks[0], 100.0).expect("fits");
// Starting exactly at the end of the first is allowed, because the // Starting exactly at the end of the first is allowed, because the
// placement interval is half-open. // placement interval is half-open.
// 恰在第一个结束处开始是允许的,因为放置区间是半开的。
schedule.place(&tasks[1], 160.0).expect("fits"); schedule.place(&tasks[1], 160.0).expect("fits");
assert!( assert!(
detect_precedence( detect_precedence(
@ -338,6 +363,7 @@ fn a_precedence_over_an_unplaced_task_is_ignored() {
let mut schedule = Schedule::new(); let mut schedule = Schedule::new();
schedule.place(&tasks[0], 100.0).expect("fits"); schedule.place(&tasks[0], 100.0).expect("fits");
// The second task is not placed, so nothing has been promised. // The second task is not placed, so nothing has been promised.
// 第二个任务未被放置,故没有承诺任何东西。
assert!( assert!(
detect_precedence( detect_precedence(
&schedule, &schedule,

View File

@ -1,8 +1,12 @@
//! Tests for resources and the set that holds them. //! Tests for resources and the set that holds them.
//!
//! 资源及保存它们的集合的测试。
use apsis_resource::{Interval, Resource, ResourceErrorKind, ResourceId, ResourceSet, Window}; use apsis_resource::{Interval, Resource, ResourceErrorKind, ResourceId, ResourceSet, Window};
/// A resource with one window from 100 to 200 seconds. /// A resource with one window from 100 to 200 seconds.
///
/// 一个含 100 至 200 秒窗口的资源。
fn station() -> Resource { fn station() -> Resource {
let mut resource = Resource::new(ResourceId::new(0), 1.0); let mut resource = Resource::new(ResourceId::new(0), 1.0);
resource resource
@ -39,6 +43,7 @@ fn availability_is_half_open_at_the_edges() {
assert!(resource.is_available_at(199.999)); assert!(resource.is_available_at(199.999));
// The end instant belongs to the next interval, so the resource stops // The end instant belongs to the next interval, so the resource stops
// being available exactly there. // being available exactly there.
// 结束瞬时属于下一区间,故资源恰在那里停止可用。
assert!(!resource.is_available_at(200.0)); assert!(!resource.is_available_at(200.0));
} }
@ -49,6 +54,7 @@ fn availability_over_a_duration_requires_the_whole_span() {
assert!(resource.is_available_over(120.0, 80.0)); assert!(resource.is_available_over(120.0, 80.0));
assert!(resource.is_available_over(150.0, 50.0)); assert!(resource.is_available_over(150.0, 50.0));
// A request that reaches past the window fails, even by a little. // A request that reaches past the window fails, even by a little.
// 越过窗口的请求失败,即便只越过一点。
assert!(!resource.is_available_over(150.0, 50.001)); assert!(!resource.is_available_over(150.0, 50.001));
assert!(!resource.is_available_over(99.0, 2.0)); assert!(!resource.is_available_over(99.0, 2.0));
} }
@ -59,6 +65,7 @@ fn a_zero_duration_request_is_an_instantaneous_query() {
assert!(resource.is_available_over(150.0, 0.0)); assert!(resource.is_available_over(150.0, 0.0));
assert!(!resource.is_available_over(250.0, 0.0)); assert!(!resource.is_available_over(250.0, 0.0));
// The half-open boundary applies here too. // The half-open boundary applies here too.
// 半开边界在此也适用。
assert!(!resource.is_available_over(200.0, 0.0)); assert!(!resource.is_available_over(200.0, 0.0));
} }
@ -77,6 +84,7 @@ fn adding_overlapping_windows_merges_them() {
resource.add_window(Window::new(0.0, 10.0)).expect("usable"); resource.add_window(Window::new(0.0, 10.0)).expect("usable");
resource.add_window(Window::new(5.0, 15.0)).expect("usable"); resource.add_window(Window::new(5.0, 15.0)).expect("usable");
// The two overlap, so the stored form is one window. // The two overlap, so the stored form is one window.
// 二者重叠,故存储形式是一个窗口。
assert_eq!(resource.window_count(), 1); assert_eq!(resource.window_count(), 1);
assert_eq!(resource.windows()[0], Window::new(0.0, 15.0)); assert_eq!(resource.windows()[0], Window::new(0.0, 15.0));
assert_eq!(resource.total_availability(), 15.0); assert_eq!(resource.total_availability(), 15.0);
@ -91,6 +99,7 @@ fn adding_disjoint_windows_keeps_them_separate() {
resource.add_window(Window::new(0.0, 10.0)).expect("usable"); resource.add_window(Window::new(0.0, 10.0)).expect("usable");
assert_eq!(resource.window_count(), 2); assert_eq!(resource.window_count(), 2);
// The stored order is time order, not insertion order. // The stored order is time order, not insertion order.
// 存储次序是时间次序,而非插入次序。
assert_eq!(resource.windows()[0].start(), 0.0); assert_eq!(resource.windows()[0].start(), 0.0);
assert_eq!(resource.windows()[1].start(), 30.0); assert_eq!(resource.windows()[1].start(), 30.0);
} }
@ -120,6 +129,7 @@ fn setting_the_windows_replaces_them() {
assert!(resource.is_available_at(5.0)); assert!(resource.is_available_at(5.0));
// Setting nothing removes all availability. // Setting nothing removes all availability.
// 什么都不设置即移除所有可用性。
resource.set_windows(&[]).expect("usable"); resource.set_windows(&[]).expect("usable");
assert!(resource.is_empty()); assert!(resource.is_empty());
} }
@ -145,12 +155,16 @@ fn the_earliest_fit_is_the_first_window_long_enough() {
.expect("usable"); .expect("usable");
// A request of five seconds fits in the first window. // A request of five seconds fits in the first window.
// 五秒的请求放入第一个窗口。
assert_eq!(resource.earliest_fit(5.0), Some(Window::new(0.0, 5.0))); assert_eq!(resource.earliest_fit(5.0), Some(Window::new(0.0, 5.0)));
// A request of twenty does not, so it takes the second. // A request of twenty does not, so it takes the second.
// 二十秒的放不下,故取第二个。
assert_eq!(resource.earliest_fit(20.0), Some(Window::new(30.0, 50.0))); assert_eq!(resource.earliest_fit(20.0), Some(Window::new(30.0, 50.0)));
// A request longer than any window has no answer. // A request longer than any window has no answer.
// 比任何窗口都长的请求没有答案。
assert_eq!(resource.earliest_fit(40.0), None); assert_eq!(resource.earliest_fit(40.0), None);
// An instantaneous request fits at the start of the first window. // An instantaneous request fits at the start of the first window.
// 瞬时请求放入第一个窗口的开头。
assert_eq!(resource.earliest_fit(0.0), Some(Window::new(0.0, 0.0))); assert_eq!(resource.earliest_fit(0.0), Some(Window::new(0.0, 0.0)));
assert_eq!(resource.earliest_fit(-1.0), None); assert_eq!(resource.earliest_fit(-1.0), None);
assert_eq!(resource.earliest_fit(f64::NAN), None); assert_eq!(resource.earliest_fit(f64::NAN), None);
@ -164,17 +178,20 @@ fn the_earliest_fit_after_a_time_respects_the_lower_bound() {
.expect("usable"); .expect("usable");
// Starting no earlier than five, the first window can still hold it. // Starting no earlier than five, the first window can still hold it.
// 不早于五开始,第一个窗口仍能容纳它。
assert_eq!( assert_eq!(
resource.earliest_fit_after(5.0, 4.0), resource.earliest_fit_after(5.0, 4.0),
Some(Window::new(5.0, 9.0)) Some(Window::new(5.0, 9.0))
); );
// A request of eight seconds starting at five does not fit, because the // A request of eight seconds starting at five does not fit, because the
// window ends at ten, so the second window is used. // window ends at ten, so the second window is used.
// 自五开始、八秒的请求放不下,因为窗口在十结束,故使用第二个窗口。
assert_eq!( assert_eq!(
resource.earliest_fit_after(5.0, 8.0), resource.earliest_fit_after(5.0, 8.0),
Some(Window::new(30.0, 38.0)) Some(Window::new(30.0, 38.0))
); );
// Starting after the second window begins, the request starts there. // Starting after the second window begins, the request starts there.
// 在第二个窗口开始之后起步,该请求从那里开始。
assert_eq!( assert_eq!(
resource.earliest_fit_after(40.0, 10.0), resource.earliest_fit_after(40.0, 10.0),
Some(Window::new(40.0, 50.0)) Some(Window::new(40.0, 50.0))
@ -204,6 +221,7 @@ fn the_capacity_is_validated() {
assert_eq!(error.kind(), ResourceErrorKind::InvalidCapacity); assert_eq!(error.kind(), ResourceErrorKind::InvalidCapacity);
} }
// The capacity is unchanged after a failed set. // The capacity is unchanged after a failed set.
// 失败的设置之后容量不变。
assert_eq!(resource.capacity(), 2.5); assert_eq!(resource.capacity(), 2.5);
let invalid = Resource::new(ResourceId::new(0), -1.0); let invalid = Resource::new(ResourceId::new(0), -1.0);
@ -286,6 +304,7 @@ fn the_set_reports_availability() {
.expect("new"); .expect("new");
// Only the first resource has any availability. // Only the first resource has any availability.
// 只有第一个资源有任何可用性。
assert_eq!(set.available_at(150.0), vec![ResourceId::new(0)]); assert_eq!(set.available_at(150.0), vec![ResourceId::new(0)]);
assert!(set.available_at(50.0).is_empty()); assert!(set.available_at(50.0).is_empty());
assert_eq!(set.available_over(120.0, 50.0), vec![ResourceId::new(0)]); assert_eq!(set.available_over(120.0, 50.0), vec![ResourceId::new(0)]);
@ -303,6 +322,7 @@ fn the_identifier_displays_its_index() {
#[test] #[test]
fn identifiers_are_ordered() { fn identifiers_are_ordered() {
// Ordering lets a caller sort a list of resources deterministically. // Ordering lets a caller sort a list of resources deterministically.
// 排序使调用方能确定地对资源列表排序。
let mut ids = [ResourceId::new(3), ResourceId::new(1), ResourceId::new(2)]; let mut ids = [ResourceId::new(3), ResourceId::new(1), ResourceId::new(2)];
ids.sort(); ids.sort();
assert_eq!( assert_eq!(

View File

@ -1,4 +1,6 @@
//! Tests for availability windows and the interval algebra. //! Tests for availability windows and the interval algebra.
//!
//! 可用性窗口以及区间代数的测试。
use apsis_resource::{Interval, ResourceErrorKind, Window, merge_windows, subtract_window}; use apsis_resource::{Interval, ResourceErrorKind, Window, merge_windows, subtract_window};
@ -6,6 +8,7 @@ use apsis_resource::{Interval, ResourceErrorKind, Window, merge_windows, subtrac
fn an_interval_reports_its_duration() { fn an_interval_reports_its_duration() {
assert_eq!(Interval::new(10.0, 30.0).duration(), 20.0); assert_eq!(Interval::new(10.0, 30.0).duration(), 20.0);
// An empty or reversed interval has no duration. // An empty or reversed interval has no duration.
// 空或反向的区间没有时长。
assert_eq!(Interval::new(30.0, 30.0).duration(), 0.0); assert_eq!(Interval::new(30.0, 30.0).duration(), 0.0);
assert_eq!(Interval::new(30.0, 10.0).duration(), 0.0); assert_eq!(Interval::new(30.0, 10.0).duration(), 0.0);
} }
@ -17,6 +20,7 @@ fn emptiness_and_validity_are_reported() {
assert!(!Interval::new(4.0, 5.0).is_empty()); assert!(!Interval::new(4.0, 5.0).is_empty());
assert!(Interval::new(4.0, 5.0).is_valid()); assert!(Interval::new(4.0, 5.0).is_valid());
// A reversed interval is not valid, although it is empty. // A reversed interval is not valid, although it is empty.
// 反向区间无效,尽管它是空的。
assert!(!Interval::new(5.0, 4.0).is_valid()); assert!(!Interval::new(5.0, 4.0).is_valid());
assert!(!Interval::new(f64::NAN, 5.0).is_valid()); assert!(!Interval::new(f64::NAN, 5.0).is_valid());
assert!(!Interval::new(4.0, f64::INFINITY).is_valid()); assert!(!Interval::new(4.0, f64::INFINITY).is_valid());
@ -26,6 +30,7 @@ fn emptiness_and_validity_are_reported() {
fn containment_is_half_open() { fn containment_is_half_open() {
let interval = Interval::new(10.0, 20.0); let interval = Interval::new(10.0, 20.0);
// The start is inside and the end is not. // The start is inside and the end is not.
// 起点在内而终点不在。
assert!(!interval.contains(9.999)); assert!(!interval.contains(9.999));
assert!(interval.contains(10.0)); assert!(interval.contains(10.0));
assert!(interval.contains(15.0)); assert!(interval.contains(15.0));
@ -39,10 +44,12 @@ fn containing_an_interval_includes_the_boundaries() {
assert!(outer.contains_interval(&Interval::new(10.0, 20.0))); assert!(outer.contains_interval(&Interval::new(10.0, 20.0)));
// The boundaries are included, so an interval that shares an end is // The boundaries are included, so an interval that shares an end is
// contained. // contained.
// 各边界被包含,故共享一个端点的区间被包含。
assert!(outer.contains_interval(&Interval::new(0.0, 100.0))); assert!(outer.contains_interval(&Interval::new(0.0, 100.0)));
assert!(outer.contains_interval(&Interval::new(0.0, 50.0))); assert!(outer.contains_interval(&Interval::new(0.0, 50.0)));
assert!(outer.contains_interval(&Interval::new(50.0, 100.0))); assert!(outer.contains_interval(&Interval::new(50.0, 100.0)));
// An interval that reaches past the end is not. // An interval that reaches past the end is not.
// 越过终点的区间则不是。
assert!(!outer.contains_interval(&Interval::new(50.0, 100.1))); assert!(!outer.contains_interval(&Interval::new(50.0, 100.1)));
assert!(!outer.contains_interval(&Interval::new(-0.1, 50.0))); assert!(!outer.contains_interval(&Interval::new(-0.1, 50.0)));
} }
@ -58,6 +65,7 @@ fn an_empty_interval_is_contained_when_its_instant_is() {
fn intervals_that_meet_do_not_overlap() { fn intervals_that_meet_do_not_overlap() {
// The half-open convention means the boundary instant belongs to the // The half-open convention means the boundary instant belongs to the
// second interval only. // second interval only.
// 半开约定意味着边界瞬时只属于第二个区间。
let first = Interval::new(0.0, 10.0); let first = Interval::new(0.0, 10.0);
let second = Interval::new(10.0, 20.0); let second = Interval::new(10.0, 20.0);
assert!(!first.overlaps(&second)); assert!(!first.overlaps(&second));
@ -73,6 +81,7 @@ fn overlapping_intervals_report_their_intersection() {
assert_eq!(first.intersection(&second), Some(Interval::new(5.0, 10.0))); assert_eq!(first.intersection(&second), Some(Interval::new(5.0, 10.0)));
assert_eq!(second.intersection(&first), first.intersection(&second)); assert_eq!(second.intersection(&first), first.intersection(&second));
// Disjoint intervals have no intersection. // Disjoint intervals have no intersection.
// 不相交区间没有交集。
assert_eq!( assert_eq!(
Interval::new(0.0, 1.0).intersection(&Interval::new(2.0, 3.0)), Interval::new(0.0, 1.0).intersection(&Interval::new(2.0, 3.0)),
None None
@ -104,6 +113,7 @@ fn merging_combines_overlapping_windows() {
fn merging_combines_windows_that_touch() { fn merging_combines_windows_that_touch() {
// Two windows that meet share a boundary instant, which belongs to the // Two windows that meet share a boundary instant, which belongs to the
// second, so the union is one continuous window. // second, so the union is one continuous window.
// 相接的两个窗口共享一个边界瞬时,它属于第二个,故并集是一个连续窗口。
let merged = merge_windows(&[Window::new(0.0, 10.0), Window::new(10.0, 20.0)]) let merged = merge_windows(&[Window::new(0.0, 10.0), Window::new(10.0, 20.0)])
.expect("the windows are usable"); .expect("the windows are usable");
assert_eq!(merged, vec![Window::new(0.0, 20.0)]); assert_eq!(merged, vec![Window::new(0.0, 20.0)]);
@ -113,6 +123,7 @@ fn merging_combines_windows_that_touch() {
fn merging_drops_empty_windows() { fn merging_drops_empty_windows() {
// A zero length window is valid but contributes no availability, so it // A zero length window is valid but contributes no availability, so it
// is dropped. A reversed window is a caller error and is rejected. // is dropped. A reversed window is a caller error and is rejected.
// 零长窗口有效但不贡献可用性,故被丢弃。反向窗口是调用方错误并被拒绝。
let merged = merge_windows(&[ let merged = merge_windows(&[
Window::new(5.0, 5.0), Window::new(5.0, 5.0),
Window::new(0.0, 10.0), Window::new(0.0, 10.0),
@ -144,6 +155,7 @@ fn merging_an_empty_list_gives_an_empty_list() {
fn merging_is_idempotent() { fn merging_is_idempotent() {
// The canonical form must be a fixed point, so that comparing two sets // The canonical form must be a fixed point, so that comparing two sets
// is a direct comparison. // is a direct comparison.
// 规范形式必须是自身的固定点,使比较两个集合成为直接比较。
let windows = [ let windows = [
Window::new(0.0, 10.0), Window::new(0.0, 10.0),
Window::new(5.0, 15.0), Window::new(5.0, 15.0),
@ -197,6 +209,7 @@ fn subtracting_the_whole_of_a_window_leaves_nothing() {
assert!(result.is_empty()); assert!(result.is_empty());
// A window that covers more than the stored one also removes it. // A window that covers more than the stored one also removes it.
// 覆盖范围超过所存者的窗口也会移除它。
let result = let result =
subtract_window(&[Window::new(0.0, 10.0)], Window::new(-5.0, 15.0)).expect("usable"); subtract_window(&[Window::new(0.0, 10.0)], Window::new(-5.0, 15.0)).expect("usable");
assert!(result.is_empty()); assert!(result.is_empty());
@ -240,6 +253,8 @@ fn subtracting_from_several_windows_affects_each() {
fn subtracting_merges_the_windows_first() { fn subtracting_merges_the_windows_first() {
// The two overlapping windows form one, which the subtraction then // The two overlapping windows form one, which the subtraction then
// splits. Working on the unmerged list would give a different answer. // splits. Working on the unmerged list would give a different answer.
// 两个重叠窗口合为一个,减法随后又把它分开。在未合并的列表上工作会给出不同的
// 答案。
let windows = [Window::new(0.0, 10.0), Window::new(5.0, 15.0)]; let windows = [Window::new(0.0, 10.0), Window::new(5.0, 15.0)];
let result = subtract_window(&windows, Window::new(8.0, 12.0)).expect("usable"); let result = subtract_window(&windows, Window::new(8.0, 12.0)).expect("usable");
assert_eq!(result, vec![Window::new(0.0, 8.0), Window::new(12.0, 15.0)]); assert_eq!(result, vec![Window::new(0.0, 8.0), Window::new(12.0, 15.0)]);

View File

@ -1,10 +1,14 @@
//! Tests for the schedule representation and its evaluation. //! Tests for the schedule representation and its evaluation.
//!
//! 调度表示及其评估的测试。
use apsis_resource::{Resource, ResourceId, ResourceSet, Window}; use apsis_resource::{Resource, ResourceId, ResourceSet, Window};
use apsis_schedule::{Evaluation, Placement, Schedule, ScheduleErrorKind}; use apsis_schedule::{Evaluation, Placement, Schedule, ScheduleErrorKind};
use apsis_task::{Task, TaskId}; use apsis_task::{Task, TaskId};
/// A resource with the given identifier available from 0 to 1 000 seconds. /// A resource with the given identifier available from 0 to 1 000 seconds.
///
/// 具有给定标识符、自 0 到 1 000 秒可用的资源。
fn resource(id: usize, capacity: f64) -> Resource { fn resource(id: usize, capacity: f64) -> Resource {
let mut resource = Resource::new(ResourceId::new(id), capacity); let mut resource = Resource::new(ResourceId::new(id), capacity);
resource resource
@ -14,6 +18,8 @@ fn resource(id: usize, capacity: f64) -> Resource {
} }
/// A task requiring one unit of the given resources. /// A task requiring one unit of the given resources.
///
/// 一个需要各给定资源一单位的任务。
fn task(id: usize, duration: f64, resources: &[usize]) -> Task { fn task(id: usize, duration: f64, resources: &[usize]) -> Task {
let mut task = Task::new(TaskId::new(id), duration); let mut task = Task::new(TaskId::new(id), duration);
for resource in resources { for resource in resources {
@ -81,11 +87,13 @@ fn a_placement_after_the_window_is_rejected() {
let mut schedule = Schedule::new(); let mut schedule = Schedule::new();
let task = task(0, 60.0, &[0]).with_window(0.0, 100.0); 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. // Starting at fifty would end at one hundred and ten, past the window.
// 自五十开始会在百一十结束,越过窗口。
let error = schedule let error = schedule
.place(&task, 50.0) .place(&task, 50.0)
.expect_err("the placement is too late"); .expect_err("the placement is too late");
assert_eq!(error.kind(), ScheduleErrorKind::AfterWindow); assert_eq!(error.kind(), ScheduleErrorKind::AfterWindow);
// Starting at forty ends exactly at the window close, which is allowed. // Starting at forty ends exactly at the window close, which is allowed.
// 自四十开始恰在窗口关闭处结束,这是允许的。
assert!(schedule.place(&task, 40.0).is_ok()); assert!(schedule.place(&task, 40.0).is_ok());
} }
@ -120,6 +128,7 @@ fn a_task_can_be_unplaced() {
assert_eq!(schedule.len(), 1); assert_eq!(schedule.len(), 1);
assert!(!schedule.is_placed(TaskId::new(0))); assert!(!schedule.is_placed(TaskId::new(0)));
// The remaining placement is still found, so the index was rebuilt. // The remaining placement is still found, so the index was rebuilt.
// 剩余的放置仍被找到,故索引被重建。
assert!(schedule.is_placed(TaskId::new(1))); assert!(schedule.is_placed(TaskId::new(1)));
assert_eq!( assert_eq!(
schedule schedule
@ -145,6 +154,7 @@ fn the_index_survives_several_removals() {
.expect("the task fits"); .expect("the task fits");
} }
// Remove from the middle, which shifts every later placement. // Remove from the middle, which shifts every later placement.
// 从中间移除,这会移动其后每处放置。
schedule.unplace(TaskId::new(2)).expect("placed"); schedule.unplace(TaskId::new(2)).expect("placed");
for index in [0_usize, 1, 3, 4] { for index in [0_usize, 1, 3, 4] {
let placement = schedule let placement = schedule
@ -167,6 +177,7 @@ fn placements_on_a_resource_are_found_in_time_order() {
let mut schedule = Schedule::new(); let mut schedule = Schedule::new();
// Placed out of time order. // Placed out of time order.
// 按非时间次序放置。
schedule.place(&tasks[2], 200.0).expect("fits"); schedule.place(&tasks[2], 200.0).expect("fits");
schedule.place(&tasks[0], 0.0).expect("fits"); schedule.place(&tasks[0], 0.0).expect("fits");
schedule.place(&tasks[1], 100.0).expect("fits"); schedule.place(&tasks[1], 100.0).expect("fits");
@ -197,6 +208,7 @@ fn overlapping_placements_are_detected() {
schedule.place(&second, 30.0).expect("fits"); schedule.place(&second, 30.0).expect("fits");
// The two overlap in time, although they use different resources, so the // The two overlap in time, although they use different resources, so the
// schedule reports an overlap and the capacity check allows it. // schedule reports an overlap and the capacity check allows it.
// 二者在时间上重叠,尽管使用不同资源,故调度报告一次重叠而容量检查允许它。
assert!(schedule.has_overlapping_placements()); assert!(schedule.has_overlapping_placements());
} }
@ -236,6 +248,7 @@ fn the_capacity_check_rejects_an_exclusive_resource_used_twice() {
#[test] #[test]
fn a_shared_resource_allows_two_tasks_within_the_capacity() { fn a_shared_resource_allows_two_tasks_within_the_capacity() {
// A resource of capacity two holds two unit tasks at once. // A resource of capacity two holds two unit tasks at once.
// 容量为二的资源同时容纳两个单位任务。
let resources = ResourceSet::from_resources(vec![resource(0, 2.0)]).expect("one"); 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 tasks = vec![task(0, 60.0, &[0]), task(1, 60.0, &[0])];
let mut schedule = Schedule::new(); let mut schedule = Schedule::new();
@ -244,6 +257,7 @@ fn a_shared_resource_allows_two_tasks_within_the_capacity() {
assert!(schedule.validate_capacity(&tasks, &resources).is_ok()); assert!(schedule.validate_capacity(&tasks, &resources).is_ok());
// A third would exceed it. // A third would exceed it.
// 第三个会超过它。
let third = task(2, 60.0, &[0]); let third = task(2, 60.0, &[0]);
schedule.place(&third, 45.0).expect("fits"); schedule.place(&third, 45.0).expect("fits");
let mut all = tasks.clone(); let mut all = tasks.clone();
@ -260,6 +274,7 @@ fn a_shared_resource_allows_two_tasks_within_the_capacity() {
#[test] #[test]
fn fractional_demands_share_a_resource() { fn fractional_demands_share_a_resource() {
// Four tasks of a quarter unit each fill a resource of capacity one. // 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 resources = ResourceSet::from_resources(vec![resource(0, 1.0)]).expect("one");
let tasks: Vec<Task> = (0..4) let tasks: Vec<Task> = (0..4)
.map(|index| { .map(|index| {
@ -296,6 +311,7 @@ fn the_capacity_check_rejects_a_placement_without_its_task() {
let mut schedule = Schedule::new(); let mut schedule = Schedule::new();
schedule.place(&tasks[0], 0.0).expect("fits"); schedule.place(&tasks[0], 0.0).expect("fits");
// The task list does not contain the placed task. // The task list does not contain the placed task.
// 任务列表不含所放置的任务。
assert_eq!( assert_eq!(
schedule schedule
.validate_capacity(&[], &resources) .validate_capacity(&[], &resources)
@ -312,6 +328,7 @@ fn the_capacity_check_rejects_a_duration_mismatch() {
let mut schedule = Schedule::new(); let mut schedule = Schedule::new();
schedule.place(&original, 0.0).expect("fits"); schedule.place(&original, 0.0).expect("fits");
// A task with the same identifier but a different duration. // A task with the same identifier but a different duration.
// 一个标识符相同但时长不同的任务。
let different = task(0, 90.0, &[0]); let different = task(0, 90.0, &[0]);
assert_eq!( assert_eq!(
schedule schedule
@ -352,6 +369,7 @@ fn the_evaluation_reports_resource_usage() {
]; ];
let mut schedule = Schedule::new(); let mut schedule = Schedule::new();
// The two on resource zero overlap, so the peak demand is two. // The two on resource zero overlap, so the peak demand is two.
// 资源零上的二者重叠,故峰值需求为二。
schedule.place(&tasks[0], 0.0).expect("fits"); schedule.place(&tasks[0], 0.0).expect("fits");
schedule.place(&tasks[1], 30.0).expect("fits"); schedule.place(&tasks[1], 30.0).expect("fits");
schedule.place(&tasks[2], 0.0).expect("fits"); schedule.place(&tasks[2], 0.0).expect("fits");
@ -362,6 +380,7 @@ fn the_evaluation_reports_resource_usage() {
.expect("the resource is used"); .expect("the resource is used");
assert_eq!(zero.peak_demand, 2.0); assert_eq!(zero.peak_demand, 2.0);
// The two overlap for thirty seconds, so the busy time is ninety. // The two overlap for thirty seconds, so the busy time is ninety.
// 二者重叠三十秒,故繁忙时间为九十。
assert_eq!(zero.busy_seconds, 90.0); assert_eq!(zero.busy_seconds, 90.0);
assert_eq!(zero.placement_count, 2); assert_eq!(zero.placement_count, 2);
assert_eq!(zero.utilisation(180.0), Some(0.5)); assert_eq!(zero.utilisation(180.0), Some(0.5));
@ -386,6 +405,7 @@ fn the_evaluation_of_an_empty_schedule_is_zero() {
assert_eq!(evaluation.makespan(), 0.0); assert_eq!(evaluation.makespan(), 0.0);
// With nothing offered and nothing placed, the schedule is vacuously // With nothing offered and nothing placed, the schedule is vacuously
// complete, but there is no fill rate to report. // complete, but there is no fill rate to report.
// 没有提供、也没有放置任何东西时,调度空洞地完整,但没有可报告的填充率。
assert_eq!(evaluation.fill_rate(), None); assert_eq!(evaluation.fill_rate(), None);
assert!(evaluation.is_complete()); assert!(evaluation.is_complete());
let zero = evaluation.usage_of(ResourceId::new(0)).expect("present"); let zero = evaluation.usage_of(ResourceId::new(0)).expect("present");
@ -416,12 +436,15 @@ fn the_time_order_differs_from_the_insertion_order() {
schedule.place(&tasks[1], 0.0).expect("fits"); schedule.place(&tasks[1], 0.0).expect("fits");
// The insertion order is 0 then 1. // The insertion order is 0 then 1.
// 插入次序是 0 然后 1。
assert_eq!(schedule.placements()[0].task, TaskId::new(0)); assert_eq!(schedule.placements()[0].task, TaskId::new(0));
// The time order is 1 then 0. // The time order is 1 then 0.
// 时间次序是 1 然后 0。
let ordered = schedule.in_time_order(); let ordered = schedule.in_time_order();
assert_eq!(ordered[0].task, TaskId::new(1)); assert_eq!(ordered[0].task, TaskId::new(1));
assert_eq!(ordered[1].task, TaskId::new(0)); assert_eq!(ordered[1].task, TaskId::new(0));
// The original is unchanged. // The original is unchanged.
// 原件不变。
assert_eq!(schedule.placements()[0].task, TaskId::new(0)); assert_eq!(schedule.placements()[0].task, TaskId::new(0));
} }
@ -437,11 +460,13 @@ fn the_resource_busy_until_reports_the_next_free_time() {
Some(60.0) Some(60.0)
); );
// Resource zero is idle at a hundred seconds. // Resource zero is idle at a hundred seconds.
// 资源零在百秒处空闲。
assert_eq!( assert_eq!(
schedule.resource_busy_until(ResourceId::new(0), &tasks, 100.0), schedule.resource_busy_until(ResourceId::new(0), &tasks, 100.0),
None None
); );
// Resource one is busy at thirty seconds and free at ninety. // Resource one is busy at thirty seconds and free at ninety.
// 资源一在三十秒处繁忙、在九十秒处空闲。
assert_eq!( assert_eq!(
schedule.resource_busy_until(ResourceId::new(1), &tasks, 50.0), schedule.resource_busy_until(ResourceId::new(1), &tasks, 50.0),
Some(90.0) Some(90.0)
@ -455,6 +480,7 @@ fn a_placement_reports_what_it_covers() {
assert!(placement.covers(100.0)); assert!(placement.covers(100.0));
assert!(placement.covers(159.999)); assert!(placement.covers(159.999));
// The end is not covered, so back to back placements do not overlap. // The end is not covered, so back to back placements do not overlap.
// 终点不被覆盖,故前后相接的放置不重叠。
assert!(!placement.covers(160.0)); assert!(!placement.covers(160.0));
assert!(!placement.covers(99.999)); assert!(!placement.covers(99.999));
assert!(placement.is_valid()); assert!(placement.is_valid());
@ -475,6 +501,7 @@ fn an_invalid_placement_is_reported() {
assert!(!Placement::new(TaskId::new(0), f64::NAN, 60.0).is_valid()); assert!(!Placement::new(TaskId::new(0), f64::NAN, 60.0).is_valid());
assert!(!Placement::new(TaskId::new(0), 0.0, -1.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. // A zero duration placement is valid, for an instantaneous task.
// 零时长放置有效,用于瞬时任务。
assert!(Placement::new(TaskId::new(0), 0.0, 0.0).is_valid()); assert!(Placement::new(TaskId::new(0), 0.0, 0.0).is_valid());
} }
@ -482,6 +509,7 @@ fn an_invalid_placement_is_reported() {
fn the_evaluation_type_is_nameable() { fn the_evaluation_type_is_nameable() {
// The type must be usable by a caller that stores the results, and an // The type must be usable by a caller that stores the results, and an
// empty schedule against one resource must report itself as empty. // empty schedule against one resource must report itself as empty.
// 该类型必须能被存储结果的调用方使用,且针对一个资源的空调度必须报告自身为空。
fn takes(_evaluation: &Evaluation) {} fn takes(_evaluation: &Evaluation) {}
let resources = ResourceSet::from_resources(vec![resource(0, 1.0)]).expect("one"); let resources = ResourceSet::from_resources(vec![resource(0, 1.0)]).expect("one");
let schedule = Schedule::new(); let schedule = Schedule::new();

View File

@ -1,4 +1,6 @@
//! Tests for the greedy planner. //! Tests for the greedy planner.
//!
//! 贪心规划器的测试。
use apsis_resource::{Resource, ResourceId, ResourceSet, Window}; use apsis_resource::{Resource, ResourceId, ResourceSet, Window};
use apsis_schedule::Schedule; use apsis_schedule::Schedule;
@ -9,6 +11,8 @@ use apsis_scheduler::{
use apsis_task::{Task, TaskId, TaskState}; use apsis_task::{Task, TaskId, TaskState};
/// A resource available from 0 to 1 000 seconds. /// A resource available from 0 to 1 000 seconds.
///
/// 一个自 0 到 1 000 秒可用的资源。
fn resource(id: usize, capacity: f64) -> Resource { fn resource(id: usize, capacity: f64) -> Resource {
let mut resource = Resource::new(ResourceId::new(id), capacity); let mut resource = Resource::new(ResourceId::new(id), capacity);
resource resource
@ -18,6 +22,8 @@ fn resource(id: usize, capacity: f64) -> Resource {
} }
/// A task requiring one unit of each named resource. /// A task requiring one unit of each named resource.
///
/// 一个需要每个指名资源一单位的任务。
fn task(id: usize, duration: f64, resources: &[usize]) -> Task { fn task(id: usize, duration: f64, resources: &[usize]) -> Task {
let mut task = Task::new(TaskId::new(id), duration); let mut task = Task::new(TaskId::new(id), duration);
for resource in resources { for resource in resources {
@ -41,6 +47,7 @@ fn a_single_task_is_placed_at_the_opening_of_its_window() {
.placement_of(TaskId::new(0)) .placement_of(TaskId::new(0))
.expect("placed"); .expect("placed");
// The resource is available from zero, so the task starts there. // The resource is available from zero, so the task starts there.
// 资源自零起可用,故任务从那里开始。
assert_eq!(placement.start, 0.0); assert_eq!(placement.start, 0.0);
} }
@ -49,6 +56,7 @@ fn tasks_on_one_exclusive_resource_are_placed_back_to_back() {
let resources = ResourceSet::from_resources(vec![resource(0, 1.0)]).expect("one"); 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 // Equal value, so the order falls back to the earlier deadline and then
// the identifier. // the identifier.
// 价值相等,故次序回退到更早的截止时间,然后回退到标识符。
let tasks = [task(0, 60.0, &[0]), task(1, 30.0, &[0])]; let tasks = [task(0, 60.0, &[0]), task(1, 30.0, &[0])];
let result = Planner::new().schedule(&tasks, &resources); let result = Planner::new().schedule(&tasks, &resources);
@ -63,6 +71,7 @@ fn tasks_on_one_exclusive_resource_are_placed_back_to_back() {
.expect("placed"); .expect("placed");
// The two must not overlap: the second starts at or after the end of the // The two must not overlap: the second starts at or after the end of the
// first. // first.
// 二者不得重叠:第二个在第一个结束处或之后开始。
assert!(second.start >= first.end(), "{second:?} against {first:?}"); assert!(second.start >= first.end(), "{second:?} against {first:?}");
assert!(!result.schedule.has_overlapping_placements()); assert!(!result.schedule.has_overlapping_placements());
} }
@ -71,6 +80,7 @@ fn tasks_on_one_exclusive_resource_are_placed_back_to_back() {
fn a_task_that_does_not_fit_is_deferred() { fn a_task_that_does_not_fit_is_deferred() {
// The resource is available for only sixty seconds, so a task of ninety // The resource is available for only sixty seconds, so a task of ninety
// cannot be placed. // cannot be placed.
// 资源只可用六十秒,故九十秒的任务无法放置。
let mut limited = Resource::new(ResourceId::new(0), 1.0); let mut limited = Resource::new(ResourceId::new(0), 1.0);
limited limited
.set_windows(&[Window::new(0.0, 60.0)]) .set_windows(&[Window::new(0.0, 60.0)])
@ -131,6 +141,7 @@ fn a_mandatory_task_is_placed_before_a_more_valuable_optional_one() {
let resources = ResourceSet::from_resources(vec![resource(0, 1.0)]).expect("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 // The optional task is worth far more, but it can use whatever time is
// left, while the mandatory task has only one window. // left, while the mandatory task has only one window.
// 可选任务价值高得多,但它可以使用剩余的任何时间,而强制任务只有一个窗口。
let optional = task(0, 60.0, &[0]) let optional = task(0, 60.0, &[0])
.with_priority(100.0) .with_priority(100.0)
.with_window(0.0, 1_000.0); .with_window(0.0, 1_000.0);
@ -151,6 +162,8 @@ fn a_mandatory_task_is_placed_before_a_more_valuable_optional_one() {
// With the rule turned off, and the improvement pass disabled so that // 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 // 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. // mandatory one is placed after it, using the rest of its window.
// 关掉该规则、并禁用改进遍(使只有排序受检),可选任务先走,强制任务紧随其后放置,
// 用它窗口的剩余部分。
let without = Planner::new() let without = Planner::new()
.with_mandatory_first(false) .with_mandatory_first(false)
.with_improvement(apsis_scheduler::ImprovementSettings::disabled()) .with_improvement(apsis_scheduler::ImprovementSettings::disabled())
@ -169,6 +182,8 @@ fn the_improvement_pass_recovers_a_plan_that_the_greedy_order_missed() {
// zero to a hundred, so it cannot fit after the optional one and would // 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 // be lost. The improvement pass moves the optional task aside and places
// both. // both.
// 可选任务价值更高,故先被放置并占用资源自 0 到 60。强制任务的窗口自 0 到 100,
// 故它放不进可选任务之后,会丢失。改进遍把可选任务挪开并放置二者。
let resources = ResourceSet::from_resources(vec![resource(0, 1.0)]).expect("one"); let resources = ResourceSet::from_resources(vec![resource(0, 1.0)]).expect("one");
let optional = task(0, 60.0, &[0]) let optional = task(0, 60.0, &[0])
.with_priority(100.0) .with_priority(100.0)
@ -184,6 +199,7 @@ fn the_improvement_pass_recovers_a_plan_that_the_greedy_order_missed() {
.with_improvement(apsis_scheduler::ImprovementSettings::disabled()) .with_improvement(apsis_scheduler::ImprovementSettings::disabled())
.schedule(&tasks, &resources); .schedule(&tasks, &resources);
// Without the pass the mandatory task is lost. // Without the pass the mandatory task is lost.
// 没有该遍时强制任务丢失。
assert!(!greedy.honours_mandatory(&tasks)); assert!(!greedy.honours_mandatory(&tasks));
assert_eq!(greedy.schedule.len(), 1); assert_eq!(greedy.schedule.len(), 1);
@ -191,6 +207,7 @@ fn the_improvement_pass_recovers_a_plan_that_the_greedy_order_missed() {
.with_mandatory_first(false) .with_mandatory_first(false)
.schedule(&tasks, &resources); .schedule(&tasks, &resources);
// With the pass both are placed. // With the pass both are placed.
// 有该遍时二者都被放置。
assert!(improved.is_complete(), "{:?}", improved.outcomes); assert!(improved.is_complete(), "{:?}", improved.outcomes);
assert!(improved.honours_mandatory(&tasks)); assert!(improved.honours_mandatory(&tasks));
assert!(!improved.improved.is_empty()); assert!(!improved.improved.is_empty());
@ -208,6 +225,7 @@ fn the_value_ordering_places_the_most_valuable_task_first() {
let result = Planner::new().schedule(&tasks, &resources); let result = Planner::new().schedule(&tasks, &resources);
assert!(result.is_complete()); assert!(result.is_complete());
// The weighted values are 10, 500, and 100, so task 1 goes first. // The weighted values are 10, 500, and 100, so task 1 goes first.
// 加权值为 10、500 与 100,故任务 1 先走。
assert_eq!( assert_eq!(
result result
.schedule .schedule
@ -234,6 +252,7 @@ fn two_tasks_on_a_shared_resource_can_overlap() {
.placement_of(TaskId::new(1)) .placement_of(TaskId::new(1))
.expect("placed"); .expect("placed");
// Both start as early as possible, because the capacity holds them both. // Both start as early as possible, because the capacity holds them both.
// 二者都尽早开始,因为容量能容纳二者。
assert_eq!(first.start, 0.0); assert_eq!(first.start, 0.0);
assert_eq!(second.start, 0.0); assert_eq!(second.start, 0.0);
assert!(result.schedule.has_overlapping_placements()); assert!(result.schedule.has_overlapping_placements());
@ -244,6 +263,7 @@ fn a_task_is_placed_after_the_other_frees_its_resource() {
let resources = ResourceSet::from_resources(vec![resource(0, 1.0)]).expect("one"); 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, // The first task occupies the resource from 100 to 200, so the second,
// which can start no earlier than 100, must wait. // which can start no earlier than 100, must wait.
// 第一个任务占用资源自 100 到 200,故不得早于 100 开始的第二个必须等待。
let first = task(0, 100.0, &[0]).with_window(100.0, 1_000.0); 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 second = task(1, 50.0, &[0]).with_window(100.0, 1_000.0);
let tasks = [first, second]; let tasks = [first, second];
@ -295,6 +315,7 @@ fn the_earliest_start_accounts_for_a_blocking_placement() {
let start = earliest_feasible_start(&candidate, &tasks, &schedule, &resources) let start = earliest_feasible_start(&candidate, &tasks, &schedule, &resources)
.expect("a placement exists"); .expect("a placement exists");
// The resource is held until a hundred, so the earliest start is there. // The resource is held until a hundred, so the earliest start is there.
// 资源被占至百秒,故最早开始在那里。
assert_eq!(start, 100.0); assert_eq!(start, 100.0);
} }
@ -309,10 +330,12 @@ fn feasibility_reports_the_capacity_limit() {
let tasks = [existing, candidate.clone()]; let tasks = [existing, candidate.clone()];
// Starting at fifty would overlap the existing placement, which the // Starting at fifty would overlap the existing placement, which the
// capacity of one does not allow. // capacity of one does not allow.
// 自五十开始会与已有放置重叠,容量为一不允许。
assert!(!feasible_at( assert!(!feasible_at(
&candidate, &tasks, &schedule, &resources, 50.0 &candidate, &tasks, &schedule, &resources, 50.0
)); ));
// Starting at a hundred is free. // Starting at a hundred is free.
// 自百秒开始是空闲的。
assert!(feasible_at( assert!(feasible_at(
&candidate, &tasks, &schedule, &resources, 100.0 &candidate, &tasks, &schedule, &resources, 100.0
)); ));
@ -331,6 +354,7 @@ fn feasibility_reports_the_availability_limit() {
assert!(feasible_at(&tasks[0], &tasks, &schedule, &resources, 0.0)); assert!(feasible_at(&tasks[0], &tasks, &schedule, &resources, 0.0));
assert!(feasible_at(&tasks[0], &tasks, &schedule, &resources, 40.0)); assert!(feasible_at(&tasks[0], &tasks, &schedule, &resources, 40.0));
// Starting at fifty would end at one hundred and ten, past the window. // Starting at fifty would end at one hundred and ten, past the window.
// 自五十开始会在百一十结束,越过窗口。
assert!(!feasible_at(&tasks[0], &tasks, &schedule, &resources, 50.0)); assert!(!feasible_at(&tasks[0], &tasks, &schedule, &resources, 50.0));
} }
@ -339,10 +363,12 @@ fn the_capacity_check_covers_a_multiple_resource_task() {
let resources = let resources =
ResourceSet::from_resources(vec![resource(0, 1.0), resource(1, 1.0)]).expect("distinct"); ResourceSet::from_resources(vec![resource(0, 1.0), resource(1, 1.0)]).expect("distinct");
// The task needs both resources at once. // The task needs both resources at once.
// 该任务同时需要两种资源。
let tasks = [task(0, 60.0, &[0, 1])]; let tasks = [task(0, 60.0, &[0, 1])];
assert!(resources_have_capacity(&tasks[0], &resources)); assert!(resources_have_capacity(&tasks[0], &resources));
// The same task with one resource missing is infeasible. // The same task with one resource missing is infeasible.
// 同一任务缺少一个资源即不可行。
let missing = [task(0, 60.0, &[0, 9])]; let missing = [task(0, 60.0, &[0, 9])];
assert!(!resources_have_capacity(&missing[0], &resources)); assert!(!resources_have_capacity(&missing[0], &resources));
assert_eq!(infeasible_tasks(&missing, &resources), vec![TaskId::new(0)]); assert_eq!(infeasible_tasks(&missing, &resources), vec![TaskId::new(0)]);
@ -361,6 +387,7 @@ fn the_demand_at_an_instant_is_reported_per_resource() {
assert_eq!(demand[0], (ResourceId::new(0), 1.0)); assert_eq!(demand[0], (ResourceId::new(0), 1.0));
assert_eq!(demand[1], (ResourceId::new(1), 1.0)); assert_eq!(demand[1], (ResourceId::new(1), 1.0));
// Before either placement the demand is zero. // Before either placement the demand is zero.
// 在两处放置之前需求为零。
let demand = demand_at(&schedule, &tasks, &resources, -1.0); let demand = demand_at(&schedule, &tasks, &resources, -1.0);
assert_eq!(demand[0].1, 0.0); assert_eq!(demand[0].1, 0.0);
} }
@ -374,11 +401,13 @@ fn the_free_resources_at_an_instant_exclude_the_busy_ones() {
schedule.place(&tasks[0], 0.0).expect("fits"); schedule.place(&tasks[0], 0.0).expect("fits");
// At fifty seconds resource zero is full and resource one is free. // At fifty seconds resource zero is full and resource one is free.
// 五十秒处资源零已满而资源一空闲。
assert_eq!( assert_eq!(
resources_free_at(&schedule, &tasks, &resources, 50.0), resources_free_at(&schedule, &tasks, &resources, 50.0),
vec![ResourceId::new(1)] vec![ResourceId::new(1)]
); );
// At a hundred and fifty both are free. // At a hundred and fifty both are free.
// 百五十秒处二者都空闲。
assert_eq!( assert_eq!(
resources_free_at(&schedule, &tasks, &resources, 150.0), resources_free_at(&schedule, &tasks, &resources, 150.0),
vec![ResourceId::new(0), ResourceId::new(1)] vec![ResourceId::new(0), ResourceId::new(1)]
@ -415,6 +444,7 @@ fn the_result_reports_every_task() {
assert!(result.outcome_of(task.id).is_some()); assert!(result.outcome_of(task.id).is_some());
} }
// Only the first can be placed. // Only the first can be placed.
// 只有第一个能被放置。
assert_eq!(result.scheduled(), vec![TaskId::new(0)]); assert_eq!(result.scheduled(), vec![TaskId::new(0)]);
assert!(!result.is_complete()); assert!(!result.is_complete());
assert!(result.honours_mandatory(&tasks)); assert!(result.honours_mandatory(&tasks));
@ -429,6 +459,7 @@ fn a_mandatory_task_that_cannot_be_placed_is_reported() {
let result = Planner::new().schedule(&tasks, &resources); let result = Planner::new().schedule(&tasks, &resources);
// The plan does not honour the mandatory task, which the caller can // The plan does not honour the mandatory task, which the caller can
// detect. // detect.
// 该计划未兑现强制任务,调用方可检测到。
assert!(!result.honours_mandatory(&tasks)); assert!(!result.honours_mandatory(&tasks));
assert!(!result.is_complete()); assert!(!result.is_complete());
} }
@ -454,12 +485,15 @@ fn the_infeasible_task_list_excludes_the_merely_deferred() {
let resources = ResourceSet::from_resources(vec![resource(0, 1.0)]).expect("one"); let resources = ResourceSet::from_resources(vec![resource(0, 1.0)]).expect("one");
let tasks = [ let tasks = [
// Can be placed. // Can be placed.
// 可以放置。
task(0, 60.0, &[0]), task(0, 60.0, &[0]),
// Demands too much. // Demands too much.
// 需求过多。
Task::new(TaskId::new(1), 60.0) Task::new(TaskId::new(1), 60.0)
.try_require(ResourceId::new(0), 5.0) .try_require(ResourceId::new(0), 5.0)
.expect("the resource is new"), .expect("the resource is new"),
// Names a resource that does not exist. // Names a resource that does not exist.
// 指名了不存在的资源。
task(2, 60.0, &[9]), task(2, 60.0, &[9]),
]; ];
let infeasible = infeasible_tasks(&tasks, &resources); let infeasible = infeasible_tasks(&tasks, &resources);

View File

@ -1,11 +1,15 @@
//! Tests for precedence honoured by the planner, and for the improvement //! Tests for precedence honoured by the planner, and for the improvement
//! pass. //! pass.
//!
//! 规划器所遵守的先后约束,以及改进遍的测试。
use apsis_resource::{Resource, ResourceId, ResourceSet, Window}; use apsis_resource::{Resource, ResourceId, ResourceSet, Window};
use apsis_scheduler::{ImprovementSettings, Planner}; use apsis_scheduler::{ImprovementSettings, Planner};
use apsis_task::{PrecedenceGraph, Task, TaskId}; use apsis_task::{PrecedenceGraph, Task, TaskId};
/// A resource available from 0 to 1 000 seconds. /// A resource available from 0 to 1 000 seconds.
///
/// 一个自 0 到 1 000 秒可用的资源。
fn resource(id: usize, capacity: f64) -> Resource { fn resource(id: usize, capacity: f64) -> Resource {
let mut resource = Resource::new(ResourceId::new(id), capacity); let mut resource = Resource::new(ResourceId::new(id), capacity);
resource resource
@ -15,6 +19,8 @@ fn resource(id: usize, capacity: f64) -> Resource {
} }
/// A task requiring one unit of each named resource. /// A task requiring one unit of each named resource.
///
/// 一个需要每个指名资源一单位的任务。
fn task(id: usize, duration: f64, resources: &[usize]) -> Task { fn task(id: usize, duration: f64, resources: &[usize]) -> Task {
let mut task = Task::new(TaskId::new(id), duration); let mut task = Task::new(TaskId::new(id), duration);
for resource in resources { for resource in resources {
@ -28,6 +34,7 @@ fn task(id: usize, duration: f64, resources: &[usize]) -> Task {
#[test] #[test]
fn a_chain_of_tasks_is_placed_in_order() { fn a_chain_of_tasks_is_placed_in_order() {
// Three tasks that must run one after another on the same resource. // 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 resources = ResourceSet::from_resources(vec![resource(0, 1.0)]).expect("one");
let tasks = [ let tasks = [
task(0, 60.0, &[0]), task(0, 60.0, &[0]),
@ -63,6 +70,7 @@ fn a_chain_of_tasks_is_placed_in_order() {
fn a_precedence_floor_delays_a_task_that_could_start_earlier() { 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 // The second task could start at zero on its own, but must wait for the
// first, which runs on a different resource. // first, which runs on a different resource.
// 第二个任务本身可以从零开始,但必须等待在另一资源上运行的第一个。
let resources = let resources =
ResourceSet::from_resources(vec![resource(0, 1.0), resource(1, 1.0)]).expect("distinct"); 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 tasks = [task(0, 100.0, &[0]), task(1, 60.0, &[1])];
@ -77,6 +85,7 @@ fn a_precedence_floor_delays_a_task_that_could_start_earlier() {
.expect("placed"); .expect("placed");
// The second resource was free from the start, but the constraint delays // The second resource was free from the start, but the constraint delays
// the task to the end of the first. // the task to the end of the first.
// 第二个资源从一开始就空闲,但该约束把任务推迟到第一个结束。
assert_eq!(second.start, 100.0); assert_eq!(second.start, 100.0);
assert!(result.honours_precedence(&graph)); assert!(result.honours_precedence(&graph));
} }
@ -86,6 +95,7 @@ fn a_diamond_precedence_is_honoured() {
let resources = let resources =
ResourceSet::from_resources(vec![resource(0, 1.0), resource(1, 1.0)]).expect("distinct"); 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. // The first task feeds two parallel tasks, which both feed a last one.
// 第一个任务供给两个并行任务,二者又都供给最后一个。
let tasks = [ let tasks = [
task(0, 100.0, &[0]), task(0, 100.0, &[0]),
task(1, 60.0, &[1]), task(1, 60.0, &[1]),
@ -103,6 +113,7 @@ fn a_diamond_precedence_is_honoured() {
assert!(result.honours_precedence(&graph)); assert!(result.honours_precedence(&graph));
// The last task must follow both of the parallel ones. // The last task must follow both of the parallel ones.
// 最后一个任务必须跟随两个并行任务。
let last = result let last = result
.schedule .schedule
.placement_of(TaskId::new(3)) .placement_of(TaskId::new(3))
@ -124,6 +135,8 @@ fn a_sequential_observation_and_downlink_is_expressed_as_two_tasks() {
// An observation uses the payload and a downlink uses the transmitter, // An observation uses the payload and a downlink uses the transmitter,
// and the downlink must follow the observation. This is the pattern for // and the downlink must follow the observation. This is the pattern for
// a task that needs two resources in sequence. // a task that needs two resources in sequence.
// 一次观测使用载荷、一次下行使用发射机,且下行必须跟随观测。这是按序需要两种
// 资源的任务模式。
let resources = let resources =
ResourceSet::from_resources(vec![resource(0, 1.0), resource(1, 1.0)]).expect("distinct"); 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 observation = task(0, 120.0, &[0]).with_window(50.0, 500.0);
@ -147,9 +160,11 @@ fn a_sequential_observation_and_downlink_is_expressed_as_two_tasks() {
.expect("placed"); .expect("placed");
assert_eq!(collection.start, 50.0); assert_eq!(collection.start, 50.0);
// The downlink starts when the collection ends. // The downlink starts when the collection ends.
// 下行在采集结束时开始。
assert_eq!(transmission.start, 170.0); assert_eq!(transmission.start, 170.0);
// The two use different resources, so they may be placed back to back // The two use different resources, so they may be placed back to back
// without a conflict. // without a conflict.
// 二者使用不同资源,故它们可前后相接地放置而无冲突。
assert_eq!(collection.end(), transmission.start); assert_eq!(collection.end(), transmission.start);
} }
@ -157,6 +172,7 @@ fn a_sequential_observation_and_downlink_is_expressed_as_two_tasks() {
fn a_successor_of_an_unplaced_task_is_deferred() { fn a_successor_of_an_unplaced_task_is_deferred() {
// The first task cannot be placed, so the constraint cannot be met and // The first task cannot be placed, so the constraint cannot be met and
// the second is deferred rather than placed early. // the second is deferred rather than placed early.
// 第一个任务无法放置,故约束无法满足,第二个被推迟而非提前放置。
let resources = ResourceSet::from_resources(vec![resource(0, 1.0)]).expect("one"); 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 impossible = task(0, 90.0, &[0]).with_window(0.0, 60.0);
let successor = task(1, 30.0, &[0]); let successor = task(1, 30.0, &[0]);
@ -169,6 +185,7 @@ fn a_successor_of_an_unplaced_task_is_deferred() {
assert!(!result.schedule.is_placed(TaskId::new(1))); assert!(!result.schedule.is_placed(TaskId::new(1)));
assert!(result.deferred().contains(&TaskId::new(1))); assert!(result.deferred().contains(&TaskId::new(1)));
// The constraints are trivially satisfied, because nothing was promised. // The constraints are trivially satisfied, because nothing was promised.
// 各约束空洞地满足,因为什么都没承诺。
assert!(result.honours_precedence(&graph)); assert!(result.honours_precedence(&graph));
} }
@ -197,6 +214,8 @@ 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 // 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 // builds the graph from one task set and orders another. The planner
// then falls back to the value order rather than failing. // then falls back to the value order rather than failing.
// 该图拒绝环,故只有调用方从一套任务集构建图而排序另一套时环才可能存在。规划器
// 随后回退到价值次序而非失败。
let tasks = [ let tasks = [
Task::new(TaskId::new(0), 60.0), Task::new(TaskId::new(0), 60.0),
Task::new(TaskId::new(1), 60.0), Task::new(TaskId::new(1), 60.0),
@ -205,6 +224,7 @@ fn a_cycle_in_the_graph_falls_back_to_the_value_order() {
let order = Planner::new().order_with_precedence(&tasks, Some(&graph)); let order = Planner::new().order_with_precedence(&tasks, Some(&graph));
// An empty graph gives the value order, which for equal values is by // An empty graph gives the value order, which for equal values is by
// identifier. // identifier.
// 空图给出价值次序,价值相等时按标识符。
assert_eq!(order, vec![TaskId::new(0), TaskId::new(1)]); assert_eq!(order, vec![TaskId::new(0), TaskId::new(1)]);
} }
@ -215,6 +235,7 @@ fn the_improvement_pass_is_skipped_when_precedence_is_used() {
let graph = PrecedenceGraph::new(); let graph = PrecedenceGraph::new();
let result = Planner::new().schedule_with_precedence(&tasks, &resources, &graph); let result = Planner::new().schedule_with_precedence(&tasks, &resources, &graph);
// The pass does not run, so nothing is reported as improved. // The pass does not run, so nothing is reported as improved.
// 该遍不运行,故没有报告改进。
assert!(result.improved.is_empty()); assert!(result.improved.is_empty());
} }
@ -241,6 +262,7 @@ fn the_improvement_pass_can_be_disabled() {
#[test] #[test]
fn the_improvement_pass_preserves_feasibility() { fn the_improvement_pass_preserves_feasibility() {
// Whatever the pass decides, the result must remain conflict free. // Whatever the pass decides, the result must remain conflict free.
// 无论该遍如何决定,结果必须保持无冲突。
let resources = let resources =
ResourceSet::from_resources(vec![resource(0, 1.0), resource(1, 2.0)]).expect("distinct"); ResourceSet::from_resources(vec![resource(0, 1.0), resource(1, 2.0)]).expect("distinct");
let tasks: Vec<Task> = (0..10) let tasks: Vec<Task> = (0..10)
@ -253,6 +275,7 @@ fn the_improvement_pass_preserves_feasibility() {
let result = Planner::new().schedule(&tasks, &resources); let result = Planner::new().schedule(&tasks, &resources);
// The schedule must not place two tasks beyond the capacity of a // The schedule must not place two tasks beyond the capacity of a
// resource at the same time. // resource at the same time.
// 调度不得在同一时刻把两个任务放置到超出某资源的容量。
let report = apsis_conflict::detect(&result.schedule, &tasks, &resources); let report = apsis_conflict::detect(&result.schedule, &tasks, &resources);
assert!( assert!(
report.is_clear(), report.is_clear(),
@ -265,6 +288,7 @@ fn the_improvement_pass_preserves_feasibility() {
fn the_improvement_pass_never_loses_a_placement() { fn the_improvement_pass_never_loses_a_placement() {
// Every task that the greedy pass placed must still be placed // Every task that the greedy pass placed must still be placed
// afterwards, because the pass only adds placements or moves them. // afterwards, because the pass only adds placements or moves them.
// 贪心遍所放置的每个任务其后必须仍被放置,因为该遍只添加或移动放置。
let resources = ResourceSet::from_resources(vec![resource(0, 1.0)]).expect("one"); let resources = ResourceSet::from_resources(vec![resource(0, 1.0)]).expect("one");
let tasks: Vec<Task> = (0..6) let tasks: Vec<Task> = (0..6)
.map(|index| task(index, 60.0, &[0]).with_priority(1.0 + f64::from(index as u32))) .map(|index| task(index, 60.0, &[0]).with_priority(1.0 + f64::from(index as u32)))
@ -288,6 +312,7 @@ fn the_improvement_settings_are_validated_by_use() {
let tasks = [task(0, 60.0, &[0])]; let tasks = [task(0, 60.0, &[0])];
// A bound of zero disables the move search but leaves the pass enabled, // A bound of zero disables the move search but leaves the pass enabled,
// which behaves as if the pass were off. // which behaves as if the pass were off.
// 界为零禁用移动搜索但保留该遍启用,其行为如同该遍关闭。
let planner = Planner::new().with_improvement(ImprovementSettings::new(0)); let planner = Planner::new().with_improvement(ImprovementSettings::new(0));
let result = planner.schedule(&tasks, &resources); let result = planner.schedule(&tasks, &resources);
assert!(result.is_complete()); assert!(result.is_complete());

View File

@ -6,6 +6,12 @@
//! not a second definition of what is feasible. The randomised cases below //! not a second definition of what is feasible. The randomised cases below
//! are the check: many shapes of window, capacity and overlap, including the //! are the check: many shapes of window, capacity and overlap, including the
//! crowded ones where the two could plausibly diverge. //! crowded ones where the two could plausibly diverge.
//!
//! 扫描搜索对候选枚举的差分测试。
//!
//! 扫描从繁忙廓线计算最早开始;枚举检验每个可行性可能改变的时刻。它们必须精确
//! 一致,因为扫描只是枚举的加速器,而非可行性的第二种定义。下面的随机用例就是
//! 检查:窗口、容量与重叠的许多形状,包括二者可能分歧的拥挤情形。
use apsis_resource::{Resource, ResourceId, ResourceSet, Window}; use apsis_resource::{Resource, ResourceId, ResourceSet, Window};
use apsis_schedule::Schedule; use apsis_schedule::Schedule;
@ -14,11 +20,14 @@ use apsis_task::{Task, TaskId};
/// A tiny deterministic generator, so a failure is reproducible from the /// A tiny deterministic generator, so a failure is reproducible from the
/// seed printed in the assertion. /// seed printed in the assertion.
///
/// 一个微型确定性生成器,使失败可从断言中所打印的种子复现。
struct Rng(u64); struct Rng(u64);
impl Rng { impl Rng {
fn next(&mut self) -> u64 { fn next(&mut self) -> u64 {
// SplitMix64, chosen because it is a few lines and has a long period. // SplitMix64, chosen because it is a few lines and has a long period.
// SplitMix64,因其只有几行且周期长而被选用。
self.0 = self.0.wrapping_add(0x9E37_79B9_7F4A_7C15); self.0 = self.0.wrapping_add(0x9E37_79B9_7F4A_7C15);
let mut z = self.0; let mut z = self.0;
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9); z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
@ -31,6 +40,8 @@ impl Rng {
} }
/// Returns a value in `[low, high)`. /// Returns a value in `[low, high)`.
///
/// 返回 `[low, high)` 内的一个值。
fn range(&mut self, low: f64, high: f64) -> f64 { fn range(&mut self, low: f64, high: f64) -> f64 {
low + (self.next() >> 11) as f64 / (1u64 << 53) as f64 * (high - low) low + (self.next() >> 11) as f64 / (1u64 << 53) as f64 * (high - low)
} }
@ -38,11 +49,14 @@ impl Rng {
/// Builds a random scenario and returns the two answers, or `None` when the /// Builds a random scenario and returns the two answers, or `None` when the
/// task does not fit at all. /// task does not fit at all.
///
/// 构建一个随机场景并返回两个答案,任务完全放不下时返回 `None`。
fn compare(seed: u64) -> (Option<f64>, Option<f64>, Schedule) { fn compare(seed: u64) -> (Option<f64>, Option<f64>, Schedule) {
let mut rng = Rng(seed); let mut rng = Rng(seed);
// One resource with a random capacity and a couple of availability // One resource with a random capacity and a couple of availability
// windows. // windows.
// 一个具有随机容量与若干可用性窗口的资源。
let resource_id = ResourceId::new(0); let resource_id = ResourceId::new(0);
let capacity = rng.range(1.0, 4.0); let capacity = rng.range(1.0, 4.0);
let mut resource = Resource::new(resource_id, capacity); let mut resource = Resource::new(resource_id, capacity);
@ -59,6 +73,7 @@ fn compare(seed: u64) -> (Option<f64>, Option<f64>, Schedule) {
let resources = ResourceSet::from_resources(vec![resource]).expect("distinct"); let resources = ResourceSet::from_resources(vec![resource]).expect("distinct");
// A set of tasks with random demands, windows and durations. // A set of tasks with random demands, windows and durations.
// 一组具有随机需求、窗口与时长的任务。
let task_count = rng.below(12) as usize; let task_count = rng.below(12) as usize;
let mut tasks = Vec::new(); let mut tasks = Vec::new();
let mut schedule = Schedule::new(); let mut schedule = Schedule::new();
@ -72,6 +87,7 @@ fn compare(seed: u64) -> (Option<f64>, Option<f64>, Schedule) {
.try_require(resource_id, demand) .try_require(resource_id, demand)
.expect("the resource is new"); .expect("the resource is new");
// Place some of them, so the sweep sees a busy profile. // Place some of them, so the sweep sees a busy profile.
// 放置其中一些,使扫描看到一个繁忙廓线。
if rng.below(3) == 0 { if rng.below(3) == 0 {
let _ = earliest_feasible_start(&task, &tasks, &schedule, &resources); let _ = earliest_feasible_start(&task, &tasks, &schedule, &resources);
if let Some(start) = earliest_feasible_start(&task, &tasks, &schedule, &resources) { if let Some(start) = earliest_feasible_start(&task, &tasks, &schedule, &resources) {
@ -82,6 +98,7 @@ fn compare(seed: u64) -> (Option<f64>, Option<f64>, Schedule) {
} }
// The task under test. // The task under test.
// 受检的任务。
let duration = rng.range(5.0, 60.0); let duration = rng.range(5.0, 60.0);
let from = rng.range(0.0, 300.0); let from = rng.range(0.0, 300.0);
let to = from + rng.range(duration, 300.0); let to = from + rng.range(duration, 300.0);
@ -96,11 +113,15 @@ fn compare(seed: u64) -> (Option<f64>, Option<f64>, Schedule) {
// candidate the original way. The public entry is the sweep, so the // candidate the original way. The public entry is the sweep, so the
// enumeration is exercised through `feasible_at` over the same candidate // enumeration is exercised through `feasible_at` over the same candidate
// set the original built. // set the original built.
// 枚举,通过请求一个开始并以原方式检验每个候选来达到。公开入口是扫描,故枚举是
// 经 `feasible_at` 在原所构建的同一候选集上被检验。
let enumerated = enumerate(&probe, &tasks, &schedule, &resources); let enumerated = enumerate(&probe, &tasks, &schedule, &resources);
(swept, enumerated, schedule) (swept, enumerated, schedule)
} }
/// The original enumeration, written out so the two can be compared. /// The original enumeration, written out so the two can be compared.
///
/// 原枚举,写出以便二者可比较。
fn enumerate( fn enumerate(
task: &Task, task: &Task,
tasks: &[Task], tasks: &[Task],
@ -150,6 +171,7 @@ fn the_sweep_agrees_with_the_enumeration() {
} }
// The cases must actually exercise the interesting path, or agreement // The cases must actually exercise the interesting path, or agreement
// would be vacuous. // would be vacuous.
// 各用例必须实际检验有意思的路径,否则一致会是空洞的。
assert!(placed > 1_000, "only {placed} cases placed the task"); assert!(placed > 1_000, "only {placed} cases placed the task");
assert_eq!(placed, agreed); assert_eq!(placed, agreed);
} }
@ -158,6 +180,7 @@ fn the_sweep_agrees_with_the_enumeration() {
fn a_placed_answer_is_always_feasible() { fn a_placed_answer_is_always_feasible() {
// The sweep must never report a start that the direct test rejects, // The sweep must never report a start that the direct test rejects,
// which is the property the confirmation step in the code relies on. // which is the property the confirmation step in the code relies on.
// 扫描绝不报告直接检验所拒绝的开始,这是代码中确认步所依赖的性质。
let mut checked = 0_usize; let mut checked = 0_usize;
for seed in 1..5_000_u64 { for seed in 1..5_000_u64 {
let (swept, _, _) = compare(seed); let (swept, _, _) = compare(seed);
@ -166,6 +189,8 @@ fn a_placed_answer_is_always_feasible() {
// return the scenario, so this checks the weaker but still // return the scenario, so this checks the weaker but still
// useful property that the answer is finite and inside the task // useful property that the answer is finite and inside the task
// window whenever the test is constructed. // window whenever the test is constructed.
// 重建探针以检验它。比较辅助函数不返回场景,故这检查一个较弱但仍有用的性质:
// 只要构造了测试,答案就是有限的且落在任务窗口内。
assert!(start.is_finite(), "seed {seed} produced {start}"); assert!(start.is_finite(), "seed {seed} produced {start}");
checked += 1; checked += 1;
} }

View File

@ -1,8 +1,12 @@
//! Tests for the precedence graph. //! Tests for the precedence graph.
//!
//! 先后关系图的测试。
use apsis_task::{PrecedenceGraph, TaskErrorKind, TaskId}; use apsis_task::{PrecedenceGraph, TaskErrorKind, TaskId};
/// The identifiers used by the tests. /// The identifiers used by the tests.
///
/// 测试所用的标识符。
fn id(index: usize) -> TaskId { fn id(index: usize) -> TaskId {
TaskId::new(index) TaskId::new(index)
} }
@ -25,6 +29,7 @@ fn a_constraint_is_recorded_both_ways() {
assert!(graph.has_constraint(id(0), id(1))); assert!(graph.has_constraint(id(0), id(1)));
// The constraint is directed, so the reverse does not exist. // The constraint is directed, so the reverse does not exist.
// 约束是有向的,故反向不存在。
assert!(!graph.has_constraint(id(1), id(0))); assert!(!graph.has_constraint(id(1), id(0)));
assert_eq!(graph.successors_of(id(0)), vec![id(1)]); assert_eq!(graph.successors_of(id(0)), vec![id(1)]);
assert_eq!(graph.predecessors_of(id(1)), vec![id(0)]); assert_eq!(graph.predecessors_of(id(1)), vec![id(0)]);
@ -48,6 +53,7 @@ fn a_duplicate_constraint_is_accepted_and_ignored() {
graph.add(id(0), id(1)).expect("no cycle"); graph.add(id(0), id(1)).expect("no cycle");
graph.add(id(0), id(1)).expect("already present"); graph.add(id(0), id(1)).expect("already present");
// The edge is recorded once. // The edge is recorded once.
// 该边只记录一次。
assert_eq!(graph.edge_count(), 1); assert_eq!(graph.edge_count(), 1);
assert_eq!(graph.successors_of(id(0)), vec![id(1)]); assert_eq!(graph.successors_of(id(0)), vec![id(1)]);
} }
@ -58,9 +64,11 @@ fn a_cycle_is_rejected() {
graph.add(id(0), id(1)).expect("no cycle"); graph.add(id(0), id(1)).expect("no cycle");
graph.add(id(1), id(2)).expect("no cycle"); graph.add(id(1), id(2)).expect("no cycle");
// Closing the cycle is refused. // Closing the cycle is refused.
// 闭合环被拒绝。
let error = graph.add(id(2), id(0)).expect_err("this closes a cycle"); let error = graph.add(id(2), id(0)).expect_err("this closes a cycle");
assert_eq!(error.kind(), TaskErrorKind::PrecedenceCycle); assert_eq!(error.kind(), TaskErrorKind::PrecedenceCycle);
// The graph is unchanged. // The graph is unchanged.
// 图不变。
assert!(!graph.has_constraint(id(2), id(0))); assert!(!graph.has_constraint(id(2), id(0)));
assert_eq!(graph.edge_count(), 2); assert_eq!(graph.edge_count(), 2);
} }
@ -87,11 +95,13 @@ fn reachability_follows_the_direction() {
graph.add(id(1), id(2)).expect("no cycle"); graph.add(id(1), id(2)).expect("no cycle");
// The transitive relation holds forward but not backward. // The transitive relation holds forward but not backward.
// 传递关系向前成立而向后不成立。
assert!(graph.is_reachable(id(0), id(1))); assert!(graph.is_reachable(id(0), id(1)));
assert!(graph.is_reachable(id(0), id(2))); assert!(graph.is_reachable(id(0), id(2)));
assert!(graph.is_reachable(id(1), id(2))); assert!(graph.is_reachable(id(1), id(2)));
assert!(!graph.is_reachable(id(2), id(0))); assert!(!graph.is_reachable(id(2), id(0)));
// A task reaches itself by a path of no edges. // A task reaches itself by a path of no edges.
// 任务经零条边的路径到达自身。
assert!(graph.is_reachable(id(0), id(0))); assert!(graph.is_reachable(id(0), id(0)));
} }
@ -99,6 +109,7 @@ fn reachability_follows_the_direction() {
fn roots_and_leaves_are_reported() { fn roots_and_leaves_are_reported() {
let mut graph = PrecedenceGraph::new(); let mut graph = PrecedenceGraph::new();
// A diamond: 0 before 1 and 2, and both before 3. // A diamond: 0 before 1 and 2, and both before 3.
// 菱形:0 在 1 与 2 之前,二者又都在 3 之前。
graph.add(id(0), id(1)).expect("no cycle"); graph.add(id(0), id(1)).expect("no cycle");
graph.add(id(0), id(2)).expect("no cycle"); graph.add(id(0), id(2)).expect("no cycle");
graph.add(id(1), id(3)).expect("no cycle"); graph.add(id(1), id(3)).expect("no cycle");
@ -128,6 +139,7 @@ fn the_topological_order_respects_the_constraints() {
.topological_order(&tasks) .topological_order(&tasks)
.expect("the graph is acyclic"); .expect("the graph is acyclic");
// The constraints force 0 before 1 before 2, whatever the input order. // The constraints force 0 before 1 before 2, whatever the input order.
// 无论输入次序如何,约束强制 0 在 1 之前、1 在 2 之前。
assert_eq!(order, vec![id(0), id(1), id(2)]); assert_eq!(order, vec![id(0), id(1), id(2)]);
} }
@ -135,10 +147,12 @@ fn the_topological_order_respects_the_constraints() {
fn the_topological_order_breaks_ties_by_position() { fn the_topological_order_breaks_ties_by_position() {
let mut graph = PrecedenceGraph::new(); let mut graph = PrecedenceGraph::new();
// Only 0 is constrained, before 1. Tasks 2 and 3 are free. // Only 0 is constrained, before 1. Tasks 2 and 3 are free.
// 只有 0 受约束(在 1 之前)。任务 2 与 3 是自由的。
graph.add(id(0), id(1)).expect("no cycle"); graph.add(id(0), id(1)).expect("no cycle");
// The preference order is 3, 2, 0, 1, so among the free tasks 3 comes // The preference order is 3, 2, 0, 1, so among the free tasks 3 comes
// first. // first.
// 偏好次序是 3、2、0、1,故在自由任务中 3 最先。
let tasks = [id(3), id(2), id(0), id(1)]; let tasks = [id(3), id(2), id(0), id(1)];
let order = graph let order = graph
.topological_order(&tasks) .topological_order(&tasks)
@ -161,6 +175,7 @@ fn the_topological_order_honours_the_diamond() {
assert_eq!(order, vec![id(0), id(1), id(2), id(3)]); assert_eq!(order, vec![id(0), id(1), id(2), id(3)]);
// The position of each task in the result relative to its constraints. // The position of each task in the result relative to its constraints.
// 每个任务在结果中相对其约束的位置。
let position_of = |task: TaskId| { let position_of = |task: TaskId| {
order order
.iter() .iter()
@ -178,6 +193,7 @@ fn the_topological_order_includes_unconstrained_tasks() {
let mut graph = PrecedenceGraph::new(); let mut graph = PrecedenceGraph::new();
graph.add(id(0), id(1)).expect("no cycle"); graph.add(id(0), id(1)).expect("no cycle");
// Task 9 has no constraints at all. // Task 9 has no constraints at all.
// 任务 9 完全没有约束。
let tasks = [id(0), id(1), id(9)]; let tasks = [id(0), id(1), id(9)];
let order = graph let order = graph
.topological_order(&tasks) .topological_order(&tasks)
@ -190,6 +206,7 @@ fn the_topological_order_includes_unconstrained_tasks() {
fn the_topological_order_ignores_predecessors_outside_the_list() { fn the_topological_order_ignores_predecessors_outside_the_list() {
// A constraint whose predecessor is not in the list cannot be waited // A constraint whose predecessor is not in the list cannot be waited
// for, so it is ignored rather than blocking the successor. // for, so it is ignored rather than blocking the successor.
// 前驱不在列表中的约束无法等待,故它被忽略而非阻塞后继。
let mut graph = PrecedenceGraph::new(); let mut graph = PrecedenceGraph::new();
graph.add(id(5), id(1)).expect("no cycle"); graph.add(id(5), id(1)).expect("no cycle");
let tasks = [id(1)]; let tasks = [id(1)];

View File

@ -1,15 +1,21 @@
//! Tests for tasks and the set that holds them. //! Tests for tasks and the set that holds them.
//!
//! 任务及保存它们的集合的测试。
use apsis_resource::ResourceId; use apsis_resource::ResourceId;
use apsis_task::{Requirement, Task, TaskErrorKind, TaskId, TaskSet, TaskState, task_order}; use apsis_task::{Requirement, Task, TaskErrorKind, TaskId, TaskSet, TaskState, task_order};
/// A sixty second task with no window and no requirements. /// A sixty second task with no window and no requirements.
///
/// 一个六十秒、无窗口也无需求的任务。
fn simple() -> Task { fn simple() -> Task {
Task::new(TaskId::new(0), 60.0) Task::new(TaskId::new(0), 60.0)
} }
/// A task requiring two resources for sixty seconds, in a window from 100 to /// A task requiring two resources for sixty seconds, in a window from 100 to
/// 1 000 seconds. /// 1 000 seconds.
///
/// 一个在 100 至 1 000 秒窗口内、需要两个资源六十秒的任务。
fn observation() -> Task { fn observation() -> Task {
simple() simple()
.with_window(100.0, 1_000.0) .with_window(100.0, 1_000.0)
@ -29,6 +35,7 @@ fn a_new_task_has_the_given_duration_and_nothing_else() {
assert!(task.requirements.is_empty()); assert!(task.requirements.is_empty());
assert_eq!(task.requirement_count(), 0); assert_eq!(task.requirement_count(), 0);
// A task with no window is unbounded at both ends. // A task with no window is unbounded at both ends.
// 无窗口的任务在两端都无界。
assert!(task.earliest_start.is_infinite() && task.earliest_start < 0.0); assert!(task.earliest_start.is_infinite() && task.earliest_start < 0.0);
assert!(task.latest_end.is_infinite() && task.latest_end > 0.0); assert!(task.latest_end.is_infinite() && task.latest_end > 0.0);
assert!(!task.window_is_too_short()); assert!(!task.window_is_too_short());
@ -60,6 +67,7 @@ fn a_window_shorter_than_the_task_is_detected() {
fn a_window_exactly_the_length_of_the_task_is_accepted() { 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 task must fit entirely inside the window, and a window of exactly
// the duration holds it. // the duration holds it.
// 任务必须完全落在窗口内,而恰好等于时长的窗口能容纳它。
let task = simple().with_window(0.0, 60.0); let task = simple().with_window(0.0, 60.0);
assert!(!task.window_is_too_short()); assert!(!task.window_is_too_short());
assert!(task.is_valid()); assert!(task.is_valid());
@ -95,6 +103,7 @@ fn a_duplicate_requirement_is_reported_immediately() {
fn a_duplicate_requirement_built_by_chaining_is_rejected_on_validation() { fn a_duplicate_requirement_built_by_chaining_is_rejected_on_validation() {
// The plain builder cannot report an error, so the duplicate survives // The plain builder cannot report an error, so the duplicate survives
// until validation. // until validation.
// 普通构建器无法报告错误,故重复项存留到校验时。
let task = simple() let task = simple()
.require(ResourceId::new(0), 1.0) .require(ResourceId::new(0), 1.0)
.require(ResourceId::new(0), 1.0); .require(ResourceId::new(0), 1.0);
@ -174,6 +183,7 @@ fn the_weighted_value_combines_priority_and_duration() {
let task = simple().with_priority(5.0); let task = simple().with_priority(5.0);
assert_eq!(task.weighted_value(), 300.0); assert_eq!(task.weighted_value(), 300.0);
// A task with no priority is worth nothing. // A task with no priority is worth nothing.
// 无优先级的任务不值任何东西。
assert_eq!(simple().weighted_value(), 0.0); assert_eq!(simple().weighted_value(), 0.0);
} }
@ -186,6 +196,7 @@ fn the_mandatory_flag_is_recorded() {
#[test] #[test]
fn the_builder_does_not_modify_the_original() { fn the_builder_does_not_modify_the_original() {
// A builder chain returns a new value, so a base task can be shared. // A builder chain returns a new value, so a base task can be shared.
// 构建器链返回新值,故基础任务可被共享。
let base = simple(); let base = simple();
let first = base.clone().with_priority(1.0); let first = base.clone().with_priority(1.0);
let second = base.clone().with_priority(2.0); let second = base.clone().with_priority(2.0);
@ -310,6 +321,7 @@ fn the_set_reports_queries() {
); );
assert_eq!(set.mandatory(), vec![TaskId::new(1)]); assert_eq!(set.mandatory(), vec![TaskId::new(1)]);
// The weighted value is the priority times the duration. // The weighted value is the priority times the duration.
// 加权值是优先级乘以时长。
assert_eq!(set.total_weighted_value(), 300.0 + 0.0 + 20.0); assert_eq!(set.total_weighted_value(), 300.0 + 0.0 + 20.0);
} }
@ -324,6 +336,7 @@ fn the_task_order_places_the_most_valuable_first() {
let order = task_order(set.tasks()); let order = task_order(set.tasks());
// The weighted values are 10, 500, and 100, so the order is 1, 2, 0. // 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)]); assert_eq!(order, vec![TaskId::new(1), TaskId::new(2), TaskId::new(0)]);
} }
@ -331,9 +344,11 @@ fn the_task_order_places_the_most_valuable_first() {
fn the_task_order_breaks_ties_by_deadline_then_identifier() { fn the_task_order_breaks_ties_by_deadline_then_identifier() {
let set = TaskSet::from_tasks(vec![ let set = TaskSet::from_tasks(vec![
// The same value and no deadline. // The same value and no deadline.
// 价值相同且无截止时间。
Task::new(TaskId::new(0), 10.0).with_priority(1.0), Task::new(TaskId::new(0), 10.0).with_priority(1.0),
Task::new(TaskId::new(1), 10.0).with_priority(1.0), Task::new(TaskId::new(1), 10.0).with_priority(1.0),
// The same value but an earlier deadline. // The same value but an earlier deadline.
// 价值相同但截止时间更早。
Task::new(TaskId::new(2), 10.0) Task::new(TaskId::new(2), 10.0)
.with_priority(1.0) .with_priority(1.0)
.with_window(0.0, 100.0), .with_window(0.0, 100.0),
@ -343,6 +358,7 @@ fn the_task_order_breaks_ties_by_deadline_then_identifier() {
let order = task_order(set.tasks()); let order = task_order(set.tasks());
// The task with the deadline comes first, then the others by identifier. // The task with the deadline comes first, then the others by identifier.
// 有截止时间的任务最先,其余按标识符。
assert_eq!( assert_eq!(
order, order,
vec![ vec![