Clone of https://github.com/NixOS/nixpkgs.git (to stress-test knotserver)
1{ pkgs }:
2let
3 inherit (pkgs) lib formats;
4
5 # merging allows us to add metadata to the input
6 # this makes error messages more readable during development
7 mergeInput =
8 name: format: input:
9 format.type.merge
10 [ ]
11 [
12 {
13 # explicitly throw here to trigger the code path that prints the error message for users
14 value =
15 lib.throwIfNot (format.type.check input)
16 (builtins.trace input "definition does not pass the type's check function")
17 input;
18 # inject the name
19 file = "format-test-${name}";
20 }
21 ];
22
23 # run a diff between expected and real output
24 runDiff =
25 name: drv: expected:
26 pkgs.runCommand name
27 {
28 passAsFile = [ "expected" ];
29 inherit expected drv;
30 }
31 ''
32 if diff -u "$expectedPath" "$drv"; then
33 touch "$out"
34 else
35 echo
36 echo "Got different values than expected; diff above."
37 exit 1
38 fi
39 '';
40
41 # use this to check for proper serialization
42 # in practice you do not have to supply the name parameter as this one will be added by runBuildTests
43 shouldPass =
44 {
45 format,
46 input,
47 expected,
48 }:
49 name: {
50 name = "pass-${name}";
51 path = runDiff "test-format-${name}" (format.generate "test-format-${name}" (
52 mergeInput name format input
53 )) expected;
54 };
55
56 # use this function to assert that a type check must fail
57 # in practice you do not have to supply the name parameter as this one will be added by runBuildTests
58 # note that as per 352e7d330a26 and 352e7d330a26 the type checking of attrsets and lists are not strict
59 # this means that the code below needs to properly merge the module type definition and also evaluate the (lazy) return value
60 shouldFail =
61 { format, input }:
62 name:
63 let
64 # trigger a deep type check using the module system
65 typeCheck = lib.modules.mergeDefinitions [ "tests" name ] format.type [
66 {
67 file = "format-test-${name}";
68 value = input;
69 }
70 ];
71 # actually use the return value to trigger the evaluation
72 eval = builtins.tryEval (typeCheck.mergedValue == input);
73 # the check failing is what we want, so don't do anything here
74 typeFails = pkgs.runCommand "test-format-${name}" { } "touch $out";
75 # bail with some verbose information in case the type check passes
76 typeSucceeds =
77 pkgs.runCommand "test-format-${name}"
78 {
79 passAsFile = [ "inputText" ];
80 testName = name;
81 # this will fail if the input contains functions as values
82 # however that should get caught by the type check already
83 inputText = builtins.toJSON input;
84 }
85 ''
86 echo "Type check $testName passed when it shouldn't."
87 echo "The following data was used as input:"
88 echo
89 cat "$inputTextPath"
90 exit 1
91 '';
92 in
93 {
94 name = "fail-${name}";
95 path = if eval.success then typeSucceeds else typeFails;
96 };
97
98 # this function creates a linkFarm for all the tests below such that the results are easily visible in the filesystem after a build
99 # the parameters are an attrset of name: test pairs where the name is automatically passed to the test
100 # the test therefore is an invocation of ShouldPass or shouldFail with the attrset parameters but *not* the name (which this adds for convenience)
101 runBuildTests = (lib.flip lib.pipe) [
102 (lib.mapAttrsToList (name: value: value name))
103 (pkgs.linkFarm "nixpkgs-pkgs-lib-format-tests")
104 ];
105
106in
107runBuildTests {
108
109 jsonAtoms = shouldPass {
110 format = formats.json { };
111 input = {
112 null = null;
113 false = false;
114 true = true;
115 int = 10;
116 float = 3.141;
117 str = "foo";
118 attrs.foo = null;
119 list = [
120 null
121 null
122 ];
123 path = ./testfile;
124 };
125 expected = ''
126 {
127 "attrs": {
128 "foo": null
129 },
130 "false": false,
131 "float": 3.141,
132 "int": 10,
133 "list": [
134 null,
135 null
136 ],
137 "null": null,
138 "path": "${./testfile}",
139 "str": "foo",
140 "true": true
141 }
142 '';
143 };
144
145 yaml_1_1Atoms = shouldPass {
146 format = formats.yaml_1_1 { };
147 input = {
148 null = null;
149 false = false;
150 true = true;
151 float = 3.141;
152 str = "foo";
153 attrs.foo = null;
154 list = [
155 null
156 null
157 ];
158 path = ./testfile;
159 no = "no";
160 time = "22:30:00";
161 };
162 expected = ''
163 attrs:
164 foo: null
165 'false': false
166 float: 3.141
167 list:
168 - null
169 - null
170 'no': 'no'
171 'null': null
172 path: ${./testfile}
173 str: foo
174 time: '22:30:00'
175 'true': true
176 '';
177 };
178
179 yaml_1_2Atoms = shouldPass {
180 format = formats.yaml_1_2 { };
181 input = {
182 null = null;
183 false = false;
184 true = true;
185 float = 3.141;
186 str = "foo";
187 attrs.foo = null;
188 list = [
189 null
190 null
191 ];
192 path = ./testfile;
193 no = "no";
194 time = "22:30:00";
195 };
196 expected = ''
197 attrs:
198 foo: null
199 'false': false
200 float: 3.141
201 list:
202 - null
203 - null
204 no: no
205 'null': null
206 path: ${./testfile}
207 str: foo
208 time: 22:30:00
209 'true': true
210 '';
211 };
212
213 iniAtoms = shouldPass {
214 format = formats.ini { };
215 input = {
216 foo = {
217 bool = true;
218 int = 10;
219 float = 3.141;
220 str = "string";
221 };
222 };
223 expected = ''
224 [foo]
225 bool=true
226 float=3.141000
227 int=10
228 str=string
229 '';
230 };
231
232 iniInvalidAtom = shouldFail {
233 format = formats.ini { };
234 input = {
235 foo = {
236 function = _: 1;
237 };
238 };
239 };
240
241 iniDuplicateKeysWithoutList = shouldFail {
242 format = formats.ini { };
243 input = {
244 foo = {
245 bar = [
246 null
247 true
248 "test"
249 1.2
250 10
251 ];
252 baz = false;
253 qux = "qux";
254 };
255 };
256 };
257
258 iniDuplicateKeys = shouldPass {
259 format = formats.ini { listsAsDuplicateKeys = true; };
260 input = {
261 foo = {
262 bar = [
263 null
264 true
265 "test"
266 1.2
267 10
268 ];
269 baz = false;
270 qux = "qux";
271 };
272 };
273 expected = ''
274 [foo]
275 bar=null
276 bar=true
277 bar=test
278 bar=1.200000
279 bar=10
280 baz=false
281 qux=qux
282 '';
283 };
284
285 iniListToValue = shouldPass {
286 format = formats.ini {
287 listToValue = lib.concatMapStringsSep ", " (lib.generators.mkValueStringDefault { });
288 };
289 input = {
290 foo = {
291 bar = [
292 null
293 true
294 "test"
295 1.2
296 10
297 ];
298 baz = false;
299 qux = "qux";
300 };
301 };
302 expected = ''
303 [foo]
304 bar=null, true, test, 1.200000, 10
305 baz=false
306 qux=qux
307 '';
308 };
309
310 iniCoercedDuplicateKeys = shouldPass rec {
311 format = formats.ini {
312 listsAsDuplicateKeys = true;
313 atomsCoercedToLists = true;
314 };
315 input =
316 format.type.merge
317 [ ]
318 [
319 {
320 file = "format-test-inner-iniCoercedDuplicateKeys";
321 value = {
322 foo = {
323 bar = 1;
324 };
325 };
326 }
327 {
328 file = "format-test-inner-iniCoercedDuplicateKeys";
329 value = {
330 foo = {
331 bar = 2;
332 };
333 };
334 }
335 ];
336 expected = ''
337 [foo]
338 bar=1
339 bar=2
340 '';
341 };
342
343 iniCoercedListToValue = shouldPass rec {
344 format = formats.ini {
345 listToValue = lib.concatMapStringsSep ", " (lib.generators.mkValueStringDefault { });
346 atomsCoercedToLists = true;
347 };
348 input =
349 format.type.merge
350 [ ]
351 [
352 {
353 file = "format-test-inner-iniCoercedListToValue";
354 value = {
355 foo = {
356 bar = 1;
357 };
358 };
359 }
360 {
361 file = "format-test-inner-iniCoercedListToValue";
362 value = {
363 foo = {
364 bar = 2;
365 };
366 };
367 }
368 ];
369 expected = ''
370 [foo]
371 bar=1, 2
372 '';
373 };
374
375 iniCoercedNoLists = shouldFail {
376 format = formats.ini { atomsCoercedToLists = true; };
377 input = {
378 foo = {
379 bar = 1;
380 };
381 };
382 };
383
384 iniNoCoercedNoLists = shouldFail {
385 format = formats.ini { atomsCoercedToLists = false; };
386 input = {
387 foo = {
388 bar = 1;
389 };
390 };
391 };
392
393 iniWithGlobalNoSections = shouldPass {
394 format = formats.iniWithGlobalSection { };
395 input = { };
396 expected = "";
397 };
398
399 iniWithGlobalOnlySections = shouldPass {
400 format = formats.iniWithGlobalSection { };
401 input = {
402 sections = {
403 foo = {
404 bar = "baz";
405 };
406 };
407 };
408 expected = ''
409 [foo]
410 bar=baz
411 '';
412 };
413
414 iniWithGlobalOnlyGlobal = shouldPass {
415 format = formats.iniWithGlobalSection { };
416 input = {
417 globalSection = {
418 bar = "baz";
419 };
420 };
421 expected = ''
422 bar=baz
423
424 '';
425 };
426
427 iniWithGlobalWrongSections = shouldFail {
428 format = formats.iniWithGlobalSection { };
429 input = {
430 foo = { };
431 };
432 };
433
434 iniWithGlobalEverything = shouldPass {
435 format = formats.iniWithGlobalSection { };
436 input = {
437 globalSection = {
438 bar = true;
439 };
440 sections = {
441 foo = {
442 bool = true;
443 int = 10;
444 float = 3.141;
445 str = "string";
446 };
447 };
448 };
449 expected = ''
450 bar=true
451
452 [foo]
453 bool=true
454 float=3.141000
455 int=10
456 str=string
457 '';
458 };
459
460 iniWithGlobalListToValue = shouldPass {
461 format = formats.iniWithGlobalSection {
462 listToValue = lib.concatMapStringsSep ", " (lib.generators.mkValueStringDefault { });
463 };
464 input = {
465 globalSection = {
466 bar = [
467 null
468 true
469 "test"
470 1.2
471 10
472 ];
473 baz = false;
474 qux = "qux";
475 };
476 sections = {
477 foo = {
478 bar = [
479 null
480 true
481 "test"
482 1.2
483 10
484 ];
485 baz = false;
486 qux = "qux";
487 };
488 };
489 };
490 expected = ''
491 bar=null, true, test, 1.200000, 10
492 baz=false
493 qux=qux
494
495 [foo]
496 bar=null, true, test, 1.200000, 10
497 baz=false
498 qux=qux
499 '';
500 };
501
502 iniWithGlobalCoercedDuplicateKeys = shouldPass rec {
503 format = formats.iniWithGlobalSection {
504 listsAsDuplicateKeys = true;
505 atomsCoercedToLists = true;
506 };
507 input =
508 format.type.merge
509 [ ]
510 [
511 {
512 file = "format-test-inner-iniWithGlobalCoercedDuplicateKeys";
513 value = {
514 globalSection = {
515 baz = 4;
516 };
517 sections = {
518 foo = {
519 bar = 1;
520 };
521 };
522 };
523 }
524 {
525 file = "format-test-inner-iniWithGlobalCoercedDuplicateKeys";
526 value = {
527 globalSection = {
528 baz = 3;
529 };
530 sections = {
531 foo = {
532 bar = 2;
533 };
534 };
535 };
536 }
537 ];
538 expected = ''
539 baz=3
540 baz=4
541
542 [foo]
543 bar=2
544 bar=1
545 '';
546 };
547
548 iniWithGlobalCoercedListToValue = shouldPass rec {
549 format = formats.iniWithGlobalSection {
550 listToValue = lib.concatMapStringsSep ", " (lib.generators.mkValueStringDefault { });
551 atomsCoercedToLists = true;
552 };
553 input =
554 format.type.merge
555 [ ]
556 [
557 {
558 file = "format-test-inner-iniWithGlobalCoercedListToValue";
559 value = {
560 globalSection = {
561 baz = 4;
562 };
563 sections = {
564 foo = {
565 bar = 1;
566 };
567 };
568 };
569 }
570 {
571 file = "format-test-inner-iniWithGlobalCoercedListToValue";
572 value = {
573 globalSection = {
574 baz = 3;
575 };
576 sections = {
577 foo = {
578 bar = 2;
579 };
580 };
581 };
582 }
583 ];
584 expected = ''
585 baz=3, 4
586
587 [foo]
588 bar=2, 1
589 '';
590 };
591
592 iniWithGlobalCoercedNoLists = shouldFail {
593 format = formats.iniWithGlobalSection { atomsCoercedToLists = true; };
594 input = {
595 globalSection = {
596 baz = 4;
597 };
598 foo = {
599 bar = 1;
600 };
601 };
602 };
603
604 iniWithGlobalNoCoercedNoLists = shouldFail {
605 format = formats.iniWithGlobalSection { atomsCoercedToLists = false; };
606 input = {
607 globalSection = {
608 baz = 4;
609 };
610 foo = {
611 bar = 1;
612 };
613 };
614 };
615
616 keyValueAtoms = shouldPass {
617 format = formats.keyValue { };
618 input = {
619 bool = true;
620 int = 10;
621 float = 3.141;
622 str = "string";
623 };
624 expected = ''
625 bool=true
626 float=3.141000
627 int=10
628 str=string
629 '';
630 };
631
632 keyValueDuplicateKeys = shouldPass {
633 format = formats.keyValue { listsAsDuplicateKeys = true; };
634 input = {
635 bar = [
636 null
637 true
638 "test"
639 1.2
640 10
641 ];
642 baz = false;
643 qux = "qux";
644 };
645 expected = ''
646 bar=null
647 bar=true
648 bar=test
649 bar=1.200000
650 bar=10
651 baz=false
652 qux=qux
653 '';
654 };
655
656 keyValueListToValue = shouldPass {
657 format = formats.keyValue {
658 listToValue = lib.concatMapStringsSep ", " (lib.generators.mkValueStringDefault { });
659 };
660 input = {
661 bar = [
662 null
663 true
664 "test"
665 1.2
666 10
667 ];
668 baz = false;
669 qux = "qux";
670 };
671 expected = ''
672 bar=null, true, test, 1.200000, 10
673 baz=false
674 qux=qux
675 '';
676 };
677
678 tomlAtoms = shouldPass {
679 format = formats.toml { };
680 input = {
681 false = false;
682 true = true;
683 int = 10;
684 float = 3.141;
685 str = "foo";
686 attrs.foo = "foo";
687 list = [
688 1
689 2
690 ];
691 level1.level2.level3.level4 = "deep";
692 };
693 expected = ''
694 false = false
695 float = 3.141
696 int = 10
697 list = [1, 2]
698 str = "foo"
699 true = true
700
701 [attrs]
702 foo = "foo"
703
704 [level1.level2.level3]
705 level4 = "deep"
706 '';
707 };
708
709 cdnAtoms = shouldPass {
710 format = formats.cdn { };
711 input = {
712 null = null;
713 false = false;
714 true = true;
715 int = 10;
716 float = 3.141;
717 str = "foo";
718 attrs.foo = null;
719 list = [
720 1
721 null
722 ];
723 path = ./testfile;
724 };
725 expected = ''
726 attrs {
727 "foo": null
728 }
729 "false": false
730 "float": 3.141
731 "int": 10
732 list [
733 1,
734 null
735 ]
736 "null": null
737 "path": "${./testfile}"
738 "str": "foo"
739 "true": true
740 '';
741 };
742
743 # This test is responsible for
744 # 1. testing type coercions
745 # 2. providing a more readable example test
746 # Whereas java-properties/default.nix tests the low level escaping, etc.
747 javaProperties = shouldPass {
748 format = formats.javaProperties { };
749 input = {
750 floaty = 3.1415;
751 tautologies = true;
752 contradictions = false;
753 foo = "bar";
754 # # Disallowed at eval time, because it's ambiguous:
755 # # add to store or convert to string?
756 # root = /root;
757 "1" = 2;
758 package = pkgs.hello;
759 "ütf 8" = "dûh";
760 # NB: Some editors (vscode) show this _whole_ line in right-to-left order
761 "الجبر" = "أكثر من مجرد أرقام";
762 };
763 expected = ''
764 # Generated with Nix
765
766 1 = 2
767 contradictions = false
768 floaty = 3.141500
769 foo = bar
770 package = ${pkgs.hello}
771 tautologies = true
772 \u00fctf\ 8 = d\u00fbh
773 \u0627\u0644\u062c\u0628\u0631 = \u0623\u0643\u062b\u0631 \u0645\u0646 \u0645\u062c\u0631\u062f \u0623\u0631\u0642\u0627\u0645
774 '';
775 };
776
777 luaTable = shouldPass {
778 format = formats.lua { };
779 input = {
780 null = null;
781 false = false;
782 true = true;
783 int = 10;
784 float = 3.141;
785 str = "foo";
786 attrs.foo = null;
787 list = [
788 null
789 null
790 ];
791 path = ./testfile;
792 inline = lib.mkLuaInline "hello('world')";
793 };
794 expected = ''
795 return {
796 ["attrs"] = {
797 ["foo"] = nil,
798 },
799 ["false"] = false,
800 ["float"] = 3.141,
801 ["inline"] = (hello("world")),
802 ["int"] = 10,
803 ["list"] = {
804 nil,
805 nil,
806 },
807 ["null"] = nil,
808 ["path"] = "${./testfile}",
809 ["str"] = "foo",
810 ["true"] = true,
811 }
812 '';
813 };
814
815 luaBindings = shouldPass {
816 format = formats.lua {
817 asBindings = true;
818 };
819 input = {
820 null = null;
821 _false = false;
822 _true = true;
823 int = 10;
824 float = 3.141;
825 str = "foo";
826 attrs.foo = null;
827 list = [
828 null
829 null
830 ];
831 path = ./testfile;
832 inline = lib.mkLuaInline "hello('world')";
833 };
834 expected = ''
835 _false = false
836 _true = true
837 attrs = {
838 ["foo"] = nil,
839 }
840 float = 3.141
841 inline = (hello("world"))
842 int = 10
843 list = {
844 nil,
845 nil,
846 }
847 null = nil
848 path = "${./testfile}"
849 str = "foo"
850 '';
851 };
852
853 phpAtoms = shouldPass rec {
854 format = formats.php { finalVariable = "config"; };
855 input = {
856 null = null;
857 false = false;
858 true = true;
859 int = 10;
860 float = 3.141;
861 str = "foo";
862 str_special = "foo\ntesthello'''";
863 attrs.foo = null;
864 list = [
865 null
866 null
867 ];
868 mixed = format.lib.mkMixedArray [ 10 3.141 ] {
869 str = "foo";
870 attrs.foo = null;
871 };
872 raw = format.lib.mkRaw "random_function()";
873 };
874 expected = ''
875 <?php
876 declare(strict_types=1);
877 $config = ['attrs' => ['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
878 testhello\'\'\'${"'"}, 'true' => true];
879 '';
880 };
881
882 phpReturn = shouldPass {
883 format = formats.php { };
884 input = {
885 int = 10;
886 float = 3.141;
887 str = "foo";
888 str_special = "foo\ntesthello'''";
889 attrs.foo = null;
890 };
891 expected = ''
892 <?php
893 declare(strict_types=1);
894 return ['attrs' => ['foo' => null], 'float' => 3.141000, 'int' => 10, 'str' => 'foo', 'str_special' => 'foo
895 testhello\'\'\'${"'"}];
896 '';
897 };
898
899 badgerfishToXmlGenerate = shouldPass {
900 format = formats.xml { };
901 input = {
902 root = {
903 "@id" = "123";
904 "@class" = "example";
905 child1 = {
906 "@name" = "child1Name";
907 "#text" = "text node";
908 };
909 child2 = {
910 grandchild = "This is a grandchild text node.";
911 };
912 nulltest = null;
913 };
914 };
915 expected = ''
916 <?xml version="1.0" encoding="utf-8"?>
917 <root class="example" id="123">
918 <child1 name="child1Name">text node</child1>
919 <child2>
920 <grandchild>This is a grandchild text node.</grandchild>
921 </child2>
922 <nulltest></nulltest>
923 </root>
924 '';
925 };
926}