Files
vscode/src/vs/workbench/contrib/debug/test/browser/breakpoints.test.ts
T
2020-01-07 16:23:56 +01:00

350 lines
16 KiB
TypeScript

/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { URI as uri } from 'vs/base/common/uri';
import { DebugModel, Breakpoint } from 'vs/workbench/contrib/debug/common/debugModel';
import { DebugSession } from 'vs/workbench/contrib/debug/browser/debugSession';
import { NullOpenerService } from 'vs/platform/opener/common/opener';
import { getExpandedBodySize, getBreakpointMessageAndClassName } from 'vs/workbench/contrib/debug/browser/breakpointsView';
import { dispose } from 'vs/base/common/lifecycle';
import { Range } from 'vs/editor/common/core/range';
import { IBreakpointData, IDebugSessionOptions, IBreakpointUpdateData, State } from 'vs/workbench/contrib/debug/common/debug';
import { TextModel } from 'vs/editor/common/model/textModel';
import { LanguageIdentifier, LanguageId } from 'vs/editor/common/modes';
import { createBreakpointDecorations } from 'vs/workbench/contrib/debug/browser/breakpointEditorContribution';
import { OverviewRulerLane } from 'vs/editor/common/model';
import { MarkdownString } from 'vs/base/common/htmlContent';
function createMockSession(model: DebugModel, name = 'mockSession', options?: IDebugSessionOptions): DebugSession {
return new DebugSession({ resolved: { name, type: 'node', request: 'launch' }, unresolved: undefined }, undefined!, model, options, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, NullOpenerService, undefined!);
}
function addBreakpointsAndCheckEvents(model: DebugModel, uri: uri, data: IBreakpointData[]): void {
let eventCount = 0;
const toDispose = model.onDidChangeBreakpoints(e => {
assert.equal(e?.sessionOnly, false);
assert.equal(e?.changed, undefined);
assert.equal(e?.removed, undefined);
const added = e?.added;
assert.notEqual(added, undefined);
assert.equal(added!.length, data.length);
eventCount++;
dispose(toDispose);
for (let i = 0; i < data.length; i++) {
assert.equal(e!.added![i] instanceof Breakpoint, true);
assert.equal((e!.added![i] as Breakpoint).lineNumber, data[i].lineNumber);
}
});
model.addBreakpoints(uri, data);
assert.equal(eventCount, 1);
}
suite('Debug - Breakpoints', () => {
let model: DebugModel;
setup(() => {
model = new DebugModel([], [], [], [], [], <any>{ isDirty: (e: any) => false });
});
// Breakpoints
test('simple', () => {
const modelUri = uri.file('/myfolder/myfile.js');
addBreakpointsAndCheckEvents(model, modelUri, [{ lineNumber: 5, enabled: true }, { lineNumber: 10, enabled: false }]);
assert.equal(model.areBreakpointsActivated(), true);
assert.equal(model.getBreakpoints().length, 2);
let eventCount = 0;
const toDispose = model.onDidChangeBreakpoints(e => {
eventCount++;
assert.equal(e?.added, undefined);
assert.equal(e?.sessionOnly, false);
assert.equal(e?.removed?.length, 2);
assert.equal(e?.changed, undefined);
dispose(toDispose);
});
model.removeBreakpoints(model.getBreakpoints());
assert.equal(eventCount, 1);
assert.equal(model.getBreakpoints().length, 0);
});
test('toggling', () => {
const modelUri = uri.file('/myfolder/myfile.js');
addBreakpointsAndCheckEvents(model, modelUri, [{ lineNumber: 5, enabled: true }, { lineNumber: 10, enabled: false }]);
addBreakpointsAndCheckEvents(model, modelUri, [{ lineNumber: 12, enabled: true, condition: 'fake condition' }]);
assert.equal(model.getBreakpoints().length, 3);
const bp = model.getBreakpoints().pop();
if (bp) {
model.removeBreakpoints([bp]);
}
assert.equal(model.getBreakpoints().length, 2);
model.setBreakpointsActivated(false);
assert.equal(model.areBreakpointsActivated(), false);
model.setBreakpointsActivated(true);
assert.equal(model.areBreakpointsActivated(), true);
});
test('two files', () => {
const modelUri1 = uri.file('/myfolder/my file first.js');
const modelUri2 = uri.file('/secondfolder/second/second file.js');
addBreakpointsAndCheckEvents(model, modelUri1, [{ lineNumber: 5, enabled: true }, { lineNumber: 10, enabled: false }]);
assert.equal(getExpandedBodySize(model), 44);
addBreakpointsAndCheckEvents(model, modelUri2, [{ lineNumber: 1, enabled: true }, { lineNumber: 2, enabled: true }, { lineNumber: 3, enabled: false }]);
assert.equal(getExpandedBodySize(model), 110);
assert.equal(model.getBreakpoints().length, 5);
assert.equal(model.getBreakpoints({ uri: modelUri1 }).length, 2);
assert.equal(model.getBreakpoints({ uri: modelUri2 }).length, 3);
assert.equal(model.getBreakpoints({ lineNumber: 5 }).length, 1);
assert.equal(model.getBreakpoints({ column: 5 }).length, 0);
const bp = model.getBreakpoints()[0];
const update = new Map<string, IBreakpointUpdateData>();
update.set(bp.getId(), { lineNumber: 100 });
let eventFired = false;
const toDispose = model.onDidChangeBreakpoints(e => {
eventFired = true;
assert.equal(e?.added, undefined);
assert.equal(e?.removed, undefined);
assert.equal(e?.changed?.length, 1);
dispose(toDispose);
});
model.updateBreakpoints(update);
assert.equal(eventFired, true);
assert.equal(bp.lineNumber, 100);
assert.equal(model.getBreakpoints({ enabledOnly: true }).length, 3);
model.enableOrDisableAllBreakpoints(false);
model.getBreakpoints().forEach(bp => {
assert.equal(bp.enabled, false);
});
assert.equal(model.getBreakpoints({ enabledOnly: true }).length, 0);
model.setEnablement(bp, true);
assert.equal(bp.enabled, true);
model.removeBreakpoints(model.getBreakpoints({ uri: modelUri1 }));
assert.equal(getExpandedBodySize(model), 66);
assert.equal(model.getBreakpoints().length, 3);
});
test('conditions', () => {
const modelUri1 = uri.file('/myfolder/my file first.js');
addBreakpointsAndCheckEvents(model, modelUri1, [{ lineNumber: 5, condition: 'i < 5', hitCondition: '17' }, { lineNumber: 10, condition: 'j < 3' }]);
const breakpoints = model.getBreakpoints();
assert.equal(breakpoints[0].condition, 'i < 5');
assert.equal(breakpoints[0].hitCondition, '17');
assert.equal(breakpoints[1].condition, 'j < 3');
assert.equal(!!breakpoints[1].hitCondition, false);
assert.equal(model.getBreakpoints().length, 2);
model.removeBreakpoints(model.getBreakpoints());
assert.equal(model.getBreakpoints().length, 0);
});
test('function breakpoints', () => {
model.addFunctionBreakpoint('foo', '1');
model.addFunctionBreakpoint('bar', '2');
model.renameFunctionBreakpoint('1', 'fooUpdated');
model.renameFunctionBreakpoint('2', 'barUpdated');
const functionBps = model.getFunctionBreakpoints();
assert.equal(functionBps[0].name, 'fooUpdated');
assert.equal(functionBps[1].name, 'barUpdated');
model.removeFunctionBreakpoints();
assert.equal(model.getFunctionBreakpoints().length, 0);
});
test('multiple sessions', () => {
const modelUri = uri.file('/myfolder/myfile.js');
addBreakpointsAndCheckEvents(model, modelUri, [{ lineNumber: 5, enabled: true, condition: 'x > 5' }, { lineNumber: 10, enabled: false }]);
const breakpoints = model.getBreakpoints();
const session = createMockSession(model);
const data = new Map<string, DebugProtocol.Breakpoint>();
assert.equal(breakpoints[0].lineNumber, 5);
assert.equal(breakpoints[1].lineNumber, 10);
data.set(breakpoints[0].getId(), { verified: false, line: 10 });
data.set(breakpoints[1].getId(), { verified: true, line: 50 });
model.setBreakpointSessionData(session.getId(), {}, data);
assert.equal(breakpoints[0].lineNumber, 5);
assert.equal(breakpoints[1].lineNumber, 50);
const session2 = createMockSession(model);
const data2 = new Map<string, DebugProtocol.Breakpoint>();
data2.set(breakpoints[0].getId(), { verified: true, line: 100 });
data2.set(breakpoints[1].getId(), { verified: true, line: 500 });
model.setBreakpointSessionData(session2.getId(), {}, data2);
// Breakpoint is verified only once, show that line
assert.equal(breakpoints[0].lineNumber, 100);
// Breakpoint is verified two times, show the original line
assert.equal(breakpoints[1].lineNumber, 10);
model.setBreakpointSessionData(session.getId(), {}, undefined);
// No more double session verification
assert.equal(breakpoints[0].lineNumber, 100);
assert.equal(breakpoints[1].lineNumber, 500);
assert.equal(breakpoints[0].supported, false);
const data3 = new Map<string, DebugProtocol.Breakpoint>();
data3.set(breakpoints[0].getId(), { verified: true, line: 500 });
model.setBreakpointSessionData(session2.getId(), { supportsConditionalBreakpoints: true }, data2);
assert.equal(breakpoints[0].supported, true);
});
test('exception breakpoints', () => {
let eventCount = 0;
model.onDidChangeBreakpoints(() => eventCount++);
model.setExceptionBreakpoints([{ filter: 'uncaught', label: 'UNCAUGHT', default: true }]);
assert.equal(eventCount, 1);
let exceptionBreakpoints = model.getExceptionBreakpoints();
assert.equal(exceptionBreakpoints.length, 1);
assert.equal(exceptionBreakpoints[0].filter, 'uncaught');
assert.equal(exceptionBreakpoints[0].enabled, true);
model.setExceptionBreakpoints([{ filter: 'uncaught', label: 'UNCAUGHT' }, { filter: 'caught', label: 'CAUGHT' }]);
assert.equal(eventCount, 2);
exceptionBreakpoints = model.getExceptionBreakpoints();
assert.equal(exceptionBreakpoints.length, 2);
assert.equal(exceptionBreakpoints[0].filter, 'uncaught');
assert.equal(exceptionBreakpoints[0].enabled, true);
assert.equal(exceptionBreakpoints[1].filter, 'caught');
assert.equal(exceptionBreakpoints[1].label, 'CAUGHT');
assert.equal(exceptionBreakpoints[1].enabled, false);
});
test('data breakpoints', () => {
let eventCount = 0;
model.onDidChangeBreakpoints(() => eventCount++);
model.addDataBreakpoint('label', 'id', true, ['read']);
model.addDataBreakpoint('second', 'secondId', false, ['readWrite']);
const dataBreakpoints = model.getDataBreakpoints();
assert.equal(dataBreakpoints[0].canPersist, true);
assert.equal(dataBreakpoints[0].dataId, 'id');
assert.equal(dataBreakpoints[1].canPersist, false);
assert.equal(dataBreakpoints[1].description, 'second');
assert.equal(eventCount, 2);
model.removeDataBreakpoints(dataBreakpoints[0].getId());
assert.equal(eventCount, 3);
assert.equal(model.getDataBreakpoints().length, 1);
model.removeDataBreakpoints();
assert.equal(model.getDataBreakpoints().length, 0);
assert.equal(eventCount, 4);
});
test('message and class name', () => {
const modelUri = uri.file('/myfolder/my file first.js');
addBreakpointsAndCheckEvents(model, modelUri, [
{ lineNumber: 5, enabled: true, condition: 'x > 5' },
{ lineNumber: 10, enabled: false },
{ lineNumber: 12, enabled: true, logMessage: 'hello' },
{ lineNumber: 15, enabled: true, hitCondition: '12' },
{ lineNumber: 500, enabled: true },
]);
const breakpoints = model.getBreakpoints();
let result = getBreakpointMessageAndClassName(State.Stopped, true, breakpoints[0]);
assert.equal(result.message, 'Expression: x > 5');
assert.equal(result.className, 'codicon-debug-breakpoint-conditional');
result = getBreakpointMessageAndClassName(State.Stopped, true, breakpoints[1]);
assert.equal(result.message, 'Disabled Breakpoint');
assert.equal(result.className, 'codicon-debug-breakpoint-disabled');
result = getBreakpointMessageAndClassName(State.Stopped, true, breakpoints[2]);
assert.equal(result.message, 'Log Message: hello');
assert.equal(result.className, 'codicon-debug-breakpoint-log');
result = getBreakpointMessageAndClassName(State.Stopped, true, breakpoints[3]);
assert.equal(result.message, 'Hit Count: 12');
assert.equal(result.className, 'codicon-debug-breakpoint-conditional');
result = getBreakpointMessageAndClassName(State.Stopped, true, breakpoints[4]);
assert.equal(result.message, 'Breakpoint');
assert.equal(result.className, 'codicon-debug-breakpoint');
result = getBreakpointMessageAndClassName(State.Stopped, false, breakpoints[2]);
assert.equal(result.message, 'Disabled Logpoint');
assert.equal(result.className, 'codicon-debug-breakpoint-log-disabled');
model.addDataBreakpoint('label', 'id', true, ['read']);
const dataBreakpoints = model.getDataBreakpoints();
result = getBreakpointMessageAndClassName(State.Stopped, true, dataBreakpoints[0]);
assert.equal(result.message, 'Data Breakpoint');
assert.equal(result.className, 'codicon-debug-breakpoint-data');
const functionBreakpoint = model.addFunctionBreakpoint('foo', '1');
result = getBreakpointMessageAndClassName(State.Stopped, true, functionBreakpoint);
assert.equal(result.message, 'Function Breakpoint');
assert.equal(result.className, 'codicon-debug-breakpoint-function');
const data = new Map<string, DebugProtocol.Breakpoint>();
data.set(breakpoints[0].getId(), { verified: false, line: 10 });
data.set(breakpoints[1].getId(), { verified: true, line: 50 });
data.set(breakpoints[2].getId(), { verified: true, line: 50, message: 'world' });
data.set(functionBreakpoint.getId(), { verified: true });
model.setBreakpointSessionData('mocksessionid', { supportsFunctionBreakpoints: false, supportsDataBreakpoints: true, supportsLogPoints: true }, data);
result = getBreakpointMessageAndClassName(State.Stopped, true, breakpoints[0]);
assert.equal(result.message, 'Unverified Breakpoint');
assert.equal(result.className, 'codicon-debug-breakpoint-unverified');
result = getBreakpointMessageAndClassName(State.Stopped, true, functionBreakpoint);
assert.equal(result.message, 'Function breakpoints not supported by this debug type');
assert.equal(result.className, 'codicon-debug-breakpoint-function-unverified');
result = getBreakpointMessageAndClassName(State.Stopped, true, breakpoints[2]);
assert.equal(result.message, 'Log Message: hello, world');
assert.equal(result.className, 'codicon-debug-breakpoint-log');
});
test('decorations', () => {
const modelUri = uri.file('/myfolder/my file first.js');
const languageIdentifier = new LanguageIdentifier('testMode', LanguageId.PlainText);
const textModel = new TextModel(
['this is line one', 'this is line two', ' this is line three it has whitespace at start', 'this is line four', 'this is line five'].join('\n'),
TextModel.DEFAULT_CREATION_OPTIONS,
languageIdentifier
);
addBreakpointsAndCheckEvents(model, modelUri, [
{ lineNumber: 1, enabled: true, condition: 'x > 5' },
{ lineNumber: 2, column: 4, enabled: false },
{ lineNumber: 3, enabled: true, logMessage: 'hello' },
{ lineNumber: 500, enabled: true },
]);
const breakpoints = model.getBreakpoints();
let decorations = createBreakpointDecorations(textModel, breakpoints, State.Running, true, true);
assert.equal(decorations.length, 3); // last breakpoint filtered out since it has a large line number
assert.deepEqual(decorations[0].range, new Range(1, 1, 1, 2));
assert.deepEqual(decorations[1].range, new Range(2, 4, 2, 5));
assert.deepEqual(decorations[2].range, new Range(3, 5, 3, 6));
assert.equal(decorations[0].options.beforeContentClassName, undefined);
assert.equal(decorations[1].options.beforeContentClassName, `debug-breakpoint-placeholder`);
assert.equal(decorations[0].options.overviewRuler?.position, OverviewRulerLane.Left);
const expected = new MarkdownString().appendCodeblock(languageIdentifier.language, 'Expression: x > 5');
assert.deepEqual(decorations[0].options.glyphMarginHoverMessage, expected);
decorations = createBreakpointDecorations(textModel, breakpoints, State.Running, true, false);
assert.equal(decorations[0].options.overviewRuler, null);
});
});