8dc54f89cd
Having `prettyRes` in the execline script causes it to fail because of the argv limit if your test suite is long enough. For the succeeding one we can work around this by hashing it (since we only care that something changes if the test suite changes), in the case of the failing one where we want to print the results, we use runExecline's stdin mechanism. Change-Id: I2489f76acfbe809351f51caefe2a477328a70ee3
179 lines
4.9 KiB
Nix
179 lines
4.9 KiB
Nix
{ lib, pkgs, depot, ... }:
|
||
|
||
# Run a nix testsuite.
|
||
#
|
||
# The tests are simple assertions on the nix level,
|
||
# and can use derivation outputs if IfD is enabled.
|
||
#
|
||
# You build a testsuite by bundling assertions into
|
||
# “it”s and then bundling the “it”s into a testsuite.
|
||
#
|
||
# Running the testsuite will abort evaluation if
|
||
# any assertion fails.
|
||
#
|
||
# Example:
|
||
#
|
||
# runTestsuite "myFancyTestsuite" [
|
||
# (it "does an assertion" [
|
||
# (assertEq "42 is equal to 42" "42" "42")
|
||
# (assertEq "also 23" 23 23)
|
||
# ])
|
||
# (it "frmbls the brlbr" [
|
||
# (assertEq true false)
|
||
# ])
|
||
# ]
|
||
#
|
||
# will fail the second it group because true is not false.
|
||
|
||
let
|
||
inherit (depot.nix.yants)
|
||
sum
|
||
struct
|
||
string
|
||
any
|
||
defun
|
||
list
|
||
drv
|
||
bool
|
||
;
|
||
|
||
bins = depot.nix.getBins pkgs.coreutils [ "printf" ]
|
||
// depot.nix.getBins pkgs.s6-portable-utils [ "s6-touch" "s6-false" "s6-cat" ];
|
||
|
||
# Returns true if the given expression throws when `deepSeq`-ed
|
||
throws = expr:
|
||
!(builtins.tryEval (builtins.deepSeq expr {})).success;
|
||
|
||
# rewrite the builtins.partition result
|
||
# to use `ok` and `err` instead of `right` and `wrong`.
|
||
partitionTests = pred: xs:
|
||
let res = builtins.partition pred xs;
|
||
in {
|
||
ok = res.right;
|
||
err = res.wrong;
|
||
};
|
||
|
||
AssertErrorContext =
|
||
sum "AssertErrorContext" {
|
||
not-equal = struct "not-equal" {
|
||
left = any;
|
||
right = any;
|
||
};
|
||
should-throw = struct "should-throw" {
|
||
expr = any;
|
||
};
|
||
unexpected-throw = struct "unexpected-throw" { };
|
||
};
|
||
|
||
# The result of an assert,
|
||
# either it’s true (yep) or false (nope).
|
||
# If it's nope we return an additional context
|
||
# attribute which gives details on the failure
|
||
# depending on the type of assert performed.
|
||
AssertResult =
|
||
sum "AssertResult" {
|
||
yep = struct "yep" {
|
||
test = string;
|
||
};
|
||
nope = struct "nope" {
|
||
test = string;
|
||
context = AssertErrorContext;
|
||
};
|
||
};
|
||
|
||
# Result of an it. An it is a bunch of asserts
|
||
# bundled up with a good description of what is tested.
|
||
ItResult =
|
||
struct "ItResult" {
|
||
it-desc = string;
|
||
asserts = list AssertResult;
|
||
};
|
||
|
||
# If the given boolean is true return a positive AssertResult.
|
||
# If the given boolean is false return a negative AssertResult
|
||
# with the provided AssertErrorContext describing the failure.
|
||
#
|
||
# This function is intended as a generic assert to implement
|
||
# more assert types and is not exposed to the user.
|
||
assertBoolContext = defun [ AssertErrorContext string bool AssertResult ]
|
||
(context: desc: res:
|
||
if res
|
||
then { yep = { test = desc; }; }
|
||
else { nope = {
|
||
test = desc;
|
||
inherit context;
|
||
};
|
||
});
|
||
|
||
# assert that left and right values are equal
|
||
assertEq = defun [ string any any AssertResult ]
|
||
(desc: left: right:
|
||
let
|
||
context = { not-equal = { inherit left right; }; };
|
||
in
|
||
assertBoolContext context desc (left == right));
|
||
|
||
# assert that the expression throws when `deepSeq`-ed
|
||
assertThrows = defun [ string any AssertResult ]
|
||
(desc: expr:
|
||
let
|
||
context = { should-throw = { inherit expr; }; };
|
||
in
|
||
assertBoolContext context desc (throws expr));
|
||
|
||
# assert that the expression does not throw when `deepSeq`-ed
|
||
assertDoesNotThrow = defun [ string any AssertResult ]
|
||
(desc: expr:
|
||
assertBoolContext { unexpected-throw = { }; } desc (!(throws expr)));
|
||
|
||
# Annotate a bunch of asserts with a descriptive name
|
||
it = desc: asserts: {
|
||
it-desc = desc;
|
||
inherit asserts;
|
||
};
|
||
|
||
# Run a bunch of its and check whether all asserts are yep.
|
||
# If not, abort evaluation with `throw`
|
||
# and print the result of the test suite.
|
||
#
|
||
# Takes a test suite name as first argument.
|
||
runTestsuite = defun [ string (list ItResult) drv ]
|
||
(name: itResults:
|
||
let
|
||
goodAss = ass: AssertResult.match ass {
|
||
yep = _: true;
|
||
nope = _: false;
|
||
};
|
||
res = partitionTests (it:
|
||
(partitionTests goodAss it.asserts).err == []
|
||
) itResults;
|
||
prettyRes = lib.generators.toPretty {} res;
|
||
in
|
||
if res.err == []
|
||
then depot.nix.runExecline.local "testsuite-${name}-successful" {} [
|
||
"importas" "out" "out"
|
||
# force derivation to rebuild if test case list changes
|
||
"ifelse" [ bins.s6-false ] [
|
||
bins.printf "" (builtins.hashString "sha512" prettyRes)
|
||
]
|
||
"if" [ bins.printf "%s\n" "testsuite ${name} successful!" ]
|
||
bins.s6-touch "$out"
|
||
]
|
||
else depot.nix.runExecline.local "testsuite-${name}-failed" {
|
||
stdin = prettyRes + "\n";
|
||
} [
|
||
"importas" "out" "out"
|
||
"if" [ bins.printf "%s\n" "testsuite ${name} failed!" ]
|
||
"if" [ bins.s6-cat ]
|
||
"exit" "1"
|
||
]);
|
||
|
||
in {
|
||
inherit
|
||
assertEq
|
||
assertThrows
|
||
assertDoesNotThrow
|
||
it
|
||
runTestsuite
|
||
;
|
||
}
|