@@ -148,6 +148,7 @@ export enum TestSymbol {
148
148
difference = "difference" ,
149
149
warning = "warning" ,
150
150
details = "details" ,
151
+ none = "none" ,
151
152
}
152
153
153
154
export interface EventMessage {
@@ -222,9 +223,27 @@ export class SwiftTestingOutputParser {
222
223
* @param chunk A chunk of stdout emitted during a test run.
223
224
*/
224
225
public parseStdout = ( ( ) => {
225
- const values = Object . values ( TestSymbol ) . map ( symbol =>
226
- regexEscapedString ( SymbolRenderer . eventMessageSymbol ( symbol ) )
227
- ) ;
226
+ const values = [
227
+ ...Object . values ( TestSymbol ) . map ( symbol =>
228
+ regexEscapedString (
229
+ // Trim the ANSI reset code from the search since some lines
230
+ // are fully colorized from the symbol to the end of line.
231
+ SymbolRenderer . eventMessageSymbol ( symbol ) . replace (
232
+ SymbolRenderer . resetANSIEscapeCode ,
233
+ ""
234
+ )
235
+ )
236
+ ) ,
237
+ // It is possible there is no symbol for a line produced by swift-testing,
238
+ // for instance if the user has a multi line comment before a failing expectation
239
+ // only the first line of the printed comment will have a symbol, but to make the
240
+ // indentation consistent the subsequent lines will have three spaces. We don't want
241
+ // to treat this as output produced by the user during the test run, so omit these.
242
+ // This isn't ideal since this will swallow lines the user prints if they start with
243
+ // three spaces, but until we have user output as part of the JSON event stream we have
244
+ // this workaround.
245
+ " " ,
246
+ ] ;
228
247
229
248
// Build a regex of all the line beginnings that come out of swift-testing events.
230
249
const isSwiftTestingLineBeginning = new RegExp ( `^${ values . join ( "|" ) } ` ) ;
@@ -313,13 +332,47 @@ export class SwiftTestingOutputParser {
313
332
testIndex : number | undefined
314
333
) {
315
334
messages . forEach ( message => {
316
- runState . recordOutput (
317
- testIndex ,
318
- `${ SymbolRenderer . eventMessageSymbol ( message . symbol ) } ${ message . text } \r\n`
319
- ) ;
335
+ runState . recordOutput ( testIndex , `${ MessageRenderer . render ( message ) } \r\n` ) ;
320
336
} ) ;
321
337
}
322
338
339
+ /**
340
+ * Partitions a collection of messages in to issues and details about the issues.
341
+ * This is used to print the issues first, followed by the details.
342
+ */
343
+ private partitionIssueMessages ( messages : EventMessage [ ] ) : {
344
+ issues : EventMessage [ ] ;
345
+ details : EventMessage [ ] ;
346
+ } {
347
+ return messages . reduce (
348
+ ( buckets , message ) => {
349
+ const key =
350
+ message . symbol === "details" ||
351
+ message . symbol === "default" ||
352
+ message . symbol === "none"
353
+ ? "details"
354
+ : "issues" ;
355
+ return { ...buckets , [ key ] : [ ...buckets [ key ] , message ] } ;
356
+ } ,
357
+ {
358
+ issues : [ ] ,
359
+ details : [ ] ,
360
+ }
361
+ ) ;
362
+ }
363
+
364
+ /*
365
+ * A multi line comment preceeding an issue will have a 'default' symbol for
366
+ * all lines except the first one. To match the swift-testing command line we
367
+ * should show no symbol on these lines.
368
+ */
369
+ private transformIssueMessageSymbols ( messages : EventMessage [ ] ) : EventMessage [ ] {
370
+ return messages . map ( message => ( {
371
+ ...message ,
372
+ symbol : message . symbol === "default" ? TestSymbol . none : message . symbol ,
373
+ } ) ) ;
374
+ }
375
+
323
376
private parse ( item : SwiftTestEvent , runState : ITestRunState ) {
324
377
if (
325
378
item . kind === "test" &&
@@ -391,14 +444,31 @@ export class SwiftTestingOutputParser {
391
444
sourceLocation . line ,
392
445
sourceLocation . column
393
446
) ;
394
- item . payload . messages . forEach ( message => {
395
- runState . recordIssue ( testIndex , message . text , isKnown , location ) ;
447
+
448
+ const messages = this . transformIssueMessageSymbols ( item . payload . messages ) ;
449
+ const { issues, details } = this . partitionIssueMessages ( messages ) ;
450
+
451
+ // Order the details after the issue text.
452
+ const additionalDetails = details
453
+ . map ( message => MessageRenderer . render ( message ) )
454
+ . join ( "\n" ) ;
455
+
456
+ issues . forEach ( message => {
457
+ runState . recordIssue (
458
+ testIndex ,
459
+ additionalDetails . length > 0
460
+ ? `${ MessageRenderer . render ( message ) } \n${ additionalDetails } `
461
+ : MessageRenderer . render ( message ) ,
462
+ isKnown ,
463
+ location
464
+ ) ;
396
465
} ) ;
397
- this . recordOutput ( runState , item . payload . messages , testIndex ) ;
466
+
467
+ this . recordOutput ( runState , messages , testIndex ) ;
398
468
399
469
if ( item . payload . _testCase && testID !== item . payload . testID ) {
400
470
const testIndex = this . getTestCaseIndex ( runState , item . payload . testID ) ;
401
- item . payload . messages . forEach ( message => {
471
+ messages . forEach ( message => {
402
472
runState . recordIssue ( testIndex , message . text , isKnown , location ) ;
403
473
} ) ;
404
474
}
@@ -439,6 +509,32 @@ export class SwiftTestingOutputParser {
439
509
}
440
510
}
441
511
512
+ export class MessageRenderer {
513
+ /**
514
+ * Converts a swift-testing `EventMessage` to a colorized symbol and message text.
515
+ *
516
+ * @param message An event message, typically found on an `EventRecordPayload`.
517
+ * @returns A string colorized with ANSI escape codes.
518
+ */
519
+ static render ( message : EventMessage ) : string {
520
+ return `${ SymbolRenderer . eventMessageSymbol ( message . symbol ) } ${ MessageRenderer . colorize ( message . symbol , message . text ) } ` ;
521
+ }
522
+
523
+ private static colorize ( symbolType : TestSymbol , message : string ) : string {
524
+ const ansiEscapeCodePrefix = "\u{001B}[" ;
525
+ const resetANSIEscapeCode = `${ ansiEscapeCodePrefix } 0m` ;
526
+ switch ( symbolType ) {
527
+ case TestSymbol . details :
528
+ case TestSymbol . skip :
529
+ case TestSymbol . difference :
530
+ case TestSymbol . passWithKnownIssue :
531
+ return `${ ansiEscapeCodePrefix } 90m${ message } ${ resetANSIEscapeCode } ` ;
532
+ default :
533
+ return message ;
534
+ }
535
+ }
536
+ }
537
+
442
538
export class SymbolRenderer {
443
539
/**
444
540
* Converts a swift-testing symbol identifier in to a colorized unicode symbol.
@@ -450,6 +546,9 @@ export class SymbolRenderer {
450
546
return this . colorize ( symbol , this . symbol ( symbol ) ) ;
451
547
}
452
548
549
+ static ansiEscapeCodePrefix = "\u{001B}[" ;
550
+ static resetANSIEscapeCode = `${ SymbolRenderer . ansiEscapeCodePrefix } 0m` ;
551
+
453
552
// This is adapted from
454
553
// https://github.com/apple/swift-testing/blob/786ade71421eb1d8a9c1d99c902cf1c93096e7df/Sources/Testing/Events/Recorder/Event.Symbol.swift#L102
455
554
public static symbol ( symbol : TestSymbol ) : string {
@@ -469,6 +568,8 @@ export class SymbolRenderer {
469
568
return "\u{25B2}" ; // Unicode: BLACK UP-POINTING TRIANGLE
470
569
case TestSymbol . details :
471
570
return "\u{2192}" ; // Unicode: RIGHTWARDS ARROW
571
+ case TestSymbol . none :
572
+ return "" ;
472
573
}
473
574
} else {
474
575
switch ( symbol ) {
@@ -486,28 +587,29 @@ export class SymbolRenderer {
486
587
return "\u{26A0}\u{FE0E}" ; // Unicode: WARNING SIGN + VARIATION SELECTOR-15 (disable emoji)
487
588
case TestSymbol . details :
488
589
return "\u{21B3}" ; // Unicode: DOWNWARDS ARROW WITH TIP RIGHTWARDS
590
+ case TestSymbol . none :
591
+ return " " ;
489
592
}
490
593
}
491
594
}
492
595
493
596
// This is adapted from
494
597
// https://github.com/apple/swift-testing/blob/786ade71421eb1d8a9c1d99c902cf1c93096e7df/Sources/Testing/Events/Recorder/Event.ConsoleOutputRecorder.swift#L164
495
598
private static colorize ( symbolType : TestSymbol , symbol : string ) : string {
496
- const ansiEscapeCodePrefix = "\u{001B}[" ;
497
- const resetANSIEscapeCode = `${ ansiEscapeCodePrefix } 0m` ;
498
599
switch ( symbolType ) {
499
600
case TestSymbol . default :
601
+ case TestSymbol . details :
500
602
case TestSymbol . skip :
501
603
case TestSymbol . difference :
502
604
case TestSymbol . passWithKnownIssue :
503
- return `${ ansiEscapeCodePrefix } 90m${ symbol } ${ resetANSIEscapeCode } ` ;
605
+ return `${ SymbolRenderer . ansiEscapeCodePrefix } 90m${ symbol } ${ SymbolRenderer . resetANSIEscapeCode } ` ;
504
606
case TestSymbol . pass :
505
- return `${ ansiEscapeCodePrefix } 92m${ symbol } ${ resetANSIEscapeCode } ` ;
607
+ return `${ SymbolRenderer . ansiEscapeCodePrefix } 92m${ symbol } ${ SymbolRenderer . resetANSIEscapeCode } ` ;
506
608
case TestSymbol . fail :
507
- return `${ ansiEscapeCodePrefix } 91m${ symbol } ${ resetANSIEscapeCode } ` ;
609
+ return `${ SymbolRenderer . ansiEscapeCodePrefix } 91m${ symbol } ${ SymbolRenderer . resetANSIEscapeCode } ` ;
508
610
case TestSymbol . warning :
509
- return `${ ansiEscapeCodePrefix } 93m${ symbol } ${ resetANSIEscapeCode } ` ;
510
- case TestSymbol . details :
611
+ return `${ SymbolRenderer . ansiEscapeCodePrefix } 93m${ symbol } ${ SymbolRenderer . resetANSIEscapeCode } ` ;
612
+ case TestSymbol . none :
511
613
default :
512
614
return symbol ;
513
615
}
0 commit comments