aboutsummaryrefslogtreecommitdiff
path: root/test
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-08-18 13:20:50 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-08-18 13:20:50 +0200
commite48fa4a2d315ac2b3832611ff48ed2e986e7b49e (patch)
tree95d0a76818140d833577a46bf778b1e92822abcb /test
parent084ff22997af208715656924d9fc23bf2ec10050 (diff)
parent49494c3aedf3f1b40c6ee36cb4de5615f097274c (diff)
downloadcolitur-e48fa4a2d315ac2b3832611ff48ed2e986e7b49e.tar.gz
colitur-e48fa4a2d315ac2b3832611ff48ed2e986e7b49e.zip
merge: overlay authoring ergonomics
Two mandatory fields made optional, parse errors that stop naming kernel source files, and a feedback loop -- colitur check and colitur new-overlay -- for a file the test layers deliberately cannot vouch for.
Diffstat (limited to 'test')
-rw-r--r--test/cli.t38
-rw-r--r--test/test_overlay.ml66
2 files changed, 100 insertions, 4 deletions
diff --git a/test/cli.t b/test/cli.t
index 0c79db4..d735e69 100644
--- a/test/cli.t
+++ b/test/cli.t
@@ -17,7 +17,7 @@ A year outside the supported domain is rejected (exit 2):
No/garbage arguments give a usage error (exit 2):
$ colitur
- colitur: usage: colitur easter <year> | colitur temporal <year> | colitur day <year> | colitur readings <year> (try: colitur --help)
+ colitur: usage: colitur easter <year> | colitur temporal <year> | colitur day <year> | colitur readings <year> | colitur check FILE | colitur new-overlay (try: colitur --help)
[2]
The EF temporal cycle for a year, one line per day:
@@ -327,17 +327,49 @@ A flag needing a value, given none:
$ colitur day 2026 --overlay
colitur: --overlay needs a file path
- colitur: usage: colitur easter <year> | colitur temporal <year> | colitur day <year> | colitur readings <year> (try: colitur --help)
+ colitur: usage: colitur easter <year> | colitur temporal <year> | colitur day <year> | colitur readings <year> | colitur check FILE | colitur new-overlay (try: colitur --help)
[2]
An unknown option is rejected rather than treated as a positional word:
$ colitur day 2026 --diocese
colitur: unknown option --diocese
- colitur: usage: colitur easter <year> | colitur temporal <year> | colitur day <year> | colitur readings <year> (try: colitur --help)
+ colitur: usage: colitur easter <year> | colitur temporal <year> | colitur day <year> | colitur readings <year> | colitur check FILE | colitur new-overlay (try: colitur --help)
[2]
The shipped example overlay is runnable documentation, and it must actually
load -- an example that silently rotted would be worse than none. The cram
sandbox cannot reach data/, so the assertion that it loads and applies lives
in test_lectionary_ef.ml, which reads it from the source tree directly.
+
+`new-overlay` prints a starter file to stdout for redirection, and what it
+prints must itself be valid -- a template that does not load is worse than no
+template, because it teaches the wrong shape. Round-tripped here rather than
+pinned line by line, so editing the template's prose does not fail this test
+while a syntax error in it still does:
+
+ $ colitur new-overlay > starter.sexp
+ $ colitur check starter.sexp
+ starter.sexp: ok -- overlay my-parish, 2 directive(s): 2 add, 0 suppress, 0 replace, 0 edit
+ every directive found its target
+ add my-local-patron
+ add my-dedication
+
+`check` exits 2 when a directive matches nothing, so it is usable in a
+Makefile or a pre-commit hook, not merely readable:
+
+ $ printf '((id p) (directives ((Suppress no-such-slug))))' > bad.sexp
+ $ colitur check bad.sexp
+ bad.sexp: ok -- overlay p, 1 directive(s): 0 add, 1 suppress, 0 replace, 0 edit
+ MATCHED NOTHING: overlay p: suppress no-such-slug: slug not present; nothing to suppress
+ suppress no-such-slug
+ [2]
+
+`citations` and `layer` may be omitted from an added celebration; they default
+to empty and to the overlay's own id:
+
+ $ printf '((id tiny) (directives ((Add ((date (Fixed (month 5) (day 20))) (cel ((slug tiny-feast) (names ((en "Tiny"))) (rank Class3) (status Feast) (colour White) (subject Saint))))))))' > tiny.sexp
+ $ colitur check tiny.sexp
+ tiny.sexp: ok -- overlay tiny, 1 directive(s): 1 add, 0 suppress, 0 replace, 0 edit
+ every directive found its target
+ add tiny-feast
diff --git a/test/test_overlay.ml b/test/test_overlay.ml
index f3d1aee..12eb56d 100644
--- a/test/test_overlay.ml
+++ b/test/test_overlay.ml
@@ -307,9 +307,73 @@ let prop_deterministic =
let a, da = O.apply (base ()) o and b, db = O.apply (base ()) o in
a = b && da = db)
+(* Overlay ERGONOMICS (branch overlay-ergonomics). A user-supplied overlay is
+ hand-written, unlike every other sexp this engine reads, and two of
+ [Celebration.t]'s eight fields carry no information a user could supply
+ meaningfully: [citations] is always empty for a local feast (colitur emits
+ citations from the lectionary, not the calendar), and [layer] just repeats
+ the overlay file's own [id]. Requiring both meant the commonest first
+ mistake -- omitting them -- produced
+ "lib/kernel/celebration.ml.t_of_sexp: the following record elements were
+ undefined: citations layer", which names a source file the author will
+ never open.
+
+ {!Overlay.load} now fills both in before the strict parser runs. This is
+ deliberately scoped to OVERLAY loading: {!Layer.load}, which reads the
+ SHIPPED sanctoral, stays strict, because that data is the project's own and
+ a missing field there is a defect rather than a convenience. *)
+let overlay_missing_citations_and_layer =
+ {|((id my-parish)
+ (directives
+ ((Add
+ ((date (Fixed (month 5) (day 20)))
+ (cel
+ ((slug st-example)
+ (names ((en "St Example")))
+ (rank Class3) (status Feast) (colour White) (subject Saint))))))))|}
+
+let test_overlay_defaults_citations_and_layer () =
+ with_temp_file (fun path ->
+ let oc = open_out path in
+ output_string oc overlay_missing_citations_and_layer;
+ close_out oc;
+ match O.load rank_of_sexp path with
+ | Error e -> Alcotest.failf "expected the overlay to load, got: %s" e
+ | Ok o ->
+ Alcotest.(check string) "id" "my-parish" o.O.id;
+ (match o.O.directives with
+ | [ O.Add e ] ->
+ Alcotest.(check string) "citations defaulted to empty" "0"
+ (string_of_int (List.length e.L.cel.Cel.citations));
+ (* [layer] defaults to the overlay's own id: the value the author
+ would have typed, so a later Suppress/Edit naming it works. *)
+ Alcotest.(check string) "layer defaulted to the overlay id" "my-parish"
+ e.L.cel.Cel.layer
+ | _ -> Alcotest.fail "expected exactly one Add directive"))
+
+(* An explicit [layer] must still win: an overlay may deliberately claim a
+ different layer name from its own id (e.g. one file shipping two logical
+ layers), and defaulting must not overwrite a stated value. *)
+let test_overlay_explicit_layer_wins () =
+ with_temp_file (fun path ->
+ let oc = open_out path in
+ output_string oc
+ (replace_first ~sub:"(subject Saint)"
+ ~by:"(subject Saint) (layer stated-explicitly)" overlay_missing_citations_and_layer);
+ close_out oc;
+ match O.load rank_of_sexp path with
+ | Ok { O.directives = [ O.Add e ]; _ } ->
+ Alcotest.(check string) "explicit layer preserved" "stated-explicitly" e.L.cel.Cel.layer
+ | Ok _ -> Alcotest.fail "expected exactly one Add directive"
+ | Error e -> Alcotest.failf "expected the overlay to load, got: %s" e)
+
let suite =
( "Layer/Overlay",
- [ Alcotest.test_case "layer basics" `Quick test_layer_basics;
+ [ Alcotest.test_case "overlay: citations and layer default when omitted" `Quick
+ test_overlay_defaults_citations_and_layer;
+ Alcotest.test_case "overlay: an explicit layer is not overwritten" `Quick
+ test_overlay_explicit_layer_wins;
+ Alcotest.test_case "layer basics" `Quick test_layer_basics;
Alcotest.test_case "layer by-date index" `Quick test_layer_index;
Alcotest.test_case "layer by-date index accumulates same date" `Quick
test_layer_index_same_date;