Raw File
test_circular_buffer_fuzzy.ml
(*****************************************************************************)
(*                                                                           *)
(* Open Source License                                                       *)
(* Copyright (c) 2020 Nomadic Labs <contact@nomadic-labs.com>                *)
(*                                                                           *)
(* Permission is hereby granted, free of charge, to any person obtaining a   *)
(* copy of this software and associated documentation files (the "Software"),*)
(* to deal in the Software without restriction, including without limitation *)
(* the rights to use, copy, modify, merge, publish, distribute, sublicense,  *)
(* and/or sell copies of the Software, and to permit persons to whom the     *)
(* Software is furnished to do so, subject to the following conditions:      *)
(*                                                                           *)
(* The above copyright notice and this permission notice shall be included   *)
(* in all copies or substantial portions of the Software.                    *)
(*                                                                           *)
(* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR*)
(* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,  *)
(* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL   *)
(* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER*)
(* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING   *)
(* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER       *)
(* DEALINGS IN THE SOFTWARE.                                                 *)
(*                                                                           *)
(*****************************************************************************)

(** Testing
    -------
    Component:    stdlib
    Invocation:   dune build @src/lib_stdlib/test/runtest
    Subject:      Test the circular buffer with a reference implementation
 *)

(* This test implement a fuzzy testing where we check that the
   `circular_buffer` behaves similarly than a reference implementation
   of the same interface. *)

open Lwt.Syntax
open Qcheck2_helpers

module type S = sig
  type t

  type data

  val create : ?maxlength:int -> ?fresh_buf_size:int -> unit -> t

  (* Write the output of [fill_using] in [data]. *)
  val write :
    maxlen:int ->
    fill_using:(bytes -> int -> int -> (int, 'err) result Lwt.t) ->
    t ->
    (data, 'err) result Lwt.t

  (* Read the value of [data]. The read may be partial if the [data]
     is not fully read. We return the [data] part which was not
     read. *)
  val read : data -> ?len:int -> t -> into:Bytes.t -> offset:int -> data option

  val length : data -> int
end

module Reference : S = struct
  (* There is not buffer, hence the type [t] is not necessary. For
     each [data] we create a new buffer. *)
  type t = unit

  type data = Bytes.t

  let create ?maxlength:_ ?fresh_buf_size:_ () = ()

  let write ~maxlen
      ~(fill_using : data -> int -> int -> (int, 'err) result Lwt.t) () =
    let bytes = Bytes.create maxlen in
    let* written_bytes = fill_using bytes 0 maxlen in
    match written_bytes with
    | Error e -> Lwt.return_error e
    | Ok written_bytes -> Lwt.return_ok (Bytes.sub bytes 0 written_bytes)

  let read data ?(len = Bytes.length data) () ~into ~offset =
    let data_length = Bytes.length data in
    if len > data_length then
      raise (Invalid_argument "Circular_buffer.read: len > (length data).") ;
    Bytes.blit data 0 into offset len ;
    if len = data_length then None
    else Some (Bytes.sub data len (data_length - len))

  let length = Bytes.length
end

(* Check that the circular buffer as the expected interface *)
module Circular_buffer : S = Circular_buffer

(* A scenario will be generate as a sequence of write/read such that
   at each moment, there is more writes than reads. Details are made
   precise in the [pp_op] function below. *)
type op = Write of int * int | Read of int

let pp_op fmt = function
  | Write (write_len, len) ->
      Format.fprintf
        fmt
        "Write %d bytes into a buffer of maxlen %d bytes."
        (min write_len len)
        len
  | Read read_len ->
      (* if [read_len] is too long, we may truncate to the correct size
         depending on the test (see below). *)
      Format.fprintf fmt "Read at most %d bytes." read_len

let pp = Format.pp_print_list ~pp_sep:Format.pp_print_newline pp_op

let write_op =
  let open QCheck2.Gen in
  map (fun (write_len, len) -> Write (write_len, len)) (pair uint8 uint8)

let read_op =
  let open QCheck2.Gen in
  map (fun len -> Read len) uint8

let op = QCheck2.Gen.oneof [write_op; read_op]

(* We record the number of writes to ensure the invariant [nb_writes >
   nb_reads]. *)
let rec ops_gen acc i =
  if i = 0 then acc
  else
    let open QCheck2.Gen in
    ops_gen
      (let* nb_writes, ops = acc in
       let gen = if nb_writes > 0 then op else write_op in
       map
         (fun op ->
           let delta = match op with Write _ -> 1 | Read _ -> -1 in
           (nb_writes + delta, op :: ops))
         gen)
      (i - 1)

(* Scenarios start with a write operation. *)
let ops_gen size =
  let open QCheck2.Gen in
  let gen = ops_gen (map (fun v -> (1, [v])) write_op) size in
  map (fun (_, ops) -> ops) gen

let values =
  let open QCheck2 in
  (* 1000 is a good trade-off between:
     - testing long scenarii using a long sequence of operations
     - quick execution
  *)
  let size_gen = Gen.int_range 0 1000 in
  Gen.(
    let* size = size_gen in
    ops_gen size)

let print_values = Format.asprintf "%a" pp

(* To generate random bytes in a buffer. *)
let random_bytes =
  let state = Random.State.make_self_init () in
  fun size ->
    let buff = Bytes.create size in
    let rec fill_random size offset buff =
      let data = Random.State.int64 state Int64.max_int in
      if size < 8 then
        for i = 0 to size - 1 do
          Bytes.set_int8
            buff
            (offset + i)
            (Int64.to_int (Int64.shift_right data i))
        done
      else (
        Bytes.set_int64_ne buff offset data ;
        fill_random (size - 8) (offset + 8) buff)
    in
    fill_random size 0 buff ;
    buff

let pp_buf fmt buf =
  Format.fprintf fmt "Length: %d@." (Bytes.length buf) ;
  Bytes.iter (fun c -> Format.fprintf fmt "%02x" (Char.code c)) buf

type state =
  | E : {
      implementation : (module S with type t = 'a and type data = 'b);
      internal_state : 'a;
      data_to_be_read : 'b Queue.t;
      mutable partial_read : 'b option;
    }
      -> state

let () =
  (* The module Circular buffer should have the same semantics as the
     reference implementation given in the Reference module. We use
     qcheck to generate write and reads, then check that both
     implementations send the same result. *)
  let fill_using write_len fresh_bytes bytes offset maxlen =
    let len = min write_len maxlen in
    Bytes.blit fresh_bytes 0 bytes offset len ;
    Lwt.return_ok len
  in
  let write_data write_len maxlen bytes_to_write (E state) =
    let (module M) = state.implementation in
    let* data =
      M.write
        ~maxlen
        ~fill_using:(fill_using write_len bytes_to_write)
        state.internal_state
    in
    match data with
    | Error _ -> failwith "read_invalid: fill_using_error"
    | Ok data ->
        Queue.add data state.data_to_be_read ;
        Lwt.return_unit
  in
  let read_data ~without_invalid_argument read_len (E state) =
    let (module M) = state.implementation in
    let data_to_read =
      match state.partial_read with
      | None -> Queue.take state.data_to_be_read
      | Some p ->
          state.partial_read <- None ;
          p
    in
    let len =
      (* to avoid the invalid_argument we take the minimum between the
         size of the data to read and the one generated by the
         [QCheck2] generator. *)
      if without_invalid_argument then min (M.length data_to_read) read_len
      else read_len
    in
    let buf = Bytes.create len in
    try
      state.partial_read <-
        M.read data_to_read ~len state.internal_state ~into:buf ~offset:0 ;
      (false, buf)
    with Invalid_argument _ -> (true, Bytes.create 0)
  in
  let update_state ?(without_invalid_argument = false) left_state right_state
      value =
    match value with
    | Write (write_len, maxlen) ->
        let len = min write_len maxlen in
        let bytes_to_write = random_bytes len in
        let* () = write_data write_len maxlen bytes_to_write left_state in
        let* () = write_data write_len maxlen bytes_to_write right_state in
        Lwt.return_false
    | Read read_len -> (
        try
          let left_has_raised, left_buf =
            read_data ~without_invalid_argument read_len left_state
          in
          let right_has_raised, right_buf =
            read_data ~without_invalid_argument read_len right_state
          in
          if left_has_raised then
            if right_has_raised then Lwt.return true
            else
              QCheck2.Test.fail_report "Different behaviors (invalid_argument)"
          else
            let _ =
              qcheck_eq' ~pp:pp_buf ~expected:left_buf ~actual:right_buf ()
            in
            Lwt.return_false
        with Queue.Empty -> QCheck2.assume_fail ())
  in
  let test_invalid_argument =
    QCheck2.Test.make
      ~name:
        "Stdlib.circular_buffer.equivalence-with-reference-implementation-without-invalid-argument"
      ~print:print_values
      values
      (fun ops ->
        (* To ensure that the number of [write] is greater than the
           number of [read] we reverse the list. *)
        let ops = List.rev ops in
        let left_state =
          E
            {
              implementation = (module Circular_buffer);
              internal_state = Circular_buffer.create ~maxlength:(1 lsl 10) ();
              data_to_be_read = Queue.create ();
              partial_read = None;
            }
        in
        let right_state =
          E
            {
              implementation = (module Reference);
              internal_state = Reference.create ~maxlength:(1 lsl 10) ();
              data_to_be_read = Queue.create ();
              partial_read = None;
            }
        in
        Lwt_main.run
          (Lwt_list.iter_s
             (fun value ->
               let* _ =
                 update_state
                   ~without_invalid_argument:true
                   left_state
                   right_state
                   value
               in
               Lwt.return_unit)
             ops) ;
        true)
  in
  let test_with_reference =
    (* The test below do not try to avoid the `invalid_argument`
       exception. It checks that both implementations raise this
       exception at the same time. *)
    QCheck2.Test.make
      ~name:"Stdlib.circular_buffer.equivalence-with-reference-implementation"
      ~print:print_values
      values
      (fun ops ->
        let ops = List.rev ops in
        let left_state =
          E
            {
              implementation = (module Circular_buffer);
              internal_state = Circular_buffer.create ();
              data_to_be_read = Queue.create ();
              partial_read = None;
            }
        in
        let right_state =
          E
            {
              implementation = (module Reference);
              internal_state = Reference.create ();
              data_to_be_read = Queue.create ();
              partial_read = None;
            }
        in
        let _ =
          Lwt_main.run
            (Lwt_list.fold_left_s
               (fun raised value ->
                 if raised then Lwt.return raised
                 else
                   let* raised' = update_state left_state right_state value in
                   Lwt.return (raised || raised'))
               false
               ops)
        in
        true)
  in
  Alcotest.run
    "Stdlib.circular_buffer"
    [
      ("Invalid argument", qcheck_wrap [test_invalid_argument]);
      ("With reference", qcheck_wrap [test_with_reference]);
    ]
back to top