// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.PowerShell.EditorServices.Hosting; using Microsoft.PowerShell.EditorServices.Services.PowerShell.Console; using Microsoft.PowerShell.EditorServices.Services.PowerShell.Execution; using Microsoft.PowerShell.EditorServices.Services.PowerShell.Host; using Microsoft.PowerShell.EditorServices.Services.PowerShell.Utility; using Microsoft.PowerShell.EditorServices.Test; using Xunit; namespace PowerShellEditorServices.Test.Session { using System.Management.Automation; using System.Management.Automation.Runspaces; [Trait("Category", "PsesInternalHost")] public class PsesInternalHostTests : IAsyncLifetime { private PsesInternalHost psesHost; public async Task InitializeAsync() => psesHost = await PsesHostFactory.Create(NullLoggerFactory.Instance); public async Task DisposeAsync() => await psesHost.StopAsync(); [Fact] public async Task CanExecutePSCommand() { Assert.True(psesHost.IsRunning); PSCommand command = new PSCommand().AddScript("$a = \"foo\"; $a"); Task> task = psesHost.ExecutePSCommandAsync(command, CancellationToken.None); IReadOnlyList result = await task; Assert.Equal("foo", result[0]); } [Fact] // https://github.com/PowerShell/vscode-powershell/issues/3677 public async Task CanHandleThrow() { await psesHost.ExecutePSCommandAsync( new PSCommand().AddScript("throw"), CancellationToken.None, new PowerShellExecutionOptions { ThrowOnError = false }); } [Fact] public async Task CanQueueParallelPSCommands() { // Concurrently initiate 4 requests in the session. Task taskOne = psesHost.ExecutePSCommandAsync( new PSCommand().AddScript("$x = 100"), CancellationToken.None); Task taskTwo = psesHost.ExecutePSCommandAsync( new PSCommand().AddScript("$x += 200"), CancellationToken.None); Task taskThree = psesHost.ExecutePSCommandAsync( new PSCommand().AddScript("$x = $x / 100"), CancellationToken.None); Task> resultTask = psesHost.ExecutePSCommandAsync( new PSCommand().AddScript("$x"), CancellationToken.None); // Wait for all of the executes to complete. await Task.WhenAll(taskOne, taskTwo, taskThree, resultTask); // Sanity checks Assert.Equal(RunspaceState.Opened, psesHost.Runspace.RunspaceStateInfo.State); // 100 + 200 = 300, then divided by 100 is 3. We are ensuring that // the commands were executed in the sequence they were called. Assert.Equal(3, (await resultTask)[0]); } [Fact] public async Task CanCancelExecutionWithToken() { using CancellationTokenSource cancellationSource = new(millisecondsDelay: 1000); await Assert.ThrowsAsync(() => { return psesHost.ExecutePSCommandAsync( new PSCommand().AddScript("Start-Sleep 10"), cancellationSource.Token); }); } [Fact] [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD003:Avoid awaiting foreign Tasks", Justification = "Explicitly checking task cancellation status.")] public async Task CanCancelExecutionWithMethod() { Task executeTask = psesHost.ExecutePSCommandAsync( new PSCommand().AddScript("Start-Sleep 10"), CancellationToken.None); // Cancel the task after 1 second in another thread. Task.Run(() => { Thread.Sleep(1000); psesHost.CancelCurrentTask(); }); await Assert.ThrowsAsync(() => executeTask); Assert.True(executeTask.IsCanceled); } [Fact] public async Task CanHandleNoProfiles() { // Call LoadProfiles with profile paths that won't exist, and assert that it does not // throw PSInvalidOperationException (which it previously did when it tried to invoke an // empty command). ProfilePathInfo emptyProfilePaths = new("", "", "", ""); await psesHost.ExecuteDelegateAsync( "LoadProfiles", executionOptions: null, (pwsh, _) => { pwsh.LoadProfiles(emptyProfilePaths); Assert.Empty(pwsh.Commands.Commands); }, CancellationToken.None); } // NOTE: Tests where we call functions that use PowerShell runspaces are slightly more // complicated than one would expect because we explicitly need the methods to run on the // pipeline thread, otherwise Windows complains about the the thread's apartment state not // matching. Hence we use a delegate where it looks like we could just call the method. [Fact] public async Task CanHandleBrokenPrompt() { _ = await Assert.ThrowsAsync(() => { return psesHost.ExecutePSCommandAsync( new PSCommand().AddScript("function prompt { throw }; prompt"), CancellationToken.None); }); string prompt = await psesHost.ExecuteDelegateAsync( nameof(psesHost.GetPrompt), executionOptions: null, (_, _) => psesHost.GetPrompt(CancellationToken.None), CancellationToken.None); Assert.Equal(PsesInternalHost.DefaultPrompt, prompt); } [Fact] public async Task CanHandleUndefinedPrompt() { Assert.Empty(await psesHost.ExecutePSCommandAsync( new PSCommand().AddScript("Remove-Item function:prompt; Get-Item function:prompt -ErrorAction Ignore"), CancellationToken.None)); string prompt = await psesHost.ExecuteDelegateAsync( nameof(psesHost.GetPrompt), executionOptions: null, (_, _) => psesHost.GetPrompt(CancellationToken.None), CancellationToken.None); Assert.Equal(PsesInternalHost.DefaultPrompt, prompt); } [Fact] public async Task CanRunOnIdleTask() { IReadOnlyList task = await psesHost.ExecutePSCommandAsync( new PSCommand().AddScript("$handled = $false; Register-EngineEvent -SourceIdentifier PowerShell.OnIdle -MaxTriggerCount 1 -Action { $global:handled = $true }"), CancellationToken.None); IReadOnlyList handled = await psesHost.ExecutePSCommandAsync( new PSCommand().AddScript("$handled"), CancellationToken.None); Assert.Collection(handled, Assert.False); await psesHost.ExecuteDelegateAsync( nameof(psesHost.OnPowerShellIdle), executionOptions: null, (_, _) => psesHost.OnPowerShellIdle(CancellationToken.None), CancellationToken.None); // TODO: Why is this racy? Thread.Sleep(2000); handled = await psesHost.ExecutePSCommandAsync( new PSCommand().AddScript("$handled"), CancellationToken.None); Assert.Collection(handled, Assert.True); } [Fact] public async Task CanLoadPSReadLine() { Assert.True(await psesHost.ExecuteDelegateAsync( nameof(psesHost.TryLoadPSReadLine), executionOptions: null, (pwsh, _) => psesHost.TryLoadPSReadLine( pwsh, (EngineIntrinsics)pwsh.Runspace.SessionStateProxy.GetVariable("ExecutionContext"), out IReadLine readLine), CancellationToken.None)); } // This test asserts that we do not mess up the console encoding, which leads to native // commands receiving piped input failing. [Fact] public async Task ExecutesNativeCommandsCorrectly() { await psesHost.ExecutePSCommandAsync( new PSCommand().AddScript("\"protocol=https`nhost=myhost.com`nusername=john`npassword=doe`n`n\" | git.exe credential approve; if ($LastExitCode) { throw }"), CancellationToken.None); } [Theory] [InlineData("")] // Regression test for "unset" path. [InlineData(@"C:\Some\Bad\Directory")] // Non-existent directory. [InlineData("testhost.dll")] // Existent file. public async Task CanHandleBadInitialWorkingDirectory(string path) { string cwd = Environment.CurrentDirectory; await psesHost.SetInitialWorkingDirectoryAsync(path, CancellationToken.None); IReadOnlyList getLocation = await psesHost.ExecutePSCommandAsync( new PSCommand().AddCommand("Get-Location"), CancellationToken.None); Assert.Collection(getLocation, (d) => Assert.Equal(cwd, d, ignoreCase: true)); } } [Trait("Category", "PsesInternalHost")] public class PsesInternalHostWithProfileTests : IAsyncLifetime { private PsesInternalHost psesHost; public async Task InitializeAsync() => psesHost = await PsesHostFactory.Create(NullLoggerFactory.Instance, loadProfiles: true); public async Task DisposeAsync() => await psesHost.StopAsync(); [Fact] public async Task CanResolveAndLoadProfilesForHostId() { // Ensure that the $PROFILE variable is a string with the value of CurrentUserCurrentHost. IReadOnlyList profileVariable = await psesHost.ExecutePSCommandAsync( new PSCommand().AddScript("$PROFILE"), CancellationToken.None); Assert.Collection(profileVariable, (p) => Assert.Equal(PsesHostFactory.TestProfilePaths.CurrentUserCurrentHost, p)); // Ensure that all the profile paths are set in the correct note properties. IReadOnlyList profileProperties = await psesHost.ExecutePSCommandAsync( new PSCommand().AddScript("$PROFILE | Get-Member -Type NoteProperty"), CancellationToken.None); Assert.Collection(profileProperties, (p) => Assert.Equal($"string AllUsersAllHosts={PsesHostFactory.TestProfilePaths.AllUsersAllHosts}", p, ignoreCase: true), (p) => Assert.Equal($"string AllUsersCurrentHost={PsesHostFactory.TestProfilePaths.AllUsersCurrentHost}", p, ignoreCase: true), (p) => Assert.Equal($"string CurrentUserAllHosts={PsesHostFactory.TestProfilePaths.CurrentUserAllHosts}", p, ignoreCase: true), (p) => Assert.Equal($"string CurrentUserCurrentHost={PsesHostFactory.TestProfilePaths.CurrentUserCurrentHost}", p, ignoreCase: true)); // Ensure that the profile was loaded. The profile also checks that $PROFILE was defined. IReadOnlyList profileLoaded = await psesHost.ExecutePSCommandAsync( new PSCommand().AddScript("Assert-ProfileLoaded"), CancellationToken.None); Assert.Collection(profileLoaded, Assert.True); } // This test specifically relies on a handler registered in the test profile, and on the // test host loading the profiles during startup, that way the pipeline timing is // consistent. [Fact] public async Task CanRunOnIdleInProfileTask() { await psesHost.ExecuteDelegateAsync( nameof(psesHost.OnPowerShellIdle), executionOptions: null, (_, _) => psesHost.OnPowerShellIdle(CancellationToken.None), CancellationToken.None); // TODO: Why is this racy? Thread.Sleep(2000); IReadOnlyList handled = await psesHost.ExecutePSCommandAsync( new PSCommand().AddScript("$handledInProfile"), CancellationToken.None); Assert.Collection(handled, Assert.True); } } }