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 { export class CompilerFacadeImpl implements CompilerFacade {
R3ResolvedDependencyType = R3ResolvedDependencyType as any; R3ResolvedDependencyType = R3ResolvedDependencyType as any;
private elementSchemaRegistry = new DomElementSchemaRegistry(); private elementSchemaRegistry = new DomElementSchemaRegistry();
private jitEvaluator = new JitEvaluator();
constructor(private jitEvaluator = new JitEvaluator()) {}
compilePipe(angularCoreEnv: CoreEnvironment, sourceMapUrl: string, facade: R3PipeMetadataFacade): compilePipe(angularCoreEnv: CoreEnvironment, sourceMapUrl: string, facade: R3PipeMetadataFacade):
any { any {

View File

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

View File

@ -6,93 +6,238 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {ResourceLoader} from '@angular/compiler'; import {ResourceLoader, SourceMap} from '@angular/compiler';
import {SourceMap} from '@angular/compiler/src/output/source_map'; 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 {extractSourceMap, originalPositionFor} from '@angular/compiler/testing/src/output/source_map_util';
import {MockResourceLoader} from '@angular/compiler/testing/src/resource_loader_mock'; import {MockResourceLoader} from '@angular/compiler/testing/src/resource_loader_mock';
import {Attribute, Component, Directive, ErrorHandler, ɵglobal} from '@angular/core'; 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 {getErrorLogger} from '@angular/core/src/errors';
import {ivyEnabled} from '@angular/core/src/ivy_switch';
import {resolveComponentResources} from '@angular/core/src/metadata/resource_loading'; import {resolveComponentResources} from '@angular/core/src/metadata/resource_loading';
import {TestBed, fakeAsync, tick} from '@angular/core/testing'; 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', () => {
describe('jit source mapping', () => { let resourceLoader: MockResourceLoader;
let jitSpy: jasmine.Spy; let jitEvaluator: MockJitEvaluator;
let resourceLoader: MockResourceLoader;
beforeEach(() => { beforeEach(() => {
// Jasmine relies on methods on `Function.prototype`, so restore the prototype on the spy. resourceLoader = new MockResourceLoader();
// Work around for: https://github.com/jasmine/jasmine/issues/1573 jitEvaluator = new MockJitEvaluator();
// TODO: Figure out a better way to retrieve the JIT sources, without spying on `Function`. TestBed.configureCompiler({
const originalProto = ɵglobal.Function.prototype; providers: [
jitSpy = spyOn(ɵglobal, 'Function').and.callThrough(); {
ɵglobal.Function.prototype = originalProto; provide: ResourceLoader,
useValue: resourceLoader,
resourceLoader = new MockResourceLoader(); },
TestBed.configureCompiler({providers: [{provide: ResourceLoader, useValue: resourceLoader}]}); {
provide: JitEvaluator,
useValue: jitEvaluator,
}
]
}); });
});
function getErrorLoggerStack(e: Error): string { modifiedInIvy('Generated filenames and stack traces have changed in ivy')
let logStack: string = undefined !; .describe('(View Engine)', () => {
getErrorLogger(e)(<any>{error: () => logStack = new Error().stack !}, e.message); describe('inline templates', () => {
return logStack; const ngUrl = 'ng:///DynamicTestModule/MyComp.html';
} function templateDecorator(template: string) { return {template}; }
declareTests({ngUrl, templateDecorator});
});
function getSourceMap(genFile: string): SourceMap { describe('external templates', () => {
const jitSources = jitSpy.calls.all().map((call) => call.args[call.args.length - 1]); const ngUrl = 'ng:///some/url.html';
return jitSources.map(source => extractSourceMap(source)) const templateUrl = 'http://localhost:1234/some/url.html';
.find(map => !!(map && map.file === genFile)) !; function templateDecorator(template: string) {
} resourceLoader.expect(templateUrl, template);
return {templateUrl};
}
declareTests({ngUrl, templateDecorator});
});
function getSourcePositionForStack(stack: string): function declareTests({ngUrl, templateDecorator}: TestConfig) {
{source: string, line: number, column: number} { const ngFactoryUrl = 'ng:///DynamicTestModule/MyComp.ngfactory.js';
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); it('should use the right source url in html parse errors', fakeAsync(() => {
return originalPositionFor( @Component({...templateDecorator('<div>\n </error>')})
sourceMap, {line: ngFactoryLocation.line, column: ngFactoryLocation.column}); class MyComp {
} }
function compileAndCreateComponent(comType: any) { expect(() => { compileAndCreateComponent(MyComp); })
TestBed.configureTestingModule({declarations: [comType]}); .toThrowError(
new RegExp(`Template parse errors[\\s\\S]*${escapeRegExp(ngUrl)}@1:2`));
}));
let error: any; it('should use the right source url in template parse errors', fakeAsync(() => {
TestBed.compileComponents().catch((e) => error = e); @Component({...templateDecorator('<div>\n <div unknown="{{ctxProp}}"></div>')})
if (resourceLoader.hasPendingRequests()) { class MyComp {
resourceLoader.flush(); }
}
tick(); expect(() => { compileAndCreateComponent(MyComp); })
if (error) { .toThrowError(
throw error; new RegExp(`Template parse errors[\\s\\S]*${escapeRegExp(ngUrl)}@1:7`));
} }));
return TestBed.createComponent(comType);
} it('should create a sourceMap for templates', fakeAsync(() => {
const template = `Hello World!`;
@Component({...templateDecorator(template)})
class MyComp {
}
compileAndCreateComponent(MyComp);
const sourceMap = jitEvaluator.getSourceMap(ngFactoryUrl);
expect(sourceMap.sources).toEqual([ngFactoryUrl, 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 {
compileAndCreateComponent(MyComp);
} catch (e) {
error = e;
}
// The error should be logged from the element
expect(
jitEvaluator.getSourcePositionForStack(getErrorLoggerStack(error), ngFactoryUrl))
.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 {
compileAndCreateComponent(MyComp);
} catch (e) {
error = e;
}
// The error should be logged from the 2nd-element
expect(
jitEvaluator.getSourcePositionForStack(getErrorLoggerStack(error), ngFactoryUrl))
.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 = compileAndCreateComponent(MyComp);
let error: any;
try {
comp.detectChanges();
} catch (e) {
error = e;
}
// the stack should point to the binding
expect(jitEvaluator.getSourcePositionForStack(error.stack, ngFactoryUrl)).toEqual({
line: 2,
column: 12,
source: ngUrl,
});
// The error should be logged from the element
expect(
jitEvaluator.getSourcePositionForStack(getErrorLoggerStack(error), ngFactoryUrl))
.toEqual({
line: 2,
column: 4,
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 = compileAndCreateComponent(MyComp);
let error: any;
const errorHandler = TestBed.get(ErrorHandler);
spyOn(errorHandler, 'handleError').and.callFake((e: any) => error = e);
comp.debugElement.children[0].children[0].triggerEventHandler('click', 'EVENT');
expect(error).toBeTruthy();
// the stack should point to the binding
expect(jitEvaluator.getSourcePositionForStack(error.stack, ngFactoryUrl)).toEqual({
line: 2,
column: 12,
source: ngUrl,
});
// The error should be logged from the element
expect(
jitEvaluator.getSourcePositionForStack(getErrorLoggerStack(error), ngFactoryUrl))
.toEqual({
line: 2,
column: 4,
source: ngUrl,
});
}));
}
});
onlyInIvy('Generated filenames and stack traces have changed in ivy').describe('(Ivy)', () => {
beforeEach(() => overrideCompilerFacade());
afterEach(() => restoreCompilerFacade());
describe('inline templates', () => { describe('inline templates', () => {
const ngUrl = 'ng:///DynamicTestModule/MyComp.html'; const ngUrl = 'ng:///MyComp/template.html';
function templateDecorator(template: string) { return {template}; } function templateDecorator(template: string) { return {template}; }
declareTests({ngUrl, templateDecorator}); declareTests({ngUrl, templateDecorator});
}); });
describe('external templates', () => { describe('external templates', () => {
const ngUrl = 'ng:///some/url.html';
const templateUrl = 'http://localhost:1234/some/url.html'; const templateUrl = 'http://localhost:1234/some/url.html';
const ngUrl = templateUrl;
function templateDecorator(template: string) { function templateDecorator(template: string) {
resourceLoader.expect(templateUrl, template); resourceLoader.expect(templateUrl, template);
return {templateUrl}; return {templateUrl};
@ -101,176 +246,251 @@ import {fixmeIvy} from '@angular/private/testing';
declareTests({ngUrl, templateDecorator}); declareTests({ngUrl, templateDecorator});
}); });
function declareTests( function declareTests({ngUrl, templateDecorator}: TestConfig) {
{ngUrl, templateDecorator}: const generatedUrl = 'ng:///MyComp.js';
{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(() => {
@Component({...templateDecorator('<div>\n </error>')})
class MyComp {
}
expect(() => { it('should use the right source url in html parse errors', fakeAsync(() => {
ivyEnabled && resolveComponentResources(null !); const template = '<div>\n </error>';
compileAndCreateComponent(MyComp); @Component({...templateDecorator(template)})
}) class MyComp {
.toThrowError(new RegExp( }
`Template parse errors[\\s\\S]*${ngUrl.replace('$', '\\$')}@1:2`));
}));
fixmeIvy('FW-223: Generate source maps during template compilation') 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(() => { .it('should use the right source url in template parse errors', fakeAsync(() => {
@Component({...templateDecorator('<div>\n <div unknown="{{ctxProp}}"></div>')}) const template = '<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`));
}));
fixmeIvy('FW-223: Generate source maps during template compilation')
.it('should create a sourceMap for templates', fakeAsync(() => {
const template = `Hello World!`;
@Component({...templateDecorator(template)}) @Component({...templateDecorator(template)})
class MyComp { class MyComp {
} }
compileAndCreateComponent(MyComp); expect(() => { resolveCompileAndCreateComponent(MyComp, template); })
.toThrowError(
const sourceMap = getSourceMap('ng:///DynamicTestModule/MyComp.ngfactory.js'); new RegExp(`Template parse errors[\\s\\S]*${escapeRegExp(ngUrl)}@1:7`));
expect(sourceMap.sources).toEqual([
'ng:///DynamicTestModule/MyComp.ngfactory.js', ngUrl
]);
expect(sourceMap.sourcesContent).toEqual([' ', template]);
})); }));
it('should create a sourceMap for templates', fakeAsync(() => {
const template = `Hello World!`;
fixmeIvy('FW-223: Generate source maps during template compilation') @Component({...templateDecorator(template)})
.it('should report source location for di errors', fakeAsync(() => { class MyComp {
const template = `<div>\n <div someDir></div></div>`; }
@Component({...templateDecorator(template)}) resolveCompileAndCreateComponent(MyComp, template);
class MyComp {
}
@Directive({selector: '[someDir]'}) const sourceMap = jitEvaluator.getSourceMap(generatedUrl);
class SomeDir { expect(sourceMap.sources).toEqual([generatedUrl, ngUrl]);
constructor() { throw new Error('Test'); } expect(sourceMap.sourcesContent).toEqual([' ', template]);
} }));
TestBed.configureTestingModule({declarations: [SomeDir]});
let error: any;
try {
compileAndCreateComponent(MyComp);
} catch (e) {
error = e;
}
// The error should be logged from the element
expect(getSourcePositionForStack(getErrorLoggerStack(error))).toEqual({
line: 2,
column: 4,
source: ngUrl,
});
}));
fixmeIvy('FW-223: Generate source maps during template compilation') it('should report source location for di errors', fakeAsync(() => {
.it('should report di errors with multiple elements and directives', fakeAsync(() => { const template = `<div>\n <div someDir></div></div>`;
const template = `<div someDir></div><div someDir="throw"></div>`;
@Component({...templateDecorator(template)}) @Component({...templateDecorator(template)})
class MyComp { class MyComp {
} }
@Directive({selector: '[someDir]'}) @Directive({selector: '[someDir]'})
class SomeDir { class SomeDir {
constructor(@Attribute('someDir') someDir: string) { constructor() { throw new Error('Test'); }
if (someDir === 'throw') { }
throw new Error('Test');
}
}
}
TestBed.configureTestingModule({declarations: [SomeDir]}); TestBed.configureTestingModule({declarations: [SomeDir]});
let error: any; let error: any;
try { try {
compileAndCreateComponent(MyComp); resolveCompileAndCreateComponent(MyComp, template);
} catch (e) { } catch (e) {
error = e; error = e;
} }
// The error should be logged from the 2nd-element // The error should be logged from the element
expect(getSourcePositionForStack(getErrorLoggerStack(error))).toEqual({ expect(jitEvaluator.getSourcePositionForStack(error.stack, generatedUrl)).toEqual({
line: 1, line: 2,
column: 19, column: 4,
source: ngUrl, 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 source location for binding errors', fakeAsync(() => { const template = `<div someDir></div><div someDir="throw"></div>`;
const template = `<div>\n <span [title]="createError()"></span></div>`;
@Component({...templateDecorator(template)}) @Component({...templateDecorator(template)})
class MyComp { class MyComp {
createError() { throw new Error('Test'); } }
}
const comp = compileAndCreateComponent(MyComp); @Directive({selector: '[someDir]'})
class SomeDir {
constructor(@Attribute('someDir') someDir: string) {
if (someDir === 'throw') {
throw new Error('Test');
}
}
}
let error: any; TestBed.configureTestingModule({declarations: [SomeDir]});
try { let error: any;
comp.detectChanges(); try {
} catch (e) { resolveCompileAndCreateComponent(MyComp, template);
error = e; } catch (e) {
} error = e;
// the stack should point to the binding }
expect(getSourcePositionForStack(error.stack)).toEqual({ // The error should be logged from the 2nd-element
line: 2, expect(jitEvaluator.getSourcePositionForStack(error.stack, generatedUrl)).toEqual({
column: 12, line: 1,
source: ngUrl, column: 19,
}); source: ngUrl,
// The error should be logged from the element });
expect(getSourcePositionForStack(getErrorLoggerStack(error))).toEqual({ }));
line: 2,
column: 4,
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 event errors', fakeAsync(() => { const template = `<div>\n <span [title]="createError()"></span></div>`;
const template = `<div>\n <span (click)="createError()"></span></div>`;
@Component({...templateDecorator(template)}) @Component({...templateDecorator(template)})
class MyComp { class MyComp {
createError() { throw new Error('Test'); } createError() { throw new Error('Test'); }
} }
const comp = compileAndCreateComponent(MyComp); const comp = resolveCompileAndCreateComponent(MyComp, template);
let error: any; let error: any;
const errorHandler = TestBed.get(ErrorHandler); try {
spyOn(errorHandler, 'handleError').and.callFake((e: any) => error = e); comp.detectChanges();
comp.debugElement.children[0].children[0].triggerEventHandler('click', 'EVENT'); } catch (e) {
expect(error).toBeTruthy(); error = e;
// the stack should point to the binding }
expect(getSourcePositionForStack(error.stack)).toEqual({ // the stack should point to the binding
line: 2, expect(jitEvaluator.getSourcePositionForStack(error.stack, generatedUrl)).toEqual({
column: 12, line: 2,
source: ngUrl, column: 12,
}); source: ngUrl,
// The error should be logged from the element });
expect(getSourcePositionForStack(getErrorLoggerStack(error))).toEqual({ }));
line: 2,
column: 4,
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;
}
});