Tests/ThreadJob.Tests.ps1
# Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. # Helper function to wait for job to reach a running or completed state # Job state can go to "Running" before the underlying runspace thread is running # so we always first wait 100 mSec before checking state. function Wait-ForJobRunning { param ( $job ) $iteration = 10 Do { Start-Sleep -Milliseconds 100 } Until (($job.State -match "Running|Completed|Failed") -or (--$iteration -eq 0)) if ($job.State -notmatch "Running|Completed|Failed") { throw ("Cannot start job '{0}'. Job state is '{1}'" -f $job,$job.State) } } Describe 'Basic ThreadJob Tests' -Tags 'CI' { BeforeAll { $scriptFilePath1 = Join-Path $testdrive "TestThreadJobFile1.ps1" @' for ($i=0; $i -lt 10; $i++) { Write-Output "Hello $i" } '@ > $scriptFilePath1 $scriptFilePath2 = Join-Path $testdrive "TestThreadJobFile2.ps1" @' param ($arg1, $arg2) Write-Output $arg1 Write-Output $arg2 '@ > $scriptFilePath2 $scriptFilePath3 = Join-Path $testdrive "TestThreadJobFile3.ps1" @' $input | foreach { Write-Output $_ } '@ > $scriptFilePath3 $scriptFilePath4 = Join-Path $testdrive "TestThreadJobFile4.ps1" @' Write-Output $using:Var1 Write-Output $($using:Array1)[2] Write-Output @(,$using:Array1) '@ > $scriptFilePath4 $scriptFilePath5 = Join-Path $testdrive "TestThreadJobFile5.ps1" @' param ([string]$param1) Write-Output "$param1 $using:Var1 $using:Var2" '@ > $scriptFilePath5 $WaitForCountFnScript = @' function Wait-ForExpectedRSCount { param ( $expectedRSCount ) $iteration = 20 while ((@(Get-Runspace).Count -ne $expectedRSCount) -and ($iteration-- -gt 0)) { Start-Sleep -Milliseconds 100 } } '@ } AfterEach { Get-Job | Where-Object PSJobTypeName -eq "ThreadJob" | Remove-Job -Force } It 'ThreadJob with ScriptBlock' { $job = Start-ThreadJob -ScriptBlock { "Hello" } $results = $job | Receive-Job -Wait $results | Should -Be "Hello" } It 'ThreadJob with ScriptBlock and Initialization script' { $job = Start-ThreadJob -ScriptBlock { "Goodbye" } -InitializationScript { "Hello" } $results = $job | Receive-Job -Wait $results[0] | Should -Be "Hello" $results[1] | Should -Be "Goodbye" } It 'ThreadJob with ScriptBlock and Argument list' { $job = Start-ThreadJob -ScriptBlock { param ($arg1, $arg2) $arg1; $arg2 } -ArgumentList @("Hello","Goodbye") $results = $job | Receive-Job -Wait $results[0] | Should -Be "Hello" $results[1] | Should -Be "Goodbye" } It 'ThreadJob with ScriptBlock and piped input' { $job = "Hello","Goodbye" | Start-ThreadJob -ScriptBlock { $input | ForEach-Object { $_ } } $results = $job | Receive-Job -Wait $results[0] | Should -Be "Hello" $results[1] | Should -Be "Goodbye" } It 'ThreadJob with ScriptBlock and Using variables' { $Var1 = "Hello" $Var2 = "Goodbye" $Var3 = 102 $Var4 = 1..5 $global:GVar1 = "GlobalVar" $job = Start-ThreadJob -ScriptBlock { Write-Output $using:Var1 Write-Output $using:Var2 Write-Output $using:Var3 Write-Output ($using:Var4)[1] Write-Output @(,$using:Var4) Write-Output $using:GVar1 } $results = $job | Receive-Job -Wait $results[0] | Should -Be $Var1 $results[1] | Should -Be $Var2 $results[2] | Should -Be $Var3 $results[3] | Should -Be 2 $results[4] | Should -Be $Var4 $results[5] | Should -Be $global:GVar1 } It 'ThreadJob with ScriptBlock and Using variables and argument list' { $Var1 = "Hello" $Var2 = 52 $job = Start-ThreadJob -ScriptBlock { param ([string] $param1) "$using:Var1 $param1 $using:Var2" } -ArgumentList "There" $results = $job | Receive-Job -Wait $results | Should -Be "Hello There 52" } It 'ThreadJob with ScriptFile' { $job = Start-ThreadJob -FilePath $scriptFilePath1 $results = $job | Receive-Job -Wait $results | Should -HaveCount 10 $results[9] | Should -Be "Hello 9" } It 'ThreadJob with ScriptFile and Initialization script' { $job = Start-ThreadJob -FilePath $scriptFilePath1 -Initialization { "Goodbye" } $results = $job | Receive-Job -Wait $results | Should -HaveCount 11 $results[0] | Should -Be "Goodbye" } It 'ThreadJob with ScriptFile and Argument list' { $job = Start-ThreadJob -FilePath $scriptFilePath2 -ArgumentList @("Hello","Goodbye") $results = $job | Receive-Job -Wait $results[0] | Should -Be "Hello" $results[1] | Should -Be "Goodbye" } It 'ThreadJob with ScriptFile and piped input' { $job = "Hello","Goodbye" | Start-ThreadJob -FilePath $scriptFilePath3 $results = $job | Receive-Job -Wait $results[0] | Should -Be "Hello" $results[1] | Should -Be "Goodbye" } It 'ThreadJob with ScriptFile and Using variables' { $Var1 = "Hello!" $Array1 = 1..10 $job = Start-ThreadJob -FilePath $scriptFilePath4 $results = $job | Receive-Job -Wait $results[0] | Should -Be $Var1 $results[1] | Should -Be 3 $results[2] | Should -Be $Array1 } It 'ThreadJob with ScriptFile and Using variables with argument list' { $Var1 = "There" $Var2 = 60 $job = Start-ThreadJob -FilePath $scriptFilePath5 -ArgumentList "Hello" $results = $job | Receive-Job -Wait $results | Should -Be "Hello There 60" } It 'ThreadJob with terminating error' { $job = Start-ThreadJob -ScriptBlock { throw "MyError!" } $job | Wait-Job $job.JobStateInfo.Reason.Message | Should -Be "MyError!" } It 'ThreadJob and Error stream output' { $job = Start-ThreadJob -ScriptBlock { Write-Error "ErrorOut" } | Wait-Job $job.Error | Should -Be "ErrorOut" } It 'ThreadJob and Warning stream output' { $job = Start-ThreadJob -ScriptBlock { Write-Warning "WarningOut" } | Wait-Job $job.Warning | Should -Be "WarningOut" } It 'ThreadJob and Verbose stream output' { $job = Start-ThreadJob -ScriptBlock { $VerbosePreference = 'Continue'; Write-Verbose "VerboseOut" } | Wait-Job $job.Verbose | Should Match "VerboseOut" } It 'ThreadJob and Verbose stream output' { $job = Start-ThreadJob -ScriptBlock { $DebugPreference = 'Continue'; Write-Debug "DebugOut" } | Wait-Job $job.Debug | Should -Be "DebugOut" } It 'ThreadJob and Information stream output' { $job = Start-ThreadJob -ScriptBlock { Write-Information "InformationOutput" -InformationAction Continue } | Wait-Job $job.Information | Should -Be "InformationOutput" } It 'ThreadJob and Host stream output' { # Host stream data is automatically added to the Information stream $job = Start-ThreadJob -ScriptBlock { Write-Host "HostOutput" } | Wait-Job $job.Information | Should -Be "HostOutput" } It 'ThreadJob ThrottleLimit and Queue' { try { # Start four thread jobs with ThrottleLimit set to two Get-Job | Where-Object PSJobTypeName -eq "ThreadJob" | Remove-Job -Force $job1 = Start-ThreadJob -ScriptBlock { Start-Sleep -Seconds 60 } -ThrottleLimit 2 $job2 = Start-ThreadJob -ScriptBlock { Start-Sleep -Seconds 60 } $job3 = Start-ThreadJob -ScriptBlock { Start-Sleep -Seconds 60 } $job4 = Start-ThreadJob -ScriptBlock { Start-Sleep -Seconds 60 } # Allow jobs to start Wait-ForJobRunning $job2 Get-Job | Where-Object { ($_.PSJobTypeName -eq "ThreadJob") -and ($_.State -eq "Running") } | Should -HaveCount 2 Get-Job | Where-Object { ($_.PSJobTypeName -eq "ThreadJob") -and ($_.State -eq "NotStarted") } | Should -HaveCount 2 } finally { Get-Job | Where-Object PSJobTypeName -eq "ThreadJob" | Remove-Job -Force } Get-Job | Where-Object PSJobTypeName -eq "ThreadJob" | Should -HaveCount 0 } It 'ThreadJob Runspaces should be cleaned up at completion' { $script = $WaitForCountFnScript + @' try { Get-Job | Where-Object PSJobTypeName -eq "ThreadJob" | Remove-Job -Force $rsStartCount = @(Get-Runspace).Count # Start four thread jobs with ThrottleLimit set to two $Job1 = Start-ThreadJob -ScriptBlock { "Hello 1!" } -ThrottleLimit 5 $job2 = Start-ThreadJob -ScriptBlock { "Hello 2!" } $job3 = Start-ThreadJob -ScriptBlock { "Hello 3!" } $job4 = Start-ThreadJob -ScriptBlock { "Hello 4!" } $null = $job1,$job2,$job3,$job4 | Wait-Job # Allow for runspace clean up to happen Wait-ForExpectedRSCount $rsStartCount Write-Output (@(Get-Runspace).Count -eq $rsStartCount) } finally { Get-Job | Where-Object PSJobTypeName -eq "ThreadJob" | Remove-Job -Force } '@ $result = & "$PSHOME/pwsh" -c $script $result | Should -BeExactly "True" } It 'ThreadJob Runspaces should be cleaned up after job removal' { $script = $WaitForCountFnScript + @' try { Get-Job | Where-Object PSJobTypeName -eq "ThreadJob" | Remove-Job -Force $rsStartCount = @(Get-Runspace).Count # Start four thread jobs with ThrottleLimit set to two $Job1 = Start-ThreadJob -ScriptBlock { Start-Sleep -Seconds 60 } -ThrottleLimit 2 $job2 = Start-ThreadJob -ScriptBlock { Start-Sleep -Seconds 60 } $job3 = Start-ThreadJob -ScriptBlock { Start-Sleep -Seconds 60 } $job4 = Start-ThreadJob -ScriptBlock { Start-Sleep -Seconds 60 } Wait-ForExpectedRSCount ($rsStartCount + 4) Write-Output (@(Get-Runspace).Count -eq ($rsStartCount + 4)) # Stop two jobs $job1 | Remove-Job -Force $job3 | Remove-Job -Force Wait-ForExpectedRSCount ($rsStartCount + 2) Write-Output (@(Get-Runspace).Count -eq ($rsStartCount + 2)) } finally { Get-Job | Where-Object PSJobTypeName -eq "ThreadJob" | Remove-Job -Force } Wait-ForExpectedRSCount $rsStartCount Write-Output (@(Get-Runspace).Count -eq $rsStartCount) '@ $result = & "$PSHOME/pwsh" -c $script $result | Should -BeExactly "True","True","True" } It 'ThreadJob jobs should work with Receive-Job -AutoRemoveJob' { Get-Job | Where-Object PSJobTypeName -eq "ThreadJob" | Remove-Job -Force $job1 = Start-ThreadJob -ScriptBlock { 1..2 | ForEach-Object { Start-Sleep -Milliseconds 100; "Output $_" } } -ThrottleLimit 5 $job2 = Start-ThreadJob -ScriptBlock { 1..2 | ForEach-Object { Start-Sleep -Milliseconds 100; "Output $_" } } $job3 = Start-ThreadJob -ScriptBlock { 1..2 | ForEach-Object { Start-Sleep -Milliseconds 100; "Output $_" } } $job4 = Start-ThreadJob -ScriptBlock { 1..2 | ForEach-Object { Start-Sleep -Milliseconds 100; "Output $_" } } $null = $job1,$job2,$job3,$job4 | Receive-Job -Wait -AutoRemoveJob Get-Job | Where-Object PSJobTypeName -eq "ThreadJob" | Should -HaveCount 0 } It 'ThreadJob jobs should run in FullLanguage mode by default' { $result = Start-ThreadJob -ScriptBlock { $ExecutionContext.SessionState.LanguageMode } | Wait-Job | Receive-Job $result | Should -Be "FullLanguage" } } Describe 'Job2 class API tests' -Tags 'CI' { AfterEach { Get-Job | Where-Object PSJobTypeName -eq "ThreadJob" | Remove-Job -Force } It 'Verifies StopJob API' { $job = Start-ThreadJob -ScriptBlock { Start-Sleep -Seconds 60 } -ThrottleLimit 5 Wait-ForJobRunning $job $job.StopJob($true, "No Reason") $job.JobStateInfo.State | Should -Be "Stopped" } It 'Verifies StopJobAsync API' { $job = Start-ThreadJob -ScriptBlock { Start-Sleep -Seconds 60 } -ThrottleLimit 5 Wait-ForJobRunning $job $job.StopJobAsync($true, "No Reason") Wait-Job $job $job.JobStateInfo.State | Should -Be "Stopped" } It 'Verifies StartJobAsync API' { $jobRunning = Start-ThreadJob -ScriptBlock { Start-Sleep -Seconds 60 } -ThrottleLimit 1 $jobNotRunning = Start-ThreadJob -ScriptBlock { Start-Sleep -Seconds 60 } $jobNotRunning.JobStateInfo.State | Should -Be "NotStarted" # StartJobAsync starts jobs synchronously for ThreadJob jobs $jobNotRunning.StartJobAsync() Wait-ForJobRunning $jobNotRunning $jobNotRunning.JobStateInfo.State | Should -Be "Running" } It 'Verifies JobSourceAdapter Get-Jobs' { $job = Start-ThreadJob -ScriptBlock { "Hello" } | Wait-Job $getJob = Get-Job -InstanceId $job.InstanceId 2> $null $getJob | Should -Be $job $getJob = Get-Job -Name $job.Name 2> $null $getJob | Should -Be $job $getJob = Get-Job -Command ' "hello" ' 2> $null $getJob | Should -Be $job $getJob = Get-Job -State $job.JobStateInfo.State 2> $null $getJob | Should -Be $job $getJob = Get-Job -Id $job.Id 2> $null $getJob | Should -Be $job # Get-Job -Filter is not supported $result = Get-Job -Filter @{Id = ($job.Id)} 3> $null $result | Should -BeNullOrEmpty } It 'Verifies terminating job error' { $job = Start-ThreadJob -ScriptBlock { throw "My Job Error!" } | Wait-Job $results = $job | Receive-Job 2>&1 $results.ToString() | Should -Be "My Job Error!" } } |