ocaml-containers/tests/core/t_cbor.ml
Simon Cruanes 83e03d2a94
wip
2026-02-10 21:29:02 -05:00

430 lines
11 KiB
OCaml

include (val Containers_testlib.make ~__FILE__ ())
module Cbor = Containers_cbor
let gen_c : Cbor.t Q.Gen.t =
let open Q.Gen in
sized @@ fix
@@ fun self size ->
let recurse = self (size - 1) in
let base =
[
1, return `Null;
1, return `Undefined;
( 3,
let+ x = int >|= Int64.of_int in
`Int x );
( 1,
let+ b = bool in
`Bool b );
( 1,
let+ x = 0 -- 19 in
`Simple x );
( 1,
let+ x = 26 -- 127 in
`Simple x );
( 1,
let+ f = float in
`Float f );
( 2,
let* n = oneof_weighted [ 20, 0 -- 150; 1, 151 -- 100_000 ] in
let+ s = string_size ~gen:printable (return n) in
`Text s );
( 2,
let* n = oneof_weighted [ 20, 0 -- 150; 1, 151 -- 100_000 ] in
let+ s = string_size ~gen:char (return n) in
`Bytes s );
]
in
let g_base = oneof_weighted base in
let rec_ =
[
( 2,
let+ l =
if size > 10 then
list_size (0 -- 1024) g_base
else
list_size (0 -- 10) recurse
in
`Array l );
( 2,
let+ l =
if size > 10 then
list_size (0 -- 1024) (pair g_base g_base)
else
list_size (0 -- 5) (pair g_base recurse)
in
`Map l );
( 1,
let+ i = 0 -- 1024 and+ sub = self (size - 1) in
`Tag (i, sub) );
]
in
oneof_weighted
(if size > 0 then
base @ rec_
else
base)
let rec shrink (c : Cbor.t) : Cbor.t Q.Iter.t =
let open Q.Iter in
match c with
| `Null | `Undefined | `Bool false -> empty
| `Bool true -> return (`Bool false)
| `Simple i ->
let+ i = Q.Shrink.int i in
`Simple i
| `Int i ->
let+ i = Q.Shrink.int (Int64.to_int i) in
`Int (Int64.of_int i)
| `Tag (t, i) ->
let+ i = shrink i in
`Tag (t, i)
| `Float _ -> empty
| `Array l ->
let+ l = Q.Shrink.list ~shrink l in
`Array l
| `Map l ->
let shrink_pair (a, b) =
(let+ a = shrink a in
a, b)
<+> let+ b = shrink b in
a, b
in
let+ l = Q.Shrink.list ~shrink:shrink_pair l in
`Map l
| `Text s ->
let+ s = Q.Shrink.string s in
`Text s
| `Bytes s ->
let+ s = Q.Shrink.string s in
`Bytes s
let arb = Q.make ~shrink ~print:Cbor.to_string_diagnostic gen_c
let rec eq_c c c' =
match c, c' with
| `Null, `Null | `Undefined, `Undefined -> true
| `Simple i, `Simple i' -> Int.equal i i'
| `Bool b, `Bool b' -> Bool.equal b b'
| `Int i, `Int i' -> Int64.equal i i'
| `Float f, `Float f' -> Float.equal f f'
| `Bytes s, `Bytes s' -> String.equal s s'
| `Text t, `Text t' -> String.equal t t'
| `Array a, `Array a' -> CCList.equal eq_c a a'
| `Map m, `Map m' ->
CCList.equal (fun (t0, t1) (t0', t1') -> eq_c t0 t0' && eq_c t1 t1') m m'
| `Tag (i, t), `Tag (i', t') -> Int.equal i i' && eq_c t t'
| _ -> false
;;
q ~count:1_000 ~long_factor:10 arb @@ fun c ->
let s = Cbor.encode c in
let c' = Cbor.decode_exn s in
if not (eq_c c c') then
Q.Test.fail_reportf "@[<hv2>roundtrip failed:@ from %a@ to %a@]"
Cbor.pp_diagnostic c Cbor.pp_diagnostic c';
true
;;
(* Additional edge case and error handling tests *)
(* Test basic encoding/decoding *)
t @@ fun () -> Cbor.decode_exn (Cbor.encode `Null) = `Null;;
t @@ fun () -> Cbor.decode_exn (Cbor.encode `Undefined) = `Undefined;;
t @@ fun () -> Cbor.decode_exn (Cbor.encode (`Bool true)) = `Bool true;;
t @@ fun () -> Cbor.decode_exn (Cbor.encode (`Bool false)) = `Bool false;;
(* Test integer edge cases *)
t @@ fun () -> Cbor.decode_exn (Cbor.encode (`Int 0L)) = `Int 0L;;
t @@ fun () -> Cbor.decode_exn (Cbor.encode (`Int 23L)) = `Int 23L;;
t @@ fun () -> Cbor.decode_exn (Cbor.encode (`Int 24L)) = `Int 24L;;
t @@ fun () -> Cbor.decode_exn (Cbor.encode (`Int 255L)) = `Int 255L;;
t @@ fun () -> Cbor.decode_exn (Cbor.encode (`Int 256L)) = `Int 256L;;
t @@ fun () -> Cbor.decode_exn (Cbor.encode (`Int 65535L)) = `Int 65535L;;
t @@ fun () -> Cbor.decode_exn (Cbor.encode (`Int 65536L)) = `Int 65536L;;
t @@ fun () ->
Cbor.decode_exn (Cbor.encode (`Int Int64.max_int)) = `Int Int64.max_int
;;
(* Test negative integers *)
t @@ fun () -> Cbor.decode_exn (Cbor.encode (`Int (-1L))) = `Int (-1L);;
t @@ fun () -> Cbor.decode_exn (Cbor.encode (`Int (-23L))) = `Int (-23L);;
t @@ fun () -> Cbor.decode_exn (Cbor.encode (`Int (-24L))) = `Int (-24L);;
t @@ fun () -> Cbor.decode_exn (Cbor.encode (`Int (-256L))) = `Int (-256L);;
t @@ fun () ->
Cbor.decode_exn (Cbor.encode (`Int Int64.min_int)) = `Int Int64.min_int
;;
(* Test floats *)
t @@ fun () -> Cbor.decode_exn (Cbor.encode (`Float 0.0)) = `Float 0.0;;
t @@ fun () -> Cbor.decode_exn (Cbor.encode (`Float 1.5)) = `Float 1.5;;
t @@ fun () -> Cbor.decode_exn (Cbor.encode (`Float (-1.5))) = `Float (-1.5);;
t @@ fun () ->
let result = Cbor.decode_exn (Cbor.encode (`Float infinity)) in
match result with
| `Float f -> classify_float f = FP_infinite && f > 0.0
| _ -> false
;;
t @@ fun () ->
let result = Cbor.decode_exn (Cbor.encode (`Float neg_infinity)) in
match result with
| `Float f -> classify_float f = FP_infinite && f < 0.0
| _ -> false
;;
t @@ fun () ->
let result = Cbor.decode_exn (Cbor.encode (`Float nan)) in
match result with
| `Float f -> classify_float f = FP_nan
| _ -> false
;;
(* Test strings *)
t @@ fun () -> Cbor.decode_exn (Cbor.encode (`Text "")) = `Text "";;
t @@ fun () -> Cbor.decode_exn (Cbor.encode (`Text "hello")) = `Text "hello";;
t @@ fun () -> Cbor.decode_exn (Cbor.encode (`Text "a")) = `Text "a";;
t @@ fun () ->
let long = String.make 1000 'x' in
Cbor.decode_exn (Cbor.encode (`Text long)) = `Text long
;;
(* Test UTF-8 strings *)
t @@ fun () ->
Cbor.decode_exn (Cbor.encode (`Text "hello 世界")) = `Text "hello 世界"
;;
t @@ fun () -> Cbor.decode_exn (Cbor.encode (`Text "émoji 🎉")) = `Text "émoji 🎉"
;;
t @@ fun () ->
Cbor.decode_exn (Cbor.encode (`Text "Здравствуй")) = `Text "Здравствуй"
;;
(* Test bytes *)
t @@ fun () -> Cbor.decode_exn (Cbor.encode (`Bytes "")) = `Bytes "";;
t @@ fun () ->
Cbor.decode_exn (Cbor.encode (`Bytes "\x00\x01\x02")) = `Bytes "\x00\x01\x02"
;;
t @@ fun () ->
let bytes = String.init 256 char_of_int in
Cbor.decode_exn (Cbor.encode (`Bytes bytes)) = `Bytes bytes
;;
(* Test arrays *)
t @@ fun () -> Cbor.decode_exn (Cbor.encode (`Array [])) = `Array [];;
t @@ fun () ->
Cbor.decode_exn (Cbor.encode (`Array [ `Int 1L ])) = `Array [ `Int 1L ]
;;
t @@ fun () ->
Cbor.decode_exn (Cbor.encode (`Array [ `Int 1L; `Int 2L; `Int 3L ]))
= `Array [ `Int 1L; `Int 2L; `Int 3L ]
;;
t @@ fun () ->
Cbor.decode_exn (Cbor.encode (`Array [ `Bool true; `Text "a"; `Int 42L ]))
= `Array [ `Bool true; `Text "a"; `Int 42L ]
;;
(* Test nested arrays *)
t @@ fun () ->
let nested = `Array [ `Array [ `Int 1L; `Int 2L ]; `Array [ `Int 3L ] ] in
Cbor.decode_exn (Cbor.encode nested) = nested
;;
(* Test maps *)
t @@ fun () -> Cbor.decode_exn (Cbor.encode (`Map [])) = `Map [];;
t @@ fun () ->
Cbor.decode_exn (Cbor.encode (`Map [ `Text "key", `Int 42L ]))
= `Map [ `Text "key", `Int 42L ]
;;
t @@ fun () ->
let map = `Map [ `Text "a", `Int 1L; `Text "b", `Int 2L; `Text "c", `Int 3L ] in
Cbor.decode_exn (Cbor.encode map) = map
;;
(* Test maps with various key types *)
t @@ fun () ->
let map = `Map [ `Int 0L, `Text "zero"; `Int 1L, `Text "one" ] in
Cbor.decode_exn (Cbor.encode map) = map
;;
(* Test tags *)
t @@ fun () ->
Cbor.decode_exn (Cbor.encode (`Tag (0, `Text "2013-03-21")))
= `Tag (0, `Text "2013-03-21")
;;
t @@ fun () ->
Cbor.decode_exn (Cbor.encode (`Tag (1, `Int 1363896240L)))
= `Tag (1, `Int 1363896240L)
;;
t @@ fun () ->
Cbor.decode_exn (Cbor.encode (`Tag (32, `Text "http://example.com")))
= `Tag (32, `Text "http://example.com")
;;
(* Test simple values *)
t @@ fun () -> Cbor.decode_exn (Cbor.encode (`Simple 0)) = `Simple 0;;
t @@ fun () -> Cbor.decode_exn (Cbor.encode (`Simple 19)) = `Simple 19;;
t @@ fun () -> Cbor.decode_exn (Cbor.encode (`Simple 255)) = `Simple 255;;
(* Test error cases *)
t @@ fun () ->
match Cbor.decode "" with
| Error _ -> true
| Ok _ -> false
;;
t @@ fun () ->
match Cbor.decode "\x1f" with
(* invalid additional info *)
| Error _ -> true
| Ok _ -> false
;;
t @@ fun () ->
match Cbor.decode "\x1c" with
(* reserved additional info *)
| Error _ -> true
| Ok _ -> false
;;
t @@ fun () ->
match Cbor.decode "\x5f\x42\x01\x02\x43\x03\x04\x05" with
(* incomplete indefinite *)
| Error _ -> true
| Ok _ -> false
;;
(* Test that decode_exn raises on invalid input *)
t @@ fun () ->
try
ignore (Cbor.decode_exn "");
false
with Failure _ -> true
;;
t @@ fun () ->
try
ignore (Cbor.decode_exn "\x1c");
false
with Failure _ -> true
;;
(* Test diagnostic string output *)
t @@ fun () -> Cbor.to_string_diagnostic `Null = "null";;
t @@ fun () -> Cbor.to_string_diagnostic `Undefined = "undefined";;
t @@ fun () -> Cbor.to_string_diagnostic (`Bool true) = "true";;
t @@ fun () -> Cbor.to_string_diagnostic (`Bool false) = "false";;
t @@ fun () -> Cbor.to_string_diagnostic (`Int 42L) = "42";;
t @@ fun () -> Cbor.to_string_diagnostic (`Int (-42L)) = "-42";;
t @@ fun () -> Cbor.to_string_diagnostic (`Float 1.5) = "1.5";;
t @@ fun () -> Cbor.to_string_diagnostic (`Text "hello") = "\"hello\"";;
t @@ fun () ->
Cbor.to_string_diagnostic (`Array [ `Int 1L; `Int 2L ]) = "[1, 2]"
;;
t @@ fun () ->
Cbor.to_string_diagnostic (`Map [ `Text "a", `Int 1L ])
|> CCString.mem ~sub:"\"a\""
;;
(* Test deeply nested structures *)
t @@ fun () ->
let rec make_nested n =
if n = 0 then
`Int 0L
else
`Array [ make_nested (n - 1) ]
in
let nested = make_nested 100 in
Cbor.decode_exn (Cbor.encode nested) = nested
;;
(* Test large collections *)
t @@ fun () ->
let large_array = `Array (List.init 1000 (fun i -> `Int (Int64.of_int i))) in
Cbor.decode_exn (Cbor.encode large_array) = large_array
;;
t @@ fun () ->
let large_map =
`Map (List.init 500 (fun i -> `Int (Int64.of_int i), `Text (string_of_int i)))
in
Cbor.decode_exn (Cbor.encode large_map) = large_map
;;
(* Test mixed nested structures *)
t @@ fun () ->
let complex =
`Map
[
`Text "array", `Array [ `Int 1L; `Int 2L; `Int 3L ];
`Text "map", `Map [ `Text "nested", `Bool true ];
`Text "tagged", `Tag (42, `Text "value");
`Text "null", `Null;
]
in
Cbor.decode_exn (Cbor.encode complex) = complex
;;
(* Test that encoding is consistent *)
t @@ fun () ->
let c = `Map [ `Text "a", `Int 1L; `Text "b", `Int 2L ] in
let e1 = Cbor.encode c in
let e2 = Cbor.encode c in
e1 = e2
;;
(* Test buffer reuse *)
t @@ fun () ->
let buf = Buffer.create 16 in
let _ = Cbor.encode ~buf (`Int 1L) in
let s1 = Buffer.contents buf in
Buffer.clear buf;
let _ = Cbor.encode ~buf (`Int 1L) in
let s2 = Buffer.contents buf in
s1 = s2
;;
(* Property: encoding then decoding gives original value *)
q ~count:5000 arb @@ fun c ->
match Cbor.decode (Cbor.encode c) with
| Ok c' -> eq_c c c'
| Error e -> Q.Test.fail_reportf "decode failed: %s" e
;;
(* Property: decode result equality *)
q ~count:2000 arb @@ fun c ->
let s = Cbor.encode c in
match Cbor.decode s with
| Error e -> Q.Test.fail_reportf "decode failed on encoded value: %s" e
| Ok c1 ->
(match Cbor.decode s with
| Error _ -> false
| Ok c2 -> eq_c c1 c2)
;;
(* Property: diagnostic string doesn't crash *)
q ~count:1000 arb @@ fun c ->
let _ = Cbor.to_string_diagnostic c in
true
;;
(* Property: encoding size is reasonable *)
q ~count:1000 arb @@ fun c ->
let s = Cbor.encode c in
String.length s < 1_000_000 (* Sanity check *)