Skip to content

Commit 414cecf

Browse files
committed
implement unit testing framework
1 parent 709da7f commit 414cecf

File tree

7 files changed

+493
-148
lines changed

7 files changed

+493
-148
lines changed

run_tests.sh

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
#! /bin/bash
2+
3+
set -o pipefail
4+
set -o nounset
5+
set -o errexit
6+
7+
red="$(tput setaf 1)"
8+
green="$(tput setaf 2)"
9+
reset="$(tput sgr0)"
10+
11+
failed=0
12+
for jsonnet_test in $(ls tests/*.jsonnet); do
13+
echo "${green}Running ${jsonnet_test}...${reset}"
14+
./jsonnet \
15+
--code-file "tests=${jsonnet_test}" \
16+
--exec \
17+
--string \
18+
'local testing = import "stdlib/testing.jsonnet"; testing.run(std.extVar("tests"))' \
19+
&& errno=0 || errno=${?}
20+
21+
if [[ "${errno}" != "0" ]]; then
22+
((failed++)) || :
23+
echo "${red}${jsonnet_test} failed${reset}"
24+
echo
25+
fi
26+
done
27+
28+
if [[ "${failed}" -gt 0 ]]; then
29+
echo "${red}(${failed}/$(ls tests/*.jsonnet | wc -l)) test packages failed.${reset}"
30+
exit 1
31+
fi
32+
33+
echo "${green}OK${reset}"
34+
exit 0
35+

stdlib/testing.jsonnet

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
/*
2+
Test:
3+
{
4+
name: string,required
5+
cases: object[],optional
6+
// if cases is not null
7+
evalute(case):: (object -> CaseResult)
8+
// if cases is null
9+
evalute():: (() -> CaseResult)
10+
}
11+
12+
Case:
13+
{
14+
name: string,optional
15+
16+
...testdata...
17+
}
18+
19+
CaseResult:
20+
{
21+
name:
22+
passed: bool,required
23+
message: string,optional
24+
}
25+
26+
TestResult:
27+
{
28+
name: string,required
29+
passed: bool,required
30+
results: CaseResult[]
31+
}
32+
33+
*/
34+
{
35+
36+
local testing = $,
37+
38+
format(results)::
39+
local testResultTemplate =
40+
"--- %(result)s '%(name)s'\n";
41+
local caseResultTemplate =
42+
"\t%(result)s\t'%(name)s'";
43+
local caseResultMessageTemplate =
44+
":\n %(message)s\n";
45+
46+
local formatResults(str, cur, results, formatResult) =
47+
if std.length(results) == cur then
48+
str
49+
else
50+
formatResults(
51+
str + formatResult(cur, results[cur]),
52+
cur + 1,
53+
results,
54+
formatResult
55+
) tailstrict;
56+
57+
local formatCaseResult(cur, caseResult) =
58+
local result =
59+
if caseResult.passed then
60+
"pass"
61+
else
62+
"fail";
63+
local name =
64+
if std.objectHas(caseResult, "name") then
65+
caseResult.name
66+
else
67+
"case %s" % cur;
68+
local caseResultString = caseResultTemplate % {result: result, name: name};
69+
70+
local message =
71+
if std.objectHas(caseResult, "message") then
72+
caseResultMessageTemplate % caseResult
73+
else
74+
"\n";
75+
76+
caseResultString + message;
77+
78+
local formatTestCases(str, results) =
79+
formatResults(str, 0, results, formatCaseResult);
80+
81+
local formatTestResult(cur, testResult) =
82+
local str = testResultTemplate % {
83+
result:
84+
if testResult.passed then
85+
"PASS"
86+
else
87+
"FAIL",
88+
name: testResult.name,
89+
};
90+
formatTestCases(str, testResult.results);
91+
92+
local formatTestResults(str, cur, results) =
93+
formatResults(str, cur, results, formatTestResult);
94+
95+
formatTestResults("", 0, results),
96+
97+
passed(results)::
98+
std.foldl(function(cur,result) cur && result.passed, results, true),
99+
100+
evaluateToCaseResult(obj)::
101+
if obj == null then
102+
{ passed: true }
103+
else if std.type(obj) == "string" then
104+
{ passed: false, message: obj }
105+
else
106+
error("evaluate must return a failure message or {}."),
107+
108+
evaluateTest(name, test)::
109+
local caseResults =
110+
if std.type(test) == "object" then
111+
[
112+
testing.evaluateToCaseResult(test.evaluate(test.cases[i])) + test.cases[i]
113+
for i in std.range(0, std.length(test.cases) - 1)
114+
]
115+
else if std.type(test) == "function" then
116+
[ testing.evaluateToCaseResult(test()) ]
117+
else
118+
error "tests muts be objects or functions";
119+
{
120+
name: name,
121+
passed: testing.passed(caseResults),
122+
results: caseResults
123+
},
124+
125+
evaluate(tests)::
126+
if std.type(tests) == "object" then
127+
local results =
128+
[ testing.evaluateTest(name, tests[name]) for name in std.objectFields(tests) ];
129+
results
130+
else
131+
error("input must be a test object"),
132+
133+
run(input)::
134+
local results = testing.evaluate(input);
135+
if testing.passed(results) then
136+
testing.format(results)
137+
else
138+
error("\n" + testing.format(results)),
139+
}

stdlib/truth.jsonnet

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
{
2+
fatalFailures: false,
3+
4+
fatal()::
5+
self + { fatalFailures: true},
6+
7+
fail(message)::
8+
if self.fatalFailures then
9+
error(message)
10+
else
11+
message,
12+
13+
local fail = self.fail,
14+
15+
expect()::
16+
self,
17+
18+
that(subject)::
19+
self + { subject: subject },
20+
21+
isEqualTo(object)::
22+
if self.subject != object then
23+
fail("expected '%s' and '%s' to be equal" % [ self.subject, object ]),
24+
25+
isNotEqualTo(object)::
26+
if self.subject == object then
27+
fail("expected '%s' and '%s' not to be equal" % [ self.subject, object ]),
28+
29+
hasType(object)::
30+
if std.type(self.subject) != object then
31+
fail("expected '%s' to be of type '%s' but was of type '%s'" % [ self.subject, object, std.type(self.subject) ]),
32+
33+
// returns the first string representing a failed assertion or returns
34+
// null if no assertions failed.
35+
assertions(arr)::
36+
std.foldl(
37+
function(cur, result)
38+
if cur != null then
39+
cur
40+
else
41+
if result != null then
42+
result,
43+
arr,
44+
null,
45+
),
46+
}

test_suite/arith_bool.jsonnet

Lines changed: 0 additions & 52 deletions
This file was deleted.

test_suite/merge.jsonnet

Lines changed: 0 additions & 96 deletions
This file was deleted.

0 commit comments

Comments
 (0)