@@ -20,15 +20,16 @@ public class CollectCoverageTests
2020 private readonly string _localPackagesPath ;
2121 private const string CoverageJsonFileName = "coverage.json" ;
2222 private const string CoverageCoberturaFileName = "coverage.cobertura.xml" ;
23+ private readonly string _repoRoot ;
2324
2425 public CollectCoverageTests ( )
2526 {
2627 _buildConfiguration = "Debug" ;
2728 _buildTargetFramework = "net8.0" ;
2829
2930 // Get local packages path (adjust based on your build output)
30- string repoRoot = Path . GetFullPath ( Path . Combine ( Directory . GetCurrentDirectory ( ) , ".." , ".." , ".." , ".." ) ) ;
31- _localPackagesPath = Path . Combine ( repoRoot , "artifacts" , "package" , _buildConfiguration . ToLowerInvariant ( ) ) ;
31+ _repoRoot = Path . GetFullPath ( Path . Combine ( Directory . GetCurrentDirectory ( ) , ".." , ".." , ".." , ".." ) ) ;
32+ _localPackagesPath = Path . Combine ( _repoRoot , "artifacts" , "package" , _buildConfiguration . ToLowerInvariant ( ) ) ;
3233 }
3334
3435 [ Fact ]
@@ -41,12 +42,10 @@ public async Task BasicCoverage_CollectsDataForCoveredLines()
4142 // Act
4243 var result = await RunTestsWithCoverage ( testProject . ProjectPath , "--coverage" ) ;
4344
44- TestContext . Current . AddAttachment (
45- "Test Output" ,
46- result . CombinedOutput ) ;
45+ TestContext . Current ? . AddAttachment ( "Test Output" , result . CombinedOutput ) ;
4746
4847 // Assert
49- Assert . Equal ( 0 , result . ExitCode ) ;
48+ Assert . True ( result . ExitCode == 0 , $ "Expected successful test run (exit code 0) but got { result . ExitCode } . \n \n { result . CombinedOutput } " ) ;
5049 Assert . Contains ( "Passed!" , result . StandardOutput ) ;
5150
5251 string [ ] coverageFiles = Directory . GetFiles ( testProject . OutputDirectory , CoverageJsonFileName , SearchOption . AllDirectories ) ;
@@ -69,12 +68,10 @@ public async Task CoverageWithFormat_GeneratesCorrectOutputFormat()
6968 testProject . ProjectPath ,
7069 "--coverage --coverage-output-format cobertura" ) ;
7170
72- TestContext . Current . AddAttachment (
73- "Test Output" ,
74- result . CombinedOutput ) ;
71+ TestContext . Current ? . AddAttachment ( "Test Output" , result . CombinedOutput ) ;
7572
7673 // Assert
77- Assert . Equal ( 0 , result . ExitCode ) ;
74+ Assert . True ( result . ExitCode == 0 , $ "Expected successful test run (exit code 0) but got { result . ExitCode } . \n \n { result . CombinedOutput } " ) ;
7875
7976 string [ ] coverageFiles = Directory . GetFiles ( testProject . OutputDirectory , CoverageCoberturaFileName , SearchOption . AllDirectories ) ;
8077 Assert . NotEmpty ( coverageFiles ) ;
@@ -94,12 +91,10 @@ public async Task CoverageInstrumentation_TracksMethodHits()
9491 // Act
9592 var result = await RunTestsWithCoverage ( testProject . ProjectPath , "--coverage" ) ;
9693
97- TestContext . Current . AddAttachment (
98- "Test Output" ,
99- result . CombinedOutput ) ;
94+ TestContext . Current ? . AddAttachment ( "Test Output" , result . CombinedOutput ) ;
10095
10196 // Assert
102- Assert . Equal ( 0 , result . ExitCode ) ;
97+ Assert . True ( result . ExitCode == 0 , $ "Expected successful test run (exit code 0) but got { result . ExitCode } . \n \n { result . CombinedOutput } " ) ;
10398
10499 string [ ] coverageFiles = Directory . GetFiles ( testProject . OutputDirectory , CoverageJsonFileName , SearchOption . AllDirectories ) ;
105100 var coverageData = ParseCoverageJson ( coverageFiles [ 0 ] ) ;
@@ -149,12 +144,10 @@ public async Task BranchCoverage_TracksConditionalPaths()
149144 // Act
150145 var result = await RunTestsWithCoverage ( testProject . ProjectPath , "--coverage" ) ;
151146
152- TestContext . Current . AddAttachment (
153- "Test Output" ,
154- result . CombinedOutput ) ;
147+ TestContext . Current ? . AddAttachment ( "Test Output" , result . CombinedOutput ) ;
155148
156149 // Assert
157- Assert . Equal ( 0 , result . ExitCode ) ;
150+ Assert . True ( result . ExitCode == 0 , $ "Expected successful test run (exit code 0) but got { result . ExitCode } . \n \n { result . CombinedOutput } " ) ;
158151
159152 string [ ] coverageFiles = Directory . GetFiles ( testProject . OutputDirectory , CoverageJsonFileName , SearchOption . AllDirectories ) ;
160153 var coverageData = ParseCoverageJson ( coverageFiles [ 0 ] ) ;
@@ -207,12 +200,10 @@ public async Task MultipleCoverageFormats_GeneratesAllReports()
207200 testProject . ProjectPath ,
208201 "--coverage --coverage-output-format json,cobertura,lcov" ) ;
209202
210- TestContext . Current . AddAttachment (
211- "Test Output" ,
212- result . CombinedOutput ) ;
203+ TestContext . Current ? . AddAttachment ( "Test Output" , result . CombinedOutput ) ;
213204
214205 // Assert
215- Assert . Equal ( 0 , result . ExitCode ) ;
206+ Assert . True ( result . ExitCode == 0 , $ "Expected successful test run (exit code 0) but got { result . ExitCode } . \n \n { result . CombinedOutput } " ) ;
216207
217208 // Verify all formats are generated
218209 Assert . NotEmpty ( Directory . GetFiles ( testProject . OutputDirectory , "coverage.json" , SearchOption . AllDirectories ) ) ;
@@ -223,14 +214,19 @@ public async Task MultipleCoverageFormats_GeneratesAllReports()
223214 #region Helper Methods
224215
225216 private TestProject CreateTestProject (
217+
226218 bool includeSimpleTest = false ,
227219 bool includeMethodTests = false ,
228220 bool includeMultipleClasses = false ,
229221 bool includeCalculatorTest = false ,
230222 bool includeBranchTest = false ,
231223 bool includeMultipleTests = false )
232224 {
233- string tempPath = Path . Combine ( Path . GetTempPath ( ) , $ "CoverletMTP_Test_{ Guid . NewGuid ( ) : N} ") ;
225+ // Use repository artifacts folder instead of user temp
226+ string artifactsTemp = Path . Combine ( _repoRoot , "artifacts" , "tmp" , _buildConfiguration . ToLowerInvariant ( ) ) ;
227+ Directory . CreateDirectory ( artifactsTemp ) ;
228+
229+ string tempPath = Path . Combine ( artifactsTemp , $ "CoverletMTP_Test_{ Guid . NewGuid ( ) : N} ") ;
234230 Directory . CreateDirectory ( tempPath ) ;
235231
236232 // Create NuGet.config to use local packages
@@ -496,10 +492,28 @@ private async Task<TestResult> RunTestsWithCoverage(string projectPath, string a
496492 string projectName = Path . GetFileNameWithoutExtension ( projectPath ) ;
497493 string testExecutable = Path . Combine ( projectDir , "bin" , _buildConfiguration , _buildTargetFramework , $ "{ projectName } .dll") ;
498494
495+ if ( ! File . Exists ( testExecutable ) )
496+ {
497+ throw new FileNotFoundException (
498+ $ "Test executable not found: { testExecutable } \n " +
499+ $ "Build may have failed silently.") ;
500+ }
501+
502+ string coverletMtpDll = Path . Combine (
503+ Path . GetDirectoryName ( testExecutable ) ! ,
504+ "coverlet.MTP.dll" ) ;
505+
506+ if ( ! File . Exists ( coverletMtpDll ) )
507+ {
508+ throw new FileNotFoundException (
509+ $ "Coverlet MTP extension not found: { coverletMtpDll } \n " +
510+ $ "The coverlet.MTP NuGet package may not have restored correctly.") ;
511+ }
512+
499513 var processStartInfo = new ProcessStartInfo
500514 {
501515 FileName = "dotnet" ,
502- Arguments = $ "exec \" { testExecutable } \" { arguments } ",
516+ Arguments = $ "exec \" { testExecutable } \" { arguments } --diagnostic --diagnostic-verbosity trace ",
503517 RedirectStandardOutput = true ,
504518 RedirectStandardError = true ,
505519 UseShellExecute = false ,
@@ -514,12 +528,28 @@ private async Task<TestResult> RunTestsWithCoverage(string projectPath, string a
514528
515529 await process . WaitForExitAsync ( ) ;
516530
531+ string errorContext = process . ExitCode switch
532+ {
533+ 0 => "Success" ,
534+ 1 => "Test failures occurred" ,
535+ 2 => "Invalid command-line arguments" ,
536+ 3 => "Test discovery failed" ,
537+ 4 => "Test execution failed" ,
538+ 5 => "Unexpected error (unhandled exception)" ,
539+ _ => "Unknown error"
540+ } ;
541+
517542 return new TestResult
518543 {
519544 ExitCode = process . ExitCode ,
545+ ErrorText = errorContext ,
520546 StandardOutput = output ,
521547 StandardError = error ,
522- CombinedOutput = $ "STDOUT:\n { output } \n \n STDERR:\n { error } "
548+ CombinedOutput = $ "=== TEST EXECUTABLE ===\n { testExecutable } \n \n " +
549+ $ "=== ARGUMENTS ===\n { arguments } \n \n " +
550+ $ "=== EXIT CODE ===\n { process . ExitCode } \n \n " +
551+ $ "=== STDOUT ===\n { output } \n \n " +
552+ $ "=== STDERR ===\n { error } "
523553 } ;
524554 }
525555
@@ -544,24 +574,43 @@ public TestProject(string projectPath, string outputDirectory)
544574
545575 public void Dispose ( )
546576 {
547- try
577+ string ? projectDir = Path . GetDirectoryName ( ProjectPath ) ;
578+ if ( projectDir == null || ! Directory . Exists ( projectDir ) )
579+ return ;
580+
581+ // Retry cleanup to handle file locks (especially on Windows)
582+ for ( int i = 0 ; i < 3 ; i ++ )
548583 {
549- string ? projectDir = Path . GetDirectoryName ( ProjectPath ) ;
550- if ( projectDir != null && Directory . Exists ( projectDir ) )
584+ try
551585 {
552- Directory . Delete ( projectDir , true ) ;
586+ Directory . Delete ( projectDir , recursive : true ) ;
587+ return ; // Success
588+ }
589+ catch ( IOException ) when ( i < 2 )
590+ {
591+ // File may be locked by antivirus or other process
592+ System . Threading . Thread . Sleep ( 100 ) ;
593+ }
594+ catch ( UnauthorizedAccessException ) when ( i < 2 )
595+ {
596+ // Mark files as normal (remove read-only) and retry
597+ foreach ( var file in Directory . GetFiles ( projectDir , "*" , SearchOption . AllDirectories ) )
598+ {
599+ File . SetAttributes ( file , FileAttributes . Normal ) ;
600+ }
601+ System . Threading . Thread . Sleep ( 100 ) ;
553602 }
554603 }
555- catch
556- {
557- // Swallow cleanup exceptions
558- }
604+
605+ // Log cleanup failure but don't throw (test already finished)
606+ Debug . WriteLine ( $ "Warning: Failed to cleanup test directory: { projectDir } ") ;
559607 }
560608 }
561609
562610 private class TestResult
563611 {
564612 public int ExitCode { get ; set ; }
613+ public string ErrorText { get ; set ; } = string . Empty ;
565614 public string StandardOutput { get ; set ; } = string . Empty ;
566615 public string StandardError { get ; set ; } = string . Empty ;
567616 public string CombinedOutput { get ; set ; } = string . Empty ;
0 commit comments