Skip to content

Commit f08902d

Browse files
Revert test to expect PSReadLine to load in tests
Co-authored-by: andyleejordan <2226434+andyleejordan@users.noreply.github.com>
1 parent 897c6af commit f08902d

3 files changed

Lines changed: 352 additions & 2 deletions

File tree

test/PowerShellEditorServices.Test/Session/PsesInternalHostTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -238,9 +238,9 @@ await psesHost.ExecuteDelegateAsync(
238238
}
239239

240240
[Fact]
241-
public async Task CannotLoadPSReadLineInTests()
241+
public async Task CanLoadPSReadLine()
242242
{
243-
Assert.False(await psesHost.ExecuteDelegateAsync(
243+
Assert.True(await psesHost.ExecuteDelegateAsync(
244244
nameof(psesHost.TryLoadPSReadLine),
245245
executionOptions: null,
246246
(pwsh, _) => psesHost.TryLoadPSReadLine(
Lines changed: 333 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,333 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Threading;
7+
using System.Threading.Tasks;
8+
using Microsoft.Extensions.Logging.Abstractions;
9+
using Microsoft.PowerShell.EditorServices.Hosting;
10+
using Microsoft.PowerShell.EditorServices.Services.PowerShell.Console;
11+
using Microsoft.PowerShell.EditorServices.Services.PowerShell.Execution;
12+
using Microsoft.PowerShell.EditorServices.Services.PowerShell.Host;
13+
using Microsoft.PowerShell.EditorServices.Services.PowerShell.Utility;
14+
using Microsoft.PowerShell.EditorServices.Test;
15+
using Xunit;
16+
17+
namespace PowerShellEditorServices.Test.Session
18+
{
19+
using System.Management.Automation;
20+
using System.Management.Automation.Runspaces;
21+
22+
// Shared helpers for the OnIdle engine-event tests, whose handler actions are
23+
// dispatched asynchronously by PowerShell's event manager.
24+
internal static class OnIdleTestHelpers
25+
{
26+
// The OnIdle engine event's -Action scriptblock is not run inline when
27+
// OnPowerShellIdle generates the event; PowerShell enqueues it as a pending
28+
// action and dispatches it asynchronously around subsequent pipeline executions.
29+
// So instead of sleeping a fixed amount, poll the handler variable until it
30+
// reports true (each read is itself a pipeline, giving the engine another chance
31+
// to drain the pending action), then assert it was set within the timeout.
32+
internal static async Task AssertHandledAsync(PsesInternalHost psesHost, string variableName)
33+
{
34+
using CancellationTokenSource cancellationSource = new(millisecondsDelay: 15000);
35+
bool handled = false;
36+
while (!handled && !cancellationSource.IsCancellationRequested)
37+
{
38+
IReadOnlyList<bool> result = await psesHost.ExecutePSCommandAsync<bool>(
39+
new PSCommand().AddScript(variableName),
40+
CancellationToken.None);
41+
42+
handled = result.Count > 0 && result[0];
43+
if (!handled)
44+
{
45+
await Task.Delay(200);
46+
}
47+
}
48+
49+
Assert.True(handled, $"Timed out waiting for the OnIdle handler to set '{variableName}'.");
50+
}
51+
}
52+
53+
[Trait("Category", "PsesInternalHost")]
54+
public class PsesInternalHostTests : IAsyncLifetime
55+
{
56+
private PsesInternalHost psesHost;
57+
58+
public async Task InitializeAsync() => psesHost = await PsesHostFactory.Create(NullLoggerFactory.Instance);
59+
60+
public async Task DisposeAsync() => await psesHost.StopAsync();
61+
62+
[Fact]
63+
public async Task CanExecutePSCommand()
64+
{
65+
Assert.True(psesHost.IsRunning);
66+
PSCommand command = new PSCommand().AddScript("$a = \"foo\"; $a");
67+
Task<IReadOnlyList<string>> task = psesHost.ExecutePSCommandAsync<string>(command, CancellationToken.None);
68+
IReadOnlyList<string> result = await task;
69+
Assert.Equal("foo", result[0]);
70+
}
71+
72+
[Fact] // https://github.com/PowerShell/vscode-powershell/issues/3677
73+
public async Task CanHandleThrow()
74+
{
75+
await psesHost.ExecutePSCommandAsync(
76+
new PSCommand().AddScript("throw"),
77+
CancellationToken.None,
78+
new PowerShellExecutionOptions { ThrowOnError = false });
79+
}
80+
81+
[Fact]
82+
public async Task CanQueueParallelPSCommands()
83+
{
84+
// Concurrently initiate 4 requests in the session.
85+
Task taskOne = psesHost.ExecutePSCommandAsync(
86+
new PSCommand().AddScript("$x = 100"),
87+
CancellationToken.None);
88+
89+
Task taskTwo = psesHost.ExecutePSCommandAsync(
90+
new PSCommand().AddScript("$x += 200"),
91+
CancellationToken.None);
92+
93+
Task taskThree = psesHost.ExecutePSCommandAsync(
94+
new PSCommand().AddScript("$x = $x / 100"),
95+
CancellationToken.None);
96+
97+
Task<IReadOnlyList<int>> resultTask = psesHost.ExecutePSCommandAsync<int>(
98+
new PSCommand().AddScript("$x"),
99+
CancellationToken.None);
100+
101+
// Wait for all of the executes to complete.
102+
await Task.WhenAll(taskOne, taskTwo, taskThree, resultTask);
103+
104+
// Sanity checks
105+
Assert.Equal(RunspaceState.Opened, psesHost.Runspace.RunspaceStateInfo.State);
106+
107+
// 100 + 200 = 300, then divided by 100 is 3. We are ensuring that
108+
// the commands were executed in the sequence they were called.
109+
Assert.Equal(3, (await resultTask)[0]);
110+
}
111+
112+
[Fact]
113+
public async Task CanCancelExecutionWithToken()
114+
{
115+
using CancellationTokenSource cancellationSource = new(millisecondsDelay: 1000);
116+
await Assert.ThrowsAsync<TaskCanceledException>(() =>
117+
{
118+
return psesHost.ExecutePSCommandAsync(
119+
new PSCommand().AddScript("Start-Sleep 10"),
120+
cancellationSource.Token);
121+
});
122+
}
123+
124+
[Fact]
125+
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD003:Avoid awaiting foreign Tasks", Justification = "Explicitly checking task cancellation status.")]
126+
public async Task CanCancelExecutionWithMethod()
127+
{
128+
Task executeTask = psesHost.ExecutePSCommandAsync(
129+
new PSCommand().AddScript("Start-Sleep 10"),
130+
CancellationToken.None);
131+
132+
// Cancel the task after 1 second in another thread.
133+
Task.Run(() => { Thread.Sleep(1000); psesHost.CancelCurrentTask(); });
134+
await Assert.ThrowsAsync<TaskCanceledException>(() => executeTask);
135+
Assert.True(executeTask.IsCanceled);
136+
}
137+
138+
[Fact]
139+
public async Task CanHandleMissingProfilePaths()
140+
{
141+
// Call LoadProfileScripts with profile paths that won't exist, and assert that it does
142+
// not throw PSInvalidOperationException (which it previously did when it tried to
143+
// invoke an empty command).
144+
ProfilePathInfo emptyProfilePaths = new("", "", "", "");
145+
await psesHost.ExecuteDelegateAsync(
146+
"SetProfileVariableAndLoadProfileScripts",
147+
executionOptions: null,
148+
(pwsh, _) =>
149+
{
150+
pwsh.SetProfileVariable(emptyProfilePaths);
151+
pwsh.LoadProfileScripts(emptyProfilePaths);
152+
153+
Assert.Equal(emptyProfilePaths.CurrentUserCurrentHost, pwsh.Runspace.SessionStateProxy.GetVariable("PROFILE")?.ToString());
154+
Assert.Empty(pwsh.Commands.Commands);
155+
},
156+
CancellationToken.None);
157+
}
158+
159+
[Fact]
160+
public async Task SetsProfileVariableWhenProfilesAreNotLoaded()
161+
{
162+
// This host fixture starts with LoadProfiles = false. Ensure $PROFILE is still set.
163+
IReadOnlyList<string> profileVariable = await psesHost.ExecutePSCommandAsync<string>(
164+
new PSCommand().AddScript("$PROFILE"),
165+
CancellationToken.None);
166+
167+
Assert.Collection(profileVariable,
168+
(p) => Assert.Equal(PsesHostFactory.TestProfilePaths.CurrentUserCurrentHost, p));
169+
170+
// Ensure profile scripts were not loaded as part of startup.
171+
IReadOnlyList<PSObject> profileLoadedCommand = await psesHost.ExecutePSCommandAsync<PSObject>(
172+
new PSCommand().AddScript("Get-Command Assert-ProfileLoaded -ErrorAction Ignore"),
173+
CancellationToken.None);
174+
175+
Assert.Empty(profileLoadedCommand);
176+
}
177+
178+
// NOTE: Tests where we call functions that use PowerShell runspaces are slightly more
179+
// complicated than one would expect because we explicitly need the methods to run on the
180+
// pipeline thread, otherwise Windows complains about the the thread's apartment state not
181+
// matching. Hence we use a delegate where it looks like we could just call the method.
182+
183+
[Fact]
184+
public async Task CanHandleBrokenPrompt()
185+
{
186+
_ = await Assert.ThrowsAsync<RuntimeException>(() =>
187+
{
188+
return psesHost.ExecutePSCommandAsync(
189+
new PSCommand().AddScript("function prompt { throw }; prompt"),
190+
CancellationToken.None);
191+
});
192+
193+
string prompt = await psesHost.ExecuteDelegateAsync(
194+
nameof(psesHost.GetPrompt),
195+
executionOptions: null,
196+
(_, _) => psesHost.GetPrompt(CancellationToken.None),
197+
CancellationToken.None);
198+
199+
Assert.Equal(PsesInternalHost.DefaultPrompt, prompt);
200+
}
201+
202+
[Fact]
203+
public async Task CanHandleUndefinedPrompt()
204+
{
205+
Assert.Empty(await psesHost.ExecutePSCommandAsync<PSObject>(
206+
new PSCommand().AddScript("Remove-Item function:prompt; Get-Item function:prompt -ErrorAction Ignore"),
207+
CancellationToken.None));
208+
209+
string prompt = await psesHost.ExecuteDelegateAsync(
210+
nameof(psesHost.GetPrompt),
211+
executionOptions: null,
212+
(_, _) => psesHost.GetPrompt(CancellationToken.None),
213+
CancellationToken.None);
214+
215+
Assert.Equal(PsesInternalHost.DefaultPrompt, prompt);
216+
}
217+
218+
[Fact]
219+
public async Task CanRunOnIdleTask()
220+
{
221+
IReadOnlyList<PSObject> task = await psesHost.ExecutePSCommandAsync<PSObject>(
222+
new PSCommand().AddScript("$handled = $false; Register-EngineEvent -SourceIdentifier PowerShell.OnIdle -MaxTriggerCount 1 -Action { $global:handled = $true }"),
223+
CancellationToken.None);
224+
225+
IReadOnlyList<bool> handled = await psesHost.ExecutePSCommandAsync<bool>(
226+
new PSCommand().AddScript("$handled"),
227+
CancellationToken.None);
228+
229+
Assert.Collection(handled, Assert.False);
230+
231+
await psesHost.ExecuteDelegateAsync(
232+
nameof(psesHost.OnPowerShellIdle),
233+
executionOptions: null,
234+
(_, _) => psesHost.OnPowerShellIdle(CancellationToken.None),
235+
CancellationToken.None);
236+
237+
await OnIdleTestHelpers.AssertHandledAsync(psesHost, "$handled");
238+
}
239+
240+
[Fact]
241+
public async Task CannotLoadPSReadLineInTests()
242+
{
243+
Assert.False(await psesHost.ExecuteDelegateAsync(
244+
nameof(psesHost.TryLoadPSReadLine),
245+
executionOptions: null,
246+
(pwsh, _) => psesHost.TryLoadPSReadLine(
247+
pwsh,
248+
(EngineIntrinsics)pwsh.Runspace.SessionStateProxy.GetVariable("ExecutionContext"),
249+
out IReadLine readLine),
250+
CancellationToken.None));
251+
}
252+
253+
// This test asserts that we do not mess up the console encoding, which leads to native
254+
// commands receiving piped input failing.
255+
[Fact]
256+
public async Task ExecutesNativeCommandsCorrectly()
257+
{
258+
await psesHost.ExecutePSCommandAsync(
259+
new PSCommand().AddScript("\"protocol=https`nhost=myhost.com`nusername=john`npassword=doe`n`n\" | git.exe credential approve; if ($LastExitCode) { throw }"),
260+
CancellationToken.None);
261+
}
262+
263+
[Theory]
264+
[InlineData("")] // Regression test for "unset" path.
265+
[InlineData(@"C:\Some\Bad\Directory")] // Non-existent directory.
266+
[InlineData("testhost.dll")] // Existent file.
267+
public async Task CanHandleBadInitialWorkingDirectory(string path)
268+
{
269+
string cwd = Environment.CurrentDirectory;
270+
await psesHost.SetInitialWorkingDirectoryAsync(path, CancellationToken.None);
271+
272+
IReadOnlyList<string> getLocation = await psesHost.ExecutePSCommandAsync<string>(
273+
new PSCommand().AddCommand("Get-Location"),
274+
CancellationToken.None);
275+
Assert.Collection(getLocation, (d) => Assert.Equal(cwd, d, ignoreCase: true));
276+
}
277+
}
278+
279+
[Trait("Category", "PsesInternalHost")]
280+
public class PsesInternalHostWithProfileTests : IAsyncLifetime
281+
{
282+
private PsesInternalHost psesHost;
283+
284+
public async Task InitializeAsync() => psesHost = await PsesHostFactory.Create(NullLoggerFactory.Instance, loadProfiles: true);
285+
286+
public async Task DisposeAsync() => await psesHost.StopAsync();
287+
288+
[Fact]
289+
public async Task CanResolveAndLoadProfilesForHostId()
290+
{
291+
// Ensure that the $PROFILE variable is a string with the value of CurrentUserCurrentHost.
292+
IReadOnlyList<string> profileVariable = await psesHost.ExecutePSCommandAsync<string>(
293+
new PSCommand().AddScript("$PROFILE"),
294+
CancellationToken.None);
295+
296+
Assert.Collection(profileVariable,
297+
(p) => Assert.Equal(PsesHostFactory.TestProfilePaths.CurrentUserCurrentHost, p));
298+
299+
// Ensure that all the profile paths are set in the correct note properties.
300+
IReadOnlyList<string> profileProperties = await psesHost.ExecutePSCommandAsync<string>(
301+
new PSCommand().AddScript("$PROFILE | Get-Member -Type NoteProperty"),
302+
CancellationToken.None);
303+
304+
Assert.Collection(profileProperties,
305+
(p) => Assert.Equal($"string AllUsersAllHosts={PsesHostFactory.TestProfilePaths.AllUsersAllHosts}", p, ignoreCase: true),
306+
(p) => Assert.Equal($"string AllUsersCurrentHost={PsesHostFactory.TestProfilePaths.AllUsersCurrentHost}", p, ignoreCase: true),
307+
(p) => Assert.Equal($"string CurrentUserAllHosts={PsesHostFactory.TestProfilePaths.CurrentUserAllHosts}", p, ignoreCase: true),
308+
(p) => Assert.Equal($"string CurrentUserCurrentHost={PsesHostFactory.TestProfilePaths.CurrentUserCurrentHost}", p, ignoreCase: true));
309+
310+
// Ensure that the profile was loaded. The profile also checks that $PROFILE was defined.
311+
IReadOnlyList<bool> profileLoaded = await psesHost.ExecutePSCommandAsync<bool>(
312+
new PSCommand().AddScript("Assert-ProfileLoaded"),
313+
CancellationToken.None);
314+
315+
Assert.Collection(profileLoaded, Assert.True);
316+
}
317+
318+
// This test specifically relies on a handler registered in the test profile, and on the
319+
// test host loading the profiles during startup, that way the pipeline timing is
320+
// consistent.
321+
[Fact]
322+
public async Task CanRunOnIdleInProfileTask()
323+
{
324+
await psesHost.ExecuteDelegateAsync(
325+
nameof(psesHost.OnPowerShellIdle),
326+
executionOptions: null,
327+
(_, _) => psesHost.OnPowerShellIdle(CancellationToken.None),
328+
CancellationToken.None);
329+
330+
await OnIdleTestHelpers.AssertHandledAsync(psesHost, "$handledInProfile");
331+
}
332+
}
333+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
--- test/PowerShellEditorServices.Test/Session/PsesInternalHostTests.cs
2+
+++ test/PowerShellEditorServices.Test/Session/PsesInternalHostTests.cs
3+
@@ -237,13 +237,13 @@
4+
await OnIdleTestHelpers.AssertHandledAsync(psesHost, "$handled");
5+
}
6+
7+
[Fact]
8+
- public async Task CannotLoadPSReadLineInTests()
9+
+ public async Task CanLoadPSReadLine()
10+
{
11+
- Assert.False(await psesHost.ExecuteDelegateAsync(
12+
+ Assert.True(await psesHost.ExecuteDelegateAsync(
13+
nameof(psesHost.TryLoadPSReadLine),
14+
executionOptions: null,
15+
(pwsh, _) => psesHost.TryLoadPSReadLine(
16+
pwsh,
17+
(EngineIntrinsics)pwsh.Runspace.SessionStateProxy.GetVariable("ExecutionContext"),

0 commit comments

Comments
 (0)