summaryrefslogtreecommitdiff
path: root/test
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-08-11 12:27:24 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-08-11 12:27:24 +0200
commite58cf9920254d7312325cf382b5be24efc175749 (patch)
tree560592ded5d4e11514ebffc97dd4b530039177bb /test
parent2e3697b5ce7338568a67531d07a5528f0de1da22 (diff)
downloadcolitur-e58cf9920254d7312325cf382b5be24efc175749.tar.gz
colitur-e58cf9920254d7312325cf382b5be24efc175749.zip
test(layer): cover load's failure modes and same-date index ordering
load previously had zero coverage despite being the module's only fallible- I/O function and the one place the brief's literal code would have let an exception escape on this sexplib version (Failure instead of Parse_error). Add cases for a missing file, a malformed/unterminated sexp, an invalid slug embedded in an otherwise well-formed layer, and a success round-trip. Also add a same-date index case with three entries inserted out of slug order, pinning that the by-date index accumulates rather than overwrites and returns buckets in canonical order regardless of insertion order -- the previous two-entry fixture only ever touched bucket sizes 0 and 1.
Diffstat (limited to 'test')
-rw-r--r--test/test_overlay.ml81
1 files changed, 80 insertions, 1 deletions
diff --git a/test/test_overlay.ml b/test/test_overlay.ml
index ece4069..b3a8f26 100644
--- a/test/test_overlay.ml
+++ b/test/test_overlay.ml
@@ -19,6 +19,24 @@ let base () =
L.of_entries ~id:"base" ~name:"Test base"
[ entry ~month:1 ~day:5 "telesphorus"; entry ~month:1 ~day:14 "hilary" ]
+(* First occurrence only -- used to corrupt one field (the slug atom) of an
+ otherwise well-formed serialised layer, without hand-writing the whole
+ sexp shape by hand. *)
+let replace_first ~sub ~by s =
+ let sub_len = String.length sub and s_len = String.length s in
+ let rec find i =
+ if i + sub_len > s_len then None
+ else if String.sub s i sub_len = sub then Some i
+ else find (i + 1)
+ in
+ match find 0 with
+ | None -> s
+ | Some i -> String.sub s 0 i ^ by ^ String.sub s (i + sub_len) (s_len - i - sub_len)
+
+let with_temp_file f =
+ let path = Filename.temp_file "colitur_layer" ".sexp" in
+ Fun.protect ~finally:(fun () -> try Sys.remove path with Sys_error _ -> ()) (fun () -> f path)
+
let test_layer_basics () =
let l = base () in
Alcotest.(check bool) "find present" true (L.find l (slug "hilary") <> None);
@@ -33,13 +51,74 @@ let test_layer_index () =
Alcotest.(check int) "Jan 14 has one" 1 (List.length (L.on_date idx ~month:1 ~day:14));
Alcotest.(check int) "Jan 20 has none" 0 (List.length (L.on_date idx ~month:1 ~day:20))
+(* Three entries sharing a date, inserted deliberately out of canonical
+ (by-slug) order: this is what actually pins accumulation (the bucket must
+ hold all three, not overwrite) and deterministic ordering (the bucket must
+ come back sorted regardless of insertion order). Entries already inserted
+ in sorted order would pass even with the sort dropped. *)
+let test_layer_index_same_date () =
+ let l =
+ L.of_entries ~id:"base" ~name:"Test same-date"
+ [ entry ~month:3 ~day:17 "zeta"; entry ~month:3 ~day:17 "alpha"; entry ~month:3 ~day:17 "mu" ]
+ in
+ let idx = L.index_by_date l in
+ let bucket = L.on_date idx ~month:3 ~day:17 in
+ Alcotest.(check int) "three entries share Mar 17" 3 (List.length bucket);
+ Alcotest.(check (list string)) "bucket sorted by slug" [ "alpha"; "mu"; "zeta" ]
+ (List.map (fun e -> S.to_string e.L.cel.Cel.slug) bucket)
+
let test_layer_sexp () =
let l = base () in
let sexp = L.sexp_of_t sexp_of_rank l in
Alcotest.(check bool) "roundtrip" true (L.t_of_sexp rank_of_sexp sexp = l)
+let test_layer_load_success () =
+ let l = base () in
+ with_temp_file (fun path ->
+ let oc = open_out path in
+ output_string oc (Sexplib.Sexp.to_string (L.sexp_of_t sexp_of_rank l));
+ close_out oc;
+ match L.load rank_of_sexp path with
+ | Ok loaded -> Alcotest.(check bool) "round-trip equal" true (loaded = l)
+ | Error msg -> Alcotest.failf "expected Ok, got Error %s" msg)
+
+let test_layer_load_missing_file () =
+ let path = Filename.temp_file "colitur_layer_missing" ".sexp" in
+ Sys.remove path;
+ match L.load rank_of_sexp path with
+ | Ok _ -> Alcotest.fail "expected Error for a missing file"
+ | Error _ -> ()
+
+let test_layer_load_malformed () =
+ with_temp_file (fun path ->
+ let oc = open_out path in
+ (* unterminated list -- sexplib raises Failure here, not Parse_error *)
+ output_string oc "((id \"x\") (name \"y\") (entries (";
+ close_out oc;
+ match L.load rank_of_sexp path with
+ | Ok _ -> Alcotest.fail "expected Error for a malformed sexp"
+ | Error _ -> ())
+
+let test_layer_load_invalid_slug () =
+ let single = L.of_entries ~id:"base" ~name:"Test" [ entry ~month:2 ~day:2 "hilary" ] in
+ let text = Sexplib.Sexp.to_string (L.sexp_of_t sexp_of_rank single) in
+ let corrupted = replace_first ~sub:"hilary" ~by:"Bad_Slug!" text in
+ with_temp_file (fun path ->
+ let oc = open_out path in
+ output_string oc corrupted;
+ close_out oc;
+ match L.load rank_of_sexp path with
+ | Ok _ -> Alcotest.fail "expected Error for an invalid slug"
+ | Error _ -> ())
+
let suite =
( "Layer/Overlay",
[ Alcotest.test_case "layer basics" `Quick test_layer_basics;
Alcotest.test_case "layer by-date index" `Quick test_layer_index;
- Alcotest.test_case "layer sexp" `Quick test_layer_sexp ] )
+ Alcotest.test_case "layer by-date index accumulates same date" `Quick
+ test_layer_index_same_date;
+ Alcotest.test_case "layer sexp" `Quick test_layer_sexp;
+ Alcotest.test_case "layer load success round-trip" `Quick test_layer_load_success;
+ Alcotest.test_case "layer load missing file" `Quick test_layer_load_missing_file;
+ Alcotest.test_case "layer load malformed sexp" `Quick test_layer_load_malformed;
+ Alcotest.test_case "layer load invalid slug" `Quick test_layer_load_invalid_slug ] )