1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-11-23 02:10:12 +01:00
metamask-extension/development/build/transforms/remove-fenced-code.test.js
Mark Stacey 345ed9f6f2
Add build type to Sentry environment (#12441)
The build type (i.e. the distribution) is now included in the Sentry
environment during setup, for all builds except the "main" build. This
will allow us to track Flask and beta errors separately from other
errors.

A constant was created for the build types. The equivalent constant in
our build scripts was updated to match it more closely, for
consistency. We can't use the same constant in both places because our
shared constants are in modules that use ES6 exports, and our build
script does not yet support ES6 exports.

The singular `BuildType` was used rather than `BuildTypes` to match our
naming conventions elsewhere for enums. We name them like classes or
types, rather than like a collection.

Relates to #11896
2021-10-25 14:27:30 -02:30

664 lines
19 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 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]);
// 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]);
});
});
// 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
`,
withoutFences: `
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,
],
},
};
data.validOutputs.flask = [data.validInputs.withFences, false];
data.validOutputs.main = [data.validInputs.withoutFences, true];
return deepFreeze(data);
}