Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fuzz test location-related parser options #14201

Merged
merged 34 commits into from Jan 29, 2022
Merged
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
0d14005
Autogenerate tests to make sure startLine and startColumn work everyw…
tolmasky Jan 16, 2022
aee9486
Only deserialize JSON files that are serialized.
tolmasky Jan 20, 2022
4737700
Don't fuzz test in node 8, since for whatever reason Jest 23 on node …
tolmasky Jan 21, 2022
4bf3933
Rename runFixtureTests.js to run-fixture-tests.js.
tolmasky Jan 21, 2022
60b039e
Fix for showing code snippet again.
tolmasky Jan 23, 2022
17c1093
Fix some linter errors.
tolmasky Jan 23, 2022
c140ba8
Fix stack traces and only generate the context error when we need it.
tolmasky Jan 23, 2022
e10465a
Create errors in deserialization step instead of using adjust to conv…
tolmasky Jan 23, 2022
e9fc211
Fix only storing cause if it's an error.
tolmasky Jan 23, 2022
00fb3f6
Fix UnexpectedSuccess error.
tolmasky Jan 23, 2022
63a3e0c
Better DifferentError and cleaned up the cause stuff a bit.
tolmasky Jan 23, 2022
e5933b3
Fix linter errors.
tolmasky Jan 24, 2022
3a555d4
First pass at serialization.
tolmasky Jan 25, 2022
5648fc0
Better errors and serialization.
tolmasky Jan 25, 2022
dc186b1
Fix saving output.
tolmasky Jan 25, 2022
c31d5ba
Fix saving options.
tolmasky Jan 25, 2022
a275b7f
Fix linter errors and incorrect removal.
tolmasky Jan 25, 2022
c6b28ec
Add FUZZ environment variable.
tolmasky Jan 25, 2022
a040ebd
Fix location undefined problem when saving.
tolmasky Jan 25, 2022
0455aeb
Read the actual options file in since we don't have a true copy of th…
tolmasky Jan 25, 2022
c703418
Fix also compacting start and end outside of loc.
tolmasky Jan 25, 2022
a64178a
Fix linter error.
tolmasky Jan 25, 2022
ffb642e
Address a few style issues.
tolmasky Jan 26, 2022
1a24870
Use environment variable for TEST_FUZZ, and disable fuzz testing for …
tolmasky Jan 26, 2022
5e76011
Address more change requests.
tolmasky Jan 26, 2022
87424e1
Fix lint error.
tolmasky Jan 26, 2022
f6aeb06
Move logic into FixtureError base class and add comments.
tolmasky Jan 26, 2022
caa9a8c
Throw early if we are in CI, and make sure runFixtureText has its JSD…
tolmasky Jan 26, 2022
9b87eea
Fix JSDocs.
tolmasky Jan 27, 2022
b1118e4
Change fuzz testing to be opt-in while we only do line changes.
tolmasky Jan 27, 2022
dec5379
Don't check if file exists before deleting it, and only overwite opti…
tolmasky Jan 27, 2022
198ad6b
Put a newline at the end of JSON files.
tolmasky Jan 28, 2022
91f73c7
Only refrain from throwing if the error is ENOENT. Also, clean up the…
tolmasky Jan 28, 2022
4ec5658
Fix linter error.
tolmasky Jan 28, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/ci.yml
Expand Up @@ -188,7 +188,9 @@ jobs:

# Todo(Babel 8): Jest execution path is hardcoded because Yarn 2 does not support node 6
run: |
BABEL_ENV=test node ./node_modules/.bin/jest --ci --color
BABEL_ENV=test node --max-old-space-size=4096 ./node_modules/.bin/jest --ci --color
env:
TEST_FUZZ: "${{ (matrix.node-version == '6' || matrix.node-version == '8' || matrix.node-version == '10') && 'false' || 'true' }}"

test-babel-8-breaking:
name: Test Babel 8 breaking changes
Expand Down
2 changes: 2 additions & 0 deletions packages/babel-helper-fixtures/src/index.ts
Expand Up @@ -105,6 +105,7 @@ function pushTask(taskName, taskDir, suite, suiteName) {

const expectLoc =
findFile(taskDir + "/output", true /* allowJSON */) ||
findFile(`${taskDir}/output.extended`, true) ||
taskDir + "/output.js";
const stdoutLoc = taskDir + "/stdout.txt";
const stderrLoc = taskDir + "/stderr.txt";
Expand All @@ -130,6 +131,7 @@ function pushTask(taskName, taskDir, suite, suiteName) {
if (taskOptsLoc) Object.assign(taskOpts, require(taskOptsLoc));

const test = {
taskDir,
optionsDir: taskOptsLoc ? path.dirname(taskOptsLoc) : null,
title: humanize(taskName, true),
disabled:
Expand Down
5 changes: 3 additions & 2 deletions packages/babel-parser/test/attachComment-false.js
@@ -1,12 +1,13 @@
import path from "path";
import { runFixtureTestsWithoutExactASTMatch } from "./helpers/runFixtureTests.js";
import runFixtureTests from "./helpers/run-fixture-tests.js";
import { parseExpression } from "../lib/index.js";
import { fileURLToPath } from "url";

runFixtureTestsWithoutExactASTMatch(
runFixtureTests(
path.join(path.dirname(fileURLToPath(import.meta.url)), "expressions"),
(input, options = {}) => {
options.attachComment = false;
return parseExpression(input, options);
},
true,
);
5 changes: 3 additions & 2 deletions packages/babel-parser/test/estree-throws.js
@@ -1,12 +1,13 @@
import path from "path";
import { runFixtureTestsWithoutExactASTMatch } from "./helpers/runFixtureTests.js";
import runFixtureTests from "./helpers/run-fixture-tests.js";
import { parse } from "../lib/index.js";
import { fileURLToPath } from "url";

runFixtureTestsWithoutExactASTMatch(
runFixtureTests(
path.join(path.dirname(fileURLToPath(import.meta.url)), "fixtures"),
(input, options = {}) => {
const plugins = options.plugins || [];
return parse(input, { ...options, plugins: plugins.concat("estree") });
},
true,
);
2 changes: 1 addition & 1 deletion packages/babel-parser/test/expressions.js
@@ -1,5 +1,5 @@
import path from "path";
import { runFixtureTests } from "./helpers/runFixtureTests.js";
import runFixtureTests from "./helpers/run-fixture-tests.js";
import { parseExpression } from "../lib/index.js";
import { fileURLToPath } from "url";

Expand Down
Expand Up @@ -23,7 +23,7 @@
"type": "Literal",
"start":10,"end":12,"loc":{"start":{"line":1,"column":10},"end":{"line":1,"column":12}},
"value": {
"$$ babel internal serialized type": "BigInt",
"$$ babel internal serialized type": "bigint",
"value": "1"
},
"raw": "1n",
Expand Down
149 changes: 149 additions & 0 deletions packages/babel-parser/test/helpers/difference.js
@@ -0,0 +1,149 @@
/* eslint-disable no-confusing-arrow */

import { isIdentifierName } from "@babel/helper-validator-identifier";

const { isArray } = Array;
const { isInteger } = Number;
const { hasOwnProperty } = Object;

export default class Difference {
constructor(adjust, expected, actual) {
const woundDifference = compare(adjust, expected, actual);

if (!woundDifference) {
return Difference.None;
}

const [path, reason] = toUnwoundDifference(woundDifference);
const message = `${toExplanationString(reason)} in ${toPathString(path)}`;

return Object.assign(this, { ...reason, path, message });
}
}

Difference.None = Object.freeze(
Object.setPrototypeOf({}, Difference.prototype),
);

const toType = value =>
value === null
? "null"
: typeof value !== "object"
? typeof value
: isArray(value)
? "Array"
: value instanceof RegExp
? "RegExp"
: value instanceof Error
? "Error"
: "Object";

function compare(adjust, expected, actual) {
// easy.
if (Object.is(expected, actual)) {
return false;
}

const typeExpected = toType(expected);
const typeActual = toType(actual);

if (typeExpected !== typeActual) {
return { discrepancy: "value", expected, actual };
tolmasky marked this conversation as resolved.
Show resolved Hide resolved
}

// Just ignore functions (AKA, assume they're equal).
if (typeActual === "function") {
return false;
}

if (typeActual === "RegExp" && expected + "" === actual + "") {
return false;
}

if (typeActual === "Error") {
return compare(
adjust,
{ message: expected.message },
{ message: actual.message },
);
}

if (typeActual !== "Object" && typeActual !== "Array") {
return { discrepancy: "value", expected, actual };
}

const keysExpected = Object.keys(expected);
const keysActual = Object.keys(actual).filter(
key => actual[key] !== void 0 && typeof actual[key] !== "function",
);
const lengthExpected = keysExpected.length;
const lengthActual = keysActual.length;

if (lengthExpected !== lengthActual && typeActual === "Array") {
return {
discrepancy: "length",
expected: lengthExpected,
actual: lengthActual,
};
}

if (lengthExpected < lengthActual) {
const keysExpectedSet = new Set(keysExpected);
const key = keysActual.find(key => !keysExpectedSet.has(key));

if (key !== void 0) {
return { discrepancy: "unexpected-key", key };
}
}

for (const key of keysExpected) {
if (!hasOwnProperty.call(actual, key)) {
return { discrepancy: "missing-key", key, actual };
}

const original = expected[key];
const adjusted = adjust
? adjust(adjust, original, key, expected)
: original;
const difference = compare(adjust, adjusted, actual[key]);

if (difference) {
return [key, difference];
}
}

return false;
}

const toUnwoundDifference = compiled =>
!isArray(compiled)
? [[], compiled]
: toUnwoundDifference(compiled[1]).map((item, index) =>
index === 0 ? [compiled[0], ...item] : item,
);

const toValueString = (value, type = toType(value)) =>
type === "string"
? JSON.stringify(value)
: type === "symbol"
? value.toString()
: type === "bigint"
? `${value}n`
: Object.is(value, -0)
? "-0"
: value + "";

const toExplanationString = ({ discrepancy, expected, actual, key }) =>
discrepancy === "length"
? `Array of wrong size, expected length of ${expected}, but got ${actual}`
: discrepancy === "unexpected-key"
? `Did not expect a property ${toValueString(key)}`
: discrepancy === "missing-key"
? `${toType(actual)} is missing property ${toValueString(key)}`
: `${toValueString(expected)} != ${toValueString(actual)}`;

const isInt = key => isInteger(+key);
const toAccess = key =>
isInt(key) ? `[${key}]` : isIdentifierName(key) ? `.${key}` : `["${key}"]`;

const toPathString = path => path.map(toAccess).join("");
97 changes: 97 additions & 0 deletions packages/babel-parser/test/helpers/fixture-error.js
@@ -0,0 +1,97 @@
import { inspect } from "util";
import Difference from "./difference.js";
import "./polyfill.js";

const { isArray } = Array;
const { defineProperty, entries, fromEntries } = Object;

const named = (name, object) => defineProperty(object, "name", { value: name });
const mapEntries = (object, f) => fromEntries(entries(object).map(f));
// eslint-disable-next-line no-confusing-arrow
const toContextError = error =>
isArray(error) ? error.map(toContextError) : error.context || error;

export default class FixtureError extends Error {
constructor(difference, { cause } = {}) {
super();

// Sigh, still have to manually set the name unfortunately...
named(this.constructor.name, this);

this.difference = difference;

// Set cause ourselves, since node < 17 has a bug where it won't show it
tolmasky marked this conversation as resolved.
Show resolved Hide resolved
// otherwise. Technically, we display it ourselves, but best to be defensive
// in case we modify this implementation later.
if (cause) this.cause = cause;
}

static toMessage() {
return this.constructor.name;
}

get message() {
return this.constructor.toMessage(this.difference, this.cause);
}

// Don't show the stack of FixtureErrors, it's irrelevant.
// Instead, show the cause, if present.
[inspect.custom](depth, options) {
return `${this.message.replace(/(?<=error(s?))\.$/, ":\n")}${
this.cause
? `\n${inspect(toContextError(this.cause), options)}`.replace(
/\n/g,
"\n ",
)
: ""
}`;
}

static fromDifference(difference, actual) {
return difference === Difference.None
? false
: difference.path[0] !== "threw"
? new FixtureError.DifferentAST(difference)
: !difference.expected
? new FixtureError.UnexpectedError(difference, { cause: actual.threw })
: difference.actual
? new FixtureError.DifferentError(difference, { cause: actual.threw })
: actual.ast && actual.ast.errors
? new FixtureError.UnexpectedRecovery(difference, {
cause: actual.ast.errors,
})
: new FixtureError.UnexpectedSuccess(difference);
}
}

Object.assign(
FixtureError,
mapEntries(
{
DifferentError: ({ expected }) =>
`Expected unrecoverable error: \n\n${expected}\n\n` +
`But instead encountered different unrecoverable error.`,

DifferentAST: ({ message }) => message,

UnexpectedError: () => `Encountered unexpected unrecoverable error.`,

UnexpectedSuccess: ({ expected }) =>
`Expected unrecoverable error:\n\n ${expected}\n\n` +
`But parsing succeeded without errors.`,

UnexpectedRecovery: ({ expected }, errors) =>
`Expected unrecoverable error:\n\n ${expected}\n\n` +
`But instead parsing recovered from ${errors.length} errors.`,
},
([name, toMessage]) => [
name,
named(
`FixtureError.${name}`,
class extends FixtureError {
static toMessage = toMessage;
},
),
],
),
);
33 changes: 33 additions & 0 deletions packages/babel-parser/test/helpers/polyfill.js
@@ -0,0 +1,33 @@
// TODO(Babel 8): Remove this file.
// We run these tests as far back as Node 6, so we need these there.
tolmasky marked this conversation as resolved.
Show resolved Hide resolved

if (!Object.entries) {
Object.entries = object => Object.keys(object).map(key => [key, object[key]]);
}

// From: https://github.com/tc39/proposal-object-from-entries/blob/main/polyfill.js
if (!Object.fromEntries) {
Object.fromEntries = function (entries) {
const obj = {};

for (const pair of entries) {
if (Object(pair) !== pair) {
throw new TypeError("iterable for fromEntries should yield objects");
}

// Consistency with Map: contract is that entry has "0" and "1" keys, not
// that it is an array or iterable.

const { 0: key, 1: val } = pair;

Object.defineProperty(obj, key, {
configurable: true,
enumerable: true,
writable: true,
value: val,
});
}

return obj;
};
}