12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288(*
QCheck: Random testing for OCaml
copyright (c) 2013-2017, Guillaume Bury, Simon Cruanes, Vincent Hugot,
Jan Midtgaard, Julien Debon, Valentin Chaboche
all rights reserved.
*)(** {1 Quickcheck inspired property-based testing} *)letpoly_compare=comparemoduleRS=struct(* Poor man's splitter for version < 5.0 *)(* This definition is shadowed by the [include] on OCaml >=5.0 *)(* For the record, this is a hack:
Seeding a child RNG based on the output of a parent RNG
does not create an independent RNG. *)(* copy of 4.14 Random.State.t to create a record of the right shape *)typers={st:intarray;mutableidx:int}[@@warning"-69"]letsplitrs:Random.State.t=letrs'={st=Array.init55(fun_i->Random.State.bitsrs);idx=0}infori=0to54dors'.st.(i)<-(rs'.st.(i)lxorrs'.st.((i+1)mod55))land0x3FFFFFFF;done;Obj.magicrs'(* sorry! *)includeRandom.State(* This is how OCaml 5.0 splits: *)(* Split a new PRNG off the given PRNG *)(*
let split s =
let i1 = bits64 s in let i2 = bits64 s in
let i3 = bits64 s in let i4 = bits64 s in
mk i1 i2 i3 i4
*)endletrecfoldn~f~init:acci=ifi=0thenaccelsefoldn~f~init:(facci)(i-1)let_opt_map_2~fab=matcha,bwith|Somex,Somey->Some(fxy)|_->Nonelet_opt_map_3~fabc=matcha,b,cwith|Somex,Somey,Somez->Some(fxyz)|_->Nonelet_opt_map_4~fabcd=matcha,b,c,dwith|Somex,Somey,Somez,Somew->Some(fxyzw)|_->Nonelet_opt_sumab=matcha,bwith|Some_,_->a|None,_->bletsum_int=List.fold_left(+)0letreclist_splitllenacc=matchlen,lwith|_,[]|0,_->List.revacc,l|_,x::xs->list_splitxs(len-1)(x::acc)letcut_exp_zeros=matchString.split_on_char'e'swith|[signif;exponent]->(matchexponent.[0]with(* int_of_string removes a leading '0' *)|'+'->Printf.sprintf"%se+%i"signif(int_of_stringexponent)(* keep a leading '+' *)|_->Printf.sprintf"%se%i"signif(int_of_stringexponent))(* keep a leading '-' *)|_->sexceptionFailed_precondition(* raised if precondition is false *)exceptionNo_example_foundofstring(* raised if an example failed to be found *)letassumeb=ifnotbthenraiseFailed_preconditionletassume_fail()=raiseFailed_preconditionlet(==>)b1b2=ifb1thenb2elseraiseFailed_precondition(** Enhancement of Stdlib [Seq] to backport some recent functions, and add a few useful others. *)moduleSeq=structincludeSeq(* The following functions are copied from https://github.com/ocaml/ocaml/blob/trunk/stdlib/seq.ml to support older OCaml versions. *)letrecunfoldfu()=matchfuwith|None->Nil|Some(x,u')->Cons(x,unfoldfu')letrecappendseq1seq2()=matchseq1()with|Nil->seq2()|Cons(x,next)->Cons(x,appendnextseq2)letconsxnext()=Cons(x,next)letrecforce_dropnxs=matchxs()with|Nil->Nil|Cons(_,xs)->letn=n-1inifn=0thenxs()elseforce_dropnxsletdropnxs=ifn<0theninvalid_arg"Seq.drop"elseifn=0thenxselsefun()->force_dropnxs(* End of copy of old functions. *)letis_empty(seq:_t):bool=matchseq()with|Nil->true|_->false(** Take at most [n] values. *)letrectake(n:int)(seq:_t):_t=fun()->match(n,seq())with|(0,_)|(_,Nil)->Nil|(n,Cons(a,rest))->Cons(a,take(n-1)rest)lethd(l:'at):'aoption=matchl()with|Nil->None|Cons(hd,_)->Somehd(** Useful to improve [Seq] code perf when chaining functions *)letapply(l:'at):'anode=l()endmoduleShrink=structmoduletypeNumber=sigtypetvalequal:t->t->boolvaldiv:t->t->tvaladd:t->t->tvalsub:t->t->tvalof_int:int->tendletnumber_towards(typea)(moduleNumber:Numberwithtypet=a)~(destination:a)(x:a):aSeq.t=fun()->Seq.unfold(funcurrent_shrink->ifNumber.equalcurrent_shrinkxthenNoneelse((* Halve the operands before subtracting them so they don't overflow.
Consider [number_towards min_int max_int] *)lethalf_diff=Number.sub(Number.divx(Number.of_int2))(Number.divcurrent_shrink(Number.of_int2))inifhalf_diff=Number.of_int0(* [current_shrink] is the last valid shrink candidate, put [x] as next step to make sure we stop *)thenSome(current_shrink,x)elseSome(current_shrink,Number.addcurrent_shrinkhalf_diff)))destination()letint_towardsdestinationx=fun()->letmoduleInt:Numberwithtypet=int=structincludeIntletof_int=Fun.idendinnumber_towards(moduleInt)~destinationx()letint32_towardsdestinationx=fun()->number_towards (moduleInt32)~destinationx()letint64_towardsdestinationx=fun()->number_towards(moduleInt64)~destinationx()(** Arbitrarily limit to 15 elements as dividing a [float] by 2 doesn't converge quickly
towards the destination. *)letfloat_towardsdestinationx=fun()->number_towards(moduleFloat)~destinationx|>Seq.take15|>Seq.applyletint_aggressive_towards(destination:int)(n:int):intSeq.t=fun()->Seq.unfold(funcurrent->ifcurrent=nthenNoneelseifcurrent<nthenletnext=succcurrentinSome(next,next)elseletnext=predcurrentinSome(next,next))destination()letint_aggressiven=fun()->int_aggressive_towards0n()endmoduleTree=structtype'at=Treeof'a*('at)Seq.tletroot(Tree(root,_):'at):'a=rootletchildren(Tree(_,children):'at):('at)Seq.t=childrenletrecpp?(depth:intoption)(inner_pp:Format.formatter->'a->unit)(ppf:Format.formatter)(t:'at):unit=letTree(x,xs)=tinletwrapper_boxppfinner=Format.fprintfppf"@[<hv2>Tree(@,%a@]@,)"inner()inletinnerppf()=Format.fprintfppf"@[<hv2>Node(@,%a@]@,),@ @[<hv>Shrinks("inner_ppx;ifOption.folddepth~none:false~some:(fundepth->depth<=0)then(Format.fprintfppf"<max depth reached>@])")elseifSeq.is_emptyxsthenFormat.fprintfppf"@])"else(Format.fprintfppf"@,%a@]@,)"(Format.pp_print_list~pp_sep:(funppf()->Format.fprintfppf",@ ")(pp?depth:(Option.mappreddepth)inner_pp))(List.of_seqxs);)inwrapper_boxppfinnerletrecmap(f:'a->'b)(a:'at):'bt=letTree(x,xs)=ainlety=fxinletys=fun()->Seq.map(funsmaller_x->mapfsmaller_x)xs()inTree(y,ys)(** Note that parameter order is reversed. *)let(>|=)af=mapfaletrecap(f:('a->'b)t)(a:'at):'bt=letTree(x0,xs)=ainletTree(f0,fs)=finlety=f0x0inletys=fun()->Seq.append(Seq.map(funf'->apf'a)fs)(Seq.map(funx'->apfx')xs)()inTree(y,ys)let(<*>)=apletliftA2(f:'a->'b->'c)(a:'at)(b:'bt):'ct=(a>|=f)<*>bletrecbind(a:'at)(f:'a->'bt):'bt=letTree(x,xs)=ainletTree(y,ys_of_x)=fxinletys_of_xs=fun()->Seq.map(funsmaller_x->bindsmaller_xf)xs()inletys=fun()->Seq.appendys_of_xsys_of_x()inTree(y,ys)let(>>=)=bindletpurex=Tree(x,Seq.empty)letrecmake_primitive(shrink:'a->'aSeq.t)(x:'a):'at=letshrink_trees=fun()->shrinkx|>Seq.map(make_primitiveshrink)|>Seq.applyinTree(x,shrink_trees)letrecopt(a:'at):'aoptiont=letTree(x,xs)=ainletshrinks=fun()->Seq.cons(pureNone)(Seq.mapoptxs)()inTree(Somex,shrinks)letrecsequence_list(l:'atlist):'alistt=matchlwith|[]->pure[]|hd::tl->liftA2List.conshd(sequence_listtl)letrecadd_shrink_invariant(p:'a->bool)(a:'at):'at=letTree(x,xs)=ainletxs'=fun()->Seq.filter_map(fun(Tree(x',_)ast)->ifpx'thenSome(add_shrink_invariantpt)elseNone)xs()inTree(x,xs')(** [applicative_take n trees] returns a tree of lists with at most the [n] first elements of the input list. *)letrecapplicative_take(n:int)(l:'atlist):'alistt=match(n,l)with|(0,_)|(_,[])->pure[]|(n,(tree::trees))->liftA2List.constree(applicative_take(predn)trees)(** [drop_one l []] returns all versions of [l] with one element removed, for example
[drop_one [1;2;3] [] = [ [2;3]; [1;3]; [1;2] ]] *)letrecdrop_one(l:'alist)(rev_prefix:'alist):'alistlist=matchlwith|[]->[]|x::xs->(List.revrev_prefix@xs)::drop_onexs(x::rev_prefix)letrecbuild_list_shrink_tree(l:'atlist):'alisttSeq.t=matchlwith|[]->Seq.empty|_::_->fun()->letlen=List.lengthliniflen<4thenletcandidates=drop_onel[]inList.fold_right(* try dropping each element in turn, starting with the list head *)(funcandacc->Seq.cons(Tree(List.maprootcand,build_list_shrink_treecand))acc)candidates(fun()->children(sequence_listl)())()(* otherwise, reduce element(s) *)elseletxs,ys=list_splitl((1+len)/2)[]inletxs_roots=List.maprootxsinletys_roots=List.maprootysin(* Try reducing a list [1;2;3;4] in halves: [1;2] and [3;4] *)Seq.cons(Tree(xs_roots,build_list_shrink_treexs))(Seq.cons(Tree(ys_roots,build_list_shrink_treeys))(fun()->(* Try dropping an element from either half: [2;3;4] and [1;2;4] *)letrest=List.tllinletrest_roots=List.maprootrestin(Seq.cons(Tree(rest_roots,build_list_shrink_treerest))(Seq.cons(Tree(xs_roots@(List.tlys_roots),build_list_shrink_tree(xs@(List.tlys))))(fun()->children(sequence_listl)())))(* at bottom: reduce elements *)()))()endmoduleGen=structtype'at=RS.t->'aTree.ttype'asized=int->RS.t->'aTree.tletmapfx=funst->Tree.mapf(xst)(** Note that parameter order is reversed. *)let(>|=)xf=mapfxlet(<$>)=mapletpure(a:'a):'at=fun_->Tree.purealetap(f:('a->'b)t)(x:'at):'bt=funst->letst'=RS.splitstinletftree=fstinletxtree=xst'inTree.apftreextreelet(<*>)=apletliftA2(f:'a->'b->'c)(a:'at)(b:'bt):'ct=(a>|=f)<*>bletliftA3(f:'a->'b->'c->'d)(a:'at)(b:'bt)(c:'ct):'dt=(a>|=f)<*>b<*>cletliftA4(f:'a->'b->'c->'d->'e)(a:'at)(b:'bt)(c:'ct)(d:'dt):'et=(a>|=f)<*>b<*>c<*>dletliftA5(f:'a->'b->'c->'d->'e->'f)(a:'at)(b:'bt)(c:'ct)(d:'dt)(e:'et):'ft=(a>|=f)<*>b<*>c<*>d<*>eletmap2=liftA2letmap3=liftA3letmap4=liftA4letmap5=liftA5letmap_keep_inputfx=funst->Tree.map(funa->a,fa)(xst)letreturn=pureletbind(gen:'at)(f:'a->('bt)):'bt=funst->letst'=RS.splitstinletgentree=genstinTree.bindgentree(funa->fa(RS.copyst'))let(>>=)=bindletsequence_list(l:'atlist):'alistt=funst->List.map(fungen->genst)l|>Tree.sequence_listletmake_primitive~(gen:RS.t->'a)~(shrink:'a->'aSeq.t):'at=funst->Tree.make_primitiveshrink(genst)letdelay(f:unit->'at):'at=funst->f()stletadd_shrink_invariant(p:'a->bool)(gen:'at):'at=funst->genst|>Tree.add_shrink_invariantpletset_shrinkshrinkgen=make_primitive~gen:(funst->genst|>Tree.root)~shrinkletno_shrink(gen:'at):'at=set_shrink(fun_->Seq.empty)genletparse_origin(loc:string)(pp:Format.formatter->'a->unit)~(origin:'a)~(low:'a)~(high:'a):'a=iforigin<lowtheninvalid_argFormat.(asprintf"%s: origin value %a is lower than low value %a"locpporiginpplow)elseiforigin>hightheninvalid_argFormat.(asprintf"%s: origin value %a is greater than high value %a"locpporiginpphigh)elseoriginletint_pos_small:intt=funst->letp=RS.floatst1.inletx=ifp<0.75thenRS.intst10elseRS.intst100inletshrinka=fun()->Shrink.int_towards0a()inTree.make_primitiveshrinkxletnat_small=int_pos_smallletsmall_nat=nat_small(** Natural number generator *)letnat:intt=funst->letp=RS.floatst1.inletx=ifp<0.5thenRS.intst10elseifp<0.75thenRS.intst100elseifp<0.95thenRS.intst1_000elseRS.intst10_000inletshrinka=fun()->Shrink.int_towards0a()inTree.make_primitiveshrinkxletint_pos_mid=natletbig_nat:intt=funst->letp=RS.floatst1.inifp<0.75thennatstelseletshrinka=fun()->Shrink.int_towards0a()inTree.make_primitiveshrink(RS.intst1_000_000)letunit:unitt=fun_st->Tree.pure()letbool:boolt=funst->letfalse_gen=Tree.purefalseinifRS.boolstthenTree.Tree(true,Seq.returnfalse_gen)elsefalse_genletfloat:floatt=funst->(* switch to [bits64] once lower bound reaches 4.14 *)(* Technically we could write [15] but this is clearer *)letfour_bits_mask=0b1111in(* Top 4 bits *)letleft=Int64.(shift_left(of_int(RS.bitsstlandfour_bits_mask))60)in(* Middle 30 bits *)letmiddle=Int64.(shift_left(of_int(RS.bitsst))30)in(* Bottom 30 bits *)letright=Int64.of_int(RS.bitsst)inletx=Int64.(float_of_bits(logorleft(logormiddleright)))inletshrinka=fun()->Shrink.float_towards0.a()inTree.make_primitiveshrinkxletfloat_pos:floatt=float>|=abs_floatletfloat_neg:floatt=float_pos>|=Float.negletpfloat=float_posletnfloat=float_negletfloat_bound_inclusive?(origin:float=0.)(bound:float):floatt=funst->let(low,high)=Float.min_max_num0.boundinletshrinka=fun()->letorigin=parse_origin"Gen.float_bound_inclusive"Format.pp_print_float~origin~low~highinShrink.float_towardsorigina()inletx=RS.floatstboundinTree.make_primitiveshrinkxletfloat_bound_exclusive?(origin:float=0.)(bound:float):floatt=ifbound=0.theninvalid_arg"Gen.float_bound_exclusive";funst->let(low,high)=Float.min_max_num0.boundinletshrinka=fun()->letorigin=parse_origin"Gen.float_bound_exclusive"Format.pp_print_float~origin~low~highinShrink.float_towardsorigina()inletbound=ifbound>0.thenbound-.epsilon_floatelsebound+.epsilon_floatinletx=RS.floatstboundinTree.make_primitiveshrinkxletpick_origin_within_range~low~high~goal=iflow>goalthenlowelseifhigh<goalthenhighelsegoalletfloat_range?(origin:floatoption)(low:float)(high:float):floatt=ifhigh<lowtheninvalid_arg"Gen.float_range: high < low"elseifhigh-.low>max_floattheninvalid_arg"Gen.float_range: high -. low > max_float";letorigin=parse_origin"Gen.float_range"Format.pp_print_float~origin:(Option.value~default:(pick_origin_within_range~low~high~goal:0.)origin)~low~highinletorigin=origin-.lowin(float_bound_inclusive~origin(high-.low))>|=(funx->low+.x)let(--.)lowhigh=float_range?origin:Nonelowhighletfloat_exp(mean:float)=ifFloat.is_nanmeantheninvalid_arg"Gen.float_exp";letunit_gen=no_shrink(float_bound_inclusive1.0)inletexp_gen=map(funp->-.mean*.(logp))unit_geninset_shrink(Shrink.float_towards0.)exp_gen(* See https://en.wikipedia.org/wiki/Relationships_among_probability_distributions *)letexponential=float_expletneg_int:intt=nat>|=Int.neg(** [option gen] shrinks towards [None] then towards shrinks of [gen]. *)letoption?(ratio:float=0.85)(gen:'at):'aoptiont=funst->letp=RS.floatst1.inifp<(1.-.ratio)thenTree.pureNoneelseTree.opt(genst)(** [opt] is an alias of {!val:option} for backward compatibility. *)letopt=optionletresult?(ratio:float=0.75)(ok_gen:'at)(err_gen:'et):('a,'e)resultt=funst->letp=RS.floatst1.inifp<(1.-.ratio)thenTree.map(fune->Errore)(err_genst)elseTree.map(funo->Oko)(ok_genst)(* Uniform positive random int generator.
We can't use {!RS.int} because the upper bound must be positive and is excluded,
so {!Int.max_int} would never be reached. We have to manipulate bits directly.
Note that the leftmost bit is used for negative numbers, so it must be [0].
{!RS.bits} only generates 30 bits, which is exactly enough on
32-bits architectures (i.e. {!Sys.int_size} = 31, i.e. 30 bits for positive numbers)
but not on 64-bits ones.
That's why for 64-bits, 3 30-bits segments are generated and shifted to craft a
62-bits number (i.e. {!Sys.int_size} = 63). The leftmost segment is masked to keep
only the last 2 bits.
The current implementation hard-codes 30/32/62/64 values, but technically we should
rely on {!Sys.int_size} to find the number of bits.
Note that we could also further generalize this function to merge it with [random_binary_string].
Technically this function is a special case of [random_binary_string] where the size is
{!Sys.int_size}.
*)letpint_raw:RS.t->int=ifSys.word_size=32thenfunst->RS.bitsstelse(* word size = 64 *)funst->(* Technically we could write [3] but this is clearer *)lettwo_bits_mask=0b11in(* Top 2 bits *)letleft=((RS.bitsstlandtwo_bits_mask)lsl60)in(* Middle 30 bits *)letmiddle=(RS.bitsstlsl30)in(* Bottom 30 bits *)letright=RS.bitsstinleftlormiddlelorrightletpint?(origin:int=0):intt=funst->letx=pint_rawstinletshrinka=fun()->letorigin=parse_origin"Gen.pint"Format.pp_print_int~origin~low:0~high:max_intinShrink.int_towardsorigina()inTree.make_primitiveshrinkxletint_pos=pint~origin:0letint_neg=int_pos>|=(funn->-n-1)letnumber_towards=Shrink.number_towardsletint_towards=Shrink.int_towardsletint64_towards=Shrink.int64_towardsletint32_towards=Shrink.int32_towardsletfloat_towards=Shrink.float_towardsletint:intt=bool>>=funb->ifbthenint_pos>|=(funn->-n-1)elseint_posletint_bound(n:int):intt=ifn<0theninvalid_arg"Gen.int_bound";funst->ifn<=(1lsl30)-2thenTree.make_primitive(funa()->Shrink.int_towards0a())(RS.intst(n+1))elseTree.map(funr->rmod(n+1))(int_posst)(** To support ranges wider than [Int.max_int], the general idea is to find the center,
and generate a random half-difference number as well as whether we add or
subtract that number from the center. *)letint_range?(origin:intoption)(low:int)(high:int):intt=ifhigh<lowtheninvalid_arg"Gen.int_range: high < low";funst->letTree.Tree(n,_shrinks)=iflow>=0||high<0then((* range smaller than max_int *)Tree.map(funn->low+n)(int_bound(high-low)st))else((* range potentially bigger than max_int: we split on 0 and
choose the interval with regard to their size ratio *)letf_low=float_of_intlowinletf_high=float_of_inthighinletratio=(-.f_low)/.(1.+.f_high-.f_low)inifRS.floatst1.<=ratiothenTree.map(funn->-n-1)(int_bound(-(low+1))st)elseint_boundhighst)inletshrinka=fun()->letorigin=matchoriginwith|None->pick_origin_within_range~low~high~goal:0|Someorigin->iforigin<lowtheninvalid_arg"Gen.int_range: origin < low"elseiforigin>hightheninvalid_arg"Gen.int_range: origin > high"elseorigininShrink.int_towardsorigina()inTree.make_primitiveshrinknlet(--)lowhigh=int_range?origin:Nonelowhighletoneof(l:'atlist):'at=int_bound(List.lengthl-1)>>=List.nthlletoneof_list(l:'alist):'at=int_bound(List.lengthl-1)>|=List.nthlletoneofl=oneof_listletoneof_array(a:'aarray):'at=int_bound(Array.lengtha-1)>|=Array.getaletoneofa=oneof_array(* NOTE: we keep this alias to not break code that uses [small_int]
for sizes of strings, arrays, etc. *)letsmall_int=small_natletint_small:intt=funst->ifRS.boolstthennat_smallstelse(nat_small>|=Int.neg)stletsmall_signed_int=int_small(** Shrink towards the first element of the list *)letoneof_weighted(l:(int*'at)list):'at=ifl=[]thenfailwith"QCheck2.Gen.oneof_weighted called with an empty list";letsums=sum_int(List.mapfstl)inifsums<1thenfailwith"QCheck2.Gen.oneof_weighted called with weight sum < 1";int_bound(sums-1)>>=funi->letrecauxacc=function|((x,g)::xs)->ifi<acc+xthengelseaux(acc+x)xs|_->assertfalseinaux0lletfrequency=oneof_weightedletoneof_list_weighted(l:(int*'a)list):'at=List.map(fun(weight,value)->(weight,purevalue))l|>oneof_weightedletfrequencyl=oneof_list_weightedletoneof_array_weighteda=oneof_list_weighted(Array.to_lista)letfrequencya=oneof_array_weightedletchar_range?(origin:charoption)(a:char)(b:char):chart=(int_range~origin:(Char.code(Option.value~default:aorigin))(Char.codea)(Char.codeb))>|=Char.chrletrandom_binary_string(length:int)(st:RS.t):string=(* 0b011101... *)lets=Bytes.create(length+2)inBytes.sets0'0';Bytes.sets1'b';fori=0tolength-1doBytes.sets(i+2)(ifRS.boolstthen'0'else'1')done;Bytes.unsafe_to_stringsletint32:int32t=funst->letx=random_binary_string32st|>Int32.of_stringinletshrinka=fun()->Shrink.int32_towards0la()inTree.make_primitiveshrinkxletui32:int32t=mapInt32.absint32letint64:int64t=funst->letx=random_binary_string64st|>Int64.of_stringinletshrinka=fun()->Shrink.int64_towards0La()inTree.make_primitiveshrinkxletui64:int64t=mapInt64.absint64(* A tail-recursive implementation over Tree.t *)letlist_size(size:intt)(gen:'at):'alistt=funst->letst'=RS.splitstinTree.bind(sizest)@@funsize->letst'=RS.copyst'in(* start each loop from same Random.State to recreate same element (prefix) *)letrecloopnacc=(* phase 1: build a list of element trees, tail recursively *)ifn<=0(* phase 2: build a list shrink Tree of element trees, tail recursively *)thenList.fold_left(funacct->Tree.liftA2List.constacc)(Tree.pure[])accelse(loop[@tailcall])(n-1)((genst')::acc)inloopsize[](** [list_ignore_size_tree] is a helper applying its own size shrinking heuristic,
and thus using only the root of [size]'s output shrink [Tree]. *)letlist_ignore_size_tree(size:intt)(gen:'at):'alistt=funst->letst'=RS.splitstinletsize=Tree.root(sizest)inletst'=RS.copyst'in(* start each loop from same Random.State to recreate same element (prefix) *)letrecloopnacc=(* phase 1: build a list of element trees, tail recursively *)ifn<=0(* phase 2: build a list shrink Tree of element trees, tail recursively *)thenletl=List.revaccinTree.Tree(List.mapTree.rootl,Tree.build_list_shrink_treel)else(loop[@tailcall])(n-1)((genst')::acc)inloopsize[]letlist(gen:'at):'alistt=list_ignore_size_treenatgenletlist_repeat(n:int)(gen:'at):'alistt=list_size(puren)genletarray_size(size:intt)(gen:'at):'aarrayt=(list_sizesizegen)>|=Array.of_listletarray(gen:'at):'aarrayt=listgen>|=Array.of_listletarray_repeat(n:int)(gen:'at):'aarrayt=list_repeatngen>|=Array.of_listletrecflatten_list(l:'atlist):'alistt=matchlwith|[]->pure[]|gen::gens->liftA2List.consgen(flatten_listgens)letflatten_l=flatten_listletflatten_array(a:'atarray):'aarrayt=Array.to_lista|>flatten_list>|=Array.of_listletflatten_a=flatten_arrayletflatten_option(o:'atoption):'aoptiont=matchowith|None->pureNone|Somegen->optiongenletflatten_opt=flatten_optionletflatten_result(res:('at,'e)result):('a,'e)resultt=matchreswith|Okgen->gen>|=Result.ok|Errore->pure(Errore)letflatten_res=flatten_resultletshuffle_array(a:'aarray):'aarrayt=funst->leta=Array.copyainfori=Array.lengtha-1downto1doletj=RS.intst(i+1)inlettmp=a.(i)ina.(i)<-a.(j);a.(j)<-tmp;done;Tree.purealetshuffle_a=shuffle_arrayletshuffle_list(l:'alist):'alistt=Array.of_listl|>shuffle_array>|=Array.to_listletshuffle_l=shuffle_listletshuffle_list_weighted(l:((int*'a)list)):'alistt=funst->letsample(w,v)=letTree.Tree(p,_)=float_bound_inclusive1.stinletfl_w=float_of_intwin(p**(1./.fl_w),v)inletsamples=List.rev_mapsamplelinsamples|>List.sort(fun(w1,_)(w2,_)->poly_comparew1w2)|>List.rev_mapsnd|>Tree.pureletshuffle_w_l=shuffle_list_weightedletpair(g1:'at)(g2:'bt):('a*'b)t=liftA2(funab->(a,b))g1g2lettriple(g1:'at)(g2:'bt)(g3:'ct):('a*'b*'c)t=(funabc->(a,b,c))<$>g1<*>g2<*>g3letquad(g1:'at)(g2:'bt)(g3:'ct)(g4:'dt):('a*'b*'c*'d)t=(funabcd->(a,b,c,d))<$>g1<*>g2<*>g3<*>g4lettup2=pairlettup3=triplelettup4=quadlettup5(g1:'at)(g2:'bt)(g3:'ct)(g4:'dt)(g5:'et):('a*'b*'c*'d*'e)t=(funabcde->(a,b,c,d,e))<$>g1<*>g2<*>g3<*>g4<*>g5lettup6(g1:'at)(g2:'bt)(g3:'ct)(g4:'dt)(g5:'et)(g6:'ft):('a*'b*'c*'d*'e*'f)t=(funabcdef->(a,b,c,d,e,f))<$>g1<*>g2<*>g3<*>g4<*>g5<*>g6lettup7(g1:'at)(g2:'bt)(g3:'ct)(g4:'dt)(g5:'et)(g6:'ft)(g7:'gt):('a*'b*'c*'d*'e*'f*'g)t=(funabcdefg->(a,b,c,d,e,f,g))<$>g1<*>g2<*>g3<*>g4<*>g5<*>g6<*>g7lettup8(g1:'at)(g2:'bt)(g3:'ct)(g4:'dt)(g5:'et)(g6:'ft)(g7:'gt)(g8:'ht):('a*'b*'c*'d*'e*'f*'g*'h)t=(funabcdefgh->(a,b,c,d,e,f,g,h))<$>g1<*>g2<*>g3<*>g4<*>g5<*>g6<*>g7<*>g8lettup9(g1:'at)(g2:'bt)(g3:'ct)(g4:'dt)(g5:'et)(g6:'ft)(g7:'gt)(g8:'ht)(g9:'it):('a*'b*'c*'d*'e*'f*'g*'h*'i)t=(funabcdefghi->(a,b,c,d,e,f,g,h,i))<$>g1<*>g2<*>g3<*>g4<*>g5<*>g6<*>g7<*>g8<*>g9(** Don't reuse {!int_range} which is much less performant (many more checks because of the possible range and origins). As a [string] generator may call this hundreds or even thousands of times for a single value, it's worth optimizing. *)letchar:chart=funst->letc=RS.intst256inletshrinka=fun()->Shrink.int_towards(int_of_char'a')a|>Seq.applyinTree.mapchar_of_int(Tree.make_primitiveshrinkc)(** The first characters are the usual lower case alphabetical letters to help shrinking. *)letprintable_chars:charlist=(* Left and right inclusive *)letrangeminmax=List.init(max-min+1)(funi->char_of_int(i+min))inleta=97inletz=122inletlower_alphabet=rangeazin(* ' ' *)letfirst_printable_char=32inletbefore_lower_alphabet=rangefirst_printable_char(a-1)in(* '~' *)letlast_printable_char=126inletafter_lower_alphabet=range(z+1)last_printable_charinletnewline=['\n']in(* Put alphabet first for shrinking *)List.flatten[lower_alphabet;before_lower_alphabet;after_lower_alphabet;newline]letchar_printable:chart=int_range~origin:00(List.lengthprintable_chars-1)>|=List.nthprintable_charsletprintable=char_printableletchar_numeral:chart=letzero=48inletnine=57inint_range~origin:zerozeronine>|=char_of_intletnumeral=char_numeralletbytes_size?(gen=char)(size:intt):bytest=funst->letopenTreeinletst'=RS.splitstinsizest>>=funsize->(* Adding char shrinks to a mutable list is expensive: ~20-30% cost increase *)(* Adding char shrinks to a mutable lazy list is less expensive: ~15% cost increase *)letst'=RS.copyst'in(* start char generation from same Random.State to recreate same char prefix (when size shrinking) *)letchar_trees_rev=ref[]inletbytes=Bytes.initsize(fun_->letchar_tree=genst'inchar_trees_rev:=char_tree::!char_trees_rev;(* Performance: return the root right now, the heavy processing of shrinks can wait until/if there is a need to shrink *)rootchar_tree)inletshrink=fun()->letchar_trees=List.rev!char_trees_revinletchar_list_tree=sequence_listchar_treesinletbytes_tree=char_list_tree>|=(funchar_list->letbytes=Bytes.createsizeinList.iteri(Bytes.setbytes)char_list;bytes)in(* Technically [bytes_tree] is the whole tree, but for perf reasons we eagerly created the root above *)childrenbytes_tree()inTree(bytes,shrink)letbytes_size_ofsizegen:bytest=bytes_size~gensizeletstring_size?(gen=char)(size:intt):stringt=bytes_size~gensize>|=Bytes.unsafe_to_stringletstring_size_ofsizegen:stringt=string_size~gensizeletbytes_of_char_listcs=letb=Buffer.create(List.lengthcs)inList.iter(func->Buffer.add_charbc)cs;letbytes=Buffer.to_bytesbinBuffer.clearb;bytesletbytes:bytest=listchar>|=bytes_of_char_listletbytes_ofgen=listgen>|=bytes_of_char_listletbytes_printable=listchar_printable>|=bytes_of_char_listletbytes_small=list_ignore_size_treenat_smallchar>|=bytes_of_char_listletbytes_small_ofgen=list_ignore_size_treenat_smallgen>|=bytes_of_char_listletstring_of_char_listcs=letb=Buffer.create(List.lengthcs)inList.iter(func->Buffer.add_charbc)cs;letstr=Buffer.contentsbinBuffer.clearb;strletstring:stringt=listchar>|=string_of_char_listletstring_ofgen=listgen>|=string_of_char_listletstring_printable=listchar_printable>|=string_of_char_listletstring_small=list_ignore_size_treenat_smallchar>|=string_of_char_listletstring_small_ofgen=list_ignore_size_treenat_smallgen>|=string_of_char_listletsmall_string?(gen=char)=string_small_ofgenletlist_smallgen=list_ignore_size_treenat_smallgenletsmall_list=list_smallletarray_smallgen=list_ignore_size_treenat_smallgen>|=Array.of_listletsmall_array=array_smallletjoin(gen:'att):'at=gen>>=Fun.id(* corner cases *)letgraft_corners(gen:'at)(corners:'alist)():'at=letcors=refcornersinfunst->match!corswith[]->genst|e::l->cors:=l;Tree.pureeletint_pos_corners=[0;1;2;max_int]letint_corners=int_pos_corners@[min_int;-2;-1]letint_small_corners():intt=graft_cornersint_smallint_corners()letsmall_int_corners():intt=graft_cornersnatint_pos_corners()(* sized, fix *)letsized_size(size:intt)(gen:'asized):'at=size>>=genletsized(gen:'asized):'at=sized_sizenatgenletfixf=letrecf'nst=ff'nstinf'letgenerate?(rand=RS.make_self_init())~(n:int)(gen:'at):'alist=list_repeatngenrand|>Tree.rootletgenerate1?(rand=RS.make_self_init())(gen:'at):'a=genrand|>Tree.rootletgenerate_tree?(rand=RS.make_self_init())(gen:'at):'aTree.t=genrandlet(let+)=(>|=)let(and+)=pairlet(let*)=(>>=)let(and*)=pairendmodulePrint=structtype'at='a->stringletunit_="()"letint=string_of_intletint32i=Int32.to_stringi^"l"letint64i=Int64.to_stringi^"L"letbool=string_of_boolletfloatf=(* Workaround for Windows and macOS to print negative nans consistently as "-nan" *)ifFloat.is_nanf&&Float.sign_bitfthen"-nan"elseifSys.win32(* Windows workaround to avoid leading exponent zero such as "-1.00001604579e-010" *)thenstring_of_floatf|>cut_exp_zeroelsestring_of_floatfletstrings=Printf.sprintf"%S"sletbytesb=string(Bytes.to_stringb)letcharc=Printf.sprintf"%C"cletoptionf=function|None->"None"|Somex->"Some ("^fx^")"letresultvpep=function|Errore->"Error ("^epe^")"|Okv->"Ok ("^vpv^")"letpairab(x,y)=Printf.sprintf"(%s, %s)"(ax)(by)lettripleabc(x,y,z)=Printf.sprintf"(%s, %s, %s)"(ax)(by)(cz)letquadabcd(x,y,z,w)=Printf.sprintf"(%s, %s, %s, %s)"(ax)(by)(cz)(dw)letlistppl=letb=Buffer.create25inBuffer.add_charb'[';List.iteri(funix->ifi>0thenBuffer.add_stringb"; ";Buffer.add_stringb(ppx))l;Buffer.add_charb']';Buffer.contentsbletarrayppa=letb=Buffer.create25inBuffer.add_stringb"[|";Array.iteri(funix->ifi>0thenBuffer.add_stringb"; ";Buffer.add_stringb(ppx))a;Buffer.add_stringb"|]";Buffer.contentsbletcontramapfpx=p(fx)letcomap=contramapletdefault=fun_->"<no printer>"lettup2p_ap_b(a,b)=Printf.sprintf"(%s, %s)"(p_aa)(p_bb)lettup2_optp_ap_b(a,b)=letp_a=Option.value~defaultp_ainletp_b=Option.value~defaultp_bintup2p_ap_b(a,b)lettup3p_ap_b(p_c)(a,b,c)=Printf.sprintf"(%s, %s, %s)"(p_aa)(p_bb)(p_cc)lettup3_optp_ap_bp_c(a,b,c)=letp_a=Option.value~defaultp_ainletp_b=Option.value~defaultp_binletp_c=Option.value~defaultp_cintup3p_ap_bp_c(a,b,c)lettup4p_ap_bp_cp_d(a,b,c,d)=Printf.sprintf"(%s, %s, %s, %s)"(p_aa)(p_bb)(p_cc)(p_dd)lettup4_optp_ap_bp_cp_d(a,b,c,d)=letp_a=Option.value~defaultp_ainletp_b=Option.value~defaultp_binletp_c=Option.value~defaultp_cinletp_d=Option.value~defaultp_dintup4p_ap_bp_cp_d(a,b,c,d)lettup5p_ap_bp_cp_dp_e(a,b,c,d,e)=Printf.sprintf"(%s, %s, %s, %s, %s)"(p_aa)(p_bb)(p_cc)(p_dd)(p_ee)lettup5_optp_ap_bp_cp_dp_e(a,b,c,d,e)=letp_a=Option.value~defaultp_ainletp_b=Option.value~defaultp_binletp_c=Option.value~defaultp_cinletp_d=Option.value~defaultp_dinletp_e=Option.value~defaultp_eintup5p_ap_bp_cp_dp_e(a,b,c,d,e)lettup6p_ap_bp_cp_dp_ep_f(a,b,c,d,e,f)=Printf.sprintf"(%s, %s, %s, %s, %s, %s)"(p_aa)(p_bb)(p_cc)(p_dd)(p_ee)(p_ff)lettup6_optp_ap_bp_cp_dp_ep_f(a,b,c,d,e,f)=letp_a=Option.value~defaultp_ainletp_b=Option.value~defaultp_binletp_c=Option.value~defaultp_cinletp_d=Option.value~defaultp_dinletp_e=Option.value~defaultp_einletp_f=Option.value~defaultp_fintup6p_ap_bp_cp_dp_ep_f(a,b,c,d,e,f)lettup7p_ap_bp_cp_dp_ep_fp_g(a,b,c,d,e,f,g)=Printf.sprintf"(%s, %s, %s, %s, %s, %s, %s)"(p_aa)(p_bb)(p_cc)(p_dd)(p_ee)(p_ff)(p_gg)lettup7_optp_ap_bp_cp_dp_ep_fp_g(a,b,c,d,e,f,g)=letp_a=Option.value~defaultp_ainletp_b=Option.value~defaultp_binletp_c=Option.value~defaultp_cinletp_d=Option.value~defaultp_dinletp_e=Option.value~defaultp_einletp_f=Option.value~defaultp_finletp_g=Option.value~defaultp_gintup7p_ap_bp_cp_dp_ep_fp_g(a,b,c,d,e,f,g)lettup8p_ap_bp_cp_dp_ep_fp_gp_h(a,b,c,d,e,f,g,h)=Printf.sprintf"(%s, %s, %s, %s, %s, %s, %s, %s)"(p_aa)(p_bb)(p_cc)(p_dd)(p_ee)(p_ff)(p_gg)(p_hh)lettup8_optp_ap_bp_cp_dp_ep_fp_gp_h(a,b,c,d,e,f,g,h)=letp_a=Option.value~defaultp_ainletp_b=Option.value~defaultp_binletp_c=Option.value~defaultp_cinletp_d=Option.value~defaultp_dinletp_e=Option.value~defaultp_einletp_f=Option.value~defaultp_finletp_g=Option.value~defaultp_ginletp_h=Option.value~defaultp_hintup8p_ap_bp_cp_dp_ep_fp_gp_h(a,b,c,d,e,f,g,h)lettup9p_ap_bp_cp_dp_ep_fp_gp_hp_i(a,b,c,d,e,f,g,h,i)=Printf.sprintf"(%s, %s, %s, %s, %s, %s, %s, %s, %s)"(p_aa)(p_bb)(p_cc)(p_dd)(p_ee)(p_ff)(p_gg)(p_hh)(p_ii)lettup9_optp_ap_bp_cp_dp_ep_fp_gp_hp_i(a,b,c,d,e,f,g,h,i)=letp_a=Option.value~defaultp_ainletp_b=Option.value~defaultp_binletp_c=Option.value~defaultp_cinletp_d=Option.value~defaultp_dinletp_e=Option.value~defaultp_einletp_f=Option.value~defaultp_finletp_g=Option.value~defaultp_ginletp_h=Option.value~defaultp_hinletp_i=Option.value~defaultp_iintup9p_ap_bp_cp_dp_ep_fp_gp_hp_i(a,b,c,d,e,f,g,h,i)end(** {2 Observe Values} *)moduleObservable=struct(** An observable is a (random) predicate on ['a] *)type-'at={print:'aPrint.t;eq:('a->'a->bool);hash:('a->int);}lethashox=o.hashxletequaloxy=o.eqxyletprintox=o.printxletmake?(eq=(=))?(hash=Hashtbl.hash)print={print;eq;hash;}moduleH=structletcombineab=Hashtbl.seeded_hashabletcombine_ffsx=Hashtbl.seeded_hashs(fx)letinti=ilandmax_intletint32(i:int32)=Hashtbl.hashiletint64(i:int64)=Hashtbl.hashiletboolb=ifbthen1else2letcharx=Char.codexletbytes(x:bytes)=Hashtbl.hashxletstring(x:string)=Hashtbl.hashxletoptionf=function|None->42|Somex->combine43(fx)letresultvheh=function|Errore->combine17(ehe)|Okv->combine19(vhv)letlistfl=List.fold_left(combine_ff)0x42lletarrayfl=Array.fold_left(combine_ff)0x42lletpairfg(x,y)=combine(fx)(gy)endmoduleEq=structtype'at='a->'a->boolletint:intt=(=)letint32:int32t=(=)letint64:int64t=(=)letbytes:bytest=(=)letstring:stringt=(=)letbool:boolt=(=)letfloat=Float.equalletunit()()=trueletchar:chart=(=)letreclistfl1l2=matchl1,l2with|[],[]->true|[],_|_,[]->false|x1::l1',x2::l2'->fx1x2&&listfl1'l2'letarrayeqab=letrecauxi=ifi=Array.lengthathentrueelseeqa.(i)b.(i)&&aux(i+1)inArray.lengtha=Array.lengthb&&aux0letoptionfo1o2=matcho1,o2with|None,None->true|Some_,None|None,Some_->false|Somex,Somey->fxyletresultokerrorr1r2=Result.equal~ok~errorr1r2letpairfg(x1,y1)(x2,y2)=fx1x2&&gy1y2endletunit:unitt=make~hash:(fun_->1)~eq:Eq.unitPrint.unitletbool:boolt=make~hash:H.bool~eq:Eq.boolPrint.boolletint:intt=make~hash:H.int~eq:Eq.intPrint.intletint32:int32t=make~hash:H.int32~eq:Eq.int32Print.int32letint64:int64t=make~hash:H.int64~eq:Eq.int64Print.int64letfloat:floatt=make~eq:Eq.floatPrint.floatletbytes=make~hash:H.bytes~eq:Eq.bytesPrint.bytesletstring=make~hash:H.string~eq:Eq.stringPrint.stringletchar=make~hash:H.char~eq:Eq.charPrint.charletoptionp=make~hash:(H.optionp.hash)~eq:(Eq.optionp.eq)(Print.optionp.print)letresultoprp=make~hash:(H.resultop.hashrp.hash)~eq:(Eq.resultop.eqrp.eq)(Print.resultop.printrp.print)letarrayp=make~hash:(H.arrayp.hash)~eq:(Eq.arrayp.eq)(Print.arrayp.print)letlistp=make~hash:(H.listp.hash)~eq:(Eq.listp.eq)(Print.listp.print)letcontramapfp=make~hash:(funx->p.hash(fx))~eq:(funxy->p.eq(fx)(fy))(funx->p.print(fx))letmap=contramapletpairab=make~hash:(H.paira.hashb.hash)~eq:(Eq.paira.eqb.eq)(Print.paira.printb.print)lettripleabc=contramap(fun(x,y,z)->x,(y,z))(paira(pairbc))letquadabcd=contramap(fun(x,y,z,u)->x,(y,z,u))(paira(triplebcd))endtype'astat=string*('a->int)(** A statistic on a distribution of values of type ['a] *)(** Internal module taking care of storing generated function bindings.
In essence, a generated function of type ['a -> 'b] is a map (table) where
keys are input values of type ['a] and values are output values of
type ['b], plus a default value of type ['b].
This module provides the "map of input/output" part.
*)modulePoly_tbl:sigtype('key,'value)tvalcreate:'keyObservable.t->?v_print:'valuePrint.t->'valueGen.t->int->('key,'value)tGen.tvalget:('key,'value)t->'key->'valueoptionvalsize:('value->int)->('key,'value)t->intvalprint:('key,'value)tPrint.tend=structtype('key,'value)t={get:'key->'valueoption;(** Don't be fooled by its name and signature: this function mutates the table during test execution by adding entries (key is the value on which the function is applied in the test, and the value is generated on the fly). *)p_size:('value->int)->int;p_print:unit->string;p_tree_bindings_rev:('key*'valueTree.t)listref;}letcreate(typek)(typev)(k_obs:kObservable.t)?(v_print:vPrint.toption)(v_gen:vGen.t)(size:int):(k,v)tGen.t=funst->letmoduleT=Hashtbl.Make(structtypet=kletequal=k_obs.Observable.eqlethash=k_obs.Observable.hashend)in(* split random state to avoid later failed [get]s to side-effect the current [st] *)letst'=RS.splitstin(* make a table
@param extend if [true], extend table [tbl] on the fly (during test execution, to "record" input values and generate an associated output value). [false] during shrinking (use the default value if the input value is not in the table). *)letmake~extendtbl=letinitial_tree_bindings_rev=T.to_seqtbl|>List.of_seq|>List.rev_map(fun(k,v)->k,Tree.purev)inletp_tree_bindings_rev=refinitial_tree_bindings_revinletget=(funkey->trySome(T.findtblkey)withNot_found->ifextendthen((* Generate a new value and "record" the binding for potential future display/shrinking *)letvalue_tree=v_genst'inp_tree_bindings_rev:=(key,value_tree)::!p_tree_bindings_rev;letv=Tree.rootvalue_treeinT.addtblkeyv;Somev)elseNone)inletp_print=(fun()->letpp_v=Option.value~default:(fun_->"<opaque>")v_printinletb=Buffer.create64inletto_b=Format.formatter_of_bufferbinT.iter(funkeyvalue->Format.fprintfto_b"%s -> %s; "(k_obs.Observable.printkey)(pp_vvalue))tbl;Format.pp_print_flushto_b();Buffer.contentsb)inletp_size=(funsize_v->T.fold(fun_vn->n+size_vv)tbl0)in{get;p_print;p_size;p_tree_bindings_rev}inletroot_tbl=T.createsizein(* During initial running of the test, record bindings, hence [~extend:true]. *)letroot=make~extend:trueroot_tblin(* Build the (lazy!) shrink tree of tables here *)letshrinks:(k,v)tTree.tSeq.t=fun()->(* This only gets evaluated *after* the test was run for [tbl], meaning it is correctly
populated with bindings recorded during the test already *)letcurrent_bindings:(k*vTree.t)list=List.rev!(root.p_tree_bindings_rev)inletcurrent_tree_bindings:(k*v)Tree.tlist=List.map(fun(k,tree)->Tree.map(funv->(k,v))tree)current_bindingsinletshrunk_bindings_tree_seq:(k*v)listTree.tSeq.t=Tree.build_list_shrink_treecurrent_tree_bindingsin(* During shrinking, we don't want to record/add bindings, so [~extend:false]. *)letshrunk_poly_tbl_tree_seq:(k,v)tTree.tSeq.t=Seq.map(funt->Tree.map(funbindings->List.to_seqbindings|>T.of_seq|>make~extend:false)t)shrunk_bindings_tree_seqin(* [shrunk_poly_tbl_tree_seq] is a bit misleading: its head *should* be the same as [root] but because of the required laziness
induced by the mutation of bindings, we don't use it, only graft its tail to the original [root]. *)Seq.drop1shrunk_poly_tbl_tree_seq()inTree.Tree(root,shrinks)letgettx=t.getxletprintt=t.p_print()letsizept=t.p_sizepend(** Internal representation of functions, used for shrinking and printing (in case of error). *)type('a,'b)fun_repr_tbl={fun_tbl:('a,'b)Poly_tbl.t;(** Input-output bindings *)fun_print:'bPrint.toption;(** How to print output values *)fun_default:'b;(** Default value for all inputs not explicitly mapped in {!fun_tbl} *)}type'ffun_repr=|Fun_tbl:('a,'ret)fun_repr_tbl->('a->'ret)fun_repr(** Input-output list of bindings *)|Fun_map:('f1->'f2)*'f1fun_repr->'f2fun_repr(** Mapped from another function (typically used for currying) *)(** A QCheck function, as in Koen Claessen's paper "Shrinking and showing functions".
Such a function is a pair of the function representation (used for shrinking and
printing the function) and a "real" function, which can be seen as an input-output
map + a default value for all other inputs.
- Test developers will only use the "real" function inside their tests (and ignore the function representation).
- During shrinking/printing, QCheck will ignore the "real" function and only use its representation.
*)type'ffun_=Funof'ffun_repr*'f(** Reifying functions *)moduleFn=structletapply(Fun(_repr,real_function))=real_function(** [function_of_repr repr] creates the "real" function (that will be used in tests)
from its representation. *)letrecfunction_of_repr:typef.ffun_repr->f=function|Fun_tbl{fun_tbl;fun_default;_}->(funx->matchPoly_tbl.getfun_tblxwith|None->fun_default|Somey->y)|Fun_map(g,sub_repr)->g(function_of_reprsub_repr)letmake_(r:'afun_repr):'afun_=Fun(r,function_of_reprr)letmk_reprtbl?printdef=Fun_tbl{fun_tbl=tbl;fun_print=print;fun_default=def;}letmap_reprfrepr=Fun_map(f,repr)letmap_funf(Fun(repr,_real_function))=make_(map_reprfrepr)(** [print_rep repr] returns a string representation of [repr]. *)letprint_reprr=letbuf=Buffer.create32inletrecaux:typef.Buffer.t->ffun_repr->unit=funbufr->matchrwith|Fun_map(_,sub_repr)->auxbufsub_repr|Fun_tblr->Buffer.add_stringbuf(Poly_tbl.printr.fun_tbl);Printf.bprintfbuf"_ -> %s"(matchr.fun_printwith|None->"<opaque>"|Someprint->printr.fun_default);inPrintf.bprintfbuf"{";auxbufr;Printf.bprintfbuf"}";Buffer.contentsbufletprint(Fun(repr,_real_function))=print_reprrepr(** [gen_rep obs gen] creates a function generator. Input values are observed with [obs] and
output values are generated with [gen]. *)letgen_rep(obs:'aObservable.t)?(print:'bPrint.toption)(gen:'bGen.t):('a->'b)fun_reprGen.t=Gen.liftA2(fundefault_valuepoly_tbl->mk_reprpoly_tbl?printdefault_value)gen(Poly_tbl.create?v_print:printobsgen8)letgen(obs:'aObservable.t)?(print:'bPrint.toption)(gen:'bGen.t):('a->'b)fun_Gen.t=Gen.mapmake_(gen_repobsgen?print)endletfun1obs?printgen=Fn.genobs?printgenmoduleTuple=struct(** heterogeneous list (generic tuple) used to uncurry functions *)type'at=|Nil:unitt|Cons:'a*'bt->('a*'b)tletnil=Nilletconsxtail=Cons(x,tail)type'aobs=|O_nil:unitobs|O_cons:'aObservable.t*'bobs->('a*'b)obsleto_nil=O_nilleto_consxtail=O_cons(x,tail)letrechash:typea.aobs->at->int=funot->matcho,twith|O_nil,Nil->42|O_cons(o,tail_o),Cons(x,tail)->Observable.H.combine(Observable.hashox)(hashtail_otail)letrecequal:typea.aobs->at->at->bool=funoab->matcho,a,bwith|O_nil,Nil,Nil->true|O_cons(o,tail_o),Cons(x1,tail1),Cons(x2,tail2)->Observable.equalox1x2&&equaltail_otail1tail2letprintotup=letrecaux:typea.aobs->Buffer.t->at->unit=funobuft->matcho,twith|O_nil,Nil->Printf.bprintfbuf"()"|O_cons(o,O_nil),Cons(x,Nil)->Printf.bprintfbuf"%s"(Observable.printox)|O_cons(o,tail_o),Cons(x,tail)->Printf.bprintfbuf"%s, %a"(Observable.printox)(auxtail_o)tailinletbuf=Buffer.create64inBuffer.add_stringbuf"(";auxobuftup;Buffer.add_stringbuf")";Buffer.contentsbufletobservable(o:'aobs):'atObservable.t=Observable.make~eq:(equalo)~hash:(hasho)(printo)letgen(o:'aobs)?(print:'bPrint.toption)(ret:'bGen.t):('at->'b)fun_Gen.t=Fn.gen(observableo)?printretmoduleInfix=structlet(@::)xtail=consxtaillet(@->)otail=o_consotailendincludeInfixendletfun_nary(o:_Tuple.obs)?printret:_Gen.t=Tuple.geno?printretletfun2o1o2?printret=Gen.map(Fn.map_fun(fungxy->gTuple.(x@::y@::nil)))(fun_naryTuple.(o1@->o2@->o_nil)?printret)letfun3o1o2o3?printret=Gen.map(Fn.map_fun(fungxyz->gTuple.(x@::y@::z@::nil)))(fun_naryTuple.(o1@->o2@->o3@->o_nil)?printret)letfun4o1o2o3o4?printret=Gen.map(Fn.map_fun(fungxyzw->gTuple.(x@::y@::z@::w@::nil)))(fun_naryTuple.(o1@->o2@->o3@->o4@->o_nil)?printret)moduleTestResult=structtype'acounter_ex={instance:'a;(** The counter-example(s) *)shrink_steps:int;(** How many shrinking steps for this counterex *)msg_l:stringlist;(** messages. @since 0.7 *)}(** Result state.
changed in 0.10 (move to inline records) *)type'astate=|Success|Failedof{instances:'acounter_exlist;(** Failed instance(s) *)}|Failed_otherof{msg:string}|Errorof{instance:'acounter_ex;exn:exn;backtrace:string;}(** Error, backtrace, and instance that triggered it *)(* result returned by running a test *)type'at={mutablestate:'astate;mutablecount:int;(* number of tests *)mutablecount_gen:int;(* number of generated cases *)collect_tbl:(string,int)Hashtbl.tlazy_t;stats_tbl:('astat*(int,int)Hashtbl.t)list;mutablewarnings:stringlist;}letget_state{state;_}=stateletget_count{count;_}=countletget_count_gen{count_gen;_}=count_gen(* indicate failure on the given [instance] *)letfail~msg_l~steps:shrink_stepsresinstance=letc_ex={instance;shrink_steps;msg_l;}inmatchres.statewith|Success->res.state<-Failed{instances=[c_ex]}|Error_|Failed_other_->()|Failed{instances=[]}->assertfalse|Failed{instances=l}->res.state<-Failed{instances=c_ex::l}leterror~msg_l~stepsresinstanceexnbacktrace=res.state<-Error{instance={instance;shrink_steps=steps;msg_l;};exn;backtrace}letget_collectr=ifLazy.is_valr.collect_tblthenSome(Lazy.forcer.collect_tbl)elseNoneletcollect=get_collectletget_statsr=r.stats_tblletstats=get_statsletget_warningsr=r.warningsletwarnings=get_warningsletis_successr=matchr.statewith|Success->true|Failed_|Error_|Failed_other_->falseletis_failedr=matchr.statewith|Failed_->true|Success|Error_|Failed_other_->falseendmoduleTest_exceptions=structexceptionTest_failofstring*stringlistexceptionTest_errorofstring*string*exn*stringexceptionTest_unexpected_successofstringendmoduleTest=structtype'acell={count:int;(* number of tests to do *)long_factor:int;(* multiplicative factor for long test count *)positive:bool;(* indicates whether test is considered positive or negative *)max_gen:int;(* max number of instances to generate (>= count) *)max_fail:int;(* max number of failures *)retries:int;(* max number of retries during shrinking *)law:'a->bool;(* the law to check *)gen:'aGen.t;(* how to generate/shrink instances *)print:'aPrint.toption;(* how to print values *)collect:('a->string)option;(* collect values by tag, useful to display distribution of generated *)stats:'astatlist;(* distribution of values of type 'a *)qcheck1_shrink:('a->('a->unit)->unit)option;(* QCheck1-backward-compatible shrinking *)if_assumptions_fail:[`Fatal|`Warning]*float;mutablename:string;(* name of the law *)}typet=|Test:'acell->tletget_name{name;_}=nameletset_namecname=c.name<-nameletget_law{law;_}=lawletget_gen{gen;_}=genletget_print_opt{print;_}=printletget_collect_opt{collect;_}=collectletget_stats{stats;_}=statsletget_count{count;_}=countletget_long_factor{long_factor;_}=long_factorletget_positive{positive;_}=positiveletdefault_count=100letdefault_long_factor=1letglobal_nonnegative_vardefaultenv_varvar=letvar=match(var,Sys.getenv_optenv_var)with|(Somex,_)->x|(_,Somex)->int_of_stringx|(None,None)->defaultinifvar<0theninvalid_arg(env_var^" must be >= 0 but value is "^string_of_intvar)elsevarletglobal_countcount=global_nonnegative_vardefault_count"QCHECK_COUNT"countletglobal_long_factorlong_factor=global_nonnegative_vardefault_long_factor"QCHECK_LONG_FACTOR"long_factorletfresh_name=letr=ref0in(fun()->incrr;Printf.sprintf"anon_test_%d"!r)letdefault_if_assumptions_fail=`Warning,0.05letmake_cell?(if_assumptions_fail=default_if_assumptions_fail)?(count)?long_factor?(negative=false)?max_gen?(max_fail=1)?(retries=1)?(name=fresh_name())?print?collect?(stats=[])genlaw=letcount=global_countcountinletlong_factor=global_long_factorlong_factorinletpositive=notnegativeinletmax_gen=matchmax_genwithNone->count+200|Somex->xin{law;gen;collect;print;stats;max_gen;max_fail;retries;name;count;long_factor;positive;if_assumptions_fail;qcheck1_shrink=None;}letmake_cell_from_QCheck1?(if_assumptions_fail=default_if_assumptions_fail)?(count)?long_factor?(negative=false)?max_gen?(max_fail=1)?(retries=1)?(name=fresh_name())~gen?shrink?print?collect~statslaw=letcount=global_countcountinletlong_factor=global_long_factorlong_factorinletpositive=notnegativein(* Make a "fake" QCheck2 arbitrary with no shrinking *)letfake_gen=Gen.make_primitive~gen~shrink:(fun_->Seq.empty)inletmax_gen=matchmax_genwithNone->count+200|Somex->xin{law;gen=fake_gen;print;collect;stats;max_gen;max_fail;retries;name;count;long_factor;positive;if_assumptions_fail;qcheck1_shrink=shrink;}letmake'?if_assumptions_fail?count?long_factor?max_gen?max_fail?retries?name?print?collect?stats~negativearblaw=Test(make_cell?if_assumptions_fail?count?long_factor?max_gen?max_fail?retries?name?print?collect?stats~negativearblaw)letmake=make'~negative:falseletmake_neg=make'~negative:truelettest_get_count(Testcell)=get_countcelllettest_get_long_factor(Testcell)=get_long_factorcell(** {6 Running the test} *)moduleR=TestResult(* Result of an instance run *)typeres=|Success|Failure|FalseAssumption|Errorofexn*string(* Step function, called after each instance test *)type'astep=string->'acell->'a->res->unitletstep_nil_____=()(* Events of a test *)type'aevent=|Generating|Collectingof'a|Testingof'a|Shrunkofint*'a|Shrinkingofint*int*'atype'ahandler=string->'acell->'aevent->unitlethandler_nil____=()(* state required by {!check} to execute *)type'astate={test:'acell;step:'astep;handler:'ahandler;rand:RS.t;res:'aTestResult.t;mutablecur_count:int;(** number of iterations remaining to do *)mutablecur_max_gen:int;(** maximum number of generations allowed *)mutablecur_max_fail:int;(** maximum number of counter-examples allowed *)}letis_donestate=state.cur_count<=0||state.cur_max_gen<=0letdecr_countstate=state.res.R.count<-state.res.R.count+1;state.cur_count<-state.cur_count-1letnew_input_treestate=state.res.R.count_gen<-state.res.R.count_gen+1;state.cur_max_gen<-state.cur_max_gen-1;state.test.genstate.rand(* statistics on inputs *)letcollectsti=matchst.test.collectwith|None->()|Somef->letkey=fiinlet(lazytbl)=st.res.R.collect_tblinletn=tryHashtbl.findtblkeywithNot_found->0inHashtbl.replacetblkey(n+1)letupdate_statssti=List.iter(fun((_,f),tbl)->letkey=fiinletn=tryHashtbl.findtblkeywithNot_found->0inHashtbl.replacetblkey(n+1))st.res.R.stats_tbltyperes_or_exn=|Shrink_fail|Shrink_exnofexn(* triggered by user to fail with a message *)exceptionUser_failofstringletfail_reportm=raise(User_failm)letfail_reportfm=letbuf=Buffer.create64inFormat.kfprintf(funout->Format.fprintfout"@?";fail_report(Buffer.contentsbuf))(Format.formatter_of_bufferbuf)mtype'arun_res=|Run_ok|Run_failofstringlist(* run_law is a helper function for testing a property [law] on a
generated input [x].
When passed a ~retries number n>1, the tested property is checked
n times for each shrunk input candidate. The default value is 1,
thus causing no change in behaviour.
Retrying a property can be useful when testing non-deterministic
code with QCheck, e.g., for multicore execution. The idea is
described in
'Testing a Database for Race Conditions with QuickCheck'
Hughes and Bolinder, Erlang 2011, Sec.6:
"As we explained in section 4, we ensure that tests fail when
races are present simply by repeating each test a large number of
times, and by running on a dual core machine. We obtained the
minimal failing cases in the previous section by repeating each
test 100 times during shrinking: thus we stopped shrinking a test
case only when all of its candidate shrinkings passed 100 tests
in a row." *)letrun_law~retrieslawx=letrecloopi=matchlawxwith|false->Run_fail[]|true->ifi<=1thenRun_okelseloop(i-1)intryloopretrieswithUser_failmsg->Run_fail[msg](* QCheck1-compatibility code *)exceptionIter_exitletiter_find_mappiter=letr=refNonein(tryiter(funx->matchpxwithSome_asy->r:=y;raiseIter_exit|None->())withIter_exit->());!r(* try to shrink counter-ex [i] into a smaller one. Returns
shrinked value and number of steps *)letshrinkst(i_tree:'aTree.t)(r:res_or_exn)m:'a*res_or_exn*stringlist*int=letis_err=matchrwith|Shrink_exn_->true|_->falseinletrecshrink_sti_treerm~steps=letTree.Tree(i,shrinks)=i_treeinst.handlerst.test.namest.test(Shrunk(steps,i));letcount=ref0inleti'=matchst.test.qcheck1_shrinkwith|Somef->(* QCheck1-compatibility, copied almost verbatim from QCheck.ml old code *)iter_find_map(funx->(* let Tree.Tree (x, _) = x_tree in *)tryincrcount;st.handlerst.test.namest.test(Shrinking(steps,!count,x));beginmatchrun_law~retries:st.test.retriesst.test.lawxwith|Run_failmwhennotis_err->Some(Tree.purex,Shrink_fail,m)|_->Noneendwith|Failed_precondition|No_example_found_->None|ewhenis_err->Some(Tree.purex,Shrink_exne,[])(* fail test (by error) *))(fi)|None->(* QCheck2 (or QCheck1 with a shrinkless tree): use the shrink tree *)Seq.filter_map(funx_tree->letTree.Tree(x,_)=x_treeintryincrcount;st.handlerst.test.namest.test(Shrinking(steps,!count,x));beginmatchrun_law~retries:st.test.retriesst.test.lawxwith|Run_failmwhennotis_err->Some(x_tree,Shrink_fail,m)|_->Noneendwith|Failed_precondition|No_example_found_->None|ewhenis_err->Some(x_tree,Shrink_exne,[])(* fail test (by error) *))shrinks|>Seq.hdinmatchi'with|None->i,r,m,steps|Some(i_tree',r',m')->shrink_sti_tree'r'm'~steps:(steps+1)(* shrink further *)inshrink_~steps:0sti_treermtype'acheck_result=|CR_continue|CR_yieldof'aTestResult.t(* test raised [e] on [input]; try to shrink then fail *)lethandle_exnstateinputebt:_check_result=(* first, shrink
TODO: shall we shrink differently (i.e. expected only an error)? *)letinput,r,msg_l,steps=shrinkstateinput(Shrink_exne)[]in(* recover exception of shrunk input *)lete=matchrwith|Shrink_fail->e|Shrink_exne'->e'instate.stepstate.test.namestate.testinput(Error(e,bt));R.errorstate.res~steps~msg_linputebt;CR_yieldstate.res(* test failed on [input], which means the law is wrong. Continue if
we should. *)lethandle_failstateinputmsg_l:_check_result=(* first, shrink *)letinput,_,msg_l,steps=shrinkstateinputShrink_failmsg_lin(* fail *)decr_countstate;state.stepstate.test.namestate.testinputFailure;state.cur_max_fail<-state.cur_max_fail-1;R.failstate.res~steps~msg_linput;ifstate.cur_max_fail>0thenCR_continueelseCR_yieldstate.res(* [check_state state] applies [state.test] repeatedly ([iter] times)
on output of [test.rand], and if [state.test] ever returns false,
then the input that caused the failure is returned in [Failed].
If [func input] raises [Failed_precondition] then the input is discarded, unless
max_gen is 0. *)letreccheck_statestate:_R.t=ifis_donestatethenstate.reselse(state.handlerstate.test.namestate.testGenerating;matchnew_input_treestatewith|i_tree->check_state_inputstatei_tree|exceptione->(* turn it into an error *)letbt=Printexc.get_backtrace()inletmsg=Printf.sprintf"ERROR: uncaught exception in generator for test %s after %d steps:\nException: %s\nBacktrace: %s"state.test.namestate.test.count(Printexc.to_stringe)btinstate.res.R.state<-R.Failed_other{msg};state.res)andcheck_state_inputstateinput_tree=letTree.Tree(input,_)=input_treeinstate.handlerstate.test.namestate.test(Collectinginput);collectstateinput;update_statsstateinput;letres=trystate.handlerstate.test.namestate.test(Testinginput);beginmatchrun_law~retries:1state.test.lawinputwith|Run_ok->(* one test ok *)decr_countstate;state.stepstate.test.namestate.testinputSuccess;CR_continue|Run_failmsg_l->handle_failstateinput_treemsg_lendwith|Failed_precondition|No_example_found_->state.stepstate.test.namestate.testinputFalseAssumption;CR_continue|e->letbt=Printexc.get_backtrace()inhandle_exnstateinput_treeebtinmatchreswith|CR_continue->check_statestate|CR_yieldx->xtype'acallback=string->'acell->'aTestResult.t->unitletcallback_nil_:_callback=fun___->()(* check that there are sufficiently many tests which passed, to avoid
the case where they all passed by failed precondition *)letcheck_if_assumptionstarget_countcellres:unit=letpercentage_of_count=float_of_intres.R.count/.float_of_inttarget_countinletassm_flag,assm_frac=cell.if_assumptions_failinifR.is_successres&&percentage_of_count<assm_fracthen(letmsg=format_of_string"%s: \
only %.1f%% tests (of %d) passed precondition for %S\n\n\
NOTE: it is likely that the precondition is too strong, or that \
the generator is buggy.\n%!"inmatchassm_flagwith|`Warning->letmsg=Printf.sprintfmsg"WARNING"(percentage_of_count*.100.)cell.countcell.nameinres.R.warnings<-msg::res.R.warnings|`Fatal->(* turn it into an error *)letmsg=Printf.sprintfmsg"ERROR"(percentage_of_count*.100.)cell.countcell.nameinres.R.state<-R.Failed_other{msg})(* main checking function *)letcheck_cell?(long=false)?(call=callback_nil_)?(step=step_nil_)?(handler=handler_nil_)?(rand=RS.make[|0|])cell=letfactor=iflongthencell.long_factorelse1inlettarget_count=factor*cell.countinletstate={test=cell;rand;step;handler;cur_count=target_count;cur_max_gen=factor*cell.max_gen;cur_max_fail=factor*cell.max_fail;res={R.state=R.Success;count=0;count_gen=0;collect_tbl=lazy(Hashtbl.create10);warnings=[];stats_tbl=List.map(funstat->stat,Hashtbl.create10)cell.stats;};}inletres=check_statestateincheck_if_assumptionstarget_countcellres;callcell.namecellres;resincludeTest_exceptions(* print instance using [arb] *)letprint_instancearbi=matcharb.printwith|None->"<instance>"|Somepp->ppiletprint_c_exarbc:string=letbuf=Buffer.create64inbeginifc.R.shrink_steps>0thenPrintf.bprintfbuf"%s (after %d shrink steps)"(print_instancearbc.R.instance)c.R.shrink_stepselseBuffer.add_stringbuf(print_instancearbc.R.instance)end;List.iter(funmsg->Buffer.add_charbuf'\n';Buffer.add_stringbufmsg;Buffer.add_charbuf'\n')c.R.msg_l;Buffer.contentsbufletpp_print_test_failnameoutl=letrecpp_listout=function|[]->()|[x]->Format.pp_print_stringoutx|x::y->Format.fprintfout"%s@,%a"xpp_listyinFormat.fprintfout"@[test `%s`@ failed on ≥ %d cases:@ @[<v>%a@]@]@,"name(List.lengthl)pp_listlletasprintffmt=letbuf=Buffer.create128inletout=Format.formatter_of_bufferbufinFormat.kfprintf(fun_->Buffer.contentsbuf)outfmtletprint_test_failnamel=asprintf"@[%a@]@?"(pp_print_test_failname)lletprint_unexpected_successname=Format.sprintf"@[negative test `%s`@ succeeded unexpectedly@]"nameletprint_test_errornameiestack=Format.sprintf"@[test `%s`@ raised exception `%s`@ on `%s`@,%s@]"name(Printexc.to_stringe)istackletprint_collectc=letout=Buffer.create64inlettotal,lab_len=Hashtbl.fold(funcasenum(total,lab_len)->(total+num,maxlab_len(String.lengthcase)))c(0,0)inletsorted_cases=(* reverse compare yields decreasing order *)Hashtbl.to_seqc|>List.of_seq|>List.sort(fun(_,n1)(_,n2)->Int.comparen2n1)inList.iter(fun(case,num)->letpercentage=100.*.(floatnum)/.(floattotal)in(* Workaround for Windows/Unix difference: *)letpercentage=(Float.round(10.*.percentage))/.10.in(* 100. *. 7525. /. 10000. -> 75.2 or 75.3 *)letperc_str=Printf.sprintf"(%.1f%%)"percentageinPrintf.bprintfout" %-*s %6d cases %8s\n"(1+lab_len)(case^":")numperc_str)sorted_cases;Buffer.contentsoutletstat_max_lines=20(* maximum number of lines for a histogram *)letprint_stat((name,_),tbl)=letneg_avg_summands=ref[]inletpos_avg_summands=ref[]inletnum=ref0inletmin_idx,max_idx=Hashtbl.fold(funires(m1,m2)->letavg_summand=float_of_int(i*res)inifavg_summand<0.thenneg_avg_summands:=avg_summand::!neg_avg_summandselsepos_avg_summands:=avg_summand::!pos_avg_summands;num:=!num+res;minim1,maxim2)tbl(max_int,min_int)in(* compute average, summing positive/negative separately by order of magnitude *)letneg_avg_sums=List.sortFloat.compare!neg_avg_summands|>funxs->List.fold_right(+.)xs0.inletpos_avg_sums=List.sortFloat.compare!pos_avg_summands|>List.fold_left(+.)0.inletavg=ref(neg_avg_sums+.pos_avg_sums)inif!num>0then(avg:=!avg/.float_of_int!num);(* compute std-dev: sqroot of sum of squared distance-to-average
https://en.wikipedia.org/wiki/Standard_deviation *)letstddev=Hashtbl.fold(funiresacc->float_of_intres*.((float_of_inti-.!avg)**2.)::acc)tbl[]|>List.sortFloat.compare(* add summands in increasing order to preserve precision *)|>List.fold_left(+.)0.|>(funs->if!num>0thens/.float_of_int!numelses)|>sqrtin(* compute median *)letmedian=ref0inletmedian_num=ref0in(* how many values have we seen yet? once >= !n/2 we set median *)(Hashtbl.fold(funicntacc->(i,cnt)::acc)tbl[])|>List.sort(fun(i,_)(j,_)->poly_compareij)|>List.iter(fun(i,cnt)->if!median_num<!num/2then(median_num:=!median_num+cnt;(* just went above median! *)if!median_num>=!num/2thenmedian:=i));(* group by buckets, if there are too many entries: *)(* first compute histogram and bucket size *)letmin_idx64,max_idx64=Int64.(of_intmin_idx,of_intmax_idx)inlethist_size,bucket_size=letsample_width=Int64.submax_idx64min_idx64inifsample_width>Int64.of_intstat_max_linesthenstat_max_lines,int_of_float(ceil(Int64.to_floatsample_width/.float_of_intstat_max_lines))elsemax_idx-min_idx,1inlethist_size=ifInt64.(addmin_idx64(mul(of_intbucket_size)(of_inthist_size)))<=max_idx64then1+hist_sizeelsehist_sizein(* accumulate bucket counts *)letmax_val=ref0in(* max value after grouping by buckets *)letbucket_count=Array.inithist_size(fun_->0)inHashtbl.iter(funjcount->letbucket=Int64.(to_int(div(sub(of_intj)min_idx64)(of_intbucket_size)))inletnew_count=bucket_count.(bucket)+countinbucket_count.(bucket)<-new_count;max_val:=max!max_valnew_count)tbl;(* print entries of the table, sorted by increasing index *)letout=Buffer.create128in(* Windows workaround to avoid annoying exponent zero such as "1.859e+018" *)letfmt_floatf=iff>1e7||f<-1e7thencut_exp_zero(Printf.sprintf"%.3e"f)elsePrintf.sprintf"%.2f"finPrintf.bprintfout"stats %s:\n"name;Printf.bprintfout" num: %d, avg: %s, stddev: %s, median %d, min %d, max %d\n"!num(fmt_float!avg)(fmt_floatstddev)!medianmin_idxmax_idx;letindwidth=letstr_widthi=String.length(Printf.sprintf"%d"i)inList.mapstr_width[min_idx;max_idx;min_idx+bucket_size*hist_size]|>List.fold_leftmaxmin_intinletlabwidth=ifbucket_size=1thenindwidthelse2+2*indwidthinfori=0tohist_size-1doleti'=min_idx+i*bucket_sizeinletblabel=ifbucket_size=1thenPrintf.sprintf"%*d"indwidthi'elseletbucket_bound=i'+bucket_size-1inPrintf.sprintf"%*d..%*d"indwidthi'indwidth(ifbucket_bound<i'thenmax_intelsebucket_bound)inletbcount=bucket_count.(i)in(* NOTE: keep in sync *)letbar_len=bcount*55/!max_valinPrintf.bprintfout" %*s: %-56s %10d\n"labwidthblabel(String.makebar_len'#')bcountdone;Buffer.contentsoutlet()=Printexc.register_printer(function|Test_fail(name,l)->Some(print_test_failnamel)|Test_error(name,i,e,st)->Some(print_test_errornameiest)|Test_unexpected_successname->Some(print_unexpected_successname)|User_fails->Some("qcheck: user fail:\n"^s)|_->None)letprint_failarbnamel=print_test_failname(List.map(print_c_exarb)l)letprint_fail_othername~msg=print_test_failname[msg]letprint_expected_failurecellc_exs=matchc_exswith|[]->Format.sprintf"negative test `%s` failed as expected\n"(get_namecell)|c_ex::_->Format.sprintf"negative test `%s` failed as expected on: %s\n"(get_namecell)(print_c_excellc_ex)letprint_error?(st="")arbname(i,e)=print_test_errorname(print_c_exarbi)estletcheck_resultcellres=matchres.R.state,cell.positivewith|R.Success,true->()|R.Success,false->raise(Test_unexpected_successcell.name)|R.Error{instance;exn;backtrace},_->raise(Test_error(cell.name,print_c_excellinstance,exn,backtrace))|R.Failed{instances=l},true->letl=List.map(print_c_excell)linraise(Test_fail(cell.name,l))|R.Failed_,false->()|R.Failed_other{msg},_->raise(Test_fail(cell.name,[msg]))letcheck_cell_exn?long?call?step?handler?randcell=letres=check_cell?long?call?step?handler?randcellincheck_resultcellresletcheck_exn?long?rand(Testcell)=check_cell_exn?long?randcellendletfind_example?(name:string="<example>")?(count:intoption)~(f:'a->bool)(gen:'aGen.t):'aGen.t=(* the random generator of examples satisfying [f]. To do that we
test the property [fun x -> not (f x)]; any counter-example *)letgenst=letcell=Test.make_cell~max_fail:1?countgen(funx->not(fx))inletres=Test.check_cell~rand:stcellinbeginmatchres.TestResult.statewith|TestResult.Success->raise(No_example_foundname)|TestResult.Error_->raise(No_example_foundname)|TestResult.Failed{instances=[]}->assertfalse|TestResult.Failed{instances=failed::_}->(* found counter-example! *)Tree.purefailed.TestResult.instance|TestResult.Failed_other{msg=_}->raise(No_example_foundname)endingenletfind_example_gen?(rand:RS.toption)?(name:stringoption)?(count:intoption)~(f:'a->bool)(gen:'aGen.t):'a=letg=find_example?name?count~fgeninGen.generate1?randg