mirror of
https://github.com/NixOS/nixpkgs.git
synced 2026-07-06 17:13:24 -05:00
925 lines
24 KiB
Nix
925 lines
24 KiB
Nix
# Run with:
|
|
# cd nixpkgs
|
|
# ./lib/tests/modules.sh
|
|
{ lib, config, ... }:
|
|
let
|
|
inherit (lib)
|
|
mkOption
|
|
mkOrder
|
|
mkMerge
|
|
mkBefore
|
|
mkAfter
|
|
mkIf
|
|
mkOverride
|
|
mkDefault
|
|
mkForce
|
|
types
|
|
;
|
|
in
|
|
{
|
|
options = {
|
|
attrList = mkOption {
|
|
type = types.lazyAttrsOf (types.attrListOf types.str);
|
|
};
|
|
|
|
attrListInt = mkOption {
|
|
type = types.lazyAttrsOf (types.attrListOf types.int);
|
|
};
|
|
|
|
attrListSubmodule = mkOption {
|
|
type = types.attrListOf (
|
|
types.submodule {
|
|
options.port = mkOption {
|
|
type = types.int;
|
|
description = "Port number";
|
|
};
|
|
options.host = mkOption {
|
|
type = types.str;
|
|
default = "localhost";
|
|
description = "Hostname";
|
|
};
|
|
}
|
|
);
|
|
};
|
|
|
|
# asAttrs: value is a merged attrset, ordered list in valueMeta
|
|
asAttrs = mkOption {
|
|
type = types.lazyAttrsOf (
|
|
types.attrListWith {
|
|
elemType = types.str;
|
|
asAttrs = true;
|
|
mergeAttrValues = _name: values: lib.last values;
|
|
}
|
|
);
|
|
};
|
|
|
|
# asAttrs with default mergeAttrValues: duplicates collected into lists
|
|
asAttrsDefault = mkOption {
|
|
type = types.lazyAttrsOf (
|
|
types.attrListWith {
|
|
elemType = types.int;
|
|
asAttrs = true;
|
|
}
|
|
);
|
|
};
|
|
|
|
# Strict wrappers that force deep evaluation, for testing error cases
|
|
attrListStrict = mkOption {
|
|
type = types.lazyAttrsOf types.raw;
|
|
};
|
|
|
|
attrListIntStrict = mkOption {
|
|
type = types.lazyAttrsOf types.raw;
|
|
};
|
|
|
|
# either picks attrList when input is list/attrset, int when input is int
|
|
eitherAttrListOrInt = mkOption {
|
|
type = types.either (types.attrListOf types.str) types.int;
|
|
};
|
|
|
|
eitherAttrListOrIntFallback = mkOption {
|
|
type = types.either (types.attrListOf types.str) types.int;
|
|
};
|
|
|
|
eitherIntOrAttrList = mkOption {
|
|
type = types.either types.int (types.attrListOf types.str);
|
|
};
|
|
|
|
eitherIntOrAttrListFallback = mkOption {
|
|
type = types.either types.int (types.attrListOf types.str);
|
|
};
|
|
|
|
assertions = mkOption { };
|
|
};
|
|
|
|
imports = [
|
|
# Second module contributing to multiModule
|
|
{
|
|
attrListInt.multiModule = [
|
|
{ b = 2; }
|
|
];
|
|
}
|
|
];
|
|
|
|
config = {
|
|
|
|
# List input: pass-through
|
|
attrList.listInput = [
|
|
{ a = "alpha"; }
|
|
{ b = "beta"; }
|
|
];
|
|
|
|
# Attrset input with explicit ordering
|
|
attrList.attrsetOrdered = {
|
|
x = mkOrder 200 "x-val";
|
|
y = mkOrder 100 "y-val";
|
|
};
|
|
|
|
# Mixed: list elements at default priority, attrset with mkOrder
|
|
attrList.mixed = mkMerge [
|
|
[
|
|
{ m = "from-list"; }
|
|
]
|
|
{
|
|
n = mkOrder 50 "from-attrset";
|
|
}
|
|
];
|
|
|
|
# Multiple list definitions from separate modules
|
|
attrListInt.multiModule = [
|
|
{ a = 1; }
|
|
];
|
|
|
|
# Attrset without mkOrder uses default priority
|
|
attrList.attrsetNoOrder = {
|
|
foo = "bar";
|
|
baz = "qux";
|
|
};
|
|
|
|
# Empty list
|
|
attrList.empty = [ ];
|
|
|
|
# Ordering test: lower priority first
|
|
attrList.ordering = mkMerge [
|
|
{
|
|
last = mkOrder 1500 "last";
|
|
}
|
|
{
|
|
first = mkOrder 500 "first";
|
|
}
|
|
[
|
|
{ middle = "middle"; }
|
|
]
|
|
];
|
|
|
|
# List elements support mkOrder/mkBefore/mkAfter
|
|
attrList.listOrdering = [
|
|
(mkAfter { z = "after"; })
|
|
{ m = "default"; }
|
|
(mkBefore { a = "before"; })
|
|
];
|
|
|
|
# Plain list entries land at default priority (1000):
|
|
# they appear after mkOrder 999 and before mkOrder 1001.
|
|
attrList.listDefaultPrio = mkMerge [
|
|
{ after = mkOrder 1001 "after"; }
|
|
[
|
|
{ mid = "list-entry"; }
|
|
]
|
|
{ before = mkOrder 999 "before"; }
|
|
];
|
|
|
|
# mkBefore and mkAfter
|
|
attrList.beforeAfter = mkMerge [
|
|
{
|
|
z = mkAfter "after";
|
|
}
|
|
{
|
|
a = mkBefore "before";
|
|
}
|
|
[
|
|
{ m = "default"; }
|
|
]
|
|
];
|
|
|
|
# mkIf: conditional definition
|
|
attrList.withMkIf = mkMerge [
|
|
(mkIf true [
|
|
{ yes = "included"; }
|
|
])
|
|
(mkIf false [
|
|
{ no = "excluded"; }
|
|
])
|
|
];
|
|
|
|
# mkOverride: higher priority override wins
|
|
attrList.withOverride = mkMerge [
|
|
(mkOverride 100 [
|
|
{ replaced = "gone"; }
|
|
])
|
|
(mkOverride 50 [
|
|
{ winner = "wins"; }
|
|
])
|
|
];
|
|
|
|
# mkDefault: lower priority than normal
|
|
attrList.withDefault = mkMerge [
|
|
(mkDefault [
|
|
{ default = "overridden"; }
|
|
])
|
|
[
|
|
{ normal = "wins"; }
|
|
]
|
|
];
|
|
|
|
# mkForce on the whole option (should discard other defs)
|
|
attrList.withForce = mkMerge [
|
|
[
|
|
{ discarded = "gone"; }
|
|
]
|
|
(mkForce [
|
|
{ forced = "wins"; }
|
|
])
|
|
];
|
|
|
|
# mkForce with mkOrder inside
|
|
attrList.forceWithOrder = mkForce [
|
|
(mkAfter { second = "after"; })
|
|
(mkBefore { first = "before"; })
|
|
];
|
|
|
|
# mkForce on individual element values; non-forced entries are discarded.
|
|
# Discarded values use a mix of: plain value, mkDefault(abort ...), mkOverride 100 (abort ...).
|
|
# The abort variants verify laziness: peelProperties sees the wrapper without forcing the content.
|
|
attrListInt.forceElementValue = [
|
|
{ a = mkDefault (abort "overridden by mkForce; laziness guarantee: MUST NOT be evaluated"); }
|
|
{ a = mkForce 42; }
|
|
{ a = mkOverride 100 (abort "overridden by mkForce; laziness guarantee: MUST NOT be evaluated"); }
|
|
{ b = 2; }
|
|
];
|
|
|
|
# mkForce on attrset format
|
|
attrList.forceAttrset = mkMerge [
|
|
[
|
|
{ discarded = "gone"; }
|
|
]
|
|
(mkForce {
|
|
x = mkOrder 200 "x-val";
|
|
y = mkOrder 100 "y-val";
|
|
})
|
|
];
|
|
|
|
# mkForce on repeated key: forced entries override non-forced
|
|
attrList.forceRepeatedKey = [
|
|
{ x = mkOverride 100 (abort "overridden by mkForce; laziness guarantee: MUST NOT be evaluated"); }
|
|
{ x = mkForce "wins"; }
|
|
{ x = mkForce "wins 2"; }
|
|
];
|
|
|
|
# mkForce on repeated key across mkMerge
|
|
attrList.forceRepeatedKeyMerge = mkMerge [
|
|
[
|
|
{ x = "unused: overridden by mkForce"; }
|
|
{ x = mkDefault (abort "overridden by mkForce; laziness guarantee: MUST NOT be evaluated"); }
|
|
]
|
|
[
|
|
{ x = mkOverride 100 (abort "overridden by mkForce; laziness guarantee: MUST NOT be evaluated"); }
|
|
]
|
|
[
|
|
{ x = mkForce "forced"; }
|
|
]
|
|
];
|
|
|
|
# mkForce on repeated key in attrset format across mkMerge
|
|
attrList.forceRepeatedKeyAttrs = mkMerge [
|
|
{
|
|
x = mkDefault (abort "overridden by mkForce; laziness guarantee: MUST NOT be evaluated");
|
|
y = "kept";
|
|
}
|
|
{ x = mkForce "forced"; }
|
|
];
|
|
|
|
# mkForce only affects the key it's on, other keys survive
|
|
attrList.forcePartialAttrs = mkMerge [
|
|
{
|
|
x = "unused: overridden by mkForce";
|
|
y = "normal y";
|
|
}
|
|
{ x = mkForce "forced x"; }
|
|
];
|
|
|
|
# mkForce in attrset format overrides same key from list format
|
|
attrList.forceMixedFormats = mkMerge [
|
|
[
|
|
{ x = mkOverride 100 (abort "overridden by mkForce; laziness guarantee: MUST NOT be evaluated"); }
|
|
{ y = "list y"; }
|
|
]
|
|
{ x = mkForce "attrset forced x"; }
|
|
];
|
|
|
|
# Nesting: list format, mkOrder on element + mkForce on value
|
|
attrList.nestListOrderForce = mkMerge [
|
|
[
|
|
{ x = mkDefault (abort "overridden by mkForce; laziness guarantee: MUST NOT be evaluated"); }
|
|
(mkOrder 500 { x = mkForce "forced-early"; })
|
|
(mkOrder 1500 { y = "late"; })
|
|
]
|
|
[
|
|
(mkOrder 100 { z = "earliest"; })
|
|
]
|
|
];
|
|
|
|
# Nesting: list format, mkOrder(mkForce(val)) on value
|
|
attrList.nestListOrderOfForce = mkMerge [
|
|
[
|
|
{ x = mkOverride 100 (abort "overridden by mkForce; laziness guarantee: MUST NOT be evaluated"); }
|
|
{ y = "plain-early"; }
|
|
]
|
|
[
|
|
{ x = mkOrder 1500 (mkForce "forced-late"); }
|
|
{ z = mkOrder 500 "earliest"; }
|
|
]
|
|
[
|
|
{ x = "unused: overridden by mkForce"; }
|
|
{ w = mkOrder 1200 "mid"; }
|
|
]
|
|
];
|
|
|
|
# Nesting: list format, mkForce(mkOrder(val)) on value
|
|
attrList.nestListForceOfOrder = mkMerge [
|
|
[
|
|
{ x = "unused: overridden by mkForce"; }
|
|
{ y = "plain-early"; }
|
|
]
|
|
[
|
|
{ x = mkForce (mkOrder 1500 "forced-late"); }
|
|
{ z = mkOrder 500 "earliest"; }
|
|
]
|
|
[
|
|
{ x = mkDefault (abort "overridden by mkForce; laziness guarantee: MUST NOT be evaluated"); }
|
|
{ w = mkOrder 1200 "mid"; }
|
|
]
|
|
];
|
|
|
|
# Nesting: attrset format, mkOrder wrapping mkForce
|
|
attrList.nestAttrsOrderOfForce = mkMerge [
|
|
{
|
|
x = mkOverride 100 (abort "overridden by mkForce; laziness guarantee: MUST NOT be evaluated");
|
|
y = "plain-early";
|
|
}
|
|
{
|
|
x = mkOrder 1500 (mkForce "forced-late");
|
|
z = mkOrder 500 "earliest";
|
|
}
|
|
{
|
|
x = "unused: overridden by mkForce";
|
|
w = mkOrder 1200 "mid";
|
|
}
|
|
];
|
|
|
|
# Nesting: attrset format, mkForce wrapping mkOrder
|
|
attrList.nestAttrsForceOfOrder = mkMerge [
|
|
{
|
|
x = "unused: overridden by mkForce";
|
|
y = "plain-early";
|
|
}
|
|
{
|
|
x = mkForce (mkOrder 1500 "forced-late");
|
|
z = mkOrder 500 "earliest";
|
|
}
|
|
{
|
|
x = mkDefault (abort "overridden by mkForce; laziness guarantee: MUST NOT be evaluated");
|
|
w = mkOrder 1200 "mid";
|
|
}
|
|
];
|
|
|
|
# mkIf false on individual element value filters it out (list format)
|
|
attrListInt.optionalValueList = [
|
|
{ a = mkIf true 1; }
|
|
{ b = mkIf false 2; }
|
|
{ c = 3; }
|
|
];
|
|
|
|
# mkIf false on individual element value filters it out (attrset format)
|
|
attrListInt.optionalValueAttrs = {
|
|
a = mkIf true 1;
|
|
b = mkIf false 2;
|
|
c = 3;
|
|
};
|
|
|
|
# submodule elemType: produces real valueMeta
|
|
attrListSubmodule = [
|
|
{
|
|
web = {
|
|
port = 80;
|
|
};
|
|
}
|
|
{
|
|
db = {
|
|
port = 5432;
|
|
host = "dbhost";
|
|
};
|
|
}
|
|
];
|
|
|
|
# asAttrs: unique keys — value is a plain attrset
|
|
asAttrs.unique = [
|
|
{ a = "alpha"; }
|
|
{ b = "beta"; }
|
|
];
|
|
|
|
# asAttrs: duplicate keys — last in order wins
|
|
asAttrs.duplicateKeys = mkMerge [
|
|
{ x = mkOrder 500 "first"; }
|
|
{ x = mkOrder 1500 "last"; }
|
|
{ y = "only"; }
|
|
];
|
|
|
|
# asAttrs: with ordering — value is attrset, ordered list in valueMeta
|
|
asAttrs.ordered = {
|
|
z = mkOrder 200 "z-val";
|
|
a = mkOrder 100 "a-val";
|
|
};
|
|
|
|
# asAttrs: with mkForce — forced key overrides
|
|
asAttrs.withForce = mkMerge [
|
|
{ x = "unused: overridden by mkForce"; }
|
|
{
|
|
x = mkForce "forced";
|
|
y = "kept";
|
|
}
|
|
];
|
|
|
|
# asAttrs: empty
|
|
asAttrs.empty = [ ];
|
|
|
|
# asAttrsDefault: unique keys
|
|
asAttrsDefault.unique = [
|
|
{ a = 1; }
|
|
{ b = 2; }
|
|
];
|
|
|
|
# asAttrsDefault: duplicate keys — default collects into lists
|
|
asAttrsDefault.duplicates = mkMerge [
|
|
{ x = mkOrder 500 10; }
|
|
{ x = mkOrder 1500 30; }
|
|
{ y = 99; }
|
|
[
|
|
{ x = 20; }
|
|
]
|
|
];
|
|
|
|
# either: attrList branch matches for list input
|
|
eitherAttrListOrInt = [
|
|
{ a = "hello"; }
|
|
{ b = "world"; }
|
|
];
|
|
|
|
# either: int input falls through to int branch
|
|
eitherAttrListOrIntFallback = 42;
|
|
|
|
# either (swapped): int first, attrList second — int input matches int
|
|
eitherIntOrAttrList = 42;
|
|
|
|
# either (swapped): list input falls through to attrList branch
|
|
eitherIntOrAttrListFallback = [
|
|
{ a = "hello"; }
|
|
];
|
|
|
|
# Bad: string where int expected
|
|
attrListInt.badValue = [
|
|
{ a = "not-an-int"; }
|
|
];
|
|
|
|
# Bad: list element with multiple keys
|
|
attrList.badListElem = [
|
|
{
|
|
a = "ok";
|
|
b = "extra";
|
|
}
|
|
];
|
|
|
|
# Bad: plain string instead of list or attrset
|
|
attrList.badString = "not-a-container";
|
|
|
|
# Bad: list element is a bare string, not a singleton attrset
|
|
attrList.badListString = [
|
|
"not a singleton attribute"
|
|
];
|
|
|
|
attrListStrict = builtins.mapAttrs (k: v: builtins.deepSeq v v) config.attrList;
|
|
attrListIntStrict = builtins.mapAttrs (k: v: builtins.deepSeq v v) config.attrListInt;
|
|
|
|
assertions =
|
|
let
|
|
c = lib.evalModules {
|
|
modules = [ ./declare-attrList.nix ];
|
|
};
|
|
cfg = c.config;
|
|
in
|
|
|
|
# List input preserves elements
|
|
assert
|
|
cfg.attrList.listInput == [
|
|
{ a = "alpha"; }
|
|
{ b = "beta"; }
|
|
];
|
|
|
|
# Attrset input with mkOrder: lower priority comes first
|
|
assert
|
|
cfg.attrList.attrsetOrdered == [
|
|
{ y = "y-val"; }
|
|
{ x = "x-val"; }
|
|
];
|
|
|
|
# Mixed input: mkOrder 50 < default 1000
|
|
assert
|
|
cfg.attrList.mixed == [
|
|
{ n = "from-attrset"; }
|
|
{ m = "from-list"; }
|
|
];
|
|
|
|
# Multiple definitions from separate modules concatenate
|
|
# (import module's definition comes before this module's)
|
|
assert
|
|
cfg.attrListInt.multiModule == [
|
|
{ b = 2; }
|
|
{ a = 1; }
|
|
];
|
|
|
|
# Attrset without mkOrder: all at default priority
|
|
assert builtins.length cfg.attrList.attrsetNoOrder == 2;
|
|
|
|
# Empty list stays empty
|
|
assert cfg.attrList.empty == [ ];
|
|
|
|
# List elements support mkOrder/mkBefore/mkAfter
|
|
assert
|
|
cfg.attrList.listOrdering == [
|
|
{ a = "before"; }
|
|
{ m = "default"; }
|
|
{ z = "after"; }
|
|
];
|
|
|
|
# Plain list entries are at default priority (1000)
|
|
assert
|
|
cfg.attrList.listDefaultPrio == [
|
|
{ before = "before"; }
|
|
{ mid = "list-entry"; }
|
|
{ after = "after"; }
|
|
];
|
|
|
|
# Ordering: 500 < 1000 (default) < 1500
|
|
assert
|
|
cfg.attrList.ordering == [
|
|
{ first = "first"; }
|
|
{ middle = "middle"; }
|
|
{ last = "last"; }
|
|
];
|
|
|
|
# mkBefore (500) < default (1000) < mkAfter (1500)
|
|
assert
|
|
cfg.attrList.beforeAfter == [
|
|
{ a = "before"; }
|
|
{ m = "default"; }
|
|
{ z = "after"; }
|
|
];
|
|
|
|
# mkIf true includes, mkIf false excludes
|
|
assert
|
|
cfg.attrList.withMkIf == [
|
|
{ yes = "included"; }
|
|
];
|
|
|
|
# mkOverride: only lowest priority override survives
|
|
assert
|
|
cfg.attrList.withOverride == [
|
|
{ winner = "wins"; }
|
|
];
|
|
|
|
# mkDefault is overridden by normal definitions
|
|
assert
|
|
cfg.attrList.withDefault == [
|
|
{ normal = "wins"; }
|
|
];
|
|
|
|
# mkForce discards other definitions
|
|
assert
|
|
cfg.attrList.withForce == [
|
|
{ forced = "wins"; }
|
|
];
|
|
|
|
# mkForce with mkOrder inside: ordering still works
|
|
assert
|
|
cfg.attrList.forceWithOrder == [
|
|
{ first = "before"; }
|
|
{ second = "after"; }
|
|
];
|
|
|
|
# mkForce on individual element values passes through
|
|
assert
|
|
cfg.attrListInt.forceElementValue == [
|
|
{ a = 42; }
|
|
{ b = 2; }
|
|
];
|
|
|
|
# mkForce on attrset format: discards other defs, ordering preserved
|
|
assert
|
|
cfg.attrList.forceAttrset == [
|
|
{ y = "y-val"; }
|
|
{ x = "x-val"; }
|
|
];
|
|
|
|
# mkForce on repeated key: forced entries override non-forced
|
|
assert
|
|
cfg.attrList.forceRepeatedKey == [
|
|
{ x = "wins"; }
|
|
{ x = "wins 2"; }
|
|
];
|
|
|
|
# mkForce on repeated key across mkMerge: forced wins
|
|
assert
|
|
cfg.attrList.forceRepeatedKeyMerge == [
|
|
{ x = "forced"; }
|
|
];
|
|
|
|
# mkForce on repeated key in attrset format: discards other x, keeps y
|
|
assert
|
|
cfg.attrList.forceRepeatedKeyAttrs == [
|
|
{ y = "kept"; }
|
|
{ x = "forced"; }
|
|
];
|
|
|
|
# mkForce only affects its own key
|
|
assert
|
|
cfg.attrList.forcePartialAttrs == [
|
|
{ y = "normal y"; }
|
|
{ x = "forced x"; }
|
|
];
|
|
|
|
# mkForce in attrset format overrides same key from list format
|
|
assert
|
|
cfg.attrList.forceMixedFormats == [
|
|
{ y = "list y"; }
|
|
{ x = "attrset forced x"; }
|
|
];
|
|
|
|
# Nesting: list format, mkOrder on element + mkForce on value
|
|
# z(100) < x-forced(500) < y(1500); x-discarded filtered by mkForce
|
|
assert
|
|
cfg.attrList.nestListOrderForce == [
|
|
{ z = "earliest"; }
|
|
{ x = "forced-early"; }
|
|
{ y = "late"; }
|
|
];
|
|
|
|
# Nesting: list format, mkOrder(mkForce(val)) on value
|
|
# z(500) < y(1000) < w(1200) < x-forced(1500); x-discarded entries filtered
|
|
assert
|
|
cfg.attrList.nestListOrderOfForce == [
|
|
{ z = "earliest"; }
|
|
{ y = "plain-early"; }
|
|
{ w = "mid"; }
|
|
{ x = "forced-late"; }
|
|
];
|
|
|
|
# Nesting: list format, mkForce(mkOrder(val)) on value
|
|
# z(500) < y(1000) < w(1200) < x-forced(1500); x-discarded entries filtered
|
|
assert
|
|
cfg.attrList.nestListForceOfOrder == [
|
|
{ z = "earliest"; }
|
|
{ y = "plain-early"; }
|
|
{ w = "mid"; }
|
|
{ x = "forced-late"; }
|
|
];
|
|
|
|
# Nesting: attrset format, mkOrder(mkForce(val))
|
|
# z(500) < y(1000) < w(1200) < x-forced(1500); x-discarded entries filtered
|
|
assert
|
|
cfg.attrList.nestAttrsOrderOfForce == [
|
|
{ z = "earliest"; }
|
|
{ y = "plain-early"; }
|
|
{ w = "mid"; }
|
|
{ x = "forced-late"; }
|
|
];
|
|
|
|
# Nesting: attrset format, mkForce(mkOrder(val))
|
|
# z(500) < y(1000) < w(1200) < x-forced(1500); x-discarded entries filtered
|
|
assert
|
|
cfg.attrList.nestAttrsForceOfOrder == [
|
|
{ z = "earliest"; }
|
|
{ y = "plain-early"; }
|
|
{ w = "mid"; }
|
|
{ x = "forced-late"; }
|
|
];
|
|
|
|
# mkIf false on individual element value filters it out (list format)
|
|
assert
|
|
cfg.attrListInt.optionalValueList == [
|
|
{ a = 1; }
|
|
{ c = 3; }
|
|
];
|
|
|
|
# mkIf false on individual element value filters it out (attrset format)
|
|
assert
|
|
cfg.attrListInt.optionalValueAttrs == [
|
|
{ a = 1; }
|
|
{ c = 3; }
|
|
];
|
|
|
|
# submodule: value, option descriptions, and valueMeta with real configuration metadata
|
|
assert
|
|
cfg.attrListSubmodule == [
|
|
{
|
|
web = {
|
|
host = "localhost";
|
|
port = 80;
|
|
};
|
|
}
|
|
{
|
|
db = {
|
|
host = "dbhost";
|
|
port = 5432;
|
|
};
|
|
}
|
|
];
|
|
assert
|
|
builtins.map (m: m.configuration.config) c.options.attrListSubmodule.valueMeta.attrList == [
|
|
{
|
|
host = "localhost";
|
|
port = 80;
|
|
}
|
|
{
|
|
host = "dbhost";
|
|
port = 5432;
|
|
}
|
|
];
|
|
assert
|
|
builtins.map (
|
|
m:
|
|
builtins.mapAttrs (n: o: o.description) (builtins.removeAttrs m.configuration.options [ "_module" ])
|
|
) c.options.attrListSubmodule.valueMeta.attrList == [
|
|
{
|
|
host = "Hostname";
|
|
port = "Port number";
|
|
}
|
|
{
|
|
host = "Hostname";
|
|
port = "Port number";
|
|
}
|
|
];
|
|
|
|
# valueMeta.attrList has one entry per (non-filtered) element
|
|
assert
|
|
c.options.attrList.valueMeta.attrs.listInput.attrList == [
|
|
{ }
|
|
{ }
|
|
];
|
|
assert
|
|
c.options.attrList.valueMeta.attrs.attrsetOrdered.attrList == [
|
|
{ }
|
|
{ }
|
|
];
|
|
assert
|
|
c.options.attrList.valueMeta.attrs.mixed.attrList == [
|
|
{ }
|
|
{ }
|
|
];
|
|
assert c.options.attrList.valueMeta.attrs.empty.attrList == [ ];
|
|
assert
|
|
c.options.attrListInt.valueMeta.attrs.optionalValueList.attrList == [
|
|
{ }
|
|
{ }
|
|
];
|
|
|
|
# either: headError is null for valid attrList input, so attrList branch is picked
|
|
assert
|
|
cfg.eitherAttrListOrInt == [
|
|
{ a = "hello"; }
|
|
{ b = "world"; }
|
|
];
|
|
|
|
# either: headError is non-null for int input, so int branch is picked
|
|
assert cfg.eitherAttrListOrIntFallback == 42;
|
|
|
|
# either (swapped): int first — int input matches
|
|
assert cfg.eitherIntOrAttrList == 42;
|
|
|
|
# either (swapped): list input falls through to attrList branch
|
|
assert
|
|
cfg.eitherIntOrAttrListFallback == [
|
|
{ a = "hello"; }
|
|
];
|
|
|
|
# asAttrs: unique keys — value is a plain attrset
|
|
assert
|
|
cfg.asAttrs.unique == {
|
|
a = "alpha";
|
|
b = "beta";
|
|
};
|
|
# ordered list preserved in valueMeta
|
|
assert
|
|
c.options.asAttrs.valueMeta.attrs.unique.attrListValue == [
|
|
{ a = "alpha"; }
|
|
{ b = "beta"; }
|
|
];
|
|
|
|
# asAttrs: duplicate keys — last in order wins
|
|
assert
|
|
cfg.asAttrs.duplicateKeys == {
|
|
x = "last";
|
|
y = "only";
|
|
};
|
|
assert
|
|
c.options.asAttrs.valueMeta.attrs.duplicateKeys.attrListValue == [
|
|
{ x = "first"; }
|
|
{ y = "only"; }
|
|
{ x = "last"; }
|
|
];
|
|
|
|
# asAttrs: ordered — value is attrset (unordered), list in valueMeta preserves order
|
|
assert
|
|
cfg.asAttrs.ordered == {
|
|
a = "a-val";
|
|
z = "z-val";
|
|
};
|
|
assert
|
|
c.options.asAttrs.valueMeta.attrs.ordered.attrListValue == [
|
|
{ a = "a-val"; }
|
|
{ z = "z-val"; }
|
|
];
|
|
|
|
# asAttrs: mkForce — forced key overrides, value is attrset
|
|
assert
|
|
cfg.asAttrs.withForce == {
|
|
x = "forced";
|
|
y = "kept";
|
|
};
|
|
|
|
# asAttrs: empty — value is empty attrset
|
|
assert cfg.asAttrs.empty == { };
|
|
|
|
# asAttrsDefault: unique keys — each value wrapped in singleton list
|
|
assert
|
|
cfg.asAttrsDefault.unique == {
|
|
a = [ 1 ];
|
|
b = [ 2 ];
|
|
};
|
|
|
|
# asAttrsDefault: duplicate keys — values collected into list in order
|
|
assert
|
|
cfg.asAttrsDefault.duplicates == {
|
|
x = [
|
|
10
|
|
20
|
|
30
|
|
];
|
|
y = [ 99 ];
|
|
};
|
|
assert
|
|
c.options.asAttrsDefault.valueMeta.attrs.duplicates.attrListValue == [
|
|
{ x = 10; }
|
|
{ y = 99; }
|
|
{ x = 20; }
|
|
{ x = 30; }
|
|
];
|
|
|
|
# valueMeta.definitions: mkDefinition records with mkOrder-wrapped single-key attrsets
|
|
# Use duplicateKeys which has mixed priorities and repeated keys
|
|
assert
|
|
let
|
|
defs = c.options.asAttrs.valueMeta.attrs.duplicateKeys.definitions;
|
|
extract = d: {
|
|
prio = d.value.priority;
|
|
value = d.value.content;
|
|
};
|
|
in
|
|
map extract defs == [
|
|
{
|
|
prio = 500;
|
|
value = {
|
|
x = "first";
|
|
};
|
|
}
|
|
{
|
|
prio = 1000;
|
|
value = {
|
|
y = "only";
|
|
};
|
|
}
|
|
{
|
|
prio = 1500;
|
|
value = {
|
|
x = "last";
|
|
};
|
|
}
|
|
];
|
|
|
|
# Round-trip: feed definitions through mapDefinitionValue + mkMerge into a listOf option
|
|
assert
|
|
let
|
|
rendered = lib.modules.mapDefinitionValue (attr: lib.cli.toCommandLineGNU { } attr) (
|
|
mkMerge c.options.asAttrs.valueMeta.attrs.duplicateKeys.definitions
|
|
);
|
|
result =
|
|
(lib.evalModules {
|
|
modules = [
|
|
{ options.out = mkOption { type = types.listOf types.str; }; }
|
|
{ config.out = rendered; }
|
|
# Interleave: mkOrder 800 lands between x(500) and y(1000)
|
|
{ config.out = mkOrder 800 [ "--interleaved" ]; }
|
|
];
|
|
}).config.out;
|
|
in
|
|
result == [
|
|
"-xfirst"
|
|
"--interleaved"
|
|
"-yonly"
|
|
"-xlast"
|
|
];
|
|
|
|
# Error cases are tested via checkConfigError in modules.sh
|
|
|
|
"ok";
|
|
};
|
|
}
|