From 361b29e47edce28af54c07d92923ab59ac378081 Mon Sep 17 00:00:00 2001 From: Andrew Schwartzmeyer Date: Tue, 6 Jul 2021 14:56:09 -0700 Subject: [PATCH 1/7] Fix unawaited `async Task` tests by changing to `void` --- .../Language/SemanticTokenTest.cs | 12 ++++++------ .../Session/PowerShellContextTests.cs | 3 +-- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/test/PowerShellEditorServices.Test/Language/SemanticTokenTest.cs b/test/PowerShellEditorServices.Test/Language/SemanticTokenTest.cs index 42fbaacb2..2111a5e88 100644 --- a/test/PowerShellEditorServices.Test/Language/SemanticTokenTest.cs +++ b/test/PowerShellEditorServices.Test/Language/SemanticTokenTest.cs @@ -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 { @@ -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( @@ -82,7 +82,7 @@ public async Task TokenizesStringExpansion() } [Fact] - public async Task RecognizesTokensWithAsterisk() + public void RecognizesTokensWithAsterisk() { string text = @" function Get-A*A { @@ -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( @@ -136,7 +136,7 @@ public async Task RecognizesArrayPropertyInExpandableString() } [Fact] - public async Task RecognizesCurlyQuotedString() + public void RecognizesCurlyQuotedString() { string text = "“^[-'a-z]*”"; ScriptFile scriptFile = new ScriptFile( @@ -150,7 +150,7 @@ public async Task RecognizesCurlyQuotedString() } [Fact] - public async Task RecognizeEnum() + public void RecognizeEnum() { string text = @" enum MyEnum{ diff --git a/test/PowerShellEditorServices.Test/Session/PowerShellContextTests.cs b/test/PowerShellEditorServices.Test/Session/PowerShellContextTests.cs index 4cf1fef8c..9d7c3270b 100644 --- a/test/PowerShellEditorServices.Test/Session/PowerShellContextTests.cs +++ b/test/PowerShellEditorServices.Test/Session/PowerShellContextTests.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.IO; using System.Linq; using System.Management.Automation; using System.Runtime.InteropServices; @@ -149,7 +148,7 @@ await this.powerShellContext.ExecuteCommandAsync( [Trait("Category", "PSReadLine")] [SkippableFact] - public async Task CanGetPSReadLineProxy() + public void CanGetPSReadLineProxy() { Skip.If(IsWindows, "This test doesn't work on Windows for some reason."); Assert.True(PSReadLinePromptContext.TryGetPSReadLineProxy( From 3c4453d20cd531e3f46e968704db1370a884e1e6 Mon Sep 17 00:00:00 2001 From: Andrew Schwartzmeyer Date: Tue, 6 Jul 2021 16:39:54 -0700 Subject: [PATCH 2/7] Revert "Define `TEST` constant when running tests" This reverts commit 75a71757997cab5bb0175dc761263a95322dd72e. We are reverting this in favor of using a runtime condition set by the test itself because this was fragile (for one, it did not work with OmniSharp running tests). --- PowerShellEditorServices.Common.props | 1 - PowerShellEditorServices.build.ps1 | 10 +++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/PowerShellEditorServices.Common.props b/PowerShellEditorServices.Common.props index 2ab30ebc7..cdb72f19d 100644 --- a/PowerShellEditorServices.Common.props +++ b/PowerShellEditorServices.Common.props @@ -11,6 +11,5 @@ git https://github.com/PowerShell/PowerShellEditorServices portable - $(DefineConstants);$(ExtraDefineConstants) diff --git a/PowerShellEditorServices.build.ps1 b/PowerShellEditorServices.build.ps1 index 8df6c5fc1..fbd126708 100644 --- a/PowerShellEditorServices.build.ps1 +++ b/PowerShellEditorServices.build.ps1 @@ -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) } } } @@ -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); } From 6d5e7095f642d313092aec4e5f4dda43d0f507ab Mon Sep 17 00:00:00 2001 From: Andrew Schwartzmeyer Date: Tue, 6 Jul 2021 16:41:33 -0700 Subject: [PATCH 3/7] Fix module path during testing with runtime hook --- .../Session/PSReadLinePromptContext.cs | 18 ++++++++++++------ .../Session/PowerShellContextTests.cs | 3 ++- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/PowerShellEditorServices/Services/PowerShellContext/Session/PSReadLinePromptContext.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Session/PSReadLinePromptContext.cs index 5f4931854..7dc3beab6 100644 --- a/src/PowerShellEditorServices/Services/PowerShellContext/Session/PSReadLinePromptContext.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/Session/PSReadLinePromptContext.cs @@ -22,13 +22,18 @@ internal class PSReadLinePromptContext : IPromptContext "..", "..", "..", -#if TEST - // When using xUnit (dotnet test) the assemblies are deployed to the - // test project folder, invalidating our relative path assumption. + "PSReadLine"); + + // When using xUnit (dotnet test) the assemblies are deployed to the + // test project folder, invalidating our relative path assumption. + private static readonly string _psReadLineTestModulePath = Path.Combine( + Path.GetDirectoryName(typeof(PSReadLinePromptContext).Assembly.Location), + "..", + "..", + "..", "..", "..", "module", -#endif "PSReadLine"); private static readonly Lazy s_lazyInvokeReadLineForEditorServicesCmdletInfo = new Lazy(() => @@ -79,7 +84,8 @@ internal PSReadLinePromptContext( internal static bool TryGetPSReadLineProxy( ILogger logger, Runspace runspace, - out PSReadLineProxy readLineProxy) + out PSReadLineProxy readLineProxy, + bool testing = false) { readLineProxy = null; logger.LogTrace("Attempting to load PSReadLine"); @@ -87,7 +93,7 @@ internal static bool TryGetPSReadLineProxy( { pwsh.Runspace = runspace; pwsh.AddCommand("Microsoft.PowerShell.Core\\Import-Module") - .AddParameter("Name", _psReadLineModulePath) + .AddParameter("Name", testing ? _psReadLineTestModulePath : _psReadLineModulePath) .Invoke(); var psReadLineType = Type.GetType("Microsoft.PowerShell.PSConsoleReadLine, Microsoft.PowerShell.PSReadLine2"); diff --git a/test/PowerShellEditorServices.Test/Session/PowerShellContextTests.cs b/test/PowerShellEditorServices.Test/Session/PowerShellContextTests.cs index 9d7c3270b..59f6493dc 100644 --- a/test/PowerShellEditorServices.Test/Session/PowerShellContextTests.cs +++ b/test/PowerShellEditorServices.Test/Session/PowerShellContextTests.cs @@ -154,7 +154,8 @@ public void CanGetPSReadLineProxy() Assert.True(PSReadLinePromptContext.TryGetPSReadLineProxy( NullLogger.Instance, PowerShellContextFactory.initialRunspace, - out PSReadLineProxy proxy)); + out PSReadLineProxy proxy, + true)); } #region Helper Methods From 41de2c8480fb966409f438d8ae75cd866f4e7af4 Mon Sep 17 00:00:00 2001 From: Andrew Schwartzmeyer Date: Tue, 6 Jul 2021 16:44:35 -0700 Subject: [PATCH 4/7] Set execution policy to `Bypass` when creating the initial runspace This eliminates the need to have `SetExecutionPolicy` which is a slow function that first queries a bunch of policies and then within PowerShell itself (as in, using a cmdlet) sets it to `Bypass`. However, we just want the process scope (AKA runspace scope) to have the execution policy set to `Bypass` all the time so that we can import our bundled modules, such as PSReadLine. --- .../PowerShellContext/PowerShellContextService.cs | 10 ++++++++++ .../PowerShellContextFactory.cs | 2 ++ .../Session/PowerShellContextTests.cs | 3 +-- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/PowerShellEditorServices/Services/PowerShellContext/PowerShellContextService.cs b/src/PowerShellEditorServices/Services/PowerShellContext/PowerShellContextService.cs index f226dd8d3..f371277e4 100644 --- a/src/PowerShellEditorServices/Services/PowerShellContext/PowerShellContextService.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/PowerShellContextService.cs @@ -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; @@ -281,6 +282,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; + } + Runspace runspace = RunspaceFactory.CreateRunspace(psHost, initialSessionState); // Windows PowerShell must be hosted in STA mode diff --git a/test/PowerShellEditorServices.Test/PowerShellContextFactory.cs b/test/PowerShellEditorServices.Test/PowerShellContextFactory.cs index 4eb93395a..dfa76d86e 100644 --- a/test/PowerShellEditorServices.Test/PowerShellContextFactory.cs +++ b/test/PowerShellEditorServices.Test/PowerShellContextFactory.cs @@ -46,6 +46,8 @@ public static PowerShellContextService Create(ILogger logger) TestProfilePaths, new List(), new List(), + // 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, diff --git a/test/PowerShellEditorServices.Test/Session/PowerShellContextTests.cs b/test/PowerShellEditorServices.Test/Session/PowerShellContextTests.cs index 59f6493dc..ef11d3263 100644 --- a/test/PowerShellEditorServices.Test/Session/PowerShellContextTests.cs +++ b/test/PowerShellEditorServices.Test/Session/PowerShellContextTests.cs @@ -147,10 +147,9 @@ await this.powerShellContext.ExecuteCommandAsync( } [Trait("Category", "PSReadLine")] - [SkippableFact] + [Fact] public void CanGetPSReadLineProxy() { - Skip.If(IsWindows, "This test doesn't work on Windows for some reason."); Assert.True(PSReadLinePromptContext.TryGetPSReadLineProxy( NullLogger.Instance, PowerShellContextFactory.initialRunspace, From f54d6b6b6225f6ecfca23a956580aa438ca780df Mon Sep 17 00:00:00 2001 From: Andrew Schwartzmeyer Date: Wed, 7 Jul 2021 14:49:50 -0700 Subject: [PATCH 5/7] Disable xUnit's app domains xUnit infuriatingly defaults to using its own custom app domains (and thus shadow copying of assemblies), which completely messes up our assembly locations, our ability to find modules such as PSReadLine, and resolving types from PSReadLine too. We were not actually configuing xUnit correctly either. --- .../Session/PSReadLinePromptContext.cs | 2 ++ .../PowerShellEditorServices.Test.E2E.csproj | 4 +--- .../xunit.runner.json | 6 ++++-- test/PowerShellEditorServices.Test/App.config | 6 ------ .../PowerShellEditorServices.Test.csproj | 14 ++++++++++++++ .../xunit.runner.json | 2 ++ 6 files changed, 23 insertions(+), 11 deletions(-) delete mode 100644 test/PowerShellEditorServices.Test/App.config diff --git a/src/PowerShellEditorServices/Services/PowerShellContext/Session/PSReadLinePromptContext.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Session/PSReadLinePromptContext.cs index 7dc3beab6..6a2fdcb45 100644 --- a/src/PowerShellEditorServices/Services/PowerShellContext/Session/PSReadLinePromptContext.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/Session/PSReadLinePromptContext.cs @@ -89,6 +89,7 @@ internal static bool TryGetPSReadLineProxy( { readLineProxy = null; logger.LogTrace("Attempting to load PSReadLine"); + Console.WriteLine($"Module path is {_psReadLineTestModulePath}"); using (var pwsh = PowerShell.Create()) { pwsh.Runspace = runspace; @@ -101,6 +102,7 @@ internal static bool TryGetPSReadLineProxy( if (psReadLineType == null) { logger.LogWarning("PSConsoleReadline type not found: {Reason}", pwsh.HadErrors ? pwsh.Streams.Error[0].ToString() : ""); + Console.WriteLine("Failed to GetType but no PowerShell error"); return false; } diff --git a/test/PowerShellEditorServices.Test.E2E/PowerShellEditorServices.Test.E2E.csproj b/test/PowerShellEditorServices.Test.E2E/PowerShellEditorServices.Test.E2E.csproj index 2c443e008..57a0514b6 100644 --- a/test/PowerShellEditorServices.Test.E2E/PowerShellEditorServices.Test.E2E.csproj +++ b/test/PowerShellEditorServices.Test.E2E/PowerShellEditorServices.Test.E2E.csproj @@ -22,8 +22,6 @@ - - PreserveNewest - + diff --git a/test/PowerShellEditorServices.Test.E2E/xunit.runner.json b/test/PowerShellEditorServices.Test.E2E/xunit.runner.json index 79d1ad980..3f3645a0a 100644 --- a/test/PowerShellEditorServices.Test.E2E/xunit.runner.json +++ b/test/PowerShellEditorServices.Test.E2E/xunit.runner.json @@ -1,4 +1,6 @@ { - "parallelizeTestCollections": false + "$schema": "https://xunit.net/schema/current/xunit.runner.schema.json", + "appDomain": "denied", + "parallelizeTestCollections": false, + "methodDisplay": "method" } - diff --git a/test/PowerShellEditorServices.Test/App.config b/test/PowerShellEditorServices.Test/App.config deleted file mode 100644 index 9735dc735..000000000 --- a/test/PowerShellEditorServices.Test/App.config +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/test/PowerShellEditorServices.Test/PowerShellEditorServices.Test.csproj b/test/PowerShellEditorServices.Test/PowerShellEditorServices.Test.csproj index 70115e8f7..bbf7c3814 100644 --- a/test/PowerShellEditorServices.Test/PowerShellEditorServices.Test.csproj +++ b/test/PowerShellEditorServices.Test/PowerShellEditorServices.Test.csproj @@ -1,27 +1,34 @@  + net6.0;netcoreapp3.1;net461 Microsoft.PowerShell.EditorServices.Test x64 + true true + + + + + @@ -30,14 +37,21 @@ + + PreserveNewest + + + + + $(DefineConstants);CoreCLR diff --git a/test/PowerShellEditorServices.Test/xunit.runner.json b/test/PowerShellEditorServices.Test/xunit.runner.json index 0b4dfc597..f8b76c8fc 100644 --- a/test/PowerShellEditorServices.Test/xunit.runner.json +++ b/test/PowerShellEditorServices.Test/xunit.runner.json @@ -1,4 +1,6 @@ { + "$schema": "https://xunit.net/schema/current/xunit.runner.schema.json", + "appDomain": "denied", "parallelizeTestCollections": false, "methodDisplay": "method" } From 773e8b6229d82b493de728ae38d747d134e1954b Mon Sep 17 00:00:00 2001 From: Darren Kattan Date: Wed, 7 Jul 2021 16:30:24 -0700 Subject: [PATCH 6/7] Search all assemblies for `PSConsoleReadLine` This is sometimes necessary (especially in CI). Co-authored-by: Andrew Schwartzmeyer --- .../Session/PSReadLinePromptContext.cs | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/src/PowerShellEditorServices/Services/PowerShellContext/Session/PSReadLinePromptContext.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Session/PSReadLinePromptContext.cs index 6a2fdcb45..b811a0771 100644 --- a/src/PowerShellEditorServices/Services/PowerShellContext/Session/PSReadLinePromptContext.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/Session/PSReadLinePromptContext.cs @@ -89,7 +89,6 @@ internal static bool TryGetPSReadLineProxy( { readLineProxy = null; logger.LogTrace("Attempting to load PSReadLine"); - Console.WriteLine($"Module path is {_psReadLineTestModulePath}"); using (var pwsh = PowerShell.Create()) { pwsh.Runspace = runspace; @@ -97,13 +96,30 @@ internal static bool TryGetPSReadLineProxy( .AddParameter("Name", testing ? _psReadLineTestModulePath : _psReadLineModulePath) .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() : ""); - Console.WriteLine("Failed to GetType but no PowerShell error"); - 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..."); + 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 From f911ef1f6c933fbc1700ac9eafdf9bd0eddea405 Mon Sep 17 00:00:00 2001 From: Darren Kattan Date: Thu, 8 Jul 2021 16:12:13 -0700 Subject: [PATCH 7/7] Respect `BundledModulePath` user option This also simplifies our testing scenario where that path needs to be configured at runtime, too. Co-authored-by: Andrew Schwartzmeyer --- .../Internal/EditorServicesRunner.cs | 3 +- .../Hosting/HostStartupInfo.cs | 10 +++++- .../PowerShellContextService.cs | 35 +++++++++++++------ .../Session/PSReadLinePromptContext.cs | 25 ++----------- .../PowerShellContextFactory.cs | 12 ++++--- .../Session/PowerShellContextTests.cs | 6 ++-- 6 files changed, 49 insertions(+), 42 deletions(-) diff --git a/src/PowerShellEditorServices.Hosting/Internal/EditorServicesRunner.cs b/src/PowerShellEditorServices.Hosting/Internal/EditorServicesRunner.cs index b4dbf7c63..e49da8527 100644 --- a/src/PowerShellEditorServices.Hosting/Internal/EditorServicesRunner.cs +++ b/src/PowerShellEditorServices.Hosting/Internal/EditorServicesRunner.cs @@ -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() diff --git a/src/PowerShellEditorServices/Hosting/HostStartupInfo.cs b/src/PowerShellEditorServices/Hosting/HostStartupInfo.cs index 5f3ae7647..9fc788e0d 100644 --- a/src/PowerShellEditorServices/Hosting/HostStartupInfo.cs +++ b/src/PowerShellEditorServices/Hosting/HostStartupInfo.cs @@ -107,6 +107,11 @@ public sealed class HostStartupInfo /// public int LogLevel { get; } + /// + /// The path to find the bundled modules. User configurable for advanced usage. + /// + public string BundledModulePath { get; } + #endregion #region Constructors @@ -135,6 +140,7 @@ public sealed class HostStartupInfo /// The minimum log event level. /// Enable console if true. /// Use PSReadLine if false, otherwise use the legacy readline implementation. + /// A custom path to the expected bundled modules. public HostStartupInfo( string name, string profileId, @@ -147,7 +153,8 @@ public HostStartupInfo( string logPath, int logLevel, bool consoleReplEnabled, - bool usesLegacyReadLine) + bool usesLegacyReadLine, + string bundledModulePath) { Name = name ?? DefaultHostName; ProfileId = profileId ?? DefaultHostProfileId; @@ -161,6 +168,7 @@ public HostStartupInfo( LogLevel = logLevel; ConsoleReplEnabled = consoleReplEnabled; UsesLegacyReadLine = usesLegacyReadLine; + BundledModulePath = bundledModulePath; } #endregion diff --git a/src/PowerShellEditorServices/Services/PowerShellContext/PowerShellContextService.cs b/src/PowerShellEditorServices/Services/PowerShellContext/PowerShellContextService.cs index f371277e4..198cd26d9 100644 --- a/src/PowerShellEditorServices/Services/PowerShellContext/PowerShellContextService.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/PowerShellContextService.cs @@ -33,10 +33,18 @@ namespace Microsoft.PowerShell.EditorServices.Services /// 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 s_runspaceApartmentStateSetter; private static readonly PropertyInfo s_writeStreamProperty; @@ -190,9 +198,16 @@ public static PowerShellContextService Create( OmniSharp.Extensions.LanguageServer.Protocol.Server.ILanguageServerFacade languageServer, HostStartupInfo hostStartupInfo) { + var logger = factory.CreateLogger(); + Validate.IsNotNull(nameof(hostStartupInfo), hostStartupInfo); - var logger = factory.CreateLogger(); + // 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; @@ -406,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, @@ -430,15 +445,13 @@ public void Initialize( /// the runspace. This method will be moved somewhere else soon. /// /// - 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(importCommand, sendOutputToHost: false, sendErrorToHost: false); } diff --git a/src/PowerShellEditorServices/Services/PowerShellContext/Session/PSReadLinePromptContext.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Session/PSReadLinePromptContext.cs index b811a0771..ee4c50634 100644 --- a/src/PowerShellEditorServices/Services/PowerShellContext/Session/PSReadLinePromptContext.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/Session/PSReadLinePromptContext.cs @@ -17,25 +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), - "..", - "..", - "..", - "PSReadLine"); - - // When using xUnit (dotnet test) the assemblies are deployed to the - // test project folder, invalidating our relative path assumption. - private static readonly string _psReadLineTestModulePath = Path.Combine( - Path.GetDirectoryName(typeof(PSReadLinePromptContext).Assembly.Location), - "..", - "..", - "..", - "..", - "..", - "module", - "PSReadLine"); - private static readonly Lazy s_lazyInvokeReadLineForEditorServicesCmdletInfo = new Lazy(() => { var type = Type.GetType("Microsoft.PowerShell.EditorServices.Commands.InvokeReadLineForEditorServicesCommand, Microsoft.PowerShell.EditorServices.Hosting"); @@ -84,8 +65,8 @@ internal PSReadLinePromptContext( internal static bool TryGetPSReadLineProxy( ILogger logger, Runspace runspace, - out PSReadLineProxy readLineProxy, - bool testing = false) + string bundledModulePath, + out PSReadLineProxy readLineProxy) { readLineProxy = null; logger.LogTrace("Attempting to load PSReadLine"); @@ -93,7 +74,7 @@ internal static bool TryGetPSReadLineProxy( { pwsh.Runspace = runspace; pwsh.AddCommand("Microsoft.PowerShell.Core\\Import-Module") - .AddParameter("Name", testing ? _psReadLineTestModulePath : _psReadLineModulePath) + .AddParameter("Name", Path.Combine(bundledModulePath, "PSReadLine")) .Invoke(); if (pwsh.HadErrors) diff --git a/test/PowerShellEditorServices.Test/PowerShellContextFactory.cs b/test/PowerShellEditorServices.Test/PowerShellContextFactory.cs index dfa76d86e..41c3e4730 100644 --- a/test/PowerShellEditorServices.Test/PowerShellContextFactory.cs +++ b/test/PowerShellEditorServices.Test/PowerShellContextFactory.cs @@ -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) { @@ -52,9 +55,10 @@ public static PowerShellContextService Create(ILogger logger) null, 0, consoleReplEnabled: false, - usesLegacyReadLine: false); + usesLegacyReadLine: false, + bundledModulePath: BundledModulePath); - initialRunspace = PowerShellContextService.CreateRunspace( + InitialRunspace = PowerShellContextService.CreateRunspace( testHostDetails, powerShellContext, new TestPSHostUserInterface(powerShellContext, logger), @@ -62,7 +66,7 @@ public static PowerShellContextService Create(ILogger logger) powerShellContext.Initialize( TestProfilePaths, - initialRunspace, + InitialRunspace, ownsInitialRunspace: true, consoleHost: null); diff --git a/test/PowerShellEditorServices.Test/Session/PowerShellContextTests.cs b/test/PowerShellEditorServices.Test/Session/PowerShellContextTests.cs index ef11d3263..ba1e07611 100644 --- a/test/PowerShellEditorServices.Test/Session/PowerShellContextTests.cs +++ b/test/PowerShellEditorServices.Test/Session/PowerShellContextTests.cs @@ -152,9 +152,9 @@ public void CanGetPSReadLineProxy() { Assert.True(PSReadLinePromptContext.TryGetPSReadLineProxy( NullLogger.Instance, - PowerShellContextFactory.initialRunspace, - out PSReadLineProxy proxy, - true)); + PowerShellContextFactory.InitialRunspace, + PowerShellContextFactory.BundledModulePath, + out PSReadLineProxy proxy)); } #region Helper Methods