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