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 ] )