1use super::backend::TuiFrame;
14use crate::result::{ProbarError, ProbarResult};
15use std::collections::HashMap;
16
17#[derive(Debug)]
19pub struct FrameAssertion<'a> {
20 frame: &'a TuiFrame,
21 soft_mode: bool,
22 errors: Vec<String>,
23}
24
25impl<'a> FrameAssertion<'a> {
26 #[must_use]
28 pub fn new(frame: &'a TuiFrame) -> Self {
29 Self {
30 frame,
31 soft_mode: false,
32 errors: Vec::new(),
33 }
34 }
35
36 #[must_use]
38 pub fn soft(mut self) -> Self {
39 self.soft_mode = true;
40 self
41 }
42
43 pub fn to_contain_text(&mut self, text: &str) -> ProbarResult<&mut Self> {
45 if !self.frame.contains(text) {
46 let msg = format!(
47 "Expected frame to contain text '{}'\nFrame content:\n{}",
48 text,
49 self.frame.as_text()
50 );
51 if self.soft_mode {
52 self.errors.push(msg);
53 } else {
54 return Err(ProbarError::AssertionFailed { message: msg });
55 }
56 }
57 Ok(self)
58 }
59
60 pub fn not_to_contain_text(&mut self, text: &str) -> ProbarResult<&mut Self> {
62 if self.frame.contains(text) {
63 let msg = format!(
64 "Expected frame NOT to contain text '{}'\nFrame content:\n{}",
65 text,
66 self.frame.as_text()
67 );
68 if self.soft_mode {
69 self.errors.push(msg);
70 } else {
71 return Err(ProbarError::AssertionFailed { message: msg });
72 }
73 }
74 Ok(self)
75 }
76
77 pub fn to_match(&mut self, pattern: &str) -> ProbarResult<&mut Self> {
79 let matches = self.frame.matches(pattern)?;
80 if !matches {
81 let msg = format!(
82 "Expected frame to match pattern '{}'\nFrame content:\n{}",
83 pattern,
84 self.frame.as_text()
85 );
86 if self.soft_mode {
87 self.errors.push(msg);
88 } else {
89 return Err(ProbarError::AssertionFailed { message: msg });
90 }
91 }
92 Ok(self)
93 }
94
95 pub fn line_to_contain(&mut self, line_num: usize, text: &str) -> ProbarResult<&mut Self> {
97 let line = self.frame.line(line_num);
98 match line {
99 Some(content) if content.contains(text) => Ok(self),
100 Some(content) => {
101 let msg = format!(
102 "Expected line {} to contain '{}'\nActual: '{}'",
103 line_num, text, content
104 );
105 if self.soft_mode {
106 self.errors.push(msg);
107 Ok(self)
108 } else {
109 Err(ProbarError::AssertionFailed { message: msg })
110 }
111 }
112 None => {
113 let msg = format!(
114 "Line {} does not exist (frame has {} lines)",
115 line_num,
116 self.frame.height()
117 );
118 if self.soft_mode {
119 self.errors.push(msg);
120 Ok(self)
121 } else {
122 Err(ProbarError::AssertionFailed { message: msg })
123 }
124 }
125 }
126 }
127
128 pub fn line_to_equal(&mut self, line_num: usize, expected: &str) -> ProbarResult<&mut Self> {
130 let line = self.frame.line(line_num);
131 match line {
132 Some(content) if content == expected => Ok(self),
133 Some(content) => {
134 let msg = format!(
135 "Expected line {} to equal '{}'\nActual: '{}'",
136 line_num, expected, content
137 );
138 if self.soft_mode {
139 self.errors.push(msg);
140 Ok(self)
141 } else {
142 Err(ProbarError::AssertionFailed { message: msg })
143 }
144 }
145 None => {
146 let msg = format!(
147 "Line {} does not exist (frame has {} lines)",
148 line_num,
149 self.frame.height()
150 );
151 if self.soft_mode {
152 self.errors.push(msg);
153 Ok(self)
154 } else {
155 Err(ProbarError::AssertionFailed { message: msg })
156 }
157 }
158 }
159 }
160
161 pub fn to_have_size(&mut self, width: u16, height: u16) -> ProbarResult<&mut Self> {
163 let actual_width = self.frame.width();
164 let actual_height = self.frame.height();
165
166 if actual_width != width || actual_height != height {
167 let msg = format!(
168 "Expected frame size {}x{}, got {}x{}",
169 width, height, actual_width, actual_height
170 );
171 if self.soft_mode {
172 self.errors.push(msg);
173 } else {
174 return Err(ProbarError::AssertionFailed { message: msg });
175 }
176 }
177 Ok(self)
178 }
179
180 pub fn to_be_identical_to(&mut self, other: &TuiFrame) -> ProbarResult<&mut Self> {
182 if !self.frame.is_identical(other) {
183 let diff = self.frame.diff(other);
184 let msg = format!("Frames are not identical:\n{diff}");
185 if self.soft_mode {
186 self.errors.push(msg);
187 } else {
188 return Err(ProbarError::AssertionFailed { message: msg });
189 }
190 }
191 Ok(self)
192 }
193
194 pub fn finalize(&self) -> ProbarResult<()> {
196 if self.errors.is_empty() {
197 Ok(())
198 } else {
199 Err(ProbarError::AssertionFailed {
200 message: format!(
201 "{} assertion(s) failed:\n{}",
202 self.errors.len(),
203 self.errors.join("\n\n")
204 ),
205 })
206 }
207 }
208
209 #[must_use]
211 pub fn errors(&self) -> &[String] {
212 &self.errors
213 }
214}
215
216#[must_use]
218pub fn expect_frame(frame: &TuiFrame) -> FrameAssertion<'_> {
219 FrameAssertion::new(frame)
220}
221
222#[derive(Debug, Clone)]
227pub struct ValueTracker<T: Clone> {
228 values: Vec<(u64, T)>, name: String,
230}
231
232impl<T: Clone> ValueTracker<T> {
233 #[must_use]
235 pub fn new(name: &str) -> Self {
236 Self {
237 values: Vec::new(),
238 name: name.to_string(),
239 }
240 }
241
242 pub fn record(&mut self, timestamp_ms: u64, value: T) {
244 self.values.push((timestamp_ms, value));
245 }
246
247 #[must_use]
249 pub fn name(&self) -> &str {
250 &self.name
251 }
252
253 #[must_use]
255 pub fn values(&self) -> &[(u64, T)] {
256 &self.values
257 }
258
259 #[must_use]
261 pub fn latest(&self) -> Option<&T> {
262 self.values.last().map(|(_, v)| v)
263 }
264
265 #[must_use]
267 pub fn at(&self, index: usize) -> Option<&T> {
268 self.values.get(index).map(|(_, v)| v)
269 }
270
271 #[must_use]
273 pub fn len(&self) -> usize {
274 self.values.len()
275 }
276
277 #[must_use]
279 pub fn is_empty(&self) -> bool {
280 self.values.is_empty()
281 }
282
283 pub fn clear(&mut self) {
285 self.values.clear();
286 }
287}
288
289impl<T: Clone + PartialEq> ValueTracker<T> {
290 #[must_use]
292 pub fn has_changed(&self) -> bool {
293 if self.values.len() < 2 {
294 return false;
295 }
296 let last = &self.values[self.values.len() - 1].1;
297 let prev = &self.values[self.values.len() - 2].1;
298 last != prev
299 }
300
301 #[must_use]
303 pub fn change_count(&self) -> usize {
304 if self.values.len() < 2 {
305 return 0;
306 }
307 self.values.windows(2).filter(|w| w[0].1 != w[1].1).count()
308 }
309}
310
311impl ValueTracker<f64> {
312 #[must_use]
314 pub fn rate_of_change(&self) -> Option<f64> {
315 if self.values.len() < 2 {
316 return None;
317 }
318 let (t1, v1) = &self.values[self.values.len() - 2];
319 let (t2, v2) = &self.values[self.values.len() - 1];
320 let dt = (*t2 as f64) - (*t1 as f64);
321 if dt.abs() < f64::EPSILON {
322 return None;
323 }
324 Some((v2 - v1) / dt)
325 }
326
327 #[must_use]
329 pub fn is_increasing(&self) -> bool {
330 if self.values.len() < 2 {
331 return true;
332 }
333 self.values.windows(2).all(|w| w[1].1 >= w[0].1)
334 }
335
336 #[must_use]
338 pub fn is_decreasing(&self) -> bool {
339 if self.values.len() < 2 {
340 return true;
341 }
342 self.values.windows(2).all(|w| w[1].1 <= w[0].1)
343 }
344
345 #[must_use]
347 pub fn min(&self) -> Option<f64> {
348 self.values
349 .iter()
350 .map(|(_, v)| *v)
351 .min_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
352 }
353
354 #[must_use]
356 pub fn max(&self) -> Option<f64> {
357 self.values
358 .iter()
359 .map(|(_, v)| *v)
360 .max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
361 }
362
363 #[must_use]
365 pub fn average(&self) -> Option<f64> {
366 if self.values.is_empty() {
367 return None;
368 }
369 let sum: f64 = self.values.iter().map(|(_, v)| v).sum();
370 Some(sum / self.values.len() as f64)
371 }
372}
373
374impl ValueTracker<i64> {
375 #[must_use]
377 pub fn is_increasing(&self) -> bool {
378 if self.values.len() < 2 {
379 return true;
380 }
381 self.values.windows(2).all(|w| w[1].1 >= w[0].1)
382 }
383
384 #[must_use]
386 pub fn is_decreasing(&self) -> bool {
387 if self.values.len() < 2 {
388 return true;
389 }
390 self.values.windows(2).all(|w| w[1].1 <= w[0].1)
391 }
392}
393
394#[derive(Debug, Default)]
396pub struct MultiValueTracker {
397 trackers: HashMap<String, ValueTracker<f64>>,
398}
399
400impl MultiValueTracker {
401 #[must_use]
403 pub fn new() -> Self {
404 Self {
405 trackers: HashMap::new(),
406 }
407 }
408
409 pub fn record(&mut self, name: &str, timestamp_ms: u64, value: f64) {
411 let tracker = self
412 .trackers
413 .entry(name.to_string())
414 .or_insert_with(|| ValueTracker::new(name));
415 tracker.record(timestamp_ms, value);
416 }
417
418 #[must_use]
420 pub fn get(&self, name: &str) -> Option<&ValueTracker<f64>> {
421 self.trackers.get(name)
422 }
423
424 #[must_use]
426 pub fn names(&self) -> Vec<&str> {
427 self.trackers.keys().map(String::as_str).collect()
428 }
429
430 pub fn assert_bounds(&self, bounds: &HashMap<String, (f64, f64)>) -> ProbarResult<()> {
432 let mut errors = Vec::new();
433
434 for (name, (min, max)) in bounds {
435 if let Some(tracker) = self.trackers.get(name) {
436 for (ts, value) in tracker.values() {
437 if value < min || value > max {
438 errors.push(format!(
439 "{}: value {} at {}ms is outside bounds [{}, {}]",
440 name, value, ts, min, max
441 ));
442 }
443 }
444 }
445 }
446
447 if errors.is_empty() {
448 Ok(())
449 } else {
450 Err(ProbarError::AssertionFailed {
451 message: errors.join("\n"),
452 })
453 }
454 }
455}
456
457#[cfg(test)]
458#[allow(clippy::unwrap_used, clippy::expect_used)]
459mod tests {
460 use super::*;
461
462 mod frame_assertion_tests {
463 use super::*;
464
465 #[test]
466 fn test_to_contain_text_pass() {
467 let frame = TuiFrame::from_lines(&["Hello World", "Goodbye"]);
468 let mut assertion = expect_frame(&frame);
469 assert!(assertion.to_contain_text("World").is_ok());
470 }
471
472 #[test]
473 fn test_to_contain_text_fail() {
474 let frame = TuiFrame::from_lines(&["Hello World"]);
475 let mut assertion = expect_frame(&frame);
476 assert!(assertion.to_contain_text("Missing").is_err());
477 }
478
479 #[test]
480 fn test_not_to_contain_text_pass() {
481 let frame = TuiFrame::from_lines(&["Hello World"]);
482 let mut assertion = expect_frame(&frame);
483 assert!(assertion.not_to_contain_text("Missing").is_ok());
484 }
485
486 #[test]
487 fn test_not_to_contain_text_fail() {
488 let frame = TuiFrame::from_lines(&["Hello World"]);
489 let mut assertion = expect_frame(&frame);
490 assert!(assertion.not_to_contain_text("World").is_err());
491 }
492
493 #[test]
494 fn test_to_match_pass() {
495 let frame = TuiFrame::from_lines(&["Score: 100"]);
496 let mut assertion = expect_frame(&frame);
497 assert!(assertion.to_match(r"Score: \d+").is_ok());
498 }
499
500 #[test]
501 fn test_to_match_fail() {
502 let frame = TuiFrame::from_lines(&["Score: abc"]);
503 let mut assertion = expect_frame(&frame);
504 assert!(assertion.to_match(r"Score: \d+").is_err());
505 }
506
507 #[test]
508 fn test_line_to_contain_pass() {
509 let frame = TuiFrame::from_lines(&["First", "Second", "Third"]);
510 let mut assertion = expect_frame(&frame);
511 assert!(assertion.line_to_contain(1, "Sec").is_ok());
512 }
513
514 #[test]
515 fn test_line_to_contain_fail() {
516 let frame = TuiFrame::from_lines(&["First", "Second"]);
517 let mut assertion = expect_frame(&frame);
518 assert!(assertion.line_to_contain(0, "Second").is_err());
519 }
520
521 #[test]
522 fn test_line_to_contain_invalid_line() {
523 let frame = TuiFrame::from_lines(&["Only one line"]);
524 let mut assertion = expect_frame(&frame);
525 assert!(assertion.line_to_contain(5, "text").is_err());
526 }
527
528 #[test]
529 fn test_line_to_equal_pass() {
530 let frame = TuiFrame::from_lines(&["Exact Match"]);
531 let mut assertion = expect_frame(&frame);
532 assert!(assertion.line_to_equal(0, "Exact Match").is_ok());
533 }
534
535 #[test]
536 fn test_line_to_equal_fail() {
537 let frame = TuiFrame::from_lines(&["Exact Match"]);
538 let mut assertion = expect_frame(&frame);
539 assert!(assertion.line_to_equal(0, "Different").is_err());
540 }
541
542 #[test]
543 fn test_to_have_size_pass() {
544 let frame = TuiFrame::from_lines(&["12345", "12345"]); let mut assertion = expect_frame(&frame);
546 assert!(assertion.to_have_size(5, 2).is_ok());
547 }
548
549 #[test]
550 fn test_to_have_size_fail() {
551 let frame = TuiFrame::from_lines(&["123"]); let mut assertion = expect_frame(&frame);
553 assert!(assertion.to_have_size(10, 10).is_err());
554 }
555
556 #[test]
557 fn test_to_be_identical_to_pass() {
558 let frame1 = TuiFrame::from_lines(&["Same", "Content"]);
559 let frame2 = TuiFrame::from_lines(&["Same", "Content"]);
560 let mut assertion = expect_frame(&frame1);
561 assert!(assertion.to_be_identical_to(&frame2).is_ok());
562 }
563
564 #[test]
565 fn test_to_be_identical_to_fail() {
566 let frame1 = TuiFrame::from_lines(&["Different"]);
567 let frame2 = TuiFrame::from_lines(&["Content"]);
568 let mut assertion = expect_frame(&frame1);
569 assert!(assertion.to_be_identical_to(&frame2).is_err());
570 }
571
572 #[test]
573 fn test_soft_assertions_collect_errors() {
574 let frame = TuiFrame::from_lines(&["Hello"]);
575 let mut assertion = expect_frame(&frame).soft();
576
577 let _ = assertion.to_contain_text("Missing1");
578 let _ = assertion.to_contain_text("Missing2");
579
580 assert_eq!(assertion.errors().len(), 2);
581 assert!(assertion.finalize().is_err());
582 }
583
584 #[test]
585 fn test_soft_assertions_no_errors() {
586 let frame = TuiFrame::from_lines(&["Hello World"]);
587 let mut assertion = expect_frame(&frame).soft();
588
589 let _ = assertion.to_contain_text("Hello");
590 let _ = assertion.to_contain_text("World");
591
592 assert!(assertion.errors().is_empty());
593 assert!(assertion.finalize().is_ok());
594 }
595
596 #[test]
597 fn test_chained_assertions() {
598 let frame = TuiFrame::from_lines(&["Score: 100", "Lives: 3"]);
599 let mut assertion = expect_frame(&frame);
600
601 assert!(assertion
602 .to_contain_text("Score")
603 .and_then(|a| a.to_contain_text("Lives"))
604 .and_then(|a| a.to_match(r"\d+"))
605 .is_ok());
606 }
607 }
608
609 mod value_tracker_tests {
610 use super::*;
611
612 #[test]
613 fn test_new() {
614 let tracker: ValueTracker<f64> = ValueTracker::new("score");
615 assert_eq!(tracker.name(), "score");
616 assert!(tracker.is_empty());
617 }
618
619 #[test]
620 fn test_record_and_latest() {
621 let mut tracker: ValueTracker<i64> = ValueTracker::new("score");
622 tracker.record(0, 100);
623 tracker.record(100, 200);
624
625 assert_eq!(tracker.len(), 2);
626 assert_eq!(tracker.latest(), Some(&200));
627 }
628
629 #[test]
630 fn test_at() {
631 let mut tracker: ValueTracker<i64> = ValueTracker::new("test");
632 tracker.record(0, 10);
633 tracker.record(100, 20);
634 tracker.record(200, 30);
635
636 assert_eq!(tracker.at(0), Some(&10));
637 assert_eq!(tracker.at(1), Some(&20));
638 assert_eq!(tracker.at(2), Some(&30));
639 assert_eq!(tracker.at(3), None);
640 }
641
642 #[test]
643 fn test_has_changed() {
644 let mut tracker: ValueTracker<i64> = ValueTracker::new("test");
645
646 assert!(!tracker.has_changed()); tracker.record(0, 100);
649 assert!(!tracker.has_changed()); tracker.record(100, 100);
652 assert!(!tracker.has_changed()); tracker.record(200, 200);
655 assert!(tracker.has_changed()); }
657
658 #[test]
659 fn test_change_count() {
660 let mut tracker: ValueTracker<i64> = ValueTracker::new("test");
661 tracker.record(0, 1);
662 tracker.record(100, 1);
663 tracker.record(200, 2);
664 tracker.record(300, 2);
665 tracker.record(400, 3);
666
667 assert_eq!(tracker.change_count(), 2); }
669
670 #[test]
671 fn test_clear() {
672 let mut tracker: ValueTracker<f64> = ValueTracker::new("test");
673 tracker.record(0, 1.0);
674 tracker.record(100, 2.0);
675
676 tracker.clear();
677 assert!(tracker.is_empty());
678 }
679 }
680
681 mod value_tracker_f64_tests {
682 use super::*;
683
684 #[test]
685 fn test_rate_of_change() {
686 let mut tracker = ValueTracker::new("position");
687 tracker.record(0, 0.0);
688 tracker.record(1000, 100.0);
689
690 let rate = tracker.rate_of_change().unwrap();
691 assert!((rate - 0.1).abs() < 0.001); }
693
694 #[test]
695 fn test_rate_of_change_no_time() {
696 let mut tracker = ValueTracker::new("test");
697 tracker.record(100, 0.0);
698 tracker.record(100, 100.0); assert!(tracker.rate_of_change().is_none());
701 }
702
703 #[test]
704 fn test_is_increasing() {
705 let mut tracker = ValueTracker::new("score");
706 tracker.record(0, 0.0);
707 tracker.record(100, 10.0);
708 tracker.record(200, 20.0);
709
710 assert!(tracker.is_increasing());
711 }
712
713 #[test]
714 fn test_is_not_increasing() {
715 let mut tracker = ValueTracker::new("health");
716 tracker.record(0, 100.0);
717 tracker.record(100, 80.0);
718 tracker.record(200, 90.0);
719
720 assert!(!tracker.is_increasing());
721 }
722
723 #[test]
724 fn test_is_decreasing() {
725 let mut tracker = ValueTracker::new("health");
726 tracker.record(0, 100.0);
727 tracker.record(100, 80.0);
728 tracker.record(200, 60.0);
729
730 assert!(tracker.is_decreasing());
731 }
732
733 #[test]
734 fn test_min_max_average() {
735 let mut tracker = ValueTracker::new("test");
736 tracker.record(0, 10.0);
737 tracker.record(100, 20.0);
738 tracker.record(200, 30.0);
739
740 assert!((tracker.min().unwrap() - 10.0).abs() < f64::EPSILON);
741 assert!((tracker.max().unwrap() - 30.0).abs() < f64::EPSILON);
742 assert!((tracker.average().unwrap() - 20.0).abs() < f64::EPSILON);
743 }
744
745 #[test]
746 fn test_empty_stats() {
747 let tracker: ValueTracker<f64> = ValueTracker::new("empty");
748 assert!(tracker.min().is_none());
749 assert!(tracker.max().is_none());
750 assert!(tracker.average().is_none());
751 assert!(tracker.rate_of_change().is_none());
752 }
753 }
754
755 mod multi_value_tracker_tests {
756 use super::*;
757
758 #[test]
759 fn test_record_and_get() {
760 let mut multi = MultiValueTracker::new();
761 multi.record("score", 0, 100.0);
762 multi.record("health", 0, 100.0);
763
764 assert!(multi.get("score").is_some());
765 assert!(multi.get("health").is_some());
766 assert!(multi.get("missing").is_none());
767 }
768
769 #[test]
770 fn test_names() {
771 let mut multi = MultiValueTracker::new();
772 multi.record("a", 0, 1.0);
773 multi.record("b", 0, 2.0);
774
775 let names = multi.names();
776 assert_eq!(names.len(), 2);
777 assert!(names.contains(&"a"));
778 assert!(names.contains(&"b"));
779 }
780
781 #[test]
782 fn test_assert_bounds_pass() {
783 let mut multi = MultiValueTracker::new();
784 multi.record("health", 0, 100.0);
785 multi.record("health", 100, 80.0);
786
787 let mut bounds = HashMap::new();
788 bounds.insert("health".to_string(), (0.0, 100.0));
789
790 assert!(multi.assert_bounds(&bounds).is_ok());
791 }
792
793 #[test]
794 fn test_assert_bounds_fail() {
795 let mut multi = MultiValueTracker::new();
796 multi.record("health", 0, 150.0); let mut bounds = HashMap::new();
799 bounds.insert("health".to_string(), (0.0, 100.0));
800
801 assert!(multi.assert_bounds(&bounds).is_err());
802 }
803
804 #[test]
805 fn test_default_implementation() {
806 let multi = MultiValueTracker::default();
807 assert!(multi.names().is_empty());
808 }
809
810 #[test]
811 fn test_assert_bounds_below_min() {
812 let mut multi = MultiValueTracker::new();
813 multi.record("health", 0, -10.0); let mut bounds = HashMap::new();
816 bounds.insert("health".to_string(), (0.0, 100.0));
817
818 let result = multi.assert_bounds(&bounds);
819 assert!(result.is_err());
820 let err = result.unwrap_err();
821 match err {
822 ProbarError::AssertionFailed { message } => {
823 assert!(message.contains("outside bounds"));
824 }
825 _ => panic!("Expected AssertionFailed error"),
826 }
827 }
828
829 #[test]
830 fn test_assert_bounds_missing_tracker() {
831 let multi = MultiValueTracker::new();
832 let mut bounds = HashMap::new();
833 bounds.insert("nonexistent".to_string(), (0.0, 100.0));
834
835 assert!(multi.assert_bounds(&bounds).is_ok());
837 }
838
839 #[test]
840 fn test_record_multiple_values_same_tracker() {
841 let mut multi = MultiValueTracker::new();
842 multi.record("score", 0, 100.0);
843 multi.record("score", 100, 200.0);
844 multi.record("score", 200, 300.0);
845
846 let tracker = multi.get("score").unwrap();
847 assert_eq!(tracker.len(), 3);
848 assert_eq!(*tracker.latest().unwrap(), 300.0);
849 }
850 }
851
852 mod soft_assertion_edge_cases {
853 use super::*;
854
855 #[test]
856 fn test_soft_not_to_contain_text_fail() {
857 let frame = TuiFrame::from_lines(&["Hello World"]);
858 let mut assertion = expect_frame(&frame).soft();
859
860 let _ = assertion.not_to_contain_text("World");
861 assert_eq!(assertion.errors().len(), 1);
862 assert!(assertion.errors()[0].contains("NOT to contain"));
863 }
864
865 #[test]
866 fn test_soft_to_match_fail() {
867 let frame = TuiFrame::from_lines(&["No numbers here"]);
868 let mut assertion = expect_frame(&frame).soft();
869
870 let _ = assertion.to_match(r"\d+");
871 assert_eq!(assertion.errors().len(), 1);
872 assert!(assertion.errors()[0].contains("match pattern"));
873 }
874
875 #[test]
876 fn test_soft_to_have_size_fail() {
877 let frame = TuiFrame::from_lines(&["Short"]);
878 let mut assertion = expect_frame(&frame).soft();
879
880 let _ = assertion.to_have_size(100, 100);
881 assert_eq!(assertion.errors().len(), 1);
882 assert!(assertion.errors()[0].contains("Expected frame size"));
883 }
884
885 #[test]
886 fn test_soft_to_be_identical_to_fail() {
887 let frame1 = TuiFrame::from_lines(&["Frame A"]);
888 let frame2 = TuiFrame::from_lines(&["Frame B"]);
889 let mut assertion = expect_frame(&frame1).soft();
890
891 let _ = assertion.to_be_identical_to(&frame2);
892 assert_eq!(assertion.errors().len(), 1);
893 assert!(assertion.errors()[0].contains("not identical"));
894 }
895
896 #[test]
897 fn test_soft_line_to_equal_nonexistent() {
898 let frame = TuiFrame::from_lines(&["Only line"]);
899 let mut assertion = expect_frame(&frame).soft();
900
901 let _ = assertion.line_to_equal(10, "anything");
902 assert_eq!(assertion.errors().len(), 1);
903 assert!(assertion.errors()[0].contains("does not exist"));
904 }
905
906 #[test]
907 fn test_soft_line_to_equal_mismatch() {
908 let frame = TuiFrame::from_lines(&["Actual"]);
909 let mut assertion = expect_frame(&frame).soft();
910
911 let _ = assertion.line_to_equal(0, "Expected");
912 assert_eq!(assertion.errors().len(), 1);
913 assert!(assertion.errors()[0].contains("Expected line 0 to equal"));
914 }
915
916 #[test]
917 fn test_soft_line_to_contain_nonexistent() {
918 let frame = TuiFrame::from_lines(&["Only line"]);
919 let mut assertion = expect_frame(&frame).soft();
920
921 let _ = assertion.line_to_contain(10, "anything");
922 assert_eq!(assertion.errors().len(), 1);
923 assert!(assertion.errors()[0].contains("does not exist"));
924 }
925
926 #[test]
927 fn test_soft_line_to_contain_mismatch() {
928 let frame = TuiFrame::from_lines(&["Actual content"]);
929 let mut assertion = expect_frame(&frame).soft();
930
931 let _ = assertion.line_to_contain(0, "Missing");
932 assert_eq!(assertion.errors().len(), 1);
933 assert!(assertion.errors()[0].contains("Expected line 0 to contain"));
934 }
935
936 #[test]
937 fn test_soft_multiple_mixed_errors() {
938 let frame = TuiFrame::from_lines(&["Hello"]);
939 let mut assertion = expect_frame(&frame).soft();
940
941 let _ = assertion.to_contain_text("Missing1");
942 let _ = assertion.not_to_contain_text("Hello");
943 let _ = assertion.to_have_size(999, 999);
944
945 assert_eq!(assertion.errors().len(), 3);
946 let result = assertion.finalize();
947 assert!(result.is_err());
948 let err_msg = format!("{:?}", result.unwrap_err());
949 assert!(err_msg.contains("3 assertion(s) failed"));
950 }
951 }
952
953 mod to_match_edge_cases {
954 use super::*;
955
956 #[test]
957 fn test_to_match_invalid_regex() {
958 let frame = TuiFrame::from_lines(&["Test"]);
959 let mut assertion = expect_frame(&frame);
960
961 let result = assertion.to_match("[invalid");
963 assert!(result.is_err());
964 }
965 }
966
967 mod value_tracker_i64_tests {
968 use super::*;
969
970 #[test]
971 fn test_i64_is_increasing() {
972 let mut tracker: ValueTracker<i64> = ValueTracker::new("score");
973 tracker.record(0, 10);
974 tracker.record(100, 20);
975 tracker.record(200, 30);
976
977 assert!(tracker.is_increasing());
978 }
979
980 #[test]
981 fn test_i64_is_not_increasing() {
982 let mut tracker: ValueTracker<i64> = ValueTracker::new("health");
983 tracker.record(0, 100);
984 tracker.record(100, 50);
985 tracker.record(200, 80);
986
987 assert!(!tracker.is_increasing());
988 }
989
990 #[test]
991 fn test_i64_is_decreasing() {
992 let mut tracker: ValueTracker<i64> = ValueTracker::new("health");
993 tracker.record(0, 100);
994 tracker.record(100, 80);
995 tracker.record(200, 60);
996
997 assert!(tracker.is_decreasing());
998 }
999
1000 #[test]
1001 fn test_i64_is_not_decreasing() {
1002 let mut tracker: ValueTracker<i64> = ValueTracker::new("score");
1003 tracker.record(0, 10);
1004 tracker.record(100, 30);
1005 tracker.record(200, 20);
1006
1007 assert!(!tracker.is_decreasing());
1008 }
1009
1010 #[test]
1011 fn test_i64_single_value_is_increasing_and_decreasing() {
1012 let mut tracker: ValueTracker<i64> = ValueTracker::new("single");
1013 tracker.record(0, 50);
1014
1015 assert!(tracker.is_increasing());
1017 assert!(tracker.is_decreasing());
1018 }
1019
1020 #[test]
1021 fn test_i64_empty_is_increasing_and_decreasing() {
1022 let tracker: ValueTracker<i64> = ValueTracker::new("empty");
1023
1024 assert!(tracker.is_increasing());
1026 assert!(tracker.is_decreasing());
1027 }
1028
1029 #[test]
1030 fn test_i64_equal_values_is_both() {
1031 let mut tracker: ValueTracker<i64> = ValueTracker::new("constant");
1032 tracker.record(0, 50);
1033 tracker.record(100, 50);
1034 tracker.record(200, 50);
1035
1036 assert!(tracker.is_increasing());
1038 assert!(tracker.is_decreasing());
1039 }
1040 }
1041
1042 mod value_tracker_additional_tests {
1043 use super::*;
1044
1045 #[test]
1046 fn test_values_accessor() {
1047 let mut tracker: ValueTracker<f64> = ValueTracker::new("test");
1048 tracker.record(0, 1.0);
1049 tracker.record(100, 2.0);
1050 tracker.record(200, 3.0);
1051
1052 let values = tracker.values();
1053 assert_eq!(values.len(), 3);
1054 assert_eq!(values[0], (0, 1.0));
1055 assert_eq!(values[1], (100, 2.0));
1056 assert_eq!(values[2], (200, 3.0));
1057 }
1058
1059 #[test]
1060 fn test_latest_empty() {
1061 let tracker: ValueTracker<f64> = ValueTracker::new("empty");
1062 assert!(tracker.latest().is_none());
1063 }
1064
1065 #[test]
1066 fn test_change_count_empty() {
1067 let tracker: ValueTracker<i64> = ValueTracker::new("empty");
1068 assert_eq!(tracker.change_count(), 0);
1069 }
1070
1071 #[test]
1072 fn test_change_count_single_value() {
1073 let mut tracker: ValueTracker<i64> = ValueTracker::new("single");
1074 tracker.record(0, 100);
1075 assert_eq!(tracker.change_count(), 0);
1076 }
1077
1078 #[test]
1079 fn test_change_count_no_changes() {
1080 let mut tracker: ValueTracker<i64> = ValueTracker::new("constant");
1081 tracker.record(0, 100);
1082 tracker.record(100, 100);
1083 tracker.record(200, 100);
1084 assert_eq!(tracker.change_count(), 0);
1085 }
1086
1087 #[test]
1088 fn test_f64_single_value_is_increasing_and_decreasing() {
1089 let mut tracker: ValueTracker<f64> = ValueTracker::new("single");
1090 tracker.record(0, 50.0);
1091
1092 assert!(tracker.is_increasing());
1093 assert!(tracker.is_decreasing());
1094 }
1095
1096 #[test]
1097 fn test_f64_empty_is_increasing_and_decreasing() {
1098 let tracker: ValueTracker<f64> = ValueTracker::new("empty");
1099
1100 assert!(tracker.is_increasing());
1101 assert!(tracker.is_decreasing());
1102 }
1103
1104 #[test]
1105 fn test_f64_is_not_decreasing() {
1106 let mut tracker: ValueTracker<f64> = ValueTracker::new("up_down");
1107 tracker.record(0, 10.0);
1108 tracker.record(100, 20.0);
1109 tracker.record(200, 15.0);
1110
1111 assert!(!tracker.is_decreasing());
1112 }
1113
1114 #[test]
1115 fn test_rate_of_change_single_value() {
1116 let mut tracker: ValueTracker<f64> = ValueTracker::new("single");
1117 tracker.record(0, 100.0);
1118 assert!(tracker.rate_of_change().is_none());
1119 }
1120
1121 #[test]
1122 fn test_rate_of_change_negative() {
1123 let mut tracker: ValueTracker<f64> = ValueTracker::new("decreasing");
1124 tracker.record(0, 100.0);
1125 tracker.record(1000, 0.0);
1126
1127 let rate = tracker.rate_of_change().unwrap();
1128 assert!((rate - (-0.1)).abs() < 0.001);
1129 }
1130 }
1131
1132 mod frame_assertion_error_messages {
1133 use super::*;
1134
1135 #[test]
1136 fn test_to_contain_text_error_shows_content() {
1137 let frame = TuiFrame::from_lines(&["Line one", "Line two"]);
1138 let mut assertion = expect_frame(&frame);
1139
1140 let result = assertion.to_contain_text("NotFound");
1141 assert!(result.is_err());
1142 let err = result.unwrap_err();
1143 match err {
1144 ProbarError::AssertionFailed { message } => {
1145 assert!(message.contains("NotFound"));
1146 assert!(message.contains("Frame content:"));
1147 assert!(message.contains("Line one"));
1148 }
1149 _ => panic!("Expected AssertionFailed error"),
1150 }
1151 }
1152
1153 #[test]
1154 fn test_not_to_contain_text_error_shows_content() {
1155 let frame = TuiFrame::from_lines(&["Hello World"]);
1156 let mut assertion = expect_frame(&frame);
1157
1158 let result = assertion.not_to_contain_text("Hello");
1159 assert!(result.is_err());
1160 let err = result.unwrap_err();
1161 match err {
1162 ProbarError::AssertionFailed { message } => {
1163 assert!(message.contains("NOT to contain"));
1164 assert!(message.contains("Hello"));
1165 }
1166 _ => panic!("Expected AssertionFailed error"),
1167 }
1168 }
1169
1170 #[test]
1171 fn test_to_match_error_shows_pattern() {
1172 let frame = TuiFrame::from_lines(&["No numbers"]);
1173 let mut assertion = expect_frame(&frame);
1174
1175 let result = assertion.to_match(r"\d+");
1176 assert!(result.is_err());
1177 let err = result.unwrap_err();
1178 match err {
1179 ProbarError::AssertionFailed { message } => {
1180 assert!(message.contains(r"\d+"));
1181 assert!(message.contains("No numbers"));
1182 }
1183 _ => panic!("Expected AssertionFailed error"),
1184 }
1185 }
1186
1187 #[test]
1188 fn test_line_to_contain_error_shows_actual() {
1189 let frame = TuiFrame::from_lines(&["Actual content"]);
1190 let mut assertion = expect_frame(&frame);
1191
1192 let result = assertion.line_to_contain(0, "Missing");
1193 assert!(result.is_err());
1194 let err = result.unwrap_err();
1195 match err {
1196 ProbarError::AssertionFailed { message } => {
1197 assert!(message.contains("Expected line 0"));
1198 assert!(message.contains("Missing"));
1199 assert!(message.contains("Actual content"));
1200 }
1201 _ => panic!("Expected AssertionFailed error"),
1202 }
1203 }
1204
1205 #[test]
1206 fn test_line_to_equal_error_shows_actual() {
1207 let frame = TuiFrame::from_lines(&["Actual"]);
1208 let mut assertion = expect_frame(&frame);
1209
1210 let result = assertion.line_to_equal(0, "Expected");
1211 assert!(result.is_err());
1212 let err = result.unwrap_err();
1213 match err {
1214 ProbarError::AssertionFailed { message } => {
1215 assert!(message.contains("Expected line 0 to equal"));
1216 assert!(message.contains("Expected"));
1217 assert!(message.contains("Actual"));
1218 }
1219 _ => panic!("Expected AssertionFailed error"),
1220 }
1221 }
1222
1223 #[test]
1224 fn test_line_to_equal_nonexistent_line_error() {
1225 let frame = TuiFrame::from_lines(&["Only line"]);
1226 let mut assertion = expect_frame(&frame);
1227
1228 let result = assertion.line_to_equal(5, "Anything");
1229 assert!(result.is_err());
1230 let err = result.unwrap_err();
1231 match err {
1232 ProbarError::AssertionFailed { message } => {
1233 assert!(message.contains("Line 5 does not exist"));
1234 assert!(message.contains("1 lines"));
1235 }
1236 _ => panic!("Expected AssertionFailed error"),
1237 }
1238 }
1239
1240 #[test]
1241 fn test_to_have_size_error_shows_dimensions() {
1242 let frame = TuiFrame::from_lines(&["Short"]);
1243 let mut assertion = expect_frame(&frame);
1244
1245 let result = assertion.to_have_size(100, 50);
1246 assert!(result.is_err());
1247 let err = result.unwrap_err();
1248 match err {
1249 ProbarError::AssertionFailed { message } => {
1250 assert!(message.contains("100x50"));
1251 assert!(message.contains("5x1"));
1252 }
1253 _ => panic!("Expected AssertionFailed error"),
1254 }
1255 }
1256
1257 #[test]
1258 fn test_to_be_identical_to_error_shows_diff() {
1259 let frame1 = TuiFrame::from_lines(&["Line A"]);
1260 let frame2 = TuiFrame::from_lines(&["Line B"]);
1261 let mut assertion = expect_frame(&frame1);
1262
1263 let result = assertion.to_be_identical_to(&frame2);
1264 assert!(result.is_err());
1265 let err = result.unwrap_err();
1266 match err {
1267 ProbarError::AssertionFailed { message } => {
1268 assert!(message.contains("not identical"));
1269 }
1270 _ => panic!("Expected AssertionFailed error"),
1271 }
1272 }
1273 }
1274
1275 mod frame_assertion_debug {
1276 use super::*;
1277
1278 #[test]
1279 fn test_frame_assertion_debug() {
1280 let frame = TuiFrame::from_lines(&["Test"]);
1281 let assertion = expect_frame(&frame);
1282 let debug = format!("{:?}", assertion);
1283 assert!(debug.contains("FrameAssertion"));
1284 }
1285 }
1286
1287 mod value_tracker_clone {
1288 use super::*;
1289
1290 #[test]
1291 fn test_value_tracker_clone() {
1292 let mut tracker: ValueTracker<f64> = ValueTracker::new("test");
1293 tracker.record(0, 1.0);
1294 tracker.record(100, 2.0);
1295
1296 let cloned = tracker.clone();
1297 assert_eq!(cloned.name(), tracker.name());
1298 assert_eq!(cloned.len(), tracker.len());
1299 assert_eq!(cloned.latest(), tracker.latest());
1300 }
1301
1302 #[test]
1303 fn test_value_tracker_debug() {
1304 let mut tracker: ValueTracker<i64> = ValueTracker::new("debug_test");
1305 tracker.record(0, 42);
1306 let debug = format!("{:?}", tracker);
1307 assert!(debug.contains("ValueTracker"));
1308 assert!(debug.contains("debug_test"));
1309 }
1310 }
1311
1312 mod multi_value_tracker_debug {
1313 use super::*;
1314
1315 #[test]
1316 fn test_multi_value_tracker_debug() {
1317 let mut multi = MultiValueTracker::new();
1318 multi.record("test", 0, 1.0);
1319 let debug = format!("{:?}", multi);
1320 assert!(debug.contains("MultiValueTracker"));
1321 }
1322 }
1323}