@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;