Bytesrw adapter for Eio
ocaml codec

add tests

+17 -1
.gitignore
···
-
_build
+
# OCaml build artifacts
+
_build/
+
*.install
+
*.merlin
+
+
# Third-party sources (fetch locally with opam source)
+
third_party/
+
+
# Editor and OS files
+
.DS_Store
+
*.swp
+
*~
+
.vscode/
+
.idea/
+
+
# Opam local switch
+
_opam/
+2
bytesrw-eio.opam
···
"bytesrw" {>= "0.2"}
"eio" {>= "1.0"}
"odoc" {with-doc}
+
"alcotest" {with-test & >= "1.7.0"}
+
"eio_main" {with-test}
]
build: [
["dune" "subst"] {dev}
+3 -1
dune-project
···
(ocaml (>= 5.0))
(bytesrw (>= 0.2))
(eio (>= 1.0))
-
(odoc :with-doc)))
+
(odoc :with-doc)
+
(alcotest (and :with-test (>= 1.7.0)))
+
(eio_main :with-test)))
+6 -3
src/bytesrw_eio.ml
···
?(slice_length = Bytes.Slice.unix_io_buffer_size)
(flow : _ Eio.Flow.source)
: Bytes.Reader.t =
-
let buf = Bytes.create (Bytes.Slice.check_length slice_length) in
-
let cstruct = Cstruct.of_bytes buf in
+
let buf_size = Bytes.Slice.check_length slice_length in
let read () =
+
let cstruct = Cstruct.create buf_size in
match Eio.Flow.single_read flow cstruct with
| 0 -> Bytes.Slice.eod
-
| count -> Bytes.Slice.make buf ~first:0 ~length:count
+
| count ->
+
let data_cs = Cstruct.sub cstruct 0 count in
+
let buf = Cstruct.to_bytes data_cs in
+
Bytes.Slice.make buf ~first:0 ~length:count
| exception End_of_file -> Bytes.Slice.eod
in
Bytes.Reader.make ~slice_length read
+3
test/dune
···
+
(test
+
(name test_bytesrw_eio)
+
(libraries bytesrw-eio bytesrw eio eio_main alcotest))
+186
test/test_bytesrw_eio.ml
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
(* Test reading from a mock flow *)
+
let test_reader_basic () =
+
Eio_main.run @@ fun _env ->
+
let test_data = "Hello, World!" in
+
let flow = Eio.Flow.string_source test_data in
+
let reader = Bytesrw_eio.bytes_reader_of_flow flow in
+
+
(* Read first slice *)
+
let slice1 = Bytesrw.Bytes.Reader.read reader in
+
Alcotest.(check bool) "slice is not eod" false (Bytesrw.Bytes.Slice.is_eod slice1);
+
+
let read_data = Bytes.sub_string
+
(Bytesrw.Bytes.Slice.bytes slice1)
+
(Bytesrw.Bytes.Slice.first slice1)
+
(Bytesrw.Bytes.Slice.length slice1) in
+
Alcotest.(check string) "data matches" test_data read_data;
+
+
(* Next read should be eod *)
+
let slice2 = Bytesrw.Bytes.Reader.read reader in
+
Alcotest.(check bool) "second read is eod" true (Bytesrw.Bytes.Slice.is_eod slice2)
+
+
(* Test reading with custom slice length *)
+
let test_reader_custom_slice_length () =
+
Eio_main.run @@ fun _env ->
+
let test_data = "Hello, World!" in
+
let flow = Eio.Flow.string_source test_data in
+
let slice_length = 5 in
+
let reader = Bytesrw_eio.bytes_reader_of_flow ~slice_length flow in
+
+
(* Read should respect slice_length as maximum *)
+
let slice = Bytesrw.Bytes.Reader.read reader in
+
Alcotest.(check bool) "slice length <= custom size" true
+
(Bytesrw.Bytes.Slice.length slice <= slice_length)
+
+
(* Test reading empty flow *)
+
let test_reader_empty () =
+
Eio_main.run @@ fun _env ->
+
let flow = Eio.Flow.string_source "" in
+
let reader = Bytesrw_eio.bytes_reader_of_flow flow in
+
+
let slice = Bytesrw.Bytes.Reader.read reader in
+
Alcotest.(check bool) "empty flow returns eod" true (Bytesrw.Bytes.Slice.is_eod slice)
+
+
(* Test writing to a mock flow *)
+
let test_writer_basic () =
+
Eio_main.run @@ fun _env ->
+
let buf = Buffer.create 100 in
+
let flow = Eio.Flow.buffer_sink buf in
+
let writer = Bytesrw_eio.bytes_writer_of_flow flow in
+
+
let test_data = "Hello, World!" in
+
let bytes = Bytes.of_string test_data in
+
let slice = Bytesrw.Bytes.Slice.make bytes ~first:0 ~length:(Bytes.length bytes) in
+
+
Bytesrw.Bytes.Writer.write writer slice;
+
+
let written = Buffer.contents buf in
+
Alcotest.(check string) "written data matches" test_data written
+
+
(* Test writing with custom slice length *)
+
let test_writer_custom_slice_length () =
+
Eio_main.run @@ fun _env ->
+
let buf = Buffer.create 100 in
+
let flow = Eio.Flow.buffer_sink buf in
+
let slice_length = 8 in
+
let writer = Bytesrw_eio.bytes_writer_of_flow ~slice_length flow in
+
+
let test_data = "Hello, World!" in
+
let bytes = Bytes.of_string test_data in
+
let slice = Bytesrw.Bytes.Slice.make bytes ~first:0 ~length:(Bytes.length bytes) in
+
+
Bytesrw.Bytes.Writer.write writer slice;
+
+
let written = Buffer.contents buf in
+
Alcotest.(check string) "written data matches regardless of slice_length" test_data written
+
+
(* Test writing eod slice (should be no-op) *)
+
let test_writer_eod () =
+
Eio_main.run @@ fun _env ->
+
let buf = Buffer.create 100 in
+
let flow = Eio.Flow.buffer_sink buf in
+
let writer = Bytesrw_eio.bytes_writer_of_flow flow in
+
+
Bytesrw.Bytes.Writer.write writer Bytesrw.Bytes.Slice.eod;
+
+
let written = Buffer.contents buf in
+
Alcotest.(check string) "eod writes nothing" "" written
+
+
(* Test writing partial slice *)
+
let test_writer_partial_slice () =
+
Eio_main.run @@ fun _env ->
+
let buf = Buffer.create 100 in
+
let flow = Eio.Flow.buffer_sink buf in
+
let writer = Bytesrw_eio.bytes_writer_of_flow flow in
+
+
let test_data = "Hello, World!" in
+
let bytes = Bytes.of_string test_data in
+
(* Write only "World" *)
+
let slice = Bytesrw.Bytes.Slice.make bytes ~first:7 ~length:5 in
+
+
Bytesrw.Bytes.Writer.write writer slice;
+
+
let written = Buffer.contents buf in
+
Alcotest.(check string) "partial slice written" "World" written
+
+
(* Test multiple reads to ensure data isolation - buffers from previous reads
+
should not be corrupted by subsequent reads *)
+
let test_reader_multiple_reads () =
+
Eio_main.run @@ fun _env ->
+
let test_data = "ABCDEFGHIJ" in (* 10 bytes *)
+
let flow = Eio.Flow.string_source test_data in
+
let reader = Bytesrw_eio.bytes_reader_of_flow ~slice_length:5 flow in
+
+
(* Read first 5 bytes *)
+
let slice1 = Bytesrw.Bytes.Reader.read reader in
+
let bytes1 = Bytesrw.Bytes.Slice.bytes slice1 in
+
let data1 = Bytes.sub_string bytes1
+
(Bytesrw.Bytes.Slice.first slice1)
+
(Bytesrw.Bytes.Slice.length slice1) in
+
+
(* Read next 5 bytes *)
+
let slice2 = Bytesrw.Bytes.Reader.read reader in
+
let data2 = Bytes.sub_string
+
(Bytesrw.Bytes.Slice.bytes slice2)
+
(Bytesrw.Bytes.Slice.first slice2)
+
(Bytesrw.Bytes.Slice.length slice2) in
+
+
(* Critical test: verify first read's data is STILL intact after second read
+
This would fail if we were reusing buffers or if Cstruct.to_bytes created a view *)
+
let data1_check = Bytes.sub_string bytes1
+
(Bytesrw.Bytes.Slice.first slice1)
+
(Bytesrw.Bytes.Slice.length slice1) in
+
+
Alcotest.(check string) "first read" "ABCDE" data1;
+
Alcotest.(check string) "second read" "FGHIJ" data2;
+
Alcotest.(check string) "first read still intact after second" "ABCDE" data1_check
+
+
(* Test round-trip: write then read *)
+
let test_roundtrip () =
+
Eio_main.run @@ fun _env ->
+
let test_data = "Round-trip test data" in
+
+
(* Write to buffer *)
+
let buf = Buffer.create 100 in
+
let write_flow = Eio.Flow.buffer_sink buf in
+
let writer = Bytesrw_eio.bytes_writer_of_flow write_flow in
+
+
let bytes = Bytes.of_string test_data in
+
let slice = Bytesrw.Bytes.Slice.make bytes ~first:0 ~length:(Bytes.length bytes) in
+
Bytesrw.Bytes.Writer.write writer slice;
+
+
(* Read back from buffer *)
+
let read_flow = Eio.Flow.string_source (Buffer.contents buf) in
+
let reader = Bytesrw_eio.bytes_reader_of_flow read_flow in
+
+
let read_slice = Bytesrw.Bytes.Reader.read reader in
+
let read_data = Bytes.sub_string
+
(Bytesrw.Bytes.Slice.bytes read_slice)
+
(Bytesrw.Bytes.Slice.first read_slice)
+
(Bytesrw.Bytes.Slice.length read_slice) in
+
+
Alcotest.(check string) "round-trip data matches" test_data read_data
+
+
let () =
+
Alcotest.run "Bytesrw_eio" [
+
"reader", [
+
Alcotest.test_case "basic read" `Quick test_reader_basic;
+
Alcotest.test_case "custom slice length" `Quick test_reader_custom_slice_length;
+
Alcotest.test_case "empty flow" `Quick test_reader_empty;
+
Alcotest.test_case "multiple reads data isolation" `Quick test_reader_multiple_reads;
+
];
+
"writer", [
+
Alcotest.test_case "basic write" `Quick test_writer_basic;
+
Alcotest.test_case "custom slice length" `Quick test_writer_custom_slice_length;
+
Alcotest.test_case "eod write" `Quick test_writer_eod;
+
Alcotest.test_case "partial slice" `Quick test_writer_partial_slice;
+
];
+
"integration", [
+
Alcotest.test_case "round-trip" `Quick test_roundtrip;
+
];
+
]