test(core): update JIT source mapping tests for ivy (#28055)

There are some differences in how ivy maps template source
compared to View Engine.  In this commit we recreate the View Engine
tests for ivy.

PR Close #28055
This commit is contained in:
Pete Bacon Darwin 2019-02-08 22:10:20 +00:00 committed by Misko Hevery
parent 4f46bfb779
commit e6a00be014
3 changed files with 432 additions and 210 deletions

View File

@ -28,7 +28,8 @@ import {DomElementSchemaRegistry} from './schema/dom_element_schema_registry';
export class CompilerFacadeImpl implements CompilerFacade {
R3ResolvedDependencyType = R3ResolvedDependencyType as any;
private elementSchemaRegistry = new DomElementSchemaRegistry();
private jitEvaluator = new JitEvaluator();
constructor(private jitEvaluator = new JitEvaluator()) {}
compilePipe(angularCoreEnv: CoreEnvironment, sourceMapUrl: string, facade: R3PipeMetadataFacade):
any {

View File

@ -46,6 +46,7 @@ ts_library(
"//packages/compiler",
"//packages/compiler/testing",
"//packages/core",
"//packages/core/src/compiler",
"//packages/core/testing",
"//packages/platform-server",
"//packages/platform-server/testing",

View File

@ -6,134 +6,82 @@
* found in the LICENSE file at https://angular.io/license
*/
import {ResourceLoader} from '@angular/compiler';
import {SourceMap} from '@angular/compiler/src/output/source_map';
import {ResourceLoader, SourceMap} from '@angular/compiler';
import {CompilerFacadeImpl} from '@angular/compiler/src/jit_compiler_facade';
import {JitEvaluator} from '@angular/compiler/src/output/output_jit';
import {escapeRegExp} from '@angular/compiler/src/util';
import {extractSourceMap, originalPositionFor} from '@angular/compiler/testing/src/output/source_map_util';
import {MockResourceLoader} from '@angular/compiler/testing/src/resource_loader_mock';
import {Attribute, Component, Directive, ErrorHandler, ɵglobal} from '@angular/core';
import {CompilerFacade, ExportedCompilerFacade} from '@angular/core/src/compiler/compiler_facade';
import {getErrorLogger} from '@angular/core/src/errors';
import {ivyEnabled} from '@angular/core/src/ivy_switch';
import {resolveComponentResources} from '@angular/core/src/metadata/resource_loading';
import {TestBed, fakeAsync, tick} from '@angular/core/testing';
import {fixmeIvy} from '@angular/private/testing';
import {fixmeIvy, modifiedInIvy, onlyInIvy} from '@angular/private/testing';
{
describe('jit source mapping', () => {
let jitSpy: jasmine.Spy;
let resourceLoader: MockResourceLoader;
let jitEvaluator: MockJitEvaluator;
beforeEach(() => {
// Jasmine relies on methods on `Function.prototype`, so restore the prototype on the spy.
// Work around for: https://github.com/jasmine/jasmine/issues/1573
// TODO: Figure out a better way to retrieve the JIT sources, without spying on `Function`.
const originalProto = ɵglobal.Function.prototype;
jitSpy = spyOn(ɵglobal, 'Function').and.callThrough();
ɵglobal.Function.prototype = originalProto;
resourceLoader = new MockResourceLoader();
TestBed.configureCompiler({providers: [{provide: ResourceLoader, useValue: resourceLoader}]});
jitEvaluator = new MockJitEvaluator();
TestBed.configureCompiler({
providers: [
{
provide: ResourceLoader,
useValue: resourceLoader,
},
{
provide: JitEvaluator,
useValue: jitEvaluator,
}
]
});
});
function getErrorLoggerStack(e: Error): string {
let logStack: string = undefined !;
getErrorLogger(e)(<any>{error: () => logStack = new Error().stack !}, e.message);
return logStack;
}
function getSourceMap(genFile: string): SourceMap {
const jitSources = jitSpy.calls.all().map((call) => call.args[call.args.length - 1]);
return jitSources.map(source => extractSourceMap(source))
.find(map => !!(map && map.file === genFile)) !;
}
function getSourcePositionForStack(stack: string):
{source: string, line: number, column: number} {
const ngFactoryLocations =
stack
.split('\n')
// e.g. at View_MyComp_0 (ng:///DynamicTestModule/MyComp.ngfactory.js:153:40)
.map(line => /\((.*\.ngfactory\.js):(\d+):(\d+)/.exec(line))
.filter(match => !!match)
.map(match => ({
file: match ![1],
line: parseInt(match ![2], 10),
column: parseInt(match ![3], 10)
}));
const ngFactoryLocation = ngFactoryLocations[0];
const sourceMap = getSourceMap(ngFactoryLocation.file);
return originalPositionFor(
sourceMap, {line: ngFactoryLocation.line, column: ngFactoryLocation.column});
}
function compileAndCreateComponent(comType: any) {
TestBed.configureTestingModule({declarations: [comType]});
let error: any;
TestBed.compileComponents().catch((e) => error = e);
if (resourceLoader.hasPendingRequests()) {
resourceLoader.flush();
}
tick();
if (error) {
throw error;
}
return TestBed.createComponent(comType);
}
modifiedInIvy('Generated filenames and stack traces have changed in ivy')
.describe('(View Engine)', () => {
describe('inline templates', () => {
const ngUrl = 'ng:///DynamicTestModule/MyComp.html';
function templateDecorator(template: string) { return {template}; }
declareTests({ngUrl, templateDecorator});
});
describe('external templates', () => {
const ngUrl = 'ng:///some/url.html';
const templateUrl = 'http://localhost:1234/some/url.html';
function templateDecorator(template: string) {
resourceLoader.expect(templateUrl, template);
return {templateUrl};
}
declareTests({ngUrl, templateDecorator});
});
function declareTests(
{ngUrl, templateDecorator}:
{ngUrl: string, templateDecorator: (template: string) => { [key: string]: any }}) {
fixmeIvy('FW-223: Generate source maps during template compilation')
.it('should use the right source url in html parse errors', fakeAsync(() => {
function declareTests({ngUrl, templateDecorator}: TestConfig) {
const ngFactoryUrl = 'ng:///DynamicTestModule/MyComp.ngfactory.js';
it('should use the right source url in html parse errors', fakeAsync(() => {
@Component({...templateDecorator('<div>\n </error>')})
class MyComp {
}
expect(() => {
ivyEnabled && resolveComponentResources(null !);
compileAndCreateComponent(MyComp);
})
.toThrowError(new RegExp(
`Template parse errors[\\s\\S]*${ngUrl.replace('$', '\\$')}@1:2`));
expect(() => { compileAndCreateComponent(MyComp); })
.toThrowError(
new RegExp(`Template parse errors[\\s\\S]*${escapeRegExp(ngUrl)}@1:2`));
}));
fixmeIvy('FW-223: Generate source maps during template compilation')
.it('should use the right source url in template parse errors', fakeAsync(() => {
it('should use the right source url in template parse errors', fakeAsync(() => {
@Component({...templateDecorator('<div>\n <div unknown="{{ctxProp}}"></div>')})
class MyComp {
}
expect(() => {
ivyEnabled && resolveComponentResources(null !);
compileAndCreateComponent(MyComp);
})
.toThrowError(new RegExp(
`Template parse errors[\\s\\S]*${ngUrl.replace('$', '\\$')}@1:7`));
expect(() => { compileAndCreateComponent(MyComp); })
.toThrowError(
new RegExp(`Template parse errors[\\s\\S]*${escapeRegExp(ngUrl)}@1:7`));
}));
fixmeIvy('FW-223: Generate source maps during template compilation')
.it('should create a sourceMap for templates', fakeAsync(() => {
it('should create a sourceMap for templates', fakeAsync(() => {
const template = `Hello World!`;
@Component({...templateDecorator(template)})
@ -142,16 +90,13 @@ import {fixmeIvy} from '@angular/private/testing';
compileAndCreateComponent(MyComp);
const sourceMap = getSourceMap('ng:///DynamicTestModule/MyComp.ngfactory.js');
expect(sourceMap.sources).toEqual([
'ng:///DynamicTestModule/MyComp.ngfactory.js', ngUrl
]);
const sourceMap = jitEvaluator.getSourceMap(ngFactoryUrl);
expect(sourceMap.sources).toEqual([ngFactoryUrl, ngUrl]);
expect(sourceMap.sourcesContent).toEqual([' ', template]);
}));
fixmeIvy('FW-223: Generate source maps during template compilation')
.it('should report source location for di errors', fakeAsync(() => {
it('should report source location for di errors', fakeAsync(() => {
const template = `<div>\n <div someDir></div></div>`;
@Component({...templateDecorator(template)})
@ -171,15 +116,16 @@ import {fixmeIvy} from '@angular/private/testing';
error = e;
}
// The error should be logged from the element
expect(getSourcePositionForStack(getErrorLoggerStack(error))).toEqual({
expect(
jitEvaluator.getSourcePositionForStack(getErrorLoggerStack(error), ngFactoryUrl))
.toEqual({
line: 2,
column: 4,
source: ngUrl,
});
}));
fixmeIvy('FW-223: Generate source maps during template compilation')
.it('should report di errors with multiple elements and directives', fakeAsync(() => {
it('should report di errors with multiple elements and directives', fakeAsync(() => {
const template = `<div someDir></div><div someDir="throw"></div>`;
@Component({...templateDecorator(template)})
@ -203,15 +149,16 @@ import {fixmeIvy} from '@angular/private/testing';
error = e;
}
// The error should be logged from the 2nd-element
expect(getSourcePositionForStack(getErrorLoggerStack(error))).toEqual({
expect(
jitEvaluator.getSourcePositionForStack(getErrorLoggerStack(error), ngFactoryUrl))
.toEqual({
line: 1,
column: 19,
source: ngUrl,
});
}));
fixmeIvy('FW-223: Generate source maps during template compilation')
.it('should report source location for binding errors', fakeAsync(() => {
it('should report source location for binding errors', fakeAsync(() => {
const template = `<div>\n <span [title]="createError()"></span></div>`;
@Component({...templateDecorator(template)})
@ -228,21 +175,22 @@ import {fixmeIvy} from '@angular/private/testing';
error = e;
}
// the stack should point to the binding
expect(getSourcePositionForStack(error.stack)).toEqual({
expect(jitEvaluator.getSourcePositionForStack(error.stack, ngFactoryUrl)).toEqual({
line: 2,
column: 12,
source: ngUrl,
});
// The error should be logged from the element
expect(getSourcePositionForStack(getErrorLoggerStack(error))).toEqual({
expect(
jitEvaluator.getSourcePositionForStack(getErrorLoggerStack(error), ngFactoryUrl))
.toEqual({
line: 2,
column: 4,
source: ngUrl,
});
}));
fixmeIvy('FW-223: Generate source maps during template compilation')
.it('should report source location for event errors', fakeAsync(() => {
it('should report source location for event errors', fakeAsync(() => {
const template = `<div>\n <span (click)="createError()"></span></div>`;
@Component({...templateDecorator(template)})
@ -258,13 +206,15 @@ import {fixmeIvy} from '@angular/private/testing';
comp.debugElement.children[0].children[0].triggerEventHandler('click', 'EVENT');
expect(error).toBeTruthy();
// the stack should point to the binding
expect(getSourcePositionForStack(error.stack)).toEqual({
expect(jitEvaluator.getSourcePositionForStack(error.stack, ngFactoryUrl)).toEqual({
line: 2,
column: 12,
source: ngUrl,
});
// The error should be logged from the element
expect(getSourcePositionForStack(getErrorLoggerStack(error))).toEqual({
expect(
jitEvaluator.getSourcePositionForStack(getErrorLoggerStack(error), ngFactoryUrl))
.toEqual({
line: 2,
column: 4,
source: ngUrl,
@ -273,4 +223,274 @@ import {fixmeIvy} from '@angular/private/testing';
}));
}
});
onlyInIvy('Generated filenames and stack traces have changed in ivy').describe('(Ivy)', () => {
beforeEach(() => overrideCompilerFacade());
afterEach(() => restoreCompilerFacade());
describe('inline templates', () => {
const ngUrl = 'ng:///MyComp/template.html';
function templateDecorator(template: string) { return {template}; }
declareTests({ngUrl, templateDecorator});
});
describe('external templates', () => {
const templateUrl = 'http://localhost:1234/some/url.html';
const ngUrl = templateUrl;
function templateDecorator(template: string) {
resourceLoader.expect(templateUrl, template);
return {templateUrl};
}
declareTests({ngUrl, templateDecorator});
});
function declareTests({ngUrl, templateDecorator}: TestConfig) {
const generatedUrl = 'ng:///MyComp.js';
it('should use the right source url in html parse errors', fakeAsync(() => {
const template = '<div>\n </error>';
@Component({...templateDecorator(template)})
class MyComp {
}
expect(() => {
resolveCompileAndCreateComponent(MyComp, template);
}).toThrowError(new RegExp(`${escapeRegExp(ngUrl)}@1:2`));
}));
fixmeIvy('FW-511: Report template typing errors')
.it('should use the right source url in template parse errors', fakeAsync(() => {
const template = '<div>\n <div unknown="{{ctxProp}}"></div>';
@Component({...templateDecorator(template)})
class MyComp {
}
expect(() => { resolveCompileAndCreateComponent(MyComp, template); })
.toThrowError(
new RegExp(`Template parse errors[\\s\\S]*${escapeRegExp(ngUrl)}@1:7`));
}));
it('should create a sourceMap for templates', fakeAsync(() => {
const template = `Hello World!`;
@Component({...templateDecorator(template)})
class MyComp {
}
resolveCompileAndCreateComponent(MyComp, template);
const sourceMap = jitEvaluator.getSourceMap(generatedUrl);
expect(sourceMap.sources).toEqual([generatedUrl, ngUrl]);
expect(sourceMap.sourcesContent).toEqual([' ', template]);
}));
it('should report source location for di errors', fakeAsync(() => {
const template = `<div>\n <div someDir></div></div>`;
@Component({...templateDecorator(template)})
class MyComp {
}
@Directive({selector: '[someDir]'})
class SomeDir {
constructor() { throw new Error('Test'); }
}
TestBed.configureTestingModule({declarations: [SomeDir]});
let error: any;
try {
resolveCompileAndCreateComponent(MyComp, template);
} catch (e) {
error = e;
}
// The error should be logged from the element
expect(jitEvaluator.getSourcePositionForStack(error.stack, generatedUrl)).toEqual({
line: 2,
column: 4,
source: ngUrl,
});
}));
it('should report di errors with multiple elements and directives', fakeAsync(() => {
const template = `<div someDir></div><div someDir="throw"></div>`;
@Component({...templateDecorator(template)})
class MyComp {
}
@Directive({selector: '[someDir]'})
class SomeDir {
constructor(@Attribute('someDir') someDir: string) {
if (someDir === 'throw') {
throw new Error('Test');
}
}
}
TestBed.configureTestingModule({declarations: [SomeDir]});
let error: any;
try {
resolveCompileAndCreateComponent(MyComp, template);
} catch (e) {
error = e;
}
// The error should be logged from the 2nd-element
expect(jitEvaluator.getSourcePositionForStack(error.stack, generatedUrl)).toEqual({
line: 1,
column: 19,
source: ngUrl,
});
}));
it('should report source location for binding errors', fakeAsync(() => {
const template = `<div>\n <span [title]="createError()"></span></div>`;
@Component({...templateDecorator(template)})
class MyComp {
createError() { throw new Error('Test'); }
}
const comp = resolveCompileAndCreateComponent(MyComp, template);
let error: any;
try {
comp.detectChanges();
} catch (e) {
error = e;
}
// the stack should point to the binding
expect(jitEvaluator.getSourcePositionForStack(error.stack, generatedUrl)).toEqual({
line: 2,
column: 12,
source: ngUrl,
});
}));
it('should report source location for event errors', fakeAsync(() => {
const template = `<div>\n <span (click)="createError()"></span></div>`;
@Component({...templateDecorator(template)})
class MyComp {
createError() { throw new Error('Test'); }
}
const comp = resolveCompileAndCreateComponent(MyComp, template);
let error: any;
const errorHandler = TestBed.get(ErrorHandler);
spyOn(errorHandler, 'handleError').and.callFake((e: any) => error = e);
try {
comp.debugElement.children[0].children[0].triggerEventHandler('click', 'EVENT');
} catch (e) {
error = e;
}
expect(error).toBeTruthy();
// the stack should point to the binding
expect(jitEvaluator.getSourcePositionForStack(error.stack, generatedUrl)).toEqual({
line: 2,
column: 21,
source: ngUrl,
});
}));
}
});
function compileAndCreateComponent(comType: any) {
TestBed.configureTestingModule({declarations: [comType]});
let error: any;
TestBed.compileComponents().catch((e) => error = e);
if (resourceLoader.hasPendingRequests()) {
resourceLoader.flush();
}
tick();
if (error) {
throw error;
}
return TestBed.createComponent(comType);
}
function createResolver(contents: string) { return (_url: string) => Promise.resolve(contents); }
function resolveCompileAndCreateComponent(comType: any, template: string) {
resolveComponentResources(createResolver(template));
return compileAndCreateComponent(comType);
}
let ɵcompilerFacade: CompilerFacade;
function overrideCompilerFacade() {
const ng: ExportedCompilerFacade = (global as any).ng;
if (ng) {
ɵcompilerFacade = ng.ɵcompilerFacade;
ng.ɵcompilerFacade = new CompilerFacadeImpl(jitEvaluator);
}
}
function restoreCompilerFacade() {
if (ɵcompilerFacade) {
const ng: ExportedCompilerFacade = (global as any).ng;
ng.ɵcompilerFacade = ɵcompilerFacade;
}
}
interface TestConfig {
ngUrl: string;
templateDecorator: (template: string) => { [key: string]: any };
}
interface SourcePos {
source: string;
line: number;
column: number;
}
/**
* A helper class that captures the sources that have been JIT compiled.
*/
class MockJitEvaluator extends JitEvaluator {
sources: string[] = [];
executeFunction(fn: Function, args: any[]) {
// Capture the source that has been generated.
this.sources.push(fn.toString());
// Then execute it anyway.
return super.executeFunction(fn, args);
}
/**
* Get the source-map for a specified JIT compiled file.
* @param genFile the URL of the file whose source-map we want.
*/
getSourceMap(genFile: string): SourceMap {
return this.sources.map(source => extractSourceMap(source))
.find(map => !!(map && map.file === genFile)) !;
}
getSourcePositionForStack(stack: string, genFile: string): SourcePos {
const urlRegexp = new RegExp(`(${escapeRegExp(genFile)}):(\\d+):(\\d+)`);
const pos = stack.split('\n')
.map(line => urlRegexp.exec(line))
.filter(match => !!match)
.map(match => ({
file: match ![1],
line: parseInt(match ![2], 10),
column: parseInt(match ![3], 10)
}))
.shift();
if (!pos) {
throw new Error(`${genFile} was not mentioned in this stack:\n${stack}`);
}
const sourceMap = this.getSourceMap(pos.file);
return originalPositionFor(sourceMap, pos);
}
}
function getErrorLoggerStack(e: Error): string {
let logStack: string = undefined !;
getErrorLogger(e)(<any>{error: () => logStack = new Error().stack !}, e.message);
return logStack;
}
});