···11-(** Record-level tests for [Irmin.Atproto].
22-33- This suite covers the auto-commit record API (put_record / get_record /
44- delete_record / list_records / list_collections / describe_repo). The
55- lower-level tree API that these build on is tested separately in
66- [test_atproto_tree.ml]. *)
77-88-open Irmin
99-1010-let test_did = "did:web:test.example.com"
1111-let pub_author = "tester"
1212-1313-let with_memory_store f =
1414- let store = Atproto.memory () in
1515- f store
1616-1717-(* ---------- put / get / delete ---------- *)
1818-1919-let test_put_get_record () =
2020- with_memory_store @@ fun store ->
2121- let _ =
2222- Atproto.put_record store ~author:pub_author ~collection:"app.bsky.feed.post"
2323- ~rkey:"post1" "{\"text\":\"hi\"}"
2424- in
2525- Alcotest.(check (option string))
2626- "get after put" (Some "{\"text\":\"hi\"}")
2727- (Atproto.get_record store ~collection:"app.bsky.feed.post" ~rkey:"post1")
2828-2929-let test_get_nonexistent () =
3030- with_memory_store @@ fun store ->
3131- Alcotest.(check (option string))
3232- "get missing" None
3333- (Atproto.get_record store ~collection:"c" ~rkey:"missing")
3434-3535-let test_put_overwrites () =
3636- with_memory_store @@ fun store ->
3737- let _ =
3838- Atproto.put_record store ~author:pub_author ~collection:"c" ~rkey:"k" "v1"
3939- in
4040- let _ =
4141- Atproto.put_record store ~author:pub_author ~collection:"c" ~rkey:"k" "v2"
4242- in
4343- Alcotest.(check (option string))
4444- "second put wins" (Some "v2")
4545- (Atproto.get_record store ~collection:"c" ~rkey:"k")
4646-4747-let test_delete_record () =
4848- with_memory_store @@ fun store ->
4949- let _ =
5050- Atproto.put_record store ~author:pub_author ~collection:"c" ~rkey:"keep"
5151- "v1"
5252- in
5353- let _ =
5454- Atproto.put_record store ~author:pub_author ~collection:"c" ~rkey:"drop"
5555- "v2"
5656- in
5757- let _ =
5858- Atproto.delete_record store ~author:pub_author ~collection:"c" ~rkey:"drop"
5959- ()
6060- in
6161- Alcotest.(check (option string))
6262- "kept" (Some "v1")
6363- (Atproto.get_record store ~collection:"c" ~rkey:"keep");
6464- Alcotest.(check (option string))
6565- "deleted" None
6666- (Atproto.get_record store ~collection:"c" ~rkey:"drop")
6767-6868-let test_delete_nonexistent_is_noop () =
6969- with_memory_store @@ fun store ->
7070- let _ =
7171- Atproto.put_record store ~author:pub_author ~collection:"c" ~rkey:"a" "v"
7272- in
7373- let _ =
7474- Atproto.delete_record store ~author:pub_author ~collection:"c"
7575- ~rkey:"never-existed" ()
7676- in
7777- Alcotest.(check (option string))
7878- "unaffected" (Some "v")
7979- (Atproto.get_record store ~collection:"c" ~rkey:"a")
8080-8181-(* ---------- listRecords semantics ---------- *)
8282-8383-let seed_feed store =
8484- let _ =
8585- Atproto.put_record store ~author:pub_author ~collection:"app.bsky.feed.post"
8686- ~rkey:"3kaaa" "{\"text\":\"first\"}"
8787- in
8888- let _ =
8989- Atproto.put_record store ~author:pub_author ~collection:"app.bsky.feed.post"
9090- ~rkey:"3kbbb" "{\"text\":\"second\"}"
9191- in
9292- let _ =
9393- Atproto.put_record store ~author:pub_author ~collection:"app.bsky.feed.post"
9494- ~rkey:"3kccc" "{\"text\":\"third\"}"
9595- in
9696- let _ =
9797- Atproto.put_record store ~author:pub_author ~collection:"app.bsky.feed.post"
9898- ~rkey:"3kddd" "{\"text\":\"fourth\"}"
9999- in
100100- ()
101101-102102-let test_list_records_sorted () =
103103- with_memory_store @@ fun store ->
104104- seed_feed store;
105105- let records, _ =
106106- Atproto.list_records store ~collection:"app.bsky.feed.post" ()
107107- in
108108- let rkeys = List.map fst records in
109109- Alcotest.(check (list string))
110110- "records in MST key order (lex sort)"
111111- [ "3kaaa"; "3kbbb"; "3kccc"; "3kddd" ]
112112- rkeys
113113-114114-let test_list_records_limit () =
115115- with_memory_store @@ fun store ->
116116- seed_feed store;
117117- let records, cursor =
118118- Atproto.list_records store ~collection:"app.bsky.feed.post" ~limit:2 ()
119119- in
120120- Alcotest.(check int) "limit respected" 2 (List.length records);
121121- let rkeys = List.map fst records in
122122- Alcotest.(check (list string)) "first page" [ "3kaaa"; "3kbbb" ] rkeys;
123123- Alcotest.(check bool) "cursor present" true (Option.is_some cursor)
124124-125125-let test_list_records_cursor_pagination () =
126126- with_memory_store @@ fun store ->
127127- seed_feed store;
128128- let first_page, cursor =
129129- Atproto.list_records store ~collection:"app.bsky.feed.post" ~limit:2 ()
130130- in
131131- let cursor =
132132- match cursor with Some c -> c | None -> Alcotest.fail "no cursor"
133133- in
134134- let second_page, cursor2 =
135135- Atproto.list_records store ~collection:"app.bsky.feed.post" ~limit:10
136136- ~cursor ()
137137- in
138138- let all = List.map fst first_page @ List.map fst second_page in
139139- Alcotest.(check (list string))
140140- "union of pages = all records (no dup, no gap)"
141141- [ "3kaaa"; "3kbbb"; "3kccc"; "3kddd" ]
142142- all;
143143- Alcotest.(check bool)
144144- "final page has no further cursor" true (Option.is_none cursor2)
145145-146146-let test_list_records_empty_collection () =
147147- with_memory_store @@ fun store ->
148148- let records, cursor =
149149- Atproto.list_records store ~collection:"empty.collection" ()
150150- in
151151- Alcotest.(check int) "no records" 0 (List.length records);
152152- Alcotest.(check bool) "no cursor" true (Option.is_none cursor)
153153-154154-(* ---------- collection isolation ---------- *)
155155-156156-let test_collections_isolated () =
157157- with_memory_store @@ fun store ->
158158- let _ =
159159- Atproto.put_record store ~author:pub_author ~collection:"app.bsky.feed.post"
160160- ~rkey:"p" "post"
161161- in
162162- let _ =
163163- Atproto.put_record store ~author:pub_author ~collection:"app.bsky.feed.like"
164164- ~rkey:"p" "like"
165165- in
166166- Alcotest.(check (option string))
167167- "post" (Some "post")
168168- (Atproto.get_record store ~collection:"app.bsky.feed.post" ~rkey:"p");
169169- Alcotest.(check (option string))
170170- "like" (Some "like")
171171- (Atproto.get_record store ~collection:"app.bsky.feed.like" ~rkey:"p");
172172- let post_keys =
173173- fst (Atproto.list_records store ~collection:"app.bsky.feed.post" ())
174174- |> List.map fst
175175- in
176176- let like_keys =
177177- fst (Atproto.list_records store ~collection:"app.bsky.feed.like" ())
178178- |> List.map fst
179179- in
180180- Alcotest.(check (list string)) "feed.post list only" [ "p" ] post_keys;
181181- Alcotest.(check (list string)) "feed.like list only" [ "p" ] like_keys
182182-183183-let test_collection_prefix_isolated () =
184184- (* "app.bsky.feed.posts" (plural) must not match "app.bsky.feed.post/". *)
185185- with_memory_store @@ fun store ->
186186- let _ =
187187- Atproto.put_record store ~author:pub_author ~collection:"app.bsky.feed.post"
188188- ~rkey:"a" "1"
189189- in
190190- let _ =
191191- Atproto.put_record store ~author:pub_author
192192- ~collection:"app.bsky.feed.posts" ~rkey:"b" "2"
193193- in
194194- let posts_list, _ =
195195- Atproto.list_records store ~collection:"app.bsky.feed.post" ()
196196- in
197197- Alcotest.(check (list string))
198198- "post collection is isolated from posts collection" [ "a" ]
199199- (List.map fst posts_list)
200200-201201-(* ---------- list_collections / describe_repo ---------- *)
202202-203203-let test_list_collections () =
204204- with_memory_store @@ fun store ->
205205- let _ =
206206- Atproto.put_record store ~author:pub_author ~collection:"app.bsky.feed.post"
207207- ~rkey:"p" "1"
208208- in
209209- let _ =
210210- Atproto.put_record store ~author:pub_author
211211- ~collection:"app.bsky.actor.profile" ~rkey:"self" "2"
212212- in
213213- let _ =
214214- Atproto.put_record store ~author:pub_author
215215- ~collection:"app.bsky.graph.follow" ~rkey:"f1" "3"
216216- in
217217- let got = Atproto.list_collections store |> List.sort String.compare in
218218- Alcotest.(check (list string))
219219- "distinct collections"
220220- [ "app.bsky.actor.profile"; "app.bsky.feed.post"; "app.bsky.graph.follow" ]
221221- got
222222-223223-let test_describe_repo_shape () =
224224- with_memory_store @@ fun store ->
225225- let _ =
226226- Atproto.put_record store ~author:pub_author ~collection:"app.bsky.feed.post"
227227- ~rkey:"p" "1"
228228- in
229229- let info = Atproto.describe_repo store ~did:test_did in
230230- Alcotest.(check string) "did" test_did info.did;
231231- Alcotest.(check (list string))
232232- "collections" [ "app.bsky.feed.post" ] info.collections
233233-234234-let test_describe_repo_empty () =
235235- with_memory_store @@ fun store ->
236236- let info = Atproto.describe_repo store ~did:test_did in
237237- Alcotest.(check string) "did present" test_did info.did;
238238- Alcotest.(check int) "no collections yet" 0 (List.length info.collections)
239239-240240-(* ---------- Wire compatibility with Pds.* ---------- *)
241241-242242-let with_pds_backed_store f =
243243- Eio_main.run @@ fun env ->
244244- let cwd = Eio.Stdenv.cwd env in
245245- let tmp_dir = Eio.Path.(cwd / "_build" / "test_atproto") in
246246- (try Eio.Path.mkdirs ~exists_ok:true ~perm:0o755 tmp_dir with Eio.Io _ -> ());
247247- let name = Fmt.str "repo_%d" (Random.int 1_000_000) in
248248- let path = Eio.Path.(tmp_dir / name) in
249249- Fun.protect
250250- ~finally:(fun () -> try Helpers.rm_rf path with Eio.Io _ -> ())
251251- (fun () ->
252252- Eio.Switch.run @@ fun sw ->
253253- let did = Atp.Did.of_string_exn test_did in
254254- let pds = Pds.v ~sw path ~did in
255255- let store = Atproto.of_pds pds in
256256- f pds store)
257257-258258-let test_atproto_writes_readable_pds () =
259259- with_pds_backed_store @@ fun pds store ->
260260- let _ =
261261- Atproto.put_record store ~author:pub_author ~collection:"app.bsky.feed.post"
262262- ~rkey:"p1" "{\"text\":\"via atproto\"}"
263263- in
264264- let _ =
265265- Atproto.put_record store ~author:pub_author ~collection:"app.bsky.feed.post"
266266- ~rkey:"p2" "{\"text\":\"via atproto 2\"}"
267267- in
268268- (* Read via Pds.find — native ATProto primitives *)
269269- let v1 = Pds.find pds ~collection:"app.bsky.feed.post" ~rkey:"p1" in
270270- Alcotest.(check (option string))
271271- "Pds.find sees atproto writes" (Some "{\"text\":\"via atproto\"}") v1;
272272- let rkeys =
273273- Pds.list pds ~collection:"app.bsky.feed.post"
274274- |> List.map fst |> List.sort String.compare
275275- in
276276- Alcotest.(check (list string))
277277- "Pds.list sees all records" [ "p1"; "p2" ] rkeys
278278-279279-let test_pds_writes_readable_atproto () =
280280- with_pds_backed_store @@ fun pds store ->
281281- let data =
282282- Atp.Dagcbor.encode_string ~cid_format:`Atproto (`String "from-pds")
283283- in
284284- Pds.put pds ~collection:"c" ~rkey:"k" data;
285285- Alcotest.(check (option string))
286286- "atproto reads pds-written record" (Some data)
287287- (Atproto.get_record store ~collection:"c" ~rkey:"k")
288288-289289-let raw_record s = Atp.Dagcbor.encode_string ~cid_format:`Atproto (`String s)
290290-291291-let test_interleaved_atproto_pds_writes () =
292292- with_pds_backed_store @@ fun pds store ->
293293- (* Three writes alternating between atproto's high-level API and the
294294- low-level Pds.put. Both surfaces must converge on the same MST so
295295- each can read what the other wrote. *)
296296- let _ =
297297- Atproto.put_record store ~author:pub_author ~collection:"col" ~rkey:"a"
298298- "via-atproto"
299299- in
300300- Pds.put pds ~collection:"col" ~rkey:"b" (raw_record "via-pds");
301301- let _ =
302302- Atproto.put_record store ~author:pub_author ~collection:"col" ~rkey:"c"
303303- "via-atproto-2"
304304- in
305305- Alcotest.(check (option string))
306306- "atproto sees a" (Some "via-atproto")
307307- (Atproto.get_record store ~collection:"col" ~rkey:"a");
308308- Alcotest.(check (option string))
309309- "atproto sees b"
310310- (Some (raw_record "via-pds"))
311311- (Atproto.get_record store ~collection:"col" ~rkey:"b");
312312- Alcotest.(check (option string))
313313- "atproto sees c" (Some "via-atproto-2")
314314- (Atproto.get_record store ~collection:"col" ~rkey:"c");
315315- Alcotest.(check (option string))
316316- "pds sees a" (Some "via-atproto")
317317- (Pds.find pds ~collection:"col" ~rkey:"a");
318318- Alcotest.(check (option string))
319319- "pds sees b"
320320- (Some (raw_record "via-pds"))
321321- (Pds.find pds ~collection:"col" ~rkey:"b");
322322- Alcotest.(check (option string))
323323- "pds sees c" (Some "via-atproto-2")
324324- (Pds.find pds ~collection:"col" ~rkey:"c")
325325-326326-let test_atproto_delete_visible_pds () =
327327- with_pds_backed_store @@ fun pds store ->
328328- let _ =
329329- Atproto.put_record store ~author:pub_author ~collection:"c" ~rkey:"k" "v"
330330- in
331331- Alcotest.(check bool)
332332- "pds sees pre-delete" true
333333- (Option.is_some (Pds.find pds ~collection:"c" ~rkey:"k"));
334334- let _ =
335335- Atproto.delete_record store ~author:pub_author ~collection:"c" ~rkey:"k" ()
336336- in
337337- Alcotest.(check (option string))
338338- "pds sees post-delete" None
339339- (Pds.find pds ~collection:"c" ~rkey:"k")
340340-341341-let suite =
342342- ( "atproto",
343343- [
344344- Alcotest.test_case "put then get" `Quick test_put_get_record;
345345- Alcotest.test_case "get non-existent" `Quick test_get_nonexistent;
346346- Alcotest.test_case "put overwrites" `Quick test_put_overwrites;
347347- Alcotest.test_case "delete record" `Quick test_delete_record;
348348- Alcotest.test_case "delete nonexistent is no-op" `Quick
349349- test_delete_nonexistent_is_noop;
350350- Alcotest.test_case "list records sorted" `Quick test_list_records_sorted;
351351- Alcotest.test_case "list records limit" `Quick test_list_records_limit;
352352- Alcotest.test_case "list records cursor pagination" `Quick
353353- test_list_records_cursor_pagination;
354354- Alcotest.test_case "list empty collection" `Quick
355355- test_list_records_empty_collection;
356356- Alcotest.test_case "collections isolated" `Quick test_collections_isolated;
357357- Alcotest.test_case "collection prefix does not leak" `Quick
358358- test_collection_prefix_isolated;
359359- Alcotest.test_case "list collections" `Quick test_list_collections;
360360- Alcotest.test_case "describe repo shape" `Quick test_describe_repo_shape;
361361- Alcotest.test_case "describe repo empty" `Quick test_describe_repo_empty;
362362- Alcotest.test_case "atproto writes readable by pds" `Quick
363363- test_atproto_writes_readable_pds;
364364- Alcotest.test_case "pds writes readable by atproto" `Quick
365365- test_pds_writes_readable_atproto;
366366- Alcotest.test_case "interleaved atproto + pds writes" `Quick
367367- test_interleaved_atproto_pds_writes;
368368- Alcotest.test_case "atproto delete visible to pds" `Quick
369369- test_atproto_delete_visible_pds;
370370- ] )
-2
test/test_atproto.mli
···11-val suite : string * unit Alcotest.test_case list
22-(** [suite] is the alcotest test suite for [test_atproto]. *)
-156
test/test_atproto_ext.ml
···11-(** ATProto backend-specific extension tests.
22-33- These tests exercise operations that only make sense on an ATProto store.
44- The phantom type [`Atproto] ensures these functions cannot be called on a
55- Git store — the wrong call doesn't typecheck. *)
66-77-let author = "tester"
88-let with_memory f = f (Irmin_atproto.memory ())
99-1010-let cleanup_path path =
1111- try Helpers.rm_rf path with Eio.Io _ | Sys_error _ -> ()
1212-1313-let with_pds_path ~sw path f =
1414- Fun.protect
1515- ~finally:(fun () -> cleanup_path path)
1616- (fun () -> f (Irmin_atproto.disk ~sw path))
1717-1818-let with_pds f =
1919- Eio_main.run @@ fun env ->
2020- Eio.Switch.run @@ fun sw ->
2121- let cwd = Eio.Stdenv.cwd env in
2222- let name = Fmt.str "irmin-test-atp-ext-%d" (Random.int 1_000_000) in
2323- let path = Eio.Path.(cwd / "_build" / name) in
2424- (try Eio.Path.mkdirs ~exists_ok:true ~perm:0o755 path with Eio.Io _ -> ());
2525- with_pds_path ~sw path f
2626-2727-(* Run on both memory and PDS backends. *)
2828-let with_each f =
2929- with_memory f;
3030- with_pds f
3131-3232-(* {1 Record-level operations} *)
3333-3434-let test_put_get_record () =
3535- with_each @@ fun (s : [ `Atproto ] Store.t) ->
3636- let _ =
3737- Irmin_atproto.put_record s ~author ~collection:"app.bsky.feed.post"
3838- ~rkey:"post1" "{\"text\":\"hi\"}"
3939- in
4040- let got =
4141- Irmin_atproto.get_record s ~collection:"app.bsky.feed.post" ~rkey:"post1"
4242- in
4343- Alcotest.(check (option string))
4444- "get after put" (Some "{\"text\":\"hi\"}") got
4545-4646-let test_delete_record () =
4747- with_each @@ fun (s : [ `Atproto ] Store.t) ->
4848- let _ =
4949- Irmin_atproto.put_record s ~author ~collection:"c" ~rkey:"keep" "v1"
5050- in
5151- let _ =
5252- Irmin_atproto.put_record s ~author ~collection:"c" ~rkey:"drop" "v2"
5353- in
5454- let _ =
5555- Irmin_atproto.delete_record s ~author ~collection:"c" ~rkey:"drop" ()
5656- in
5757- Alcotest.(check (option string))
5858- "kept" (Some "v1")
5959- (Irmin_atproto.get_record s ~collection:"c" ~rkey:"keep");
6060- Alcotest.(check (option string))
6161- "deleted" None
6262- (Irmin_atproto.get_record s ~collection:"c" ~rkey:"drop")
6363-6464-let test_list_records () =
6565- with_each @@ fun (s : [ `Atproto ] Store.t) ->
6666- List.iter
6767- (fun rkey ->
6868- let _ =
6969- Irmin_atproto.put_record s ~author ~collection:"app.bsky.feed.post"
7070- ~rkey ("v-" ^ rkey)
7171- in
7272- ())
7373- [ "c"; "a"; "b" ];
7474- let page, _ =
7575- Irmin_atproto.list_records s ~collection:"app.bsky.feed.post" ()
7676- in
7777- let rkeys = List.map fst page in
7878- Alcotest.(check (list string)) "sorted" [ "a"; "b"; "c" ] rkeys
7979-8080-let test_list_collections () =
8181- with_each @@ fun (s : [ `Atproto ] Store.t) ->
8282- let _ =
8383- Irmin_atproto.put_record s ~author ~collection:"app.bsky.feed.post"
8484- ~rkey:"k" "v"
8585- in
8686- let _ =
8787- Irmin_atproto.put_record s ~author ~collection:"app.bsky.graph.follow"
8888- ~rkey:"k" "v"
8989- in
9090- let cols = Irmin_atproto.list_collections s in
9191- Alcotest.(check (list string))
9292- "two collections"
9393- [ "app.bsky.feed.post"; "app.bsky.graph.follow" ]
9494- cols
9595-9696-(* {1 Mixed generic + extension operations}
9797-9898- The same store is used through both the generic Irmin API and the
9999- ATProto-specific extensions. Both surfaces must see the same data. *)
100100-101101-let test_generic_reads_extension_writes () =
102102- with_each @@ fun (s : [ `Atproto ] Store.t) ->
103103- (* Write via ATProto extension *)
104104- let _ =
105105- Irmin_atproto.put_record s ~author ~collection:"col" ~rkey:"a" "via-ext"
106106- in
107107- (* Read via generic API *)
108108- match Store.checkout s ~branch:"main" with
109109- | None -> Alcotest.fail "checkout returned None"
110110- | Some tree ->
111111- Alcotest.(check (option string))
112112- "generic sees extension write" (Some "via-ext")
113113- (Tree.find tree [ "col"; "a" ])
114114-115115-let test_extension_reads_generic_writes () =
116116- with_each @@ fun (s : [ `Atproto ] Store.t) ->
117117- (* Write via generic API *)
118118- let tree = Tree.add Tree.empty [ "col"; "b" ] "via-generic" in
119119- let h = Store.commit s ~tree ~parents:[] ~message:"gen" ~author in
120120- Store.set_head s ~branch:"main" h;
121121- (* Read via ATProto extension *)
122122- Alcotest.(check (option string))
123123- "extension sees generic write" (Some "via-generic")
124124- (Irmin_atproto.get_record s ~collection:"col" ~rkey:"b")
125125-126126-(* {1 Wire compatibility with PDS}
127127-128128- An ATProto store backed by PDS must be readable through the
129129- low-level Pds.find / Pds.list primitives too. *)
130130-131131-let test_pds_wire_compat () =
132132- with_pds @@ fun (s : [ `Atproto ] Store.t) ->
133133- let _ =
134134- Irmin_atproto.put_record s ~author ~collection:"col" ~rkey:"a"
135135- "{\"text\":\"a\"}"
136136- in
137137- let pds = Irmin_atproto.pds_of s in
138138- Alcotest.(check (option string))
139139- "Pds.find sees Irmin write" (Some "{\"text\":\"a\"}")
140140- (Pds.find pds ~collection:"col" ~rkey:"a")
141141-142142-(* {1 Suite} *)
143143-144144-let suite =
145145- ( "atproto_ext",
146146- [
147147- Alcotest.test_case "put/get record" `Quick test_put_get_record;
148148- Alcotest.test_case "delete record" `Quick test_delete_record;
149149- Alcotest.test_case "list records sorted" `Quick test_list_records;
150150- Alcotest.test_case "list collections" `Quick test_list_collections;
151151- Alcotest.test_case "generic reads extension writes" `Quick
152152- test_generic_reads_extension_writes;
153153- Alcotest.test_case "extension reads generic writes" `Quick
154154- test_extension_reads_generic_writes;
155155- Alcotest.test_case "PDS wire compat" `Quick test_pds_wire_compat;
156156- ] )
-2
test/test_atproto_ext.mli
···11-val suite : string * unit Alcotest.test_case list
22-(** [suite] is the alcotest test suite for [test_atproto_ext]. *)
-389
test/test_atproto_tree.ml
···11-(** Tree-level tests for [Irmin.Atproto].
22-33- The ATProto adapter exposes two layers: a tree-level API with pure [tree]
44- values and explicit commits, and a record-level API with auto-commit against
55- [main] (covered in [test_atproto.ml]). This suite exercises the former: tree
66- mutation, sorted iteration, prefix scans, hash determinism, and the
77- wire-format contract with [Pds.find] / [Pds.list]. *)
88-99-open Irmin
1010-1111-let test_did = Atp.Did.of_string_exn "did:web:test.example.com"
1212-1313-let with_memory_store f =
1414- let store = Atproto.memory () in
1515- f store
1616-1717-let with_pds_store f =
1818- Eio_main.run @@ fun env ->
1919- let cwd = Eio.Stdenv.cwd env in
2020- let tmp_dir = Eio.Path.(cwd / "_build" / "test_atproto_tree") in
2121- (try Eio.Path.mkdirs ~exists_ok:true ~perm:0o755 tmp_dir with Eio.Io _ -> ());
2222- let name = Fmt.str "repo_%d" (Random.int 1_000_000) in
2323- let path = Eio.Path.(tmp_dir / name) in
2424- Fun.protect
2525- ~finally:(fun () -> try Helpers.rm_rf path with Eio.Io _ -> ())
2626- (fun () ->
2727- Eio.Switch.run @@ fun sw ->
2828- let pds = Pds.v ~sw path ~did:test_did in
2929- let store = Atproto.of_pds pds in
3030- f store)
3131-3232-let post k = ("app.bsky.feed.post", k)
3333-let profile k = ("app.bsky.actor.profile", k)
3434-3535-(* ---------- Basic API shape ---------- *)
3636-3737-let test_empty_store () =
3838- with_memory_store @@ fun _store ->
3939- let tree = Atproto.empty () in
4040- Alcotest.(check (option string))
4141- "find in empty returns None" None
4242- (Atproto.find tree (post "whatever"));
4343- Alcotest.(check int)
4444- "empty leaves is 0 entries" 0
4545- (Seq.length (Atproto.leaves tree))
4646-4747-let test_add_then_find () =
4848- with_memory_store @@ fun _store ->
4949- let t = Atproto.empty () in
5050- let t = Atproto.add t (post "post1") "content-1" in
5151- Alcotest.(check (option string))
5252- "find added" (Some "content-1")
5353- (Atproto.find t (post "post1"));
5454- Alcotest.(check (option string))
5555- "find non-existent" None
5656- (Atproto.find t (post "post2"))
5757-5858-let test_add_overwrites () =
5959- with_memory_store @@ fun _store ->
6060- let t = Atproto.empty () in
6161- let t = Atproto.add t (post "p") "v1" in
6262- let t = Atproto.add t (post "p") "v2" in
6363- Alcotest.(check (option string))
6464- "second add wins" (Some "v2")
6565- (Atproto.find t (post "p"))
6666-6767-let test_remove () =
6868- with_memory_store @@ fun _store ->
6969- let t = Atproto.empty () in
7070- let t = Atproto.add t (post "k1") "v1" in
7171- let t = Atproto.add t (post "k2") "v2" in
7272- let t = Atproto.remove t (post "k1") in
7373- Alcotest.(check (option string))
7474- "removed gone" None
7575- (Atproto.find t (post "k1"));
7676- Alcotest.(check (option string))
7777- "sibling still there" (Some "v2")
7878- (Atproto.find t (post "k2"));
7979- Alcotest.(check int) "one leaf remains" 1 (Seq.length (Atproto.leaves t))
8080-8181-let test_remove_nonexistent () =
8282- with_memory_store @@ fun _store ->
8383- let t = Atproto.empty () in
8484- let t = Atproto.add t (post "a") "v" in
8585- let t' = Atproto.remove t (post "ghost") in
8686- Alcotest.(check (option string))
8787- "untouched" (Some "v")
8888- (Atproto.find t' (post "a"));
8989- Alcotest.(check int) "still 1 leaf" 1 (Seq.length (Atproto.leaves t'))
9090-9191-(* ---------- leaves: ordering and completeness ---------- *)
9292-9393-let test_leaves_sorted () =
9494- with_memory_store @@ fun _store ->
9595- let t = Atproto.empty () in
9696- let rkeys = [ "zzz"; "aaa"; "mmm"; "abc"; "xyz" ] in
9797- let t =
9898- List.fold_left
9999- (fun t rkey -> Atproto.add t (post rkey) ("v-" ^ rkey))
100100- t rkeys
101101- in
102102- let got =
103103- Atproto.leaves t |> Seq.map (fun ((_, rkey), _) -> rkey) |> List.of_seq
104104- in
105105- Alcotest.(check (list string))
106106- "leaves sorted"
107107- [ "aaa"; "abc"; "mmm"; "xyz"; "zzz" ]
108108- got
109109-110110-let test_leaves_many_entries () =
111111- with_memory_store @@ fun _store ->
112112- let t = Atproto.empty () in
113113- let n = 500 in
114114- let t =
115115- List.init n (fun i -> (post (Fmt.str "k%06d" i), Fmt.str "v%d" i))
116116- |> List.fold_left (fun t (k, v) -> Atproto.add t k v) t
117117- in
118118- Alcotest.(check int) "all present" n (Seq.length (Atproto.leaves t));
119119- Alcotest.(check (option string))
120120- "first record" (Some "v0")
121121- (Atproto.find t (post "k000000"));
122122- Alcotest.(check (option string))
123123- "middle record" (Some "v250")
124124- (Atproto.find t (post "k000250"));
125125- Alcotest.(check (option string))
126126- "last record" (Some "v499")
127127- (Atproto.find t (post "k000499"))
128128-129129-(* ---------- list: the primitive for listRecords ---------- *)
130130-131131-let test_list_basic () =
132132- with_memory_store @@ fun _store ->
133133- let t = Atproto.empty () in
134134- let t = Atproto.add t (post "p1") "v1" in
135135- let t = Atproto.add t (post "p2") "v2" in
136136- let t = Atproto.add t (post "p3") "v3" in
137137- let t = Atproto.add t (profile "self") "me" in
138138- let rkeys =
139139- Atproto.list t ~collection:"app.bsky.feed.post"
140140- |> Seq.map fst |> List.of_seq
141141- in
142142- Alcotest.(check (list string))
143143- "only feed.post, sorted" [ "p1"; "p2"; "p3" ] rkeys
144144-145145-let test_list_values () =
146146- with_memory_store @@ fun _store ->
147147- let t = Atproto.empty () in
148148- let t = Atproto.add t (post "a") "val-a" in
149149- let t = Atproto.add t (post "b") "val-b" in
150150- let got = Atproto.list t ~collection:"app.bsky.feed.post" |> List.of_seq in
151151- Alcotest.(check (list (pair string string)))
152152- "rkey,value pairs"
153153- [ ("a", "val-a"); ("b", "val-b") ]
154154- got
155155-156156-let test_list_isolation () =
157157- (* "app.bsky.feed.post" scan must not leak into "app.bsky.feed.posts"
158158- (plural). That was the guarantee the old String.starts_with code got
159159- wrong; the split_key approach avoids it structurally. *)
160160- with_memory_store @@ fun _store ->
161161- let t = Atproto.empty () in
162162- let t = Atproto.add t ("app.bsky.feed.post", "a") "in" in
163163- let t = Atproto.add t ("app.bsky.feed.posts", "x") "out" in
164164- let got =
165165- Atproto.list t ~collection:"app.bsky.feed.post"
166166- |> Seq.map fst |> List.of_seq
167167- in
168168- Alcotest.(check (list string)) "isolated from posts (plural)" [ "a" ] got
169169-170170-(* ---------- Hash determinism ---------- *)
171171-172172-let test_hash_deterministic () =
173173- with_memory_store @@ fun store ->
174174- let build () =
175175- let t = Atproto.empty () in
176176- let t = Atproto.add t (post "p1") "v1" in
177177- let t = Atproto.add t (post "p2") "v2" in
178178- let t = Atproto.add t (post "p3") "v3" in
179179- Atproto.hash t ~store
180180- in
181181- Alcotest.(check bool)
182182- "same content → same hash" true
183183- (Irmin.Hash.equal (build ()) (build ()))
184184-185185-let test_hash_order_independent () =
186186- with_memory_store @@ fun store ->
187187- let h_asc =
188188- let t = Atproto.empty () in
189189- let t = Atproto.add t (post "a") "1" in
190190- let t = Atproto.add t (post "b") "2" in
191191- let t = Atproto.add t (post "c") "3" in
192192- Atproto.hash t ~store
193193- in
194194- let h_desc =
195195- let t = Atproto.empty () in
196196- let t = Atproto.add t (post "c") "3" in
197197- let t = Atproto.add t (post "b") "2" in
198198- let t = Atproto.add t (post "a") "1" in
199199- Atproto.hash t ~store
200200- in
201201- Alcotest.(check bool)
202202- "insertion order does not affect hash" true
203203- (Irmin.Hash.equal h_asc h_desc)
204204-205205-let test_hash_changes_with_content () =
206206- with_memory_store @@ fun store ->
207207- let h1 =
208208- let t = Atproto.empty () in
209209- let t = Atproto.add t (post "k") "v1" in
210210- Atproto.hash t ~store
211211- in
212212- let h2 =
213213- let t = Atproto.empty () in
214214- let t = Atproto.add t (post "k") "v2" in
215215- Atproto.hash t ~store
216216- in
217217- Alcotest.(check bool)
218218- "different content → different hash" false (Irmin.Hash.equal h1 h2)
219219-220220-(* ---------- Commit + checkout roundtrip ---------- *)
221221-222222-let test_commit_checkout () =
223223- with_memory_store @@ fun store ->
224224- let t = Atproto.empty () in
225225- let t = Atproto.add t (post "p1") "hello" in
226226- let h =
227227- Atproto.commit store ~tree:t ~parents:[] ~message:"init" ~author:"alice"
228228- in
229229- Atproto.set_head store ~branch:"main" h;
230230- match Atproto.checkout store ~branch:"main" with
231231- | None -> Alcotest.fail "checkout returned None"
232232- | Some t' ->
233233- Alcotest.(check (option string))
234234- "round trip" (Some "hello")
235235- (Atproto.find t' (post "p1"))
236236-237237-(* ---------- Commit metadata survives ---------- *)
238238-239239-let test_commit_stores_metadata () =
240240- with_memory_store @@ fun store ->
241241- let t = Atproto.empty () in
242242- let t = Atproto.add t (post "k") "v" in
243243- let h =
244244- Atproto.commit store ~tree:t ~parents:[] ~message:"first real commit"
245245- ~author:"alice"
246246- in
247247- match Atproto.read_commit store h with
248248- | None -> Alcotest.fail "read_commit returned None"
249249- | Some c ->
250250- Alcotest.(check string) "author" "alice" c.author;
251251- Alcotest.(check string) "message" "first real commit" c.message;
252252- Alcotest.(check int) "no parents" 0 (List.length c.parents)
253253-254254-let test_commit_message_changes_hash () =
255255- with_memory_store @@ fun store ->
256256- let t = Atproto.empty () in
257257- let t = Atproto.add t (post "k") "v" in
258258- let h1 =
259259- Atproto.commit store ~tree:t ~parents:[] ~message:"msg one" ~author:"a"
260260- in
261261- let h2 =
262262- Atproto.commit store ~tree:t ~parents:[] ~message:"msg two" ~author:"a"
263263- in
264264- Alcotest.(check bool)
265265- "same tree, different message → different hash" false
266266- (Irmin.Hash.equal h1 h2)
267267-268268-let test_commit_parent_chain () =
269269- with_memory_store @@ fun store ->
270270- let t = Atproto.empty () in
271271- let t = Atproto.add t (post "k") "v1" in
272272- let h1 = Atproto.commit store ~tree:t ~parents:[] ~message:"c1" ~author:"a" in
273273- Atproto.set_head store ~branch:"main" h1;
274274- let t = Atproto.add t (post "k") "v2" in
275275- let h2 =
276276- Atproto.commit store ~tree:t ~parents:[ h1 ] ~message:"c2" ~author:"a"
277277- in
278278- Atproto.set_head store ~branch:"main" h2;
279279- let t = Atproto.add t (post "k") "v3" in
280280- let h3 =
281281- Atproto.commit store ~tree:t ~parents:[ h2 ] ~message:"c3" ~author:"a"
282282- in
283283- Atproto.set_head store ~branch:"main" h3;
284284- match Atproto.read_commit store h3 with
285285- | None -> Alcotest.fail "no h3"
286286- | Some c3 -> (
287287- Alcotest.(check (list string))
288288- "h3 parent is h2"
289289- [ Irmin.Hash.to_hex h2 ]
290290- (List.map Irmin.Hash.to_hex c3.parents);
291291- match Atproto.read_commit store h2 with
292292- | None -> Alcotest.fail "no h2"
293293- | Some c2 ->
294294- Alcotest.(check (list string))
295295- "h2 parent is h1"
296296- [ Irmin.Hash.to_hex h1 ]
297297- (List.map Irmin.Hash.to_hex c2.parents))
298298-299299-(* ---------- PDS persistence across sessions ---------- *)
300300-301301-let persist_session_write path =
302302- Eio.Switch.run @@ fun sw ->
303303- let pds = Pds.v ~sw path ~did:test_did in
304304- let store = Atproto.of_pds pds in
305305- let t = Atproto.empty () in
306306- let t = Atproto.add t (post "persist") "value" in
307307- let h = Atproto.commit store ~tree:t ~parents:[] ~message:"w" ~author:"a" in
308308- Atproto.set_head store ~branch:"main" h;
309309- Pds.close pds
310310-311311-let persist_session_read path =
312312- Eio.Switch.run @@ fun sw ->
313313- let pds = Pds.open_ ~sw path in
314314- let store = Atproto.of_pds pds in
315315- match Atproto.checkout store ~branch:"main" with
316316- | None -> Alcotest.fail "session 2 checkout None"
317317- | Some t ->
318318- Alcotest.(check (option string))
319319- "read after reopen" (Some "value")
320320- (Atproto.find t (post "persist"));
321321- Pds.close pds
322322-323323-let test_commit_survives_reopen_pds () =
324324- Eio_main.run @@ fun env ->
325325- let cwd = Eio.Stdenv.cwd env in
326326- let tmp_dir = Eio.Path.(cwd / "_build" / "test_atproto_tree") in
327327- (try Eio.Path.mkdirs ~exists_ok:true ~perm:0o755 tmp_dir with Eio.Io _ -> ());
328328- let name = Fmt.str "persist_%d" (Random.int 1_000_000) in
329329- let path = Eio.Path.(tmp_dir / name) in
330330- Fun.protect
331331- ~finally:(fun () -> try Helpers.rm_rf path with Eio.Io _ -> ())
332332- (fun () ->
333333- persist_session_write path;
334334- persist_session_read path)
335335-336336-(* ---------- Wire compatibility with the raw Atp.Mst + Pds layer ----- *)
337337-338338-let test_wire_compat_with_pds () =
339339- (* An [Irmin.Atproto] tree committed into a PDS must be readable
340340- through the native [Pds.find] / [Pds.list] primitives without any
341341- translation layer. *)
342342- with_pds_store @@ fun store ->
343343- let t = Atproto.empty () in
344344- let t = Atproto.add t (post "p1") "{\"text\":\"a\"}" in
345345- let t = Atproto.add t (post "p2") "{\"text\":\"b\"}" in
346346- let t = Atproto.add t (profile "self") "{\"name\":\"x\"}" in
347347- let h = Atproto.commit store ~tree:t ~parents:[] ~message:"w" ~author:"a" in
348348- Atproto.set_head store ~branch:"main" h;
349349- let pds = Atproto.pds_of store in
350350- let rkeys =
351351- Pds.list pds ~collection:"app.bsky.feed.post"
352352- |> List.map fst |> List.sort String.compare
353353- in
354354- Alcotest.(check (list string))
355355- "Pds.list sees feed.post rkeys" [ "p1"; "p2" ] rkeys;
356356- Alcotest.(check bool)
357357- "Pds.find sees profile" true
358358- (Option.is_some
359359- (Pds.find pds ~collection:"app.bsky.actor.profile" ~rkey:"self"))
360360-361361-let suite =
362362- ( "atproto_tree",
363363- [
364364- Alcotest.test_case "empty store" `Quick test_empty_store;
365365- Alcotest.test_case "add then find" `Quick test_add_then_find;
366366- Alcotest.test_case "add overwrites" `Quick test_add_overwrites;
367367- Alcotest.test_case "remove" `Quick test_remove;
368368- Alcotest.test_case "remove non-existent" `Quick test_remove_nonexistent;
369369- Alcotest.test_case "leaves sorted" `Quick test_leaves_sorted;
370370- Alcotest.test_case "leaves 500 entries" `Quick test_leaves_many_entries;
371371- Alcotest.test_case "prefix scan basic" `Quick test_list_basic;
372372- Alcotest.test_case "prefix scan values" `Quick test_list_values;
373373- Alcotest.test_case "prefix scan isolation" `Quick test_list_isolation;
374374- Alcotest.test_case "hash deterministic" `Quick test_hash_deterministic;
375375- Alcotest.test_case "hash order-independent" `Quick
376376- test_hash_order_independent;
377377- Alcotest.test_case "hash changes with content" `Quick
378378- test_hash_changes_with_content;
379379- Alcotest.test_case "commit then checkout" `Quick test_commit_checkout;
380380- Alcotest.test_case "commit stores metadata" `Quick
381381- test_commit_stores_metadata;
382382- Alcotest.test_case "commit message changes hash" `Quick
383383- test_commit_message_changes_hash;
384384- Alcotest.test_case "commit parent chain" `Quick test_commit_parent_chain;
385385- Alcotest.test_case "PDS persistence across sessions" `Quick
386386- test_commit_survives_reopen_pds;
387387- Alcotest.test_case "wire-compat with Pds.*" `Quick
388388- test_wire_compat_with_pds;
389389- ] )
-2
test/test_atproto_tree.mli
···11-val suite : string * unit Alcotest.test_case list
22-(** [suite] is the alcotest test suite for [test_atproto_tree]. *)
-161
test/test_backend.ml
···11-open Irmin
22-open Private
33-44-let with_temp_dir f =
55- Eio_main.run @@ fun env ->
66- let cwd = Eio.Stdenv.cwd env in
77- Eio.Switch.run @@ fun sw ->
88- let tmp_name = Fmt.str "irmin-test-%d" (Random.int 100000) in
99- let tmp_path = Eio.Path.(cwd / tmp_name) in
1010- Eio.Path.mkdirs ~exists_ok:true ~perm:0o755 tmp_path;
1111- Fun.protect
1212- ~finally:(fun () -> Test_helpers.rm_rf tmp_path)
1313- (fun () -> f ~sw tmp_path)
1414-1515-let test_memory_backend () =
1616- let backend = Backend.Memory.sha1 () in
1717- let data = "test content" in
1818- let hash = Hash.sha1 data in
1919- Backend.write backend hash data;
2020- Alcotest.(check (option string))
2121- "read back" (Some data)
2222- (Backend.read backend hash)
2323-2424-let test_backend_refs () =
2525- let backend = Backend.Memory.sha1 () in
2626- let data = "content" in
2727- let hash = Hash.sha1 data in
2828- Backend.write backend hash data;
2929- Backend.set_ref backend "refs/heads/main" hash;
3030- Alcotest.(check bool)
3131- "ref exists" true
3232- (Option.is_some (Backend.get_ref backend "refs/heads/main"));
3333- match Backend.get_ref backend "refs/heads/main" with
3434- | Some h -> Alcotest.(check bool) "ref matches" true (Hash.equal hash h)
3535- | None -> Alcotest.fail "ref not found"
3636-3737-let test_backend_test_and_set () =
3838- let backend = Backend.Memory.sha1 () in
3939- let h1 = Hash.sha1 "content1" in
4040- let h2 = Hash.sha1 "content2" in
4141- Backend.write backend h1 "content1";
4242- Backend.write backend h2 "content2";
4343- Backend.set_ref backend "ref" h1;
4444- let result =
4545- Backend.test_and_set_ref backend "ref" ~test:(Some h2) ~set:(Some h2)
4646- in
4747- Alcotest.(check bool) "wrong test fails" false result;
4848- let result =
4949- Backend.test_and_set_ref backend "ref" ~test:(Some h1) ~set:(Some h2)
5050- in
5151- Alcotest.(check bool) "correct test succeeds" true result
5252-5353-let test_disk_backend () =
5454- with_temp_dir @@ fun ~sw tmp_path ->
5555- let backend = Backend.Disk.sha1 ~sw tmp_path in
5656- let data = "test content" in
5757- let hash = Hash.sha1 data in
5858- Backend.write backend hash data;
5959- Alcotest.(check (option string))
6060- "read back" (Some data)
6161- (Backend.read backend hash);
6262- Backend.close backend
6363-6464-let test_disk_backend_persistence () =
6565- Eio_main.run @@ fun env ->
6666- let cwd = Eio.Stdenv.cwd env in
6767- let tmp_name = Fmt.str "irmin-test-%d" (Random.int 100000) in
6868- let tmp_path = Eio.Path.(cwd / tmp_name) in
6969- let data = "persistent content" in
7070- let hash = Hash.sha1 data in
7171- Eio.Switch.run (fun sw ->
7272- let backend = Backend.Disk.sha1 ~sw tmp_path in
7373- Backend.write backend hash data;
7474- Backend.set_ref backend "refs/heads/main" hash;
7575- Backend.flush backend;
7676- Backend.close backend);
7777- Eio.Switch.run (fun sw ->
7878- let backend = Backend.Disk.sha1 ~sw tmp_path in
7979- Alcotest.(check (option string))
8080- "read after reopen" (Some data)
8181- (Backend.read backend hash);
8282- Alcotest.(check bool)
8383- "ref persisted" true
8484- (Option.is_some (Backend.get_ref backend "refs/heads/main"));
8585- Backend.close backend);
8686- Test_helpers.rm_rf tmp_path
8787-8888-let test_disk_backend_refs () =
8989- with_temp_dir @@ fun ~sw tmp_path ->
9090- let backend = Backend.Disk.sha1 ~sw tmp_path in
9191- let data = "content" in
9292- let hash = Hash.sha1 data in
9393- Backend.write backend hash data;
9494- Backend.set_ref backend "refs/heads/main" hash;
9595- Alcotest.(check bool)
9696- "ref exists" true
9797- (Option.is_some (Backend.get_ref backend "refs/heads/main"));
9898- (match Backend.get_ref backend "refs/heads/main" with
9999- | Some h -> Alcotest.(check bool) "ref matches" true (Hash.equal hash h)
100100- | None -> Alcotest.fail "ref not found");
101101- Backend.close backend
102102-103103-let test_disk_backend_write_batch () =
104104- with_temp_dir @@ fun ~sw tmp_path ->
105105- let backend = Backend.Disk.sha1 ~sw tmp_path in
106106- let objects =
107107- [
108108- (Hash.sha1 "data1", "data1");
109109- (Hash.sha1 "data2", "data2");
110110- (Hash.sha1 "data3", "data3");
111111- ]
112112- in
113113- Backend.write_batch backend objects;
114114- List.iter
115115- (fun (hash, data) ->
116116- Alcotest.(check (option string))
117117- "batch item" (Some data)
118118- (Backend.read backend hash))
119119- objects;
120120- Backend.close backend
121121-122122-let test_disk_backend_wal_recovery () =
123123- Eio_main.run @@ fun env ->
124124- let cwd = Eio.Stdenv.cwd env in
125125- let tmp_name = Fmt.str "irmin-wal-test-%d" (Random.int 100000) in
126126- let tmp_path = Eio.Path.(cwd / tmp_name) in
127127- let data = "wal recovery content" in
128128- let hash = Hash.sha1 data in
129129- Eio.Switch.run (fun sw ->
130130- let backend = Backend.Disk.sha1 ~sw tmp_path in
131131- Backend.write backend hash data;
132132- Alcotest.(check (option string))
133133- "readable before crash" (Some data)
134134- (Backend.read backend hash);
135135- Backend.close backend);
136136- Eio.Switch.run (fun sw ->
137137- let backend = Backend.Disk.sha1 ~sw tmp_path in
138138- Alcotest.(check (option string))
139139- "recovered from WAL" (Some data)
140140- (Backend.read backend hash);
141141- Alcotest.(check bool)
142142- "exists after recovery" true
143143- (Backend.exists backend hash);
144144- Backend.close backend);
145145- Test_helpers.rm_rf tmp_path
146146-147147-let suite =
148148- ( "backend",
149149- [
150150- Alcotest.test_case "memory backend" `Quick test_memory_backend;
151151- Alcotest.test_case "backend refs" `Quick test_backend_refs;
152152- Alcotest.test_case "backend test_and_set" `Quick test_backend_test_and_set;
153153- Alcotest.test_case "disk backend" `Quick test_disk_backend;
154154- Alcotest.test_case "disk backend persistence" `Quick
155155- test_disk_backend_persistence;
156156- Alcotest.test_case "disk backend refs" `Quick test_disk_backend_refs;
157157- Alcotest.test_case "disk backend write_batch" `Quick
158158- test_disk_backend_write_batch;
159159- Alcotest.test_case "disk backend WAL recovery" `Quick
160160- test_disk_backend_wal_recovery;
161161- ] )
-2
test/test_backend.mli
···11-val suite : string * unit Alcotest.test_case list
22-(** Test suite. *)
-32
test/test_codec.ml
···11-open Irmin
22-open Private
33-44-let test_git_tree_format () =
55- let node = Codec.Git.empty_node in
66- Alcotest.(check bool) "empty is empty" true (Codec.Git.is_empty node);
77- let h = Hash.sha1 "content" in
88- let node = Codec.Git.add node "file.txt" (`Contents h) in
99- Alcotest.(check bool) "not empty after add" false (Codec.Git.is_empty node);
1010- match Codec.Git.find node "file.txt" with
1111- | Some (`Contents h') ->
1212- Alcotest.(check bool) "find matches" true (Hash.equal h h')
1313- | _ -> Alcotest.fail "entry not found"
1414-1515-let test_git_tree_serialization () =
1616- let h = Hash.sha1 "content" in
1717- let node = Codec.Git.empty_node in
1818- let node = Codec.Git.add node "file.txt" (`Contents h) in
1919- let bytes = Codec.Git.bytes_of_node node in
2020- match Codec.Git.node_of_bytes bytes with
2121- | Ok node' ->
2222- let entries = Codec.Git.list node' in
2323- Alcotest.(check int) "one entry" 1 (List.length entries)
2424- | Error (`Msg msg) -> Alcotest.fail msg
2525-2626-let suite =
2727- ( "codec",
2828- [
2929- Alcotest.test_case "git tree format" `Quick test_git_tree_format;
3030- Alcotest.test_case "git tree serialization" `Quick
3131- test_git_tree_serialization;
3232- ] )
-2
test/test_codec.mli
···11-val suite : string * unit Alcotest.test_case list
22-(** Test suite. *)
-72
test/test_commit.ml
···11-open Irmin
22-open Private
33-44-let test_commit_fields () =
55- let tree_hash = Hash.sha1 "tree content" in
66- let c =
77- Commit.Git.v ~tree:tree_hash ~parents:[] ~author:"Alice"
88- ~message:"Initial commit" ()
99- in
1010- Alcotest.(check string) "author" "Alice" (Commit.Git.author c);
1111- Alcotest.(check string) "message" "Initial commit" (Commit.Git.message c);
1212- Alcotest.(check (list (Alcotest.testable Hash.pp Hash.equal)))
1313- "no parents" [] (Commit.Git.parents c);
1414- Alcotest.(check bool)
1515- "tree matches" true
1616- (Hash.equal tree_hash (Commit.Git.tree c))
1717-1818-let test_commit_committer () =
1919- let tree_hash = Hash.sha1 "tree" in
2020- let c =
2121- Commit.Git.v ~tree:tree_hash ~parents:[] ~author:"Alice" ~committer:"Bob"
2222- ~message:"test" ()
2323- in
2424- Alcotest.(check string) "committer" "Bob" (Commit.Git.committer c)
2525-2626-let test_commit_parents () =
2727- let tree_hash = Hash.sha1 "tree" in
2828- let parent1 = Hash.sha1 "parent1" in
2929- let parent2 = Hash.sha1 "parent2" in
3030- let c =
3131- Commit.Git.v ~tree:tree_hash ~parents:[ parent1; parent2 ] ~author:"test"
3232- ~message:"merge" ()
3333- in
3434- Alcotest.(check int) "two parents" 2 (List.length (Commit.Git.parents c))
3535-3636-let test_commit_roundtrip () =
3737- let tree_hash = Hash.sha1 "tree content" in
3838- let c =
3939- Commit.Git.v ~tree:tree_hash ~parents:[] ~author:"Alice"
4040- ~message:"test commit" ()
4141- in
4242- let bytes = Commit.Git.to_bytes c in
4343- match Commit.Git.of_bytes bytes with
4444- | Ok c' ->
4545- Alcotest.(check string) "author roundtrip" "Alice" (Commit.Git.author c');
4646- Alcotest.(check string)
4747- "message roundtrip" "test commit" (Commit.Git.message c')
4848- | Error (`Msg msg) -> Alcotest.fail msg
4949-5050-let test_commit_hash () =
5151- let tree_hash = Hash.sha1 "tree" in
5252- let c1 =
5353- Commit.Git.v ~tree:tree_hash ~parents:[] ~author:"Alice" ~message:"same"
5454- ~timestamp:1000L ()
5555- in
5656- let c2 =
5757- Commit.Git.v ~tree:tree_hash ~parents:[] ~author:"Alice" ~message:"same"
5858- ~timestamp:1000L ()
5959- in
6060- Alcotest.(check bool)
6161- "same content same hash" true
6262- (Hash.equal (Commit.Git.hash c1) (Commit.Git.hash c2))
6363-6464-let suite =
6565- ( "commit",
6666- [
6767- Alcotest.test_case "fields" `Quick test_commit_fields;
6868- Alcotest.test_case "committer" `Quick test_commit_committer;
6969- Alcotest.test_case "parents" `Quick test_commit_parents;
7070- Alcotest.test_case "roundtrip" `Quick test_commit_roundtrip;
7171- Alcotest.test_case "hash" `Quick test_commit_hash;
7272- ] )
-2
test/test_commit.mli
···11-val suite : string * unit Alcotest.test_case list
22-(** Test suite. *)
-104
test/test_git_interop.ml
···11-open Irmin
22-open Private
33-44-let with_temp_dir f =
55- Eio_main.run @@ fun env ->
66- let fs = Eio.Stdenv.fs env in
77- let cwd = Eio.Stdenv.cwd env in
88- Eio.Switch.run @@ fun sw ->
99- let tmp_name = Fmt.str "irmin-git-test-%d" (Random.int 100000) in
1010- let tmp_path = Eio.Path.(cwd / tmp_name) in
1111- Eio.Path.mkdirs ~exists_ok:true ~perm:0o755 tmp_path;
1212- Fun.protect
1313- ~finally:(fun () -> Test_helpers.rm_rf tmp_path)
1414- (fun () -> f ~sw ~fs tmp_path)
1515-1616-let test_init_git () =
1717- with_temp_dir @@ fun ~sw ~fs tmp_path ->
1818- let fpath = Fpath.v (Eio.Path.native_exn tmp_path) in
1919- let _store = Git.init ~sw ~fs ~path:fpath in
2020- let git_dir = Eio.Path.(tmp_path / ".git") in
2121- Alcotest.(check bool) "git dir exists" true (Eio.Path.is_directory git_dir)
2222-2323-let test_write_read_object () =
2424- with_temp_dir @@ fun ~sw ~fs tmp_path ->
2525- let fpath = Fpath.v (Eio.Path.native_exn tmp_path) in
2626- let _store = Git.init ~sw ~fs ~path:fpath in
2727- let git_dir = Fpath.(fpath / ".git") in
2828- let data = "hello world" in
2929- let hash = Git.write_object ~sw ~fs ~git_dir ~typ:"blob" data in
3030- match Git.read_object ~sw ~fs ~git_dir hash with
3131- | Ok (typ, content) ->
3232- Alcotest.(check string) "type" "blob" typ;
3333- Alcotest.(check string) "content" data content
3434- | Error (`Msg msg) -> Alcotest.fail msg
3535-3636-let test_write_read_ref () =
3737- with_temp_dir @@ fun ~sw ~fs tmp_path ->
3838- let fpath = Fpath.v (Eio.Path.native_exn tmp_path) in
3939- let _store = Git.init ~sw ~fs ~path:fpath in
4040- let git_dir = Fpath.(fpath / ".git") in
4141- let hash = Git.write_object ~sw ~fs ~git_dir ~typ:"blob" "content" in
4242- Git.write_ref ~sw ~fs ~git_dir "refs/heads/test" hash;
4343- match Git.read_ref ~sw ~fs ~git_dir "refs/heads/test" with
4444- | Some h -> Alcotest.(check bool) "ref matches" true (Irmin.Hash.equal hash h)
4545- | None -> Alcotest.fail "ref not found"
4646-4747-(* Regression: Repository.init used mkdir (non-recursive), failing when parent
4848- dirs don't exist. Fixed to mkdirs. *)
4949-let test_init_nested_path () =
5050- with_temp_dir @@ fun ~sw ~fs tmp_path ->
5151- let nested = Eio.Path.native_exn tmp_path ^ "/a/b/repo" in
5252- let fpath = Fpath.v nested in
5353- let _store = Git.init ~sw ~fs ~path:fpath in
5454- let git_dir = Eio.Path.(fs / (nested ^ "/.git")) in
5555- Alcotest.(check bool)
5656- "git dir in nested path" true
5757- (Eio.Path.is_directory git_dir)
5858-5959-(* Regression: git_backend.write called Repository.write unconditionally,
6060- failing on duplicate objects. Fixed to skip if already exists. *)
6161-let test_write_duplicate_object () =
6262- with_temp_dir @@ fun ~sw ~fs tmp_path ->
6363- let fpath = Fpath.v (Eio.Path.native_exn tmp_path) in
6464- let store = Irmin.Git.init ~sw ~fs ~path:fpath in
6565- let tree = Irmin.Tree.add Irmin.Tree.empty [ "file.txt" ] "hello world" in
6666- let _ =
6767- Irmin.commit store ~tree ~parents:[] ~message:"first" ~author:"test"
6868- in
6969- (* Committing the same tree again triggers duplicate backend writes — must not raise *)
7070- let _ =
7171- Irmin.commit store ~tree ~parents:[] ~message:"again" ~author:"test"
7272- in
7373- Alcotest.(check bool) "double commit did not raise" true true
7474-7575-(* Integration: write commits to disk, reopen, read back. *)
7676-let test_store_git_roundtrip () =
7777- with_temp_dir @@ fun ~sw ~fs tmp_path ->
7878- let fpath = Fpath.v (Eio.Path.native_exn tmp_path) in
7979- let store = Irmin.Git.init ~sw ~fs ~path:fpath in
8080- let tree = Irmin.Tree.add Irmin.Tree.empty [ "README.md" ] "# Hello" in
8181- let h = Irmin.commit store ~tree ~parents:[] ~message:"init" ~author:"test" in
8282- Irmin.set_head store ~branch:"main" h;
8383- let store2 = Irmin.Git.open_ ~sw ~fs ~path:fpath in
8484- Alcotest.(check bool)
8585- "head survived reopen" true
8686- (Irmin.head store2 ~branch:"main" = Some h);
8787- match Irmin.checkout store2 ~branch:"main" with
8888- | None -> Alcotest.fail "checkout failed"
8989- | Some tree2 ->
9090- Alcotest.(check (option string))
9191- "content survived reopen" (Some "# Hello")
9292- (Irmin.Tree.find tree2 [ "README.md" ])
9393-9494-let suite =
9595- ( "git_interop",
9696- [
9797- Alcotest.test_case "init git" `Quick test_init_git;
9898- Alcotest.test_case "write/read object" `Quick test_write_read_object;
9999- Alcotest.test_case "write/read ref" `Quick test_write_read_ref;
100100- Alcotest.test_case "init nested path" `Quick test_init_nested_path;
101101- Alcotest.test_case "write duplicate object" `Quick
102102- test_write_duplicate_object;
103103- Alcotest.test_case "store roundtrip" `Quick test_store_git_roundtrip;
104104- ] )
-2
test/test_git_interop.mli
···11-val suite : string * unit Alcotest.test_case list
22-(** Test suite. *)
-105
test/test_link.ml
···11-open Irmin
22-open Private
33-44-let test_link_v_get () =
55- let s = Link.Mst.v () in
66- let l = Link.v s 42 in
77- Alcotest.(check int) "get (v s x) = x" 42 (Link.get l)
88-99-let test_link_is_val () =
1010- let s = Link.Mst.v () in
1111- let l = Link.v s "hello" in
1212- Alcotest.(check bool) "in-memory is_val" true (Link.is_val l)
1313-1414-let test_link_equal () =
1515- let s = Link.Mst.v () in
1616- let l0 = Link.v s [ 1; 2; 3 ] in
1717- let l1 = Link.v s [ 1; 2; 3 ] in
1818- let l2 = Link.v s [ 1; 2; 4 ] in
1919- Alcotest.(check bool) "same value equal" true (Link.equal l0 l1);
2020- Alcotest.(check bool) "diff value not equal" false (Link.equal l0 l2)
2121-2222-let test_link_address () =
2323- let s = Link.Mst.v () in
2424- let l0 = Link.v s "test" in
2525- let l1 = Link.v s "test" in
2626- Alcotest.(check bool) "same address" true (Link.address l0 = Link.address l1)
2727-2828-let test_link_pp () =
2929- let s = Link.Mst.v () in
3030- let l = Link.v s "test" in
3131- let _ = Link.address l in
3232- let str = Fmt.str "%a" Link.pp l in
3333- Alcotest.(check int) "pp is 7 chars" 7 (String.length str)
3434-3535-let test_link_read_write () =
3636- let s : int Link.store = Link.Mst.v () in
3737- Link.write s 42;
3838- Alcotest.(check int) "after write" 42 (Link.read s);
3939- Link.write s 100;
4040- Alcotest.(check int) "after second write" 100 (Link.read s)
4141-4242-let test_link_is_open () =
4343- let s = Link.Mst.v () in
4444- Alcotest.(check bool) "initially open" true (Link.is_open s);
4545- Link.close s;
4646- Alcotest.(check bool) "closed after close" false (Link.is_open s)
4747-4848-type test_tree = test_node Link.t
4949-and test_node = TEmpty | TNode of { l : test_tree; x : int; r : test_tree }
5050-5151-let test_link_tree () =
5252- let s = Link.Mst.v () in
5353- let empty = Link.v s TEmpty in
5454- let leaf x = Link.v s (TNode { l = empty; x; r = empty }) in
5555- let node l x r = Link.v s (TNode { l; x; r }) in
5656- let t = node (leaf 1) 2 (leaf 3) in
5757- match Link.get t with
5858- | TEmpty -> Alcotest.fail "expected node"
5959- | TNode n -> (
6060- Alcotest.(check int) "root" 2 n.x;
6161- match (Link.get n.l, Link.get n.r) with
6262- | TNode l, TNode r ->
6363- Alcotest.(check int) "left" 1 l.x;
6464- Alcotest.(check int) "right" 3 r.x
6565- | _ -> Alcotest.fail "expected leaves")
6666-6767-(* of_backend: links persist through the backend and can be fetched by address
6868- using a second store instance backed by the same backend. *)
6969-let test_link_of_backend_persist () =
7070- let backend = Backend.Memory.cid () in
7171- let s : int Link.store = Link.Mst.of_backend backend in
7272- let l = Link.v s 42 in
7373- let addr = Link.address l in
7474- (* Second store wired to the same backend *)
7575- let s2 : int Link.store = Link.Mst.of_backend backend in
7676- let l2 = Link.of_address s2 addr in
7777- Alcotest.(check int) "fetched from backend" 42 (Link.get l2)
7878-7979-(* of_backend with Git codec *)
8080-let test_link_of_backend_git () =
8181- let backend = Backend.Memory.sha1 () in
8282- let s : string Link.store = Link.Git.of_backend backend in
8383- let l = Link.v s "hello" in
8484- let addr = Link.address l in
8585- let s2 : string Link.store = Link.Git.of_backend backend in
8686- Alcotest.(check string)
8787- "fetched via git backend" "hello"
8888- (Link.get (Link.of_address s2 addr))
8989-9090-let suite =
9191- ( "link",
9292- [
9393- Alcotest.test_case "v/get" `Quick test_link_v_get;
9494- Alcotest.test_case "is_val" `Quick test_link_is_val;
9595- Alcotest.test_case "equal" `Quick test_link_equal;
9696- Alcotest.test_case "address" `Quick test_link_address;
9797- Alcotest.test_case "pp" `Quick test_link_pp;
9898- Alcotest.test_case "read/write" `Quick test_link_read_write;
9999- Alcotest.test_case "is_open" `Quick test_link_is_open;
100100- Alcotest.test_case "tree" `Quick test_link_tree;
101101- Alcotest.test_case "of_backend persist (mst)" `Quick
102102- test_link_of_backend_persist;
103103- Alcotest.test_case "of_backend persist (git)" `Quick
104104- test_link_of_backend_git;
105105- ] )
-2
test/test_link.mli
···11-val suite : string * unit Alcotest.test_case list
22-(** Test suite. *)
-501
test/test_pds_interop.ml
···11-(** PDS interop tests — verify Irmin can read PDS stores and vice versa.
22-33- These tests exercise the data path directly: writing via [Pds] API and
44- reading via [Atp.Mst] + blockstore (the same code path the Irmin CLI PDS
55- backend uses), and vice versa. *)
66-77-open Irmin
88-open Private
99-1010-let test_did = Atp.Did.of_string_exn "did:web:example.com"
1111-1212-let with_temp_dir f =
1313- Eio_main.run @@ fun env ->
1414- let cwd = Eio.Stdenv.cwd env in
1515- let tmp_dir = Eio.Path.(cwd / "_build" / "test_pds_interop") in
1616- (try Eio.Path.mkdirs ~exists_ok:true ~perm:0o755 tmp_dir with Eio.Io _ -> ());
1717- let name = Fmt.str "repo_%d" (Random.int 1_000_000) in
1818- let path = Eio.Path.(tmp_dir / name) in
1919- Fun.protect
2020- ~finally:(fun () -> try Helpers.rm_rf path with Eio.Io _ -> ())
2121- (fun () -> f path)
2222-2323-(* ---- PDS write → MST read ---- *)
2424-2525-let test_pds_mst_read_record () =
2626- with_temp_dir @@ fun path ->
2727- Eio.Switch.run @@ fun sw ->
2828- let pds = Pds.v ~sw path ~did:test_did in
2929- let data =
3030- Atp.Dagcbor.encode_string ~cid_format:`Atproto
3131- (`Map [ ("text", `String "Hello from PDS"); ("num", `Int 42L) ])
3232- in
3333- Pds.put pds ~collection:"app.bsky.feed.post" ~rkey:"post1" data;
3434- (* Read back via MST traversal *)
3535- let found = Pds_interop.mst_find pds "app.bsky.feed.post/post1" in
3636- Alcotest.(check (option string)) "MST finds PDS record" (Some data) found
3737-3838-let test_pds_mst_list_collections () =
3939- with_temp_dir @@ fun path ->
4040- Eio.Switch.run @@ fun sw ->
4141- let pds = Pds.v ~sw path ~did:test_did in
4242- let post = Atp.Dagcbor.encode_string ~cid_format:`Atproto (`String "post") in
4343- let like = Atp.Dagcbor.encode_string ~cid_format:`Atproto (`String "like") in
4444- let profile =
4545- Atp.Dagcbor.encode_string ~cid_format:`Atproto (`String "profile")
4646- in
4747- Pds.put pds ~collection:"app.bsky.feed.post" ~rkey:"a" post;
4848- Pds.put pds ~collection:"app.bsky.feed.like" ~rkey:"b" like;
4949- Pds.put pds ~collection:"app.bsky.actor.profile" ~rkey:"self" profile;
5050- (* List all MST leaves *)
5151- match Pds.checkout pds with
5252- | None -> Alcotest.fail "checkout returned None"
5353- | Some mst ->
5454- let bs = (Pds.blockstore pds :> Atp.Blockstore.readable) in
5555- let all_keys =
5656- Atp.Mst.leaves mst ~store:bs |> Seq.map fst |> List.of_seq
5757- in
5858- Alcotest.(check int) "3 leaves total" 3 (List.length all_keys);
5959- let has key = List.mem key all_keys in
6060- Alcotest.(check bool) "has post" true (has "app.bsky.feed.post/a");
6161- Alcotest.(check bool) "has like" true (has "app.bsky.feed.like/b");
6262- Alcotest.(check bool)
6363- "has profile" true
6464- (has "app.bsky.actor.profile/self")
6565-6666-let test_pds_write_mst_head () =
6767- with_temp_dir @@ fun path ->
6868- Eio.Switch.run @@ fun sw ->
6969- let pds = Pds.v ~sw path ~did:test_did in
7070- let data = Atp.Dagcbor.encode_string ~cid_format:`Atproto (`String "x") in
7171- Pds.put pds ~collection:"test" ~rkey:"k" data;
7272- let head = Pds.head pds in
7373- Alcotest.(check bool) "head exists" true (Option.is_some head)
7474-7575-(* ---- MST write → PDS read ---- *)
7676-7777-let test_mst_pds_read_record () =
7878- with_temp_dir @@ fun path ->
7979- Eio.Switch.run @@ fun sw ->
8080- let pds = Pds.v ~sw path ~did:test_did in
8181- let data =
8282- Atp.Dagcbor.encode_string ~cid_format:`Atproto
8383- (`Map [ ("text", `String "Written via MST") ])
8484- in
8585- Pds_interop.mst_add pds ~collection:"app.bsky.feed.post" ~rkey:"mst1" data;
8686- (* Read back via PDS API *)
8787- let result = Pds.find pds ~collection:"app.bsky.feed.post" ~rkey:"mst1" in
8888- Alcotest.(check (option string))
8989- "PDS reads MST-written record" (Some data) result
9090-9191-let test_mst_write_pds_list () =
9292- with_temp_dir @@ fun path ->
9393- Eio.Switch.run @@ fun sw ->
9494- let pds = Pds.v ~sw path ~did:test_did in
9595- let data1 = Atp.Dagcbor.encode_string ~cid_format:`Atproto (`String "rec1") in
9696- let data2 = Atp.Dagcbor.encode_string ~cid_format:`Atproto (`String "rec2") in
9797- Pds_interop.mst_add pds ~collection:"my.collection" ~rkey:"a" data1;
9898- Pds_interop.mst_add pds ~collection:"my.collection" ~rkey:"b" data2;
9999- let records = Pds.list pds ~collection:"my.collection" in
100100- let rkeys = List.map fst records |> List.sort String.compare in
101101- Alcotest.(check (list string))
102102- "PDS lists MST-written records" [ "a"; "b" ] rkeys
103103-104104-(* ---- Roundtrip: PDS → MST → PDS ---- *)
105105-106106-let test_roundtrip_modify () =
107107- with_temp_dir @@ fun path ->
108108- Eio.Switch.run @@ fun sw ->
109109- let pds = Pds.v ~sw path ~did:test_did in
110110- let original =
111111- Atp.Dagcbor.encode_string ~cid_format:`Atproto
112112- (`Map [ ("text", `String "original") ])
113113- in
114114- let updated =
115115- Atp.Dagcbor.encode_string ~cid_format:`Atproto
116116- (`Map [ ("text", `String "updated via MST") ])
117117- in
118118- (* PDS: create initial data *)
119119- Pds.put pds ~collection:"test.col" ~rkey:"key1" original;
120120- (* MST: modify the record *)
121121- Pds_interop.mst_add pds ~collection:"test.col" ~rkey:"key1" updated;
122122- (* PDS: verify the update *)
123123- let result = Pds.find pds ~collection:"test.col" ~rkey:"key1" in
124124- Alcotest.(check (option string)) "roundtrip update" (Some updated) result
125125-126126-let test_roundtrip_add_delete () =
127127- with_temp_dir @@ fun path ->
128128- Eio.Switch.run @@ fun sw ->
129129- let pds = Pds.v ~sw path ~did:test_did in
130130- let data =
131131- Atp.Dagcbor.encode_string ~cid_format:`Atproto (`String "keepme")
132132- in
133133- let extra =
134134- Atp.Dagcbor.encode_string ~cid_format:`Atproto (`String "deleteme")
135135- in
136136- (* PDS: create 2 records *)
137137- Pds.put pds ~collection:"col" ~rkey:"keep" data;
138138- Pds.put pds ~collection:"col" ~rkey:"delete" extra;
139139- (* MST: delete one, add another *)
140140- let new_data =
141141- Atp.Dagcbor.encode_string ~cid_format:`Atproto (`String "newrecord")
142142- in
143143- Pds_interop.mst_remove pds ~collection:"col" ~rkey:"delete";
144144- Pds_interop.mst_add pds ~collection:"col" ~rkey:"added" new_data;
145145- (* PDS: verify final state *)
146146- let records = Pds.list pds ~collection:"col" in
147147- let rkeys = List.map fst records |> List.sort String.compare in
148148- let kept = Pds.find pds ~collection:"col" ~rkey:"keep" in
149149- let deleted = Pds.find pds ~collection:"col" ~rkey:"delete" in
150150- let added = Pds.find pds ~collection:"col" ~rkey:"added" in
151151- Alcotest.(check (list string)) "final rkeys" [ "added"; "keep" ] rkeys;
152152- Alcotest.(check (option string)) "kept record" (Some data) kept;
153153- Alcotest.(check (option string)) "deleted record gone" None deleted;
154154- Alcotest.(check (option string)) "added record" (Some new_data) added
155155-156156-(* ---- CAR export interop ---- *)
157157-158158-let test_car_export_reimport () =
159159- with_temp_dir @@ fun path ->
160160- Eio.Switch.run @@ fun sw ->
161161- let pds = Pds.v ~sw path ~did:test_did in
162162- let data =
163163- Atp.Dagcbor.encode_string ~cid_format:`Atproto
164164- (`Map [ ("text", `String "car test") ])
165165- in
166166- Pds.put pds ~collection:"test" ~rkey:"rec1" data;
167167- let head_before = Pds.head pds in
168168- (* Export CAR *)
169169- let car_data = Pds.export_car pds in
170170- Alcotest.(check bool) "CAR not empty" true (String.length car_data > 0);
171171- (* Parse and verify CAR structure *)
172172- let header, blocks = Atp.Car.of_string ~cid_format:`Atproto car_data in
173173- Alcotest.(check int) "CAR v1" 1 header.version;
174174- Alcotest.(check bool) "has roots" true (List.length header.roots > 0);
175175- Alcotest.(check bool) "has blocks" true (List.length blocks > 0);
176176- (* Root should match HEAD *)
177177- match (head_before, header.roots) with
178178- | Some head_cid, [ root ] ->
179179- Alcotest.(check string)
180180- "CAR root matches HEAD"
181181- (Atp.Cid.to_string head_cid)
182182- (Atp.Cid.to_string root)
183183- | _ -> ()
184184-185185-let test_car_import_then_read () =
186186- Eio_main.run @@ fun env ->
187187- let cwd = Eio.Stdenv.cwd env in
188188- let tmp_dir = Eio.Path.(cwd / "_build" / "test_pds_interop") in
189189- (try Eio.Path.mkdirs ~exists_ok:true ~perm:0o755 tmp_dir with Eio.Io _ -> ());
190190- let name1 = Fmt.str "export_%d" (Random.int 1_000_000) in
191191- let name2 = Fmt.str "import_%d" (Random.int 1_000_000) in
192192- let path1 = Eio.Path.(tmp_dir / name1) in
193193- let path2 = Eio.Path.(tmp_dir / name2) in
194194- Fun.protect
195195- ~finally:(fun () ->
196196- (try Helpers.rm_rf path1 with Eio.Io _ -> ());
197197- try Helpers.rm_rf path2 with Eio.Io _ -> ())
198198- (fun () ->
199199- (* Create source repo with data *)
200200- let car_data =
201201- Eio.Switch.run @@ fun sw ->
202202- let pds = Pds.v ~sw path1 ~did:test_did in
203203- let data =
204204- Atp.Dagcbor.encode_string ~cid_format:`Atproto (`String "imported")
205205- in
206206- Pds.put pds ~collection:"test" ~rkey:"x" data;
207207- let car = Pds.export_car pds in
208208- Pds.close pds;
209209- car
210210- in
211211- (* Import into fresh repo *)
212212- Eio.Switch.run @@ fun sw ->
213213- let pds2 = Pds.v ~sw path2 ~did:test_did in
214214- let count = Pds.import_car pds2 car_data in
215215- Pds.close pds2;
216216- Alcotest.(check bool) "imported blocks" true (count > 0))
217217-218218-(* ---- Persistence across sessions ---- *)
219219-220220-let test_persistence_across_sessions () =
221221- Eio_main.run @@ fun env ->
222222- let cwd = Eio.Stdenv.cwd env in
223223- let tmp_dir = Eio.Path.(cwd / "_build" / "test_pds_interop") in
224224- (try Eio.Path.mkdirs ~exists_ok:true ~perm:0o755 tmp_dir with Eio.Io _ -> ());
225225- let name = Fmt.str "persist_%d" (Random.int 1_000_000) in
226226- let path = Eio.Path.(tmp_dir / name) in
227227- Fun.protect
228228- ~finally:(fun () -> try Helpers.rm_rf path with Eio.Io _ -> ())
229229- (fun () ->
230230- let data =
231231- Atp.Dagcbor.encode_string ~cid_format:`Atproto
232232- (`Map [ ("content", `String "persistent") ])
233233- in
234234- (* Session 1: write via PDS *)
235235- Eio.Switch.run (fun sw ->
236236- let pds = Pds.v ~sw path ~did:test_did in
237237- Pds.put pds ~collection:"test" ~rkey:"persist" data;
238238- Pds.close pds);
239239- (* Session 2: read via MST traversal *)
240240- let found1 =
241241- Eio.Switch.run @@ fun sw ->
242242- let pds = Pds.open_ ~sw path in
243243- let result = Pds_interop.mst_find pds "test/persist" in
244244- Pds.close pds;
245245- result
246246- in
247247- Alcotest.(check (option string)) "session 2 reads" (Some data) found1;
248248- (* Session 3: read via PDS API *)
249249- let found2 =
250250- Eio.Switch.run @@ fun sw ->
251251- let pds = Pds.open_ ~sw path in
252252- let result = Pds.find pds ~collection:"test" ~rkey:"persist" in
253253- Pds.close pds;
254254- result
255255- in
256256- Alcotest.(check (option string)) "session 3 reads" (Some data) found2)
257257-258258-(* ---- DID preservation ---- *)
259259-260260-let test_did_preserved () =
261261- Eio_main.run @@ fun env ->
262262- let cwd = Eio.Stdenv.cwd env in
263263- let tmp_dir = Eio.Path.(cwd / "_build" / "test_pds_interop") in
264264- (try Eio.Path.mkdirs ~exists_ok:true ~perm:0o755 tmp_dir with Eio.Io _ -> ());
265265- let name = Fmt.str "did_%d" (Random.int 1_000_000) in
266266- let path = Eio.Path.(tmp_dir / name) in
267267- Fun.protect
268268- ~finally:(fun () -> try Helpers.rm_rf path with Eio.Io _ -> ())
269269- (fun () ->
270270- let custom_did = Atp.Did.of_string_exn "did:plc:abc123xyz" in
271271- Eio.Switch.run (fun sw ->
272272- let pds = Pds.v ~sw path ~did:custom_did in
273273- Pds.close pds);
274274- Eio.Switch.run @@ fun sw ->
275275- let pds = Pds.open_ ~sw path in
276276- let did = Pds.did pds in
277277- Pds.close pds;
278278- Alcotest.(check string)
279279- "DID preserved" "did:plc:abc123xyz" (Atp.Did.to_string did))
280280-281281-(* ---- Empty store ---- *)
282282-283283-let test_empty_pds_checkout () =
284284- with_temp_dir @@ fun path ->
285285- Eio.Switch.run @@ fun sw ->
286286- let pds = Pds.v ~sw path ~did:test_did in
287287- let mst = Pds.checkout pds in
288288- Alcotest.(check bool) "empty checkout is None" true (Option.is_none mst)
289289-290290-let test_empty_pds_head () =
291291- with_temp_dir @@ fun path ->
292292- Eio.Switch.run @@ fun sw ->
293293- let pds = Pds.v ~sw path ~did:test_did in
294294- let head = Pds.head pds in
295295- Alcotest.(check bool) "empty head is None" true (Option.is_none head);
296296- ignore pds
297297-298298-(* ---- Large collection ---- *)
299299-300300-let test_many_records () =
301301- with_temp_dir @@ fun path ->
302302- Eio.Switch.run @@ fun sw ->
303303- let pds = Pds.v ~sw path ~did:test_did in
304304- let n = 100 in
305305- (* Write N records via PDS *)
306306- for i = 0 to n - 1 do
307307- let data =
308308- Atp.Dagcbor.encode_string ~cid_format:`Atproto (`Int (Int64.of_int i))
309309- in
310310- Pds.put pds ~collection:"b" ~rkey:(Fmt.str "%d" i) data
311311- done;
312312- (* Read all via MST *)
313313- let keys = Pds_interop.mst_list_prefix pds "b/" in
314314- Alcotest.(check int) "100 records via MST" n (List.length keys);
315315- (* Spot-check via PDS API *)
316316- let check i =
317317- let expected =
318318- Atp.Dagcbor.encode_string ~cid_format:`Atproto (`Int (Int64.of_int i))
319319- in
320320- let rkey = Fmt.str "%d" i in
321321- let found = Pds.find pds ~collection:"b" ~rkey in
322322- Alcotest.(check (option string))
323323- (Fmt.str "record %d" i) (Some expected) found
324324- in
325325- check 0;
326326- check 50;
327327- check 99
328328-329329-(* ---- MST leaf ordering consistency ---- *)
330330-331331-let test_leaf_order_matches () =
332332- with_temp_dir @@ fun path ->
333333- Eio.Switch.run @@ fun sw ->
334334- let pds = Pds.v ~sw path ~did:test_did in
335335- (* Write records with keys that test sort order *)
336336- let keys = [ "zzz"; "aaa"; "mmm"; "abc"; "xyz" ] in
337337- List.iter
338338- (fun k ->
339339- let data = Atp.Dagcbor.encode_string ~cid_format:`Atproto (`String k) in
340340- Pds.put pds ~collection:"order" ~rkey:k data)
341341- keys;
342342- (* Read via PDS list *)
343343- let pds_keys = Pds.list pds ~collection:"order" |> List.map fst in
344344- (* Read via MST leaves *)
345345- let mst_keys = Pds_interop.mst_list_prefix pds "order/" in
346346- (* Both should be sorted identically *)
347347- Alcotest.(check (list string)) "PDS and MST agree on order" pds_keys mst_keys
348348-349349-(* ---- Multiple collections interop ---- *)
350350-351351-let test_multiple_collections_interop () =
352352- with_temp_dir @@ fun path ->
353353- Eio.Switch.run @@ fun sw ->
354354- let pds = Pds.v ~sw path ~did:test_did in
355355- (* Write some via PDS, some via MST *)
356356- let pds_data =
357357- Atp.Dagcbor.encode_string ~cid_format:`Atproto (`String "via-pds")
358358- in
359359- let mst_data =
360360- Atp.Dagcbor.encode_string ~cid_format:`Atproto (`String "via-mst")
361361- in
362362- Pds.put pds ~collection:"col.a" ~rkey:"r1" pds_data;
363363- Pds_interop.mst_add pds ~collection:"col.b" ~rkey:"r2" mst_data;
364364- (* Both should be readable via PDS *)
365365- let a = Pds.find pds ~collection:"col.a" ~rkey:"r1" in
366366- let b = Pds.find pds ~collection:"col.b" ~rkey:"r2" in
367367- Alcotest.(check (option string)) "col.a via PDS" (Some pds_data) a;
368368- Alcotest.(check (option string)) "col.b via PDS" (Some mst_data) b;
369369- (* Both should be readable via MST *)
370370- let a' = Pds_interop.mst_find pds "col.a/r1" in
371371- let b' = Pds_interop.mst_find pds "col.b/r2" in
372372- Alcotest.(check (option string)) "col.a via MST" (Some pds_data) a';
373373- Alcotest.(check (option string)) "col.b via MST" (Some mst_data) b'
374374-375375-(* ---- DAG-CBOR content integrity ---- *)
376376-377377-let test_dagcbor_integrity () =
378378- with_temp_dir @@ fun path ->
379379- Eio.Switch.run @@ fun sw ->
380380- let pds = Pds.v ~sw path ~did:test_did in
381381- (* Write a complex DAG-CBOR record *)
382382- let complex =
383383- Atp.Dagcbor.encode_string ~cid_format:`Atproto
384384- (`Map
385385- [
386386- ("text", `String "Hello world");
387387- ("createdAt", `String "2025-01-01T00:00:00Z");
388388- ("langs", `List [ `String "en"; `String "fr" ]);
389389- ("nested", `Map [ ("key", `Int 123L); ("flag", `Bool true) ]);
390390- ])
391391- in
392392- Pds.put pds ~collection:"app.bsky.feed.post" ~rkey:"complex1" complex;
393393- (* Read back via MST and decode *)
394394- let found = Pds_interop.mst_find pds "app.bsky.feed.post/complex1" in
395395- match found with
396396- | None -> Alcotest.fail "record not found"
397397- | Some raw -> (
398398- Alcotest.(check string) "raw bytes match" complex raw;
399399- (* Verify it decodes correctly *)
400400- let decoded = Atp.Dagcbor.decode_string ~cid_format:`Atproto raw in
401401- match decoded with
402402- | `Map fields ->
403403- let text = List.assoc_opt "text" fields in
404404- Alcotest.(check bool)
405405- "has text field" true
406406- (text = Some (`String "Hello world"))
407407- | _ -> Alcotest.fail "expected Map")
408408-409409-(* ---- Irmin store API on PDS ---- *)
410410-411411-let test_pds_store_main_branch () =
412412- with_temp_dir @@ fun path ->
413413- Eio.Switch.run @@ fun sw ->
414414- let pds = Pds.v ~sw path ~did:test_did in
415415- let store = Irmin.Atproto.(of_pds pds |> v) in
416416- let tree = Irmin.Tree.add Irmin.Tree.empty [ "key" ] "value" in
417417- let h = Irmin.commit store ~tree ~parents:[] ~message:"init" ~author:"test" in
418418- Irmin.set_head store ~branch:"main" h;
419419- Alcotest.(check bool) "has main" true (List.mem "main" (Irmin.branches store));
420420- match Irmin.head store ~branch:"main" with
421421- | Some h' -> Alcotest.(check bool) "head set" true (Irmin.Hash.equal h h')
422422- | None -> Alcotest.fail "head should exist"
423423-424424-let test_pds_store_multiple_branches () =
425425- with_temp_dir @@ fun path ->
426426- Eio.Switch.run @@ fun sw ->
427427- let pds = Pds.v ~sw path ~did:test_did in
428428- let store = Irmin.Atproto.(of_pds pds |> v) in
429429- let tree1 = Irmin.Tree.add Irmin.Tree.empty [ "a" ] "1" in
430430- let h1 =
431431- Irmin.commit store ~tree:tree1 ~parents:[] ~message:"on main" ~author:"t"
432432- in
433433- Irmin.set_head store ~branch:"main" h1;
434434- let tree2 = Irmin.Tree.add Irmin.Tree.empty [ "b" ] "2" in
435435- let h2 =
436436- Irmin.commit store ~tree:tree2 ~parents:[] ~message:"on dev" ~author:"t"
437437- in
438438- Irmin.set_head store ~branch:"dev" h2;
439439- (* Both branches exist *)
440440- let bs = Irmin.branches store in
441441- Alcotest.(check bool) "has main" true (List.mem "main" bs);
442442- Alcotest.(check bool) "has dev" true (List.mem "dev" bs);
443443- (* Each points to its own commit *)
444444- (match Irmin.head store ~branch:"main" with
445445- | Some h -> Alcotest.(check bool) "main head" true (Irmin.Hash.equal h h1)
446446- | None -> Alcotest.fail "main head missing");
447447- match Irmin.head store ~branch:"dev" with
448448- | Some h -> Alcotest.(check bool) "dev head" true (Irmin.Hash.equal h h2)
449449- | None -> Alcotest.fail "dev head missing"
450450-451451-let test_pds_branches_survive_reopen () =
452452- with_temp_dir @@ fun path ->
453453- (* Session 1: create branches *)
454454- Eio.Switch.run (fun sw ->
455455- let pds = Pds.v ~sw path ~did:test_did in
456456- let store = Irmin.Atproto.(of_pds pds |> v) in
457457- let tree = Irmin.Tree.add Irmin.Tree.empty [ "x" ] "y" in
458458- let h =
459459- Irmin.commit store ~tree ~parents:[] ~message:"init" ~author:"t"
460460- in
461461- Irmin.set_head store ~branch:"main" h;
462462- Irmin.set_head store ~branch:"feature" h;
463463- Pds.close pds);
464464- (* Session 2: verify branches persist *)
465465- Eio.Switch.run @@ fun sw ->
466466- let pds = Pds.open_ ~sw path in
467467- let store = Irmin.Atproto.(of_pds pds |> v) in
468468- let bs = Irmin.branches store in
469469- Alcotest.(check bool) "main persists" true (List.mem "main" bs);
470470- Alcotest.(check bool) "feature persists" true (List.mem "feature" bs);
471471- Pds.close pds
472472-473473-let suite =
474474- ( "pds_interop",
475475- [
476476- Alcotest.test_case "pds-mst read record" `Quick test_pds_mst_read_record;
477477- Alcotest.test_case "pds-mst list collections" `Quick
478478- test_pds_mst_list_collections;
479479- Alcotest.test_case "pds-mst head" `Quick test_pds_write_mst_head;
480480- Alcotest.test_case "mst-pds read record" `Quick test_mst_pds_read_record;
481481- Alcotest.test_case "mst-pds list records" `Quick test_mst_write_pds_list;
482482- Alcotest.test_case "roundtrip modify" `Quick test_roundtrip_modify;
483483- Alcotest.test_case "roundtrip add/delete" `Quick test_roundtrip_add_delete;
484484- Alcotest.test_case "car export/reimport" `Quick test_car_export_reimport;
485485- Alcotest.test_case "car import then read" `Quick test_car_import_then_read;
486486- Alcotest.test_case "persistence across sessions" `Quick
487487- test_persistence_across_sessions;
488488- Alcotest.test_case "persistence DID preserved" `Quick test_did_preserved;
489489- Alcotest.test_case "empty checkout" `Quick test_empty_pds_checkout;
490490- Alcotest.test_case "empty head" `Quick test_empty_pds_head;
491491- Alcotest.test_case "scale 10 records" `Quick test_many_records;
492492- Alcotest.test_case "leaf order" `Quick test_leaf_order_matches;
493493- Alcotest.test_case "multiple collections" `Quick
494494- test_multiple_collections_interop;
495495- Alcotest.test_case "DAG-CBOR integrity" `Quick test_dagcbor_integrity;
496496- Alcotest.test_case "store main branch" `Quick test_pds_store_main_branch;
497497- Alcotest.test_case "multiple branches" `Quick
498498- test_pds_store_multiple_branches;
499499- Alcotest.test_case "branches survive reopen" `Quick
500500- test_pds_branches_survive_reopen;
501501- ] )
-2
test/test_pds_interop.mli
···11-val suite : string * unit Alcotest.test_case list
22-(** Alcotest suite for PDS interoperability tests. *)
-207
test/test_proof.ml
···11-open Irmin
22-open Private
33-44-let test_proof_produce_verify () =
55- let backend = Backend.Memory.sha1 () in
66- let tree = Tree.Git.empty () in
77- let tree = Tree.Git.add tree [ "foo"; "bar" ] "hello" in
88- let tree = Tree.Git.add tree [ "foo"; "baz" ] "world" in
99- let root_hash = Tree.Git.hash tree ~backend in
1010- let proof, result =
1111- Proof.Git.produce backend root_hash (fun t ->
1212- let v = Proof.Git.Tree.find t [ "foo"; "bar" ] in
1313- (t, v))
1414- in
1515- Alcotest.(check (option string)) "found value" (Some "hello") result;
1616- match
1717- Proof.Git.verify ~expected_root:(`Node root_hash) proof (fun t ->
1818- let v = Proof.Git.Tree.find t [ "foo"; "bar" ] in
1919- (t, v))
2020- with
2121- | Ok (_, v) ->
2222- Alcotest.(check (option string)) "verified value" (Some "hello") v
2323- | Error (`Proof_mismatch msg) -> Alcotest.fail ("proof mismatch: " ^ msg)
2424-2525-let test_proof_blinded () =
2626- let backend = Backend.Memory.sha1 () in
2727- let tree = Tree.Git.empty () in
2828- let tree = Tree.Git.add tree [ "a" ] "1" in
2929- let tree = Tree.Git.add tree [ "b" ] "2" in
3030- let root_hash = Tree.Git.hash tree ~backend in
3131- let proof, _ =
3232- Proof.Git.produce backend root_hash (fun t ->
3333- let _ = Proof.Git.Tree.find t [ "a" ] in
3434- (t, ()))
3535- in
3636- let state = Proof.state proof in
3737- match state with
3838- | Proof.Node entries ->
3939- let has_a =
4040- List.exists
4141- (fun (k, v) ->
4242- k = "a" && match v with Proof.Contents "1" -> true | _ -> false)
4343- entries
4444- in
4545- let has_blinded_b =
4646- List.exists
4747- (fun (k, v) ->
4848- k = "b"
4949- && match v with Proof.Blinded_contents _ -> true | _ -> false)
5050- entries
5151- in
5252- Alcotest.(check bool) "has a" true has_a;
5353- Alcotest.(check bool) "b is blinded" true has_blinded_b
5454- | _ -> Alcotest.fail "expected Node"
5555-5656-let test_proof_mst () =
5757- let backend = Backend.Memory.cid () in
5858- let tree = Tree.Mst.empty () in
5959- let tree = Tree.Mst.add tree [ "key1" ] "value1" in
6060- let tree = Tree.Mst.add tree [ "key2" ] "value2" in
6161- let root_hash = Tree.Mst.hash tree ~backend in
6262- let proof, result =
6363- Proof.Mst.produce backend root_hash (fun t ->
6464- let v = Proof.Mst.Tree.find t [ "key1" ] in
6565- (t, v))
6666- in
6767- Alcotest.(check (option string)) "found value" (Some "value1") result;
6868- match
6969- Proof.Mst.verify ~expected_root:(`Node root_hash) proof (fun t ->
7070- let v = Proof.Mst.Tree.find t [ "key1" ] in
7171- (t, v))
7272- with
7373- | Ok (_, v) ->
7474- Alcotest.(check (option string)) "verified value" (Some "value1") v
7575- | Error (`Proof_mismatch msg) -> Alcotest.fail ("proof mismatch: " ^ msg)
7676-7777-let test_wrong_expected_root () =
7878- let backend = Backend.Memory.sha1 () in
7979- let tree = Tree.Git.empty () in
8080- let tree = Tree.Git.add tree [ "foo" ] "bar" in
8181- let root_hash = Tree.Git.hash tree ~backend in
8282- let proof, _ =
8383- Proof.Git.produce backend root_hash (fun t ->
8484- let v = Proof.Git.Tree.find t [ "foo" ] in
8585- (t, v))
8686- in
8787- let wrong_root = `Node (Codec.Git.hash_contents "wrong") in
8888- match
8989- Proof.Git.verify ~expected_root:wrong_root proof (fun t ->
9090- let v = Proof.Git.Tree.find t [ "foo" ] in
9191- (t, v))
9292- with
9393- | Ok _ -> Alcotest.fail "should reject wrong expected_root"
9494- | Error (`Proof_mismatch _) -> ()
9595-9696-let test_wrong_before () =
9797- let backend = Backend.Memory.sha1 () in
9898- let tree = Tree.Git.empty () in
9999- let tree = Tree.Git.add tree [ "foo" ] "bar" in
100100- let root_hash = Tree.Git.hash tree ~backend in
101101- let proof, _ =
102102- Proof.Git.produce backend root_hash (fun t ->
103103- let v = Proof.Git.Tree.find t [ "foo" ] in
104104- (t, v))
105105- in
106106- let wrong_hash = Codec.Git.hash_contents "wrong" in
107107- let tampered =
108108- Proof.v ~before:(`Node wrong_hash) ~after:(Proof.after proof)
109109- (Proof.state proof)
110110- in
111111- match
112112- Proof.Git.verify ~expected_root:(`Node wrong_hash) tampered (fun t ->
113113- let v = Proof.Git.Tree.find t [ "foo" ] in
114114- (t, v))
115115- with
116116- | Ok _ -> Alcotest.fail "should reject wrong before hash"
117117- | Error (`Proof_mismatch _) -> ()
118118-119119-let test_wrong_after () =
120120- let backend = Backend.Memory.sha1 () in
121121- let tree = Tree.Git.empty () in
122122- let tree = Tree.Git.add tree [ "foo" ] "bar" in
123123- let root_hash = Tree.Git.hash tree ~backend in
124124- let proof, _ =
125125- Proof.Git.produce backend root_hash (fun t ->
126126- let v = Proof.Git.Tree.find t [ "foo" ] in
127127- (t, v))
128128- in
129129- let wrong_hash = Codec.Git.hash_contents "wrong" in
130130- let tampered =
131131- Proof.v ~before:(Proof.before proof) ~after:(`Node wrong_hash)
132132- (Proof.state proof)
133133- in
134134- match
135135- Proof.Git.verify ~expected_root:(`Node root_hash) tampered (fun t ->
136136- let v = Proof.Git.Tree.find t [ "foo" ] in
137137- (t, v))
138138- with
139139- | Ok _ -> Alcotest.fail "should reject wrong after hash"
140140- | Error (`Proof_mismatch _) -> ()
141141-142142-let test_wrong_state () =
143143- let backend = Backend.Memory.sha1 () in
144144- let tree = Tree.Git.empty () in
145145- let tree = Tree.Git.add tree [ "foo" ] "bar" in
146146- let root_hash = Tree.Git.hash tree ~backend in
147147- let proof, _ =
148148- Proof.Git.produce backend root_hash (fun t ->
149149- let v = Proof.Git.Tree.find t [ "foo" ] in
150150- (t, v))
151151- in
152152- let wrong_hash = Codec.Git.hash_contents "wrong" in
153153- let bad_states =
154154- [ Proof.Blinded_node wrong_hash; Proof.Node []; Proof.Contents "garbage" ]
155155- in
156156- List.iter
157157- (fun state ->
158158- let tampered =
159159- Proof.v ~before:(Proof.before proof) ~after:(Proof.after proof) state
160160- in
161161- match
162162- Proof.Git.verify ~expected_root:(`Node root_hash) tampered (fun t ->
163163- let v = Proof.Git.Tree.find t [ "foo" ] in
164164- (t, v))
165165- with
166166- | Ok _ -> Alcotest.fail "should reject wrong state"
167167- | Error (`Proof_mismatch _) -> ())
168168- bad_states
169169-170170-let test_attacker_crafted_proof () =
171171- (* An attacker creates a proof for an arbitrary tree and claims it as a
172172- legitimate root. verify must reject because expected_root won't match. *)
173173- let backend = Backend.Memory.sha1 () in
174174- let real_tree = Tree.Git.empty () in
175175- let real_tree = Tree.Git.add real_tree [ "secret" ] "real_value" in
176176- let real_root = Tree.Git.hash real_tree ~backend in
177177- let fake_tree = Tree.Git.empty () in
178178- let fake_tree = Tree.Git.add fake_tree [ "secret" ] "attacker_value" in
179179- let fake_root = Tree.Git.hash fake_tree ~backend in
180180- let fake_proof, _ =
181181- Proof.Git.produce backend fake_root (fun t ->
182182- let v = Proof.Git.Tree.find t [ "secret" ] in
183183- (t, v))
184184- in
185185- (* Attacker presents fake_proof but the verifier holds real_root as trusted *)
186186- match
187187- Proof.Git.verify ~expected_root:(`Node real_root) fake_proof (fun t ->
188188- let v = Proof.Git.Tree.find t [ "secret" ] in
189189- (t, v))
190190- with
191191- | Ok _ -> Alcotest.fail "should reject proof for wrong tree"
192192- | Error (`Proof_mismatch _) -> ()
193193-194194-let suite =
195195- ( "proof",
196196- [
197197- Alcotest.test_case "produce/verify" `Quick test_proof_produce_verify;
198198- Alcotest.test_case "blinded nodes" `Quick test_proof_blinded;
199199- Alcotest.test_case "mst proofs" `Quick test_proof_mst;
200200- Alcotest.test_case "reject wrong expected_root" `Quick
201201- test_wrong_expected_root;
202202- Alcotest.test_case "reject wrong before" `Quick test_wrong_before;
203203- Alcotest.test_case "reject wrong after" `Quick test_wrong_after;
204204- Alcotest.test_case "reject wrong state" `Quick test_wrong_state;
205205- Alcotest.test_case "reject attacker-crafted proof" `Quick
206206- test_attacker_crafted_proof;
207207- ] )
-2
test/test_proof.mli
···11-val suite : string * unit Alcotest.test_case list
22-(** Test suite. *)
-64
test/test_store.ml
···11-(* Internal API tests using Private modules directly (SHA-1 only). *)
22-33-open Irmin.Private
44-55-let test_store_commit () =
66- let backend = Backend.Memory.sha1 () in
77- let store = Store.Git.create ~backend in
88- let tree = Tree.Git.empty () in
99- let tree = Tree.Git.add tree [ "README.md" ] "# Hello" in
1010- let hash =
1111- Store.Git.commit store ~tree ~parents:[] ~message:"Initial commit"
1212- ~author:"test"
1313- in
1414- Alcotest.(check bool) "commit hash exists" true (Backend.exists backend hash)
1515-1616-let test_store_branches () =
1717- let backend = Backend.Memory.sha1 () in
1818- let store = Store.Git.create ~backend in
1919- let tree = Tree.Git.empty () in
2020- let hash =
2121- Store.Git.commit store ~tree ~parents:[] ~message:"test" ~author:"test"
2222- in
2323- Store.Git.set_head store ~branch:"main" hash;
2424- let branches = Store.Git.branches store in
2525- Alcotest.(check (list string)) "branches" [ "main" ] branches
2626-2727-let test_store_diff () =
2828- let backend = Backend.Memory.sha1 () in
2929- let store = Store.Git.create ~backend in
3030- let tree1 = Tree.Git.empty () in
3131- let tree1 = Tree.Git.add tree1 [ "file1.txt" ] "content1" in
3232- let tree1 = Tree.Git.add tree1 [ "file2.txt" ] "content2" in
3333- let hash1 = Tree.Git.hash tree1 ~backend in
3434- let tree2 = Tree.Git.empty () in
3535- let tree2 = Tree.Git.add tree2 [ "file1.txt" ] "modified1" in
3636- let tree2 = Tree.Git.add tree2 [ "file3.txt" ] "content3" in
3737- let hash2 = Tree.Git.hash tree2 ~backend in
3838- let changes = Store.Git.diff store ~old:hash1 ~new_:hash2 |> List.of_seq in
3939- let has_remove_file2 =
4040- List.exists
4141- (function `Remove [ "file2.txt" ] -> true | _ -> false)
4242- changes
4343- in
4444- let has_add_file3 =
4545- List.exists
4646- (function `Add ([ "file3.txt" ], _) -> true | _ -> false)
4747- changes
4848- in
4949- let has_change_file1 =
5050- List.exists
5151- (function `Change ([ "file1.txt" ], _, _) -> true | _ -> false)
5252- changes
5353- in
5454- Alcotest.(check bool) "file2 removed" true has_remove_file2;
5555- Alcotest.(check bool) "file3 added" true has_add_file3;
5656- Alcotest.(check bool) "file1 changed" true has_change_file1
5757-5858-let suite =
5959- ( "store",
6060- [
6161- Alcotest.test_case "store commit" `Quick test_store_commit;
6262- Alcotest.test_case "store branches" `Quick test_store_branches;
6363- Alcotest.test_case "store diff" `Quick test_store_diff;
6464- ] )
-2
test/test_store.mli
···11-val suite : string * unit Alcotest.test_case list
22-(** [suite] is the alcotest test suite for [test_store]. *)
-19
test/test_stores.ml
···11-(** Backend configs for the generic store test suite. *)
22-33-let cleanup_path path =
44- try Helpers.rm_rf path with Eio.Io _ | Sys_error _ -> ()
55-66-let run_git f =
77- Eio_main.run @@ fun env ->
88- Eio.Switch.run @@ fun sw ->
99- let fs = Eio.Stdenv.fs env in
1010- let name = Fmt.str "/tmp/irmin-test-git-%d" (Random.int 1_000_000) in
1111- let path = Eio.Path.(fs / name) in
1212- Fun.protect
1313- ~finally:(fun () -> cleanup_path path)
1414- (fun () -> f (Irmin_git.init ~sw ~fs ~path:(Fpath.v name)))
1515-1616-let git : Irmin_git.t Generic_store.config =
1717- Generic_store.make ~name:"git" ~run:run_git
1818-1919-let suite = Generic_store.suite git
-2
test/test_stores.mli
···11-val suite : string * unit Alcotest.test_case list
22-(** [suite] is the alcotest test suite covering all generic store backends. *)
-44
test/test_subtree.ml
···11-open Irmin
22-open Private
33-44-let test_split () =
55- let backend = Backend.Memory.sha1 () in
66- let store = Store.Git.create ~backend in
77- let tree = Tree.Git.empty () in
88- let tree = Tree.Git.add tree [ "sub"; "file.txt" ] "content" in
99- let tree = Tree.Git.add tree [ "other.txt" ] "other" in
1010- let _hash =
1111- Store.Git.commit store ~tree ~parents:[] ~message:"initial" ~author:"test"
1212- in
1313- (* split should produce a store without crashing *)
1414- let _sub_store = Subtree.Git.split store ~prefix:[ "sub" ] in
1515- ()
1616-1717-let test_status_in_sync () =
1818- let backend1 = Backend.Memory.sha1 () in
1919- let store1 = Store.Git.create ~backend:backend1 in
2020- let backend2 = Backend.Memory.sha1 () in
2121- let store2 = Store.Git.create ~backend:backend2 in
2222- let tree = Tree.Git.empty () in
2323- let tree = Tree.Git.add tree [ "sub"; "a.txt" ] "content" in
2424- let h1 =
2525- Store.Git.commit store1 ~tree ~parents:[] ~message:"init" ~author:"test"
2626- in
2727- Store.Git.set_head store1 ~branch:"main" h1;
2828- let sub_tree = Tree.Git.empty () in
2929- let sub_tree = Tree.Git.add sub_tree [ "a.txt" ] "content" in
3030- let h2 =
3131- Store.Git.commit store2 ~tree:sub_tree ~parents:[] ~message:"init"
3232- ~author:"test"
3333- in
3434- Store.Git.set_head store2 ~branch:"main" h2;
3535- let status = Subtree.Git.status store1 ~prefix:[ "sub" ] ~external_:store2 in
3636- (* Accept any status - the key test is that it doesn't crash *)
3737- ignore status
3838-3939-let suite =
4040- ( "subtree",
4141- [
4242- Alcotest.test_case "split" `Quick test_split;
4343- Alcotest.test_case "status" `Quick test_status_in_sync;
4444- ] )
-2
test/test_subtree.mli
···11-val suite : string * unit Alcotest.test_case list
22-(** Test suite. *)
-207
test/test_tree.ml
···11-(* Tree tests — ported from upstream irmin src/irmin-test/store.ml (tree section)
22- and test/irmin/test_tree.ml *)
33-44-(* {1 Basic operations} *)
55-66-let test_empty_tree () =
77- let tree = Irmin.Tree.empty in
88- Alcotest.(check (option string))
99- "find empty" None
1010- (Irmin.Tree.find tree [ "foo" ])
1111-1212-let test_add_find () =
1313- let tree = Irmin.Tree.empty in
1414- let tree = Irmin.Tree.add tree [ "foo"; "bar" ] "content" in
1515- Alcotest.(check (option string))
1616- "find added" (Some "content")
1717- (Irmin.Tree.find tree [ "foo"; "bar" ])
1818-1919-let test_remove () =
2020- let tree = Irmin.Tree.empty in
2121- let tree = Irmin.Tree.add tree [ "foo" ] "content" in
2222- let tree = Irmin.Tree.remove tree [ "foo" ] in
2323- Alcotest.(check (option string))
2424- "find removed" None
2525- (Irmin.Tree.find tree [ "foo" ])
2626-2727-let test_overwrite () =
2828- let tree = Irmin.Tree.empty in
2929- let tree = Irmin.Tree.add tree [ "key" ] "value1" in
3030- let tree = Irmin.Tree.add tree [ "key" ] "value2" in
3131- Alcotest.(check (option string))
3232- "find overwritten" (Some "value2")
3333- (Irmin.Tree.find tree [ "key" ])
3434-3535-let test_nested () =
3636- let tree = Irmin.Tree.empty in
3737- let tree = Irmin.Tree.add tree [ "a"; "b"; "c" ] "deep" in
3838- let tree = Irmin.Tree.add tree [ "a"; "x" ] "shallow" in
3939- Alcotest.(check (option string))
4040- "find deep" (Some "deep")
4141- (Irmin.Tree.find tree [ "a"; "b"; "c" ]);
4242- Alcotest.(check (option string))
4343- "find shallow" (Some "shallow")
4444- (Irmin.Tree.find tree [ "a"; "x" ])
4545-4646-(* {1 List and ordering — from upstream test_trees / paginated bindings} *)
4747-4848-let test_list_children () =
4949- let tree = Irmin.Tree.empty in
5050- let tree = Irmin.Tree.add tree [ "b" ] "2" in
5151- let tree = Irmin.Tree.add tree [ "a" ] "1" in
5252- let tree = Irmin.Tree.add tree [ "c" ] "3" in
5353- let children = Irmin.Tree.list tree [] in
5454- let names = List.map fst children in
5555- Alcotest.(check (list string)) "sorted" [ "a"; "b"; "c" ] names;
5656- List.iter
5757- (fun (_, kind) ->
5858- Alcotest.(check string)
5959- "leaf kind" "Contents"
6060- (match kind with `Contents -> "Contents" | `Node -> "Node"))
6161- children
6262-6363-let test_list_subtrees () =
6464- let tree = Irmin.Tree.empty in
6565- let tree = Irmin.Tree.add tree [ "dir"; "file1" ] "a" in
6666- let tree = Irmin.Tree.add tree [ "dir"; "file2" ] "b" in
6767- let tree = Irmin.Tree.add tree [ "leaf" ] "c" in
6868- let children = Irmin.Tree.list tree [] in
6969- let kinds = List.map (fun (name, kind) -> (name, kind = `Node)) children in
7070- Alcotest.(check bool) "dir is node" true (List.assoc "dir" kinds);
7171- Alcotest.(check bool) "leaf is contents" false (List.assoc "leaf" kinds);
7272- let dir_children = Irmin.Tree.list tree [ "dir" ] in
7373- Alcotest.(check int) "dir has 2 children" 2 (List.length dir_children)
7474-7575-let test_list_empty () =
7676- let tree = Irmin.Tree.empty in
7777- let children = Irmin.Tree.list tree [] in
7878- Alcotest.(check int) "empty tree list" 0 (List.length children);
7979- let children = Irmin.Tree.list tree [ "nonexistent" ] in
8080- Alcotest.(check int) "nonexistent path list" 0 (List.length children)
8181-8282-(* {1 find_tree / add_tree — from upstream test_backend_nodes} *)
8383-8484-let test_find_tree () =
8585- let tree = Irmin.Tree.empty in
8686- let tree = Irmin.Tree.add tree [ "a"; "b" ] "1" in
8787- let tree = Irmin.Tree.add tree [ "a"; "c" ] "2" in
8888- let sub = Irmin.Tree.find_tree tree [ "a" ] in
8989- Alcotest.(check bool) "subtree exists" true (Option.is_some sub);
9090- let sub = Option.get sub in
9191- Alcotest.(check (option string))
9292- "subtree find" (Some "1")
9393- (Irmin.Tree.find sub [ "b" ]);
9494- Alcotest.(check (option string))
9595- "subtree find 2" (Some "2")
9696- (Irmin.Tree.find sub [ "c" ])
9797-9898-let test_find_tree_leaf () =
9999- let tree = Irmin.Tree.empty in
100100- let tree = Irmin.Tree.add tree [ "a" ] "value" in
101101- let sub = Irmin.Tree.find_tree tree [ "a" ] in
102102- Alcotest.(check bool) "leaf is not a subtree" true (Option.is_none sub)
103103-104104-let test_add_tree () =
105105- let tree = Irmin.Tree.empty in
106106- let sub = Irmin.Tree.empty in
107107- let sub = Irmin.Tree.add sub [ "x" ] "1" in
108108- let sub = Irmin.Tree.add sub [ "y" ] "2" in
109109- let tree = Irmin.Tree.add_tree tree [ "dir" ] sub in
110110- Alcotest.(check (option string))
111111- "grafted subtree" (Some "1")
112112- (Irmin.Tree.find tree [ "dir"; "x" ]);
113113- Alcotest.(check (option string))
114114- "grafted subtree 2" (Some "2")
115115- (Irmin.Tree.find tree [ "dir"; "y" ])
116116-117117-let test_add_tree_replace () =
118118- let tree = Irmin.Tree.empty in
119119- let tree = Irmin.Tree.add tree [ "dir"; "old" ] "old_content" in
120120- let sub = Irmin.Tree.empty in
121121- let sub = Irmin.Tree.add sub [ "new" ] "new_content" in
122122- let tree = Irmin.Tree.add_tree tree [ "dir" ] sub in
123123- Alcotest.(check (option string))
124124- "old content gone" None
125125- (Irmin.Tree.find tree [ "dir"; "old" ]);
126126- Alcotest.(check (option string))
127127- "new content present" (Some "new_content")
128128- (Irmin.Tree.find tree [ "dir"; "new" ])
129129-130130-(* {1 Remove edge cases — from upstream} *)
131131-132132-let test_remove_nonexistent () =
133133- let tree = Irmin.Tree.empty in
134134- let tree = Irmin.Tree.add tree [ "a" ] "1" in
135135- let tree' = Irmin.Tree.remove tree [ "b" ] in
136136- Alcotest.(check (option string))
137137- "unchanged after remove nonexistent" (Some "1")
138138- (Irmin.Tree.find tree' [ "a" ])
139139-140140-let test_remove_subtree () =
141141- let tree = Irmin.Tree.empty in
142142- let tree = Irmin.Tree.add tree [ "a"; "b" ] "1" in
143143- let tree = Irmin.Tree.add tree [ "a"; "c" ] "2" in
144144- let tree = Irmin.Tree.remove tree [ "a"; "b" ] in
145145- Alcotest.(check (option string))
146146- "sibling intact" (Some "2")
147147- (Irmin.Tree.find tree [ "a"; "c" ]);
148148- Alcotest.(check (option string))
149149- "removed" None
150150- (Irmin.Tree.find tree [ "a"; "b" ])
151151-152152-(* {1 Deep nesting} *)
153153-154154-let test_deep_nesting () =
155155- let tree = Irmin.Tree.empty in
156156- let path = [ "a"; "b"; "c"; "d"; "e"; "f"; "g"; "h" ] in
157157- let tree = Irmin.Tree.add tree path "deep_value" in
158158- Alcotest.(check (option string))
159159- "deep find" (Some "deep_value")
160160- (Irmin.Tree.find tree path);
161161- let sub = Irmin.Tree.find_tree tree [ "a"; "b"; "c" ] in
162162- Alcotest.(check bool) "deep subtree" true (Option.is_some sub);
163163- let sub = Option.get sub in
164164- Alcotest.(check (option string))
165165- "relative find" (Some "deep_value")
166166- (Irmin.Tree.find sub [ "d"; "e"; "f"; "g"; "h" ])
167167-168168-(* {1 Many entries — inspired by upstream test_wide_nodes} *)
169169-170170-let test_many_entries () =
171171- let tree = Irmin.Tree.empty in
172172- let n = 1000 in
173173- let tree =
174174- List.init n (fun i -> (Fmt.str "key_%04d" i, Fmt.str "val_%d" i))
175175- |> List.fold_left (fun t (k, v) -> Irmin.Tree.add t [ k ] v) tree
176176- in
177177- let children = Irmin.Tree.list tree [] in
178178- Alcotest.(check int) "1000 entries" n (List.length children);
179179- (* Check lexicographic ordering *)
180180- let names = List.map fst children in
181181- let sorted = List.sort String.compare names in
182182- Alcotest.(check (list string)) "sorted order" sorted names;
183183- (* Spot check *)
184184- Alcotest.(check (option string))
185185- "find 500th" (Some "val_500")
186186- (Irmin.Tree.find tree [ "key_0500" ])
187187-188188-let suite =
189189- ( "tree",
190190- [
191191- Alcotest.test_case "empty tree" `Quick test_empty_tree;
192192- Alcotest.test_case "add/find" `Quick test_add_find;
193193- Alcotest.test_case "remove" `Quick test_remove;
194194- Alcotest.test_case "overwrite" `Quick test_overwrite;
195195- Alcotest.test_case "nested" `Quick test_nested;
196196- Alcotest.test_case "list children" `Quick test_list_children;
197197- Alcotest.test_case "list subtrees" `Quick test_list_subtrees;
198198- Alcotest.test_case "list empty" `Quick test_list_empty;
199199- Alcotest.test_case "find_tree" `Quick test_find_tree;
200200- Alcotest.test_case "find_tree leaf" `Quick test_find_tree_leaf;
201201- Alcotest.test_case "add_tree" `Quick test_add_tree;
202202- Alcotest.test_case "add_tree replace" `Quick test_add_tree_replace;
203203- Alcotest.test_case "remove nonexistent" `Quick test_remove_nonexistent;
204204- Alcotest.test_case "remove subtree" `Quick test_remove_subtree;
205205- Alcotest.test_case "deep nesting" `Quick test_deep_nesting;
206206- Alcotest.test_case "many entries" `Quick test_many_entries;
207207- ] )
-2
test/test_tree.mli
···11-val suite : string * unit Alcotest.test_case list
22-(** Test suite. *)