Pure OCaml Yaml 1.2 reader and writer using Bytesrw

Expand fuzz testing with emitter tests and additional coverage

Add fuzz_emitter module testing the streaming emitter API with random
event sequences, valid roundtrips, nested structures, scalar styles,
anchors/aliases, tags, and multiple documents.

Extend fuzz_value tests for value roundtrips, serialization styles,
JSON conversion, compare/equal properties, and Util functions.

Extend fuzz_yamlrw tests for double roundtrip stabilization, cross-style
roundtrips, scanner/parser crash safety, block scalars, quoted strings,
deep nesting, and document handling modes.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

+707 -2
+2 -1
fuzz/dune
··· 22 22 fuzz_chomping 23 23 fuzz_tag 24 24 fuzz_value 25 - fuzz_yamlrw)) 25 + fuzz_yamlrw 26 + fuzz_emitter)) 26 27 27 28 ; Standalone AFL fuzzer for targeted parser testing 28 29 ; This is a simpler executable that directly reads input and exercises the parser
+2 -1
fuzz/fuzz.ml
··· 32 32 Fuzz_chomping.run (); 33 33 Fuzz_tag.run (); 34 34 Fuzz_value.run (); 35 - Fuzz_yamlrw.run () 35 + Fuzz_yamlrw.run (); 36 + Fuzz_emitter.run ()
+283
fuzz/fuzz_emitter.ml
··· 1 + (*--------------------------------------------------------------------------- 2 + Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved. 3 + SPDX-License-Identifier: ISC 4 + ---------------------------------------------------------------------------*) 5 + 6 + (** Fuzz tests for the Emitter module - test random event sequences *) 7 + 8 + open Crowbar 9 + open Fuzz_common 10 + 11 + (** Event type for fuzzing *) 12 + type fuzz_event = 13 + | Stream_start 14 + | Stream_end 15 + | Doc_start 16 + | Doc_end 17 + | Scalar of string 18 + | Alias of string 19 + | Seq_start 20 + | Seq_end 21 + | Map_start 22 + | Map_end 23 + 24 + (** Generator for fuzz events *) 25 + let fuzz_event = 26 + choose 27 + [ 28 + const Stream_start; 29 + const Stream_end; 30 + const Doc_start; 31 + const Doc_end; 32 + map [ ident_string ] (fun s -> Scalar s); 33 + map [ ident_string ] (fun s -> Alias s); 34 + const Seq_start; 35 + const Seq_end; 36 + const Map_start; 37 + const Map_end; 38 + ] 39 + 40 + (** Emit a fuzz event to an emitter - may fail with Yamlrw_error *) 41 + let emit_fuzz_event emitter = function 42 + | Stream_start -> Yamlrw.Stream.stream_start emitter `Utf8 43 + | Stream_end -> Yamlrw.Stream.stream_end emitter 44 + | Doc_start -> Yamlrw.Stream.document_start emitter () 45 + | Doc_end -> Yamlrw.Stream.document_end emitter () 46 + | Scalar s -> Yamlrw.Stream.scalar emitter s 47 + | Alias s -> Yamlrw.Stream.alias emitter s 48 + | Seq_start -> Yamlrw.Stream.sequence_start emitter () 49 + | Seq_end -> Yamlrw.Stream.sequence_end emitter 50 + | Map_start -> Yamlrw.Stream.mapping_start emitter () 51 + | Map_end -> Yamlrw.Stream.mapping_end emitter 52 + 53 + (** Test that random event sequences don't crash the emitter *) 54 + let () = 55 + add_test ~name:"emitter: random events crash safety" [ list fuzz_event ] 56 + @@ fun events -> 57 + let emitter = Yamlrw.Stream.emitter () in 58 + List.iter 59 + (fun ev -> 60 + try emit_fuzz_event emitter ev with Yamlrw.Yamlrw_error _ -> ()) 61 + events; 62 + check true 63 + 64 + (** Test that valid event sequences produce parseable output *) 65 + let () = 66 + add_test ~name:"emitter: valid sequence roundtrip" [ list ident_string ] 67 + @@ fun items -> 68 + if List.length items > 0 then begin 69 + let emitter = Yamlrw.Stream.emitter () in 70 + (try 71 + Yamlrw.Stream.stream_start emitter `Utf8; 72 + Yamlrw.Stream.document_start emitter (); 73 + Yamlrw.Stream.sequence_start emitter (); 74 + List.iter (fun s -> Yamlrw.Stream.scalar emitter s) items; 75 + Yamlrw.Stream.sequence_end emitter; 76 + Yamlrw.Stream.document_end emitter (); 77 + Yamlrw.Stream.stream_end emitter; 78 + let yaml = Yamlrw.Stream.contents emitter in 79 + (* Try to parse the emitted YAML *) 80 + let _ = Yamlrw.of_string yaml in 81 + () 82 + with Yamlrw.Yamlrw_error _ -> ()); 83 + check true 84 + end 85 + else check true 86 + 87 + (** Test that valid mapping event sequences produce parseable output *) 88 + let () = 89 + add_test ~name:"emitter: valid mapping roundtrip" 90 + [ list (pair ident_string ident_string) ] 91 + @@ fun pairs -> 92 + if List.length pairs > 0 then begin 93 + let emitter = Yamlrw.Stream.emitter () in 94 + (try 95 + Yamlrw.Stream.stream_start emitter `Utf8; 96 + Yamlrw.Stream.document_start emitter (); 97 + Yamlrw.Stream.mapping_start emitter (); 98 + List.iter 99 + (fun (k, v) -> 100 + Yamlrw.Stream.scalar emitter k; 101 + Yamlrw.Stream.scalar emitter v) 102 + pairs; 103 + Yamlrw.Stream.mapping_end emitter; 104 + Yamlrw.Stream.document_end emitter (); 105 + Yamlrw.Stream.stream_end emitter; 106 + let yaml = Yamlrw.Stream.contents emitter in 107 + (* Try to parse the emitted YAML *) 108 + let _ = Yamlrw.of_string yaml in 109 + () 110 + with Yamlrw.Yamlrw_error _ -> ()); 111 + check true 112 + end 113 + else check true 114 + 115 + (** Test nested sequences *) 116 + let () = 117 + add_test ~name:"emitter: nested sequences" [ range 10; list ident_string ] 118 + @@ fun depth items -> 119 + if depth > 0 && List.length items > 0 then begin 120 + let emitter = Yamlrw.Stream.emitter () in 121 + (try 122 + Yamlrw.Stream.stream_start emitter `Utf8; 123 + Yamlrw.Stream.document_start emitter (); 124 + for _ = 1 to depth do 125 + Yamlrw.Stream.sequence_start emitter () 126 + done; 127 + List.iter (fun s -> Yamlrw.Stream.scalar emitter s) items; 128 + for _ = 1 to depth do 129 + Yamlrw.Stream.sequence_end emitter 130 + done; 131 + Yamlrw.Stream.document_end emitter (); 132 + Yamlrw.Stream.stream_end emitter; 133 + let yaml = Yamlrw.Stream.contents emitter in 134 + let _ = Yamlrw.of_string yaml in 135 + () 136 + with Yamlrw.Yamlrw_error _ -> ()); 137 + check true 138 + end 139 + else check true 140 + 141 + (** Test nested mappings *) 142 + let () = 143 + add_test ~name:"emitter: nested mappings" [ range 10; ident_string ] 144 + @@ fun depth value -> 145 + if depth > 0 && String.length value > 0 then begin 146 + let emitter = Yamlrw.Stream.emitter () in 147 + (try 148 + Yamlrw.Stream.stream_start emitter `Utf8; 149 + Yamlrw.Stream.document_start emitter (); 150 + for i = 1 to depth do 151 + Yamlrw.Stream.mapping_start emitter (); 152 + Yamlrw.Stream.scalar emitter (Printf.sprintf "key%d" i) 153 + done; 154 + Yamlrw.Stream.scalar emitter value; 155 + for _ = 1 to depth do 156 + Yamlrw.Stream.mapping_end emitter 157 + done; 158 + Yamlrw.Stream.document_end emitter (); 159 + Yamlrw.Stream.stream_end emitter; 160 + let yaml = Yamlrw.Stream.contents emitter in 161 + let _ = Yamlrw.of_string yaml in 162 + () 163 + with Yamlrw.Yamlrw_error _ -> ()); 164 + check true 165 + end 166 + else check true 167 + 168 + (** Test emitter with different scalar styles *) 169 + let () = 170 + add_test ~name:"emitter: scalar styles" [ printable_string ] @@ fun s -> 171 + let styles = 172 + [ `Any; `Plain; `Single_quoted; `Double_quoted; `Literal; `Folded ] 173 + in 174 + List.iter 175 + (fun style -> 176 + let emitter = Yamlrw.Stream.emitter () in 177 + (try 178 + Yamlrw.Stream.stream_start emitter `Utf8; 179 + Yamlrw.Stream.document_start emitter (); 180 + Yamlrw.Stream.scalar emitter ~style s; 181 + Yamlrw.Stream.document_end emitter (); 182 + Yamlrw.Stream.stream_end emitter; 183 + let yaml = Yamlrw.Stream.contents emitter in 184 + let _ = Yamlrw.of_string yaml in 185 + () 186 + with Yamlrw.Yamlrw_error _ -> ())) 187 + styles; 188 + check true 189 + 190 + (** Test emitter with anchors and aliases *) 191 + let () = 192 + add_test ~name:"emitter: anchors and aliases" [ ident_string; ident_string ] 193 + @@ fun anchor value -> 194 + if String.length anchor > 0 && String.length value > 0 then begin 195 + let emitter = Yamlrw.Stream.emitter () in 196 + (try 197 + Yamlrw.Stream.stream_start emitter `Utf8; 198 + Yamlrw.Stream.document_start emitter (); 199 + Yamlrw.Stream.mapping_start emitter (); 200 + Yamlrw.Stream.scalar emitter "original"; 201 + Yamlrw.Stream.scalar emitter ~anchor value; 202 + Yamlrw.Stream.scalar emitter "reference"; 203 + Yamlrw.Stream.alias emitter anchor; 204 + Yamlrw.Stream.mapping_end emitter; 205 + Yamlrw.Stream.document_end emitter (); 206 + Yamlrw.Stream.stream_end emitter; 207 + let yaml = Yamlrw.Stream.contents emitter in 208 + let _ = Yamlrw.of_string yaml in 209 + () 210 + with Yamlrw.Yamlrw_error _ -> ()); 211 + check true 212 + end 213 + else check true 214 + 215 + (** Test emitter with tags *) 216 + let () = 217 + add_test ~name:"emitter: tagged scalars" [ ident_string; ident_string ] 218 + @@ fun tag value -> 219 + if String.length value > 0 then begin 220 + let emitter = Yamlrw.Stream.emitter () in 221 + (try 222 + Yamlrw.Stream.stream_start emitter `Utf8; 223 + Yamlrw.Stream.document_start emitter (); 224 + Yamlrw.Stream.scalar emitter ~tag:("!" ^ tag) value; 225 + Yamlrw.Stream.document_end emitter (); 226 + Yamlrw.Stream.stream_end emitter; 227 + let yaml = Yamlrw.Stream.contents emitter in 228 + let _ = Yamlrw.yaml_of_string yaml in 229 + () 230 + with Yamlrw.Yamlrw_error _ -> ()); 231 + check true 232 + end 233 + else check true 234 + 235 + (** Test emitter with layout styles *) 236 + let () = 237 + add_test ~name:"emitter: layout styles" [ list ident_string ] @@ fun items -> 238 + if List.length items > 0 then begin 239 + let styles = [ `Any; `Block; `Flow ] in 240 + List.iter 241 + (fun style -> 242 + let emitter = Yamlrw.Stream.emitter () in 243 + (try 244 + Yamlrw.Stream.stream_start emitter `Utf8; 245 + Yamlrw.Stream.document_start emitter (); 246 + Yamlrw.Stream.sequence_start emitter ~style (); 247 + List.iter (fun s -> Yamlrw.Stream.scalar emitter s) items; 248 + Yamlrw.Stream.sequence_end emitter; 249 + Yamlrw.Stream.document_end emitter (); 250 + Yamlrw.Stream.stream_end emitter; 251 + let yaml = Yamlrw.Stream.contents emitter in 252 + let _ = Yamlrw.of_string yaml in 253 + () 254 + with Yamlrw.Yamlrw_error _ -> ())) 255 + styles; 256 + check true 257 + end 258 + else check true 259 + 260 + (** Test multiple documents *) 261 + let () = 262 + add_test ~name:"emitter: multiple documents" [ range 5; ident_string ] 263 + @@ fun count value -> 264 + if count > 0 && String.length value > 0 then begin 265 + let emitter = Yamlrw.Stream.emitter () in 266 + (try 267 + Yamlrw.Stream.stream_start emitter `Utf8; 268 + for i = 1 to count do 269 + Yamlrw.Stream.document_start emitter (); 270 + Yamlrw.Stream.scalar emitter (Printf.sprintf "%s%d" value i); 271 + Yamlrw.Stream.document_end emitter () 272 + done; 273 + Yamlrw.Stream.stream_end emitter; 274 + let yaml = Yamlrw.Stream.contents emitter in 275 + let docs = Yamlrw.documents_of_string yaml in 276 + if List.length docs <> count then fail "document count mismatch" 277 + else () 278 + with Yamlrw.Yamlrw_error _ -> ()); 279 + check true 280 + end 281 + else check true 282 + 283 + let run () = ()
+191
fuzz/fuzz_value.ml
··· 196 196 | _ -> fail "combine should produce object") 197 197 | _ -> check true 198 198 199 + (** Test generated value -> serialize -> parse roundtrip *) 200 + let () = 201 + add_test ~name:"value: generated value roundtrip" [ value ] @@ fun v -> 202 + (try 203 + let s = Yamlrw.to_string v in 204 + let v' = Yamlrw.of_string s in 205 + if not (Yamlrw.equal v v') then fail "generated value roundtrip mismatch" 206 + else () 207 + with Yamlrw.Yamlrw_error _ -> 208 + (* Some generated values might not roundtrip perfectly due to YAML ambiguities *) 209 + ()); 210 + check true 211 + 212 + (** Test generated value serialization with block style *) 213 + let () = 214 + add_test ~name:"value: generated block style" [ value ] @@ fun v -> 215 + (try 216 + let s = Yamlrw.to_string ~layout_style:`Block v in 217 + let _ = Yamlrw.of_string s in 218 + () 219 + with Yamlrw.Yamlrw_error _ -> ()); 220 + check true 221 + 222 + (** Test generated value serialization with flow style *) 223 + let () = 224 + add_test ~name:"value: generated flow style" [ value ] @@ fun v -> 225 + (try 226 + let s = Yamlrw.to_string ~layout_style:`Flow v in 227 + let _ = Yamlrw.of_string s in 228 + () 229 + with Yamlrw.Yamlrw_error _ -> ()); 230 + check true 231 + 232 + (** Test to_json/of_json roundtrip for generated values *) 233 + let () = 234 + add_test ~name:"value: to_json/of_json generated" [ value ] @@ fun v -> 235 + let y = Yamlrw.of_json v in 236 + let v' = Yamlrw.to_json y in 237 + if not (Yamlrw.equal v v') then fail "to_json/of_json roundtrip mismatch" 238 + else check true 239 + 240 + (** Test compare is transitive *) 241 + let () = 242 + add_test ~name:"value: compare transitive" [ value; value; value ] 243 + @@ fun v1 v2 v3 -> 244 + let c12 = Yamlrw.Value.compare v1 v2 in 245 + let c23 = Yamlrw.Value.compare v2 v3 in 246 + let c13 = Yamlrw.Value.compare v1 v3 in 247 + (* If v1 <= v2 and v2 <= v3 then v1 <= v3 *) 248 + if c12 <= 0 && c23 <= 0 && c13 > 0 then fail "compare not transitive" 249 + else if c12 >= 0 && c23 >= 0 && c13 < 0 then fail "compare not transitive" 250 + else check true 251 + 252 + (** Test equal is symmetric *) 253 + let () = 254 + add_test ~name:"value: equal symmetric" [ value; value ] @@ fun v1 v2 -> 255 + let eq12 = Yamlrw.Value.equal v1 v2 in 256 + let eq21 = Yamlrw.Value.equal v2 v1 in 257 + if eq12 <> eq21 then fail "equal not symmetric" else check true 258 + 259 + (** Test filter on lists *) 260 + let () = 261 + add_test ~name:"value: filter" [ value ] @@ fun v -> 262 + match v with 263 + | `A _ -> 264 + let filtered = Yamlrw.Value.filter (fun _ -> true) v in 265 + if not (Yamlrw.Value.equal v filtered) then 266 + fail "filter (fun _ -> true) should be identity" 267 + else 268 + let empty = Yamlrw.Value.filter (fun _ -> false) v in 269 + (match empty with 270 + | `A [] -> check true 271 + | `A _ -> fail "filter (fun _ -> false) should be empty" 272 + | _ -> fail "filter should preserve list type") 273 + | _ -> check true 274 + 275 + (** Test keys/values for objects *) 276 + let () = 277 + add_test ~name:"value: keys/values" [ value ] @@ fun v -> 278 + match v with 279 + | `O pairs -> 280 + let ks = Yamlrw.Value.keys v in 281 + let vs = Yamlrw.Value.values v in 282 + if List.length ks <> List.length pairs then fail "keys length mismatch" 283 + else if List.length vs <> List.length pairs then 284 + fail "values length mismatch" 285 + else check true 286 + | _ -> check true 287 + 288 + (** Test Util.update *) 289 + let () = 290 + add_test ~name:"value: Util.update" [ value; ident_string; value ] 291 + @@ fun v key newv -> 292 + match v with 293 + | `O _ -> 294 + (try 295 + let updated = Yamlrw.Util.update key newv v in 296 + let found = Yamlrw.Value.find key updated in 297 + match found with 298 + | Some x when Yamlrw.Value.equal x newv -> check true 299 + | Some _ -> fail "update: found wrong value" 300 + | None -> fail "update: key not found after update" 301 + with Yamlrw.Util.Type_error _ -> fail "Type_error on update") 302 + | _ -> check true 303 + 304 + (** Test Util.remove *) 305 + let () = 306 + add_test ~name:"value: Util.remove" [ value; ident_string ] @@ fun v key -> 307 + match v with 308 + | `O _ -> 309 + (try 310 + let removed = Yamlrw.Util.remove key v in 311 + let found = Yamlrw.Value.find key removed in 312 + if Option.is_some found then fail "remove: key still present" 313 + else check true 314 + with Yamlrw.Util.Type_error _ -> fail "Type_error on remove") 315 + | _ -> check true 316 + 317 + (** Test Util.get_path *) 318 + let () = 319 + add_test ~name:"value: Util.get_path" [ value; list ident_string ] 320 + @@ fun v path -> 321 + let _ = Yamlrw.Util.get_path path v in 322 + check true 323 + 324 + (** Test Util.flatten *) 325 + let () = 326 + add_test ~name:"value: Util.flatten" [ value ] @@ fun v -> 327 + match v with 328 + | `A _ -> 329 + (try 330 + let _ = Yamlrw.Util.flatten v in 331 + check true 332 + with Yamlrw.Util.Type_error _ -> fail "Type_error on flatten of list") 333 + | _ -> check true 334 + 335 + (** Test Util.nth *) 336 + let () = 337 + add_test ~name:"value: Util.nth" [ value; range 100 ] @@ fun v idx -> 338 + match v with 339 + | `A lst -> 340 + let result = Yamlrw.Util.nth idx v in 341 + if idx < List.length lst then 342 + match result with 343 + | Some x when Yamlrw.Value.equal x (List.nth lst idx) -> check true 344 + | Some _ -> fail "nth returned wrong element" 345 + | None -> fail "nth returned None for valid index" 346 + else if Option.is_some result then 347 + fail "nth returned Some for invalid index" 348 + else check true 349 + | _ -> check true 350 + 351 + (** Test Util.length *) 352 + let () = 353 + add_test ~name:"value: Util.length" [ value ] @@ fun v -> 354 + let len = Yamlrw.Util.length v in 355 + (match v with 356 + | `A lst when len = List.length lst -> () 357 + | `O pairs when len = List.length pairs -> () 358 + | `A _ -> fail "length mismatch for list" 359 + | `O _ -> fail "length mismatch for object" 360 + | _ when len = 0 -> () 361 + | _ -> fail "length should be 0 for scalars"); 362 + check true 363 + 364 + (** Test Util.fold *) 365 + let () = 366 + add_test ~name:"value: Util.fold" [ value ] @@ fun v -> 367 + match v with 368 + | `A lst -> 369 + (try 370 + let count = Yamlrw.Util.fold (fun acc _ -> acc + 1) 0 v in 371 + if count <> List.length lst then fail "fold count mismatch" 372 + else check true 373 + with Yamlrw.Util.Type_error _ -> fail "Type_error on fold of list") 374 + | _ -> check true 375 + 376 + (** Test Util.mapi preserves length *) 377 + let () = 378 + add_test ~name:"value: Util.mapi preserves length" [ value ] @@ fun v -> 379 + match v with 380 + | `A lst -> 381 + (try 382 + let mapped = Yamlrw.Util.mapi (fun _ x -> x) v in 383 + (match mapped with 384 + | `A lst' when List.length lst = List.length lst' -> check true 385 + | `A _ -> fail "mapi changed list length" 386 + | _ -> fail "mapi changed type") 387 + with Yamlrw.Util.Type_error _ -> fail "Type_error on mapi of list") 388 + | _ -> check true 389 + 199 390 let run () = ()
+229
fuzz/fuzz_yamlrw.ml
··· 226 226 with Yamlrw.Yamlrw_error _ -> ()); 227 227 check true 228 228 229 + (** Test double roundtrip stabilizes - serialize twice should be identical *) 230 + let () = 231 + add_test ~name:"yamlrw: double roundtrip stabilizes" [ bytes ] @@ fun buf -> 232 + (try 233 + let v1 = Yamlrw.of_string buf in 234 + let s1 = Yamlrw.to_string v1 in 235 + let v2 = Yamlrw.of_string s1 in 236 + let s2 = Yamlrw.to_string v2 in 237 + let v3 = Yamlrw.of_string s2 in 238 + let s3 = Yamlrw.to_string v3 in 239 + (* After two roundtrips, serialization should stabilize *) 240 + if s2 <> s3 then fail "serialization did not stabilize after 2 roundtrips" 241 + else if not (Yamlrw.equal v2 v3) then fail "values differ after stabilization" 242 + else () 243 + with Yamlrw.Yamlrw_error _ -> ()); 244 + check true 245 + 246 + (** Test cross-style roundtrip: parse any, emit block, re-parse *) 247 + let () = 248 + add_test ~name:"yamlrw: cross-style block roundtrip" [ bytes ] @@ fun buf -> 249 + (try 250 + let v1 = Yamlrw.of_string buf in 251 + let s_block = Yamlrw.to_string ~layout_style:`Block v1 in 252 + let v2 = Yamlrw.of_string s_block in 253 + if not (Yamlrw.equal v1 v2) then fail "block style roundtrip mismatch" 254 + else () 255 + with Yamlrw.Yamlrw_error _ -> ()); 256 + check true 257 + 258 + (** Test cross-style roundtrip: parse any, emit flow, re-parse *) 259 + let () = 260 + add_test ~name:"yamlrw: cross-style flow roundtrip" [ bytes ] @@ fun buf -> 261 + (try 262 + let v1 = Yamlrw.of_string buf in 263 + let s_flow = Yamlrw.to_string ~layout_style:`Flow v1 in 264 + let v2 = Yamlrw.of_string s_flow in 265 + if not (Yamlrw.equal v1 v2) then fail "flow style roundtrip mismatch" 266 + else () 267 + with Yamlrw.Yamlrw_error _ -> ()); 268 + check true 269 + 270 + (** Test scanner never crashes on arbitrary input *) 271 + let () = 272 + add_test ~name:"yamlrw: scanner crash safety" [ bytes ] @@ fun buf -> 273 + (try 274 + let scanner = Yamlrw.Scanner.of_string buf in 275 + let _ = Yamlrw.Scanner.to_list scanner in 276 + () 277 + with Yamlrw.Yamlrw_error _ -> ()); 278 + check true 279 + 280 + (** Test streaming parser never crashes *) 281 + let () = 282 + add_test ~name:"yamlrw: stream parser crash safety" [ bytes ] @@ fun buf -> 283 + (try 284 + let parser = Yamlrw.Stream.parser buf in 285 + Yamlrw.Stream.iter (fun _ _ _ -> ()) parser 286 + with Yamlrw.Yamlrw_error _ -> ()); 287 + check true 288 + 289 + (** Test that scanner tokens and parser events are consistent *) 290 + let () = 291 + add_test ~name:"yamlrw: scanner/parser consistency" [ bytes ] @@ fun buf -> 292 + let scanner_ok = 293 + try 294 + let scanner = Yamlrw.Scanner.of_string buf in 295 + let _ = Yamlrw.Scanner.to_list scanner in 296 + true 297 + with Yamlrw.Yamlrw_error _ -> false 298 + in 299 + let parser_ok = 300 + try 301 + let parser = Yamlrw.Stream.parser buf in 302 + Yamlrw.Stream.iter (fun _ _ _ -> ()) parser; 303 + true 304 + with Yamlrw.Yamlrw_error _ -> false 305 + in 306 + (* If scanner succeeds, parser should not crash (may still error on invalid structure) *) 307 + if scanner_ok && not parser_ok then 308 + (* This is actually OK - scanner can tokenize invalid YAML structure *) 309 + check true 310 + else check true 311 + 312 + (** Test literal block scalar style *) 313 + let () = 314 + add_test ~name:"yamlrw: literal block scalar" [ printable_string ] @@ fun s -> 315 + if String.length s > 0 then begin 316 + let yaml = "|\n " ^ String.concat "\n " (String.split_on_char '\n' s) in 317 + (try 318 + let _ = Yamlrw.of_string yaml in 319 + () 320 + with Yamlrw.Yamlrw_error _ -> ()); 321 + check true 322 + end 323 + else check true 324 + 325 + (** Test folded block scalar style *) 326 + let () = 327 + add_test ~name:"yamlrw: folded block scalar" [ printable_string ] @@ fun s -> 328 + if String.length s > 0 then begin 329 + let yaml = ">\n " ^ String.concat "\n " (String.split_on_char '\n' s) in 330 + (try 331 + let _ = Yamlrw.of_string yaml in 332 + () 333 + with Yamlrw.Yamlrw_error _ -> ()); 334 + check true 335 + end 336 + else check true 337 + 338 + (** Test single-quoted scalar *) 339 + let () = 340 + add_test ~name:"yamlrw: single quoted scalar" [ printable_string ] @@ fun s -> 341 + (* Escape single quotes by doubling them *) 342 + let escaped = Str.global_replace (Str.regexp "'") "''" s in 343 + let yaml = "'" ^ escaped ^ "'" in 344 + (try 345 + let _ = Yamlrw.of_string yaml in 346 + () 347 + with Yamlrw.Yamlrw_error _ -> ()); 348 + check true 349 + 350 + (** Test double-quoted scalar with escape sequences *) 351 + let () = 352 + add_test ~name:"yamlrw: double quoted with escapes" [ printable_string ] 353 + @@ fun s -> 354 + let yaml = "\"" ^ String.escaped s ^ "\"" in 355 + (try 356 + let _ = Yamlrw.of_string yaml in 357 + () 358 + with Yamlrw.Yamlrw_error _ -> ()); 359 + check true 360 + 361 + (** Test deeply nested structures don't crash *) 362 + let () = 363 + add_test ~name:"yamlrw: deep nesting" [ range 50 ] @@ fun depth -> 364 + let yaml = String.make depth '[' ^ "null" ^ String.make depth ']' in 365 + (try 366 + let _ = Yamlrw.of_string yaml in 367 + () 368 + with Yamlrw.Yamlrw_error _ -> ()); 369 + check true 370 + 371 + (** Test multiple anchors and aliases *) 372 + let () = 373 + add_test ~name:"yamlrw: multiple anchors" [ ident_string; ident_string ] 374 + @@ fun name1 name2 -> 375 + if String.length name1 > 0 && String.length name2 > 0 then begin 376 + let yaml = 377 + Printf.sprintf "a: &%s value1\nb: &%s value2\nc: *%s\nd: *%s" name1 name2 378 + name1 name2 379 + in 380 + (try 381 + let _ = Yamlrw.of_string yaml in 382 + () 383 + with Yamlrw.Yamlrw_error _ -> ()); 384 + check true 385 + end 386 + else check true 387 + 388 + (** Test error positions are within input bounds *) 389 + let () = 390 + add_test ~name:"yamlrw: error position bounds" [ bytes ] @@ fun buf -> 391 + (try 392 + let _ = Yamlrw.of_string buf in 393 + () 394 + with Yamlrw.Yamlrw_error err -> 395 + (* Error has span : Span.t option, and Span has start/end positions *) 396 + match err.span with 397 + | None -> () (* No position info, that's ok *) 398 + | Some span -> 399 + let start_pos = span.start in 400 + let line = start_pos.Yamlrw.Position.line in 401 + let col = start_pos.Yamlrw.Position.column in 402 + let offset = start_pos.Yamlrw.Position.index in 403 + if line < 1 then fail "error line < 1" 404 + else if col < 0 then fail "error column < 0" 405 + else if offset < 0 then fail "error offset < 0" 406 + else if offset > String.length buf then 407 + fail "error offset > input length" 408 + else ()); 409 + check true 410 + 411 + (** Test yaml_of_string with resolve_aliases=true vs false *) 412 + let () = 413 + add_test ~name:"yamlrw: yaml resolve_aliases modes" [ bytes ] @@ fun buf -> 414 + let with_resolve = 415 + try Some (Yamlrw.yaml_of_string ~resolve_aliases:true buf) 416 + with Yamlrw.Yamlrw_error _ -> None 417 + in 418 + let without_resolve = 419 + try Some (Yamlrw.yaml_of_string ~resolve_aliases:false buf) 420 + with Yamlrw.Yamlrw_error _ -> None 421 + in 422 + (* Both should either succeed or fail, but not crash *) 423 + (match (with_resolve, without_resolve) with 424 + | Some y1, Some _y2 -> 425 + (* If both succeed, serializing resolved version should work *) 426 + let _ = Yamlrw.yaml_to_string y1 in 427 + () 428 + | _ -> ()); 429 + check true 430 + 431 + (** Test documents roundtrip with resolve_aliases=false preserves structure *) 432 + let () = 433 + add_test ~name:"yamlrw: documents roundtrip (no resolve)" [ bytes ] @@ fun buf -> 434 + (try 435 + let docs = Yamlrw.documents_of_string buf in 436 + let serialized = Yamlrw.documents_to_string ~resolve_aliases:false docs in 437 + let docs' = Yamlrw.documents_of_string serialized in 438 + if List.length docs <> List.length docs' then 439 + fail "document count mismatch after roundtrip (no resolve)" 440 + else () 441 + with Yamlrw.Yamlrw_error _ -> ()); 442 + check true 443 + 444 + (** Test documents roundtrip with resolve_aliases=true *) 445 + let () = 446 + add_test ~name:"yamlrw: documents roundtrip (resolve)" [ bytes ] @@ fun buf -> 447 + (try 448 + let docs = Yamlrw.documents_of_string buf in 449 + let serialized = Yamlrw.documents_to_string ~resolve_aliases:true docs in 450 + (* With resolve_aliases=true, anchors are stripped. Empty scalars with 451 + only anchors become truly empty, which may reduce document count. 452 + We just verify re-parsing doesn't crash. *) 453 + let _ = Yamlrw.documents_of_string serialized in 454 + () 455 + with Yamlrw.Yamlrw_error _ -> ()); 456 + check true 457 + 229 458 let run () = ()