diff --git a/test/PowerShellEditorServices.Test.E2E/DAPTestsFixures.cs b/test/PowerShellEditorServices.Test.E2E/DAPTestsFixures.cs deleted file mode 100644 index a56cf6ff0..000000000 --- a/test/PowerShellEditorServices.Test.E2E/DAPTestsFixures.cs +++ /dev/null @@ -1,86 +0,0 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. -// - -using System; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using OmniSharp.Extensions.DebugAdapter.Client; -using OmniSharp.Extensions.DebugAdapter.Protocol.Requests; -using OmniSharp.Extensions.DebugAdapter.Protocol.Serialization; - -namespace PowerShellEditorServices.Test.E2E -{ - public class DAPTestsFixture : TestsFixture - { - public override bool IsDebugAdapterTests => true; - - public DebugAdapterClient PsesDebugAdapterClient { get; private set; } - - public TaskCompletionSource Started { get; } = new TaskCompletionSource(); - - public async override Task CustomInitializeAsync( - ILoggerFactory factory, - Stream inputStream, - Stream outputStream) - { - var initialized = new TaskCompletionSource(); - PsesDebugAdapterClient = DebugAdapterClient.Create(options => - { - options - .WithInput(inputStream) - .WithOutput(outputStream) - // The OnStarted delegate gets run when we receive the _Initialized_ event from the server: - // https://microsoft.github.io/debug-adapter-protocol/specification#Events_Initialized - .OnStarted((client, token) => { - Started.SetResult(true); - return Task.CompletedTask; - }) - // The OnInitialized delegate gets run when we first receive the _Initialize_ response: - // https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Initialize - .OnInitialized((client, request, response, token) => { - initialized.SetResult(true); - return Task.CompletedTask; - }); - }); - - // PSES follows the following flow: - // Receive a Initialize request - // Run Initialize handler and send response back - // Receive a Launch/Attach request - // Run Launch/Attach handler and send response back - // PSES sends the initialized event at the end of the Launch/Attach handler - - // The way that the Omnisharp client works is that this Initialize method doesn't return until - // after OnStarted is run... which only happens when Initialized is received from the server. - // so if we would await this task, it would deadlock. - // To get around this, we run the Initialize() without await but use a `TaskCompletionSource` - // that gets completed when we receive the response to Initialize - // This tells us that we are ready to send messages to PSES... but are not stuck waiting for - // Initialized. - PsesDebugAdapterClient.Initialize(CancellationToken.None).ConfigureAwait(false); - await initialized.Task.ConfigureAwait(false); - } - - public override async Task DisposeAsync() - { - try - { - await PsesDebugAdapterClient.RequestDisconnect(new DisconnectArguments - { - Restart = false, - TerminateDebuggee = true - }).ConfigureAwait(false); - await _psesProcess.Stop().ConfigureAwait(false); - PsesDebugAdapterClient?.Dispose(); - } - catch (ObjectDisposedException) - { - // Language client has a disposal bug in it - } - } - } -} diff --git a/test/PowerShellEditorServices.Test.E2E/DebugAdapterClientExtensions.cs b/test/PowerShellEditorServices.Test.E2E/DebugAdapterClientExtensions.cs new file mode 100644 index 000000000..e3fc8c0cd --- /dev/null +++ b/test/PowerShellEditorServices.Test.E2E/DebugAdapterClientExtensions.cs @@ -0,0 +1,46 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.IO; +using System.Reflection; +using System.Threading.Tasks; +using Microsoft.PowerShell.EditorServices.Handlers; +using Xunit; +using OmniSharp.Extensions.DebugAdapter.Client; +using OmniSharp.Extensions.DebugAdapter.Protocol.Requests; +using System.Threading; +using System.Text; +using System.Linq; +using Xunit.Abstractions; +using Microsoft.Extensions.Logging; +using OmniSharp.Extensions.DebugAdapter.Protocol.Models; + +namespace PowerShellEditorServices.Test.E2E +{ + public static class DebugAdapterClientExtensions + { + public static async Task LaunchScript(this DebugAdapterClient debugAdapterClient, string filePath, TaskCompletionSource started) + { + LaunchResponse launchResponse = await debugAdapterClient.RequestLaunch(new PsesLaunchRequestArguments + { + NoDebug = false, + Script = filePath, + Cwd = "", + CreateTemporaryIntegratedConsole = false, + }).ConfigureAwait(false); + + if (launchResponse == null) + { + throw new Exception("Launch response was null."); + } + + // This will check to see if we received the Initialized event from the server. + await Task.Run( + async () => await started.Task.ConfigureAwait(false), + new CancellationTokenSource(2000).Token).ConfigureAwait(false); + } + } +} diff --git a/test/PowerShellEditorServices.Test.E2E/DebugAdapterProtocolMessageTests.cs b/test/PowerShellEditorServices.Test.E2E/DebugAdapterProtocolMessageTests.cs index 7bd21f6b4..a70f570f5 100644 --- a/test/PowerShellEditorServices.Test.E2E/DebugAdapterProtocolMessageTests.cs +++ b/test/PowerShellEditorServices.Test.E2E/DebugAdapterProtocolMessageTests.cs @@ -5,28 +5,100 @@ using System; using System.IO; +using System.Linq; using System.Reflection; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading; using System.Threading.Tasks; -using Microsoft.PowerShell.EditorServices.Handlers; -using Xunit; +using Microsoft.Extensions.Logging; using OmniSharp.Extensions.DebugAdapter.Client; +using OmniSharp.Extensions.DebugAdapter.Protocol.Models; using OmniSharp.Extensions.DebugAdapter.Protocol.Requests; -using System.Threading; +using Xunit; +using Xunit.Abstractions; namespace PowerShellEditorServices.Test.E2E { - public class DebugAdapterProtocolMessageTests : IClassFixture + public class DebugAdapterProtocolMessageTests : IAsyncLifetime { + private const string TestOutputFileName = "__dapTestOutputFile.txt"; + private readonly static bool s_isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); private readonly static string s_binDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + private readonly static string s_testOutputPath = Path.Combine(s_binDir, TestOutputFileName); - private readonly DebugAdapterClient PsesDebugAdapterClient; - private readonly DAPTestsFixture _dapTestsFixture; + private readonly ITestOutputHelper _output; + private DebugAdapterClient PsesDebugAdapterClient; + private PsesStdioProcess _psesProcess; - public DebugAdapterProtocolMessageTests(DAPTestsFixture data) + public TaskCompletionSource Started { get; } = new TaskCompletionSource(); + + public DebugAdapterProtocolMessageTests(ITestOutputHelper output) { - _dapTestsFixture = data; - PsesDebugAdapterClient = data.PsesDebugAdapterClient; + _output = output; + } + + public async Task InitializeAsync() + { + var factory = new LoggerFactory(); + _psesProcess = new PsesStdioProcess(factory, true); + await _psesProcess.Start(); + + var initialized = new TaskCompletionSource(); + PsesDebugAdapterClient = DebugAdapterClient.Create(options => + { + options + .WithInput(_psesProcess.OutputStream) + .WithOutput(_psesProcess.InputStream) + // The OnStarted delegate gets run when we receive the _Initialized_ event from the server: + // https://microsoft.github.io/debug-adapter-protocol/specification#Events_Initialized + .OnStarted((client, token) => { + Started.SetResult(true); + return Task.CompletedTask; + }) + // The OnInitialized delegate gets run when we first receive the _Initialize_ response: + // https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Initialize + .OnInitialized((client, request, response, token) => { + initialized.SetResult(true); + return Task.CompletedTask; + }); + }); + + // PSES follows the following flow: + // Receive a Initialize request + // Run Initialize handler and send response back + // Receive a Launch/Attach request + // Run Launch/Attach handler and send response back + // PSES sends the initialized event at the end of the Launch/Attach handler + + // The way that the Omnisharp client works is that this Initialize method doesn't return until + // after OnStarted is run... which only happens when Initialized is received from the server. + // so if we would await this task, it would deadlock. + // To get around this, we run the Initialize() without await but use a `TaskCompletionSource` + // that gets completed when we receive the response to Initialize + // This tells us that we are ready to send messages to PSES... but are not stuck waiting for + // Initialized. + PsesDebugAdapterClient.Initialize(CancellationToken.None).ConfigureAwait(false); + await initialized.Task.ConfigureAwait(false); + } + + public async Task DisposeAsync() + { + try + { + await PsesDebugAdapterClient.RequestDisconnect(new DisconnectArguments + { + Restart = false, + TerminateDebuggee = true + }).ConfigureAwait(false); + await _psesProcess.Stop().ConfigureAwait(false); + PsesDebugAdapterClient?.Dispose(); + } + catch (ObjectDisposedException) + { + // Language client has a disposal bug in it + } } private string NewTestFile(string script, bool isPester = false) @@ -38,6 +110,32 @@ private string NewTestFile(string script, bool isPester = false) return filePath; } + private string GenerateScriptFromLoggingStatements(params string[] logStatements) + { + if (logStatements.Length == 0) + { + throw new ArgumentNullException("Expected at least one argument."); + } + + // Have script create/overwrite file first with `>`. + StringBuilder builder = new StringBuilder().Append('\'').Append(logStatements[0]).Append("' > '").Append(s_testOutputPath).AppendLine("'"); + for (int i = 1; i < logStatements.Length; i++) + { + // Then append to that script with `>>`. + builder.Append('\'').Append(logStatements[i]).Append("' >> '").Append(s_testOutputPath).AppendLine("'"); + } + + _output.WriteLine("Script is:"); + _output.WriteLine(builder.ToString()); + return builder.ToString(); + } + + private string[] GetLog() + { + return File.ReadLines(s_testOutputPath).ToArray(); + } + + [Trait("Category", "DAP")] [Fact] public void CanInitializeWithCorrectServerSettings() { @@ -49,24 +147,63 @@ public void CanInitializeWithCorrectServerSettings() Assert.True(PsesDebugAdapterClient.ServerSettings.SupportsSetVariable); } + [Trait("Category", "DAP")] [Fact] public async Task CanLaunchScriptWithNoBreakpointsAsync() { - string filePath = NewTestFile("'works' > \"$PSScriptRoot/testFile.txt\""); - LaunchResponse launchResponse = await PsesDebugAdapterClient.RequestLaunch(new PsesLaunchRequestArguments + string filePath = NewTestFile(GenerateScriptFromLoggingStatements("works")); + + await PsesDebugAdapterClient.LaunchScript(filePath, Started).ConfigureAwait(false); + + ConfigurationDoneResponse configDoneResponse = await PsesDebugAdapterClient.RequestConfigurationDone(new ConfigurationDoneArguments()).ConfigureAwait(false); + Assert.NotNull(configDoneResponse); + + // At this point the script should be running so lets give it time + await Task.Delay(2000).ConfigureAwait(false); + + string[] log = GetLog(); + Assert.Equal("works", log[0]); + } + + [Trait("Category", "DAP")] + [SkippableFact] + public async Task CanSetBreakpointsAsync() + { + Skip.If( + PsesStdioProcess.RunningInConstainedLanguageMode, + "You can't set breakpoints in ConstrainedLanguage mode."); + + string filePath = NewTestFile(GenerateScriptFromLoggingStatements( + "before breakpoint", + "at breakpoint", + "after breakpoint" + )); + + await PsesDebugAdapterClient.LaunchScript(filePath, Started).ConfigureAwait(false); + + // {"command":"setBreakpoints","arguments":{"source":{"name":"dfsdfg.ps1","path":"/Users/tyleonha/Code/PowerShell/Misc/foo/dfsdfg.ps1"},"lines":[2],"breakpoints":[{"line":2}],"sourceModified":false},"type":"request","seq":3} + SetBreakpointsResponse setBreakpointsResponse = await PsesDebugAdapterClient.RequestSetBreakpoints(new SetBreakpointsArguments { - NoDebug = false, - Script = filePath, - Cwd = "", - CreateTemporaryIntegratedConsole = false, + Source = new Source + { + Name = Path.GetFileName(filePath), + Path = filePath + }, + Lines = new long[] { 2 }, + Breakpoints = new SourceBreakpoint[] + { + new SourceBreakpoint + { + Line = 2, + } + }, + SourceModified = false, }).ConfigureAwait(false); - Assert.NotNull(launchResponse); - - // This will check to see if we received the Initialized event from the server. - await Task.Run( - async () => await _dapTestsFixture.Started.Task.ConfigureAwait(false), - new CancellationTokenSource(2000).Token).ConfigureAwait(false); + var breakpoint = setBreakpointsResponse.Breakpoints.First(); + Assert.True(breakpoint.Verified); + Assert.Equal(filePath, breakpoint.Source.Path, ignoreCase: s_isWindows); + Assert.Equal(2, breakpoint.Line); ConfigurationDoneResponse configDoneResponse = await PsesDebugAdapterClient.RequestConfigurationDone(new ConfigurationDoneArguments()).ConfigureAwait(false); Assert.NotNull(configDoneResponse); @@ -74,9 +211,24 @@ await Task.Run( // At this point the script should be running so lets give it time await Task.Delay(2000).ConfigureAwait(false); - string testFile = Path.Join(Path.GetDirectoryName(filePath), "testFile.txt"); - string contents = await File.ReadAllTextAsync(testFile).ConfigureAwait(false); - Assert.Equal($"works{Environment.NewLine}", contents); + string[] log = GetLog(); + Assert.Single(log, (i) => i == "before breakpoint"); + + ContinueResponse continueResponse = await PsesDebugAdapterClient.RequestContinue(new ContinueArguments + { + ThreadId = 1, + }).ConfigureAwait(true); + + Assert.NotNull(continueResponse); + + // At this point the script should be running so lets give it time + await Task.Delay(2000).ConfigureAwait(false); + + log = GetLog(); + Assert.Collection(log, + (i) => Assert.Equal("before breakpoint", i), + (i) => Assert.Equal("at breakpoint", i), + (i) => Assert.Equal("after breakpoint", i)); } } } diff --git a/test/PowerShellEditorServices.Test.E2E/LSPTestsFixures.cs b/test/PowerShellEditorServices.Test.E2E/LSPTestsFixures.cs index 6fffa344c..ebf2c4692 100644 --- a/test/PowerShellEditorServices.Test.E2E/LSPTestsFixures.cs +++ b/test/PowerShellEditorServices.Test.E2E/LSPTestsFixures.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Reflection; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; @@ -21,24 +22,31 @@ using OmniSharp.Extensions.LanguageServer.Protocol.Models; using OmniSharp.Extensions.LanguageServer.Protocol.Window; using OmniSharp.Extensions.LanguageServer.Protocol.Workspace; +using Xunit; using Xunit.Abstractions; namespace PowerShellEditorServices.Test.E2E { - public class LSPTestsFixture : TestsFixture + public class LSPTestsFixture : IAsyncLifetime { - public override bool IsDebugAdapterTests => false; + protected readonly static string s_binDir = + Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + + private const bool IsDebugAdapterTests = false; public ILanguageClient PsesLanguageClient { get; private set; } public List Diagnostics { get; set; } internal List TelemetryEvents { get; set; } public ITestOutputHelper Output { get; set; } - public async override Task CustomInitializeAsync( - ILoggerFactory factory, - Stream inputStream, - Stream outputStream) + protected PsesStdioProcess _psesProcess; + + public async Task InitializeAsync() { + var factory = new LoggerFactory(); + _psesProcess = new PsesStdioProcess(factory, IsDebugAdapterTests); + await _psesProcess.Start(); + Diagnostics = new List(); TelemetryEvents = new List(); DirectoryInfo testdir = @@ -47,8 +55,8 @@ public async override Task CustomInitializeAsync( PsesLanguageClient = LanguageClient.PreInit(options => { options - .WithInput(inputStream) - .WithOutput(outputStream) + .WithInput(_psesProcess.OutputStream) + .WithOutput(_psesProcess.InputStream) .WithRootUri(DocumentUri.FromFileSystemPath(testdir.FullName)) .OnPublishDiagnostics(diagnosticParams => Diagnostics.AddRange(diagnosticParams.Diagnostics.Where(d => d != null))) .OnLogMessage(logMessageParams => Output?.WriteLine($"{logMessageParams.Type.ToString()}: {logMessageParams.Message}")) @@ -85,7 +93,7 @@ public async override Task CustomInitializeAsync( }); } - public override async Task DisposeAsync() + public async Task DisposeAsync() { try { diff --git a/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs b/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs index 2655f2927..3b6af939b 100644 --- a/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs +++ b/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs @@ -50,7 +50,7 @@ public LanguageServerProtocolMessageTests(ITestOutputHelper output, LSPTestsFixt TelemetryEvents = data.TelemetryEvents; TelemetryEvents.Clear(); - PwshExe = TestsFixture.PwshExe; + PwshExe = PsesStdioProcess.PwshExe; } public void Dispose() @@ -114,6 +114,7 @@ private async Task WaitForTelemetryEventsAsync() } } + [Trait("Category", "LSP")] [Fact] public async Task CanSendPowerShellGetVersionRequestAsync() { @@ -132,8 +133,9 @@ PowerShellVersion details } } + [Trait("Category", "LSP")] [Fact] - public async Task CanSendWorkspaceSymbolRequest() + public async Task CanSendWorkspaceSymbolRequestAsync() { NewTestFile(@" @@ -155,11 +157,12 @@ function CanSendWorkspaceSymbolRequest { Assert.Equal("CanSendWorkspaceSymbolRequest { }", symbol.Name); } + [Trait("Category", "LSP")] [SkippableFact] - public async Task CanReceiveDiagnosticsFromFileOpen() + public async Task CanReceiveDiagnosticsFromFileOpenAsync() { Skip.If( - TestsFixture.RunningInConstainedLanguageMode && TestsFixture.IsWindowsPowerShell, + PsesStdioProcess.RunningInConstainedLanguageMode && PsesStdioProcess.IsWindowsPowerShell, "Windows PowerShell doesn't trust PSScriptAnalyzer by default so it won't load."); NewTestFile("$a = 4"); @@ -169,8 +172,9 @@ public async Task CanReceiveDiagnosticsFromFileOpen() Assert.Equal("PSUseDeclaredVarsMoreThanAssignments", diagnostic.Code); } + [Trait("Category", "LSP")] [Fact] - public async Task WontReceiveDiagnosticsFromFileOpenThatIsNotPowerShell() + public async Task WontReceiveDiagnosticsFromFileOpenThatIsNotPowerShellAsync() { NewTestFile("$a = 4", languageId: "plaintext"); await Task.Delay(2000); @@ -178,11 +182,12 @@ public async Task WontReceiveDiagnosticsFromFileOpenThatIsNotPowerShell() Assert.Empty(Diagnostics); } + [Trait("Category", "LSP")] [SkippableFact] public async Task CanReceiveDiagnosticsFromFileChangedAsync() { Skip.If( - TestsFixture.RunningInConstainedLanguageMode && TestsFixture.IsWindowsPowerShell, + PsesStdioProcess.RunningInConstainedLanguageMode && PsesStdioProcess.IsWindowsPowerShell, "Windows PowerShell doesn't trust PSScriptAnalyzer by default so it won't load."); string filePath = NewTestFile("$a = 4"); @@ -230,11 +235,12 @@ public async Task CanReceiveDiagnosticsFromFileChangedAsync() Assert.Equal("PSUseDeclaredVarsMoreThanAssignments", diagnostic.Code); } + [Trait("Category", "LSP")] [SkippableFact] public async Task CanReceiveDiagnosticsFromConfigurationChangeAsync() { Skip.If( - TestsFixture.RunningInConstainedLanguageMode && TestsFixture.IsWindowsPowerShell, + PsesStdioProcess.RunningInConstainedLanguageMode && PsesStdioProcess.IsWindowsPowerShell, "Windows PowerShell doesn't trust PSScriptAnalyzer by default so it won't load."); NewTestFile("gci | % { $_ }"); @@ -290,6 +296,7 @@ public async Task CanReceiveDiagnosticsFromConfigurationChangeAsync() Assert.Empty(TelemetryEvents.Where(e => e.EventName == "NonDefaultPsesFeatureConfiguration")); } + [Trait("Category", "LSP")] [Fact] public async Task CanSendFoldingRangeRequestAsync() { @@ -331,11 +338,12 @@ await PsesLanguageClient }); } + [Trait("Category", "LSP")] [SkippableFact] public async Task CanSendFormattingRequestAsync() { Skip.If( - TestsFixture.RunningInConstainedLanguageMode && TestsFixture.IsWindowsPowerShell, + PsesStdioProcess.RunningInConstainedLanguageMode && PsesStdioProcess.IsWindowsPowerShell, "Windows PowerShell doesn't trust PSScriptAnalyzer by default so it won't load."); string scriptPath = NewTestFile(@" @@ -368,11 +376,12 @@ public async Task CanSendFormattingRequestAsync() Assert.Contains("\t", textEdit.NewText); } + [Trait("Category", "LSP")] [SkippableFact] public async Task CanSendRangeFormattingRequestAsync() { Skip.If( - TestsFixture.RunningInConstainedLanguageMode && TestsFixture.IsWindowsPowerShell, + PsesStdioProcess.RunningInConstainedLanguageMode && PsesStdioProcess.IsWindowsPowerShell, "Windows PowerShell doesn't trust PSScriptAnalyzer by default so it won't load."); string scriptPath = NewTestFile(@" @@ -418,6 +427,7 @@ public async Task CanSendRangeFormattingRequestAsync() Assert.Contains("\t", textEdit.NewText); } + [Trait("Category", "LSP")] [Fact] public async Task CanSendDocumentSymbolRequestAsync() { @@ -453,6 +463,7 @@ await PsesLanguageClient }); } + [Trait("Category", "LSP")] [Fact] public async Task CanSendReferencesRequestAsync() { @@ -505,6 +516,7 @@ function CanSendReferencesRequest { }); } + [Trait("Category", "LSP")] [Fact] public async Task CanSendDocumentHighlightRequestAsync() { @@ -552,6 +564,7 @@ await PsesLanguageClient }); } + [Trait("Category", "LSP")] [Fact] public async Task CanSendPowerShellGetPSHostProcessesRequestAsync() { @@ -593,6 +606,7 @@ await PsesLanguageClient Assert.NotEmpty(pSHostProcessResponses); } + [Trait("Category", "LSP")] [Fact] public async Task CanSendPowerShellGetRunspaceRequestAsync() { @@ -636,6 +650,7 @@ await PsesLanguageClient Assert.NotEmpty(runspaceResponses); } + [Trait("Category", "LSP")] [Fact] public async Task CanSendPesterLegacyCodeLensRequestAsync() { @@ -701,6 +716,7 @@ public async Task CanSendPesterLegacyCodeLensRequestAsync() }); } + [Trait("Category", "LSP")] [Fact] public async Task CanSendPesterCodeLensRequestAsync() { @@ -810,6 +826,7 @@ public async Task CanSendPesterCodeLensRequestAsync() }); } + [Trait("Category", "LSP")] [Fact] public async Task CanSendReferencesCodeLensRequestAsync() { @@ -848,11 +865,12 @@ function CanSendReferencesCodeLensRequest { Assert.Equal("1 reference", codeLensResolveResult.Command.Title); } + [Trait("Category", "LSP")] [SkippableFact] public async Task CanSendCodeActionRequestAsync() { Skip.If( - TestsFixture.RunningInConstainedLanguageMode && TestsFixture.IsWindowsPowerShell, + PsesStdioProcess.RunningInConstainedLanguageMode && PsesStdioProcess.IsWindowsPowerShell, "Windows PowerShell doesn't trust PSScriptAnalyzer by default so it won't load."); string filePath = NewTestFile("gci"); @@ -905,6 +923,7 @@ await PsesLanguageClient }); } + [Trait("Category", "LSP")] [Fact] public async Task CanSendCompletionAndCompletionResolveRequestAsync() { @@ -930,6 +949,7 @@ public async Task CanSendCompletionAndCompletionResolveRequestAsync() Assert.Contains("Writes customized output to a host", updatedCompletionItem.Documentation.String); } + [Trait("Category", "LSP")] [Fact] public async Task CanSendCompletionResolveWithModulePrefixRequestAsync() { @@ -964,6 +984,7 @@ await PsesLanguageClient Assert.Contains("Extracts files from a specified archive", updatedCompletionItem.Documentation.String); } + [Trait("Category", "LSP")] [Fact] public async Task CanSendHoverRequestAsync() { @@ -992,6 +1013,7 @@ public async Task CanSendHoverRequestAsync() }); } + [Trait("Category", "LSP")] [Fact] public async Task CanSendSignatureHelpRequestAsync() { @@ -1017,6 +1039,7 @@ public async Task CanSendSignatureHelpRequestAsync() Assert.Contains("Get-Date", signatureHelp.Signatures.First().Label); } + [Trait("Category", "LSP")] [Fact] public async Task CanSendDefinitionRequestAsync() { @@ -1055,10 +1078,11 @@ await PsesLanguageClient Assert.Equal(33, locationOrLocationLink.Location.Range.End.Character); } + [Trait("Category", "LSP")] [SkippableFact] public async Task CanSendGetProjectTemplatesRequestAsync() { - Skip.If(TestsFixture.RunningInConstainedLanguageMode, "Plaster doesn't work in ConstrainedLanguage mode."); + Skip.If(PsesStdioProcess.RunningInConstainedLanguageMode, "Plaster doesn't work in ConstrainedLanguage mode."); GetProjectTemplatesResponse getProjectTemplatesResponse = await PsesLanguageClient @@ -1081,11 +1105,12 @@ await PsesLanguageClient }); } + [Trait("Category", "LSP")] [SkippableFact] public async Task CanSendGetCommentHelpRequestAsync() { Skip.If( - TestsFixture.RunningInConstainedLanguageMode && TestsFixture.IsWindowsPowerShell, + PsesStdioProcess.RunningInConstainedLanguageMode && PsesStdioProcess.IsWindowsPowerShell, "Windows PowerShell doesn't trust PSScriptAnalyzer by default so it won't load."); string scriptPath = NewTestFile(@" @@ -1121,6 +1146,7 @@ await PsesLanguageClient Assert.Contains("myParam", commentHelpRequestResult.Content[7]); } + [Trait("Category", "LSP")] [Fact] public async Task CanSendEvaluateRequestAsync() { @@ -1139,6 +1165,7 @@ await PsesLanguageClient Assert.Equal(0, evaluateResponseBody.VariablesReference); } + [Trait("Category", "LSP")] [Fact] public async Task CanSendGetCommandRequestAsync() { @@ -1152,11 +1179,12 @@ await PsesLanguageClient Assert.True(pSCommandMessages.Count > 20); } + [Trait("Category", "LSP")] [SkippableFact] public async Task CanSendExpandAliasRequestAsync() { Skip.If( - TestsFixture.RunningInConstainedLanguageMode, + PsesStdioProcess.RunningInConstainedLanguageMode, "This feature currently doesn't support ConstrainedLanguage Mode."); ExpandAliasResult expandAliasResult = @@ -1172,6 +1200,7 @@ await PsesLanguageClient Assert.Equal("Get-ChildItem", expandAliasResult.Text); } + [Trait("Category", "LSP")] [Fact] public async Task CanSendSemanticTokenRequestAsync() { diff --git a/test/PowerShellEditorServices.Test.E2E/TestsFixture.cs b/test/PowerShellEditorServices.Test.E2E/Processes/PsesStdioProcess.cs similarity index 67% rename from test/PowerShellEditorServices.Test.E2E/TestsFixture.cs rename to test/PowerShellEditorServices.Test.E2E/Processes/PsesStdioProcess.cs index 4fee3ef36..616d2b20f 100644 --- a/test/PowerShellEditorServices.Test.E2E/TestsFixture.cs +++ b/test/PowerShellEditorServices.Test.E2E/Processes/PsesStdioProcess.cs @@ -1,19 +1,22 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Reflection; -using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using Xunit; namespace PowerShellEditorServices.Test.E2E { - public abstract class TestsFixture : IAsyncLifetime + /// + /// A is responsible for launching or attaching to a language server, providing access to its input and output streams, and tracking its lifetime. + /// + public class PsesStdioProcess : StdioServerProcess { protected readonly static string s_binDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + #region private static or constants members + private readonly static string s_bundledModulePath = new FileInfo(Path.Combine( s_binDir, "..", "..", "..", "..", "..", @@ -32,30 +35,46 @@ public abstract class TestsFixture : IAsyncLifetime const string s_hostName = "TestHost"; const string s_hostProfileId = "TestHost"; const string s_hostVersion = "1.0.0"; - readonly static string[] s_additionalModules = { "PowerShellEditorServices.VSCode" }; + private readonly static string[] s_additionalModules = { "PowerShellEditorServices.VSCode" }; - protected StdioServerProcess _psesProcess; + #endregion - public static string PwshExe { get; } = Environment.GetEnvironmentVariable("PWSH_EXE_NAME") ?? "pwsh"; + #region public static properties + public static string PwshExe { get; } = Environment.GetEnvironmentVariable("PWSH_EXE_NAME") ?? "pwsh"; public static bool IsWindowsPowerShell { get; } = PwshExe.Contains("powershell"); public static bool RunningInConstainedLanguageMode { get; } = Environment.GetEnvironmentVariable("__PSLockdownPolicy", EnvironmentVariableTarget.Machine) != null; - public virtual bool IsDebugAdapterTests { get; set; } + #endregion - public async Task InitializeAsync() + #region ctor + + public PsesStdioProcess(ILoggerFactory loggerFactory, bool isDebugAdapter) : base(loggerFactory, GeneratePsesStartInfo(isDebugAdapter)) { - var factory = new LoggerFactory(); + } + + #endregion + #region helper private methods + + private static ProcessStartInfo GeneratePsesStartInfo(bool isDebugAdapter) + { ProcessStartInfo processStartInfo = new ProcessStartInfo { FileName = PwshExe }; - processStartInfo.ArgumentList.Add("-NoLogo"); - processStartInfo.ArgumentList.Add("-NoProfile"); - processStartInfo.ArgumentList.Add("-EncodedCommand"); + foreach (string arg in GeneratePsesArguments(isDebugAdapter)) + { + processStartInfo.ArgumentList.Add(arg); + } + + return processStartInfo; + } + + private static string[] GeneratePsesArguments(bool isDebugAdapter) + { List args = new List { "&", @@ -72,7 +91,7 @@ public async Task InitializeAsync() "-Stdio" }; - if (IsDebugAdapterTests) + if (isDebugAdapter) { args.Add("-DebugServiceOnly"); } @@ -80,27 +99,20 @@ public async Task InitializeAsync() string base64Str = Convert.ToBase64String( System.Text.Encoding.Unicode.GetBytes(string.Join(' ', args))); - processStartInfo.ArgumentList.Add(base64Str); - - _psesProcess = new StdioServerProcess(factory, processStartInfo); - await _psesProcess.Start(); - - await CustomInitializeAsync(factory, _psesProcess.OutputStream, _psesProcess.InputStream).ConfigureAwait(false); - } - - public virtual async Task DisposeAsync() - { - await _psesProcess.Stop(); + return new string[] + { + "-NoLogo", + "-NoProfile", + "-EncodedCommand", + base64Str + }; } - public abstract Task CustomInitializeAsync( - ILoggerFactory factory, - Stream inputStream, - Stream outputStream); - private static string SingleQuoteEscape(string str) { return $"'{str.Replace("'", "''")}'"; } + + #endregion } }