From 002dcddbe9e35e787c19bd3a1ee9d570dea4611e Mon Sep 17 00:00:00 2001 From: Fred Bricon Date: Mon, 13 Oct 2025 13:30:23 +0200 Subject: [PATCH] fix: update parent node state when test completes Signed-off-by: Fred Bricon --- .../baseRunner/RunnerResultAnalyzer.ts | 97 ++++++++++++++++++- .../junitRunner/JUnitRunnerResultAnalyzer.ts | 6 ++ .../TestNGRunnerResultAnalyzer.ts | 8 +- 3 files changed, 109 insertions(+), 2 deletions(-) diff --git a/src/runners/baseRunner/RunnerResultAnalyzer.ts b/src/runners/baseRunner/RunnerResultAnalyzer.ts index af6848e4..1009b160 100644 --- a/src/runners/baseRunner/RunnerResultAnalyzer.ts +++ b/src/runners/baseRunner/RunnerResultAnalyzer.ts @@ -2,10 +2,14 @@ // Licensed under the MIT license. import { Location, MarkdownString, TestItem } from 'vscode'; -import { IRunTestContext } from '../../java-test-runner.api'; +import { dataCache, ITestItemData } from '../../controller/testItemDataCache'; +import { IRunTestContext, TestLevel, TestResultState } from '../../java-test-runner.api'; import { processStackTraceLine } from '../utils'; export abstract class RunnerResultAnalyzer { + // Track parent test item states to update them when all children complete + protected parentStates: Map = new Map(); + constructor(protected testContext: IRunTestContext) { } public abstract analyzeData(data: string): void; @@ -36,4 +40,95 @@ export abstract class RunnerResultAnalyzer { return stacktrace.includes(s); }); } + + /** + * Initialize parent state tracking for a test item. + * Counts how many method-level children are being tested. + */ + protected initializeParentState(item: TestItem, triggeredTestsMapping: Map): void { + const parent: TestItem | undefined = item.parent; + if (!parent) { + return; + } + + const parentData: ITestItemData | undefined = dataCache.get(parent); + if (!parentData || parentData.testLevel !== TestLevel.Class) { + return; + } + + if (!this.parentStates.has(parent)) { + // Count how many method-level children are being tested (only count triggered tests) + let childCount: number = 0; + parent.children.forEach((child: TestItem) => { + const childData: ITestItemData | undefined = dataCache.get(child); + if (childData?.testLevel === TestLevel.Method && triggeredTestsMapping.has(child.id)) { + childCount++; + } + }); + + this.parentStates.set(parent, { + started: false, + childrenTotal: childCount, + childrenCompleted: 0, + hasFailure: false, + }); + } + } + + /** + * Update parent test item when a child test starts. + * Marks the parent as "started" when the first child starts. + */ + protected updateParentOnChildStart(item: TestItem): void { + const parent: TestItem | undefined = item.parent; + if (!parent) { + return; + } + + const parentState: ParentItemState | undefined = this.parentStates.get(parent); + if (parentState && !parentState.started) { + parentState.started = true; + this.testContext.testRun.started(parent); + } + } + + /** + * Update parent test item when a child test completes. + * Marks the parent as "passed" or "failed" when all children complete. + */ + protected updateParentOnChildComplete(item: TestItem, childState: TestResultState): void { + const parent: TestItem | undefined = item.parent; + if (!parent) { + return; + } + + const parentState: ParentItemState | undefined = this.parentStates.get(parent); + if (!parentState) { + return; + } + + // Consider failed or errored tests as failures for the parent + if (childState === TestResultState.Failed || + childState === TestResultState.Errored) { + parentState.hasFailure = true; + } + + parentState.childrenCompleted++; + + // Check if all children have completed + if (parentState.childrenCompleted >= parentState.childrenTotal && parentState.childrenTotal > 0) { + if (parentState.hasFailure) { + this.testContext.testRun.failed(parent, []); + } else { + this.testContext.testRun.passed(parent); + } + } + } +} + +interface ParentItemState { + started: boolean; + childrenTotal: number; + childrenCompleted: number; + hasFailure: boolean; } diff --git a/src/runners/junitRunner/JUnitRunnerResultAnalyzer.ts b/src/runners/junitRunner/JUnitRunnerResultAnalyzer.ts index 2720d9a8..5a376a2a 100644 --- a/src/runners/junitRunner/JUnitRunnerResultAnalyzer.ts +++ b/src/runners/junitRunner/JUnitRunnerResultAnalyzer.ts @@ -65,9 +65,11 @@ export class JUnitRunnerResultAnalyzer extends RunnerResultAnalyzer { if (!item) { return; } + this.initializeParentState(item, this.triggeredTestsMapping); this.setCurrentState(item, TestResultState.Running, 0); this.setDurationAtStart(this.getCurrentState(item)); setTestState(this.testContext.testRun, item, this.getCurrentState(item).resultState); + this.updateParentOnChildStart(item); } else if (data.startsWith(MessageId.TestEnd)) { const item: TestItem | undefined = this.getTestItem(data.substr(MessageId.TestEnd.length)); if (!item) { @@ -77,6 +79,10 @@ export class JUnitRunnerResultAnalyzer extends RunnerResultAnalyzer { this.calcDurationAtEnd(currentState); this.determineResultStateAtEnd(data, currentState); setTestState(this.testContext.testRun, item, currentState.resultState, undefined, currentState.duration); + const itemData: ITestItemData | undefined = dataCache.get(item); + if (itemData?.testLevel === TestLevel.Method) { + this.updateParentOnChildComplete(item, currentState.resultState); + } } else if (data.startsWith(MessageId.TestFailed)) { const item: TestItem | undefined = this.getTestItem(data.substr(MessageId.TestFailed.length)); if (!item) { diff --git a/src/runners/testngRunner/TestNGRunnerResultAnalyzer.ts b/src/runners/testngRunner/TestNGRunnerResultAnalyzer.ts index a968ef29..39f46d75 100644 --- a/src/runners/testngRunner/TestNGRunnerResultAnalyzer.ts +++ b/src/runners/testngRunner/TestNGRunnerResultAnalyzer.ts @@ -2,7 +2,7 @@ // Licensed under the MIT license. import { Location, MarkdownString, TestItem, TestMessage } from 'vscode'; -import { dataCache } from '../../controller/testItemDataCache'; +import { dataCache, ITestItemData } from '../../controller/testItemDataCache'; import { RunnerResultAnalyzer } from '../baseRunner/RunnerResultAnalyzer'; import { setTestState } from '../utils'; import { IRunTestContext, TestLevel, TestResultState } from '../../java-test-runner.api'; @@ -64,8 +64,10 @@ export class TestNGRunnerResultAnalyzer extends RunnerResultAnalyzer { if (!item) { return; } + this.initializeParentState(item, this.triggeredTestsMapping); this.currentTestState = TestResultState.Running; this.testContext.testRun.started(item); + this.updateParentOnChildStart(item); } else if (outputData.name === TEST_FAIL) { const item: TestItem | undefined = this.getTestItem(id); if (!item) { @@ -103,6 +105,10 @@ export class TestNGRunnerResultAnalyzer extends RunnerResultAnalyzer { } const duration: number = Number.parseInt(outputData.attributes.duration, 10); setTestState(this.testContext.testRun, item, this.currentTestState, undefined, duration); + const itemData: ITestItemData | undefined = dataCache.get(item); + if (itemData?.testLevel === TestLevel.Method) { + this.updateParentOnChildComplete(item, this.currentTestState); + } } }