2018-07-06 09:09:52 -04:00

224 lines
7.1 KiB
TypeScript

/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
// tslint:disable:no-implicit-dependencies
import { logging } from '@angular-devkit/core';
import * as glob from 'glob';
import * as Istanbul from 'istanbul';
import 'jasmine';
import { SpecReporter as JasmineSpecReporter } from 'jasmine-spec-reporter';
import { ParsedArgs } from 'minimist';
import { join, relative } from 'path';
import { Position, SourceMapConsumer } from 'source-map';
import * as ts from 'typescript';
import { packages } from '../lib/packages';
const codeMap = require('../lib/istanbul-local').codeMap;
const Jasmine = require('jasmine');
const projectBaseDir = join(__dirname, '..');
require('source-map-support').install({
hookRequire: true,
});
interface CoverageLocation {
start: Position;
end: Position;
}
type CoverageType = any; // tslint:disable-line:no-any
declare const global: {
__coverage__: CoverageType;
};
// Add the Istanbul (not Constantinople) reporter.
const istanbulCollector = new Istanbul.Collector({});
const istanbulReporter = new Istanbul.Reporter(undefined, 'coverage/');
istanbulReporter.addAll(['json', 'lcov']);
class IstanbulReporter implements jasmine.CustomReporter {
// Update a location object from a SourceMap. Will ignore the location if the sourcemap does
// not have a valid mapping.
private _updateLocation(consumer: SourceMapConsumer, location: CoverageLocation) {
const start = consumer.originalPositionFor(location.start);
const end = consumer.originalPositionFor(location.end);
// Filter invalid original positions.
if (start.line !== null && start.column !== null) {
// Filter unwanted properties.
location.start = { line: start.line, column: start.column };
}
if (end.line !== null && end.column !== null) {
location.end = { line: end.line, column: end.column };
}
}
private _updateCoverageJsonSourceMap(coverageJson: CoverageType) {
// Update the coverageJson with the SourceMap.
for (const path of Object.keys(coverageJson)) {
const entry = codeMap.get(path);
if (!entry) {
continue;
}
const consumer = entry.map;
const coverage = coverageJson[path];
// Update statement maps.
for (const branchId of Object.keys(coverage.branchMap)) {
const branch = coverage.branchMap[branchId];
let line: number | null = null;
let column = 0;
do {
line = consumer.originalPositionFor({ line: branch.line, column: column++ }).line;
} while (line === null && column < 100);
branch.line = line;
for (const location of branch.locations) {
this._updateLocation(consumer, location);
}
}
for (const id of Object.keys(coverage.statementMap)) {
const location = coverage.statementMap[id];
this._updateLocation(consumer, location);
}
for (const id of Object.keys(coverage.fnMap)) {
const fn = coverage.fnMap[id];
fn.line = consumer.originalPositionFor({ line: fn.line, column: 0 }).line;
this._updateLocation(consumer, fn.loc);
}
}
}
jasmineDone(_runDetails: jasmine.RunDetails): void {
if (global.__coverage__) {
this._updateCoverageJsonSourceMap(global.__coverage__);
istanbulCollector.add(global.__coverage__);
istanbulReporter.write(istanbulCollector, true, () => {});
}
}
}
// Create a Jasmine runner and configure it.
const runner = new Jasmine({ projectBaseDir: projectBaseDir });
if (process.argv.indexOf('--spec-reporter') != -1) {
runner.env.clearReporters();
runner.env.addReporter(new JasmineSpecReporter({
stacktrace: {
// Filter all JavaScript files that appear after a TypeScript file (callers) from the stack
// trace.
filter: (x: string) => {
return x.substr(0, x.indexOf('\n', x.indexOf('\n', x.lastIndexOf('.ts:')) + 1));
},
},
spec: {
displayDuration: true,
},
suite: {
displayNumber: true,
},
summary: {
displayStacktrace: true,
displayErrorMessages: true,
displayDuration: true,
},
}));
}
// Manually set exit code (needed with custom reporters)
runner.onComplete((success: boolean) => {
process.exitCode = success ? 0 : 1;
if (process.platform.startsWith('win')) {
// TODO(filipesilva): finish figuring out why this happens.
// We should not need to force exit here, but when:
// - on windows
// - running webpack-dev-server
// - with ngtools/webpack on the compilation
// Something seems to hang and the process never exists.
// This does not happen on linux, nor with webpack on watch mode.
// Until this is figured out, we need to exit the process manually after tests finish
// otherwise appveyor will hang until it timeouts.
process.exit();
}
});
glob.sync('packages/**/*.spec.ts')
.filter(p => !/\/schematics\/.*\/(other-)?files\//.test(p))
.forEach(path => {
console.error(`Invalid spec file name: ${path}. You're using the old convention.`);
});
export default function (args: ParsedArgs, logger: logging.Logger) {
const specGlob = args.large ? '*_spec_large.ts' : '*_spec.ts';
const regex = args.glob ? args.glob : `packages/**/${specGlob}`;
if (args['code-coverage']) {
runner.env.addReporter(new IstanbulReporter());
}
if (args.large) {
// Default timeout for large specs is 2.5 minutes.
jasmine.DEFAULT_TIMEOUT_INTERVAL = 150000;
}
// Run the tests.
const allTests =
glob.sync(regex)
.map(p => relative(projectBaseDir, p));
const tsConfigPath = join(__dirname, '../tsconfig.json');
const tsConfig = ts.readConfigFile(tsConfigPath, ts.sys.readFile);
const pattern = '^('
+ (tsConfig.config.exclude as string[])
.map(ex => '('
+ ex.split(/[\/\\]/g).map(f => f
.replace(/[\-\[\]{}()+?.^$|]/g, '\\$&')
.replace(/^\*\*/g, '(.+?)?')
.replace(/\*/g, '[^/\\\\]*'))
.join('[\/\\\\]')
+ ')')
.join('|')
+ ')($|/|\\\\)';
const excludeRe = new RegExp(pattern);
let tests = allTests.filter(x => !excludeRe.test(x));
if (!args.full) {
// Remove the tests from packages that haven't changed.
tests = tests
.filter(p => Object.keys(packages).some(name => {
const relativeRoot = relative(projectBaseDir, packages[name].root);
return p.startsWith(relativeRoot) && packages[name].dirty;
}));
logger.info(`Found ${tests.length} spec files, out of ${allTests.length}.`);
}
if (args.shard !== undefined) {
// Remove tests that are not part of this shard.
const shardId = args['shard'];
const nbShards = args['nb-shards'] || 2;
tests = tests.filter((name, i) => (i % nbShards) == shardId);
}
return new Promise(resolve => {
runner.onComplete((passed: boolean) => resolve(passed ? 0 : 1));
runner.execute(tests);
});
}