Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions bin/build.ml
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ let poll_handling_rpc_build_requests ~(common : Common.t) ~config =
| Runtest test_paths ->
Runtest_common.make_request
~contexts:setup.contexts
~scontexts:setup.scontexts
~to_cwd:root.to_cwd
~test_paths
in
Expand Down
4 changes: 4 additions & 0 deletions bin/import.ml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ include struct
module Library = Library
module Melange = Melange
module Executables = Executables
module Dune_load = Dune_load
module Dir_contents = Dir_contents
module Sub_system_name = Sub_system_name
module Inline_tests_info = Inline_tests_info
end

include struct
Expand Down
1 change: 1 addition & 0 deletions bin/runtest.ml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ let runtest_term =
Build.run_build_command ~common ~config ~request:(fun setup ->
Runtest_common.make_request
~contexts:setup.contexts
~scontexts:setup.scontexts
~to_cwd:(Common.root common).to_cwd
~test_paths)
| Error lock_held_by ->
Expand Down
124 changes: 105 additions & 19 deletions bin/runtest_common.ml
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,22 @@ module Test_kind = struct
type t =
| Runtest of Path.t
| Cram of Path.t * Source.Cram_test.t
| Test_executable of Path.t * string (* dir, executable name *)
| Inline_tests of Path.t * string (* dir, library name *)

let alias ~contexts = function
| Cram (dir, cram) ->
let name = Dune_engine.Alias.Name.of_string (Source.Cram_test.name cram) in
Alias.in_dir ~name ~recursive:false ~contexts dir
| Test_executable (dir, exe_name) ->
(* CR-someday Alizter: get the proper alias, also check js_of_ocaml
runtst aliases? *)
let name = Dune_engine.Alias.Name.of_string ("runtest-" ^ exe_name) in
Alias.in_dir ~name ~recursive:false ~contexts dir
| Inline_tests (dir, lib_name) ->
(* CR-someday Alizter: get the proper alias where it is defined. *)
let name = Dune_engine.Alias.Name.of_string ("runtest-" ^ lib_name) in
Alias.in_dir ~name ~recursive:false ~contexts dir
| Runtest dir ->
Alias.in_dir ~name:Dune_rules.Alias.runtest ~recursive:true ~contexts dir
;;
Expand All @@ -34,13 +45,60 @@ let find_cram_test cram_tests path =
| Error (Dune_rules.Cram_rules.Missing_run_t _) | Ok _ -> None)
;;

let all_tests_of_dir parent_dir =
let has_inline_tests (lib : Dune_rules.Library.t) =
Dune_rules.Sub_system_name.Map.mem
lib.sub_systems
Dune_rules.Inline_tests_info.Tests.name
;;

let classify_ml_test ~sctx ~dir ~ml_file =
let open Memo.O in
let module_name = Filename.remove_extension ml_file in
match Dune_lang.Module_name.of_string_opt module_name with
| None -> Memo.return (Error `Not_a_test)
| Some module_name ->
let build_dir =
Path.Build.append_source (Super_context.context sctx |> Context.build_dir) dir
in
let* dir_contents = Dir_contents.get sctx ~dir:build_dir in
let* ml_sources = Dir_contents.ocaml dir_contents
and* scope = Dir_contents.dir dir_contents |> Dune_rules.Scope.DB.find_by_dir in
Dune_rules.Ml_sources.find_origin
ml_sources
~libs:(Dune_rules.Scope.libs scope)
[ module_name ]
>>| (function
| Some (Library lib) when has_inline_tests lib ->
let lib_name = snd lib.name |> Lib_name.Local.to_string in
Ok (`Inline_tests_library lib_name)
| Some (Library _ | Executables _ | Melange _) | None -> Error `Not_a_test
| Some (Tests ({ exes; _ } as _test)) ->
let exe_names = Nonempty_list.to_list exes.names |> List.map ~f:snd in
if List.mem exe_names (Filename.remove_extension ml_file) ~equal:String.equal
then Ok (`Test_executable (Filename.remove_extension ml_file))
else (
match exe_names with
| [ single_exe ] -> Ok (`Test_executable single_exe)
| [] | _ :: _ -> Error `Not_an_entry_point))
;;

let all_tests_of_dir ~sctx parent_dir =
let open Memo.O in
let+ cram_candidates =
cram_tests_of_dir parent_dir
>>| List.filter_map ~f:(fun res ->
Result.to_option res
|> Option.map ~f:(fun test -> Source.Cram_test.path test |> Path.Source.to_string))
and+ test_executable_candidates =
Source_tree.find_dir parent_dir
>>= function
| None -> Memo.return []
| Some source_dir ->
Source_tree.Dir.filenames source_dir
|> Filename.Set.to_list
|> List.filter ~f:(fun f -> String.is_suffix f ~suffix:".ml")
|> Memo.List.filter ~f:(fun ml_file ->
classify_ml_test ~sctx ~dir:parent_dir ~ml_file >>| Result.is_ok)
and+ dir_candidates =
let* parent_source_dir = Source_tree.find_dir parent_dir in
match parent_source_dir with
Expand All @@ -53,23 +111,23 @@ let all_tests_of_dir parent_dir =
>>| Source_tree.Dir.path
>>| Path.Source.to_string)
in
List.concat [ cram_candidates; dir_candidates ]
List.concat [ cram_candidates; test_executable_candidates; dir_candidates ]
|> String.Set.of_list
|> String.Set.to_list
;;

let explain_unsuccessful_search path ~parent_dir =
let explain_unsuccessful_search ~sctx path ~parent_dir =
let open Memo.O in
let+ candidates = all_tests_of_dir parent_dir in
let+ candidates = all_tests_of_dir ~sctx parent_dir in
User_error.raise
~hints:(User_message.did_you_mean (Path.Source.to_string path) ~candidates)
[ Pp.textf "%S does not match any known test." (Path.Source.to_string path) ]
;;

(* [disambiguate_test_name path] is a function that takes in a
directory [path] and classifies it as either a cram test or a directory to
(* [disambiguate_test_name path] is a function that takes in a directory [path]
and classifies it as either a cram test, test executable, or a directory to
run tests in. *)
let disambiguate_test_name path =
let disambiguate_test_name ~sctx path =
match Path.Source.parent path with
| None -> Memo.return @@ Test_kind.Runtest (Path.source Path.Source.root)
| Some parent_dir ->
Expand All @@ -80,27 +138,55 @@ let disambiguate_test_name path =
(* If we find the cram test, then we request that is run. *)
Memo.return (Test_kind.Cram (Path.source parent_dir, test))
| None ->
(* If we don't find it, then we assume the user intended a directory for
@runtest to be used. *)
Source_tree.find_dir path
>>= (function
(* We need to make sure that this directory or file exists. *)
| Some _ -> Memo.return (Test_kind.Runtest (Path.source path))
| None -> explain_unsuccessful_search path ~parent_dir))
(* Check for test executables *)
let filename = Path.Source.basename path in
let* test_kind_opt =
classify_ml_test ~sctx ~dir:parent_dir ~ml_file:filename
>>| function
| Ok (`Test_executable exe_name) -> Some (`Test_exe exe_name)
| Ok (`Inline_tests_library lib_name) -> Some (`Inline_tests lib_name)
| Error `Not_an_entry_point ->
User_error.raise
[ Pp.textf
"%S is used by multiple test executables and cannot be run directly."
filename
]
| Error `Not_a_test -> None
in
(match test_kind_opt with
| Some (`Test_exe exe_name) ->
(* Found a test executable for this ML file *)
Memo.return (Test_kind.Test_executable (Path.source parent_dir, exe_name))
| Some (`Inline_tests lib_name) ->
(* Found an inline tests library for this ML file *)
Memo.return (Test_kind.Inline_tests (Path.source parent_dir, lib_name))
| None ->
(* If we don't find it, then we assume the user intended a directory for
@runtest to be used. *)
Source_tree.find_dir path
>>= (function
(* We need to make sure that this directory or file exists. *)
| Some _ -> Memo.return (Test_kind.Runtest (Path.source path))
| None -> explain_unsuccessful_search ~sctx path ~parent_dir)))
;;

let make_request ~contexts ~to_cwd ~test_paths =
let make_request ~contexts ~scontexts ~to_cwd ~test_paths =
List.map test_paths ~f:(fun dir ->
let dir = Path.of_string dir |> Path.Expert.try_localize_external in
let contexts, src_dir =
let sctx, contexts, src_dir =
match (Util.check_path contexts dir : Util.checked) with
| In_build_dir (context, dir) -> [ context ], dir
| In_build_dir (context, dir) ->
( Dune_engine.Context_name.Map.find_exn scontexts (Context.name context)
, [ context ]
, dir )
| In_source_dir dir ->
(* We need to adjust the path here to make up for the current working directory. *)
let dir =
Path.Source.L.relative Path.Source.root (to_cwd @ Path.Source.explode dir)
in
contexts, dir
( Dune_engine.Context_name.Map.find_exn scontexts Context_name.default
, contexts
, dir )
| In_private_context _ | In_install_dir _ ->
User_error.raise
[ Pp.textf "This path is internal to dune: %s" (Path.to_string_maybe_quoted dir)
Expand All @@ -113,7 +199,7 @@ let make_request ~contexts ~to_cwd ~test_paths =
]
in
let open Action_builder.O in
Action_builder.of_memo (disambiguate_test_name src_dir)
Action_builder.of_memo (disambiguate_test_name ~sctx src_dir)
>>| Test_kind.alias ~contexts
>>= Alias.request)
|> Action_builder.all_unit
Expand Down
1 change: 1 addition & 0 deletions bin/runtest_common.mli
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ open Import

val make_request
: contexts:Context.t list
-> scontexts:Super_context.t Context_name.Map.t
-> to_cwd:string list
-> test_paths:string list
-> unit Action_builder.t
70 changes: 52 additions & 18 deletions src/dune_rules/ml_sources.ml
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,27 @@ module Origin = struct
type t =
| Library of Library.t
| Executables of Executables.t
| Tests of Tests.t
| Melange of Melange_stanzas.Emit.t

let loc = function
| Library l -> l.buildable.loc
| Executables e -> e.buildable.loc
| Tests t -> t.exes.buildable.loc
| Melange mel -> mel.loc
;;

let preprocess = function
| Library l -> l.buildable.preprocess
| Executables e -> e.buildable.preprocess
| Tests t -> t.exes.buildable.preprocess
| Melange mel -> mel.preprocess
;;

let to_dyn = function
| Library _ -> Dyn.variant "Library" [ Dyn.Opaque ]
| Executables _ -> Dyn.variant "Executables" [ Dyn.Opaque ]
| Tests _ -> Dyn.variant "Tests" [ Dyn.Opaque ]
| Melange _ -> Dyn.variant "Melange" [ Dyn.Opaque ]
;;
end
Expand Down Expand Up @@ -58,10 +62,11 @@ module Per_stanza = struct
type groups =
{ libraries : Library.t group_part list
; executables : Executables.t group_part list
; tests : Tests.t group_part list
; melange_emits : Melange_stanzas.Emit.t group_part list
}

let make { libraries = libs; executables = exes; melange_emits = emits } =
let make { libraries = libs; executables = exes; tests; melange_emits = emits } =
let libraries, libraries_by_obj_dir =
List.fold_left
libs
Expand All @@ -84,17 +89,29 @@ module Per_stanza = struct
by_id, by_obj_dir)
in
let executables =
match
String.Map.of_list_map exes ~f:(fun (part : Executables.t group_part) ->
let first_exe = snd (Nonempty_list.hd part.stanza.names) in
let origin : Origin.t = Executables part.stanza in
first_exe, (origin, part.modules, part.obj_dir))
with
| Ok x -> x
| Error (name, _, part) ->
let entries =
List.concat
[ List.map exes ~f:(fun (part : Executables.t group_part) ->
let first_exe = snd (Nonempty_list.hd part.stanza.names) in
let origin : Origin.t = Executables part.stanza in
first_exe, (origin, part.modules, part.obj_dir, part.stanza.buildable.loc))
; List.map tests ~f:(fun (part : Tests.t group_part) ->
let first_exe = snd (Nonempty_list.hd part.stanza.exes.names) in
let origin : Origin.t = Tests part.stanza in
( first_exe
, (origin, part.modules, part.obj_dir, part.stanza.exes.buildable.loc) ))
]
in
match String.Map.of_list entries with
| Ok map ->
String.Map.map map ~f:(fun (origin, modules, obj_dir, _loc) ->
origin, modules, obj_dir)
| Error (name, (_, _, _, loc1), (_, _, _, loc2)) ->
User_error.raise
~loc:part.stanza.buildable.loc
[ Pp.textf "Executable %S appears for the second time in this directory" name ]
~loc:loc1
[ Pp.textf "Executable %S appears for the second time in this directory" name
; Pp.textf "Already defined at %s" (Loc.to_file_colon_line loc2)
]
in
let melange_emits =
match
Expand All @@ -118,6 +135,8 @@ module Per_stanza = struct
by_path (Library part.stanza, part.dir) part.sources)
; List.rev_concat_map exes ~f:(fun part ->
by_path (Executables part.stanza, part.dir) part.sources)
; List.rev_concat_map tests ~f:(fun part ->
by_path (Tests part.stanza, part.dir) part.sources)
; List.rev_concat_map emits ~f:(fun part ->
by_path (Melange part.stanza, part.dir) part.sources)
]
Expand Down Expand Up @@ -252,7 +271,7 @@ let find_origin (t : t) ~libs path =
| Some origins ->
Memo.List.filter_map origins ~f:(fun (origin, dir) ->
match origin with
| Executables _ | Melange _ -> Memo.return (Some origin)
| Executables _ | Tests _ | Melange _ -> Memo.return (Some origin)
| Library lib ->
let src_dir = Path.drop_optional_build_context_src_exn (Path.build dir) in
Lib.DB.available_by_lib_id libs (Local (Library.to_lib_id ~src_dir lib))
Expand Down Expand Up @@ -461,14 +480,18 @@ let modules_of_stanzas =
| `Skip -> loop l acc
| `Library y -> loop l { acc with libraries = y :: acc.libraries }
| `Executables y -> loop l { acc with executables = y :: acc.executables }
| `Tests y -> loop l { acc with tests = y :: acc.tests }
| `Melange_emit y -> loop l { acc with melange_emits = y :: acc.melange_emits })
in
fun l -> loop l { libraries = []; executables = []; melange_emits = [] }
fun l -> loop l { libraries = []; executables = []; tests = []; melange_emits = [] }
in
fun l ->
let { Per_stanza.libraries; executables; melange_emits } = rev_filter_partition l in
let { Per_stanza.libraries; executables; tests; melange_emits } =
rev_filter_partition l
in
{ Per_stanza.libraries = List.rev libraries
; executables = List.rev executables
; tests = List.rev tests
; melange_emits = List.rev melange_emits
}
in
Expand Down Expand Up @@ -496,6 +519,12 @@ let modules_of_stanzas =
in
`Executables { Per_stanza.stanza = exes; sources; modules; obj_dir; dir }
in
let make_tests ~dir ~expander ~modules ~project tests =
let+ result = make_executables ~dir ~expander ~modules ~project tests.Tests.exes in
match result with
| `Executables group_part -> `Tests { group_part with stanza = tests }
| _ -> assert false
in
fun stanzas ~expander ~project ~dir ~libs ~lookup_vlib ~modules ~include_subdirs ->
Memo.parallel_map stanzas ~f:(fun stanza ->
let enabled_if =
Expand Down Expand Up @@ -532,7 +561,7 @@ let modules_of_stanzas =
let obj_dir = Library.obj_dir lib ~dir in
`Library { Per_stanza.stanza = lib; sources; modules; dir; obj_dir }
| Executables.T exes -> make_executables ~dir ~expander ~modules ~project exes
| Tests.T { exes; _ } -> make_executables ~dir ~expander ~modules ~project exes
| Tests.T tests -> make_tests ~dir ~expander ~modules ~project tests
| Melange_stanzas.Emit.T mel ->
let obj_dir = Obj_dir.make_melange_emit ~dir ~name:mel.target in
let+ sources, modules =
Expand Down Expand Up @@ -652,9 +681,14 @@ let make
part.stanza, part.modules, part.obj_dir)
in
let exes =
List.map
modules_of_stanzas.executables
~f:(fun (part : _ Per_stanza.group_part) -> part.modules, part.obj_dir)
List.concat
[ List.map
modules_of_stanzas.executables
~f:(fun { Per_stanza.modules; obj_dir; _ } -> modules, obj_dir)
; List.map
modules_of_stanzas.tests
~f:(fun { Per_stanza.modules; obj_dir; _ } -> modules, obj_dir)
]
in
Artifacts_obj.make
~dir
Expand Down
1 change: 1 addition & 0 deletions src/dune_rules/ml_sources.mli
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ module Origin : sig
type t =
| Library of Library.t
| Executables of Executables.t
| Tests of Tests.t
| Melange of Melange_stanzas.Emit.t

val preprocess : t -> Preprocess.With_instrumentation.t Preprocess.Per_module.t
Expand Down
Loading
Loading