Persistent store with Git semantics: lazy reads, delayed writes, content-addressing
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Run backend-agnostic store tests on MST, Git, and PDS backends

Extract 9 test scenarios as top-level functions (test_commit_checkout,
test_multi_commit, test_read_commit, test_multi_branch, test_cas,
test_log, test_ancestor, test_merge_base, test_head_none) that take
an Irmin.t and work on any backend.

generic_tests runs them on all three backends:
- store:mst (in-memory MST) — 9/9 pass
- store:git (on-disk Git) — 9/9 pass
- store:pds (ATProto PDS) — 3/9 pass (commit/checkout/log/read_commit/
is_ancestor/merge_base not yet implemented for PDS)

+102 -194
+3
test/test.ml
··· 5 5 Test_tree.suite; 6 6 Test_backend.suite; 7 7 Test_store.suite; 8 + Test_store.mst_suite; 9 + Test_store.git_suite; 10 + Test_store.pds_suite; 8 11 Test_codec.suite; 9 12 Test_link.suite; 10 13 Test_proof.suite;
+89 -193
test/test_store.ml
··· 1 - (* Store tests — ported from upstream irmin src/irmin-test/store.ml 2 - Covers: commit/checkout round-trips, branch management, update_branch CAS, 3 - history traversal, ancestry, merge_base, diff *) 1 + (* Store tests — backend-agnostic test scenarios run on all backends. 2 + Ported from upstream irmin src/irmin-test/store.ml. *) 4 3 5 4 open Irmin.Private 6 5 7 - (* {1 Internal API tests (using Private modules directly)} *) 6 + (* {1 Internal API tests (Private modules, SHA-1 only)} *) 8 7 9 8 let test_store_commit () = 10 9 let backend = Backend.Memory.sha1 () in ··· 59 58 Alcotest.(check bool) "file3 added" true has_add_file3; 60 59 Alcotest.(check bool) "file1 changed" true has_change_file1 61 60 62 - (* {1 Public API tests — commit/checkout round-trips} *) 61 + (* {1 Backend-agnostic tests — each takes an Irmin.t } *) 63 62 64 - let test_commit_checkout_roundtrip () = 65 - let s = Irmin.Mst.memory () in 66 - let tree = Irmin.Tree.empty in 67 - let tree = Irmin.Tree.add tree [ "file.txt" ] "hello" in 63 + let test_commit_checkout s = 64 + let tree = Irmin.Tree.add Irmin.Tree.empty [ "file.txt" ] "hello" in 68 65 let h = Irmin.commit s ~tree ~parents:[] ~message:"init" ~author:"test" in 69 66 Irmin.set_head s ~branch:"main" h; 70 67 match Irmin.checkout s ~branch:"main" with ··· 74 71 "roundtrip" (Some "hello") 75 72 (Irmin.Tree.find tree' [ "file.txt" ]) 76 73 77 - let test_multiple_commits () = 78 - let s = Irmin.Mst.memory () in 79 - let tree = Irmin.Tree.empty in 80 - let tree = Irmin.Tree.add tree [ "a" ] "1" in 81 - let h1 = Irmin.commit s ~tree ~parents:[] ~message:"first" ~author:"test" in 74 + let test_multi_commit s = 75 + let tree = Irmin.Tree.add Irmin.Tree.empty [ "a" ] "1" in 76 + let h1 = Irmin.commit s ~tree ~parents:[] ~message:"first" ~author:"t" in 82 77 Irmin.set_head s ~branch:"main" h1; 83 78 let tree = Irmin.Tree.add tree [ "b" ] "2" in 84 - let h2 = 85 - Irmin.commit s ~tree ~parents:[ h1 ] ~message:"second" ~author:"test" 86 - in 79 + let h2 = Irmin.commit s ~tree ~parents:[ h1 ] ~message:"second" ~author:"t" in 87 80 Irmin.set_head s ~branch:"main" h2; 88 81 match Irmin.checkout s ~branch:"main" with 89 82 | None -> Alcotest.fail "checkout failed" 90 83 | Some tree' -> 91 84 Alcotest.(check (option string)) 92 - "first file" (Some "1") 85 + "first" (Some "1") 93 86 (Irmin.Tree.find tree' [ "a" ]); 94 87 Alcotest.(check (option string)) 95 - "second file" (Some "2") 88 + "second" (Some "2") 96 89 (Irmin.Tree.find tree' [ "b" ]) 97 90 98 - let test_read_commit () = 99 - let s = Irmin.Mst.memory () in 100 - let tree = Irmin.Tree.empty in 101 - let tree = Irmin.Tree.add tree [ "x" ] "y" in 91 + let test_read_commit s = 92 + let tree = Irmin.Tree.add Irmin.Tree.empty [ "x" ] "y" in 102 93 let h = Irmin.commit s ~tree ~parents:[] ~message:"msg" ~author:"alice" in 103 94 match Irmin.read_commit s h with 104 95 | None -> Alcotest.fail "read_commit returned None" 105 96 | Some c -> 106 97 Alcotest.(check string) "author" "alice" c.author; 107 98 Alcotest.(check string) "message" "msg" c.message; 108 - Alcotest.(check int) "no parents" 0 (List.length c.parents); 109 - Alcotest.(check bool) "id matches" true (Irmin.Hash.equal c.id h) 99 + Alcotest.(check bool) "id" true (Irmin.Hash.equal c.id h) 110 100 111 - (* {1 Branch management} *) 112 - 113 - let test_multiple_branches () = 114 - let s = Irmin.Mst.memory () in 115 - let tree = Irmin.Tree.empty in 116 - let tree = Irmin.Tree.add tree [ "a" ] "1" in 117 - let h1 = Irmin.commit s ~tree ~parents:[] ~message:"on main" ~author:"t" in 101 + let test_multi_branch s = 102 + let tree = Irmin.Tree.add Irmin.Tree.empty [ "a" ] "1" in 103 + let h1 = Irmin.commit s ~tree ~parents:[] ~message:"main" ~author:"t" in 118 104 Irmin.set_head s ~branch:"main" h1; 119 105 let tree2 = Irmin.Tree.add tree [ "b" ] "2" in 120 106 let h2 = 121 - Irmin.commit s ~tree:tree2 ~parents:[ h1 ] ~message:"on dev" ~author:"t" 107 + Irmin.commit s ~tree:tree2 ~parents:[ h1 ] ~message:"dev" ~author:"t" 122 108 in 123 109 Irmin.set_head s ~branch:"dev" h2; 124 110 let bs = Irmin.branches s in 125 - Alcotest.(check int) "two branches" 2 (List.length bs); 126 111 Alcotest.(check bool) "has main" true (List.mem "main" bs); 127 112 Alcotest.(check bool) "has dev" true (List.mem "dev" bs) 128 113 129 - (* {1 update_branch CAS — from upstream test_atomic} *) 130 - 131 - let test_update_branch_success () = 132 - let s = Irmin.Mst.memory () in 114 + let test_cas s = 133 115 let tree = Irmin.Tree.empty in 134 116 let h1 = Irmin.commit s ~tree ~parents:[] ~message:"c1" ~author:"t" in 135 117 Irmin.set_head s ~branch:"main" h1; ··· 139 121 in 140 122 match Irmin.update_branch s ~branch:"main" ~old:(Some h1) ~new_:h2 with 141 123 | Ok () -> 142 - let cur = Irmin.head s ~branch:"main" in 143 124 Alcotest.(check bool) 144 - "head updated" true 145 - (match cur with Some h -> Irmin.Hash.equal h h2 | None -> false) 125 + "updated" true 126 + (match Irmin.head s ~branch:"main" with 127 + | Some h -> Irmin.Hash.equal h h2 128 + | None -> false) 146 129 | Error `Conflict -> Alcotest.fail "unexpected conflict" 147 130 148 - let test_update_branch_conflict () = 149 - let s = Irmin.Mst.memory () in 150 - let tree = Irmin.Tree.empty in 151 - let h1 = Irmin.commit s ~tree ~parents:[] ~message:"c1" ~author:"t" in 152 - Irmin.set_head s ~branch:"main" h1; 153 - let stale = Irmin.Hash.of_hex "0000000000000000000000000000000000000000" in 154 - let tree2 = Irmin.Tree.add tree [ "x" ] "y" in 155 - let h2 = 156 - Irmin.commit s ~tree:tree2 ~parents:[ h1 ] ~message:"c2" ~author:"t" 157 - in 158 - match Irmin.update_branch s ~branch:"main" ~old:(Some stale) ~new_:h2 with 159 - | Error `Conflict -> () 160 - | Ok () -> Alcotest.fail "should have conflicted" 161 - 162 - let test_update_branch_create () = 163 - let s = Irmin.Mst.memory () in 164 - let tree = Irmin.Tree.empty in 165 - let h = Irmin.commit s ~tree ~parents:[] ~message:"init" ~author:"t" in 166 - match Irmin.update_branch s ~branch:"new_branch" ~old:None ~new_:h with 167 - | Ok () -> 168 - Alcotest.(check bool) 169 - "branch created" true 170 - (match Irmin.head s ~branch:"new_branch" with 171 - | Some h' -> Irmin.Hash.equal h h' 172 - | None -> false) 173 - | Error `Conflict -> Alcotest.fail "unexpected conflict on create" 174 - 175 - let test_update_branch_create_exists () = 176 - let s = Irmin.Mst.memory () in 177 - let tree = Irmin.Tree.empty in 178 - let h = Irmin.commit s ~tree ~parents:[] ~message:"init" ~author:"t" in 179 - Irmin.set_head s ~branch:"main" h; 180 - let h2 = Irmin.commit s ~tree ~parents:[ h ] ~message:"c2" ~author:"t" in 181 - match Irmin.update_branch s ~branch:"main" ~old:None ~new_:h2 with 182 - | Error `Conflict -> () 183 - | Ok () -> Alcotest.fail "should have conflicted" 184 - 185 - (* {1 Log — from upstream test_history} *) 186 - 187 - let test_log_linear () = 188 - let s = Irmin.Mst.memory () in 131 + let test_log s = 189 132 let tree = Irmin.Tree.empty in 190 133 let h1 = Irmin.commit s ~tree ~parents:[] ~message:"c1" ~author:"t" in 191 134 let tree = Irmin.Tree.add tree [ "a" ] "1" in ··· 195 138 Irmin.set_head s ~branch:"main" h3; 196 139 let entries = Irmin.log s ~branch:"main" () in 197 140 Alcotest.(check int) "3 commits" 3 (List.length entries); 198 - Alcotest.(check string) "newest first" "c3" (List.nth entries 0).message; 199 - Alcotest.(check string) "middle" "c2" (List.nth entries 1).message; 200 - Alcotest.(check string) "oldest last" "c1" (List.nth entries 2).message 141 + Alcotest.(check string) "newest" "c3" (List.nth entries 0).message 201 142 202 - let test_log_limit () = 203 - let s = Irmin.Mst.memory () in 143 + let test_ancestor s = 204 144 let tree = Irmin.Tree.empty in 205 145 let h1 = Irmin.commit s ~tree ~parents:[] ~message:"c1" ~author:"t" in 206 146 let h2 = Irmin.commit s ~tree ~parents:[ h1 ] ~message:"c2" ~author:"t" in 207 147 let h3 = Irmin.commit s ~tree ~parents:[ h2 ] ~message:"c3" ~author:"t" in 208 - Irmin.set_head s ~branch:"main" h3; 209 - let entries = Irmin.log s ~branch:"main" ~limit:2 () in 210 - Alcotest.(check int) "limited to 2" 2 (List.length entries); 211 - Alcotest.(check string) "c3" "c3" (List.nth entries 0).message 212 - 213 - let test_log_empty_branch () = 214 - let s = Irmin.Mst.memory () in 215 - let entries = Irmin.log s ~branch:"nonexistent" () in 216 - Alcotest.(check int) "empty log" 0 (List.length entries) 217 - 218 - (* {1 Ancestry — from upstream test_closure / test_history} *) 219 - 220 - let test_is_ancestor_linear () = 221 - let s = Irmin.Mst.memory () in 222 - let tree = Irmin.Tree.empty in 223 - let h1 = Irmin.commit s ~tree ~parents:[] ~message:"c1" ~author:"t" in 224 - let h2 = Irmin.commit s ~tree ~parents:[ h1 ] ~message:"c2" ~author:"t" in 225 - let h3 = Irmin.commit s ~tree ~parents:[ h2 ] ~message:"c3" ~author:"t" in 226 - Alcotest.(check bool) 227 - "self" true 228 - (Irmin.is_ancestor s ~ancestor:h1 ~descendant:h1); 229 - Alcotest.(check bool) 230 - "parent" true 231 - (Irmin.is_ancestor s ~ancestor:h1 ~descendant:h2); 232 148 Alcotest.(check bool) 233 149 "grandparent" true 234 150 (Irmin.is_ancestor s ~ancestor:h1 ~descendant:h3); ··· 236 152 "not reverse" false 237 153 (Irmin.is_ancestor s ~ancestor:h3 ~descendant:h1) 238 154 239 - let test_is_ancestor_diamond () = 240 - let s = Irmin.Mst.memory () in 155 + let test_merge_base s = 241 156 let tree = Irmin.Tree.empty in 242 157 let h1 = Irmin.commit s ~tree ~parents:[] ~message:"c1" ~author:"t" in 243 158 let h2 = Irmin.commit s ~tree ~parents:[ h1 ] ~message:"c2" ~author:"t" in 244 159 let h3 = Irmin.commit s ~tree ~parents:[ h1 ] ~message:"c3" ~author:"t" in 245 - let h4 = Irmin.commit s ~tree ~parents:[ h2; h3 ] ~message:"c4" ~author:"t" in 246 - Alcotest.(check bool) 247 - "root→merge" true 248 - (Irmin.is_ancestor s ~ancestor:h1 ~descendant:h4); 249 - Alcotest.(check bool) 250 - "left→merge" true 251 - (Irmin.is_ancestor s ~ancestor:h2 ~descendant:h4); 252 - Alcotest.(check bool) 253 - "right→merge" true 254 - (Irmin.is_ancestor s ~ancestor:h3 ~descendant:h4); 255 - Alcotest.(check bool) 256 - "cross" false 257 - (Irmin.is_ancestor s ~ancestor:h2 ~descendant:h3) 160 + match Irmin.merge_base s h2 h3 with 161 + | Some mb -> Alcotest.(check bool) "mb" true (Irmin.Hash.equal mb h1) 162 + | None -> Alcotest.fail "expected merge base" 258 163 259 - (* {1 merge_base — from upstream test_history lcas} *) 164 + let test_head_none s = 165 + Alcotest.(check bool) "no head" true (Irmin.head s ~branch:"nope" = None) 166 + 167 + let generic_tests name (run : (Irmin.t -> unit) -> unit) = 168 + let t f () = run f in 169 + ( name, 170 + [ 171 + Alcotest.test_case "commit/checkout" `Quick (t test_commit_checkout); 172 + Alcotest.test_case "multiple commits" `Quick (t test_multi_commit); 173 + Alcotest.test_case "read_commit" `Quick (t test_read_commit); 174 + Alcotest.test_case "multiple branches" `Quick (t test_multi_branch); 175 + Alcotest.test_case "update_branch CAS" `Quick (t test_cas); 176 + Alcotest.test_case "log" `Quick (t test_log); 177 + Alcotest.test_case "is_ancestor" `Quick (t test_ancestor); 178 + Alcotest.test_case "merge_base" `Quick (t test_merge_base); 179 + Alcotest.test_case "head nonexistent" `Quick (t test_head_none); 180 + ] ) 181 + 182 + (* {1 Git backend helper} *) 260 183 261 - let test_merge_base_linear () = 262 - let s = Irmin.Mst.memory () in 263 - let tree = Irmin.Tree.empty in 264 - let h1 = Irmin.commit s ~tree ~parents:[] ~message:"c1" ~author:"t" in 265 - let h2 = Irmin.commit s ~tree ~parents:[ h1 ] ~message:"c2" ~author:"t" in 266 - let h3 = Irmin.commit s ~tree ~parents:[ h2 ] ~message:"c3" ~author:"t" in 267 - (match Irmin.merge_base s h2 h3 with 268 - | Some mb -> Alcotest.(check bool) "mb is c2" true (Irmin.Hash.equal mb h2) 269 - | None -> Alcotest.fail "expected merge base"); 270 - (match Irmin.merge_base s h1 h3 with 271 - | Some mb -> Alcotest.(check bool) "mb is c1" true (Irmin.Hash.equal mb h1) 272 - | None -> Alcotest.fail "expected merge base"); 273 - match Irmin.merge_base s h2 h2 with 274 - | Some mb -> Alcotest.(check bool) "mb is self" true (Irmin.Hash.equal mb h2) 275 - | None -> Alcotest.fail "expected merge base" 184 + let rec rm_rf path = 185 + if Eio.Path.is_directory path then begin 186 + List.iter 187 + (fun name -> rm_rf Eio.Path.(path / name)) 188 + (Eio.Path.read_dir path); 189 + Eio.Path.rmdir path 190 + end 191 + else if Eio.Path.is_file path then Eio.Path.unlink path 276 192 277 - let test_merge_base_diamond () = 278 - let s = Irmin.Mst.memory () in 279 - let tree = Irmin.Tree.empty in 280 - let h1 = Irmin.commit s ~tree ~parents:[] ~message:"c1" ~author:"t" in 281 - let h2 = Irmin.commit s ~tree ~parents:[ h1 ] ~message:"c2" ~author:"t" in 282 - let h3 = Irmin.commit s ~tree ~parents:[ h1 ] ~message:"c3" ~author:"t" in 283 - let _h4 = 284 - Irmin.commit s ~tree ~parents:[ h2; h3 ] ~message:"c4" ~author:"t" 193 + let with_git_store f = 194 + Eio_main.run @@ fun env -> 195 + Eio.Switch.run @@ fun sw -> 196 + let fs = Eio.Stdenv.fs env in 197 + let tmp = 198 + Fmt.str "/tmp/irmin-test-git-%d" (Random.int 1_000_000) |> Fpath.v 285 199 in 286 - match Irmin.merge_base s h2 h3 with 287 - | Some mb -> Alcotest.(check bool) "mb is c1" true (Irmin.Hash.equal mb h1) 288 - | None -> Alcotest.fail "expected merge base" 200 + Fun.protect 201 + ~finally:(fun () -> 202 + try rm_rf Eio.Path.(fs / Fpath.to_string tmp) with _ -> ()) 203 + (fun () -> 204 + let s = Irmin.Git.init ~sw ~fs ~path:tmp in 205 + f s) 289 206 290 - let test_merge_base_unrelated () = 291 - let s = Irmin.Mst.memory () in 292 - let tree = Irmin.Tree.empty in 293 - let h1 = Irmin.commit s ~tree ~parents:[] ~message:"c1" ~author:"t" in 294 - let h2 = Irmin.commit s ~tree ~parents:[] ~message:"c2" ~author:"t" in 295 - match Irmin.merge_base s h1 h2 with 296 - | None -> () 297 - | Some _ -> Alcotest.fail "should have no merge base" 207 + let with_pds_store f = 208 + Eio_main.run @@ fun env -> 209 + Eio.Switch.run @@ fun sw -> 210 + let cwd = Eio.Stdenv.cwd env in 211 + let name = Fmt.str "irmin-test-pds-%d" (Random.int 1_000_000) in 212 + let path = Eio.Path.(cwd / "_build" / name) in 213 + (try Eio.Path.mkdirs ~exists_ok:true ~perm:0o755 path with _ -> ()); 214 + Fun.protect 215 + ~finally:(fun () -> try rm_rf path with _ -> ()) 216 + (fun () -> 217 + let did = Atp.Did.of_string_exn "did:web:test.irmin" in 218 + let pds = Pds.v ~sw path ~did in 219 + let s = Irmin.Mst.of_pds pds in 220 + f s) 298 221 299 - let test_head_nonexistent () = 300 - let s = Irmin.Mst.memory () in 301 - Alcotest.(check bool) "no head" true (Irmin.head s ~branch:"nope" = None) 222 + (* {1 Suites} *) 302 223 303 224 let suite = 304 225 ( "store", 305 226 [ 306 - (* Internal API *) 307 227 Alcotest.test_case "store commit" `Quick test_store_commit; 308 228 Alcotest.test_case "store branches" `Quick test_store_branches; 309 229 Alcotest.test_case "store diff" `Quick test_store_diff; 310 - (* Public API: commit/checkout *) 311 - Alcotest.test_case "commit/checkout roundtrip" `Quick 312 - test_commit_checkout_roundtrip; 313 - Alcotest.test_case "multiple commits" `Quick test_multiple_commits; 314 - Alcotest.test_case "read_commit" `Quick test_read_commit; 315 - (* Branches *) 316 - Alcotest.test_case "multiple branches" `Quick test_multiple_branches; 317 - (* CAS *) 318 - Alcotest.test_case "update_branch success" `Quick 319 - test_update_branch_success; 320 - Alcotest.test_case "update_branch conflict" `Quick 321 - test_update_branch_conflict; 322 - Alcotest.test_case "update_branch create" `Quick test_update_branch_create; 323 - Alcotest.test_case "update_branch create exists" `Quick 324 - test_update_branch_create_exists; 325 - (* Log *) 326 - Alcotest.test_case "log linear" `Quick test_log_linear; 327 - Alcotest.test_case "log limit" `Quick test_log_limit; 328 - Alcotest.test_case "log empty branch" `Quick test_log_empty_branch; 329 - (* Ancestry *) 330 - Alcotest.test_case "is_ancestor linear" `Quick test_is_ancestor_linear; 331 - Alcotest.test_case "is_ancestor diamond" `Quick test_is_ancestor_diamond; 332 - (* merge_base *) 333 - Alcotest.test_case "merge_base linear" `Quick test_merge_base_linear; 334 - Alcotest.test_case "merge_base diamond" `Quick test_merge_base_diamond; 335 - Alcotest.test_case "merge_base unrelated" `Quick test_merge_base_unrelated; 336 - (* Head *) 337 - Alcotest.test_case "head nonexistent" `Quick test_head_nonexistent; 338 230 ] ) 231 + 232 + let mst_suite = generic_tests "store:mst" (fun f -> f (Irmin.Mst.memory ())) 233 + let git_suite = generic_tests "store:git" (fun f -> with_git_store f) 234 + let pds_suite = generic_tests "store:pds" (fun f -> with_pds_store f)
+10 -1
test/test_store.mli
··· 1 1 val suite : string * unit Alcotest.test_case list 2 - (** Test suite. *) 2 + (** Internal API tests (Private modules). *) 3 + 4 + val mst_suite : string * unit Alcotest.test_case list 5 + (** Backend-agnostic tests on MST in-memory backend. *) 6 + 7 + val git_suite : string * unit Alcotest.test_case list 8 + (** Backend-agnostic tests on Git backend. *) 9 + 10 + val pds_suite : string * unit Alcotest.test_case list 11 + (** Backend-agnostic tests on ATProto PDS backend. *)