diff --git a/packages/compiler-cli/ngcc/src/execution/cluster/executor.ts b/packages/compiler-cli/ngcc/src/execution/cluster/executor.ts index c601b889a6..1edd161488 100644 --- a/packages/compiler-cli/ngcc/src/execution/cluster/executor.ts +++ b/packages/compiler-cli/ngcc/src/execution/cluster/executor.ts @@ -13,7 +13,7 @@ import * as cluster from 'cluster'; import {Logger} from '../../logging/logger'; import {PackageJsonUpdater} from '../../writing/package_json_updater'; import {AnalyzeEntryPointsFn, CreateCompileFn, Executor} from '../api'; -import {LockFileAsync} from '../lock_file'; +import {AsyncLocker} from '../lock_file'; import {ClusterMaster} from './master'; import {ClusterWorker} from './worker'; @@ -26,7 +26,7 @@ import {ClusterWorker} from './worker'; export class ClusterExecutor implements Executor { constructor( private workerCount: number, private logger: Logger, - private pkgJsonUpdater: PackageJsonUpdater, private lockFile: LockFileAsync) {} + private pkgJsonUpdater: PackageJsonUpdater, private lockFile: AsyncLocker) {} async execute(analyzeEntryPoints: AnalyzeEntryPointsFn, createCompileFn: CreateCompileFn): Promise { diff --git a/packages/compiler-cli/ngcc/src/execution/lock_file.ts b/packages/compiler-cli/ngcc/src/execution/lock_file.ts index a9a98d209a..ecb0fd0daa 100644 --- a/packages/compiler-cli/ngcc/src/execution/lock_file.ts +++ b/packages/compiler-cli/ngcc/src/execution/lock_file.ts @@ -7,59 +7,80 @@ */ 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'; -export abstract class LockFileBase { - lockFilePath = - this.fs.resolve(require.resolve('@angular/compiler-cli/ngcc'), '../__ngcc_lock_file__'); +let _lockFilePath: AbsoluteFsPath; +export function getLockFilePath(fs: FileSystem) { + 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) {} - protected writeLockFile(): void { + path = getLockFilePath(this.fs); + + write(): void { try { this.addSignalHandlers(); - // To avoid race conditions, we check for existence of the lockfile - // by actually trying to create it exclusively. - return this.fs.writeFile(this.lockFilePath, process.pid.toString(), /* exclusive */ true); + // To avoid race conditions, we check for existence of the lockFile by actually trying to + // create it exclusively. + return this.fs.writeFile(this.path, process.pid.toString(), /* exclusive */ true); } catch (e) { this.removeSignalHandlers(); throw e; } } - /** - * 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 { + read(): string { try { if (this.fs instanceof CachedFileSystem) { // This file is "volatile", it might be changed by an external process, // 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 { return '{unknown}'; } } - /** - * Remove the lock file from disk. - */ - protected remove() { + remove() { this.removeSignalHandlers(); - if (this.fs.exists(this.lockFilePath)) { - this.fs.removeFile(this.lockFilePath); + if (this.fs.exists(this.path)) { + this.fs.removeFile(this.path); } } /** * 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() { 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 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. * * 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. * @@ -113,7 +136,7 @@ export class LockFileSync extends LockFileBase { try { return fn(); } finally { - this.remove(); + this.lockFile.remove(); } } @@ -122,7 +145,7 @@ export class LockFileSync extends LockFileBase { */ protected create(): void { try { - this.writeLockFile(); + this.lockFile.write(); } catch (e) { if (e.code !== 'EEXIST') { 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 { - const pid = this.readLockFile(); + const pid = this.lockFile.read(); throw new Error( `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` + `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 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. * * When ngcc completes executing, it removes the file so that future ngcc executions can start. */ -export class LockFileAsync extends LockFileBase { +export class AsyncLocker { constructor( - fs: FileSystem, protected logger: Logger, private retryDelay: number, - private retryAttempts: number) { - super(fs); - } + private lockFile: LockFile, protected logger: Logger, private retryDelay: number, + private retryAttempts: number) {} /** * Run a function guarded by the lock file. @@ -169,19 +190,19 @@ export class LockFileAsync extends LockFileBase { */ async lock(fn: () => Promise): Promise { await this.create(); - return fn().finally(() => this.remove()); + return fn().finally(() => this.lockFile.remove()); } protected async create() { let pid: string = ''; for (let attempts = 0; attempts < this.retryAttempts; attempts++) { try { - return this.writeLockFile(); + return this.lockFile.write(); } catch (e) { if (e.code !== 'EEXIST') { throw e; } - const newPid = this.readLockFile(); + const newPid = this.lockFile.read(); if (newPid !== pid) { // The process locking the file has changed, so restart the timeout 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 throw new Error( `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}.)`); } } diff --git a/packages/compiler-cli/ngcc/src/execution/single_process_executor.ts b/packages/compiler-cli/ngcc/src/execution/single_process_executor.ts index 29775ae068..4f6992a859 100644 --- a/packages/compiler-cli/ngcc/src/execution/single_process_executor.ts +++ b/packages/compiler-cli/ngcc/src/execution/single_process_executor.ts @@ -10,7 +10,7 @@ import {Logger} from '../logging/logger'; import {PackageJsonUpdater} from '../writing/package_json_updater'; import {AnalyzeEntryPointsFn, CreateCompileFn, Executor} from './api'; -import {LockFileAsync, LockFileSync} from './lock_file'; +import {AsyncLocker, SyncLocker} from './lock_file'; import {onTaskCompleted} from './utils'; export abstract class SingleProcessorExecutorBase { @@ -43,11 +43,11 @@ export abstract class SingleProcessorExecutorBase { * An `Executor` that processes all tasks serially and completes synchronously. */ 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); } 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. */ 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); } async execute(analyzeEntryPoints: AnalyzeEntryPointsFn, createCompileFn: CreateCompileFn): Promise { - await this.lockfile.lock(async() => this.doExecute(analyzeEntryPoints, createCompileFn)); + await this.lockFile.lock(async() => this.doExecute(analyzeEntryPoints, createCompileFn)); } } diff --git a/packages/compiler-cli/ngcc/src/main.ts b/packages/compiler-cli/ngcc/src/main.ts index 135b2ae812..baacf964e3 100644 --- a/packages/compiler-cli/ngcc/src/main.ts +++ b/packages/compiler-cli/ngcc/src/main.ts @@ -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 {ClusterExecutor} from './execution/cluster/executor'; 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 {ParallelTaskQueue} from './execution/task_selection/parallel_task_queue'; import {SerialTaskQueue} from './execution/task_selection/serial_task_queue'; @@ -336,20 +336,21 @@ function getTaskQueue( function getExecutor( async: boolean, inParallel: boolean, logger: Logger, pkgJsonUpdater: PackageJsonUpdater, fileSystem: FileSystem): Executor { + const lockFile = new LockFileWithSignalHandlers(fileSystem); if (async) { // 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) { // 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); - return new ClusterExecutor(workerCount, logger, pkgJsonUpdater, lockFile); + return new ClusterExecutor(workerCount, logger, pkgJsonUpdater, locker); } else { // Execute serially, on a single thread (async). - return new SingleProcessExecutorAsync(logger, pkgJsonUpdater, lockFile); + return new SingleProcessExecutorAsync(logger, pkgJsonUpdater, locker); } } else { // Execute serially, on a single thread (sync). - return new SingleProcessExecutorSync(logger, pkgJsonUpdater, new LockFileSync(fileSystem)); + return new SingleProcessExecutorSync(logger, pkgJsonUpdater, new SyncLocker(lockFile)); } } diff --git a/packages/compiler-cli/ngcc/test/execution/cluster/executor_spec.ts b/packages/compiler-cli/ngcc/test/execution/cluster/executor_spec.ts index d42d62479c..20a8840b4b 100644 --- a/packages/compiler-cli/ngcc/test/execution/cluster/executor_spec.ts +++ b/packages/compiler-cli/ngcc/test/execution/cluster/executor_spec.ts @@ -10,11 +10,13 @@ import * as cluster from 'cluster'; +import {MockFileSystemNative} from '../../../../src/ngtsc/file_system/testing'; import {ClusterExecutor} from '../../../src/execution/cluster/executor'; import {ClusterMaster} from '../../../src/execution/cluster/master'; import {ClusterWorker} from '../../../src/execution/cluster/worker'; +import {AsyncLocker} from '../../../src/execution/lock_file'; 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 {mockProperty} from '../../helpers/spy_utils'; @@ -24,7 +26,9 @@ describe('ClusterExecutor', () => { let masterRunSpy: jasmine.Spy; let workerRunSpy: jasmine.Spy; let mockLogger: MockLogger; - let mockLockFile: MockLockFileAsync; + let lockFileLog: string[]; + let mockLockFile: MockLockFile; + let locker: AsyncLocker; let executor: ClusterExecutor; beforeEach(() => { @@ -34,9 +38,10 @@ describe('ClusterExecutor', () => { .and.returnValue(Promise.resolve('CusterWorker#run()')); mockLogger = new MockLogger(); - mockLockFile = new MockLockFileAsync(); - executor = - new ClusterExecutor(42, mockLogger, null as unknown as PackageJsonUpdater, mockLockFile); + lockFileLog = []; + mockLockFile = new MockLockFile(new MockFileSystemNative(), lockFileLog); + locker = new AsyncLocker(mockLockFile, mockLogger, 200, 2); + executor = new ClusterExecutor(42, mockLogger, null as unknown as PackageJsonUpdater, locker); }); describe('execute()', () => { @@ -66,14 +71,14 @@ describe('ClusterExecutor', () => { 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() => { const anyFn: () => any = () => undefined; 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; masterRunSpy.and.returnValue(Promise.reject(new Error('master runner error'))); let error = ''; @@ -83,30 +88,37 @@ describe('ClusterExecutor', () => { error = e.message; } 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 lockFile = new MockLockFileAsync({throwOnCreate: true}); + spyOn(mockLockFile, 'write').and.callFake(() => { + lockFileLog.push('write()'); + throw new Error('LockFile.write() error'); + }); + executor = - new ClusterExecutor(42, mockLogger, null as unknown as PackageJsonUpdater, lockFile); + new ClusterExecutor(42, mockLogger, null as unknown as PackageJsonUpdater, locker); let error = ''; try { await executor.execute(anyFn, anyFn); } catch (e) { error = e.message; } - expect(error).toEqual('LockFile.create() error'); - expect(lockFile.log).toEqual(['create()']); + expect(error).toEqual('LockFile.write() error'); 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 lockFile = new MockLockFileAsync({throwOnRemove: true}); + spyOn(mockLockFile, 'remove').and.callFake(() => { + lockFileLog.push('remove()'); + throw new Error('LockFile.remove() error'); + }); + executor = - new ClusterExecutor(42, mockLogger, null as unknown as PackageJsonUpdater, lockFile); + new ClusterExecutor(42, mockLogger, null as unknown as PackageJsonUpdater, locker); let error = ''; try { await executor.execute(anyFn, anyFn); @@ -114,7 +126,7 @@ describe('ClusterExecutor', () => { error = e.message; } expect(error).toEqual('LockFile.remove() error'); - expect(lockFile.log).toEqual(['create()', 'remove()']); + expect(lockFileLog).toEqual(['write()', 'remove()']); expect(masterRunSpy).toHaveBeenCalled(); }); }); @@ -143,10 +155,10 @@ describe('ClusterExecutor', () => { 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; await executor.execute(anyFn, anyFn); - expect(mockLockFile.log).toEqual([]); + expect(lockFileLog).toEqual([]); }); }); }); diff --git a/packages/compiler-cli/ngcc/test/execution/lock_file_spec.ts b/packages/compiler-cli/ngcc/test/execution/lock_file_spec.ts index 3bfbe29068..8a3c1258b6 100644 --- a/packages/compiler-cli/ngcc/test/execution/lock_file_spec.ts +++ b/packages/compiler-cli/ngcc/test/execution/lock_file_spec.ts @@ -9,29 +9,41 @@ import * as process from 'process'; import {CachedFileSystem, FileSystem, getFileSystem} from '../../../src/ngtsc/file_system'; 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'; 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[] = []; constructor(fs: FileSystem, private handleSignals = false) { 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() { this.log.push('addSignalHandlers()'); if (this.handleSignals) { super.addSignalHandlers(); } } - writeLockFile() { super.writeLockFile(); } - readLockFile() { return super.readLockFile(); } + write() { + this.log.push('write()'); + super.write(); + } + read() { + const contents = super.read(); + this.log.push('read() => ' + contents); + return contents; + } removeSignalHandlers() { this.log.push('removeSignalHandlers()'); super.removeSignalHandlers(); @@ -39,45 +51,91 @@ runInEachFileSystem(() => { exit(code: number) { this.log.push(`exit(${code})`); } } - describe('writeLockFile()', () => { + describe('write()', () => { it('should call `addSignalHandlers()`', () => { const fs = getFileSystem(); const lockFile = new LockFileUnderTest(fs); - lockFile.writeLockFile(); - expect(lockFile.log).toEqual(['addSignalHandlers()']); + lockFile.write(); + expect(lockFile.log).toEqual(['write()', 'addSignalHandlers()']); }); it('should call `removeSignalHandlers()` if there is an error', () => { const fs = getFileSystem(); spyOn(fs, 'writeFile').and.throwError('WRITING ERROR'); const lockFile = new LockFileUnderTest(fs); - expect(() => lockFile.writeLockFile()).toThrowError('WRITING ERROR'); - expect(lockFile.log).toEqual(['addSignalHandlers()', 'removeSignalHandlers()']); + expect(() => lockFile.write()).toThrowError('WRITING ERROR'); + 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()', () => { - it('should return the contents of the lockfile', () => { + describe('read()', () => { + it('should return the contents of the lockFile', () => { const fs = getFileSystem(); const lockFile = new LockFileUnderTest(fs); - fs.writeFile(lockFile.lockFilePath, '188'); - expect(lockFile.readLockFile()).toEqual('188'); + fs.writeFile(lockFile.path, '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 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()', () => { it('should remove the lock file from the file-system', () => { const fs = getFileSystem(); const lockFile = new LockFileUnderTest(fs); - fs.writeFile(lockFile.lockFilePath, '188'); + fs.writeFile(lockFile.path, '188'); 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', () => { @@ -89,298 +147,219 @@ runInEachFileSystem(() => { it('should call removeSignalHandlers()', () => { const fs = getFileSystem(); const lockFile = new LockFileUnderTest(fs); - fs.writeFile(lockFile.lockFilePath, '188'); + fs.writeFile(lockFile.path, '188'); lockFile.remove(); - expect(lockFile.log).toEqual(['removeSignalHandlers()']); + expect(lockFile.log).toEqual(['remove()', 'removeSignalHandlers()']); }); }); }); - describe('LockFileSync', () => { - /** - * 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('SyncLocker', () => { 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 lockFile = new LockFileUnderTest(fs); + const log: string[] = []; + const lockFile = new MockLockFile(fs, log); + const locker = new SyncLocker(lockFile); - lockFile.lock(() => lockFile.log.push('fn()')); - expect(lockFile.log).toEqual(['create()', 'fn()', 'remove()']); + locker.lock(() => log.push('fn()')); + + 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 = ''; const fs = getFileSystem(); - const lockFile = new LockFileUnderTest(fs); + const log: string[] = []; + const lockFile = new MockLockFile(fs, log); + const locker = new SyncLocker(lockFile); try { - lockFile.lock(() => { - lockFile.log.push('fn()'); + locker.lock(() => { + log.push('fn()'); throw new Error('ERROR'); }); } catch (e) { error = e.message; } 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', () => { const fs = getFileSystem(); - const lockFile = new LockFileUnderTest(fs); - fs.writeFile(lockFile.lockFilePath, '188'); - expect(() => lockFile.create()) + const log: string[] = []; + const lockFile = new MockLockFile(fs, log); + const locker = new SyncLocker(lockFile); + + spyOn(lockFile, 'write').and.callFake(() => { throw {code: 'EEXIST'}; }); + spyOn(lockFile, 'read').and.returnValue('188'); + + expect(() => locker.lock(() => {})) .toThrowError( `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` + `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', () => { - /** - * 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('AsyncLocker', () => { 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 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() => { - lockFile.log.push('fn() - before'); + await locker.lock(async() => { + 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'); + log.push('fn() - after'); 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() => { let error: string = ''; 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 { - await lockFile.lock(async() => { - lockFile.log.push('fn()'); + await locker.lock(async() => { + log.push('fn()'); throw new Error('ERROR'); }); } catch (e) { error = e.message; } 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() => { const fs = getFileSystem(); - const lockFile = new LockFileUnderTest(fs); - fs.writeFile(lockFile.lockFilePath, '188'); - 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. - expect(lockFile.log).toEqual(['create()']); - expect(lockFile.getLogger().logs.info).toEqual([[ + const log: string[] = []; + const lockFile = new MockLockFile(fs, log); + const logger = new MockLogger(); + const locker = new AsyncLocker(lockFile, logger, 100, 10); + + 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.' ]]); - fs.removeFile(lockFile.lockFilePath); - // The lockfile has been removed, so we can create our own lockfile, call `fn()` and then - // remove the lockfile. + + lockFileContents = null; + // The lockFile has been removed, so we can create our own lockFile, call `fn()` and then + // remove the lockFile. 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() => { - // Use a cached file system to test that we are invalidating it correctly - const rawFs = getFileSystem(); - const fs = new CachedFileSystem(rawFs); - const lockFile = new LockFileUnderTest(fs); - fs.writeFile(lockFile.lockFilePath, '188'); - 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. - expect(lockFile.log).toEqual(['create()']); - expect(lockFile.getLogger().logs.info).toEqual([[ - 'Another process, with id 188, is currently running ngcc.\nWaiting up to 1s for it to finish.' - ]]); - // 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)); - expect(lockFile.getLogger().logs.info).toEqual([ - [ + const fs = getFileSystem(); + const log: string[] = []; + const lockFile = new MockLockFile(fs, log); + const logger = new MockLogger(); + const locker = new AsyncLocker(lockFile, logger, 100, 10); + + 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; + }); + + 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 444, 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 - // remove the lockfile. - await promise; - expect(lockFile.log).toEqual(['create()', 'fn()', 'remove()']); + ]]); + + lockFileContents = '444'; + // The lockFile has been taken over by another process + await new Promise(resolve => setTimeout(resolve, 250)); + expect(log).toEqual(['write()', 'read() => 188', 'write()', 'read() => 444']); + 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 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() => { const fs = getFileSystem(); - const lockFile = new LockFileUnderTest(fs, 100, 2); - fs.writeFile(lockFile.lockFilePath, '188'); - 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. - expect(lockFile.log).toEqual(['create()']); - // Do not remove the lockfile and let the call to `lock()` timeout. + const log: string[] = []; + const lockFile = new MockLockFile(fs, log); + const logger = new MockLogger(); + const locker = new AsyncLocker(lockFile, logger, 100, 2); + + 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; await promise.catch(e => error = e); - expect(lockFile.log).toEqual(['create()']); + expect(log).toEqual(['write()', 'read() => 188', 'write()', 'read() => 188']); expect(error !.message) .toEqual( `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}.)`); }); }); }); diff --git a/packages/compiler-cli/ngcc/test/execution/single_processor_executor_spec.ts b/packages/compiler-cli/ngcc/test/execution/single_processor_executor_spec.ts index 6ae1473124..4fa3c4d4e8 100644 --- a/packages/compiler-cli/ngcc/test/execution/single_processor_executor_spec.ts +++ b/packages/compiler-cli/ngcc/test/execution/single_processor_executor_spec.ts @@ -8,35 +8,41 @@ /// +import {MockFileSystemNative} from '../../../src/ngtsc/file_system/testing'; +import {SyncLocker} from '../../src/execution/lock_file'; import {SingleProcessExecutorSync} from '../../src/execution/single_process_executor'; import {SerialTaskQueue} from '../../src/execution/task_selection/serial_task_queue'; 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'; describe('SingleProcessExecutor', () => { let mockLogger: MockLogger; - let mockLockFile: MockLockFileSync; + let lockFileLog: string[]; + let mockLockFile: MockLockFile; + let locker: SyncLocker; let executor: SingleProcessExecutorSync; beforeEach(() => { mockLogger = new MockLogger(); - mockLockFile = new MockLockFileSync(); - executor = new SingleProcessExecutorSync( - mockLogger, null as unknown as PackageJsonUpdater, mockLockFile); + lockFileLog = []; + mockLockFile = new MockLockFile(new MockFileSystemNative(), lockFileLog); + locker = new SyncLocker(mockLockFile); + executor = + new SingleProcessExecutorSync(mockLogger, null as unknown as PackageJsonUpdater, locker); }); 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 createCompileFn: () => any = () => undefined; 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 createCompileFn: () => any = () => undefined; let error: string = ''; @@ -46,10 +52,10 @@ describe('SingleProcessExecutor', () => { error = e.message; } 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 createErrorCompileFn: () => any = () => { throw new Error('compile error'); }; let error: string = ''; @@ -59,31 +65,39 @@ describe('SingleProcessExecutor', () => { error = e.message; } 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', () => { - const lockFile = new MockLockFileSync({throwOnCreate: true}); - const analyzeFn: () => any = () => { lockFile.log.push('analyzeFn'); }; + it('should not call `analyzeEntryPoints` if LockFile.write() fails', () => { + spyOn(mockLockFile, 'write').and.callFake(() => { + lockFileLog.push('write()'); + throw new Error('LockFile.write() error'); + }); + + const analyzeFn: () => any = () => { lockFileLog.push('analyzeFn'); }; const anyFn: () => any = () => undefined; - executor = new SingleProcessExecutorSync( - mockLogger, null as unknown as PackageJsonUpdater, lockFile); + executor = + new SingleProcessExecutorSync(mockLogger, null as unknown as PackageJsonUpdater, locker); let error = ''; try { executor.execute(analyzeFn, anyFn); } catch (e) { error = e.message; } - expect(error).toEqual('LockFile.create() error'); - expect(lockFile.log).toEqual(['create()']); + expect(error).toEqual('LockFile.write() error'); + 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 anyFn: () => any = () => undefined; - const lockFile = new MockLockFileSync({throwOnRemove: true}); - executor = new SingleProcessExecutorSync( - mockLogger, null as unknown as PackageJsonUpdater, lockFile); + spyOn(mockLockFile, 'remove').and.callFake(() => { + lockFileLog.push('remove()'); + throw new Error('LockFile.remove() error'); + }); + + executor = + new SingleProcessExecutorSync(mockLogger, null as unknown as PackageJsonUpdater, locker); let error = ''; try { executor.execute(noTasks, anyFn); @@ -91,7 +105,7 @@ describe('SingleProcessExecutor', () => { error = e.message; } expect(error).toEqual('LockFile.remove() error'); - expect(lockFile.log).toEqual(['create()', 'remove()']); + expect(lockFileLog).toEqual(['write()', 'remove()']); }); }); }); diff --git a/packages/compiler-cli/ngcc/test/helpers/mock_lock_file.ts b/packages/compiler-cli/ngcc/test/helpers/mock_lock_file.ts index 873bebf2b7..a630265d8a 100644 --- a/packages/compiler-cli/ngcc/test/helpers/mock_lock_file.ts +++ b/packages/compiler-cli/ngcc/test/helpers/mock_lock_file.ts @@ -5,40 +5,20 @@ * 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 {MockFileSystemNative} from '../../../src/ngtsc/file_system/testing'; -import {LockFileAsync, LockFileSync} from '../../src/execution/lock_file'; -import {MockLogger} from './mock_logger'; +import {FileSystem} from '../../../src/ngtsc/file_system'; +import {LockFile} from '../../src/execution/lock_file'; -export class MockLockFileSync extends LockFileSync { - 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()); - } - 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'); - } -} - -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'); +/** + * A mock implementation of `LockFile` that just logs its calls. + */ +export class MockLockFile implements LockFile { + constructor( + fs: FileSystem, private log: string[] = [], public path = fs.resolve('/lockfile'), + private pid = '1234') {} + write() { this.log.push('write()'); } + read(): string { + this.log.push('read()'); + return this.pid; } + remove() { this.log.push('remove()'); } } diff --git a/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts b/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts index 721519c350..7861ea16a1 100644 --- a/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts +++ b/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts @@ -13,7 +13,7 @@ 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'; -import {LockFileSync} from '../../src/execution/lock_file'; +import {getLockFilePath} from '../../src/execution/lock_file'; import {mainNgcc} from '../../src/main'; import {markAsProcessed} from '../../src/packages/build_marker'; import {EntryPointJsonProperty, EntryPointPackageJson, SUPPORTED_FORMAT_PROPERTIES} from '../../src/packages/entry_point'; @@ -1540,7 +1540,7 @@ runInEachFileSystem(() => { function initMockFileSystem(fs: FileSystem, testFiles: Folder) { if (fs instanceof MockFileSystem) { 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.