1+ import * as vscode from "vscode" ;
12import * as readline from "readline" ;
23import { Readable } from "stream" ;
34import {
67 WindowsNamedPipeReader ,
78} from "./TestEventStreamReader" ;
89import { ITestRunState } from "./TestRunState" ;
10+ import { TestClass } from "../TestDiscovery" ;
911
1012// All events produced by a swift-testing run will be one of these three types.
1113export type SwiftTestEvent = MetadataRecord | TestRecord | EventRecord ;
@@ -21,7 +23,7 @@ interface MetadataRecord extends VersionedRecord {
2123
2224interface TestRecord extends VersionedRecord {
2325 kind : "test" ;
24- payload : Test ;
26+ payload : TestSuite | TestFunction ;
2527}
2628
2729export type EventRecordPayload =
@@ -43,15 +45,23 @@ interface Metadata {
4345 [ key : string ] : object ; // Currently unstructured content
4446}
4547
46- interface Test {
47- kind : "suite" | "function" | "parameterizedFunction" ;
48+ interface TestBase {
4849 id : string ;
4950 name : string ;
5051 _testCases ?: TestCase [ ] ;
5152 sourceLocation : SourceLocation ;
5253}
5354
54- interface TestCase {
55+ interface TestSuite extends TestBase {
56+ kind : "suite" ;
57+ }
58+
59+ interface TestFunction extends TestBase {
60+ kind : "function" ;
61+ isParameterized : boolean ;
62+ }
63+
64+ export interface TestCase {
5565 id : string ;
5666 displayName : string ;
5767}
@@ -76,6 +86,11 @@ interface BaseEvent {
7686 testID : string ;
7787}
7888
89+ interface TestCaseEvent {
90+ sourceLocation : SourceLocation ;
91+ _testCase : TestCase ;
92+ }
93+
7994interface TestStarted extends BaseEvent {
8095 kind : "testStarted" ;
8196}
@@ -84,19 +99,19 @@ interface TestEnded extends BaseEvent {
8499 kind : "testEnded" ;
85100}
86101
87- interface TestCaseStarted extends BaseEvent {
102+ interface TestCaseStarted extends BaseEvent , TestCaseEvent {
88103 kind : "testCaseStarted" ;
89104}
90105
91- interface TestCaseEnded extends BaseEvent {
106+ interface TestCaseEnded extends BaseEvent , TestCaseEvent {
92107 kind : "testCaseEnded" ;
93108}
94109
95110interface TestSkipped extends BaseEvent {
96111 kind : "testSkipped" ;
97112}
98113
99- interface IssueRecorded extends BaseEvent {
114+ interface IssueRecorded extends BaseEvent , TestCaseEvent {
100115 kind : "issueRecorded" ;
101116 issue : {
102117 sourceLocation : SourceLocation ;
@@ -115,6 +130,12 @@ export interface SourceLocation {
115130
116131export class SwiftTestingOutputParser {
117132 private completionMap = new Map < number , boolean > ( ) ;
133+ private testCaseMap = new Map < string , Map < string , TestCase > > ( ) ;
134+
135+ constructor (
136+ public testRunStarted : ( ) => void ,
137+ public addParameterizedTestCase : ( testClass : TestClass , parentIndex : number ) => void
138+ ) { }
118139
119140 /**
120141 * Watches for test events on the named pipe at the supplied path.
@@ -155,31 +176,131 @@ export class SwiftTestingOutputParser {
155176 return ! matches ? id : matches [ 1 ] ;
156177 }
157178
179+ private testCaseId ( testId : string , testCaseId : string ) : string {
180+ const testCase = this . testCaseMap . get ( testId ) ?. get ( testCaseId ) ;
181+ return testCase ? this . createTestCaseId ( testCase ) : testId ;
182+ }
183+
184+ // Test cases do not have a unique ID if their arguments are not serializable
185+ // with Codable. If they aren't, their id appears as `argumentIDs: nil`, and we
186+ // fall back to using the testCase display name as the test case ID. This isn't
187+ // ideal because its possible to have multiple test cases with the same display name,
188+ // but until we have a better solution for identifying test cases it will have to do.
189+ // SEE: rdar://119522099.
190+ private createTestCaseId ( testCase : TestCase ) : string {
191+ return testCase . id === "argumentIDs: nil" ? testCase . displayName : testCase . id ;
192+ }
193+
194+ private parameterizedFunctionTestCaseToTestClass (
195+ testCase : TestCase ,
196+ location : vscode . Location ,
197+ index : number
198+ ) : TestClass {
199+ return {
200+ id : this . createTestCaseId ( testCase ) ,
201+ label : testCase . displayName ,
202+ tags : [ ] ,
203+ children : [ ] ,
204+ style : "swift-testing" ,
205+ location : location ,
206+ disabled : true ,
207+ sortText : `${ index } ` . padStart ( 8 , "0" ) ,
208+ } ;
209+ }
210+
211+ private buildTestCaseMapForParameterizedTest ( record : TestRecord ) {
212+ const map = new Map < string , TestCase > ( ) ;
213+ ( record . payload . _testCases ?? [ ] ) . forEach ( testCase => {
214+ map . set ( this . createTestCaseId ( testCase ) , testCase ) ;
215+ } ) ;
216+ this . testCaseMap . set ( record . payload . id , map ) ;
217+ }
218+
158219 private parse ( item : SwiftTestEvent , runState : ITestRunState ) {
159- if ( item . kind === "event" ) {
160- if ( item . payload . kind === "testCaseStarted" || item . payload . kind === "testStarted" ) {
220+ if (
221+ item . kind === "test" &&
222+ item . payload . kind === "function" &&
223+ item . payload . isParameterized &&
224+ item . payload . _testCases
225+ ) {
226+ // Store a map of [Test ID, [Test Case ID, TestCase]] so we can quickly
227+ // map an event.payload.testID back to a test case.
228+ this . buildTestCaseMapForParameterizedTest ( item ) ;
229+
230+ const testName = this . testName ( item . payload . id ) ;
231+ const testIndex = runState . getTestItemIndex ( testName , undefined ) ;
232+ // If a test has test cases it is paramterized and we need to notify
233+ // the caller that the TestClass should be added to the vscode.TestRun
234+ // before it starts.
235+ item . payload . _testCases
236+ . map ( ( testCase , index ) =>
237+ this . parameterizedFunctionTestCaseToTestClass (
238+ testCase ,
239+ sourceLocationToVSCodeLocation ( item . payload . sourceLocation ) ,
240+ index
241+ )
242+ )
243+ . flatMap ( testClass => ( testClass ? [ testClass ] : [ ] ) )
244+ . forEach ( testClass => this . addParameterizedTestCase ( testClass , testIndex ) ) ;
245+ } else if ( item . kind === "event" ) {
246+ if ( item . payload . kind === "runStarted" ) {
247+ // Notify the runner that we've recieved all the test cases and
248+ // are going to start running tests now.
249+ this . testRunStarted ( ) ;
250+ } else if ( item . payload . kind === "testStarted" ) {
161251 const testName = this . testName ( item . payload . testID ) ;
162252 const testIndex = runState . getTestItemIndex ( testName , undefined ) ;
163253 runState . started ( testIndex , item . payload . instant . absolute ) ;
254+ } else if ( item . payload . kind === "testCaseStarted" ) {
255+ const testID = this . testCaseId (
256+ item . payload . testID ,
257+ this . createTestCaseId ( item . payload . _testCase )
258+ ) ;
259+ const testName = this . testName ( testID ) ;
260+ const testIndex = runState . getTestItemIndex ( testName , undefined ) ;
261+ runState . started ( testIndex , item . payload . instant . absolute ) ;
164262 } else if ( item . payload . kind === "testSkipped" ) {
165263 const testName = this . testName ( item . payload . testID ) ;
166264 const testIndex = runState . getTestItemIndex ( testName , undefined ) ;
167265 runState . skipped ( testIndex ) ;
168266 } else if ( item . payload . kind === "issueRecorded" ) {
169- const testName = this . testName ( item . payload . testID ) ;
267+ const testID = this . testCaseId (
268+ item . payload . testID ,
269+ this . createTestCaseId ( item . payload . _testCase )
270+ ) ;
271+ const testName = this . testName ( testID ) ;
170272 const testIndex = runState . getTestItemIndex ( testName , undefined ) ;
171- const sourceLocation = item . payload . issue . sourceLocation ;
273+ const location = sourceLocationToVSCodeLocation ( item . payload . issue . sourceLocation ) ;
172274 item . payload . messages . forEach ( message => {
173- runState . recordIssue ( testIndex , message . text , {
174- file : sourceLocation . _filePath ,
175- line : sourceLocation . line ,
176- column : sourceLocation . column ,
177- } ) ;
275+ runState . recordIssue ( testIndex , message . text , location ) ;
178276 } ) ;
179- } else if ( item . payload . kind === "testCaseEnded" || item . payload . kind === "testEnded" ) {
277+
278+ if ( testID !== item . payload . testID ) {
279+ const testName = this . testName ( item . payload . testID ) ;
280+ const testIndex = runState . getTestItemIndex ( testName , undefined ) ;
281+ item . payload . messages . forEach ( message => {
282+ runState . recordIssue ( testIndex , message . text , location ) ;
283+ } ) ;
284+ }
285+ } else if ( item . payload . kind === "testEnded" ) {
180286 const testName = this . testName ( item . payload . testID ) ;
181287 const testIndex = runState . getTestItemIndex ( testName , undefined ) ;
182288
289+ // When running a single test the testEnded and testCaseEnded events
290+ // have the same ID, and so we'd end the same test twice.
291+ if ( this . completionMap . get ( testIndex ) ) {
292+ return ;
293+ }
294+ this . completionMap . set ( testIndex , true ) ;
295+ runState . completed ( testIndex , { timestamp : item . payload . instant . absolute } ) ;
296+ } else if ( item . payload . kind === "testCaseEnded" ) {
297+ const testID = this . testCaseId (
298+ item . payload . testID ,
299+ this . createTestCaseId ( item . payload . _testCase )
300+ ) ;
301+ const testName = this . testName ( testID ) ;
302+ const testIndex = runState . getTestItemIndex ( testName , undefined ) ;
303+
183304 // When running a single test the testEnded and testCaseEnded events
184305 // have the same ID, and so we'd end the same test twice.
185306 if ( this . completionMap . get ( testIndex ) ) {
@@ -191,3 +312,10 @@ export class SwiftTestingOutputParser {
191312 }
192313 }
193314}
315+
316+ function sourceLocationToVSCodeLocation ( sourceLocation : SourceLocation ) : vscode . Location {
317+ return new vscode . Location (
318+ vscode . Uri . file ( sourceLocation . _filePath ) ,
319+ new vscode . Position ( sourceLocation . line - 1 , sourceLocation ?. column ?? 0 )
320+ ) ;
321+ }
0 commit comments