22// Licensed under the MIT License.
33
44import { injectable , inject , named } from 'inversify' ;
5- import { Location , TestItem , TestMessage , TestRun , TestRunProfileKind } from 'vscode' ;
5+ import { Location , TestController , TestItem , TestMessage , TestRun , TestRunProfileKind } from 'vscode' ;
66import { traceError , traceInfo } from '../../../common/logger' ;
77import * as internalScripts from '../../../common/process/internal/scripts' ;
88import { IOutputChannel } from '../../../common/types' ;
99import { noop } from '../../../common/utils/misc' ;
1010import { UNITTEST_PROVIDER } from '../../common/constants' ;
1111import { ITestRunner , ITestDebugLauncher , IUnitTestSocketServer , LaunchOptions , Options } from '../../common/types' ;
1212import { TEST_OUTPUT_CHANNEL } from '../../constants' ;
13- import { getTestCaseNodes } from '../common/testItemUtilities' ;
13+ import { clearAllChildren , getTestCaseNodes } from '../common/testItemUtilities' ;
1414import { ITestRun , ITestsRunner , TestData , TestRunInstanceOptions , TestRunOptions } from '../common/types' ;
1515import { fixLogLines } from '../common/utils' ;
1616import { getTestRunArgs } from './arguments' ;
@@ -20,6 +20,7 @@ interface ITestData {
2020 message : string ;
2121 outcome : string ;
2222 traceback : string ;
23+ subtest ?: string ;
2324}
2425
2526@injectable ( )
@@ -35,6 +36,7 @@ export class UnittestRunner implements ITestsRunner {
3536 testRun : ITestRun ,
3637 options : TestRunOptions ,
3738 idToRawData : Map < string , TestData > ,
39+ testController ?: TestController ,
3840 ) : Promise < void > {
3941 const runOptions : TestRunInstanceOptions = {
4042 ...options ,
@@ -43,7 +45,7 @@ export class UnittestRunner implements ITestsRunner {
4345 } ;
4446
4547 try {
46- await this . runTest ( testRun . includes , testRun . runInstance , runOptions , idToRawData ) ;
48+ await this . runTest ( testRun . includes , testRun . runInstance , runOptions , idToRawData , testController ) ;
4749 } catch ( ex ) {
4850 testRun . runInstance . appendOutput ( `Error while running tests:\r\n${ ex } \r\n\r\n` ) ;
4951 }
@@ -54,6 +56,7 @@ export class UnittestRunner implements ITestsRunner {
5456 runInstance : TestRun ,
5557 options : TestRunInstanceOptions ,
5658 idToRawData : Map < string , TestData > ,
59+ testController ?: TestController ,
5760 ) : Promise < void > {
5861 runInstance . appendOutput ( `Running tests (unittest): ${ testNodes . map ( ( t ) => t . id ) . join ( ' ; ' ) } \r\n` ) ;
5962 const testCaseNodes : TestItem [ ] = [ ] ;
@@ -77,12 +80,13 @@ export class UnittestRunner implements ITestsRunner {
7780 const tested : string [ ] = [ ] ;
7881
7982 const counts = {
80- total : testCaseNodes . length ,
83+ total : 0 ,
8184 passed : 0 ,
8285 skipped : 0 ,
8386 errored : 0 ,
8487 failed : 0 ,
8588 } ;
89+ const subTestStats : Map < string , { passed : number ; failed : number } > = new Map ( ) ;
8690
8791 let failFast = false ;
8892 let stopTesting = false ;
@@ -98,6 +102,7 @@ export class UnittestRunner implements ITestsRunner {
98102 const testCase = testCaseNodes . find ( ( node ) => idToRawData . get ( node . id ) ?. runId === data . test ) ;
99103 const rawTestCase = idToRawData . get ( testCase ?. id ?? '' ) ;
100104 if ( testCase && rawTestCase ) {
105+ counts . total += 1 ;
101106 tested . push ( rawTestCase . runId ) ;
102107
103108 if ( data . outcome === 'passed' || data . outcome === 'failed-expected' ) {
@@ -147,6 +152,70 @@ export class UnittestRunner implements ITestsRunner {
147152 runInstance . skipped ( testCase ) ;
148153 runInstance . appendOutput ( fixLogLines ( text ) ) ;
149154 counts . skipped += 1 ;
155+ } else if ( data . outcome === 'subtest-passed' ) {
156+ const sub = subTestStats . get ( data . test ) ;
157+ if ( sub ) {
158+ sub . passed += 1 ;
159+ } else {
160+ counts . passed += 1 ;
161+ subTestStats . set ( data . test , { passed : 1 , failed : 0 } ) ;
162+ runInstance . appendOutput ( fixLogLines ( `${ rawTestCase . rawId } [subtests]:\r\n` ) ) ;
163+
164+ // We are seeing the first subtest for this node. Clear all other nodes under it
165+ // because we have no way to detect these at discovery, they can always be different
166+ // for each run.
167+ clearAllChildren ( testCase ) ;
168+ }
169+ if ( data . subtest ) {
170+ runInstance . appendOutput ( fixLogLines ( `${ data . subtest } Passed\r\n` ) ) ;
171+
172+ // This is a runtime only node for unittest subtest, since they can only be detected
173+ // at runtime. So, create a fresh one for each result.
174+ const subtest = testController ?. createTestItem ( data . subtest , data . subtest ) ;
175+ if ( subtest ) {
176+ testCase . children . add ( subtest ) ;
177+ runInstance . started ( subtest ) ;
178+ runInstance . passed ( subtest ) ;
179+ }
180+ }
181+ } else if ( data . outcome === 'subtest-failed' ) {
182+ const sub = subTestStats . get ( data . test ) ;
183+ if ( sub ) {
184+ sub . failed += 1 ;
185+ } else {
186+ counts . failed += 1 ;
187+ subTestStats . set ( data . test , { passed : 0 , failed : 1 } ) ;
188+
189+ runInstance . appendOutput ( fixLogLines ( `${ rawTestCase . rawId } [subtests]:\r\n` ) ) ;
190+
191+ // We are seeing the first subtest for this node. Clear all other nodes under it
192+ // because we have no way to detect these at discovery, they can always be different
193+ // for each run.
194+ clearAllChildren ( testCase ) ;
195+ }
196+
197+ if ( data . subtest ) {
198+ runInstance . appendOutput ( fixLogLines ( `${ data . subtest } Failed\r\n` ) ) ;
199+ const traceback = data . traceback
200+ ? data . traceback . splitLines ( { trim : false , removeEmptyEntries : true } ) . join ( '\r\n' )
201+ : '' ;
202+ const text = `${ data . subtest } Failed: ${ data . message ?? data . outcome } \r\n${ traceback } \r\n` ;
203+ runInstance . appendOutput ( fixLogLines ( text ) ) ;
204+
205+ // This is a runtime only node for unittest subtest, since they can only be detected
206+ // at runtime. So, create a fresh one for each result.
207+ const subtest = testController ?. createTestItem ( data . subtest , data . subtest ) ;
208+ if ( subtest ) {
209+ testCase . children . add ( subtest ) ;
210+ runInstance . started ( subtest ) ;
211+ const message = new TestMessage ( text ) ;
212+ if ( testCase . uri && testCase . range ) {
213+ message . location = new Location ( testCase . uri , testCase . range ) ;
214+ }
215+
216+ runInstance . failed ( subtest , message ) ;
217+ }
218+ }
150219 } else {
151220 const text = `Unknown outcome type for test ${ rawTestCase . rawId } : ${ data . outcome } ` ;
152221 runInstance . appendOutput ( fixLogLines ( text ) ) ;
@@ -233,7 +302,17 @@ export class UnittestRunner implements ITestsRunner {
233302 runInstance . appendOutput ( `Total number of tests passed: ${ counts . passed } \r\n` ) ;
234303 runInstance . appendOutput ( `Total number of tests failed: ${ counts . failed } \r\n` ) ;
235304 runInstance . appendOutput ( `Total number of tests failed with errors: ${ counts . errored } \r\n` ) ;
236- runInstance . appendOutput ( `Total number of tests skipped: ${ counts . skipped } \r\n` ) ;
305+ runInstance . appendOutput ( `Total number of tests skipped: ${ counts . skipped } \r\n\r\n` ) ;
306+
307+ if ( subTestStats . size > 0 ) {
308+ runInstance . appendOutput ( 'Sub-test stats: \r\n' ) ;
309+ }
310+
311+ subTestStats . forEach ( ( v , k ) => {
312+ runInstance . appendOutput (
313+ `Sub-tests for [${ k } ]: Total=${ v . passed + v . failed } Passed=${ v . passed } Failed=${ v . failed } \r\n\r\n` ,
314+ ) ;
315+ } ) ;
237316
238317 if ( failFast ) {
239318 runInstance . appendOutput (
0 commit comments