Pete Bacon Darwin a107e9edc6 feat(ngcc): lock ngcc when processing (#34722)
Previously, it was possible for multiple instance of ngcc to be running
at the same time, but this is not supported and can cause confusing and
flakey errors at build time.

Now, only one instance of ngcc can run at a time. If a second instance
tries to execute it fails with an appropriate error message.

See https://github.com/angular/angular/issues/32431#issuecomment-571825781

PR Close #34722
2020-01-22 15:35:34 -08:00

207 lines
7.8 KiB
TypeScript

/**
* @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
*/
import * as process from 'process';
import {FileSystem, getFileSystem} from '../../../src/ngtsc/file_system';
import {runInEachFileSystem} from '../../../src/ngtsc/file_system/testing';
import {LockFile} from '../../src/execution/lock_file';
/**
* This class allows us to test the protected methods of LockFile directly,
* which are normally hidden as "protected".
*
* We also add logging in here to track what is being called and in what order.
*
* Finally this class stubs out the `exit()` method to prevent unit tests from exiting the process.
*/
class LockFileUnderTest extends LockFile {
log: string[] = [];
constructor(fs: FileSystem, private handleSignals = false) {
super(fs);
fs.ensureDir(fs.dirname(this.lockFilePath));
}
create() {
this.log.push('create()');
super.create();
}
remove() {
this.log.push('remove()');
super.remove();
}
addSignalHandlers() {
if (this.handleSignals) {
super.addSignalHandlers();
}
}
removeSignalHandlers() { super.removeSignalHandlers(); }
exit(code: number) { this.log.push(`exit(${code})`); }
}
runInEachFileSystem(() => {
describe('LockFile', () => {
describe('lock() - synchronous', () => {
it('should guard the `fn()` with calls to `create()` and `remove()`', () => {
const fs = getFileSystem();
const lockFile = new LockFileUnderTest(fs);
lockFile.lock(() => lockFile.log.push('fn()'));
expect(lockFile.log).toEqual(['create()', 'fn()', 'remove()']);
});
it('should guard the `fn()` with calls to `create()` and `remove()`, even if it throws',
() => {
let error: string = '';
const fs = getFileSystem();
const lockFile = new LockFileUnderTest(fs);
try {
lockFile.lock(() => {
lockFile.log.push('fn()');
throw new Error('ERROR');
});
} catch (e) {
error = e.message;
}
expect(error).toEqual('ERROR');
expect(lockFile.log).toEqual(['create()', 'fn()', 'remove()']);
});
it('should remove the lockfile if CTRL-C is triggered', () => {
const fs = getFileSystem();
const lockFile = new LockFileUnderTest(fs, /* handleSignals */ true);
lockFile.lock(() => {
lockFile.log.push('SIGINT');
process.emit('SIGINT', 'SIGINT');
});
// Since the test does not actually exit process, the `remove()` is called one more time.
expect(lockFile.log).toEqual(['create()', 'SIGINT', 'remove()', 'exit(1)', 'remove()']);
// Clean up the signal handlers. In practice this is not needed since the process would have
// been terminated already.
lockFile.removeSignalHandlers();
});
it('should remove the lockfile if terminal is closed', () => {
const fs = getFileSystem();
const lockFile = new LockFileUnderTest(fs, /* handleSignals */ true);
lockFile.lock(() => {
lockFile.log.push('SIGHUP');
process.emit('SIGHUP', 'SIGHUP');
});
// Since this does not actually exit process, the `remove()` is called one more time.
expect(lockFile.log).toEqual(['create()', 'SIGHUP', 'remove()', 'exit(1)', 'remove()']);
// Clean up the signal handlers. In practice this is not needed since the process would have
// been terminated already.
lockFile.removeSignalHandlers();
});
});
describe('lock() - asynchronous', () => {
it('should guard the `fn()` with calls to `create()` and `remove()`', async() => {
const fs = getFileSystem();
const lockFile = new LockFileUnderTest(fs);
await lockFile.lock(async() => {
lockFile.log.push('fn() - before');
// This promise forces node to do a tick in this function, ensuring that we are truly
// testing an async scenario.
await Promise.resolve();
lockFile.log.push('fn() - after');
});
expect(lockFile.log).toEqual(['create()', 'fn() - before', 'fn() - after', 'remove()']);
});
it('should guard the `fn()` with calls to `create()` and `remove()`, even if it throws',
async() => {
let error: string = '';
const fs = getFileSystem();
const lockFile = new LockFileUnderTest(fs);
lockFile.create = () => lockFile.log.push('create()');
lockFile.remove = () => lockFile.log.push('remove()');
try {
await lockFile.lock(async() => {
lockFile.log.push('fn()');
throw new Error('ERROR');
});
} catch (e) {
error = e.message;
}
expect(error).toEqual('ERROR');
expect(lockFile.log).toEqual(['create()', 'fn()', 'remove()']);
});
it('should remove the lockfile if CTRL-C is triggered', async() => {
const fs = getFileSystem();
const lockFile = new LockFileUnderTest(fs, /* handleSignals */ true);
await lockFile.lock(async() => {
lockFile.log.push('SIGINT');
process.emit('SIGINT', 'SIGINT');
});
// Since the test does not actually exit process, the `remove()` is called one more time.
expect(lockFile.log).toEqual(['create()', 'SIGINT', 'remove()', 'exit(1)', 'remove()']);
// Clean up the signal handlers. In practice this is not needed since the process would have
// been terminated already.
lockFile.removeSignalHandlers();
});
it('should remove the lockfile if terminal is closed', async() => {
const fs = getFileSystem();
const lockFile = new LockFileUnderTest(fs, /* handleSignals */ true);
await lockFile.lock(async() => {
lockFile.log.push('SIGHUP');
process.emit('SIGHUP', 'SIGHUP');
});
// Since this does not actually exit process, the `remove()` is called one more time.
expect(lockFile.log).toEqual(['create()', 'SIGHUP', 'remove()', 'exit(1)', 'remove()']);
// Clean up the signal handlers. In practice this is not needed since the process would have
// been terminated already.
lockFile.removeSignalHandlers();
});
});
describe('create()', () => {
it('should write a lock file to the file-system', () => {
const fs = getFileSystem();
const lockFile = new LockFileUnderTest(fs);
expect(fs.exists(lockFile.lockFilePath)).toBe(false);
lockFile.create();
expect(fs.exists(lockFile.lockFilePath)).toBe(true);
});
it('should error if a lock file already exists', () => {
const fs = getFileSystem();
const lockFile = new LockFileUnderTest(fs);
fs.writeFile(lockFile.lockFilePath, '188');
expect(() => lockFile.create())
.toThrowError(
`ngcc is already running at process with id 188.\n` +
`(If you are sure no ngcc process is running then you should delete the lockfile at ${lockFile.lockFilePath}.)`);
});
});
describe('remove()', () => {
it('should remove the lock file from the file-system', () => {
const fs = getFileSystem();
const lockFile = new LockFileUnderTest(fs);
fs.writeFile(lockFile.lockFilePath, '188');
lockFile.remove();
expect(fs.exists(lockFile.lockFilePath)).toBe(false);
});
it('should not error if the lock file does not exist', () => {
const fs = getFileSystem();
const lockFile = new LockFileUnderTest(fs);
expect(() => lockFile.remove()).not.toThrow();
});
});
});
});