open Colitur_kernel let slug = Slug.of_string_exn let cit part reference = { Citation.part; reference } let entry s cs = (slug s, cs) let ok_exn = function Ok x -> x | Error e -> Alcotest.fail e (* Stdlib-only substring search -- Astring is not among the frozen deps. *) let contains_substring ~needle haystack = let hn = String.length needle and hh = String.length haystack in let rec go i = i + hn <= hh && (String.sub haystack i hn = needle || go (i + 1)) in hn = 0 || go 0 let test_find_present () = let l = ok_exn (Lectionary.of_entries [ entry "ef-advent-sunday-1" [ cit Citation.First "Rom 13:11-14"; cit Citation.Gospel "Luke 21:25-33" ] ]) in match Lectionary.find l (slug "ef-advent-sunday-1") with | Some [ a; b ] -> Alcotest.(check string) "epistle" "Rom 13:11-14" a.Citation.reference; Alcotest.(check string) "gospel" "Luke 21:25-33" b.Citation.reference | Some _ -> Alcotest.fail "expected exactly two citations" | None -> Alcotest.fail "slug not found" let test_find_absent () = let l = ok_exn (Lectionary.of_entries []) in Alcotest.(check bool) "absent" true (Lectionary.find l (slug "ef-advent-sunday-1") = None) let test_duplicate_slug_rejected () = let e = entry "ef-advent-sunday-1" [ cit Citation.First "Rom 13:11-14" ] in match Lectionary.of_entries [ e; e ] with | Ok _ -> Alcotest.fail "duplicate slug must be rejected" | Error msg -> (* Strengthened beyond the brief's "non-empty and contains 'e'" check: the interface doc promises the error names the slug, so assert the offending slug string actually appears in the message. *) Alcotest.(check bool) "names the slug" true (contains_substring ~needle:"ef-advent-sunday-1" msg) let test_sexp_round_trip () = let l = ok_exn (Lectionary.of_entries [ entry "ef-lent-1-monday" [ cit Citation.First "Ezech 34:11-16"; cit Citation.Gospel "Matt 25:31-46" ] ]) in let l' = Lectionary.t_of_sexp (Lectionary.sexp_of_t l) in Alcotest.(check bool) "round trips" true (l = l') let with_temp_file contents f = let path = Filename.temp_file "lectionary_test" ".sexp" in let oc = open_out path in output_string oc contents; close_out oc; Fun.protect ~finally:(fun () -> Sys.remove path) (fun () -> f path) let load_is_error contents what = with_temp_file contents (fun path -> match Lectionary.load path with | Error _ -> () | Ok _ -> Alcotest.fail (what ^ ": expected Error, got Ok")) let test_load_never_raises () = load_is_error "((ef-advent-sunday-1 (" "unterminated list"; load_is_error "((ef-advent-sunday-1 ((part First) (reference \"x" "unterminated string"; load_is_error "" "empty file"; load_is_error "() ()" "more than one sexp"; load_is_error "not-a-list" "wrong shape"; load_is_error "((\"NOT A SLUG\" ()))" "invalid slug"; match Lectionary.load "/nonexistent/path/lectionary.sexp" with | Error _ -> () | Ok _ -> Alcotest.fail "missing file: expected Error, got Ok" let test_ef_data_file_loads () = match Lectionary.load "../data/ef/lectionary.sexp" with | Error e -> Alcotest.fail e | Ok l -> (* 119 lectio ini sections, translated/widened/renamed into colitur's own Temporal_ef vocabulary, PLUS hand-authored/derived entries (tools/bootstrap_lectionary.ml's own [colitur_keys]/ [vigil_entries]/[holy_week_entries]/[nativity_octave_entries]/ [derived_entries] comments have the full account, task 8, branch ef-lectionary, fix round 1): "ef-christmas-sunday-0" widens into itself PLUS "ef-holy-name-sunday" (+1); the six Passiontide/Lent- Ember sections translate/rename 1:1 (no count change: Passiontide no longer widens into two colitur keys since fix round 1's Critical 1 correction, and the three Lent Ember sections rename rather than widen); one hand-authored entry sourced from lectio's SANCTORAL calendar, not this lectionary ini ([ef-nativity-vigil], +1); four hand-authored directly from the Missal, Holy Week (+4); three hand-authored directly from the Missal, the fixed Nativity-Octave days (+3); one entry derived from an already-translated one rather than re-typed ([ef-holy-name], +1). 119 + 1 + 1 + 4 + 3 + 1 = 129. *) Alcotest.(check int) "entry count" 129 (List.length (Lectionary.entries l)); (match Lectionary.find l (slug "ef-lent-1-monday") with | Some [ a; b ] -> Alcotest.(check string) "epistle" "Ezech 34:11-16" a.Citation.reference; Alcotest.(check string) "gospel" "Matt 25:31-46" b.Citation.reference | Some _ -> Alcotest.fail "expected exactly two citations" | None -> Alcotest.fail "ef-lent-1-monday missing"); (* Task 8's own renamed/derived/hand-authored keys, spot-checked here so a future regeneration that silently drops one of them fails loudly and locally, not only via the much bigger differential suite. *) let check_entry name first gospel = match Lectionary.find l (slug name) with | Some [ a; b ] -> Alcotest.(check string) (name ^ ": epistle") first a.Citation.reference; Alcotest.(check string) (name ^ ": gospel") gospel b.Citation.reference | Some _ -> Alcotest.fail (name ^ ": expected exactly two citations") | None -> Alcotest.fail (name ^ ": missing") in check_entry "ef-holy-name-sunday" "Gal 4:1-7" "Luke 2:33-40"; check_entry "ef-christmas-sunday-0" "Gal 4:1-7" "Luke 2:33-40"; check_entry "ef-holy-name" "Gal 4:1-7" "Luke 2:33-40"; check_entry "ef-nativity-vigil" "Rom 1:1-6" "Matt 1:18-21"; check_entry "ef-pentecost-ember-wed" "Acts 5:12-16" "John 6:44-52."; check_entry "ef-pentecost-ember-fri" "Joel 2:23-24; 26-27" "Luke 5:17-26"; check_entry "ef-pentecost-ember-sat" "Rom 5:1-5." "Luke 4:38-44."; (* Critical 2 fix (coordinator review round 1): the Lenten Ember days, previously dead keys under lectio's own "ef-lent-1-" naming -- now reachable under colitur's own slugs. *) check_entry "ef-lent-ember-wed" "3 Kgs. 19:3-8" "Matt 12:38-50"; (* Passiontide widens into Passion week ONLY now (Critical 1 fix) -- "ef-passiontide-1-monday" keeps lectio's own citation; "ef-passiontide-2-monday" (Holy Monday) is Missal-sourced, DIFFERENT text, hand-authored in [holy_week_entries]. *) check_entry "ef-passiontide-1-monday" "Jonas 3:1-10" "John 7:32-39"; check_entry "ef-passiontide-2-monday" "Isai. 50, 5-10" "Io. 12, 1-9"; check_entry "ef-passiontide-2-tuesday" "Ier. 11, 18-20" "Mark 14, 32-72; 15, 1-46"; check_entry "ef-passiontide-2-thursday" "1 Cor. 11, 20-32" "Io. 13, 1-15"; check_entry "ef-passiontide-2-saturday" "Col. 3, 1-4" "Matt. 28, 1-7"; (* Important 3(a) fix (coordinator review round 1): the fixed Nativity-Octave days now carry their own direct Missal formulary ("Diebus infra octavam Nativitatis Domini"), not Holy Name Sunday's (a first guess, measured wrong, reverted -- see [nativity_octave_entries]'s own comment). *) check_entry "ef-nativity-octave-day-5" "Tit. 3, 4-7" "Luc. 2, 15-20"; check_entry "ef-nativity-octave-day-6" "Tit. 3, 4-7" "Luc. 2, 15-20"; check_entry "ef-nativity-octave-day-7" "Tit. 3, 4-7" "Luc. 2, 15-20"; (* These three MUST stay absent -- Holy Wednesday and Good Friday are deliberately not hand-authored (multi-lesson liturgies, no single Epistle slot; see [holy_week_entries]'s own comment for why), and Lent's OWN "ef-lent-1-wednesday/friday/saturday" keys must not reappear once renamed away (colitur never computes them as its own slug at all). A regression net for those decisions, not merely documentation of them. *) let check_absent name = match Lectionary.find l (slug name) with | None -> () | Some _ -> Alcotest.fail (name ^ ": must stay absent -- see bootstrap_lectionary.ml") in check_absent "ef-passiontide-2-wednesday"; check_absent "ef-passiontide-2-friday"; check_absent "ef-lent-1-wednesday" let suite = [ ("find present", `Quick, test_find_present); ("find absent", `Quick, test_find_absent); ("duplicate slug rejected", `Quick, test_duplicate_slug_rejected); ("sexp round-trip", `Quick, test_sexp_round_trip); ("load never raises", `Quick, test_load_never_raises); ("ef data file loads", `Quick, test_ef_data_file_loads) ]