perf(ngcc): process tasks in parallel in async mode (#32427)
`ngcc` supports both synchronous and asynchronous execution. The default mode when using `ngcc` programmatically (which is how `@angular/cli` is using it) is synchronous. When running `ngcc` from the command line (i.e. via the `ivy-ngcc` script), it runs in async mode. Previously, the work would be executed in the same way in both modes. This commit improves the performance of `ngcc` in async mode by processing tasks in parallel on multiple processes. It uses the Node.js built-in [`cluster` module](https://nodejs.org/api/cluster.html) to launch a cluster of Node.js processes and take advantage of multi-core systems. Preliminary comparisons indicate a 1.8x to 2.6x speed improvement when processing the angular.io app (apparently depending on the OS, number of available cores, system load, etc.). Further investigation is needed to better understand these numbers and identify potential areas of improvement. Inspired by/Based on @alxhub's prototype: alxhub/angular@cb631bdb1 Original design doc: https://hackmd.io/uYG9CJrFQZ-6FtKqpnYJAA?view Jira issue: [FW-1460](https://angular-team.atlassian.net/browse/FW-1460) PR Close #32427
This commit is contained in:

committed by
Matias Niemelä

parent
f4e4bb2085
commit
e36e6c85ef
@ -5,6 +5,9 @@
|
||||
* 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 {DepGraph} from 'dependency-graph';
|
||||
|
||||
import {FileSystem, absoluteFrom, getFileSystem} from '../../../src/ngtsc/file_system';
|
||||
import {runInEachFileSystem} from '../../../src/ngtsc/file_system/testing';
|
||||
import {DependencyResolver, SortedEntryPointsInfo} from '../../src/dependencies/dependency_resolver';
|
||||
@ -13,6 +16,7 @@ import {ModuleResolver} from '../../src/dependencies/module_resolver';
|
||||
import {EntryPoint} from '../../src/packages/entry_point';
|
||||
import {MockLogger} from '../helpers/mock_logger';
|
||||
|
||||
|
||||
interface DepMap {
|
||||
[path: string]: {resolved: string[], missing: string[]};
|
||||
}
|
||||
@ -162,6 +166,15 @@ runInEachFileSystem(() => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return the computed dependency graph', () => {
|
||||
spyOn(host, 'findDependencies').and.callFake(createFakeComputeDependencies(dependencies));
|
||||
const result = resolver.sortEntryPointsByDependency([fifth, first, fourth, second, third]);
|
||||
|
||||
expect(result.graph).toEqual(jasmine.any(DepGraph));
|
||||
expect(result.graph.size()).toBe(5);
|
||||
expect(result.graph.dependenciesOf(third.path)).toEqual([fifth.path, fourth.path]);
|
||||
});
|
||||
|
||||
it('should only return dependencies of the target, if provided', () => {
|
||||
spyOn(host, 'findDependencies').and.callFake(createFakeComputeDependencies(dependencies));
|
||||
const entryPoints = [fifth, first, fourth, second, third];
|
||||
|
@ -0,0 +1,91 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. 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
|
||||
*/
|
||||
|
||||
/// <reference types="node" />
|
||||
|
||||
import * as cluster from 'cluster';
|
||||
|
||||
import {ClusterExecutor} from '../../../src/execution/cluster/executor';
|
||||
import {ClusterMaster} from '../../../src/execution/cluster/master';
|
||||
import {ClusterWorker} from '../../../src/execution/cluster/worker';
|
||||
import {PackageJsonUpdater} from '../../../src/writing/package_json_updater';
|
||||
import {MockLogger} from '../../helpers/mock_logger';
|
||||
import {mockProperty} from '../../helpers/spy_utils';
|
||||
|
||||
|
||||
describe('ClusterExecutor', () => {
|
||||
const runAsClusterMaster = mockProperty(cluster, 'isMaster');
|
||||
let masterRunSpy: jasmine.Spy;
|
||||
let workerRunSpy: jasmine.Spy;
|
||||
let mockLogger: MockLogger;
|
||||
let executor: ClusterExecutor;
|
||||
|
||||
beforeEach(() => {
|
||||
masterRunSpy = spyOn(ClusterMaster.prototype, 'run');
|
||||
workerRunSpy = spyOn(ClusterWorker.prototype, 'run');
|
||||
|
||||
mockLogger = new MockLogger();
|
||||
executor = new ClusterExecutor(42, mockLogger, null as unknown as PackageJsonUpdater);
|
||||
});
|
||||
|
||||
describe('execute()', () => {
|
||||
describe('(on cluster master)', () => {
|
||||
beforeEach(() => runAsClusterMaster(true));
|
||||
|
||||
it('should log debug info about the executor', () => {
|
||||
const anyFn: () => any = () => undefined;
|
||||
executor.execute(anyFn, anyFn);
|
||||
|
||||
expect(mockLogger.logs.debug).toEqual([
|
||||
['Running ngcc on ClusterExecutor (using 42 worker processes).'],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should delegate to `ClusterMaster#run()`', async() => {
|
||||
masterRunSpy.and.returnValue('CusterMaster#run()');
|
||||
const analyzeEntryPointsSpy = jasmine.createSpy('analyzeEntryPoints');
|
||||
const createCompilerFnSpy = jasmine.createSpy('createCompilerFn');
|
||||
|
||||
expect(await executor.execute(analyzeEntryPointsSpy, createCompilerFnSpy))
|
||||
.toBe('CusterMaster#run()' as any);
|
||||
|
||||
expect(masterRunSpy).toHaveBeenCalledWith();
|
||||
expect(workerRunSpy).not.toHaveBeenCalled();
|
||||
|
||||
expect(analyzeEntryPointsSpy).toHaveBeenCalledWith();
|
||||
expect(createCompilerFnSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('(on cluster worker)', () => {
|
||||
beforeEach(() => runAsClusterMaster(false));
|
||||
|
||||
it('should not log debug info about the executor', () => {
|
||||
const anyFn: () => any = () => undefined;
|
||||
executor.execute(anyFn, anyFn);
|
||||
|
||||
expect(mockLogger.logs.debug).toEqual([]);
|
||||
});
|
||||
|
||||
it('should delegate to `ClusterWorker#run()`', async() => {
|
||||
workerRunSpy.and.returnValue('CusterWorker#run()');
|
||||
const analyzeEntryPointsSpy = jasmine.createSpy('analyzeEntryPoints');
|
||||
const createCompilerFnSpy = jasmine.createSpy('createCompilerFn');
|
||||
|
||||
expect(await executor.execute(analyzeEntryPointsSpy, createCompilerFnSpy))
|
||||
.toBe('CusterWorker#run()' as any);
|
||||
|
||||
expect(masterRunSpy).not.toHaveBeenCalledWith();
|
||||
expect(workerRunSpy).toHaveBeenCalled();
|
||||
|
||||
expect(analyzeEntryPointsSpy).not.toHaveBeenCalled();
|
||||
expect(createCompilerFnSpy).toHaveBeenCalledWith(jasmine.any(Function));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,198 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. 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
|
||||
*/
|
||||
|
||||
/// <reference types="node" />
|
||||
|
||||
import * as cluster from 'cluster';
|
||||
|
||||
import {absoluteFrom as _} from '../../../../src/ngtsc/file_system';
|
||||
import {runInEachFileSystem} from '../../../../src/ngtsc/file_system/testing';
|
||||
import {ClusterPackageJsonUpdater} from '../../../src/execution/cluster/package_json_updater';
|
||||
import {JsonObject} from '../../../src/packages/entry_point';
|
||||
import {PackageJsonUpdate, PackageJsonUpdater} from '../../../src/writing/package_json_updater';
|
||||
import {mockProperty} from '../../helpers/spy_utils';
|
||||
|
||||
|
||||
runInEachFileSystem(() => {
|
||||
describe('ClusterPackageJsonUpdater', () => {
|
||||
const runAsClusterMaster = mockProperty(cluster, 'isMaster');
|
||||
const mockProcessSend = mockProperty(process, 'send');
|
||||
let processSendSpy: jasmine.Spy;
|
||||
let delegate: PackageJsonUpdater;
|
||||
let updater: ClusterPackageJsonUpdater;
|
||||
|
||||
beforeEach(() => {
|
||||
processSendSpy = jasmine.createSpy('process.send');
|
||||
mockProcessSend(processSendSpy);
|
||||
|
||||
delegate = new MockPackageJsonUpdater();
|
||||
updater = new ClusterPackageJsonUpdater(delegate);
|
||||
});
|
||||
|
||||
describe('createUpdate()', () => {
|
||||
[true, false].forEach(
|
||||
isMaster => describe(`(on cluster ${isMaster ? 'master' : 'worker'})`, () => {
|
||||
beforeEach(() => runAsClusterMaster(isMaster));
|
||||
|
||||
it('should return a `PackageJsonUpdate` instance',
|
||||
() => { expect(updater.createUpdate()).toEqual(jasmine.any(PackageJsonUpdate)); });
|
||||
|
||||
it('should wire up the `PackageJsonUpdate` with its `writeChanges()` method', () => {
|
||||
const writeChangesSpy = spyOn(updater, 'writeChanges');
|
||||
const jsonPath = _('/foo/package.json');
|
||||
const update = updater.createUpdate();
|
||||
|
||||
update.addChange(['foo'], 'updated');
|
||||
update.writeChanges(jsonPath);
|
||||
|
||||
expect(writeChangesSpy)
|
||||
.toHaveBeenCalledWith([[['foo'], 'updated']], jsonPath, undefined);
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
describe('writeChanges()', () => {
|
||||
describe('(on cluster master)', () => {
|
||||
beforeEach(() => runAsClusterMaster(true));
|
||||
afterEach(() => expect(processSendSpy).not.toHaveBeenCalled());
|
||||
|
||||
it('should forward the call to the delegate `PackageJsonUpdater`', () => {
|
||||
const jsonPath = _('/foo/package.json');
|
||||
const parsedJson = {foo: 'bar'};
|
||||
|
||||
updater.createUpdate().addChange(['foo'], 'updated').writeChanges(jsonPath, parsedJson);
|
||||
|
||||
expect(delegate.writeChanges)
|
||||
.toHaveBeenCalledWith([[['foo'], 'updated']], jsonPath, parsedJson);
|
||||
});
|
||||
|
||||
it('should throw, if trying to re-apply an already applied update', () => {
|
||||
const update = updater.createUpdate().addChange(['foo'], 'updated');
|
||||
|
||||
expect(() => update.writeChanges(_('/foo/package.json'))).not.toThrow();
|
||||
expect(() => update.writeChanges(_('/foo/package.json')))
|
||||
.toThrowError('Trying to apply a `PackageJsonUpdate` that has already been applied.');
|
||||
expect(() => update.writeChanges(_('/bar/package.json')))
|
||||
.toThrowError('Trying to apply a `PackageJsonUpdate` that has already been applied.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('(on cluster worker)', () => {
|
||||
beforeEach(() => runAsClusterMaster(false));
|
||||
|
||||
it('should send an `update-package-json` message to the master process', () => {
|
||||
const jsonPath = _('/foo/package.json');
|
||||
|
||||
const writeToProp = (propPath: string[], parsed?: JsonObject) =>
|
||||
updater.createUpdate().addChange(propPath, 'updated').writeChanges(jsonPath, parsed);
|
||||
|
||||
writeToProp(['foo']);
|
||||
expect(processSendSpy).toHaveBeenCalledWith({
|
||||
type: 'update-package-json',
|
||||
packageJsonPath: jsonPath,
|
||||
changes: [[['foo'], 'updated']],
|
||||
});
|
||||
|
||||
writeToProp(['bar', 'baz', 'qux'], {});
|
||||
expect(processSendSpy).toHaveBeenCalledWith({
|
||||
type: 'update-package-json',
|
||||
packageJsonPath: jsonPath,
|
||||
changes: [[['bar', 'baz', 'qux'], 'updated']],
|
||||
});
|
||||
});
|
||||
|
||||
it('should update an in-memory representation (if provided)', () => {
|
||||
const jsonPath = _('/foo/package.json');
|
||||
const parsedJson: JsonObject = {
|
||||
foo: true,
|
||||
bar: {baz: 'OK'},
|
||||
};
|
||||
|
||||
const update =
|
||||
updater.createUpdate().addChange(['foo'], false).addChange(['bar', 'baz'], 42);
|
||||
|
||||
// Not updated yet.
|
||||
expect(parsedJson).toEqual({
|
||||
foo: true,
|
||||
bar: {baz: 'OK'},
|
||||
});
|
||||
|
||||
update.writeChanges(jsonPath, parsedJson);
|
||||
|
||||
// Updated now.
|
||||
expect(parsedJson).toEqual({
|
||||
foo: false,
|
||||
bar: {baz: 42},
|
||||
});
|
||||
});
|
||||
|
||||
it('should create any missing ancestor objects', () => {
|
||||
const jsonPath = _('/foo/package.json');
|
||||
const parsedJson: JsonObject = {foo: {}};
|
||||
|
||||
updater.createUpdate()
|
||||
.addChange(['foo', 'bar', 'baz', 'qux'], 'updated')
|
||||
.writeChanges(jsonPath, parsedJson);
|
||||
|
||||
expect(parsedJson).toEqual({
|
||||
foo: {
|
||||
bar: {
|
||||
baz: {
|
||||
qux: 'updated',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw, if a property-path is empty', () => {
|
||||
const jsonPath = _('/foo/package.json');
|
||||
|
||||
expect(() => updater.createUpdate().addChange([], 'missing').writeChanges(jsonPath, {}))
|
||||
.toThrowError(`Missing property path for writing value to '${jsonPath}'.`);
|
||||
});
|
||||
|
||||
it('should throw, if a property-path points to a non-object intermediate value', () => {
|
||||
const jsonPath = _('/foo/package.json');
|
||||
const parsedJson = {foo: null, bar: 42, baz: {qux: []}};
|
||||
|
||||
const writeToProp = (propPath: string[], parsed?: JsonObject) =>
|
||||
updater.createUpdate().addChange(propPath, 'updated').writeChanges(jsonPath, parsed);
|
||||
|
||||
expect(() => writeToProp(['foo', 'child'], parsedJson))
|
||||
.toThrowError('Property path \'foo.child\' does not point to an object.');
|
||||
expect(() => writeToProp(['bar', 'child'], parsedJson))
|
||||
.toThrowError('Property path \'bar.child\' does not point to an object.');
|
||||
expect(() => writeToProp(['baz', 'qux', 'child'], parsedJson))
|
||||
.toThrowError('Property path \'baz.qux.child\' does not point to an object.');
|
||||
|
||||
// It should not throw, if no parsed representation is provided.
|
||||
// (The error will still be thrown on the master process, but that is out of scope for
|
||||
// this test.)
|
||||
expect(() => writeToProp(['foo', 'child'])).not.toThrow();
|
||||
});
|
||||
|
||||
it('should throw, if trying to re-apply an already applied update', () => {
|
||||
const update = updater.createUpdate().addChange(['foo'], 'updated');
|
||||
|
||||
expect(() => update.writeChanges(_('/foo/package.json'))).not.toThrow();
|
||||
expect(() => update.writeChanges(_('/foo/package.json')))
|
||||
.toThrowError('Trying to apply a `PackageJsonUpdate` that has already been applied.');
|
||||
expect(() => update.writeChanges(_('/bar/package.json')))
|
||||
.toThrowError('Trying to apply a `PackageJsonUpdate` that has already been applied.');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Helpers
|
||||
class MockPackageJsonUpdater implements PackageJsonUpdater {
|
||||
createUpdate = jasmine.createSpy('MockPackageJsonUpdater#createUpdate');
|
||||
writeChanges = jasmine.createSpy('MockPackageJsonUpdater#writeChanges');
|
||||
}
|
||||
});
|
||||
});
|
144
packages/compiler-cli/ngcc/test/execution/cluster/worker_spec.ts
Normal file
144
packages/compiler-cli/ngcc/test/execution/cluster/worker_spec.ts
Normal file
@ -0,0 +1,144 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. 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
|
||||
*/
|
||||
|
||||
/// <reference types="node" />
|
||||
|
||||
import * as cluster from 'cluster';
|
||||
import {EventEmitter} from 'events';
|
||||
|
||||
import {Task, TaskCompletedCallback, TaskProcessingOutcome} from '../../../src/execution/api';
|
||||
import {ClusterWorker} from '../../../src/execution/cluster/worker';
|
||||
import {mockProperty} from '../../helpers/spy_utils';
|
||||
|
||||
|
||||
describe('ClusterWorker', () => {
|
||||
const runAsClusterMaster = mockProperty(cluster, 'isMaster');
|
||||
const mockProcessSend = mockProperty(process, 'send');
|
||||
let processSendSpy: jasmine.Spy;
|
||||
let compileFnSpy: jasmine.Spy;
|
||||
let createCompileFnSpy: jasmine.Spy;
|
||||
|
||||
beforeEach(() => {
|
||||
compileFnSpy = jasmine.createSpy('compileFn');
|
||||
createCompileFnSpy = jasmine.createSpy('createCompileFn').and.returnValue(compileFnSpy);
|
||||
|
||||
processSendSpy = jasmine.createSpy('process.send');
|
||||
mockProcessSend(processSendSpy);
|
||||
});
|
||||
|
||||
describe('constructor()', () => {
|
||||
describe('(on cluster master)', () => {
|
||||
beforeEach(() => runAsClusterMaster(true));
|
||||
|
||||
it('should throw an error', () => {
|
||||
expect(() => new ClusterWorker(createCompileFnSpy))
|
||||
.toThrowError('Tried to instantiate `ClusterWorker` on the master process.');
|
||||
expect(createCompileFnSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('(on cluster worker)', () => {
|
||||
beforeEach(() => runAsClusterMaster(false));
|
||||
|
||||
it('should create the `compileFn()`', () => {
|
||||
new ClusterWorker(createCompileFnSpy);
|
||||
expect(createCompileFnSpy).toHaveBeenCalledWith(jasmine.any(Function));
|
||||
});
|
||||
|
||||
it('should set up `compileFn()` to send a `task-completed` message to master', () => {
|
||||
new ClusterWorker(createCompileFnSpy);
|
||||
const onTaskCompleted: TaskCompletedCallback = createCompileFnSpy.calls.argsFor(0)[0];
|
||||
|
||||
onTaskCompleted(null as any, TaskProcessingOutcome.AlreadyProcessed);
|
||||
expect(processSendSpy).toHaveBeenCalledTimes(1);
|
||||
expect(processSendSpy).toHaveBeenCalledWith({
|
||||
type: 'task-completed',
|
||||
outcome: TaskProcessingOutcome.AlreadyProcessed,
|
||||
});
|
||||
|
||||
onTaskCompleted(null as any, TaskProcessingOutcome.Processed);
|
||||
expect(processSendSpy).toHaveBeenCalledTimes(2);
|
||||
expect(processSendSpy).toHaveBeenCalledWith({
|
||||
type: 'task-completed',
|
||||
outcome: TaskProcessingOutcome.Processed,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('run()', () => {
|
||||
describe(
|
||||
'(on cluster master)',
|
||||
() => {/* No tests needed, becasue the constructor would have thrown. */});
|
||||
|
||||
describe('(on cluster worker)', () => {
|
||||
// The `cluster.worker` property is normally `undefined` on the master process and set to the
|
||||
// current `cluster.Worker` on worker processes.
|
||||
const mockClusterWorker = mockProperty(cluster, 'worker');
|
||||
let worker: ClusterWorker;
|
||||
|
||||
beforeEach(() => {
|
||||
runAsClusterMaster(false);
|
||||
mockClusterWorker(Object.assign(new EventEmitter(), {id: 42}) as cluster.Worker);
|
||||
|
||||
worker = new ClusterWorker(createCompileFnSpy);
|
||||
});
|
||||
|
||||
it('should return a promise (that is never resolved)', done => {
|
||||
const promise = worker.run();
|
||||
|
||||
expect(promise).toEqual(jasmine.any(Promise));
|
||||
|
||||
promise.then(
|
||||
() => done.fail('Expected promise not to resolve'),
|
||||
() => done.fail('Expected promise not to reject'));
|
||||
|
||||
// We can't wait forever to verify that the promise is not resolved, but at least verify
|
||||
// that it is not resolved immediately.
|
||||
setTimeout(done, 100);
|
||||
});
|
||||
|
||||
it('should handle `process-task` messages', () => {
|
||||
const mockTask = { foo: 'bar' } as unknown as Task;
|
||||
|
||||
worker.run();
|
||||
cluster.worker.emit('message', {type: 'process-task', task: mockTask});
|
||||
|
||||
expect(compileFnSpy).toHaveBeenCalledWith(mockTask);
|
||||
expect(processSendSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should send errors during task processing back to the master process', () => {
|
||||
let err: string|Error;
|
||||
compileFnSpy.and.callFake(() => { throw err; });
|
||||
|
||||
worker.run();
|
||||
|
||||
err = 'Error string.';
|
||||
cluster.worker.emit('message', {type: 'process-task', task: {} as Task});
|
||||
expect(processSendSpy).toHaveBeenCalledWith({type: 'error', error: err});
|
||||
|
||||
err = new Error('Error object.');
|
||||
cluster.worker.emit('message', {type: 'process-task', task: {} as Task});
|
||||
expect(processSendSpy).toHaveBeenCalledWith({type: 'error', error: err.stack});
|
||||
});
|
||||
|
||||
it('should throw, when an unknown message type is received', () => {
|
||||
worker.run();
|
||||
cluster.worker.emit('message', {type: 'unknown', foo: 'bar'});
|
||||
|
||||
expect(compileFnSpy).not.toHaveBeenCalled();
|
||||
expect(processSendSpy).toHaveBeenCalledWith({
|
||||
type: 'error',
|
||||
error: jasmine.stringMatching(
|
||||
'Error: Invalid message received on worker #42: {"type":"unknown","foo":"bar"}'),
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
112
packages/compiler-cli/ngcc/test/helpers/spy_utils.ts
Normal file
112
packages/compiler-cli/ngcc/test/helpers/spy_utils.ts
Normal file
@ -0,0 +1,112 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. 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
|
||||
*/
|
||||
|
||||
/** An object with helpers for mocking/spying on an object's property. */
|
||||
export interface IPropertySpyHelpers<T, P extends keyof T> {
|
||||
/**
|
||||
* A `jasmine.Spy` for `get` operations on the property (i.e. reading the current property value).
|
||||
* (This is useful in case one needs to make assertions against property reads.)
|
||||
*/
|
||||
getSpy: jasmine.Spy;
|
||||
|
||||
/**
|
||||
* A `jasmine.Spy` for `set` operations on the property (i.e. setting a new property value).
|
||||
* (This is useful in case one needs to make assertions against property writes.)
|
||||
*/
|
||||
setSpy: jasmine.Spy;
|
||||
|
||||
/** Install the getter/setter spies for the property. */
|
||||
installSpies(): void;
|
||||
|
||||
/**
|
||||
* Uninstall the property spies and restore the original value (from before installing the
|
||||
* spies), including the property descriptor.
|
||||
*/
|
||||
uninstallSpies(): void;
|
||||
|
||||
/** Update the current value of the mocked property. */
|
||||
setMockValue(value: T[P]): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up mocking an object's property (using spies) and return a function for updating the mocked
|
||||
* property's value during tests.
|
||||
*
|
||||
* This is, essentially, a wrapper around `spyProperty()` which additionally takes care of
|
||||
* installing the spies before each test (via `beforeEach()`) and uninstalling them after each test
|
||||
* (via `afterEach()`).
|
||||
*
|
||||
* Example usage:
|
||||
*
|
||||
* ```ts
|
||||
* describe('something', () => {
|
||||
* // Assuming `window.foo` is an object...
|
||||
* const mockWindowFooBar = mockProperty(window.foo, 'bar');
|
||||
*
|
||||
* it('should do this', () => {
|
||||
* mockWindowFooBar('baz');
|
||||
* expect(window.foo.bar).toBe('baz');
|
||||
*
|
||||
* mockWindowFooBar('qux');
|
||||
* expect(window.foo.bar).toBe('qux');
|
||||
* });
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @param ctx The object whose property needs to be spied on.
|
||||
* @param prop The name of the property to spy on.
|
||||
*
|
||||
* @return A function for updating the current value of the mocked property.
|
||||
*/
|
||||
export const mockProperty =
|
||||
<T, P extends keyof T>(ctx: T, prop: P): IPropertySpyHelpers<T, P>['setMockValue'] => {
|
||||
const {setMockValue, installSpies, uninstallSpies} = spyProperty(ctx, prop);
|
||||
|
||||
beforeEach(installSpies);
|
||||
afterEach(uninstallSpies);
|
||||
|
||||
return setMockValue;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return utility functions to help mock and spy on an object's property.
|
||||
*
|
||||
* It supports spying on properties that are either defined on the object instance itself or on its
|
||||
* prototype. It also supports spying on non-writable properties (as long as they are configurable).
|
||||
*
|
||||
* NOTE: Unlike `jasmine`'s spying utilities, spies are not automatically installed/uninstalled, so
|
||||
* the caller is responsible for manually taking care of that (by calling
|
||||
* `installSpies()`/`uninstallSpies()` as necessary).
|
||||
*
|
||||
* @param ctx The object whose property needs to be spied on.
|
||||
* @param prop The name of the property to spy on.
|
||||
*
|
||||
* @return An object with helpers for mocking/spying on an object's property.
|
||||
*/
|
||||
export const spyProperty = <T, P extends keyof T>(ctx: T, prop: P): IPropertySpyHelpers<T, P> => {
|
||||
const originalDescriptor = Object.getOwnPropertyDescriptor(ctx, prop);
|
||||
|
||||
let value = ctx[prop];
|
||||
const setMockValue = (mockValue: typeof value) => value = mockValue;
|
||||
const setSpy = jasmine.createSpy(`set ${prop}`).and.callFake(setMockValue);
|
||||
const getSpy = jasmine.createSpy(`get ${prop}`).and.callFake(() => value);
|
||||
|
||||
const installSpies = () => {
|
||||
value = ctx[prop];
|
||||
Object.defineProperty(ctx, prop, {
|
||||
configurable: true,
|
||||
enumerable: originalDescriptor ? originalDescriptor.enumerable : true,
|
||||
get: getSpy,
|
||||
set: setSpy,
|
||||
});
|
||||
};
|
||||
const uninstallSpies = () =>
|
||||
originalDescriptor ? Object.defineProperty(ctx, prop, originalDescriptor) : delete ctx[prop];
|
||||
|
||||
return {installSpies, uninstallSpies, setMockValue, getSpy, setSpy};
|
||||
};
|
@ -5,6 +5,11 @@
|
||||
* 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
|
||||
*/
|
||||
|
||||
/// <reference types="node" />
|
||||
|
||||
import * as os from 'os';
|
||||
|
||||
import {AbsoluteFsPath, FileSystem, absoluteFrom, getFileSystem, join} from '../../../src/ngtsc/file_system';
|
||||
import {Folder, MockFileSystem, TestFile, runInEachFileSystem} from '../../../src/ngtsc/file_system/testing';
|
||||
import {loadStandardTestFiles, loadTestFiles} from '../../../test/helpers';
|
||||
@ -15,6 +20,7 @@ import {Transformer} from '../../src/packages/transformer';
|
||||
import {DirectPackageJsonUpdater, PackageJsonUpdater} from '../../src/writing/package_json_updater';
|
||||
import {MockLogger} from '../helpers/mock_logger';
|
||||
|
||||
|
||||
const testFiles = loadStandardTestFiles({fakeCore: false, rxjs: true});
|
||||
|
||||
runInEachFileSystem(() => {
|
||||
@ -28,6 +34,9 @@ runInEachFileSystem(() => {
|
||||
fs = getFileSystem();
|
||||
pkgJsonUpdater = new DirectPackageJsonUpdater(fs);
|
||||
initMockFileSystem(fs, testFiles);
|
||||
|
||||
// Force single-process execution in unit tests by mocking available CPUs to 1.
|
||||
spyOn(os, 'cpus').and.returnValue([{model: 'Mock CPU'}]);
|
||||
});
|
||||
|
||||
it('should run ngcc without errors for esm2015', () => {
|
||||
@ -402,7 +411,6 @@ runInEachFileSystem(() => {
|
||||
propertiesToConsider: ['module', 'fesm5', 'esm5'],
|
||||
compileAllFormats: false,
|
||||
logger: new MockLogger(),
|
||||
|
||||
});
|
||||
// * In the Angular packages fesm5 and module have the same underlying format,
|
||||
// so both are marked as compiled.
|
||||
|
Reference in New Issue
Block a user