1use super::types::{
7 AnimationEvent, AnimationReport, AnimationTimeline, AnimationVerdict, EventResult,
8};
9
10#[derive(Clone, Debug)]
12pub struct ObservedEvent {
13 pub name: String,
15 pub time_secs: f64,
17}
18
19#[must_use]
24pub fn verify_timeline(
25 timeline: &AnimationTimeline,
26 observed: &[ObservedEvent],
27 tolerance_ms: f64,
28) -> AnimationReport {
29 if !timeline.has_events() {
30 return AnimationReport {
31 video_id: timeline.video_id.clone(),
32 verdict: AnimationVerdict::NoEvents,
33 events: Vec::new(),
34 total_events: 0,
35 verified_events: 0,
36 max_delta_ms: 0.0,
37 mean_delta_ms: 0.0,
38 };
39 }
40
41 let mut results = Vec::new();
42 let mut verified = 0;
43 let mut max_delta: f64 = 0.0;
44 let mut delta_sum: f64 = 0.0;
45 let mut delta_count: usize = 0;
46
47 for event in &timeline.events {
48 let observed_event = find_observed_by_name(observed, &event.name);
49 let result = match observed_event {
50 Some(obs) => {
51 let delta_ms = (obs.time_secs - event.expected_secs) * 1000.0;
52 let abs_delta = delta_ms.abs();
53 let passed = abs_delta <= tolerance_ms;
54 if passed {
55 verified += 1;
56 }
57 if abs_delta > max_delta {
58 max_delta = abs_delta;
59 }
60 delta_sum += abs_delta;
61 delta_count += 1;
62 EventResult {
63 name: event.name.clone(),
64 event_type: event.event_type.clone(),
65 expected_secs: event.expected_secs,
66 actual_secs: Some(obs.time_secs),
67 delta_ms: Some(delta_ms),
68 passed,
69 }
70 }
71 None => EventResult {
72 name: event.name.clone(),
73 event_type: event.event_type.clone(),
74 expected_secs: event.expected_secs,
75 actual_secs: None,
76 delta_ms: None,
77 passed: false,
78 },
79 };
80 results.push(result);
81 }
82
83 let mean_delta = if delta_count > 0 {
84 delta_sum / delta_count as f64
85 } else {
86 0.0
87 };
88
89 let verdict = if verified == timeline.events.len() {
90 AnimationVerdict::Pass
91 } else {
92 AnimationVerdict::Fail
93 };
94
95 AnimationReport {
96 video_id: timeline.video_id.clone(),
97 verdict,
98 events: results,
99 total_events: timeline.events.len(),
100 verified_events: verified,
101 max_delta_ms: max_delta,
102 mean_delta_ms: mean_delta,
103 }
104}
105
106#[must_use]
110pub fn verify_events(
111 expected: &[AnimationEvent],
112 observed: &[ObservedEvent],
113 tolerance_ms: f64,
114 video_id: &str,
115) -> AnimationReport {
116 let timeline = AnimationTimeline {
117 video_id: video_id.to_string(),
118 events: expected.to_vec(),
119 };
120 verify_timeline(&timeline, observed, tolerance_ms)
121}
122
123fn find_observed_by_name<'a>(
124 observed: &'a [ObservedEvent],
125 name: &str,
126) -> Option<&'a ObservedEvent> {
127 observed.iter().find(|o| o.name == name)
128}
129
130#[cfg(test)]
131#[allow(clippy::unwrap_used)]
132mod tests {
133 use super::*;
134 use crate::animation::types::AnimationEventType;
135
136 fn make_event(name: &str, secs: f64) -> AnimationEvent {
137 AnimationEvent {
138 name: name.to_string(),
139 event_type: AnimationEventType::PhysicsEvent,
140 expected_secs: secs,
141 duration_secs: None,
142 easing: None,
143 }
144 }
145
146 fn make_observed(name: &str, secs: f64) -> ObservedEvent {
147 ObservedEvent {
148 name: name.to_string(),
149 time_secs: secs,
150 }
151 }
152
153 #[test]
154 fn test_perfect_match() {
155 let timeline = AnimationTimeline {
156 video_id: "test".to_string(),
157 events: vec![make_event("land_0", 1.7), make_event("land_1", 2.4)],
158 };
159 let observed = vec![make_observed("land_0", 1.7), make_observed("land_1", 2.4)];
160 let report = verify_timeline(&timeline, &observed, 20.0);
161 assert_eq!(report.verdict, AnimationVerdict::Pass);
162 assert_eq!(report.verified_events, 2);
163 }
164
165 #[test]
166 fn test_within_tolerance() {
167 let timeline = AnimationTimeline {
168 video_id: "test".to_string(),
169 events: vec![make_event("land_0", 1.7)],
170 };
171 let observed = vec![make_observed("land_0", 1.71)]; let report = verify_timeline(&timeline, &observed, 20.0);
173 assert_eq!(report.verdict, AnimationVerdict::Pass);
174 }
175
176 #[test]
177 fn test_exceeds_tolerance() {
178 let timeline = AnimationTimeline {
179 video_id: "test".to_string(),
180 events: vec![make_event("land_0", 1.7)],
181 };
182 let observed = vec![make_observed("land_0", 1.408)]; let report = verify_timeline(&timeline, &observed, 20.0);
184 assert_eq!(report.verdict, AnimationVerdict::Fail);
185 assert!((report.max_delta_ms - 292.0).abs() < 0.1);
186 }
187
188 #[test]
189 fn test_missing_observed_event() {
190 let timeline = AnimationTimeline {
191 video_id: "test".to_string(),
192 events: vec![make_event("land_0", 1.7)],
193 };
194 let observed: Vec<ObservedEvent> = vec![];
195 let report = verify_timeline(&timeline, &observed, 20.0);
196 assert_eq!(report.verdict, AnimationVerdict::Fail);
197 assert!(report.events[0].actual_secs.is_none());
198 }
199
200 #[test]
201 fn test_no_events() {
202 let timeline = AnimationTimeline {
203 video_id: "empty".to_string(),
204 events: vec![],
205 };
206 let observed: Vec<ObservedEvent> = vec![];
207 let report = verify_timeline(&timeline, &observed, 20.0);
208 assert_eq!(report.verdict, AnimationVerdict::NoEvents);
209 }
210
211 #[test]
212 fn test_partial_match() {
213 let timeline = AnimationTimeline {
214 video_id: "test".to_string(),
215 events: vec![make_event("land_0", 1.7), make_event("land_1", 2.4)],
216 };
217 let observed = vec![make_observed("land_0", 1.7)]; let report = verify_timeline(&timeline, &observed, 20.0);
219 assert_eq!(report.verdict, AnimationVerdict::Fail);
220 assert_eq!(report.verified_events, 1);
221 assert_eq!(report.total_events, 2);
222 }
223
224 #[test]
225 fn test_mean_delta() {
226 let timeline = AnimationTimeline {
227 video_id: "test".to_string(),
228 events: vec![make_event("a", 1.0), make_event("b", 2.0)],
229 };
230 let observed = vec![make_observed("a", 1.01), make_observed("b", 2.02)];
232 let report = verify_timeline(&timeline, &observed, 25.0);
233 assert_eq!(report.verdict, AnimationVerdict::Pass);
234 assert!((report.mean_delta_ms - 15.0).abs() < 0.1);
235 }
236
237 #[test]
238 fn test_verify_events_api() {
239 let events = vec![make_event("land", 1.5)];
240 let observed = vec![make_observed("land", 1.5)];
241 let report = verify_events(&events, &observed, 20.0, "test");
242 assert_eq!(report.verdict, AnimationVerdict::Pass);
243 assert_eq!(report.video_id, "test");
244 }
245
246 #[test]
247 fn test_event_result_details() {
248 let timeline = AnimationTimeline {
249 video_id: "test".to_string(),
250 events: vec![make_event("bounce", 3.0)],
251 };
252 let observed = vec![make_observed("bounce", 3.015)];
253 let report = verify_timeline(&timeline, &observed, 20.0);
254 let ev = &report.events[0];
255 assert_eq!(ev.name, "bounce");
256 assert!((ev.actual_secs.unwrap() - 3.015).abs() < f64::EPSILON);
257 assert!((ev.delta_ms.unwrap() - 15.0).abs() < 0.1);
258 assert!(ev.passed);
259 }
260
261 #[test]
262 fn test_negative_delta() {
263 let timeline = AnimationTimeline {
264 video_id: "test".to_string(),
265 events: vec![make_event("land", 1.7)],
266 };
267 let observed = vec![make_observed("land", 1.69)]; let report = verify_timeline(&timeline, &observed, 20.0);
269 assert_eq!(report.verdict, AnimationVerdict::Pass);
270 let delta = report.events[0].delta_ms.unwrap();
271 assert!(delta < 0.0); }
273}