test remove superfluous tests, add commit parsing and diff coverage

Remove test_routes.ml (route correctness already proven by router tests) and test_static.ml (only asserted build-output existence). Add test_commit_parsing.ml covering parse_commit_message and parse_conventional edge cases: None/empty/single-line/body messages, scoped types, uppercase, unknown types, and all known type keywords. Expand test_diff.ml with empty-vs-empty, identical content, entirely new/deleted files, trailing newline handling, multiple hunks, and the large-file LCS fallback path. Add config port boundary tests (0 and 65536) and a router test for the ?type= commit filter (valid and unknown types both return 200). Expose parse_commit_message and parse_conventional from Ogit.Views.Repo for testability.

Commit
74e1caf9f2629687bd25cc30f1397f4188270860
Author
GPT-5.6 Sol <kiro@amazon.com>
Author date
Committer
Marius Peter <dev@marius-peter.com>
Committer date
Changed files
lib/views.ml
index 6d57e1e5..d7be46f9 100644..100644
@@ -6,7 +6,14 @@
6 6 let root = Root.render
7 7
8 8 module Repo = struct
9 Added: type commit_message = Repo.commit_message = {
10 Added: summary : string;
11 Added: body : string;
12 Added: }
13 Added:
9 14 let context = Repo.context
15 Added: let parse_commit_message = Repo.parse_commit_message
16 Added: let parse_conventional = Repo.parse_conventional
10 17 let summary = Repo.summary
11 18 let commits = Repo.commits
12 19 let files = Repo.files
test/test_commit_parsing.ml
index 00000000..a7fe313d 000000..100644
@@ -0,0 +1,92 @@
1 Added: (* -*- mode: tuareg; -*- *)
2 Added:
3 Added: (** Tests for commit message parsing and conventional commit type extraction.
4 Added: These functions drive both the pill display and the commit type filter. *)
5 Added:
6 Added: open Ogit.Views.Repo
7 Added:
8 Added: let test_parse_message_none () =
9 Added: let msg = parse_commit_message None in
10 Added: Alcotest.(check string) "summary" "" msg.summary;
11 Added: Alcotest.(check string) "body" "" msg.body
12 Added:
13 Added: let test_parse_message_single_line () =
14 Added: let msg = parse_commit_message (Some "single line") in
15 Added: Alcotest.(check string) "summary" "single line" msg.summary;
16 Added: Alcotest.(check string) "body" "" msg.body
17 Added:
18 Added: let test_parse_message_with_body () =
19 Added: let msg = parse_commit_message (Some "summary\n\nbody paragraph") in
20 Added: Alcotest.(check string) "summary" "summary" msg.summary;
21 Added: Alcotest.(check string) "body" "body paragraph" msg.body
22 Added:
23 Added: let test_parse_message_body_trimmed () =
24 Added: let msg = parse_commit_message (Some "title\n\n indented\n\n") in
25 Added: Alcotest.(check string) "summary" "title" msg.summary;
26 Added: Alcotest.(check string) "body trimmed" "indented" msg.body
27 Added:
28 Added: let test_conventional_feat () =
29 Added: let typ, rest = parse_conventional "feat: add login" in
30 Added: Alcotest.(check (option string)) "type" (Some "feat") typ;
31 Added: Alcotest.(check string) "rest" "add login" rest
32 Added:
33 Added: let test_conventional_with_scope () =
34 Added: let typ, rest = parse_conventional "fix(auth): handle timeout" in
35 Added: Alcotest.(check (option string)) "type" (Some "fix") typ;
36 Added: Alcotest.(check string) "rest" "handle timeout" rest
37 Added:
38 Added: let test_conventional_uppercase () =
39 Added: let typ, rest = parse_conventional "FEAT: uppercase" in
40 Added: Alcotest.(check (option string)) "type" (Some "feat") typ;
41 Added: Alcotest.(check string) "rest" "uppercase" rest
42 Added:
43 Added: let test_conventional_unknown_type () =
44 Added: let typ, rest = parse_conventional "unknown: something" in
45 Added: Alcotest.(check (option string)) "type" None typ;
46 Added: Alcotest.(check string) "rest" "unknown: something" rest
47 Added:
48 Added: let test_conventional_no_colon () =
49 Added: let typ, rest = parse_conventional "just a message" in
50 Added: Alcotest.(check (option string)) "type" None typ;
51 Added: Alcotest.(check string) "rest" "just a message" rest
52 Added:
53 Added: let test_conventional_empty () =
54 Added: let typ, rest = parse_conventional "" in
55 Added: Alcotest.(check (option string)) "type" None typ;
56 Added: Alcotest.(check string) "rest" "" rest
57 Added:
58 Added: let test_conventional_all_types () =
59 Added: List.iter
60 Added: (fun expected_type ->
61 Added: let input = expected_type ^ ": msg" in
62 Added: let typ, _ = parse_conventional input in
63 Added: Alcotest.(check (option string)) expected_type (Some expected_type) typ)
64 Added: [
65 Added: "feat";
66 Added: "fix";
67 Added: "docs";
68 Added: "style";
69 Added: "refactor";
70 Added: "perf";
71 Added: "test";
72 Added: "build";
73 Added: "ci";
74 Added: "chore";
75 Added: "revert";
76 Added: ]
77 Added:
78 Added: let suite =
79 Added: ( "commit parsing",
80 Added: [
81 Added: Alcotest.test_case "None message" `Quick test_parse_message_none;
82 Added: Alcotest.test_case "single line" `Quick test_parse_message_single_line;
83 Added: Alcotest.test_case "with body" `Quick test_parse_message_with_body;
84 Added: Alcotest.test_case "body trimmed" `Quick test_parse_message_body_trimmed;
85 Added: Alcotest.test_case "feat type" `Quick test_conventional_feat;
86 Added: Alcotest.test_case "scoped type" `Quick test_conventional_with_scope;
87 Added: Alcotest.test_case "uppercase" `Quick test_conventional_uppercase;
88 Added: Alcotest.test_case "unknown type" `Quick test_conventional_unknown_type;
89 Added: Alcotest.test_case "no colon" `Quick test_conventional_no_colon;
90 Added: Alcotest.test_case "empty string" `Quick test_conventional_empty;
91 Added: Alcotest.test_case "all known types" `Quick test_conventional_all_types;
92 Added: ] )
test/test_config.ml
index 497c15e6..db562c60 100644..100644
@@ -62,6 +62,24 @@
62 62 | Error error -> fail_config_error error
63 63 | Ok _ -> Alcotest.fail "invalid commit limit was accepted")
64 64
65 Added: let test_invalid_port_high () =
66 Added: with_temp_file "ogit-port-high" ".toml" (fun file ->
67 Added: let invalid = Ogit.Config.{ default with port = 65536 } in
68 Added: Ogit.Config.write_file ~file (Ogit.Config.to_table invalid);
69 Added: match Ogit.Config.read_file ~file () with
70 Added: | Error (Ogit.Config.Invalid_value _) -> ()
71 Added: | Error error -> fail_config_error error
72 Added: | Ok _ -> Alcotest.fail "port 65536 was accepted")
73 Added:
74 Added: let test_invalid_port_zero () =
75 Added: with_temp_file "ogit-port-zero" ".toml" (fun file ->
76 Added: let invalid = Ogit.Config.{ default with port = 0 } in
77 Added: Ogit.Config.write_file ~file (Ogit.Config.to_table invalid);
78 Added: match Ogit.Config.read_file ~file () with
79 Added: | Error (Ogit.Config.Invalid_value _) -> ()
80 Added: | Error error -> fail_config_error error
81 Added: | Ok _ -> Alcotest.fail "port 0 was accepted")
82 Added:
65 83 let test_directory_rejected () =
66 84 with_temp_directory "ogit-config-directory" (fun directory ->
67 85 match Ogit.Config.read_file ~file:directory () with
@@ -116,6 +134,8 @@
116 134 Alcotest.test_case "backward compat" `Quick test_backward_compat;
117 135 Alcotest.test_case "malformed" `Quick test_malformed;
118 136 Alcotest.test_case "invalid value" `Quick test_invalid_value;
137 Added: Alcotest.test_case "port too high" `Quick test_invalid_port_high;
138 Added: Alcotest.test_case "port zero" `Quick test_invalid_port_zero;
119 139 Alcotest.test_case "directory rejected" `Quick test_directory_rejected;
120 140 Alcotest.test_case "invalid implicit" `Quick test_invalid_implicit;
121 141 Alcotest.test_case "explicit missing" `Quick test_explicit_missing;
test/test_diff.ml
index 42b91317..f1c79dfc 100644..100644
@@ -1,5 +1,7 @@
1 1 (* -*- mode: tuareg; -*- *)
2 2
3 Added: (** Tests for the line-level diff algorithm and hunk generation. *)
4 Added:
3 5 let test_line_diff () =
4 6 let open Ogit.Resolvers.Diff in
5 7 match line_diff "first\nold\nlast\n" "first\nnew\nlast\n" with
@@ -10,6 +12,59 @@
10 12 Alcotest.(check string) "ctx2 text" "last" ctx2.text
11 13 | lines -> Alcotest.failf "expected 4 lines, got %d" (List.length lines)
12 14
15 Added: let test_empty_vs_empty () =
16 Added: let open Ogit.Resolvers.Diff in
17 Added: let lines = line_diff "" "" in
18 Added: Alcotest.(check int) "no lines" 0 (List.length lines);
19 Added: let hunks = hunks lines in
20 Added: Alcotest.(check int) "no hunks" 0 (List.length hunks)
21 Added:
22 Added: let test_identical_content () =
23 Added: let open Ogit.Resolvers.Diff in
24 Added: let content = "line1\nline2\nline3\n" in
25 Added: let lines = line_diff content content in
26 Added: List.iter
27 Added: (fun line -> Alcotest.(check bool) "all context" true (line.kind = Context))
28 Added: lines;
29 Added: let hunks = hunks lines in
30 Added: Alcotest.(check int) "no hunks for identical" 0 (List.length hunks)
31 Added:
32 Added: let test_entirely_new_file () =
33 Added: let open Ogit.Resolvers.Diff in
34 Added: let lines = line_diff "" "new1\nnew2\n" in
35 Added: Alcotest.(check int) "2 additions" 2 (List.length lines);
36 Added: List.iter
37 Added: (fun line ->
38 Added: Alcotest.(check bool) "all additions" true (line.kind = Addition))
39 Added: lines
40 Added:
41 Added: let test_entirely_deleted_file () =
42 Added: let open Ogit.Resolvers.Diff in
43 Added: let lines = line_diff "old1\nold2\n" "" in
44 Added: Alcotest.(check int) "2 deletions" 2 (List.length lines);
45 Added: List.iter
46 Added: (fun line ->
47 Added: Alcotest.(check bool) "all deletions" true (line.kind = Deletion))
48 Added: lines
49 Added:
50 Added: let test_trailing_newline_handling () =
51 Added: let open Ogit.Resolvers.Diff in
52 Added: let with_newline = line_diff "a\n" "a\n" in
53 Added: let without_newline = line_diff "a" "a" in
54 Added: Alcotest.(check int) "with newline: 1 line" 1 (List.length with_newline);
55 Added: Alcotest.(check int) "without newline: 1 line" 1 (List.length without_newline);
56 Added: Alcotest.(check string) "text matches" "a" (List.hd with_newline).text;
57 Added: Alcotest.(check string) "text matches" "a" (List.hd without_newline).text
58 Added:
59 Added: let test_no_trailing_newline_diff () =
60 Added: let open Ogit.Resolvers.Diff in
61 Added: let lines = line_diff "a\nb" "a\nc" in
62 Added: Alcotest.(check int) "3 lines" 3 (List.length lines);
63 Added: let kinds = List.map (fun l -> l.kind) lines in
64 Added: Alcotest.(check bool)
65 Added: "context+del+add" true
66 Added: (kinds = [ Context; Deletion; Addition ])
67 Added:
13 68 let test_hunks () =
14 69 let open Ogit.Resolvers.Diff in
15 70 let old_content = List.init 12 (fun i -> string_of_int (i + 1)) in
@@ -29,9 +84,50 @@
29 84 Alcotest.(check int) "new_count" 7 hunk.new_count
30 85 | hunks -> Alcotest.failf "expected 1 hunk, got %d" (List.length hunks)
31 86
87 Added: let test_multiple_hunks () =
88 Added: let open Ogit.Resolvers.Diff in
89 Added: let old_content = List.init 20 (fun i -> string_of_int (i + 1)) in
90 Added: let new_content =
91 Added: List.mapi (fun i l -> if i = 2 || i = 17 then "changed" else l) old_content
92 Added: in
93 Added: let result =
94 Added: hunks
95 Added: (line_diff
96 Added: (String.concat "\n" old_content)
97 Added: (String.concat "\n" new_content))
98 Added: in
99 Added: Alcotest.(check int) "2 hunks" 2 (List.length result)
100 Added:
101 Added: let test_large_file_fallback () =
102 Added: let open Ogit.Resolvers.Diff in
103 Added: (* 2001 * 2001 = 4_004_001 > 4_000_000 threshold *)
104 Added: let old_content = String.concat "\n" (List.init 2001 string_of_int) in
105 Added: let new_content =
106 Added: String.concat "\n" (List.init 2001 (fun i -> string_of_int (i + 1000)))
107 Added: in
108 Added: let lines = line_diff old_content new_content in
109 Added: let has_deletions = List.exists (fun l -> l.kind = Deletion) lines in
110 Added: let has_additions = List.exists (fun l -> l.kind = Addition) lines in
111 Added: Alcotest.(check bool) "has deletions" true has_deletions;
112 Added: Alcotest.(check bool) "has additions" true has_additions;
113 Added: (* No context lines in fallback mode *)
114 Added: let has_context = List.exists (fun l -> l.kind = Context) lines in
115 Added: Alcotest.(check bool) "no context in fallback" false has_context
116 Added:
32 117 let suite =
33 118 ( "diff",
34 119 [
35 Removed: Alcotest.test_case "line diff" `Quick test_line_diff;
36 Removed: Alcotest.test_case "hunks" `Quick test_hunks;
120 Added: Alcotest.test_case "basic line diff" `Quick test_line_diff;
121 Added: Alcotest.test_case "empty vs empty" `Quick test_empty_vs_empty;
122 Added: Alcotest.test_case "identical content" `Quick test_identical_content;
123 Added: Alcotest.test_case "entirely new file" `Quick test_entirely_new_file;
124 Added: Alcotest.test_case "entirely deleted file" `Quick
125 Added: test_entirely_deleted_file;
126 Added: Alcotest.test_case "trailing newline" `Quick
127 Added: test_trailing_newline_handling;
128 Added: Alcotest.test_case "no trailing newline diff" `Quick
129 Added: test_no_trailing_newline_diff;
130 Added: Alcotest.test_case "single hunk" `Quick test_hunks;
131 Added: Alcotest.test_case "multiple hunks" `Quick test_multiple_hunks;
132 Added: Alcotest.test_case "large file fallback" `Quick test_large_file_fallback;
37 133 ] )
test/test_ogit.ml
index b577820e..673d0e61 100644..100644
@@ -4,12 +4,11 @@
4 4 Alcotest.run "ogit"
5 5 [
6 6 Test_validation.suite;
7 Added: Test_commit_parsing.suite;
7 8 Test_config.suite;
8 9 Test_discovery.suite;
9 10 Test_tree_paths.suite;
10 11 Test_router.suite;
11 12 Test_diff.suite;
12 13 Test_views.suite;
13 Removed: Test_static.suite;
14 Removed: Test_routes.suite;
15 14 ]
test/test_router.ml
index a7390ccf..6dc62516 100644..100644
@@ -58,6 +58,33 @@
58 58 let status = Dream.request ~target "" |> request |> Dream.status in
59 59 Alcotest.(check int) "404" 404 (Dream.status_to_int status))
60 60
61 Added: let test_commits_type_filter () =
62 Added: with_temp_directory "ogit-router" (fun root ->
63 Added: let name = "project" in
64 Added: let path = Filename.concat root name in
65 Added: Unix.mkdir path 0o755;
66 Added: ignore (git [ "-C"; path; "init"; "-q"; "-b"; "main" ]);
67 Added: ignore (git [ "-C"; path; "config"; "user.name"; "Test" ]);
68 Added: ignore (git [ "-C"; path; "config"; "user.email"; "t@t.invalid" ]);
69 Added: Out_channel.with_open_text (Filename.concat path "f.txt") (fun ch ->
70 Added: output_string ch "x\n");
71 Added: ignore (git [ "-C"; path; "add"; "." ]);
72 Added: ignore (git [ "-C"; path; "commit"; "-q"; "-m"; "feat: initial" ]);
73 Added: let config = Ogit.Config.{ default with git_project_root = root } in
74 Added: let request = Dream.test (Dream.router (Ogit.Handlers.routes config)) in
75 Added: (* Valid filter returns 200 *)
76 Added: let status =
77 Added: Dream.request ~target:"/project/commits/?type=feat" ""
78 Added: |> request |> Dream.status
79 Added: in
80 Added: Alcotest.(check int) "filter feat 200" 200 (Dream.status_to_int status);
81 Added: (* Unknown filter returns 200 with empty list, not an error *)
82 Added: let status =
83 Added: Dream.request ~target:"/project/commits/?type=xyz" ""
84 Added: |> request |> Dream.status
85 Added: in
86 Added: Alcotest.(check int) "filter unknown 200" 200 (Dream.status_to_int status))
87 Added:
61 88 let suite =
62 89 ( "router",
63 90 [
@@ -65,4 +92,5 @@
65 92 Alcotest.test_case "missing repo" `Slow test_missing_repo;
66 93 Alcotest.test_case "invalid hash" `Slow test_invalid_hash;
67 94 Alcotest.test_case "missing object" `Slow test_missing_object;
95 Added: Alcotest.test_case "commits type filter" `Slow test_commits_type_filter;
68 96 ] )
test/test_routes.ml
index 6f074ef3..00000000 100644..000000
@@ -1,12 +0,0 @@
1 Removed: (* -*- mode: tuareg; -*- *)
2 Removed:
3 Removed: let test_patterns () =
4 Removed: let path route = Format.asprintf "%a" Dream_html.pp_path route in
5 Removed: Alcotest.(check string) "root" "/" (path Ogit.Routes.root_path);
6 Removed: Alcotest.(check string) "repo" "/%s/summary/" (path Ogit.Routes.repo_path);
7 Removed: Alcotest.(check string)
8 Removed: "commit" "/%s/commit/%s"
9 Removed: (path Ogit.Routes.commit_path);
10 Removed: Alcotest.(check string) "static" "/static/%*s" (path Ogit.Routes.static_path)
11 Removed:
12 Removed: let suite = ("routes", [ Alcotest.test_case "patterns" `Quick test_patterns ])
test/test_static.ml
index fb010fcc..00000000 100644..000000
@@ -1,26 +0,0 @@
1 Removed: (* -*- mode: tuareg; -*- *)
2 Removed:
3 Removed: let test_styles () =
4 Removed: match Ogit.Static_assets.read "styles.css" with
5 Removed: | Some content ->
6 Removed: Alcotest.(check bool) "non-empty" true (String.length content > 0)
7 Removed: | None -> Alcotest.fail "styles.css should be embedded"
8 Removed:
9 Removed: let test_icon () =
10 Removed: match Ogit.Static_assets.read "git_icon.svg" with
11 Removed: | Some content ->
12 Removed: Alcotest.(check bool) "non-empty" true (String.length content > 0)
13 Removed: | None -> Alcotest.fail "git_icon.svg should be embedded"
14 Removed:
15 Removed: let test_missing () =
16 Removed: Alcotest.(check (option string))
17 Removed: "nonexistent" None
18 Removed: (Ogit.Static_assets.read "nonexistent")
19 Removed:
20 Removed: let suite =
21 Removed: ( "static assets",
22 Removed: [
23 Removed: Alcotest.test_case "styles.css" `Quick test_styles;
24 Removed: Alcotest.test_case "git_icon.svg" `Quick test_icon;
25 Removed: Alcotest.test_case "missing asset" `Quick test_missing;
26 Removed: ] )