From 645496a34d233470e0eb54a9003e86510a57a73a Mon Sep 17 00:00:00 2001 From: "Vasudha Salapala (TATA CONSULTANCY SERVICES LTD)" Date: Fri, 28 Aug 2026 15:18:36 +0530 Subject: [PATCH] Fix parser race condition --- ...mingStandardOutputTestResultParserTests.cs | 68 +++++++++++++++++++ ...StreamingStandardOutputTestResultParser.cs | 35 ++++++---- 2 files changed, 89 insertions(+), 14 deletions(-) diff --git a/GoogleTestAdapter/Core.Tests/TestResults/StreamingStandardOutputTestResultParserTests.cs b/GoogleTestAdapter/Core.Tests/TestResults/StreamingStandardOutputTestResultParserTests.cs index 9793c055a..6847d0d69 100644 --- a/GoogleTestAdapter/Core.Tests/TestResults/StreamingStandardOutputTestResultParserTests.cs +++ b/GoogleTestAdapter/Core.Tests/TestResults/StreamingStandardOutputTestResultParserTests.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Text; using System.Threading; +using System.Threading.Tasks; using FluentAssertions; using GoogleTestAdapter.DiaResolver; using GoogleTestAdapter.Model; @@ -395,6 +396,73 @@ public void OutputHandling_OneLine_IsParsedCorrectly() testResult.ErrorMessage.Should().Be(expectedErrorMessage); } + [TestMethod] + [TestCategory(Unit)] + public void ReportLine_ConcurrentFlush_DoesNotOvertakeIncompleteStateTransition() + { + const int timeoutMilliseconds = 5000; + var cases = new List + { + TestDataCreator.ToTestCase("Test.First", TestDataCreator.DummyExecutable, ""), + TestDataCreator.ToTestCase("Test.Second", TestDataCreator.DummyExecutable, "") + }; + var reportedResults = new List(); + + using (var reportLinePaused = new ManualResetEventSlim()) + using (var releaseReportLine = new ManualResetEventSlim()) + using (var flushStarted = new ManualResetEventSlim()) + { + MockFrameworkReporter + .Setup(r => r.ReportTestResults(It.IsAny>())) + .Callback>(results => reportedResults.AddRange(results)); + MockFrameworkReporter + .Setup(r => r.ReportTestsStarted( + It.Is>(testCases => + testCases.Single().FullyQualifiedName == "Test.Second"))) + .Callback(() => + { + reportLinePaused.Set(); + releaseReportLine.Wait(timeoutMilliseconds).Should().BeTrue(); + }); + + var parser = new StreamingStandardOutputTestResultParser( + cases, MockLogger.Object, MockFrameworkReporter.Object, String.Empty); + parser.ReportLine("[ RUN ] Test.First"); + parser.ReportLine("[ OK ] Test.First (1 ms)"); + + Task reportLineTask = Task.Run(() => + parser.ReportLine("[ RUN ] Test.Second[ OK ] Test.Second (2 ms)")); + reportLinePaused.Wait(timeoutMilliseconds).Should().BeTrue(); + + Task flushTask = Task.Run(() => + { + flushStarted.Set(); + parser.Flush(); + }); + flushStarted.Wait(timeoutMilliseconds).Should().BeTrue(); + + bool flushCompletedBeforeRelease; + try + { + flushCompletedBeforeRelease = flushTask.Wait(500); + } + finally + { + releaseReportLine.Set(); + } + + Task.WaitAll(new[] { reportLineTask, flushTask }, timeoutMilliseconds).Should().BeTrue(); + + flushCompletedBeforeRelease.Should().BeFalse(); + reportedResults.Select(result => result.TestCase.FullyQualifiedName) + .Should().Equal("Test.First", "Test.Second"); + parser.TestResults.Select(result => result.TestCase.FullyQualifiedName) + .Should().Equal("Test.First", "Test.Second"); + reportedResults.Should().OnlyContain(result => result.ErrorMessage == null); + parser.CrashedTestCase.Should().BeNull(); + } + } + private IList GetTestResultsFromCompleteOutputFile() { var testCases = new GoogleTestDiscoverer(MockLogger.Object, MockOptions.Object, new DefaultDiaResolverFactory()) diff --git a/GoogleTestAdapter/Core/TestResults/StreamingStandardOutputTestResultParser.cs b/GoogleTestAdapter/Core/TestResults/StreamingStandardOutputTestResultParser.cs index aba7c4528..666b209ea 100644 --- a/GoogleTestAdapter/Core/TestResults/StreamingStandardOutputTestResultParser.cs +++ b/GoogleTestAdapter/Core/TestResults/StreamingStandardOutputTestResultParser.cs @@ -27,6 +27,7 @@ public class StreamingStandardOutputTestResultParser private readonly ILogger _logger; private readonly ITestFrameworkReporter _reporter; + private readonly object _syncObject = new object(); private readonly List _consoleOutput = new List(); private readonly string _executable; @@ -58,19 +59,22 @@ public List GetXMLResults(IEnumerable testCasesRun) public void ReportLine(string line) { - Match testEndMatch = PrefixedLineRegex.Match(line); - if (testEndMatch.Success) + lock (_syncObject) { - string restOfErrorMessage = testEndMatch.Groups[1].Value; - if (!string.IsNullOrEmpty(restOfErrorMessage)) - DoReportLine(restOfErrorMessage); + Match testEndMatch = PrefixedLineRegex.Match(line); + if (testEndMatch.Success) + { + string restOfErrorMessage = testEndMatch.Groups[1].Value; + if (!string.IsNullOrEmpty(restOfErrorMessage)) + DoReportLine(restOfErrorMessage); - string testEndPart = testEndMatch.Groups[2].Value; - DoReportLine(testEndPart); - } - else - { - DoReportLine(line); + string testEndPart = testEndMatch.Groups[2].Value; + DoReportLine(testEndPart); + } + else + { + DoReportLine(line); + } } } @@ -95,10 +99,13 @@ private void DoReportLine(string line) public void Flush() { - if (_consoleOutput.Count > 0) + lock (_syncObject) { - ReportTestResult(); - _consoleOutput.Clear(); + if (_consoleOutput.Count > 0) + { + ReportTestResult(); + _consoleOutput.Clear(); + } } }