359 lines
11 KiB
JavaScript
359 lines
11 KiB
JavaScript
|
/*---------------------------------------------------------------------------------------------
|
||
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||
|
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||
|
*--------------------------------------------------------------------------------------------*/
|
||
|
|
||
|
//@ts-check
|
||
|
'use strict';
|
||
|
|
||
|
// mocha disables running through electron by default. Note that this must
|
||
|
// come before any mocha imports.
|
||
|
process.env.MOCHA_COLORS = '1';
|
||
|
|
||
|
const { app, BrowserWindow, ipcMain, crashReporter, session } = require('electron');
|
||
|
const product = require('../../../product.json');
|
||
|
const { tmpdir } = require('os');
|
||
|
const { existsSync, mkdirSync } = require('fs');
|
||
|
const path = require('path');
|
||
|
const mocha = require('mocha');
|
||
|
const events = require('events');
|
||
|
const MochaJUnitReporter = require('mocha-junit-reporter');
|
||
|
const url = require('url');
|
||
|
const net = require('net');
|
||
|
const createStatsCollector = require('mocha/lib/stats-collector');
|
||
|
const { applyReporter, importMochaReporter } = require('../reporter');
|
||
|
|
||
|
const minimist = require('minimist');
|
||
|
|
||
|
/**
|
||
|
* @type {{
|
||
|
* grep: string;
|
||
|
* run: string;
|
||
|
* runGlob: string;
|
||
|
* dev: boolean;
|
||
|
* reporter: string;
|
||
|
* 'reporter-options': string;
|
||
|
* 'waitServer': string;
|
||
|
* timeout: string;
|
||
|
* 'crash-reporter-directory': string;
|
||
|
* tfs: string;
|
||
|
* build: boolean;
|
||
|
* coverage: boolean;
|
||
|
* coveragePath: string;
|
||
|
* coverageFormats: string | string[];
|
||
|
* 'per-test-coverage': boolean;
|
||
|
* help: boolean;
|
||
|
* }}
|
||
|
*/
|
||
|
const args = minimist(process.argv.slice(2), {
|
||
|
string: ['grep', 'run', 'runGlob', 'reporter', 'reporter-options', 'waitServer', 'timeout', 'crash-reporter-directory', 'tfs', 'coveragePath', 'coverageFormats'],
|
||
|
boolean: ['build', 'coverage', 'help', 'dev', 'per-test-coverage'],
|
||
|
alias: {
|
||
|
'grep': ['g', 'f'],
|
||
|
'runGlob': ['glob', 'runGrep'],
|
||
|
'dev': ['dev-tools', 'devTools'],
|
||
|
'help': 'h'
|
||
|
},
|
||
|
default: {
|
||
|
'reporter': 'spec',
|
||
|
'reporter-options': ''
|
||
|
}
|
||
|
});
|
||
|
|
||
|
if (args.help) {
|
||
|
console.log(`Usage: node ${process.argv[1]} [options]
|
||
|
|
||
|
Options:
|
||
|
--grep, -g, -f <pattern> only run tests matching <pattern>
|
||
|
--run <file> only run tests from <file>
|
||
|
--runGlob, --glob, --runGrep <file_pattern> only run tests matching <file_pattern>
|
||
|
--build run with build output (out-build)
|
||
|
--coverage generate coverage report
|
||
|
--per-test-coverage generate a per-test V8 coverage report, only valid with the full-json-stream reporter
|
||
|
--dev, --dev-tools, --devTools <window> open dev tools, keep window open, reuse app data
|
||
|
--reporter <reporter> the mocha reporter (default: "spec")
|
||
|
--reporter-options <options> the mocha reporter options (default: "")
|
||
|
--waitServer <port> port to connect to and wait before running tests
|
||
|
--timeout <ms> timeout for tests
|
||
|
--crash-reporter-directory <path> crash reporter directory
|
||
|
--tfs <url> TFS server URL
|
||
|
--help, -h show the help`);
|
||
|
process.exit(0);
|
||
|
}
|
||
|
|
||
|
let crashReporterDirectory = args['crash-reporter-directory'];
|
||
|
if (crashReporterDirectory) {
|
||
|
crashReporterDirectory = path.normalize(crashReporterDirectory);
|
||
|
|
||
|
if (!path.isAbsolute(crashReporterDirectory)) {
|
||
|
console.error(`The path '${crashReporterDirectory}' specified for --crash-reporter-directory must be absolute.`);
|
||
|
app.exit(1);
|
||
|
}
|
||
|
|
||
|
if (!existsSync(crashReporterDirectory)) {
|
||
|
try {
|
||
|
mkdirSync(crashReporterDirectory);
|
||
|
} catch (error) {
|
||
|
console.error(`The path '${crashReporterDirectory}' specified for --crash-reporter-directory does not seem to exist or cannot be created.`);
|
||
|
app.exit(1);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Crashes are stored in the crashDumps directory by default, so we
|
||
|
// need to change that directory to the provided one
|
||
|
console.log(`Found --crash-reporter-directory argument. Setting crashDumps directory to be '${crashReporterDirectory}'`);
|
||
|
app.setPath('crashDumps', crashReporterDirectory);
|
||
|
|
||
|
crashReporter.start({
|
||
|
companyName: 'Microsoft',
|
||
|
productName: process.env['VSCODE_DEV'] ? `${product.nameShort} Dev` : product.nameShort,
|
||
|
uploadToServer: false,
|
||
|
compress: true
|
||
|
});
|
||
|
}
|
||
|
|
||
|
if (!args.dev) {
|
||
|
app.setPath('userData', path.join(tmpdir(), `vscode-tests-${Date.now()}`));
|
||
|
}
|
||
|
|
||
|
function deserializeSuite(suite) {
|
||
|
return {
|
||
|
root: suite.root,
|
||
|
suites: suite.suites,
|
||
|
tests: suite.tests,
|
||
|
title: suite.title,
|
||
|
titlePath: () => suite.titlePath,
|
||
|
fullTitle: () => suite.fullTitle,
|
||
|
timeout: () => suite.timeout,
|
||
|
retries: () => suite.retries,
|
||
|
slow: () => suite.slow,
|
||
|
bail: () => suite.bail
|
||
|
};
|
||
|
}
|
||
|
|
||
|
function deserializeRunnable(runnable) {
|
||
|
return {
|
||
|
title: runnable.title,
|
||
|
titlePath: () => runnable.titlePath,
|
||
|
fullTitle: () => runnable.fullTitle,
|
||
|
async: runnable.async,
|
||
|
slow: () => runnable.slow,
|
||
|
speed: runnable.speed,
|
||
|
duration: runnable.duration,
|
||
|
currentRetry: () => runnable.currentRetry
|
||
|
};
|
||
|
}
|
||
|
|
||
|
function deserializeError(err) {
|
||
|
const inspect = err.inspect;
|
||
|
err.inspect = () => inspect;
|
||
|
// Unfortunately, mocha rewrites and formats err.actual/err.expected.
|
||
|
// This formatting is hard to reverse, so err.*JSON includes the unformatted value.
|
||
|
if (err.actual) {
|
||
|
err.actual = JSON.parse(err.actual).value;
|
||
|
err.actualJSON = err.actual;
|
||
|
}
|
||
|
if (err.expected) {
|
||
|
err.expected = JSON.parse(err.expected).value;
|
||
|
err.expectedJSON = err.expected;
|
||
|
}
|
||
|
return err;
|
||
|
}
|
||
|
|
||
|
class IPCRunner extends events.EventEmitter {
|
||
|
|
||
|
constructor(win) {
|
||
|
super();
|
||
|
|
||
|
this.didFail = false;
|
||
|
this.didEnd = false;
|
||
|
|
||
|
ipcMain.on('start', () => this.emit('start'));
|
||
|
ipcMain.on('end', () => {
|
||
|
this.didEnd = true;
|
||
|
this.emit('end');
|
||
|
});
|
||
|
ipcMain.on('suite', (e, suite) => this.emit('suite', deserializeSuite(suite)));
|
||
|
ipcMain.on('suite end', (e, suite) => this.emit('suite end', deserializeSuite(suite)));
|
||
|
ipcMain.on('test', (e, test) => this.emit('test', deserializeRunnable(test)));
|
||
|
ipcMain.on('test end', (e, test) => this.emit('test end', deserializeRunnable(test)));
|
||
|
ipcMain.on('hook', (e, hook) => this.emit('hook', deserializeRunnable(hook)));
|
||
|
ipcMain.on('hook end', (e, hook) => this.emit('hook end', deserializeRunnable(hook)));
|
||
|
ipcMain.on('pass', (e, test) => this.emit('pass', deserializeRunnable(test)));
|
||
|
ipcMain.on('fail', (e, test, err) => {
|
||
|
this.didFail = true;
|
||
|
this.emit('fail', deserializeRunnable(test), deserializeError(err));
|
||
|
});
|
||
|
ipcMain.on('pending', (e, test) => this.emit('pending', deserializeRunnable(test)));
|
||
|
|
||
|
ipcMain.handle('startCoverage', async () => {
|
||
|
win.webContents.debugger.attach();
|
||
|
await win.webContents.debugger.sendCommand('Debugger.enable');
|
||
|
await win.webContents.debugger.sendCommand('Profiler.enable');
|
||
|
await win.webContents.debugger.sendCommand('Profiler.startPreciseCoverage', {
|
||
|
detailed: true,
|
||
|
allowTriggeredUpdates: false,
|
||
|
});
|
||
|
});
|
||
|
|
||
|
const coverageScriptsReported = new Set();
|
||
|
ipcMain.handle('snapshotCoverage', async (_, test) => {
|
||
|
const coverage = await win.webContents.debugger.sendCommand('Profiler.takePreciseCoverage');
|
||
|
await Promise.all(coverage.result.map(async (r) => {
|
||
|
if (!coverageScriptsReported.has(r.scriptId)) {
|
||
|
coverageScriptsReported.add(r.scriptId);
|
||
|
const src = await win.webContents.debugger.sendCommand('Debugger.getScriptSource', { scriptId: r.scriptId });
|
||
|
r.source = src.scriptSource;
|
||
|
}
|
||
|
}));
|
||
|
|
||
|
if (!test) {
|
||
|
this.emit('coverage init', coverage);
|
||
|
} else {
|
||
|
this.emit('coverage increment', test, coverage);
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
|
||
|
app.on('ready', () => {
|
||
|
|
||
|
// needed when loading resources from the renderer, e.g xterm.js or the encoding lib
|
||
|
session.defaultSession.protocol.registerFileProtocol('vscode-file', (request, callback) => {
|
||
|
const path = new URL(request.url).pathname;
|
||
|
callback({ path });
|
||
|
});
|
||
|
|
||
|
ipcMain.on('error', (_, err) => {
|
||
|
if (!args.dev) {
|
||
|
console.error(err);
|
||
|
app.exit(1);
|
||
|
}
|
||
|
});
|
||
|
|
||
|
// We need to provide a basic `ISandboxConfiguration`
|
||
|
// for our preload script to function properly because
|
||
|
// some of our types depend on it (e.g. product.ts).
|
||
|
ipcMain.handle('vscode:test-vscode-window-config', async () => {
|
||
|
return {
|
||
|
product: {
|
||
|
version: '1.x.y',
|
||
|
nameShort: 'Code - OSS Dev',
|
||
|
nameLong: 'Code - OSS Dev',
|
||
|
applicationName: 'code-oss',
|
||
|
dataFolderName: '.vscode-oss',
|
||
|
urlProtocol: 'code-oss',
|
||
|
}
|
||
|
};
|
||
|
});
|
||
|
|
||
|
// No-op since invoke the IPC as part of IIFE in the preload.
|
||
|
ipcMain.handle('vscode:fetchShellEnv', event => { });
|
||
|
|
||
|
const win = new BrowserWindow({
|
||
|
height: 600,
|
||
|
width: 800,
|
||
|
show: false,
|
||
|
webPreferences: {
|
||
|
preload: path.join(__dirname, 'preload.js'), // ensure similar environment as VSCode as tests may depend on this
|
||
|
additionalArguments: [`--vscode-window-config=vscode:test-vscode-window-config`],
|
||
|
nodeIntegration: true,
|
||
|
contextIsolation: false,
|
||
|
enableWebSQL: false,
|
||
|
spellcheck: false
|
||
|
}
|
||
|
});
|
||
|
|
||
|
win.webContents.on('did-finish-load', () => {
|
||
|
if (args.dev) {
|
||
|
win.show();
|
||
|
win.webContents.openDevTools();
|
||
|
}
|
||
|
|
||
|
if (args.waitServer) {
|
||
|
waitForServer(Number(args.waitServer)).then(sendRun);
|
||
|
} else {
|
||
|
sendRun();
|
||
|
}
|
||
|
});
|
||
|
|
||
|
async function waitForServer(port) {
|
||
|
let timeout;
|
||
|
let socket;
|
||
|
|
||
|
return new Promise(resolve => {
|
||
|
socket = net.connect(port, '127.0.0.1');
|
||
|
socket.on('error', e => {
|
||
|
console.error('error connecting to waitServer', e);
|
||
|
resolve(undefined);
|
||
|
});
|
||
|
|
||
|
socket.on('close', () => {
|
||
|
resolve(undefined);
|
||
|
});
|
||
|
|
||
|
timeout = setTimeout(() => {
|
||
|
console.error('timed out waiting for before starting tests debugger');
|
||
|
resolve(undefined);
|
||
|
}, 15000);
|
||
|
}).finally(() => {
|
||
|
if (socket) {
|
||
|
socket.end();
|
||
|
}
|
||
|
clearTimeout(timeout);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
function sendRun() {
|
||
|
win.webContents.send('run', args);
|
||
|
}
|
||
|
|
||
|
const target = url.pathToFileURL(path.join(__dirname, 'renderer.html'));
|
||
|
target.searchParams.set('argv', JSON.stringify(args));
|
||
|
win.loadURL(target.href);
|
||
|
|
||
|
const runner = new IPCRunner(win);
|
||
|
createStatsCollector(runner);
|
||
|
|
||
|
// Handle renderer crashes, #117068
|
||
|
win.webContents.on('render-process-gone', (evt, details) => {
|
||
|
if (!runner.didEnd) {
|
||
|
console.error(`Renderer process crashed with: ${JSON.stringify(details)}`);
|
||
|
app.exit(1);
|
||
|
}
|
||
|
});
|
||
|
|
||
|
const reporters = [];
|
||
|
|
||
|
if (args.tfs) {
|
||
|
reporters.push(
|
||
|
new mocha.reporters.Spec(runner),
|
||
|
new MochaJUnitReporter(runner, {
|
||
|
reporterOptions: {
|
||
|
testsuitesTitle: `${args.tfs} ${process.platform}`,
|
||
|
mochaFile: process.env.BUILD_ARTIFACTSTAGINGDIRECTORY ? path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${process.arch}-${args.tfs.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) : undefined
|
||
|
}
|
||
|
}),
|
||
|
);
|
||
|
} else {
|
||
|
// mocha patches symbols to use windows escape codes, but it seems like
|
||
|
// Electron mangles these in its output.
|
||
|
if (process.platform === 'win32') {
|
||
|
Object.assign(importMochaReporter('base').symbols, {
|
||
|
ok: '+',
|
||
|
err: 'X',
|
||
|
dot: '.',
|
||
|
});
|
||
|
}
|
||
|
|
||
|
reporters.push(applyReporter(runner, args));
|
||
|
}
|
||
|
|
||
|
if (!args.dev) {
|
||
|
ipcMain.on('all done', async () => {
|
||
|
await Promise.all(reporters.map(r => r.drain?.()));
|
||
|
app.exit(runner.didFail ? 1 : 0);
|
||
|
});
|
||
|
}
|
||
|
});
|