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 into colitur's own Temporal_ef vocabulary (tools/bootstrap_lectionary.ml's own [colitur_keys]/[vigil_entries]/[derived_entries] comments have the full account, task 8, branch ef-lectionary): Passiontide's 6 shared sections each widen into 2 colitur keys (+6); "ef-christmas- sunday-0" widens into itself PLUS "ef-holy-name-sunday" (+1, a fix round after the first version replaced rather than widened it and broke a pre-existing test); one hand-authored entry sourced from lectio's SANCTORAL calendar, not this lectionary ini ([ef-nativity-vigil], +1); one entry derived from an already-translated one rather than re-typed ([ef-holy-name], +1). 119 + 6 + 1 + 1 + 1 = 128. *) Alcotest.(check int) "entry count" 128 (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 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."; check_entry "ef-passiontide-1-monday" "Jonas 3:1-10" "John 7:32-39"; check_entry "ef-passiontide-2-monday" "Jonas 3:1-10" "John 7:32-39"; (* This one MUST be absent: rejected and reverted in the generator's own [derived_entries] comment -- a regression-net for that decision, not merely documentation of it. *) (match Lectionary.find l (slug "ef-nativity-octave-day-6") with | None -> () | Some _ -> Alcotest.fail "ef-nativity-octave-day-6: must stay absent -- see bootstrap_lectionary.ml's own \ derived_entries comment for why a direct entry here was tried and reverted") 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) ]