Module-level assertions (#8)
I had a usecase for assertions in the module system for my [trivial builders PR in tidepool](auxolotl/labs#24), so I added a pretty basic way of defining them. It's somewhat quick and dirty I feel like, and given lib has hit 1.0 I precautiously added a warning "module assertions are experimental" if a module defines them, which is traced just before checking them. Assertions are not checked if `__module__.check = false`. I also took the liberty to fixup little things in tests and the module lib (the 3 commits with `fix:`). Each commit is rather tiny, but I split them up for review. re-formatting is in the last one. Co-authored-by: austreelis <git@swhaele.net> Reviewed-on: #8 Reviewed-by: Jake Hamilton <jake.hamilton@hey.com> Co-authored-by: Austreelis <austreelis@noreply.git.auxolotl.org> Co-committed-by: Austreelis <austreelis@noreply.git.auxolotl.org>
This commit is contained in:
parent
97e4032e45
commit
0f5182ff0d
3 changed files with 149 additions and 7 deletions
|
|
@ -101,7 +101,7 @@ in
|
|||
];
|
||||
};
|
||||
in
|
||||
(builtins.trace (builtins.deepSeq actual actual)) actual == expected;
|
||||
actual == expected;
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -219,6 +219,7 @@ lib: {
|
|||
"__key__"
|
||||
"includes"
|
||||
"excludes"
|
||||
"assertions"
|
||||
"options"
|
||||
"config"
|
||||
"freeform"
|
||||
|
|
@ -240,11 +241,12 @@ lib: {
|
|||
let
|
||||
invalid = builtins.removeAttrs module lib.modules.VALID_KEYS;
|
||||
invalidKeys = builtins.concatStringsSep ", " (builtins.attrNames invalid);
|
||||
__file__ = builtins.toString module.__file__ or file;
|
||||
__key__ = builtins.toString module.__key__ or key;
|
||||
in
|
||||
if lib.modules.validate.keys module then
|
||||
{
|
||||
__file__ = builtins.toString module.__file__ or file;
|
||||
__key__ = builtins.toString module.__key__ or key;
|
||||
inherit __file__ __key__;
|
||||
includes = module.includes or [ ];
|
||||
excludes = module.excludes or [ ];
|
||||
options = module.options or { };
|
||||
|
|
@ -271,9 +273,44 @@ lib: {
|
|||
config;
|
||||
in
|
||||
withFreeform (withMeta base);
|
||||
assertions =
|
||||
let
|
||||
type = builtins.typeOf module.assertions;
|
||||
validateAssert =
|
||||
assertion:
|
||||
(builtins.isAttrs assertion)
|
||||
&& (
|
||||
builtins.removeAttrs assertion [
|
||||
"value"
|
||||
"message"
|
||||
] == { }
|
||||
)
|
||||
&& (assertion ? value)
|
||||
&& (assertion ? message);
|
||||
normalizeAssert =
|
||||
assertion:
|
||||
assertion
|
||||
// {
|
||||
file = __file__;
|
||||
key = __key__;
|
||||
};
|
||||
in
|
||||
if !module ? assertions then
|
||||
[ ]
|
||||
else if type != "list" then
|
||||
builtins.throw "Module `${__key__}` (${__file__}) has an 'assertions' attribute that is not a list but a ${type}"
|
||||
else if !builtins.all (validateAssert) module.assertions then
|
||||
builtins.throw ''
|
||||
Module `${__key__}` (${__file__}) has invalid assertion(s)
|
||||
Each assertion should be a set with:
|
||||
- a 'value' attribute, which indicates the assertion fails if and only if it is not `true`.
|
||||
- a 'message' attribute containing the message to throw if the assertion fails.
|
||||
No other attribute is permitted''
|
||||
else
|
||||
builtins.map normalizeAssert module.assertions;
|
||||
}
|
||||
else
|
||||
builtins.throw "Module `${key}` (${file}) has unsupported attribute(s): ${invalidKeys}";
|
||||
builtins.throw "Module `${__key__}` (${__file__}) has unsupported attribute(s): ${invalidKeys}";
|
||||
|
||||
/**
|
||||
Convert a module that is either a function or an attribute set into
|
||||
|
|
@ -520,6 +557,8 @@ lib: {
|
|||
|
||||
configs = builtins.concatMap getConfig modules;
|
||||
|
||||
assertions = builtins.concatMap (module: module.assertions) modules;
|
||||
|
||||
process =
|
||||
prefix: options: configs:
|
||||
let
|
||||
|
|
@ -630,7 +669,7 @@ lib: {
|
|||
);
|
||||
};
|
||||
in
|
||||
process prefix modules configs;
|
||||
process prefix modules configs // { inherit assertions; };
|
||||
|
||||
/**
|
||||
Run a set of modules. Custom arguments can also be supplied which will
|
||||
|
|
@ -856,6 +895,22 @@ lib: {
|
|||
''
|
||||
else
|
||||
builtins.throw message
|
||||
else if config.__module__.check && merged.assertions != [ ] then
|
||||
builtins.trace "[WARN]: module assertions are experimental" builtins.foldl' (
|
||||
_:
|
||||
{
|
||||
value,
|
||||
message,
|
||||
file,
|
||||
key,
|
||||
}:
|
||||
if (builtins.tryEval value).value == true then
|
||||
null
|
||||
else
|
||||
builtins.throw ''
|
||||
Assertion failed in module `${key}` (${file}):
|
||||
${builtins.toString message}''
|
||||
) null merged.assertions
|
||||
else
|
||||
null;
|
||||
|
||||
|
|
|
|||
|
|
@ -186,6 +186,7 @@ in
|
|||
__key__ = "aux/example";
|
||||
includes = [ ];
|
||||
excludes = [ ];
|
||||
assertions = [ ];
|
||||
options = { };
|
||||
config = { };
|
||||
freeform = null;
|
||||
|
|
@ -374,6 +375,7 @@ in
|
|||
config = { };
|
||||
excludes = [ ];
|
||||
includes = [ ];
|
||||
assertions = [ ];
|
||||
options = { };
|
||||
};
|
||||
actual = lib.modules.normalize "/aux/example.nix" "example" { };
|
||||
|
|
@ -390,6 +392,7 @@ in
|
|||
};
|
||||
excludes = [ ];
|
||||
includes = [ ];
|
||||
assertions = [ ];
|
||||
options = { };
|
||||
};
|
||||
actual = lib.modules.normalize "/aux/example.nix" "example" {
|
||||
|
|
@ -399,6 +402,29 @@ in
|
|||
};
|
||||
in
|
||||
actual == expected;
|
||||
|
||||
"adds details to assertions" =
|
||||
let
|
||||
assertions = [
|
||||
{
|
||||
value = true;
|
||||
message = "unreachable";
|
||||
file = "myfile.nix";
|
||||
key = "mykey";
|
||||
}
|
||||
];
|
||||
actual = lib.modules.normalize "/aux/example.nix" "example" {
|
||||
__file__ = "myfile.nix";
|
||||
__key__ = "mykey";
|
||||
assertions = [
|
||||
{
|
||||
value = true;
|
||||
message = "unreachable";
|
||||
}
|
||||
];
|
||||
};
|
||||
in
|
||||
actual.assertions == assertions;
|
||||
};
|
||||
|
||||
"resolve" = {
|
||||
|
|
@ -445,6 +471,7 @@ in
|
|||
expected = {
|
||||
matched = { };
|
||||
unmatched = [ ];
|
||||
assertions = [ ];
|
||||
};
|
||||
actual = lib.modules.combine [ ] [ (lib.modules.normalize "/aux/example.nix" "example" { }) ];
|
||||
in
|
||||
|
|
@ -461,6 +488,14 @@ in
|
|||
value = 1;
|
||||
}
|
||||
];
|
||||
assertions = [
|
||||
{
|
||||
value = true;
|
||||
message = "unreachable";
|
||||
file = "/aux/example.nix";
|
||||
key = "example";
|
||||
}
|
||||
];
|
||||
};
|
||||
actual =
|
||||
lib.modules.combine
|
||||
|
|
@ -470,6 +505,12 @@ in
|
|||
config = {
|
||||
x = 1;
|
||||
};
|
||||
assertions = [
|
||||
{
|
||||
value = true;
|
||||
message = "unreachable";
|
||||
}
|
||||
];
|
||||
})
|
||||
];
|
||||
in
|
||||
|
|
@ -484,11 +525,25 @@ in
|
|||
value = 2;
|
||||
}
|
||||
];
|
||||
assertions = [
|
||||
{
|
||||
value = true;
|
||||
message = "unreachable";
|
||||
file = "/aux/example1.nix";
|
||||
key = "example1";
|
||||
}
|
||||
{
|
||||
value = true;
|
||||
message = "unreachable";
|
||||
file = "/aux/example2.nix";
|
||||
key = "example2";
|
||||
}
|
||||
];
|
||||
actual =
|
||||
lib.modules.combine
|
||||
[ ]
|
||||
[
|
||||
(lib.modules.normalize "/aux/example1.nix" "example2" {
|
||||
(lib.modules.normalize "/aux/example1.nix" "example1" {
|
||||
options = {
|
||||
x = lib.options.create { };
|
||||
};
|
||||
|
|
@ -496,15 +551,29 @@ in
|
|||
config = {
|
||||
x = 1;
|
||||
};
|
||||
|
||||
assertions = [
|
||||
{
|
||||
value = true;
|
||||
message = "unreachable";
|
||||
}
|
||||
];
|
||||
})
|
||||
(lib.modules.normalize "/aux/example2.nix" "example2" {
|
||||
config = {
|
||||
y = 2;
|
||||
};
|
||||
|
||||
assertions = [
|
||||
{
|
||||
value = true;
|
||||
message = "unreachable";
|
||||
}
|
||||
];
|
||||
})
|
||||
];
|
||||
in
|
||||
(actual.unmatched == unmatched) && actual.matched ? x;
|
||||
(actual.unmatched == unmatched) && actual.matched ? x && (actual.assertions == assertions);
|
||||
};
|
||||
|
||||
"run" = {
|
||||
|
|
@ -868,5 +937,23 @@ in
|
|||
};
|
||||
in
|
||||
(evaluated.config.aux.message == evaluated.config.aux.message2) && evaluated.config.aux.exists;
|
||||
|
||||
"check assertions" =
|
||||
let
|
||||
evaluated = lib.modules.run {
|
||||
modules = [
|
||||
{
|
||||
assertions = [
|
||||
{
|
||||
value = false;
|
||||
message = "spooky";
|
||||
}
|
||||
];
|
||||
}
|
||||
];
|
||||
};
|
||||
in
|
||||
(builtins.tryEval evaluated).success == true
|
||||
&& (builtins.tryEval evaluated.config).success == false;
|
||||
};
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue