aboutsummaryrefslogtreecommitdiff
path: root/test/test_overlay_ini.ml
blob: 65e681a28902897347500d639d70dad8470d64fb (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
(* The INI overlay front end. Its contract is narrow and the tests follow it:
   the flat form parses to exactly the Overlay.t the sexp form would, the
   conversion PROVES that before emitting, and everything it cannot express is
   refused by name rather than silently dropped. *)
module OI = Colitur_kernel.Overlay_ini
module O = Colitur_kernel.Overlay
module L = Colitur_kernel.Layer
module Cel = Colitur_kernel.Celebration
module DS = Colitur_kernel.Date_spec
module S = Colitur_kernel.Slug
module Col = Colitur_kernel.Colour
module Sub = Colitur_kernel.Subject
module V = Rite_ef.Vocab_ef

let parse = OI.parse ~rank_of_string:V.rank_of_string

let convert =
  OI.convert ~rank_of_string:V.rank_of_string ~rank_to_sexp:V.sexp_of_rank
    ~rank_of_sexp:V.rank_of_sexp

let ok_exn = function Ok v -> v | Error e -> Alcotest.failf "expected Ok, got: %s" e
let err_exn = function Error e -> e | Ok _ -> Alcotest.fail "expected Error, got Ok"

let minimal =
  {|[overlay]
id = my-parish
[our-patron]
date = 07-11
rank = class-3
colour = white
name.en = St Example|}

let test_minimal () =
  let t = ok_exn (parse minimal) in
  Alcotest.(check string) "id" "my-parish" t.O.id;
  match t.O.directives with
  | [ O.Add e ] ->
      Alcotest.(check string) "slug" "our-patron" (S.to_string e.L.cel.Cel.slug);
      Alcotest.(check bool) "colour" true (e.L.cel.Cel.colour = Col.White);
      (* status and subject default rather than being required: a local feast
         is overwhelmingly an ordinary saint's feast, and making the common
         case silent is the whole point of this format. *)
      Alcotest.(check bool) "status defaults to Feast" true (e.L.cel.Cel.status = Cel.Feast);
      Alcotest.(check bool) "subject defaults to Saint" true (e.L.cel.Cel.subject = Sub.Saint);
      (* layer defaults to the file's own id, exactly as the sexp form does *)
      Alcotest.(check string) "layer" "my-parish" e.L.cel.Cel.layer
  | _ -> Alcotest.fail "expected one Add"

(* All three Date_spec shapes, since a flat string has to encode what the sexp
   form spells out, and getting one wrong would be silent. *)
let test_date_forms () =
  let one date =
    match (ok_exn (parse (Printf.sprintf "[overlay]\nid=x\n[s]\ndate=%s\nrank=class-3\ncolour=white\nname.en=N" date))).O.directives with
    | [ O.Add e ] -> e.L.date
    | _ -> Alcotest.fail "expected one Add"
  in
  Alcotest.(check bool) "MM-DD" true (one "07-11" = ok_exn (DS.fixed ~month:7 ~day:11));
  Alcotest.(check bool) "easter+N" true (one "easter+60" = ok_exn (DS.easter_offset 60));
  Alcotest.(check bool) "easter-N" true (one "easter-46" = ok_exn (DS.easter_offset (-46)));
  Alcotest.(check bool) "nth weekday" true
    (one "oct/sun/1" = ok_exn (DS.nth_weekday ~month:10 ~nth:1 ~weekday:Colitur_kernel.Date.Sun));
  Alcotest.(check bool) "nth weekday, from the end" true
    (one "oct/sun/-1" = ok_exn (DS.nth_weekday ~month:10 ~nth:(-1) ~weekday:Colitur_kernel.Date.Sun))

let test_suppress_and_edit () =
  let t = ok_exn (parse "[overlay]\nid=x\n[barbara]\nsuppress=yes\n[stanislaus]\nedit=yes\ncolour=red") in
  match t.O.directives with
  | [ O.Suppress s; O.Edit (e, [ O.Set_colour Col.Red ]) ] ->
      Alcotest.(check string) "suppress" "barbara" (S.to_string s);
      Alcotest.(check string) "edit" "stanislaus" (S.to_string e)
  | _ -> Alcotest.fail "expected a Suppress then an Edit"

(* THE contract of this module. convert must never hand back text that means
   something other than the INI said -- a transpiler emitting valid-but-wrong
   sexp is the failure a convenience format invites, and `colitur check` could
   not catch it, since the output would parse cleanly and simply differ. *)
let test_convert_round_trips () =
  let text = ok_exn (convert minimal) in
  let reparsed =
    O.t_of_sexp V.rank_of_sexp (Sexplib.Sexp.of_string (Sexplib.Sexp.to_string (Sexplib.Sexp.of_string
      (let i = String.index text '(' in String.sub text i (String.length text - i)))))
  in
  Alcotest.(check bool) "emitted sexp means exactly what the INI did" true
    (reparsed = ok_exn (parse minimal))

(* Every date form, through the full convert path, so the round-trip proof
   covers the shapes most likely to be mis-rendered rather than just one. *)
let test_convert_round_trips_every_date_form () =
  List.iter
    (fun d ->
      let src = Printf.sprintf "[overlay]\nid=x\n[s]\ndate=%s\nrank=class-3\ncolour=white\nname.en=N" d in
      match convert src with
      | Ok _ -> ()
      | Error e -> Alcotest.failf "convert failed to verify its own output for date %S: %s" d e)
    [ "07-11"; "02-29"; "easter+60"; "easter-46"; "easter+0"; "oct/sun/1"; "oct/sun/-1"; "jan/mon/3" ]

let test_errors_are_values_not_exceptions () =
  let cases =
    [ ("[s]\ndate=07-11\nrank=class-3\ncolour=white\nname.en=N", "no [overlay] section");
      ("[overlay]\nid=x\n[s]\nrank=class-3\ncolour=white\nname.en=N", "date is required");
      ("[overlay]\nid=x\n[s]\ndate=07-11\ncolour=white\nname.en=N", "rank is required");
      ("[overlay]\nid=x\n[s]\ndate=07-11\nrank=class-3\nname.en=N", "colour is required");
      ("[overlay]\nid=x\n[s]\ndate=07-11\nrank=class-3\ncolour=white", "name.<lang>");
      (* A date that matches no form at all falls through to the three-forms
         message. Note 99-99 does NOT belong here: it parses as MM-DD and gets
         the more specific "month 99 out of range 1..12", which is the better
         error and worth not regressing. *)
      ("[overlay]\nid=x\n[s]\ndate=whenever\nrank=class-3\ncolour=white\nname.en=N", "three forms");
      ("[overlay]\nid=x\n[s]\ndate=99-99\nrank=class-3\ncolour=white\nname.en=N", "month 99");
      ("[overlay]\nid=x\n[s]\nreplace=yes", "not expressible") ]
  in
  List.iter
    (fun (src, needle) ->
      let e = err_exn (parse src) in
      let contains hay n =
        let nl = String.length n and hl = String.length hay in
        let rec at i = i + nl <= hl && (String.sub hay i nl = n || at (i + 1)) in
        at 0
      in
      Alcotest.(check bool)
        (Printf.sprintf "error mentions %S (got: %s)" needle e)
        true (contains e needle))
    cases

(* An INI overlay must reach the engine by exactly the path a hand-written sexp
   does -- if the two diverged, this format would be a second semantics rather
   than a front door. *)
let test_same_result_as_handwritten_sexp () =
  let from_ini = ok_exn (parse minimal) in
  let handwritten =
    {|((id my-parish)
       (directives
         ((Add ((date (Fixed (month 7) (day 11)))
                (cel ((slug our-patron) (names ((en "St Example")))
                      (rank Class3) (status Feast) (colour White)
                      (subject Saint) (citations ()) (layer my-parish))))))))|}
  in
  let from_sexp = O.t_of_sexp V.rank_of_sexp (Sexplib.Sexp.of_string handwritten) in
  Alcotest.(check bool) "identical Overlay.t" true (from_ini = from_sexp)

let suite =
  ( "Overlay_ini",
    [ Alcotest.test_case "minimal file, with defaults" `Quick test_minimal;
      Alcotest.test_case "all three date forms" `Quick test_date_forms;
      Alcotest.test_case "suppress and edit" `Quick test_suppress_and_edit;
      Alcotest.test_case "convert verifies its own output" `Quick test_convert_round_trips;
      Alcotest.test_case "convert verifies every date form" `Quick
        test_convert_round_trips_every_date_form;
      Alcotest.test_case "errors are values, never exceptions" `Quick
        test_errors_are_values_not_exceptions;
      Alcotest.test_case "an INI overlay equals the hand-written sexp" `Quick
        test_same_result_as_handwritten_sexp ] )