1
+ import * as vscode from "vscode" ;
1
2
import * as readline from "readline" ;
2
3
import { Readable } from "stream" ;
3
4
import {
@@ -6,8 +7,12 @@ import {
6
7
WindowsNamedPipeReader ,
7
8
} from "./TestEventStreamReader" ;
8
9
import { ITestRunState } from "./TestRunState" ;
10
+ import { TestClass } from "../TestDiscovery" ;
11
+ import { sourceLocationToVSCodeLocation } from "../../utilities/utilities" ;
9
12
10
13
// All events produced by a swift-testing run will be one of these three types.
14
+ // Detailed information about swift-testing's JSON schema is available here:
15
+ // https://github.com/apple/swift-testing/blob/main/Documentation/ABI/JSON.md
11
16
export type SwiftTestEvent = MetadataRecord | TestRecord | EventRecord ;
12
17
13
18
interface VersionedRecord {
@@ -21,7 +26,7 @@ interface MetadataRecord extends VersionedRecord {
21
26
22
27
interface TestRecord extends VersionedRecord {
23
28
kind : "test" ;
24
- payload : Test ;
29
+ payload : TestSuite | TestFunction ;
25
30
}
26
31
27
32
export type EventRecordPayload =
@@ -43,15 +48,23 @@ interface Metadata {
43
48
[ key : string ] : object ; // Currently unstructured content
44
49
}
45
50
46
- interface Test {
47
- kind : "suite" | "function" | "parameterizedFunction" ;
51
+ interface TestBase {
48
52
id : string ;
49
53
name : string ;
50
54
_testCases ?: TestCase [ ] ;
51
55
sourceLocation : SourceLocation ;
52
56
}
53
57
54
- interface TestCase {
58
+ interface TestSuite extends TestBase {
59
+ kind : "suite" ;
60
+ }
61
+
62
+ interface TestFunction extends TestBase {
63
+ kind : "function" ;
64
+ isParameterized : boolean ;
65
+ }
66
+
67
+ export interface TestCase {
55
68
id : string ;
56
69
displayName : string ;
57
70
}
@@ -76,6 +89,11 @@ interface BaseEvent {
76
89
testID : string ;
77
90
}
78
91
92
+ interface TestCaseEvent {
93
+ sourceLocation : SourceLocation ;
94
+ _testCase : TestCase ;
95
+ }
96
+
79
97
interface TestStarted extends BaseEvent {
80
98
kind : "testStarted" ;
81
99
}
@@ -84,19 +102,19 @@ interface TestEnded extends BaseEvent {
84
102
kind : "testEnded" ;
85
103
}
86
104
87
- interface TestCaseStarted extends BaseEvent {
105
+ interface TestCaseStarted extends BaseEvent , TestCaseEvent {
88
106
kind : "testCaseStarted" ;
89
107
}
90
108
91
- interface TestCaseEnded extends BaseEvent {
109
+ interface TestCaseEnded extends BaseEvent , TestCaseEvent {
92
110
kind : "testCaseEnded" ;
93
111
}
94
112
95
113
interface TestSkipped extends BaseEvent {
96
114
kind : "testSkipped" ;
97
115
}
98
116
99
- interface IssueRecorded extends BaseEvent {
117
+ interface IssueRecorded extends BaseEvent , TestCaseEvent {
100
118
kind : "issueRecorded" ;
101
119
issue : {
102
120
sourceLocation : SourceLocation ;
@@ -115,6 +133,12 @@ export interface SourceLocation {
115
133
116
134
export class SwiftTestingOutputParser {
117
135
private completionMap = new Map < number , boolean > ( ) ;
136
+ private testCaseMap = new Map < string , Map < string , TestCase > > ( ) ;
137
+
138
+ constructor (
139
+ public testRunStarted : ( ) => void ,
140
+ public addParameterizedTestCase : ( testClass : TestClass , parentIndex : number ) => void
141
+ ) { }
118
142
119
143
/**
120
144
* Watches for test events on the named pipe at the supplied path.
@@ -155,31 +179,147 @@ export class SwiftTestingOutputParser {
155
179
return ! matches ? id : matches [ 1 ] ;
156
180
}
157
181
182
+ private testCaseId ( testId : string , testCaseId : string ) : string {
183
+ const testCase = this . testCaseMap . get ( testId ) ?. get ( testCaseId ) ;
184
+ return testCase ? `${ testId } /${ this . idFromTestCase ( testCase ) } ` : testId ;
185
+ }
186
+
187
+ // Test cases do not have a unique ID if their arguments are not serializable
188
+ // with Codable. If they aren't, their id appears as `argumentIDs: nil`, and we
189
+ // fall back to using the testCase display name as the test case ID. This isn't
190
+ // ideal because its possible to have multiple test cases with the same display name,
191
+ // but until we have a better solution for identifying test cases it will have to do.
192
+ // SEE: rdar://119522099.
193
+ private idFromTestCase ( testCase : TestCase ) : string {
194
+ return testCase . id === "argumentIDs: nil" ? testCase . displayName : testCase . id ;
195
+ }
196
+
197
+ private parameterizedFunctionTestCaseToTestClass (
198
+ testId : string ,
199
+ testCase : TestCase ,
200
+ location : vscode . Location ,
201
+ index : number
202
+ ) : TestClass {
203
+ return {
204
+ id : this . testCaseId ( testId , this . idFromTestCase ( testCase ) ) ,
205
+ label : testCase . displayName ,
206
+ tags : [ ] ,
207
+ children : [ ] ,
208
+ style : "swift-testing" ,
209
+ location : location ,
210
+ disabled : true ,
211
+ sortText : `${ index } ` . padStart ( 8 , "0" ) ,
212
+ } ;
213
+ }
214
+
215
+ private buildTestCaseMapForParameterizedTest ( record : TestRecord ) {
216
+ const map = new Map < string , TestCase > ( ) ;
217
+ ( record . payload . _testCases ?? [ ] ) . forEach ( testCase => {
218
+ map . set ( this . idFromTestCase ( testCase ) , testCase ) ;
219
+ } ) ;
220
+ this . testCaseMap . set ( record . payload . id , map ) ;
221
+ }
222
+
223
+ private getTestCaseIndex ( runState : ITestRunState , testID : string ) : number {
224
+ const fullNameIndex = runState . getTestItemIndex ( testID , undefined ) ;
225
+ if ( fullNameIndex === - 1 ) {
226
+ return runState . getTestItemIndex ( this . testName ( testID ) , undefined ) ;
227
+ }
228
+ return fullNameIndex ;
229
+ }
230
+
158
231
private parse ( item : SwiftTestEvent , runState : ITestRunState ) {
159
- if ( item . kind === "event" ) {
160
- if ( item . payload . kind === "testCaseStarted" || item . payload . kind === "testStarted" ) {
232
+ if (
233
+ item . kind === "test" &&
234
+ item . payload . kind === "function" &&
235
+ item . payload . isParameterized &&
236
+ item . payload . _testCases
237
+ ) {
238
+ // Store a map of [Test ID, [Test Case ID, TestCase]] so we can quickly
239
+ // map an event.payload.testID back to a test case.
240
+ this . buildTestCaseMapForParameterizedTest ( item ) ;
241
+
242
+ const testName = this . testName ( item . payload . id ) ;
243
+ const testIndex = runState . getTestItemIndex ( testName , undefined ) ;
244
+ // If a test has test cases it is paramterized and we need to notify
245
+ // the caller that the TestClass should be added to the vscode.TestRun
246
+ // before it starts.
247
+ item . payload . _testCases
248
+ . map ( ( testCase , index ) =>
249
+ this . parameterizedFunctionTestCaseToTestClass (
250
+ item . payload . id ,
251
+ testCase ,
252
+ sourceLocationToVSCodeLocation (
253
+ item . payload . sourceLocation . _filePath ,
254
+ item . payload . sourceLocation . line ,
255
+ item . payload . sourceLocation . column
256
+ ) ,
257
+ index
258
+ )
259
+ )
260
+ . flatMap ( testClass => ( testClass ? [ testClass ] : [ ] ) )
261
+ . forEach ( testClass => this . addParameterizedTestCase ( testClass , testIndex ) ) ;
262
+ } else if ( item . kind === "event" ) {
263
+ if ( item . payload . kind === "runStarted" ) {
264
+ // Notify the runner that we've recieved all the test cases and
265
+ // are going to start running tests now.
266
+ this . testRunStarted ( ) ;
267
+ } else if ( item . payload . kind === "testStarted" ) {
161
268
const testName = this . testName ( item . payload . testID ) ;
162
269
const testIndex = runState . getTestItemIndex ( testName , undefined ) ;
163
270
runState . started ( testIndex , item . payload . instant . absolute ) ;
271
+ } else if ( item . payload . kind === "testCaseStarted" ) {
272
+ const testID = this . testCaseId (
273
+ item . payload . testID ,
274
+ this . idFromTestCase ( item . payload . _testCase )
275
+ ) ;
276
+ const testIndex = this . getTestCaseIndex ( runState , testID ) ;
277
+ runState . started ( testIndex , item . payload . instant . absolute ) ;
164
278
} else if ( item . payload . kind === "testSkipped" ) {
165
279
const testName = this . testName ( item . payload . testID ) ;
166
280
const testIndex = runState . getTestItemIndex ( testName , undefined ) ;
167
281
runState . skipped ( testIndex ) ;
168
282
} else if ( item . payload . kind === "issueRecorded" ) {
169
- const testName = this . testName ( item . payload . testID ) ;
170
- const testIndex = runState . getTestItemIndex ( testName , undefined ) ;
283
+ const testID = this . testCaseId (
284
+ item . payload . testID ,
285
+ this . idFromTestCase ( item . payload . _testCase )
286
+ ) ;
287
+ const testIndex = this . getTestCaseIndex ( runState , testID ) ;
171
288
const sourceLocation = item . payload . issue . sourceLocation ;
289
+ const location = sourceLocationToVSCodeLocation (
290
+ sourceLocation . _filePath ,
291
+ sourceLocation . line ,
292
+ sourceLocation . column
293
+ ) ;
172
294
item . payload . messages . forEach ( message => {
173
- runState . recordIssue ( testIndex , message . text , {
174
- file : sourceLocation . _filePath ,
175
- line : sourceLocation . line ,
176
- column : sourceLocation . column ,
177
- } ) ;
295
+ runState . recordIssue ( testIndex , message . text , location ) ;
178
296
} ) ;
179
- } else if ( item . payload . kind === "testCaseEnded" || item . payload . kind === "testEnded" ) {
297
+
298
+ if ( testID !== item . payload . testID ) {
299
+ // const testName = this.testName(item.payload.testID);
300
+ const testIndex = this . getTestCaseIndex ( runState , item . payload . testID ) ;
301
+ item . payload . messages . forEach ( message => {
302
+ runState . recordIssue ( testIndex , message . text , location ) ;
303
+ } ) ;
304
+ }
305
+ } else if ( item . payload . kind === "testEnded" ) {
180
306
const testName = this . testName ( item . payload . testID ) ;
181
307
const testIndex = runState . getTestItemIndex ( testName , undefined ) ;
182
308
309
+ // When running a single test the testEnded and testCaseEnded events
310
+ // have the same ID, and so we'd end the same test twice.
311
+ if ( this . completionMap . get ( testIndex ) ) {
312
+ return ;
313
+ }
314
+ this . completionMap . set ( testIndex , true ) ;
315
+ runState . completed ( testIndex , { timestamp : item . payload . instant . absolute } ) ;
316
+ } else if ( item . payload . kind === "testCaseEnded" ) {
317
+ const testID = this . testCaseId (
318
+ item . payload . testID ,
319
+ this . idFromTestCase ( item . payload . _testCase )
320
+ ) ;
321
+ const testIndex = this . getTestCaseIndex ( runState , testID ) ;
322
+
183
323
// When running a single test the testEnded and testCaseEnded events
184
324
// have the same ID, and so we'd end the same test twice.
185
325
if ( this . completionMap . get ( testIndex ) ) {
0 commit comments