Bytesrw adapter for Eio
ocaml codec

add tests

+217 -5
+17 -1
.gitignore
··· 1 - _build 1 + # OCaml build artifacts 2 + _build/ 3 + *.install 4 + *.merlin 5 + 6 + # Third-party sources (fetch locally with opam source) 7 + third_party/ 8 + 9 + # Editor and OS files 10 + .DS_Store 11 + *.swp 12 + *~ 13 + .vscode/ 14 + .idea/ 15 + 16 + # Opam local switch 17 + _opam/
+2
bytesrw-eio.opam
··· 14 14 "bytesrw" {>= "0.2"} 15 15 "eio" {>= "1.0"} 16 16 "odoc" {with-doc} 17 + "alcotest" {with-test & >= "1.7.0"} 18 + "eio_main" {with-test} 17 19 ] 18 20 build: [ 19 21 ["dune" "subst"] {dev}
+3 -1
dune-project
··· 19 19 (ocaml (>= 5.0)) 20 20 (bytesrw (>= 0.2)) 21 21 (eio (>= 1.0)) 22 - (odoc :with-doc))) 22 + (odoc :with-doc) 23 + (alcotest (and :with-test (>= 1.7.0))) 24 + (eio_main :with-test)))
+6 -3
src/bytesrw_eio.ml
··· 20 20 ?(slice_length = Bytes.Slice.unix_io_buffer_size) 21 21 (flow : _ Eio.Flow.source) 22 22 : Bytes.Reader.t = 23 - let buf = Bytes.create (Bytes.Slice.check_length slice_length) in 24 - let cstruct = Cstruct.of_bytes buf in 23 + let buf_size = Bytes.Slice.check_length slice_length in 25 24 let read () = 25 + let cstruct = Cstruct.create buf_size in 26 26 match Eio.Flow.single_read flow cstruct with 27 27 | 0 -> Bytes.Slice.eod 28 - | count -> Bytes.Slice.make buf ~first:0 ~length:count 28 + | count -> 29 + let data_cs = Cstruct.sub cstruct 0 count in 30 + let buf = Cstruct.to_bytes data_cs in 31 + Bytes.Slice.make buf ~first:0 ~length:count 29 32 | exception End_of_file -> Bytes.Slice.eod 30 33 in 31 34 Bytes.Reader.make ~slice_length read
+3
test/dune
··· 1 + (test 2 + (name test_bytesrw_eio) 3 + (libraries bytesrw-eio bytesrw eio eio_main alcotest))
+186
test/test_bytesrw_eio.ml
··· 1 + (*--------------------------------------------------------------------------- 2 + Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved. 3 + SPDX-License-Identifier: ISC 4 + ---------------------------------------------------------------------------*) 5 + 6 + (* Test reading from a mock flow *) 7 + let test_reader_basic () = 8 + Eio_main.run @@ fun _env -> 9 + let test_data = "Hello, World!" in 10 + let flow = Eio.Flow.string_source test_data in 11 + let reader = Bytesrw_eio.bytes_reader_of_flow flow in 12 + 13 + (* Read first slice *) 14 + let slice1 = Bytesrw.Bytes.Reader.read reader in 15 + Alcotest.(check bool) "slice is not eod" false (Bytesrw.Bytes.Slice.is_eod slice1); 16 + 17 + let read_data = Bytes.sub_string 18 + (Bytesrw.Bytes.Slice.bytes slice1) 19 + (Bytesrw.Bytes.Slice.first slice1) 20 + (Bytesrw.Bytes.Slice.length slice1) in 21 + Alcotest.(check string) "data matches" test_data read_data; 22 + 23 + (* Next read should be eod *) 24 + let slice2 = Bytesrw.Bytes.Reader.read reader in 25 + Alcotest.(check bool) "second read is eod" true (Bytesrw.Bytes.Slice.is_eod slice2) 26 + 27 + (* Test reading with custom slice length *) 28 + let test_reader_custom_slice_length () = 29 + Eio_main.run @@ fun _env -> 30 + let test_data = "Hello, World!" in 31 + let flow = Eio.Flow.string_source test_data in 32 + let slice_length = 5 in 33 + let reader = Bytesrw_eio.bytes_reader_of_flow ~slice_length flow in 34 + 35 + (* Read should respect slice_length as maximum *) 36 + let slice = Bytesrw.Bytes.Reader.read reader in 37 + Alcotest.(check bool) "slice length <= custom size" true 38 + (Bytesrw.Bytes.Slice.length slice <= slice_length) 39 + 40 + (* Test reading empty flow *) 41 + let test_reader_empty () = 42 + Eio_main.run @@ fun _env -> 43 + let flow = Eio.Flow.string_source "" in 44 + let reader = Bytesrw_eio.bytes_reader_of_flow flow in 45 + 46 + let slice = Bytesrw.Bytes.Reader.read reader in 47 + Alcotest.(check bool) "empty flow returns eod" true (Bytesrw.Bytes.Slice.is_eod slice) 48 + 49 + (* Test writing to a mock flow *) 50 + let test_writer_basic () = 51 + Eio_main.run @@ fun _env -> 52 + let buf = Buffer.create 100 in 53 + let flow = Eio.Flow.buffer_sink buf in 54 + let writer = Bytesrw_eio.bytes_writer_of_flow flow in 55 + 56 + let test_data = "Hello, World!" in 57 + let bytes = Bytes.of_string test_data in 58 + let slice = Bytesrw.Bytes.Slice.make bytes ~first:0 ~length:(Bytes.length bytes) in 59 + 60 + Bytesrw.Bytes.Writer.write writer slice; 61 + 62 + let written = Buffer.contents buf in 63 + Alcotest.(check string) "written data matches" test_data written 64 + 65 + (* Test writing with custom slice length *) 66 + let test_writer_custom_slice_length () = 67 + Eio_main.run @@ fun _env -> 68 + let buf = Buffer.create 100 in 69 + let flow = Eio.Flow.buffer_sink buf in 70 + let slice_length = 8 in 71 + let writer = Bytesrw_eio.bytes_writer_of_flow ~slice_length flow in 72 + 73 + let test_data = "Hello, World!" in 74 + let bytes = Bytes.of_string test_data in 75 + let slice = Bytesrw.Bytes.Slice.make bytes ~first:0 ~length:(Bytes.length bytes) in 76 + 77 + Bytesrw.Bytes.Writer.write writer slice; 78 + 79 + let written = Buffer.contents buf in 80 + Alcotest.(check string) "written data matches regardless of slice_length" test_data written 81 + 82 + (* Test writing eod slice (should be no-op) *) 83 + let test_writer_eod () = 84 + Eio_main.run @@ fun _env -> 85 + let buf = Buffer.create 100 in 86 + let flow = Eio.Flow.buffer_sink buf in 87 + let writer = Bytesrw_eio.bytes_writer_of_flow flow in 88 + 89 + Bytesrw.Bytes.Writer.write writer Bytesrw.Bytes.Slice.eod; 90 + 91 + let written = Buffer.contents buf in 92 + Alcotest.(check string) "eod writes nothing" "" written 93 + 94 + (* Test writing partial slice *) 95 + let test_writer_partial_slice () = 96 + Eio_main.run @@ fun _env -> 97 + let buf = Buffer.create 100 in 98 + let flow = Eio.Flow.buffer_sink buf in 99 + let writer = Bytesrw_eio.bytes_writer_of_flow flow in 100 + 101 + let test_data = "Hello, World!" in 102 + let bytes = Bytes.of_string test_data in 103 + (* Write only "World" *) 104 + let slice = Bytesrw.Bytes.Slice.make bytes ~first:7 ~length:5 in 105 + 106 + Bytesrw.Bytes.Writer.write writer slice; 107 + 108 + let written = Buffer.contents buf in 109 + Alcotest.(check string) "partial slice written" "World" written 110 + 111 + (* Test multiple reads to ensure data isolation - buffers from previous reads 112 + should not be corrupted by subsequent reads *) 113 + let test_reader_multiple_reads () = 114 + Eio_main.run @@ fun _env -> 115 + let test_data = "ABCDEFGHIJ" in (* 10 bytes *) 116 + let flow = Eio.Flow.string_source test_data in 117 + let reader = Bytesrw_eio.bytes_reader_of_flow ~slice_length:5 flow in 118 + 119 + (* Read first 5 bytes *) 120 + let slice1 = Bytesrw.Bytes.Reader.read reader in 121 + let bytes1 = Bytesrw.Bytes.Slice.bytes slice1 in 122 + let data1 = Bytes.sub_string bytes1 123 + (Bytesrw.Bytes.Slice.first slice1) 124 + (Bytesrw.Bytes.Slice.length slice1) in 125 + 126 + (* Read next 5 bytes *) 127 + let slice2 = Bytesrw.Bytes.Reader.read reader in 128 + let data2 = Bytes.sub_string 129 + (Bytesrw.Bytes.Slice.bytes slice2) 130 + (Bytesrw.Bytes.Slice.first slice2) 131 + (Bytesrw.Bytes.Slice.length slice2) in 132 + 133 + (* Critical test: verify first read's data is STILL intact after second read 134 + This would fail if we were reusing buffers or if Cstruct.to_bytes created a view *) 135 + let data1_check = Bytes.sub_string bytes1 136 + (Bytesrw.Bytes.Slice.first slice1) 137 + (Bytesrw.Bytes.Slice.length slice1) in 138 + 139 + Alcotest.(check string) "first read" "ABCDE" data1; 140 + Alcotest.(check string) "second read" "FGHIJ" data2; 141 + Alcotest.(check string) "first read still intact after second" "ABCDE" data1_check 142 + 143 + (* Test round-trip: write then read *) 144 + let test_roundtrip () = 145 + Eio_main.run @@ fun _env -> 146 + let test_data = "Round-trip test data" in 147 + 148 + (* Write to buffer *) 149 + let buf = Buffer.create 100 in 150 + let write_flow = Eio.Flow.buffer_sink buf in 151 + let writer = Bytesrw_eio.bytes_writer_of_flow write_flow in 152 + 153 + let bytes = Bytes.of_string test_data in 154 + let slice = Bytesrw.Bytes.Slice.make bytes ~first:0 ~length:(Bytes.length bytes) in 155 + Bytesrw.Bytes.Writer.write writer slice; 156 + 157 + (* Read back from buffer *) 158 + let read_flow = Eio.Flow.string_source (Buffer.contents buf) in 159 + let reader = Bytesrw_eio.bytes_reader_of_flow read_flow in 160 + 161 + let read_slice = Bytesrw.Bytes.Reader.read reader in 162 + let read_data = Bytes.sub_string 163 + (Bytesrw.Bytes.Slice.bytes read_slice) 164 + (Bytesrw.Bytes.Slice.first read_slice) 165 + (Bytesrw.Bytes.Slice.length read_slice) in 166 + 167 + Alcotest.(check string) "round-trip data matches" test_data read_data 168 + 169 + let () = 170 + Alcotest.run "Bytesrw_eio" [ 171 + "reader", [ 172 + Alcotest.test_case "basic read" `Quick test_reader_basic; 173 + Alcotest.test_case "custom slice length" `Quick test_reader_custom_slice_length; 174 + Alcotest.test_case "empty flow" `Quick test_reader_empty; 175 + Alcotest.test_case "multiple reads data isolation" `Quick test_reader_multiple_reads; 176 + ]; 177 + "writer", [ 178 + Alcotest.test_case "basic write" `Quick test_writer_basic; 179 + Alcotest.test_case "custom slice length" `Quick test_writer_custom_slice_length; 180 + Alcotest.test_case "eod write" `Quick test_writer_eod; 181 + Alcotest.test_case "partial slice" `Quick test_writer_partial_slice; 182 + ]; 183 + "integration", [ 184 + Alcotest.test_case "round-trip" `Quick test_roundtrip; 185 + ]; 186 + ]