diff --git a/.gitignore b/.gitignore index ec464113b6..87b0876181 100644 --- a/.gitignore +++ b/.gitignore @@ -56,3 +56,6 @@ setup.log # Backup of opam lock file hazel.opam.locked.old + +# Code coverage +_coverage/ diff --git a/Makefile b/Makefile index 516016ddf8..5eef59a1d4 100644 --- a/Makefile +++ b/Makefile @@ -66,5 +66,13 @@ test: watch-test: dune build @ocaml-index @fmt @runtest --auto-promote --watch +coverage: + dune build @src/fmt @test/fmt --auto-promote src test --profile dev + dune runtest --instrument-with bisect_ppx --force + bisect-ppx-report summary + +generate-coverage-html: + bisect-ppx-report html + clean: dune clean diff --git a/README.md b/README.md index a15bdd424e..15caf27308 100644 --- a/README.md +++ b/README.md @@ -197,6 +197,9 @@ You can run all of the unit tests located in `test` by running `make test`. Unit tests are written using the [Alcotest framework](https://github.com/mirage/alcotest). +#### Coverage +Code coverage is provided by [bisect_ppx](https://github.com/aantron/bisect_ppx). To collect coverage statistics from tests run `make coverage`. After coverage statistics are generated, running `make generate-coverage-html` will generate a local webpage at `_coverage/index.html` that can be viewed to see line coverage per module. + ### Continuous Integration When you push your branch to the main `hazelgrove/hazel` repository, we diff --git a/dune-project b/dune-project index 1194593485..4dd3442bc4 100644 --- a/dune-project +++ b/dune-project @@ -26,9 +26,11 @@ (menhir (>= 2.0)) yojson - reason + (reason (>= 3.12.0)) ppx_yojson_conv_lib ppx_yojson_conv + incr_dom + bisect_ppx (omd (>= 2.0.0~alpha4)) ezjs_idb bonsai diff --git a/hazel.opam b/hazel.opam index e78b188325..09ee887ab3 100644 --- a/hazel.opam +++ b/hazel.opam @@ -11,9 +11,10 @@ depends: [ "ocaml" {>= "5.2.0"} "menhir" {>= "2.0"} "yojson" - "reason" + "reason" {>= "3.12.0"} "ppx_yojson_conv_lib" "ppx_yojson_conv" + "bisect_ppx" "omd" {>= "2.0.0~alpha4"} "ezjs_idb" "bonsai" diff --git a/hazel.opam.locked b/hazel.opam.locked index b2cfce2cea..856b83ad33 100644 --- a/hazel.opam.locked +++ b/hazel.opam.locked @@ -39,6 +39,7 @@ depends: [ "bigstringaf" {= "0.10.0"} "bin_prot" {= "v0.16.0"} "bonsai" {= "v0.16.0"} + "bisect_ppx" {= "2.8.3"} "camlp-streams" {= "5.0.1"} "chrome-trace" {= "3.16.0"} "cmdliner" {= "1.3.0"} diff --git a/src/haz3lcore/dune b/src/haz3lcore/dune index 77e2ca3fe1..a0d9770816 100644 --- a/src/haz3lcore/dune +++ b/src/haz3lcore/dune @@ -4,6 +4,8 @@ (name haz3lcore) (libraries util sexplib unionFind uuidm virtual_dom yojson core) (js_of_ocaml) + (instrumentation + (backend bisect_ppx)) (preprocess (pps ppx_yojson_conv diff --git a/src/haz3lweb/Main.re b/src/haz3lweb/Main.re index af8058b770..16811a32cb 100644 --- a/src/haz3lweb/Main.re +++ b/src/haz3lweb/Main.re @@ -4,8 +4,6 @@ open Haz3lweb; open Bonsai.Let_syntax; let scroll_to_caret = ref(true); -let edit_action_applied = ref(true); -let last_edit_action = ref(JsUtil.timestamp()); let restart_caret_animation = () => // necessary to trigger reflow @@ -19,16 +17,24 @@ let restart_caret_animation = () => | _ => () }; -let apply = (model, action, ~schedule_action): Model.t => { +let apply = (model, action, ~schedule_action, ~schedule_autosave): Model.t => { restart_caret_animation(); if (UpdateAction.is_edit(action)) { - last_edit_action := JsUtil.timestamp(); - edit_action_applied := true; + schedule_autosave( + BonsaiUtil.Alarm.Action.SetAlarm( + Core.Time_ns.add(Core.Time_ns.now(), Core.Time_ns.Span.of_sec(1.0)), + ), + ); + } else { + schedule_autosave( + BonsaiUtil.Alarm.Action.SnoozeAlarm( + Core.Time_ns.add(Core.Time_ns.now(), Core.Time_ns.Span.of_sec(1.0)), + ), + ); }; if (Update.should_scroll_to_caret(action)) { scroll_to_caret := true; }; - last_edit_action := JsUtil.timestamp(); switch ( try({ let new_model = Update.apply(model, action, ~schedule_action); @@ -53,16 +59,6 @@ let apply = (model, action, ~schedule_action): Model.t => { }; }; -let app = - Bonsai.state_machine0( - (module Model), - (module Update), - ~apply_action= - (~inject, ~schedule_event) => - apply(~schedule_action=x => schedule_event(inject(x))), - ~default_model=Model.load(Model.blank), - ); - /* This subcomponent is used to run an effect once when the app starts up, After the first draw */ let on_startup = effect => { @@ -80,31 +76,44 @@ let on_startup = effect => { }; let view = { - let%sub app = app; + let%sub save_scheduler = BonsaiUtil.Alarm.alarm; + let%sub app = + Bonsai.state_machine1( + (module Model), + (module Update), + ~apply_action= + (~inject, ~schedule_event, input) => { + let schedule_action = x => schedule_event(inject(x)); + let schedule_autosave = action => + switch (input) { + | Active((_, alarm_inject)) => + schedule_event(alarm_inject(action)) + | Inactive => () + }; + apply(~schedule_action, ~schedule_autosave); + }, + ~default_model=Model.load(Model.blank), + save_scheduler, + ); let%sub () = { on_startup( Bonsai.Value.map(~f=((_model, inject)) => inject(Startup), app), ); }; - let%sub after_display = { - let%arr (_model, inject) = app; - if (scroll_to_caret.contents) { - scroll_to_caret := false; - JsUtil.scroll_cursor_into_view_if_needed(); - }; - if (edit_action_applied^ - && JsUtil.timestamp() - -. last_edit_action^ > 1000.0) { - /* If an edit action has been applied, but no other edit action - has been applied for 1 second, save the model. */ - edit_action_applied := false; - print_endline("Saving..."); - inject(Update.Save); - } else { - Ui_effect.Ignore; - }; + let after_display = { + Bonsai.Effect.of_sync_fun( + () => + if (scroll_to_caret.contents) { + scroll_to_caret := false; + JsUtil.scroll_cursor_into_view_if_needed(); + }, + (), + ); }; - let%sub () = Bonsai.Edge.after_display(after_display); + let save_effect = Bonsai.Value.map(~f=((_, g)) => g(Update.Save), app); + let%sub () = BonsaiUtil.Alarm.listen(save_scheduler, ~event=save_effect); + let%sub () = + Bonsai.Edge.after_display(after_display |> Bonsai.Value.return); let%arr (model, inject) = app; Haz3lweb.Page.view(~inject, model); }; diff --git a/src/haz3lweb/Update.re b/src/haz3lweb/Update.re index 1a47210a0e..43a5e593f6 100644 --- a/src/haz3lweb/Update.re +++ b/src/haz3lweb/Update.re @@ -508,7 +508,9 @@ let apply = (model: Model.t, update: t, ~schedule_action): Result.t(Model.t) => | DebugConsole(key) => DebugConsole.print(model, key); Ok(model); - | Save => Model.save_and_return(model) + | Save => + print_endline("Saving..."); + Model.save_and_return(model); | InitImportAll(file) => JsUtil.read_file(file, data => schedule_action(FinishImportAll(data))); Ok(model); diff --git a/src/haz3lweb/dune b/src/haz3lweb/dune index d3e42ec636..e2792b76b8 100644 --- a/src/haz3lweb/dune +++ b/src/haz3lweb/dune @@ -8,6 +8,8 @@ (library (name workerServer) (modules WorkerServer) + (instrumentation + (backend bisect_ppx)) (libraries bonsai bonsai.web @@ -29,6 +31,8 @@ (library (name haz3lweb) + (instrumentation + (backend bisect_ppx)) (modules (:standard \ Main) \ @@ -55,7 +59,8 @@ ppx_let ppx_sexp_conv ppx_deriving.show - ppx_yojson_conv))) + ppx_yojson_conv + bonsai.ppx_bonsai))) (executable (name main) diff --git a/src/pretty/dune b/src/pretty/dune index 868d03defc..c131965aff 100644 --- a/src/pretty/dune +++ b/src/pretty/dune @@ -3,6 +3,8 @@ (library (name pretty) (libraries util sexplib) + (instrumentation + (backend bisect_ppx)) (preprocess (pps ppx_let ppx_sexp_conv))) diff --git a/src/util/BonsaiUtil.re b/src/util/BonsaiUtil.re new file mode 100644 index 0000000000..127172ce21 --- /dev/null +++ b/src/util/BonsaiUtil.re @@ -0,0 +1,42 @@ +open Core; +open Bonsai; +open Bonsai.Let_syntax; + +module Alarm = { + module Action = { + [@deriving sexp] + type t = + | SetAlarm(Time_ns.Alternate_sexp.t) + | SnoozeAlarm(Time_ns.Alternate_sexp.t) + | UnsetAlarm; + }; + + let alarm = + state_machine0( + (module Time_ns.Alternate_sexp), + (module Action), + ~default_model=Time_ns.max_value_representable, + ~apply_action=(~inject as _, ~schedule_event as _, model, action) => { + switch (action) { + | SetAlarm(time) => time + | SnoozeAlarm(time) => Time_ns.max(time, model) + | UnsetAlarm => Time_ns.max_value_representable + } + }); + + let listen = (alarm, ~event) => { + let%sub before_or_after = Clock.at(alarm |> Value.map(~f=fst)); + Edge.on_change( + (module Clock.Before_or_after), + before_or_after, + ~callback={ + open Clock.Before_or_after; + let%map (_, inject) = alarm + and event = event; + fun + | After => Effect.Many([inject(Action.UnsetAlarm), event]) + | Before => Effect.Ignore; + }, + ); + }; +}; diff --git a/src/util/Util.re b/src/util/Util.re index c901907b60..2c7f084100 100644 --- a/src/util/Util.re +++ b/src/util/Util.re @@ -1,4 +1,5 @@ module Aba = Aba; +module BonsaiUtil = BonsaiUtil; module Direction = Direction; module Either = Either; module IntMap = IntMap; diff --git a/src/util/dune b/src/util/dune index a3494db579..f50e6ac0f7 100644 --- a/src/util/dune +++ b/src/util/dune @@ -2,13 +2,16 @@ (name util) (libraries re base ptmap bonsai bonsai.web virtual_dom yojson) (js_of_ocaml) + (instrumentation + (backend bisect_ppx)) (preprocess (pps ppx_yojson_conv js_of_ocaml-ppx ppx_let ppx_sexp_conv - ppx_deriving.show))) + ppx_deriving.show + bonsai.ppx_bonsai))) (env (dev diff --git a/test/dune b/test/dune index 832c9689f2..3b9dc8bb2e 100644 --- a/test/dune +++ b/test/dune @@ -2,7 +2,7 @@ (test (name haz3ltest) - (libraries haz3lcore alcotest junit junit_alcotest) + (libraries haz3lcore alcotest junit junit_alcotest bisect_ppx.runtime) (modes js) (preprocess - (pps js_of_ocaml-ppx))) + (pps js_of_ocaml-ppx ppx_deriving.show))) diff --git a/test/haz3ltest.re b/test/haz3ltest.re index 03cebe774c..8e4a838b0a 100644 --- a/test/haz3ltest.re +++ b/test/haz3ltest.re @@ -13,3 +13,4 @@ let (suite, _) = ], ); Junit.to_file(Junit.make([suite]), "junit_tests.xml"); +Bisect.Runtime.write_coverage_data();