Skip to content

Commit 541a449

Browse files
blockString-test: add fuzzing test for 'printBlockString' (#2574)
1 parent 437cc1b commit 541a449

File tree

7 files changed

+239
-42
lines changed

7 files changed

+239
-42
lines changed

.babelrc.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@
66
],
77
"overrides": [
88
{
9-
"exclude": ["**/__tests__/**/*", "**/__fixtures__/**/*"],
9+
"exclude": [
10+
"src/__testUtils__/**/*",
11+
"**/__tests__/**/*",
12+
"**/__fixtures__/**/*"
13+
],
1014
"presets": ["@babel/preset-env"],
1115
"plugins": [
1216
["@babel/plugin-transform-classes", { "loose": true }],
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
// @flow strict
2+
3+
import { expect } from 'chai';
4+
import { describe, it } from 'mocha';
5+
6+
import genFuzzStrings from '../genFuzzStrings';
7+
8+
function expectFuzzStrings(options) {
9+
return expect(Array.from(genFuzzStrings(options)));
10+
}
11+
12+
describe('genFuzzStrings', () => {
13+
it('always provide empty string', () => {
14+
expectFuzzStrings({ allowedChars: [], maxLength: 0 }).to.deep.equal(['']);
15+
expectFuzzStrings({ allowedChars: [], maxLength: 1 }).to.deep.equal(['']);
16+
expectFuzzStrings({ allowedChars: ['a'], maxLength: 0 }).to.deep.equal([
17+
'',
18+
]);
19+
});
20+
21+
it('generate strings with single character', () => {
22+
expectFuzzStrings({ allowedChars: ['a'], maxLength: 1 }).to.deep.equal([
23+
'',
24+
'a',
25+
]);
26+
27+
expectFuzzStrings({
28+
allowedChars: ['a', 'b', 'c'],
29+
maxLength: 1,
30+
}).to.deep.equal(['', 'a', 'b', 'c']);
31+
});
32+
33+
it('generate strings with multiple character', () => {
34+
expectFuzzStrings({ allowedChars: ['a'], maxLength: 2 }).to.deep.equal([
35+
'',
36+
'a',
37+
'aa',
38+
]);
39+
40+
expectFuzzStrings({
41+
allowedChars: ['a', 'b', 'c'],
42+
maxLength: 2,
43+
}).to.deep.equal([
44+
'',
45+
'a',
46+
'b',
47+
'c',
48+
'aa',
49+
'ab',
50+
'ac',
51+
'ba',
52+
'bb',
53+
'bc',
54+
'ca',
55+
'cb',
56+
'cc',
57+
]);
58+
});
59+
60+
it('generate strings longer than possible number of characters', () => {
61+
expectFuzzStrings({
62+
allowedChars: ['a', 'b'],
63+
maxLength: 3,
64+
}).to.deep.equal([
65+
'',
66+
'a',
67+
'b',
68+
'aa',
69+
'ab',
70+
'ba',
71+
'bb',
72+
'aaa',
73+
'aab',
74+
'aba',
75+
'abb',
76+
'baa',
77+
'bab',
78+
'bba',
79+
'bbb',
80+
]);
81+
});
82+
});
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// @flow strict
2+
3+
import { expect } from 'chai';
4+
import { describe, it } from 'mocha';
5+
6+
import inspectStr from '../inspectStr';
7+
8+
describe('inspectStr', () => {
9+
it('handles null and undefined values', () => {
10+
expect(inspectStr(null)).to.equal('null');
11+
expect(inspectStr(undefined)).to.equal('null');
12+
});
13+
14+
it('correctly print various strings', () => {
15+
expect(inspectStr('')).to.equal('``');
16+
expect(inspectStr('a')).to.equal('`a`');
17+
expect(inspectStr('"')).to.equal('`"`');
18+
expect(inspectStr("'")).to.equal("`'`");
19+
expect(inspectStr('\\"')).to.equal('`\\"`');
20+
});
21+
});

src/__testUtils__/genFuzzStrings.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// @flow strict
2+
3+
/**
4+
* Generator that produces all possible combinations of allowed characters.
5+
*/
6+
export default function* genFuzzStrings(options: {|
7+
allowedChars: Array<string>,
8+
maxLength: number,
9+
|}): Generator<string, void, void> {
10+
const { allowedChars, maxLength } = options;
11+
const numAllowedChars = allowedChars.length;
12+
13+
let numCombinations = 0;
14+
for (let length = 1; length <= maxLength; ++length) {
15+
numCombinations += numAllowedChars ** length;
16+
}
17+
18+
yield ''; // special case for empty string
19+
for (let combination = 0; combination < numCombinations; ++combination) {
20+
let permutation = '';
21+
22+
let leftOver = combination;
23+
while (leftOver >= 0) {
24+
const reminder = leftOver % numAllowedChars;
25+
permutation = allowedChars[reminder] + permutation;
26+
leftOver = (leftOver - reminder) / numAllowedChars - 1;
27+
}
28+
29+
yield permutation;
30+
}
31+
}

src/__testUtils__/inspectStr.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// @flow strict
2+
3+
/**
4+
* Special inspect function to produce readable string literal for error messages in tests
5+
*/
6+
export default function inspectStr(str: ?string): string {
7+
if (str == null) {
8+
return 'null';
9+
}
10+
return JSON.stringify(str)
11+
.replace(/^"|"$/g, '`')
12+
.replace(/\\"/g, '"')
13+
.replace(/\\\\/g, '\\');
14+
}

src/language/__tests__/blockString-test.js

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,14 @@
33
import { expect } from 'chai';
44
import { describe, it } from 'mocha';
55

6+
import dedent from '../../__testUtils__/dedent';
7+
import inspectStr from '../../__testUtils__/inspectStr';
8+
import genFuzzStrings from '../../__testUtils__/genFuzzStrings';
9+
10+
import invariant from '../../jsutils/invariant';
11+
12+
import { Lexer } from '../lexer';
13+
import { Source } from '../source';
614
import {
715
dedentBlockStringValue,
816
getBlockStringIndentation,
@@ -181,4 +189,57 @@ describe('printBlockString', () => {
181189
),
182190
);
183191
});
192+
193+
it('correctly print random strings', () => {
194+
// Testing with length >5 is taking exponentially more time. However it is
195+
// highly recommended to test with increased limit if you make any change.
196+
for (const fuzzStr of genFuzzStrings({
197+
allowedChars: ['\n', '\t', ' ', '"', 'a', '\\'],
198+
maxLength: 5,
199+
})) {
200+
const testStr = '"""' + fuzzStr + '"""';
201+
202+
let testValue;
203+
try {
204+
testValue = lexValue(testStr);
205+
} catch (e) {
206+
continue; // skip invalid values
207+
}
208+
invariant(typeof testValue === 'string');
209+
210+
const printedValue = lexValue(printBlockString(testValue));
211+
212+
invariant(
213+
testValue === printedValue,
214+
dedent`
215+
Expected lexValue(printBlockString(${inspectStr(testValue)}))
216+
to equal ${inspectStr(testValue)}
217+
but got ${inspectStr(printedValue)}
218+
`,
219+
);
220+
221+
const printedMultilineString = lexValue(
222+
printBlockString(testValue, ' ', true),
223+
);
224+
225+
invariant(
226+
testValue === printedMultilineString,
227+
dedent`
228+
Expected lexValue(printBlockString(${inspectStr(
229+
testValue,
230+
)}, ' ', true))
231+
to equal ${inspectStr(testValue)}
232+
but got ${inspectStr(printedMultilineString)}
233+
`,
234+
);
235+
}
236+
237+
function lexValue(str) {
238+
const lexer = new Lexer(new Source(str));
239+
const value = lexer.advance().value;
240+
241+
invariant(lexer.advance().kind === '<EOF>', 'Expected EOF');
242+
return value;
243+
}
244+
});
184245
});

src/utilities/__tests__/stripIgnoredCharacters-test.js

Lines changed: 25 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { expect } from 'chai';
44
import { describe, it } from 'mocha';
55

66
import dedent from '../../__testUtils__/dedent';
7+
import inspectStr from '../../__testUtils__/inspectStr';
8+
import genFuzzStrings from '../../__testUtils__/genFuzzStrings';
79

810
import invariant from '../../jsutils/invariant';
911

@@ -67,13 +69,6 @@ function lexValue(str) {
6769
return value;
6870
}
6971

70-
// istanbul ignore next (called only to make error messages for failing tests)
71-
function inspectStr(str) {
72-
return (JSON.stringify(str) ?? '')
73-
.replace(/^"|"$/g, '`')
74-
.replace(/\\"/g, '"');
75-
}
76-
7772
function expectStripped(docString) {
7873
return {
7974
toEqual(expected) {
@@ -441,45 +436,34 @@ describe('stripIgnoredCharacters', () => {
441436
expectStrippedString('"""\na\n b"""').toStayTheSame();
442437
expectStrippedString('"""\n a\n b"""').toEqual('"""a\nb"""');
443438
expectStrippedString('"""\na\n b\nc"""').toEqual('"""a\n b\nc"""');
439+
});
444440

441+
it('strips ignored characters inside random block strings', () => {
445442
// Testing with length >5 is taking exponentially more time. However it is
446443
// highly recommended to test with increased limit if you make any change.
447-
const maxCombinationLength = 5;
448-
const possibleChars = ['\n', ' ', '"', 'a', '\\'];
449-
const numPossibleChars = possibleChars.length;
450-
let numCombinations = 1;
451-
for (let length = 1; length < maxCombinationLength; ++length) {
452-
numCombinations *= numPossibleChars;
453-
for (let combination = 0; combination < numCombinations; ++combination) {
454-
let testStr = '"""';
455-
456-
let leftOver = combination;
457-
for (let i = 0; i < length; ++i) {
458-
const reminder = leftOver % numPossibleChars;
459-
testStr += possibleChars[reminder];
460-
leftOver = (leftOver - reminder) / numPossibleChars;
461-
}
462-
463-
testStr += '"""';
464-
465-
let testValue;
466-
try {
467-
testValue = lexValue(testStr);
468-
} catch (e) {
469-
continue; // skip invalid values
470-
}
444+
for (const fuzzStr of genFuzzStrings({
445+
allowedChars: ['\n', '\t', ' ', '"', 'a', '\\'],
446+
maxLength: 5,
447+
})) {
448+
const testStr = '"""' + fuzzStr + '"""';
449+
450+
let testValue;
451+
try {
452+
testValue = lexValue(testStr);
453+
} catch (e) {
454+
continue; // skip invalid values
455+
}
471456

472-
const strippedValue = lexValue(stripIgnoredCharacters(testStr));
457+
const strippedValue = lexValue(stripIgnoredCharacters(testStr));
473458

474-
invariant(
475-
testValue === strippedValue,
476-
dedent`
477-
Expected lexValue(stripIgnoredCharacters(${inspectStr(testStr)}))
478-
to equal ${inspectStr(testValue)}
479-
but got ${inspectStr(strippedValue)}
480-
`,
481-
);
482-
}
459+
invariant(
460+
testValue === strippedValue,
461+
dedent`
462+
Expected lexValue(stripIgnoredCharacters(${inspectStr(testStr)}))
463+
to equal ${inspectStr(testValue)}
464+
but got ${inspectStr(strippedValue)}
465+
`,
466+
);
483467
}
484468
});
485469

0 commit comments

Comments
 (0)