initial
This commit is contained in:
commit
baa0056244
352 changed files with 47928 additions and 0 deletions
7
test/PowerShellEditorServices.Test/AssemblyInfo.cs
Normal file
7
test/PowerShellEditorServices.Test/AssemblyInfo.cs
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using Xunit;
|
||||
|
||||
// Disable test parallelization to avoid port reuse issues
|
||||
[assembly: CollectionBehavior(DisableTestParallelization = true)]
|
||||
1178
test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs
Normal file
1178
test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
# donotfind.ps1
|
||||
|
|
@ -0,0 +1 @@
|
|||
donotfind.txt
|
||||
|
|
@ -0,0 +1 @@
|
|||
# nestedmodule.psd1
|
||||
|
|
@ -0,0 +1 @@
|
|||
# nestedmodule.psm1
|
||||
|
|
@ -0,0 +1 @@
|
|||
<!-- other.cdxml -->
|
||||
|
|
@ -0,0 +1 @@
|
|||
<!-- other.ps1xml -->
|
||||
|
|
@ -0,0 +1 @@
|
|||
# other.psrc
|
||||
|
|
@ -0,0 +1 @@
|
|||
# other.pssc
|
||||
|
|
@ -0,0 +1 @@
|
|||
# rootfile.ps1
|
||||
|
|
@ -0,0 +1,152 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.PowerShell.EditorServices.Handlers;
|
||||
using Microsoft.PowerShell.EditorServices.Services;
|
||||
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 Microsoft.PowerShell.EditorServices.Test.Shared.Completion;
|
||||
using Microsoft.PowerShell.EditorServices.Utility;
|
||||
using OmniSharp.Extensions.LanguageServer.Protocol.Models;
|
||||
using Xunit;
|
||||
|
||||
namespace PowerShellEditorServices.Test.Language
|
||||
{
|
||||
[Trait("Category", "Completions")]
|
||||
public class CompletionHandlerTests : IAsyncLifetime
|
||||
{
|
||||
private PsesInternalHost psesHost;
|
||||
private WorkspaceService workspace;
|
||||
private PsesCompletionHandler completionHandler;
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
psesHost = await PsesHostFactory.Create(NullLoggerFactory.Instance);
|
||||
workspace = new WorkspaceService(NullLoggerFactory.Instance);
|
||||
completionHandler = new PsesCompletionHandler(NullLoggerFactory.Instance, psesHost, psesHost, workspace);
|
||||
}
|
||||
|
||||
public async Task DisposeAsync() => await Task.Run(psesHost.StopAsync);
|
||||
|
||||
private ScriptFile GetScriptFile(ScriptRegion scriptRegion) => workspace.GetFile(TestUtilities.GetSharedPath(scriptRegion.File));
|
||||
|
||||
private Task<CompletionResults> GetCompletionResultsAsync(ScriptRegion scriptRegion)
|
||||
{
|
||||
return completionHandler.GetCompletionsInFileAsync(
|
||||
GetScriptFile(scriptRegion),
|
||||
scriptRegion.StartLineNumber,
|
||||
scriptRegion.StartColumnNumber,
|
||||
CancellationToken.None);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CompletesCommandInFile()
|
||||
{
|
||||
(_, IEnumerable<CompletionItem> results) = await GetCompletionResultsAsync(CompleteCommandInFile.SourceDetails);
|
||||
CompletionItem actual = Assert.Single(results);
|
||||
Assert.Equal(CompleteCommandInFile.ExpectedCompletion, actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CompletesCommandFromModule()
|
||||
{
|
||||
(_, IEnumerable<CompletionItem> results) = await GetCompletionResultsAsync(CompleteCommandFromModule.SourceDetails);
|
||||
CompletionItem actual = Assert.Single(results);
|
||||
// NOTE: The tooltip varies across PowerShell and OS versions, so we ignore it.
|
||||
Assert.Equal(CompleteCommandFromModule.ExpectedCompletion, actual with { Detail = "" });
|
||||
Assert.StartsWith(CompleteCommandFromModule.GetRandomDetail, actual.Detail);
|
||||
}
|
||||
|
||||
[SkippableFact]
|
||||
public async Task CompletesTypeName()
|
||||
{
|
||||
Skip.If(VersionUtils.PSEdition == "Desktop", "Windows PowerShell has trouble with this test right now.");
|
||||
(_, IEnumerable<CompletionItem> results) = await GetCompletionResultsAsync(CompleteTypeName.SourceDetails);
|
||||
CompletionItem actual = Assert.Single(results);
|
||||
if (VersionUtils.IsNetCore)
|
||||
{
|
||||
Assert.Equal(CompleteTypeName.ExpectedCompletion, actual);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Windows PowerShell shows ArrayList as a Class.
|
||||
Assert.Equal(CompleteTypeName.ExpectedCompletion with
|
||||
{
|
||||
Kind = CompletionItemKind.Class,
|
||||
Detail = "System.Collections.ArrayList"
|
||||
}, actual);
|
||||
}
|
||||
}
|
||||
|
||||
[SkippableFact]
|
||||
public async Task CompletesNamespace()
|
||||
{
|
||||
Skip.If(VersionUtils.PSEdition == "Desktop", "Windows PowerShell has trouble with this test right now.");
|
||||
(_, IEnumerable<CompletionItem> results) = await GetCompletionResultsAsync(CompleteNamespace.SourceDetails);
|
||||
CompletionItem actual = Assert.Single(results);
|
||||
Assert.Equal(CompleteNamespace.ExpectedCompletion, actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CompletesVariableInFile()
|
||||
{
|
||||
(_, IEnumerable<CompletionItem> results) = await GetCompletionResultsAsync(CompleteVariableInFile.SourceDetails);
|
||||
CompletionItem actual = Assert.Single(results);
|
||||
Assert.Equal(CompleteVariableInFile.ExpectedCompletion, actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CompletesAttributeValue()
|
||||
{
|
||||
(_, IEnumerable<CompletionItem> results) = await GetCompletionResultsAsync(CompleteAttributeValue.SourceDetails);
|
||||
// NOTE: Since the completions come through un-ordered from PowerShell, their SortText
|
||||
// (which has an index prepended from the original order) will mis-match our assumed
|
||||
// order; hence we ignore it.
|
||||
Assert.Collection(results.OrderBy(c => c.Label),
|
||||
actual => Assert.Equal(actual with { Data = null, SortText = null }, CompleteAttributeValue.ExpectedCompletion1),
|
||||
actual => Assert.Equal(actual with { Data = null, SortText = null }, CompleteAttributeValue.ExpectedCompletion2),
|
||||
actual => Assert.Equal(actual with { Data = null, SortText = null }, CompleteAttributeValue.ExpectedCompletion3));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CompletesFilePath()
|
||||
{
|
||||
(_, IEnumerable<CompletionItem> results) = await GetCompletionResultsAsync(CompleteFilePath.SourceDetails);
|
||||
Assert.NotEmpty(results);
|
||||
CompletionItem actual = results.First();
|
||||
// Paths are system dependent so we ignore the text and just check the type and range.
|
||||
Assert.Equal(actual.TextEdit.TextEdit with { NewText = "" }, CompleteFilePath.ExpectedEdit);
|
||||
Assert.All(results, r => Assert.True(r.Kind is CompletionItemKind.File or CompletionItemKind.Folder));
|
||||
}
|
||||
|
||||
// TODO: These should be an integration tests at a higher level if/when https://github.com/PowerShell/PowerShell/pull/25108 is merged. As of today, we can't actually test this in the PS engine currently.
|
||||
[Fact]
|
||||
public void CanExtractTypeAndDescriptionFromTooltip()
|
||||
{
|
||||
string expectedType = "[string]";
|
||||
string expectedDescription = "Test String";
|
||||
string paramName = "TestParam";
|
||||
string testHelp = $"{expectedType} {paramName} - {expectedDescription}";
|
||||
Assert.True(PsesCompletionHandler.TryExtractType(testHelp, paramName, out string type, out string description));
|
||||
Assert.Equal(expectedType, type);
|
||||
Assert.Equal(expectedDescription, description);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanExtractTypeFromTooltip()
|
||||
{
|
||||
string expectedType = "[string]";
|
||||
string testHelp = $"{expectedType}";
|
||||
Assert.True(PsesCompletionHandler.TryExtractType(testHelp, string.Empty, out string type, out string description));
|
||||
Assert.Null(description);
|
||||
Assert.Equal(expectedType, type);
|
||||
}
|
||||
}
|
||||
}
|
||||
200
test/PowerShellEditorServices.Test/Language/SemanticTokenTest.cs
Normal file
200
test/PowerShellEditorServices.Test/Language/SemanticTokenTest.cs
Normal file
|
|
@ -0,0 +1,200 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Management.Automation.Language;
|
||||
using Microsoft.PowerShell.EditorServices.Handlers;
|
||||
using Microsoft.PowerShell.EditorServices.Services.TextDocument;
|
||||
using OmniSharp.Extensions.LanguageServer.Protocol;
|
||||
using OmniSharp.Extensions.LanguageServer.Protocol.Models;
|
||||
using Xunit;
|
||||
|
||||
namespace PowerShellEditorServices.Test.Language
|
||||
{
|
||||
public class SemanticTokenTest
|
||||
{
|
||||
[Fact]
|
||||
public void TokenizesFunctionElements()
|
||||
{
|
||||
const string text = @"
|
||||
function Get-Sum {
|
||||
param( [parameter()] [int]$a, [int]$b )
|
||||
:loopLabel while (0) {break loopLabel}
|
||||
return $a + $b
|
||||
}
|
||||
";
|
||||
ScriptFile scriptFile = ScriptFile.Create(
|
||||
// Use any absolute path. Even if it doesn't exist.
|
||||
DocumentUri.FromFileSystemPath(Path.Combine(Path.GetTempPath(), "TestFile.ps1")),
|
||||
text,
|
||||
Version.Parse("5.0"));
|
||||
|
||||
foreach (Token t in scriptFile.ScriptTokens)
|
||||
{
|
||||
List<SemanticToken> mappedTokens = new(PsesSemanticTokensHandler.ConvertToSemanticTokens(t));
|
||||
switch (t.Text)
|
||||
{
|
||||
case "function":
|
||||
case "param":
|
||||
case "return":
|
||||
case "while":
|
||||
case "break":
|
||||
Assert.Single(mappedTokens, sToken => SemanticTokenType.Keyword == sToken.Type);
|
||||
break;
|
||||
case "parameter":
|
||||
Assert.Single(mappedTokens, sToken => SemanticTokenType.Decorator == sToken.Type);
|
||||
break;
|
||||
case "0":
|
||||
Assert.Single(mappedTokens, sToken => SemanticTokenType.Number == sToken.Type);
|
||||
break;
|
||||
case ":loopLabel":
|
||||
Assert.Single(mappedTokens, sToken => SemanticTokenType.Label == sToken.Type);
|
||||
break;
|
||||
case "loopLabel":
|
||||
Assert.Single(mappedTokens, sToken => SemanticTokenType.Property == sToken.Type);
|
||||
break;
|
||||
case "$a":
|
||||
case "$b":
|
||||
Assert.Single(mappedTokens, sToken => SemanticTokenType.Variable == sToken.Type);
|
||||
break;
|
||||
case "[int]":
|
||||
Assert.Single(mappedTokens, sToken => SemanticTokenType.Type == sToken.Type);
|
||||
break;
|
||||
case "+":
|
||||
Assert.Single(mappedTokens, sToken => SemanticTokenType.Operator == sToken.Type);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TokenizesStringExpansion()
|
||||
{
|
||||
const string text = "Write-Host \"$(Test-Property Get-Whatever) $(Get-Whatever)\"";
|
||||
ScriptFile scriptFile = ScriptFile.Create(
|
||||
// Use any absolute path. Even if it doesn't exist.
|
||||
DocumentUri.FromFileSystemPath(Path.Combine(Path.GetTempPath(), "TestFile.ps1")),
|
||||
text,
|
||||
Version.Parse("5.0"));
|
||||
|
||||
Token commandToken = scriptFile.ScriptTokens[0];
|
||||
List<SemanticToken> mappedTokens = new(PsesSemanticTokensHandler.ConvertToSemanticTokens(commandToken));
|
||||
Assert.Single(mappedTokens, sToken => SemanticTokenType.Function == sToken.Type);
|
||||
|
||||
Token stringExpandableToken = scriptFile.ScriptTokens[1];
|
||||
mappedTokens = new List<SemanticToken>(PsesSemanticTokensHandler.ConvertToSemanticTokens(stringExpandableToken));
|
||||
Assert.Collection(mappedTokens,
|
||||
sToken => Assert.Equal(SemanticTokenType.Function, sToken.Type),
|
||||
sToken => Assert.Equal(SemanticTokenType.Function, sToken.Type)
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RecognizesTokensWithAsterisk()
|
||||
{
|
||||
const string text = @"
|
||||
function Get-A*A {
|
||||
}
|
||||
Get-A*A
|
||||
";
|
||||
ScriptFile scriptFile = ScriptFile.Create(
|
||||
// Use any absolute path. Even if it doesn't exist.
|
||||
DocumentUri.FromFileSystemPath(Path.Combine(Path.GetTempPath(), "TestFile.ps1")),
|
||||
text,
|
||||
Version.Parse("5.0"));
|
||||
|
||||
foreach (Token t in scriptFile.ScriptTokens)
|
||||
{
|
||||
List<SemanticToken> mappedTokens = new(PsesSemanticTokensHandler.ConvertToSemanticTokens(t));
|
||||
switch (t.Text)
|
||||
{
|
||||
case "function":
|
||||
Assert.Single(mappedTokens, sToken => SemanticTokenType.Keyword == sToken.Type);
|
||||
break;
|
||||
case "Get-A*A":
|
||||
if (t.TokenFlags.HasFlag(TokenFlags.CommandName))
|
||||
{
|
||||
Assert.Single(mappedTokens, sToken => SemanticTokenType.Function == sToken.Type);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RecognizesArrayPropertyInExpandableString()
|
||||
{
|
||||
const string text = "\"$(@($Array).Count) OtherText\"";
|
||||
ScriptFile scriptFile = ScriptFile.Create(
|
||||
// Use any absolute path. Even if it doesn't exist.
|
||||
DocumentUri.FromFileSystemPath(Path.Combine(Path.GetTempPath(), "TestFile.ps1")),
|
||||
text,
|
||||
Version.Parse("5.0"));
|
||||
|
||||
foreach (Token t in scriptFile.ScriptTokens)
|
||||
{
|
||||
List<SemanticToken> mappedTokens = new(PsesSemanticTokensHandler.ConvertToSemanticTokens(t));
|
||||
switch (t.Text)
|
||||
{
|
||||
case "$Array":
|
||||
Assert.Single(mappedTokens, sToken => SemanticTokenType.Variable == sToken.Type);
|
||||
break;
|
||||
case "Count":
|
||||
Assert.Single(mappedTokens, sToken => SemanticTokenType.Property == sToken.Type);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RecognizesCurlyQuotedString()
|
||||
{
|
||||
const string text = "“^[-'a-z]*”";
|
||||
ScriptFile scriptFile = ScriptFile.Create(
|
||||
// Use any absolute path. Even if it doesn't exist.
|
||||
DocumentUri.FromFileSystemPath(Path.Combine(Path.GetTempPath(), "TestFile.ps1")),
|
||||
text,
|
||||
Version.Parse("5.0"));
|
||||
|
||||
List<SemanticToken> mappedTokens = new(PsesSemanticTokensHandler.ConvertToSemanticTokens(scriptFile.ScriptTokens[0]));
|
||||
Assert.Single(mappedTokens, sToken => SemanticTokenType.String == sToken.Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RecognizeEnum()
|
||||
{
|
||||
const string text = @"
|
||||
enum MyEnum{
|
||||
one
|
||||
two
|
||||
three
|
||||
}
|
||||
";
|
||||
ScriptFile scriptFile = ScriptFile.Create(
|
||||
// Use any absolute path. Even if it doesn't exist.
|
||||
DocumentUri.FromFileSystemPath(Path.Combine(Path.GetTempPath(), "TestFile.ps1")),
|
||||
text,
|
||||
Version.Parse("5.0"));
|
||||
|
||||
foreach (Token t in scriptFile.ScriptTokens)
|
||||
{
|
||||
List<SemanticToken> mappedTokens = new(PsesSemanticTokensHandler.ConvertToSemanticTokens(t));
|
||||
switch (t.Text)
|
||||
{
|
||||
case "enum":
|
||||
Assert.Single(mappedTokens, sToken => SemanticTokenType.Keyword == sToken.Type);
|
||||
break;
|
||||
case "MyEnum":
|
||||
case "one":
|
||||
case "two":
|
||||
case "three":
|
||||
Assert.Single(mappedTokens, sToken => SemanticTokenType.Property == sToken.Type);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,993 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Management.Automation;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.PowerShell.EditorServices.Services;
|
||||
using Microsoft.PowerShell.EditorServices.Services.PowerShell.Host;
|
||||
using Microsoft.PowerShell.EditorServices.Services.PowerShell.Utility;
|
||||
using Microsoft.PowerShell.EditorServices.Services.Symbols;
|
||||
using Microsoft.PowerShell.EditorServices.Services.TextDocument;
|
||||
using Microsoft.PowerShell.EditorServices.Test;
|
||||
using Microsoft.PowerShell.EditorServices.Test.Shared;
|
||||
using Microsoft.PowerShell.EditorServices.Test.Shared.Definition;
|
||||
using Microsoft.PowerShell.EditorServices.Test.Shared.Occurrences;
|
||||
using Microsoft.PowerShell.EditorServices.Test.Shared.ParameterHint;
|
||||
using Microsoft.PowerShell.EditorServices.Test.Shared.References;
|
||||
using Microsoft.PowerShell.EditorServices.Test.Shared.SymbolDetails;
|
||||
using Microsoft.PowerShell.EditorServices.Test.Shared.Symbols;
|
||||
using OmniSharp.Extensions.LanguageServer.Protocol;
|
||||
using OmniSharp.Extensions.LanguageServer.Protocol.Models;
|
||||
using Xunit;
|
||||
|
||||
namespace PowerShellEditorServices.Test.Language
|
||||
{
|
||||
[Trait("Category", "Symbols")]
|
||||
public class SymbolsServiceTests : IAsyncLifetime
|
||||
{
|
||||
private PsesInternalHost psesHost;
|
||||
private WorkspaceService workspace;
|
||||
private SymbolsService symbolsService;
|
||||
private static readonly bool isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
psesHost = await PsesHostFactory.Create(NullLoggerFactory.Instance);
|
||||
workspace = new WorkspaceService(NullLoggerFactory.Instance);
|
||||
workspace.WorkspaceFolders.Add(new WorkspaceFolder
|
||||
{
|
||||
Uri = DocumentUri.FromFileSystemPath(TestUtilities.GetSharedPath("References"))
|
||||
});
|
||||
symbolsService = new SymbolsService(
|
||||
NullLoggerFactory.Instance,
|
||||
psesHost,
|
||||
psesHost,
|
||||
workspace,
|
||||
new ConfigurationService());
|
||||
}
|
||||
|
||||
public async Task DisposeAsync()
|
||||
{
|
||||
psesHost.StopAsync();
|
||||
CommandHelpers.s_cmdletToAliasCache.Clear();
|
||||
CommandHelpers.s_aliasToCmdletCache.Clear();
|
||||
}
|
||||
|
||||
private static void AssertIsRegion(
|
||||
ScriptRegion region,
|
||||
int startLineNumber,
|
||||
int startColumnNumber,
|
||||
int endLineNumber,
|
||||
int endColumnNumber)
|
||||
{
|
||||
Assert.Equal(startLineNumber, region.StartLineNumber);
|
||||
Assert.Equal(startColumnNumber, region.StartColumnNumber);
|
||||
Assert.Equal(endLineNumber, region.EndLineNumber);
|
||||
Assert.Equal(endColumnNumber, region.EndColumnNumber);
|
||||
}
|
||||
|
||||
private ScriptFile GetScriptFile(ScriptRegion scriptRegion) => workspace.GetFile(TestUtilities.GetSharedPath(scriptRegion.File));
|
||||
|
||||
private Task<ParameterSetSignatures> GetParamSetSignatures(ScriptRegion scriptRegion)
|
||||
{
|
||||
return symbolsService.FindParameterSetsInFileAsync(
|
||||
GetScriptFile(scriptRegion),
|
||||
scriptRegion.StartLineNumber,
|
||||
scriptRegion.StartColumnNumber);
|
||||
}
|
||||
|
||||
private async Task<IEnumerable<SymbolReference>> GetDefinitions(ScriptRegion scriptRegion)
|
||||
{
|
||||
ScriptFile scriptFile = GetScriptFile(scriptRegion);
|
||||
|
||||
// TODO: We should just use the name to find it.
|
||||
SymbolReference symbol = SymbolsService.FindSymbolAtLocation(
|
||||
scriptFile,
|
||||
scriptRegion.StartLineNumber,
|
||||
scriptRegion.StartColumnNumber);
|
||||
|
||||
Assert.NotNull(symbol);
|
||||
|
||||
IEnumerable<SymbolReference> symbols =
|
||||
await symbolsService.GetDefinitionOfSymbolAsync(scriptFile, symbol);
|
||||
|
||||
return symbols.OrderBy((i) => i.ScriptRegion.ToRange().Start);
|
||||
}
|
||||
|
||||
private async Task<SymbolReference> GetDefinition(ScriptRegion scriptRegion)
|
||||
{
|
||||
IEnumerable<SymbolReference> definitions = await GetDefinitions(scriptRegion);
|
||||
return definitions.FirstOrDefault();
|
||||
}
|
||||
|
||||
private async Task<IEnumerable<SymbolReference>> GetReferences(ScriptRegion scriptRegion)
|
||||
{
|
||||
ScriptFile scriptFile = GetScriptFile(scriptRegion);
|
||||
|
||||
SymbolReference symbol = SymbolsService.FindSymbolAtLocation(
|
||||
scriptFile,
|
||||
scriptRegion.StartLineNumber,
|
||||
scriptRegion.StartColumnNumber);
|
||||
|
||||
Assert.NotNull(symbol);
|
||||
|
||||
IEnumerable<SymbolReference> symbols =
|
||||
await symbolsService.ScanForReferencesOfSymbolAsync(symbol);
|
||||
|
||||
return symbols.OrderBy((i) => i.ScriptRegion.ToRange().Start);
|
||||
}
|
||||
|
||||
private IEnumerable<SymbolReference> GetOccurrences(ScriptRegion scriptRegion)
|
||||
{
|
||||
return SymbolsService
|
||||
.FindOccurrencesInFile(
|
||||
GetScriptFile(scriptRegion),
|
||||
scriptRegion.StartLineNumber,
|
||||
scriptRegion.StartColumnNumber)
|
||||
.OrderBy(symbol => symbol.ScriptRegion.ToRange().Start)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private IEnumerable<SymbolReference> FindSymbolsInFile(ScriptRegion scriptRegion)
|
||||
{
|
||||
return symbolsService
|
||||
.FindSymbolsInFile(GetScriptFile(scriptRegion))
|
||||
.OrderBy(symbol => symbol.ScriptRegion.ToRange().Start);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FindsParameterHintsOnCommand()
|
||||
{
|
||||
// TODO: Fix signatures to use parameters, not sets.
|
||||
ParameterSetSignatures signatures = await GetParamSetSignatures(FindsParameterSetsOnCommandData.SourceDetails);
|
||||
Assert.NotNull(signatures);
|
||||
Assert.Equal("Get-Process", signatures.CommandName);
|
||||
Assert.Equal(6, signatures.Signatures.Length);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FindsCommandForParamHintsWithSpaces()
|
||||
{
|
||||
ParameterSetSignatures signatures = await GetParamSetSignatures(FindsParameterSetsOnCommandWithSpacesData.SourceDetails);
|
||||
Assert.NotNull(signatures);
|
||||
Assert.Equal("Write-Host", signatures.CommandName);
|
||||
Assert.Single(signatures.Signatures);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FindsFunctionDefinition()
|
||||
{
|
||||
SymbolReference symbol = await GetDefinition(FindsFunctionDefinitionData.SourceDetails);
|
||||
Assert.Equal("fn My-Function", symbol.Id);
|
||||
Assert.Equal("function My-Function ($myInput)", symbol.Name);
|
||||
Assert.Equal(SymbolType.Function, symbol.Type);
|
||||
AssertIsRegion(symbol.NameRegion, 1, 10, 1, 21);
|
||||
AssertIsRegion(symbol.ScriptRegion, 1, 1, 4, 2);
|
||||
Assert.True(symbol.IsDeclaration);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FindsFunctionDefinitionForAlias()
|
||||
{
|
||||
// TODO: Eventually we should get the aliases through the AST instead of relying on them
|
||||
// being defined in the runspace.
|
||||
await psesHost.ExecutePSCommandAsync(
|
||||
new PSCommand().AddScript("Set-Alias -Name My-Alias -Value My-Function"),
|
||||
CancellationToken.None);
|
||||
|
||||
SymbolReference symbol = await GetDefinition(FindsFunctionDefinitionOfAliasData.SourceDetails);
|
||||
Assert.Equal("function My-Function ($myInput)", symbol.Name);
|
||||
Assert.Equal(SymbolType.Function, symbol.Type);
|
||||
AssertIsRegion(symbol.NameRegion, 1, 10, 1, 21);
|
||||
AssertIsRegion(symbol.ScriptRegion, 1, 1, 4, 2);
|
||||
Assert.True(symbol.IsDeclaration);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FindsReferencesOnFunction()
|
||||
{
|
||||
IEnumerable<SymbolReference> symbols = await GetReferences(FindsReferencesOnFunctionData.SourceDetails);
|
||||
Assert.Collection(symbols,
|
||||
(i) =>
|
||||
{
|
||||
Assert.Equal("fn My-Function", i.Id);
|
||||
Assert.Equal("function My-Function ($myInput)", i.Name);
|
||||
Assert.Equal(SymbolType.Function, i.Type);
|
||||
Assert.True(i.IsDeclaration);
|
||||
},
|
||||
(i) =>
|
||||
{
|
||||
Assert.Equal("fn My-Function", i.Id);
|
||||
Assert.Equal("My-Function", i.Name);
|
||||
Assert.Equal(SymbolType.Function, i.Type);
|
||||
Assert.EndsWith(FindsFunctionDefinitionInWorkspaceData.SourceDetails.File, i.FilePath);
|
||||
Assert.False(i.IsDeclaration);
|
||||
},
|
||||
(i) =>
|
||||
{
|
||||
Assert.Equal("fn My-Function", i.Id);
|
||||
Assert.Equal("My-Function", i.Name);
|
||||
Assert.Equal(SymbolType.Function, i.Type);
|
||||
Assert.False(i.IsDeclaration);
|
||||
},
|
||||
(i) =>
|
||||
{
|
||||
Assert.Equal("fn My-Function", i.Id);
|
||||
Assert.Equal("My-Function", i.Name);
|
||||
Assert.Equal(SymbolType.Function, i.Type);
|
||||
Assert.False(i.IsDeclaration);
|
||||
},
|
||||
(i) =>
|
||||
{
|
||||
Assert.Equal("fn My-Function", i.Id);
|
||||
Assert.Equal("$Function:My-Function", i.Name);
|
||||
Assert.Equal(SymbolType.Function, i.Type);
|
||||
Assert.False(i.IsDeclaration);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FindsReferenceAcrossMultiRootWorkspace()
|
||||
{
|
||||
workspace.WorkspaceFolders = new[] { "Debugging", "ParameterHints", "SymbolDetails" }
|
||||
.Select(i => new WorkspaceFolder
|
||||
{
|
||||
Uri = DocumentUri.FromFileSystemPath(TestUtilities.GetSharedPath(i))
|
||||
}).ToList();
|
||||
|
||||
SymbolReference symbol = new("fn Get-Process", SymbolType.Function);
|
||||
IEnumerable<SymbolReference> symbols = await symbolsService.ScanForReferencesOfSymbolAsync(symbol);
|
||||
Assert.Collection(symbols.OrderBy(i => i.FilePath),
|
||||
i => Assert.EndsWith("VariableTest.ps1", i.FilePath),
|
||||
i => Assert.EndsWith("ParamHints.ps1", i.FilePath),
|
||||
i => Assert.EndsWith("SymbolDetails.ps1", i.FilePath));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FindsReferencesOnFunctionIncludingAliases()
|
||||
{
|
||||
// TODO: Same as in FindsFunctionDefinitionForAlias.
|
||||
await psesHost.ExecutePSCommandAsync(
|
||||
new PSCommand().AddScript("Set-Alias -Name My-Alias -Value My-Function"),
|
||||
CancellationToken.None);
|
||||
|
||||
IEnumerable<SymbolReference> symbols = await GetReferences(FindsReferencesOnFunctionData.SourceDetails);
|
||||
|
||||
Assert.Collection(symbols,
|
||||
(i) => AssertIsRegion(i.NameRegion, 1, 10, 1, 21),
|
||||
(i) => AssertIsRegion(i.NameRegion, 3, 1, 3, 12),
|
||||
(i) => AssertIsRegion(i.NameRegion, 3, 5, 3, 16),
|
||||
(i) => AssertIsRegion(i.NameRegion, 10, 1, 10, 12),
|
||||
// The alias.
|
||||
(i) =>
|
||||
{
|
||||
AssertIsRegion(i.NameRegion, 20, 1, 20, 9);
|
||||
Assert.Equal("fn My-Alias", i.Id);
|
||||
},
|
||||
(i) => AssertIsRegion(i.NameRegion, 22, 29, 22, 52));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FindsFunctionDefinitionInWorkspace()
|
||||
{
|
||||
IEnumerable<SymbolReference> symbols = await GetDefinitions(FindsFunctionDefinitionInWorkspaceData.SourceDetails);
|
||||
SymbolReference symbol = Assert.Single(symbols);
|
||||
Assert.Equal("fn My-Function", symbol.Id);
|
||||
Assert.Equal("function My-Function ($myInput)", symbol.Name);
|
||||
Assert.True(symbol.IsDeclaration);
|
||||
Assert.EndsWith(FindsFunctionDefinitionData.SourceDetails.File, symbol.FilePath);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FindsVariableDefinition()
|
||||
{
|
||||
IEnumerable<SymbolReference> definitions = await GetDefinitions(FindsVariableDefinitionData.SourceDetails);
|
||||
SymbolReference symbol = Assert.Single(definitions); // Even though it's re-assigned
|
||||
Assert.Equal("var things", symbol.Id);
|
||||
Assert.Equal("$things", symbol.Name);
|
||||
Assert.Equal(SymbolType.Variable, symbol.Type);
|
||||
Assert.True(symbol.IsDeclaration);
|
||||
AssertIsRegion(symbol.NameRegion, 6, 1, 6, 8);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FindsTypedVariableDefinition()
|
||||
{
|
||||
IEnumerable<SymbolReference> definitions = await GetDefinitions(FindsTypedVariableDefinitionData.SourceDetails);
|
||||
SymbolReference symbol = Assert.Single(definitions);
|
||||
Assert.Equal("var hello", symbol.Id);
|
||||
Assert.Equal("$hello", symbol.Name);
|
||||
Assert.Equal(SymbolType.Variable, symbol.Type);
|
||||
Assert.True(symbol.IsDeclaration);
|
||||
AssertIsRegion(symbol.NameRegion, 24, 9, 24, 15);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FindsReferencesOnVariable()
|
||||
{
|
||||
IEnumerable<SymbolReference> symbols = await GetReferences(FindsReferencesOnVariableData.SourceDetails);
|
||||
Assert.Collection(symbols,
|
||||
(i) =>
|
||||
{
|
||||
Assert.Equal("var things", i.Id);
|
||||
Assert.Equal("$things", i.Name);
|
||||
Assert.Equal(SymbolType.Variable, i.Type);
|
||||
Assert.True(i.IsDeclaration);
|
||||
},
|
||||
(i) =>
|
||||
{
|
||||
Assert.Equal("var things", i.Id);
|
||||
Assert.Equal("$things", i.Name);
|
||||
Assert.Equal(SymbolType.Variable, i.Type);
|
||||
Assert.False(i.IsDeclaration);
|
||||
},
|
||||
(i) =>
|
||||
{
|
||||
Assert.Equal("var things", i.Id);
|
||||
Assert.Equal("$things", i.Name);
|
||||
Assert.Equal(SymbolType.Variable, i.Type);
|
||||
Assert.False(i.IsDeclaration);
|
||||
});
|
||||
|
||||
Assert.Equal(symbols, GetOccurrences(FindsOccurrencesOnVariableData.SourceDetails));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FindsOccurrencesOnFunction()
|
||||
{
|
||||
IEnumerable<SymbolReference> symbols = GetOccurrences(FindsOccurrencesOnFunctionData.SourceDetails);
|
||||
Assert.Collection(symbols,
|
||||
(i) =>
|
||||
{
|
||||
Assert.Equal("fn My-Function", i.Id);
|
||||
Assert.Equal(SymbolType.Function, i.Type);
|
||||
Assert.True(i.IsDeclaration);
|
||||
},
|
||||
(i) =>
|
||||
{
|
||||
Assert.Equal("fn My-Function", i.Id);
|
||||
Assert.Equal(SymbolType.Function, i.Type);
|
||||
Assert.False(i.IsDeclaration);
|
||||
},
|
||||
(i) =>
|
||||
{
|
||||
Assert.Equal("fn My-Function", i.Id);
|
||||
Assert.Equal(SymbolType.Function, i.Type);
|
||||
Assert.False(i.IsDeclaration);
|
||||
},
|
||||
(i) =>
|
||||
{
|
||||
Assert.Equal("fn My-Function", i.Id);
|
||||
Assert.Equal("$Function:My-Function", i.Name);
|
||||
Assert.Equal(SymbolType.Function, i.Type);
|
||||
Assert.False(i.IsDeclaration);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FindsOccurrencesOnParameter()
|
||||
{
|
||||
IEnumerable<SymbolReference> symbols = GetOccurrences(FindOccurrencesOnParameterData.SourceDetails);
|
||||
Assert.Collection(symbols,
|
||||
(i) =>
|
||||
{
|
||||
Assert.Equal("var myInput", i.Id);
|
||||
// TODO: Parameter names need work.
|
||||
Assert.Equal("(parameter) [System.Object]$myInput", i.Name);
|
||||
Assert.Equal(SymbolType.Parameter, i.Type);
|
||||
AssertIsRegion(i.NameRegion, 1, 23, 1, 31);
|
||||
Assert.True(i.IsDeclaration);
|
||||
},
|
||||
(i) =>
|
||||
{
|
||||
Assert.Equal("var myInput", i.Id);
|
||||
Assert.Equal("$myInput", i.Name);
|
||||
Assert.Equal(SymbolType.Variable, i.Type);
|
||||
AssertIsRegion(i.NameRegion, 3, 17, 3, 25);
|
||||
Assert.False(i.IsDeclaration);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FindsReferencesOnCommandWithAlias()
|
||||
{
|
||||
// NOTE: This doesn't use GetOccurrences as it's testing for aliases.
|
||||
IEnumerable<SymbolReference> symbols = await GetReferences(FindsReferencesOnBuiltInCommandWithAliasData.SourceDetails);
|
||||
Assert.Collection(symbols.Where(
|
||||
(i) => i.FilePath
|
||||
.EndsWith(FindsReferencesOnBuiltInCommandWithAliasData.SourceDetails.File)),
|
||||
(i) => Assert.Equal("fn Get-ChildItem", i.Id),
|
||||
(i) => Assert.Equal("fn gci", i.Id),
|
||||
(i) => Assert.Equal("fn dir", i.Id),
|
||||
(i) => Assert.Equal("fn Get-ChildItem", i.Id));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FindsClassDefinition()
|
||||
{
|
||||
SymbolReference symbol = await GetDefinition(FindsTypeSymbolsDefinitionData.ClassSourceDetails);
|
||||
Assert.Equal("type SuperClass", symbol.Id);
|
||||
Assert.Equal("class SuperClass { }", symbol.Name);
|
||||
Assert.Equal(SymbolType.Class, symbol.Type);
|
||||
Assert.True(symbol.IsDeclaration);
|
||||
AssertIsRegion(symbol.NameRegion, 8, 7, 8, 17);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FindsReferencesOnClass()
|
||||
{
|
||||
IEnumerable<SymbolReference> symbols = await GetReferences(FindsReferencesOnTypeSymbolsData.ClassSourceDetails);
|
||||
Assert.Collection(symbols,
|
||||
(i) =>
|
||||
{
|
||||
Assert.Equal("type SuperClass", i.Id);
|
||||
Assert.Equal("class SuperClass { }", i.Name);
|
||||
Assert.Equal(SymbolType.Class, i.Type);
|
||||
Assert.True(i.IsDeclaration);
|
||||
},
|
||||
(i) =>
|
||||
{
|
||||
Assert.Equal("type SuperClass", i.Id);
|
||||
Assert.Equal("(type) SuperClass", i.Name);
|
||||
Assert.Equal(SymbolType.Type, i.Type);
|
||||
Assert.False(i.IsDeclaration);
|
||||
});
|
||||
|
||||
Assert.Equal(symbols, GetOccurrences(FindsOccurrencesOnTypeSymbolsData.ClassSourceDetails));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FindsEnumDefinition()
|
||||
{
|
||||
SymbolReference symbol = await GetDefinition(FindsTypeSymbolsDefinitionData.EnumSourceDetails);
|
||||
Assert.Equal("type MyEnum", symbol.Id);
|
||||
Assert.Equal("enum MyEnum { }", symbol.Name);
|
||||
Assert.Equal(SymbolType.Enum, symbol.Type);
|
||||
Assert.True(symbol.IsDeclaration);
|
||||
AssertIsRegion(symbol.NameRegion, 39, 6, 39, 12);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FindsReferencesOnEnum()
|
||||
{
|
||||
IEnumerable<SymbolReference> symbols = await GetReferences(FindsReferencesOnTypeSymbolsData.EnumSourceDetails);
|
||||
Assert.Collection(symbols,
|
||||
(i) =>
|
||||
{
|
||||
Assert.Equal("type MyEnum", i.Id);
|
||||
Assert.Equal("(type) MyEnum", i.Name);
|
||||
Assert.Equal(SymbolType.Type, i.Type);
|
||||
Assert.False(i.IsDeclaration);
|
||||
},
|
||||
(i) =>
|
||||
{
|
||||
Assert.Equal("type MyEnum", i.Id);
|
||||
Assert.Equal("enum MyEnum { }", i.Name);
|
||||
Assert.Equal(SymbolType.Enum, i.Type);
|
||||
Assert.True(i.IsDeclaration);
|
||||
},
|
||||
(i) =>
|
||||
{
|
||||
Assert.Equal("type MyEnum", i.Id);
|
||||
Assert.Equal("(type) MyEnum", i.Name);
|
||||
Assert.Equal(SymbolType.Type, i.Type);
|
||||
Assert.False(i.IsDeclaration);
|
||||
},
|
||||
(i) =>
|
||||
{
|
||||
Assert.Equal("type MyEnum", i.Id);
|
||||
Assert.Equal("(type) MyEnum", i.Name);
|
||||
Assert.Equal(SymbolType.Type, i.Type);
|
||||
Assert.False(i.IsDeclaration);
|
||||
});
|
||||
|
||||
Assert.Equal(symbols, GetOccurrences(FindsOccurrencesOnTypeSymbolsData.EnumSourceDetails));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FindsTypeExpressionDefinition()
|
||||
{
|
||||
SymbolReference symbol = await GetDefinition(FindsTypeSymbolsDefinitionData.TypeExpressionSourceDetails);
|
||||
AssertIsRegion(symbol.NameRegion, 39, 6, 39, 12);
|
||||
Assert.Equal("type MyEnum", symbol.Id);
|
||||
Assert.Equal("enum MyEnum { }", symbol.Name);
|
||||
Assert.True(symbol.IsDeclaration);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FindsReferencesOnTypeExpression()
|
||||
{
|
||||
IEnumerable<SymbolReference> symbols = await GetReferences(FindsReferencesOnTypeSymbolsData.TypeExpressionSourceDetails);
|
||||
Assert.Collection(symbols,
|
||||
(i) =>
|
||||
{
|
||||
Assert.Equal("type SuperClass", i.Id);
|
||||
Assert.Equal("class SuperClass { }", i.Name);
|
||||
Assert.Equal(SymbolType.Class, i.Type);
|
||||
Assert.True(i.IsDeclaration);
|
||||
},
|
||||
(i) =>
|
||||
{
|
||||
Assert.Equal("type SuperClass", i.Id);
|
||||
Assert.Equal("(type) SuperClass", i.Name);
|
||||
Assert.Equal(SymbolType.Type, i.Type);
|
||||
Assert.False(i.IsDeclaration);
|
||||
});
|
||||
|
||||
Assert.Equal(symbols, GetOccurrences(FindsOccurrencesOnTypeSymbolsData.TypeExpressionSourceDetails));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FindsTypeConstraintDefinition()
|
||||
{
|
||||
SymbolReference symbol = await GetDefinition(FindsTypeSymbolsDefinitionData.TypeConstraintSourceDetails);
|
||||
AssertIsRegion(symbol.NameRegion, 39, 6, 39, 12);
|
||||
Assert.Equal("type MyEnum", symbol.Id);
|
||||
Assert.Equal("enum MyEnum { }", symbol.Name);
|
||||
Assert.True(symbol.IsDeclaration);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FindsReferencesOnTypeConstraint()
|
||||
{
|
||||
IEnumerable<SymbolReference> symbols = await GetReferences(FindsReferencesOnTypeSymbolsData.TypeConstraintSourceDetails);
|
||||
Assert.Collection(symbols,
|
||||
(i) =>
|
||||
{
|
||||
Assert.Equal("type MyEnum", i.Id);
|
||||
Assert.Equal("(type) MyEnum", i.Name);
|
||||
Assert.Equal(SymbolType.Type, i.Type);
|
||||
Assert.False(i.IsDeclaration);
|
||||
},
|
||||
(i) =>
|
||||
{
|
||||
Assert.Equal("type MyEnum", i.Id);
|
||||
Assert.Equal("enum MyEnum { }", i.Name);
|
||||
Assert.Equal(SymbolType.Enum, i.Type);
|
||||
Assert.True(i.IsDeclaration);
|
||||
},
|
||||
(i) =>
|
||||
{
|
||||
Assert.Equal("type MyEnum", i.Id);
|
||||
Assert.Equal("(type) MyEnum", i.Name);
|
||||
Assert.Equal(SymbolType.Type, i.Type);
|
||||
Assert.False(i.IsDeclaration);
|
||||
},
|
||||
(i) =>
|
||||
{
|
||||
Assert.Equal("type MyEnum", i.Id);
|
||||
Assert.Equal("(type) MyEnum", i.Name);
|
||||
Assert.Equal(SymbolType.Type, i.Type);
|
||||
Assert.False(i.IsDeclaration);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FindsOccurrencesOnTypeConstraint()
|
||||
{
|
||||
IEnumerable<SymbolReference> symbols = GetOccurrences(FindsOccurrencesOnTypeSymbolsData.TypeConstraintSourceDetails);
|
||||
Assert.Collection(symbols,
|
||||
(i) =>
|
||||
{
|
||||
Assert.Equal("type BaseClass", i.Id);
|
||||
Assert.Equal("class BaseClass { }", i.Name);
|
||||
Assert.Equal(SymbolType.Class, i.Type);
|
||||
Assert.True(i.IsDeclaration);
|
||||
},
|
||||
(i) =>
|
||||
{
|
||||
Assert.Equal("type BaseClass", i.Id);
|
||||
Assert.Equal("(type) BaseClass", i.Name);
|
||||
Assert.Equal(SymbolType.Type, i.Type);
|
||||
Assert.False(i.IsDeclaration);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FindsConstructorDefinition()
|
||||
{
|
||||
IEnumerable<SymbolReference> symbols = await GetDefinitions(FindsTypeSymbolsDefinitionData.ConstructorSourceDetails);
|
||||
Assert.Collection(symbols,
|
||||
(i) =>
|
||||
{
|
||||
Assert.Equal("mtd SuperClass", i.Id);
|
||||
Assert.Equal("SuperClass([string]$name)", i.Name);
|
||||
Assert.Equal(SymbolType.Constructor, i.Type);
|
||||
Assert.True(i.IsDeclaration);
|
||||
},
|
||||
(i) =>
|
||||
{
|
||||
Assert.Equal("mtd SuperClass", i.Id);
|
||||
Assert.Equal("SuperClass()", i.Name);
|
||||
Assert.Equal(SymbolType.Constructor, i.Type);
|
||||
Assert.True(i.IsDeclaration);
|
||||
});
|
||||
|
||||
Assert.Equal(symbols, await GetReferences(FindsReferencesOnTypeSymbolsData.ConstructorSourceDetails));
|
||||
Assert.Equal(symbols, GetOccurrences(FindsOccurrencesOnTypeSymbolsData.ConstructorSourceDetails));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FindsMethodDefinition()
|
||||
{
|
||||
IEnumerable<SymbolReference> symbols = await GetDefinitions(FindsTypeSymbolsDefinitionData.MethodSourceDetails);
|
||||
Assert.Collection(symbols,
|
||||
(i) =>
|
||||
{
|
||||
Assert.Equal("mtd MyClassMethod", i.Id);
|
||||
Assert.Equal("string MyClassMethod([string]$param1, $param2, [int]$param3)", i.Name);
|
||||
Assert.Equal(SymbolType.Method, i.Type);
|
||||
Assert.True(i.IsDeclaration);
|
||||
},
|
||||
(i) =>
|
||||
{
|
||||
Assert.Equal("mtd MyClassMethod", i.Id);
|
||||
Assert.Equal("string MyClassMethod([MyEnum]$param1)", i.Name);
|
||||
Assert.Equal(SymbolType.Method, i.Type);
|
||||
Assert.True(i.IsDeclaration);
|
||||
},
|
||||
(i) =>
|
||||
{
|
||||
Assert.Equal("mtd MyClassMethod", i.Id);
|
||||
Assert.Equal("string MyClassMethod()", i.Name);
|
||||
Assert.Equal(SymbolType.Method, i.Type);
|
||||
Assert.True(i.IsDeclaration);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FindsReferencesOnMethod()
|
||||
{
|
||||
IEnumerable<SymbolReference> symbols = await GetReferences(FindsReferencesOnTypeSymbolsData.MethodSourceDetails);
|
||||
Assert.Collection(symbols,
|
||||
(i) => Assert.Equal("string MyClassMethod([string]$param1, $param2, [int]$param3)", i.Name),
|
||||
(i) => Assert.Equal("string MyClassMethod([MyEnum]$param1)", i.Name),
|
||||
(i) => Assert.Equal("string MyClassMethod()", i.Name),
|
||||
(i) => // The invocation!
|
||||
{
|
||||
Assert.Equal("mtd MyClassMethod", i.Id);
|
||||
Assert.Equal("(method) MyClassMethod", i.Name);
|
||||
Assert.Equal("$o.MyClassMethod()", i.SourceLine);
|
||||
Assert.Equal(SymbolType.Method, i.Type);
|
||||
Assert.False(i.IsDeclaration);
|
||||
});
|
||||
|
||||
Assert.Equal(symbols, GetOccurrences(FindsOccurrencesOnTypeSymbolsData.MethodSourceDetails));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FindsPropertyDefinition()
|
||||
{
|
||||
SymbolReference symbol = await GetDefinition(FindsTypeSymbolsDefinitionData.PropertySourceDetails);
|
||||
Assert.Equal("prop SomePropWithDefault", symbol.Id);
|
||||
Assert.Equal("[string] $SomePropWithDefault", symbol.Name);
|
||||
Assert.Equal(SymbolType.Property, symbol.Type);
|
||||
Assert.True(symbol.IsDeclaration);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FindsReferencesOnProperty()
|
||||
{
|
||||
IEnumerable<SymbolReference> symbols = await GetReferences(FindsReferencesOnTypeSymbolsData.PropertySourceDetails);
|
||||
Assert.Collection(symbols,
|
||||
(i) =>
|
||||
{
|
||||
Assert.Equal("prop SomeProp", i.Id);
|
||||
Assert.Equal("[int] $SomeProp", i.Name);
|
||||
Assert.Equal(SymbolType.Property, i.Type);
|
||||
Assert.True(i.IsDeclaration);
|
||||
},
|
||||
(i) =>
|
||||
{
|
||||
Assert.Equal("prop SomeProp", i.Id);
|
||||
Assert.Equal("(property) SomeProp", i.Name);
|
||||
Assert.Equal(SymbolType.Property, i.Type);
|
||||
Assert.False(i.IsDeclaration);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FindsOccurrencesOnProperty()
|
||||
{
|
||||
IEnumerable<SymbolReference> symbols = GetOccurrences(FindsOccurrencesOnTypeSymbolsData.PropertySourceDetails);
|
||||
Assert.Collection(symbols,
|
||||
(i) =>
|
||||
{
|
||||
Assert.Equal("prop SomePropWithDefault", i.Id);
|
||||
Assert.Equal("[string] $SomePropWithDefault", i.Name);
|
||||
Assert.Equal(SymbolType.Property, i.Type);
|
||||
Assert.True(i.IsDeclaration);
|
||||
},
|
||||
(i) =>
|
||||
{
|
||||
Assert.Equal("prop SomePropWithDefault", i.Id);
|
||||
Assert.Equal("(property) SomePropWithDefault", i.Name);
|
||||
Assert.Equal(SymbolType.Property, i.Type);
|
||||
Assert.False(i.IsDeclaration);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FindsEnumMemberDefinition()
|
||||
{
|
||||
SymbolReference symbol = await GetDefinition(FindsTypeSymbolsDefinitionData.EnumMemberSourceDetails);
|
||||
Assert.Equal("prop Second", symbol.Id);
|
||||
// Doesn't include [MyEnum]:: because that'd be redundant in the outline.
|
||||
Assert.Equal("Second", symbol.Name);
|
||||
Assert.Equal(SymbolType.EnumMember, symbol.Type);
|
||||
Assert.True(symbol.IsDeclaration);
|
||||
AssertIsRegion(symbol.NameRegion, 41, 5, 41, 11);
|
||||
|
||||
symbol = await GetDefinition(FindsReferencesOnTypeSymbolsData.EnumMemberSourceDetails);
|
||||
Assert.Equal("prop First", symbol.Id);
|
||||
Assert.Equal("First", symbol.Name);
|
||||
Assert.Equal(SymbolType.EnumMember, symbol.Type);
|
||||
Assert.True(symbol.IsDeclaration);
|
||||
AssertIsRegion(symbol.NameRegion, 40, 5, 40, 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FindsReferencesOnEnumMember()
|
||||
{
|
||||
IEnumerable<SymbolReference> symbols = await GetReferences(FindsReferencesOnTypeSymbolsData.EnumMemberSourceDetails);
|
||||
Assert.Collection(symbols,
|
||||
(i) =>
|
||||
{
|
||||
Assert.Equal("prop First", i.Id);
|
||||
Assert.Equal("First", i.Name);
|
||||
Assert.Equal(SymbolType.EnumMember, i.Type);
|
||||
Assert.True(i.IsDeclaration);
|
||||
},
|
||||
(i) =>
|
||||
{
|
||||
Assert.Equal("prop First", i.Id);
|
||||
// The reference is just a member invocation, and so indistinguishable from a property.
|
||||
Assert.Equal("(property) First", i.Name);
|
||||
Assert.Equal(SymbolType.Property, i.Type);
|
||||
Assert.False(i.IsDeclaration);
|
||||
});
|
||||
|
||||
Assert.Equal(symbols, GetOccurrences(FindsOccurrencesOnTypeSymbolsData.EnumMemberSourceDetails));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FindsDetailsForBuiltInCommand()
|
||||
{
|
||||
SymbolDetails symbolDetails = await symbolsService.FindSymbolDetailsAtLocationAsync(
|
||||
GetScriptFile(FindsDetailsForBuiltInCommandData.SourceDetails),
|
||||
FindsDetailsForBuiltInCommandData.SourceDetails.StartLineNumber,
|
||||
FindsDetailsForBuiltInCommandData.SourceDetails.StartColumnNumber,
|
||||
CancellationToken.None);
|
||||
|
||||
Assert.Equal("Extracts files from a specified archive (zipped) file.", symbolDetails.Documentation);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FindsSymbolsInFile()
|
||||
{
|
||||
IEnumerable<SymbolReference> symbols = FindSymbolsInFile(FindSymbolsInMultiSymbolFile.SourceDetails);
|
||||
|
||||
Assert.Equal(7, symbols.Count(i => i.Type == SymbolType.Function));
|
||||
Assert.Equal(8, symbols.Count(i => i.Type == SymbolType.Variable));
|
||||
Assert.Equal(4, symbols.Count(i => i.Type == SymbolType.Parameter));
|
||||
Assert.Equal(12, symbols.Count(i => i.Id.StartsWith("var ")));
|
||||
Assert.Equal(2, symbols.Count(i => i.Id.StartsWith("prop ")));
|
||||
|
||||
SymbolReference symbol = symbols.First(i => i.Type == SymbolType.Function);
|
||||
Assert.Equal("fn AFunction", symbol.Id);
|
||||
Assert.Equal("function script:AFunction ()", symbol.Name);
|
||||
Assert.True(symbol.IsDeclaration);
|
||||
Assert.Equal(2, GetOccurrences(symbol.NameRegion).Count());
|
||||
|
||||
symbol = symbols.First(i => i.Id == "fn AFilter");
|
||||
Assert.Equal("filter AFilter ()", symbol.Name);
|
||||
Assert.True(symbol.IsDeclaration);
|
||||
|
||||
symbol = symbols.Last(i => i.Type == SymbolType.Variable);
|
||||
Assert.Equal("var nestedVar", symbol.Id);
|
||||
Assert.Equal("$nestedVar", symbol.Name);
|
||||
Assert.False(symbol.IsDeclaration);
|
||||
AssertIsRegion(symbol.NameRegion, 16, 29, 16, 39);
|
||||
|
||||
symbol = Assert.Single(symbols, i => i.Type == SymbolType.Workflow);
|
||||
Assert.Equal("fn AWorkflow", symbol.Id);
|
||||
Assert.Equal("workflow AWorkflow ()", symbol.Name);
|
||||
Assert.True(symbol.IsDeclaration);
|
||||
|
||||
symbol = Assert.Single(symbols, i => i.Type == SymbolType.Class);
|
||||
Assert.Equal("type AClass", symbol.Id);
|
||||
Assert.Equal("class AClass { }", symbol.Name);
|
||||
Assert.True(symbol.IsDeclaration);
|
||||
|
||||
symbol = Assert.Single(symbols, i => i.Type == SymbolType.Property);
|
||||
Assert.Equal("prop AProperty", symbol.Id);
|
||||
Assert.Equal("[string] $AProperty", symbol.Name);
|
||||
Assert.True(symbol.IsDeclaration);
|
||||
|
||||
symbol = Assert.Single(symbols, i => i.Type == SymbolType.Constructor);
|
||||
Assert.Equal("mtd AClass", symbol.Id);
|
||||
Assert.Equal("AClass([string]$AParameter)", symbol.Name);
|
||||
Assert.True(symbol.IsDeclaration);
|
||||
|
||||
symbol = Assert.Single(symbols, i => i.Type == SymbolType.Method);
|
||||
Assert.Equal("mtd AMethod", symbol.Id);
|
||||
Assert.Equal("void AMethod([string]$param1, [int]$param2, $param3)", symbol.Name);
|
||||
Assert.True(symbol.IsDeclaration);
|
||||
|
||||
symbol = Assert.Single(symbols, i => i.Type == SymbolType.Enum);
|
||||
Assert.Equal("type AEnum", symbol.Id);
|
||||
Assert.Equal("enum AEnum { }", symbol.Name);
|
||||
Assert.True(symbol.IsDeclaration);
|
||||
|
||||
symbol = Assert.Single(symbols, i => i.Type == SymbolType.EnumMember);
|
||||
Assert.Equal("prop AValue", symbol.Id);
|
||||
Assert.Equal("AValue", symbol.Name);
|
||||
Assert.True(symbol.IsDeclaration);
|
||||
|
||||
// There should be no region symbols unless the provider has been registered.
|
||||
Assert.DoesNotContain(symbols, i => i.Type == SymbolType.Region);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FindsRegionsInFile()
|
||||
{
|
||||
symbolsService.TryRegisterDocumentSymbolProvider(new RegionDocumentSymbolProvider());
|
||||
IEnumerable<SymbolReference> symbols = FindSymbolsInFile(FindSymbolsInMultiSymbolFile.SourceDetails);
|
||||
Assert.Collection(symbols.Where(i => i.Type == SymbolType.Region),
|
||||
(i) =>
|
||||
{
|
||||
Assert.Equal("region find me outer", i.Id);
|
||||
Assert.Equal("#region find me outer", i.Name);
|
||||
Assert.Equal(SymbolType.Region, i.Type);
|
||||
Assert.True(i.IsDeclaration);
|
||||
AssertIsRegion(i.NameRegion, 51, 1, 51, 22);
|
||||
AssertIsRegion(i.ScriptRegion, 51, 1, 55, 11);
|
||||
},
|
||||
(i) =>
|
||||
{
|
||||
Assert.Equal("region find me inner", i.Id);
|
||||
Assert.Equal("#region find me inner", i.Name);
|
||||
Assert.Equal(SymbolType.Region, i.Type);
|
||||
Assert.True(i.IsDeclaration);
|
||||
AssertIsRegion(i.NameRegion, 52, 1, 52, 22);
|
||||
AssertIsRegion(i.ScriptRegion, 52, 1, 54, 11);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FindsSymbolsWithNewLineInFile()
|
||||
{
|
||||
IEnumerable<SymbolReference> symbols = FindSymbolsInFile(FindSymbolsInNewLineSymbolFile.SourceDetails);
|
||||
|
||||
SymbolReference symbol = Assert.Single(symbols, i => i.Type == SymbolType.Function);
|
||||
Assert.Equal("fn returnTrue", symbol.Id);
|
||||
AssertIsRegion(symbol.NameRegion, 2, 1, 2, 11);
|
||||
AssertIsRegion(symbol.ScriptRegion, 1, 1, 4, 2);
|
||||
|
||||
symbol = Assert.Single(symbols, i => i.Type == SymbolType.Class);
|
||||
Assert.Equal("type NewLineClass", symbol.Id);
|
||||
AssertIsRegion(symbol.NameRegion, 7, 1, 7, 13);
|
||||
AssertIsRegion(symbol.ScriptRegion, 6, 1, 23, 2);
|
||||
|
||||
symbol = Assert.Single(symbols, i => i.Type == SymbolType.Constructor);
|
||||
Assert.Equal("mtd NewLineClass", symbol.Id);
|
||||
AssertIsRegion(symbol.NameRegion, 8, 5, 8, 17);
|
||||
AssertIsRegion(symbol.ScriptRegion, 8, 5, 10, 6);
|
||||
|
||||
symbol = Assert.Single(symbols, i => i.Type == SymbolType.Property);
|
||||
Assert.Equal("prop SomePropWithDefault", symbol.Id);
|
||||
AssertIsRegion(symbol.NameRegion, 15, 5, 15, 25);
|
||||
AssertIsRegion(symbol.ScriptRegion, 12, 5, 15, 40);
|
||||
|
||||
symbol = Assert.Single(symbols, i => i.Type == SymbolType.Method);
|
||||
Assert.Equal("mtd MyClassMethod", symbol.Id);
|
||||
Assert.Equal("string MyClassMethod([MyNewLineEnum]$param1)", symbol.Name);
|
||||
AssertIsRegion(symbol.NameRegion, 20, 5, 20, 18);
|
||||
AssertIsRegion(symbol.ScriptRegion, 17, 5, 22, 6);
|
||||
|
||||
symbol = Assert.Single(symbols, i => i.Type == SymbolType.Enum);
|
||||
Assert.Equal("type MyNewLineEnum", symbol.Id);
|
||||
AssertIsRegion(symbol.NameRegion, 26, 1, 26, 14);
|
||||
AssertIsRegion(symbol.ScriptRegion, 25, 1, 28, 2);
|
||||
|
||||
symbol = Assert.Single(symbols, i => i.Type == SymbolType.EnumMember);
|
||||
Assert.Equal("prop First", symbol.Id);
|
||||
AssertIsRegion(symbol.NameRegion, 27, 5, 27, 10);
|
||||
AssertIsRegion(symbol.ScriptRegion, 27, 5, 27, 10);
|
||||
}
|
||||
|
||||
[SkippableFact]
|
||||
public void FindsSymbolsInDSCFile()
|
||||
{
|
||||
Skip.If(!isWindows, "DSC only works properly on Windows.");
|
||||
|
||||
IEnumerable<SymbolReference> symbols = FindSymbolsInFile(FindSymbolsInDSCFile.SourceDetails);
|
||||
SymbolReference symbol = Assert.Single(symbols, i => i.Type == SymbolType.Configuration);
|
||||
// The prefix "dsc" is added for sorting reasons.
|
||||
Assert.Equal("dsc AConfiguration", symbol.Id);
|
||||
Assert.Equal(2, symbol.ScriptRegion.StartLineNumber);
|
||||
Assert.Equal(1, symbol.ScriptRegion.StartColumnNumber);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FindsSymbolsInPesterFile()
|
||||
{
|
||||
IEnumerable<PesterSymbolReference> symbols = FindSymbolsInFile(FindSymbolsInPesterFile.SourceDetails).OfType<PesterSymbolReference>();
|
||||
Assert.Equal(12, symbols.Count(i => i.Type == SymbolType.Function));
|
||||
|
||||
SymbolReference symbol = Assert.Single(symbols, i => i.Command == PesterCommandType.Describe);
|
||||
Assert.Equal("Describe \"Testing Pester symbols\"", symbol.Id);
|
||||
Assert.Equal(9, symbol.ScriptRegion.StartLineNumber);
|
||||
Assert.Equal(1, symbol.ScriptRegion.StartColumnNumber);
|
||||
|
||||
symbol = Assert.Single(symbols, i => i.Command == PesterCommandType.Context);
|
||||
Assert.Equal("Context \"When a Pester file is given\"", symbol.Id);
|
||||
Assert.Equal(10, symbol.ScriptRegion.StartLineNumber);
|
||||
Assert.Equal(5, symbol.ScriptRegion.StartColumnNumber);
|
||||
|
||||
Assert.Equal(4, symbols.Count(i => i.Command == PesterCommandType.It));
|
||||
symbol = symbols.Last(i => i.Command == PesterCommandType.It);
|
||||
Assert.Equal("It \"Should return setup and teardown symbols\"", symbol.Id);
|
||||
Assert.Equal(31, symbol.ScriptRegion.StartLineNumber);
|
||||
Assert.Equal(9, symbol.ScriptRegion.StartColumnNumber);
|
||||
|
||||
symbol = Assert.Single(symbols, i => i.Command == PesterCommandType.BeforeDiscovery);
|
||||
Assert.Equal("BeforeDiscovery", symbol.Id);
|
||||
Assert.Equal(1, symbol.ScriptRegion.StartLineNumber);
|
||||
Assert.Equal(1, symbol.ScriptRegion.StartColumnNumber);
|
||||
|
||||
Assert.Equal(2, symbols.Count(i => i.Command == PesterCommandType.BeforeAll));
|
||||
symbol = symbols.Last(i => i.Command == PesterCommandType.BeforeAll);
|
||||
Assert.Equal("BeforeAll", symbol.Id);
|
||||
Assert.Equal(11, symbol.ScriptRegion.StartLineNumber);
|
||||
Assert.Equal(9, symbol.ScriptRegion.StartColumnNumber);
|
||||
|
||||
symbol = Assert.Single(symbols, i => i.Command == PesterCommandType.BeforeEach);
|
||||
Assert.Equal("BeforeEach", symbol.Id);
|
||||
Assert.Equal(15, symbol.ScriptRegion.StartLineNumber);
|
||||
Assert.Equal(9, symbol.ScriptRegion.StartColumnNumber);
|
||||
|
||||
symbol = Assert.Single(symbols, i => i.Command == PesterCommandType.AfterEach);
|
||||
Assert.Equal("AfterEach", symbol.Id);
|
||||
Assert.Equal(35, symbol.ScriptRegion.StartLineNumber);
|
||||
Assert.Equal(9, symbol.ScriptRegion.StartColumnNumber);
|
||||
|
||||
symbol = Assert.Single(symbols, i => i.Command == PesterCommandType.AfterAll);
|
||||
Assert.Equal("AfterAll", symbol.Id);
|
||||
Assert.Equal(40, symbol.ScriptRegion.StartLineNumber);
|
||||
Assert.Equal(5, symbol.ScriptRegion.StartColumnNumber);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FindsSymbolsInPSKoansFile()
|
||||
{
|
||||
IEnumerable<PesterSymbolReference> symbols = FindSymbolsInFile(FindSymbolsInPSKoansFile.SourceDetails).OfType<PesterSymbolReference>();
|
||||
|
||||
// Pester symbols are properly tested in FindsSymbolsInPesterFile so only counting to make sure they appear
|
||||
Assert.Equal(7, symbols.Count(i => i.Type == SymbolType.Function));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FindsSymbolsInPSDFile()
|
||||
{
|
||||
IEnumerable<SymbolReference> symbols = FindSymbolsInFile(FindSymbolsInPSDFile.SourceDetails);
|
||||
Assert.All(symbols, i => Assert.Equal(SymbolType.HashtableKey, i.Type));
|
||||
Assert.Collection(symbols,
|
||||
i => Assert.Equal("property1", i.Id),
|
||||
i => Assert.Equal("property2", i.Id),
|
||||
i => Assert.Equal("property3", i.Id));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FindsSymbolsInNoSymbolsFile()
|
||||
{
|
||||
IEnumerable<SymbolReference> symbolsResult = FindSymbolsInFile(FindSymbolsInNoSymbolsFile.SourceDetails);
|
||||
Assert.Empty(symbolsResult);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,351 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using Microsoft.PowerShell.EditorServices.Services.TextDocument;
|
||||
using OmniSharp.Extensions.LanguageServer.Protocol;
|
||||
using OmniSharp.Extensions.LanguageServer.Protocol.Models;
|
||||
using Xunit;
|
||||
|
||||
namespace PowerShellEditorServices.Test.Language
|
||||
{
|
||||
public class TokenOperationsTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Helper method to create a stub script file and then call FoldableRegions
|
||||
/// </summary>
|
||||
private static FoldingReference[] GetRegions(string text)
|
||||
{
|
||||
ScriptFile scriptFile = ScriptFile.Create(
|
||||
// Use any absolute path. Even if it doesn't exist.
|
||||
DocumentUri.FromFileSystemPath(Path.Combine(Path.GetTempPath(), "TestFile.ps1")),
|
||||
text,
|
||||
Version.Parse("5.0"));
|
||||
|
||||
FoldingReference[] result = TokenOperations.FoldableReferences(scriptFile.ScriptTokens).ToArray();
|
||||
// The foldable regions need to be deterministic for testing so sort the array.
|
||||
Array.Sort(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper method to create FoldingReference objects with less typing
|
||||
/// </summary>
|
||||
private static FoldingReference CreateFoldingReference(int startLine, int startCharacter, int endLine, int endCharacter, FoldingRangeKind? matchKind)
|
||||
{
|
||||
return new FoldingReference
|
||||
{
|
||||
StartLine = startLine,
|
||||
StartCharacter = startCharacter,
|
||||
EndLine = endLine,
|
||||
EndCharacter = endCharacter,
|
||||
Kind = matchKind
|
||||
};
|
||||
}
|
||||
|
||||
// This PowerShell script will exercise all of the
|
||||
// folding regions and regions which should not be
|
||||
// detected. Due to file encoding this could be CLRF or LF line endings
|
||||
private const string allInOneScript =
|
||||
@"#Region This should fold
|
||||
<#
|
||||
Nested different comment types. This should fold
|
||||
#>
|
||||
#EndRegion
|
||||
|
||||
# region This should not fold due to whitespace
|
||||
$shouldFold = $false
|
||||
# endRegion
|
||||
function short-func-not-fold {};
|
||||
<#
|
||||
.SYNOPSIS
|
||||
This whole comment block should fold, not just the SYNOPSIS
|
||||
.EXAMPLE
|
||||
This whole comment block should fold, not just the EXAMPLE
|
||||
#>
|
||||
function New-VSCodeShouldFold {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
This whole comment block should fold, not just the SYNOPSIS
|
||||
.EXAMPLE
|
||||
This whole comment block should fold, not just the EXAMPLE
|
||||
#>
|
||||
$I = @'
|
||||
herestrings should fold
|
||||
|
||||
'@
|
||||
|
||||
# This won't confuse things
|
||||
Get-Command -Param @I
|
||||
|
||||
$I = @""
|
||||
double quoted herestrings should also fold
|
||||
|
||||
""@
|
||||
|
||||
# this won't be folded
|
||||
|
||||
# This block of comments should be foldable as a single block
|
||||
# This block of comments should be foldable as a single block
|
||||
# This block of comments should be foldable as a single block
|
||||
|
||||
#region This fools the indentation folding.
|
||||
Write-Host ""Hello""
|
||||
#region Nested regions should be foldable
|
||||
Write-Host ""Hello""
|
||||
# comment1
|
||||
Write-Host ""Hello""
|
||||
#endregion
|
||||
Write-Host ""Hello""
|
||||
# comment2
|
||||
Write-Host ""Hello""
|
||||
#endregion
|
||||
|
||||
$c = {
|
||||
Write-Host ""Script blocks should be foldable""
|
||||
}
|
||||
|
||||
# Array fools indentation folding
|
||||
$d = @(
|
||||
'should fold1',
|
||||
'should fold2'
|
||||
)
|
||||
}
|
||||
|
||||
# Make sure contiguous comment blocks can be folded properly
|
||||
|
||||
# Comment Block 1
|
||||
# Comment Block 1
|
||||
# Comment Block 1
|
||||
#region Comment Block 3
|
||||
# Comment Block 2
|
||||
# Comment Block 2
|
||||
# Comment Block 2
|
||||
$something = $true
|
||||
#endregion Comment Block 3
|
||||
|
||||
# What about anonymous variable assignment
|
||||
${this
|
||||
is
|
||||
valid} = 5
|
||||
|
||||
#RegIon This should fold due to casing
|
||||
$foo = 'bar'
|
||||
#EnDReGion
|
||||
";
|
||||
private readonly FoldingReference[] expectedAllInOneScriptFolds = {
|
||||
CreateFoldingReference(0, 0, 4, 10, FoldingRangeKind.Region),
|
||||
CreateFoldingReference(1, 0, 3, 2, FoldingRangeKind.Comment),
|
||||
CreateFoldingReference(10, 0, 15, 2, FoldingRangeKind.Comment),
|
||||
CreateFoldingReference(16, 30, 63, 1, null),
|
||||
CreateFoldingReference(17, 0, 22, 2, FoldingRangeKind.Comment),
|
||||
CreateFoldingReference(23, 7, 26, 2, null),
|
||||
CreateFoldingReference(31, 5, 34, 2, null),
|
||||
CreateFoldingReference(38, 2, 40, 0, FoldingRangeKind.Comment),
|
||||
CreateFoldingReference(42, 2, 52, 14, FoldingRangeKind.Region),
|
||||
CreateFoldingReference(44, 4, 48, 14, FoldingRangeKind.Region),
|
||||
CreateFoldingReference(54, 7, 56, 3, null),
|
||||
CreateFoldingReference(59, 7, 62, 3, null),
|
||||
CreateFoldingReference(67, 0, 69, 0, FoldingRangeKind.Comment),
|
||||
CreateFoldingReference(70, 0, 75, 26, FoldingRangeKind.Region),
|
||||
CreateFoldingReference(71, 0, 73, 0, FoldingRangeKind.Comment),
|
||||
CreateFoldingReference(78, 0, 80, 6, null),
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Assertion helper to compare two FoldingReference arrays.
|
||||
/// </summary>
|
||||
private static void AssertFoldingReferenceArrays(
|
||||
FoldingReference[] expected,
|
||||
FoldingReference[] actual)
|
||||
{
|
||||
for (int index = 0; index < expected.Length; index++)
|
||||
{
|
||||
Assert.Equal(expected[index], actual[index]);
|
||||
}
|
||||
Assert.Equal(expected.Length, actual.Length);
|
||||
}
|
||||
|
||||
[Trait("Category", "Folding")]
|
||||
[Fact]
|
||||
public void LanguageServiceFindsFoldablRegionsWithLF()
|
||||
{
|
||||
// Remove and CR characters
|
||||
string testString = allInOneScript.Replace("\r", "");
|
||||
// Ensure that there are no CR characters in the string
|
||||
Assert.False(testString.Contains("\r\n"), "CRLF should not be present in the test string");
|
||||
FoldingReference[] result = GetRegions(testString);
|
||||
AssertFoldingReferenceArrays(expectedAllInOneScriptFolds, result);
|
||||
}
|
||||
|
||||
[Trait("Category", "Folding")]
|
||||
[Fact]
|
||||
public void LanguageServiceFindsFoldablRegionsWithCRLF()
|
||||
{
|
||||
// The Foldable regions should be the same regardless of line ending type
|
||||
// Enforce CRLF line endings, if none exist
|
||||
string testString = allInOneScript;
|
||||
if (!testString.Contains("\r\n"))
|
||||
{
|
||||
testString = testString.Replace("\n", "\r\n");
|
||||
}
|
||||
// Ensure that there are CRLF characters in the string
|
||||
Assert.True(testString.IndexOf("\r\n") != -1, "CRLF should be present in the teststring");
|
||||
FoldingReference[] result = GetRegions(testString);
|
||||
AssertFoldingReferenceArrays(expectedAllInOneScriptFolds, result);
|
||||
}
|
||||
|
||||
[Trait("Category", "Folding")]
|
||||
[Fact]
|
||||
public void LanguageServiceFindsFoldablRegionsWithMismatchedRegions()
|
||||
{
|
||||
const string testString =
|
||||
@"#endregion should not fold - mismatched
|
||||
|
||||
#region This should fold
|
||||
$something = 'foldable'
|
||||
#endregion
|
||||
|
||||
#region should not fold - mismatched
|
||||
";
|
||||
FoldingReference[] expectedFolds = {
|
||||
CreateFoldingReference(2, 0, 4, 10, FoldingRangeKind.Region)
|
||||
};
|
||||
|
||||
FoldingReference[] result = GetRegions(testString);
|
||||
AssertFoldingReferenceArrays(expectedFolds, result);
|
||||
}
|
||||
|
||||
[Trait("Category", "Folding")]
|
||||
[Fact]
|
||||
public void LanguageServiceFindsFoldablRegionsWithDuplicateRegions()
|
||||
{
|
||||
const string testString =
|
||||
@"# This script causes duplicate/overlapping ranges due to the `(` and `{` characters
|
||||
$AnArray = @(Get-ChildItem -Path C:\ -Include *.ps1 -File).Where({
|
||||
$_.FullName -ne 'foo'}).ForEach({
|
||||
# Do Something
|
||||
})
|
||||
";
|
||||
FoldingReference[] expectedFolds = {
|
||||
CreateFoldingReference(1, 64, 2, 27, null),
|
||||
CreateFoldingReference(2, 35, 4, 2, null)
|
||||
};
|
||||
|
||||
FoldingReference[] result = GetRegions(testString);
|
||||
AssertFoldingReferenceArrays(expectedFolds, result);
|
||||
}
|
||||
|
||||
// This tests that token matching { -> }, @{ -> } and
|
||||
// ( -> ), @( -> ) and $( -> ) does not confuse the folder
|
||||
[Trait("Category", "Folding")]
|
||||
[Fact]
|
||||
public void LanguageServiceFindsFoldablRegionsWithSameEndToken()
|
||||
{
|
||||
const string testString =
|
||||
@"foreach ($1 in $2) {
|
||||
|
||||
$x = @{
|
||||
'abc' = 'def'
|
||||
}
|
||||
}
|
||||
|
||||
$y = $(
|
||||
$arr = @('1', '2'); Write-Host ($arr)
|
||||
)
|
||||
";
|
||||
FoldingReference[] expectedFolds = {
|
||||
CreateFoldingReference(0, 19, 5, 1, null),
|
||||
CreateFoldingReference(2, 9, 4, 5, null),
|
||||
CreateFoldingReference(7, 5, 9, 1, null)
|
||||
};
|
||||
|
||||
FoldingReference[] result = GetRegions(testString);
|
||||
|
||||
AssertFoldingReferenceArrays(expectedFolds, result);
|
||||
}
|
||||
|
||||
// A simple PowerShell Classes test
|
||||
[Trait("Category", "Folding")]
|
||||
[Fact]
|
||||
public void LanguageServiceFindsFoldablRegionsWithClasses()
|
||||
{
|
||||
const string testString =
|
||||
@"class TestClass {
|
||||
[string[]] $TestProperty = @(
|
||||
'first',
|
||||
'second',
|
||||
'third')
|
||||
|
||||
[string] TestMethod() {
|
||||
return $this.TestProperty[0]
|
||||
}
|
||||
}
|
||||
";
|
||||
FoldingReference[] expectedFolds = {
|
||||
CreateFoldingReference(0, 16, 9, 1, null),
|
||||
CreateFoldingReference(1, 31, 4, 16, null),
|
||||
CreateFoldingReference(6, 26, 8, 5, null)
|
||||
};
|
||||
|
||||
FoldingReference[] result = GetRegions(testString);
|
||||
|
||||
AssertFoldingReferenceArrays(expectedFolds, result);
|
||||
}
|
||||
|
||||
// This tests DSC style keywords and param blocks
|
||||
[Trait("Category", "Folding")]
|
||||
[Fact]
|
||||
public void LanguageServiceFindsFoldablRegionsWithDSC()
|
||||
{
|
||||
const string testString =
|
||||
@"Configuration Example
|
||||
{
|
||||
param
|
||||
(
|
||||
[Parameter()]
|
||||
[System.String[]]
|
||||
$NodeName = 'localhost',
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[System.Management.Automation.PSCredential]
|
||||
$Credential
|
||||
)
|
||||
|
||||
Import-DscResource -Module ActiveDirectoryCSDsc
|
||||
|
||||
Node $AllNodes.NodeName
|
||||
{
|
||||
WindowsFeature ADCS-Cert-Authority
|
||||
{
|
||||
Ensure = 'Present'
|
||||
Name = 'ADCS-Cert-Authority'
|
||||
}
|
||||
|
||||
AdcsCertificationAuthority CertificateAuthority
|
||||
{
|
||||
IsSingleInstance = 'Yes'
|
||||
Ensure = 'Present'
|
||||
Credential = $Credential
|
||||
CAType = 'EnterpriseRootCA'
|
||||
DependsOn = '[WindowsFeature]ADCS-Cert-Authority'
|
||||
}
|
||||
}
|
||||
}
|
||||
";
|
||||
FoldingReference[] expectedFolds = {
|
||||
CreateFoldingReference(1, 0, 33, 1, null),
|
||||
CreateFoldingReference(3, 4, 12, 5, null),
|
||||
CreateFoldingReference(17, 4, 32, 5, null),
|
||||
CreateFoldingReference(19, 8, 22, 9, null),
|
||||
CreateFoldingReference(25, 8, 31, 9, null)
|
||||
};
|
||||
|
||||
FoldingReference[] result = GetRegions(testString);
|
||||
|
||||
AssertFoldingReferenceArrays(expectedFolds, result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), PowerShellEditorServices.Common.props))\PowerShellEditorServices.Common.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net8.0;net462</TargetFrameworks>
|
||||
<AssemblyName>Microsoft.PowerShell.EditorServices.Test</AssemblyName>
|
||||
<TargetPlatform>x64</TargetPlatform>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(TargetFramework)' != 'net462' ">
|
||||
<DefineConstants>$(DefineConstants);CoreCLR</DefineConstants>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\PowerShellEditorServices\PowerShellEditorServices.csproj" />
|
||||
<ProjectReference Include="..\PowerShellEditorServices.Test.Shared\PowerShellEditorServices.Test.Shared.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- PowerShell 7.4.x -->
|
||||
<ItemGroup Condition=" '$(TargetFramework)' == 'net8.0' ">
|
||||
<PackageReference Include="Microsoft.PowerShell.SDK" VersionOverride="7.4.7" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Windows PowerShell 5.1 -->
|
||||
<ItemGroup Condition=" '$(TargetFramework)' == 'net462' ">
|
||||
<PackageReference Include="Microsoft.PowerShell.5.ReferenceAssemblies" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||
<PackageReference Include="xunit" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" />
|
||||
<PackageReference Include="Xunit.SkippableFact" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Service Include="{82a7f48d-3b50-4b1e-b82e-3ada8210c358}" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="Fixtures\**">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
89
test/PowerShellEditorServices.Test/PsesHostFactory.cs
Normal file
89
test/PowerShellEditorServices.Test/PsesHostFactory.cs
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Management.Automation.Host;
|
||||
using System.Management.Automation.Runspaces;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.PowerShell.EditorServices.Hosting;
|
||||
using Microsoft.PowerShell.EditorServices.Services.PowerShell.Host;
|
||||
using Microsoft.PowerShell.EditorServices.Test.Shared;
|
||||
using Microsoft.PowerShell.EditorServices.Utility;
|
||||
|
||||
namespace Microsoft.PowerShell.EditorServices.Test
|
||||
{
|
||||
internal static class PsesHostFactory
|
||||
{
|
||||
// NOTE: These paths are arbitrarily chosen just to verify that the profile paths can be set
|
||||
// to whatever they need to be for the given host.
|
||||
|
||||
public static readonly ProfilePathInfo TestProfilePaths = new(
|
||||
Path.GetFullPath(TestUtilities.NormalizePath("../../../../PowerShellEditorServices.Test.Shared/Profile/Test.PowerShellEditorServices_profile.ps1")),
|
||||
Path.GetFullPath(TestUtilities.NormalizePath("../../../../PowerShellEditorServices.Test.Shared/Profile/ProfileTest.ps1")),
|
||||
Path.GetFullPath(TestUtilities.NormalizePath("../../../../PowerShellEditorServices.Test.Shared/Test.PowerShellEditorServices_profile.ps1")),
|
||||
Path.GetFullPath(TestUtilities.NormalizePath("../../../../PowerShellEditorServices.Test.Shared/ProfileTest.ps1")));
|
||||
|
||||
public static readonly string BundledModulePath = Path.GetFullPath(TestUtilities.NormalizePath("../../../../../module"));
|
||||
|
||||
public static async Task<PsesInternalHost> Create(ILoggerFactory loggerFactory, bool loadProfiles = false)
|
||||
{
|
||||
// We intentionally use `CreateDefault2()` as it loads `Microsoft.PowerShell.Core` only,
|
||||
// which is a more minimal and therefore safer state.
|
||||
InitialSessionState initialSessionState = InitialSessionState.CreateDefault2();
|
||||
|
||||
// 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 `-ExecutionPolicy 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 (VersionUtils.IsWindows)
|
||||
{
|
||||
initialSessionState.ExecutionPolicy = ExecutionPolicy.Bypass;
|
||||
}
|
||||
|
||||
HostStartupInfo testHostDetails = new(
|
||||
name: "PowerShell Editor Services Test Host",
|
||||
profileId: "Test.PowerShellEditorServices",
|
||||
version: new Version("1.0.0"),
|
||||
psHost: new NullPSHost(),
|
||||
profilePaths: TestProfilePaths,
|
||||
featureFlags: Array.Empty<string>(),
|
||||
additionalModules: Array.Empty<string>(),
|
||||
initialSessionState: initialSessionState,
|
||||
logPath: null,
|
||||
logLevel: (int)LogLevel.None,
|
||||
consoleReplEnabled: false,
|
||||
usesLegacyReadLine: false,
|
||||
useNullPSHostUI: true,
|
||||
bundledModulePath: BundledModulePath);
|
||||
|
||||
PsesInternalHost psesHost = new(loggerFactory, null, testHostDetails);
|
||||
|
||||
if (await psesHost.TryStartAsync(new HostStartOptions { LoadProfiles = loadProfiles }, CancellationToken.None))
|
||||
{
|
||||
return psesHost;
|
||||
}
|
||||
|
||||
throw new Exception("Host didn't start!");
|
||||
}
|
||||
}
|
||||
|
||||
internal class NullPSHost : PSHost
|
||||
{
|
||||
public override CultureInfo CurrentCulture => CultureInfo.CurrentCulture;
|
||||
public override CultureInfo CurrentUICulture => CultureInfo.CurrentUICulture;
|
||||
public override Guid InstanceId { get; } = Guid.NewGuid();
|
||||
public override string Name => nameof(NullPSHost);
|
||||
public override PSHostUserInterface UI { get; } = new NullPSHostUI();
|
||||
public override Version Version { get; } = new Version(1, 0, 0);
|
||||
public override void EnterNestedPrompt() { /* Do nothing */ }
|
||||
public override void ExitNestedPrompt() { /* Do nothing */ }
|
||||
public override void NotifyBeginApplication() { /* Do nothing */ }
|
||||
public override void NotifyEndApplication() { /* Do nothing */ }
|
||||
public override void SetShouldExit(int exitCode) { /* Do nothing */ }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.PowerShell.EditorServices.Services;
|
||||
using Microsoft.PowerShell.EditorServices.Services.Symbols;
|
||||
using Microsoft.PowerShell.EditorServices.Services.TextDocument;
|
||||
using Microsoft.PowerShell.EditorServices.Test.Shared;
|
||||
using OmniSharp.Extensions.LanguageServer.Protocol.Models;
|
||||
using Xunit;
|
||||
|
||||
namespace PowerShellEditorServices.Test.Services.Symbols
|
||||
{
|
||||
[Trait("Category", "AstOperations")]
|
||||
public class AstOperationsTests
|
||||
{
|
||||
private readonly ScriptFile scriptFile;
|
||||
|
||||
public AstOperationsTests()
|
||||
{
|
||||
WorkspaceService workspace = new(NullLoggerFactory.Instance);
|
||||
scriptFile = workspace.GetFile(TestUtilities.GetSharedPath("References/FunctionReference.ps1"));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(1, 15, "fn BasicFunction")]
|
||||
[InlineData(2, 3, "fn BasicFunction")]
|
||||
[InlineData(4, 31, "fn FunctionWithExtraSpace")]
|
||||
[InlineData(7, 18, "fn FunctionWithExtraSpace")]
|
||||
[InlineData(12, 22, "fn FunctionNameOnDifferentLine")]
|
||||
[InlineData(22, 13, "fn FunctionNameOnDifferentLine")]
|
||||
[InlineData(24, 30, "fn IndentedFunction")]
|
||||
[InlineData(24, 52, "fn IndentedFunction")]
|
||||
public void CanFindSymbolAtPosition(int line, int column, string expectedName)
|
||||
{
|
||||
SymbolReference symbol = scriptFile.References.TryGetSymbolAtPosition(line, column);
|
||||
Assert.NotNull(symbol);
|
||||
Assert.Equal(expectedName, symbol.Id);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(FindReferencesOfSymbolAtPositionData))]
|
||||
public void CanFindReferencesOfSymbolAtPosition(int line, int column, Range[] symbolRange)
|
||||
{
|
||||
SymbolReference symbol = scriptFile.References.TryGetSymbolAtPosition(line, column);
|
||||
|
||||
IEnumerable<SymbolReference> references = scriptFile.References.TryGetReferences(symbol);
|
||||
Assert.NotEmpty(references);
|
||||
|
||||
int positionsIndex = 0;
|
||||
foreach (SymbolReference reference in references.OrderBy((i) => i.ScriptRegion.ToRange().Start))
|
||||
{
|
||||
Assert.Equal(symbolRange[positionsIndex].Start.Line, reference.NameRegion.StartLineNumber);
|
||||
Assert.Equal(symbolRange[positionsIndex].Start.Character, reference.NameRegion.StartColumnNumber);
|
||||
Assert.Equal(symbolRange[positionsIndex].End.Line, reference.NameRegion.EndLineNumber);
|
||||
Assert.Equal(symbolRange[positionsIndex].End.Character, reference.NameRegion.EndColumnNumber);
|
||||
|
||||
positionsIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
public static object[][] FindReferencesOfSymbolAtPositionData { get; } = new object[][]
|
||||
{
|
||||
new object[] { 1, 15, new[] { new Range(1, 10, 1, 23), new Range(2, 1, 2, 14) } },
|
||||
new object[] { 2, 3, new[] { new Range(1, 10, 1, 23), new Range(2, 1, 2, 14) } },
|
||||
new object[] { 4, 31, new[] { new Range(4, 19, 4, 41), new Range(7, 3, 7, 25) } },
|
||||
new object[] { 7, 18, new[] { new Range(4, 19, 4, 41), new Range(7, 3, 7, 25) } },
|
||||
new object[] { 22, 13, new[] { new Range(12, 8, 12, 35), new Range(22, 5, 22, 32) } },
|
||||
new object[] { 12, 22, new[] { new Range(12, 8, 12, 35), new Range(22, 5, 22, 32) } },
|
||||
new object[] { 24, 30, new[] { new Range(24, 22, 24, 38), new Range(24, 44, 24, 60) } },
|
||||
new object[] { 24, 52, new[] { new Range(24, 22, 24, 38), new Range(24, 44, 24, 60) } },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,107 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.PowerShell.EditorServices.Hosting;
|
||||
using Microsoft.PowerShell.EditorServices.Services;
|
||||
using Microsoft.PowerShell.EditorServices.Services.TextDocument;
|
||||
using Microsoft.PowerShell.EditorServices.Test;
|
||||
using Xunit;
|
||||
|
||||
namespace PowerShellEditorServices.Test.Services.Symbols
|
||||
{
|
||||
[Trait("Category", "PSScriptAnalyzer")]
|
||||
public class PSScriptAnalyzerTests
|
||||
{
|
||||
private readonly WorkspaceService workspaceService = new(NullLoggerFactory.Instance);
|
||||
private readonly AnalysisService analysisService;
|
||||
private const string script = "function Do-Work {}";
|
||||
|
||||
public PSScriptAnalyzerTests() => analysisService = new(
|
||||
NullLoggerFactory.Instance,
|
||||
languageServer: null,
|
||||
configurationService: null,
|
||||
workspaceService: workspaceService,
|
||||
new HostStartupInfo(
|
||||
name: "",
|
||||
profileId: "",
|
||||
version: null,
|
||||
psHost: null,
|
||||
profilePaths: null,
|
||||
featureFlags: null,
|
||||
additionalModules: null,
|
||||
initialSessionState: null,
|
||||
logPath: null,
|
||||
logLevel: 0,
|
||||
consoleReplEnabled: false,
|
||||
useNullPSHostUI: true,
|
||||
usesLegacyReadLine: false,
|
||||
bundledModulePath: PsesHostFactory.BundledModulePath));
|
||||
|
||||
[Fact]
|
||||
public void IncludesDefaultRules()
|
||||
{
|
||||
Assert.Null(analysisService.AnalysisEngine._settingsParameter);
|
||||
Assert.Equal(AnalysisService.s_defaultRules, analysisService.AnalysisEngine._rulesToInclude);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CanLoadPSScriptAnalyzerAsync()
|
||||
{
|
||||
ScriptFileMarker[] violations = await analysisService
|
||||
.AnalysisEngine
|
||||
.AnalyzeScriptAsync(script);
|
||||
|
||||
Assert.Collection(violations,
|
||||
(actual) =>
|
||||
{
|
||||
Assert.Empty(actual.Corrections);
|
||||
Assert.Equal(ScriptFileMarkerLevel.Warning, actual.Level);
|
||||
Assert.Equal("The cmdlet 'Do-Work' uses an unapproved verb.", actual.Message);
|
||||
Assert.Equal("PSUseApprovedVerbs", actual.RuleName);
|
||||
Assert.Equal("PSScriptAnalyzer", actual.Source);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DoesNotDuplicateScriptMarkersAsync()
|
||||
{
|
||||
ScriptFile scriptFile = workspaceService.GetFileBuffer("untitled:Untitled-1", script);
|
||||
ScriptFile[] scriptFiles = { scriptFile };
|
||||
|
||||
await analysisService
|
||||
.DelayThenInvokeDiagnosticsAsync(scriptFiles, CancellationToken.None);
|
||||
Assert.Single(scriptFile.DiagnosticMarkers);
|
||||
|
||||
// This is repeated to test that the markers are not duplicated.
|
||||
await analysisService
|
||||
.DelayThenInvokeDiagnosticsAsync(scriptFiles, CancellationToken.None);
|
||||
Assert.Single(scriptFile.DiagnosticMarkers);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DoesNotClearParseErrorsAsync()
|
||||
{
|
||||
// Causing a missing closing } parser error
|
||||
ScriptFile scriptFile = workspaceService.GetFileBuffer("untitled:Untitled-2", script.TrimEnd('}'));
|
||||
ScriptFile[] scriptFiles = { scriptFile };
|
||||
|
||||
await analysisService
|
||||
.DelayThenInvokeDiagnosticsAsync(scriptFiles, CancellationToken.None);
|
||||
|
||||
Assert.Collection(scriptFile.DiagnosticMarkers,
|
||||
(actual) =>
|
||||
{
|
||||
Assert.Equal("Missing closing '}' in statement block or type definition.", actual.Message);
|
||||
Assert.Equal("PowerShell", actual.Source);
|
||||
},
|
||||
(actual) =>
|
||||
{
|
||||
Assert.Equal("PSUseApprovedVerbs", actual.RuleName);
|
||||
Assert.Equal("PSScriptAnalyzer", actual.Source);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using Xunit;
|
||||
using Microsoft.PowerShell.EditorServices.Utility;
|
||||
|
||||
namespace PowerShellEditorServices.Test.Session
|
||||
{
|
||||
public class PathEscapingTests
|
||||
{
|
||||
[Trait("Category", "PathEscaping")]
|
||||
[Theory]
|
||||
[InlineData("DebugTest.ps1", "DebugTest.ps1")]
|
||||
[InlineData("../../DebugTest.ps1", "../../DebugTest.ps1")]
|
||||
[InlineData(@"C:\Users\me\Documents\DebugTest.ps1", @"C:\Users\me\Documents\DebugTest.ps1")]
|
||||
[InlineData("/home/me/Documents/weird&folder/script.ps1", "/home/me/Documents/weird&folder/script.ps1")]
|
||||
[InlineData("./path/with some/spaces", "./path/with some/spaces")]
|
||||
[InlineData(@"C:\path\with[some]brackets\file.ps1", @"C:\path\with`[some`]brackets\file.ps1")]
|
||||
[InlineData(@"C:\look\an*\here.ps1", @"C:\look\an`*\here.ps1")]
|
||||
[InlineData("/Users/me/Documents/?here.ps1", "/Users/me/Documents/`?here.ps1")]
|
||||
[InlineData("/Brackets [and s]paces/path.ps1", "/Brackets `[and s`]paces/path.ps1")]
|
||||
[InlineData("/CJK.chars/脚本/hello.ps1", "/CJK.chars/脚本/hello.ps1")]
|
||||
[InlineData("/CJK.chars/脚本/[hello].ps1", "/CJK.chars/脚本/`[hello`].ps1")]
|
||||
[InlineData(@"C:\Animals\утка\quack.ps1", @"C:\Animals\утка\quack.ps1")]
|
||||
[InlineData(@"C:\&nimals\утка\qu*ck?.ps1", @"C:\&nimals\утка\qu`*ck`?.ps1")]
|
||||
public void CorrectlyWildcardEscapesPathsNoSpaces(string unescapedPath, string escapedPath)
|
||||
{
|
||||
string extensionEscapedPath = PathUtils.WildcardEscapePath(unescapedPath);
|
||||
Assert.Equal(escapedPath, extensionEscapedPath);
|
||||
}
|
||||
|
||||
[Trait("Category", "PathEscaping")]
|
||||
[Theory]
|
||||
[InlineData("DebugTest.ps1", "DebugTest.ps1")]
|
||||
[InlineData("../../DebugTest.ps1", "../../DebugTest.ps1")]
|
||||
[InlineData(@"C:\Users\me\Documents\DebugTest.ps1", @"C:\Users\me\Documents\DebugTest.ps1")]
|
||||
[InlineData("/home/me/Documents/weird&folder/script.ps1", "/home/me/Documents/weird&folder/script.ps1")]
|
||||
[InlineData("./path/with some/spaces", "./path/with` some/spaces")]
|
||||
[InlineData(@"C:\path\with[some]brackets\file.ps1", @"C:\path\with`[some`]brackets\file.ps1")]
|
||||
[InlineData(@"C:\look\an*\here.ps1", @"C:\look\an`*\here.ps1")]
|
||||
[InlineData("/Users/me/Documents/?here.ps1", "/Users/me/Documents/`?here.ps1")]
|
||||
[InlineData("/Brackets [and s]paces/path.ps1", "/Brackets` `[and` s`]paces/path.ps1")]
|
||||
[InlineData("/CJK chars/脚本/hello.ps1", "/CJK` chars/脚本/hello.ps1")]
|
||||
[InlineData("/CJK chars/脚本/[hello].ps1", "/CJK` chars/脚本/`[hello`].ps1")]
|
||||
[InlineData(@"C:\Animal s\утка\quack.ps1", @"C:\Animal` s\утка\quack.ps1")]
|
||||
[InlineData(@"C:\&nimals\утка\qu*ck?.ps1", @"C:\&nimals\утка\qu`*ck`?.ps1")]
|
||||
public void CorrectlyWildcardEscapesPathsSpaces(string unescapedPath, string escapedPath)
|
||||
{
|
||||
string extensionEscapedPath = PathUtils.WildcardEscapePath(unescapedPath, escapeSpaces: true);
|
||||
Assert.Equal(escapedPath, extensionEscapedPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,294 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.PowerShell.EditorServices.Hosting;
|
||||
using Microsoft.PowerShell.EditorServices.Services.PowerShell.Console;
|
||||
using Microsoft.PowerShell.EditorServices.Services.PowerShell.Execution;
|
||||
using Microsoft.PowerShell.EditorServices.Services.PowerShell.Host;
|
||||
using Microsoft.PowerShell.EditorServices.Services.PowerShell.Utility;
|
||||
using Microsoft.PowerShell.EditorServices.Test;
|
||||
using Xunit;
|
||||
|
||||
namespace PowerShellEditorServices.Test.Session
|
||||
{
|
||||
using System.Management.Automation;
|
||||
using System.Management.Automation.Runspaces;
|
||||
|
||||
[Trait("Category", "PsesInternalHost")]
|
||||
public class PsesInternalHostTests : IAsyncLifetime
|
||||
{
|
||||
private PsesInternalHost psesHost;
|
||||
|
||||
public async Task InitializeAsync() => psesHost = await PsesHostFactory.Create(NullLoggerFactory.Instance);
|
||||
|
||||
public async Task DisposeAsync() => await psesHost.StopAsync();
|
||||
|
||||
[Fact]
|
||||
public async Task CanExecutePSCommand()
|
||||
{
|
||||
Assert.True(psesHost.IsRunning);
|
||||
PSCommand command = new PSCommand().AddScript("$a = \"foo\"; $a");
|
||||
Task<IReadOnlyList<string>> task = psesHost.ExecutePSCommandAsync<string>(command, CancellationToken.None);
|
||||
IReadOnlyList<string> result = await task;
|
||||
Assert.Equal("foo", result[0]);
|
||||
}
|
||||
|
||||
[Fact] // https://github.com/PowerShell/vscode-powershell/issues/3677
|
||||
public async Task CanHandleThrow()
|
||||
{
|
||||
await psesHost.ExecutePSCommandAsync(
|
||||
new PSCommand().AddScript("throw"),
|
||||
CancellationToken.None,
|
||||
new PowerShellExecutionOptions { ThrowOnError = false });
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CanQueueParallelPSCommands()
|
||||
{
|
||||
// Concurrently initiate 4 requests in the session.
|
||||
Task taskOne = psesHost.ExecutePSCommandAsync(
|
||||
new PSCommand().AddScript("$x = 100"),
|
||||
CancellationToken.None);
|
||||
|
||||
Task taskTwo = psesHost.ExecutePSCommandAsync(
|
||||
new PSCommand().AddScript("$x += 200"),
|
||||
CancellationToken.None);
|
||||
|
||||
Task taskThree = psesHost.ExecutePSCommandAsync(
|
||||
new PSCommand().AddScript("$x = $x / 100"),
|
||||
CancellationToken.None);
|
||||
|
||||
Task<IReadOnlyList<int>> resultTask = psesHost.ExecutePSCommandAsync<int>(
|
||||
new PSCommand().AddScript("$x"),
|
||||
CancellationToken.None);
|
||||
|
||||
// Wait for all of the executes to complete.
|
||||
await Task.WhenAll(taskOne, taskTwo, taskThree, resultTask);
|
||||
|
||||
// Sanity checks
|
||||
Assert.Equal(RunspaceState.Opened, psesHost.Runspace.RunspaceStateInfo.State);
|
||||
|
||||
// 100 + 200 = 300, then divided by 100 is 3. We are ensuring that
|
||||
// the commands were executed in the sequence they were called.
|
||||
Assert.Equal(3, (await resultTask)[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CanCancelExecutionWithToken()
|
||||
{
|
||||
using CancellationTokenSource cancellationSource = new(millisecondsDelay: 1000);
|
||||
await Assert.ThrowsAsync<TaskCanceledException>(() =>
|
||||
{
|
||||
return psesHost.ExecutePSCommandAsync(
|
||||
new PSCommand().AddScript("Start-Sleep 10"),
|
||||
cancellationSource.Token);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD003:Avoid awaiting foreign Tasks", Justification = "Explicitly checking task cancellation status.")]
|
||||
public async Task CanCancelExecutionWithMethod()
|
||||
{
|
||||
Task executeTask = psesHost.ExecutePSCommandAsync(
|
||||
new PSCommand().AddScript("Start-Sleep 10"),
|
||||
CancellationToken.None);
|
||||
|
||||
// Cancel the task after 1 second in another thread.
|
||||
Task.Run(() => { Thread.Sleep(1000); psesHost.CancelCurrentTask(); });
|
||||
await Assert.ThrowsAsync<TaskCanceledException>(() => executeTask);
|
||||
Assert.True(executeTask.IsCanceled);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CanHandleNoProfiles()
|
||||
{
|
||||
// Call LoadProfiles with profile paths that won't exist, and assert that it does not
|
||||
// throw PSInvalidOperationException (which it previously did when it tried to invoke an
|
||||
// empty command).
|
||||
ProfilePathInfo emptyProfilePaths = new("", "", "", "");
|
||||
await psesHost.ExecuteDelegateAsync(
|
||||
"LoadProfiles",
|
||||
executionOptions: null,
|
||||
(pwsh, _) =>
|
||||
{
|
||||
pwsh.LoadProfiles(emptyProfilePaths);
|
||||
Assert.Empty(pwsh.Commands.Commands);
|
||||
},
|
||||
CancellationToken.None);
|
||||
}
|
||||
|
||||
// NOTE: Tests where we call functions that use PowerShell runspaces are slightly more
|
||||
// complicated than one would expect because we explicitly need the methods to run on the
|
||||
// pipeline thread, otherwise Windows complains about the the thread's apartment state not
|
||||
// matching. Hence we use a delegate where it looks like we could just call the method.
|
||||
|
||||
[Fact]
|
||||
public async Task CanHandleBrokenPrompt()
|
||||
{
|
||||
_ = await Assert.ThrowsAsync<RuntimeException>(() =>
|
||||
{
|
||||
return psesHost.ExecutePSCommandAsync(
|
||||
new PSCommand().AddScript("function prompt { throw }; prompt"),
|
||||
CancellationToken.None);
|
||||
});
|
||||
|
||||
string prompt = await psesHost.ExecuteDelegateAsync(
|
||||
nameof(psesHost.GetPrompt),
|
||||
executionOptions: null,
|
||||
(_, _) => psesHost.GetPrompt(CancellationToken.None),
|
||||
CancellationToken.None);
|
||||
|
||||
Assert.Equal(PsesInternalHost.DefaultPrompt, prompt);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CanHandleUndefinedPrompt()
|
||||
{
|
||||
Assert.Empty(await psesHost.ExecutePSCommandAsync<PSObject>(
|
||||
new PSCommand().AddScript("Remove-Item function:prompt; Get-Item function:prompt -ErrorAction Ignore"),
|
||||
CancellationToken.None));
|
||||
|
||||
string prompt = await psesHost.ExecuteDelegateAsync(
|
||||
nameof(psesHost.GetPrompt),
|
||||
executionOptions: null,
|
||||
(_, _) => psesHost.GetPrompt(CancellationToken.None),
|
||||
CancellationToken.None);
|
||||
|
||||
Assert.Equal(PsesInternalHost.DefaultPrompt, prompt);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CanRunOnIdleTask()
|
||||
{
|
||||
IReadOnlyList<PSObject> task = await psesHost.ExecutePSCommandAsync<PSObject>(
|
||||
new PSCommand().AddScript("$handled = $false; Register-EngineEvent -SourceIdentifier PowerShell.OnIdle -MaxTriggerCount 1 -Action { $global:handled = $true }"),
|
||||
CancellationToken.None);
|
||||
|
||||
IReadOnlyList<bool> handled = await psesHost.ExecutePSCommandAsync<bool>(
|
||||
new PSCommand().AddScript("$handled"),
|
||||
CancellationToken.None);
|
||||
|
||||
Assert.Collection(handled, Assert.False);
|
||||
|
||||
await psesHost.ExecuteDelegateAsync(
|
||||
nameof(psesHost.OnPowerShellIdle),
|
||||
executionOptions: null,
|
||||
(_, _) => psesHost.OnPowerShellIdle(CancellationToken.None),
|
||||
CancellationToken.None);
|
||||
|
||||
// TODO: Why is this racy?
|
||||
Thread.Sleep(2000);
|
||||
|
||||
handled = await psesHost.ExecutePSCommandAsync<bool>(
|
||||
new PSCommand().AddScript("$handled"),
|
||||
CancellationToken.None);
|
||||
|
||||
Assert.Collection(handled, Assert.True);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CanLoadPSReadLine()
|
||||
{
|
||||
Assert.True(await psesHost.ExecuteDelegateAsync(
|
||||
nameof(psesHost.TryLoadPSReadLine),
|
||||
executionOptions: null,
|
||||
(pwsh, _) => psesHost.TryLoadPSReadLine(
|
||||
pwsh,
|
||||
(EngineIntrinsics)pwsh.Runspace.SessionStateProxy.GetVariable("ExecutionContext"),
|
||||
out IReadLine readLine),
|
||||
CancellationToken.None));
|
||||
}
|
||||
|
||||
// This test asserts that we do not mess up the console encoding, which leads to native
|
||||
// commands receiving piped input failing.
|
||||
[Fact]
|
||||
public async Task ExecutesNativeCommandsCorrectly()
|
||||
{
|
||||
await psesHost.ExecutePSCommandAsync(
|
||||
new PSCommand().AddScript("\"protocol=https`nhost=myhost.com`nusername=john`npassword=doe`n`n\" | git.exe credential approve; if ($LastExitCode) { throw }"),
|
||||
CancellationToken.None);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("")] // Regression test for "unset" path.
|
||||
[InlineData(@"C:\Some\Bad\Directory")] // Non-existent directory.
|
||||
[InlineData("testhost.dll")] // Existent file.
|
||||
public async Task CanHandleBadInitialWorkingDirectory(string path)
|
||||
{
|
||||
string cwd = Environment.CurrentDirectory;
|
||||
await psesHost.SetInitialWorkingDirectoryAsync(path, CancellationToken.None);
|
||||
|
||||
IReadOnlyList<string> getLocation = await psesHost.ExecutePSCommandAsync<string>(
|
||||
new PSCommand().AddCommand("Get-Location"),
|
||||
CancellationToken.None);
|
||||
Assert.Collection(getLocation, (d) => Assert.Equal(cwd, d, ignoreCase: true));
|
||||
}
|
||||
}
|
||||
|
||||
[Trait("Category", "PsesInternalHost")]
|
||||
public class PsesInternalHostWithProfileTests : IAsyncLifetime
|
||||
{
|
||||
private PsesInternalHost psesHost;
|
||||
|
||||
public async Task InitializeAsync() => psesHost = await PsesHostFactory.Create(NullLoggerFactory.Instance, loadProfiles: true);
|
||||
|
||||
public async Task DisposeAsync() => await psesHost.StopAsync();
|
||||
|
||||
[Fact]
|
||||
public async Task CanResolveAndLoadProfilesForHostId()
|
||||
{
|
||||
// Ensure that the $PROFILE variable is a string with the value of CurrentUserCurrentHost.
|
||||
IReadOnlyList<string> profileVariable = await psesHost.ExecutePSCommandAsync<string>(
|
||||
new PSCommand().AddScript("$PROFILE"),
|
||||
CancellationToken.None);
|
||||
|
||||
Assert.Collection(profileVariable,
|
||||
(p) => Assert.Equal(PsesHostFactory.TestProfilePaths.CurrentUserCurrentHost, p));
|
||||
|
||||
// Ensure that all the profile paths are set in the correct note properties.
|
||||
IReadOnlyList<string> profileProperties = await psesHost.ExecutePSCommandAsync<string>(
|
||||
new PSCommand().AddScript("$PROFILE | Get-Member -Type NoteProperty"),
|
||||
CancellationToken.None);
|
||||
|
||||
Assert.Collection(profileProperties,
|
||||
(p) => Assert.Equal($"string AllUsersAllHosts={PsesHostFactory.TestProfilePaths.AllUsersAllHosts}", p, ignoreCase: true),
|
||||
(p) => Assert.Equal($"string AllUsersCurrentHost={PsesHostFactory.TestProfilePaths.AllUsersCurrentHost}", p, ignoreCase: true),
|
||||
(p) => Assert.Equal($"string CurrentUserAllHosts={PsesHostFactory.TestProfilePaths.CurrentUserAllHosts}", p, ignoreCase: true),
|
||||
(p) => Assert.Equal($"string CurrentUserCurrentHost={PsesHostFactory.TestProfilePaths.CurrentUserCurrentHost}", p, ignoreCase: true));
|
||||
|
||||
// Ensure that the profile was loaded. The profile also checks that $PROFILE was defined.
|
||||
IReadOnlyList<bool> profileLoaded = await psesHost.ExecutePSCommandAsync<bool>(
|
||||
new PSCommand().AddScript("Assert-ProfileLoaded"),
|
||||
CancellationToken.None);
|
||||
|
||||
Assert.Collection(profileLoaded, Assert.True);
|
||||
}
|
||||
|
||||
// This test specifically relies on a handler registered in the test profile, and on the
|
||||
// test host loading the profiles during startup, that way the pipeline timing is
|
||||
// consistent.
|
||||
[Fact]
|
||||
public async Task CanRunOnIdleInProfileTask()
|
||||
{
|
||||
await psesHost.ExecuteDelegateAsync(
|
||||
nameof(psesHost.OnPowerShellIdle),
|
||||
executionOptions: null,
|
||||
(_, _) => psesHost.OnPowerShellIdle(CancellationToken.None),
|
||||
CancellationToken.None);
|
||||
|
||||
// TODO: Why is this racy?
|
||||
Thread.Sleep(2000);
|
||||
|
||||
IReadOnlyList<bool> handled = await psesHost.ExecutePSCommandAsync<bool>(
|
||||
new PSCommand().AddScript("$handledInProfile"),
|
||||
CancellationToken.None);
|
||||
|
||||
Assert.Collection(handled, Assert.True);
|
||||
}
|
||||
}
|
||||
}
|
||||
674
test/PowerShellEditorServices.Test/Session/ScriptFileTests.cs
Normal file
674
test/PowerShellEditorServices.Test/Session/ScriptFileTests.cs
Normal file
|
|
@ -0,0 +1,674 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Microsoft.PowerShell.EditorServices.Services.TextDocument;
|
||||
using Microsoft.PowerShell.EditorServices.Test.Shared;
|
||||
using Microsoft.PowerShell.EditorServices.Utility;
|
||||
using OmniSharp.Extensions.LanguageServer.Protocol;
|
||||
using Xunit;
|
||||
|
||||
namespace PowerShellEditorServices.Test.Session
|
||||
{
|
||||
public class ScriptFileChangeTests
|
||||
{
|
||||
#if CoreCLR
|
||||
private static readonly Version PowerShellVersion = new(7, 2);
|
||||
#else
|
||||
private static readonly Version PowerShellVersion = new(5, 1);
|
||||
#endif
|
||||
|
||||
[Trait("Category", "ScriptFile")]
|
||||
[Fact]
|
||||
public void CanApplySingleLineInsert()
|
||||
{
|
||||
AssertFileChange(
|
||||
"This is a test.",
|
||||
"This is a working test.",
|
||||
new FileChange
|
||||
{
|
||||
Line = 1,
|
||||
EndLine = 1,
|
||||
Offset = 10,
|
||||
EndOffset = 10,
|
||||
InsertString = " working"
|
||||
});
|
||||
}
|
||||
|
||||
[Trait("Category", "ScriptFile")]
|
||||
[Fact]
|
||||
public void CanApplySingleLineReplace()
|
||||
{
|
||||
AssertFileChange(
|
||||
"This is a potentially broken test.",
|
||||
"This is a working test.",
|
||||
new FileChange
|
||||
{
|
||||
Line = 1,
|
||||
EndLine = 1,
|
||||
Offset = 11,
|
||||
EndOffset = 29,
|
||||
InsertString = "working"
|
||||
});
|
||||
}
|
||||
|
||||
[Trait("Category", "ScriptFile")]
|
||||
[Fact]
|
||||
public void CanApplySingleLineDelete()
|
||||
{
|
||||
AssertFileChange(
|
||||
"This is a test of the emergency broadcasting system.",
|
||||
"This is a test.",
|
||||
new FileChange
|
||||
{
|
||||
Line = 1,
|
||||
EndLine = 1,
|
||||
Offset = 15,
|
||||
EndOffset = 52,
|
||||
InsertString = ""
|
||||
});
|
||||
}
|
||||
|
||||
[Trait("Category", "ScriptFile")]
|
||||
[Fact]
|
||||
public void CanApplyMultiLineInsert()
|
||||
{
|
||||
AssertFileChange(
|
||||
TestUtilities.NormalizeNewlines("first\nsecond\nfifth"),
|
||||
TestUtilities.NormalizeNewlines("first\nsecond\nthird\nfourth\nfifth"),
|
||||
new FileChange
|
||||
{
|
||||
Line = 3,
|
||||
EndLine = 3,
|
||||
Offset = 1,
|
||||
EndOffset = 1,
|
||||
InsertString = TestUtilities.NormalizeNewlines("third\nfourth\n")
|
||||
});
|
||||
}
|
||||
|
||||
[Trait("Category", "ScriptFile")]
|
||||
[Fact]
|
||||
public void CanApplyMultiLineReplace()
|
||||
{
|
||||
AssertFileChange(
|
||||
TestUtilities.NormalizeNewlines("first\nsecoXX\nXXfth"),
|
||||
TestUtilities.NormalizeNewlines("first\nsecond\nthird\nfourth\nfifth"),
|
||||
new FileChange
|
||||
{
|
||||
Line = 2,
|
||||
EndLine = 3,
|
||||
Offset = 5,
|
||||
EndOffset = 3,
|
||||
InsertString = TestUtilities.NormalizeNewlines("nd\nthird\nfourth\nfi")
|
||||
});
|
||||
}
|
||||
|
||||
[Trait("Category", "ScriptFile")]
|
||||
[Fact]
|
||||
public void CanApplyMultiLineReplaceWithRemovedLines()
|
||||
{
|
||||
AssertFileChange(
|
||||
TestUtilities.NormalizeNewlines("first\nsecoXX\nREMOVE\nTHESE\nLINES\nXXfth"),
|
||||
TestUtilities.NormalizeNewlines("first\nsecond\nthird\nfourth\nfifth"),
|
||||
new FileChange
|
||||
{
|
||||
Line = 2,
|
||||
EndLine = 6,
|
||||
Offset = 5,
|
||||
EndOffset = 3,
|
||||
InsertString = TestUtilities.NormalizeNewlines("nd\nthird\nfourth\nfi")
|
||||
});
|
||||
}
|
||||
|
||||
[Trait("Category", "ScriptFile")]
|
||||
[Fact]
|
||||
public void CanApplyMultiLineDelete()
|
||||
{
|
||||
AssertFileChange(
|
||||
TestUtilities.NormalizeNewlines("first\nsecond\nREMOVE\nTHESE\nLINES\nthird"),
|
||||
TestUtilities.NormalizeNewlines("first\nsecond\nthird"),
|
||||
new FileChange
|
||||
{
|
||||
Line = 3,
|
||||
EndLine = 6,
|
||||
Offset = 1,
|
||||
EndOffset = 1,
|
||||
InsertString = ""
|
||||
});
|
||||
}
|
||||
|
||||
[Trait("Category", "ScriptFile")]
|
||||
[Fact]
|
||||
public void CanApplyEditsToEndOfFile()
|
||||
{
|
||||
AssertFileChange(
|
||||
TestUtilities.NormalizeNewlines("line1\nline2\nline3\n\n"),
|
||||
TestUtilities.NormalizeNewlines("line1\nline2\nline3\n\n\n\n"),
|
||||
new FileChange
|
||||
{
|
||||
Line = 5,
|
||||
EndLine = 5,
|
||||
Offset = 1,
|
||||
EndOffset = 1,
|
||||
InsertString = Environment.NewLine + Environment.NewLine
|
||||
});
|
||||
}
|
||||
|
||||
[Trait("Category", "ScriptFile")]
|
||||
[Fact]
|
||||
public void CanAppendToEndOfFile()
|
||||
{
|
||||
AssertFileChange(
|
||||
TestUtilities.NormalizeNewlines("line1\nline2\nline3"),
|
||||
TestUtilities.NormalizeNewlines("line1\nline2\nline3\nline4\nline5"),
|
||||
new FileChange
|
||||
{
|
||||
Line = 4,
|
||||
EndLine = 5,
|
||||
Offset = 1,
|
||||
EndOffset = 1,
|
||||
InsertString = $"line4{Environment.NewLine}line5"
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
[Trait("Category", "ScriptFile")]
|
||||
[Fact]
|
||||
public void ThrowsExceptionWithEditOutsideOfRange()
|
||||
{
|
||||
Assert.Throws<ArgumentOutOfRangeException>(
|
||||
() =>
|
||||
{
|
||||
AssertFileChange(
|
||||
TestUtilities.NormalizeNewlines("first\nsecond\nREMOVE\nTHESE\nLINES\nthird"),
|
||||
TestUtilities.NormalizeNewlines("first\nsecond\nthird"),
|
||||
new FileChange
|
||||
{
|
||||
Line = 3,
|
||||
EndLine = 8,
|
||||
Offset = 1,
|
||||
EndOffset = 1,
|
||||
InsertString = ""
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
[Trait("Category", "ScriptFile")]
|
||||
[Fact]
|
||||
public void CanDeleteFromEndOfFile()
|
||||
{
|
||||
AssertFileChange(
|
||||
TestUtilities.NormalizeNewlines("line1\nline2\nline3\nline4"),
|
||||
TestUtilities.NormalizeNewlines("line1\nline2"),
|
||||
new FileChange
|
||||
{
|
||||
Line = 3,
|
||||
EndLine = 5,
|
||||
Offset = 1,
|
||||
EndOffset = 1,
|
||||
InsertString = ""
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
[Trait("Category", "ScriptFile")]
|
||||
[Fact]
|
||||
public void UpdatesParseErrorDiagnosticMarkers()
|
||||
{
|
||||
ScriptFile myScript = CreateScriptFile(TestUtilities.NormalizeNewlines("{\n{"));
|
||||
|
||||
// Verify parse errors were detected on file open
|
||||
Assert.Collection(myScript.DiagnosticMarkers.OrderBy(dm => dm.ScriptRegion.StartLineNumber),
|
||||
(actual) =>
|
||||
{
|
||||
Assert.Equal(1, actual.ScriptRegion.StartLineNumber);
|
||||
Assert.Equal("Missing closing '}' in statement block or type definition.", actual.Message);
|
||||
Assert.Equal("PowerShell", actual.Source);
|
||||
},
|
||||
(actual) =>
|
||||
{
|
||||
Assert.Equal(2, actual.ScriptRegion.StartLineNumber);
|
||||
Assert.Equal("Missing closing '}' in statement block or type definition.", actual.Message);
|
||||
Assert.Equal("PowerShell", actual.Source);
|
||||
});
|
||||
|
||||
// Remove second {
|
||||
myScript.ApplyChange(
|
||||
new FileChange
|
||||
{
|
||||
Line = 2,
|
||||
EndLine = 2,
|
||||
Offset = 1,
|
||||
EndOffset = 2,
|
||||
InsertString = ""
|
||||
});
|
||||
|
||||
// Verify parse errors were updated on file change
|
||||
Assert.Collection(myScript.DiagnosticMarkers,
|
||||
(actual) =>
|
||||
{
|
||||
Assert.Equal(1, actual.ScriptRegion.StartLineNumber);
|
||||
Assert.Equal("Missing closing '}' in statement block or type definition.", actual.Message);
|
||||
Assert.Equal("PowerShell", actual.Source);
|
||||
});
|
||||
}
|
||||
|
||||
internal static ScriptFile CreateScriptFile(string initialString)
|
||||
{
|
||||
using StringReader stringReader = new(initialString);
|
||||
// Create an in-memory file from the StringReader
|
||||
ScriptFile fileToChange =
|
||||
new(
|
||||
// Use any absolute path. Even if it doesn't exist.
|
||||
DocumentUri.FromFileSystemPath(Path.Combine(Path.GetTempPath(), "TestFile.ps1")),
|
||||
stringReader,
|
||||
PowerShellVersion);
|
||||
|
||||
return fileToChange;
|
||||
}
|
||||
|
||||
private static void AssertFileChange(
|
||||
string initialString,
|
||||
string expectedString,
|
||||
FileChange fileChange)
|
||||
{
|
||||
// Create an in-memory file from the StringReader
|
||||
ScriptFile fileToChange = CreateScriptFile(initialString);
|
||||
|
||||
// Apply the FileChange and assert the resulting contents
|
||||
fileToChange.ApplyChange(fileChange);
|
||||
Assert.Equal(expectedString, fileToChange.Contents);
|
||||
}
|
||||
}
|
||||
|
||||
public class ScriptFileGetLinesTests
|
||||
{
|
||||
private static readonly string TestString_NoTrailingNewline = TestUtilities.NormalizeNewlines(
|
||||
"Line One\nLine Two\nLine Three\nLine Four\nLine Five");
|
||||
|
||||
private static readonly string TestString_TrailingNewline = TestUtilities.NormalizeNewlines(
|
||||
TestString_NoTrailingNewline + "\n");
|
||||
|
||||
private static readonly string[] s_newLines = new string[] { Environment.NewLine };
|
||||
|
||||
private static readonly string[] s_testStringLines_noTrailingNewline = TestString_NoTrailingNewline.Split(s_newLines, StringSplitOptions.None);
|
||||
|
||||
private static readonly string[] s_testStringLines_trailingNewline = TestString_TrailingNewline.Split(s_newLines, StringSplitOptions.None);
|
||||
|
||||
private readonly ScriptFile _scriptFile_trailingNewline;
|
||||
|
||||
private readonly ScriptFile _scriptFile_noTrailingNewline;
|
||||
|
||||
public ScriptFileGetLinesTests()
|
||||
{
|
||||
_scriptFile_noTrailingNewline = ScriptFileChangeTests.CreateScriptFile(
|
||||
TestString_NoTrailingNewline);
|
||||
|
||||
_scriptFile_trailingNewline = ScriptFileChangeTests.CreateScriptFile(
|
||||
TestString_TrailingNewline);
|
||||
}
|
||||
|
||||
[Trait("Category", "ScriptFile")]
|
||||
[Fact]
|
||||
public void CanGetWholeLine()
|
||||
{
|
||||
string[] lines =
|
||||
_scriptFile_noTrailingNewline.GetLinesInRange(
|
||||
new BufferRange(5, 1, 5, 10));
|
||||
|
||||
Assert.Single(lines);
|
||||
Assert.Equal("Line Five", lines[0]);
|
||||
}
|
||||
|
||||
[Trait("Category", "ScriptFile")]
|
||||
[Fact]
|
||||
public void CanGetMultipleWholeLines()
|
||||
{
|
||||
string[] lines =
|
||||
_scriptFile_noTrailingNewline.GetLinesInRange(
|
||||
new BufferRange(2, 1, 4, 10));
|
||||
|
||||
Assert.Equal(s_testStringLines_noTrailingNewline.Skip(1).Take(3), lines);
|
||||
}
|
||||
|
||||
[Trait("Category", "ScriptFile")]
|
||||
[Fact]
|
||||
public void CanGetSubstringInSingleLine()
|
||||
{
|
||||
string[] lines =
|
||||
_scriptFile_noTrailingNewline.GetLinesInRange(
|
||||
new BufferRange(4, 3, 4, 8));
|
||||
|
||||
Assert.Single(lines);
|
||||
Assert.Equal("ne Fo", lines[0]);
|
||||
}
|
||||
|
||||
[Trait("Category", "ScriptFile")]
|
||||
[Fact]
|
||||
public void CanGetEmptySubstringRange()
|
||||
{
|
||||
string[] lines =
|
||||
_scriptFile_noTrailingNewline.GetLinesInRange(
|
||||
new BufferRange(4, 3, 4, 3));
|
||||
|
||||
Assert.Single(lines);
|
||||
Assert.Equal("", lines[0]);
|
||||
}
|
||||
|
||||
[Trait("Category", "ScriptFile")]
|
||||
[Fact]
|
||||
public void CanGetSubstringInMultipleLines()
|
||||
{
|
||||
string[] expectedLines = new string[]
|
||||
{
|
||||
"Two",
|
||||
"Line Three",
|
||||
"Line Fou"
|
||||
};
|
||||
|
||||
string[] lines =
|
||||
_scriptFile_noTrailingNewline.GetLinesInRange(
|
||||
new BufferRange(2, 6, 4, 9));
|
||||
|
||||
Assert.Equal(expectedLines, lines);
|
||||
}
|
||||
|
||||
[Trait("Category", "ScriptFile")]
|
||||
[Fact]
|
||||
public void CanGetRangeAtLineBoundaries()
|
||||
{
|
||||
string[] expectedLines = new string[]
|
||||
{
|
||||
"",
|
||||
"Line Three",
|
||||
""
|
||||
};
|
||||
|
||||
string[] lines =
|
||||
_scriptFile_noTrailingNewline.GetLinesInRange(
|
||||
new BufferRange(2, 9, 4, 1));
|
||||
|
||||
Assert.Equal(expectedLines, lines);
|
||||
}
|
||||
|
||||
[Trait("Category", "ScriptFile")]
|
||||
[Fact]
|
||||
public void CanSplitLinesNoTrailingNewline() => Assert.Equal(s_testStringLines_noTrailingNewline, _scriptFile_noTrailingNewline.FileLines);
|
||||
|
||||
[Trait("Category", "ScriptFile")]
|
||||
[Fact]
|
||||
public void CanSplitLinesTrailingNewline() => Assert.Equal(s_testStringLines_trailingNewline, _scriptFile_trailingNewline.FileLines);
|
||||
|
||||
[Trait("Category", "ScriptFile")]
|
||||
[Fact]
|
||||
public void CanGetSameLinesWithUnixLineBreaks()
|
||||
{
|
||||
ScriptFile unixFile = ScriptFileChangeTests.CreateScriptFile(TestString_NoTrailingNewline.Replace("\r\n", "\n"));
|
||||
Assert.Equal(_scriptFile_noTrailingNewline.FileLines, unixFile.FileLines);
|
||||
}
|
||||
|
||||
[Trait("Category", "ScriptFile")]
|
||||
[Fact]
|
||||
public void CanGetLineForEmptyString()
|
||||
{
|
||||
ScriptFile emptyFile = ScriptFileChangeTests.CreateScriptFile(string.Empty);
|
||||
Assert.Single(emptyFile.FileLines);
|
||||
Assert.Equal(string.Empty, emptyFile.FileLines[0]);
|
||||
}
|
||||
|
||||
[Trait("Category", "ScriptFile")]
|
||||
[Fact]
|
||||
public void CanGetLineForSpace()
|
||||
{
|
||||
ScriptFile spaceFile = ScriptFileChangeTests.CreateScriptFile(" ");
|
||||
Assert.Single(spaceFile.FileLines);
|
||||
Assert.Equal(" ", spaceFile.FileLines[0]);
|
||||
}
|
||||
}
|
||||
|
||||
public class ScriptFilePositionTests
|
||||
{
|
||||
private readonly ScriptFile scriptFile;
|
||||
|
||||
public ScriptFilePositionTests()
|
||||
{
|
||||
scriptFile =
|
||||
ScriptFileChangeTests.CreateScriptFile(@"
|
||||
First line
|
||||
Second line is longer
|
||||
Third line
|
||||
");
|
||||
}
|
||||
|
||||
[Trait("Category", "ScriptFile")]
|
||||
[Fact]
|
||||
public void CanOffsetByLine()
|
||||
{
|
||||
AssertNewPosition(
|
||||
1, 1,
|
||||
2, 0,
|
||||
3, 1);
|
||||
|
||||
AssertNewPosition(
|
||||
3, 1,
|
||||
-2, 0,
|
||||
1, 1);
|
||||
}
|
||||
|
||||
[Trait("Category", "ScriptFile")]
|
||||
[Fact]
|
||||
public void CanOffsetByColumn()
|
||||
{
|
||||
AssertNewPosition(
|
||||
2, 1,
|
||||
0, 2,
|
||||
2, 3);
|
||||
|
||||
AssertNewPosition(
|
||||
2, 5,
|
||||
0, -3,
|
||||
2, 2);
|
||||
}
|
||||
|
||||
[Trait("Category", "ScriptFile")]
|
||||
[Fact]
|
||||
public void ThrowsWhenPositionOutOfRange()
|
||||
{
|
||||
// Less than line range
|
||||
Assert.Throws<ArgumentOutOfRangeException>(
|
||||
() =>
|
||||
{
|
||||
scriptFile.CalculatePosition(
|
||||
new BufferPosition(1, 1),
|
||||
-10, 0);
|
||||
});
|
||||
|
||||
// Greater than line range
|
||||
Assert.Throws<ArgumentOutOfRangeException>(
|
||||
() =>
|
||||
{
|
||||
scriptFile.CalculatePosition(
|
||||
new BufferPosition(1, 1),
|
||||
10, 0);
|
||||
});
|
||||
|
||||
// Less than column range
|
||||
Assert.Throws<ArgumentOutOfRangeException>(
|
||||
() =>
|
||||
{
|
||||
scriptFile.CalculatePosition(
|
||||
new BufferPosition(1, 1),
|
||||
0, -10);
|
||||
});
|
||||
|
||||
// Greater than column range
|
||||
Assert.Throws<ArgumentOutOfRangeException>(
|
||||
() =>
|
||||
{
|
||||
scriptFile.CalculatePosition(
|
||||
new BufferPosition(1, 1),
|
||||
0, 10);
|
||||
});
|
||||
}
|
||||
|
||||
[Trait("Category", "ScriptFile")]
|
||||
[Fact]
|
||||
public void CanFindBeginningOfLine()
|
||||
{
|
||||
AssertNewPosition(
|
||||
4, 12,
|
||||
pos => pos.GetLineStart(),
|
||||
4, 5);
|
||||
}
|
||||
|
||||
[Trait("Category", "ScriptFile")]
|
||||
[Fact]
|
||||
public void CanFindEndOfLine()
|
||||
{
|
||||
AssertNewPosition(
|
||||
4, 12,
|
||||
pos => pos.GetLineEnd(),
|
||||
4, 15);
|
||||
}
|
||||
|
||||
[Trait("Category", "ScriptFile")]
|
||||
[Fact]
|
||||
public void CanComposePositionOperations()
|
||||
{
|
||||
AssertNewPosition(
|
||||
4, 12,
|
||||
pos => pos.AddOffset(-1, 1).GetLineStart(),
|
||||
3, 3);
|
||||
}
|
||||
|
||||
private void AssertNewPosition(
|
||||
int originalLine, int originalColumn,
|
||||
int lineOffset, int columnOffset,
|
||||
int expectedLine, int expectedColumn)
|
||||
{
|
||||
AssertNewPosition(
|
||||
originalLine, originalColumn,
|
||||
pos => pos.AddOffset(lineOffset, columnOffset),
|
||||
expectedLine, expectedColumn);
|
||||
}
|
||||
|
||||
private void AssertNewPosition(
|
||||
int originalLine, int originalColumn,
|
||||
Func<FilePosition, FilePosition> positionOperation,
|
||||
int expectedLine, int expectedColumn)
|
||||
{
|
||||
FilePosition newPosition =
|
||||
positionOperation(
|
||||
new FilePosition(
|
||||
scriptFile,
|
||||
originalLine,
|
||||
originalColumn));
|
||||
|
||||
Assert.Equal(expectedLine, newPosition.Line);
|
||||
Assert.Equal(expectedColumn, newPosition.Column);
|
||||
}
|
||||
}
|
||||
|
||||
public class ScriptFileConstructorTests
|
||||
{
|
||||
private static readonly Version PowerShellVersion = new("5.0");
|
||||
|
||||
[Trait("Category", "ScriptFile")]
|
||||
[Fact]
|
||||
public void PropertiesInitializedCorrectlyForFile()
|
||||
{
|
||||
// Use any absolute path. Even if it doesn't exist.
|
||||
string path = Path.Combine(Path.GetTempPath(), "TestFile.ps1");
|
||||
ScriptFile scriptFile = ScriptFileChangeTests.CreateScriptFile("");
|
||||
|
||||
Assert.Equal(path, scriptFile.FilePath, ignoreCase: !VersionUtils.IsLinux);
|
||||
Assert.True(scriptFile.IsAnalysisEnabled);
|
||||
Assert.False(scriptFile.IsInMemory);
|
||||
Assert.Empty(scriptFile.DiagnosticMarkers);
|
||||
Assert.Single(scriptFile.ScriptTokens);
|
||||
Assert.Single(scriptFile.FileLines);
|
||||
}
|
||||
|
||||
[Trait("Category", "ScriptFile")]
|
||||
[Fact]
|
||||
public void PropertiesInitializedCorrectlyForUntitled()
|
||||
{
|
||||
const string path = "untitled:untitled-1";
|
||||
|
||||
// 3 lines and 10 tokens in this script.
|
||||
const string script = @"function foo() {
|
||||
'foo'
|
||||
}";
|
||||
|
||||
using StringReader stringReader = new(script);
|
||||
// Create an in-memory file from the StringReader
|
||||
ScriptFile scriptFile = new(DocumentUri.From(path), stringReader, PowerShellVersion);
|
||||
|
||||
Assert.Equal(path, scriptFile.FilePath);
|
||||
Assert.Equal(path, scriptFile.DocumentUri);
|
||||
Assert.True(scriptFile.IsAnalysisEnabled);
|
||||
Assert.True(scriptFile.IsInMemory);
|
||||
Assert.Empty(scriptFile.DiagnosticMarkers);
|
||||
Assert.Equal(10, scriptFile.ScriptTokens.Length);
|
||||
Assert.Equal(3, scriptFile.FileLines.Count);
|
||||
}
|
||||
|
||||
[Trait("Category", "ScriptFile")]
|
||||
[Fact]
|
||||
public void DocumentUriReturnsCorrectStringForAbsolutePath()
|
||||
{
|
||||
string path;
|
||||
ScriptFile scriptFile;
|
||||
StringReader emptyStringReader = new("");
|
||||
|
||||
if (Environment.OSVersion.Platform == PlatformID.Win32NT)
|
||||
{
|
||||
path = @"C:\Users\AmosBurton\projects\Rocinate\ProtoMolecule.ps1";
|
||||
scriptFile = new ScriptFile(DocumentUri.FromFileSystemPath(path), emptyStringReader, PowerShellVersion);
|
||||
Assert.Equal("file:///c:/Users/AmosBurton/projects/Rocinate/ProtoMolecule.ps1", scriptFile.DocumentUri);
|
||||
|
||||
path = @"c:\Users\BobbieDraper\projects\Rocinate\foo's_~#-[@] +,;=%.ps1";
|
||||
scriptFile = new ScriptFile(DocumentUri.FromFileSystemPath(path), emptyStringReader, PowerShellVersion);
|
||||
Assert.Equal("file:///c:/Users/BobbieDraper/projects/Rocinate/foo%27s_~%23-%5B%40%5D%20%2B%2C%3B%3D%25.ps1", scriptFile.DocumentUri);
|
||||
|
||||
// Test UNC path
|
||||
path = @"\\ClarissaMao\projects\Rocinate\foo's_~#-[@] +,;=%.ps1";
|
||||
scriptFile = new ScriptFile(DocumentUri.FromFileSystemPath(path), emptyStringReader, PowerShellVersion);
|
||||
// UNC authorities are lowercased. This is what VS Code does as well.
|
||||
Assert.Equal("file://clarissamao/projects/Rocinate/foo%27s_~%23-%5B%40%5D%20%2B%2C%3B%3D%25.ps1", scriptFile.DocumentUri);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Test the following only on Linux and macOS.
|
||||
path = "/home/AlexKamal/projects/Rocinate/ProtoMolecule.ps1";
|
||||
scriptFile = new ScriptFile(DocumentUri.FromFileSystemPath(path), emptyStringReader, PowerShellVersion);
|
||||
Assert.Equal("file:///home/AlexKamal/projects/Rocinate/ProtoMolecule.ps1", scriptFile.DocumentUri);
|
||||
|
||||
path = "/home/BobbieDraper/projects/Rocinate/foo's_~#-[@] +,;=%.ps1";
|
||||
scriptFile = new ScriptFile(DocumentUri.FromFileSystemPath(path), emptyStringReader, PowerShellVersion);
|
||||
Assert.Equal("file:///home/BobbieDraper/projects/Rocinate/foo%27s_~%23-%5B%40%5D%20%2B%2C%3B%3D%25.ps1", scriptFile.DocumentUri);
|
||||
|
||||
path = "/home/NaomiNagata/projects/Rocinate/Proto:Mole:cule.ps1";
|
||||
scriptFile = new ScriptFile(DocumentUri.FromFileSystemPath(path), emptyStringReader, PowerShellVersion);
|
||||
Assert.Equal("file:///home/NaomiNagata/projects/Rocinate/Proto%3AMole%3Acule.ps1", scriptFile.DocumentUri);
|
||||
|
||||
path = @"/home/JamesHolden/projects/Rocinate/Proto:Mole\cule.ps1";
|
||||
scriptFile = new ScriptFile(DocumentUri.FromFileSystemPath(path), emptyStringReader, PowerShellVersion);
|
||||
Assert.Equal("file:///home/JamesHolden/projects/Rocinate/Proto%3AMole%5Ccule.ps1", scriptFile.DocumentUri);
|
||||
}
|
||||
}
|
||||
|
||||
[Trait("Category", "ScriptFile")]
|
||||
[Theory]
|
||||
[InlineData(@"C:\Users\me\Documents\test.ps1", false)]
|
||||
[InlineData("/Users/me/Documents/test.ps1", false)]
|
||||
[InlineData("vscode-notebook-cell:/Users/me/Documents/test.ps1#0001", true)]
|
||||
[InlineData("https://microsoft.com", true)]
|
||||
[InlineData("Untitled:Untitled-1", true)]
|
||||
[InlineData(@"'a log statement' > 'c:\Users\me\Documents\test.txt'
|
||||
", false)]
|
||||
public void IsUntitledFileIsCorrect(string path, bool expected) => Assert.Equal(expected, ScriptFile.IsUntitledPath(path));
|
||||
}
|
||||
}
|
||||
202
test/PowerShellEditorServices.Test/Session/WorkspaceTests.cs
Normal file
202
test/PowerShellEditorServices.Test/Session/WorkspaceTests.cs
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.PowerShell.EditorServices.Services;
|
||||
using Microsoft.PowerShell.EditorServices.Services.TextDocument;
|
||||
using Xunit;
|
||||
using Microsoft.PowerShell.EditorServices.Utility;
|
||||
using OmniSharp.Extensions.LanguageServer.Protocol.Models;
|
||||
using OmniSharp.Extensions.LanguageServer.Protocol;
|
||||
|
||||
namespace PowerShellEditorServices.Test.Session
|
||||
{
|
||||
[Trait("Category", "Workspace")]
|
||||
public class WorkspaceTests
|
||||
{
|
||||
private static readonly Lazy<string> s_lazyDriveLetter = new(() => Path.GetFullPath("\\").Substring(0, 1));
|
||||
|
||||
public static string CurrentDriveLetter => RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
|
||||
? s_lazyDriveLetter.Value
|
||||
: string.Empty;
|
||||
|
||||
internal static ScriptFile CreateScriptFile(string path) => ScriptFile.Create(path, "", VersionUtils.PSVersion);
|
||||
|
||||
// Remember that LSP does weird stuff to the drive letter, so we have to convert it to a URI
|
||||
// and back to ensure that drive letter gets lower cased and everything matches up.
|
||||
private static string s_workspacePath =>
|
||||
DocumentUri.FromFileSystemPath(Path.GetFullPath("Fixtures/Workspace")).GetFileSystemPath();
|
||||
|
||||
[Fact]
|
||||
public void CanResolveWorkspaceRelativePath()
|
||||
{
|
||||
string workspacePath = "c:/Test/Workspace/";
|
||||
ScriptFile testPathInside = CreateScriptFile("c:/Test/Workspace/SubFolder/FilePath.ps1");
|
||||
ScriptFile testPathOutside = CreateScriptFile("c:/Test/PeerPath/FilePath.ps1");
|
||||
ScriptFile testPathAnotherDrive = CreateScriptFile("z:/TryAndFindMe/FilePath.ps1");
|
||||
|
||||
WorkspaceService workspace = new(NullLoggerFactory.Instance);
|
||||
|
||||
// Test with zero workspace folders
|
||||
Assert.Equal(
|
||||
testPathOutside.DocumentUri.ToUri().AbsolutePath,
|
||||
workspace.GetRelativePath(testPathOutside));
|
||||
|
||||
string expectedInsidePath = "SubFolder/FilePath.ps1";
|
||||
string expectedOutsidePath = "../PeerPath/FilePath.ps1";
|
||||
|
||||
// Test with a single workspace folder
|
||||
workspace.WorkspaceFolders.Add(new WorkspaceFolder
|
||||
{
|
||||
Uri = DocumentUri.FromFileSystemPath(workspacePath)
|
||||
});
|
||||
|
||||
Assert.Equal(expectedInsidePath, workspace.GetRelativePath(testPathInside));
|
||||
Assert.Equal(expectedOutsidePath, workspace.GetRelativePath(testPathOutside));
|
||||
Assert.Equal(
|
||||
testPathAnotherDrive.DocumentUri.ToUri().AbsolutePath,
|
||||
workspace.GetRelativePath(testPathAnotherDrive));
|
||||
|
||||
// Test with two workspace folders
|
||||
string anotherWorkspacePath = "c:/Test/AnotherWorkspace/";
|
||||
ScriptFile anotherTestPathInside = CreateScriptFile("c:/Test/AnotherWorkspace/DifferentFolder/FilePath.ps1");
|
||||
string anotherExpectedInsidePath = "DifferentFolder/FilePath.ps1";
|
||||
|
||||
workspace.WorkspaceFolders.Add(new WorkspaceFolder
|
||||
{
|
||||
Uri = DocumentUri.FromFileSystemPath(anotherWorkspacePath)
|
||||
});
|
||||
|
||||
Assert.Equal(expectedInsidePath, workspace.GetRelativePath(testPathInside));
|
||||
Assert.Equal(anotherExpectedInsidePath, workspace.GetRelativePath(anotherTestPathInside));
|
||||
}
|
||||
|
||||
internal static WorkspaceService FixturesWorkspace()
|
||||
{
|
||||
return new WorkspaceService(NullLoggerFactory.Instance)
|
||||
{
|
||||
WorkspaceFolders =
|
||||
{
|
||||
new WorkspaceFolder { Uri = DocumentUri.FromFileSystemPath(s_workspacePath) }
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HasDefaultForWorkspacePaths()
|
||||
{
|
||||
WorkspaceService workspace = FixturesWorkspace();
|
||||
string workspacePath = Assert.Single(workspace.WorkspacePaths);
|
||||
Assert.Equal(s_workspacePath, workspacePath);
|
||||
// We shouldn't assume an initial working directory since none was given.
|
||||
Assert.Null(workspace.InitialWorkingDirectory);
|
||||
}
|
||||
|
||||
// These are the default values for the EnumeratePSFiles() method
|
||||
// in Microsoft.PowerShell.EditorServices.Workspace class
|
||||
private static readonly string[] s_defaultExcludeGlobs = Array.Empty<string>();
|
||||
private static readonly string[] s_defaultIncludeGlobs = new[] { "**/*" };
|
||||
private const int s_defaultMaxDepth = 64;
|
||||
private const bool s_defaultIgnoreReparsePoints = false;
|
||||
|
||||
internal static List<string> ExecuteEnumeratePSFiles(
|
||||
WorkspaceService workspace,
|
||||
string[] excludeGlobs,
|
||||
string[] includeGlobs,
|
||||
int maxDepth,
|
||||
bool ignoreReparsePoints)
|
||||
{
|
||||
List<string> fileList = new(workspace.EnumeratePSFiles(
|
||||
excludeGlobs: excludeGlobs,
|
||||
includeGlobs: includeGlobs,
|
||||
maxDepth: maxDepth,
|
||||
ignoreReparsePoints: ignoreReparsePoints
|
||||
));
|
||||
|
||||
// Assume order is not important from EnumeratePSFiles and sort the array so we can use
|
||||
// deterministic asserts
|
||||
fileList.Sort();
|
||||
return fileList;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanRecurseDirectoryTree()
|
||||
{
|
||||
WorkspaceService workspace = FixturesWorkspace();
|
||||
List<string> actual = ExecuteEnumeratePSFiles(
|
||||
workspace: workspace,
|
||||
excludeGlobs: s_defaultExcludeGlobs,
|
||||
includeGlobs: s_defaultIncludeGlobs,
|
||||
maxDepth: s_defaultMaxDepth,
|
||||
ignoreReparsePoints: s_defaultIgnoreReparsePoints
|
||||
);
|
||||
|
||||
List<string> expected = new()
|
||||
{
|
||||
Path.Combine(s_workspacePath, "nested", "donotfind.ps1"),
|
||||
Path.Combine(s_workspacePath, "nested", "nestedmodule.psd1"),
|
||||
Path.Combine(s_workspacePath, "nested", "nestedmodule.psm1"),
|
||||
Path.Combine(s_workspacePath, "rootfile.ps1")
|
||||
};
|
||||
|
||||
// .NET Core doesn't appear to use the same three letter pattern matching rule although the docs
|
||||
// suggest it should be find the '.ps1xml' files because we search for the pattern '*.ps1'
|
||||
// ref https://docs.microsoft.com/en-us/dotnet/api/system.io.directory.getfiles?view=netcore-2.1#System_IO_Directory_GetFiles_System_String_System_String_System_IO_EnumerationOptions_
|
||||
if (RuntimeInformation.FrameworkDescription.StartsWith(".NET Framework"))
|
||||
{
|
||||
expected.Insert(3, Path.Combine(s_workspacePath, "other", "other.ps1xml"));
|
||||
}
|
||||
|
||||
Assert.Equal(expected, actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanRecurseDirectoryTreeWithLimit()
|
||||
{
|
||||
WorkspaceService workspace = FixturesWorkspace();
|
||||
List<string> actual = ExecuteEnumeratePSFiles(
|
||||
workspace: workspace,
|
||||
excludeGlobs: s_defaultExcludeGlobs,
|
||||
includeGlobs: s_defaultIncludeGlobs,
|
||||
maxDepth: 1,
|
||||
ignoreReparsePoints: s_defaultIgnoreReparsePoints
|
||||
);
|
||||
Assert.Equal(new[] { Path.Combine(s_workspacePath, "rootfile.ps1") }, actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanRecurseDirectoryTreeWithGlobs()
|
||||
{
|
||||
WorkspaceService workspace = FixturesWorkspace();
|
||||
List<string> actual = ExecuteEnumeratePSFiles(
|
||||
workspace: workspace,
|
||||
excludeGlobs: new[] { "**/donotfind*" }, // Exclude any files starting with donotfind
|
||||
includeGlobs: new[] { "**/*.ps1", "**/*.psd1" }, // Only include PS1 and PSD1 files
|
||||
maxDepth: s_defaultMaxDepth,
|
||||
ignoreReparsePoints: s_defaultIgnoreReparsePoints
|
||||
);
|
||||
|
||||
Assert.Equal(new[] {
|
||||
Path.Combine(s_workspacePath, "nested", "nestedmodule.psd1"),
|
||||
Path.Combine(s_workspacePath, "rootfile.ps1")
|
||||
}, actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanOpenAndCloseFile()
|
||||
{
|
||||
WorkspaceService workspace = FixturesWorkspace();
|
||||
string filePath = Path.GetFullPath(Path.Combine(s_workspacePath, "rootfile.ps1"));
|
||||
|
||||
ScriptFile file = workspace.GetFile(filePath);
|
||||
Assert.Equal(workspace.GetOpenedFiles(), new[] { file });
|
||||
|
||||
workspace.CloseFile(file);
|
||||
Assert.Equal(workspace.GetOpenedFiles(), Array.Empty<ScriptFile>());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using Microsoft.PowerShell.EditorServices.Utility;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.PowerShell.EditorServices.Test.Utility
|
||||
{
|
||||
public class VersionUtilsTests
|
||||
{
|
||||
[Trait("Category", "VersionUtils")]
|
||||
[Fact]
|
||||
public void IsNetCoreTest() =>
|
||||
#if CoreCLR
|
||||
Assert.True(VersionUtils.IsNetCore);
|
||||
#else
|
||||
Assert.False(VersionUtils.IsNetCore);
|
||||
#endif
|
||||
|
||||
}
|
||||
}
|
||||
6
test/PowerShellEditorServices.Test/xunit.runner.json
Normal file
6
test/PowerShellEditorServices.Test/xunit.runner.json
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"$schema": "https://xunit.net/schema/current/xunit.runner.schema.json",
|
||||
"appDomain": "denied",
|
||||
"parallelizeTestCollections": false,
|
||||
"methodDisplay": "method"
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue