1 //
2 // Copyright (c) 2010-2025 Antmicro
3 // Copyright (c) 2011-2015 Realtime Embedded
4 //
5 // This file is licensed under the MIT License.
6 // Full license text is available in 'licenses/MIT.txt'.
7 //
8 
9 // Uncomment the following line to enable events debugging
10 // #define DEBUG_EVENTS
11 
12 using System;
13 using System.Collections.Generic;
14 using System.Diagnostics;
15 using System.Linq;
16 using System.Text;
17 using System.Text.RegularExpressions;
18 using System.Threading;
19 using Antmicro.Renode.Backends.Terminals;
20 using Antmicro.Renode.Core;
21 using Antmicro.Renode.Debugging;
22 using Antmicro.Renode.Peripherals;
23 using Antmicro.Renode.Peripherals.UART;
24 using Antmicro.Renode.Time;
25 using Antmicro.Renode.Logging;
26 using Antmicro.Renode.Utilities;
27 using Antmicro.Migrant;
28 using Antmicro.Migrant.Hooks;
29 
30 namespace Antmicro.Renode.Testing
31 {
32     public class TerminalTester : BackendTerminal
33     {
TerminalTester(TimeInterval timeout, EndLineOption endLineOption = EndLineOption.TreatLineFeedAsEndLine, bool binaryMode = false)34         public TerminalTester(TimeInterval timeout, EndLineOption endLineOption = EndLineOption.TreatLineFeedAsEndLine, bool binaryMode = false)
35         {
36             GlobalTimeout = timeout;
37             this.endLineOption = endLineOption;
38             this.binaryMode = binaryMode;
39             charEvent = new AutoResetEvent(false);
40             matchEvent = new AutoResetEvent(false);
41             lines = new List<Line>();
42             currentLineBuffer = new SafeStringBuilder();
43             sgrDecodingBuffer = new SafeStringBuilder();
44             report = new SafeStringBuilder();
45         }
46 
AttachTo(IUART uart)47         public override void AttachTo(IUART uart)
48         {
49             machine = uart.GetMachine();
50             if(machine == null)
51             {
52                 throw new ArgumentException("Could not find machine for UART");
53             }
54             base.AttachTo(uart);
55 
56             HandleSuccess("Attached to UART", matchingLineId: NoLine);
57         }
58 
Write(string text)59         public void Write(string text)
60         {
61             if(WriteCharDelay != TimeSpan.Zero)
62             {
63                 lock(delayedChars)
64                 {
65                     foreach(var character in text)
66                     {
67                         delayedChars.Enqueue(Tuple.Create(WriteCharDelay, character));
68                     }
69 
70                     if(!delayedCharInProgress)
71                     {
72                         HandleDelayedChars();
73                     }
74                 }
75             }
76             else
77             {
78                 foreach(var chr in text)
79                 {
80                     CallCharReceived((byte)chr);
81                 }
82             }
83         }
84 
WriteLine(string line = R)85         public void WriteLine(string line = "")
86         {
87             Write(line + CarriageReturn);
88         }
89 
WriteChar(byte value)90         public override void WriteChar(byte value)
91         {
92             if(!binaryMode && value == CarriageReturn && endLineOption == EndLineOption.TreatLineFeedAsEndLine)
93             {
94                 return;
95             }
96 
97             if(binaryMode || value != (endLineOption == EndLineOption.TreatLineFeedAsEndLine ? LineFeed : CarriageReturn))
98             {
99                 AppendCharToBuffer((char)value);
100             }
101             else
102             {
103                 lock(lines)
104                 {
105                     var line = currentLineBuffer.Unload();
106                     lines.Add(new Line(line, machine.ElapsedVirtualTime.TimeElapsed.TotalMilliseconds));
107                 }
108             }
109 
110 #if DEBUG_EVENTS
111             this.Log(LogLevel.Noisy, "Char received {0} (hex 0x{1:X})", (char)value, value);
112 #endif
113             charEvent.Set();
114 
115             lock(lines)
116             {
117                 // If we're not waiting for a match, we have nothing to do
118                 if(resultMatcher == null)
119                 {
120                     return;
121                 }
122 
123                 testerResult = resultMatcher.Invoke();
124 #if DEBUG_EVENTS
125                 this.Log(LogLevel.Noisy, "Matching result: {0}", testerResult);
126 #endif
127                 // If there was no match, we just keep waiting
128                 if(testerResult == null)
129                 {
130                     return;
131                 }
132 
133                 // Stop matching if we have already matched something
134                 resultMatcher = null;
135 
136                 if(pauseEmulation)
137                 {
138                     machine.PauseAndRequestEmulationPause(precise: true);
139                     pauseEmulation = false;
140                 }
141             }
142 
143             matchEvent.Set();
144         }
145 
WaitFor(string pattern, TimeInterval? timeout = null, bool treatAsRegex = false, bool includeUnfinishedLine = false, bool pauseEmulation = false, bool matchNextLine = false)146         public TerminalTesterResult WaitFor(string pattern, TimeInterval? timeout = null, bool treatAsRegex = false, bool includeUnfinishedLine = false, bool pauseEmulation = false, bool matchNextLine = false)
147         {
148             var eventName = "Line containing{1} >>{0}<<".FormatWith(pattern, treatAsRegex ? " regex" : string.Empty);
149 #if DEBUG_EVENTS
150             this.Log(LogLevel.Noisy, "Waiting for a line containing >>{0}<< (include unfinished line: {1}, with timeout {2}, regex {3}, pause on match {4}, match next line {5}) ",
151                 pattern, includeUnfinishedLine, timeout ?? GlobalTimeout, treatAsRegex, pauseEmulation, matchNextLine);
152 #endif
153 
154             if(binaryMode && !treatAsRegex)
155             {
156                 // The pattern is provided as a hex string. Parse it to a byte array and then decode
157                 // using the Latin1 encoding which passes through byte values as character values (that is,
158                 // { 0x00, 0x80, 0xff } becomes "\x00\x80\xff") to convert to a string where each char
159                 // is equivalent to one input byte.
160                 pattern = Encoding.GetEncoding("iso-8859-1").GetString(Misc.HexStringToByteArray(pattern, ignoreWhitespace: true));
161             }
162 
163             var result = WaitForMatch(() =>
164             {
165                 var lineMatch = CheckFinishedLines(pattern, treatAsRegex, eventName, matchNextLine);
166                 if(lineMatch != null)
167                 {
168                     return lineMatch;
169                 }
170 
171                 if(!includeUnfinishedLine)
172                 {
173                     return null;
174                 }
175 
176                 return CheckUnfinishedLine(pattern, treatAsRegex, eventName, matchAtStart: matchNextLine);
177 
178             }, timeout ?? GlobalTimeout, pauseEmulation);
179 
180             if(result == null)
181             {
182                 HandleFailure(eventName);
183             }
184             return result;
185         }
186 
WaitFor(string[] patterns, TimeInterval? timeout = null, bool treatAsRegex = false, bool includeUnfinishedLine = false, bool pauseEmulation = false, bool matchFromNextLine = false)187         public TerminalTesterResult WaitFor(string[] patterns, TimeInterval? timeout = null, bool treatAsRegex = false, bool includeUnfinishedLine = false, bool pauseEmulation = false, bool matchFromNextLine = false)
188         {
189             if(patterns.Length == 0)
190             {
191                 return HandleSuccess("Empty match", matchingLineId: NoLine);
192             }
193 #if DEBUG_EVENTS
194             this.Log(LogLevel.Noisy, "Waiting for a multi-line match starting with >>{0}<< (include unfinished line: {1}, with timeout {2}, regex {3}, pause on match {4}, match from next line {5}) ",
195                 patterns[0], includeUnfinishedLine, timeout ?? GlobalTimeout, treatAsRegex, pauseEmulation, matchFromNextLine);
196 #endif
197             if(!matchFromNextLine)
198             {
199                 if(includeUnfinishedLine && patterns.Length == 1)
200                 {
201                     return WaitFor(patterns[0], timeout, treatAsRegex, true, pauseEmulation);
202                 }
203                 return WaitForMultilineMatch(patterns, timeout, treatAsRegex, includeUnfinishedLine, pauseEmulation);
204             }
205 
206             TerminalTesterResult result = null;
207             for(var i = 0; i < patterns.Length; ++i)
208             {
209                 result = WaitFor(patterns[i], timeout, treatAsRegex, includeUnfinishedLine && (i == patterns.Length - 1),
210                     pauseEmulation, true);
211 
212                 if(result == null)
213                 {
214                    return null;
215                 }
216             }
217             return result;
218         }
219 
220         public TerminalTesterResult NextLine(TimeInterval? timeout = null, bool pauseEmulation = false)
221         {
222             var result = WaitForMatch(() =>
223             {
224                 if(lines.Count == 0)
225                 {
226                     return null;
227                 }
228 
229                 return HandleSuccess("Next line", matchingLineId: 0);
230             }, timeout ?? GlobalTimeout, pauseEmulation);
231 
232             if(result == null)
233             {
234                 HandleFailure("Next line");
235             }
236             return result;
237         }
238 
239         public bool IsIdle(TimeInterval? timeout = null, bool pauseEmulation = true)
240         {
241             var emulation = EmulationManager.Instance.CurrentEmulation;
242             this.pauseEmulation = pauseEmulation;
243             var timeoutEvent = machine.LocalTimeSource.EnqueueTimeoutEvent(
244                 (ulong)(timeout ?? GlobalTimeout).TotalMilliseconds,
245                 () =>
246                 {
247                     if(this.pauseEmulation)
248                     {
249                         emulation.PauseAll();
250                         this.pauseEmulation = false;
251                     }
252                 }
253             );
254 
255             charEvent.Reset();
256             if(!emulation.IsStarted)
257             {
258                 emulation.StartAll();
259             }
260 
261             var eventIdx = WaitHandle.WaitAny( new [] { timeoutEvent.WaitHandle, charEvent } );
262             var result = eventIdx == 0;
263             if(!result)
264             {
265                 HandleFailure("Terminal is idle");
266             }
267             this.pauseEmulation = false;
268             return result;
269         }
270 
ClearReport()271         public void ClearReport()
272         {
273             lock(lines)
274             {
275                 lines.Clear();
276                 report.Unload();
277                 generatedReport = null;
278             }
279         }
280 
GetReport()281         public string GetReport()
282         {
283             if(generatedReport == null)
284             {
285                 if(sgrDecodingBuffer.TryDump(out var sgr))
286                 {
287                     report.AppendFormat("--- SGR decoding buffer contains {0} characters: >>{1}<<\n", sgr.Length, sgr);
288                 }
289 
290                 if(currentLineBuffer.TryDump(out var line))
291                 {
292                     report.AppendFormat("--- Current line buffer contains {0} characters: >>{1}<<\n", line.Length, line);
293                 }
294 
295                 generatedReport = report.Unload();
296             }
297 
298             return generatedReport;
299         }
300 
301         public TimeInterval GlobalTimeout { get; set; }
302         public TimeSpan WriteCharDelay { get; set; }
303         public bool BinaryMode => binaryMode;
304 
HandleDelayedChars()305         private void HandleDelayedChars()
306         {
307             lock(delayedChars)
308             {
309                 if(!delayedChars.TryDequeue(out var delayed))
310                 {
311                     delayedCharInProgress = false;
312                     return;
313                 }
314 
315                 delayedCharInProgress = true;
316 
317                 var delay = TimeInterval.FromSeconds(delayed.Item1.TotalSeconds);
318                 machine.ScheduleAction(delay, _ =>
319                 {
320                     CallCharReceived((byte)delayed.Item2);
321                     HandleDelayedChars();
322                 });
323             }
324         }
325 
WaitForMatch(Func<TerminalTesterResult> resultMatcher, TimeInterval timeout, bool pauseEmulation = false)326         private TerminalTesterResult WaitForMatch(Func<TerminalTesterResult> resultMatcher, TimeInterval timeout, bool pauseEmulation = false)
327         {
328             var emulation = EmulationManager.Instance.CurrentEmulation;
329             TerminalTesterResult immediateResult = null;
330 
331             lock(lines)
332             {
333                 // Clear the old result and save the matcher for use in CharReceived
334                 this.testerResult = null;
335                 this.resultMatcher = resultMatcher;
336                 this.pauseEmulation = pauseEmulation;
337 
338                 // Handle the case where the match has already happened
339                 immediateResult = resultMatcher.Invoke();
340                 if(immediateResult != null)
341                 {
342                     // Prevent matching in CharReceived in this case
343                     this.resultMatcher = null;
344                 }
345             }
346 
347             // Pause the emulation without the `lines` lock held to avoid deadlocks in some cases
348             if(immediateResult != null)
349             {
350                 if(pauseEmulation)
351                 {
352                     this.Log(LogLevel.Warning, "Pause on match was requested, but the matching string had already " +
353                         "been printed when the assertion was made. Pause time will not be deterministic.");
354                     emulation.PauseAll();
355                 }
356                 return immediateResult;
357             }
358             // If we had timeout=0 and there was no immediate match, fail immediately
359             else if(timeout == TimeInterval.Empty)
360             {
361                 return null;
362             }
363 
364             var timeoutEvent = machine.LocalTimeSource.EnqueueTimeoutEvent((ulong)timeout.TotalMilliseconds);
365             var waitHandles = new [] { matchEvent, timeoutEvent.WaitHandle };
366 
367             var emulationPausedEvent = emulation.GetStartedStateChangedEvent(false);
368             if(!emulation.IsStarted)
369             {
370                 emulation.StartAll();
371             }
372 
373             do
374             {
375                 if(testerResult != null)
376                 {
377                     // We know our machine is paused - we did that in WriteChar
378                     // Now let's make sure the whole emulation is paused if necessary
379                     if(pauseEmulation)
380                     {
381                         emulationPausedEvent.WaitOne();
382                     }
383                     return testerResult;
384                 }
385 #if DEBUG_EVENTS
386                 this.Log(LogLevel.Noisy, "Waiting for the next event");
387 #endif
388                 WaitHandle.WaitAny(waitHandles);
389             }
390             while(!timeoutEvent.IsTriggered);
391 
392 #if DEBUG_EVENTS
393             this.Log(LogLevel.Noisy, "Matching timeout");
394 #endif
395 
396             lock(lines)
397             {
398                 // Clear the saved matcher in the case of a timeout
399                 this.resultMatcher = null;
400             }
401             return null;
402         }
403 
WaitForMultilineMatch(string[] patterns, TimeInterval? timeout = null, bool treatAsRegex = false, bool includeUnfinishedLine = false, bool pauseEmulation = false)404         private TerminalTesterResult WaitForMultilineMatch(string[] patterns, TimeInterval? timeout = null, bool treatAsRegex = false, bool includeUnfinishedLine = false, bool pauseEmulation = false)
405         {
406             DebugHelper.Assert(patterns.Length > (includeUnfinishedLine ? 1 : 0));
407             var eventName = "Lines starting with{1} >>{0}<<".FormatWith(patterns[0], treatAsRegex ? " regex" : string.Empty);
408 
409             var matcher = new MultilineMatcher(patterns, treatAsRegex, includeUnfinishedLine);
410             var onCandidate = false;
411             var lineIndexOffset = 0;
412 
413             var result = WaitForMatch(() =>
414             {
415                 for(var i = lineIndexOffset; i < lines.Count; ++i)
416                 {
417                     onCandidate = false;
418                     if(matcher.FeedLine(lines[i]))
419                     {
420                         if(includeUnfinishedLine)
421                         {
422                             onCandidate = true;
423                             break;
424                         }
425                         return HandleSuccess(eventName, i);
426                     }
427                     lineIndexOffset += 1;
428                 }
429 
430                 if(onCandidate && includeUnfinishedLine && matcher.CheckUnfinishedLine(currentLineBuffer.ToString()))
431                 {
432                     return HandleSuccess(eventName, CurrentLine);
433                 }
434 
435                 return null;
436             }, timeout ?? GlobalTimeout, pauseEmulation);
437 
438             if(result == null)
439             {
440                 HandleFailure(eventName);
441             }
442             return result;
443         }
444 
CheckFinishedLines(string pattern, bool regex, string eventName, bool matchFirstLine)445         private TerminalTesterResult CheckFinishedLines(string pattern, bool regex, string eventName, bool matchFirstLine)
446         {
447             lock(lines)
448             {
449                 string[] matchGroups = null;
450 
451                 var matcher = !regex
452                     ? (Predicate<Line>)(x => x.Content.Contains(pattern))
453                     : (Predicate<Line>)(x =>
454                 {
455                     var match = Regex.Match(x.Content, pattern);
456                     if(match.Success)
457                     {
458                         matchGroups = GetMatchGroups(match);
459                     }
460                     return match.Success;
461                 });
462 
463                 if(matchFirstLine)
464                 {
465                     if(lines.Count == 0)
466                     {
467                         return null;
468                     }
469                     if(matcher(lines.First()))
470                     {
471                         return HandleSuccess(eventName, matchingLineId: 0);
472                     }
473                     return null;
474                 }
475 
476                 var index = lines.FindIndex(matcher);
477                 if(index != -1)
478                 {
479                     return HandleSuccess(eventName, matchingLineId: index, matchGroups: matchGroups);
480                 }
481 
482                 return null;
483             }
484         }
485 
CheckUnfinishedLine(string pattern, bool regex, string eventName, bool matchAtStart = false)486         private TerminalTesterResult CheckUnfinishedLine(string pattern, bool regex, string eventName, bool matchAtStart = false)
487         {
488             var content = currentLineBuffer.ToString();
489 
490 #if DEBUG_EVENTS
491             this.Log(LogLevel.Noisy, "Current line buffer content: >>{0}<<", content);
492 #endif
493 
494             var matchStart = -1;
495             var matchEnd = -1;
496             string[] matchGroups = null;
497 
498             if(regex)
499             {
500                 // In binary mode, make . match any character
501                 var options = binaryMode ? RegexOptions.Singleline : 0;
502                 var match = Regex.Match(content, pattern, options);
503                 if(match.Success)
504                 {
505                     matchStart = match.Index;
506                     matchEnd = match.Index + match.Length;
507                     matchGroups = GetMatchGroups(match);
508                 }
509             }
510             else
511             {
512                 // In binary mode, use ordinal (here: raw byte value) comparison.
513                 var comparisonType = binaryMode ? StringComparison.Ordinal : StringComparison.CurrentCulture;
514                 matchStart = content.IndexOf(pattern, comparisonType);
515                 if(matchStart != -1)
516                 {
517                     matchEnd = matchStart + pattern.Length;
518                 }
519             }
520 
521             // If we want a match at the start, then anything other than 0 simply won't cut it.
522             if(matchAtStart && matchStart != 0)
523             {
524                 return null;
525             }
526 
527             // We know the match was successful, but only want to cut the current line buffer exactly when in binary mode.
528             // Otherwise, we just throw out the whole thing.
529             if(matchStart != -1)
530             {
531                 return HandleSuccess(eventName, matchingLineId: CurrentLine, matchGroups: matchGroups,
532                     matchStart: binaryMode ? matchStart : 0,
533                     matchEnd: binaryMode ? (int?)matchEnd : null);
534             }
535 
536             return null;
537         }
538 
GetMatchGroups(Match match)539         private string[] GetMatchGroups(Match match)
540         {
541             // group '0' is the whole match; we return it in a separate field, so we don't want it here
542             return match.Groups.Cast<Group>().Skip(1).Select(y => y.Value).ToArray();
543         }
544 
FinishSGRDecoding(bool abort = false)545         private void FinishSGRDecoding(bool abort = false)
546         {
547 #if DEBUG_EVENTS
548             this.Log(LogLevel.Noisy, "Finishing SGR decoding (abort: {0}, filterBuffer: >>{1}<<)", abort, sgrDecodingBuffer.ToString());
549 #endif
550             var sgr = sgrDecodingBuffer.Unload();
551             if(abort)
552             {
553                 currentLineBuffer.Append(sgr);
554             }
555 
556             sgrDecodingState = SGRDecodingState.NotDecoding;
557         }
558 
AppendCharToBuffer(char value)559         private void AppendCharToBuffer(char value)
560         {
561 #if DEBUG_EVENTS
562             this.Log(LogLevel.Noisy, "Appending char >>{0}<< to buffer in state {1}", value, sgrDecodingState);
563 #endif
564             if(binaryMode)
565             {
566                 currentLineBuffer.Append(value);
567                 return;
568             }
569 
570             switch(sgrDecodingState)
571             {
572                 case SGRDecodingState.NotDecoding:
573                 {
574                     if(value == EscapeChar)
575                     {
576                         sgrDecodingBuffer.Append(value);
577                         sgrDecodingState = SGRDecodingState.EscapeDetected;
578                     }
579                     else
580                     {
581                         currentLineBuffer.Append(value);
582                     }
583                 }
584                 break;
585 
586                 case SGRDecodingState.EscapeDetected:
587                 {
588                     if(value == '[')
589                     {
590                         sgrDecodingState = SGRDecodingState.LeftBracketDetected;
591                     }
592                     else
593                     {
594                         FinishSGRDecoding(abort: true);
595                     }
596                 }
597                 break;
598 
599                 case SGRDecodingState.LeftBracketDetected:
600                 {
601                     if((value >= '0' && value <= '9') || value == ';')
602                     {
603                         sgrDecodingBuffer.Append(value);
604                     }
605                     else
606                     {
607                         FinishSGRDecoding(abort: value != 'm');
608                     }
609                     break;
610                 }
611 
612                 default:
613                     throw new ArgumentException($"Unexpected state when decoding an SGR code: {sgrDecodingState}");
614             }
615         }
616 
617         private const int NoLine = -2;
618         private const int CurrentLine = -1;
619 
HandleSuccess(string eventName, int matchingLineId, string[] matchGroups = null, int matchStart = 0, int? matchEnd = null)620         private TerminalTesterResult HandleSuccess(string eventName, int matchingLineId, string[] matchGroups = null, int matchStart = 0, int? matchEnd = null)
621         {
622             lock(lines)
623             {
624                 var numberOfLinesToCopy = 0;
625                 var includeCurrentLineBuffer = false;
626                 switch(matchingLineId)
627                 {
628                     case NoLine:
629                         // default values are ok
630                         break;
631 
632                     case CurrentLine:
633                         includeCurrentLineBuffer = true;
634                         numberOfLinesToCopy = lines.Count;
635                         break;
636 
637                     default:
638                         numberOfLinesToCopy = matchingLineId + 1;
639                         break;
640                 }
641 
642                 ReportInner(eventName, "success", numberOfLinesToCopy, includeCurrentLineBuffer);
643 
644                 string content = null;
645                 double timestamp = 0;
646                 if(includeCurrentLineBuffer)
647                 {
648                     timestamp = machine.ElapsedVirtualTime.TimeElapsed.TotalMilliseconds;
649 
650                     content = currentLineBuffer.Unload(matchEnd).Substring(matchStart);
651                 }
652                 else if(numberOfLinesToCopy > 0)
653                 {
654                     var item = lines[matchingLineId];
655                     content = item.Content;
656                     timestamp = item.VirtualTimestamp;
657                 }
658 
659                 lines.RemoveRange(0, numberOfLinesToCopy);
660                 if(content != null && !binaryMode)
661                 {
662                     content = content.StripNonSafeCharacters();
663                 }
664 
665                 return new TerminalTesterResult(content, timestamp, matchGroups);
666             }
667         }
668 
HandleFailure(string eventName)669         private void HandleFailure(string eventName)
670         {
671             lock(lines)
672             {
673                 ReportInner(eventName, "failure", lines.Count, true);
674             }
675         }
676 
677         // does not need to be locked, as both `HandleSuccess` and `HandleFailure` use locks
ReportInner(string eventName, string what, int copyLinesToReport, bool includeCurrentLineBuffer)678         private void ReportInner(string eventName, string what, int copyLinesToReport, bool includeCurrentLineBuffer)
679         {
680             if(copyLinesToReport > 0)
681             {
682                 foreach(var line in lines.Take(copyLinesToReport))
683                 {
684                     report.AppendLine(line.ToString());
685                 }
686             }
687 
688             if(includeCurrentLineBuffer)
689             {
690                 // Don't say there was no newline if we are in binary mode as it does not operate on lines
691                 var newlineIndication = !binaryMode ? " [[no newline]]" : "";
692                 var displayString = currentLineBuffer.ToString();
693                 if(binaryMode)
694                 {
695                     // Not using PrettyPrintCollection(Hex) to use simpler `01 02 03...` formatting.
696                     displayString = string.Join(" ", displayString.Select(ch => ((byte)ch).ToHex()));
697                 }
698                 report.AppendFormat("{0}{1}\n", displayString, newlineIndication);
699             }
700 
701             var virtMs = machine.ElapsedVirtualTime.TimeElapsed.TotalMilliseconds;
702             report.AppendFormat("([host: {2}, virt: {3, 7}] {0} event: {1})\n", eventName, what, CustomDateTime.Now, virtMs);
703         }
704 
705         private IMachine machine;
706         private SGRDecodingState sgrDecodingState;
707         private string generatedReport;
708         private Func<TerminalTesterResult> resultMatcher;
709         private TerminalTesterResult testerResult;
710         private bool pauseEmulation;
711         private bool delayedCharInProgress;
712 
713         // Similarly how it is handled for FrameBufferTester it shouldn't matter if we unset the charEvent during deserialization
714         // as we check for char match on load in `WaitForMatch` either way
715         // Additionally in `IsIdle` the timeout would long since expire so it doesn't matter there either.
716         [Constructor(false)]
717         private AutoResetEvent charEvent;
718         // The same logic as above also applies for the matchEvent.
719         [Constructor(false)]
720         private AutoResetEvent matchEvent;
721         private readonly SafeStringBuilder currentLineBuffer;
722         private readonly SafeStringBuilder sgrDecodingBuffer;
723         private readonly EndLineOption endLineOption;
724         private readonly bool binaryMode;
725         private readonly List<Line> lines;
726         private readonly SafeStringBuilder report;
727         private readonly Queue<Tuple<TimeSpan, char>> delayedChars = new Queue<Tuple<TimeSpan, char>>();
728 
729         private const char LineFeed = '\x0A';
730         private const char CarriageReturn = '\x0D';
731         private const char EscapeChar = '\x1B';
732 
733         private class Line
734         {
Line(string content, double timestamp)735             public Line(string content, double timestamp)
736             {
737                 this.Content = content;
738                 this.VirtualTimestamp = timestamp;
739                 this.HostTimestamp = CustomDateTime.Now;
740             }
741 
ToString()742             public override string ToString()
743             {
744                 return $"[host: {HostTimestamp}, virt: {(int)VirtualTimestamp, 7}] {Content}";
745             }
746 
747             public string Content { get; }
748             public double VirtualTimestamp { get; }
749             public DateTime HostTimestamp { get; }
750         }
751 
752         private class MultilineMatcher
753         {
MultilineMatcher(string[] patterns, bool regex, bool lastUnfinished)754             public MultilineMatcher(string[] patterns, bool regex, bool lastUnfinished)
755             {
756                 if(regex)
757                 {
758                     matchLine = patterns.Select(pattern =>
759                     {
760                         var matcher = new Regex(pattern);
761                         return (Predicate<string>)(x => matcher.Match(x).Success);
762                     }).ToArray();
763                 }
764                 else
765                 {
766                     matchLine = patterns
767                         .Select(pattern => (Predicate<string>)(x => x.Contains(pattern)))
768                         .ToArray()
769                     ;
770                 }
771 
772                 length = patterns.Length - (lastUnfinished ? 2 : 1);
773                 candidates = new bool[length];
774             }
775 
FeedLine(Line line)776             public bool FeedLine(Line line)
777             {
778                 // Let lines[0] be the current `line` and lines[-n] be nth previous line.
779                 // The value of candidates[(last + n) % length] is a conjunction for k=1..n of line[-k] matching pattern[k - 1]
780                 if(candidates[last] && matchLine[length](line.Content))
781                 {
782                     return true;
783                 }
784 
785                 last = (last + 1) % length;
786                 var l = last;
787                 for(var i = 0; i < length; i++)
788                 {
789                     var patternIndex = length - 1 - i;
790                     if(candidates[l] || patternIndex == 0)
791                     {
792                         candidates[l] = matchLine[patternIndex](line.Content);
793                     }
794                     l = (l + 1) % length;
795                 }
796 
797                 return false;
798             }
799 
CheckUnfinishedLine(string line)800             public bool CheckUnfinishedLine(string line)
801             {
802                 return matchLine[matchLine.Length - 1](line);
803             }
804 
805             private int last;
806             private readonly Predicate<string>[] matchLine;
807             private readonly bool[] candidates;
808             private readonly int length;
809         }
810 
811         private enum SGRDecodingState
812         {
813             NotDecoding,
814             EscapeDetected,
815             LeftBracketDetected
816         }
817     }
818 
819     public class TerminalTesterResult
820     {
TerminalTesterResult(string content, double timestamp, string[] groups = null)821         public TerminalTesterResult(string content, double timestamp, string[] groups = null)
822         {
823             this.line = content ?? string.Empty;
824             this.timestamp = timestamp;
825             this.groups = groups ?? new string[0];
826         }
827 
828         public string line { get; }
829         public double timestamp { get; }
830         public string[] groups { get; set; }
831     }
832 
833     public enum EndLineOption
834     {
835         TreatLineFeedAsEndLine,
836         TreatCarriageReturnAsEndLine
837     }
838 }
839 
840