module B = Colitur_citation.Book let s = Alcotest.string let test_both_spellings_are_one_book () = (* 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 -> Alcotest.(check s) (a ^ " = " ^ b) (B.to_string x) (B.to_string y) | _ -> Alcotest.failf "%s or %s did not resolve" a b in same "Isa" "Isa."; same "Matt" "Matt."; same "1 Cor" "1 Cor."; same "1 Pet" "1 Pet."; same "1 Thess" "1 Thess."; same "Eph" "Eph."; 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) let test_vulgate_is_identity () = match B.of_token "3 Kings" with | None -> Alcotest.fail "3 Kings did not resolve" | Some k -> Alcotest.(check s) "unmapped" "kings_3" (B.to_string (B.map B.vulgate k)) let test_modern_renumbers () = let modern = B.tradition_of_fields [ ("kings_3", "kings_1") ] in match B.of_token "3 Kings" with | None -> Alcotest.fail "3 Kings did not resolve" | 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 "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 ] )