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::script::{IfCondition, ScriptLocation, ScriptRunContext};
10
11#[derive(Clone, Debug, PartialEq, Eq)]
12pub struct Line {
13    pub number: usize,
14    pub text: String,
15}
16
17#[derive(Clone)]
18pub struct Lines {
19    lines: Arc<Vec<String>>,
20    current_line: usize,
21    ignored_patterns: OutputPatterns,
22    negative_disabled: bool,
23    rejected_patterns: OutputPatterns,
24}
25
26impl std::fmt::Debug for Lines {
27    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28        write!(
29            f,
30            "Lines {{ ignored: {} pattern(s), rejected: {} pattern(s) }}",
31            self.ignored_patterns.len(),
32            self.rejected_patterns.len()
33        )
34    }
35}
36
37impl<'s> IntoIterator for &'s Lines {
38    type Item = &'s String;
39    type IntoIter = std::slice::Iter<'s, String>;
40
41    fn into_iter(self) -> Self::IntoIter {
42        self.lines.iter()
43    }
44}
45
46impl std::fmt::Display for Lines {
47    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48        write!(f, "{}", self.lines[self.current_line..].join("\n"))
49    }
50}
51
52impl Lines {
53    pub fn new(lines: Vec<String>) -> Self {
54        Self {
55            lines: Arc::new(lines),
56            current_line: 0,
57            ignored_patterns: Default::default(),
58            negative_disabled: false,
59            rejected_patterns: Default::default(),
60        }
61    }
62
63    pub fn is_exhausted(&self) -> bool {
64        self.current_line >= self.lines.len()
65    }
66
67    pub fn next_line(&self) -> Option<Line> {
68        if self.current_line < self.lines.len() {
69            Some(Line {
70                number: self.current_line,
71                text: self.lines[self.current_line].clone(),
72            })
73        } else {
74            None
75        }
76    }
77
78    pub fn next(
79        &self,
80        context: OutputMatchContext,
81    ) -> Result<(Option<Line>, Lines), OutputPatternMatchFailure> {
82        let mut next = self.clone();
83        'outer: while next.current_line < next.lines.len() {
84            if !self.negative_disabled {
85                let ignore_check = next.without_negatives();
86                for ignored_pattern in &*next.ignored_patterns {
87                    if let Ok(next_next) =
88                        ignored_pattern.matches(context.ignore(), ignore_check.clone())
89                    {
90                        next = next_next.with_negatives();
91                        continue 'outer;
92                    }
93                }
94                for rejected_pattern in &*next.rejected_patterns {
95                    if rejected_pattern
96                        .matches(context.ignore(), ignore_check.clone())
97                        .is_ok()
98                    {
99                        return Err(OutputPatternMatchFailure {
100                            location: rejected_pattern.location.clone(),
101                            pattern_type: "reject",
102                            output_line: next.next_line(),
103                        });
104                    }
105                }
106            }
107            let line = Line {
108                number: next.current_line,
109                text: next.lines[next.current_line].clone(),
110            };
111            next.current_line += 1;
112            return Ok((Some(line), next));
113        }
114        Ok((None, next))
115    }
116
117    pub fn with_ignore(&self, ignore: &OutputPatterns) -> Self {
118        let mut ignored_patterns = self.ignored_patterns.clone();
119        ignored_patterns.extend(ignore);
120        Self {
121            ignored_patterns,
122            ..self.clone()
123        }
124    }
125
126    pub fn with_reject(&self, reject: &OutputPatterns) -> Self {
127        let mut rejected_patterns = self.rejected_patterns.clone();
128        rejected_patterns.extend(reject);
129        Self {
130            rejected_patterns,
131            ..self.clone()
132        }
133    }
134
135    fn without_negatives(&self) -> Self {
136        Self {
137            negative_disabled: true,
138            ..self.clone()
139        }
140    }
141
142    fn with_negatives(&self) -> Self {
143        Self {
144            negative_disabled: false,
145            ..self.clone()
146        }
147    }
148
149    pub fn into_inner(self) -> Vec<String> {
150        Arc::unwrap_or_clone(self.lines).split_off(self.current_line)
151    }
152
153    pub fn is_empty(&self) -> bool {
154        self.lines.is_empty()
155    }
156}
157
158#[derive(Clone, Default, Debug, Serialize)]
159
160pub struct OutputPatterns {
161    patterns: Arc<Vec<OutputPattern>>,
162}
163
164impl OutputPatterns {
165    pub fn new(patterns: Vec<OutputPattern>) -> Self {
166        Self {
167            patterns: Arc::new(patterns),
168        }
169    }
170
171    pub fn is_empty(&self) -> bool {
172        self.patterns.is_empty()
173    }
174
175    pub fn len(&self) -> usize {
176        self.patterns.len()
177    }
178
179    pub fn extend(&mut self, patterns: &OutputPatterns) {
180        if self.is_empty() {
181            self.patterns = patterns.patterns.clone();
182            return;
183        }
184        let new_patterns = std::mem::take(&mut self.patterns);
185        let mut new_patterns = Arc::unwrap_or_clone(new_patterns);
186        new_patterns.extend(patterns.patterns.iter().cloned());
187        self.patterns = Arc::new(new_patterns);
188    }
189}
190
191impl std::ops::Deref for OutputPatterns {
192    type Target = Vec<OutputPattern>;
193    fn deref(&self) -> &Self::Target {
194        &self.patterns
195    }
196}
197
198#[derive(Clone)]
199pub struct OutputPattern {
200    pub location: ScriptLocation,
201    pub pattern: OutputPatternType,
202    pub ignore: OutputPatterns,
203    pub reject: OutputPatterns,
204}
205
206impl Serialize for OutputPattern {
207    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
208    where
209        S: serde::Serializer,
210    {
211        self.pattern.serialize(serializer)
212    }
213}
214
215impl std::fmt::Debug for OutputPattern {
216    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
217        write!(f, "{:?}", self.pattern)
218    }
219}
220
221impl OutputPattern {
222    pub fn new_sequence(location: ScriptLocation, mut patterns: Vec<OutputPattern>) -> Self {
223        if patterns.len() == 1 {
224            patterns.remove(0)
225        } else {
226            Self {
227                pattern: OutputPatternType::Sequence(patterns),
228                ignore: Default::default(),
229                reject: Default::default(),
230                location: location.clone(),
231            }
232        }
233    }
234
235    pub fn prepare(&self, grok: &Grok) -> Result<(), OutputPatternPrepareError> {
236        for pattern in &*self.ignore.patterns {
237            pattern.prepare(grok)?
238        }
239        for pattern in &*self.reject.patterns {
240            pattern.prepare(grok)?
241        }
242        match &self.pattern {
243            OutputPatternType::Pattern(pattern) => {
244                pattern
245                    .prepare(grok)
246                    .map_err(|e| OutputPatternPrepareError {
247                        location: self.location.clone(),
248                        pattern: pattern.pattern.clone(),
249                        error: e,
250                    })?
251            }
252            OutputPatternType::Sequence(patterns) => {
253                for pattern in patterns {
254                    pattern.prepare(grok)?;
255                }
256            }
257            OutputPatternType::Unordered(patterns) => {
258                for pattern in patterns {
259                    pattern.prepare(grok)?;
260                }
261            }
262            OutputPatternType::Choice(patterns) => {
263                for pattern in patterns {
264                    pattern.prepare(grok)?;
265                }
266            }
267            OutputPatternType::If(_, pattern) => pattern.prepare(grok)?,
268            OutputPatternType::Not(pattern) => pattern.prepare(grok)?,
269            OutputPatternType::Any(pattern) => pattern.prepare(grok)?,
270            OutputPatternType::Repeat(pattern) => pattern.prepare(grok)?,
271            OutputPatternType::Optional(pattern) => pattern.prepare(grok)?,
272            OutputPatternType::Literal(_) => {}
273            OutputPatternType::End | OutputPatternType::None => {}
274        }
275        Ok(())
276    }
277
278    pub fn matches(
279        &self,
280        context: OutputMatchContext,
281        output: Lines,
282    ) -> Result<Lines, OutputPatternMatchFailure> {
283        if self.ignore.is_empty() && self.reject.is_empty() {
284            self.pattern.matches(&self.location, context, output)
285        } else {
286            let output = output.with_ignore(&self.ignore).with_reject(&self.reject);
287            self.pattern.matches(&self.location, context, output)
288        }
289    }
290
291    /// The minimum number of lines this pattern will match.
292    pub fn min_matches(&self) -> usize {
293        self.pattern.min_matches()
294    }
295
296    /// The maximum number of lines this pattern will match (or usize::MAX if unbounded).
297    pub fn max_matches(&self) -> usize {
298        self.pattern.max_matches()
299    }
300}
301
302#[derive(thiserror::Error, Debug)]
303#[error("pattern {pattern} at line {location} failed to compile: {error}")]
304pub struct OutputPatternPrepareError {
305    pub location: ScriptLocation,
306    pub pattern: String,
307    pub error: grok::Error,
308}
309
310#[derive(Clone)]
311pub enum OutputPatternType {
312    /// The end of the output
313    End,
314    /// Matches no lines of output, always succeeds
315    None,
316    /// Any lines, followed by a pattern.
317    Any(Box<OutputPattern>),
318    /// A literal string
319    Literal(String),
320    /// A grok pattern
321    Pattern(Arc<GrokPattern>),
322    /// A pattern that matches one or more of the given pattern
323    Repeat(Box<OutputPattern>),
324    /// A pattern that matches zero or one of the given pattern
325    Optional(Box<OutputPattern>),
326    /// A pattern that all of its subpatterns, but in any order
327    Unordered(Vec<OutputPattern>),
328    /// A pattern that matches one of the given patterns
329    Choice(Vec<OutputPattern>),
330    /// A pattern that matches a sequence of patterns
331    Sequence(Vec<OutputPattern>),
332    /// A negative look-ahead pattern
333    Not(Box<OutputPattern>),
334    /// A pattern that matches a condition
335    If(IfCondition, Box<OutputPattern>),
336}
337
338impl Serialize for OutputPatternType {
339    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
340    where
341        S: serde::Serializer,
342    {
343        match self {
344            OutputPatternType::Literal(literal) => {
345                serializer.serialize_str(&format!("! {literal}"))
346            }
347            OutputPatternType::Pattern(pattern) => {
348                serializer.serialize_str(&format!("? {}", pattern.pattern))
349            }
350            OutputPatternType::Repeat(pattern) => {
351                HashMap::from([("repeat", &pattern)]).serialize(serializer)
352            }
353            OutputPatternType::Optional(pattern) => {
354                HashMap::from([("optional", &pattern)]).serialize(serializer)
355            }
356            OutputPatternType::Unordered(patterns) => {
357                HashMap::from([("unordered", &patterns)]).serialize(serializer)
358            }
359            OutputPatternType::Choice(patterns) => {
360                HashMap::from([("choice", &patterns)]).serialize(serializer)
361            }
362            OutputPatternType::Sequence(patterns) => {
363                HashMap::from([("sequence", &patterns)]).serialize(serializer)
364            }
365            OutputPatternType::Not(pattern) => {
366                HashMap::from([("not", &pattern)]).serialize(serializer)
367            }
368            OutputPatternType::Any(pattern) => {
369                HashMap::from([("any", &pattern)]).serialize(serializer)
370            }
371            OutputPatternType::If(condition, pattern) => {
372                #[derive(Serialize)]
373                struct If<'a> {
374                    condition: &'a IfCondition,
375                    pattern: &'a OutputPattern,
376                }
377                If { condition, pattern }.serialize(serializer)
378            }
379            OutputPatternType::End => serializer.serialize_str("end"),
380            OutputPatternType::None => serializer.serialize_str("none"),
381        }
382    }
383}
384
385impl OutputPatternType {
386    /// The minimum number of lines this pattern will match.
387    pub fn min_matches(&self) -> usize {
388        match self {
389            OutputPatternType::None => 0,
390            OutputPatternType::Literal(_) => 1,
391            OutputPatternType::Pattern(_) => 1,
392            OutputPatternType::Repeat(pattern) => pattern.min_matches(),
393            OutputPatternType::Optional(_) => 0,
394            OutputPatternType::Unordered(patterns) => {
395                patterns.iter().map(|p| p.min_matches()).sum()
396            }
397            OutputPatternType::Choice(patterns) => {
398                patterns.iter().map(|p| p.min_matches()).min().unwrap_or(0)
399            }
400            OutputPatternType::Sequence(patterns) => patterns.iter().map(|p| p.min_matches()).sum(),
401            OutputPatternType::Not(_) => 0,
402            OutputPatternType::Any(pattern) => pattern.min_matches(),
403            OutputPatternType::If(_, _) => 0,
404            OutputPatternType::End => 0,
405        }
406    }
407
408    /// The maximum number of lines this pattern will match (or usize::MAX if unbounded).
409    pub fn max_matches(&self) -> usize {
410        fn saturating_iter_sum<I>(iter: I) -> usize
411        where
412            I: IntoIterator<Item = usize>,
413        {
414            iter.into_iter()
415                .reduce(|n, i| n.saturating_add(i))
416                .unwrap_or(0)
417        }
418
419        match self {
420            OutputPatternType::None => 0,
421            OutputPatternType::Literal(_) => 1,
422            OutputPatternType::Pattern(_) => 1,
423            OutputPatternType::Repeat(pattern) => {
424                if pattern.max_matches() == 0 {
425                    0
426                } else {
427                    usize::MAX
428                }
429            }
430            OutputPatternType::Optional(pattern) => pattern.max_matches(),
431            OutputPatternType::Unordered(patterns) => {
432                saturating_iter_sum(patterns.iter().map(|p| p.max_matches()))
433            }
434            OutputPatternType::Choice(patterns) => {
435                patterns.iter().map(|p| p.max_matches()).max().unwrap_or(0)
436            }
437            OutputPatternType::Sequence(patterns) => {
438                saturating_iter_sum(patterns.iter().map(|p| p.max_matches()))
439            }
440            OutputPatternType::Not(_) => 0,
441            OutputPatternType::Any(_) => usize::MAX,
442            OutputPatternType::If(_, pattern) => pattern.max_matches(),
443            OutputPatternType::End => 0,
444        }
445    }
446}
447
448impl Default for OutputPatternType {
449    fn default() -> Self {
450        Self::Sequence(vec![])
451    }
452}
453
454impl std::fmt::Debug for OutputPatternType {
455    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
456        match self {
457            OutputPatternType::Literal(literal) => write!(f, "Literal({literal})"),
458            OutputPatternType::Pattern(pattern) => write!(f, "Pattern({pattern:?})"),
459            OutputPatternType::Repeat(pattern) => write!(f, "Repeat({pattern:?})"),
460            OutputPatternType::Optional(pattern) => write!(f, "Optional({pattern:?})"),
461            OutputPatternType::Unordered(patterns) => write!(f, "Unordered({patterns:?})"),
462            OutputPatternType::Choice(patterns) => write!(f, "Choice({patterns:?})"),
463            OutputPatternType::Sequence(patterns) => write!(f, "Sequence({patterns:?})"),
464            OutputPatternType::Not(pattern) => write!(f, "Not({pattern:?})"),
465            OutputPatternType::Any(until) => write!(f, "Any({until:?})"),
466            OutputPatternType::If(condition, pattern) => {
467                write!(f, "If({condition:?}, {pattern:?})")
468            }
469            OutputPatternType::End => write!(f, "End"),
470            OutputPatternType::None => write!(f, "None"),
471        }
472    }
473}
474
475#[derive(Serialize, derive_more::Debug)]
476#[debug("/{pattern:?}/")]
477pub struct GrokPattern {
478    pattern: String,
479    aliases: Vec<String>,
480    #[serde(skip)]
481    grok: OnceLock<grok::Pattern>,
482}
483
484impl GrokPattern {
485    pub fn compile(line: &str, escape_non_grok: bool) -> Result<Self, String> {
486        use grok::parser::GrokPatternError;
487        let mut test_pattern = String::new();
488        let mut final_pattern = String::new();
489        let mut aliases = vec![];
490        for bit in grok::parser::grok_split(line) {
491            match bit {
492                grok::parser::GrokComponent::RegularExpression { string, .. } => {
493                    if escape_non_grok {
494                        for char in string.chars() {
495                            if char.is_ascii() && !char.is_alphanumeric() {
496                                test_pattern.push('\\');
497                                test_pattern.push(char);
498                                final_pattern.push('\\');
499                                final_pattern.push(char);
500                            } else {
501                                test_pattern.push(char);
502                                final_pattern.push(char);
503                            }
504                        }
505                    } else {
506                        test_pattern.push_str(string);
507                        final_pattern.push_str(string);
508                    }
509                }
510                grok::parser::GrokComponent::GrokPattern { pattern, alias, .. } => {
511                    test_pattern.push('.');
512                    final_pattern.push_str(pattern);
513                    if !alias.is_empty() {
514                        aliases.push(alias.to_string());
515                    }
516                }
517                grok::parser::GrokComponent::PatternError(GrokPatternError::InvalidCharacter(
518                    c,
519                )) => {
520                    return Err(format!("Invalid character in pattern: {c:?}"));
521                }
522                grok::parser::GrokComponent::PatternError(GrokPatternError::InvalidPattern) => {
523                    return Err("Invalid grok pattern".to_string());
524                }
525                grok::parser::GrokComponent::PatternError(
526                    GrokPatternError::InvalidPatternDefinition,
527                ) => {
528                    return Err("Invalid grok pattern definition".to_string());
529                }
530            }
531        }
532
533        test_pattern.push('$');
534        final_pattern.push('$');
535
536        _ = Grok::empty()
537            .compile(&test_pattern, false)
538            .map_err(|e| e.to_string())?;
539
540        Ok(Self {
541            pattern: final_pattern,
542            aliases,
543            grok: OnceLock::new(),
544        })
545    }
546
547    pub fn prepare(&self, grok: &Grok) -> Result<(), grok::Error> {
548        // This could technically suffer from multiple init, but they should
549        // always initialize the same way.
550        if self.grok.get().is_none() {
551            let pattern = grok.compile(&self.pattern, false)?;
552            self.grok.get_or_init(move || pattern);
553        }
554        Ok(())
555    }
556
557    pub fn matches<'a>(&'a self, text: &'a str) -> Option<grok::Matches<'a>> {
558        let pattern_ref = self.grok.get().expect("grok pattern not compiled");
559        pattern_ref.match_against(text)
560    }
561}
562
563#[derive(Clone, Debug, thiserror::Error, derive_more::Display, PartialEq, Eq)]
564#[display("pattern {pattern_type} at line {location} {verb} output line {line:?}", verb = self.verb(), line = self.line())]
565pub struct OutputPatternMatchFailure {
566    pub location: ScriptLocation,
567    pub pattern_type: &'static str,
568    pub output_line: Option<Line>,
569}
570
571impl OutputPatternMatchFailure {
572    fn verb(&self) -> &'static str {
573        if self.pattern_type == "reject" {
574            "rejected"
575        } else {
576            "does not match"
577        }
578    }
579
580    fn line(&self) -> String {
581        self.output_line
582            .as_ref()
583            .map(|l| l.text.clone())
584            .unwrap_or("<eof>".to_string())
585    }
586}
587
588#[derive(Debug, Clone)]
589pub struct OutputMatchContext<'s> {
590    depth: usize,
591    trace: Arc<Mutex<Vec<String>>>,
592    ignore: bool,
593    expectations: Arc<Mutex<HashMap<String, String>>>,
594    script_context: &'s ScriptRunContext,
595}
596
597impl<'s> OutputMatchContext<'s> {
598    pub fn new(script_context: &'s ScriptRunContext) -> Self {
599        Self {
600            depth: 0,
601            trace: Default::default(),
602            ignore: false,
603            script_context,
604            expectations: Default::default(),
605        }
606    }
607
608    pub fn descend(&self) -> Self {
609        Self {
610            depth: self.depth + 1,
611            trace: self.trace.clone(),
612            ignore: self.ignore,
613            script_context: self.script_context,
614            expectations: self.expectations.clone(),
615        }
616    }
617
618    pub fn ignore(&self) -> Self {
619        Self {
620            depth: self.depth,
621            trace: self.trace.clone(),
622            ignore: true,
623            script_context: self.script_context,
624            expectations: self.expectations.clone(),
625        }
626    }
627
628    pub fn trace(&self, line: &str) {
629        let ignore = if self.ignore { "-" } else { "" };
630        self.trace.lock().unwrap().push(format!(
631            "{:indent$}{ignore}{}",
632            "",
633            line,
634            indent = self.depth * 2
635        ));
636    }
637
638    pub fn traces(&self) -> Vec<String> {
639        std::mem::take(&mut self.trace.lock().unwrap())
640    }
641
642    pub fn expect(&self, key: &str, value: String) {
643        self.expectations
644            .lock()
645            .unwrap()
646            .insert(key.to_string(), value);
647    }
648
649    pub fn expects(&self) -> HashMap<String, String> {
650        self.expectations.lock().unwrap().clone()
651    }
652}
653
654impl OutputPatternType {
655    pub fn matches(
656        &self,
657        location: &ScriptLocation,
658        context: OutputMatchContext,
659        mut output: Lines,
660    ) -> Result<Lines, OutputPatternMatchFailure> {
661        context.trace(&format!("matching {self:?}"));
662        match self {
663            OutputPatternType::None => Ok(output),
664            OutputPatternType::Literal(literal) => {
665                let (line, next) = output.next(context.clone())?;
666                if let Some(line) = line {
667                    let text = line.text.trim_end();
668                    if text == literal {
669                        context.trace(&format!("literal match: {:?} == {literal:?}", line.text));
670                        Ok(next)
671                    } else if line.text.contains('\x1b')
672                        && fast_strip_ansi::strip_ansi_string(&line.text).as_ref() == literal
673                    {
674                        context.trace(&format!("literal match: {text:?} == {literal:?}"));
675                        Ok(next)
676                    } else {
677                        context.trace(&format!(
678                            "literal FAILED match: {:?} == {literal:?}",
679                            line.text
680                        ));
681                        Err(OutputPatternMatchFailure {
682                            location: location.clone(),
683                            pattern_type: "literal",
684                            output_line: Some(line),
685                        })
686                    }
687                } else {
688                    Err(OutputPatternMatchFailure {
689                        location: location.clone(),
690                        pattern_type: "literal",
691                        output_line: None,
692                    })
693                }
694            }
695            OutputPatternType::Pattern(pattern) => {
696                let (line, next) = output.next(context.clone())?;
697                if let Some(line) = line {
698                    let mut text = line.text.clone();
699                    let mut res = pattern.matches(&text);
700                    if res.is_none() {
701                        // Give it a second chance with the ANSI-stripped text IF we detect escape sequences
702                        if text.contains('\x1b') {
703                            text = fast_strip_ansi::strip_ansi_string(&text).into_owned();
704                            res = pattern.matches(&text);
705                        }
706                    }
707                    if let Some(matches) = res {
708                        for alias in &pattern.aliases {
709                            if let Some(value) = matches.get(alias) {
710                                let existing = context
711                                    .expectations
712                                    .lock()
713                                    .unwrap()
714                                    .insert(alias.clone(), value.to_string());
715                                if let Some(existing) = existing
716                                    && existing != value
717                                {
718                                    context.trace(&format!(
719                                        "pattern alias FAILED match: {existing:?} != {value:?}",
720                                    ));
721                                    return Err(OutputPatternMatchFailure {
722                                        location: location.clone(),
723                                        pattern_type: "pattern",
724                                        output_line: Some(line),
725                                    });
726                                }
727                            }
728                        }
729                        context.trace(&format!("pattern match: {:?} =~ {pattern:?}", line.text));
730                        Ok(next)
731                    } else {
732                        context.trace(&format!(
733                            "pattern FAILED match: {:?} =~ {pattern:?}",
734                            line.text
735                        ));
736                        Err(OutputPatternMatchFailure {
737                            location: location.clone(),
738                            pattern_type: "pattern",
739                            output_line: Some(line),
740                        })
741                    }
742                } else {
743                    Err(OutputPatternMatchFailure {
744                        location: location.clone(),
745                        pattern_type: "pattern",
746                        output_line: None,
747                    })
748                }
749            }
750            OutputPatternType::Sequence(patterns) => {
751                for pattern in patterns {
752                    match pattern.matches(context.descend(), output) {
753                        Ok(v) => {
754                            output = v;
755                        }
756                        Err(e) => {
757                            return Err(e);
758                        }
759                    }
760                }
761                Ok(output)
762            }
763            OutputPatternType::Repeat(pattern) => {
764                // Mandatory first match
765                let mut output = pattern.matches(context.descend(), output)?;
766                // Any number of additional matches, greedy
767                loop {
768                    match pattern.matches(context.descend(), output.clone()) {
769                        Ok(new_rest) => {
770                            output = new_rest;
771                        }
772                        Err(_) => break Ok(output),
773                    }
774                }
775            }
776            OutputPatternType::Optional(pattern) => {
777                // Never fails
778                match pattern.matches(context.descend(), output.clone()) {
779                    Ok(v) => Ok(v),
780                    Err(_) => Ok(output),
781                }
782            }
783            OutputPatternType::Unordered(patterns) => {
784                // Found is initialized with 0..patterns.len()
785                let mut not_found = (0..patterns.len()).collect::<BTreeSet<_>>();
786                'outer: while !not_found.is_empty() {
787                    for pattern in &not_found {
788                        let pattern = *pattern;
789                        match patterns[pattern].matches(context.descend(), output.clone()) {
790                            Ok(v) => {
791                                not_found.remove(&pattern);
792                                output = v;
793                                continue 'outer;
794                            }
795                            Err(_) => {}
796                        }
797                    }
798                    return Err(OutputPatternMatchFailure {
799                        location: location.clone(),
800                        pattern_type: "unordered",
801                        output_line: output.next_line(),
802                    });
803                }
804                Ok(output)
805            }
806            OutputPatternType::Choice(patterns) => {
807                for pattern in patterns {
808                    if let Ok(v) = pattern.matches(context.descend(), output.clone()) {
809                        return Ok(v);
810                    }
811                }
812                Err(OutputPatternMatchFailure {
813                    location: location.clone(),
814                    pattern_type: "choice",
815                    output_line: output.next_line(),
816                })
817            }
818            OutputPatternType::Not(pattern) => {
819                // Negative lookahead
820                if pattern.matches(context.descend(), output.clone()).is_err() {
821                    Ok(output)
822                } else {
823                    Err(OutputPatternMatchFailure {
824                        location: location.clone(),
825                        pattern_type: "not",
826                        output_line: output.next_line(),
827                    })
828                }
829            }
830            OutputPatternType::Any(until) => {
831                loop {
832                    match until.matches(context.descend(), output.clone()) {
833                        Ok(v) => {
834                            output = v;
835                            break Ok(output);
836                        }
837                        Err(e) => {
838                            // Eat one line and try again
839                            let (line, next) = output.next(context.clone())?;
840                            if line.is_some() {
841                                output = next;
842                            } else {
843                                break Err(e);
844                            }
845                        }
846                    }
847                }
848            }
849            OutputPatternType::If(condition, pattern) => {
850                if condition.matches(context.script_context) {
851                    context.trace(&format!("if match: {condition:?}"));
852                    pattern.matches(context.clone(), output.clone())
853                } else {
854                    context.trace(&format!("if FAILED match: {condition:?}"));
855                    Ok(output)
856                }
857            }
858            OutputPatternType::End => {
859                let (line, next) = output.next(context)?;
860                if let Some(line) = line {
861                    Err(OutputPatternMatchFailure {
862                        location: location.clone(),
863                        pattern_type: "end",
864                        output_line: Some(line),
865                    })
866                } else {
867                    Ok(next)
868                }
869            }
870        }
871    }
872}
873
874#[derive(Debug)]
875pub enum PatternResult {
876    Matches,
877    MatchesFailure,
878    ExpectedFailure,
879    Mismatch(OutputPatternMatchFailure, String),
880}