2021-10-11 14:49:09 -07:00
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
Describe 'Function Pipeline Behaviour' -Tag 'CI' {
BeforeAll {
$filePath = " $TestDrive \output.txt"
if ( Test-Path $filePath ) {
Remove-Item $filePath -Force
}
}
Context "'Clean' block runs when any other named blocks run" {
AfterEach {
if ( Test-Path $filePath ) {
Remove-Item $filePath -Force
}
}
It "'Clean' block executes only if at least one of the other named blocks executed" {
## The 'Clean' block is for cleanup purpose. When none of other named blocks execute,
## there is no point to execute the 'Clean' block, so it will be skipped in this case.
function test-1 {
clean { 'clean-redirected-output' > $filePath }
}
function test-2 {
End { 'end' }
clean { 'clean-redirected-output' > $filePath }
}
## The 'Clean' block is skipped.
test - 1 | Should -BeNullOrEmpty
Test-Path -Path $filePath | Should -BeFalse
## The 'Clean' block runs.
test - 2 | Should -BeExactly 'end'
Test-Path -Path $filePath | Should -BeTrue
Get-Content $filePath | Should -BeExactly 'clean-redirected-output'
}
It "'Clean' block is skipped when the command doesn't run due to no input from upstream command" {
function test-1 ([ switch ] $WriteOutput ) {
Process {
if ( $WriteOutput ) {
Write-Output 'process'
} else {
Write-Verbose -Verbose 'process'
}
}
}
function test-2 {
Process { Write-Output "test-2: $_ " }
clean { Write-Warning 'test-2-clean-warning' }
}
## No output from 'test-1.Process', so 'test-2.Process' didn't run, and thus 'test-2.Clean' was skipped.
test - 1 | test - 2 *>& 1 | Should -BeNullOrEmpty
## Output from 'test-1.Process' would trigger 'test-2.Process' to run, and thus 'test-2.Clean' would run.
$output = test - 1 -WriteOutput | test - 2 *>& 1
$output | Should -Be @ ( 'test-2: process' , 'test-2-clean-warning' )
}
It "'Clean' block is skipped when the command doesn't run due to terminating error from upstream Process block" {
function test-1 ([ switch ] $ThrowException ) {
Process {
if ( $ThrowException ) {
throw 'process'
} else {
Write-Output 'process'
}
}
}
function test-2 {
Process { Write-Output "test-2: $_ " }
clean { 'clean-redirected-output' > $filePath }
}
$failure = $null
try { test - 1 -ThrowException | test - 2 } catch { $failure = $_ }
$failure | Should -Not -BeNullOrEmpty
$failure . Exception . Message | Should -BeExactly 'process'
## 'test-2' didn't run because 'test-1' throws terminating exception, so 'test-2.Clean' didn't run either.
Test-Path -Path $filePath | Should -BeFalse
test - 1 | test - 2 | Should -BeExactly 'test-2: process'
Test-Path -Path $filePath | Should -BeTrue
Get-Content $filePath | Should -BeExactly 'clean-redirected-output'
}
It "'Clean' block is skipped when the command doesn't run due to terminating error from upstream Begin block" {
function test-1 {
Begin { throw 'begin' }
End { 'end' }
}
function test-2 {
Begin { 'begin' }
Process { Write-Output "test-2: $_ " }
clean { 'clean-redirected-output' > $filePath }
}
$failure = $null
try { test - 1 | test - 2 } catch { $failure = $_ }
$failure | Should -Not -BeNullOrEmpty
$failure . Exception . Message | Should -BeExactly 'begin'
## 'test-2' didn't run because 'test-1' throws terminating exception, so 'test-2.Clean' didn't run either.
Test-Path -Path $filePath | Should -BeFalse
}
It "'Clean' block runs when '<BlockName>' runs" -TestCases @ (
@ { Script = { [ CmdletBinding ()] param () begin { 'output' } clean { Write-Warning 'clean-warning' } }; BlockName = 'Begin' }
@ { Script = { [ CmdletBinding ()] param () process { 'output' } clean { Write-Warning 'clean-warning' } }; BlockName = 'Process' }
@ { Script = { [ CmdletBinding ()] param () end { 'output' } clean { Write-Warning 'clean-warning' } }; BlockName = 'End' }
) {
param ( $Script , $BlockName )
& $Script -WarningVariable wv | Should -BeExactly 'output'
$wv | Should -BeExactly 'clean-warning'
}
It "'Clean' block runs when '<BlockName>' throws terminating error" -TestCases @ (
@ { Script = { [ CmdletBinding ()] param () begin { throw 'failure' } clean { Write-Warning 'clean-warning' } }; BlockName = 'Begin' }
@ { Script = { [ CmdletBinding ()] param () process { throw 'failure' } clean { Write-Warning 'clean-warning' } }; BlockName = 'Process' }
@ { Script = { [ CmdletBinding ()] param () end { throw 'failure' } clean { Write-Warning 'clean-warning' } }; BlockName = 'End' }
) {
param ( $Script , $BlockName )
$failure = $null
try { & $Script -WarningVariable wv } catch { $failure = $_ }
$failure | Should -Not -BeNullOrEmpty
$failure . Exception . Message | Should -BeExactly 'failure'
$wv | Should -BeExactly 'clean-warning'
}
It "'Clean' block runs in pipeline - simple function" {
function test-1 {
param ([ switch ] $EmitError )
process {
if ( $EmitError ) {
throw 'test-1-process-error'
} else {
Write-Output 'test-1'
}
}
clean { 'test-1-clean' >> $filePath }
}
function test-2 {
begin { Write-Verbose -Verbose 'test-2-begin' }
process { $_ }
clean { 'test-2-clean' >> $filePath }
}
function test-3 {
end { Write-Verbose -Verbose 'test-3-end' }
clean { 'test-3-clean' >> $filePath }
}
## All command will run, so all 'Clean' blocks will run
test - 1 | test - 2 | test - 3
Test-Path $filePath | Should -BeTrue
$content = Get-Content $filePath
$content | Should -Be @ ( 'test-1-clean' , 'test-2-clean' , 'test-3-clean' )
$failure = $null
Remove-Item $filePath -Force
try {
test - 1 -EmitError | test - 2 | test - 3
} catch {
$failure = $_
}
## Exception is thrown from 'test-1.Process'. By that time, the 'test-2.Begin' has run,
## so 'test-2.Clean' will run. However, 'test-3.End' won't run, so 'test-3.Clean' won't run.
$failure | Should -Not -BeNullOrEmpty
$failure . Exception . Message | Should -BeExactly 'test-1-process-error'
Test-Path $filePath | Should -BeTrue
$content = Get-Content $filePath
$content | Should -Be @ ( 'test-1-clean' , 'test-2-clean' )
}
It "'Clean' block runs in pipeline - advanced function" {
function test-1 {
[ CmdletBinding ()]
param ([ switch ] $EmitError )
process {
if ( $EmitError ) {
throw 'test-1-process-error'
} else {
Write-Output 'test-1'
}
}
clean { 'test-1-clean' >> $filePath }
}
function test-2 {
[ CmdletBinding ()]
param (
[ Parameter ( ValueFromPipeline )]
$pipeInput
)
begin { Write-Verbose -Verbose 'test-2-begin' }
process { $pipeInput }
clean { 'test-2-clean' >> $filePath }
}
function test-3 {
[ CmdletBinding ()]
param (
[ Parameter ( ValueFromPipeline )]
$pipeInput
)
end { Write-Verbose -Verbose 'test-3-end' }
clean { 'test-3-clean' >> $filePath }
}
## All command will run, so all 'Clean' blocks will run
test - 1 | test - 2 | test - 3
Test-Path $filePath | Should -BeTrue
$content = Get-Content $filePath
$content | Should -Be @ ( 'test-1-clean' , 'test-2-clean' , 'test-3-clean' )
$failure = $null
Remove-Item $filePath -Force
## Exception will be thrown from 'test-1.Process'. By that time, the 'test-2.Begin' has run,
## so 'test-2.Clean' will run. However, 'test-3.End' won't run, so 'test-3.Clean' won't run.
try {
test - 1 -EmitError | test - 2 | test - 3
} catch {
$failure = $_
}
$failure | Should -Not -BeNullOrEmpty
$failure . Exception . Message | Should -BeExactly 'test-1-process-error'
Test-Path $filePath | Should -BeTrue
$content = Get-Content $filePath
$content | Should -Be @ ( 'test-1-clean' , 'test-2-clean' )
}
It 'does not execute End {} if the pipeline is halted during Process {}' {
# We don't need Should -Not -Throw as if this reaches end{} and throws the test will fail anyway.
1 . .10 |
& {
begin { "BEGIN" }
process { "PROCESS $_ " }
end { "END" ; throw "This should not be reached." }
} |
Select-Object -First 3 |
Should -Be @ ( "BEGIN" , "PROCESS 1" , "PROCESS 2" )
}
It "still executes 'Clean' block if the pipeline is halted" {
1 . .10 |
& {
process { $_ }
clean { "Clean block hit" > $filePath }
} |
Select-Object -First 1 |
Should -Be 1
Test-Path $filePath | Should -BeTrue
Get-Content $filePath | Should -BeExactly 'Clean block hit'
}
It "Select-Object in pipeline" {
function bar {
process { 'bar_' + $_ } end { 'bar_end' } clean { 'bar_clean' > $filePath }
}
function zoo {
process { 'zoo_' + $_ } end { 'zoo_end' } clean { 'zoo_clean' >> $filePath }
}
1 . .10 | bar | Select-Object -First 2 | zoo | Should -Be @ ( 'zoo_bar_1' , 'zoo_bar_2' , 'zoo_end' )
Test-Path $filePath | Should -BeTrue
$content = Get-Content $filePath
$content | Should -Be @ ( 'bar_clean' , 'zoo_clean' )
}
}
Context 'Streams from Named Blocks' {
It 'Permits output from named block: <Script>' -TestCases @ (
@ { Script = { begin { 10 } }; ExpectedResult = 10 }
@ { Script = { process { 15 } }; ExpectedResult = 15 }
@ { Script = { end { 22 } }; ExpectedResult = 22 }
) {
param ( $Script , $ExpectedResult )
& $Script | Should -Be $ExpectedResult
}
It "Does not allow output from 'Clean' block" {
& { end { } clean { 11 } } | Should -BeNullOrEmpty
}
It "OutVariable should not capture anything from 'Clean' block" {
function test {
[ CmdletBinding ()]
param ()
Begin { 'begin' }
Process { 'process' }
End { 'end' }
clean { 'clean' }
}
test -OutVariable ov | Should -Be @ ( 'begin' , 'process' , 'end' )
$ov | Should -Be @ ( 'begin' , 'process' , 'end' )
}
It "Other streams can be captured from 'Clean' block" {
function test {
[ CmdletBinding ()]
param ()
End { }
clean {
Write-Output 'clean-output'
Write-Warning 'clean-warning'
Write-Verbose -Verbose 'clean-verbose'
Write-Debug -Debug 'clean-debug'
Write-Information 'clean-information'
}
}
test -OutVariable ov -WarningVariable wv -InformationVariable iv
$ov . Count | Should -Be 0
$wv | Should -BeExactly 'clean-warning'
$iv | Should -BeExactly 'clean-information'
$allStreams = test *>& 1
$allStreams | Should -Be @ ( 'clean-warning' , 'clean-verbose' , 'clean-debug' , 'clean-information' )
}
It 'passes output for begin, then process, then end, then clean' {
$Script = {
clean { Write-Warning 'clean-warning' }
process { "PROCESS" }
begin { "BEGIN" }
end { "END" }
}
$results = & $Script 3 >& 1
$results | Should -Be @ ( "BEGIN" , "PROCESS" , "END" , "clean-warning" )
}
}
Context "Steppable pipeline" {
AfterEach {
if ( Test-Path $filePath ) {
Remove-Item $filePath -Force
}
}
It "'Clean' runs when steppable pipeline runs to the end successfully (<Block> block)" -TestCases @ (
@ { Script = { begin { 'BEGIN' } clean { 'clean is hit' > $filePath } }; Block = 'Begin' ; ProcessResult = @ ( 'BEGIN' ); EndResult = $null }
@ { Script = { process { 'PROCESS' } clean { 'clean is hit' > $filePath } }; Block = 'Process' ; ProcessResult = @ ( 'PROCESS' ); EndResult = $null }
@ { Script = { end { 'END' } clean { 'clean is hit' > $filePath } }; Block = 'End' ; ProcessResult = $null ; EndResult = @ ( 'END' ) }
@ { Script = { begin { 'BEGIN' } process { 'PROCESS' } end { 'END' } clean { 'clean is hit' > $filePath } }; Block = 'All' ; ProcessResult = @ ( 'BEGIN' , 'PROCESS' ); EndResult = @ ( 'END' ) }
) {
param ( $Script , $ProcessResult , $EndResult )
try {
$step = { & $Script }. GetSteppablePipeline ()
$step . Begin ( $false )
$step . Process () | Should -Be $ProcessResult
$step . End () | Should -Be $EndResult
}
finally {
$step . Dispose ()
}
Test-Path $filePath | Should -BeTrue
Get-Content $filePath | Should -BeExactly 'clean is hit'
}
It "'Clean' runs when exception thrown from '<Block>' block" -TestCases @ (
@ { Script = { begin { throw 'begin-error' } clean { 'clean is hit' > $filePath } }; Block = 'Process' ; ErrorMessage = 'begin-error' }
@ { Script = { process { throw 'process-error' } clean { 'clean is hit' > $filePath } }; Block = 'Process' ; ErrorMessage = 'process-error' }
@ { Script = { end { throw 'end-error' } clean { 'clean is hit' > $filePath } }; Block = 'End' ; ErrorMessage = 'end-error' }
) {
param ( $Script , $ErrorMessage )
$failure = $null
$step = { & $Script }. GetSteppablePipeline ()
try {
$step . Begin ( $false )
$step . Process ()
$step . End ()
} catch {
$failure = $_
}
finally {
$step . Dispose ()
}
$failure | Should -Not -BeNullOrEmpty
$failure . Exception . Message | Should -BeExactly $ErrorMessage
Test-Path $filePath | Should -BeTrue
Get-Content $filePath | Should -BeExactly 'clean is hit'
}
It "'Clean' runs when we explicitly call it on a steppable pipeline" {
$script = { begin { 'begin' > $filePath } clean { 'clean is hit' >> $filePath } }
$step = { & $script }. GetSteppablePipeline ()
try {
$step . Begin ( $false )
$step . Clean ()
}
finally {
$step . Dispose ()
}
Test-Path $filePath | Should -BeTrue
Get-Content $filePath | Should -BeExactly @ ( 'begin' , 'clean is hit' )
}
It "Calling 'Clean' on steppable pipeline after it has run automatically upon Exception won't trigger the 'Clean' block to run again" {
$script = { begin { throw 'begin-error' } clean { 'clean is hit' > $filePath } }
$step = { & $script }. GetSteppablePipeline ()
$failure = $null
try {
$step . Begin ( $false )
$step . Process ()
$step . End ()
}
catch {
$failure = $_
}
$failure | Should -Not -BeNullOrEmpty
$failure . Exception . Message | Should -BeExactly 'begin-error'
Test-Path $filePath | Should -BeTrue
Get-Content $filePath | Should -BeExactly 'clean is hit'
Remove-Item $filePath -Force -ErrorAction Stop
## The 'Clean' block has already run automatically after the exception was thrown from 'Begin',
## and it won't run again when calling it explicitly.
$step . Clean ()
Test-Path $filePath | Should -BeFalse
## Dispose the steppable pipeline.
$step . Dispose ()
}
It "Calling 'Clean' on steppable pipeline after it has run automatically upon success won't trigger the 'Clean' block to run again" {
$script = { end { 'END' } clean { 'clean is hit' > $filePath } }
$step = { & $script }. GetSteppablePipeline ()
$step . Begin ( $false )
$step . Process ()
$step . End ()
Test-Path $filePath | Should -BeTrue
Get-Content $filePath | Should -BeExactly 'clean is hit'
Remove-Item $filePath -Force -ErrorAction Stop
## The 'Clean' block has already run automatically after the exception was thrown from 'Begin',
## and it won't run again when calling it explicitly.
$step . Clean ()
Test-Path $filePath | Should -BeFalse
## Dispose the steppable pipeline.
$step . Dispose ()
}
2022-02-03 19:39:08 +00:00
It "Clean block runs fine in a proxy function when a dynamic parameter fails to bind" {
$function : TestProxyGci = [ scriptblock ]:: Create (
[ Management.Automation.ProxyCommand ]:: Create (
( Get-Command Get-ChildItem )))
## The proxy function 'TestProxyGci' contains the 'dynamicparam' block, which will
## run during parameter binding. However, the parameter binding failed, and thus
## the 'begin', 'process', and 'end' blocks will not run, so '$steppablePipeline'
## in the proxy function is null (never created). The 'clean' block will run anyway,
## but it should skip calling '$steppablePipeline.Clean()' in this case.
{ TestProxyGci -Attributes } | Should -Throw -ErrorId 'MissingArgument,TestProxyGci'
}
2021-10-11 14:49:09 -07:00
}
Context "'exit' statement in command" {
AfterEach {
if ( Test-Path $filePath ) {
Remove-Item $filePath -Force
}
}
It "'Clean' block runs when 'exit' is used in other named blocks" {
pwsh -c "& { process { exit 122 } clean { 'Clean block is hit' > $filePath } }"
$LASTEXITCODE | Should -BeExactly 122
Test-Path $filePath | Should -BeTrue
Get-Content $filePath | Should -BeExactly 'Clean block is hit'
}
It "ExitException within 'Clean' block will not be propagated up" {
function test {
end {}
clean {
'Clean block is hit' > $filePath
exit
'more text' >> $filePath
}
}
test
Test-Path $filePath | Should -BeTrue
Get-Content $filePath | Should -BeExactly 'Clean block is hit'
}
It "Exit from a script file works the same in 'Clean' block" {
$scriptFile = " $TestDrive \script.ps1"
"exit 122" > $scriptFile
function test {
end {}
clean {
& $scriptFile
$LASTEXITCODE > $filePath
}
}
test
Test-Path $filePath | Should -BeTrue
Get-Content $filePath | Should -BeExactly '122'
}
}
Context 'Ctrl-C behavior' {
AfterEach {
if ( $pwsh ) {
$pwsh . Dispose ()
$pwsh = $null
}
}
It 'still executes clean {} when StopProcessing() is triggered mid-pipeline' {
$script = @"
function test {
process { Start-Sleep -Seconds 10 }
clean { Write-Information "CLEAN" }
}
"@
$pwsh = [ powershell ]:: Create ()
$pwsh . AddScript ( $script ). Invoke ()
$pwsh . Commands . Clear ()
$pwsh . AddCommand ( 'test' ) > $null
$asyncResult = $pwsh . BeginInvoke ()
Start-Sleep -Seconds 2
$pwsh . Stop ()
{ $pwsh . EndInvoke ( $asyncResult ) } | Should -Throw -ErrorId 'PipelineStoppedException'
$pwsh . Streams . Information [ 0 ]. MessageData | Should -BeExactly "CLEAN"
}
<#
It 'still completes clean {} execution when StopProcessing() is triggered mid-clean {}' {
$script = @"
function test {
begin {}
process {
"PROCESS"
}
end {}
clean {
Start-Sleep -Seconds 10
Write-Information "CLEAN"
}
}
"@
$pwsh = [powershell]::Create()
$pwsh.AddScript($script).AddStatement().AddCommand('test') > $null
$asyncResult = $pwsh.BeginInvoke()
Start-Sleep -Seconds 2
$pwsh.Stop()
$output = $pwsh.EndInvoke($asyncResult)
$output | Should -Be "PROCESS"
$pwsh.Streams.Information[0].MessageData | Should -BeExactly "CLEAN"
}
#>
}
}
2022-03-04 16:16:28 -08:00
Describe 'Other Pipeline Behaviour' -Tag 'CI' {
It "Array with 'Automation.Null' elements can be piped to pipeline" {
$automationNull = & {}
## 'Automation.Null' elements will not be sent to the pipeline (skipped).
1 , $automationNull , 2 , $automationNull , 3 , 4 , 5 | ForEach-Object { $_ } | Should -Be ( 1 . .5 )
$array = 1 , $automationNull , 2 , $automationNull , 3 , 4 , 5
$array . Count | Should -Be 7
$array | ForEach-Object { $_ } | Should -Be ( 1 . .5 )
}
It "Automation.Null is not written to pipeline in a function" {
$automationNull = & {}
function MyTest { 1 , $automationNull , 2 , $automationNull , 3 , 4 , 5 }
$result = MyTest
$result . Count | Should -Be 5
MyTest | Should -Be ( 1 . .5 )
}
}