From 2085dc2792d536eb589b4cad2d7411af190b8f69 Mon Sep 17 00:00:00 2001 From: Tyler Akins Date: Sat, 8 Apr 2023 11:52:22 -0500 Subject: [PATCH] Rewriting how specs run --- .eslintrc.yaml | 168 ++++++++++++++++++++ run-spec | 7 +- run-spec.js | 409 +++++++++++++++++++++++++------------------------ 3 files changed, 382 insertions(+), 202 deletions(-) create mode 100644 .eslintrc.yaml diff --git a/.eslintrc.yaml b/.eslintrc.yaml new file mode 100644 index 0000000..6ee5d88 --- /dev/null +++ b/.eslintrc.yaml @@ -0,0 +1,168 @@ +parserOptions: + ecmaVersion: latest + sourceType: module +env: + es6: true + jasmine: true + node: true +extends: eslint:recommended +rules: + accessor-pairs: error + array-bracket-spacing: + - error + - never + array-callback-return: error + block-spacing: + - error + - never + brace-style: error + comma-dangle: error + comma-spacing: error + comma-style: error + complexity: + - error + - 10 + computed-property-spacing: error + consistent-return: error + consistent-this: error + constructor-super: error + curly: error + default-case: error + dot-notation: error + eol-last: error + eqeqeq: error + generator-star-spacing: error + global-require: off + guard-for-in: error + jsx-quotes: error + key-spacing: error + keyword-spacing: error + linebreak-style: error + lines-around-comment: + - error + - + allowBlockStart: true + allowObjectStart: true + allowArrayStart: true + max-statements-per-line: error + new-cap: error + new-parens: error + no-array-constructor: error + no-bitwise: error + no-caller: error + no-case-declarations: error + no-catch-shadow: error + no-class-assign: error + no-cond-assign: error + no-confusing-arrow: error + no-console: off + no-const-assign: error + no-constant-condition: error + no-continue: error + no-delete-var: error + no-dupe-args: error + no-dupe-class-members: error + no-dupe-keys: error + no-duplicate-case: error + no-duplicate-imports: error + no-empty: off + no-empty-character-class: error + no-empty-pattern: error + no-eq-null: error + no-eval: error + no-extend-native: error + no-extra-bind: error + no-extra-boolean-cast: error + no-extra-label: error + no-extra-semi: error + no-fallthrough: error + no-func-assign: error + no-implied-eval: error + no-inner-declarations: error + no-invalid-this: error + no-invalid-regexp: error + no-irregular-whitespace: error + no-iterator: error + no-label-var: error + no-labels: error + no-lone-blocks: error + no-lonely-if: error + no-loop-func: error + no-mixed-spaces-and-tabs: error + no-multi-spaces: error + no-multi-str: error + no-multiple-empty-lines: + - error + - + max: 2 + no-native-reassign: error + no-negated-condition: error + no-nested-ternary: error + no-new: error + no-new-func: error + no-new-object: error + no-new-symbol: error + no-new-wrappers: error + no-obj-calls: error + no-octal: error + no-octal-escape: error + no-path-concat: error + no-plusplus: error + no-proto: error + no-redeclare: error + no-regex-spaces: error + no-restricted-globals: error + no-return-assign: error + no-script-url: error + no-self-assign: error + no-self-compare: error + no-sequences: error + no-shadow: error + no-shadow-restricted-names: error + no-spaced-func: error + no-sparse-arrays: error + no-this-before-super: error + no-throw-literal: error + no-trailing-spaces: error + no-undef: error + no-undef-init: error + no-unexpected-multiline: error + no-unmodified-loop-condition: error + no-unneeded-ternary: error + no-unreachable: error + no-unsafe-finally: error + no-unused-expressions: error + no-unused-labels: error + no-unused-vars: error + no-useless-call: error + no-useless-computed-key: error + no-useless-concat: error + no-useless-constructor: error + no-useless-escape: error + no-void: error + no-warning-comments: warn + no-whitespace-before-property: error + no-with: error + operator-assignment: error + padded-blocks: + - error + - never + prefer-const: error + quote-props: + - error + - as-needed + radix: error + require-yield: error + semi: error + semi-spacing: error + space-before-blocks: error + space-in-parens: error + space-infix-ops: + - error + - + int32Hint: false + space-unary-ops: error + spaced-comment: error + use-isnan: error + valid-typeof: error + yield-star-spacing: error diff --git a/run-spec b/run-spec index 8d8676d..ba1ac8d 100755 --- a/run-spec +++ b/run-spec @@ -1,16 +1,11 @@ #!/usr/bin/env bash -# Create a package.json so the dependency package is installed in the local -# directory -echo '{"private":true, "dependencies":{"async": "*"}}' > package.json -npm install - # Install or update the specs if [[ ! -d spec ]]; then git clone https://github.com/mustache/spec.git spec else ( - cd spec; + cd spec git pull ) fi diff --git a/run-spec.js b/run-spec.js index 69b290d..eb61807 100644 --- a/run-spec.js +++ b/run-spec.js @@ -1,241 +1,258 @@ #!/usr/bin/env node -var async, exec, fs, summary; +const exec = require("child_process").exec; +const fsPromises = require("fs").promises; -function makeShellString(value) { - if (typeof value === 'string') { - // Newlines are tricky - return value.split(/\n/).map(function (chunk) { - return JSON.stringify(chunk); - }).join('"\n"'); +function specFileToName(file) { + return file + .replace(/.*\//, "") + .replace(".json", "") + .replace("~", "") + .replace(/(^|-)[a-z]/g, function (match) { + return match.toUpperCase(); + }); +} + +function processArraySequentially(array, callback) { + function processCopy() { + if (arrayCopy.length) { + const item = arrayCopy.shift(); + return Promise.resolve(item) + .then(callback) + .then((singleResult) => { + result.push(singleResult); + + return processCopy(); + }); + } else { + return Promise.resolve(result); + } } - if (typeof value === 'number') { + const result = []; + const arrayCopy = array.slice(); + + return processCopy(); +} + +function debug(...args) { + if (process.env.DEBUG) { + console.debug(...args); + } +} + +function makeShellString(value) { + if (typeof value === "string") { + // Newlines are tricky + return value + .split(/\n/) + .map(function (chunk) { + return JSON.stringify(chunk); + }) + .join('"\n"'); + } + + if (typeof value === "number") { return value; } - return 'ERR_CONVERTING'; + return "ERR_CONVERTING"; +} + +function addToEnvironmentArray(name, value) { + const result = ["("]; + value.forEach(function (subValue) { + result.push(makeShellString(subValue)); + }); + result.push(")"); + + return name + "=" + result.join(" "); +} + +function addToEnvironmentObject(name, value) { + // Sometimes the __tag__ property of the code in the lambdas may + // be missing. :-( + if ( + (value && value.__tag__ === "code") || + (value.ruby && value.php && value.perl) + ) { + if (value.bash) { + return `${name}() { ${value.bash}; }`; + } + + return `${name}() { perl -e 'print ((${value.perl})->("'"$1"'"))'; }`; + } + + if (value) { + return `#${name} is an object and will not work in Bash`; + } + + // null + return `#${name} is null`; } function addToEnvironment(name, value) { - var result; - if (Array.isArray(value)) { - result = [ - '(' - ]; - value.forEach(function (subValue) { - result.push(makeShellString(subValue)); - }); - result.push(')'); - - return name + '=' + result.join(' '); + return addToEnvironmentArray(name, value); } - // Sometimes the __tag__ property of the code in the lambdas may - // be missing. :-( - if (typeof value === 'object') { - if (value.__tag__ === 'code' || (value.ruby && value.php && value.perl)) { - if (value.bash) { - return name + '() { ' + value.bash + '; }'; - } - - return name + '() { perl -e \'print ((' + value.perl + ')->("\'"$1"\'"))\'; }'; - } + if (typeof value === "object" && value) { + return addToEnvironmentObject(name, value); } - - if (typeof value === 'object') { - return '# ' + name + ' is an object and will not work in bash'; + if (typeof value === "boolean") { + return `${name}="${value ? "true" : ""}"`; } - if (typeof value === 'boolean') { - if (value) { - return name + '="true"'; - } - - return name + '=""'; - } - - return name + '=' + makeShellString(value); + return `${name}=${makeShellString(value)}`; } -function runTest(test, done) { - var output, partials, script; - - script = [ - '#!/usr/bin/env bash' - ]; - partials = test.partials || {}; - +function buildScript(test) { + const script = ["#!/usr/bin/env bash"]; Object.keys(test.data).forEach(function (name) { script.push(addToEnvironment(name, test.data[name])); }); - script.push('. mo'); - script.push('mo spec-runner/spec-template'); - test.script = script.join('\n'); - async.series([ - function (taskDone) { - fs.mkdir("spec-runner/", function (err) { - if (err && err.code !== 'EEXIST') { - return taskDone(err); - } + script.push(". ./mo"); + script.push("mo spec-runner/spec-template"); + script.push(""); - taskDone(); - }); - }, - function (taskDone) { - fs.writeFile('spec-runner/spec-script', test.script, taskDone); - }, - function (taskDone) { - fs.writeFile('spec-runner/spec-template', test.template, taskDone); - }, - function (taskDone) { - async.eachSeries(Object.keys(partials), function (partialName, partialDone) { - fs.writeFile('spec-runner/' + partialName, test.partials[partialName], partialDone); - }, taskDone); - }, - function (taskDone) { - exec('bash spec-runner/spec-script', function (err, stdout) { - if (err) { - return taskDone(err); - } - - output = stdout; - taskDone(); - }); - }, - function (taskDone) { - async.eachSeries(Object.keys(partials), function (partialName, partialDone) { - fs.unlink('spec-runner/' + partialName, partialDone); - }, taskDone); - }, - function (taskDone) { - fs.unlink('spec-runner/spec-script', taskDone); - }, - function (taskDone) { - fs.unlink('spec-runner/spec-template', taskDone); - }, - function (taskDone) { - fs.rmdir('spec-runner/', taskDone); - } - ], function (err) { - if (err) { - return done(err); - } - - done(null, output); - }); - - return ''; + return script.join("\n"); } -function prepareAndRunTest(test, done) { - async.waterfall([ - function (taskDone) { - console.log('### ' + test.name); - console.log(''); - console.log(test.desc); - console.log(''); - runTest(test, taskDone); - }, - function (actual, taskDone) { - test.actual = actual; - test.pass = (test.actual === test.expected); +function writePartials(test) { + return processArraySequentially( + Object.keys(test.partials), + (partialName) => { + debug("Writing partial:", partialName); - if (test.pass) { - console.log('Passed.'); - } else { - console.log('Failed.'); - console.log(''); - console.log(test); + return fsPromises.writeFile( + "spec-runner/" + partialName, + test.partials[partialName] + ); + } + ); +} + +function setupEnvironment(test) { + return cleanup() + .then(() => fsPromises.mkdir("spec-runner/")) + .then(() => + fsPromises.writeFile("spec-runner/spec-script", test.script) + ) + .then(() => + fsPromises.writeFile("spec-runner/spec-template", test.template) + ) + .then(() => writePartials(test)); +} + +function executeScript(test) { + return new Promise((resolve) => { + exec("bash spec-runner/spec-script 2>&1", { + timeout: 2000 + }, (err, stdout) => { + if (err) { + test.scriptError = err.toString(); } - console.log(''); - taskDone(); - } - ], done); -} - -function specFileToName(file) { - return file.replace(/.*\//, '').replace('.json', '').replace('~', '').replace(/(^|-)[a-z]/g, function (match) { - return match.toUpperCase(); + test.output = stdout; + resolve(); + }); }); } -function processSpecFile(specFile, done) { - fs.readFile(specFile, 'utf8', function (err, data) { - var name; +function cleanup() { + return fsPromises.rm("spec-runner/", { force: true, recursive: true }); +} - if (err) { - return done(err); - } +function detectFailure(test) { + if (test.scriptError) { + return true; + } - name = specFileToName(specFile); - data = JSON.parse(data); - console.log(name); - console.log('===================='); - console.log(''); - console.log(data.overview); - console.log(''); - console.log('Tests'); - console.log('-----'); - console.log(''); - async.series([ - function (taskDone) { - async.eachSeries(data.tests, prepareAndRunTest, taskDone); - }, - function (taskDone) { - summary[name] = {}; - data.tests.forEach(function (test) { - summary[name][test.name] = test.pass; - }); - taskDone(); + if (test.output !== test.expected) { + return true; + } + + return false; +} + +function showFailureDetails(testSet, test) { + if (!test.isFailure) { + return; + } + + console.log(`FAILURE: ${testSet.name} -> ${test.name}`) + console.log(''); + console.log(test.desc); + console.log(''); + console.log(test); +} + +function runTest(testSet, test) { + test.script = buildScript(test); + test.partials = test.partials || {}; + debug('Running test:', testSet.name, "->", test.name); + + return setupEnvironment(test) + .then(() => executeScript(test)) + .then(cleanup) + .then(() => test.isFailure = detectFailure(test)) + .then(() => showFailureDetails(testSet, test)); +} + +function processSpecFile(filename) { + debug("Read spec file:", filename); + + return fsPromises.readFile(filename, "utf8").then((fileContents) => { + const testSet = JSON.parse(fileContents); + testSet.name = specFileToName(filename); + + return processArraySequentially(testSet.tests, (test) => + runTest(testSet, test) + ).then(() => { + testSet.pass = 0; + testSet.fail = 0; + + for (const test of testSet.tests) { + if (test.isFailure) { + testSet.fail += 1; + } else { + testSet.pass += 1; + } } - ], done); + console.log(`### ${testSet.name} Results = ${testSet.pass} pass, ${testSet.fail} fail`); + + return testSet; + }); }); } // 0 = node, 1 = script, 2 = file if (process.argv.length < 3) { - console.log('Specify one or more JSON spec files on the command line'); + console.log("Specify one or more JSON spec files on the command line"); process.exit(); } -async = require('async'); -fs = require('fs'); -exec = require('child_process').exec; -summary = {}; -async.eachSeries(process.argv.slice(2), processSpecFile, function () { - var fail, pass; +processArraySequentially(process.argv.slice(2), processSpecFile).then( + (result) => { + console.log('========================================='); + console.log(''); + console.log('Failed Test Summary'); + console.log(''); - console.log(''); - console.log('Summary'); - console.log('======='); - console.log(''); - pass = 0; - fail = 0; - Object.keys(summary).forEach(function (name) { - var groupPass, groupFail, testResults; - - testResults = []; - groupPass = 0; - groupFail = 0; - Object.keys(summary[name]).forEach(function (testName) { - if (summary[name][testName]) { - testResults.push(' * pass - ' + testName); - groupPass += 1; - pass += 1; - } else { - testResults.push(' * FAIL - ' + testName); - groupFail += 1; - fail += 1; + for (const testSet of result) { + console.log(`* ${testSet.name}: ${testSet.tests.length} total, ${testSet.pass} pass, ${testSet.fail} fail`); + + for (const test of testSet.tests) { + if (test.isFailure) { + console.log(` * Failure: ${test.name}`); + } } - }); - testResults.unshift('* ' + name + ' (failed ' + groupFail + ' out of ' + (groupPass + groupFail) + ' tests)'); - console.log(testResults.join('\n')); - }); - - console.log(''); - console.log('Failed ' + fail + ' out of ' + (pass + fail) + ' tests'); -}); + } + }, + (err) => { + console.error(err); + console.error("FAILURE RUNNING SCRIPT"); + console.error("Testing artifacts are left in script-runner/ folder"); + } +);