Skip to content

Add support for Hit Count breakpoints #300

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ public class InitializeResponseBody
/// </summary>
public bool SupportsConditionalBreakpoints { get; set; }

/// <summary>
/// Gets or sets a boolean value that determines whether the debug adapter
/// supports breakpoints that break execution after a specified number of hits.
/// </summary>
public bool SupportsHitConditionalBreakpoints { get; set; }

/// <summary>
/// Gets or sets a boolean value that determines whether the debug adapter
/// supports a (side effect free) evaluate request for data hovers.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ public class SourceBreakpoint
public int? Column { get; set; }

public string Condition { get; set; }

public string HitCondition { get; set; }
}

public class SetBreakpointsResponseBody
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,7 @@ public class FunctionBreakpoint
public string Name { get; set; }

public string Condition { get; set; }

public string HitCondition { get; set; }
}
}
6 changes: 4 additions & 2 deletions src/PowerShellEditorServices.Protocol/Server/DebugAdapter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,8 @@ await requestContext.SendResult(
scriptFile.FilePath,
srcBreakpoint.Line,
srcBreakpoint.Column,
srcBreakpoint.Condition);
srcBreakpoint.Condition,
srcBreakpoint.HitCondition);
}

// If this is a "run without debugging (Ctrl+F5)" session ignore requests to set breakpoints.
Expand Down Expand Up @@ -294,7 +295,8 @@ protected async Task HandleSetFunctionBreakpointsRequest(
FunctionBreakpoint funcBreakpoint = setBreakpointsParams.Breakpoints[i];
breakpointDetails[i] = CommandBreakpointDetails.Create(
funcBreakpoint.Name,
funcBreakpoint.Condition);
funcBreakpoint.Condition,
funcBreakpoint.HitCondition);
}

// If this is a "run without debugging (Ctrl+F5)" session ignore requests to set breakpoints.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,9 @@ private async Task HandleInitializeRequest(
await requestContext.SendResult(
new InitializeResponseBody {
SupportsConfigurationDoneRequest = true,
SupportsConditionalBreakpoints = true,
SupportsFunctionBreakpoints = true,
SupportsConditionalBreakpoints = true,
SupportsHitConditionalBreakpoints = true,
SupportsSetVariable = true
});

Expand Down
7 changes: 5 additions & 2 deletions src/PowerShellEditorServices/Debugging/BreakpointDetails.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,14 @@ private BreakpointDetails()
/// <param name="line"></param>
/// <param name="column"></param>
/// <param name="condition"></param>
/// <param name="hitCondition"></param>
/// <returns></returns>
public static BreakpointDetails Create(
string source,
int line,
int? column = null,
string condition = null)
string condition = null,
string hitCondition = null)
{
Validate.IsNotNull("source", source);

Expand All @@ -57,7 +59,8 @@ public static BreakpointDetails Create(
Source = source,
LineNumber = line,
ColumnNumber = column,
Condition = condition
Condition = condition,
HitCondition = hitCondition
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,10 @@ public abstract class BreakpointDetailsBase
/// Gets the breakpoint condition string.
/// </summary>
public string Condition { get; protected set; }

/// <summary>
/// Gets the breakpoint hit condition string.
/// </summary>
public string HitCondition { get; protected set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,12 @@ private CommandBreakpointDetails()
/// </summary>
/// <param name="name">The name of the command to break on.</param>
/// <param name="condition">Condition string that would be applied to the breakpoint Action parameter.</param>
/// <param name="hitCondition">Hit condition string that would be applied to the breakpoint Action parameter.</param>
/// <returns></returns>
public static CommandBreakpointDetails Create(
string name,
string condition = null)
string condition = null,
string hitCondition = null)
{
Validate.IsNotNull(nameof(name), name);

Expand Down
103 changes: 80 additions & 23 deletions src/PowerShellEditorServices/Debugging/DebugService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ public class DebugService
private VariableContainerDetails scriptScopeVariables;
private StackFrameDetails[] stackFrameDetails;

private static int breakpointHitCounter = 0;

#endregion

#region Constructors
Expand Down Expand Up @@ -102,7 +104,8 @@ public async Task<BreakpointDetails[]> SetLineBreakpoints(
}

// Check if this is a "conditional" line breakpoint.
if (breakpoint.Condition != null)
if (!String.IsNullOrWhiteSpace(breakpoint.Condition) ||
!String.IsNullOrWhiteSpace(breakpoint.HitCondition))
{
ScriptBlock actionScriptBlock =
GetBreakpointActionScriptBlock(breakpoint);
Expand Down Expand Up @@ -157,7 +160,8 @@ public async Task<CommandBreakpointDetails[]> SetCommandBreakpoints(
psCommand.AddParameter("Command", breakpoint.Name);

// Check if this is a "conditional" command breakpoint.
if (breakpoint.Condition != null)
if (!String.IsNullOrWhiteSpace(breakpoint.Condition) ||
!String.IsNullOrWhiteSpace(breakpoint.HitCondition))
{
ScriptBlock actionScriptBlock = GetBreakpointActionScriptBlock(breakpoint);

Expand Down Expand Up @@ -719,32 +723,85 @@ private ScriptBlock GetBreakpointActionScriptBlock(
{
try
{
ScriptBlock actionScriptBlock = ScriptBlock.Create(breakpoint.Condition);
ScriptBlock actionScriptBlock;
int? hitCount = null;

// Check for simple, common errors that ScriptBlock parsing will not catch
// e.g. $i == 3 and $i > 3
string message;
if (!ValidateBreakpointConditionAst(actionScriptBlock.Ast, out message))
// If HitCondition specified, parse and verify it.
if (!(String.IsNullOrWhiteSpace(breakpoint.HitCondition)))
{
breakpoint.Verified = false;
breakpoint.Message = message;
return null;
int parsedHitCount;

if (Int32.TryParse(breakpoint.HitCondition, out parsedHitCount))
{
hitCount = parsedHitCount;
}
else
{
breakpoint.Verified = false;
breakpoint.Message = $"The specified HitCount '{breakpoint.HitCondition}' is not valid. " +
"The HitCount must be an integer number.";
return null;
}
}

// Check for "advanced" condition syntax i.e. if the user has specified
// a "break" or "continue" statement anywhere in their scriptblock,
// pass their scriptblock through to the Action parameter as-is.
Ast breakOrContinueStatementAst =
actionScriptBlock.Ast.Find(
ast => (ast is BreakStatementAst || ast is ContinueStatementAst), true);

// If this isn't advanced syntax then the conditions string should be a simple
// expression that needs to be wrapped in a "if" test that conditionally executes
// a break statement.
if (breakOrContinueStatementAst == null)
// Create an Action scriptblock based on condition and/or hit count passed in.
if (hitCount.HasValue && String.IsNullOrWhiteSpace(breakpoint.Condition))
{
// In the HitCount only case, this is simple as we can just use the HitCount
// property on the breakpoint object which is represented by $_.
string action = $"if ($_.HitCount -eq {hitCount}) {{ break }}";
actionScriptBlock = ScriptBlock.Create(action);
}
else if (!String.IsNullOrWhiteSpace(breakpoint.Condition))
{
// Must be either condition only OR condition and hit count.
actionScriptBlock = ScriptBlock.Create(breakpoint.Condition);

// Check for simple, common errors that ScriptBlock parsing will not catch
// e.g. $i == 3 and $i > 3
string message;
if (!ValidateBreakpointConditionAst(actionScriptBlock.Ast, out message))
{
breakpoint.Verified = false;
breakpoint.Message = message;
return null;
}

// Check for "advanced" condition syntax i.e. if the user has specified
// a "break" or "continue" statement anywhere in their scriptblock,
// pass their scriptblock through to the Action parameter as-is.
Ast breakOrContinueStatementAst =
actionScriptBlock.Ast.Find(
ast => (ast is BreakStatementAst || ast is ContinueStatementAst), true);

// If this isn't advanced syntax then the conditions string should be a simple
// expression that needs to be wrapped in a "if" test that conditionally executes
// a break statement.
if (breakOrContinueStatementAst == null)
{
string wrappedCondition;

if (hitCount.HasValue)
{
string globalHitCountVarName =
$"$global:__psEditorServices_BreakHitCounter_{breakpointHitCounter++}";

wrappedCondition =
$"if ({breakpoint.Condition}) {{ if (++{globalHitCountVarName} -eq {hitCount}) {{ break }} }}";
}
else
{
wrappedCondition = $"if ({breakpoint.Condition}) {{ break }}";
}

actionScriptBlock = ScriptBlock.Create(wrappedCondition);
}
}
else
{
string wrappedCondition = $"if ({breakpoint.Condition}) {{ break }}";
actionScriptBlock = ScriptBlock.Create(wrappedCondition);
// Shouldn't get here unless someone called this with no condition and no hit count.
actionScriptBlock = ScriptBlock.Create("break");
Logger.Write(LogLevel.Warning, "No condition and no hit count specified by caller.");
}

return actionScriptBlock;
Expand Down
73 changes: 73 additions & 0 deletions test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,79 @@ await this.debugService.SetLineBreakpoints(
await executeTask;
}

[Fact]
public async Task DebuggerStopsOnHitConditionBreakpoint()
{
const int hitCount = 5;

BreakpointDetails[] breakpoints =
await this.debugService.SetLineBreakpoints(
this.debugScriptFile,
new[] {
BreakpointDetails.Create("", 6, null, null, $"{hitCount}"),
});

await this.AssertStateChange(PowerShellContextState.Ready);

Task executeTask =
this.powerShellContext.ExecuteScriptAtPath(
this.debugScriptFile.FilePath);

// Wait for conditional breakpoint to hit
await this.AssertDebuggerStopped(this.debugScriptFile.FilePath, 6);

StackFrameDetails[] stackFrames = debugService.GetStackFrames();
VariableDetailsBase[] variables =
debugService.GetVariables(stackFrames[0].LocalVariables.Id);

// Verify the breakpoint only broke at the condition ie. $i -eq breakpointValue1
var i = variables.FirstOrDefault(v => v.Name == "$i");
Assert.NotNull(i);
Assert.False(i.IsExpandable);
Assert.Equal($"{hitCount}", i.ValueString);

// Abort script execution early and wait for completion
this.debugService.Abort();
await executeTask;
}

[Fact]
public async Task DebuggerStopsOnConditionalAndHitConditionBreakpoint()
{
const int hitCount = 5;

BreakpointDetails[] breakpoints =
await this.debugService.SetLineBreakpoints(
this.debugScriptFile,
new[] {
BreakpointDetails.Create("", 6, null, $"$i % 2 -eq 0", $"{hitCount}"),
});

await this.AssertStateChange(PowerShellContextState.Ready);

Task executeTask =
this.powerShellContext.ExecuteScriptAtPath(
this.debugScriptFile.FilePath);

// Wait for conditional breakpoint to hit
await this.AssertDebuggerStopped(this.debugScriptFile.FilePath, 6);

StackFrameDetails[] stackFrames = debugService.GetStackFrames();
VariableDetailsBase[] variables =
debugService.GetVariables(stackFrames[0].LocalVariables.Id);

// Verify the breakpoint only broke at the condition ie. $i -eq breakpointValue1
var i = variables.FirstOrDefault(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);

// Abort script execution early and wait for completion
this.debugService.Abort();
await executeTask;
}

[Fact]
public async Task DebuggerProvidesMessageForInvalidConditionalBreakpoint()
{
Expand Down