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
|