-
Notifications
You must be signed in to change notification settings - Fork 440
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
RFC: implement unit testing framework #111
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
#! /bin/bash | ||
|
||
set -o pipefail | ||
set -o nounset | ||
set -o errexit | ||
|
||
red="$(tput setaf 1)" | ||
green="$(tput setaf 2)" | ||
reset="$(tput sgr0)" | ||
|
||
failed=0 | ||
for jsonnet_test in $(ls tests/*.jsonnet); do | ||
echo "${green}Running ${jsonnet_test}...${reset}" | ||
./jsonnet \ | ||
--code-file "tests=${jsonnet_test}" \ | ||
--exec \ | ||
--string \ | ||
'local testing = import "stdlib/testing.jsonnet"; testing.run(std.extVar("tests"))' \ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You could also --exec 'import "'"${jsonnet_test}"'"' There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's also not unthinkable to have some jsonnet test foo.jsonnet command. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It seems that we are doing a fair amount of special-casing for the purposes of running tests, such as adding the |
||
&& errno=0 || errno=${?} | ||
|
||
if [[ "${errno}" != "0" ]]; then | ||
((failed++)) || : | ||
echo "${red}${jsonnet_test} failed${reset}" | ||
echo | ||
fi | ||
done | ||
|
||
if [[ "${failed}" -gt 0 ]]; then | ||
echo "${red}(${failed}/$(ls tests/*.jsonnet | wc -l)) test packages failed.${reset}" | ||
exit 1 | ||
fi | ||
|
||
echo "${green}OK${reset}" | ||
exit 0 | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,139 @@ | ||
/* | ||
Test: | ||
{ | ||
name: string,required | ||
cases: object[],optional | ||
// if cases is not null | ||
evalute(case):: (object -> CaseResult) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. typo: evaluate |
||
// if cases is null | ||
evalute():: (() -> CaseResult) | ||
} | ||
|
||
Case: | ||
{ | ||
name: string,optional | ||
|
||
...testdata... | ||
} | ||
|
||
CaseResult: | ||
{ | ||
name: | ||
passed: bool,required | ||
message: string,optional | ||
} | ||
|
||
TestResult: | ||
{ | ||
name: string,required | ||
passed: bool,required | ||
results: CaseResult[] | ||
} | ||
|
||
*/ | ||
{ | ||
|
||
local testing = $, | ||
|
||
format(results):: | ||
local testResultTemplate = | ||
"--- %(result)s '%(name)s'\n"; | ||
local caseResultTemplate = | ||
"\t%(result)s\t'%(name)s'"; | ||
local caseResultMessageTemplate = | ||
":\n %(message)s\n"; | ||
|
||
local formatResults(str, cur, results, formatResult) = | ||
if std.length(results) == cur then | ||
str | ||
else | ||
formatResults( | ||
str + formatResult(cur, results[cur]), | ||
cur + 1, | ||
results, | ||
formatResult | ||
) tailstrict; | ||
|
||
local formatCaseResult(cur, caseResult) = | ||
local result = | ||
if caseResult.passed then | ||
"pass" | ||
else | ||
"fail"; | ||
local name = | ||
if std.objectHas(caseResult, "name") then | ||
caseResult.name | ||
else | ||
"case %s" % cur; | ||
local caseResultString = caseResultTemplate % {result: result, name: name}; | ||
|
||
local message = | ||
if std.objectHas(caseResult, "message") then | ||
caseResultMessageTemplate % caseResult | ||
else | ||
"\n"; | ||
|
||
caseResultString + message; | ||
|
||
local formatTestCases(str, results) = | ||
formatResults(str, 0, results, formatCaseResult); | ||
|
||
local formatTestResult(cur, testResult) = | ||
local str = testResultTemplate % { | ||
result: | ||
if testResult.passed then | ||
"PASS" | ||
else | ||
"FAIL", | ||
name: testResult.name, | ||
}; | ||
formatTestCases(str, testResult.results); | ||
|
||
local formatTestResults(str, cur, results) = | ||
formatResults(str, cur, results, formatTestResult); | ||
|
||
formatTestResults("", 0, results), | ||
|
||
passed(results):: | ||
std.foldl(function(cur,result) cur && result.passed, results, true), | ||
|
||
evaluateToCaseResult(obj):: | ||
if obj == null then | ||
{ passed: true } | ||
else if std.type(obj) == "string" then | ||
{ passed: false, message: obj } | ||
else | ||
error("evaluate must return a failure message or {}."), | ||
|
||
evaluateTest(name, test):: | ||
local caseResults = | ||
if std.type(test) == "object" then | ||
[ | ||
testing.evaluateToCaseResult(test.evaluate(test.cases[i])) + test.cases[i] | ||
for i in std.range(0, std.length(test.cases) - 1) | ||
] | ||
else if std.type(test) == "function" then | ||
[ testing.evaluateToCaseResult(test()) ] | ||
else | ||
error "tests muts be objects or functions"; | ||
{ | ||
name: name, | ||
passed: testing.passed(caseResults), | ||
results: caseResults | ||
}, | ||
|
||
evaluate(tests):: | ||
if std.type(tests) == "object" then | ||
local results = | ||
[ testing.evaluateTest(name, tests[name]) for name in std.objectFields(tests) ]; | ||
results | ||
else | ||
error("input must be a test object"), | ||
|
||
run(input):: | ||
local results = testing.evaluate(input); | ||
if testing.passed(results) then | ||
testing.format(results) | ||
else | ||
error("\n" + testing.format(results)), | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
{ | ||
|
||
|
||
expect():: | ||
self, | ||
|
||
that(subject):: | ||
if std.objectHas(self, "subject") then | ||
error("subject cannot be set twice") | ||
else | ||
self + { subject: subject }, | ||
|
||
|
||
// returns the first string representing a failed expecatation or returns | ||
// null if no assertions failed. | ||
expectations(arr):: | ||
std.foldl( | ||
function(cur, result) | ||
if cur != null then | ||
cur | ||
else | ||
if result != null then | ||
result, | ||
arr, | ||
null, | ||
), | ||
|
||
fatalFailures: false, | ||
|
||
fatal():: | ||
self + { fatalFailures: true}, | ||
|
||
fail(message):: | ||
if self.fatalFailures then | ||
error(message) | ||
else | ||
message, | ||
|
||
local fail = self.fail, | ||
|
||
named(name):: | ||
if std.objectHas(self, "name") then | ||
error("name cannot be set twice") | ||
else | ||
self + { name: name }, | ||
|
||
|
||
// propositions | ||
|
||
hasType(object):: | ||
if std.type(self.subject) != object then | ||
fail("expected '%s' to be of type '%s' but was of type '%s'" % [ self.subject, object, std.type(self.subject) ]), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: @sparkprime - Do we have a recommended line length limit? AFAIU, the line length we're using for the C++ code is 100 characters. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The rest of the Jsonnet code in the repo uses 4 and 100. It may be time to write a style guide. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agreed. I've opened #118 to discuss a style guide. |
||
|
||
isEqualTo(object):: | ||
if self.subject != object then | ||
fail("expected '%s' and '%s' to be equal" % [ self.subject, object ]), | ||
|
||
isNotEqualTo(object):: | ||
if self.subject == object then | ||
fail("expected '%s' and '%s' not to be equal" % [ self.subject, object ]), | ||
|
||
isTrue():: | ||
self.expectations([ | ||
self.hasType("boolean"), | ||
if !self.subject then | ||
fail("expected true"), | ||
]), | ||
|
||
isFalse():: | ||
self.expectaions([ | ||
self.hasType("boolean"), | ||
if self.subject then | ||
fail("expected false"), | ||
]), | ||
|
||
|
||
} |
This file was deleted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will cause the stacktrace to refer to extvar:tests rather than the name of the file. But perhaps this is something that should be fixed elsewhere.