Pete Bacon Darwin 78a44768e0 fix(ngcc): ensure lockfile is removed when analyzeFn fails (#37739)
Previously an error thrown in the `analyzeFn` would cause
the ngcc process to exit immediately without removing the
lockfile, and potentially before the unlocker process had been
successfully spawned resulting in the lockfile being orphaned
and left behind.

Now we catch these errors and remove the lockfile as needed.

PR Close #37739
2020-06-29 10:29:11 -07:00

153 lines
6.0 KiB
TypeScript

/**
* @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
*/
/// <reference types="node" />
import {getFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system';
import * as cluster from 'cluster';
import {MockFileSystemNative, runInEachFileSystem} from '../../../../src/ngtsc/file_system/testing';
import {MockLogger} from '../../../../src/ngtsc/logging/testing';
import {ClusterExecutor} from '../../../src/execution/cluster/executor';
import {ClusterMaster} from '../../../src/execution/cluster/master';
import {AsyncLocker} from '../../../src/locking/async_locker';
import {FileWriter} from '../../../src/writing/file_writer';
import {PackageJsonUpdater} from '../../../src/writing/package_json_updater';
import {MockLockFile} from '../../helpers/mock_lock_file';
import {mockProperty} from '../../helpers/spy_utils';
runInEachFileSystem(() => {
describe('ClusterExecutor', () => {
const runAsClusterMaster = mockProperty(cluster, 'isMaster');
let masterRunSpy: jasmine.Spy;
let mockLogger: MockLogger;
let lockFileLog: string[];
let mockLockFile: MockLockFile;
let locker: AsyncLocker;
let executor: ClusterExecutor;
let createTaskCompletedCallback: jasmine.Spy;
beforeEach(() => {
masterRunSpy = spyOn(ClusterMaster.prototype, 'run')
.and.returnValue(Promise.resolve('ClusterMaster#run()' as any));
createTaskCompletedCallback = jasmine.createSpy('createTaskCompletedCallback');
mockLogger = new MockLogger();
lockFileLog = [];
mockLockFile = new MockLockFile(new MockFileSystemNative(), lockFileLog);
locker = new AsyncLocker(mockLockFile, mockLogger, 200, 2);
executor = new ClusterExecutor(
42, getFileSystem(), mockLogger, null as unknown as FileWriter,
null as unknown as PackageJsonUpdater, locker, createTaskCompletedCallback);
});
describe('execute()', () => {
beforeEach(() => runAsClusterMaster(true));
it('should log debug info about the executor', async () => {
const anyFn: () => any = () => undefined;
await executor.execute(anyFn, anyFn);
expect(mockLogger.logs.debug).toEqual([
['Running ngcc on ClusterExecutor (using 42 worker processes).'],
]);
});
it('should delegate to `ClusterMaster#run()`', async () => {
const analyzeEntryPointsSpy = jasmine.createSpy('analyzeEntryPoints');
const createCompilerFnSpy = jasmine.createSpy('createCompilerFn');
expect(await executor.execute(analyzeEntryPointsSpy, createCompilerFnSpy))
.toBe('ClusterMaster#run()' as any);
expect(masterRunSpy).toHaveBeenCalledWith();
expect(analyzeEntryPointsSpy).toHaveBeenCalledWith();
expect(createCompilerFnSpy).not.toHaveBeenCalled();
});
it('should call LockFile.write() and LockFile.remove() if master runner completes successfully',
async () => {
const anyFn: () => any = () => undefined;
await executor.execute(anyFn, anyFn);
expect(lockFileLog).toEqual(['write()', 'remove()']);
});
it('should call LockFile.write() and LockFile.remove() if analyzeFn fails', async () => {
const analyzeEntryPointsSpy =
jasmine.createSpy('analyzeEntryPoints').and.throwError('analyze error');
const createCompilerFnSpy = jasmine.createSpy('createCompilerFn');
let error = '';
try {
await executor.execute(analyzeEntryPointsSpy, createCompilerFnSpy);
} catch (e) {
error = e.message;
}
expect(analyzeEntryPointsSpy).toHaveBeenCalledWith();
expect(createCompilerFnSpy).not.toHaveBeenCalled();
expect(error).toEqual('analyze error');
expect(lockFileLog).toEqual(['write()', 'remove()']);
});
it('should call LockFile.write() and LockFile.remove() if master runner fails', async () => {
const anyFn: () => any = () => undefined;
masterRunSpy.and.returnValue(Promise.reject(new Error('master runner error')));
let error = '';
try {
await executor.execute(anyFn, anyFn);
} catch (e) {
error = e.message;
}
expect(error).toEqual('master runner error');
expect(lockFileLog).toEqual(['write()', 'remove()']);
});
it('should not call master runner if LockFile.write() fails', async () => {
const anyFn: () => any = () => undefined;
spyOn(mockLockFile, 'write').and.callFake(() => {
lockFileLog.push('write()');
throw new Error('LockFile.write() error');
});
executor = new ClusterExecutor(
42, getFileSystem(), mockLogger, null as unknown as FileWriter,
null as unknown as PackageJsonUpdater, locker, createTaskCompletedCallback);
let error = '';
try {
await executor.execute(anyFn, anyFn);
} catch (e) {
error = e.message;
}
expect(error).toEqual('LockFile.write() error');
expect(masterRunSpy).not.toHaveBeenCalled();
});
it('should fail if LockFile.remove() fails', async () => {
const anyFn: () => any = () => undefined;
spyOn(mockLockFile, 'remove').and.callFake(() => {
lockFileLog.push('remove()');
throw new Error('LockFile.remove() error');
});
executor = new ClusterExecutor(
42, getFileSystem(), mockLogger, null as unknown as FileWriter,
null as unknown as PackageJsonUpdater, locker, createTaskCompletedCallback);
let error = '';
try {
await executor.execute(anyFn, anyFn);
} catch (e) {
error = e.message;
}
expect(error).toEqual('LockFile.remove() error');
expect(lockFileLog).toEqual(['write()', 'remove()']);
expect(masterRunSpy).toHaveBeenCalled();
});
});
});
});