Merge pull request #26741 from Microsoft/gulpWatch
Fix overlapping test runs in 'gulp watch'
This commit is contained in:
commit
838110a4f8
6 changed files with 135 additions and 143 deletions
92
Gulpfile.js
92
Gulpfile.js
|
@ -24,10 +24,9 @@ const baselineAccept = require("./scripts/build/baselineAccept");
|
||||||
const cmdLineOptions = require("./scripts/build/options");
|
const cmdLineOptions = require("./scripts/build/options");
|
||||||
const exec = require("./scripts/build/exec");
|
const exec = require("./scripts/build/exec");
|
||||||
const browserify = require("./scripts/build/browserify");
|
const browserify = require("./scripts/build/browserify");
|
||||||
const debounce = require("./scripts/build/debounce");
|
|
||||||
const prepend = require("./scripts/build/prepend");
|
const prepend = require("./scripts/build/prepend");
|
||||||
const { removeSourceMaps } = require("./scripts/build/sourcemaps");
|
const { removeSourceMaps } = require("./scripts/build/sourcemaps");
|
||||||
const { CancelSource, CancelError } = require("./scripts/build/cancellation");
|
const { CancellationTokenSource, CancelError, delay, Semaphore } = require("prex");
|
||||||
const { libraryTargets, generateLibs } = require("./scripts/build/lib");
|
const { libraryTargets, generateLibs } = require("./scripts/build/lib");
|
||||||
const { runConsoleTests, cleanTestDirs, writeTestConfigFile, refBaseline, localBaseline, refRwcBaseline, localRwcBaseline } = require("./scripts/build/tests");
|
const { runConsoleTests, cleanTestDirs, writeTestConfigFile, refBaseline, localBaseline, refRwcBaseline, localRwcBaseline } = require("./scripts/build/tests");
|
||||||
|
|
||||||
|
@ -534,57 +533,80 @@ gulp.task(
|
||||||
["watch-diagnostics", "watch-lib"].concat(useCompilerDeps),
|
["watch-diagnostics", "watch-lib"].concat(useCompilerDeps),
|
||||||
() => project.watch(tsserverProject, { typescript: useCompiler }));
|
() => project.watch(tsserverProject, { typescript: useCompiler }));
|
||||||
|
|
||||||
gulp.task(
|
|
||||||
"watch-local",
|
|
||||||
/*help*/ false,
|
|
||||||
["watch-lib", "watch-tsc", "watch-services", "watch-server"]);
|
|
||||||
|
|
||||||
gulp.task(
|
gulp.task(
|
||||||
"watch-runner",
|
"watch-runner",
|
||||||
/*help*/ false,
|
/*help*/ false,
|
||||||
useCompilerDeps,
|
useCompilerDeps,
|
||||||
() => project.watch(testRunnerProject, { typescript: useCompiler }));
|
() => project.watch(testRunnerProject, { typescript: useCompiler }));
|
||||||
|
|
||||||
const watchPatterns = [
|
gulp.task(
|
||||||
runJs,
|
"watch-local",
|
||||||
typescriptDts,
|
"Watches for changes to projects in src/ (but does not execute tests).",
|
||||||
tsserverlibraryDts
|
["watch-lib", "watch-tsc", "watch-services", "watch-server", "watch-runner", "watch-lssl"]);
|
||||||
];
|
|
||||||
|
|
||||||
gulp.task(
|
gulp.task(
|
||||||
"watch",
|
"watch",
|
||||||
"Watches for changes to the build inputs for built/local/run.js, then executes runtests-parallel.",
|
"Watches for changes to the build inputs for built/local/run.js, then runs tests.",
|
||||||
["build-rules", "watch-runner", "watch-services", "watch-lssl"],
|
["build-rules", "watch-runner", "watch-services", "watch-lssl"],
|
||||||
() => {
|
() => {
|
||||||
/** @type {CancelSource | undefined} */
|
const sem = new Semaphore(1);
|
||||||
let runTestsSource;
|
|
||||||
|
|
||||||
const fn = debounce(() => {
|
gulp.watch([runJs, typescriptDts, tsserverlibraryDts], () => {
|
||||||
runTests().catch(error => {
|
runTests();
|
||||||
if (error instanceof CancelError) {
|
});
|
||||||
log.warn("Operation was canceled");
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
log.error(error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, /*timeout*/ 100, { max: 500 });
|
|
||||||
|
|
||||||
gulp.watch(watchPatterns, () => project.wait().then(fn));
|
|
||||||
|
|
||||||
// NOTE: gulp.watch is far too slow when watching tests/cases/**/* as it first enumerates *every* file
|
// NOTE: gulp.watch is far too slow when watching tests/cases/**/* as it first enumerates *every* file
|
||||||
const testFilePattern = /(\.ts|[\\/]tsconfig\.json)$/;
|
const testFilePattern = /(\.ts|[\\/]tsconfig\.json)$/;
|
||||||
fs.watch("tests/cases", { recursive: true }, (_, file) => {
|
fs.watch("tests/cases", { recursive: true }, (_, file) => {
|
||||||
if (testFilePattern.test(file)) project.wait().then(fn);
|
if (testFilePattern.test(file)) runTests();
|
||||||
});
|
});
|
||||||
|
|
||||||
function runTests() {
|
async function runTests() {
|
||||||
if (runTestsSource) runTestsSource.cancel();
|
try {
|
||||||
runTestsSource = new CancelSource();
|
// Ensure only one instance of the test runner is running at any given time.
|
||||||
return cmdLineOptions.tests || cmdLineOptions.failed
|
if (sem.count > 0) {
|
||||||
? runConsoleTests(runJs, "mocha-fivemat-progress-reporter", /*runInParallel*/ false, /*watchMode*/ true, runTestsSource.token)
|
await sem.wait();
|
||||||
: runConsoleTests(runJs, "min", /*runInParallel*/ true, /*watchMode*/ true, runTestsSource.token);
|
try {
|
||||||
}
|
// Wait for any concurrent recompilations to complete...
|
||||||
|
try {
|
||||||
|
await delay(100);
|
||||||
|
while (project.hasRemainingWork()) {
|
||||||
|
await project.waitForWorkToComplete();
|
||||||
|
await delay(500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
if (e instanceof CancelError) return;
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
|
// cancel any pending or active test run if a new recompilation is triggered
|
||||||
|
const source = new CancellationTokenSource();
|
||||||
|
project.waitForWorkToStart().then(() => {
|
||||||
|
source.cancel();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (cmdLineOptions.tests || cmdLineOptions.failed) {
|
||||||
|
await runConsoleTests(runJs, "mocha-fivemat-progress-reporter", /*runInParallel*/ false, /*watchMode*/ true, source.token);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
await runConsoleTests(runJs, "min", /*runInParallel*/ true, /*watchMode*/ true, source.token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
sem.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
if (e instanceof CancelError) {
|
||||||
|
log.warn("Operation was canceled");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
log.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
gulp.task("clean-built", /*help*/ false, [`clean:${diagnosticInformationMapTs}`], () => del(["built"]));
|
gulp.task("clean-built", /*help*/ false, [`clean:${diagnosticInformationMapTs}`], () => del(["built"]));
|
||||||
|
|
|
@ -81,6 +81,7 @@
|
||||||
"mocha": "latest",
|
"mocha": "latest",
|
||||||
"mocha-fivemat-progress-reporter": "latest",
|
"mocha-fivemat-progress-reporter": "latest",
|
||||||
"plugin-error": "latest",
|
"plugin-error": "latest",
|
||||||
|
"prex": "^0.4.3",
|
||||||
"q": "latest",
|
"q": "latest",
|
||||||
"remove-internal": "^2.9.2",
|
"remove-internal": "^2.9.2",
|
||||||
"run-sequence": "latest",
|
"run-sequence": "latest",
|
||||||
|
|
|
@ -1,71 +0,0 @@
|
||||||
// @ts-check
|
|
||||||
const symSource = Symbol("CancelToken.source");
|
|
||||||
const symToken = Symbol("CancelSource.token");
|
|
||||||
const symCancellationRequested = Symbol("CancelSource.cancellationRequested");
|
|
||||||
const symCancellationCallbacks = Symbol("CancelSource.cancellationCallbacks");
|
|
||||||
|
|
||||||
class CancelSource {
|
|
||||||
constructor() {
|
|
||||||
this[symCancellationRequested] = false;
|
|
||||||
this[symCancellationCallbacks] = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @type {CancelToken} */
|
|
||||||
get token() {
|
|
||||||
return this[symToken] || (this[symToken] = new CancelToken(this));
|
|
||||||
}
|
|
||||||
|
|
||||||
cancel() {
|
|
||||||
if (!this[symCancellationRequested]) {
|
|
||||||
this[symCancellationRequested] = true;
|
|
||||||
for (const callback of this[symCancellationCallbacks]) {
|
|
||||||
callback();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
exports.CancelSource = CancelSource;
|
|
||||||
|
|
||||||
class CancelToken {
|
|
||||||
/**
|
|
||||||
* @param {CancelSource} source
|
|
||||||
*/
|
|
||||||
constructor(source) {
|
|
||||||
if (source[symToken]) return source[symToken];
|
|
||||||
this[symSource] = source;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @type {boolean} */
|
|
||||||
get cancellationRequested() {
|
|
||||||
return this[symSource][symCancellationRequested];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {() => void} callback
|
|
||||||
*/
|
|
||||||
subscribe(callback) {
|
|
||||||
const source = this[symSource];
|
|
||||||
if (source[symCancellationRequested]) {
|
|
||||||
callback();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
source[symCancellationCallbacks].push(callback);
|
|
||||||
|
|
||||||
return {
|
|
||||||
unsubscribe() {
|
|
||||||
const index = source[symCancellationCallbacks].indexOf(callback);
|
|
||||||
if (index !== -1) source[symCancellationCallbacks].splice(index, 1);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
exports.CancelToken = CancelToken;
|
|
||||||
|
|
||||||
class CancelError extends Error {
|
|
||||||
constructor(message = "Operation was canceled") {
|
|
||||||
super(message);
|
|
||||||
this.name = "CancelError";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
exports.CancelError = CancelError;
|
|
|
@ -3,7 +3,7 @@ const cp = require("child_process");
|
||||||
const log = require("fancy-log"); // was `require("gulp-util").log (see https://github.com/gulpjs/gulp-util)
|
const log = require("fancy-log"); // was `require("gulp-util").log (see https://github.com/gulpjs/gulp-util)
|
||||||
const isWin = /^win/.test(process.platform);
|
const isWin = /^win/.test(process.platform);
|
||||||
const chalk = require("./chalk");
|
const chalk = require("./chalk");
|
||||||
const { CancelToken, CancelError } = require("./cancellation");
|
const { CancellationToken, CancelError } = require("prex");
|
||||||
|
|
||||||
module.exports = exec;
|
module.exports = exec;
|
||||||
|
|
||||||
|
@ -15,31 +15,36 @@ module.exports = exec;
|
||||||
*
|
*
|
||||||
* @typedef ExecOptions
|
* @typedef ExecOptions
|
||||||
* @property {boolean} [ignoreExitCode]
|
* @property {boolean} [ignoreExitCode]
|
||||||
* @property {CancelToken} [cancelToken]
|
* @property {import("prex").CancellationToken} [cancelToken]
|
||||||
*/
|
*/
|
||||||
function exec(cmd, args, options = {}) {
|
function exec(cmd, args, options = {}) {
|
||||||
return /**@type {Promise<{exitCode: number}>}*/(new Promise((resolve, reject) => {
|
return /**@type {Promise<{exitCode: number}>}*/(new Promise((resolve, reject) => {
|
||||||
log(`> ${chalk.green(cmd)} ${args.join(" ")}`);
|
const { ignoreExitCode, cancelToken = CancellationToken.none } = options;
|
||||||
|
cancelToken.throwIfCancellationRequested();
|
||||||
|
|
||||||
// TODO (weswig): Update child_process types to add windowsVerbatimArguments to the type definition
|
// TODO (weswig): Update child_process types to add windowsVerbatimArguments to the type definition
|
||||||
const subshellFlag = isWin ? "/c" : "-c";
|
const subshellFlag = isWin ? "/c" : "-c";
|
||||||
const command = isWin ? [possiblyQuote(cmd), ...args] : [`${cmd} ${args.join(" ")}`];
|
const command = isWin ? [possiblyQuote(cmd), ...args] : [`${cmd} ${args.join(" ")}`];
|
||||||
const ex = cp.spawn(isWin ? "cmd" : "/bin/sh", [subshellFlag, ...command], { stdio: "inherit", windowsVerbatimArguments: true });
|
|
||||||
const subscription = options.cancelToken && options.cancelToken.subscribe(() => {
|
log(`> ${chalk.green(cmd)} ${args.join(" ")}`);
|
||||||
ex.kill("SIGINT");
|
const proc = cp.spawn(isWin ? "cmd" : "/bin/sh", [subshellFlag, ...command], { stdio: "inherit", windowsVerbatimArguments: true });
|
||||||
ex.kill("SIGTERM");
|
const registration = cancelToken.register(() => {
|
||||||
|
log(`${chalk.red("killing")} '${chalk.green(cmd)} ${args.join(" ")}'...`);
|
||||||
|
proc.kill("SIGINT");
|
||||||
|
proc.kill("SIGTERM");
|
||||||
reject(new CancelError());
|
reject(new CancelError());
|
||||||
});
|
});
|
||||||
ex.on("exit", exitCode => {
|
proc.on("exit", exitCode => {
|
||||||
subscription && subscription.unsubscribe();
|
registration.unregister();
|
||||||
if (exitCode === 0 || options.ignoreExitCode) {
|
if (exitCode === 0 || ignoreExitCode) {
|
||||||
resolve({ exitCode });
|
resolve({ exitCode });
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
reject(new Error(`Process exited with code: ${exitCode}`));
|
reject(new Error(`Process exited with code: ${exitCode}`));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
ex.on("error", error => {
|
proc.on("error", error => {
|
||||||
subscription && subscription.unsubscribe();
|
registration.unregister();
|
||||||
reject(error);
|
reject(error);
|
||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
|
|
|
@ -3,6 +3,8 @@ const path = require("path");
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const gulp = require("./gulp");
|
const gulp = require("./gulp");
|
||||||
const gulpif = require("gulp-if");
|
const gulpif = require("gulp-if");
|
||||||
|
const log = require("fancy-log"); // was `require("gulp-util").log (see https://github.com/gulpjs/gulp-util)
|
||||||
|
const chalk = require("./chalk");
|
||||||
const sourcemaps = require("gulp-sourcemaps");
|
const sourcemaps = require("gulp-sourcemaps");
|
||||||
const merge2 = require("merge2");
|
const merge2 = require("merge2");
|
||||||
const tsc = require("gulp-typescript");
|
const tsc = require("gulp-typescript");
|
||||||
|
@ -12,7 +14,12 @@ const ts = require("../../lib/typescript");
|
||||||
const del = require("del");
|
const del = require("del");
|
||||||
const needsUpdate = require("./needsUpdate");
|
const needsUpdate = require("./needsUpdate");
|
||||||
const mkdirp = require("./mkdirp");
|
const mkdirp = require("./mkdirp");
|
||||||
|
const prettyTime = require("pretty-hrtime");
|
||||||
const { reportDiagnostics } = require("./diagnostics");
|
const { reportDiagnostics } = require("./diagnostics");
|
||||||
|
const { CountdownEvent, ManualResetEvent } = require("prex");
|
||||||
|
|
||||||
|
const workStartedEvent = new ManualResetEvent();
|
||||||
|
const countdown = new CountdownEvent(0);
|
||||||
|
|
||||||
class CompilationGulp extends gulp.Gulp {
|
class CompilationGulp extends gulp.Gulp {
|
||||||
/**
|
/**
|
||||||
|
@ -20,15 +27,39 @@ class CompilationGulp extends gulp.Gulp {
|
||||||
*/
|
*/
|
||||||
fork(verbose) {
|
fork(verbose) {
|
||||||
const child = new ForkedGulp(this.tasks);
|
const child = new ForkedGulp(this.tasks);
|
||||||
if (verbose) {
|
child.on("task_start", e => {
|
||||||
child.on("task_start", e => gulp.emit("task_start", e));
|
if (countdown.remainingCount === 0) {
|
||||||
child.on("task_stop", e => gulp.emit("task_stop", e));
|
countdown.reset(1);
|
||||||
child.on("task_err", e => gulp.emit("task_err", e));
|
workStartedEvent.set();
|
||||||
child.on("task_not_found", e => gulp.emit("task_not_found", e));
|
workStartedEvent.reset();
|
||||||
child.on("task_recursion", e => gulp.emit("task_recursion", e));
|
}
|
||||||
}
|
else {
|
||||||
|
countdown.add();
|
||||||
|
}
|
||||||
|
if (verbose) {
|
||||||
|
log('Starting', `'${chalk.cyan(e.task)}' ${chalk.gray(`(${countdown.remainingCount} remaining)`)}...`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
child.on("task_stop", e => {
|
||||||
|
countdown.signal();
|
||||||
|
if (verbose) {
|
||||||
|
log('Finished', `'${chalk.cyan(e.task)}' after ${chalk.magenta(prettyTime(/** @type {*}*/(e).hrDuration))} ${chalk.gray(`(${countdown.remainingCount} remaining)`)}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
child.on("task_err", e => {
|
||||||
|
countdown.signal();
|
||||||
|
if (verbose) {
|
||||||
|
log(`'${chalk.cyan(e.task)}' ${chalk.red("errored after")} ${chalk.magenta(prettyTime(/** @type {*}*/(e).hrDuration))} ${chalk.gray(`(${countdown.remainingCount} remaining)`)}`);
|
||||||
|
log(e.err ? e.err.stack : e.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
return child;
|
return child;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
start() {
|
||||||
|
throw new Error("Not supported, use fork.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ForkedGulp extends gulp.Gulp {
|
class ForkedGulp extends gulp.Gulp {
|
||||||
|
@ -211,24 +242,26 @@ exports.flatten = flatten;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a Promise that resolves when all pending build tasks have completed
|
* Returns a Promise that resolves when all pending build tasks have completed
|
||||||
|
* @param {import("prex").CancellationToken} [token]
|
||||||
*/
|
*/
|
||||||
function wait() {
|
function waitForWorkToComplete(token) {
|
||||||
return new Promise(resolve => {
|
return countdown.wait(token);
|
||||||
if (compilationGulp.allDone()) {
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
const onDone = () => {
|
|
||||||
compilationGulp.removeListener("onDone", onDone);
|
|
||||||
compilationGulp.removeListener("err", onDone);
|
|
||||||
resolve();
|
|
||||||
};
|
|
||||||
compilationGulp.on("stop", onDone);
|
|
||||||
compilationGulp.on("err", onDone);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
exports.wait = wait;
|
exports.waitForWorkToComplete = waitForWorkToComplete;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a Promise that resolves when all pending build tasks have completed
|
||||||
|
* @param {import("prex").CancellationToken} [token]
|
||||||
|
*/
|
||||||
|
function waitForWorkToStart(token) {
|
||||||
|
return workStartedEvent.wait(token);
|
||||||
|
}
|
||||||
|
exports.waitForWorkToStart = waitForWorkToStart;
|
||||||
|
|
||||||
|
function getRemainingWork() {
|
||||||
|
return countdown.remainingCount > 0;
|
||||||
|
}
|
||||||
|
exports.hasRemainingWork = getRemainingWork;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve a TypeScript specifier into a fully-qualified module specifier and any requisite dependencies.
|
* Resolve a TypeScript specifier into a fully-qualified module specifier and any requisite dependencies.
|
||||||
|
|
|
@ -8,6 +8,7 @@ const mkdirP = require("./mkdirp");
|
||||||
const cmdLineOptions = require("./options");
|
const cmdLineOptions = require("./options");
|
||||||
const exec = require("./exec");
|
const exec = require("./exec");
|
||||||
const log = require("fancy-log"); // was `require("gulp-util").log (see https://github.com/gulpjs/gulp-util)
|
const log = require("fancy-log"); // was `require("gulp-util").log (see https://github.com/gulpjs/gulp-util)
|
||||||
|
const { CancellationToken } = require("prex");
|
||||||
const mochaJs = require.resolve("mocha/bin/_mocha");
|
const mochaJs = require.resolve("mocha/bin/_mocha");
|
||||||
|
|
||||||
exports.localBaseline = "tests/baselines/local/";
|
exports.localBaseline = "tests/baselines/local/";
|
||||||
|
@ -21,9 +22,9 @@ exports.localTest262Baseline = "internal/baselines/test262/local";
|
||||||
* @param {string} defaultReporter
|
* @param {string} defaultReporter
|
||||||
* @param {boolean} runInParallel
|
* @param {boolean} runInParallel
|
||||||
* @param {boolean} watchMode
|
* @param {boolean} watchMode
|
||||||
* @param {InstanceType<typeof import("./cancellation").CancelToken>} [cancelToken]
|
* @param {import("prex").CancellationToken} [cancelToken]
|
||||||
*/
|
*/
|
||||||
async function runConsoleTests(runJs, defaultReporter, runInParallel, watchMode, cancelToken) {
|
async function runConsoleTests(runJs, defaultReporter, runInParallel, watchMode, cancelToken = CancellationToken.none) {
|
||||||
let testTimeout = cmdLineOptions.timeout;
|
let testTimeout = cmdLineOptions.timeout;
|
||||||
let tests = cmdLineOptions.tests;
|
let tests = cmdLineOptions.tests;
|
||||||
const lintFlag = cmdLineOptions.lint;
|
const lintFlag = cmdLineOptions.lint;
|
||||||
|
@ -37,6 +38,7 @@ async function runConsoleTests(runJs, defaultReporter, runInParallel, watchMode,
|
||||||
const keepFailed = cmdLineOptions.keepFailed;
|
const keepFailed = cmdLineOptions.keepFailed;
|
||||||
if (!cmdLineOptions.dirty) {
|
if (!cmdLineOptions.dirty) {
|
||||||
await cleanTestDirs();
|
await cleanTestDirs();
|
||||||
|
cancelToken.throwIfCancellationRequested();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fs.existsSync(testConfigFile)) {
|
if (fs.existsSync(testConfigFile)) {
|
||||||
|
|
Loading…
Reference in a new issue