Added hit count breakpoints tests and breakpoints utility

This commit is contained in:
D 2019-04-15 23:18:07 -07:00
Родитель e108005655
Коммит 7390889540
24 изменённых файлов: 2518 добавлений и 1668 удалений

25
.vscode/launch.json поставляемый
Просмотреть файл

@ -61,6 +61,31 @@
"${workspaceFolder}/out/**/*.js"
]
},
{
"type": "node",
"request": "launch",
"name": "debug integration test",
"program": "${workspaceFolder}/node_modules/mocha/bin/_mocha",
"env": {
"MSFT_TEST_DA_PORT": "4712",
},
"args": [
"--require", "source-map-support/register",
"-u", "tdd",
"--timeout", "999999",
"--colors",
"--grep", "Hit count breakpoint = 3 pauses on the button's 3rd click",
"--reporter", "node_modules/vscode-chrome-debug-core-testsupport/out/loggingReporter.js",
"${workspaceFolder}/out/test/int/**/*.test.js",
],
"skipFiles": [
"<node_internals>/**",
"methodCalledLogger.ts"
],
"outFiles": [
"${workspaceFolder}/out/**/*.js"
]
},
{
"name": "Run Extension",
"type": "extensionHost",

3186
package-lock.json сгенерированный

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -31,13 +31,16 @@
},
"devDependencies": {
"@types/chai": "^4.1.7",
"@types/mocha": "^2.2.35",
"@types/chai-string": "^1.4.1",
"@types/lodash": "^4.14.120",
"@types/mocha": "^2.2.48",
"@types/mockery": "^1.4.29",
"@types/node": "^8.0.58",
"@types/node": "^8.10.39",
"@types/puppeteer": "^1.12.3",
"@types/source-map": "^0.1.27",
"@types/tmp": "0.0.32",
"chai": "^4.2.0",
"chai-string": "^1.5.0",
"concurrently": "^3.1.0",
"del": "^3.0.0",
"event-stream": "^3.3.4",
@ -48,11 +51,13 @@
"gulp-typescript": "^4.0.1",
"gulp-util": "^3.0.8",
"http-server": "^0.10.0",
"lodash": "^4.17.11",
"minimist": "^1.2.0",
"mocha": "^5.2.0",
"mocha-multi-reporters": "^1.1.7",
"mockery": "^1.7.0",
"puppeteer": "^1.13.0",
"source-map-support": "^0.5.11",
"tmp": "0.0.31",
"ts-loader": "^1.0.0",
"tslint": "^5.7.0",
@ -79,6 +84,7 @@
"test": "mocha --exit --timeout 20000 -s 2000 -u tdd --colors \"./out/test/*.test.js\"",
"intTest": "mocha --exit --timeout 20000 -s 3500 -u tdd --colors --reporter node_modules/vscode-chrome-debug-core-testsupport/out/loggingReporter.js ./out/test/int/*.test.js",
"frameworkTest": "mocha --exit --timeout 20000 -s 3500 -u tdd --colors --reporter mocha-multi-reporters --reporter-options configFile=test/int/testConfig.json ./out/test/int/framework/*.test.js",
"allIntTest": "mocha --require source-map-support/register --exit --timeout 20000 -s 3500 -u tdd --colors --reporter node_modules/vscode-chrome-debug-core-testsupport/out/loggingReporter.js ./out/test/int/**/*.test.js",
"lint": "gulp tslint",
"vscode:prepublish": "gulp verify-no-linked-modules",
"postinstall": "node ./node_modules/vscode/bin/install",

Просмотреть файл

@ -9,8 +9,9 @@ import { createServer } from 'http-server';
import * as ts from 'vscode-chrome-debug-core-testsupport';
import * as testSetup from './testSetup';
import { BreakOnLoadStrategy } from 'vscode-chrome-debug-core';
function runCommonTests(breakOnLoadStrategy: string) {
function runCommonTests(breakOnLoadStrategy: BreakOnLoadStrategy) {
const DATA_ROOT = testSetup.DATA_ROOT;
let dc: ts.ExtendedDebugClient;

Просмотреть файл

@ -0,0 +1,160 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/*
* Hit count breakpoints' scenarios
* Hit count breakpoint syntax: (>|>=|=|<|<=|%)?\s*([0-9]+)
*/
import * as _ from 'lodash';
import { puppeteerSuite, puppeteerTest } from '../puppeteer/puppeteerSuite';
import { reactTestSpecification } from '../resources/resourceProjects';
import { BreakpointsWizard as BreakpointsWizard } from '../wizards/breakpoints/breakpointsWizard';
import { asyncRepeatSerially } from '../utils/repeat';
puppeteerSuite('Hit count breakpoints on a React project', reactTestSpecification, (suiteContext) => {
suite('Some basic tests', () => {
const reactCounterAppBaseStack = `
ca [react-dom.production.min.js] Line 49:1
ja [react-dom.production.min.js] Line 69:1
ka [react-dom.production.min.js] Line 73:1
wa [react-dom.production.min.js] Line 140:1
Aa [react-dom.production.min.js] Line 169:6
ya [react-dom.production.min.js] Line 158:1
Da [react-dom.production.min.js] Line 232:1
Ad [react-dom.production.min.js] Line 1718:1
Gi [react-dom.production.min.js] Line 5990:1
Kb [react-dom.production.min.js] Line 660:1
Dd [react-dom.production.min.js] Line 1760:1
(anonymous function) [react-dom.production.min.js] Line 6017:1
push../node_modules/scheduler/cjs/scheduler.production.min.js.exports.unstable_runWithPriority [scheduler.production.min.js] Line 274:1
Ii [react-dom.production.min.js] Line 6016:1
Cd [react-dom.production.min.js] Line 1737:1`;
puppeteerTest("Hit count breakpoint = 3 pauses on the button's 3rd click", suiteContext, async (_context, page) => {
const incBtn = await page.waitForSelector('#incrementBtn');
const breakpoints = BreakpointsWizard.create(suiteContext.debugClient, reactTestSpecification);
const counterBreakpoints = breakpoints.at('Counter.jsx');
const setStateBreakpoint = await counterBreakpoints.hitCountBreakpoint({
lineText: 'this.setState({ count: newval });',
hitCountCondition: '% 3'
});
await asyncRepeatSerially(2, () => incBtn.click());
await setStateBreakpoint.assertIsHitThenResumeWhen(() => incBtn.click(), `
increment [Counter.jsx] Line 17:12
onClick [Counter.jsx] Line 30:60
${reactCounterAppBaseStack}`);
await incBtn.click();
await breakpoints.assertNotPaused();
await setStateBreakpoint.unset();
});
puppeteerTest("Hit count breakpoints = 3, = 4 and = 5 pause on the button's 3rd, 4th and 5th clicks", suiteContext, async (_context, page) => {
const incBtn = await page.waitForSelector('#incrementBtn');
const breakpoints = BreakpointsWizard.create(suiteContext.debugClient, reactTestSpecification);
const counterBreakpoints = breakpoints.at('Counter.jsx');
const setStateBreakpoint = await counterBreakpoints.hitCountBreakpoint({
lineText: 'this.setState({ count: newval });',
hitCountCondition: '= 3'
});
const setNewValBreakpoint = await counterBreakpoints.hitCountBreakpoint({
lineText: 'const newval = this.state.count + 1',
hitCountCondition: '= 5'
});
const stepInBreakpoint = await counterBreakpoints.hitCountBreakpoint({
lineText: 'this.stepIn();',
hitCountCondition: '= 4'
});
await asyncRepeatSerially(2, () => incBtn.click());
await setStateBreakpoint.assertIsHitThenResumeWhen(() => incBtn.click(), `
increment [Counter.jsx] Line 17:12
onClick [Counter.jsx] Line 30:60
${reactCounterAppBaseStack}`);
await stepInBreakpoint.assertIsHitThenResumeWhen(() => incBtn.click(), `
increment [Counter.jsx] Line 18:12
onClick [Counter.jsx] Line 30:60
${reactCounterAppBaseStack}`);
await setNewValBreakpoint.assertIsHitThenResumeWhen(() => incBtn.click(), `
increment [Counter.jsx] Line 16:27
onClick [Counter.jsx] Line 30:60
${reactCounterAppBaseStack}`);
await incBtn.click();
await breakpoints.assertNotPaused();
await setStateBreakpoint.unset();
await setNewValBreakpoint.unset();
await stepInBreakpoint.unset();
});
puppeteerTest("Hit count breakpoints = 3, = 4 and = 5 set in batch pause on the button's 3rd, 4th and 5th clicks", suiteContext, async (_context, page) => {
const incBtn = await page.waitForSelector('#incrementBtn');
const breakpoints = BreakpointsWizard.create(suiteContext.debugClient, reactTestSpecification);
const counterBreakpoints = breakpoints.at('Counter.jsx');
const { setStateBreakpoint, stepInBreakpoint, setNewValBreakpoint } = await counterBreakpoints.batch(async () => ({
setStateBreakpoint: await counterBreakpoints.hitCountBreakpoint({
lineText: 'this.setState({ count: newval });',
hitCountCondition: '= 3'
}),
setNewValBreakpoint: await counterBreakpoints.hitCountBreakpoint({
lineText: 'const newval = this.state.count + 1',
hitCountCondition: '= 5'
}),
stepInBreakpoint: await counterBreakpoints.hitCountBreakpoint({
lineText: 'this.stepIn();',
hitCountCondition: '= 4'
})
}));
await asyncRepeatSerially(2, () => incBtn.click());
await setStateBreakpoint.assertIsHitThenResumeWhen(() => incBtn.click(), `
increment [Counter.jsx] Line 17:12
onClick [Counter.jsx] Line 30:60
${reactCounterAppBaseStack}`);
await stepInBreakpoint.assertIsHitThenResumeWhen(() => incBtn.click(), `
increment [Counter.jsx] Line 18:12
onClick [Counter.jsx] Line 30:60
${reactCounterAppBaseStack}`);
await setNewValBreakpoint.assertIsHitThenResumeWhen(() => incBtn.click(), `
increment [Counter.jsx] Line 16:27
onClick [Counter.jsx] Line 30:60
${reactCounterAppBaseStack}`);
await incBtn.click();
await breakpoints.assertNotPaused();
await counterBreakpoints.batch(async () => {
await setStateBreakpoint.unset();
await setNewValBreakpoint.unset();
await stepInBreakpoint.unset();
});
});
});
});

Просмотреть файл

@ -32,7 +32,7 @@ puppeteerSuite('React Framework Tests', TEST_SPEC, (suiteContext) => {
suite('React specific tests', () => {
puppeteerTest('Should hit breakpoint in .jsx file', suiteContext, async (context, page) => {
puppeteerTest('Should hit breakpoint in .jsx file', suiteContext, async (_context, page) => {
const location = suiteContext.breakpointLabels.get('react_Counter_increment');
const incBtn = await page.waitForSelector('#incrementBtn');
@ -43,7 +43,7 @@ puppeteerSuite('React Framework Tests', TEST_SPEC, (suiteContext) => {
await clicked;
});
puppeteerTest('Should hit conditional breakpoint in .jsx file', suiteContext, async (context, page) => {
puppeteerTest('Should hit conditional breakpoint in .jsx file', suiteContext, async (_context, page) => {
const location = suiteContext.breakpointLabels.get('react_Counter_increment');
const incBtn = await page.waitForSelector('#incrementBtn');

Просмотреть файл

@ -1,5 +1,5 @@
import { BreakpointLocation } from '../intTestSupport';
import { loadProjectLabels, loadLabelsFromFile } from '../labels';
import { loadProjectLabels } from '../labels';
import { expect } from 'chai';
suite('Test framework tests', () => {

Просмотреть файл

@ -59,7 +59,7 @@ export class FrameworkTestSuite {
*/
testStepIn(bpLabelStop: string, bpLabelStepIn: string) {
return puppeteerTest(`${this.frameworkName} - Should step in correctly`, this.suiteContext,
async (context, page) => {
async (_context, page) => {
const location = this.suiteContext.breakpointLabels.get(bpLabelStop);
const stepInLocation = this.suiteContext.breakpointLabels.get(bpLabelStepIn);
@ -83,7 +83,7 @@ export class FrameworkTestSuite {
*/
testStepOver(bpLabel: string) {
return puppeteerTest(`${this.frameworkName} - Should step over correctly`, this.suiteContext,
async (context, page) => {
async (_context, page) => {
const location = this.suiteContext.breakpointLabels.get(bpLabel);
const incBtn = await page.waitForSelector('#incrementBtn');
@ -105,7 +105,7 @@ export class FrameworkTestSuite {
*/
testStepOut(bpLabelStop: string, bpLabelStepOut: string) {
return puppeteerTest(`${this.frameworkName} - Should step out correctly`, this.suiteContext,
async (context, page) => {
async (_context, page) => {
const location = this.suiteContext.breakpointLabels.get(bpLabelStop);
const stepOutLocation = this.suiteContext.breakpointLabels.get(bpLabelStepOut);
@ -126,7 +126,7 @@ export class FrameworkTestSuite {
* @param bpLocation
*/
testPauseExecution() {
return puppeteerTest(`${this.frameworkName} - Should correctly pause execution on a pause request`, this.suiteContext, async (context, page) => {
return puppeteerTest(`${this.frameworkName} - Should correctly pause execution on a pause request`, this.suiteContext, async (context, _page) => {
const debugClient = context.debugClient;
await debugClient.pauseRequest({ threadId: 0 });
await debugClient.waitForEvent('stopped');

Просмотреть файл

@ -6,6 +6,7 @@
import * as path from 'path';
import { ExtendedDebugClient } from 'vscode-chrome-debug-core-testsupport';
import { BreakpointLocation } from '../intTestSupport';
import { ILaunchRequestArgs } from '../../../src/chromeDebugInterfaces';
/*
* A collection of supporting classes/functions for running framework tests
@ -21,7 +22,7 @@ export interface ProjectSpecProps {
/** The outfiles directory for the test project */
outFiles?: string;
/** The default launch configuration for the test project */
launchConfig?: any;
launchConfig?: ILaunchRequestArgs;
/** Port to use for the server */
port?: number;
/** Url to use for the project */
@ -49,7 +50,7 @@ export class TestProjectSpec {
this._props.url = props.url || `http://localhost:${props.port}/`;
this._props.launchConfig = props.launchConfig || {
url: props.url,
outFiles: props.outFiles,
outFiles: [props.outFiles],
sourceMaps: true,
/* TODO: get this dynamically */
runtimeExecutable: 'node_modules/puppeteer/.local-chromium/win64-637110/chrome-win/chrome.exe',

Просмотреть файл

@ -10,15 +10,16 @@ import { launchTestAdapter } from '../intTestSupport';
import { getPageByUrl, connectPuppeteer } from './puppeteerSupport';
import { FrameworkTestContext, TestProjectSpec } from '../framework/frameworkTestSupport';
import { loadProjectLabels } from '../labels';
import { isThisV2, isThisV1 } from '../testSetup';
/**
* Extends the normal debug adapter context to include context relevant to puppeteer tests.
*/
export interface PuppeteerTestContext extends FrameworkTestContext {
/** The connected puppeteer browser object */
browser: puppeteer.Browser;
/** The currently running html page in Chrome */
page: puppeteer.Page;
/** The connected puppeteer browser object */
browser: puppeteer.Browser;
/** The currently running html page in Chrome */
page: puppeteer.Page;
}
/**
@ -30,27 +31,29 @@ export interface PuppeteerTestContext extends FrameworkTestContext {
* @param testFunction The inner test function that will run a test using puppeteer
*/
export async function puppeteerTest(
description: string,
context: FrameworkTestContext,
testFunction: (context: PuppeteerTestContext, page: puppeteer.Page) => Promise<any>
) {
return test(description, async () => {
const debugClient = await context.debugClient;
await launchTestAdapter(debugClient, context.testSpec.props.launchConfig);
const browser = await connectPuppeteer(9222);
const page = await getPageByUrl(browser, context.testSpec.props.url);
description: string,
context: FrameworkTestContext,
testFunction: (context: PuppeteerTestContext, page: puppeteer.Page) => Promise<any>
) {
return test(description, async function () {
const debugClient = await context.debugClient;
await launchTestAdapter(debugClient, context.testSpec.props.launchConfig);
const browser = await connectPuppeteer(9222);
const page = await getPageByUrl(browser, context.testSpec.props.url);
// This short wait appears to be necessary to completely avoid a race condition in V1 (tried several other
// strategies to wait deterministically for all scripts to be loaded and parsed, but have been unsuccessful so far)
// If we don't wait here, there's always a possibility that we can send the set breakpoint request
// for a subsequent test after the scripts have started being parsed/run by Chrome, yet before
// the target script is parsed, in which case the adapter will try to use breakOnLoad, but
// the instrumentation BP will never be hit, leaving our breakpoint in limbo
// This short wait appears to be necessary to completely avoid a race condition in V1 (tried several other
// strategies to wait deterministically for all scripts to be loaded and parsed, but have been unsuccessful so far)
// If we don't wait here, there's always a possibility that we can send the set breakpoint request
// for a subsequent test after the scripts have started being parsed/run by Chrome, yet before
// the target script is parsed, in which case the adapter will try to use breakOnLoad, but
// the instrumentation BP will never be hit, leaving our breakpoint in limbo
if (isThisV1) {
await new Promise(a => setTimeout(a, 500));
}
await testFunction({ ...context, browser, page}, page);
});
}
await testFunction({ ...context, browser, page }, page);
});
}
/**
* Defines a custom test suite which will:
@ -75,11 +78,18 @@ export function puppeteerSuite(
let server: any;
setup(async () => {
suiteContext.debugClient = await testSetup.setup();
const portNumber = process.env['MSFT_TEST_DA_PORT']
? parseInt(process.env['MSFT_TEST_DA_PORT'], 10)
: undefined;
let debugClient = await testSetup.setup(portNumber);
suiteContext.debugClient = debugClient;
await suiteContext.debugClient;
// Running tests on CI can time out at the default 5s, so we up this to 10s
suiteContext.breakpointLabels = await loadProjectLabels(testSpec.props.webRoot);
suiteContext.debugClient.defaultTimeout = 15000;
server = createServer({ root: testSpec.props.webRoot });
server.listen(7890);
});

Просмотреть файл

@ -53,7 +53,7 @@ export async function getPageByUrl(browser: puppeteer.Browser, url: string, time
}
// TODO: yuck, clean up
await new Promise((a, r) => setTimeout(() => a(), timeout / 10));
await new Promise((a, _r) => setTimeout(() => a(), timeout / 10));
current = new Date().getTime();
}
throw `Page with url: ${url} could not be found within ${timeout}ms`;

Просмотреть файл

@ -0,0 +1,7 @@
import * as path from 'path';
import * as testSetup from '../testSetup';
import { TestProjectSpec } from '../framework/frameworkTestSupport';
const DATA_ROOT = testSetup.DATA_ROOT;
const REACT_PROJECT_ROOT = path.join(DATA_ROOT, 'react', 'dist');
export const reactTestSpecification = new TestProjectSpec({ projectRoot: REACT_PROJECT_ROOT });

Просмотреть файл

@ -3,20 +3,22 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as os from 'os';
import * as path from 'path';
import * as tmp from 'tmp';
import * as ts from 'vscode-chrome-debug-core-testsupport';
import { DebugProtocol } from 'vscode-debugprotocol';
import { DebugClient } from 'vscode-debugadapter-testsupport';
import { ILaunchRequestArgs } from '../../src/chromeDebugInterfaces';
const DEBUG_ADAPTER = './out/src/chromeDebug.js';
let testLaunchProps: any;
let testLaunchProps: ILaunchRequestArgs;
function formLaunchArgs(launchArgs: any): void {
export const isThisV2 = false;
export const isThisV1 = !isThisV2;
function formLaunchArgs(launchArgs: ILaunchRequestArgs): void {
launchArgs.trace = 'verbose';
launchArgs.logTimestamps = true;
launchArgs.disableNetworkCache = true;
// Start with a clean userDataDir for each test run
@ -30,7 +32,7 @@ function formLaunchArgs(launchArgs: any): void {
}
}
function patchLaunchArgs(launchArgs: any): void {
function patchLaunchArgs(launchArgs: ILaunchRequestArgs): void {
formLaunchArgs(launchArgs);
}
@ -38,7 +40,7 @@ export const lowercaseDriveLetterDirname = __dirname.charAt(0).toLowerCase() + _
export const PROJECT_ROOT = path.join(lowercaseDriveLetterDirname, '../../../');
export const DATA_ROOT = path.join(PROJECT_ROOT, 'testdata/');
export function setup(port?: number, launchProps?: any) {
export function setup(port?: number, launchProps?: ILaunchRequestArgs) {
if (launchProps) {
testLaunchProps = launchProps;
}

Просмотреть файл

@ -0,0 +1,20 @@
import * as _ from 'lodash';
import { createColumnNumber, createLineNumber } from '../core-v2/chrome/internal/locations/subtypes';
import { utils } from 'vscode-chrome-debug-core';
import { Position } from '../core-v2/chrome/internal/locations/location';
export async function findPositionOfTextInFile(filePath: string, text: string): Promise<Position> {
const contentsIncludingCarriageReturns = await utils.readFileP(filePath, 'utf8');
const contents = contentsIncludingCarriageReturns.replace(/\r/g, '');
const textStartIndex = contents.indexOf(text);
if (textStartIndex >= 0) {
const contentsBeforeText = contents.substr(0, textStartIndex);
const textLineNumber = createLineNumber(_.countBy(contentsBeforeText, c => c === '\n')['true'] || 0);
const lastNewLineBeforeTextIndex = contents.lastIndexOf('\n', textStartIndex);
const textColumNumber = createColumnNumber(textStartIndex - (lastNewLineBeforeTextIndex + 1));
return new Position(textLineNumber, textColumNumber);
} else {
throw new Error(`Couldn't find ${text} in ${filePath}`);
}
}

5
test/int/utils/repeat.ts Normal file
Просмотреть файл

@ -0,0 +1,5 @@
export async function asyncRepeatSerially(howManyTimes: number, action: () => Promise<void>): Promise<void> {
for (let index = 0; index < howManyTimes; ++index) {
await action();
}
}

Просмотреть файл

@ -0,0 +1,13 @@
import { utils } from 'vscode-chrome-debug-core';
export async function waitUntilReadyWithTimeout(isReady: () => boolean, maxWaitTimeInMilliseconds = 5000) {
const maximumDateTimeToWaitUntil = Date.now() + maxWaitTimeInMilliseconds;
while (!isReady() && Date.now() < maximumDateTimeToWaitUntil) {
await utils.promiseTimeout(null, 50);
}
if (!isReady()) {
throw new Error(`Timed-out after waiting for condition to be ready for ${maxWaitTimeInMilliseconds}ms`);
}
}

Просмотреть файл

@ -0,0 +1,123 @@
import { Position } from '../../core-v2/chrome/internal/locations/location';
import { IBPActionWhenHit } from '../../core-v2/chrome/internal/breakpoints/bpActionWhenHit';
import { InternalFileBreakpointsWizard } from './implementation/internalFileBreakpointsWizard';
import { RemoveProperty } from '../../core-v2/typeUtils';
import { DebugProtocol } from 'vscode-debugprotocol';
export class BreakpointWizard {
private _state: IBreakpointSetOrUnsetState = new BreakpointUnsetState(this, this._internal, this.changeStateFunction());
public constructor(
private readonly _internal: InternalFileBreakpointsWizard, public readonly position: Position,
public readonly actionWhenHit: IBPActionWhenHit, public readonly name: string) { }
public async setThenWaitForVerifiedThenValidate(): Promise<BreakpointWizard> {
await this.setWithoutVerifying();
await this.waitUntilVerified();
this.assertIsVerified();
return this;
}
public async waitUntilVerified(): Promise<BreakpointWizard> {
await this._state.waitUntilVerified();
return this;
}
public async setWithoutVerifying(): Promise<BreakpointWizard> {
await this._state.set();
return this;
}
public async unset(): Promise<BreakpointWizard> {
await this._state.unset();
return this;
}
public async assertIsHitThenResumeWhen(lastActionToMakeBreakpointHit: () => Promise<void>, expectedStackTrace: string): Promise<BreakpointWizard> {
await this._state.assertIsHitThenResumeWhen(lastActionToMakeBreakpointHit, expectedStackTrace);
return this;
}
public assertIsVerified(): this {
this._state.assertIsVerified();
return this;
}
private changeStateFunction(): ChangeBreakpointWizardState {
return newState => this._state = newState;
}
public toString(): string {
return this.name;
}
}
export type VSCodeActionWhenHit = RemoveProperty<DebugProtocol.SourceBreakpoint, 'line' | 'column'>;
export type ChangeBreakpointWizardState = (newState: IBreakpointSetOrUnsetState) => void;
export interface IBreakpointSetOrUnsetState {
set(): Promise<void>;
unset(): Promise<void>;
assertIsHitThenResumeWhen(lastActionToMakeBreakpointHit: () => Promise<void>, expectedStackTrace: string): Promise<void>;
assertIsVerified(): void;
waitUntilVerified(): Promise<void>;
}
class BreakpointSetState implements IBreakpointSetOrUnsetState {
public constructor(
private readonly _breakpoint: BreakpointWizard,
private readonly _internal: InternalFileBreakpointsWizard,
private readonly _changeState: ChangeBreakpointWizardState) {
}
public set(): Promise<void> {
throw new Error(`Can't set a breakpoint that is already set`);
}
public async unset(): Promise<void> {
await this._internal.unset(this._breakpoint);
this._changeState(new BreakpointUnsetState(this._breakpoint, this._internal, this._changeState));
}
public assertIsHitThenResumeWhen(lastActionToMakeBreakpointHit: () => Promise<void>, expectedStackTrace: string): Promise<void> {
return this._internal.assertIsHitThenResumeWhen(this._breakpoint, lastActionToMakeBreakpointHit, expectedStackTrace);
}
public assertIsVerified(): void {
this._internal.assertIsVerified(this._breakpoint);
}
public async waitUntilVerified(): Promise<void> {
await this._internal.waitUntilVerified(this._breakpoint);
}
}
export class BreakpointUnsetState implements IBreakpointSetOrUnsetState {
public constructor(
private readonly _breakpoint: BreakpointWizard,
private readonly _internal: InternalFileBreakpointsWizard,
private readonly _changeState: ChangeBreakpointWizardState) {
}
public async set(): Promise<void> {
await this._internal.set(this._breakpoint);
this._changeState(new BreakpointSetState(this._breakpoint, this._internal, this._changeState));
}
public unset(): Promise<void> {
throw new Error(`Can't unset a breakpoint that is already unset`);
}
public assertIsHitThenResumeWhen(_lastActionToMakeBreakpointHit: () => Promise<void>, _expectedStackTrace: string): Promise<void> {
throw new Error(`Can't expect to hit a breakpoint that is unset`);
}
public assertIsVerified(): never {
throw new Error(`Can't expect an unset breakpoint to be verified`);
}
public async waitUntilVerified(): Promise<void> {
throw new Error(`Can't expect an unset breakpoint to ever become verified`);
}
}

Просмотреть файл

@ -0,0 +1,143 @@
import { ExtendedDebugClient } from 'vscode-chrome-debug-core-testsupport';
import { TestProjectSpec } from '../../framework/frameworkTestSupport';
import { InternalFileBreakpointsWizard } from './implementation/internalFileBreakpointsWizard';
import { DebugProtocol } from 'vscode-debugprotocol';
import { ValidatedMap } from '../../core-v2/chrome/collections/validatedMap';
import { wrapWithMethodLogger } from '../../core-v2/chrome/logging/methodsCalledLogger';
import assert = require('assert');
import { FileBreakpointsWizard } from './fileBreakpointsWizard';
import { waitUntilReadyWithTimeout } from '../../utils/waitUntilReadyWithTimeout';
import { isThisV2, isThisV1 } from '../../testSetup';
import { expect } from 'chai';
export class BreakpointsWizard {
private _state: IEventForConsumptionAvailabilityState = new NoEventAvailableToBeConsumed(this.changeStateFunction());
private readonly _pathToFileWizard = new ValidatedMap<string, InternalFileBreakpointsWizard>();
private constructor(private readonly _client: ExtendedDebugClient, private readonly _project: TestProjectSpec) {
this._client.on('stopped', stopped => this._state.onPaused(stopped));
this._client.on('continued', continued => this._state.onResumed(continued));
this._client.on('breakpoint', breakpointStatusChange => this.onBreakpointStatusChange(breakpointStatusChange));
}
public static create(debugClient: ExtendedDebugClient, testProjectSpecification: TestProjectSpec): BreakpointsWizard {
return wrapWithMethodLogger(new this(debugClient, testProjectSpecification));
}
public at(filePath: string): FileBreakpointsWizard {
return wrapWithMethodLogger(new FileBreakpointsWizard(this._pathToFileWizard.getOrAdd(filePath,
() => new InternalFileBreakpointsWizard(this, this._client, this._project.src(filePath)))));
}
public async assertNotPaused(): Promise<void> {
await this._state.assertNotPaused();
}
public assertIsPaused(): void {
this._state.assertIsPaused();
}
public async waitUntilJustResumed(): Promise<void> {
await waitUntilReadyWithTimeout(() => this._state instanceof EventAvailableToBeConsumed);
await this._state.waitUntilJustResumed();
}
public toString(): string {
return 'Breakpoints';
}
private onBreakpointStatusChange(breakpointStatusChanged: DebugProtocol.BreakpointEvent): void {
for (const fileWizard of this._pathToFileWizard.values()) {
fileWizard.onBreakpointStatusChange(breakpointStatusChanged);
}
}
private changeStateFunction(): (newState: IEventForConsumptionAvailabilityState) => void {
return newState => this._state = newState;
}
}
interface IEventForConsumptionAvailabilityState {
readonly latestEvent: DebugProtocol.StoppedEvent | DebugProtocol.ContinuedEvent;
onPaused(stopped: DebugProtocol.StoppedEvent): void;
onResumed(continued: DebugProtocol.ContinuedEvent): void;
waitUntilJustResumed(): Promise<void>;
assertIsPaused(): void;
assertNotPaused(): Promise<void>;
}
type ChangeState = (newState: IEventForConsumptionAvailabilityState) => void;
class EventAvailableToBeConsumed implements IEventForConsumptionAvailabilityState {
public constructor(private readonly _changeState: ChangeState, public readonly latestEvent: DebugProtocol.StoppedEvent | DebugProtocol.ContinuedEvent) { }
public onPaused(stopped: DebugProtocol.StoppedEvent): void {
if (isThisV1 && this.latestEvent.event === 'continued') {
this._changeState(new EventAvailableToBeConsumed(this._changeState, stopped));
} else {
throw new Error(`Expected to consume previous event: ${JSON.stringify(this.latestEvent)} before receiving a new stopped event: ${JSON.stringify(stopped)}`);
}
}
public onResumed(continued: DebugProtocol.ContinuedEvent): void {
if (isThisV2) {
throw new Error(`Expected to consume previous event: ${JSON.stringify(this.latestEvent)} before receiving a new continued event: ${JSON.stringify(continued)}`);
}
}
public async waitUntilJustResumed(): Promise<void> {
if (this.latestEvent.event === 'continued') {
this._changeState(new NoEventAvailableToBeConsumed(this._changeState));
}
}
public assertIsPaused(): void {
if (this.latestEvent.event === 'stopped') {
this._changeState(new NoEventAvailableToBeConsumed(this._changeState));
}
}
public async assertNotPaused(): Promise<void> {
expect(this.latestEvent.event, `Expected that there was not new paused event to be consumed, and that the debugger wasn't paused yet the state was: ${this}`)
.to.not.equal('stopped');
}
public toString(): string {
return `Event available to be consumed: ${JSON.stringify(this.latestEvent)}`;
}
}
class NoEventAvailableToBeConsumed implements IEventForConsumptionAvailabilityState {
public constructor(private readonly _changeState: ChangeState) { }
public get latestEvent(): never {
throw new Error(`There is no event available to be consumed`);
}
public onPaused(stopped: DebugProtocol.StoppedEvent): void {
this._changeState(new EventAvailableToBeConsumed(this._changeState, stopped));
}
public onResumed(continued: DebugProtocol.ContinuedEvent): void {
this._changeState(new EventAvailableToBeConsumed(this._changeState, continued));
}
public waitUntilJustResumed(): Promise<void> {
throw new Error(`There is no event available to be consumed`);
}
public assertIsPaused(): void {
throw new Error(`There is no event available to be consumed`);
}
public async assertNotPaused(): Promise<void> {
// Always true for this state
}
public toString(): string {
return `NoEventAvailableToBeConsumed`;
}
}

Просмотреть файл

@ -0,0 +1,36 @@
import { BreakpointWizard } from './breakpointWizard';
import { InternalFileBreakpointsWizard } from './implementation/internalFileBreakpointsWizard';
import { PromiseOrNot } from 'vscode-chrome-debug-core';
import { wrapWithMethodLogger } from '../../core-v2/chrome/logging/methodsCalledLogger';
export interface IBreakpointOptions {
lineText: string;
}
export interface IHitCountBreakpointOptions extends IBreakpointOptions {
hitCountCondition: string;
}
export class FileBreakpointsWizard {
public constructor(private readonly _internal: InternalFileBreakpointsWizard) { }
public async hitCountBreakpoint(options: IHitCountBreakpointOptions): Promise<BreakpointWizard> {
return (await (await this.unsetHitCountBreakpoint(options)).setThenWaitForVerifiedThenValidate());
}
public async unsetHitCountBreakpoint(options: IHitCountBreakpointOptions): Promise<BreakpointWizard> {
return wrapWithMethodLogger(await this._internal.hitCountBreakpoint({
lineText: options.lineText,
hitCountCondition: options.hitCountCondition,
name: `BP @ ${options.lineText}`
}));
}
public batch<T>(batchAction: (fileBreakpointsWizard: FileBreakpointsWizard) => PromiseOrNot<T>): Promise<T> {
return this._internal.batch(batchAction);
}
public toString(): string {
return `Breakpoints at ${this._internal.filePath}`;
}
}

Просмотреть файл

@ -0,0 +1,58 @@
import { DebugProtocol } from 'vscode-debugprotocol';
import { BreakpointWizard } from '../breakpointWizard';
import { ValidatedMap } from '../../../core-v2/chrome/collections/validatedMap';
import { IPerformChangesImmediatelyOrBatchState, InternalFileBreakpointsWizard, CurrentBreakpointsMapping, BreakpointsUpdate } from './internalFileBreakpointsWizard';
import { BreakpointsAssertions } from './breakpointsAssertions';
import { BreakpointsWizard } from '../breakpointsWizard';
export class PerformChangesImmediatelyState implements IPerformChangesImmediatelyOrBatchState {
private readonly _idToBreakpoint = new ValidatedMap<number, BreakpointWizard>();
private readonly _breakpointsAssertions = new BreakpointsAssertions(this._breakpointsWizard, this._internal, this.currentBreakpointsMapping);
public constructor(
private readonly _breakpointsWizard: BreakpointsWizard,
private readonly _internal: InternalFileBreakpointsWizard,
public readonly currentBreakpointsMapping: CurrentBreakpointsMapping) {
this.currentBreakpointsMapping.forEach((vsCodeStatus, breakpoint) => {
this._idToBreakpoint.set(vsCodeStatus.id, breakpoint);
});
}
public async set(breakpointWizard: BreakpointWizard): Promise<void> {
if (this.currentBreakpointsMapping.has(breakpointWizard)) {
throw new Error(`Can't set the breakpoint: ${breakpointWizard} because it's already set`);
}
await this._internal.sendBreakpointsToClient(new BreakpointsUpdate([breakpointWizard], [], this.currentBreakpoints()));
}
public async unset(breakpointWizard: BreakpointWizard): Promise<void> {
if (!this.currentBreakpointsMapping.has(breakpointWizard)) {
throw new Error(`Can't unset the breakpoint: ${breakpointWizard} because it is not set`);
}
const remainingBreakpoints = this.currentBreakpoints().filter(bp => bp !== breakpointWizard);
await this._internal.sendBreakpointsToClient(new BreakpointsUpdate([], [breakpointWizard], remainingBreakpoints));
}
public onBreakpointStatusChange(breakpointStatusChanged: DebugProtocol.BreakpointEvent): void {
const breakpoint = this._idToBreakpoint.get(breakpointStatusChanged.body.breakpoint.id);
this.currentBreakpointsMapping.setAndReplaceIfExist(breakpoint, breakpointStatusChanged.body.breakpoint);
}
public assertIsVerified(breakpoint: BreakpointWizard): void {
this._breakpointsAssertions.assertIsVerified(breakpoint);
}
public async waitUntilVerified(breakpoint: BreakpointWizard): Promise<void> {
await this._breakpointsAssertions.waitUntilVerified(breakpoint);
}
public async assertIsHitThenResumeWhen(breakpoint: BreakpointWizard, lastActionToMakeBreakpointHit: () => Promise<void>, expectedStackTrace: string): Promise<void> {
await this._breakpointsAssertions.assertIsHitThenResumeWhen(breakpoint, lastActionToMakeBreakpointHit, expectedStackTrace);
}
private currentBreakpoints(): BreakpointWizard[] {
return Array.from(this.currentBreakpointsMapping.keys());
}
}

Просмотреть файл

@ -0,0 +1,65 @@
import _ = require('lodash');
import { DebugProtocol } from 'vscode-debugprotocol';
import { BreakpointWizard } from '../breakpointWizard';
import { PromiseOrNot } from 'vscode-chrome-debug-core';
import { ValidatedSet } from '../../../core-v2/chrome/collections/validatedSet';
import {
IPerformChangesImmediatelyOrBatchState, InternalFileBreakpointsWizard, CurrentBreakpointsMapping, BreakpointsUpdate
} from './internalFileBreakpointsWizard';
export class BatchingUpdatesState implements IPerformChangesImmediatelyOrBatchState {
private readonly _breakpointsToSet = new ValidatedSet<BreakpointWizard>();
private readonly _breakpointsToUnset = new ValidatedSet<BreakpointWizard>();
private readonly _actionsToCompleteAfterBatch: (() => PromiseOrNot<void>)[] = [];
public constructor(private readonly _internal: InternalFileBreakpointsWizard, public readonly currentBreakpointsMapping: CurrentBreakpointsMapping) { }
public set(breakpointWizard: BreakpointWizard): void {
this._breakpointsToSet.add(breakpointWizard);
this._breakpointsToUnset.deleteIfExists(breakpointWizard);
}
public unset(breakpointWizard: BreakpointWizard) {
this._breakpointsToUnset.add(breakpointWizard);
this._breakpointsToSet.deleteIfExists(breakpointWizard);
}
public assertIsVerified(breakpoint: BreakpointWizard): void {
this._actionsToCompleteAfterBatch.push(() => this._internal.assertIsVerified(breakpoint));
}
public async waitUntilVerified(breakpoint: BreakpointWizard): Promise<void> {
this._actionsToCompleteAfterBatch.push(() => this._internal.waitUntilVerified(breakpoint));
}
public onBreakpointStatusChange(_breakpointStatusChanged: DebugProtocol.BreakpointEvent): void {
throw new Error(`Breakpoint status shouldn't be updated while doing a batch update. Is this happening due to a product or test bug?`);
}
public async assertIsHitThenResumeWhen(_breakpoint: BreakpointWizard, _lastActionToMakeBreakpointHit: () => Promise<void>, _expectedStackTrace: string): Promise<void> {
throw new Error(`Breakpoint shouldn't be verified while doing a batch update. Is this happening due to a product or test bug?`);
}
public async processBatch(): Promise<void> {
const breakpointsToKeepAsIs = _.difference(Array.from(this.currentBreakpointsMapping.keys()), this._breakpointsToSet.toArray(), this._breakpointsToUnset.toArray());
await this._internal.sendBreakpointsToClient(new BreakpointsUpdate(Array.from(this._breakpointsToSet), Array.from(this._breakpointsToUnset), breakpointsToKeepAsIs));
// this._internal.sendBreakpointsToClient changed the state to PerformChangesImmediatelyState so we can now execute the actions we had pending
await this.executeActionsToCompleteAfterBatch();
}
private async executeActionsToCompleteAfterBatch(): Promise<void> {
// Validate with the originalSize that the actionsToCompleteAfterBatch aren't re-scheduled in a recursive way forever...
const originalSize = this._actionsToCompleteAfterBatch.length;
for (const actionToComplete of this._actionsToCompleteAfterBatch) {
await actionToComplete();
}
if (this._actionsToCompleteAfterBatch.length > originalSize) {
throw new Error(`The list of actions to complete increased while performing the actions to complete.`
+ ` The actions to complete probably ended up recursively scheduling more actions which is a bug`);
}
}
}

Просмотреть файл

@ -0,0 +1,102 @@
import * as assert from 'assert';
import { expect, use } from 'chai';
import * as chaiString from 'chai-string';
import { DebugProtocol } from 'vscode-debugprotocol';
import { THREAD_ID } from 'vscode-chrome-debug-core-testsupport';
import { BreakpointWizard } from '../breakpointWizard';
import { InternalFileBreakpointsWizard, CurrentBreakpointsMapping } from './internalFileBreakpointsWizard';
import { BreakpointsWizard } from '../breakpointsWizard';
import { waitUntilReadyWithTimeout } from '../../../utils/waitUntilReadyWithTimeout';
import { isThisV2, isThisV1 } from '../../../testSetup';
use(chaiString);
export class BreakpointsAssertions {
public constructor(
private readonly _breakpointsWizard: BreakpointsWizard,
private readonly _internal: InternalFileBreakpointsWizard,
public readonly currentBreakpointsMapping: CurrentBreakpointsMapping) { }
public assertIsVerified(breakpoint: BreakpointWizard): void {
const breakpointStatus = this.currentBreakpointsMapping.get(breakpoint);
assert(breakpointStatus.verified, `Expected ${breakpoint} to be verified yet it wasn't: ${breakpointStatus.message}`);
// Convert to one based to match the VS Code potocol and what VS Code does if you try to open that file at that line number
// const oneBasedExpectedLineNumber = breakpoint.position.lineNumber + 1;
// const oneBasedExpectedColumnNumber = breakpoint.position.columnNumber + 1;
// const filePath = this._internal.filePath;
// TODO: Re-enable this once we figure out how to deal with source-maps that do unexpected things
// assert.equal(breakpointStatus.line, oneBasedExpectedLineNumber,
// `Expected ${breakpoint} actual line to be ${filePath}:${oneBasedExpectedLineNumber}:${oneBasedExpectedColumnNumber}`
// + ` yet it was ${filePath}:${breakpointStatus.line}:${breakpointStatus.column}`);
}
public async waitUntilVerified(breakpoint: BreakpointWizard): Promise<void> {
await waitUntilReadyWithTimeout(() => this.currentBreakpointsMapping.get(breakpoint).verified);
}
public async assertIsHitThenResumeWhen(breakpoint: BreakpointWizard, lastActionToMakeBreakpointHit: () => Promise<void>, expectedStackTrace: string): Promise<void> {
const actionResult = lastActionToMakeBreakpointHit();
const vsCodeStatus = this.currentBreakpointsMapping.get(breakpoint);
const location = { path: this._internal.filePath, line: vsCodeStatus.line, colum: vsCodeStatus.column };
// TODO: Merge the two following calls together
await this._internal.client.assertStoppedLocation('breakpoint', location);
await this._breakpointsWizard.assertIsPaused();
const stackTraceResponse = await this._internal.client.send('stackTrace', {
threadId: THREAD_ID,
format: {
parameters: true,
parameterTypes: true,
parameterNames: true,
line: true,
module: true
}
});
this.validateStackTraceResponse(stackTraceResponse);
const formattedExpectedStackTrace = expectedStackTrace.replace(/^\s+/gm, ''); // Remove the white space we put at the start of the lines to make the stack trace align with the code
const actualStackTrace = this.extractStackTrace(stackTraceResponse);
assert.equal(actualStackTrace, formattedExpectedStackTrace, `Expected the stack trace when hitting ${breakpoint} to be:\n${formattedExpectedStackTrace}\nyet it is:\n${actualStackTrace}`);
// const scopesResponse = await this._internal.client.scopesRequest({ frameId: stackTraceResponse.body.stackFrames[0].id });
/// const scopes = scopesResponse.body.scopes;
await this._internal.client.continueRequest();
if (isThisV2) {
// TODO: Is getting this event on V2 a bug? See: Continued Event at https://microsoft.github.io/debug-adapter-protocol/specification
await this._breakpointsWizard.waitUntilJustResumed();
}
await actionResult;
}
private extractStackTrace(stackTraceResponse: DebugProtocol.StackTraceResponse): string {
return stackTraceResponse.body.stackFrames.map(f => this.printStackTraceFrame(f)).join('\n');
}
private printStackTraceFrame(frame: DebugProtocol.StackFrame): string {
let frameName = frame.name;
if (isThisV1) {
// V1 currently has a bug where line numbers are off by 1
frameName = frameName.replace(`Line ${frame.line - 1}`, `Line ${frame.line}`);
}
return `${frameName}:${frame.column}${frame.presentationHint && frame.presentationHint !== 'normal' ? ` (${frame.presentationHint})` : ''}`;
}
private validateStackTraceResponse(stackTraceResponse: DebugProtocol.StackTraceResponse) {
expect(stackTraceResponse.success, `Expected the response to the stack trace request to be succesful yet it failed: ${JSON.stringify(stackTraceResponse)}`).to.equal(true);
expect(stackTraceResponse.body.totalFrames, `The number of stackFrames was different than the value supplied on the totalFrames field`)
.to.equal(stackTraceResponse.body.stackFrames.length);
stackTraceResponse.body.stackFrames.forEach(frame => {
// Warning: We don't currently validate frame.source.path
const expectedSourceNameAndLine = ` [${frame.source.name}] Line ${frame.line}`;
if (isThisV2) {
// V1 has a bug where frame.name is not coherent with frame.line
expect(frame.name, 'Expected the formatted name to match the source name and line supplied as individual attributes').to.endsWith(expectedSourceNameAndLine);
}
});
}
}

Просмотреть файл

@ -0,0 +1,53 @@
import * as _ from 'lodash';
import { DebugProtocol } from 'vscode-debugprotocol';
import { BreakpointsUpdate, StateChanger, InternalFileBreakpointsWizard } from './internalFileBreakpointsWizard';
import { ExtendedDebugClient } from 'vscode-chrome-debug-core-testsupport';
import { BreakpointWizard, VSCodeActionWhenHit } from '../breakpointWizard';
import { PerformChangesImmediatelyState } from './performChangesImmediatelyState';
import { ValidatedMap } from '../../../core-v2/chrome/collections/validatedMap';
import { PauseOnHitCount } from '../../../core-v2/chrome/internal/breakpoints/bpActionWhenHit';
import { BreakpointsWizard } from '../breakpointsWizard';
export class BreakpointsUpdater {
public constructor(
private readonly _breakpointsWizard: BreakpointsWizard,
private readonly _internal: InternalFileBreakpointsWizard,
private readonly _client: ExtendedDebugClient,
private readonly _update: BreakpointsUpdate,
private readonly _changeState: StateChanger) { }
public async update(): Promise<void> {
const updatedBreakpoints = this._update.toKeepAsIs.concat(this._update.toAdd);
const vsCodeBps = updatedBreakpoints.map(bp => this.toVSCodeProtocol(bp));
const response = await this._client.setBreakpointsRequest({ breakpoints: vsCodeBps, source: { path: this._internal.filePath } });
if (!response.success) {
throw new Error(`Failed to set the breakpoints for: ${this._internal.filePath}`);
}
const expected = vsCodeBps.length;
const actual = response.body.breakpoints.length;
if (actual !== expected) {
throw new Error(`Expected to receive ${expected} breakpoints yet we got ${actual}. Received breakpoints: ${JSON.stringify(response.body.breakpoints)}`);
}
const breakpointToStatus = new ValidatedMap<BreakpointWizard, DebugProtocol.Breakpoint>(_.zip(updatedBreakpoints, response.body.breakpoints));
this._changeState(new PerformChangesImmediatelyState(this._breakpointsWizard, this._internal, breakpointToStatus));
}
private toVSCodeProtocol(breakpoint: BreakpointWizard): DebugProtocol.SourceBreakpoint {
// VS Code protocol is 1-based so we add one to the line and colum numbers
const commonInformation = { line: breakpoint.position.lineNumber + 1, column: breakpoint.position.columnNumber + 1 };
const actionWhenHitInformation = this.actionWhenHitToVSCodeProtocol(breakpoint);
return Object.assign({}, commonInformation, actionWhenHitInformation);
}
private actionWhenHitToVSCodeProtocol(breakpoint: BreakpointWizard): VSCodeActionWhenHit {
if (breakpoint.actionWhenHit instanceof PauseOnHitCount) {
return { hitCondition: breakpoint.actionWhenHit.pauseOnHitCondition };
} else {
throw new Error('Not yet implemented');
}
}
}

Просмотреть файл

@ -0,0 +1,84 @@
import * as _ from 'lodash';
import { ExtendedDebugClient } from 'vscode-chrome-debug-core-testsupport';
import { findPositionOfTextInFile } from '../../../utils/findPositionOfTextInFile';
import { DebugProtocol } from 'vscode-debugprotocol';
import { PauseOnHitCount } from '../../../core-v2/chrome/internal/breakpoints/bpActionWhenHit';
import { BreakpointWizard } from '../breakpointWizard';
import { ValidatedMap } from '../../../core-v2/chrome/collections/validatedMap';
import { FileBreakpointsWizard } from '../fileBreakpointsWizard';
import { PromiseOrNot } from 'vscode-chrome-debug-core';
import { BatchingUpdatesState } from './batchingUpdatesState';
import { PerformChangesImmediatelyState } from './performChangesImmediatelyState';
import { BreakpointsUpdater } from './breakpointsUpdater';
import { BreakpointsWizard } from '../breakpointsWizard';
export class BreakpointsUpdate {
public constructor(
public readonly toAdd: BreakpointWizard[],
public readonly toRemove: BreakpointWizard[],
public readonly toKeepAsIs: BreakpointWizard[]) { }
}
export interface IPerformChangesImmediatelyOrBatchState {
readonly currentBreakpointsMapping: CurrentBreakpointsMapping;
set(breakpointWizard: BreakpointWizard): void;
unset(breakpointWizard: BreakpointWizard): void;
waitUntilVerified(breakpoint: BreakpointWizard): Promise<void>;
assertIsVerified(breakpoint: BreakpointWizard): void;
assertIsHitThenResumeWhen(breakpoint: BreakpointWizard, lastActionToMakeBreakpointHit: () => Promise<void>, expectedStackTrace: string): Promise<void>;
onBreakpointStatusChange(breakpointStatusChanged: DebugProtocol.BreakpointEvent): void;
}
export type CurrentBreakpointsMapping = ValidatedMap<BreakpointWizard, DebugProtocol.Breakpoint>;
export type StateChanger = (newState: IPerformChangesImmediatelyOrBatchState) => void;
export class InternalFileBreakpointsWizard {
private _state: IPerformChangesImmediatelyOrBatchState = new PerformChangesImmediatelyState(this._breakpointsWizard, this, new ValidatedMap());
public constructor(private readonly _breakpointsWizard: BreakpointsWizard, public readonly client: ExtendedDebugClient, public readonly filePath: string) { }
public async hitCountBreakpoint(options: { lineText: string; hitCountCondition: string; name: string }): Promise<BreakpointWizard> {
const position = await findPositionOfTextInFile(this.filePath, options.lineText);
return new BreakpointWizard(this, position, new PauseOnHitCount(options.hitCountCondition), options.name);
}
public async set(breakpointWizard: BreakpointWizard): Promise<void> {
await this._state.set(breakpointWizard);
}
public async unset(breakpointWizard: BreakpointWizard): Promise<void> {
await this._state.unset(breakpointWizard);
}
public async waitUntilVerified(breakpoint: BreakpointWizard): Promise<void> {
await this._state.waitUntilVerified(breakpoint);
}
public assertIsVerified(breakpoint: BreakpointWizard): void {
this._state.assertIsVerified(breakpoint);
}
public async assertIsHitThenResumeWhen(breakpoint: BreakpointWizard, lastActionToMakeBreakpointHit: () => Promise<void>, expectedStackTrace: string): Promise<void> {
return this._state.assertIsHitThenResumeWhen(breakpoint, lastActionToMakeBreakpointHit, expectedStackTrace);
}
public onBreakpointStatusChange(breakpointStatusChanged: DebugProtocol.BreakpointEvent): void {
this._state.onBreakpointStatusChange(breakpointStatusChanged);
}
public async batch<T>(batchAction: (fileBreakpointsWizard: FileBreakpointsWizard) => PromiseOrNot<T>): Promise<T> {
const batchingUpdates = new BatchingUpdatesState(this, this._state.currentBreakpointsMapping);
this._state = batchingUpdates;
const result = await batchAction(new FileBreakpointsWizard(this));
await batchingUpdates.processBatch(); // processBatch calls sendBreakpointsToClient which will change the state back to PerformChangesImmediatelyState
return result;
}
public async sendBreakpointsToClient(update: BreakpointsUpdate): Promise<void> {
return new BreakpointsUpdater(this._breakpointsWizard, this, this.client, update, state => this._state = state).update();
}
}