123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887moduleTest_result=structtypet=|Success|Failure|Errorletto_exit_code=function|Success->0|Failure->2|Error->1;;letto_string=function|Success->"success"|Failure->"failure"|Error->"error";;letcombinet1t2=matcht1,t2with|Success,Success->Success|Error,_|_,Error->Error|Failure,_|_,Failure->Failure;;letcombine_allts=List.fold_leftcombineSuccesstsendtypedescr=stringletalready_initialized=reffalselettest_modules_ran=ref0lettest_modules_failed=ref0lettests_ran=ref0lettests_failed=ref0letdynamic_lib:stringoptionref=refNonetypefilename=stringtypeline_number=inttypestart_pos=inttypeend_pos=inttypeconfig=(moduleInline_test_nobase_config.S)type'atest_function_args=config:config->descr:descrLazy.t->tags:stringlist->filename:filename->line_number:line_number->start_pos:start_pos->end_pos:end_pos->'amoduleTag_predicate=structtypet={required_tags:stringlist;dropped_tags:stringlist}letinitial={required_tags=[];dropped_tags=["disabled"]}letdropttag={dropped_tags=tag::t.dropped_tags;required_tags=List.filter((<>)tag)t.required_tags};;letrequirettag={dropped_tags=List.filter((<>)tag)t.dropped_tags;required_tags=tag::t.required_tags};;letentire_module_disabledt~partial_tags:tags=List.exists(fundropped->List.memdroppedtags)t.dropped_tags;;letdisabledt~complete_tags:tags=List.exists(funreq->not(List.memreqtags))t.required_tags||List.exists(fundropped->List.memdroppedtags)t.dropped_tags;;endmoduleWhere_to_list=structtypet=|Stdout|Fileofstringendtypewhich_tests={libname:string;only_test_location:(filename*line_numberoption*boolref)list;name_filter:stringlist;which_tags:Tag_predicate.t}typetest_mode={which_tests:which_tests;what_to_do:[`Run_partitionofstringoption|`List_partitionsofWhere_to_list.t]}moduleAction:sigtypet=[`Ignore|`Test_modeoftest_mode]valget:unit->tvalset:t->unitend=structtypet=[`Ignore|`Test_modeoftest_mode]letaction:tref=ref`Ignoreletforce_drop=tryignore(Sys.getenv"FORCE_DROP_INLINE_TEST":string);truewith|Not_found->false;;letget()=(* This is useful when compiling to javascript.
Js_of_ocaml can statically evaluate [Sys.getenv "FORCE_DROP_INLINE_TEST"]
and inline the result ([`Ignore]) whenever [get ()] is called.
Unit tests can then be treated as deadcode since the argument [f] of the [test]
function below is never used. *)ifforce_dropthen`Ignoreelse!action;;letsetv=action:=vendmodulePartition:sigvalfound_test:unit->unitvalset_current:string->unitvalis_current:stringoption->boolvalall:unit->stringlistend=structletall=Hashtbl.create23letcurrent=ref""letset_currentx=current:=xletfound_test()=if!current<>""&¬(Hashtbl.memall!current)thenHashtbl.addall!current();;letis_current=function|None->true|Somep->p=!current;;letall()=List.sortString.compare(Hashtbl.fold(funk()acc->k::acc)all[])endmoduleModule_context=structmoduleT=structtypeone_module={descr:string;tags:stringlist}typet=one_modulelistletdescrt=List.map(funm->m.descr)tlettagst=List.concat(List.map(funm->m.tags)t)endletcurrent:T.tref=ref[]letwith_~descr~tagsf=letprev=!currentincurrent:={T.descr;tags}::prev;tryletx=f()incurrent:=prev;xwith|e->current:=prev;raisee;;letcurrent_descr()=T.descr!currentletcurrent_tags()=T.tags!currentendletverbose=reffalseletstrict=reffalseletshow_counts=reffalseletlist_test_names=reffalseletdelayed_errors=ref[]letstop_on_error=reffalseletlog=refNonelettime_sec=ref0.letuse_color=reftrueletin_place=reffalseletdiff_command=refNoneletsource_tree_root=refNoneletdiff_path_prefix=refNoneletdisplayed_descrdescrfilenamelinestart_posend_pos=let(lazydescr)=descrinPrintf.sprintf"File %S, line %d, characters %d-%d%s"filenamelinestart_posend_pos(ifdescr=""then""else": "^descr);;letparse_descrstr=trySome(Scanf.sscanfstr" File %S , line %d , characters %d - %d %!"(funfileline_start_pos_end_pos->file,Someline))with|_->(trySome(Scanf.sscanfstr" File %S , line %d %!"(funfileline->file,Someline))with|_->(trySome(Scanf.sscanfstr" File %S %!"(funfile->file,None))with|_->None));;letparse_argv?currentargs=matchargswith|name::"inline-test-runner"(* when we see this argument, we switch to test mode *)::lib::rest->(* initialization should only occur once *)if!already_initializedthenraise(Arg.Bad"The inline test runner can only be initialized once, and has already been \
initialized.");already_initialized:=true;lettests=ref[]inletlist_partitions=(refNone:Where_to_list.toptionref)inletpartition=refNoneinlettag_predicate=refTag_predicate.initialinletname_filter=ref[]inArg.parse_argv?current(Array.of_list(name::rest))(Arg.align[("-list-test-names",Arg.Unit(fun()->list_test_names:=true;verbose:=true)," Do not run tests but show what would have been run");("-list-partitions",Arg.Unit(fun()->list_partitions:=SomeStdout)," Lists all the partitions that contain at least one test or test_module");("-list-partitions-into-file",Arg.String(funfile->list_partitions:=Some(Filefile))," Lists all the partitions that contain at least one test or test_module \
into FILE");("-partition",Arg.String(funi->partition:=Somei)," Only run the tests in the given partition");"-verbose",Arg.Setverbose," Show the tests as they run";("-stop-on-error",Arg.Setstop_on_error," Run tests only up to the first error (doesn't work for expect tests)");"-strict",Arg.Setstrict," End with an error if no tests were run";"-show-counts",Arg.Setshow_counts," Show the number of tests ran";("-log",Arg.Unit(fun()->(trySys.remove"inline_tests.log"with|_->());log:=Some(open_out"inline_tests.log"))," Log the tests run in inline_tests.log");("-drop-tag",Arg.String(funs->tag_predicate:=Tag_predicate.drop!tag_predicates),"tag Only run tests not tagged with [tag] (overrides previous -require-tag)");("-require-tag",Arg.String(funs->tag_predicate:=Tag_predicate.require!tag_predicates),"tag Only run tests tagged with [tag] (overrides previous -drop-tag)");("-matching",Arg.String(funs->name_filter:=s::!name_filter),"substring Only run tests whose names contain the given substring");("-only-test",Arg.String(funs->letfilename,index=matchparse_descrswith|Some(file,index)->file,index|None->ifString.containss':'then(leti=String.indexs':'inletfilename=String.subs0iinletindex_string=String.subs(i+1)(String.lengths-i-1)inletindex=tryint_of_stringindex_stringwith|Failure_->raise(Arg.Bad(Printf.sprintf"Argument %s doesn't fit the format \
filename[:line_number]\n\
%!"s))infilename,Someindex)elses,Noneintests:=(filename,index,reffalse)::!tests),"location Run only the tests specified by all the -only-test options.\n\
\ Locations can be one of these forms:\n\
\ - file.ml\n\
\ - file.ml:line_number\n\
\ - File \"file.ml\"\n\
\ - File \"file.ml\", line 23\n\
\ - File \"file.ml\", line 23, characters 2-3");"-no-color",Arg.Clearuse_color," Summarize tests without using color";"-in-place",Arg.Setin_place," Update expect tests in place";("-diff-cmd",Arg.String(funs->diff_command:=Somes)," Diff command for tests that require diffing (use - to disable diffing)");("-source-tree-root",Arg.String(funs->source_tree_root:=Somes)," Path to the root of the source tree");("-diff-path-prefix",Arg.String(funs->diff_path_prefix:=Somes)," Prefix to prepend to filepaths in test output")])(funanon->raise(Arg.Bad(Printf.sprintf"%s: unexpected anonymous argument %s\n%!"nameanon)))(Printf.sprintf"%s %s %s [args]"name"inline-test-runner"lib);Action.set(`Test_mode{which_tests={libname=lib;only_test_location=!tests;which_tags=!tag_predicate;name_filter=!name_filter};what_to_do=(match!list_partitionswith|Somewhere_to_list->`List_partitionswhere_to_list|None->`Run_partition!partition)})|_->();;let()=iftruethen(tryparse_argv(Array.to_listSys.argv)with|Arg.Badmsg->Printf.eprintf"%s"msg;exit1|Arg.Helpmsg->Printf.printf"%s"msg;exit0);;letinitargs=letcurrent=ref0intryparse_argv~currentargs;OkNonewith|Arg.Badmsg->Errormsg|Arg.Helpmsg->Ok(Somemsg);;letam_test_runner=matchAction.get()with|`Test_mode_->true|`Ignore->false;;letam_running_env_var=(* for approximate compatibility, given that the variable is not exactly equivalent
to what PPX_INLINE_TEST_LIB_AM_RUNNING_INLINE_TEST used to be *)"TESTING_FRAMEWORK";;(* This value is deprecated in principle, in favor of Core.am_running_test, so
we're going to live with the ugly pattern match. *)letam_running=matchSys.getenv"PPX_INLINE_TEST_LIB_AM_RUNNING_INLINE_TEST"with|(_:string)->true(* for compatibility with people setting this variable directly *)|exceptionNot_found->(matchSys.getenvam_running_env_varwith|"inline-test"->true|exceptionNot_found->false|_->false);;lettesting=ifam_test_runnerthen`Testing`Am_test_runnerelseifam_runningthen`Testing`Am_child_of_test_runnerelse`Not_testing;;(* This function returns an int63 representing the number of nanos since
some (fixed) baseline. On unix, this baseline will be the unix epoch,
and in javascript, the baseline will be "program initialization time."
Regardless, it's always safe to subtract two values and use the diff,
which is all that ppx_inline_test_lib uses it for. *)lettimestamp_ns()=Mtime_clock.now()|>Mtime.to_uint64_ns(* Time_now.nanoseconds_since_unix_epoch () *)letwhere_to_cut_backtrace=lazy(Search_pattern.create(__MODULE__^"."^"time_without_resetting_random_seeds"));;lettime_without_resetting_random_seedsf=letbefore_ns=timestamp_ns()inletres=(* To avoid noise in backtraces, we do two things.
We use [where_to_cut_backtrace] above to remove the stack frames for the current
function and any function it gets inlined into, as it's not of any interest to the
user, since it's not talking about their test but instead talking about the
ppx_inline_test machinery.
We also avoid inserting any code between the [f] that comes from the user's file
and grabbing the backtrace from its exceptions (no wrapping of [f] with high order
functions like Exn.protect, or (fun () -> f (); true)). *)tryOk(f())with|exn->Error(exn,Printexc.get_backtrace())in(time_sec:=Stdlib.Int64.(to_float(sub(timestamp_ns())before_ns)/.1e9));res;;letsaved_caml_random_state=lazy(Stdlib.Random.State.make[|100;200;300|])letsaved_base_random_state=lazy(Stdlib.Random.State.make[|111;222;333|])letdeault_state=Random.get_state()lettime_and_reset_random_seedsf=letcaml_random_state=Stdlib.Random.get_state()inletbase_random_state=Stdlib.Random.State.copydeault_statein(* TODO: Check that we are dealing with random state properly *)Stdlib.Random.set_state(Lazy.forcesaved_caml_random_state);Stdlib.Random.set_state(Lazy.forcesaved_base_random_state);letresult=time_without_resetting_random_seedsfinStdlib.Random.set_statecaml_random_state;Stdlib.Random.set_statebase_random_state;result;;letstring_of_module_descr()=String.concat""(List.map(funs->" in TES"^"T_MODULE at "^String.uncapitalize_asciis^"\n")(Module_context.current_descr()));;letposition_matchdef_filenamedef_line_numberl=List.exists(fun(filename,line_number_opt,used)->letposition_start=String.lengthdef_filename-String.lengthfilenameinletfound=position_start>=0&&letend_of_def_filename=String.subdef_filenameposition_start(String.lengthfilename)inend_of_def_filename=filename&&(position_start=0||def_filename.[position_start-1]='/')&&matchline_number_optwith|None->true|Someline_number->def_line_number=line_numberiniffoundthenused:=true;found)l;;letname_filter_match~name_filterdescr=matchname_filterwith|[]->true|_::_->List.exists(funsubstring->Search_pattern.is_substring~substringdescr)name_filter;;letprint_delayed_errors()=matchList.rev!delayed_errorswith|[]->()|_::_asdelayed_errors->Printf.eprintf"\n%s\n%!"(String.make70'=');List.iter(funmessage->Printf.eprintf"%s%!"message)delayed_errors;;leteprintf_or_delayfmt=Printf.ksprintf(funs->if!verbosethendelayed_errors:=s::!delayed_errorselsePrintf.eprintf"%s%!"s;if!stop_on_errorthen(print_delayed_errors();exit2))fmt;;letadd_hooks((moduleC):config)f()=C.pre_test_hook();f();;letlist_take_while~f:cond=letrechelperacc=function|[]->List.revacc|h::tlwhencondh->helper(h::acc)tl|_::tl->helperacctlinhelper[];;lethum_backtracebacktrace=(* let open Base in *)backtrace|>String.split_on_char'\n'|>list_take_while~f:(funstr->not(Search_pattern.matches(Lazy.forcewhere_to_cut_backtrace)str))|>ListLabels.map~f:(funstr->" "^str^"\n")|>String.concat"";;let[@inlinenever]test_inner~config~descr~tags~filename:def_filename~line_number:def_line_number~start_pos~end_posfbool_of_f=matchAction.get()with|`Ignore->()|`Test_mode{which_tests={libname;only_test_location;which_tags;name_filter};what_to_do}->letf=add_hooksconfigfinletdescr=lazy(displayed_descrdescrdef_filenamedef_line_numberstart_posend_pos)inletcomplete_tags=tags@Module_context.current_tags()inletshould_run=Somelibname=!dynamic_lib&&(matchonly_test_locationwith|[]->true|_::_->position_matchdef_filenamedef_line_numberonly_test_location)&&(not(Tag_predicate.disabledwhich_tags~complete_tags))&&name_filter_match~name_filter(Lazy.forcedescr)inifshould_runthen(matchwhat_to_dowith|`List_partitions_->Partition.found_test()|`Run_partitionpartition->ifPartition.is_currentpartitionthen(letdescr=Lazy.forcedescrinincrtests_ran;(match!logwith|None->()|Somech->Printf.fprintfch"%s\n%s"descr(string_of_module_descr()));if!verbosethenPrintf.printf"%s%!"descr;letresult=if!list_test_namesthenOktrueelse(* See [time_without_resetting_random_seeds] for why we use [bool_of_f]
rather have the caller wrap [f] to adjust its return value. *)Result.mapbool_of_f(time_and_reset_random_seedsf)in(* If !list_test_names, this is is a harmless zero. *)if!verbosethenPrintf.printf" (%.3f sec)\n%!"!time_sec;matchresultwith|Oktrue->()|Okfalse->incrtests_failed;eprintf_or_delay"%s is false.\n%s\n%!"descr(string_of_module_descr())|Error(exn,backtrace)->incrtests_failed;letbacktrace=hum_backtracebacktracein(* let exn_str = Sexplib0.Sexp_conv.printexc_prefer_sexp exn in *)letexn_str=Printexc.to_stringexninletsep=ifString.containsexn_str'\n'then"\n"else" "ineprintf_or_delay"%s threw%s%s.\n%s%s\n%!"descrsepexn_strbacktrace(string_of_module_descr())));;letset_lib_and_partitionstatic_libpartition=match!dynamic_libwith|Some_->(* possible if the interface is used explicitly or if we happen to dynlink something
that contain tests *)()|None->dynamic_lib:=Somestatic_lib;(matchAction.get()with|`Ignore->()|`Test_mode{which_tests;what_to_do}->ifwhich_tests.libname=static_libthen(letrequires_partition=matchwhat_to_dowith|`List_partitions_|`Run_partition(Some_)->true|`Run_partitionNone->falseinifpartition=""&&requires_partitionthenfailwith"ppx_inline_test: cannot use -list-partition or -partition without \
specifying a partition at preprocessing time"elsePartition.set_currentpartition));;letunset_libstatic_lib=match!dynamic_libwith|None->(* not giving an error, because when some annoying people put pa_ounit in their list
of preprocessors, pa_ounit is set up twice and we have two calls to unset_lib at
the end of the file, and the second one comes in this branch *)()|Somelib->iflib=static_libthendynamic_lib:=None;;lettest~config~descr~tags~filename~line_number~start_pos~end_posf=test_inner~config~descr~tags~filename~line_number~start_pos~end_posf(funb->b);;lettest_unit~config~descr~tags~filename~line_number~start_pos~end_posf=test_inner~config~descr~tags~filename~line_number~start_pos~end_posf(fun()->true);;let[@inlinenever]test_module~config~descr~tags~filename:def_filename~line_number:def_line_number~start_pos~end_posf=matchAction.get()with|`Ignore->()|`Test_mode{which_tests={libname;only_test_location=_;name_filter=_;which_tags};what_to_do}->letf=add_hooksconfigfinletdescr()=displayed_descrdescrdef_filenamedef_line_numberstart_posend_posinletpartial_tags=tags@Module_context.current_tags()inletshould_run=Somelibname=!dynamic_lib(* If, no matter what tags a test defines, we certainly will drop all tests within
this module, then don't run the module at all. This means people can write
things like the following without breaking the 32-bit build:
let%test_module [@tags "64-bits-only"] = (module struct
let i = Int64.to_int_exn ....
end)
We don't shortcut based on position, as we can't tell what positions the
inner tests will have. *)&¬(Tag_predicate.entire_module_disabledwhich_tags~partial_tags)inifshould_runthen(matchwhat_to_dowith|`List_partitions_->Partition.found_test()|`Run_partitionpartition->ifPartition.is_currentpartitionthen(incrtest_modules_ran;letdescr=descr()inmatchModule_context.with_~descr~tags(fun()->(* We do not reset random states upon entering [let%test_module].
Con: Code in test modules can accidentally depend on top-level random
state effects.
Pros: (1) We don't reset to the same seed on entering a [let%test_module]
and then a [let%test] inside that module, which could lead to
accidentally randomly generating the same values in some test. (2) Moving
code into and out of [let%test_module] does not change its random seed.
*)time_without_resetting_random_seedsf)with|Ok()->()|Error(exn,backtrace)->incrtest_modules_failed;letbacktrace=hum_backtracebacktracein(* let exn_str = Sexplib0.Sexp_conv.printexc_prefer_sexp exn in *)letexn_str=Printexc.to_stringexninletsep=ifString.containsexn_str'\n'then"\n"else" "ineprintf_or_delay("TES"^^"T_MODULE at %s threw%s%s.\n%s%s\n%!")(String.uncapitalize_asciidescr)sepexn_strbacktrace(string_of_module_descr())));;(* *** Part from Base *)letraise_with_original_backtracetbacktrace=Stdlib.Printexc.raise_with_backtracetbacktrace;;exceptionFinallyofexn*exnletprotectx~fx~(finally:_->unit)=matchfxwith|res->finallyx;res|exceptionexn->letbt=Stdlib.Printexc.get_raw_backtrace()in(matchfinallyxwith|()->raise_with_original_backtraceexnbt|exceptionfinal_exn->(* Unfortunately, the backtrace of the [final_exn] is discarded here. *)raise_with_original_backtrace(Finally(exn,final_exn))bt);;letsummarize()=matchAction.get()with|`Ignore->Printf.printf"Sys.argv.(0) = %S\n"Sys.argv.(0);ifSys.argv<>[||]&&letbname=Filename.basenameSys.argv.(0)inPrintf.printf"bname = %S\n"bname;bname="inline_tests_runner.exe"(*||
(StringLabels.starts_with ~prefix:"inline_tests_runner" bname
&& StringLabels.ends_with ~suffix:".exe" bname) *)thenPrintf.eprintf"inline_tests_runner.exe is not supposed to be run by hand, you \n\
should run the inline_tests_runner script instead.\n\
%!"elsePrintf.eprintf"You are doing something unexpected with the tests. No tests have \n\
been run. You should use the inline_tests_runner script to run \n\
tests.\n\
%!";Test_result.Error|`Test_mode{which_tests=_;what_to_do=`List_partitionswhere_to_list}->letwith_out_channelf=matchwhere_to_listwith|Stdout->fstdout|Filefile->(* Not passing Open_creat ensures that the file we are supposed to write to exists *)open_out_gen[Open_wronly;Open_text]0file|>protectx~f~finally:close_outinwith_out_channel(funfout->List.iter(Printf.fprintffout"%s\n")(Partition.all()));Test_result.Success|`Test_mode{what_to_do=`Run_partition_;which_tests}->(match!logwith|None->()|Somech->close_outch);print_delayed_errors();(match!tests_failed,!test_modules_failedwith|0,0->if!show_countsthenPrintf.eprintf"%d tests ran, %d test_modules ran\n%!"!tests_ran!test_modules_ran;leterrors=letunused_tests=List.filter(fun(_,_,used)->not!used)which_tests.only_test_locationinmatchunused_testswith|[]->None|_::_->Someunused_testsin(matcherrorswith|Sometests->Printf.eprintf"ppx_inline_test error: the following -only-test flags matched nothing:";List.iter(fun(filename,line_number_opt,_)->matchline_number_optwith|None->Printf.eprintf" %s"filename|Someline_number->Printf.eprintf" %s:%d"filenameline_number)tests;Printf.eprintf".\n%!";Test_result.Error|None->if!tests_ran=0&&!strictthen(Printf.eprintf"ppx_inline_test error: no tests have been run.\n%!";Test_result.Error)elseTest_result.Success)|count,count_test_modules->Printf.eprintf"FAILED %d / %d tests%s\n%!"count!tests_ran(ifcount_test_modules=0then""elsePrintf.sprintf(", %d TES"^^"T_MODULES")count_test_modules);Test_result.Failure);;letassert_test_configs_initializedconfig=ifnot!already_initializedthenPrintf.sprintf"ppx_inline_test error: attempted to access the [%s] config before [init] was \
called"config|>failwith;;letuse_color()=assert_test_configs_initialized"use_color";!use_color;;letin_place()=assert_test_configs_initialized"in_place";!in_place;;letdiff_command()=assert_test_configs_initialized"diff_command";!diff_command;;letdiff_path_prefix()=assert_test_configs_initialized"diff_path_prefix";!diff_path_prefix;;letsource_tree_root()=assert_test_configs_initialized"source_tree_root";!source_tree_root;;letevaluators=ref[summarize]letadd_evaluator~f=evaluators:=f::!evaluatorsletexit()=List.map(funf->f())(List.rev!evaluators)|>Test_result.combine_all|>Test_result.to_exit_code|>exit;;