Skip to content

Commit 5f1f135

Browse files
committed
feat(runtest): dune runtest for (tests)
Signed-off-by: Ali Caglayan <[email protected]>
3 parents 1a4af53 + 8097cb7 + 31de016 commit 5f1f135

File tree

18 files changed

+625
-258
lines changed

18 files changed

+625
-258
lines changed

.github/workflows/docker-image.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ jobs:
3737

3838
- name: Extract metadata (tags, labels) for Docker
3939
id: meta
40-
uses: docker/metadata-action@v5.8.0
40+
uses: docker/metadata-action@v5.9.0
4141
with:
4242
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
4343

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
name: Multi-Repo Build
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
repos:
7+
description: 'Space-separated list of GitHub repos (e.g., "ocaml-dune/notty ocaml/ocaml-re")'
8+
required: true
9+
type: string
10+
depext_linux:
11+
description: 'Space-separated apt packages for Linux (optional)'
12+
required: false
13+
type: string
14+
default: ''
15+
depext_macos:
16+
description: 'Space-separated brew packages for macOS (optional)'
17+
required: false
18+
type: string
19+
default: ''
20+
21+
env:
22+
EXTRA_NIX_CONFIG: |
23+
extra-substituters = https://anmonteiro.nix-cache.workers.dev
24+
extra-trusted-public-keys = ocaml.nix-cache.com-1:/xI2h2+56rwFfKyyFVbkJSeGqSIYMC/Je+7XXqGKDIY=
25+
26+
jobs:
27+
build:
28+
name: Build repos with Dune
29+
strategy:
30+
fail-fast: false
31+
matrix:
32+
os:
33+
- ubuntu-latest
34+
- macos-latest
35+
runs-on: ${{ matrix.os }}
36+
env:
37+
# TODO: Remove
38+
DUNE_CONFIG__PKG_BUILD_PROGRESS: enabled
39+
steps:
40+
- name: Checkout Dune
41+
uses: actions/checkout@v5
42+
43+
- name: Set up Nix
44+
uses: nixbuild/nix-quick-install-action@v34
45+
with:
46+
nix_conf: ${{ env.EXTRA_NIX_CONFIG }}
47+
48+
- name: Cache Nix store
49+
uses: nix-community/cache-nix-action@v6
50+
with:
51+
primary-key: nix-${{ runner.os }}-nix-build-${{ hashFiles('**/*.nix', '**/flake.lock') }}
52+
restore-prefixes-first-match: nix-${{ runner.os }}-nix-build-
53+
save: false
54+
55+
- name: Build Dune
56+
run: nix build
57+
58+
- name: Set up workspace directory
59+
run: mkdir workspace
60+
61+
- name: Clone repositories
62+
run: |
63+
cd workspace
64+
for repo in ${{ inputs.repos }}; do
65+
echo "Cloning $repo..."
66+
repo_name=$(echo $repo | sed 's/.*\///')
67+
git clone https://github.com/$repo.git $repo_name
68+
done
69+
70+
- name: Generate dune-workspace
71+
run: |
72+
cd workspace
73+
cat > dune-workspace <<EOF
74+
(lang dune 3.21)
75+
(pkg enabled)
76+
EOF
77+
78+
echo "Generated dune-workspace:"
79+
cat dune-workspace
80+
81+
- name: Install system dependencies (Linux)
82+
if: runner.os == 'Linux' && inputs.depext_linux != ''
83+
run: |
84+
sudo apt-get update
85+
sudo apt-get install -y ${{ inputs.depext_linux }}
86+
87+
- name: Install system dependencies (macOS)
88+
if: runner.os == 'macOS' && inputs.depext_macos != ''
89+
run: |
90+
brew install ${{ inputs.depext_macos }}
91+
92+
- name: Lock dependencies
93+
run: cd workspace && nix run .. -- pkg lock
94+
95+
- name: Build
96+
run: cd workspace && nix run .. -- build

bin/build.ml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,8 +98,12 @@ let poll_handling_rpc_build_requests ~(common : Common.t) ~config =
9898
match kind with
9999
| Build targets ->
100100
Target.interpret_targets (Common.root common) config setup targets
101-
| Runtest dir_or_cram_test_paths ->
102-
Runtest_common.make_request ~dir_or_cram_test_paths ~to_cwd:root.to_cwd setup
101+
| Runtest test_paths ->
102+
Runtest_common.make_request
103+
~contexts:setup.contexts
104+
~scontexts:setup.scontexts
105+
~to_cwd:root.to_cwd
106+
~test_paths
103107
in
104108
run_build_system ~common ~request, outcome)
105109
;;

bin/import.ml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ include struct
3939
module Library = Library
4040
module Melange = Melange
4141
module Executables = Executables
42+
module Dune_load = Dune_load
43+
module Dir_contents = Dir_contents
4244
end
4345

4446
include struct

bin/runtest.ml

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -36,17 +36,16 @@ let runtest_info =
3636
let runtest_term =
3737
let name = Arg.info [] ~docv:"TEST" in
3838
let+ builder = Common.Builder.term
39-
and+ dir_or_cram_test_paths = Arg.(value & pos_all string [ "." ] name) in
39+
and+ test_paths = Arg.(value & pos_all string [ "." ] name) in
4040
let common, config = Common.init builder in
4141
match Dune_util.Global_lock.lock ~timeout:None with
4242
| Ok () ->
43-
Build.run_build_command
44-
~common
45-
~config
46-
~request:
47-
(Runtest_common.make_request
48-
~dir_or_cram_test_paths
49-
~to_cwd:(Common.root common).to_cwd)
43+
Build.run_build_command ~common ~config ~request:(fun setup ->
44+
Runtest_common.make_request
45+
~contexts:setup.contexts
46+
~scontexts:setup.scontexts
47+
~to_cwd:(Common.root common).to_cwd
48+
~test_paths)
5049
| Error lock_held_by ->
5150
Scheduler.go_without_rpc_server ~common ~config (fun () ->
5251
let open Fiber.O in
@@ -56,7 +55,7 @@ let runtest_term =
5655
~lock_held_by
5756
builder
5857
Dune_rpc.Procedures.Public.runtest
59-
dir_or_cram_test_paths
58+
test_paths
6059
>>| Rpc.Rpc_common.wrap_build_outcome_exn ~print_on_success:true)
6160
;;
6261

bin/runtest_common.ml

Lines changed: 105 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,25 @@
11
open Import
22

3+
module Test_kind = struct
4+
type t =
5+
| Runtest of Path.t
6+
| Cram of Path.t * Source.Cram_test.t
7+
| Test_executable of Path.t * string (* dir, executable name *)
8+
9+
let alias ~contexts = function
10+
| Cram (dir, cram) ->
11+
let name = Dune_engine.Alias.Name.of_string (Source.Cram_test.name cram) in
12+
Alias.in_dir ~name ~recursive:false ~contexts dir
13+
| Test_executable (dir, exe_name) ->
14+
(* CR-someday Alizter: get the proper alias, also check js_of_ocaml
15+
runtst aliases? *)
16+
let name = Dune_engine.Alias.Name.of_string ("runtest-" ^ exe_name) in
17+
Alias.in_dir ~name ~recursive:false ~contexts dir
18+
| Runtest dir ->
19+
Alias.in_dir ~name:Dune_rules.Alias.runtest ~recursive:true ~contexts dir
20+
;;
21+
end
22+
323
let cram_tests_of_dir parent_dir =
424
let open Memo.O in
525
Source_tree.find_dir parent_dir
@@ -20,13 +40,54 @@ let find_cram_test cram_tests path =
2040
| Error (Dune_rules.Cram_rules.Missing_run_t _) | Ok _ -> None)
2141
;;
2242

23-
let all_tests_of_dir parent_dir =
43+
let find_test_executable ~sctx ~dir ~ml_file =
44+
let open Memo.O in
45+
let module_name = Filename.remove_extension ml_file in
46+
match Dune_lang.Module_name.of_string_opt module_name with
47+
| None -> Memo.return `Not_a_test
48+
| Some module_name ->
49+
let build_dir =
50+
Path.Build.append_source (Super_context.context sctx |> Context.build_dir) dir
51+
in
52+
let* dir_contents = Dir_contents.get sctx ~dir:build_dir in
53+
let* ml_sources = Dir_contents.ocaml dir_contents
54+
and* scope = Dir_contents.dir dir_contents |> Dune_rules.Scope.DB.find_by_dir in
55+
Dune_rules.Ml_sources.find_origin
56+
ml_sources
57+
~libs:(Dune_rules.Scope.libs scope)
58+
[ module_name ]
59+
>>| (function
60+
| Some (Library _ | Executables _ | Melange _) | None -> `Not_a_test
61+
| Some (Tests ({ exes; _ } as _test)) ->
62+
let exe_names = Nonempty_list.to_list exes.names |> List.map ~f:snd in
63+
if List.mem exe_names (Filename.remove_extension ml_file) ~equal:String.equal
64+
then `Runnable (Filename.remove_extension ml_file)
65+
else (
66+
match exe_names with
67+
| [ single_exe ] -> `Runnable single_exe
68+
| [] | _ :: _ -> `Multiple_executables))
69+
;;
70+
71+
let all_tests_of_dir ~sctx parent_dir =
2472
let open Memo.O in
2573
let+ cram_candidates =
2674
cram_tests_of_dir parent_dir
2775
>>| List.filter_map ~f:(fun res ->
2876
Result.to_option res
2977
|> Option.map ~f:(fun test -> Source.Cram_test.path test |> Path.Source.to_string))
78+
and+ test_executable_candidates =
79+
Source_tree.find_dir parent_dir
80+
>>= function
81+
| None -> Memo.return []
82+
| Some source_dir ->
83+
Source_tree.Dir.filenames source_dir
84+
|> Filename.Set.to_list
85+
|> List.filter ~f:(fun f -> String.is_suffix f ~suffix:".ml")
86+
|> Memo.List.filter ~f:(fun ml_file ->
87+
find_test_executable ~sctx ~dir:parent_dir ~ml_file
88+
>>| function
89+
| `Runnable _ -> true
90+
| `Multiple_executables | `Not_a_test -> false)
3091
and+ dir_candidates =
3192
let* parent_source_dir = Source_tree.find_dir parent_dir in
3293
match parent_source_dir with
@@ -39,59 +100,75 @@ let all_tests_of_dir parent_dir =
39100
>>| Source_tree.Dir.path
40101
>>| Path.Source.to_string)
41102
in
42-
List.concat [ cram_candidates; dir_candidates ]
103+
List.concat [ cram_candidates; test_executable_candidates; dir_candidates ]
43104
|> String.Set.of_list
44105
|> String.Set.to_list
45106
;;
46107

47-
let explain_unsuccessful_search path ~parent_dir =
108+
let explain_unsuccessful_search ~sctx path ~parent_dir =
48109
let open Memo.O in
49-
let+ candidates = all_tests_of_dir parent_dir in
110+
let+ candidates = all_tests_of_dir ~sctx parent_dir in
50111
User_error.raise
51112
~hints:(User_message.did_you_mean (Path.Source.to_string path) ~candidates)
52113
[ Pp.textf "%S does not match any known test." (Path.Source.to_string path) ]
53114
;;
54115

55-
(* [disambiguate_test_name path] is a function that takes in a
56-
directory [path] and classifies it as either a cram test or a directory to
116+
(* [disambiguate_test_name path] is a function that takes in a directory [path]
117+
and classifies it as either a cram test, test executable, or a directory to
57118
run tests in. *)
58-
let disambiguate_test_name path =
119+
let disambiguate_test_name ~sctx path =
59120
match Path.Source.parent path with
60-
| None -> Memo.return @@ `Runtest (Path.source Path.Source.root)
121+
| None -> Memo.return @@ Test_kind.Runtest (Path.source Path.Source.root)
61122
| Some parent_dir ->
62123
let open Memo.O in
63124
let* cram_tests = cram_tests_of_dir parent_dir in
64125
(match find_cram_test cram_tests path with
65126
| Some test ->
66127
(* If we find the cram test, then we request that is run. *)
67-
Memo.return (`Cram (parent_dir, test))
128+
Memo.return (Test_kind.Cram (Path.source parent_dir, test))
68129
| None ->
69-
(* If we don't find it, then we assume the user intended a directory for
70-
@runtest to be used. *)
71-
Source_tree.find_dir path
72-
>>= (function
73-
(* We need to make sure that this directory or file exists. *)
74-
| Some _ -> Memo.return (`Runtest (Path.source path))
75-
| None -> explain_unsuccessful_search path ~parent_dir))
130+
(* Check for test executables *)
131+
let filename = Path.Source.basename path in
132+
let* test_exe_opt =
133+
find_test_executable ~sctx ~dir:parent_dir ~ml_file:filename
134+
>>| function
135+
| `Runnable exe_name -> Some exe_name
136+
| `Multiple_executables ->
137+
User_error.raise
138+
[ Pp.text "Running multiple test executables at once is not yet supported" ]
139+
| `Not_a_test -> None
140+
in
141+
(match test_exe_opt with
142+
| Some exe_name ->
143+
(* Found a test executable for this ML file *)
144+
Memo.return (Test_kind.Test_executable (Path.source parent_dir, exe_name))
145+
| None ->
146+
(* If we don't find it, then we assume the user intended a directory for
147+
@runtest to be used. *)
148+
Source_tree.find_dir path
149+
>>= (function
150+
(* We need to make sure that this directory or file exists. *)
151+
| Some _ -> Memo.return (Test_kind.Runtest (Path.source path))
152+
| None -> explain_unsuccessful_search ~sctx path ~parent_dir)))
76153
;;
77154

78-
let make_request ~dir_or_cram_test_paths ~to_cwd (setup : Import.Main.build_system) =
79-
let contexts = setup.contexts in
80-
List.map dir_or_cram_test_paths ~f:(fun dir ->
155+
let make_request ~contexts ~scontexts ~to_cwd ~test_paths =
156+
List.map test_paths ~f:(fun dir ->
81157
let dir = Path.of_string dir |> Path.Expert.try_localize_external in
82-
let open Action_builder.O in
83-
let* contexts, alias_kind =
158+
let sctx, contexts, src_dir =
84159
match (Util.check_path contexts dir : Util.checked) with
85160
| In_build_dir (context, dir) ->
86-
let+ res = Action_builder.of_memo (disambiguate_test_name dir) in
87-
[ context ], res
161+
( Dune_engine.Context_name.Map.find_exn scontexts (Context.name context)
162+
, [ context ]
163+
, dir )
88164
| In_source_dir dir ->
89165
(* We need to adjust the path here to make up for the current working directory. *)
90166
let dir =
91167
Path.Source.L.relative Path.Source.root (to_cwd @ Path.Source.explode dir)
92168
in
93-
let+ res = Action_builder.of_memo (disambiguate_test_name dir) in
94-
contexts, res
169+
( Dune_engine.Context_name.Map.find_exn scontexts Context_name.default
170+
, contexts
171+
, dir )
95172
| In_private_context _ | In_install_dir _ ->
96173
User_error.raise
97174
[ Pp.textf "This path is internal to dune: %s" (Path.to_string_maybe_quoted dir)
@@ -103,17 +180,8 @@ let make_request ~dir_or_cram_test_paths ~to_cwd (setup : Import.Main.build_syst
103180
(Path.to_string_maybe_quoted dir)
104181
]
105182
in
106-
Alias.request
107-
@@
108-
match alias_kind with
109-
| `Cram (dir, cram) ->
110-
let alias_name = Source.Cram_test.name cram in
111-
Alias.in_dir
112-
~name:(Dune_engine.Alias.Name.of_string alias_name)
113-
~recursive:false
114-
~contexts
115-
(Path.source dir)
116-
| `Runtest dir ->
117-
Alias.in_dir ~name:Dune_rules.Alias.runtest ~recursive:true ~contexts dir)
183+
let open Action_builder.O in
184+
let* res = Action_builder.of_memo (disambiguate_test_name ~sctx src_dir) in
185+
Alias.request (Test_kind.alias ~contexts res))
118186
|> Action_builder.all_unit
119187
;;

bin/runtest_common.mli

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
open Import
22

3-
(** [make_request ~dir_or_cram_test_paths ~to_cwd] returns a function suitable
4-
for passing to [Build_cmd.run_build_system] which runs the tests referred
5-
to by the elements of [dir_or_cram_test_paths]. *)
63
val make_request
7-
: dir_or_cram_test_paths:string list
4+
: contexts:Context.t list
5+
-> scontexts:Super_context.t Context_name.Map.t
86
-> to_cwd:string list
9-
-> Dune_rules.Main.build_system
7+
-> test_paths:string list
108
-> unit Action_builder.t

bin/tools/group.ml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,14 @@ module Exec = struct
88
Cmd.group
99
info
1010
(List.map
11-
[ Ocamlformat; Ocamllsp; Ocamlearlybird; Odig; Opam_publish; Dune_release ]
11+
[ Ocamlformat
12+
; Ocamllsp
13+
; Ocamlearlybird
14+
; Odig
15+
; Opam_publish
16+
; Dune_release
17+
; Ocaml_index
18+
]
1219
~f:Tools_common.exec_command)
1320
;;
1421
end

0 commit comments

Comments
 (0)