|
1 |
| -$moduleName = (Get-Item ([IO.Path]::Combine($PSScriptRoot, '..', 'module', '*.psd1'))).BaseName |
2 |
| -$manifestPath = [IO.Path]::Combine($PSScriptRoot, '..', 'output', $moduleName) |
| 1 | +using namespace System.IO |
| 2 | +using namespace System.Management.Automation |
| 3 | +using namespace System.Diagnostics |
| 4 | +using namespace System.Collections.Concurrent |
| 5 | + |
| 6 | +$moduleName = (Get-Item ([Path]::Combine($PSScriptRoot, '..', 'module', '*.psd1'))).BaseName |
| 7 | +$manifestPath = [Path]::Combine($PSScriptRoot, '..', 'output', $moduleName) |
3 | 8 |
|
4 | 9 | Import-Module $manifestPath
|
5 |
| -Import-Module ([System.IO.Path]::Combine($PSScriptRoot, 'common.psm1')) |
| 10 | +Import-Module ([Path]::Combine($PSScriptRoot, 'common.psm1')) |
6 | 11 |
|
7 | 12 | Describe PSParallelPipeline {
|
8 |
| - Context 'Invoke-Parallel' -Tag 'Invoke-Parallel' { |
9 |
| - It 'Should process all pipeline input' { |
10 |
| - { 0..10 | Invoke-Parallel { $_ } } | |
11 |
| - Should -Not -Throw |
| 13 | + Context 'Output Streams' { |
| 14 | + It 'Success' { |
| 15 | + 1 | Invoke-Parallel { $_ } | Should -BeOfType ([int]) |
| 16 | + 1 | Invoke-Parallel { $_ } | Should -BeExactly 1 |
| 17 | + } |
12 | 18 |
|
13 |
| - $items = 0..10 | Invoke-Parallel { $_ } | |
14 |
| - Sort-Object |
| 19 | + It 'Error' { |
| 20 | + 1 | Invoke-Parallel { Write-Error $_ } 2>&1 | |
| 21 | + Should -BeOfType ([ErrorRecord]) |
| 22 | + } |
15 | 23 |
|
16 |
| - $items | Should -BeExactly (0..10) |
17 |
| - $items | Should -HaveCount 11 |
| 24 | + It 'Warning' { |
| 25 | + 1 | Invoke-Parallel { Write-Warning $_ } 3>&1 | |
| 26 | + Should -BeOfType ([WarningRecord]) |
18 | 27 | }
|
19 | 28 |
|
20 |
| - It 'Should process in parallel' { |
21 |
| - $timer = [System.Diagnostics.Stopwatch]::StartNew() |
22 |
| - 0..5 | Invoke-Parallel { Start-Sleep 1 } -ThrottleLimit 5 |
23 |
| - $timer.Elapsed | Should -BeLessOrEqual ([timespan]::FromSeconds(5)) |
24 |
| - $timer.Stop() |
| 29 | + It 'Verbose' { |
| 30 | + 1 | Invoke-Parallel { Write-Verbose $_ -Verbose } -Verbose 4>&1 | |
| 31 | + Should -BeOfType ([VerboseRecord]) |
25 | 32 | }
|
26 | 33 |
|
27 |
| - It 'Should stop processing after a set timeout' { |
28 |
| - $timer = [System.Diagnostics.Stopwatch]::StartNew() |
| 34 | + It 'Debug' { |
| 35 | + if ($IsCoreCLR) { |
| 36 | + 1 | Invoke-Parallel { Write-Debug $_ -Debug } -Debug 5>&1 | |
| 37 | + Should -BeOfType ([DebugRecord]) |
| 38 | + return |
| 39 | + } |
29 | 40 |
|
30 |
| - { 0..5 | Invoke-Parallel { Start-Sleep 10 } -TimeoutSeconds 2 -ErrorAction Stop } | |
31 |
| - Should -Throw -ExceptionType ([System.TimeoutException]) |
| 41 | + # Debug is weird in PowerShell 5.1. Needs a different test. |
| 42 | + $DebugPreference = 'Continue' |
| 43 | + 1 | Invoke-Parallel { & { [CmdletBinding()]param() $PSCmdlet.WriteDebug(123) } -Debug } 5>&1 | |
| 44 | + Should -BeOfType ([DebugRecord]) |
| 45 | + } |
32 | 46 |
|
33 |
| - $timer.Elapsed | Should -BeLessOrEqual ([timespan]::FromSeconds(4)) |
34 |
| - $timer.Stop() |
| 47 | + It 'Information' { |
| 48 | + 1 | Invoke-Parallel { Write-Host $_ } 6>&1 | |
| 49 | + Should -BeOfType ([InformationRecord]) |
35 | 50 | }
|
36 | 51 |
|
37 |
| - It 'Allows $using: statements' { |
38 |
| - $message = 'Hello world from {0:D2}' |
39 |
| - $items = 0..10 | Invoke-Parallel { $using:message -f $_ } | |
40 |
| - Sort-Object |
| 52 | + It 'Progress' { |
| 53 | + $ProgressPreference = 'SilentlyContinue' |
41 | 54 |
|
42 |
| - $items | Should -BeExactly @( |
43 |
| - 0..10 | ForEach-Object { 'Hello world from {0:D2}' -f $_ } |
44 |
| - ) |
| 55 | + $null | Invoke-Parallel { |
| 56 | + 1..10 | ForEach-Object { |
| 57 | + Write-Progress -Activity 'Progress Output' -PercentComplete (10 * $_) |
| 58 | + Start-Sleep -Milliseconds 200 |
| 59 | + } |
| 60 | + Write-Progress -Completed -Activity 'Progress Output' |
| 61 | + } | Should -BeNullOrEmpty |
45 | 62 | }
|
| 63 | + } |
46 | 64 |
|
47 |
| - It 'Can make variables available through the -Variables parameter' { |
48 |
| - $invokeParallelSplat = @{ |
49 |
| - Variables = @{ message = 'Hello world from {0:D2}' } |
50 |
| - ScriptBlock = { $message -f $_ } |
51 |
| - } |
| 65 | + Context 'Common Parameters' { |
| 66 | + It 'Supports ActionPreference' { |
| 67 | + { 1 | Invoke-Parallel { Write-Error $_ } -ErrorAction Stop } | |
| 68 | + Should -Throw |
52 | 69 |
|
53 |
| - $items = 0..10 | Invoke-Parallel @invokeParallelSplat | |
54 |
| - Sort-Object |
| 70 | + { 1 | Invoke-Parallel { Write-Warning $_ } -WarningAction Stop 3>$null } | |
| 71 | + Should -Throw |
55 | 72 |
|
56 |
| - $items | Should -BeExactly @( |
57 |
| - 0..10 | ForEach-Object { 'Hello world from {0:D2}' -f $_ } |
58 |
| - ) |
| 73 | + { 1 | Invoke-Parallel { Write-Host $_ } -InformationAction Stop 6>$null } | |
| 74 | + Should -Throw |
| 75 | + |
| 76 | + 1 | Invoke-Parallel { Write-Error $_ } -ErrorAction Ignore 2>&1 | |
| 77 | + Should -BeNullOrEmpty |
| 78 | + |
| 79 | + 1 | Invoke-Parallel { Write-Warning $_ } -WarningAction Ignore 2>&1 | |
| 80 | + Should -BeNullOrEmpty |
| 81 | + |
| 82 | + 1 | Invoke-Parallel { Write-Host $_ } -InformationAction Ignore 2>&1 | |
| 83 | + Should -BeNullOrEmpty |
59 | 84 | }
|
60 | 85 |
|
| 86 | + It 'Supports PipelineVariable' { |
| 87 | + 1 | Invoke-Parallel { $_ } -PipelineVariable pipe | |
| 88 | + ForEach-Object { Get-Variable pipe -ValueOnly } | |
| 89 | + Should -BeExactly 1 |
| 90 | + } |
| 91 | + } |
| 92 | + |
| 93 | + Context 'UseNewRunspace Parameter' { |
61 | 94 | It 'Should reuse runspaces by default' {
|
62 |
| - 0..10 | Invoke-Parallel { [runspace]::DefaultRunspace.InstanceId } -ThrottleLimit 5 | |
63 |
| - Select-Object -Unique | |
| 95 | + 0..10 | Invoke-Parallel { [runspace]::DefaultRunspace } | |
| 96 | + Select-Object -ExpandProperty InstanceId -Unique | |
64 | 97 | Should -HaveCount 5
|
65 | 98 | }
|
66 | 99 |
|
67 |
| - It 'Should use a new runspace when the -UseNewRunspace parameter is used' { |
68 |
| - 0..10 | Invoke-Parallel { [runspace]::DefaultRunspace.InstanceId } -UseNewRunspace | |
69 |
| - Select-Object -Unique | |
| 100 | + It 'Should use a new runspace when the -UseNewRunspace is used' { |
| 101 | + 0..10 | Invoke-Parallel { [runspace]::DefaultRunspace } -UseNewRunspace | |
| 102 | + Select-Object -ExpandProperty InstanceId -Unique | |
70 | 103 | Should -HaveCount 11
|
71 | 104 | }
|
| 105 | + } |
72 | 106 |
|
73 |
| - It 'Can add items to a single thread instance' { |
74 |
| - $dict = [System.Collections.Concurrent.ConcurrentDictionary[string, object]]::new() |
75 |
| - |
76 |
| - Get-Process | Invoke-Parallel { ($using:dict).TryAdd($_.Id, $_) } | |
77 |
| - Should -Contain $true |
| 107 | + Context 'Variables Parameter' { |
| 108 | + It 'Makes variables available in the parallel scope' { |
| 109 | + $items = 0..10 | Invoke-Parallel { $message -f $_ } -Variables @{ |
| 110 | + message = 'Hello world from {0:D2}' |
| 111 | + } | Sort-Object |
78 | 112 |
|
79 |
| - $dict[$PID].ProcessName | Should -Be (Get-Process -Id $PID).ProcessName |
| 113 | + $shouldBe = 0..10 | ForEach-Object { 'Hello world from {0:D2}' -f $_ } |
| 114 | + $items | Should -BeExactly $shouldBe |
80 | 115 | }
|
| 116 | + } |
81 | 117 |
|
82 |
| - It 'Should add functions to the parallel scope with -Functions parameter' { |
83 |
| - $invokeParallelSplat = @{ |
84 |
| - Functions = 'Test-Function' |
85 |
| - ScriptBlock = { Test-Function $_ } |
86 |
| - } |
87 |
| - |
88 |
| - 0..10 | Invoke-Parallel @invokeParallelSplat | |
| 118 | + Context 'Functions Parameter' { |
| 119 | + It 'Makes functions available in the parallel scope' { |
| 120 | + 0..10 | Invoke-Parallel { Test-Function $_ } -Functions Test-Function | |
89 | 121 | Sort-Object |
|
90 | 122 | Should -BeExactly @(0..10 | ForEach-Object { Test-Function $_ })
|
91 | 123 | }
|
| 124 | + } |
92 | 125 |
|
93 |
| - It 'Should autocomplete existing commands in the caller scope' { |
94 |
| - $result = TabExpansion2 -inputScript ($s = 'Invoke-Parallel -Function Get-') -cursorColumn $s.Length |
95 |
| - $result.CompletionMatches.Count | Should -BeGreaterThan 0 |
96 |
| - $result.CompletionMatches.ListItemText | Should -Match '^Get-' |
97 |
| - } |
| 126 | + Context 'ThrottleLimit Parameter' { |
| 127 | + It 'Defines the degree of parallelism' { |
| 128 | + Measure-Command { |
| 129 | + 0..10 | Invoke-Parallel { Start-Sleep 1 } |
| 130 | + } | ForEach-Object TotalSeconds | Should -BeGreaterOrEqual 3 |
98 | 131 |
|
99 |
| - It 'Should throw a terminating error' { |
100 |
| - { $null | Invoke-Parallel { Write-Error 'Error' } -ErrorAction Stop } | |
101 |
| - Should -Throw |
| 132 | + Measure-Command { |
| 133 | + 0..10 | Invoke-Parallel { Start-Sleep 1 } -ThrottleLimit 11 |
| 134 | + } | ForEach-Object TotalSeconds | Should -BeLessOrEqual 1.5 |
102 | 135 | }
|
| 136 | + } |
103 | 137 |
|
104 |
| - It 'Should write to the Error Stream' { |
105 |
| - $null | Invoke-Parallel { Write-Error 'Error' } 2>&1 | |
106 |
| - Should -BeOfType ([System.Management.Automation.ErrorRecord]) |
| 138 | + Context 'TimeoutSeconds Parameter' { |
| 139 | + It 'Stops processing after the specified seconds' { |
| 140 | + $timer = [Stopwatch]::StartNew() |
| 141 | + { 0..5 | Invoke-Parallel { Start-Sleep 10 } -TimeoutSeconds 2 -ErrorAction Stop } | |
| 142 | + Should -Throw -ExceptionType ([TimeoutException]) |
| 143 | + $timer.Stop() |
| 144 | + $timer.Elapsed | Should -BeLessOrEqual ([timespan]::FromSeconds(2.1)) |
107 | 145 | }
|
| 146 | + } |
| 147 | + |
| 148 | + Context 'CommandCompleter' { |
| 149 | + It 'Should autocomplete existing commands in the caller scope' { |
| 150 | + Complete 'Invoke-Parallel -Functions Compl' | |
| 151 | + Should -Not -BeNullOrEmpty |
108 | 152 |
|
109 |
| - It 'Should throw if passing a scriptblock with using: scope modifier' { |
110 |
| - { |
111 |
| - $sb = { } |
112 |
| - 1..1 | Invoke-Parallel { $using:sb } |
113 |
| - } | Should -Throw |
| 153 | + Complete 'Invoke-Parallel -Functions Compl' | |
| 154 | + ForEach-Object ListItemText | |
| 155 | + Should -Contain 'Complete' |
114 | 156 | }
|
| 157 | + } |
115 | 158 |
|
116 |
| - It 'Should throw if passing a scriptblock with the -Variables parameter' { |
117 |
| - { |
118 |
| - $sb = { } |
119 |
| - 1..1 | Invoke-Parallel { $sb } -Variables @{ sb = $sb } |
120 |
| - } | Should -Throw |
| 159 | + Context '$using: keyword Support' { |
| 160 | + It 'Allows passed-in variables through $using: keyword' { |
| 161 | + $message = 'Hello world from {0:D2}' |
| 162 | + $items = 0..10 | Invoke-Parallel { $using:message -f $_ } | |
| 163 | + Sort-Object |
| 164 | + |
| 165 | + $shouldBe = 0..10 | ForEach-Object { 'Hello world from {0:D2}' -f $_ } |
| 166 | + $items | Should -BeExactly $shouldBe |
121 | 167 | }
|
122 | 168 |
|
123 |
| - It 'Allows indexing on using: statements' { |
| 169 | + It 'Allows indexing on $using: passed-in variables' { |
124 | 170 | $arr = 0..10; $hash = @{ foo = 'bar' }
|
125 | 171 | 1 | Invoke-Parallel { $using:arr[-1] } | Should -BeExactly 10
|
126 | 172 | 1 | Invoke-Parallel { $using:hash['FOO'] } | Should -BeExactly 'bar'
|
127 | 173 | }
|
128 | 174 |
|
129 |
| - It 'Allows member accessing on using: statemets' { |
| 175 | + It 'Allows member access on $using: passed-in variables' { |
130 | 176 | $hash = @{
|
131 | 177 | foo = @{
|
132 | 178 | bar = [pscustomobject]@{ Index = 0..10 }
|
133 | 179 | }
|
134 | 180 | }
|
135 | 181 |
|
136 |
| - 1 | Invoke-Parallel { $using:hash['foo']['bar'].Index[5] } | Should -BeExactly 5 |
| 182 | + 1 | Invoke-Parallel { $using:hash['foo']['bar'].Index[5] } | |
| 183 | + Should -BeExactly 5 |
| 184 | + } |
| 185 | + } |
| 186 | + |
| 187 | + Context 'Script Block Assertions' { |
| 188 | + It 'Should throw on passed-in Script Block via $using: keyword' { |
| 189 | + { $sb = { }; 1..1 | Invoke-Parallel { $using:sb } } | |
| 190 | + Should -Throw -ExceptionType ([PSArgumentException]) |
| 191 | + } |
| 192 | + |
| 193 | + It 'Should throw on passed-in Script Block via -Variables parameter' { |
| 194 | + { $sb = { }; 1..1 | Invoke-Parallel { $sb } -Variables @{ sb = $sb } } | |
| 195 | + Should -Throw -ExceptionType ([PSArgumentException]) |
| 196 | + } |
| 197 | + |
| 198 | + It 'Should throw on passed-in Script Block via input object' { |
| 199 | + { { 1 + 1 } | Invoke-Parallel { & $_ } } | |
| 200 | + Should -Throw -ExceptionType ([PSArgumentException]) |
| 201 | + } |
| 202 | + } |
| 203 | + |
| 204 | + Context 'Invoke-Parallel' { |
| 205 | + It 'Process in parallel' { |
| 206 | + $timer = [Stopwatch]::StartNew() |
| 207 | + 1..5 | Invoke-Parallel { Start-Sleep 1 } |
| 208 | + $timer.Stop() |
| 209 | + $timer.Elapsed | Should -BeLessOrEqual ([timespan]::FromSeconds(1.5)) |
| 210 | + } |
| 211 | + |
| 212 | + It 'Supports streaming output' { |
| 213 | + Measure-Command { |
| 214 | + 0..10 | Invoke-Parallel { |
| 215 | + 0..10 | ForEach-Object { |
| 216 | + Start-Sleep 1 |
| 217 | + $_ |
| 218 | + } |
| 219 | + } | Select-Object -First 5 | |
| 220 | + Should -HaveCount 5 |
| 221 | + } | ForEach-Object TotalSeconds | |
| 222 | + Should -BeLessThan 2 |
| 223 | + |
| 224 | + Measure-Command { |
| 225 | + 0..10 | Invoke-Parallel { Start-Sleep 1; $_ } -ThrottleLimit 2 | |
| 226 | + Select-Object -First 10 | |
| 227 | + Should -HaveCount 10 |
| 228 | + } | ForEach-Object TotalSeconds | |
| 229 | + Should -BeLessThan 6 |
| 230 | + } |
| 231 | + |
| 232 | + It 'Can add items to a single thread instance' { |
| 233 | + $dict = [ConcurrentDictionary[string, object]]::new() |
| 234 | + |
| 235 | + Get-Process | Invoke-Parallel { ($using:dict).TryAdd($_.Id, $_) } | |
| 236 | + Should -Contain $true |
| 237 | + |
| 238 | + $dict[$PID].ProcessName | Should -Be (Get-Process -Id $PID).ProcessName |
137 | 239 | }
|
138 | 240 | }
|
139 | 241 | }
|
0 commit comments