initial
Some checks are pending
CI Tests / dotnet (push) Waiting to run
CI Tests / dotnet-1 (push) Waiting to run
CI Tests / dotnet-2 (push) Waiting to run
Emacs End-to-End Tests / ert (push) Waiting to run
Vim End-to-End Tests / themis (push) Waiting to run

This commit is contained in:
fwastring 2026-02-17 13:06:31 +01:00
commit baa0056244
352 changed files with 47928 additions and 0 deletions

View file

@ -0,0 +1,43 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#nullable enable
using System.Diagnostics;
using System.IO;
using System.Text;
using Nerdbank.Streams;
namespace PowerShellEditorServices.Test.E2E;
/// <summary>
/// A stream that logs all data read and written to the debug stream which is visible in the debug console when a
/// debugger is attached.
/// </summary>
internal class DebugOutputStream : MonitoringStream
{
public DebugOutputStream(Stream? underlyingStream)
: base(underlyingStream ?? new MemoryStream())
{
DidRead += (_, segment) =>
{
if (segment.Array is null) { return; }
LogData("⬅️", segment.Array, segment.Offset, segment.Count);
};
DidWrite += (_, segment) =>
{
if (segment.Array is null) { return; }
LogData("➡️", segment.Array, segment.Offset, segment.Count);
};
}
private static void LogData(string header, byte[] buffer, int offset, int count)
{
// If debugging, the raw traffic will be visible in the debug console
if (Debugger.IsAttached)
{
string data = Encoding.UTF8.GetString(buffer, offset, count);
Debug.WriteLine($"{header} {data}");
}
}
}

View file

@ -0,0 +1,24 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#nullable enable
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace PowerShellEditorServices.Test.E2E;
/// <summary>
/// Represents a debug adapter server host that can be started and stopped and provides streams for communication.
/// </summary>
public interface IAsyncLanguageServerHost : IAsyncDisposable
{
// Start the host and return when the host is ready to communicate. It should return a tuple of a stream Reader and stream Writer for communication with the LSP. The underlying streams can be retrieved via baseStream propertyif needed.
Task<(StreamReader, StreamWriter)> Start(CancellationToken token = default);
// Stops the host and returns when the host has fully stopped. It should be idempotent, such that if called while the host is already stopping/stopped, it will have the same result
Task<bool> Stop(CancellationToken token = default);
// Optional to implement if more is required than a simple stop
async ValueTask IAsyncDisposable.DisposeAsync() => await Stop();
}

View file

@ -0,0 +1,27 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
using System.Threading.Tasks;
using Microsoft.PowerShell.EditorServices.Handlers;
using OmniSharp.Extensions.DebugAdapter.Protocol.Client;
using OmniSharp.Extensions.DebugAdapter.Protocol.Requests;
namespace PowerShellEditorServices.Test.E2E
{
public static class IDebugAdapterClientExtensions
{
public static async Task LaunchScript(this IDebugAdapterClient debugAdapterClient, string script, string executeMode = "DotSource")
{
_ = await debugAdapterClient.Launch(
new PsesLaunchRequestArguments
{
NoDebug = false,
Script = script,
Cwd = "",
CreateTemporaryIntegratedConsole = false,
ExecuteMode = executeMode,
}) ?? throw new Exception("Launch response was null.");
}
}
}

View file

@ -0,0 +1,84 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
namespace PowerShellEditorServices.Test.E2E;
/// <summary>
/// A <see cref="ServerManager"/> is responsible for launching or attaching to a language server, providing access to its input and output streams, and tracking its lifetime.
/// </summary>
internal class PsesStdioLanguageServerProcessHost(bool isDebugAdapter)
: StdioLanguageServerProcessHost(PwshExe, GeneratePsesArguments(isDebugAdapter))
{
protected static readonly string s_binDir =
Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
private static readonly string s_bundledModulePath = new FileInfo(Path.Combine(
s_binDir, "..", "..", "..", "..", "..", "module")).FullName;
private static readonly string s_sessionDetailsPath = Path.Combine(
s_binDir, $"pses_test_sessiondetails_{Path.GetRandomFileName()}");
private static readonly string s_logPath = Path.Combine(
s_binDir, $"pses_test_logs_{Path.GetRandomFileName()}");
private const string s_logLevel = "Diagnostic";
private static readonly string[] s_featureFlags = { "PSReadLine" };
private const string s_hostName = "TestHost";
private const string s_hostProfileId = "TestHost";
private const string s_hostVersion = "1.0.0";
// Adjust the environment variable if wanting to test with 5.1 or a specific pwsh path
public static string PwshExe { get; } = Environment.GetEnvironmentVariable("PWSH_EXE_NAME") ?? "pwsh";
public static bool IsWindowsPowerShell { get; } = PwshExe.EndsWith("powershell");
public static bool RunningInConstrainedLanguageMode { get; } =
Environment.GetEnvironmentVariable("__PSLockdownPolicy", EnvironmentVariableTarget.Machine) != null;
private static string[] GeneratePsesArguments(bool isDebugAdapter)
{
List<string> args = new()
{
"&",
SingleQuoteEscape(Path.Combine(s_bundledModulePath, "PowerShellEditorServices", "Start-EditorServices.ps1")),
"-LogPath",
SingleQuoteEscape(s_logPath),
"-LogLevel",
s_logLevel,
"-SessionDetailsPath",
SingleQuoteEscape(s_sessionDetailsPath),
"-FeatureFlags",
string.Join(',', s_featureFlags),
"-HostName",
s_hostName,
"-HostProfileId",
s_hostProfileId,
"-HostVersion",
s_hostVersion,
"-BundledModulesPath",
SingleQuoteEscape(s_bundledModulePath),
"-Stdio"
};
if (isDebugAdapter)
{
args.Add("-DebugServiceOnly");
}
string base64Str = Convert.ToBase64String(
System.Text.Encoding.Unicode.GetBytes(string.Join(' ', args)));
return
[
"-NoLogo",
"-NoProfile",
"-EncodedCommand",
base64Str
];
}
private static string SingleQuoteEscape(string str) => $"'{str.Replace("'", "''")}'";
}

View file

@ -0,0 +1,116 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#nullable enable
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace PowerShellEditorServices.Test.E2E;
/// <summary>
/// Hosts a language server process that communicates over stdio
/// </summary>
internal class StdioLanguageServerProcessHost(string fileName, IEnumerable<string> argumentList) : IAsyncLanguageServerHost
{
// The PSES process that will be started and managed
private readonly Process process = new()
{
EnableRaisingEvents = true,
StartInfo = new ProcessStartInfo(fileName, argumentList)
{
RedirectStandardInput = true,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
}
};
// Track the state of the startup
private TaskCompletionSource<(StreamReader, StreamWriter)>? startTcs;
private TaskCompletionSource<bool>? stopTcs;
// Starts the process. Returns when the process has started and streams are available.
public async Task<(StreamReader, StreamWriter)> Start(CancellationToken token = default)
{
// Runs this once upon process exit to clean up the state.
EventHandler? exitHandler = null;
exitHandler = (sender, e) =>
{
// Complete the stopTcs task when the process finally exits, allowing stop to complete
stopTcs?.TrySetResult(true);
stopTcs = null;
startTcs = null;
process.Exited -= exitHandler;
};
process.Exited += exitHandler;
if (stopTcs is not null)
{
throw new InvalidOperationException("The process is currently stopping and cannot be started.");
}
// Await the existing task if we have already started, making this operation idempotent
if (startTcs is not null)
{
return await startTcs.Task;
}
// Initiate a new startTcs to track the startup
startTcs = new();
token.ThrowIfCancellationRequested();
// Should throw if there are any startup problems such as invalid path, etc.
process.Start();
// According to the source the streams should be allocated synchronously after the process has started, however it's not super clear so we will put this here in case there is an explicit race condition.
if (process.StandardInput.BaseStream is null || process.StandardOutput.BaseStream is null)
{
throw new InvalidOperationException("The process has started but the StandardInput or StandardOutput streams are not available. This should never happen and is probably a race condition, please report it to PowerShellEditorServices.");
}
startTcs.SetResult((
process.StandardOutput,
process.StandardInput
));
// Return the result of the completion task
return await startTcs.Task;
}
public async Task WaitForExit(CancellationToken token = default)
{
AssertStarting();
await process.WaitForExitAsync(token);
}
/// <summary>
/// Determines if the process is in the starting state and throws if not.
/// </summary>
private void AssertStarting()
{
if (startTcs is null)
{
throw new InvalidOperationException("The process is not starting/started, use Start() first.");
}
}
public async Task<bool> Stop(CancellationToken token = default)
{
AssertStarting();
if (stopTcs is not null)
{
return await stopTcs.Task;
}
stopTcs = new();
token.ThrowIfCancellationRequested();
process.Kill();
await process.WaitForExitAsync(token);
return true;
}
}