A very simplistic test framework for Elvish.
This file is written in literate programming style, to make it easy to explain. See test.elv for the generated file.
Install the elvish-modules
package using epm:
use epm
epm:install github.com/zzamboni/elvish-modules
In your rc.elv
, load this module:
use github.com/zzamboni/elvish-modules/test
The basic block is the test:check
function. It takes a lambda and an optional description, and returns a function which executes the lambda, and reports the test as passed if it returns a booleanly true value. The description, if given, is printed together with the results. All test functions return $true
if all the tests it contains passed, $false
otherwise.
Note that test:check
returns a function, so to actually run the test, you need to execute its result by enclosing the call in parenthesis (this is so that test functions can be aggregated using test:set
, see below).
~> (test:check { eq ( + 2 2 ) (float64 4) } True arithmetic)
PASS True arithmetic { eq ( + 2 2 ) (float64 4) }
~> (test:check { eq ( + 2 2 ) (float64 5) } Funky arithmetic)
FAIL Funky arithmetic { eq ( + 2 2 ) (float64 5) }
▶ $false
test:is
and test:is-not
are frontends to test:check
which explicitly take a lambda and a value. The test passes depending on whether the result from the lambda is equal/not equal to the given value. The main difference to test:check
is that when the test fails, the value returned from the lambda is reported as well.
~> (test:is { + 2 2 } (float64 4) True arithmetic)
PASS True arithmetic (eq ( + 2 2 ) 4)
▶ $true
~> (test:is { + 2 2 } (float64 5) Funky arithmetic)
FAIL Funky arithmetic (eq ( + 2 2 ) 5)
actual: (not (eq 4 5))
▶ $false
Test functions generated with test:check
, test:is
and test:is-not
can be grouped using test:set
, which takes an identifier and a list of test functions, which get executed in sequence. The list can also contain other test:set
objects. Each nested test:set
counts as a single test to its outer set, but it reports its own results.
~> use github.com/zzamboni/elvish-modules/util
~> (test:set github.com/zzamboni/elvish-modules/util [
(test:set dotify-string [
(test:is { util:dotify-string "somelongstring" 5 } "somel…" Long string gets dotified)
(test:is { util:dotify-string "short" 5 } "short" Equal-as-limit string stays the same)
(test:is { util:dotify-string "bah" 5 } "bah" Short string stays the same)])])
Testing github.com/zzamboni/elvish-modules/util
Testing github.com/zzamboni/elvish-modules/util dotify-string
PASS Long string gets dotified (eq ( util:dotify-string "somelongstring" 5 ) somel…)
PASS Equal-as-limit string stays the same (eq ( util:dotify-string "short" 5 ) short)
PASS Short string stays the same (eq ( util:dotify-string "bah" 5 ) bah)
github.com/zzamboni/elvish-modules/util dotify-string results: 3/3 passed
github.com/zzamboni/elvish-modules/util results: 1/1 passed
▶ $true
You can change the colors for successful and failed tests by assigning the corresponding values to $test:pass-style
(default green
) and $test:fail-style
(default red
) respectively. You can also change the string used to indent each level of nested test:set
(default ' '
, two spaces) by changing $test:set-indent
.
Load libraries.
use str
Style to use for success/failure messages, and for other informational strings.
var pass-style = green
var fail-style = red
var info-style = blue
Indentation to use for each level of nested test:set
.
var set-indent = ' '
Return the given text in the corresponding style according to the test result. Takes a value which will be converted to boolean using bool
, and two strings, the first one for success and the second one for failure (when $result
is a true or false, respectively). If the second string is empty, the first one is used in both cases, with only the style changing.
fn status {|result text-pass text-fail|
if (eq $text-fail '') {
set text-fail = $text-pass
}
var style = [&$true=$pass-style &$false=$fail-style]
var texts = [&$true=$text-pass &$false=$text-fail]
var index = (bool $result)
styled $texts[$index] $style[$index]
}
Some utility functions to produce the test output.
fn -level-indent {|level|
repeat $level $set-indent
}
fn -output {|@msg &level=0|
print (-level-indent $level) >/dev/tty
echo $@msg >/dev/tty
}
test:check
is the basic building block. It takes a lamda, and returns a function which verifies that the output of the lambda is true. Optionally a description of the test can be passed, which gets printed together with the result of the test. The &check-txt
option is mainly for internal use by test:is/is-not
, and allows to specify the code to be displayed as the check, which by default is the source code definition of $f
. The returned function runs the lambda, prints the result, and returns $true
or $false
depending on the result of the check. The returned function takes an option &top-id
to be consistent with the API of the function returned by test:set
, but this option is not used. The &level
option is used to determine the indentation to use for the report.
fn check {|f @d &check-txt=''|
var msg = (styled (str:join " " [$@d]) $info-style)
if (eq $check-txt '') {
set check-txt = $f[def]
}
put {|&top-id='' &level=0|
var res = (bool ($f))
-output &level=$level (status $res PASS FAIL) $msg $check-txt
put $res
}
}
test:compare
uses test:check
in the backend, but allows separate specification of the lambda to run and the value to which its output should be compared using the given $cmpfn
function (its name should get passed as $cmp
for the report. The advantage over test:check
is that it can report not only whether the check failed, but also which value was produced instead of the expected one.
fn compare {|cmp cmpfn f v @d|
put {|&top-id='' &level=0|
var res = ($f)
var check-res = ((check { $cmpfn $res $v } $@d &check-txt='('$cmp' ('$f[body]') '(to-string $v)')') &level=$level)
if (not $check-res) {
-output &level=$level " actual: (not ("$cmp' '(to-string $res)' '(to-string $v)'))'
}
put $check-res
}
}
test:is
and test:is-not
are shortcuts for test:compare
with eq
and not-eq
as comparison functions, respectively.
fn is {|f v @d|
compare eq $eq~ $f $v $@d
}
fn is-not {|f v @d|
compare not-eq $not-eq~ $f $v $@d
}
test:set
receives a description and an array containing test functions (can be other test:set
’s, test:check
, test:compare
, test:is
or test:is-not
) and returns a function which calls them in sequence. A header with the description is printed. In nested test:set
objects, the description of the enclosing set gets prepended, separated by a space. It keeps count of how many of the checks succeed, and prints a report at the end. It returns $true
if all the checks passed, $false
otherwise.
fn set {|id tests|
put {|&top-id="" &level=0|
if (not-eq $top-id '') {
set id = $top-id' '$id
}
-output &level=$level (styled "Testing "$id $info-style)
var -nextlevel = (+ $level 1)
var passed = (each {|t|
if ($t &top-id=$id &level=$-nextlevel) { put $true }
} $tests | count)
var res = (eq $passed (count $tests))
var msg = (status $res $passed"/"(count $tests)" passed" '')
-output &level=$level (styled $id" results:" $info-style) $msg
put $res
}
}