diff --git a/src/MICore/CommandFactories/MICommandFactory.cs b/src/MICore/CommandFactories/MICommandFactory.cs
index 7b8454857..07de24092 100644
--- a/src/MICore/CommandFactories/MICommandFactory.cs
+++ b/src/MICore/CommandFactories/MICommandFactory.cs
@@ -567,6 +567,15 @@ public virtual async Task BreakCondition(string bkptno, string expr)
await _debugger.CmdAsync(command, ResultClass.done);
}
+ ///
+ /// Sends -break-after to set an ignore count on a breakpoint.
+ ///
+ public virtual async Task BreakAfter(string bkptno, uint count)
+ {
+ string command = string.Format(CultureInfo.InvariantCulture, "-break-after {0} {1}", bkptno, count);
+ return await _debugger.CmdAsync(command, ResultClass.done);
+ }
+
public virtual IEnumerable GetSupportedExceptionCategories()
{
return new Guid[0];
diff --git a/src/MIDebugEngine/AD7.Impl/AD7BoundBreakpoint.cs b/src/MIDebugEngine/AD7.Impl/AD7BoundBreakpoint.cs
index 39609f20e..c3b0a549e 100644
--- a/src/MIDebugEngine/AD7.Impl/AD7BoundBreakpoint.cs
+++ b/src/MIDebugEngine/AD7.Impl/AD7BoundBreakpoint.cs
@@ -19,6 +19,8 @@ internal class AD7BoundBreakpoint : IDebugBoundBreakpoint2
private BoundBreakpoint _bp;
private bool _deleted;
+ private enum_BP_PASSCOUNT_STYLE _passCountStyle;
+ private uint _passCountValue;
internal bool Enabled
{
@@ -37,6 +39,7 @@ internal bool Enabled
internal string Number { get { return _bp.Number; } }
internal AD7PendingBreakpoint PendingBreakpoint { get { return _pendingBreakpoint; } }
internal bool IsDataBreakpoint { get { return PendingBreakpoint.IsDataBreakpoint; } }
+ internal bool HasPassCount { get { return _passCountStyle != enum_BP_PASSCOUNT_STYLE.BP_PASSCOUNT_NONE; } }
public AD7BoundBreakpoint(AD7Engine engine, AD7PendingBreakpoint pendingBreakpoint, AD7BreakpointResolution breakpointResolution, BoundBreakpoint bp)
{
@@ -143,8 +146,7 @@ int IDebugBoundBreakpoint2.GetState(enum_BP_STATE[] pState)
return Constants.S_OK;
}
- // The sample engine does not support hit counts on breakpoints. A real-world debugger will want to keep track
- // of how many times a particular bound breakpoint has been hit and return it here.
+ // Returns the number of times this breakpoint has been hit.
int IDebugBoundBreakpoint2.GetHitCount(out uint pdwHitCount)
{
pdwHitCount = _bp.HitCount;
@@ -156,29 +158,59 @@ int IDebugBoundBreakpoint2.SetCondition(BP_CONDITION bpCondition)
return ((IDebugPendingBreakpoint2)_pendingBreakpoint).SetCondition(bpCondition); // setting on the pending break will set the condition
}
- // The sample engine does not support hit counts on breakpoints. A real-world debugger will want to keep track
- // of how many times a particular bound breakpoint has been hit. The debugger calls SetHitCount when the user
- // resets a breakpoint's hit count.
+ // Called by the debugger when the user resets a breakpoint's hit count.
int IDebugBoundBreakpoint2.SetHitCount(uint dwHitCount)
{
- throw new NotImplementedException();
+ _bp.SetHitCount(dwHitCount);
+ return Constants.S_OK;
+ }
+
+ ///
+ /// Syncs the hit count from GDB's "times" field in =breakpoint-modified events.
+ ///
+ internal void SetHitCount(uint hitCount)
+ {
+ _bp.SetHitCount(hitCount);
}
- // The sample engine does not support pass counts on breakpoints.
// This is used to specify the breakpoint hit count condition.
int IDebugBoundBreakpoint2.SetPassCount(BP_PASSCOUNT bpPassCount)
{
- if (bpPassCount.stylePassCount != enum_BP_PASSCOUNT_STYLE.BP_PASSCOUNT_NONE)
- {
- Delete();
- _engine.Callback.OnBreakpointUnbound(this, enum_BP_UNBOUND_REASON.BPUR_BREAKPOINT_ERROR);
- return Constants.E_FAIL;
- }
+ _passCountStyle = bpPassCount.stylePassCount;
+ _passCountValue = bpPassCount.dwPassCount;
return Constants.S_OK;
}
#endregion
+ internal void IncrementHitCount()
+ {
+ _bp.IncrementHitCount();
+ }
+
+ ///
+ /// Evaluates whether the debugger should break at this breakpoint based on the
+ /// current hit count and the configured pass count condition.
+ /// Must be called after IncrementHitCount.
+ ///
+ internal bool ShouldBreak()
+ {
+ uint hitCount = _bp.HitCount;
+ switch (_passCountStyle)
+ {
+ case enum_BP_PASSCOUNT_STYLE.BP_PASSCOUNT_NONE:
+ return true;
+ case enum_BP_PASSCOUNT_STYLE.BP_PASSCOUNT_EQUAL:
+ return hitCount == _passCountValue;
+ case enum_BP_PASSCOUNT_STYLE.BP_PASSCOUNT_EQUAL_OR_GREATER:
+ return hitCount >= _passCountValue;
+ case enum_BP_PASSCOUNT_STYLE.BP_PASSCOUNT_MOD:
+ return _passCountValue != 0 && (hitCount % _passCountValue) == 0;
+ default:
+ return true;
+ }
+ }
+
internal void UpdateAddr(ulong addr)
{
_bp.Addr = addr;
diff --git a/src/MIDebugEngine/AD7.Impl/AD7PendingBreakpoint.cs b/src/MIDebugEngine/AD7.Impl/AD7PendingBreakpoint.cs
index f929c73f2..1c0f2c1df 100644
--- a/src/MIDebugEngine/AD7.Impl/AD7PendingBreakpoint.cs
+++ b/src/MIDebugEngine/AD7.Impl/AD7PendingBreakpoint.cs
@@ -115,11 +115,6 @@ private bool CanBind()
return false;
}
}
- if ((_bpRequestInfo.dwFields & enum_BPREQI_FIELDS.BPREQI_PASSCOUNT) != 0)
- {
- this.SetError(new AD7ErrorBreakpoint(this, ResourceStrings.UnsupportedPassCountBreakpoint, enum_BP_ERROR_TYPE.BPET_GENERAL_ERROR));
- return false;
- }
return true;
}
@@ -393,6 +388,40 @@ internal async Task BindAsync()
}
}
}
+
+ // Set ignore count via -break-after if a pass count is configured
+ if (_bp != null && (_bpRequestInfo.dwFields & enum_BPREQI_FIELDS.BPREQI_PASSCOUNT) != 0
+ && _bpRequestInfo.bpPassCount.stylePassCount != enum_BP_PASSCOUNT_STYLE.BP_PASSCOUNT_NONE)
+ {
+ uint ignoreCount = ComputeIgnoreCount(_bpRequestInfo.bpPassCount.stylePassCount, _bpRequestInfo.bpPassCount.dwPassCount, 0);
+ await _bp.SetBreakAfterAsync(ignoreCount, _engine.DebuggedProcess);
+ }
+ }
+ }
+
+ ///
+ /// Computes the ignore count for -break-after, accounting for hits already
+ /// counted from a prior breakpoint ().
+ ///
+ private static uint ComputeIgnoreCount(enum_BP_PASSCOUNT_STYLE style, uint passCount, uint currentHits)
+ {
+ if (passCount == 0)
+ {
+ return 0;
+ }
+
+ switch (style)
+ {
+ case enum_BP_PASSCOUNT_STYLE.BP_PASSCOUNT_EQUAL:
+ case enum_BP_PASSCOUNT_STYLE.BP_PASSCOUNT_EQUAL_OR_GREATER:
+ // Need to stop at hit N. Already counted currentHits, so skip (N - 1 - currentHits) more.
+ return passCount - 1 > currentHits ? passCount - 1 - currentHits : 0;
+ case enum_BP_PASSCOUNT_STYLE.BP_PASSCOUNT_MOD:
+ // Next stop is at the next multiple of passCount after currentHits.
+ uint remainder = currentHits % passCount;
+ return remainder == 0 ? passCount - 1 : passCount - 1 - remainder;
+ default:
+ return 0;
}
}
@@ -406,6 +435,11 @@ internal AD7BoundBreakpoint AddBoundBreakpoint(BoundBreakpoint bp)
}
AD7BreakpointResolution breakpointResolution = new AD7BreakpointResolution(_engine, IsDataBreakpoint, bp.Addr, bp.FunctionName, bp.DocumentContext(_engine));
AD7BoundBreakpoint boundBreakpoint = new AD7BoundBreakpoint(_engine, this, breakpointResolution, bp);
+ // Apply pass count (hit count condition) from the original request to the bound breakpoint
+ if ((_bpRequestInfo.dwFields & enum_BPREQI_FIELDS.BPREQI_PASSCOUNT) != 0)
+ {
+ ((IDebugBoundBreakpoint2)boundBreakpoint).SetPassCount(_bpRequestInfo.bpPassCount);
+ }
//check can bind one last time. If the pending breakpoint was deleted before now, we need to clean up gdb side
if (CanBind())
{
@@ -645,13 +679,47 @@ int IDebugPendingBreakpoint2.SetCondition(BP_CONDITION bpCondition)
return Constants.S_OK;
}
- // The sample engine does not support pass counts on breakpoints.
int IDebugPendingBreakpoint2.SetPassCount(BP_PASSCOUNT bpPassCount)
{
- if (bpPassCount.stylePassCount != enum_BP_PASSCOUNT_STYLE.BP_PASSCOUNT_NONE)
+ _bpRequestInfo.bpPassCount = bpPassCount;
+ _bpRequestInfo.dwFields |= enum_BPREQI_FIELDS.BPREQI_PASSCOUNT;
+
+ PendingBreakpoint bp = null;
+ lock (_boundBreakpoints)
{
- this.SetError(new AD7ErrorBreakpoint(this, ResourceStrings.UnsupportedPassCountBreakpoint, enum_BP_ERROR_TYPE.BPET_GENERAL_ERROR), true);
- return Constants.E_FAIL;
+ foreach (AD7BoundBreakpoint boundBp in _boundBreakpoints)
+ {
+ ((IDebugBoundBreakpoint2)boundBp).SetPassCount(bpPassCount);
+ }
+ if (_bp != null)
+ {
+ bp = _bp;
+ }
+ }
+
+ // Re-send -break-after to GDB with the updated ignore count, accounting
+ // for the current hit count so the breakpoint fires at the right time.
+ if (bp != null && bpPassCount.stylePassCount != enum_BP_PASSCOUNT_STYLE.BP_PASSCOUNT_NONE)
+ {
+ uint currentHits = 0;
+ lock (_boundBreakpoints)
+ {
+ foreach (AD7BoundBreakpoint boundBp in _boundBreakpoints)
+ {
+ uint hc;
+ if (((IDebugBoundBreakpoint2)boundBp).GetHitCount(out hc) == Constants.S_OK && hc > currentHits)
+ {
+ currentHits = hc;
+ }
+ }
+ }
+ uint ignoreCount = ComputeIgnoreCount(bpPassCount.stylePassCount, bpPassCount.dwPassCount, currentHits);
+ _engine.DebuggedProcess.WorkerThread.RunOperation(() =>
+ {
+ _engine.DebuggedProcess.AddInternalBreakAction(
+ () => bp.SetBreakAfterAsync(ignoreCount, _engine.DebuggedProcess)
+ );
+ });
}
return Constants.S_OK;
}
diff --git a/src/MIDebugEngine/Engine.Impl/BreakpointManager.cs b/src/MIDebugEngine/Engine.Impl/BreakpointManager.cs
index 39090e1f0..b4f92b908 100644
--- a/src/MIDebugEngine/Engine.Impl/BreakpointManager.cs
+++ b/src/MIDebugEngine/Engine.Impl/BreakpointManager.cs
@@ -71,6 +71,20 @@ public async Task BreakpointModified(object sender, EventArgs args)
return;
}
+ // Sync GDB's hit count ("times") for pass count breakpoints.
+ // e.g. =breakpoint-modified,bkpt={number="1",...,times="5",ignore="2",...}
+ string timesStr = bkpt.TryFindString("times");
+ if (!string.IsNullOrEmpty(timesStr) && uint.TryParse(timesStr, out uint times))
+ {
+ foreach (AD7BoundBreakpoint boundBp in pending.EnumBoundBreakpoints())
+ {
+ if (boundBp.HasPassCount)
+ {
+ boundBp.SetHitCount(times);
+ }
+ }
+ }
+
string warning = bkpt.TryFindString("warning");
if (!string.IsNullOrEmpty(warning))
{
@@ -212,7 +226,15 @@ public AD7BoundBreakpoint[] FindHitBreakpoints(string bkptno, ulong addr, /*OPTI
continue;
}
- hitBoundBreakpoints.Add(currBoundBp);
+ // Pass count breakpoints get their hit count from =breakpoint-modified.
+ if (!currBoundBp.HasPassCount)
+ {
+ currBoundBp.IncrementHitCount();
+ }
+ if (currBoundBp.ShouldBreak())
+ {
+ hitBoundBreakpoints.Add(currBoundBp);
+ }
}
fContinue = (hitBoundBreakpoints.Count == 0 && hitBps.Length != 0);
diff --git a/src/MIDebugEngine/Engine.Impl/Breakpoints.cs b/src/MIDebugEngine/Engine.Impl/Breakpoints.cs
index 1f22ae702..9ec557a1f 100644
--- a/src/MIDebugEngine/Engine.Impl/Breakpoints.cs
+++ b/src/MIDebugEngine/Engine.Impl/Breakpoints.cs
@@ -353,6 +353,18 @@ internal async Task SetConditionAsync(string expr, DebuggedProcess process)
await process.MICommandFactory.BreakCondition(Number, expr);
}
}
+
+ ///
+ /// Sends -break-after to set an ignore count on this breakpoint.
+ ///
+ internal async Task SetBreakAfterAsync(uint count, DebuggedProcess process)
+ {
+ if (process.ProcessState != MICore.ProcessState.Exited)
+ {
+ return await process.MICommandFactory.BreakAfter(Number, count);
+ }
+ return null;
+ }
}
internal class BoundBreakpoint
@@ -398,6 +410,7 @@ internal BoundBreakpoint(PendingBreakpoint parent, ulong addr, /*optional*/ Tupl
internal BoundBreakpoint(PendingBreakpoint parent, ulong addr, uint size, string bkptno)
{
Addr = addr;
+ HitCount = 0;
Enabled = true;
this.Number = bkptno;
_parent = parent;
@@ -434,5 +447,15 @@ internal uint Line
_textPosition = new MITextPosition(_textPosition.FileName, value);
}
}
+
+ internal void IncrementHitCount()
+ {
+ HitCount++;
+ }
+
+ internal void SetHitCount(uint count)
+ {
+ HitCount = count;
+ }
}
}
diff --git a/src/OpenDebugAD7/AD7DebugSession.cs b/src/OpenDebugAD7/AD7DebugSession.cs
index bd22d1d05..19b03af8d 100644
--- a/src/OpenDebugAD7/AD7DebugSession.cs
+++ b/src/OpenDebugAD7/AD7DebugSession.cs
@@ -1095,7 +1095,8 @@ protected override void HandleInitializeRequestAsync(IRequestResponder errorMessages = new List();
if (!string.IsNullOrEmpty(bp.LogMessage))
{
// Make sure tracepoint is valid.
- verified = pBPRequest.SetLogMessage(bp.LogMessage);
+ if (!pBPRequest.SetLogMessage(bp.LogMessage))
+ {
+ errorMessages.Add(AD7Resources.Error_UnableToParseLogMessage);
+ }
+ }
+
+ if (!pBPRequest.IsHitConditionValid)
+ {
+ errorMessages.Add(AD7Resources.Error_UnableToParseHitCondition);
}
+ bool verified = errorMessages.Count == 0;
+ string errorMessage = verified ? null : string.Join(" ", errorMessages);
+
if (verified)
{
eb.CheckHR(m_engine.CreatePendingBreakpoint(pBPRequest, out pendingBp));
@@ -2510,7 +2556,7 @@ protected override void HandleSetBreakpointsRequestAsync(IRequestResponder m_Tracepoint;
#endregion
+
+ #region Hit Conditions
+
+ ///
+ /// Attempts to parse the hit condition string into a pass count style and value.
+ /// Returns true if HitCondition is null/empty (no condition) or if it is a valid hit condition.
+ ///
+ ///
+ /// Updates the hit condition string so that subsequent
+ /// calls return the new pass count, and future comparisons see the updated value.
+ ///
+ internal void UpdateHitCondition(string hitCondition)
+ {
+ HitCondition = hitCondition;
+ }
+
+ internal bool TryParseHitCondition(out enum_BP_PASSCOUNT_STYLE style, out uint passCount)
+ {
+ style = enum_BP_PASSCOUNT_STYLE.BP_PASSCOUNT_NONE;
+ passCount = 0;
+
+ if (string.IsNullOrWhiteSpace(HitCondition))
+ {
+ return true;
+ }
+
+ string hc = HitCondition.Trim();
+ string numberPart = hc;
+
+ if (hc.StartsWith(">="))
+ {
+ style = enum_BP_PASSCOUNT_STYLE.BP_PASSCOUNT_EQUAL_OR_GREATER;
+ numberPart = hc.Substring(2).Trim();
+ }
+ else if (hc.StartsWith("%"))
+ {
+ style = enum_BP_PASSCOUNT_STYLE.BP_PASSCOUNT_MOD;
+ numberPart = hc.Substring(1).Trim();
+ }
+ else
+ {
+ style = enum_BP_PASSCOUNT_STYLE.BP_PASSCOUNT_EQUAL;
+ }
+
+ return uint.TryParse(numberPart, out passCount);
+ }
+
+ ///
+ /// Validates that the hit condition string can be parsed into a pass count.
+ /// Returns true if HitCondition is null/empty (no condition) or if it is a valid hit condition.
+ ///
+ internal bool IsHitConditionValid => TryParseHitCondition(out _, out _);
+
+ #endregion
}
}
diff --git a/src/OpenDebugAD7/AD7Resources.Designer.cs b/src/OpenDebugAD7/AD7Resources.Designer.cs
index abb7d62bf..0af693149 100644
--- a/src/OpenDebugAD7/AD7Resources.Designer.cs
+++ b/src/OpenDebugAD7/AD7Resources.Designer.cs
@@ -478,6 +478,15 @@ internal static string Error_UnableToParseLogMessage {
}
}
+ ///
+ /// Looks up a localized string similar to Unable to parse 'hitCondition'. Expected a number, optionally prefixed with >= or %..
+ ///
+ internal static string Error_UnableToParseHitCondition {
+ get {
+ return ResourceManager.GetString("Error_UnableToParseHitCondition", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Error setting breakpoint. {0}.
///
diff --git a/src/OpenDebugAD7/AD7Resources.resx b/src/OpenDebugAD7/AD7Resources.resx
index e50d73343..b3dd45908 100644
--- a/src/OpenDebugAD7/AD7Resources.resx
+++ b/src/OpenDebugAD7/AD7Resources.resx
@@ -198,6 +198,9 @@
Unable to parse 'logMessage'.
+
+ Unable to parse 'hitCondition'. Expected a number, optionally prefixed with >= or %.
+
This operation is not supported when debugging dump files.
diff --git a/test/CppTests/Tests/BreakpointTests.cs b/test/CppTests/Tests/BreakpointTests.cs
index 4ea4279b9..c9625e716 100644
--- a/test/CppTests/Tests/BreakpointTests.cs
+++ b/test/CppTests/Tests/BreakpointTests.cs
@@ -516,6 +516,866 @@ public void BreakpointSettingsVerification(ITestSettings settings)
Assert.True(runner.InitializeResponse.body.supportsFunctionBreakpoints.HasValue
&& runner.InitializeResponse.body.supportsFunctionBreakpoints.Value == true, "Function breakpoints should be supported");
+ Assert.True(runner.InitializeResponse.body.supportsHitConditionalBreakpoints.HasValue
+ && runner.InitializeResponse.body.supportsHitConditionalBreakpoints.Value == true, "Hit conditional breakpoints should be supported");
+
+ this.Comment("Run to completion");
+ runner.Expects.ExitedEvent()
+ .TerminatedEvent()
+ .AfterConfigurationDone();
+
+ runner.DisconnectAndVerify();
+ }
+ }
+
+ [Theory]
+ [DependsOnTest(nameof(CompileKitchenSinkForBreakpointTests))]
+ [RequiresTestSettings]
+ public void HitConditionBreakpointEqual(ITestSettings settings)
+ {
+ this.TestPurpose("Tests that a breakpoint with a hit count condition (equal) only breaks on the Nth hit");
+ this.WriteSettings(settings);
+
+ IDebuggee debuggee = SinkHelper.Open(this, settings.CompilerSettings, DebuggeeMonikers.KitchenSink.Breakpoint);
+
+ using (IDebuggerRunner runner = CreateDebugAdapterRunner(settings))
+ {
+ this.Comment("Configure launch");
+ runner.Launch(settings.DebuggerSettings, debuggee, "-fCalling");
+
+ this.Comment("Set a breakpoint with hit condition = 5 inside a loop that iterates 10 times");
+ SourceBreakpoints callingBreakpoints = new SourceBreakpoints(debuggee, SinkHelper.Calling);
+ callingBreakpoints.Add(17, hitCondition: "5");
+ runner.SetBreakpoints(callingBreakpoints);
+
+ this.Comment("Run to breakpoint - should only stop on 5th hit");
+ runner.Expects.HitBreakpointEvent(SinkHelper.Calling, 17)
+ .AfterConfigurationDone();
+
+ this.Comment("Verify that the loop variable equals 4 (0-indexed, 5th iteration)");
+ using (IThreadInspector inspector = runner.GetThreadInspector())
+ {
+ IFrameInspector mainFrame = inspector.Stack.First();
+ mainFrame.AssertVariables("i", "4");
+ }
+
+ this.Comment("Run to completion - hit count 5 already reached, should not stop again");
+ runner.Expects.ExitedEvent()
+ .TerminatedEvent()
+ .AfterContinue();
+
+ runner.DisconnectAndVerify();
+ }
+ }
+
+ [Theory]
+ [DependsOnTest(nameof(CompileKitchenSinkForBreakpointTests))]
+ [RequiresTestSettings]
+ public void HitConditionBreakpointGreaterOrEqual(ITestSettings settings)
+ {
+ this.TestPurpose("Tests that a breakpoint with a hit count condition (>=) breaks on the Nth hit and every subsequent hit");
+ this.WriteSettings(settings);
+
+ IDebuggee debuggee = SinkHelper.Open(this, settings.CompilerSettings, DebuggeeMonikers.KitchenSink.Breakpoint);
+
+ using (IDebuggerRunner runner = CreateDebugAdapterRunner(settings))
+ {
+ this.Comment("Configure launch");
+ runner.Launch(settings.DebuggerSettings, debuggee, "-fCalling");
+
+ this.Comment("Set a breakpoint with hit condition >= 8 inside a loop that iterates 10 times");
+ SourceBreakpoints callingBreakpoints = new SourceBreakpoints(debuggee, SinkHelper.Calling);
+ callingBreakpoints.Add(17, hitCondition: ">=8");
+ runner.SetBreakpoints(callingBreakpoints);
+
+ this.Comment("Run to breakpoint - should stop on 8th hit");
+ runner.Expects.HitBreakpointEvent(SinkHelper.Calling, 17)
+ .AfterConfigurationDone();
+
+ this.Comment("Verify that the loop variable equals 7 (0-indexed, 8th iteration)");
+ using (IThreadInspector inspector = runner.GetThreadInspector())
+ {
+ IFrameInspector mainFrame = inspector.Stack.First();
+ mainFrame.AssertVariables("i", "7");
+ }
+
+ this.Comment("Continue - should stop on 9th hit");
+ runner.Expects.HitBreakpointEvent(SinkHelper.Calling, 17)
+ .AfterContinue();
+
+ this.Comment("Verify that the loop variable equals 8 (0-indexed, 9th iteration)");
+ using (IThreadInspector inspector = runner.GetThreadInspector())
+ {
+ IFrameInspector mainFrame = inspector.Stack.First();
+ mainFrame.AssertVariables("i", "8");
+ }
+
+ this.Comment("Continue - should stop on 10th hit");
+ runner.Expects.HitBreakpointEvent(SinkHelper.Calling, 17)
+ .AfterContinue();
+
+ this.Comment("Verify that the loop variable equals 9 (0-indexed, 10th iteration)");
+ using (IThreadInspector inspector = runner.GetThreadInspector())
+ {
+ IFrameInspector mainFrame = inspector.Stack.First();
+ mainFrame.AssertVariables("i", "9");
+ }
+
+ this.Comment("Run to completion");
+ runner.Expects.ExitedEvent()
+ .TerminatedEvent()
+ .AfterContinue();
+
+ runner.DisconnectAndVerify();
+ }
+ }
+
+ [Theory]
+ [DependsOnTest(nameof(CompileKitchenSinkForBreakpointTests))]
+ [RequiresTestSettings]
+ public void HitConditionBreakpointModulo(ITestSettings settings)
+ {
+ this.TestPurpose("Tests that a breakpoint with a hit count condition (modulo) breaks on every Nth hit");
+ this.WriteSettings(settings);
+
+ IDebuggee debuggee = SinkHelper.Open(this, settings.CompilerSettings, DebuggeeMonikers.KitchenSink.Breakpoint);
+
+ using (IDebuggerRunner runner = CreateDebugAdapterRunner(settings))
+ {
+ this.Comment("Configure launch");
+ runner.Launch(settings.DebuggerSettings, debuggee, "-fCalling");
+
+ this.Comment("Set a breakpoint with hit condition %3 inside a loop that iterates 10 times");
+ SourceBreakpoints callingBreakpoints = new SourceBreakpoints(debuggee, SinkHelper.Calling);
+ callingBreakpoints.Add(17, hitCondition: "%3");
+ runner.SetBreakpoints(callingBreakpoints);
+
+ this.Comment("Run to breakpoint - should stop on 3rd hit");
+ runner.Expects.HitBreakpointEvent(SinkHelper.Calling, 17)
+ .AfterConfigurationDone();
+
+ this.Comment("Verify that the loop variable equals 2 (0-indexed, 3rd iteration)");
+ using (IThreadInspector inspector = runner.GetThreadInspector())
+ {
+ IFrameInspector mainFrame = inspector.Stack.First();
+ mainFrame.AssertVariables("i", "2");
+ }
+
+ this.Comment("Continue - should stop on 6th hit");
+ runner.Expects.HitBreakpointEvent(SinkHelper.Calling, 17)
+ .AfterContinue();
+
+ this.Comment("Verify that the loop variable equals 5 (0-indexed, 6th iteration)");
+ using (IThreadInspector inspector = runner.GetThreadInspector())
+ {
+ IFrameInspector mainFrame = inspector.Stack.First();
+ mainFrame.AssertVariables("i", "5");
+ }
+
+ this.Comment("Continue - should stop on 9th hit");
+ runner.Expects.HitBreakpointEvent(SinkHelper.Calling, 17)
+ .AfterContinue();
+
+ this.Comment("Verify that the loop variable equals 8 (0-indexed, 9th iteration)");
+ using (IThreadInspector inspector = runner.GetThreadInspector())
+ {
+ IFrameInspector mainFrame = inspector.Stack.First();
+ mainFrame.AssertVariables("i", "8");
+ }
+
+ this.Comment("Run to completion - no more multiples of 3 within 10 iterations");
+ runner.Expects.ExitedEvent()
+ .TerminatedEvent()
+ .AfterContinue();
+
+ runner.DisconnectAndVerify();
+ }
+ }
+
+ [Theory]
+ [DependsOnTest(nameof(CompileKitchenSinkForBreakpointTests))]
+ [RequiresTestSettings]
+ public void HitConditionBreakpointEqualFirst(ITestSettings settings)
+ {
+ this.TestPurpose("Tests that hitCondition '1' breaks on the very first hit");
+ this.WriteSettings(settings);
+
+ IDebuggee debuggee = SinkHelper.Open(this, settings.CompilerSettings, DebuggeeMonikers.KitchenSink.Breakpoint);
+
+ using (IDebuggerRunner runner = CreateDebugAdapterRunner(settings))
+ {
+ this.Comment("Configure launch");
+ runner.Launch(settings.DebuggerSettings, debuggee, "-fCalling");
+
+ this.Comment("Set a breakpoint with hit condition = 1 (first hit)");
+ SourceBreakpoints callingBreakpoints = new SourceBreakpoints(debuggee, SinkHelper.Calling);
+ callingBreakpoints.Add(17, hitCondition: "1");
+ runner.SetBreakpoints(callingBreakpoints);
+
+ this.Comment("Run to breakpoint - should stop on the very first hit");
+ runner.Expects.HitBreakpointEvent(SinkHelper.Calling, 17)
+ .AfterConfigurationDone();
+
+ this.Comment("Verify i == 0 (first iteration)");
+ using (IThreadInspector inspector = runner.GetThreadInspector())
+ {
+ IFrameInspector mainFrame = inspector.Stack.First();
+ mainFrame.AssertVariables("i", "0");
+ }
+
+ this.Comment("Run to completion - equal condition already satisfied, should not stop again");
+ runner.Expects.ExitedEvent()
+ .TerminatedEvent()
+ .AfterContinue();
+
+ runner.DisconnectAndVerify();
+ }
+ }
+
+ [Theory]
+ [DependsOnTest(nameof(CompileKitchenSinkForBreakpointTests))]
+ [RequiresTestSettings]
+ public void HitConditionBreakpointEqualLast(ITestSettings settings)
+ {
+ this.TestPurpose("Tests that hitCondition equal to the loop bound stops on the last iteration");
+ this.WriteSettings(settings);
+
+ IDebuggee debuggee = SinkHelper.Open(this, settings.CompilerSettings, DebuggeeMonikers.KitchenSink.Breakpoint);
+
+ using (IDebuggerRunner runner = CreateDebugAdapterRunner(settings))
+ {
+ this.Comment("Configure launch");
+ runner.Launch(settings.DebuggerSettings, debuggee, "-fCalling");
+
+ this.Comment("Set a breakpoint with hit condition = 10 (last hit in a 10-iteration loop)");
+ SourceBreakpoints callingBreakpoints = new SourceBreakpoints(debuggee, SinkHelper.Calling);
+ callingBreakpoints.Add(17, hitCondition: "10");
+ runner.SetBreakpoints(callingBreakpoints);
+
+ this.Comment("Run to breakpoint - should stop on the 10th and final hit");
+ runner.Expects.HitBreakpointEvent(SinkHelper.Calling, 17)
+ .AfterConfigurationDone();
+
+ this.Comment("Verify i == 9 (last iteration, 0-indexed)");
+ using (IThreadInspector inspector = runner.GetThreadInspector())
+ {
+ IFrameInspector mainFrame = inspector.Stack.First();
+ mainFrame.AssertVariables("i", "9");
+ }
+
+ this.Comment("Run to completion");
+ runner.Expects.ExitedEvent()
+ .TerminatedEvent()
+ .AfterContinue();
+
+ runner.DisconnectAndVerify();
+ }
+ }
+
+ [Theory]
+ [DependsOnTest(nameof(CompileKitchenSinkForBreakpointTests))]
+ [RequiresTestSettings]
+ public void HitConditionBreakpointEqualExceedsIterations(ITestSettings settings)
+ {
+ this.TestPurpose("Tests that hitCondition exceeding total iterations never stops");
+ this.WriteSettings(settings);
+
+ IDebuggee debuggee = SinkHelper.Open(this, settings.CompilerSettings, DebuggeeMonikers.KitchenSink.Breakpoint);
+
+ using (IDebuggerRunner runner = CreateDebugAdapterRunner(settings))
+ {
+ this.Comment("Configure launch");
+ runner.Launch(settings.DebuggerSettings, debuggee, "-fCalling");
+
+ this.Comment("Set a breakpoint with hit condition = 99 inside a loop that only iterates 10 times");
+ SourceBreakpoints callingBreakpoints = new SourceBreakpoints(debuggee, SinkHelper.Calling);
+ callingBreakpoints.Add(17, hitCondition: "99");
+ runner.SetBreakpoints(callingBreakpoints);
+
+ this.Comment("Run to completion - breakpoint should never fire");
+ runner.Expects.ExitedEvent()
+ .TerminatedEvent()
+ .AfterConfigurationDone();
+
+ runner.DisconnectAndVerify();
+ }
+ }
+
+ [Theory]
+ [DependsOnTest(nameof(CompileKitchenSinkForBreakpointTests))]
+ [RequiresTestSettings]
+ public void HitConditionBreakpointGreaterOrEqualOne(ITestSettings settings)
+ {
+ this.TestPurpose("Tests that hitCondition '>=1' stops on every single hit (same as no condition)");
+ this.WriteSettings(settings);
+
+ IDebuggee debuggee = SinkHelper.Open(this, settings.CompilerSettings, DebuggeeMonikers.KitchenSink.Breakpoint);
+
+ using (IDebuggerRunner runner = CreateDebugAdapterRunner(settings))
+ {
+ this.Comment("Configure launch");
+ runner.Launch(settings.DebuggerSettings, debuggee, "-fCalling");
+
+ this.Comment("Set a breakpoint with hit condition >= 1 (should stop on every hit)");
+ SourceBreakpoints callingBreakpoints = new SourceBreakpoints(debuggee, SinkHelper.Calling);
+ callingBreakpoints.Add(17, hitCondition: ">=1");
+ runner.SetBreakpoints(callingBreakpoints);
+
+ this.Comment("Run to breakpoint - should stop on first hit");
+ runner.Expects.HitBreakpointEvent(SinkHelper.Calling, 17)
+ .AfterConfigurationDone();
+
+ using (IThreadInspector inspector = runner.GetThreadInspector())
+ {
+ IFrameInspector mainFrame = inspector.Stack.First();
+ mainFrame.AssertVariables("i", "0");
+ }
+
+ this.Comment("Continue - should stop on second hit");
+ runner.Expects.HitBreakpointEvent(SinkHelper.Calling, 17)
+ .AfterContinue();
+
+ using (IThreadInspector inspector = runner.GetThreadInspector())
+ {
+ IFrameInspector mainFrame = inspector.Stack.First();
+ mainFrame.AssertVariables("i", "1");
+ }
+
+ this.Comment("Continue - should stop on third hit");
+ runner.Expects.HitBreakpointEvent(SinkHelper.Calling, 17)
+ .AfterContinue();
+
+ using (IThreadInspector inspector = runner.GetThreadInspector())
+ {
+ IFrameInspector mainFrame = inspector.Stack.First();
+ mainFrame.AssertVariables("i", "2");
+ }
+
+ // Remove the breakpoint so we can run to completion without stopping 7 more times
+ this.Comment("Remove the breakpoint and run to completion");
+ callingBreakpoints.Remove(17);
+ runner.SetBreakpoints(callingBreakpoints);
+
+ runner.Expects.ExitedEvent()
+ .TerminatedEvent()
+ .AfterContinue();
+
+ runner.DisconnectAndVerify();
+ }
+ }
+
+ [Theory]
+ [DependsOnTest(nameof(CompileKitchenSinkForBreakpointTests))]
+ [RequiresTestSettings]
+ public void HitConditionBreakpointModuloOne(ITestSettings settings)
+ {
+ this.TestPurpose("Tests that hitCondition '%%1' stops on every hit (degenerate modulo)");
+ this.WriteSettings(settings);
+
+ IDebuggee debuggee = SinkHelper.Open(this, settings.CompilerSettings, DebuggeeMonikers.KitchenSink.Breakpoint);
+
+ using (IDebuggerRunner runner = CreateDebugAdapterRunner(settings))
+ {
+ this.Comment("Configure launch");
+ runner.Launch(settings.DebuggerSettings, debuggee, "-fCalling");
+
+ this.Comment("Set a breakpoint with hit condition %1 (every hit is a multiple of 1)");
+ SourceBreakpoints callingBreakpoints = new SourceBreakpoints(debuggee, SinkHelper.Calling);
+ callingBreakpoints.Add(17, hitCondition: "%1");
+ runner.SetBreakpoints(callingBreakpoints);
+
+ this.Comment("Run to breakpoint - should stop on first hit");
+ runner.Expects.HitBreakpointEvent(SinkHelper.Calling, 17)
+ .AfterConfigurationDone();
+
+ using (IThreadInspector inspector = runner.GetThreadInspector())
+ {
+ IFrameInspector mainFrame = inspector.Stack.First();
+ mainFrame.AssertVariables("i", "0");
+ }
+
+ this.Comment("Continue - should stop on second hit");
+ runner.Expects.HitBreakpointEvent(SinkHelper.Calling, 17)
+ .AfterContinue();
+
+ using (IThreadInspector inspector = runner.GetThreadInspector())
+ {
+ IFrameInspector mainFrame = inspector.Stack.First();
+ mainFrame.AssertVariables("i", "1");
+ }
+
+ // Remove the breakpoint so we can run to completion
+ this.Comment("Remove the breakpoint and run to completion");
+ callingBreakpoints.Remove(17);
+ runner.SetBreakpoints(callingBreakpoints);
+
+ runner.Expects.ExitedEvent()
+ .TerminatedEvent()
+ .AfterContinue();
+
+ runner.DisconnectAndVerify();
+ }
+ }
+
+ [Theory]
+ [DependsOnTest(nameof(CompileKitchenSinkForBreakpointTests))]
+ [RequiresTestSettings]
+ public void HitConditionBreakpointModuloNoMultiple(ITestSettings settings)
+ {
+ this.TestPurpose("Tests that a modulo hit condition whose value exceeds total iterations only fires once at the Nth hit");
+ this.WriteSettings(settings);
+
+ IDebuggee debuggee = SinkHelper.Open(this, settings.CompilerSettings, DebuggeeMonikers.KitchenSink.Breakpoint);
+
+ using (IDebuggerRunner runner = CreateDebugAdapterRunner(settings))
+ {
+ this.Comment("Configure launch");
+ runner.Launch(settings.DebuggerSettings, debuggee, "-fCalling");
+
+ this.Comment("Set a breakpoint with hit condition %7 inside a loop that iterates 10 times");
+ SourceBreakpoints callingBreakpoints = new SourceBreakpoints(debuggee, SinkHelper.Calling);
+ callingBreakpoints.Add(17, hitCondition: "%7");
+ runner.SetBreakpoints(callingBreakpoints);
+
+ this.Comment("Run to breakpoint - should stop on 7th hit");
+ runner.Expects.HitBreakpointEvent(SinkHelper.Calling, 17)
+ .AfterConfigurationDone();
+
+ this.Comment("Verify i == 6 (0-indexed, 7th iteration)");
+ using (IThreadInspector inspector = runner.GetThreadInspector())
+ {
+ IFrameInspector mainFrame = inspector.Stack.First();
+ mainFrame.AssertVariables("i", "6");
+ }
+
+ this.Comment("Run to completion - 14th hit would be next multiple but loop only has 10 iterations");
+ runner.Expects.ExitedEvent()
+ .TerminatedEvent()
+ .AfterContinue();
+
+ runner.DisconnectAndVerify();
+ }
+ }
+
+ [Theory]
+ [DependsOnTest(nameof(CompileKitchenSinkForBreakpointTests))]
+ [RequiresTestSettings]
+ public void HitConditionBreakpointModifyMidRun(ITestSettings settings)
+ {
+ this.TestPurpose("Tests changing a hitCondition while stopped at the breakpoint");
+ this.WriteSettings(settings);
+
+ IDebuggee debuggee = SinkHelper.Open(this, settings.CompilerSettings, DebuggeeMonikers.KitchenSink.Breakpoint);
+
+ using (IDebuggerRunner runner = CreateDebugAdapterRunner(settings))
+ {
+ this.Comment("Configure launch");
+ runner.Launch(settings.DebuggerSettings, debuggee, "-fCalling");
+
+ this.Comment("Set a breakpoint with hit condition = 3");
+ SourceBreakpoints callingBreakpoints = new SourceBreakpoints(debuggee, SinkHelper.Calling);
+ callingBreakpoints.Add(17, hitCondition: "3");
+ runner.SetBreakpoints(callingBreakpoints);
+
+ this.Comment("Run to breakpoint - should stop on 3rd hit");
+ runner.Expects.HitBreakpointEvent(SinkHelper.Calling, 17)
+ .AfterConfigurationDone();
+
+ this.Comment("Verify i == 2 (3rd iteration)");
+ using (IThreadInspector inspector = runner.GetThreadInspector())
+ {
+ IFrameInspector mainFrame = inspector.Stack.First();
+ mainFrame.AssertVariables("i", "2");
+ }
+
+ // Change from EQUAL to GREATER_OR_EQUAL. The engine carries over the
+ // 3 hits already counted, so >=8 should fire on the 8th overall hit.
+ this.Comment("Change hit condition to >=8 while stopped");
+ callingBreakpoints.Remove(17);
+ callingBreakpoints.Add(17, hitCondition: ">=8");
+ runner.SetBreakpoints(callingBreakpoints);
+
+ this.Comment("Continue - should stop on 8th overall hit (i == 7)");
+ runner.Expects.HitBreakpointEvent(SinkHelper.Calling, 17)
+ .AfterContinue();
+
+ this.Comment("Verify i == 7 (8th iteration)");
+ using (IThreadInspector inspector = runner.GetThreadInspector())
+ {
+ IFrameInspector mainFrame = inspector.Stack.First();
+ mainFrame.AssertVariables("i", "7");
+ }
+
+ this.Comment("Continue - should stop on 9th hit");
+ runner.Expects.HitBreakpointEvent(SinkHelper.Calling, 17)
+ .AfterContinue();
+
+ using (IThreadInspector inspector = runner.GetThreadInspector())
+ {
+ IFrameInspector mainFrame = inspector.Stack.First();
+ mainFrame.AssertVariables("i", "8");
+ }
+
+ this.Comment("Continue - should stop on 10th hit");
+ runner.Expects.HitBreakpointEvent(SinkHelper.Calling, 17)
+ .AfterContinue();
+
+ using (IThreadInspector inspector = runner.GetThreadInspector())
+ {
+ IFrameInspector mainFrame = inspector.Stack.First();
+ mainFrame.AssertVariables("i", "9");
+ }
+
+ this.Comment("Run to completion");
+ runner.Expects.ExitedEvent()
+ .TerminatedEvent()
+ .AfterContinue();
+
+ runner.DisconnectAndVerify();
+ }
+ }
+
+ [Theory]
+ [DependsOnTest(nameof(CompileKitchenSinkForBreakpointTests))]
+ [RequiresTestSettings]
+ public void HitConditionBreakpointModifyGteToEqual(ITestSettings settings)
+ {
+ this.TestPurpose("Tests changing hitCondition from >=N to exact N (GTE -> EQUAL) mid-run");
+ this.WriteSettings(settings);
+
+ IDebuggee debuggee = SinkHelper.Open(this, settings.CompilerSettings, DebuggeeMonikers.KitchenSink.Breakpoint);
+
+ using (IDebuggerRunner runner = CreateDebugAdapterRunner(settings))
+ {
+ this.Comment("Configure launch");
+ runner.Launch(settings.DebuggerSettings, debuggee, "-fCalling");
+
+ this.Comment("Set a breakpoint with hit condition >=2");
+ SourceBreakpoints callingBreakpoints = new SourceBreakpoints(debuggee, SinkHelper.Calling);
+ callingBreakpoints.Add(17, hitCondition: ">=2");
+ runner.SetBreakpoints(callingBreakpoints);
+
+ this.Comment("Run to breakpoint - should stop on 2nd hit");
+ runner.Expects.HitBreakpointEvent(SinkHelper.Calling, 17)
+ .AfterConfigurationDone();
+
+ this.Comment("Verify i == 1 (2nd iteration)");
+ using (IThreadInspector inspector = runner.GetThreadInspector())
+ {
+ IFrameInspector mainFrame = inspector.Stack.First();
+ mainFrame.AssertVariables("i", "1");
+ }
+
+ // Change from GTE to EQUAL. Hit count is 2. We want to stop at exactly hit 5.
+ this.Comment("Change hit condition to 5 (exact) while stopped at hit 2");
+ callingBreakpoints.Remove(17);
+ callingBreakpoints.Add(17, hitCondition: "5");
+ runner.SetBreakpoints(callingBreakpoints);
+
+ this.Comment("Continue - should stop on 5th overall hit (i == 4)");
+ runner.Expects.HitBreakpointEvent(SinkHelper.Calling, 17)
+ .AfterContinue();
+
+ this.Comment("Verify i == 4 (5th iteration)");
+ using (IThreadInspector inspector = runner.GetThreadInspector())
+ {
+ IFrameInspector mainFrame = inspector.Stack.First();
+ mainFrame.AssertVariables("i", "4");
+ }
+
+ // Now change from EQUAL to a target that the hit count has already passed.
+ // Changing to "3" while at hit 5 means the target is already passed — program should run to completion.
+ this.Comment("Change hit condition to 3 (already passed) while stopped at hit 5");
+ callingBreakpoints.Remove(17);
+ callingBreakpoints.Add(17, hitCondition: "3");
+ runner.SetBreakpoints(callingBreakpoints);
+
+ this.Comment("Run to completion - hit 3 already passed, EQUAL never fires again");
+ runner.Expects.ExitedEvent()
+ .TerminatedEvent()
+ .AfterContinue();
+
+ runner.DisconnectAndVerify();
+ }
+ }
+
+ [Theory]
+ [DependsOnTest(nameof(CompileKitchenSinkForBreakpointTests))]
+ [RequiresTestSettings]
+ public void HitConditionBreakpointRemoveAndReAdd(ITestSettings settings)
+ {
+ this.TestPurpose("Tests removing a hit condition breakpoint and re-adding it with a different condition");
+ this.WriteSettings(settings);
+
+ IDebuggee debuggee = SinkHelper.Open(this, settings.CompilerSettings, DebuggeeMonikers.KitchenSink.Breakpoint);
+
+ using (IDebuggerRunner runner = CreateDebugAdapterRunner(settings))
+ {
+ this.Comment("Configure launch");
+ runner.Launch(settings.DebuggerSettings, debuggee, "-fCalling");
+
+ this.Comment("Set a breakpoint with hit condition = 2");
+ SourceBreakpoints callingBreakpoints = new SourceBreakpoints(debuggee, SinkHelper.Calling);
+ callingBreakpoints.Add(17, hitCondition: "2");
+ runner.SetBreakpoints(callingBreakpoints);
+
+ this.Comment("Run to breakpoint - should stop on 2nd hit");
+ runner.Expects.HitBreakpointEvent(SinkHelper.Calling, 17)
+ .AfterConfigurationDone();
+
+ this.Comment("Verify i == 1 (2nd iteration)");
+ using (IThreadInspector inspector = runner.GetThreadInspector())
+ {
+ IFrameInspector mainFrame = inspector.Stack.First();
+ mainFrame.AssertVariables("i", "1");
+ }
+
+ this.Comment("Remove the breakpoint entirely");
+ callingBreakpoints.Remove(17);
+ runner.SetBreakpoints(callingBreakpoints);
+
+ this.Comment("Re-add with modulo condition %4 - hit count resets for new breakpoint");
+ callingBreakpoints.Add(17, hitCondition: "%4");
+ runner.SetBreakpoints(callingBreakpoints);
+
+ this.Comment("Continue - breakpoint was removed and re-added, hit count restarted from 0");
+ // After removal and re-add, the next hits are 3rd through 10th loop iterations.
+ // With a fresh %4 condition, it should stop when the new hit count reaches 4.
+ // The 3rd loop iteration is the 1st new hit, so 4th new hit = 6th loop iteration (i==5).
+ runner.Expects.HitBreakpointEvent(SinkHelper.Calling, 17)
+ .AfterContinue();
+
+ this.Comment("Verify the breakpoint stopped at the expected iteration");
+ using (IThreadInspector inspector = runner.GetThreadInspector())
+ {
+ IFrameInspector mainFrame = inspector.Stack.First();
+ mainFrame.AssertVariables("i", "5");
+ }
+
+ this.Comment("Continue - next %4 would be the 8th new hit = 10th loop iteration (i==9)");
+ runner.Expects.HitBreakpointEvent(SinkHelper.Calling, 17)
+ .AfterContinue();
+
+ using (IThreadInspector inspector = runner.GetThreadInspector())
+ {
+ IFrameInspector mainFrame = inspector.Stack.First();
+ mainFrame.AssertVariables("i", "9");
+ }
+
+ this.Comment("Run to completion");
+ runner.Expects.ExitedEvent()
+ .TerminatedEvent()
+ .AfterContinue();
+
+ runner.DisconnectAndVerify();
+ }
+ }
+
+ [Theory]
+ [DependsOnTest(nameof(CompileKitchenSinkForBreakpointTests))]
+ [RequiresTestSettings]
+ public void HitConditionBreakpointWithCondition(ITestSettings settings)
+ {
+ this.TestPurpose("Tests combining a boolean condition with a hitCondition on the same breakpoint");
+ this.WriteSettings(settings);
+
+ IDebuggee debuggee = SinkHelper.Open(this, settings.CompilerSettings, DebuggeeMonikers.KitchenSink.Breakpoint);
+
+ using (IDebuggerRunner runner = CreateDebugAdapterRunner(settings))
+ {
+ this.Comment("Configure launch");
+ runner.Launch(settings.DebuggerSettings, debuggee, "-fCalling");
+
+ this.Comment("Set a breakpoint with condition 'i >= 4' and hitCondition '3'");
+ // The loop runs i = 0..9. The condition 'i >= 4' is true for i = 4,5,6,7,8,9.
+ // Among those qualifying hits, the hitCondition '3' means break on the 3rd qualifying hit.
+ // 1st qualifying hit: i=4, 2nd: i=5, 3rd: i=6 -> should stop at i=6.
+ SourceBreakpoints callingBreakpoints = new SourceBreakpoints(debuggee, SinkHelper.Calling);
+ callingBreakpoints.Add(17, condition: "i >= 4", hitCondition: "3");
+ runner.SetBreakpoints(callingBreakpoints);
+
+ this.Comment("Run to breakpoint");
+ runner.Expects.HitBreakpointEvent(SinkHelper.Calling, 17)
+ .AfterConfigurationDone();
+
+ this.Comment("Verify the breakpoint stopped at the 3rd hit where i >= 4 (i == 6)");
+ using (IThreadInspector inspector = runner.GetThreadInspector())
+ {
+ IFrameInspector mainFrame = inspector.Stack.First();
+ mainFrame.AssertVariables("i", "6");
+ }
+
+ this.Comment("Run to completion - equal condition already satisfied");
+ runner.Expects.ExitedEvent()
+ .TerminatedEvent()
+ .AfterContinue();
+
+ runner.DisconnectAndVerify();
+ }
+ }
+
+ [Theory]
+ [DependsOnTest(nameof(CompileKitchenSinkForBreakpointTests))]
+ [RequiresTestSettings]
+ public void HitConditionBreakpointGreaterOrEqualWithCondition(ITestSettings settings)
+ {
+ this.TestPurpose("Tests combining a boolean condition with a >= hitCondition");
+ this.WriteSettings(settings);
+
+ IDebuggee debuggee = SinkHelper.Open(this, settings.CompilerSettings, DebuggeeMonikers.KitchenSink.Breakpoint);
+
+ using (IDebuggerRunner runner = CreateDebugAdapterRunner(settings))
+ {
+ this.Comment("Configure launch");
+ runner.Launch(settings.DebuggerSettings, debuggee, "-fCalling");
+
+ this.Comment("Set a breakpoint with condition 'i % 2 == 0' and hitCondition '>=3'");
+ // The loop runs i = 0..9. The condition 'i % 2 == 0' is true for i = 0,2,4,6,8.
+ // Among those qualifying hits: 1st: i=0, 2nd: i=2, 3rd: i=4, 4th: i=6, 5th: i=8
+ // The hitCondition '>=3' means break from the 3rd qualifying hit onward.
+ SourceBreakpoints callingBreakpoints = new SourceBreakpoints(debuggee, SinkHelper.Calling);
+ callingBreakpoints.Add(17, condition: "i % 2 == 0", hitCondition: ">=3");
+ runner.SetBreakpoints(callingBreakpoints);
+
+ this.Comment("Run to breakpoint - should stop on 3rd qualifying hit (i == 4)");
+ runner.Expects.HitBreakpointEvent(SinkHelper.Calling, 17)
+ .AfterConfigurationDone();
+
+ using (IThreadInspector inspector = runner.GetThreadInspector())
+ {
+ IFrameInspector mainFrame = inspector.Stack.First();
+ mainFrame.AssertVariables("i", "4");
+ }
+
+ this.Comment("Continue - should stop on 4th qualifying hit (i == 6)");
+ runner.Expects.HitBreakpointEvent(SinkHelper.Calling, 17)
+ .AfterContinue();
+
+ using (IThreadInspector inspector = runner.GetThreadInspector())
+ {
+ IFrameInspector mainFrame = inspector.Stack.First();
+ mainFrame.AssertVariables("i", "6");
+ }
+
+ this.Comment("Continue - should stop on 5th qualifying hit (i == 8)");
+ runner.Expects.HitBreakpointEvent(SinkHelper.Calling, 17)
+ .AfterContinue();
+
+ using (IThreadInspector inspector = runner.GetThreadInspector())
+ {
+ IFrameInspector mainFrame = inspector.Stack.First();
+ mainFrame.AssertVariables("i", "8");
+ }
+
+ this.Comment("Run to completion");
+ runner.Expects.ExitedEvent()
+ .TerminatedEvent()
+ .AfterContinue();
+
+ runner.DisconnectAndVerify();
+ }
+ }
+
+ [Theory]
+ [DependsOnTest(nameof(CompileKitchenSinkForBreakpointTests))]
+ [RequiresTestSettings]
+ [UnsupportedDebugger(SupportedDebugger.VsDbg, SupportedArchitecture.x86 | SupportedArchitecture.x64)]
+ public void InvalidHitConditionBreakpoint(ITestSettings settings)
+ {
+ this.TestPurpose("Tests that an invalid hit condition returns a non-verified breakpoint with an error message");
+ this.WriteSettings(settings);
+
+ IDebuggee debuggee = SinkHelper.Open(this, settings.CompilerSettings, DebuggeeMonikers.KitchenSink.Breakpoint);
+
+ using (IDebuggerRunner runner = CreateDebugAdapterRunner(settings))
+ {
+ this.Comment("Configure launch");
+ runner.Launch(settings.DebuggerSettings, debuggee, "-fCalling");
+
+ this.Comment("Set a breakpoint with an invalid hit condition");
+ SourceBreakpoints callingBreakpoints = new SourceBreakpoints(debuggee, SinkHelper.Calling);
+ callingBreakpoints.Add(17, hitCondition: "invalid");
+ var response = runner.SetBreakpoints(callingBreakpoints);
+
+ this.Comment("Verify breakpoint is not verified and has an error message about hit condition");
+ Assert.NotNull(response.body.breakpoints);
+ Assert.Single(response.body.breakpoints);
+ Assert.False(response.body.breakpoints[0].verified, "Breakpoint with invalid hit condition should not be verified");
+ Assert.Contains("hitCondition", response.body.breakpoints[0].message);
+
+ this.Comment("Run to completion");
+ runner.Expects.ExitedEvent()
+ .TerminatedEvent()
+ .AfterConfigurationDone();
+
+ runner.DisconnectAndVerify();
+ }
+ }
+
+ [Theory]
+ [DependsOnTest(nameof(CompileKitchenSinkForBreakpointTests))]
+ [RequiresTestSettings]
+ [UnsupportedDebugger(SupportedDebugger.VsDbg, SupportedArchitecture.x86 | SupportedArchitecture.x64)]
+ public void InvalidLogMessageBreakpoint(ITestSettings settings)
+ {
+ this.TestPurpose("Tests that an invalid log message returns a non-verified breakpoint with an error message");
+ this.WriteSettings(settings);
+
+ IDebuggee debuggee = SinkHelper.Open(this, settings.CompilerSettings, DebuggeeMonikers.KitchenSink.Breakpoint);
+
+ using (IDebuggerRunner runner = CreateDebugAdapterRunner(settings))
+ {
+ this.Comment("Configure launch");
+ runner.Launch(settings.DebuggerSettings, debuggee, "-fCalling");
+
+ this.Comment("Set a breakpoint with an invalid log message (unmatched brace)");
+ SourceBreakpoints callingBreakpoints = new SourceBreakpoints(debuggee, SinkHelper.Calling);
+ callingBreakpoints.Add(17, logMessage: "{unmatched");
+ var response = runner.SetBreakpoints(callingBreakpoints);
+
+ this.Comment("Verify breakpoint is not verified and has an error message about log message");
+ Assert.NotNull(response.body.breakpoints);
+ Assert.Single(response.body.breakpoints);
+ Assert.False(response.body.breakpoints[0].verified, "Breakpoint with invalid log message should not be verified");
+ Assert.Contains("logMessage", response.body.breakpoints[0].message);
+
+ this.Comment("Run to completion");
+ runner.Expects.ExitedEvent()
+ .TerminatedEvent()
+ .AfterConfigurationDone();
+
+ runner.DisconnectAndVerify();
+ }
+ }
+
+ [Theory]
+ [DependsOnTest(nameof(CompileKitchenSinkForBreakpointTests))]
+ [RequiresTestSettings]
+ [UnsupportedDebugger(SupportedDebugger.VsDbg, SupportedArchitecture.x86 | SupportedArchitecture.x64)]
+ public void InvalidLogMessageAndHitConditionBreakpoint(ITestSettings settings)
+ {
+ this.TestPurpose("Tests that both invalid log message and invalid hit condition errors are returned together");
+ this.WriteSettings(settings);
+
+ IDebuggee debuggee = SinkHelper.Open(this, settings.CompilerSettings, DebuggeeMonikers.KitchenSink.Breakpoint);
+
+ using (IDebuggerRunner runner = CreateDebugAdapterRunner(settings))
+ {
+ this.Comment("Configure launch");
+ runner.Launch(settings.DebuggerSettings, debuggee, "-fCalling");
+
+ this.Comment("Set a breakpoint with both an invalid log message and an invalid hit condition");
+ SourceBreakpoints callingBreakpoints = new SourceBreakpoints(debuggee, SinkHelper.Calling);
+ callingBreakpoints.Add(17, logMessage: "{unmatched", hitCondition: "invalid");
+ var response = runner.SetBreakpoints(callingBreakpoints);
+
+ this.Comment("Verify breakpoint is not verified and error message mentions both logMessage and hitCondition");
+ Assert.NotNull(response.body.breakpoints);
+ Assert.Single(response.body.breakpoints);
+ Assert.False(response.body.breakpoints[0].verified, "Breakpoint with invalid log message and hit condition should not be verified");
+ Assert.Contains("logMessage", response.body.breakpoints[0].message);
+ Assert.Contains("hitCondition", response.body.breakpoints[0].message);
+
this.Comment("Run to completion");
runner.Expects.ExitedEvent()
.TerminatedEvent()
diff --git a/test/DebuggerTesting/OpenDebug/Commands/InitializeCommand.cs b/test/DebuggerTesting/OpenDebug/Commands/InitializeCommand.cs
index caca01140..b91fe14d0 100644
--- a/test/DebuggerTesting/OpenDebug/Commands/InitializeCommand.cs
+++ b/test/DebuggerTesting/OpenDebug/Commands/InitializeCommand.cs
@@ -35,6 +35,9 @@ public sealed class Body
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
public bool? supportsSetVariable;
+
+ [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
+ public bool? supportsHitConditionalBreakpoints;
}
public Body body = new Body();
diff --git a/test/DebuggerTesting/OpenDebug/Commands/SetBreakpointsCommand.cs b/test/DebuggerTesting/OpenDebug/Commands/SetBreakpointsCommand.cs
index d16f9fabd..4ccc12c72 100644
--- a/test/DebuggerTesting/OpenDebug/Commands/SetBreakpointsCommand.cs
+++ b/test/DebuggerTesting/OpenDebug/Commands/SetBreakpointsCommand.cs
@@ -18,12 +18,13 @@ public sealed class SetBreakpointsCommandArgs : JsonValue
{
public sealed class SourceBreakpoint
{
- public SourceBreakpoint(int line, int? column, string condition, string logMessage)
+ public SourceBreakpoint(int line, int? column, string condition, string logMessage, string hitCondition)
{
this.line = line;
this.column = column;
this.condition = condition;
this.logMessage = logMessage;
+ this.hitCondition = hitCondition;
}
public int line;
@@ -36,6 +37,9 @@ public SourceBreakpoint(int line, int? column, string condition, string logMessa
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
public string logMessage;
+
+ [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
+ public string hitCondition;
}
public Source source = new Source();
@@ -72,11 +76,11 @@ public SourceBreakpoints(string sourceRoot, string relativePath)
#region Add/Remove
- public SourceBreakpoints Add(int lineNumber, string condition = null, string logMessage = null)
+ public SourceBreakpoints Add(int lineNumber, string condition = null, string logMessage = null, string hitCondition = null)
{
if (this.Breakpoints.ContainsKey(lineNumber))
throw new RunnerException("Breakpoint line {0} already added to file {1}.", lineNumber, this.RelativePath);
- this.Breakpoints.Add(lineNumber, new SetBreakpointsCommandArgs.SourceBreakpoint(lineNumber, null, condition, logMessage));
+ this.Breakpoints.Add(lineNumber, new SetBreakpointsCommandArgs.SourceBreakpoint(lineNumber, null, condition, logMessage, hitCondition));
return this;
}