aboutsummaryrefslogtreecommitdiffstats
open Core

let with_dir path ~f =
  let fd = Core_unix.opendir path in
  f fd;
  Core_unix.closedir fd

let walk_dir path ~f =
  with_dir path ~f:(fun fd ->
      let rec go () =
        match Core_unix.readdir_opt fd with
        | Some entry ->
            f (Filename.concat path entry);
            go ()
        | None -> ()
      in
      go ())

type testcase = {
  name : string;
  dir : string;
  input : string;
  expected_output : [ `Okay of string | `Fail ];
}

let testdata_dir = "./testdata"
and testcases = ref []
and testcases_ignored = ref 0

let add_testcase c = testcases := c :: !testcases

let print_testcase_stats () =
  let okay, fail =
    List.fold !testcases ~init:(0, 0)
      ~f:(fun (okay, fail) { expected_output; _ } ->
        match expected_output with
        | `Okay _ -> (okay + 1, fail)
        | `Fail -> (okay, fail + 1))
  in
  printf
    "Loaded %d test cases (ignored %d), expected results: okay %d, fail %d\n%!"
    (okay + fail) !testcases_ignored okay fail

let imports () =
  Mininix.Import.materialize
    [ { filename = "./testdata/lib.nix"; deps = [] } ]
    ~relative_to:(Core_unix.getcwd ())

type eval_err = [ `Timeout | `ParseError | `ProgramError | `ElaborateError ]
[@@deriving sexp]

type eval_result = (string, eval_err) Result.t [@@deriving sexp]

let eval input ~name ~dir ~imports =
  let dir = Filename.to_absolute_exn dir ~relative_to:(Core_unix.getcwd ()) in
  try
    input
    |> Nix.parse ~filename:(name ^ ".nix")
    |> Nix.elaborate ~dir:(Some dir)
    |> Mininix.Nix2mininix.from_nix |> Mininix.apply_prelude
    |> Mininix.interp_tl ~fuel:`Limited ~mode:`Deep ~imports
    |> function
    | Res (Some v) ->
        Ok (v |> Mininix.Mininix2nix.from_val |> Nix.Printer.to_string)
    | Res None -> Error `ProgramError
    | NoFuel -> Error `Timeout
  with
  | Nix.ParseError _ -> Error `ParseError
  | Nix.ElaborateError _ -> Error `ElaborateError
  | Mininix.Nix2mininix.FromNixError _ -> Error `ElaborateError

let eval_subproc input ~name ~dir ~imports =
  let rxfd, txfd = Core_unix.pipe () in
  match Core_unix.fork () with
  | `In_the_child ->
      let txc = Core_unix.out_channel_of_descr txfd in
      eval input ~name ~dir ~imports
      |> [%sexp_of: eval_result] |> Sexp.output txc;
      exit 0
  | `In_the_parent child_pid ->
      let select_res =
        Core_unix.select ~restart:true ~read:[ rxfd ] ~write:[] ~except:[]
          ~timeout:(`After (Time_ns.Span.of_min 1.))
          ()
      in
      if List.is_empty select_res.read then (
        ignore (Signal_unix.send Signal.kill (`Pid child_pid));
        ignore (Core_unix.waitpid child_pid);
        Error `Timeout)
      else
        let rxc = Core_unix.in_channel_of_descr rxfd in
        let res = Sexp.input_sexp rxc |> [%of_sexp: eval_result] in
        ignore (Core_unix.waitpid child_pid);
        Core_unix.close ~restart:true rxfd;
        Core_unix.close ~restart:true txfd;
        res

type test_result =
  [ `Timeout
  | `ParseError
  | `ProgramError
  | `ElaborateError
  | `WrongOutput
  | `UnexpectedSuccess
  | `Okay ]

let run_testcase ~imports = function
  | { name; dir; input; expected_output = `Okay expected_output } -> (
      match eval_subproc input ~name ~dir ~imports with
      | Ok got_output ->
          if String.(strip got_output = strip expected_output) then `Okay
          else `WrongOutput
      | Error err -> (err :> test_result))
  | { name; dir; input; expected_output = `Fail } -> (
      match eval_subproc input ~name ~dir ~imports with
      | Ok _ -> `UnexpectedSuccess
      | Error _ -> `Okay)

type test_stats = {
  okay : int;
  unexpected_success : int;
  wrong_output : int;
  parse_error : int;
  elaborate_error : int;
  program_error : int;
  timeout : int;
}

let test_stats_empty =
  {
    okay = 0;
    unexpected_success = 0;
    wrong_output = 0;
    parse_error = 0;
    elaborate_error = 0;
    program_error = 0;
    timeout = 0;
  }

let run_testcases () =
  Nix.Printer.set_width 1000000;
  let mat_imports = imports () in
  let stats =
    List.foldi !testcases ~init:test_stats_empty ~f:(fun i stats c ->
        printf "[%d/%d] %s  %!" (i + 1) (List.length !testcases) c.name;
        match run_testcase c ~imports:mat_imports with
        | `Okay ->
            printf "okay\n%!";
            { stats with okay = stats.okay + 1 }
        | `UnexpectedSuccess ->
            printf "unexpectedly succeeded\n%!";
            { stats with unexpected_success = stats.unexpected_success + 1 }
        | `WrongOutput ->
            printf "gave wrong output\n%!";
            { stats with wrong_output = stats.wrong_output + 1 }
        | `ParseError ->
            printf "could not be parsed\n%!";
            { stats with parse_error = stats.parse_error + 1 }
        | `ElaborateError ->
            printf "could not be elaborated\n%!";
            { stats with elaborate_error = stats.elaborate_error + 1 }
        | `ProgramError ->
            printf "failed to execute\n%!";
            { stats with program_error = stats.program_error + 1 }
        | `Timeout ->
            printf "timed out\n%!";
            { stats with timeout = stats.timeout + 1 })
  in
  printf
    "Results:\n\
    \  %d gave the expected output\n\
    \  %d unexpectedly succeeded\n\
    \  %d gave wrong output\n\
    \  %d could not be parsed\n\
    \  %d could not be elaborated\n\
    \  %d failed to execute\n\
    \  %d timed out\n\
     %!"
    stats.okay stats.unexpected_success stats.wrong_output stats.parse_error
    stats.elaborate_error stats.program_error stats.timeout

let try_add_testcase without_ext =
  try
    let dir = Filename.dirname without_ext in
    let input = In_channel.read_all (without_ext ^ ".nix") in
    let name = Filename.basename without_ext in
    if String.is_prefix ~prefix:"eval-fail" name then
      add_testcase { name; dir; input; expected_output = `Fail }
    else if String.is_prefix ~prefix:"eval-okay" name then
      let expected_output = In_channel.read_all (without_ext ^ ".exp") in
      add_testcase { name; dir; input; expected_output = `Okay expected_output }
  with
  (* There are certain test cases where the '.nix' file is available, but
     there is no '.exp' file. (Instead, for example, there may be a
     '.exp-disabled' file, which we don't check for.) So [add_testcase] fails
     when trying to read the '.exp' file, which does not exist. We catch the
     exception that is then raised in [add_testcase] here. *)
  | Sys_error _ ->
    ()

let ignore_tests =
  [
    (* We do not implement '«repeated»' *)
    "eval-okay-repeated-empty-attrs";
    "eval-okay-repeated-empty-list";
    (* # Very specific / hard-to-implement builtins: *)
    (* We do not implement conversion from/to JSON/XML *)
    "eval-okay-toxml";
    "eval-okay-toxml2";
    "eval-okay-tojson";
    "eval-okay-fromTOML";
    "eval-okay-fromTOML-timestamps";
    "eval-okay-fromjson";
    "eval-okay-fromjson-escapes";
    "eval-fail-fromJSON-overflowing";
    "eval-fail-fromTOML-timestamps";
    "eval-fail-toJSON";
    (* We do not implement hasing *)
    "eval-okay-convertHash";
    "eval-okay-hashstring";
    "eval-okay-hashfile";
    "eval-okay-groupBy";
    "eval-okay-zipAttrsWith";
    "eval-fail-hashfile-missing";
    (* We do not support filesystem operations *)
    "eval-okay-readDir";
    "eval-okay-readfile";
    "eval-okay-readFileType";
    "eval-okay-symlink-resolution";
    (* We do not support version operations *)
    "eval-okay-splitversion";
    "eval-okay-versions";
    (* We do not support flake references *)
    "eval-okay-parse-flake-ref";
    "eval-okay-flake-ref-to-string";
    "eval-fail-flake-ref-to-string-negative-integer";
    (* We do not support regexes *)
    "eval-okay-regex-match";
    "eval-okay-regex-split";
    (* # Features that the core interpreter lacks *)
    (* We do not implement derivations and contexts *)
    "eval-okay-derivation-legacy";
    "eval-okay-eq-derivations";
    "eval-fail-addDrvOutputDependencies-empty-context";
    "eval-fail-addDrvOutputDependencies-multi-elem-context";
    "eval-fail-addDrvOutputDependencies-wrong-element-kind";
    "eval-fail-assert-equal-derivations";
    "eval-fail-assert-equal-derivations-extra";
    "eval-fail-derivation-name";
    "eval-okay-context";
    "eval-okay-context-introspection";
    "eval-okay-substring-context";
    "eval-fail-addErrorContext-example";
    (* We do not support scopedImport *)
    "eval-okay-import";
    (* We do not support tryEval *)
    "eval-okay-redefine-builtin";
    "eval-okay-tryeval";
    (* We do not support unsafeGetAttrPos nor __curPos *)
    "eval-okay-curpos";
    "eval-okay-getattrpos";
    "eval-okay-getattrpos-functionargs";
    "eval-okay-getattrpos-undefined";
    "eval-okay-inherit-attr-pos";
    (* We do not support environment variable lookup *)
    "eval-okay-getenv";
    (* We do not support '__override's. Rationale: this construct has expressly
       been avoided in Nixpkgs since the 13.10 release, see
       https://github.com/NixOS/nixpkgs/issues/2112 *)
    "eval-okay-attrs6";
    "eval-okay-overrides";
    "eval-fail-set-override";
    (* We do not implement the 'trace' builtin *)
    "eval-okay-print";
    "eval-okay-inherit-from";
    (* ^ also uses __overrides, for which we lack support *)
    (* We do not implement flags to set arguments / retrieve attributes
       for the evaluator *)
    (* We do not support setting variables outside of the program *)
    "eval-okay-autoargs";
    (* We do not support paths *)
    "eval-okay-baseNameOf";
    "eval-okay-path";
    "eval-okay-path-string-interpolation";
    "eval-okay-pathexists";
    "eval-okay-search-path";
    "eval-okay-string";
    "eval-okay-types";
    "eval-fail-assert-equal-paths";
    "eval-fail-bad-string-interpolation-2";
    "eval-fail-nonexist-path";
    "eval-fail-path-slash";
    "eval-fail-to-path";
    (* We do not implement the 'currentSystem' and 'dirOf' builtins *)
    "eval-okay-builtins";
    (* We do not support fetch operations *)
    "eval-fail-fetchTree-negative";
    "eval-fail-fetchurl-baseName";
    "eval-fail-fetchurl-baseName-attrs";
    "eval-fail-fetchurl-baseName-attrs-name";
    (* We do not support the pipe operator *)
    "eval-fail-pipe-operators";
  ]

let () =
  Printf.printf "Running in %s\n%!" (Core_unix.getcwd ());
  walk_dir testdata_dir ~f:(fun entry ->
      match Filename.split_extension entry with
      | without_ext, Some "nix" ->
          if
            List.exists ignore_tests ~f:(fun name ->
                String.(name = Filename.basename without_ext))
          then testcases_ignored := !testcases_ignored + 1
          else try_add_testcase without_ext
      | _ -> ());
  testcases :=
    List.sort !testcases ~compare:(fun c1 c2 -> String.compare c1.name c2.name);
  print_testcase_stats ();
  run_testcases ()