I’m learning a bit about async and I’m trying to make the following test pass:
let%test_unit _ =
let path = "/tmp/ocaml-async" in
let promise : string list Async.Deferred.t = Async.Reader.file_lines path in
let lst = Async.Deferred.value_exn promise in
[%test_eq: Base.string Base.list] [ "Hello"; "World" ] lst
I know I need to start the scheduler somewhere, with Core.never_returns (Async.Scheduler.go ()).
But I’m confused about how to proceed, since if I call the scheduler too early, I’l block the program too soon.
And if I call it after my test, then the test fails because no scheduler was running at the time of execution.
I get this error:
Failure "Deferred.value_exn called on undetermined deferred"
Simplest way is to probably switch to expect tests, which will naturally support the async tests. Assuming you have (inline_tests (deps data.txt)) somewhere in your dune file:
I prefer to use ppx_assert in most cases so Thread_safe.block_on_async_exn is perfect!
I see that opening Async changes the behaviour of let%expect_test as a side effect but I’m trying to understand how all the pieces fit together so I prefer to not open things globally, and just open at the call site.
Looking at the ppx_expect README, I understand that I should be able to write the test in a similar fashion:
let%expect_test "I am broken" =
let open Async.Deferred.Let_syntax in
let%map lines = Async.Reader.file_lines "data.txt" in
let () = Async.print_s @@ [%sexp_of: Base.string Base.list] lines in
let%bind () = [%expect {| (hello world) |}] in
()
But that’s obviously wrong. What am I misunderstanding? (I would prefer not having to deal with setting up an expect test module if that’s a semi-requirement).
For the time being, I can work around the issue by using block_on_async_exn, as I would with ppx_assert:
let%expect_test _ =
Async.Thread_safe.block_on_async_exn (fun () ->
let open Async.Deferred.Let_syntax in
let%map (lines : string list) = Async.Reader.file_lines "data.txt" in
Async.print_s @@ [%sexp_of: Base.string Base.list] lines;
[%expect {| (hello world) |}])
I understand not wanting to open things globally, but in the case of core and aysnc, it is designed to be opened in that way. E.g., this quote from the Real World OCaml book:
Async, like Core, is designed to be an extension to your basic programming environment, and is intended to be opened.
Not saying you should or shouldn’t open, but just something to keep in mind when using base/core/async, that design.
So the broken one you show is broken for a couple of reasons.
The [%expect ...] thing returns unit not unit Deferred.t so the let%bind () = [%expect {| (hello world) |}] in () should be just [%expect {| (hello world) |}]. (See below)
Normally, the expect_test should be unit.
But the let%map lines = ... in () says return unit Deferred.t (well after that first fix i mention at least)
So that will not work
With open! Async the expect_test will need to be unit Deferred.t, so it will work if you open Async
It’s because the Async changes the normal Expect_test_config (also see below)
Regarding [%expect ...] not returning unit Deferred.t even though the readme says it should when Async is opened…Sometime the Expect_test_config was renamed from Expect_test_config_with_unit_expect, so they must have made the transition that was mentioned as planned in the readme. (ie that you get the %expect that returns unit rather than unit Deferred.ml as the readme mentions. So while you used to write let%bind () = [%expect foo] in you can now just write [%expect foo]; even with the open! Async.
Finally check this out. You can “customize” the expect test configuration with the Expect_test_config module. It will be passed to Expect_test_collector.Make (functor docs, sig docs) during the ppx expansion. Note that the B.Expect_test_config is taken from the Async code directly.
module A = struct
module Expect_test_config = struct
module IO = struct
type 'a t = 'a
let return x = x
end
let run f = f ()
let sanitize x = x
let upon_unreleasable_issue = Expect_test_config.upon_unreleasable_issue
end
let%expect_test "i'm not broken" =
print_endline "yo" ;
let () = [%expect {| yo |}] in
()
end
module B = struct
module Expect_test_config = struct
open! Async_kernel
open! Async_unix
module IO = Deferred
module Expect_test_config = Core.Expect_test_config
(* See how it uses the same block_on_async thing as we did above? *)
let run f = Thread_safe.block_on_async_exn f
let sanitize s = s
let upon_unreleasable_issue = Expect_test_config.upon_unreleasable_issue
end
(* Without opening Async out here *)
let%expect_test "I am not broken" =
let open Async.Deferred.Let_syntax in
let%map lines = Async.Reader.file_lines "data.txt" in
Async.print_s @@ [%sexp_of: Base.string Base.list] lines ;
[%expect {| (hello world) |}]
end
Unfortunately, I’m pretty sure that the ppx_inline_test cannot be configured to automatically work with the deferreds, unless you go and change a bit of the code. So you have to ues the block_on_async_exn directly.
I did note that async has been designed to be opened. It’s surely convenient but I find it makes things more difficult to understand. So I try to avoid doing that while I’m learning.
As an exercise, I’ve managed to reduce the test module further:
module MyAsyncTests = struct
module Expect_test_config = struct
module IO = Async.Deferred
let run f = Async.Thread_safe.block_on_async_exn f
let sanitize s = s
let upon_unreleasable_issue = Expect_test_config.upon_unreleasable_issue
end
let%expect_test "It works! :)" =
let open Async.Deferred.Let_syntax in
let%map lines = Async.Reader.file_lines "data.txt" in
Async.print_s @@ [%sexp_of: Base.string Base.list] lines;
[%expect {| (Hello World) |}]
end
I like this testing package. I find I can make my tests quite readable by introducing my own functions. For simple cases, I think I’ll go with something like this.
let async_test = Async.Thread_safe.block_on_async_exn
let ( => ) = [%test_eq: Base.string Base.list]
let%test_unit _ =
async_test @@ fun () ->
let open Async.Deferred.Let_syntax in
let%map lines = Async.Reader.file_lines "data.txt" in
lines => [ "Hello"; "World" ]
let%test_unit _ =
async_test @@ fun () ->
let open Async.Deferred.Let_syntax in
let%map lines = Async.Reader.file_lines "data2.txt" in
lines => [ "Goodbye"; "World" ]