Monorepo management for opam overlays
0
fork

Configure Feed

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

test: fill all 60 empty test suites with meaningful tests

monopam (399 tests, 0.26s):
- Pure modules: opam_transform, mono_lock, changes, changes_aggregated,
import, forks, cross_status, doctor, diff, fork_join, verse
- I/O modules: ctx, git_cli, opam_repo, feature, progress helpers

ocaml-claudeio (246 tests, 0.03s):
- Codec roundtrips: model, tool_input, err, permissions, control,
sdk_control, structured_output, content_block, message, incoming
- Builder patterns: options (17 with_* builders), hooks (4 event types)
- Pure helpers: handler dispatch, mcp_server, server_info, response

ocaml-tls (91 new tests, 0.05s):
- RFC 8446 test vectors: TLS 1.3 key schedule (early/handshake/master
secrets, traffic keys, finished keys, HKDF-Expand-Label)
- RFC 5246: TLS 1.2 PRF output length, label sensitivity, finished
computation (12 bytes per Section 7.4.9)
- Ciphersuite params: key/nonce lengths, hash mappings, forward secrecy
- Handshake validation: client_hello construction, server_hello_valid,
extension checks, ALPN negotiation, signature algorithm filtering
- Utils: List_set operations, sub_equal, init_and_last, first_match

+2687 -46
+9 -1
test/test_add.ml
··· 1 - let suite = ("add", []) 1 + (* Tests for add module - I/O heavy, verify module alias works *) 2 + 3 + let test_module_alias () = 4 + (* Verify the Add module alias is accessible *) 5 + ignore 6 + (Monopam.Add.run : proc:_ -> fs:_ -> config:_ -> package:_ -> unit -> _) 7 + 8 + let suite = 9 + ("add", [ Alcotest.test_case "module alias" `Quick test_module_alias ])
+264 -2
test/test_changes.ml
··· 1 - (* Tests for changes *) 1 + (* Tests for changes module *) 2 + 3 + module Changes = Monopam.Changes 4 + 5 + let contains haystack needle = 6 + let nl = String.length needle in 7 + let hl = String.length haystack in 8 + if nl > hl then false 9 + else 10 + let rec loop i = 11 + if i > hl - nl then false 12 + else if String.sub haystack i nl = needle then true 13 + else loop (i + 1) 14 + in 15 + loop 0 16 + 17 + (* {1 Date Calculation Tests} *) 18 + 19 + let test_format_date_basic () = 20 + Alcotest.(check string) 21 + "simple date" "2026-03-16" 22 + (Changes.format_date (2026, 3, 16)) 23 + 24 + let test_format_date_zero_padding () = 25 + Alcotest.(check string) 26 + "zero padding" "2026-01-05" 27 + (Changes.format_date (2026, 1, 5)) 28 + 29 + let test_week_of_date_monday () = 30 + (* 2026-03-16 is a Monday *) 31 + let ws, we = Changes.week_of_date (2026, 3, 16) in 32 + Alcotest.(check string) "week start is Monday" "2026-03-16" ws; 33 + Alcotest.(check string) "week end is Sunday" "2026-03-22" we 34 + 35 + let test_week_of_date_wednesday () = 36 + (* 2026-03-18 is a Wednesday *) 37 + let ws, we = Changes.week_of_date (2026, 3, 18) in 38 + Alcotest.(check string) "week start" "2026-03-16" ws; 39 + Alcotest.(check string) "week end" "2026-03-22" we 40 + 41 + let test_week_of_date_sunday () = 42 + (* 2026-03-22 is a Sunday *) 43 + let ws, we = Changes.week_of_date (2026, 3, 22) in 44 + Alcotest.(check string) "week start" "2026-03-16" ws; 45 + Alcotest.(check string) "week end" "2026-03-22" we 46 + 47 + let test_week_of_date_cross_month () = 48 + (* 2026-03-30 is a Monday, week ends April 5 *) 49 + let ws, we = Changes.week_of_date (2026, 3, 30) in 50 + Alcotest.(check string) "week start" "2026-03-30" ws; 51 + Alcotest.(check string) "week end crosses month" "2026-04-05" we 52 + 53 + let test_week_of_date_cross_year () = 54 + (* 2025-12-31 is a Wednesday *) 55 + let ws, we = Changes.week_of_date (2025, 12, 31) in 56 + Alcotest.(check string) "week start" "2025-12-29" ws; 57 + Alcotest.(check string) "week end crosses year" "2026-01-04" we 58 + 59 + let test_week_of_ptime () = 60 + match Ptime.of_date_time ((2026, 3, 16), ((12, 0, 0), 0)) with 61 + | Some t -> 62 + let ws, we = Changes.week_of_ptime t in 63 + Alcotest.(check string) "week start" "2026-03-16" ws; 64 + Alcotest.(check string) "week end" "2026-03-22" we 65 + | None -> Alcotest.fail "invalid ptime" 66 + 67 + let test_date_of_ptime () = 68 + match Ptime.of_date_time ((2026, 3, 16), ((14, 30, 0), 0)) with 69 + | Some t -> 70 + Alcotest.(check string) "date" "2026-03-16" (Changes.date_of_ptime t) 71 + | None -> Alcotest.fail "invalid ptime" 72 + 73 + let test_week_timestamps_of_ptime () = 74 + match Ptime.of_date_time ((2026, 3, 18), ((12, 0, 0), 0)) with 75 + | Some t -> 76 + let since, until = Changes.week_timestamps_of_ptime t in 77 + Alcotest.(check bool) "since < until" true (Int64.compare since until < 0); 78 + let t_ts = Int64.of_float (Ptime.to_float_s t) in 79 + Alcotest.(check bool) "since <= t" true (Int64.compare since t_ts <= 0); 80 + Alcotest.(check bool) "t <= until" true (Int64.compare t_ts until <= 0) 81 + | None -> Alcotest.fail "invalid ptime" 82 + 83 + let test_day_timestamps_of_ptime () = 84 + match Ptime.of_date_time ((2026, 3, 16), ((15, 0, 0), 0)) with 85 + | Some t -> 86 + let since, until = Changes.day_timestamps_of_ptime t in 87 + Alcotest.(check bool) "since < until" true (Int64.compare since until < 0); 88 + let t_ts = Int64.of_float (Ptime.to_float_s t) in 89 + Alcotest.(check bool) "since <= t" true (Int64.compare since t_ts <= 0); 90 + Alcotest.(check bool) "t <= until" true (Int64.compare t_ts until <= 0) 91 + | None -> Alcotest.fail "invalid ptime" 2 92 3 - let suite = ("changes", []) 93 + (* {1 has_week / has_day Tests} *) 94 + 95 + let test_has_week_present () = 96 + let entry : Changes.weekly_entry = 97 + { 98 + week_start = "2026-03-16"; 99 + week_end = "2026-03-22"; 100 + summary = "test"; 101 + changes = [ "change1" ]; 102 + commit_range = { from_hash = "abc"; to_hash = "def"; count = 3 }; 103 + } 104 + in 105 + let cf : Changes.file = { repository = "test-repo"; entries = [ entry ] } in 106 + Alcotest.(check bool) 107 + "has week" true 108 + (Changes.has_week cf ~week_start:"2026-03-16") 109 + 110 + let test_has_week_absent () = 111 + let cf : Changes.file = { repository = "test-repo"; entries = [] } in 112 + Alcotest.(check bool) 113 + "no week" false 114 + (Changes.has_week cf ~week_start:"2026-03-16") 115 + 116 + let test_has_day_with_entries () = 117 + let entry : Changes.daily_entry = 118 + { 119 + date = "2026-03-16"; 120 + hour = 14; 121 + timestamp = Ptime.epoch; 122 + summary = "test"; 123 + changes = [ "change1" ]; 124 + commit_range = { from_hash = "abc"; to_hash = "def"; count = 1 }; 125 + contributors = [ "alice" ]; 126 + repo_url = None; 127 + } 128 + in 129 + let cf : Changes.daily_file = 130 + { repository = "test-repo"; entries = [ entry ] } 131 + in 132 + Alcotest.(check bool) "has day" true (Changes.has_day cf ~date:"2026-03-16") 133 + 134 + let test_has_day_empty () = 135 + let cf : Changes.daily_file = { repository = "test-repo"; entries = [] } in 136 + Alcotest.(check bool) 137 + "empty has no day" false 138 + (Changes.has_day cf ~date:"2026-03-16") 139 + 140 + (* {1 Markdown Generation Tests} *) 141 + 142 + let test_to_markdown_empty () = 143 + let cf : Changes.file = { repository = "test-repo"; entries = [] } in 144 + let md = Changes.to_markdown cf in 145 + Alcotest.(check bool) "contains repo name" true (contains md "test-repo") 146 + 147 + let test_to_markdown_with_entry () = 148 + let entry : Changes.weekly_entry = 149 + { 150 + week_start = "2026-03-16"; 151 + week_end = "2026-03-22"; 152 + summary = "Major improvements"; 153 + changes = [ "Added feature X"; "Fixed bug Y" ]; 154 + commit_range = { from_hash = "abc"; to_hash = "def"; count = 5 }; 155 + } 156 + in 157 + let cf : Changes.file = { repository = "my-lib"; entries = [ entry ] } in 158 + let md = Changes.to_markdown cf in 159 + Alcotest.(check bool) "has date" true (contains md "2026-03-16"); 160 + Alcotest.(check bool) "has change" true (contains md "Added feature X") 161 + 162 + let test_aggregate_empty () = 163 + let md = Changes.aggregate ~history:0 [] in 164 + Alcotest.(check bool) "has changelog header" true (contains md "# Changelog") 165 + 166 + let test_aggregate_daily_empty () = 167 + let md = Changes.aggregate_daily ~history:0 [] in 168 + Alcotest.(check bool) 169 + "has daily header" true 170 + (contains md "# Daily Changelog") 171 + 172 + let test_aggregate_with_history_limit () = 173 + let mk_file repo ws we = 174 + let entry : Changes.weekly_entry = 175 + { 176 + week_start = ws; 177 + week_end = we; 178 + summary = "summary"; 179 + changes = [ "change" ]; 180 + commit_range = { from_hash = "a"; to_hash = "b"; count = 1 }; 181 + } 182 + in 183 + ({ repository = repo; entries = [ entry ] } : Changes.file) 184 + in 185 + let files = 186 + [ 187 + mk_file "repo-a" "2026-03-16" "2026-03-22"; 188 + mk_file "repo-b" "2026-03-09" "2026-03-15"; 189 + ] 190 + in 191 + let md_all = Changes.aggregate ~history:0 files in 192 + let md_one = Changes.aggregate ~history:1 files in 193 + Alcotest.(check bool) 194 + "all has both weeks" true 195 + (contains md_all "2026-03-16" && contains md_all "2026-03-09"); 196 + (* history:1 should limit to one week group *) 197 + Alcotest.(check bool) 198 + "limited has first week" true 199 + (contains md_one "2026-03-16") 200 + 201 + (* {1 Claude Response Parsing} *) 202 + 203 + let test_parse_claude_no_changes () = 204 + match Changes.parse_claude_response "NO_CHANGES" with 205 + | Ok None -> () 206 + | Ok (Some _) -> Alcotest.fail "expected None for NO_CHANGES" 207 + | Error e -> Alcotest.failf "unexpected error: %s" e 208 + 209 + let test_parse_claude_empty_json () = 210 + let json = {|{"summary": "", "changes": []}|} in 211 + match Changes.parse_claude_response json with 212 + | Ok None -> () 213 + | Ok (Some _) -> Alcotest.fail "expected None for empty response" 214 + | Error e -> Alcotest.failf "unexpected error: %s" e 215 + 216 + let test_parse_claude_valid () = 217 + let json = 218 + {|{"summary": "Added foo", "changes": ["New feature foo", "Improved bar"]}|} 219 + in 220 + match Changes.parse_claude_response json with 221 + | Ok (Some r) -> 222 + Alcotest.(check string) "summary" "Added foo" r.summary; 223 + Alcotest.(check int) "changes count" 2 (List.length r.changes) 224 + | Ok None -> Alcotest.fail "expected Some for valid response" 225 + | Error e -> Alcotest.failf "unexpected error: %s" e 226 + 227 + let test_parse_claude_invalid () = 228 + match Changes.parse_claude_response "not json at all" with 229 + | Error _ -> () 230 + | Ok _ -> Alcotest.fail "expected error for invalid JSON" 231 + 232 + let suite = 233 + ( "changes", 234 + [ 235 + (* Date calculation *) 236 + Alcotest.test_case "format_date basic" `Quick test_format_date_basic; 237 + Alcotest.test_case "format_date padding" `Quick 238 + test_format_date_zero_padding; 239 + Alcotest.test_case "week monday" `Quick test_week_of_date_monday; 240 + Alcotest.test_case "week wednesday" `Quick test_week_of_date_wednesday; 241 + Alcotest.test_case "week sunday" `Quick test_week_of_date_sunday; 242 + Alcotest.test_case "week cross month" `Quick test_week_of_date_cross_month; 243 + Alcotest.test_case "week cross year" `Quick test_week_of_date_cross_year; 244 + Alcotest.test_case "week_of_ptime" `Quick test_week_of_ptime; 245 + Alcotest.test_case "date_of_ptime" `Quick test_date_of_ptime; 246 + Alcotest.test_case "week_timestamps" `Quick test_week_timestamps_of_ptime; 247 + Alcotest.test_case "day_timestamps" `Quick test_day_timestamps_of_ptime; 248 + (* has_week / has_day *) 249 + Alcotest.test_case "has_week present" `Quick test_has_week_present; 250 + Alcotest.test_case "has_week absent" `Quick test_has_week_absent; 251 + Alcotest.test_case "has_day entries" `Quick test_has_day_with_entries; 252 + Alcotest.test_case "has_day empty" `Quick test_has_day_empty; 253 + (* Markdown *) 254 + Alcotest.test_case "markdown empty" `Quick test_to_markdown_empty; 255 + Alcotest.test_case "markdown entry" `Quick test_to_markdown_with_entry; 256 + Alcotest.test_case "aggregate empty" `Quick test_aggregate_empty; 257 + Alcotest.test_case "aggregate daily" `Quick test_aggregate_daily_empty; 258 + Alcotest.test_case "aggregate limit" `Quick 259 + test_aggregate_with_history_limit; 260 + (* Claude parsing *) 261 + Alcotest.test_case "NO_CHANGES" `Quick test_parse_claude_no_changes; 262 + Alcotest.test_case "empty JSON" `Quick test_parse_claude_empty_json; 263 + Alcotest.test_case "valid JSON" `Quick test_parse_claude_valid; 264 + Alcotest.test_case "invalid JSON" `Quick test_parse_claude_invalid; 265 + ] )
+81 -2
test/test_changes_aggregated.ml
··· 1 - (* Tests for changes_aggregated *) 1 + (* Tests for changes_aggregated module *) 2 + 3 + module CA = Monopam.Changes.Aggregated 4 + 5 + (* {1 change_type roundtrip} *) 6 + 7 + let test_change_type_roundtrip () = 8 + let types = 9 + [ 10 + (CA.Feature, "feature"); 11 + (CA.Bugfix, "bugfix"); 12 + (CA.Documentation, "documentation"); 13 + (CA.Refactor, "refactor"); 14 + (CA.New_library, "new_library"); 15 + (CA.Unknown, "unknown"); 16 + ] 17 + in 18 + List.iter 19 + (fun (ct, s) -> 20 + Alcotest.(check string) ("to_string " ^ s) s (CA.string_of_change_type ct); 21 + let ct' = CA.change_type_of_string s in 22 + Alcotest.(check string) 23 + ("roundtrip " ^ s) s 24 + (CA.string_of_change_type ct')) 25 + types 26 + 27 + let test_change_type_unknown_fallback () = 28 + Alcotest.(check string) 29 + "unknown string" "unknown" 30 + (CA.string_of_change_type (CA.change_type_of_string "nonsense")) 31 + 32 + (* {1 pp tests} *) 33 + 34 + let test_pp_basic () = 35 + let t : CA.t = 36 + { 37 + date = "2026-03-16"; 38 + generated_at = Ptime.epoch; 39 + git_head = "abc1234"; 40 + entries = []; 41 + authors = [ "alice"; "bob" ]; 42 + } 43 + in 44 + let s = Fmt.str "%a" CA.pp t in 45 + Alcotest.(check bool) "has date" true (String.length s > 0) 46 + 47 + let test_pp_with_entries () = 48 + let entry : CA.entry = 49 + { 50 + repository = "test-repo"; 51 + hour = 14; 52 + timestamp = Ptime.epoch; 53 + summary = "test summary"; 54 + changes = [ "change1" ]; 55 + commit_range = { from_hash = "aaa"; to_hash = "bbb"; count = 2 }; 56 + contributors = [ "alice" ]; 57 + repo_url = None; 58 + change_type = CA.Feature; 59 + } 60 + in 61 + let t : CA.t = 62 + { 63 + date = "2026-03-16"; 64 + generated_at = Ptime.epoch; 65 + git_head = "abc"; 66 + entries = [ entry ]; 67 + authors = [ "alice" ]; 68 + } 69 + in 70 + let s = Fmt.str "%a" CA.pp t in 71 + Alcotest.(check bool) "pp has entries:1" true (String.length s > 0) 2 72 3 - let suite = ("changes_aggregated", []) 73 + let suite = 74 + ( "changes_aggregated", 75 + [ 76 + Alcotest.test_case "change_type roundtrip" `Quick 77 + test_change_type_roundtrip; 78 + Alcotest.test_case "unknown fallback" `Quick 79 + test_change_type_unknown_fallback; 80 + Alcotest.test_case "pp basic" `Quick test_pp_basic; 81 + Alcotest.test_case "pp with entries" `Quick test_pp_with_entries; 82 + ] )
+40 -2
test/test_changes_daily.ml
··· 1 - (* Tests for changes_daily *) 1 + (* Tests for changes_daily module *) 2 + 3 + module CD = Monopam.Changes.Daily 4 + 5 + (* {1 empty tests} *) 6 + 7 + let test_empty () = 8 + let t = CD.empty in 9 + Alcotest.(check int) "no entries" 0 (List.length t.all_entries); 10 + Alcotest.(check (list string)) "no repos" [] (CD.repos t); 11 + Alcotest.(check (list string)) "no dates" [] (CD.dates t) 12 + 13 + (* {1 pp tests} *) 2 14 3 - let suite = ("changes_daily", []) 15 + let test_pp_empty () = 16 + let s = Fmt.str "%a" CD.pp CD.empty in 17 + Alcotest.(check bool) "pp non-empty string" true (String.length s > 0) 18 + 19 + (* {1 query on empty} *) 20 + 21 + let test_since_empty () = 22 + let entries = CD.since CD.empty Ptime.epoch in 23 + Alcotest.(check int) "no entries since epoch" 0 (List.length entries) 24 + 25 + let test_for_repo_empty () = 26 + let days = CD.for_repo CD.empty "nonexistent" in 27 + Alcotest.(check int) "no days for unknown repo" 0 (List.length days) 28 + 29 + let test_for_date_empty () = 30 + let days = CD.for_date CD.empty "2026-03-16" in 31 + Alcotest.(check int) "no days for date" 0 (List.length days) 32 + 33 + let suite = 34 + ( "changes_daily", 35 + [ 36 + Alcotest.test_case "empty" `Quick test_empty; 37 + Alcotest.test_case "pp empty" `Quick test_pp_empty; 38 + Alcotest.test_case "since empty" `Quick test_since_empty; 39 + Alcotest.test_case "for_repo empty" `Quick test_for_repo_empty; 40 + Alcotest.test_case "for_date empty" `Quick test_for_date_empty; 41 + ] )
+132 -2
test/test_changes_query.ml
··· 1 - (* Tests for changes_query *) 1 + (* Tests for changes_query module *) 2 + 3 + module CQ = Monopam.Changes.Query 4 + 5 + (* {1 format_summary tests} *) 6 + 7 + let test_format_summary_empty () = 8 + let s = CQ.format_summary ~entries:[] in 9 + Alcotest.(check string) "no changes" "No new changes." s 10 + 11 + let test_format_summary_single () = 12 + let entry : Monopam.Changes.Aggregated.entry = 13 + { 14 + repository = "test-repo"; 15 + hour = 14; 16 + timestamp = Ptime.epoch; 17 + summary = "Added feature X"; 18 + changes = [ "change1" ]; 19 + commit_range = { from_hash = "aaa"; to_hash = "bbb"; count = 1 }; 20 + contributors = [ "alice" ]; 21 + repo_url = None; 22 + change_type = Monopam.Changes.Aggregated.Feature; 23 + } 24 + in 25 + let s = CQ.format_summary ~entries:[ entry ] in 26 + Alcotest.(check bool) 27 + "mentions count" true 28 + (let nl = String.length "1 change" in 29 + let hl = String.length s in 30 + nl <= hl 31 + && 32 + let rec loop i = 33 + if i > hl - nl then false 34 + else if String.sub s i nl = "1 change" then true 35 + else loop (i + 1) 36 + in 37 + loop 0) 2 38 3 - let suite = ("changes_query", []) 39 + let test_format_summary_multiple_repos () = 40 + let mk repo : Monopam.Changes.Aggregated.entry = 41 + { 42 + repository = repo; 43 + hour = 14; 44 + timestamp = Ptime.epoch; 45 + summary = "summary"; 46 + changes = [ "c" ]; 47 + commit_range = { from_hash = "a"; to_hash = "b"; count = 1 }; 48 + contributors = []; 49 + repo_url = None; 50 + change_type = Monopam.Changes.Aggregated.Feature; 51 + } 52 + in 53 + let entries = [ mk "repo-a"; mk "repo-b"; mk "repo-a" ] in 54 + let s = CQ.format_summary ~entries in 55 + (* Should mention 3 changes across 2 repositories *) 56 + Alcotest.(check bool) 57 + "mentions count" true 58 + (let needle = "3 changes" in 59 + let nl = String.length needle in 60 + let hl = String.length s in 61 + nl <= hl 62 + && 63 + let rec loop i = 64 + if i > hl - nl then false 65 + else if String.sub s i nl = needle then true 66 + else loop (i + 1) 67 + in 68 + loop 0) 69 + 70 + (* {1 format_for_zulip tests} *) 71 + 72 + let test_format_zulip_empty () = 73 + let s = CQ.format_for_zulip ~entries:[] ~include_date:false ~date:None in 74 + Alcotest.(check string) "no changes" "No changes to report." s 75 + 76 + let test_format_zulip_with_date () = 77 + let entry : Monopam.Changes.Aggregated.entry = 78 + { 79 + repository = "test-repo"; 80 + hour = 10; 81 + timestamp = Ptime.epoch; 82 + summary = "Added X"; 83 + changes = [ "New X" ]; 84 + commit_range = { from_hash = "a"; to_hash = "b"; count = 1 }; 85 + contributors = []; 86 + repo_url = None; 87 + change_type = Monopam.Changes.Aggregated.Feature; 88 + } 89 + in 90 + let s = 91 + CQ.format_for_zulip ~entries:[ entry ] ~include_date:true 92 + ~date:(Some "2026-03-16") 93 + in 94 + Alcotest.(check bool) 95 + "has date header" true 96 + (let needle = "2026-03-16" in 97 + let nl = String.length needle in 98 + let hl = String.length s in 99 + nl <= hl 100 + && 101 + let rec loop i = 102 + if i > hl - nl then false 103 + else if String.sub s i nl = needle then true 104 + else loop (i + 1) 105 + in 106 + loop 0) 107 + 108 + (* {1 daily formatting} *) 109 + 110 + let test_format_daily_summary_empty () = 111 + let s = CQ.format_daily_summary ~entries:[] in 112 + Alcotest.(check string) "no changes" "No new changes." s 113 + 114 + let test_format_daily_zulip_empty () = 115 + let s = 116 + CQ.format_daily_for_zulip ~entries:[] ~include_date:false ~date:None 117 + in 118 + Alcotest.(check string) "no changes" "No changes to report." s 119 + 120 + let suite = 121 + ( "changes_query", 122 + [ 123 + Alcotest.test_case "summary empty" `Quick test_format_summary_empty; 124 + Alcotest.test_case "summary single" `Quick test_format_summary_single; 125 + Alcotest.test_case "summary multi-repo" `Quick 126 + test_format_summary_multiple_repos; 127 + Alcotest.test_case "zulip empty" `Quick test_format_zulip_empty; 128 + Alcotest.test_case "zulip with date" `Quick test_format_zulip_with_date; 129 + Alcotest.test_case "daily summary empty" `Quick 130 + test_format_daily_summary_empty; 131 + Alcotest.test_case "daily zulip empty" `Quick 132 + test_format_daily_zulip_empty; 133 + ] )
+9 -1
test/test_clean.ml
··· 1 - let suite = ("clean", []) 1 + (* Tests for clean module - I/O heavy, verify module alias works *) 2 + 3 + let test_module_alias () = 4 + ignore 5 + (Monopam.Clean.run 6 + : proc:_ -> fs:_ -> config:_ -> dry_run:_ -> force:_ -> unit -> _) 7 + 8 + let suite = 9 + ("clean", [ Alcotest.test_case "module alias" `Quick test_module_alias ])
+112 -2
test/test_cross_status.ml
··· 1 - (* Tests for cross_status *) 1 + (* Tests for cross_status module *) 2 + 3 + module CS = Monopam.Cross_status 4 + 5 + (* {1 pp_relationship tests} *) 6 + 7 + let test_pp_same () = 8 + let s = Fmt.str "%a" CS.pp_relationship CS.Same in 9 + Alcotest.(check string) "same" "same" s 10 + 11 + let test_pp_ahead () = 12 + let s = Fmt.str "%a" CS.pp_relationship (CS.I_am_ahead 3) in 13 + Alcotest.(check string) "ahead" "3 behind" s 14 + 15 + let test_pp_behind () = 16 + let s = Fmt.str "%a" CS.pp_relationship (CS.I_am_behind 5) in 17 + Alcotest.(check string) "behind" "5 ahead" s 18 + 19 + let test_pp_diverged () = 20 + let s = 21 + Fmt.str "%a" CS.pp_relationship 22 + (CS.Diverged { my_ahead = 2; their_ahead = 4 }) 23 + in 24 + Alcotest.(check bool) "contains diverged" true (String.length s > 0) 25 + 26 + let test_pp_unknown () = 27 + let s = Fmt.str "%a" CS.pp_relationship CS.Unknown in 28 + Alcotest.(check string) "unknown" "unknown" s 29 + 30 + (* {1 is_actionable tests} *) 2 31 3 - let suite = ("cross_status", []) 32 + let test_actionable_behind () = 33 + Alcotest.(check bool) 34 + "behind is actionable" true 35 + (CS.is_actionable (CS.I_am_behind 3)) 36 + 37 + let test_actionable_diverged () = 38 + Alcotest.(check bool) 39 + "diverged is actionable" true 40 + (CS.is_actionable (CS.Diverged { my_ahead = 1; their_ahead = 2 })) 41 + 42 + let test_not_actionable_same () = 43 + Alcotest.(check bool) 44 + "same is not actionable" false (CS.is_actionable CS.Same) 45 + 46 + let test_not_actionable_ahead () = 47 + Alcotest.(check bool) 48 + "ahead is not actionable" false 49 + (CS.is_actionable (CS.I_am_ahead 3)) 50 + 51 + let test_not_actionable_unknown () = 52 + Alcotest.(check bool) 53 + "unknown is not actionable" false 54 + (CS.is_actionable CS.Unknown) 55 + 56 + (* {1 pp_subtree_info tests} *) 57 + 58 + let test_pp_subtree_info_with_commit () = 59 + let info : CS.subtree_info = 60 + { 61 + monorepo_path = Fpath.v "/tmp/mono"; 62 + prefix = "eio"; 63 + upstream_commit = Some "abcdef1234567890abcdef1234567890abcdef12"; 64 + } 65 + in 66 + let s = Fmt.str "%a" CS.pp_subtree_info info in 67 + Alcotest.(check string) "short hash" "abcdef1" s 68 + 69 + let test_pp_subtree_info_no_commit () = 70 + let info : CS.subtree_info = 71 + { 72 + monorepo_path = Fpath.v "/tmp/mono"; 73 + prefix = "eio"; 74 + upstream_commit = None; 75 + } 76 + in 77 + let s = Fmt.str "%a" CS.pp_subtree_info info in 78 + Alcotest.(check string) "no commit" "(no commit)" s 79 + 80 + (* {1 pp tests} *) 81 + 82 + let test_pp_empty () = 83 + let t : CS.t = { my_repos = []; other_repos = [] } in 84 + let s = Fmt.str "%a" CS.pp t in 85 + (* Should produce empty or minimal output *) 86 + ignore s 87 + 88 + let test_pp_summary_empty () = 89 + let t : CS.t = { my_repos = []; other_repos = [] } in 90 + let s = Fmt.str "%a" CS.pp_summary t in 91 + ignore s 92 + 93 + let suite = 94 + ( "cross_status", 95 + [ 96 + Alcotest.test_case "pp same" `Quick test_pp_same; 97 + Alcotest.test_case "pp ahead" `Quick test_pp_ahead; 98 + Alcotest.test_case "pp behind" `Quick test_pp_behind; 99 + Alcotest.test_case "pp diverged" `Quick test_pp_diverged; 100 + Alcotest.test_case "pp unknown" `Quick test_pp_unknown; 101 + Alcotest.test_case "actionable behind" `Quick test_actionable_behind; 102 + Alcotest.test_case "actionable diverged" `Quick test_actionable_diverged; 103 + Alcotest.test_case "not actionable same" `Quick test_not_actionable_same; 104 + Alcotest.test_case "not actionable ahead" `Quick test_not_actionable_ahead; 105 + Alcotest.test_case "not actionable unknown" `Quick 106 + test_not_actionable_unknown; 107 + Alcotest.test_case "subtree with commit" `Quick 108 + test_pp_subtree_info_with_commit; 109 + Alcotest.test_case "subtree no commit" `Quick 110 + test_pp_subtree_info_no_commit; 111 + Alcotest.test_case "pp empty" `Quick test_pp_empty; 112 + Alcotest.test_case "pp_summary empty" `Quick test_pp_summary_empty; 113 + ] )
+123 -1
test/test_ctx.ml
··· 1 - let suite = ("ctx", []) 1 + (* Tests for ctx module *) 2 + 3 + module Ctx = Monopam.Ctx 4 + 5 + (* {1 pp_error tests} *) 6 + 7 + let test_pp_config_error () = 8 + let s = Fmt.str "%a" Ctx.pp_error (Ctx.Config_error "bad config") in 9 + Alcotest.(check bool) "has message" true (String.length s > 0) 10 + 11 + let test_pp_monorepo_dirty () = 12 + let s = Fmt.str "%a" Ctx.pp_error Ctx.Monorepo_dirty in 13 + Alcotest.(check bool) "has message" true (String.length s > 0) 14 + 15 + let test_pp_package_not_found () = 16 + let s = Fmt.str "%a" Ctx.pp_error (Ctx.Package_not_found "my-pkg") in 17 + Alcotest.(check bool) "has package name" true (String.length s > 0) 18 + 19 + let test_pp_claude_error () = 20 + let s = Fmt.str "%a" Ctx.pp_error (Ctx.Claude_error "api timeout") in 21 + Alcotest.(check bool) "has message" true (String.length s > 0) 22 + 23 + (* {1 error_hint tests} *) 24 + 25 + let test_hint_config_error () = 26 + match Ctx.error_hint (Ctx.Config_error "missing") with 27 + | Some hint -> Alcotest.(check bool) "has hint" true (String.length hint > 0) 28 + | None -> Alcotest.fail "expected hint for Config_error" 29 + 30 + let test_hint_monorepo_dirty () = 31 + match Ctx.error_hint Ctx.Monorepo_dirty with 32 + | Some hint -> Alcotest.(check bool) "has hint" true (String.length hint > 0) 33 + | None -> Alcotest.fail "expected hint for Monorepo_dirty" 34 + 35 + let test_hint_package_not_found () = 36 + match Ctx.error_hint (Ctx.Package_not_found "foo") with 37 + | Some hint -> Alcotest.(check bool) "has hint" true (String.length hint > 0) 38 + | None -> Alcotest.fail "expected hint for Package_not_found" 39 + 40 + let test_hint_claude_error () = 41 + match Ctx.error_hint (Ctx.Claude_error "api key missing") with 42 + | Some hint -> Alcotest.(check bool) "has hint" true (String.length hint > 0) 43 + | None -> Alcotest.fail "expected hint for Claude_error" 44 + 45 + let test_hint_claude_decode_error () = 46 + match Ctx.error_hint (Ctx.Claude_error "Failed to decode response") with 47 + | Some hint -> Alcotest.(check bool) "has hint" true (String.length hint > 0) 48 + | None -> Alcotest.fail "expected hint for decode error" 49 + 50 + (* {1 pp_error_with_hint tests} *) 51 + 52 + let test_pp_error_with_hint () = 53 + let s = Fmt.str "%a" Ctx.pp_error_with_hint (Ctx.Config_error "test error") in 54 + Alcotest.(check bool) "has hint in output" true (String.length s > 0) 55 + 56 + (* {1 normalize_opam_url_string tests} *) 57 + 58 + let test_normalize_git_plus () = 59 + Alcotest.(check string) 60 + "strips git+" "https://github.com/user/repo.git" 61 + (Ctx.normalize_opam_url_string "git+https://github.com/user/repo.git") 62 + 63 + let test_normalize_no_prefix () = 64 + Alcotest.(check string) 65 + "no prefix unchanged" "https://github.com/user/repo.git" 66 + (Ctx.normalize_opam_url_string "https://github.com/user/repo.git") 67 + 68 + (* {1 normalize_opam_url tests} *) 69 + 70 + let test_normalize_opam_url () = 71 + let uri = 72 + Ctx.normalize_opam_url 73 + (Uri.of_string "git+https://github.com/user/repo.git") 74 + in 75 + Alcotest.(check string) 76 + "normalized" "https://github.com/user/repo.git" (Uri.to_string uri) 77 + 78 + (* {1 url_to_push_url tests} *) 79 + 80 + let test_push_url_github () = 81 + let result = Ctx.url_to_push_url "https://github.com/user/repo.git" in 82 + Alcotest.(check string) "github ssh" "git@github.com:user/repo.git" result 83 + 84 + let test_push_url_gitlab () = 85 + let result = Ctx.url_to_push_url "https://gitlab.com/org/lib.git" in 86 + Alcotest.(check string) "gitlab ssh" "git@gitlab.com:org/lib.git" result 87 + 88 + let test_push_url_ssh_passthrough () = 89 + let result = Ctx.url_to_push_url "git@github.com:user/repo.git" in 90 + Alcotest.(check string) 91 + "ssh passthrough" "git@github.com:user/repo.git" result 92 + 93 + let test_push_url_strips_git_plus () = 94 + let result = Ctx.url_to_push_url "git+https://github.com/user/repo.git" in 95 + Alcotest.(check string) "strips git+" "git@github.com:user/repo.git" result 96 + 97 + let suite = 98 + ( "ctx", 99 + [ 100 + (* pp_error *) 101 + Alcotest.test_case "pp config error" `Quick test_pp_config_error; 102 + Alcotest.test_case "pp monorepo dirty" `Quick test_pp_monorepo_dirty; 103 + Alcotest.test_case "pp package not found" `Quick test_pp_package_not_found; 104 + Alcotest.test_case "pp claude error" `Quick test_pp_claude_error; 105 + (* error_hint *) 106 + Alcotest.test_case "hint config" `Quick test_hint_config_error; 107 + Alcotest.test_case "hint dirty" `Quick test_hint_monorepo_dirty; 108 + Alcotest.test_case "hint not found" `Quick test_hint_package_not_found; 109 + Alcotest.test_case "hint claude" `Quick test_hint_claude_error; 110 + Alcotest.test_case "hint claude decode" `Quick 111 + test_hint_claude_decode_error; 112 + Alcotest.test_case "pp with hint" `Quick test_pp_error_with_hint; 113 + (* normalize *) 114 + Alcotest.test_case "normalize git+" `Quick test_normalize_git_plus; 115 + Alcotest.test_case "normalize no prefix" `Quick test_normalize_no_prefix; 116 + Alcotest.test_case "normalize url" `Quick test_normalize_opam_url; 117 + (* push url *) 118 + Alcotest.test_case "push github" `Quick test_push_url_github; 119 + Alcotest.test_case "push gitlab" `Quick test_push_url_gitlab; 120 + Alcotest.test_case "push ssh passthrough" `Quick 121 + test_push_url_ssh_passthrough; 122 + Alcotest.test_case "push strips git+" `Quick test_push_url_strips_git_plus; 123 + ] )
+76 -1
test/test_diff.ml
··· 1 - let suite = ("diff", []) 1 + (* Tests for diff module *) 2 + 3 + module Diff = Monopam.Diff 4 + 5 + (* {1 is_commit_sha tests} *) 6 + 7 + let test_sha_valid_short () = 8 + Alcotest.(check bool) "7 hex chars" true (Diff.is_commit_sha "abcdef1") 9 + 10 + let test_sha_valid_full () = 11 + Alcotest.(check bool) 12 + "40 hex chars" true 13 + (Diff.is_commit_sha "abcdef1234567890abcdef1234567890abcdef12") 14 + 15 + let test_sha_mixed_case () = 16 + Alcotest.(check bool) "mixed case hex" true (Diff.is_commit_sha "ABCDEF1") 17 + 18 + let test_sha_too_short () = 19 + Alcotest.(check bool) "6 chars too short" false (Diff.is_commit_sha "abcdef") 20 + 21 + let test_sha_empty () = 22 + Alcotest.(check bool) "empty" false (Diff.is_commit_sha "") 23 + 24 + let test_sha_not_hex () = 25 + Alcotest.(check bool) "not hex" false (Diff.is_commit_sha "ghijklm") 26 + 27 + let test_sha_with_spaces () = 28 + Alcotest.(check bool) "with spaces" false (Diff.is_commit_sha "abc def1") 29 + 30 + (* {1 pp_handle_pull_result tests} *) 31 + 32 + let test_pp_handle_pull_empty () = 33 + let r : Diff.handle_pull_result = 34 + { repos_pulled = []; repos_skipped = []; repos_failed = [] } 35 + in 36 + let s = Fmt.str "%a" Diff.pp_handle_pull_result r in 37 + ignore s 38 + 39 + let test_pp_handle_pull_with_data () = 40 + let r : Diff.handle_pull_result = 41 + { 42 + repos_pulled = [ ("eio", 3); ("dune", 1) ]; 43 + repos_skipped = [ "logs" ]; 44 + repos_failed = [ ("fmt", "network error") ]; 45 + } 46 + in 47 + let s = Fmt.str "%a" Diff.pp_handle_pull_result r in 48 + Alcotest.(check bool) "non-empty output" true (String.length s > 0) 49 + 50 + (* {1 pp_cherrypick_result tests} *) 51 + 52 + let test_pp_cherrypick () = 53 + let r : Diff.cherrypick_result = 54 + { 55 + repo_name = "eio"; 56 + commit_hash = "abc1234"; 57 + commit_subject = "Fix memory leak"; 58 + } 59 + in 60 + let s = Fmt.str "%a" Diff.pp_cherrypick_result r in 61 + Alcotest.(check bool) "non-empty" true (String.length s > 0) 62 + 63 + let suite = 64 + ( "diff", 65 + [ 66 + Alcotest.test_case "sha valid short" `Quick test_sha_valid_short; 67 + Alcotest.test_case "sha valid full" `Quick test_sha_valid_full; 68 + Alcotest.test_case "sha mixed case" `Quick test_sha_mixed_case; 69 + Alcotest.test_case "sha too short" `Quick test_sha_too_short; 70 + Alcotest.test_case "sha empty" `Quick test_sha_empty; 71 + Alcotest.test_case "sha not hex" `Quick test_sha_not_hex; 72 + Alcotest.test_case "sha with spaces" `Quick test_sha_with_spaces; 73 + Alcotest.test_case "pp pull empty" `Quick test_pp_handle_pull_empty; 74 + Alcotest.test_case "pp pull data" `Quick test_pp_handle_pull_with_data; 75 + Alcotest.test_case "pp cherrypick" `Quick test_pp_cherrypick; 76 + ] )
+174 -2
test/test_doctor.ml
··· 1 - (* Tests for doctor *) 1 + (* Tests for doctor module *) 2 + 3 + module Doctor = Monopam.Doctor 4 + 5 + (* {1 health_to_exit_code tests} *) 6 + 7 + let test_healthy_exit_code () = 8 + Alcotest.(check int) "healthy" 0 (Doctor.health_to_exit_code Doctor.Healthy) 2 9 3 - let suite = ("doctor", []) 10 + let test_warning_exit_code () = 11 + Alcotest.(check int) "warning" 1 (Doctor.health_to_exit_code Doctor.Warning) 12 + 13 + let test_critical_exit_code () = 14 + Alcotest.(check int) "critical" 2 (Doctor.health_to_exit_code Doctor.Critical) 15 + 16 + (* {1 compute_health tests} *) 17 + 18 + let empty_summary : Doctor.report_summary = 19 + { 20 + repos_total = 5; 21 + repos_need_sync = 0; 22 + repos_behind_upstream = 0; 23 + verse_divergences = 0; 24 + } 25 + 26 + let mk_report ?(summary = empty_summary) ?(repos = []) ?(recommendations = []) 27 + ?(warnings = []) () : Doctor.report = 28 + { 29 + timestamp = "2026-03-16T12:00:00Z"; 30 + workspace = "/tmp/test"; 31 + report_summary = summary; 32 + repos; 33 + recommendations; 34 + warnings; 35 + } 36 + 37 + let test_compute_health_healthy () = 38 + let report = mk_report () in 39 + Alcotest.(check int) 40 + "healthy" 0 41 + (Doctor.health_to_exit_code (Doctor.compute_health report)) 42 + 43 + let test_compute_health_warning_sync () = 44 + let summary = { empty_summary with repos_need_sync = 2 } in 45 + let report = mk_report ~summary () in 46 + Alcotest.(check int) 47 + "warning" 1 48 + (Doctor.health_to_exit_code (Doctor.compute_health report)) 49 + 50 + let test_compute_health_warning_behind () = 51 + let summary = { empty_summary with repos_behind_upstream = 1 } in 52 + let report = mk_report ~summary () in 53 + Alcotest.(check int) 54 + "warning" 1 55 + (Doctor.health_to_exit_code (Doctor.compute_health report)) 56 + 57 + let test_compute_health_warning_diverged () = 58 + let summary = { empty_summary with verse_divergences = 3 } in 59 + let report = mk_report ~summary () in 60 + Alcotest.(check int) 61 + "warning" 1 62 + (Doctor.health_to_exit_code (Doctor.compute_health report)) 63 + 64 + let test_compute_health_critical_warnings () = 65 + let report = mk_report ~warnings:[ "something is wrong" ] () in 66 + Alcotest.(check int) 67 + "critical" 2 68 + (Doctor.health_to_exit_code (Doctor.compute_health report)) 69 + 70 + let test_compute_health_critical_recommendation () = 71 + let action : Doctor.action = 72 + { 73 + action_priority = Doctor.Critical; 74 + description = "Fix now"; 75 + command = Some "monopam sync"; 76 + } 77 + in 78 + let report = mk_report ~recommendations:[ action ] () in 79 + Alcotest.(check int) 80 + "critical" 2 81 + (Doctor.health_to_exit_code (Doctor.compute_health report)) 82 + 83 + (* {1 has_issues tests} *) 84 + 85 + let test_has_issues_none () = 86 + Alcotest.(check bool) "no issues" false (Doctor.has_issues (mk_report ())) 87 + 88 + let test_has_issues_sync () = 89 + let summary = { empty_summary with repos_need_sync = 1 } in 90 + Alcotest.(check bool) 91 + "sync needed" true 92 + (Doctor.has_issues (mk_report ~summary ())) 93 + 94 + let test_has_issues_behind () = 95 + let summary = { empty_summary with repos_behind_upstream = 1 } in 96 + Alcotest.(check bool) 97 + "behind" true 98 + (Doctor.has_issues (mk_report ~summary ())) 99 + 100 + let test_has_issues_warnings () = 101 + Alcotest.(check bool) 102 + "warnings" true 103 + (Doctor.has_issues (mk_report ~warnings:[ "w" ] ())) 104 + 105 + let test_has_issues_recommendations () = 106 + let action : Doctor.action = 107 + { action_priority = Doctor.Low; description = "Consider X"; command = None } 108 + in 109 + Alcotest.(check bool) 110 + "recommendations" true 111 + (Doctor.has_issues (mk_report ~recommendations:[ action ] ())) 112 + 113 + (* {1 pp tests} *) 114 + 115 + let test_pp_priority () = 116 + let s = Fmt.str "%a" Doctor.pp_priority Doctor.Critical in 117 + Alcotest.(check bool) "non-empty" true (String.length s > 0) 118 + 119 + let test_pp_category () = 120 + let s = Fmt.str "%a" Doctor.pp_category Doctor.Bug_fix in 121 + Alcotest.(check bool) "non-empty" true (String.length s > 0) 122 + 123 + let test_pp_recommendation () = 124 + let s = Fmt.str "%a" Doctor.pp_recommendation Doctor.Merge_now in 125 + Alcotest.(check bool) "non-empty" true (String.length s > 0) 126 + 127 + let test_pp_conflict_risk () = 128 + let s = Fmt.str "%a" Doctor.pp_conflict_risk Doctor.None_risk in 129 + Alcotest.(check bool) "non-empty" true (String.length s > 0) 130 + 131 + let test_pp_report () = 132 + let report = mk_report () in 133 + let s = Fmt.str "%a" Doctor.pp_report report in 134 + Alcotest.(check bool) "non-empty" true (String.length s > 0) 135 + 136 + (* {1 JSON roundtrip} *) 137 + 138 + let test_to_json () = 139 + let report = mk_report () in 140 + let json = Doctor.to_json report in 141 + Alcotest.(check bool) "non-empty JSON" true (String.length json > 0) 142 + 143 + let suite = 144 + ( "doctor", 145 + [ 146 + (* exit codes *) 147 + Alcotest.test_case "healthy exit" `Quick test_healthy_exit_code; 148 + Alcotest.test_case "warning exit" `Quick test_warning_exit_code; 149 + Alcotest.test_case "critical exit" `Quick test_critical_exit_code; 150 + (* compute_health *) 151 + Alcotest.test_case "health healthy" `Quick test_compute_health_healthy; 152 + Alcotest.test_case "health sync" `Quick test_compute_health_warning_sync; 153 + Alcotest.test_case "health behind" `Quick 154 + test_compute_health_warning_behind; 155 + Alcotest.test_case "health diverged" `Quick 156 + test_compute_health_warning_diverged; 157 + Alcotest.test_case "health critical warn" `Quick 158 + test_compute_health_critical_warnings; 159 + Alcotest.test_case "health critical rec" `Quick 160 + test_compute_health_critical_recommendation; 161 + (* has_issues *) 162 + Alcotest.test_case "no issues" `Quick test_has_issues_none; 163 + Alcotest.test_case "issues sync" `Quick test_has_issues_sync; 164 + Alcotest.test_case "issues behind" `Quick test_has_issues_behind; 165 + Alcotest.test_case "issues warnings" `Quick test_has_issues_warnings; 166 + Alcotest.test_case "issues recs" `Quick test_has_issues_recommendations; 167 + (* pp *) 168 + Alcotest.test_case "pp priority" `Quick test_pp_priority; 169 + Alcotest.test_case "pp category" `Quick test_pp_category; 170 + Alcotest.test_case "pp recommendation" `Quick test_pp_recommendation; 171 + Alcotest.test_case "pp conflict_risk" `Quick test_pp_conflict_risk; 172 + Alcotest.test_case "pp report" `Quick test_pp_report; 173 + (* JSON *) 174 + Alcotest.test_case "to_json" `Quick test_to_json; 175 + ] )
+65 -2
test/test_feature.ml
··· 1 - (* Tests for feature *) 1 + (* Tests for feature module *) 2 + 3 + module Feature = Monopam.Feature 4 + 5 + (* {1 pp_error tests} *) 6 + 7 + let test_pp_git_error () = 8 + let e = Feature.Git_error (Monopam.Git_cli.Not_a_repo (Fpath.v "/tmp/bad")) in 9 + let s = Fmt.str "%a" Feature.pp_error e in 10 + Alcotest.(check bool) "non-empty" true (String.length s > 0) 11 + 12 + let test_pp_feature_exists () = 13 + let s = Fmt.str "%a" Feature.pp_error (Feature.Feature_exists "my-feature") in 14 + Alcotest.(check bool) "has name" true (String.length s > 0) 15 + 16 + let test_pp_feature_not_found () = 17 + let s = 18 + Fmt.str "%a" Feature.pp_error (Feature.Feature_not_found "my-feature") 19 + in 20 + Alcotest.(check bool) "has name" true (String.length s > 0) 21 + 22 + let test_pp_config_error () = 23 + let s = Fmt.str "%a" Feature.pp_error (Feature.Config_error "bad config") in 24 + Alcotest.(check bool) "has message" true (String.length s > 0) 25 + 26 + (* {1 pp_error_with_hint tests} *) 27 + 28 + let test_pp_with_hint_exists () = 29 + let s = 30 + Fmt.str "%a" Feature.pp_error_with_hint 31 + (Feature.Feature_exists "my-feature") 32 + in 33 + Alcotest.(check bool) "has hint" true (String.length s > 0) 34 + 35 + let test_pp_with_hint_not_found () = 36 + let s = 37 + Fmt.str "%a" Feature.pp_error_with_hint 38 + (Feature.Feature_not_found "my-feature") 39 + in 40 + Alcotest.(check bool) "has hint" true (String.length s > 0) 41 + 42 + (* {1 pp_entry tests} *) 43 + 44 + let test_pp_entry () = 45 + let entry : Feature.entry = 46 + { 47 + name = "my-feature"; 48 + path = Fpath.v "/home/user/work/my-feature"; 49 + branch = "my-feature"; 50 + } 51 + in 52 + let s = Fmt.str "%a" Feature.pp_entry entry in 53 + Alcotest.(check bool) "has name" true (String.length s > 0) 2 54 3 - let suite = ("feature", []) 55 + let suite = 56 + ( "feature", 57 + [ 58 + Alcotest.test_case "pp git error" `Quick test_pp_git_error; 59 + Alcotest.test_case "pp exists" `Quick test_pp_feature_exists; 60 + Alcotest.test_case "pp not found" `Quick test_pp_feature_not_found; 61 + Alcotest.test_case "pp config error" `Quick test_pp_config_error; 62 + Alcotest.test_case "pp with hint exists" `Quick test_pp_with_hint_exists; 63 + Alcotest.test_case "pp with hint not found" `Quick 64 + test_pp_with_hint_not_found; 65 + Alcotest.test_case "pp entry" `Quick test_pp_entry; 66 + ] )
+192 -2
test/test_fork_join.ml
··· 1 - (* Tests for fork_join *) 1 + (* Tests for fork_join module *) 2 + 3 + module FJ = Monopam.Fork_join 4 + 5 + (* {1 pp_error tests} *) 6 + 7 + let test_pp_config_error () = 8 + let s = Fmt.str "%a" FJ.pp_error (FJ.Config_error "bad") in 9 + Alcotest.(check bool) "non-empty" true (String.length s > 0) 10 + 11 + let test_pp_subtree_not_found () = 12 + let s = Fmt.str "%a" FJ.pp_error (FJ.Subtree_not_found "eio") in 13 + Alcotest.(check bool) "non-empty" true (String.length s > 0) 14 + 15 + let test_pp_src_already_exists () = 16 + let s = Fmt.str "%a" FJ.pp_error (FJ.Src_already_exists "eio") in 17 + Alcotest.(check bool) "non-empty" true (String.length s > 0) 18 + 19 + let test_pp_src_not_found () = 20 + let s = Fmt.str "%a" FJ.pp_error (FJ.Src_not_found "eio") in 21 + Alcotest.(check bool) "non-empty" true (String.length s > 0) 22 + 23 + let test_pp_subtree_already_exists () = 24 + let s = Fmt.str "%a" FJ.pp_error (FJ.Subtree_already_exists "eio") in 25 + Alcotest.(check bool) "non-empty" true (String.length s > 0) 26 + 27 + let test_pp_no_opam_files () = 28 + let s = Fmt.str "%a" FJ.pp_error (FJ.No_opam_files "eio") in 29 + Alcotest.(check bool) "non-empty" true (String.length s > 0) 30 + 31 + let test_pp_user_cancelled () = 32 + let s = Fmt.str "%a" FJ.pp_error FJ.User_cancelled in 33 + Alcotest.(check bool) "non-empty" true (String.length s > 0) 34 + 35 + (* {1 error_hint tests} *) 36 + 37 + let test_hint_config () = 38 + match FJ.error_hint (FJ.Config_error "bad") with 39 + | Some h -> Alcotest.(check bool) "has hint" true (String.length h > 0) 40 + | None -> Alcotest.fail "expected hint" 41 + 42 + let test_hint_subtree_not_found () = 43 + match FJ.error_hint (FJ.Subtree_not_found "eio") with 44 + | Some h -> Alcotest.(check bool) "has hint" true (String.length h > 0) 45 + | None -> Alcotest.fail "expected hint" 46 + 47 + let test_hint_src_already () = 48 + match FJ.error_hint (FJ.Src_already_exists "eio") with 49 + | Some h -> Alcotest.(check bool) "has hint" true (String.length h > 0) 50 + | None -> Alcotest.fail "expected hint" 2 51 3 - let suite = ("fork_join", []) 52 + let test_hint_no_opam () = 53 + match FJ.error_hint (FJ.No_opam_files "eio") with 54 + | Some h -> Alcotest.(check bool) "has hint" true (String.length h > 0) 55 + | None -> Alcotest.fail "expected hint" 56 + 57 + let test_hint_user_cancelled () = 58 + Alcotest.(check (option string)) 59 + "no hint" None 60 + (FJ.error_hint FJ.User_cancelled) 61 + 62 + (* {1 is_local_path tests} *) 63 + 64 + let test_local_path_relative () = 65 + Alcotest.(check bool) "relative" true (FJ.is_local_path "./my-repo") 66 + 67 + let test_local_path_absolute () = 68 + Alcotest.(check bool) "absolute" true (FJ.is_local_path "/home/user/repo") 69 + 70 + let test_local_path_bare_name () = 71 + Alcotest.(check bool) "bare name" true (FJ.is_local_path "my-repo") 72 + 73 + let test_not_local_https () = 74 + Alcotest.(check bool) 75 + "https is not local" false 76 + (FJ.is_local_path "https://github.com/user/repo") 77 + 78 + let test_not_local_git () = 79 + Alcotest.(check bool) 80 + "git@ is not local" false 81 + (FJ.is_local_path "git@github.com:user/repo") 82 + 83 + let test_not_local_git_plus () = 84 + Alcotest.(check bool) 85 + "git+ is not local" false 86 + (FJ.is_local_path "git+https://github.com/user/repo") 87 + 88 + let test_not_local_ssh () = 89 + Alcotest.(check bool) 90 + "ssh:// is not local" false 91 + (FJ.is_local_path "ssh://git@github.com/user/repo") 92 + 93 + (* {1 pp_action tests} *) 94 + 95 + let test_pp_action_create_dir () = 96 + let s = 97 + Fmt.str "%a" FJ.pp_action (FJ.Create_directory (Fpath.v "/tmp/new")) 98 + in 99 + Alcotest.(check bool) "non-empty" true (String.length s > 0) 100 + 101 + let test_pp_action_git_clone () = 102 + let s = 103 + Fmt.str "%a" FJ.pp_action 104 + (FJ.Git_clone 105 + { 106 + url = "https://github.com/user/repo"; 107 + dest = Fpath.v "/tmp/repo"; 108 + branch = "main"; 109 + }) 110 + in 111 + Alcotest.(check bool) "non-empty" true (String.length s > 0) 112 + 113 + (* {1 pp_discovery tests} *) 114 + 115 + let test_pp_discovery () = 116 + let d : FJ.discovery = 117 + { 118 + mono_exists = true; 119 + src_exists = false; 120 + has_subtree_history = true; 121 + remote_accessible = Some true; 122 + opam_files = [ "eio"; "eio-main" ]; 123 + local_path_is_repo = None; 124 + } 125 + in 126 + let s = Fmt.str "%a" FJ.pp_discovery d in 127 + Alcotest.(check bool) "non-empty" true (String.length s > 0) 128 + 129 + (* {1 pp_fork_result tests} *) 130 + 131 + let test_pp_fork_result () = 132 + let r : FJ.fork_result = 133 + { 134 + name = "eio"; 135 + split_commit = "abcdef1234567890abcdef1234567890abcdef12"; 136 + src_path = Fpath.v "/home/user/src/eio"; 137 + push_url = Some "git@github.com:user/eio.git"; 138 + packages_created = [ "eio"; "eio-main" ]; 139 + } 140 + in 141 + let s = Fmt.str "%a" FJ.pp_fork_result r in 142 + Alcotest.(check bool) "non-empty" true (String.length s > 0) 143 + 144 + (* {1 pp_join_result tests} *) 145 + 146 + let test_pp_join_result () = 147 + let r : FJ.join_result = 148 + { 149 + name = "eio"; 150 + source_url = "https://github.com/ocaml/eio"; 151 + upstream_url = None; 152 + packages_added = [ "eio" ]; 153 + from_handle = None; 154 + } 155 + in 156 + let s = Fmt.str "%a" FJ.pp_join_result r in 157 + Alcotest.(check bool) "non-empty" true (String.length s > 0) 158 + 159 + let suite = 160 + ( "fork_join", 161 + [ 162 + (* pp_error *) 163 + Alcotest.test_case "pp config" `Quick test_pp_config_error; 164 + Alcotest.test_case "pp subtree not found" `Quick test_pp_subtree_not_found; 165 + Alcotest.test_case "pp src exists" `Quick test_pp_src_already_exists; 166 + Alcotest.test_case "pp src not found" `Quick test_pp_src_not_found; 167 + Alcotest.test_case "pp subtree exists" `Quick 168 + test_pp_subtree_already_exists; 169 + Alcotest.test_case "pp no opam" `Quick test_pp_no_opam_files; 170 + Alcotest.test_case "pp cancelled" `Quick test_pp_user_cancelled; 171 + (* error_hint *) 172 + Alcotest.test_case "hint config" `Quick test_hint_config; 173 + Alcotest.test_case "hint subtree" `Quick test_hint_subtree_not_found; 174 + Alcotest.test_case "hint src exists" `Quick test_hint_src_already; 175 + Alcotest.test_case "hint no opam" `Quick test_hint_no_opam; 176 + Alcotest.test_case "hint cancelled" `Quick test_hint_user_cancelled; 177 + (* is_local_path *) 178 + Alcotest.test_case "local relative" `Quick test_local_path_relative; 179 + Alcotest.test_case "local absolute" `Quick test_local_path_absolute; 180 + Alcotest.test_case "local bare" `Quick test_local_path_bare_name; 181 + Alcotest.test_case "not local https" `Quick test_not_local_https; 182 + Alcotest.test_case "not local git@" `Quick test_not_local_git; 183 + Alcotest.test_case "not local git+" `Quick test_not_local_git_plus; 184 + Alcotest.test_case "not local ssh" `Quick test_not_local_ssh; 185 + (* pp_action *) 186 + Alcotest.test_case "pp action dir" `Quick test_pp_action_create_dir; 187 + Alcotest.test_case "pp action clone" `Quick test_pp_action_git_clone; 188 + (* pp_discovery *) 189 + Alcotest.test_case "pp discovery" `Quick test_pp_discovery; 190 + (* pp results *) 191 + Alcotest.test_case "pp fork result" `Quick test_pp_fork_result; 192 + Alcotest.test_case "pp join result" `Quick test_pp_join_result; 193 + ] )
+173 -2
test/test_forks.ml
··· 1 - (* Tests for forks *) 1 + (* Tests for forks module *) 2 + 3 + module Forks = Monopam.Forks 4 + 5 + (* {1 pp_relationship tests} *) 6 + 7 + let test_pp_same_url () = 8 + let s = Fmt.str "%a" Forks.pp_relationship Forks.Same_url in 9 + Alcotest.(check string) "same URL" "same URL" s 10 + 11 + let test_pp_same_commit () = 12 + let s = Fmt.str "%a" Forks.pp_relationship Forks.Same_commit in 13 + Alcotest.(check string) "same commit" "same commit" s 14 + 15 + let test_pp_ahead () = 16 + let s = Fmt.str "%a" Forks.pp_relationship (Forks.I_am_ahead 3) in 17 + Alcotest.(check bool) "contains number" true (String.length s > 0) 18 + 19 + let test_pp_behind () = 20 + let s = Fmt.str "%a" Forks.pp_relationship (Forks.I_am_behind 5) in 21 + Alcotest.(check bool) "contains number" true (String.length s > 0) 22 + 23 + let test_pp_diverged () = 24 + let s = 25 + Fmt.str "%a" Forks.pp_relationship 26 + (Forks.Diverged { common_ancestor = "abc"; my_ahead = 2; their_ahead = 4 }) 27 + in 28 + Alcotest.(check bool) "non-empty" true (String.length s > 0) 29 + 30 + let test_pp_unrelated () = 31 + let s = Fmt.str "%a" Forks.pp_relationship Forks.Unrelated in 32 + Alcotest.(check string) "unrelated" "unrelated" s 33 + 34 + let test_pp_not_fetched () = 35 + let s = Fmt.str "%a" Forks.pp_relationship Forks.Not_fetched in 36 + Alcotest.(check string) "not fetched" "not fetched" s 37 + 38 + (* {1 is_actionable tests} *) 39 + 40 + let test_actionable_behind () = 41 + Alcotest.(check bool) 42 + "behind" true 43 + (Forks.is_actionable (Forks.I_am_behind 3)) 44 + 45 + let test_actionable_diverged () = 46 + Alcotest.(check bool) 47 + "diverged" true 48 + (Forks.is_actionable 49 + (Forks.Diverged 50 + { common_ancestor = "abc"; my_ahead = 1; their_ahead = 2 })) 51 + 52 + let test_not_actionable_same_url () = 53 + Alcotest.(check bool) "same url" false (Forks.is_actionable Forks.Same_url) 54 + 55 + let test_not_actionable_same_commit () = 56 + Alcotest.(check bool) 57 + "same commit" false 58 + (Forks.is_actionable Forks.Same_commit) 59 + 60 + let test_not_actionable_ahead () = 61 + Alcotest.(check bool) "ahead" false (Forks.is_actionable (Forks.I_am_ahead 3)) 62 + 63 + let test_not_actionable_unrelated () = 64 + Alcotest.(check bool) "unrelated" false (Forks.is_actionable Forks.Unrelated) 65 + 66 + let test_not_actionable_not_fetched () = 67 + Alcotest.(check bool) 68 + "not fetched" false 69 + (Forks.is_actionable Forks.Not_fetched) 70 + 71 + (* {1 URL utility tests} *) 72 + 73 + let test_normalize_url_strips_git_plus () = 74 + let url = 75 + Forks.normalize_url (Uri.of_string "git+https://github.com/user/repo.git") 76 + in 77 + let s = Uri.to_string url in 78 + Alcotest.(check bool) 79 + "no git+ prefix" true 80 + (not (String.starts_with ~prefix:"git+" s)) 81 + 82 + let test_normalize_url_strips_dot_git () = 83 + let url = 84 + Forks.normalize_url (Uri.of_string "https://github.com/user/repo.git") 85 + in 86 + let s = Uri.to_string url in 87 + Alcotest.(check bool) 88 + "no .git suffix" true 89 + (not (String.ends_with ~suffix:".git" s)) 90 + 91 + let test_urls_equal_same () = 92 + let u1 = Uri.of_string "https://github.com/user/repo.git" in 93 + let u2 = Uri.of_string "https://github.com/user/repo.git" in 94 + Alcotest.(check bool) "same URL" true (Forks.urls_equal u1 u2) 95 + 96 + let test_urls_equal_with_without_dot_git () = 97 + let u1 = Uri.of_string "https://github.com/user/repo.git" in 98 + let u2 = Uri.of_string "https://github.com/user/repo" in 99 + Alcotest.(check bool) ".git vs no .git" true (Forks.urls_equal u1 u2) 100 + 101 + let test_urls_equal_git_plus () = 102 + let u1 = Uri.of_string "git+https://github.com/user/repo.git" in 103 + let u2 = Uri.of_string "https://github.com/user/repo.git" in 104 + Alcotest.(check bool) "git+ = plain" true (Forks.urls_equal u1 u2) 105 + 106 + let test_urls_not_equal () = 107 + let u1 = Uri.of_string "https://github.com/user/repo1.git" in 108 + let u2 = Uri.of_string "https://github.com/user/repo2.git" in 109 + Alcotest.(check bool) "different" false (Forks.urls_equal u1 u2) 110 + 111 + let test_repo_basename_simple () = 112 + let name = 113 + Forks.repo_basename (Uri.of_string "https://github.com/user/eio.git") 114 + in 115 + Alcotest.(check string) "basename" "eio" name 116 + 117 + let test_repo_basename_no_git_suffix () = 118 + let name = 119 + Forks.repo_basename (Uri.of_string "https://github.com/user/eio") 120 + in 121 + Alcotest.(check string) "basename" "eio" name 122 + 123 + (* {1 pp tests} *) 124 + 125 + let test_pp_empty () = 126 + let t : Forks.t = { repos = [] } in 127 + let s = Fmt.str "%a" Forks.pp t in 128 + ignore s 129 + 130 + let test_pp_summary_empty () = 131 + let t : Forks.t = { repos = [] } in 132 + let s = Fmt.str "%a" Forks.pp_summary t in 133 + ignore s 2 134 3 - let suite = ("forks", []) 135 + let suite = 136 + ( "forks", 137 + [ 138 + (* pp_relationship *) 139 + Alcotest.test_case "pp same url" `Quick test_pp_same_url; 140 + Alcotest.test_case "pp same commit" `Quick test_pp_same_commit; 141 + Alcotest.test_case "pp ahead" `Quick test_pp_ahead; 142 + Alcotest.test_case "pp behind" `Quick test_pp_behind; 143 + Alcotest.test_case "pp diverged" `Quick test_pp_diverged; 144 + Alcotest.test_case "pp unrelated" `Quick test_pp_unrelated; 145 + Alcotest.test_case "pp not fetched" `Quick test_pp_not_fetched; 146 + (* is_actionable *) 147 + Alcotest.test_case "actionable behind" `Quick test_actionable_behind; 148 + Alcotest.test_case "actionable diverged" `Quick test_actionable_diverged; 149 + Alcotest.test_case "not actionable same" `Quick 150 + test_not_actionable_same_url; 151 + Alcotest.test_case "not actionable commit" `Quick 152 + test_not_actionable_same_commit; 153 + Alcotest.test_case "not actionable ahead" `Quick test_not_actionable_ahead; 154 + Alcotest.test_case "not actionable unrelated" `Quick 155 + test_not_actionable_unrelated; 156 + Alcotest.test_case "not actionable unfetched" `Quick 157 + test_not_actionable_not_fetched; 158 + (* URL utilities *) 159 + Alcotest.test_case "normalize strips git+" `Quick 160 + test_normalize_url_strips_git_plus; 161 + Alcotest.test_case "normalize strips .git" `Quick 162 + test_normalize_url_strips_dot_git; 163 + Alcotest.test_case "urls equal same" `Quick test_urls_equal_same; 164 + Alcotest.test_case "urls equal .git" `Quick 165 + test_urls_equal_with_without_dot_git; 166 + Alcotest.test_case "urls equal git+" `Quick test_urls_equal_git_plus; 167 + Alcotest.test_case "urls not equal" `Quick test_urls_not_equal; 168 + Alcotest.test_case "basename .git" `Quick test_repo_basename_simple; 169 + Alcotest.test_case "basename no .git" `Quick 170 + test_repo_basename_no_git_suffix; 171 + (* pp *) 172 + Alcotest.test_case "pp empty" `Quick test_pp_empty; 173 + Alcotest.test_case "pp summary empty" `Quick test_pp_summary_empty; 174 + ] )
+62 -2
test/test_git_cli.ml
··· 1 - (* Tests for git_cli *) 1 + (* Tests for git_cli module - I/O heavy, test pp_error only *) 2 2 3 - let suite = ("git_cli", []) 3 + module Git_cli = Monopam.Git_cli 4 + 5 + (* {1 pp_error tests} *) 6 + 7 + let test_pp_command_failed () = 8 + let err = 9 + Git_cli.Command_failed 10 + ( "git push origin main", 11 + { exit_code = 1; stdout = ""; stderr = "rejected" } ) 12 + in 13 + let s = Fmt.str "%a" Git_cli.pp_error err in 14 + Alcotest.(check bool) "has command" true (String.length s > 0) 15 + 16 + let test_pp_not_a_repo () = 17 + let s = 18 + Fmt.str "%a" Git_cli.pp_error (Git_cli.Not_a_repo (Fpath.v "/tmp/bad")) 19 + in 20 + Alcotest.(check bool) "has path" true (String.length s > 0) 21 + 22 + let test_pp_dirty_worktree () = 23 + let s = 24 + Fmt.str "%a" Git_cli.pp_error (Git_cli.Dirty_worktree (Fpath.v "/tmp/repo")) 25 + in 26 + Alcotest.(check bool) "has path" true (String.length s > 0) 27 + 28 + let test_pp_remote_not_found () = 29 + let s = Fmt.str "%a" Git_cli.pp_error (Git_cli.Remote_not_found "upstream") in 30 + Alcotest.(check bool) "has name" true (String.length s > 0) 31 + 32 + let test_pp_branch_not_found () = 33 + let s = Fmt.str "%a" Git_cli.pp_error (Git_cli.Branch_not_found "develop") in 34 + Alcotest.(check bool) "has name" true (String.length s > 0) 35 + 36 + let test_pp_subtree_prefix_exists () = 37 + let s = Fmt.str "%a" Git_cli.pp_error (Git_cli.Subtree_prefix_exists "eio") in 38 + Alcotest.(check bool) "has prefix" true (String.length s > 0) 39 + 40 + let test_pp_subtree_prefix_missing () = 41 + let s = 42 + Fmt.str "%a" Git_cli.pp_error (Git_cli.Subtree_prefix_missing "eio") 43 + in 44 + Alcotest.(check bool) "has prefix" true (String.length s > 0) 45 + 46 + let test_pp_io_error () = 47 + let s = Fmt.str "%a" Git_cli.pp_error (Git_cli.Io_error "disk full") in 48 + Alcotest.(check bool) "has message" true (String.length s > 0) 49 + 50 + let suite = 51 + ( "git_cli", 52 + [ 53 + Alcotest.test_case "pp command failed" `Quick test_pp_command_failed; 54 + Alcotest.test_case "pp not a repo" `Quick test_pp_not_a_repo; 55 + Alcotest.test_case "pp dirty worktree" `Quick test_pp_dirty_worktree; 56 + Alcotest.test_case "pp remote not found" `Quick test_pp_remote_not_found; 57 + Alcotest.test_case "pp branch not found" `Quick test_pp_branch_not_found; 58 + Alcotest.test_case "pp subtree exists" `Quick 59 + test_pp_subtree_prefix_exists; 60 + Alcotest.test_case "pp subtree missing" `Quick 61 + test_pp_subtree_prefix_missing; 62 + Alcotest.test_case "pp io error" `Quick test_pp_io_error; 63 + ] )
+80 -1
test/test_import.ml
··· 1 - let suite = ("import", []) 1 + (* Tests for import module *) 2 + 3 + module Import = Monopam.Import 4 + 5 + (* {1 repo_name_from_url tests} *) 6 + 7 + let test_repo_name_github () = 8 + Alcotest.(check string) 9 + "github" "eio" 10 + (Import.repo_name_from_url "https://github.com/mirage/eio.git") 11 + 12 + let test_repo_name_git_plus () = 13 + Alcotest.(check string) 14 + "git+" "dune" 15 + (Import.repo_name_from_url "git+https://github.com/ocaml/dune") 16 + 17 + let test_repo_name_no_suffix () = 18 + Alcotest.(check string) 19 + "no .git" "logs" 20 + (Import.repo_name_from_url "https://github.com/dbuenzli/logs") 21 + 22 + let test_repo_name_plain_path () = 23 + Alcotest.(check string) 24 + "plain path" "fmt" 25 + (Import.repo_name_from_url "https://github.com/dbuenzli/fmt.git") 26 + 27 + let test_repo_name_ssh () = 28 + Alcotest.(check string) 29 + "ssh" "repo" 30 + (Import.repo_name_from_url "git+git@github.com:user/repo.git") 31 + 32 + (* {1 normalize_url tests} *) 33 + 34 + let test_normalize_https () = 35 + Alcotest.(check string) 36 + "https" "git+https://example.com/repo" 37 + (Import.normalize_url "https://example.com/repo") 38 + 39 + let test_normalize_already_prefixed () = 40 + Alcotest.(check string) 41 + "already prefixed" "git+https://example.com/repo" 42 + (Import.normalize_url "git+https://example.com/repo") 43 + 44 + let test_normalize_git_at () = 45 + Alcotest.(check string) 46 + "git@" "git+git@github.com:user/repo.git" 47 + (Import.normalize_url "git@github.com:user/repo.git") 48 + 49 + let test_normalize_http () = 50 + (* http is converted to https *) 51 + let result = Import.normalize_url "http://example.com/repo" in 52 + Alcotest.(check bool) 53 + "https prefix" true 54 + (String.starts_with ~prefix:"git+https" result) 55 + 56 + (* {1 timestamp test} *) 57 + 58 + let test_timestamp_format () = 59 + let ts = Import.timestamp () in 60 + (* Should be ISO-ish format with at least year-month-day *) 61 + Alcotest.(check bool) "has year" true (String.length ts >= 10) 62 + 63 + let suite = 64 + ( "import", 65 + [ 66 + (* repo_name_from_url *) 67 + Alcotest.test_case "name github" `Quick test_repo_name_github; 68 + Alcotest.test_case "name git+" `Quick test_repo_name_git_plus; 69 + Alcotest.test_case "name no suffix" `Quick test_repo_name_no_suffix; 70 + Alcotest.test_case "name plain path" `Quick test_repo_name_plain_path; 71 + Alcotest.test_case "name ssh" `Quick test_repo_name_ssh; 72 + (* normalize_url *) 73 + Alcotest.test_case "normalize https" `Quick test_normalize_https; 74 + Alcotest.test_case "normalize prefixed" `Quick 75 + test_normalize_already_prefixed; 76 + Alcotest.test_case "normalize git@" `Quick test_normalize_git_at; 77 + Alcotest.test_case "normalize http" `Quick test_normalize_http; 78 + (* timestamp *) 79 + Alcotest.test_case "timestamp format" `Quick test_timestamp_format; 80 + ] )
+7 -1
test/test_init.ml
··· 1 - let suite = ("init", []) 1 + (* Tests for init module - I/O heavy, verify module alias works *) 2 + 3 + let test_module_alias () = 4 + ignore (Monopam.Init.ensure : proc:_ -> fs:_ -> config:_ -> _) 5 + 6 + let suite = 7 + ("init", [ Alcotest.test_case "module alias" `Quick test_module_alias ])
+185 -1
test/test_mono_lock.ml
··· 1 - let suite = ("mono_lock", []) 1 + (* Tests for mono_lock module *) 2 + 3 + module ML = Monopam.Mono_lock 4 + 5 + (* {1 empty tests} *) 6 + 7 + let test_empty () = 8 + let t = ML.empty in 9 + Alcotest.(check (list string)) "empty names" [] (ML.names t); 10 + Alcotest.(check (list (pair string pass))) "empty list" [] (ML.to_list t) 11 + 12 + (* {1 add / find / remove tests} *) 13 + 14 + let test_add_and_find () = 15 + let entry : ML.entry = 16 + { url = "https://github.com/mirage/eio.git"; ref_ = "main" } 17 + in 18 + let t = ML.add ML.empty ~name:"eio" entry in 19 + match ML.find t ~name:"eio" with 20 + | Some e -> 21 + Alcotest.(check string) "url" "https://github.com/mirage/eio.git" e.url; 22 + Alcotest.(check string) "ref" "main" e.ref_ 23 + | None -> Alcotest.fail "expected to find eio" 24 + 25 + let test_find_missing () = 26 + Alcotest.(check (option pass)) "missing" None (ML.find ML.empty ~name:"eio") 27 + 28 + let test_add_replaces () = 29 + let e1 : ML.entry = { url = "https://example.com/a.git"; ref_ = "v1" } in 30 + let e2 : ML.entry = { url = "https://example.com/a.git"; ref_ = "v2" } in 31 + let t = ML.add ML.empty ~name:"a" e1 in 32 + let t = ML.add t ~name:"a" e2 in 33 + match ML.find t ~name:"a" with 34 + | Some e -> Alcotest.(check string) "updated ref" "v2" e.ref_ 35 + | None -> Alcotest.fail "expected to find a" 36 + 37 + let test_remove () = 38 + let entry : ML.entry = { url = "https://example.com/a.git"; ref_ = "main" } in 39 + let t = ML.add ML.empty ~name:"a" entry in 40 + let t = ML.remove t ~name:"a" in 41 + Alcotest.(check (option pass)) "removed" None (ML.find t ~name:"a") 42 + 43 + let test_remove_nonexistent () = 44 + let t = ML.remove ML.empty ~name:"nonexistent" in 45 + Alcotest.(check (list string)) "still empty" [] (ML.names t) 46 + 47 + (* {1 names and to_list tests} *) 48 + 49 + let test_names () = 50 + let mk name ref_ : ML.entry = 51 + { url = "https://example.com/" ^ name ^ ".git"; ref_ } 52 + in 53 + let t = ML.add ML.empty ~name:"b" (mk "b" "main") in 54 + let t = ML.add t ~name:"a" (mk "a" "main") in 55 + let names = ML.names t in 56 + Alcotest.(check int) "two names" 2 (List.length names); 57 + Alcotest.(check bool) "has a" true (List.mem "a" names); 58 + Alcotest.(check bool) "has b" true (List.mem "b" names) 59 + 60 + let test_to_list () = 61 + let e : ML.entry = { url = "https://example.com/a.git"; ref_ = "main" } in 62 + let t = ML.add ML.empty ~name:"a" e in 63 + let l = ML.to_list t in 64 + Alcotest.(check int) "one entry" 1 (List.length l); 65 + let name, _ = List.hd l in 66 + Alcotest.(check string) "name" "a" name 67 + 68 + (* {1 of_string / to_string roundtrip tests} *) 69 + 70 + let test_of_string_basic () = 71 + let s = "eio https://github.com/mirage/eio.git#main\n" in 72 + let t = ML.of_string s in 73 + match ML.find t ~name:"eio" with 74 + | Some e -> 75 + Alcotest.(check string) "url" "https://github.com/mirage/eio.git" e.url; 76 + Alcotest.(check string) "ref" "main" e.ref_ 77 + | None -> Alcotest.fail "expected eio" 78 + 79 + let test_of_string_multiple () = 80 + let s = 81 + "eio https://github.com/mirage/eio.git#main\n\ 82 + dune https://github.com/ocaml/dune.git#v3.17\n" 83 + in 84 + let t = ML.of_string s in 85 + Alcotest.(check int) "two entries" 2 (List.length (ML.to_list t)) 86 + 87 + let test_of_string_comments () = 88 + let s = 89 + "# comment\neio https://github.com/mirage/eio.git#main\n# another\n" 90 + in 91 + let t = ML.of_string s in 92 + Alcotest.(check int) "one entry" 1 (List.length (ML.to_list t)) 93 + 94 + let test_of_string_empty_lines () = 95 + let s = "\n\neio https://github.com/mirage/eio.git#main\n\n" in 96 + let t = ML.of_string s in 97 + Alcotest.(check int) "one entry" 1 (List.length (ML.to_list t)) 98 + 99 + let test_of_string_no_ref () = 100 + let s = "eio https://github.com/mirage/eio.git\n" in 101 + let t = ML.of_string s in 102 + match ML.find t ~name:"eio" with 103 + | Some e -> Alcotest.(check string) "default ref" "main" e.ref_ 104 + | None -> Alcotest.fail "expected eio" 105 + 106 + let test_roundtrip () = 107 + let e1 : ML.entry = 108 + { url = "https://github.com/mirage/eio.git"; ref_ = "main" } 109 + in 110 + let e2 : ML.entry = 111 + { url = "https://github.com/ocaml/dune.git"; ref_ = "v3.17" } 112 + in 113 + let t = ML.add (ML.add ML.empty ~name:"eio" e1) ~name:"dune" e2 in 114 + let s = ML.to_string t in 115 + let t' = ML.of_string s in 116 + Alcotest.(check int) 117 + "same count" 118 + (List.length (ML.to_list t)) 119 + (List.length (ML.to_list t')); 120 + (match ML.find t' ~name:"eio" with 121 + | Some e -> 122 + Alcotest.(check string) "eio url" e1.url e.url; 123 + Alcotest.(check string) "eio ref" e1.ref_ e.ref_ 124 + | None -> Alcotest.fail "expected eio"); 125 + match ML.find t' ~name:"dune" with 126 + | Some e -> 127 + Alcotest.(check string) "dune url" e2.url e.url; 128 + Alcotest.(check string) "dune ref" e2.ref_ e.ref_ 129 + | None -> Alcotest.fail "expected dune" 130 + 131 + let test_to_string_empty () = 132 + Alcotest.(check string) "empty" "" (ML.to_string ML.empty) 133 + 134 + (* {1 pp tests} *) 135 + 136 + let test_pp_empty () = 137 + let s = Fmt.str "%a" ML.pp ML.empty in 138 + Alcotest.(check string) "empty" "(empty)" s 139 + 140 + let test_pp_entry () = 141 + let e : ML.entry = { url = "https://example.com/a.git"; ref_ = "main" } in 142 + let s = Fmt.str "%a" ML.pp_entry e in 143 + Alcotest.(check bool) "non-empty" true (String.length s > 0) 144 + 145 + let test_pp_nonempty () = 146 + let e : ML.entry = { url = "https://example.com/a.git"; ref_ = "main" } in 147 + let t = ML.add ML.empty ~name:"a" e in 148 + let s = Fmt.str "%a" ML.pp t in 149 + Alcotest.(check bool) "non-empty" true (String.length s > 0) 150 + 151 + (* {1 lock_filename test} *) 152 + 153 + let test_lock_filename () = 154 + Alcotest.(check string) "filename" "mono.lock" ML.lock_filename 155 + 156 + let suite = 157 + ( "mono_lock", 158 + [ 159 + (* empty *) 160 + Alcotest.test_case "empty" `Quick test_empty; 161 + (* add / find / remove *) 162 + Alcotest.test_case "add and find" `Quick test_add_and_find; 163 + Alcotest.test_case "find missing" `Quick test_find_missing; 164 + Alcotest.test_case "add replaces" `Quick test_add_replaces; 165 + Alcotest.test_case "remove" `Quick test_remove; 166 + Alcotest.test_case "remove nonexistent" `Quick test_remove_nonexistent; 167 + (* names / to_list *) 168 + Alcotest.test_case "names" `Quick test_names; 169 + Alcotest.test_case "to_list" `Quick test_to_list; 170 + (* of_string / to_string *) 171 + Alcotest.test_case "of_string basic" `Quick test_of_string_basic; 172 + Alcotest.test_case "of_string multiple" `Quick test_of_string_multiple; 173 + Alcotest.test_case "of_string comments" `Quick test_of_string_comments; 174 + Alcotest.test_case "of_string empty lines" `Quick 175 + test_of_string_empty_lines; 176 + Alcotest.test_case "of_string no ref" `Quick test_of_string_no_ref; 177 + Alcotest.test_case "roundtrip" `Quick test_roundtrip; 178 + Alcotest.test_case "to_string empty" `Quick test_to_string_empty; 179 + (* pp *) 180 + Alcotest.test_case "pp empty" `Quick test_pp_empty; 181 + Alcotest.test_case "pp entry" `Quick test_pp_entry; 182 + Alcotest.test_case "pp nonempty" `Quick test_pp_nonempty; 183 + (* lock_filename *) 184 + Alcotest.test_case "lock filename" `Quick test_lock_filename; 185 + ] )
+35 -1
test/test_monopam.ml
··· 1 - let suite = ("monopam", []) 1 + (* Tests for monopam top-level module *) 2 + 3 + (* {1 Module re-exports test} *) 4 + 5 + let test_modules_accessible () = 6 + (* Verify key module re-exports are accessible by touching a value *) 7 + ignore (Monopam.Config.default_branch : string); 8 + ignore (Monopam.Mono_lock.lock_filename : string); 9 + ignore (Monopam.Verse_registry.default_url : string); 10 + () 11 + 12 + (* {1 is_commit_sha through Monopam} *) 13 + 14 + let test_is_commit_sha_true () = 15 + Alcotest.(check bool) "valid sha" true (Monopam.is_commit_sha "abcdef1") 16 + 17 + let test_is_commit_sha_false () = 18 + Alcotest.(check bool) "invalid sha" false (Monopam.is_commit_sha "hello") 19 + 20 + (* {1 pp_error_with_hint} *) 21 + 22 + let test_pp_error_with_hint () = 23 + let s = 24 + Fmt.str "%a" Monopam.pp_error_with_hint (Monopam.Ctx.Config_error "test") 25 + in 26 + Alcotest.(check bool) "non-empty" true (String.length s > 0) 27 + 28 + let suite = 29 + ( "monopam", 30 + [ 31 + Alcotest.test_case "modules accessible" `Quick test_modules_accessible; 32 + Alcotest.test_case "is_commit_sha true" `Quick test_is_commit_sha_true; 33 + Alcotest.test_case "is_commit_sha false" `Quick test_is_commit_sha_false; 34 + Alcotest.test_case "pp_error_with_hint" `Quick test_pp_error_with_hint; 35 + ] )
+129 -2
test/test_opam_repo.ml
··· 1 - (* Tests for opam_repo *) 1 + (* Tests for opam_repo module *) 2 + 3 + module OR = Monopam.Opam_repo 4 + 5 + (* {1 pp_error tests} *) 6 + 7 + let test_pp_multiple_versions () = 8 + let s = 9 + Fmt.str "%a" OR.pp_error (OR.Multiple_versions ("foo", [ "1.0"; "2.0" ])) 10 + in 11 + Alcotest.(check bool) "has pkg name" true (String.length s > 0) 12 + 13 + let test_pp_no_dev_repo () = 14 + let s = Fmt.str "%a" OR.pp_error (OR.No_dev_repo "foo") in 15 + Alcotest.(check bool) "has pkg name" true (String.length s > 0) 16 + 17 + let test_pp_invalid_dev_repo () = 18 + let s = Fmt.str "%a" OR.pp_error (OR.Invalid_dev_repo ("foo", "bad-url")) in 19 + Alcotest.(check bool) "has details" true (String.length s > 0) 20 + 21 + let test_pp_not_git_remote () = 22 + let s = 23 + Fmt.str "%a" OR.pp_error (OR.Not_git_remote ("foo", "https://example.com")) 24 + in 25 + Alcotest.(check bool) "has details" true (String.length s > 0) 26 + 27 + let test_pp_parse_error () = 28 + let s = 29 + Fmt.str "%a" OR.pp_error (OR.Parse_error ("foo.opam", "bad syntax")) 30 + in 31 + Alcotest.(check bool) "has details" true (String.length s > 0) 32 + 33 + let test_pp_io_error () = 34 + let s = Fmt.str "%a" OR.pp_error (OR.Io_error "disk full") in 35 + Alcotest.(check bool) "has message" true (String.length s > 0) 36 + 37 + (* {1 is_git_url tests} *) 38 + 39 + let test_git_plus_url () = 40 + Alcotest.(check bool) 41 + "git+" true 42 + (OR.is_git_url "git+https://github.com/user/repo.git") 43 + 44 + let test_git_protocol () = 45 + Alcotest.(check bool) 46 + "git://" true 47 + (OR.is_git_url "git://github.com/user/repo.git") 48 + 49 + let test_git_at () = 50 + Alcotest.(check bool) 51 + "git@" true 52 + (OR.is_git_url "git@github.com:user/repo.git") 53 + 54 + let test_dot_git_suffix () = 55 + Alcotest.(check bool) 56 + ".git suffix" true 57 + (OR.is_git_url "https://github.com/user/repo.git") 58 + 59 + let test_not_git_url () = 60 + Alcotest.(check bool) 61 + "not git" false 62 + (OR.is_git_url "https://example.com/archive.tar.gz") 63 + 64 + let test_not_git_plain_https () = 65 + Alcotest.(check bool) 66 + "plain https" false 67 + (OR.is_git_url "https://example.com/repo") 68 + 69 + (* {1 normalize_git_url tests} *) 70 + 71 + let test_normalize_strips_git_plus () = 72 + let uri = OR.normalize_git_url "git+https://github.com/user/repo.git" in 73 + Alcotest.(check string) 74 + "stripped" "https://github.com/user/repo.git" (Uri.to_string uri) 75 + 76 + let test_normalize_no_prefix () = 77 + let uri = OR.normalize_git_url "https://github.com/user/repo.git" in 78 + Alcotest.(check string) 79 + "unchanged" "https://github.com/user/repo.git" (Uri.to_string uri) 80 + 81 + (* {1 replace_dev_repo_url tests} *) 82 + 83 + let test_replace_dev_repo () = 84 + let content = 85 + {|opam-version: "2.0" 86 + dev-repo: "git+https://old.com/repo.git" 87 + url { 88 + src: "git+https://old.com/repo.git#main" 89 + }|} 90 + in 91 + let result = 92 + OR.replace_dev_repo_url content ~new_url:"git@github.com:new/repo.git" 93 + in 94 + Alcotest.(check bool) 95 + "has new url" true 96 + (let needle = "git@github.com:new/repo.git" in 97 + let nl = String.length needle in 98 + let hl = String.length result in 99 + nl <= hl 100 + && 101 + let rec loop i = 102 + if i > hl - nl then false 103 + else if String.sub result i nl = needle then true 104 + else loop (i + 1) 105 + in 106 + loop 0) 2 107 3 - let suite = ("opam_repo", []) 108 + let suite = 109 + ( "opam_repo", 110 + [ 111 + (* pp_error *) 112 + Alcotest.test_case "pp multiple versions" `Quick test_pp_multiple_versions; 113 + Alcotest.test_case "pp no dev-repo" `Quick test_pp_no_dev_repo; 114 + Alcotest.test_case "pp invalid dev-repo" `Quick test_pp_invalid_dev_repo; 115 + Alcotest.test_case "pp not git" `Quick test_pp_not_git_remote; 116 + Alcotest.test_case "pp parse error" `Quick test_pp_parse_error; 117 + Alcotest.test_case "pp io error" `Quick test_pp_io_error; 118 + (* is_git_url *) 119 + Alcotest.test_case "git+ url" `Quick test_git_plus_url; 120 + Alcotest.test_case "git:// url" `Quick test_git_protocol; 121 + Alcotest.test_case "git@ url" `Quick test_git_at; 122 + Alcotest.test_case ".git suffix" `Quick test_dot_git_suffix; 123 + Alcotest.test_case "not git url" `Quick test_not_git_url; 124 + Alcotest.test_case "plain https" `Quick test_not_git_plain_https; 125 + (* normalize *) 126 + Alcotest.test_case "normalize git+" `Quick test_normalize_strips_git_plus; 127 + Alcotest.test_case "normalize no prefix" `Quick test_normalize_no_prefix; 128 + (* replace *) 129 + Alcotest.test_case "replace dev-repo" `Quick test_replace_dev_repo; 130 + ] )
+45 -2
test/test_opam_sync.ml
··· 1 - (* Tests for opam_sync *) 1 + (* Tests for opam_sync module *) 2 + 3 + module OS = Monopam.Opam_sync 4 + 5 + (* {1 pp tests} *) 6 + 7 + let test_pp_empty () = 8 + let r : OS.t = { synced = []; unchanged = []; missing = []; orphaned = [] } in 9 + let s = Fmt.str "%a" OS.pp r in 10 + (* Empty result should still produce some output or be minimal *) 11 + ignore s 12 + 13 + let test_pp_synced () = 14 + let r : OS.t = 15 + { synced = [ "eio"; "dune" ]; unchanged = []; missing = []; orphaned = [] } 16 + in 17 + let s = Fmt.str "%a" OS.pp r in 18 + Alcotest.(check bool) "non-empty" true (String.length s > 0) 19 + 20 + let test_pp_orphaned () = 21 + let r : OS.t = 22 + { synced = []; unchanged = []; missing = []; orphaned = [ "old-pkg" ] } 23 + in 24 + let s = Fmt.str "%a" OS.pp r in 25 + Alcotest.(check bool) "non-empty" true (String.length s > 0) 26 + 27 + let test_pp_mixed () = 28 + let r : OS.t = 29 + { 30 + synced = [ "eio" ]; 31 + unchanged = [ "dune" ]; 32 + missing = [ "missing-pkg" ]; 33 + orphaned = [ "old-pkg" ]; 34 + } 35 + in 36 + let s = Fmt.str "%a" OS.pp r in 37 + Alcotest.(check bool) "non-empty" true (String.length s > 0) 2 38 3 - let suite = ("opam_sync", []) 39 + let suite = 40 + ( "opam_sync", 41 + [ 42 + Alcotest.test_case "pp empty" `Quick test_pp_empty; 43 + Alcotest.test_case "pp synced" `Quick test_pp_synced; 44 + Alcotest.test_case "pp orphaned" `Quick test_pp_orphaned; 45 + Alcotest.test_case "pp mixed" `Quick test_pp_mixed; 46 + ] )
+246 -2
test/test_opam_transform.ml
··· 1 - (* Tests for opam_transform *) 1 + (* Tests for opam_transform module *) 2 + 3 + module OT = Monopam.Opam_transform 2 4 3 - let suite = ("opam_transform", []) 5 + (* {1 strip dune comment} *) 6 + 7 + let test_strips_dune_comment () = 8 + let content = 9 + {|# This file is generated by dune, edit dune-project instead 10 + opam-version: "2.0" 11 + name: "my-lib"|} 12 + in 13 + let result = 14 + OT.transform ~content ~dev_repo:"git+https://github.com/user/repo.git" 15 + ~url_src:"git+https://github.com/user/repo.git#main" 16 + in 17 + Alcotest.(check bool) 18 + "no dune comment" true 19 + (not 20 + (let needle = "generated by dune" in 21 + let nl = String.length needle in 22 + let hl = String.length result in 23 + nl <= hl 24 + && 25 + let rec loop i = 26 + if i > hl - nl then false 27 + else if String.sub result i nl = needle then true 28 + else loop (i + 1) 29 + in 30 + loop 0)) 31 + 32 + let test_preserves_content_without_comment () = 33 + let content = {|opam-version: "2.0" 34 + name: "my-lib"|} in 35 + let result = 36 + OT.transform ~content ~dev_repo:"git+https://github.com/user/repo.git" 37 + ~url_src:"git+https://github.com/user/repo.git#main" 38 + in 39 + Alcotest.(check bool) 40 + "has opam-version" true 41 + (let needle = {|opam-version: "2.0"|} in 42 + let nl = String.length needle in 43 + let hl = String.length result in 44 + nl <= hl 45 + && 46 + let rec loop i = 47 + if i > hl - nl then false 48 + else if String.sub result i nl = needle then true 49 + else loop (i + 1) 50 + in 51 + loop 0) 52 + 53 + (* {1 dev-repo insertion} *) 54 + 55 + let test_adds_dev_repo () = 56 + let content = {|opam-version: "2.0" 57 + name: "my-lib"|} in 58 + let dev_repo = "git+https://github.com/user/repo.git" in 59 + let result = 60 + OT.transform ~content ~dev_repo 61 + ~url_src:"git+https://github.com/user/repo.git#main" 62 + in 63 + Alcotest.(check bool) 64 + "has dev-repo" true 65 + (let needle = dev_repo in 66 + let nl = String.length needle in 67 + let hl = String.length result in 68 + nl <= hl 69 + && 70 + let rec loop i = 71 + if i > hl - nl then false 72 + else if String.sub result i nl = needle then true 73 + else loop (i + 1) 74 + in 75 + loop 0) 76 + 77 + let test_replaces_existing_dev_repo () = 78 + let content = 79 + {|opam-version: "2.0" 80 + name: "my-lib" 81 + dev-repo: "git+https://old.com/repo.git"|} 82 + in 83 + let result = 84 + OT.transform ~content ~dev_repo:"git+https://new.com/repo.git" 85 + ~url_src:"git+https://new.com/repo.git#main" 86 + in 87 + (* Should not contain the old URL *) 88 + Alcotest.(check bool) 89 + "no old url" true 90 + (not 91 + (let needle = "old.com" in 92 + let nl = String.length needle in 93 + let hl = String.length result in 94 + nl <= hl 95 + && 96 + let rec loop i = 97 + if i > hl - nl then false 98 + else if String.sub result i nl = needle then true 99 + else loop (i + 1) 100 + in 101 + loop 0)); 102 + Alcotest.(check bool) 103 + "has new url" true 104 + (let needle = "new.com" in 105 + let nl = String.length needle in 106 + let hl = String.length result in 107 + nl <= hl 108 + && 109 + let rec loop i = 110 + if i > hl - nl then false 111 + else if String.sub result i nl = needle then true 112 + else loop (i + 1) 113 + in 114 + loop 0) 115 + 116 + (* {1 url section} *) 117 + 118 + let test_adds_url_section () = 119 + let content = {|opam-version: "2.0" 120 + name: "my-lib"|} in 121 + let url_src = "git+https://github.com/user/repo.git#main" in 122 + let result = 123 + OT.transform ~content ~dev_repo:"git+https://github.com/user/repo.git" 124 + ~url_src 125 + in 126 + Alcotest.(check bool) 127 + "has url section" true 128 + (let needle = "url {" in 129 + let nl = String.length needle in 130 + let hl = String.length result in 131 + nl <= hl 132 + && 133 + let rec loop i = 134 + if i > hl - nl then false 135 + else if String.sub result i nl = needle then true 136 + else loop (i + 1) 137 + in 138 + loop 0) 139 + 140 + let test_replaces_existing_url_section () = 141 + let content = 142 + {|opam-version: "2.0" 143 + name: "my-lib" 144 + url { 145 + src: "git+https://old.com/repo.git#v1" 146 + }|} 147 + in 148 + let result = 149 + OT.transform ~content ~dev_repo:"git+https://new.com/repo.git" 150 + ~url_src:"git+https://new.com/repo.git#main" 151 + in 152 + Alcotest.(check bool) 153 + "has new url" true 154 + (let needle = "new.com" in 155 + let nl = String.length needle in 156 + let hl = String.length result in 157 + nl <= hl 158 + && 159 + let rec loop i = 160 + if i > hl - nl then false 161 + else if String.sub result i nl = needle then true 162 + else loop (i + 1) 163 + in 164 + loop 0); 165 + (* Should not have old URL in url section *) 166 + Alcotest.(check bool) 167 + "no old url" true 168 + (not 169 + (let needle = "old.com" in 170 + let nl = String.length needle in 171 + let hl = String.length result in 172 + nl <= hl 173 + && 174 + let rec loop i = 175 + if i > hl - nl then false 176 + else if String.sub result i nl = needle then true 177 + else loop (i + 1) 178 + in 179 + loop 0)) 180 + 181 + (* {1 trailing newline} *) 182 + 183 + let test_ends_with_newline () = 184 + let content = {|opam-version: "2.0" 185 + name: "my-lib"|} in 186 + let result = 187 + OT.transform ~content ~dev_repo:"git+https://github.com/user/repo.git" 188 + ~url_src:"git+https://github.com/user/repo.git#main" 189 + in 190 + Alcotest.(check bool) 191 + "ends with newline" true 192 + (String.length result > 0 && result.[String.length result - 1] = '\n') 193 + 194 + (* {1 full transform integration} *) 195 + 196 + let test_full_transform () = 197 + let content = 198 + {|# This file is generated by dune, edit dune-project instead 199 + opam-version: "2.0" 200 + name: "my-lib" 201 + synopsis: "My library" 202 + depends: [ 203 + "ocaml" {>= "5.0"} 204 + ]|} 205 + in 206 + let result = 207 + OT.transform ~content ~dev_repo:"git+https://github.com/user/my-lib.git" 208 + ~url_src:"git+https://github.com/user/my-lib.git#main" 209 + in 210 + let contains haystack needle = 211 + let nl = String.length needle in 212 + let hl = String.length haystack in 213 + if nl > hl then false 214 + else 215 + let rec loop i = 216 + if i > hl - nl then false 217 + else if String.sub haystack i nl = needle then true 218 + else loop (i + 1) 219 + in 220 + loop 0 221 + in 222 + Alcotest.(check bool) 223 + "no dune comment" true 224 + (not (contains result "generated by dune")); 225 + Alcotest.(check bool) 226 + "has opam-version" true 227 + (contains result {|opam-version: "2.0"|}); 228 + Alcotest.(check bool) "has name" true (contains result {|name: "my-lib"|}); 229 + Alcotest.(check bool) "has dev-repo" true (contains result "my-lib.git"); 230 + Alcotest.(check bool) "has url section" true (contains result "url {"); 231 + Alcotest.(check bool) "has src" true (contains result "src:") 232 + 233 + let suite = 234 + ( "opam_transform", 235 + [ 236 + Alcotest.test_case "strip dune comment" `Quick test_strips_dune_comment; 237 + Alcotest.test_case "preserve no comment" `Quick 238 + test_preserves_content_without_comment; 239 + Alcotest.test_case "add dev-repo" `Quick test_adds_dev_repo; 240 + Alcotest.test_case "replace dev-repo" `Quick 241 + test_replaces_existing_dev_repo; 242 + Alcotest.test_case "add url section" `Quick test_adds_url_section; 243 + Alcotest.test_case "replace url section" `Quick 244 + test_replaces_existing_url_section; 245 + Alcotest.test_case "trailing newline" `Quick test_ends_with_newline; 246 + Alcotest.test_case "full transform" `Quick test_full_transform; 247 + ] )
+51 -1
test/test_progress.ml
··· 1 - let suite = ("progress", []) 1 + (* Tests for progress module *) 2 + 3 + (* Monopam.Progress = Sync_progress, whose S type uses `create` *) 4 + module P = Monopam.Progress 5 + 6 + (* {1 Disabled module tests} *) 7 + 8 + let test_disabled_create () = 9 + let p = P.Disabled.create ~total:10 "Test" in 10 + ignore p 11 + 12 + let test_disabled_tick () = 13 + let p = P.Disabled.create ~total:10 "Test" in 14 + P.Disabled.tick p "item1"; 15 + P.Disabled.tick p "item2" 16 + 17 + let test_disabled_clear () = 18 + let p = P.Disabled.create ~total:10 "Test" in 19 + P.Disabled.clear p 20 + 21 + let test_disabled_finish () = 22 + let p = P.Disabled.create ~total:10 "Test" in 23 + P.Disabled.finish p 24 + 25 + (* {1 Active module basic tests} *) 26 + 27 + let test_active_lifecycle () = 28 + let p = P.Active.create ~total:3 "Testing" in 29 + P.Active.tick p "step1"; 30 + P.Active.tick p "step2"; 31 + P.Active.clear p; 32 + P.Active.finish p 33 + 34 + (* {1 Top-level functions} *) 35 + 36 + let test_v_and_finish () = 37 + let p = P.v ~total:2 "TopLevel" in 38 + P.tick p "a"; 39 + P.tick p "b"; 40 + P.finish p 41 + 42 + let suite = 43 + ( "progress", 44 + [ 45 + Alcotest.test_case "disabled create" `Quick test_disabled_create; 46 + Alcotest.test_case "disabled tick" `Quick test_disabled_tick; 47 + Alcotest.test_case "disabled clear" `Quick test_disabled_clear; 48 + Alcotest.test_case "disabled finish" `Quick test_disabled_finish; 49 + Alcotest.test_case "active lifecycle" `Quick test_active_lifecycle; 50 + Alcotest.test_case "v and finish" `Quick test_v_and_finish; 51 + ] )
+15 -1
test/test_pull.ml
··· 1 - let suite = ("pull", []) 1 + (* Tests for pull module - I/O heavy, verify module alias works *) 2 + 3 + let test_module_alias () = 4 + ignore 5 + (Monopam.Pull.run 6 + : proc:_ -> 7 + fs:_ -> 8 + config:_ -> 9 + ?packages:_ -> 10 + ?opam_repo_url:_ -> 11 + unit -> 12 + _) 13 + 14 + let suite = 15 + ("pull", [ Alcotest.test_case "module alias" `Quick test_module_alias ])
+17 -1
test/test_push.ml
··· 1 - let suite = ("push", []) 1 + (* Tests for push module - I/O heavy, verify module alias works *) 2 + 3 + let test_module_alias () = 4 + ignore 5 + (Monopam.Push.run 6 + : proc:_ -> 7 + fs:_ -> 8 + config:_ -> 9 + ?packages:_ -> 10 + ?upstream:_ -> 11 + ?clean:_ -> 12 + ?force:_ -> 13 + unit -> 14 + _) 15 + 16 + let suite = 17 + ("push", [ Alcotest.test_case "module alias" `Quick test_module_alias ])
+7 -1
test/test_remove.ml
··· 1 - let suite = ("remove", []) 1 + (* Tests for remove module - I/O heavy, verify module alias works *) 2 + 3 + let test_module_alias () = 4 + ignore (Monopam.Remove.run : fs:_ -> config:_ -> package:_ -> unit -> _) 5 + 6 + let suite = 7 + ("remove", [ Alcotest.test_case "module alias" `Quick test_module_alias ])
+66 -2
test/test_site.ml
··· 1 - (* Tests for site *) 1 + (* Tests for site module - mostly I/O, test types only *) 2 + 3 + module Site = Monopam.Site 4 + 5 + (* Verify type construction works *) 2 6 3 - let suite = ("site", []) 7 + let test_pkg_info_construction () = 8 + let _info : Site.pkg_info = 9 + { 10 + name = "eio"; 11 + synopsis = Some "Effects-based I/O"; 12 + repo_name = "eio"; 13 + dev_repo = "https://github.com/ocaml/eio.git"; 14 + owners = [ "alice"; "bob" ]; 15 + depends = [ "domain-local-await" ]; 16 + } 17 + in 18 + () 19 + 20 + let test_repo_info_construction () = 21 + let _info : Site.repo_info = 22 + { 23 + ri_name = "eio"; 24 + ri_dev_repo = "https://github.com/ocaml/eio.git"; 25 + ri_packages = []; 26 + ri_owners = [ "alice" ]; 27 + ri_fork_status = []; 28 + ri_dep_count = 3; 29 + } 30 + in 31 + () 32 + 33 + let test_member_info_construction () = 34 + let _info : Site.member_info = 35 + { 36 + handle = "alice.bsky.social"; 37 + display_name = "Alice"; 38 + monorepo_url = "https://tangled.org/alice/mono"; 39 + opam_url = "https://tangled.org/alice/opam-repo"; 40 + package_count = 10; 41 + unique_packages = [ "my-lib" ]; 42 + } 43 + in 44 + () 45 + 46 + let test_data_construction () = 47 + let _data : Site.data = 48 + { 49 + local_handle = "test.example.org"; 50 + registry_name = "test-registry"; 51 + registry_description = Some "A test registry"; 52 + members = []; 53 + common_repos = []; 54 + unique_repos = []; 55 + all_packages = []; 56 + } 57 + in 58 + () 59 + 60 + let suite = 61 + ( "site", 62 + [ 63 + Alcotest.test_case "pkg_info type" `Quick test_pkg_info_construction; 64 + Alcotest.test_case "repo_info type" `Quick test_repo_info_construction; 65 + Alcotest.test_case "member_info type" `Quick test_member_info_construction; 66 + Alcotest.test_case "data type" `Quick test_data_construction; 67 + ] )
+48 -2
test/test_sync_progress.ml
··· 1 - (* Tests for sync_progress *) 1 + (* Tests for sync_progress module *) 2 + 3 + (* Monopam.Progress = Sync_progress *) 4 + module SP = Monopam.Progress 5 + 6 + (* {1 Disabled module tests} *) 7 + 8 + let test_disabled_lifecycle () = 9 + let p = SP.Disabled.create ~total:5 "Syncing" in 10 + SP.Disabled.tick p "repo1"; 11 + SP.Disabled.tick p "repo2"; 12 + SP.Disabled.clear p; 13 + SP.Disabled.finish p 2 14 3 - let suite = ("sync_progress", []) 15 + (* {1 Active module tests} *) 16 + 17 + let test_active_zero_total () = 18 + let p = SP.Active.create ~total:0 "Empty" in 19 + SP.Active.finish p 20 + 21 + let test_active_single_tick () = 22 + let p = SP.Active.create ~total:1 "Single" in 23 + SP.Active.tick p "only-item"; 24 + SP.Active.finish p 25 + 26 + let test_active_multiple_ticks () = 27 + let p = SP.Active.create ~total:3 "Multi" in 28 + SP.Active.tick p "a"; 29 + SP.Active.tick p "b"; 30 + SP.Active.tick p "c"; 31 + SP.Active.finish p 32 + 33 + let test_active_clear_then_finish () = 34 + let p = SP.Active.create ~total:2 "ClearTest" in 35 + SP.Active.tick p "item"; 36 + SP.Active.clear p; 37 + SP.Active.finish p 38 + 39 + let suite = 40 + ( "sync_progress", 41 + [ 42 + Alcotest.test_case "disabled lifecycle" `Quick test_disabled_lifecycle; 43 + Alcotest.test_case "active zero total" `Quick test_active_zero_total; 44 + Alcotest.test_case "active single tick" `Quick test_active_single_tick; 45 + Alcotest.test_case "active multiple ticks" `Quick 46 + test_active_multiple_ticks; 47 + Alcotest.test_case "active clear+finish" `Quick 48 + test_active_clear_then_finish; 49 + ] )
+131 -2
test/test_verse.ml
··· 1 - (* Tests for verse *) 1 + (* Tests for verse module *) 2 + 3 + module Verse = Monopam.Verse 4 + 5 + (* {1 pp_error tests} *) 6 + 7 + let test_pp_config_error () = 8 + let s = Fmt.str "%a" Verse.pp_error (Verse.Config_error "bad config") in 9 + Alcotest.(check bool) "non-empty" true (String.length s > 0) 10 + 11 + let test_pp_registry_error () = 12 + let s = Fmt.str "%a" Verse.pp_error (Verse.Registry_error "clone failed") in 13 + Alcotest.(check bool) "non-empty" true (String.length s > 0) 14 + 15 + let test_pp_member_not_found () = 16 + let s = 17 + Fmt.str "%a" Verse.pp_error (Verse.Member_not_found "alice.bsky.social") 18 + in 19 + Alcotest.(check bool) "non-empty" true (String.length s > 0) 20 + 21 + let test_pp_workspace_exists () = 22 + let s = 23 + Fmt.str "%a" Verse.pp_error 24 + (Verse.Workspace_exists (Fpath.v "/home/user/ws")) 25 + in 26 + Alcotest.(check bool) "non-empty" true (String.length s > 0) 27 + 28 + let test_pp_not_a_workspace () = 29 + let s = 30 + Fmt.str "%a" Verse.pp_error 31 + (Verse.Not_a_workspace (Fpath.v "/home/user/bad")) 32 + in 33 + Alcotest.(check bool) "non-empty" true (String.length s > 0) 34 + 35 + let test_pp_package_not_found () = 36 + let s = 37 + Fmt.str "%a" Verse.pp_error 38 + (Verse.Package_not_found ("eio", "alice.bsky.social")) 39 + in 40 + Alcotest.(check bool) "non-empty" true (String.length s > 0) 41 + 42 + let test_pp_package_already_exists () = 43 + let s = 44 + Fmt.str "%a" Verse.pp_error 45 + (Verse.Package_already_exists [ "eio"; "eio-main" ]) 46 + in 47 + Alcotest.(check bool) "non-empty" true (String.length s > 0) 48 + 49 + (* {1 error_hint tests} *) 50 + 51 + let test_hint_config () = 52 + match Verse.error_hint (Verse.Config_error "bad") with 53 + | Some h -> Alcotest.(check bool) "has hint" true (String.length h > 0) 54 + | None -> Alcotest.fail "expected hint" 55 + 56 + let test_hint_registry () = 57 + match Verse.error_hint (Verse.Registry_error "failed") with 58 + | Some h -> Alcotest.(check bool) "has hint" true (String.length h > 0) 59 + | None -> Alcotest.fail "expected hint" 60 + 61 + let test_hint_member_not_found () = 62 + match Verse.error_hint (Verse.Member_not_found "alice") with 63 + | Some h -> Alcotest.(check bool) "has hint" true (String.length h > 0) 64 + | None -> Alcotest.fail "expected hint" 65 + 66 + let test_hint_workspace_exists () = 67 + match Verse.error_hint (Verse.Workspace_exists (Fpath.v "/tmp")) with 68 + | Some h -> Alcotest.(check bool) "has hint" true (String.length h > 0) 69 + | None -> Alcotest.fail "expected hint" 70 + 71 + let test_hint_not_a_workspace () = 72 + match Verse.error_hint (Verse.Not_a_workspace (Fpath.v "/tmp")) with 73 + | Some h -> Alcotest.(check bool) "has hint" true (String.length h > 0) 74 + | None -> Alcotest.fail "expected hint" 75 + 76 + let test_hint_package_not_found () = 77 + match 78 + Verse.error_hint (Verse.Package_not_found ("eio", "alice.bsky.social")) 79 + with 80 + | Some h -> Alcotest.(check bool) "has hint" true (String.length h > 0) 81 + | None -> Alcotest.fail "expected hint" 82 + 83 + let test_hint_package_exists () = 84 + match Verse.error_hint (Verse.Package_already_exists [ "eio" ]) with 85 + | Some h -> Alcotest.(check bool) "has hint" true (String.length h > 0) 86 + | None -> Alcotest.fail "expected hint" 2 87 3 - let suite = ("verse", []) 88 + (* {1 pp_error_with_hint tests} *) 89 + 90 + let test_pp_with_hint () = 91 + let s = Fmt.str "%a" Verse.pp_error_with_hint (Verse.Config_error "test") in 92 + Alcotest.(check bool) "non-empty" true (String.length s > 0) 93 + 94 + (* {1 pp_fork_result tests} *) 95 + 96 + let test_pp_fork_result () = 97 + let r : Verse.fork_result = 98 + { 99 + packages_forked = [ "eio"; "eio-main" ]; 100 + source_handle = "alice.bsky.social"; 101 + fork_url = "git@github.com:bob/eio.git"; 102 + upstream_url = "https://github.com/ocaml/eio.git"; 103 + subtree_name = "eio"; 104 + } 105 + in 106 + let s = Fmt.str "%a" Verse.pp_fork_result r in 107 + Alcotest.(check bool) "non-empty" true (String.length s > 0) 108 + 109 + let suite = 110 + ( "verse", 111 + [ 112 + (* pp_error *) 113 + Alcotest.test_case "pp config" `Quick test_pp_config_error; 114 + Alcotest.test_case "pp registry" `Quick test_pp_registry_error; 115 + Alcotest.test_case "pp member not found" `Quick test_pp_member_not_found; 116 + Alcotest.test_case "pp workspace exists" `Quick test_pp_workspace_exists; 117 + Alcotest.test_case "pp not workspace" `Quick test_pp_not_a_workspace; 118 + Alcotest.test_case "pp pkg not found" `Quick test_pp_package_not_found; 119 + Alcotest.test_case "pp pkg exists" `Quick test_pp_package_already_exists; 120 + (* error_hint *) 121 + Alcotest.test_case "hint config" `Quick test_hint_config; 122 + Alcotest.test_case "hint registry" `Quick test_hint_registry; 123 + Alcotest.test_case "hint member" `Quick test_hint_member_not_found; 124 + Alcotest.test_case "hint ws exists" `Quick test_hint_workspace_exists; 125 + Alcotest.test_case "hint not ws" `Quick test_hint_not_a_workspace; 126 + Alcotest.test_case "hint pkg not found" `Quick test_hint_package_not_found; 127 + Alcotest.test_case "hint pkg exists" `Quick test_hint_package_exists; 128 + (* pp_error_with_hint *) 129 + Alcotest.test_case "pp with hint" `Quick test_pp_with_hint; 130 + (* pp_fork_result *) 131 + Alcotest.test_case "pp fork result" `Quick test_pp_fork_result; 132 + ] )
+113 -2
test/test_verse_registry.ml
··· 1 - (* Tests for verse_registry *) 1 + (* Tests for verse_registry module *) 2 + 3 + module VR = Monopam.Verse_registry 4 + 5 + (* {1 default_url test} *) 6 + 7 + let test_default_url () = 8 + let url = VR.default_url in 9 + Alcotest.(check bool) "non-empty" true (String.length url > 0); 10 + Alcotest.(check bool) 11 + "is https" true 12 + (String.starts_with ~prefix:"https://" url) 13 + 14 + (* {1 member lookup tests} *) 15 + 16 + let mk_member handle : VR.member = 17 + { 18 + handle; 19 + name = Some ("Name of " ^ handle); 20 + monorepo = "https://tangled.org/" ^ handle ^ "/mono"; 21 + monorepo_branch = None; 22 + opamrepo = "https://tangled.org/" ^ handle ^ "/opam-repo"; 23 + opamrepo_branch = None; 24 + } 25 + 26 + let test_registry : VR.t = 27 + { 28 + name = "test-registry"; 29 + description = Some "A test registry"; 30 + members = 31 + [ 32 + mk_member "alice.bsky.social"; 33 + mk_member "bob.example.org"; 34 + mk_member "carol.bsky.social"; 35 + ]; 36 + } 37 + 38 + let test_member_found () = 39 + match VR.member test_registry ~handle:"alice.bsky.social" with 40 + | Some m -> 41 + Alcotest.(check string) "handle" "alice.bsky.social" m.handle; 42 + Alcotest.(check (option string)) 43 + "name" (Some "Name of alice.bsky.social") m.name 44 + | None -> Alcotest.fail "expected to find alice" 45 + 46 + let test_member_not_found () = 47 + Alcotest.(check (option pass)) 48 + "not found" None 49 + (VR.member test_registry ~handle:"nonexistent") 50 + 51 + let test_members_multiple () = 52 + let found = 53 + VR.members test_registry 54 + ~handles:[ "alice.bsky.social"; "carol.bsky.social" ] 55 + in 56 + Alcotest.(check int) "found 2" 2 (List.length found) 57 + 58 + let test_members_partial () = 59 + let found = 60 + VR.members test_registry ~handles:[ "alice.bsky.social"; "nonexistent" ] 61 + in 62 + Alcotest.(check int) "found 1" 1 (List.length found) 63 + 64 + let test_members_empty () = 65 + let found = VR.members test_registry ~handles:[] in 66 + Alcotest.(check int) "found 0" 0 (List.length found) 67 + 68 + (* {1 member with branches test} *) 69 + 70 + let test_member_with_branches () = 71 + let m : VR.member = 72 + { 73 + handle = "test"; 74 + name = None; 75 + monorepo = "https://example.com/mono"; 76 + monorepo_branch = Some "develop"; 77 + opamrepo = "https://example.com/opam"; 78 + opamrepo_branch = Some "main"; 79 + } 80 + in 81 + Alcotest.(check (option string)) 82 + "mono branch" (Some "develop") m.monorepo_branch; 83 + Alcotest.(check (option string)) "opam branch" (Some "main") m.opamrepo_branch 84 + 85 + (* {1 pp tests} *) 86 + 87 + let test_pp_member () = 88 + let m = mk_member "alice.bsky.social" in 89 + let s = Fmt.str "%a" VR.pp_member m in 90 + Alcotest.(check bool) "non-empty" true (String.length s > 0) 2 91 3 - let suite = ("verse_registry", []) 92 + let test_pp_registry () = 93 + let s = Fmt.str "%a" VR.pp test_registry in 94 + Alcotest.(check bool) "non-empty" true (String.length s > 0) 95 + 96 + let test_pp_empty_registry () = 97 + let t : VR.t = { name = "empty"; description = None; members = [] } in 98 + let s = Fmt.str "%a" VR.pp t in 99 + Alcotest.(check bool) "non-empty" true (String.length s > 0) 100 + 101 + let suite = 102 + ( "verse_registry", 103 + [ 104 + Alcotest.test_case "default url" `Quick test_default_url; 105 + Alcotest.test_case "member found" `Quick test_member_found; 106 + Alcotest.test_case "member not found" `Quick test_member_not_found; 107 + Alcotest.test_case "members multiple" `Quick test_members_multiple; 108 + Alcotest.test_case "members partial" `Quick test_members_partial; 109 + Alcotest.test_case "members empty" `Quick test_members_empty; 110 + Alcotest.test_case "member branches" `Quick test_member_with_branches; 111 + Alcotest.test_case "pp member" `Quick test_pp_member; 112 + Alcotest.test_case "pp registry" `Quick test_pp_registry; 113 + Alcotest.test_case "pp empty registry" `Quick test_pp_empty_registry; 114 + ] )