aboutsummaryrefslogtreecommitdiff
path: root/test/test_lectionary.ml
blob: a95c98c9bab9eaf24c11f820dda35a5e96a92269 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
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) ]