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; }