123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205(*****************************************************************************)(* *)(* SPDX-License-Identifier: MIT *)(* Copyright (c) 2024 Nomadic Labs, <contact@nomadic-labs.com> *)(* *)(*****************************************************************************)openLog_helpersexceptionTest_failed(** A scenario is a succession of actions. We define a branching path as a way to create multiple tests
from the same point. This allows easy compositionality of behaviors with minimal code sharing.
The [Tag] allows to give meaningful identifiers to the branches. It is good practice to tag each
case in a branch (it's not necessary, but since test names must be unique, at most one branch can
remain unnamed, and even then it can create conflicting names.)
*)type('input,'output)scenarios=|Action:('input->'outputtzresultLwt.t)->('input,'output)scenarios|Empty:('t,'t)scenarios|Concat:(('a,'b)scenarios*('b,'c)scenarios)->('a,'c)scenarios|Branch:(('a,'b)scenarios*('a,'b)scenarios)->('a,'b)scenarios|Tag:(* Name for test branch *)string->('t,'t)scenarios|Slow:(* If in scenario branch, makes the test `Slow *)('t,'t)scenarios(** Unfolded scenario type *)type('input,'output)single_scenario=|End_scenario:('t,'t)single_scenario|Cons:(('input->'ttzresultLwt.t)*('t,'output)single_scenario)->('input,'output)single_scenarioletreccat_ss:typeabc.(a,b)single_scenario->(b,c)single_scenario->(a,c)single_scenario=funab->matchawithEnd_scenario->b|Cons(act,a')->Cons(act,cat_ssa'b)letcombinefl1l2=List.map(funa->List.map(funb->fab)l2)l1|>List.flattenletrecunfold_scenarios:typeinputoutput.(input,output)scenarios->((input,output)single_scenario*stringlist*bool)list=function|Slow->[(End_scenario,[],true)]|Tags->[(End_scenario,[s],false)]|Empty->[(End_scenario,[],false)]|Actiona->[(Cons(a,End_scenario),[],false)]|Branch(left,right)->unfold_scenariosleft@unfold_scenariosright|Concat(left,right)->letl=unfold_scenariosleftinletr=unfold_scenariosrightincombine(fun(sl,tl,bl)(sr,tr,br)->(cat_ssslsr,tl@tr,bl||br))lrletrecrun_scenario:typeinputoutput.(input,output)single_scenario->input->outputtzresultLwt.t=letopenLwt_result_syntaxinfunscenarioinput->matchscenariowith|End_scenario->returninput|Cons(action,next)->let*result=actioninputinrun_scenarionextresulttypetest_closure=string*bool*(Tezt_tezos.Protocol.t->unitLwt.t)letunfolded_to_test:(unit,unit)single_scenario*stringlist*bool->test_closure=letopenLwt_syntaxinfun(s,title,is_slow)->lettitle=matchtitlewith|[]->""|[n]->n|header::tags->(* We chose to separate all tags with a comma, and use the head tag as a header for the test *)header^": "^String.concat", "tagsin(title,is_slow,fun_proto->let*r=(run_scenarios)()inmatchrwith|Ok()->return_unit|Errore->Log.error"%a@."Error_monad.pp_print_tracee;raiseTest_failed)letregister_test~__FILE__~tags((title,is_slow,test):test_closure):unit=lettags=ifis_slowthenTezos_test_helpers.Tag.slow::tagselsetagsinTezt_tezos.Protocol.(register_test~__FILE__~title~tags~uses:(fun_->[])~uses_node:false~uses_client:false~uses_admin_client:falsetest[Alpha])letregister_tests~__FILE__~tags(l:test_closurelist):unit=List.iter(register_test~__FILE__~tags)l(** Useful aliases and operators *)(* Aliases for [Empty]. Can be used as first component of a scenario instead of a tag if its not needed. *)letnoop=Emptyletno_tag=Emptyletconcat:typeabc.(a,b)scenarios->(b,c)scenarios->(a,c)scenarios=funab->match(a,b)with|Empty,Empty->Empty|_,Empty->a|Empty,_->b|_->Concat(a,b)letbranch:typeab.(a,b)scenarios->(a,b)scenarios->(a,b)scenarios=funab->match(a,b)withEmpty,Empty->Empty|_->Branch(a,b)(** Continuation connector: execute a then b *)let(-->)ab=concatab(** Branching connector: creates two tests with different execution paths *)let(|+)ab=branchab(** Ends the test. Dump the state, returns [unit] *)letend_test:('a,unit)scenarios=letopenLwt_result_syntaxinAction(fun_->Log.info~color:begin_end_color"-- End test --";return_unit)(** Transforms scenarios into tests *)lettests_of_scenarios:(string*(unit,'t)scenarios)list->test_closurelist=funscenarios->List.map(fun(s,x)->Tags-->x-->end_test)scenarios|>function|[]->[]|a::t->List.fold_left(|+)at|>unfold_scenarios|>List.mapunfolded_to_test(** Arbitrary execution *)letexecf=Actionf(** Execute a function that does not modify the block, only the state *)letexec_statef=letopenLwt_result_syntaxinAction(fun((block,_state)asinput)->let*state=finputinreturn(block,state))(** Execute a function that does not modify neither the block nor the state.
Usually used for checks/asserts *)letexec_unitf=letopenLwt_result_syntaxinAction(funinput->let*()=finputinreturninput)(** [fold f l] folds [f] over [l], fails on empty list *)letrecfold:('a->('b,'c)scenarios)->'alist->('b,'c)scenarios=funflist->matchlistwith|[]->Stdlib.failwith"Scenario_dsl.fold: empty list"|[x]->fx|h::t->fh|+foldft(** [fold_tag f l] folds [f] over [l], [l] has a tag for each of its elements.
Fails on empty list. *)letfold_tag:('a->('b,'c)scenarios)->(string*'a)list->('b,'c)scenarios=funf->letf(s,x)=Tags-->fxinfoldf(** [fold_tag_f f tag l] folds [f] over [l], [tag] returns a tag for each element of [l].
Fails on empty list. *)letfold_tag_f:('a->('b,'c)scenarios)->('a->string)->'alist->('b,'c)scenarios=funftag->letfx=Tag(tagx)-->fxinfoldf(** [unfold f l] maps [f] over [l], and runs them in order *)letrecunfold:('a->('b,'b)scenarios)->'alist->('b,'b)scenarios=funf->function[]->Empty|[x]->fx|h::t->fh-->unfoldft