Skip to content

Commit 4123b7e

Browse files
committed
Add support for handling just HitCount and Condition + HitCount.
Add unit tests for this functionality.
1 parent f56a683 commit 4123b7e

File tree

2 files changed

+153
-23
lines changed

2 files changed

+153
-23
lines changed

src/PowerShellEditorServices/Debugging/DebugService.cs

Lines changed: 80 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ public class DebugService
3535
private VariableContainerDetails scriptScopeVariables;
3636
private StackFrameDetails[] stackFrameDetails;
3737

38+
private static int breakpointHitCounter = 0;
39+
3840
#endregion
3941

4042
#region Constructors
@@ -102,7 +104,8 @@ public async Task<BreakpointDetails[]> SetLineBreakpoints(
102104
}
103105

104106
// Check if this is a "conditional" line breakpoint.
105-
if (breakpoint.Condition != null)
107+
if (!String.IsNullOrWhiteSpace(breakpoint.Condition) ||
108+
!String.IsNullOrWhiteSpace(breakpoint.HitCondition))
106109
{
107110
ScriptBlock actionScriptBlock =
108111
GetBreakpointActionScriptBlock(breakpoint);
@@ -157,7 +160,8 @@ public async Task<CommandBreakpointDetails[]> SetCommandBreakpoints(
157160
psCommand.AddParameter("Command", breakpoint.Name);
158161

159162
// Check if this is a "conditional" command breakpoint.
160-
if (breakpoint.Condition != null)
163+
if (!String.IsNullOrWhiteSpace(breakpoint.Condition) ||
164+
!String.IsNullOrWhiteSpace(breakpoint.HitCondition))
161165
{
162166
ScriptBlock actionScriptBlock = GetBreakpointActionScriptBlock(breakpoint);
163167

@@ -719,32 +723,85 @@ private ScriptBlock GetBreakpointActionScriptBlock(
719723
{
720724
try
721725
{
722-
ScriptBlock actionScriptBlock = ScriptBlock.Create(breakpoint.Condition);
726+
ScriptBlock actionScriptBlock;
727+
int? hitCount = null;
723728

724-
// Check for simple, common errors that ScriptBlock parsing will not catch
725-
// e.g. $i == 3 and $i > 3
726-
string message;
727-
if (!ValidateBreakpointConditionAst(actionScriptBlock.Ast, out message))
729+
// If HitCondition specified, parse and verify it.
730+
if (!(String.IsNullOrWhiteSpace(breakpoint.HitCondition)))
728731
{
729-
breakpoint.Verified = false;
730-
breakpoint.Message = message;
731-
return null;
732+
int parsedHitCount;
733+
734+
if (Int32.TryParse(breakpoint.HitCondition, out parsedHitCount))
735+
{
736+
hitCount = parsedHitCount;
737+
}
738+
else
739+
{
740+
breakpoint.Verified = false;
741+
breakpoint.Message = $"The specified HitCount '{breakpoint.HitCondition}' is not valid. " +
742+
"The HitCount must be an integer number.";
743+
return null;
744+
}
732745
}
733746

734-
// Check for "advanced" condition syntax i.e. if the user has specified
735-
// a "break" or "continue" statement anywhere in their scriptblock,
736-
// pass their scriptblock through to the Action parameter as-is.
737-
Ast breakOrContinueStatementAst =
738-
actionScriptBlock.Ast.Find(
739-
ast => (ast is BreakStatementAst || ast is ContinueStatementAst), true);
740-
741-
// If this isn't advanced syntax then the conditions string should be a simple
742-
// expression that needs to be wrapped in a "if" test that conditionally executes
743-
// a break statement.
744-
if (breakOrContinueStatementAst == null)
747+
// Create an Action scriptblock based on condition and/or hit count passed in.
748+
if (hitCount.HasValue && String.IsNullOrWhiteSpace(breakpoint.Condition))
749+
{
750+
// In the HitCount only case, this is simple as we can just use the HitCount
751+
// property on the breakpoint object which is represented by $_.
752+
string action = $"if ($_.HitCount -eq {hitCount}) {{ break }}";
753+
actionScriptBlock = ScriptBlock.Create(action);
754+
}
755+
else if (!String.IsNullOrWhiteSpace(breakpoint.Condition))
756+
{
757+
// Must be either condition only OR condition and hit count.
758+
actionScriptBlock = ScriptBlock.Create(breakpoint.Condition);
759+
760+
// Check for simple, common errors that ScriptBlock parsing will not catch
761+
// e.g. $i == 3 and $i > 3
762+
string message;
763+
if (!ValidateBreakpointConditionAst(actionScriptBlock.Ast, out message))
764+
{
765+
breakpoint.Verified = false;
766+
breakpoint.Message = message;
767+
return null;
768+
}
769+
770+
// Check for "advanced" condition syntax i.e. if the user has specified
771+
// a "break" or "continue" statement anywhere in their scriptblock,
772+
// pass their scriptblock through to the Action parameter as-is.
773+
Ast breakOrContinueStatementAst =
774+
actionScriptBlock.Ast.Find(
775+
ast => (ast is BreakStatementAst || ast is ContinueStatementAst), true);
776+
777+
// If this isn't advanced syntax then the conditions string should be a simple
778+
// expression that needs to be wrapped in a "if" test that conditionally executes
779+
// a break statement.
780+
if (breakOrContinueStatementAst == null)
781+
{
782+
string wrappedCondition;
783+
784+
if (hitCount.HasValue)
785+
{
786+
string globalHitCountVarName =
787+
$"$global:__psEditorServices_BreakHitCounter_{breakpointHitCounter++}";
788+
789+
wrappedCondition =
790+
$"if ({breakpoint.Condition}) {{ if (++{globalHitCountVarName} -eq {hitCount}) {{ break }} }}";
791+
}
792+
else
793+
{
794+
wrappedCondition = $"if ({breakpoint.Condition}) {{ break }}";
795+
}
796+
797+
actionScriptBlock = ScriptBlock.Create(wrappedCondition);
798+
}
799+
}
800+
else
745801
{
746-
string wrappedCondition = $"if ({breakpoint.Condition}) {{ break }}";
747-
actionScriptBlock = ScriptBlock.Create(wrappedCondition);
802+
// Shouldn't get here unless someone called this with no condition and no hit count.
803+
actionScriptBlock = ScriptBlock.Create("break");
804+
Logger.Write(LogLevel.Warning, "No condition and no hit count specified by caller.");
748805
}
749806

750807
return actionScriptBlock;

test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,79 @@ await this.debugService.SetLineBreakpoints(
336336
await executeTask;
337337
}
338338

339+
[Fact]
340+
public async Task DebuggerStopsOnHitConditionBreakpoint()
341+
{
342+
const int hitCount = 5;
343+
344+
BreakpointDetails[] breakpoints =
345+
await this.debugService.SetLineBreakpoints(
346+
this.debugScriptFile,
347+
new[] {
348+
BreakpointDetails.Create("", 6, null, null, $"{hitCount}"),
349+
});
350+
351+
await this.AssertStateChange(PowerShellContextState.Ready);
352+
353+
Task executeTask =
354+
this.powerShellContext.ExecuteScriptAtPath(
355+
this.debugScriptFile.FilePath);
356+
357+
// Wait for conditional breakpoint to hit
358+
await this.AssertDebuggerStopped(this.debugScriptFile.FilePath, 6);
359+
360+
StackFrameDetails[] stackFrames = debugService.GetStackFrames();
361+
VariableDetailsBase[] variables =
362+
debugService.GetVariables(stackFrames[0].LocalVariables.Id);
363+
364+
// Verify the breakpoint only broke at the condition ie. $i -eq breakpointValue1
365+
var i = variables.FirstOrDefault(v => v.Name == "$i");
366+
Assert.NotNull(i);
367+
Assert.False(i.IsExpandable);
368+
Assert.Equal($"{hitCount}", i.ValueString);
369+
370+
// Abort script execution early and wait for completion
371+
this.debugService.Abort();
372+
await executeTask;
373+
}
374+
375+
[Fact]
376+
public async Task DebuggerStopsOnConditionalAndHitConditionBreakpoint()
377+
{
378+
const int hitCount = 5;
379+
380+
BreakpointDetails[] breakpoints =
381+
await this.debugService.SetLineBreakpoints(
382+
this.debugScriptFile,
383+
new[] {
384+
BreakpointDetails.Create("", 6, null, $"$i % 2 -eq 0", $"{hitCount}"),
385+
});
386+
387+
await this.AssertStateChange(PowerShellContextState.Ready);
388+
389+
Task executeTask =
390+
this.powerShellContext.ExecuteScriptAtPath(
391+
this.debugScriptFile.FilePath);
392+
393+
// Wait for conditional breakpoint to hit
394+
await this.AssertDebuggerStopped(this.debugScriptFile.FilePath, 6);
395+
396+
StackFrameDetails[] stackFrames = debugService.GetStackFrames();
397+
VariableDetailsBase[] variables =
398+
debugService.GetVariables(stackFrames[0].LocalVariables.Id);
399+
400+
// Verify the breakpoint only broke at the condition ie. $i -eq breakpointValue1
401+
var i = variables.FirstOrDefault(v => v.Name == "$i");
402+
Assert.NotNull(i);
403+
Assert.False(i.IsExpandable);
404+
// Condition is even numbers ($i starting at 1) should end up on 10 with a hit count of 5.
405+
Assert.Equal("10", i.ValueString);
406+
407+
// Abort script execution early and wait for completion
408+
this.debugService.Abort();
409+
await executeTask;
410+
}
411+
339412
[Fact]
340413
public async Task DebuggerProvidesMessageForInvalidConditionalBreakpoint()
341414
{

0 commit comments

Comments
 (0)