Skip to content

Commit 1fe0783

Browse files
committed
Port Lwttester to Alcotest
[Alcotest][] is "a lightweight and colourful test framework". It provides benefits over Lwt's current lwttester: it allows selecting which test cases and test suites to run, has a colorful logging, better integration with dune and standalone testing, and better logging overall. Porting the whole test suite was deemed too complex without code-rewriting tools, so only Lwt's Test library was re-implemented on top of Alcotest. We can revisit that later. 1. The main caveat is that Alcotest has no support for running tests concurrently, so the whole run is slightly longer. 2. Another problem is that alcotest-lwt re-exports lwt.unix which causes circular dependencies, so version alcotest-lwt.1.5.0 was embedded. 3. Usage of the Skip exception to skip a test while it's running cannot be ported to Alcotest too. 4. Alcotest requires OCaml >= 4.05. We keep the old test suite for OCaml < 4.05.
1 parent 5c333d0 commit 1fe0783

File tree

5 files changed

+160
-17
lines changed

5 files changed

+160
-17
lines changed

lwt.opam

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ maintainer: [
1919
dev-repo: "git+https://github.com/ocsigen/lwt.git"
2020

2121
depends: [
22-
"cppo" {build & >= "1.1.0"}
22+
"cppo" {build & with-test & >= "1.1.0"}
2323
"dune" {>= "1.8.0"}
2424
"dune-configurator"
2525
"mmap" {>= "1.1.0" & "os" != "win32"} # mmap is needed as long as Lwt supports OCaml < 4.06.0.
@@ -31,6 +31,8 @@ depends: [
3131
# Until https://github.com/aantron/bisect_ppx/pull/327.
3232
# "bisect_ppx" {dev & >= "2.0.0"}
3333
"ocamlfind" {dev & >= "1.7.3-1"}
34+
35+
("alcotest" {with-test & >= "1.5.0"} & "ocaml" {>= "4.05"})
3436
]
3537

3638
depopts: [

lwt_luv.opam

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ depends: [
2222

2323
# Until https://github.com/aantron/bisect_ppx/pull/327.
2424
# "bisect_ppx" {dev & >= "2.0.0"}
25+
26+
("alcotest" {with-test & >= "1.5.0"} & "ocaml" {>= "4.05"})
27+
"cppo" {with-test & >= "1.1.0"}
2528
]
2629

2730
build: [

lwt_react.opam

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ depends: [
2121
"lwt" {>= "3.0.0"}
2222
"ocaml"
2323
"react" {>= "1.0.0"}
24+
25+
("alcotest" {with-test & >= "1.5.0"} & "ocaml" {>= "4.05"})
26+
"cppo" {with-test & >= "1.1.0"}
2427
]
2528

2629
build: [

test/dune

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1+
(rule
2+
(targets test.ml)
3+
(deps (:ml test.cppo.ml))
4+
(action
5+
(chdir %{project_root}
6+
(run %{bin:cppo} -V OCAML:%{ocaml_version} %{ml} -o %{targets}))))
7+
18
(library
29
(name lwttester)
310
(wrapped false)
4-
(libraries lwt unix lwt.unix))
11+
(libraries lwt unix lwt.unix alcotest))

test/test.ml renamed to test/test.cppo.ml

Lines changed: 143 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,32 @@
11
(* This file is part of Lwt, released under the MIT license. See LICENSE.md for
22
details, or visit https://github.com/ocsigen/lwt/blob/master/LICENSE.md. *)
33

4-
4+
exception Skip
55

66
type test = {
77
test_name : string;
88
skip_if_this_is_false : unit -> bool;
9-
sequential : bool;
10-
run : unit -> bool Lwt.t;
9+
sequential : bool; (* Sequential is ignored in Alcotest *)
10+
run : [`Lwt of unit -> bool Lwt.t | `Direct of unit -> bool ];
1111
}
1212

13+
let test_direct test_name ?(only_if = fun () -> true) run =
14+
{ test_name; skip_if_this_is_false = only_if; sequential = false; run = `Direct run; }
15+
16+
let test test_name ?(only_if = fun () -> true) ?(sequential = false) run =
17+
{ test_name; skip_if_this_is_false = only_if; sequential; run = `Lwt run; }
18+
19+
20+
#if OCAML_VERSION < (4, 05, 0)
21+
1322
type outcome =
1423
| Passed
1524
| Failed
1625
| Exception of exn
1726
| Skipped
1827
19-
exception Skip
2028
exception Duplicate_Test_Names of string
2129
22-
let test_direct test_name ?(only_if = fun () -> true) run =
23-
let run =
24-
fun () ->
25-
Lwt.return (run ())
26-
in
27-
{test_name; skip_if_this_is_false = only_if; sequential = false; run}
28-
29-
let test test_name ?(only_if = fun () -> true) ?(sequential = false) run =
30-
{test_name; skip_if_this_is_false = only_if; sequential; run}
31-
3230
module Log =
3331
struct
3432
let log_file =
@@ -82,7 +80,9 @@ let run_test : test -> outcome Lwt.t = fun test ->
8280
let test_completion_promise =
8381
Lwt.try_bind
8482
(fun () ->
85-
test.run ())
83+
match test.run with
84+
| `Lwt run -> run ()
85+
| `Direct run -> Lwt.return (run ()))
8686
8787
(fun test_did_pass ->
8888
if test_did_pass then
@@ -340,6 +340,134 @@ let concurrent library_name suites =
340340
let concurrent library_name suites =
341341
Lwt_main.run (concurrent library_name suites)
342342
343+
#else
344+
345+
(* Alcotest_lwt 1.5.0
346+
*
347+
* Copyright (c) 2017 Thomas Gazagnaire <[email protected]>
348+
*
349+
* Permission to use, copy, modify, and distribute this software for any
350+
* purpose with or without fee is hereby granted, provided that the above
351+
* copyright notice and this permission notice appear in all copies.
352+
*
353+
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
354+
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
355+
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
356+
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
357+
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
358+
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
359+
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
360+
*)
361+
362+
module Alcotest_lwt_intf = struct
363+
module type V1 = sig
364+
include Alcotest_engine.V1.Cli.S with type return = unit Lwt.t
365+
366+
val test_case :
367+
string ->
368+
Alcotest.speed_level ->
369+
(Lwt_switch.t -> 'a -> unit Lwt.t) ->
370+
'a test_case
371+
372+
val test_case_sync :
373+
string -> Alcotest.speed_level -> ('a -> unit) -> 'a test_case
374+
end
375+
376+
module type Alcotest_lwt = sig
377+
include V1
378+
379+
(** {1 Versioned APIs} *)
380+
381+
module V1 : V1
382+
(** An alias of the above API that provides a stability guarantees over major
383+
version changes. *)
384+
end
385+
end
386+
387+
module Alcotest_lwt : sig
388+
include Alcotest_lwt_intf.Alcotest_lwt
389+
end = struct
390+
let run_test fn args =
391+
let async_ex, async_waker = Lwt.wait () in
392+
let handle_exn ex =
393+
Printf.eprintf "Uncaught async exception: %s\n%s" (Printexc.to_string ex) (Printexc.get_backtrace ());
394+
if Lwt.state async_ex = Lwt.Sleep then Lwt.wakeup_exn async_waker ex
395+
in
396+
Lwt.async_exception_hook := handle_exn;
397+
Lwt_switch.with_switch (fun sw -> Lwt.pick [ fn sw args; async_ex ])
398+
399+
module V1 = struct
400+
module Tester = Alcotest_engine.V1.Cli.Make (Alcotest.Unix_platform) (Lwt)
401+
include Tester
402+
403+
let test_case_sync n s f = test_case n s (fun x -> Lwt.return (f x))
404+
let test_case n s f = test_case n s (run_test f)
405+
end
406+
407+
include V1
408+
end
409+
410+
411+
module List = struct
412+
include List
413+
414+
let filter_map f =
415+
let rec aux accu = function
416+
| [] -> rev accu
417+
| x :: l ->
418+
match f x with
419+
| None -> aux accu l
420+
| Some v -> aux (v :: accu) l
421+
in
422+
aux []
423+
end
424+
425+
426+
open Lwt.Infix
427+
428+
type suite = {
429+
suite_name : string;
430+
suite_tests : unit Alcotest_lwt.test_case list;
431+
skip_suite_if_this_is_false : unit -> bool;
432+
}
433+
434+
let suite name ?(only_if = fun () -> true) tests =
435+
let to_test_case test =
436+
match test.run with
437+
| `Lwt run ->
438+
Alcotest_lwt.test_case test.test_name `Quick (fun _switch () ->
439+
run () >|= fun b ->
440+
Alcotest.(check bool) "success" b true)
441+
| `Direct run ->
442+
Alcotest_lwt.test_case_sync test.test_name `Quick (fun () ->
443+
let b = run () in
444+
Alcotest.(check bool) "success" b true)
445+
in
446+
let tests =
447+
List.filter_map (fun test ->
448+
if test.skip_if_this_is_false () then
449+
Some (to_test_case test)
450+
else None)
451+
tests
452+
in
453+
{suite_name = name;
454+
suite_tests = tests;
455+
skip_suite_if_this_is_false = only_if}
456+
457+
let run library_name suites =
458+
let tests =
459+
List.filter_map (fun suite ->
460+
if suite.skip_suite_if_this_is_false () then
461+
Some (suite.suite_name, suite.suite_tests)
462+
else None)
463+
suites in
464+
Alcotest_lwt.run library_name tests
465+
466+
let run library_name suites = Lwt_main.run @@ run library_name suites
467+
let concurrent = run
468+
469+
#endif
470+
343471
let with_async_exception_hook hook f =
344472
let old_hook = !Lwt.async_exception_hook in
345473
Lwt.async_exception_hook := hook;

0 commit comments

Comments
 (0)