123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454(*
* Copyright (c) 2013-2016 Thomas Gazagnaire <thomas@gazagnaire.org>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*)includeCore_intfopen!ImportopenModelexceptionCheck_errorofunitFmt.texceptionSkiplet()=letprint_error=(* We instantiate the error print buffer lazily, so as to be sensitive to
[Fmt_tty.setup_std_outputs]. *)lazy(letbuf=Buffer.create0inletppf=Format.formatter_of_bufferbufinFmt.set_style_rendererppfFmt.(style_renderer(Formatters.get_stderr():>Format.formatter));funerror->Fmt.pfppf"Alcotest assertion failure@.%a@."error();letcontents=Buffer.contentsbufinBuffer.clearbuf;contents)inPrintexc.register_printer(function|Check_errorerr->Some(Lazy.forceprint_errorerr)|_->None)moduleMake(P:Platform.MAKER)(M:Monad.S)=structmoduleP=P(M)modulePp=structincludePpincludePp.Make(P)endmoduleM=Monad.Extend(M)moduleSuite=Suite(M)moduleLog_trap=Log_trap.Make(M)(P)includeM.Syntax(* Types *)typereturn=unitM.ttype'arun='a->unitM.ttypespeed_level=[`Quick|`Slow]exceptionTest_errortype'atest_case=string*speed_level*'arunlettest_casensf=(n,s,f)type'atest=string*'atest_caselist(* global state *)type'at={(* library values. *)suite:'aSuite.t;(* runtime state. *)mutableerrors:unitFmt.tlist;(* runtime options. *)max_label:int;(** Longest test label in the suite, in UTF-8 characters. *)config:Config.t;run_id:string;log_trap:Log_trap.t;stdout:Formatters.stdout;stderr:Formatters.stderr;}letgen_run_id=letrandom_state=lazy(Random.State.make_self_init())inletrandom_hex_=letstate=Lazy.forcerandom_stateinmatchRandom.State.intstate36with|nwhenn<10->Char.chr(n+Char.code'0')|n->Char.chr(n-10+Char.code'A')infun()->String.v~len:8random_hexletempty~config~trap_logs~suite_name:unescaped_name=leterrors=[]inletsuite=matchSuite.v~name:unescaped_namewith|Oks->s|Error`Empty_name->Pp.user_error"Suite name cannot cannot be empty. Please pass a non-empty string \
to `run`."inletmax_label=0inletrun_id=gen_run_id()inletlog_trap=matchtrap_logswith|false->Log_trap.inactive|true->Log_trap.active~root:config#log_dir~uuid:run_id~suite_name:(Suite.namesuite)inletstdout=config#stdoutinletstderr=config#stderrin{suite;errors;max_label;config;run_id;log_trap;stdout;stderr}letcompare_speed_levels1s2=match(s1,s2)with|`Quick,`Quick|`Slow,`Slow->0|`Quick,_->1|_,`Quick->-1letpp_suite_resultst=letlog_dir=Log_trap.pp_current_run_dirt.log_trapinPp.suite_results~log_dirt.configletpp_event~isatty~prior_error~tests_so_fart=letconfig=t.configinletselector_on_failure=(notprior_error)&¬(config#verbose||config#show_errors)inifnotconfig#jsonthenPp.event~isatty~compact:config#compact~max_label:t.max_label~doc_of_test_name:(Suite.doc_of_test_namet.suite)~selector_on_failure~tests_so_farelseFmt.nopletpp_infot=Pp.info~max_label:t.max_label~doc_of_test_name:(Suite.doc_of_test_namet.suite)letcolorcppffmt=Fmt.(styledcstring)ppffmtletred_sfmt=color`Redfmtletredppffmt=Fmt.kstr(funstr->red_sppfstr)fmtletpp_errortppfe=letpath,error_fmt=matchewith`Error(p,f)->(p,f)|`Exn(p,_,f)->(p,f)inletpp_logsppf()=letpp_logs=Log_trap.recover_logs~tail:t.config#tail_errorst.log_trappathinmatch(t.config#verbose,pp_logs)with|true,_|_,None->Fmt.pfppf"%a@,"error_fmt()|false,Somepp_logs->letpp_log_dir=Pp.map_theta~f:(funs->Pp.quoted(Fmt.styled`Cyans))(Log_trap.pp_log_locationt.log_trappath)inFmt.pfppf"%tLogs saved to %t.@,"pp_logspp_log_dirinFmt.(Pp.with_surrounding_box(const(Pp.event_line~margins:3~max_label:t.max_label~doc_of_test_name:(Suite.doc_of_test_namet.suite))(`Result(path,e)))++pp_logs++Pp.horizontal_rule++cut)ppf()lethas_run:Run_result.t->bool=function|`Ok|`Error_|`Exn_->true|`Skip|`Todo_->falseletbt()=matchPrintexc.get_backtrace()with""->""|s->"\n"^sletexnpathnamepp=`Exn(path,name,Fmt.(pp++constlines(bt())))letprotect_testpath(f:'arun):'a->Run_result.tM.t=funargs->M.catch(fun()->fargs>|=fun()->`Ok)((function|Check_errorerr->leterr=Fmt.(err++conststring(bt()))in`Error(path,err)|Skip->`Skip|Failures->exnpath"failure"Fmt.(conststrings)|Invalid_arguments->exnpath"invalid"Fmt.(conststrings)|e->exnpath"exception"Fmt.(constexne))>>M.return)typerunning_state={tests_so_far:int;first_error:intoption}(** State that is kept during the test executions. *)letwith_captured_logstnamefnargs=ift.config#verbosethenfnargselseLog_trap.with_captured_logst.log_trapname(fun()->(* When capturing the logs of a test, also add the result of the test
at the end. *)let+result=fnargsinPp.rresult_error(t.stdout:>Format.formatter)result;result)()letperform_testtargs{tests_so_far;first_error}(test:_Suite.test_case)=letopenSuiteinletprint_event=pp_eventt~prior_error:(Option.is_somefirst_error)~tests_so_far~isatty:(P.stdout_isatty())(t.stdout:>Format.formatter)inlet*()=M.return()inprint_event(`Starttest.name);let+result,errored=matchtest.fnwith|`Skip->M.return(`Skip,false)|`Runfn->Fmt.(flush(t.stdout:>Format.formatter))()(* Show event before any test stderr *);let+result=with_captured_logsttest.namefnargsin(* Store errors *)leterrored:bool=leterror,errored=matchresultwith|(`Error_|`Exn(_,_,_))ase->([Fmt.const(pp_errort)e],true)|_->([],false)int.errors<-error@t.errors;erroredin(* Show any remaining test output before the event *)Fmt.(flush(t.stdout:>Format.formatter)());Fmt.(flush(t.stderr:>Format.formatter)());(result,errored)inprint_event(`Result(test.name,result));leterror=iferroredthenSometests_so_farelseNoneinletstate={tests_so_far=tests_so_far+1;first_error=Option.(first_error||error);}in(state,result)letperform_teststtestsargs=letcurrently_bailingacc=Option.is_someacc.first_error&&t.config#bailinlet+state,test_results=M.List.fold_map_s(funacctest->ifcurrently_bailingaccthenM.return({accwithtests_so_far=succacc.tests_so_far},`Skip)elseperform_testtargsacctest){tests_so_far=0;first_error=None}testsinlet()=ifcurrently_bailingstatethenmatchstate.tests_so_far-Option.get_exnstate.first_error-1with|nwhenn>0->Formatters.pr"@\n %a@\n"Fmt.(styled`Faintstring)(Fmt.str"... with %d subsequent test%a skipped."nPp.pp_pluraln)|0->()|_->assertfalseintest_resultsletskip_labeltest_case=Suite.{test_casewithfn=`Skip}letfilter_test_caseptest_case=matchpwith|None->true|Somep->(letname,index=lettn=test_case.Suite.nameinTest_name.(Safe_string.to_unescaped_string(nametn),indextn)inmatchp~name~indexwith`Run->true|`Skip->false)letfilter_test_cases~substpathtest_cases=letfilter_test_case=filter_test_casepathintest_cases|>List.filter_map(funtc->iffilter_test_casetcthenSometcelseifsubstthenSome(skip_labeltc)elseNone)letselect_speedspeed_level(test_case:'aSuite.test_caseas'tc):'tc=ifcompare_speed_leveltest_case.speed_levelspeed_level>=0thentest_caseelseSuite.{test_casewithfn=`Skip}letresultttestargs=letinitial_backtrace_status=Printexc.backtrace_status()inift.config#record_backtracethenPrintexc.record_backtracetrue;letstart_time=P.time()inletspeed_level=ift.config#quick_onlythen`Quickelse`Slowinlettest=List.map(select_speedspeed_level)testinlet+results=perform_teststtestargsinlettime=P.time()-.start_timeinletsuccess=List.length(List.filterhas_runresults)inletfailures=List.length(List.filterRun_result.is_failureresults)inift.config#record_backtracethenPrintexc.record_backtraceinitial_backtrace_status;Pp.{time;success;failures;errors=List.revt.errors}letlist_registered_testst()=Suite.testst.suite|>List.map(funt->t.Suite.name)|>List.sortTest_name.compare|>Fmt.(list~sep:(conststring"\n")(pp_infot)(t.stdout:>Format.formatter))letregister(typea)(t:at)(name,(ts:atest_caselist)):at=letmax_label=maxt.max_label(String.length_utf8name)inlettest_details=List.mapi(funindex(doc,speed,test)->letpath=Test_name.v~name~indexinletdoc=ifdoc=""||doc.[String.lengthdoc-1]='.'thendocelsedoc^"."inlettesta=protect_testpathtestain(path,doc,speed,`Runtest))tsinletsuite=List.fold_left(funacctd->matchSuite.addacctdwith|Okacc->acc|Error(`Duplicate_test_pathpath)->Fmt.kstrPp.user_error"Duplicate test path: `%s'"path)t.suitetest_detailsin{twithsuite;max_label}letregister_alltcases=List.fold_leftregistertcasesletrun_testst()args=letfilter=t.config#filterinletsuite=Suite.testst.suiteinletis_empty=filter_test_cases~subst:falsefiltersuite=[]inlet+result=ifis_empty&&Option.is_somefilterthen((* NOTE(dinosaure): [Stdlib.flush_all] is really deep in OCaml and try to flush
all opened file descriptors (including [1] and [2]). Even if the user create
its own [Format.formatter], if it uses a file-descriptor, it will be flushed
too. We don't need to register a channel even if the user specify its own
[Format.formatter] for [stdout] and/or [stderr]. *)flush_all();Fmt.(pf(Formatters.get_stderr():>Format.formatter)"%a\n%!"red"Invalid request (no tests to run, filter skipped everything)!");exit1)elselettests=filter_test_cases~subst:truefiltersuiteinresultttestsargsin(pp_suite_resultst)(t.stdout:>Format.formatter)result;result.failuresletdefault_log_dir()=letfname_concatl=List.fold_leftFilename.concat""linfname_concat[P.getcwd();"_build";"_tests"]type'awith_options='aConfig.with_optionsletlist_tests(typea)(tl:atestlist)=(* TODO: refactor [register_all] to not require dummy state *)letconfig=Config.apply_defaults~default_log_dir:"<not-shown-to-user>"(Config.User.create())inlett=register_all(empty~config~trap_logs:false~suite_name:"<not-shown-to-user>")tlinlist_registered_testst();M.return()letrun_with_args'(config:Config.User.t)name(typea)(args:a)(tl:atestlist)=letconfig=Config.apply_defaults~default_log_dir:(default_log_dir())configinlett=empty~config~trap_logs:(notconfig#verbose)~suite_name:nameinlett=register_allttlinletstdout'=Formatters.get_stdout()inletstderr'=Formatters.get_stderr()inFormatters.set_stdoutt.stdout;Formatters.set_stderrt.stderr;let+test_failures=(* Only print inside the concurrency monad *)let*()=M.return()inletopenFmtinifconfig#ci=`Github_actionsthenFormatters.pr"::group::{%a}\n"Suite.pp_namet.suite;Formatters.pr"Testing %a.@,"(Pp.quotedFmt.(styled`BoldSuite.pp_name))t.suite;Formatters.pr"@[<v>%a@]"(styled`Faint(funppf()->pfppf"This run has ID %a.@,@,"(Pp.quotedstring)t.run_id))();letr=run_testst()argsinifconfig#ci=`Github_actionsthenFormatters.pr"::endgroup::\n";rinat_exit(Format.pp_print_flush(Formatters.get_stderr():>Format.formatter));Formatters.set_stdoutstdout';Formatters.set_stderrstderr';match(test_failures,t.config#and_exit)with|0,true->exit0|0,false->()|_,true->exit1|_,false->raiseTest_errorletrun'configname(tl:unittestlist)=run_with_args'configname()tlletrun_with_args?stdout?stderr?and_exit?verbose?compact?tail_errors?quick_only?show_errors?json?filter?log_dir?bail?record_backtrace?ci=Config.User.kcreaterun_with_args'?stdout?stderr?and_exit?verbose?compact?tail_errors?quick_only?show_errors?json?filter?log_dir?bail?record_backtrace?ciletrun=Config.User.kcreaterun'endmoduleV1=structincludeV1_typesmoduleMake=MakeexceptionSkip=Skipend