aboutsummaryrefslogtreecommitdiff
path: root/test/test_lang_coverage.ml
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-08-19 23:48:35 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-08-19 23:48:35 +0200
commitecadd969e1d96918820a1fdac0cf0d520d96ba06 (patch)
treea06ed1160bfd28ae4578d9445242e48ac9ac3ea4 /test/test_lang_coverage.ml
parent6762ce46af3cb12bc6ae37cda762c5d95add7903 (diff)
parent329d07b49e397cb65ab52e7ca019b47313027136 (diff)
downloadcolitur-ecadd969e1d96918820a1fdac0cf0d520d96ba06.tar.gz
colitur-ecadd969e1d96918820a1fdac0cf0d520d96ba06.zip
feat: naming, localisation and the rebuilt printed output
colitur computed the calendar correctly and could not say what it had computed. A printed ordo read ef-septuagesima-sunday-2 where a reader expects Dominica in Sexagesima, and the wall calendar showed slugs in every cell. lib/naming a language table and a config file, both pure and total, parsing the INI reader Overlay_ini already had lang/ la.ini and en.ini -- 725 names, every one transcribed from the 1962 Missal and citing the line it came from templates the ordo rebuilt as an A5 booklet: one week per page, a table of contents, framed days, a colour swatch; the wall calendar now fills its sheet instead of a quarter of it tools check_citations.py verifies all 400 citations resolve, with 33 self-tests of its own Names resolve through lang -> declared fallback -> the slug, so a partial translation is usable from its first line and the fully degraded case is the old output rather than a blank page. colitur day and colitur readings are BYTE-IDENTICAL to before, verified against main rather than asserted; --lang, --raw and the lang/config subcommands are still to come, and man/colitur-templates.5 still documents the pre-naming view, so writing a custom template needs the source until that lands.
Diffstat (limited to 'test/test_lang_coverage.ml')
-rw-r--r--test/test_lang_coverage.ml123
1 files changed, 123 insertions, 0 deletions
diff --git a/test/test_lang_coverage.ml b/test/test_lang_coverage.ml
new file mode 100644
index 0000000..34758b2
--- /dev/null
+++ b/test/test_lang_coverage.ml
@@ -0,0 +1,123 @@
+module L = Colitur_naming.Lang
+
+let read path =
+ let ic = open_in_bin path in
+ let s = really_input_string ic (in_channel_length ic) in
+ close_in ic;
+ s
+
+let la () =
+ match L.of_string (read "../lang/la.ini") with
+ | Ok t -> t
+ | Error e -> Alcotest.failf "lang/la.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-
+ sunday-2" because nothing asserted coverage. It must fail loudly the
+ moment a new slug appears without a name.
+
+ Task 3 restricted this to "^ef-" slugs only (la.ini's [celebration] table
+ carried the temporal half alone at the time); Task 4 added the sanctoral
+ half and REMOVED that filter -- every slug is now in scope, with no
+ exceptions.
+
+ Task 5 CLOSED A SECOND BLIND SPOT: this test used to walk only the
+ OBSERVED day (one Celebration.t per date). A Liturgical_day.t also
+ carries a whole second stream of slugs -- commemorations (kept when the
+ observed day does not fully displace a losing candidate, RG 108-111) and
+ transfers (an impeded I/II-class feast moved to a later date, RG 96-98).
+ The ordo booklet printed raw slugs ("Commemoratio canute-martyr",
+ "Commemoratio maur-abbot", "Commemoratio peter") precisely because
+ nothing here ever looked at [commemorations], [transferred_in] or
+ [transferred_out] -- the test asserted coverage of what it happened to
+ WALK, not of what the engine can EMIT. Now walks all four fields, so any
+ slug reachable through any of them is in scope. *)
+let slugs_of_day (d : (Rite_ef.Vocab_ef.season, Rite_ef.Vocab_ef.rank) Colitur_kernel.Liturgical_day.t) =
+ let open Colitur_kernel in
+ let slug (c : _ Celebration.t) = Slug.to_string c.Celebration.slug in
+ (slug d.Liturgical_day.observed)
+ :: List.map (fun (c, _priv) -> slug c) d.Liturgical_day.commemorations
+ @ (match d.Liturgical_day.transferred_in with None -> [] | Some c -> [ slug c ])
+ @ List.map (fun (c, _date) -> slug c) d.Liturgical_day.transferred_out
+
+let test_every_slug_has_a_latin_name () =
+ let t = la () in
+ 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
+ let missing = ref [] in
+ for y = 2020 to 2045 do
+ Array.iter
+ (fun d ->
+ List.iter
+ (fun slug ->
+ (* A miss returns the key itself, so name = slug means "no entry". *)
+ if L.celebration t slug = slug && not (List.mem slug !missing) then
+ missing := slug :: !missing)
+ (slugs_of_day d))
+ (Colitur_kernel.Calendar.year ctx layer y)
+ done;
+ if !missing <> [] then
+ Alcotest.failf "%d slugs have no Latin name: %s" (List.length !missing)
+ (String.concat ", " (List.sort compare !missing))
+
+let test_vocabularies_are_complete () =
+ let t = la () in
+ List.iter
+ (fun s -> if L.season t s = s then Alcotest.failf "no Latin season name for %S" s)
+ [ "advent"; "christmastide"; "time-after-epiphany"; "septuagesima"; "lent";
+ "passiontide"; "paschaltide"; "time-after-pentecost" ];
+ List.iter
+ (fun r -> if L.rank t r = r then Alcotest.failf "no Latin rank name for %S" r)
+ [ "class-1"; "class-2"; "class-3"; "class-4" ];
+ List.iter
+ (fun c -> if L.colour t c = c then Alcotest.failf "no Latin colour name for %S" c)
+ [ "white"; "red"; "green"; "violet"; "rose"; "black" ];
+ for n = 0 to 6 do
+ if L.weekday t n = string_of_int n then Alcotest.failf "no Latin weekday for %d" n
+ done;
+ for n = 1 to 12 do
+ if L.month t n = string_of_int n then Alcotest.failf "no Latin month for %d" n
+ done
+
+(* 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
+ slug -- that is what makes an incomplete translation shippable from its
+ first line. This test proves the CHAIN MECHANISM itself, independent of
+ how complete lang/en.ini happens to be today: a from-scratch table with NO
+ [celebration] entries at all, chained to the real la.ini, must still
+ resolve a real la.ini key -- so the test cannot be defeated simply by
+ en.ini becoming more complete over time. It also sanity-checks the real
+ shipped file: that it parses, declares the right fallback code, and that
+ at least one of its own entries resolves directly (not merely through the
+ chain). *)
+let test_en_falls_back_to_latin () =
+ let en =
+ match L.of_string (read "../lang/en.ini") with
+ | Ok t -> t
+ | Error e -> Alcotest.failf "lang/en.ini: %s" e
+ in
+ Alcotest.(check string) "declares la fallback" "la"
+ (Option.value (L.fallback_code en) ~default:"NONE");
+ (* The real shipped file: at least one of its own entries resolves without
+ needing the chain at all. *)
+ Alcotest.(check bool) "en.ini names ef-epiphany directly" true
+ (L.celebration en "ef-epiphany" <> "ef-epiphany");
+ (* The mechanism, isolated from today's en.ini coverage: an EMPTY table
+ (no [celebration] section) chained to la.ini must still resolve a real
+ la.ini-only key through the fallback. *)
+ let empty =
+ match L.of_string "[meta]\nlang = en\nfallback = la\n" with
+ | Ok t -> t
+ | Error e -> Alcotest.failf "synthetic empty en table: %s" e
+ in
+ let chained = L.with_fallback empty (la ()) in
+ Alcotest.(check bool) "empty table falls back to la.ini for a real slug" true
+ (L.celebration chained "hilary" <> "hilary")
+
+let suite =
+ ( "Lang/coverage",
+ [ Alcotest.test_case "every slug has a Latin name" `Slow test_every_slug_has_a_latin_name;
+ Alcotest.test_case "vocabularies complete" `Quick test_vocabularies_are_complete;
+ Alcotest.test_case "en.ini falls back to Latin" `Quick test_en_falls_back_to_latin ] )