From 7130e2233204f0f4b01bc179e9ab3d3070d1a0ef Mon Sep 17 00:00:00 2001 From: Malcolm Crum Date: Sun, 22 Dec 2024 22:31:24 +1300 Subject: [PATCH] Format tests, clean up, add automated runner --- .github/workflows/test.yml | 15 ++ pPEG.mjs | 2 - package.json | 59 ++++--- peg-play.mjs | 314 ++++++++++++++++++++----------------- 4 files changed, 212 insertions(+), 178 deletions(-) create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..36bf54b --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,15 @@ +name: Unit tests + +on: + push: + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Install + run: npm install + - name: Run tests + run: npm test diff --git a/pPEG.mjs b/pPEG.mjs index 12bb31a..a961069 100644 --- a/pPEG.mjs +++ b/pPEG.mjs @@ -1220,7 +1220,6 @@ function err_report(env) { */ function compile(grammar, extend, options) { const peg = parse(pPEG_codex, grammar, {}, options); - // console.log(JSON.stringify(peg)); if (!peg.ok) { return { show_err: peg.show_err, @@ -1233,7 +1232,6 @@ function compile(grammar, extend, options) { } try { peg.codex = compiler(peg.ptree[1]); - // console.log("codex\n",JSON.stringify(peg.codex)); } catch (err) { return { err: 1, diff --git a/package.json b/package.json index 657d7b6..a7aeb16 100644 --- a/package.json +++ b/package.json @@ -1,34 +1,29 @@ { - "name": "ppegjs", - "version": "0.1.2", - "description": "A portable Parser Expression Grammar.", - "main": "index.js", - "type": "module", - "directories": { - "example": "examples", - "test": "tests" - }, - "scripts": { - "test": "node peg-play.mjs tests", - "format": "biome format --write pPEG.mjs" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/crummy/pPEGjs.git" - }, - "keywords": [ - "ppeg", - "parser", - "expression", - "grammar" - ], - "author": "Peter Cashin", - "license": "MIT", - "bugs": { - "url": "https://github.com/pcanz/pPEGjs/issues" - }, - "homepage": "https://github.com/pcanz/pPEGjs#readme", - "devDependencies": { - "@biomejs/biome": "1.9.4" - } + "name": "ppegjs", + "version": "0.1.2", + "description": "A portable Parser Expression Grammar.", + "main": "index.js", + "type": "module", + "directories": { + "example": "examples", + "test": "tests" + }, + "scripts": { + "test": "node peg-play.mjs tests", + "format": "biome format --write *.mjs" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/crummy/pPEGjs.git" + }, + "keywords": ["ppeg", "parser", "expression", "grammar"], + "author": "Peter Cashin", + "license": "MIT", + "bugs": { + "url": "https://github.com/pcanz/pPEGjs/issues" + }, + "homepage": "https://github.com/pcanz/pPEGjs#readme", + "devDependencies": { + "@biomejs/biome": "1.9.4" + } } diff --git a/peg-play.mjs b/peg-play.mjs index 918b204..19f5c57 100755 --- a/peg-play.mjs +++ b/peg-play.mjs @@ -46,7 +46,7 @@ Multiple grammars with their tests can be separated with:
========================
a line with at least 4 ==== chars. -WHen reading a grammar any intial comment lines (starting with #) +WHen reading a grammar any initial comment lines (starting with #) will be stripped off, and if there are no grammar rules then this grammar block is skipped over as comments in the test file. @@ -77,165 +77,191 @@ grammar block is skipped over as comments in the test file. `; // doco -import peg from './pPEG.mjs' // <== EDIT.ME to relocate +import peg from "./pPEG.mjs"; // <== EDIT.ME to relocate -import { existsSync, lstatSync, readFileSync, readdirSync } from 'node:fs' +import { existsSync, lstatSync, readFileSync, readdirSync } from "node:fs"; +import process from "node:process"; // check command line args ---------------------------- -let json = false // -j json, default pretty print ptree +let json = false; // -j json, default pretty print ptree let bad_opt = false; // if -x undefined -let path_arg = 2 // argv first cmd arg - -const args = process.argv.length - 2 - -if (args > 0) { // check for option... - const arg1 = process.argv[2] - if (arg1.startsWith('-')) { - path_arg += 1; - if (arg1.startsWith('-h')) { // -help doco .... - console.log(doco); - process.exit(1); - } - if (arg1.startsWith('-j')) json = true; - else bad_opt = true; - } +let path_arg = 2; // argv first cmd arg + +const args = process.argv.length - 2; + +if (args > 0) { + // check for option... + const arg1 = process.argv[2]; + if (arg1.startsWith("-")) { + path_arg += 1; + if (arg1.startsWith("-h")) { + // -help doco .... + console.log(doco); + process.exit(1); + } + if (arg1.startsWith("-j")) json = true; + else bad_opt = true; + } } if (args < 1 || bad_opt) { - console.log("Useage: -option? path-name (file or directory)\n"+ - " option:\n"+ - " -j, -json for json format ptree\n"+ - " -h, -help for more info.\n"); - process.exit(1); + console.log( + "Usage: -option? path-name (file or directory)\n" + + " option:\n" + + " -j, -json for json format ptree\n" + + " -h, -help for more info.\n", + ); + process.exit(1); } // OK run tests --------------------------------------- -for (let path = process.argv[path_arg]; - path_arg < process.argv.length; - path_arg += 1) { - - if (!existsSync(path)) { - console.log("**** Can't find: '"+path+"' in "+process.cwd()) - continue; } - const ptype = lstatSync(path) - if (ptype.isDirectory()) { - const files = readdirSync(path, 'utf8'); - for (const file of files) { - test_file(path+'/'+file, json, true); // silent - } - } else if (ptype.isFile()) { - test_file(path, json); - } +let failure = 0; +for ( + let path = process.argv[path_arg]; + path_arg < process.argv.length; + path_arg += 1 +) { + if (!existsSync(path)) { + console.log(`**** Can't find: '${path}' in ${process.cwd()}`); + continue; + } + const ptype = lstatSync(path); + if (ptype.isDirectory()) { + const files = readdirSync(path, "utf8"); + for (const file of files) { + test_file(`${path}/${file}`, json, true); // silent + } + } else if (ptype.isFile()) { + test_file(path, json); + } } // all args done.. // read and compile the grammar ----------------------------------------- -function test_file(file, json, silent) { - if (!file.endsWith('.txt')) { - say("**** Skip '"+file+"' this is not a .txt file..."); - return; - }; - let f1 = readFileSync(file, 'utf8'); - - if (f1.startsWith("====")) f1 = '\n'+f1; // skips empty grammar - - const grammars = f1.split(/[\n\r]*====+[ \t]*([^ \t\n\r]*)[^\n\r]*\r?\n/); - - let peg_ok = 0, peg_err = 0; // pPEG grammars - - let test_ok = 0, test_err = 0; // input tests - - for (let i=0; i>>> not"); - } else { - say(">>>>"); - } - const tp = pp.parse(s); - if (tp.ok) { - say(tp.show_ptree(json)); - } else { // parse failed ... - say(tp.show_err()); - } - if (tp.ok && !neg || !tp.ok && neg) { - ok += 1; - say("----------------------------- ok "+ok); - } else { - err += 1; - say("***************************** err "+err+" ********"); - } - } - - test_ok += ok; - test_err += err; - peg_ok += 1; - - } // grammars - - if (peg_err === 0 && test_err === 0) { - console.log("OK "+file+": all "+test_ok+" test(s), "+peg_ok+" grammar(s) ....."); - } else { - console.log("**** Error "+file+": Failed "+test_err+" test(s), passed ok "+ - test_ok+" test(s), failed "+peg_err+" grammar(s)"); - } - - function say(msg) { - if (!silent) console.log(msg); - } - - function strip_leading_comments(str) { - if (str === "") return str; - let rx = str.match(/^((?:[ \t\n\r]*#[^\n\r]*[\n\r]*)*)[ \t\n\r]*(.*)/s); - return rx[2]; - } - +/** + * + * @param {string} file + * @param {boolean} json + * @param {boolean} silent + * @return {boolean} + */ +function test_file(file, json, silent = false) { + if (!file.endsWith(".txt")) { + say(`**** Skip '${file}' this is not a .txt file...`); + return false; + } + let f1 = readFileSync(file, "utf8"); + + if (f1.startsWith("====")) f1 = `\n${f1}`; // skips empty grammar + + const grammars = f1.split(/[\n\r]*====+[ \t]*([^ \t\n\r]*)[^\n\r]*\r?\n/); + + let peg_ok = 0; + let peg_err = 0; // pPEG grammars + + let test_ok = 0; + let test_err = 0; // input tests + + for (let i = 0; i < grammars.length; i += 2) { + const tests = grammars[i].split( + /\r?\n----+[ \t]*([^ \t\n\r]*)[^\n\r]*\r?\n/, + ); + + const px = tests[0]; // pPEG grammar source + + const ps = strip_leading_comments(px); // # lines prior to rules + + if (ps === "") continue; // skip grammar that is all comment lines + + let peg_not = false; + if (grammars[i - 1] === "not") { + peg_not = true; + say("==================================================== not"); + } else { + say("========================================================"); + } + + say(px); // pPEG grammar text + + const pp = peg.compile(ps); + + if (!pp.ok) { + // bad grammar + say(pp.show_err()); + say("********************* grammar failed, skip tests...."); + peg_err += peg_not ? 0 : 1; // don't count if expected to fail + continue; + } + if (peg_not) { + // was expected to fail, but didn't + say("********************* grammar was expected to fail ..."); + peg_err += 1; + } + + if (tests[1] === "not") { + say("---------------------------------------------------- not"); + } else { + say("--------------------------------------------------------"); + } + + // parse the input tests ------------------------------------------- + + let ok = 0; + let err = 0; + + for (let i = 2; i < tests.length; i += 2) { + const neg = tests[i - 1] === "not"; + const s = tests[i]; + say(s); + if (neg) { + say(">>>> not"); + } else { + say(">>>>"); + } + const tp = pp.parse(s); + if (tp.ok) { + say(tp.show_ptree(json)); + } else { + // parse failed ... + say(tp.show_err()); + } + if ((tp.ok && !neg) || (!tp.ok && neg)) { + ok += 1; + say(`----------------------------- ok ${ok}`); + } else { + err += 1; + say(`***************************** err ${err} ********`); + } + } + + test_ok += ok; + test_err += err; + peg_ok += 1; + } // grammars + + if (peg_err === 0 && test_err === 0) { + console.log( + `OK ${file}: all ${test_ok} test(s), ${peg_ok} grammar(s) .....`, + ); + } else { + console.log( + `**** Error ${file}: Failed ${test_err} test(s), passed ok ${test_ok} test(s), failed ${peg_err} grammar(s)`, + ); + failure = 1; + } + + function say(msg) { + if (!silent) console.log(msg); + } + + function strip_leading_comments(str) { + if (str === "") return str; + const rx = str.match(/^((?:[ \t\n\r]*#[^\n\r]*[\n\r]*)*)[ \t\n\r]*(.*)/s); + return rx[2]; + } } // test_file -process.exit(0); +process.exit(failure ? 1 : 0);