This commit is contained in:
Mateusz Gruszczyński committed 2026-09-02 22:41:27 +02:00
1 parent 8004be0841
commit db6d2f09db
20 files changed
+945 -59

No files matched your search

+58 -1
View File
@@ -86,6 +86,63 @@ mod tests {
}
}
#[test]
fn flow_cron_validation_and_matching_cover_ranges_steps_and_sunday_alias() {
assert!(cron_expression_valid("*/5 * * * *"));
assert!(cron_expression_valid("0,15,30,45 6-18 * * 1-5"));
for invalid in ["* * * *", "*/0 * * * *", "61 * * * *", "* 24 * * *", "* * 0 * *", "* * * 13 *", "* * * * 8", "*/100 * * * *"] {
assert!(!cron_expression_valid(invalid), "accepted invalid cron: {invalid}");
}
let monday = Local.with_ymd_and_hms(2026, 9, 7, 10, 15, 0).single().unwrap();
assert!(cron_matches("15 10 * * 1", &monday));
assert!(cron_matches("15 10 * * 1-7", &monday));
assert!(!cron_matches("16 10 * * 1", &monday));
let sunday = Local.with_ymd_and_hms(2026, 9, 6, 8, 0, 0).single().unwrap();
assert!(cron_matches("0 8 * * 0", &sunday));
assert!(cron_matches("0 8 * * 7", &sunday));
assert!(cron_matches("0 8 * * 1-7", &sunday));
}
#[test]
fn flow_timed_gate_requires_continuity_and_resets_on_false() {
let mut state = crate::models::FlowRuntimeNodeState::default();
let start = Utc.with_ymd_and_hms(2026, 9, 2, 12, 0, 0).unwrap();
assert!(!flow_timed_gate_update(&mut state, true, 30, start));
assert!(!flow_timed_gate_update(&mut state, true, 30, start + chrono::Duration::seconds(29)));
assert!(flow_timed_gate_update(&mut state, true, 30, start + chrono::Duration::seconds(30)));
assert!(!flow_timed_gate_update(&mut state, false, 30, start + chrono::Duration::seconds(31)));
assert!(state.since.is_none());
assert!(!flow_timed_gate_update(&mut state, true, 30, start + chrono::Duration::seconds(32)));
}
#[test]
fn flow_rolling_statistics_compute_mean_median_and_prune_window() {
let mut state = crate::models::FlowRuntimeNodeState::default();
let start = Utc.with_ymd_and_hms(2026, 9, 2, 12, 0, 0).unwrap();
assert_eq!(flow_rolling_stat_update(&mut state, 10.0, 60, "mean", start), Some(10.0));
assert_eq!(flow_rolling_stat_update(&mut state, 20.0, 60, "mean", start + chrono::Duration::seconds(10)), Some(15.0));
assert_eq!(flow_rolling_stat_update(&mut state, 30.0, 60, "median", start + chrono::Duration::seconds(20)), Some(20.0));
assert_eq!(flow_rolling_stat_update(&mut state, 40.0, 60, "median", start + chrono::Duration::seconds(30)), Some(25.0));
assert_eq!(flow_rolling_stat_update(&mut state, 100.0, 60, "mean", start + chrono::Duration::seconds(100)), Some(100.0));
assert_eq!(state.samples.len(), 1);
}
#[test]
fn flow_oscillation_requires_direction_changes_not_only_large_span() {
let start = Utc.with_ymd_and_hms(2026, 9, 2, 12, 0, 0).unwrap();
let bouncing = [20.0, 22.0, 19.0, 23.0, 20.0].into_iter().enumerate().map(|(index, value)| crate::models::FlowRuntimeSample {
at: start + chrono::Duration::seconds(index as i64 * 10), value,
}).collect::<Vec<_>>();
let monotonic = [20.0, 21.0, 22.0, 23.0, 24.0].into_iter().enumerate().map(|(index, value)| crate::models::FlowRuntimeSample {
at: start + chrono::Duration::seconds(index as i64 * 10), value,
}).collect::<Vec<_>>();
let (span, changes) = flow_oscillation_metrics(&bouncing).unwrap();
assert!(span >= 4.0);
assert!(changes >= 2);
let (_, monotonic_changes) = flow_oscillation_metrics(&monotonic).unwrap();
assert_eq!(monotonic_changes, 0);
}
#[test]
fn time_automation_fires_only_once_in_the_same_minute() {
let now = Local.with_ymd_and_hms(2025, 1, 6, 10, 15, 40).single().unwrap();
@@ -94,7 +151,7 @@ mod tests {
trigger_device_id: None, threshold: None, at_time: Some("10:15".into()),
action_device_id: "d".into(), action_group_id: None, action_preset: None,
action: DeviceCommand { power: Some(true), ..Default::default() }, cooldown_seconds: 30,
last_fired_at: None, action_zone_id: None, action_zone_preset: None, flow_conditions: vec![], flow_id: None, flow_node_id: None, created_at: Utc::now(), updated_at: Utc::now(),
last_fired_at: None, action_zone_id: None, action_zone_preset: None, action_ha_domain: None, action_ha_service: None, action_ha_entity_id: None, action_ha_data: Value::Null, flow_conditions: vec![], flow_id: None, flow_node_id: None, flow_runtime: Default::default(), created_at: Utc::now(), updated_at: Utc::now(),
};
assert!(time_automation_due(&item, now.clone()));
item.last_fired_at = Some((now.clone() - chrono::Duration::seconds(35)).with_timezone(&Utc));