From e6a00be014a986780386f484adb999ae9129a790 Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Fri, 8 Feb 2019 22:10:20 +0000 Subject: [PATCH] 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 --- packages/compiler/src/jit_compiler_facade.ts | 3 +- packages/core/test/BUILD.bazel | 1 + .../source_map_integration_node_only_spec.ts | 638 ++++++++++++------ 3 files changed, 432 insertions(+), 210 deletions(-) diff --git a/packages/compiler/src/jit_compiler_facade.ts b/packages/compiler/src/jit_compiler_facade.ts index a7d8cef6dd..9a5b3ba510 100644 --- a/packages/compiler/src/jit_compiler_facade.ts +++ b/packages/compiler/src/jit_compiler_facade.ts @@ -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 { diff --git a/packages/core/test/BUILD.bazel b/packages/core/test/BUILD.bazel index b546ff61a7..9bfbf00aa8 100644 --- a/packages/core/test/BUILD.bazel +++ b/packages/core/test/BUILD.bazel @@ -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", diff --git a/packages/core/test/linker/source_map_integration_node_only_spec.ts b/packages/core/test/linker/source_map_integration_node_only_spec.ts index ce3cd63746..1ea8e36f62 100644 --- a/packages/core/test/linker/source_map_integration_node_only_spec.ts +++ b/packages/core/test/linker/source_map_integration_node_only_spec.ts @@ -6,93 +6,238 @@ * 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; +describe('jit source mapping', () => { + 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}]}); + beforeEach(() => { + resourceLoader = new MockResourceLoader(); + 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)({error: () => logStack = new Error().stack !}, e.message); - return logStack; - } + 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}); + }); - 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)) !; - } + 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 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]; + function declareTests({ngUrl, templateDecorator}: TestConfig) { + const ngFactoryUrl = 'ng:///DynamicTestModule/MyComp.ngfactory.js'; - const sourceMap = getSourceMap(ngFactoryLocation.file); - return originalPositionFor( - sourceMap, {line: ngFactoryLocation.line, column: ngFactoryLocation.column}); - } + it('should use the right source url in html parse errors', fakeAsync(() => { + @Component({...templateDecorator('
\n ')}) + class MyComp { + } - function compileAndCreateComponent(comType: any) { - TestBed.configureTestingModule({declarations: [comType]}); + expect(() => { compileAndCreateComponent(MyComp); }) + .toThrowError( + new RegExp(`Template parse errors[\\s\\S]*${escapeRegExp(ngUrl)}@1:2`)); + })); - let error: any; - TestBed.compileComponents().catch((e) => error = e); - if (resourceLoader.hasPendingRequests()) { - resourceLoader.flush(); - } - tick(); - if (error) { - throw error; - } - return TestBed.createComponent(comType); - } + it('should use the right source url in template parse errors', fakeAsync(() => { + @Component({...templateDecorator('
\n
')}) + class MyComp { + } + + expect(() => { compileAndCreateComponent(MyComp); }) + .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 { + } + + 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 = `
\n
`; + + @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 = `
`; + + @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 = `
\n
`; + + @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 = `
\n
`; + + @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', () => { - const ngUrl = 'ng:///DynamicTestModule/MyComp.html'; - + const ngUrl = 'ng:///MyComp/template.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'; - + const ngUrl = templateUrl; function templateDecorator(template: string) { resourceLoader.expect(templateUrl, template); return {templateUrl}; @@ -101,176 +246,251 @@ import {fixmeIvy} from '@angular/private/testing'; 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(() => { - @Component({...templateDecorator('
\n ')}) - class MyComp { - } + function declareTests({ngUrl, templateDecorator}: TestConfig) { + const generatedUrl = 'ng:///MyComp.js'; - expect(() => { - ivyEnabled && resolveComponentResources(null !); - compileAndCreateComponent(MyComp); - }) - .toThrowError(new RegExp( - `Template parse errors[\\s\\S]*${ngUrl.replace('$', '\\$')}@1:2`)); - })); + it('should use the right source url in html parse errors', fakeAsync(() => { + const template = '
\n '; + @Component({...templateDecorator(template)}) + class MyComp { + } - 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(() => { - @Component({...templateDecorator('
\n
')}) - 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!`; - + const template = '
\n
'; @Component({...templateDecorator(template)}) class MyComp { } - compileAndCreateComponent(MyComp); - - const sourceMap = getSourceMap('ng:///DynamicTestModule/MyComp.ngfactory.js'); - expect(sourceMap.sources).toEqual([ - 'ng:///DynamicTestModule/MyComp.ngfactory.js', ngUrl - ]); - expect(sourceMap.sourcesContent).toEqual([' ', template]); + 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!`; - fixmeIvy('FW-223: Generate source maps during template compilation') - .it('should report source location for di errors', fakeAsync(() => { - const template = `
\n
`; + @Component({...templateDecorator(template)}) + class MyComp { + } - @Component({...templateDecorator(template)}) - class MyComp { - } + resolveCompileAndCreateComponent(MyComp, template); - @Directive({selector: '[someDir]'}) - class SomeDir { - constructor() { throw new Error('Test'); } - } + const sourceMap = jitEvaluator.getSourceMap(generatedUrl); + expect(sourceMap.sources).toEqual([generatedUrl, ngUrl]); + 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 di errors with multiple elements and directives', fakeAsync(() => { - const template = `
`; + it('should report source location for di errors', fakeAsync(() => { + const template = `
\n
`; - @Component({...templateDecorator(template)}) - class MyComp { - } + @Component({...templateDecorator(template)}) + class MyComp { + } - @Directive({selector: '[someDir]'}) - class SomeDir { - constructor(@Attribute('someDir') someDir: string) { - if (someDir === 'throw') { - throw new Error('Test'); - } - } - } + @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 2nd-element - expect(getSourcePositionForStack(getErrorLoggerStack(error))).toEqual({ - line: 1, - column: 19, - source: ngUrl, - }); - })); + 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, + }); + })); - fixmeIvy('FW-223: Generate source maps during template compilation') - .it('should report source location for binding errors', fakeAsync(() => { - const template = `
\n
`; + it('should report di errors with multiple elements and directives', fakeAsync(() => { + const template = `
`; - @Component({...templateDecorator(template)}) - class MyComp { - createError() { throw new Error('Test'); } - } + @Component({...templateDecorator(template)}) + class MyComp { + } - const comp = compileAndCreateComponent(MyComp); + @Directive({selector: '[someDir]'}) + class SomeDir { + constructor(@Attribute('someDir') someDir: string) { + if (someDir === 'throw') { + throw new Error('Test'); + } + } + } - let error: any; - try { - comp.detectChanges(); - } catch (e) { - error = e; - } - // the stack should point to the binding - expect(getSourcePositionForStack(error.stack)).toEqual({ - line: 2, - column: 12, - source: ngUrl, - }); - // The error should be logged from the element - expect(getSourcePositionForStack(getErrorLoggerStack(error))).toEqual({ - line: 2, - column: 4, - source: ngUrl, - }); - })); + 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, + }); + })); - fixmeIvy('FW-223: Generate source maps during template compilation') - .it('should report source location for event errors', fakeAsync(() => { - const template = `
\n
`; + it('should report source location for binding errors', fakeAsync(() => { + const template = `
\n
`; - @Component({...templateDecorator(template)}) - class MyComp { - createError() { throw new Error('Test'); } - } + @Component({...templateDecorator(template)}) + class MyComp { + createError() { throw new Error('Test'); } + } - const comp = compileAndCreateComponent(MyComp); + const comp = resolveCompileAndCreateComponent(MyComp, template); - 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(getSourcePositionForStack(error.stack)).toEqual({ - line: 2, - column: 12, - source: ngUrl, - }); - // The error should be logged from the element - expect(getSourcePositionForStack(getErrorLoggerStack(error))).toEqual({ - line: 2, - column: 4, - source: ngUrl, - }); + 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 = `
\n
`; + + @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)({error: () => logStack = new Error().stack !}, e.message); + return logStack; + } +});