261 lines
8.7 KiB
Rust
261 lines
8.7 KiB
Rust
//! Tests for availability windows and the interval algebra.
|
|
|
|
use apsis_resource::{Interval, ResourceErrorKind, Window, merge_windows, subtract_window};
|
|
|
|
#[test]
|
|
fn an_interval_reports_its_duration() {
|
|
assert_eq!(Interval::new(10.0, 30.0).duration(), 20.0);
|
|
// 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, 10.0).duration(), 0.0);
|
|
}
|
|
|
|
#[test]
|
|
fn emptiness_and_validity_are_reported() {
|
|
assert!(Interval::new(5.0, 5.0).is_empty());
|
|
assert!(Interval::new(5.0, 4.0).is_empty());
|
|
assert!(!Interval::new(4.0, 5.0).is_empty());
|
|
assert!(Interval::new(4.0, 5.0).is_valid());
|
|
// A reversed interval is not valid, although it is empty.
|
|
assert!(!Interval::new(5.0, 4.0).is_valid());
|
|
assert!(!Interval::new(f64::NAN, 5.0).is_valid());
|
|
assert!(!Interval::new(4.0, f64::INFINITY).is_valid());
|
|
}
|
|
|
|
#[test]
|
|
fn containment_is_half_open() {
|
|
let interval = Interval::new(10.0, 20.0);
|
|
// The start is inside and the end is not.
|
|
assert!(!interval.contains(9.999));
|
|
assert!(interval.contains(10.0));
|
|
assert!(interval.contains(15.0));
|
|
assert!(interval.contains(19.999));
|
|
assert!(!interval.contains(20.0));
|
|
}
|
|
|
|
#[test]
|
|
fn containing_an_interval_includes_the_boundaries() {
|
|
let outer = Interval::new(0.0, 100.0);
|
|
assert!(outer.contains_interval(&Interval::new(10.0, 20.0)));
|
|
// The boundaries are included, so an interval that shares an end is
|
|
// contained.
|
|
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(50.0, 100.0)));
|
|
// 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(-0.1, 50.0)));
|
|
}
|
|
|
|
#[test]
|
|
fn an_empty_interval_is_contained_when_its_instant_is() {
|
|
let outer = Interval::new(0.0, 100.0);
|
|
assert!(outer.contains_interval(&Interval::new(50.0, 50.0)));
|
|
assert!(!outer.contains_interval(&Interval::new(200.0, 200.0)));
|
|
}
|
|
|
|
#[test]
|
|
fn intervals_that_meet_do_not_overlap() {
|
|
// The half-open convention means the boundary instant belongs to the
|
|
// second interval only.
|
|
let first = Interval::new(0.0, 10.0);
|
|
let second = Interval::new(10.0, 20.0);
|
|
assert!(!first.overlaps(&second));
|
|
assert!(!second.overlaps(&first));
|
|
assert!(first.is_adjacent_to(&second));
|
|
}
|
|
|
|
#[test]
|
|
fn overlapping_intervals_report_their_intersection() {
|
|
let first = Interval::new(0.0, 10.0);
|
|
let second = Interval::new(5.0, 15.0);
|
|
assert!(first.overlaps(&second));
|
|
assert_eq!(first.intersection(&second), Some(Interval::new(5.0, 10.0)));
|
|
assert_eq!(second.intersection(&first), first.intersection(&second));
|
|
// Disjoint intervals have no intersection.
|
|
assert_eq!(
|
|
Interval::new(0.0, 1.0).intersection(&Interval::new(2.0, 3.0)),
|
|
None
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn the_hull_covers_both_intervals() {
|
|
let first = Interval::new(0.0, 10.0);
|
|
let second = Interval::new(5.0, 15.0);
|
|
assert_eq!(first.hull(&second), Interval::new(0.0, 15.0));
|
|
assert_eq!(first.hull(&second), second.hull(&first));
|
|
}
|
|
|
|
#[test]
|
|
fn merging_combines_overlapping_windows() {
|
|
let windows = [
|
|
Window::new(0.0, 10.0),
|
|
Window::new(5.0, 15.0),
|
|
Window::new(30.0, 40.0),
|
|
];
|
|
let merged = merge_windows(&windows).expect("the windows are usable");
|
|
assert_eq!(merged.len(), 2);
|
|
assert_eq!(merged[0], Window::new(0.0, 15.0));
|
|
assert_eq!(merged[1], Window::new(30.0, 40.0));
|
|
}
|
|
|
|
#[test]
|
|
fn merging_combines_windows_that_touch() {
|
|
// Two windows that meet share a boundary instant, which belongs to the
|
|
// second, so the union is one continuous window.
|
|
let merged = merge_windows(&[Window::new(0.0, 10.0), Window::new(10.0, 20.0)])
|
|
.expect("the windows are usable");
|
|
assert_eq!(merged, vec![Window::new(0.0, 20.0)]);
|
|
}
|
|
|
|
#[test]
|
|
fn merging_drops_empty_windows() {
|
|
// A zero length window is valid but contributes no availability, so it
|
|
// is dropped. A reversed window is a caller error and is rejected.
|
|
let merged = merge_windows(&[
|
|
Window::new(5.0, 5.0),
|
|
Window::new(0.0, 10.0),
|
|
Window::new(50.0, 50.0),
|
|
])
|
|
.expect("the windows are usable");
|
|
assert_eq!(merged, vec![Window::new(0.0, 10.0)]);
|
|
}
|
|
|
|
#[test]
|
|
fn merging_sorts_windows_into_time_order() {
|
|
let merged = merge_windows(&[
|
|
Window::new(30.0, 40.0),
|
|
Window::new(0.0, 10.0),
|
|
Window::new(15.0, 20.0),
|
|
])
|
|
.expect("the windows are usable");
|
|
assert_eq!(merged[0].start(), 0.0);
|
|
assert_eq!(merged[1].start(), 15.0);
|
|
assert_eq!(merged[2].start(), 30.0);
|
|
}
|
|
|
|
#[test]
|
|
fn merging_an_empty_list_gives_an_empty_list() {
|
|
assert!(merge_windows(&[]).expect("nothing to check").is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn merging_is_idempotent() {
|
|
// The canonical form must be a fixed point, so that comparing two sets
|
|
// is a direct comparison.
|
|
let windows = [
|
|
Window::new(0.0, 10.0),
|
|
Window::new(5.0, 15.0),
|
|
Window::new(30.0, 40.0),
|
|
];
|
|
let once = merge_windows(&windows).expect("usable");
|
|
let twice = merge_windows(&once).expect("usable");
|
|
assert_eq!(once, twice);
|
|
}
|
|
|
|
#[test]
|
|
fn a_reversed_window_is_rejected() {
|
|
let error = merge_windows(&[Window::new(10.0, 0.0)]).expect_err("reversed");
|
|
assert_eq!(error.kind(), ResourceErrorKind::ReversedInterval);
|
|
|
|
let error = Window::new(10.0, 0.0).validate().expect_err("reversed");
|
|
assert_eq!(error.kind(), ResourceErrorKind::ReversedInterval);
|
|
}
|
|
|
|
#[test]
|
|
fn a_non_finite_window_is_rejected() {
|
|
assert_eq!(
|
|
Window::new(f64::NAN, 1.0)
|
|
.validate()
|
|
.expect_err("not finite")
|
|
.kind(),
|
|
ResourceErrorKind::NotFinite
|
|
);
|
|
assert_eq!(
|
|
merge_windows(&[Window::new(0.0, f64::INFINITY)])
|
|
.expect_err("not finite")
|
|
.kind(),
|
|
ResourceErrorKind::NotFinite
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn subtracting_removes_the_middle_of_a_window() {
|
|
let windows = [Window::new(0.0, 100.0)];
|
|
let result = subtract_window(&windows, Window::new(30.0, 40.0)).expect("usable");
|
|
assert_eq!(
|
|
result,
|
|
vec![Window::new(0.0, 30.0), Window::new(40.0, 100.0)]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn subtracting_the_whole_of_a_window_leaves_nothing() {
|
|
let result =
|
|
subtract_window(&[Window::new(0.0, 10.0)], Window::new(0.0, 10.0)).expect("usable");
|
|
assert!(result.is_empty());
|
|
|
|
// A window that covers more than the stored one also removes it.
|
|
let result =
|
|
subtract_window(&[Window::new(0.0, 10.0)], Window::new(-5.0, 15.0)).expect("usable");
|
|
assert!(result.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn subtracting_the_start_trims_the_window() {
|
|
let result = subtract_window(&[Window::new(0.0, 10.0)], Window::new(0.0, 4.0)).expect("usable");
|
|
assert_eq!(result, vec![Window::new(4.0, 10.0)]);
|
|
}
|
|
|
|
#[test]
|
|
fn subtracting_the_end_trims_the_window() {
|
|
let result =
|
|
subtract_window(&[Window::new(0.0, 10.0)], Window::new(6.0, 10.0)).expect("usable");
|
|
assert_eq!(result, vec![Window::new(0.0, 6.0)]);
|
|
}
|
|
|
|
#[test]
|
|
fn subtracting_a_disjoint_window_changes_nothing() {
|
|
let windows = [Window::new(0.0, 10.0), Window::new(20.0, 30.0)];
|
|
let result = subtract_window(&windows, Window::new(12.0, 18.0)).expect("usable");
|
|
assert_eq!(result, windows.to_vec());
|
|
}
|
|
|
|
#[test]
|
|
fn subtracting_an_empty_window_changes_nothing() {
|
|
let windows = [Window::new(0.0, 10.0)];
|
|
let result = subtract_window(&windows, Window::new(5.0, 5.0)).expect("usable");
|
|
assert_eq!(result, windows.to_vec());
|
|
}
|
|
|
|
#[test]
|
|
fn subtracting_from_several_windows_affects_each() {
|
|
let windows = [Window::new(0.0, 10.0), Window::new(20.0, 30.0)];
|
|
let result = subtract_window(&windows, Window::new(5.0, 25.0)).expect("usable");
|
|
assert_eq!(result, vec![Window::new(0.0, 5.0), Window::new(25.0, 30.0)]);
|
|
}
|
|
|
|
#[test]
|
|
fn subtracting_merges_the_windows_first() {
|
|
// The two overlapping windows form one, which the subtraction then
|
|
// 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 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)]);
|
|
}
|
|
|
|
#[test]
|
|
fn a_window_displays_its_interval() {
|
|
assert_eq!(Window::new(1.5, 2.5).to_string(), "[1.5, 2.5)");
|
|
assert_eq!(Interval::new(0.0, 1.0).to_string(), "[0, 1)");
|
|
}
|
|
|
|
#[test]
|
|
fn a_window_can_be_built_from_an_interval() {
|
|
let interval = Interval::new(3.0, 7.0);
|
|
let window = Window::from_interval(interval);
|
|
assert_eq!(window.interval, interval);
|
|
assert_eq!(window.duration(), 4.0);
|
|
}
|