test Cover commit list pagination arithmetic

Exercise the orphan-absorption rule through the router: plain first pages, the widest absorbed page at the window boundary, remainders beyond the window, single pages, and page numbers past the end.

Commit
ad97b7e3f09f7a9d55ca42bf542128e34ab7862f
Author
Claude Fable 5 (high reasoning) <claude-fable-5@agents.anthropic.invalid>
Author date
Committer
Claude Fable 5 (high reasoning) <claude-fable-5@agents.anthropic.invalid>
Committer date
Changed files
test/test_ogit.ml
index 1f520c7a..8fa78c61 100644..100644
@@ -10,6 +10,7 @@
10 10 Test_tree_paths.suite;
11 11 Test_commit_walk.suite;
12 12 Test_router.suite;
13 Added: Test_pagination.suite;
13 14 Test_dispatch.suite;
14 15 Test_list_ext.suite;
15 16 Test_cache.suite;
test/test_pagination.ml
index 00000000..6ace2807 000000..100644
@@ -0,0 +1,104 @@
1 Added: (** Commit list pagination arithmetic, exercised through the router.
2 Added:
3 Added: The orphan-absorption rule: a page absorbs up to ten commits beyond the
4 Added: page size when they would otherwise form a near-empty final page. *)
5 Added:
6 Added: open Test_helpers
7 Added:
8 Added: let with_repository_of ~commits ~page_size test =
9 Added: with_temp_directory "ogit-pagination" (fun root ->
10 Added: let name = "project" in
11 Added: let path = Filename.concat root name in
12 Added: Unix.mkdir path 0o755;
13 Added: ignore (git [ "-C"; path; "init"; "-q"; "-b"; "main" ]);
14 Added: ignore (git [ "-C"; path; "config"; "user.name"; "Test" ]);
15 Added: ignore (git [ "-C"; path; "config"; "user.email"; "t@t.invalid" ]);
16 Added: for index = 1 to commits do
17 Added: Out_channel.with_open_text (Filename.concat path "f.txt") (fun ch ->
18 Added: Printf.fprintf ch "revision %d\n" index);
19 Added: ignore (git [ "-C"; path; "add"; "." ]);
20 Added: ignore
21 Added: (git [ "-C"; path; "commit"; "-q"; "-m"; Printf.sprintf "c%d" index ])
22 Added: done;
23 Added: let config =
24 Added: Ogit.Config.
25 Added: {
26 Added: default with
27 Added: git_project_root = root;
28 Added: commits_max_displayed = page_size;
29 Added: }
30 Added: in
31 Added: let request = Dream.test (Dream.router (Ogit.Handlers.routes config)) in
32 Added: test (fun target ->
33 Added: Dream.request ~target "" |> request |> Dream.body |> Lwt_main.run))
34 Added:
35 Added: let count_occurrences needle haystack =
36 Added: let nl = String.length needle and hl = String.length haystack in
37 Added: let rec go index count =
38 Added: if index + nl > hl then count
39 Added: else if String.sub haystack index nl = needle then go (index + 1) (count + 1)
40 Added: else go (index + 1) count
41 Added: in
42 Added: go 0 0
43 Added:
44 Added: let commit_rows body = count_occurrences "commit-title" body
45 Added: let has_link_to_page n body = count_occurrences (Printf.sprintf "page=%d" n) body > 0
46 Added:
47 Added: (* 25 commits at page size 10: the first page holds exactly the page size and
48 Added: links onward. *)
49 Added: let test_first_page () =
50 Added: with_repository_of ~commits:25 ~page_size:10 (fun body ->
51 Added: let page = body "/project/commits/" in
52 Added: Alcotest.(check int) "rows" 10 (commit_rows page);
53 Added: Alcotest.(check bool) "next link" true (has_link_to_page 2 page))
54 Added:
55 Added: (* The remaining 15 commits fit within page size + 10, so the second page
56 Added: absorbs them all rather than leaving a five-commit final page. *)
57 Added: let test_orphan_absorption () =
58 Added: with_repository_of ~commits:25 ~page_size:10 (fun body ->
59 Added: let page = body "/project/commits/?page=2" in
60 Added: Alcotest.(check int) "absorbed rows" 15 (commit_rows page);
61 Added: Alcotest.(check bool) "no next link" false (has_link_to_page 3 page))
62 Added:
63 Added: (* 35 commits at page size 10: 25 remain after page one, beyond the
64 Added: absorption window of page size + 10, so page two stays at the page size and
65 Added: links onward. *)
66 Added: let test_beyond_absorption_window () =
67 Added: with_repository_of ~commits:35 ~page_size:10 (fun body ->
68 Added: let page = body "/project/commits/?page=2" in
69 Added: Alcotest.(check int) "rows" 10 (commit_rows page);
70 Added: Alcotest.(check bool) "next link" true (has_link_to_page 3 page))
71 Added:
72 Added: (* Exactly page size + 10 remaining is the widest page absorption produces:
73 Added: with 30 commits, the 20 left after page one all land on page two. *)
74 Added: let test_absorption_window_boundary () =
75 Added: with_repository_of ~commits:30 ~page_size:10 (fun body ->
76 Added: let page = body "/project/commits/?page=2" in
77 Added: Alcotest.(check int) "rows" 20 (commit_rows page);
78 Added: Alcotest.(check bool) "no next link" false (has_link_to_page 3 page))
79 Added:
80 Added: (* Fewer commits than one page: a single page and no pagination links. *)
81 Added: let test_single_page () =
82 Added: with_repository_of ~commits:4 ~page_size:10 (fun body ->
83 Added: let page = body "/project/commits/" in
84 Added: Alcotest.(check int) "rows" 4 (commit_rows page);
85 Added: Alcotest.(check bool) "no next link" false (has_link_to_page 2 page))
86 Added:
87 Added: (* A page number beyond the history renders an empty list, not an error. *)
88 Added: let test_page_past_end () =
89 Added: with_repository_of ~commits:4 ~page_size:10 (fun body ->
90 Added: let page = body "/project/commits/?page=9" in
91 Added: Alcotest.(check int) "no rows" 0 (commit_rows page))
92 Added:
93 Added: let suite =
94 Added: ( "pagination",
95 Added: [
96 Added: Alcotest.test_case "first page" `Slow test_first_page;
97 Added: Alcotest.test_case "orphan absorption" `Slow test_orphan_absorption;
98 Added: Alcotest.test_case "beyond absorption window" `Slow
99 Added: test_beyond_absorption_window;
100 Added: Alcotest.test_case "absorption window boundary" `Slow
101 Added: test_absorption_window_boundary;
102 Added: Alcotest.test_case "single page" `Slow test_single_page;
103 Added: Alcotest.test_case "page past end" `Slow test_page_past_end;
104 Added: ] )