From 26f14f7dfd6586953caac050738f97bf9c9b1e58 Mon Sep 17 00:00:00 2001 From: Patrick Ferris Date: Sat, 14 Jan 2023 00:00:47 +0000 Subject: [PATCH] Add browser-based alcotest tests --- dune-project | 4 +- lib_eio_js/browser/example/dune | 13 ++ lib_eio_js/browser/example/index.html | 15 ++ lib_eio_js/browser/{test => example}/index.ml | 0 lib_eio_js/browser/test/dune | 20 ++- lib_eio_js/browser/test/eio_test.ml | 156 ++++++++++++++++++ lib_eio_js/browser/test/index.html | 13 +- lib_eio_js/browser/test/test.ml | 86 ++++++++++ 8 files changed, 293 insertions(+), 14 deletions(-) create mode 100644 lib_eio_js/browser/example/dune create mode 100644 lib_eio_js/browser/example/index.html rename lib_eio_js/browser/{test => example}/index.ml (100%) create mode 100644 lib_eio_js/browser/test/eio_test.ml create mode 100644 lib_eio_js/browser/test/test.ml diff --git a/dune-project b/dune-project index 7011d7a5c..a9a377bcd 100644 --- a/dune-project +++ b/dune-project @@ -56,7 +56,9 @@ (description "An eio implementation that is compatible with programs running in the browser.") (depends (eio (= :version)) - (brr (>= 0.0.4)))) + (brr (>= 0.0.4)) + (alcotest (and (>= 1.6.0) :with-test)) + (ansi :with-test))) (package (name eio_main) (synopsis "Effect-based direct-style IO mainloop for OCaml") diff --git a/lib_eio_js/browser/example/dune b/lib_eio_js/browser/example/dune new file mode 100644 index 000000000..8541da5c1 --- /dev/null +++ b/lib_eio_js/browser/example/dune @@ -0,0 +1,13 @@ +; We compile by hand because dune is a little broken w.r.t to +; separate compilation, js_of_ocaml and effects. +(executable + (name index) + (modes js) + (js_of_ocaml (flags --target-env=nodejs --enable=effects --debug-info --source-map --pretty)) + (libraries eio_browser)) + +(rule + (alias default) + (deps index.html index.bc.js) + (targets index.js) + (action (copy index.bc.js index.js))) diff --git a/lib_eio_js/browser/example/index.html b/lib_eio_js/browser/example/index.html new file mode 100644 index 000000000..0c35cf098 --- /dev/null +++ b/lib_eio_js/browser/example/index.html @@ -0,0 +1,15 @@ + + + + + + + Eio in the Browser + + +
+ +
+ + + \ No newline at end of file diff --git a/lib_eio_js/browser/test/index.ml b/lib_eio_js/browser/example/index.ml similarity index 100% rename from lib_eio_js/browser/test/index.ml rename to lib_eio_js/browser/example/index.ml diff --git a/lib_eio_js/browser/test/dune b/lib_eio_js/browser/test/dune index 8541da5c1..959149572 100644 --- a/lib_eio_js/browser/test/dune +++ b/lib_eio_js/browser/test/dune @@ -1,13 +1,17 @@ -; We compile by hand because dune is a little broken w.r.t to -; separate compilation, js_of_ocaml and effects. +(library + (name eio_test) + (modules eio_test) + (libraries eio alcotest)) + (executable - (name index) + (name test) + (modules test) (modes js) - (js_of_ocaml (flags --target-env=nodejs --enable=effects --debug-info --source-map --pretty)) - (libraries eio_browser)) + (js_of_ocaml (flags --target-env=nodejs --enable=effects --setenv=ALCOTEST_COLOR=always)) + (libraries alcotest ansi eio_test eio_browser)) (rule (alias default) - (deps index.html index.bc.js) - (targets index.js) - (action (copy index.bc.js index.js))) + (deps index.html test.bc.js) + (targets test.js) + (action (copy test.bc.js test.js))) \ No newline at end of file diff --git a/lib_eio_js/browser/test/eio_test.ml b/lib_eio_js/browser/test/eio_test.ml new file mode 100644 index 000000000..aa811ae3b --- /dev/null +++ b/lib_eio_js/browser/test/eio_test.ml @@ -0,0 +1,156 @@ +open Eio + +(* Filesystem tests *) +module Fs = struct + let ( / ) = Path.(/) + let test_read_write env () = + let test_file = "test.txt" in + let text = "abcdefgh" in + let len = String.length text in + let _write = + Path.with_open_out ~create:(`If_missing 0o644) (env#fs / test_file) @@ fun sink -> + Flow.copy_string text sink + in + let buf = Cstruct.create len in + let _read = + Path.with_open_in (env#fs / test_file) @@ fun source -> + Flow.read_exact source buf + in + Path.unlink (env#fs / test_file); + Alcotest.(check string) "same string" text (Cstruct.to_string buf) + + let test_append env () = + let test_file = "test.txt" in + let text = "abcdefgh" in + let len = String.length text in + let _write = + Path.with_open_out ~create:(`If_missing 0o644) (env#fs / test_file) @@ fun sink -> + Flow.copy_string text sink + in + let _write2 = + Path.with_open_out ~append:true ~create:(`If_missing 0o644) (env#fs / test_file) @@ fun sink -> + Flow.copy_string text sink + in + let buf = Cstruct.create (2 * len) in + let _read = + Path.with_open_in (env#fs / test_file) @@ fun source -> + Flow.read_exact source buf + in + Path.unlink (env#fs / test_file); + Alcotest.(check string) "same string" (text ^ text) (Cstruct.to_string buf) + + let optint_int63 = Alcotest.testable Optint.Int63.pp Optint.Int63.equal + + let test_fails_if_exists env () = + let test_file = "./test.txt" in + let text = "abcdefgh" in + let _write = + Path.with_open_out ~create:(`If_missing 0o644) (env#fs / test_file) @@ fun sink -> + Flow.copy_string text sink + in + Fun.protect + (fun () -> + try + Path.with_open_out ~create:(`Exclusive 0o644) (env#fs / test_file) @@ fun _ -> + Alcotest.failf "Expected to fail with already exists" + with + | Eio.Exn.Io (Eio.Fs.E (Fs.Already_exists _), _) -> () + | exn -> Alcotest.failf "Expected already exists exception, got: %a" Fmt.exn exn + ) ~finally:(fun () -> Path.unlink (env#fs / test_file)) + + let test_read_write env () = + let test_file = "test.txt" in + let text = "abcdefgh" in + let len = String.length text in + let _write = + Path.with_open_out ~create:(`If_missing 0o644) (env#fs / test_file) @@ fun sink -> + Flow.copy_string text sink + in + let stat = + Eio.Switch.run @@ fun sw -> + let fd = Path.open_in ~sw (env#fs / test_file) in + File.stat fd + in + let is_file = stat.kind = `Regular_file in + Alcotest.(check bool) "is file" true is_file; + Alcotest.(check optint_int63) "same size" (Optint.Int63.of_int len) stat.size; + Path.unlink (env#fs / test_file) + + let tests env = [ + Alcotest.test_case "read and write" `Quick (test_read_write env); + Alcotest.test_case "append" `Quick (test_append env); + Alcotest.test_case "fail if exists" `Quick (test_fails_if_exists env); + Alcotest.test_case "fstat" `Quick (test_fails_if_exists env); + ] +end + +module Fibers = struct + let test_yield () = + let expect = [1; 2] in + let res = ref [] in + Fiber.both + (fun () -> Fiber.yield (); res := 1 :: !res) + (fun () -> res := 2 :: !res); + Alcotest.(check (list int)) "same list" expect !res + + let test_simple_cancel () = + let res = + Fiber.first + (fun () -> "a") + (fun () -> Fiber.yield (); failwith "b crashed") + in + Alcotest.(check string) "same string" "a" res; + let p, _r = Promise.create () in + let res = Fiber.first + (fun () -> "a") + (fun () -> Promise.await p) + in + Alcotest.(check string) "promise cancelled" "a" res + + + let tests = [ + Alcotest.test_case "yielding" `Quick test_yield; + Alcotest.test_case "fiber first cancel" `Quick test_simple_cancel; + ] +end + +module Stream = struct + type op = [ `Add of int | `Take of int ] + + let pp_op ppf = function + | `Add i -> Fmt.pf ppf "add %i" i + | `Take i -> Fmt.pf ppf "take %i" i + + let op = Alcotest.of_pp pp_op + + let test_stream () = + let l = ref [] in + let add s v = + Stream.add s v; + l := (`Add v) :: !l + in + let take s = + l := (`Take (Stream.take s)) :: !l + in + let t = Stream.create 3 in + add t 1; + Fiber.both + (fun () -> + add t 2; + add t 3; + add t 4; + ) + (fun () -> + take t; + take t; + take t; + take t + ); + let actual = List.rev !l in + let expected = [ `Add 1; `Add 2; `Add 3; `Take 1; `Take 2; `Take 3; `Take 4; `Add 4 ] in + Alcotest.(check (list op)) "same sequence" expected actual + + let tests = [ + Alcotest.test_case "stream1" `Quick test_stream + ] +end \ No newline at end of file diff --git a/lib_eio_js/browser/test/index.html b/lib_eio_js/browser/test/index.html index 0c35cf098..fc524af40 100644 --- a/lib_eio_js/browser/test/index.html +++ b/lib_eio_js/browser/test/index.html @@ -4,12 +4,15 @@ - Eio in the Browser + Eio Browser Tests + -
- -
- + \ No newline at end of file diff --git a/lib_eio_js/browser/test/test.ml b/lib_eio_js/browser/test/test.ml new file mode 100644 index 000000000..408ff1933 --- /dev/null +++ b/lib_eio_js/browser/test/test.ml @@ -0,0 +1,86 @@ +(* Avoiding dependency on js_of_ocaml *) +external set_channel_output' : + out_channel -> Jv.t -> unit + = "caml_ml_set_channel_output" + +let set_channel_flusher (out_channel : out_channel) (f : string -> unit) = + let f' = + Jv.callback ~arity:1 (fun s -> f (Jstr.binary_to_octets s)) + in + set_channel_output' out_channel f' + +let init () = + let open Brr in + let parser = Ansi.create () in + let output = + let out = El.div [ El.h1 [ El.txt' "Eio_browser" ] ] in + El.set_at (Jstr.v "style") + (Some + (Jstr.v + "font-family: monospace; color: #e8e8e8; background: #131212; \ + padding: 2em;")) + out; + El.(set_prop Prop.id (Jstr.v "output") out); + let style = El.style [ El.txt' Ansi.css ] in + El.append_children (Document.body G.document) [ style; out ]; + out + in + let get_or_make name = + match Document.find_el_by_id G.document (Jstr.v name) with + | Some v -> v + | None -> + let d = El.div [] in + El.append_children output [ d ]; + El.(set_prop Prop.id (Jstr.v name) d); + d + in + let append name s = + Brr.Console.log [ s ]; + let s = Ansi.process parser s in + let p = El.pre [] in + El.to_jv p |> fun jv -> + Jv.set jv "innerHTML" (Jv.of_string s); + El.append_children (get_or_make name) [ p ] + in + set_channel_flusher stdout (fun content -> append "stdout" content); + set_channel_flusher stderr (fun content -> append "stderr" content); + () + +module Browser_tests = struct + open Eio + + let test_timeout_cancel () = + let v = + Fiber.first + (fun () -> Eio_browser.Timeout.sleep ~ms:5000; "A") + (fun () -> "B") + in + Alcotest.(check string) "timeout cancelled" "B" v + + let test_fut_cancel () = + let p, _ = Fut.create () in + let v = + Fiber.first + (fun () -> Eio_browser.await p; "A") + (fun () -> "B") + in + Alcotest.(check string) "fut cancelled" "B" v + + let tests = [ + Alcotest.test_case "timeout cancelled" `Quick test_timeout_cancel; + Alcotest.test_case "fut cancelled" `Quick test_fut_cancel + ] +end + + +let () = + init (); + let main = + Eio_browser.run @@ fun env -> + try Alcotest.run "eio" [ + "fibers", Eio_test.Fibers.tests; + "stream", Eio_test.Stream.tests; + "browser", Browser_tests.tests + ] with Exit -> () + in + Fut.await main (fun () -> ()) \ No newline at end of file