powershell-es/test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs
fwastring baa0056244
Some checks are pending
CI Tests / dotnet (push) Waiting to run
CI Tests / dotnet-1 (push) Waiting to run
CI Tests / dotnet-2 (push) Waiting to run
Emacs End-to-End Tests / ert (push) Waiting to run
Vim End-to-End Tests / themis (push) Waiting to run
initial
2026-02-17 13:06:31 +01:00

1178 lines
54 KiB
C#

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Management.Automation;
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.DebugAdapter;
using Microsoft.PowerShell.EditorServices.Services.PowerShell.Console;
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.Utility;
using Xunit;
namespace PowerShellEditorServices.Test.Debugging
{
internal class TestReadLine : IReadLine
{
public List<string> history = new();
public string ReadLine(CancellationToken cancellationToken) => "";
public void AddToHistory(string historyEntry) => history.Add(historyEntry);
}
[Trait("Category", "DebugService")]
public class DebugServiceTests : IAsyncLifetime
{
private PsesInternalHost psesHost;
private BreakpointService breakpointService;
private DebugService debugService;
private readonly BlockingCollection<DebuggerStoppedEventArgs> debuggerStoppedQueue = new();
private WorkspaceService workspace;
private ScriptFile debugScriptFile;
private ScriptFile oddPathScriptFile;
private ScriptFile variableScriptFile;
private readonly TestReadLine testReadLine = new();
public async Task InitializeAsync()
{
psesHost = await PsesHostFactory.Create(NullLoggerFactory.Instance);
// This is required for remote debugging, but we call it here to end up in the same
// state as the usual startup path.
psesHost.DebugContext.EnableDebugMode();
psesHost._readLineProvider.ReadLine = testReadLine;
breakpointService = new BreakpointService(
NullLoggerFactory.Instance,
psesHost,
psesHost,
new DebugStateService());
debugService = new DebugService(
psesHost,
psesHost.DebugContext,
remoteFileManager: null,
breakpointService,
psesHost,
NullLoggerFactory.Instance);
debugService.DebuggerStopped += OnDebuggerStopped;
// Load the test debug files.
workspace = new WorkspaceService(NullLoggerFactory.Instance);
debugScriptFile = GetDebugScript("DebugTest.ps1");
oddPathScriptFile = GetDebugScript("Debug' W&ith $Params [Test].ps1");
variableScriptFile = GetDebugScript("VariableTest.ps1");
}
public async Task DisposeAsync()
{
debugService.Abort();
await Task.Run(psesHost.StopAsync);
debuggerStoppedQueue.Dispose();
}
/// <summary>
/// This event handler lets us test that the debugger stopped or paused
/// as expected. It will deadlock if called in the PSES Pipeline Thread.
/// Hence we use 'Task.Run(...)' when accessing the queue to ensure we
/// stay OFF the pipeline thread.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD110:Observe result of async calls", Justification = "This intentionally fires and forgets on another thread.")]
private void OnDebuggerStopped(object sender, DebuggerStoppedEventArgs e) => Task.Run(() => debuggerStoppedQueue.Add(e));
private ScriptFile GetDebugScript(string fileName) => workspace.GetFile(TestUtilities.GetSharedPath(Path.Combine("Debugging", fileName)));
private Task<VariableDetailsBase[]> GetVariables(string scopeName)
{
VariableScope scope = Array.Find(
debugService.GetVariableScopes(0),
s => s.Name == scopeName);
return debugService.GetVariables(scope.Id, CancellationToken.None);
}
private Task ExecuteScriptFileAsync(string scriptFilePath, params string[] args)
{
return psesHost.ExecutePSCommandAsync(
PSCommandHelpers.BuildDotSourceCommandWithArguments(PSCommandHelpers.EscapeScriptFilePath(scriptFilePath), args),
CancellationToken.None);
}
private Task ExecuteDebugFileAsync() => ExecuteScriptFileAsync(debugScriptFile.FilePath);
private Task ExecuteVariableScriptFileAsync() => ExecuteScriptFileAsync(variableScriptFile.FilePath);
private async Task AssertDebuggerPaused()
{
using CancellationTokenSource cts = new(60000);
DebuggerStoppedEventArgs eventArgs = await Task.Run(() => debuggerStoppedQueue.Take(cts.Token));
Assert.Empty(eventArgs.OriginalEvent.Breakpoints);
}
private async Task AssertDebuggerStopped(
string scriptPath = "",
int lineNumber = -1,
CommandBreakpointDetails commandBreakpointDetails = default)
{
using CancellationTokenSource cts = new(30000);
DebuggerStoppedEventArgs eventArgs = await Task.Run(() => debuggerStoppedQueue.Take(cts.Token));
Assert.True(psesHost.DebugContext.IsStopped);
if (!string.IsNullOrEmpty(scriptPath))
{
// TODO: The drive letter becomes lower cased on Windows for some reason.
Assert.Equal(scriptPath, eventArgs.ScriptPath, ignoreCase: true);
}
else
{
Assert.Equal(string.Empty, scriptPath);
}
if (lineNumber > -1)
{
Assert.Equal(lineNumber, eventArgs.LineNumber);
}
if (commandBreakpointDetails is not null)
{
Assert.Equal(commandBreakpointDetails.Name, eventArgs.OriginalEvent.InvocationInfo.MyCommand.Name);
}
}
private Task<IReadOnlyList<LineBreakpoint>> GetConfirmedBreakpoints(ScriptFile scriptFile)
{
// TODO: Should we use the APIs in BreakpointService to get these?
return psesHost.ExecutePSCommandAsync<LineBreakpoint>(
new PSCommand().AddCommand("Get-PSBreakpoint").AddParameter("Script", scriptFile.FilePath),
CancellationToken.None);
}
[Fact]
// This regression test asserts that `ExecuteScriptWithArgsAsync` works for both script
// files and, in this case, in-line scripts (commands). The bug was that the cwd was
// erroneously prepended when the script argument was a command.
public async Task DebuggerAcceptsInlineScript()
{
await debugService.SetCommandBreakpointsAsync(
new[] { CommandBreakpointDetails.Create("Get-Random") });
Task<IReadOnlyList<int>> executeTask = psesHost.ExecutePSCommandAsync<int>(
new PSCommand().AddScript("Get-Random -SetSeed 42 -Maximum 100"), CancellationToken.None);
await AssertDebuggerStopped("", 1);
debugService.Continue();
Assert.Equal(17, (await executeTask)[0]);
StackFrameDetails[] stackFrames = await debugService.GetStackFramesAsync();
Assert.Equal(StackFrameDetails.NoFileScriptPath, stackFrames[0].ScriptPath);
// NOTE: This assertion will fail if any error occurs. Notably this happens in testing
// when the assembly path changes and the commands definition file can't be found.
VariableDetailsBase[] variables = await GetVariables(VariableContainerDetails.GlobalScopeName);
VariableDetailsBase var = Array.Find(variables, v => v.Name == "$Error");
Assert.NotNull(var);
Assert.True(var.IsExpandable);
Assert.Equal("[ArrayList: 0]", var.ValueString);
}
// See https://www.thomasbogholm.net/2021/06/01/convenient-member-data-sources-with-xunit/
public static IEnumerable<object[]> DebuggerAcceptsScriptArgsTestData => new List<object[]>()
{
new object[] { new object[] { "Foo -Param2 @('Bar','Baz') -Force Extra1" } },
new object[] { new object[] { "Foo", "-Param2", "@('Bar','Baz')", "-Force", "Extra1" } }
};
[Theory]
[MemberData(nameof(DebuggerAcceptsScriptArgsTestData))]
public async Task DebuggerAcceptsScriptArgs(string[] args)
{
IReadOnlyList<BreakpointDetails> breakpoints = await debugService.SetLineBreakpointsAsync(
oddPathScriptFile.FilePath,
new[] { BreakpointDetails.Create(oddPathScriptFile.FilePath, 3) });
Assert.Single(breakpoints);
Assert.Collection(breakpoints, (breakpoint) =>
{
// TODO: The drive letter becomes lower cased on Windows for some reason.
Assert.Equal(oddPathScriptFile.FilePath, breakpoint.Source, ignoreCase: true);
Assert.Equal(3, breakpoint.LineNumber);
Assert.True(breakpoint.Verified);
});
Task _ = ExecuteScriptFileAsync(oddPathScriptFile.FilePath, args);
await AssertDebuggerStopped(oddPathScriptFile.FilePath, 3);
VariableDetailsBase[] variables = await GetVariables(VariableContainerDetails.LocalScopeName);
VariableDetailsBase var = Array.Find(variables, v => v.Name == "$Param1");
Assert.NotNull(var);
Assert.Equal("\"Foo\"", var.ValueString);
Assert.False(var.IsExpandable);
var = Array.Find(variables, v => v.Name == "$Param2");
Assert.NotNull(var);
Assert.True(var.IsExpandable);
VariableDetailsBase[] childVars = await debugService.GetVariables(var.Id, CancellationToken.None);
// 2 variables plus "Raw View"
Assert.Equal(3, childVars.Length);
Assert.Equal("\"Bar\"", childVars[0].ValueString);
Assert.Equal("\"Baz\"", childVars[1].ValueString);
var = Array.Find(variables, v => v.Name == "$Force");
Assert.NotNull(var);
Assert.Equal("True", var.ValueString);
Assert.True(var.IsExpandable);
// NOTE: $args are no longer found in AutoVariables but CommandVariables instead.
StackFrameDetails[] stackFrames = await debugService.GetStackFramesAsync();
variables = await debugService.GetVariables(stackFrames[0].CommandVariables.Id, CancellationToken.None);
var = Array.Find(variables, v => v.Name == "$args");
Assert.NotNull(var);
Assert.True(var.IsExpandable);
childVars = await debugService.GetVariables(var.Id, CancellationToken.None);
Assert.Equal(2, childVars.Length);
Assert.Equal("\"Extra1\"", childVars[0].ValueString);
}
[Fact]
public async Task DebuggerSetsAndClearsFunctionBreakpoints()
{
IReadOnlyList<CommandBreakpointDetails> breakpoints = await debugService.SetCommandBreakpointsAsync(
new[] {
CommandBreakpointDetails.Create("Write-Host"),
CommandBreakpointDetails.Create("Get-Date")
});
Assert.Equal(2, breakpoints.Count);
Assert.Equal("Write-Host", breakpoints[0].Name);
Assert.Equal("Get-Date", breakpoints[1].Name);
breakpoints = await debugService.SetCommandBreakpointsAsync(
new[] { CommandBreakpointDetails.Create("Get-Host") });
Assert.Equal("Get-Host", Assert.Single(breakpoints).Name);
breakpoints = await debugService.SetCommandBreakpointsAsync(
Array.Empty<CommandBreakpointDetails>());
Assert.Empty(breakpoints);
}
[Fact]
public async Task DebuggerStopsOnFunctionBreakpoints()
{
IReadOnlyList<CommandBreakpointDetails> breakpoints = await debugService.SetCommandBreakpointsAsync(
new[] { CommandBreakpointDetails.Create("Write-Host") });
Task _ = ExecuteDebugFileAsync();
await AssertDebuggerStopped(debugScriptFile.FilePath, 6);
VariableDetailsBase[] variables = await GetVariables(VariableContainerDetails.LocalScopeName);
// Verify the function breakpoint broke at Write-Host and $i is 1
VariableDetailsBase i = Array.Find(variables, v => v.Name == "$i");
Assert.NotNull(i);
Assert.False(i.IsExpandable);
Assert.Equal("1", i.ValueString);
// The function breakpoint should fire the next time through the loop.
debugService.Continue();
await AssertDebuggerStopped(debugScriptFile.FilePath, 6);
variables = await GetVariables(VariableContainerDetails.LocalScopeName);
// Verify the function breakpoint broke at Write-Host and $i is 1
i = Array.Find(variables, v => v.Name == "$i");
Assert.NotNull(i);
Assert.False(i.IsExpandable);
Assert.Equal("2", i.ValueString);
}
[Fact]
public async Task DebuggerSetsAndClearsLineBreakpoints()
{
IReadOnlyList<BreakpointDetails> breakpoints =
await debugService.SetLineBreakpointsAsync(
debugScriptFile.FilePath,
new[] {
BreakpointDetails.Create(debugScriptFile.FilePath, 5),
BreakpointDetails.Create(debugScriptFile.FilePath, 10)
});
IReadOnlyList<LineBreakpoint> confirmedBreakpoints = await GetConfirmedBreakpoints(debugScriptFile);
Assert.Equal(2, confirmedBreakpoints.Count);
Assert.Equal(5, breakpoints[0].LineNumber);
Assert.Equal(10, breakpoints[1].LineNumber);
breakpoints = await debugService.SetLineBreakpointsAsync(
debugScriptFile.FilePath,
new[] { BreakpointDetails.Create(debugScriptFile.FilePath, 2) });
confirmedBreakpoints = await GetConfirmedBreakpoints(debugScriptFile);
Assert.Single(confirmedBreakpoints);
Assert.Equal(2, breakpoints[0].LineNumber);
await debugService.SetLineBreakpointsAsync(
debugScriptFile.FilePath,
Array.Empty<BreakpointDetails>());
IReadOnlyList<LineBreakpoint> remainingBreakpoints = await GetConfirmedBreakpoints(debugScriptFile);
Assert.Empty(remainingBreakpoints);
}
[Fact]
public async Task DebuggerStopsOnLineBreakpoints()
{
await debugService.SetLineBreakpointsAsync(
debugScriptFile.FilePath,
new[] {
BreakpointDetails.Create(debugScriptFile.FilePath, 5),
BreakpointDetails.Create(debugScriptFile.FilePath, 7)
});
Task _ = ExecuteDebugFileAsync();
await AssertDebuggerStopped(debugScriptFile.FilePath, 5);
debugService.Continue();
await AssertDebuggerStopped(debugScriptFile.FilePath, 7);
}
[Fact]
public async Task DebuggerStopsOnConditionalBreakpoints()
{
const int breakpointValue1 = 10;
const int breakpointValue2 = 20;
await debugService.SetLineBreakpointsAsync(
debugScriptFile.FilePath,
new[] {
BreakpointDetails.Create(debugScriptFile.FilePath, 7, null, $"$i -eq {breakpointValue1} -or $i -eq {breakpointValue2}"),
});
Task _ = ExecuteDebugFileAsync();
await AssertDebuggerStopped(debugScriptFile.FilePath, 7);
VariableDetailsBase[] variables = await GetVariables(VariableContainerDetails.LocalScopeName);
// Verify the breakpoint only broke at the condition ie. $i -eq breakpointValue1
VariableDetailsBase i = Array.Find(variables, v => v.Name == "$i");
Assert.NotNull(i);
Assert.False(i.IsExpandable);
Assert.Equal($"{breakpointValue1}", i.ValueString);
// The conditional breakpoint should not fire again, until the value of
// i reaches breakpointValue2.
debugService.Continue();
await AssertDebuggerStopped(debugScriptFile.FilePath, 7);
variables = await GetVariables(VariableContainerDetails.LocalScopeName);
// Verify the breakpoint only broke at the condition ie. $i -eq breakpointValue1
i = Array.Find(variables, v => v.Name == "$i");
Assert.NotNull(i);
Assert.False(i.IsExpandable);
Assert.Equal($"{breakpointValue2}", i.ValueString);
}
[Fact]
public async Task DebuggerStopsOnHitConditionBreakpoint()
{
const int hitCount = 5;
await debugService.SetLineBreakpointsAsync(
debugScriptFile.FilePath,
new[] {
BreakpointDetails.Create(debugScriptFile.FilePath, 6, null, null, $"{hitCount}"),
});
Task _ = ExecuteDebugFileAsync();
await AssertDebuggerStopped(debugScriptFile.FilePath, 6);
VariableDetailsBase[] variables = await GetVariables(VariableContainerDetails.LocalScopeName);
// Verify the breakpoint only broke at the condition ie. $i -eq breakpointValue1
VariableDetailsBase i = Array.Find(variables, v => v.Name == "$i");
Assert.NotNull(i);
Assert.False(i.IsExpandable);
Assert.Equal($"{hitCount}", i.ValueString);
}
[Fact]
public async Task DebuggerStopsOnConditionalAndHitConditionBreakpoint()
{
const int hitCount = 5;
await debugService.SetLineBreakpointsAsync(
debugScriptFile.FilePath,
new[] { BreakpointDetails.Create(debugScriptFile.FilePath, 6, null, "$i % 2 -eq 0", $"{hitCount}") });
Task _ = ExecuteDebugFileAsync();
await AssertDebuggerStopped(debugScriptFile.FilePath, 6);
VariableDetailsBase[] variables = await GetVariables(VariableContainerDetails.LocalScopeName);
// Verify the breakpoint only broke at the condition ie. $i -eq breakpointValue1
VariableDetailsBase i = Array.Find(variables, v => v.Name == "$i");
Assert.NotNull(i);
Assert.False(i.IsExpandable);
// Condition is even numbers ($i starting at 1) should end up on 10 with a hit count of 5.
Assert.Equal("10", i.ValueString);
}
[Fact]
public async Task DebuggerProvidesMessageForInvalidConditionalBreakpoint()
{
IReadOnlyList<BreakpointDetails> breakpoints =
await debugService.SetLineBreakpointsAsync(
debugScriptFile.FilePath,
new[] {
// TODO: Add this breakpoint back when it stops moving around?! The ordering
// of these two breakpoints seems to do with which framework executes the
// code. Best guess is that `IEnumerable` is not stably sorted so `ToArray`
// returns different orderings. However, that doesn't explain why this is
// the only affected test.
// BreakpointDetails.Create(debugScriptFile.FilePath, 5),
BreakpointDetails.Create(debugScriptFile.FilePath, 10, column: null, condition: "$i -ez 100")
});
Assert.Single(breakpoints);
// Assert.Equal(5, breakpoints[0].LineNumber);
// Assert.True(breakpoints[0].Verified);
// Assert.Null(breakpoints[0].Message);
Assert.Equal(10, breakpoints[0].LineNumber);
Assert.False(breakpoints[0].Verified);
Assert.NotNull(breakpoints[0].Message);
Assert.Contains("Unexpected token '-ez'", breakpoints[0].Message);
}
[Fact]
public async Task DebuggerFindsParsableButInvalidSimpleBreakpointConditions()
{
IReadOnlyList<BreakpointDetails> breakpoints =
await debugService.SetLineBreakpointsAsync(
debugScriptFile.FilePath,
new[] {
BreakpointDetails.Create(debugScriptFile.FilePath, 5, column: null, condition: "$i == 100"),
BreakpointDetails.Create(debugScriptFile.FilePath, 7, column: null, condition: "$i > 100")
});
Assert.Equal(2, breakpoints.Count);
Assert.Equal(5, breakpoints[0].LineNumber);
Assert.False(breakpoints[0].Verified);
Assert.Contains("Use '-eq' instead of '=='", breakpoints[0].Message);
Assert.Equal(7, breakpoints[1].LineNumber);
Assert.False(breakpoints[1].Verified);
Assert.NotNull(breakpoints[1].Message);
Assert.Contains("Use '-gt' instead of '>'", breakpoints[1].Message);
}
[Fact]
public async Task DebuggerBreaksWhenRequested()
{
IReadOnlyList<LineBreakpoint> confirmedBreakpoints = await GetConfirmedBreakpoints(debugScriptFile);
Assert.Empty(confirmedBreakpoints);
Task _ = ExecuteDebugFileAsync();
debugService.Break();
await AssertDebuggerPaused();
}
[Fact]
public async Task DebuggerRunsCommandsWhileStopped()
{
Task _ = ExecuteDebugFileAsync();
debugService.Break();
await AssertDebuggerPaused();
// Try running a command from outside the pipeline thread
Task<IReadOnlyList<int>> executeTask = psesHost.ExecutePSCommandAsync<int>(
new PSCommand().AddScript("Get-Random -SetSeed 42 -Maximum 100"), CancellationToken.None);
Assert.Equal(17, (await executeTask)[0]);
}
// Regression test asserting that the PSDebugContext variable is available when running the
// "prompt" function. While we're unable to test the REPL loop, this still covers the
// behavior as I verified that it stepped through "ExecuteInDebugger" (which was the
// original problem).
[Fact]
public async Task DebugContextAvailableInPrompt()
{
await debugService.SetCommandBreakpointsAsync(
new[] { CommandBreakpointDetails.Create("Write-Host") });
ScriptFile testScript = GetDebugScript("PSDebugContextTest.ps1");
Task _ = ExecuteScriptFileAsync(testScript.FilePath);
await AssertDebuggerStopped(testScript.FilePath, 11);
VariableDetails prompt = await debugService.EvaluateExpressionAsync("prompt", false, CancellationToken.None);
Assert.Equal("True > ", prompt.ValueString);
}
[Theory]
[InlineData("Command")]
[InlineData("Line")]
public async Task DebuggerBreaksInUntitledScript(string breakpointType)
{
const string contents = "Write-Output $($MyInvocation.Line)";
const string scriptPath = "untitled:Untitled-1";
Assert.True(ScriptFile.IsUntitledPath(scriptPath));
ScriptFile scriptFile = workspace.GetFileBuffer(scriptPath, contents);
Assert.Equal(scriptPath, scriptFile.DocumentUri);
Assert.Equal(contents, scriptFile.Contents);
Assert.True(workspace.TryGetFile(scriptPath, out ScriptFile _));
if (breakpointType == "Command")
{
await debugService.SetCommandBreakpointsAsync(
new[] { CommandBreakpointDetails.Create("Write-Output") });
}
else
{
await debugService.SetLineBreakpointsAsync(
scriptFile.FilePath,
new[] { BreakpointDetails.Create(scriptPath, 1) });
}
ConfigurationDoneHandler configurationDoneHandler = new(
NullLoggerFactory.Instance, null, debugService, null, null, psesHost, workspace, null);
Task _ = configurationDoneHandler.LaunchScriptAsync(scriptPath);
await AssertDebuggerStopped(scriptPath, 1);
VariableDetailsBase[] variables = await GetVariables(VariableContainerDetails.CommandVariablesName);
VariableDetailsBase myInvocation = Array.Find(variables, v => v.Name == "$MyInvocation");
Assert.NotNull(myInvocation);
Assert.True(myInvocation.IsExpandable);
// Here we're asserting that our hacky workaround to support breakpoints in untitled
// scripts is working, namely that we're actually dot-sourcing our first argument, which
// should be a cached script block. See the `LaunchScriptAsync` for more info.
VariableDetailsBase[] myInvocationChildren = await debugService.GetVariables(myInvocation.Id, CancellationToken.None);
VariableDetailsBase myInvocationLine = Array.Find(myInvocationChildren, v => v.Name == "Line");
Assert.Equal("\". $args[0]\"", myInvocationLine.ValueString);
}
[Fact]
public async Task RecordsF5CommandInPowerShellHistory()
{
ConfigurationDoneHandler configurationDoneHandler = new(
NullLoggerFactory.Instance, null, debugService, null, null, psesHost, workspace, null);
await configurationDoneHandler.LaunchScriptAsync(debugScriptFile.FilePath);
IReadOnlyList<string> historyResult = await psesHost.ExecutePSCommandAsync<string>(
new PSCommand().AddScript("(Get-History).CommandLine"),
CancellationToken.None);
// Check the PowerShell history
Assert.Equal(". '" + debugScriptFile.FilePath + "'", Assert.Single(historyResult));
// Check the stubbed PSReadLine history
Assert.Equal(". '" + debugScriptFile.FilePath + "'", Assert.Single(testReadLine.history));
}
[Fact]
public async Task RecordsF8CommandInHistory()
{
const string script = "Write-Output Hello";
EvaluateHandler evaluateHandler = new(psesHost);
EvaluateResponseBody evaluateResponseBody = await evaluateHandler.Handle(
new EvaluateRequestArguments { Expression = script, Context = "repl" },
CancellationToken.None);
// TODO: Right now this response is hard-coded, maybe it should change?
Assert.Equal("", evaluateResponseBody.Result);
IReadOnlyList<string> historyResult = await psesHost.ExecutePSCommandAsync<string>(
new PSCommand().AddScript("(Get-History).CommandLine"),
CancellationToken.None);
// Check the PowerShell history
Assert.Equal(script, Assert.Single(historyResult));
// Check the stubbed PSReadLine history
Assert.Equal(script, Assert.Single(testReadLine.history));
}
[Fact]
public async Task OddFilePathsLaunchCorrectly()
{
ConfigurationDoneHandler configurationDoneHandler = new(
NullLoggerFactory.Instance, null, debugService, null, null, psesHost, workspace, null);
await configurationDoneHandler.LaunchScriptAsync(oddPathScriptFile.FilePath);
IReadOnlyList<string> historyResult = await psesHost.ExecutePSCommandAsync<string>(
new PSCommand().AddScript("(Get-History).CommandLine"),
CancellationToken.None);
// Check the PowerShell history
Assert.Equal(". " + PSCommandHelpers.EscapeScriptFilePath(oddPathScriptFile.FilePath), Assert.Single(historyResult));
}
[Fact]
public async Task DebuggerVariableStringDisplaysCorrectly()
{
await debugService.SetLineBreakpointsAsync(
variableScriptFile.FilePath,
new[] { BreakpointDetails.Create(variableScriptFile.FilePath, 8) });
Task _ = ExecuteVariableScriptFileAsync();
await AssertDebuggerStopped(variableScriptFile.FilePath);
VariableDetailsBase[] variables = await GetVariables(VariableContainerDetails.LocalScopeName);
VariableDetailsBase var = Array.Find(variables, v => v.Name == "$strVar");
Assert.NotNull(var);
Assert.Equal("\"Hello\"", var.ValueString);
Assert.False(var.IsExpandable);
}
[Fact]
public async Task DebuggerGetsVariables()
{
await debugService.SetLineBreakpointsAsync(
variableScriptFile.FilePath,
new[] { BreakpointDetails.Create(variableScriptFile.FilePath, 21) });
Task _ = ExecuteVariableScriptFileAsync();
await AssertDebuggerStopped(variableScriptFile.FilePath);
VariableDetailsBase[] variables = await GetVariables(VariableContainerDetails.LocalScopeName);
// TODO: Add checks for correct value strings as well
VariableDetailsBase strVar = Array.Find(variables, v => v.Name == "$strVar");
Assert.NotNull(strVar);
Assert.False(strVar.IsExpandable);
VariableDetailsBase objVar = Array.Find(variables, v => v.Name == "$assocArrVar");
Assert.NotNull(objVar);
Assert.True(objVar.IsExpandable);
VariableDetailsBase[] objChildren = await debugService.GetVariables(objVar.Id, CancellationToken.None);
// Two variables plus "Raw View"
Assert.Equal(3, objChildren.Length);
VariableDetailsBase arrVar = Array.Find(variables, v => v.Name == "$arrVar");
Assert.NotNull(arrVar);
Assert.True(arrVar.IsExpandable);
VariableDetailsBase[] arrChildren = await debugService.GetVariables(arrVar.Id, CancellationToken.None);
Assert.Equal(5, arrChildren.Length);
VariableDetailsBase classVar = Array.Find(variables, v => v.Name == "$classVar");
Assert.NotNull(classVar);
Assert.True(classVar.IsExpandable);
VariableDetailsBase[] classChildren = await debugService.GetVariables(classVar.Id, CancellationToken.None);
Assert.Equal(2, classChildren.Length);
VariableDetailsBase trueVar = Array.Find(variables, v => v.Name == "$trueVar");
Assert.NotNull(trueVar);
Assert.Equal("boolean", trueVar.Type);
Assert.Equal("$true", trueVar.ValueString);
VariableDetailsBase falseVar = Array.Find(variables, v => v.Name == "$falseVar");
Assert.NotNull(falseVar);
Assert.Equal("boolean", falseVar.Type);
Assert.Equal("$false", falseVar.ValueString);
}
[Fact]
public async Task DebuggerSetsVariablesNoConversion()
{
await debugService.SetLineBreakpointsAsync(
variableScriptFile.FilePath,
new[] { BreakpointDetails.Create(variableScriptFile.FilePath, 14) });
Task _ = ExecuteVariableScriptFileAsync();
await AssertDebuggerStopped(variableScriptFile.FilePath);
VariableScope[] scopes = debugService.GetVariableScopes(0);
VariableDetailsBase[] variables = await GetVariables(VariableContainerDetails.LocalScopeName);
// Test set of a local string variable (not strongly typed)
const string newStrValue = "\"Goodbye\"";
VariableScope localScope = Array.Find(scopes, s => s.Name == VariableContainerDetails.LocalScopeName);
string setStrValue = await debugService.SetVariableAsync(localScope.Id, "$strVar", newStrValue);
Assert.Equal(newStrValue, setStrValue);
// Test set of script scope int variable (not strongly typed)
VariableScope scriptScope = Array.Find(scopes, s => s.Name == VariableContainerDetails.ScriptScopeName);
const string newIntValue = "49";
const string newIntExpr = "7 * 7";
string setIntValue = await debugService.SetVariableAsync(scriptScope.Id, "$scriptInt", newIntExpr);
Assert.Equal(newIntValue, setIntValue);
// Test set of global scope int variable (not strongly typed)
VariableScope globalScope = Array.Find(scopes, s => s.Name == VariableContainerDetails.GlobalScopeName);
const string newGlobalIntValue = "4242";
string setGlobalIntValue = await debugService.SetVariableAsync(globalScope.Id, "$MaximumHistoryCount", newGlobalIntValue);
Assert.Equal(newGlobalIntValue, setGlobalIntValue);
// The above just tests that the debug service returns the correct new value string.
// Let's step the debugger and make sure the values got set to the new values.
debugService.StepOver();
await AssertDebuggerStopped(variableScriptFile.FilePath);
// Test set of a local string variable (not strongly typed)
variables = await GetVariables(VariableContainerDetails.LocalScopeName);
VariableDetailsBase strVar = Array.Find(variables, v => v.Name == "$strVar");
Assert.Equal(newStrValue, strVar.ValueString);
// Test set of script scope int variable (not strongly typed)
variables = await GetVariables(VariableContainerDetails.ScriptScopeName);
VariableDetailsBase intVar = Array.Find(variables, v => v.Name == "$scriptInt");
Assert.Equal(newIntValue, intVar.ValueString);
// Test set of global scope int variable (not strongly typed)
variables = await GetVariables(VariableContainerDetails.GlobalScopeName);
VariableDetailsBase intGlobalVar = Array.Find(variables, v => v.Name == "$MaximumHistoryCount");
Assert.Equal(newGlobalIntValue, intGlobalVar.ValueString);
}
[Fact]
public async Task DebuggerSetsVariablesWithConversion()
{
await debugService.SetLineBreakpointsAsync(
variableScriptFile.FilePath,
new[] { BreakpointDetails.Create(variableScriptFile.FilePath, 14) });
// Execute the script and wait for the breakpoint to be hit
Task _ = ExecuteVariableScriptFileAsync();
await AssertDebuggerStopped(variableScriptFile.FilePath);
VariableScope[] scopes = debugService.GetVariableScopes(0);
VariableDetailsBase[] variables = await GetVariables(VariableContainerDetails.LocalScopeName);
// Test set of a local string variable (not strongly typed but force conversion)
const string newStrValue = "\"False\"";
const string newStrExpr = "$false";
VariableScope localScope = Array.Find(scopes, s => s.Name == VariableContainerDetails.LocalScopeName);
string setStrValue = await debugService.SetVariableAsync(localScope.Id, "$strVar2", newStrExpr);
Assert.Equal(newStrValue, setStrValue);
// Test set of script scope bool variable (strongly typed)
VariableScope scriptScope = Array.Find(scopes, s => s.Name == VariableContainerDetails.ScriptScopeName);
const string newBoolValue = "$true";
const string newBoolExpr = "1";
string setBoolValue = await debugService.SetVariableAsync(scriptScope.Id, "$scriptBool", newBoolExpr);
Assert.Equal(newBoolValue, setBoolValue);
// Test set of global scope ActionPreference variable (strongly typed)
VariableScope globalScope = Array.Find(scopes, s => s.Name == VariableContainerDetails.GlobalScopeName);
const string newGlobalValue = "Continue";
const string newGlobalExpr = "'Continue'";
string setGlobalValue = await debugService.SetVariableAsync(globalScope.Id, "$VerbosePreference", newGlobalExpr);
Assert.Equal(newGlobalValue, setGlobalValue);
// The above just tests that the debug service returns the correct new value string.
// Let's step the debugger and make sure the values got set to the new values.
debugService.StepOver();
await AssertDebuggerStopped(variableScriptFile.FilePath);
// Test set of a local string variable (not strongly typed but force conversion)
variables = await GetVariables(VariableContainerDetails.LocalScopeName);
VariableDetailsBase strVar = Array.Find(variables, v => v.Name == "$strVar2");
Assert.Equal(newStrValue, strVar.ValueString);
// Test set of script scope bool variable (strongly typed)
variables = await GetVariables(VariableContainerDetails.ScriptScopeName);
VariableDetailsBase boolVar = Array.Find(variables, v => v.Name == "$scriptBool");
Assert.Equal(newBoolValue, boolVar.ValueString);
// Test set of global scope ActionPreference variable (strongly typed)
variables = await GetVariables(VariableContainerDetails.GlobalScopeName);
VariableDetailsBase globalVar = Array.Find(variables, v => v.Name == "$VerbosePreference");
Assert.Equal(newGlobalValue, globalVar.ValueString);
}
[Fact]
public async Task DebuggerVariableEnumDisplaysCorrectly()
{
await debugService.SetLineBreakpointsAsync(
variableScriptFile.FilePath,
new[] { BreakpointDetails.Create(variableScriptFile.FilePath, 15) });
// Execute the script and wait for the breakpoint to be hit
Task _ = ExecuteVariableScriptFileAsync();
await AssertDebuggerStopped(variableScriptFile.FilePath);
StackFrameDetails[] stackFrames = await debugService.GetStackFramesAsync();
VariableDetailsBase[] variables = await debugService.GetVariables(stackFrames[0].AutoVariables.Id, CancellationToken.None);
VariableDetailsBase var = Array.Find(variables, v => v.Name == "$enumVar");
Assert.NotNull(var);
Assert.Equal("Continue", var.ValueString);
Assert.False(var.IsExpandable);
}
[Fact]
public async Task DebuggerVariableHashtableDisplaysCorrectly()
{
await debugService.SetLineBreakpointsAsync(
variableScriptFile.FilePath,
new[] { BreakpointDetails.Create(variableScriptFile.FilePath, 11) });
// Execute the script and wait for the breakpoint to be hit
Task _ = ExecuteVariableScriptFileAsync();
await AssertDebuggerStopped(variableScriptFile.FilePath);
StackFrameDetails[] stackFrames = await debugService.GetStackFramesAsync();
VariableDetailsBase[] variables = await debugService.GetVariables(stackFrames[0].AutoVariables.Id, CancellationToken.None);
VariableDetailsBase var = Array.Find(variables, v => v.Name == "$assocArrVar");
Assert.NotNull(var);
Assert.Equal("[Hashtable: 2]", var.ValueString);
Assert.True(var.IsExpandable);
VariableDetailsBase[] childVars = await debugService.GetVariables(var.Id, CancellationToken.None);
// 2 variables plus "Raw View"
Assert.Equal(3, childVars.Length);
// Hashtables are unordered hence the Linq examination, examination by index is unreliable
VariableDetailsBase firstChild = Array.Find(childVars, v => v.Name == "[firstChild]");
Assert.NotNull(firstChild);
Assert.Equal("\"Child\"", firstChild.ValueString);
VariableDetailsBase secondChild = Array.Find(childVars, v => v.Name == "[secondChild]");
Assert.NotNull(secondChild);
Assert.Equal("42", secondChild.ValueString);
}
[Fact]
public async Task DebuggerVariableNullStringDisplaysCorrectly()
{
await debugService.SetLineBreakpointsAsync(
variableScriptFile.FilePath,
new[] { BreakpointDetails.Create(variableScriptFile.FilePath, 16) });
// Execute the script and wait for the breakpoint to be hit
Task _ = ExecuteVariableScriptFileAsync();
await AssertDebuggerStopped(variableScriptFile.FilePath);
StackFrameDetails[] stackFrames = await debugService.GetStackFramesAsync();
VariableDetailsBase[] variables = await debugService.GetVariables(stackFrames[0].AutoVariables.Id, CancellationToken.None);
VariableDetailsBase nullStringVar = Array.Find(variables, v => v.Name == "$nullString");
Assert.NotNull(nullStringVar);
Assert.Equal("[NullString]", nullStringVar.ValueString);
Assert.True(nullStringVar.IsExpandable);
}
[Fact]
public async Task DebuggerVariablePSObjectDisplaysCorrectly()
{
await debugService.SetLineBreakpointsAsync(
variableScriptFile.FilePath,
new[] { BreakpointDetails.Create(variableScriptFile.FilePath, 17) });
// Execute the script and wait for the breakpoint to be hit
Task _ = ExecuteVariableScriptFileAsync();
await AssertDebuggerStopped(variableScriptFile.FilePath);
StackFrameDetails[] stackFrames = await debugService.GetStackFramesAsync();
VariableDetailsBase[] variables = await debugService.GetVariables(stackFrames[0].AutoVariables.Id, CancellationToken.None);
VariableDetailsBase psObjVar = Array.Find(variables, v => v.Name == "$psObjVar");
Assert.NotNull(psObjVar);
Assert.True("@{Age=75; Name=John}".Equals(psObjVar.ValueString) || "@{Name=John; Age=75}".Equals(psObjVar.ValueString));
Assert.True(psObjVar.IsExpandable);
VariableDetailsBase[] childVars = await debugService.GetVariables(psObjVar.Id, CancellationToken.None);
IDictionary<string, string> dictionary = childVars.ToDictionary(v => v.Name, v => v.ValueString);
Assert.Equal(2, dictionary.Count);
Assert.Contains("Age", dictionary.Keys);
Assert.Contains("Name", dictionary.Keys);
Assert.Equal("75", dictionary["Age"]);
Assert.Equal("\"John\"", dictionary["Name"]);
}
[Fact]
public async Task DebuggerEnumerableShowsRawView()
{
CommandBreakpointDetails breakpoint = CommandBreakpointDetails.Create("__BreakDebuggerEnumerableShowsRawView");
await debugService.SetCommandBreakpointsAsync(new[] { breakpoint });
// Execute the script and wait for the breakpoint to be hit
Task _ = ExecuteVariableScriptFileAsync();
await AssertDebuggerStopped(commandBreakpointDetails: breakpoint);
VariableDetailsBase simpleArrayVar = Array.Find(
await GetVariables(VariableContainerDetails.ScriptScopeName),
v => v.Name == "$simpleArray");
Assert.NotNull(simpleArrayVar);
VariableDetailsBase rawDetailsView = Array.Find(
simpleArrayVar.GetChildren(NullLogger.Instance),
v => v.Name == "Raw View");
Assert.NotNull(rawDetailsView);
Assert.Empty(rawDetailsView.Type);
Assert.Empty(rawDetailsView.ValueString);
VariableDetailsBase[] rawViewChildren = rawDetailsView.GetChildren(NullLogger.Instance);
Assert.Collection(rawViewChildren,
(i) =>
{
Assert.Equal("Length", i.Name);
Assert.Equal("4", i.ValueString);
},
(i) =>
{
Assert.Equal("LongLength", i.Name);
Assert.Equal("4", i.ValueString);
},
(i) =>
{
Assert.Equal("Rank", i.Name);
Assert.Equal("1", i.ValueString);
},
(i) =>
{
Assert.Equal("SyncRoot", i.Name);
Assert.True(i.IsExpandable);
},
(i) =>
{
Assert.Equal("IsReadOnly", i.Name);
Assert.Equal("$false", i.ValueString);
}, (i) =>
{
Assert.Equal("IsFixedSize", i.Name);
Assert.Equal("$true", i.ValueString);
}, (i) =>
{
Assert.Equal("IsSynchronized", i.Name);
Assert.Equal("$false", i.ValueString);
});
}
[Fact]
public async Task DebuggerDictionaryShowsRawView()
{
CommandBreakpointDetails breakpoint = CommandBreakpointDetails.Create("__BreakDebuggerDictionaryShowsRawView");
await debugService.SetCommandBreakpointsAsync(new[] { breakpoint });
// Execute the script and wait for the breakpoint to be hit
Task _ = ExecuteVariableScriptFileAsync();
await AssertDebuggerStopped(commandBreakpointDetails: breakpoint);
VariableDetailsBase simpleDictionaryVar = Array.Find(
await GetVariables(VariableContainerDetails.ScriptScopeName),
v => v.Name == "$simpleDictionary");
Assert.NotNull(simpleDictionaryVar);
VariableDetailsBase rawDetailsView = Array.Find(
simpleDictionaryVar.GetChildren(NullLogger.Instance),
v => v.Name == "Raw View");
Assert.NotNull(rawDetailsView);
Assert.Empty(rawDetailsView.Type);
Assert.Empty(rawDetailsView.ValueString);
VariableDetailsBase[] rawDetailsChildren = rawDetailsView.GetChildren(NullLogger.Instance);
Assert.Collection(rawDetailsChildren,
(i) =>
{
Assert.Equal("IsReadOnly", i.Name);
Assert.Equal("$false", i.ValueString);
},
(i) =>
{
Assert.Equal("IsFixedSize", i.Name);
Assert.Equal("$false", i.ValueString);
},
(i) =>
{
Assert.Equal("IsSynchronized", i.Name);
Assert.Equal("$false", i.ValueString);
},
(i) =>
{
Assert.Equal("Keys", i.Name);
Assert.Equal("[KeyCollection: 4]", i.ValueString);
},
(i) =>
{
Assert.Equal("Values", i.Name);
Assert.Equal("[ValueCollection: 4]", i.ValueString);
},
(i) =>
{
Assert.Equal("SyncRoot", i.Name);
#if CoreCLR
Assert.Equal("[Hashtable: 4]", i.ValueString);
#else
Assert.Equal("[Object]", i.ValueString);
#endif
},
(i) =>
{
Assert.Equal("Count", i.Name);
Assert.Equal("4", i.ValueString);
});
}
[Fact]
public async Task DebuggerDerivedDictionaryPropertyInRawView()
{
CommandBreakpointDetails breakpoint = CommandBreakpointDetails.Create("__BreakDebuggerDerivedDictionaryPropertyInRawView");
await debugService.SetCommandBreakpointsAsync(new[] { breakpoint });
// Execute the script and wait for the breakpoint to be hit
Task _ = ExecuteVariableScriptFileAsync();
await AssertDebuggerStopped(commandBreakpointDetails: breakpoint);
VariableDetailsBase sortedDictionaryVar = Array.Find(
await GetVariables(VariableContainerDetails.ScriptScopeName),
v => v.Name == "$sortedDictionary");
Assert.NotNull(sortedDictionaryVar);
VariableDetailsBase[] simpleDictionaryChildren = sortedDictionaryVar.GetChildren(NullLogger.Instance);
// 4 items + Raw View
Assert.Equal(5, simpleDictionaryChildren.Length);
VariableDetailsBase rawDetailsView = Array.Find(
simpleDictionaryChildren,
v => v.Name == "Raw View");
Assert.NotNull(rawDetailsView);
Assert.Empty(rawDetailsView.Type);
Assert.Empty(rawDetailsView.ValueString);
VariableDetailsBase[] rawViewChildren = rawDetailsView.GetChildren(NullLogger.Instance);
Assert.Collection(rawViewChildren,
(i) =>
{
Assert.Equal("Count", i.Name);
Assert.Equal("4", i.ValueString);
},
(i) =>
{
Assert.Equal("Comparer", i.Name);
Assert.Equal("[GenericComparer`1]", i.ValueString);
},
(i) =>
{
Assert.Equal("Keys", i.Name);
Assert.Equal("[KeyCollection: 4]", i.ValueString);
},
(i) =>
{
Assert.Equal("Values", i.Name);
Assert.Equal("[ValueCollection: 4]", i.ValueString);
}
);
}
[Fact]
public async Task DebuggerVariablePSCustomObjectDisplaysCorrectly()
{
await debugService.SetLineBreakpointsAsync(
variableScriptFile.FilePath,
new[] { BreakpointDetails.Create(variableScriptFile.FilePath, 18) });
// Execute the script and wait for the breakpoint to be hit
Task _ = ExecuteVariableScriptFileAsync();
await AssertDebuggerStopped(variableScriptFile.FilePath);
StackFrameDetails[] stackFrames = await debugService.GetStackFramesAsync();
VariableDetailsBase[] variables = await debugService.GetVariables(stackFrames[0].AutoVariables.Id, CancellationToken.None);
VariableDetailsBase var = Array.Find(variables, v => v.Name == "$psCustomObjVar");
Assert.NotNull(var);
Assert.Equal("@{Name=Paul; Age=73}", var.ValueString);
Assert.True(var.IsExpandable);
VariableDetailsBase[] childVars = await debugService.GetVariables(var.Id, CancellationToken.None);
Assert.Equal(2, childVars.Length);
Assert.Equal("Name", childVars[0].Name);
Assert.Equal("\"Paul\"", childVars[0].ValueString);
Assert.Equal("Age", childVars[1].Name);
Assert.Equal("73", childVars[1].ValueString);
}
// Verifies fix for issue #86, $proc = Get-Process foo displays just the ETS property set
// and not all process properties.
[Fact]
public async Task DebuggerVariableProcessObjectDisplaysCorrectly()
{
await debugService.SetLineBreakpointsAsync(
variableScriptFile.FilePath,
new[] { BreakpointDetails.Create(variableScriptFile.FilePath, 19) });
// Execute the script and wait for the breakpoint to be hit
Task _ = ExecuteVariableScriptFileAsync();
await AssertDebuggerStopped(variableScriptFile.FilePath);
StackFrameDetails[] stackFrames = await debugService.GetStackFramesAsync();
VariableDetailsBase[] variables = await debugService.GetVariables(stackFrames[0].AutoVariables.Id, CancellationToken.None);
VariableDetailsBase var = Array.Find(variables, v => v.Name == "$procVar");
Assert.NotNull(var);
Assert.StartsWith("System.Diagnostics.Process", var.ValueString);
Assert.True(var.IsExpandable);
VariableDetailsBase[] childVars = await debugService.GetVariables(var.Id, CancellationToken.None);
Assert.Contains(childVars, i => i.Name is "Name");
Assert.Contains(childVars, i => i.Name is "Handles");
#if CoreCLR
Assert.Contains(childVars, i => i.Name is "CommandLine");
Assert.Contains(childVars, i => i.Name is "ExitCode");
Assert.Contains(childVars, i => i.Name is "HasExited" && i.ValueString is "$false");
#endif
Assert.Contains(childVars, i => i.Name is "Id");
}
[Fact]
public async Task DebuggerVariableFileObjectDisplaysCorrectly()
{
await debugService.SetCommandBreakpointsAsync(
new[] { CommandBreakpointDetails.Create("Write-Host") });
ScriptFile testScript = GetDebugScript("GetChildItemTest.ps1");
Task _ = ExecuteScriptFileAsync(testScript.FilePath);
await AssertDebuggerStopped(testScript.FilePath, 2);
VariableDetailsBase[] variables = await GetVariables(VariableContainerDetails.LocalScopeName);
VariableDetailsBase var = Array.Find(variables, v => v.Name == "$file");
VariableDetailsBase[] childVars = await debugService.GetVariables(var.Id, CancellationToken.None);
Assert.Contains(childVars, i => i.Name is "PSPath");
Assert.Contains(childVars, i => i.Name is "PSProvider" && i.ValueString is @"Microsoft.PowerShell.Core\FileSystem");
Assert.Contains(childVars, i => i.Name is "Exists" && i.ValueString is "$true");
Assert.Contains(childVars, i => i.Name is "LastAccessTime");
}
// Verifies Issue #1686
[Fact]
public async Task DebuggerToStringShouldMarshallToPipeline()
{
CommandBreakpointDetails breakpoint = CommandBreakpointDetails.Create("__BreakDebuggerToStringShouldMarshallToPipeline");
await debugService.SetCommandBreakpointsAsync(new[] { breakpoint });
// Execute the script and wait for the breakpoint to be hit
Task _ = ExecuteVariableScriptFileAsync();
await AssertDebuggerStopped(commandBreakpointDetails: breakpoint);
VariableDetailsBase[] vars = await GetVariables(VariableContainerDetails.ScriptScopeName);
VariableDetailsBase customToStrings = Array.Find(vars, i => i.Name is "$CustomToStrings");
Assert.True(customToStrings.IsExpandable);
Assert.Equal("[System.Object[]]", customToStrings.Type);
VariableDetailsBase[] childVars = await debugService.GetVariables(customToStrings.Id, CancellationToken.None);
// Check everything but the last variable (which is "Raw View")
Assert.Equal(1001, childVars.Length); // 1000 custom variables plus "Raw View"
Assert.All(childVars.Take(childVars.Length - 1), i =>
{
Assert.Equal("HELLO", i.ValueString);
Assert.Equal("[CustomToString]", i.Type);
});
}
}
}