Skip to main content

clitest_lib/
output.rs

1use std::{
2    collections::{BTreeSet, HashMap},
3    sync::{Arc, Mutex, OnceLock},
4};
5
6use grok::Grok;
7use serde::Serialize;
8
9use crate::failure::OutputPatternMatchFailure;
10use crate::{
11    failure::{OutputMatchTraceNode, PatternTraceNote},
12    script::{IfCondition, ScriptLocation, ScriptRunContext},
13};
14
15#[derive(Clone, Debug, PartialEq, Eq)]
16pub struct Line {
17    pub number: usize,
18    pub text: String,
19}
20
21#[derive(Clone)]
22pub struct Lines {
23    lines: Arc<Vec<String>>,
24    current_line: usize,
25    ignored_patterns: OutputPatterns,
26    negative_disabled: bool,
27    rejected_patterns: OutputPatterns,
28}
29
30impl std::fmt::Debug for Lines {
31    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
32        write!(
33            f,
34            "Lines {{ ignored: {} pattern(s), rejected: {} pattern(s) }}",
35            self.ignored_patterns.len(),
36            self.rejected_patterns.len()
37        )
38    }
39}
40
41impl<'s> IntoIterator for &'s Lines {
42    type Item = &'s String;
43    type IntoIter = std::slice::Iter<'s, String>;
44
45    fn into_iter(self) -> Self::IntoIter {
46        self.lines.iter()
47    }
48}
49
50impl std::fmt::Display for Lines {
51    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52        write!(f, "{}", self.lines[self.current_line..].join("\n"))
53    }
54}
55
56impl Lines {
57    pub fn new(lines: Vec<String>) -> Self {
58        Self {
59            lines: Arc::new(lines),
60            current_line: 0,
61            ignored_patterns: Default::default(),
62            negative_disabled: false,
63            rejected_patterns: Default::default(),
64        }
65    }
66
67    pub fn is_exhausted(&self) -> bool {
68        self.current_line >= self.lines.len()
69    }
70
71    pub fn next_line(&self) -> Option<Line> {
72        if self.current_line < self.lines.len() {
73            Some(Line {
74                number: self.current_line,
75                text: self.lines[self.current_line].clone(),
76            })
77        } else {
78            None
79        }
80    }
81
82    pub fn next(
83        &self,
84        context: OutputMatchContext,
85    ) -> Result<(Option<Line>, Lines), OutputPatternMatchFailure> {
86        let mut next = self.clone();
87        'outer: while next.current_line < next.lines.len() {
88            if !self.negative_disabled {
89                let ignore_check = next.without_negatives();
90                for ignored_pattern in &*next.ignored_patterns {
91                    if let Ok(next_next) =
92                        ignored_pattern.matches(context.ignore(), ignore_check.clone())
93                    {
94                        next = next_next.with_negatives();
95                        continue 'outer;
96                    }
97                }
98                for rejected_pattern in &*next.rejected_patterns {
99                    if rejected_pattern
100                        .matches(context.ignore(), ignore_check.clone())
101                        .is_ok()
102                    {
103                        return Err(OutputPatternMatchFailure {
104                            location: rejected_pattern.location.clone(),
105                            pattern_type: "reject",
106                            output_line: next.next_line(),
107                        });
108                    }
109                }
110            }
111            let line = Line {
112                number: next.current_line,
113                text: next.lines[next.current_line].clone(),
114            };
115            next.current_line += 1;
116            return Ok((Some(line), next));
117        }
118        Ok((None, next))
119    }
120
121    pub fn with_ignore(&self, ignore: &OutputPatterns) -> Self {
122        let mut ignored_patterns = self.ignored_patterns.clone();
123        ignored_patterns.extend(ignore);
124        Self {
125            ignored_patterns,
126            ..self.clone()
127        }
128    }
129
130    pub fn with_reject(&self, reject: &OutputPatterns) -> Self {
131        let mut rejected_patterns = self.rejected_patterns.clone();
132        rejected_patterns.extend(reject);
133        Self {
134            rejected_patterns,
135            ..self.clone()
136        }
137    }
138
139    fn without_negatives(&self) -> Self {
140        Self {
141            negative_disabled: true,
142            ..self.clone()
143        }
144    }
145
146    fn with_negatives(&self) -> Self {
147        Self {
148            negative_disabled: false,
149            ..self.clone()
150        }
151    }
152
153    pub fn into_inner(self) -> Vec<String> {
154        Arc::unwrap_or_clone(self.lines).split_off(self.current_line)
155    }
156
157    pub fn is_empty(&self) -> bool {
158        self.lines.is_empty()
159    }
160}
161
162#[derive(Clone, Default, Debug, Serialize)]
163
164pub struct OutputPatterns {
165    patterns: Arc<Vec<OutputPattern>>,
166}
167
168impl OutputPatterns {
169    pub fn new(patterns: Vec<OutputPattern>) -> Self {
170        Self {
171            patterns: Arc::new(patterns),
172        }
173    }
174
175    pub fn is_empty(&self) -> bool {
176        self.patterns.is_empty()
177    }
178
179    pub fn len(&self) -> usize {
180        self.patterns.len()
181    }
182
183    pub fn extend(&mut self, patterns: &OutputPatterns) {
184        if self.is_empty() {
185            self.patterns = patterns.patterns.clone();
186            return;
187        }
188        let new_patterns = std::mem::take(&mut self.patterns);
189        let mut new_patterns = Arc::unwrap_or_clone(new_patterns);
190        new_patterns.extend(patterns.patterns.iter().cloned());
191        self.patterns = Arc::new(new_patterns);
192    }
193}
194
195impl std::ops::Deref for OutputPatterns {
196    type Target = Vec<OutputPattern>;
197    fn deref(&self) -> &Self::Target {
198        &self.patterns
199    }
200}
201
202#[derive(Clone)]
203pub struct OutputPattern {
204    pub location: ScriptLocation,
205    pub pattern: OutputPatternType,
206    pub ignore: OutputPatterns,
207    pub reject: OutputPatterns,
208}
209
210impl Serialize for OutputPattern {
211    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
212    where
213        S: serde::Serializer,
214    {
215        self.pattern.serialize(serializer)
216    }
217}
218
219impl std::fmt::Debug for OutputPattern {
220    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
221        write!(f, "{:?}", self.pattern)
222    }
223}
224
225impl OutputPattern {
226    pub fn new_sequence(location: ScriptLocation, mut patterns: Vec<OutputPattern>) -> Self {
227        if patterns.len() == 1 {
228            patterns.remove(0)
229        } else {
230            Self {
231                pattern: OutputPatternType::Sequence(patterns),
232                ignore: Default::default(),
233                reject: Default::default(),
234                location: location.clone(),
235            }
236        }
237    }
238
239    pub fn prepare(&self, grok: &Grok) -> Result<(), OutputPatternPrepareError> {
240        for pattern in &*self.ignore.patterns {
241            pattern.prepare(grok)?
242        }
243        for pattern in &*self.reject.patterns {
244            pattern.prepare(grok)?
245        }
246        match &self.pattern {
247            OutputPatternType::Pattern(pattern) => {
248                pattern
249                    .prepare(grok)
250                    .map_err(|e| OutputPatternPrepareError {
251                        location: self.location.clone(),
252                        pattern: pattern.pattern.clone(),
253                        error: e,
254                    })?
255            }
256            OutputPatternType::Sequence(patterns) => {
257                for pattern in patterns {
258                    pattern.prepare(grok)?;
259                }
260            }
261            OutputPatternType::Unordered(patterns) => {
262                for pattern in patterns {
263                    pattern.prepare(grok)?;
264                }
265            }
266            OutputPatternType::Choice(patterns) => {
267                for pattern in patterns {
268                    pattern.prepare(grok)?;
269                }
270            }
271            OutputPatternType::If(_, pattern) => pattern.prepare(grok)?,
272            OutputPatternType::Not(pattern) => pattern.prepare(grok)?,
273            OutputPatternType::Any(pattern) => pattern.prepare(grok)?,
274            OutputPatternType::Repeat(pattern) => pattern.prepare(grok)?,
275            OutputPatternType::Optional(pattern) => pattern.prepare(grok)?,
276            OutputPatternType::Literal(_) => {}
277            OutputPatternType::End | OutputPatternType::None => {}
278        }
279        Ok(())
280    }
281
282    pub fn matches(
283        &self,
284        context: OutputMatchContext,
285        output: Lines,
286    ) -> Result<Lines, OutputPatternMatchFailure> {
287        if self.ignore.is_empty() && self.reject.is_empty() {
288            self.pattern.matches(&self.location, context, output)
289        } else {
290            let output = output.with_ignore(&self.ignore).with_reject(&self.reject);
291            self.pattern.matches(&self.location, context, output)
292        }
293    }
294
295    /// The minimum number of lines this pattern will match.
296    pub fn min_matches(&self) -> usize {
297        self.pattern.min_matches()
298    }
299
300    /// The maximum number of lines this pattern will match (or usize::MAX if unbounded).
301    pub fn max_matches(&self) -> usize {
302        self.pattern.max_matches()
303    }
304}
305
306#[derive(thiserror::Error, Debug)]
307#[error("pattern {pattern} at line {location} failed to compile: {error}")]
308pub struct OutputPatternPrepareError {
309    pub location: ScriptLocation,
310    pub pattern: String,
311    pub error: grok::Error,
312}
313
314#[derive(Clone)]
315pub enum OutputPatternType {
316    /// The end of the output
317    End,
318    /// Matches no lines of output, always succeeds
319    None,
320    /// Any lines, followed by a pattern.
321    Any(Box<OutputPattern>),
322    /// A literal string
323    Literal(String),
324    /// A grok pattern
325    Pattern(Arc<GrokPattern>),
326    /// A pattern that matches one or more of the given pattern
327    Repeat(Box<OutputPattern>),
328    /// A pattern that matches zero or one of the given pattern
329    Optional(Box<OutputPattern>),
330    /// A pattern that all of its subpatterns, but in any order
331    Unordered(Vec<OutputPattern>),
332    /// A pattern that matches one of the given patterns
333    Choice(Vec<OutputPattern>),
334    /// A pattern that matches a sequence of patterns
335    Sequence(Vec<OutputPattern>),
336    /// A negative look-ahead pattern
337    Not(Box<OutputPattern>),
338    /// A pattern that matches a condition
339    If(IfCondition, Box<OutputPattern>),
340}
341
342impl Serialize for OutputPatternType {
343    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
344    where
345        S: serde::Serializer,
346    {
347        match self {
348            OutputPatternType::Literal(literal) => {
349                serializer.serialize_str(&format!("! {literal}"))
350            }
351            OutputPatternType::Pattern(pattern) => {
352                serializer.serialize_str(&format!("? {}", pattern.pattern))
353            }
354            OutputPatternType::Repeat(pattern) => {
355                HashMap::from([("repeat", &pattern)]).serialize(serializer)
356            }
357            OutputPatternType::Optional(pattern) => {
358                HashMap::from([("optional", &pattern)]).serialize(serializer)
359            }
360            OutputPatternType::Unordered(patterns) => {
361                HashMap::from([("unordered", &patterns)]).serialize(serializer)
362            }
363            OutputPatternType::Choice(patterns) => {
364                HashMap::from([("choice", &patterns)]).serialize(serializer)
365            }
366            OutputPatternType::Sequence(patterns) => {
367                HashMap::from([("sequence", &patterns)]).serialize(serializer)
368            }
369            OutputPatternType::Not(pattern) => {
370                HashMap::from([("not", &pattern)]).serialize(serializer)
371            }
372            OutputPatternType::Any(pattern) => {
373                HashMap::from([("any", &pattern)]).serialize(serializer)
374            }
375            OutputPatternType::If(condition, pattern) => {
376                #[derive(Serialize)]
377                struct If<'a> {
378                    condition: &'a IfCondition,
379                    pattern: &'a OutputPattern,
380                }
381                If { condition, pattern }.serialize(serializer)
382            }
383            OutputPatternType::End => serializer.serialize_str("end"),
384            OutputPatternType::None => serializer.serialize_str("none"),
385        }
386    }
387}
388
389impl OutputPatternType {
390    /// The minimum number of lines this pattern will match.
391    pub fn min_matches(&self) -> usize {
392        match self {
393            OutputPatternType::None => 0,
394            OutputPatternType::Literal(_) => 1,
395            OutputPatternType::Pattern(_) => 1,
396            OutputPatternType::Repeat(pattern) => pattern.min_matches(),
397            OutputPatternType::Optional(_) => 0,
398            OutputPatternType::Unordered(patterns) => {
399                patterns.iter().map(|p| p.min_matches()).sum()
400            }
401            OutputPatternType::Choice(patterns) => {
402                patterns.iter().map(|p| p.min_matches()).min().unwrap_or(0)
403            }
404            OutputPatternType::Sequence(patterns) => patterns.iter().map(|p| p.min_matches()).sum(),
405            OutputPatternType::Not(_) => 0,
406            OutputPatternType::Any(pattern) => pattern.min_matches(),
407            OutputPatternType::If(_, _) => 0,
408            OutputPatternType::End => 0,
409        }
410    }
411
412    /// The maximum number of lines this pattern will match (or usize::MAX if unbounded).
413    pub fn max_matches(&self) -> usize {
414        fn saturating_iter_sum<I>(iter: I) -> usize
415        where
416            I: IntoIterator<Item = usize>,
417        {
418            iter.into_iter()
419                .reduce(|n, i| n.saturating_add(i))
420                .unwrap_or(0)
421        }
422
423        match self {
424            OutputPatternType::None => 0,
425            OutputPatternType::Literal(_) => 1,
426            OutputPatternType::Pattern(_) => 1,
427            OutputPatternType::Repeat(pattern) => {
428                if pattern.max_matches() == 0 {
429                    0
430                } else {
431                    usize::MAX
432                }
433            }
434            OutputPatternType::Optional(pattern) => pattern.max_matches(),
435            OutputPatternType::Unordered(patterns) => {
436                saturating_iter_sum(patterns.iter().map(|p| p.max_matches()))
437            }
438            OutputPatternType::Choice(patterns) => {
439                patterns.iter().map(|p| p.max_matches()).max().unwrap_or(0)
440            }
441            OutputPatternType::Sequence(patterns) => {
442                saturating_iter_sum(patterns.iter().map(|p| p.max_matches()))
443            }
444            OutputPatternType::Not(_) => 0,
445            OutputPatternType::Any(_) => usize::MAX,
446            OutputPatternType::If(_, pattern) => pattern.max_matches(),
447            OutputPatternType::End => 0,
448        }
449    }
450
451    pub fn keyword(&self) -> &'static str {
452        match self {
453            OutputPatternType::Literal(_) => "\"...\"",
454            OutputPatternType::Pattern(_) => "? ...",
455            OutputPatternType::Repeat(_) => "repeat",
456            OutputPatternType::Optional(_) => "optional",
457            OutputPatternType::Unordered(_) => "unordered",
458            OutputPatternType::Choice(_) => "choice",
459            OutputPatternType::Sequence(_) => "sequence",
460            OutputPatternType::Not(_) => "not",
461            OutputPatternType::Any(_) => "*",
462            OutputPatternType::If(_, _) => "if",
463            OutputPatternType::End => "end",
464            OutputPatternType::None => "none",
465        }
466    }
467
468    pub fn is_container(&self) -> bool {
469        match self {
470            OutputPatternType::Literal(_) => false,
471            OutputPatternType::Pattern(_) => false,
472            OutputPatternType::Repeat(_) => true,
473            OutputPatternType::Optional(_) => true,
474            OutputPatternType::Unordered(_) => true,
475            OutputPatternType::Choice(_) => true,
476            OutputPatternType::Sequence(_) => true,
477            OutputPatternType::Not(_) => true,
478            OutputPatternType::Any(_) => false,
479            OutputPatternType::If(_, _) => true,
480            OutputPatternType::End => false,
481            OutputPatternType::None => false,
482        }
483    }
484
485    pub fn trace_string(&self) -> String {
486        use std::fmt::Write;
487        let mut out = String::new();
488        _ = match self {
489            OutputPatternType::Any(pattern) => {
490                if let OutputPatternType::End = pattern.pattern {
491                    write!(out, "*")
492                } else {
493                    write!(out, "* ... {}", pattern.pattern.trace_string())
494                }
495            }
496            OutputPatternType::End => {
497                write!(out, "<eof>")
498            }
499            _ if self.is_container() => {
500                write!(out, "{} {{ ... }}", self.keyword())
501            }
502            _ => {
503                write!(out, "{:?}", self)
504            }
505        };
506        out
507    }
508}
509
510impl Default for OutputPatternType {
511    fn default() -> Self {
512        Self::Sequence(vec![])
513    }
514}
515
516impl std::fmt::Debug for OutputPatternType {
517    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
518        match self {
519            OutputPatternType::Literal(literal) => write!(f, "{literal:?}"),
520            OutputPatternType::Pattern(pattern) => write!(f, "Pattern({pattern:?})"),
521            OutputPatternType::Repeat(pattern) => write!(f, "Repeat({pattern:?})"),
522            OutputPatternType::Optional(pattern) => write!(f, "Optional({pattern:?})"),
523            OutputPatternType::Unordered(patterns) => write!(f, "Unordered({patterns:?})"),
524            OutputPatternType::Choice(patterns) => write!(f, "Choice({patterns:?})"),
525            OutputPatternType::Sequence(patterns) => write!(f, "Sequence({patterns:?})"),
526            OutputPatternType::Not(pattern) => write!(f, "Not({pattern:?})"),
527            OutputPatternType::Any(until) => write!(f, "Any({until:?})"),
528            OutputPatternType::If(condition, pattern) => {
529                write!(f, "If({condition:?}, {pattern:?})")
530            }
531            OutputPatternType::End => write!(f, "End"),
532            OutputPatternType::None => write!(f, "None"),
533        }
534    }
535}
536
537#[derive(Serialize, derive_more::Debug)]
538#[debug("/{pattern:?}/")]
539pub struct GrokPattern {
540    pattern: String,
541    aliases: Vec<String>,
542    #[serde(skip)]
543    grok: OnceLock<grok::Pattern>,
544}
545
546impl GrokPattern {
547    pub fn compile(line: &str, escape_non_grok: bool) -> Result<Self, String> {
548        use grok::parser::GrokPatternError;
549        let mut test_pattern = String::new();
550        let mut final_pattern = String::new();
551        let mut aliases = vec![];
552        for bit in grok::parser::grok_split(line) {
553            match bit {
554                grok::parser::GrokComponent::RegularExpression { string, .. } => {
555                    if escape_non_grok {
556                        for char in string.chars() {
557                            if char.is_ascii() && !char.is_alphanumeric() {
558                                test_pattern.push('\\');
559                                test_pattern.push(char);
560                                final_pattern.push('\\');
561                                final_pattern.push(char);
562                            } else {
563                                test_pattern.push(char);
564                                final_pattern.push(char);
565                            }
566                        }
567                    } else {
568                        test_pattern.push_str(string);
569                        final_pattern.push_str(string);
570                    }
571                }
572                grok::parser::GrokComponent::GrokPattern { pattern, alias, .. } => {
573                    test_pattern.push('.');
574                    final_pattern.push_str(pattern);
575                    if !alias.is_empty() {
576                        aliases.push(alias.to_string());
577                    }
578                }
579                grok::parser::GrokComponent::PatternError(GrokPatternError::InvalidCharacter(
580                    c,
581                )) => {
582                    return Err(format!("Invalid character in pattern: {c:?}"));
583                }
584                grok::parser::GrokComponent::PatternError(GrokPatternError::InvalidPattern) => {
585                    return Err("Invalid grok pattern".to_string());
586                }
587                grok::parser::GrokComponent::PatternError(
588                    GrokPatternError::InvalidPatternDefinition,
589                ) => {
590                    return Err("Invalid grok pattern definition".to_string());
591                }
592            }
593        }
594
595        test_pattern.push('$');
596        final_pattern.push('$');
597
598        _ = Grok::empty()
599            .compile(&test_pattern, false)
600            .map_err(|e| e.to_string())?;
601
602        Ok(Self {
603            pattern: final_pattern,
604            aliases,
605            grok: OnceLock::new(),
606        })
607    }
608
609    pub fn prepare(&self, grok: &Grok) -> Result<(), grok::Error> {
610        // This could technically suffer from multiple init, but they should
611        // always initialize the same way.
612        if self.grok.get().is_none() {
613            let pattern = grok.compile(&self.pattern, false)?;
614            self.grok.get_or_init(move || pattern);
615        }
616        Ok(())
617    }
618
619    pub fn matches<'a>(&'a self, text: &'a str) -> Option<grok::Matches<'a>> {
620        let pattern_ref = self.grok.get().expect("grok pattern not compiled");
621        pattern_ref.match_against(text)
622    }
623}
624
625#[derive(Debug, Default)]
626struct OutputMatchTraceCollector {
627    root: Vec<OutputMatchTraceNode>,
628    /// Path of indices from [`Self::root`] down to the [`OutputMatchTraceNode`] whose
629    /// [`OutputMatchTraceNode::children`] receives nested pattern nodes.
630    path: Vec<usize>,
631}
632
633fn resolve_trace_node_mut<'a>(
634    root: &'a mut Vec<OutputMatchTraceNode>,
635    path: &[usize],
636    idx: usize,
637) -> &'a mut OutputMatchTraceNode {
638    let mut cur = root;
639    for &p in path {
640        cur = &mut cur[p].children;
641    }
642    &mut cur[idx]
643}
644
645impl OutputMatchTraceCollector {
646    fn navigate_mut<'a>(&'a mut self) -> &'a mut Vec<OutputMatchTraceNode> {
647        let mut cur = &mut self.root;
648        for &idx in &self.path {
649            cur = &mut cur[idx].children;
650        }
651        cur
652    }
653
654    fn composite_pattern_begin(&mut self, pattern: OutputPatternType, ignore: bool) {
655        let list = self.navigate_mut();
656        let idx = list.len();
657        list.push(OutputMatchTraceNode {
658            ignore,
659            pattern,
660            succeeded: false,
661            output_line: None,
662            note: None,
663            children: Vec::new(),
664        });
665        self.path.push(idx);
666    }
667
668    fn composite_pattern_end(
669        &mut self,
670        succeeded: bool,
671        output_line: Option<Line>,
672        note: Option<PatternTraceNote>,
673    ) {
674        let idx = self
675            .path
676            .pop()
677            .expect("composite_pattern_end without composite_pattern_begin");
678        let node = resolve_trace_node_mut(&mut self.root, &self.path, idx);
679        node.succeeded = succeeded;
680        node.output_line = output_line;
681        node.note = note;
682    }
683
684    fn pop_traces_before_last(&mut self, count: usize) {
685        let trace = self.navigate_mut().pop().expect("no trace to pop");
686        for _ in 0..count {
687            self.navigate_mut().pop().expect("no trace to pop");
688        }
689        self.navigate_mut().push(trace);
690    }
691
692    fn leaf_pattern(&mut self, node: OutputMatchTraceNode) {
693        let list = self.navigate_mut();
694        list.push(node);
695    }
696
697    fn take_root(&mut self) -> Vec<OutputMatchTraceNode> {
698        self.path.clear();
699        std::mem::take(&mut self.root)
700    }
701}
702
703#[derive(Debug, Clone)]
704pub struct OutputMatchContext<'s> {
705    trace: Arc<Mutex<OutputMatchTraceCollector>>,
706    ignore: bool,
707    expectations: Arc<Mutex<HashMap<String, String>>>,
708    script_context: &'s ScriptRunContext,
709}
710
711/// Successful internal match before trace decoration.
712struct RawPatternOk {
713    lines: Lines,
714    matched_line_if_ok: Option<Line>,
715    /// Used by composites such as [`OutputPatternType::If`] (branch annotation).
716    note: Option<PatternTraceNote>,
717}
718
719/// Failed internal match before trace decoration.
720struct RawPatternErr {
721    failure: OutputPatternMatchFailure,
722    note: Option<PatternTraceNote>,
723}
724
725impl From<OutputPatternMatchFailure> for RawPatternErr {
726    fn from(failure: OutputPatternMatchFailure) -> Self {
727        Self {
728            failure,
729            note: None,
730        }
731    }
732}
733
734/// Internal [`OutputPatternType::raw_matches`] result before public [`Result`] mapping.
735type RawPatternMatch = Result<RawPatternOk, RawPatternErr>;
736
737fn raw_ok(lines: Lines, matched_line_if_ok: Option<Line>) -> RawPatternMatch {
738    Ok(RawPatternOk {
739        lines,
740        matched_line_if_ok,
741        note: None,
742    })
743}
744
745fn raw_err(failure: OutputPatternMatchFailure, note: Option<PatternTraceNote>) -> RawPatternMatch {
746    Err(RawPatternErr { failure, note })
747}
748
749fn raw_into_public(raw: RawPatternMatch) -> Result<Lines, OutputPatternMatchFailure> {
750    raw.map(|ok| ok.lines).map_err(|e| e.failure)
751}
752
753fn record_leaf_pattern(
754    context: &OutputMatchContext<'_>,
755    pattern: OutputPatternType,
756    raw: &RawPatternMatch,
757) {
758    let node = match raw {
759        Ok(ok) => OutputMatchTraceNode {
760            ignore: context.ignore,
761            pattern,
762            succeeded: true,
763            output_line: ok.matched_line_if_ok.clone(),
764            note: ok.note.clone(),
765            children: Vec::new(),
766        },
767        Err(err) => OutputMatchTraceNode {
768            ignore: context.ignore,
769            pattern,
770            succeeded: false,
771            output_line: err.failure.output_line.clone(),
772            note: err.note.clone(),
773            children: Vec::new(),
774        },
775    };
776    context.trace.lock().unwrap().leaf_pattern(node);
777}
778
779fn finish_composite_pattern(context: &OutputMatchContext<'_>, raw: &RawPatternMatch) {
780    let (succeeded, output_line, note) = match raw {
781        Ok(ok) => (true, ok.matched_line_if_ok.clone(), ok.note.clone()),
782        Err(err) => (false, err.failure.output_line.clone(), err.note.clone()),
783    };
784    context
785        .trace
786        .lock()
787        .unwrap()
788        .composite_pattern_end(succeeded, output_line, note);
789}
790
791impl<'s> OutputMatchContext<'s> {
792    pub fn new(script_context: &'s ScriptRunContext) -> Self {
793        Self {
794            trace: Default::default(),
795            ignore: false,
796            script_context,
797            expectations: Default::default(),
798        }
799    }
800
801    /// Cheap clone passed into nested [`OutputPattern::matches`] calls.
802    pub fn descend(&self) -> Self {
803        Self {
804            trace: self.trace.clone(),
805            ignore: self.ignore,
806            script_context: self.script_context,
807            expectations: self.expectations.clone(),
808        }
809    }
810
811    pub fn composite_pattern_begin(&self, pattern: OutputPatternType) {
812        self.trace
813            .lock()
814            .unwrap()
815            .composite_pattern_begin(pattern, self.ignore);
816    }
817
818    pub fn ignore(&self) -> Self {
819        Self {
820            trace: self.trace.clone(),
821            ignore: true,
822            script_context: self.script_context,
823            expectations: self.expectations.clone(),
824        }
825    }
826
827    pub fn traces(&self) -> Vec<OutputMatchTraceNode> {
828        self.trace.lock().unwrap().take_root()
829    }
830
831    pub fn expect(&self, key: &str, value: String) {
832        self.expectations
833            .lock()
834            .unwrap()
835            .insert(key.to_string(), value);
836    }
837
838    pub fn expects(&self) -> HashMap<String, String> {
839        self.expectations.lock().unwrap().clone()
840    }
841}
842
843impl OutputPatternType {
844    pub fn matches(
845        &self,
846        location: &ScriptLocation,
847        context: OutputMatchContext,
848        output: Lines,
849    ) -> Result<Lines, OutputPatternMatchFailure> {
850        match self {
851            OutputPatternType::None
852            | OutputPatternType::Literal(_)
853            | OutputPatternType::Pattern(_)
854            | OutputPatternType::End => {
855                let raw = self.raw_matches(location, &context, output);
856                record_leaf_pattern(&context, self.clone(), &raw);
857                raw_into_public(raw)
858            }
859            _ => {
860                context.composite_pattern_begin(self.clone());
861                let raw = self.raw_matches(location, &context, output);
862                finish_composite_pattern(&context, &raw);
863                raw_into_public(raw)
864            }
865        }
866    }
867
868    fn raw_matches(
869        &self,
870        location: &ScriptLocation,
871        context: &OutputMatchContext<'_>,
872        mut output: Lines,
873    ) -> RawPatternMatch {
874        match self {
875            OutputPatternType::None => raw_ok(output, None),
876            OutputPatternType::Literal(literal) => {
877                let (line, next) = output.next(context.clone()).map_err(RawPatternErr::from)?;
878                let Some(line) = line else {
879                    return raw_err(
880                        OutputPatternMatchFailure {
881                            location: location.clone(),
882                            pattern_type: "literal",
883                            output_line: None,
884                        },
885                        None,
886                    );
887                };
888                let text = line.text.trim_end();
889                if text == literal
890                    || (line.text.contains('\x1b')
891                        && fast_strip_ansi::strip_ansi_string(&line.text).as_ref() == literal)
892                {
893                    raw_ok(next, Some(line.clone()))
894                } else {
895                    raw_err(
896                        OutputPatternMatchFailure {
897                            location: location.clone(),
898                            pattern_type: "literal",
899                            output_line: Some(line.clone()),
900                        },
901                        None,
902                    )
903                }
904            }
905            OutputPatternType::Pattern(pattern) => {
906                let (line, next) = output.next(context.clone()).map_err(RawPatternErr::from)?;
907                let Some(line) = line else {
908                    return raw_err(
909                        OutputPatternMatchFailure {
910                            location: location.clone(),
911                            pattern_type: "pattern",
912                            output_line: None,
913                        },
914                        None,
915                    );
916                };
917                let mut text = line.text.clone();
918                let mut res = pattern.matches(&text);
919                if res.is_none() {
920                    // Give it a second chance with the ANSI-stripped text IF we detect escape sequences
921                    if text.contains('\x1b') {
922                        text = fast_strip_ansi::strip_ansi_string(&text).into_owned();
923                        res = pattern.matches(&text);
924                    }
925                }
926                match res {
927                    None => raw_err(
928                        OutputPatternMatchFailure {
929                            location: location.clone(),
930                            pattern_type: "pattern",
931                            output_line: Some(line.clone()),
932                        },
933                        None,
934                    ),
935                    Some(matches) => {
936                        for alias in &pattern.aliases {
937                            if let Some(value) = matches.get(alias) {
938                                let existing = context
939                                    .expectations
940                                    .lock()
941                                    .unwrap()
942                                    .insert(alias.clone(), value.to_string());
943                                if let Some(existing) = existing
944                                    && existing != value
945                                {
946                                    return raw_err(
947                                        OutputPatternMatchFailure {
948                                            location: location.clone(),
949                                            pattern_type: "pattern",
950                                            output_line: Some(line.clone()),
951                                        },
952                                        Some(PatternTraceNote::AliasMismatch(
953                                            existing,
954                                            value.to_string(),
955                                        )),
956                                    );
957                                }
958                            }
959                        }
960                        raw_ok(next, Some(line.clone()))
961                    }
962                }
963            }
964            OutputPatternType::Sequence(patterns) => {
965                for pattern in patterns {
966                    output = pattern
967                        .matches(context.descend(), output)
968                        .map_err(RawPatternErr::from)?;
969                }
970                raw_ok(output, None)
971            }
972            OutputPatternType::Repeat(pattern) => {
973                let mut output = pattern
974                    .matches(context.descend(), output)
975                    .map_err(RawPatternErr::from)?;
976                loop {
977                    match pattern.matches(context.descend(), output.clone()) {
978                        Ok(new_rest) => output = new_rest,
979                        Err(_) => break,
980                    }
981                }
982                raw_ok(output, None)
983            }
984            OutputPatternType::Optional(pattern) => {
985                let lines = match pattern.matches(context.descend(), output.clone()) {
986                    Ok(v) => v,
987                    Err(_) => output,
988                };
989                raw_ok(lines, None)
990            }
991            OutputPatternType::Unordered(patterns) => {
992                let mut not_found = (0..patterns.len()).collect::<BTreeSet<_>>();
993                'outer: while !not_found.is_empty() {
994                    let mut cleanup = 0;
995                    for idx in &not_found {
996                        let idx = *idx;
997                        match patterns[idx].matches(context.descend(), output.clone()) {
998                            Ok(v) => {
999                                not_found.remove(&idx);
1000                                output = v;
1001                                context
1002                                    .trace
1003                                    .lock()
1004                                    .unwrap()
1005                                    .pop_traces_before_last(cleanup);
1006                                continue 'outer;
1007                            }
1008                            Err(_) => {
1009                                cleanup += 1;
1010                            }
1011                        }
1012                    }
1013                    return raw_err(
1014                        OutputPatternMatchFailure {
1015                            location: location.clone(),
1016                            pattern_type: "unordered",
1017                            output_line: output.next_line(),
1018                        },
1019                        None,
1020                    );
1021                }
1022                raw_ok(output, None)
1023            }
1024            OutputPatternType::Choice(patterns) => {
1025                for pattern in patterns {
1026                    if let Ok(v) = pattern.matches(context.descend(), output.clone()) {
1027                        return Ok(RawPatternOk {
1028                            lines: v,
1029                            matched_line_if_ok: None,
1030                            note: None,
1031                        });
1032                    }
1033                }
1034                raw_err(
1035                    OutputPatternMatchFailure {
1036                        location: location.clone(),
1037                        pattern_type: "choice",
1038                        output_line: output.next_line(),
1039                    },
1040                    None,
1041                )
1042            }
1043            OutputPatternType::Not(pattern) => {
1044                if pattern.matches(context.descend(), output.clone()).is_err() {
1045                    raw_ok(output, None)
1046                } else {
1047                    raw_err(
1048                        OutputPatternMatchFailure {
1049                            location: location.clone(),
1050                            pattern_type: "not",
1051                            output_line: output.next_line(),
1052                        },
1053                        None,
1054                    )
1055                }
1056            }
1057            OutputPatternType::Any(until) => loop {
1058                match until.matches(context.descend(), output.clone()) {
1059                    Ok(v) => {
1060                        output = v;
1061                        break raw_ok(output, None);
1062                    }
1063                    Err(e) => match output.next(context.clone()) {
1064                        Err(failure) => break Err(failure.into()),
1065                        Ok((Some(_), next)) => output = next,
1066                        Ok((None, _)) => break Err(e.into()),
1067                    },
1068                }
1069            },
1070            OutputPatternType::If(condition, pattern) => {
1071                let branch_met = condition.matches(context.script_context);
1072                let branch_note = if branch_met {
1073                    PatternTraceNote::IfConditionMet(condition.clone())
1074                } else {
1075                    PatternTraceNote::IfConditionSkipped(condition.clone())
1076                };
1077                let inner = if branch_met {
1078                    pattern.matches(context.clone(), output.clone())
1079                } else {
1080                    Ok(output)
1081                };
1082                inner
1083                    .map(|lines| RawPatternOk {
1084                        lines,
1085                        matched_line_if_ok: None,
1086                        note: Some(branch_note.clone()),
1087                    })
1088                    .map_err(|failure| RawPatternErr {
1089                        failure,
1090                        note: Some(branch_note),
1091                    })
1092            }
1093            OutputPatternType::End => {
1094                let (line, next) = output.next(context.clone()).map_err(RawPatternErr::from)?;
1095                if let Some(line) = line {
1096                    raw_err(
1097                        OutputPatternMatchFailure {
1098                            location: location.clone(),
1099                            pattern_type: "end",
1100                            output_line: Some(line),
1101                        },
1102                        None,
1103                    )
1104                } else {
1105                    raw_ok(next, None)
1106                }
1107            }
1108        }
1109    }
1110}
1111
1112#[derive(Debug)]
1113pub enum PatternResult {
1114    Matches,
1115    MatchesFailure,
1116    ExpectedFailure,
1117    Mismatch(OutputPatternMatchFailure, String),
1118}