Skip to content

Commit 753b96c

Browse files
committed
feat(runtest): dune runtest for (tests) and (inline_tests)
Signed-off-by: Ali Caglayan <alizter@gmail.com>
1 parent 9ee8f06 commit 753b96c

File tree

12 files changed

+520
-40
lines changed

12 files changed

+520
-40
lines changed

bin/build.ml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ let poll_handling_rpc_build_requests ~(common : Common.t) =
9999
| Build targets -> Target.interpret_targets (Common.root common) setup targets
100100
| Runtest test_paths ->
101101
Runtest_common.make_request
102-
~contexts:setup.contexts
102+
~scontexts:setup.scontexts
103103
~to_cwd:root.to_cwd
104104
~test_paths
105105
in

bin/import.ml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ include struct
3939
module Library = Library
4040
module Melange = Melange
4141
module Executables = Executables
42+
module Dir_contents = Dir_contents
4243
end
4344

4445
include struct

bin/runtest.ml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ let runtest_term =
4343
| Ok () ->
4444
Build.run_build_command ~common ~config ~request:(fun setup ->
4545
Runtest_common.make_request
46-
~contexts:setup.contexts
46+
~scontexts:setup.scontexts
4747
~to_cwd:(Common.root common).to_cwd
4848
~test_paths)
4949
| Error lock_held_by ->

bin/runtest_common.ml

Lines changed: 110 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,27 @@ module Test_kind = struct
44
type t =
55
| Runtest of Path.t
66
| Cram of Path.t * Source.Cram_test.t
7+
| Test_executable of
8+
{ dir : Path.t
9+
; exe_name : string
10+
}
11+
| Inline_tests of
12+
{ dir : Path.t
13+
; lib_name : Lib_name.Local.t
14+
}
715

816
let alias ~contexts = function
917
| Cram (dir, cram) ->
1018
let name = Dune_engine.Alias.Name.of_string (Source.Cram_test.name cram) in
1119
Alias.in_dir ~name ~recursive:false ~contexts dir
20+
| Test_executable { dir; exe_name } ->
21+
let name = Dune_engine.Alias.Name.of_string ("runtest-" ^ exe_name) in
22+
Alias.in_dir ~name ~recursive:false ~contexts dir
23+
| Inline_tests { dir; lib_name } ->
24+
let name =
25+
Dune_engine.Alias.Name.of_string ("runtest-" ^ Lib_name.Local.to_string lib_name)
26+
in
27+
Alias.in_dir ~name ~recursive:false ~contexts dir
1228
| Runtest dir ->
1329
Alias.in_dir ~name:Dune_rules.Alias.runtest ~recursive:true ~contexts dir
1430
;;
@@ -34,13 +50,65 @@ let find_cram_test cram_tests path =
3450
| Error (Dune_rules.Cram_rules.Missing_run_t _) | Ok _ -> None)
3551
;;
3652

37-
let all_tests_of_dir parent_dir =
53+
(** [classify_ml_test ~sctx ~dir ~ml_file] checks if [ml_file] is part of a
54+
(tests) or (library (inline_tests ...)) stanza in [dir]. Returns:
55+
- [Ok (`Test_executable exe_name)] or [Ok (`Inline_tests_library lib_name)]
56+
- [Error `Not_an_entry_point] if the file belongs to a multi-test stanza
57+
- [Error `Not_a_test] if not part of any test stanza *)
58+
let classify_ml_test ~sctx ~dir ~ml_file =
59+
let open Memo.O in
60+
let ml_file_no_ext = Filename.remove_extension ml_file in
61+
match Dune_lang.Module_name.of_string_opt ml_file_no_ext with
62+
| None -> Memo.return (Error `Not_a_test)
63+
| Some module_name ->
64+
let* dir_contents =
65+
Dir_contents.get
66+
sctx
67+
~dir:
68+
(Path.Build.append_source (Context.build_dir (Super_context.context sctx)) dir)
69+
in
70+
let* ml_sources = Dir_contents.ocaml dir_contents
71+
and* scope = Dir_contents.dir dir_contents |> Dune_rules.Scope.DB.find_by_dir in
72+
Dune_rules.Ml_sources.find_origin
73+
ml_sources
74+
~libs:(Dune_rules.Scope.libs scope)
75+
[ module_name ]
76+
>>| (function
77+
| Some (Library lib) ->
78+
if
79+
Dune_rules.Sub_system_name.Map.mem
80+
lib.sub_systems
81+
Dune_rules.Inline_tests_info.Tests.name
82+
then Ok (`Inline_tests_library (snd lib.name))
83+
else Error `Not_a_test
84+
| Some (Executables _ | Melange _) | None -> Error `Not_a_test
85+
| Some (Tests { exes; _ }) ->
86+
let exe_names = Nonempty_list.to_list exes.names |> List.map ~f:snd in
87+
if List.mem exe_names ml_file_no_ext ~equal:String.equal
88+
then Ok (`Test_executable ml_file_no_ext)
89+
else (
90+
match exe_names with
91+
| [ single_exe ] -> Ok (`Test_executable single_exe)
92+
| [] | _ :: _ :: _ -> Error `Not_an_entry_point))
93+
;;
94+
95+
let all_tests_of_dir ~sctx parent_dir =
3896
let open Memo.O in
3997
let+ cram_candidates =
4098
cram_tests_of_dir parent_dir
4199
>>| List.filter_map ~f:(fun res ->
42100
Result.to_option res
43101
|> Option.map ~f:(fun test -> Source.Cram_test.path test |> Path.Source.to_string))
102+
and+ ml_test_candidates =
103+
Source_tree.find_dir parent_dir
104+
>>= function
105+
| None -> Memo.return []
106+
| Some source_dir ->
107+
Source_tree.Dir.filenames source_dir
108+
|> Filename.Set.to_list
109+
|> List.filter ~f:(String.is_suffix ~suffix:".ml")
110+
|> Memo.List.filter ~f:(fun ml_file ->
111+
classify_ml_test ~sctx ~dir:parent_dir ~ml_file >>| Result.is_ok)
44112
and+ dir_candidates =
45113
let* parent_source_dir = Source_tree.find_dir parent_dir in
46114
match parent_source_dir with
@@ -53,23 +121,22 @@ let all_tests_of_dir parent_dir =
53121
>>| Source_tree.Dir.path
54122
>>| Path.Source.to_string)
55123
in
56-
List.concat [ cram_candidates; dir_candidates ]
124+
List.concat [ cram_candidates; ml_test_candidates; dir_candidates ]
57125
|> String.Set.of_list
58126
|> String.Set.to_list
59127
;;
60128

61-
let explain_unsuccessful_search path ~parent_dir =
129+
let explain_unsuccessful_search ~sctx path ~parent_dir =
62130
let open Memo.O in
63-
let+ candidates = all_tests_of_dir parent_dir in
131+
let+ candidates = all_tests_of_dir ~sctx parent_dir in
64132
User_error.raise
65133
~hints:(User_message.did_you_mean (Path.Source.to_string path) ~candidates)
66134
[ Pp.textf "%S does not match any known test." (Path.Source.to_string path) ]
67135
;;
68136

69-
(* [disambiguate_test_name path] is a function that takes in a
70-
directory [path] and classifies it as either a cram test or a directory to
71-
run tests in. *)
72-
let disambiguate_test_name path =
137+
(* [disambiguate_test_name ~sctx path] classifies [path] as a cram test,
138+
ml test executable, inline tests library, or a directory to run @runtest in. *)
139+
let disambiguate_test_name ~sctx path =
73140
match Path.Source.parent path with
74141
| None -> Memo.return @@ Test_kind.Runtest (Path.source Path.Source.root)
75142
| Some parent_dir ->
@@ -80,27 +147,50 @@ let disambiguate_test_name path =
80147
(* If we find the cram test, then we request that is run. *)
81148
Memo.return (Test_kind.Cram (Path.source parent_dir, test))
82149
| None ->
83-
(* If we don't find it, then we assume the user intended a directory for
84-
@runtest to be used. *)
85-
Source_tree.find_dir path
86-
>>= (function
87-
(* We need to make sure that this directory or file exists. *)
88-
| Some _ -> Memo.return (Test_kind.Runtest (Path.source path))
89-
| None -> explain_unsuccessful_search path ~parent_dir))
150+
(* Check if it's an ML test file *)
151+
let filename = Path.Source.basename path in
152+
let* ml_test =
153+
if String.is_suffix filename ~suffix:".ml"
154+
then classify_ml_test ~sctx ~dir:parent_dir ~ml_file:filename
155+
else Memo.return (Error `Not_a_test)
156+
in
157+
(match ml_test with
158+
| Ok (`Test_executable exe_name) ->
159+
Memo.return
160+
(Test_kind.Test_executable { dir = Path.source parent_dir; exe_name })
161+
| Ok (`Inline_tests_library lib_name) ->
162+
Memo.return (Test_kind.Inline_tests { dir = Path.source parent_dir; lib_name })
163+
| Error `Not_an_entry_point ->
164+
User_error.raise
165+
[ Pp.textf
166+
"%S is used by multiple test executables and cannot be run directly."
167+
filename
168+
]
169+
| Error `Not_a_test ->
170+
(* Assume the user intended a directory for @runtest to be used. *)
171+
Source_tree.find_dir path
172+
>>= (function
173+
(* We need to make sure that this directory or file exists. *)
174+
| Some _ -> Memo.return (Test_kind.Runtest (Path.source path))
175+
| None -> explain_unsuccessful_search ~sctx path ~parent_dir)))
90176
;;
91177

92-
let make_request ~contexts ~to_cwd ~test_paths =
178+
let make_request ~scontexts ~to_cwd ~test_paths =
179+
let contexts =
180+
Context_name.Map.to_list_map scontexts ~f:(fun _ -> Super_context.context)
181+
in
93182
List.map test_paths ~f:(fun dir ->
94183
let dir = Path.of_string dir |> Path.Expert.try_localize_external in
95-
let contexts, src_dir =
184+
let sctx, contexts, src_dir =
96185
match (Util.check_path contexts dir : Util.checked) with
97-
| In_build_dir (context, dir) -> [ context ], dir
186+
| In_build_dir (context, dir) ->
187+
Context_name.Map.find_exn scontexts (Context.name context), [ context ], dir
98188
| In_source_dir dir ->
99189
(* We need to adjust the path here to make up for the current working directory. *)
100190
let dir =
101191
Path.Source.L.relative Path.Source.root (to_cwd @ Path.Source.explode dir)
102192
in
103-
contexts, dir
193+
Context_name.Map.find_exn scontexts Context_name.default, contexts, dir
104194
| In_private_context _ | In_install_dir _ ->
105195
User_error.raise
106196
[ Pp.textf "This path is internal to dune: %s" (Path.to_string_maybe_quoted dir)
@@ -113,7 +203,7 @@ let make_request ~contexts ~to_cwd ~test_paths =
113203
]
114204
in
115205
let open Action_builder.O in
116-
Action_builder.of_memo (disambiguate_test_name src_dir)
206+
Action_builder.of_memo (disambiguate_test_name ~sctx src_dir)
117207
>>| Test_kind.alias ~contexts
118208
>>= Alias.request)
119209
|> Action_builder.all_unit

bin/runtest_common.mli

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
open Import
22

33
val make_request
4-
: contexts:Context.t list
4+
: scontexts:Super_context.t Context_name.Map.t
55
-> to_cwd:string list
66
-> test_paths:string list
77
-> unit Action_builder.t

doc/changes/added/13064.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
- `dune runtest` can now run individual test executables from `(tests)` stanzas
2+
and inline tests from `(library (inline_tests))` stanzas by providing their
3+
source files as arguments. (#13064, fixes #870, @Alizter)

src/dune_rules/ml_sources.ml

Lines changed: 45 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,27 @@ module Origin = struct
55
type t =
66
| Library of Library.t
77
| Executables of Executables.t
8+
| Tests of Tests.t
89
| Melange of Melange_stanzas.Emit.t
910

1011
let loc = function
1112
| Library l -> l.buildable.loc
1213
| Executables e -> e.buildable.loc
14+
| Tests t -> t.exes.buildable.loc
1315
| Melange mel -> mel.loc
1416
;;
1517

1618
let preprocess = function
1719
| Library l -> l.buildable.preprocess
1820
| Executables e -> e.buildable.preprocess
21+
| Tests t -> t.exes.buildable.preprocess
1922
| Melange mel -> mel.preprocess
2023
;;
2124

2225
let to_dyn = function
2326
| Library _ -> Dyn.variant "Library" [ Dyn.Opaque ]
2427
| Executables _ -> Dyn.variant "Executables" [ Dyn.Opaque ]
28+
| Tests _ -> Dyn.variant "Tests" [ Dyn.Opaque ]
2529
| Melange _ -> Dyn.variant "Melange" [ Dyn.Opaque ]
2630
;;
2731
end
@@ -58,10 +62,11 @@ module Per_stanza = struct
5862
type groups =
5963
{ libraries : Library.t group_part list
6064
; executables : Executables.t group_part list
65+
; tests : Tests.t group_part list
6166
; melange_emits : Melange_stanzas.Emit.t group_part list
6267
}
6368

64-
let make { libraries = libs; executables = exes; melange_emits = emits } =
69+
let make { libraries = libs; executables = exes; tests; melange_emits = emits } =
6570
let libraries, libraries_by_obj_dir =
6671
List.fold_left
6772
libs
@@ -84,16 +89,26 @@ module Per_stanza = struct
8489
by_id, by_obj_dir)
8590
in
8691
let executables =
87-
match
88-
String.Map.of_list_map exes ~f:(fun (part : Executables.t group_part) ->
89-
let first_exe = snd (Nonempty_list.hd part.stanza.names) in
90-
let origin : Origin.t = Executables part.stanza in
91-
first_exe, (origin, part.modules, part.obj_dir))
92-
with
93-
| Ok x -> x
94-
| Error (name, _, part) ->
92+
let entries =
93+
List.concat
94+
[ List.map exes ~f:(fun (part : Executables.t group_part) ->
95+
let first_exe = snd (Nonempty_list.hd part.stanza.names) in
96+
let origin : Origin.t = Executables part.stanza in
97+
first_exe, (origin, part.modules, part.obj_dir, part.stanza.buildable.loc))
98+
; List.map tests ~f:(fun (part : Tests.t group_part) ->
99+
let first_exe = snd (Nonempty_list.hd part.stanza.exes.names) in
100+
let origin : Origin.t = Tests part.stanza in
101+
( first_exe
102+
, (origin, part.modules, part.obj_dir, part.stanza.exes.buildable.loc) ))
103+
]
104+
in
105+
match String.Map.of_list entries with
106+
| Ok map ->
107+
String.Map.map map ~f:(fun (origin, modules, obj_dir, _loc) ->
108+
origin, modules, obj_dir)
109+
| Error (name, (_, _, _, loc1), (_, _, _, _loc2)) ->
95110
User_error.raise
96-
~loc:part.stanza.buildable.loc
111+
~loc:loc1
97112
[ Pp.textf "Executable %S appears for the second time in this directory" name ]
98113
in
99114
let melange_emits =
@@ -118,6 +133,8 @@ module Per_stanza = struct
118133
by_path (Library part.stanza, part.dir) part.sources)
119134
; List.rev_concat_map exes ~f:(fun part ->
120135
by_path (Executables part.stanza, part.dir) part.sources)
136+
; List.rev_concat_map tests ~f:(fun part ->
137+
by_path (Tests part.stanza, part.dir) part.sources)
121138
; List.rev_concat_map emits ~f:(fun part ->
122139
by_path (Melange part.stanza, part.dir) part.sources)
123140
]
@@ -252,7 +269,7 @@ let find_origin (t : t) ~libs path =
252269
| Some origins ->
253270
Memo.List.filter_map origins ~f:(fun (origin, dir) ->
254271
match origin with
255-
| Executables _ | Melange _ -> Memo.return (Some origin)
272+
| Executables _ | Tests _ | Melange _ -> Memo.return (Some origin)
256273
| Library lib ->
257274
let src_dir = Path.drop_optional_build_context_src_exn (Path.build dir) in
258275
Lib.DB.available_by_lib_id libs (Local (Library.to_lib_id ~src_dir lib))
@@ -469,14 +486,18 @@ let modules_of_stanzas =
469486
| `Skip -> loop l acc
470487
| `Library y -> loop l { acc with libraries = y :: acc.libraries }
471488
| `Executables y -> loop l { acc with executables = y :: acc.executables }
489+
| `Tests y -> loop l { acc with tests = y :: acc.tests }
472490
| `Melange_emit y -> loop l { acc with melange_emits = y :: acc.melange_emits })
473491
in
474-
fun l -> loop l { libraries = []; executables = []; melange_emits = [] }
492+
fun l -> loop l { libraries = []; executables = []; tests = []; melange_emits = [] }
475493
in
476494
fun l ->
477-
let { Per_stanza.libraries; executables; melange_emits } = rev_filter_partition l in
495+
let { Per_stanza.libraries; executables; tests; melange_emits } =
496+
rev_filter_partition l
497+
in
478498
{ Per_stanza.libraries = List.rev libraries
479499
; executables = List.rev executables
500+
; tests = List.rev tests
480501
; melange_emits = List.rev melange_emits
481502
}
482503
in
@@ -505,6 +526,11 @@ let modules_of_stanzas =
505526
in
506527
`Executables { Per_stanza.stanza = exes; sources; modules; obj_dir; dir }
507528
in
529+
let make_tests ~dir ~expander ~modules ~project tests =
530+
let+ result = make_executables ~dir ~expander ~modules ~project tests.Tests.exes in
531+
match result with
532+
| `Executables group_part -> `Tests { group_part with stanza = tests }
533+
in
508534
fun stanzas ~expander ~project ~dir ~libs ~lookup_vlib ~modules ~include_subdirs ->
509535
Memo.parallel_map stanzas ~f:(fun stanza ->
510536
let enabled_if =
@@ -541,7 +567,7 @@ let modules_of_stanzas =
541567
let obj_dir = Library.obj_dir lib ~dir in
542568
`Library { Per_stanza.stanza = lib; sources; modules; dir; obj_dir }
543569
| Executables.T exes -> make_executables ~dir ~expander ~modules ~project exes
544-
| Tests.T { exes; _ } -> make_executables ~dir ~expander ~modules ~project exes
570+
| Tests.T tests -> make_tests ~dir ~expander ~modules ~project tests
545571
| Melange_stanzas.Emit.T mel ->
546572
let obj_dir = Obj_dir.make_melange_emit ~dir ~name:mel.target in
547573
let+ sources, modules =
@@ -665,9 +691,11 @@ let make
665691
part.stanza, part.modules, part.obj_dir)
666692
in
667693
let exes =
668-
List.map
669-
modules_of_stanzas.executables
670-
~f:(fun (part : _ Per_stanza.group_part) -> part.modules, part.obj_dir)
694+
let modules_and_obj_dir { Per_stanza.modules; obj_dir; _ } = modules, obj_dir in
695+
List.concat
696+
[ List.map modules_of_stanzas.executables ~f:modules_and_obj_dir
697+
; List.map modules_of_stanzas.tests ~f:modules_and_obj_dir
698+
]
671699
in
672700
Artifacts_obj.make
673701
~dir

src/dune_rules/ml_sources.mli

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ module Origin : sig
88
type t =
99
| Library of Library.t
1010
| Executables of Executables.t
11+
| Tests of Tests.t
1112
| Melange of Melange_stanzas.Emit.t
1213

1314
val preprocess : t -> Preprocess.With_instrumentation.t Preprocess.Per_module.t

0 commit comments

Comments
 (0)