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