From 6173609c5b1fd27c75cee8dd9b67de9ef332ef98 Mon Sep 17 00:00:00 2001 From: Alan Agius Date: Tue, 19 Oct 2021 15:41:19 +0200 Subject: [PATCH] fix(@angular-devkit/build-angular): transform remapped sourcemap into a plain object `remapping` will return a SourceMap which it's prototype is not a simple object. This causes Babel to fail when it invoked from `istanbul-lib-instrument` because Babel with `don't know how to turn this value into a node` error as Babel will not process not simple objects. See: https://github.com/babel/babel/blob/780aa48d2a34dc55f556843074b6aed45e7eabeb/packages/babel-types/src/converters/valueToNode.ts#L115-L130 Closes #21967 (cherry picked from commit da1733cc69eeac1417e47df23389da1658850ada) --- .../build_angular/src/babel/webpack-loader.ts | 14 ++- .../tests/behavior/code-coverage_spec.ts | 89 +++++++++++++++++++ .../karma/tests/options/code-coverage_spec.ts | 47 ---------- 3 files changed, 99 insertions(+), 51 deletions(-) create mode 100644 packages/angular_devkit/build_angular/src/karma/tests/behavior/code-coverage_spec.ts diff --git a/packages/angular_devkit/build_angular/src/babel/webpack-loader.ts b/packages/angular_devkit/build_angular/src/babel/webpack-loader.ts index 9250b0e8d117..2a14774f9af0 100644 --- a/packages/angular_devkit/build_angular/src/babel/webpack-loader.ts +++ b/packages/angular_devkit/build_angular/src/babel/webpack-loader.ts @@ -185,10 +185,16 @@ export default custom(() => { // `@ampproject/remapping` source map objects but both are compatible with Webpack. // This method for merging is used because it provides more accurate output // and is faster while using less memory. - result.map = remapping( - [result.map as SourceMapInput, inputSourceMap as SourceMapInput], - () => null, - ) as typeof result.map; + result.map = { + // Convert the SourceMap back to simple plain object. + // This is needed because otherwise code-coverage will fail with `don't know how to turn this value into a node` + // Which is throw by Babel when it is invoked again from `istanbul-lib-instrument`. + // https://github.com/babel/babel/blob/780aa48d2a34dc55f556843074b6aed45e7eabeb/packages/babel-types/src/converters/valueToNode.ts#L115-L130 + ...(remapping( + [result.map as SourceMapInput, inputSourceMap as SourceMapInput], + () => null, + ) as typeof result.map), + }; } return result; diff --git a/packages/angular_devkit/build_angular/src/karma/tests/behavior/code-coverage_spec.ts b/packages/angular_devkit/build_angular/src/karma/tests/behavior/code-coverage_spec.ts new file mode 100644 index 000000000000..0793c4d2c4b0 --- /dev/null +++ b/packages/angular_devkit/build_angular/src/karma/tests/behavior/code-coverage_spec.ts @@ -0,0 +1,89 @@ +/** + * @license + * Copyright Google LLC 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 + */ + +import { last, tap } from 'rxjs/operators'; +import { promisify } from 'util'; +import { execute } from '../../index'; +import { BASE_OPTIONS, KARMA_BUILDER_INFO, describeBuilder } from '../setup'; + +// In each of the test below we'll have to call setTimeout to wait for the coverage +// analysis to be done. This is because karma-coverage performs the analysis +// asynchronously but the promise that it returns is not awaited by Karma. +// Coverage analysis begins when onRunComplete() is invoked, and output files +// are subsequently written to disk. For more information, see +// https://github.com/karma-runner/karma-coverage/blob/32acafa90ed621abd1df730edb44ae55a4009c2c/lib/reporter.js#L221 + +const setTimeoutPromise = promisify(setTimeout); +const coveragePath = 'coverage/lcov.info'; + +describeBuilder(execute, KARMA_BUILDER_INFO, (harness) => { + describe('Behavior: "codeCoverage"', () => { + it('should generate coverage report when file was previously processed by Babel', async () => { + // Force Babel transformation. + await harness.appendToFile('src/app/app.component.ts', '// async'); + + harness.useTarget('test', { + ...BASE_OPTIONS, + codeCoverage: true, + }); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + + await setTimeoutPromise(1000); + harness.expectFile(coveragePath).toExist(); + }); + + it('should exit with non-zero code when coverage is below threshold', async () => { + await harness.modifyFile('karma.conf.js', (content) => + content.replace( + 'coverageReporter: {', + `coverageReporter: { + check: { + global: { + statements: 100, + lines: 100, + branches: 100, + functions: 100 + } + }, + `, + ), + ); + + await harness.appendToFile( + 'src/app/app.component.ts', + ` + export function nonCovered(): boolean { + return true; + } + `, + ); + + harness.useTarget('test', { + ...BASE_OPTIONS, + codeCoverage: true, + }); + + await harness + .execute() + .pipe( + // In incremental mode, karma-coverage does not have the ability to mark a + // run as failed if code coverage does not pass. This is because it does + // the coverage asynchoronously and Karma does not await the promise + // returned by the plugin. + + // However the program must exit with non-zero exit code. + // This is a more common use case of coverage testing and must be supported. + last(), + tap((buildEvent) => expect(buildEvent.result?.success).toBeFalse()), + ) + .toPromise(); + }); + }); +}); diff --git a/packages/angular_devkit/build_angular/src/karma/tests/options/code-coverage_spec.ts b/packages/angular_devkit/build_angular/src/karma/tests/options/code-coverage_spec.ts index c36a9d21335c..7fd1abdb0ac9 100644 --- a/packages/angular_devkit/build_angular/src/karma/tests/options/code-coverage_spec.ts +++ b/packages/angular_devkit/build_angular/src/karma/tests/options/code-coverage_spec.ts @@ -109,52 +109,5 @@ describeBuilder(execute, KARMA_BUILDER_INFO, (harness) => { await setTimeoutPromise(1000); harness.expectFile(coveragePath).content.not.toContain('my-lib'); }); - - it('should exit with non-zero code when coverage is below threshold', async () => { - await harness.modifyFile('karma.conf.js', (content) => - content.replace( - 'coverageReporter: {', - `coverageReporter: { - check: { - global: { - statements: 100, - lines: 100, - branches: 100, - functions: 100 - } - }, - `, - ), - ); - - await harness.appendToFile( - 'src/app/app.component.ts', - ` - export function nonCovered(): boolean { - return true; - } - `, - ); - - harness.useTarget('test', { - ...BASE_OPTIONS, - codeCoverage: true, - }); - - await harness - .execute() - .pipe( - // In incremental mode, karma-coverage does not have the ability to mark a - // run as failed if code coverage does not pass. This is because it does - // the coverage asynchoronously and Karma does not await the promise - // returned by the plugin. - - // However the program must exit with non-zero exit code. - // This is a more common use case of coverage testing and must be supported. - last(), - tap((buildEvent) => expect(buildEvent.result?.success).toBeFalse()), - ) - .toPromise(); - }); }); });