{ pkgs }: let inherit (pkgs) lib formats; # merging allows us to add metadata to the input # this makes error messages more readable during development mergeInput = name: format: input: format.type.merge [] [ { # explicitly throw here to trigger the code path that prints the error message for users value = lib.throwIfNot (format.type.check input) (builtins.trace input "definition does not pass the type's check function") input; # inject the name file = "format-test-${name}"; } ]; # run a diff between expected and real output runDiff = name: drv: expected: pkgs.runCommand name { passAsFile = ["expected"]; inherit expected drv; } '' if diff -u "$expectedPath" "$drv"; then touch "$out" else echo echo "Got different values than expected; diff above." exit 1 fi ''; # use this to check for proper serialization # in practice you do not have to supply the name parameter as this one will be added by runBuildTests shouldPass = { format, input, expected }: name: { name = "pass-${name}"; path = runDiff "test-format-${name}" (format.generate "test-format-${name}" (mergeInput name format input)) expected; }; # use this function to assert that a type check must fail # in practice you do not have to supply the name parameter as this one will be added by runBuildTests # note that as per 352e7d330a26 and 352e7d330a26 the type checking of attrsets and lists are not strict # this means that the code below needs to properly merge the module type definition and also evaluate the (lazy) return value shouldFail = { format, input }: name: let # trigger a deep type check using the module system typeCheck = lib.modules.mergeDefinitions [ "tests" name ] format.type [ { file = "format-test-${name}"; value = input; } ]; # actually use the return value to trigger the evaluation eval = builtins.tryEval (typeCheck.mergedValue == input); # the check failing is what we want, so don't do anything here typeFails = pkgs.runCommand "test-format-${name}" {} "touch $out"; # bail with some verbose information in case the type check passes typeSucceeds = pkgs.runCommand "test-format-${name}" { passAsFile = [ "inputText" ]; testName = name; # this will fail if the input contains functions as values # however that should get caught by the type check already inputText = builtins.toJSON input; } '' echo "Type check $testName passed when it shouldn't." echo "The following data was used as input:" echo cat "$inputTextPath" exit 1 ''; in { name = "fail-${name}"; path = if eval.success then typeSucceeds else typeFails; }; # this function creates a linkFarm for all the tests below such that the results are easily visible in the filesystem after a build # the parameters are an attrset of name: test pairs where the name is automatically passed to the test # the test therefore is an invocation of ShouldPass or shouldFail with the attrset parameters but *not* the name (which this adds for convenience) runBuildTests = (lib.flip lib.pipe) [ (lib.mapAttrsToList (name: value: value name)) (pkgs.linkFarm "nixpkgs-pkgs-lib-format-tests") ]; in runBuildTests { jsonAtoms = shouldPass { format = formats.json {}; input = { null = null; false = false; true = true; int = 10; float = 3.141; str = "foo"; attrs.foo = null; list = [ null null ]; path = ./formats.nix; }; expected = '' { "attrs": { "foo": null }, "false": false, "float": 3.141, "int": 10, "list": [ null, null ], "null": null, "path": "${./formats.nix}", "str": "foo", "true": true } ''; }; yamlAtoms = shouldPass { format = formats.yaml {}; input = { null = null; false = false; true = true; float = 3.141; str = "foo"; attrs.foo = null; list = [ null null ]; path = ./formats.nix; }; expected = '' attrs: foo: null 'false': false float: 3.141 list: - null - null 'null': null path: ${./formats.nix} str: foo 'true': true ''; }; iniAtoms = shouldPass { format = formats.ini {}; input = { foo = { bool = true; int = 10; float = 3.141; str = "string"; }; }; expected = '' [foo] bool=true float=3.141000 int=10 str=string ''; }; iniInvalidAtom = shouldFail { format = formats.ini {}; input = { foo = { function = _: 1; }; }; }; iniDuplicateKeysWithoutList = shouldFail { format = formats.ini {}; input = { foo = { bar = [ null true "test" 1.2 10 ]; baz = false; qux = "qux"; }; }; }; iniDuplicateKeys = shouldPass { format = formats.ini { listsAsDuplicateKeys = true; }; input = { foo = { bar = [ null true "test" 1.2 10 ]; baz = false; qux = "qux"; }; }; expected = '' [foo] bar=null bar=true bar=test bar=1.200000 bar=10 baz=false qux=qux ''; }; iniListToValue = shouldPass { format = formats.ini { listToValue = lib.concatMapStringsSep ", " (lib.generators.mkValueStringDefault {}); }; input = { foo = { bar = [ null true "test" 1.2 10 ]; baz = false; qux = "qux"; }; }; expected = '' [foo] bar=null, true, test, 1.200000, 10 baz=false qux=qux ''; }; iniWithGlobalNoSections = shouldPass { format = formats.iniWithGlobalSection {}; input = {}; expected = ""; }; iniWithGlobalOnlySections = shouldPass { format = formats.iniWithGlobalSection {}; input = { sections = { foo = { bar = "baz"; }; }; }; expected = '' [foo] bar=baz ''; }; iniWithGlobalOnlyGlobal = shouldPass { format = formats.iniWithGlobalSection {}; input = { globalSection = { bar = "baz"; }; }; expected = '' bar=baz ''; }; iniWithGlobalWrongSections = shouldFail { format = formats.iniWithGlobalSection {}; input = { foo = {}; }; }; iniWithGlobalEverything = shouldPass { format = formats.iniWithGlobalSection {}; input = { globalSection = { bar = true; }; sections = { foo = { bool = true; int = 10; float = 3.141; str = "string"; }; }; }; expected = '' bar=true [foo] bool=true float=3.141000 int=10 str=string ''; }; iniWithGlobalListToValue = shouldPass { format = formats.iniWithGlobalSection { listToValue = lib.concatMapStringsSep ", " (lib.generators.mkValueStringDefault {}); }; input = { globalSection = { bar = [ null true "test" 1.2 10 ]; baz = false; qux = "qux"; }; sections = { foo = { bar = [ null true "test" 1.2 10 ]; baz = false; qux = "qux"; }; }; }; expected = '' bar=null, true, test, 1.200000, 10 baz=false qux=qux [foo] bar=null, true, test, 1.200000, 10 baz=false qux=qux ''; }; keyValueAtoms = shouldPass { format = formats.keyValue {}; input = { bool = true; int = 10; float = 3.141; str = "string"; }; expected = '' bool=true float=3.141000 int=10 str=string ''; }; keyValueDuplicateKeys = shouldPass { format = formats.keyValue { listsAsDuplicateKeys = true; }; input = { bar = [ null true "test" 1.2 10 ]; baz = false; qux = "qux"; }; expected = '' bar=null bar=true bar=test bar=1.200000 bar=10 baz=false qux=qux ''; }; keyValueListToValue = shouldPass { format = formats.keyValue { listToValue = lib.concatMapStringsSep ", " (lib.generators.mkValueStringDefault {}); }; input = { bar = [ null true "test" 1.2 10 ]; baz = false; qux = "qux"; }; expected = '' bar=null, true, test, 1.200000, 10 baz=false qux=qux ''; }; tomlAtoms = shouldPass { format = formats.toml {}; input = { false = false; true = true; int = 10; float = 3.141; str = "foo"; attrs.foo = "foo"; list = [ 1 2 ]; level1.level2.level3.level4 = "deep"; }; expected = '' false = false float = 3.141 int = 10 list = [1, 2] str = "foo" true = true [attrs] foo = "foo" [level1.level2.level3] level4 = "deep" ''; }; # This test is responsible for # 1. testing type coercions # 2. providing a more readable example test # Whereas java-properties/default.nix tests the low level escaping, etc. javaProperties = shouldPass { format = formats.javaProperties {}; input = { floaty = 3.1415; tautologies = true; contradictions = false; foo = "bar"; # # Disallowed at eval time, because it's ambiguous: # # add to store or convert to string? # root = /root; "1" = 2; package = pkgs.hello; "ütf 8" = "dûh"; # NB: Some editors (vscode) show this _whole_ line in right-to-left order "الجبر" = "أكثر من مجرد أرقام"; }; expected = '' # Generated with Nix 1 = 2 contradictions = false floaty = 3.141500 foo = bar package = ${pkgs.hello} tautologies = true \u00fctf\ 8 = d\u00fbh \u0627\u0644\u062c\u0628\u0631 = \u0623\u0643\u062b\u0631 \u0645\u0646 \u0645\u062c\u0631\u062f \u0623\u0631\u0642\u0627\u0645 ''; }; phpAtoms = shouldPass rec { format = formats.php { finalVariable = "config"; }; input = { null = null; false = false; true = true; int = 10; float = 3.141; str = "foo"; str_special = "foo\ntesthello'''"; attrs.foo = null; list = [ null null ]; mixed = format.lib.mkMixedArray [ 10 3.141 ] { str = "foo"; attrs.foo = null; }; raw = format.lib.mkRaw "random_function()"; }; expected = '' ['foo' => null], 'false' => false, 'float' => 3.141000, 'int' => 10, 'list' => [null, null], 'mixed' => [10, 3.141000, 'attrs' => ['foo' => null], 'str' => 'foo'], 'null' => null, 'raw' => random_function(), 'str' => 'foo', 'str_special' => 'foo testhello\'\'\'${"'"}, 'true' => true]; ''; }; phpReturn = shouldPass { format = formats.php { }; input = { int = 10; float = 3.141; str = "foo"; str_special = "foo\ntesthello'''"; attrs.foo = null; }; expected = '' ['foo' => null], 'float' => 3.141000, 'int' => 10, 'str' => 'foo', 'str_special' => 'foo testhello\'\'\'${"'"}]; ''; }; }