How to test a function returning `'a Async.Deferred.t` with ppx_assert?

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"

Can anyone enlighten me?

1 Like

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:

open! Core
open! Async

let%expect_test _ =
  let%map lines = Reader.file_lines "data.txt" in
  print_s @@ [%sexp_of: string list] lines ;
  [%expect {| (hello world) |}]

If you really want to use the ppx_assert, you can do Thread_safe.block_on_async_exn like so:

open! Core
open! Async

let%test_unit _ =
  Thread_safe.block_on_async_exn (fun () ->
      let%map lines = Reader.file_lines "data.txt" in
      [%test_result: string list] lines ~expect:["hello"; "world"] )

Edit: and just in case you haven’t seen it, the let%map is the ppx_let.

Awesome, thanks!

I didn’t know about (deps data.txt), very nice.

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) |}])
1 Like

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.

3 Likes

Your explanation really helped, thank you!

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" ]

1 Like