Skip to content

Commit

Permalink
fix(@angular-devkit/build-angular): transform remapped sourcemap into…
Browse files Browse the repository at this point in the history
… 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 da1733c)
  • Loading branch information
alan-agius4 authored and clydin committed Oct 19, 2021
1 parent b93e63f commit 6173609
Show file tree
Hide file tree
Showing 3 changed files with 99 additions and 51 deletions.
14 changes: 10 additions & 4 deletions packages/angular_devkit/build_angular/src/babel/webpack-loader.ts
Expand Up @@ -185,10 +185,16 @@ export default custom<AngularCustomOptions>(() => {
// `@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;
Expand Down
@@ -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();
});
});
});
Expand Up @@ -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();
});
});
});

0 comments on commit 6173609

Please sign in to comment.