PowerShell/test/powershell/Host/ConsoleHost.Tests.ps1
2016-11-15 14:49:39 -08:00

399 lines
16 KiB
PowerShell

using namespace System.Diagnostics
# Minishell (Singleshell) is a powershell concept.
# Its primary use-case is when somebody executes a scriptblock in the new powershell process.
# The objects are automatically marshelled to the child process and
# back to the parent session, so users can avoid custom
# serialization to pass objects between two processes.
Describe 'minishell for native executables' -Tag 'CI' {
BeforeAll {
$powershell = Join-Path -Path $PsHome -ChildPath "powershell"
}
Context 'Streams from minishell' {
It 'gets a hashtable object from minishell' {
$output = & $powershell -noprofile { @{'a' = 'b'} }
($output | measure).Count | Should Be 1
($output.GetType().Name) | Should Be 'Hashtable'
$output['a'] | Should Be 'b'
}
It 'gets the error stream from minishell' {
$output = & $powershell -noprofile { Write-Error 'foo' } 2>&1
($output | measure).Count | Should Be 1
($output.GetType().Name) | Should Be 'ErrorRecord'
$output.FullyQualifiedErrorId | Should Be 'Microsoft.PowerShell.Commands.WriteErrorException'
}
It 'gets the information stream from minishell' {
$output = & $powershell -noprofile { Write-Information 'foo' } 6>&1
($output.GetType().Name) | Should Be 'InformationRecord'
$output | Should Be 'foo'
}
}
Context 'Streams to minishell' {
It "passes input into minishell" {
$a = 1,2,3
$val = $a | & $powershell -noprofile -command { $input }
$val.Count | Should Be 3
$val[0] | Should Be 1
$val[1] | Should Be 2
$val[2] | Should Be 3
}
}
}
Describe "ConsoleHost unit tests" -tags "Feature" {
BeforeAll {
$powershell = Join-Path -Path $PsHome -ChildPath "powershell"
function NewProcessStartInfo([string]$CommandLine, [switch]$RedirectStdIn)
{
return [ProcessStartInfo]@{
FileName = $powershell
Arguments = $CommandLine
RedirectStandardInput = $RedirectStdIn
RedirectStandardOutput = $true
RedirectStandardError = $true
UseShellExecute = $false
}
}
function RunPowerShell([ProcessStartInfo]$si)
{
$process = [Process]::Start($si)
return $process
}
function EnsureChildHasExited([Process]$process, [int]$WaitTimeInMS = 15000)
{
$process.WaitForExit($WaitTimeInMS)
if (!$process.HasExited)
{
$process.HasExited | Should Be $true
$process.Kill()
}
}
}
AfterEach {
$Error.Clear()
}
Context "ShellInterop" {
It "Verify Parsing Error Output Format Single Shell should throw exception" {
try
{
& $powershell -outp blah -comm { $input }
Throw "Test execution should not reach here!"
}
catch
{
$_.FullyQualifiedErrorId | Should Be "IncorrectValueForFormatParameter"
}
}
It "Verify Validate Dollar Error Populated should throw exception" {
$origEA = $ErrorActionPreference
$ErrorActionPreference = "Stop"
try
{
$a = 1,2,3
$a | & $powershell -noprofile -command { wgwg-wrwrhqwrhrh35h3h3}
Throw "Test execution should not reach here!"
}
catch
{
$_.ToString() | Should Match "wgwg-wrwrhqwrhrh35h3h3"
$_.FullyQualifiedErrorId | Should Be "CommandNotFoundException"
}
finally
{
$ErrorActionPreference = $origEA
}
}
It "Verify Validate Output Format As Text Explicitly Child Single Shell should works" {
{
$a="blahblah"
$a | & $powershell -noprofile -out text -com { $input }
} | Should Not Throw
}
It "Verify Parsing Error Input Format Single Shell should throw exception" {
try
{
& $powershell -input blah -comm { $input }
Throw "Test execution should not reach here!"
}
catch
{
$_.FullyQualifiedErrorId | Should Be "IncorrectValueForFormatParameter"
}
}
}
Context "CommandLine" {
It "simple -args" {
& $powershell -noprofile { $args[0] } -args "hello world" | Should Be "hello world"
}
It "array -args" {
& $powershell -noprofile { $args[0] } -args 1,(2,3) | Should Be 1
(& $powershell -noprofile { $args[1] } -args 1,(2,3))[1] | Should Be 3
}
foreach ($x in "--help", "-help", "-h", "-?", "--he", "-hel", "--HELP", "-hEl") {
It "Accepts '$x' as a parameter for help" {
& $powershell -noprofile $x | ?{ $_ -match "PowerShell[.exe] -Help | -? | /?" } | Should Not BeNullOrEmpty
}
}
It "Should accept a Base64 encoded command" {
$commandString = "Get-Location"
$encodedCommand = [System.Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes($commandString))
# We don't compare to `Get-Location` directly because object and formatted output comparisons are difficult
$expected = & $powershell -noprofile -command $commandString
$actual = & $powershell -noprofile -EncodedCommand $encodedCommand
$actual | Should Be $expected
}
}
Context "Pipe to/from powershell" {
$p = [PSCustomObject]@{X=10;Y=20}
It "xml input" {
$p | & $powershell -noprofile { $input | Foreach-Object {$a = 0} { $a += $_.X + $_.Y } { $a } } | Should Be 30
$p | & $powershell -noprofile -inputFormat xml { $input | Foreach-Object {$a = 0} { $a += $_.X + $_.Y } { $a } } | Should Be 30
}
It "text input" {
# Join (multiple lines) and remove whitespace (we don't care about spacing) to verify we converted to string (by generating a table)
$p | & $powershell -noprofile -inputFormat text { -join ($input -replace "\s","") } | Should Be "XY--1020"
}
It "xml output" {
& $powershell -noprofile { [PSCustomObject]@{X=10;Y=20} } | Foreach-Object {$a = 0} { $a += $_.X + $_.Y } { $a } | Should Be 30
& $powershell -noprofile -outputFormat xml { [PSCustomObject]@{X=10;Y=20} } | Foreach-Object {$a = 0} { $a += $_.X + $_.Y } { $a } | Should Be 30
}
It "text output" {
# Join (multiple lines) and remove whitespace (we don't care about spacing) to verify we converted to string (by generating a table)
-join (& $powershell -noprofile -outputFormat text { [PSCustomObject]@{X=10;Y=20} }) -replace "\s","" | Should Be "XY--1020"
}
}
Context "Redirected standard output" {
It "Simple redirected output" {
$si = NewProcessStartInfo "-noprofile 1+1"
$process = RunPowerShell $si
$process.StandardOutput.ReadToEnd() | Should Be 2
EnsureChildHasExited $process
}
}
Context "Input redirected but not reading from stdin (not really interactive)" {
# Tests under this context are testing that we do not read from StandardInput
# even though it is redirected - we want to make sure we don't hang.
# So none of these tests should close StandardInput
It "Redirected input w/ implicit -Command w/ -NonInteractive" {
$si = NewProcessStartInfo "-NonInteractive -noprofile 1+1" -RedirectStdIn
$process = RunPowerShell $si
$process.StandardOutput.ReadToEnd() | Should Be 2
EnsureChildHasExited $process
}
It "Redirected input w/ implicit -Command w/o -NonInteractive" {
$si = NewProcessStartInfo "-noprofile 1+1" -RedirectStdIn
$process = RunPowerShell $si
$process.StandardOutput.ReadToEnd() | Should Be 2
EnsureChildHasExited $process
}
It "Redirected input w/ explicit -Command w/ -NonInteractive" {
$si = NewProcessStartInfo "-NonInteractive -noprofile -Command 1+1" -RedirectStdIn
$process = RunPowerShell $si
$process.StandardOutput.ReadToEnd() | Should Be 2
EnsureChildHasExited $process
}
It "Redirected input w/ explicit -Command w/o -NonInteractive" {
$si = NewProcessStartInfo "-noprofile -Command 1+1" -RedirectStdIn
$process = RunPowerShell $si
$process.StandardOutput.ReadToEnd() | Should Be 2
EnsureChildHasExited $process
}
It "Redirected input w/ -File w/ -NonInteractive" {
'1+1' | Out-File -Encoding Ascii -FilePath TestDrive:test.ps1 -Force
$si = NewProcessStartInfo "-noprofile -NonInteractive -File $testDrive\test.ps1" -RedirectStdIn
$process = RunPowerShell $si
$process.StandardOutput.ReadToEnd() | Should Be 2
EnsureChildHasExited $process
}
It "Redirected input w/ -File w/o -NonInteractive" {
'1+1' | Out-File -Encoding Ascii -FilePath TestDrive:test.ps1 -Force
$si = NewProcessStartInfo "-noprofile -File $testDrive\test.ps1" -RedirectStdIn
$process = RunPowerShell $si
$process.StandardOutput.ReadToEnd() | Should Be 2
EnsureChildHasExited $process
}
}
Context "Redirected standard input for 'interactive' use" {
$nl = [Environment]::Newline
# All of the following tests replace the prompt (either via an initial command or interactively)
# so that we can read StandardOutput and reliably know exactly what the prompt is.
It "Interactive redirected input" {
$si = NewProcessStartInfo "-noprofile -nologo" -RedirectStdIn
$process = RunPowerShell $si
$process.StandardInput.Write("`$function:prompt = { 'PS> ' }`n")
$null = $process.StandardOutput.ReadLine()
$process.StandardInput.Write("1+1`n")
$process.StandardOutput.ReadLine() | Should Be "PS> 1+1"
$process.StandardOutput.ReadLine() | Should Be "2"
$process.StandardInput.Write("1+2`n")
$process.StandardOutput.ReadLine() | Should Be "PS> 1+2"
$process.StandardOutput.ReadLine() | Should Be "3"
# Backspace should work as expected
$process.StandardInput.Write("1+2`b3`n")
# A real console should render 2`b3 as just 3, but we're just capturing exactly what is written
$process.StandardOutput.ReadLine() | Should Be "PS> 1+2`b3"
$process.StandardOutput.ReadLine() | Should Be "4"
$process.StandardInput.Close()
$process.StandardOutput.ReadToEnd() | Should Be "PS> "
EnsureChildHasExited $process
}
It "Interactive redirected input w/ initial command" {
$si = NewProcessStartInfo "-noprofile -noexit ""`$function:prompt = { 'PS> ' }""" -RedirectStdIn
$process = RunPowerShell $si
$process.StandardInput.Write("1+1`n")
$process.StandardOutput.ReadLine() | Should Be "PS> 1+1"
$process.StandardOutput.ReadLine() | Should Be "2"
$process.StandardInput.Write("1+2`n")
$process.StandardOutput.ReadLine() | Should Be "PS> 1+2"
$process.StandardOutput.ReadLine() | Should Be "3"
$process.StandardInput.Close()
$process.StandardOutput.ReadToEnd() | Should Be "PS> "
EnsureChildHasExited $process
}
It "Redirected input explicit prompting (-File -)" {
$si = NewProcessStartInfo "-noprofile -File -" -RedirectStdIn
$process = RunPowerShell $si
$process.StandardInput.Write("`$function:prompt = { 'PS> ' }`n")
$null = $process.StandardOutput.ReadLine()
$process.StandardInput.Write("1+1`n")
$process.StandardOutput.ReadLine() | Should Be "PS> 1+1"
$process.StandardOutput.ReadLine() | Should Be "2"
$process.StandardInput.Close()
$process.StandardOutput.ReadToEnd() | Should Be "PS> "
EnsureChildHasExited $process
}
It "Redirected input no prompting (-Command -)" {
$si = NewProcessStartInfo "-noprofile -" -RedirectStdIn
$process = RunPowerShell $si
$process.StandardInput.Write("1+1`n")
$process.StandardOutput.ReadLine() | Should Be "2"
# Multi-line input
$process.StandardInput.Write("if (1)`n{`n 42`n}`n`n")
$process.StandardOutput.ReadLine() | Should Be "42"
$process.StandardInput.Write(@"
function foo
{
'in foo'
}
foo
"@)
$process.StandardOutput.ReadLine() | Should Be "in foo"
# Backspace sent through stdin should be in the final string
$process.StandardInput.Write("`"a`bc`".Length`n")
$process.StandardOutput.ReadLine() | Should Be "3"
# Last command with no newline - should be accepted and
# produce output after closing stdin.
$process.StandardInput.Write('22 + 22')
$process.StandardInput.Close()
$process.StandardOutput.ReadLine() | Should Be "44"
EnsureChildHasExited $process
}
It "Redirected input w/ nested prompt" {
$si = NewProcessStartInfo "-noprofile -noexit ""`$function:prompt = { 'PS' + ('>'*(`$nestedPromptLevel+1)) + ' ' }""" -RedirectStdIn
$process = RunPowerShell $si
$process.StandardInput.Write("`$host.EnterNestedPrompt()`n")
$process.StandardOutput.ReadLine() | Should Be "PS> `$host.EnterNestedPrompt()"
$process.StandardInput.Write("exit`n")
$process.StandardOutput.ReadLine() | Should Be "PS>> exit"
$process.StandardInput.Close()
$process.StandardOutput.ReadToEnd() | Should Be "PS> "
EnsureChildHasExited $process
}
}
Context "Exception handling" {
It "Should handle a CallDepthOverflow" {
# Infinite recursion
function recurse
{
recurse $args
}
try
{
recurse "args"
Throw "Incorrect exception"
}
catch
{
$_.FullyQualifiedErrorId | Should Be "CallDepthOverflow"
}
}
}
}
Describe "Console host api tests" -Tag CI {
Context "String escape sequences" {
$esc = [char]0x1b
$testCases =
@{InputObject = "abc"; Length = 3; Name = "No escapes"},
@{InputObject = "${esc} [31mabc"; Length = 9; Name = "Malformed escape - extra space"},
@{InputObject = "${esc}abc"; Length = 4; Name = "Malformed escape - no csi"},
@{InputObject = "[31mabc"; Length = 7; Name = "Malformed escape - no escape"}
$testCases += if ($host.UI.SupportsVirtualTerminal)
{
@{InputObject = "$esc[31mabc"; Length = 3; Name = "Escape at start"}
@{InputObject = "$esc[31mabc$esc[0m"; Length = 3; Name = "Escape at start and end"}
}
else
{
@{InputObject = "$esc[31mabc"; Length = 8; Name = "Escape at start - no virtual term support"}
@{InputObject = "$esc[31mabc$esc[0m"; Length = 12; Name = "Escape at start and end - no virtual term support"}
}
It "Should properly calculate buffer cell width of '<Name>'" -TestCases $testCases {
param($InputObject, $Length)
$host.UI.RawUI.LengthInBufferCells($InputObject) | Should Be $Length
}
}
}