11param (
2+ [ValidateSet (' Microsoft.Testing.Platform' , ' VSTest' )]
3+ [string ] $TestPlatform = ' Microsoft.Testing.Platform' ,
24 [string ] $SolutionOrProject ,
35 [string ] $Verbosity ,
46 [string ] $Filter ,
@@ -13,14 +15,13 @@ param (
1315# "-InformationAction Continue" to every call. This is not best practice for general PowerShell scripts, but makes sense
1416# for scripts made for GHA.
1517$informationPreference = ' Continue'
18+ $useMtp = $TestPlatform -eq ' Microsoft.Testing.Platform'
19+ $SolutionOrProject = (Resolve-Path $SolutionOrProject ).Path
20+ Set-GitHubOutput ' test-count' 0
21+ Set-GitHubOutput ' dotnet-test-hang-dump' 0
1622
17- # First, we globally set test configurations using environment variables. Then acquire the list of all test projects
18- # (excluding the two test libraries) and then run each until one fails or all concludes. If a test fails, the output is
19- # sanitized from unnecessary diagnostics messages from chromedriver if the output doesn't already contain groupings,
20- # then it wraps them in "::group::<project name>". If there are already groupings, then it is not possible to nest them
21- # (https://github.com/actions/runner/issues/802) so that's omitted. The groupings make the output collapsible region on
22- # the Actions web UI. Note that we use bash to output the log using bash to avoid pwsh wrapping the output to the
23- # default buffer width.
23+ # Set test configuration through environment variables, identify test applications, then run each until one fails.
24+ # Preserve the test output, including the UI Testing Toolbox's GitHub Actions groups and annotations.
2425
2526if ($Env: RUNNER_OS -eq ' Windows' )
2627{
@@ -85,11 +86,45 @@ if ($SolutionOrProject -imatch '\.slnx?$')
8586 $tests = @ ()
8687 dotnet sln $SolutionOrProject list |
8788 Select-Object - Skip 2 |
88- Select-String ' \.Tests\.' |
89- Select-String -NotMatch ' Lombiq.Tests.UI.csproj' |
90- Select-String -NotMatch ' Lombiq.Tests.csproj' |
9189 ForEach-Object {
92- $absolutePath = Resolve-Path - Path (Join-Path - Path $solutionDirectory - ChildPath $PSItem )
90+ $absolutePath = (Resolve-Path - Path (Join-Path - Path $solutionDirectory - ChildPath $PSItem )).Path
91+
92+ # Evaluate project properties instead of relying on project names or localized test runner output.
93+ $evaluationSwitches = @ (
94+ " -p:Configuration=$Configuration "
95+ ' -getProperty:IsTestingPlatformApplication,IsTestProject'
96+ ' -verbosity:quiet'
97+ )
98+
99+ $properties = dotnet msbuild $absolutePath @evaluationSwitches | Out-String
100+
101+ if ($LASTEXITCODE -ne 0 )
102+ {
103+ Write-GitHub " Failed to evaluate test project properties for `" $absolutePath `" ."
104+ exit 1
105+ }
106+
107+ $properties = ($properties | ConvertFrom-Json ).Properties
108+
109+ if ($useMtp )
110+ {
111+ if ($properties.IsTestingPlatformApplication -eq ' true' )
112+ {
113+ $tests += $absolutePath
114+ }
115+ elseif ($properties.IsTestProject -eq ' true' )
116+ {
117+ Write-GitHub " The test project `" $absolutePath `" does not support Microsoft.Testing.Platform. Migrate it or use test-platform VSTest."
118+ exit 1
119+ }
120+
121+ return
122+ }
123+
124+ if ($properties.IsTestProject -ne ' true' )
125+ {
126+ return
127+ }
93128
94129 # While the test projects are run individually, passing in the solution name and solution dir via the
95130 # conventional MSBuild properties allows build customization.
@@ -132,7 +167,7 @@ if ($SolutionOrProject -imatch '\.slnx?$')
132167}
133168elseif ($SolutionOrProject -like ' *.csproj' )
134169{
135- Write-Information " Running tests for the `" $SolutionOrProject `' project."
170+ Write-Information " Running tests for the `" $SolutionOrProject `" project."
136171 $tests = @ ($SolutionOrProject )
137172}
138173else
@@ -215,14 +250,17 @@ function Failed($Job, $ProcessId, $Switches, $Test)
215250
216251function StartProcessAndWaitForExit ($Switches , $Test , $Timeout , $ShowTimeRemainingUntilTimeout )
217252{
218- # This is executed in a separate proecess so no variables or settings come through except what's copied over in the
253+ # This is executed in a separate process so no variables or settings come through except what's copied over in the
219254 # "$args" automatic variable. Only Write-Output should be used here, so "Receive-Job" can reliably capture it.
220255 $block = {
221256 Write-Output " StartProcessAndWaitForExitProcessId:$PID "
222257
223258 $argSwitches = $args [0 ]
224259 $argTest = $args [1 ]
225- dotnet test @argSwitches $argTest 2>&1
260+ dotnet test @argSwitches 2>&1
261+
262+ # Use the process exit code, not human-readable runner output, to determine success.
263+ Write-Output " DotnetTestExitCode:$LASTEXITCODE "
226264
227265 if ($LASTEXITCODE -ne 0 )
228266 {
@@ -239,20 +277,16 @@ function StartProcessAndWaitForExit($Switches, $Test, $Timeout, $ShowTimeRemaini
239277 {
240278 Receive-Job $job | Tee-Object - Variable line | Out-Host
241279
242- if (" $line " .StartsWith(' StartProcessAndWaitForExitProcessId:' ))
243- {
244- $processId = [int ](" $line " .Split(' StartProcessAndWaitForExitProcessId:' )[1 ].Split()[0 ])
245- }
246-
247- if (" $line " .Contains(' Test Run Successful.' ))
248- {
249- $hasTestRunSuccessfully = $true
250- }
251-
252- if (" $line " .Contains(' ::error::' ))
280+ foreach ($outputLine in $line )
253281 {
254- $hasTestRunSuccessfully = $false
255- break
282+ if (" $outputLine " -match ' ^StartProcessAndWaitForExitProcessId:(\d+)$' )
283+ {
284+ $processId = [int ]$Matches [1 ]
285+ }
286+ elseif (" $outputLine " -match ' ^DotnetTestExitCode:(\d+)$' )
287+ {
288+ $hasTestRunSuccessfully = [int ]$Matches [1 ] -eq 0
289+ }
256290 }
257291
258292 if ($Timeout -gt 0 )
@@ -278,6 +312,8 @@ function StartProcessAndWaitForExit($Switches, $Test, $Timeout, $ShowTimeRemaini
278312 Failed - Job $job - ProcessId $processId - Switches $Switches - Test $Test
279313 }
280314
315+ Remove-Job $job
316+
281317 return $hasTestRunSuccessfully
282318}
283319
@@ -292,30 +328,76 @@ foreach ($test in $tests)
292328
293329 $switches = @ (
294330 ' --configuration' , $Configuration
295- ' --nologo' ,
296- ' --no-build' ,
297- ' --logger' , ' trx;LogFileName=test-results.trx'
298- # This is for xUnit ITestOutputHelper, see https://xunit.net/docs/capturing-output.
299- ' --logger' , ' console;verbosity=detailed'
331+ ' --no-build'
300332 ' --verbosity' , $Verbosity
301333 )
302334
303- if ($BlameHangTimeout )
335+ if ($useMtp )
304336 {
305- $switches += (' --blame-hang-timeout' , $BlameHangTimeout , ' --blame-hang-dump-type' , ' full' )
306- }
337+ $switches += @ (
338+ ' --project' , $test
339+ ' --output' , ' Detailed'
340+ )
307341
308- if ($Filter )
342+ if ($EnableDiagnosticMode )
343+ {
344+ $switches += (' --diagnostic-output-directory' , (Join-Path (Get-Location ) ' DiagnosticLogs' ))
345+ }
346+
347+ $switches += @ (
348+ ' --'
349+ ' --report-trx'
350+ ' --report-gh'
351+ # UI tests already emit per-test groups; GitHub Actions doesn't support nested groups.
352+ ' --report-gh-groups' , ' off'
353+ ' --show-stdout' , ' all'
354+ ' --show-stderr' , ' all'
355+ )
356+
357+ # A solution can contain empty test projects, or a filter can select no tests in some of its projects.
358+ # Explicit project runs still fail when no tests run. Other failures always retain their exit code.
359+ if ($SolutionOrProject -imatch ' \.slnx?$' )
360+ {
361+ $switches += (' --ignore-exit-code' , ' 8' )
362+ }
363+
364+ if ($BlameHangTimeout )
365+ {
366+ $switches += (' --hangdump' , ' --hangdump-timeout' , $BlameHangTimeout , ' --hangdump-type' , ' Full' )
367+ $switches += (' --hangdump-filename' , ' {asm}_{tfm}_{pid}_hangdump.dmp' )
368+ }
369+
370+ if ($EnableDiagnosticMode )
371+ {
372+ $switches += ' --diagnostic'
373+ }
374+ }
375+ else
309376 {
310- $switches += (' --filter' , " $Filter " )
377+ $switches += @ (
378+ $test
379+ ' --nologo'
380+ ' --logger' , ' trx;LogFileName=test-results.trx'
381+ ' --logger' , ' console;verbosity=detailed'
382+ )
383+
384+ if ($BlameHangTimeout )
385+ {
386+ $switches += (' --blame-hang-timeout' , $BlameHangTimeout , ' --blame-hang-dump-type' , ' full' )
387+ }
388+
389+ if ($EnableDiagnosticMode )
390+ {
391+ $switches += (' --diag' , ' DiagnosticLogs/dotnet-test.log' )
392+ }
311393 }
312394
313- if ($EnableDiagnosticMode )
395+ if ($Filter )
314396 {
315- $switches += (' --diag ' , ' DiagnosticLogs/dotnet-test.log ' )
397+ $switches += (' --filter ' , $Filter )
316398 }
317399
318- Write-Information " Starting testing with `` dotnet test $switches $test `` ."
400+ Write-Information " Starting testing with `` dotnet test $switches `` ."
319401
320402 $success = StartProcessAndWaitForExit - Switches $switches - Test $test - Timeout $TestProcessTimeout - ShowTimeRemainingUntilTimeout $ShowTimeRemainingUntilTimeout
321403
0 commit comments