From 667698fb135b8db720eca3c222cd63c2760b67be Mon Sep 17 00:00:00 2001 From: Tom Brewer Date: Thu, 15 Jan 2026 16:15:38 -0700 Subject: [PATCH 01/11] feat: add support for conditional breakpoints and hit counts Adds support for conditional breakpoints and hit count evaluation with common expressions Adds event and request handlers for exposing new functionality --- src/SharpDbg.Application/DebugAdapter.cs | 13 +- .../Debugger/BreakpointManager.cs | 32 +++- .../Debugger/ManagedDebugger_EventHandlers.cs | 161 +++++++++++++++++- .../ManagedDebugger_RequestHandlers.cs | 22 ++- 4 files changed, 218 insertions(+), 10 deletions(-) diff --git a/src/SharpDbg.Application/DebugAdapter.cs b/src/SharpDbg.Application/DebugAdapter.cs index d18ccdb..d3df1e1 100644 --- a/src/SharpDbg.Application/DebugAdapter.cs +++ b/src/SharpDbg.Application/DebugAdapter.cs @@ -182,7 +182,8 @@ protected override InitializeResponse HandleInitializeRequest(InitializeArgument { SupportsConfigurationDoneRequest = true, SupportsFunctionBreakpoints = true, - SupportsConditionalBreakpoints = false, + SupportsConditionalBreakpoints = true, + SupportsHitConditionalBreakpoints = true, SupportsEvaluateForHovers = true, SupportsStepBack = false, SupportsSetVariable = false, @@ -260,8 +261,14 @@ protected override SetBreakpointsResponse HandleSetBreakpointsRequest(SetBreakpo throw new ProtocolException("Missing source path"); } - var lines = arguments.Breakpoints?.Select(bp => ConvertClientLineToDebugger(bp.Line)).ToArray() ?? Array.Empty(); - var breakpoints = _debugger.SetBreakpoints(arguments.Source.Path, lines); + var breakpointRequests = arguments.Breakpoints? + .Select(bp => new BreakpointRequest( + ConvertClientLineToDebugger(bp.Line), + bp.Condition, + bp.HitCondition)) + .ToArray() ?? Array.Empty(); + + var breakpoints = _debugger.SetBreakpoints(arguments.Source.Path, breakpointRequests); var responseBreakpoints = breakpoints.Select(bp => new MSBreakpoint { diff --git a/src/SharpDbg.Infrastructure/Debugger/BreakpointManager.cs b/src/SharpDbg.Infrastructure/Debugger/BreakpointManager.cs index 1f81f38..439c356 100644 --- a/src/SharpDbg.Infrastructure/Debugger/BreakpointManager.cs +++ b/src/SharpDbg.Infrastructure/Debugger/BreakpointManager.cs @@ -48,12 +48,23 @@ public class BreakpointInfo /// Module base address where breakpoint is bound public long? ModuleBaseAddress { get; set; } + + // Conditional breakpoint support + + /// Conditional expression to evaluate when breakpoint is hit + public string? Condition { get; set; } + + /// Hit count condition (e.g., ">=10", "==5", "%3") + public string? HitCondition { get; set; } + + /// Current hit count for this breakpoint + public int HitCount { get; set; } } /// /// Create a new breakpoint /// - public BreakpointInfo CreateBreakpoint(string filePath, int line) + public BreakpointInfo CreateBreakpoint(string filePath, int line, string? condition = null, string? hitCondition = null) { lock (_lock) { @@ -63,7 +74,10 @@ public BreakpointInfo CreateBreakpoint(string filePath, int line) Id = id, FilePath = filePath, Line = line, - Verified = false + Verified = false, + Condition = condition, + HitCondition = hitCondition, + HitCount = 0 }; _breakpoints[id] = bp; @@ -78,6 +92,20 @@ public BreakpointInfo CreateBreakpoint(string filePath, int line) } } + /// + /// Reset hit counts for all breakpoints (e.g., when restarting debugging) + /// + public void ResetHitCounts() + { + lock (_lock) + { + foreach (var bp in _breakpoints.Values) + { + bp.HitCount = 0; + } + } + } + /// /// Update breakpoint with ClrDebug breakpoint /// diff --git a/src/SharpDbg.Infrastructure/Debugger/ManagedDebugger_EventHandlers.cs b/src/SharpDbg.Infrastructure/Debugger/ManagedDebugger_EventHandlers.cs index 0d1766f..41c9805 100644 --- a/src/SharpDbg.Infrastructure/Debugger/ManagedDebugger_EventHandlers.cs +++ b/src/SharpDbg.Infrastructure/Debugger/ManagedDebugger_EventHandlers.cs @@ -1,5 +1,7 @@ -using ClrDebug; +using System.Runtime.InteropServices; +using ClrDebug; using SharpDbg.Infrastructure.Debugger.ExpressionEvaluator; +using SharpDbg.Infrastructure.Debugger.ExpressionEvaluator.Compiler; using SharpDbg.Infrastructure.Debugger.ExpressionEvaluator.Interpreter; namespace SharpDbg.Infrastructure.Debugger; @@ -145,6 +147,30 @@ private async void HandleBreakpoint(object? sender, BreakpointCorDebugManagedCal var managedBreakpoint = _breakpointManager.FindByCorBreakpoint(functionBreakpoint.Raw); ArgumentNullException.ThrowIfNull(managedBreakpoint); + + managedBreakpoint.HitCount++; + + if (!string.IsNullOrEmpty(managedBreakpoint.HitCondition)) + { + if (!EvaluateHitCondition(managedBreakpoint.HitCount, managedBreakpoint.HitCondition)) + { + _logger?.Invoke($"Hit count condition not met: count={managedBreakpoint.HitCount}, condition={managedBreakpoint.HitCondition}"); + Continue(); + return; + } + } + + if (!string.IsNullOrEmpty(managedBreakpoint.Condition)) + { + var conditionResult = await EvaluateBreakpointCondition(corThread, managedBreakpoint.Condition); + if (!conditionResult) + { + _logger?.Invoke($"Conditional breakpoint condition not met: {managedBreakpoint.Condition}"); + Continue(); + return; + } + } + IsRunning = false; OnStopped2?.Invoke(corThread.Id, managedBreakpoint.FilePath, managedBreakpoint.Line, "breakpoint"); } @@ -154,6 +180,139 @@ private async void HandleBreakpoint(object? sender, BreakpointCorDebugManagedCal } } + /// + /// Evaluate a breakpoint condition expression + /// + private async Task EvaluateBreakpointCondition(CorDebugThread corThread, string condition) + { + try + { + if (_expressionInterpreter is null) + { + _logger?.Invoke("Expression interpreter not initialized, condition evaluation skipped"); + return true; // Stop anyway if we can't evaluate + } + + var threadId = new ThreadId(corThread.Id); + var frameStackDepth = new FrameStackDepth(0); // Top frame + + var compiledExpression = ExpressionCompiler.Compile(condition, false); + var evalContext = new CompiledExpressionEvaluationContext(corThread, threadId, frameStackDepth); + var result = await _expressionInterpreter.Interpret(compiledExpression, evalContext); + + if (result.Error is not null) + { + _logger?.Invoke($"Condition evaluation error for '{condition}': {result.Error}"); + return false; // Don't stop on error - condition couldn't be evaluated, so skip the breakpoint + } + + return IsTruthyValue(result.Value); + } + catch (Exception ex) + { + _logger?.Invoke($"Exception evaluating condition '{condition}': {ex.Message}"); + return false; // Don't stop on exception - condition couldn't be evaluated, so skip the breakpoint + } + } + + /// + /// Evaluate a hit count condition + /// + private static bool EvaluateHitCondition(int hitCount, string hitCondition) + { + // Support common hit count formats: + // "10" or "==10" - break when hit count equals 10 + // ">=10" - break when hit count is >= 10 + // ">10" - break when hit count is > 10 + // "%10" - break every 10th hit (modulo) + + hitCondition = hitCondition.Trim(); + + if (hitCondition.StartsWith(">=")) + { + if (int.TryParse(hitCondition[2..], out var threshold)) + return hitCount >= threshold; + } + else if (hitCondition.StartsWith(">")) + { + if (int.TryParse(hitCondition[1..], out var threshold)) + return hitCount > threshold; + } + else if (hitCondition.StartsWith("<=")) + { + if (int.TryParse(hitCondition[2..], out var threshold)) + return hitCount <= threshold; + } + else if (hitCondition.StartsWith("<")) + { + if (int.TryParse(hitCondition[1..], out var threshold)) + return hitCount < threshold; + } + else if (hitCondition.StartsWith("%")) + { + if (int.TryParse(hitCondition[1..], out var modulo) && modulo > 0) + return hitCount % modulo == 0; + } + else if (hitCondition.StartsWith("==")) + { + if (int.TryParse(hitCondition[2..], out var target)) + return hitCount == target; + } + else + { + // Plain number means "break when hit count equals this" + if (int.TryParse(hitCondition, out var target)) + return hitCount == target; + } + + return false; + } + + /// + /// Check if a debug value is truthy (true, non-zero, non-null) + /// + private bool IsTruthyValue(CorDebugValue? value) + { + if (value is null) return false; + + var unwrapped = value.UnwrapDebugValue(); + + if (unwrapped is CorDebugGenericValue genericValue) + { + IntPtr buffer = Marshal.AllocHGlobal(genericValue.Size); + try + { + genericValue.GetValue(buffer); + return genericValue.Type switch + { + CorElementType.Boolean => Marshal.ReadByte(buffer) != 0, + CorElementType.I1 or CorElementType.U1 => Marshal.ReadByte(buffer) != 0, + CorElementType.I2 or CorElementType.U2 => Marshal.ReadInt16(buffer) != 0, + CorElementType.I4 or CorElementType.U4 => Marshal.ReadInt32(buffer) != 0, + CorElementType.I8 or CorElementType.U8 => Marshal.ReadInt64(buffer) != 0, + CorElementType.R4 => BitConverter.ToSingle(BitConverter.GetBytes(Marshal.ReadInt32(buffer)), 0) != 0, + CorElementType.R8 => BitConverter.ToDouble(BitConverter.GetBytes(Marshal.ReadInt64(buffer)), 0) != 0, + _ => true // Unknown types - default to true + }; + } + catch + { + return false; + } + finally + { + Marshal.FreeHGlobal(buffer); + } + } + + if (unwrapped is CorDebugReferenceValue refValue) + { + return !refValue.IsNull; + } + + return true; + } + private void HandleStepComplete(object? sender, StepCompleteCorDebugManagedCallbackEventArgs stepCompleteEventArgs) { var corThread = stepCompleteEventArgs.Thread; diff --git a/src/SharpDbg.Infrastructure/Debugger/ManagedDebugger_RequestHandlers.cs b/src/SharpDbg.Infrastructure/Debugger/ManagedDebugger_RequestHandlers.cs index 54176ed..03aac72 100644 --- a/src/SharpDbg.Infrastructure/Debugger/ManagedDebugger_RequestHandlers.cs +++ b/src/SharpDbg.Infrastructure/Debugger/ManagedDebugger_RequestHandlers.cs @@ -7,6 +7,11 @@ namespace SharpDbg.Infrastructure.Debugger; +/// +/// Request to set a breakpoint with optional condition and hit condition +/// +public record BreakpointRequest(int Line, string? Condition = null, string? HitCondition = null); + public partial class ManagedDebugger { /// @@ -172,12 +177,21 @@ public async void StepOut(int threadId) } /// - /// Set breakpoints for a source file + /// Set breakpoints for a source file without conditions /// public List SetBreakpoints(string filePath, int[] lines) + { + var requests = lines.Select(line => new BreakpointRequest(line)).ToArray(); + return SetBreakpoints(filePath, requests); + } + + /// + /// Set breakpoints for a source file with optional conditions + /// + public List SetBreakpoints(string filePath, BreakpointRequest[] breakpoints) { //System.Diagnostics.Debugger.Launch(); - _logger?.Invoke($"SetBreakpoints: {filePath}, lines: {string.Join(",", lines)}"); + _logger?.Invoke($"SetBreakpoints: {filePath}, breakpoints: {string.Join(",", breakpoints.Select(b => $"L{b.Line}" + (b.Condition != null ? $"[{b.Condition}]" : "")))}"); // Deactivate and clear existing breakpoints for this file var existingBreakpoints = _breakpointManager.GetBreakpointsForFile(filePath); @@ -199,9 +213,9 @@ public async void StepOut(int threadId) // Create new breakpoints var result = new List(); - foreach (var line in lines) + foreach (var request in breakpoints) { - var bp = _breakpointManager.CreateBreakpoint(filePath, line); + var bp = _breakpointManager.CreateBreakpoint(filePath, request.Line, request.Condition, request.HitCondition); // Try to bind the breakpoint if we have a process if (_process != null) From 7998f132514bb463161a670366a859db36a8dbfc Mon Sep 17 00:00:00 2001 From: Tom Brewer Date: Thu, 15 Jan 2026 16:24:36 -0700 Subject: [PATCH 02/11] tests: add tests covering new conditional breakpoint functionality --- .../ConditionalBreakpointTests.cs | 149 ++++++++++++++++++ .../Helpers/DebugAdapterProcessHelper.cs | 4 +- tests/SharpDbg.Cli.Tests/TestHelper.cs | 8 + 3 files changed, 159 insertions(+), 2 deletions(-) create mode 100644 tests/SharpDbg.Cli.Tests/ConditionalBreakpointTests.cs diff --git a/tests/SharpDbg.Cli.Tests/ConditionalBreakpointTests.cs b/tests/SharpDbg.Cli.Tests/ConditionalBreakpointTests.cs new file mode 100644 index 0000000..a3d5967 --- /dev/null +++ b/tests/SharpDbg.Cli.Tests/ConditionalBreakpointTests.cs @@ -0,0 +1,149 @@ +using AwesomeAssertions; +using SharpDbg.Cli.Tests.Helpers; + +namespace SharpDbg.Cli.Tests; + +public class ConditionalBreakpointTests(ITestOutputHelper testOutputHelper) +{ + [Fact] + public async Task ConditionalBreakpoint_WithTrueCondition_Stops() + { + var startSuspended = true; + + var (debugProtocolHost, initializedEventTcs, stoppedEventTcs, adapter, p2) = TestHelper.GetRunningDebugProtocolHostInProc(testOutputHelper, startSuspended); + using var _ = adapter; + using var __ = new ProcessKiller(p2); + + await debugProtocolHost + .WithInitializeRequest() + .WithAttachRequest(p2.Id) + .WaitForInitializedEvent(initializedEventTcs); + + debugProtocolHost + .WithConditionalBreakpointsRequest(22, condition: "myInt == 4") + .WithConfigurationDoneRequest() + .WithOptionalResumeRuntime(p2.Id, startSuspended); + + var stoppedEvent = await debugProtocolHost.WaitForStoppedEvent(stoppedEventTcs); + var stopInfo = stoppedEvent.ReadStopInfo(); + stopInfo.filePath.Should().EndWith("MyClass.cs"); + stopInfo.line.Should().Be(22); + } + + [Fact] + public async Task ConditionalBreakpoint_WithFalseCondition_DoesNotStop() + { + var startSuspended = true; + + var (debugProtocolHost, initializedEventTcs, stoppedEventTcs, adapter, p2) = TestHelper.GetRunningDebugProtocolHostInProc(testOutputHelper, startSuspended); + using var _ = adapter; + using var __ = new ProcessKiller(p2); + + await debugProtocolHost + .WithInitializeRequest() + .WithAttachRequest(p2.Id) + .WaitForInitializedEvent(initializedEventTcs); + + debugProtocolHost + .WithConditionalBreakpointsRequest(22, condition: "myInt == 999") + .WithBreakpointsRequest(20, Path.JoinFromGitRoot("tests", "DebuggableConsoleApp", "MyClass.cs")) + .WithConfigurationDoneRequest() + .WithOptionalResumeRuntime(p2.Id, startSuspended); + + // Should hit the unconditional breakpoint on line 20, not the conditional one on line 22 + var stoppedEvent = await debugProtocolHost.WaitForStoppedEvent(stoppedEventTcs); + var stopInfo = stoppedEvent.ReadStopInfo(); + stopInfo.filePath.Should().EndWith("MyClass.cs"); + stopInfo.line.Should().Be(20); + } + + [Fact] + public async Task HitCondition_EqualsN_StopsOnNthHit() + { + var startSuspended = true; + + var (debugProtocolHost, initializedEventTcs, stoppedEventTcs, adapter, p2) = TestHelper.GetRunningDebugProtocolHostInProc(testOutputHelper, startSuspended); + using var _ = adapter; + using var __ = new ProcessKiller(p2); + + await debugProtocolHost + .WithInitializeRequest() + .WithAttachRequest(p2.Id) + .WaitForInitializedEvent(initializedEventTcs); + + debugProtocolHost + .WithConditionalBreakpointsRequest(22, hitCondition: "==2") + .WithConfigurationDoneRequest() + .WithOptionalResumeRuntime(p2.Id, startSuspended); + + // Should stop on 2nd hit, not 1st + var stoppedEvent = await debugProtocolHost.WaitForStoppedEvent(stoppedEventTcs); + var stopInfo = stoppedEvent.ReadStopInfo(); + stopInfo.filePath.Should().EndWith("MyClass.cs"); + stopInfo.line.Should().Be(22); + } + + [Fact] + public async Task HitCondition_GreaterThanOrEqual_StopsAfterThreshold() + { + var startSuspended = true; + + var (debugProtocolHost, initializedEventTcs, stoppedEventTcs, adapter, p2) = TestHelper.GetRunningDebugProtocolHostInProc(testOutputHelper, startSuspended); + using var _ = adapter; + using var __ = new ProcessKiller(p2); + + await debugProtocolHost + .WithInitializeRequest() + .WithAttachRequest(p2.Id) + .WaitForInitializedEvent(initializedEventTcs); + + debugProtocolHost + .WithConditionalBreakpointsRequest(22, hitCondition: ">=2") + .WithConfigurationDoneRequest() + .WithOptionalResumeRuntime(p2.Id, startSuspended); + + // First stop should be on 2nd iteration (hit count >= 2) + var stoppedEvent = await debugProtocolHost.WaitForStoppedEvent(stoppedEventTcs); + var stopInfo = stoppedEvent.ReadStopInfo(); + stopInfo.filePath.Should().EndWith("MyClass.cs"); + stopInfo.line.Should().Be(22); + + // Continue - should stop again on 3rd iteration + var stoppedEvent2 = await debugProtocolHost.WithContinueRequest().WaitForStoppedEvent(stoppedEventTcs); + var stopInfo2 = stoppedEvent2.ReadStopInfo(); + stopInfo2.filePath.Should().EndWith("MyClass.cs"); + stopInfo2.line.Should().Be(22); + } + + [Fact] + public async Task HitCondition_Modulo_StopsEveryNthHit() + { + var startSuspended = true; + + var (debugProtocolHost, initializedEventTcs, stoppedEventTcs, adapter, p2) = TestHelper.GetRunningDebugProtocolHostInProc(testOutputHelper, startSuspended); + using var _ = adapter; + using var __ = new ProcessKiller(p2); + + await debugProtocolHost + .WithInitializeRequest() + .WithAttachRequest(p2.Id) + .WaitForInitializedEvent(initializedEventTcs); + + debugProtocolHost + .WithConditionalBreakpointsRequest(22, hitCondition: "%2") + .WithConfigurationDoneRequest() + .WithOptionalResumeRuntime(p2.Id, startSuspended); + + // First stop should be on 2nd iteration (2 % 2 == 0) + var stoppedEvent = await debugProtocolHost.WaitForStoppedEvent(stoppedEventTcs); + var stopInfo = stoppedEvent.ReadStopInfo(); + stopInfo.filePath.Should().EndWith("MyClass.cs"); + stopInfo.line.Should().Be(22); + + // Continue - should skip 3rd, stop on 4th (4 % 2 == 0) + var stoppedEvent2 = await debugProtocolHost.WithContinueRequest().WaitForStoppedEvent(stoppedEventTcs); + var stopInfo2 = stoppedEvent2.ReadStopInfo(); + stopInfo2.filePath.Should().EndWith("MyClass.cs"); + stopInfo2.line.Should().Be(22); + } +} diff --git a/tests/SharpDbg.Cli.Tests/Helpers/DebugAdapterProcessHelper.cs b/tests/SharpDbg.Cli.Tests/Helpers/DebugAdapterProcessHelper.cs index 9900c35..d95c305 100644 --- a/tests/SharpDbg.Cli.Tests/Helpers/DebugAdapterProcessHelper.cs +++ b/tests/SharpDbg.Cli.Tests/Helpers/DebugAdapterProcessHelper.cs @@ -86,7 +86,7 @@ public static AttachRequest GetAttachRequest(int processId) }; } - public static SetBreakpointsRequest GetSetBreakpointsRequest(int? line = null, string? filePath = null) + public static SetBreakpointsRequest GetSetBreakpointsRequest(int? line = null, string? filePath = null, string? condition = null, string? hitCondition = null) { line ??= 22; filePath ??= Path.JoinFromGitRoot("tests", "DebuggableConsoleApp", "MyClass.cs"); @@ -96,7 +96,7 @@ public static SetBreakpointsRequest GetSetBreakpointsRequest(int? line = null, s var setBreakpointsRequest = new SetBreakpointsRequest { Source = new Source { Path = debugFilePath }, - Breakpoints = [new SourceBreakpoint { Line = debugFileBreakpointLine }] + Breakpoints = [new SourceBreakpoint { Line = debugFileBreakpointLine, Condition = condition, HitCondition = hitCondition }] }; return setBreakpointsRequest; } diff --git a/tests/SharpDbg.Cli.Tests/TestHelper.cs b/tests/SharpDbg.Cli.Tests/TestHelper.cs index 0bbca6f..3cee74f 100644 --- a/tests/SharpDbg.Cli.Tests/TestHelper.cs +++ b/tests/SharpDbg.Cli.Tests/TestHelper.cs @@ -70,6 +70,14 @@ public static DebugProtocolHost WithBreakpointsRequest(this DebugProtocolHost de return debugProtocolHost; } + public static DebugProtocolHost WithConditionalBreakpointsRequest(this DebugProtocolHost debugProtocolHost, int line, string? condition = null, string? hitCondition = null, string? filePath = null) + { + var setBreakpointsRequest = DebugAdapterProcessHelper.GetSetBreakpointsRequest(line, filePath, condition, hitCondition); + if (File.Exists(setBreakpointsRequest.Source.Path) is false) throw new FileNotFoundException("Source file for breakpoint not found", setBreakpointsRequest.Source.Path); + debugProtocolHost.SendRequestSync(setBreakpointsRequest); + return debugProtocolHost; + } + public static DebugProtocolHost WithClearBreakpointsRequest(this DebugProtocolHost debugProtocolHost, string filePath) { var setBreakpointsRequest = new SetBreakpointsRequest From 04d0e99f754a3b2120726081c1757e940aede4e0 Mon Sep 17 00:00:00 2001 From: Matt Parker <61717342+MattParkerDev@users.noreply.github.com> Date: Tue, 10 Feb 2026 22:52:37 +1000 Subject: [PATCH 03/11] Update DebugAdapter.cs --- src/SharpDbg.Application/DebugAdapter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SharpDbg.Application/DebugAdapter.cs b/src/SharpDbg.Application/DebugAdapter.cs index d3df1e1..6227b20 100644 --- a/src/SharpDbg.Application/DebugAdapter.cs +++ b/src/SharpDbg.Application/DebugAdapter.cs @@ -266,7 +266,7 @@ protected override SetBreakpointsResponse HandleSetBreakpointsRequest(SetBreakpo ConvertClientLineToDebugger(bp.Line), bp.Condition, bp.HitCondition)) - .ToArray() ?? Array.Empty(); + .ToArray() ?? []; var breakpoints = _debugger.SetBreakpoints(arguments.Source.Path, breakpointRequests); From 6c187f4c0cfcae0bcbc65859f545ce759e8f7a7d Mon Sep 17 00:00:00 2001 From: Matt Parker <61717342+MattParkerDev@users.noreply.github.com> Date: Tue, 10 Feb 2026 23:03:57 +1000 Subject: [PATCH 04/11] remove unused method --- .../Debugger/ManagedDebugger_RequestHandlers.cs | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/SharpDbg.Infrastructure/Debugger/ManagedDebugger_RequestHandlers.cs b/src/SharpDbg.Infrastructure/Debugger/ManagedDebugger_RequestHandlers.cs index 8b8d09b..b5269ff 100644 --- a/src/SharpDbg.Infrastructure/Debugger/ManagedDebugger_RequestHandlers.cs +++ b/src/SharpDbg.Infrastructure/Debugger/ManagedDebugger_RequestHandlers.cs @@ -318,15 +318,6 @@ public async void StepOut(int threadId) } } - /// - /// Set breakpoints for a source file without conditions - /// - public List SetBreakpoints(string filePath, int[] lines) - { - var requests = lines.Select(line => new BreakpointRequest(line)).ToArray(); - return SetBreakpoints(filePath, requests); - } - /// /// Set breakpoints for a source file with optional conditions /// From 2cc28c7cfb84cb34a59cb9465c4b4c37f25b7644 Mon Sep 17 00:00:00 2001 From: Matt Parker <61717342+MattParkerDev@users.noreply.github.com> Date: Tue, 10 Feb 2026 23:10:20 +1000 Subject: [PATCH 05/11] remove null check --- src/SharpDbg.Infrastructure/Debugger/ManagedDebugger.cs | 2 +- .../Debugger/ManagedDebugger_EventHandlers.cs | 6 ------ 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/src/SharpDbg.Infrastructure/Debugger/ManagedDebugger.cs b/src/SharpDbg.Infrastructure/Debugger/ManagedDebugger.cs index c2a2681..c7812b1 100644 --- a/src/SharpDbg.Infrastructure/Debugger/ManagedDebugger.cs +++ b/src/SharpDbg.Infrastructure/Debugger/ManagedDebugger.cs @@ -23,7 +23,7 @@ public partial class ManagedDebugger : IDisposable private bool _isAttached; private int? _pendingAttachProcessId; private AsyncStepper? _asyncStepper; - private CompiledExpressionInterpreter? _expressionInterpreter; + private CompiledExpressionInterpreter _expressionInterpreter = null!; public event Action? OnStopped; // ThreadId, FilePath, Line, Reason diff --git a/src/SharpDbg.Infrastructure/Debugger/ManagedDebugger_EventHandlers.cs b/src/SharpDbg.Infrastructure/Debugger/ManagedDebugger_EventHandlers.cs index 41c9805..7d3cad4 100644 --- a/src/SharpDbg.Infrastructure/Debugger/ManagedDebugger_EventHandlers.cs +++ b/src/SharpDbg.Infrastructure/Debugger/ManagedDebugger_EventHandlers.cs @@ -187,12 +187,6 @@ private async Task EvaluateBreakpointCondition(CorDebugThread corThread, s { try { - if (_expressionInterpreter is null) - { - _logger?.Invoke("Expression interpreter not initialized, condition evaluation skipped"); - return true; // Stop anyway if we can't evaluate - } - var threadId = new ThreadId(corThread.Id); var frameStackDepth = new FrameStackDepth(0); // Top frame From 85716fa5fa36200d4078f167ad62980367703100 Mon Sep 17 00:00:00 2001 From: Matt Parker <61717342+MattParkerDev@users.noreply.github.com> Date: Tue, 10 Feb 2026 23:12:36 +1000 Subject: [PATCH 06/11] remove comments --- .../Debugger/ManagedDebugger_EventHandlers.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/SharpDbg.Infrastructure/Debugger/ManagedDebugger_EventHandlers.cs b/src/SharpDbg.Infrastructure/Debugger/ManagedDebugger_EventHandlers.cs index 7d3cad4..565422d 100644 --- a/src/SharpDbg.Infrastructure/Debugger/ManagedDebugger_EventHandlers.cs +++ b/src/SharpDbg.Infrastructure/Debugger/ManagedDebugger_EventHandlers.cs @@ -180,9 +180,6 @@ private async void HandleBreakpoint(object? sender, BreakpointCorDebugManagedCal } } - /// - /// Evaluate a breakpoint condition expression - /// private async Task EvaluateBreakpointCondition(CorDebugThread corThread, string condition) { try @@ -209,9 +206,6 @@ private async Task EvaluateBreakpointCondition(CorDebugThread corThread, s } } - /// - /// Evaluate a hit count condition - /// private static bool EvaluateHitCondition(int hitCount, string hitCondition) { // Support common hit count formats: From 8970ecefef13cd7b0fa83dc86bd50cdaf35be89c Mon Sep 17 00:00:00 2001 From: Matt Parker <61717342+MattParkerDev@users.noreply.github.com> Date: Tue, 10 Feb 2026 23:16:55 +1000 Subject: [PATCH 07/11] refactor to partial --- .../ManagedDebugger_ConditionalBreakpoints.cs | 132 ++++++++++++++++++ .../Debugger/ManagedDebugger_EventHandlers.cs | 121 ---------------- 2 files changed, 132 insertions(+), 121 deletions(-) create mode 100644 src/SharpDbg.Infrastructure/Debugger/ManagedDebugger_ConditionalBreakpoints.cs diff --git a/src/SharpDbg.Infrastructure/Debugger/ManagedDebugger_ConditionalBreakpoints.cs b/src/SharpDbg.Infrastructure/Debugger/ManagedDebugger_ConditionalBreakpoints.cs new file mode 100644 index 0000000..8b3505b --- /dev/null +++ b/src/SharpDbg.Infrastructure/Debugger/ManagedDebugger_ConditionalBreakpoints.cs @@ -0,0 +1,132 @@ +using System.Runtime.InteropServices; +using ClrDebug; +using SharpDbg.Infrastructure.Debugger.ExpressionEvaluator; +using SharpDbg.Infrastructure.Debugger.ExpressionEvaluator.Compiler; + +namespace SharpDbg.Infrastructure.Debugger; + +public partial class ManagedDebugger +{ + private async Task EvaluateBreakpointCondition(CorDebugThread corThread, string condition) + { + try + { + var threadId = new ThreadId(corThread.Id); + var frameStackDepth = new FrameStackDepth(0); // Top frame + + var compiledExpression = ExpressionCompiler.Compile(condition, false); + var evalContext = new CompiledExpressionEvaluationContext(corThread, threadId, frameStackDepth); + var result = await _expressionInterpreter.Interpret(compiledExpression, evalContext); + + if (result.Error is not null) + { + _logger?.Invoke($"Condition evaluation error for '{condition}': {result.Error}"); + return false; // Don't stop on error - condition couldn't be evaluated, so skip the breakpoint + } + + return IsTruthyValue(result.Value); + } + catch (Exception ex) + { + _logger?.Invoke($"Exception evaluating condition '{condition}': {ex.Message}"); + return false; // Don't stop on exception - condition couldn't be evaluated, so skip the breakpoint + } + } + + private static bool EvaluateHitCondition(int hitCount, string hitCondition) + { + // Support common hit count formats: + // "10" or "==10" - break when hit count equals 10 + // ">=10" - break when hit count is >= 10 + // ">10" - break when hit count is > 10 + // "%10" - break every 10th hit (modulo) + + hitCondition = hitCondition.Trim(); + + if (hitCondition.StartsWith(">=")) + { + if (int.TryParse(hitCondition[2..], out var threshold)) + return hitCount >= threshold; + } + else if (hitCondition.StartsWith(">")) + { + if (int.TryParse(hitCondition[1..], out var threshold)) + return hitCount > threshold; + } + else if (hitCondition.StartsWith("<=")) + { + if (int.TryParse(hitCondition[2..], out var threshold)) + return hitCount <= threshold; + } + else if (hitCondition.StartsWith("<")) + { + if (int.TryParse(hitCondition[1..], out var threshold)) + return hitCount < threshold; + } + else if (hitCondition.StartsWith("%")) + { + if (int.TryParse(hitCondition[1..], out var modulo) && modulo > 0) + return hitCount % modulo == 0; + } + else if (hitCondition.StartsWith("==")) + { + if (int.TryParse(hitCondition[2..], out var target)) + return hitCount == target; + } + else + { + // Plain number means "break when hit count equals this" + if (int.TryParse(hitCondition, out var target)) + return hitCount == target; + } + + return false; + } + + /// + /// Check if a debug value is truthy (true, non-zero, non-null) + /// + private static bool IsTruthyValue(CorDebugValue? value) + { + if (value is null) return false; + + var unwrapped = value.UnwrapDebugValue(); + + if (unwrapped is CorDebugGenericValue genericValue) + { + IntPtr buffer = Marshal.AllocHGlobal(genericValue.Size); + try + { + genericValue.GetValue(buffer); + return genericValue.Type switch + { + CorElementType.Boolean => Marshal.ReadByte(buffer) != 0, + CorElementType.I1 or CorElementType.U1 => Marshal.ReadByte(buffer) != 0, + CorElementType.I2 or CorElementType.U2 => Marshal.ReadInt16(buffer) != 0, + CorElementType.I4 or CorElementType.U4 => Marshal.ReadInt32(buffer) != 0, + CorElementType.I8 or CorElementType.U8 => Marshal.ReadInt64(buffer) != 0, + CorElementType.R4 => BitConverter.ToSingle(BitConverter.GetBytes(Marshal.ReadInt32(buffer)), 0) != + 0, + CorElementType.R8 => BitConverter.ToDouble(BitConverter.GetBytes(Marshal.ReadInt64(buffer)), 0) != + 0, + _ => true // Unknown types - default to true + }; + } + catch + { + return false; + } + finally + { + Marshal.FreeHGlobal(buffer); + } + } + + if (unwrapped is CorDebugReferenceValue refValue) + { + return !refValue.IsNull; + } + + return true; + } +} diff --git a/src/SharpDbg.Infrastructure/Debugger/ManagedDebugger_EventHandlers.cs b/src/SharpDbg.Infrastructure/Debugger/ManagedDebugger_EventHandlers.cs index 565422d..62dfd70 100644 --- a/src/SharpDbg.Infrastructure/Debugger/ManagedDebugger_EventHandlers.cs +++ b/src/SharpDbg.Infrastructure/Debugger/ManagedDebugger_EventHandlers.cs @@ -180,127 +180,6 @@ private async void HandleBreakpoint(object? sender, BreakpointCorDebugManagedCal } } - private async Task EvaluateBreakpointCondition(CorDebugThread corThread, string condition) - { - try - { - var threadId = new ThreadId(corThread.Id); - var frameStackDepth = new FrameStackDepth(0); // Top frame - - var compiledExpression = ExpressionCompiler.Compile(condition, false); - var evalContext = new CompiledExpressionEvaluationContext(corThread, threadId, frameStackDepth); - var result = await _expressionInterpreter.Interpret(compiledExpression, evalContext); - - if (result.Error is not null) - { - _logger?.Invoke($"Condition evaluation error for '{condition}': {result.Error}"); - return false; // Don't stop on error - condition couldn't be evaluated, so skip the breakpoint - } - - return IsTruthyValue(result.Value); - } - catch (Exception ex) - { - _logger?.Invoke($"Exception evaluating condition '{condition}': {ex.Message}"); - return false; // Don't stop on exception - condition couldn't be evaluated, so skip the breakpoint - } - } - - private static bool EvaluateHitCondition(int hitCount, string hitCondition) - { - // Support common hit count formats: - // "10" or "==10" - break when hit count equals 10 - // ">=10" - break when hit count is >= 10 - // ">10" - break when hit count is > 10 - // "%10" - break every 10th hit (modulo) - - hitCondition = hitCondition.Trim(); - - if (hitCondition.StartsWith(">=")) - { - if (int.TryParse(hitCondition[2..], out var threshold)) - return hitCount >= threshold; - } - else if (hitCondition.StartsWith(">")) - { - if (int.TryParse(hitCondition[1..], out var threshold)) - return hitCount > threshold; - } - else if (hitCondition.StartsWith("<=")) - { - if (int.TryParse(hitCondition[2..], out var threshold)) - return hitCount <= threshold; - } - else if (hitCondition.StartsWith("<")) - { - if (int.TryParse(hitCondition[1..], out var threshold)) - return hitCount < threshold; - } - else if (hitCondition.StartsWith("%")) - { - if (int.TryParse(hitCondition[1..], out var modulo) && modulo > 0) - return hitCount % modulo == 0; - } - else if (hitCondition.StartsWith("==")) - { - if (int.TryParse(hitCondition[2..], out var target)) - return hitCount == target; - } - else - { - // Plain number means "break when hit count equals this" - if (int.TryParse(hitCondition, out var target)) - return hitCount == target; - } - - return false; - } - - /// - /// Check if a debug value is truthy (true, non-zero, non-null) - /// - private bool IsTruthyValue(CorDebugValue? value) - { - if (value is null) return false; - - var unwrapped = value.UnwrapDebugValue(); - - if (unwrapped is CorDebugGenericValue genericValue) - { - IntPtr buffer = Marshal.AllocHGlobal(genericValue.Size); - try - { - genericValue.GetValue(buffer); - return genericValue.Type switch - { - CorElementType.Boolean => Marshal.ReadByte(buffer) != 0, - CorElementType.I1 or CorElementType.U1 => Marshal.ReadByte(buffer) != 0, - CorElementType.I2 or CorElementType.U2 => Marshal.ReadInt16(buffer) != 0, - CorElementType.I4 or CorElementType.U4 => Marshal.ReadInt32(buffer) != 0, - CorElementType.I8 or CorElementType.U8 => Marshal.ReadInt64(buffer) != 0, - CorElementType.R4 => BitConverter.ToSingle(BitConverter.GetBytes(Marshal.ReadInt32(buffer)), 0) != 0, - CorElementType.R8 => BitConverter.ToDouble(BitConverter.GetBytes(Marshal.ReadInt64(buffer)), 0) != 0, - _ => true // Unknown types - default to true - }; - } - catch - { - return false; - } - finally - { - Marshal.FreeHGlobal(buffer); - } - } - - if (unwrapped is CorDebugReferenceValue refValue) - { - return !refValue.IsNull; - } - - return true; - } - private void HandleStepComplete(object? sender, StepCompleteCorDebugManagedCallbackEventArgs stepCompleteEventArgs) { var corThread = stepCompleteEventArgs.Thread; From eed8c3f22ad3280c6f3f50d99b57b11cf3356f1c Mon Sep 17 00:00:00 2001 From: Matt Parker <61717342+MattParkerDev@users.noreply.github.com> Date: Tue, 10 Feb 2026 23:18:11 +1000 Subject: [PATCH 08/11] Update ManagedDebugger_ConditionalBreakpoints.cs --- .../ManagedDebugger_ConditionalBreakpoints.cs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/SharpDbg.Infrastructure/Debugger/ManagedDebugger_ConditionalBreakpoints.cs b/src/SharpDbg.Infrastructure/Debugger/ManagedDebugger_ConditionalBreakpoints.cs index 8b3505b..7acf35f 100644 --- a/src/SharpDbg.Infrastructure/Debugger/ManagedDebugger_ConditionalBreakpoints.cs +++ b/src/SharpDbg.Infrastructure/Debugger/ManagedDebugger_ConditionalBreakpoints.cs @@ -48,7 +48,7 @@ private static bool EvaluateHitCondition(int hitCount, string hitCondition) if (int.TryParse(hitCondition[2..], out var threshold)) return hitCount >= threshold; } - else if (hitCondition.StartsWith(">")) + else if (hitCondition.StartsWith('>')) { if (int.TryParse(hitCondition[1..], out var threshold)) return hitCount > threshold; @@ -58,12 +58,12 @@ private static bool EvaluateHitCondition(int hitCount, string hitCondition) if (int.TryParse(hitCondition[2..], out var threshold)) return hitCount <= threshold; } - else if (hitCondition.StartsWith("<")) + else if (hitCondition.StartsWith('<')) { if (int.TryParse(hitCondition[1..], out var threshold)) return hitCount < threshold; } - else if (hitCondition.StartsWith("%")) + else if (hitCondition.StartsWith('%')) { if (int.TryParse(hitCondition[1..], out var modulo) && modulo > 0) return hitCount % modulo == 0; @@ -94,7 +94,7 @@ private static bool IsTruthyValue(CorDebugValue? value) if (unwrapped is CorDebugGenericValue genericValue) { - IntPtr buffer = Marshal.AllocHGlobal(genericValue.Size); + var buffer = Marshal.AllocHGlobal(genericValue.Size); try { genericValue.GetValue(buffer); @@ -105,10 +105,8 @@ private static bool IsTruthyValue(CorDebugValue? value) CorElementType.I2 or CorElementType.U2 => Marshal.ReadInt16(buffer) != 0, CorElementType.I4 or CorElementType.U4 => Marshal.ReadInt32(buffer) != 0, CorElementType.I8 or CorElementType.U8 => Marshal.ReadInt64(buffer) != 0, - CorElementType.R4 => BitConverter.ToSingle(BitConverter.GetBytes(Marshal.ReadInt32(buffer)), 0) != - 0, - CorElementType.R8 => BitConverter.ToDouble(BitConverter.GetBytes(Marshal.ReadInt64(buffer)), 0) != - 0, + CorElementType.R4 => BitConverter.ToSingle(BitConverter.GetBytes(Marshal.ReadInt32(buffer)), 0) != 0, + CorElementType.R8 => BitConverter.ToDouble(BitConverter.GetBytes(Marshal.ReadInt64(buffer)), 0) != 0, _ => true // Unknown types - default to true }; } From 123353f48d4ab09bccc765c72bdc3bd5422c74e6 Mon Sep 17 00:00:00 2001 From: Matt Parker <61717342+MattParkerDev@users.noreply.github.com> Date: Tue, 10 Feb 2026 23:21:11 +1000 Subject: [PATCH 09/11] simplify null check --- src/SharpDbg.Infrastructure/Debugger/BreakpointManager.cs | 2 ++ .../Debugger/ManagedDebugger_EventHandlers.cs | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/SharpDbg.Infrastructure/Debugger/BreakpointManager.cs b/src/SharpDbg.Infrastructure/Debugger/BreakpointManager.cs index d71c30c..154fd8b 100644 --- a/src/SharpDbg.Infrastructure/Debugger/BreakpointManager.cs +++ b/src/SharpDbg.Infrastructure/Debugger/BreakpointManager.cs @@ -69,6 +69,8 @@ public BreakpointInfo CreateBreakpoint(string filePath, int line, string? condit lock (_lock) { var id = _nextBreakpointId++; + if (string.IsNullOrWhiteSpace(condition)) condition = null; + if (string.IsNullOrWhiteSpace(hitCondition)) hitCondition = null; var bp = new BreakpointInfo { Id = id, diff --git a/src/SharpDbg.Infrastructure/Debugger/ManagedDebugger_EventHandlers.cs b/src/SharpDbg.Infrastructure/Debugger/ManagedDebugger_EventHandlers.cs index 62dfd70..60b2e68 100644 --- a/src/SharpDbg.Infrastructure/Debugger/ManagedDebugger_EventHandlers.cs +++ b/src/SharpDbg.Infrastructure/Debugger/ManagedDebugger_EventHandlers.cs @@ -150,7 +150,7 @@ private async void HandleBreakpoint(object? sender, BreakpointCorDebugManagedCal managedBreakpoint.HitCount++; - if (!string.IsNullOrEmpty(managedBreakpoint.HitCondition)) + if (managedBreakpoint.HitCondition is not null) { if (!EvaluateHitCondition(managedBreakpoint.HitCount, managedBreakpoint.HitCondition)) { @@ -160,7 +160,7 @@ private async void HandleBreakpoint(object? sender, BreakpointCorDebugManagedCal } } - if (!string.IsNullOrEmpty(managedBreakpoint.Condition)) + if (managedBreakpoint.Condition is not null) { var conditionResult = await EvaluateBreakpointCondition(corThread, managedBreakpoint.Condition); if (!conditionResult) From 285f31c1b3e9097f8815f72bf0f2717c0ef7fc6c Mon Sep 17 00:00:00 2001 From: Matt Parker <61717342+MattParkerDev@users.noreply.github.com> Date: Tue, 10 Feb 2026 23:24:12 +1000 Subject: [PATCH 10/11] Update ManagedDebugger_EventHandlers.cs --- .../Debugger/ManagedDebugger_EventHandlers.cs | 23 +++++++------------ 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/src/SharpDbg.Infrastructure/Debugger/ManagedDebugger_EventHandlers.cs b/src/SharpDbg.Infrastructure/Debugger/ManagedDebugger_EventHandlers.cs index 60b2e68..750594e 100644 --- a/src/SharpDbg.Infrastructure/Debugger/ManagedDebugger_EventHandlers.cs +++ b/src/SharpDbg.Infrastructure/Debugger/ManagedDebugger_EventHandlers.cs @@ -150,25 +150,18 @@ private async void HandleBreakpoint(object? sender, BreakpointCorDebugManagedCal managedBreakpoint.HitCount++; - if (managedBreakpoint.HitCondition is not null) + if (managedBreakpoint.HitCondition is not null && EvaluateHitCondition(managedBreakpoint.HitCount, managedBreakpoint.HitCondition) is false) { - if (!EvaluateHitCondition(managedBreakpoint.HitCount, managedBreakpoint.HitCondition)) - { - _logger?.Invoke($"Hit count condition not met: count={managedBreakpoint.HitCount}, condition={managedBreakpoint.HitCondition}"); - Continue(); - return; - } + _logger?.Invoke($"Hit count condition not met: count={managedBreakpoint.HitCount}, condition={managedBreakpoint.HitCondition}"); + Continue(); + return; } - if (managedBreakpoint.Condition is not null) + if (managedBreakpoint.Condition is not null && await EvaluateBreakpointCondition(corThread, managedBreakpoint.Condition) is false) { - var conditionResult = await EvaluateBreakpointCondition(corThread, managedBreakpoint.Condition); - if (!conditionResult) - { - _logger?.Invoke($"Conditional breakpoint condition not met: {managedBreakpoint.Condition}"); - Continue(); - return; - } + _logger?.Invoke($"Conditional breakpoint condition not met: {managedBreakpoint.Condition}"); + Continue(); + return; } IsRunning = false; From 942ec110d6a228b537f4b56e8eabb510077fd14a Mon Sep 17 00:00:00 2001 From: Tom Brewer Date: Wed, 11 Feb 2026 20:21:40 -0500 Subject: [PATCH 11/11] refactor: improve new conditional breakpoint test structure - Fixes an issue where new tests weren't correctly covering the change due to breaking on the unconditional line first - Uses a new `HitConditionClass` for testing to match other dedicated test classes --- .../DebuggableConsoleApp/HitConditionClass.cs | 12 ++++ tests/DebuggableConsoleApp/Program.cs | 2 + .../ConditionalBreakpointTests.cs | 62 ++++++++++++++----- 3 files changed, 59 insertions(+), 17 deletions(-) create mode 100644 tests/DebuggableConsoleApp/HitConditionClass.cs diff --git a/tests/DebuggableConsoleApp/HitConditionClass.cs b/tests/DebuggableConsoleApp/HitConditionClass.cs new file mode 100644 index 0000000..3c4e937 --- /dev/null +++ b/tests/DebuggableConsoleApp/HitConditionClass.cs @@ -0,0 +1,12 @@ +namespace DebuggableConsoleApp; + +public class HitConditionClass +{ + private static int _count = 0; + + public void Test() + { + _count++; + ; // breakpoint here + } +} diff --git a/tests/DebuggableConsoleApp/Program.cs b/tests/DebuggableConsoleApp/Program.cs index ad9c74f..407ffb5 100644 --- a/tests/DebuggableConsoleApp/Program.cs +++ b/tests/DebuggableConsoleApp/Program.cs @@ -13,12 +13,14 @@ public static void Main(string[] args) var myClass = new MyClass(); var myAsyncClass = new MyAsyncClass(); var myClassNoMembers = new MyClassNoMembers(); + var hitConditionClass = new HitConditionClass(); while (true) { // Keep the application running to allow debugging myLambdaClass.Test(); myClass.MyMethod(13, 6); myClassNoMembers.MyMethod(42); + hitConditionClass.Test(); var asyncResult = myAsyncClass.MyMethodAsync(4).GetAwaiter().GetResult(); Thread.Sleep(100); //await Task.Delay(500); diff --git a/tests/SharpDbg.Cli.Tests/ConditionalBreakpointTests.cs b/tests/SharpDbg.Cli.Tests/ConditionalBreakpointTests.cs index a3d5967..5246c58 100644 --- a/tests/SharpDbg.Cli.Tests/ConditionalBreakpointTests.cs +++ b/tests/SharpDbg.Cli.Tests/ConditionalBreakpointTests.cs @@ -45,22 +45,23 @@ await debugProtocolHost .WaitForInitializedEvent(initializedEventTcs); debugProtocolHost - .WithConditionalBreakpointsRequest(22, condition: "myInt == 999") - .WithBreakpointsRequest(20, Path.JoinFromGitRoot("tests", "DebuggableConsoleApp", "MyClass.cs")) + .WithConditionalBreakpointsRequest(15, condition: "myInt == 999") + .WithBreakpointsRequest(22, Path.JoinFromGitRoot("tests", "DebuggableConsoleApp", "MyClass.cs")) .WithConfigurationDoneRequest() .WithOptionalResumeRuntime(p2.Id, startSuspended); - // Should hit the unconditional breakpoint on line 20, not the conditional one on line 22 + // Should skip the conditional breakpoint on line 15 (false condition) and hit the unconditional one on line 22 var stoppedEvent = await debugProtocolHost.WaitForStoppedEvent(stoppedEventTcs); var stopInfo = stoppedEvent.ReadStopInfo(); stopInfo.filePath.Should().EndWith("MyClass.cs"); - stopInfo.line.Should().Be(20); + stopInfo.line.Should().Be(22); } [Fact] public async Task HitCondition_EqualsN_StopsOnNthHit() { var startSuspended = true; + var hitConditionFilePath = Path.JoinFromGitRoot("tests", "DebuggableConsoleApp", "HitConditionClass.cs"); var (debugProtocolHost, initializedEventTcs, stoppedEventTcs, adapter, p2) = TestHelper.GetRunningDebugProtocolHostInProc(testOutputHelper, startSuspended); using var _ = adapter; @@ -72,21 +73,27 @@ await debugProtocolHost .WaitForInitializedEvent(initializedEventTcs); debugProtocolHost - .WithConditionalBreakpointsRequest(22, hitCondition: "==2") + .WithConditionalBreakpointsRequest(10, hitCondition: "==2", filePath: hitConditionFilePath) .WithConfigurationDoneRequest() .WithOptionalResumeRuntime(p2.Id, startSuspended); // Should stop on 2nd hit, not 1st var stoppedEvent = await debugProtocolHost.WaitForStoppedEvent(stoppedEventTcs); var stopInfo = stoppedEvent.ReadStopInfo(); - stopInfo.filePath.Should().EndWith("MyClass.cs"); - stopInfo.line.Should().Be(22); + stopInfo.filePath.Should().EndWith("HitConditionClass.cs"); + stopInfo.line.Should().Be(10); + + debugProtocolHost.WithStackTraceRequest(stoppedEvent.ThreadId!.Value, out var stackTraceResponse); + var stackFrameId = stackTraceResponse.StackFrames!.First().Id; + debugProtocolHost.WithEvaluateRequest(stackFrameId, "_count", out var evaluateResponse); + evaluateResponse.Result.Should().Be("2"); } [Fact] public async Task HitCondition_GreaterThanOrEqual_StopsAfterThreshold() { var startSuspended = true; + var hitConditionFilePath = Path.JoinFromGitRoot("tests", "DebuggableConsoleApp", "HitConditionClass.cs"); var (debugProtocolHost, initializedEventTcs, stoppedEventTcs, adapter, p2) = TestHelper.GetRunningDebugProtocolHostInProc(testOutputHelper, startSuspended); using var _ = adapter; @@ -98,27 +105,38 @@ await debugProtocolHost .WaitForInitializedEvent(initializedEventTcs); debugProtocolHost - .WithConditionalBreakpointsRequest(22, hitCondition: ">=2") + .WithConditionalBreakpointsRequest(10, hitCondition: ">=2", filePath: hitConditionFilePath) .WithConfigurationDoneRequest() .WithOptionalResumeRuntime(p2.Id, startSuspended); // First stop should be on 2nd iteration (hit count >= 2) var stoppedEvent = await debugProtocolHost.WaitForStoppedEvent(stoppedEventTcs); var stopInfo = stoppedEvent.ReadStopInfo(); - stopInfo.filePath.Should().EndWith("MyClass.cs"); - stopInfo.line.Should().Be(22); + stopInfo.filePath.Should().EndWith("HitConditionClass.cs"); + stopInfo.line.Should().Be(10); + + debugProtocolHost.WithStackTraceRequest(stoppedEvent.ThreadId!.Value, out var stackTraceResponse); + var stackFrameId = stackTraceResponse.StackFrames!.First().Id; + debugProtocolHost.WithEvaluateRequest(stackFrameId, "_count", out var evaluateResponse); + evaluateResponse.Result.Should().Be("2"); // Continue - should stop again on 3rd iteration var stoppedEvent2 = await debugProtocolHost.WithContinueRequest().WaitForStoppedEvent(stoppedEventTcs); var stopInfo2 = stoppedEvent2.ReadStopInfo(); - stopInfo2.filePath.Should().EndWith("MyClass.cs"); - stopInfo2.line.Should().Be(22); + stopInfo2.filePath.Should().EndWith("HitConditionClass.cs"); + stopInfo2.line.Should().Be(10); + + debugProtocolHost.WithStackTraceRequest(stoppedEvent2.ThreadId!.Value, out var stackTraceResponse2); + var stackFrameId2 = stackTraceResponse2.StackFrames!.First().Id; + debugProtocolHost.WithEvaluateRequest(stackFrameId2, "_count", out var evaluateResponse2); + evaluateResponse2.Result.Should().Be("3"); } [Fact] public async Task HitCondition_Modulo_StopsEveryNthHit() { var startSuspended = true; + var hitConditionFilePath = Path.JoinFromGitRoot("tests", "DebuggableConsoleApp", "HitConditionClass.cs"); var (debugProtocolHost, initializedEventTcs, stoppedEventTcs, adapter, p2) = TestHelper.GetRunningDebugProtocolHostInProc(testOutputHelper, startSuspended); using var _ = adapter; @@ -130,20 +148,30 @@ await debugProtocolHost .WaitForInitializedEvent(initializedEventTcs); debugProtocolHost - .WithConditionalBreakpointsRequest(22, hitCondition: "%2") + .WithConditionalBreakpointsRequest(10, hitCondition: "%2", filePath: hitConditionFilePath) .WithConfigurationDoneRequest() .WithOptionalResumeRuntime(p2.Id, startSuspended); // First stop should be on 2nd iteration (2 % 2 == 0) var stoppedEvent = await debugProtocolHost.WaitForStoppedEvent(stoppedEventTcs); var stopInfo = stoppedEvent.ReadStopInfo(); - stopInfo.filePath.Should().EndWith("MyClass.cs"); - stopInfo.line.Should().Be(22); + stopInfo.filePath.Should().EndWith("HitConditionClass.cs"); + stopInfo.line.Should().Be(10); + + debugProtocolHost.WithStackTraceRequest(stoppedEvent.ThreadId!.Value, out var stackTraceResponse); + var stackFrameId = stackTraceResponse.StackFrames!.First().Id; + debugProtocolHost.WithEvaluateRequest(stackFrameId, "_count", out var evaluateResponse); + evaluateResponse.Result.Should().Be("2"); // Continue - should skip 3rd, stop on 4th (4 % 2 == 0) var stoppedEvent2 = await debugProtocolHost.WithContinueRequest().WaitForStoppedEvent(stoppedEventTcs); var stopInfo2 = stoppedEvent2.ReadStopInfo(); - stopInfo2.filePath.Should().EndWith("MyClass.cs"); - stopInfo2.line.Should().Be(22); + stopInfo2.filePath.Should().EndWith("HitConditionClass.cs"); + stopInfo2.line.Should().Be(10); + + debugProtocolHost.WithStackTraceRequest(stoppedEvent2.ThreadId!.Value, out var stackTraceResponse2); + var stackFrameId2 = stackTraceResponse2.StackFrames!.First().Id; + debugProtocolHost.WithEvaluateRequest(stackFrameId2, "_count", out var evaluateResponse2); + evaluateResponse2.Result.Should().Be("4"); } }