Generate tests reports from 'clerk test'

This is a proper replacement for the previous shell-based placeholder hack.

Here is a summary:

- `clerk runtest` (normally run by ninja) is much extended:
  * besides generating the test@out file, it checks individual tests for success
    and can write a report file containing their status, and the positions for
    their (expected/current) outputs (this uses `Marshal`)
  * it now handles out-tests directly in addition to inline-tests, for which
    it generates the separate output file ; they are included in the report

- ninja is now tasked with building all the test reports (which shouldn't fail);
  for directories, individual reports are concatenated (as before).
  Removing intermediate report rules, and out-test rules means that the ninja
  file is much simplified.

- then, clerk takes back control, reads the final reports and formats them in a
  user-friendly way. Printing the reports may imply running `diff` internally.
  In particular, the commands to easily reproduce each test are provided.
  Resetting the test results if required is also done directly by clerk, at this
  stage.

A few switches are available to customise the output, but I am waiting for some
feedback before deciding what to make available from the CLI.

The `clerk report` command is available to manually explore test reports, but
normally the processing is done directly at the end of `clerk test` (i.e. ninja
will no longer call that command)
This commit is contained in:
Louis Gesbert 2024-06-14 21:05:19 +02:00 committed by vbot
parent b4a14bb102
commit 45b0feaf20
No known key found for this signature in database
GPG Key ID: A102739F983C6C72
16 changed files with 754 additions and 303 deletions

View File

@ -80,7 +80,26 @@ module Cli = struct
NOTE: if this is set, all inline tests that are $(i,not) \
$(b,catala test-scope) are skipped to avoid redundant testing.")
let runtest_report =
Arg.(
value
& opt (some string) None
& info ["report"] ~docv:"FILE"
~doc:
"If set, $(i,clerk runtest) will output a tests result summary in \
binary format to the given $(b,FILE)")
let runtest_out =
Arg.(
value
& pos 1 (some string) None
& info [] ~docv:"OUTFILE"
~doc:"Write the test outcome to file $(b,OUTFILE) instead of stdout.")
module Global : sig
val color : Catala_utils.Global.when_enum Term.t
val debug : bool Term.t
val term :
(chdir:File.t option ->
catala_exe:File.t option ->
@ -385,26 +404,6 @@ module Poll = struct
let ocaml_link_flags : string list Lazy.t =
lazy (snd (Lazy.force ocaml_include_and_lib_flags))
let has_command cmd =
let check_cmd = Printf.sprintf "type %s >/dev/null 2>&1" cmd in
Sys.command check_cmd = 0
let diff_command =
lazy
(if has_command "patdiff" then
["patdiff"; "-alt-old"; "reference"; "-alt-new"; "current-output"]
else
[
"diff";
"-u";
"-b";
"--color";
"--label";
"reference";
"--label";
"current-output";
])
end
(* Adjusts paths specified from the command-line relative to the user cwd to be
@ -435,8 +434,6 @@ module Var = struct
let ocamlopt_exe = make "OCAMLOPT_EXE"
let ocaml_flags = make "OCAML_FLAGS"
let runtime_ocaml_libs = make "RUNTIME_OCAML_LIBS"
let diff = make "DIFF"
let post_test = make "POST_TEST"
(** Rule vars, Used in specific rules *)
@ -447,7 +444,6 @@ module Var = struct
let orig_src = make "orig-src"
let scope = make "scope"
let test_id = make "test-id"
let test_command = make "test-command"
let ( ! ) = Var.v
end
@ -500,13 +496,10 @@ let base_bindings catala_exe catala_flags build_dir include_dirs test_flags =
Nj.binding Var.ocamlopt_exe ["ocamlopt"];
Nj.binding Var.ocaml_flags (ocaml_flags @ includes);
Nj.binding Var.runtime_ocaml_libs (Lazy.force Poll.ocaml_link_flags);
Nj.binding Var.diff (Lazy.force Poll.diff_command);
Nj.binding Var.post_test [Var.(!diff)];
]
let[@ocamlformat "disable"] static_base_rules =
let open Var in
let color = Message.has_color stdout in
let shellout l = Format.sprintf "$$(%s)" (String.concat " " l) in
[
Nj.rule "copy"
@ -545,30 +538,11 @@ let[@ocamlformat "disable"] static_base_rules =
!input; "-o"; !output]
~description:["<catala>"; "python"; ""; !output];
Nj.rule "out-test"
~command: [
!catala_exe; !test_command; "--plugin-dir="; "-o -"; !catala_flags;
!input; "2>&1";
"|"; "sed";
"'s/\"CM0|[a-zA-Z0-9|]*\"/\"CMX|XXXXXXXX|XXXXXXXX|XXXXXXXX\"/g'";
">"; !output;
"||"; "true"
]
~description:
["<catala>"; "test"; !test_id; ""; !input; "(" ^ !test_command ^ ")"];
Nj.rule "inline-tests"
Nj.rule "tests"
~command:
[!clerk_exe; "runtest"; !clerk_flags; !input; ">"; !output; "2>&1";
"||"; "true"]
~description:["<catala>"; "inline-tests"; ""; !input];
Nj.rule "post-test"
~command:[
!post_test; !input; ";";
"echo"; "-n"; "$$?"; ">"; !output;
]
~description:["<test>"; !test_id];
[!clerk_exe; "runtest"; !clerk_flags; !input;
"--report"; !output;]
~description:["<catala>"; "tests"; ""; !input];
Nj.rule "interpret"
~command:
@ -579,37 +553,6 @@ let[@ocamlformat "disable"] static_base_rules =
Nj.rule "dir-tests"
~command:["cat"; !input; ">"; !output; ";"]
~description:["<test>"; !test_id];
Nj.rule "test-results"
~command:[
"out=" ^ !output; ";";
"success=$$("; "tr"; "-cd"; "0"; "<"; !input; "|"; "wc"; "-c"; ")"; ";";
"total=$$("; "wc"; "-c"; "<"; !input; ")"; ";";
"pass=$$("; ")"; ";";
"if"; "test"; "\"$$success\""; "-eq"; "\"$$total\""; ";"; "then";
"printf";
(if color then "\"\\n[\\033[32mPASS\\033[m] \\033[1m%s\\033[m: \
\\033[32m%3d\\033[m/\\033[32m%d\\033[m\\n\""
else "\"\\n[PASS] %s: %3d/%d\\n\"");
"$${out%@test}"; "$$success"; "$$total"; ";";
"else";
"printf";
(if color then "\"\\n[\\033[31mFAIL\\033[m] \\033[1m%s\\033[m: \
\\033[31m%3d\\033[m/\\033[32m%d\\033[m\\n\""
else "\"\\n[FAIL] %s: %3d/%d\\n\"");
"$${out%@test}"; "$$success"; "$$total"; ";";
"return"; "1"; ";";
"fi";
]
~description:["<test>"; !output];
(* Note: this last rule looks horrible, but the processing is pretty simple:
in the rules above, we output the returning code of diffing individual
tests to a [<testfile>@test] file, then the rules for directories just
concat these files. What this last rule does is then just count the number
of `0` and the total number of characters in the file, and print a readable
message. Instead of this disgusting shell code embedded in the ninja file,
this could be a specialised subcommand of clerk, e.g. `clerk
test-diagnostic <results-file@test>` *)
]
let gen_build_statements
@ -722,75 +665,29 @@ let gen_build_statements
(src /../ "output" / Filename.basename src) -.- test.Scan.id
in
let tests =
let legacy_tests =
List.fold_left
(fun acc test ->
let vars =
[Var.test_id, [test.Scan.id]; Var.test_command, test.Scan.cmd]
in
let reference = legacy_test_reference test in
let test_out =
(!Var.builddir / src /../ "output" / Filename.basename src)
-.- test.id
in
Nj.build "out-test"
~inputs:[inc srcv]
~implicit_in:interp_deps ~outputs:[test_out] ~vars
:: (* The test reference is an input because of the cases when we run
diff; it should actually be an output for the cases when we
reset but that shouldn't cause trouble. *)
Nj.build "post-test" ~inputs:[reference; test_out]
~implicit_in:["always"]
~outputs:[(!Var.builddir / reference) ^ "@post"]
~vars:[Var.test_id, [reference]]
:: acc)
[] item.legacy_tests
let out_tests_references =
List.map (fun test -> legacy_test_reference test) item.legacy_tests
in
let inline_tests =
if not item.has_inline_tests then []
else
[
Nj.build "inline-tests"
~inputs:[inc srcv]
~implicit_in:(!Var.clerk_exe :: interp_deps)
~outputs:[(!Var.builddir / srcv) ^ "@out"];
]
let out_tests_prepare =
List.map
(fun f -> Nj.build "copy" ~inputs:[f] ~outputs:[inc f])
out_tests_references
in
let tests =
let results =
Nj.build "test-results"
~outputs:[srcv ^ "@test"]
~inputs:[inc (srcv ^ "@test")]
in
let inline_test label =
Nj.build "post-test"
~outputs:[inc (srcv ^ label)]
~inputs:[srcv; inc (srcv ^ "@out")]
~implicit_in:["always"]
~vars:[Var.test_id, [srcv]]
in
match item.legacy_tests with
| [] ->
if item.has_inline_tests then [inline_test "@test"; results] else []
| legacy ->
let inline =
if item.has_inline_tests then [inline_test "@inline"] else []
in
inline
@ [
Nj.build "dir-tests"
~outputs:[inc (srcv ^ "@test")]
~inputs:
((if item.has_inline_tests then [inc (srcv ^ "@inline")] else [])
@ List.map
(fun test ->
(!Var.builddir / legacy_test_reference test) ^ "@post")
legacy)
~vars:[Var.test_id, [srcv]];
results;
]
if (not item.has_inline_tests) && item.legacy_tests = [] then []
else
[
Nj.build "tests"
~inputs:[inc srcv]
~implicit_in:
((!Var.clerk_exe :: interp_deps)
@ List.map inc out_tests_references)
~outputs:[inc srcv ^ "@test"; inc srcv ^ "@out"]
~implicit_out:
(List.map (fun o -> inc o ^ "@out") out_tests_references);
]
in
legacy_tests @ inline_tests @ tests
out_tests_prepare @ tests
in
Seq.concat
@@ List.to_seq
@ -839,9 +736,6 @@ let dir_test_rules dir subdirs items =
~outputs:[(Var.(!builddir) / dir) ^ "@test"]
~inputs
~vars:[Var.test_id, [dir]];
Nj.build "test-results"
~outputs:[dir ^ "@test"]
~inputs:[(Var.(!builddir) / dir) ^ "@test"];
]
let build_statements include_dirs dir =
@ -854,11 +748,6 @@ let build_statements include_dirs dir =
let gen_ninja_file catala_exe catala_flags build_dir include_dirs test_flags dir
=
let build_dir =
match test_flags with
| [] -> build_dir
| flags -> File.((build_dir / "test") ^ String.concat "" flags)
in
let ( @+ ) = Seq.append in
Seq.return
(Nj.Comment (Printf.sprintf "File generated by Clerk v.%s\n" version))
@ -883,7 +772,8 @@ let ninja_init
~color
~debug
~ninja_output :
extra:def Seq.t -> test_flags:string list -> (File.t -> 'a) -> 'a =
extra:def Seq.t -> test_flags:string list -> (File.t -> File.t -> 'a) -> 'a
=
let _options = Catala_utils.Global.enforce_options ~debug ~color () in
let chdir =
match chdir with None -> Lazy.force Poll.project_root | some -> some
@ -898,6 +788,11 @@ let ninja_init
in
fun ~extra ~test_flags k ->
Message.debug "building ninja rules...";
let build_dir =
match test_flags with
| [] -> build_dir
| flags -> File.((build_dir / "test") ^ String.concat "" flags)
in
with_ninja_output
@@ fun nin_file ->
File.with_formatter_of_file nin_file (fun nin_ppf ->
@ -912,7 +807,7 @@ let ninja_init
]
in
Nj.format nin_ppf ninja_contents);
k nin_file
k build_dir nin_file
let cleaned_up_env () =
let passthrough_vars =
@ -947,14 +842,14 @@ let run_ninja ~clean_up_env cmdline =
| _, Unix.WEXITED n -> n
| _, (Unix.WSIGNALED n | Unix.WSTOPPED n) -> 128 - n
in
raise (Catala_utils.Cli.Exit_with return_code)
return_code
open Cmdliner
let build_cmd =
let run ninja_init (targets : string list) (ninja_flags : string list) =
ninja_init ~extra:Seq.empty ~test_flags:[]
@@ fun nin_file ->
@@ fun _build_dir nin_file ->
let targets =
List.map
(fun f ->
@ -965,7 +860,7 @@ let build_cmd =
in
let ninja_cmd = ninja_cmdline ninja_flags nin_file targets in
Message.debug "executing '%s'..." (String.concat " " ninja_cmd);
run_ninja ~clean_up_env:false ninja_cmd
raise (Catala_utils.Cli.Exit_with (run_ninja ~clean_up_env:false ninja_cmd))
in
let doc =
"Low-level build command: can be used to forward build targets or options \
@ -982,30 +877,60 @@ let test_cmd =
(reset_test_outputs : bool)
(test_flags : string list)
(ninja_flags : string list) =
ninja_init ~extra:Seq.empty ~test_flags
@@ fun build_dir nin_file ->
let targets =
let fs = if files_or_folders = [] then ["."] else files_or_folders in
List.map (fun f -> fix_path f ^ "@test") fs
List.map File.(fun f -> (build_dir / fix_path f) ^ "@test") fs
in
let extra =
List.to_seq
((if reset_test_outputs then
[
Nj.binding Var.post_test
[
"test_reset() { if ! diff -q $$1 $$2 >/dev/null; then cp -f \
$$2 $$1; fi; }";
";";
"test_reset";
];
]
else [])
@ [Nj.default targets])
in
ninja_init ~extra ~test_flags
@@ fun nin_file ->
let ninja_cmd = ninja_cmdline ninja_flags nin_file targets in
Message.debug "executing '%s'..." (String.concat " " ninja_cmd);
run_ninja ~clean_up_env:true ninja_cmd
match run_ninja ~clean_up_env:true ninja_cmd with
| 0 ->
Message.debug "gathering test results...";
let open Clerk_report in
let reports = List.flatten (List.map read_many targets) in
if reset_test_outputs then
let () =
let ppf = Message.formatter_of_out_channel stdout () in
match List.filter (fun f -> f.successful < f.total) reports with
| [] ->
Format.fprintf ppf
"[@{<green>DONE@}] All tests passed, nothing to reset@."
| need_reset ->
List.iter
(fun f ->
let files =
List.fold_left
(fun files t ->
if t.success then files
else
File.Map.add (fst t.result).Lexing.pos_fname
(String.remove_prefix
~prefix:File.(build_dir / "")
(fst t.expected).Lexing.pos_fname)
files)
File.Map.empty f.tests
in
File.Map.iter
(fun result expected ->
Format.kasprintf Sys.command "cp -f %a %a@." File.format
result File.format expected
|> ignore)
files)
need_reset;
Format.fprintf ppf
"[@{<green>DONE@}] @{<yellow;bold>%d@} test files were \
@{<yellow>RESET@}@."
(List.length need_reset)
in
raise (Catala_utils.Cli.Exit_with 0)
else if summary ~build_dir reports then
raise (Catala_utils.Cli.Exit_with 0)
else raise (Catala_utils.Cli.Exit_with 1)
| 1 -> raise (Catala_utils.Cli.Exit_with 10) (* Ninja build failed *)
| err -> raise (Catala_utils.Cli.Exit_with err)
(* Other Ninja error ? *)
in
let doc =
"Scan the given files or directories for catala tests, build their \
@ -1036,10 +961,10 @@ let run_cmd =
(List.map (fun file -> file ^ "@interpret") files_or_folders)))
in
ninja_init ~extra ~test_flags:[]
@@ fun nin_file ->
@@ fun _build_dir nin_file ->
let ninja_cmd = ninja_cmdline ninja_flags nin_file [] in
Message.debug "executing '%s'..." (String.concat " " ninja_cmd);
run_ninja ~clean_up_env:false ninja_cmd
raise (Catala_utils.Cli.Exit_with (run_ninja ~clean_up_env:false ninja_cmd))
in
let doc =
"Runs the Catala interpreter on the given files, after building their \
@ -1055,15 +980,15 @@ let run_cmd =
$ Cli.ninja_flags)
let runtest_cmd =
let run catala_exe catala_opts include_dirs test_flags file =
let run catala_exe catala_opts include_dirs test_flags report out file =
let catala_opts =
List.fold_left
(fun opts dir -> "-I" :: dir :: opts)
catala_opts include_dirs
in
Clerk_runtest.run_inline_tests
(Option.value ~default:"catala" catala_exe)
catala_opts test_flags file;
Clerk_runtest.run_tests
~catala_exe:(Option.value ~default:"catala" catala_exe)
~catala_opts ~test_flags ~report ~out file;
0
in
let doc =
@ -1077,9 +1002,33 @@ let runtest_cmd =
$ Cli.catala_opts
$ Cli.include_dirs
$ Cli.test_flags
$ Cli.runtest_report
$ Cli.runtest_out
$ Cli.single_file)
let main_cmd = Cmd.group Cli.info [build_cmd; test_cmd; run_cmd; runtest_cmd]
let report_cmd =
let run color debug build_dir file =
let _options = Catala_utils.Global.enforce_options ~debug ~color () in
let build_dir = Option.value ~default:"_build" build_dir in
let open Clerk_report in
let tests = read_many file in
let success = summary ~build_dir tests in
exit (if success then 0 else 1)
in
let doc =
"Mainly for internal purposes. Reads a test report file and displays a \
summary of the results, returning 0 on success and 1 if any test failed."
in
Cmd.v (Cmd.info ~doc "report")
Term.(
const run
$ Cli.Global.color
$ Cli.Global.debug
$ Cli.build_dir
$ Cli.single_file)
let main_cmd =
Cmd.group Cli.info [build_cmd; test_cmd; run_cmd; runtest_cmd; report_cmd]
let main () =
try exit (Cmdliner.Cmd.eval' ~catch:false main_cmd) with

View File

@ -0,0 +1,272 @@
(* This file is part of the Catala build system, a specification language for
tax and social benefits computation rules. Copyright (C) 2024 Inria,
contributors: Louis Gesbert <louis.gesbert@inria.fr>
Licensed under the Apache License, Version 2.0 (the "License"); you may not
use this file except in compliance with the License. You may obtain a copy of
the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
License for the specific language governing permissions and limitations under
the License. *)
(** This module defines and manipulates Clerk test reports, which can be written
by `clerk runtest` and read to provide test result summaries. This only
concerns inline tests (```catala-test-inline blocks). *)
open Catala_utils
type test = {
success : bool;
command_line : string list;
expected : Lexing.position * Lexing.position;
result : Lexing.position * Lexing.position;
}
type file = { name : File.t; successful : int; total : int; tests : test list }
type disp_toggle = DAll | DFailed | DNone
type disp_flags = {
mutable files : disp_toggle;
mutable tests : disp_toggle;
mutable diffs : bool;
mutable use_patdiff : bool;
}
let disp_flags =
{ files = DFailed; tests = DAll; diffs = true; use_patdiff = false }
let set_display_flags
?(files = disp_flags.files)
?(tests = disp_flags.tests)
?(diffs = disp_flags.diffs)
?(use_patdiff = disp_flags.use_patdiff)
() =
disp_flags.files <- files;
disp_flags.tests <- tests;
disp_flags.diffs <- diffs;
disp_flags.use_patdiff <- use_patdiff
let write_to f file =
File.with_out_channel f (fun oc -> Marshal.to_channel oc (file : file) [])
let read_from f = File.with_in_channel f Marshal.from_channel
let read_many f =
File.with_in_channel f
@@ fun ic ->
let rec results () =
match Marshal.from_channel ic with
| file -> file :: results ()
| exception End_of_file -> []
in
results ()
let has_command cmd =
let check_cmd = Printf.sprintf "type %s >/dev/null 2>&1" cmd in
Sys.command check_cmd = 0
let diff_command =
lazy
(if
disp_flags.use_patdiff
&& has_command "patdiff"
&& Message.has_color stdout
then ["patdiff"; "-alt-old"; "expected"; "-alt-new"; "result"]
else
[
"diff";
"-y";
"-t";
(* "--suppress-common-lines"; "--horizon-lines=3"; *)
"-W";
string_of_int (Message.terminal_columns () - 5);
(* "-b"; *)
("--color=" ^ if Message.has_color stdout then "always" else "never");
"--palette=ad=31:de=32";
"--label";
"expected";
"--label";
"result";
])
let get_diff p1 p2 =
let get_str (pstart, pend) =
assert (pstart.Lexing.pos_fname = pend.Lexing.pos_fname);
File.with_in_channel pstart.Lexing.pos_fname
@@ fun ic ->
seek_in ic pstart.Lexing.pos_cnum;
really_input_string ic (pend.Lexing.pos_cnum - pstart.Lexing.pos_cnum)
in
File.with_temp_file "clerk-diff" "a" ~contents:(get_str p1)
@@ fun f1 ->
File.with_temp_file "clerk_diff" "b" ~contents:(get_str p2)
@@ fun f2 ->
match Lazy.force diff_command with
| [] -> assert false
| cmd :: args ->
File.process_out ~check_exit:(fun _ -> ()) cmd (args @ [f1; f2])
let catala_commands_with_output_flag =
[
"makefile";
"html";
"latex";
"scopelang";
"dcalc";
"lcalc";
"ocaml";
"scalc";
"python";
"r";
"c";
]
let display ~build_dir ppf t =
let pfile f =
f
|> String.remove_prefix ~prefix:(build_dir ^ Filename.dir_sep)
|> String.remove_prefix ~prefix:(Sys.getcwd () ^ Filename.dir_sep)
in
let command_line_cleaned =
List.filter_map
(fun s -> if s = "--directory=" ^ build_dir then None else Some (pfile s))
t.command_line
|> function
| catala :: cmd :: args
when List.mem
(String.lowercase_ascii cmd)
catala_commands_with_output_flag ->
(catala :: cmd :: args) @ ["-o -"]
| cl -> cl
in
let pp_pos ppf (start, stop) =
assert (start.Lexing.pos_fname = stop.Lexing.pos_fname);
Format.fprintf ppf "@{<cyan>%s:%d-%d@}"
(pfile start.Lexing.pos_fname)
start.Lexing.pos_lnum stop.Lexing.pos_lnum
in
if t.success then (
if disp_flags.tests = DAll then
Format.fprintf ppf "@{<green>■@} %a passed" pp_pos t.expected)
else (
Format.pp_open_vbox ppf 2;
Format.fprintf ppf "@{<red>■@} %a failed@," pp_pos t.expected;
Format.fprintf ppf "@[<h>$ @{<yellow>%a@}@]@,"
(Format.pp_print_list ~pp_sep:Format.pp_print_space Format.pp_print_string)
command_line_cleaned;
if disp_flags.diffs then
get_diff t.expected t.result
|> String.split_on_char '\n'
|> List.filter (( <> ) "")
|> Format.pp_print_list Format.pp_print_string ppf;
Format.pp_close_box ppf ())
let display_file ~build_dir ppf t =
let pfile f = String.remove_prefix ~prefix:(build_dir ^ Filename.dir_sep) f in
if t.successful = t.total then (
if disp_flags.files = DAll then
Format.fprintf ppf
"@{<green;reverse;ul> @} @{<cyan>%s@}: @{<green;bold>%d@} / %d tests \
passed@,"
(pfile t.name) t.successful t.total)
else (
(function
| 0 -> Format.fprintf ppf "@{<red;reverse;ul> @}"
| _ -> Format.fprintf ppf "@{<yellow;reverse;ul> @}")
t.successful;
Format.fprintf ppf " @{<cyan>%s@}: " (pfile t.name);
(function
| 0 -> Format.fprintf ppf "@{<red;bold>0@}"
| n -> Format.fprintf ppf "@{<yellow;bold>%d@}" n)
t.successful;
Format.fprintf ppf " / %d tests passed" t.total;
Format.pp_print_break ppf 0 3;
if disp_flags.tests <> DNone then (
Format.pp_open_vbox ppf 0;
Format.pp_print_list (display ~build_dir) ppf t.tests;
Format.pp_close_box ppf ());
Format.pp_print_cut ppf ())
type box = { print_line : 'a. ('a, Format.formatter, unit) format -> 'a }
[@@ocaml.unboxed]
let print_box tcolor ppf title (pcontents : box -> unit) =
let columns = Message.terminal_columns () in
let tpad = columns - String.width title - 6 in
Format.fprintf ppf "@,%t┏%s @{<bold;reverse> %s @} %s┓@}@," tcolor
(String.repeat (tpad / 2) "")
title
(String.repeat (tpad - (tpad / 2)) "");
Format.pp_open_tbox ppf ();
Format.fprintf ppf "%t@<1>%s@}%*s" tcolor "" (columns - 2) "";
Format.pp_set_tab ppf ();
Format.fprintf ppf "%t┃@}@," tcolor;
let box =
{
print_line =
(fun fmt ->
Format.kfprintf
(fun ppf ->
Format.pp_print_tab ppf ();
Format.fprintf ppf "%t┃@}@," tcolor)
ppf ("%t@<1>%s@} " ^^ fmt) tcolor "");
}
in
pcontents box;
box.print_line "";
Format.pp_close_tbox ppf ();
Format.fprintf ppf "%t┗%s┛@}@," tcolor (String.repeat (columns - 2) "")
let summary ~build_dir tests =
let ppf = Message.formatter_of_out_channel stdout () in
Format.pp_open_vbox ppf 0;
let tests = List.filter (fun f -> f.total > 0) tests in
let files, success_files, success, total =
List.fold_left
(fun (files, success_files, success, total) file ->
( files + 1,
(if file.successful < file.total then success_files
else success_files + 1),
success + file.successful,
total + file.total ))
(0, 0, 0, 0) tests
in
if disp_flags.files <> DNone then
List.iter (fun f -> display_file ~build_dir ppf f) tests;
let result_box =
if success < total then
print_box (fun ppf -> Format.fprintf ppf "@{<red>") ppf "TESTS FAILED"
else
print_box
(fun ppf -> Format.fprintf ppf "@{<green>")
ppf "ALL TESTS PASSED"
in
result_box (fun box ->
box.print_line "@{<ul>%-5s %10s %10s %10s@}" "" "FAILED" "PASSED" "TOTAL";
if files > 1 then
box.print_line "%-5s @{<red;bold>%a@} @{<green;bold>%a@} %10d@}" "files"
(fun ppf -> function
| 0 -> Format.fprintf ppf "@{<green>%10d@}" 0
| n -> Format.fprintf ppf "%10d" n)
(files - success_files)
(fun ppf -> function
| 0 -> Format.fprintf ppf "@{<red>%10d@}" 0
| n -> Format.fprintf ppf "%10d" n)
success_files files;
box.print_line "%-5s @{<red;bold>%a@} @{<green;bold>%a@} %10d" "tests"
(fun ppf -> function
| 0 -> Format.fprintf ppf "@{<green>%10d@}" 0
| n -> Format.fprintf ppf "%10d" n)
(total - success)
(fun ppf -> function
| 0 -> Format.fprintf ppf "@{<red>%10d@}" 0
| n -> Format.fprintf ppf "%10d" n)
success total);
Format.pp_close_box ppf ();
Format.pp_print_flush ppf ();
success = total

View File

@ -0,0 +1,50 @@
(* This file is part of the Catala build system, a specification language for
tax and social benefits computation rules. Copyright (C) 2024 Inria,
contributors: Louis Gesbert <louis.gesbert@inria.fr>
Licensed under the Apache License, Version 2.0 (the "License"); you may not
use this file except in compliance with the License. You may obtain a copy of
the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
License for the specific language governing permissions and limitations under
the License. *)
(** This module defines and manipulates Clerk test reports, which can be written
by `clerk runtest` and read to provide test result summaries. This only
concerns inline tests (```catala-test-inline blocks). *)
open Catala_utils
type test = {
success : bool;
command_line : string list;
expected : Lexing.position * Lexing.position;
(** The precise offsets of the expected result in the source file *)
result : Lexing.position * Lexing.position;
(** Same for the actual result in the destination file *)
}
type file = { name : File.t; successful : int; total : int; tests : test list }
val write_to : File.t -> file -> unit
val read_from : File.t -> file
val read_many : File.t -> file list
val display : build_dir:File.t -> Format.formatter -> test -> unit
val summary : build_dir:File.t -> file list -> bool
(** Displays a summary to stdout; returns true if all tests succeeded *)
type disp_toggle = DAll | DFailed | DNone
val set_display_flags :
?files:disp_toggle ->
?tests:disp_toggle ->
?diffs:bool ->
?use_patdiff:bool ->
unit ->
unit

View File

@ -16,6 +16,31 @@
open Catala_utils
type output_buf = { oc : out_channel; mutable pos : Lexing.position }
let pos0 pos_fname =
{ Lexing.pos_fname; pos_cnum = 0; pos_lnum = 1; pos_bol = 0 }
let with_output file_opt f =
match file_opt with
| Some file ->
File.with_out_channel file @@ fun oc -> f { oc; pos = pos0 file }
| None -> f { oc = stdout; pos = pos0 "<stdout>" }
let out_line output_buf str =
let len = String.length str in
let has_nl = str <> "" && str.[len - 1] = '\n' in
output_string output_buf.oc str;
if not has_nl then output_char output_buf.oc '\n';
let pos_cnum = output_buf.pos.pos_cnum + len + if has_nl then 0 else 1 in
output_buf.pos <-
{
output_buf.pos with
Lexing.pos_cnum;
pos_lnum = output_buf.pos.pos_lnum + 1;
pos_bol = pos_cnum;
}
let sanitize =
let re_endtest = Re.(compile @@ seq [bol; str "```"]) in
let re_modhash =
@ -37,11 +62,7 @@ let sanitize =
|> Re.replace_string re_endtest ~by:"\\```"
|> Re.replace_string re_modhash ~by:"\"CMX|XXXXXXXX|XXXXXXXX|XXXXXXXX\""
let run_catala_test test_flags catala_exe catala_opts file program args oc =
let cmd_in_rd, cmd_in_wr = Unix.pipe ~cloexec:true () in
let cmd_out_rd, cmd_out_wr = Unix.pipe ~cloexec:true () in
let command_oc = Unix.out_channel_of_descr cmd_in_wr in
let command_ic = Unix.in_channel_of_descr cmd_out_rd in
let catala_test_command test_flags catala_exe catala_opts args out =
let catala_exe =
(* If the exe name contains directories, make it absolute. Otherwise don't
modify it so that it can be looked up in PATH. *)
@ -49,15 +70,15 @@ let run_catala_test test_flags catala_exe catala_opts file program args oc =
Unix.realpath catala_exe
else catala_exe
in
let cmd =
match args with
| cmd0 :: flags ->
match args with
| cmd0 :: flags -> (
try
let cmd0, flags =
match String.lowercase_ascii cmd0, flags, test_flags with
| "test-scope", scope_name :: flags, test_flags ->
"interpret", (("--scope=" ^ scope_name) :: flags) @ test_flags
| "test-scope", [], _ ->
output_string oc
out_line out
"[INVALID TEST] Invalid test command syntax, the 'test-scope' \
pseudo-command takes a scope name as first argument\n";
"interpret", test_flags
@ -65,24 +86,31 @@ let run_catala_test test_flags catala_exe catala_opts file program args oc =
| _, _, _ :: _ ->
raise Exit (* Skip other tests when test-flags is specified *)
in
Array.of_list
((catala_exe :: cmd0 :: catala_opts) @ flags @ ["--name=" ^ file; "-"])
| [] -> Array.of_list ((catala_exe :: catala_opts) @ [file])
in
let env =
Unix.environment ()
|> Array.to_seq
|> Seq.filter (fun s ->
not
(String.starts_with ~prefix:"OCAMLRUNPARAM=" s
|| String.starts_with ~prefix:"CATALA_" s))
|> Seq.cons "CATALA_OUT=-"
(* |> Seq.cons "CATALA_COLOR=never" *)
|> Seq.cons "CATALA_PLUGINS="
|> Array.of_seq
in
Some (Array.of_list ((catala_exe :: cmd0 :: catala_opts) @ flags))
with Exit -> None)
| [] -> Some (Array.of_list (catala_exe :: catala_opts))
let catala_test_env () =
Unix.environment ()
|> Array.to_seq
|> Seq.filter (fun s ->
not
(String.starts_with ~prefix:"OCAMLRUNPARAM=" s
|| String.starts_with ~prefix:"CATALA_" s))
|> Seq.cons "CATALA_OUT=-"
|> Seq.cons "CATALA_COLOR=never"
|> Seq.cons "CATALA_PLUGINS="
|> Array.of_seq
let run_catala_test filename cmd program expected out_line =
let cmd_in_rd, cmd_in_wr = Unix.pipe ~cloexec:true () in
let cmd_out_rd, cmd_out_wr = Unix.pipe ~cloexec:true () in
let command_oc = Unix.out_channel_of_descr cmd_in_wr in
let command_ic = Unix.in_channel_of_descr cmd_out_rd in
let env = catala_test_env () in
let cmd = Array.append cmd [| "--name=" ^ filename; "-" |] in
let pid =
Unix.create_process_env catala_exe cmd env cmd_in_rd cmd_out_wr cmd_out_wr
Unix.create_process_env cmd.(0) cmd env cmd_in_rd cmd_out_wr cmd_out_wr
in
Unix.close cmd_in_rd;
Unix.close cmd_out_wr;
@ -91,21 +119,36 @@ let run_catala_test test_flags catala_exe catala_opts file program args oc =
let out_lines =
Seq.of_dispenser (fun () -> In_channel.input_line command_ic)
in
Seq.iter
(fun line ->
output_string oc (sanitize line);
output_char oc '\n')
out_lines;
let success, expected =
Seq.fold_left
(fun (success, expected) result_line ->
let result_line = sanitize result_line ^ "\n" in
out_line result_line;
match Seq.uncons expected with
| Some (l, expected) -> success && String.equal result_line l, expected
| None -> false, expected)
(true, expected) out_lines
in
let return_code =
match Unix.waitpid [] pid with
| _, Unix.WEXITED n -> n
| _, (Unix.WSIGNALED n | Unix.WSTOPPED n) -> 128 - n
in
if return_code <> 0 then Printf.fprintf oc "#return code %d#\n" return_code
let success, expected =
if return_code = 0 then success, expected
else
let line = Printf.sprintf "#return code %d#\n" return_code in
out_line line;
match Seq.uncons expected with
| Some (l, expected) when String.equal l line -> success, expected
| Some (_, expected) -> false, expected
| None -> false, expected
in
success && Seq.is_empty expected
(** Directly runs the test (not using ninja, this will be called by ninja rules
through the "clerk runtest" command) *)
let run_inline_tests catala_exe catala_opts test_flags filename =
let run_tests ~catala_exe ~catala_opts ~test_flags ~report ~out filename =
let module L = Surface.Lexer_common in
let lang =
match Clerk_scan.get_lang filename with
@ -115,30 +158,83 @@ let run_inline_tests catala_exe catala_opts test_flags filename =
File.format filename
in
let lines = Surface.Parser_driver.lines filename lang in
let oc = stdout in
with_output out
@@ fun out ->
let lines_until_now = Queue.create () in
let push str =
output_string oc str;
let push_line str =
out_line out str;
Queue.add str lines_until_now
in
let rec run_test lines =
let rtests : Clerk_report.test list ref = ref [] in
let rec skip_block lines =
match Seq.uncons lines with
| Some ((l, tok, _), lines) ->
push_line l;
if tok = L.LINE_BLOCK_END then lines else skip_block lines
| None -> lines
in
let rec get_block acc lines =
let return lines acc =
let endpos =
match acc with
| (_, _, (_, epos)) :: _ -> epos
| [] -> { Lexing.dummy_pos with pos_fname = filename }
in
let block = List.rev acc in
let startpos =
match block with
| (_, _, (spos, _)) :: _ -> spos
| [] -> { Lexing.dummy_pos with pos_fname = filename }
in
lines, block, (startpos, endpos)
in
match Seq.uncons lines with
| None -> return lines acc
| Some ((_, L.LINE_BLOCK_END, _), lines) -> return lines acc
| Some (li, lines) -> get_block (li :: acc) lines
in
let broken_test msg =
let opos_start = out.pos in
push_line msg;
{
Clerk_report.success = false;
command_line = [];
expected =
( { Lexing.dummy_pos with pos_fname = filename },
{ Lexing.dummy_pos with pos_fname = filename } );
result = opos_start, out.pos;
}
in
let get_test_command lines =
match Seq.uncons lines with
| None ->
output_string oc
"[INVALID TEST] Missing test command, use '$ catala <args>'\n"
| Some ((str, L.LINE_BLOCK_END), lines) ->
output_string oc
"[INVALID TEST] Missing test command, use '$ catala <args>'\n";
push str;
process lines
| Some ((str, _), lines) -> (
push str;
let t =
broken_test
"[INVALID TEST] Missing test command, use '$ catala <args>'\n"
in
rtests := t :: !rtests;
None, lines
| Some ((str, L.LINE_BLOCK_END, _), lines) ->
let t =
broken_test
"[INVALID TEST] Missing test command, use '$ catala <args>'\n"
in
rtests := t :: !rtests;
push_line str;
None, lines
| Some ((str, _, _), lines) -> (
push_line str;
match Clerk_scan.test_command_args str with
| None ->
output_string oc
"[INVALID TEST] Invalid test command syntax, must match '$ catala \
<args>'\n";
skip_block lines
let t =
broken_test
"[INVALID TEST] Invalid test command syntax, must match '$ catala \
<args>'\n"
in
let lines, _, ipos = get_block [] lines in
push_line "```\n";
rtests := { t with Clerk_report.expected = ipos } :: !rtests;
None, lines
| Some args -> (
let args = String.split_on_char ' ' args in
let program =
@ -152,29 +248,107 @@ let run_inline_tests catala_exe catala_opts test_flags filename =
in
Queue.to_seq lines_until_now |> drop_last |> drop_last
in
let opos_start = out.pos in
match
run_catala_test test_flags catala_exe catala_opts filename program
args oc
catala_test_command test_flags catala_exe catala_opts args out
with
| () -> skip_block lines
| exception Exit -> process lines))
and skip_block lines =
match Seq.uncons lines with
| None -> ()
| Some ((str, L.LINE_BLOCK_END), lines) ->
push str;
| Some cmd -> Some (cmd, program, opos_start), lines
| None -> None, skip_block lines))
in
let rec run_inline_test lines =
match get_test_command lines with
| None, lines -> process lines
| Some (cmd, program, opos_start), lines ->
let lines, expected, ipos = get_block [] lines in
let expected = Seq.map (fun (s, _, _) -> s) (List.to_seq expected) in
let success = run_catala_test filename cmd program expected push_line in
let opos_end = out.pos in
push_line "```\n";
rtests :=
{
Clerk_report.success;
command_line = Array.to_list cmd @ [filename];
result = opos_start, opos_end;
expected = ipos;
}
:: !rtests;
process lines
and run_output_test id lines =
match get_test_command lines with
| None, lines -> process lines
| Some (cmd, program, _), lines ->
let lines = skip_block lines in
let ref_file =
File.((filename /../ "output" / Filename.basename filename) -.- id)
in
if not (Sys.file_exists ref_file) then
(* Create the file if it doesn't exist *)
File.with_out_channel ref_file ignore;
let output = ref_file ^ "@out" in
let ipos_start = pos0 ref_file in
let ipos_end = ref ipos_start in
let report =
File.with_in_channel ref_file
@@ fun ic ->
let expected =
Seq.of_dispenser (fun () ->
match In_channel.input_line ic with
| None -> None
| Some s ->
let s = s ^ "\n" in
let pos_cnum = !ipos_end.pos_cnum + String.length s in
ipos_end :=
{
!ipos_end with
Lexing.pos_cnum;
pos_lnum = !ipos_end.pos_lnum + 1;
pos_bol = pos_cnum;
};
Some s)
in
with_output (Some output)
@@ fun test_out ->
let opos_start = test_out.pos in
let success =
run_catala_test filename cmd program expected (out_line test_out)
in
Seq.iter ignore expected;
{
Clerk_report.success;
command_line = Array.to_list cmd @ [filename];
result = opos_start, test_out.pos;
expected = ipos_start, !ipos_end;
}
in
rtests := report :: !rtests;
process lines
| Some ((str, _), lines) ->
Queue.add str lines_until_now;
skip_block lines
and process lines =
match Seq.uncons lines with
| Some ((str, L.LINE_INLINE_TEST), lines) ->
push str;
run_test lines
| Some ((str, _), lines) ->
push str;
| Some ((str, L.LINE_INLINE_TEST, _), lines) ->
push_line str;
run_inline_test lines
| Some ((str, L.LINE_TEST id, _), lines) ->
push_line str;
run_output_test id lines
| Some ((str, _, _), lines) ->
push_line str;
process lines
| None -> ()
in
process lines
process lines;
let tests_report =
List.fold_left
Clerk_report.(
fun tests t ->
{
tests with
total = tests.total + 1;
successful = (tests.successful + if t.success then 1 else 0);
tests = t :: tests.tests;
})
{ Clerk_report.name = filename; successful = 0; total = 0; tests = [] }
!rtests
in
match report with
| Some file -> Clerk_report.write_to file tests_report
| None -> ()

View File

@ -22,7 +22,14 @@
open Catala_utils
val run_inline_tests : string -> string list -> string list -> File.t -> unit
(** [run_inline_tests catala_exe catala_opts test_flags file] runs the tests in
Catala [file] using the given path to the Catala executable and the provided
options. Output is printed to [stdout]. *)
val run_tests :
catala_exe:string ->
catala_opts:string list ->
test_flags:string list ->
report:File.t option ->
out:File.t option ->
File.t ->
unit
(** [run_tests ~catala_exe ~catala_opts ~test_flags ~report ~out file] runs the
tests in Catala [file] using the given path to the Catala executable and the
provided options. Output is printed to [stdout] if [out] is [None]. *)

View File

@ -60,10 +60,10 @@ let catala_file (file : File.t) (lang : Catala_utils.Global.backend_lang) : item
let rec parse lines n acc =
match Seq.uncons lines with
| None -> acc
| Some ((_, L.LINE_TEST id), lines) ->
| Some ((_, L.LINE_TEST id, _), lines) ->
let test, lines, n = parse_test id lines (n + 1) in
parse lines n { acc with legacy_tests = test :: acc.legacy_tests }
| Some ((_, line), lines) -> (
| Some ((_, line, _), lines) -> (
parse lines (n + 1)
@@
match line with
@ -88,7 +88,7 @@ let catala_file (file : File.t) (lang : Catala_utils.Global.backend_lang) : item
[Format.asprintf "'invalid test syntax at %a:%d'" File.format file n]
in
match Seq.uncons lines with
| Some ((str, L.LINE_ANY), lines) -> (
| Some ((str, L.LINE_ANY, _), lines) -> (
match test_command_args str with
| Some cmd ->
let cmd, lines, n = parse_block lines (n + 1) [cmd] in
@ -103,8 +103,8 @@ let catala_file (file : File.t) (lang : Catala_utils.Global.backend_lang) : item
| None -> { test with cmd = err n }, lines, n
and parse_block lines n acc =
match Seq.uncons lines with
| Some ((_, L.LINE_BLOCK_END), lines) -> List.rev acc, lines, n + 1
| Some ((str, _), lines) -> String.trim str :: acc, lines, n + 1
| Some ((_, L.LINE_BLOCK_END, _), lines) -> List.rev acc, lines, n + 1
| Some ((str, _, _), lines) -> String.trim str :: acc, lines, n + 1
| None -> List.rev acc, lines, n
in
parse

View File

@ -9,7 +9,7 @@
cmdliner
re
ocolor)
(modules clerk_scan clerk_runtest clerk_driver))
(modules clerk_scan clerk_report clerk_runtest clerk_driver))
(rule
(target custom_linking.sexp)

View File

@ -34,14 +34,14 @@ let unstyle_formatter ppf =
[Format.sprintf] etc. functions (ignoring them) *)
let () = ignore (unstyle_formatter Format.str_formatter)
let terminal_columns, set_terminal_width_function =
let get_cols = ref (fun () -> 80) in
(fun () -> !get_cols ()), fun f -> get_cols := f
(* Note: we could do the same for std_formatter, err_formatter... but we'd
rather promote the use of the formatting functions of this module and the
below std_ppf / err_ppf *)
let terminal_columns, set_terminal_width_function =
let get_cols = ref (fun () -> 80) in
(fun () -> !get_cols ()), fun f -> get_cols := f
let has_color_raw ~(tty : bool Lazy.t) =
match Global.options.color with
| Global.Never -> false

View File

@ -73,6 +73,7 @@ val unformat : (Format.formatter -> unit) -> string
val has_color : out_channel -> bool
val set_terminal_width_function : (unit -> int) -> unit
val terminal_columns : unit -> int
(* {1 More general color-enabled formatting helpers}*)

View File

@ -105,14 +105,6 @@ let indent_number (s : string) : int =
aux 0
with Invalid_argument _ -> String.length s
let string_repeat n s =
let slen = String.length s in
let buf = Bytes.create (n * slen) in
for i = 0 to n - 1 do
Bytes.blit_string s 0 buf (i * slen) slen
done;
Bytes.to_string buf
let utf8_byte_index s ui0 =
let rec aux bi ui =
if ui >= ui0 then bi
@ -200,13 +192,13 @@ let format_loc_text_parts (pos : t) =
Format.pp_print_cut ppf ();
if line_no >= sline && line_no <= eline then
Format.fprintf ppf "@{<blue>%s │@} %s@{<bold;red>%a@}"
(string_repeat nspaces " ")
(string_repeat match_start_col " ")
(String.repeat nspaces " ")
(String.repeat match_start_col " ")
(fun ppf -> Format.pp_print_as ppf match_num_cols)
(string_repeat match_num_cols "")
(String.repeat match_num_cols "")
in
let pr_context ppf =
Format.fprintf ppf "@{<blue> %s│@}@," (string_repeat nspaces " ");
Format.fprintf ppf "@{<blue> %s│@}@," (String.repeat nspaces " ");
Format.pp_print_list print_matched_line ppf pos_lines
in
let legal_pos_lines =

View File

@ -50,6 +50,14 @@ let remove_prefix ~prefix s =
sub s plen (length s - plen)
else s
let repeat n s =
let slen = length s in
let buf = Bytes.create (n * slen) in
for i = 0 to n - 1 do
Bytes.blit_string s 0 buf (i * slen) slen
done;
Bytes.to_string buf
(* Note: this should do, but remains incorrect for combined unicode characters
that display as one (e.g. `e` + postfix `'`). We should switch to Uuseg at
some poing *)

View File

@ -56,3 +56,6 @@ val width : string -> int
(** Returns the width of a given string in screen columns (assuming a monospace
font). Useful for alignment. This takes unicode (except composite chars) and
tabs into account, but not escape sequences. *)
val repeat : int -> string -> string
(** Repeats the given string the given number of times *)

View File

@ -300,12 +300,13 @@ let lines (file : File.t) (language : Global.backend_lang) =
Sedlexing.set_filename lexbuf file;
let rec aux () =
match lex_line lexbuf with
| Some line -> Seq.Cons (line, aux)
| Some (str, tok) ->
Seq.Cons ((str, tok, Sedlexing.lexing_bytes_positions lexbuf), aux)
| None ->
close_in input;
Seq.Nil
in
aux
Seq.once aux
with exc ->
let bt = Printexc.get_raw_backtrace () in
close_in input;

View File

@ -20,7 +20,9 @@
open Catala_utils
val lines :
File.t -> Global.backend_lang -> (string * Lexer_common.line_token) Seq.t
File.t ->
Global.backend_lang ->
(string * Lexer_common.line_token * (Lexing.position * Lexing.position)) Seq.t
(** Raw file parser that doesn't interpret any includes and returns the flat law
structure as is *)

View File

@ -52,31 +52,25 @@ int main()
{
char *error_kind;
switch (catala_fatal_error_raised.code)
{
case catala_assertion_failed:
error_kind = "an assertion doesn't hold";
{
case catala_no_value_provided:
error_kind = "No value provided";
break;
case catala_no_value:
error_kind = "no applicable rule to define this variable in this situation";
case catala_conflict:
error_kind = "Conflict between exceptions";
break;
case catala_conflict:
error_kind = "conflict between multiple valid consequences for assigning the same variable";
case catala_crash:
error_kind = "Crash";
break;
case catala_division_by_zero:
error_kind = "a value is being used as denominator in a division and it computed to zero";
case catala_empty:
error_kind = "Empty error not caught";
break;
case catala_not_same_length:
error_kind = "traversing multiple lists of different lengths";
case catala_assertion_failure:
error_kind = "Asssertion failure";
break;
case catala_uncomparable_durations:
error_kind = "ambiguous comparison between durations in different units (e.g. months vs. days)";
break;
case catala_indivisible_durations:
error_kind = "dividing durations that are not in days";
break;
case catala_malloc_error:
case catala_malloc_error:
error_kind = "Malloc error";
}
}
printf("\033[1;31m[ERROR]\033[0m %s in file %s:%d.%d-%d.%d\n",
error_kind,
catala_fatal_error_raised.position.filename,

View File

@ -24,10 +24,8 @@ scope S2:
definition a equals if r then Int content (number of sub.z) else Dec content 0.0
```
Should be "catala Typecheck", see test err3
```catala-test-inline
$ catala ocaml
$ catala typecheck
┌─[ERROR]─
│ Error during typechecking, incompatible types: