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 -> Alcotest.(check int) "entry count" 119 (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") 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) ]