Skip to content

Commit

Permalink
Fixes #6: ensure multiple generated versions can exist at once. Add n…
Browse files Browse the repository at this point in the history
…oOriginal. Fix a few small issues in generated code.
  • Loading branch information
hildjj committed May 21, 2024
1 parent aea46c7 commit ec3931c
Show file tree
Hide file tree
Showing 3 changed files with 87 additions and 64 deletions.
9 changes: 9 additions & 0 deletions lib/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
* Defaults to true if peggy$debugger is set on any start, otherwise false.
* @prop {boolean} [noGenerate] Do not generate a file, only run tests on the
* original.
* @prop {boolean} [noOriginal] Do not run tests on the original code, only
* on the generated code.
*/
/**
* Test the basic functionality of a Peggy grammar, to make coverage easier.
Expand All @@ -20,6 +22,8 @@ export function testPeggy<T>(grammarUrl: URL | string, starts: PeggyTestOptions<
export type TestCounts = {
valid: number;
invalid: number;
grammarPath: string;
modifiedPath: string;
};
export type ExtraParserOptions = {
/**
Expand Down Expand Up @@ -86,4 +90,9 @@ export type TestPeggyOptions = {
* original.
*/
noGenerate?: boolean | undefined;
/**
* Do not run tests on the original code, only
* on the generated code.
*/
noOriginal?: boolean | undefined;
};
129 changes: 66 additions & 63 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,14 @@ import fs from "node:fs/promises";
import path from "node:path";

const INVALID = "\uffff";
let counter = 0;

/**
* @typedef {object} TestCounts
* @prop {number} valid
* @prop {number} invalid
* @prop {string} grammarPath
* @prop {string} modifiedPath
*/

/**
Expand Down Expand Up @@ -176,6 +179,8 @@ function checkParserStarts(grammar, starts, modified, counts) {
* Defaults to true if peggy$debugger is set on any start, otherwise false.
* @prop {boolean} [noGenerate] Do not generate a file, only run tests on the
* original.
* @prop {boolean} [noOriginal] Do not run tests on the original code, only
* on the generated code.
*/

/**
Expand All @@ -189,51 +194,67 @@ function checkParserStarts(grammar, starts, modified, counts) {
* @returns {Promise<TestCounts>}
*/
export async function testPeggy(grammarUrl, starts, opts) {
/** @type {TestCounts} */
const counts = {
valid: 0,
invalid: 0,
};

if (!(typeof grammarUrl === "string") && !(grammarUrl instanceof URL)) {
throw new TypeError("Invalid grammarUrl");
}

let grammarPath = String(grammarUrl);
if (grammarPath.startsWith("file:")) {
grammarPath = fileURLToPath(grammarPath);
}
const grammar = /** @type {Parser} */ (
await import(grammarPath)
);
// @ts-ignore
ok(grammar);
ok(grammar.parse);
ok(typeof grammar.parse, "function");
ok(grammar.StartRules);
ok(Array.isArray(grammar.StartRules));
ok(grammar.StartRules.length > 0);
ok(grammar.SyntaxError);
equal(typeof grammar.SyntaxError, "function");

// @ts-expect-error null is not valid input
throws(() => grammar.parse(null));
throws(() => grammar.parse("", {
startRule: "__ INVALID __",
}));
// Can't use @peggyjs/from-mem since c8 can't see into those modules for
// coverage. Maybe file a bug on c8? Given that, the modified file MUST be
// written to the same directory, with the same file name, so that `import`
// or `require` of relative paths or npm modules will work as expected.
const gp = path.parse(grammarPath);
// @ts-expect-error This TS error is bogus.
delete gp.base;
gp.name += `___TEST-${process.pid}-${counter++}`;
const modifiedPath = path.format(gp);

/** @type {TestCounts} */
const counts = {
valid: 0,
invalid: 0,
grammarPath,
modifiedPath,
};

if (!opts?.noOriginal) {
const grammar = /** @type {Parser} */ (
await import(grammarPath)
);
// @ts-ignore
ok(grammar);
ok(grammar.parse);
ok(typeof grammar.parse, "function");
ok(grammar.StartRules);
ok(Array.isArray(grammar.StartRules));
ok(grammar.StartRules.length > 0);
ok(grammar.SyntaxError);
equal(typeof grammar.SyntaxError, "function");

for (const startRule of grammar.StartRules) {
// @ts-expect-error null is not valid input
throws(() => grammar.parse(null, {
startRule,
throws(() => grammar.parse(null));
throws(() => grammar.parse("", {
startRule: "__ INVALID __",
}));
}
checkParserStarts(grammar, starts, false, counts);

const grammarJs = await fs.readFile(grammarPath, "utf8");
ok(grammarJs);
equal(typeof grammarJs, "string");
for (const startRule of grammar.StartRules) {
// @ts-expect-error null is not valid input
throws(() => grammar.parse(null, {
startRule,
}));
}
checkParserStarts(grammar, starts, false, counts);
}

if (!opts?.noGenerate) {
const grammarJs = await fs.readFile(grammarPath, "utf8");
ok(grammarJs);
equal(typeof grammarJs, "string");

// Approach: generate a new file next to the existing grammar file, with
// test code injected just before the parser runs. Source map information
// embedded in the new file will make coverage show up on the original file.
Expand All @@ -242,11 +263,11 @@ export async function testPeggy(grammarUrl, starts, opts) {
for (const line of grammarJs.split(/(?<=\n)/)) {
if (/^\s*peg\$result = peg\$startRuleFunction\(\);/.test(line)) {
src.add(`\
//#region Inserted by @peggyjs/coverage
(() => {
if (options.peg$debugger) {
debugger;
}
// Inserted by @peggyjs/coverage
if (options.peg$startRuleFunction) {
peg$startRuleFunction = eval(options.peg$startRuleFunction); // Ew.
}
Expand Down Expand Up @@ -293,34 +314,26 @@ export async function testPeggy(grammarUrl, starts, opts) {
err.format([{ source: 'source', text: 'a\\nb' }]);
peg$buildStructuredError([
{ type: 'other', description: 'one' },
{ type: 'any' },
peg$otherExpectation('one'),
peg$anyExpectation(),
], "", loc);
peg$buildStructuredError([
{ type: 'other', description: 'one' },
{ type: 'any' },
{ type: 'literal', text: 'b', ignoreCase: false },
{ type: 'literal', text: 'b', ignoreCase: true },
{
type: 'class',
parts: [ [ 'a', 'b' ], '\x7f' ],
inverted: true,
ignoreCase: false
},
{
type: 'class',
parts: [ 'a' ],
inverted: false,
ignoreCase: false
},
peg$literalExpectation('b', false),
peg$literalExpectation('b', true),
peg$classExpectation([ [ 'a', 'b' ], '\\x7f' ], true, false),
peg$classExpectation([ 'a' ], false, false),
peg$anyExpectation(),
peg$endExpectation(),
peg$otherExpectation('one'),
], "", loc);
peg$padEnd("foo", 2);
const oldMax = peg$maxFailPos;
peg$maxFailPos = Infinity;
peg$fail();
peg$maxFailPos = oldMax;
})();
//#endregion
`);
}
Expand All @@ -333,23 +346,13 @@ export async function testPeggy(grammarUrl, starts, opts) {
const sm = (opts && Object.prototype.hasOwnProperty.call(opts, "noMap"))
? !opts.noMap
: starts.every(s => !s.options?.peg$debugger);
const start = "//# "; // c8: THIS file is not mapped.
const start = "//#"; // c8: THIS file is not mapped.
if (sm) {
code += `
${start}sourceMappingURL=data:application/json;charset=utf-8;base64,${map}
`;
${start} sourceMappingURL=data:application/json;charset=utf-8;base64,${map}
`;
}

// Can't use @peggyjs/from-mem since c8 can't see into those modules for
// coverage. Maybe file a bug on c8? Given that, the modified file MUST be
// written to the same directory, with the same file name, so that `import`
// or `require` of relative paths or npm modules will work as expected.
const gp = path.parse(grammarPath);
// @ts-expect-error This TS error is bogus.
delete gp.base;
gp.name += `___TEST-${process.pid}`;
const modifiedPath = path.format(gp);

await fs.writeFile(modifiedPath, code);
try {
const agrammar = /** @type {Parser} */ (
Expand Down
13 changes: 12 additions & 1 deletion test/index.test.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import { deepEqual, rejects } from "node:assert";
import { deepEqual, equal, rejects } from "node:assert";
import test from "node:test";
import { testPeggy } from "../lib/index.js";

function cleanCounts(counts) {
equal(typeof counts.grammarPath, "string");
delete counts.grammarPath;
equal(typeof counts.modifiedPath, "string");
delete counts.modifiedPath;
}

test("test peggy coverage", async() => {
const counts = await testPeggy(new URL("minimal.js", import.meta.url), [
{
Expand Down Expand Up @@ -51,6 +58,8 @@ test("test peggy coverage", async() => {
},
},
]);

cleanCounts(counts);
deepEqual(counts, {
valid: 10,
invalid: 8,
Expand All @@ -67,6 +76,7 @@ test("noGenerate", async() => {
noGenerate: true,
});

cleanCounts(counts);
deepEqual(counts, {
valid: 1,
invalid: 1,
Expand All @@ -83,6 +93,7 @@ test("noMap", async() => {
noMap: true,
});

cleanCounts(counts);
deepEqual(counts, {
valid: 2,
invalid: 2,
Expand Down

0 comments on commit ec3931c

Please sign in to comment.