123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757open!Coreopen!ImportopenBonsai.For_openopenBonsai.Let_syntaxopenProc(* A big focus of the tests in this file is about making sure that there are no
"in-transit" frames - frames during which the result has some intermediate
value because lifecycle events haven't yet run. To accomplish this goal, all
the tests have been written in a [Common] functor, which accepts a module
that specifies what [Handle.show] should do. We supply three different
answers to that question:
- it should do what it normally does.
- it should recompute_view an extra time prior to calling Handle.show.
- it should recompute_view_until_stable prior to calling Handle.show.
We do this to ensure that all the functions being tested behave the same no
matter which one of those options is chosen. *)letadvance_by_sechandleseconds=Handle.advance_clock_byhandle(Time_ns.Span.of_secseconds);;let%test_module"Bonsai_extra.with_last_modified_time"=(modulestructmoduleCommon(M:sigvalwith_last_modified_time:equal:('a->'a->bool)->'aValue.t->('a*Time_ns.t)Computation.tvalshow_handle:('a,'b)Handle.t->unitend)=structletshow=M.show_handlelet%expect_test_=letv'=Bonsai.Var.create1inletv=Bonsai.Var.valuev'inletc=M.with_last_modified_time~equal:Int.equalvinlethandle=Handle.create(Result_spec.sexp(modulestructtypet=int*Time_ns.Alternate_sexp.t[@@derivingsexp]end))cinshowhandle;[%expect{| (1 "1970-01-01 00:00:00Z") |}];advance_by_sechandle1.0;showhandle;[%expect{| (1 "1970-01-01 00:00:00Z") |}];Bonsai.Var.setv'2;showhandle;[%expect{| (2 "1970-01-01 00:00:01Z") |}];Bonsai.Var.setv'3;advance_by_sechandle1.0;showhandle;[%expect{| (3 "1970-01-01 00:00:02Z") |}];showhandle;[%expect{| (3 "1970-01-01 00:00:02Z") |}];;let%expect_test_=letv'=Bonsai.Var.create1inleton'=Bonsai.Var.createtrueinletv=Bonsai.Var.valuev'inleton=Bonsai.Var.valueon'inletc=match%subonwith|true->let%subx=M.with_last_modified_time~equal:Int.equalvinlet%arrx=xinSomex|false->Bonsai.constNoneinlethandle=Handle.create(Result_spec.sexp(modulestructtypet=(int*Time_ns.Alternate_sexp.t)option[@@derivingsexp]end))cinshowhandle;[%expect{| ((1 "1970-01-01 00:00:00Z")) |}];advance_by_sechandle1.0;showhandle;[%expect{| ((1 "1970-01-01 00:00:00Z")) |}];Bonsai.Var.seton'false;showhandle;[%expect{| () |}];Bonsai.Var.seton'true;showhandle;[%expect{| ((1 "1970-01-01 00:00:01Z")) |}];Bonsai.Var.seton'false;showhandle;[%expect{| () |}];advance_by_sechandle1.0;showhandle;[%expect{| () |}];Bonsai.Var.seton'true;showhandle;[%expect{| ((1 "1970-01-01 00:00:02Z")) |}];advance_by_sechandle1.0;showhandle;[%expect{| ((1 "1970-01-01 00:00:02Z")) |}];Bonsai.Var.setv'2;showhandle;[%expect{| ((2 "1970-01-01 00:00:03Z")) |}];;endmodule_=Common(structletwith_last_modified_time=Bonsai_extra.with_last_modified_timeletshow_handle=Handle.showend)module_=Common(structletwith_last_modified_time=Bonsai_extra.with_last_modified_timeletshow_handlehandle=Handle.recompute_viewhandle;Handle.showhandle;;end)module_=Common(structletwith_last_modified_time=Bonsai_extra.with_last_modified_timeletshow_handlehandle=Handle.recompute_view_until_stablehandle;Handle.showhandle;;end)end);;let%test_module"Bonsai_extra.is_stable"=(modulestructmoduleCommon(M:sigvalis_stable:equal:('a->'a->bool)->'aValue.t->time_to_stable:Time_ns.Span.t->boolComputation.tvalshow_handle:('a,'b)Handle.t->unitend)=structletshow=M.show_handlelet%expect_test_=letv'=Bonsai.Var.create1inletv=Bonsai.Var.valuev'inletc=let%subis_stable=M.is_stable~equal:Int.equalv~time_to_stable:(Time_ns.Span.of_sec1.0)inreturn(Value.bothvis_stable)inlethandle=Handle.create(Result_spec.sexp(modulestructtypet=int*bool[@@derivingsexp]end))cinshowhandle;[%expect{| (1 false) |}];advance_by_sechandle1.0;showhandle;[%expect{| (1 true) |}];Bonsai.Var.setv'2;showhandle;[%expect{| (2 false) |}];advance_by_sechandle0.5;showhandle;[%expect{| (2 false) |}];Bonsai.Var.setv'3;showhandle;[%expect{| (3 false) |}];advance_by_sechandle0.5;showhandle;[%expect{| (3 false) |}];advance_by_sechandle0.5;showhandle;[%expect{| (3 true) |}];Bonsai.Var.setv'4;showhandle;[%expect{| (4 false) |}];advance_by_sechandle1.0;Bonsai.Var.setv'5;showhandle;[%expect{| (5 false) |}];advance_by_sechandle1.0;showhandle;[%expect{| (5 true) |}];advance_by_sechandle0.5;Bonsai.Var.setv'4;showhandle;[%expect{| (4 false) |}];advance_by_sechandle0.5;Bonsai.Var.setv'5;showhandle;[%expect{| (5 false) |}];;let%expect_test_=letv'=Bonsai.Var.create1inleton'=Bonsai.Var.createtrueinletv=Bonsai.Var.valuev'inleton=Bonsai.Var.valueon'inletc=match%subonwith|true->let%subx=M.is_stable~equal:Int.equalv~time_to_stable:(Time_ns.Span.of_sec1.0)inlet%arrx=xandv=vinSome(x,v)|false->Bonsai.constNoneinlethandle=Handle.create(Result_spec.sexp(modulestructtypet=(bool*int)option[@@derivingsexp]end))cinshowhandle;[%expect{| ((false 1)) |}];advance_by_sechandle1.0;showhandle;[%expect{| ((true 1)) |}];Bonsai.Var.seton'false;showhandle;[%expect{| () |}];Bonsai.Var.seton'true;showhandle;[%expect{| ((false 1)) |}];;let%expect_test{|zero values for the timespan should be permitted (but issue a warning) and always return false |}=letv'=Bonsai.Var.create1inleton'=Bonsai.Var.createtrueinletv=Bonsai.Var.valuev'inleton=Bonsai.Var.valueon'inletc=match%subonwith|true->let%subx=M.is_stable~equal:Int.equalv~time_to_stable:(Time_ns.Span.of_sec0.0)inlet%arrx=xandv=vinSome(x,v)|false->Bonsai.constNoneinlethandle=Handle.create(Result_spec.sexp(modulestructtypet=(bool*int)option[@@derivingsexp]end))cin[%expect{| "Bonsai_extra.is_stable: [time_to_stable] should be positive" |}];showhandle;[%expect{| ((false 1)) |}];advance_by_sechandle1.0;showhandle;[%expect{| ((false 1)) |}];Bonsai.Var.seton'false;showhandle;[%expect{| () |}];Bonsai.Var.seton'true;showhandle;[%expect{| ((false 1)) |}];;let%expect_test{|negative values for the timespan should be permitted (but issue a warning) and always return false |}=letv'=Bonsai.Var.create1inletv=Bonsai.Var.valuev'inletc=M.is_stable~equal:Int.equalv~time_to_stable:(Time_ns.Span.of_sec(-1.0))in[%expect{| "Bonsai_extra.is_stable: [time_to_stable] should be positive" |}];lethandle=Handle.create(Result_spec.sexp(moduleBool))cinshowhandle;[%expect{| false |}];;endmodule_=Common(structletis_stable=Bonsai_extra.is_stableletshow_handle=Handle.showend)module_=Common(structletis_stable=Bonsai_extra.is_stableletshow_handlehandle=Handle.recompute_viewhandle;Handle.showhandle;;end)module_=Common(structletis_stable=Bonsai_extra.is_stableletshow_handlehandle=Handle.recompute_view_until_stablehandle;Handle.showhandle;;end)end);;let%test_module"Bonsai.most_recent_value_satisfying"=(modulestructmoduleCommon(M:sigvalmost_recent_value_satisfying:(moduleBonsai.Modelwithtypet='a)->'aValue.t->condition:('a->bool)->'aoptionComputation.tvalshow_handle:('a,'b)Handle.t->unitend)=structletshow=M.show_handlelet%expect_test_=letv'=Bonsai.Var.create1inletv=Bonsai.Var.valuev'inletc=M.most_recent_value_satisfying(moduleInt)v~condition:(funx->x%2=0)inlethandle=Handle.create(Result_spec.sexp(modulestructtypet=intoption[@@derivingsexp]end))cinshowhandle;[%expect{| () |}];Bonsai.Var.setv'2;showhandle;[%expect{| (2) |}];Bonsai.Var.setv'3;showhandle;[%expect{| (2) |}];Bonsai.Var.setv'4;showhandle;[%expect{| (4) |}];;let%expect_test_=letv'=Bonsai.Var.create1inleton'=Bonsai.Var.createtrueinletv=Bonsai.Var.valuev'inleton=Bonsai.Var.valueon'inletc=match%subonwith|true->let%subx=M.most_recent_value_satisfying(moduleInt)v~condition:(funx->x%2=0)inlet%arrx=xinSomex|false->Bonsai.constNoneinlethandle=Handle.create(Result_spec.sexp(modulestructtypet=intoptionoption[@@derivingsexp]end))cinshowhandle;[%expect{| (()) |}];Bonsai.Var.setv'2;showhandle;[%expect{| ((2)) |}];Bonsai.Var.seton'false;showhandle;[%expect{| () |}];Bonsai.Var.setv'3;showhandle;[%expect{| () |}];Bonsai.Var.seton'true;showhandle;[%expect{| ((2)) |}];;endmodule_=Common(structletmost_recent_value_satisfying=Bonsai.most_recent_value_satisfyingletshow_handle=Handle.showend)module_=Common(structletmost_recent_value_satisfying=Bonsai.most_recent_value_satisfyingletshow_handlehandle=Handle.recompute_viewhandle;Handle.showhandle;;end)module_=Common(structletmost_recent_value_satisfying=Bonsai.most_recent_value_satisfyingletshow_handlehandle=Handle.recompute_view_until_stablehandle;Handle.showhandle;;end)end);;let%test_module"Bonsai_extra.value_stability"=(modulestructletalternate_value_stability_implementation(typea)(moduleM:Bonsai.Modelwithtypet=a)input~time_to_stable=let%subinput=(* apply cutoff as an optimistic performance improvement *)Bonsai.Incr.value_cutoffinput~equal:M.equalinletmoduleT=structmoduleModel=structtypestability=|Inactiveof{previously_stable:M.toption}|Unstableof{previously_stable:M.toption;unstable_value:M.t}|StableofM.t[@@derivingsexp,equal]letset_valuenew_value=function|Inactive{previously_stable}->Unstable{previously_stable;unstable_value=new_value}|Stablestable->Unstable{previously_stable=Somestable;unstable_value=new_value}|Unstable{previously_stable;unstable_value=_}->Unstable{previously_stable;unstable_value=new_value};;typet={stability:stability;time_to_next_stable:Time_ns.Alternate_sexp.toption}[@@derivingsexp,equal]letdefault={stability=Inactive{previously_stable=None};time_to_next_stable=None};;endmoduleAction=structtypet=|Deactivate|BounceofM.t*Time_ns.Alternate_sexp.t|Set_stableofM.t*Time_ns.Alternate_sexp.t[@@derivingsexp_of]endendinletopenTinlet%sub{stability;time_to_next_stable},inject=Bonsai.state_machine0(moduleModel)(moduleAction)~default_model:Model.default~apply_action:(fun~inject:_~schedule_event:_modelaction->matchaction,modelwith|Deactivate,{stability;_}->letstability=matchstabilitywith|Inactive_->stability|Unstable{previously_stable;_}->Inactive{previously_stable}|Stablestable->Inactive{previously_stable=Somestable}in(* Deactivating this component will automatically cause the value to be
considered unstable. This is because we have no way to tell what is
happening to the value when this component is inactive, and I consider
it safer to assume instability than to assume stability. *){stability;time_to_next_stable=None}|Bounce(new_value,now),{stability;_}->(* Bouncing will cause the value to become unstable, and set the
time-to-next-stable to the provided value. *)letstability=Model.set_valuenew_valuestabilityinlettime_to_next_stable=Some(Time_ns.addnowtime_to_stable)in{stability;time_to_next_stable}|Set_stable(stable,now),{stability;time_to_next_stable}->(* Sets the value which is considered to be stable and resets
the time until next stability. *)(matchstabilitywith|Inactive{previously_stable}->{stability=Unstable{previously_stable;unstable_value=stable};time_to_next_stable=Some(Time_ns.addnowtime_to_stable)}|Stablepreviously_stable->ifM.equalpreviously_stablestablethen{stability=Stablestable;time_to_next_stable=None}else{stability=Unstable{unstable_value=stable;previously_stable=Somepreviously_stable};time_to_next_stable=Some(Time_ns.addnowtime_to_stable)}|Unstable{unstable_value;previously_stable}->letcandidate_time_to_next_stable=Time_ns.addnowtime_to_stablein(matchM.equalunstable_valuestable,time_to_next_stablewith|true,Sometime_to_next_stablewhenTime_ns.(>=)nowtime_to_next_stable->{stability=Stablestable;time_to_next_stable=None}|_->{stability=Unstable{unstable_value=stable;previously_stable};time_to_next_stable=Somecandidate_time_to_next_stable})))inlet%subget_current_time=Bonsai.Clock.get_current_timeinlet%subbounce=(* [bounce] is an effect which, when scheduled, will bounce the
state-machine and set the time-until-stable to the current wallclock
time plus the provided offset *)let%arrget_current_time=get_current_timeandinject=injectandinput=inputinlet%bind.Effectnow=get_current_timeininject(Bounce(input,now))inlet%sub()=(* the input value changing triggers a bounce *)let%subcallback=let%arrbounce=bounceinfun_->bounceinBonsai.Edge.on_change(moduleM)input~callbackinlet%sub()=let%subon_deactivate=let%arrinject=injectininjectDeactivatein(* activating the component bounces it to reset the timer *)Bonsai.Edge.lifecycle~on_deactivate~on_activate:bounce()inlet%sub()=match%subtime_to_next_stablewith|None->Bonsai.const()|Somenext_stable->let%subcallback=let%arrinject=injectandinput=inputandget_current_time=get_current_timeandbounce=bounceinfun(prev:Bonsai.Clock.Before_or_after.toption)(cur:Bonsai.Clock.Before_or_after.t)->matchprev,curwith|SomeBefore,After->let%bind.Effectnow=get_current_timeininject(Set_stable(input,now))|None,After->print_s[%message"BUG"[%here]"clock moves straight to 'after'"];bounce|_->Effect.Ignoreinlet%subbefore_or_after=Bonsai.Clock.atnext_stableinBonsai.Edge.on_change'(moduleBonsai.Clock.Before_or_after)before_or_after~callbackinlet%arrstability=stabilityandinput=inputinmatchstabilitywith|Stableinput'whenM.equalinput'input->Bonsai_extra.Stability.Stableinput|Stablepreviously_stable->(* Even if the state-machine claims that the value is stable, we can still witness
instability one frame before the lifecycle events run. *)Unstable{previously_stable=Somepreviously_stable;unstable_value=input}|Unstable{previously_stable;unstable_value=_}->Unstable{previously_stable;unstable_value=input}|Inactive{previously_stable}->Unstable{previously_stable;unstable_value=input};;moduleCommon(M:sigvalvalue_stability:(moduleBonsai.Modelwithtypet='a)->'aValue.t->time_to_stable:Time_ns.Span.t->'aBonsai_extra.Stability.tComputation.tvalshow_handle:('a,'b)Handle.t->unitend)=structletshow=M.show_handlelet%expect_test_=letv'=Bonsai.Var.create1inletv=Bonsai.Var.valuev'inletc=M.value_stability(moduleInt)v~time_to_stable:(Time_ns.Span.of_sec1.0)inlethandle=Handle.create(Result_spec.sexp(modulestructtypet=intBonsai_extra.Stability.t[@@derivingsexp]end))cinshowhandle;[%expect{| (Unstable (previously_stable ()) (unstable_value 1)) |}];advance_by_sechandle1.0;showhandle;[%expect{| (Stable 1) |}];Bonsai.Var.setv'2;showhandle;[%expect{| (Unstable (previously_stable (1)) (unstable_value 2)) |}];advance_by_sechandle0.5;showhandle;[%expect{| (Unstable (previously_stable (1)) (unstable_value 2)) |}];Bonsai.Var.setv'3;showhandle;[%expect{| (Unstable (previously_stable (1)) (unstable_value 3)) |}];advance_by_sechandle0.5;showhandle;[%expect{| (Unstable (previously_stable (1)) (unstable_value 3)) |}];advance_by_sechandle0.5;showhandle;[%expect{| (Stable 3) |}];Bonsai.Var.setv'4;showhandle;[%expect{| (Unstable (previously_stable (3)) (unstable_value 4)) |}];advance_by_sechandle1.0;Bonsai.Var.setv'5;showhandle;[%expect{| (Unstable (previously_stable (3)) (unstable_value 5)) |}];advance_by_sechandle1.0;showhandle;[%expect{| (Stable 5) |}];advance_by_sechandle0.5;Bonsai.Var.setv'4;showhandle;[%expect{| (Unstable (previously_stable (5)) (unstable_value 4)) |}];advance_by_sechandle0.5;Bonsai.Var.setv'5;showhandle;[%expect{| (Unstable (previously_stable (5)) (unstable_value 5)) |}];;let%expect_test_=letv'=Bonsai.Var.create1inleton'=Bonsai.Var.createtrueinletv=Bonsai.Var.valuev'inleton=Bonsai.Var.valueon'inletc=match%subonwith|true->let%subx=M.value_stability(moduleInt)v~time_to_stable:(Time_ns.Span.of_sec1.0)inlet%arrx=xinSomex|false->Bonsai.constNoneinlethandle=Handle.create(Result_spec.sexp(modulestructtypet=intBonsai_extra.Stability.toption[@@derivingsexp]end))cinshowhandle;[%expect{| ((Unstable (previously_stable ()) (unstable_value 1))) |}];advance_by_sechandle1.0;showhandle;[%expect{| ((Stable 1)) |}];Bonsai.Var.seton'false;showhandle;[%expect{| () |}];Bonsai.Var.seton'true;showhandle;[%expect{| ((Unstable (previously_stable (1)) (unstable_value 1))) |}];;endmodule_=Common(struct(* The function reference below is an implementation that exists purely
as a sanity check for the real implementation. If two vastly
different implemenations always yield the same result, that's an
encouraging sign. Sadly, this implementation relies on having a
frame between certain real-world events, so we only run the tests
with [recompute_view_until_stable] being called before Handle.show.
(This downside is one reason why this is not the real
implementation.) *)letvalue_stability=alternate_value_stability_implementationletshow_handlehandle=Handle.recompute_view_until_stablehandle;Handle.showhandle;;end)module_=Common(structletvalue_stability=Bonsai_extra.value_stabilityletshow_handle=Handle.showend)module_=Common(structletvalue_stability=Bonsai_extra.value_stabilityletshow_handlehandle=Handle.recompute_viewhandle;Handle.showhandle;;end)module_=Common(structletvalue_stability=Bonsai_extra.value_stabilityletshow_handlehandle=Handle.recompute_view_until_stablehandle;Handle.showhandle;;end)end);;