diff options
author | Rutger Broekhoff | 2025-07-07 21:52:08 +0200 |
---|---|---|
committer | Rutger Broekhoff | 2025-07-07 21:52:08 +0200 |
commit | ba61dfd69504ec6263a9dee9931d93adeb6f3142 (patch) | |
tree | d6c9b78e50eeab24e0c1c09ab45909a6ae3fd5db /test/test_mininix.ml | |
download | verified-dyn-lang-interp-ba61dfd69504ec6263a9dee9931d93adeb6f3142.tar.gz verified-dyn-lang-interp-ba61dfd69504ec6263a9dee9931d93adeb6f3142.zip |
Initialize repository
Diffstat (limited to 'test/test_mininix.ml')
-rw-r--r-- | test/test_mininix.ml | 319 |
1 files changed, 319 insertions, 0 deletions
diff --git a/test/test_mininix.ml b/test/test_mininix.ml new file mode 100644 index 0000000..f628a8a --- /dev/null +++ b/test/test_mininix.ml | |||
@@ -0,0 +1,319 @@ | |||
1 | open Core | ||
2 | |||
3 | let with_dir path ~f = | ||
4 | let fd = Core_unix.opendir path in | ||
5 | f fd; | ||
6 | Core_unix.closedir fd | ||
7 | |||
8 | let walk_dir path ~f = | ||
9 | with_dir path ~f:(fun fd -> | ||
10 | let rec go () = | ||
11 | match Core_unix.readdir_opt fd with | ||
12 | | Some entry -> | ||
13 | f (Filename.concat path entry); | ||
14 | go () | ||
15 | | None -> () | ||
16 | in | ||
17 | go ()) | ||
18 | |||
19 | type testcase = { | ||
20 | name : string; | ||
21 | dir : string; | ||
22 | input : string; | ||
23 | expected_output : [ `Okay of string | `Fail ]; | ||
24 | } | ||
25 | |||
26 | let testdata_dir = "./testdata" | ||
27 | and testcases = ref [] | ||
28 | and testcases_ignored = ref 0 | ||
29 | |||
30 | let add_testcase c = testcases := c :: !testcases | ||
31 | |||
32 | let print_testcase_stats () = | ||
33 | let okay, fail = | ||
34 | List.fold !testcases ~init:(0, 0) | ||
35 | ~f:(fun (okay, fail) { expected_output; _ } -> | ||
36 | match expected_output with | ||
37 | | `Okay _ -> (okay + 1, fail) | ||
38 | | `Fail -> (okay, fail + 1)) | ||
39 | in | ||
40 | printf | ||
41 | "Loaded %d test cases (ignored %d), expected results: okay %d, fail %d\n%!" | ||
42 | (okay + fail) !testcases_ignored okay fail | ||
43 | |||
44 | let imports () = | ||
45 | Mininix.Import.materialize | ||
46 | [ { filename = "./testdata/lib.nix"; deps = [] } ] | ||
47 | ~relative_to:(Core_unix.getcwd ()) | ||
48 | |||
49 | type eval_err = [ `Timeout | `ParseError | `ProgramError | `ElaborateError ] | ||
50 | [@@deriving sexp] | ||
51 | |||
52 | type eval_result = (string, eval_err) Result.t [@@deriving sexp] | ||
53 | |||
54 | let eval input ~name ~dir ~imports = | ||
55 | let dir = Filename.to_absolute_exn dir ~relative_to:(Core_unix.getcwd ()) in | ||
56 | try | ||
57 | input | ||
58 | |> Nix.parse ~filename:(name ^ ".nix") | ||
59 | |> Nix.elaborate ~dir:(Some dir) | ||
60 | |> Mininix.Nix2mininix.from_nix |> Mininix.apply_prelude | ||
61 | |> Mininix.interp_tl ~fuel:`Limited ~mode:`Deep ~imports | ||
62 | |> function | ||
63 | | Res (Some v) -> | ||
64 | Ok (v |> Mininix.Mininix2nix.from_val |> Nix.Printer.to_string) | ||
65 | | Res None -> Error `ProgramError | ||
66 | | NoFuel -> Error `Timeout | ||
67 | with | ||
68 | | Nix.ParseError _ -> Error `ParseError | ||
69 | | Nix.ElaborateError _ -> Error `ElaborateError | ||
70 | | Mininix.Nix2mininix.FromNixError _ -> Error `ElaborateError | ||
71 | |||
72 | let eval_subproc input ~name ~dir ~imports = | ||
73 | let rxfd, txfd = Core_unix.pipe () in | ||
74 | match Core_unix.fork () with | ||
75 | | `In_the_child -> | ||
76 | let txc = Core_unix.out_channel_of_descr txfd in | ||
77 | eval input ~name ~dir ~imports | ||
78 | |> [%sexp_of: eval_result] |> Sexp.output txc; | ||
79 | exit 0 | ||
80 | | `In_the_parent child_pid -> | ||
81 | let select_res = | ||
82 | Core_unix.select ~restart:true ~read:[ rxfd ] ~write:[] ~except:[] | ||
83 | ~timeout:(`After (Time_ns.Span.of_min 1.)) | ||
84 | () | ||
85 | in | ||
86 | if List.is_empty select_res.read then ( | ||
87 | ignore (Signal_unix.send Signal.kill (`Pid child_pid)); | ||
88 | ignore (Core_unix.waitpid child_pid); | ||
89 | Error `Timeout) | ||
90 | else | ||
91 | let rxc = Core_unix.in_channel_of_descr rxfd in | ||
92 | let res = Sexp.input_sexp rxc |> [%of_sexp: eval_result] in | ||
93 | ignore (Core_unix.waitpid child_pid); | ||
94 | Core_unix.close ~restart:true rxfd; | ||
95 | Core_unix.close ~restart:true txfd; | ||
96 | res | ||
97 | |||
98 | type test_result = | ||
99 | [ `Timeout | ||
100 | | `ParseError | ||
101 | | `ProgramError | ||
102 | | `ElaborateError | ||
103 | | `WrongOutput | ||
104 | | `UnexpectedSuccess | ||
105 | | `Okay ] | ||
106 | |||
107 | let run_testcase ~imports = function | ||
108 | | { name; dir; input; expected_output = `Okay expected_output } -> ( | ||
109 | match eval_subproc input ~name ~dir ~imports with | ||
110 | | Ok got_output -> | ||
111 | if String.(strip got_output = strip expected_output) then `Okay | ||
112 | else `WrongOutput | ||
113 | | Error err -> (err :> test_result)) | ||
114 | | { name; dir; input; expected_output = `Fail } -> ( | ||
115 | match eval_subproc input ~name ~dir ~imports with | ||
116 | | Ok _ -> `UnexpectedSuccess | ||
117 | | Error _ -> `Okay) | ||
118 | |||
119 | type test_stats = { | ||
120 | okay : int; | ||
121 | unexpected_success : int; | ||
122 | wrong_output : int; | ||
123 | parse_error : int; | ||
124 | elaborate_error : int; | ||
125 | program_error : int; | ||
126 | timeout : int; | ||
127 | } | ||
128 | |||
129 | let test_stats_empty = | ||
130 | { | ||
131 | okay = 0; | ||
132 | unexpected_success = 0; | ||
133 | wrong_output = 0; | ||
134 | parse_error = 0; | ||
135 | elaborate_error = 0; | ||
136 | program_error = 0; | ||
137 | timeout = 0; | ||
138 | } | ||
139 | |||
140 | let run_testcases () = | ||
141 | Nix.Printer.set_width 1000000; | ||
142 | let mat_imports = imports () in | ||
143 | let stats = | ||
144 | List.foldi !testcases ~init:test_stats_empty ~f:(fun i stats c -> | ||
145 | printf "[%d/%d] %s %!" (i + 1) (List.length !testcases) c.name; | ||
146 | match run_testcase c ~imports:mat_imports with | ||
147 | | `Okay -> | ||
148 | printf "okay\n%!"; | ||
149 | { stats with okay = stats.okay + 1 } | ||
150 | | `UnexpectedSuccess -> | ||
151 | printf "unexpectedly succeeded\n%!"; | ||
152 | { stats with unexpected_success = stats.unexpected_success + 1 } | ||
153 | | `WrongOutput -> | ||
154 | printf "gave wrong output\n%!"; | ||
155 | { stats with wrong_output = stats.wrong_output + 1 } | ||
156 | | `ParseError -> | ||
157 | printf "could not be parsed\n%!"; | ||
158 | { stats with parse_error = stats.parse_error + 1 } | ||
159 | | `ElaborateError -> | ||
160 | printf "could not be elaborated\n%!"; | ||
161 | { stats with elaborate_error = stats.elaborate_error + 1 } | ||
162 | | `ProgramError -> | ||
163 | printf "failed to execute\n%!"; | ||
164 | { stats with program_error = stats.program_error + 1 } | ||
165 | | `Timeout -> | ||
166 | printf "timed out\n%!"; | ||
167 | { stats with timeout = stats.timeout + 1 }) | ||
168 | in | ||
169 | printf | ||
170 | "Results:\n\ | ||
171 | \ %d gave the expected output\n\ | ||
172 | \ %d unexpectedly succeeded\n\ | ||
173 | \ %d gave wrong output\n\ | ||
174 | \ %d could not be parsed\n\ | ||
175 | \ %d could not be elaborated\n\ | ||
176 | \ %d failed to execute\n\ | ||
177 | \ %d timed out\n\ | ||
178 | %!" | ||
179 | stats.okay stats.unexpected_success stats.wrong_output stats.parse_error | ||
180 | stats.elaborate_error stats.program_error stats.timeout | ||
181 | |||
182 | let try_add_testcase without_ext = | ||
183 | try | ||
184 | let dir = Filename.dirname without_ext in | ||
185 | let input = In_channel.read_all (without_ext ^ ".nix") in | ||
186 | let name = Filename.basename without_ext in | ||
187 | if String.is_prefix ~prefix:"eval-fail" name then | ||
188 | add_testcase { name; dir; input; expected_output = `Fail } | ||
189 | else if String.is_prefix ~prefix:"eval-okay" name then | ||
190 | let expected_output = In_channel.read_all (without_ext ^ ".exp") in | ||
191 | add_testcase { name; dir; input; expected_output = `Okay expected_output } | ||
192 | with | ||
193 | (* There are certain test cases where the '.nix' file is available, but | ||
194 | there is no '.exp' file. (Instead, for example, there may be a | ||
195 | '.exp-disabled' file, which we don't check for.) So [add_testcase] fails | ||
196 | when trying to read the '.exp' file, which does not exist. We catch the | ||
197 | exception that is then raised in [add_testcase] here. *) | ||
198 | | Sys_error _ -> | ||
199 | () | ||
200 | |||
201 | let ignore_tests = | ||
202 | [ | ||
203 | (* We do not implement '«repeated»' *) | ||
204 | "eval-okay-repeated-empty-attrs"; | ||
205 | "eval-okay-repeated-empty-list"; | ||
206 | (* # Very specific / hard-to-implement builtins: *) | ||
207 | (* We do not implement conversion from/to JSON/XML *) | ||
208 | "eval-okay-toxml"; | ||
209 | "eval-okay-toxml2"; | ||
210 | "eval-okay-tojson"; | ||
211 | "eval-okay-fromTOML"; | ||
212 | "eval-okay-fromTOML-timestamps"; | ||
213 | "eval-okay-fromjson"; | ||
214 | "eval-okay-fromjson-escapes"; | ||
215 | "eval-fail-fromJSON-overflowing"; | ||
216 | "eval-fail-fromTOML-timestamps"; | ||
217 | "eval-fail-toJSON"; | ||
218 | (* We do not implement hasing *) | ||
219 | "eval-okay-convertHash"; | ||
220 | "eval-okay-hashstring"; | ||
221 | "eval-okay-hashfile"; | ||
222 | "eval-okay-groupBy"; | ||
223 | "eval-okay-zipAttrsWith"; | ||
224 | "eval-fail-hashfile-missing"; | ||
225 | (* We do not support filesystem operations *) | ||
226 | "eval-okay-readDir"; | ||
227 | "eval-okay-readfile"; | ||
228 | "eval-okay-readFileType"; | ||
229 | "eval-okay-symlink-resolution"; | ||
230 | (* We do not support version operations *) | ||
231 | "eval-okay-splitversion"; | ||
232 | "eval-okay-versions"; | ||
233 | (* We do not support flake references *) | ||
234 | "eval-okay-parse-flake-ref"; | ||
235 | "eval-okay-flake-ref-to-string"; | ||
236 | "eval-fail-flake-ref-to-string-negative-integer"; | ||
237 | (* We do not support regexes *) | ||
238 | "eval-okay-regex-match"; | ||
239 | "eval-okay-regex-split"; | ||
240 | (* # Features that the core interpreter lacks *) | ||
241 | (* We do not implement derivations and contexts *) | ||
242 | "eval-okay-derivation-legacy"; | ||
243 | "eval-okay-eq-derivations"; | ||
244 | "eval-fail-addDrvOutputDependencies-empty-context"; | ||
245 | "eval-fail-addDrvOutputDependencies-multi-elem-context"; | ||
246 | "eval-fail-addDrvOutputDependencies-wrong-element-kind"; | ||
247 | "eval-fail-assert-equal-derivations"; | ||
248 | "eval-fail-assert-equal-derivations-extra"; | ||
249 | "eval-fail-derivation-name"; | ||
250 | "eval-okay-context"; | ||
251 | "eval-okay-context-introspection"; | ||
252 | "eval-okay-substring-context"; | ||
253 | "eval-fail-addErrorContext-example"; | ||
254 | (* We do not support scopedImport *) | ||
255 | "eval-okay-import"; | ||
256 | (* We do not support tryEval *) | ||
257 | "eval-okay-redefine-builtin"; | ||
258 | "eval-okay-tryeval"; | ||
259 | (* We do not support unsafeGetAttrPos nor __curPos *) | ||
260 | "eval-okay-curpos"; | ||
261 | "eval-okay-getattrpos"; | ||
262 | "eval-okay-getattrpos-functionargs"; | ||
263 | "eval-okay-getattrpos-undefined"; | ||
264 | "eval-okay-inherit-attr-pos"; | ||
265 | (* We do not support environment variable lookup *) | ||
266 | "eval-okay-getenv"; | ||
267 | (* We do not support '__override's. Rationale: this construct has expressly | ||
268 | been avoided in Nixpkgs since the 13.10 release, see | ||
269 | https://github.com/NixOS/nixpkgs/issues/2112 *) | ||
270 | "eval-okay-attrs6"; | ||
271 | "eval-okay-overrides"; | ||
272 | "eval-fail-set-override"; | ||
273 | (* We do not implement the 'trace' builtin *) | ||
274 | "eval-okay-print"; | ||
275 | "eval-okay-inherit-from"; | ||
276 | (* ^ also uses __overrides, for which we lack support *) | ||
277 | (* We do not implement flags to set arguments / retrieve attributes | ||
278 | for the evaluator *) | ||
279 | (* We do not support setting variables outside of the program *) | ||
280 | "eval-okay-autoargs"; | ||
281 | (* We do not support paths *) | ||
282 | "eval-okay-baseNameOf"; | ||
283 | "eval-okay-path"; | ||
284 | "eval-okay-path-string-interpolation"; | ||
285 | "eval-okay-pathexists"; | ||
286 | "eval-okay-search-path"; | ||
287 | "eval-okay-string"; | ||
288 | "eval-okay-types"; | ||
289 | "eval-fail-assert-equal-paths"; | ||
290 | "eval-fail-bad-string-interpolation-2"; | ||
291 | "eval-fail-nonexist-path"; | ||
292 | "eval-fail-path-slash"; | ||
293 | "eval-fail-to-path"; | ||
294 | (* We do not implement the 'currentSystem' and 'dirOf' builtins *) | ||
295 | "eval-okay-builtins"; | ||
296 | (* We do not support fetch operations *) | ||
297 | "eval-fail-fetchTree-negative"; | ||
298 | "eval-fail-fetchurl-baseName"; | ||
299 | "eval-fail-fetchurl-baseName-attrs"; | ||
300 | "eval-fail-fetchurl-baseName-attrs-name"; | ||
301 | (* We do not support the pipe operator *) | ||
302 | "eval-fail-pipe-operators"; | ||
303 | ] | ||
304 | |||
305 | let () = | ||
306 | Printf.printf "Running in %s\n%!" (Core_unix.getcwd ()); | ||
307 | walk_dir testdata_dir ~f:(fun entry -> | ||
308 | match Filename.split_extension entry with | ||
309 | | without_ext, Some "nix" -> | ||
310 | if | ||
311 | List.exists ignore_tests ~f:(fun name -> | ||
312 | String.(name = Filename.basename without_ext)) | ||
313 | then testcases_ignored := !testcases_ignored + 1 | ||
314 | else try_add_testcase without_ext | ||
315 | | _ -> ()); | ||
316 | testcases := | ||
317 | List.sort !testcases ~compare:(fun c1 c2 -> String.compare c1.name c2.name); | ||
318 | print_testcase_stats (); | ||
319 | run_testcases () | ||