2015-01-23 20:26:21 +00:00
|
|
|
#!/usr/bin/env node
|
|
|
|
|
2023-04-08 16:52:22 +00:00
|
|
|
const exec = require("child_process").exec;
|
|
|
|
const fsPromises = require("fs").promises;
|
|
|
|
|
2023-04-08 17:41:46 +00:00
|
|
|
// Skip or override portions of tests. The goal is to still have as much
|
|
|
|
// coverage as possible, but skip things that Bash does not support.
|
|
|
|
//
|
|
|
|
// To skip a test, define a "skip" property and explain why the test is
|
|
|
|
// skipped.
|
|
|
|
//
|
|
|
|
// To override any test property, just define that property.
|
|
|
|
const testOverrides = {
|
2023-04-09 15:20:17 +00:00
|
|
|
'Interpolation -> HTML Escaping': {
|
2023-04-08 17:41:46 +00:00
|
|
|
skip: 'HTML escaping is not supported'
|
|
|
|
},
|
|
|
|
'Interpolation -> Implicit Iterators - HTML Escaping': {
|
|
|
|
skip: 'HTML escaping is not supported'
|
|
|
|
},
|
2023-04-09 15:20:17 +00:00
|
|
|
'Lambdas -> Escaping': {
|
2023-04-08 17:41:46 +00:00
|
|
|
skip: 'HTML escaping is not supported'
|
2023-04-09 15:20:17 +00:00
|
|
|
},
|
|
|
|
'Sections -> Dotted Names - Broken Chains': {
|
|
|
|
// Complex objects are not supported
|
|
|
|
template: `"{{#a.b}}Here{{/a.b}}" == ""`
|
|
|
|
},
|
|
|
|
'Sections -> Dotted Names - Falsey': {
|
|
|
|
// Complex objects are not supported
|
|
|
|
data: { a: { b: false } },
|
|
|
|
template: `"{{#a.b}}Here{{/a.b}}" == ""`
|
|
|
|
},
|
|
|
|
'Sections -> Dotted Names - Truthy': {
|
|
|
|
// Complex objects are not supported
|
|
|
|
data: { a: { b: true } },
|
|
|
|
template: `"{{#a.b}}Here{{/a.b}}" == "Here"`
|
2023-04-08 17:41:46 +00:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2023-04-08 16:52:22 +00:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const result = [];
|
|
|
|
const arrayCopy = array.slice();
|
|
|
|
|
|
|
|
return processCopy();
|
|
|
|
}
|
|
|
|
|
|
|
|
function debug(...args) {
|
|
|
|
if (process.env.DEBUG) {
|
|
|
|
console.debug(...args);
|
|
|
|
}
|
|
|
|
}
|
2015-01-23 20:26:21 +00:00
|
|
|
|
|
|
|
function makeShellString(value) {
|
2023-04-09 15:20:17 +00:00
|
|
|
if (typeof value === "boolean") {
|
|
|
|
return value ? '"true"' : '""';
|
|
|
|
}
|
|
|
|
|
2023-04-08 16:52:22 +00:00
|
|
|
if (typeof value === "string") {
|
2015-01-26 21:08:23 +00:00
|
|
|
// Newlines are tricky
|
2023-04-08 16:52:22 +00:00
|
|
|
return value
|
|
|
|
.split(/\n/)
|
|
|
|
.map(function (chunk) {
|
|
|
|
return JSON.stringify(chunk);
|
|
|
|
})
|
|
|
|
.join('"\n"');
|
2015-01-23 20:26:21 +00:00
|
|
|
}
|
|
|
|
|
2023-04-08 16:52:22 +00:00
|
|
|
if (typeof value === "number") {
|
2015-01-23 20:26:21 +00:00
|
|
|
return value;
|
|
|
|
}
|
|
|
|
|
2023-04-08 16:52:22 +00:00
|
|
|
return "ERR_CONVERTING";
|
2015-01-23 20:26:21 +00:00
|
|
|
}
|
|
|
|
|
2023-04-08 16:52:22 +00:00
|
|
|
function addToEnvironmentArray(name, value) {
|
|
|
|
const result = ["("];
|
|
|
|
value.forEach(function (subValue) {
|
|
|
|
result.push(makeShellString(subValue));
|
|
|
|
});
|
|
|
|
result.push(")");
|
2015-01-23 20:26:21 +00:00
|
|
|
|
2023-04-08 16:52:22 +00:00
|
|
|
return name + "=" + result.join(" ");
|
|
|
|
}
|
2015-01-23 20:26:21 +00:00
|
|
|
|
2023-04-09 15:20:17 +00:00
|
|
|
function addToEnvironmentObjectConvertedToAssociativeArray(name, value) {
|
|
|
|
const values = [];
|
|
|
|
|
|
|
|
for (const [k, v] of Object.entries(value)) {
|
|
|
|
if (typeof v === 'object') {
|
|
|
|
if (v) {
|
|
|
|
// An object - abort
|
|
|
|
return `# ${name}.${k} is an object that can not be converted to an associative array`;
|
2023-04-08 17:41:46 +00:00
|
|
|
}
|
2015-01-26 18:50:11 +00:00
|
|
|
|
2023-04-09 15:20:17 +00:00
|
|
|
// null
|
|
|
|
values.push(`[${k}]=`);
|
|
|
|
} else {
|
|
|
|
values.push(`[${k}]=${makeShellString(v)}`);
|
2023-04-08 17:41:46 +00:00
|
|
|
}
|
2023-04-09 15:20:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return `declare -A ${name}\n${name}=(${values.join(' ')})`;
|
|
|
|
}
|
2015-01-26 18:50:11 +00:00
|
|
|
|
2023-04-09 15:20:17 +00:00
|
|
|
function addToEnvironmentObject(name, value) {
|
|
|
|
if (!value) {
|
|
|
|
// null
|
|
|
|
return `#${name} is null`;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Sometimes the __tag__ property of the code in the lambdas may be
|
|
|
|
// missing. Compensate by detecting commonly defined languages.
|
|
|
|
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"'"))'; }`;
|
2015-01-23 20:26:21 +00:00
|
|
|
}
|
|
|
|
|
2023-04-09 15:20:17 +00:00
|
|
|
|
|
|
|
return addToEnvironmentObjectConvertedToAssociativeArray(name, value);
|
2023-04-08 16:52:22 +00:00
|
|
|
}
|
2015-01-23 20:26:21 +00:00
|
|
|
|
2023-04-08 16:52:22 +00:00
|
|
|
function addToEnvironment(name, value) {
|
|
|
|
if (Array.isArray(value)) {
|
|
|
|
return addToEnvironmentArray(name, value);
|
2015-01-23 20:26:21 +00:00
|
|
|
}
|
|
|
|
|
2023-04-08 17:41:46 +00:00
|
|
|
if (typeof value === "object") {
|
2023-04-08 16:52:22 +00:00
|
|
|
return addToEnvironmentObject(name, value);
|
|
|
|
}
|
2015-01-23 20:26:21 +00:00
|
|
|
|
2023-04-08 16:52:22 +00:00
|
|
|
return `${name}=${makeShellString(value)}`;
|
|
|
|
}
|
2015-01-23 20:26:21 +00:00
|
|
|
|
2023-04-08 16:52:22 +00:00
|
|
|
function buildScript(test) {
|
|
|
|
const script = ["#!/usr/bin/env bash"];
|
2015-01-23 20:26:21 +00:00
|
|
|
Object.keys(test.data).forEach(function (name) {
|
|
|
|
script.push(addToEnvironment(name, test.data[name]));
|
|
|
|
});
|
2023-04-08 16:52:22 +00:00
|
|
|
script.push(". ./mo");
|
|
|
|
script.push("mo spec-runner/spec-template");
|
|
|
|
script.push("");
|
2015-01-26 18:50:11 +00:00
|
|
|
|
2023-04-08 16:52:22 +00:00
|
|
|
return script.join("\n");
|
|
|
|
}
|
2015-01-23 20:26:21 +00:00
|
|
|
|
2023-04-08 16:52:22 +00:00
|
|
|
function writePartials(test) {
|
|
|
|
return processArraySequentially(
|
|
|
|
Object.keys(test.partials),
|
|
|
|
(partialName) => {
|
|
|
|
debug("Writing partial:", partialName);
|
2015-01-23 20:26:21 +00:00
|
|
|
|
2023-04-08 16:52:22 +00:00
|
|
|
return fsPromises.writeFile(
|
|
|
|
"spec-runner/" + partialName,
|
|
|
|
test.partials[partialName]
|
|
|
|
);
|
2015-01-23 20:26:21 +00:00
|
|
|
}
|
2023-04-08 16:52:22 +00:00
|
|
|
);
|
2015-01-23 20:26:21 +00:00
|
|
|
}
|
|
|
|
|
2023-04-08 16:52:22 +00:00
|
|
|
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();
|
|
|
|
});
|
2015-01-23 20:26:21 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2023-04-08 16:52:22 +00:00
|
|
|
function cleanup() {
|
|
|
|
return fsPromises.rm("spec-runner/", { force: true, recursive: true });
|
|
|
|
}
|
2015-01-23 20:26:21 +00:00
|
|
|
|
2023-04-08 16:52:22 +00:00
|
|
|
function detectFailure(test) {
|
|
|
|
if (test.scriptError) {
|
|
|
|
return true;
|
|
|
|
}
|
2015-01-23 20:26:21 +00:00
|
|
|
|
2023-04-08 16:52:22 +00:00
|
|
|
if (test.output !== test.expected) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2023-04-08 17:41:46 +00:00
|
|
|
function showFailureDetails(test) {
|
|
|
|
console.log(`FAILURE: ${test.fullName}`);
|
2023-04-08 16:52:22 +00:00
|
|
|
console.log('');
|
|
|
|
console.log(test.desc);
|
|
|
|
console.log('');
|
2023-04-08 17:41:46 +00:00
|
|
|
console.log(JSON.stringify(test, null, 4));
|
|
|
|
}
|
|
|
|
|
|
|
|
function applyTestOverrides(test) {
|
2023-04-09 15:20:17 +00:00
|
|
|
const overrides = testOverrides[test.fullName];
|
|
|
|
const originals = {};
|
|
|
|
|
|
|
|
if (!overrides) {
|
|
|
|
return;
|
|
|
|
}
|
2023-04-08 17:41:46 +00:00
|
|
|
|
|
|
|
for (const [key, value] of Object.entries(overrides)) {
|
2023-04-09 15:20:17 +00:00
|
|
|
originals[key] = test[key];
|
2023-04-08 17:41:46 +00:00
|
|
|
test[key] = value;
|
|
|
|
}
|
2023-04-09 15:20:17 +00:00
|
|
|
|
|
|
|
test.valuesBeforeOverride = originals;
|
2023-04-08 16:52:22 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
function runTest(testSet, test) {
|
|
|
|
test.partials = test.partials || {};
|
2023-04-08 17:41:46 +00:00
|
|
|
test.fullName = `${testSet.name} -> ${test.name}`;
|
|
|
|
applyTestOverrides(test);
|
2023-04-09 15:20:17 +00:00
|
|
|
test.script = buildScript(test);
|
2023-04-08 17:41:46 +00:00
|
|
|
|
|
|
|
if (test.skip) {
|
|
|
|
debug('Skipping test:', testSet.fullName, `$(${test.skip})`);
|
|
|
|
|
|
|
|
return Promise.resolve();
|
|
|
|
}
|
|
|
|
|
|
|
|
debug('Running test:', testSet.fullName);
|
2023-04-08 16:52:22 +00:00
|
|
|
|
|
|
|
return setupEnvironment(test)
|
|
|
|
.then(() => executeScript(test))
|
|
|
|
.then(cleanup)
|
2023-04-08 17:41:46 +00:00
|
|
|
.then(() => {
|
|
|
|
test.isFailure = detectFailure(test);
|
|
|
|
|
|
|
|
if (test.isFailure) {
|
|
|
|
showFailureDetails(test);
|
|
|
|
}
|
|
|
|
});
|
2023-04-08 16:52:22 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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;
|
2023-04-08 17:41:46 +00:00
|
|
|
testSet.skip = 0;
|
2023-04-08 16:52:22 +00:00
|
|
|
|
|
|
|
for (const test of testSet.tests) {
|
|
|
|
if (test.isFailure) {
|
|
|
|
testSet.fail += 1;
|
2023-04-08 17:41:46 +00:00
|
|
|
} else if (test.skip) {
|
|
|
|
testSet.skip += 1;
|
2023-04-08 16:52:22 +00:00
|
|
|
} else {
|
|
|
|
testSet.pass += 1;
|
|
|
|
}
|
2015-01-23 20:26:21 +00:00
|
|
|
}
|
2023-04-08 17:41:46 +00:00
|
|
|
console.log(`### ${testSet.name} Results = ${testSet.pass} passed, ${testSet.fail} failed, ${testSet.skip} skipped`);
|
2023-04-08 16:52:22 +00:00
|
|
|
|
|
|
|
return testSet;
|
|
|
|
});
|
2015-01-23 20:26:21 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
// 0 = node, 1 = script, 2 = file
|
|
|
|
if (process.argv.length < 3) {
|
2023-04-08 16:52:22 +00:00
|
|
|
console.log("Specify one or more JSON spec files on the command line");
|
2015-01-23 20:26:21 +00:00
|
|
|
process.exit();
|
|
|
|
}
|
|
|
|
|
2023-04-08 16:52:22 +00:00
|
|
|
processArraySequentially(process.argv.slice(2), processSpecFile).then(
|
|
|
|
(result) => {
|
|
|
|
console.log('=========================================');
|
|
|
|
console.log('');
|
|
|
|
console.log('Failed Test Summary');
|
|
|
|
console.log('');
|
2023-04-08 17:41:46 +00:00
|
|
|
let pass = 0, fail = 0, skip = 0, total = 0;
|
2015-01-23 20:26:21 +00:00
|
|
|
|
2023-04-08 16:52:22 +00:00
|
|
|
for (const testSet of result) {
|
2023-04-08 17:41:46 +00:00
|
|
|
pass += testSet.pass;
|
|
|
|
fail += testSet.fail;
|
|
|
|
skip += testSet.skip;
|
|
|
|
total += testSet.tests.length;
|
|
|
|
|
|
|
|
console.log(`* ${testSet.name}: ${testSet.tests.length} total, ${testSet.pass} pass, ${testSet.fail} fail, ${testSet.skip} skip`);
|
2015-01-23 20:26:21 +00:00
|
|
|
|
2023-04-08 16:52:22 +00:00
|
|
|
for (const test of testSet.tests) {
|
|
|
|
if (test.isFailure) {
|
|
|
|
console.log(` * Failure: ${test.name}`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2023-04-08 17:41:46 +00:00
|
|
|
|
|
|
|
console.log('');
|
|
|
|
console.log(`Final result: ${total} total, ${pass} pass, ${fail} fail, ${skip} skip`);
|
|
|
|
|
|
|
|
if (fail) {
|
|
|
|
process.exit(1);
|
|
|
|
}
|
2023-04-08 16:52:22 +00:00
|
|
|
},
|
|
|
|
(err) => {
|
|
|
|
console.error(err);
|
|
|
|
console.error("FAILURE RUNNING SCRIPT");
|
|
|
|
console.error("Testing artifacts are left in script-runner/ folder");
|
|
|
|
}
|
|
|
|
);
|