angular-cli/tests/legacy-cli/e2e_runner.ts
Charles Lyding b3d7080147 build: enable esModuleInterop TypeScript option
The `esModuleInterop` option is recommended to be enable by TypeScript and corrects several assumptions TypeScript would otherwise make when importing CommonJS files.
This option change helps ensure compatibility as packages move towards ESM.
Reference: https://www.typescriptlang.org/tsconfig#esModuleInterop
2021-06-13 11:45:55 -04:00

266 lines
9.3 KiB
TypeScript

// This may seem awkward but we're using Logger in our e2e. At this point the unit tests
// have run already so it should be "safe", teehee.
import { logging } from '@angular-devkit/core';
import { createConsoleLogger } from '@angular-devkit/core/node';
import * as colors from 'ansi-colors';
import { spawn } from 'child_process';
import * as fs from 'fs';
import glob from 'glob';
import minimist from 'minimist';
import * as os from 'os';
import * as path from 'path';
import { setGlobalVariable } from './e2e/utils/env';
import { gitClean } from './e2e/utils/git';
Error.stackTraceLimit = Infinity;
// tslint:disable:no-global-tslint-disable no-console
/**
* Here's a short description of those flags:
* --debug If a test fails, block the thread so the temporary directory isn't deleted.
* --noproject Skip creating a project or using one.
* --nobuild Skip building the packages. Use with --noglobal and --reuse to quickly
* rerun tests.
* --noglobal Skip linking your local @angular/cli directory. Can save a few seconds.
* --nosilent Never silence ng commands.
* --ng-tag=TAG Use a specific tag for build snapshots. Similar to ng-snapshots but point to a
* tag instead of using the latest master.
* --ng-snapshots Install angular snapshot builds in the test project.
* --ve Use the View Engine compiler.
* --glob Run tests matching this glob pattern (relative to tests/e2e/).
* --ignore Ignore tests matching this glob pattern.
* --reuse=/path Use a path instead of create a new project. That project should have been
* created, and npm installed. Ideally you want a project created by a previous
* run of e2e.
* --nb-shards Total number of shards that this is part of. Default is 2 if --shard is
* passed in.
* --shard Index of this processes' shard.
* --devkit=path Path to the devkit to use. The devkit will be built prior to running.
* --tmpdir=path Override temporary directory to use for new projects.
* If unnamed flags are passed in, the list of tests will be filtered to include only those passed.
*/
const argv = minimist(process.argv.slice(2), {
boolean: ['debug', 'ng-snapshots', 'noglobal', 'nosilent', 'noproject', 'verbose'],
string: ['devkit', 'glob', 'ignore', 'reuse', 'ng-tag', 'tmpdir', 'ng-version'],
});
/**
* Set the error code of the process to 255. This is to ensure that if something forces node
* to exit without finishing properly, the error code will be 255. Right now that code is not used.
*
* - 1 When tests succeed we already call `process.exit(0)`, so this doesn't change any correct
* behaviour.
*
* One such case that would force node <= v6 to exit with code 0, is a Promise that doesn't resolve.
*/
process.exitCode = 255;
const logger = createConsoleLogger(argv.verbose, process.stdout, process.stderr, {
info: (s) => s,
debug: (s) => s,
warn: (s) => colors.bold.yellow(s),
error: (s) => colors.bold.red(s),
fatal: (s) => colors.bold.red(s),
});
const logStack = [logger];
function lastLogger() {
return logStack[logStack.length - 1];
}
const testGlob = argv.glob || 'tests/**/*.ts';
let currentFileName = null;
const e2eRoot = path.join(__dirname, 'e2e');
const allSetups = glob
.sync(path.join(e2eRoot, 'setup/**/*.ts'), { nodir: true })
.map((name) => path.relative(e2eRoot, name))
.sort();
const allTests = glob
.sync(path.join(e2eRoot, testGlob), { nodir: true, ignore: argv.ignore })
.map((name) => path.relative(e2eRoot, name))
// Replace windows slashes.
.map((name) => name.replace(/\\/g, '/'))
.sort()
.filter((name) => !name.endsWith('/setup.ts'));
const shardId = 'shard' in argv ? argv['shard'] : null;
const nbShards = (shardId === null ? 1 : argv['nb-shards']) || 2;
const tests = allTests.filter((name) => {
// Check for naming tests on command line.
if (argv._.length == 0) {
return true;
}
return argv._.some((argName) => {
return (
path.join(process.cwd(), argName) == path.join(__dirname, 'e2e', name) ||
argName == name ||
argName == name.replace(/\.ts$/, '')
);
});
});
// Remove tests that are not part of this shard.
const shardedTests = tests.filter((name, i) => shardId === null || i % nbShards == shardId);
const testsToRun = allSetups.concat(shardedTests);
if (shardedTests.length === 0) {
console.log(`No tests would be ran, aborting.`);
process.exit(1);
}
console.log(testsToRun.join('\n'));
/**
* Load all the files from the e2e, filter and sort them and build a promise of their default
* export.
*/
if (testsToRun.length == allTests.length) {
console.log(`Running ${testsToRun.length} tests`);
} else {
console.log(`Running ${testsToRun.length} tests (${allTests.length + allSetups.length} total)`);
}
setGlobalVariable('argv', argv);
setGlobalVariable('ci', process.env['CI']?.toLowerCase() === 'true' || process.env['CI'] === '1');
setGlobalVariable('package-manager', argv.yarn ? 'yarn' : 'npm');
setGlobalVariable('package-registry', 'http://localhost:4873');
// Setup local package registry
const registryPath = fs.mkdtempSync(
path.join(fs.realpathSync(os.tmpdir()), 'angular-cli-e2e-registry-'),
);
fs.copyFileSync(path.join(__dirname, 'verdaccio.yaml'), path.join(registryPath, 'verdaccio.yaml'));
const registryProcess = spawn(
'node',
[require.resolve('verdaccio/bin/verdaccio'), '-c', './verdaccio.yaml'],
{ cwd: registryPath, stdio: 'inherit' },
);
testsToRun
.reduce((previous, relativeName, testIndex) => {
// Make sure this is a windows compatible path.
let absoluteName = path.join(e2eRoot, relativeName);
if (/^win/.test(process.platform)) {
absoluteName = absoluteName.replace(/\\/g, path.posix.sep);
}
return previous.then(() => {
currentFileName = relativeName.replace(/\.ts$/, '');
const start = +new Date();
const module = require(absoluteName);
const fn: (skipClean?: () => void) => Promise<void> | void =
typeof module == 'function'
? module
: typeof module.default == 'function'
? module.default
: () => {
throw new Error('Invalid test module.');
};
let clean = true;
let previousDir = null;
return Promise.resolve()
.then(() => printHeader(currentFileName, testIndex))
.then(() => (previousDir = process.cwd()))
.then(() => logStack.push(lastLogger().createChild(currentFileName)))
.then(() => fn(() => (clean = false)))
.then(
() => logStack.pop(),
(err) => {
logStack.pop();
throw err;
},
)
.then(() => console.log('----'))
.then(() => {
// If we're not in a setup, change the directory back to where it was before the test.
// This allows tests to chdir without worrying about keeping the original directory.
if (allSetups.indexOf(relativeName) == -1 && previousDir) {
process.chdir(previousDir);
}
})
.then(() => {
// Only clean after a real test, not a setup step. Also skip cleaning if the test
// requested an exception.
if (allSetups.indexOf(relativeName) == -1 && clean) {
logStack.push(new logging.NullLogger());
return gitClean().then(
() => logStack.pop(),
(err) => {
logStack.pop();
throw err;
},
);
}
})
.then(
() => printFooter(currentFileName, start),
(err) => {
printFooter(currentFileName, start);
console.error(err);
throw err;
},
);
});
}, Promise.resolve())
.then(
() => {
if (registryProcess) {
registryProcess.kill();
}
console.log(colors.green('Done.'));
process.exit(0);
},
(err) => {
console.log('\n');
console.error(colors.red(`Test "${currentFileName}" failed...`));
console.error(colors.red(err.message));
console.error(colors.red(err.stack));
if (registryProcess) {
registryProcess.kill();
}
if (argv.debug) {
console.log(`Current Directory: ${process.cwd()}`);
console.log('Will loop forever while you debug... CTRL-C to quit.');
/* eslint-disable no-constant-condition */
while (1) {
// That's right!
}
}
process.exit(1);
},
);
function printHeader(testName: string, testIndex: number) {
const text = `${testIndex + 1} of ${testsToRun.length}`;
const fullIndex =
(testIndex < allSetups.length
? testIndex
: (testIndex - allSetups.length) * nbShards + shardId + allSetups.length) + 1;
const length = tests.length + allSetups.length;
const shard =
shardId === null
? ''
: colors.yellow(` [${shardId}:${nbShards}]` + colors.bold(` (${fullIndex}/${length})`));
console.log(
colors.green(`Running "${colors.bold.blue(testName)}" (${colors.bold.white(text)}${shard})...`),
);
}
function printFooter(testName: string, startTime: number) {
// Round to hundredth of a second.
const t = Math.round((Date.now() - startTime) / 10) / 100;
console.log(colors.green('Last step took ') + colors.bold.blue('' + t) + colors.green('s...'));
console.log('');
}