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,195 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Management.Automation;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.PowerShell.EditorServices.Extensions;
using Microsoft.PowerShell.EditorServices.Extensions.Services;
using Microsoft.PowerShell.EditorServices.Services.Extension;
using Microsoft.PowerShell.EditorServices.Services.PowerShell.Host;
using Microsoft.PowerShell.EditorServices.Services.TextDocument;
using Microsoft.PowerShell.EditorServices.Test;
using Microsoft.PowerShell.EditorServices.Test.Shared;
using Xunit;
namespace PowerShellEditorServices.Test.Extensions
{
[Trait("Category", "Extensions")]
public class ExtensionCommandTests : IAsyncLifetime
{
private PsesInternalHost psesHost;
private ExtensionCommandService extensionCommandService;
public async Task InitializeAsync()
{
psesHost = await PsesHostFactory.Create(NullLoggerFactory.Instance);
ExtensionService extensionService = new(
languageServer: null,
serviceProvider: null,
editorOperations: null,
executionService: psesHost);
await extensionService.InitializeAsync();
extensionCommandService = new(extensionService);
}
public async Task DisposeAsync() => await psesHost.StopAsync();
[Fact]
public async Task CanRegisterAndInvokeCommandWithCmdletName()
{
string filePath = TestUtilities.NormalizePath(@"C:\Temp\Test.ps1");
ScriptFile currentFile = ScriptFile.Create(new Uri(filePath), "This is a test file", new Version("7.0"));
EditorContext editorContext = new(
editorOperations: null,
currentFile,
new BufferPosition(line: 1, column: 1),
BufferRange.None);
EditorCommand commandAdded = null;
extensionCommandService.CommandAdded += (_, command) => commandAdded = command;
const string commandName = "test.function";
const string commandDisplayName = "Function extension";
await psesHost.ExecutePSCommandAsync(
new PSCommand().AddScript(
"function Invoke-Extension { $global:extensionValue = 5 }; " +
$"Register-EditorCommand -Name {commandName} -DisplayName \"{commandDisplayName}\" -Function Invoke-Extension"),
CancellationToken.None);
Assert.NotNull(commandAdded);
Assert.Equal(commandName, commandAdded.Name);
Assert.Equal(commandDisplayName, commandAdded.DisplayName);
// Invoke the command
await extensionCommandService.InvokeCommandAsync(commandName, editorContext);
// Assert the expected value
PSCommand psCommand = new PSCommand().AddScript("$global:extensionValue");
IEnumerable<int> results = await psesHost.ExecutePSCommandAsync<int>(psCommand, CancellationToken.None);
Assert.Equal(5, results.FirstOrDefault());
}
[Fact]
public async Task CanRegisterAndInvokeCommandWithScriptBlock()
{
string filePath = TestUtilities.NormalizePath(@"C:\Temp\Test.ps1");
ScriptFile currentFile = ScriptFile.Create(new Uri(filePath), "This is a test file", new Version("7.0"));
EditorContext editorContext = new(
editorOperations: null,
currentFile,
new BufferPosition(line: 1, column: 1),
BufferRange.None);
EditorCommand commandAdded = null;
extensionCommandService.CommandAdded += (_, command) => commandAdded = command;
const string commandName = "test.scriptblock";
const string commandDisplayName = "ScriptBlock extension";
await psesHost.ExecutePSCommandAsync(
new PSCommand()
.AddCommand("Register-EditorCommand")
.AddParameter("Name", commandName)
.AddParameter("DisplayName", commandDisplayName)
.AddParameter("ScriptBlock", ScriptBlock.Create("$global:extensionValue = 10")),
CancellationToken.None);
Assert.NotNull(commandAdded);
Assert.Equal(commandName, commandAdded.Name);
Assert.Equal(commandDisplayName, commandAdded.DisplayName);
// Invoke the command.
// TODO: What task was this cancelling?
await extensionCommandService.InvokeCommandAsync("test.scriptblock", editorContext);
// Assert the expected value
PSCommand psCommand = new PSCommand().AddScript("$global:extensionValue");
IEnumerable<int> results = await psesHost.ExecutePSCommandAsync<int>(psCommand, CancellationToken.None);
Assert.Equal(10, results.FirstOrDefault());
}
[Fact]
public async Task CanUpdateRegisteredCommand()
{
EditorCommand updatedCommand = null;
extensionCommandService.CommandUpdated += (_, command) => updatedCommand = command;
const string commandName = "test.function";
const string commandDisplayName = "Updated function extension";
// Register a command and then update it
await psesHost.ExecutePSCommandAsync(
new PSCommand().AddScript(
"function Invoke-Extension { Write-Output \"Extension output!\" }; " +
$"Register-EditorCommand -Name {commandName} -DisplayName \"Old function extension\" -Function Invoke-Extension; " +
$"Register-EditorCommand -Name {commandName} -DisplayName \"{commandDisplayName}\" -Function Invoke-Extension"),
CancellationToken.None);
// Wait for the add and update events
Assert.NotNull(updatedCommand);
Assert.Equal(commandName, updatedCommand.Name);
Assert.Equal(commandDisplayName, updatedCommand.DisplayName);
}
[Fact]
public async Task CanUnregisterCommand()
{
string filePath = TestUtilities.NormalizePath(@"C:\Temp\Test.ps1");
ScriptFile currentFile = ScriptFile.Create(new Uri(filePath), "This is a test file", new Version("7.0"));
EditorContext editorContext = new(
editorOperations: null,
currentFile,
new BufferPosition(line: 1, column: 1),
BufferRange.None);
const string commandName = "test.scriptblock";
const string commandDisplayName = "ScriptBlock extension";
EditorCommand removedCommand = null;
extensionCommandService.CommandRemoved += (_, command) => removedCommand = command;
// Add the command and wait for the add event
await psesHost.ExecutePSCommandAsync(
new PSCommand()
.AddCommand("Register-EditorCommand")
.AddParameter("Name", commandName)
.AddParameter("DisplayName", commandDisplayName)
.AddParameter("ScriptBlock", ScriptBlock.Create("Write-Output \"Extension output!\"")),
CancellationToken.None);
// Remove the command and wait for the remove event
await psesHost.ExecutePSCommandAsync(
new PSCommand().AddCommand("Unregister-EditorCommand").AddParameter("Name", commandName),
CancellationToken.None);
Assert.NotNull(removedCommand);
Assert.Equal(commandName, removedCommand.Name);
Assert.Equal(commandDisplayName, removedCommand.DisplayName);
// Ensure that the command has been unregistered
await Assert.ThrowsAsync<KeyNotFoundException>(
() => extensionCommandService.InvokeCommandAsync("test.scriptblock", editorContext));
}
[Fact]
public async Task CannotRemovePSEditorVariable()
{
ActionPreferenceStopException exception = await Assert.ThrowsAsync<ActionPreferenceStopException>(
() => psesHost.ExecutePSCommandAsync<string>(
new PSCommand().AddScript("Remove-Variable psEditor -ErrorAction Stop"),
CancellationToken.None)
);
Assert.Equal(
"The running command stopped because the preference variable \"ErrorActionPreference\" or common parameter is set to Stop: Cannot remove variable psEditor because it is constant or read-only. If the variable is read-only, try the operation again specifying the Force option.",
exception.Message);
}
}
}