feat(ngcc): support reverting a file written by FileWriter (#36626)

This commit adds a `revertFile()` method to `FileWriter`, which can
revert a transformed file (and its backup - if any) written by the
`FileWriter`.

In a subsequent commit, this will be used to allow ngcc to recover
when a worker process crashes in the middle of processing a task.

PR Close #36626
This commit is contained in:
George Kalpakas
2020-04-29 21:28:17 +03:00
committed by Andrew Kushnir
parent ff6e93163f
commit 772ccf0d9f
5 changed files with 365 additions and 75 deletions

View File

@ -8,8 +8,9 @@
import {absoluteFrom, getFileSystem} from '../../../src/ngtsc/file_system';
import {runInEachFileSystem} from '../../../src/ngtsc/file_system/testing';
import {loadTestFiles} from '../../../test/helpers';
import {EntryPoint} from '../../src/packages/entry_point';
import {EntryPointBundle} from '../../src/packages/entry_point_bundle';
import {InPlaceFileWriter} from '../../src/writing/in_place_file_writer';
import {InPlaceFileWriter, NGCC_BACKUP_EXTENSION} from '../../src/writing/in_place_file_writer';
import {MockLogger} from '../helpers/mock_logger';
runInEachFileSystem(() => {
@ -29,77 +30,144 @@ runInEachFileSystem(() => {
]);
});
it('should write all the FileInfo to the disk', () => {
const fs = getFileSystem();
const logger = new MockLogger();
const fileWriter = new InPlaceFileWriter(fs, logger, /* errorOnFailedEntryPoint */ true);
fileWriter.writeBundle({} as EntryPointBundle, [
{path: _('/package/path/top-level.js'), contents: 'MODIFIED TOP LEVEL'},
{path: _('/package/path/folder-1/file-1.js'), contents: 'MODIFIED FILE 1'},
{path: _('/package/path/folder-2/file-4.js'), contents: 'MODIFIED FILE 4'},
{path: _('/package/path/folder-3/file-5.js'), contents: 'NEW FILE 5'},
]);
expect(fs.readFile(_('/package/path/top-level.js'))).toEqual('MODIFIED TOP LEVEL');
expect(fs.readFile(_('/package/path/folder-1/file-1.js'))).toEqual('MODIFIED FILE 1');
expect(fs.readFile(_('/package/path/folder-1/file-2.js'))).toEqual('ORIGINAL FILE 2');
expect(fs.readFile(_('/package/path/folder-2/file-3.js'))).toEqual('ORIGINAL FILE 3');
expect(fs.readFile(_('/package/path/folder-2/file-4.js'))).toEqual('MODIFIED FILE 4');
expect(fs.readFile(_('/package/path/folder-3/file-5.js'))).toEqual('NEW FILE 5');
describe('writeBundle()', () => {
it('should write all the FileInfo to the disk', () => {
const fs = getFileSystem();
const logger = new MockLogger();
const fileWriter = new InPlaceFileWriter(fs, logger, /* errorOnFailedEntryPoint */ true);
fileWriter.writeBundle({} as EntryPointBundle, [
{path: _('/package/path/top-level.js'), contents: 'MODIFIED TOP LEVEL'},
{path: _('/package/path/folder-1/file-1.js'), contents: 'MODIFIED FILE 1'},
{path: _('/package/path/folder-2/file-4.js'), contents: 'MODIFIED FILE 4'},
{path: _('/package/path/folder-3/file-5.js'), contents: 'NEW FILE 5'},
]);
expect(fs.readFile(_('/package/path/top-level.js'))).toEqual('MODIFIED TOP LEVEL');
expect(fs.readFile(_('/package/path/folder-1/file-1.js'))).toEqual('MODIFIED FILE 1');
expect(fs.readFile(_('/package/path/folder-1/file-2.js'))).toEqual('ORIGINAL FILE 2');
expect(fs.readFile(_('/package/path/folder-2/file-3.js'))).toEqual('ORIGINAL FILE 3');
expect(fs.readFile(_('/package/path/folder-2/file-4.js'))).toEqual('MODIFIED FILE 4');
expect(fs.readFile(_('/package/path/folder-3/file-5.js'))).toEqual('NEW FILE 5');
});
it('should create backups of all files that previously existed', () => {
const fs = getFileSystem();
const logger = new MockLogger();
const fileWriter = new InPlaceFileWriter(fs, logger, /* errorOnFailedEntryPoint */ true);
fileWriter.writeBundle({} as EntryPointBundle, [
{path: _('/package/path/top-level.js'), contents: 'MODIFIED TOP LEVEL'},
{path: _('/package/path/folder-1/file-1.js'), contents: 'MODIFIED FILE 1'},
{path: _('/package/path/folder-2/file-4.js'), contents: 'MODIFIED FILE 4'},
{path: _('/package/path/folder-3/file-5.js'), contents: 'NEW FILE 5'},
]);
expect(fs.readFile(_('/package/path/top-level.js.__ivy_ngcc_bak')))
.toEqual('ORIGINAL TOP LEVEL');
expect(fs.readFile(_('/package/path/folder-1/file-1.js.__ivy_ngcc_bak')))
.toEqual('ORIGINAL FILE 1');
expect(fs.exists(_('/package/path/folder-1/file-2.js.__ivy_ngcc_bak'))).toBe(false);
expect(fs.exists(_('/package/path/folder-2/file-3.js.__ivy_ngcc_bak'))).toBe(false);
expect(fs.readFile(_('/package/path/folder-2/file-4.js.__ivy_ngcc_bak')))
.toEqual('ORIGINAL FILE 4');
expect(fs.exists(_('/package/path/folder-3/file-5.js.__ivy_ngcc_bak'))).toBe(false);
});
it('should throw an error if the backup file already exists and errorOnFailedEntryPoint is true',
() => {
const fs = getFileSystem();
const logger = new MockLogger();
const fileWriter = new InPlaceFileWriter(fs, logger, /* errorOnFailedEntryPoint */ true);
const absoluteBackupPath = _('/package/path/already-backed-up.js');
expect(
() => fileWriter.writeBundle(
{} as EntryPointBundle,
[{path: absoluteBackupPath, contents: 'MODIFIED BACKED UP'}]))
.toThrowError(`Tried to overwrite ${
absoluteBackupPath}.__ivy_ngcc_bak with an ngcc back up file, which is disallowed.`);
});
it('should log an error, and skip writing the file, if the backup file already exists and errorOnFailedEntryPoint is false',
() => {
const fs = getFileSystem();
const logger = new MockLogger();
const fileWriter =
new InPlaceFileWriter(fs, logger, /* errorOnFailedEntryPoint */ false);
const absoluteBackupPath = _('/package/path/already-backed-up.js');
fileWriter.writeBundle(
{} as EntryPointBundle,
[{path: absoluteBackupPath, contents: 'MODIFIED BACKED UP'}]);
// Should not have written the new file nor overwritten the backup file.
expect(fs.readFile(absoluteBackupPath)).toEqual('ORIGINAL ALREADY BACKED UP');
expect(fs.readFile(_(absoluteBackupPath + '.__ivy_ngcc_bak'))).toEqual('BACKED UP');
expect(logger.logs.error).toEqual([[
`Tried to write ${
absoluteBackupPath}.__ivy_ngcc_bak with an ngcc back up file but it already exists so not writing, nor backing up, ${
absoluteBackupPath}.\n` +
`This error may be because two or more entry-points overlap and ngcc has been asked to process some files more than once.\n` +
`You should check other entry-points in this package and set up a config to ignore any that you are not using.`
]]);
});
});
it('should create backups of all files that previously existed', () => {
const fs = getFileSystem();
const logger = new MockLogger();
const fileWriter = new InPlaceFileWriter(fs, logger, /* errorOnFailedEntryPoint */ true);
fileWriter.writeBundle({} as EntryPointBundle, [
{path: _('/package/path/top-level.js'), contents: 'MODIFIED TOP LEVEL'},
{path: _('/package/path/folder-1/file-1.js'), contents: 'MODIFIED FILE 1'},
{path: _('/package/path/folder-2/file-4.js'), contents: 'MODIFIED FILE 4'},
{path: _('/package/path/folder-3/file-5.js'), contents: 'NEW FILE 5'},
]);
expect(fs.readFile(_('/package/path/top-level.js.__ivy_ngcc_bak')))
.toEqual('ORIGINAL TOP LEVEL');
expect(fs.readFile(_('/package/path/folder-1/file-1.js.__ivy_ngcc_bak')))
.toEqual('ORIGINAL FILE 1');
expect(fs.exists(_('/package/path/folder-1/file-2.js.__ivy_ngcc_bak'))).toBe(false);
expect(fs.exists(_('/package/path/folder-2/file-3.js.__ivy_ngcc_bak'))).toBe(false);
expect(fs.readFile(_('/package/path/folder-2/file-4.js.__ivy_ngcc_bak')))
.toEqual('ORIGINAL FILE 4');
expect(fs.exists(_('/package/path/folder-3/file-5.js.__ivy_ngcc_bak'))).toBe(false);
describe('revertBundle()', () => {
it('should revert the written files (and their backups)', () => {
const fs = getFileSystem();
const logger = new MockLogger();
const fileWriter = new InPlaceFileWriter(fs, logger, /* errorOnFailedEntryPoint */ true);
const filePath1 = _('/package/path/folder-1/file-1.js');
const filePath2 = _('/package/path/folder-1/file-2.js');
const fileBackupPath1 = _(`/package/path/folder-1/file-1.js${NGCC_BACKUP_EXTENSION}`);
const fileBackupPath2 = _(`/package/path/folder-1/file-2.js${NGCC_BACKUP_EXTENSION}`);
fileWriter.writeBundle({} as EntryPointBundle, [
{path: filePath1, contents: 'MODIFIED FILE 1'},
{path: filePath2, contents: 'MODIFIED FILE 2'},
]);
expect(fs.readFile(filePath1)).toBe('MODIFIED FILE 1');
expect(fs.readFile(filePath2)).toBe('MODIFIED FILE 2');
expect(fs.readFile(fileBackupPath1)).toBe('ORIGINAL FILE 1');
expect(fs.readFile(fileBackupPath2)).toBe('ORIGINAL FILE 2');
fileWriter.revertBundle({} as EntryPoint, [filePath1, filePath2], []);
expect(fs.readFile(filePath1)).toBe('ORIGINAL FILE 1');
expect(fs.readFile(filePath2)).toBe('ORIGINAL FILE 2');
expect(fs.exists(fileBackupPath1)).toBeFalse();
expect(fs.exists(fileBackupPath2)).toBeFalse();
});
it('should just remove the written files if there is no backup', () => {
const fs = getFileSystem();
const logger = new MockLogger();
const fileWriter = new InPlaceFileWriter(fs, logger, /* errorOnFailedEntryPoint */ true);
const filePath = _('/package/path/folder-1/file-1.js');
const fileBackupPath = _(`/package/path/folder-1/file-1.js${NGCC_BACKUP_EXTENSION}`);
fileWriter.writeBundle({} as EntryPointBundle, [
{path: filePath, contents: 'MODIFIED FILE 1'},
]);
fs.removeFile(fileBackupPath);
expect(fs.readFile(filePath)).toBe('MODIFIED FILE 1');
expect(fs.exists(fileBackupPath)).toBeFalse();
fileWriter.revertBundle({} as EntryPoint, [filePath], []);
expect(fs.exists(filePath)).toBeFalse();
expect(fs.exists(fileBackupPath)).toBeFalse();
});
it('should do nothing if the file does not exist', () => {
const fs = getFileSystem();
const logger = new MockLogger();
const fileWriter = new InPlaceFileWriter(fs, logger, /* errorOnFailedEntryPoint */ true);
const filePath = _('/package/path/non-existent.js');
const fileBackupPath = _(`/package/path/non-existent.js${NGCC_BACKUP_EXTENSION}`);
fs.writeFile(fileBackupPath, 'BACKUP WITHOUT FILE');
fileWriter.revertBundle({} as EntryPoint, [filePath], []);
expect(fs.exists(filePath)).toBeFalse();
expect(fs.readFile(fileBackupPath)).toBe('BACKUP WITHOUT FILE');
});
});
it('should throw an error if the backup file already exists and errorOnFailedEntryPoint is true',
() => {
const fs = getFileSystem();
const logger = new MockLogger();
const fileWriter = new InPlaceFileWriter(fs, logger, /* errorOnFailedEntryPoint */ true);
const absoluteBackupPath = _('/package/path/already-backed-up.js');
expect(
() => fileWriter.writeBundle(
{} as EntryPointBundle,
[{path: absoluteBackupPath, contents: 'MODIFIED BACKED UP'}]))
.toThrowError(`Tried to overwrite ${
absoluteBackupPath}.__ivy_ngcc_bak with an ngcc back up file, which is disallowed.`);
});
it('should log an error, and skip writing the file, if the backup file already exists and errorOnFailedEntryPoint is false',
() => {
const fs = getFileSystem();
const logger = new MockLogger();
const fileWriter = new InPlaceFileWriter(fs, logger, /* errorOnFailedEntryPoint */ false);
const absoluteBackupPath = _('/package/path/already-backed-up.js');
fileWriter.writeBundle(
{} as EntryPointBundle, [{path: absoluteBackupPath, contents: 'MODIFIED BACKED UP'}]);
// Should not have written the new file nor overwritten the backup file.
expect(fs.readFile(absoluteBackupPath)).toEqual('ORIGINAL ALREADY BACKED UP');
expect(fs.readFile(_(absoluteBackupPath + '.__ivy_ngcc_bak'))).toEqual('BACKED UP');
expect(logger.logs.error).toEqual([[
`Tried to write ${
absoluteBackupPath}.__ivy_ngcc_bak with an ngcc back up file but it already exists so not writing, nor backing up, ${
absoluteBackupPath}.\n` +
`This error may be because two or more entry-points overlap and ngcc has been asked to process some files more than once.\n` +
`You should check other entry-points in this package and set up a config to ignore any that you are not using.`
]]);
});
});
});

View File

@ -5,7 +5,7 @@
* 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 {absoluteFrom, FileSystem, getFileSystem} from '../../../src/ngtsc/file_system';
import {absoluteFrom, FileSystem, getFileSystem, join} from '../../../src/ngtsc/file_system';
import {runInEachFileSystem} from '../../../src/ngtsc/file_system/testing';
import {loadTestFiles} from '../../../test/helpers';
import {NgccConfiguration} from '../../src/packages/configuration';
@ -32,7 +32,6 @@ runInEachFileSystem(() => {
fs = getFileSystem();
logger = new MockLogger();
loadTestFiles([
{
name: _('/node_modules/test/package.json'),
contents: `
@ -494,6 +493,142 @@ runInEachFileSystem(() => {
expect(fs.exists(_('/node_modules/test/__ivy_ngcc__/typings/index.d.ts'))).toBe(false);
});
});
describe('revertFile()', () => {
beforeEach(() => {
fileWriter = new NewEntryPointFileWriter(
fs, logger, /* errorOnFailedEntryPoint */ true, new DirectPackageJsonUpdater(fs));
const config = new NgccConfiguration(fs, _('/'));
const result = getEntryPointInfo(
fs, config, logger, _('/node_modules/test'), _('/node_modules/test'))!;
if (result === NO_ENTRY_POINT || result === INCOMPATIBLE_ENTRY_POINT) {
return fail(`Expected an entry point but got ${result}`);
}
entryPoint = result;
esm5bundle = makeTestBundle(fs, entryPoint, 'module', 'esm5');
});
it('should remove non-typings files', () => {
fileWriter.writeBundle(
esm5bundle,
[
{
path: _('/node_modules/test/esm5.js'),
contents: 'export function FooTop() {} // MODIFIED',
},
{
path: _('/node_modules/test/esm5.js.map'),
contents: 'MODIFIED MAPPING DATA',
},
// Normally there will be no backup file. Write one here to ensure it is not removed.
{
path: _('/node_modules/test/esm5.js.__ivy_ngcc_bak'),
contents: 'NOT AN ACTUAL BACKUP',
},
],
['module']);
expect(fs.exists(_('/node_modules/test/__ivy_ngcc__/esm5.js'))).toBeTrue();
expect(fs.exists(_('/node_modules/test/__ivy_ngcc__/esm5.js.map'))).toBeTrue();
expect(fs.exists(_('/node_modules/test/__ivy_ngcc__/esm5.js.__ivy_ngcc_bak'))).toBeTrue();
fileWriter.revertBundle(
esm5bundle.entryPoint,
[_('/node_modules/test/esm5.js'), _('/node_modules/test/esm5.js.map')], []);
expect(fs.exists(_('/node_modules/test/__ivy_ngcc__/esm5.js'))).toBeFalse();
expect(fs.exists(_('/node_modules/test/__ivy_ngcc__/esm5.js.map'))).toBeFalse();
expect(fs.exists(_('/node_modules/test/__ivy_ngcc__/esm5.js.__ivy_ngcc_bak'))).toBeTrue();
});
it('should revert written typings files (and their backups)', () => {
fileWriter.writeBundle(
esm5bundle,
[
{
path: _('/node_modules/test/index.d.ts'),
contents: 'export declare class FooTop {} // MODIFIED'
},
{
path: _('/node_modules/test/index.d.ts.map'),
contents: 'MODIFIED MAPPING DATA',
},
],
['module']);
expect(fs.readFile(_('/node_modules/test/index.d.ts')))
.toBe('export declare class FooTop {} // MODIFIED');
expect(fs.readFile(_('/node_modules/test/index.d.ts.__ivy_ngcc_bak')))
.toBe('export declare class FooTop {}');
expect(fs.readFile(_('/node_modules/test/index.d.ts.map'))).toBe('MODIFIED MAPPING DATA');
expect(fs.readFile(_('/node_modules/test/index.d.ts.map.__ivy_ngcc_bak')))
.toBe('ORIGINAL MAPPING DATA');
fileWriter.revertBundle(
esm5bundle.entryPoint,
[_('/node_modules/test/index.d.ts'), _('/node_modules/test/index.d.ts.map')], []);
expect(fs.readFile(_('/node_modules/test/index.d.ts')))
.toBe('export declare class FooTop {}');
expect(fs.exists(_('/node_modules/test/index.d.ts.__ivy_ngcc_bak'))).toBeFalse();
expect(fs.readFile(_('/node_modules/test/index.d.ts.map'))).toBe('ORIGINAL MAPPING DATA');
expect(fs.exists(_('/node_modules/test/index.d.ts.map.__ivy_ngcc_bak'))).toBeFalse();
});
it('should revert changes to `package.json`', () => {
const entryPoint = esm5bundle.entryPoint;
const packageJsonPath = join(entryPoint.package, 'package.json');
fileWriter.writeBundle(
esm5bundle,
[
{
path: _('/node_modules/test/index.d.ts'),
contents: 'export declare class FooTop {} // MODIFIED'
},
{
path: _('/node_modules/test/index.d.ts.map'),
contents: 'MODIFIED MAPPING DATA',
},
],
['fesm5', 'module']);
const packageJsonFromFile1 = JSON.parse(fs.readFile(packageJsonPath));
expect(entryPoint.packageJson).toEqual(jasmine.objectContaining({
fesm5_ivy_ngcc: '__ivy_ngcc__/esm5.js',
fesm5: './esm5.js',
module_ivy_ngcc: '__ivy_ngcc__/esm5.js',
module: './esm5.js',
}));
expect(packageJsonFromFile1).toEqual(jasmine.objectContaining({
fesm5_ivy_ngcc: '__ivy_ngcc__/esm5.js',
fesm5: './esm5.js',
module_ivy_ngcc: '__ivy_ngcc__/esm5.js',
module: './esm5.js',
}));
fileWriter.revertBundle(
esm5bundle.entryPoint,
[_('/node_modules/test/index.d.ts'), _('/node_modules/test/index.d.ts.map')],
['fesm5', 'module']);
const packageJsonFromFile2 = JSON.parse(fs.readFile(packageJsonPath));
expect(entryPoint.packageJson).toEqual(jasmine.objectContaining({
fesm5: './esm5.js',
module: './esm5.js',
}));
expect(entryPoint.packageJson.fesm5_ivy_ngcc).toBeUndefined();
expect(entryPoint.packageJson.module_ivy_ngcc).toBeUndefined();
expect(packageJsonFromFile2).toEqual(jasmine.objectContaining({
fesm5: './esm5.js',
module: './esm5.js',
}));
expect(packageJsonFromFile2.fesm5_ivy_ngcc).toBeUndefined();
expect(packageJsonFromFile2.module_ivy_ngcc).toBeUndefined();
});
});
});
function makeTestBundle(