@itwin/build-tools
Version:
Bentley build tools
149 lines (148 loc) • 7.24 kB
JavaScript
;
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/* eslint-disable @typescript-eslint/naming-convention */
/* eslint-disable @typescript-eslint/no-require-imports */
/* eslint-disable no-console */
const debugLeaks = process.env.DEBUG_LEAKS;
let asyncResourceStats;
if (debugLeaks) {
require("wtfnode");
asyncResourceStats = new Map();
setupAsyncHooks();
}
function setupAsyncHooks() {
const async_hooks = require("node:async_hooks");
const init = (asyncId, type, triggerAsyncId, _resource) => {
const eid = async_hooks.executionAsyncId(); // (An executionAsyncId() of 0 means that it is being executed from C++ with no JavaScript stack above it.)
const stack = new Error().stack;
asyncResourceStats.set(asyncId, { type, eid, triggerAsyncId, initStack: stack });
};
const destroy = (asyncId) => {
if (asyncResourceStats.get(asyncId) === undefined) {
return;
}
asyncResourceStats.delete(asyncId);
};
const asyncHook = async_hooks.createHook({ init, destroy });
asyncHook.enable();
}
const fs = require("fs-extra");
const path = require("path");
const { logBuildWarning, logBuildError, failBuild } = require("../scripts/utils/utils");
const Base = require("mocha/lib/reporters/base");
const Spec = require("mocha/lib/reporters/spec");
const MochaJUnitReporter = require("mocha-junit-reporter");
function withStdErr(callback) {
const originalConsoleLog = Base.consoleLog;
Base.consoleLog = console.error;
callback();
Base.consoleLog = originalConsoleLog;
}
const isCI = process.env.CI || process.env.TF_BUILD;
// Force rush test to fail CI builds if describe.only or it.only is used.
// These should only be used for debugging and must not be committed, otherwise we may be accidentally skipping lots of tests.
if (isCI) {
if (typeof (mocha) !== "undefined")
mocha.forbidOnly();
else
require.cache[require.resolve("mocha/lib/mocharc.json", { paths: require.main?.paths ?? module.paths })].exports.forbidOnly = true;
}
// This is necessary to enable colored output when running in rush test:
Object.defineProperty(Base, "color", {
get: () => process.env.FORCE_COLOR !== "false" && process.env.FORCE_COLOR !== "0",
set: () => { },
});
class BentleyMochaReporter extends Spec {
_junitReporter;
_chrome = false;
_electron = true;
constructor(_runner, _options) {
super(...arguments);
this._junitReporter = new MochaJUnitReporter(...arguments);
// Detect hangs caused by tests that leave timers/other handles open - not possible in electron frontends.
if (!("electron" in process.versions)) {
this._electron = false;
if (process.argv.length > 1 && process.argv[1].endsWith("certa.js")) {
for (let i = 2; i < process.argv.length; ++i) {
if (process.argv[i] === "-r") {
if (i + 1 < process.argv.length && process.argv[i + 1] === "chrome") {
this._chrome = true;
process.on("chrome-test-runner-done", () => {
this.confirmExit();
});
}
break;
}
}
}
}
}
confirmExit(seconds = 30) {
// NB: By calling unref() on this timer, we stop it from keeping the process alive, so it will only fire if _something else_ is still keeping
// the process alive after n seconds. This also has the benefit of preventing the timer from showing up in wtfnode's dump of open handles.
setTimeout(() => {
logBuildError(`Handle leak detected. Node was still running ${seconds} seconds after tests completed.`);
if (debugLeaks) {
const wtf = require("wtfnode");
wtf.setLogger("info", console.error);
wtf.dump();
let activeResourcesInfo = process.getActiveResourcesInfo(); // https://nodejs.dev/en/api/v18/process#processgetactiveresourcesinfo (Not added to @types/node yet I suppose)
console.error(activeResourcesInfo);
activeResourcesInfo = activeResourcesInfo.map((value) => value.toLowerCase());
// asyncResourceStats.set(asyncId, {before: 0, after: 0, type, eid, triggerAsyncId, initStack: stack});
asyncResourceStats.forEach((value, key) => {
if (activeResourcesInfo.includes(value.type.toLowerCase())) {
console.error(`asyncId: ${key}: type: ${value.type}, eid: ${value.eid},triggerAsyncId: ${value.triggerAsyncId}, initStack: ${value.initStack}`);
}
});
}
else {
console.error("Try running with the DEBUG_LEAKS env var set to see open handles.");
}
// Not sure why, but process.exit(1) wasn't working here...
process.kill(process.pid);
}, seconds * 1000).unref();
}
epilogue(...args) {
// Force test errors to be printed to stderr instead of stdout.
// This will allow rush to correctly summarize test failure when running rush test.
if (this.stats.failures) {
withStdErr(() => super.epilogue(...args));
}
else {
super.epilogue(...args);
if (0 === this.stats.passes) {
logBuildError("There were 0 passing tests. That doesn't seem right."
+ "\nIf there are really no passing tests and no failures, then what was even the point?"
+ "\nIt seems likely that tests were skipped by it.only, it.skip, or grep filters, so I'm going to fail now.");
failBuild();
}
}
// Detect hangs caused by tests that leave timers/other handles open - not possible in electron frontends.
if (!this._electron && !this._chrome) {
// Not running in Chrome or Electron, so check for open handles.
this.confirmExit(30);
}
if (!this.stats.pending)
return;
// Also log warnings in CI builds when tests have been skipped.
const currentPkgJson = path.join(process.cwd(), "package.json");
if (fs.existsSync(currentPkgJson)) {
const currentPackage = require(currentPkgJson).name;
if (this.stats.pending === 1)
logBuildWarning(`1 test skipped in ${currentPackage}`);
else
logBuildWarning(`${this.stats.pending} tests skipped in ${currentPackage}`);
}
else {
if (this.stats.pending === 1)
logBuildWarning(`1 test skipped`);
else
logBuildWarning(`${this.stats.pending} tests skipped`);
}
}
}
module.exports = BentleyMochaReporter;