diff --git a/editors/code/src/debugAdapter.ts b/editors/code/src/debugAdapter.ts index 4434ebd..365d8ac 100644 --- a/editors/code/src/debugAdapter.ts +++ b/editors/code/src/debugAdapter.ts @@ -1,9 +1,6 @@ /** * debugAdapter.ts implements the Debug Adapter protocol and integrates it with the log2src * "debugger". - * - * Care should be given to make sure that this module is independent from VS Code so that it - * could potentially be used in other IDE. */ import { @@ -18,6 +15,7 @@ import * as vscode from 'vscode'; import * as path from 'path'; import { outputChannel } from './extension'; +import { LogDebugger } from './logDebugger'; interface CallSite { name: string, @@ -43,7 +41,7 @@ interface SourceRef { name: string, } -interface ILaunchRequestArguments extends DebugProtocol.LaunchRequestArguments { +export interface ILaunchRequestArguments extends DebugProtocol.LaunchRequestArguments { // the source to debug, currently a single file source: string; // the log files to use for "debugging" @@ -81,18 +79,16 @@ export class DebugSession extends LoggingDebugSession { private static _threadID = 1; private _binaryPath: string; - private _breakPoints = new Map(); - private _variableHandles = new Handles<'locals'>(); - private _line = 1; + private readonly _variableHandles = new Handles<'locals'>(); private _launchArgs: ILaunchRequestArguments = { source: "", log: "", log_format: "" }; - private _logLines = Number.MAX_SAFE_INTEGER; - private _highlightDecoration: vscode.TextEditorDecorationType; + private readonly _highlightDecoration: vscode.TextEditorDecorationType; private _mapping?: LogMapping = undefined; + private readonly _logDebugger: LogDebugger; /** * Create a new debug adapter to use with a debug session. */ - public constructor() { + public constructor(logDebugger: LogDebugger) { super("log2src-dap.txt"); this._binaryPath = PLATFORM_TO_BINARY.get(`${process.platform}-${process.arch}`)!; @@ -103,6 +99,7 @@ export class DebugSession extends LoggingDebugSession { ); } + this._logDebugger = logDebugger; this.setDebuggerLinesStartAt1(true); this.setDebuggerColumnsStartAt1(true); @@ -136,19 +133,10 @@ export class DebugSession extends LoggingDebugSession { protected setBreakPointsRequest(response: DebugProtocol.SetBreakpointsResponse, args: DebugProtocol.SetBreakpointsArguments) { console.log(`setBreakPointsRequest ${JSON.stringify(args)}`); - const bpPath = args.source.path as string; - // TODO handle lines? + const source = args.source.path as string; const bps = args.breakpoints || []; - this._breakPoints.set(bpPath, new Array()); - bps.forEach((sourceBp) => { - if (this._line === 1) { - this._line = sourceBp.line; - } - let bps = this._breakPoints.get(bpPath) || []; - const verified = sourceBp.line > 0 && sourceBp.line < this._logLines; - bps.push({ line: sourceBp.line, verified: verified }); - }); - const breakpoints = this._breakPoints.get(bpPath) || []; + const breakpoints = this._logDebugger.setBreakPoint(source, bps); + response.body = { breakpoints: breakpoints }; @@ -168,18 +156,16 @@ export class DebugSession extends LoggingDebugSession { outputChannel.appendLine(`launchRequest ${JSON.stringify(args)}`); // make sure to 'Stop' the buffered logging if 'trace' is not set - logger.setup(args.trace ? Logger.LogLevel.Verbose : Logger.LogLevel.Verbose, false); + logger.setup(args.trace ? Logger.LogLevel.Verbose : Logger.LogLevel.Error, false); this._launchArgs = args; this.openLogAndFocus(); - var execFile = require('child_process').execFileSync; + const execFile = require('child_process').execFileSync; let stdout = execFile('wc', ['-l', this._launchArgs.log]); - this._logLines = +stdout.toString().trim().split(" ")[0] || Number.MAX_VALUE; + const logLines = +stdout.toString().trim().split(" ")[0] || Number.MAX_VALUE + this._logDebugger.setToLog(this._launchArgs.log, logLines); - // TODO do we need this? - // wait 1 second until configuration has finished (and configurationDoneRequest has been called) - // await this._configurationDone.wait(1000); - if (this._breakPoints.size === 0) { + if (!this._logDebugger.hasBreakpoints()) { this.sendEvent(new StoppedEvent('entry', DebugSession._threadID)); } this.sendResponse(response); @@ -190,15 +176,22 @@ export class DebugSession extends LoggingDebugSession { if (editors.length >= 1) { this.focusEditor(editors[0]); } else { - vscode.workspace - .openTextDocument(this._launchArgs.log) + Promise.resolve(vscode.workspace.openTextDocument(this._launchArgs.log)) .then(doc => { return vscode.window.showTextDocument(doc, { viewColumn: vscode.ViewColumn.Beside, preserveFocus: false }); }) - .then(editor => this.focusEditor(editor)); + .then(editor => { + this.focusEditor(editor); + return editor; + }) + .catch(error => { + const message = `Failed to open log file: ${error.message}`; + outputChannel.appendLine(message); + console.error(message); + }); } } @@ -217,8 +210,7 @@ export class DebugSession extends LoggingDebugSession { protected continueRequest(response: DebugProtocol.ContinueResponse, args: DebugProtocol.ContinueArguments): void { console.log(`continueRequest ${JSON.stringify(args)}`); - const next = this.findNextLineToStop(); - this._line = next; + this._logDebugger.gotoNextBreakpoint(); this.sendEvent(new StoppedEvent('breakpoint', DebugSession._threadID)); this.sendResponse(response); } @@ -226,46 +218,21 @@ export class DebugSession extends LoggingDebugSession { protected reverseContinueRequest(response: DebugProtocol.ReverseContinueResponse, args: DebugProtocol.ReverseContinueArguments): void { console.log(`reverseContinueRequest ${JSON.stringify(args)}`); - const next = this.findNextLineToStop(true); - this._line = next; + this._logDebugger.gotoNextBreakpoint(true); this.sendEvent(new StoppedEvent('breakpoint', DebugSession._threadID)); this.sendResponse(response); } - private findNextLineToStop(reverse = false): number { - const bps = this._breakPoints.get(this._launchArgs.log) || []; - let bp; - if (reverse) { - bp = bps.findLast((bp) => { - return reverse ? - (bp.line !== undefined && this._line > bp.line) : - (bp.line !== undefined && this._line < bp.line); - }); - } else { - bp = bps.find((bp) => { - return reverse ? - (bp.line !== undefined && this._line > bp.line) : - (bp.line !== undefined && this._line < bp.line); - }); - } - - if (bp !== undefined && bp.line !== undefined) { - return bp.line; - } else { - return reverse ? 1 : this._logLines; - } - } - protected nextRequest(response: DebugProtocol.NextResponse, args: DebugProtocol.NextArguments): void { - console.log(`nextRequest ${JSON.stringify(args)} line=${this._line}`); - this._line = Math.min(this._logLines, this._line + 1); + console.log(`nextRequest ${JSON.stringify(args)} line=${this._logDebugger.linenum()}`); + this._logDebugger.stepForward() this.sendEvent(new StoppedEvent('step', DebugSession._threadID)); this.sendResponse(response); } protected stepBackRequest(response: DebugProtocol.StepBackResponse, args: DebugProtocol.StepBackArguments): void { - console.log(`stepBackRequest ${JSON.stringify(args)} line=${this._line}`); - this._line = Math.max(1, this._line - 1); + console.log(`stepBackRequest ${JSON.stringify(args)} line=${this._logDebugger.linenum()}`); + this._logDebugger.stepBackward(); this.sendEvent(new StoppedEvent('step', DebugSession._threadID)); this.sendResponse(response); } @@ -275,7 +242,7 @@ export class DebugSession extends LoggingDebugSession { const log2srcPath = path.resolve(__dirname, this._binaryPath); const execFile = require('child_process').execFileSync; - const start = this._line - 1; + const start = this._logDebugger.linenum() - 1; const editors = this.findEditors(); if (editors.length > 0) { @@ -292,7 +259,7 @@ export class DebugSession extends LoggingDebugSession { } outputChannel.appendLine(`args ${l2sArgs.join(" ")}`); let stdout = execFile(log2srcPath, l2sArgs); - this._mapping = JSON.parse(stdout); + this._mapping = JSON.parse(stdout.toString('utf8')); outputChannel.appendLine(`mapped ${JSON.stringify(this._mapping)}`); let index = 0; @@ -309,11 +276,12 @@ export class DebugSession extends LoggingDebugSession { } private findEditors(): vscode.TextEditor[] { - return vscode.window.visibleTextEditors.filter((editor) => editor.document.fileName === this._launchArgs.log); + const target = path.resolve(this._launchArgs.log); + return vscode.window.visibleTextEditors.filter((editor) => editor.document.fileName === target); } private focusEditor(editor: vscode.TextEditor) { - const start = this._line - 1; + const start = this._logDebugger.linenum() - 1; let range = new vscode.Range( new vscode.Position(start, 0), new vscode.Position(start, Number.MAX_VALUE) @@ -367,11 +335,11 @@ export class DebugSession extends LoggingDebugSession { const v = this._variableHandles.get(args.variablesReference); if (v === 'locals' && this._mapping !== undefined) { for (let pair of this._mapping.variables) { - vs.push({ - name: pair.expr, - value: pair.value, - variablesReference: 0 - }); + vs.push({ + name: pair.expr, + value: pair.value, + variablesReference: 0 + }); } } diff --git a/editors/code/src/extension.ts b/editors/code/src/extension.ts index f65f85b..ad48c72 100644 --- a/editors/code/src/extension.ts +++ b/editors/code/src/extension.ts @@ -15,6 +15,7 @@ import * as vscode from 'vscode'; import { ProviderResult } from 'vscode'; import { BinaryNotFoundError, DebugSession } from './debugAdapter'; +import { LogDebugger } from './logDebugger'; const runMode: 'external' | 'server' | 'namedPipeServer' | 'inline' = 'inline'; const outputChannel = vscode.window.createOutputChannel("Log2Src"); @@ -47,7 +48,7 @@ class InlineDebugAdapterFactory implements vscode.DebugAdapterDescriptorFactory createDebugAdapterDescriptor(_session: vscode.DebugSession): ProviderResult { try { - return new vscode.DebugAdapterInlineImplementation(new DebugSession()); + return new vscode.DebugAdapterInlineImplementation(new DebugSession(new LogDebugger())); } catch (error) { if (error instanceof BinaryNotFoundError) { vscode.window.showErrorMessage(`Log2Src Error: ${error.message}`); diff --git a/editors/code/src/logDebugger.ts b/editors/code/src/logDebugger.ts new file mode 100644 index 0000000..7bfbd4c --- /dev/null +++ b/editors/code/src/logDebugger.ts @@ -0,0 +1,75 @@ +/** + * logDebugger.ts handles tracking the state of the "log driven debugger". + * + */ + +import { DebugProtocol } from '@vscode/debugprotocol'; +import * as path from 'path'; + + +export class LogDebugger { + + private _breakPoints = new Map(); + private _log: string | undefined = undefined; + private _line = 1; + private _logLines = Number.MAX_SAFE_INTEGER; + + public constructor() { + } + + setToLog(log: string, logLines: number): void { + this._log = path.resolve(log); + this._logLines = logLines; + } + + setBreakPoint(source: string, breakpoints: DebugProtocol.SourceBreakpoint[]): DebugProtocol.Breakpoint[] { + const bps = new Array(); + const sourcePath = path.resolve(source); + this._breakPoints.set(sourcePath, bps); + breakpoints.forEach((breakpoint) => { + if (this._line === 1) { + this._line = breakpoint.line; + } + const verified = breakpoint.line > 0 && breakpoint.line < this._logLines; + bps.push({ line: breakpoint.line, verified: verified }); + }); + return bps; + } + + hasBreakpoints(): boolean { + const bps = (this._log && this._breakPoints.get(this._log)) || []; + return bps.length !== 0; + } + + linenum(): number { + return this._line; + } + + stepForward(): void { + this._line = Math.min(this._logLines, this._line + 1); + } + + stepBackward(): void { + this._line = Math.max(1, this._line - 1); + } + + gotoNextBreakpoint(reverse = false): void { + this._line = this.findNextLineToStop(reverse); + } + + private findNextLineToStop(reverse = false): number { + const bps = (this._log && this._breakPoints.get(this._log)) || []; + let bp; + if (reverse) { + bp = bps.findLast((bp) => bp.line !== undefined && this._line > bp.line); + } else { + bp = bps.find((bp) => bp.line !== undefined && this._line < bp.line); + } + + if (bp !== undefined && bp.line !== undefined) { + return bp.line; + } else { + return reverse ? 1 : this._logLines; + } + } +} \ No newline at end of file diff --git a/editors/code/src/test/suite/debugAdapter.test.ts b/editors/code/src/test/suite/debugAdapter.test.ts index 9698b0e..5d6da85 100644 --- a/editors/code/src/test/suite/debugAdapter.test.ts +++ b/editors/code/src/test/suite/debugAdapter.test.ts @@ -2,31 +2,78 @@ import * as assert from 'assert'; import { DebugProtocol } from '@vscode/debugprotocol'; import { DebugSession, BinaryNotFoundError } from '../../debugAdapter'; +import { LogDebugger } from '../../logDebugger'; + +type PatchedSession = DebugSession & { + sendResponse: (response: DebugProtocol.Response) => void; + sendEvent: (...args: unknown[]) => void; + dispose?: () => void; +}; + +interface RequestCapture { + response: R | undefined; + eventCount: number; +} + +/** + * Patches sendResponse and sendEvent on a session, invokes fn, then restores + * the originals. Returns the captured response and number of events sent. + */ +function captureRequest( + session: PatchedSession, + fn: () => void +): RequestCapture { + let response: R | undefined; + let eventCount = 0; + + const originalSendResponse = session.sendResponse.bind(session); + const originalSendEvent = session.sendEvent.bind(session); + session.sendResponse = (resp: DebugProtocol.Response) => { response = resp as R; }; + session.sendEvent = () => { eventCount++; }; + try { + fn(); + } finally { + session.sendResponse = originalSendResponse; + session.sendEvent = originalSendEvent; + } + return { response, eventCount }; +} + +function setPlatformArch(platform: NodeJS.Platform | string, arch: string): void { + Object.defineProperty(process, 'platform', { + value: platform, + configurable: true + }); + Object.defineProperty(process, 'arch', { + value: arch, + configurable: true + }); +} + +function createSession(logDebugger: LogDebugger): DebugSession { + setPlatformArch('darwin', 'arm64'); + return new DebugSession(logDebugger); +} suite('DebugAdapter Test Suite', () => { let debugSession: DebugSession | undefined; + let logDebugger: LogDebugger; let originalPlatform: string; let originalArch: string; setup(() => { originalPlatform = process.platform; originalArch = process.arch; + logDebugger = new LogDebugger(); }); teardown(() => { - Object.defineProperty(process, 'platform', { - value: originalPlatform, - configurable: true - }); - Object.defineProperty(process, 'arch', { - value: originalArch, - configurable: true - }); + setPlatformArch(originalPlatform, originalArch); if (debugSession) { try { - (debugSession as any).dispose?.(); + (debugSession as PatchedSession).dispose?.(); } catch (e) { // Ignore } @@ -36,77 +83,41 @@ suite('DebugAdapter Test Suite', () => { suite('Constructor Tests', () => { test('Should create debug session with correct binary path for darwin-arm64', () => { - Object.defineProperty(process, 'platform', { - value: 'darwin', - configurable: true - }); - Object.defineProperty(process, 'arch', { - value: 'arm64', - configurable: true - }); + setPlatformArch('darwin', 'arm64'); - debugSession = new DebugSession(); + debugSession = new DebugSession(logDebugger); assert.ok(debugSession, 'Debug session should be created'); }); test('Should create debug session with correct binary path for linux-x64', () => { - Object.defineProperty(process, 'platform', { - value: 'linux', - configurable: true - }); - Object.defineProperty(process, 'arch', { - value: 'x64', - configurable: true - }); + setPlatformArch('linux', 'x64'); - debugSession = new DebugSession(); + debugSession = new DebugSession(logDebugger); assert.ok(debugSession, 'Debug session should be created'); }); test('Should create debug session with correct binary path for win32-x64', () => { - Object.defineProperty(process, 'platform', { - value: 'win32', - configurable: true - }); - Object.defineProperty(process, 'arch', { - value: 'x64', - configurable: true - }); + setPlatformArch('win32', 'x64'); - debugSession = new DebugSession(); + debugSession = new DebugSession(logDebugger); assert.ok(debugSession, 'Debug session should be created'); }); test('Should throw BinaryNotFoundError for unsupported platform', () => { - Object.defineProperty(process, 'platform', { - value: 'unsupported', - configurable: true - }); - Object.defineProperty(process, 'arch', { - value: 'unsupported', - configurable: true - }); + setPlatformArch('unsupported', 'unsupported'); assert.throws(() => { - new DebugSession(); + new DebugSession(logDebugger); }, BinaryNotFoundError, 'Should throw BinaryNotFoundError for unsupported platform'); }); }); suite('Initialize Request Tests', () => { setup(() => { - Object.defineProperty(process, 'platform', { - value: 'darwin', - configurable: true - }); - Object.defineProperty(process, 'arch', { - value: 'arm64', - configurable: true - }); - debugSession = new DebugSession(); + debugSession = createSession(logDebugger); }); test('Should handle initialize request correctly', () => { @@ -116,7 +127,6 @@ suite('DebugAdapter Test Suite', () => { adapterID: 'log2src', pathFormat: 'path' }; - const response: DebugProtocol.InitializeResponse = { request_seq: 1, success: true, @@ -126,97 +136,49 @@ suite('DebugAdapter Test Suite', () => { body: {} }; - let capturedResponse: DebugProtocol.InitializeResponse | undefined; - const originalSendResponse = (debugSession as any).sendResponse.bind(debugSession); - (debugSession as any).sendResponse = (resp: any) => { - capturedResponse = resp; - }; - - let eventSent = false; - const originalSendEvent = (debugSession as any).sendEvent.bind(debugSession); - (debugSession as any).sendEvent = () => { - eventSent = true; - }; - - try { - (debugSession as any).initializeRequest(response, args); - - assert.ok(capturedResponse, 'Response should be sent'); - assert.ok(eventSent, 'Event should be sent'); - - assert.ok(capturedResponse!.body, 'Response should have body'); - assert.strictEqual(capturedResponse!.body.supportsStepBack, true); - assert.strictEqual(capturedResponse!.body.supportTerminateDebuggee, true); - } finally { - (debugSession as any).sendResponse = originalSendResponse; - (debugSession as any).sendEvent = originalSendEvent; - } + const session = debugSession as PatchedSession; + const { response: captured, eventCount } = captureRequest( + session, + () => (session as any).initializeRequest(response, args) + ); + + assert.ok(captured, 'Response should be sent'); + assert.ok(captured!.body, 'Response should have body'); + assert.strictEqual(captured!.body.supportsStepBack, true); + assert.strictEqual(captured!.body.supportTerminateDebuggee, true); + assert.strictEqual(eventCount, 1, 'InitializedEvent should be sent'); }); }); suite('Breakpoint Tests', () => { setup(() => { - Object.defineProperty(process, 'platform', { - value: 'darwin', - configurable: true - }); - Object.defineProperty(process, 'arch', { - value: 'arm64', - configurable: true - }); - debugSession = new DebugSession(); + debugSession = createSession(logDebugger); }); test('Should set breakpoints correctly', () => { const sourcePath = '/test/source/file.log'; const args: DebugProtocol.SetBreakpointsArguments = { source: { path: sourcePath }, - breakpoints: [ - { line: 10 }, - { line: 20 }, - { line: 30 } - ] + breakpoints: [{ line: 10 }, { line: 20 }, { line: 30 }] }; - const response: DebugProtocol.SetBreakpointsResponse = { - request_seq: 1, - success: true, - command: 'setBreakpoints', - seq: 1, - type: 'response', - body: { breakpoints: [] } + request_seq: 1, success: true, command: 'setBreakpoints', + seq: 1, type: 'response', body: { breakpoints: [] } }; - let capturedResponse: DebugProtocol.SetBreakpointsResponse | undefined; - const originalSendResponse = (debugSession as any).sendResponse.bind(debugSession); - (debugSession as any).sendResponse = (resp: any) => { - capturedResponse = resp; - }; - - let eventSent = false; - const originalSendEvent = (debugSession as any).sendEvent.bind(debugSession); - (debugSession as any).sendEvent = () => { - eventSent = true; - }; - - try { - (debugSession as any).setBreakPointsRequest(response, args); - - assert.ok(capturedResponse, 'Response should be sent'); - - assert.ok(capturedResponse!.body, 'Response should have body'); - assert.ok(capturedResponse!.body.breakpoints, 'Response should have breakpoints'); - assert.strictEqual(capturedResponse!.body.breakpoints.length, 3, 'Should have 3 breakpoints'); - - capturedResponse!.body.breakpoints.forEach((bp, index) => { - assert.strictEqual(bp.line, args.breakpoints![index].line, `Breakpoint ${index} should have correct line`); - }); - - assert.ok(eventSent, 'Should send stopped event'); - } finally { - (debugSession as any).sendResponse = originalSendResponse; - (debugSession as any).sendEvent = originalSendEvent; - } + const session = debugSession as PatchedSession; + const { response: captured, eventCount } = captureRequest( + session, + () => (session as any).setBreakPointsRequest(response, args) + ); + + assert.ok(captured, 'Response should be sent'); + assert.ok(captured!.body.breakpoints, 'Response should have breakpoints'); + assert.strictEqual(captured!.body.breakpoints.length, 3, 'Should have 3 breakpoints'); + captured!.body.breakpoints.forEach((bp, index) => { + assert.strictEqual(bp.line, args.breakpoints![index].line, `Breakpoint ${index} should have correct line`); + }); + assert.strictEqual(eventCount, 1, 'Should send stopped event'); }); test('Should handle empty breakpoints array', () => { @@ -225,41 +187,21 @@ suite('DebugAdapter Test Suite', () => { source: { path: sourcePath }, breakpoints: [] }; - const response: DebugProtocol.SetBreakpointsResponse = { - request_seq: 1, - success: true, - command: 'setBreakpoints', - seq: 1, - type: 'response', - body: { breakpoints: [] } + request_seq: 1, success: true, command: 'setBreakpoints', + seq: 1, type: 'response', body: { breakpoints: [] } }; - let capturedResponse: DebugProtocol.SetBreakpointsResponse | undefined; - let eventSent = false; + const session = debugSession as PatchedSession; + const { response: captured, eventCount } = captureRequest( + session, + () => (session as any).setBreakPointsRequest(response, args) + ); - const originalSendResponse = (debugSession as any).sendResponse.bind(debugSession); - (debugSession as any).sendResponse = (resp: any) => { - capturedResponse = resp; - }; - - const originalSendEvent = (debugSession as any).sendEvent.bind(debugSession); - (debugSession as any).sendEvent = () => { - eventSent = true; - }; - - try { - (debugSession as any).setBreakPointsRequest(response, args); - - assert.ok(capturedResponse, 'Response should be sent'); - assert.ok(capturedResponse!.body, 'Response should have body'); - assert.ok(capturedResponse!.body.breakpoints, 'Response should have breakpoints array'); - assert.strictEqual(capturedResponse!.body.breakpoints.length, 0, 'Should have no breakpoints'); - assert.strictEqual(eventSent, false, 'Should not send stopped event for empty breakpoints'); - } finally { - (debugSession as any).sendResponse = originalSendResponse; - (debugSession as any).sendEvent = originalSendEvent; - } + assert.ok(captured, 'Response should be sent'); + assert.strictEqual(captured!.body.breakpoints.length, 0, 'Should have no breakpoints'); + assert.strictEqual(eventCount, 0, 'Should not send stopped event for empty breakpoints'); + assert.strictEqual(logDebugger.hasBreakpoints(), false, 'LogDebugger should report no breakpoints'); }); }); -}); \ No newline at end of file +}); diff --git a/editors/code/src/test/suite/logDebugger.test.ts b/editors/code/src/test/suite/logDebugger.test.ts new file mode 100644 index 0000000..e82bc3f --- /dev/null +++ b/editors/code/src/test/suite/logDebugger.test.ts @@ -0,0 +1,101 @@ +import * as assert from 'assert'; +import { LogDebugger } from '../../logDebugger'; + +suite('LogDebugger Test Suite', () => { + let logDebugger: LogDebugger; + + setup(() => { + logDebugger = new LogDebugger(); + }); + + suite('Line navigation', () => { + test('starts at line 1', () => { + assert.strictEqual(logDebugger.linenum(), 1); + }); + + test('stepForward increments line', () => { + logDebugger.setToLog('/fake/log', 100); + logDebugger.stepForward(); + assert.strictEqual(logDebugger.linenum(), 2); + }); + + test('stepForward clamps at logLines', () => { + logDebugger.setToLog('/fake/log', 3); + logDebugger.stepForward(); + logDebugger.stepForward(); + logDebugger.stepForward(); + assert.strictEqual(logDebugger.linenum(), 3); + }); + + test('stepBackward decrements line', () => { + logDebugger.setToLog('/fake/log', 100); + logDebugger.stepForward(); + logDebugger.stepForward(); + logDebugger.stepBackward(); + assert.strictEqual(logDebugger.linenum(), 2); + }); + + test('stepBackward clamps at line 1', () => { + logDebugger.setToLog('/fake/log', 100); + logDebugger.stepBackward(); + assert.strictEqual(logDebugger.linenum(), 1); + }); + }); + + suite('Breakpoints', () => { + const logPath = '/fake/log.log'; + + test('hasBreakpoints is false with no breakpoints set', () => { + logDebugger.setToLog(logPath, 100); + assert.strictEqual(logDebugger.hasBreakpoints(), false); + }); + + test('hasBreakpoints is true after breakpoints set', () => { + logDebugger.setToLog(logPath, 100); + logDebugger.setBreakPoint(logPath, [{ line: 10 }]); + assert.ok(logDebugger.hasBreakpoints()); + }); + + test('hasBreakpoints is false after empty breakpoints set', () => { + logDebugger.setToLog(logPath, 100); + logDebugger.setBreakPoint(logPath, []); + assert.strictEqual(logDebugger.hasBreakpoints(), false); + }); + + test('gotoNextBreakpoint advances to next breakpoint', () => { + logDebugger.setToLog(logPath, 100); + // setBreakPoint moves _line to the first breakpoint (10) when starting at line 1 + logDebugger.setBreakPoint(logPath, [{ line: 10 }, { line: 20 }, { line: 30 }]); + assert.strictEqual(logDebugger.linenum(), 10); + logDebugger.gotoNextBreakpoint(); + assert.strictEqual(logDebugger.linenum(), 20); + logDebugger.gotoNextBreakpoint(); + assert.strictEqual(logDebugger.linenum(), 30); + }); + + test('gotoNextBreakpoint reverse retreats to previous breakpoint', () => { + logDebugger.setToLog(logPath, 100); + // setBreakPoint moves _line to 10; navigate forward to 30, then reverse to 20 + logDebugger.setBreakPoint(logPath, [{ line: 10 }, { line: 20 }, { line: 30 }]); + logDebugger.gotoNextBreakpoint(); // 10 → 20 + logDebugger.gotoNextBreakpoint(); // 20 → 30 + logDebugger.gotoNextBreakpoint(true); // 30 → 20 + assert.strictEqual(logDebugger.linenum(), 20); + }); + + test('gotoNextBreakpoint past last clamps to logLines', () => { + logDebugger.setToLog(logPath, 100); + logDebugger.setBreakPoint(logPath, [{ line: 10 }]); + logDebugger.gotoNextBreakpoint(); + logDebugger.gotoNextBreakpoint(); + assert.strictEqual(logDebugger.linenum(), 100); + }); + + test('gotoNextBreakpoint before first in reverse clamps to line 1', () => { + logDebugger.setToLog(logPath, 100); + logDebugger.setBreakPoint(logPath, [{ line: 10 }]); + logDebugger.gotoNextBreakpoint(true); + assert.strictEqual(logDebugger.linenum(), 1); + }); + }); +}); \ No newline at end of file