aboutsummaryrefslogtreecommitdiff
path: root/test/test_config.ml
blob: 1dc13820291343e39318e86fe4da29f3a800070a (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
167
168
169
170
171
172
173
174
module C = Colitur_naming.Config

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

let sample =
  "[defaults]\n\
   lang = pl\n\
   overlay = ~/a.ini\n\
   overlay = ~/b.ini\n\
   template = ~/my-ordo.tex\n\
   format = json\n"

let test_reads_defaults () =
  let c = ok (C.of_string sample) in
  Alcotest.(check (option string)) "lang" (Some "pl") (C.lang c);
  Alcotest.(check (option string)) "template" (Some "~/my-ordo.tex") (C.template c);
  Alcotest.(check (option string)) "format" (Some "json") (C.format c)

(* Repeated keys accumulate for overlay -- a user has more than one. The INI
   reader keeps every line, so this asserts we do not silently take the last. *)
let test_overlays_accumulate () =
  let c = ok (C.of_string sample) in
  Alcotest.(check (list string)) "both overlays" [ "~/a.ini"; "~/b.ini" ] (C.overlays c)

let test_empty_config_is_all_none () =
  Alcotest.(check (option string)) "lang" None (C.lang C.empty);
  Alcotest.(check (list string)) "overlays" [] (C.overlays C.empty)

(* THE precedence rule: flag > config > default, and the SOURCE is reported,
   because a setting that silently comes from a file the user forgot about is
   worse than no setting at all. *)
let test_precedence_and_provenance () =
  let check ~flag ~config ~default (ev, es) =
    let v, s = C.resolve ~flag ~config ~default in
    Alcotest.(check string) "value" ev v;
    Alcotest.(check string) "source" es s
  in
  check ~flag:(Some "en") ~config:(Some "pl") ~default:"la" ("en", "flag");
  check ~flag:None ~config:(Some "pl") ~default:"la" ("pl", "config");
  check ~flag:None ~config:None ~default:"la" ("la", "default")

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

(* An unknown key is a WARNING case, not a hard error: a config written for a
   newer colitur must still work on an older one. But it must be reportable, so
   it is not silently dropped either -- assert it is actually COLLECTED, not
   only that parsing survives it: a no-op accumulator would also pass a test
   that checked survival alone. *)
let test_unknown_key_is_reported_not_fatal () =
  match C.of_string "[defaults]\nlang = la\nnonsense = 1\n" with
  | Error _ -> Alcotest.fail "an unknown key must not be fatal"
  | Ok c ->
      Alcotest.(check (option string)) "known key still read" (Some "la") (C.lang c);
      Alcotest.(check (list string)) "unknown key reported" [ "nonsense" ] (C.unknown_keys c)

(* The highest-value case: a misspelled SECTION name (not merely a misspelled
   key inside a recognised one). Before this fix the whole section, lang
   included, vanished with nothing reported -- exactly the typo this feature
   exists to surface, in its worst form: a user whose config silently does
   nothing has no way to discover why. *)
let test_misspelled_section_is_reported_not_silently_dropped () =
  let c = ok (C.of_string "[deafults]\nlang = pl\n") in
  Alcotest.(check (option string)) "lang not read from the wrong section" None (C.lang c);
  Alcotest.(check (list string)) "section reported" [ "deafults" ] (C.unknown_sections c)

(* THE regression test for the cross-module inconsistency: [config.ml] used
   to locate [\[defaults\]] with [List.find_opt], taking only the FIRST
   matching section and silently discarding every later one, while
   [lang.ml]'s [of_string] folds over ALL matching sections. A scalar set in
   the first block and a DIFFERENT scalar set only in the second block must
   both resolve -- before the fix, [template] (second-block-only) came back
   [None]. *)
let test_two_defaults_blocks_both_contribute () =
  let text = "[defaults]\nlang = pl\n\n[defaults]\ntemplate = ~/my-ordo.tex\n" in
  let c = ok (C.of_string text) in
  Alcotest.(check (option string)) "lang from first block" (Some "pl") (C.lang c);
  Alcotest.(check (option string)) "template from second block" (Some "~/my-ordo.tex") (C.template c)

(* Consistent with the existing within-section last-wins rule (see
   [config.mli]'s [lang] comment): a key repeated ACROSS two [\[defaults\]]
   blocks resolves to the value from the LATER block, exactly as it would if
   both lines sat in one block. *)
let test_key_repeated_across_blocks_last_wins () =
  let text = "[defaults]\nlang = pl\n\n[defaults]\nlang = en\n" in
  let c = ok (C.of_string text) in
  Alcotest.(check (option string)) "later block's lang wins" (Some "en") (C.lang c)

(* [overlay] must keep accumulating across block boundaries, in file order,
   not merely within one block. *)
let test_overlays_accumulate_across_blocks () =
  let text = "[defaults]\noverlay = ~/a.ini\n\n[defaults]\noverlay = ~/b.ini\n" in
  let c = ok (C.of_string text) in
  Alcotest.(check (list string)) "both overlays, in file order" [ "~/a.ini"; "~/b.ini" ] (C.overlays c)

(* Merging a second [\[defaults\]] block is the whole point -- it must not
   start being reported as an unknown section. *)
let test_two_defaults_blocks_report_no_unknown_sections () =
  let text = "[defaults]\nlang = pl\n\n[defaults]\ntemplate = ~/my-ordo.tex\n" in
  let c = ok (C.of_string text) in
  Alcotest.(check (list string)) "no unknown sections" [] (C.unknown_sections c)

(* Task 8: sigla_style/sigla_book/sigla_tradition read the same way
   lang/template/format do, in the same [\[defaults\]] section -- no new
   section, and every key recognised (so [unknown_keys] stays empty). *)
(* The escaping flavour is usually inferred from the template's extension, so
   this key exists for the template whose extension says nothing. Getting it
   wrong yields malformed output rather than ugly output, which is why there
   is no silent fallback and why it is worth being settable rather than
   passable only per-run. *)
let test_flavour_is_read () =
  match C.of_string "[defaults]\nflavour = groff\n" with
  | Error e -> Alcotest.failf "parse: %s" e
  | Ok c ->
      Alcotest.(check (option string)) "flavour" (Some "groff") (C.flavour c);
      Alcotest.(check (list string)) "nothing unknown" [] (C.unknown_keys c)

let test_flavour_absent_is_none () =
  match C.of_string "[defaults]\nlang = en\n" with
  | Error e -> Alcotest.failf "parse: %s" e
  | Ok c -> Alcotest.(check bool) "unset" true (C.flavour c = None)

let test_sigla_keys_are_read () =
  let text =
    "[defaults]\nsigla_style = la\nsigla_book = full\nsigla_tradition = modern\n"
  in
  match C.of_string text with
  | Error e -> Alcotest.failf "parse: %s" e
  | Ok c ->
      Alcotest.(check (option string)) "style" (Some "la") (C.sigla_style c);
      Alcotest.(check (option string)) "book" (Some "full") (C.sigla_book c);
      Alcotest.(check (option string)) "tradition" (Some "modern") (C.sigla_tradition c);
      Alcotest.(check (list string)) "nothing unknown" [] (C.unknown_keys c)

(* The empty config carries no sigla settings either -- same "all None"
   contract [test_empty_config_is_all_none] already asserts for lang. *)
let test_empty_config_sigla_keys_are_none () =
  Alcotest.(check (option string)) "sigla_style" None (C.sigla_style C.empty);
  Alcotest.(check (option string)) "sigla_book" None (C.sigla_book C.empty);
  Alcotest.(check (option string)) "sigla_tradition" None (C.sigla_tradition C.empty)

(* Same last-wins duplicate policy as lang/template/format: a key repeated
   across two [\[defaults\]] blocks resolves to the LATER block's value. *)
let test_sigla_key_repeated_across_blocks_last_wins () =
  let text = "[defaults]\nsigla_book = full\n\n[defaults]\nsigla_book = abbr\n" in
  let c = ok (C.of_string text) in
  Alcotest.(check (option string)) "later block's sigla_book wins" (Some "abbr") (C.sigla_book c)

let suite =
  ( "Config",
    [ Alcotest.test_case "reads defaults" `Quick test_reads_defaults;
      Alcotest.test_case "overlays accumulate" `Quick test_overlays_accumulate;
      Alcotest.test_case "empty is all none" `Quick test_empty_config_is_all_none;
      Alcotest.test_case "precedence and provenance" `Quick test_precedence_and_provenance;
      Alcotest.test_case "malformed is error" `Quick test_malformed_is_error_not_crash;
      Alcotest.test_case "unknown key not fatal" `Quick test_unknown_key_is_reported_not_fatal;
      Alcotest.test_case "misspelled section reported"
        `Quick test_misspelled_section_is_reported_not_silently_dropped;
      Alcotest.test_case "two defaults blocks both contribute"
        `Quick test_two_defaults_blocks_both_contribute;
      Alcotest.test_case "key repeated across blocks last wins"
        `Quick test_key_repeated_across_blocks_last_wins;
      Alcotest.test_case "overlays accumulate across blocks"
        `Quick test_overlays_accumulate_across_blocks;
      Alcotest.test_case "two defaults blocks report no unknown sections"
        `Quick test_two_defaults_blocks_report_no_unknown_sections;
      Alcotest.test_case "flavour is read" `Quick test_flavour_is_read;
      Alcotest.test_case "flavour absent is none" `Quick test_flavour_absent_is_none;
      Alcotest.test_case "sigla keys are read" `Quick test_sigla_keys_are_read;
      Alcotest.test_case "empty config sigla keys are none" `Quick test_empty_config_sigla_keys_are_none;
      Alcotest.test_case "sigla key repeated across blocks last wins"
        `Quick test_sigla_key_repeated_across_blocks_last_wins ] )