14
14
15
15
import { ITestRunState , TestIssueDiff } from "./TestRunState" ;
16
16
import { sourceLocationToVSCodeLocation } from "../../utilities/utilities" ;
17
- import { MarkdownString , Location } from "vscode" ;
18
17
// eslint-disable-next-line @typescript-eslint/no-require-imports
19
18
import stripAnsi = require( "strip-ansi" ) ;
19
+ import { Location , MarkdownString } from "vscode" ;
20
20
21
21
/* eslint-disable no-console */
22
22
@@ -94,8 +94,9 @@ export class ParallelXCTestOutputParser implements IXCTestOutputParser {
94
94
public parseResult ( output : string , runState : ITestRunState ) {
95
95
// From 5.7 to 5.10 running with the --parallel option dumps the test results out
96
96
// to the console with no newlines, so it isn't possible to distinguish where errors
97
- // begin and end. Consequently we can't record them, and so we manually mark them
98
- // as passed or failed here with a manufactured issue.
97
+ // begin and end. Consequently we can't record them. For these versions we rely on the
98
+ // generated xunit XML, which we can parse and mark tests as passed or failed here with
99
+ // manufactured issues.
99
100
// Don't attempt to parse the console output of parallel tests between 5.7 and 5.10
100
101
// as it doesn't have newlines. You might get lucky and find the output is split
101
102
// in the right spot, but more often than not we wont be able to parse it.
@@ -116,6 +117,56 @@ export class ParallelXCTestOutputParser implements IXCTestOutputParser {
116
117
class ParallelXCTestRunStateProxy implements ITestRunState {
117
118
constructor ( private runState : ITestRunState ) { }
118
119
120
+ get excess ( ) : string | undefined {
121
+ return this . runState . excess ;
122
+ }
123
+
124
+ set excess ( value : string | undefined ) {
125
+ this . runState . excess = value ;
126
+ }
127
+
128
+ get activeSuite ( ) : string | undefined {
129
+ return this . runState . activeSuite ;
130
+ }
131
+
132
+ set activeSuite ( value : string | undefined ) {
133
+ this . runState . activeSuite = value ;
134
+ }
135
+
136
+ get pendingSuiteOutput ( ) : string [ ] | undefined {
137
+ return this . runState . pendingSuiteOutput ;
138
+ }
139
+
140
+ set pendingSuiteOutput ( value : string [ ] | undefined ) {
141
+ this . runState . pendingSuiteOutput = value ;
142
+ }
143
+
144
+ get failedTest ( ) :
145
+ | {
146
+ testIndex : number ;
147
+ message : string ;
148
+ file : string ;
149
+ lineNumber : number ;
150
+ complete : boolean ;
151
+ }
152
+ | undefined {
153
+ return this . runState . failedTest ;
154
+ }
155
+
156
+ set failedTest (
157
+ value :
158
+ | {
159
+ testIndex : number ;
160
+ message : string ;
161
+ file : string ;
162
+ lineNumber : number ;
163
+ complete : boolean ;
164
+ }
165
+ | undefined
166
+ ) {
167
+ this . runState . failedTest = value ;
168
+ }
169
+
119
170
getTestItemIndex ( id : string , filename : string | undefined ) : number {
120
171
return this . runState . getTestItemIndex ( id , filename ) ;
121
172
}
@@ -153,160 +204,154 @@ export class XCTestOutputParser implements IXCTestOutputParser {
153
204
* @param output Output from `swift test`
154
205
*/
155
206
public parseResult ( rawOutput : string , runState : ITestRunState ) {
156
- try {
157
- console . log (
158
- ">>>>>>>>>>>>>>>> RAW XCTEST OUTPUT >>>>>>>>>>>>>>>>>>> :\n" +
159
- rawOutput . split ( "\n" ) . join ( "\n" )
160
- ) ;
161
- console . log ( ">>>>>>>>>>>>>>>> RAW XCTEST OUTPUT COMPLETE >>>>>>>>>>>>>>>>>>>" ) ;
162
- const annotated = rawOutput
163
- . replaceAll ( "\r\n" , "🐀❌" )
164
- . replaceAll ( "\n\r" , "❌🐀" )
165
- . replaceAll ( "\n" , "❌" )
166
- . replaceAll ( "\r" , "🐀" ) ;
167
- console . log (
168
- ">>>>>>>>>>>>>>>> ANNOTATED XCTEST OUTPUT >>>>>>>>>>>>>>>>>>> :\n" + annotated
169
- ) ;
170
- console . log ( ">>>>>>>>>>>>>>>> ANNOTATED XCTEST OUTPUT COMPLETE >>>>>>>>>>>>>>>>>>>" ) ;
171
-
172
- // Windows is inserting ANSI codes into the output to do things like clear the cursor,
173
- // which we don't care about.
174
- const output = process . platform === "win32" ? stripAnsi ( rawOutput ) : rawOutput ;
175
- const output2 = output . replace ( / \r \n / g, "\n" ) ;
176
- const lines = output2 . split ( "\n" ) ;
177
- console . log ( ">>> Lines to Process:" , lines ) ;
178
- console . log ( ">>> Run State Excess |" + runState . excess + "|" ) ;
179
- if ( runState . excess ) {
180
- console . log ( ">>> !Had Excess from previous output |" + runState . excess + "|" ) ;
181
- lines [ 0 ] = runState . excess + lines [ 0 ] ;
182
- console . log ( ">>> !New line[0] is now |" + lines [ 0 ] + "|" ) ;
207
+ console . log (
208
+ ">>>>>>>>>>>>>>>> RAW XCTEST OUTPUT >>>>>>>>>>>>>>>>>>> :\n" +
209
+ rawOutput . split ( "\n" ) . join ( "\n" )
210
+ ) ;
211
+ console . log ( ">>>>>>>>>>>>>>>> RAW XCTEST OUTPUT COMPLETE >>>>>>>>>>>>>>>>>>>" ) ;
212
+ const annotated = rawOutput
213
+ . replaceAll ( "\r\n" , "🐀❌" )
214
+ . replaceAll ( "\n\r" , "❌🐀" )
215
+ . replaceAll ( "\n" , "❌" )
216
+ . replaceAll ( "\r" , "🐀" ) ;
217
+ console . log ( ">>>>>>>>>>>>>>>> ANNOTATED XCTEST OUTPUT >>>>>>>>>>>>>>>>>>> :\n" + annotated ) ;
218
+ console . log ( ">>>>>>>>>>>>>>>> ANNOTATED XCTEST OUTPUT COMPLETE >>>>>>>>>>>>>>>>>>>" ) ;
219
+
220
+ // Windows is inserting ANSI codes into the output to do things like clear the cursor,
221
+ // which we don't care about.
222
+ const output = process . platform === "win32" ? stripAnsi ( rawOutput ) : rawOutput ;
223
+ const output2 = output . replace ( / \r \n / g, "\n" ) ;
224
+ const lines = output2 . split ( "\n" ) ;
225
+ console . log ( ">>> Lines to Process:" , lines ) ;
226
+ console . log ( ">>> Run State Excess |" + runState . excess + "|" ) ;
227
+ if ( runState . excess ) {
228
+ console . log ( ">>> !Had Excess from previous output |" + runState . excess + "|" ) ;
229
+ lines [ 0 ] = runState . excess + lines [ 0 ] ;
230
+ console . log ( ">>> !New line[0] is now |" + lines [ 0 ] + "|" ) ;
231
+ }
232
+ // pop empty string off the end of the lines array
233
+ if ( lines . length > 0 && lines [ lines . length - 1 ] === "" ) {
234
+ console . log ( ">>> Trimming last line" ) ;
235
+ lines . pop ( ) ;
236
+ }
237
+ // if submitted text does not end with a newline then pop that off and store in excess
238
+ // for next call of parseResult
239
+ if ( output2 [ output2 . length - 1 ] !== "\n" ) {
240
+ runState . excess = lines . pop ( ) ;
241
+ console . log ( ">>> Saving Excess |" + runState . excess + "|" ) ;
242
+ } else {
243
+ console . log ( ">>> Unsetting Excess" ) ;
244
+ runState . excess = undefined ;
245
+ }
246
+
247
+ console . log ( ">>> Lines to Process AFTER:" , lines ) ;
248
+
249
+ // Non-Darwin test output does not include the test target name. The only way to find out
250
+ // the target for a test is when it fails and returns a file name. If we find failed tests
251
+ // first and then remove them from the list we cannot set them to passed by mistake.
252
+ // We extract the file name from the error and use that to check whether the file belongs
253
+ // to the target associated with the TestItem. This does not work 100% as the error could
254
+ // occur in another target, so we revert to just searching for class and function name if
255
+ // the above method is unsuccessful.
256
+ for ( const line of lines ) {
257
+ console . log ( ">>> Process Line: |" + line + "|" ) ;
258
+ // Regex "Test Case '-[<test target> <class.function>]' started"
259
+ const startedMatch = this . regex . started . exec ( line ) ;
260
+ if ( startedMatch ) {
261
+ const testName = `${ startedMatch [ 1 ] } /${ startedMatch [ 2 ] } ` ;
262
+
263
+ // Save the active TestTarget.SuiteClass.
264
+ // Note that TestTarget is only present on Darwin.
265
+ runState . activeSuite = startedMatch [ 1 ] ;
266
+ this . processPendingSuiteOutput ( runState , startedMatch [ 1 ] ) ;
267
+
268
+ const startedTestIndex = runState . getTestItemIndex ( testName , undefined ) ;
269
+ this . startTest ( startedTestIndex , runState ) ;
270
+ this . appendTestOutput ( startedTestIndex , line , runState ) ;
271
+ continue ;
183
272
}
184
- // pop empty string off the end of the lines array
185
- if ( lines . length > 0 && lines [ lines . length - 1 ] === "" ) {
186
- console . log ( ">>> Trimming last line" ) ;
187
- lines . pop ( ) ;
273
+ // Regex "Test Case '-[<test target> <class.function>]' <completion_state> (<duration> seconds)"
274
+ const finishedMatch = this . regex . finished . exec ( line ) ;
275
+ if ( finishedMatch ) {
276
+ const testName = `${ finishedMatch [ 1 ] } /${ finishedMatch [ 2 ] } ` ;
277
+ const testIndex = runState . getTestItemIndex ( testName , undefined ) ;
278
+ const state = finishedMatch [ 3 ] as TestCompletionState ;
279
+ const duration = + finishedMatch [ 4 ] ;
280
+ console . log (
281
+ "!!! >>>>> !!!! FINISHED MATCH ON LINE:" ,
282
+ line ,
283
+ "...STATE IS" ,
284
+ state ,
285
+ "... TEST INDEX IS" ,
286
+ testIndex ,
287
+ "... FAILED TEST IS" ,
288
+ runState . failedTest
289
+ ) ;
290
+ switch ( state ) {
291
+ case TestCompletionState . failed :
292
+ this . failTest ( testIndex , { duration } , runState ) ;
293
+ break ;
294
+ case TestCompletionState . passed :
295
+ this . passTest ( testIndex , { duration } , runState ) ;
296
+ break ;
297
+ case TestCompletionState . skipped :
298
+ this . skipTest ( testIndex , runState ) ;
299
+ break ;
300
+ }
301
+
302
+ this . appendTestOutput ( testIndex , line , runState ) ;
303
+ continue ;
188
304
}
189
- // if submitted text does not end with a newline then pop that off and store in excess
190
- // for next call of parseResult
191
- if ( output2 [ output2 . length - 1 ] !== "\n" ) {
192
- runState . excess = lines . pop ( ) ;
193
- console . log ( ">>> Saving Excess |" + runState . excess + "|" ) ;
194
- } else {
195
- console . log ( ">>> Unsetting Excess" ) ;
196
- runState . excess = undefined ;
305
+ // Regex "<path/to/test>:<line number>: error: <class>.<function> : <error>"
306
+ const errorMatch = this . regex . error . exec ( line ) ;
307
+ if ( errorMatch ) {
308
+ const testName = `${ errorMatch [ 3 ] } /${ errorMatch [ 4 ] } ` ;
309
+ const failedTestIndex = runState . getTestItemIndex ( testName , errorMatch [ 1 ] ) ;
310
+ console . log (
311
+ ">>> ERROR MATCH ON LINE:" ,
312
+ line ,
313
+ "...FAILED TEST INDEX IS" ,
314
+ failedTestIndex
315
+ ) ;
316
+ this . startErrorMessage (
317
+ failedTestIndex ,
318
+ errorMatch [ 5 ] ,
319
+ errorMatch [ 1 ] ,
320
+ errorMatch [ 2 ] ,
321
+ runState
322
+ ) ;
323
+ this . appendTestOutput ( failedTestIndex , line , runState ) ;
324
+ continue ;
197
325
}
198
-
199
- console . log ( ">>> Lines to Process AFTER:" , lines ) ;
200
-
201
- // Non-Darwin test output does not include the test target name. The only way to find out
202
- // the target for a test is when it fails and returns a file name. If we find failed tests
203
- // first and then remove them from the list we cannot set them to passed by mistake.
204
- // We extract the file name from the error and use that to check whether the file belongs
205
- // to the target associated with the TestItem. This does not work 100% as the error could
206
- // occur in another target, so we revert to just searching for class and function name if
207
- // the above method is unsuccessful.
208
- for ( const line of lines ) {
209
- console . log ( ">>> Process Line: |" + line + "|" ) ;
210
- // Regex "Test Case '-[<test target> <class.function>]' started"
211
- const startedMatch = this . regex . started . exec ( line ) ;
212
- if ( startedMatch ) {
213
- const testName = `${ startedMatch [ 1 ] } /${ startedMatch [ 2 ] } ` ;
214
-
215
- // Save the active TestTarget.SuiteClass.
216
- // Note that TestTarget is only present on Darwin.
217
- runState . activeSuite = startedMatch [ 1 ] ;
218
- this . processPendingSuiteOutput ( runState , startedMatch [ 1 ] ) ;
219
-
220
- const startedTestIndex = runState . getTestItemIndex ( testName , undefined ) ;
221
- this . startTest ( startedTestIndex , runState ) ;
222
- this . appendTestOutput ( startedTestIndex , line , runState ) ;
223
- continue ;
224
- }
225
- // Regex "Test Case '-[<test target> <class.function>]' <completion_state> (<duration> seconds)"
226
- const finishedMatch = this . regex . finished . exec ( line ) ;
227
- if ( finishedMatch ) {
228
- const testName = `${ finishedMatch [ 1 ] } /${ finishedMatch [ 2 ] } ` ;
229
- const testIndex = runState . getTestItemIndex ( testName , undefined ) ;
230
- const state = finishedMatch [ 3 ] as TestCompletionState ;
231
- const duration = + finishedMatch [ 4 ] ;
232
- console . log (
233
- "!!! >>>>> !!!! FINISHED MATCH ON LINE:" ,
234
- line ,
235
- "...STATE IS" ,
236
- state ,
237
- "... TEST INDEX IS" ,
238
- testIndex ,
239
- "... FAILED TEST IS" ,
240
- runState . failedTest
241
- ) ;
242
- switch ( state ) {
243
- case TestCompletionState . failed :
244
- this . failTest ( testIndex , { duration } , runState ) ;
245
- break ;
246
- case TestCompletionState . passed :
247
- this . passTest ( testIndex , { duration } , runState ) ;
248
- break ;
249
- case TestCompletionState . skipped :
250
- this . skipTest ( testIndex , runState ) ;
251
- break ;
252
- }
253
-
254
- this . appendTestOutput ( testIndex , line , runState ) ;
255
- continue ;
256
- }
257
- // Regex "<path/to/test>:<line number>: error: <class>.<function> : <error>"
258
- const errorMatch = this . regex . error . exec ( line ) ;
259
- if ( errorMatch ) {
260
- const testName = `${ errorMatch [ 3 ] } /${ errorMatch [ 4 ] } ` ;
261
- const failedTestIndex = runState . getTestItemIndex ( testName , errorMatch [ 1 ] ) ;
262
- console . log (
263
- ">>> ERROR MATCH ON LINE:" ,
264
- line ,
265
- "...FAILED TEST INDEX IS" ,
266
- failedTestIndex
267
- ) ;
268
- this . startErrorMessage (
269
- failedTestIndex ,
270
- errorMatch [ 5 ] ,
271
- errorMatch [ 1 ] ,
272
- errorMatch [ 2 ] ,
273
- runState
274
- ) ;
275
- this . appendTestOutput ( failedTestIndex , line , runState ) ;
276
- continue ;
277
- }
278
- // Regex "<path/to/test>:<line number>: <class>.<function> : Test skipped"
279
- const skippedMatch = this . regex . skipped . exec ( line ) ;
280
- if ( skippedMatch ) {
281
- const testName = `${ skippedMatch [ 3 ] } /${ skippedMatch [ 4 ] } ` ;
282
- const skippedTestIndex = runState . getTestItemIndex ( testName , skippedMatch [ 1 ] ) ;
283
- this . skipTest ( skippedTestIndex , runState ) ;
284
- this . appendTestOutput ( skippedTestIndex , line , runState ) ;
285
- continue ;
286
- }
287
- // Regex "Test Suite '-[<test target> <class.function>]' started"
288
- const startedSuiteMatch = this . regex . startedSuite . exec ( line ) ;
289
- if ( startedSuiteMatch ) {
290
- this . startTestSuite ( startedSuiteMatch [ 1 ] , line , runState ) ;
291
- continue ;
292
- }
293
- // Regex "Test Suite '-[<test target> <class.function>]' passed"
294
- const passedSuiteMatch = this . regex . passedSuite . exec ( line ) ;
295
- if ( passedSuiteMatch ) {
296
- this . completeSuite ( runState , line , this . passTestSuite ) ;
297
- continue ;
298
- }
299
- // Regex "Test Suite '-[<test target> <class.function>]' failed"
300
- const failedSuiteMatch = this . regex . failedSuite . exec ( line ) ;
301
- if ( failedSuiteMatch ) {
302
- this . completeSuite ( runState , line , this . failTestSuite ) ;
303
- continue ;
304
- }
305
- // unrecognised output could be the continuation of a previous error message
306
- this . continueErrorMessage ( line , runState ) ;
326
+ // Regex "<path/to/test>:<line number>: <class>.<function> : Test skipped"
327
+ const skippedMatch = this . regex . skipped . exec ( line ) ;
328
+ if ( skippedMatch ) {
329
+ const testName = `${ skippedMatch [ 3 ] } /${ skippedMatch [ 4 ] } ` ;
330
+ const skippedTestIndex = runState . getTestItemIndex ( testName , skippedMatch [ 1 ] ) ;
331
+ this . skipTest ( skippedTestIndex , runState ) ;
332
+ this . appendTestOutput ( skippedTestIndex , line , runState ) ;
333
+ continue ;
334
+ }
335
+ // Regex "Test Suite '-[<test target> <class.function>]' started"
336
+ const startedSuiteMatch = this . regex . startedSuite . exec ( line ) ;
337
+ if ( startedSuiteMatch ) {
338
+ this . startTestSuite ( startedSuiteMatch [ 1 ] , line , runState ) ;
339
+ continue ;
340
+ }
341
+ // Regex "Test Suite '-[<test target> <class.function>]' passed"
342
+ const passedSuiteMatch = this . regex . passedSuite . exec ( line ) ;
343
+ if ( passedSuiteMatch ) {
344
+ this . completeSuite ( runState , line , this . passTestSuite ) ;
345
+ continue ;
346
+ }
347
+ // Regex "Test Suite '-[<test target> <class.function>]' failed"
348
+ const failedSuiteMatch = this . regex . failedSuite . exec ( line ) ;
349
+ if ( failedSuiteMatch ) {
350
+ this . completeSuite ( runState , line , this . failTestSuite ) ;
351
+ continue ;
307
352
}
308
- } catch ( e ) {
309
- console . error ( "!!! >>>>> !!! ERROR PARSING OUTPUT" , e ) ;
353
+ // unrecognised output could be the continuation of a previous error message
354
+ this . continueErrorMessage ( line , runState ) ;
310
355
}
311
356
}
312
357
0 commit comments