1
1
using System ;
2
2
using System . Collections . Concurrent ;
3
3
using System . Collections . Generic ;
4
+ using System . IO ;
5
+ using System . Linq ;
6
+ using System . Text ;
4
7
using System . Threading ;
8
+ using Microsoft . VisualStudio . TestPlatform . ObjectModel ;
5
9
using Xunit . Runner . Common ;
6
10
using Xunit . Sdk ;
7
11
using VsTestCase = Microsoft . VisualStudio . TestPlatform . ObjectModel . TestCase ;
@@ -16,6 +20,8 @@ namespace Xunit.Runner.VisualStudio;
16
20
17
21
internal sealed class VsExecutionSink : TestMessageSink , IDisposable
18
22
{
23
+ static readonly HashSet < char > InvalidFileNameChars = Path . GetInvalidFileNameChars ( ) . ToHashSet ( ) ;
24
+
19
25
readonly Func < bool > cancelledThunk ;
20
26
readonly LoggerHelper logger ;
21
27
readonly IMessageSink innerSink ;
@@ -24,10 +30,12 @@ internal sealed class VsExecutionSink : TestMessageSink, IDisposable
24
30
readonly ConcurrentDictionary < string , DateTimeOffset > startTimeByTestID = [ ] ;
25
31
readonly ConcurrentDictionary < string , List < ITestCaseStarting > > testCasesByAssemblyID = [ ] ;
26
32
readonly ConcurrentDictionary < string , ITestCaseStarting > testCasesByCaseID = [ ] ;
33
+ readonly ConcurrentDictionary < string , ( string actionDescription , ITestMessage test , VsTestResult testResult ) > testResultByCaseID = [ ] ;
27
34
readonly ConcurrentDictionary < string , List < ITestCaseStarting > > testCasesByClassID = [ ] ;
28
35
readonly ConcurrentDictionary < string , List < ITestCaseStarting > > testCasesByCollectionID = [ ] ;
29
36
readonly ConcurrentDictionary < string , List < ITestCaseStarting > > testCasesByMethodID = [ ] ;
30
37
readonly Dictionary < string , VsTestCase > testCasesMap ;
38
+ static readonly Uri uri = new ( Constants . ExecutorUri ) ;
31
39
32
40
public VsExecutionSink (
33
41
IMessageSink innerSink ,
@@ -298,16 +306,77 @@ void HandleTestFailed(MessageHandlerArgs<ITestFailed> args)
298
306
result . ErrorMessage = ExceptionUtility . CombineMessages ( testFailed ) ;
299
307
result . ErrorStackTrace = ExceptionUtility . CombineStackTraces ( testFailed ) ;
300
308
301
- TryAndReport ( "RecordResult (Fail)" , testFailed , ( ) => recorder . RecordResult ( result ) ) ;
309
+ DeferReportUntilTestFinished ( "RecordResult (Fail)" , testFailed , result ) ;
302
310
}
303
311
else
304
312
LogWarning ( testFailed , "(Fail) Could not find VS test case for {0} (ID = {1})" , TestDisplayName ( testFailed ) , testFailed . TestCaseUniqueID ) ;
305
313
306
314
HandleCancellation ( args ) ;
307
315
}
308
316
309
- void HandleTestFinished ( MessageHandlerArgs < ITestFinished > args ) =>
317
+ void HandleTestFinished ( MessageHandlerArgs < ITestFinished > args )
318
+ {
319
+ var testUniqueID = args . Message . TestUniqueID ;
320
+
321
+ if ( testResultByCaseID . TryRemove ( testUniqueID , out var testResultEntry ) )
322
+ {
323
+ var ( actionDescription , test , testResult ) = testResultEntry ;
324
+ var attachments = args . Message . Attachments ;
325
+
326
+ if ( attachments . Count != 0 )
327
+ try
328
+ {
329
+ var basePath = Path . Combine ( Path . GetTempPath ( ) , testUniqueID ) ;
330
+ Directory . CreateDirectory ( basePath ) ;
331
+
332
+ var attachmentSet = new AttachmentSet ( uri , "xUnit.net" ) ;
333
+
334
+ foreach ( var kvp in attachments )
335
+ {
336
+ var localFilePath = Path . Combine ( basePath , SanitizeFileName ( kvp . Key ) ) ;
337
+
338
+ try
339
+ {
340
+ var attachmentType = kvp . Value . AttachmentType ;
341
+
342
+ if ( attachmentType == TestAttachmentType . String )
343
+ {
344
+ localFilePath += ".txt" ;
345
+ File . WriteAllText ( localFilePath , kvp . Value . AsString ( ) ) ;
346
+ }
347
+ else if ( attachmentType == TestAttachmentType . ByteArray )
348
+ {
349
+ var ( byteArray , mediaType ) = kvp . Value . AsByteArray ( ) ;
350
+ localFilePath += MediaTypeUtility . GetFileExtension ( mediaType ) ;
351
+ File . WriteAllBytes ( localFilePath , byteArray ) ;
352
+ }
353
+ else
354
+ {
355
+ LogWarning ( test , "Unknown test attachment type '{0}' for attachment '{1}' [test case ID '{2}']" , attachmentType , kvp . Key , testUniqueID ) ;
356
+ localFilePath = null ;
357
+ }
358
+
359
+ if ( localFilePath is not null )
360
+ attachmentSet . Attachments . Add ( UriDataAttachment . CreateFrom ( localFilePath , kvp . Key ) ) ;
361
+ }
362
+ catch ( Exception ex )
363
+ {
364
+ LogWarning ( test , "Exception while adding attachment '{0}' in '{1}' [test case ID '{2}']: {3}" , kvp . Key , localFilePath , testUniqueID , ex ) ;
365
+ }
366
+ }
367
+
368
+ testResult . Attachments . Add ( attachmentSet ) ;
369
+ }
370
+ catch ( Exception ex )
371
+ {
372
+ LogWarning ( test , "Exception while adding attachments [test case ID '{0}']: {1}" , testUniqueID , ex ) ;
373
+ }
374
+
375
+ TryAndReport ( actionDescription , test , ( ) => recorder . RecordResult ( testResult ) ) ;
376
+ }
377
+
310
378
MetadataCache ( args . Message ) ? . TryRemove ( args . Message ) ;
379
+ }
311
380
312
381
void HandleTestMethodCleanupFailure ( MessageHandlerArgs < ITestMethodCleanupFailure > args )
313
382
{
@@ -340,7 +409,7 @@ void HandleTestNotRun(MessageHandlerArgs<ITestNotRun> args)
340
409
341
410
var result = MakeVsTestResult ( VsTestOutcome . None , testNotRun , startTime ) ;
342
411
if ( result is not null )
343
- TryAndReport ( "RecordResult (None)" , testNotRun , ( ) => recorder . RecordResult ( result ) ) ;
412
+ DeferReportUntilTestFinished ( "RecordResult (None)" , testNotRun , result ) ;
344
413
else
345
414
LogWarning ( testNotRun , "(NotRun) Could not find VS test case for {0} (ID = {1})" , TestDisplayName ( testNotRun ) , testNotRun . TestCaseUniqueID ) ;
346
415
@@ -354,7 +423,7 @@ void HandleTestPassed(MessageHandlerArgs<ITestPassed> args)
354
423
355
424
var result = MakeVsTestResult ( VsTestOutcome . Passed , testPassed , startTime ) ;
356
425
if ( result is not null )
357
- TryAndReport ( "RecordResult (Pass)" , testPassed , ( ) => recorder . RecordResult ( result ) ) ;
426
+ DeferReportUntilTestFinished ( "RecordResult (Pass)" , testPassed , result ) ;
358
427
else
359
428
LogWarning ( testPassed , "(Pass) Could not find VS test case for {0} (ID = {1})" , TestDisplayName ( testPassed ) , testPassed . TestCaseUniqueID ) ;
360
429
@@ -368,7 +437,7 @@ void HandleTestSkipped(MessageHandlerArgs<ITestSkipped> args)
368
437
369
438
var result = MakeVsTestResult ( VsTestOutcome . Skipped , testSkipped , startTime ) ;
370
439
if ( result is not null )
371
- TryAndReport ( "RecordResult (Skip)" , testSkipped , ( ) => recorder . RecordResult ( result ) ) ;
440
+ DeferReportUntilTestFinished ( "RecordResult (Skip)" , testSkipped , result ) ;
372
441
else
373
442
LogWarning ( testSkipped , "(Skip) Could not find VS test case for {0} (ID = {1})" , TestDisplayName ( testSkipped ) , testSkipped . TestCaseUniqueID ) ;
374
443
@@ -494,6 +563,25 @@ string TestDisplayName(ITestMessage msg) =>
494
563
string TestMethodName ( ITestMethodMessage msg ) =>
495
564
TestClassName ( msg ) + "." + MetadataCache ( msg ) ? . TryGetMethodMetadata ( msg ) ? . MethodName ?? $ "<unknown test method ID { msg . TestMethodUniqueID } >";
496
565
566
+ void DeferReportUntilTestFinished (
567
+ string actionDescription ,
568
+ ITestMessage test ,
569
+ VsTestResult testResult ) =>
570
+ testResultByCaseID . TryAdd ( test . TestUniqueID , ( actionDescription , test , testResult ) ) ;
571
+
572
+ string SanitizeFileName ( string fileName )
573
+ {
574
+ var result = new StringBuilder ( fileName . Length ) ;
575
+
576
+ foreach ( var c in fileName )
577
+ if ( InvalidFileNameChars . Contains ( c ) )
578
+ result . Append ( '_' ) ;
579
+ else
580
+ result . Append ( c ) ;
581
+
582
+ return result . ToString ( ) ;
583
+ }
584
+
497
585
void TryAndReport (
498
586
string actionDescription ,
499
587
ITestCaseMessage testCase ,
0 commit comments