diff options
Diffstat (limited to 'test/test_lang_coverage.ml')
| -rw-r--r-- | test/test_lang_coverage.ml | 123 |
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 ] ) |
