diff --git a/.gitlab/ci/templates.yml b/.gitlab/ci/templates.yml index fae4c965236c34d99271fadf60a216416b363da2..ee3691c73f5077914ab8375dd2a91ccd700024cf 100644 --- a/.gitlab/ci/templates.yml +++ b/.gitlab/ci/templates.yml @@ -2,7 +2,7 @@ variables: # /!\ CI_REGISTRY is overriden to use a private Docker registry mirror in AWS ECR # in GitLab namespaces `nomadic-labs` and `tezos` ## This value MUST be the same as `opam_repository_tag` in `scripts/version.sh` - build_deps_image_version: 9dada6519f5e523101f5e1feefd91db849e26f1c + build_deps_image_version: 8bab954749485fb55f75a4d493b04f858ab48481 build_deps_image_name: "${CI_REGISTRY}/tezos/opam-repository" GIT_STRATEGY: fetch GIT_DEPTH: "1" diff --git a/manifest/main.ml b/manifest/main.ml index 01a1b49ccde63fe7cae3914e51be3e8a0a8f6b93..399bfce67369053bad8020e88b9c673131d93004 100644 --- a/manifest/main.ml +++ b/manifest/main.ml @@ -184,6 +184,18 @@ let irmin_pack_unix = external_sublib irmin_pack "irmin-pack.unix" let irmin_pack_mem = external_sublib irmin_pack "irmin-pack.mem" +let js_of_ocaml = + external_lib + ~js_compatible:true + "js_of_ocaml" + V.(at_least "4.0.0" && less_than "5.0.0") + +let js_of_ocaml_lwt = + external_lib + ~js_compatible:true + "js_of_ocaml-lwt" + V.(at_least "4.0.0" && less_than "5.0.0") + let json_data_encoding = external_lib ~js_compatible:true @@ -320,6 +332,9 @@ let tls = external_lib "tls" V.(at_least "0.10") let unix = external_lib ~opam:"base-unix" "unix" V.True +(* Declare that we depend on the JS-compatible part of unix. *) +let unix_js = external_lib ~opam:"base-unix" "unix" ~js_compatible:true V.True + let uri = external_lib ~js_compatible:true "uri" V.(at_least "2.2.0") let utop = external_lib "utop" V.(at_least "2.8") @@ -2930,6 +2945,15 @@ let octez_shell_benchmarks = ] ~linkall:true +let tezt_core_lib = + public_lib + "tezt.core" + ~path:"tezt/lib_core" + ~ocaml:V.(at_least "4.12") + ~bisect_ppx:false + ~js_compatible:true + ~deps:[re; lwt; unix_js; ezjsonm] + let tezt_lib = public_lib "tezt" @@ -2940,13 +2964,28 @@ let tezt_lib = ~opam_doc:"https://tezos.gitlab.io/api/odoc/_html/tezt/Tezt/index.html" ~ocaml:V.(at_least "4.12") ~bisect_ppx:false - ~deps:[re; lwt_unix; ezjsonm] + ~deps:[re; lwt_unix; ezjsonm; tezt_core_lib |> open_] + +let tezt_js_lib = + public_lib + "tezt.js" + ~path:"tezt/lib_js" + ~ocaml:V.(at_least "4.12") + ~js_compatible:true + ~bisect_ppx:false + ~optional:true + ~deps:[re; ezjsonm; js_of_ocaml; js_of_ocaml_lwt; tezt_core_lib |> open_] -let tezt ~opam ~path ?(deps = []) ?dep_globs l = +let tezt ~opam ~path ?js_compatible ?modes ?(deps = []) ?dep_globs ?synopsis l = tezt_without_tezt_lib_dependency ~opam ~path - ~deps:((tezt_lib |> open_ |> open_ ~m:"Base") :: deps) + ?synopsis + ?js_compatible + ?modes + ~lib_deps:((tezt_core_lib |> open_ |> open_ ~m:"Base") :: deps) + ~exe_deps:[tezt_lib] + ~js_deps:[tezt_js_lib] ?dep_globs l diff --git a/manifest/manifest.ml b/manifest/manifest.ml index e5650fccf7c174235e1e115d56f4a0912529233e..f441894a179b2c75136fa79dc2ad463404a01564 100644 --- a/manifest/manifest.ml +++ b/manifest/manifest.ml @@ -1655,23 +1655,52 @@ type release = {version : string; url : Opam.url} type tezt_target = { opam : string; - deps : target list; + lib_deps : target list; + exe_deps : target list; + js_deps : target list; dep_globs : string list; modules : string list; + js_compatible : bool option; + modes : Dune.mode list option; + synopsis : string option; } let tezt_targets_by_path : tezt_target String_map.t ref = ref String_map.empty -let tezt ~opam ~path ?(deps = []) ?(dep_globs = []) modules = +let tezt ~opam ~path ?js_compatible ?modes ?(lib_deps = []) ?(exe_deps = []) + ?(js_deps = []) ?(dep_globs = []) ?synopsis modules = if String_map.mem path !tezt_targets_by_path then invalid_arg ("cannot call Manifest.tezt twice for the same directory: " ^ path) ; - let tezt_target = {opam; deps; dep_globs; modules} in + let tezt_target = + { + opam; + lib_deps; + exe_deps; + js_deps; + dep_globs; + modules; + js_compatible; + modes; + synopsis; + } + in tezt_targets_by_path := String_map.add path tezt_target !tezt_targets_by_path let register_tezt_targets ~make_tezt_exe = let tezt_test_libs = ref [] in - let register_path path {opam; deps; dep_globs; modules} = + let register_path path + { + opam; + lib_deps; + exe_deps; + js_deps; + dep_globs; + modules; + js_compatible; + modes; + synopsis; + } = let path_with_underscores = String.map (function '-' | '/' -> '_' | c -> c) path in @@ -1682,38 +1711,57 @@ let register_tezt_targets ~make_tezt_exe = (path_with_underscores ^ "_tezt_lib") ~path ~opam:"" - ~deps + ?js_compatible + ~deps:lib_deps ~modules ~linkall:true in tezt_test_libs := lib :: !tezt_test_libs ; - let exe_name = "main" in - let _exe = - (* Alias is "runtezt" and not "runtest" to make sure that the test is - not run in the CI twice (once with [dune @src/.../runtest] and once - with [dune exec tezt/tests/main.exe]). *) - Target.test - exe_name - ~alias:"runtezt" - ~path - ~opam - ~deps:[lib] - ~dep_globs - ~modules:[exe_name] - ~dune: - Dune. - [ - targets_rule - [exe_name ^ ".ml"] - ~action: - [ - S "with-stdout-to"; - S "%{targets}"; - [S "echo"; S "let () = Tezt.Test.run ()"]; - ]; - ] - in - () + let declare_exe ?js_compatible exe_name modes deps main = + let (_ : Target.t option) = + Target.test + exe_name + ~alias:"runtezt" + ~path + ~opam + ?synopsis + ?js_compatible + ?modes + ~deps:(lib :: deps) + ~dep_globs + ~modules:[exe_name] + ~dune: + Dune. + [ + targets_rule + [exe_name ^ ".ml"] + ~action: + [ + S "with-stdout-to"; + S "%{targets}"; + [S "echo"; S ("let () = " ^ main ^ ".Test.run ()")]; + ]; + ] + in + () + in + match modes with + | None -> declare_exe "main" None exe_deps "Tezt" + | Some modes -> + (match + List.filter + (function Dune.Byte | Native -> true | JS -> false) + modes + with + | [] -> () + | modes -> declare_exe "main" (Some modes) exe_deps "Tezt") ; + if List.mem Dune.JS modes then + declare_exe + "main_js" + (Some [JS]) + js_deps + "Tezt_js" + ~js_compatible:true in String_map.iter register_path !tezt_targets_by_path ; make_tezt_exe !tezt_test_libs diff --git a/manifest/manifest.mli b/manifest/manifest.mli index d099b4754fafc6b0e93b7c7e5404a205f2b0c8d4..369a428c7c8da8f264726c9aa5733bcd15747eb6 100644 --- a/manifest/manifest.mli +++ b/manifest/manifest.mli @@ -830,8 +830,13 @@ val tests : val tezt : opam:string -> path:string -> - ?deps:target list -> + ?js_compatible:bool -> + ?modes:Dune.mode list -> + ?lib_deps:target list -> + ?exe_deps:target list -> + ?js_deps:target list -> ?dep_globs:string list -> + ?synopsis:string -> string list -> unit diff --git a/opam/tezt.opam b/opam/tezt.opam index db6b729a586e5d8e1fd5eeb9522939ad4944599c..3d74cbe3c4f32029966773d5aa42e611892f4b4e 100644 --- a/opam/tezt.opam +++ b/opam/tezt.opam @@ -10,11 +10,20 @@ dev-repo: "git+https://gitlab.com/tezos/tezos.git" license: "MIT" depends: [ "dune" { >= "3.0" } - "ocaml" { >= "4.12" } + "ocaml" { >= "4.12" & >= "4.12" & >= "4.12" } "re" { >= "1.7.2" } "lwt" { >= "5.6.0" } + "base-unix" "ezjsonm" { >= "1.1.0" } ] +depopts: [ + "js_of_ocaml" + "js_of_ocaml-lwt" +] +conflicts: [ + "js_of_ocaml" { ! (>= "4.0.0" & < "5.0.0") } + "js_of_ocaml-lwt" { ! (>= "4.0.0" & < "5.0.0") } +] build: [ ["rm" "-r" "vendors"] ["dune" "build" "-p" name "-j" jobs] diff --git a/scripts/update_opam_repo.sh b/scripts/update_opam_repo.sh index e4b8839245563111cc28d912f46c1e593e34b24e..7c7c4376eb1ff98e1459f99ecd955d76694e434e 100755 --- a/scripts/update_opam_repo.sh +++ b/scripts/update_opam_repo.sh @@ -123,7 +123,7 @@ case $(opam --version) in esac #shellcheck disable=SC2086 OPAMSOLVERTIMEOUT=600 opam admin filter --yes --resolve \ - $packages,ocaml,ocaml-base-compiler,odoc,${opam_depext_dep}ledgerwallet-tezos,caqti-driver-postgresql,$dummy_pkg + $packages,ocaml,ocaml-base-compiler,odoc,${opam_depext_dep}ledgerwallet-tezos,caqti-driver-postgresql,js_of_ocaml-lwt,$dummy_pkg ## - ocaml-base-compiler has to be explicitely listed for the solver ## to not prefer the "variant" `system` of the compiler ## - odoc is used by the CI to generate the doc @@ -131,6 +131,8 @@ OPAMSOLVERTIMEOUT=600 opam admin filter --yes --resolve \ ## we want to have when building released binaries ## - caqti-driver-postgresq is needed by tps measurement software to ## read tezos-indexer databases +## - js_of_ocaml-lwt is an optional dependency of tezt which is needed +## to build tezt.js, and we do want to run some tests using nodejs ## Adding useful compiler variants for variant in afl flambda fp ; do diff --git a/scripts/version.sh b/scripts/version.sh index 6084be512ab635c1533f5fa9213597bf9b97653d..63f0db25faf82d5ce290718ddbeb13a3f371a5e3 100755 --- a/scripts/version.sh +++ b/scripts/version.sh @@ -25,7 +25,7 @@ export full_opam_repository_tag=b52ec3157016ea7c3c021bf6a099fc3adbe6ce9a ## opam_repository is an additional, tezos-specific opam repository. ## This value MUST be the same as `build_deps_image_version` in `.gitlab/ci/templates.ym export opam_repository_url=https://gitlab.com/tezos/opam-repository -export opam_repository_tag=9dada6519f5e523101f5e1feefd91db849e26f1c +export opam_repository_tag=8bab954749485fb55f75a4d493b04f858ab48481 export opam_repository_git=$opam_repository_url.git export opam_repository=$opam_repository_git\#$opam_repository_tag diff --git a/src/proto_014_PtKathma/lib_protocol/test/regression/dune b/src/proto_014_PtKathma/lib_protocol/test/regression/dune index 810763d05b67e901dc57aa9df6cddd9cd558d88b..571b80c8d26542d2147daefac3141130c76b3ecc 100644 --- a/src/proto_014_PtKathma/lib_protocol/test/regression/dune +++ b/src/proto_014_PtKathma/lib_protocol/test/regression/dune @@ -5,7 +5,7 @@ (name src_proto_014_PtKathma_lib_protocol_test_regression_tezt_lib) (instrumentation (backend bisect_ppx)) (libraries - tezt + tezt.core tezos-base tezt-tezos tezos-protocol-014-PtKathma @@ -16,8 +16,8 @@ (library_flags (:standard -linkall)) (flags (:standard) - -open Tezt - -open Tezt.Base + -open Tezt_core + -open Tezt_core.Base -open Tezos_base.TzPervasives -open Tezt_tezos -open Tezos_protocol_014_PtKathma @@ -30,7 +30,8 @@ (executable (name main) (libraries - src_proto_014_PtKathma_lib_protocol_test_regression_tezt_lib) + src_proto_014_PtKathma_lib_protocol_test_regression_tezt_lib + tezt) (modules main)) (rule diff --git a/src/proto_alpha/lib_protocol/test/regression/dune b/src/proto_alpha/lib_protocol/test/regression/dune index 99f68a5e614e6e31483bb911bc126cd45788bd31..73806cb913affc601ffded43e515e6b595b57efa 100644 --- a/src/proto_alpha/lib_protocol/test/regression/dune +++ b/src/proto_alpha/lib_protocol/test/regression/dune @@ -5,7 +5,7 @@ (name src_proto_alpha_lib_protocol_test_regression_tezt_lib) (instrumentation (backend bisect_ppx)) (libraries - tezt + tezt.core tezos-base tezt-tezos tezos-protocol-alpha @@ -16,8 +16,8 @@ (library_flags (:standard -linkall)) (flags (:standard) - -open Tezt - -open Tezt.Base + -open Tezt_core + -open Tezt_core.Base -open Tezos_base.TzPervasives -open Tezt_tezos -open Tezos_protocol_alpha @@ -30,7 +30,8 @@ (executable (name main) (libraries - src_proto_alpha_lib_protocol_test_regression_tezt_lib) + src_proto_alpha_lib_protocol_test_regression_tezt_lib + tezt) (modules main)) (rule diff --git a/tezt/lib/dune b/tezt/lib/dune index 13f6b233b7cb0de9109234cf28890ed89cb3ac96..6be84f09811803489247e5e51b011dfa50a9157e 100644 --- a/tezt/lib/dune +++ b/tezt/lib/dune @@ -7,4 +7,8 @@ (libraries re lwt.unix - ezjsonm)) + ezjsonm + tezt.core) + (flags + (:standard) + -open Tezt_core)) diff --git a/tezt/lib/main.ml b/tezt/lib/main.ml new file mode 100644 index 0000000000000000000000000000000000000000..7d4d642521ad31fbb575d21d02d212d7c17b7bc8 --- /dev/null +++ b/tezt/lib/main.ml @@ -0,0 +1,272 @@ +(*****************************************************************************) +(* *) +(* Open Source License *) +(* Copyright (c) 2022 Nomadic Labs *) +(* *) +(* Permission is hereby granted, free of charge, to any person obtaining a *) +(* copy of this software and associated documentation files (the "Software"),*) +(* to deal in the Software without restriction, including without limitation *) +(* the rights to use, copy, modify, merge, publish, distribute, sublicense, *) +(* and/or sell copies of the Software, and to permit persons to whom the *) +(* Software is furnished to do so, subject to the following conditions: *) +(* *) +(* The above copyright notice and this permission notice shall be included *) +(* in all copies or substantial portions of the Software. *) +(* *) +(* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR*) +(* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, *) +(* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL *) +(* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER*) +(* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING *) +(* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER *) +(* DEALINGS IN THE SOFTWARE. *) +(* *) +(*****************************************************************************) + +open Base + +(* [Unix.error_message] is not available in JS, so we register the printer here + instead of in [Base]. *) +let () = + Printexc.register_printer @@ function + | Unix.Unix_error (error, _, _) -> Some (Unix.error_message error) + | _ -> None + +module Scheduler : Test.SCHEDULER = struct + type request = Run_test of {test_title : string} + + type response = Test_result of Log.test_result + + type status = Idle | Working of (response -> unit) | Dead + + type worker = { + pid : int; + mutable status : status; + pipe_to_worker : out_channel; + pipe_from_worker : in_channel; + } + + let send_request channel request = + Marshal.to_channel channel (request : request) [] ; + flush channel + + let read_request channel = + try Some (Marshal.from_channel channel : request) with End_of_file -> None + + let send_response channel response = + Marshal.to_channel channel (response : response) [] ; + flush channel + + let read_response channel = + try Some (Marshal.from_channel channel : response) + with End_of_file -> None + + let internal_worker_error x = + Printf.ksprintf + (fun s -> + Log.error "internal error in worker: %s" s ; + exit 1) + x + + let internal_scheduler_error x = + Printf.ksprintf + (fun s -> + Log.error "internal error in scheduler: %s" s ; + exit 1) + x + + let perform_request (Run_test {test_title}) = + match Test.get_test_by_title test_title with + | None -> + internal_worker_error + "scheduler requested to run test %S, but worker doesn't know about \ + this test" + test_title + | Some test -> + let clean_up () = + Lwt.catch Process.clean_up @@ fun exn -> + Log.warn "Failed to clean up processes: %s" (Printexc.to_string exn) ; + unit + in + let test_result = + Lwt_main.run + @@ Test.run_one + ~sleep:Lwt_unix.sleep + ~clean_up + ~temp_start:Temp.start + ~temp_stop:Temp.stop + ~temp_clean_up:Temp.clean_up + test + in + Test_result test_result + + let rec worker_listen_loop pipe_from_scheduler pipe_to_scheduler = + let request = read_request pipe_from_scheduler in + match request with + | None -> + (* End of file: no more request will come. *) + exit 0 + | Some request -> + let response = perform_request request in + send_response pipe_to_scheduler response ; + worker_listen_loop pipe_from_scheduler pipe_to_scheduler + + let worker_listen pipe_from_scheduler pipe_to_scheduler = + try worker_listen_loop pipe_from_scheduler pipe_to_scheduler + with exn -> + (* Note: if a test fails, its exception is caught and handled by [really_run]. + So here we have an error of Tezt itself. *) + internal_worker_error "%s" (Printexc.to_string exn) + + let next_worker_id = ref 0 + + let current_worker_id = ref None + + let spawn_worker () = + let worker_id = !next_worker_id in + incr next_worker_id ; + let pipe_to_worker_exit, pipe_to_worker_entrance = Unix.pipe () in + let pipe_from_worker_exit, pipe_from_worker_entrance = Unix.pipe () in + let pid = Lwt_unix.fork () in + if pid = 0 then ( + (* This is now a worker process. *) + current_worker_id := Some worker_id ; + Temp.set_pid (Unix.getpid ()) ; + Unix.close pipe_to_worker_entrance ; + Unix.close pipe_from_worker_exit ; + worker_listen + (Unix.in_channel_of_descr pipe_to_worker_exit) + (Unix.out_channel_of_descr pipe_from_worker_entrance)) + else ( + (* This is the scheduler process. *) + Unix.close pipe_to_worker_exit ; + Unix.close pipe_from_worker_entrance ; + { + pid; + status = Idle; + pipe_to_worker = Unix.out_channel_of_descr pipe_to_worker_entrance; + pipe_from_worker = Unix.in_channel_of_descr pipe_from_worker_exit; + }) + + let kill_worker worker = + match worker.status with + | Dead -> () + | Idle | Working _ -> + worker.status <- Dead ; + close_out worker.pipe_to_worker ; + close_in worker.pipe_from_worker ; + Unix.kill worker.pid Sys.sigterm ; + let (_ : int * Unix.process_status) = Unix.waitpid [] worker.pid in + () + + let rec run_single_process ~on_worker_available = + Temp.set_pid (Unix.getpid ()) ; + match on_worker_available () with + | None -> () + | Some (request, on_response) -> + let response = perform_request request in + on_response response ; + run_single_process ~on_worker_available + + let run_multi_process ~on_worker_available ~worker_count = + (* Start workers. *) + let workers = List.init worker_count (fun _ -> spawn_worker ()) in + (* Handle Ctrl+C in the scheduler process. + Note: Ctrl+C is also received by workers automatically. *) + let received_sigint = ref false in + Sys.(set_signal sigint) + (Signal_handle + (fun _ -> + received_sigint := true ; + (* If the user presses Ctrl+C again, let the program die immediately. *) + Sys.(set_signal sigint) Signal_default)) ; + (* Give work to workers until there is no work to give. *) + let trigger_worker_available worker = + if !received_sigint then kill_worker worker + else + match on_worker_available () with + | None -> kill_worker worker + | Some (request, on_response) -> + worker.status <- Working on_response ; + send_request worker.pipe_to_worker request + in + let rec loop () = + (* Calling [trigger_worker_available] not only gives work to idle workers, + it also kills them if we don't need them any more. + It also ensures that [file_descriptors_to_read] will only be empty if there + are no working workers. *) + List.iter + (fun worker -> + match worker.status with + | Dead | Working _ -> () + | Idle -> trigger_worker_available worker) + workers ; + let file_descriptors_to_read = + List.filter_map + (fun worker -> + match worker.status with + | Idle | Dead -> None + | Working _ -> + Some (Unix.descr_of_in_channel worker.pipe_from_worker)) + workers + in + match file_descriptors_to_read with + | [] -> + (* We maintain the invariant that if there is work to do, at least one + worker is [Working] at this particular point. + This is enforced by the [List.iter] of [trigger_worker_available] above. + So if there is no working worker, we can stop the loop. *) + () + | _ :: _ -> + let ready, _, _ = + (* In case of SIGINT, this returns EINTR. *) + try Unix.select file_descriptors_to_read [] [] (-1.) + with Unix.Unix_error (EINTR, _, _) -> ([], [], []) + in + let read_response file_descriptor = + match + List.find_opt + (fun worker -> + match worker.status with + | Idle | Dead -> false + | Working _ -> + Unix.descr_of_in_channel worker.pipe_from_worker + = file_descriptor) + workers + with + | None -> + internal_scheduler_error + "received a response from an unknown worker" + | Some worker -> ( + match worker.status with + | Idle | Dead -> + (* Please do not consider this error message to be political. *) + internal_scheduler_error + "worker is idle or dead while it should be working" + | Working on_response -> ( + (* Note: [read_response] is blocking. + We assume that if a worker starts writing something, + it will finish writing almost immediately. *) + let response = read_response worker.pipe_from_worker in + match response with + | None -> internal_scheduler_error "no response from worker" + | Some response -> + on_response response ; + worker.status <- Idle)) + in + List.iter read_response ready ; + loop () + in + loop () + + let run ~on_worker_available ~worker_count continue = + (if worker_count = 1 then run_single_process ~on_worker_available + else + try run_multi_process ~on_worker_available ~worker_count + with exn -> internal_scheduler_error "%s" (Printexc.to_string exn)) ; + continue () + + let get_current_worker_id () = !current_worker_id +end + +let run () = Test.run_with_scheduler (module Scheduler) diff --git a/tezt/lib/main.mli b/tezt/lib/main.mli new file mode 100644 index 0000000000000000000000000000000000000000..ea98d250e5f0323f0a58365321620993759b87f8 --- /dev/null +++ b/tezt/lib/main.mli @@ -0,0 +1,27 @@ +(*****************************************************************************) +(* *) +(* Open Source License *) +(* Copyright (c) 2022 Nomadic Labs *) +(* *) +(* Permission is hereby granted, free of charge, to any person obtaining a *) +(* copy of this software and associated documentation files (the "Software"),*) +(* to deal in the Software without restriction, including without limitation *) +(* the rights to use, copy, modify, merge, publish, distribute, sublicense, *) +(* and/or sell copies of the Software, and to permit persons to whom the *) +(* Software is furnished to do so, subject to the following conditions: *) +(* *) +(* The above copyright notice and this permission notice shall be included *) +(* in all copies or substantial portions of the Software. *) +(* *) +(* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR*) +(* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, *) +(* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL *) +(* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER*) +(* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING *) +(* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER *) +(* DEALINGS IN THE SOFTWARE. *) +(* *) +(*****************************************************************************) + +(** Run registered tests that should be run. *) +val run : unit -> unit diff --git a/tezt/lib/process.ml b/tezt/lib/process.ml index df83f459e65b389c1a51af7ecf074cc7b3b1d426..c91ff2a3b5a7a3a5c7f74e8b30011cb2b09be6ed 100644 --- a/tezt/lib/process.ml +++ b/tezt/lib/process.ml @@ -1,7 +1,7 @@ (*****************************************************************************) (* *) (* Open Source License *) -(* Copyright (c) 2020-2021 Nomadic Labs *) +(* Copyright (c) 2020-2022 Nomadic Labs *) (* Copyright (c) 2020 Metastate AG *) (* *) (* Permission is hereby granted, free of charge, to any person obtaining a *) diff --git a/tezt/lib/process.mli b/tezt/lib/process.mli index 1ee01cd00e2bddea2f3d3093711c15eeaf72b711..e1e8d04bd1ecb9a0edcd66d2a363e1dc14a255ec 100644 --- a/tezt/lib/process.mli +++ b/tezt/lib/process.mli @@ -1,7 +1,7 @@ (*****************************************************************************) (* *) (* Open Source License *) -(* Copyright (c) 2020-2021 Nomadic Labs *) +(* Copyright (c) 2020-2022 Nomadic Labs *) (* *) (* Permission is hereby granted, free of charge, to any person obtaining a *) (* copy of this software and associated documentation files (the "Software"),*) @@ -23,7 +23,7 @@ (* *) (*****************************************************************************) -(** External processes launched by tests. *) +(** This module is only available in [tezt], not in [tezt.core] and [tezt.js]. *) (** A process which was {!spawn}ed. *) type t diff --git a/tezt/lib/runner.ml b/tezt/lib/runner.ml index 1e04426358c24cc95907b310878f56d6e56d467c..db2af6876103bd19544fc364854f024323d2f3ec 100644 --- a/tezt/lib/runner.ml +++ b/tezt/lib/runner.ml @@ -1,7 +1,7 @@ (*****************************************************************************) (* *) (* Open Source License *) -(* Copyright (c) 2021 Nomadic Labs *) +(* Copyright (c) 2021-2022 Nomadic Labs *) (* *) (* Permission is hereby granted, free of charge, to any person obtaining a *) (* copy of this software and associated documentation files (the "Software"),*) diff --git a/tezt/lib/runner.mli b/tezt/lib/runner.mli index c5c3b6efd7d00a3367d8976a6e192c3104a10a97..f7d3459c04b4ae19a7fa92362920387023df7ad6 100644 --- a/tezt/lib/runner.mli +++ b/tezt/lib/runner.mli @@ -1,7 +1,7 @@ (*****************************************************************************) (* *) (* Open Source License *) -(* Copyright (c) 2021 Nomadic Labs *) +(* Copyright (c) 2021-2022 Nomadic Labs *) (* *) (* Permission is hereby granted, free of charge, to any person obtaining a *) (* copy of this software and associated documentation files (the "Software"),*) diff --git a/tezt/lib/temp.ml b/tezt/lib/temp.ml index d7d9fc92e3a94257e88b81bbdd863653ed4307d9..b4870d3f9d75a2dbc1275df65295dfb42c1d6709 100644 --- a/tezt/lib/temp.ml +++ b/tezt/lib/temp.ml @@ -1,7 +1,7 @@ (*****************************************************************************) (* *) (* Open Source License *) -(* Copyright (c) 2020 Nomadic Labs *) +(* Copyright (c) 2020-2022 Nomadic Labs *) (* *) (* Permission is hereby granted, free of charge, to any person obtaining a *) (* copy of this software and associated documentation files (the "Software"),*) @@ -52,7 +52,11 @@ let get_fs ?runner () = let next_name = ref 0 -let base_main_dir () = "tezt-" ^ string_of_int (Unix.getpid ()) +let pid = ref 0 + +let set_pid value = pid := value + +let base_main_dir () = "tezt-" ^ string_of_int !pid (* [add_file], [add_dir] and [add_parent] select the file system to use.*) let add_file ?runner file = diff --git a/tezt/lib/temp.mli b/tezt/lib/temp.mli index 3f7e7534b663501e0dddb6903943b246a084f8aa..480b49c2364747c0a8f66832f2a90ea7e7c4f82f 100644 --- a/tezt/lib/temp.mli +++ b/tezt/lib/temp.mli @@ -1,7 +1,7 @@ (*****************************************************************************) (* *) (* Open Source License *) -(* Copyright (c) 2020 Nomadic Labs *) +(* Copyright (c) 2020-2022 Nomadic Labs *) (* *) (* Permission is hereby granted, free of charge, to any person obtaining a *) (* copy of this software and associated documentation files (the "Software"),*) @@ -49,6 +49,17 @@ val file : ?runner:Runner.t -> ?perms:Unix.file_perm -> string -> string (** Get a temporary file name and create it as a directory. *) val dir : ?runner:Runner.t -> ?perms:Unix.file_perm -> string -> string +(** Set the current process identifier. + + This affects the main temporary directory, e.g. ["/tmp/tezt-1234"] + where [1234] is the PID set by [set_pid]. + + The default PID if [set_pid] is not called is [0]. + + This is automatically called by {!Tezt.Main.run}. + This is, however, not called by {!Tezt_js.Main.run}. *) +val set_pid : int -> unit + (** Allow calls to [file] and [dir] until the next [clean_up] or [stop]. Return the main temporary directory, e.g. ["/tmp/tezt-1234/1"], @@ -56,7 +67,7 @@ val dir : ?runner:Runner.t -> ?perms:Unix.file_perm -> string -> string Calls to [file] and [dir] which are made before [start] result in an error. - Don't call this directly, it is called by {!Test.run}. + Do not call this directly, it is called by {!Test.run}. This prevents you from creating temporary files accidentally before your test actually runs. Indeed, if your test is disabled from the command-line it should not create temporary files. By using {!Test.run} you also ensure that {!clean_up} diff --git a/tezt/lib/tezt.ml b/tezt/lib/tezt.ml index 6d8e99f0f589f2349ed76c9ab340327e9db60d5c..3ddf236977a2fb30824c05d7840e20533f74cdae 100644 --- a/tezt/lib/tezt.ml +++ b/tezt/lib/tezt.ml @@ -23,7 +23,7 @@ (* *) (*****************************************************************************) -(** Tezt core library. *) +(** Tezt. *) (** Tezt (pronounced "tezty", as in "tasty" with a "z") is a test framework for OCaml. It is well suited for writing and executing unit, integration and @@ -73,6 +73,46 @@ get the list. See also the {{: https://research-development.nomadic-labs.com/announcing-tezt.html } Tezt mini-tutorial}. *) +(** {2 Backends} *) + +(** Tezt executables can be compiled to bytecode, native code, or JavaScript. + This is reflected by three Dune libraries: + - code which is common to all backends is provided by [tezt.core]; + - the bytecode and native code backends are provided by [tezt]; + - the JavaScript backend is provided by [tezt.js]. + + Function [Test.run] is only available in [tezt] and [tezt.js]. + In other words, to actually run the tests you have to decide whether + to use the JavaScript backend or not. + + The difference between [tezt] and [tezt.js] is that [tezt.js] does not provide: + - the [Process] module; + - the [Temp] module; + - the [Runner] module. + So the JavaScript backend is not well suited for integration tests and regression tests. + But it does support unit tests. + + If you want to run your tests on multiple backends you have to write + two executables: one linked with [tezt] and one linked with [tezt.js]. + To share the code of your tests, you can write your calls to [Test.register] + in a library that depends on [tezt.core]. Here is an example of [dune] files: + {[ +; Dune file for the library that calls [Test.register]. +(library + (name tezts) + (libraries tezt.core) + (library_flags (:standard -linkall)) + (flags (:standard) -open Tezt_core -open Tezt_core.Base)) + +; Dune file for the executable used to run tests in native or bytecode mode. +(executable (name main) (libraries tezts tezt)) + +; Dune file for the executable used to run tests using nodejs. +(executable (name main_js) (modes js) (libraries tezts tezt.js)) + ]} *) + +(** {2 Modules} *) + (** Support for running promises in the background. Background promises are promises that are not bound. @@ -100,7 +140,7 @@ module JSON = JSON (** Functions for logging messages. *) module Log = Log -(** Managing external processes (spawning them, capturing outputs, exit codes, etc). *) +(** Managing external processes. *) module Process = Process (** Process hooks, in particular for use with regression tests. *) @@ -116,4 +156,12 @@ module Runner = Runner module Temp = Temp (** Base test management (registering, initializations, etc). *) -module Test = Test +module Test = struct + include Test + + (** Run registered tests that should be run. + + This function is not available in [tezt.core]. + It is available in [tezt] and [tezt.js]. *) + let run = Main.run +end diff --git a/tezt/lib/JSON.ml b/tezt/lib_core/JSON.ml similarity index 99% rename from tezt/lib/JSON.ml rename to tezt/lib_core/JSON.ml index 4f823a442e7c8a05843c8be56f7558f47638c150..904622ceeb60049bc85dba7b106f810db0d0750a 100644 --- a/tezt/lib/JSON.ml +++ b/tezt/lib_core/JSON.ml @@ -1,7 +1,7 @@ (*****************************************************************************) (* *) (* Open Source License *) -(* Copyright (c) 2020 Nomadic Labs *) +(* Copyright (c) 2020-2022 Nomadic Labs *) (* *) (* Permission is hereby granted, free of charge, to any person obtaining a *) (* copy of this software and associated documentation files (the "Software"),*) diff --git a/tezt/lib/JSON.mli b/tezt/lib_core/JSON.mli similarity index 99% rename from tezt/lib/JSON.mli rename to tezt/lib_core/JSON.mli index 017796c7ddb6015a4e1fe34af74890db2dd1503c..274024292fb54d56aed5a7499be52cfbd8a32e92 100644 --- a/tezt/lib/JSON.mli +++ b/tezt/lib_core/JSON.mli @@ -1,7 +1,7 @@ (*****************************************************************************) (* *) (* Open Source License *) -(* Copyright (c) 2020 Nomadic Labs *) +(* Copyright (c) 2020-2022 Nomadic Labs *) (* *) (* Permission is hereby granted, free of charge, to any person obtaining a *) (* copy of this software and associated documentation files (the "Software"),*) diff --git a/tezt/lib/background.ml b/tezt/lib_core/background.ml similarity index 97% rename from tezt/lib/background.ml rename to tezt/lib_core/background.ml index 9f59a125d9d8ffa7420d0d55e75e5cb275ddb694..9c5912ebc1fe0ba6dbb8a97622c9ad52d226c13a 100644 --- a/tezt/lib/background.ml +++ b/tezt/lib_core/background.ml @@ -1,7 +1,7 @@ (*****************************************************************************) (* *) (* Open Source License *) -(* Copyright (c) 2021 Nomadic Labs *) +(* Copyright (c) 2021-2022 Nomadic Labs *) (* *) (* Permission is hereby granted, free of charge, to any person obtaining a *) (* copy of this software and associated documentation files (the "Software"),*) diff --git a/tezt/lib/background.mli b/tezt/lib_core/background.mli similarity index 97% rename from tezt/lib/background.mli rename to tezt/lib_core/background.mli index 5a936b21cc510862aee92f89dca48fa8559ed58d..32b90ce42bbbc12ccae84d6e2084e4b56251034c 100644 --- a/tezt/lib/background.mli +++ b/tezt/lib_core/background.mli @@ -1,7 +1,7 @@ (*****************************************************************************) (* *) (* Open Source License *) -(* Copyright (c) 2021 Nomadic Labs *) +(* Copyright (c) 2021-2022 Nomadic Labs *) (* *) (* Permission is hereby granted, free of charge, to any person obtaining a *) (* copy of this software and associated documentation files (the "Software"),*) diff --git a/tezt/lib/base.ml b/tezt/lib_core/base.ml similarity index 98% rename from tezt/lib/base.ml rename to tezt/lib_core/base.ml index aa81dfac23926c7251640deeb94aa20dc8804ca8..e4b7e90b418a504a18cb2021bd973ca480b2250d 100644 --- a/tezt/lib/base.ml +++ b/tezt/lib_core/base.ml @@ -1,7 +1,7 @@ (*****************************************************************************) (* *) (* Open Source License *) -(* Copyright (c) 2020-2021 Nomadic Labs *) +(* Copyright (c) 2020-2022 Nomadic Labs *) (* *) (* Permission is hereby granted, free of charge, to any person obtaining a *) (* copy of this software and associated documentation files (the "Software"),*) @@ -29,7 +29,6 @@ do not need a human-readable version. *) let () = Printexc.register_printer @@ function - | Unix.Unix_error (error, _, _) -> Some (Unix.error_message error) | Failure error -> Some error | Sys_error error -> Some error | _ -> None diff --git a/tezt/lib/base.mli b/tezt/lib_core/base.mli similarity index 99% rename from tezt/lib/base.mli rename to tezt/lib_core/base.mli index 9df58e82380df6806492ec3072f1ac8e65d94cc1..562ebab995aed81af8d4b07cedcc437b98a24c17 100644 --- a/tezt/lib/base.mli +++ b/tezt/lib_core/base.mli @@ -1,7 +1,7 @@ (*****************************************************************************) (* *) (* Open Source License *) -(* Copyright (c) 2020-2021 Nomadic Labs *) +(* Copyright (c) 2020-2022 Nomadic Labs *) (* *) (* Permission is hereby granted, free of charge, to any person obtaining a *) (* copy of this software and associated documentation files (the "Software"),*) diff --git a/tezt/lib/check.ml b/tezt/lib_core/check.ml similarity index 99% rename from tezt/lib/check.ml rename to tezt/lib_core/check.ml index 4a5319dfa837d7a52b71bb0993bae2b187481eea..073865fcfc0f41d82bcd4dc7dbd99f9f9af6de73 100644 --- a/tezt/lib/check.ml +++ b/tezt/lib_core/check.ml @@ -1,7 +1,7 @@ (*****************************************************************************) (* *) (* Open Source License *) -(* Copyright (c) 2021 Nomadic Labs *) +(* Copyright (c) 2021-2022 Nomadic Labs *) (* *) (* Permission is hereby granted, free of charge, to any person obtaining a *) (* copy of this software and associated documentation files (the "Software"),*) diff --git a/tezt/lib/check.mli b/tezt/lib_core/check.mli similarity index 99% rename from tezt/lib/check.mli rename to tezt/lib_core/check.mli index 3fc20bbbc21d0a0131ad54a0dd034f89685db3b9..9a9abc81d580a1a8d6bd4947fa32e9358c7cce45 100644 --- a/tezt/lib/check.mli +++ b/tezt/lib_core/check.mli @@ -1,7 +1,7 @@ (*****************************************************************************) (* *) (* Open Source License *) -(* Copyright (c) 2021 Nomadic Labs *) +(* Copyright (c) 2021-2022 Nomadic Labs *) (* *) (* Permission is hereby granted, free of charge, to any person obtaining a *) (* copy of this software and associated documentation files (the "Software"),*) diff --git a/tezt/lib/cli.ml b/tezt/lib_core/cli.ml similarity index 96% rename from tezt/lib/cli.ml rename to tezt/lib_core/cli.ml index 79d42ebd8bd738b8cc67dce796b56e3a93ba1a5c..f84326c287c7d39709d58cac8c37bb491d4ae518 100644 --- a/tezt/lib/cli.ml +++ b/tezt/lib_core/cli.ml @@ -1,7 +1,7 @@ (*****************************************************************************) (* *) (* Open Source License *) -(* Copyright (c) 2020 Nomadic Labs *) +(* Copyright (c) 2020-2022 Nomadic Labs *) (* Copyright (c) 2020 Metastate AG *) (* *) (* Permission is hereby granted, free of charge, to any person obtaining a *) @@ -453,18 +453,18 @@ let init ?args () = \ - the test must have one of the specified titles;\n\ \ - the test must have a title matching one of the specified patterns;\n\ \ - the test must be implemented in one of the specified files.\n\n\ - \ The tags of a test are given by the ~tags argument of Test.run. To \ - negate a tag, prefix it with a slash: /\n\n\ - \ The title of a test is given by the ~title argument of Test.run. It \ - is what is printed after [SUCCESS] (or [FAILURE] or [ABORTED]) in the \ - reports. Use --title (respectively --not-title) to select (respectively \ - unselect) a test by its title on the command-line. You can also select \ - tests whose title matches one or several Perl regular expressions using \ - --match.\n\n\ + \ The tags of a test are given by the ~tags argument of Test.register. \ + To negate a tag, prefix it with a slash: /\n\n\ + \ The title of a test is given by the ~title argument of Test.register. \ + It is what is printed after [SUCCESS] (or [FAILURE] or [ABORTED]) in \ + the reports. Use --title (respectively --not-title) to select \ + (respectively unselect) a test by its title on the command-line. You \ + can also select tests whose title matches one or several Perl regular \ + expressions using --match.\n\n\ \ The file in which a test is implemented is specified by the ~__FILE__ \ - argument of Test.run. In other words, it is the name of the file in \ - which the test is defined, without directories. Use --file to select a \ - test by its filename on the command-line.\n\n\ + argument of Test.register. In other words, it is the name of the file \ + in which the test is defined, without directories. Use --file to select \ + a test by its filename on the command-line.\n\n\ \ For instance:\n\n\ \ " ^ Sys.argv.(0) ^ " node bake /rpc --file bootstrap.ml --file sync.ml\n\n\ diff --git a/tezt/lib/cli.mli b/tezt/lib_core/cli.mli similarity index 98% rename from tezt/lib/cli.mli rename to tezt/lib_core/cli.mli index 3cd2c34663f5e10fdd731c2aa1b79d81b10559fa..ed42c6708f599556dc320d5ad95b89263d5aab3d 100644 --- a/tezt/lib/cli.mli +++ b/tezt/lib_core/cli.mli @@ -1,7 +1,7 @@ (*****************************************************************************) (* *) (* Open Source License *) -(* Copyright (c) 2020 Nomadic Labs *) +(* Copyright (c) 2020-2022 Nomadic Labs *) (* Copyright (c) 2020 Metastate AG *) (* *) (* Permission is hereby granted, free of charge, to any person obtaining a *) diff --git a/tezt/lib/diff.ml b/tezt/lib_core/diff.ml similarity index 100% rename from tezt/lib/diff.ml rename to tezt/lib_core/diff.ml diff --git a/tezt/lib/diff.mli b/tezt/lib_core/diff.mli similarity index 100% rename from tezt/lib/diff.mli rename to tezt/lib_core/diff.mli diff --git a/tezt/lib_core/dune b/tezt/lib_core/dune new file mode 100644 index 0000000000000000000000000000000000000000..024680ba69072d6c202610f9ec343b7a96ca5b41 --- /dev/null +++ b/tezt/lib_core/dune @@ -0,0 +1,12 @@ +; This file was automatically generated, do not edit. +; Edit file manifest/main.ml instead. + +(library + (name tezt_core) + (public_name tezt.core) + (libraries + re + lwt + unix + ezjsonm) + (js_of_ocaml)) diff --git a/tezt/lib/log.ml b/tezt/lib_core/log.ml similarity index 99% rename from tezt/lib/log.ml rename to tezt/lib_core/log.ml index 56b43fdf23fe70ad93944d108b80591f672a732f..ee362800c91be541b84328d9f27f8b0a2860c47d 100644 --- a/tezt/lib/log.ml +++ b/tezt/lib_core/log.ml @@ -1,7 +1,7 @@ (*****************************************************************************) (* *) (* Open Source License *) -(* Copyright (c) 2020 Nomadic Labs *) +(* Copyright (c) 2020-2022 Nomadic Labs *) (* Copyright (c) 2020 Metastate AG *) (* *) (* Permission is hereby granted, free of charge, to any person obtaining a *) diff --git a/tezt/lib/log.mli b/tezt/lib_core/log.mli similarity index 98% rename from tezt/lib/log.mli rename to tezt/lib_core/log.mli index b65c604bc2e75563ae38d6583f7e88449b8f9d2c..e5c9d1b7d86d9dc60cf09a8389da3a6fd1f51148 100644 --- a/tezt/lib/log.mli +++ b/tezt/lib_core/log.mli @@ -1,7 +1,7 @@ (*****************************************************************************) (* *) (* Open Source License *) -(* Copyright (c) 2020 Nomadic Labs *) +(* Copyright (c) 2020-2022 Nomadic Labs *) (* Copyright (c) 2020 Metastate AG *) (* *) (* Permission is hereby granted, free of charge, to any person obtaining a *) diff --git a/tezt/lib/process_hooks.ml b/tezt/lib_core/process_hooks.ml similarity index 100% rename from tezt/lib/process_hooks.ml rename to tezt/lib_core/process_hooks.ml diff --git a/tezt/lib/process_hooks.mli b/tezt/lib_core/process_hooks.mli similarity index 100% rename from tezt/lib/process_hooks.mli rename to tezt/lib_core/process_hooks.mli diff --git a/tezt/lib/regression.ml b/tezt/lib_core/regression.ml similarity index 87% rename from tezt/lib/regression.ml rename to tezt/lib_core/regression.ml index cbd88dc9ce67b27f49aa98f7aad9f1cd877e2610..ec8ee4e20191850351a7d1fbdc1f01604b55d8c1 100644 --- a/tezt/lib/regression.ml +++ b/tezt/lib_core/regression.ml @@ -2,6 +2,7 @@ (* *) (* Open Source License *) (* Copyright (c) 2020 Metastate AG *) +(* Copyright (c) 2020-2022 Nomadic Labs *) (* *) (* Permission is hereby granted, free of charge, to any person obtaining a *) (* copy of this software and associated documentation files (the "Software"),*) @@ -25,15 +26,15 @@ open Base -let capture_output : out_channel option ref = ref None +let capture_output : (string -> unit) option ref = ref None (* Capture a string into a regression output. *) let capture line = match !capture_output with | None -> () - | Some channel -> - output_string channel line ; - output_string channel "\n" + | Some output -> + output line ; + output "\n" let hooks : Process_hooks.t = { @@ -45,8 +46,14 @@ let hooks : Process_hooks.t = on_log = capture; } +let run_and_capture_output ~capture (f : unit -> 'a Lwt.t) = + capture_output := Some capture ; + Lwt.finalize f @@ fun () -> + capture_output := None ; + unit + (* Run [f] and capture the output of ran processes into the [output_file]. *) -let run_and_capture_output ~output_file (f : unit -> 'a Lwt.t) = +let run_and_capture_output_to_file ~output_file (f : unit -> 'a Lwt.t) = let rec create_parent filename = let parent = Filename.dirname filename in if String.length parent < String.length filename then ( @@ -59,11 +66,11 @@ let run_and_capture_output ~output_file (f : unit -> 'a Lwt.t) = in create_parent output_file ; let channel = open_out output_file in - capture_output := Option.some channel ; - Lwt.finalize f (fun () -> - capture_output := None ; - close_out channel ; - unit) + capture_output := Some (output_string channel) ; + Lwt.finalize f @@ fun () -> + capture_output := None ; + close_out channel ; + unit (* Map from output directories to output files. Output directories are directories that are supposed to only contain output files. @@ -123,16 +130,27 @@ let register ~__FILE__ ~title ~tags ?file f = --reset-regressions --title %s" (Log.quote_shell stored_full_output_file) (Log.quote_shell title) ; - let capture_f ~full_output_file = - run_and_capture_output ~output_file:full_output_file f - in if Cli.options.reset_regressions then - capture_f ~full_output_file:stored_full_output_file + run_and_capture_output_to_file ~output_file:stored_full_output_file f else - (* store the current run into a temp file *) - let temp_full_output_file = Temp.file relative_output_file in - let* () = capture_f ~full_output_file:temp_full_output_file in - let diff = Diff.files stored_full_output_file temp_full_output_file in + let* after = + let buffer = Buffer.create 512 in + let* () = run_and_capture_output ~capture:(Buffer.add_string buffer) f in + Buffer.contents buffer |> String.split_on_char '\n' |> Array.of_list + |> return + in + let before = + read_file stored_full_output_file + |> String.split_on_char '\n' |> Array.of_list + in + let diff = + Diff.arrays + ~equal:String.equal + ~before:stored_full_output_file + ~after:"captured" + before + after + in if diff.different then ( Diff.log (Diff.reduce_context diff) ; Test.fail diff --git a/tezt/lib/regression.mli b/tezt/lib_core/regression.mli similarity index 97% rename from tezt/lib/regression.mli rename to tezt/lib_core/regression.mli index 5fe3c5202879c4d9c5910b5531bd2f91300b83e8..c5e28f9207c65274ee99f43726ca2bc8cf05acd5 100644 --- a/tezt/lib/regression.mli +++ b/tezt/lib_core/regression.mli @@ -2,7 +2,7 @@ (* *) (* Open Source License *) (* Copyright (c) 2020 Metastate AG *) -(* Copyright (c) 2021 Nomadic Labs *) +(* Copyright (c) 2020-2022 Nomadic Labs *) (* *) (* Permission is hereby granted, free of charge, to any person obtaining a *) (* copy of this software and associated documentation files (the "Software"),*) diff --git a/tezt/lib/test.ml b/tezt/lib_core/test.ml similarity index 78% rename from tezt/lib/test.ml rename to tezt/lib_core/test.ml index 5a5c5a117e8efccefb738abf0b436423d669cd50..ddebbc706e6a55bcda80296e2204996d1ac11248 100644 --- a/tezt/lib/test.ml +++ b/tezt/lib_core/test.ml @@ -1,7 +1,7 @@ (*****************************************************************************) (* *) (* Open Source License *) -(* Copyright (c) 2020 Nomadic Labs *) +(* Copyright (c) 2020-2022 Nomadic Labs *) (* *) (* Permission is hereby granted, free of charge, to any person obtaining a *) (* copy of this software and associated documentation files (the "Software"),*) @@ -145,12 +145,12 @@ type test = { mutable result : Log.test_result option; } -let really_run test = +type t = test + +let really_run ~sleep ~clean_up ~temp_start ~temp_stop ~temp_clean_up test = Log.info "Starting test: %s" test.title ; List.iter (fun reset -> reset ()) !reset_functions ; test.result <- None ; - Lwt_main.run - @@ (* It may happen that the promise of the function resolves successfully at the same time as a background promise is rejected or that we receive SIGINT. To handle those race conditions, setting the value @@ -180,7 +180,7 @@ let really_run test = in Background.start handle_background_exception ; (* Run the test until it succeeds, fails, or we receive SIGINT. *) - let main_temporary_directory = Temp.start () in + let main_temporary_directory = temp_start () in let* () = let run_test () = let* () = test.body () in @@ -213,7 +213,7 @@ let really_run test = max 0. (delay -. local_starting_time +. global_starting_time) in [ - (let* () = Lwt_unix.sleep remaining_delay in + (let* () = sleep remaining_delay in fail "the set of tests took more than specified global timeout (%gs) \ to run" @@ -225,7 +225,7 @@ let really_run test = | None -> [] | Some delay -> [ - (let* () = Lwt_unix.sleep delay in + (let* () = sleep delay in fail "test took more than specified timeout (%gs) to run" delay); ] in @@ -236,28 +236,23 @@ let really_run test = @ test_timeout)) handle_exception in - (* Terminate all remaining processes. *) - let* () = - Lwt.catch Process.clean_up @@ fun exn -> - Log.warn "Failed to clean up processes: %s" (Printexc.to_string exn) ; - unit - in + let* () = clean_up () in (* Remove temporary files. *) let kept_temp = try match Cli.options.temporary_file_mode with | Delete -> - Temp.clean_up () ; + temp_clean_up () ; false | Delete_if_successful -> if test.result = Some Successful then ( - Temp.clean_up () ; + temp_clean_up () ; false) else ( - Temp.stop () ; + temp_stop () ; true) | Keep -> - Temp.stop () ; + temp_stop () ; true with exn -> Log.warn "Failed to clean up: %s" (Printexc.to_string exn) ; @@ -280,16 +275,37 @@ let really_run test = in return test_result -let rec really_run_with_retry remaining_retry_count test = - match really_run test with +let rec really_run_with_retry ~sleep ~clean_up ~temp_start ~temp_stop + ~temp_clean_up remaining_retry_count test = + let* test_result = + really_run ~sleep ~clean_up ~temp_start ~temp_stop ~temp_clean_up test + in + match test_result with | Failed _ when remaining_retry_count > 0 -> Log.warn "%d retry(ies) left for test: %s" remaining_retry_count test.title ; test.session_retries <- test.session_retries + 1 ; - really_run_with_retry (remaining_retry_count - 1) test - | x -> x + really_run_with_retry + ~sleep + ~clean_up + ~temp_start + ~temp_stop + ~temp_clean_up + (remaining_retry_count - 1) + test + | x -> return x + +let run_one ~sleep ~clean_up ~temp_start ~temp_stop ~temp_clean_up test = + really_run_with_retry + ~sleep + ~clean_up + ~temp_start + ~temp_stop + ~temp_clean_up + Cli.options.retry + test let test_should_be_run ~file ~title ~tags = List.for_all (fun tag -> List.mem tag tags) Cli.options.tags_to_run @@ -360,6 +376,8 @@ let map_registered_list f = in order of registration. *) List.map f (list_registered ()) +let get_test_by_title test_title = String_map.find_opt test_title !registered + let list_tests format = match format with | `Tsv -> @@ -791,7 +809,7 @@ let register ~__FILE__ ~title ~tags body = in registered := String_map.add title test !registered -module Scheduler : sig +module type SCHEDULER = sig type request = Run_test of {test_title : string} type response = Test_result of Log.test_result @@ -807,236 +825,23 @@ module Scheduler : sig val run : on_worker_available:(unit -> (request * (response -> unit)) option) -> worker_count:int -> + (unit -> unit) -> unit val get_current_worker_id : unit -> int option -end = struct - type request = Run_test of {test_title : string} - - type response = Test_result of Log.test_result - - type status = Idle | Working of (response -> unit) | Dead - - type worker = { - pid : int; - mutable status : status; - pipe_to_worker : out_channel; - pipe_from_worker : in_channel; - } - - let send_request channel request = - Marshal.to_channel channel (request : request) [] ; - flush channel - - let read_request channel = - try Some (Marshal.from_channel channel : request) with End_of_file -> None - - let send_response channel response = - Marshal.to_channel channel (response : response) [] ; - flush channel - - let read_response channel = - try Some (Marshal.from_channel channel : response) - with End_of_file -> None - - let internal_worker_error x = - Printf.ksprintf - (fun s -> - Log.error "internal error in worker: %s" s ; - exit 1) - x - - let internal_scheduler_error x = - Printf.ksprintf - (fun s -> - Log.error "internal error in scheduler: %s" s ; - exit 1) - x - - let perform_request (Run_test {test_title}) = - match String_map.find_opt test_title !registered with - | None -> - internal_worker_error - "scheduler requested to run test %S, but worker doesn't know about \ - this test" - test_title - | Some test -> - let test_result = really_run_with_retry Cli.options.retry test in - Test_result test_result - - let rec worker_listen_loop pipe_from_scheduler pipe_to_scheduler = - let request = read_request pipe_from_scheduler in - match request with - | None -> - (* End of file: no more request will come. *) - exit 0 - | Some request -> - let response = perform_request request in - send_response pipe_to_scheduler response ; - worker_listen_loop pipe_from_scheduler pipe_to_scheduler - - let worker_listen pipe_from_scheduler pipe_to_scheduler = - try worker_listen_loop pipe_from_scheduler pipe_to_scheduler - with exn -> - (* Note: if a test fails, its exception is caught and handled by [really_run]. - So here we have an error of Tezt itself. *) - internal_worker_error "%s" (Printexc.to_string exn) - - let next_worker_id = ref 0 - - let current_worker_id = ref None - - let spawn_worker () = - let worker_id = !next_worker_id in - incr next_worker_id ; - let pipe_to_worker_exit, pipe_to_worker_entrance = Unix.pipe () in - let pipe_from_worker_exit, pipe_from_worker_entrance = Unix.pipe () in - let pid = Lwt_unix.fork () in - if pid = 0 then ( - (* This is now a worker process. *) - current_worker_id := Some worker_id ; - Unix.close pipe_to_worker_entrance ; - Unix.close pipe_from_worker_exit ; - worker_listen - (Unix.in_channel_of_descr pipe_to_worker_exit) - (Unix.out_channel_of_descr pipe_from_worker_entrance)) - else ( - (* This is the scheduler process. *) - Unix.close pipe_to_worker_exit ; - Unix.close pipe_from_worker_entrance ; - { - pid; - status = Idle; - pipe_to_worker = Unix.out_channel_of_descr pipe_to_worker_entrance; - pipe_from_worker = Unix.in_channel_of_descr pipe_from_worker_exit; - }) - - let kill_worker worker = - match worker.status with - | Dead -> () - | Idle | Working _ -> - worker.status <- Dead ; - close_out worker.pipe_to_worker ; - close_in worker.pipe_from_worker ; - Unix.kill worker.pid Sys.sigterm ; - let (_ : int * Unix.process_status) = Unix.waitpid [] worker.pid in - () - - let rec run_single_process ~on_worker_available = - match on_worker_available () with - | None -> () - | Some (request, on_response) -> - let response = perform_request request in - on_response response ; - run_single_process ~on_worker_available - - let run_multi_process ~on_worker_available ~worker_count = - (* Start workers. *) - let workers = List.init worker_count (fun _ -> spawn_worker ()) in - (* Handle Ctrl+C in the scheduler process. - Note: Ctrl+C is also received by workers automatically. *) - let received_sigint = ref false in - Sys.(set_signal sigint) - (Signal_handle - (fun _ -> - received_sigint := true ; - (* If the user presses Ctrl+C again, let the program die immediately. *) - Sys.(set_signal sigint) Signal_default)) ; - (* Give work to workers until there is no work to give. *) - let trigger_worker_available worker = - if !received_sigint then kill_worker worker - else - match on_worker_available () with - | None -> kill_worker worker - | Some (request, on_response) -> - worker.status <- Working on_response ; - send_request worker.pipe_to_worker request - in - let rec loop () = - (* Calling [trigger_worker_available] not only gives work to idle workers, - it also kills them if we don't need them any more. - It also ensures that [file_descriptors_to_read] will only be empty if there - are no working workers. *) - List.iter - (fun worker -> - match worker.status with - | Dead | Working _ -> () - | Idle -> trigger_worker_available worker) - workers ; - let file_descriptors_to_read = - List.filter_map - (fun worker -> - match worker.status with - | Idle | Dead -> None - | Working _ -> - Some (Unix.descr_of_in_channel worker.pipe_from_worker)) - workers - in - match file_descriptors_to_read with - | [] -> - (* We maintain the invariant that if there is work to do, at least one - worker is [Working] at this particular point. - This is enforced by the [List.iter] of [trigger_worker_available] above. - So if there is no working worker, we can stop the loop. *) - () - | _ :: _ -> - let ready, _, _ = - (* In case of SIGINT, this returns EINTR. *) - try Unix.select file_descriptors_to_read [] [] (-1.) - with Unix.Unix_error (EINTR, _, _) -> ([], [], []) - in - let read_response file_descriptor = - match - List.find_opt - (fun worker -> - match worker.status with - | Idle | Dead -> false - | Working _ -> - Unix.descr_of_in_channel worker.pipe_from_worker - = file_descriptor) - workers - with - | None -> - internal_scheduler_error - "received a response from an unknown worker" - | Some worker -> ( - match worker.status with - | Idle | Dead -> - (* Please do not consider this error message to be political. *) - internal_scheduler_error - "worker is idle or dead while it should be working" - | Working on_response -> ( - (* Note: [read_response] is blocking. - We assume that if a worker starts writing something, - it will finish writing almost immediately. *) - let response = read_response worker.pipe_from_worker in - match response with - | None -> internal_scheduler_error "no response from worker" - | Some response -> - on_response response ; - worker.status <- Idle)) - in - List.iter read_response ready ; - loop () - in - loop () - - let run ~on_worker_available ~worker_count = - if worker_count = 1 then run_single_process ~on_worker_available - else - try run_multi_process ~on_worker_available ~worker_count - with exn -> internal_scheduler_error "%s" (Printexc.to_string exn) - - let get_current_worker_id () = !current_worker_id end (* [iteration] is between 1 and the value of [--loop-count]. [index] is between 1 and [test_count]. *) type test_instance = {iteration : int; index : int} -let current_worker_id = Scheduler.get_current_worker_id +let current_worker_id_ref = ref (fun () -> None) + +let current_worker_id () = !current_worker_id_ref () -let run () = +let run_with_scheduler scheduler = + let module Scheduler = (val scheduler : SCHEDULER) in + current_worker_id_ref := Scheduler.get_current_worker_id ; List.iter (fun f -> f ()) !before_test_run_functions ; (* Check command-line options. *) check_existence "--file" known_files Cli.options.files_to_run ; @@ -1152,7 +957,8 @@ let run () = in Some (Scheduler.Run_test {test_title = test.title}, on_response) in - Scheduler.run ~on_worker_available ~worker_count:Cli.options.job_count ; + Scheduler.run ~on_worker_available ~worker_count:Cli.options.job_count + @@ fun () -> (* Output reports. *) Option.iter output_junit Cli.options.junit ; Option.iter Record.(output_file (current ())) Cli.options.record ; diff --git a/tezt/lib/test.mli b/tezt/lib_core/test.mli similarity index 64% rename from tezt/lib/test.mli rename to tezt/lib_core/test.mli index c78e324b9b281c5405486b95539f976108131c63..8c2cd2e9a55d73f832309509c8c2246267f254e9 100644 --- a/tezt/lib/test.mli +++ b/tezt/lib_core/test.mli @@ -1,7 +1,7 @@ (*****************************************************************************) (* *) (* Open Source License *) -(* Copyright (c) 2020 Nomadic Labs *) +(* Copyright (c) 2020-2022 Nomadic Labs *) (* *) (* Permission is hereby granted, free of charge, to any person obtaining a *) (* copy of this software and associated documentation files (the "Software"),*) @@ -31,9 +31,6 @@ choose default process names. *) val declare_reset_function : (unit -> unit) -> unit -(** Add a function to be called by [Test.run] before it does anything. *) -val before_test_run : (unit -> unit) -> unit - (** Log an error and stop the test right here. If the optional location [__LOC__] is provided, @@ -45,7 +42,7 @@ val fail : ?__LOC__:string -> ('a, Format.formatter, unit, 'b) format4 -> 'a (** Register a test. The [__FILE__] argument, which should be equal to [__FILE__] - (i.e. just write [Test.run ~__FILE__]), is used to let the user + (i.e. just write [Test.register ~__FILE__]), is used to let the user select which files to run from the command-line. One should be able to infer, from [title], what the test will do. @@ -81,13 +78,6 @@ val register : (unit -> unit Lwt.t) -> unit -(** Run registered tests that should be run. - - Call this once you have registered all tests. - This will check command-line options and run the tests that have been selected, - or display the list of tests. *) -val run : unit -> unit - (** Get the current worker id. In single-process mode (with [-j 1]), this always returns [None]. @@ -97,3 +87,73 @@ val run : unit -> unit - or [Some id] where [0 <= id < Cli.options.job_count] and where [id] uniquely identifies the current worker process that is running the current test. *) val current_worker_id : unit -> int option + +(** {2 Internals} *) + +(** The rest of this module is used by other modules of Tezt. + You usually do not need to use it yourself. *) + +(** Add a function to be called by [run_with_scheduler] before it does anything. *) +val before_test_run : (unit -> unit) -> unit + +module type SCHEDULER = sig + (** Signature of schedulers to pass to {!run_with_scheduler}. *) + + (** Requests that schedulers can perform. *) + type request = Run_test of {test_title : string} + + (** Request results. *) + type response = Test_result of Log.test_result + + (** Run a scheduler that manages several workers. + + This starts [worker_count] workers. + As soon as a worker is available, it calls [on_worker_available]. + [on_worker_available] shall return [None] if there is nothing else to do, + in which case the worker is killed, or [Some (request, on_response)], + in which case the worker executes [request]. + The result of this request, [response], is then given to [on_response]. + + The last argument is a continuation to call once there is nothing left to do. *) + val run : + on_worker_available:(unit -> (request * (response -> unit)) option) -> + worker_count:int -> + (unit -> unit) -> + unit + + (** Get the current worker id. *) + val get_current_worker_id : unit -> int option +end + +(** Generic function to run registered tests that should be run. + + Depending on command-line options, this may do something else, + such as printing the list of tests. + + Instead of calling this directly, call [Test.run], which is provided + by the particular variant of Tezt you are using (Unix or JavaScript). *) +val run_with_scheduler : (module SCHEDULER) -> unit + +(** Test descriptions. *) +type t + +(** Get a test by its title. + + Return [None] if no test was [register]ed with this title. *) +val get_test_by_title : string -> t option + +(** Run one test. + + [sleep] is a function such as [Lwt_unix.sleep] or [Lwt_js.sleep]. + It is used to implement timeouts. + + [clean_up] is a function such as [Process.clean_up] (plus a wrapper + to handle exceptions). It is ran at the end of the test. *) +val run_one : + sleep:(float -> unit Lwt.t) -> + clean_up:(unit -> unit Lwt.t) -> + temp_start:(unit -> string) -> + temp_stop:(unit -> unit) -> + temp_clean_up:(unit -> unit) -> + t -> + Log.test_result Lwt.t diff --git a/tezt/lib_js/dune b/tezt/lib_js/dune new file mode 100644 index 0000000000000000000000000000000000000000..56c65298c54cf78c05ac671d03898160ba8fcad1 --- /dev/null +++ b/tezt/lib_js/dune @@ -0,0 +1,17 @@ +; This file was automatically generated, do not edit. +; Edit file manifest/main.ml instead. + +(library + (name tezt_js) + (public_name tezt.js) + (optional) + (libraries + re + ezjsonm + js_of_ocaml + js_of_ocaml-lwt + tezt.core) + (js_of_ocaml) + (flags + (:standard) + -open Tezt_core)) diff --git a/tezt/lib_js/main.ml b/tezt/lib_js/main.ml new file mode 100644 index 0000000000000000000000000000000000000000..ef7d7fe81d13dc8ed9680cd537e76068afc904de --- /dev/null +++ b/tezt/lib_js/main.ml @@ -0,0 +1,74 @@ +(*****************************************************************************) +(* *) +(* Open Source License *) +(* Copyright (c) 2022 Nomadic Labs *) +(* *) +(* Permission is hereby granted, free of charge, to any person obtaining a *) +(* copy of this software and associated documentation files (the "Software"),*) +(* to deal in the Software without restriction, including without limitation *) +(* the rights to use, copy, modify, merge, publish, distribute, sublicense, *) +(* and/or sell copies of the Software, and to permit persons to whom the *) +(* Software is furnished to do so, subject to the following conditions: *) +(* *) +(* The above copyright notice and this permission notice shall be included *) +(* in all copies or substantial portions of the Software. *) +(* *) +(* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR*) +(* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, *) +(* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL *) +(* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER*) +(* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING *) +(* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER *) +(* DEALINGS IN THE SOFTWARE. *) +(* *) +(*****************************************************************************) + +open Base + +module Scheduler : Test.SCHEDULER = struct + type request = Run_test of {test_title : string} + + type response = Test_result of Log.test_result + + let run_test test_title = + match Test.get_test_by_title test_title with + | None -> + Log.error + "scheduler requested to run test %S, but worker doesn't know about \ + this test" + test_title ; + exit 1 + | Some test -> + let clean_up () = unit in + let* test_result = + Test.run_one + ~sleep:Js_of_ocaml_lwt.Lwt_js.sleep + ~clean_up + ~temp_start:(fun () -> + "(temporary files are not implemented for JS)") + ~temp_stop:(fun () -> ()) + ~temp_clean_up:(fun () -> ()) + test + in + return (Test_result test_result) + + let rec run ~on_worker_available continue = + match on_worker_available () with + | Some (Run_test {test_title}, handle_response) -> + let* response = run_test test_title in + handle_response response ; + run ~on_worker_available continue + | None -> + continue () ; + unit + + let run ~on_worker_available ~worker_count continue = + if worker_count <> 1 then + Log.warn "The -j argument is ignored when using Tezt_js.run." ; + let (_ : unit Lwt.t) = run ~on_worker_available continue in + () + + let get_current_worker_id () = None +end + +let run () = Test.run_with_scheduler (module Scheduler) diff --git a/tezt/lib_js/main.mli b/tezt/lib_js/main.mli new file mode 100644 index 0000000000000000000000000000000000000000..ea98d250e5f0323f0a58365321620993759b87f8 --- /dev/null +++ b/tezt/lib_js/main.mli @@ -0,0 +1,27 @@ +(*****************************************************************************) +(* *) +(* Open Source License *) +(* Copyright (c) 2022 Nomadic Labs *) +(* *) +(* Permission is hereby granted, free of charge, to any person obtaining a *) +(* copy of this software and associated documentation files (the "Software"),*) +(* to deal in the Software without restriction, including without limitation *) +(* the rights to use, copy, modify, merge, publish, distribute, sublicense, *) +(* and/or sell copies of the Software, and to permit persons to whom the *) +(* Software is furnished to do so, subject to the following conditions: *) +(* *) +(* The above copyright notice and this permission notice shall be included *) +(* in all copies or substantial portions of the Software. *) +(* *) +(* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR*) +(* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, *) +(* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL *) +(* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER*) +(* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING *) +(* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER *) +(* DEALINGS IN THE SOFTWARE. *) +(* *) +(*****************************************************************************) + +(** Run registered tests that should be run. *) +val run : unit -> unit diff --git a/tezt/lib_js/tezt_js.ml b/tezt/lib_js/tezt_js.ml new file mode 100644 index 0000000000000000000000000000000000000000..03d46e386fc920c2706e7b82252e1884b25658ea --- /dev/null +++ b/tezt/lib_js/tezt_js.ml @@ -0,0 +1,48 @@ +(*****************************************************************************) +(* *) +(* Open Source License *) +(* Copyright (c) 2022 Nomadic Labs *) +(* *) +(* Permission is hereby granted, free of charge, to any person obtaining a *) +(* copy of this software and associated documentation files (the "Software"),*) +(* to deal in the Software without restriction, including without limitation *) +(* the rights to use, copy, modify, merge, publish, distribute, sublicense, *) +(* and/or sell copies of the Software, and to permit persons to whom the *) +(* Software is furnished to do so, subject to the following conditions: *) +(* *) +(* The above copyright notice and this permission notice shall be included *) +(* in all copies or substantial portions of the Software. *) +(* *) +(* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR*) +(* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, *) +(* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL *) +(* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER*) +(* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING *) +(* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER *) +(* DEALINGS IN THE SOFTWARE. *) +(* *) +(*****************************************************************************) + +(** Tezt (for JavaScript). *) + +(** This library is the same as {!Tezt} but for JavaScript. + Contrary to {!Tezt}, it does not provide: + - the [Process] module; + - the [Temp] module; + - the [Runner] module. *) + +module Background = Background +module Base = Base +module Check = Check +module Cli = Cli +module Diff = Diff +module JSON = JSON +module Log = Log +module Process_hooks = Process_hooks +module Regression = Regression + +module Test = struct + include Test + + let run = Main.run +end