aboutsummaryrefslogtreecommitdiff
path: root/test/test_lang_coverage.ml
blob: 9ed633c4201ef8254aa62746d9ece1a983763187 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
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

(* Defect 1: the booklet used to print the bare rite id ("ef") because no
   [rite] section existed in either language file. Walks every rite id the
   engine can actually emit -- one today (Rite_ef.Temporal_ef.id, the same
   id `Rite_ef.context` stamps onto every Rite.t.id -- see rite_ef.ml) -- so
   this is cheap now and fails loudly the moment a second rite module (OF)
   is wired in without a matching [rite] entry in la.ini. *)
let test_every_rite_has_a_latin_name () =
  let t = la () in
  List.iter
    (fun id -> if L.rite t id = id then Alcotest.failf "no Latin display name for rite %S" id)
    [ Rite_ef.Temporal_ef.id ]

(* 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 "every rite has a Latin name" `Quick test_every_rite_has_a_latin_name;
      Alcotest.test_case "en.ini falls back to Latin" `Quick test_en_falls_back_to_latin ] )