Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System;
using System.Collections.Generic;
using System.Threading;

using Microsoft.TestPlatform.Extensions.TrxLogger.Utility;

Expand All @@ -16,6 +17,7 @@ namespace Microsoft.TestPlatform.Extensions.TrxLogger.ObjectModel;
internal class TestResultAggregation : TestResult, ITestResultAggregation
{
protected List<ITestResult>? _innerResults;
private long _innerResultCount;

public TestResultAggregation(
Guid runId,
Expand All @@ -42,6 +44,21 @@ public List<ITestResult> InnerResults
}
}

/// <summary>
/// Atomically adds an inner result and returns the new count.
/// Use the returned count (== 1) to detect the first inner result in a thread-safe way.
/// </summary>
public long AddInnerResult(ITestResult result)
{
lock (this)
{
_innerResults ??= new List<ITestResult>();
Comment on lines +51 to +55
_innerResults.Add(result);
}

return Interlocked.Increment(ref _innerResultCount);
}

public override void Save(System.Xml.XmlElement element, XmlTestStoreParameters? parameters)
{
base.Save(element, parameters);
Expand Down
31 changes: 27 additions & 4 deletions src/Microsoft.TestPlatform.Extensions.TrxLogger/TrxLogger.cs
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,8 @@ internal void TestResultHandler(object? sender, TestResultEventArgs e)
UpdateTestLinks(testElement, parentTestElement);

// Convert the rocksteady result to trx test result
var testResult = CreateTestResult(executionId, parentExecutionId, testType, testElement, parentTestElement, parentTestResult, e.Result);
var testResult = CreateTestResult(executionId, parentExecutionId, testType, testElement, parentTestElement, parentTestResult, e.Result,
out bool isFirstDataDrivenInnerResult);

// Update test entries
UpdateTestEntries(executionId, parentExecutionId, testElement, parentTestElement);
Expand All @@ -329,6 +330,24 @@ internal void TestResultHandler(object? sender, TestResultEventArgs e)
{
Interlocked.Increment(ref _passedTestCount);
}

// When the first inner DataDriven result is encountered, the parent is promoted to a
// DataDrivenTest container. Undo the count that was previously recorded for the parent,
// since the parent is only a container and should not be counted as a separate test result.
// isFirstDataDrivenInnerResult is set atomically via Interlocked.Increment, so this is
// race-safe even when multiple data-row results are processed concurrently.
if (isFirstDataDrivenInnerResult)
{
Interlocked.Decrement(ref _totalTestCount);
if (parentTestResult!.Outcome == TrxLoggerObjectModel.TestOutcome.Failed)
{
Interlocked.Decrement(ref _failedTestCount);
}
else if (parentTestResult.Outcome == TrxLoggerObjectModel.TestOutcome.Passed)
{
Interlocked.Decrement(ref _passedTestCount);
}
}
}

/// <summary>
Expand Down Expand Up @@ -695,11 +714,14 @@ private static void UpdateTestLinks(ITestElement testElement, ITestElement? pare
/// <param name="parentTestElement"></param>
/// <param name="parentTestResult"></param>
/// <param name="rocksteadyTestResult"></param>
/// <param name="isFirstDataDrivenInnerResult">Set to true when this is the first inner DataDriven result for the parent, indicating the parent count should be undone.</param>
/// <returns>Trx test result</returns>
private ITestResult CreateTestResult(Guid executionId, Guid parentExecutionId, TestType testType,
ITestElement testElement, ITestElement? parentTestElement, ITestResult? parentTestResult, ObjectModel.TestResult rocksteadyTestResult)
ITestElement testElement, ITestElement? parentTestElement, ITestResult? parentTestResult, ObjectModel.TestResult rocksteadyTestResult,
out bool isFirstDataDrivenInnerResult)
{
TPDebug.Assert(IsInitialized, "Logger is not initialized");
isFirstDataDrivenInnerResult = false;
// Create test result
TrxLoggerObjectModel.TestOutcome testOutcome = Converter.ToOutcome(rocksteadyTestResult.Outcome);
TPDebug.Assert(LoggerTestRun != null, "LoggerTestRun is null");
Expand Down Expand Up @@ -727,10 +749,11 @@ private ITestResult CreateTestResult(Guid executionId, Guid parentExecutionId, T
{
TPDebug.Assert(parentTestResult is TestResultAggregation, "parentTestResult is not of type TestResultAggregation");
var testResultAggregation = (TestResultAggregation)parentTestResult;
testResultAggregation.InnerResults.Add(testResult);
testResult.DataRowInfo = testResultAggregation.InnerResults.Count;
var innerCount = testResultAggregation.AddInnerResult(testResult);
testResult.DataRowInfo = (int)innerCount;
testResult.ResultType = TrxLoggerConstants.InnerDataDrivenResultType;
parentTestResult.ResultType = TrxLoggerConstants.ParentDataDrivenResultType;
isFirstDataDrivenInnerResult = innerCount == 1;
return testResult;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,38 @@ private static void IsFileAndContentEqual(string filePath)
return null;
}

[TestMethod]
[NetFullTargetFrameworkDataSource]
[NetCoreTargetFrameworkDataSource]
public void TrxLoggerShouldNotDoubleCountDataDrivenTestResults(RunnerInfo runnerInfo)
{
// Regression test for https://github.com/microsoft/vstest/issues/15643
// DataDriven (DataRow) test results were double-counted in TRX ResultSummary:
// both the parent container and each inner data row result were counted.
SetTestEnvironment(_testEnvironment, runnerInfo);

var assemblyPaths = GetAssetFullPath("DataDrivenTestProject.dll");
var trxFilePath = Path.Combine(TempDirectory.Path, "DataDriven.trx");
var arguments = PrepareArguments(assemblyPaths, null, string.Empty, FrameworkArgValue, runnerInfo.InIsolationValue, resultsDirectory: TempDirectory.Path);
arguments = string.Concat(arguments, $" /logger:\"trx;LogFileName={trxFilePath}\"");

InvokeVsTest(arguments);

ValidateSummaryStatus(4, 0, 0);

// Parse the TRX file and verify Counters reflect actual test executions (4),
// not inflated by parent container results.
var totalAttr = GetElementAttributeValueFromTrx(trxFilePath, "Counters", "total");
var passedAttr = GetElementAttributeValueFromTrx(trxFilePath, "Counters", "passed");

Assert.IsNotNull(totalAttr, "TRX Counters element should have a 'total' attribute.");
Assert.IsNotNull(passedAttr, "TRX Counters element should have a 'passed' attribute.");
// DataDrivenTestProject has: 3 DataRow rows + 1 SimpleTest = 4 test executions.
// Before the fix, total would be 5 (parent container counted as extra).
Assert.AreEqual("4", totalAttr, "TRX total count should reflect actual test executions, not include parent containers.");
Assert.AreEqual("4", passedAttr, "TRX passed count should reflect actual passed tests.");
}

[TestMethod]
[NetFullTargetFrameworkDataSource]
[NetCoreTargetFrameworkDataSource]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -362,7 +362,7 @@ public void TestResultHandlerShouldAddHierarchicalResultsIfParentTestResultIsPre
_testableTrxLogger.TestResultHandler(new object(), resultEventArg3.Object);

Assert.AreEqual(1, _testableTrxLogger.TestResultCount, "TestResultHandler is not creating hierarchical results when parent result is present.");
Assert.AreEqual(3, _testableTrxLogger.TotalTestCount, "TestResultHandler is not adding all inner results in parent test result.");
Assert.AreEqual(2, _testableTrxLogger.TotalTestCount, "TestResultHandler should count only inner DataDriven results, not the parent container.");
}

[TestMethod]
Expand Down Expand Up @@ -525,6 +525,43 @@ public void TestResultHandlerShouldAddSingleTestEntryForOrderedTest()
Assert.AreEqual(1, _testableTrxLogger.TestEntryCount, "TestResultHandler is adding multiple test entries for ordered test.");
}

[TestMethod]
public void TestResultHandlerShouldNotDoubleCountParentDataDrivenTestInTotalTestCount()
{
// Regression test for https://github.com/microsoft/vstest/issues/15643
// DataDriven test results were double-counted: the parent container result AND each inner
// data row result were all included in TotalTestCount / PassedTestCount / FailedTestCount.
Comment thread
nohwnd marked this conversation as resolved.
TestCase testCase1 = CreateTestCase("TestCase1");

Guid parentExecutionId = Guid.NewGuid();

// Parent (container) result – arrives first
VisualStudio.TestPlatform.ObjectModel.TestResult parentResult = new(testCase1);
parentResult.Outcome = TestOutcome.Passed;
parentResult.SetPropertyValue(TrxLoggerConstants.ExecutionIdProperty, parentExecutionId);

// Inner data-row result 1 – Passed
VisualStudio.TestPlatform.ObjectModel.TestResult innerResult1 = new(testCase1);
innerResult1.Outcome = TestOutcome.Passed;
innerResult1.SetPropertyValue(TrxLoggerConstants.ExecutionIdProperty, Guid.NewGuid());
innerResult1.SetPropertyValue(TrxLoggerConstants.ParentExecIdProperty, parentExecutionId);

// Inner data-row result 2 – Failed
VisualStudio.TestPlatform.ObjectModel.TestResult innerResult2 = new(testCase1);
innerResult2.Outcome = TestOutcome.Failed;
innerResult2.SetPropertyValue(TrxLoggerConstants.ExecutionIdProperty, Guid.NewGuid());
innerResult2.SetPropertyValue(TrxLoggerConstants.ParentExecIdProperty, parentExecutionId);

_testableTrxLogger.TestResultHandler(new object(), new Mock<TestResultEventArgs>(parentResult).Object);
_testableTrxLogger.TestResultHandler(new object(), new Mock<TestResultEventArgs>(innerResult1).Object);
_testableTrxLogger.TestResultHandler(new object(), new Mock<TestResultEventArgs>(innerResult2).Object);

// TotalTestCount should reflect the 2 actual data-row executions, not 3 (parent + 2 inner).
Assert.AreEqual(2, _testableTrxLogger.TotalTestCount, "Parent DataDriven result must not be counted separately in TotalTestCount.");
Assert.AreEqual(1, _testableTrxLogger.PassedTestCount, "Only the passed inner result should be counted.");
Assert.AreEqual(1, _testableTrxLogger.FailedTestCount, "Only the failed inner result should be counted.");
}

[TestMethod]
public void TestRunCompleteHandlerShouldReportFailedOutcomeIfTestRunIsAborted()
{
Expand Down
21 changes: 21 additions & 0 deletions test/TestAssets/DataDrivenTestProject/DataDrivenTestProject.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<AssemblyName>DataDrivenTestProject</AssemblyName>
<TargetFrameworks>$(TestProjectTargetFrameworks)</TargetFrameworks>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="MSTest.TestFramework">
<Version>$(MSTestTestFrameworkVersion)</Version>
</PackageReference>
<PackageReference Include="MSTest.TestAdapter">
<Version>$(MSTestTestAdapterVersion)</Version>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk">
<Version>$(PackageVersion)</Version>
</PackageReference>
</ItemGroup>

</Project>
25 changes: 25 additions & 0 deletions test/TestAssets/DataDrivenTestProject/DataDrivenTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace DataDrivenTestProject;

[TestClass]
public class DataDrivenTests
{
[TestMethod]
[DataRow(1, "first")]
[DataRow(2, "second")]
[DataRow(3, "third")]
public void ParameterizedTest(int value, string name)
{
Assert.IsTrue(value > 0);
Assert.IsNotNull(name);
}

[TestMethod]
public void SimpleTest()
{
}
}
1 change: 1 addition & 0 deletions test/TestAssets/TestAssets.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
<Project Path="SimpleDataCollector/SimpleDataCollector.csproj" />
<Project Path="SimpleTestAdapter/SimpleTestAdapter.csproj" />
<Project Path="SimpleTestProject/SimpleTestProject.csproj" />
<Project Path="DataDrivenTestProject/DataDrivenTestProject.csproj" />
<Project Path="SimpleTestProject2/SimpleTestProject2.csproj" />
<Project Path="SimpleTestProject3/SimpleTestProject3.csproj" />
<Project Path="SimpleTestProject4/SimpleTestProject4.csproj" />
Expand Down
Loading