Skip to content

Commit

Permalink
Merge pull request #5 from hildjj/fix-coverage
Browse files Browse the repository at this point in the history
Fix coverage
  • Loading branch information
hildjj authored May 14, 2024
2 parents c031fda + bd5d18d commit 6123a95
Show file tree
Hide file tree
Showing 8 changed files with 193 additions and 52 deletions.
3 changes: 1 addition & 2 deletions .c8rc
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
"lcov"
],
"exclude": [
"**/test/*.test.js",
"**/node_modules/**"
"test/*.test.js"
]
}
4 changes: 2 additions & 2 deletions .github/workflows/node.js.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:

strategy:
matrix:
node-version: [18.x, 20.x, 21.x]
node-version: [18.x, 20.x, 21.x, 22.x]
platform: [ubuntu-latest]

runs-on: ${{ matrix.platform }}
Expand All @@ -37,7 +37,7 @@ jobs:
- name: Test ${{ matrix.node-version }} ${{ matrix.platform }}
run: npm run test
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v4.0.1
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
slug: peggyjs/coverage
32 changes: 31 additions & 1 deletion lib/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,26 @@
/**
* @typedef {object} TestPeggyOptions
* @prop {boolean} [noDelete] Do not delete the generated file.
* @prop {boolean} [noMap] Do not add a sourcemap to the generated file.
* 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.
*/
/**
* Test the basic functionality of a Peggy grammar, to make coverage easier.
*
* @template T
* @param {URL | string} grammarUrl The file name for the compiled grammar.
* @param {PeggyTestOptions<T>[]} starts List of tests. Ensure you have at
* least one validInput and at least one invalidInput.
* @param {TestPeggyOptions} [opts] Options for processing.
* @returns {Promise<TestCounts>}
*/
export function testPeggy<T>(grammarUrl: URL | string, starts: PeggyTestOptions<T>[]): Promise<void>;
export function testPeggy<T>(grammarUrl: URL | string, starts: PeggyTestOptions<T>[], opts?: TestPeggyOptions | undefined): Promise<TestCounts>;
export type TestCounts = {
valid: number;
invalid: number;
};
export type ExtraParserOptions = {
/**
* In the augmented code only, use this
Expand Down Expand Up @@ -57,3 +71,19 @@ export type Parser = import('peggy').Parser & {
StartRules: string[];
};
export type Location = import('peggy').Location;
export type TestPeggyOptions = {
/**
* Do not delete the generated file.
*/
noDelete?: boolean | undefined;
/**
* Do not add a sourcemap to the generated file.
* Defaults to true if peggy$debugger is set on any start, otherwise false.
*/
noMap?: boolean | undefined;
/**
* Do not generate a file, only run tests on the
* original.
*/
noGenerate?: boolean | undefined;
};
116 changes: 77 additions & 39 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ import path from "node:path";

const INVALID = "\uffff";

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

/**
* @typedef {object} ExtraParserOptions
* @prop {string} [peg$startRuleFunction] In the augmented code only, use this
Expand Down Expand Up @@ -51,8 +57,9 @@ const INVALID = "\uffff";
* @param {Parser} grammar
* @param {PeggyTestOptions<T>[]} starts
* @param {boolean} modified
* @param {TestCounts} counts
*/
function checkParserStarts(grammar, starts, modified) {
function checkParserStarts(grammar, starts, modified, counts) {
for (const start of starts) {
const startRule = start.startRule || undefined; // NOT `??`
const peg$maxFailPos = start.peg$maxFailPos ?? 0;
Expand Down Expand Up @@ -113,6 +120,7 @@ function checkParserStarts(grammar, starts, modified) {
...options,
}));
}
counts.valid++;
}

if (typeof start.invalidInput === "string") {
Expand Down Expand Up @@ -156,19 +164,40 @@ function checkParserStarts(grammar, starts, modified) {
}]);
equal(typeof fmt, "string");
}
counts.invalid++;
}
}
}

/**
* @typedef {object} TestPeggyOptions
* @prop {boolean} [noDelete] Do not delete the generated file.
* @prop {boolean} [noMap] Do not add a sourcemap to the generated file.
* 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.
*/

/**
* Test the basic functionality of a Peggy grammar, to make coverage easier.
*
* @template T
* @param {URL | string} grammarUrl The file name for the compiled grammar.
* @param {PeggyTestOptions<T>[]} starts List of tests. Ensure you have at
* least one validInput and at least one invalidInput.
* @param {TestPeggyOptions} [opts] Options for processing.
* @returns {Promise<TestCounts>}
*/
export async function testPeggy(grammarUrl, starts) {
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);
Expand Down Expand Up @@ -198,20 +227,21 @@ export async function testPeggy(grammarUrl, starts) {
startRule,
}));
}
checkParserStarts(grammar, starts, false);
checkParserStarts(grammar, starts, false, counts);

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.
const src = new SourceNode();
let lineNum = 1;
for (const line of grammarJs.split(/(?<=\n)/)) {
if (/^\s*peg\$result = peg\$startRuleFunction\(\);/.test(line)) {
src.add(`\
if (!opts?.noGenerate) {
// 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.
const src = new SourceNode();
let lineNum = 1;
for (const line of grammarJs.split(/(?<=\n)/)) {
if (/^\s*peg\$result = peg\$startRuleFunction\(\);/.test(line)) {
src.add(`\
(() => {
if (options.peg$debugger) {
debugger;
Expand Down Expand Up @@ -293,37 +323,45 @@ export async function testPeggy(grammarUrl, starts) {
})();
`);
}
src.add(new SourceNode(lineNum++, 0, grammarPath, line));
}
src.add(new SourceNode(lineNum++, 0, grammarPath, line));
}

const withMap = src.toStringWithSourceMap();
const map = Buffer.from(withMap.map.toString()).toString("base64");
let code = withMap.code;
if (starts.every(s => !s.options?.peg$debugger)) {
code += `
//# sourceMappingURL=data:application/json;charset=utf-8;base64,${map}
`;
}
const withMap = src.toStringWithSourceMap();
const map = Buffer.from(withMap.map.toString()).toString("base64");
let code = withMap.code;
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.
if (sm) {
code += `
${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);
// 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} */ (
await import(modifiedPath)
);
ok(agrammar);
checkParserStarts(agrammar, starts, true);
} finally {
await fs.rm(modifiedPath);
await fs.writeFile(modifiedPath, code);
try {
const agrammar = /** @type {Parser} */ (
await import(modifiedPath)
);
ok(agrammar);
checkParserStarts(agrammar, starts, true, counts);
} finally {
if (!opts?.noDelete) {
await fs.rm(modifiedPath);
}
}
}
return counts;
}
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@peggyjs/coverage",
"version": "1.0.0",
"version": "1.1.0",
"decription": "Generate better code coverage for Peggy grammars",
"main": "lib/index.js",
"type": "module",
Expand All @@ -25,7 +25,7 @@
"scripts": {
"build": "peggy --format es test/minimal.peggy && tsc",
"lint": "eslint .",
"test": "c8 node --test"
"test": "c8 node --test test/*.test.js"
},
"dependencies": {
"peggy": "4.0.2",
Expand Down
61 changes: 60 additions & 1 deletion test/index.test.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { deepEqual, rejects } from "node:assert";
import test from "node:test";
import { testPeggy } from "../lib/index.js";

test("test peggy coverage", async() => {
await testPeggy(new URL("minimal.js", import.meta.url), [
const counts = await testPeggy(new URL("minimal.js", import.meta.url), [
{
validInput: "foo",
invalidInput: "",
Expand Down Expand Up @@ -31,5 +32,63 @@ test("test peggy coverage", async() => {
peg$silentFails: -1,
},
},
{
validInput: "a",
validResult(r) {
return r;
},
peg$maxFailPos: 1,
options: {
peg$startRuleFunction: "peg$parseinit",
},
},
{
validInput: "aa",
validResult: ["a", "a"],
peg$maxFailPos: 2,
options: {
peg$startRuleFunction: "peg$parseinit",
},
},
]);
deepEqual(counts, {
valid: 10,
invalid: 8,
});
});

test("noGenerate", async() => {
const counts = await testPeggy(new URL("minimal.js", import.meta.url), [
{
validInput: "foo",
invalidInput: "",
},
], {
noGenerate: true,
});

deepEqual(counts, {
valid: 1,
invalid: 1,
});
});

test("noMap", async() => {
const counts = await testPeggy(new URL("minimal.js", import.meta.url), [
{
validInput: "foo",
invalidInput: "",
},
], {
noMap: true,
});

deepEqual(counts, {
valid: 2,
invalid: 2,
});
});

test("edges", async() => {
await rejects(() => testPeggy(), TypeError);
});
23 changes: 19 additions & 4 deletions test/minimal.js
Original file line number Diff line number Diff line change
Expand Up @@ -408,15 +408,30 @@ function peg$parse(input, options) {
}

function peg$parseinit() {
var s0;
var s0, s1;

s0 = input.charAt(peg$currPos);
if (peg$r0.test(s0)) {
s0 = [];
s1 = input.charAt(peg$currPos);
if (peg$r0.test(s1)) {
peg$currPos++;
} else {
s0 = peg$FAILED;
s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e2); }
}
if (s1 !== peg$FAILED) {
while (s1 !== peg$FAILED) {
s0.push(s1);
s1 = input.charAt(peg$currPos);
if (peg$r0.test(s1)) {
peg$currPos++;
} else {
s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e2); }
}
}
} else {
s0 = peg$FAILED;
}

return s0;
}
Expand Down
2 changes: 1 addition & 1 deletion test/minimal.peggy
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ foo
= (ws / init)* @$("fo" .)

init
= [a-b\x7f]
= [a-b\x7f]+

ws "whitespace"
= [ \t\r\n]

0 comments on commit 6123a95

Please sign in to comment.