aboutsummaryrefslogtreecommitdiff
path: root/test/test_citation_coverage.ml
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-08-20 19:05:13 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-08-20 19:05:13 +0200
commit7dbd16cdf5880c0006dffacd09214ee607050b64 (patch)
tree01e1ee37a2be97a0b06fad70de975d8007beb702 /test/test_citation_coverage.ml
parent1aeca948c2a2a82bffaaec65077140aa05f7b3f4 (diff)
parent1da70dc7ac03fe33fb92b172a0e26932764170d6 (diff)
downloadcolitur-7dbd16cdf5880c0006dffacd09214ee607050b64.tar.gz
colitur-7dbd16cdf5880c0006dffacd09214ee607050b64.zip
Merge branch 'citations-and-sigla'
Citations are parsed into structure and re-rendered, so book names, abbreviations, punctuation style and numbering tradition become files a user edits rather than strings frozen in the data. New lib/citation (Book, Parse, Render, Sigla); [bible] and [sigla] sections in language files; lang/traditions.ini for numbering; three config keys and CLI flags. --raw emits every citation byte-for-byte as stored, bypassing the whole pipeline, so output stays diffable against lectio and the raw view does not depend on the parser being correct.
Diffstat (limited to 'test/test_citation_coverage.ml')
-rw-r--r--test/test_citation_coverage.ml89
1 files changed, 89 insertions, 0 deletions
diff --git a/test/test_citation_coverage.ml b/test/test_citation_coverage.ml
new file mode 100644
index 0000000..a131d50
--- /dev/null
+++ b/test/test_citation_coverage.ml
@@ -0,0 +1,89 @@
+(* SPDX-License-Identifier: AGPL-3.0-or-later *)
+
+(* Every citation the engine can emit must parse. This is the check that
+ would have caught the seven duplicate book spellings -- nothing validated
+ them before.
+
+ Same shape as test_lang_coverage.ml, which this file deliberately mirrors:
+ a miss there is a slug with no name, a miss here is a citation the parser
+ cannot read.
+
+ The walk below also drives the round-trip check (Sigla facade, Task 5):
+ ONE pass over 1970-2070 collects into TWO tables, [bad_parse] and
+ [bad_round_trip], rather than walking the whole range twice for two
+ independent Alcotest cases -- measured, a second walk would cost ~1.5s of
+ a 5.65s fast suite for zero extra coverage. *)
+
+module P = Colitur_citation.Parse
+
+let bad_parse : (string, string) Hashtbl.t = Hashtbl.create 64
+let bad_round_trip : (string, string) Hashtbl.t = Hashtbl.create 16
+let walked = ref false
+
+(* Parse -> render -> parse must reach the same structure.
+
+ NOTE the [names] function: [Book.default_spelling], NOT [Book.to_string].
+ [to_string] returns the internal id ("corinthians_1"), which is not a
+ registered token, so rendering with it produces something Parse cannot
+ read back -- 0 of 738 round-trip. Measured during Task 4; do not
+ "simplify" this back to to_string. A renderer that emits an unparseable
+ string, or a style whose own output it cannot read, fails here. Compares
+ STRUCTURE, not text: the rendered form legitimately differs from the
+ input (a normalised book spelling, a dropped trailing period), and
+ asserting text equality would just pin those differences. *)
+let sg =
+ Colitur_citation.Sigla.make ~style:Colitur_citation.Render.default_style
+ ~tradition:Colitur_citation.Book.vulgate
+ ~names:(fun id _form -> Colitur_citation.Book.default_spelling id)
+
+let walk () =
+ if not !walked then begin
+ walked := true;
+ let layer =
+ match Test_support.load_ef_layer () with
+ | Ok l -> l
+ | Error e -> Alcotest.failf "%s" e
+ in
+ let ctx = Test_support.ef_context () in
+ for y = 1970 to 2070 do
+ Array.iter
+ (fun (d : (Rite_ef.Vocab_ef.season, Rite_ef.Vocab_ef.rank) Colitur_kernel.Liturgical_day.t) ->
+ List.iter
+ (fun (c : Colitur_kernel.Citation.t) ->
+ let r = c.Colitur_kernel.Citation.reference in
+ match P.parse r with
+ | Error e -> if not (Hashtbl.mem bad_parse r) then Hashtbl.add bad_parse r e
+ | Ok first ->
+ let rendered = Colitur_citation.Sigla.format sg r in
+ (match P.parse rendered with
+ | Error e ->
+ Hashtbl.replace bad_round_trip r ("re-parse failed: " ^ e)
+ | Ok again ->
+ if again <> first then
+ Hashtbl.replace bad_round_trip r ("structure changed: " ^ rendered)))
+ d.Colitur_kernel.Liturgical_day.citations)
+ (Colitur_kernel.Calendar.year ctx layer y)
+ done
+ end
+
+let test_every_citation_parses () =
+ walk ();
+ if Hashtbl.length bad_parse > 0 then begin
+ Hashtbl.iter (fun r e -> Printf.eprintf "UNPARSED %S: %s\n" r e) bad_parse;
+ Alcotest.failf "%d citation(s) did not parse" (Hashtbl.length bad_parse)
+ end
+
+let test_round_trip () =
+ walk ();
+ if Hashtbl.length bad_round_trip > 0 then begin
+ Hashtbl.iter (fun r e -> Printf.eprintf "ROUND-TRIP %S: %s\n" r e) bad_round_trip;
+ Alcotest.failf "%d citation(s) failed round-trip" (Hashtbl.length bad_round_trip)
+ end
+
+(* This project has ONE test executable. Every test_<module>.ml exposes a
+ [suite] value and test_colitur.ml aggregates them -- do NOT call
+ Alcotest.run here. Marked `Slow like test_lang_coverage's own year walk. *)
+let suite =
+ ( "citation-coverage",
+ [ Alcotest.test_case "every citation parses" `Slow test_every_citation_parses;
+ Alcotest.test_case "sigla round-trip" `Slow test_round_trip ] )