mirror of
https://github.com/tests-always-included/mo.git
synced 2024-12-18 16:27:52 +00:00
Bugfixes, pretty printing, new tests added
Spec result: 108 specs pass, 6 skipped, 67 fail
This commit is contained in:
parent
a1e4398547
commit
e0e9189355
111
mo
111
mo
@ -786,7 +786,7 @@ mo::parsePartial() {
|
|||||||
moR=$'\r'
|
moR=$'\r'
|
||||||
moIndentation="$moN${moPrevious//$moR/$moN}"
|
moIndentation="$moN${moPrevious//$moR/$moN}"
|
||||||
moIndentation=${moIndentation##*$moN}
|
moIndentation=${moIndentation##*$moN}
|
||||||
mo::debug "Adding indentation: '$moIndentation'"
|
mo::debug "Adding indentation to partial: '$moIndentation'"
|
||||||
mo::standaloneProcessBefore moPrevious "$moPrevious"
|
mo::standaloneProcessBefore moPrevious "$moPrevious"
|
||||||
mo::standaloneProcessAfter moContent "$moContent"
|
mo::standaloneProcessAfter moContent "$moContent"
|
||||||
moStandaloneContent=$'\n'
|
moStandaloneContent=$'\n'
|
||||||
@ -810,6 +810,8 @@ mo::parsePartial() {
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
mo::indentLines moResult "$moResult" "$moIndentation"
|
||||||
|
|
||||||
# Delimiters are reset when loading a new partial
|
# Delimiters are reset when loading a new partial
|
||||||
mo::parse moResult "$moResult" "$moCurrent" "" "{{" "}}" "" $'\n'
|
mo::parse moResult "$moResult" "$moCurrent" "" "{{" "}}" "" $'\n'
|
||||||
|
|
||||||
@ -823,7 +825,7 @@ mo::parsePartial() {
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
mo::indentLines moResult "${moResult%.}" "$moIndentation"
|
moResult=${moResult%.}
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local "$1" && mo::indirectArray "$1" "$moPrevious$moResult" "$moContent" "$moStandaloneContent"
|
local "$1" && mo::indirectArray "$1" "$moPrevious$moResult" "$moContent" "$moStandaloneContent"
|
||||||
@ -1317,6 +1319,42 @@ mo::isArray() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Internal: Determine if an array index exists.
|
||||||
|
#
|
||||||
|
# $1 - Variable name to check
|
||||||
|
# $2 - The index to check
|
||||||
|
#
|
||||||
|
# Has to check if the variable is an array and if the index is valid for that
|
||||||
|
# type of array.
|
||||||
|
#
|
||||||
|
# Returns true (0) if everything was ok, 1 if there's any condition that fails.
|
||||||
|
mo::isArrayIndexValid() {
|
||||||
|
local moDeclare moTest
|
||||||
|
|
||||||
|
moDeclare=$(declare -p "$1")
|
||||||
|
moTest=""
|
||||||
|
|
||||||
|
if [[ "${moDeclare:0:10}" == "declare -a" ]]; then
|
||||||
|
# Numerically indexed array - must check if the index looks like a
|
||||||
|
# number because using a string to index a numerically indexed array
|
||||||
|
# will appear like it worked.
|
||||||
|
if [[ "$2" == "0" ]] || [[ "$2" =~ ^[1-9][0-9]*$ ]]; then
|
||||||
|
# Index looks like a number
|
||||||
|
eval "moTest=\"\${$1[$2]+ok}\""
|
||||||
|
fi
|
||||||
|
elif [[ "${moDeclare:0:10}" == "declare -A" ]]; then
|
||||||
|
# Associative array
|
||||||
|
eval "moTest=\"\${$1[$2]+ok}\""
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$moTest" ]]; then
|
||||||
|
return 0;
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# Internal: Determine if a variable is assigned, even if it is assigned an empty
|
# Internal: Determine if a variable is assigned, even if it is assigned an empty
|
||||||
# value.
|
# value.
|
||||||
#
|
#
|
||||||
@ -1442,24 +1480,24 @@ mo::evaluateListOfSingles() {
|
|||||||
#
|
#
|
||||||
# Returns nothing
|
# Returns nothing
|
||||||
mo::evaluateSingle() {
|
mo::evaluateSingle() {
|
||||||
local moResult moCurrent moVarNameParts moType moArg
|
local moResult moCurrent moType moArg
|
||||||
|
|
||||||
moCurrent=$2
|
moCurrent=$2
|
||||||
moType=$3
|
moType=$3
|
||||||
moArg=$4
|
moArg=$4
|
||||||
mo::debug "Evaluating $moType: $moArg"
|
|
||||||
|
mo::debug "Evaluating $moType: $moArg ($moCurrent)"
|
||||||
|
|
||||||
if [[ "$moType" == "VALUE" ]]; then
|
if [[ "$moType" == "VALUE" ]]; then
|
||||||
moResult=$moArg
|
moResult=$moArg
|
||||||
elif [[ "$moArg" == "." ]]; then
|
elif [[ "$moArg" == "." ]]; then
|
||||||
mo::evaluateVariable moResult "$moCurrent"
|
mo::evaluateVariable moResult "$moCurrent" ""
|
||||||
elif [[ "$moArg" == "@key" ]]; then
|
elif [[ "$moArg" == "@key" ]]; then
|
||||||
mo::evaluateKey moResult "$moCurrent"
|
mo::evaluateKey moResult "$moCurrent"
|
||||||
elif mo::isFunction "$moArg"; then
|
elif mo::isFunction "$moArg"; then
|
||||||
mo::evaluateFunction moResult "" "$moArg"
|
mo::evaluateFunction moResult "" "$moArg"
|
||||||
else
|
else
|
||||||
mo::split moVarNameParts "$moArg" .
|
mo::evaluateVariable moResult "$moArg" "$moCurrent"
|
||||||
mo::evaluateVariable moResult "$moArg"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local "$1" && mo::indirect "$1" "$moResult"
|
local "$1" && mo::indirect "$1" "$moResult"
|
||||||
@ -1491,16 +1529,19 @@ mo::evaluateKey() {
|
|||||||
#
|
#
|
||||||
# $1 - Destination variable name
|
# $1 - Destination variable name
|
||||||
# $2 - Variable name
|
# $2 - Variable name
|
||||||
|
# $3 - Current value
|
||||||
#
|
#
|
||||||
# Returns nothing.
|
# Returns nothing.
|
||||||
mo::evaluateVariable() {
|
mo::evaluateVariable() {
|
||||||
local moResult moCurrent moArg moNameParts moJoined moKey moValue
|
local moResult moCurrent moArg moNameParts moJoined moKey moValue
|
||||||
|
|
||||||
moArg=$2
|
moArg=$2
|
||||||
|
moCurrent=$3
|
||||||
moResult=""
|
moResult=""
|
||||||
mo::split moNameParts "$moArg" .
|
mo::findVariableName moNameParts "$moArg" "$moCurrent"
|
||||||
|
mo::debug "Evaluate variable ($moArg + $moCurrent): ${moNameParts[*]}"
|
||||||
|
|
||||||
if [[ -z "${moNameParts[1]-}" ]]; then
|
if [[ -z "${moNameParts[1]}" ]]; then
|
||||||
if mo::isArray "$moArg"; then
|
if mo::isArray "$moArg"; then
|
||||||
eval mo::join moResult "," "\${$moArg[@]}"
|
eval mo::join moResult "," "\${$moArg[@]}"
|
||||||
else
|
else
|
||||||
@ -1523,6 +1564,58 @@ mo::evaluateVariable() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Internal: Find the name of a variable to use
|
||||||
|
#
|
||||||
|
# $1 - Destination variable name, receives an array
|
||||||
|
# $2 - Variable name from the template
|
||||||
|
# $3 - The name of the "current value", from block parsing
|
||||||
|
#
|
||||||
|
# The array contains the following values
|
||||||
|
# [0] - Variable name
|
||||||
|
# [1] - Array index, or empty string
|
||||||
|
#
|
||||||
|
# Example variables
|
||||||
|
# a="a"
|
||||||
|
# b="b"
|
||||||
|
# c=("c.0" "c.1")
|
||||||
|
# d=([b]="d.b" [d]="d.d")
|
||||||
|
#
|
||||||
|
# Given these inputs, produce these outputs
|
||||||
|
# a c => a
|
||||||
|
# a c.0 => a
|
||||||
|
# b d => d.b
|
||||||
|
# b d.d => d.b
|
||||||
|
# a d => d.a
|
||||||
|
# a d.d => d.a
|
||||||
|
# c.0 d => c.0
|
||||||
|
# d.b d => d.b
|
||||||
|
# Returns nothing.
|
||||||
|
mo::findVariableName() {
|
||||||
|
local moVar moCurrent moNameParts moResultBase moResultIndex
|
||||||
|
|
||||||
|
moVar=$2
|
||||||
|
moCurrent=$3
|
||||||
|
moResultBase=$moVar
|
||||||
|
moResultIndex=""
|
||||||
|
|
||||||
|
if [[ "$moVar" == *.* ]]; then
|
||||||
|
mo::debug "Find variable name; name has dot: $moVar"
|
||||||
|
moResultBase=${moVar%%.*}
|
||||||
|
moResultIndex=${moVar#*.}
|
||||||
|
elif [[ -n "$moCurrent" ]]; then
|
||||||
|
moCurrent=${moCurrent%%.*}
|
||||||
|
mo::debug "Find variable name; look in array: $moCurrent"
|
||||||
|
|
||||||
|
if mo::isArrayIndexValid "$moCurrent" "$moVar"; then
|
||||||
|
moResultBase=$moCurrent
|
||||||
|
moResultIndex=$moVar
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
local "$1" && mo::indirectArray "$1" "$moResultBase" "$moResultIndex"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# Internal: Join / implode an array
|
# Internal: Join / implode an array
|
||||||
#
|
#
|
||||||
# $1 - Variable name to receive the joined content
|
# $1 - Variable name to receive the joined content
|
||||||
|
90
run-spec.js
90
run-spec.js
@ -11,28 +11,45 @@ const fsPromises = require("fs").promises;
|
|||||||
//
|
//
|
||||||
// To override any test property, just define that property.
|
// To override any test property, just define that property.
|
||||||
const testOverrides = {
|
const testOverrides = {
|
||||||
'Interpolation -> HTML Escaping': {
|
"Interpolation -> HTML Escaping": {
|
||||||
skip: 'HTML escaping is not supported'
|
skip: "HTML escaping is not supported"
|
||||||
},
|
},
|
||||||
'Interpolation -> Implicit Iterators - HTML Escaping': {
|
"Interpolation -> Implicit Iterators - HTML Escaping": {
|
||||||
skip: 'HTML escaping is not supported'
|
skip: "HTML escaping is not supported"
|
||||||
},
|
},
|
||||||
'Lambdas -> Escaping': {
|
"Lambdas -> Escaping": {
|
||||||
skip: 'HTML escaping is not supported'
|
skip: "HTML escaping is not supported"
|
||||||
},
|
},
|
||||||
'Sections -> Dotted Names - Broken Chains': {
|
"Sections -> Deeply Nested Contexts": {
|
||||||
|
skip: "Nested objects are not supported"
|
||||||
|
},
|
||||||
|
"Sections -> Dotted Names - Broken Chains": {
|
||||||
// Complex objects are not supported
|
// Complex objects are not supported
|
||||||
template: `"{{#a.b}}Here{{/a.b}}" == ""`
|
template: `"{{#a.b}}Here{{/a.b}}" == ""`
|
||||||
},
|
},
|
||||||
'Sections -> Dotted Names - Falsey': {
|
"Sections -> Dotted Names - Falsey": {
|
||||||
// Complex objects are not supported
|
// Complex objects are not supported
|
||||||
data: { a: { b: false } },
|
data: { a: { b: false } },
|
||||||
template: `"{{#a.b}}Here{{/a.b}}" == ""`
|
template: `"{{#a.b}}Here{{/a.b}}" == ""`
|
||||||
},
|
},
|
||||||
'Sections -> Dotted Names - Truthy': {
|
"Sections -> Dotted Names - Truthy": {
|
||||||
// Complex objects are not supported
|
// Complex objects are not supported
|
||||||
data: { a: { b: true } },
|
data: { a: { b: true } },
|
||||||
template: `"{{#a.b}}Here{{/a.b}}" == "Here"`
|
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"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -110,7 +127,7 @@ function addToEnvironmentObjectConvertedToAssociativeArray(name, value) {
|
|||||||
const values = [];
|
const values = [];
|
||||||
|
|
||||||
for (const [k, v] of Object.entries(value)) {
|
for (const [k, v] of Object.entries(value)) {
|
||||||
if (typeof v === 'object') {
|
if (typeof v === "object") {
|
||||||
if (v) {
|
if (v) {
|
||||||
// An object - abort
|
// An object - abort
|
||||||
return `# ${name}.${k} is an object that can not be converted to an associative array`;
|
return `# ${name}.${k} is an object that can not be converted to an associative array`;
|
||||||
@ -123,7 +140,7 @@ function addToEnvironmentObjectConvertedToAssociativeArray(name, value) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return `declare -A ${name}\n${name}=(${values.join(' ')})`;
|
return `declare -A ${name}\n${name}=(${values.join(" ")})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function addToEnvironmentObject(name, value) {
|
function addToEnvironmentObject(name, value) {
|
||||||
@ -134,10 +151,7 @@ function addToEnvironmentObject(name, value) {
|
|||||||
|
|
||||||
// Sometimes the __tag__ property of the code in the lambdas may be
|
// Sometimes the __tag__ property of the code in the lambdas may be
|
||||||
// missing. Compensate by detecting commonly defined languages.
|
// missing. Compensate by detecting commonly defined languages.
|
||||||
if (
|
if (value.__tag__ === "code" || (value.ruby && value.php && value.perl)) {
|
||||||
(value.__tag__ === "code") ||
|
|
||||||
(value.ruby && value.php && value.perl)
|
|
||||||
) {
|
|
||||||
if (value.bash) {
|
if (value.bash) {
|
||||||
return `${name}() { ${value.bash}; }`;
|
return `${name}() { ${value.bash}; }`;
|
||||||
}
|
}
|
||||||
@ -145,7 +159,6 @@ function addToEnvironmentObject(name, value) {
|
|||||||
return `${name}() { perl -e 'print ((${value.perl})->("'"$1"'"))'; }`;
|
return `${name}() { perl -e 'print ((${value.perl})->("'"$1"'"))'; }`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return addToEnvironmentObjectConvertedToAssociativeArray(name, value);
|
return addToEnvironmentObjectConvertedToAssociativeArray(name, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -201,16 +214,20 @@ function setupEnvironment(test) {
|
|||||||
|
|
||||||
function executeScript(test) {
|
function executeScript(test) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
exec("bash spec-runner/spec-script 2>&1", {
|
exec(
|
||||||
|
"bash spec-runner/spec-script 2>&1",
|
||||||
|
{
|
||||||
timeout: 2000
|
timeout: 2000
|
||||||
}, (err, stdout) => {
|
},
|
||||||
|
(err, stdout) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
test.scriptError = err.toString();
|
test.scriptError = err.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
test.output = stdout;
|
test.output = stdout;
|
||||||
resolve();
|
resolve();
|
||||||
});
|
}
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -232,9 +249,9 @@ function detectFailure(test) {
|
|||||||
|
|
||||||
function showFailureDetails(test) {
|
function showFailureDetails(test) {
|
||||||
console.log(`FAILURE: ${test.fullName}`);
|
console.log(`FAILURE: ${test.fullName}`);
|
||||||
console.log('');
|
console.log("");
|
||||||
console.log(test.desc);
|
console.log(test.desc);
|
||||||
console.log('');
|
console.log("");
|
||||||
console.log(JSON.stringify(test, null, 4));
|
console.log(JSON.stringify(test, null, 4));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -261,12 +278,12 @@ function runTest(testSet, test) {
|
|||||||
test.script = buildScript(test);
|
test.script = buildScript(test);
|
||||||
|
|
||||||
if (test.skip) {
|
if (test.skip) {
|
||||||
debug('Skipping test:', testSet.fullName, `$(${test.skip})`);
|
debug("Skipping test:", testSet.fullName, `$(${test.skip})`);
|
||||||
|
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
debug('Running test:', testSet.fullName);
|
debug("Running test:", testSet.fullName);
|
||||||
|
|
||||||
return setupEnvironment(test)
|
return setupEnvironment(test)
|
||||||
.then(() => executeScript(test))
|
.then(() => executeScript(test))
|
||||||
@ -303,7 +320,9 @@ function processSpecFile(filename) {
|
|||||||
testSet.pass += 1;
|
testSet.pass += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.log(`### ${testSet.name} Results = ${testSet.pass} passed, ${testSet.fail} failed, ${testSet.skip} skipped`);
|
console.log(
|
||||||
|
`### ${testSet.name} Results = ${testSet.pass} passed, ${testSet.fail} failed, ${testSet.skip} skipped`
|
||||||
|
);
|
||||||
|
|
||||||
return testSet;
|
return testSet;
|
||||||
});
|
});
|
||||||
@ -318,11 +337,14 @@ if (process.argv.length < 3) {
|
|||||||
|
|
||||||
processArraySequentially(process.argv.slice(2), processSpecFile).then(
|
processArraySequentially(process.argv.slice(2), processSpecFile).then(
|
||||||
(result) => {
|
(result) => {
|
||||||
console.log('=========================================');
|
console.log("=========================================");
|
||||||
console.log('');
|
console.log("");
|
||||||
console.log('Failed Test Summary');
|
console.log("Failed Test Summary");
|
||||||
console.log('');
|
console.log("");
|
||||||
let pass = 0, fail = 0, skip = 0, total = 0;
|
let pass = 0,
|
||||||
|
fail = 0,
|
||||||
|
skip = 0,
|
||||||
|
total = 0;
|
||||||
|
|
||||||
for (const testSet of result) {
|
for (const testSet of result) {
|
||||||
pass += testSet.pass;
|
pass += testSet.pass;
|
||||||
@ -330,7 +352,9 @@ processArraySequentially(process.argv.slice(2), processSpecFile).then(
|
|||||||
skip += testSet.skip;
|
skip += testSet.skip;
|
||||||
total += testSet.tests.length;
|
total += testSet.tests.length;
|
||||||
|
|
||||||
console.log(`* ${testSet.name}: ${testSet.tests.length} total, ${testSet.pass} pass, ${testSet.fail} fail, ${testSet.skip} skip`);
|
console.log(
|
||||||
|
`* ${testSet.name}: ${testSet.tests.length} total, ${testSet.pass} pass, ${testSet.fail} fail, ${testSet.skip} skip`
|
||||||
|
);
|
||||||
|
|
||||||
for (const test of testSet.tests) {
|
for (const test of testSet.tests) {
|
||||||
if (test.isFailure) {
|
if (test.isFailure) {
|
||||||
@ -339,8 +363,10 @@ processArraySequentially(process.argv.slice(2), processSpecFile).then(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('');
|
console.log("");
|
||||||
console.log(`Final result: ${total} total, ${pass} pass, ${fail} fail, ${skip} skip`);
|
console.log(
|
||||||
|
`Final result: ${total} total, ${pass} pass, ${fail} fail, ${skip} skip`
|
||||||
|
);
|
||||||
|
|
||||||
if (fail) {
|
if (fail) {
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
@ -50,7 +50,7 @@ runTest() (
|
|||||||
echo "Actual:"
|
echo "Actual:"
|
||||||
echo "$testActual"
|
echo "$testActual"
|
||||||
|
|
||||||
if [[ -n "${MO_DEBUG-}" ]]; then
|
if [[ -n "${MO_DEBUG_TEST-}" ]]; then
|
||||||
declare -p testExpected
|
declare -p testExpected
|
||||||
declare -p testActual
|
declare -p testActual
|
||||||
fi
|
fi
|
||||||
|
3
tests/fixtures/standalone-indentation.partial
vendored
Normal file
3
tests/fixtures/standalone-indentation.partial
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
|
|
||||||
|
{{content}}
|
||||||
|
|
|
14
tests/list-contexts
Executable file
14
tests/list-contexts
Executable file
@ -0,0 +1,14 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
cd "${0%/*}" || exit 1
|
||||||
|
. ../run-tests
|
||||||
|
|
||||||
|
a=foo
|
||||||
|
b=wrong
|
||||||
|
declare -A sec
|
||||||
|
sec=([b]="bar")
|
||||||
|
declare -A c
|
||||||
|
c=([d]="baz")
|
||||||
|
template="{{#sec}}{{a}} {{b}} {{c.d}}{{/sec}}"
|
||||||
|
expected="foo bar baz"
|
||||||
|
|
||||||
|
runTest
|
@ -24,8 +24,9 @@ line 2
|
|||||||
Indented:
|
Indented:
|
||||||
|
|
||||||
line 1
|
line 1
|
||||||
line 2
|
line 2
|
||||||
EOF
|
EOF
|
||||||
|
# This one looks odd, but if you check the spec spec/specs/partials.yaml, name "Standalone Indentation" (mirrors "standalone-indentation" in tests/), then the spec clearly shows that the indentation is applied before rendering.
|
||||||
}
|
}
|
||||||
|
|
||||||
runTest
|
runTest
|
||||||
|
24
tests/standalone-indentation
Executable file
24
tests/standalone-indentation
Executable file
@ -0,0 +1,24 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
cd "${0%/*}" || exit 1
|
||||||
|
. ../run-tests
|
||||||
|
|
||||||
|
content=$'<\n->'
|
||||||
|
template() {
|
||||||
|
cat <<EOF
|
||||||
|
\
|
||||||
|
{{>fixtures/standalone-indentation.partial}}
|
||||||
|
/
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
expected() {
|
||||||
|
cat <<EOF
|
||||||
|
\
|
||||||
|
|
|
||||||
|
<
|
||||||
|
->
|
||||||
|
|
|
||||||
|
/
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
runTest
|
Loading…
Reference in New Issue
Block a user