2020-01-10 09:54:58 +00:00
/ * *
* @license
* Copyright Google Inc . All Rights Reserved .
*
* Use of this source code is governed by an MIT - style license that can be
* found in the LICENSE file at https : //angular.io/license
* /
import * as process from 'process' ;
2020-02-03 20:25:15 +00:00
import { CachedFileSystem , FileSystem , getFileSystem } from '../../../src/ngtsc/file_system' ;
2020-01-10 09:54:58 +00:00
import { runInEachFileSystem } from '../../../src/ngtsc/file_system/testing' ;
2020-02-03 20:25:15 +00:00
import { LockFileAsync , LockFileBase , LockFileSync } from '../../src/execution/lock_file' ;
import { MockLogger } from '../helpers/mock_logger' ;
2020-01-10 09:54:58 +00:00
2020-02-03 20:25:15 +00:00
runInEachFileSystem ( ( ) = > {
describe ( 'LockFileBase' , ( ) = > {
/ * *
* This class allows us to test the abstract class LockFileBase .
* /
class LockFileUnderTest extends LockFileBase {
log : string [ ] = [ ] ;
constructor ( fs : FileSystem , private handleSignals = false ) {
super ( fs ) ;
fs . ensureDir ( fs . dirname ( this . lockFilePath ) ) ;
}
remove() { super . remove ( ) ; }
addSignalHandlers() {
this . log . push ( 'addSignalHandlers()' ) ;
if ( this . handleSignals ) {
super . addSignalHandlers ( ) ;
}
}
writeLockFile() { super . writeLockFile ( ) ; }
readLockFile() { return super . readLockFile ( ) ; }
removeSignalHandlers() {
this . log . push ( 'removeSignalHandlers()' ) ;
super . removeSignalHandlers ( ) ;
}
exit ( code : number ) { this . log . push ( ` exit( ${ code } ) ` ) ; }
2020-01-10 09:54:58 +00:00
}
2020-02-03 20:25:15 +00:00
describe ( 'writeLockFile()' , ( ) = > {
it ( 'should call `addSignalHandlers()`' , ( ) = > {
const fs = getFileSystem ( ) ;
const lockFile = new LockFileUnderTest ( fs ) ;
lockFile . writeLockFile ( ) ;
expect ( lockFile . log ) . toEqual ( [ '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()' ] ) ;
} ) ;
} ) ;
describe ( 'readLockFile()' , ( ) = > {
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' ) ;
} ) ;
it ( 'should return `{unknown}` if the lockfile does not exist' , ( ) = > {
const fs = getFileSystem ( ) ;
const lockFile = new LockFileUnderTest ( fs ) ;
expect ( lockFile . readLockFile ( ) ) . toEqual ( '{unknown}' ) ;
} ) ;
} ) ;
describe ( 'remove()' , ( ) = > {
it ( 'should remove the lock file from the file-system' , ( ) = > {
const fs = getFileSystem ( ) ;
const lockFile = new LockFileUnderTest ( fs ) ;
fs . writeFile ( lockFile . lockFilePath , '188' ) ;
lockFile . remove ( ) ;
expect ( fs . exists ( lockFile . lockFilePath ) ) . toBe ( false ) ;
} ) ;
it ( 'should not error if the lock file does not exist' , ( ) = > {
const fs = getFileSystem ( ) ;
const lockFile = new LockFileUnderTest ( fs ) ;
expect ( ( ) = > lockFile . remove ( ) ) . not . toThrow ( ) ;
} ) ;
it ( 'should call removeSignalHandlers()' , ( ) = > {
const fs = getFileSystem ( ) ;
const lockFile = new LockFileUnderTest ( fs ) ;
fs . writeFile ( lockFile . lockFilePath , '188' ) ;
lockFile . remove ( ) ;
expect ( lockFile . log ) . toEqual ( [ '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 ( 'lock()' , ( ) = > {
2020-01-10 09:54:58 +00:00
it ( 'should guard the `fn()` with calls to `create()` and `remove()`' , ( ) = > {
const fs = getFileSystem ( ) ;
const lockFile = new LockFileUnderTest ( fs ) ;
lockFile . lock ( ( ) = > lockFile . log . push ( 'fn()' ) ) ;
expect ( lockFile . log ) . toEqual ( [ 'create()' , 'fn()' , 'remove()' ] ) ;
} ) ;
it ( 'should guard the `fn()` with calls to `create()` and `remove()`, even if it throws' ,
( ) = > {
let error : string = '' ;
const fs = getFileSystem ( ) ;
const lockFile = new LockFileUnderTest ( fs ) ;
try {
lockFile . lock ( ( ) = > {
lockFile . log . push ( 'fn()' ) ;
throw new Error ( 'ERROR' ) ;
} ) ;
} catch ( e ) {
error = e . message ;
}
expect ( error ) . toEqual ( 'ERROR' ) ;
expect ( lockFile . log ) . toEqual ( [ 'create()' , 'fn()' , 'remove()' ] ) ;
} ) ;
it ( 'should remove the lockfile if CTRL-C is triggered' , ( ) = > {
const fs = getFileSystem ( ) ;
const lockFile = new LockFileUnderTest ( fs , /* handleSignals */ true ) ;
lockFile . lock ( ( ) = > {
lockFile . log . push ( 'SIGINT' ) ;
process . emit ( 'SIGINT' , 'SIGINT' ) ;
} ) ;
// Since the test does not actually exit process, the `remove()` is called one more time.
expect ( lockFile . log ) . toEqual ( [ 'create()' , 'SIGINT' , 'remove()' , 'exit(1)' , 'remove()' ] ) ;
// Clean up the signal handlers. In practice this is not needed since the process would have
// been terminated already.
lockFile . removeSignalHandlers ( ) ;
} ) ;
it ( 'should remove the lockfile if terminal is closed' , ( ) = > {
const fs = getFileSystem ( ) ;
const lockFile = new LockFileUnderTest ( fs , /* handleSignals */ true ) ;
lockFile . lock ( ( ) = > {
lockFile . log . push ( 'SIGHUP' ) ;
process . emit ( 'SIGHUP' , 'SIGHUP' ) ;
} ) ;
// Since this does not actually exit process, the `remove()` is called one more time.
expect ( lockFile . log ) . toEqual ( [ 'create()' , 'SIGHUP' , 'remove()' , 'exit(1)' , 'remove()' ] ) ;
// Clean up the signal handlers. In practice this is not needed since the process would have
// been terminated already.
lockFile . removeSignalHandlers ( ) ;
} ) ;
} ) ;
2020-02-03 20:25:15 +00:00
describe ( 'create()' , ( ) = > {
it ( 'should write a lock file to the file-system' , ( ) = > {
const fs = getFileSystem ( ) ;
const lockFile = new LockFileUnderTest ( fs ) ;
expect ( fs . exists ( lockFile . lockFilePath ) ) . toBe ( false ) ;
lockFile . create ( ) ;
expect ( fs . exists ( lockFile . lockFilePath ) ) . toBe ( true ) ;
} ) ;
it ( 'should error if a lock file already exists' , ( ) = > {
const fs = getFileSystem ( ) ;
const lockFile = new LockFileUnderTest ( fs ) ;
fs . writeFile ( lockFile . lockFilePath , '188' ) ;
expect ( ( ) = > lockFile . create ( ) )
. toThrowError (
` ngcc is already running at process with id 188. \ n ` +
` If you are 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 } .) ` ) ;
} ) ;
} ) ;
} ) ;
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 ( 'lock()' , ( ) = > {
2020-01-10 09:54:58 +00:00
it ( 'should guard the `fn()` with calls to `create()` and `remove()`' , async ( ) = > {
const fs = getFileSystem ( ) ;
const lockFile = new LockFileUnderTest ( fs ) ;
await lockFile . lock ( async ( ) = > {
lockFile . log . push ( 'fn() - before' ) ;
// This promise forces node to do a tick in this function, ensuring that we are truly
// testing an async scenario.
await Promise . resolve ( ) ;
lockFile . log . push ( 'fn() - after' ) ;
2020-02-03 20:25:15 +00:00
return Promise . resolve ( ) ;
2020-01-10 09:54:58 +00:00
} ) ;
expect ( lockFile . log ) . toEqual ( [ 'create()' , 'fn() - before' , 'fn() - after' , 'remove()' ] ) ;
} ) ;
it ( 'should guard the `fn()` with calls to `create()` and `remove()`, even if it throws' ,
async ( ) = > {
let error : string = '' ;
const fs = getFileSystem ( ) ;
const lockFile = new LockFileUnderTest ( fs ) ;
try {
await lockFile . lock ( async ( ) = > {
lockFile . log . push ( 'fn()' ) ;
throw new Error ( 'ERROR' ) ;
} ) ;
} catch ( e ) {
error = e . message ;
}
expect ( error ) . toEqual ( 'ERROR' ) ;
expect ( lockFile . log ) . toEqual ( [ 'create()' , 'fn()' , 'remove()' ] ) ;
} ) ;
it ( 'should remove the lockfile if CTRL-C is triggered' , async ( ) = > {
const fs = getFileSystem ( ) ;
2020-02-03 20:25:15 +00:00
const lockFile = new LockFileUnderTest ( fs , 100 , 3 , /* handleSignals */ true ) ;
2020-01-10 09:54:58 +00:00
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 ( ) ;
2020-02-03 20:25:15 +00:00
const lockFile = new LockFileUnderTest ( fs , 100 , 3 , /* handleSignals */ true ) ;
2020-01-10 09:54:58 +00:00
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()' , ( ) = > {
2020-02-03 20:25:15 +00:00
it ( 'should write a lock file to the file-system' , async ( ) = > {
2020-01-10 09:54:58 +00:00
const fs = getFileSystem ( ) ;
const lockFile = new LockFileUnderTest ( fs ) ;
expect ( fs . exists ( lockFile . lockFilePath ) ) . toBe ( false ) ;
2020-02-03 20:25:15 +00:00
await lockFile . create ( ) ;
2020-01-10 09:54:58 +00:00
expect ( fs . exists ( lockFile . lockFilePath ) ) . toBe ( true ) ;
} ) ;
2020-02-03 20:25:15 +00:00
it ( 'should retry if another process is locking' , async ( ) = > {
2020-01-10 09:54:58 +00:00
const fs = getFileSystem ( ) ;
const lockFile = new LockFileUnderTest ( fs ) ;
fs . writeFile ( lockFile . lockFilePath , '188' ) ;
2020-02-03 20:25:15 +00:00
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.'
] ] ) ;
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()' ] ) ;
2020-01-10 09:54:58 +00:00
} ) ;
2020-02-03 20:25:15 +00:00
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 ) ;
2020-01-10 09:54:58 +00:00
const lockFile = new LockFileUnderTest ( fs ) ;
fs . writeFile ( lockFile . lockFilePath , '188' ) ;
2020-02-03 20:25:15 +00:00
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 ( [
[
'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()' ] ) ;
2020-01-10 09:54:58 +00:00
} ) ;
2020-02-03 20:25:15 +00:00
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.
let error : Error ;
await promise . catch ( e = > error = e ) ;
expect ( lockFile . log ) . toEqual ( [ 'create()' ] ) ;
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 } .) ` ) ;
} ) ;
2020-01-10 09:54:58 +00:00
} ) ;
} ) ;
} ) ;