[fix] Fix DataDrivenTest parent result being double-counted in TRX summary#15757
[fix] Fix DataDrivenTest parent result being double-counted in TRX summary#15757nohwnd wants to merge 2 commits into
Conversation
The TRX logger was counting both the parent DataDrivenTest result and each inner data row result toward the test run totals. For a test with N data rows, this caused N+1 counts (1 parent + N children) instead of the correct N counts. The fix detects when the first inner data-driven result is added (i.e., the parent's InnerResults.Count reaches 1) and subtracts the parent's previously-counted contribution from the totals. Fixes #15643 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR fixes TRX ResultSummary totals for MSTest data-driven tests so that only the individual data row results contribute to TotalTestCount/PassedTestCount/FailedTestCount, matching console logger behavior and resolving #15643.
Changes:
- Adjust
TrxLogger.TestResultHandlerto “undo” the previously-counted parent data-driven result when the first inner data-row result is attached to it. - Update an existing hierarchical-results unit test to expect data-row-only totals.
- Add a dedicated unit test validating total/passed/failed counts for a parent + 2 data-row scenario.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
src/Microsoft.TestPlatform.Extensions.TrxLogger/TrxLogger.cs |
Fixes summary counting by subtracting the parent data-driven container’s contribution once the first inner data-row result is observed. |
test/Microsoft.TestPlatform.Extensions.TrxLogger.UnitTests/TrxLoggerTests.cs |
Updates expectations for hierarchical/data-driven results and adds coverage for passed/failed counting with inner data rows. |
nohwnd
left a comment
There was a problem hiding this comment.
The fix correctly addresses the N+1 counting bug for data-driven tests in the TRX logger. The logic — detect the first inner data-row result and subtract the parent's already-counted contribution — mirrors how the console logger handles the same scenario, and the new tests validate the correct counts.
One threading concern is flagged inline: the InnerResults.Count == 1 sentinel can be missed if two inner results are dispatched concurrently, leaving the parent contribution uncorrected. A ConcurrentDictionary-based "already corrected" flag would make the correction exactly-once and race-free.
🧠 Reviewed by Expert Code Reviewer
🧠 Reviewed by Expert Code Reviewer 🧠
| if (parentTestElement != null | ||
| && parentTestElement.TestType.Equals(TrxLoggerConstants.UnitTestType) | ||
| && parentTestResult is TestResultAggregation parentAggregation | ||
| && parentAggregation.InnerResults.Count == 1) |
There was a problem hiding this comment.
[Threading] The InnerResults.Count == 1 sentinel relies on the assumption that TestResultHandler is never called concurrently. If two inner data-row results arrive on different threads simultaneously, both threads call testResultAggregation.InnerResults.Add(testResult) (inside CreateTestResult, line 727) on a non-thread-safe List<ITestResult> before either checks the count. After both adds complete, Count is already 2, so neither thread sees Count == 1 and the parent's contribution is never decremented — leaving _totalTestCount inflated by 1.
The fact that the surrounding code already uses Interlocked for the counters signals that concurrent invocation is a real concern (defensive or otherwise).
Suggestion: Instead of checking Count == 1, use a per-aggregation "already corrected" flag (e.g., a ConcurrentDictionary<Guid, bool> keyed by parentExecutionId) and a TryAdd/CompareExchange to ensure the decrement fires exactly once regardless of arrival order:
if (parentTestElement != null
&& parentTestElement.TestType.Equals(TrxLoggerConstants.UnitTestType)
&& parentTestResult is TestResultAggregation
&& _parentResultsAlreadyCorrected.TryAdd(parentExecutionId, true))
{
Interlocked.Decrement(ref _totalTestCount);
...
}This is safe even under concurrent dispatch because TryAdd is atomic.
There was a problem hiding this comment.
Fixed. Replaced InnerResults.Count == 1 with a ConcurrentDictionary<Guid, bool> (_parentResultsCorrected) that uses TryAdd to atomically ensure the decrement fires exactly once, regardless of arrival order under concurrent dispatch. Build succeeded (0 warnings/errors, Release config) and all TrxLogger unit tests pass.
🔧 Iterated by PR Iteration Agent 🔧
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
|
The threading fix has been implemented, built (Release config, 0 warnings/errors), and all tests pass (69 TrxLogger tests + full suite). Changes made (commit
|
…nt correction Replace InnerResults.Count == 1 sentinel with a ConcurrentDictionary<Guid, bool> (_parentResultsCorrected) that uses TryAdd to atomically ensure the parent's count contribution is decremented exactly once, safe under concurrent dispatch. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Commit pushed:
|
nohwnd
left a comment
There was a problem hiding this comment.
The threading concern from the previous review (using InnerResults.Count == 1 as a sentinel) has been addressed correctly. The updated fix uses ConcurrentDictionary<Guid, bool>.TryAdd to ensure the parent's contribution is decremented exactly once, regardless of concurrent inner-result dispatch.
All aspects of the fix look solid:
TryAddatomicity: Correctly ensures exactly-once decrement even under concurrentTestResultHandlercalls- Outcome snapshot: The parent's
Outcomeis evaluated at the time of correction — that's the outcome set when the parent result was processed, which is the right value to undo Guid.Emptyguard: WhenparentExecutionId == Guid.Empty,parentTestElementis reset tonull(line 294), so the correction block is never entered with a degenerate key- Test coverage: The new test independently validates
TotalTestCount,PassedTestCount, andFailedTestCount
🧠 Reviewed by Expert Code Reviewer 🧠
🧠 Reviewed by Expert Code Reviewer 🧠
|
/azp run |
|
Azure Pipelines successfully started running 1 pipeline(s). |
|
Closing in favor of #15766 which uses a cleaner atomic approach (AddInnerResult on TestResultAggregation) rather than a separate ConcurrentDictionary. |
Summary
Fixes #15643
Root Cause
When a data-driven test (e.g., a test with
[DataRow]attributes) runs, the TRX logger receives two types ofTestResultHandlerevents:DataDrivenTestresult (a container that holds all data row results)DataDrivenDataRowresult (the actual per-row execution)Previously, all events incremented
_totalTestCount,_passedTestCount, and_failedTestCountunconditionally. This caused N+1 counts (parent + N data rows) instead of the correct N counts (just the data rows).Fix
In
TestResultHandler, afterCreateTestResultreturns, detect when the first inner data-driven result has been added to its parent (parentTestElement.TestType == UnitTestTypeandparentAggregation.InnerResults.Count == 1). At that point, subtract the parent's already-counted contribution from the totals.This mirrors how the console logger works (which does report the correct total).
Test
TestResultHandlerShouldAddHierarchicalResultsIfParentTestResultIsPresenttest to assertTotalTestCount == 2(not 3) for a data-driven test with 1 parent + 2 data rows.TestResultHandlerShouldCountOnlyDataRowResultsNotParentForDataDrivenTeststhat explicitly validatesTotalTestCount,PassedTestCount, andFailedTestCountare correct for a data-driven test scenario.All 69 TrxLogger unit tests pass.