Add "mo" from "https://git.knownelement.com/ExternalVendorCode/mo.git@master"
git-vendor-name: mo git-vendor-dir: vendor/git.knownelement.com/ExternalVendorCode/mo git-vendor-repository: https://git.knownelement.com/ExternalVendorCode/mo.git git-vendor-ref: master
This commit is contained in:
512
vendor/git.knownelement.com/ExternalVendorCode/mo/run-spec.js
generated
vendored
Normal file
512
vendor/git.knownelement.com/ExternalVendorCode/mo/run-spec.js
generated
vendored
Normal file
@@ -0,0 +1,512 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const exec = require("child_process").exec;
|
||||
const fsPromises = require("fs").promises;
|
||||
|
||||
// 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. It replaces the
|
||||
// original property, not augmenting it.
|
||||
const testOverrides = {
|
||||
"Comments -> Variable Name Collision": {
|
||||
// Can't use variables with exclamation points easily
|
||||
data: {
|
||||
comment: 4
|
||||
}
|
||||
},
|
||||
"Interpolation -> Dotted Names - Arbitrary Depth": {
|
||||
skip: "Not able to use more than one level of depth"
|
||||
},
|
||||
"Interpolation -> Dotted Names - Broken Chain Resolution": {
|
||||
data: {
|
||||
a: {
|
||||
b: "wrong"
|
||||
},
|
||||
name: "Jim"
|
||||
},
|
||||
template: '"{{a.name}}" == ""'
|
||||
},
|
||||
"Interpolation -> Dotted Names - Initial Resolution": {
|
||||
data: {
|
||||
a: {
|
||||
name: "Phil"
|
||||
},
|
||||
name: "Wrong"
|
||||
},
|
||||
template: "\"{{#a}}{{name}}{{/a}}\" == \"Phil\""
|
||||
},
|
||||
"Interpolation -> Implicit Iterators - Ampersand": {
|
||||
skip: "HTML escaping is not supported"
|
||||
},
|
||||
"Interpolation -> Implicit Iterators - Basic Interpolation": {
|
||||
skip: "Can not use {{.}} outside of a loop. Need to use a variable name."
|
||||
},
|
||||
"Interpolation -> Implicit Iterators - Basic Integer Interpolation": {
|
||||
skip: "Can not use {{.}} outside of a loop. Need to use a variable name."
|
||||
},
|
||||
"Interpolation -> Implicit Iterators - Triple Mustache": {
|
||||
skip: "Can not use {{.}} outside of a loop. Need to use a variable name."
|
||||
},
|
||||
"Interpolation -> HTML Escaping": {
|
||||
skip: "HTML escaping is not supported"
|
||||
},
|
||||
"Interpolation -> Implicit Iterators - HTML Escaping": {
|
||||
skip: "HTML escaping is not supported"
|
||||
},
|
||||
"Inverted -> Dotted Names - Falsey": {
|
||||
data: {
|
||||
a: {
|
||||
b: ""
|
||||
}
|
||||
},
|
||||
template: '"{{^a.b}}Not Here{{/a.b}}" == "Not Here"'
|
||||
},
|
||||
"Inverted -> Dotted Names - Truthy": {
|
||||
data: {
|
||||
a: {
|
||||
b: "1"
|
||||
}
|
||||
},
|
||||
template: '"{{^a.b}}Not Here{{/a.b}}" == ""'
|
||||
},
|
||||
"Lambdas -> Escaping": {
|
||||
skip: "HTML escaping is not supported"
|
||||
},
|
||||
"Lambdas -> Interpolation - Alternate Delimiters": {
|
||||
skip: "There is no difference between a lamba used as a value and a lambda used as a block. Both will parse using the current delimiters."
|
||||
},
|
||||
"Lambdas -> Inverted Section": {
|
||||
// This one passed mostly by accident. Correcting so the test still
|
||||
// tests what is was designed to illustrate.
|
||||
data: {
|
||||
static: "static",
|
||||
lambda: {
|
||||
__tag__: 'code',
|
||||
bash: 'false'
|
||||
}
|
||||
}
|
||||
},
|
||||
"Lambdas -> Interpolation": {
|
||||
data: {
|
||||
lambda: {
|
||||
__tag__: 'code',
|
||||
bash: 'echo -n "world"'
|
||||
}
|
||||
}
|
||||
},
|
||||
"Lambdas -> Interpolation - Expansion": {
|
||||
data: {
|
||||
lambda: {
|
||||
__tag__: 'code',
|
||||
bash: 'mo::parse result "{{planet}}"; echo -n "$result"'
|
||||
},
|
||||
planet: 'world'
|
||||
}
|
||||
},
|
||||
"Lambdas -> Interpolation - Multiple Calls": {
|
||||
skip: "Calls are not cached, but they run in isolated environments, so saving a global variable does not work."
|
||||
},
|
||||
"Lambdas -> Section": {
|
||||
data: {
|
||||
lambda: {
|
||||
__tag__: 'code',
|
||||
bash: 'if [[ "$(cat)" == "{{x}}" ]]; then echo -n yes; else echo -n no; fi'
|
||||
},
|
||||
x: "Error!"
|
||||
}
|
||||
},
|
||||
"Lambdas -> Section - Alternate Delimiters": {
|
||||
data: {
|
||||
lambda: {
|
||||
__tag__: 'code',
|
||||
bash: 'local content=$(cat); mo::parse content "$content{{planet}} => |planet|$content"; echo -n "$content"'
|
||||
},
|
||||
planet: 'Earth'
|
||||
}
|
||||
},
|
||||
"Lambdas -> Section - Expansion": {
|
||||
data: {
|
||||
lambda: {
|
||||
__tag__: 'code',
|
||||
bash: 'local content=$(cat); mo::parse content "$content{{planet}}$content"; echo -n "$content"'
|
||||
},
|
||||
planet: "Earth"
|
||||
}
|
||||
},
|
||||
"Lambdas -> Section - Multiple Calls": {
|
||||
data: {
|
||||
lambda: {
|
||||
__tag__: 'code',
|
||||
bash: 'echo -n "__$(cat)__"'
|
||||
}
|
||||
}
|
||||
},
|
||||
"Partials -> Recursion": {
|
||||
skip: "Complex objects are not supported and context is reset to the global level, so the recursion will loop forever"
|
||||
},
|
||||
"Sections -> Deeply Nested Contexts": {
|
||||
skip: "Nested objects are not supported"
|
||||
},
|
||||
"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"`
|
||||
},
|
||||
"Sections -> Implicit Iterator - Array": {
|
||||
skip: "Nested arrays are not supported"
|
||||
},
|
||||
"Sections -> List": {
|
||||
// Arrays of objects are not supported
|
||||
data: { list: [1, 2, 3] },
|
||||
template: `"{{#list}}{{.}}{{/list}}"`
|
||||
},
|
||||
"Sections -> List Context": {
|
||||
skip: "Deeply nested objects are not supported"
|
||||
},
|
||||
"Sections -> List Contexts": {
|
||||
skip: "Deeply nested objects are not supported"
|
||||
}
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
function makeShellString(value) {
|
||||
if (typeof value === "boolean") {
|
||||
return value ? '"true"' : '""';
|
||||
}
|
||||
|
||||
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 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`;
|
||||
}
|
||||
|
||||
// null
|
||||
values.push(`[${k}]=`);
|
||||
} else {
|
||||
values.push(`[${k}]=${makeShellString(v)}`);
|
||||
}
|
||||
}
|
||||
|
||||
return `declare -A ${name}\n${name}=(${values.join(" ")})`;
|
||||
}
|
||||
|
||||
function addToEnvironmentObject(name, value) {
|
||||
if (!value) {
|
||||
// null
|
||||
return `#${name} is null`;
|
||||
}
|
||||
|
||||
if (value.__tag__ === "code") {
|
||||
return `${name}() { ${value.bash || 'echo "NO BASH VERSION OF CODE"'}; }`;
|
||||
}
|
||||
|
||||
return addToEnvironmentObjectConvertedToAssociativeArray(name, value);
|
||||
}
|
||||
|
||||
function addToEnvironment(name, value) {
|
||||
if (Array.isArray(value)) {
|
||||
return addToEnvironmentArray(name, value);
|
||||
}
|
||||
|
||||
if (typeof value === "object") {
|
||||
return addToEnvironmentObject(name, value);
|
||||
}
|
||||
|
||||
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(test) {
|
||||
console.log(`FAILURE: ${test.fullName}`);
|
||||
console.log("");
|
||||
console.log(test.desc);
|
||||
console.log("");
|
||||
console.log(JSON.stringify(test, null, 4));
|
||||
}
|
||||
|
||||
function applyTestOverrides(test) {
|
||||
const overrides = testOverrides[test.fullName];
|
||||
const originals = {};
|
||||
|
||||
if (!overrides) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(overrides)) {
|
||||
originals[key] = test[key];
|
||||
test[key] = value;
|
||||
}
|
||||
|
||||
test.overridesApplied = true;
|
||||
test.valuesBeforeOverride = originals;
|
||||
}
|
||||
|
||||
function runTest(testSet, test) {
|
||||
test.partials = test.partials || {};
|
||||
test.fullName = `${testSet.name} -> ${test.name}`;
|
||||
applyTestOverrides(test);
|
||||
test.script = buildScript(test);
|
||||
|
||||
if (test.skip) {
|
||||
debug("Skipping test:", test.fullName, `(${test.skip})`);
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
debug("Running test:", test.fullName);
|
||||
|
||||
return setupEnvironment(test)
|
||||
.then(() => executeScript(test))
|
||||
.then(cleanup)
|
||||
.then(() => {
|
||||
test.isFailure = detectFailure(test);
|
||||
|
||||
if (test.isFailure) {
|
||||
showFailureDetails(test);
|
||||
} else {
|
||||
debug('Test pass:', test.fullName);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
testSet.skip = 0;
|
||||
testSet.passOverride = 0;
|
||||
|
||||
for (const test of testSet.tests) {
|
||||
if (test.isFailure) {
|
||||
testSet.fail += 1;
|
||||
} else if (test.skip) {
|
||||
testSet.skip += 1;
|
||||
} else {
|
||||
testSet.pass += 1;
|
||||
|
||||
if (test.overridesApplied) {
|
||||
testSet.passOverride += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log(
|
||||
`### ${testSet.name} Results = ${testSet.pass} passed (with ${testSet.passOverride} overridden), ${testSet.fail} failed, ${testSet.skip} skipped`
|
||||
);
|
||||
|
||||
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");
|
||||
process.exit();
|
||||
}
|
||||
|
||||
processArraySequentially(process.argv.slice(2), processSpecFile).then(
|
||||
(result) => {
|
||||
console.log("=========================================");
|
||||
console.log("");
|
||||
console.log("Failed Test Summary");
|
||||
console.log("");
|
||||
let pass = 0,
|
||||
fail = 0,
|
||||
skip = 0,
|
||||
total = 0,
|
||||
passOverride = 0;
|
||||
|
||||
for (const testSet of result) {
|
||||
pass += testSet.pass;
|
||||
fail += testSet.fail;
|
||||
skip += testSet.skip;
|
||||
total += testSet.tests.length;
|
||||
passOverride += testSet.passOverride;
|
||||
|
||||
console.log(
|
||||
`* ${testSet.name}: ${testSet.tests.length} total, ${testSet.pass} pass (with ${passOverride} overridden), ${testSet.fail} fail, ${testSet.skip} skip`
|
||||
);
|
||||
|
||||
for (const test of testSet.tests) {
|
||||
if (test.isFailure) {
|
||||
console.log(` * Failure: ${test.name}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log("");
|
||||
console.log(
|
||||
`Final result: ${total} total, ${pass} pass (with ${passOverride} overridden), ${fail} fail, ${skip} skip`
|
||||
);
|
||||
|
||||
if (fail) {
|
||||
process.exit(1);
|
||||
}
|
||||
},
|
||||
(err) => {
|
||||
console.error(err);
|
||||
console.error("FAILURE RUNNING SCRIPT");
|
||||
console.error("Testing artifacts are left in script-runner/ folder");
|
||||
}
|
||||
);
|
Reference in New Issue
Block a user