123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209(*---------------------------------------------------------------------------
Copyright (c) 2024 The b0 programmers. All rights reserved.
SPDX-License-Identifier: ISC
---------------------------------------------------------------------------*)openB0_stdletarray_equalequala0a1=(* 5.3, still no Array.equal (?!)
https://github.com/ocaml/ocaml/pull/13836 *)letl0=Array.lengtha0andl1=Array.lengtha1inInt.equall0l1&&tryfori=0tol0-1doifnot(equal(Array.geta0i)(Array.geta1i))thenraiseExitdone;truewithExit->falsemoduleText=structletfind_newline~nths=(* Returns the byte position after the [nth] newline (if any).
Note, also work on \r\n delimited lines *)letrecloopnth~starts=ifnth=0thenSomestartelsematchString.index_from_optsstart'\n'with|None->None|Somenl_pos->loop(nth-1)~start:(nl_pos+1)sinloopnth~start:0sletfind_col~starts=(* zero based column of [start] on the line. *)leti=refstartinwhile(!i>=0&&s.[!i]<>'\n')dodecridone;if!i<0thenstartelsestart-!iletteststrcount=ifcount<=1then"test"else"tests"endmoduleTest_fmt=structletpadding=" "letlong_str=" LONG "letpass_str=" PASS "letskip_str=" SKIP "letfail_str=" FAIL "lettest_color=[`Bg`White;`Fg`Black]letfail_color=[`Bg`Red_bright;`Fg`Black]letpass_color=[`Bg`Green_bright;`Fg`Black]letskip_color=[`Bg`Yellow_bright;`Fg`Black]letneqppf()=Fmt.st[`Fg`Red_bright;`Bold]ppf"<>"leteqppf()=Fmt.st[`Fg`Red_bright;`Bold]ppf"="lettestppf()=Fmt.sttest_colorppf" TEST "letfailppf()=Fmt.stfail_colorppffail_strletpassppf()=Fmt.stpass_colorppfpass_strletskipppf()=Fmt.stskip_colorppfskip_strletskip_longppf()=Fmt.stskip_colorppflong_strletlongppf()=Fmt.sttest_colorppflong_strletpassedppf()=Fmt.st[`Fg`Green_bright]ppf"passed"letfailedppf()=Fmt.st[`Fg`Red_bright]ppf"failed"letincorrectppf()=Fmt.st[`Fg`Red_bright]ppf"incorrect"letskippedppf()=Fmt.st[`Fg`Yellow_bright]ppf"skipped"letdur=Fmt.st'[`Bold]Mtime.Span.ppletfail_count=Fmt.st'[`Bold;`Fg`Red_bright]Fmt.intletcount=Fmt.st'[`Bold]Fmt.intletfail_count_ratioppf(c,t)=fail_countppfc;Fmt.charppf'/';countppftletraw_locppf(file,lnum,cnum,enum)=Fmt.pfppf"File \"%s\", line %d, characters %d-%d"filelnumcnumenumletlocppfloc=Fmt.pfppf"%a:"raw_loclocletbt_slot_locppfslot=matchPrintexc.Slot.locationslotwith|None->()|Somel->raw_locppf(l.Printexc.filename,l.Printexc.line_number,l.Printexc.start_char,l.Printexc.end_char)letanonppf_=Fmt.stringppf"<abstr>"endmoduleTest=structtypeloc=string*int*int*int(* Logging *)type'alog=?__POS__:loc->('a,Format.formatter,unit,unit)format4->'aletout=refFormat.std_formatterletlog_rawfmt=Fmt.pf!outfmtletlog_raw_flushfmt=log_raw(fmt^^"@.")letlog_pad_noflush?(pad=Test_fmt.padding)~colorfmt=Fmt.pf!out("%a @["^^fmt^^"@]@?")(Fmt.stcolor)padletlog_pad_flush?(pad=Test_fmt.padding)~colorfmt=Fmt.pf!out("%a @["^^fmt^^"@]@.")(Fmt.stcolor)padletklog_pad_flush?(pad=Test_fmt.padding)~colorkfmt=Fmt.kpfk!out("%a @["^^fmt^^"@]@.")(Fmt.stcolor)padletlog_padded_loc_flush?(pad=Test_fmt.padding)~color~mark_noneloc=matchlocwith|Someloc->log_pad_flush~pad~color"%a"Test_fmt.locloc|None->ifmark_nonethenFmt.pf!out"%a@."(Fmt.stcolor)padletloc_log?pad~colorlocfmt=log_padded_loc_flush~color~mark_none:falseloc;log_pad_flush?pad~colorfmtletloc_klog?pad~colorlockfmt=log_padded_loc_flush~color~mark_none:falseloc;klog_pad_flush?pad~colorkfmtletlog_finishfmt=Fmt.pf!out("@["^^fmt^^"@]@.")letlog_start?__POS__:locfmt=letcolor=Test_fmt.test_colorinlog_padded_loc_flush~color~mark_none:falseloc;log_pad_noflush~colorfmtletlog?__POS__:locfmt=loc_log~color:Test_fmt.test_colorlocfmtletlog_pass?__POS__:locfmt=loc_log~color:Test_fmt.pass_colorlocfmtletlog_fail?__POS__:locfmt=loc_log~color:Test_fmt.fail_colorlocfmtletklog_fail?__POS__:lockfmt=loc_klog~color:Test_fmt.fail_colorlockfmtletlog_exn_failbtexn=letcolor=Test_fmt.fail_colorinbeginmatchexnwith|Assert_failure_->log_pad_flush~color"%s""Assertion failed";|exn->log_pad_flush~color"%a raised"(Fmt.code'Fmt.exn)exnend;matchPrintexc.backtrace_slotsbtwith|None->log_pad_flush~color"No backtrace. Did you compile with %a ?"Fmt.code"-g"|Someslots->Array.iter(log_pad_flush~color"%a"Test_fmt.bt_slot_loc)slots(* Testing state, note this stuff is not thread safe.
When we can afford effects we could move this to [test].
Basically all fail combinators (except failstop) raise an
effect to increment local references and we continue. *)letrun_test_count=Atomic.make0modulePass=structletcount=Atomic.make0letincr()=Atomic.incrcountletreport~dur=ifAtomic.getrun_test_count=0thenbeginlog_pad_flush~pad:Test_fmt.pass_str~color:Test_fmt.pass_color"@[Test %a in %a@]"Test_fmt.passed()Test_fmt.dur(Os.Mtime.countdur)endelsebeginlog_pad_flush~pad:Test_fmt.pass_str~color:Test_fmt.pass_color"@[%a %s %a in %a@]"Test_fmt.count(Atomic.getrun_test_count)(Text.teststr(Atomic.getrun_test_count))Test_fmt.passed()Test_fmt.dur(Os.Mtime.countdur)endendmoduleFail=structletcount=Atomic.make0letincr()=Atomic.incrcountlettest_count=Atomic.make0letreport~dur=letpad=Test_fmt.fail_strandcolor=Test_fmt.fail_colorinifAtomic.gettest_count=0thenbeginlog_pad_flush~pad~color"@[Test %a in %a@]"Test_fmt.failed()Test_fmt.dur(Os.Mtime.countdur)endelsebeginletratio=Atomic.gettest_count,Atomic.getrun_test_countinlog_pad_flush~pad~color"@[%a %s %a in %a@]"Test_fmt.fail_count_ratioratio(Text.teststr(Atomic.gettest_count))Test_fmt.failed()Test_fmt.dur(Os.Mtime.countdur)endendmoduleSkip=structletcount=Atomic.make0letincr()=Atomic.incrcountlettest_count=Atomic.make0letincr_test()=Atomic.incrtest_countletreport()=letcount=Atomic.gettest_countinifcount<>0thenbeginlog_pad_flush~color:Test_fmt.skip_color"@[%a %s %a@]"Test_fmt.countcount(Text.teststrcount)Test_fmt.skipped()endend(* Stop, pass and fail *)exceptionStopexceptionSkipletstop()=raise_notraceStopletskip?__POS__:posfmt=Skip.incr();loc_klog~pad:Test_fmt.skip_str~color:Test_fmt.skip_colorpos(fun_->raise_notraceSkip)fmtletpass()=Pass.incr()letfail?__POS__fmt=Fail.incr();log_fail?__POS__fmtletfailstop?__POS__fmt=Fail.incr();klog_fail?__POS__(fun_->stop())fmt(* Blocks and loops *)letblock?fail?__POS__f=letbefore_pass_count=Atomic.getPass.countinletbefore_fail_count=Atomic.getFail.countinletfinish()=letfail_diff=Atomic.getFail.count-before_fail_countiniffail_diff=0then()elseletassertions=Atomic.getPass.count-before_pass_count+fail_diffinmatchfailwith|Somefail->fail?__POS__fail_diff~assertions|None->log_fail?__POS__"Block %a on %a assertions"Test_fmt.failed()Test_fmt.fail_count_ratio(fail_diff,assertions)inbegintryf()with|Stop|Skip->()|exnwhennot(Os.exn_don't_catchexn)->letbt=Printexc.get_raw_backtrace()inlog_exn_failbtexn;Fail.incr()end;finish()letrecrange?(kind="Test")~first~last?__POS__:locf=letlog_fail?__POS__~kind~first~lastn=log_fail?__POS__"%s in range [%d;%d] failed on %a"kindfirstlast(Fmt.code'Fmt.int)ninletreclooploc~kind~first~lastnf=ifn>lastthen()elseletbefore_fail_count=Atomic.getFail.countintryfn;ifbefore_fail_count<>Atomic.getFail.countthenlog_fail?__POS__:loc~kind~first~lastn;looploc~kind~first~last(n+1)fwith|Stop|Skip->ifbefore_fail_count<>Atomic.getFail.countthenlog_fail?__POS__:loc~kind~first~lastn|exnwhennot(Os.exn_don't_catchexn)->letbt=Printexc.get_raw_backtrace()inlog_exn_failbtexn;log_fail?__POS__:loc~kind~first~lastn;looploc~kind~first~last(n+1)finlooploc~kind~first~lastfirstf(* Testers *)moduletypeT=sigtypetvalequal:t->t->boolvalpp:Format.formatter->t->unitendmoduleT=structtype'at=(moduleTwithtypet='a)letrepr_equalv0v1=Stdlib.comparev0v1=0(* works on nan values *)letmake(typea)?(equal=repr_equal)?(pp=Test_fmt.anon)():at=(module(structtypet=aletequal=equalletpp=ppend))letequal(typea)(moduleT:Twithtypet=a)=T.equalletpp(typea)(moduleT:Twithtypet=a)=T.ppletwith'(typea)?equal?pp(moduleT:Twithtypet=a)=letequal=Option.value~default:T.equalequalinletpp=Option.value~default:T.ppppinmake~equal~pp()(* Predefined testers *)lettrue'(typea):at=letequal__=trueandpp=Test_fmt.anonin(modulestructtypet=aletequal=equalletpp=ppend)letfalse'(typea):at=letequal__=falseandpp=Test_fmt.anonin(modulestructtypet=aletequal=equalletpp=ppend)letany(typea):at=letequal=repr_equalandpp=Test_fmt.anonin(modulestructtypet=aletequal=equalletpp=ppend)letexn=make~equal:repr_equal~pp:Fmt.exn()letinvalid_arg=letequalexn_=matchexnwith|Invalid_argument_->true|_->falseinletppppf=function|Invalid_argument_->Fmt.stringppf"Invalid_argument _"|exn->Fmt.exnppfexninmake~equal~pp()letfailure=letequalexn_=matchexnwith|Failure_->true|_->falseinletppppf=function|Failure_->Fmt.stringppf"Failure _"|exn->Fmt.exnppfexninmake~equal~pp()letunit=make~equal:(fun()()->true)~pp:Fmt.OCaml.unit()letbool=make~equal:Bool.equal~pp:Fmt.OCaml.bool()letint=make~equal:Int.equal~pp:Fmt.OCaml.int()letint32=make~equal:Int32.equal~pp:Fmt.OCaml.int32()letuint32=make~equal:Int32.equal~pp:Fmt.OCaml.uint32()letint64=make~equal:Int64.equal~pp:Fmt.OCaml.int64()letuint64=make~equal:Int64.equal~pp:Fmt.OCaml.uint64()letnativeint=make~equal:Nativeint.equal~pp:Fmt.OCaml.nativeint()letnativeuint=make~equal:Nativeint.equal~pp:Fmt.OCaml.nativeuint()letfloat=make~equal:Float.equal~pp:Fmt.OCaml.float()lethex_float=make~equal:Float.equal~pp:Fmt.OCaml.hex_float()letchar=make~equal:Char.equal~pp:Fmt.OCaml.char()letascii_string=make~equal:String.equal~pp:Fmt.OCaml.ascii_string()letstring=make~equal:String.equal~pp:Fmt.OCaml.string()letlines=make~equal:String.equal~pp:Fmt.lines()letbinary_string=make~equal:String.equal~pp:Fmt.binary_string()letstyled_string=make~equal:String.equal~pp:Fmt.styled_text_string_literal()letbytes:bytest=make~equal:Bytes.equal~pp:Fmt.bytes()letoption(typea)(moduleSome:Twithtypet=a):aoptiont=make~equal:(Option.equalSome.equal)~pp:(Fmt.OCaml.optionSome.pp)()leteither(typea)~left:(moduleLeft:Twithtypet=a)(typeb)~right:(moduleRight:Twithtypet=b)=letequal=Either.equal~left:Left.equal~right:Right.equalinletpp=Fmt.OCaml.either~left:Left.pp~right:Right.ppinmake~equal~pp()letresult'(typea)~ok:(moduleOk:Twithtypet=a)(typeb)~error:(moduleError:Twithtypet=b)=letequal=Result.equal~ok:Ok.equal~error:Error.equalinletpp=Fmt.OCaml.result~ok:Ok.pp~error:Error.ppinmake~equal~pp()letresult~ok=result'~ok~error:stringletlist(typea)(moduleElt:Twithtypet=a)=make~equal:(List.equalElt.equal)~pp:(Fmt.OCaml.listElt.pp)()letarray(typea)(moduleElt:Twithtypet=a)=make~equal:(array_equalElt.equal)~pp:(Fmt.OCaml.arrayElt.pp)()letpair(typea)(moduleFst:Twithtypet=a)(typeb)(moduleSnd:Twithtypet=b)=letequal(x0,y0)(x1,y1)=Fst.equalx0x1&&Snd.equaly0y1inletpp=Fmt.OCaml.pairFst.ppSnd.ppinmake~equal~pp()lett2=pairlett3v0v1v2=letequal(a0,a1,a2)(b0,b1,b2)=(equalv0)a0b0&&(equalv1)a1b1&&(equalv2)a2b2inletpp=Fmt.OCaml.t3(ppv0)(ppv1)(ppv2)inmake~equal~pp()lett4v0v1v2v3=letequal(a0,a1,a2,a3)(b0,b1,b2,b3)=(equalv0)a0b0&&(equalv1)a1b1&&(equalv2)a2b2&&(equalv3)a3b3inletpp=Fmt.OCaml.t4(ppv0)(ppv1)(ppv2)(ppv3)inmake~equal~pp()lett5v0v1v2v3v4=letequal(a0,a1,a2,a3,a4)(b0,b1,b2,b3,b4)=(equalv0)a0b0&&(equalv1)a1b1&&(equalv2)a2b2&&(equalv3)a3b3&&(equalv4)a4b4inletpp=Fmt.OCaml.t5(ppv0)(ppv1)(ppv2)(ppv3)(ppv4)inmake~equal~pp()lett6v0v1v2v3v4v5=letequal(a0,a1,a2,a3,a4,a5)(b0,b1,b2,b3,b4,b5)=(equalv0)a0b0&&(equalv1)a1b1&&(equalv2)a2b2&&(equalv3)a3b3&&(equalv4)a4b4&&(equalv5)a5b5inletpp=Fmt.OCaml.t6(ppv0)(ppv1)(ppv2)(ppv3)(ppv4)(ppv5)inmake~equal~pp()endmoduleDiff=structtype'at='aT.t->fnd:'a->exp:'a->unitFmt.tletmakerender=renderopenResult.Syntaxletdumb(typea)t~fnd~expppf()=Fmt.pfppf"@[<v>@[%a@]@,%a@,@[%a@]@]"(T.ppt)fndTest_fmt.neq()(T.ppt)expletrun_diff_cmd?envcmd~fnd~exp=Result.join@@Os.Dir.with_tmp@@fundir->letforce=falseandmake_path=falseinlet*()=Os.File.write~force~make_pathFpath.(dir/"expected")expinlet*()=Os.File.write~force~make_pathFpath.(dir/"found")fndinletdiff=Cmd.(cmd%"expected"%"found")inmatchOs.Cmd.run_status_out?env~trim:false~cwd:dirdiffwith|Error_ase->e|Ok(st,diff)->Okdiffletof_cmdcmdt~fnd~expppf()=(* The newlines are to avoid 'No newline at the end of file' reported
by diffing tools. *)letfnd=Fmt.str"%a@\n"(T.ppt)fndinletexp=Fmt.str"%a@\n"(T.ppt)expinmatchrun_diff_cmdcmd~fnd~expwith|Errore->Fmt.pfppf"diff command error: %s"e|Okdiff->Fmt.pfppf"@[%a@]"Fmt.linesdiffletgit_diff_cmd=B0_std.Cmd.(tool"git"%"diff")letgit_diff=lazy(Os.Cmd.getgit_diff_cmd)letgit_diff~fnd~exp=let*git_diff=Lazy.forcegit_diffinletopts=letcolor=matchFmt.styler()with|Fmt.Plain->"--color=never"|Fmt.Ansi->"--color=always"inCmd.(arg"--ws-error-highlight=all"%"--no-index"%"--no-prefix"%"--patience"%color)inletenv=["GIT_CONFIG_SYSTEM=/dev/null";"GIT_CONFIG_GLOBAL=/dev/null"]in(* Add newlines to avoid 'No newline at the end of file' reported
by diffing tools. *)letfnd=fnd^"\n"inletexp=exp^"\n"inletcmd=Cmd.(git_diff%%opts)inlet*diff=run_diff_cmd~envcmd~fnd~expin(* Trim the first two lines these are: diff --git …\n index … *)matchText.find_newline~nth:2diffwith|None->Okdiff|Somefirst->Ok(String.subrange~firstdiff)letgitt~fnd~expppf()=letfnd=Fmt.str"%a"(T.ppt)fndinletexp=Fmt.str"%a"(T.ppt)expinmatchgit_diff~fnd~expwith|Errore->Fmt.pfppf"git diff error: %s"e|Okdiff->Fmt.pfppf"@[%a@]"Fmt.linesdifftypedefault={default:'a.'at}letdefault=ref{default=git}letset_cmd=function|None->()|Somecmd->letd=matchCmd.to_listcmdwith|["dumb"]->{default=dumb}|_::_->letdefault:'a.'at=funt~fnd~expppf()->of_cmdcmdt~fnd~expppf()in{default}|[]->(Log.warn@@funm->m"No diff command specified. Using %a."Fmt.code"dumb");{default=dumb}indefault:=dletdefault()=!default.defaultletpp?difft~fnd~exp=letdiff=matchdiffwithNone->default()|Somediff->diffindifft~fnd~expendmodulePatch=structtypesubst={first:int;last:int;subst:string}typet={src:string;substs:substlist;}letmake~src={src;substs=[]}letsrcp=p.srcletsubstsp=p.substsletis_emptyp=p.substs=[]letadd_substpsubst={pwithsubsts=subst::p.substs}letapply{src;substs}=letrecloopaccsrc~start=function|[]->letlast=String.subrange~first:startsrcinString.concat""(List.rev(last::acc))|{first;last;subst}::substs->letbefore=String.subrange~first:start~last:(first-1)srcinloop(subst::before::acc)src~start:(last+1)substsinletcompares0s1=Int.compares0.firsts1.firstinloop[]src~start:0(List.stable_sortcompare(List.revsubsts))(* Files to patch *)letsrc_root=refNoneletset_src_rootroot=src_root:=rootletfiles:toptionFpath.Map.tref=refFpath.Map.emptyletgetfile=letfile=match!src_rootwith|None->file|Someroot->Fpath.appendrootfileinmatchFpath.Map.find_optfile!fileswith|Somev->v|None->letsubsts=matchOs.File.readfilewith|Oksrc->Some{src;substs=[]}|Errore->log_fail"Correction failure: %a: %s"Fpath.ppfilee;Noneinfiles:=Fpath.Map.addfilesubsts!files;substsletupdatefilepatch=files:=Fpath.Map.addfile(Somepatch)!filesletwrite_files()=letwrite_filefile=function|None->()|Somepatchwhenis_emptypatch->()|Somepatch->letforce=trueandmake_path=falseinmatchOs.File.write~force~make_pathfile(applypatch)with|Errore->log_fail"Correction failure: %a: %s"Fpath.ppfilee|Ok()->()inFpath.Map.iterwrite_file!filesletsrc_root()=!src_rootend(* Assertions *)type'aeq=?diff:'aDiff.t->?__POS__:loc->'a->'a->unitletfailneq?diff?__POS__:loctfndexp=Fail.incr();log_padded_loc_flush~color:Test_fmt.fail_color~mark_none:trueloc;log_raw_flush"%a"(Diff.pp?difft~fnd~exp)()leteqt?diff?__POS__fndexp=ifT.equaltfndexpthenpass()elsefailneq?diff?__POS__tfndexpletneqt?diff?__POS__v0v1=ifnot(T.equaltv0v1)thenpass()elsefail"@[<v>Must not be equal:@,%a@,%a@,%a@]"(T.ppt)v0Test_fmt.eq()(T.ppt)v1?__POS__letholds?msg?__POS__b=ifbthenpass()elsematchmsgwith|None->fail?__POS__"@[Assertion failed@]"|Somemsg->fail?__POS__"@[Assertion failed: %s@]"msg(* Exceptions *)letnoraise?__POS__f=tryf()with|Stop|Skipasexn->raise_notraceexn|exnwhennot(Os.exn_don't_catchexn)->Fail.incr();log_fail?__POS__"@[Unexpected exception:@]";log_raw_flush"%a"Fmt.exnexn;stop()letcatch?ret?__POS__fk=tryletv=f()inmatchretwith|None->fail?__POS__"@[Expected an exception but got a value.@]"|Someret->Fail.incr();log_fail?__POS__"@[Expected an exception but got value:";log_raw_flush"%a"(T.ppret)vwith|Stop|Skipasexn->raise_notraceexn|exnwhennot(Os.exn_don't_catchexn)->kexnletraises?ret?(exn=T.exn)?diff?__POS__expf=catch?ret?__POS__f@@funfnd->eqexn?diff?__POS__fndexpletinvalid_arg?ret?diff?__POS__f=raises?ret~exn:T.invalid_arg?diff?__POS__(Invalid_argument"")fletfailure?ret?diff?__POS__f=raises?ret~exn:T.failure?diff?__POS__(Failure"")f(* Values *)letany?(diff=Diff.dumb)=eqT.any~diffletexn=eqT.exnletunit=eqT.unitletbool=eqT.boolletchar=eqT.charletint=eqT.intletint32=eqT.int32letuint32=eqT.uint32letint64=eqT.int64letuint64=eqT.uint64letnativeint=eqT.nativeintletnativeuint=eqT.nativeuintletfloat=eqT.floatletstring=eqT.stringletlines=eqT.linesletbinary_string=eqT.binary_stringletstyled_string=eqT.styled_stringletbytes=eqT.bytesletoptionsome=eq(T.optionsome)leteither~left~right=eq(T.either~left~right)letresult~ok=eq(T.result~ok)letresult'~ok~error=eq(T.result'~ok~error)letlistelt=eq(T.listelt)letarrayelt=eq(T.arrayelt)letpairfstsnd=eq(T.pairfstsnd)lett2=pairlett3v0v1v2=eq(T.t3v0v1v2)lett4v0v1v2v3=eq(T.t4v0v1v2v3)lett5v0v1v2v3v4=eq(T.t5v0v1v2v3v4)lett6v0v1v2v3v4v5=eq(T.t6v0v1v2v3v4v5)(* Randomized testing *)moduleRand=structletcli_seed="seed"letenv_seed="SEED"letrseed=refNoneletrstate=refNoneletset_seedseed=rseed:=seedletrecinit_random_stateseed=matchseedwith|None->(* auto-seed *)lets=Random.State.make_self_init()ininit_random_state(Some(Random.State.bitss))|Someseed->rseed:=Someseed;rstate:=Some(Random.State.make[|seed|])letrecstate()=match!rstatewith|Somes->s|None->matchOs.Env.var~empty_is_none:trueenv_seedwith|None->init_random_stateNone;state()|Somei->matchint_of_string_optiwith|Some_asseed->init_random_stateseed;state()|None->log_fail"Cannot parse integer from %a env value %s, ignoring."Fmt.codeenv_seedi;init_random_stateNone;state()(* Reporting *)letreport(log:'alog)=match!rstatewith|None->()(* Was not used *)|Some_->log"Run with %a %a to reproduce randomness."Fmt.code("--"^cli_seed)Fmt.(code'int)(Option.get!rseed);end(* Snapshot testing *)moduleSnapshot=structletcli_correct="correct"letcorrect=reffalseletset_correctc=correct:=cletcorrect()=!correctletcli_force_correct="force-correct"letforce_correct=reffalseletset_force_correctf=force_correct:=fletforce_correct()=!force_correct(* Reporting *)letfail_count=Atomic.make0letincr_fail()=Atomic.incrfail_countletreport_fail()=letfail_count=Atomic.getfail_countiniffail_count=0then()elseletplural,it=iffail_count>1then"s are","them"else" is","it"inlog_fail"%a snapshot%s %a. Run with %a to correct %s."Test_fmt.fail_countfail_countpluralTest_fmt.incorrect()Fmt.code("--"^cli_correct)itletlog_correcting?__POS__:locfmt=letcolor=Test_fmt.pass_colorinlog_padded_loc_flush~color~mark_none:trueloc;log_raw_flush"%a reference snapshot to:"(Fmt.st[`Fg`Green_bright])"Correcting";log_raw_flushfmt(* Correcting sources *)letreindent~indents=(* indent by [indent] after each newline *)ifindent=0thenselse(* The first line is kept unchanged which is what we want. *)letsep="\n"^String.makeindent' 'inString.concatsep(String.split_on_char'\n's)letrelayout_to_multi_line_string_literal~indents=(* Note we could perhaps have something adequate directly in Fmt.
This breaks on '\n', escapes with a slash and newline and indents. *)letwhite_startl=ifl=""thenfalseelseChar.Ascii.is_whitel.[0]inletescape_startl=ifwhite_startlthen"\\"^lelse" "^linletlines=String.split_all~sep:{|\n|}sinletlines=matchList.revlineswith|"\""::prevs->(* Last empty line, dont add a line for that *)beginmatchprevswith|[]->lines|prev::prevs->List.rev((prev^{|\n"|})::prevs)end|_->linesinletsplit_line="\\n\\\n"inifindent=0thenString.concatsplit_linelineselseletindent=String.makeindent' 'inletlines=matchlineswith|[]->[]|[l]->[l]|fst::rest->fst::(List.mapescape_startrest)inString.concat(split_line^indent)linesletfind_value_subrange(fname,lnum,first,last)s=(* First find the line on which we are. Then skip over
__POS_OF__ and whitepace. The actual range spanned by
__POS_OF__ position is not entirely clear, for example it
will span the parenthese here (( __POS_OF__ "bla" ) ). For
now we don't deal with that and assume [last - 1] is spanned by
the (possibly parenthesised) value. *)matchText.find_newline~nth:(lnum-1)swith|None->Fmt.failwith"%s: no line %d found"fnamelnum|Someline_start->letstart=line_start+firstinletlast=line_start+last-1(* last is a position in the String sense *)inletloc_of="__POS_OF__"inmatchString.find_first~start~sub:loc_ofswith|None->Fmt.failwith"%s:%d: no __POS_OF__ found"fnamelnum|Somestart->letstart=start+String.lengthloc_ofinletis_value_start=Fun.negateChar.Ascii.is_whiteinmatchString.find_first_index~startis_value_startswith|None->Fmt.failwith"%s:%d: no value found"fnamelnum|Somevstart->vstart,last(* Snapshots *)type'at=loc*'atype'asubst='aT.t->loc*'a->by:'a->src:string->Patch.substletloc=fstletvalue=sndletgeneric_substtexp~by~src=letsubst=Fmt.str"@[%a@]"(T.ppt)byinletfirst,last=find_value_subrange(locexp)srcinletfirst,last=ifsrc.[first]='('&&subst.[0]<>'('thenfirst+1,last-1(* keep parens *)elsefirst,lastinletindent=Text.find_col~start:(first-1)srcinletsubst=reindent~indentsubstin{Patch.first;last;subst}letstring_subst_exp~by~src=letfirst,last=find_value_subrange(locexp)srcinifsrc.[first]='\"'thenbeginletindent=Text.find_col~start:(first-1)srcinletsubst=Fmt.str"%a"Fmt.text_string_literalbyinletsubst=relayout_to_multi_line_string_literal~indentsubstin{Patch.first;last;subst}endelse(* assume "{|" for now, but be more subtle here. First parse any quoted
string and recognize some to drive layout (and automatically
expand the token if it happens in [by]. *){Patch.first=first+2;last=last-2;subst=by}letcorrect_snapshot?substtfnd((fname,_,_,_),_asexp)=letfile=Fpath.of_stringfname|>Result.error_to_failureinmatchPatch.getfilewith|None->()|Somepatch->letsubst=matchsubstwith|None->generic_substtexp~by:fnd~src:patch.src|Somesubst->substtexp~by:fnd~src:patch.srcinPatch.updatefile(Patch.add_substpatchsubst)letsnap?substt?difffndexp=tryifT.equaltfnd(valueexp)thenbeginifforce_correct()thencorrect_snapshot?substtfndexp;pass()endelsebeginincr_fail();ifnot(correct()||force_correct())thenfailneq~__POS__:(locexp)?difftfnd(valueexp)elsebeginlog_correcting~__POS__:(locexp)"%a"(T.ppt)fnd;correct_snapshot?substtfndexpendendwith|Failuree->log_fail"Correction failure: %s"eendtype'asnap=?diff:'aDiff.t->'a->'aSnapshot.t->unitletsnap=Snapshot.snapmoduleLong=structletcli_l="l"letcount=Atomic.make0letincr()=Atomic.incrcountletrun=reffalseletset_runl=run:=lletrun()=!runletskip_exit=reffalseletset_skip_exitl=skip_exit:=lletskip_exit()=!skip_exitletreport()=letcount=Atomic.getcountinifnot(run())&&count>0thenbeginlog_pad_flush~color:Test_fmt.skip_color"@[%a long %s %a. Run with %a to execute.@]"Test_fmt.countcount(Text.teststrcount)Test_fmt.skipped()Fmt.code("-"^cli_l)endendmoduleName=structletincludes=ref[]letset_includesincs=includes:=(List.mapString.lowercase_asciiincs)letmatch_includesname=List.exists(funprefix->String.starts_with~prefixname)!includesletexcludes=ref[]letset_excludesexcs=excludes:=(List.mapString.lowercase_asciiexcs)letmatch_excludesname=List.exists(funprefix->String.starts_with~prefixname)!excludesletselectedname=letname=String.lowercase_asciinamein(!includes=[]||match_includesname)&¬(match_excludesname)end(* Tests *)moduleArg=structtype'at='aType.Id.ttypevalue=Value:'at*'a->valueletmake=Type.Id.makeletvalueargv=Value(arg,v)letrecfind_value:typea.at->valuelist->aoption=funarg->function|[]->None|Value(arg',v)::vs->matchType.Id.provably_equalargarg'with|SomeType.Equal->Somev|None->find_valueargvsendmoduleDef=structtypefunc=|Unit:(unit->unit)->func|Arg:'aType.Id.t*('a->unit)->functypet={name:string;long:bool;func:func}letcount=Atomic.make0letincr()=Atomic.incrcountletlist=ref[]letadd~longnamefunc=incr();list:={name;long;func}::!listletoutput_list=reffalseletset_output_listo=output_list:=oletoutput_list()=!output_listletprint_list()=letdefs=List.filter(fundef->Name.selecteddef.name)!listinletdefs=List.sort(fund0d1->String.compared0.named1.name)defsinletoutputdef=Fmt.pr"%s@."def.nameinList.iteroutputdefsendletrun_test~long~namef=funv->ifnot(Name.selectedname)then()elsebeginiflong&¬(Long.run())thenbeginletpad=Test_fmt.long_strandcolor=Test_fmt.skip_colorinlog_pad_flush~pad~color"Test %s"nameendelsebeginletpad=iflongthenTest_fmt.long_strelseTest_fmt.paddinginlog_pad_flush~pad"Test %s"~color:Test_fmt.test_colorname;Atomic.incrrun_test_count;Atomic.setPass.count0;Atomic.setFail.count0;Atomic.setSkip.count0;begintryfvwith|Stop|Skip->()|exnwhenOs.exn_don't_catchexn->letbt=Printexc.get_raw_backtrace()inlog_exn_failbtexn;Fail.incr();end;ifAtomic.getFail.count<>0thenAtomic.incrFail.test_count;ifAtomic.getSkip.count<>0thenAtomic.incrSkip.test_countendendletmake~long~namefunc=iflongthenLong.incr();Def.incr();Def.add~longnamefunclettest?(long=false)namef=letfunc=Def.Unitfinmake~long~namefunc;run_test~long~nameflettest'arg?(long=false)namef=letfunc=Def.Arg(arg,f)inmake~long~namefunc;run_test~long~namefletautorun?(args=[])()=letexect=matcht.Def.funcwith|Unitf->run_test~long:t.long~name:t.namef()|Arg(arg,f)->matchArg.find_valueargargswith|Somearg->run_test~long:t.long~name:t.namefarg|None->tryskip"Test %s: missing test argument"t.Def.namewithSkip->Skip.incr_test()inList.iterexec(List.rev!Def.list)moduleCli=structletsetup~correct~diff_cmd~force_correct~seed~src_root~long~long_skip_exit~includes~excludes~output_list~locs:_=Name.set_includesincludes;Name.set_excludesexcludes;Def.set_output_listoutput_list;Long.set_runlong;Long.set_skip_exitlong_skip_exit;Patch.set_src_rootsrc_root;Diff.set_cmddiff_cmd;Rand.set_seedseed;Snapshot.set_correctcorrect;Snapshot.set_force_correctforce_correctopenCmdlinerletexit_long_skip=99letexits=Cmd.Exit.info1~doc:"on test failure."::Cmd.Exit.infoexit_long_skip~doc:"on success but skipped long tests if \
$(b,--long-skip-exit) is specified."::Cmd.Exit.defaultslets_common_options="COMMON TESTING OPTIONS"letdefault_man=[(* `S Cmdliner.Manpage.s_description; *)`SCmdliner.Manpage.s_arguments;`SCmdliner.Manpage.s_options;`Ss_common_options]letdocs=s_common_optionsletdiff_cmd=letdoc="$(docv) is the command used for making textual diffs. The tool is \
invoked as $(docv) $(b,exp) $(b,fnd) with $(b,exp) and $(b,fnd) the \
text files to compare. The tool should output a visual represention \
of the differences between the file contents. This representation
can be ANSI styled. If $(docv) is $(b,dumb) no external tool \
is invoked, an internal, non-helpful depiction of the value \
differences is used."inArg.(value&opt(some'~none:Diff.git_diff_cmdB0_std_cli.cmd)None&info["diff-cmd"]~doc~docv:"CMD"~docs)letcorrect=letdoc="Update expected snapshot mismatches to the snapshots computed \
during the run. See also $(b,--force-correct)."inletenv=Cmd.Env.info"CORRECT"inArg.(value&flag&info["c";Snapshot.cli_correct]~env~doc~docs)letforce_correct=letdoc="Force all expected snapshots to update to the snapshots \
computed during the run, regardless of their correctness. \
Use for example if you changed a snapshot printer."inArg.(value&flag&info[Snapshot.cli_force_correct]~doc~docs)letincludes=letdoc="Select tests whose lowercased name match prefix \
$(docv). Repeatable. The selection is filtered by \
$(b,-x) options which take over. Test names are printed \
during execution and can be (partially) listed with \
$(b,--list)."inletabsent="All tests are selected"inArg.(value&opt_allstring[]&info["i";"include"]~doc~docv:"PREFIX"~absent~docs)letexcludes=letdoc="Drop selected tests whose lowercased names match \
prefix $(docv), takes over $(b,-i). Repeatable."inArg.(value&opt_allstring[]&info["x";"exclude"]~doc~docv:"PREFIX"~docs)letlocs=letdoc="No effect. Option name reserved for the future."inArg.(value&flag&info["locs"]~doc~docs)letlong_skip_exit=letdoc="Exit with a special code on success but skipped long tests."inletenv=Cmd.Env.info"LONG_SKIP_EXIT"inArg.(value&flag&info["long-skip-exit"]~env~doc~docs)letoutput_list=letdoc="Do not run selected tests, output their names on $(b,stdout), \
one per line. Depending on how the test executable is defined \
the list may be incomplete since tests can be defined dynamically."inArg.(value&flag&info["list"]~doc~docs)letlong=letdoc="Do not skip long tests, run them."inletenv=Cmd.Env.info"LONG"inArg.(value&flag&info[Long.cli_l;"long"]~doc~env~docs)letsrc_root=letdoc="$(docv) is prependend to relative snapshot source file \
locations to look them up for patching."inArg.(value&opt(some'B0_std_cli.dirpath)None&info["src-root"]~doc~docs)letseed=letdoc="$(docv) is the integer used to seed pseudo-random generation."inletenv=Cmd.Env.infoRand.env_seedinletabsent="Automatically generated"inArg.(value&opt(someint)None&info[Rand.cli_seed]~absent~env~doc~docv:"SEED"~docs)letsetup=letopenCmdliner.Term.Syntaxinlet+seedand+correctand+force_correctand+diff_cmdand+src_rootand+longand+long_skip_exitand+includesand+excludesand+output_listand+locsinsetup~diff_cmd~correct~force_correct~seed~src_root~long~long_skip_exit~includes~excludes~output_list~locsend(* Main *)letmain_exit=Atomic.makeNoneletset_main_exitf=Atomic.setmain_exit(Somef)letreport_pass~dur=Skip.report();Long.report();Rand.reportlog_pass;Pass.report~durletreport_fail~dur=Snapshot.report_fail();Rand.reportlog_fail;Fail.report~durletmain'?(man=Cli.default_man)?doc?nameargsf=letrun()args=ifDef.output_list()then(Def.print_list();0)elselet()=Printexc.record_backtracetrueinletdur=Os.Mtime.counter()inletexit_maindur=matchAtomic.getmain_exitwith|Somef->f()|None->Patch.write_files();ifAtomic.getFail.test_count=0&&Atomic.getFail.count=0then(report_pass~dur;ifLong.skip_exit()&¬(Long.run())&&Atomic.getLong.count<>0thenCli.exit_long_skipelse0)else(report_fail~dur;1)intry(fargs;exit_maindur)with|Stop->exit_maindur|Skip->log_raw_flush"@[%a The test was %a in %a@]"Test_fmt.skip()Test_fmt.skipped()Test_fmt.dur(Os.Mtime.countdur);0|exnwhennot(Os.exn_don't_catchexn)->letbt=Printexc.get_raw_backtrace()inlog_exn_failbtexn;log_raw_flush"@[%a The test %a unexpectedly in %a@]"Test_fmt.fail()Test_fmt.failed()Test_fmt.dur(Os.Mtime.countdur);1inletname=matchnamewith|Somename->name|None->Filename.basenameSys.executable_nameinletinfo=Cmdliner.Cmd.info?doc~man~exits:Cli.exitsnameinletterm=Cmdliner.Term.(construn$Cli.setup$args)inletcmd=Cmdliner.Cmd.makeinfoterminCmdliner.Cmd.eval'cmdletmain?doc?namef=main'?doc(Cmdliner.Term.const())fmoduleFmt=Test_fmtendmoduleSnap=structmoduleT=Test.Tletexn=Test.(snapT.exn)letraise?ret?(exn=T.exn)?diff?__POS__:posfexp=letpos=matchposwithNone->Test.Snapshot.locexp|Somepos->posinTest.catch?ret~__POS__:posf@@funfnd->Test.snapexn?difffndexpletunit=Test.(snapT.unit)letbool=Test.(snapT.bool)letint=Test.(snapT.int)letint32=Test.(snapT.int32)letuint32=Test.(snapT.uint32)letint64=Test.(snapT.int64)letuint64=Test.(snapT.uint64)letnativeint=Test.(snapT.nativeint)letnativeuint=Test.(snapT.nativeuint)letnegative_parenst=letppppfv=ifFloat.sign_bitv&¬(Float.equalvneg_infinity)then(Fmt.parens(Test.T.ppt))ppfvelse(Test.T.pptppfv)inTest.T.with'~pptletfloat=Test.(snap(negative_parensT.float))lethex_float=Test.(snap(negative_parensT.hex_float))letchar=Test.(snapT.char)letstring=Test.snap~subst:Test.Snapshot.string_substTest.T.stringletlines=Test.snap~subst:Test.Snapshot.string_substTest.T.linesletascii_string=Test.snap~subst:Test.Snapshot.string_substTest.T.ascii_stringletline=Test.snap(Test.T.make~equal:String.equal~pp:Fmt.text_string_literal())letoptionsome=Test.(snap(T.optionsome))leteitherleftright=Test.(snap(T.either~left~right))letresultok=Test.(snap(T.result~ok))letresult'okerror=Test.(snap(T.result'~ok~error))letlistelt=Test.(snap(T.listelt))letarrayelt=Test.(snap(T.arrayelt))letpairfstsnd=Test.(snap(T.pairfstsnd))lett2=pairlett3v0v1v2=Test.(snap(T.t3v0v1v2))lett4v0v1v2v3=Test.(snap(T.t4v0v1v2v3))lett5v0v1v2v3v4=Test.(snap(T.t5v0v1v2v3v4))lett6v0v1v2v3v4v5=Test.(snap(T.t6v0v1v2v3v4v5))(* Command spawns. *)letstdout?__POS__?diff?env?cwd?stdin?stderr~trimcmdexp=matchOs.Cmd.run_status_out?env?cwd?stdin?stderr~trimcmdwith|Errore->Test.fail?__POS__"%s"e|Ok(_status,out)->string?diffoutexpendlet(!!)?(loc=("/dev/null",0,0,0))v=(loc,v)