// 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 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 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(); } /// /// 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. /// /// /// [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 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> GetConfirmedBreakpoints(ScriptFile scriptFile) { // TODO: Should we use the APIs in BreakpointService to get these? return psesHost.ExecutePSCommandAsync( 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> executeTask = psesHost.ExecutePSCommandAsync( 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 DebuggerAcceptsScriptArgsTestData => new List() { 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 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 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()); Assert.Empty(breakpoints); } [Fact] public async Task DebuggerStopsOnFunctionBreakpoints() { IReadOnlyList 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 breakpoints = await debugService.SetLineBreakpointsAsync( debugScriptFile.FilePath, new[] { BreakpointDetails.Create(debugScriptFile.FilePath, 5), BreakpointDetails.Create(debugScriptFile.FilePath, 10) }); IReadOnlyList 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()); IReadOnlyList 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 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 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 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> executeTask = psesHost.ExecutePSCommandAsync( 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 historyResult = await psesHost.ExecutePSCommandAsync( 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 historyResult = await psesHost.ExecutePSCommandAsync( 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 historyResult = await psesHost.ExecutePSCommandAsync( 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 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); }); } } }