blob: ac43267ccdd2540382e3df8910fdaded80c74988 [file]
const fs = require('fs');
const path = require('path');
const bazelJasmine = require('@bazel/jasmine');
const runfiles = require(process.env['BAZEL_NODE_RUNFILES_HELPER']);
const JasmineRunner = bazelJasmine.jasmine;
const JUnitXmlReporter = bazelJasmine.JUnitXmlReporter;
let jasmineCore = null
if (global.jasmine) {
// global.jasmine has been initialized which means a bootstrap script
// has already required `jasmine-core` and called jasmineCore.boot()
jasmineCore = global.jasmineCore;
if (!jasmineCore) {
jasmineCore = bazelJasmine.jasmineCore;
}
// Override the jasmineCore boot function so that the jasmine
// runner gets the already initialize jasmine and its shared environment
jasmineCore.boot = function() {
return global.jasmine
};
}
const UTF8 = {
encoding: 'utf-8'
};
// These exit codes are handled specially by Bazel:
// https://github.com/bazelbuild/bazel/blob/486206012a664ecb20bdb196a681efc9a9825049/src/main/java/com/google/devtools/build/lib/util/ExitCode.java#L44
const BAZEL_EXIT_SUCCESS = 0;
const BAZEL_EXIT_TESTS_FAILED = 3;
const BAZEL_EXIT_NO_TESTS_FOUND = 4;
const BAZEL_EXIT_INTERRUPTED = 8;
// Test sharding support
// See https://docs.bazel.build/versions/main/test-encyclopedia.html#role-of-the-test-runner
const TOTAL_SHARDS = Number(process.env['TEST_TOTAL_SHARDS']);
const SHARD_INDEX = Number(process.env['TEST_SHARD_INDEX']);
// Tell Bazel that this test runner supports sharding by updating the last modified date of the
// magic file
if (TOTAL_SHARDS) {
fs.open(process.env['TEST_SHARD_STATUS_FILE'], 'w', (err, fd) => {
if (err) throw err;
fs.close(fd, err => {
if (err) throw err;
});
});
}
// Set the StackTraceLimit to infinity. This will make stack capturing slower, but more useful.
// Since we are running tests having proper stack traces is very useful and should be always set to
// the maximum (See: https://nodejs.org/api/errors.html#errors_error_stacktracelimit)
Error.stackTraceLimit = Infinity;
const IS_TEST_FILE = /[^a-zA-Z0-9](spec|test)\.js$/i;
const IS_NODE_MODULE = /\/node_modules\//
// We process arguments by splicing them out of the process.argv
// Users could set their own templated_args on their test, then
// the tested code might process the argv
// So it shouldn't see these Bazel-specific ones
function readArg() {
return process.argv.splice(2, 1)[0];
}
async function main(args) {
if (args.length < 2) {
throw new Error('expected argument missing');
}
// first args is always the path to the manifest
const manifest = runfiles.resolveWorkspaceRelative(readArg());
// config file is the next arg
const configFile = readArg();
const jrunner = new JasmineRunner({jasmineCore: jasmineCore});
if (configFile !== '--noconfig') {
jrunner.loadConfigFile(runfiles.resolveWorkspaceRelative(configFile));
}
fs.readFileSync(manifest, UTF8)
.split('\n')
.filter(l => l.length > 0)
// Filter out files from node_modules
.filter(f => !IS_NODE_MODULE.test(f))
// Use runfiles resolve to resolve the file path that
// bazel passes to the runner to its absolute path
.map(f => runfiles.resolveWorkspaceRelative(f))
// Filter here so that only files ending in `spec.js` and `test.js`
// are added to jasmine as spec files. This is important as other
// deps such as "@npm//typescript" if executed may cause the test to
// fail or have unexpected side-effects. "@npm//typescript" would
// try to execute tsc, print its help, and process.exit(1)
.filter(f => IS_TEST_FILE.test(f))
.forEach(f => jrunner.addSpecFile(f));
if (JUnitXmlReporter) {
const testOutputFile = process.env.XML_OUTPUT_FILE;
if (testOutputFile) {
jrunner.addReporter(new JUnitXmlReporter({
filePrefix: path.basename(testOutputFile),
savePath: path.dirname(testOutputFile),
consolidate: true,
consolidateAll: true
}));
// addReporter throws away the default console reporter
// so we need to add it back
jrunner.configureDefaultReporter({});
} else {
console.warn('Skipping XML Test Result: $XML_OUTPUT_FILE not found.')
}
}
if (TOTAL_SHARDS) {
// Since we want to collect all the loaded specs, we have to do this after
// loadSpecs() in jasmine/lib/jasmine.js
// However, we must add our filter before the runnable specs are calculated
// so that our filtering is applied.
// The jasmineStarted() callback is called by the "inner" execute function
// in jasmine-core, which is too late.
// Patch the inner execute function to do our filtering first.
const env = jasmine.getEnv();
const originalExecute = env.execute.bind(env);
env.execute = () => {
const allSpecs = getAllSpecs(env);
// Partition the specs among the shards.
// This ensures that the specs are evenly divided over the shards.
// Also it keeps specs in the same order and prefers to keep specs grouped together.
// This way, common beforeEach/beforeAll setup steps aren't repeated as much over different
// shards.
const start = allSpecs.length * SHARD_INDEX / TOTAL_SHARDS;
const end = allSpecs.length * (SHARD_INDEX + 1) / TOTAL_SHARDS;
const enabledSpecs = allSpecs.slice(start, end);
env.configure({specFilter: (s) => enabledSpecs.includes(s.id)});
return originalExecute();
};
// Special case!
// To allow us to test sharding, always run the specs in the order they are declared
if (process.env['TEST_WORKSPACE'] === 'build_bazel_rules_nodejs' &&
process.env['TEST_TARGET'].startsWith('//packages/jasmine/test:sharding_')) {
jrunner.randomizeTests(false);
}
}
// TODO(6.0): remove support for deprecated versions of Jasmine that use the old API &
// remember to update the `peerDependencies` as well.
// Jasmine versions prior to 3.10.0 should use the old API.
if (/^3\.[1-9]\.|^2\./.test(jrunner.coreVersion())) {
console.warn(`DEPRECATED: Support for Jasmine versions prior to '3.10.x' is deprecated in '@bazel/jasmine'.`);
// Old Jasmine API.
let noSpecsFound = true;
jrunner.addReporter({
specDone: () => {
noSpecsFound = false
},
});
jrunner.onComplete((passed) => {
let exitCode = passed ? BAZEL_EXIT_SUCCESS : BAZEL_EXIT_TESTS_FAILED;
if (noSpecsFound) exitCode = BAZEL_EXIT_NO_TESTS_FOUND;
process.exit(exitCode);
});
// addReporter throws away the default console reporter
// so we need to add it back
jrunner.configureDefaultReporter({});
await jrunner.execute();
return BAZEL_EXIT_SUCCESS;
}
// New Jasmine API.
jrunner.exitOnCompletion = false;
const { overallStatus, incompleteReason } = await jrunner.execute();
switch (overallStatus) {
case 'passed':
return BAZEL_EXIT_SUCCESS;
case 'incomplete':
return incompleteReason === 'No specs found' ? BAZEL_EXIT_NO_TESTS_FOUND : BAZEL_EXIT_INTERRUPTED;
case 'failed':
default:
return BAZEL_EXIT_TESTS_FAILED;
}
}
function getAllSpecs(jasmineEnv) {
const specs = [];
// Walk the test suite tree depth first and collect all test specs
const stack = [jasmineEnv.topSuite()];
let currentNode;
while (currentNode = stack.pop()) {
if (!currentNode) {
continue;
}
const { children, id } = currentNode;
if (Array.isArray(children)) {
// This is a suite.
stack.push(...children);
} else if (id) {
// This is a spec.
specs.unshift(currentNode);
}
}
return specs.map(s => s.id);
}
if (require.main === module) {
(async () => {
try {
process.exitCode = await main(process.argv.slice(2));
} catch (error) {
console.error('[jasmine_runner.js] An error has been reported:', error);
process.exitCode = 1;
}
})();
}