33
44import * as _ from 'lodash' ;
55import * as path from 'path' ;
6- import { CancellationToken , DebugConfiguration , Disposable , FileCoverage , FileCoverageDetail , FileSystemWatcher , RelativePattern , TestController , TestItem , TestRun , TestRunProfileKind , TestRunRequest , tests , TestTag , Uri , window , workspace , WorkspaceFolder } from 'vscode' ;
6+ import { CancellationToken , DebugConfiguration , Disposable , FileCoverage , FileCoverageDetail , FileSystemWatcher , Location , MarkdownString , RelativePattern , TestController , TestItem , TestMessage , TestRun , TestRunProfileKind , TestRunRequest , tests , TestTag , Uri , window , workspace , WorkspaceFolder } from 'vscode' ;
77import { instrumentOperation , sendError , sendInfo } from 'vscode-extension-telemetry-wrapper' ;
88import { refreshExplorer } from '../commands/testExplorerCommands' ;
99import { IProgressReporter } from '../debugger.api' ;
1010import { progressProvider } from '../extension' ;
1111import { testSourceProvider } from '../provider/testSourceProvider' ;
12- import { IExecutionConfig } from '../runConfigs' ;
1312import { BaseRunner } from '../runners/baseRunner/BaseRunner' ;
1413import { JUnitRunner } from '../runners/junitRunner/JunitRunner' ;
1514import { TestNGRunner } from '../runners/testngRunner/TestNGRunner' ;
16- import { IJavaTestItem , IRunTestContext , TestKind , TestLevel } from '../types' ;
15+ import { IJavaTestItem } from '../types' ;
1716import { loadRunConfig } from '../utils/configUtils' ;
1817import { resolveLaunchConfigurationForRunner } from '../utils/launchUtils' ;
1918import { dataCache , ITestItemData } from './testItemDataCache' ;
20- import { findDirectTestChildrenForClass , findTestPackagesAndTypes , findTestTypesAndMethods , loadJavaProjects , resolvePath , synchronizeItemsRecursively , updateItemForDocumentWithDebounce } from './utils' ;
19+ import { createTestItem , findDirectTestChildrenForClass , findTestPackagesAndTypes , findTestTypesAndMethods , loadJavaProjects , resolvePath , synchronizeItemsRecursively , updateItemForDocumentWithDebounce } from './utils' ;
2120import { JavaTestCoverageProvider } from '../provider/JavaTestCoverageProvider' ;
21+ import { testRunnerService } from './testRunnerService' ;
22+ import { IRunTestContext , TestRunner , TestFinishEvent , TestItemStatusChangeEvent , TestKind , TestLevel , TestResultState , TestIdParts } from '../java-test-runner.api' ;
23+ import { processStackTraceLine } from '../runners/utils' ;
24+ import { parsePartsFromTestId } from '../utils/testItemUtils' ;
2225
2326export let testController : TestController | undefined ;
2427export const watchers : Disposable [ ] = [ ] ;
@@ -43,6 +46,10 @@ export function createTestController(): void {
4346 startWatchingWorkspace ( ) ;
4447}
4548
49+ export function creatTestProfile ( name : string , kind : TestRunProfileKind ) : void {
50+ testController ?. createRunProfile ( name , kind , runHandler , false , runnableTag ) ;
51+ }
52+
4653export const loadChildren : ( item : TestItem , token ?: CancellationToken ) => any = instrumentOperation ( 'java.test.explorer.loadChildren' , async ( _operationId : string , item : TestItem , token ?: CancellationToken ) => {
4754 if ( ! item ) {
4855 await loadJavaProjects ( ) ;
@@ -178,21 +185,48 @@ export const runTests: (request: TestRunRequest, option: IRunOption) => any = in
178185 try {
179186 await new Promise < void > ( async ( resolve : ( ) => void ) : Promise < void > => {
180187 const token : CancellationToken = option . token ?? run . token ;
188+ let disposables : Disposable [ ] = [ ] ;
181189 token . onCancellationRequested ( ( ) => {
182190 option . progressReporter ?. done ( ) ;
183191 run . end ( ) ;
192+ disposables . forEach ( ( d : Disposable ) => d . dispose ( ) ) ;
184193 return resolve ( ) ;
185194 } ) ;
186195 enqueueTestMethods ( testItems , run ) ;
196+ // TODO: first group by project, then merge test methods.
187197 const queue : TestItem [ ] [ ] = mergeTestMethods ( testItems ) ;
188198 for ( const testsInQueue of queue ) {
189199 if ( testsInQueue . length === 0 ) {
190200 continue ;
191201 }
192202 const testProjectMapping : Map < string , TestItem [ ] > = mapTestItemsByProject ( testsInQueue ) ;
193203 for ( const [ projectName , itemsPerProject ] of testProjectMapping . entries ( ) ) {
204+ const workspaceFolder : WorkspaceFolder | undefined = workspace . getWorkspaceFolder ( itemsPerProject [ 0 ] . uri ! ) ;
205+ if ( ! workspaceFolder ) {
206+ window . showErrorMessage ( `Failed to get workspace folder from test item: ${ itemsPerProject [ 0 ] . label } .` ) ;
207+ continue ;
208+ }
209+ const testContext : IRunTestContext = {
210+ isDebug : option . isDebug ,
211+ kind : TestKind . None ,
212+ projectName,
213+ testItems : itemsPerProject ,
214+ testRun : run ,
215+ workspaceFolder,
216+ profile : request . profile ,
217+ testConfig : await loadRunConfig ( itemsPerProject , workspaceFolder ) ,
218+ } ;
219+ const testRunner : TestRunner | undefined = testRunnerService . getRunner ( request . profile ?. label , request . profile ?. kind ) ;
220+ if ( testRunner ) {
221+ await executeWithTestRunner ( option , testRunner , testContext , run , disposables ) ;
222+ disposables . forEach ( ( d : Disposable ) => d . dispose ( ) ) ;
223+ disposables = [ ] ;
224+ continue ;
225+ }
194226 const testKindMapping : Map < TestKind , TestItem [ ] > = mapTestItemsByKind ( itemsPerProject ) ;
195227 for ( const [ kind , items ] of testKindMapping . entries ( ) ) {
228+ testContext . kind = kind ;
229+ testContext . testItems = items ;
196230 if ( option . progressReporter ?. isCancelled ( ) ) {
197231 option . progressReporter = progressProvider ?. createProgressReporter ( option . isDebug ? 'Debug Tests' : 'Run Tests' ) ;
198232 }
@@ -208,33 +242,17 @@ export const runTests: (request: TestRunRequest, option: IRunOption) => any = in
208242 return resolve ( ) ;
209243 } ) ;
210244 option . progressReporter ?. report ( 'Resolving launch configuration...' ) ;
211- // TODO: improve the config experience
212- const workspaceFolder : WorkspaceFolder | undefined = workspace . getWorkspaceFolder ( items [ 0 ] . uri ! ) ;
213- if ( ! workspaceFolder ) {
214- window . showErrorMessage ( `Failed to get workspace folder from test item: ${ items [ 0 ] . label } .` ) ;
215- continue ;
216- }
217- const config : IExecutionConfig | undefined = await loadRunConfig ( items , workspaceFolder ) ;
218- if ( ! config ) {
245+ if ( ! testContext . testConfig ) {
219246 continue ;
220247 }
221- const testContext : IRunTestContext = {
222- isDebug : option . isDebug ,
223- kind,
224- projectName,
225- testItems : items ,
226- testRun : run ,
227- workspaceFolder,
228- profile : request . profile ,
229- } ;
230248 const runner : BaseRunner | undefined = getRunnerByContext ( testContext ) ;
231249 if ( ! runner ) {
232250 window . showErrorMessage ( `Failed to get suitable runner for the test kind: ${ testContext . kind } .` ) ;
233251 continue ;
234252 }
235253 try {
236254 await runner . setup ( ) ;
237- const resolvedConfiguration : DebugConfiguration = mergeConfigurations ( option . launchConfiguration , config ) ?? await resolveLaunchConfigurationForRunner ( runner , testContext , config ) ;
255+ const resolvedConfiguration : DebugConfiguration = mergeConfigurations ( option . launchConfiguration , testContext . testConfig ) ?? await resolveLaunchConfigurationForRunner ( runner , testContext , testContext . testConfig ) ;
238256 resolvedConfiguration . __progressId = option . progressReporter ?. getId ( ) ;
239257 delegatedToDebugger = true ;
240258 trackTestFrameworkVersion ( testContext . kind , resolvedConfiguration . classPaths , resolvedConfiguration . modulePaths ) ;
@@ -258,6 +276,133 @@ export const runTests: (request: TestRunRequest, option: IRunOption) => any = in
258276 }
259277} ) ;
260278
279+ async function executeWithTestRunner ( option : IRunOption , testRunner : TestRunner , testContext : IRunTestContext , run : TestRun , disposables : Disposable [ ] ) {
280+ option . progressReporter ?. done ( ) ;
281+ await new Promise < void > ( async ( resolve : ( ) => void ) : Promise < void > => {
282+ disposables . push ( testRunner . onDidChangeTestItemStatus ( ( event : TestItemStatusChangeEvent ) => {
283+ const parts : TestIdParts = parsePartsFromTestId ( event . testId ) ;
284+ let parentItem : TestItem ;
285+ try {
286+ parentItem = findTestClass ( parts ) ;
287+ } catch ( e ) {
288+ sendError ( e ) ;
289+ window . showErrorMessage ( e . message ) ;
290+ return resolve ( ) ;
291+ }
292+ let currentItem : TestItem | undefined ;
293+ const invocations : string [ ] | undefined = parts . invocations ;
294+ if ( invocations ?. length ) {
295+ let i : number = 0 ;
296+ for ( ; i < invocations . length ; i ++ ) {
297+ currentItem = parentItem . children . get ( `${ parentItem . id } #${ invocations [ i ] } ` ) ;
298+ if ( ! currentItem ) {
299+ break ;
300+ }
301+ parentItem = currentItem ;
302+ }
303+
304+ if ( i < invocations . length - 1 ) {
305+ window . showErrorMessage ( 'Test not found:' + event . testId ) ;
306+ sendError ( new Error ( 'Test not found:' + event . testId ) ) ;
307+ return resolve ( ) ;
308+ }
309+
310+ if ( ! currentItem ) {
311+ currentItem = createTestItem ( {
312+ children : [ ] ,
313+ uri : parentItem . uri ?. toString ( ) ,
314+ range : parentItem . range ,
315+ jdtHandler : '' ,
316+ fullName : `${ parentItem . id } #${ invocations [ invocations . length - 1 ] } ` ,
317+ label : event . displayName || invocations [ invocations . length - 1 ] ,
318+ id : `${ parentItem . id } #${ invocations [ invocations . length - 1 ] } ` ,
319+ projectName : testContext . projectName ,
320+ testKind : TestKind . None ,
321+ testLevel : TestLevel . Invocation ,
322+ } , parentItem ) ;
323+ }
324+ } else {
325+ currentItem = parentItem ;
326+ }
327+
328+ if ( event . displayName && getLabelWithoutCodicon ( currentItem . label ) !== event . displayName ) {
329+ currentItem . description = event . displayName ;
330+ }
331+ switch ( event . state ) {
332+ case TestResultState . Running :
333+ run . started ( currentItem ) ;
334+ break ;
335+ case TestResultState . Passed :
336+ run . passed ( currentItem ) ;
337+ break ;
338+ case TestResultState . Failed :
339+ case TestResultState . Errored :
340+ const testMessages : TestMessage [ ] = [ ] ;
341+ if ( event . message ) {
342+ const markdownTrace : MarkdownString = new MarkdownString ( ) ;
343+ markdownTrace . supportHtml = true ;
344+ markdownTrace . isTrusted = true ;
345+ const testMessage : TestMessage = new TestMessage ( markdownTrace ) ;
346+ testMessages . push ( testMessage ) ;
347+ const lines : string [ ] = event . message . split ( / \r ? \n / ) ;
348+ for ( const line of lines ) {
349+ const location : Location | undefined = processStackTraceLine ( line , markdownTrace , currentItem , testContext . projectName ) ;
350+ if ( location ) {
351+ testMessage . location = location ;
352+ }
353+ }
354+ }
355+ run . failed ( currentItem , testMessages ) ;
356+ break ;
357+ case TestResultState . Skipped :
358+ run . skipped ( currentItem ) ;
359+ break ;
360+ default :
361+ break ;
362+ }
363+ } ) ) ;
364+
365+ disposables . push ( testRunner . onDidFinishTestRun ( ( _event : TestFinishEvent ) => {
366+ return resolve ( ) ;
367+ } ) ) ;
368+
369+ testRunner . launch ( testContext ) ;
370+ } ) ;
371+
372+ function findTestClass ( parts : TestIdParts ) : TestItem {
373+ const projectItem : TestItem | undefined = testController ?. items . get ( parts . project ) ;
374+ if ( ! projectItem ) {
375+ throw new Error ( 'Failed to get the project test item.' ) ;
376+ }
377+
378+ if ( parts . package === undefined ) { // '' means default package
379+ throw new Error ( 'package is undefined in the id parts.' ) ;
380+ }
381+
382+ const packageItem : TestItem | undefined = projectItem . children . get ( `${ projectItem . id } @${ parts . package } ` ) ;
383+ if ( ! packageItem ) {
384+ throw new Error ( 'Failed to get the package test item.' ) ;
385+ }
386+
387+ if ( ! parts . class ) {
388+ throw new Error ( 'class is undefined in the id parts.' ) ;
389+ }
390+
391+ const classes : string [ ] = parts . class . split ( '$' ) ; // handle nested classes
392+ let current : TestItem | undefined = packageItem . children . get ( `${ projectItem . id } @${ classes [ 0 ] } ` ) ;
393+ if ( ! current ) {
394+ throw new Error ( 'Failed to get the class test item.' ) ;
395+ }
396+ for ( let i : number = 1 ; i < classes . length ; i ++ ) {
397+ current = current . children . get ( `${ current . id } $${ classes [ i ] } ` ) ;
398+ if ( ! current ) {
399+ throw new Error ( 'Failed to get the class test item.' ) ;
400+ }
401+ }
402+ return current ;
403+ }
404+ }
405+
261406function mergeConfigurations ( launchConfiguration : DebugConfiguration | undefined , config : any ) : DebugConfiguration | undefined {
262407 if ( ! launchConfiguration ) {
263408 return undefined ;
@@ -586,6 +731,18 @@ function trackTestFrameworkVersion(testKind: TestKind, classpaths: string[], mod
586731 } ) ;
587732}
588733
734+ function getLabelWithoutCodicon ( name : string ) : string {
735+ if ( name . includes ( '#' ) ) {
736+ name = name . substring ( name . indexOf ( '#' ) + 1 ) ;
737+ }
738+
739+ const result : RegExpMatchArray | null = name . match ( / (?: \$ \( .+ \) ) ? ( .* ) / ) ;
740+ if ( result ?. length === 2 ) {
741+ return result [ 1 ] ;
742+ }
743+ return name ;
744+ }
745+
589746interface IRunOption {
590747 isDebug : boolean ;
591748 progressReporter ?: IProgressReporter ;
0 commit comments