diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/ConvertToJsonCommand.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/ConvertToJsonCommand.cs index 7848a61b227..9cf6062e95d 100644 --- a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/ConvertToJsonCommand.cs +++ b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/ConvertToJsonCommand.cs @@ -109,6 +109,16 @@ protected override void BeginProcessing() /// protected override void ProcessRecord() { + if (InternalTestHooks.ActivateSleepForStoppingTest != 0) + { + // We'll wait the verbose stream marker in tests and then call Stop(). + // The cmdlet is very fast and likely to end before the test sees the marker + // so we have to slow down it by Sleep(50). + // We can not sleep for a long time because Stop() doesn't work on sleeping runspace. + WriteVerbose("ConvertTo-Json started"); + System.Threading.Thread.Sleep(InternalTestHooks.ActivateSleepForStoppingTest); + } + if (InputObject != null) { _inputObjects.Add(InputObject); diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/XmlCommands.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/XmlCommands.cs index 1fa3f9befa5..8f467b024c7 100644 --- a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/XmlCommands.cs +++ b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/XmlCommands.cs @@ -131,6 +131,12 @@ protected override void ProcessRecord() { + if (InternalTestHooks.ActivateSleepForStoppingTest != 0) + { + WriteVerbose("Export-Clixml started"); + System.Threading.Thread.Sleep(InternalTestHooks.ActivateSleepForStoppingTest); + } + if (_serializer != null) { _serializer.Serialize(InputObject); @@ -334,6 +340,12 @@ protected override void ProcessRecord() { foreach (string path in Path) { + if (InternalTestHooks.ActivateSleepForStoppingTest != 0) + { + WriteVerbose("Import-Clixml started"); + System.Threading.Thread.Sleep(InternalTestHooks.ActivateSleepForStoppingTest); + } + _helper = new ImportXmlHelper(path, this, _isLiteralPath); _helper.Import(); } @@ -423,6 +435,12 @@ protected override void BeginProcessing() /// protected override void ProcessRecord() { + if (InternalTestHooks.ActivateSleepForStoppingTest != 0) + { + WriteVerbose("ConvertTo-Xml started"); + System.Threading.Thread.Sleep(InternalTestHooks.ActivateSleepForStoppingTest); + } + if (As.Equals("Stream", StringComparison.OrdinalIgnoreCase)) { CreateMemoryStream(); diff --git a/src/System.Management.Automation/engine/Utils.cs b/src/System.Management.Automation/engine/Utils.cs index 5210755f0bd..9f0a44680c1 100644 --- a/src/System.Management.Automation/engine/Utils.cs +++ b/src/System.Management.Automation/engine/Utils.cs @@ -1366,6 +1366,7 @@ namespace System.Management.Automation.Internal { /// This class is used for internal test purposes. [SuppressMessage("Microsoft.MSInternal", "CA903:InternalNamespaceShouldNotContainPublicTypes", Justification = "Needed Internal use only")] + [SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:FieldsMustBePrivate", Justification = "Reviewed.")] public static class InternalTestHooks { internal static bool BypassGroupPolicyCaching; @@ -1395,6 +1396,9 @@ public static class InternalTestHooks internal static bool ShowMarkdownOutputBypass; + // Slow down a cmdlet to predictably check the cmdlet stopping (that 'PowerShell.Stop()' or 'Ctrl-C' work). + internal static int ActivateSleepForStoppingTest; + /// This member is used for internal test purposes. public static void SetTestHook(string property, object value) { diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/ConvertTo-Json.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Utility/ConvertTo-Json.Tests.ps1 index 198cccf5964..f643fb901ce 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Utility/ConvertTo-Json.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/ConvertTo-Json.Tests.ps1 @@ -22,22 +22,12 @@ Describe 'ConvertTo-Json' -tags "CI" { } It "StopProcessing should succeed" { - $ps = [PowerShell]::Create() - $null = $ps.AddScript({ + $sb = { $obj = [PSCustomObject]@{P1 = ''; P2 = ''; P3 = ''; P4 = ''; P5 = ''; P6 = ''} $obj.P1 = $obj.P2 = $obj.P3 = $obj.P4 = $obj.P5 = $obj.P6 = $obj 1..100 | Foreach-Object { $obj } | ConvertTo-Json -Depth 10 -Verbose - # the conversion is expected to take some time, this throw is in case it doesn't - throw "Should not have thrown exception" - }) - $null = $ps.BeginInvoke() - # wait for verbose message from ConvertTo-Json to ensure cmdlet is processing - Wait-UntilTrue { $ps.Streams.Verbose.Count -gt 0 } - $null = $ps.BeginStop($null, $null) - # wait a bit to ensure state has changed, not using synchronous Stop() to avoid blocking Pester - Start-Sleep -Milliseconds 100 - $ps.InvocationStateInfo.State | Should -BeExactly "Stopped" - $ps.Dispose() + } + Test-Stopping $sb -IntervalInMilliseconds 50 } It "The result string is packed in an array symbols when AsArray parameter is used." { diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/ConvertTo-Xml.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Utility/ConvertTo-Xml.Tests.ps1 index 848167703ab..df94b523323 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Utility/ConvertTo-Xml.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/ConvertTo-Xml.Tests.ps1 @@ -59,22 +59,16 @@ Describe "ConvertTo-Xml DRT Unit Tests" -Tags "CI" { } It "StopProcessing should work" { - $ps = [PowerShell]::Create() - $ps.AddCommand("Get-Process") - $ps.AddCommand("ConvertTo-Xml") - $ps.AddParameter("Depth", 2) - $ps.BeginInvoke() - $ps.Stop() - $ps.InvocationStateInfo.State | Should -BeExactly "Stopped" + Test-Stopping { Get-Process | ConvertTo-Xml -Depth 2 -Verbose } -IntervalInMilliseconds 50 } # these tests just cover aspects that aren't normally exercised being used as a cmdlet - It "Can read back switch and parameter values using api" { + It "Can read back switch and parameter values using api" { Add-Type -AssemblyName "${pshome}/Microsoft.PowerShell.Commands.Utility.dll" - $cmd = [Microsoft.PowerShell.Commands.ConvertToXmlCommand]::new() - $cmd.NoTypeInformation = $true - $cmd.NoTypeInformation | Should -BeTrue + $cmd = [Microsoft.PowerShell.Commands.ConvertToXmlCommand]::new() + $cmd.NoTypeInformation = $true + $cmd.NoTypeInformation | Should -BeTrue } It "Serialize primitive type" { diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/XMLCommand.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Utility/XMLCommand.Tests.ps1 index 2882d88f888..d9b1e3ae86d 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Utility/XMLCommand.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/XMLCommand.Tests.ps1 @@ -2,11 +2,11 @@ # Licensed under the MIT License. Describe "XmlCommand DRT basic functionality Tests" -Tags "CI" { - BeforeAll { - if(-not ('IsHiddenTestType' -as "type")) - { - Add-Type -TypeDefinition @" - public class IsHiddenTestType + BeforeAll { + if(-not ('IsHiddenTestType' -as "type")) + { + Add-Type -TypeDefinition @" + public class IsHiddenTestType { public IsHiddenTestType() { @@ -24,139 +24,125 @@ Describe "XmlCommand DRT basic functionality Tests" -Tags "CI" { public string Property2; } "@ - } + } } - BeforeEach { - $testfile = Join-Path -Path $TestDrive -ChildPath "clixml-directive.xml" - } + BeforeEach { + $testfile = Join-Path -Path $TestDrive -ChildPath "clixml-directive.xml" + } AfterEach { - remove-item $testfile -Force -ErrorAction SilentlyContinue + remove-item $testfile -Force -ErrorAction SilentlyContinue } - It "Import with CliXml directive should work" { + It "Import with CliXml directive should work" { Get-Command export* -Type Cmdlet | Select-Object -First 3 | Export-Clixml -Path $testfile - $results = Import-Clixml $testfile - $results.Count | Should -BeExactly 3 + $results = Import-Clixml $testfile + $results.Count | Should -BeExactly 3 $results[0].PSTypeNames[0] | Should -Be "Deserialized.System.Management.Automation.CmdletInfo" } - It "Import with Rehydration should work" { - $property1 = 256 - $property2 = "abcdef" - $isHiddenTestType = [IsHiddenTestType]::New($property1,$property2) - $isHiddenTestType | Export-Clixml $testfile - $results = Import-Clixml $testfile - $results.Property1 | Should -Be $property1 - $results.Property2 | Should -Be $property2 + It "Import with Rehydration should work" { + $property1 = 256 + $property2 = "abcdef" + $isHiddenTestType = [IsHiddenTestType]::New($property1,$property2) + $isHiddenTestType | Export-Clixml $testfile + $results = Import-Clixml $testfile + $results.Property1 | Should -Be $property1 + $results.Property2 | Should -Be $property2 + } + + It "Export-Clixml StopProcessing should succeed" { + $sb = { 1..20000 | Export-CliXml -Path $testfile -Verbose } + Test-Stopping $sb -IntervalInMilliseconds 50 + } + + It "Import-Clixml StopProcessing should succeed" { + 1..20000 | Export-Clixml -Path $testfile + $sb = { Import-CliXml -Path $testfile,$testfile,$testfile,$testfile,$testfile -Verbose } + Test-Stopping $sb -IntervalInMilliseconds 20 } - It "Export-Clixml StopProcessing should succeed" { - $ps = [PowerShell]::Create() - $null = $ps.AddScript("1..10") - $null = $ps.AddCommand("foreach-object") - $null = $ps.AddParameter("Process", { $_; start-sleep 1 }) - $null = $ps.AddCommand("Export-CliXml") - $null = $ps.AddParameter("Path", $testfile) - $null = $ps.BeginInvoke() - Start-Sleep 1 - $null = $ps.Stop() - $ps.InvocationStateInfo.State | Should -Be "Stopped" - $ps.Dispose() - } - - It "Import-Clixml StopProcessing should succeed" { - 1,2,3 | Export-Clixml -Path $testfile - $ps = [PowerShell]::Create() - $ps.AddCommand("Get-Process") - $ps.AddCommand("Import-CliXml") - $ps.AddParameter("Path", $testfile) - $ps.BeginInvoke() - $ps.Stop() - $ps.InvocationStateInfo.State | Should -Be "Stopped" - } - - It "Export-Clixml using -Depth should work" { - class Three - { - [int] $num = 3; - } - - class Two - { - [Three] $three = [Three]::New(); - [int] $value = 2; - } - - class One - { - [Two] $two = [Two]::New(); - [int] $value = 1; - } - - $one = [One]::New() - $one | Export-Clixml -Depth 2 -Path $testfile - $deserialized_one = Import-Clixml -Path $testfile - $deserialized_one.Value | Should -Be 1 - $deserialized_one.two.Value | Should -Be 2 - $deserialized_one.two.Three | Should -Not -BeNullOrEmpty - $deserialized_one.two.three.num | Should -BeNullOrEmpty - } - - It "Import-Clixml should work with XML serialization from pwsh.exe" { - # need to create separate process so that current powershell doesn't interpret clixml output - Start-Process -FilePath $pshome\pwsh -RedirectStandardOutput $testfile -Args "-noprofile -nologo -outputformat xml -command get-command import-clixml" -Wait - $out = Import-Clixml -Path $testfile - $out.Name | Should -Be "Import-CliXml" - $out.CommandType.ToString() | Should -Be "Cmdlet" - $out.Source | Should -Be "Microsoft.PowerShell.Utility" - } - - It "Import-Clixml -IncludeTotalCount always returns unknown total count" { - # this cmdlets supports paging, but not this switch - [PSCustomObject]@{foo=1;bar=@{hello="world"}} | Export-Clixml -Path $testfile - $out = Import-Clixml -Path $testfile -IncludeTotalCount - $out[0].ToString() | Should -BeExactly "Unknown total count" - } - - It "Import-Clixml -First and -Skip work together for simple types" { - "one","two","three","four" | Export-Clixml -Path $testfile - $out = Import-Clixml -Path $testfile -First 2 -Skip 1 - $out.Count | Should -Be 2 - $out[0] | Should -BeExactly "two" - $out[1] | Should -BeExactly "three" - } - - It "Import-Clixml -First and -Skip work together for collections" { - @{a=1;b=2;c=3;d=4} | Export-Clixml -Path $testfile - # order not guaranteed, even with [ordered] so we have to be smart here and compare against the full result - $out1 = Import-Clixml -Path $testfile # this results in a hashtable - $out2 = Import-Clixml -Path $testfile -First 2 -Skip 1 # this results in a dictionary entry - $out2.Count | Should -Be 2 + It "Export-Clixml using -Depth should work" { + class Three + { + [int] $num = 3; + } + + class Two + { + [Three] $three = [Three]::New(); + [int] $value = 2; + } + + class One + { + [Two] $two = [Two]::New(); + [int] $value = 1; + } + + $one = [One]::New() + $one | Export-Clixml -Depth 2 -Path $testfile + $deserialized_one = Import-Clixml -Path $testfile + $deserialized_one.Value | Should -Be 1 + $deserialized_one.two.Value | Should -Be 2 + $deserialized_one.two.Three | Should -Not -BeNullOrEmpty + $deserialized_one.two.three.num | Should -BeNullOrEmpty + } + + It "Import-Clixml should work with XML serialization from pwsh.exe" { + # need to create separate process so that current powershell doesn't interpret clixml output + Start-Process -FilePath $pshome\pwsh -RedirectStandardOutput $testfile -Args "-noprofile -nologo -outputformat xml -command get-command import-clixml" -Wait + $out = Import-Clixml -Path $testfile + $out.Name | Should -Be "Import-CliXml" + $out.CommandType.ToString() | Should -Be "Cmdlet" + $out.Source | Should -Be "Microsoft.PowerShell.Utility" + } + + It "Import-Clixml -IncludeTotalCount always returns unknown total count" { + # this cmdlets supports paging, but not this switch + [PSCustomObject]@{foo=1;bar=@{hello="world"}} | Export-Clixml -Path $testfile + $out = Import-Clixml -Path $testfile -IncludeTotalCount + $out[0].ToString() | Should -BeExactly "Unknown total count" + } + + It "Import-Clixml -First and -Skip work together for simple types" { + "one","two","three","four" | Export-Clixml -Path $testfile + $out = Import-Clixml -Path $testfile -First 2 -Skip 1 + $out.Count | Should -Be 2 + $out[0] | Should -BeExactly "two" + $out[1] | Should -BeExactly "three" + } + + It "Import-Clixml -First and -Skip work together for collections" { + @{a=1;b=2;c=3;d=4} | Export-Clixml -Path $testfile + # order not guaranteed, even with [ordered] so we have to be smart here and compare against the full result + $out1 = Import-Clixml -Path $testfile # this results in a hashtable + $out2 = Import-Clixml -Path $testfile -First 2 -Skip 1 # this results in a dictionary entry + $out2.Count | Should -Be 2 ($out2.Name) -join ":" | Should -Be (@($out1.Keys)[1, 2] -join ":") ($out2.Value) -join ":" | Should -Be (@($out1.Values)[1, 2] -join ":") - } - - # these tests just cover aspects that aren't normally exercised being used as a cmdlet - It "Can read back switch and parameter values using api" { - Add-Type -AssemblyName "${pshome}/Microsoft.PowerShell.Commands.Utility.dll" - - $cmd = [Microsoft.PowerShell.Commands.ExportClixmlCommand]::new() - $cmd.LiteralPath = "foo" - $cmd.LiteralPath | Should -BeExactly "foo" - $cmd.NoClobber = $true - $cmd.NoClobber | Should -BeTrue - - $cmd = [Microsoft.PowerShell.Commands.ImportClixmlCommand]::new() - $cmd.LiteralPath = "bar" - $cmd.LiteralPath | Should -BeExactly "bar" - - $cmd = [Microsoft.PowerShell.Commands.SelectXmlCommand]::new() - $cmd.LiteralPath = "foo" - $cmd.LiteralPath | Should -BeExactly "foo" - $xml = [xml]"" - $cmd.Xml = $xml - $cmd.Xml | Should -Be $xml - } + } + + # these tests just cover aspects that aren't normally exercised being used as a cmdlet + It "Can read back switch and parameter values using api" { + Add-Type -AssemblyName "${pshome}/Microsoft.PowerShell.Commands.Utility.dll" + + $cmd = [Microsoft.PowerShell.Commands.ExportClixmlCommand]::new() + $cmd.LiteralPath = "foo" + $cmd.LiteralPath | Should -BeExactly "foo" + $cmd.NoClobber = $true + $cmd.NoClobber | Should -BeTrue + + $cmd = [Microsoft.PowerShell.Commands.ImportClixmlCommand]::new() + $cmd.LiteralPath = "bar" + $cmd.LiteralPath | Should -BeExactly "bar" + + $cmd = [Microsoft.PowerShell.Commands.SelectXmlCommand]::new() + $cmd.LiteralPath = "foo" + $cmd.LiteralPath | Should -BeExactly "foo" + $xml = [xml]"" + $cmd.Xml = $xml + $cmd.Xml | Should -Be $xml + } } diff --git a/test/tools/Modules/HelpersCommon/HelpersCommon.psd1 b/test/tools/Modules/HelpersCommon/HelpersCommon.psd1 index 0dea69c59dc..7c65fc5d457 100644 --- a/test/tools/Modules/HelpersCommon/HelpersCommon.psd1 +++ b/test/tools/Modules/HelpersCommon/HelpersCommon.psd1 @@ -30,6 +30,7 @@ FunctionsToExport = @( 'Test-IsVstsLinux' 'Test-IsVstsWindows' 'Test-TesthookIsSet' + 'Test-Stopping' 'Wait-FileToBePresent' 'Wait-UntilTrue' ) diff --git a/test/tools/Modules/HelpersCommon/HelpersCommon.psm1 b/test/tools/Modules/HelpersCommon/HelpersCommon.psm1 index f3ca73a1fed..16d1e0591b4 100644 --- a/test/tools/Modules/HelpersCommon/HelpersCommon.psm1 +++ b/test/tools/Modules/HelpersCommon/HelpersCommon.psm1 @@ -1,5 +1,6 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. + function Wait-UntilTrue { [CmdletBinding()] @@ -314,6 +315,34 @@ function Start-NativeExecution } } +# Test if the scriptblock (cmdlet) can be stopped +function Test-Stopping +{ + [CmdletBinding()] + param ( + [ScriptBlock]$sb, + [int]$TimeoutInMilliseconds = 10000, + [int]$IntervalInMilliseconds = 100, + [int]$SlowDownCmdletInMilliseconds = 50 + ) + + try { + $ps = [PowerShell]::Create() + $null = $ps.AddScript($sb) + ${Script:TesthookType}::SetTestHook('ActivateSleepForStoppingTest', $SlowDownCmdletInMilliseconds) + $null = $ps.BeginInvoke() + Wait-UntilTrue { $ps.Streams.Verbose.Count -gt 0 } -TimeoutInMilliseconds $TimeoutInMilliseconds -IntervalInMilliseconds $IntervalInMilliseconds + $null = $ps.BeginStop($null, $null) + Wait-UntilTrue { $ps.InvocationStateInfo.State -eq "Stopped" } -TimeoutInMilliseconds $TimeoutInMilliseconds -IntervalInMilliseconds $IntervalInMilliseconds + + $ps.InvocationStateInfo.State | Should -Be "Stopped" + + } finally { + $ps.Dispose() + ${Script:TesthookType}::SetTestHook('ActivateSleepForStoppingTest', 0) + } +} + # Creates a new random hex string for use with things like test certificate passwords function New-RandomHexString {