Clean up int-chrome tests, port some (#306)

* Add hit condition tests

* Add tests for conditional breakpoints

* Delete unneeded int-chrome tests

* Add test that runs create-react-app

* Clean up testdata/ and remaining int-chrome references

* Fix env var spelling

* Fix "invalid condition" test

* Categorize tests correctly

* Disable framework tests for Windows

* Make React test a substring test

* Add new devops pipeline config
This commit is contained in:
Rob Lourens 2020-02-06 11:55:03 -08:00 коммит произвёл GitHub
Родитель 9991f3060a
Коммит c1bb81457b
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
271 изменённых файлов: 397 добавлений и 48625 удалений

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

@ -1,5 +1,6 @@
parameters:
runTests: true
runFrameworkTests: false
steps:
- task: NodeTool@0
@ -36,6 +37,18 @@ steps:
env:
DISPLAY: ':99.0'
- task: Npm@1
displayName: npm test (framework tests)
inputs:
command: custom
verbose: false
customCommand: test
timeoutInMinutes: 10
condition: eq(${{ parameters.runFrameworkTests }}, true)
env:
FRAMEWORK_TESTS: 1
DISPLAY: ':99.0'
- task: Gulp@0
displayName: gulp lint
inputs:

26
.ci/slow-tests.yml Normal file
Просмотреть файл

@ -0,0 +1,26 @@
# Run on a schedule
trigger: none
pr: none
jobs:
- job: macOS
pool:
vmImage: 'macOS-10.13'
steps:
- template: common-validation.yml
parameters:
runFrameworkTests: true
- job: Linux
pool:
vmImage: 'ubuntu-16.04'
steps:
- template: common-validation.yml
parameters:
runFrameworkTests: true
- job: Windows
pool:
vmImage: 'vs2017-win2016'
steps:
- template: common-validation.yml

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

@ -1,10 +1,10 @@
module.exports = {
ignorePatterns: [
'**/*.d.ts',
'src/int-chrome/**/*.ts',
'src/test/**/*.ts',
'demos/**/*',
'**/*.js',
'testWorkspace/**'
],
parser: '@typescript-eslint/parser',
extends: ['plugin:@typescript-eslint/recommended'],

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

@ -85,20 +85,6 @@
],
"outFiles": ["${workspaceFolder}/out/src/test/**/*.js"],
// "preLaunchTask": "npm: watch"
},
{
"type": "node",
"request": "launch",
"name": "Run int tests",
"program": "${workspaceFolder}/node_modules/mocha/bin/_mocha",
"args": ["--timeout", "10000", "-u", "tdd", "--colors", "--reporter", "out/src/int-chrome/testSupport/loggingReporter.js", "./out/src/int-chrome/**/*.test.js",
// "--grep", "Variables scopes"
],
"outFiles": ["${workspaceFolder}/out/src/**/*.js"],
"skipFiles": ["<node_internals>/**"],
"env": {
"DISPLAY": ":1.0"
}
},
{
"name": "Reset Results",

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

@ -45,8 +45,7 @@
"publish": "gulp publish",
"test": "gulp && npm-run-all --parallel test:golden test:lint",
"test:golden": "node ./out/src/test/runTest.js",
"test:lint": "gulp lint",
"intTest": "mocha --exit --timeout 20000 -s 3500 -u tdd --colors --reporter out/src/int-chrome/testSupport/loggingReporter.js \"./out/src/int-chrome/**/*.test.js\""
"test:lint": "gulp lint"
},
"dependencies": {
"@c4312/chromehash": "^0.2.0",

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

@ -1,291 +0,0 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/
import * as assert from 'assert';
import { expect } from 'chai';
import { createServer } from 'http-server';
import * as path from 'path';
import puppeteer from 'puppeteer';
import * as testSetup from './testSetup';
import { ExtendedDebugClient } from './testSupport/debugClient';
import { IChromeTestAttachConfiguration, killAllChrome, retryAsync } from './testUtils';
import { HttpOrHttpsServer } from './types/server';
import { getDebugAdapterLogFilePath } from './utils/logging';
const DATA_ROOT = testSetup.DATA_ROOT;
suite('Chrome Debug Adapter etc', () => {
let dc: ExtendedDebugClient;
let server: HttpOrHttpsServer | null;
setup(function() {
return testSetup.setup(this).then(_dc => (dc = _dc));
});
teardown(() => {
return testSetup.teardown();
});
suite('basic', () => {
test('unknown request should produce error', done => {
dc.send('illegal_request')
.then(() => {
done(new Error('does not report error on unknown request'));
})
.catch(() => {
done();
});
});
});
suite('initialize', () => {
test('should return supported features', () => {
return dc.initializeRequest().then(response => {
assert.notEqual(response.body, undefined);
assert.equal(response.body!.supportsConfigurationDoneRequest, true);
});
});
});
suite('launch', () => {
const testProjectRoot = path.join(DATA_ROOT, 'intervalDebugger');
setup(() => {
server = createServer({ root: testProjectRoot });
server!.listen(7890);
});
teardown(() => {
if (server) {
server.close(err =>
console.log('Error closing server in teardown: ' + (err && err.message)),
);
server = null;
}
});
/**
* On MacOS it fails because: stopped location: path mismatch:
* + expected: /users/vsts/agent/2.150.0/work/1/s/testdata/intervaldebugger/out/app.js
* - actual: users/vsts/agent/2.150.0/work/1/s/testdata/intervaldebugger/out/app.js
*/
(testSetup.isWindows ? test : test.skip)(
'should stop on debugger statement in file:///, sourcemaps disabled',
() => {
const launchFile = path.join(testProjectRoot, 'index.html');
const breakFile = path.join(testProjectRoot, 'out/app.js');
const DEBUGGER_LINE = 2;
return Promise.all([
dc.configurationSequence(),
dc.launch({ file: launchFile, sourceMaps: false }),
dc.assertStoppedLocation('debugger_statement', { path: breakFile, line: DEBUGGER_LINE }),
]);
},
);
test('should stop on debugger statement in http://localhost', () => {
const breakFile = path.join(testProjectRoot, 'src/app.ts');
const DEBUGGER_LINE = 2;
return Promise.all([
dc.configurationSequence(),
dc.launch({ url: 'http://localhost:7890', webRoot: testProjectRoot }),
dc.assertStoppedLocation('pause', { path: breakFile, line: DEBUGGER_LINE }),
]);
});
const testTitle =
'Should attach to existing instance of chrome and break on debugger statement';
test(testTitle, async () => {
const fullTestTitle = `Chrome Debug Adapter etc launch ${testTitle}`;
const breakFile = path.join(testProjectRoot, 'src/app.ts');
const DEBUGGER_LINE = 2;
const remoteDebuggingPort = 7777;
const browser = await puppeteer.launch({
headless: false,
args: ['http://localhost:7890', `--remote-debugging-port=${remoteDebuggingPort}`],
});
try {
await Promise.all([
dc.configurationSequence(),
dc.initializeRequest().then(_ => {
return dc.attachRequest(<IChromeTestAttachConfiguration>{
url: 'http://localhost:7890',
port: remoteDebuggingPort,
webRoot: testProjectRoot,
logFilePath: getDebugAdapterLogFilePath(fullTestTitle),
logTimestamps: true,
});
}),
dc.assertStoppedLocation('debugger_statement', { path: breakFile, line: DEBUGGER_LINE }),
]);
} finally {
await browser.close();
}
});
test.skip('Should hit breakpoint even if webRoot has unexpected case all lowercase for VisualStudio', async () => {
const breakFile = path.join(testProjectRoot, 'src/app.ts');
const DEBUGGER_LINE = 2;
await dc.initializeRequest({
adapterID: 'chrome',
clientID: 'visualstudio',
linesStartAt1: true,
columnsStartAt1: true,
pathFormat: 'path',
});
await dc.launchRequest({
url: 'http://localhost:7890',
webRoot: testProjectRoot.toLowerCase(),
runtimeExecutable: puppeteer.executablePath(),
} as any);
await dc.setBreakpointsRequest({
source: { path: breakFile },
breakpoints: [{ line: DEBUGGER_LINE }],
});
await dc.configurationDoneRequest();
await dc.assertStoppedLocation('debugger_statement', {
path: breakFile,
line: DEBUGGER_LINE,
});
});
test.skip('Should hit breakpoint even if webRoot has unexpected case all uppercase for VisualStudio', async () => {
const breakFile = path.join(testProjectRoot, 'src/app.ts');
const DEBUGGER_LINE = 2;
await dc.initializeRequest({
adapterID: 'chrome',
clientID: 'visualstudio',
linesStartAt1: true,
columnsStartAt1: true,
pathFormat: 'path',
});
await dc.launchRequest({
url: 'http://localhost:7890',
webRoot: testProjectRoot.toUpperCase(),
runtimeExecutable: puppeteer.executablePath(),
} as any);
await dc.setBreakpointsRequest({
source: { path: breakFile },
breakpoints: [{ line: DEBUGGER_LINE }],
});
await dc.configurationDoneRequest();
await dc.assertStoppedLocation('debugger_statement', {
path: breakFile,
line: DEBUGGER_LINE,
});
});
/**
* This test is baselining behvaior from V1 around what happens when the adapter tries to launch when
* there is another running instance of chrome with --remote-debugging-port set to the same port the adapter is trying to use.
* We expect the debug adapter to throw an exception saying that the connection attempt timed out after N milliseconds.
* TODO: We don't think is is ideal behavior for the adapter, and want to change it fairly quickly after V2 is ready to launch.
* right now this test exists only to verify that we match the behavior of V1
*/
test('Should throw error when launching if chrome debug port is in use', async () => {
// browser already launched to the default port, and navigated away from about:blank
const remoteDebuggingPort = 9222;
const browser = await puppeteer.launch({
headless: false,
args: ['http://localhost:7890', `--remote-debugging-port=${remoteDebuggingPort}`],
});
try {
await Promise.all([
dc.configurationSequence(),
dc.launch({
url: 'http://localhost:7890',
timeout: 2000,
webRoot: testProjectRoot,
port: remoteDebuggingPort,
}),
]);
assert.fail("Expected launch to throw a timeout exception, but it didn't.");
} catch (err) {
expect(err.message).to.satisfy((x: string) =>
x.startsWith('Cannot connect to runtime process, timeout after 2000 ms'),
);
} finally {
await browser.close();
}
// force kill chrome here, as it will be left open by the debug adapter (same behavior as v1)
killAllChrome();
});
test('Should launch successfully on port 0', async () => {
// browser already launched to the default port, and navigated away from about:blank
const remoteDebuggingPort = 0;
await Promise.all([
dc.configurationSequence(),
dc.launch({
url: 'http://localhost:7890',
timeout: 5000,
webRoot: testProjectRoot,
port: remoteDebuggingPort,
}),
]);
// wait for url to === http://localhost:7890 (launch response can come back before the navigation completes)
return waitForUrl(dc, 'http://localhost:7890/');
});
test('Should launch successfully on port 0, even when a browser instance is already running', async () => {
// browser already launched to the default port, and navigated away from about:blank
const remoteDebuggingPort = 0;
const dataDir = path.join(__dirname, 'testDataDir');
const browser = await puppeteer.launch({
headless: false,
args: [
'https://bing.com',
`--user-data-dir=${dataDir}`,
`--remote-debugging-port=${remoteDebuggingPort}`,
],
});
try {
await Promise.all([
dc.configurationSequence(),
dc.launch({
url: 'http://localhost:7890',
timeout: 5000,
webRoot: testProjectRoot,
port: remoteDebuggingPort,
userDataDir: dataDir,
}),
]);
await waitForUrl(dc, 'http://localhost:7890/');
} finally {
await browser.close();
}
});
});
});
async function waitForUrl(dc: ExtendedDebugClient, url: string): Promise<string> {
const timeoutMs = 5000;
const intervalDelayMs = 50;
return await retryAsync(
async () => {
const response = await dc.evaluateRequest({
context: 'repl',
expression: 'window.location.href',
});
expect(response.body.result).to.equal(`"${url}"`);
return url;
},
timeoutMs,
intervalDelayMs,
).catch(err => {
throw err;
});
}

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

@ -1,323 +0,0 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/
import * as path from 'path';
import { createServer } from 'http-server';
import * as testSetup from './testSetup';
import { HttpOrHttpsServer } from './types/server';
import { ExtendedDebugClient } from './testSupport/debugClient';
import { chromeLaunchConfigDefaults } from '../configuration';
suite('BreakOnLoad', () => {
const DATA_ROOT = testSetup.DATA_ROOT;
let dc: ExtendedDebugClient;
setup(function() {
return testSetup
.setup(this, {
sourceMapPathOverrides: chromeLaunchConfigDefaults.sourceMapPathOverrides,
})
.then(_dc => (dc = _dc));
});
let server: HttpOrHttpsServer | null;
teardown(() => {
if (server) {
server.close(err => console.log('Error closing server in teardown: ' + (err && err.message)));
server = null;
}
return testSetup.teardown();
});
// this function is to help when launching and setting a breakpoint
// currently, the chrome debug adapter, when launching in instrument mode and setting a breakpoint at (1, 1)
// the breakpoint is not yet 'hit' so the reason is given as 'debugger_statement'
// https://github.com/Microsoft/vscode-chrome-debug-core/blob/90797bc4a3599b0a7c0f197efe10ef7fab8442fd/src/chrome/chromeDebugAdapter.ts#L692
// so we don't want to use hitBreakpointUnverified function because it specifically checks for 'breakpoint' as the reason
function launchWithUrlAndSetBreakpoints(
url: string,
projectRoot: string,
scriptPath: string,
lineNum: number,
colNum: number,
): Promise<any> {
return Promise.all([
dc.launch({ url: url, webRoot: projectRoot }),
dc
.waitForEvent('initialized')
.then(_event => {
return dc.setBreakpointsRequest({
lines: [lineNum],
breakpoints: [{ line: lineNum, column: colNum }],
source: { path: scriptPath },
});
})
.then(_response => {
return dc.configurationDoneRequest();
}),
]);
}
suite('TypeScript Project with SourceMaps', () => {
test('Hits a single breakpoint in a file on load', async () => {
const testProjectRoot = path.join(DATA_ROOT, 'breakOnLoad_sourceMaps');
const scriptPath = path.join(testProjectRoot, 'src/script.ts');
server = createServer({ root: testProjectRoot });
server.listen(7890);
const url = 'http://localhost:7890/index.html';
const bpLine = 3;
const bpCol = 11;
await dc.hitBreakpointUnverified(
{ url, webRoot: testProjectRoot },
{ path: scriptPath, line: bpLine, column: bpCol },
);
});
test('Hits multiple breakpoints in a file on load', async () => {
const testProjectRoot = path.join(DATA_ROOT, 'breakOnLoad_sourceMaps');
const scriptPath = path.join(testProjectRoot, 'src/script.ts');
server = createServer({ root: testProjectRoot });
server.listen(7890);
const url = 'http://localhost:7890/index.html';
const bp1Line = 3;
const bp1Col = 11;
const bp2Line = 6;
await dc.hitBreakpointUnverified(
{ url, webRoot: testProjectRoot },
{ path: scriptPath, line: bp1Line, column: bp1Col },
);
await dc.setBreakpointsRequest({
source: { path: scriptPath },
breakpoints: [{ line: bp2Line }],
});
await dc.continueTo('breakpoint', { line: bp2Line });
});
test('Hits a breakpoint at (1,1) in a file on load', async () => {
const testProjectRoot = path.join(DATA_ROOT, 'breakOnLoad_sourceMaps');
const scriptPath = path.join(testProjectRoot, 'src/script.ts');
server = createServer({ root: testProjectRoot });
server.listen(7890);
const url = 'http://localhost:7890/index.html';
const bpLine = 1;
const bpCol = 1;
await launchWithUrlAndSetBreakpoints(url, testProjectRoot, scriptPath, bpLine, bpCol);
await dc.assertStoppedLocation('breakpoint', {
path: scriptPath,
line: bpLine,
column: bpCol,
});
});
test('Hits a breakpoint in the first line in a file on load', async () => {
const testProjectRoot = path.join(DATA_ROOT, 'breakOnLoad_sourceMaps');
const scriptPath = path.join(testProjectRoot, 'src/script.ts');
server = createServer({ root: testProjectRoot });
server.listen(7890);
const url = 'http://localhost:7890/index.html';
const bpLine = 1;
const bpCol = 34;
await dc.hitBreakpointUnverified(
{ url, webRoot: testProjectRoot },
{ path: scriptPath, line: bpLine, column: bpCol },
);
});
});
suite('Simple JavaScript Project', () => {
test('Hits a single breakpoint in a file on load', async () => {
const testProjectRoot = path.join(DATA_ROOT, 'breakOnLoad_javaScript');
const scriptPath = path.join(testProjectRoot, 'src/script.js');
server = createServer({ root: testProjectRoot });
server.listen(7890);
const url = 'http://localhost:7890/index.html';
const bpLine = 3;
const bpCol = 12;
await dc.hitBreakpointUnverified(
{ url, webRoot: testProjectRoot },
{ path: scriptPath, line: bpLine, column: bpCol },
);
});
test('Hits multiple breakpoints in a file on load', async () => {
const testProjectRoot = path.join(DATA_ROOT, 'breakOnLoad_javaScript');
const scriptPath = path.join(testProjectRoot, 'src/script.js');
server = createServer({ root: testProjectRoot });
server.listen(7890);
const url = 'http://localhost:7890/index.html';
const bp1Line = 3;
const bp1Col = 12;
const bp2Line = 6;
await dc.hitBreakpointUnverified(
{ url, webRoot: testProjectRoot },
{ path: scriptPath, line: bp1Line, column: bp1Col },
);
await dc.setBreakpointsRequest({
source: { path: scriptPath },
breakpoints: [{ line: bp2Line }],
});
await dc.continueTo('breakpoint', { line: bp2Line });
});
test('Hits a breakpoint at (1,1) in a file on load', async () => {
const testProjectRoot = path.join(DATA_ROOT, 'breakOnLoad_javaScript');
const scriptPath = path.join(testProjectRoot, 'src/script.js');
server = createServer({ root: testProjectRoot });
server.listen(7890);
const url = 'http://localhost:7890/index.html';
const bpLine = 1;
const bpCol = 1;
await launchWithUrlAndSetBreakpoints(url, testProjectRoot, scriptPath, bpLine, bpCol);
await dc.assertStoppedLocation('breakpoint', {
path: scriptPath,
line: bpLine,
column: bpCol,
});
});
test('Hits a breakpoint in the first line in a file on load', async () => {
const testProjectRoot = path.join(DATA_ROOT, 'breakOnLoad_javaScript');
const scriptPath = path.join(testProjectRoot, 'src/script.js');
server = createServer({ root: testProjectRoot });
server.listen(7890);
const url = 'http://localhost:7890/index.html';
const bpLine = 1;
const bpCol = 35;
await dc.hitBreakpointUnverified(
{ url, webRoot: testProjectRoot },
{ path: scriptPath, line: bpLine, column: bpCol },
);
});
test('Hits breakpoints on the first line of two scripts', async () => {
const testProjectRoot = path.join(DATA_ROOT, 'breakOnLoad_javaScript');
const scriptPath = path.join(testProjectRoot, 'src/script.js');
const script2Path = path.join(testProjectRoot, 'src/script2.js');
server = createServer({ root: testProjectRoot });
server.listen(7890);
const url = 'http://localhost:7890/index.html';
const bpLine = 1;
const bpCol = 1;
await launchWithUrlAndSetBreakpoints(url, testProjectRoot, scriptPath, bpLine, bpCol);
await dc.assertStoppedLocation('breakpoint', {
path: scriptPath,
line: bpLine,
column: bpCol,
});
await dc.setBreakpointsRequest({
lines: [bpLine],
breakpoints: [{ line: bpLine, column: bpCol }],
source: { path: script2Path },
});
await dc.continueRequest();
await dc.assertStoppedLocation('breakpoint', {
path: script2Path,
line: bpLine,
column: bpCol,
});
});
});
suite('Instrument Webpack Project', () => {
test('Hits a single breakpoint in a file on load', async () => {
const testProjectRoot = path.join(DATA_ROOT, 'breakOnLoad_webpack');
const scriptPath = path.join(testProjectRoot, 'src/script.js');
server = createServer({ root: testProjectRoot });
server.listen(7890);
const url = 'http://localhost:7890/dist/index.html';
const bpLine = 3;
const bpCol = 1;
await dc.hitBreakpointUnverified(
{ url, webRoot: testProjectRoot },
{ path: scriptPath, line: bpLine, column: bpCol },
);
});
test('Hits multiple breakpoints in a file on load', async () => {
const testProjectRoot = path.join(DATA_ROOT, 'breakOnLoad_webpack');
const scriptPath = path.join(testProjectRoot, 'src/script.js');
server = createServer({ root: testProjectRoot });
server.listen(7890);
const url = 'http://localhost:7890/dist/index.html';
// For some reason, column numbers > don't work perfectly with webpack
const bp1Line = 3;
const bp1Col = 1;
const bp2Line = 5;
const bp2Col = 1;
await dc.hitBreakpointUnverified(
{ url, webRoot: testProjectRoot },
{ path: scriptPath, line: bp1Line, column: bp1Col },
);
await dc.setBreakpointsRequest({
source: { path: scriptPath },
breakpoints: [{ line: bp2Line, column: bp2Col }],
});
await dc.continueTo('breakpoint', { line: bp2Line, column: bp2Col });
});
test('Hits a breakpoint at (1,1) in a file on load', async () => {
const testProjectRoot = path.join(DATA_ROOT, 'breakOnLoad_webpack');
const scriptPath = path.join(testProjectRoot, 'src/script.js');
server = createServer({ root: testProjectRoot });
server.listen(7890);
const url = 'http://localhost:7890/dist/index.html';
const bpLine = 1;
const bpCol = 1;
await dc.hitBreakpointUnverified(
{ url, webRoot: testProjectRoot },
{ path: scriptPath, line: bpLine, column: bpCol },
);
});
});
});

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

@ -1,162 +0,0 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/
import * as path from 'path';
import { createServer } from 'http-server';
import { expect } from 'chai';
import * as testSetup from './testSetup';
import { HttpOrHttpsServer } from './types/server';
import { ExtendedDebugClient } from './testSupport/debugClient';
suite('Breakpoints', () => {
const DATA_ROOT = testSetup.DATA_ROOT;
let dc: ExtendedDebugClient;
setup(function() {
return testSetup.setup(this).then(_dc => (dc = _dc));
});
let server: HttpOrHttpsServer | null;
teardown(async () => {
if (server) {
server.close(err => console.log('Error closing server in teardown: ' + (err && err.message)));
server = null;
}
await testSetup.teardown();
});
suite('getPossibleBreakpoints', () => {
const testProjectRoot = path.join(DATA_ROOT, 'columns');
const scriptPath = path.join(testProjectRoot, 'src/script.ts');
setup(async () => {
const url = 'http://localhost:7890/index.html';
server = createServer({ root: testProjectRoot });
server.listen(7890);
await dc.hitBreakpointUnverified(
{ url, webRoot: testProjectRoot },
{ path: scriptPath, line: 4, column: 16 },
);
});
test('gets breakpoints on a single line', async () => {
const result = await dc.breakpointLocations({
source: { path: scriptPath },
line: 4,
});
expect(result).to.deep.equal({
breakpoints: [
{ line: 4, column: 9 },
{ line: 4, column: 16 },
{ line: 4, column: 24 },
],
});
});
test('gets breakpoints across multiple lines', async () => {
const result = await dc.breakpointLocations({
source: { path: scriptPath },
line: 3,
column: 2,
endLine: 4,
endColumn: 20,
});
expect(result).to.deep.equal({
breakpoints: [
{ line: 3, column: 18 },
{ line: 3, column: 21 },
{ line: 3, column: 28 },
{ line: 4, column: 9 },
{ line: 4, column: 16 },
],
});
});
test('no-ops if getting breakpoints outside range', async () => {
const result = await dc.breakpointLocations({
source: { path: scriptPath },
line: 100,
});
expect(result.breakpoints).to.be.empty;
});
test('no-ops if getting a non-existent file', async () => {
const result = await dc.breakpointLocations({
source: { path: 'potato.js' },
line: 100,
});
expect(result.breakpoints).to.be.empty;
});
});
suite('Column BPs', () => {
test('Column BP is hit on correct column', async () => {
const testProjectRoot = path.join(DATA_ROOT, 'columns');
const scriptPath = path.join(testProjectRoot, 'src/script.ts');
server = createServer({ root: testProjectRoot });
server.listen(7890);
const url = 'http://localhost:7890/index.html';
const bpLine = 4;
const bpCol = 16;
await dc.hitBreakpointUnverified(
{ url, webRoot: testProjectRoot },
{ path: scriptPath, line: bpLine, column: bpCol },
);
});
test('Multiple column BPs are hit on correct columns', async () => {
const testProjectRoot = path.join(DATA_ROOT, 'columns');
const scriptPath = path.join(testProjectRoot, 'src/script.ts');
server = createServer({ root: testProjectRoot });
server.listen(7890);
const url = 'http://localhost:7890/index.html';
const bpLine = 4;
const bpCol1 = 16;
const bpCol2 = 24;
await dc.hitBreakpointUnverified(
{ url, webRoot: testProjectRoot },
{ path: scriptPath, line: bpLine, column: bpCol1 },
);
await dc.setBreakpointsRequest({
source: { path: scriptPath },
breakpoints: [{ line: bpLine, column: bpCol2 }],
});
await dc.continueTo('breakpoint', { line: bpLine, column: bpCol2 });
});
test.skip('BP col is adjusted to correct col', async () => {
const testProjectRoot = path.join(DATA_ROOT, 'columns');
const scriptPath = path.join(testProjectRoot, 'src/script.ts');
server = createServer({ root: testProjectRoot });
server.listen(7890);
const url = 'http://localhost:7890/index.html';
const bpLine = 4;
const bpCol1 = 19;
const correctBpCol1 = 16;
const expectedLocation = { path: scriptPath, line: bpLine, column: correctBpCol1 };
await dc.hitBreakpointUnverified(
{ url, webRoot: testProjectRoot },
{ path: scriptPath, line: bpLine, column: bpCol1 },
expectedLocation,
);
});
});
});

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

@ -1,11 +0,0 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/
export function asyncMap<T, U>(
array: ReadonlyArray<T>,
callbackfn: (value: T, index: number, array: ReadonlyArray<T>) => Promise<U> | U,
thisArg?: any,
): Promise<U[]> {
return Promise.all(array.map(callbackfn, thisArg));
}

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

@ -1,128 +0,0 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/
import * as assert from 'assert';
import { ValidatedMap } from './validatedMap';
import { printMap } from './printing';
import { breakWhileDebugging } from '../../validation';
/** A map where we can efficiently get the key from the value or the value from the key */
export class BidirectionalMap<Left, Right> {
private readonly _leftToRight = new ValidatedMap<Left, Right>();
private readonly _rightToLeft = new ValidatedMap<Right, Left>();
constructor(initialContents?: Iterable<[Left, Right]> | ReadonlyArray<[Left, Right]>) {
this._leftToRight = initialContents
? new ValidatedMap<Left, Right>(initialContents)
: new ValidatedMap<Left, Right>();
const reversed = Array.from(this._leftToRight.entries()).map(e => <[Right, Left]>[e[1], e[0]]);
this._rightToLeft = new ValidatedMap<Right, Left>(reversed);
}
public clear(): void {
this._leftToRight.clear();
this._rightToLeft.clear();
}
public deleteByLeft(left: Left): boolean {
const right = this._leftToRight.get(left);
if (right !== undefined) {
this.delete(left, right);
return true;
} else {
return false;
}
}
public deleteByRight(right: Right): boolean {
const left = this._rightToLeft.get(right);
if (left !== undefined) {
this.delete(left, right);
return true;
} else {
return false;
}
}
private delete(left: Left, right: Right): void {
assert.ok(
this._leftToRight.delete(left),
`Expected left (${left}) associated with right (${right}) to exist on the left to right internal map`,
);
assert.ok(
this._rightToLeft.delete(right),
`Expected right (${right}) associated with left (${left}) to exist on the right to left internal map`,
);
}
public forEach(
callbackfn: (Right: Right, left: Left, map: Map<Left, Right>) => void,
thisArg?: any,
): void {
return this._leftToRight.forEach(callbackfn, thisArg);
}
public getByLeft(left: Left): Right {
return this._leftToRight.get(left);
}
public getByRight(right: Right): Left {
return this._rightToLeft.get(right);
}
public tryGettingByLeft(left: Left): Right | undefined {
return this._leftToRight.tryGetting(left);
}
public tryGettingByRight(right: Right): Left | undefined {
return this._rightToLeft.tryGetting(right);
}
public hasLeft(left: Left): boolean {
return this._leftToRight.has(left);
}
public hasRight(right: Right): boolean {
return this._rightToLeft.has(right);
}
public set(left: Left, right: Right): this {
const existingRightForLeft = this._leftToRight.tryGetting(left);
const existingLeftForRight = this._rightToLeft.tryGetting(right);
if (existingRightForLeft !== undefined) {
breakWhileDebugging();
throw new Error(
`Can't set the pair left (${left}) and right (${right}) because there is already a right element (${existingRightForLeft}) associated with the left element`,
);
}
if (existingLeftForRight !== undefined) {
breakWhileDebugging();
throw new Error(
`Can't set the pair left (${left}) and right (${right}) because there is already a left element (${existingLeftForRight}) associated with the right element`,
);
}
this._leftToRight.set(left, right);
this._rightToLeft.set(right, left);
return this;
}
public size(): number {
return this._leftToRight.size;
}
public lefts(): IterableIterator<Left> {
return this._leftToRight.keys();
}
public rights(): IterableIterator<Right> {
return this._rightToLeft.keys();
}
public toString(): string {
return printMap('BidirectionalMap', this._leftToRight);
}
}

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

@ -1,33 +0,0 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/
/** Methods to print the contents of a collection for logging and debugging purposes (This is not intended for the end-user to see) */
export function printMap<K, V>(
typeDescription: string,
map: { entries(): IterableIterator<[K, V]> },
): string {
const elementsPrinted = Array.from(map.entries())
.map(entry => `${entry[0]}: ${entry[1]}`)
.join('; ');
return `${typeDescription} { ${elementsPrinted} }`;
}
export function printSet<T>(typeDescription: string, set: Set<T>): string {
const elementsPrinted = printElements(Array.from(set), '; ');
return `${typeDescription} { ${elementsPrinted} }`;
}
export function printArray<T>(typeDescription: string, elements: T[]): string {
const elementsPrinted = printElements(elements, ', ');
return typeDescription ? `${typeDescription} [ ${elementsPrinted} ]` : `[ ${elementsPrinted} ]`;
}
export function printIterable<T>(typeDescription: string, iterable: IterableIterator<T>): string {
const elementsPrinted = printElements(Array.from(iterable), '; ');
return `${typeDescription} { ${elementsPrinted} }`;
}
function printElements<T>(elements: T[], separator = '; '): string {
return elements.map(element => `${element}`).join(separator);
}

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

@ -1,34 +0,0 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/
import { ValidatedMultiMap } from './validatedMultiMap';
export function groupByKey<T, K>(
elements: T[],
obtainKey: (element: T) => K,
): ValidatedMultiMap<K, T> {
const grouped = ValidatedMultiMap.empty<K, T>();
elements.forEach(element => grouped.add(obtainKey(element), element));
return grouped;
}
export function determineOrderingOfStrings(left: string, right: string): number {
if (left < right) {
return -1;
} else if (left > right) {
return 1;
} else {
return 0;
}
}
export function singleElementOfArray<T>(array: ReadonlyArray<T>): T {
if (array.length === 1) {
return array[0];
} else {
throw new Error(
`Expected array ${array} to have exactly a single element yet it had ${array.length}`,
);
}
}

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

@ -1,154 +0,0 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/
import { printMap } from './printing';
import { breakWhileDebugging } from '../../validation';
export type ValueComparerFunction<V> = (left: V, right: V) => boolean;
export interface IValidatedMap<K, V> extends Map<K, V> {
get(key: K): V;
tryGetting(key: K): V | undefined;
getOr(key: K, elementDoesntExistAction: () => V): V;
getOrAdd(key: K, obtainValueToAdd: () => V): V;
setAndReplaceIfExist(key: K, value: V): this;
setAndIgnoreDuplicates(key: K, value: V, comparer?: ValueComparerFunction<V>): this;
}
/** A map that throws exceptions instead of returning error codes. */
export class ValidatedMap<K, V> implements IValidatedMap<K, V> {
private readonly _wrappedMap: Map<K, V>;
constructor(initialContents?: Map<K, V>);
constructor(iterable: Iterable<[K, V]>);
constructor(array: ReadonlyArray<[K, V]>);
constructor(initialContents?: Map<K, V> | Iterable<[K, V]> | ReadonlyArray<[K, V]>) {
if (initialContents !== undefined) {
this._wrappedMap =
initialContents instanceof Map
? new Map<K, V>(initialContents.entries())
: new Map<K, V>(initialContents);
} else {
this._wrappedMap = new Map<K, V>();
}
}
public static with<K, V>(key: K, value: V): ValidatedMap<K, V> {
return new ValidatedMap<K, V>([[key, value]]);
}
public get size(): number {
return this._wrappedMap.size;
}
public get [Symbol.toStringTag](): 'Map' {
return 'ValidatedMap' as 'Map';
}
public clear(): void {
this._wrappedMap.clear();
}
public delete(key: K): boolean {
if (!this._wrappedMap.delete(key)) {
breakWhileDebugging();
throw new Error(
`Couldn't delete element with key ${key} because it wasn't present in the map`,
);
}
return true;
}
public forEach(callbackfn: (value: V, key: K, map: Map<K, V>) => void, thisArg?: any): void {
this._wrappedMap.forEach(callbackfn, thisArg);
}
public get(key: K): V {
const value = this._wrappedMap.get(key);
if (value === undefined) {
breakWhileDebugging();
throw new Error(
`Couldn't get the element with key '${key}' because it wasn't present in this map <${this}>`,
);
}
return value;
}
public getOr(key: K, elementDoesntExistAction: () => V): V {
const existingValue = this.tryGetting(key);
if (existingValue !== undefined) {
return existingValue;
} else {
return elementDoesntExistAction();
}
}
public getOrAdd(key: K, obtainValueToAdd: () => V): V {
return this.getOr(key, () => {
const newValue = obtainValueToAdd();
this.set(key, newValue);
return newValue;
});
}
public has(key: K): boolean {
return this._wrappedMap.has(key);
}
public set(key: K, value: V): this {
if (this.has(key)) {
breakWhileDebugging();
throw new Error(`Cannot set key ${key} because it already exists`);
}
return this.setAndReplaceIfExist(key, value);
}
public setAndReplaceIfExist(key: K, value: V): this {
this._wrappedMap.set(key, value);
return this;
}
public setAndIgnoreDuplicates(
key: K,
value: V,
comparer: ValueComparerFunction<V> = (left, right) => left === right,
) {
const existingValueOrUndefined = this.tryGetting(key);
if (existingValueOrUndefined !== undefined && !comparer(existingValueOrUndefined, value)) {
breakWhileDebugging();
throw new Error(
`Cannot set key ${key} for value ${value} because it already exists and it's associated to a different value: ${existingValueOrUndefined}`,
);
}
return this.setAndReplaceIfExist(key, value);
}
[Symbol.iterator](): IterableIterator<[K, V]> {
return this._wrappedMap.entries();
}
public entries(): IterableIterator<[K, V]> {
return this._wrappedMap.entries();
}
public keys(): IterableIterator<K> {
return this._wrappedMap.keys();
}
public values(): IterableIterator<V> {
return this._wrappedMap.values();
}
// TODO: Remove the use of undefined
public tryGetting(key: K): V | undefined {
return this._wrappedMap.get(key) || undefined;
}
public toString(): string {
return printMap('ValidatedMap', this);
}
}

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

@ -1,140 +0,0 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/
import { ValidatedMap, IValidatedMap } from './validatedMap';
import { printMap } from './printing';
import { ValidatedSet, IValidatedSet } from './validatedSet';
/** A multi map that throws exceptions instead of returning error codes. */
export class ValidatedMultiMap<K, V> {
public get keysSize(): number {
return this._wrappedMap.size;
}
public get [Symbol.toStringTag](): 'Map' {
return 'ValidatedMultiMap' as 'Map';
}
private constructor(private readonly _wrappedMap: IValidatedMap<K, IValidatedSet<V>>) {}
public static empty<K, V>(): ValidatedMultiMap<K, V> {
return this.usingCustomMap(new ValidatedMap<K, IValidatedSet<V>>());
}
public static withContents<K, V>(
initialContents: Map<K, Set<V>> | Iterable<[K, Set<V>]> | ReadonlyArray<[K, Set<V>]>,
): ValidatedMultiMap<K, V> {
const elements = Array.from(initialContents).map(
element => <[K, IValidatedSet<V>]>[element[0], new ValidatedSet(element[1])],
);
return this.usingCustomMap(new ValidatedMap<K, IValidatedSet<V>>(elements));
}
public static usingCustomMap<K, V>(
wrappedMap: IValidatedMap<K, IValidatedSet<V>>,
): ValidatedMultiMap<K, V> {
return new ValidatedMultiMap(wrappedMap);
}
public clear(): void {
this._wrappedMap.clear();
}
public delete(key: K): boolean {
return this._wrappedMap.delete(key);
}
public forEach(
callbackfn: (value: Set<V>, key: K, map: Map<K, Set<V>>) => void,
thisArg?: any,
): void {
this._wrappedMap.forEach(callbackfn, thisArg);
}
public get(key: K): Set<V> {
return this._wrappedMap.get(key);
}
public getOr(key: K, elementDoesntExistAction: () => Set<V>): Set<V> {
return this._wrappedMap.getOr(key, () => new ValidatedSet(elementDoesntExistAction()));
}
public has(key: K): boolean {
return this._wrappedMap.has(key);
}
public addKeyIfNotExistant(key: K): this {
const existingValues = this._wrappedMap.tryGetting(key);
if (existingValues === undefined) {
this._wrappedMap.set(key, new ValidatedSet());
}
return this;
}
public add(key: K, value: V): this {
const existingValues = this._wrappedMap.tryGetting(key);
if (existingValues !== undefined) {
existingValues.add(value);
} else {
this._wrappedMap.set(key, new ValidatedSet([value]));
}
return this;
}
public addAndIgnoreDuplicates(key: K, value: V): this {
const existingValues = this._wrappedMap.tryGetting(key);
if (existingValues !== undefined) {
existingValues.addOrReplaceIfExists(value);
} else {
this._wrappedMap.set(key, new ValidatedSet([value]));
}
return this;
}
public removeValueAndIfLastRemoveKey(key: K, value: V): this {
const remainingValues = this.removeValue(key, value);
if (remainingValues.size === 0) {
this._wrappedMap.delete(key);
}
return this;
}
public removeValue(key: K, value: V): Set<V> {
const existingValues = this._wrappedMap.get(key);
if (!existingValues.delete(value)) {
throw new Error(
`Failed to delete the value ${value} under key ${key} because it wasn't present`,
);
}
return existingValues;
}
[Symbol.iterator](): IterableIterator<[K, Set<V>]> {
return this._wrappedMap.entries();
}
public entries(): IterableIterator<[K, Set<V>]> {
return this._wrappedMap.entries();
}
public keys(): IterableIterator<K> {
return this._wrappedMap.keys();
}
public values(): IterableIterator<Set<V>> {
return this._wrappedMap.values();
}
public tryGetting(key: K): Set<V> | undefined {
return this._wrappedMap.tryGetting(key);
}
public toString(): string {
return printMap('ValidatedMultiMap', this);
}
}

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

@ -1,96 +0,0 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/
import { printSet } from './printing';
import { breakWhileDebugging } from '../../validation';
export interface IValidatedSet<K> extends Set<K> {
addOrReplaceIfExists(key: K): this;
deleteIfExists(key: K): boolean;
toArray(): K[];
}
/** A set that throws exceptions instead of returning error codes. */
export class ValidatedSet<K> implements IValidatedSet<K> {
private readonly _wrappedSet: Set<K>;
public constructor();
public constructor(iterable: Iterable<K>);
public constructor(values?: ReadonlyArray<K>);
public constructor(valuesOrIterable?: ReadonlyArray<K> | undefined | Iterable<K>) {
this._wrappedSet = valuesOrIterable ? new Set(valuesOrIterable) : new Set();
}
public get size(): number {
return this._wrappedSet.size;
}
public get [Symbol.toStringTag](): 'Set' {
return 'ValidatedSet' as 'Set';
}
public clear(): void {
this._wrappedSet.clear();
}
public delete(key: K): boolean {
if (!this._wrappedSet.delete(key)) {
breakWhileDebugging();
throw new Error(
`Couldn't delete element with key ${key} because it wasn't present in the set`,
);
}
return true;
}
public deleteIfExists(key: K): boolean {
return this._wrappedSet.delete(key);
}
public forEach(callbackfn: (key: K, sameKeyAgain: K, set: Set<K>) => void, thisArg?: any): void {
this._wrappedSet.forEach(callbackfn, thisArg);
}
public has(key: K): boolean {
return this._wrappedSet.has(key);
}
public add(key: K): this {
if (this.has(key)) {
breakWhileDebugging();
throw new Error(`Cannot add key ${key} because it already exists`);
}
return this.addOrReplaceIfExists(key);
}
public addOrReplaceIfExists(key: K): this {
this._wrappedSet.add(key);
return this;
}
[Symbol.iterator](): IterableIterator<K> {
return this._wrappedSet[Symbol.iterator]();
}
public entries(): IterableIterator<[K, K]> {
return this._wrappedSet.entries();
}
public keys(): IterableIterator<K> {
return this._wrappedSet.keys();
}
public values(): IterableIterator<K> {
return this._wrappedSet.values();
}
public toString(): string {
return printSet('ValidatedSet', this);
}
public toArray(): K[] {
return Array.from(this);
}
}

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

@ -1,71 +0,0 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/
import { IEquivalenceComparable } from '../../utils/equivalence';
/**
* These classes represents the different actions that a breakpoint can take when hit
* Breakpoint: AlwaysPause
* Conditional Breakpoint: ConditionalPause
* Logpoint: LogMessage
* Hit Count Breakpoint: PauseOnHitCount
*/
export interface IBPActionWhenHit extends IEquivalenceComparable {
isEquivalentTo(bpActionWhenHit: IBPActionWhenHit): boolean;
}
export class AlwaysPause implements IBPActionWhenHit {
public isEquivalentTo(bpActionWhenHit: IBPActionWhenHit): boolean {
return bpActionWhenHit instanceof AlwaysPause;
}
public toString(): string {
return 'always pause';
}
}
export class ConditionalPause implements IBPActionWhenHit {
constructor(public readonly expressionOfWhenToPause: string) {}
public isEquivalentTo(bpActionWhenHit: IBPActionWhenHit): boolean {
return (
bpActionWhenHit instanceof ConditionalPause &&
this.expressionOfWhenToPause === bpActionWhenHit.expressionOfWhenToPause
);
}
public toString(): string {
return `pause if: ${this.expressionOfWhenToPause}`;
}
}
export class PauseOnHitCount implements IBPActionWhenHit {
constructor(public readonly pauseOnHitCondition: string) {}
public isEquivalentTo(bpActionWhenHit: IBPActionWhenHit): boolean {
return (
bpActionWhenHit instanceof PauseOnHitCount &&
this.pauseOnHitCondition === bpActionWhenHit.pauseOnHitCondition
);
}
public toString(): string {
return `pause when hits: ${this.pauseOnHitCondition}`;
}
}
export class LogMessage implements IBPActionWhenHit {
constructor(public readonly expressionToLog: string) {}
public isEquivalentTo(bpActionWhenHit: IBPActionWhenHit): boolean {
return (
bpActionWhenHit instanceof LogMessage &&
this.expressionToLog === bpActionWhenHit.expressionToLog
);
}
public toString(): string {
return `log: ${this.expressionToLog}`;
}
}

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

@ -1,72 +0,0 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/
import * as Validation from '../../../validation';
import { ColumnNumber, LineNumber, createLineNumber, createColumnNumber } from './subtypes';
import { IEquivalenceComparable } from '../../utils/equivalence';
import * as _ from 'lodash';
export type integer = number;
export class Position implements IEquivalenceComparable {
public static readonly origin = new Position(createLineNumber(0), createColumnNumber(0));
constructor(public readonly lineNumber: LineNumber, public readonly columnNumber: ColumnNumber) {
Validation.zeroOrPositive('Line number', lineNumber);
if (columnNumber !== undefined) {
Validation.zeroOrPositive('Column number', columnNumber);
}
}
public static appearingLastOf(...positions: Position[]): Position {
const lastPosition = _.reduce(positions, (left, right) =>
left.doesAppearBefore(right) ? right : left,
);
if (lastPosition !== undefined) {
return lastPosition;
} else {
throw new Error(
`Couldn't find the position appearing last from the list: ${positions}. Is it possible the list was empty?`,
);
}
}
public static appearingFirstOf(...positions: Position[]): Position {
const firstPosition = _.reduce(positions, (left, right) =>
left.doesAppearBefore(right) ? left : right,
);
if (firstPosition !== undefined) {
return firstPosition;
} else {
throw new Error(
`Couldn't find the position appearing first from the list: ${positions}. Is it possible the list was empty?`,
);
}
}
public static isBetween(start: Position, maybeInBetween: Position, end: Position): boolean {
return !maybeInBetween.doesAppearBefore(start) && !end.doesAppearBefore(maybeInBetween);
}
public isEquivalentTo(location: Position): boolean {
return this.lineNumber === location.lineNumber && this.columnNumber === location.columnNumber;
}
public isOrigin(): boolean {
return this.lineNumber === 0 && (this.columnNumber === undefined || this.columnNumber === 0);
}
public doesAppearBefore(right: Position): boolean {
return (
this.lineNumber < right.lineNumber ||
(this.lineNumber === right.lineNumber && this.columnNumber < right.columnNumber)
);
}
public toString(): string {
return this.columnNumber !== undefined
? `${this.lineNumber}:${this.columnNumber}`
: `${this.lineNumber}`;
}
}

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

@ -1,26 +0,0 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/
// We use these types to have the compiler check that we are not sending a ColumnNumber where a LineNumber is expected
const lineIndexSymbol = Symbol();
export type LineNumber = number & { [lineIndexSymbol]: true };
export function createLineNumber(numberRepresentation: number): LineNumber {
return <LineNumber>numberRepresentation;
}
const columnIndexSymbol = Symbol();
export type ColumnNumber = number & { [columnIndexSymbol]: true };
export function createColumnNumber(numberRepresentation: number): ColumnNumber {
return <ColumnNumber>numberRepresentation;
}
const URLRegexpSymbol = Symbol();
export type URLRegexp = string & { [URLRegexpSymbol]: true };
export function createURLRegexp(textRepresentation: string): URLRegexp {
return <URLRegexp>textRepresentation;
}

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

@ -1,252 +0,0 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/
import * as _ from 'lodash';
enum Synchronicity {
Sync,
Async,
}
enum Outcome {
Succesful,
Failure,
}
export class ReplacementInstruction {
public constructor(public readonly pattern: RegExp, public readonly replacement: string) {}
}
export interface IMethodsCalledLoggerConfiguration {
readonly replacements: ReplacementInstruction[];
customizeResult(methodName: string | symbol | number, args: unknown[], result: unknown): unknown;
customizeArgumentsBeforeCall(
receiverName: string,
methodName: string | symbol | number,
args: unknown[],
): void;
}
export class MethodsCalledLoggerConfiguration implements IMethodsCalledLoggerConfiguration {
public constructor(
public readonly containerName: string,
private _replacements: ReplacementInstruction[],
) {}
public customizeResult(
_methodName: string | symbol | number,
_args: unknown[],
result: unknown,
): unknown {
return result;
}
public customizeArgumentsBeforeCall(
receiverName: string,
methodName: string | symbol | number,
args: object[],
): void {
if (methodName === 'on' && args.length >= 2) {
args[1] = new MethodsCalledLogger(
this,
args[1],
`(${receiverName} emits ${args[0]})`,
).wrapped();
}
}
public get replacements(): ReplacementInstruction[] {
return this._replacements;
}
public updateReplacements(replacements: ReplacementInstruction[]): void {
this._replacements = replacements;
}
}
export class MethodsCalledLogger<T extends object> {
private static _nextCallId = 10000;
constructor(
private readonly _configuration: IMethodsCalledLoggerConfiguration,
private readonly _objectToWrap: T,
private readonly _objectToWrapName: string,
) {}
public wrapped(): T {
const handler = {
get: <K extends keyof T>(target: T, propertyKey: K, receiver: any) => {
const originalPropertyValue = target[propertyKey];
if (typeof originalPropertyValue === 'function') {
return (...args: any) => {
const callId = this.generateCallId();
try {
this.logCallStart(propertyKey, args, callId);
this._configuration.customizeArgumentsBeforeCall(
this._objectToWrapName,
propertyKey,
args,
);
const result = originalPropertyValue.apply(target, args);
if (!result || !result.then) {
this.logCall(
propertyKey,
Synchronicity.Sync,
args,
Outcome.Succesful,
result,
callId,
);
if (result === target) {
return receiver;
} else {
return this._configuration.customizeResult(propertyKey, args, result);
}
} else {
this.logSyncPartFinished(propertyKey, args, callId);
return result.then(
(promiseResult: unknown) => {
this.logCall(
propertyKey,
Synchronicity.Async,
args,
Outcome.Succesful,
promiseResult,
callId,
);
if (promiseResult === target) {
return receiver;
} else {
return this._configuration.customizeResult(propertyKey, args, promiseResult);
}
},
(error: unknown) => {
this.logCall(
propertyKey,
Synchronicity.Async,
args,
Outcome.Failure,
error,
callId,
);
return Promise.reject(error);
},
);
}
} catch (exception) {
this.logCall(
propertyKey,
Synchronicity.Sync,
args,
Outcome.Failure,
exception,
callId,
);
throw exception;
}
};
} else {
return originalPropertyValue;
}
},
};
return new Proxy<T>(this._objectToWrap, handler);
}
private generateCallId(): number {
return MethodsCalledLogger._nextCallId++;
}
// private printMethodCall(propertyKey: PropertyKey, methodCallArguments: any[]): string {
// return `${this._objectToWrapName}.${String(propertyKey)}(${this.printArguments(methodCallArguments)})`;
// }
// private printMethodResponse(outcome: Outcome, resultOrException: unknown): string {
// return `${outcome === Outcome.Succesful ? '->' : 'threw'} ${this.printObject(resultOrException)}`;
// }
// private printMethodSynchronicity(synchronicity: Synchronicity): string {
// return `${synchronicity === Synchronicity.Sync ? '' : ' async'}`;
// }
/** Returns the test file and line that the code is currently executing e.g.:
* < >
* [22:23:28.468 UTC] START 10026: hitCountBreakpointTests.test.ts:34:2 | #incrementBtn.click()
*/
// TODO: Figure out how to integrate this with V2. We don't want to do this for production logging because new Error().stack is slow
// private getTestFileAndLine(): string {
// const stack = new Error().stack;
// if (stack) {
// const stackLines = stack.split('\n');
// const testCaseLine = stackLines.find(line => line.indexOf('test.ts') >= 0);
// if (testCaseLine) {
// const filenameAndLine = testCaseLine.lastIndexOf(path.sep);
// if (filenameAndLine >= 0) {
// const fileNameAndLineNumber = testCaseLine.substring(filenameAndLine + 1, testCaseLine.length - 2);
// return `${fileNameAndLineNumber} | `;
// }
// }
// }
// return '';
// }
private logCallStart(
_propertyKey: PropertyKey,
_methodCallArguments: any[],
_callId: number,
): void {
// const getTestFileAndLine = this.getTestFileAndLine();
// const message = `START ${callId}: ${getTestFileAndLine}${this.printMethodCall(propertyKey, methodCallArguments)}`;
// logger.verbose(message);
}
private logSyncPartFinished(
_propertyKey: PropertyKey,
_methodCallArguments: any[],
_callId: number,
): void {
// const getTestFileAndLine = this.getTestFileAndLine();
// const message = `PROMISE-RETURNED ${callId}: ${getTestFileAndLine}${this.printMethodCall(propertyKey, methodCallArguments)}`;
// logger.verbose(message);
}
private logCall(
_propertyKey: PropertyKey,
_synchronicity: Synchronicity,
_methodCallArguments: any[],
_outcome: Outcome,
_resultOrException: unknown,
_callId: number,
): void {
// const endPrefix = callId ? `END ${callId}: ` : '';
// const message = `${endPrefix}${this.printMethodCall(propertyKey, methodCallArguments)} ${this.printMethodSynchronicity(synchronicity)} ${this.printMethodResponse(outcome, resultOrException)}`;
// logger.verbose(message);
}
// private printArguments(methodCallArguments: any[]): string {
// return methodCallArguments.map(methodCallArgument => this.printObject(methodCallArgument)).join(', ');
// }
// private printObject(objectToPrint: unknown): string {
// const description = printTopLevelObjectDescription(objectToPrint);
// const printedReduced = _.reduce(Array.from(this._configuration.replacements),
// (text, replacement) =>
// text.replace(replacement.pattern, replacement.replacement),
// description);
// return printedReduced;
// }
}
export function wrapWithMethodLogger<T extends object>(
objectToWrap: T,
objectToWrapName = `${objectToWrap}`,
): T {
return new MethodsCalledLogger(
new MethodsCalledLoggerConfiguration('no container', []),
objectToWrap,
objectToWrapName,
).wrapped();
}

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

@ -1,79 +0,0 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/
import * as _ from 'lodash';
export function printTopLevelObjectDescription(objectToPrint: unknown) {
return printObjectDescription(objectToPrint, printFirstLevelProperties);
}
export function printObjectDescription(
objectToPrint: unknown,
fallbackPrintDescription = (obj: unknown) => `${obj}`,
) {
let printed = `<logic to print this object doesn't exist>`;
if (!objectToPrint) {
printed = `${objectToPrint}`;
} else if (typeof objectToPrint === 'object') {
// Proxies throw an exception when toString is called, so we need to check this first
if (typeof (<any>objectToPrint).on === 'function') {
// This is a noice-json-rpc proxy
printed = 'CDTP Proxy';
} else {
// This if is actually unnecesary, the previous if (!objectToPrint) { does the same thing. For some reason the typescript compiler cannot infer the type from that if
// so we just write this code to leave the compiler happy
// TODO: Sync with the typescript team and figure out how to remove this
if (!objectToPrint) {
printed = `${objectToPrint}`;
} else {
const toString = objectToPrint.toString();
if (toString !== '[object Object]') {
printed = toString;
} else if (isJSONObject(objectToPrint)) {
printed = JSON.stringify(objectToPrint);
} else if (objectToPrint.constructor === Object) {
printed = fallbackPrintDescription(objectToPrint);
} else {
printed = `${objectToPrint}(${objectToPrint.constructor.name})`;
}
}
}
} else if (typeof objectToPrint === 'function') {
if (objectToPrint.name) {
printed = objectToPrint.name;
} else {
const functionSourceCode = objectToPrint.toString();
// Find param => or (param1, param2)
const parenthesisIndex = _.findIndex(
functionSourceCode,
character => character === ')' || character === '=',
);
const functionParameters = functionSourceCode.substr(
functionSourceCode[0] === '(' ? 1 : 0,
parenthesisIndex - 1,
);
printed = `Anonymous function: ${functionParameters}`;
}
} else {
printed = `${objectToPrint}`;
}
return printed;
}
function isJSONObject(objectToPrint: any): boolean {
if (objectToPrint.constructor === Object) {
const values = _.values(objectToPrint);
return values.every(value => !value || value.constructor === Object);
} else {
return false;
}
}
function printFirstLevelProperties(objectToPrint: any): string {
const printedProeprties = Object.keys(objectToPrint).map(
key => `${key}: ${printObjectDescription(objectToPrint[key])}`,
);
return `{ ${printedProeprties.join(', ')} }`;
}

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

@ -1,7 +0,0 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/
export interface IEquivalenceComparable {
isEquivalentTo(right: this): boolean;
}

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

@ -1,22 +0,0 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/
/**
* Type utilities to construct derived types from the original types, rather than have to manually write them
*/
export type MakePropertyRequired<T, K extends keyof T> = T & { [P in K]-?: T[K] };
export type RemoveProperty<T, K> = Pick<T, Exclude<keyof T, K>>;
export type SpecializeProperty<T, K extends keyof T, S extends T[K]> = T & { [P in K]: S };
export function isNotUndefined<T>(object: T | undefined): object is T {
return object !== undefined;
}
export interface Array<T> {
filter<U extends T>(predicate: (element: T) => element is U): U[];
}
export type Replace<T, R extends keyof T, N> = {
[K in keyof T]: K extends R ? N : T[K];
};

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

@ -1,30 +0,0 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/
export function zeroOrPositive(name: string, value: number) {
if (value < 0) {
breakWhileDebugging();
throw new Error(
`Expected ${name} to be either zero or a positive number and instead it was ${value}`,
);
}
}
/** Used for debugging while developing to automatically break when something unexpected happened */
export function breakWhileDebugging() {
if (process.env.BREAK_WHILE_DEBUGGING === 'true') {
// tslint:disable-next-line:no-debugger
debugger;
}
}
export function notNullNorUndefinedElements(name: string, array: unknown[]): void {
const index = array.findIndex(element => element === null || element === undefined);
if (index >= 0) {
breakWhileDebugging();
throw new Error(
`Expected ${name} to not have any null or undefined elements, yet the element at #${index} was ${array[index]}`,
);
}
}

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

@ -1,48 +0,0 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/
import { testUsing } from '../fixtures/testUsing';
import { TestProjectSpec } from '../framework/frameworkTestSupport';
import { LaunchProject } from '../fixtures/launchProject';
import { DebugProtocol } from 'vscode-debugprotocol';
import { isWindows } from '../testSetup';
import { onUnhandledException } from '../utils/onUnhandledException';
import * as testUtils from '../testUtils';
const testSpec = TestProjectSpec.fromTestPath('featuresTests/attachNoUrl');
let waitForOutput: testUtils.IDeferred<void>;
// TODO: The attach test is currently failing on MAC. We need to investigate it and fix it
((isWindows ? testUsing : testUsing.skip) as typeof testUsing)(
'Attach without specifying an url parameter',
async context => {
waitForOutput = await testUtils.getDeferred();
return LaunchProject.attach(context, testSpec, undefined, {
registerListeners: client => {
// This test tests 2 different things while attaching:
// 1. We don't get an unhandled error while attaching (due to Runtime.consoleAPICalled being called with a scriptId that hasn't been parsed yet)
onUnhandledException(client, exceptionMessage =>
waitForOutput.reject(new Error(exceptionMessage)),
);
client.on('output', (args: DebugProtocol.OutputEvent) => {
// 2. We eventually see this console.log message, because we attached succesfully to the web-page
if (
args.body.category === 'stdout' &&
args.body.output.startsWith('If you see this message, you are attached...')
) {
// Wait 1 second to see if any unhandled errors happen while attaching to the page
testUtils.promiseTimeout(undefined, 1000).then(() => {
waitForOutput.resolve();
});
}
});
},
});
},
async () => {
await waitForOutput.promise;
},
);

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

@ -1,24 +0,0 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/
import { testUsing } from '../fixtures/testUsing';
import { TestProjectSpec } from '../framework/frameworkTestSupport';
import { LaunchProject } from '../fixtures/launchProject';
testUsing.skip(
'Hit breakpoint on JavaScript when source map is invalid',
context =>
LaunchProject.launch(context, TestProjectSpec.fromTestPath('featuresTests/invalidSourceMap')),
async launchProject => {
const runCodeButton = await launchProject.page.waitForSelector('#runCode');
const breakpoint = await launchProject.breakpoints
.at('../app.js')
.breakpoint({ text: `console.log('line 5');` });
await breakpoint.assertIsHitThenResumeWhen(() => runCodeButton.click(), {
stackTrace: `
runCode [app.js] Line 11:5 // Because the source-map is invalid we hit in app.js:11:5 instead of app.ts:5`,
});
},
);

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

@ -1,38 +0,0 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/
import * as _ from 'lodash';
import { testUsing } from '../fixtures/testUsing';
import { LaunchProject } from '../fixtures/launchProject';
import { TestProjectSpec } from '../framework/frameworkTestSupport';
import { readFileP, writeFileP } from '../testUtils';
suite('Unusual source-maps', () => {
testUsing(
`file:/// url in sources' field`,
async context => {
const testSpec = TestProjectSpec.fromTestPath(
'featuresTests/unusualSourceMaps/fileUrlInSources',
);
// Update source-map to have a file:/// url in the sources field
const sourceMapPath = testSpec.src('../app.js.map');
const sourceMapContents = await readFileP(sourceMapPath);
const sourceMapJSON = JSON.parse(sourceMapContents.toString());
sourceMapJSON['sources'] = [`file:///${testSpec.src('../app.ts').replace(/\\/g, '/')}`];
await writeFileP(sourceMapPath, JSON.stringify(sourceMapJSON));
return LaunchProject.launch(context, testSpec);
},
async launchProject => {
const executeActionButton = await launchProject.page.waitForSelector('#executeAction');
const buttonClickedBreakpoint = await launchProject.breakpoints
.at('../app.ts')
.breakpoint({ text: `console.log('You clicked the button');` });
await buttonClickedBreakpoint.assertIsHitThenResumeWhen(() => executeActionButton.click());
},
);
});

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

@ -1,150 +0,0 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/
/*
* 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 } from '../wizards/breakpoints/breakpointsWizard';
import { asyncRepeatSerially } from '../utils/repeat';
puppeteerSuite.skip(
'Hit count breakpoints on a React project',
reactTestSpecification,
suiteContext => {
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({
text: 'this.setState({ count: newval });',
boundText: 'setState({ count: newval })',
hitCountCondition: '% 3',
});
await asyncRepeatSerially(2, () => incBtn.click());
await setStateBreakpoint.assertIsHitThenResumeWhen(() => incBtn.click());
await incBtn.click();
await breakpoints.waitAndAssertNoMoreEvents();
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({
text: 'this.setState({ count: newval })',
boundText: 'setState({ count: newval })',
hitCountCondition: '= 3',
});
const setNewValBreakpoint = await counterBreakpoints.hitCountBreakpoint({
text: 'const newval = this.state.count + 1',
boundText: 'state.count + 1',
hitCountCondition: '= 5',
});
const stepInBreakpoint = await counterBreakpoints.hitCountBreakpoint({
text: 'this.stepIn()',
boundText: 'stepIn()',
hitCountCondition: '= 4',
});
await asyncRepeatSerially(2, () => incBtn.click());
await setStateBreakpoint.assertIsHitThenResumeWhen(() => incBtn.click());
await stepInBreakpoint.assertIsHitThenResumeWhen(() => incBtn.click());
await setNewValBreakpoint.assertIsHitThenResumeWhen(() => incBtn.click());
await incBtn.click();
await breakpoints.waitAndAssertNoMoreEvents();
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({
text: 'this.setState({ count: newval });',
boundText: 'setState({ count: newval })',
hitCountCondition: '= 3',
}),
setNewValBreakpoint: await counterBreakpoints.hitCountBreakpoint({
text: 'const newval = this.state.count + 1',
boundText: 'state.count + 1',
hitCountCondition: '= 5',
}),
stepInBreakpoint: await counterBreakpoints.hitCountBreakpoint({
text: 'this.stepIn();',
boundText: 'stepIn()',
hitCountCondition: '= 4',
}),
}));
await asyncRepeatSerially(2, () => incBtn.click());
await setStateBreakpoint.assertIsHitThenResumeWhen(() => incBtn.click());
await stepInBreakpoint.assertIsHitThenResumeWhen(() => incBtn.click());
await setNewValBreakpoint.assertIsHitThenResumeWhen(() => incBtn.click());
await incBtn.click();
await breakpoints.waitAndAssertNoMoreEvents();
await counterBreakpoints.batch(async () => {
await setStateBreakpoint.unset();
await setNewValBreakpoint.unset();
await stepInBreakpoint.unset();
});
},
);
},
);

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

@ -1,195 +0,0 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/
/*
* Hit count breakpoints' scenarios
* Hit count breakpoint syntax: (>|>=|=|<|<=|%)?\s*([0-9]+)
*/
import * as _ from 'lodash';
import { puppeteerSuite, puppeteerTest } from '../puppeteer/puppeteerSuite';
import { reactWithLoopTestSpecification } from '../resources/resourceProjects';
import { BreakpointsWizard } from '../wizards/breakpoints/breakpointsWizard';
puppeteerSuite(
'Hit count breakpoints combinations',
reactWithLoopTestSpecification,
suiteContext => {
interface IConditionConfiguration {
condition: string; // The condition for the hit count breakpoint
iterationsExpectedToPause: number[]; // In which iteration numbers it should pause (e.g.: 1st, 5th, 12th, etc...)
noMorePausesAfterwards: boolean;
}
// * Hit count breakpoint syntax: (>|>=|=|<|<=|%)?\s*([0-9]+)
const manyConditionsConfigurations: IConditionConfiguration[] = [
{ condition: '= 0', iterationsExpectedToPause: [], noMorePausesAfterwards: true },
{ condition: '= 1', iterationsExpectedToPause: [1], noMorePausesAfterwards: true },
{ condition: '= 2', iterationsExpectedToPause: [2], noMorePausesAfterwards: true },
{ condition: '= 12', iterationsExpectedToPause: [12], noMorePausesAfterwards: true },
{
condition: '> 0',
iterationsExpectedToPause: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
noMorePausesAfterwards: false,
},
{
condition: '> 1',
iterationsExpectedToPause: [2, 3, 4, 5, 6, 7, 8, 9, 10],
noMorePausesAfterwards: false,
},
{
condition: '>\t2',
iterationsExpectedToPause: [3, 4, 5, 6, 7, 8, 9, 10],
noMorePausesAfterwards: false,
},
{
condition: '> 187',
iterationsExpectedToPause: [188, 189, 190, 191],
noMorePausesAfterwards: false,
},
{
condition: '>= 0',
iterationsExpectedToPause: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
noMorePausesAfterwards: false,
},
{
condition: '>= 1',
iterationsExpectedToPause: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
noMorePausesAfterwards: false,
},
{
condition: '>= 2',
iterationsExpectedToPause: [2, 3, 4, 5, 6, 7, 8, 9, 10],
noMorePausesAfterwards: false,
},
{
condition: '>= 37',
iterationsExpectedToPause: [37, 38, 39],
noMorePausesAfterwards: false,
},
{ condition: '< 0', iterationsExpectedToPause: [], noMorePausesAfterwards: true },
{ condition: '< \t \t 1', iterationsExpectedToPause: [], noMorePausesAfterwards: true },
{ condition: '< 2', iterationsExpectedToPause: [1], noMorePausesAfterwards: true },
{
condition: '< \t13',
iterationsExpectedToPause: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
noMorePausesAfterwards: true,
},
{ condition: '<=\t 0', iterationsExpectedToPause: [], noMorePausesAfterwards: true },
{ condition: '<= 1', iterationsExpectedToPause: [1], noMorePausesAfterwards: true },
{
condition: '<= 15',
iterationsExpectedToPause: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
noMorePausesAfterwards: true,
},
{ condition: '% 0', iterationsExpectedToPause: [], noMorePausesAfterwards: true },
{
condition: '% 1',
iterationsExpectedToPause: [1, 2, 3, 4, 5, 6],
noMorePausesAfterwards: false,
},
{
condition: '% 2',
iterationsExpectedToPause: [2, 4, 6, 8, 10],
noMorePausesAfterwards: false,
},
{
condition: '%\t3',
iterationsExpectedToPause: [3, 6, 9, 12, 15],
noMorePausesAfterwards: false,
},
{
condition: '% \t \t \t 12',
iterationsExpectedToPause: [12, 24, 36, 48, 60],
noMorePausesAfterwards: false,
},
{
condition: '%\t\t\t17',
iterationsExpectedToPause: [17, 34, 51, 68],
noMorePausesAfterwards: false,
},
{
condition: '% 37',
iterationsExpectedToPause: [37, 74, 111, 148],
noMorePausesAfterwards: false,
},
];
manyConditionsConfigurations.forEach(conditionConfiguration => {
puppeteerTest.skip(
`condition ${conditionConfiguration.condition}`,
suiteContext,
async (_context, page) => {
const incBtn = await page.waitForSelector('#incrementBtn');
const breakpoints = BreakpointsWizard.create(
suiteContext.debugClient,
reactWithLoopTestSpecification,
);
const counterBreakpoints = breakpoints.at('Counter.jsx');
const setStateBreakpoint = await counterBreakpoints.hitCountBreakpoint({
text: 'iterationNumber * iterationNumber',
hitCountCondition: conditionConfiguration.condition,
});
const buttonClicked = incBtn.click();
for (const nextIterationToPause of conditionConfiguration.iterationsExpectedToPause) {
/**
* The iterationNumber variable counts in the js-debuggee code how many times the loop was executed. We verify
* the value of this variable to validate that a bp with = 12 paused on the 12th iteration rather than on the 1st one
* (The breakpoint is located in the same place in both iterations, so we need to use state to differenciate between those two cases)
*/
await setStateBreakpoint.assertIsHitThenResume({
variables: { local_contains: { iterationNumber: nextIterationToPause } },
});
}
// logger.log(`No more pauses afterwards = ${conditionConfiguration.noMorePausesAfterwards}`); // TODO@rob
if (conditionConfiguration.noMorePausesAfterwards) {
await breakpoints.waitAndAssertNoMoreEvents();
await setStateBreakpoint.unset();
} else {
await breakpoints.waitAndConsumePausedEvent(setStateBreakpoint);
await setStateBreakpoint.unset();
await breakpoints.resume();
}
await buttonClicked;
},
);
});
// * Hit count breakpoint syntax: (>|>=|=|<|<=|%)?\s*([0-9]+)
const manyInvalidConditions: string[] = [
'== 3',
'= -1',
'> -200',
'< -24',
'>= -95',
'<= -5',
'< = 4',
'% -200',
'stop always',
'= 1 + 1',
'> 3.5',
];
manyInvalidConditions.forEach(invalidCondition => {
puppeteerTest.skip(`invalid condition ${invalidCondition}`, suiteContext, async () => {
const breakpoints = BreakpointsWizard.create(
suiteContext.debugClient,
reactWithLoopTestSpecification,
);
const counterBreakpoints = breakpoints.at('Counter.jsx');
await counterBreakpoints.unverifiedHitCountBreakpoint({
text: 'iterationNumber * iterationNumber',
hitCountCondition: invalidCondition,
unverifiedReason: `Didn't recognize <${invalidCondition}> as a valid hit count condition`,
});
});
});
},
);

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

@ -1,45 +0,0 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/
import * as testSetup from '../testSetup';
import { puppeteerSuite } from '../puppeteer/puppeteerSuite';
import { TestProjectSpec } from '../framework/frameworkTestSupport';
import { FrameworkTestSuite } from '../framework/frameworkCommonTests';
import * as path from 'path';
import { pathToFileURL } from '../testUtils';
const SINGLE_INLINE_TEST_SPEC = TestProjectSpec.fromTestPath(
'inline_scripts',
'',
pathToFileURL(path.join(testSetup.DATA_ROOT, 'inline_scripts/single.html')),
);
const MULTIPLE_INLINE_TEST_SPEC = TestProjectSpec.fromTestPath(
'inline_scripts',
'',
pathToFileURL(path.join(testSetup.DATA_ROOT, 'inline_scripts/multiple.html')),
);
suite('Inline Script Tests', () => {
puppeteerSuite('Single inline script', SINGLE_INLINE_TEST_SPEC, suiteContext => {
const frameworkTests = new FrameworkTestSuite('Simple JS', suiteContext);
frameworkTests.testBreakpointHitsOnPageAction(
'Should stop on a breakpoint in an in-line script',
'#actionButton',
'single.html',
'a + b;',
page => page.click('#actionButton'),
);
});
puppeteerSuite.skip('Multiple inline scripts', MULTIPLE_INLINE_TEST_SPEC, suiteContext => {
const frameworkTests = new FrameworkTestSuite('Simple JS', suiteContext);
frameworkTests.testBreakpointHitsOnPageAction(
'Should stop on a breakpoint in multiple in-line scripts (Skipped, not currently working in V2)',
'#actionButton',
'multiple.html',
'inlineScript1',
page => page.click('#actionButton'),
);
});
});

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

@ -1,44 +0,0 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/
import { testUsing } from '../fixtures/testUsing';
import { TestProjectSpec } from '../framework/frameworkTestSupport';
import { LaunchProject } from '../fixtures/launchProject';
import { onUnhandledException, onHandledError } from '../utils/onUnhandledException';
import { promiseTimeout, getDeferred, IDeferred } from '../testUtils';
import { IChromeLaunchConfiguration } from '../../configuration';
let waitForTestResult: IDeferred<void>;
testUsing(
'No unhandled exceptions when we parse invalid JavaScript code. We get a handled error',
async context => {
waitForTestResult = await getDeferred<void>();
return LaunchProject.launch(
context,
TestProjectSpec.fromTestPath('featuresTests/invalidJavaScriptCode'),
{} as IChromeLaunchConfiguration,
{
registerListeners: client => {
// We fail the test if we get an unhandled exception
onUnhandledException(client, exceptionMessage =>
waitForTestResult.reject(new Error(exceptionMessage)),
);
// We expect to get a handled error instead
onHandledError(client, async errorMessage => {
if (errorMessage.startsWith(`SyntaxError: Unexpected token 'function'`)) {
// After we get the message, we wait 1 more second to verify we don't get any unhandled exceptions, and then we succeed the test
await promiseTimeout(undefined, 1000 /* 1 sec */);
waitForTestResult.resolve();
}
});
},
},
);
},
async _launchProject => {
await waitForTestResult.promise;
},
);

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

@ -1,117 +0,0 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/
import { TestProjectSpec } from '../framework/frameworkTestSupport';
import { LaunchProject } from '../fixtures/launchProject';
import { testUsing } from '../fixtures/testUsing';
import { DebugProtocol } from 'vscode-debugprotocol';
import { fail } from 'assert';
import { expect } from 'chai';
import { IChromeLaunchConfiguration } from '../../configuration';
import { readFileP } from '../testUtils';
let loadedSources: DebugProtocol.Source[] = [];
function onLoadedSource(args: DebugProtocol.LoadedSourceEvent): void {
switch (args.body.reason) {
case 'new':
// We ignore scripts added by puppeteer
if (args.body.source.name !== '__puppeteer_evaluation_script__') {
loadedSources.push(args.body.source);
}
break;
case 'changed':
case 'removed':
fail(`Only expected new loaded source events`);
break;
default:
fail(`Unrecognized loaded source reason: ${args.body.reason}`);
}
}
suite('loaded sources', () => {
setup(() => {
loadedSources = []; // Reset before each test
});
const testSpec = TestProjectSpec.fromTestPath('featuresTests/loadedSources/basicLoadedSources');
testUsing(
'we receive events for js, ts, and eval sources',
context =>
LaunchProject.launch(
context,
testSpec,
{} as IChromeLaunchConfiguration, // TODO@rob
{
registerListeners: client =>
client.on('loadedSource', args =>
onLoadedSource(<DebugProtocol.LoadedSourceEvent>args),
),
},
),
async launchProject => {
await launchProject.pausedWizard.waitUntilPausedOnDebuggerStatement();
expect(loadedSources.length).to.equal(4);
expect(loadedSources[0].name).to.equal('app.js');
expect(loadedSources[0].path).to.match(new RegExp('http://localhost:[0-9]+/app.js'));
expect(loadedSources[1].name).to.match(/VM[0-9]+/);
expect(loadedSources[1].path).to.match(/<eval>\\VM[0-9]+/);
expect(loadedSources[2].name).to.equal('jsUtilities.js');
expect(loadedSources[2].path).to.match(new RegExp('http://localhost:[0-9]+/jsUtilities.js'));
// These are the 2 inline scripts in the .html file
expect(loadedSources[3].name).to.match(new RegExp('localhost:[0-9]+'));
expect(loadedSources[3].path).to.match(new RegExp('http://localhost:[0-9]+'));
},
);
testUsing(
'can get dynamic JavaScript file source',
context =>
LaunchProject.launch(context, testSpec, {} as IChromeLaunchConfiguration, {
registerListeners: client =>
client.on('loadedSource', args => onLoadedSource(<DebugProtocol.LoadedSourceEvent>args)),
}),
async launchProject => {
await launchProject.pausedWizard.waitUntilPausedOnDebuggerStatement();
expect(loadedSources[0].name).to.equal('app.js');
const contents = await launchProject.debugClient.sourceRequest({
source: { sourceReference: loadedSources[0].sourceReference },
sourceReference: 0 /** Not used. Backwards compatibility */,
});
expect(contents.success).to.equal(true);
const appFileContents = await readFileP(testSpec.src('../app.js'));
expect(contents.body.content).to.equal(appFileContents);
},
);
testUsing(
'can get dynamic .html file source',
context =>
LaunchProject.launch(context, testSpec, {} as IChromeLaunchConfiguration, {
registerListeners: client =>
client.on('loadedSource', args => onLoadedSource(<DebugProtocol.LoadedSourceEvent>args)),
}),
async launchProject => {
await launchProject.pausedWizard.waitUntilPausedOnDebuggerStatement();
// We need to finish loading the .html file, so we can request it's source content
await launchProject.pausedWizard.resume();
expect(loadedSources[3].name).to.match(new RegExp('localhost:[0-9]+'));
const contents = await launchProject.debugClient.sourceRequest({
source: { sourceReference: loadedSources[3].sourceReference },
sourceReference: 0 /** Not used. Backwards compatibility */,
});
expect(contents.success).to.equal(true);
const appFileContents = await readFileP(testSpec.src('../index.html'));
expect(contents.body.content).to.equal(appFileContents);
},
);
});

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

@ -1,37 +0,0 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/
import { TestProjectSpec } from '../framework/frameworkTestSupport';
import { VariablesWizard } from '../wizards/variables/variablesWizard';
import { LaunchProject } from '../fixtures/launchProject';
import { testUsing } from '../fixtures/testUsing';
import { BreakpointsWizard } from '../wizards/breakpoints/breakpointsWizard';
suite('modify variable', function() {
const testSpec = TestProjectSpec.fromTestPath('featuresTests/setVariable');
testUsing(
'local',
context => LaunchProject.launch(context, testSpec),
async launchProject => {
const variables = new VariablesWizard(launchProject.debugClient);
const breakpoints = BreakpointsWizard.create(launchProject.debugClient, testSpec).at(
'../app.ts',
);
const changeShouldExitBreakpoint = await breakpoints.breakpoint({
text: `console.log('Change shouldExit value here')`,
});
const exitedPreviousFunctionBreakpoint = await breakpoints.breakpoint({
text: `console.log('We exited the previous function');`,
});
await changeShouldExitBreakpoint.assertIsHitThenResume({
action: async () => {
await variables.set('shouldExit', 'true');
},
});
await exitedPreviousFunctionBreakpoint.assertIsHitThenResume({});
},
);
});

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

@ -1,59 +0,0 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/
import { puppeteerSuite, puppeteerTest } from '../puppeteer/puppeteerSuite';
import { reactTestSpecification } from '../resources/resourceProjects';
import { BreakpointsWizard } from '../wizards/breakpoints/breakpointsWizard';
puppeteerSuite('Multiple breakpoints on a React project', reactTestSpecification, suiteContext => {
puppeteerTest.skip(
'Can hit two valid breakpoints, while we set them with an invalid hit count breakpoints',
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, setNewValBreakpoint } = await counterBreakpoints.batch(
async () => ({
stepInBreakpoint: await (
await counterBreakpoints.unsetHitCountBreakpoint({
text: 'this.stepIn();',
boundText: 'stepIn()',
hitCountCondition: 'bad bad hit count breakpoint condition = 2',
})
).setWithoutVerifying(), // We want the invalid condition to be first to see that the other 2 breakpoints are actually set
setNewValBreakpoint: await counterBreakpoints.breakpoint({
text: 'const newval = this.state.count + 1',
boundText: 'state.count + 1',
}),
setStateBreakpoint: await counterBreakpoints.breakpoint({
text: 'this.setState({ count: newval });',
boundText: 'setState({ count: newval })',
}),
}),
);
await breakpoints.assertIsHitThenResumeWhen(
[setNewValBreakpoint, setStateBreakpoint],
() => incBtn.click(),
{},
);
await breakpoints.waitAndAssertNoMoreEvents();
await counterBreakpoints.batch(async () => {
await setStateBreakpoint.unset();
await setNewValBreakpoint.unset();
});
},
);
});

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

@ -1,58 +0,0 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/
import { testUsing } from '../fixtures/testUsing';
import { TestProjectSpec } from '../framework/frameworkTestSupport';
import { LaunchProject } from '../fixtures/launchProject';
import { expect } from 'chai';
import { DebugProtocol } from 'vscode-debugprotocol';
import { IChromeLaunchConfiguration } from '../../configuration';
import { THREAD_ID } from '../testSupport/debugClient';
testUsing(
'Pause on promise rejections when unhandled exceptions are enabled',
context =>
LaunchProject.launch(
context,
TestProjectSpec.fromTestPath('featuresTests/pauseOnPromisesRejections'),
{} as IChromeLaunchConfiguration, // TODO@rob
{
configureDebuggee: debugClient =>
debugClient.setExceptionBreakpointsRequest({ filters: ['uncaught'] }),
},
),
async launchProject => {
await waitUntilPausedOnPromiseRejection(launchProject, `Things didn't go as expected`);
},
);
/** Wait and block until the debuggee is paused on an unhandled promise */
async function waitUntilPausedOnPromiseRejection(
launchProject: LaunchProject,
exceptionMessage: string,
): Promise<void> {
return launchProject.pausedWizard.waitAndConsumePausedEvent(async pauseInfo => {
expect(pauseInfo.description).to.equal('Paused on promise rejection');
expect(pauseInfo.reason).to.equal('exception');
const exceptionInfo = await launchProject.debugClient.exceptionInfoRequest({
threadId: THREAD_ID,
});
validateExceptionHasCorrectInformation(exceptionInfo, exceptionMessage);
});
}
function validateExceptionHasCorrectInformation(
exceptionInfo: DebugProtocol.ExceptionInfoResponse,
exceptionMessage: string,
): void {
expect(exceptionInfo.success).to.equal(true);
expect(exceptionInfo.body.breakMode).to.equal('unhandled');
expect(exceptionInfo.body.description).to.equal(undefined);
expect(exceptionInfo.body.details).to.not.equal(undefined);
expect(exceptionInfo.body.details!.message).to.equal(exceptionMessage);
expect(exceptionInfo.body.exceptionId).to.equal('string');
// formattedDescription is a VS-specific property
expect((<any>exceptionInfo.body.details).formattedDescription).to.equal(exceptionMessage);
}

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

@ -1,51 +0,0 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/
import { testUsing } from '../fixtures/testUsing';
import { TestProjectSpec } from '../framework/frameworkTestSupport';
import { LaunchProject } from '../fixtures/launchProject';
import { expect } from 'chai';
import * as _ from 'lodash';
import { promiseTimeout } from '../testUtils';
// There is no way to validate whether we are showing the paused overlay with puppeteer, so we look into the debug-adapter
// log and see if we sent the proper Overlay.setPausedInDebuggerMessage message
async function latestPausedOverlay(): Promise<string | undefined> {
// Wait a little to give the log file time to get written...
// Warning: If this test starts failing because 500 ms being too little time, we should change the logic to read the file, and retry a few times to see if the assertion passes eventually
// If that doesn't work either, we'll need to do something less hacky like implementing a sniffer or proxy of the protocol, and get the information directly from there instead of
// reading it from a file
await promiseTimeout(undefined, 500);
// TODO@rob
// const logFilePath = launchArgs().logFilePath!;
// const logFileContents = await readFileP(logFilePath);
// const lines = logFileContents.split('\n');
// const lastEvent = _.findLast(lines, line => line.indexOf('Overlay.setPausedInDebuggerMessage') >= 0);
// expect(lastEvent).to.not.equal(undefined);
// We are trying to match this string: Overlay.setPausedInDebuggerMessage\",\"params\":{ <contents here> }
// const matches = lastEvent!.match(/Overlay\.setPausedInDebuggerMessage\\",\\"params\\":\{([^}]*)\}/);
// expect(matches).to.not.equal(null);
// expect(matches!.length).to.equal(2);
// return matches![1];
return;
}
suite.skip('Pause overlay is shown', () => {
testUsing(
'when hitting a debugger statement',
context =>
LaunchProject.launch(context, TestProjectSpec.fromTestPath('featuresTests/pausedOverlay')),
async launchProject => {
await launchProject.pausedWizard.waitUntilPausedOnDebuggerStatement();
expect(await latestPausedOverlay()).to.equal(
`\\"message\\":\\"Paused in Visual Studio Code\\"`,
);
await launchProject.pausedWizard.resume();
expect(await latestPausedOverlay()).to.equal(''); // An empty message removes the overlay
},
);
});

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

@ -1,31 +0,0 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/
import { testUsing } from '../fixtures/testUsing';
import { TestProjectSpec } from '../framework/frameworkTestSupport';
import { LaunchProject } from '../fixtures/launchProject';
import { pathToFileURL } from '../testUtils';
const testSpec = TestProjectSpec.fromTestPath('simple');
const appPath = testSpec.src('../index.html');
// appPathUrl will have on Windows a character escaped like file:///C%3A/myproject/index.html
const appPathUrl = pathToFileURL(appPath).replace(/file:\/\/\/([a-z]):\//, 'file:///$1%3A/');
suite('Unusual launch.json', () => {
testUsing(
'Hit breakpoint when using an escape character in the url',
context => LaunchProject.launch(context, testSpec.usingStaticUrl(appPathUrl)),
async launchProject => {
// Wait for the page to load
await launchProject.page.waitForSelector('#helloWorld');
// Set a breakpoint, and reload to hit the breakpoint
const breakpoint = await launchProject.breakpoints
.at('../app.js')
.breakpoint({ text: `console.log('Very simple webpage');` });
await breakpoint.assertIsHitThenResumeWhen(() => launchProject.page.reload());
},
);
});

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

@ -1,338 +0,0 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/
import { TestProjectSpec } from '../framework/frameworkTestSupport';
import { VariablesWizard } from '../wizards/variables/variablesWizard';
import { LaunchProject } from '../fixtures/launchProject';
import { testUsing } from '../fixtures/testUsing';
// Scopes' kinds: 'global' | 'local' | 'with' | 'closure' | 'catch' | 'block' | 'script' | 'eval' | 'module'
// TODO: Test several scopes at the same time. They can be repeated, and the order does matter
suite('Variables scopes', function() {
testUsing(
'local',
context =>
LaunchProject.launch(context, TestProjectSpec.fromTestPath('variablesScopes/localScope')),
async launchProject => {
await launchProject.pausedWizard.waitUntilPausedOnDebuggerStatement();
await new VariablesWizard(launchProject.debugClient).assertTopFrameVariablesAre({
local: `
this = Window (Object)
arguments = Arguments(0) [] (Object)
b = body {text: "", link: "", vLink: "", } (Object)
bool = true (boolean)
buffer = ArrayBuffer(8) {} (Object)
buffView = Int32Array(2) [234, 0] (Object)
consoleDotLog = function consoleDotLog(m) { } (Function)
e = Error: hi (Object)
element = body {text: "", link: "", vLink: "", } (Object)
fn = () => { } (Function)
fn2 = function () { } (Function)
globalCode = "page loaded" (string)
inf = Infinity (number)
infStr = "Infinity" (string)
longStr = "this is a\nstring with\nnewlines" (string)
m = Map(1) {} (Object)
manyPropsObj = Object {0: 1, 1: 3, 2: 5, } (Object)
myVar = Object {num: 1, str: "Global", obj: Object, } (Object)
nan = NaN (number)
obj = Object {a: 2, thing: <accessor>} (Object)
qqq = undefined (undefined)
r = /^asdf.*$/g {lastIndex: 0} (Object)
s = Symbol(hi) (symbol)
str = "hello" (string)
xyz = 4 (number)`,
});
},
);
testUsing(
'globals',
context =>
LaunchProject.launch(context, TestProjectSpec.fromTestPath('variablesScopes/globalScope')),
async launchProject => {
await launchProject.pausedWizard.waitUntilPausedOnDebuggerStatement();
await new VariablesWizard(launchProject.debugClient).assertNewGlobalVariariablesAre(
async () => {
await launchProject.pausedWizard.resume();
await launchProject.pausedWizard.waitUntilPausedOnDebuggerStatement();
},
// The variables declared with const, and let aren't global variables so they won't appear here
`
b = body {text: "", link: "", vLink: "", } (Object)
bool = true (boolean)
buffer = ArrayBuffer(8) {} (Object)
buffView = Int32Array(2) [234, 0] (Object)
consoleDotLog = function consoleDotLog(m) { } (Function)
e = Error: hi (Object)
element = p {align: "", title: "", lang: "", } (Object)
evalVar1 = 16 (number)
evalVar2 = "sdlfk" (string)
evalVar3 = Array(3) [1, 2, 3] (Object)
fn = () => { } (Function)
fn2 = function () { } (Function)
globalCode = "page loaded" (string)
i = 101 (number)
inf = Infinity (number)
infStr = "Infinity" (string)
longStr = "this is a\nstring with\nnewlines" (string)
m = Map(1) {} (Object)
manyPropsObj = Object {0: 1, 1: 3, 2: 5, } (Object)
myVar = Object {num: 1, str: "Global", obj: Object, } (Object)
nan = NaN (number)
obj = Object {a: 2, thing: <accessor>} (Object)
qqq = undefined (undefined)
r = /^asdf.*$/g {lastIndex: 0} (Object) // TODO: This and other types seems wrong. Investigate
s = Symbol(hi) (symbol)
str = "hello" (string)
xyz = 4 (number)`,
);
},
);
testUsing(
'script',
context =>
LaunchProject.launch(context, TestProjectSpec.fromTestPath('variablesScopes/scriptScope')),
async launchProject => {
await launchProject.pausedWizard.waitUntilPausedOnDebuggerStatement();
await new VariablesWizard(launchProject.debugClient).assertTopFrameVariablesAre({
script: `
this = Window (Object)
b = body {text: "", link: "", vLink: "", } (Object)
bool = true (boolean)
buffer = ArrayBuffer(8) {} (Object)
buffView = Int32Array(2) [234, 0] (Object)
e = Error: hi (Object)
element = body {text: "", link: "", vLink: "", } (Object)
fn = () => { } (Function)
fn2 = function () { } (Function)
globalCode = "page loaded" (string)
inf = Infinity (number)
infStr = "Infinity" (string)
longStr = "this is a\nstring with\nnewlines" (string)
m = Map(1) {} (Object)
manyPropsObj = Object {0: 1, 1: 3, 2: 5, } (Object)
myVar = Object {num: 1, str: "Global", obj: Object, } (Object)
nan = NaN (number)
obj = Object {a: 2, thing: <accessor>} (Object)
qqq = undefined (undefined)
r = /^asdf.*$/g {lastIndex: 0} (Object)
s = Symbol(hi) (symbol)
str = "hello" (string)
xyz = 4 (number)`,
});
},
);
testUsing(
'block',
context =>
LaunchProject.launch(context, TestProjectSpec.fromTestPath('variablesScopes/blockScope')),
async launchProject => {
await launchProject.pausedWizard.waitUntilPausedOnDebuggerStatement();
await new VariablesWizard(launchProject.debugClient).assertTopFrameVariablesAre({
block: `
this = Window (Object)
b = body {text: "", link: "", vLink: "", } (Object)
bool = true (boolean)
buffer = ArrayBuffer(8) {} (Object)
buffView = Int32Array(2) [234, 0] (Object)
consoleDotLog = function consoleDotLog(m) { } (Function)
e = Error: hi (Object)
element = body {text: "", link: "", vLink: "", } (Object)
fn = () => { } (Function)
fn2 = function () { } (Function)
globalCode = "page loaded" (string)
inf = Infinity (number)
infStr = "Infinity" (string)
longStr = "this is a\nstring with\nnewlines" (string)
m = Map(1) {} (Object)
manyPropsObj = Object {0: 1, 1: 3, 2: 5, } (Object)
myVar = Object {num: 1, str: "Global", obj: Object, } (Object)
nan = NaN (number)
obj = Object {a: 2, thing: <accessor>} (Object)
qqq = undefined (undefined)
r = /^asdf.*$/g {lastIndex: 0} (Object)
s = Symbol(hi) (symbol)
str = "hello" (string)
xyz = 4 (number)`,
});
},
);
testUsing(
'catch',
context =>
LaunchProject.launch(context, TestProjectSpec.fromTestPath('variablesScopes/catchScope')),
async launchProject => {
await launchProject.pausedWizard.waitUntilPausedOnDebuggerStatement();
await new VariablesWizard(launchProject.debugClient).assertTopFrameVariablesAre({
catch: `
exception = Error: Something went wrong (Object)`,
});
},
);
testUsing(
'closure',
context =>
LaunchProject.launch(context, TestProjectSpec.fromTestPath('variablesScopes/closureScope')),
async launchProject => {
await launchProject.pausedWizard.waitUntilPausedOnDebuggerStatement();
await new VariablesWizard(launchProject.debugClient).assertTopFrameVariablesAre({
closure: `
arguments = Arguments(0) [] (Object)
b = body {text: "", link: "", vLink: "", } (Object)
bool = true (boolean)
buffer = ArrayBuffer(8) {} (Object)
buffView = Int32Array(2) [234, 0] (Object)
consoleDotLog = function consoleDotLog(m) { } (Function)
e = Error: hi (Object)
element = body {text: "", link: "", vLink: "", } (Object)
fn = () => { } (Function)
fn2 = function () { } (Function)
globalCode = "page loaded" (string)
inf = Infinity (number)
infStr = "Infinity" (string)
longStr = "this is a\nstring with\nnewlines" (string)
m = Map(1) {} (Object)
manyPropsObj = Object {0: 1, 1: 3, 2: 5, } (Object)
myVar = Object {num: 1, str: "Global", obj: Object, } (Object)
nan = NaN (number)
obj = Object {a: 2, thing: <accessor>} (Object)
pauseInside = function pauseInside() { } (Function)
qqq = undefined (undefined)
r = /^asdf.*$/g {lastIndex: 0} (Object)
s = Symbol(hi) (symbol)
str = "hello" (string)
xyz = 4 (number)`,
});
},
);
testUsing(
'eval',
context =>
LaunchProject.launch(context, TestProjectSpec.fromTestPath('variablesScopes/evalScope')),
async launchProject => {
await launchProject.pausedWizard.waitUntilPausedOnDebuggerStatement();
await new VariablesWizard(launchProject.debugClient).assertTopFrameVariablesAre({
eval: `
this = Window (Object)
b = body {text: "", link: "", vLink: "", } (Object)
bool = true (boolean)
buffer = ArrayBuffer(8) {} (Object)
buffView = Int32Array(2) [234, 0] (Object)
e = Error: hi (Object)
element = body {text: "", link: "", vLink: "", } (Object)
fn = () => { } (Function)
fn2 = function () { } (Function)
globalCode = "page loaded" (string)
inf = Infinity (number)
infStr = "Infinity" (string)
longStr = "this is a\nstring with\nnewlines" (string)
m = Map(1) {} (Object)
manyPropsObj = Object {0: 1, 1: 3, 2: 5, } (Object)
myVar = Object {num: 1, str: "Global", obj: Object, } (Object)
nan = NaN (number)
obj = Object {a: 2, thing: <accessor>} (Object)
qqq = undefined (undefined)
r = /^asdf.*$/g {lastIndex: 0} (Object)
s = Symbol(hi) (symbol)
str = "hello" (string)
xyz = 4 (number)`,
});
},
);
testUsing(
'with',
context =>
LaunchProject.launch(context, TestProjectSpec.fromTestPath('variablesScopes/withScope')),
async launchProject => {
await launchProject.pausedWizard.waitUntilPausedOnDebuggerStatement();
await new VariablesWizard(launchProject.debugClient).assertTopFrameVariablesAre({
with: `
this = Window (Object)
b = body {text: "", link: "", vLink: "", } (Object)
bool = true (boolean)
buffer = ArrayBuffer(8) {} (Object)
buffView = Int32Array(2) [234, 0] (Object)
consoleDotLog = function (m) { } (Function)
e = Error: hi (Object)
element = body {text: "", link: "", vLink: "", } (Object)
evalVar1 = 16 (number)
evalVar2 = "sdlfk" (string)
evalVar3 = Array(3) [1, 2, 3] (Object)
fn = () => { } (Function)
fn2 = function () { } (Function)
globalCode = "page loaded" (string)
i = 101 (number)
inf = Infinity (number)
infStr = "Infinity" (string)
longStr = "this is a
string with
newlines" (string)
m = Map(1) {} (Object)
manyPropsObj = Object {0: 1, 1: 3, 2: 5, } (Object)
myVar = Object {num: 1, str: "Global", obj: Object, } (Object)
nan = NaN (number)
obj = Object {a: 2, thing: <accessor>} (Object)
r = /^asdf.*$/g {lastIndex: 0} (Object)
s = Symbol(hi) (symbol)
str = "hello" (string)
xyz = 4 (number)
__proto__ = Object {constructor: , __defineGetter__: , __defineSetter__: , } (Object)`,
});
},
);
testUsing(
'module',
context =>
LaunchProject.launch(context, TestProjectSpec.fromTestPath('variablesScopes/moduleScope')),
async launchProject => {
await launchProject.pausedWizard.waitUntilPausedOnDebuggerStatement();
await new VariablesWizard(launchProject.debugClient).assertTopFrameVariablesAre({
module: `
this = undefined (undefined)
b = body {text: "", link: "", vLink: "", } (Object)
bool = true (boolean)
buffer = ArrayBuffer(8) {} (Object)
buffView = Int32Array(2) [234, 0] (Object)
consoleDotLog = function consoleDotLog(m2) { } (Function)
e = Error: hi (Object)
element = body {text: "", link: "", vLink: "", } (Object)
fn = () => { } (Function)
fn2 = function (param) { } (Function)
globalCode = "page loaded" (string)
inf = Infinity (number)
infStr = "Infinity" (string)
longStr = "this is a
string with
newlines" (string)
m = Map(1) {} (Object)
manyPropsObj = Object {0: 1, 1: 3, 2: 5, } (Object)
myVar = Object {num: 1, str: "Global", obj: Object, } (Object)
nan = NaN (number)
obj = Object {a: 2, thing: <accessor>} (Object)
qqq = undefined (undefined)
r = /^asdf.*$/g {lastIndex: 0} (Object)
s = Symbol(hi) (symbol)
str = "hello" (string)
xyz = 4 (number)`,
});
},
);
});

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

@ -1,41 +0,0 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/
import * as testSetup from '../testSetup';
import { IFixture } from './fixture';
import { IBeforeAndAfterContext, ITestCallbackContext } from 'mocha';
import { ExtendedDebugClient } from '../testSupport/debugClient';
/**
* Default set up for all our tests. We expect all our tests to need to do this setup
* which includes configure the debug adapter, logging, etc...
*/
export class DefaultFixture implements IFixture {
private constructor(public readonly debugClient: ExtendedDebugClient) {
// Running tests on CI can time out at the default 5s, so we up this to 15s
debugClient.defaultTimeout = 15000;
}
/** Create a new fixture using the provided setup context */
public static async create(
context: IBeforeAndAfterContext | ITestCallbackContext,
): Promise<DefaultFixture> {
return new DefaultFixture(await testSetup.setup(context));
}
/** Create a new fixture using the full title of the test case currently running */
public static async createWithTitle(testTitle: string): Promise<DefaultFixture> {
return new DefaultFixture(await testSetup.setupWithTitle(testTitle));
}
public async cleanUp(): Promise<void> {
// logger.log(`Default test clean-up`); // TODO@rob
await testSetup.teardown();
// logger.log(`Default test clean-up finished`); // TODO@rob
}
public toString(): string {
return `DefaultFixture`;
}
}

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

@ -1,27 +0,0 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/
import { PromiseOrNot } from '../testUtils';
/**
* See https://en.wikipedia.org/wiki/Test_fixture for more context
*/
/**
* A fixture represents a particular piece of set up of the context, or the environment or
* the configuration needed for a test or suite to run.
* The fixture should make those changes during it's constructor or static constructor method,
* and it'll "clean up" those changes with the cleanUp method
*/
export interface IFixture {
/** Clean-up the context, or changes made by the fixture */
cleanUp(): PromiseOrNot<void>;
}
/**
* A fixture representing that no setup is needed
*/
export class NullFixture implements IFixture {
public cleanUp(): void {}
}

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

@ -1,115 +0,0 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/
import { IFixture } from './fixture';
import { DefaultFixture } from './defaultFixture';
import { LaunchWebServer, ProvideStaticUrl } from './launchWebServer';
import { Page, Browser } from 'puppeteer';
import { ITestCallbackContext, IBeforeAndAfterContext } from 'mocha';
import { URL } from 'url';
import { IChromeLaunchConfiguration, IChromeAttachConfiguration } from '../../configuration';
import { ExtendedDebugClient } from '../testSupport/debugClient';
import { PausedWizard } from '../wizards/pausedWizard';
import { BreakpointsWizard } from '../wizards/breakpoints/breakpointsWizard';
import { LaunchPuppeteer } from '../puppeteer/launchPuppeteer';
import { TestProjectSpec } from '../framework/frameworkTestSupport';
import { IDebugAdapterCallbacks, IScenarioConfiguration } from '../intTestSupport';
/** Perform all the steps neccesary to launch a particular project such as:
* - Default fixture/setup
* - Launch web-server
* - Connect puppeteer to Chrome
*/
export class LaunchProject implements IFixture {
private constructor(
private readonly _defaultFixture: DefaultFixture,
private readonly _launchWebServer: LaunchWebServer | ProvideStaticUrl,
public readonly pausedWizard: PausedWizard,
public readonly breakpoints: BreakpointsWizard,
private readonly _launchPuppeteer: LaunchPuppeteer,
) {}
public static async launch(
testContext: IBeforeAndAfterContext | ITestCallbackContext,
testSpec: TestProjectSpec,
launchConfig: IChromeLaunchConfiguration = {} as IChromeLaunchConfiguration,
callbacks: IDebugAdapterCallbacks = {},
): Promise<LaunchProject> {
return this.start(testContext, testSpec, { ...launchConfig, scenario: 'launch' }, callbacks);
}
public static async attach(
testContext: IBeforeAndAfterContext | ITestCallbackContext,
testSpec: TestProjectSpec,
attachConfig: IChromeAttachConfiguration = { port: 0 } as IChromeAttachConfiguration,
callbacks: IDebugAdapterCallbacks = {},
): Promise<LaunchProject> {
return this.start(testContext, testSpec, { ...attachConfig, scenario: 'attach' }, callbacks);
}
public static async start(
testContext: IBeforeAndAfterContext | ITestCallbackContext,
testSpec: TestProjectSpec,
daConfig: IScenarioConfiguration,
callbacks: IDebugAdapterCallbacks,
): Promise<LaunchProject> {
const launchWebServer = testSpec.staticUrl
? new ProvideStaticUrl(new URL(testSpec.staticUrl), testSpec)
: await LaunchWebServer.launch(testSpec);
const defaultFixture = await DefaultFixture.create(testContext);
// We need to create the PausedWizard before launching the debuggee to listen to all events and avoid race conditions
const pausedWizard = PausedWizard.forClient(defaultFixture.debugClient);
const breakpointsWizard = BreakpointsWizard.createWithPausedWizard(
defaultFixture.debugClient,
pausedWizard,
testSpec,
);
const chromeArgsForPuppeteer =
daConfig.scenario === 'attach' ? [launchWebServer.url.toString()] : []; // For attach we need to launch puppeteer/chrome pointing to the web-server
const launchConfig = { ...launchWebServer.launchConfig };
const launchPuppeteer = await LaunchPuppeteer.start(
defaultFixture.debugClient,
{ ...launchConfig, ...daConfig },
chromeArgsForPuppeteer,
callbacks,
);
return new LaunchProject(
defaultFixture,
launchWebServer,
pausedWizard,
breakpointsWizard,
launchPuppeteer,
);
}
/** Client for the debug adapter being used for this test */
public get debugClient(): ExtendedDebugClient {
return this._defaultFixture.debugClient;
}
/** Object to control the debugged browser via puppeteer */
public get browser(): Browser {
return this._launchPuppeteer.browser;
}
/** Object to control the debugged page via puppeteer */
public get page(): Page {
return this._launchPuppeteer.page;
}
public get url(): URL {
return this._launchWebServer.url;
}
public async cleanUp(): Promise<void> {
await this.pausedWizard.waitAndAssertNoMoreEvents();
await this._defaultFixture.cleanUp(); // Disconnect the debug-adapter first
await this._launchPuppeteer.cleanUp(); // Then disconnect puppeteer and close chrome
await this._launchWebServer.cleanUp(); // Finally disconnect the web-server
}
}

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

@ -1,90 +0,0 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/
import { createServer } from 'http-server';
import { IFixture } from './fixture';
import { URL } from 'url';
import { HttpOrHttpsServer } from '../types/server';
import { TestProjectSpec } from '../framework/frameworkTestSupport';
import { IChromeLaunchConfiguration } from '../../configuration';
import { AddressInfo } from 'net';
async function createServerAsync(root: string): Promise<HttpOrHttpsServer> {
const server = createServer({ root });
return await new Promise((resolve, reject) => {
// logger.log(`About to launch web-server on: ${root}`);
server.listen(0, '127.0.0.1', function(this: HttpOrHttpsServer, error?: any) {
if (error) {
reject(error);
} else {
// logger.log(`Web-server on: ${root} listening on: ${JSON.stringify(this.address())}`);
resolve(this); // We return the this pointer which is the internal server object, which has access to the .address() method
}
});
});
}
async function closeServer(server: HttpOrHttpsServer): Promise<void> {
// logger.log(`Closing web-server`);
await new Promise((resolve, reject) => {
server.close((error?: any) => {
if (error) {
// logger.log('Error closing server in teardown: ' + (error && error.message));
reject(error);
} else {
resolve();
}
});
});
// logger.log(`Web-server closed`);
}
/**
* Launch a web-server for the test project listening on the default port
*/
export class LaunchWebServer implements IFixture {
private constructor(
private readonly _server: HttpOrHttpsServer,
public readonly testSpec: TestProjectSpec,
) {}
public static async launch(testSpec: TestProjectSpec): Promise<LaunchWebServer> {
return new LaunchWebServer(await createServerAsync(testSpec.props.webRoot), testSpec);
}
public get url(): URL {
return new URL(`http://localhost:${this.port}/`);
}
public get launchConfig(): IChromeLaunchConfiguration {
return Object.assign({}, this.testSpec.props.launchConfig, {
url: this.url.toString(),
}) as IChromeLaunchConfiguration; // TODO@rob
}
public get port(): number {
const address = this._server.address();
return (address as AddressInfo).port;
}
public async cleanUp(): Promise<void> {
await closeServer(this._server);
}
public toString(): string {
return `LaunchWebServer`;
}
}
export class ProvideStaticUrl implements IFixture {
public constructor(public readonly url: URL, public readonly testSpec: TestProjectSpec) {}
public get launchConfig(): IChromeLaunchConfiguration {
return {
...this.testSpec.props.launchConfig,
url: this.url.href,
} as IChromeLaunchConfiguration; // TODO@rob
}
cleanUp() {}
}

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

@ -1,19 +0,0 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/
import { IFixture } from './fixture';
import { asyncMap } from '../core-v2/chrome/collections/async';
/** Combine multiple fixtures into a single fixture, for easier management (e.g. you just need to call a single cleanUp method) */
export class MultipleFixtures implements IFixture {
private readonly _fixtures: IFixture[];
public constructor(...fixtures: IFixture[]) {
this._fixtures = fixtures;
}
public async cleanUp(): Promise<void> {
await asyncMap(this._fixtures, fixture => fixture.cleanUp());
}
}

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

@ -1,38 +0,0 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/
import { IFixture } from './fixture';
import { ITestCallbackContext } from 'mocha';
import { PromiseOrNot } from '../testUtils';
/** Run a test doing the setup/cleanup indicated by the provided fixtures */
function testUsingFunction<T extends IFixture>(
expectation: string,
fixtureProvider: (context: ITestCallbackContext) => PromiseOrNot<T>,
testFunction: (fixtures: T) => Promise<void>,
): void {
suite(expectation, function() {
let fixture: T | undefined;
test(expectation, async function() {
fixture = await fixtureProvider(this);
await testFunction(fixture);
});
teardown(() => {
if (fixture) {
return fixture.cleanUp();
}
});
});
}
testUsingFunction.skip = <T extends IFixture>(
expectation: string,
_fixtureProvider: (context: ITestCallbackContext) => PromiseOrNot<T>,
_testFunction: (fixtures: T) => Promise<void>,
) =>
test.skip(expectation, () => {
throw new Error(`We don't expect this to be called`);
});
export const testUsing = testUsingFunction;

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

@ -1 +0,0 @@
This folder contains tests that apply to specific frameworks or project types (e.g. React, Angular, Vue, etc.)

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

@ -1,76 +0,0 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/
/*
* Integration tests for the React framework
*/
import * as path from 'path';
import * as testSetup from '../testSetup';
import { setBreakpoint, setConditionalBreakpoint } from '../intTestSupport';
import { FrameworkTestSuite } from './frameworkCommonTests';
import { TestProjectSpec } from './frameworkTestSupport';
import { puppeteerSuite, puppeteerTest } from '../puppeteer/puppeteerSuite';
const DATA_ROOT = testSetup.DATA_ROOT;
const REACT_PROJECT_ROOT = path.join(DATA_ROOT, 'react', 'dist');
const TEST_SPEC = new TestProjectSpec({ projectRoot: REACT_PROJECT_ROOT });
// This test doesn't use puppeteer, so we leave it outside the suite
// testBreakOnLoad('React', TEST_SPEC, 'react_App_render'); // TODO@rob break on load
puppeteerSuite.skip('React Framework Tests', TEST_SPEC, suiteContext => {
// Need Chrome attach mode
suite('Common Framework Tests', () => {
const frameworkTests = new FrameworkTestSuite('React', suiteContext);
frameworkTests.testPageReloadBreakpoint('react_App_render');
frameworkTests.testPauseExecution();
frameworkTests.testStepOver('react_Counter_increment');
frameworkTests.testStepOut('react_Counter_increment', 'react_Counter_stepOut');
frameworkTests.testStepIn('react_Counter_stepInStop', 'react_Counter_stepIn');
});
suite('React specific tests', () => {
puppeteerTest('Should hit breakpoint in .jsx file', suiteContext, async (_context, page) => {
const pausedWizard = suiteContext.launchProject!.pausedWizard;
const location = suiteContext.breakpointLabels.get('react_Counter_increment');
const incBtn = await page.waitForSelector('#incrementBtn');
await setBreakpoint(suiteContext.debugClient, location);
const clicked = incBtn.click();
await suiteContext.debugClient.assertStoppedLocation('breakpoint', location);
await pausedWizard.waitAndConsumePausedEvent(() => {});
await pausedWizard.resume();
await clicked;
});
puppeteerTest(
'Should hit conditional breakpoint in .jsx file',
suiteContext,
async (_context, page) => {
const pausedWizard = suiteContext.launchProject!.pausedWizard;
const location = suiteContext.breakpointLabels.get('react_Counter_increment');
const incBtn = await page.waitForSelector('#incrementBtn');
await setConditionalBreakpoint(suiteContext.debugClient, location, 'this.state.count == 2');
// click 3 times, state will be = 2 on the third click
await incBtn.click();
await incBtn.click();
// don't await the last click, as the stopped debugger will deadlock it
const clicked = incBtn.click();
await suiteContext.debugClient.assertStoppedLocation('breakpoint', location);
await pausedWizard.waitAndConsumePausedEvent(() => {});
// Be sure to await the continue request, otherwise sometimes the last click promise will
// be rejected because the chrome instance is closed before it completes.
await pausedWizard.resume();
await clicked;
},
);
});
});

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

@ -1,24 +0,0 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/
import { loadProjectLabels } from '../labels';
import { expect } from 'chai';
import * as path from 'path';
suite('Test framework tests', () => {
test('Should correctly find breakpoint labels in test source files', async () => {
const labels = await loadProjectLabels('./testdata');
const worldLabel = labels.get('WorldLabel');
expect(worldLabel.path).to.eql(path.join('testdata', 'labelTest.ts'));
expect(worldLabel.line).to.eql(9);
});
test('Should correctly find block comment breakpoint labels in test source files', async () => {
const labels = await loadProjectLabels('./testdata');
const blockLabel = labels.get('blockLabel');
expect(blockLabel.path).to.eql(path.join('testdata', 'labelTest.ts'));
expect(blockLabel.line).to.eql(10);
});
});

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

@ -1,235 +0,0 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/
import { setBreakpoint } from '../intTestSupport';
import { LaunchWebServer } from '../fixtures/launchWebServer';
import { loadProjectLabels } from '../labels';
import { TestProjectSpec } from './frameworkTestSupport';
import { DefaultFixture } from '../fixtures/defaultFixture';
import { MultipleFixtures } from '../fixtures/multipleFixtures';
import * as puppeteer from 'puppeteer';
import { BreakpointsWizard } from '../wizards/breakpoints/breakpointsWizard';
import { PuppeteerTestContext, puppeteerTest } from '../puppeteer/puppeteerSuite';
import { PausedWizard } from '../wizards/pausedWizard';
/**
* A common framework test suite that allows for easy (one-liner) testing of various
* functionality in different framework projects (note: this isn't a suite in the mocha sense, but rather
* a collection of functions that return mocha tests)
*/
export class FrameworkTestSuite {
constructor(private frameworkName: string, private suiteContext: PuppeteerTestContext) {}
private get pausedWizard(): PausedWizard {
return this.suiteContext.launchProject!.pausedWizard;
}
/**
* Test that a breakpoint set after the page loads is hit on reload
* @param bpLabel Label for the breakpoint to set
*/
testPageReloadBreakpoint(bpLabel: string) {
return puppeteerTest(
`${this.frameworkName} - Should hit breakpoint on page reload`,
this.suiteContext,
async (context, page) => {
const debugClient = context.debugClient;
const bpLocation = context.breakpointLabels.get(bpLabel);
// wait for something on the page to ensure we're fully loaded, TODO: make this more generic?
await page.waitForSelector('#incrementBtn');
await setBreakpoint(debugClient, bpLocation);
const reloaded = page.reload();
await debugClient.assertStoppedLocation('breakpoint', bpLocation);
await this.pausedWizard.waitAndConsumePausedEvent(() => {});
await debugClient.continueRequest();
await this.pausedWizard.waitAndConsumeResumedEvent();
await reloaded;
},
);
}
/**
* Test that step in command works as expected.
* @param bpLabelStop Label for the breakpoint to set
* @param bpLabelStepIn Label for the location where the 'step out' command should land us
*/
testStepIn(bpLabelStop: string, bpLabelStepIn: string) {
return puppeteerTest(
`${this.frameworkName} - Should step in correctly`,
this.suiteContext,
async (_context, page) => {
const location = this.suiteContext.breakpointLabels.get(bpLabelStop);
const stepInLocation = this.suiteContext.breakpointLabels.get(bpLabelStepIn);
// wait for selector **before** setting breakpoint to avoid race conditions against scriptParsed event
const incBtn = await page.waitForSelector('#incrementBtn');
await setBreakpoint(this.suiteContext.debugClient, location);
const clicked = incBtn.click();
await this.suiteContext.debugClient.assertStoppedLocation('breakpoint', location);
await this.pausedWizard.waitAndConsumePausedEvent(() => {});
const stopOnStep = this.suiteContext.debugClient.assertStoppedLocation(
'step',
stepInLocation,
);
await this.suiteContext.debugClient.stepInAndStop();
await this.pausedWizard.waitAndConsumeResumedEvent();
await stopOnStep;
await this.pausedWizard.waitAndConsumePausedEvent(() => {});
await this.pausedWizard.resume();
await clicked;
},
);
}
/**
* Test that step over (next) command works as expected.
* Note: currently this test assumes that next will land us on the very next line in the file.
* @param bpLabel Label for the breakpoint to set
*/
testStepOver(bpLabel: string) {
return puppeteerTest(
`${this.frameworkName} - Should step over correctly`,
this.suiteContext,
async (_context, page) => {
const location = this.suiteContext.breakpointLabels.get(bpLabel);
const incBtn = await page.waitForSelector('#incrementBtn');
await setBreakpoint(this.suiteContext.debugClient, location);
const clicked = incBtn.click();
await this.suiteContext.debugClient.assertStoppedLocation('breakpoint', location);
await this.pausedWizard.waitAndConsumePausedEvent(() => {});
const stopOnStep = this.suiteContext.debugClient.assertStoppedLocation('step', {
...location,
line: location.line + 1,
});
await this.suiteContext.debugClient.nextAndStop();
await this.pausedWizard.waitAndConsumeResumedEvent();
await stopOnStep;
await this.pausedWizard.waitAndConsumePausedEvent(() => {});
await this.pausedWizard.resume();
await clicked;
},
);
}
/**
* Test that step out command works as expected.
* @param bpLabelStop Label for the breakpoint to set
* @param bpLabelStepOut Label for the location where the 'step out' command should land us
*/
testStepOut(bpLabelStop: string, bpLabelStepOut: string) {
return puppeteerTest(
`${this.frameworkName} - Should step out correctly`,
this.suiteContext,
async (_context, page) => {
const location = this.suiteContext.breakpointLabels.get(bpLabelStop);
const stepOutLocation = this.suiteContext.breakpointLabels.get(bpLabelStepOut);
const incBtn = await page.waitForSelector('#incrementBtn');
await setBreakpoint(this.suiteContext.debugClient, location);
const clicked = incBtn.click();
await this.suiteContext.debugClient.assertStoppedLocation('breakpoint', location);
await this.pausedWizard.waitAndConsumePausedEvent(() => {});
const stopOnStep = this.suiteContext.debugClient.assertStoppedLocation(
'step',
stepOutLocation,
);
await this.suiteContext.debugClient.stepOutAndStop();
await this.pausedWizard.waitAndConsumeResumedEvent();
await stopOnStep;
await this.pausedWizard.waitAndConsumePausedEvent(() => {});
await this.pausedWizard.resume();
await clicked;
},
);
}
/**
* Test that the debug adapter can correctly pause execution
* @param bpLocation
*/
testPauseExecution() {
return puppeteerTest(
`${this.frameworkName} - Should correctly pause execution on a pause request`,
this.suiteContext,
async (_context, _page) => {
await this.pausedWizard.pause();
// TODO: Verify we are actually pausing in the expected line
await this.pausedWizard.resume();
},
);
}
/**
* A generic breakpoint test. This can be used for many different types of breakpoint tests with the following structure:
*
* 1. Wait for the page to load by waiting for the selector: `waitSelectorId`
* 2. Set a breakpoint at `bpLabel`
* 3. Execute a trigger event that should cause the breakpoint to be hit using the function `trigger`
* 4. Assert that the breakpoint is hit on the expected location, and continue
*
* @param waitSelector an html selector to identify a resource to wait for for page load
* @param bpLabel
* @param trigger
*/
testBreakpointHitsOnPageAction(
description: string,
waitSelector: string,
file: string,
bpLabel: string,
trigger: (page: puppeteer.Page) => Promise<void>,
) {
return puppeteerTest(
`${this.frameworkName} - ${description}`,
this.suiteContext,
async (context, page) => {
await page.waitForSelector(`${waitSelector}`);
const breakpoints = BreakpointsWizard.create(context.debugClient, context.testSpec);
const breakpointWizard = breakpoints.at(file);
const bp = await breakpointWizard.breakpoint({ text: bpLabel });
await bp.assertIsHitThenResumeWhen(() => trigger(page));
},
);
}
}
/**
* Test that we can stop on a breakpoint set before launch
* @param bpLabel Label for the breakpoint to set
*/
export function testBreakOnLoad(frameworkName: string, testSpec: TestProjectSpec, bpLabel: string) {
const testTitle = `${frameworkName} - Should stop on breakpoint on initial page load`;
return test(testTitle, async () => {
const defaultFixture = await DefaultFixture.createWithTitle(testTitle);
const launchWebServer = await LaunchWebServer.launch(testSpec);
const fixture = new MultipleFixtures(launchWebServer, defaultFixture);
try {
const breakpointLabels = await loadProjectLabels(testSpec.props.webRoot);
const location = breakpointLabels.get(bpLabel);
await defaultFixture.debugClient.hitBreakpointUnverified(
launchWebServer.launchConfig,
location,
);
} finally {
await fixture.cleanUp();
}
});
}

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

@ -1,143 +0,0 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/
import * as path from 'path';
const puppeteer = require('puppeteer');
import { BreakpointLocation } from '../intTestSupport';
import { IValidatedMap } from '../core-v2/chrome/collections/validatedMap';
import { DATA_ROOT } from '../testSetup';
import { IChromeTestLaunchConfiguration } from '../testUtils';
import { ExtendedDebugClient } from '../testSupport/debugClient';
/*
* A collection of supporting classes/functions for running framework tests
*/
export interface ProjectSpecProps {
/** The root directory of the test project */
projectRoot: string;
/** Source files directory of the test project */
projectSrc?: string;
/** The directory from which to host the project for a test */
webRoot?: string;
/** The outfiles directory for the test project */
outFiles?: string[];
/** The default launch configuration for the test project */
launchConfig?: Partial<IChromeTestLaunchConfiguration>; // TODO@rob
}
/**
* Specifies an integration test project (i.e. a project that will be launched and
* attached to in order to test the debug adapter)
*/
export class TestProjectSpec {
_props: Required<ProjectSpecProps>;
get props() {
return this._props;
}
/**
* @param props Parameters for the project spec. The only required param is "projectRoot", others will be set to sensible defaults
*/
constructor(props: ProjectSpecProps, public readonly staticUrl?: string) {
const outFiles = props.outFiles || [path.join(props.projectRoot, 'out')];
const webRoot = props.webRoot || props.projectRoot;
this._props = {
projectRoot: props.projectRoot,
projectSrc: props.projectSrc || path.join(props.projectRoot, 'src'),
webRoot: webRoot,
outFiles: outFiles,
launchConfig: props.launchConfig || {
// outFiles: outFiles, // TODO@rob
sourceMaps: true,
runtimeExecutable: puppeteer.executablePath(),
webRoot: webRoot,
},
};
}
/**
* Specify project by it's location relative to the testdata folder e.g.:
* - TestProjectSpec.fromTestPath('react_with_loop/dist')
* - TestProjectSpec.fromTestPath('simple')
*
* The path *can only* use forward-slahes "/" as separators
*/
public static fromTestPath(
reversedSlashesRelativePath: string,
sourceDir = 'src',
staticUrl?: string,
): TestProjectSpec {
const pathComponents = reversedSlashesRelativePath.split('/');
const projectAbsolutePath = path.join(...[DATA_ROOT].concat(pathComponents));
const projectSrc = path.join(projectAbsolutePath, sourceDir);
const props: ProjectSpecProps = { projectRoot: projectAbsolutePath, projectSrc };
return new TestProjectSpec(props, staticUrl);
}
/**
* Returns the full path to a source file
* @param filename
*/
src(filename: string) {
return path.join(this.props.projectSrc, filename);
}
public usingStaticUrl(staticUrl: string): TestProjectSpec {
return new TestProjectSpec(this.props, staticUrl);
}
}
/**
* A wrapper for all the relevant context info needed to run a debug adapter test
*/
export interface FrameworkTestContext {
/** The test project specs for the currently executing test suite */
readonly testSpec: TestProjectSpec;
/** A mapping of labels set in source files to a breakpoint location for a test */
readonly breakpointLabels: IValidatedMap<string, BreakpointLocation>;
/** The debug adapter test support client */
readonly debugClient: ExtendedDebugClient;
}
export class ReassignableFrameworkTestContext implements FrameworkTestContext {
private _wrapped: FrameworkTestContext = new NotInitializedFrameworkTestContext();
public get testSpec(): TestProjectSpec {
return this._wrapped.testSpec;
}
public get breakpointLabels(): IValidatedMap<string, BreakpointLocation> {
return this._wrapped.breakpointLabels;
}
public get debugClient(): ExtendedDebugClient {
return this._wrapped.debugClient;
}
public reassignTo(newWrapped: FrameworkTestContext): this {
this._wrapped = newWrapped;
return this;
}
}
export class NotInitializedFrameworkTestContext implements FrameworkTestContext {
public get testSpec(): TestProjectSpec {
return this.throwNotInitializedException();
}
public get breakpointLabels(): IValidatedMap<string, BreakpointLocation> {
return this.throwNotInitializedException();
}
public get debugClient(): ExtendedDebugClient {
return this.throwNotInitializedException();
}
private throwNotInitializedException(): never {
throw new Error(
`This test context hasn't been initialized yet. This is probably a bug in the tests`,
);
}
}

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

@ -1,109 +0,0 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/
/*
* This file contains support functions to make integration testing easier
*/
import { DebugClient } from 'vscode-debugadapter-testsupport';
import { PromiseOrNot } from './testUtils';
import { IChromeLaunchConfiguration, IChromeAttachConfiguration } from '../configuration';
const ImplementsBreakpointLocation = Symbol();
/**
* Simple breakpoint location params (based on what the debug test client accepts)
*/
export class BreakpointLocation {
[ImplementsBreakpointLocation]: 'BreakpointLocation';
public constructor(
/** The path to the source file in which to set a breakpoint */
public readonly path: string,
/** The line number in the file to set a breakpoint on */
public readonly line: number,
/** Optional breakpoint column */
public readonly column?: number,
/** Whether or not we should assert if the bp is verified or not */
public readonly verified?: boolean,
) {}
public toString(): string {
return `${this.path}:${this.line}:${this.column} verified: ${this.verified}`;
}
}
export type IScenarioConfiguration =
| (IChromeLaunchConfiguration & { scenario: 'launch' })
| (IChromeAttachConfiguration & { scenario: 'attach' });
export interface IDebugAdapterCallbacks {
registerListeners?: (client: DebugClient) => PromiseOrNot<unknown>;
configureDebuggee?: (client: DebugClient) => PromiseOrNot<unknown>;
}
/**
* Launch an instance of chrome and wait for the debug adapter to initialize and attach
* @param client Debug Client
* @param launchConfig The launch config to use
*/
export async function launchTestAdapter(
client: DebugClient,
launchConfig: IScenarioConfiguration,
callbacks: IDebugAdapterCallbacks,
) {
const init = client.waitForEvent('initialized');
if (callbacks.registerListeners !== undefined) {
await callbacks.registerListeners(client);
}
if (launchConfig.scenario === 'attach') {
delete launchConfig.url; // We don't want the url property when we attach
await client.initializeRequest();
await client.attachRequest(launchConfig);
} else {
await client.launch(launchConfig);
}
await init;
if (callbacks.configureDebuggee !== undefined) {
await callbacks.configureDebuggee(client);
}
await client.configurationDoneRequest();
}
/**
* Easier way to set breakpoints for testing
* @param client DebugClient
* @param location Breakpoint location
*/
export function setBreakpoint(
client: DebugClient,
location: { path: string; line: number; column?: number; verified?: boolean },
) {
return client.setBreakpointsRequest({
lines: [location.line],
breakpoints: [{ line: location.line, column: location.column }],
source: { path: location.path },
});
}
/**
* Set a conditional breakpoint in a file
* @param client DebugClient
* @param location Desired breakpoint location
* @param condition The condition on which the breakpoint should be hit
*/
export function setConditionalBreakpoint(
client: DebugClient,
location: { path: string; line: number; column?: number; verified?: boolean },
condition: string,
) {
return client.setBreakpointsRequest({
lines: [location.line],
breakpoints: [{ line: location.line, column: location.column, condition }],
source: { path: location.path },
});
}

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

@ -1,103 +0,0 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/
import { BreakpointLocation } from './intTestSupport';
import * as fs from 'fs';
import * as util from 'util';
import * as readline from 'readline';
import * as path from 'path';
import { ValidatedMap, IValidatedMap } from './core-v2/chrome/collections/validatedMap';
/*
* Contains classes and functions to find and use test breakpoint labels in test project files
*/
const readdirAsync = util.promisify(fs.readdir);
const labelRegex = /(\/\/|\/\*)\s*bpLabel:\s*(.+?)\b/;
const ignoreList = [
'node_modules',
'.git',
path.join('dist', 'out'),
path.join('testdata', 'react', 'src'),
];
/**
* A label in a source file that tells us where to put a breakpoint for a specific test
*/
export interface BreakpointLabel {
label: string;
location: BreakpointLocation;
}
/**
* Load all breakpoint labels that exist in the 'projectRoot' directory
* @param projectRoot Root directory for the test project
*/
export async function loadProjectLabels(
projectRoot: string,
): Promise<IValidatedMap<string, BreakpointLocation>> {
const labelMap = new ValidatedMap<string, BreakpointLocation>();
if (containsIgnores(projectRoot)) return labelMap;
const files = await readdirAsync(projectRoot);
for (const file of files) {
let subMap: Map<string, BreakpointLocation> | null = null;
const fullPath = path.join(projectRoot, file);
if (fs.lstatSync(fullPath).isDirectory()) {
subMap = await loadProjectLabels(fullPath);
} else {
subMap = await loadLabelsFromFile(fullPath);
}
for (const entry of subMap.entries()) {
labelMap.set(entry[0], entry[1]);
}
}
return labelMap;
}
/**
* Load breakpoint labels from a specific file
* @param filePath
*/
export async function loadLabelsFromFile(
filePath: string,
): Promise<Map<string, BreakpointLocation>> {
const fileStream = fs.createReadStream(filePath);
const labelMap = new Map<string, BreakpointLocation>();
let lineNumber = 1; // breakpoint locations start at 1
const lineReader = readline.createInterface({
input: fileStream,
});
lineReader.on('line', fileLine => {
const match = labelRegex.exec(fileLine);
if (match) {
labelMap.set(match[2], new BreakpointLocation(filePath, lineNumber));
}
lineNumber++;
});
const waitForClose = new Promise((accept, _reject) => {
lineReader.on('close', () => {
accept();
});
});
await waitForClose;
return labelMap;
}
/**
* Check if our filepath contains anything from our ignore list
* @param filePath
*/
function containsIgnores(filePath: string) {
return ignoreList.find(ignoreItem => filePath.includes(ignoreItem));
}

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

@ -1 +0,0 @@
Functions and test suite extensions to run integration tests with puppeteer on Chrome.

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

@ -1,85 +0,0 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/
import getPort from 'get-port';
import { IFixture } from '../fixtures/fixture';
import {
launchTestAdapter,
IScenarioConfiguration,
IDebugAdapterCallbacks,
} from '../intTestSupport';
import { connectPuppeteer, getPageByUrl, launchPuppeteer } from './puppeteerSupport';
import { logCallsTo } from '../utils/logging';
import { isThisV1 } from '../testSetup';
import { Browser, Page } from 'puppeteer';
import { ExtendedDebugClient } from '../testSupport/debugClient';
import * as utils from '../testUtils';
/**
* Launch the debug adapter using the Puppeteer version of chrome, and then connect to it
*
* The fixture offers access to both the browser, and page objects of puppeteer
*/
export class LaunchPuppeteer implements IFixture {
public constructor(public readonly browser: Browser, public readonly page: Page) {}
public static async start(
debugClient: ExtendedDebugClient,
daConfig: IScenarioConfiguration,
chromeArgs: string[] = [],
callbacks: IDebugAdapterCallbacks,
): Promise<LaunchPuppeteer> {
const daPort = await getPort();
// logger.log(`About to ${daConfig.scenario} debug-adapter at port: ${daPort}`); // TODO@rob
let browser: Browser;
if (daConfig.scenario === 'launch') {
await launchTestAdapter(
debugClient,
Object.assign({}, daConfig, { runtimeArgs: [`--remote-debugging-port=${daPort}`] }),
callbacks,
);
browser = await connectPuppeteer(daPort);
} else {
browser = await launchPuppeteer(daPort, chromeArgs);
// We want to attach after the page is fully loaded, and things happened, to simulate a real attach scenario. So we wait for a little bit
await utils.promiseTimeout(undefined, 1000);
await launchTestAdapter(
debugClient,
Object.assign({}, daConfig, { port: daPort }),
callbacks,
);
}
const page = logCallsTo(await getPageByUrl(browser, daConfig.url!), 'PuppeteerPage');
// 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));
}
return new LaunchPuppeteer(browser, page);
}
public async cleanUp(): Promise<void> {
// logger.log(`Closing puppeteer and chrome`);
try {
await this.browser.close();
// logger.log(`Scucesfully closed puppeteer and chrome`);
} catch (exception) {
// logger.log(`Failed to close puppeteer: ${exception}`);
}
}
public toString(): string {
return `LaunchPuppeteer`;
}
}

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

@ -1,162 +0,0 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/
import * as puppeteer from 'puppeteer';
import { loadProjectLabels } from '../labels';
import { setTestLogName } from '../utils/logging';
import {
FrameworkTestContext,
ReassignableFrameworkTestContext,
TestProjectSpec,
} from '../framework/frameworkTestSupport';
import { LaunchProject } from '../fixtures/launchProject';
import { PromiseOrNot } from '../testUtils';
import { NullFixture } from '../fixtures/fixture';
/**
* Extends the normal debug adapter context to include context relevant to puppeteer tests.
*/
export interface IPuppeteerTestContext extends FrameworkTestContext {
/** The connected puppeteer browser object */
browser: puppeteer.Browser | null;
/** The currently running html page in Chrome */
page: puppeteer.Page | null;
launchProject: LaunchProject | null;
}
export class PuppeteerTestContext extends ReassignableFrameworkTestContext {
private _browser: puppeteer.Browser | null = null;
private _page: puppeteer.Page | null = null;
private _launchProject: LaunchProject | null = null;
public constructor() {
super();
}
public get browser(): puppeteer.Browser | null {
return this._browser;
}
public get page(): puppeteer.Page | null {
return this._page;
}
public get launchProject(): LaunchProject | null {
return this._launchProject;
}
public reassignTo(newWrapped: IPuppeteerTestContext): this {
super.reassignTo(newWrapped);
this._page = newWrapped.page;
this._browser = newWrapped.browser;
this._launchProject = newWrapped.launchProject;
return this;
}
}
/**
* Launch a test with default settings and attach puppeteer. The test will start with the debug adapter
* and chrome launched, and puppeteer attached.
*
* @param description Describe what this test should be testing
* @param context The test context for this test sutie
* @param testFunction The inner test function that will run a test using puppeteer
*/
function puppeteerTestFunction(
description: string,
context: PuppeteerTestContext,
testFunction: (context: PuppeteerTestContext, page: puppeteer.Page) => PromiseOrNot<void>,
functionToDeclareTest: Mocha.TestFunction | Mocha.ExclusiveTestFunction = test,
): void {
functionToDeclareTest(description, function() {
return testFunction(context, context.page!);
});
}
puppeteerTestFunction.skip = (
description: string,
_context: PuppeteerTestContext,
_testFunction: (context: IPuppeteerTestContext, page: puppeteer.Page) => Promise<any>,
) =>
test.skip(description, () => {
throw new Error(`We don't expect this to be called`);
});
puppeteerTestFunction.only = (
description: string,
context: PuppeteerTestContext,
testFunction: (context: IPuppeteerTestContext, page: puppeteer.Page) => Promise<any>,
) => puppeteerTestFunction(description, context, testFunction, test.only);
export const puppeteerTest = puppeteerTestFunction;
/**
* Defines a custom test suite which will:
* 1) automatically launch a server from a test project directory,
* 2) launch the debug adapter (with chrome)
*
* From there, consumers can either launch a puppeteer instrumented test, or a normal test (i.e. without puppeteer) using
* the test methods defined here, and can get access to the relevant variables.
*
* @param description Description for the mocha test suite
* @param testSpec Info about the test project on which this suite will be based
* @param callback The inner test suite that uses this context
*/
function puppeteerSuiteFunction(
description: string,
testSpec: TestProjectSpec,
callback: (suiteContext: PuppeteerTestContext) => void,
suiteFunctionToUse:
| Mocha.SuiteFunction
| Mocha.ExclusiveSuiteFunction
| Mocha.PendingSuiteFunction = suite,
): Mocha.ISuite | void {
return suiteFunctionToUse(description, () => {
const testContext = new PuppeteerTestContext();
let fixture: LaunchProject | NullFixture = new NullFixture(); // This variable is shared across all test of this suite
setup(async function() {
setTestLogName(this.currentTest!.fullTitle());
const breakpointLabels = await loadProjectLabels(testSpec.props.webRoot);
const launchProject = (fixture = await LaunchProject.launch(this, testSpec));
testContext.reassignTo({
testSpec,
debugClient: launchProject.debugClient,
breakpointLabels,
browser: launchProject.browser,
page: launchProject.page,
launchProject,
});
});
teardown(async () => {
await fixture.cleanUp();
fixture = new NullFixture();
// logger.log(`teardown finished`);
});
callback(testContext);
});
}
puppeteerSuiteFunction.skip = (
description: string,
testSpec: TestProjectSpec,
callback: (suiteContext: PuppeteerTestContext) => any,
) => puppeteerSuiteFunction(description, testSpec, callback, suite.skip);
puppeteerSuiteFunction.only = (
description: string,
testSpec: TestProjectSpec,
callback: (suiteContext: PuppeteerTestContext) => any,
) => puppeteerSuiteFunction(description, testSpec, callback, suite.only);
puppeteerSuiteFunction.skip = (
description: string,
_testSpec: TestProjectSpec,
_callback: (suiteContext: PuppeteerTestContext) => any,
) => suite.skip(description, () => {});
export const puppeteerSuite = puppeteerSuiteFunction;

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

@ -1,81 +0,0 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/
/*
* Functions that make puppeteer testing easier
*/
import request from 'request-promise-native';
import puppeteer from 'puppeteer';
/**
* Connect puppeteer to a currently running instance of chrome
* @param port The port on which the chrome debugger is running
*/
export async function connectPuppeteer(port: number): Promise<puppeteer.Browser> {
const resp = await request(`http://localhost:${port}/json/version`);
const { webSocketDebuggerUrl } = JSON.parse(resp);
const browser = await (puppeteer as any).connect({
browserWSEndpoint: webSocketDebuggerUrl,
defaultViewport: null,
});
// logger.log(`Connected puppeteer on port: ${port} and websocket: ${JSON.stringify(webSocketDebuggerUrl)}`); // TODO@rob
return browser;
}
/**
* Launch puppeteer and a new instance of chrome
* @param port The port on which the chrome debugger is running
*/
export async function launchPuppeteer(
port: number,
chromeArgs: string[] = [],
): Promise<puppeteer.Browser> {
const browser = await puppeteer.launch({
headless: false,
defaultViewport: null,
args: chromeArgs.concat([`--remote-debugging-port=${port}`]),
});
// logger.log(`Launched puppeteer on port: ${port} and args: ${JSON.stringify(chromeArgs)}`);
return browser;
}
/**
* Get the first (or only) page loaded in chrome
* @param browser Puppeteer browser object
*/
export async function firstPage(browser: puppeteer.Browser): Promise<puppeteer.Page> {
return (await browser.pages())[0];
}
/**
* Get a page in the browser by the url
* @param browser Puppeteer browser object
* @param url The url of the desired page
* @param timeout Timeout in milliseconds
*/
export async function getPageByUrl(
browser: puppeteer.Browser,
url: string,
timeout = 5000,
): Promise<puppeteer.Page> {
const before = new Date().getTime();
let current = before;
// poll for the desired page url. If we don't find it within the timeout, throw an error
while (current - before < timeout) {
const pages = await browser.pages();
const desiredPage = pages.find(p => p.url().toLowerCase() === url.toLowerCase());
if (desiredPage) {
return desiredPage;
}
// TODO: yuck, clean up
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`;
}

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

@ -1,4 +0,0 @@
// Needed to make @types/puppeteer pass type checking
// See https://github.com/DefinitelyTyped/DefinitelyTyped/issues/24419
interface Element {}

4
src/int-chrome/puppeteerTypeFix.d.ts поставляемый
Просмотреть файл

@ -1,4 +0,0 @@
// Needed to make @types/puppeteer pass type checking
// See https://github.com/DefinitelyTyped/DefinitelyTyped/issues/24419
interface Element {}

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

@ -1,13 +0,0 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/
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 });
export const reactWithLoopTestSpecification = new TestProjectSpec({
projectRoot: path.join(DATA_ROOT, 'react_with_loop', 'dist'),
});

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

@ -1,295 +0,0 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/
import * as path from 'path';
import * as puppeteer from 'puppeteer';
import * as testSetup from './testSetup';
import { DebugProtocol } from 'vscode-debugprotocol';
import { FrameworkTestContext, TestProjectSpec } from './framework/frameworkTestSupport';
import { BreakpointsWizard } from './wizards/breakpoints/breakpointsWizard';
import { ExpectedFrame } from './wizards/breakpoints/implementation/stackTraceObjectAssertions';
import { puppeteerSuite, puppeteerTest } from './puppeteer/puppeteerSuite';
const DATA_ROOT = testSetup.DATA_ROOT;
const SIMPLE_PROJECT_ROOT = path.join(DATA_ROOT, 'stackTrace');
const TEST_SPEC = new TestProjectSpec({
projectRoot: SIMPLE_PROJECT_ROOT,
projectSrc: SIMPLE_PROJECT_ROOT,
});
interface StackTraceValidationConfig {
suiteContext: FrameworkTestContext;
page: puppeteer.Page;
breakPointLabel: string;
buttonIdToClick: string;
format?: DebugProtocol.StackFrameFormat;
expectedFrames: ExpectedFrame[];
}
async function validateStackTrace(config: StackTraceValidationConfig): Promise<void> {
const incBtn = await config.page.waitForSelector(config.buttonIdToClick);
const breakpoints = BreakpointsWizard.create(config.suiteContext.debugClient, TEST_SPEC);
const breakpointWizard = breakpoints.at('app.js');
const setStateBreakpoint = await breakpointWizard.breakpoint({
text: "console.log('Test stack trace here')",
});
await setStateBreakpoint.assertIsHitThenResumeWhen(() => incBtn.click(), {
stackTrace: config.expectedFrames,
stackFrameFormat: config.format,
});
}
puppeteerSuite('Stack Traces', TEST_SPEC, suiteContext => {
// Need attach mode
puppeteerTest(
'Stack trace is generated with no formatting',
suiteContext,
async (_context, page) => {
await validateStackTrace({
suiteContext: suiteContext,
page: page,
breakPointLabel: 'stackTraceBreakpoint',
buttonIdToClick: '#button',
format: {},
expectedFrames: [
{
name: '<anonymous>',
line: 11,
column: 9,
source: { fileRelativePath: 'app.js' },
presentationHint: 'normal',
},
{
name: 'evalCallback',
line: 12,
column: 7,
source: { fileRelativePath: 'app.js' },
presentationHint: 'normal',
},
{
name: '<anonymous>',
line: 1,
column: 1,
source: { evalCode: true },
presentationHint: 'normal',
},
{
name: 'timeoutCallback',
line: 6,
column: 5,
source: { fileRelativePath: 'app.js' },
presentationHint: 'normal',
},
{ name: 'setTimeout', presentationHint: 'label' },
{
name: 'buttonClick',
line: 2,
column: 5,
source: { fileRelativePath: 'app.js' },
presentationHint: 'normal',
},
{
name: 'onclick',
line: 7,
column: 49,
source: { url: suiteContext.launchProject!.url },
presentationHint: 'normal',
},
],
});
},
);
puppeteerTest(
'Stack trace is generated with module formatting',
suiteContext,
async (_context, page) => {
const url = suiteContext.launchProject!.url;
await validateStackTrace({
suiteContext: suiteContext,
page: page,
breakPointLabel: 'stackTraceBreakpoint',
buttonIdToClick: '#button',
format: {
module: true,
},
expectedFrames: [
{
name: '(anonymous function) [app.js]',
line: 11,
column: 9,
source: { fileRelativePath: 'app.js' },
presentationHint: 'normal',
},
{
name: 'evalCallback [app.js]',
line: 12,
column: 7,
source: { fileRelativePath: 'app.js' },
presentationHint: 'normal',
},
{
name: /\(eval code\) \[.*VM.*]/,
line: 1,
column: 1,
source: { evalCode: true },
presentationHint: 'normal',
},
{
name: 'timeoutCallback [app.js]',
line: 6,
column: 5,
source: { fileRelativePath: 'app.js' },
presentationHint: 'normal',
},
{ name: '[ setTimeout ]', presentationHint: 'label' },
{
name: 'buttonClick [app.js]',
line: 2,
column: 5,
source: { fileRelativePath: 'app.js' },
presentationHint: 'normal',
},
{
name: `onclick [${url.host}]`,
line: 7,
column: 49,
source: { url },
presentationHint: 'normal',
},
],
});
},
);
puppeteerTest(
'Stack trace is generated with line formatting',
suiteContext,
async (_context, page) => {
await validateStackTrace({
suiteContext: suiteContext,
page: page,
breakPointLabel: 'stackTraceBreakpoint',
buttonIdToClick: '#button',
format: {
line: true,
},
expectedFrames: [
{
name: '(anonymous function) Line 11',
line: 11,
column: 9,
source: { fileRelativePath: 'app.js' },
presentationHint: 'normal',
},
{
name: 'evalCallback Line 12',
line: 12,
column: 7,
source: { fileRelativePath: 'app.js' },
presentationHint: 'normal',
},
{
name: '(eval code) Line 1',
line: 1,
column: 1,
source: { evalCode: true },
presentationHint: 'normal',
},
{
name: 'timeoutCallback Line 6',
line: 6,
column: 5,
source: { fileRelativePath: 'app.js' },
presentationHint: 'normal',
},
{ name: '[ setTimeout ]', presentationHint: 'label' },
{
name: 'buttonClick Line 2',
line: 2,
column: 5,
source: { fileRelativePath: 'app.js' },
presentationHint: 'normal',
},
{
name: 'onclick Line 7',
line: 7,
column: 49,
source: { url: suiteContext.launchProject!.url },
presentationHint: 'normal',
},
],
});
},
);
puppeteerTest(
'Stack trace is generated with all formatting',
suiteContext,
async (_context, page) => {
const url = suiteContext.launchProject!.url;
await validateStackTrace({
suiteContext: suiteContext,
page: page,
breakPointLabel: 'stackTraceBreakpoint',
buttonIdToClick: '#button',
format: {
parameters: true,
parameterTypes: true,
parameterNames: true,
line: true,
module: true,
},
expectedFrames: [
{
name: '(anonymous function) [app.js] Line 11',
line: 11,
column: 9,
source: { fileRelativePath: 'app.js' },
presentationHint: 'normal',
},
{
name: 'evalCallback [app.js] Line 12',
line: 12,
column: 7,
source: { fileRelativePath: 'app.js' },
presentationHint: 'normal',
},
{
name: /\(eval code\) \[.*VM.*] Line 1/,
line: 1,
column: 1,
source: { evalCode: true },
presentationHint: 'normal',
},
{
name: 'timeoutCallback [app.js] Line 6',
line: 6,
column: 5,
source: { fileRelativePath: 'app.js' },
presentationHint: 'normal',
},
{ name: '[ setTimeout ]', presentationHint: 'label' },
{
name: 'buttonClick [app.js] Line 2',
line: 2,
column: 5,
source: { fileRelativePath: 'app.js' },
presentationHint: 'normal',
},
{
name: `onclick [${url.host}] Line 7`,
line: 7,
column: 49,
source: { url },
presentationHint: 'normal',
},
],
});
},
);
});

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

@ -1,130 +0,0 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/
import * as path from 'path';
import { createServer } from 'http-server';
import * as testSetup from './testSetup';
import { HttpOrHttpsServer } from './types/server';
import { ExtendedDebugClient } from './testSupport/debugClient';
suite('Stepping', () => {
const DATA_ROOT = testSetup.DATA_ROOT;
let dc: ExtendedDebugClient;
setup(function() {
return testSetup.setup(this).then(_dc => (dc = _dc));
});
let server: HttpOrHttpsServer | null;
teardown(async () => {
if (server) {
server.close(err => console.log('Error closing server in teardown: ' + (err && err.message)));
server = null;
}
await testSetup.teardown();
});
suite.skip('skipFiles', () => {
test('when generated script is skipped via regex, the source can be un-skipped', async () => {
const testProjectRoot = path.join(DATA_ROOT, 'calls-between-merged-files');
const sourceA = path.join(testProjectRoot, 'sourceA.ts');
const sourceB2 = path.join(testProjectRoot, 'sourceB2.ts');
server = createServer({ root: testProjectRoot });
server.listen(7890);
const url = 'http://localhost:7890/index.html';
// Skip the full B generated script via launch config
const bpLineA = 6;
const skipFiles = ['b.js'];
await dc.hitBreakpointUnverified(
{ url, skipFiles, webRoot: testProjectRoot },
{ path: sourceA, line: bpLineA },
);
// Step in, verify B sources are skipped
await dc.stepInRequest();
await dc.assertStoppedLocation('step', { path: sourceA, line: 2 });
await dc.send('toggleSkipFileStatus', { path: sourceB2 });
// Continue back to sourceA, step in, should skip B1 and land on B2
await dc.continueRequest();
await dc.assertStoppedLocation('breakpoint', { path: sourceA, line: bpLineA });
await dc.stepInRequest();
await dc.assertStoppedLocation('step', { path: sourceB2, line: 2 });
});
test('when a non-sourcemapped script is skipped via regex, it can be unskipped', async () => {
// Using this program, but run with sourcemaps disabled
const testProjectRoot = path.join(DATA_ROOT, 'calls-between-sourcemapped-files');
const sourceA = path.join(testProjectRoot, 'out/sourceA.js');
const sourceB = path.join(testProjectRoot, 'out/sourceB.js');
server = createServer({ root: testProjectRoot });
server.listen(7890);
const url = 'http://localhost:7890/index.html';
// Skip the full B generated script via launch config
const skipFiles = ['sourceB.js'];
const bpLineA = 5;
await dc.hitBreakpointUnverified(
{ url, sourceMaps: false, skipFiles, webRoot: testProjectRoot },
{ path: sourceA, line: bpLineA },
);
// Step in, verify B sources are skipped
await dc.stepInRequest();
await dc.assertStoppedLocation('step', { path: sourceA, line: 2 });
await dc.send('toggleSkipFileStatus', { path: sourceB });
// Continue back to A, step in, should land in B
await dc.continueRequest();
await dc.assertStoppedLocation('breakpoint', { path: sourceA, line: bpLineA });
await dc.stepInRequest();
await dc.assertStoppedLocation('step', { path: sourceB, line: 2 });
});
test('skip statuses for sourcemapped files are persisted across page reload', async () => {
const testProjectRoot = path.join(DATA_ROOT, 'calls-between-merged-files');
const sourceA = path.join(testProjectRoot, 'sourceA.ts');
const sourceB2 = path.join(testProjectRoot, 'sourceB2.ts');
server = createServer({ root: testProjectRoot });
server.listen(7890);
const url = 'http://localhost:7890/index.html';
// Skip the full B generated script via launch config
const bpLineA = 6;
const skipFiles = ['b.js'];
await dc.hitBreakpointUnverified(
{ url, skipFiles, webRoot: testProjectRoot },
{ path: sourceA, line: bpLineA },
);
await Promise.all([dc.stepInRequest(), dc.waitForEvent('stopped')]);
// Un-skip b2 and refresh the page
await Promise.all([
// Wait for extra pause event sent after toggling skip status
dc.waitForEvent('stopped'),
dc.send('toggleSkipFileStatus', { path: sourceB2 }),
]);
await Promise.all([
dc.send('restart'),
dc.assertStoppedLocation('breakpoint', { path: sourceA, line: bpLineA }),
]);
// Persisted bp should be hit. Step in, and assert we stepped through B1 into B2
await Promise.all([
dc.stepInRequest(),
dc.assertStoppedLocation('step', { path: sourceB2, line: 2 }),
]);
});
});
});

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

@ -1,3 +0,0 @@
{
"reporterEnabled": "node_modules/vscode-chrome-debug-core-testsupport/out/loggingReporter.js, mocha-junit-reporter"
}

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

@ -1,132 +0,0 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/
import * as path from 'path';
import * as tmp from 'tmp';
import puppeteer from 'puppeteer';
import * as _ from 'lodash';
import { Dictionary } from 'lodash';
import { logCallsTo, getDebugAdapterLogFilePath, setTestLogName } from './utils/logging';
import { IBeforeAndAfterContext, ITestCallbackContext } from 'mocha';
import { killAllChrome } from './testUtils';
import { DefaultTimeoutMultiplier } from './utils/waitUntilReadyWithTimeout';
import { IChromeLaunchConfiguration, chromeLaunchConfigDefaults } from '../configuration';
import { ExtendedDebugClient } from './testSupport/debugClient';
import * as testSupportSetup from './testSupport/testSetup';
import { startDebugServer } from '../debugServer';
import { IDisposable } from '../common/disposable';
import getPort from 'get-port';
let testLaunchProps: (IChromeLaunchConfiguration & Dictionary<unknown>) | undefined; // TODO@rob i don't know
export const isThisV2 = true;
export const isThisV1 = !isThisV2;
export const isWindows = process.platform === 'win32';
function formLaunchArgs(
launchArgs: IChromeLaunchConfiguration & Dictionary<unknown>,
testTitle: string,
): void {
launchArgs.type = 'pwa-chrome' as any; // TODO@rob
launchArgs.logging = { dap: '/tmp/dap.log', cdp: '/tmp/cdp.log' };
launchArgs.sourceMapPathOverrides = {};
launchArgs.trace = true;
launchArgs.logTimestamps = true;
launchArgs.disableNetworkCache = true;
launchArgs.logFilePath = getDebugAdapterLogFilePath(testTitle);
if (!launchArgs.runtimeExecutable) {
launchArgs.runtimeExecutable = puppeteer.executablePath();
}
const hideWindows = process.env['TEST_DA_HIDE_WINDOWS'] === 'true';
if (hideWindows) {
launchArgs.runtimeArgs = ['--headless', '--disable-gpu'];
}
// Start with a clean userDataDir for each test run
const tmpDir = tmp.dirSync({ prefix: 'chrome2-' });
launchArgs.userDataDir = tmpDir.name;
if (testLaunchProps) {
for (const key in testLaunchProps) {
launchArgs[key] = testLaunchProps[key];
}
testLaunchProps = undefined;
}
const argsWithDefaults = { ...chromeLaunchConfigDefaults, ...launchArgs };
Object.assign(launchArgs, argsWithDefaults);
}
let storedLaunchArgs: Partial<IChromeLaunchConfiguration> = {};
export function launchArgs(): Partial<IChromeLaunchConfiguration> {
return { ...storedLaunchArgs };
}
function patchLaunchArgs(launchArgs: IChromeLaunchConfiguration, testTitle: string): void {
formLaunchArgs(launchArgs as any, testTitle); // TODO@rob
storedLaunchArgs = launchArgs;
}
export const lowercaseDriveLetterDirname = __dirname.charAt(0).toLowerCase() + __dirname.substr(1);
export const PROJECT_ROOT = path.join(lowercaseDriveLetterDirname, '../../../');
export const DATA_ROOT = path.join(PROJECT_ROOT, 'testdata/');
/** Default setup for all our tests, using the context of the setup method
* - Best practise: The new best practise is to use the DefaultFixture when possible instead of calling this method directly
*/
export async function setup(
context: IBeforeAndAfterContext | ITestCallbackContext,
launchProps?: Partial<IChromeLaunchConfiguration>,
): Promise<ExtendedDebugClient> {
const currentTest = _.defaultTo(context.currentTest, context.test);
return setupWithTitle(currentTest.fullTitle(), launchProps);
}
let currentServer: IDisposable | undefined;
/** Default setup for all our tests, using the test title
* - Best practise: The new best practise is to use the DefaultFixture when possible instead of calling this method directly
*/
export async function setupWithTitle(
testTitle: string,
launchProps?: Partial<IChromeLaunchConfiguration>,
): Promise<ExtendedDebugClient> {
// killAllChromesOnWin32(); // Kill chrome.exe instances before the tests. Killing them after the tests is not as reliable. If setup fails, teardown is not executed.
setTestLogName(testTitle);
if (launchProps) {
testLaunchProps = launchProps as any; // TODO@rob
}
const port = await getPort();
currentServer = await startDebugServer(port); // TODO@rob
const debugClient = await testSupportSetup.setup({
type: 'pwa-chrome',
patchLaunchArgs: args => patchLaunchArgs(args, testTitle),
port,
});
debugClient.defaultTimeout = DefaultTimeoutMultiplier * 10000 /*10 seconds*/;
const wrappedDebugClient = logCallsTo(debugClient, 'DebugAdapterClient');
return wrappedDebugClient;
}
export async function teardown() {
if (currentServer) {
currentServer.dispose();
}
await testSupportSetup.teardown();
}
export function killAllChromesOnWin32() {
if (process.platform === 'win32') {
// We only need to kill the chrome.exe instances on the Windows agent
// TODO: Figure out a way to remove this
killAllChrome();
}
}

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

@ -1,229 +0,0 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/
import * as assert from 'assert';
import { DebugClient } from 'vscode-debugadapter-testsupport';
import { DebugProtocol } from 'vscode-debugprotocol';
export const THREAD_ID = 1;
export function setBreakpointOnStart(
dc: DebugClient,
bps: DebugProtocol.SourceBreakpoint[],
program: string,
expLine?: number,
expCol?: number,
expVerified?: boolean,
): Promise<void> {
return dc
.waitForEvent('initialized')
.then(event => setBreakpoint(dc, bps, program, expLine, expCol, expVerified))
.then(() => dc.configurationDoneRequest())
.then(() => {});
}
export function setBreakpoint(
dc: DebugClient,
bps: DebugProtocol.SourceBreakpoint[],
program: string,
expLine?: number,
expCol?: number,
expVerified?: boolean,
): Promise<void> {
return dc
.setBreakpointsRequest({
breakpoints: bps,
source: { path: program },
})
.then(response => {
const bp = response.body.breakpoints[0];
if (typeof expVerified === 'boolean')
assert.equal(bp.verified, expVerified, 'breakpoint verification mismatch: verified');
if (typeof expLine === 'number')
assert.equal(bp.line, expLine, 'breakpoint verification mismatch: line');
if (typeof expCol === 'number')
assert.equal(bp.column, expCol, 'breakpoint verification mismatch: column');
});
}
export interface IExpectedStopLocation {
path?: string;
line?: number;
column?: number;
}
export class ExtendedDebugClient extends DebugClient {
async toggleSkipFileStatus(aPath: string): Promise<DebugProtocol.Response> {
const results = await Promise.all([
this.send('toggleSkipFileStatus', { path: aPath }),
this.waitForEvent('stopped'),
]);
return results[0];
}
async loadedSources(args: DebugProtocol.LoadedSourcesArguments): Promise<any> {
const response = await this.send('loadedSources');
return response.body;
}
async breakpointLocations(
args: DebugProtocol.BreakpointLocationsArguments,
): Promise<DebugProtocol.BreakpointLocationsResponse['body']> {
const response = await super.send('breakpointLocations', args);
return response.body;
}
continueRequest(): Promise<DebugProtocol.ContinueResponse> {
return super.continueRequest({ threadId: THREAD_ID });
}
nextRequest(): Promise<DebugProtocol.NextResponse> {
return super.nextRequest({ threadId: THREAD_ID });
}
stepOutRequest(): Promise<DebugProtocol.StepOutResponse> {
return super.stepOutRequest({ threadId: THREAD_ID });
}
stepInRequest(): Promise<DebugProtocol.StepInResponse> {
return super.stepInRequest({ threadId: THREAD_ID });
}
stackTraceRequest(): Promise<DebugProtocol.StackTraceResponse> {
return super.stackTraceRequest({ threadId: THREAD_ID });
}
continueAndStop(): Promise<any> {
return Promise.all([
super.continueRequest({ threadId: THREAD_ID }),
this.waitForEvent('stopped'),
]);
}
nextAndStop(): Promise<any> {
return Promise.all([super.nextRequest({ threadId: THREAD_ID }), this.waitForEvent('stopped')]);
}
stepOutAndStop(): Promise<any> {
return Promise.all([
super.stepOutRequest({ threadId: THREAD_ID }),
this.waitForEvent('stopped'),
]);
}
stepInAndStop(): Promise<any> {
return Promise.all([
super.stepInRequest({ threadId: THREAD_ID }),
this.waitForEvent('stopped'),
]);
}
async continueTo(
reason: string,
expected: IExpectedStopLocation,
): Promise<DebugProtocol.StackTraceResponse> {
const results = await Promise.all([
this.continueRequest(),
this.assertStoppedLocation(reason, expected),
]);
return results[1];
}
async nextTo(
reason: string,
expected: IExpectedStopLocation,
): Promise<DebugProtocol.StackTraceResponse> {
const results = await Promise.all([
this.nextRequest(),
this.assertStoppedLocation(reason, expected),
]);
return results[1] as any;
}
async stepOutTo(
reason: string,
expected: IExpectedStopLocation,
): Promise<DebugProtocol.StackTraceResponse> {
const results = await Promise.all([
this.stepOutRequest(),
this.assertStoppedLocation(reason, expected),
]);
return results[1] as any;
}
async stepInTo(
reason: string,
expected: IExpectedStopLocation,
): Promise<DebugProtocol.StackTraceResponse> {
const results = await Promise.all([
this.stepInRequest(),
this.assertStoppedLocation(reason, expected),
]);
return results[1] as any;
}
waitForEvent(eventType: string): Promise<DebugProtocol.Event> {
return super.waitForEvent(eventType);
}
/**
* This is a copy of DebugClient's hitBreakpoint, except that it doesn't assert 'verified' by default. In the Chrome debugger, a bp may be verified or unverified at launch,
* depending on whether it's randomly received before or after the 'scriptParsed' event for its script. So we can't always check this prop.
*/
hitBreakpointUnverified(
launchArgs: any,
location: { path: string; line: number; column?: number; verified?: boolean },
expected?: { path?: string; line?: number; column?: number; verified?: boolean },
): Promise<any> {
return Promise.all([
this.waitForEvent('initialized')
.then(event => {
return this.setBreakpointsRequest({
lines: [location.line],
breakpoints: [{ line: location.line, column: location.column }],
source: { path: location.path },
});
})
.then(response => {
if (response.body.breakpoints.length > 0) {
const bp = response.body.breakpoints[0];
if (typeof location.verified === 'boolean') {
assert.equal(
bp.verified,
location.verified,
'breakpoint verification mismatch: verified',
);
}
if (bp.source && bp.source.path) {
this.assertPath(
bp.source.path,
location.path,
'breakpoint verification mismatch: path',
);
}
if (typeof bp.line === 'number') {
assert.equal(bp.line, location.line, 'breakpoint verification mismatch: line');
}
if (typeof location.column === 'number' && typeof bp.column === 'number') {
assert.equal(bp.column, location.column, 'breakpoint verification mismatch: column');
}
}
return this.configurationDoneRequest();
}),
this.launch(launchArgs),
this.assertStoppedLocation('breakpoint', expected || location),
]);
}
}

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

@ -1,52 +0,0 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/
import * as mocha from 'mocha';
import * as events from 'events';
class LoggingReporter extends mocha.reporters.Spec {
static alwaysDumpLogs = false;
static logEE = new events.EventEmitter();
private testLogs: string[] = [];
private inTest = false;
constructor(runner: any) {
super(runner);
LoggingReporter.logEE.on('log', msg => {
if (this.inTest) {
this.testLogs.push(msg);
}
});
runner.on('test', () => {
this.inTest = true;
this.testLogs = [];
});
runner.on('pass', () => {
this.inTest = false;
if (LoggingReporter.alwaysDumpLogs) {
this.dumpLogs();
}
});
runner.on('fail', () => {
this.inTest = false;
this.dumpLogs();
// console.log(new Date().toISOString().split(/[TZ]/)[1] + ' Finished'); // TODO@rob
});
}
private dumpLogs(): void {
this.testLogs.forEach(msg => {
console.log(msg);
});
}
}
export = LoggingReporter;

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

@ -1,120 +0,0 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/
import { DebugProtocol } from 'vscode-debugprotocol';
import { ExtendedDebugClient } from './debugClient';
// ES6 default export...
// tslint:disable-next-line:no-var-requires
const LoggingReporter = require('./loggingReporter');
// LoggingReporter.alwaysDumpLogs = true;
let unhandledAdapterErrors: string[];
const origTest = test;
const checkLogTest = (
title: string,
testCallback?: any,
testFn: Function = origTest,
): Mocha.ITest => {
// Hack to always check logs after a test runs, can simplify after this issue:
// https://github.com/mochajs/mocha/issues/1635
if (!testCallback) {
return origTest(title, testCallback);
}
function runTest(): Promise<any> {
return new Promise((resolve, reject) => {
const optionalCallback = (e: Error) => {
if (e) reject(e);
else resolve();
};
const maybeP = testCallback(optionalCallback);
if (maybeP && maybeP.then) {
maybeP.then(resolve, reject);
}
});
}
return testFn(title, () => {
return runTest().then(() => {
// If any unhandled errors were logged, then ensure the test fails
if (unhandledAdapterErrors.length) {
const errStr =
unhandledAdapterErrors.length === 1
? unhandledAdapterErrors[0]
: JSON.stringify(unhandledAdapterErrors);
throw new Error(errStr);
}
});
});
};
(<Mocha.ITestDefinition>checkLogTest).only = (expectation, assertion) =>
checkLogTest(expectation, assertion, origTest.only);
(<Mocha.ITestDefinition>checkLogTest).skip = test.skip;
test = <any>checkLogTest;
function log(e: DebugProtocol.OutputEvent): void {
// Skip telemetry events
if (e.body.category === 'telemetry') return;
if (!e.body.output) return; // TODO@rob
const timestamp = new Date().toISOString().split(/[TZ]/)[1];
const outputBody = e.body.output
? e.body.output.trim()
: 'variablesReference: ' + e.body.variablesReference;
const msg = ` ${timestamp} ${outputBody}`;
LoggingReporter.logEE.emit('log', msg);
if (msg.indexOf('********') >= 0) unhandledAdapterErrors.push(msg);
}
export type PatchLaunchArgsCb = (launchArgs: any) => Promise<void> | void;
let dc: ExtendedDebugClient;
function patchLaunchFn(patchLaunchArgsCb: PatchLaunchArgsCb): void {
function patchLaunchArgs(launchArgs: any): Promise<void> {
launchArgs.request = 'launch';
launchArgs.trace = 'verbose';
const patchReturnVal = patchLaunchArgsCb(launchArgs);
return patchReturnVal || Promise.resolve();
}
const origLaunch = dc.launch;
dc.launch = (launchArgs: any) => {
return patchLaunchArgs(launchArgs).then(() => origLaunch.call(dc, launchArgs));
};
const origAttachRequest = dc.attachRequest;
dc.attachRequest = (attachArgs: any) => {
return patchLaunchArgs(attachArgs).then(() => origAttachRequest.call(dc, attachArgs));
};
}
export interface ISetupOpts {
type: string;
patchLaunchArgs?: PatchLaunchArgsCb;
port?: number;
alwaysDumpLogs?: boolean;
}
export function setup(opts: ISetupOpts): Promise<ExtendedDebugClient> {
unhandledAdapterErrors = [];
dc = new ExtendedDebugClient('node', '', opts.type); // Will always use 'port'
if (opts.patchLaunchArgs) {
patchLaunchFn(opts.patchLaunchArgs);
}
LoggingReporter.alwaysDumpLogs = opts.alwaysDumpLogs;
dc.addListener('output', log);
return dc.start(opts.port).then(() => dc);
}
export async function teardown(): Promise<void> {
dc.removeListener('output', log);
await dc.stop();
}

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

@ -1,219 +0,0 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/
import * as util from 'util';
import * as fs from 'fs';
import * as path from 'path';
import { execSync } from 'child_process';
const puppeteer = require('puppeteer');
import { IChromeLaunchConfiguration, IChromeAttachConfiguration } from '../configuration';
export function setupUnhandledRejectionListener(): void {
process.addListener('unhandledRejection', unhandledRejectionListener);
}
export function removeUnhandledRejectionListener(): void {
process.removeListener('unhandledRejection', unhandledRejectionListener);
}
function unhandledRejectionListener(reason: any, _p: Promise<any>) {
console.log('*');
console.log('**');
console.log('***');
console.log('****');
console.log('*****');
console.log(
`ERROR!! Unhandled promise rejection, a previous test may have failed but reported success.`,
);
console.log(reason.toString());
console.log('*****');
console.log('****');
console.log('***');
console.log('**');
console.log('*');
}
/**
* path.resolve + fixing the drive letter to match what VS Code does. Basically tests can use this when they
* want to force a path to native slashes and the correct letter case, but maybe can't use un-mocked utils.
*/
export function pathResolve(...segments: string[]): string {
let aPath = path.resolve.apply(null, segments);
if (aPath.match(/^[A-Za-z]:/)) {
aPath = aPath[0].toLowerCase() + aPath.substr(1);
}
return aPath;
}
/**
* Kills all running instances of chrome (that were launched by the tests, on Windows at least) on the test host
*/
export function killAllChrome() {
try {
const killCmd =
process.platform === 'win32'
? `start powershell -WindowStyle hidden -Command "Get-Process | Where-Object {$_.Path -like '*${puppeteer.executablePath()}*'} | Stop-Process"`
: 'killall chrome';
const hideWindows = process.env['TEST_DA_HIDE_WINDOWS'] === 'true';
const output = execSync(killCmd, { windowsHide: hideWindows }); // TODO: windowsHide paramenter doesn't currently work. It might be related to this: https://github.com/nodejs/node/issues/21825
if (output.length > 0) {
// Don't print empty lines
console.log(output.toString());
}
} catch (e) {
console.error(`Error killing chrome: ${e.message}`);
}
// the kill command will exit with a non-zero code (and cause execSync to throw) if chrome is already stopped
}
export const readFileP = util.promisify(fs.readFile);
export const writeFileP = util.promisify(fs.writeFile);
export type PromiseOrNot<T> = T | Promise<T>;
export interface IDeferred<T> {
resolve: (result: T) => void;
reject: (err: Error) => void;
promise: Promise<T>;
}
export function getDeferred<T>(): Promise<IDeferred<T>> {
return new Promise(r => {
// Promise callback is called synchronously
let resolve: IDeferred<T>['resolve'] = () => {
throw new Error('Deferred was resolved before intialization');
};
let reject: IDeferred<T>['reject'] = () => {
throw new Error('Deferred was rejected before initialization');
};
const promise = new Promise<T>((_resolve, _reject) => {
resolve = _resolve;
reject = _reject;
});
r({ resolve, reject, promise });
});
}
export function sleep(ms: number): Promise<void> {
return new Promise(resolve => {
setTimeout(resolve, ms);
});
}
export function promiseTimeout(
p?: Promise<any>,
timeoutMs = 1000,
timeoutMsg?: string,
): Promise<any> {
if (timeoutMsg === undefined) {
timeoutMsg = `Promise timed out after ${timeoutMs}ms`;
}
return new Promise((resolve, reject) => {
if (p) {
p.then(resolve, reject);
}
setTimeout(() => {
if (p) {
reject(new Error(timeoutMsg));
} else {
resolve();
}
}, timeoutMs);
});
}
export function retryAsync(
fn: () => Promise<any>,
timeoutMs: number,
intervalDelay = 0,
): Promise<any> {
const startTime = Date.now();
function tryUntilTimeout(): Promise<any> {
return fn().catch(e => {
if (Date.now() - startTime < timeoutMs - intervalDelay) {
return promiseTimeout(undefined, intervalDelay).then(tryUntilTimeout);
} else {
return errP(e);
}
});
}
return tryUntilTimeout();
}
/**
* A helper for returning a rejected promise with an Error object. Avoids double-wrapping an Error, which could happen
* when passing on a failure from a Promise error handler.
* @param msg - Should be either a string or an Error
*/
export function errP(msg: string | Error): Promise<never> {
const isErrorLike = (thing: any): thing is Error => !!thing.message;
let e: Error;
if (!msg) {
e = new Error('Unknown error');
} else if (isErrorLike(msg)) {
// msg is already an Error object
e = msg;
} else {
e = new Error(msg);
}
return Promise.reject(e);
}
export interface IChromeTestLaunchConfiguration extends Partial<IChromeLaunchConfiguration> {
request: 'launch';
url: string;
}
export interface IChromeTestAttachConfiguration extends Partial<IChromeAttachConfiguration> {
url: string;
}
/**
* Convert a local path to a file URL, like
* C:/code/app.js => file:///C:/code/app.js
* /code/app.js => file:///code/app.js
* \\code\app.js => file:///code/app.js
*/
export function pathToFileURL(_absPath: string, normalize?: boolean): string {
let absPath = forceForwardSlashes(_absPath);
if (isTrue(normalize)) {
absPath = path.normalize(absPath);
absPath = forceForwardSlashes(absPath);
}
const filePrefix = _absPath.startsWith('\\\\')
? 'file:/'
: absPath.startsWith('/')
? 'file://'
: 'file:///';
absPath = filePrefix + absPath;
return encodeURI(absPath);
}
/**
* Replace any backslashes with forward slashes
* blah\something => blah/something
*/
function forceForwardSlashes(aUrl: string): string {
return aUrl
.replace(/\\\//g, '/') // Replace \/ (unnecessarily escaped forward slash)
.replace(/\\/g, '/');
}
/**
* Returns whether the parameter is defined and is true
*/
function isTrue(booleanOrUndefined: boolean | undefined): boolean {
return booleanOrUndefined === true;
}

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

@ -1,7 +0,0 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/
import * as http from 'http';
import * as https from 'https';
export type HttpOrHttpsServer = http.Server | https.Server;

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

@ -1,34 +0,0 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/
import * as _ from 'lodash';
import {
createColumnNumber,
createLineNumber,
LineNumber,
} from '../core-v2/chrome/internal/locations/subtypes';
import { Position } from '../core-v2/chrome/internal/locations/location';
import { readFileP } from '../testUtils';
export async function findPositionOfTextInFile(filePath: string, text: string): Promise<Position> {
const contentsIncludingCarriageReturns = await readFileP(filePath, 'utf8');
const contents = contentsIncludingCarriageReturns.replace(/\r/g, '');
const textStartIndex = contents.indexOf(text);
if (textStartIndex >= 0) {
const textLineNumber = findLineNumber(contents, textStartIndex);
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}`);
}
}
export function findLineNumber(contents: string, characterIndex: number): LineNumber {
const contentsBeforeCharacter = contents.substr(0, characterIndex);
const textLineNumber = createLineNumber(
_.countBy(contentsBeforeCharacter, c => c === '\n')['true'] || 0,
);
return textLineNumber;
}

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

@ -1,71 +0,0 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/
import * as utils from '../testUtils';
/**
* The test normally run very fast, so it's difficult to see what actions they are taking in the browser.
* We can use the HumanSlownessSimulator to artifically slow some classes like the puppeteer classes, so it's the actions
* will be taken at a lower speed, and it'll be easier to see and understand what is happening
*/
export class HumanSlownessSimulator {
public constructor(
private readonly _slownessInMillisecondsValueGenerator: () => number = () => 500,
) {}
public simulateSlowness(): Promise<void> {
return utils.promiseTimeout(undefined, this._slownessInMillisecondsValueGenerator());
}
public wrap<T extends object>(object: T): T {
return new HumanSpeedProxy(this, object).wrapped();
}
}
export class HumanSpeedProxy<T extends object> {
constructor(
private readonly _humanSlownessSimulator: HumanSlownessSimulator,
private readonly _objectToWrap: T,
) {}
public wrapped(): T {
const handler = {
get: <K extends keyof T>(target: T, propertyKey: K, _receiver: any) => {
this._humanSlownessSimulator.simulateSlowness();
const originalPropertyValue = target[propertyKey];
if (typeof originalPropertyValue === 'function') {
return (...args: any) => {
const result = originalPropertyValue.apply(target, args);
if (result && result.then) {
// Currently we only slow down async operations
return result.then(
async (promiseResult: object) => {
await this._humanSlownessSimulator.simulateSlowness();
return typeof promiseResult === 'object'
? this._humanSlownessSimulator.wrap(promiseResult)
: promiseResult;
},
(rejection: unknown) => {
return rejection;
},
);
}
};
} else {
return originalPropertyValue;
}
},
};
return new Proxy<T>(this._objectToWrap, handler);
}
}
const humanSlownessSimulator = new HumanSlownessSimulator();
const humanSlownessEnabeld = process.env.RUN_TESTS_SLOWLY === 'true';
export function slowToHumanLevel<T extends object>(object: T): T {
return humanSlownessEnabeld ? humanSlownessSimulator.wrap(object) : object;
}

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

@ -1,100 +0,0 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/
import * as path from 'path';
import {
MethodsCalledLogger,
IMethodsCalledLoggerConfiguration,
MethodsCalledLoggerConfiguration,
ReplacementInstruction,
wrapWithMethodLogger,
} from '../core-v2/chrome/logging/methodsCalledLogger';
const useDateTimeInLog = false;
function dateTimeForFilePath(): string {
return new Date()
.toISOString()
.replace(/:/g, '')
.replace('T', ' ')
.replace(/\.[0-9]+^/, '');
}
function dateTimeForFilePathIfNeeded() {
return useDateTimeInLog ? `-${dateTimeForFilePath()}` : '';
}
const logsFolderPath = path.resolve(process.cwd(), 'logs');
export function getDebugAdapterLogFilePath(testTitle: string): string {
return logFilePath(testTitle, 'DA');
}
/**
* Transforms a title to an equivalent title that can be used as a filename (We use this to convert the name of our tests into the name of the logfile for that test)
*/
function sanitizeTestTitle(testTitle: string) {
return (
testTitle
.replace(/[:\/\\]/g, '-')
// These replacements are needed for the hit count breakpoint tests, which have these characters in their title
.replace(/ > /g, ' bigger than ')
.replace(/ < /g, ' smaller than ')
.replace(/ >= /g, ' bigger than or equal to ')
.replace(/ <= /g, ' smaller than or equal to ')
);
}
function logFilePath(testTitle: string, logType: string) {
return path.join(
logsFolderPath,
`${process.platform}-${sanitizeTestTitle(
testTitle,
)}-${logType}${dateTimeForFilePathIfNeeded()}.log`,
);
}
// logger.init(() => { });
// // Dispose the logger on unhandled errors, so it'll flush the remaining contents of the log...
// process.on('uncaughtException', () => logger.dispose());
// process.on('unhandledRejection', () => logger.dispose());
const currentTestTitle = '';
export function setTestLogName(testTitle: string): void {
// We call setTestLogName in the common setup code. We want to call it earlier in puppeteer tests to get the logs even when the setup fails
// So we write this code to be able to call it two times, and the second time will get ignored
if (testTitle !== currentTestTitle) {
// logger.setup(LogLevel.Verbose, logFilePath(testTitle, 'TEST'));
testTitle = currentTestTitle;
}
}
class PuppeteerMethodsCalledLoggerConfiguration implements IMethodsCalledLoggerConfiguration {
private readonly _wrapped = new MethodsCalledLoggerConfiguration('', []);
public readonly replacements: ReplacementInstruction[] = [];
public customizeResult<T>(methodName: string | symbol | number, args: any, result: T): T {
if (methodName === 'waitForSelector' && typeof result === 'object' && args.length >= 1) {
return wrapWithMethodLogger(<T & object>result, args[0]);
} else {
return result;
}
}
public customizeArgumentsBeforeCall(
receiverName: string,
methodName: string | symbol | number,
args: object[],
): void {
this._wrapped.customizeArgumentsBeforeCall(receiverName, methodName, args);
}
}
export function logCallsTo<T extends object>(object: T, name: string): T {
return new MethodsCalledLogger(
new PuppeteerMethodsCalledLoggerConfiguration(),
object,
name,
).wrapped();
}

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

@ -1,29 +0,0 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/
import { DebugClient } from 'vscode-debugadapter-testsupport';
import { DebugProtocol } from 'vscode-debugprotocol';
export function onUnhandledException(
client: DebugClient,
actionWhenUnhandledException: (exceptionMessage: string) => void,
): void {
client.on('output', (args: DebugProtocol.OutputEvent) => {
if (args.body.category === 'telemetry' && args.body.output === 'error') {
actionWhenUnhandledException(
`Debug adapter had an unhandled error: ${args.body.data.exceptionMessage}`,
);
}
});
}
export function onHandledError(
client: DebugClient,
actionWhenHandledError: (exceptionMessage: string) => void,
): void {
client.on('output', (args: DebugProtocol.OutputEvent) => {
if (args.body.category === 'stderr') {
actionWhenHandledError(args.body.output);
}
});
}

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

@ -1,11 +0,0 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/
export async function asyncRepeatSerially(
howManyTimes: number,
action: () => Promise<void>,
): Promise<void> {
for (let index = 0; index < howManyTimes; ++index) {
await action();
}
}

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

@ -1,30 +0,0 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/
import * as _ from 'lodash';
import * as utils from '../testUtils';
// The VSTS agents run slower than our machines. Use this value to reduce proportinoally the timeouts in your dev machine
export const DefaultTimeoutMultiplier = parseFloat(
_.defaultTo(process.env['TEST_TIMEOUT_MULTIPLIER'], '1'),
);
/**
* Wait until the isReady condition evaluates to true. This method will evaluate it every 50 milliseconds until it returns true. It will time-out after maxWaitTimeInMilliseconds milliseconds
*/
export async function waitUntilReadyWithTimeout(
isReady: () => boolean,
maxWaitTimeInMilliseconds = DefaultTimeoutMultiplier * 30000 /* 30 seconds */,
) {
const maximumDateTimeToWaitUntil = Date.now() + maxWaitTimeInMilliseconds;
while (!isReady() && Date.now() < maximumDateTimeToWaitUntil) {
await utils.promiseTimeout(undefined, 10 /*ms*/);
}
if (!isReady()) {
throw new Error(
`Timed-out after waiting for condition to be ready for ${maxWaitTimeInMilliseconds}ms. Condition: ${isReady}`,
);
}
}

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

@ -1,109 +0,0 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/
import { InternalFileBreakpointsWizard } from './implementation/internalFileBreakpointsWizard';
import { DebugProtocol } from 'vscode-debugprotocol';
import { IVerificationsAndAction } from './breakpointsWizard';
import { IBPActionWhenHit } from '../../core-v2/chrome/internal/breakpoints/bpActionWhenHit';
import { RemoveProperty } from '../../core-v2/typeUtils';
import { Position } from '../../core-v2/chrome/internal/locations/location';
export class BreakpointWizard {
private isBreakpointSet = false;
public constructor(
private readonly _internal: InternalFileBreakpointsWizard,
public readonly position: Position,
public readonly actionWhenHit: IBPActionWhenHit,
public readonly name: string,
public readonly boundPosition: Position,
) {}
public get filePath(): string {
return this._internal.filePath;
}
public async setThenWaitForVerifiedThenValidate(): Promise<BreakpointWizard> {
await this.setWithoutVerifying();
await this.waitUntilVerified();
this.assertIsVerified();
return this;
}
public async waitUntilVerified(): Promise<BreakpointWizard> {
this.validateIsSet('waitUntilVerified');
await this._internal.waitUntilVerified(this);
return Promise.resolve(this);
}
public async setWithoutVerifying(): Promise<BreakpointWizard> {
this.validateIsUnset('setWithoutVerifying');
await this._internal.set(this);
this.isBreakpointSet = true;
return Promise.resolve(this);
}
public async unset(): Promise<BreakpointWizard> {
this.validateIsSet('unset');
await this._internal.unset(this);
this.isBreakpointSet = false;
return Promise.resolve(this);
}
public async assertIsHitThenResumeWhen(
lastActionToMakeBreakpointHit: () => Promise<unknown>,
verifications: IVerificationsAndAction = {},
): Promise<BreakpointWizard> {
this.validateIsSet('assertIsHitThenResumeWhen');
await this._internal.assertIsHitThenResumeWhen(
this,
lastActionToMakeBreakpointHit,
verifications,
);
return Promise.resolve(this);
}
public async assertIsHitThenResume(
verifications: IVerificationsAndAction,
): Promise<BreakpointWizard> {
this.validateIsSet('assertIsHitThenResume');
await this._internal.assertIsHitThenResume(this, verifications);
return Promise.resolve(this);
}
public assertIsVerified(): this {
this.validateIsSet('assertIsVerified');
this._internal.assertIsVerified(this);
return this;
}
public assertIsNotVerified(unverifiedReason: string): this {
this.validateIsSet('assertIsNotVerified');
this._internal.assertIsNotVerified(this, unverifiedReason);
return this;
}
public toString(): string {
return this.name;
}
private validateIsSet(operationName: string): void {
this.validateInExpectedState(true, operationName);
}
private validateIsUnset(operationName: string): void {
this.validateInExpectedState(false, operationName);
}
private validateInExpectedState(needsToBeSet: boolean, operationName: string): void {
if (this.isBreakpointSet !== needsToBeSet) {
throw new Error(
`Can't perform operation ${operationName} because it needs breakpoint to ${
needsToBeSet ? '' : 'NOT '
} be set`,
);
}
}
}
export type VSCodeActionWhenHit = RemoveProperty<DebugProtocol.SourceBreakpoint, 'line' | 'column'>;

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

@ -1,178 +0,0 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/
import { PromiseOrNot } from '../../testUtils';
import { IExpectedVariables, VariablesWizard } from '../variables/variablesWizard';
import {
ExpectedFrame,
StackTraceObjectAssertions,
} from './implementation/stackTraceObjectAssertions';
import { DebugProtocol } from 'vscode-debugprotocol';
import { ValidatedMap } from '../../core-v2/chrome/collections/validatedMap';
import {
InternalFileBreakpointsWizard,
BreakpointStatusChangedWithId,
} from './implementation/internalFileBreakpointsWizard';
import { ExtendedDebugClient } from '../../testSupport/debugClient';
import { PausedWizard } from '../pausedWizard';
import { TestProjectSpec } from '../../framework/frameworkTestSupport';
import { wrapWithMethodLogger } from '../../core-v2/chrome/logging/methodsCalledLogger';
import { FileBreakpointsWizard } from './fileBreakpointsWizard';
import { BreakpointWizard } from './breakpointWizard';
import { expect } from 'chai';
import { stackTrace, StackFrameWizard } from '../variables/stackFrameWizard';
import { assertMatchesBreakpointLocation } from './implementation/breakpointsAssertions';
import { StackTraceStringAssertions } from './implementation/stackTraceStringAssertions';
export interface IVerificationsAndAction {
action?: () => PromiseOrNot<void>;
variables?: IExpectedVariables;
stackTrace?: string | ExpectedFrame[];
stackFrameFormat?: DebugProtocol.StackFrameFormat;
}
export class BreakpointsWizard {
private readonly _variablesWizard = new VariablesWizard(this._client);
private readonly _pathToFileWizard = new ValidatedMap<string, InternalFileBreakpointsWizard>();
private constructor(
private readonly _client: ExtendedDebugClient,
private readonly _pausedWizard: PausedWizard,
private readonly _project: TestProjectSpec,
) {
this._client.on('breakpoint', breakpointStatusChange =>
this.onBreakpointStatusChange(breakpointStatusChange.body),
);
}
public get project() {
return this._project;
}
public static create(
debugClient: ExtendedDebugClient,
testProjectSpecification: TestProjectSpec,
): BreakpointsWizard {
return this.createWithPausedWizard(
debugClient,
PausedWizard.forClient(debugClient),
testProjectSpecification,
);
}
public static createWithPausedWizard(
debugClient: ExtendedDebugClient,
pausedWizard: PausedWizard,
testProjectSpecification: TestProjectSpec,
): BreakpointsWizard {
return wrapWithMethodLogger(new this(debugClient, pausedWizard, testProjectSpecification));
}
public at(filePath: string): FileBreakpointsWizard {
return wrapWithMethodLogger(
new FileBreakpointsWizard(
this._pathToFileWizard.getOrAdd(
filePath,
() =>
new InternalFileBreakpointsWizard(
wrapWithMethodLogger(this),
this._client,
this._project.src(filePath),
),
),
),
);
}
public async waitAndConsumePausedEvent(_breakpoint: BreakpointWizard): Promise<void> {
// TODO: Should we validate the stack trace is on breakpoint here?
await this._pausedWizard.waitAndConsumePausedEvent(pausedInfo => {
expect(pausedInfo.reason).to.equal('breakpoint');
});
}
/**
* Instruct the debuggee to resume, and verify that the Debug-Adapter sends the proper notification after that happens
*/
public async resume(): Promise<void> {
return this._pausedWizard.resume();
}
public async waitAndConsumeResumedEvent(): Promise<void> {
return this._pausedWizard.waitAndConsumeResumedEvent();
}
public async waitAndAssertNoMoreEvents(): Promise<void> {
return this._pausedWizard.waitAndAssertNoMoreEvents();
}
public toString(): string {
return 'Breakpoints';
}
public async assertIsHitThenResumeWhen(
breakpoints: BreakpointWizard[],
lastActionToMakeBreakpointHit: () => Promise<void>,
verifications: IVerificationsAndAction,
): Promise<void> {
const actionResult = lastActionToMakeBreakpointHit();
for (const breakpoint of breakpoints) {
await this.assertIsHitThenResume(breakpoint, verifications);
}
await actionResult;
}
public async assertIsHitThenResume(
breakpoint: BreakpointWizard,
verifications: IVerificationsAndAction,
): Promise<void> {
await this.waitAndConsumePausedEvent(breakpoint);
const stackTraceFrames = (await stackTrace(this._client, verifications.stackFrameFormat))
.stackFrames;
// Validate that the topFrame is locate in the same place as the breakpoint
assertMatchesBreakpointLocation(stackTraceFrames[0], breakpoint.filePath, breakpoint);
if (typeof verifications.stackTrace === 'string') {
const assertions = new StackTraceStringAssertions(breakpoint);
assertions.assertResponseMatches(stackTraceFrames, verifications.stackTrace);
} else if (typeof verifications.stackTrace === 'object') {
const assertions = new StackTraceObjectAssertions(this);
assertions.assertResponseMatches(stackTraceFrames, verifications.stackTrace);
}
if (verifications.variables !== undefined) {
await this._variablesWizard.assertStackFrameVariablesAre(
new StackFrameWizard(this._client, stackTraceFrames[0]),
verifications.variables,
);
}
if (verifications.action !== undefined) {
await verifications.action();
}
await this.resume();
}
private onBreakpointStatusChange(
breakpointStatusChanged: DebugProtocol.BreakpointEvent['body'],
): void {
if (this.isBreakpointStatusChangedWithId(breakpointStatusChanged)) {
// TODO: Update this code to only send the breakpoint to the file that owns it
for (const fileWizard of this._pathToFileWizard.values()) {
fileWizard.onBreakpointStatusChange(breakpointStatusChanged);
}
}
}
private isBreakpointStatusChangedWithId(
statusChanged: DebugProtocol.BreakpointEvent['body'],
): statusChanged is BreakpointStatusChangedWithId {
return statusChanged.breakpoint.id !== undefined;
}
}

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

@ -1,77 +0,0 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/
import { BreakpointWizard } from './breakpointWizard';
import { InternalFileBreakpointsWizard } from './implementation/internalFileBreakpointsWizard';
import { PromiseOrNot } from '../../testUtils';
import { wrapWithMethodLogger } from '../../core-v2/chrome/logging/methodsCalledLogger';
import { PauseOnHitCount } from '../../core-v2/chrome/internal/breakpoints/bpActionWhenHit';
export interface IBreakpointOptions {
text: string;
boundText?: string;
}
export interface IHitCountBreakpointOptions extends IBreakpointOptions {
hitCountCondition: string;
}
export interface IUnverifiedBreakpointOptions {
text: string;
unverifiedReason: string;
}
export interface IUnverifiedHitCountBreakpointOptions extends IUnverifiedBreakpointOptions {
hitCountCondition: string;
}
export class FileBreakpointsWizard {
public constructor(private readonly _internal: InternalFileBreakpointsWizard) {}
public async breakpoint(options: IBreakpointOptions): Promise<BreakpointWizard> {
const wrappedBreakpoint = wrapWithMethodLogger(
await this._internal.breakpoint({
text: options.text,
boundText: options.boundText,
name: `BP @ ${options.text}`,
}),
);
return wrappedBreakpoint.setThenWaitForVerifiedThenValidate();
}
public async hitCountBreakpoint(options: IHitCountBreakpointOptions): Promise<BreakpointWizard> {
return await (await this.unsetHitCountBreakpoint(options)).setThenWaitForVerifiedThenValidate();
}
public async unverifiedHitCountBreakpoint(
options: IUnverifiedHitCountBreakpointOptions,
): Promise<BreakpointWizard> {
return (
await (await this.unsetHitCountBreakpoint(options)).setWithoutVerifying()
).assertIsNotVerified(options.unverifiedReason);
}
public async unsetHitCountBreakpoint(
options: IHitCountBreakpointOptions,
): Promise<BreakpointWizard> {
return wrapWithMethodLogger(
await this._internal.breakpoint({
text: options.text,
boundText: options.boundText,
actionWhenHit: new PauseOnHitCount(options.hitCountCondition),
name: `BP @ ${options.text}`,
}),
);
}
public batch<T>(
batchAction: (fileBreakpointsWizard: FileBreakpointsWizard) => PromiseOrNot<T>,
): Promise<T> {
return this._internal.batch(batchAction);
}
public toString(): string {
return `Breakpoints at ${this._internal.filePath}`;
}
}

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

@ -1,111 +0,0 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/
import _ = require('lodash');
import { BreakpointWizard } from '../breakpointWizard';
import { PromiseOrNot } from '../../../testUtils';
import {
IBreakpointsBatchingStrategy,
InternalFileBreakpointsWizard,
CurrentBreakpointsMapping,
BreakpointsUpdate,
BreakpointStatusChangedWithId,
} from './internalFileBreakpointsWizard';
import { IVerificationsAndAction } from '../breakpointsWizard';
import { ValidatedSet } from '../../../core-v2/chrome/collections/validatedSet';
export class BatchingUpdatesState implements IBreakpointsBatchingStrategy {
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 assertIsNotVerified(breakpoint: BreakpointWizard, unverifiedReason: string): void {
this._actionsToCompleteAfterBatch.push(() =>
this._internal.assertIsNotVerified(breakpoint, unverifiedReason),
);
}
public async waitUntilVerified(breakpoint: BreakpointWizard): Promise<void> {
this._actionsToCompleteAfterBatch.push(() => this._internal.waitUntilVerified(breakpoint));
}
public onBreakpointStatusChange(_breakpointStatusChanged: BreakpointStatusChangedWithId): 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>,
_verifications: IVerificationsAndAction,
): 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 assertIsHitThenResume(
_breakpoint: BreakpointWizard,
_verifications: IVerificationsAndAction,
): 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`,
);
}
}
}

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

@ -1,87 +0,0 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/
import { expect, use } from 'chai';
const chaiString = require('chai-string');
import * as path from 'path';
import { DebugProtocol } from 'vscode-debugprotocol';
import { waitUntilReadyWithTimeout } from '../../../utils/waitUntilReadyWithTimeout';
import { BreakpointWizard } from '../breakpointWizard';
import {
CurrentBreakpointsMapping,
InternalFileBreakpointsWizard,
} from './internalFileBreakpointsWizard';
use(chaiString);
interface IObjectWithLocation {
source?: DebugProtocol.Source;
line?: number; // One based line number
column?: number; // One based colum number
}
export class BreakpointsAssertions {
public constructor(
private readonly _internal: InternalFileBreakpointsWizard,
public readonly currentBreakpointsMapping: CurrentBreakpointsMapping,
) {}
public assertIsVerified(breakpoint: BreakpointWizard): void {
// 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 breakpointStatus = this.currentBreakpointsMapping.get(breakpoint);
assertMatchesBreakpointLocation(breakpointStatus, this._internal.filePath, breakpoint);
expect(
breakpointStatus.verified,
`Expected ${breakpoint} to be verified yet it wasn't: ${breakpointStatus.message}`,
).to.equal(true);
}
public assertIsNotVerified(breakpoint: BreakpointWizard, unverifiedReason: string): void {
const breakpointLocation = `res:${breakpoint.filePath}:${breakpoint.position.lineNumber +
1}:${breakpoint.position.columnNumber + 1}`;
// For the moment we are assuming that the breakpoint maps to a single script file. If we need to support other cases we'll need to compose the message in the proper way
const fullMessage = `[ Breakpoint at ${breakpointLocation} do: ${breakpoint.actionWhenHit} is unbound because ${unverifiedReason} ]`;
const breakpointStatus = this.currentBreakpointsMapping.get(breakpoint);
expect(
breakpointStatus.verified,
`Expected ${breakpoint} to not be verified yet it was: ${breakpointStatus.message}`,
).to.equal(false);
expect(
breakpointStatus.message,
`Expected ${breakpoint} to have a particular unverified message`,
).to.equal(fullMessage);
}
public async waitUntilVerified(breakpoint: BreakpointWizard): Promise<void> {
await waitUntilReadyWithTimeout(() => this.currentBreakpointsMapping.get(breakpoint).verified);
}
}
export function assertMatchesBreakpointLocation(
objectWithLocation: IObjectWithLocation,
expectedFilePath: string,
breakpoint: BreakpointWizard,
): void {
expect(objectWithLocation.source).to.not.equal(undefined);
expect(objectWithLocation.source!.path!.toLowerCase()).to.be.equal(
expectedFilePath.toLowerCase(),
);
expect(objectWithLocation.source!.name!.toLowerCase()).to.be.equal(
path.basename(expectedFilePath.toLowerCase()),
);
const expectedLineNumber = breakpoint.boundPosition.lineNumber + 1;
const expectedColumNumber = breakpoint.boundPosition.columnNumber + 1;
const expectedBPLocationPrinted = `${expectedFilePath}:${expectedLineNumber}:${expectedColumNumber}`;
const actualBPLocationPrinted = `${objectWithLocation.source!.path}:${objectWithLocation.line}:${
objectWithLocation.column
}`;
expect(actualBPLocationPrinted.toLowerCase()).to.be.equal(
expectedBPLocationPrinted.toLowerCase(),
);
}

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

@ -1,111 +0,0 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/
import * as _ from 'lodash';
import { DebugProtocol } from 'vscode-debugprotocol';
import {
BreakpointsUpdate,
StateChanger,
InternalFileBreakpointsWizard,
BreakpointWithId,
} from './internalFileBreakpointsWizard';
import { BreakpointWizard, VSCodeActionWhenHit } from '../breakpointWizard';
import { PerformChangesImmediatelyState } from './performChangesImmediatelyState';
import { BreakpointsWizard } from '../breakpointsWizard';
import { Replace } from '../../../core-v2/typeUtils';
import { ExtendedDebugClient } from '../../../testSupport/debugClient';
import { ValidatedMap } from '../../../core-v2/chrome/collections/validatedMap';
import {
AlwaysPause,
PauseOnHitCount,
} from '../../../core-v2/chrome/internal/breakpoints/bpActionWhenHit';
type SetBreakpointsResponseWithId = Replace<
DebugProtocol.SetBreakpointsResponse,
'body',
Replace<DebugProtocol.SetBreakpointsResponse['body'], 'breakpoints', BreakpointWithId[]>
>;
export class BreakpointsUpdater {
public constructor(
private readonly _breakpointsWizard: BreakpointsWizard,
private readonly _internal: InternalFileBreakpointsWizard,
private readonly _client: ExtendedDebugClient,
private readonly _changeState: StateChanger,
) {}
public async update(update: BreakpointsUpdate): Promise<void> {
const updatedBreakpoints = update.toKeepAsIs.concat(update.toAdd);
const vsCodeBps = updatedBreakpoints.map(bp => this.toVSCodeProtocol(bp));
const response = await this._client.setBreakpointsRequest({
breakpoints: vsCodeBps,
source: { path: this._internal.filePath },
});
this.validateResponse(response, vsCodeBps);
const responseWithIds = <SetBreakpointsResponseWithId>response;
const breakpointToStatus = new ValidatedMap<BreakpointWizard, BreakpointWithId>(
<[[BreakpointWizard, BreakpointWithId]]>(
_.zip(updatedBreakpoints, responseWithIds.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 AlwaysPause) {
return {};
} else if (breakpoint.actionWhenHit instanceof PauseOnHitCount) {
return { hitCondition: breakpoint.actionWhenHit.pauseOnHitCondition };
} else {
throw new Error('Not yet implemented');
}
}
private validateResponse(
response: DebugProtocol.SetBreakpointsResponse,
vsCodeBps: DebugProtocol.SourceBreakpoint[],
): void {
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 bpsWithoutId = response.body.breakpoints.filter(bp => bp.id === undefined);
if (bpsWithoutId.length !== 0) {
throw new Error(
`Expected to receive all breakpoints with id yet we got some without ${JSON.stringify(
response.body.breakpoints,
)}`,
);
}
}
}

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

@ -1,154 +0,0 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/
import { MakePropertyRequired, Replace } from '../../../core-v2/typeUtils';
import { DebugProtocol } from 'vscode-debugprotocol';
import { BreakpointWizard } from '../breakpointWizard';
import { IVerificationsAndAction, BreakpointsWizard } from '../breakpointsWizard';
import { ValidatedMap } from '../../../core-v2/chrome/collections/validatedMap';
import { BreakpointsUpdater } from './breakpointsUpdater';
import { PerformChangesImmediatelyState } from './performChangesImmediatelyState';
import { ExtendedDebugClient } from '../../../testSupport/debugClient';
import {
IBPActionWhenHit,
AlwaysPause,
} from '../../../core-v2/chrome/internal/breakpoints/bpActionWhenHit';
import { findPositionOfTextInFile } from '../../../utils/findPositionOfTextInFile';
import { FileBreakpointsWizard } from '../fileBreakpointsWizard';
import { PromiseOrNot } from '../../../testUtils';
import { BatchingUpdatesState } from './batchingUpdatesState';
export type BreakpointWithId = MakePropertyRequired<DebugProtocol.Breakpoint, 'id'>;
export type BreakpointStatusChangedWithId = Replace<
DebugProtocol.BreakpointEvent['body'],
'breakpoint',
BreakpointWithId
>;
export class BreakpointsUpdate {
public constructor(
public readonly toAdd: BreakpointWizard[],
public readonly toRemove: BreakpointWizard[],
public readonly toKeepAsIs: BreakpointWizard[],
) {}
}
export interface IBreakpointsBatchingStrategy {
readonly currentBreakpointsMapping: CurrentBreakpointsMapping;
set(breakpointWizard: BreakpointWizard): void;
unset(breakpointWizard: BreakpointWizard): void;
waitUntilVerified(breakpoint: BreakpointWizard): Promise<void>;
assertIsVerified(breakpoint: BreakpointWizard): void;
assertIsNotVerified(breakpoint: BreakpointWizard, unverifiedReason: string): void;
assertIsHitThenResumeWhen(
breakpoint: BreakpointWizard,
lastActionToMakeBreakpointHit: () => Promise<unknown>,
verifications: IVerificationsAndAction,
): Promise<void>;
assertIsHitThenResume(
breakpoint: BreakpointWizard,
verifications: IVerificationsAndAction,
): Promise<void>;
onBreakpointStatusChange(breakpointStatusChanged: BreakpointStatusChangedWithId): void;
}
export type CurrentBreakpointsMapping = ValidatedMap<BreakpointWizard, BreakpointWithId>;
export type StateChanger = (newState: IBreakpointsBatchingStrategy) => void;
export class InternalFileBreakpointsWizard {
private readonly _breakpointsUpdater = new BreakpointsUpdater(
this._breakpointsWizard,
this,
this.client,
state => (this._state = state),
);
private _state: IBreakpointsBatchingStrategy = new PerformChangesImmediatelyState(
this._breakpointsWizard,
this,
new ValidatedMap(),
);
public constructor(
private readonly _breakpointsWizard: BreakpointsWizard,
public readonly client: ExtendedDebugClient,
public readonly filePath: string,
) {}
public async breakpoint(options: {
name: string;
text: string;
boundText?: string;
actionWhenHit?: IBPActionWhenHit;
}) {
const position = await findPositionOfTextInFile(this.filePath, options.text);
const boundPosition = options.boundText
? await findPositionOfTextInFile(this.filePath, options.boundText)
: position;
const actionWhenHit = options.actionWhenHit || new AlwaysPause();
return new BreakpointWizard(this, position, actionWhenHit, options.name, boundPosition);
}
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 assertIsNotVerified(breakpoint: BreakpointWizard, unverifiedReason: string) {
this._state.assertIsNotVerified(breakpoint, unverifiedReason);
}
public async assertIsHitThenResumeWhen(
breakpoint: BreakpointWizard,
lastActionToMakeBreakpointHit: () => Promise<unknown>,
verifications: IVerificationsAndAction,
): Promise<void> {
return this._state.assertIsHitThenResumeWhen(
breakpoint,
lastActionToMakeBreakpointHit,
verifications,
);
}
public async assertIsHitThenResume(
breakpoint: BreakpointWizard,
verifications: IVerificationsAndAction,
): Promise<void> {
return this._state.assertIsHitThenResume(breakpoint, verifications);
}
public onBreakpointStatusChange(breakpointStatusChanged: BreakpointStatusChangedWithId): 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 this._breakpointsUpdater.update(update);
}
}

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

@ -1,97 +0,0 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/
import { BreakpointWizard } from '../breakpointWizard';
import {
IBreakpointsBatchingStrategy,
InternalFileBreakpointsWizard,
CurrentBreakpointsMapping,
BreakpointsUpdate,
BreakpointStatusChangedWithId,
} from './internalFileBreakpointsWizard';
import { BreakpointsAssertions } from './breakpointsAssertions';
import { BreakpointsWizard, IVerificationsAndAction } from '../breakpointsWizard';
import { ValidatedMap } from '../../../core-v2/chrome/collections/validatedMap';
export class PerformChangesImmediatelyState implements IBreakpointsBatchingStrategy {
private readonly _idToBreakpoint = new ValidatedMap<number, BreakpointWizard>();
private readonly _breakpointsAssertions = new BreakpointsAssertions(
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: BreakpointStatusChangedWithId): void {
const breakpoint = this._idToBreakpoint.get(breakpointStatusChanged.breakpoint.id);
this.currentBreakpointsMapping.setAndReplaceIfExist(
breakpoint,
breakpointStatusChanged.breakpoint,
);
}
public assertIsVerified(breakpoint: BreakpointWizard): void {
this._breakpointsAssertions.assertIsVerified(breakpoint);
}
public assertIsNotVerified(breakpoint: BreakpointWizard, unverifiedReason: string): void {
this._breakpointsAssertions.assertIsNotVerified(breakpoint, unverifiedReason);
}
public async waitUntilVerified(breakpoint: BreakpointWizard): Promise<void> {
await this._breakpointsAssertions.waitUntilVerified(breakpoint);
}
public async assertIsHitThenResumeWhen(
breakpoint: BreakpointWizard,
lastActionToMakeBreakpointHit: () => Promise<void>,
verifications: IVerificationsAndAction,
): Promise<void> {
await this._breakpointsWizard.assertIsHitThenResumeWhen(
[breakpoint],
lastActionToMakeBreakpointHit,
verifications,
);
}
public async assertIsHitThenResume(
breakpoint: BreakpointWizard,
verifications: IVerificationsAndAction,
): Promise<void> {
await this._breakpointsWizard.assertIsHitThenResume(breakpoint, verifications);
}
private currentBreakpoints(): BreakpointWizard[] {
return Array.from(this.currentBreakpointsMapping.keys());
}
}

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

@ -1,8 +0,0 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/
/** Remove the whitespaces from the start of each line and any comments we find at the end */
export function trimWhitespaceAndComments(printedTestInput: string): string {
return printedTestInput.replace(/^\s+/gm, '').replace(/ ?\/\/.*$/gm, ''); // Remove the white space we put at the start of the lines to make the printed test input align with the code
}

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

@ -1,140 +0,0 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/
import * as assert from 'assert';
import * as path from 'path';
import { expect } from 'chai';
import { DebugProtocol } from 'vscode-debugprotocol';
import { BreakpointsWizard } from '../breakpointsWizard';
import * as testSetup from '../../../testSetup';
import { URL } from 'url';
export interface ExpectedSource {
fileRelativePath?: string;
url?: URL;
evalCode?: boolean;
}
export interface ExpectedFrame {
name: string | RegExp;
line?: number;
column?: number;
source?: ExpectedSource;
presentationHint?: string;
}
export class StackTraceObjectAssertions {
private readonly _projectRoot: string;
public constructor(breakpointsWizard: BreakpointsWizard) {
this._projectRoot = breakpointsWizard.project.props.projectRoot;
}
private assertSourceMatches(
actual: DebugProtocol.Source | undefined,
expected: ExpectedSource | undefined,
index: number,
) {
// tslint:disable-next-line: triple-equals
if (actual == null && expected == null) {
// TODO@rob
return;
}
// tslint:disable-next-line: triple-equals
if (expected == null) {
// TODO@rob
assert.fail(`Source was returned for frame ${index} but none was expected`);
return;
}
// tslint:disable-next-line: triple-equals
if (actual == null) {
// TODO@rob
assert.fail(`Source was expected for frame ${index} but none was returned`);
return;
}
let expectedName: string;
let expectedPath: string;
if (expected.fileRelativePath) {
// Generate the expected path from the relative path and the project root
expectedPath = path.join(this._projectRoot, expected.fileRelativePath);
expectedName = path.parse(expectedPath).base;
} else if (expected.url) {
expectedName = expected.url.host;
expectedPath = expected.url.toString();
} else if (expected.evalCode === true) {
// Eval code has source that looks like 'VM123'. Check it by regex instead.
expect(actual.name).to.match(/.*VM.*/, `Frame ${index} source name`);
expect(actual.path).to.match(/.*VM.*/, `Frame ${index} source path`);
return;
} else {
assert.fail(
'Not enough information for expected source: set either "fileRelativePath" or "urlRelativePath" or "eval"',
);
return;
}
expect(actual.name).to.equal(expectedName, `Frame ${index} source name`);
expect(actual.path).to.equal(expectedPath, `Frame ${index} source path`);
}
private assertFrameMatches(
actual: DebugProtocol.StackFrame,
expected: ExpectedFrame,
index: number,
) {
if (typeof expected.name === 'string') {
expect(actual.name).to.equal(expected.name, `Frame ${index} name`);
} else if (expected.name instanceof RegExp) {
expect(actual.name).to.match(expected.name, `Frame ${index} name`);
}
expect(actual.line).to.equal(expected.line, `Frame ${index} line`);
expect(actual.column).to.equal(expected.column, `Frame ${index} column`);
// Normal V1 stack frames will have no presentationHint, normal V2 stack frames will have presentationHint 'normal'
if (testSetup.isThisV1 && expected.presentationHint === 'normal') {
// tslint:disable-next-line:no-unused-expression
expect(actual.presentationHint, `Frame ${index} presentationHint`).to.be.undefined;
} else {
expect(actual.presentationHint).to.equal(
expected.presentationHint,
`Frame ${index} presentationHint`,
);
}
this.assertSourceMatches(actual.source, expected.source, index);
}
private assertResponseMatchesFrames(
actualFrames: DebugProtocol.StackFrame[],
expectedFrames: ExpectedFrame[],
) {
// Check array length
expect(actualFrames.length).to.equal(expectedFrames.length, 'Number of stack frames');
// Check each frame
actualFrames.forEach((actualFrame, i) => {
this.assertFrameMatches(actualFrame, expectedFrames[i], i);
});
}
public assertResponseMatches(
stackTraceFrames: DebugProtocol.StackFrame[],
expectedFrames: ExpectedFrame[],
) {
try {
this.assertResponseMatchesFrames(stackTraceFrames, expectedFrames);
} catch (e) {
const error: assert.AssertionError = e;
error.message +=
'\nActual stack trace response: \n' + JSON.stringify(stackTraceFrames, null, 2);
throw error;
}
}
}

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

@ -1,67 +0,0 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/
import * as assert from 'assert';
import { DebugProtocol } from 'vscode-debugprotocol';
import { expect } from 'chai';
import { BreakpointWizard } from '../breakpointWizard';
import { trimWhitespaceAndComments } from './printedTestInputl';
import { findLineNumber } from '../../../utils/findPositionOfTextInFile';
export class StackTraceStringAssertions {
public constructor(private readonly _breakpoint: BreakpointWizard) {}
public assertResponseMatches(
stackTraceFrames: DebugProtocol.StackFrame[],
expectedString: string,
) {
stackTraceFrames.forEach(frame => {
// Warning: We don't currently validate frame.source.path
expect(frame.source).not.to.equal(undefined);
const expectedSourceNameAndLine = ` [${frame.source!.name}] Line ${frame.line}`;
(expect(
frame.name,
'Expected the formatted name to match the source name and line supplied as individual attributes',
).to as any).endsWith(expectedSourceNameAndLine); // TODO@rob
});
const formattedExpectedStackTrace = trimWhitespaceAndComments(expectedString);
this.applyIgnores(formattedExpectedStackTrace, stackTraceFrames);
const actualStackTrace = this.extractStackTrace(stackTraceFrames);
assert.equal(
actualStackTrace,
formattedExpectedStackTrace,
`Expected the stack trace when hitting ${this._breakpoint} to be:\n${formattedExpectedStackTrace}\nyet it is:\n${actualStackTrace}`,
);
}
private applyIgnores(
formattedExpectedStackTrace: string,
stackTrace: DebugProtocol.StackFrame[],
): void {
const ignoreFunctionNameText = '<__IGNORE_FUNCTION_NAME__>';
const lineWithIgnoreIndex = formattedExpectedStackTrace.indexOf(ignoreFunctionNameText);
if (lineWithIgnoreIndex >= 0) {
const ignoreFunctionName = findLineNumber(formattedExpectedStackTrace, lineWithIgnoreIndex);
expect(stackTrace.length).to.be.greaterThan(ignoreFunctionName);
const ignoredFrame = stackTrace[ignoreFunctionName];
ignoredFrame.name = `${ignoreFunctionNameText} [${ignoredFrame.source!.name}] Line ${
ignoredFrame.line
}`;
}
}
private extractStackTrace(stackTrace: DebugProtocol.StackFrame[]): string {
return stackTrace.map(f => this.printStackTraceFrame(f)).join('\n');
}
private printStackTraceFrame(frame: DebugProtocol.StackFrame): string {
const frameName = frame.name;
return `${frameName}:${frame.column}${
frame.presentationHint && frame.presentationHint !== 'normal'
? ` (${frame.presentationHint})`
: ''
}`;
}
}

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

@ -1,170 +0,0 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/
import { DebugProtocol } from 'vscode-debugprotocol';
import { expect } from 'chai';
import { ValidatedMap } from '../core-v2/chrome/collections/validatedMap';
import { ExtendedDebugClient, THREAD_ID } from '../testSupport/debugClient';
import { wrapWithMethodLogger } from '../core-v2/chrome/logging/methodsCalledLogger';
import { waitUntilReadyWithTimeout } from '../utils/waitUntilReadyWithTimeout';
import { isThisV2 } from '../testSetup';
import { promiseTimeout } from '../testUtils';
enum EventToConsume {
Paused,
Resumed,
None,
}
/** Helper methods to wait and/or verify when the debuggee was paused for any kind of pause.
*
* Warning: Needs to be created before the debuggee is launched to capture all events and avoid race conditions
*/
export class PausedWizard {
private _noMoreEventsExpected = false;
private _eventsToBeConsumed: (DebugProtocol.ContinuedEvent | DebugProtocol.StoppedEvent)[] = [];
private static _clientToPausedWizard = new ValidatedMap<ExtendedDebugClient, PausedWizard>();
private constructor(private readonly _client: ExtendedDebugClient) {
this._client.on('stopped', stopped => this.onEvent(stopped));
this._client.on('continued', continued => this.onEvent(continued));
}
private onEvent(continued: any) {
this.validateNoMoreEventsIfSet(continued);
this._eventsToBeConsumed.push(continued);
this.logState();
}
// The PausedWizard logic will break if we create 2 PausedWizards for the same client. So we warranty we only create one
public static forClient(client: ExtendedDebugClient): PausedWizard {
return this._clientToPausedWizard.getOrAdd(client, () =>
wrapWithMethodLogger(new PausedWizard(client)),
);
}
/**
* Verify that the debuggee is not paused
*
* @param millisecondsToWaitForPauses How much time to wait for pauses
*/
public async waitAndConsumeResumedEvent(): Promise<void> {
await waitUntilReadyWithTimeout(() => this.nextEventToConsume === EventToConsume.Resumed);
this.markNextEventAsConsumed('continued');
}
/** Return whether the debuggee is currently paused */
public isPaused(): boolean {
return this.nextEventToConsume === EventToConsume.Paused;
}
/** Wait and block until the debuggee is paused on a debugger statement */
public async waitUntilPausedOnDebuggerStatement(): Promise<void> {
return this.waitAndConsumePausedEvent(pauseInfo => {
expect(pauseInfo.description).to.equal('Paused on debugger statement');
expect(pauseInfo.reason).to.equal('debugger_statement');
});
}
/** Wait and block until the debuggee is paused, and then perform the specified action with the pause event's body */
public async waitAndConsumePausedEvent(
actionWithPausedInfo: (pausedInfo: DebugProtocol.StoppedEvent['body']) => void,
): Promise<void> {
await waitUntilReadyWithTimeout(() => this.nextEventToConsume === EventToConsume.Paused);
const pausedEvent = <DebugProtocol.StoppedEvent>this._eventsToBeConsumed[0];
this.markNextEventAsConsumed('stopped');
await actionWithPausedInfo(pausedEvent.body);
}
/** Wait and block until the debuggee has been resumed */
public async waitUntilResumed(): Promise<void> {
// We assume that nobody is consuming events in parallel, so if we start paused, the wait call won't ever succeed
expect(this.nextEventToConsume).to.not.equal(EventToConsume.Paused);
await waitUntilReadyWithTimeout(() => this.nextEventToConsume === EventToConsume.Resumed);
this.markNextEventAsConsumed('continued');
}
/**
* Instruct the debuggee to resume, and verify that the Debug-Adapter sends the proper notification after that happens
*/
public async resume(): Promise<void> {
await this._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.waitUntilResumed();
}
}
/**
* Instruct the debuggee to pause, and verify that the Debug-Adapter sends the proper notification after that happens
*/
public async pause(): Promise<void> {
await this._client.pauseRequest({ threadId: THREAD_ID });
await this.waitAndConsumePausedEvent(event => {
expect(event.reason).to.equal('pause');
expect(event.description).to.equal('Paused on user request');
});
}
public async waitAndAssertNoMoreEvents(): Promise<void> {
expect(this.nextEventToConsume).to.equal(EventToConsume.None);
this._noMoreEventsExpected = true;
// Wait some time, to see if any events appear eventually
await promiseTimeout(undefined, 500);
expect(this.nextEventToConsume).to.equal(EventToConsume.None);
}
private validateNoMoreEventsIfSet(
event: DebugProtocol.ContinuedEvent | DebugProtocol.StoppedEvent,
): void {
if (this._noMoreEventsExpected && event.event !== 'continued') {
// TODO@rob
throw new Error(
`Received an event after it was signaled that no more events were expected: ${JSON.stringify(
event,
)}`,
);
}
}
private logState() {
// logger.log(`Resume/Pause #events = ${this._eventsToBeConsumed.length}, state = ${EventToConsume[this.nextEventToConsume]}`); // TODO@rob
}
private get nextEventToConsume(): EventToConsume {
if (this._eventsToBeConsumed.length === 0) {
return EventToConsume.None;
} else {
const nextEventToBeConsumed = this._eventsToBeConsumed[0];
switch (nextEventToBeConsumed.event) {
case 'stopped':
return EventToConsume.Paused;
case 'continued':
return EventToConsume.Resumed;
default:
throw new Error(
`Expected the event to be consumed to be either a stopped or continued yet it was: ${JSON.stringify(
nextEventToBeConsumed,
)}`,
);
}
}
}
private markNextEventAsConsumed(eventName: 'continued' | 'stopped'): void {
expect(this._eventsToBeConsumed).length.to.be.greaterThan(0);
expect(this._eventsToBeConsumed[0].event).to.equal(eventName);
this._eventsToBeConsumed.shift();
this.logState();
}
public toString(): string {
return 'PausedWizard';
}
}

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

@ -1,4 +0,0 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/
export type ManyVariablesPropertiesPrinted = string; // `${variable.name} = ${variable.value} ${(variable.type)}\n`

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

@ -1,143 +0,0 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/
import { expect } from 'chai';
import * as _ from 'lodash';
import { DebugProtocol } from 'vscode-debugprotocol';
import { VariablesScopeName } from './variablesWizard';
import { ValidatedSet, IValidatedSet } from '../../core-v2/chrome/collections/validatedSet';
import { singleElementOfArray } from '../../core-v2/chrome/collections/utilities';
import { ExtendedDebugClient, THREAD_ID } from '../../testSupport/debugClient';
interface IVariablesOfScope {
scopeName: VariablesScopeName;
variables: DebugProtocol.Variable[];
}
const defaultStackFrameFormat: DebugProtocol.StackFrameFormat = {
parameters: true,
parameterTypes: true,
parameterNames: true,
line: true,
module: true,
};
export async function stackTrace(
client: ExtendedDebugClient,
optionalStackFrameFormat?: DebugProtocol.StackFrameFormat,
): Promise<DebugProtocol.StackTraceResponse['body']> {
const stackFrameFormat = _.defaultTo(optionalStackFrameFormat, defaultStackFrameFormat);
const stackTraceResponse = await client.send('stackTrace', {
threadId: THREAD_ID,
format: stackFrameFormat,
});
expect(
stackTraceResponse.success,
`Expected the response to the stack trace request to be succesful yet it failed: ${JSON.stringify(
stackTraceResponse,
)}`,
).to.equal(true);
// Check totalFrames property
expect(stackTraceResponse.body.totalFrames).to.equal(
stackTraceResponse.body.stackFrames.length,
'body.totalFrames',
);
return stackTraceResponse.body;
}
export async function topStackFrame(
client: ExtendedDebugClient,
optionalStackFrameFormat?: DebugProtocol.StackFrameFormat,
): Promise<DebugProtocol.StackFrame> {
const stackFrames = (await stackTrace(client, optionalStackFrameFormat)).stackFrames;
expect(stackFrames.length).to.be.greaterThan(0);
return stackFrames[0];
}
/** Utility functions to operate on the stack straces and stack frames of the debuggee.
* It also provides utilities to access the scopes available in a particular stack frame.
*/
export class StackFrameWizard {
public constructor(
private readonly _client: ExtendedDebugClient,
private readonly _stackFrame: DebugProtocol.StackFrame,
) {}
/** Return a Wizard to interact with the top stack frame of the debuggee of the client */
public static async topStackFrame(client: ExtendedDebugClient): Promise<StackFrameWizard> {
return new StackFrameWizard(client, await topStackFrame(client));
}
/** Return the variables information for the scopes selected by name */
public async variablesOfScopes(
manyScopeNames: VariablesScopeName[],
): Promise<IVariablesOfScope[]> {
const scopes = await this.scopesByNames(manyScopeNames);
return Promise.all(
scopes.map(async scope => {
const variablesResponse = await this._client.variablesRequest({
variablesReference: scope!.variablesReference,
});
expect(variablesResponse.success).to.equal(true);
expect(variablesResponse.body).not.to.equal(undefined);
const variables = variablesResponse.body.variables;
expect(variables).not.to.equal(undefined);
return { scopeName: <VariablesScopeName>scope.name.toLowerCase(), variables };
}),
);
}
private async scopesByNames(
manyScopeNames: VariablesScopeName[],
): Promise<DebugProtocol.Scope[]> {
const scopeNamesSet = new ValidatedSet(manyScopeNames.map(name => name.toLowerCase()));
const requestedScopes = (await this.scopes()).filter(scope =>
scopeNamesSet.has(scope.name.toLowerCase()),
);
expect(requestedScopes).to.have.lengthOf(manyScopeNames.length);
return requestedScopes;
}
/** Return all the scopes available in the underlying stack frame */
public async scopes(): Promise<DebugProtocol.Scope[]> {
const scopesResponse = await this._client.scopesRequest({ frameId: this._stackFrame.id });
// logger.log(`Scopes: ${scopesResponse.body.scopes.map(s => s.name).join(', ')}`); // TODO@rob
return scopesResponse.body.scopes;
}
/** Return the names of all the global variables in the underlying stack frame */
public async globalVariableNames(): Promise<IValidatedSet<string>> {
const existingGlobalVariables = await this.variablesOfScope('global');
return new ValidatedSet(existingGlobalVariables.map(variable => variable.name));
}
/** Return the variables information for a particular scope of the underlying stack frame */
public async variablesOfScope(scopeName: VariablesScopeName): Promise<DebugProtocol.Variable[]> {
return singleElementOfArray(await this.variablesOfScopes([scopeName])).variables;
}
public async variable(
variableName: string,
): Promise<{ scope: DebugProtocol.Scope; variable: DebugProtocol.Variable }> {
const scopes = await this.scopes();
for (const scope of scopes) {
const variablesResponse = await this._client.variablesRequest({
variablesReference: scope!.variablesReference,
});
expect(variablesResponse.success).to.equal(true);
expect(variablesResponse.body).not.to.equal(undefined);
const variables = variablesResponse.body.variables;
expect(variables).not.to.equal(undefined);
const variable = variables.find(eachVariable => eachVariable.name === variableName);
if (variable !== undefined) {
return { scope, variable };
}
}
throw new Error(
`A variable named ${variableName} wasn't found in any of the scopes of ${this}`,
);
}
}

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

@ -1,22 +0,0 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/
export interface IVariableInformation {
name: string;
value: string;
type?: string;
}
/**
* Print a collection of variable informations to make it easier to compare
* the expected variables of a test, and the actual variables of the debuggee
*/
export function printVariables(variables: IVariableInformation[]): string {
const variablesPrinted = variables.map(variable => printVariable(variable));
return variablesPrinted.join('\n');
}
function printVariable(variable: IVariableInformation): string {
return `${variable.name} = ${variable.value} (${variable.type})`;
}

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

@ -1,100 +0,0 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/
import * as _ from 'lodash';
import { expect } from 'chai';
import { DebugProtocol } from 'vscode-debugprotocol';
import { trimWhitespaceAndComments } from '../breakpoints/implementation/printedTestInputl';
import { ManyVariablesValues } from './variablesWizard';
import { printVariables } from './variablesPrinting';
/** Whether the expected variables should match exactly the actual variables of the debuggee
* or whether the expected variables should only be a subset of the actual variables of the debuggee
*/
export enum KindOfVerification {
SameAndExact /** Same exact variables */,
ProperSubset /** Expected variables are a subset of the actual variables */,
}
/**
* Provide methods to validate that the variables appearing on the stack trace are what we expect
*/
export class VariablesVerifier {
/** Verify that the actual variables are exactly the variables that we expect */
public assertVariablesAre(
variables: DebugProtocol.Variable[],
expectedVariables: string | ManyVariablesValues,
): void {
if (typeof expectedVariables === 'string') {
this.assertVariablesPrintedAre(variables, expectedVariables);
} else {
this.assertVariablesValuesAre(variables, expectedVariables);
}
}
private assertVariablesPrintedAre(
variables: DebugProtocol.Variable[],
expectedVariablesPrinted: string,
): void {
const trimmedVariables = trimWhitespaceAndComments(expectedVariablesPrinted);
expect(printVariables(variables)).to.equal(trimmedVariables);
}
private assertVariablesValuesAre(
manyVariables: DebugProtocol.Variable[],
expectedVariablesValues: ManyVariablesValues,
): void {
return this.assertVariablesValuesSatisfy(
manyVariables,
expectedVariablesValues,
KindOfVerification.SameAndExact,
);
}
/** Verify that the actual variables include as a proper subset the variables that we expect */
public assertVariablesValuesContain(
manyVariables: DebugProtocol.Variable[],
expectedVariablesValues: ManyVariablesValues,
): void {
return this.assertVariablesValuesSatisfy(
manyVariables,
expectedVariablesValues,
KindOfVerification.ProperSubset,
);
}
/** Verify that the actual variables match the expected variables with the verification specified as a parameter (Same or subset) */
public assertVariablesValuesSatisfy(
manyVariables: DebugProtocol.Variable[],
expectedVariablesValues: ManyVariablesValues,
kindOfVerification: KindOfVerification,
): void {
const actualVariableNames = manyVariables.map(variable => variable.name);
const expectedVariablesNames = Object.keys(expectedVariablesValues);
switch (kindOfVerification) {
case KindOfVerification.ProperSubset:
expect(actualVariableNames).to.contain.members(expectedVariablesNames);
break;
case KindOfVerification.SameAndExact:
expect(actualVariableNames).to.have.members(expectedVariablesNames);
break;
default:
throw new Error(`Unexpected comparison algorithm: ${kindOfVerification}`);
}
for (const variable of manyVariables) {
const variableName = variable.name;
if (expectedVariablesNames.indexOf(variableName) >= 0) {
const expectedValue = expectedVariablesValues[variableName];
expect(expectedValue).to.not.equal(undefined);
expect(variable!.evaluateName).to.be.equal(variable!.name); // Is this ever different?
expect(variable!.variablesReference).to.be.greaterThan(-1);
expect(variable!.value).to.be.equal(`${expectedValue}`);
// TODO: Validate variable type too
} else {
expect(kindOfVerification).to.equal(KindOfVerification.ProperSubset); // This should not happen for same elements
}
}
}
}

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

@ -1,151 +0,0 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/
import * as _ from 'lodash';
import { PromiseOrNot } from '../../testUtils';
import { StackFrameWizard } from './stackFrameWizard';
import { VariablesVerifier } from './variablesVerifier';
import { ValidatedMap } from '../../core-v2/chrome/collections/validatedMap';
import { trimWhitespaceAndComments } from '../breakpoints/implementation/printedTestInputl';
import { expect } from 'chai';
import { printVariables } from './variablesPrinting';
import { ExtendedDebugClient } from '../../testSupport/debugClient';
export interface VariablePrintedProperties {
value: string;
type: string;
}
export interface ManyVariablePrintedProperties {
[variableName: string]: VariablePrintedProperties;
}
export interface ManyVariablesValues {
[variableName: string]: unknown;
}
export type ManyVariablesPropertiesPrinted = string; // `${variable.name} = ${variable.value} ${(variable.type)}\n`
export type IScopeExpectedVariables = ManyVariablesPropertiesPrinted | ManyVariablesValues;
export interface IExpectedVariables {
script?: IScopeExpectedVariables;
local?: IScopeExpectedVariables;
global?: IScopeExpectedVariables;
catch?: IScopeExpectedVariables;
block?: IScopeExpectedVariables;
closure?: IScopeExpectedVariables;
eval?: IScopeExpectedVariables;
with?: IScopeExpectedVariables;
module?: IScopeExpectedVariables;
local_contains?: ManyVariablesValues;
}
export type VariablesScopeName = keyof IExpectedVariables;
export type VerificationModifier = 'contains' | '';
export class VariablesWizard {
public constructor(private readonly _client: ExtendedDebugClient) {}
/** Verify that the global variables have the expected values, ignoring the variables in <namesOfGlobalsToIgnore> */
public async assertNewGlobalVariariablesAre(
actionThatAddsNewVariables: () => PromiseOrNot<void>,
expectedGlobals: ManyVariablesPropertiesPrinted,
): Promise<void> {
// Store pre-existing global variables' names
const namesOfGlobalsToIgnore = await (await this.topStackFrameHelper()).globalVariableNames();
// Perform an action that adds new global variables
await actionThatAddsNewVariables();
const globalsOnFrame = await (await this.topStackFrameHelper()).variablesOfScope('global');
const nonIgnoredGlobals = globalsOnFrame.filter(
global => !namesOfGlobalsToIgnore.has(global.name),
);
const expectedGlobalsTrimmed = trimWhitespaceAndComments(expectedGlobals);
expect(printVariables(nonIgnoredGlobals)).to.equal(expectedGlobalsTrimmed);
}
/**
* Verify that the stackFrame contains some variables with a specific value
*/
public async assertTopFrameVariablesAre(verifications: IExpectedVariables): Promise<void> {
await this.assertStackFrameVariablesAre(await this.topStackFrameHelper(), verifications);
}
public async assertStackFrameVariablesAre(
stackFrame: StackFrameWizard,
verifications: IExpectedVariables,
) {
const scopesWithModifiers = Object.keys(verifications);
const scopesWithoutModifiers = scopesWithModifiers.map(
s => this.splitIntoScopeNameAndModifier(s)[0],
);
const zippedScopes = _.zip(scopesWithoutModifiers, scopesWithModifiers) as [
keyof IExpectedVariables,
keyof IExpectedVariables,
][];
const withoutModifiersToWith = new ValidatedMap<
keyof IExpectedVariables,
keyof IExpectedVariables
>(zippedScopes);
const manyScopes = await stackFrame.variablesOfScopes(scopesWithoutModifiers);
for (const scope of manyScopes) {
const scopeNameWithModifier = withoutModifiersToWith.get(scope.scopeName)!;
const [, modifier] = this.splitIntoScopeNameAndModifier(scopeNameWithModifier);
switch (modifier) {
case '':
this.verificator.assertVariablesAre(
scope.variables,
verifications[scopeNameWithModifier] as IScopeExpectedVariables,
);
break;
case 'contains':
this.verificator.assertVariablesValuesContain(
scope.variables,
<ManyVariablesValues>verifications[scopeNameWithModifier]!,
);
break;
default:
throw new Error(
`Unknown modified used for variables verification: ${modifier} in ${scopeNameWithModifier}`,
);
}
}
}
public async set(variableName: string, newValue: string): Promise<void> {
const stackFrame = await this.topStackFrameHelper();
const { scope, variable } = await stackFrame.variable(variableName);
const response = await this._client.setVariableRequest({
variablesReference: scope.variablesReference,
name: variable.name,
value: newValue,
});
expect(response.success).to.equal(true);
}
private splitIntoScopeNameAndModifier(
modifiedScopeName: keyof IExpectedVariables,
): [VariablesScopeName, VerificationModifier] {
const components = modifiedScopeName.split('_');
if (components.length > 2) {
throw new Error(`Invalid modified scope name: ${modifiedScopeName}`);
}
return [
<VariablesScopeName>components[0],
<VerificationModifier>_.defaultTo(components[1], ''),
];
}
private get verificator(): VariablesVerifier {
return new VariablesVerifier();
}
private async topStackFrameHelper(): Promise<StackFrameWizard> {
return await StackFrameWizard.topStackFrame(this._client);
}
}

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

@ -0,0 +1,7 @@
{
allThreadsStopped : false
description : Paused on breakpoint
reason : breakpoint
threadId : <number>
}
<anonymous> @ ${workspaceFolder}/web/script.js:9:1

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

@ -0,0 +1,15 @@
{
allThreadsStopped : false
description : Paused on breakpoint
reason : breakpoint
threadId : <number>
}
<anonymous> @ ${workspaceFolder}/web/condition.js:2:3
result: 2
{
allThreadsStopped : false
description : Paused on debugger statement
reason : pause
threadId : <number>
}
<anonymous> @ ${workspaceFolder}/web/condition.js:5:1

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

@ -0,0 +1,7 @@
{
allThreadsStopped : false
description : Paused on debugger statement
reason : pause
threadId : <number>
}
<anonymous> @ ${workspaceFolder}/web/condition.js:5:1

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

@ -0,0 +1,7 @@
{
allThreadsStopped : false
description : Paused on debugger statement
reason : pause
threadId : <number>
}
<anonymous> @ ${workspaceFolder}/web/condition.js:5:1

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

@ -0,0 +1,15 @@
{
allThreadsStopped : false
description : Paused on breakpoint
reason : breakpoint
threadId : <number>
}
<anonymous> @ ${workspaceFolder}/web/condition.js:2:3
result: 1
{
allThreadsStopped : false
description : Paused on debugger statement
reason : pause
threadId : <number>
}
<anonymous> @ ${workspaceFolder}/web/condition.js:5:1

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

@ -0,0 +1,16 @@
{
allThreadsStopped : false
description : Paused on breakpoint
reason : breakpoint
threadId : <number>
}
<anonymous> @ ${workspaceFolder}/web/condition.js:2:3
result: 3
{
allThreadsStopped : false
description : Paused on breakpoint
reason : breakpoint
threadId : <number>
}
<anonymous> @ ${workspaceFolder}/web/condition.js:2:3
result: 4

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

@ -0,0 +1,8 @@
stderr> Invalid hit condition "abc". Expected an expression like "> 42" or "== 2".
{
allThreadsStopped : false
description : Paused on debugger statement
reason : pause
threadId : <number>
}
<anonymous> @ ${workspaceFolder}/web/condition.js:5:1

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

@ -0,0 +1,23 @@
{
allThreadsStopped : false
description : Paused on breakpoint
reason : breakpoint
threadId : <number>
}
<anonymous> @ ${workspaceFolder}/web/condition.js:2:3
result: 0
{
allThreadsStopped : false
description : Paused on breakpoint
reason : breakpoint
threadId : <number>
}
<anonymous> @ ${workspaceFolder}/web/condition.js:2:3
result: 1
{
allThreadsStopped : false
description : Paused on debugger statement
reason : pause
threadId : <number>
}
<anonymous> @ ${workspaceFolder}/web/condition.js:5:1

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

@ -407,6 +407,122 @@ describe('breakpoints', () => {
});
});
describe('hit condition', () => {
async function waitForPauseAndLogI(p: ITestHandle) {
await waitForPause(p, async () => {
await p.logger.evaluateAndLog('i');
});
}
itIntegrates('exact', async ({ r }) => {
const p = await r.launchUrl('condition.html');
const source: Dap.Source = {
path: p.workspacePath('web/condition.js'),
};
await p.dap.setBreakpoints({
source,
breakpoints: [{ line: 2, column: 0, hitCondition: '==2' }],
});
p.load();
await waitForPauseAndLogI(p);
await waitForPause(p);
p.assertLog();
});
itIntegrates('less than', async ({ r }) => {
// Breakpoint in separate script set before launch.
const p = await r.launchUrl('condition.html');
const source: Dap.Source = {
path: p.workspacePath('web/condition.js'),
};
await p.dap.setBreakpoints({
source,
breakpoints: [{ line: 2, column: 0, hitCondition: '<3' }],
});
p.load();
await waitForPauseAndLogI(p);
await waitForPauseAndLogI(p);
await waitForPause(p);
p.assertLog();
});
itIntegrates('greater than', async ({ r }) => {
// Breakpoint in separate script set before launch.
const p = await r.launchUrl('condition.html');
const source: Dap.Source = {
path: p.workspacePath('web/condition.js'),
};
await p.dap.setBreakpoints({
source,
breakpoints: [{ line: 2, column: 0, hitCondition: '>3' }],
});
p.load();
await waitForPauseAndLogI(p);
await waitForPauseAndLogI(p);
p.assertLog();
});
itIntegrates('invalid', async ({ r }) => {
// Breakpoint in separate script set before launch.
const p = await r.launchUrl('condition.html');
const source: Dap.Source = {
path: p.workspacePath('web/condition.js'),
};
p.dap.on('output', output => {
if (output.category === 'stderr') {
p.logger.logOutput(output);
}
});
await p.dap.setBreakpoints({
source,
breakpoints: [{ line: 2, column: 0, hitCondition: 'abc' }],
});
p.load();
await waitForPause(p); // falls through to debugger statement
p.assertLog();
});
});
describe('condition', () => {
async function waitForPauseAndLogI(p: ITestHandle) {
await waitForPause(p, async () => {
await p.logger.evaluateAndLog('i');
});
}
itIntegrates('basic', async ({ r }) => {
const p = await r.launchUrl('condition.html');
const source: Dap.Source = {
path: p.workspacePath('web/condition.js'),
};
await p.dap.setBreakpoints({
source,
breakpoints: [{ line: 2, column: 0, condition: 'i==2' }],
});
p.load();
await waitForPauseAndLogI(p);
await waitForPause(p);
p.assertLog();
});
itIntegrates('ignores bp with invalid condition', async ({ r }) => {
// Breakpoint in separate script set before launch.
const p = await r.launchUrl('condition.html');
const source: Dap.Source = {
path: p.workspacePath('web/condition.js'),
};
await p.dap.setBreakpoints({
source,
breakpoints: [{ line: 2, column: 0, condition: ')(}{][.&' }],
});
p.load();
await waitForPause(p); // falls through to debugger statement
p.assertLog();
});
});
describe('custom', () => {
itIntegrates('inner html', async ({ r }) => {
// Custom breakpoint for innerHtml.

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

@ -0,0 +1,7 @@
{
allThreadsStopped : false
description : Paused on breakpoint
reason : breakpoint
threadId : <number>
}
App @ ${fixturesDir}/react-test/src/App.tsx:6:1

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

@ -0,0 +1,103 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/
import * as cp from 'child_process';
import { join } from 'path';
import { getDeferred } from '../../common/promiseUtil';
import Dap from '../../dap/api';
import { killTree } from '../../targets/node/killTree';
import { ITestHandle, testFixturesDir } from '../test';
import { itIntegrates } from '../testIntegrationUtils';
describe('react', () => {
async function waitForPause(p: ITestHandle, cb?: (threadId: string) => Promise<void>) {
const { threadId } = p.log(await p.dap.once('stopped'));
await p.logger.logStackTrace(threadId);
if (cb) await cb(threadId);
return p.dap.continue({ threadId });
}
const projectName = 'react-test';
let projectFolder: string;
let devServerProc: cp.ChildProcessWithoutNullStreams | undefined;
beforeEach(async function() {
this.timeout(60000 * 4);
projectFolder = join(testFixturesDir, projectName);
await setupCRA(projectName, testFixturesDir);
devServerProc = await startDevServer(projectFolder);
});
afterEach(() => {
if (devServerProc) {
console.log('Killing ' + devServerProc.pid);
killTree(devServerProc.pid);
}
});
itIntegrates('hit breakpoint', async ({ r }) => {
// Breakpoint in inline script set before launch.
const p = await r._launch('http://localhost:3000', {
webRoot: projectFolder,
__workspaceFolder: projectFolder,
rootPath: projectFolder,
});
const source: Dap.Source = {
path: join(projectFolder, 'src/App.tsx'),
};
await p.dap.setBreakpoints({ source, breakpoints: [{ line: 6, column: 0 }] });
p.load();
await waitForPause(p);
p.assertLog({ substring: true });
});
});
async function setupCRA(projectName: string, cwd: string): Promise<void> {
console.log('Setting up CRA in ' + cwd);
const setupProc = cp.spawn(
'npx',
['create-react-app', '--template', 'cra-template-typescript', projectName],
{
cwd,
stdio: 'pipe',
env: process.env,
},
);
setupProc.stdout.on('data', d => console.log(d.toString().replace(/\r?\n$/, '')));
setupProc.stderr.on('data', d => console.error(d.toString().replace(/\r?\n$/, '')));
const done = getDeferred();
setupProc.once('exit', () => {
done.resolve(undefined);
});
await done.promise;
}
async function startDevServer(projectFolder: string): Promise<cp.ChildProcessWithoutNullStreams> {
const devServerListening = getDeferred();
const devServerProc = cp.spawn('npm', ['run-script', 'start'], {
env: { ...process.env, BROWSER: 'none', SKIP_PREFLIGHT_CHECK: 'true' },
cwd: projectFolder,
stdio: 'pipe',
});
const timer = setTimeout(() => {
console.log('Did not get recognized dev server output, continuing');
devServerListening.resolve(undefined);
}, 10000);
devServerProc.stdout.on('data', d => {
d = d.toString();
if (d.includes('You can now view')) {
console.log('Detected CRA dev server started');
devServerListening.resolve(undefined);
} else if (d.includes('Something is already')) {
devServerListening.reject(new Error('Failed to start the dev server: ' + d));
}
console.log(d.toString().replace(/\r?\n$/, ''));
});
devServerProc.stderr.on('data', d => console.error(d.toString().replace(/\r?\n$/, '')));
await devServerListening.promise;
clearTimeout(timer);
return devServerProc;
}

Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше