1use std::collections::HashMap;
2use std::sync::Arc;
3
4use bytes::BytesMut;
5use regex::Regex;
6use tokio::sync::Mutex;
7use tokio::sync::Notify;
8
9use crate::observe::event_log::BufferSnapshot;
10use crate::vm::context::FailPattern;
11
12#[derive(Debug, Clone, PartialEq, Eq)]
16pub struct FailPatternHit {
17 pub(crate) pattern: String,
19 pub(crate) matched_text: String,
21}
22
23const BUFFER_PREFIX_LEN: usize = 40;
26const BUFFER_SUFFIX_LEN: usize = 40;
27pub(crate) const BUFFER_TAIL_LEN: usize = 80;
28
29fn truncate_before(s: &str, max: usize) -> String {
32 if s.len() <= max {
33 s.to_string()
34 } else {
35 let start = s.ceil_char_boundary(s.len() - max);
36 format!("...{}", &s[start..])
37 }
38}
39
40fn truncate_after(s: &str, max: usize) -> String {
41 if s.len() <= max {
42 s.to_string()
43 } else {
44 let end = s.floor_char_boundary(max);
45 format!("{}...", &s[..end])
46 }
47}
48
49pub(crate) fn regex_error_summary(e: ®ex::Error) -> String {
50 let full = e.to_string();
51 full.lines()
52 .rev()
53 .find(|l| !l.is_empty())
54 .unwrap_or(&full)
55 .strip_prefix("error: ")
56 .unwrap_or(&full)
57 .to_string()
58}
59
60pub trait MatchKind {}
64
65#[derive(Debug, Clone, PartialEq, Eq)]
67pub struct LiteralMatch(pub String);
68impl MatchKind for LiteralMatch {}
69
70#[derive(Debug, Clone, PartialEq, Eq)]
72pub struct RegexMatch(pub HashMap<String, String>);
73impl MatchKind for RegexMatch {}
74
75#[derive(Debug, Clone)]
77pub struct Match<T: MatchKind> {
78 pub start: usize,
80 pub end: usize,
82 pub consumed: usize,
84 pub value: T,
86}
87
88struct BufferInner {
91 data: BytesMut,
92 base: usize,
93}
94
95#[derive(Clone)]
96pub struct OutputBuffer {
97 inner: Arc<Mutex<BufferInner>>,
98 pub(crate) notify: Arc<Notify>,
99 recv_pending: Arc<Mutex<BytesMut>>,
100}
101
102impl Default for OutputBuffer {
103 fn default() -> Self {
104 Self::new()
105 }
106}
107
108impl OutputBuffer {
109 pub fn new() -> Self {
110 Self {
111 inner: Arc::new(Mutex::new(BufferInner {
112 data: BytesMut::new(),
113 base: 0,
114 })),
115 notify: Arc::new(Notify::new()),
116 recv_pending: Arc::new(Mutex::new(BytesMut::new())),
117 }
118 }
119
120 pub async fn append(&self, bytes: &[u8]) {
121 self.inner.lock().await.data.extend_from_slice(bytes);
122 self.recv_pending.lock().await.extend_from_slice(bytes);
123 self.notify.notify_waiters();
124 }
125
126 pub async fn drain_recv(&self) -> Option<String> {
127 let mut pending = self.recv_pending.lock().await;
128 if pending.is_empty() {
129 return None;
130 }
131 let bytes = pending.split();
132 Some(String::from_utf8_lossy(&bytes).to_string())
133 }
134
135 pub async fn consume_literal(
138 &self,
139 needle: &str,
140 ) -> Option<(Match<LiteralMatch>, BufferSnapshot)> {
141 let mut inner = self.inner.lock().await;
142 let text = String::from_utf8_lossy(&inner.data);
143 let pos = text.find(needle)?;
144 let end_pos = pos + needle.len();
145
146 let before_raw = &text[..pos];
147 let after_raw = &text[end_pos..];
148 let snapshot = BufferSnapshot::Match {
149 before: truncate_before(before_raw, BUFFER_PREFIX_LEN),
150 matched: needle.to_string(),
151 after: truncate_after(after_raw, BUFFER_SUFFIX_LEN),
152 };
153
154 let consumed = end_pos;
155 let m = Match {
156 start: inner.base + pos,
157 end: inner.base + end_pos,
158 consumed,
159 value: LiteralMatch(needle.to_string()),
160 };
161
162 drop(text);
163 let _ = inner.data.split_to(end_pos);
164 inner.base += end_pos;
165
166 Some((m, snapshot))
167 }
168
169 pub async fn consume_regex(&self, re: &Regex) -> Option<(Match<RegexMatch>, BufferSnapshot)> {
176 let mut inner = self.inner.lock().await;
177 let text = String::from_utf8_lossy(&inner.data);
178 let cap = re.captures(&text)?;
179 let whole = cap.get(0)?;
180 let pos = whole.start();
181 let end_pos = whole.end();
182
183 if is_partial_line_match(re, end_pos, &text) {
184 return None;
185 }
186
187 let matched_str = whole.as_str().to_string();
188
189 let before_raw = &text[..pos];
190 let after_raw = &text[end_pos..];
191 let snapshot = BufferSnapshot::Match {
192 before: truncate_before(before_raw, BUFFER_PREFIX_LEN),
193 matched: matched_str.clone(),
194 after: truncate_after(after_raw, BUFFER_SUFFIX_LEN),
195 };
196
197 let mut captures = HashMap::new();
198 for i in 0..cap.len() {
199 if let Some(m) = cap.get(i) {
200 captures.insert(i.to_string(), m.as_str().to_string());
201 }
202 }
203
204 let consumed = end_pos;
205 let m = Match {
206 start: inner.base + pos,
207 end: inner.base + end_pos,
208 consumed,
209 value: RegexMatch(captures),
210 };
211
212 drop(text);
213 let _ = inner.data.split_to(end_pos);
214 inner.base += end_pos;
215
216 Some((m, snapshot))
217 }
218
219 pub async fn fail_check_consume_literal(
222 &self,
223 needle: &str,
224 fail_pattern: Option<&FailPattern>,
225 ) -> Result<Option<(Match<LiteralMatch>, BufferSnapshot)>, FailPatternHit> {
226 let mut inner = self.inner.lock().await;
227 let text = String::from_utf8_lossy(&inner.data);
228
229 if let Some(fp) = fail_pattern
231 && let Some(hit) = check_fail_in_buffer(&text, fp)
232 {
233 return Err(hit);
234 }
235
236 let Some(pos) = text.find(needle) else {
238 return Ok(None);
239 };
240 let end_pos = pos + needle.len();
241
242 let before_raw = &text[..pos];
243 let after_raw = &text[end_pos..];
244 let snapshot = BufferSnapshot::Match {
245 before: truncate_before(before_raw, BUFFER_PREFIX_LEN),
246 matched: needle.to_string(),
247 after: truncate_after(after_raw, BUFFER_SUFFIX_LEN),
248 };
249
250 let consumed = end_pos;
251 let m = Match {
252 start: inner.base + pos,
253 end: inner.base + end_pos,
254 consumed,
255 value: LiteralMatch(needle.to_string()),
256 };
257
258 drop(text);
259 let _ = inner.data.split_to(end_pos);
260 inner.base += end_pos;
261
262 Ok(Some((m, snapshot)))
263 }
264
265 pub async fn fail_check_consume_regex(
268 &self,
269 re: &Regex,
270 fail_pattern: Option<&FailPattern>,
271 ) -> Result<Option<(Match<RegexMatch>, BufferSnapshot)>, FailPatternHit> {
272 let mut inner = self.inner.lock().await;
273 let text = String::from_utf8_lossy(&inner.data);
274
275 if let Some(fp) = fail_pattern
277 && let Some(hit) = check_fail_in_buffer(&text, fp)
278 {
279 return Err(hit);
280 }
281
282 let Some(cap) = re.captures(&text) else {
284 return Ok(None);
285 };
286 let Some(whole) = cap.get(0) else {
287 return Ok(None);
288 };
289 let pos = whole.start();
290 let end_pos = whole.end();
291
292 if is_partial_line_match(re, end_pos, &text) {
293 return Ok(None);
294 }
295
296 let matched_str = whole.as_str().to_string();
297
298 let before_raw = &text[..pos];
299 let after_raw = &text[end_pos..];
300 let snapshot = BufferSnapshot::Match {
301 before: truncate_before(before_raw, BUFFER_PREFIX_LEN),
302 matched: matched_str.clone(),
303 after: truncate_after(after_raw, BUFFER_SUFFIX_LEN),
304 };
305
306 let mut captures = HashMap::new();
307 for i in 0..cap.len() {
308 if let Some(m) = cap.get(i) {
309 captures.insert(i.to_string(), m.as_str().to_string());
310 }
311 }
312
313 let consumed = end_pos;
314 let m = Match {
315 start: inner.base + pos,
316 end: inner.base + end_pos,
317 consumed,
318 value: RegexMatch(captures),
319 };
320
321 drop(text);
322 let _ = inner.data.split_to(end_pos);
323 inner.base += end_pos;
324
325 Ok(Some((m, snapshot)))
326 }
327
328 pub async fn check_fail_pattern(
330 &self,
331 fail_pattern: Option<&FailPattern>,
332 ) -> Option<FailPatternHit> {
333 let fp = fail_pattern?;
334 let inner = self.inner.lock().await;
335 let text = String::from_utf8_lossy(&inner.data);
336 check_fail_in_buffer(&text, fp)
337 }
338
339 pub async fn clear(&self) {
341 let mut inner = self.inner.lock().await;
342 let len = inner.data.len();
343 let _ = inner.data.split_to(len);
344 inner.base += len;
345 }
346
347 pub async fn snapshot_tail(&self, n: usize) -> BufferSnapshot {
349 let inner = self.inner.lock().await;
350 let text = String::from_utf8_lossy(&inner.data);
351 BufferSnapshot::Tail {
352 content: truncate_before(&text, n),
353 }
354 }
355
356 pub async fn remaining(&self) -> Vec<u8> {
358 let inner = self.inner.lock().await;
359 inner.data.to_vec()
360 }
361}
362
363fn is_partial_line_match(re: &Regex, match_end: usize, text: &str) -> bool {
371 re.as_str().ends_with('$') && match_end == text.len() && !text.ends_with('\n')
372}
373
374fn check_fail_in_buffer(text: &str, pattern: &FailPattern) -> Option<FailPatternHit> {
376 match pattern {
377 FailPattern::Regex(re) => {
378 let m = re.find(text)?;
379 Some(FailPatternHit {
380 pattern: re.as_str().to_string(),
381 matched_text: m.as_str().to_string(),
382 })
383 }
384 FailPattern::Literal(s) => {
385 text.find(s.as_str())?;
386 Some(FailPatternHit {
387 pattern: s.clone(),
388 matched_text: s.clone(),
389 })
390 }
391 }
392}
393
394#[cfg(test)]
397mod tests {
398 use super::*;
399 use regex::RegexBuilder;
400
401 #[test]
404 fn truncate_before_short_string_unchanged() {
405 assert_eq!(truncate_before("hello", 10), "hello");
406 }
407
408 #[test]
409 fn truncate_before_exact_length_unchanged() {
410 assert_eq!(truncate_before("hello", 5), "hello");
411 }
412
413 #[test]
414 fn truncate_before_keeps_last_n_chars() {
415 assert_eq!(truncate_before("hello world", 5), "...world");
416 }
417
418 #[test]
419 fn truncate_before_empty_string() {
420 assert_eq!(truncate_before("", 5), "");
421 }
422
423 #[test]
424 fn truncate_before_max_zero() {
425 assert_eq!(truncate_before("hello", 0), "...");
426 }
427
428 #[test]
431 fn truncate_after_short_string_unchanged() {
432 assert_eq!(truncate_after("hello", 10), "hello");
433 }
434
435 #[test]
436 fn truncate_after_exact_length_unchanged() {
437 assert_eq!(truncate_after("hello", 5), "hello");
438 }
439
440 #[test]
441 fn truncate_after_keeps_first_n_chars() {
442 assert_eq!(truncate_after("hello world", 5), "hello...");
443 }
444
445 #[test]
446 fn truncate_after_empty_string() {
447 assert_eq!(truncate_after("", 5), "");
448 }
449
450 #[test]
451 fn truncate_after_max_zero() {
452 assert_eq!(truncate_after("hello", 0), "...");
453 }
454
455 #[test]
458 #[allow(clippy::invalid_regex)]
459 fn regex_error_summary_extracts_last_line() {
460 let err = Regex::new("(unclosed").unwrap_err();
461 let summary = regex_error_summary(&err);
462 assert!(!summary.is_empty());
463 assert!(!summary.starts_with("error: "));
464 }
465
466 #[test]
467 #[allow(clippy::invalid_regex)]
468 fn regex_error_summary_strips_error_prefix() {
469 let err = Regex::new("[invalid").unwrap_err();
470 let summary = regex_error_summary(&err);
471 assert!(!summary.starts_with("error: "));
472 }
473
474 #[tokio::test]
477 async fn output_buffer_new_is_empty() {
478 let buf = OutputBuffer::new();
479 assert!(buf.remaining().await.is_empty());
480 }
481
482 #[tokio::test]
485 async fn output_buffer_append_and_remaining() {
486 let buf = OutputBuffer::new();
487 buf.append(b"hello ").await;
488 buf.append(b"world").await;
489 assert_eq!(buf.remaining().await, b"hello world");
490 }
491
492 #[tokio::test]
493 async fn output_buffer_append_empty_bytes() {
494 let buf = OutputBuffer::new();
495 buf.append(b"").await;
496 assert!(buf.remaining().await.is_empty());
497 }
498
499 #[tokio::test]
502 async fn consume_literal_basic() {
503 let buf = OutputBuffer::new();
504 buf.append(b"hello world").await;
505 let (m, snapshot) = buf.consume_literal("hello").await.unwrap();
506 assert_eq!(m.start, 0);
507 assert_eq!(m.end, 5);
508 assert_eq!(m.consumed, 5);
509 assert_eq!(m.value.0, "hello");
510 assert!(matches!(snapshot, BufferSnapshot::Match { .. }));
511 assert_eq!(buf.remaining().await, b" world");
513 }
514
515 #[tokio::test]
516 async fn consume_literal_drains_up_to_match_end() {
517 let buf = OutputBuffer::new();
518 buf.append(b"prefix MATCH suffix").await;
519 let (m, _) = buf.consume_literal("MATCH").await.unwrap();
520 assert_eq!(m.start, 7);
521 assert_eq!(m.end, 12);
522 assert_eq!(m.consumed, 12);
523 assert_eq!(buf.remaining().await, b" suffix");
524 }
525
526 #[tokio::test]
527 async fn consume_literal_not_found() {
528 let buf = OutputBuffer::new();
529 buf.append(b"hello world").await;
530 assert!(buf.consume_literal("xyz").await.is_none());
531 assert_eq!(buf.remaining().await, b"hello world");
533 }
534
535 #[tokio::test]
536 async fn consume_literal_absolute_offsets_after_drain() {
537 let buf = OutputBuffer::new();
538 buf.append(b"aaa bbb ccc").await;
539 let (m1, _) = buf.consume_literal("aaa").await.unwrap();
541 assert_eq!(m1.start, 0);
542 assert_eq!(m1.end, 3);
543 let (m2, _) = buf.consume_literal("bbb").await.unwrap();
545 assert_eq!(m2.start, 4);
546 assert_eq!(m2.end, 7);
547 assert_eq!(buf.remaining().await, b" ccc");
549 }
550
551 #[tokio::test]
552 async fn consume_literal_snapshot_has_truncated_context() {
553 let buf = OutputBuffer::new();
554 buf.append(b"before MATCH after").await;
555 let (_, snapshot) = buf.consume_literal("MATCH").await.unwrap();
556 match snapshot {
557 BufferSnapshot::Match {
558 before,
559 matched,
560 after,
561 } => {
562 assert_eq!(before, "before ");
563 assert_eq!(matched, "MATCH");
564 assert_eq!(after, " after");
565 }
566 _ => panic!("expected BufferSnapshot::Match"),
567 }
568 }
569
570 #[tokio::test]
573 async fn consume_regex_basic() {
574 let buf = OutputBuffer::new();
575 buf.append(b"abc 123 def").await;
576 let re = Regex::new(r"\d+").unwrap();
577 let (m, _) = buf.consume_regex(&re).await.unwrap();
578 assert_eq!(m.start, 4);
579 assert_eq!(m.end, 7);
580 assert_eq!(m.value.0.get("0").unwrap(), "123");
581 assert_eq!(buf.remaining().await, b" def");
582 }
583
584 #[tokio::test]
585 async fn consume_regex_with_captures() {
586 let buf = OutputBuffer::new();
587 buf.append(b"name: Alice age: 30\n").await;
588 let re = Regex::new(r"name: (\w+) age: (\d+)").unwrap();
589 let (m, _) = buf.consume_regex(&re).await.unwrap();
590 assert_eq!(m.start, 0);
591 assert_eq!(m.end, 19);
592 assert_eq!(m.value.0.get("0").unwrap(), "name: Alice age: 30");
593 assert_eq!(m.value.0.get("1").unwrap(), "Alice");
594 assert_eq!(m.value.0.get("2").unwrap(), "30");
595 }
596
597 #[tokio::test]
598 async fn consume_regex_not_found() {
599 let buf = OutputBuffer::new();
600 buf.append(b"hello world").await;
601 let re = Regex::new(r"\d+").unwrap();
602 assert!(buf.consume_regex(&re).await.is_none());
603 assert_eq!(buf.remaining().await, b"hello world");
604 }
605
606 #[tokio::test]
607 async fn consume_regex_absolute_offsets_after_drain() {
608 let buf = OutputBuffer::new();
609 buf.append(b"aaa 123 bbb 456\n").await;
610 let re = Regex::new(r"\d+").unwrap();
611 let (m1, _) = buf.consume_regex(&re).await.unwrap();
612 assert_eq!(m1.start, 4);
613 assert_eq!(m1.end, 7);
614 let (m2, _) = buf.consume_regex(&re).await.unwrap();
616 assert_eq!(m2.start, 12);
617 assert_eq!(m2.end, 15);
618 }
619
620 #[tokio::test]
623 async fn consume_regex_defers_partial_line() {
624 let buf = OutputBuffer::new();
626 buf.append(b"hello wor").await;
627 let re = RegexBuilder::new(r"^(.+)$")
628 .multi_line(true)
629 .build()
630 .unwrap();
631 assert!(buf.consume_regex(&re).await.is_none());
633 assert_eq!(buf.remaining().await, b"hello wor");
635
636 buf.append(b"ld\n").await;
638 let (m, _) = buf.consume_regex(&re).await.unwrap();
639 assert_eq!(m.value.0.get("0").unwrap(), "hello world");
640 }
641
642 #[tokio::test]
643 async fn consume_regex_allows_match_before_partial_tail() {
644 let buf = OutputBuffer::new();
646 buf.append(b"first line\nsecond li").await;
647 let re = RegexBuilder::new(r"^(.+)$")
648 .multi_line(true)
649 .build()
650 .unwrap();
651 let (m, _) = buf.consume_regex(&re).await.unwrap();
653 assert_eq!(m.value.0.get("1").unwrap(), "first line");
654 }
655
656 #[tokio::test]
657 async fn fail_check_consume_regex_defers_partial_line() {
658 let buf = OutputBuffer::new();
659 buf.append(b"partial data").await;
660 let re = RegexBuilder::new(r"^(.+)$")
661 .multi_line(true)
662 .build()
663 .unwrap();
664 let result = buf.fail_check_consume_regex(&re, None).await;
665 assert!(result.unwrap().is_none());
666
667 buf.append(b"\n").await;
668 let result = buf.fail_check_consume_regex(&re, None).await;
669 let (m, _) = result.unwrap().unwrap();
670 assert_eq!(m.value.0.get("0").unwrap(), "partial data");
671 }
672
673 #[tokio::test]
676 async fn clear_empties_buffer() {
677 let buf = OutputBuffer::new();
678 buf.append(b"hello world").await;
679 buf.clear().await;
680 assert!(buf.remaining().await.is_empty());
681 }
682
683 #[tokio::test]
684 async fn clear_advances_base_correctly() {
685 let buf = OutputBuffer::new();
686 buf.append(b"hello world").await;
687 buf.clear().await;
688 buf.append(b"abc 123\n").await;
689 let re = Regex::new(r"\d+").unwrap();
690 let (m, _) = buf.consume_regex(&re).await.unwrap();
691 assert_eq!(m.start, 15);
693 assert_eq!(m.end, 18);
694 }
695
696 #[tokio::test]
699 async fn snapshot_tail_returns_tail() {
700 let buf = OutputBuffer::new();
701 buf.append(b"hello world").await;
702 let snapshot = buf.snapshot_tail(5).await;
703 match snapshot {
704 BufferSnapshot::Tail { content } => {
705 assert_eq!(content, "...world");
706 }
707 _ => panic!("expected Tail"),
708 }
709 }
710
711 #[tokio::test]
712 async fn snapshot_tail_full_content_when_short() {
713 let buf = OutputBuffer::new();
714 buf.append(b"hi").await;
715 let snapshot = buf.snapshot_tail(80).await;
716 match snapshot {
717 BufferSnapshot::Tail { content } => {
718 assert_eq!(content, "hi");
719 }
720 _ => panic!("expected Tail"),
721 }
722 }
723
724 #[test]
727 fn check_fail_in_buffer_regex_match() {
728 let fp = FailPattern::Regex(Regex::new(r"ERROR").unwrap());
729 let hit = check_fail_in_buffer("some ERROR here", &fp).unwrap();
730 assert_eq!(hit.pattern, "ERROR");
731 assert_eq!(hit.matched_text, "ERROR");
732 }
733
734 #[test]
735 fn check_fail_in_buffer_regex_no_match() {
736 let fp = FailPattern::Regex(Regex::new(r"ERROR").unwrap());
737 assert!(check_fail_in_buffer("all good", &fp).is_none());
738 }
739
740 #[test]
741 fn check_fail_in_buffer_literal_match() {
742 let fp = FailPattern::Literal("FATAL".to_string());
743 let hit = check_fail_in_buffer("got FATAL crash", &fp).unwrap();
744 assert_eq!(hit.pattern, "FATAL");
745 assert_eq!(hit.matched_text, "FATAL");
746 }
747
748 #[test]
749 fn check_fail_in_buffer_literal_no_match() {
750 let fp = FailPattern::Literal("FATAL".to_string());
751 assert!(check_fail_in_buffer("all good", &fp).is_none());
752 }
753
754 #[tokio::test]
757 async fn fail_check_consume_literal_no_fail_pattern() {
758 let buf = OutputBuffer::new();
759 buf.append(b"hello world").await;
760 let result = buf.fail_check_consume_literal("hello", None).await;
761 let (m, _) = result.unwrap().unwrap();
762 assert_eq!(m.value.0, "hello");
763 }
764
765 #[tokio::test]
766 async fn fail_check_consume_literal_fail_pattern_not_matched() {
767 let buf = OutputBuffer::new();
768 buf.append(b"hello world").await;
769 let fp = FailPattern::Regex(Regex::new(r"ERROR").unwrap());
770 let result = buf.fail_check_consume_literal("hello", Some(&fp)).await;
771 let (m, _) = result.unwrap().unwrap();
772 assert_eq!(m.value.0, "hello");
773 }
774
775 #[tokio::test]
776 async fn fail_check_consume_literal_fail_pattern_triggers() {
777 let buf = OutputBuffer::new();
778 buf.append(b"ERROR: something broke").await;
779 let fp = FailPattern::Regex(Regex::new(r"ERROR").unwrap());
780 let result = buf.fail_check_consume_literal("broke", Some(&fp)).await;
781 let hit = result.unwrap_err();
782 assert_eq!(hit.pattern, "ERROR");
783 assert_eq!(hit.matched_text, "ERROR");
784 assert_eq!(buf.remaining().await, b"ERROR: something broke");
786 }
787
788 #[tokio::test]
789 async fn fail_check_consume_literal_target_not_found() {
790 let buf = OutputBuffer::new();
791 buf.append(b"hello world").await;
792 let result = buf.fail_check_consume_literal("xyz", None).await;
793 assert!(result.unwrap().is_none());
794 }
795
796 #[tokio::test]
799 async fn fail_check_consume_regex_no_fail_pattern() {
800 let buf = OutputBuffer::new();
801 buf.append(b"abc 123 def").await;
802 let re = Regex::new(r"\d+").unwrap();
803 let result = buf.fail_check_consume_regex(&re, None).await;
804 let (m, _) = result.unwrap().unwrap();
805 assert_eq!(m.value.0.get("0").unwrap(), "123");
806 }
807
808 #[tokio::test]
809 async fn fail_check_consume_regex_fail_pattern_triggers() {
810 let buf = OutputBuffer::new();
811 buf.append(b"FATAL: abc 123").await;
812 let fp = FailPattern::Literal("FATAL".to_string());
813 let re = Regex::new(r"\d+").unwrap();
814 let result = buf.fail_check_consume_regex(&re, Some(&fp)).await;
815 let hit = result.unwrap_err();
816 assert_eq!(hit.pattern, "FATAL");
817 assert_eq!(hit.matched_text, "FATAL");
818 assert_eq!(buf.remaining().await, b"FATAL: abc 123");
820 }
821
822 #[tokio::test]
823 async fn fail_check_consume_regex_target_not_found() {
824 let buf = OutputBuffer::new();
825 buf.append(b"hello world").await;
826 let re = Regex::new(r"\d+").unwrap();
827 let result = buf.fail_check_consume_regex(&re, None).await;
828 assert!(result.unwrap().is_none());
829 }
830
831 #[tokio::test]
834 async fn check_fail_pattern_none() {
835 let buf = OutputBuffer::new();
836 buf.append(b"ERROR here").await;
837 assert!(buf.check_fail_pattern(None).await.is_none());
838 }
839
840 #[tokio::test]
841 async fn check_fail_pattern_found() {
842 let buf = OutputBuffer::new();
843 buf.append(b"got ERROR output").await;
844 let fp = FailPattern::Regex(Regex::new(r"ERROR").unwrap());
845 let hit = buf.check_fail_pattern(Some(&fp)).await.unwrap();
846 assert_eq!(hit.pattern, "ERROR");
847 assert_eq!(hit.matched_text, "ERROR");
848 }
849
850 #[tokio::test]
851 async fn check_fail_pattern_not_found() {
852 let buf = OutputBuffer::new();
853 buf.append(b"all good").await;
854 let fp = FailPattern::Regex(Regex::new(r"ERROR").unwrap());
855 assert!(buf.check_fail_pattern(Some(&fp)).await.is_none());
856 }
857}