286 lines
9.2 KiB
Rust
286 lines
9.2 KiB
Rust
//! Tests for the output trace and recorder.
|
|
//!
|
|
//! 输出轨迹与记录器的测试。
|
|
|
|
use apsis_output::{Record, RecordKind, Recorder, Trace};
|
|
|
|
#[test]
|
|
fn an_empty_trace_has_nothing() {
|
|
let trace = Trace::new();
|
|
assert!(trace.is_empty());
|
|
assert_eq!(trace.len(), 0);
|
|
assert!(trace.is_stable());
|
|
assert!(trace.records().is_empty());
|
|
assert_eq!(trace.labels(), Vec::<&str>::new());
|
|
assert_eq!(trace.value_of("anything"), None);
|
|
assert_eq!(trace.summarize("anything"), None);
|
|
assert_eq!(trace.warning_count(), 0);
|
|
assert_eq!(trace.longest_series(), 0);
|
|
}
|
|
|
|
#[test]
|
|
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));
|
|
let labels: Vec<&str> = trace
|
|
.records()
|
|
.iter()
|
|
.map(|record| record.label.as_str())
|
|
.collect();
|
|
assert_eq!(labels, vec!["b", "a"]);
|
|
}
|
|
|
|
#[test]
|
|
fn time_order_is_available_on_request() {
|
|
let mut trace = Trace::new();
|
|
trace.push(Record::observation(10.0, "b", 1.0));
|
|
trace.push(Record::observation(5.0, "a", 2.0));
|
|
trace.push(Record::observation(7.0, "c", 3.0));
|
|
let ordered = trace.in_time_order();
|
|
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);
|
|
}
|
|
|
|
#[test]
|
|
fn time_order_is_stable_for_equal_times() {
|
|
let mut trace = Trace::new();
|
|
trace.push(Record::observation(5.0, "first", 1.0));
|
|
trace.push(Record::observation(5.0, "second", 2.0));
|
|
trace.push(Record::observation(5.0, "third", 3.0));
|
|
let ordered = trace.in_time_order();
|
|
let labels: Vec<&str> = ordered.iter().map(|record| record.label.as_str()).collect();
|
|
assert_eq!(labels, vec!["first", "second", "third"]);
|
|
}
|
|
|
|
#[test]
|
|
fn records_can_be_filtered_by_kind() {
|
|
let mut trace = Trace::new();
|
|
trace.push(Record::observation(1.0, "a", 1.0));
|
|
trace.push(Record::warning(2.0, "b", 2.0));
|
|
trace.push(Record::state(3.0, "c", 3.0));
|
|
trace.push(Record::warning(4.0, "d", 4.0));
|
|
|
|
assert_eq!(trace.of_kind(RecordKind::Observation).len(), 1);
|
|
assert_eq!(trace.of_kind(RecordKind::Warning).len(), 2);
|
|
assert_eq!(trace.of_kind(RecordKind::State).len(), 1);
|
|
assert_eq!(trace.warning_count(), 2);
|
|
}
|
|
|
|
#[test]
|
|
fn records_can_be_filtered_by_label() {
|
|
let mut trace = Trace::new();
|
|
trace.push(Record::observation(1.0, "range", 1_000.0));
|
|
trace.push(Record::observation(2.0, "range", 900.0));
|
|
trace.push(Record::observation(3.0, "rate", 5.0));
|
|
assert_eq!(trace.with_label("range").len(), 2);
|
|
assert_eq!(trace.with_label("rate").len(), 1);
|
|
assert_eq!(trace.with_label("absent").len(), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn the_last_value_of_a_label_is_reported() {
|
|
let mut trace = Trace::new();
|
|
trace.push(Record::observation(1.0, "range", 1_000.0));
|
|
trace.push(Record::observation(5.0, "range", 900.0));
|
|
trace.push(Record::observation(9.0, "range", 950.0));
|
|
assert_eq!(trace.value_of("range"), Some(950.0));
|
|
assert_eq!(trace.last_time_of("range"), Some(9.0));
|
|
assert_eq!(trace.value_of("absent"), None);
|
|
}
|
|
|
|
#[test]
|
|
fn the_labels_are_unique_and_sorted() {
|
|
let mut trace = Trace::new();
|
|
trace.push(Record::observation(1.0, "zeta", 1.0));
|
|
trace.push(Record::observation(2.0, "alpha", 2.0));
|
|
trace.push(Record::observation(3.0, "zeta", 3.0));
|
|
assert_eq!(trace.labels(), vec!["alpha", "zeta"]);
|
|
}
|
|
|
|
#[test]
|
|
fn a_series_summarizes_its_values() {
|
|
let mut trace = Trace::new();
|
|
for (time, value) in [(0.0, 10.0), (1.0, 20.0), (2.0, 30.0), (3.0, 5.0)] {
|
|
trace.push(Record::observation(time, "x", value));
|
|
}
|
|
let summary = trace.summarize("x").expect("the series exists");
|
|
assert_eq!(summary.label, "x");
|
|
assert_eq!(summary.count, 4);
|
|
assert_eq!(summary.min, 5.0);
|
|
assert_eq!(summary.max, 30.0);
|
|
assert_eq!(summary.first, 10.0);
|
|
assert_eq!(summary.last, 5.0);
|
|
assert_eq!(summary.change, -5.0);
|
|
assert_eq!(summary.mean, 65.0 / 4.0);
|
|
assert_eq!(summary.range(), 25.0);
|
|
assert!(!summary.is_constant());
|
|
}
|
|
|
|
#[test]
|
|
fn a_constant_series_is_recognized() {
|
|
let mut trace = Trace::new();
|
|
for time in 0..3 {
|
|
trace.push(Record::observation(f64::from(time), "x", 7.0));
|
|
}
|
|
let summary = trace.summarize("x").expect("the series exists");
|
|
assert!(summary.is_constant());
|
|
assert_eq!(summary.range(), 0.0);
|
|
assert_eq!(summary.change, 0.0);
|
|
}
|
|
|
|
#[test]
|
|
fn the_summaries_cover_every_label_in_order() {
|
|
let mut trace = Trace::new();
|
|
trace.push(Record::observation(1.0, "b", 1.0));
|
|
trace.push(Record::observation(1.0, "a", 2.0));
|
|
trace.push(Record::observation(2.0, "b", 3.0));
|
|
let summaries = trace.summaries();
|
|
assert_eq!(summaries.len(), 2);
|
|
assert_eq!(summaries[0].label, "a");
|
|
assert_eq!(summaries[1].label, "b");
|
|
assert_eq!(summaries[1].count, 2);
|
|
}
|
|
|
|
#[test]
|
|
fn the_longest_series_is_reported() {
|
|
let mut trace = Trace::new();
|
|
for index in 0..5 {
|
|
trace.push(Record::observation(f64::from(index), "x", 1.0));
|
|
}
|
|
for index in 0..2 {
|
|
trace.push(Record::observation(f64::from(index), "y", 1.0));
|
|
}
|
|
assert_eq!(trace.longest_series(), 5);
|
|
}
|
|
|
|
#[test]
|
|
fn a_non_finite_record_is_refused() {
|
|
let mut trace = Trace::new();
|
|
assert!(!trace.push(Record::observation(f64::NAN, "x", 1.0)));
|
|
assert!(!trace.push(Record::observation(1.0, "x", f64::INFINITY)));
|
|
assert!(!trace.push(Record::observation(1.0, "", 1.0)));
|
|
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());
|
|
}
|
|
|
|
#[test]
|
|
fn clearing_empties_the_trace() {
|
|
let mut trace = Trace::new();
|
|
trace.push(Record::observation(1.0, "x", 1.0));
|
|
trace.push(Record::observation(2.0, "x", 2.0));
|
|
assert_eq!(trace.clear(), 2);
|
|
assert!(trace.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn the_recorder_collects_records_of_each_kind() {
|
|
let mut recorder = Recorder::new();
|
|
assert!(recorder.observe(1.0, "range", 100.0));
|
|
assert!(recorder.note(2.0, "eclipse", 1.0));
|
|
assert!(recorder.warn(3.0, "temperature", 90.0));
|
|
|
|
assert_eq!(recorder.len(), 3);
|
|
assert!(!recorder.is_empty());
|
|
let trace = recorder.trace();
|
|
assert_eq!(trace.of_kind(RecordKind::Observation).len(), 1);
|
|
assert_eq!(trace.of_kind(RecordKind::State).len(), 1);
|
|
assert_eq!(trace.of_kind(RecordKind::Warning).len(), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn the_recorder_counts_what_it_refused() {
|
|
let mut recorder = Recorder::new();
|
|
assert!(!recorder.observe(f64::NAN, "x", 1.0));
|
|
assert!(!recorder.observe(1.0, "x", f64::NAN));
|
|
assert!(recorder.observe(1.0, "x", 1.0));
|
|
assert_eq!(recorder.refused(), 2);
|
|
assert_eq!(recorder.len(), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn taking_the_trace_leaves_the_recorder_empty() {
|
|
let mut recorder = Recorder::new();
|
|
recorder.observe(1.0, "x", 1.0);
|
|
recorder.observe(2.0, "x", 2.0);
|
|
|
|
let trace = recorder.take_trace();
|
|
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);
|
|
}
|
|
|
|
#[test]
|
|
fn the_recorder_can_be_cleared_without_taking() {
|
|
let mut recorder = Recorder::new();
|
|
recorder.observe(1.0, "x", 1.0);
|
|
assert_eq!(recorder.clear(), 1);
|
|
assert!(recorder.is_empty());
|
|
assert_eq!(recorder.trace().len(), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn a_trace_can_be_built_from_records() {
|
|
let records = vec![
|
|
Record::observation(1.0, "a", 1.0),
|
|
Record::observation(2.0, "b", 2.0),
|
|
];
|
|
let trace = Trace::from_records(records);
|
|
assert_eq!(trace.len(), 2);
|
|
assert_eq!(trace.value_of("b"), Some(2.0));
|
|
}
|
|
|
|
#[test]
|
|
fn a_record_displays_its_shape() {
|
|
let record = Record::warning(3.5, "temperature", 90.0);
|
|
let text = record.to_string();
|
|
assert!(text.contains("warning"), "{text}");
|
|
assert!(text.contains("temperature"), "{text}");
|
|
assert!(text.contains("90"), "{text}");
|
|
}
|
|
|
|
#[test]
|
|
fn the_record_kinds_report_themselves() {
|
|
assert_eq!(RecordKind::Observation.name(), "observation");
|
|
assert_eq!(RecordKind::State.name(), "state");
|
|
assert_eq!(RecordKind::Warning.name(), "warning");
|
|
assert!(RecordKind::Warning.is_warning());
|
|
assert!(!RecordKind::State.is_warning());
|
|
assert_eq!(RecordKind::State.to_string(), "state");
|
|
}
|
|
|
|
#[test]
|
|
fn a_record_reports_whether_it_is_usable() {
|
|
assert!(Record::observation(1.0, "x", 1.0).is_valid());
|
|
assert!(!Record::observation(f64::NAN, "x", 1.0).is_valid());
|
|
assert!(!Record::observation(1.0, "x", f64::NAN).is_valid());
|
|
assert!(!Record::observation(1.0, "", 1.0).is_valid());
|
|
}
|
|
|
|
#[test]
|
|
fn the_trace_default_is_empty() {
|
|
let trace = Trace::default();
|
|
assert!(trace.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn the_recorder_default_is_empty() {
|
|
let recorder = Recorder::default();
|
|
assert!(recorder.is_empty());
|
|
assert_eq!(recorder.refused(), 0);
|
|
}
|