Skip to content

Commit 5b12f9e

Browse files
authored
feat: Add support for hit count breakpoints (#69)
* Add support for hit count breakpoints * Implement hit condition in new breakpoint subsystem. * Added hit condition to function breakpoints. * Changelog. * Added test cases for hit condition (line) breakpoints. * Added function breakpoints with hit condition coverage. * Also cover invalid hit condition.
1 parent 36fcdd0 commit 5b12f9e

File tree

8 files changed

+219
-27
lines changed

8 files changed

+219
-27
lines changed

.vscode/launch.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
"NODE_ENV": "development"
1313
},
1414
"sourceMaps": true,
15-
"outDir": "${workspaceRoot}/out"
15+
"outFiles": ["${workspaceRoot}/out/**/*.js"]
1616
},
1717
{
1818
"name": "Launch Extension",
@@ -21,7 +21,7 @@
2121
"runtimeExecutable": "${execPath}",
2222
"args": ["--extensionDevelopmentPath=${workspaceRoot}"],
2323
"sourceMaps": true,
24-
"outDir": "${workspaceRoot}/out"
24+
"outFiles": ["${workspaceRoot}/out/**/*.js"]
2525
},
2626
{
2727
"name": "Mocha",
@@ -31,7 +31,7 @@
3131
"args": ["out/test", "--no-timeouts", "--colors"],
3232
"cwd": "${workspaceRoot}",
3333
"sourceMaps": true,
34-
"outDir": "${workspaceRoot}/out"
34+
"outFiles": ["${workspaceRoot}/out/**/*.js"]
3535
}
3636
]
3737
}

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file.
44

55
The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/).
66

7+
## [1.18.0]
8+
9+
- Added hit count breakpoint condition.
10+
711
## [1.17.0]
812

913
## Added

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ Options specific to CLI debugging:
9595

9696
- Line breakpoints
9797
- Conditional breakpoints
98+
- Hit count breakpoints: supports the conditions like `>=n`, `==n` and `%n`
9899
- Function breakpoints
99100
- Step over, step in, step out
100101
- Break on entry

src/breakpoints.ts

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,14 +47,35 @@ export class BreakpointManager extends EventEmitter {
4747

4848
vscodeBreakpoints = breakpoints.map(sourceBreakpoint => {
4949
let xdebugBreakpoint: xdebug.Breakpoint
50+
let hitValue: number | undefined
51+
let hitCondition: xdebug.HitCondition | undefined
52+
if (sourceBreakpoint.hitCondition) {
53+
const match = sourceBreakpoint.hitCondition.match(/^\s*(>=|==|%)?\s*(\d+)\s*$/)
54+
if (match) {
55+
hitCondition = (match[1] as xdebug.HitCondition) || '=='
56+
hitValue = parseInt(match[2])
57+
} else {
58+
let vscodeBreakpoint: VSCodeDebugProtocol.Breakpoint = {
59+
verified: false,
60+
line: sourceBreakpoint.line,
61+
source: source,
62+
// id: this._nextId++,
63+
message:
64+
'Invalid hit condition. Specify a number, optionally prefixed with one of the operators >= (default), == or %',
65+
}
66+
return vscodeBreakpoint
67+
}
68+
}
5069
if (sourceBreakpoint.condition) {
5170
xdebugBreakpoint = new xdebug.ConditionalBreakpoint(
5271
sourceBreakpoint.condition,
5372
fileUri,
54-
sourceBreakpoint.line
73+
sourceBreakpoint.line,
74+
hitCondition,
75+
hitValue
5576
)
5677
} else {
57-
xdebugBreakpoint = new xdebug.LineBreakpoint(fileUri, sourceBreakpoint.line)
78+
xdebugBreakpoint = new xdebug.LineBreakpoint(fileUri, sourceBreakpoint.line, hitCondition, hitValue)
5879
}
5980

6081
let vscodeBreakpoint: VSCodeDebugProtocol.Breakpoint = {
@@ -124,9 +145,28 @@ export class BreakpointManager extends EventEmitter {
124145
this._callBreakpoints.clear()
125146

126147
vscodeBreakpoints = breakpoints.map(functionBreakpoint => {
148+
let hitValue: number | undefined
149+
let hitCondition: xdebug.HitCondition | undefined
150+
if (functionBreakpoint.hitCondition) {
151+
const match = functionBreakpoint.hitCondition.match(/^\s*(>=|==|%)?\s*(\d+)\s*$/)
152+
if (match) {
153+
hitCondition = (match[1] as xdebug.HitCondition) || '=='
154+
hitValue = parseInt(match[2])
155+
} else {
156+
let vscodeBreakpoint: VSCodeDebugProtocol.Breakpoint = {
157+
verified: false,
158+
// id: this._nextId++,
159+
message:
160+
'Invalid hit condition. Specify a number, optionally prefixed with one of the operators >= (default), == or %',
161+
}
162+
return vscodeBreakpoint
163+
}
164+
}
127165
let xdebugBreakpoint: xdebug.Breakpoint = new xdebug.CallBreakpoint(
128166
functionBreakpoint.name,
129-
functionBreakpoint.condition
167+
functionBreakpoint.condition,
168+
hitCondition,
169+
hitValue
130170
)
131171

132172
let vscodeBreakpoint: VSCodeDebugProtocol.Breakpoint = {

src/phpDebug.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,7 @@ class PhpDebugSession extends vscode.DebugSession {
179179
supportsConditionalBreakpoints: true,
180180
supportsFunctionBreakpoints: true,
181181
supportsLogPoints: true,
182+
supportsHitConditionalBreakpoints: true,
182183
exceptionBreakpointFilters: [
183184
{
184185
filter: 'Notice',

src/test/adapter.ts

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,117 @@ describe('PHP Debug Adapter', () => {
389389
await assertStoppedLocation('breakpoint', program, 5)
390390
})
391391
})
392+
393+
describe('hit count breakpoints', () => {
394+
const program = path.join(TEST_PROJECT, 'hit.php')
395+
396+
async function testHits(condition: string, hits: string[], verified: boolean = true): Promise<void> {
397+
client.launch({ program })
398+
await client.waitForEvent('initialized')
399+
const breakpoint = (
400+
await client.setBreakpointsRequest({
401+
breakpoints: [{ line: 4, hitCondition: condition }],
402+
source: { path: program },
403+
})
404+
).body.breakpoints[0]
405+
await client.configurationDoneRequest()
406+
if (verified) {
407+
await waitForBreakpointUpdate(breakpoint)
408+
} else {
409+
assert.strictEqual(
410+
breakpoint.message,
411+
'Invalid hit condition. Specify a number, optionally prefixed with one of the operators >= (default), == or %'
412+
)
413+
}
414+
assert.strictEqual(breakpoint.verified, verified)
415+
for (const hitVal of hits) {
416+
const { threadId, frame } = await assertStoppedLocation('breakpoint', program, 4)
417+
const result = (
418+
await client.evaluateRequest({
419+
context: 'watch',
420+
frameId: frame.id,
421+
expression: '$i',
422+
})
423+
).body.result
424+
assert.equal(result, hitVal)
425+
await client.continueRequest({ threadId })
426+
}
427+
await client.waitForEvent('terminated')
428+
}
429+
430+
async function testFunctionHits(
431+
condition: string,
432+
hits: string[],
433+
verified: boolean = true
434+
): Promise<void> {
435+
client.launch({ program })
436+
await client.waitForEvent('initialized')
437+
const breakpoint = (
438+
await client.setFunctionBreakpointsRequest({
439+
breakpoints: [{ name: 'f1', hitCondition: condition }],
440+
})
441+
).body.breakpoints[0]
442+
await client.configurationDoneRequest()
443+
if (verified) {
444+
await waitForBreakpointUpdate(breakpoint)
445+
} else {
446+
assert.strictEqual(
447+
breakpoint.message,
448+
'Invalid hit condition. Specify a number, optionally prefixed with one of the operators >= (default), == or %'
449+
)
450+
}
451+
assert.strictEqual(breakpoint.verified, verified)
452+
for (const hitVal of hits) {
453+
const { threadId, frame } = await assertStoppedLocation('breakpoint', program, 9)
454+
const result = (
455+
await client.evaluateRequest({
456+
context: 'watch',
457+
frameId: frame.id,
458+
expression: '$i',
459+
})
460+
).body.result
461+
assert.equal(result, hitVal)
462+
await client.continueRequest({ threadId })
463+
}
464+
await client.waitForEvent('terminated')
465+
}
466+
467+
describe('hit count line breakpoints', () => {
468+
it('should not stop for broken condition "a"', async () => {
469+
await testHits('a', [], false)
470+
})
471+
it('should stop when the hit count is gte than 3 with condition "3"', async () => {
472+
await testHits('3', ['3'])
473+
})
474+
it('should stop when the hit count is gte than 3 with condition ">=3"', async () => {
475+
await testHits('>=3', ['3', '4', '5'])
476+
})
477+
it('should stop when the hit count is equal to 3 with condition "==3"', async () => {
478+
await testHits('==3', ['3'])
479+
})
480+
it('should stop on every 2nd hit with condition "%2"', async () => {
481+
await testHits('%2', ['2', '4'])
482+
})
483+
})
484+
485+
describe('hit count function breakpoints', () => {
486+
it('should not stop for broken condition "a"', async () => {
487+
await testFunctionHits('a', [], false)
488+
})
489+
it('should stop when the hit count is gte than 3 with condition "3"', async () => {
490+
await testFunctionHits('3', ['3'])
491+
})
492+
it('should stop when the hit count is gte than 3 with condition ">=3"', async () => {
493+
await testFunctionHits('>=3', ['3', '4', '5'])
494+
})
495+
it('should stop when the hit count is equal to 3 with condition "==3"', async () => {
496+
await testFunctionHits('==3', ['3'])
497+
})
498+
it('should stop on every 2nd hit with condition "%2"', async () => {
499+
await testFunctionHits('%2', ['2', '4'])
500+
})
501+
})
502+
})
392503
})
393504

394505
describe('variables', () => {

0 commit comments

Comments
 (0)