Rewriting how specs run

This commit is contained in:
Tyler Akins 2023-04-08 11:52:22 -05:00
parent d997ad0e0a
commit 2085dc2792
No known key found for this signature in database
GPG Key ID: 8F3B8C432F4393BD
3 changed files with 382 additions and 202 deletions

168
.eslintrc.yaml Normal file
View File

@ -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

View File

@ -1,16 +1,11 @@
#!/usr/bin/env bash #!/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 # Install or update the specs
if [[ ! -d spec ]]; then if [[ ! -d spec ]]; then
git clone https://github.com/mustache/spec.git spec git clone https://github.com/mustache/spec.git spec
else else
( (
cd spec; cd spec
git pull git pull
) )
fi fi

View File

@ -1,241 +1,258 @@
#!/usr/bin/env node #!/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"');
}
if (typeof value === 'number') {
return value;
}
return 'ERR_CONVERTING';
}
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(' ');
}
// 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') {
return '# ' + name + ' is an object and will not work in bash';
}
if (typeof value === 'boolean') {
if (value) {
return name + '="true"';
}
return name + '=""';
}
return name + '=' + makeShellString(value);
}
function runTest(test, done) {
var output, partials, script;
script = [
'#!/usr/bin/env bash'
];
partials = test.partials || {};
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);
}
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 '';
}
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);
if (test.pass) {
console.log('Passed.');
} else {
console.log('Failed.');
console.log('');
console.log(test);
}
console.log('');
taskDone();
}
], done);
}
function specFileToName(file) { function specFileToName(file) {
return file.replace(/.*\//, '').replace('.json', '').replace('~', '').replace(/(^|-)[a-z]/g, function (match) { return file
.replace(/.*\//, "")
.replace(".json", "")
.replace("~", "")
.replace(/(^|-)[a-z]/g, function (match) {
return match.toUpperCase(); return match.toUpperCase();
}); });
} }
function processSpecFile(specFile, done) { function processArraySequentially(array, callback) {
fs.readFile(specFile, 'utf8', function (err, data) { function processCopy() {
var name; if (arrayCopy.length) {
const item = arrayCopy.shift();
return Promise.resolve(item)
.then(callback)
.then((singleResult) => {
result.push(singleResult);
if (err) { return processCopy();
return done(err);
}
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(); } else {
return Promise.resolve(result);
} }
], done); }
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";
}
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) {
if (Array.isArray(value)) {
return addToEnvironmentArray(name, value);
}
if (typeof value === "object" && value) {
return addToEnvironmentObject(name, value);
}
if (typeof value === "boolean") {
return `${name}="${value ? "true" : ""}"`;
}
return `${name}=${makeShellString(value)}`;
}
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");
script.push("");
return script.join("\n");
}
function writePartials(test) {
return processArraySequentially(
Object.keys(test.partials),
(partialName) => {
debug("Writing partial:", partialName);
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();
}
test.output = stdout;
resolve();
});
});
}
function cleanup() {
return fsPromises.rm("spec-runner/", { force: true, recursive: true });
}
function detectFailure(test) {
if (test.scriptError) {
return true;
}
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;
}
}
console.log(`### ${testSet.name} Results = ${testSet.pass} pass, ${testSet.fail} fail`);
return testSet;
});
}); });
} }
// 0 = node, 1 = script, 2 = file // 0 = node, 1 = script, 2 = file
if (process.argv.length < 3) { 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(); process.exit();
} }
async = require('async'); processArraySequentially(process.argv.slice(2), processSpecFile).then(
fs = require('fs'); (result) => {
exec = require('child_process').exec; console.log('=========================================');
summary = {};
async.eachSeries(process.argv.slice(2), processSpecFile, function () {
var fail, pass;
console.log(''); console.log('');
console.log('Summary'); console.log('Failed Test Summary');
console.log('=======');
console.log(''); console.log('');
pass = 0;
fail = 0;
Object.keys(summary).forEach(function (name) {
var groupPass, groupFail, testResults;
testResults = []; for (const testSet of result) {
groupPass = 0; console.log(`* ${testSet.name}: ${testSet.tests.length} total, ${testSet.pass} pass, ${testSet.fail} fail`);
groupFail = 0;
Object.keys(summary[name]).forEach(function (testName) { for (const test of testSet.tests) {
if (summary[name][testName]) { if (test.isFailure) {
testResults.push(' * pass - ' + testName); console.log(` * Failure: ${test.name}`);
groupPass += 1;
pass += 1;
} else {
testResults.push(' * FAIL - ' + testName);
groupFail += 1;
fail += 1;
} }
}); }
testResults.unshift('* ' + name + ' (failed ' + groupFail + ' out of ' + (groupPass + groupFail) + ' tests)'); }
console.log(testResults.join('\n')); },
}); (err) => {
console.error(err);
console.log(''); console.error("FAILURE RUNNING SCRIPT");
console.log('Failed ' + fail + ' out of ' + (pass + fail) + ' tests'); console.error("Testing artifacts are left in script-runner/ folder");
}); }
);