UNPKG

@itwin/build-tools

Version:
149 lines (148 loc) • 7.24 kB
"use strict"; /*--------------------------------------------------------------------------------------------- * 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;