TOML 1.1 codecs for OCaml
at main 1538 lines 51 kB view raw
1(* Comprehensive tests for Tomlt codecs *) 2 3open Tomlt 4 5(* Helper to encode TOML to string via writer *) 6let toml_to_string value = 7 let buf = Buffer.create 256 in 8 Tomlt_bytesrw.to_writer (Bytesrw.Bytes.Writer.of_buffer buf) value; 9 Buffer.contents buf 10 11(* ============================================================================ 12 Test Helpers 13 ============================================================================ *) 14 15(* Decode a value from "value = X" TOML *) 16let check_decode_ok name codec input expected = 17 let toml = Tomlt_bytesrw.parse input in 18 let value = Toml.get "value" toml in 19 let actual = decode codec value in 20 match actual with 21 | Ok v when v = expected -> () 22 | Ok _ -> Alcotest.failf "%s: decode returned unexpected value" name 23 | Error e -> 24 Alcotest.failf "%s: decode failed: %s" name (Toml.Error.to_string e) 25 26(* Check that decode fails *) 27let check_decode_error name codec input = 28 let toml = Tomlt_bytesrw.parse input in 29 let value = Toml.get "value" toml in 30 match decode codec value with 31 | Error _ -> () 32 | Ok _ -> Alcotest.failf "%s: expected decode error but succeeded" name 33 34(* Decode from a table (for table codecs) *) 35let check_decode_table_ok name codec input expected = 36 let toml = Tomlt_bytesrw.parse input in 37 let value = Toml.get "value" toml in 38 let actual = decode codec value in 39 match actual with 40 | Ok v when v = expected -> () 41 | Ok _ -> Alcotest.failf "%s: decode returned unexpected value" name 42 | Error e -> 43 Alcotest.failf "%s: decode failed: %s" name (Toml.Error.to_string e) 44 45(* Check table decode error *) 46let check_decode_table_error name codec input = 47 let toml = Tomlt_bytesrw.parse input in 48 let value = Toml.get "value" toml in 49 match decode codec value with 50 | Error _ -> () 51 | Ok _ -> Alcotest.failf "%s: expected decode error but succeeded" name 52 53(* Roundtrip test *) 54let check_roundtrip name codec value = 55 let toml = encode codec value in 56 match decode codec toml with 57 | Ok v when v = value -> () 58 | Ok _ -> Alcotest.failf "%s: roundtrip mismatch, got different value" name 59 | Error e -> 60 Alcotest.failf "%s: roundtrip decode failed: %s" name 61 (Toml.Error.to_string e) 62 63(* ============================================================================ 64 Datetime Type Tests 65 ============================================================================ *) 66 67(* ---- Tz tests ---- *) 68 69let test_tz_utc () = 70 Alcotest.(check string) "utc to_string" "Z" (Tz.to_string Tz.utc); 71 Alcotest.(check bool) "utc equal" true (Tz.equal Tz.utc Tz.utc); 72 match Tz.of_string "Z" with 73 | Ok tz -> Alcotest.(check bool) "parse Z" true (Tz.equal tz Tz.utc) 74 | Error e -> Alcotest.failf "failed to parse Z: %s" e 75 76let test_tz_offset () = 77 let tz_pos = Tz.offset ~hours:5 ~minutes:30 in 78 Alcotest.(check string) "positive offset" "+05:30" (Tz.to_string tz_pos); 79 80 let tz_neg = Tz.offset ~hours:(-8) ~minutes:0 in 81 Alcotest.(check string) "negative offset" "-08:00" (Tz.to_string tz_neg); 82 83 let tz_zero = Tz.offset ~hours:0 ~minutes:0 in 84 Alcotest.(check string) "zero offset" "+00:00" (Tz.to_string tz_zero) 85 86let test_tz_parse () = 87 (match Tz.of_string "+05:30" with 88 | Ok tz -> Alcotest.(check string) "parse +05:30" "+05:30" (Tz.to_string tz) 89 | Error e -> Alcotest.failf "failed to parse +05:30: %s" e); 90 91 (match Tz.of_string "-08:00" with 92 | Ok tz -> Alcotest.(check string) "parse -08:00" "-08:00" (Tz.to_string tz) 93 | Error e -> Alcotest.failf "failed to parse -08:00: %s" e); 94 95 match Tz.of_string "z" with 96 | Ok tz -> Alcotest.(check bool) "parse lowercase z" true (Tz.equal tz Tz.utc) 97 | Error e -> Alcotest.failf "failed to parse z: %s" e 98 99let test_tz_compare () = 100 let tz1 = Tz.offset ~hours:5 ~minutes:0 in 101 let tz2 = Tz.offset ~hours:6 ~minutes:0 in 102 Alcotest.(check int) "compare less" (-1) (Int.compare (Tz.compare tz1 tz2) 0); 103 Alcotest.(check int) "compare greater" 1 (Int.compare (Tz.compare tz2 tz1) 0); 104 Alcotest.(check int) "compare equal" 0 (Tz.compare tz1 tz1); 105 Alcotest.(check int) 106 "utc < offset" (-1) 107 (Int.compare (Tz.compare Tz.utc tz1) 0) 108 109(* ---- Date tests ---- *) 110 111let test_date_basic () = 112 let d = Date.make ~year:2024 ~month:6 ~day:15 in 113 Alcotest.(check string) "to_string" "2024-06-15" (Date.to_string d); 114 Alcotest.(check int) "year" 2024 d.year; 115 Alcotest.(check int) "month" 6 d.month; 116 Alcotest.(check int) "day" 15 d.day 117 118let test_date_equal () = 119 let d1 = Date.make ~year:2024 ~month:6 ~day:15 in 120 let d2 = Date.make ~year:2024 ~month:6 ~day:15 in 121 let d3 = Date.make ~year:2024 ~month:6 ~day:16 in 122 Alcotest.(check bool) "equal same" true (Date.equal d1 d2); 123 Alcotest.(check bool) "not equal diff day" false (Date.equal d1 d3) 124 125let test_date_compare () = 126 let d1 = Date.make ~year:2024 ~month:6 ~day:15 in 127 let d2 = Date.make ~year:2024 ~month:6 ~day:16 in 128 let d3 = Date.make ~year:2024 ~month:7 ~day:1 in 129 let d4 = Date.make ~year:2025 ~month:1 ~day:1 in 130 Alcotest.(check int) "compare day" (-1) (Int.compare (Date.compare d1 d2) 0); 131 Alcotest.(check int) "compare month" (-1) (Int.compare (Date.compare d1 d3) 0); 132 Alcotest.(check int) "compare year" (-1) (Int.compare (Date.compare d1 d4) 0) 133 134let test_date_parse () = 135 (match Date.of_string "2024-06-15" with 136 | Ok d -> 137 Alcotest.(check int) "year" 2024 d.year; 138 Alcotest.(check int) "month" 6 d.month; 139 Alcotest.(check int) "day" 15 d.day 140 | Error e -> Alcotest.failf "parse failed: %s" e); 141 142 match Date.of_string "1979-05-27" with 143 | Ok d -> Alcotest.(check string) "roundtrip" "1979-05-27" (Date.to_string d) 144 | Error e -> Alcotest.failf "parse failed: %s" e 145 146let test_date_edge_cases () = 147 (* First day of year *) 148 let d1 = Date.make ~year:2024 ~month:1 ~day:1 in 149 Alcotest.(check string) "jan 1" "2024-01-01" (Date.to_string d1); 150 151 (* Last day of year *) 152 let d2 = Date.make ~year:2024 ~month:12 ~day:31 in 153 Alcotest.(check string) "dec 31" "2024-12-31" (Date.to_string d2); 154 155 (* Leading zeros in year *) 156 let d3 = Date.make ~year:99 ~month:1 ~day:1 in 157 Alcotest.(check string) "year 99" "0099-01-01" (Date.to_string d3) 158 159(* ---- Time tests ---- *) 160 161let test_time_basic () = 162 let t = Time.make ~hour:14 ~minute:30 ~second:45 () in 163 Alcotest.(check string) "to_string" "14:30:45" (Time.to_string t); 164 Alcotest.(check int) "hour" 14 t.hour; 165 Alcotest.(check int) "minute" 30 t.minute; 166 Alcotest.(check int) "second" 45 t.second; 167 Alcotest.(check (float 0.001)) "frac" 0.0 t.frac 168 169let test_time_fractional () = 170 let t1 = Time.make ~hour:14 ~minute:30 ~second:45 ~frac:0.123 () in 171 Alcotest.(check string) "frac 3 digits" "14:30:45.123" (Time.to_string t1); 172 173 let t2 = Time.make ~hour:0 ~minute:0 ~second:0 ~frac:0.123456789 () in 174 Alcotest.(check string) 175 "frac 9 digits" "00:00:00.123456789" (Time.to_string t2); 176 177 let t3 = Time.make ~hour:12 ~minute:0 ~second:0 ~frac:0.1 () in 178 Alcotest.(check string) "frac 1 digit" "12:00:00.1" (Time.to_string t3) 179 180let test_time_equal () = 181 let t1 = Time.make ~hour:14 ~minute:30 ~second:45 () in 182 let t2 = Time.make ~hour:14 ~minute:30 ~second:45 () in 183 let t3 = Time.make ~hour:14 ~minute:30 ~second:46 () in 184 Alcotest.(check bool) "equal same" true (Time.equal t1 t2); 185 Alcotest.(check bool) "not equal" false (Time.equal t1 t3) 186 187let test_time_compare () = 188 let t1 = Time.make ~hour:14 ~minute:30 ~second:45 () in 189 let t2 = Time.make ~hour:14 ~minute:30 ~second:46 () in 190 let t3 = Time.make ~hour:14 ~minute:31 ~second:0 () in 191 let t4 = Time.make ~hour:15 ~minute:0 ~second:0 () in 192 Alcotest.(check int) 193 "compare second" (-1) 194 (Int.compare (Time.compare t1 t2) 0); 195 Alcotest.(check int) 196 "compare minute" (-1) 197 (Int.compare (Time.compare t1 t3) 0); 198 Alcotest.(check int) "compare hour" (-1) (Int.compare (Time.compare t1 t4) 0) 199 200let test_time_parse () = 201 (match Time.of_string "14:30:45" with 202 | Ok t -> 203 Alcotest.(check int) "hour" 14 t.hour; 204 Alcotest.(check int) "minute" 30 t.minute; 205 Alcotest.(check int) "second" 45 t.second 206 | Error e -> Alcotest.failf "parse failed: %s" e); 207 208 match Time.of_string "00:00:00.123456" with 209 | Ok t -> Alcotest.(check (float 0.000001)) "frac" 0.123456 t.frac 210 | Error e -> Alcotest.failf "parse failed: %s" e 211 212let test_time_edge_cases () = 213 let t1 = Time.make ~hour:0 ~minute:0 ~second:0 () in 214 Alcotest.(check string) "midnight" "00:00:00" (Time.to_string t1); 215 216 let t2 = Time.make ~hour:23 ~minute:59 ~second:59 () in 217 Alcotest.(check string) "end of day" "23:59:59" (Time.to_string t2) 218 219(* ---- Datetime tests ---- *) 220 221let test_datetime_basic () = 222 let dt = 223 Datetime.make 224 ~date:(Date.make ~year:2024 ~month:6 ~day:15) 225 ~time:(Time.make ~hour:14 ~minute:30 ~second:0 ()) 226 ~tz:Tz.utc 227 in 228 Alcotest.(check string) 229 "to_string" "2024-06-15T14:30:00Z" (Datetime.to_string dt) 230 231let test_datetime_with_offset () = 232 let dt = 233 Datetime.make 234 ~date:(Date.make ~year:2024 ~month:6 ~day:15) 235 ~time:(Time.make ~hour:14 ~minute:30 ~second:0 ()) 236 ~tz:(Tz.offset ~hours:5 ~minutes:30) 237 in 238 Alcotest.(check string) 239 "with offset" "2024-06-15T14:30:00+05:30" (Datetime.to_string dt) 240 241let test_datetime_with_frac () = 242 let dt = 243 Datetime.make 244 ~date:(Date.make ~year:2024 ~month:6 ~day:15) 245 ~time:(Time.make ~hour:14 ~minute:30 ~second:0 ~frac:0.123456 ()) 246 ~tz:Tz.utc 247 in 248 Alcotest.(check string) 249 "with frac" "2024-06-15T14:30:00.123456Z" (Datetime.to_string dt) 250 251let test_datetime_parse () = 252 (match Datetime.of_string "2024-06-15T14:30:00Z" with 253 | Ok dt -> 254 Alcotest.(check int) "year" 2024 dt.date.year; 255 Alcotest.(check int) "hour" 14 dt.time.hour; 256 Alcotest.(check bool) "tz" true (Tz.equal dt.tz Tz.utc) 257 | Error e -> Alcotest.failf "parse failed: %s" e); 258 259 match Datetime.of_string "1979-05-27T07:32:00-08:00" with 260 | Ok dt -> 261 Alcotest.(check int) "year" 1979 dt.date.year; 262 Alcotest.(check string) "tz" "-08:00" (Tz.to_string dt.tz) 263 | Error e -> Alcotest.failf "parse failed: %s" e 264 265let test_datetime_equal_compare () = 266 let dt1 = 267 Datetime.make 268 ~date:(Date.make ~year:2024 ~month:6 ~day:15) 269 ~time:(Time.make ~hour:14 ~minute:30 ~second:0 ()) 270 ~tz:Tz.utc 271 in 272 let dt2 = 273 Datetime.make 274 ~date:(Date.make ~year:2024 ~month:6 ~day:15) 275 ~time:(Time.make ~hour:14 ~minute:30 ~second:0 ()) 276 ~tz:Tz.utc 277 in 278 let dt3 = 279 Datetime.make 280 ~date:(Date.make ~year:2024 ~month:6 ~day:16) 281 ~time:(Time.make ~hour:14 ~minute:30 ~second:0 ()) 282 ~tz:Tz.utc 283 in 284 Alcotest.(check bool) "equal same" true (Datetime.equal dt1 dt2); 285 Alcotest.(check bool) "not equal" false (Datetime.equal dt1 dt3); 286 Alcotest.(check int) "compare" (-1) (Int.compare (Datetime.compare dt1 dt3) 0) 287 288(* ---- Datetime_local tests ---- *) 289 290let test_datetime_local_basic () = 291 let dt = 292 Datetime_local.make 293 ~date:(Date.make ~year:2024 ~month:6 ~day:15) 294 ~time:(Time.make ~hour:14 ~minute:30 ~second:0 ()) 295 in 296 Alcotest.(check string) 297 "to_string" "2024-06-15T14:30:00" 298 (Datetime_local.to_string dt) 299 300let test_datetime_local_parse () = 301 match Datetime_local.of_string "2024-06-15T14:30:00" with 302 | Ok dt -> 303 Alcotest.(check int) "year" 2024 dt.date.year; 304 Alcotest.(check int) "hour" 14 dt.time.hour 305 | Error e -> Alcotest.failf "parse failed: %s" e 306 307let test_datetime_local_equal_compare () = 308 let dt1 = 309 Datetime_local.make 310 ~date:(Date.make ~year:2024 ~month:6 ~day:15) 311 ~time:(Time.make ~hour:14 ~minute:30 ~second:0 ()) 312 in 313 let dt2 = 314 Datetime_local.make 315 ~date:(Date.make ~year:2024 ~month:6 ~day:15) 316 ~time:(Time.make ~hour:14 ~minute:30 ~second:0 ()) 317 in 318 Alcotest.(check bool) "equal" true (Datetime_local.equal dt1 dt2); 319 Alcotest.(check int) "compare" 0 (Datetime_local.compare dt1 dt2) 320 321(* ============================================================================ 322 Base Codec Tests 323 ============================================================================ *) 324 325(* ---- Bool codec ---- *) 326 327let test_bool_codec () = 328 check_decode_ok "true" bool "value = true" true; 329 check_decode_ok "false" bool "value = false" false 330 331let test_bool_roundtrip () = 332 check_roundtrip "true roundtrip" bool true; 333 check_roundtrip "false roundtrip" bool false 334 335let test_bool_type_error () = 336 check_decode_error "string not bool" bool {|value = "true"|} 337 338(* ---- Int codec ---- *) 339 340let test_int_codec () = 341 check_decode_ok "positive" int "value = 42" 42; 342 check_decode_ok "negative" int "value = -17" (-17); 343 check_decode_ok "zero" int "value = 0" 0; 344 check_decode_ok "large" int "value = 1000000" 1000000 345 346let test_int_formats () = 347 check_decode_ok "hex" int "value = 0xDEADBEEF" 0xDEADBEEF; 348 check_decode_ok "octal" int "value = 0o755" 0o755; 349 check_decode_ok "binary" int "value = 0b11010110" 0b11010110; 350 check_decode_ok "underscore" int "value = 1_000_000" 1_000_000 351 352let test_int_roundtrip () = 353 check_roundtrip "positive" int 42; 354 check_roundtrip "negative" int (-17); 355 check_roundtrip "zero" int 0 356 357let test_int_type_error () = 358 check_decode_error "float not int" int "value = 3.14"; 359 check_decode_error "string not int" int {|value = "42"|} 360 361(* ---- Int32 codec ---- *) 362 363let test_int32_codec () = 364 check_decode_ok "positive" int32 "value = 42" 42l; 365 check_decode_ok "negative" int32 "value = -17" (-17l); 366 check_decode_ok "max" int32 "value = 2147483647" Int32.max_int; 367 check_decode_ok "min" int32 "value = -2147483648" Int32.min_int 368 369let test_int32_roundtrip () = 370 check_roundtrip "positive" int32 42l; 371 check_roundtrip "max" int32 Int32.max_int; 372 check_roundtrip "min" int32 Int32.min_int 373 374(* ---- Int64 codec ---- *) 375 376let test_int64_codec () = 377 check_decode_ok "positive" int64 "value = 42" 42L; 378 check_decode_ok "large" int64 "value = 9223372036854775807" Int64.max_int; 379 check_decode_ok "large neg" int64 "value = -9223372036854775808" Int64.min_int 380 381let test_int64_roundtrip () = 382 check_roundtrip "positive" int64 42L; 383 check_roundtrip "max" int64 Int64.max_int; 384 check_roundtrip "min" int64 Int64.min_int 385 386(* ---- Float codec ---- *) 387 388let test_float_codec () = 389 check_decode_ok "positive" float "value = 3.14" 3.14; 390 check_decode_ok "negative" float "value = -2.5" (-2.5); 391 check_decode_ok "zero" float "value = 0.0" 0.0; 392 check_decode_ok "exponent" float "value = 5e+22" 5e+22; 393 check_decode_ok "neg exponent" float "value = 1e-10" 1e-10 394 395let test_float_special () = 396 check_decode_ok "inf" float "value = inf" Float.infinity; 397 check_decode_ok "neg inf" float "value = -inf" Float.neg_infinity; 398 check_decode_ok "pos inf" float "value = +inf" Float.infinity; 399 (* nan requires special handling since nan <> nan *) 400 let toml = Tomlt_bytesrw.parse "value = nan" in 401 let value = Toml.get "value" toml in 402 match decode float value with 403 | Ok f when Float.is_nan f -> () 404 | Ok _ -> Alcotest.fail "expected nan" 405 | Error e -> Alcotest.failf "decode failed: %s" (Toml.Error.to_string e) 406 407let test_float_roundtrip () = 408 check_roundtrip "positive" float 3.14; 409 check_roundtrip "negative" float (-2.5); 410 check_roundtrip "zero" float 0.0 411 412let test_float_type_error () = 413 check_decode_error "int not float" float "value = 42"; 414 check_decode_error "string not float" float {|value = "3.14"|} 415 416(* ---- Number codec ---- *) 417 418let test_number_codec () = 419 check_decode_ok "float" number "value = 3.14" 3.14; 420 check_decode_ok "int as float" number "value = 42" 42.0; 421 check_decode_ok "negative int" number "value = -17" (-17.0) 422 423let test_number_type_error () = 424 check_decode_error "string not number" number {|value = "42"|} 425 426(* ---- String codec ---- *) 427 428let test_string_codec () = 429 check_decode_ok "basic" string {|value = "hello"|} "hello"; 430 check_decode_ok "empty" string {|value = ""|} ""; 431 check_decode_ok "unicode" string 432 {|value = "hello \u0048\u0065\u006C\u006C\u006F"|} "hello Hello" 433 434let test_string_escapes () = 435 check_decode_ok "newline" string {|value = "line1\nline2"|} "line1\nline2"; 436 check_decode_ok "tab" string {|value = "col1\tcol2"|} "col1\tcol2"; 437 check_decode_ok "quote" string {|value = "say \"hello\""|} {|say "hello"|}; 438 check_decode_ok "backslash" string {|value = "path\\to\\file"|} 439 "path\\to\\file" 440 441let test_string_multiline () = 442 check_decode_ok "multiline" string {|value = """ 443hello 444world"""|} 445 "hello\nworld"; 446 check_decode_ok "literal" string "value = 'C:\\path\\to\\file'" 447 "C:\\path\\to\\file" 448 449let test_string_roundtrip () = 450 check_roundtrip "basic" string "hello"; 451 check_roundtrip "empty" string ""; 452 check_roundtrip "unicode" string "Hello, \xE4\xB8\x96\xE7\x95\x8C!" 453 454let test_string_type_error () = 455 check_decode_error "int not string" string "value = 42"; 456 check_decode_error "bool not string" string "value = true" 457 458(* ============================================================================ 459 Ptime Codec Tests 460 ============================================================================ *) 461 462(* ---- Ptime codecs ---- *) 463 464let ptime_testable = 465 let pp fmt t = Fmt.pf fmt "%s" (Ptime.to_rfc3339 ~tz_offset_s:0 t) in 466 Alcotest.testable pp Ptime.equal 467 468let ptime_date_testable = 469 let pp fmt (y, m, d) = Fmt.pf fmt "%04d-%02d-%02d" y m d in 470 let eq (y1, m1, d1) (y2, m2, d2) = y1 = y2 && m1 = m2 && d1 = d2 in 471 Alcotest.testable pp eq 472 473let ptime_span_testable = 474 let pp fmt span = Fmt.pf fmt "%f" (Ptime.Span.to_float_s span) in 475 let eq a b = 476 Float.abs (Ptime.Span.to_float_s a -. Ptime.Span.to_float_s b) < 0.001 477 in 478 Alcotest.testable pp eq 479 480let test_ptime_codec () = 481 let input = "value = 2024-06-15T14:30:00Z" in 482 let expected = 483 match Ptime.of_date_time ((2024, 6, 15), ((14, 30, 0), 0)) with 484 | Some t -> t 485 | None -> failwith "invalid test datetime" 486 in 487 let toml = Tomlt_bytesrw.parse input in 488 let value = Toml.get "value" toml in 489 match decode (ptime ()) value with 490 | Ok v -> Alcotest.(check ptime_testable) "ptime" expected v 491 | Error e -> Alcotest.fail (Toml.Error.to_string e) 492 493let test_ptime_codec_offset () = 494 (* Test parsing datetime with offset and verify UTC conversion *) 495 let input = "value = 1979-05-27T00:32:00-07:00" in 496 (* UTC time should be 1979-05-27T07:32:00Z *) 497 let expected = 498 match Ptime.of_date_time ((1979, 5, 27), ((7, 32, 0), 0)) with 499 | Some t -> t 500 | None -> failwith "invalid test datetime" 501 in 502 let toml = Tomlt_bytesrw.parse input in 503 let value = Toml.get "value" toml in 504 match decode (ptime ()) value with 505 | Ok v -> Alcotest.(check ptime_testable) "ptime with offset" expected v 506 | Error e -> Alcotest.fail (Toml.Error.to_string e) 507 508let test_ptime_codec_roundtrip () = 509 let original = 510 match Ptime.of_date_time ((2024, 12, 19), ((15, 30, 45), 0)) with 511 | Some t -> t 512 | None -> failwith "invalid test datetime" 513 in 514 let toml = encode (ptime ()) original in 515 match decode (ptime ()) toml with 516 | Ok v -> Alcotest.(check ptime_testable) "roundtrip" original v 517 | Error e -> Alcotest.fail (Toml.Error.to_string e) 518 519let test_ptime_codec_optional_seconds () = 520 (* TOML 1.1 allows optional seconds *) 521 let input = "value = 1979-05-27T07:32Z" in 522 let expected = 523 match Ptime.of_date_time ((1979, 5, 27), ((7, 32, 0), 0)) with 524 | Some t -> t 525 | None -> failwith "invalid test datetime" 526 in 527 let toml = Tomlt_bytesrw.parse input in 528 let value = Toml.get "value" toml in 529 match decode (ptime ()) value with 530 | Ok v -> Alcotest.(check ptime_testable) "optional seconds" expected v 531 | Error e -> Alcotest.fail (Toml.Error.to_string e) 532 533let test_ptime_opt_codec () = 534 (* ptime_opt only accepts offset datetimes *) 535 let input = "value = 1979-05-27T07:32:00Z" in 536 let expected = 537 match Ptime.of_date_time ((1979, 5, 27), ((7, 32, 0), 0)) with 538 | Some t -> t 539 | None -> failwith "invalid test datetime" 540 in 541 let toml = Tomlt_bytesrw.parse input in 542 let value = Toml.get "value" toml in 543 match decode (ptime_opt ()) value with 544 | Ok t -> Alcotest.(check ptime_testable) "ptime_opt" expected t 545 | Error e -> Alcotest.fail (Toml.Error.to_string e) 546 547let test_ptime_opt_rejects_local () = 548 (* ptime_opt should reject local datetime *) 549 let input = "value = 1979-05-27T07:32:00" in 550 let toml = Tomlt_bytesrw.parse input in 551 let value = Toml.get "value" toml in 552 match decode (ptime_opt ()) value with 553 | Ok _ -> Alcotest.fail "expected error for local datetime" 554 | Error _ -> () 555 556let test_ptime_span_codec () = 557 let input = "value = 14:30:45" in 558 let expected = 559 match 560 Ptime.Span.of_float_s ((14.0 *. 3600.0) +. (30.0 *. 60.0) +. 45.0) 561 with 562 | Some s -> s 563 | None -> failwith "invalid span" 564 in 565 let toml = Tomlt_bytesrw.parse input in 566 let value = Toml.get "value" toml in 567 match decode ptime_span value with 568 | Ok span -> Alcotest.(check ptime_span_testable) "span" expected span 569 | Error e -> Alcotest.fail (Toml.Error.to_string e) 570 571let test_ptime_span_roundtrip () = 572 let original = 573 match Ptime.Span.of_float_s ((7.0 *. 3600.0) +. (32.0 *. 60.0)) with 574 | Some s -> s 575 | None -> failwith "invalid span" 576 in 577 let toml = encode ptime_span original in 578 match decode ptime_span toml with 579 | Ok v -> Alcotest.(check ptime_span_testable) "roundtrip" original v 580 | Error e -> Alcotest.fail (Toml.Error.to_string e) 581 582let test_ptime_date_codec () = 583 let input = "value = 1979-05-27" in 584 let toml = Tomlt_bytesrw.parse input in 585 let value = Toml.get "value" toml in 586 match decode ptime_date value with 587 | Ok date -> Alcotest.(check ptime_date_testable) "date" (1979, 5, 27) date 588 | Error e -> Alcotest.fail (Toml.Error.to_string e) 589 590let test_ptime_date_roundtrip () = 591 let original = (2024, 12, 19) in 592 let toml = encode ptime_date original in 593 match decode ptime_date toml with 594 | Ok v -> Alcotest.(check ptime_date_testable) "roundtrip" original v 595 | Error e -> Alcotest.fail (Toml.Error.to_string e) 596 597let test_ptime_local_datetime () = 598 (* The new ptime () codec accepts local datetime and uses provided tz *) 599 let input = "value = 1979-05-27T07:32:00" in 600 let expected = 601 match Ptime.of_date_time ((1979, 5, 27), ((7, 32, 0), 0)) with 602 | Some t -> t 603 | None -> failwith "invalid test datetime" 604 in 605 let toml = Tomlt_bytesrw.parse input in 606 let value = Toml.get "value" toml in 607 match decode (ptime ~tz_offset_s:0 ()) value with 608 | Ok v -> Alcotest.(check ptime_testable) "local datetime" expected v 609 | Error e -> Alcotest.fail (Toml.Error.to_string e) 610 611let test_ptime_date_as_ptime () = 612 (* The new ptime () codec accepts date and assumes midnight *) 613 let input = "value = 1979-05-27" in 614 let expected = 615 match Ptime.of_date_time ((1979, 5, 27), ((0, 0, 0), 0)) with 616 | Some t -> t 617 | None -> failwith "invalid test datetime" 618 in 619 let toml = Tomlt_bytesrw.parse input in 620 let value = Toml.get "value" toml in 621 match decode (ptime ~tz_offset_s:0 ()) value with 622 | Ok v -> Alcotest.(check ptime_testable) "date as ptime" expected v 623 | Error e -> Alcotest.fail (Toml.Error.to_string e) 624 625(* ---- Unified ptime_full codec ---- *) 626 627let ptime_full_testable = 628 Alcotest.testable Toml.pp_ptime_datetime (fun a b -> 629 match (a, b) with 630 | `Datetime (t1, tz1), `Datetime (t2, tz2) -> 631 Ptime.equal t1 t2 && tz1 = tz2 632 | `Datetime_local t1, `Datetime_local t2 -> Ptime.equal t1 t2 633 | `Date d1, `Date d2 -> d1 = d2 634 | `Time t1, `Time t2 -> t1 = t2 635 | _ -> false) 636 637let test_ptime_full_offset () = 638 let input = "value = 1979-05-27T07:32:00Z" in 639 let toml = Tomlt_bytesrw.parse input in 640 let value = Toml.get "value" toml in 641 match decode (ptime_full ()) value with 642 | Ok (`Datetime (ptime, Some 0)) -> 643 let expected = 644 match Ptime.of_date_time ((1979, 5, 27), ((7, 32, 0), 0)) with 645 | Some t -> t 646 | None -> failwith "invalid datetime" 647 in 648 Alcotest.(check ptime_testable) "ptime" expected ptime 649 | Ok other -> 650 Alcotest.failf "expected `Datetime, got %a" Toml.pp_ptime_datetime other 651 | Error e -> Alcotest.fail (Toml.Error.to_string e) 652 653let test_ptime_full_local_datetime () = 654 let input = "value = 1979-05-27T07:32:00" in 655 let toml = Tomlt_bytesrw.parse input in 656 let value = Toml.get "value" toml in 657 match decode (ptime_full ~tz_offset_s:0 ()) value with 658 | Ok (`Datetime_local ptime) -> 659 let expected = 660 match Ptime.of_date_time ((1979, 5, 27), ((7, 32, 0), 0)) with 661 | Some t -> t 662 | None -> failwith "invalid datetime" 663 in 664 Alcotest.(check ptime_testable) "ptime" expected ptime 665 | Ok other -> 666 Alcotest.failf "expected `Datetime_local, got %a" Toml.pp_ptime_datetime 667 other 668 | Error e -> Alcotest.fail (Toml.Error.to_string e) 669 670let test_ptime_full_date () = 671 let input = "value = 1979-05-27" in 672 let toml = Tomlt_bytesrw.parse input in 673 let value = Toml.get "value" toml in 674 match decode (ptime_full ()) value with 675 | Ok (`Date (y, m, d)) -> 676 Alcotest.(check int) "year" 1979 y; 677 Alcotest.(check int) "month" 5 m; 678 Alcotest.(check int) "day" 27 d 679 | Ok other -> 680 Alcotest.failf "expected `Date, got %a" Toml.pp_ptime_datetime other 681 | Error e -> Alcotest.fail (Toml.Error.to_string e) 682 683let test_ptime_full_time () = 684 let input = "value = 07:32:00" in 685 let toml = Tomlt_bytesrw.parse input in 686 let value = Toml.get "value" toml in 687 match decode (ptime_full ()) value with 688 | Ok (`Time (h, m, s, ns)) -> 689 Alcotest.(check int) "hour" 7 h; 690 Alcotest.(check int) "minute" 32 m; 691 Alcotest.(check int) "second" 0 s; 692 Alcotest.(check int) "nanoseconds" 0 ns 693 | Ok other -> 694 Alcotest.failf "expected `Time, got %a" Toml.pp_ptime_datetime other 695 | Error e -> Alcotest.fail (Toml.Error.to_string e) 696 697let test_ptime_full_roundtrip () = 698 let original : Toml.ptime_datetime = 699 `Datetime 700 ( (match Ptime.of_date_time ((1979, 5, 27), ((7, 32, 0), 0)) with 701 | Some t -> t 702 | None -> failwith "invalid datetime"), 703 Some 0 ) 704 in 705 let toml = encode (ptime_full ()) original in 706 match decode (ptime_full ()) toml with 707 | Ok result -> 708 Alcotest.(check ptime_full_testable) "roundtrip" original result 709 | Error e -> Alcotest.fail (Toml.Error.to_string e) 710 711(* ============================================================================ 712 Combinator Tests 713 ============================================================================ *) 714 715(* ---- Map combinator ---- *) 716 717let uppercase_string = 718 map string ~dec:String.uppercase_ascii ~enc:String.lowercase_ascii 719 720let test_map_combinator () = 721 check_decode_ok "uppercase" uppercase_string {|value = "hello"|} "HELLO" 722 723let test_map_roundtrip () = 724 check_roundtrip "map roundtrip" uppercase_string "HELLO" 725 726let doubled_int = map int ~dec:(fun x -> x * 2) ~enc:(fun x -> x / 2) 727 728let test_map_int () = 729 check_decode_ok "doubled" doubled_int "value = 21" 42; 730 check_roundtrip "doubled roundtrip" doubled_int 42 731 732(* ---- Const combinator ---- *) 733 734let test_const () = 735 let c = const "default_value" in 736 check_decode_ok "const ignores input" c "value = 42" "default_value"; 737 check_decode_ok "const ignores string" c {|value = "ignored"|} "default_value" 738 739(* ---- Enum combinator ---- *) 740 741type level = Debug | Info | Warn | Error 742 743let level_codec = 744 enum [ ("debug", Debug); ("info", Info); ("warn", Warn); ("error", Error) ] 745 746let test_enum () = 747 check_decode_ok "debug" level_codec {|value = "debug"|} Debug; 748 check_decode_ok "info" level_codec {|value = "info"|} Info; 749 check_decode_ok "warn" level_codec {|value = "warn"|} Warn; 750 check_decode_ok "error" level_codec {|value = "error"|} Error 751 752let test_enum_roundtrip () = 753 check_roundtrip "debug" level_codec Debug; 754 check_roundtrip "error" level_codec Error 755 756let test_enum_unknown () = 757 check_decode_error "unknown value" level_codec {|value = "trace"|} 758 759let test_enum_type_error () = 760 check_decode_error "not string" level_codec "value = 42" 761 762(* ---- Option combinator ---- *) 763 764let test_option_codec () = 765 let opt_int = option int in 766 check_decode_ok "some" opt_int "value = 42" (Some 42) 767 768let test_option_roundtrip () = 769 let opt_str = option string in 770 check_roundtrip "some string" opt_str (Some "hello") 771 772(* ---- Result combinator ---- *) 773 774let string_or_int_codec : (string, int) result t = result ~ok:string ~error:int 775 776let test_result_codec () = 777 check_decode_ok "ok string" string_or_int_codec {|value = "hello"|} 778 (Ok "hello"); 779 check_decode_ok "error int" string_or_int_codec "value = 42" (Error 42) 780 781let test_result_roundtrip () = 782 check_roundtrip "ok" string_or_int_codec (Ok "hello"); 783 check_roundtrip "error" string_or_int_codec (Error 42) 784 785(* ---- Recursive codec ---- *) 786 787(* Simple recursive structure for testing rec' *) 788type nested_list = { value : int; next : nested_list option } 789 790let rec nested_list_codec = 791 lazy 792 Table.( 793 obj (fun value next -> { value; next }) 794 |> mem "value" int ~enc:(fun n -> n.value) 795 |> opt_mem "next" (rec' nested_list_codec) ~enc:(fun n -> n.next) 796 |> finish) 797 798let test_recursive_codec () = 799 let input = 800 {| 801 [value] 802 value = 1 803 804 [value.next] 805 value = 2 806 807 [value.next.next] 808 value = 3 809 |} 810 in 811 let expected = 812 { 813 value = 1; 814 next = Some { value = 2; next = Some { value = 3; next = None } }; 815 } 816 in 817 check_decode_table_ok "nested list" (rec' nested_list_codec) input expected 818 819(* ============================================================================ 820 Array Codec Tests 821 ============================================================================ *) 822 823let test_list_codec () = 824 check_decode_ok "int list" (list int) "value = [1, 2, 3]" [ 1; 2; 3 ]; 825 check_decode_ok "empty list" (list int) "value = []" []; 826 check_decode_ok "string list" (list string) {|value = ["a", "b", "c"]|} 827 [ "a"; "b"; "c" ] 828 829let test_list_roundtrip () = 830 check_roundtrip "int list" (list int) [ 1; 2; 3 ]; 831 check_roundtrip "empty" (list int) []; 832 check_roundtrip "strings" (list string) [ "hello"; "world" ] 833 834let test_array_codec () = 835 check_decode_ok "int array" (array int) "value = [1, 2, 3]" [| 1; 2; 3 |]; 836 check_decode_ok "empty array" (array int) "value = []" [||] 837 838let test_array_roundtrip () = 839 check_roundtrip "int array" (array int) [| 1; 2; 3 |]; 840 check_roundtrip "empty" (array int) [||] 841 842let test_nested_list () = 843 let nested = list (list int) in 844 check_decode_ok "nested" nested "value = [[1, 2], [3, 4], [5]]" 845 [ [ 1; 2 ]; [ 3; 4 ]; [ 5 ] ]; 846 check_roundtrip "nested roundtrip" nested [ [ 1; 2 ]; [ 3; 4 ] ] 847 848let test_list_of_tables () = 849 let point_codec = 850 Table.( 851 obj (fun x y -> (x, y)) 852 |> mem "x" int ~enc:fst |> mem "y" int ~enc:snd |> finish) 853 in 854 let points_codec = list point_codec in 855 let input = {|value = [{x = 1, y = 2}, {x = 3, y = 4}]|} in 856 check_decode_ok "list of inline tables" points_codec input [ (1, 2); (3, 4) ] 857 858let test_list_type_error () = 859 check_decode_error "not array" (list int) "value = 42"; 860 check_decode_error "mixed types" (list int) {|value = [1, "two", 3]|} 861 862(* ============================================================================ 863 Table Codec Tests 864 ============================================================================ *) 865 866(* ---- Basic table ---- *) 867 868type point = { x : int; y : int } 869 870let point_codec = 871 Table.( 872 obj (fun x y -> { x; y }) 873 |> mem "x" int ~enc:(fun p -> p.x) 874 |> mem "y" int ~enc:(fun p -> p.y) 875 |> finish) 876 877let test_table_codec () = 878 let input = {| 879 [value] 880 x = 10 881 y = 20 882 |} in 883 check_decode_table_ok "point" point_codec input { x = 10; y = 20 } 884 885let test_table_roundtrip () = 886 check_roundtrip "point roundtrip" point_codec { x = 5; y = 15 } 887 888let test_table_missing_member () = 889 let input = {| 890 [value] 891 x = 10 892 |} in 893 check_decode_table_error "missing y" point_codec input 894 895let test_table_type_error () = 896 check_decode_error "not table" point_codec "value = 42" 897 898(* ---- Optional members ---- *) 899 900type config = { name : string; debug : bool; timeout : int option } 901 902let config_codec = 903 Table.( 904 obj (fun name debug timeout -> { name; debug; timeout }) 905 |> mem "name" string ~enc:(fun c -> c.name) 906 |> mem "debug" bool ~enc:(fun c -> c.debug) ~dec_absent:false 907 |> opt_mem "timeout" int ~enc:(fun c -> c.timeout) 908 |> finish) 909 910let test_optional_members () = 911 let input1 = 912 {| 913 [value] 914 name = "test" 915 debug = true 916 timeout = 30 917 |} 918 in 919 check_decode_table_ok "with all" config_codec input1 920 { name = "test"; debug = true; timeout = Some 30 }; 921 922 let input2 = {| 923 [value] 924 name = "test" 925 |} in 926 check_decode_table_ok "with defaults" config_codec input2 927 { name = "test"; debug = false; timeout = None } 928 929let test_optional_roundtrip () = 930 let c1 = { name = "app"; debug = true; timeout = Some 60 } in 931 check_roundtrip "with timeout" config_codec c1; 932 933 let c2 = { name = "app"; debug = false; timeout = None } in 934 check_roundtrip "without timeout" config_codec c2 935 936let test_opt_mem_omits_none () = 937 let c = { name = "app"; debug = false; timeout = None } in 938 let toml = encode config_codec c in 939 (* Just verify encoding doesn't crash *) 940 let _ = toml_to_string toml in 941 (* Verify None is not encoded *) 942 match Toml.opt "timeout" toml with 943 | None -> () 944 | Some _ -> Alcotest.fail "timeout should not be encoded when None" 945 946(* ---- enc_omit ---- *) 947 948type with_omit = { always : string; maybe : string } 949 950let with_omit_codec = 951 Table.( 952 obj (fun always maybe -> { always; maybe }) 953 |> mem "always" string ~enc:(fun r -> r.always) 954 |> mem "maybe" string 955 ~enc:(fun r -> r.maybe) 956 ~dec_absent:"" 957 ~enc_omit:(fun s -> String.length s = 0) 958 |> finish) 959 960let test_enc_omit () = 961 let r1 = { always = "hello"; maybe = "world" } in 962 let toml1 = encode with_omit_codec r1 in 963 (match Toml.opt "maybe" toml1 with 964 | Some _ -> () 965 | None -> Alcotest.fail "maybe should be encoded when non-empty"); 966 967 let r2 = { always = "hello"; maybe = "" } in 968 let toml2 = encode with_omit_codec r2 in 969 match Toml.opt "maybe" toml2 with 970 | None -> () 971 | Some _ -> Alcotest.fail "maybe should be omitted when empty" 972 973(* ---- Nested tables ---- *) 974 975type server = { host : string; port : int } 976type app_config = { title : string; server : server } 977 978let server_codec = 979 Table.( 980 obj (fun host port -> { host; port }) 981 |> mem "host" string ~enc:(fun s -> s.host) 982 |> mem "port" int ~enc:(fun s -> s.port) 983 |> finish) 984 985let app_config_codec = 986 Table.( 987 obj (fun title server -> { title; server }) 988 |> mem "title" string ~enc:(fun c -> c.title) 989 |> mem "server" server_codec ~enc:(fun c -> c.server) 990 |> finish) 991 992let test_nested_tables () = 993 let input = 994 {| 995 [value] 996 title = "My App" 997 998 [value.server] 999 host = "localhost" 1000 port = 8080 1001 |} 1002 in 1003 check_decode_table_ok "nested" app_config_codec input 1004 { title = "My App"; server = { host = "localhost"; port = 8080 } } 1005 1006let test_nested_roundtrip () = 1007 let config = 1008 { title = "Production"; server = { host = "0.0.0.0"; port = 443 } } 1009 in 1010 check_roundtrip "nested roundtrip" app_config_codec config 1011 1012(* ---- Deeply nested tables ---- *) 1013 1014type deep = { a : int; inner : deep option } 1015 1016let rec deep_codec = 1017 lazy 1018 Table.( 1019 obj (fun a inner -> { a; inner }) 1020 |> mem "a" int ~enc:(fun d -> d.a) 1021 |> opt_mem "inner" (rec' deep_codec) ~enc:(fun d -> d.inner) 1022 |> finish) 1023 1024let test_deeply_nested () = 1025 let input = 1026 {| 1027 [value] 1028 a = 1 1029 1030 [value.inner] 1031 a = 2 1032 1033 [value.inner.inner] 1034 a = 3 1035 |} 1036 in 1037 let expected = 1038 { a = 1; inner = Some { a = 2; inner = Some { a = 3; inner = None } } } 1039 in 1040 check_decode_table_ok "deep" (rec' deep_codec) input expected 1041 1042(* ---- Unknown member handling ---- *) 1043 1044type strict_config = { name : string } 1045 1046let strict_config_codec = 1047 Table.( 1048 obj (fun name -> { name }) 1049 |> mem "name" string ~enc:(fun c -> c.name) 1050 |> error_unknown |> finish) 1051 1052let test_error_unknown () = 1053 let input1 = {| 1054 [value] 1055 name = "test" 1056 |} in 1057 check_decode_table_ok "known only" strict_config_codec input1 1058 { name = "test" }; 1059 1060 (* error_unknown returns Error for unknown members *) 1061 let input2 = {| 1062 [value] 1063 name = "test" 1064 extra = 42 1065 |} in 1066 let toml = Tomlt_bytesrw.parse input2 in 1067 let value_toml = Toml.get "value" toml in 1068 match decode strict_config_codec value_toml with 1069 | Error _ -> () 1070 | Ok _ -> Alcotest.fail "expected error for unknown member" 1071 1072type extensible_config = { name : string; extras : (string * Toml.t) list } 1073 1074let extensible_config_codec = 1075 Table.( 1076 obj (fun name extras -> { name; extras }) 1077 |> mem "name" string ~enc:(fun c -> c.name) 1078 |> keep_unknown (Mems.assoc value) ~enc:(fun c -> c.extras) 1079 |> finish) 1080 1081let test_keep_unknown () = 1082 let input = 1083 {| 1084 [value] 1085 name = "test" 1086 extra1 = 42 1087 extra2 = "hello" 1088 |} 1089 in 1090 let toml = Tomlt_bytesrw.parse input in 1091 let value_toml = Toml.get "value" toml in 1092 match decode extensible_config_codec value_toml with 1093 | Ok c -> 1094 Alcotest.(check string) "name" "test" c.name; 1095 Alcotest.(check int) "extras count" 2 (List.length c.extras); 1096 (* Check extras contains the unknown members *) 1097 let has_extra1 = List.exists (fun (k, _) -> k = "extra1") c.extras in 1098 let has_extra2 = List.exists (fun (k, _) -> k = "extra2") c.extras in 1099 Alcotest.(check bool) "has extra1" true has_extra1; 1100 Alcotest.(check bool) "has extra2" true has_extra2 1101 | Error e -> Alcotest.failf "decode failed: %s" (Toml.Error.to_string e) 1102 1103let test_keep_unknown_roundtrip () = 1104 let c = 1105 { 1106 name = "test"; 1107 extras = [ ("custom", Toml.Int 42L); ("flag", Toml.Bool true) ]; 1108 } 1109 in 1110 check_roundtrip "keep_unknown roundtrip" extensible_config_codec c 1111 1112(* ---- Skip unknown (default) ---- *) 1113 1114type lenient_config = { lname : string } 1115 1116let lenient_codec = 1117 Table.( 1118 obj (fun lname -> { lname }) 1119 |> mem "name" string ~enc:(fun c -> c.lname) 1120 |> skip_unknown |> finish) 1121 1122let test_skip_unknown () = 1123 let input = 1124 {| 1125 [value] 1126 name = "test" 1127 ignored = 42 1128 also_ignored = "hello" 1129 |} 1130 in 1131 check_decode_table_ok "skip unknown" lenient_codec input { lname = "test" } 1132 1133(* ============================================================================ 1134 Array of Tables Tests 1135 ============================================================================ *) 1136 1137type product = { name : string; price : float } 1138 1139let product_codec = 1140 Table.( 1141 obj (fun name price -> { name; price }) 1142 |> mem "name" string ~enc:(fun p -> p.name) 1143 |> mem "price" float ~enc:(fun p -> p.price) 1144 |> finish) 1145 1146let test_array_of_tables () = 1147 let products_codec = array_of_tables product_codec in 1148 let input = 1149 {| 1150 [[value]] 1151 name = "Apple" 1152 price = 1.50 1153 1154 [[value]] 1155 name = "Banana" 1156 price = 0.75 1157 |} 1158 in 1159 let expected = 1160 [ { name = "Apple"; price = 1.50 }; { name = "Banana"; price = 0.75 } ] 1161 in 1162 check_decode_ok "products" products_codec input expected 1163 1164let test_array_of_tables_roundtrip () = 1165 let products_codec = array_of_tables product_codec in 1166 let products = 1167 [ { name = "Apple"; price = 1.50 }; { name = "Banana"; price = 0.75 } ] 1168 in 1169 check_roundtrip "products roundtrip" products_codec products 1170 1171let test_array_of_tables_empty () = 1172 let products_codec = array_of_tables product_codec in 1173 check_decode_ok "empty" products_codec "value = []" [] 1174 1175(* ============================================================================ 1176 Any/Value Codec Tests 1177 ============================================================================ *) 1178 1179let test_value_codec () = 1180 check_decode_ok "int" value "value = 42" (Toml.Int 42L); 1181 check_decode_ok "string" value {|value = "hello"|} (Toml.String "hello"); 1182 check_decode_ok "bool" value "value = true" (Toml.Bool true); 1183 check_decode_ok "float" value "value = 3.14" (Toml.Float 3.14); 1184 check_decode_ok "array" value "value = [1, 2, 3]" 1185 (Toml.Array [ Toml.Int 1L; Toml.Int 2L; Toml.Int 3L ]) 1186 1187let test_value_roundtrip () = 1188 check_roundtrip "int" value (Toml.Int 42L); 1189 check_roundtrip "string" value (Toml.String "hello"); 1190 check_roundtrip "bool" value (Toml.Bool true) 1191 1192let test_value_mems_codec () = 1193 let input = {| 1194 [value] 1195 a = 1 1196 b = "hello" 1197 c = true 1198 |} in 1199 let toml = Tomlt_bytesrw.parse input in 1200 let v = Toml.get "value" toml in 1201 match decode value_mems v with 1202 | Ok pairs -> 1203 Alcotest.(check int) "count" 3 (List.length pairs); 1204 let has_a = List.exists (fun (k, _) -> k = "a") pairs in 1205 let has_b = List.exists (fun (k, _) -> k = "b") pairs in 1206 let has_c = List.exists (fun (k, _) -> k = "c") pairs in 1207 Alcotest.(check bool) "has a" true has_a; 1208 Alcotest.(check bool) "has b" true has_b; 1209 Alcotest.(check bool) "has c" true has_c 1210 | Error e -> Alcotest.failf "decode failed: %s" (Toml.Error.to_string e) 1211 1212type string_or_int_any = String of string | Int of int 1213 1214let string_or_int_any_codec = 1215 any () 1216 ~dec_string:(map string ~dec:(fun s -> String s)) 1217 ~dec_int:(map int ~dec:(fun i -> Int i)) 1218 ~enc:(function 1219 | String _ -> map string ~enc:(function String s -> s | _ -> "") 1220 | Int _ -> map int ~enc:(function Int i -> i | _ -> 0)) 1221 1222let test_any_codec () = 1223 check_decode_ok "string" string_or_int_any_codec {|value = "hello"|} 1224 (String "hello"); 1225 check_decode_ok "int" string_or_int_any_codec "value = 42" (Int 42) 1226 1227let test_any_type_error () = 1228 check_decode_error "bool not handled" string_or_int_any_codec "value = true" 1229 1230(* ============================================================================ 1231 Encoding/Decoding Function Tests 1232 ============================================================================ *) 1233 1234let test_decode_string () = 1235 let toml_str = {|name = "test"|} in 1236 let codec = 1237 Table.(obj (fun name -> name) |> mem "name" string ~enc:Fun.id |> finish) 1238 in 1239 match Tomlt_bytesrw.decode_string codec toml_str with 1240 | Ok name -> Alcotest.(check string) "name" "test" name 1241 | Error e -> Alcotest.failf "decode failed: %s" (Toml.Error.to_string e) 1242 1243let test_decode_string_exn () = 1244 let toml_str = {|value = 42|} in 1245 let toml = Tomlt_bytesrw.parse toml_str in 1246 let v = Toml.get "value" toml in 1247 let result = decode_exn int v in 1248 Alcotest.(check int) "value" 42 result 1249 1250let test_encode_string () = 1251 let codec = 1252 Table.(obj (fun name -> name) |> mem "name" string ~enc:Fun.id |> finish) 1253 in 1254 let s = Tomlt_bytesrw.encode_string codec "test" in 1255 (* Just verify it produces valid TOML *) 1256 let _ = Tomlt_bytesrw.parse s in 1257 () 1258 1259(* ============================================================================ 1260 Edge Cases and Error Handling 1261 ============================================================================ *) 1262 1263let test_empty_table () = 1264 let empty_codec = Table.(obj () |> finish) in 1265 let input = "[value]" in 1266 check_decode_table_ok "empty table" empty_codec input () 1267 1268let test_unicode_keys () = 1269 let codec = 1270 Table.( 1271 obj (fun v -> v) 1272 |> mem "\xE4\xB8\xAD\xE6\x96\x87" string ~enc:Fun.id (* "中文" in UTF-8 *) 1273 |> finish) 1274 in 1275 let input = {| 1276 [value] 1277 "中文" = "hello" 1278 |} in 1279 check_decode_table_ok "unicode key" codec input "hello" 1280 1281let test_special_string_values () = 1282 check_decode_ok "empty" string {|value = ""|} ""; 1283 check_decode_ok "spaces" string {|value = " "|} " "; 1284 check_decode_ok "newlines" string {|value = "a\nb\nc"|} "a\nb\nc" 1285 1286let test_large_integers () = 1287 check_decode_ok "large" int64 "value = 9007199254740992" 9007199254740992L; 1288 check_decode_ok "max i64" int64 "value = 9223372036854775807" 1289 9223372036854775807L 1290 1291let test_codec_kind_doc () = 1292 Alcotest.(check string) "bool kind" "boolean" (kind bool); 1293 Alcotest.(check string) "int kind" "integer" (kind int); 1294 Alcotest.(check string) "string kind" "string" (kind string); 1295 Alcotest.(check string) "float kind" "float" (kind float); 1296 1297 let documented = with_doc ~kind:"custom" ~doc:"A custom codec" int in 1298 Alcotest.(check string) "custom kind" "custom" (kind documented); 1299 Alcotest.(check string) "custom doc" "A custom codec" (doc documented) 1300 1301let test_duplicate_member_error () = 1302 try 1303 let _ = 1304 Table.( 1305 obj (fun a b -> (a, b)) 1306 |> mem "same" int ~enc:fst |> mem "same" int ~enc:snd |> finish) 1307 in 1308 Alcotest.fail "should raise on duplicate member" 1309 with Invalid_argument _ -> () 1310 1311(* ============================================================================ 1312 Test Registration 1313 ============================================================================ *) 1314 1315let tz_tests = 1316 [ 1317 ("utc", `Quick, test_tz_utc); 1318 ("offset", `Quick, test_tz_offset); 1319 ("parse", `Quick, test_tz_parse); 1320 ("compare", `Quick, test_tz_compare); 1321 ] 1322 1323let date_tests = 1324 [ 1325 ("basic", `Quick, test_date_basic); 1326 ("equal", `Quick, test_date_equal); 1327 ("compare", `Quick, test_date_compare); 1328 ("parse", `Quick, test_date_parse); 1329 ("edge cases", `Quick, test_date_edge_cases); 1330 ] 1331 1332let time_tests = 1333 [ 1334 ("basic", `Quick, test_time_basic); 1335 ("fractional", `Quick, test_time_fractional); 1336 ("equal", `Quick, test_time_equal); 1337 ("compare", `Quick, test_time_compare); 1338 ("parse", `Quick, test_time_parse); 1339 ("edge cases", `Quick, test_time_edge_cases); 1340 ] 1341 1342let datetime_tests = 1343 [ 1344 ("basic", `Quick, test_datetime_basic); 1345 ("with offset", `Quick, test_datetime_with_offset); 1346 ("with frac", `Quick, test_datetime_with_frac); 1347 ("parse", `Quick, test_datetime_parse); 1348 ("equal compare", `Quick, test_datetime_equal_compare); 1349 ] 1350 1351let datetime_local_tests = 1352 [ 1353 ("basic", `Quick, test_datetime_local_basic); 1354 ("parse", `Quick, test_datetime_local_parse); 1355 ("equal compare", `Quick, test_datetime_local_equal_compare); 1356 ] 1357 1358let bool_tests = 1359 [ 1360 ("codec", `Quick, test_bool_codec); 1361 ("roundtrip", `Quick, test_bool_roundtrip); 1362 ("type error", `Quick, test_bool_type_error); 1363 ] 1364 1365let int_tests = 1366 [ 1367 ("codec", `Quick, test_int_codec); 1368 ("formats", `Quick, test_int_formats); 1369 ("roundtrip", `Quick, test_int_roundtrip); 1370 ("type error", `Quick, test_int_type_error); 1371 ] 1372 1373let int32_tests = 1374 [ 1375 ("codec", `Quick, test_int32_codec); 1376 ("roundtrip", `Quick, test_int32_roundtrip); 1377 ] 1378 1379let int64_tests = 1380 [ 1381 ("codec", `Quick, test_int64_codec); 1382 ("roundtrip", `Quick, test_int64_roundtrip); 1383 ] 1384 1385let float_tests = 1386 [ 1387 ("codec", `Quick, test_float_codec); 1388 ("special", `Quick, test_float_special); 1389 ("roundtrip", `Quick, test_float_roundtrip); 1390 ("type error", `Quick, test_float_type_error); 1391 ] 1392 1393let number_tests = 1394 [ 1395 ("codec", `Quick, test_number_codec); 1396 ("type error", `Quick, test_number_type_error); 1397 ] 1398 1399let string_tests = 1400 [ 1401 ("codec", `Quick, test_string_codec); 1402 ("escapes", `Quick, test_string_escapes); 1403 ("multiline", `Quick, test_string_multiline); 1404 ("roundtrip", `Quick, test_string_roundtrip); 1405 ("type error", `Quick, test_string_type_error); 1406 ] 1407 1408let ptime_codec_tests = 1409 [ 1410 ("ptime offset datetime", `Quick, test_ptime_codec); 1411 ("ptime with timezone offset", `Quick, test_ptime_codec_offset); 1412 ("ptime roundtrip", `Quick, test_ptime_codec_roundtrip); 1413 ("ptime optional seconds", `Quick, test_ptime_codec_optional_seconds); 1414 ("ptime_opt", `Quick, test_ptime_opt_codec); 1415 ("ptime_opt rejects local", `Quick, test_ptime_opt_rejects_local); 1416 ("ptime_span", `Quick, test_ptime_span_codec); 1417 ("ptime_span roundtrip", `Quick, test_ptime_span_roundtrip); 1418 ("ptime_date", `Quick, test_ptime_date_codec); 1419 ("ptime_date roundtrip", `Quick, test_ptime_date_roundtrip); 1420 ("ptime local datetime", `Quick, test_ptime_local_datetime); 1421 ("ptime date as ptime", `Quick, test_ptime_date_as_ptime); 1422 ] 1423 1424let ptime_full_codec_tests = 1425 [ 1426 ("offset datetime", `Quick, test_ptime_full_offset); 1427 ("local datetime", `Quick, test_ptime_full_local_datetime); 1428 ("local date", `Quick, test_ptime_full_date); 1429 ("local time", `Quick, test_ptime_full_time); 1430 ("roundtrip", `Quick, test_ptime_full_roundtrip); 1431 ] 1432 1433let combinator_tests = 1434 [ 1435 ("map", `Quick, test_map_combinator); 1436 ("map roundtrip", `Quick, test_map_roundtrip); 1437 ("map int", `Quick, test_map_int); 1438 ("const", `Quick, test_const); 1439 ("enum", `Quick, test_enum); 1440 ("enum roundtrip", `Quick, test_enum_roundtrip); 1441 ("enum unknown", `Quick, test_enum_unknown); 1442 ("enum type error", `Quick, test_enum_type_error); 1443 ("option", `Quick, test_option_codec); 1444 ("option roundtrip", `Quick, test_option_roundtrip); 1445 ("result", `Quick, test_result_codec); 1446 ("result roundtrip", `Quick, test_result_roundtrip); 1447 ("recursive", `Quick, test_recursive_codec); 1448 ] 1449 1450let array_tests = 1451 [ 1452 ("list", `Quick, test_list_codec); 1453 ("list roundtrip", `Quick, test_list_roundtrip); 1454 ("array", `Quick, test_array_codec); 1455 ("array roundtrip", `Quick, test_array_roundtrip); 1456 ("nested list", `Quick, test_nested_list); 1457 ("list of tables", `Quick, test_list_of_tables); 1458 ("list type error", `Quick, test_list_type_error); 1459 ] 1460 1461let table_tests = 1462 [ 1463 ("basic", `Quick, test_table_codec); 1464 ("roundtrip", `Quick, test_table_roundtrip); 1465 ("missing member", `Quick, test_table_missing_member); 1466 ("type error", `Quick, test_table_type_error); 1467 ("optional members", `Quick, test_optional_members); 1468 ("optional roundtrip", `Quick, test_optional_roundtrip); 1469 ("opt_mem omits none", `Quick, test_opt_mem_omits_none); 1470 ("enc_omit", `Quick, test_enc_omit); 1471 ("nested tables", `Quick, test_nested_tables); 1472 ("nested roundtrip", `Quick, test_nested_roundtrip); 1473 ("deeply nested", `Quick, test_deeply_nested); 1474 ("error unknown", `Quick, test_error_unknown); 1475 ("keep unknown", `Quick, test_keep_unknown); 1476 ("keep unknown roundtrip", `Quick, test_keep_unknown_roundtrip); 1477 ("skip unknown", `Quick, test_skip_unknown); 1478 ] 1479 1480let array_of_tables_tests = 1481 [ 1482 ("basic", `Quick, test_array_of_tables); 1483 ("roundtrip", `Quick, test_array_of_tables_roundtrip); 1484 ("empty", `Quick, test_array_of_tables_empty); 1485 ] 1486 1487let any_value_tests = 1488 [ 1489 ("value codec", `Quick, test_value_codec); 1490 ("value roundtrip", `Quick, test_value_roundtrip); 1491 ("value_mems", `Quick, test_value_mems_codec); 1492 ("any codec", `Quick, test_any_codec); 1493 ("any type error", `Quick, test_any_type_error); 1494 ] 1495 1496let function_tests = 1497 [ 1498 ("decode_string", `Quick, test_decode_string); 1499 ("decode_exn", `Quick, test_decode_string_exn); 1500 ("encode_string", `Quick, test_encode_string); 1501 ] 1502 1503let edge_case_tests = 1504 [ 1505 ("empty table", `Quick, test_empty_table); 1506 ("unicode keys", `Quick, test_unicode_keys); 1507 ("special strings", `Quick, test_special_string_values); 1508 ("large integers", `Quick, test_large_integers); 1509 ("codec kind doc", `Quick, test_codec_kind_doc); 1510 ("duplicate member error", `Quick, test_duplicate_member_error); 1511 ] 1512 1513let suite = 1514 ( "codec", 1515 List.concat 1516 [ 1517 tz_tests; 1518 date_tests; 1519 time_tests; 1520 datetime_tests; 1521 datetime_local_tests; 1522 bool_tests; 1523 int_tests; 1524 int32_tests; 1525 int64_tests; 1526 float_tests; 1527 number_tests; 1528 string_tests; 1529 ptime_codec_tests; 1530 ptime_full_codec_tests; 1531 combinator_tests; 1532 array_tests; 1533 table_tests; 1534 array_of_tables_tests; 1535 any_value_tests; 1536 function_tests; 1537 edge_case_tests; 1538 ] )