aboutsummaryrefslogtreecommitdiff
path: root/test/test_lang.ml
blob: 59bb9a03040a6d628124b1ebf00d461e06339c9e (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
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
module L = Colitur_naming.Lang

let ok = function Ok x -> x | Error e -> Alcotest.failf "parse: %s" e

let sample =
  "[meta]\n\
   lang = xx\n\
   fallback = la\n\
   [celebration]\n\
   ef-lent-3-monday = Feria II hebdomadae III Quadragesimae\n\
   francis-de-sales = S. Francisci Salesii\n\
   [weekday]\n\
   sunday = Dominica\n\
   monday = Feria II\n\
   [month]\n\
   1 = Ianuarius\n\
   [month_abbr]\n\
   1 = Ian\n\
   [season]\n\
   lent = Quadragesima\n\
   [rank]\n\
   class-1 = I classis\n\
   [colour]\n\
   white = albus\n\
   [term]\n\
   epistle = Epistola\n"

let test_meta () =
  let t = ok (L.of_string sample) in
  Alcotest.(check string) "code" "xx" (L.code t);
  Alcotest.(check (option string)) "fallback" (Some "la") (L.fallback_code t)

let test_lookups () =
  let t = ok (L.of_string sample) in
  Alcotest.(check string) "celebration" "Feria II hebdomadae III Quadragesimae"
    (L.celebration t "ef-lent-3-monday");
  Alcotest.(check string) "weekday 0 is Sunday" "Dominica" (L.weekday t 0);
  Alcotest.(check string) "month 1" "Ianuarius" (L.month t 1);
  Alcotest.(check string) "month_abbr 1" "Ian" (L.month_abbr t 1);
  Alcotest.(check string) "season" "Quadragesima" (L.season t "lent");
  Alcotest.(check string) "rank" "I classis" (L.rank t "class-1");
  Alcotest.(check string) "colour" "albus" (L.colour t "white");
  Alcotest.(check string) "term" "Epistola" (L.term t "epistle")

(* THE load-bearing property: a missing key degrades to the key itself, never to
   empty. A partial translation must be usable from its first line, and an
   untranslated day must still say something a reader can act on. *)
let test_missing_degrades_to_key () =
  let t = ok (L.of_string sample) in
  Alcotest.(check string) "unknown celebration" "ef-advent-1-monday"
    (L.celebration t "ef-advent-1-monday");
  Alcotest.(check string) "unknown colour" "rose" (L.colour t "rose");
  Alcotest.(check string) "unknown term" "gospel" (L.term t "gospel")

let test_fallback_chain () =
  let base = ok (L.of_string "[meta]\nlang = la\n[celebration]\na = ALPHA\nb = BETA\n") in
  let over = ok (L.of_string "[meta]\nlang = xx\nfallback = la\n[celebration]\nb = BETA-XX\n") in
  let t = L.with_fallback over base in
  Alcotest.(check string) "own key wins" "BETA-XX" (L.celebration t "b");
  Alcotest.(check string) "falls back" "ALPHA" (L.celebration t "a");
  Alcotest.(check string) "neither: the key" "c" (L.celebration t "c")

(* --raw must be a real identity table, not a special case threaded through every
   call site: one table the whole program can pass around. *)
let test_raw_is_identity () =
  Alcotest.(check string) "celebration" "ef-epiphany" (L.celebration L.raw "ef-epiphany");
  Alcotest.(check string) "colour" "white" (L.colour L.raw "white");
  Alcotest.(check string) "weekday" "0" (L.weekday L.raw 0)

let test_malformed_is_error_not_crash () =
  match L.of_string "[celebration\nbroken" with
  | Error _ -> ()
  | Ok _ -> Alcotest.fail "a malformed language file must be an Error, never accepted"

let test_missing_meta_lang_is_error () =
  match L.of_string "[celebration]\na = B\n" with
  | Error _ -> ()
  | Ok _ -> Alcotest.fail "a language file with no [meta] lang must be an Error"

(* F1 regression: a hand-edited language file WILL grow duplicate [section]
   headers as contributors append entries over time (Tasks 3/4's 595-entry
   la.ini). Both blocks' keys must resolve -- silently dropping the second
   block is a data-loss footgun that surfaces as a false "missing name"
   report far from its real cause. *)
let test_duplicate_sections_all_merge () =
  let t =
    ok
      (L.of_string
         "[meta]\nlang = la\n[celebration]\na = ALPHA\n[weekday]\nsunday = Dominica\n\
          [celebration]\nb = BETA\n")
  in
  Alcotest.(check string) "first block's key" "ALPHA" (L.celebration t "a");
  Alcotest.(check string) "second block's key" "BETA" (L.celebration t "b")

let test_duplicate_key_across_sections_last_wins () =
  let t =
    ok
      (L.of_string
         "[meta]\nlang = la\n[celebration]\na = FIRST\n[celebration]\na = SECOND\n")
  in
  Alcotest.(check string) "later block's value wins" "SECOND" (L.celebration t "a")

let test_duplicate_key_within_section_last_wins () =
  let t = ok (L.of_string "[meta]\nlang = la\n[celebration]\na = FIRST\na = SECOND\n") in
  Alcotest.(check string) "later line's value wins" "SECOND" (L.celebration t "a")

(* Task 6: [bible] and [sigla] used to be two more section names [of_string]
   never looked for -- a tenth (or eleventh) section a file author writes was
   silently ignored, not rejected. Verified directly against a checked-out
   scratch executable before this change: [Lang.keys] on a table built from
   text containing a [bible] section came back with zero entries, no error
   either. *)
let test_bible_section_is_read () =
  let text = "[meta]\nlang = xx\n[bible]\nluke.abbr = Lc\nluke.full = Ewangelia\n" in
  match L.of_string text with
  | Error e -> Alcotest.failf "parse: %s" e
  | Ok t ->
      Alcotest.(check string) "abbr" "Lc" (L.bible t "luke.abbr");
      Alcotest.(check string) "full" "Ewangelia" (L.bible t "luke.full");
      (* the TOTAL contract: a miss returns the key *)
      Alcotest.(check string) "miss" "mark.abbr" (L.bible t "mark.abbr")

let test_sigla_section_is_read () =
  let text = "[meta]\nlang = xx\n[sigla]\nbook = full\npart_sep = \"; \"\n" in
  match L.of_string text with
  | Error e -> Alcotest.failf "parse: %s" e
  | Ok t ->
      let f = L.sigla_fields t in
      Alcotest.(check (option string)) "book" (Some "full") (List.assoc_opt "book" f);
      (* the quotes survive Lang; Render.style_of_fields strips them *)
      Alcotest.(check (option string)) "sep" (Some "\"; \"") (List.assoc_opt "part_sep" f)

(* The deliberate asymmetry: [bible] joins [keys] (lang --check's reference
   set, so a translation missing every book name is reported as incomplete);
   [sigla] does not (it is five settings with working defaults, not names a
   translator owes -- putting it in [keys] would make --check demand five
   settings from every language file). Pinned so a later change here is a
   deliberate one, not a drive-by. *)
let test_check_demands_bible_not_sigla () =
  let text = "[meta]\nlang = xx\n[bible]\nluke.abbr = Lc\n[sigla]\nbook = full\n" in
  match L.of_string text with
  | Error e -> Alcotest.failf "parse: %s" e
  | Ok t ->
      let ks = L.keys t in
      Alcotest.(check bool) "bible in keys" true (List.mem_assoc "bible.luke.abbr" ks);
      Alcotest.(check bool) "sigla not in keys" false
        (List.exists (fun (k, _) -> String.length k > 6 && String.sub k 0 6 = "sigla.") ks)

let suite =
  ( "Lang",
    [ Alcotest.test_case "meta" `Quick test_meta;
      Alcotest.test_case "lookups" `Quick test_lookups;
      Alcotest.test_case "missing degrades to key" `Quick test_missing_degrades_to_key;
      Alcotest.test_case "fallback chain" `Quick test_fallback_chain;
      Alcotest.test_case "raw is identity" `Quick test_raw_is_identity;
      Alcotest.test_case "malformed is error" `Quick test_malformed_is_error_not_crash;
      Alcotest.test_case "missing meta lang is error" `Quick test_missing_meta_lang_is_error;
      Alcotest.test_case "duplicate sections all merge" `Quick test_duplicate_sections_all_merge;
      Alcotest.test_case "duplicate key across sections: last wins" `Quick
        test_duplicate_key_across_sections_last_wins;
      Alcotest.test_case "duplicate key within section: last wins" `Quick
        test_duplicate_key_within_section_last_wins;
      Alcotest.test_case "bible section is read" `Quick test_bible_section_is_read;
      Alcotest.test_case "sigla section is read" `Quick test_sigla_section_is_read;
      Alcotest.test_case "check demands bible not sigla" `Quick
        test_check_demands_bible_not_sigla ] )