refactor(ngcc): separate `(Async/Sync)Locker` and `LockFile` (#35861)
The previous implementation mixed up the management of locking a piece of code (both sync and async) with the management of writing and removing the lockFile that is used as the flag for which process has locked the code. This change splits these two concepts up. Apart from avoiding the awkward base class it allows the `LockFile` implementation to be replaced cleanly. PR Close #35861
This commit is contained in:
parent
bdaab4184d
commit
94fa140888
|
@ -13,7 +13,7 @@ import * as cluster from 'cluster';
|
||||||
import {Logger} from '../../logging/logger';
|
import {Logger} from '../../logging/logger';
|
||||||
import {PackageJsonUpdater} from '../../writing/package_json_updater';
|
import {PackageJsonUpdater} from '../../writing/package_json_updater';
|
||||||
import {AnalyzeEntryPointsFn, CreateCompileFn, Executor} from '../api';
|
import {AnalyzeEntryPointsFn, CreateCompileFn, Executor} from '../api';
|
||||||
import {LockFileAsync} from '../lock_file';
|
import {AsyncLocker} from '../lock_file';
|
||||||
|
|
||||||
import {ClusterMaster} from './master';
|
import {ClusterMaster} from './master';
|
||||||
import {ClusterWorker} from './worker';
|
import {ClusterWorker} from './worker';
|
||||||
|
@ -26,7 +26,7 @@ import {ClusterWorker} from './worker';
|
||||||
export class ClusterExecutor implements Executor {
|
export class ClusterExecutor implements Executor {
|
||||||
constructor(
|
constructor(
|
||||||
private workerCount: number, private logger: Logger,
|
private workerCount: number, private logger: Logger,
|
||||||
private pkgJsonUpdater: PackageJsonUpdater, private lockFile: LockFileAsync) {}
|
private pkgJsonUpdater: PackageJsonUpdater, private lockFile: AsyncLocker) {}
|
||||||
|
|
||||||
async execute(analyzeEntryPoints: AnalyzeEntryPointsFn, createCompileFn: CreateCompileFn):
|
async execute(analyzeEntryPoints: AnalyzeEntryPointsFn, createCompileFn: CreateCompileFn):
|
||||||
Promise<void> {
|
Promise<void> {
|
||||||
|
|
|
@ -7,59 +7,80 @@
|
||||||
*/
|
*/
|
||||||
import * as process from 'process';
|
import * as process from 'process';
|
||||||
|
|
||||||
import {CachedFileSystem, FileSystem} from '../../../src/ngtsc/file_system';
|
import {AbsoluteFsPath, CachedFileSystem, FileSystem} from '../../../src/ngtsc/file_system';
|
||||||
import {Logger} from '../logging/logger';
|
import {Logger} from '../logging/logger';
|
||||||
|
|
||||||
export abstract class LockFileBase {
|
let _lockFilePath: AbsoluteFsPath;
|
||||||
lockFilePath =
|
export function getLockFilePath(fs: FileSystem) {
|
||||||
this.fs.resolve(require.resolve('@angular/compiler-cli/ngcc'), '../__ngcc_lock_file__');
|
if (!_lockFilePath) {
|
||||||
|
_lockFilePath =
|
||||||
|
fs.resolve(require.resolve('@angular/compiler-cli/ngcc'), '../__ngcc_lock_file__');
|
||||||
|
}
|
||||||
|
return _lockFilePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LockFile {
|
||||||
|
path: AbsoluteFsPath;
|
||||||
|
/**
|
||||||
|
* Write a lock file to disk containing the PID of the current process.
|
||||||
|
*/
|
||||||
|
write(): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read the PID, of the process holding the lock, from the lockFile.
|
||||||
|
*
|
||||||
|
* It is feasible that the lockFile was removed between the call to `write()` that effectively
|
||||||
|
* checks for existence and this attempt to read the file. If so then this method should just
|
||||||
|
* gracefully return `"{unknown}"`.
|
||||||
|
*/
|
||||||
|
read(): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the lock file from disk, whether or not it exists.
|
||||||
|
*/
|
||||||
|
remove(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class LockFileWithSignalHandlers implements LockFile {
|
||||||
constructor(protected fs: FileSystem) {}
|
constructor(protected fs: FileSystem) {}
|
||||||
|
|
||||||
protected writeLockFile(): void {
|
path = getLockFilePath(this.fs);
|
||||||
|
|
||||||
|
write(): void {
|
||||||
try {
|
try {
|
||||||
this.addSignalHandlers();
|
this.addSignalHandlers();
|
||||||
// To avoid race conditions, we check for existence of the lockfile
|
// To avoid race conditions, we check for existence of the lockFile by actually trying to
|
||||||
// by actually trying to create it exclusively.
|
// create it exclusively.
|
||||||
return this.fs.writeFile(this.lockFilePath, process.pid.toString(), /* exclusive */ true);
|
return this.fs.writeFile(this.path, process.pid.toString(), /* exclusive */ true);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.removeSignalHandlers();
|
this.removeSignalHandlers();
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
read(): string {
|
||||||
* Read the pid from the lockfile.
|
|
||||||
*
|
|
||||||
* It is feasible that the lockfile was removed between the previous check for existence
|
|
||||||
* and this file-read. If so then we still error but as gracefully as possible.
|
|
||||||
*/
|
|
||||||
protected readLockFile(): string {
|
|
||||||
try {
|
try {
|
||||||
if (this.fs instanceof CachedFileSystem) {
|
if (this.fs instanceof CachedFileSystem) {
|
||||||
// This file is "volatile", it might be changed by an external process,
|
// This file is "volatile", it might be changed by an external process,
|
||||||
// so we cannot rely upon the cached value when reading it.
|
// so we cannot rely upon the cached value when reading it.
|
||||||
this.fs.invalidateCaches(this.lockFilePath);
|
this.fs.invalidateCaches(this.path);
|
||||||
}
|
}
|
||||||
return this.fs.readFile(this.lockFilePath);
|
return this.fs.readFile(this.path);
|
||||||
} catch {
|
} catch {
|
||||||
return '{unknown}';
|
return '{unknown}';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
remove() {
|
||||||
* Remove the lock file from disk.
|
|
||||||
*/
|
|
||||||
protected remove() {
|
|
||||||
this.removeSignalHandlers();
|
this.removeSignalHandlers();
|
||||||
if (this.fs.exists(this.lockFilePath)) {
|
if (this.fs.exists(this.path)) {
|
||||||
this.fs.removeFile(this.lockFilePath);
|
this.fs.removeFile(this.path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Capture CTRL-C and terminal closing events.
|
* Capture CTRL-C and terminal closing events.
|
||||||
* When these occur we remove the lockfile and exit.
|
* When these occur we remove the lockFile and exit.
|
||||||
*/
|
*/
|
||||||
protected addSignalHandlers() {
|
protected addSignalHandlers() {
|
||||||
process.addListener('SIGINT', this.signalHandler);
|
process.addListener('SIGINT', this.signalHandler);
|
||||||
|
@ -94,14 +115,16 @@ export abstract class LockFileBase {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* LockFileSync is used to prevent more than one instance of ngcc executing at the same time,
|
* SyncLocker is used to prevent more than one instance of ngcc executing at the same time,
|
||||||
* when being called in a synchronous context.
|
* when being called in a synchronous context.
|
||||||
*
|
*
|
||||||
* * When ngcc starts executing, it creates a file in the `compiler-cli/ngcc` folder.
|
* * When ngcc starts executing, it creates a file in the `compiler-cli/ngcc` folder.
|
||||||
* * If it finds one is already there then it fails with a suitable error message.
|
* * If it finds one is already there then it fails with a suitable error message.
|
||||||
* * When ngcc completes executing, it removes the file so that future ngcc executions can start.
|
* * When ngcc completes executing, it removes the file so that future ngcc executions can start.
|
||||||
*/
|
*/
|
||||||
export class LockFileSync extends LockFileBase {
|
export class SyncLocker {
|
||||||
|
constructor(private lockFile: LockFile) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Run the given function guarded by the lock file.
|
* Run the given function guarded by the lock file.
|
||||||
*
|
*
|
||||||
|
@ -113,7 +136,7 @@ export class LockFileSync extends LockFileBase {
|
||||||
try {
|
try {
|
||||||
return fn();
|
return fn();
|
||||||
} finally {
|
} finally {
|
||||||
this.remove();
|
this.lockFile.remove();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -122,7 +145,7 @@ export class LockFileSync extends LockFileBase {
|
||||||
*/
|
*/
|
||||||
protected create(): void {
|
protected create(): void {
|
||||||
try {
|
try {
|
||||||
this.writeLockFile();
|
this.lockFile.write();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.code !== 'EEXIST') {
|
if (e.code !== 'EEXIST') {
|
||||||
throw e;
|
throw e;
|
||||||
|
@ -132,20 +155,20 @@ export class LockFileSync extends LockFileBase {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The lockfile already exists so raise a helpful error.
|
* The lockFile already exists so raise a helpful error.
|
||||||
*/
|
*/
|
||||||
protected handleExistingLockFile(): void {
|
protected handleExistingLockFile(): void {
|
||||||
const pid = this.readLockFile();
|
const pid = this.lockFile.read();
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`ngcc is already running at process with id ${pid}.\n` +
|
`ngcc is already running at process with id ${pid}.\n` +
|
||||||
`If you are running multiple builds in parallel then you should pre-process your node_modules via the command line ngcc tool before starting the builds;\n` +
|
`If you are running multiple builds in parallel then you should pre-process your node_modules via the command line ngcc tool before starting the builds;\n` +
|
||||||
`See https://v9.angular.io/guide/ivy#speeding-up-ngcc-compilation.\n` +
|
`See https://v9.angular.io/guide/ivy#speeding-up-ngcc-compilation.\n` +
|
||||||
`(If you are sure no ngcc process is running then you should delete the lockfile at ${this.lockFilePath}.)`);
|
`(If you are sure no ngcc process is running then you should delete the lockFile at ${this.lockFile.path}.)`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* LockFileAsync is used to prevent more than one instance of ngcc executing at the same time,
|
* AsyncLocker is used to prevent more than one instance of ngcc executing at the same time,
|
||||||
* when being called in an asynchronous context.
|
* when being called in an asynchronous context.
|
||||||
*
|
*
|
||||||
* * When ngcc starts executing, it creates a file in the `compiler-cli/ngcc` folder.
|
* * When ngcc starts executing, it creates a file in the `compiler-cli/ngcc` folder.
|
||||||
|
@ -155,12 +178,10 @@ export class LockFileSync extends LockFileBase {
|
||||||
* * If the process locking the file changes, then we restart the timeout.
|
* * If the process locking the file changes, then we restart the timeout.
|
||||||
* * When ngcc completes executing, it removes the file so that future ngcc executions can start.
|
* * When ngcc completes executing, it removes the file so that future ngcc executions can start.
|
||||||
*/
|
*/
|
||||||
export class LockFileAsync extends LockFileBase {
|
export class AsyncLocker {
|
||||||
constructor(
|
constructor(
|
||||||
fs: FileSystem, protected logger: Logger, private retryDelay: number,
|
private lockFile: LockFile, protected logger: Logger, private retryDelay: number,
|
||||||
private retryAttempts: number) {
|
private retryAttempts: number) {}
|
||||||
super(fs);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Run a function guarded by the lock file.
|
* Run a function guarded by the lock file.
|
||||||
|
@ -169,19 +190,19 @@ export class LockFileAsync extends LockFileBase {
|
||||||
*/
|
*/
|
||||||
async lock<T>(fn: () => Promise<T>): Promise<T> {
|
async lock<T>(fn: () => Promise<T>): Promise<T> {
|
||||||
await this.create();
|
await this.create();
|
||||||
return fn().finally(() => this.remove());
|
return fn().finally(() => this.lockFile.remove());
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async create() {
|
protected async create() {
|
||||||
let pid: string = '';
|
let pid: string = '';
|
||||||
for (let attempts = 0; attempts < this.retryAttempts; attempts++) {
|
for (let attempts = 0; attempts < this.retryAttempts; attempts++) {
|
||||||
try {
|
try {
|
||||||
return this.writeLockFile();
|
return this.lockFile.write();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.code !== 'EEXIST') {
|
if (e.code !== 'EEXIST') {
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
const newPid = this.readLockFile();
|
const newPid = this.lockFile.read();
|
||||||
if (newPid !== pid) {
|
if (newPid !== pid) {
|
||||||
// The process locking the file has changed, so restart the timeout
|
// The process locking the file has changed, so restart the timeout
|
||||||
attempts = 0;
|
attempts = 0;
|
||||||
|
@ -199,6 +220,6 @@ export class LockFileAsync extends LockFileBase {
|
||||||
// If we fall out of the loop then we ran out of rety attempts
|
// If we fall out of the loop then we ran out of rety attempts
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Timed out waiting ${this.retryAttempts * this.retryDelay/1000}s for another ngcc process, with id ${pid}, to complete.\n` +
|
`Timed out waiting ${this.retryAttempts * this.retryDelay/1000}s for another ngcc process, with id ${pid}, to complete.\n` +
|
||||||
`(If you are sure no ngcc process is running then you should delete the lockfile at ${this.lockFilePath}.)`);
|
`(If you are sure no ngcc process is running then you should delete the lockFile at ${this.lockFile.path}.)`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@ import {Logger} from '../logging/logger';
|
||||||
import {PackageJsonUpdater} from '../writing/package_json_updater';
|
import {PackageJsonUpdater} from '../writing/package_json_updater';
|
||||||
|
|
||||||
import {AnalyzeEntryPointsFn, CreateCompileFn, Executor} from './api';
|
import {AnalyzeEntryPointsFn, CreateCompileFn, Executor} from './api';
|
||||||
import {LockFileAsync, LockFileSync} from './lock_file';
|
import {AsyncLocker, SyncLocker} from './lock_file';
|
||||||
import {onTaskCompleted} from './utils';
|
import {onTaskCompleted} from './utils';
|
||||||
|
|
||||||
export abstract class SingleProcessorExecutorBase {
|
export abstract class SingleProcessorExecutorBase {
|
||||||
|
@ -43,11 +43,11 @@ export abstract class SingleProcessorExecutorBase {
|
||||||
* An `Executor` that processes all tasks serially and completes synchronously.
|
* An `Executor` that processes all tasks serially and completes synchronously.
|
||||||
*/
|
*/
|
||||||
export class SingleProcessExecutorSync extends SingleProcessorExecutorBase implements Executor {
|
export class SingleProcessExecutorSync extends SingleProcessorExecutorBase implements Executor {
|
||||||
constructor(logger: Logger, pkgJsonUpdater: PackageJsonUpdater, private lockfile: LockFileSync) {
|
constructor(logger: Logger, pkgJsonUpdater: PackageJsonUpdater, private lockFile: SyncLocker) {
|
||||||
super(logger, pkgJsonUpdater);
|
super(logger, pkgJsonUpdater);
|
||||||
}
|
}
|
||||||
execute(analyzeEntryPoints: AnalyzeEntryPointsFn, createCompileFn: CreateCompileFn): void {
|
execute(analyzeEntryPoints: AnalyzeEntryPointsFn, createCompileFn: CreateCompileFn): void {
|
||||||
this.lockfile.lock(() => this.doExecute(analyzeEntryPoints, createCompileFn));
|
this.lockFile.lock(() => this.doExecute(analyzeEntryPoints, createCompileFn));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,11 +55,11 @@ export class SingleProcessExecutorSync extends SingleProcessorExecutorBase imple
|
||||||
* An `Executor` that processes all tasks serially, but still completes asynchronously.
|
* An `Executor` that processes all tasks serially, but still completes asynchronously.
|
||||||
*/
|
*/
|
||||||
export class SingleProcessExecutorAsync extends SingleProcessorExecutorBase implements Executor {
|
export class SingleProcessExecutorAsync extends SingleProcessorExecutorBase implements Executor {
|
||||||
constructor(logger: Logger, pkgJsonUpdater: PackageJsonUpdater, private lockfile: LockFileAsync) {
|
constructor(logger: Logger, pkgJsonUpdater: PackageJsonUpdater, private lockFile: AsyncLocker) {
|
||||||
super(logger, pkgJsonUpdater);
|
super(logger, pkgJsonUpdater);
|
||||||
}
|
}
|
||||||
async execute(analyzeEntryPoints: AnalyzeEntryPointsFn, createCompileFn: CreateCompileFn):
|
async execute(analyzeEntryPoints: AnalyzeEntryPointsFn, createCompileFn: CreateCompileFn):
|
||||||
Promise<void> {
|
Promise<void> {
|
||||||
await this.lockfile.lock(async() => this.doExecute(analyzeEntryPoints, createCompileFn));
|
await this.lockFile.lock(async() => this.doExecute(analyzeEntryPoints, createCompileFn));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,7 +27,7 @@ import {TargetedEntryPointFinder} from './entry_point_finder/targeted_entry_poin
|
||||||
import {AnalyzeEntryPointsFn, CreateCompileFn, Executor, PartiallyOrderedTasks, Task, TaskProcessingOutcome, TaskQueue} from './execution/api';
|
import {AnalyzeEntryPointsFn, CreateCompileFn, Executor, PartiallyOrderedTasks, Task, TaskProcessingOutcome, TaskQueue} from './execution/api';
|
||||||
import {ClusterExecutor} from './execution/cluster/executor';
|
import {ClusterExecutor} from './execution/cluster/executor';
|
||||||
import {ClusterPackageJsonUpdater} from './execution/cluster/package_json_updater';
|
import {ClusterPackageJsonUpdater} from './execution/cluster/package_json_updater';
|
||||||
import {LockFileAsync, LockFileSync} from './execution/lock_file';
|
import {AsyncLocker, LockFileWithSignalHandlers, SyncLocker} from './execution/lock_file';
|
||||||
import {SingleProcessExecutorAsync, SingleProcessExecutorSync} from './execution/single_process_executor';
|
import {SingleProcessExecutorAsync, SingleProcessExecutorSync} from './execution/single_process_executor';
|
||||||
import {ParallelTaskQueue} from './execution/task_selection/parallel_task_queue';
|
import {ParallelTaskQueue} from './execution/task_selection/parallel_task_queue';
|
||||||
import {SerialTaskQueue} from './execution/task_selection/serial_task_queue';
|
import {SerialTaskQueue} from './execution/task_selection/serial_task_queue';
|
||||||
|
@ -336,20 +336,21 @@ function getTaskQueue(
|
||||||
function getExecutor(
|
function getExecutor(
|
||||||
async: boolean, inParallel: boolean, logger: Logger, pkgJsonUpdater: PackageJsonUpdater,
|
async: boolean, inParallel: boolean, logger: Logger, pkgJsonUpdater: PackageJsonUpdater,
|
||||||
fileSystem: FileSystem): Executor {
|
fileSystem: FileSystem): Executor {
|
||||||
|
const lockFile = new LockFileWithSignalHandlers(fileSystem);
|
||||||
if (async) {
|
if (async) {
|
||||||
// Execute asynchronously (either serially or in parallel)
|
// Execute asynchronously (either serially or in parallel)
|
||||||
const lockFile = new LockFileAsync(fileSystem, logger, 500, 50);
|
const locker = new AsyncLocker(lockFile, logger, 500, 50);
|
||||||
if (inParallel) {
|
if (inParallel) {
|
||||||
// Execute in parallel. Use up to 8 CPU cores for workers, always reserving one for master.
|
// Execute in parallel. Use up to 8 CPU cores for workers, always reserving one for master.
|
||||||
const workerCount = Math.min(8, os.cpus().length - 1);
|
const workerCount = Math.min(8, os.cpus().length - 1);
|
||||||
return new ClusterExecutor(workerCount, logger, pkgJsonUpdater, lockFile);
|
return new ClusterExecutor(workerCount, logger, pkgJsonUpdater, locker);
|
||||||
} else {
|
} else {
|
||||||
// Execute serially, on a single thread (async).
|
// Execute serially, on a single thread (async).
|
||||||
return new SingleProcessExecutorAsync(logger, pkgJsonUpdater, lockFile);
|
return new SingleProcessExecutorAsync(logger, pkgJsonUpdater, locker);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Execute serially, on a single thread (sync).
|
// Execute serially, on a single thread (sync).
|
||||||
return new SingleProcessExecutorSync(logger, pkgJsonUpdater, new LockFileSync(fileSystem));
|
return new SingleProcessExecutorSync(logger, pkgJsonUpdater, new SyncLocker(lockFile));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,11 +10,13 @@
|
||||||
|
|
||||||
import * as cluster from 'cluster';
|
import * as cluster from 'cluster';
|
||||||
|
|
||||||
|
import {MockFileSystemNative} from '../../../../src/ngtsc/file_system/testing';
|
||||||
import {ClusterExecutor} from '../../../src/execution/cluster/executor';
|
import {ClusterExecutor} from '../../../src/execution/cluster/executor';
|
||||||
import {ClusterMaster} from '../../../src/execution/cluster/master';
|
import {ClusterMaster} from '../../../src/execution/cluster/master';
|
||||||
import {ClusterWorker} from '../../../src/execution/cluster/worker';
|
import {ClusterWorker} from '../../../src/execution/cluster/worker';
|
||||||
|
import {AsyncLocker} from '../../../src/execution/lock_file';
|
||||||
import {PackageJsonUpdater} from '../../../src/writing/package_json_updater';
|
import {PackageJsonUpdater} from '../../../src/writing/package_json_updater';
|
||||||
import {MockLockFileAsync} from '../../helpers/mock_lock_file';
|
import {MockLockFile} from '../../helpers/mock_lock_file';
|
||||||
import {MockLogger} from '../../helpers/mock_logger';
|
import {MockLogger} from '../../helpers/mock_logger';
|
||||||
import {mockProperty} from '../../helpers/spy_utils';
|
import {mockProperty} from '../../helpers/spy_utils';
|
||||||
|
|
||||||
|
@ -24,7 +26,9 @@ describe('ClusterExecutor', () => {
|
||||||
let masterRunSpy: jasmine.Spy;
|
let masterRunSpy: jasmine.Spy;
|
||||||
let workerRunSpy: jasmine.Spy;
|
let workerRunSpy: jasmine.Spy;
|
||||||
let mockLogger: MockLogger;
|
let mockLogger: MockLogger;
|
||||||
let mockLockFile: MockLockFileAsync;
|
let lockFileLog: string[];
|
||||||
|
let mockLockFile: MockLockFile;
|
||||||
|
let locker: AsyncLocker;
|
||||||
let executor: ClusterExecutor;
|
let executor: ClusterExecutor;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
@ -34,9 +38,10 @@ describe('ClusterExecutor', () => {
|
||||||
.and.returnValue(Promise.resolve('CusterWorker#run()'));
|
.and.returnValue(Promise.resolve('CusterWorker#run()'));
|
||||||
|
|
||||||
mockLogger = new MockLogger();
|
mockLogger = new MockLogger();
|
||||||
mockLockFile = new MockLockFileAsync();
|
lockFileLog = [];
|
||||||
executor =
|
mockLockFile = new MockLockFile(new MockFileSystemNative(), lockFileLog);
|
||||||
new ClusterExecutor(42, mockLogger, null as unknown as PackageJsonUpdater, mockLockFile);
|
locker = new AsyncLocker(mockLockFile, mockLogger, 200, 2);
|
||||||
|
executor = new ClusterExecutor(42, mockLogger, null as unknown as PackageJsonUpdater, locker);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('execute()', () => {
|
describe('execute()', () => {
|
||||||
|
@ -66,14 +71,14 @@ describe('ClusterExecutor', () => {
|
||||||
expect(createCompilerFnSpy).not.toHaveBeenCalled();
|
expect(createCompilerFnSpy).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call LockFile.create() and LockFile.remove() if master runner completes successfully',
|
it('should call LockFile.write() and LockFile.remove() if master runner completes successfully',
|
||||||
async() => {
|
async() => {
|
||||||
const anyFn: () => any = () => undefined;
|
const anyFn: () => any = () => undefined;
|
||||||
await executor.execute(anyFn, anyFn);
|
await executor.execute(anyFn, anyFn);
|
||||||
expect(mockLockFile.log).toEqual(['create()', 'remove()']);
|
expect(lockFileLog).toEqual(['write()', 'remove()']);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call LockFile.create() and LockFile.remove() if master runner fails', async() => {
|
it('should call LockFile.write() and LockFile.remove() if master runner fails', async() => {
|
||||||
const anyFn: () => any = () => undefined;
|
const anyFn: () => any = () => undefined;
|
||||||
masterRunSpy.and.returnValue(Promise.reject(new Error('master runner error')));
|
masterRunSpy.and.returnValue(Promise.reject(new Error('master runner error')));
|
||||||
let error = '';
|
let error = '';
|
||||||
|
@ -83,30 +88,37 @@ describe('ClusterExecutor', () => {
|
||||||
error = e.message;
|
error = e.message;
|
||||||
}
|
}
|
||||||
expect(error).toEqual('master runner error');
|
expect(error).toEqual('master runner error');
|
||||||
expect(mockLockFile.log).toEqual(['create()', 'remove()']);
|
expect(lockFileLog).toEqual(['write()', 'remove()']);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not call master runner if Lockfile.create() fails', async() => {
|
it('should not call master runner if LockFile.write() fails', async() => {
|
||||||
const anyFn: () => any = () => undefined;
|
const anyFn: () => any = () => undefined;
|
||||||
const lockFile = new MockLockFileAsync({throwOnCreate: true});
|
spyOn(mockLockFile, 'write').and.callFake(() => {
|
||||||
|
lockFileLog.push('write()');
|
||||||
|
throw new Error('LockFile.write() error');
|
||||||
|
});
|
||||||
|
|
||||||
executor =
|
executor =
|
||||||
new ClusterExecutor(42, mockLogger, null as unknown as PackageJsonUpdater, lockFile);
|
new ClusterExecutor(42, mockLogger, null as unknown as PackageJsonUpdater, locker);
|
||||||
let error = '';
|
let error = '';
|
||||||
try {
|
try {
|
||||||
await executor.execute(anyFn, anyFn);
|
await executor.execute(anyFn, anyFn);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error = e.message;
|
error = e.message;
|
||||||
}
|
}
|
||||||
expect(error).toEqual('LockFile.create() error');
|
expect(error).toEqual('LockFile.write() error');
|
||||||
expect(lockFile.log).toEqual(['create()']);
|
|
||||||
expect(masterRunSpy).not.toHaveBeenCalled();
|
expect(masterRunSpy).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should fail if Lockfile.remove() fails', async() => {
|
it('should fail if LockFile.remove() fails', async() => {
|
||||||
const anyFn: () => any = () => undefined;
|
const anyFn: () => any = () => undefined;
|
||||||
const lockFile = new MockLockFileAsync({throwOnRemove: true});
|
spyOn(mockLockFile, 'remove').and.callFake(() => {
|
||||||
|
lockFileLog.push('remove()');
|
||||||
|
throw new Error('LockFile.remove() error');
|
||||||
|
});
|
||||||
|
|
||||||
executor =
|
executor =
|
||||||
new ClusterExecutor(42, mockLogger, null as unknown as PackageJsonUpdater, lockFile);
|
new ClusterExecutor(42, mockLogger, null as unknown as PackageJsonUpdater, locker);
|
||||||
let error = '';
|
let error = '';
|
||||||
try {
|
try {
|
||||||
await executor.execute(anyFn, anyFn);
|
await executor.execute(anyFn, anyFn);
|
||||||
|
@ -114,7 +126,7 @@ describe('ClusterExecutor', () => {
|
||||||
error = e.message;
|
error = e.message;
|
||||||
}
|
}
|
||||||
expect(error).toEqual('LockFile.remove() error');
|
expect(error).toEqual('LockFile.remove() error');
|
||||||
expect(lockFile.log).toEqual(['create()', 'remove()']);
|
expect(lockFileLog).toEqual(['write()', 'remove()']);
|
||||||
expect(masterRunSpy).toHaveBeenCalled();
|
expect(masterRunSpy).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -143,10 +155,10 @@ describe('ClusterExecutor', () => {
|
||||||
expect(createCompilerFnSpy).toHaveBeenCalledWith(jasmine.any(Function));
|
expect(createCompilerFnSpy).toHaveBeenCalledWith(jasmine.any(Function));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not call LockFile.create() or LockFile.remove()', async() => {
|
it('should not call LockFile.write() or LockFile.remove()', async() => {
|
||||||
const anyFn: () => any = () => undefined;
|
const anyFn: () => any = () => undefined;
|
||||||
await executor.execute(anyFn, anyFn);
|
await executor.execute(anyFn, anyFn);
|
||||||
expect(mockLockFile.log).toEqual([]);
|
expect(lockFileLog).toEqual([]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -9,29 +9,41 @@ import * as process from 'process';
|
||||||
|
|
||||||
import {CachedFileSystem, FileSystem, getFileSystem} from '../../../src/ngtsc/file_system';
|
import {CachedFileSystem, FileSystem, getFileSystem} from '../../../src/ngtsc/file_system';
|
||||||
import {runInEachFileSystem} from '../../../src/ngtsc/file_system/testing';
|
import {runInEachFileSystem} from '../../../src/ngtsc/file_system/testing';
|
||||||
import {LockFileAsync, LockFileBase, LockFileSync} from '../../src/execution/lock_file';
|
import {AsyncLocker, LockFileWithSignalHandlers, SyncLocker} from '../../src/execution/lock_file';
|
||||||
|
import {MockLockFile} from '../helpers/mock_lock_file';
|
||||||
import {MockLogger} from '../helpers/mock_logger';
|
import {MockLogger} from '../helpers/mock_logger';
|
||||||
|
|
||||||
runInEachFileSystem(() => {
|
runInEachFileSystem(() => {
|
||||||
describe('LockFileBase', () => {
|
describe('LockFileWithSignalHandlers', () => {
|
||||||
/**
|
/**
|
||||||
* This class allows us to test the abstract class LockFileBase.
|
* This class allows us to test ordering of the calls, and to avoid actually attaching signal
|
||||||
|
* handlers and most importantly not actually exiting the process.
|
||||||
*/
|
*/
|
||||||
class LockFileUnderTest extends LockFileBase {
|
class LockFileUnderTest extends LockFileWithSignalHandlers {
|
||||||
log: string[] = [];
|
log: string[] = [];
|
||||||
constructor(fs: FileSystem, private handleSignals = false) {
|
constructor(fs: FileSystem, private handleSignals = false) {
|
||||||
super(fs);
|
super(fs);
|
||||||
fs.ensureDir(fs.dirname(this.lockFilePath));
|
fs.ensureDir(fs.dirname(this.path));
|
||||||
|
}
|
||||||
|
remove() {
|
||||||
|
this.log.push('remove()');
|
||||||
|
super.remove();
|
||||||
}
|
}
|
||||||
remove() { super.remove(); }
|
|
||||||
addSignalHandlers() {
|
addSignalHandlers() {
|
||||||
this.log.push('addSignalHandlers()');
|
this.log.push('addSignalHandlers()');
|
||||||
if (this.handleSignals) {
|
if (this.handleSignals) {
|
||||||
super.addSignalHandlers();
|
super.addSignalHandlers();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
writeLockFile() { super.writeLockFile(); }
|
write() {
|
||||||
readLockFile() { return super.readLockFile(); }
|
this.log.push('write()');
|
||||||
|
super.write();
|
||||||
|
}
|
||||||
|
read() {
|
||||||
|
const contents = super.read();
|
||||||
|
this.log.push('read() => ' + contents);
|
||||||
|
return contents;
|
||||||
|
}
|
||||||
removeSignalHandlers() {
|
removeSignalHandlers() {
|
||||||
this.log.push('removeSignalHandlers()');
|
this.log.push('removeSignalHandlers()');
|
||||||
super.removeSignalHandlers();
|
super.removeSignalHandlers();
|
||||||
|
@ -39,45 +51,91 @@ runInEachFileSystem(() => {
|
||||||
exit(code: number) { this.log.push(`exit(${code})`); }
|
exit(code: number) { this.log.push(`exit(${code})`); }
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('writeLockFile()', () => {
|
describe('write()', () => {
|
||||||
it('should call `addSignalHandlers()`', () => {
|
it('should call `addSignalHandlers()`', () => {
|
||||||
const fs = getFileSystem();
|
const fs = getFileSystem();
|
||||||
const lockFile = new LockFileUnderTest(fs);
|
const lockFile = new LockFileUnderTest(fs);
|
||||||
lockFile.writeLockFile();
|
lockFile.write();
|
||||||
expect(lockFile.log).toEqual(['addSignalHandlers()']);
|
expect(lockFile.log).toEqual(['write()', 'addSignalHandlers()']);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call `removeSignalHandlers()` if there is an error', () => {
|
it('should call `removeSignalHandlers()` if there is an error', () => {
|
||||||
const fs = getFileSystem();
|
const fs = getFileSystem();
|
||||||
spyOn(fs, 'writeFile').and.throwError('WRITING ERROR');
|
spyOn(fs, 'writeFile').and.throwError('WRITING ERROR');
|
||||||
const lockFile = new LockFileUnderTest(fs);
|
const lockFile = new LockFileUnderTest(fs);
|
||||||
expect(() => lockFile.writeLockFile()).toThrowError('WRITING ERROR');
|
expect(() => lockFile.write()).toThrowError('WRITING ERROR');
|
||||||
expect(lockFile.log).toEqual(['addSignalHandlers()', 'removeSignalHandlers()']);
|
expect(lockFile.log).toEqual(['write()', 'addSignalHandlers()', 'removeSignalHandlers()']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove the lockFile if CTRL-C is triggered', () => {
|
||||||
|
const fs = getFileSystem();
|
||||||
|
const lockFile = new LockFileUnderTest(fs, /* handleSignals */ true);
|
||||||
|
|
||||||
|
lockFile.write();
|
||||||
|
expect(lockFile.log).toEqual(['write()', 'addSignalHandlers()']);
|
||||||
|
|
||||||
|
// Simulate the CTRL-C signal
|
||||||
|
lockFile.log.push('SIGINT');
|
||||||
|
process.emit('SIGINT', 'SIGINT');
|
||||||
|
|
||||||
|
expect(lockFile.log).toEqual([
|
||||||
|
'write()', 'addSignalHandlers()', 'SIGINT', 'remove()', 'removeSignalHandlers()',
|
||||||
|
'exit(1)'
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove the lockFile if terminal is closed', () => {
|
||||||
|
const fs = getFileSystem();
|
||||||
|
const lockFile = new LockFileUnderTest(fs, /* handleSignals */ true);
|
||||||
|
|
||||||
|
lockFile.write();
|
||||||
|
expect(lockFile.log).toEqual(['write()', 'addSignalHandlers()']);
|
||||||
|
|
||||||
|
// Simulate the terminal being closed
|
||||||
|
lockFile.log.push('SIGHUP');
|
||||||
|
process.emit('SIGHUP', 'SIGHUP');
|
||||||
|
|
||||||
|
expect(lockFile.log).toEqual([
|
||||||
|
'write()', 'addSignalHandlers()', 'SIGHUP', 'remove()', 'removeSignalHandlers()',
|
||||||
|
'exit(1)'
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('readLockFile()', () => {
|
describe('read()', () => {
|
||||||
it('should return the contents of the lockfile', () => {
|
it('should return the contents of the lockFile', () => {
|
||||||
const fs = getFileSystem();
|
const fs = getFileSystem();
|
||||||
const lockFile = new LockFileUnderTest(fs);
|
const lockFile = new LockFileUnderTest(fs);
|
||||||
fs.writeFile(lockFile.lockFilePath, '188');
|
fs.writeFile(lockFile.path, '188');
|
||||||
expect(lockFile.readLockFile()).toEqual('188');
|
expect(lockFile.read()).toEqual('188');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return `{unknown}` if the lockfile does not exist', () => {
|
it('should return `{unknown}` if the lockFile does not exist', () => {
|
||||||
const fs = getFileSystem();
|
const fs = getFileSystem();
|
||||||
const lockFile = new LockFileUnderTest(fs);
|
const lockFile = new LockFileUnderTest(fs);
|
||||||
expect(lockFile.readLockFile()).toEqual('{unknown}');
|
expect(lockFile.read()).toEqual('{unknown}');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not read file from the cache, since the file may have been modified externally',
|
||||||
|
() => {
|
||||||
|
const rawFs = getFileSystem();
|
||||||
|
const fs = new CachedFileSystem(rawFs);
|
||||||
|
const lockFile = new LockFileUnderTest(fs);
|
||||||
|
rawFs.writeFile(lockFile.path, '188');
|
||||||
|
expect(lockFile.read()).toEqual('188');
|
||||||
|
// We need to write to the rawFs to ensure that we don't update the cache at this point
|
||||||
|
rawFs.writeFile(lockFile.path, '444');
|
||||||
|
expect(lockFile.read()).toEqual('444');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('remove()', () => {
|
describe('remove()', () => {
|
||||||
it('should remove the lock file from the file-system', () => {
|
it('should remove the lock file from the file-system', () => {
|
||||||
const fs = getFileSystem();
|
const fs = getFileSystem();
|
||||||
const lockFile = new LockFileUnderTest(fs);
|
const lockFile = new LockFileUnderTest(fs);
|
||||||
fs.writeFile(lockFile.lockFilePath, '188');
|
fs.writeFile(lockFile.path, '188');
|
||||||
lockFile.remove();
|
lockFile.remove();
|
||||||
expect(fs.exists(lockFile.lockFilePath)).toBe(false);
|
expect(fs.exists(lockFile.path)).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not error if the lock file does not exist', () => {
|
it('should not error if the lock file does not exist', () => {
|
||||||
|
@ -89,298 +147,219 @@ runInEachFileSystem(() => {
|
||||||
it('should call removeSignalHandlers()', () => {
|
it('should call removeSignalHandlers()', () => {
|
||||||
const fs = getFileSystem();
|
const fs = getFileSystem();
|
||||||
const lockFile = new LockFileUnderTest(fs);
|
const lockFile = new LockFileUnderTest(fs);
|
||||||
fs.writeFile(lockFile.lockFilePath, '188');
|
fs.writeFile(lockFile.path, '188');
|
||||||
lockFile.remove();
|
lockFile.remove();
|
||||||
expect(lockFile.log).toEqual(['removeSignalHandlers()']);
|
expect(lockFile.log).toEqual(['remove()', 'removeSignalHandlers()']);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('LockFileSync', () => {
|
describe('SyncLocker', () => {
|
||||||
/**
|
|
||||||
* This class allows us to test the protected methods of LockFileSync 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 LockFileSync {
|
|
||||||
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})`); }
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('lock()', () => {
|
describe('lock()', () => {
|
||||||
it('should guard the `fn()` with calls to `create()` and `remove()`', () => {
|
it('should guard the `fn()` with calls to `write()` and `remove()`', () => {
|
||||||
const fs = getFileSystem();
|
const fs = getFileSystem();
|
||||||
const lockFile = new LockFileUnderTest(fs);
|
const log: string[] = [];
|
||||||
|
const lockFile = new MockLockFile(fs, log);
|
||||||
|
const locker = new SyncLocker(lockFile);
|
||||||
|
|
||||||
lockFile.lock(() => lockFile.log.push('fn()'));
|
locker.lock(() => log.push('fn()'));
|
||||||
expect(lockFile.log).toEqual(['create()', 'fn()', 'remove()']);
|
|
||||||
|
expect(log).toEqual(['write()', 'fn()', 'remove()']);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should guard the `fn()` with calls to `create()` and `remove()`, even if it throws',
|
it('should guard the `fn()` with calls to `write()` and `remove()`, even if it throws',
|
||||||
() => {
|
() => {
|
||||||
let error: string = '';
|
let error: string = '';
|
||||||
const fs = getFileSystem();
|
const fs = getFileSystem();
|
||||||
const lockFile = new LockFileUnderTest(fs);
|
const log: string[] = [];
|
||||||
|
const lockFile = new MockLockFile(fs, log);
|
||||||
|
const locker = new SyncLocker(lockFile);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
lockFile.lock(() => {
|
locker.lock(() => {
|
||||||
lockFile.log.push('fn()');
|
log.push('fn()');
|
||||||
throw new Error('ERROR');
|
throw new Error('ERROR');
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error = e.message;
|
error = e.message;
|
||||||
}
|
}
|
||||||
expect(error).toEqual('ERROR');
|
expect(error).toEqual('ERROR');
|
||||||
expect(lockFile.log).toEqual(['create()', 'fn()', 'remove()']);
|
expect(log).toEqual(['write()', '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('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', () => {
|
it('should error if a lock file already exists', () => {
|
||||||
const fs = getFileSystem();
|
const fs = getFileSystem();
|
||||||
const lockFile = new LockFileUnderTest(fs);
|
const log: string[] = [];
|
||||||
fs.writeFile(lockFile.lockFilePath, '188');
|
const lockFile = new MockLockFile(fs, log);
|
||||||
expect(() => lockFile.create())
|
const locker = new SyncLocker(lockFile);
|
||||||
|
|
||||||
|
spyOn(lockFile, 'write').and.callFake(() => { throw {code: 'EEXIST'}; });
|
||||||
|
spyOn(lockFile, 'read').and.returnValue('188');
|
||||||
|
|
||||||
|
expect(() => locker.lock(() => {}))
|
||||||
.toThrowError(
|
.toThrowError(
|
||||||
`ngcc is already running at process with id 188.\n` +
|
`ngcc is already running at process with id 188.\n` +
|
||||||
`If you are running multiple builds in parallel then you should pre-process your node_modules via the command line ngcc tool before starting the builds;\n` +
|
`If you are running multiple builds in parallel then you should pre-process your node_modules via the command line ngcc tool before starting the builds;\n` +
|
||||||
`See https://v9.angular.io/guide/ivy#speeding-up-ngcc-compilation.\n` +
|
`See https://v9.angular.io/guide/ivy#speeding-up-ngcc-compilation.\n` +
|
||||||
`(If you are sure no ngcc process is running then you should delete the lockfile at ${lockFile.lockFilePath}.)`);
|
`(If you are sure no ngcc process is running then you should delete the lockFile at ${lockFile.path}.)`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('LockFileAsync', () => {
|
describe('AsyncLocker', () => {
|
||||||
/**
|
|
||||||
* This class allows us to test the protected methods of LockFileAsync 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 LockFileAsync {
|
|
||||||
log: string[] = [];
|
|
||||||
constructor(
|
|
||||||
fs: FileSystem, retryDelay = 100, retryAttempts = 10, private handleSignals = false) {
|
|
||||||
super(fs, new MockLogger(), retryDelay, retryAttempts);
|
|
||||||
fs.ensureDir(fs.dirname(this.lockFilePath));
|
|
||||||
}
|
|
||||||
async create() {
|
|
||||||
this.log.push('create()');
|
|
||||||
await 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})`); }
|
|
||||||
getLogger() { return this.logger as MockLogger; }
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('lock()', () => {
|
describe('lock()', () => {
|
||||||
it('should guard the `fn()` with calls to `create()` and `remove()`', async() => {
|
it('should guard the `fn()` with calls to `write()` and `remove()`', async() => {
|
||||||
const fs = getFileSystem();
|
const fs = getFileSystem();
|
||||||
const lockFile = new LockFileUnderTest(fs);
|
const log: string[] = [];
|
||||||
|
const lockFile = new MockLockFile(fs, log);
|
||||||
|
const locker = new AsyncLocker(lockFile, new MockLogger(), 100, 10);
|
||||||
|
|
||||||
await lockFile.lock(async() => {
|
await locker.lock(async() => {
|
||||||
lockFile.log.push('fn() - before');
|
log.push('fn() - before');
|
||||||
// This promise forces node to do a tick in this function, ensuring that we are truly
|
// This promise forces node to do a tick in this function, ensuring that we are truly
|
||||||
// testing an async scenario.
|
// testing an async scenario.
|
||||||
await Promise.resolve();
|
await Promise.resolve();
|
||||||
lockFile.log.push('fn() - after');
|
log.push('fn() - after');
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
});
|
});
|
||||||
expect(lockFile.log).toEqual(['create()', 'fn() - before', 'fn() - after', 'remove()']);
|
expect(log).toEqual(['write()', 'fn() - before', 'fn() - after', 'remove()']);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should guard the `fn()` with calls to `create()` and `remove()`, even if it throws',
|
it('should guard the `fn()` with calls to `write()` and `remove()`, even if it throws',
|
||||||
async() => {
|
async() => {
|
||||||
let error: string = '';
|
let error: string = '';
|
||||||
const fs = getFileSystem();
|
const fs = getFileSystem();
|
||||||
const lockFile = new LockFileUnderTest(fs);
|
const log: string[] = [];
|
||||||
|
const lockFile = new MockLockFile(fs, log);
|
||||||
|
const locker = new AsyncLocker(lockFile, new MockLogger(), 100, 10);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await lockFile.lock(async() => {
|
await locker.lock(async() => {
|
||||||
lockFile.log.push('fn()');
|
log.push('fn()');
|
||||||
throw new Error('ERROR');
|
throw new Error('ERROR');
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error = e.message;
|
error = e.message;
|
||||||
}
|
}
|
||||||
expect(error).toEqual('ERROR');
|
expect(error).toEqual('ERROR');
|
||||||
expect(lockFile.log).toEqual(['create()', 'fn()', 'remove()']);
|
expect(log).toEqual(['write()', 'fn()', 'remove()']);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should remove the lockfile if CTRL-C is triggered', async() => {
|
|
||||||
const fs = getFileSystem();
|
|
||||||
const lockFile = new LockFileUnderTest(fs, 100, 3, /* 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, 100, 3, /* 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', async() => {
|
|
||||||
const fs = getFileSystem();
|
|
||||||
const lockFile = new LockFileUnderTest(fs);
|
|
||||||
expect(fs.exists(lockFile.lockFilePath)).toBe(false);
|
|
||||||
await lockFile.create();
|
|
||||||
expect(fs.exists(lockFile.lockFilePath)).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should retry if another process is locking', async() => {
|
it('should retry if another process is locking', async() => {
|
||||||
const fs = getFileSystem();
|
const fs = getFileSystem();
|
||||||
const lockFile = new LockFileUnderTest(fs);
|
const log: string[] = [];
|
||||||
fs.writeFile(lockFile.lockFilePath, '188');
|
const lockFile = new MockLockFile(fs, log);
|
||||||
const promise = lockFile.lock(async() => lockFile.log.push('fn()'));
|
const logger = new MockLogger();
|
||||||
// The lock is now waiting on the lockfile becoming free, so no `fn()` in the log.
|
const locker = new AsyncLocker(lockFile, logger, 100, 10);
|
||||||
expect(lockFile.log).toEqual(['create()']);
|
|
||||||
expect(lockFile.getLogger().logs.info).toEqual([[
|
let lockFileContents: string|null = '188';
|
||||||
|
spyOn(lockFile, 'write').and.callFake(() => {
|
||||||
|
log.push('write()');
|
||||||
|
if (lockFileContents) {
|
||||||
|
throw {code: 'EEXIST'};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
spyOn(lockFile, 'read').and.callFake(() => {
|
||||||
|
log.push('read() => ' + lockFileContents);
|
||||||
|
return lockFileContents;
|
||||||
|
});
|
||||||
|
|
||||||
|
const promise = locker.lock(async() => log.push('fn()'));
|
||||||
|
// The lock is now waiting on the lockFile becoming free, so no `fn()` in the log.
|
||||||
|
expect(log).toEqual(['write()', 'read() => 188']);
|
||||||
|
expect(logger.logs.info).toEqual([[
|
||||||
'Another process, with id 188, is currently running ngcc.\nWaiting up to 1s for it to finish.'
|
'Another process, with id 188, is currently running ngcc.\nWaiting up to 1s for it to finish.'
|
||||||
]]);
|
]]);
|
||||||
fs.removeFile(lockFile.lockFilePath);
|
|
||||||
// The lockfile has been removed, so we can create our own lockfile, call `fn()` and then
|
lockFileContents = null;
|
||||||
// remove the lockfile.
|
// The lockFile has been removed, so we can create our own lockFile, call `fn()` and then
|
||||||
|
// remove the lockFile.
|
||||||
await promise;
|
await promise;
|
||||||
expect(lockFile.log).toEqual(['create()', 'fn()', 'remove()']);
|
expect(log).toEqual(['write()', 'read() => 188', 'write()', 'fn()', 'remove()']);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should extend the retry timeout if the other process locking the file changes', async() => {
|
it('should extend the retry timeout if the other process locking the file changes', async() => {
|
||||||
// Use a cached file system to test that we are invalidating it correctly
|
const fs = getFileSystem();
|
||||||
const rawFs = getFileSystem();
|
const log: string[] = [];
|
||||||
const fs = new CachedFileSystem(rawFs);
|
const lockFile = new MockLockFile(fs, log);
|
||||||
const lockFile = new LockFileUnderTest(fs);
|
const logger = new MockLogger();
|
||||||
fs.writeFile(lockFile.lockFilePath, '188');
|
const locker = new AsyncLocker(lockFile, logger, 100, 10);
|
||||||
const promise = lockFile.lock(async() => lockFile.log.push('fn()'));
|
|
||||||
// The lock is now waiting on the lockfile becoming free, so no `fn()` in the log.
|
let lockFileContents: string|null = '188';
|
||||||
expect(lockFile.log).toEqual(['create()']);
|
spyOn(lockFile, 'write').and.callFake(() => {
|
||||||
expect(lockFile.getLogger().logs.info).toEqual([[
|
log.push('write()');
|
||||||
'Another process, with id 188, is currently running ngcc.\nWaiting up to 1s for it to finish.'
|
if (lockFileContents) {
|
||||||
]]);
|
throw {code: 'EEXIST'};
|
||||||
// We need to write to the rawFs to ensure that we don't update the cache at this point
|
}
|
||||||
rawFs.writeFile(lockFile.lockFilePath, '444');
|
});
|
||||||
await new Promise(resolve => setTimeout(resolve, 250));
|
spyOn(lockFile, 'read').and.callFake(() => {
|
||||||
expect(lockFile.getLogger().logs.info).toEqual([
|
log.push('read() => ' + lockFileContents);
|
||||||
[
|
return lockFileContents;
|
||||||
|
});
|
||||||
|
|
||||||
|
async() => {
|
||||||
|
const promise = locker.lock(async() => log.push('fn()'));
|
||||||
|
// The lock is now waiting on the lockFile becoming free, so no `fn()` in the log.
|
||||||
|
expect(log).toEqual(['write()', 'read() => 188']);
|
||||||
|
expect(logger.logs.info).toEqual([[
|
||||||
'Another process, with id 188, is currently running ngcc.\nWaiting up to 1s for it to finish.'
|
'Another process, with id 188, is currently running ngcc.\nWaiting up to 1s for it to finish.'
|
||||||
],
|
]]);
|
||||||
[
|
|
||||||
'Another process, with id 444, is currently running ngcc.\nWaiting up to 1s for it to finish.'
|
lockFileContents = '444';
|
||||||
]
|
// The lockFile has been taken over by another process
|
||||||
]);
|
await new Promise(resolve => setTimeout(resolve, 250));
|
||||||
fs.removeFile(lockFile.lockFilePath);
|
expect(log).toEqual(['write()', 'read() => 188', 'write()', 'read() => 444']);
|
||||||
// The lockfile has been removed, so we can create our own lockfile, call `fn()` and then
|
expect(logger.logs.info).toEqual([
|
||||||
// remove the lockfile.
|
[
|
||||||
await promise;
|
'Another process, with id 188, is currently running ngcc.\nWaiting up to 1s for it to finish.'
|
||||||
expect(lockFile.log).toEqual(['create()', 'fn()', 'remove()']);
|
],
|
||||||
|
[
|
||||||
|
'Another process, with id 444, is currently running ngcc.\nWaiting up to 1s for it to finish.'
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
lockFileContents = null;
|
||||||
|
// The lockFile has been removed, so we can create our own lockFile, call `fn()` and then
|
||||||
|
// remove the lockFile.
|
||||||
|
await promise;
|
||||||
|
expect(log).toEqual([
|
||||||
|
'write()', 'read() => 188', 'write()', 'read() => 444', 'write()', 'fn()', 'remove()'
|
||||||
|
]);
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should error if another process does not release the lockfile before this times out',
|
it('should error if another process does not release the lockFile before this times out',
|
||||||
async() => {
|
async() => {
|
||||||
const fs = getFileSystem();
|
const fs = getFileSystem();
|
||||||
const lockFile = new LockFileUnderTest(fs, 100, 2);
|
const log: string[] = [];
|
||||||
fs.writeFile(lockFile.lockFilePath, '188');
|
const lockFile = new MockLockFile(fs, log);
|
||||||
const promise = lockFile.lock(async() => lockFile.log.push('fn()'));
|
const logger = new MockLogger();
|
||||||
// The lock is now waiting on the lockfile becoming free, so no `fn()` in the log.
|
const locker = new AsyncLocker(lockFile, logger, 100, 2);
|
||||||
expect(lockFile.log).toEqual(['create()']);
|
|
||||||
// Do not remove the lockfile and let the call to `lock()` timeout.
|
let lockFileContents: string|null = '188';
|
||||||
|
spyOn(lockFile, 'write').and.callFake(() => {
|
||||||
|
log.push('write()');
|
||||||
|
if (lockFileContents) {
|
||||||
|
throw {code: 'EEXIST'};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
spyOn(lockFile, 'read').and.callFake(() => {
|
||||||
|
log.push('read() => ' + lockFileContents);
|
||||||
|
return lockFileContents;
|
||||||
|
});
|
||||||
|
|
||||||
|
const promise = locker.lock(async() => log.push('fn()'));
|
||||||
|
|
||||||
|
// The lock is now waiting on the lockFile becoming free, so no `fn()` in the log.
|
||||||
|
expect(log).toEqual(['write()', 'read() => 188']);
|
||||||
|
// Do not remove the lockFile and let the call to `lock()` timeout.
|
||||||
let error: Error;
|
let error: Error;
|
||||||
await promise.catch(e => error = e);
|
await promise.catch(e => error = e);
|
||||||
expect(lockFile.log).toEqual(['create()']);
|
expect(log).toEqual(['write()', 'read() => 188', 'write()', 'read() => 188']);
|
||||||
expect(error !.message)
|
expect(error !.message)
|
||||||
.toEqual(
|
.toEqual(
|
||||||
`Timed out waiting 0.2s for another ngcc process, with id 188, to complete.\n` +
|
`Timed out waiting 0.2s for another ngcc process, with id 188, to complete.\n` +
|
||||||
`(If you are sure no ngcc process is running then you should delete the lockfile at ${lockFile.lockFilePath}.)`);
|
`(If you are sure no ngcc process is running then you should delete the lockFile at ${lockFile.path}.)`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -8,35 +8,41 @@
|
||||||
|
|
||||||
/// <reference types="node" />
|
/// <reference types="node" />
|
||||||
|
|
||||||
|
import {MockFileSystemNative} from '../../../src/ngtsc/file_system/testing';
|
||||||
|
import {SyncLocker} from '../../src/execution/lock_file';
|
||||||
import {SingleProcessExecutorSync} from '../../src/execution/single_process_executor';
|
import {SingleProcessExecutorSync} from '../../src/execution/single_process_executor';
|
||||||
import {SerialTaskQueue} from '../../src/execution/task_selection/serial_task_queue';
|
import {SerialTaskQueue} from '../../src/execution/task_selection/serial_task_queue';
|
||||||
import {PackageJsonUpdater} from '../../src/writing/package_json_updater';
|
import {PackageJsonUpdater} from '../../src/writing/package_json_updater';
|
||||||
import {MockLockFileSync} from '../helpers/mock_lock_file';
|
import {MockLockFile} from '../helpers/mock_lock_file';
|
||||||
import {MockLogger} from '../helpers/mock_logger';
|
import {MockLogger} from '../helpers/mock_logger';
|
||||||
|
|
||||||
|
|
||||||
describe('SingleProcessExecutor', () => {
|
describe('SingleProcessExecutor', () => {
|
||||||
let mockLogger: MockLogger;
|
let mockLogger: MockLogger;
|
||||||
let mockLockFile: MockLockFileSync;
|
let lockFileLog: string[];
|
||||||
|
let mockLockFile: MockLockFile;
|
||||||
|
let locker: SyncLocker;
|
||||||
let executor: SingleProcessExecutorSync;
|
let executor: SingleProcessExecutorSync;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockLogger = new MockLogger();
|
mockLogger = new MockLogger();
|
||||||
mockLockFile = new MockLockFileSync();
|
lockFileLog = [];
|
||||||
executor = new SingleProcessExecutorSync(
|
mockLockFile = new MockLockFile(new MockFileSystemNative(), lockFileLog);
|
||||||
mockLogger, null as unknown as PackageJsonUpdater, mockLockFile);
|
locker = new SyncLocker(mockLockFile);
|
||||||
|
executor =
|
||||||
|
new SingleProcessExecutorSync(mockLogger, null as unknown as PackageJsonUpdater, locker);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('execute()', () => {
|
describe('execute()', () => {
|
||||||
it('should call LockFile.create() and LockFile.remove() if processing completes successfully',
|
it('should call LockFile.write() and LockFile.remove() if processing completes successfully',
|
||||||
() => {
|
() => {
|
||||||
const noTasks = () => new SerialTaskQueue([] as any);
|
const noTasks = () => new SerialTaskQueue([] as any);
|
||||||
const createCompileFn: () => any = () => undefined;
|
const createCompileFn: () => any = () => undefined;
|
||||||
executor.execute(noTasks, createCompileFn);
|
executor.execute(noTasks, createCompileFn);
|
||||||
expect(mockLockFile.log).toEqual(['create()', 'remove()']);
|
expect(lockFileLog).toEqual(['write()', 'remove()']);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call LockFile.create() and LockFile.remove() if `analyzeEntryPoints` fails', () => {
|
it('should call LockFile.write() and LockFile.remove() if `analyzeEntryPoints` fails', () => {
|
||||||
const errorFn: () => never = () => { throw new Error('analyze error'); };
|
const errorFn: () => never = () => { throw new Error('analyze error'); };
|
||||||
const createCompileFn: () => any = () => undefined;
|
const createCompileFn: () => any = () => undefined;
|
||||||
let error: string = '';
|
let error: string = '';
|
||||||
|
@ -46,10 +52,10 @@ describe('SingleProcessExecutor', () => {
|
||||||
error = e.message;
|
error = e.message;
|
||||||
}
|
}
|
||||||
expect(error).toEqual('analyze error');
|
expect(error).toEqual('analyze error');
|
||||||
expect(mockLockFile.log).toEqual(['create()', 'remove()']);
|
expect(lockFileLog).toEqual(['write()', 'remove()']);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call LockFile.create() and LockFile.remove() if `createCompileFn` fails', () => {
|
it('should call LockFile.write() and LockFile.remove() if `createCompileFn` fails', () => {
|
||||||
const oneTask = () => new SerialTaskQueue([{}] as any);
|
const oneTask = () => new SerialTaskQueue([{}] as any);
|
||||||
const createErrorCompileFn: () => any = () => { throw new Error('compile error'); };
|
const createErrorCompileFn: () => any = () => { throw new Error('compile error'); };
|
||||||
let error: string = '';
|
let error: string = '';
|
||||||
|
@ -59,31 +65,39 @@ describe('SingleProcessExecutor', () => {
|
||||||
error = e.message;
|
error = e.message;
|
||||||
}
|
}
|
||||||
expect(error).toEqual('compile error');
|
expect(error).toEqual('compile error');
|
||||||
expect(mockLockFile.log).toEqual(['create()', 'remove()']);
|
expect(lockFileLog).toEqual(['write()', 'remove()']);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not call `analyzeEntryPoints` if Lockfile.create() fails', () => {
|
it('should not call `analyzeEntryPoints` if LockFile.write() fails', () => {
|
||||||
const lockFile = new MockLockFileSync({throwOnCreate: true});
|
spyOn(mockLockFile, 'write').and.callFake(() => {
|
||||||
const analyzeFn: () => any = () => { lockFile.log.push('analyzeFn'); };
|
lockFileLog.push('write()');
|
||||||
|
throw new Error('LockFile.write() error');
|
||||||
|
});
|
||||||
|
|
||||||
|
const analyzeFn: () => any = () => { lockFileLog.push('analyzeFn'); };
|
||||||
const anyFn: () => any = () => undefined;
|
const anyFn: () => any = () => undefined;
|
||||||
executor = new SingleProcessExecutorSync(
|
executor =
|
||||||
mockLogger, null as unknown as PackageJsonUpdater, lockFile);
|
new SingleProcessExecutorSync(mockLogger, null as unknown as PackageJsonUpdater, locker);
|
||||||
let error = '';
|
let error = '';
|
||||||
try {
|
try {
|
||||||
executor.execute(analyzeFn, anyFn);
|
executor.execute(analyzeFn, anyFn);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error = e.message;
|
error = e.message;
|
||||||
}
|
}
|
||||||
expect(error).toEqual('LockFile.create() error');
|
expect(error).toEqual('LockFile.write() error');
|
||||||
expect(lockFile.log).toEqual(['create()']);
|
expect(lockFileLog).toEqual(['write()']);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should fail if Lockfile.remove() fails', () => {
|
it('should fail if LockFile.remove() fails', () => {
|
||||||
const noTasks = () => new SerialTaskQueue([] as any);
|
const noTasks = () => new SerialTaskQueue([] as any);
|
||||||
const anyFn: () => any = () => undefined;
|
const anyFn: () => any = () => undefined;
|
||||||
const lockFile = new MockLockFileSync({throwOnRemove: true});
|
spyOn(mockLockFile, 'remove').and.callFake(() => {
|
||||||
executor = new SingleProcessExecutorSync(
|
lockFileLog.push('remove()');
|
||||||
mockLogger, null as unknown as PackageJsonUpdater, lockFile);
|
throw new Error('LockFile.remove() error');
|
||||||
|
});
|
||||||
|
|
||||||
|
executor =
|
||||||
|
new SingleProcessExecutorSync(mockLogger, null as unknown as PackageJsonUpdater, locker);
|
||||||
let error = '';
|
let error = '';
|
||||||
try {
|
try {
|
||||||
executor.execute(noTasks, anyFn);
|
executor.execute(noTasks, anyFn);
|
||||||
|
@ -91,7 +105,7 @@ describe('SingleProcessExecutor', () => {
|
||||||
error = e.message;
|
error = e.message;
|
||||||
}
|
}
|
||||||
expect(error).toEqual('LockFile.remove() error');
|
expect(error).toEqual('LockFile.remove() error');
|
||||||
expect(lockFile.log).toEqual(['create()', 'remove()']);
|
expect(lockFileLog).toEqual(['write()', 'remove()']);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -5,40 +5,20 @@
|
||||||
* Use of this source code is governed by an MIT-style license that can be
|
* 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
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
import {MockFileSystemNative} from '../../../src/ngtsc/file_system/testing';
|
import {FileSystem} from '../../../src/ngtsc/file_system';
|
||||||
import {LockFileAsync, LockFileSync} from '../../src/execution/lock_file';
|
import {LockFile} from '../../src/execution/lock_file';
|
||||||
import {MockLogger} from './mock_logger';
|
|
||||||
|
|
||||||
export class MockLockFileSync extends LockFileSync {
|
/**
|
||||||
log: string[] = [];
|
* A mock implementation of `LockFile` that just logs its calls.
|
||||||
constructor(private options: {throwOnCreate?: boolean, throwOnRemove?: boolean} = {}) {
|
*/
|
||||||
// This `MockLockFile` is not used in tests that are run via `runInEachFileSystem()`
|
export class MockLockFile implements LockFile {
|
||||||
// So we cannot use `getFileSystem()` but instead just instantiate a mock file-system.
|
constructor(
|
||||||
super(new MockFileSystemNative());
|
fs: FileSystem, private log: string[] = [], public path = fs.resolve('/lockfile'),
|
||||||
}
|
private pid = '1234') {}
|
||||||
create() {
|
write() { this.log.push('write()'); }
|
||||||
this.log.push('create()');
|
read(): string {
|
||||||
if (this.options.throwOnCreate) throw new Error('LockFile.create() error');
|
this.log.push('read()');
|
||||||
}
|
return this.pid;
|
||||||
remove() {
|
|
||||||
this.log.push('remove()');
|
|
||||||
if (this.options.throwOnRemove) throw new Error('LockFile.remove() error');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class MockLockFileAsync extends LockFileAsync {
|
|
||||||
log: string[] = [];
|
|
||||||
constructor(private options: {throwOnCreate?: boolean, throwOnRemove?: boolean} = {}) {
|
|
||||||
// This `MockLockFile` is not used in tests that are run via `runInEachFileSystem()`
|
|
||||||
// So we cannot use `getFileSystem()` but instead just instantiate a mock file-system.
|
|
||||||
super(new MockFileSystemNative(), new MockLogger(), 200, 2);
|
|
||||||
}
|
|
||||||
async create() {
|
|
||||||
this.log.push('create()');
|
|
||||||
if (this.options.throwOnCreate) throw new Error('LockFile.create() error');
|
|
||||||
}
|
|
||||||
remove() {
|
|
||||||
this.log.push('remove()');
|
|
||||||
if (this.options.throwOnRemove) throw new Error('LockFile.remove() error');
|
|
||||||
}
|
}
|
||||||
|
remove() { this.log.push('remove()'); }
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,7 @@ import * as os from 'os';
|
||||||
import {AbsoluteFsPath, FileSystem, absoluteFrom, getFileSystem, join} from '../../../src/ngtsc/file_system';
|
import {AbsoluteFsPath, FileSystem, absoluteFrom, getFileSystem, join} from '../../../src/ngtsc/file_system';
|
||||||
import {Folder, MockFileSystem, TestFile, runInEachFileSystem} from '../../../src/ngtsc/file_system/testing';
|
import {Folder, MockFileSystem, TestFile, runInEachFileSystem} from '../../../src/ngtsc/file_system/testing';
|
||||||
import {loadStandardTestFiles, loadTestFiles} from '../../../test/helpers';
|
import {loadStandardTestFiles, loadTestFiles} from '../../../test/helpers';
|
||||||
import {LockFileSync} from '../../src/execution/lock_file';
|
import {getLockFilePath} from '../../src/execution/lock_file';
|
||||||
import {mainNgcc} from '../../src/main';
|
import {mainNgcc} from '../../src/main';
|
||||||
import {markAsProcessed} from '../../src/packages/build_marker';
|
import {markAsProcessed} from '../../src/packages/build_marker';
|
||||||
import {EntryPointJsonProperty, EntryPointPackageJson, SUPPORTED_FORMAT_PROPERTIES} from '../../src/packages/entry_point';
|
import {EntryPointJsonProperty, EntryPointPackageJson, SUPPORTED_FORMAT_PROPERTIES} from '../../src/packages/entry_point';
|
||||||
|
@ -1540,7 +1540,7 @@ runInEachFileSystem(() => {
|
||||||
function initMockFileSystem(fs: FileSystem, testFiles: Folder) {
|
function initMockFileSystem(fs: FileSystem, testFiles: Folder) {
|
||||||
if (fs instanceof MockFileSystem) {
|
if (fs instanceof MockFileSystem) {
|
||||||
fs.init(testFiles);
|
fs.init(testFiles);
|
||||||
fs.ensureDir(fs.dirname(new LockFileSync(fs).lockFilePath));
|
fs.ensureDir(fs.dirname(getLockFilePath(fs)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// a random test package that no metadata.json file so not compiled by Angular.
|
// a random test package that no metadata.json file so not compiled by Angular.
|
||||||
|
|
Loading…
Reference in New Issue