summaryrefslogtreecommitdiff
path: root/test/test_lang_coverage.ml
diff options
context:
space:
mode:
Diffstat (limited to 'test/test_lang_coverage.ml')
-rw-r--r--test/test_lang_coverage.ml118
1 files changed, 118 insertions, 0 deletions
diff --git a/test/test_lang_coverage.ml b/test/test_lang_coverage.ml
index 0271a59..d41f7e7 100644
--- a/test/test_lang_coverage.ml
+++ b/test/test_lang_coverage.ml
@@ -11,6 +11,14 @@ let la () =
| Ok t -> t
| Error e -> Alcotest.failf "lang/la.ini: %s" e
+(* en.ini resolved THROUGH la.ini, the way a real run resolves it: en
+ declares `fallback = la`, so a key it omits must reach the Latin table
+ rather than degrade to the raw key. *)
+let en () =
+ match L.of_string (read "../lang/en.ini") with
+ | Ok t -> L.with_fallback t (la ())
+ | Error e -> Alcotest.failf "lang/en.ini: %s" e
+
(* Every slug the engine can emit -- temporal ("^ef-") AND sanctoral (a fixed
saint's day) alike -- must have a Latin name. THIS IS THE TEST THAT WOULD
HAVE CAUGHT THE ORIGINAL DEFECT -- a booklet printed "ef-septuagesima-
@@ -145,6 +153,112 @@ let test_no_book_name_is_an_internal_id () =
Alcotest.(check (list string)) "no book name is its own internal id" []
leaked
+(* Two DIFFERENT books must not share a name. Found by audit: `1 Cor` and
+ `2 Cor` both rendered as "Epistola ad Corinthios" under --sigla-book full,
+ and so did Thessalonians, Timothy and Peter -- 108 citations in 2027 alone
+ that a reader cannot resolve to a book. The same defect had already been
+ fixed for the two Books of Kings and simply not generalised.
+
+ Sharing a name is CORRECT, though, when the two ids are the same physical
+ book under different numbering -- `osee`/`hosea`, `jonas`/`jonah`,
+ `apocalypse`/`revelation`. A tradition maps one onto the other, so the
+ rule is exact: a shared name is a defect UNLESS some tradition relates the
+ two ids. *)
+let test_no_two_books_share_a_name () =
+ let la = la () in
+ let traditions =
+ let text =
+ let ic = open_in_bin "../lang/traditions.ini" in
+ let s = really_input_string ic (in_channel_length ic) in
+ close_in ic; s
+ in
+ match Colitur_kernel.Overlay_ini.parse_sections text with
+ | Ok ss ->
+ List.concat_map (fun (sc : Colitur_kernel.Overlay_ini.section) ->
+ sc.Colitur_kernel.Overlay_ini.fields) ss
+ | Error e -> Alcotest.failf "traditions.ini: %s" e
+ in
+ let related a b =
+ List.exists (fun (x, y) -> (x = a && y = b) || (x = b && y = a)) traditions
+ in
+ let offenders = ref [] in
+ List.iter
+ (fun form ->
+ let seen = Hashtbl.create 64 in
+ List.iter
+ (fun id ->
+ let n = Colitur_citation.Book.to_string id in
+ let key = n ^ "." ^ form in
+ let v = L.bible la key in
+ if v <> key then
+ match Hashtbl.find_opt seen v with
+ | Some other when not (related n other) ->
+ offenders := Printf.sprintf "%s: %s and %s" v other n :: !offenders
+ | _ -> Hashtbl.replace seen v n)
+ Colitur_citation.Book.all)
+ [ "full"; "abbr" ];
+ Alcotest.(check (list string)) "no two different books share a name" []
+ (List.sort compare !offenders)
+
+(* SPEC SECTION 8.5: each shipped style must parse its own rendered output.
+ It did not. Measured at the time: 32 of 52 Latin abbreviations and 49 of 52
+ Latin full titles failed to re-parse, so a user who copied a citation out
+ of `colitur readings` into an overlay handed the parser a string it could
+ not read; [Sigla.format] passed it through untouched and, say, an English
+ full-name booklet printed a Latin abbreviation with no warning. Closed by
+ registering every shipped name as a spelling and by teaching [split_book]
+ multi-word titles.
+
+ A name may resolve to a DIFFERENT id than the one it was rendered from,
+ but only when the two are the same physical book under different
+ numbering: "Sir" is registered to [ecclesiasticus] and the modern id
+ [sirach] maps onto it. The parser has no tradition context, so it returns
+ the Vulgate id, and that is right rather than tolerated. *)
+let test_shipped_styles_round_trip () =
+ let traditions =
+ let text =
+ let ic = open_in_bin "../lang/traditions.ini" in
+ let s = really_input_string ic (in_channel_length ic) in
+ close_in ic; s
+ in
+ match Colitur_kernel.Overlay_ini.parse_sections text with
+ | Ok ss ->
+ List.concat_map
+ (fun (sc : Colitur_kernel.Overlay_ini.section) ->
+ sc.Colitur_kernel.Overlay_ini.fields)
+ ss
+ | Error e -> Alcotest.failf "traditions.ini: %s" e
+ in
+ let related a b =
+ a = b || List.exists (fun (x, y) -> (x = a && y = b) || (x = b && y = a)) traditions
+ in
+ let check_file label t =
+ List.concat_map
+ (fun form ->
+ List.filter_map
+ (fun id ->
+ let n = Colitur_citation.Book.to_string id in
+ let name = L.bible t (n ^ "." ^ form) in
+ if name = n ^ "." ^ form then None
+ else
+ match Colitur_citation.Parse.parse (name ^ " 5:12-14") with
+ | Ok r
+ when related
+ (Colitur_citation.Book.to_string r.Colitur_citation.Parse.book)
+ n ->
+ None
+ | Ok r ->
+ Some
+ (Printf.sprintf "%s %s/%s -> %s" label name form
+ (Colitur_citation.Book.to_string r.Colitur_citation.Parse.book))
+ | Error e -> Some (Printf.sprintf "%s %s/%s: %s" label name form e))
+ Colitur_citation.Book.all)
+ [ "full"; "abbr" ]
+ in
+ let bad = check_file "la" (la ()) @ check_file "en" (en ()) in
+ Alcotest.(check (list string)) "every shipped book name re-parses" []
+ (List.sort compare bad)
+
(* lang/en.ini is DELIBERATELY partial (see its own header note): it declares
[meta] fallback = la, so a slug it does not carry itself should still
resolve through the chain to la.ini's name rather than degrade to the bare
@@ -189,4 +303,8 @@ let suite =
Alcotest.test_case "every book named, both forms" `Quick test_every_book_named;
Alcotest.test_case "no book name is an internal id" `Quick
test_no_book_name_is_an_internal_id;
+ Alcotest.test_case "no two books share a name" `Quick
+ test_no_two_books_share_a_name;
+ Alcotest.test_case "shipped styles round-trip" `Quick
+ test_shipped_styles_round_trip;
Alcotest.test_case "en.ini falls back to Latin" `Quick test_en_falls_back_to_latin ] )