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
|
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). *)
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 "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 ] )
|