clitest_lib/
output.rs

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