mirror of
https://gitlab.com/TECHNOFAB/nixtest.git
synced 2026-11-01 13:12:19 +01:00
feat: add new assertions test type
This commit is contained in:
parent
6c4c1bace6
commit
17d08b9975
10 changed files with 603 additions and 16 deletions
296
lib/asserts.nix
Normal file
296
lib/asserts.nix
Normal file
|
|
@ -0,0 +1,296 @@
|
|||
{lib, ...}: let
|
||||
inherit (lib) generators;
|
||||
|
||||
# Format any value to a readable string safely
|
||||
formatVal = val:
|
||||
if builtins.isString val
|
||||
then val
|
||||
else if builtins.isBool val || builtins.isInt val || builtins.isFloat val
|
||||
then builtins.toString val
|
||||
else generators.toPretty {} val;
|
||||
|
||||
mkFailure = {
|
||||
message ? "Assertion failed",
|
||||
expected ? null,
|
||||
actual ? null,
|
||||
}: {
|
||||
ok = false;
|
||||
inherit message;
|
||||
expected =
|
||||
if expected != null
|
||||
then formatVal expected
|
||||
else "";
|
||||
actual =
|
||||
if actual != null
|
||||
then formatVal actual
|
||||
else "";
|
||||
};
|
||||
in rec {
|
||||
inherit formatVal;
|
||||
|
||||
equal = expected: actual:
|
||||
if actual == expected
|
||||
then true
|
||||
else
|
||||
mkFailure {
|
||||
message = "Expected values to be equal";
|
||||
inherit expected actual;
|
||||
};
|
||||
eq = equal;
|
||||
|
||||
notEqual = expected: actual:
|
||||
if actual != expected
|
||||
then true
|
||||
else
|
||||
mkFailure {
|
||||
message = "Expected values to not be equal";
|
||||
expected = "not ${formatVal expected}";
|
||||
inherit actual;
|
||||
};
|
||||
ne = notEqual;
|
||||
|
||||
greaterThan = actual: min:
|
||||
if actual > min
|
||||
then true
|
||||
else
|
||||
mkFailure {
|
||||
message = "Expected value to be greater than ${formatVal min}";
|
||||
expected = "> ${formatVal min}";
|
||||
inherit actual;
|
||||
};
|
||||
gt = greaterThan;
|
||||
|
||||
greaterThanOrEqual = actual: min:
|
||||
if actual >= min
|
||||
then true
|
||||
else
|
||||
mkFailure {
|
||||
message = "Expected value to be greater than or equal to ${formatVal min}";
|
||||
expected = ">= ${formatVal min}";
|
||||
inherit actual;
|
||||
};
|
||||
gte = greaterThanOrEqual;
|
||||
|
||||
lessThan = actual: max:
|
||||
if actual < max
|
||||
then true
|
||||
else
|
||||
mkFailure {
|
||||
message = "Expected value to be less than ${formatVal max}";
|
||||
expected = "< ${formatVal max}";
|
||||
inherit actual;
|
||||
};
|
||||
lt = lessThan;
|
||||
|
||||
lessThanOrEqual = actual: max:
|
||||
if actual <= max
|
||||
then true
|
||||
else
|
||||
mkFailure {
|
||||
message = "Expected value to be less than or equal to ${formatVal max}";
|
||||
expected = "<= ${formatVal max}";
|
||||
inherit actual;
|
||||
};
|
||||
lte = lessThanOrEqual;
|
||||
|
||||
contains = container: item:
|
||||
if builtins.isList container
|
||||
then
|
||||
if builtins.elem item container
|
||||
then true
|
||||
else
|
||||
mkFailure {
|
||||
message = "Expected list to contain item";
|
||||
expected = "to contain: ${formatVal item}";
|
||||
actual = container;
|
||||
}
|
||||
else if builtins.isString container
|
||||
then
|
||||
if lib.strings.hasInfix item container
|
||||
then true
|
||||
else
|
||||
mkFailure {
|
||||
message = "Expected string to contain substring";
|
||||
expected = "to contain: ${formatVal item}";
|
||||
actual = container;
|
||||
}
|
||||
else
|
||||
mkFailure {
|
||||
message = "Expected list or string for 'contains'";
|
||||
actual = container;
|
||||
};
|
||||
|
||||
notContains = container: item:
|
||||
if builtins.isList container
|
||||
then
|
||||
if !(builtins.elem item container)
|
||||
then true
|
||||
else
|
||||
mkFailure {
|
||||
message = "Expected list to not contain item";
|
||||
expected = "not to contain: ${formatVal item}";
|
||||
actual = container;
|
||||
}
|
||||
else if builtins.isString container
|
||||
then
|
||||
if !(lib.strings.hasInfix item container)
|
||||
then true
|
||||
else
|
||||
mkFailure {
|
||||
message = "Expected string to not contain substring";
|
||||
expected = "not to contain: ${formatVal item}";
|
||||
actual = container;
|
||||
}
|
||||
else
|
||||
mkFailure {
|
||||
message = "Expected list or string for 'notContains'";
|
||||
actual = container;
|
||||
};
|
||||
|
||||
hasAttr = attr: set:
|
||||
if !(builtins.isAttrs set)
|
||||
then
|
||||
mkFailure {
|
||||
message = "Expected attrset for 'hasAttr'";
|
||||
actual = set;
|
||||
}
|
||||
else if builtins.hasAttr attr set
|
||||
then true
|
||||
else
|
||||
mkFailure {
|
||||
message = "Expected attrset to have attribute '${attr}'";
|
||||
expected = "to have attribute: ${attr}";
|
||||
actual = "available attributes: ${formatVal (builtins.attrNames set)}";
|
||||
};
|
||||
|
||||
missingAttr = attr: set:
|
||||
if !(builtins.isAttrs set)
|
||||
then
|
||||
mkFailure {
|
||||
message = "Expected attrset for 'missingAttr'";
|
||||
actual = set;
|
||||
}
|
||||
else if !(builtins.hasAttr attr set)
|
||||
then true
|
||||
else
|
||||
mkFailure {
|
||||
message = "Expected attrset to not have attribute '${attr}'";
|
||||
expected = "not to have attribute: ${attr}";
|
||||
actual = "found attribute '${attr}' with value: ${formatVal set.${attr}}";
|
||||
};
|
||||
|
||||
hasAttrs = attrs: set:
|
||||
if !(builtins.isAttrs set)
|
||||
then
|
||||
mkFailure {
|
||||
message = "Expected attrset for 'hasAttrs'";
|
||||
actual = set;
|
||||
}
|
||||
else let
|
||||
missing = builtins.filter (a: !builtins.hasAttr a set) attrs;
|
||||
in
|
||||
if missing == []
|
||||
then true
|
||||
else
|
||||
mkFailure {
|
||||
message = "Expected attrset to have attributes ${formatVal attrs}";
|
||||
expected = "missing attributes: ${formatVal missing}";
|
||||
actual = "available attributes: ${formatVal (builtins.attrNames set)}";
|
||||
};
|
||||
|
||||
matches = regex: str:
|
||||
if !(builtins.isString str)
|
||||
then
|
||||
mkFailure {
|
||||
message = "Expected string for 'matches'";
|
||||
actual = str;
|
||||
}
|
||||
else if builtins.match regex str != null
|
||||
then true
|
||||
else
|
||||
mkFailure {
|
||||
message = "Expected string to match regex '${regex}'";
|
||||
expected = "match regex: ${regex}";
|
||||
actual = str;
|
||||
};
|
||||
|
||||
isNull = val:
|
||||
if val == null
|
||||
then true
|
||||
else
|
||||
mkFailure {
|
||||
message = "Expected value to be null";
|
||||
expected = "null";
|
||||
actual = val;
|
||||
};
|
||||
|
||||
isNotNull = val:
|
||||
if val != null
|
||||
then true
|
||||
else
|
||||
mkFailure {
|
||||
message = "Expected value to not be null";
|
||||
expected = "not null";
|
||||
actual = "null";
|
||||
};
|
||||
|
||||
isEmpty = val: let
|
||||
empty =
|
||||
if builtins.isList val
|
||||
then val == []
|
||||
else if builtins.isAttrs val
|
||||
then val == {}
|
||||
else if builtins.isString val
|
||||
then val == ""
|
||||
else false;
|
||||
in
|
||||
if empty
|
||||
then true
|
||||
else
|
||||
mkFailure {
|
||||
message = "Expected value to be empty";
|
||||
expected = "empty";
|
||||
actual = val;
|
||||
};
|
||||
|
||||
isNotEmpty = val: let
|
||||
notEmpty =
|
||||
if builtins.isList val
|
||||
then val != []
|
||||
else if builtins.isAttrs val
|
||||
then val != {}
|
||||
else if builtins.isString val
|
||||
then val != ""
|
||||
else true;
|
||||
in
|
||||
if notEmpty
|
||||
then true
|
||||
else
|
||||
mkFailure {
|
||||
message = "Expected value to not be empty";
|
||||
expected = "not empty";
|
||||
actual = val;
|
||||
};
|
||||
|
||||
throws = expr: let
|
||||
res = builtins.tryEval expr;
|
||||
in
|
||||
if !res.success
|
||||
then true
|
||||
else
|
||||
mkFailure {
|
||||
message = "Expected expression to throw an error, but it succeeded";
|
||||
expected = "to throw";
|
||||
actual = "evaluated to: ${formatVal res.value}";
|
||||
};
|
||||
fails = throws;
|
||||
|
||||
custom = cond: message:
|
||||
if cond
|
||||
then true
|
||||
else
|
||||
mkFailure {
|
||||
inherit message;
|
||||
};
|
||||
that = custom;
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@
|
|||
inherit (lib) evalModules toList;
|
||||
in rec {
|
||||
helpers = import ./testHelpers.nix {inherit lib;};
|
||||
inherit (helpers) asserts expect;
|
||||
|
||||
mkBinary = {
|
||||
nixtests,
|
||||
|
|
|
|||
|
|
@ -73,9 +73,9 @@
|
|||
in "${fileRelative}:${toString val.line}";
|
||||
};
|
||||
type = mkOption {
|
||||
type = types.enum ["unit" "snapshot" "script" "vm"];
|
||||
type = types.enum ["unit" "snapshot" "script" "assertions" "vm"];
|
||||
description = ''
|
||||
Type of test, has to be one of "unit", "snapshot", "script", or "vm".
|
||||
Type of test, has to be one of "unit", "snapshot", "script", "assertions", or "vm".
|
||||
'';
|
||||
default = "unit";
|
||||
apply = value:
|
||||
|
|
@ -84,7 +84,9 @@
|
|||
assert assertMsg (value == "unit" -> !isUnset config.expected)
|
||||
"test '${config.name}' as type 'unit' requires 'expected' to be set";
|
||||
assert assertMsg (value == "unit" -> (xor (isUnset config.actual) (isUnset config.actualDrv)))
|
||||
"test '${config.name}' as type 'unit' requires only 'actual' OR 'actualDrv' to be set"; value;
|
||||
"test '${config.name}' as type 'unit' requires only 'actual' OR 'actualDrv' to be set";
|
||||
assert assertMsg (value == "assertions" -> !isUnset config.assertions)
|
||||
"test '${config.name}' as type 'assertions' requires 'assertions' to be set"; value;
|
||||
};
|
||||
name = mkOption {
|
||||
type = types.str;
|
||||
|
|
@ -172,6 +174,16 @@
|
|||
'';
|
||||
};
|
||||
};
|
||||
assertions = mkUnsetOption {
|
||||
type = types.attrsOf (types.oneOf [types.bool types.str types.attrs]);
|
||||
description = ''
|
||||
Attribute set of assertions to run for this test.
|
||||
Each assertion can evaluate to:
|
||||
- boolean (`true` for pass, `false` for fail)
|
||||
- string (custom failure message)
|
||||
- matcher result attrset (e.g. from `ntlib.asserts.*`)
|
||||
'';
|
||||
};
|
||||
|
||||
finalConfig = mkOption {
|
||||
internal = true;
|
||||
|
|
@ -180,11 +192,75 @@
|
|||
};
|
||||
config = {
|
||||
finalConfig = builtins.addErrorContext "[nixtest] while processing test ${config.name}" {
|
||||
inherit (config) name expected actual actualDrv;
|
||||
inherit (config) name expected actual actualDrv pos description;
|
||||
type =
|
||||
if config.type == "vm"
|
||||
then "script"
|
||||
else config.type;
|
||||
assertions =
|
||||
if config.type == "assertions"
|
||||
then
|
||||
assert assertMsg (!isUnset config.assertions)
|
||||
"test '${config.name}' as type 'assertions' requires 'assertions' to be set";
|
||||
assert assertMsg (builtins.isAttrs config.assertions)
|
||||
"test '${config.name}' assertions must be an attribute set";
|
||||
builtins.mapAttrs (
|
||||
assertName: assertionExpr: let
|
||||
evalRes = builtins.tryEval assertionExpr;
|
||||
in
|
||||
if !evalRes.success
|
||||
then {
|
||||
ok = false;
|
||||
error = true;
|
||||
message = "Nix evaluation error (expression threw or aborted)";
|
||||
}
|
||||
else let
|
||||
val = evalRes.value;
|
||||
in
|
||||
if builtins.isBool val
|
||||
then {
|
||||
ok = val;
|
||||
message =
|
||||
if val
|
||||
then ""
|
||||
else "Assertion evaluated to false";
|
||||
}
|
||||
else if builtins.isString val
|
||||
then {
|
||||
ok = false;
|
||||
message = val;
|
||||
}
|
||||
else if builtins.isAttrs val && (val ? ok)
|
||||
then {
|
||||
ok = val.ok;
|
||||
error = val.error or false;
|
||||
message = val.message or "";
|
||||
expected =
|
||||
if val ? expected
|
||||
then
|
||||
(
|
||||
if builtins.isString val.expected
|
||||
then val.expected
|
||||
else generators.toPretty {} val.expected
|
||||
)
|
||||
else "";
|
||||
actual =
|
||||
if val ? actual
|
||||
then
|
||||
(
|
||||
if builtins.isString val.actual
|
||||
then val.actual
|
||||
else generators.toPretty {} val.actual
|
||||
)
|
||||
else "";
|
||||
}
|
||||
else {
|
||||
ok = false;
|
||||
message = "Invalid assertion return type: expected boolean, failure message string, or matcher result";
|
||||
}
|
||||
)
|
||||
config.assertions
|
||||
else unset;
|
||||
script =
|
||||
if config.type == "vm"
|
||||
then
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
{lib, ...}: {
|
||||
{lib, ...}: rec {
|
||||
path = pkgs: "export PATH=${lib.makeBinPath pkgs}";
|
||||
pathAdd = pkgs: "export PATH=$PATH:${lib.makeBinPath pkgs}";
|
||||
scriptHelpers = builtins.readFile ./scriptHelpers.sh;
|
||||
toJsonFile = any: builtins.toFile "actual" (builtins.unsafeDiscardStringContext (builtins.toJSON any));
|
||||
toPrettyFile = any: builtins.toFile "actual" (lib.generators.toPretty {} any);
|
||||
|
||||
asserts = import ./asserts.nix {inherit lib;};
|
||||
expect = asserts;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue