From 7f3862d3c71d6237b89df95a8ccbd247e5f9c997 Mon Sep 17 00:00:00 2001 From: Calascibetta Romain Date: Fri, 13 Oct 2023 14:50:07 +0200 Subject: [PATCH] Be able to allocate a formatter for stdout/stderr and use it instead of using OCaml's formatter - fix some data-race when some parallel tasks use the same formatter. This commit tries to solve an issue about OCaml 5 and parallelism. Alcotest uses OCaml's formatter which can be used by some libraries such as Logs. In this situation and if Logs is used into a parallel task, a data-race exists about the internal queue used by formatters. This commit allows the user to allocate its own formatter for stdout/stderr and give them to Alcotest. --- src/alcotest-engine/alcotest_engine.ml | 2 + src/alcotest-engine/alcotest_engine.mli | 2 + src/alcotest-engine/cli.ml | 41 +++++++++------- src/alcotest-engine/config.ml | 29 +++++++++++- src/alcotest-engine/config_intf.ml | 17 ++++++- src/alcotest-engine/core.ml | 62 +++++++++++++++++-------- src/alcotest-engine/core_intf.ml | 6 +++ src/alcotest-engine/global.ml | 15 ++++++ src/alcotest-engine/global.mli | 23 +++++++++ src/alcotest-engine/platform.ml | 10 ++-- src/alcotest-engine/pp.ml | 4 +- src/alcotest-engine/test.ml | 8 ++-- src/alcotest-mirage/alcotest_mirage.ml | 2 +- src/alcotest/alcotest.ml | 60 +++++++++++++++++++++--- 14 files changed, 226 insertions(+), 55 deletions(-) create mode 100644 src/alcotest-engine/global.ml create mode 100644 src/alcotest-engine/global.mli diff --git a/src/alcotest-engine/alcotest_engine.ml b/src/alcotest-engine/alcotest_engine.ml index 014c84a9..6d9163dc 100644 --- a/src/alcotest-engine/alcotest_engine.ml +++ b/src/alcotest-engine/alcotest_engine.ml @@ -10,3 +10,5 @@ module Platform = Platform module Private = struct module Pp = Pp end + +module Global = Global diff --git a/src/alcotest-engine/alcotest_engine.mli b/src/alcotest-engine/alcotest_engine.mli index 7aea4a6f..9a8595d1 100644 --- a/src/alcotest-engine/alcotest_engine.mli +++ b/src/alcotest-engine/alcotest_engine.mli @@ -54,3 +54,5 @@ module Platform = Platform module Private : sig module Pp = Pp end + +module Global = Global diff --git a/src/alcotest-engine/cli.ml b/src/alcotest-engine/cli.ml index 1d7b2e57..bf46e18c 100644 --- a/src/alcotest-engine/cli.ml +++ b/src/alcotest-engine/cli.ml @@ -86,7 +86,7 @@ module Make (P : Platform.MAKER) (M : Monad.S) : alcotest_columns; ] - let set_color = + let set_color stdout stderr = let env = Cmd.Env.info "ALCOTEST_COLOR" in let+ color_flag = let enum = [ ("auto", `Auto); ("always", `Ansi_tty); ("never", `None) ] in @@ -112,17 +112,20 @@ module Make (P : Platform.MAKER) (M : Monad.S) : Some `Ansi_tty with Not_found -> None) in - P.setup_std_outputs ?style_renderer () + P.setup_std_outputs ?style_renderer stdout stderr let default_cmd config args library_name tests = let and_exit = Config.User.and_exit config and record_backtrace = Config.User.record_backtrace config - and ci = Config.User.ci config in + and ci = Config.User.ci config + and stdout = Config.User.stdout config + and stderr = Config.User.stderr config in let exec_name = Filename.basename Sys.argv.(0) in let doc = "Run all the tests." in let term = - let+ () = set_color - and+ cli_config = Config.User.term ~and_exit ~record_backtrace ~ci + let+ () = set_color stdout stderr + and+ cli_config = + Config.User.term ~stdout ~stderr ~and_exit ~record_backtrace ~ci and+ args = args in let config = Config.User.(cli_config || config) in run_with_args' config library_name args tests @@ -130,31 +133,36 @@ module Make (P : Platform.MAKER) (M : Monad.S) : (term, Cmd.info exec_name ~doc ~envs) let test_cmd config args library_name tests = - let ci = Config.User.ci config in + let ci = Config.User.ci config + and stdout = Config.User.stdout config + and stderr = Config.User.stderr config in let doc = "Run a subset of the tests." in let term = - let+ () = set_color + let+ () = set_color stdout stderr and+ cli_config = - Config.User.term ~and_exit:true ~record_backtrace:true ~ci + Config.User.term ~stdout ~stderr ~and_exit:true ~record_backtrace:true + ~ci and+ args = args in let config = Config.User.(cli_config || config) in run_with_args' config library_name args tests in (term, Cmd.info "test" ~doc ~envs) - let list_cmd tests = + let list_cmd ~stdout ~stderr tests = let doc = "List all available tests." in - ( (let+ () = set_color in + ( (let+ () = set_color stdout stderr in list_tests tests), Cmd.info "list" ~doc ) let run_with_args' (type a) ~argv config name (args : a Term.t) (tl : a test list) = let ( >>= ) = M.bind in + let stdout = Config.User.stdout config + and stderr = Config.User.stderr config in let choices = List.map (fun (term, info) -> Cmd.v info term) - [ list_cmd tl; test_cmd config args name tl ] + [ list_cmd ~stdout ~stderr tl; test_cmd config args name tl ] in let and_exit = Config.User.and_exit config in let exit_or_return exit_code = @@ -172,11 +180,12 @@ module Make (P : Platform.MAKER) (M : Monad.S) : | Error (`Parse | `Term) -> exit_or_return Cmd.Exit.cli_error | Error `Exn -> exit Cmd.Exit.internal_error - let run_with_args ?and_exit ?verbose ?compact ?tail_errors ?quick_only - ?show_errors ?json ?filter ?log_dir ?bail ?record_backtrace ?ci ?argv = - Config.User.kcreate (run_with_args' ~argv) ?and_exit ?verbose ?compact - ?tail_errors ?quick_only ?show_errors ?json ?filter ?log_dir ?bail - ?record_backtrace ?ci + let run_with_args ?stdout ?stderr ?and_exit ?verbose ?compact ?tail_errors + ?quick_only ?show_errors ?json ?filter ?log_dir ?bail ?record_backtrace + ?ci ?argv = + Config.User.kcreate (run_with_args' ~argv) ?stdout ?stderr ?and_exit + ?verbose ?compact ?tail_errors ?quick_only ?show_errors ?json ?filter + ?log_dir ?bail ?record_backtrace ?ci let run = Config.User.kcreate (fun config ?argv name tl -> diff --git a/src/alcotest-engine/config.ml b/src/alcotest-engine/config.ml index 4ba58321..3f0b2fd2 100644 --- a/src/alcotest-engine/config.ml +++ b/src/alcotest-engine/config.ml @@ -253,10 +253,22 @@ module User = struct bail : Bail.t option; record_backtrace : Record_backtrace.t option; ci : CI.t option; + stdout : Global.stdout; + stderr : Global.stderr; } let ( || ) a b = let merge_on f = Option.(f a || f b) in + let stdout = + merge_on @@ fun t -> + if t.stdout == Global.ocaml_stdout then None else Some t.stdout + in + let stderr = + merge_on @@ fun t -> + if t.stderr == Global.ocaml_stderr then None else Some t.stderr + in + let stdout = Option.value ~default:Global.ocaml_stdout stdout in + let stderr = Option.value ~default:Global.ocaml_stderr stderr in { and_exit = merge_on (fun t -> t.and_exit); verbose = merge_on (fun t -> t.verbose); @@ -270,9 +282,11 @@ module User = struct bail = merge_on (fun t -> t.bail); record_backtrace = merge_on (fun t -> t.record_backtrace); ci = merge_on (fun t -> t.ci); + stdout; + stderr; } - let term ~and_exit ~record_backtrace ~ci = + let term ~stdout ~stderr ~and_exit ~record_backtrace ~ci = let+ verbose = Verbose.term and+ compact = Compact.term and+ tail_errors = Tail_errors.term @@ -295,12 +309,15 @@ module User = struct bail; record_backtrace = Some record_backtrace; ci = Some ci; + stdout; + stderr; } (* Lift a config-sensitive function to one that consumes optional arguments that override config defaults. *) let kcreate : 'a. (t -> 'a) -> 'a with_options = - fun f ?and_exit ?verbose ?compact ?tail_errors ?quick_only ?show_errors ?json + fun f ?(stdout = Global.ocaml_stdout) ?(stderr = Global.ocaml_stderr) + ?and_exit ?verbose ?compact ?tail_errors ?quick_only ?show_errors ?json ?filter ?log_dir ?bail ?record_backtrace ?ci -> f { @@ -316,6 +333,8 @@ module User = struct bail; record_backtrace; ci; + stdout; + stderr; } let create : (unit -> t) with_options = kcreate (fun t () -> t) @@ -325,6 +344,8 @@ module User = struct Option.value ~default:Record_backtrace.default t.record_backtrace let ci t = Option.value ~default:CI.default t.ci + let stdout t = t.stdout + let stderr t = t.stderr end let apply_defaults ~default_log_dir : User.t -> t = @@ -341,6 +362,8 @@ let apply_defaults ~default_log_dir : User.t -> t = bail; record_backtrace; ci; + stdout; + stderr; } -> let open Key in object (self) @@ -365,4 +388,6 @@ let apply_defaults ~default_log_dir : User.t -> t = Option.value ~default:Record_backtrace.default record_backtrace method ci = Option.value ~default:CI.default ci + method stdout = stdout + method stderr = stderr end diff --git a/src/alcotest-engine/config_intf.ml b/src/alcotest-engine/config_intf.ml index adfdf79e..db1ee7be 100644 --- a/src/alcotest-engine/config_intf.ml +++ b/src/alcotest-engine/config_intf.ml @@ -1,4 +1,6 @@ module Types = struct + type stdout = Global.stdout + type stderr = Global.stderr type bound = [ `Unlimited | `Limit of int ] type filter = name:string -> index:int -> [ `Run | `Skip ] @@ -6,7 +8,9 @@ module Types = struct (** All supported Continuous Integration (CI) systems. *) type t = - < and_exit : bool + < stdout : stdout + ; stderr : stderr + ; and_exit : bool ; verbose : bool ; compact : bool ; tail_errors : bound @@ -20,6 +24,8 @@ module Types = struct ; ci : ci > type 'a with_options = + ?stdout:stdout -> + ?stderr:stderr -> ?and_exit:bool -> ?verbose:bool -> ?compact:bool -> @@ -51,7 +57,12 @@ module type Config = sig rather than returning directly. *) val term : - and_exit:bool -> record_backtrace:bool -> ci:ci -> t Cmdliner.Term.t + stdout:Global.stdout -> + stderr:Global.stderr -> + and_exit:bool -> + record_backtrace:bool -> + ci:ci -> + t Cmdliner.Term.t (** [term] provides a command-line interface for building configs. *) val ( || ) : t -> t -> t @@ -63,6 +74,8 @@ module type Config = sig val and_exit : t -> bool val record_backtrace : t -> bool val ci : t -> ci + val stdout : t -> Global.stdout + val stderr : t -> Global.stderr end val apply_defaults : default_log_dir:string -> User.t -> t diff --git a/src/alcotest-engine/core.ml b/src/alcotest-engine/core.ml index e558fcb4..afc597e5 100644 --- a/src/alcotest-engine/core.ml +++ b/src/alcotest-engine/core.ml @@ -28,7 +28,8 @@ let () = lazy (let buf = Buffer.create 0 in let ppf = Format.formatter_of_buffer buf in - Fmt.set_style_renderer ppf Fmt.(style_renderer stderr); + Fmt.set_style_renderer ppf + Fmt.(style_renderer (Global.get_stderr () :> Format.formatter)); fun error -> Fmt.pf ppf "Alcotest assertion failure@.%a@." error (); let contents = Buffer.contents buf in @@ -77,6 +78,8 @@ module Make (P : Platform.MAKER) (M : Monad.S) = struct config : Config.t; run_id : string; log_trap : Log_trap.t; + stdout : Global.stdout; + stderr : Global.stderr; } let gen_run_id = @@ -108,7 +111,9 @@ module Make (P : Platform.MAKER) (M : Monad.S) = struct Log_trap.active ~root:config#log_dir ~uuid:run_id ~suite_name:(Suite.name suite) in - { suite; errors; max_label; config; run_id; log_trap } + let stdout = config#stdout in + let stderr = config#stderr in + { suite; errors; max_label; config; run_id; log_trap; stdout; stderr } let compare_speed_level s1 s2 = match (s1, s2) with @@ -200,7 +205,7 @@ module Make (P : Platform.MAKER) (M : Monad.S) = struct (* When capturing the logs of a test, also add the result of the test at the end. *) let+ result = fn args in - Pp.rresult_error Fmt.stdout result; + Pp.rresult_error (t.stdout :> Format.formatter) result; result) () @@ -210,7 +215,8 @@ module Make (P : Platform.MAKER) (M : Monad.S) = struct let print_event = pp_event t ~prior_error:(Option.is_some first_error) - ~tests_so_far ~isatty:(P.stdout_isatty ()) Fmt.stdout + ~tests_so_far ~isatty:(P.stdout_isatty ()) + (t.stdout :> Format.formatter) in let* () = M.return () in print_event (`Start test.name); @@ -218,7 +224,8 @@ module Make (P : Platform.MAKER) (M : Monad.S) = struct match test.fn with | `Skip -> M.return (`Skip, false) | `Run fn -> - Fmt.(flush stdout) () (* Show event before any test stderr *); + Fmt.(flush (t.stdout :> Format.formatter)) + () (* Show event before any test stderr *); let+ result = with_captured_logs t test.name fn args in (* Store errors *) let errored : bool = @@ -232,8 +239,8 @@ module Make (P : Platform.MAKER) (M : Monad.S) = struct errored in (* Show any remaining test output before the event *) - Fmt.(flush stdout ()); - Fmt.(flush stderr ()); + Fmt.(flush (t.stdout :> Format.formatter) ()); + Fmt.(flush (t.stderr :> Format.formatter) ()); (result, errored) in print_event (`Result (test.name, result)); @@ -263,7 +270,7 @@ module Make (P : Platform.MAKER) (M : Monad.S) = struct if currently_bailing state then match state.tests_so_far - Option.get_exn state.first_error - 1 with | n when n > 0 -> - Fmt.pr "@\n %a@\n" + Global.pr "@\n %a@\n" Fmt.(styled `Faint string) (Fmt.str "... with %d subsequent test%a skipped." n Pp.pp_plural n) | 0 -> () @@ -313,7 +320,9 @@ module Make (P : Platform.MAKER) (M : Monad.S) = struct Suite.tests t.suite |> List.map (fun t -> t.Suite.name) |> List.sort Test_name.compare - |> Fmt.(list ~sep:(const string "\n") (pp_info t) stdout) + |> Fmt.( + list ~sep:(const string "\n") (pp_info t) + (t.stdout :> Format.formatter)) let register (type a) (t : a t) (name, (ts : a test_case list)) : a t = let max_label = max t.max_label (String.length_utf8 name) in @@ -348,8 +357,13 @@ module Make (P : Platform.MAKER) (M : Monad.S) = struct let is_empty = filter_test_cases ~subst:false filter suite = [] in let+ result = if is_empty && Option.is_some filter then ( + (* NOTE(dinosaure): [Stdlib.flush_all] is really deep in OCaml and try to flush + all opened file descriptors (including [1] and [2]). Even if the user create + its own [Format.formatter], if it uses a file-descriptor, it will be flushed + too. We don't need to register a channel even if the user specify its own + [Format.formatter] for [stdout] and/or [stderr]. *) flush_all (); - Fmt.(pf stderr) + Fmt.(pf (t.stderr :> Format.formatter)) "%a\n" red "Invalid request (no tests to run, filter skipped everything)!"; exit 1) @@ -357,7 +371,7 @@ module Make (P : Platform.MAKER) (M : Monad.S) = struct let tests = filter_test_cases ~subst:true filter suite in result t tests args in - (pp_suite_results t) Fmt.stdout result; + (pp_suite_results t) (t.stdout :> Format.formatter) result; result.failures let default_log_dir () = @@ -387,21 +401,30 @@ module Make (P : Platform.MAKER) (M : Monad.S) = struct in let t = empty ~config ~trap_logs:(not config#verbose) ~suite_name:name in let t = register_all t tl in + let stdout' = Global.get_stdout () in + let stderr' = Global.get_stderr () in + Global.set_stdout t.stdout; + Global.set_stderr t.stderr; let+ test_failures = (* Only print inside the concurrency monad *) let* () = M.return () in let open Fmt in if config#ci = `Github_actions then - pr "::group::{%a}\n" Suite.pp_name t.suite; - pr "Testing %a.@," (Pp.quoted Fmt.(styled `Bold Suite.pp_name)) t.suite; - pr "@[%a@]" + Global.pr "::group::{%a}\n" Suite.pp_name t.suite; + Global.pr "Testing %a.@," + (Pp.quoted Fmt.(styled `Bold Suite.pp_name)) + t.suite; + Global.pr "@[%a@]" (styled `Faint (fun ppf () -> pf ppf "This run has ID %a.@,@," (Pp.quoted string) t.run_id)) (); let r = run_tests t () args in - if config#ci = `Github_actions then pr "::endgroup::\n"; + if config#ci = `Github_actions then Global.pr "::endgroup::\n"; r in + at_exit (Format.pp_print_flush (Global.get_stderr () :> Format.formatter)); + Global.set_stdout stdout'; + Global.set_stderr stderr'; match (test_failures, t.config#and_exit) with | 0, true -> exit 0 | 0, false -> () @@ -410,11 +433,12 @@ module Make (P : Platform.MAKER) (M : Monad.S) = struct let run' config name (tl : unit test list) = run_with_args' config name () tl - let run_with_args ?and_exit ?verbose ?compact ?tail_errors ?quick_only - ?show_errors ?json ?filter ?log_dir ?bail ?record_backtrace ?ci = - Config.User.kcreate run_with_args' ?and_exit ?verbose ?compact ?tail_errors + let run_with_args ?stdout ?stderr ?and_exit ?verbose ?compact ?tail_errors ?quick_only ?show_errors ?json ?filter ?log_dir ?bail ?record_backtrace - ?ci + ?ci = + Config.User.kcreate run_with_args' ?stdout ?stderr ?and_exit ?verbose + ?compact ?tail_errors ?quick_only ?show_errors ?json ?filter ?log_dir + ?bail ?record_backtrace ?ci let run = Config.User.kcreate run' end diff --git a/src/alcotest-engine/core_intf.ml b/src/alcotest-engine/core_intf.ml index fc8b642d..16c2bda6 100644 --- a/src/alcotest-engine/core_intf.ml +++ b/src/alcotest-engine/core_intf.ml @@ -46,6 +46,8 @@ module V1_types = struct used for filtering which tests to run on the CLI. *) type 'a with_options = + ?stdout:Global.stdout -> + ?stderr:Global.stderr -> ?and_exit:bool -> ?verbose:bool -> ?compact:bool -> @@ -62,6 +64,10 @@ module V1_types = struct (** The various options taken by the tests runners {!run} and {!run_with_args}: + - [stdout] (default to [Fmt.stdout]). The formatter used to print on the + standard output. + - [stderr] (default to [Fmt.stderr]). The formatter used to print ont + the standard error output. - [and_exit] (default [true]). Once the tests have completed, exit with return code [0] if all tests passed, otherwise [1]. - [verbose] (default [false]). Display the test std.out and std.err diff --git a/src/alcotest-engine/global.ml b/src/alcotest-engine/global.ml new file mode 100644 index 00000000..e863a1af --- /dev/null +++ b/src/alcotest-engine/global.ml @@ -0,0 +1,15 @@ +type stdout = Format.formatter +type stderr = Format.formatter + +let stdout = ref Fmt.stdout +let stderr = ref Fmt.stderr +let set_stdout stdout' = stdout := stdout' +let set_stderr stderr' = stderr := stderr' +let get_stdout () = !stdout +let get_stderr () = !stderr +let ocaml_stdout = Format.std_formatter +let ocaml_stderr = Format.err_formatter +let make_stdout () = Format.formatter_of_out_channel Stdlib.stdout +let make_stderr () = Format.formatter_of_out_channel Stdlib.stderr +let pr fmt = Format.fprintf !stdout fmt +let epr fmt = Format.fprintf !stderr fmt diff --git a/src/alcotest-engine/global.mli b/src/alcotest-engine/global.mli new file mode 100644 index 00000000..3c9c8349 --- /dev/null +++ b/src/alcotest-engine/global.mli @@ -0,0 +1,23 @@ +(** Alcotest uses [Format.std_formatter] and [Format.err_formatter] formatters. + However, in a parallel context (OCaml 5 and domains), using these values in + parallel can lead to {i data-races}, since these values are not + {i domains-safe}. As such, Alcotest offers a way to create your own + formatter equivalent in behavior to [Format.std_formatter] and + [Format.err_formatter] (i.e. they write well on [1] and [2]) but they can be + used without risk even if another library (such as [Logs]) uses + [Format.std_formatter] and/or [Format.err_formatter] and is used in + parallel. *) + +type stdout = private Format.formatter +type stderr = private Format.formatter + +val set_stdout : stdout -> unit +val set_stderr : stderr -> unit +val get_stdout : unit -> stdout +val get_stderr : unit -> stderr +val ocaml_stdout : stdout +val ocaml_stderr : stderr +val make_stdout : unit -> stdout +val make_stderr : unit -> stderr +val pr : ('a, Format.formatter, unit) format -> 'a +val epr : ('a, Format.formatter, unit) format -> 'a diff --git a/src/alcotest-engine/platform.ml b/src/alcotest-engine/platform.ml index 70c28496..cd537302 100644 --- a/src/alcotest-engine/platform.ml +++ b/src/alcotest-engine/platform.ml @@ -20,9 +20,13 @@ module type S = sig if no width can be determined (e.g. [stdout] is not a TTY). *) val setup_std_outputs : - ?style_renderer:Fmt.style_renderer -> ?utf_8:bool -> unit -> unit - (** [setup_std_outputs ~style_renderer ~utf_8 ()] is called at startup of - alcotest and sets up the standard streams for colored output. *) + ?style_renderer:Fmt.style_renderer -> + ?utf_8:bool -> + Global.stdout -> + Global.stderr -> + unit + (** [setup_std_outputs ~style_renderer ~utf_8 stdout stderr] is called at + startup of alcotest and sets up the standard streams for colored output. *) val log_trap_supported : bool (** Whether or not the test runner should trap test logs. The following diff --git a/src/alcotest-engine/pp.ml b/src/alcotest-engine/pp.ml index d5271db4..156b2d35 100644 --- a/src/alcotest-engine/pp.ml +++ b/src/alcotest-engine/pp.ml @@ -212,7 +212,7 @@ struct | 0 -> green_s ppf "Test Successful" | n -> red ppf "%d failure%a!" n pp_plural n in - Fmt.pf ppf "%a in %.3fs. %d test%a run.@," pp_failures r.failures r.time + Fmt.pf ppf "%a in %.3fs. %d test%a run.@\n" pp_failures r.failures r.time r.success pp_plural r.success let suite_results ~log_dir config ppf r = @@ -241,6 +241,6 @@ struct Format.pp_close_box ppf () let user_error msg = - Fmt.epr "%a: %s\n" Fmt.(styled `Red string) "ERROR" msg; + Global.epr "%a: %s\n" Fmt.(styled `Red string) "ERROR" msg; exit 1 end diff --git a/src/alcotest-engine/test.ml b/src/alcotest-engine/test.ml index 912e0be5..4d1b6c98 100644 --- a/src/alcotest-engine/test.ml +++ b/src/alcotest-engine/test.ml @@ -151,8 +151,11 @@ let reject (type a) = let show_assert = function | "" -> () | msg -> - Fmt.(flush stdout) () (* Flush any test stdout preceding the assert *); - Format.eprintf "%a %s\n%!" Pp.tag `Assert msg + Fmt.(flush (Global.get_stdout () :> Format.formatter)) + () (* Flush any test stdout preceding the assert *); + Format.fprintf + (Global.get_stderr () :> Format.formatter) + "%a %s\n%!" Pp.tag `Assert msg let check_err fmt = raise (Core.Check_error fmt) @@ -253,4 +256,3 @@ let match_raises ?here ?pos msg exnp f = msg Fmt.exn e) let skip () = raise Core.Skip -let () = at_exit (Format.pp_print_flush Format.err_formatter) diff --git a/src/alcotest-mirage/alcotest_mirage.ml b/src/alcotest-mirage/alcotest_mirage.ml index 3d14f797..ac288093 100644 --- a/src/alcotest-mirage/alcotest_mirage.ml +++ b/src/alcotest-mirage/alcotest_mirage.ml @@ -4,7 +4,7 @@ module Make (C : Mirage_clock.MCLOCK) = struct let getcwd () = "" let stdout_isatty () = true let stdout_columns () = None - let setup_std_outputs ?style_renderer:_ ?utf_8:_ () = () + let setup_std_outputs ?style_renderer:_ ?utf_8:_ _stdout _stderr = () (* Pre-4.07 doesn't support empty variant types. *) type file_descriptor = { empty : 'a. 'a } diff --git a/src/alcotest/alcotest.ml b/src/alcotest/alcotest.ml index 631c8006..b9d66936 100644 --- a/src/alcotest/alcotest.ml +++ b/src/alcotest/alcotest.ml @@ -97,16 +97,62 @@ module Unix_platform (M : Alcotest_engine.Monad.S) = struct let with_redirect fd_file fn = let* () = M.return () in - Fmt.(flush stdout) (); - Fmt.(flush stderr) (); - before_test ~output:fd_file ~stdout ~stderr; + Fmt.flush (Alcotest_engine.Global.get_stdout () :> Format.formatter) (); + Fmt.flush (Alcotest_engine.Global.get_stderr () :> Format.formatter) (); + before_test ~output:fd_file ~stdout:Stdlib.stdout ~stderr:Stdlib.stderr; let+ r = try fn () >|= fun o -> `Ok o with e -> M.return @@ `Error e in - Fmt.(flush stdout ()); - Fmt.(flush stderr ()); - after_test ~stdout ~stderr; + Fmt.flush (Alcotest_engine.Global.get_stdout () :> Format.formatter) (); + Fmt.flush (Alcotest_engine.Global.get_stderr () :> Format.formatter) (); + after_test ~stdout:Stdlib.stdout ~stderr:Stdlib.stderr; match r with `Ok x -> x | `Error e -> raise e - let setup_std_outputs = Fmt_tty.setup_std_outputs + let contains s1 s2 = + let exception Found in + try + let len = String.length s2 in + for i = 0 to String.length s1 - len do + if String.sub s1 i len = s2 then raise_notrace Found + done; + false + with Found -> true + + let setup_std_outputs ?style_renderer ?utf_8 + (stdout : Alcotest_engine.Global.stdout) + (stderr : Alcotest_engine.Global.stderr) = + let style_renderer oc = + match style_renderer with + | Some value -> value + | None -> + let dumb = + match Sys.getenv "TERM" with + | "dumb" | "" -> true + | _ -> false + | exception _ -> true + in + let is_a_tty = + try Unix.(isatty (descr_of_out_channel oc)) + with Unix.Unix_error _ -> false + in + if (not dumb) && is_a_tty then `Ansi_tty else `None + in + let utf_8 = + match utf_8 with + | Some value -> value + | None -> + let has_utf_8 var = + try contains "UTF-8" (String.uppercase_ascii (Sys.getenv var)) + with Not_found -> false + in + has_utf_8 "LANG" || has_utf_8 "LC_ALL" || has_utf_8 "LC_CTYPE" + in + Fmt.set_style_renderer + (stdout :> Format.formatter) + (style_renderer Stdlib.stdout); + Fmt.set_utf_8 (stdout :> Format.formatter) utf_8; + Fmt.set_style_renderer + (stderr :> Format.formatter) + (style_renderer Stdlib.stderr); + Fmt.set_utf_8 (stderr :> Format.formatter) utf_8 (* Implementation similar to that of [Bos.Os.Dir]. *) let home_directory () =