Skip to content

Run new PSReadLine test on Windows #1522

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Jul 9, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion PowerShellEditorServices.Common.props
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,5 @@
<RepositoryType>git</RepositoryType>
<RepositoryUrl>https://github.com/PowerShell/PowerShellEditorServices</RepositoryUrl>
<DebugType>portable</DebugType>
<DefineConstants Condition=" '$(ExtraDefineConstants)' != '' ">$(DefineConstants);$(ExtraDefineConstants)</DefineConstants>
</PropertyGroup>
</Project>
10 changes: 5 additions & 5 deletions PowerShellEditorServices.build.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -259,20 +259,20 @@ task TestServer TestServerWinPS,TestServerPS7,TestServerPS72

task TestServerWinPS -If (-not $script:IsNix) {
Set-Location .\test\PowerShellEditorServices.Test\
exec { & $script:dotnetExe test -p:ExtraDefineConstants=TEST --logger trx -f $script:NetRuntime.Desktop (DotNetTestFilter) }
exec { & $script:dotnetExe test --logger trx -f $script:NetRuntime.Desktop (DotNetTestFilter) }
}

task TestServerPS7 -If (-not $script:IsRosetta) {
Set-Location .\test\PowerShellEditorServices.Test\
Invoke-WithCreateDefaultHook -NewModulePath $script:PSCoreModulePath {
exec { & $script:dotnetExe test -p:ExtraDefineConstants=TEST --logger trx -f $script:NetRuntime.PS7 (DotNetTestFilter) }
exec { & $script:dotnetExe test --logger trx -f $script:NetRuntime.PS7 (DotNetTestFilter) }
}
}

task TestServerPS72 {
Set-Location .\test\PowerShellEditorServices.Test\
Invoke-WithCreateDefaultHook -NewModulePath $script:PSCoreModulePath {
exec { & $script:dotnetExe test -p:ExtraDefineConstants=TEST --logger trx -f $script:NetRuntime.PS72 (DotNetTestFilter) }
exec { & $script:dotnetExe test --logger trx -f $script:NetRuntime.PS72 (DotNetTestFilter) }
}
}

Expand All @@ -281,13 +281,13 @@ task TestE2E {

$env:PWSH_EXE_NAME = if ($IsCoreCLR) { "pwsh" } else { "powershell" }
$NetRuntime = if ($IsRosetta) { $script:NetRuntime.PS72 } else { $script:NetRuntime.PS7 }
exec { & $script:dotnetExe test -p:ExtraDefineConstants=TEST --logger trx -f $NetRuntime (DotNetTestFilter) }
exec { & $script:dotnetExe test --logger trx -f $NetRuntime (DotNetTestFilter) }

# Run E2E tests in ConstrainedLanguage mode.
if (!$script:IsNix) {
try {
[System.Environment]::SetEnvironmentVariable("__PSLockdownPolicy", "0x80000007", [System.EnvironmentVariableTarget]::Machine);
exec { & $script:dotnetExe test -p:ExtraDefineConstants=TEST --logger trx -f $script:NetRuntime.PS7 (DotNetTestFilter) }
exec { & $script:dotnetExe test --logger trx -f $script:NetRuntime.PS7 (DotNetTestFilter) }
} finally {
[System.Environment]::SetEnvironmentVariable("__PSLockdownPolicy", $null, [System.EnvironmentVariableTarget]::Machine);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,8 @@ private HostStartupInfo CreateHostStartupInfo()
_config.LogPath,
(int)_config.LogLevel,
consoleReplEnabled: _config.ConsoleRepl != ConsoleReplKind.None,
usesLegacyReadLine: _config.ConsoleRepl == ConsoleReplKind.LegacyReadLine);
usesLegacyReadLine: _config.ConsoleRepl == ConsoleReplKind.LegacyReadLine,
bundledModulePath: _config.BundledModulePath);
}

private void WriteStartupBanner()
Expand Down
10 changes: 9 additions & 1 deletion src/PowerShellEditorServices/Hosting/HostStartupInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,11 @@ public sealed class HostStartupInfo
/// </remarks>
public int LogLevel { get; }

/// <summary>
/// The path to find the bundled modules. User configurable for advanced usage.
/// </summary>
public string BundledModulePath { get; }

#endregion

#region Constructors
Expand Down Expand Up @@ -135,6 +140,7 @@ public sealed class HostStartupInfo
/// <param name="logLevel">The minimum log event level.</param>
/// <param name="consoleReplEnabled">Enable console if true.</param>
/// <param name="usesLegacyReadLine">Use PSReadLine if false, otherwise use the legacy readline implementation.</param>
/// <param name="bundledModulePath">A custom path to the expected bundled modules.</param>
public HostStartupInfo(
string name,
string profileId,
Expand All @@ -147,7 +153,8 @@ public HostStartupInfo(
string logPath,
int logLevel,
bool consoleReplEnabled,
bool usesLegacyReadLine)
bool usesLegacyReadLine,
string bundledModulePath)
{
Name = name ?? DefaultHostName;
ProfileId = profileId ?? DefaultHostProfileId;
Expand All @@ -161,6 +168,7 @@ public HostStartupInfo(
LogLevel = logLevel;
ConsoleReplEnabled = consoleReplEnabled;
UsesLegacyReadLine = usesLegacyReadLine;
BundledModulePath = bundledModulePath;
}

#endregion
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using System.Management.Automation.Remoting;
using System.Management.Automation.Runspaces;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
Expand All @@ -32,10 +33,18 @@ namespace Microsoft.PowerShell.EditorServices.Services
/// </summary>
internal class PowerShellContextService : IHostSupportsInteractiveSession
{
private static readonly string s_commandsModulePath = Path.GetFullPath(
Path.Combine(
Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location),
"../../Commands/PowerShellEditorServices.Commands.psd1"));
// This is a default that can be overriden at runtime by the user or tests.
private static string s_bundledModulePath = Path.GetFullPath(Path.Combine(
Path.GetDirectoryName(typeof(PowerShellContextService).Assembly.Location),
"..",
"..",
".."));

private static string s_commandsModulePath => Path.GetFullPath(Path.Combine(
s_bundledModulePath,
"PowerShellEditorServices",
"Commands",
"PowerShellEditorServices.Commands.psd1"));

private static readonly Action<Runspace, ApartmentState> s_runspaceApartmentStateSetter;
private static readonly PropertyInfo s_writeStreamProperty;
Expand Down Expand Up @@ -189,9 +198,16 @@ public static PowerShellContextService Create(
OmniSharp.Extensions.LanguageServer.Protocol.Server.ILanguageServerFacade languageServer,
HostStartupInfo hostStartupInfo)
{
var logger = factory.CreateLogger<PowerShellContextService>();

Validate.IsNotNull(nameof(hostStartupInfo), hostStartupInfo);

var logger = factory.CreateLogger<PowerShellContextService>();
// Respect a user provided bundled module path.
if (Directory.Exists(hostStartupInfo.BundledModulePath))
{
logger.LogTrace($"Using new bundled module path: {hostStartupInfo.BundledModulePath}");
s_bundledModulePath = hostStartupInfo.BundledModulePath;
}

bool shouldUsePSReadLine = hostStartupInfo.ConsoleReplEnabled
&& !hostStartupInfo.UsesLegacyReadLine;
Expand Down Expand Up @@ -281,6 +297,15 @@ public static Runspace CreateRunspace(PSHost psHost, PSLanguageMode languageMode
// should have the same LanguageMode of whatever is set by the system.
initialSessionState.LanguageMode = languageMode;

// We set the process scope's execution policy (which is really the runspace's scope) to
// Bypass so we can import our bundled modules. This is equivalent in scope to the CLI
// argument `-Bypass`, which (for instance) the extension passes. Thus we emulate this
// behavior for consistency such that unit tests can pass in a similar environment.
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
initialSessionState.ExecutionPolicy = ExecutionPolicy.Bypass;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dkattan So we resurrected the SetExecutionPolicy() function because on closer inspection, the actual issue was that in the unit test scenario we weren't having Bypass set (because the codebase's expectation is that the startup script is run with -ExecutionPolicy Bypass, like what the extension does), and the SetExecutionPolicy() actually sets the execution policy back to the original inherited scoped policy, not the process policy. That is, it override's the process/runspace's policy after everything is loaded to be equivalent to the user's machine/user policy.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rjmholt I might rename that function ResetExecutionPolicy() 😆

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That works. I'm a fan of removing large chunks of code, as long as it doesn't change expected functionality :D

}

Runspace runspace = RunspaceFactory.CreateRunspace(psHost, initialSessionState);

// Windows PowerShell must be hosted in STA mode
Expand Down Expand Up @@ -396,7 +421,7 @@ public void Initialize(

if (powerShellVersion.Major >= 5 &&
this.isPSReadLineEnabled &&
PSReadLinePromptContext.TryGetPSReadLineProxy(logger, initialRunspace, out PSReadLineProxy proxy))
PSReadLinePromptContext.TryGetPSReadLineProxy(logger, initialRunspace, s_bundledModulePath, out PSReadLineProxy proxy))
{
this.PromptContext = new PSReadLinePromptContext(
this,
Expand All @@ -420,15 +445,13 @@ public void Initialize(
/// the runspace. This method will be moved somewhere else soon.
/// </summary>
/// <returns></returns>
public Task ImportCommandsModuleAsync() => ImportCommandsModuleAsync(s_commandsModulePath);

public Task ImportCommandsModuleAsync(string path)
public Task ImportCommandsModuleAsync()
{
this.logger.LogTrace($"Importing PowershellEditorServices commands from {path}");
this.logger.LogTrace($"Importing PowershellEditorServices commands from {s_commandsModulePath}");

PSCommand importCommand = new PSCommand()
.AddCommand("Import-Module")
.AddArgument(path);
.AddArgument(s_commandsModulePath);

return this.ExecuteCommandAsync<PSObject>(importCommand, sendOutputToHost: false, sendErrorToHost: false);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,6 @@ namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext

internal class PSReadLinePromptContext : IPromptContext
{
private static readonly string _psReadLineModulePath = Path.Combine(
Path.GetDirectoryName(typeof(PSReadLinePromptContext).Assembly.Location),
"..",
"..",
"..",
#if TEST
// When using xUnit (dotnet test) the assemblies are deployed to the
// test project folder, invalidating our relative path assumption.
"..",
"..",
"module",
#endif
"PSReadLine");

private static readonly Lazy<CmdletInfo> s_lazyInvokeReadLineForEditorServicesCmdletInfo = new Lazy<CmdletInfo>(() =>
{
var type = Type.GetType("Microsoft.PowerShell.EditorServices.Commands.InvokeReadLineForEditorServicesCommand, Microsoft.PowerShell.EditorServices.Hosting");
Expand Down Expand Up @@ -79,6 +65,7 @@ internal PSReadLinePromptContext(
internal static bool TryGetPSReadLineProxy(
ILogger logger,
Runspace runspace,
string bundledModulePath,
out PSReadLineProxy readLineProxy)
{
readLineProxy = null;
Expand All @@ -87,15 +74,33 @@ internal static bool TryGetPSReadLineProxy(
{
pwsh.Runspace = runspace;
pwsh.AddCommand("Microsoft.PowerShell.Core\\Import-Module")
.AddParameter("Name", _psReadLineModulePath)
.AddParameter("Name", Path.Combine(bundledModulePath, "PSReadLine"))
.Invoke();

if (pwsh.HadErrors)
{
logger.LogWarning("PSConsoleReadline type not found: {Reason}", pwsh.Streams.Error[0].ToString());
return false;
}

var psReadLineType = Type.GetType("Microsoft.PowerShell.PSConsoleReadLine, Microsoft.PowerShell.PSReadLine2");

if (psReadLineType == null)
{
logger.LogWarning("PSConsoleReadline type not found: {Reason}", pwsh.HadErrors ? pwsh.Streams.Error[0].ToString() : "<Unknown reason>");
return false;
// NOTE: For some reason `Type.GetType(...)` can fail to find the type,
// and in that case, this search through the `AppDomain` for some reason will succeed.
// It's slower, but only happens when needed.
logger.LogTrace("PSConsoleReadline type not found using Type.GetType(), searching all loaded assemblies...");
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rjmholt This should only happen in the case that it needs to. When Type.GetType works, then this logic is skipped. So it feels fine, though I still wish I knew why it was necessary.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not saying it's "right" but I keep seeing code similar to this come up when I research the issue. I think we will get to the bottom of it as we continue to refactor but this works.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My suspicion is that this always worked in script because it always runs on the pipeline thread and through PowerShell's own type search logic. When we sift through the whole AppDomain (which is kind of a dead concept) for a type, we ignore assembly load contexts, and so that might be why the direction reflection query fails but the search succeeds. Just a theory though. An ALC shouldn't be thread-dependent though and I don't think anyone is using something like EnterContextualReflection()

psReadLineType = AppDomain.CurrentDomain
.GetAssemblies()
.FirstOrDefault(asm => asm.GetName().Name.Equals("Microsoft.PowerShell.PSReadLine2"))
?.ExportedTypes
?.FirstOrDefault(type => type.FullName.Equals("Microsoft.PowerShell.PSConsoleReadLine"));
if (psReadLineType == null)
{
logger.LogWarning("PSConsoleReadLine type not found anywhere!");
return false;
}
}

try
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,6 @@
</ItemGroup>

<ItemGroup>
<None Update="xunit.runner.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<Content Include="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
</Project>
6 changes: 4 additions & 2 deletions test/PowerShellEditorServices.Test.E2E/xunit.runner.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
{
"parallelizeTestCollections": false
"$schema": "https://xunit.net/schema/current/xunit.runner.schema.json",
"appDomain": "denied",
"parallelizeTestCollections": false,
"methodDisplay": "method"
}

6 changes: 0 additions & 6 deletions test/PowerShellEditorServices.Test/App.config

This file was deleted.

12 changes: 6 additions & 6 deletions test/PowerShellEditorServices.Test/Language/SemanticTokenTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ namespace Microsoft.PowerShell.EditorServices.Test.Language
public class SemanticTokenTest
{
[Fact]
public async Task TokenizesFunctionElements()
public void TokenizesFunctionElements()
{
string text = @"
function Get-Sum {
Expand Down Expand Up @@ -59,7 +59,7 @@ function Get-Sum {
}

[Fact]
public async Task TokenizesStringExpansion()
public void TokenizesStringExpansion()
{
string text = "Write-Host \"$(Test-Property Get-Whatever) $(Get-Whatever)\"";
ScriptFile scriptFile = new ScriptFile(
Expand All @@ -82,7 +82,7 @@ public async Task TokenizesStringExpansion()
}

[Fact]
public async Task RecognizesTokensWithAsterisk()
public void RecognizesTokensWithAsterisk()
{
string text = @"
function Get-A*A {
Expand Down Expand Up @@ -111,7 +111,7 @@ function Get-A*A {
}

[Fact]
public async Task RecognizesArrayPropertyInExpandableString()
public void RecognizesArrayPropertyInExpandableString()
{
string text = "\"$(@($Array).Count) OtherText\"";
ScriptFile scriptFile = new ScriptFile(
Expand All @@ -136,7 +136,7 @@ public async Task RecognizesArrayPropertyInExpandableString()
}

[Fact]
public async Task RecognizesCurlyQuotedString()
public void RecognizesCurlyQuotedString()
{
string text = "“^[-'a-z]*”";
ScriptFile scriptFile = new ScriptFile(
Expand All @@ -150,7 +150,7 @@ public async Task RecognizesCurlyQuotedString()
}

[Fact]
public async Task RecognizeEnum()
public void RecognizeEnum()
{
string text = @"
enum MyEnum{
Expand Down
14 changes: 10 additions & 4 deletions test/PowerShellEditorServices.Test/PowerShellContextFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@ internal static class PowerShellContextFactory
Path.GetFullPath(
TestUtilities.NormalizePath("../../../../PowerShellEditorServices.Test.Shared/ProfileTest.ps1")));

public static System.Management.Automation.Runspaces.Runspace initialRunspace;
public static readonly string BundledModulePath = Path.GetFullPath(
TestUtilities.NormalizePath("../../../../../module"));

public static System.Management.Automation.Runspaces.Runspace InitialRunspace;

public static PowerShellContextService Create(ILogger logger)
{
Expand All @@ -46,21 +49,24 @@ public static PowerShellContextService Create(ILogger logger)
TestProfilePaths,
new List<string>(),
new List<string>(),
// TODO: We want to replace this property with an entire initial session state,
// which would then also control the process-scoped execution policy.
PSLanguageMode.FullLanguage,
null,
0,
consoleReplEnabled: false,
usesLegacyReadLine: false);
usesLegacyReadLine: false,
bundledModulePath: BundledModulePath);

initialRunspace = PowerShellContextService.CreateRunspace(
InitialRunspace = PowerShellContextService.CreateRunspace(
testHostDetails,
powerShellContext,
new TestPSHostUserInterface(powerShellContext, logger),
logger);

powerShellContext.Initialize(
TestProfilePaths,
initialRunspace,
InitialRunspace,
ownsInitialRunspace: true,
consoleHost: null);

Expand Down
Loading