File: test_build_logic.ml

package info (click to toggle)
ocaml-obuild 0.2.2-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 1,456 kB
  • sloc: ml: 14,491; sh: 211; ansic: 34; makefile: 11
file content (353 lines) | stat: -rw-r--r-- 14,027 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
open Test_framework
open Test_build_helpers

(* Pipe operator for OCaml < 4.01 compatibility *)
let (|>) x f = f x

(** Test 1: MLI change triggers CMI and ML rebuild *)
let test_mli_triggers_rebuild () =
  with_temp_build_project
    ~name:"mli_rebuild"
    ~files:[
      ("src/foo.mli", "val x : int\n");
      ("src/foo.ml", "let x = 42\n");
      ("src/bar.ml", "let y = Foo.x + 1\n");
    ]
    ~obuild_content:"name: mli-test\nversion: 1.0\nobuild-ver: 1\n\nlibrary foo\n  modules: Foo, Bar\n  src-dir: src\n"
    ~test_fn:(fun dir ->
      (* Initial build *)
      let (success, output) = run_obuild_command ~project_dir:dir ~command:"configure" ~args:[] in
      if not success then
        failwith ("Configure failed: " ^ output);

      let (success, output) = run_obuild_command ~project_dir:dir ~command:"build" ~args:[] in
      if not success then
        failwith ("Initial build failed: " ^ output);

      (* Get initial mtimes *)
      let foo_cmi = dir ^ "/dist/build/lib-foo/foo.cmi" in
      let foo_cmo = dir ^ "/dist/build/lib-foo/foo.cmo" in
      let bar_cmo = dir ^ "/dist/build/lib-foo/bar.cmo" in

      assert_file_exists foo_cmi;
      assert_file_exists foo_cmo;
      assert_file_exists bar_cmo;

      let cmi_mtime1 = get_mtime foo_cmi in
      let foo_mtime1 = get_mtime foo_cmo in
      let bar_mtime1 = get_mtime bar_cmo in

      (* Sleep to ensure different mtime *)
      short_sleep ();

      (* Modify .mli file content (add comment) *)
      write_file_with_dirs (dir ^ "/src/foo.mli") "(* changed *)\nval x : int\n";

      (* Rebuild *)
      let (success, output) = run_obuild_command ~project_dir:dir ~command:"build" ~args:[] in
      if not success then
        failwith ("Rebuild after mli change failed: " ^ output);

      (* Verify .cmi was rebuilt *)
      let cmi_mtime2 = get_mtime foo_cmi in
      assert_mtime_newer ~msg:"foo.cmi should be rebuilt when foo.mli changes" cmi_mtime1 cmi_mtime2;

      (* Verify .ml was recompiled (depends on own .cmi) *)
      let foo_mtime2 = get_mtime foo_cmo in
      assert_mtime_newer ~msg:"foo.ml should be recompiled when foo.mli changes" foo_mtime1 foo_mtime2;

      (* Verify dependent module was recompiled *)
      let bar_mtime2 = get_mtime bar_cmo in
      assert_mtime_newer ~msg:"bar.ml should be recompiled when dependency interface changes" bar_mtime1 bar_mtime2;
    )

(** Test 2: ML change only rebuilds that module (not dependencies) *)
(** Note: Tests bytecode mode - native code rebuilds are expected due to .cmx inlining *)
let test_ml_incremental_rebuild () =
  with_temp_build_project
    ~name:"ml_incremental"
    ~files:[
      ("src/foo.mli", "val x : int\n");  (* Explicit interface prevents .cmi regeneration *)
      ("src/foo.ml", "let x = 42\n");
      ("src/bar.ml", "let y = Foo.x + 1\n");
      ("src/baz.ml", "let z = Bar.y * 2\n");
    ]
    ~obuild_content:"name: incremental-test\nversion: 1.0\nobuild-ver: 1\n\nlibrary test\n  modules: Foo, Bar, Baz\n  src-dir: src\n"
    ~test_fn:(fun dir ->
      (* Configure for bytecode only to test incremental compilation *)
      let (success, _) = run_obuild_command ~project_dir:dir ~command:"configure"
        ~args:["--library-bytecode=true"; "--library-native=false"] in
      if not success then failwith "configure should succeed";

      let (success, _) = run_obuild_command ~project_dir:dir ~command:"build" ~args:[] in
      if not success then failwith "initial build should succeed";

      (* Get initial mtimes - check bytecode (.cmo) files *)
      let foo_cmo = dir ^ "/dist/build/lib-test/foo.cmo" in
      let bar_cmo = dir ^ "/dist/build/lib-test/bar.cmo" in
      let baz_cmo = dir ^ "/dist/build/lib-test/baz.cmo" in

      let foo_mtime1 = get_mtime foo_cmo in
      let bar_mtime1 = get_mtime bar_cmo in
      let baz_mtime1 = get_mtime baz_cmo in

      short_sleep ();

      (* Modify only foo.ml (no interface change) *)
      write_file_with_dirs (dir ^ "/src/foo.ml") "let x = 99\n";

      (* Rebuild *)
      let (success, _) = run_obuild_command ~project_dir:dir ~command:"build" ~args:[] in
      if not success then failwith "rebuild should succeed";

      (* Verify only foo.ml was rebuilt *)
      let foo_mtime2 = get_mtime foo_cmo in
      assert_mtime_newer ~msg:"foo.ml should be rebuilt" foo_mtime1 foo_mtime2;

      (* In bytecode mode, bar and baz should NOT rebuild (no interface change) *)
      let bar_mtime2 = get_mtime bar_cmo in
      let baz_mtime2 = get_mtime baz_cmo in

      assert_mtime_unchanged ~msg:"bar.ml should NOT rebuild in bytecode (foo interface unchanged)" bar_mtime1 bar_mtime2;
      assert_mtime_unchanged ~msg:"baz.ml should NOT rebuild in bytecode (foo interface unchanged)" baz_mtime1 baz_mtime2;
    )

(** Test 3: C file change triggers recompilation and relinking *)
let test_c_file_rebuild () =
  with_temp_build_project
    ~name:"c_rebuild"
    ~files:[
      ("src/cbits.c", "int add(int a, int b) { return a + b; }\n");
      ("src/cbits.h", "int add(int a, int b);\n");
      ("src/main.ml", "external add : int -> int -> int = \"add\"\nlet () = Printf.printf \"%d\\n\" (add 1 2)\n");
    ]
    ~obuild_content:"name: c-test\nversion: 1.0\nobuild-ver: 1\n\nexecutable ctest\n  main-is: main.ml\n  src-dir: src\n  c-sources: cbits.c\n  c-dir: src\n"
    ~test_fn:(fun dir ->
      (* Initial build *)
      let (success, _) = run_obuild_command ~project_dir:dir ~command:"configure" ~args:[] in
      if not success then failwith "configure should succeed";

      let (success, _) = run_obuild_command ~project_dir:dir ~command:"build" ~args:[] in
      if not success then failwith "initial build should succeed";

      (* Get initial mtimes *)
      (* C objects are stored in the target's build directory *)
      let c_obj = dir ^ "/dist/build/ctest/cbits.c.o" in
      let exe = dir ^ "/dist/build/ctest/ctest" in

      assert_file_exists c_obj;
      assert_file_exists exe;

      let obj_mtime1 = get_mtime c_obj in
      let exe_mtime1 = get_mtime exe in

      short_sleep ();

      (* Modify C file *)
      write_file_with_dirs (dir ^ "/src/cbits.c") "int add(int a, int b) { return a + b + 1; }\n";

      (* Rebuild *)
      let (success, _) = run_obuild_command ~project_dir:dir ~command:"build" ~args:[] in
      if not success then failwith "rebuild should succeed";

      (* Verify .o was rebuilt *)
      let obj_mtime2 = get_mtime c_obj in
      assert_mtime_newer ~msg:"C object file should be rebuilt" obj_mtime1 obj_mtime2;

      (* Verify executable was relinked *)
      let exe_mtime2 = get_mtime exe in
      assert_mtime_newer ~msg:"Executable should be relinked" exe_mtime1 exe_mtime2;
    )

(** Test 4: Clean removes all build artifacts *)
let test_clean_build () =
  with_temp_build_project
    ~name:"clean_test"
    ~files:[
      ("src/foo.ml", "let x = 42\n");
    ]
    ~obuild_content:"name: clean-test\nversion: 1.0\nobuild-ver: 1\n\nlibrary foo\n  modules: Foo\n  src-dir: src\n"
    ~test_fn:(fun dir ->
      (* Build *)
      let (success, _) = run_obuild_command ~project_dir:dir ~command:"configure" ~args:[] in
      if not success then failwith "configure should succeed";

      let (success, _) = run_obuild_command ~project_dir:dir ~command:"build" ~args:[] in
      if not success then failwith "build should succeed";

      (* Verify artifacts exist *)
      let dist_dir = dir ^ "/dist" in
      assert_file_exists dist_dir;
      assert_file_exists (dir ^ "/dist/build/lib-foo/foo.cmi");

      (* Clean *)
      let (success, _) = run_obuild_command ~project_dir:dir ~command:"clean" ~args:[] in
      if not success then failwith "clean should succeed";

      (* Verify dist directory is cleaned *)
      (* Note: dist/ itself may still exist but should be empty or only contain setup *)
      let artifacts = [
        dir ^ "/dist/build/lib-foo/foo.cmi";
        dir ^ "/dist/build/lib-foo/foo.cmo";
        dir ^ "/dist/build/lib-foo/foo.cmx";
      ] in

      List.iter (fun artifact ->
        if Sys.file_exists artifact then
          failwith (Printf.sprintf "Artifact should be removed by clean: %s" artifact)
      ) artifacts;
    )

(** Test 5: Configure change triggers rebuild *)
let test_configure_rebuild () =
  with_temp_build_project
    ~name:"config_rebuild"
    ~files:[
      ("src/foo.ml", "let x = 42\n");
    ]
    ~obuild_content:"name: config-test\nversion: 1.0\nobuild-ver: 1\n\nlibrary foo\n  modules: Foo\n  src-dir: src\n"
    ~test_fn:(fun dir ->
      (* Initial build *)
      let (success, _) = run_obuild_command ~project_dir:dir ~command:"configure" ~args:[] in
      if not success then failwith "configure should succeed";

      let (success, _) = run_obuild_command ~project_dir:dir ~command:"build" ~args:[] in
      if not success then failwith "build should succeed";

      let foo_cmo = dir ^ "/dist/build/lib-foo/foo.cmo" in
      let mtime1 = get_mtime foo_cmo in

      short_sleep ();

      (* Reconfigure with different options *)
      let (success, _) = run_obuild_command ~project_dir:dir ~command:"configure"
        ~args:["--library-debugging=true"] in
      if not success then failwith "reconfigure should succeed";

      (* Build again *)
      let (success, _) = run_obuild_command ~project_dir:dir ~command:"build" ~args:[] in
      if not success then failwith "rebuild should succeed";

      (* Verify module was rebuilt with new flags *)
      let mtime2 = get_mtime foo_cmo in
      assert_mtime_newer ~msg:"Module should rebuild after configure change" mtime1 mtime2;
    )

(** Test 6: Parallel build with dependencies *)
let test_parallel_build () =
  with_temp_build_project
    ~name:"parallel_build"
    ~files:[
      ("src/a.ml", "let a = 1\n");
      ("src/b.ml", "let b = 2\n");
      ("src/c.ml", "let c = A.a + B.b\n");
    ]
    ~obuild_content:"name: parallel-test\nversion: 1.0\nobuild-ver: 1\n\nlibrary test\n  modules: A, B, C\n  src-dir: src\n"
    ~test_fn:(fun dir ->
      (* Build with parallelism *)
      let (success, _) = run_obuild_command ~project_dir:dir ~command:"configure" ~args:[] in
      if not success then failwith "configure should succeed";

      let (success, output) = run_obuild_command ~project_dir:dir ~command:"build" ~args:["-j"; "2"] in
      if not success then
        failwith ("Parallel build failed: " ^ output);

      (* Verify all artifacts exist (build completed successfully) *)
      assert_file_exists (dir ^ "/dist/build/lib-test/a.cmo");
      assert_file_exists (dir ^ "/dist/build/lib-test/b.cmo");
      assert_file_exists (dir ^ "/dist/build/lib-test/c.cmo");

      (* If parallel build succeeded, dependencies were respected *)
      (* (C couldn't build before A and B) *)
    )

(** Test 7: Source file overlap between libraries emits a warning *)
(** Regression test: reproduces the obuild self-build bug on macOS/OCaml 5.x
 *  where library obuild had 'modules: lib' (directory module) causing it to
 *  claim the same .ml files as library obuild_base (flat), leading to
 *  conflicting -for-pack flags and a hard error in OCaml 5.x. *)
let test_source_overlap_warning () =
  with_temp_build_project
    ~name:"overlap_warning"
    ~files:[
      ("helper/utils.ml", "let x = 42\n");
    ]
    ~obuild_content:"name: overlap-warning\nversion: 1.0\nobuild-ver: 1\n\nlibrary base_lib\n  modules: Utils\n  src-dir: helper\n\nlibrary top_lib\n  modules: helper\n"
    ~test_fn:(fun dir ->
      let (success, _) = run_obuild_command ~project_dir:dir ~command:"configure" ~args:[] in
      if not success then failwith "configure should succeed";

      (* Build should still succeed: the overlap is a warning, not an error *)
      let (success, output) = run_obuild_command ~project_dir:dir ~command:"build" ~args:[] in
      if not success then
        failwith ("Build should succeed despite overlap warning: " ^ output);

      (* Verify the source overlap warning was emitted *)
      let string_contains s sub =
        let n = String.length sub in
        let m = String.length s in
        if n = 0 then true
        else if m < n then false
        else begin
          let found = ref false in
          for i = 0 to m - n do
            if String.sub s i n = sub then found := true
          done;
          !found
        end
      in
      if not (string_contains output "warning: source file") then
        failwith ("Expected source overlap warning in build output, but got:\n" ^ output);
    )

(** Run all build logic tests *)
let () =
  print_endline "";
  print_endline "Build Logic Tests";
  print_endline "=================";
  print_endline "";

  let tests = [
    ("mli_triggers_rebuild", test_mli_triggers_rebuild,
     "MLI change triggers CMI and dependent ML rebuilds");
    ("ml_incremental_rebuild", test_ml_incremental_rebuild,
     "ML change only rebuilds that module (incremental)");
    ("c_file_rebuild", test_c_file_rebuild,
     "C file change triggers recompilation and relinking");
    ("clean_build", test_clean_build,
     "Clean removes all build artifacts");
    ("configure_rebuild", test_configure_rebuild,
     "Configure change triggers rebuild");
    ("parallel_build", test_parallel_build,
     "Parallel build respects dependencies");
    ("source_overlap_warning", test_source_overlap_warning,
     "Source file overlap between libraries emits warning");
  ] in

  let run_test (name, test_fn, description) =
    Printf.printf "Running test: %s... " description;
    flush stdout;
    try
      test_fn ();
      print_endline "PASS";
      true
    with
    | Failure msg ->
        Printf.printf "FAIL\n  %s\n" msg;
        false
    | e ->
        Printf.printf "ERROR\n  %s\n" (Printexc.to_string e);
        false
  in

  let results = List.map run_test tests in
  let passed = List.filter (fun x -> x) results |> List.length in
  let total = List.length tests in

  print_endline "";
  Printf.printf "Results: %d/%d tests passed\n" passed total;

  if passed = total then
    exit 0
  else
    exit 1