1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-11-29 23:58:06 +01:00
metamask-extension/development/build/transforms/remove-fenced-code.test.js
Erik Marks d89e5336a6
Improve code fence transform error handling (#12742)
This PR improves the error handling of the code fence removal transform stream by catching errors thrown by the `removeFencedCode` function and passing them to the `end` callback. This appears to resolve a problem where watched builds would blow up whenever a file with fences was reloaded.
2021-11-19 08:35:04 -08:00

801 lines
22 KiB
JavaScript

const deepFreeze = require('deep-freeze-strict');
const { BuildType } = require('../utils');
const {
createRemoveFencedCodeTransform,
removeFencedCode,
} = require('./remove-fenced-code');
const transformUtils = require('./utils');
jest.mock('./utils', () => ({
lintTransformedFile: jest.fn(),
}));
// The test data is just strings. We get it from a function at the end of this
// file because it takes up a lot of lines and is very distracting.
const testData = getTestData();
const getMinimalFencedCode = (params = 'flask') =>
`///: BEGIN:ONLY_INCLUDE_IN(${params})
Conditionally_Included
///: END:ONLY_INCLUDE_IN
`;
describe('build/transforms/remove-fenced-code', () => {
describe('createRemoveFencedCodeTransform', () => {
const { lintTransformedFile: lintTransformedFileMock } = transformUtils;
const mockJsFileName = 'file.js';
beforeEach(() => {
lintTransformedFileMock.mockImplementation(() => Promise.resolve());
});
it('rejects invalid build types', () => {
expect(() => createRemoveFencedCodeTransform('foobar')).toThrow(
/received unrecognized build type "foobar".$/u,
);
});
it('returns a PassThrough stream for files with ignored extensions', async () => {
const fileContent = '"Valid JSON content"\n';
const stream = createRemoveFencedCodeTransform('main')('file.json');
let streamOutput = '';
await new Promise((resolve) => {
stream.on('data', (data) => {
streamOutput = streamOutput.concat(data.toString('utf8'));
});
stream.on('end', () => {
expect(streamOutput).toStrictEqual(fileContent);
expect(lintTransformedFileMock).not.toHaveBeenCalled();
resolve();
});
stream.write(Buffer.from(fileContent));
setTimeout(() => stream.end());
});
});
it('transforms a file read as a single chunk', async () => {
const filePrefix = '// A comment\n';
const fileContent = filePrefix.concat(getMinimalFencedCode());
const stream = createRemoveFencedCodeTransform('main')(mockJsFileName);
let streamOutput = '';
await new Promise((resolve) => {
stream.on('data', (data) => {
streamOutput = streamOutput.concat(data.toString('utf8'));
});
stream.on('end', () => {
expect(streamOutput).toStrictEqual(filePrefix);
expect(lintTransformedFileMock).toHaveBeenCalledTimes(1);
expect(lintTransformedFileMock).toHaveBeenCalledWith(
filePrefix,
mockJsFileName,
);
resolve();
});
stream.end(fileContent);
});
});
it('transforms a file read as multiple chunks', async () => {
const filePrefix = '// A comment\n';
const chunks = filePrefix
.concat(getMinimalFencedCode())
.split('\n')
// The final element in the split array is the empty string, which is
// useful for calling .join, but undesirable here.
.filter((line) => line !== '')
.map((line) => `${line}\n`);
const stream = createRemoveFencedCodeTransform('main')(mockJsFileName);
let streamOutput = '';
await new Promise((resolve) => {
stream.on('data', (data) => {
streamOutput = streamOutput.concat(data.toString('utf8'));
});
stream.on('end', () => {
expect(streamOutput).toStrictEqual(filePrefix);
expect(lintTransformedFileMock).toHaveBeenCalledTimes(1);
expect(lintTransformedFileMock).toHaveBeenCalledWith(
filePrefix,
mockJsFileName,
);
resolve();
});
chunks.forEach((chunk) => stream.write(chunk));
setTimeout(() => stream.end());
});
});
it('handles file with fences that is unmodified by the transform', async () => {
const fileContent = getMinimalFencedCode('main');
const stream = createRemoveFencedCodeTransform('main')(mockJsFileName);
let streamOutput = '';
await new Promise((resolve) => {
stream.on('data', (data) => {
streamOutput = streamOutput.concat(data.toString('utf8'));
});
stream.on('end', () => {
expect(streamOutput).toStrictEqual(fileContent);
expect(lintTransformedFileMock).not.toHaveBeenCalled();
resolve();
});
stream.end(fileContent);
});
});
it('skips linting for transformed file if shouldLintTransformedFiles is false', async () => {
const filePrefix = '// A comment\n';
const fileContent = filePrefix.concat(getMinimalFencedCode());
const stream = createRemoveFencedCodeTransform(
'main',
false,
)(mockJsFileName);
let streamOutput = '';
await new Promise((resolve) => {
stream.on('data', (data) => {
streamOutput = streamOutput.concat(data.toString('utf8'));
});
stream.on('end', () => {
expect(streamOutput).toStrictEqual(filePrefix);
expect(lintTransformedFileMock).not.toHaveBeenCalled();
resolve();
});
stream.end(fileContent);
});
});
it('handles error during code fence removal or parsing', async () => {
const fileContent = getMinimalFencedCode().concat(
'///: END:ONLY_INCLUDE_IN',
);
const stream = createRemoveFencedCodeTransform('main')(mockJsFileName);
await new Promise((resolve) => {
stream.on('error', (error) => {
expect(error.message).toStrictEqual(
expect.stringContaining(
'A valid fence consists of two fence lines, but the file contains an uneven number, "3", of fence lines.',
),
);
expect(lintTransformedFileMock).toHaveBeenCalledTimes(0);
resolve();
});
stream.end(fileContent);
});
});
it('handles transformed file lint failure', async () => {
lintTransformedFileMock.mockImplementationOnce(() =>
Promise.reject(new Error('lint failure')),
);
const filePrefix = '// A comment\n';
const fileContent = filePrefix.concat(getMinimalFencedCode());
const stream = createRemoveFencedCodeTransform('main')(mockJsFileName);
await new Promise((resolve) => {
stream.on('error', (error) => {
expect(error).toStrictEqual(new Error('lint failure'));
expect(lintTransformedFileMock).toHaveBeenCalledTimes(1);
expect(lintTransformedFileMock).toHaveBeenCalledWith(
filePrefix,
mockJsFileName,
);
resolve();
});
stream.end(fileContent);
});
});
});
describe('removeFencedCode', () => {
const mockFileName = 'file.js';
// Valid inputs
Object.keys(BuildType).forEach((buildType) => {
it(`transforms file with fences for build type "${buildType}"`, () => {
expect(
removeFencedCode(
mockFileName,
buildType,
testData.validInputs.withFences,
),
).toStrictEqual(testData.validOutputs[buildType]);
expect(
removeFencedCode(
mockFileName,
buildType,
testData.validInputs.extraContentWithFences,
),
).toStrictEqual(testData.validOutputsWithExtraContent[buildType]);
// Ensure that the minimal input template is in fact valid
const minimalInput = getMinimalFencedCode(buildType);
expect(
removeFencedCode(mockFileName, buildType, minimalInput),
).toStrictEqual([minimalInput, false]);
});
it(`does not modify file without fences for build type "${buildType}"`, () => {
expect(
removeFencedCode(
mockFileName,
buildType,
testData.validInputs.withoutFences,
),
).toStrictEqual([testData.validInputs.withoutFences, false]);
expect(
removeFencedCode(
mockFileName,
buildType,
testData.validInputs.extraContentWithoutFences,
),
).toStrictEqual([
testData.validInputs.extraContentWithoutFences,
false,
]);
});
});
// This is an edge case for the splicing function
it('transforms file with two fence lines', () => {
expect(
removeFencedCode(
mockFileName,
BuildType.flask,
getMinimalFencedCode('main'),
),
).toStrictEqual(['', true]);
});
it('ignores sentinels preceded by non-whitespace', () => {
const validBeginDirective = '///: BEGIN:ONLY_INCLUDE_IN(flask)\n';
const ignoredLines = [
`a ${validBeginDirective}`,
`2 ${validBeginDirective}`,
`@ ${validBeginDirective}`,
];
ignoredLines.forEach((ignoredLine) => {
// These inputs will be transformed
expect(
removeFencedCode(
mockFileName,
BuildType.flask,
getMinimalFencedCode('main').concat(ignoredLine),
),
).toStrictEqual([ignoredLine, true]);
const modifiedInputWithoutFences = testData.validInputs.withoutFences.concat(
ignoredLine,
);
// These inputs will not be transformed
expect(
removeFencedCode(
mockFileName,
BuildType.flask,
modifiedInputWithoutFences,
),
).toStrictEqual([modifiedInputWithoutFences, false]);
});
});
// Invalid inputs
it('rejects empty fences', () => {
const jsComment = '// A comment\n';
const emptyFence = getMinimalFencedCode()
.split('\n')
.filter((line) => line.startsWith('///:'))
.map((line) => `${line}\n`)
.join('');
const emptyFenceWithPrefix = jsComment.concat(emptyFence);
const emptyFenceWithSuffix = emptyFence.concat(jsComment);
const emptyFenceSurrounded = emptyFenceWithPrefix.concat(jsComment);
const inputs = [
emptyFence,
emptyFenceWithPrefix,
emptyFenceWithSuffix,
emptyFenceSurrounded,
];
inputs.forEach((input) => {
expect(() =>
removeFencedCode(mockFileName, BuildType.flask, input),
).toThrow(
`Empty fence found in file "${mockFileName}":\n${emptyFence}`,
);
});
});
it('rejects sentinels not followed by a single space and a multi-character alphabetical string', () => {
// Matches the sentinel and terminus component of the first line
// beginning with "///: TERMINUS"
const fenceSentinelAndTerminusRegex = /^\/\/\/: \w+/mu;
const replacements = [
'///:BEGIN',
'///:XBEGIN',
'///:_BEGIN',
'///:B',
'///:_',
'///: ',
'///: B',
'///:',
];
replacements.forEach((replacement) => {
expect(() =>
removeFencedCode(
mockFileName,
BuildType.flask,
getMinimalFencedCode().replace(
fenceSentinelAndTerminusRegex,
replacement,
),
),
).toThrow(
/Fence sentinel must be followed by a single space and an alphabetical string of two or more characters.$/u,
);
});
});
it('rejects malformed BEGIN directives', () => {
// This is the first line of the minimal input template
const directiveString = '///: BEGIN:ONLY_INCLUDE_IN(flask)';
const replacements = [
// Invalid terminus
'///: BE_GIN:ONLY_INCLUDE_IN(flask)',
'///: BE6IN:ONLY_INCLUDE_IN(flask)',
'///: BEGIN7:ONLY_INCLUDE_IN(flask)',
'///: BeGIN:ONLY_INCLUDE_IN(flask)',
'///: BE3:ONLY_INCLUDE_IN(flask)',
'///: BEG-IN:ONLY_INCLUDE_IN(flask)',
'///: BEG N:ONLY_INCLUDE_IN(flask)',
// Invalid commands
'///: BEGIN:ONLY-INCLUDE_IN(flask)',
'///: BEGIN:ONLY_INCLUDE:IN(flask)',
'///: BEGIN:ONL6_INCLUDE_IN(flask)',
'///: BEGIN:ONLY_IN@LUDE_IN(flask)',
'///: BEGIN:ONLy_INCLUDE_IN(flask)',
'///: BEGIN:ONLY INCLUDE_IN(flask)',
// Invalid parameters
'///: BEGIN:ONLY_INCLUDE_IN(,flask)',
'///: BEGIN:ONLY_INCLUDE_IN(flask,)',
'///: BEGIN:ONLY_INCLUDE_IN(flask,,main)',
'///: BEGIN:ONLY_INCLUDE_IN(,)',
'///: BEGIN:ONLY_INCLUDE_IN()',
'///: BEGIN:ONLY_INCLUDE_IN( )',
'///: BEGIN:ONLY_INCLUDE_IN(flask]',
'///: BEGIN:ONLY_INCLUDE_IN[flask)',
'///: BEGIN:ONLY_INCLUDE_IN(flask.main)',
'///: BEGIN:ONLY_INCLUDE_IN(flask,@)',
'///: BEGIN:ONLY_INCLUDE_IN(fla k)',
// Stuff after the directive
'///: BEGIN:ONLY_INCLUDE_IN(flask) A',
'///: BEGIN:ONLY_INCLUDE_IN(flask) 9',
'///: BEGIN:ONLY_INCLUDE_IN(flask)A',
'///: BEGIN:ONLY_INCLUDE_IN(flask)9',
'///: BEGIN:ONLY_INCLUDE_IN(flask)_',
'///: BEGIN:ONLY_INCLUDE_IN(flask))',
];
replacements.forEach((replacement) => {
expect(() =>
removeFencedCode(
mockFileName,
BuildType.flask,
getMinimalFencedCode().replace(directiveString, replacement),
),
).toThrow(
new RegExp(
`${replacement.replace(
/([()[\]])/gu,
'\\$1',
)}":\nFailed to parse fence directive.$`,
'u',
),
);
});
});
it('rejects malformed END directives', () => {
// This is the last line of the minimal input template
const directiveString = '///: END:ONLY_INCLUDE_IN';
const replacements = [
// Invalid terminus
'///: ENx:ONLY_INCLUDE_IN',
'///: EN3:ONLY_INCLUDE_IN',
'///: EN_:ONLY_INCLUDE_IN',
'///: EN :ONLY_INCLUDE_IN',
'///: EN::ONLY_INCLUDE_IN',
// Invalid commands
'///: END:ONLY-INCLUDE_IN',
'///: END::ONLY_INCLUDE_IN',
'///: END:ONLY_INCLUDE:IN',
'///: END:ONL6_INCLUDE_IN',
'///: END:ONLY_IN@LUDE_IN',
'///: END:ONLy_INCLUDE_IN',
'///: END:ONLY INCLUDE_IN',
// Stuff after the directive
'///: END:ONLY_INCLUDE_IN A',
'///: END:ONLY_INCLUDE_IN 9',
'///: END:ONLY_INCLUDE_IN _',
];
replacements.forEach((replacement) => {
expect(() =>
removeFencedCode(
mockFileName,
BuildType.flask,
getMinimalFencedCode().replace(directiveString, replacement),
),
).toThrow(
new RegExp(
`${replacement}":\nFailed to parse fence directive.$`,
'u',
),
);
});
});
it('rejects files with uneven number of fence lines', () => {
const additions = [
'///: BEGIN:ONLY_INCLUDE_IN(flask)',
'///: END:ONLY_INCLUDE_IN',
];
additions.forEach((addition) => {
expect(() =>
removeFencedCode(
mockFileName,
BuildType.flask,
getMinimalFencedCode().concat(addition),
),
).toThrow(
/A valid fence consists of two fence lines, but the file contains an uneven number, "3", of fence lines.$/u,
);
});
});
it('rejects invalid terminuses', () => {
const testCases = [
['BEGIN', ['KAPLAR', 'FLASK', 'FOO']],
['END', ['KAPLAR', 'FOO', 'BAR']],
];
testCases.forEach(([validTerminus, replacements]) => {
replacements.forEach((replacement) => {
expect(() =>
removeFencedCode(
mockFileName,
BuildType.flask,
getMinimalFencedCode().replace(validTerminus, replacement),
),
).toThrow(
new RegExp(
`Line contains invalid directive terminus "${replacement}".$`,
'u',
),
);
});
});
});
it('rejects invalid commands', () => {
const testCases = [
[/ONLY_INCLUDE_IN\(/mu, ['ONLY_KEEP_IN(', 'FLASK(', 'FOO(']],
[/ONLY_INCLUDE_IN$/mu, ['ONLY_KEEP_IN', 'FLASK', 'FOO']],
];
testCases.forEach(([validCommand, replacements]) => {
replacements.forEach((replacement) => {
expect(() =>
removeFencedCode(
mockFileName,
BuildType.flask,
getMinimalFencedCode().replace(validCommand, replacement),
),
).toThrow(
new RegExp(
`Line contains invalid directive command "${replacement.replace(
'(',
'',
)}".$`,
'u',
),
);
});
});
});
it('rejects invalid command parameters', () => {
const testCases = [
['bar', ['bar', 'flask,bar', 'flask,beta,main,bar']],
['Foo', ['Foo', 'flask,Foo', 'flask,beta,main,Foo']],
['b3ta', ['b3ta', 'flask,b3ta', 'flask,beta,main,b3ta']],
['bEta', ['bEta', 'flask,bEta', 'flask,beta,main,bEta']],
];
testCases.forEach(([invalidParam, replacements]) => {
replacements.forEach((replacement) => {
expect(() =>
removeFencedCode(
mockFileName,
BuildType.flask,
getMinimalFencedCode(replacement),
),
).toThrow(
new RegExp(`"${invalidParam}" is not a valid build type.$`, 'u'),
);
});
});
// Should fail for empty params
expect(() =>
removeFencedCode(
mockFileName,
BuildType.flask,
getMinimalFencedCode('').replace('()', ''),
),
).toThrow(/No params specified.$/u);
});
it('rejects directive pairs with wrong terminus order', () => {
// We need more than one directive pair for this test
const input = getMinimalFencedCode().concat(getMinimalFencedCode('beta'));
const expectedBeginError =
'The first directive of a pair must be a "BEGIN" directive.';
const expectedEndError =
'The second directive of a pair must be an "END" directive.';
const testCases = [
[
'BEGIN:ONLY_INCLUDE_IN(flask)',
'END:ONLY_INCLUDE_IN',
expectedBeginError,
],
[
/END:ONLY_INCLUDE_IN/mu,
'BEGIN:ONLY_INCLUDE_IN(main)',
expectedEndError,
],
[
'BEGIN:ONLY_INCLUDE_IN(beta)',
'END:ONLY_INCLUDE_IN',
expectedBeginError,
],
];
testCases.forEach(([target, replacement, expectedError]) => {
expect(() =>
removeFencedCode(
mockFileName,
BuildType.flask,
input.replace(target, replacement),
),
).toThrow(expectedError);
});
});
// We can't do this until there's more than one command
it.todo('rejects directive pairs with mismatched commands');
});
});
function getTestData() {
const data = {
validInputs: {
withFences: `
///: BEGIN:ONLY_INCLUDE_IN(flask,beta)
Conditionally_Included
///: END:ONLY_INCLUDE_IN
Always_Included
Always_Included
Always_Included
Always_Included
///: BEGIN:ONLY_INCLUDE_IN(flask,beta)
Conditionally_Included
Conditionally_Included
Conditionally_Included
///: END:ONLY_INCLUDE_IN
Always_Included
Always_Included
Always_Included
///: BEGIN:ONLY_INCLUDE_IN(flask)
Conditionally_Included
Conditionally_Included
///: END:ONLY_INCLUDE_IN
Always_Included
Always_Included
Always_Included
///: BEGIN:ONLY_INCLUDE_IN(flask)
Conditionally_Included
Conditionally_Included
///: END:ONLY_INCLUDE_IN
`,
extraContentWithFences: `
///: BEGIN:ONLY_INCLUDE_IN(flask,beta)
Conditionally_Included
///: END:ONLY_INCLUDE_IN
Always_Included
Always_Included
Always_Included
Always_Included
///: BEGIN:ONLY_INCLUDE_IN(flask,beta)
Conditionally_Included
Conditionally_Included
Conditionally_Included
///: END:ONLY_INCLUDE_IN
Always_Included
Always_Included
Always_Included
///: BEGIN:ONLY_INCLUDE_IN(flask)
Conditionally_Included
Conditionally_Included
///: END:ONLY_INCLUDE_IN
Always_Included
Always_Included
Always_Included
///: BEGIN:ONLY_INCLUDE_IN(flask)
Conditionally_Included
Conditionally_Included
///: END:ONLY_INCLUDE_IN
Always_Included
Always_Included
Always_Included
`,
withoutFences: `
Always_Included
Always_Included
Always_Included
Always_Included
Always_Included
Always_Included
Always_Included
Always_Included
Always_Included
Always_Included
`,
extraContentWithoutFences: `
Always_Included
Always_Included
Always_Included
Always_Included
Always_Included
Always_Included
Always_Included
Always_Included
Always_Included
Always_Included
Always_Included
Always_Included
Always_Included
`,
},
validOutputs: {
beta: [
`
///: BEGIN:ONLY_INCLUDE_IN(flask,beta)
Conditionally_Included
///: END:ONLY_INCLUDE_IN
Always_Included
Always_Included
Always_Included
Always_Included
///: BEGIN:ONLY_INCLUDE_IN(flask,beta)
Conditionally_Included
Conditionally_Included
Conditionally_Included
///: END:ONLY_INCLUDE_IN
Always_Included
Always_Included
Always_Included
Always_Included
Always_Included
Always_Included
`,
true,
],
},
validOutputsWithExtraContent: {
beta: [
`
///: BEGIN:ONLY_INCLUDE_IN(flask,beta)
Conditionally_Included
///: END:ONLY_INCLUDE_IN
Always_Included
Always_Included
Always_Included
Always_Included
///: BEGIN:ONLY_INCLUDE_IN(flask,beta)
Conditionally_Included
Conditionally_Included
Conditionally_Included
///: END:ONLY_INCLUDE_IN
Always_Included
Always_Included
Always_Included
Always_Included
Always_Included
Always_Included
Always_Included
Always_Included
Always_Included
`,
true,
],
},
};
data.validOutputs.flask = [data.validInputs.withFences, false];
data.validOutputs.main = [data.validInputs.withoutFences, true];
data.validOutputsWithExtraContent.flask = [
data.validInputs.extraContentWithFences,
false,
];
data.validOutputsWithExtraContent.main = [
data.validInputs.extraContentWithoutFences,
true,
];
return deepFreeze(data);
}