diff --git a/lib/index.d.ts b/lib/index.d.ts index 64a127e..9357596 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -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. @@ -20,6 +22,8 @@ export function testPeggy(grammarUrl: URL | string, starts: PeggyTestOptions< export type TestCounts = { valid: number; invalid: number; + grammarPath: string; + modifiedPath: string; }; export type ExtraParserOptions = { /** @@ -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; }; diff --git a/lib/index.js b/lib/index.js index 21bc397..57416d3 100644 --- a/lib/index.js +++ b/lib/index.js @@ -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 */ /** @@ -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. */ /** @@ -189,51 +194,67 @@ function checkParserStarts(grammar, starts, modified, counts) { * @returns {Promise} */ 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. @@ -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. } @@ -293,27 +314,18 @@ 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; @@ -321,6 +333,7 @@ export async function testPeggy(grammarUrl, starts, opts) { peg$fail(); peg$maxFailPos = oldMax; })(); + //#endregion `); } @@ -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} */ ( diff --git a/package.json b/package.json index 1ad3016..9821be0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@peggyjs/coverage", - "version": "1.1.0", + "version": "1.1.1", "decription": "Generate better code coverage for Peggy grammars", "main": "lib/index.js", "type": "module", diff --git a/test/index.test.js b/test/index.test.js index 2de8f2b..502e103 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -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), [ { @@ -51,6 +58,8 @@ test("test peggy coverage", async() => { }, }, ]); + + cleanCounts(counts); deepEqual(counts, { valid: 10, invalid: 8, @@ -67,6 +76,7 @@ test("noGenerate", async() => { noGenerate: true, }); + cleanCounts(counts); deepEqual(counts, { valid: 1, invalid: 1, @@ -83,6 +93,7 @@ test("noMap", async() => { noMap: true, }); + cleanCounts(counts); deepEqual(counts, { valid: 2, invalid: 2,