|
3 | 3 | import * as assert from 'assert'; |
4 | 4 | import { DebugState } from '../debugState'; |
5 | 5 | import { DebuggingHandler } from '../debuggingHandler'; |
| 6 | +import { IDebuggingExecutor } from '../debuggingExecutor'; |
6 | 7 |
|
7 | 8 | /** |
8 | 9 | * Test suite for DebuggingHandler state change detection |
@@ -106,3 +107,99 @@ suite('DebuggingHandler State Change Detection', () => { |
106 | 107 | assert.strictEqual(hasStateChanged, true); |
107 | 108 | }); |
108 | 109 | }); |
| 110 | + |
| 111 | +/** |
| 112 | + * Tests for the event-driven waitForStateChange (exercised via handleStepOver). |
| 113 | + * |
| 114 | + * The previous implementation blind-slept ~1s per step regardless of when the |
| 115 | + * stop actually landed. The current one resolves the moment the new state is |
| 116 | + * observable (fast-path) or the session ends, and only falls back to the |
| 117 | + * configured timeout when nothing changes. These tests drive that logic through |
| 118 | + * the injected executor's getCurrentDebugState — the real vscode.debug events |
| 119 | + * can't be fired from a unit test, but the fast-path and timeout branches both |
| 120 | + * funnel through the executor, which is what we assert on here. |
| 121 | + */ |
| 122 | +suite('DebuggingHandler waitForStateChange (event-driven)', () => { |
| 123 | + |
| 124 | + function lineState(line: number, active = true): DebugState { |
| 125 | + const s = new DebugState(); |
| 126 | + s.sessionActive = active; |
| 127 | + if (active) { |
| 128 | + s.updateLocation('/test/file.js', 'file.js', line, `let v = ${line};`, []); |
| 129 | + s.updateContext(1, 1); |
| 130 | + s.updateFrameName('main'); |
| 131 | + } |
| 132 | + return s; |
| 133 | + } |
| 134 | + |
| 135 | + // Minimal executor whose getCurrentDebugState is driven by a per-call |
| 136 | + // function (call index is 0-based). All other methods are inert. |
| 137 | + function makeExecutor(getState: (call: number) => DebugState): IDebuggingExecutor { |
| 138 | + let call = 0; |
| 139 | + return { |
| 140 | + startDebugging: async () => true, |
| 141 | + debugTestAtCursor: async () => ({ started: true, runComplete: new Promise<void>(() => { /* pending */ }) }), |
| 142 | + stopDebugging: async () => { /* noop */ }, |
| 143 | + stepOver: async () => { /* noop */ }, |
| 144 | + stepInto: async () => { /* noop */ }, |
| 145 | + stepOut: async () => { /* noop */ }, |
| 146 | + continue: async () => { /* noop */ }, |
| 147 | + restart: async () => { /* noop */ }, |
| 148 | + addBreakpoint: async () => { /* noop */ }, |
| 149 | + removeBreakpoint: async () => { /* noop */ }, |
| 150 | + getCurrentDebugState: async () => getState(call++), |
| 151 | + getVariables: async () => ({}), |
| 152 | + evaluateExpression: async () => ({}), |
| 153 | + getBreakpoints: () => [], |
| 154 | + clearAllBreakpoints: () => { /* noop */ }, |
| 155 | + hasActiveSession: async () => true, |
| 156 | + getActiveSession: () => undefined, |
| 157 | + waitForDebugSessionReady: async () => 'no-session' |
| 158 | + }; |
| 159 | + } |
| 160 | + |
| 161 | + test('fast-path: resolves immediately when the new line is already observable', async () => { |
| 162 | + // call 0 = before (line 10); subsequent calls = after (line 11). |
| 163 | + const executor = makeExecutor(call => (call === 0 ? lineState(10) : lineState(11))); |
| 164 | + // Large timeout: if this resolved via the timer instead of the fast |
| 165 | + // path, the test would take 30s and the latency assertion would fail. |
| 166 | + const handler = new DebuggingHandler(executor, {} as any, 30); |
| 167 | + |
| 168 | + const started = Date.now(); |
| 169 | + const result = await handler.handleStepOver(); |
| 170 | + const elapsed = Date.now() - started; |
| 171 | + |
| 172 | + assert.match(result, /"currentLine": 11/, `expected to land on line 11, got: ${result}`); |
| 173 | + assert.ok(elapsed < 2000, `fast-path should resolve in ms, took ${elapsed}ms (did it wait for the timeout?)`); |
| 174 | + }); |
| 175 | + |
| 176 | + test('fast-path: resolves immediately when the session has ended', async () => { |
| 177 | + // call 0 = active (line 10); subsequent calls = session inactive. |
| 178 | + const executor = makeExecutor(call => (call === 0 ? lineState(10) : lineState(0, false))); |
| 179 | + const handler = new DebuggingHandler(executor, {} as any, 30); |
| 180 | + |
| 181 | + const started = Date.now(); |
| 182 | + const result = await handler.handleStepOver(); |
| 183 | + const elapsed = Date.now() - started; |
| 184 | + |
| 185 | + assert.match(result, /"sessionActive": false/, `expected an inactive session, got: ${result}`); |
| 186 | + assert.ok(elapsed < 2000, `termination should resolve in ms, took ${elapsed}ms`); |
| 187 | + }); |
| 188 | + |
| 189 | + test('timeout: falls back to the bounded timeout when nothing changes', async () => { |
| 190 | + // Every call reports the same active line — no change, no event. |
| 191 | + const executor = makeExecutor(() => lineState(10)); |
| 192 | + // 0.3s timeout so the fallback path is quick to exercise. |
| 193 | + const handler = new DebuggingHandler(executor, {} as any, 0.3); |
| 194 | + |
| 195 | + const started = Date.now(); |
| 196 | + const result = await handler.handleStepOver(); |
| 197 | + const elapsed = Date.now() - started; |
| 198 | + |
| 199 | + assert.match(result, /"currentLine": 10/, `expected to stay on line 10, got: ${result}`); |
| 200 | + // Must actually wait out the timer (not settle spuriously) but still be |
| 201 | + // bounded by it (not hang). |
| 202 | + assert.ok(elapsed >= 200, `should wait for the ~300ms timeout, only took ${elapsed}ms`); |
| 203 | + assert.ok(elapsed < 3000, `timeout should bound the wait, took ${elapsed}ms`); |
| 204 | + }); |
| 205 | +}); |
0 commit comments