2025-06-12 21:23:28 +02:00
|
|
|
{
|
|
|
|
|
pkgs,
|
|
|
|
|
lib,
|
|
|
|
|
...
|
|
|
|
|
}: let
|
2025-07-30 21:53:53 +02:00
|
|
|
inherit
|
|
|
|
|
(lib)
|
|
|
|
|
mkOptionType
|
|
|
|
|
mkOption
|
|
|
|
|
types
|
|
|
|
|
filterAttrs
|
|
|
|
|
isType
|
|
|
|
|
removePrefix
|
|
|
|
|
assertMsg
|
|
|
|
|
generators
|
2025-10-01 16:43:30 +02:00
|
|
|
literalExpression
|
2025-07-30 21:53:53 +02:00
|
|
|
;
|
2025-06-12 21:23:28 +02:00
|
|
|
|
|
|
|
|
nixtest-lib = import ./default.nix {inherit pkgs lib;};
|
|
|
|
|
|
|
|
|
|
unsetType = mkOptionType {
|
|
|
|
|
name = "unset";
|
|
|
|
|
description = "unset";
|
|
|
|
|
descriptionClass = "noun";
|
|
|
|
|
check = value: true;
|
|
|
|
|
};
|
|
|
|
|
unset = {
|
|
|
|
|
_type = "unset";
|
|
|
|
|
};
|
2025-07-30 21:53:53 +02:00
|
|
|
isUnset = isType "unset";
|
2025-10-01 16:43:30 +02:00
|
|
|
unsetOr = typ:
|
|
|
|
|
(types.either unsetType typ)
|
|
|
|
|
// {
|
|
|
|
|
inherit (typ) description getSubOptions;
|
|
|
|
|
};
|
|
|
|
|
mkUnsetOption = opts:
|
|
|
|
|
mkOption (opts
|
|
|
|
|
// {
|
|
|
|
|
type = unsetOr opts.type;
|
|
|
|
|
default = opts.default or unset;
|
|
|
|
|
defaultText = literalExpression "unset";
|
|
|
|
|
});
|
2025-06-12 21:23:28 +02:00
|
|
|
|
|
|
|
|
filterUnset = value:
|
|
|
|
|
if builtins.isAttrs value && !builtins.hasAttr "_type" value
|
|
|
|
|
then let
|
|
|
|
|
filteredAttrs = builtins.mapAttrs (n: v: filterUnset v) value;
|
|
|
|
|
in
|
2025-07-30 21:53:53 +02:00
|
|
|
filterAttrs (name: value: (!isUnset value)) filteredAttrs
|
2025-06-12 21:23:28 +02:00
|
|
|
else if builtins.isList value
|
|
|
|
|
then builtins.filter (elem: !isUnset elem) (map filterUnset value)
|
|
|
|
|
else value;
|
|
|
|
|
|
|
|
|
|
testsSubmodule = {
|
|
|
|
|
config,
|
|
|
|
|
testsBase,
|
|
|
|
|
pos,
|
|
|
|
|
...
|
|
|
|
|
}: {
|
|
|
|
|
options = {
|
2025-10-01 16:43:30 +02:00
|
|
|
pos = mkUnsetOption {
|
|
|
|
|
type = types.attrs;
|
|
|
|
|
description = ''
|
|
|
|
|
Position of test, use `__curPos` for automatic insertion of current position.
|
|
|
|
|
'';
|
2025-06-12 21:23:28 +02:00
|
|
|
default = pos;
|
|
|
|
|
apply = val:
|
|
|
|
|
if isUnset val
|
|
|
|
|
then val
|
|
|
|
|
else let
|
2025-07-30 21:53:53 +02:00
|
|
|
fileRelative = removePrefix testsBase val.file;
|
2025-06-12 21:23:28 +02:00
|
|
|
in "${fileRelative}:${toString val.line}";
|
|
|
|
|
};
|
|
|
|
|
type = mkOption {
|
|
|
|
|
type = types.enum ["unit" "snapshot" "script"];
|
2025-10-01 16:43:30 +02:00
|
|
|
description = ''
|
|
|
|
|
Type of test, has to be one of "unit", "snapshot" or "script".
|
|
|
|
|
'';
|
2025-06-12 21:23:28 +02:00
|
|
|
default = "unit";
|
|
|
|
|
apply = value:
|
2025-07-30 21:53:53 +02:00
|
|
|
assert assertMsg (value != "script" || !isUnset config.script)
|
2025-06-12 21:23:28 +02:00
|
|
|
"test '${config.name}' as type 'script' requires 'script' to be set";
|
2025-07-30 21:53:53 +02:00
|
|
|
assert assertMsg (value != "unit" || !isUnset config.expected)
|
2025-06-12 21:23:28 +02:00
|
|
|
"test '${config.name}' as type 'unit' requires 'expected' to be set";
|
2025-07-30 21:53:53 +02:00
|
|
|
assert assertMsg (
|
2025-06-12 21:23:28 +02:00
|
|
|
let
|
|
|
|
|
actualIsUnset = isUnset config.actual;
|
|
|
|
|
actualDrvIsUnset = isUnset config.actualDrv;
|
|
|
|
|
in
|
|
|
|
|
(value != "unit")
|
|
|
|
|
|| (!actualIsUnset && actualDrvIsUnset)
|
|
|
|
|
|| (actualIsUnset && !actualDrvIsUnset)
|
|
|
|
|
)
|
|
|
|
|
"test '${config.name}' as type 'unit' requires only 'actual' OR 'actualDrv' to be set"; value;
|
|
|
|
|
};
|
|
|
|
|
name = mkOption {
|
|
|
|
|
type = types.str;
|
2025-10-01 16:43:30 +02:00
|
|
|
description = ''
|
|
|
|
|
Name of this test.
|
|
|
|
|
'';
|
2025-06-12 21:23:28 +02:00
|
|
|
};
|
2025-10-01 16:43:30 +02:00
|
|
|
description = mkUnsetOption {
|
|
|
|
|
type = types.str;
|
|
|
|
|
description = ''
|
|
|
|
|
Short description of the test.
|
|
|
|
|
'';
|
2025-06-12 21:23:28 +02:00
|
|
|
};
|
|
|
|
|
format = mkOption {
|
|
|
|
|
type = types.enum ["json" "pretty"];
|
2025-10-01 16:43:30 +02:00
|
|
|
description = ''
|
|
|
|
|
Which format to use for serializing arbitrary values.
|
|
|
|
|
Required since this config is serialized to JSON for passing it to Nixtest, so no Nix-values can be used directly.
|
|
|
|
|
|
|
|
|
|
- `json`: serializes the data to json using `builtins.toJSON`
|
|
|
|
|
- `pretty`: serializes the data to a "pretty" format using `lib.generators.toPretty`
|
|
|
|
|
'';
|
2025-06-12 21:23:28 +02:00
|
|
|
default = "json";
|
|
|
|
|
};
|
2025-10-01 16:43:30 +02:00
|
|
|
expected = mkUnsetOption {
|
2025-06-12 21:23:28 +02:00
|
|
|
type = types.anything;
|
2025-10-01 16:43:30 +02:00
|
|
|
description = ''
|
|
|
|
|
Expected value of the test. Remember, the values are serialized (see [here](#suitesnametestsformat)).
|
|
|
|
|
'';
|
2025-06-12 21:23:28 +02:00
|
|
|
apply = val:
|
|
|
|
|
if isUnset val || config.format == "json"
|
|
|
|
|
then val
|
2025-07-30 21:53:53 +02:00
|
|
|
else generators.toPretty {} val;
|
2025-06-12 21:23:28 +02:00
|
|
|
};
|
2025-10-01 16:43:30 +02:00
|
|
|
actual = mkUnsetOption {
|
2025-06-12 21:23:28 +02:00
|
|
|
type = types.anything;
|
2025-10-01 16:43:30 +02:00
|
|
|
description = ''
|
|
|
|
|
Actual value of the test. Remember, the values are serialized (see [here](#suitesnametestsformat)).
|
|
|
|
|
'';
|
2025-06-12 21:23:28 +02:00
|
|
|
apply = val:
|
|
|
|
|
if isUnset val || config.format == "json"
|
|
|
|
|
then val
|
2025-07-30 21:53:53 +02:00
|
|
|
else generators.toPretty {} val;
|
2025-06-12 21:23:28 +02:00
|
|
|
};
|
2025-10-01 16:43:30 +02:00
|
|
|
actualDrv = mkUnsetOption {
|
|
|
|
|
type = types.package;
|
|
|
|
|
description = ''
|
|
|
|
|
Actual value of the test, but as a derivation.
|
|
|
|
|
Nixtest will build this derivation when running the test, then compare the contents of the
|
|
|
|
|
resulting file to the [`expected`](#suitesnametestsexpected) value.
|
|
|
|
|
'';
|
2025-06-12 21:23:28 +02:00
|
|
|
apply = val:
|
|
|
|
|
# keep unset value
|
|
|
|
|
if isUnset val
|
|
|
|
|
then val
|
|
|
|
|
else builtins.unsafeDiscardStringContext (val.drvPath or "");
|
|
|
|
|
};
|
2025-10-01 16:43:30 +02:00
|
|
|
script = mkUnsetOption {
|
|
|
|
|
type = types.str;
|
|
|
|
|
description = ''
|
|
|
|
|
Script to run for the test.
|
|
|
|
|
Nixtest will run this, failing the test if it exits with a non-zero exit code.
|
|
|
|
|
'';
|
2025-06-12 21:23:28 +02:00
|
|
|
apply = val:
|
|
|
|
|
if isUnset val
|
|
|
|
|
then val
|
|
|
|
|
else
|
|
|
|
|
builtins.unsafeDiscardStringContext
|
2025-06-13 15:43:21 +02:00
|
|
|
(pkgs.writeShellScript "nixtest-${config.name}" val).drvPath;
|
2025-06-12 21:23:28 +02:00
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
suitesSubmodule = {
|
|
|
|
|
name,
|
|
|
|
|
config,
|
|
|
|
|
testsBase,
|
|
|
|
|
...
|
|
|
|
|
}: {
|
|
|
|
|
options = {
|
|
|
|
|
name = mkOption {
|
|
|
|
|
type = types.str;
|
2025-10-01 16:43:30 +02:00
|
|
|
description = ''
|
|
|
|
|
Name of the suite, uses attrset name by default.
|
|
|
|
|
'';
|
2025-06-12 21:23:28 +02:00
|
|
|
default = name;
|
2025-10-01 16:43:30 +02:00
|
|
|
defaultText = literalExpression name;
|
2025-06-12 21:23:28 +02:00
|
|
|
};
|
2025-10-01 16:43:30 +02:00
|
|
|
pos = mkUnsetOption {
|
|
|
|
|
type = types.attrs;
|
|
|
|
|
description = ''
|
|
|
|
|
Position for tests, use `__curPos` for automatic insertion of current position.
|
|
|
|
|
This will set `pos` for every test of this suite, useful if the suite's tests are all in a single file.
|
|
|
|
|
'';
|
|
|
|
|
example = literalExpression "__curPos";
|
2025-06-12 21:23:28 +02:00
|
|
|
};
|
|
|
|
|
tests = mkOption {
|
|
|
|
|
type = types.listOf (types.submoduleWith {
|
|
|
|
|
modules = [testsSubmodule];
|
|
|
|
|
specialArgs = {
|
|
|
|
|
inherit (config) pos;
|
|
|
|
|
inherit testsBase;
|
|
|
|
|
};
|
|
|
|
|
});
|
2025-10-01 16:43:30 +02:00
|
|
|
description = ''
|
|
|
|
|
Define tests of this suite here.
|
|
|
|
|
'';
|
2025-06-12 21:23:28 +02:00
|
|
|
default = [];
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
nixtestSubmodule = {config, ...}: {
|
2025-10-01 19:42:30 +02:00
|
|
|
_file = ./module.nix;
|
2025-06-12 21:23:28 +02:00
|
|
|
options = {
|
|
|
|
|
base = mkOption {
|
|
|
|
|
type = types.str;
|
2025-10-01 16:43:30 +02:00
|
|
|
description = ''
|
|
|
|
|
Base directory of the tests, will be removed from the test file path.
|
|
|
|
|
This makes it possible to show the relative path from the git repo, instead of ugly Nix store paths.
|
|
|
|
|
'';
|
2025-06-12 21:23:28 +02:00
|
|
|
default = "";
|
|
|
|
|
};
|
|
|
|
|
skip = mkOption {
|
|
|
|
|
type = types.str;
|
2025-10-01 16:43:30 +02:00
|
|
|
description = ''
|
|
|
|
|
Tests to skip, is passed to Nixtest's `--skip` param.
|
|
|
|
|
'';
|
2025-06-12 21:23:28 +02:00
|
|
|
default = "";
|
|
|
|
|
};
|
|
|
|
|
suites = mkOption {
|
|
|
|
|
type = types.attrsOf (types.submoduleWith {
|
|
|
|
|
modules = [suitesSubmodule];
|
|
|
|
|
specialArgs = {
|
|
|
|
|
testsBase = config.base;
|
|
|
|
|
};
|
|
|
|
|
});
|
2025-10-01 16:43:30 +02:00
|
|
|
description = ''
|
|
|
|
|
Define your test suites here, every test belongs to a suite.
|
|
|
|
|
'';
|
2025-06-12 21:23:28 +02:00
|
|
|
default = {};
|
|
|
|
|
apply = suites:
|
|
|
|
|
map (
|
|
|
|
|
n: filterUnset (builtins.removeAttrs suites.${n} ["pos"])
|
|
|
|
|
)
|
|
|
|
|
(builtins.attrNames suites);
|
2025-10-01 16:43:30 +02:00
|
|
|
example = {
|
|
|
|
|
"Suite A".tests = [
|
|
|
|
|
{
|
|
|
|
|
name = "Some Test";
|
|
|
|
|
}
|
|
|
|
|
];
|
|
|
|
|
};
|
2025-06-12 21:23:28 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
finalConfigJson = mkOption {
|
|
|
|
|
internal = true;
|
|
|
|
|
type = types.package;
|
|
|
|
|
};
|
|
|
|
|
app = mkOption {
|
|
|
|
|
internal = true;
|
|
|
|
|
type = types.package;
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
config = {
|
|
|
|
|
finalConfigJson = nixtest-lib.exportSuites config.suites;
|
|
|
|
|
app = nixtest-lib.mkBinary {
|
|
|
|
|
nixtests = config.finalConfigJson;
|
|
|
|
|
extraParams = ''--skip="${config.skip}"'';
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
in
|
|
|
|
|
nixtestSubmodule
|