summaryrefslogtreecommitdiff
path: root/test/test_emit.ml
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-08-27 00:24:03 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-08-27 00:24:03 +0200
commitbf42e85ed622da5227a69c26ec801d32c767bb32 (patch)
treee226fc4dfe25252bd90d706ea415c60c5c9791d9 /test/test_emit.ml
parent90584d87e763789808329847c46dbaa8e22dca47 (diff)
downloadcolitur-bf42e85ed622da5227a69c26ec801d32c767bb32.tar.gz
colitur-bf42e85ed622da5227a69c26ec801d32c767bb32.zip
fix(render): derive citation keys from the day, not a hardcoded pair (W5)
lib/render/view.ml hardcoded exactly two template keys, "first" and "gospel", regardless of what a day's citations actually contain. The OF now resolves a third reading, Second, on Sundays and solemnities (OLM 1981 Praenotanda n. 69.1/n. 84(b)(c)) via the rite-supplied Rite.t.citation_shapes, which the render layer could not express. Latent today (bin/main.ml's reject_rite_for refuses --rite on every command that reaches View.of_days, so only EF ever reaches this path), but the wrong shape to carry forward. View.citation_fields now derives one key per Citation.part ACTUALLY PRESENT in a day's own citations, in Citation.all_parts order, using Citation.part_to_string for the key name. An absent part contributes no key at all, not a key holding the empty string: Template.render already treats a missing key and a present Str "" identically in Var, Section and Inverted position, so nothing is lost, and Emit_json.year dumps the whole Obj verbatim, so an unconditionally-present-but-empty key for a still-unbuilt part (Psalm/Tract/Alleluia/Sequence) would sit in every EF day's JSON forever. Filtering to present parts alone is what makes EF's output identical byte for byte BY CONSTRUCTION rather than by a rite check: EF's citations are always exactly [First; Gospel], so the derived key set is always {"first"; "gospel"}, in that order. padding_cell keeps its own fixed "first"/"gospel" pair rather than deriving from a (nonexistent) day: it mirrors the closed set every rite currently reachable through this render path (EF alone) always carries, documented as debt for whenever a rite whose citation_shapes includes Second is admitted here -- test_padding_and_real_share_key_set would catch the mismatch immediately. Downstream consumers: Emit_json needed no change (already a verbatim dump). Emit_xml and Emit_ics gained a third, order-preserving element/line (a <citation part="second"> element; a "Second ..." DESCRIPTION line spliced between Epistle and Gospel) since neither is a fixed-column format. Emit_csv is deliberately left unwidened and commented: RFC 4180's header-row contract makes a variable column count illegal, and OF cannot reach `emit` today regardless, so there is nothing to widen for yet; admitting OF here later needs a disclosed, deliberate format change, not a silent one. term_keys (line ~239) is left unchanged, with a comment recording the judgment call: "epistle" and "lesson" are indeed EF/Roman vocabulary (Epistola vs. a non-apostolic Lectio, e.g. an Ember day's Old Testament reading) sitting in an otherwise rite-agnostic list, but nothing forces a fix yet -- no OF template exists, and every in-repo EF template already reads term.epistle, so renaming it would break 11 shipped templates and any user template for zero present benefit. "lesson" already sits unused, already translated in both lang files, and is the natural neutral term a future OF ordo template would reach for. Tests: test_view.ml gains test_ef_days_never_carry_a_second_key (a whole year, not one day) and test_three_citation_day_exposes_all_three, driving View.of_days directly with a hand-built Liturgical_day.t since OF cannot reach this path through the CLI yet -- the only honest way to exercise a latent path. test_emit.ml/test_ics.ml extend coverage to Emit_xml/Emit_ics (presence, value, and reading order) and pin Emit_csv's deliberate 16-column non-widening. EF byte-identity: zero diff in data/ef/ or lib/rites/rite_ef/ against the pre-change commit. `colitur day`, `readings`, `emit --format csv/xml/json/ ics`, and `table --template templates/ef/ordo.txt` for 2026, all non-empty, all byte-identical (cmp) against a worktree build of the pre-change commit.
Diffstat (limited to 'test/test_emit.ml')
-rw-r--r--test/test_emit.ml46
1 files changed, 45 insertions, 1 deletions
diff --git a/test/test_emit.ml b/test/test_emit.ml
index 0a40054..bd9cadc 100644
--- a/test/test_emit.ml
+++ b/test/test_emit.ml
@@ -91,6 +91,20 @@ let test_csv_quotes_commas () =
Alcotest.(check bool) "Joseph's comma-bearing name is quoted whole, not split" true
(contains ~needle:"\"St. Joseph, Spouse of the Bl. Virgin Mary\"" joseph)
+(* W5: a day with [[First; Second; Gospel]] MUST NOT widen the CSV's own
+ fixed 16-column header -- see [Emit_csv]'s own comment for why a variable
+ column count is not RFC 4180-legal. Guards the deliberate-drop claim made
+ there with an actual assertion, not only a comment: still exactly 16
+ fields (the [Second] reading genuinely absent from the row, not merely
+ untested), and the header is unchanged. *)
+let test_csv_does_not_widen_for_a_third_citation () =
+ let out = Csv.year (Test_view.three_citation_view ()) in
+ let lines = String.split_on_char '\n' out |> List.filter (fun l -> l <> "") in
+ Alcotest.(check string) "header unchanged"
+ "date,rite,season,season_name,week,slug,name,weekday,rank,rank_name,colour,colour_name,subject,first,gospel,comms"
+ (List.hd lines);
+ Alcotest.(check int) "row still 16 fields" 16 (List.length (parse_csv_row (List.nth lines 1)))
+
let test_json_parses_back () =
let out = Json.year (view_2027 ()) in
Alcotest.(check bool) "starts as an object" true (out.[0] = '{');
@@ -133,6 +147,8 @@ let suite =
( "Emit/csv+json",
[ Alcotest.test_case "csv header and rows" `Quick test_csv_header_and_rows;
Alcotest.test_case "csv quotes commas" `Quick test_csv_quotes_commas;
+ Alcotest.test_case "csv does not widen for a third citation" `Quick
+ test_csv_does_not_widen_for_a_third_citation;
Alcotest.test_case "json parses back" `Quick test_json_parses_back;
Alcotest.test_case "json escapes" `Quick test_json_escapes;
Alcotest.test_case "json passes utf8 through" `Quick test_utf8_passes_through_json ] )
@@ -194,10 +210,38 @@ let test_xml_escapes_live_data () =
String.iteri (fun i c -> if c = '&' && not (is_entity_at out i) then incr bare) out;
Alcotest.(check int) "no bare, unescaped '&' anywhere in the document" 0 !bare
+(* First index [needle] starts at in [hay], or fails the test -- a small,
+ dependency-free substring search (no [Str], per the project's frozen
+ deps), sufficient for the fixed literal tags searched for below. *)
+let index_of ~needle hay =
+ let n = String.length needle and h = String.length hay in
+ let rec go i =
+ if i + n > h then Alcotest.failf "%S not found" needle
+ else if String.sub hay i n = needle then i
+ else go (i + 1)
+ in
+ go 0
+
+(* W5: unlike CSV, XML has no fixed column count -- [<citation>] is a
+ repeated element, so a [Second] reading is a third element, not a
+ breaking schema change. Checks BOTH the presence and the ORDER
+ (first, second, gospel -- reading order, not declaration order in
+ [Emit_xml.day]'s own three [cite] calls). *)
+let test_xml_three_citations () =
+ let out = Xml.year (Test_view.three_citation_view ()) in
+ Alcotest.(check bool) "second citation present" true
+ (contains ~needle:"<citation part=\"second\">1 Cor 1:3-9</citation>" out);
+ let first_at = index_of ~needle:"<citation part=\"first\">" out in
+ let second_at = index_of ~needle:"<citation part=\"second\">" out in
+ let gospel_at = index_of ~needle:"<citation part=\"gospel\">" out in
+ Alcotest.(check bool) "reading order: first < second < gospel" true
+ (first_at < second_at && second_at < gospel_at)
+
let xml_suite =
( "Emit/xml",
[ Alcotest.test_case "shape" `Quick test_xml_shape;
Alcotest.test_case "tags balance" `Quick test_xml_tags_balance;
Alcotest.test_case "escapes data" `Quick test_xml_escapes_data;
Alcotest.test_case "escapes live data (2035, Fabian & Sebastian)" `Quick
- test_xml_escapes_live_data ] )
+ test_xml_escapes_live_data;
+ Alcotest.test_case "three citations, in reading order" `Quick test_xml_three_citations ] )