docs: bilingual comments for the sim tests

This commit is contained in:
ApsisRS contributors 2026-10-02 02:00:56 +08:00
parent 1526928097
commit 92ad0bcd1c
5 changed files with 69 additions and 0 deletions

View File

@ -1,4 +1,6 @@
//! Tests for the event queue.
//!
//! 事件队列的测试。
use apsis_event::{Event, EventId, EventQueue};
@ -37,6 +39,7 @@ fn events_are_delivered_in_time_order() {
#[test]
fn events_at_the_same_time_are_delivered_in_schedule_order() {
// The tie-break is what makes a run reproducible.
// 并列打破规则正是使运行可复现的东西。
let mut queue = EventQueue::new();
queue.schedule(10.0, 'a');
queue.schedule(10.0, 'b');
@ -54,6 +57,7 @@ fn events_at_the_same_time_are_delivered_in_schedule_order() {
fn the_tie_break_holds_across_a_heap_rebuild() {
// Scheduling in a different order must still deliver in schedule order,
// because the sequence number, not the insertion position, decides.
// 以不同次序调度仍必须按调度次序投递,因为决定的是序号而非插入位置。
let mut queue = EventQueue::new();
let mut ids = Vec::new();
for index in 0..20 {
@ -67,6 +71,7 @@ fn the_tie_break_holds_across_a_heap_rebuild() {
assert_eq!(delivered, (0..20).collect::<Vec<u64>>());
// The identifiers are also in schedule order.
// 标识符也按调度次序排列。
for (position, id) in ids.iter().enumerate() {
assert_eq!(id.sequence(), position as u64);
}
@ -92,6 +97,7 @@ fn scheduling_a_negative_or_non_finite_time_is_refused() {
assert_eq!(queue.schedule(time, "x"), None, "time {time}");
}
// The queue is unchanged and the sequence number was not consumed.
// 队列不变,且序号未被消耗。
assert!(queue.is_empty());
let id = queue.schedule(0.0, "zero").expect("zero is usable");
assert_eq!(id.sequence(), 0);
@ -115,6 +121,7 @@ fn a_follow_up_with_a_bad_delay_is_refused() {
assert_eq!(queue.schedule_after(0.0, f64::INFINITY, "x"), None);
assert!(queue.is_empty());
// A zero delay schedules at the current instant.
// 零延时在当前瞬时调度。
let id = queue.schedule_after(5.0, 0.0, "now").expect("usable");
assert_eq!(id.time(), 5.0);
}
@ -123,6 +130,7 @@ fn a_follow_up_with_a_bad_delay_is_refused() {
fn events_can_be_scheduled_from_a_handler() {
// A handler that schedules its successor, which is the pattern a
// discrete-event simulation uses for a periodic process.
// 一个调度其后继的处理器,这是离散事件仿真用于周期过程的模式。
let mut queue: EventQueue<u32> = EventQueue::new();
queue.schedule(0.0, 0).expect("usable");
let mut ticks = 0;
@ -151,6 +159,7 @@ fn clearing_discards_the_pending_events() {
assert_eq!(queue.clear(), 4);
assert!(queue.is_empty());
// The delivered count records history and is not reset.
// 已投递计数记录历史且不重置。
assert_eq!(queue.delivered(), 1);
assert_eq!(queue.pop(), None);
}
@ -191,6 +200,7 @@ fn the_queue_state_reports_what_is_pending() {
fn a_payload_slot_is_reused_after_a_pop() {
// The queue indexes payloads by sequence, so a popped slot must be
// reusable without disturbing a later event.
// 队列按序号索引载荷,故被弹出的槽位必须可复用而不扰动更晚的事件。
let mut queue = EventQueue::new();
queue.schedule(1.0, "first").expect("usable");
queue.schedule(2.0, "second").expect("usable");
@ -205,6 +215,8 @@ fn a_payload_slot_is_reused_after_a_pop() {
fn the_identifier_orders_by_time_then_sequence() {
// The derived ordering on the identifier is the delivery order, which is
// what lets a caller sort identifiers to reproduce a run.
// 标识符上派生的次序即投递次序,这正是使调用方可对标识符排序以复现一次运行的
// 原因。
let earlier = EventId::new(1.0, 5).expect("usable");
let later_time = EventId::new(2.0, 0).expect("usable");
let later_sequence = EventId::new(1.0, 6).expect("usable");
@ -246,6 +258,7 @@ fn an_event_can_be_rescheduled() {
assert_eq!(moved.id.sequence(), 1);
assert_eq!(moved.payload, "x");
// The original is unchanged.
// 原件不变。
assert_eq!(event.time, 5.0);
assert!(event.reschedule(-1.0, 2).is_none());

View File

@ -1,4 +1,6 @@
//! Tests for the output trace and recorder.
//!
//! 输出轨迹与记录器的测试。
use apsis_output::{Record, RecordKind, Recorder, Trace};
@ -20,6 +22,7 @@ fn an_empty_trace_has_nothing() {
fn a_trace_keeps_insertion_order() {
// The order a model recorded in is part of what it decided, so the trace
// does not sort.
// 模型记录所用的次序是其决策的一部分,故轨迹不排序。
let mut trace = Trace::new();
trace.push(Record::observation(10.0, "b", 1.0));
trace.push(Record::observation(5.0, "a", 2.0));
@ -41,6 +44,7 @@ fn time_order_is_available_on_request() {
let times: Vec<f64> = ordered.iter().map(|record| record.time).collect();
assert_eq!(times, vec![5.0, 7.0, 10.0]);
// The original is unchanged.
// 原件不变。
assert_eq!(trace.records()[0].time, 10.0);
}
@ -165,6 +169,7 @@ fn a_non_finite_record_is_refused() {
assert!(trace.is_empty());
// A finite record is accepted.
// 有限的记录被接受。
assert!(trace.push(Record::observation(1.0, "x", 1.0)));
assert_eq!(trace.len(), 1);
assert!(trace.is_stable());
@ -214,6 +219,7 @@ fn taking_the_trace_leaves_the_recorder_empty() {
assert_eq!(trace.len(), 2);
assert!(recorder.is_empty());
// The refused count belongs to the recorder and is not reset.
// 被拒绝计数属于记录器且不重置。
recorder.observe(f64::NAN, "x", 1.0);
assert_eq!(recorder.refused(), 1);
}

View File

@ -1,4 +1,6 @@
//! Tests for building and reading a scenario.
//!
//! 构建与读取场景的测试。
use apsis_resource::{Resource, ResourceId, Window};
use apsis_scenario::{Scenario, ScenarioErrorKind};
@ -44,6 +46,8 @@ fn the_header_setters_take_effect() {
fn a_limit_the_run_cannot_use_is_cleared() {
// The setters match the engine, which ignores a limit it cannot use, so
// a scenario never carries one that would surprise a run.
// 各设置器与引擎一致,后者忽略它无法使用的限制,故场景绝不携带会让一次运行意外
// 的限制。
let scenario = Scenario::new("demo")
.with_time_limit(-1.0)
.with_event_limit(0);
@ -63,6 +67,7 @@ fn the_checked_setters_report_a_bad_value() {
assert!(scenario.set_time_limit(f64::INFINITY).is_err());
assert!(scenario.set_time_limit(-5.0).is_err());
// A good value is accepted and the scenario keeps its old one on failure.
// 好的值被接受,失败时场景保留其旧值。
scenario.set_epoch(10.0).expect("the epoch is usable");
assert_eq!(scenario.epoch(), 10.0);
scenario.set_time_limit(60.0).expect("the limit is usable");
@ -78,6 +83,7 @@ fn a_resource_is_added_and_checked() {
assert_eq!(scenario.resources().len(), 1);
// A second resource with the same identifier is refused.
// 标识符相同的第二个资源被拒绝。
let error = scenario
.add_resource(station(2.0, &[]))
.expect_err("the identifier is taken");
@ -102,6 +108,7 @@ fn a_task_is_added_only_when_its_resources_exist() {
.expect("the resource is new");
// The resource is not in the scenario yet.
// 该资源还不在场景中。
let error = scenario
.add_task(task.clone())
.expect_err("unknown resource");
@ -114,6 +121,7 @@ fn a_task_is_added_only_when_its_resources_exist() {
assert_eq!(scenario.tasks().len(), 1);
// A second task with the same identifier is refused.
// 标识符相同的第二个任务被拒绝。
let error = scenario
.add_task(Task::new(TaskId::new(0), 30.0))
.expect_err("the identifier is taken");
@ -124,6 +132,7 @@ fn a_task_is_added_only_when_its_resources_exist() {
fn an_unusable_task_is_refused() {
let mut scenario = Scenario::new("demo");
// A task with a zero duration is not usable.
// 零时长的任务不可用。
let error = scenario
.add_task(Task::new(TaskId::new(0), 0.0))
.expect_err("the task is not usable");
@ -284,6 +293,7 @@ fn a_scenario_round_trips_through_text() {
let reloaded = Scenario::parse(&text).expect("the rendered text parses");
assert_eq!(reloaded, original);
// A second round trip is stable, so the text form is a fixed point.
// 第二次往返是稳定的,故文本形式是固定点。
assert_eq!(reloaded.to_text(), text);
}
@ -291,6 +301,7 @@ fn a_scenario_round_trips_through_text() {
fn a_task_with_no_window_round_trips() {
// A task with no window leaves both ends infinite, which the text form
// cannot write; the round trip must still recover an equivalent task.
// 无窗口的任务使两端均为无限,文本形式写不出来;往返仍必须恢复一个等价任务。
let mut original = Scenario::new("open");
original
.add_resource(station(1.0, &[(0.0, 600.0)]))
@ -308,6 +319,8 @@ fn a_task_with_no_window_round_trips() {
// The window is written as a finite interval that holds the task, which
// is the best a text form with no infinity can do. The task is still
// usable.
// 该窗口被写为容纳该任务的有限区间,这是没有无穷的文本形式所能做的最好。任务
// 仍可用。
assert!(task.is_valid());
assert!(task.fits_window());
}
@ -373,6 +386,7 @@ fn a_qualifier_before_any_task_is_reported() {
#[test]
fn precedence_may_name_tasks_in_either_order_in_the_file() {
// The three passes let a constraint name a task defined later.
// 三遍解析使约束可以指名稍后才定义的任务。
let text = "\
scenario demo
resource 0 capacity 1
@ -430,6 +444,7 @@ precedence 1 0
assert_eq!(error.kind(), ScenarioErrorKind::CyclicPrecedence);
// The second constraint, on the fifth line, is the one that closes the
// cycle.
// 第五行上的第二条约束正是闭合该环的那条。
assert_eq!(error.line(), Some(5));
}

View File

@ -1,4 +1,6 @@
//! Tests for the simulation engine.
//!
//! 仿真引擎的测试。
use apsis_sim_engine::{Engine, StopReason};
@ -43,6 +45,7 @@ fn a_run_executes_every_event_in_order() {
fn a_handler_can_schedule_follow_up_events() {
// A periodic process that reschedules itself, which is the pattern a
// discrete-event model uses for a clock driven process.
// 一个重新调度自身的周期过程,这是离散事件模型用于时钟驱动过程的模式。
let mut engine = Engine::new();
engine.schedule(0.0, 0_u32);
@ -61,6 +64,7 @@ fn a_handler_can_schedule_follow_up_events() {
assert_eq!(outcome.reason, StopReason::QueueEmpty);
// The clock ends at the last event, not at the delay of the last
// follow-up, which was never scheduled.
// 时钟结束于最后一个事件,而非最后一个后续事件的延时,后者从未被调度。
assert_eq!(outcome.ended_at, 30.0);
}
@ -80,6 +84,8 @@ fn events_at_the_same_instant_are_delivered_in_schedule_order() {
fn a_follow_up_scheduled_for_the_same_instant_is_delivered_after_the_pending_ones() {
// This is what makes a cascade at one instant well defined: events
// scheduled by a handler go behind the events already queued.
// 这正是使某一瞬时的级联有明确含义的东西:处理器所调度的事件排在已排队的事件
// 之后。
let mut engine = Engine::new();
engine.schedule(5.0, "first");
engine.schedule(5.0, "second");
@ -100,6 +106,7 @@ fn a_follow_up_scheduled_for_the_same_instant_is_delivered_after_the_pending_one
fn a_run_is_reproducible() {
// The same inputs must give the same output, which the tie-break on the
// schedule order guarantees.
// 相同输入必须给出相同输出,这由调度次序上的并列打破规则保证。
let build = || {
let mut engine = Engine::new();
for index in 0..20 {
@ -128,9 +135,11 @@ fn a_time_limit_stops_the_run_and_leaves_the_event_pending() {
assert_eq!(outcome.reason, StopReason::TimeLimit);
assert!(!outcome.is_natural());
// The event beyond the limit is still queued.
// 超限的事件仍在队列中。
assert_eq!(outcome.remaining, 2);
assert_eq!(engine.next_time(), Some(20.0));
// The clock is at the last event executed, not at the limit.
// 时钟在最后执行的事件处,而非在限制处。
assert_eq!(outcome.ended_at, 10.0);
}
@ -164,6 +173,8 @@ fn an_event_limit_stops_the_run_without_discarding_events() {
// The limit is a lifetime cap rather than a per run one, so a second
// run at once stops without executing anything. Raising the limit lets
// the remaining events run.
// 该限制是生命期上限而非每次运行的上限,故紧接着的第二次运行不执行任何东西即
// 停止。提高限制使剩余事件运行。
let outcome = engine.run(|_, event| seen.push(event.payload));
assert_eq!(outcome.executed, 0);
assert_eq!(outcome.reason, StopReason::EventLimit);
@ -179,6 +190,7 @@ fn an_event_limit_stops_the_run_without_discarding_events() {
fn the_event_limit_counts_across_runs() {
// The limit is on the total number of events, so a run that already
// reached it does nothing.
// 该限制针对事件总数,故已达到它的运行什么也不做。
let mut engine = Engine::new().with_event_limit(1);
engine.schedule(1.0, "a").expect("usable");
assert_eq!(engine.run(|_, _| {}).executed, 1);
@ -204,11 +216,13 @@ fn a_stop_request_ends_the_run_after_the_current_event() {
});
// The event that requested the stop is the last one executed.
// 请求停止的事件是最后执行的那个。
assert_eq!(seen, vec![0, 1, 2]);
assert_eq!(outcome.reason, StopReason::Requested);
assert_eq!(outcome.remaining, 2);
// The request is cleared by the run, so the next run continues.
// 该请求由运行清除,故下次运行继续。
let outcome = engine.run(|_, event| seen.push(event.payload));
assert_eq!(seen, vec![0, 1, 2, 3, 4]);
assert_eq!(outcome.reason, StopReason::QueueEmpty);
@ -221,6 +235,7 @@ fn a_stop_request_can_be_cleared_by_hand() {
engine.request_stop();
engine.clear_stop();
// The run clears the flag itself, so a clear beforehand is harmless.
// 运行自身会清除该标志,故事先清除是无害的。
assert_eq!(engine.run(|_, _| {}).executed, 1);
}
@ -240,6 +255,7 @@ fn the_clock_can_be_set_to_start_at_an_offset() {
engine.set_now(1_000.0);
assert_eq!(engine.now(), 1_000.0);
// A relative schedule is measured from the offset.
// 相对调度自该偏移量起度量。
engine.schedule_after(5.0, "x").expect("usable");
assert_eq!(engine.next_time(), Some(1_005.0));
@ -249,6 +265,7 @@ fn the_clock_can_be_set_to_start_at_an_offset() {
assert_eq!(outcome.duration(), 5.0);
// An unusable time is ignored, leaving the clock alone.
// 不可用的时间被忽略,时钟不动。
engine.set_now(-1.0);
assert_eq!(engine.now(), 1_005.0);
engine.set_now(f64::NAN);
@ -265,6 +282,7 @@ fn the_outcome_reports_the_event_rate() {
}
let outcome = engine.run(|_, _| {});
// Ten events over ninety seconds, so the rate is a ninth per second.
// 九十秒内十个事件,故速率为每秒九分之一。
assert_eq!(outcome.duration(), 90.0);
assert!((outcome.event_rate().expect("a positive duration") - 10.0 / 90.0).abs() < 1e-12);
}
@ -287,6 +305,7 @@ fn the_queue_is_reachable_from_the_engine() {
assert_eq!(engine.queue().len(), 1);
assert_eq!(engine.queue().peek(), Some(&"a"));
// The queue may be modified directly.
// 队列可被直接修改。
engine.queue_mut().clear();
assert!(engine.is_idle());
}
@ -296,6 +315,7 @@ fn a_bad_time_limit_is_ignored() {
let mut engine = Engine::new().with_time_limit(-1.0);
engine.schedule(100.0, "x").expect("usable");
// The run reaches the event, because no limit applies.
// 该运行到达该事件,因为没有限制适用。
assert_eq!(engine.run(|_, _| {}).executed, 1);
let mut engine = Engine::new().with_time_limit(f64::NAN);

View File

@ -1,10 +1,14 @@
//! Tests for the simulation facade.
//!
//! 仿真门面的测试。
use apsis_sim::{
Activity, RecordKind, RunReport, Scenario, Simulation, SimulationErrorKind, StopReason,
};
/// Builds a scenario with one station and a set of tasks.
///
/// 构建一个含一座站与一组任务的场景。
fn scenario_with(tasks: &[(usize, f64, f64)]) -> Scenario {
let mut text = String::from("scenario test\nresource 0 capacity 1\nwindow 0 0 1000\n");
for &(id, duration, priority) in tasks {
@ -34,6 +38,7 @@ fn the_timeline_has_a_start_and_an_end_for_each_task() {
let simulation = Simulation::prepare(scenario_with(&[(0, 60.0, 1.0)])).expect("usable");
let timeline = simulation.timeline();
// In time order: the start precedes the end.
// 按时间次序:开始在结束之前。
assert_eq!(timeline.len(), 2);
assert!(timeline[0].1.is_start());
assert_eq!(timeline[0].1.task(), apsis_task::TaskId::new(0));
@ -83,6 +88,7 @@ fn the_run_starts_at_the_scenario_epoch() {
assert_eq!(session.now(), 1_000.0);
let report = session.run(|_, _, _| {});
// The timeline events are absolute, so the run reaches the end event.
// 时间线事件是绝对的,故运行到达结束事件。
assert!(report.outcome.ended_at > 1_000.0);
}
@ -128,6 +134,7 @@ fn a_session_leaves_the_prepared_simulation_alone() {
let mut first = simulation.start();
first.run(|_, _, _| {});
// A second session starts from the full timeline again.
// 第二个会话又从完整的时间线开始。
let mut second = simulation.start();
assert_eq!(second.run(|_, _, _| {}).executed(), 2);
}
@ -135,6 +142,7 @@ fn a_session_leaves_the_prepared_simulation_alone() {
#[test]
fn a_handler_may_schedule_further_activity() {
// The engine interleaves events the handler schedules with the timeline.
// 引擎把处理器所调度的事件与时间线交织。
let simulation = Simulation::prepare(scenario_with(&[(0, 60.0, 1.0)])).expect("usable");
let mut session = simulation.start();
let mut extra = 0_u32;
@ -148,12 +156,14 @@ fn a_handler_may_schedule_further_activity() {
});
assert_eq!(extra, 1);
// Two timeline events plus the one the handler scheduled.
// 两个时间线事件加上处理器所调度的那个。
assert_eq!(report.executed(), 3);
}
#[test]
fn deferred_tasks_are_reported() {
// Two tasks of 60 s in a 100 s window: only one fits.
// 一百秒窗口中的两个六十秒任务:只有一个放得下。
let scenario = Scenario::parse(
"scenario tight\n\
resource 0 capacity 1\n\
@ -209,6 +219,7 @@ fn the_schedule_evaluates() {
fn an_unusable_scenario_is_refused() {
let mut scenario = Scenario::new("");
// An empty name is refused by validation.
// 空名称被校验拒绝。
scenario
.add_resource(apsis_resource::Resource::new(
apsis_resource::ResourceId::new(0),
@ -223,6 +234,7 @@ fn an_unusable_scenario_is_refused() {
fn a_plan_naming_a_task_outside_the_scenario_is_refused() {
// A scheduling result that places a task the scenario does not hold is
// inconsistent and must be rejected rather than run.
// 一个放置了场景所不持有任务的调度结果是不一致的,必须被拒绝而非运行。
let mut scenario = Scenario::new("mismatch");
scenario
.add_resource(apsis_resource::Resource::new(
@ -237,6 +249,7 @@ fn a_plan_naming_a_task_outside_the_scenario_is_refused() {
let mut schedule = apsis_schedule::Schedule::new();
// Task 99 is not in the scenario. A placement records the duration, so
// it is placed directly rather than through a task.
// 任务 99 不在场景中。放置记录时长,故直接放置它而非经任务。
schedule
.place(
&apsis_task::Task::new(apsis_task::TaskId::new(99), 60.0),
@ -256,6 +269,7 @@ fn a_plan_naming_a_task_outside_the_scenario_is_refused() {
fn an_empty_plan_is_accepted() {
// A scenario whose tasks none can be placed still prepares; the
// resulting simulation simply has an empty timeline.
// 没有一个任务放得下的场景仍可准备;所得仿真只是有一条空时间线。
let scenario = Scenario::new("empty");
let result =
apsis_scheduler::Planner::new().schedule(&[], &scenario.resource_set().expect("ok"));
@ -285,6 +299,7 @@ fn the_session_can_record_without_running() {
assert!(session.record(2.0, RecordKind::Warning, "manual", 6.0));
assert_eq!(session.recorder().len(), 2);
// A run takes the trace, so the manual records come out with it.
// 一次运行取走轨迹,故手工记录随之而出。
let report: RunReport = session.run(|_, _, _| {});
assert_eq!(report.trace.len(), 2);
assert_eq!(report.warning_count(), 1);