Skip to content

Commit 9fd70c9

Browse files
alxhubvicb
authored andcommitted
refactor(ivy): run the compiler compliance tests against ngtsc (angular#24862)
This commit moves the compiler compliance tests into compiler-cli, and uses ngtsc to run them instead of the custom compilation pipeline used before. Testing against ngtsc allows for validation of the real compiler output. This commit also fixes a few small issues that prevented the tests from passing. PR Close angular#24862
1 parent b7bbc82 commit 9fd70c9

17 files changed

+529
-652
lines changed

karma-js.conf.js

+1
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ module.exports = function(config) {
7373
'dist/all/@angular/benchpress/**',
7474
'dist/all/@angular/compiler-cli/**',
7575
'dist/all/@angular/compiler-cli/src/ngtsc/**',
76+
'dist/all/@angular/compiler-cli/test/compliance/**',
7677
'dist/all/@angular/compiler-cli/test/ngtsc/**',
7778
'dist/all/@angular/compiler/test/aot/**',
7879
'dist/all/@angular/compiler/test/render3/**',
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
load("//tools:defaults.bzl", "ts_library")
2+
load("@build_bazel_rules_nodejs//:defs.bzl", "jasmine_node_test")
3+
4+
ts_library(
5+
name = "test_lib",
6+
testonly = 1,
7+
srcs = glob(
8+
["**/*.ts"],
9+
),
10+
deps = [
11+
"//packages:types",
12+
"//packages/compiler",
13+
"//packages/compiler-cli",
14+
"//packages/compiler/test:test_utils",
15+
"//packages/compiler/testing",
16+
"//packages/core",
17+
],
18+
)
19+
20+
jasmine_node_test(
21+
name = "compliance",
22+
bootstrap = ["angular/tools/testing/init_node_spec.js"],
23+
data = [
24+
"//packages/common:npm_package",
25+
"//packages/core:npm_package",
26+
],
27+
tags = [
28+
"ivy-local",
29+
"ivy-only",
30+
],
31+
deps = [
32+
":test_lib",
33+
"//tools/testing:node",
34+
],
35+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Tests in this directory are excluded from running in the browser and only run in node.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {AotCompilerOptions} from '@angular/compiler';
10+
import {escapeRegExp} from '@angular/compiler/src/util';
11+
import {MockCompilerHost, MockData, MockDirectory, arrayToMockDir, settings, toMockFileArray} from '@angular/compiler/test/aot/test_util';
12+
import * as ts from 'typescript';
13+
14+
import {NgtscProgram} from '../../src/ngtsc/program';
15+
16+
17+
const IDENTIFIER = /[A-Za-z_$ɵ][A-Za-z0-9_$]*/;
18+
const OPERATOR =
19+
/!|%|\*|\/|\^|&&?|\|\|?|\(|\)|\{|\}|\[|\]|:|;|<=?|>=?|={1,3}|!==?|=>|\+\+?|--?|@|,|\.|\.\.\./;
20+
const STRING = /'[^']*'|"[^"]*"|`[\s\S]*?`/;
21+
const NUMBER = /\d+/;
22+
23+
const ELLIPSIS = '…';
24+
const TOKEN = new RegExp(
25+
`\\s*((${IDENTIFIER.source})|(${OPERATOR.source})|(${STRING.source})|${NUMBER.source}|${ELLIPSIS})`,
26+
'y');
27+
28+
type Piece = string | RegExp;
29+
30+
const SKIP = /(?:.|\n|\r)*/;
31+
32+
const ERROR_CONTEXT_WIDTH = 30;
33+
// Transform the expected output to set of tokens
34+
function tokenize(text: string): Piece[] {
35+
TOKEN.lastIndex = 0;
36+
37+
let match: RegExpMatchArray|null;
38+
const pieces: Piece[] = [];
39+
40+
while ((match = TOKEN.exec(text)) !== null) {
41+
const token = match[1];
42+
if (token === 'IDENT') {
43+
pieces.push(IDENTIFIER);
44+
} else if (token === ELLIPSIS) {
45+
pieces.push(SKIP);
46+
} else {
47+
pieces.push(token);
48+
}
49+
}
50+
51+
if (pieces.length === 0 || TOKEN.lastIndex !== 0) {
52+
const from = TOKEN.lastIndex;
53+
const to = from + ERROR_CONTEXT_WIDTH;
54+
throw Error(`Invalid test, no token found for '${text.substr(from, to)}...'`);
55+
}
56+
57+
return pieces;
58+
}
59+
60+
export function expectEmit(
61+
source: string, expected: string, description: string,
62+
assertIdentifiers?: {[name: string]: RegExp}) {
63+
// turns `// ...` into `…`
64+
// remove `// TODO` comment lines
65+
expected = expected.replace(/\/\/\s*\.\.\./g, ELLIPSIS).replace(/\/\/\s*TODO.*?\n/g, '');
66+
67+
const pieces = tokenize(expected);
68+
const {regexp, groups} = buildMatcher(pieces);
69+
const matches = source.match(regexp);
70+
if (matches === null) {
71+
let last: number = 0;
72+
for (let i = 1; i < pieces.length; i++) {
73+
const {regexp} = buildMatcher(pieces.slice(0, i));
74+
const m = source.match(regexp);
75+
const expectedPiece = pieces[i - 1] == IDENTIFIER ? '<IDENT>' : pieces[i - 1];
76+
if (!m) {
77+
fail(
78+
`${description}: Expected to find ${expectedPiece} '${source.substr(0,last)}[<---HERE expected "${expectedPiece}"]${source.substr(last)}'`);
79+
return;
80+
} else {
81+
last = (m.index || 0) + m[0].length;
82+
}
83+
}
84+
fail(
85+
`Test helper failure: Expected expression failed but the reporting logic could not find where it failed in: ${source}`);
86+
} else {
87+
if (assertIdentifiers) {
88+
// It might be possible to add the constraints in the original regexp (see `buildMatcher`)
89+
// by transforming the assertion regexps when using anchoring, grouping, back references,
90+
// flags, ...
91+
//
92+
// Checking identifiers after they have matched allows for a simple and flexible
93+
// implementation.
94+
// The overall performance are not impacted when `assertIdentifiers` is empty.
95+
const ids = Object.keys(assertIdentifiers);
96+
for (let i = 0; i < ids.length; i++) {
97+
const id = ids[i];
98+
if (groups.has(id)) {
99+
const name = matches[groups.get(id) as number];
100+
const regexp = assertIdentifiers[id];
101+
if (!regexp.test(name)) {
102+
throw Error(
103+
`${description}: The matching identifier "${id}" is "${name}" which doesn't match ${regexp}`);
104+
}
105+
}
106+
}
107+
}
108+
}
109+
}
110+
111+
const IDENT_LIKE = /^[a-z][A-Z]/;
112+
const MATCHING_IDENT = /^\$.*\$$/;
113+
114+
/*
115+
* Builds a regexp that matches the given `pieces`
116+
*
117+
* It returns:
118+
* - the `regexp` to be used to match the generated code,
119+
* - the `groups` which maps `$...$` identifier to their position in the regexp matches.
120+
*/
121+
function buildMatcher(pieces: (string | RegExp)[]): {regexp: RegExp, groups: Map<string, number>} {
122+
const results: string[] = [];
123+
let first = true;
124+
let group = 0;
125+
126+
const groups = new Map<string, number>();
127+
for (const piece of pieces) {
128+
if (!first)
129+
results.push(`\\s${typeof piece === 'string' && IDENT_LIKE.test(piece) ? '+' : '*'}`);
130+
first = false;
131+
if (typeof piece === 'string') {
132+
if (MATCHING_IDENT.test(piece)) {
133+
const matchGroup = groups.get(piece);
134+
if (!matchGroup) {
135+
results.push('(' + IDENTIFIER.source + ')');
136+
const newGroup = ++group;
137+
groups.set(piece, newGroup);
138+
} else {
139+
results.push(`\\${matchGroup}`);
140+
}
141+
} else {
142+
results.push(escapeRegExp(piece));
143+
}
144+
} else {
145+
results.push('(?:' + piece.source + ')');
146+
}
147+
}
148+
return {
149+
regexp: new RegExp(results.join('')),
150+
groups,
151+
};
152+
}
153+
154+
155+
export function compile(
156+
data: MockDirectory, angularFiles: MockData, options: AotCompilerOptions = {},
157+
errorCollector: (error: any, fileName?: string) => void = error => { throw error;}): {
158+
source: string,
159+
} {
160+
const testFiles = toMockFileArray(data);
161+
const scripts = testFiles.map(entry => entry.fileName);
162+
const angularFilesArray = toMockFileArray(angularFiles);
163+
const files = arrayToMockDir([...testFiles, ...angularFilesArray]);
164+
const mockCompilerHost = new MockCompilerHost(scripts, files);
165+
166+
const program = new NgtscProgram(
167+
scripts, {
168+
target: ts.ScriptTarget.ES2015,
169+
module: ts.ModuleKind.ES2015,
170+
moduleResolution: ts.ModuleResolutionKind.NodeJs, ...options,
171+
},
172+
mockCompilerHost);
173+
program.emit();
174+
const source =
175+
scripts.map(script => mockCompilerHost.readFile(script.replace(/\.ts$/, '.js'))).join('\n');
176+
177+
return {source};
178+
}

packages/compiler/test/render3/mock_compiler_spec.ts renamed to packages/compiler-cli/test/compliance/mock_compiler_spec.ts

+2-5
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
*/
88

99

10-
import {MockDirectory, setup} from '../aot/test_util';
10+
import {MockDirectory, setup} from '@angular/compiler/test/aot/test_util';
1111
import {compile, expectEmit} from './mock_compile';
1212

1313
describe('mock_compiler', () => {
@@ -47,9 +47,6 @@ describe('mock_compiler', () => {
4747
// result.source contains just the emitted factory declarations regardless of the original
4848
// module.
4949
expect(result.source).toContain('Hello');
50-
51-
// The output context is also returned if the actual output ast is needed.
52-
expect(result.outputContext.statements.length).toBeGreaterThan(0);
5350
});
5451
});
5552

@@ -143,7 +140,7 @@ describe('mock_compiler', () => {
143140
expectEmit(
144141
result.source, `
145142
// TODO: this comment should not be taken into account
146-
$r3$.ɵT(0, 'Hello!');
143+
$r3$.ɵT(0, "Hello!");
147144
// TODO: this comment should not be taken into account
148145
`,
149146
'todo comments should be ignored');

0 commit comments

Comments
 (0)