diff options
Diffstat (limited to 'test')
| -rw-r--r-- | test/cli.t | 19 | ||||
| -rw-r--r-- | test/test_colitur.ml | 2 | ||||
| -rw-r--r-- | test/test_overlay_ini.ml | 152 |
3 files changed, 172 insertions, 1 deletions
@@ -373,3 +373,22 @@ to empty and to the overlay's own id: tiny.sexp: ok -- overlay tiny, 1 directive(s): 1 add, 0 suppress, 0 replace, 0 edit every directive found its target add tiny-feast + +`convert` turns the flat INI form into the S-expression one and verifies its +own output before emitting it. The full pipeline, INI to a resolved day: + + $ printf '[overlay]\nid = my-parish\n[our-patron]\ndate = 07-11\nrank = class-3\ncolour = white\nname.en = St Example\n' > p.ini + $ colitur convert p.ini > p.sexp + $ colitur check p.sexp + p.sexp: ok -- overlay my-parish, 1 directive(s): 1 add, 0 suppress, 0 replace, 0 edit + every directive found its target + add our-patron + $ colitur day 2026 --overlay p.sexp | grep '^2026-07-11' + 2026-07-11 saturday time-after-pentecost 6 our-patron class-3 white +pius-i + +What the INI form cannot express is refused by name, not dropped silently: + + $ printf '[overlay]\nid = x\n[y]\nreplace = yes\n' > r.ini + $ colitur convert r.ini + colitur: r.ini: section [y]: replace is not expressible in the INI form -- it needs a whole entry, which is what the S-expression form is for (see colitur-overlay(5)). Suppress plus a fresh section is usually what you want instead. + [2] diff --git a/test/test_colitur.ml b/test/test_colitur.ml index 1e6b7ed..76ff685 100644 --- a/test/test_colitur.ml +++ b/test/test_colitur.ml @@ -2,7 +2,7 @@ let () = Alcotest.run "colitur" [ Test_date.suite; Test_computus.suite; Test_colour.suite; Test_slug.suite; Test_names.suite; - Test_overlay.suite; Test_temporal_ef.suite; Test_validate.suite; Test_precedence.suite; + Test_overlay.suite; Test_overlay_ini.suite; Test_temporal_ef.suite; Test_validate.suite; Test_precedence.suite; Test_calendar.suite; Test_precedence_ef.suite; Test_sanctoral_ef.suite; Test_rite_ef.suite; Test_differential.suite; Test_oracle.suite; Test_oracle.suite_2038; Test_oracle.suite_2035; Test_golden.suite; ("lectionary", Test_lectionary.suite); diff --git a/test/test_overlay_ini.ml b/test/test_overlay_ini.ml new file mode 100644 index 0000000..65e681a --- /dev/null +++ b/test/test_overlay_ini.ml @@ -0,0 +1,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 ] ) |
