aboutsummaryrefslogtreecommitdiff
path: root/test/test_citation.ml
diff options
context:
space:
mode:
Diffstat (limited to 'test/test_citation.ml')
-rw-r--r--test/test_citation.ml113
1 files changed, 109 insertions, 4 deletions
diff --git a/test/test_citation.ml b/test/test_citation.ml
index 0694d8b..7bc8ac7 100644
--- a/test/test_citation.ml
+++ b/test/test_citation.ml
@@ -3,8 +3,11 @@ module B = Colitur_citation.Book
let s = Alcotest.string
let test_both_spellings_are_one_book () =
- (* The seven inherited duplicate spellings must collapse. This is the
- whole reason the parser exists rather than a regex. *)
+ (* The books listed here arrive in more than one spelling and must
+ collapse onto one id. This is the whole reason the parser exists rather
+ than a regex. Most pairs are dotted/undotted; "Sir"/"Ecclus" and
+ "Apoc"/"Rev" are a modern spelling sitting inside otherwise-Vulgate
+ data -- see book.mli. *)
let same a b =
match B.of_token a, B.of_token b with
| Some x, Some y ->
@@ -17,7 +20,16 @@ let test_both_spellings_are_one_book () =
same "1 Pet" "1 Pet.";
same "1 Thess" "1 Thess.";
same "Eph" "Eph.";
- same "3 Kgs." "3 Kings"
+ same "3 Kgs." "3 Kings";
+ same "Sir" "Ecclus";
+ same "Apoc" "Rev";
+ same "2 Cor" "2 Cor.";
+ same "Col" "Col.";
+ same "Wis" "Wis.";
+ same "2 Tim" "2 Tim.";
+ same "Ex" "Exod";
+ same "Ezech" "Ezek";
+ same "Jas" "James"
let test_unknown_token_is_none () =
Alcotest.(check bool) "not a book" true (B.of_token "Nonesuch" = None)
@@ -35,9 +47,102 @@ let test_modern_renumbers () =
| Some k ->
Alcotest.(check s) "renumbered" "kings_1" (B.to_string (B.map modern k))
+let test_tokens_has_no_duplicate_spelling () =
+ (* [of_token] resolves via [List.assoc_opt], which silently prefers the
+ first match on a duplicate key. A copy-paste collision in the table
+ would therefore mis-map a book in total silence, never an exception --
+ assert the invariant directly rather than trust it by inspection. *)
+ let spellings = List.map fst B.tokens in
+ let sorted = List.sort compare spellings in
+ let rec find_dup = function
+ | a :: (b :: _ as rest) -> if a = b then Some a else find_dup rest
+ | _ -> None
+ in
+ match find_dup sorted with
+ | None -> ()
+ | Some dup -> Alcotest.failf "duplicate spelling in Book.tokens: %S" dup
+
+(* Read a whole file into a string. Test-only I/O; the library itself never
+ touches the filesystem. *)
+let read_file path =
+ let ic = open_in_bin path in
+ let n = in_channel_length ic in
+ let content = really_input_string ic n in
+ close_in ic;
+ content
+
+let starts_with_at content pos prefix =
+ let plen = String.length prefix in
+ pos + plen <= String.length content && String.sub content pos plen = prefix
+
+(* Every [(reference ...)] payload in a data file, in file order. No
+ [Str]/regex -- a plain forward scan for the marker, then read to the
+ closing quote. Mirrors the coordinator's own survey command (grep -oh
+ over the reference marker, quote-delimited). *)
+let references content =
+ let marker = "(reference \"" in
+ let mlen = String.length marker in
+ let len = String.length content in
+ let rec loop pos acc =
+ if pos >= len then List.rev acc
+ else if starts_with_at content pos marker then
+ let start = pos + mlen in
+ match String.index_from_opt content start '"' with
+ | None -> List.rev acc
+ | Some close ->
+ let payload = String.sub content start (close - start) in
+ loop (close + 1) (payload :: acc)
+ else loop (pos + 1) acc
+ in
+ loop 0 []
+
+let is_alpha c = (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')
+let is_one_to_four c = c >= '1' && c <= '4'
+
+(* The leading book token of one reference payload, e.g. ["2 Tim 4:1-8"] ->
+ ["2 Tim"], ["Wis. 5:1-5"] -> ["Wis."]. Mirrors the coordinator's own
+ survey command's second stage:
+ `sed -E 's/^(([1-4] )?[A-Za-z]+\.?).*/\1/'`. *)
+let book_token r =
+ let len = String.length r in
+ let start = if len >= 2 && is_one_to_four r.[0] && r.[1] = ' ' then 2 else 0 in
+ let i = ref start in
+ while !i < len && is_alpha r.[!i] do
+ incr i
+ done;
+ let stop = if !i < len && r.[!i] = '.' then !i + 1 else !i in
+ String.sub r 0 stop
+
+let test_every_data_file_token_resolves () =
+ (* The check whose absence caused fix round 1: the brief surveyed only
+ the lectionary and missed 21 tokens, several common, living in
+ sanctoral.sexp and commons.sexp. Read all three files at test time and
+ re-derive the token set from them, rather than hardcoding a list, so
+ this keeps working when the data changes. *)
+ let files =
+ [ "../data/ef/lectionary.sexp"; "../data/ef/sanctoral.sexp"; "../data/ef/commons.sexp" ]
+ in
+ let tokens =
+ files
+ |> List.concat_map (fun f -> references (read_file f))
+ |> List.map book_token
+ |> List.sort_uniq compare
+ in
+ Alcotest.(check bool) "at least one token found" true (List.length tokens > 0);
+ let unresolved = List.filter (fun t -> B.of_token t = None) tokens in
+ match unresolved with
+ | [] -> ()
+ | _ ->
+ Alcotest.failf "unresolved book tokens in shipped data: %s"
+ (String.concat ", " unresolved)
+
let suite =
( "book",
[ Alcotest.test_case "both spellings one book" `Quick test_both_spellings_are_one_book;
Alcotest.test_case "unknown token" `Quick test_unknown_token_is_none;
Alcotest.test_case "vulgate identity" `Quick test_vulgate_is_identity;
- Alcotest.test_case "modern renumbers" `Quick test_modern_renumbers ] )
+ Alcotest.test_case "modern renumbers" `Quick test_modern_renumbers;
+ Alcotest.test_case "tokens has no duplicate spelling" `Quick
+ test_tokens_has_no_duplicate_spelling;
+ Alcotest.test_case "every data file token resolves" `Quick
+ test_every_data_file_token_resolves ] )