Giter VIP home page Giter VIP logo

qcstm's Introduction

QCSTM: A Simple State-Machine Framework for OCaml Based on QCheck

Build Status

This library implements a simple, typed state machine framework for property-based testing of imperative code. Tests are described by (a generator of) symbolic commands and two command interpreters over an abstract model and the system under test.

The library requires a recent installation of both OCaml and the QCheck framework.

State-machine frameworks for other languages include:

QCSTM takes inspiration from the commercial Erlang state machine framework from Quviq and from ScalaCheck's state machine framework.

The library is formulated as an OCaml functor. As its argument, the functor expects a module specifying 3 types:

  • cmd: the type of commands
  • state: the type of model's state
  • sut: the type of the system under test

In addition the user has to provide:

  • arb_cmd: a generator of commands. It accepts a state parameter to enable state-dependent command generation.
  • init_state and next_state: specifies the initial state and the (single-step) state transition function of the model.
  • run_cmd: interprets a command over the system under test and returns a Boolean, indicating whether the execution went well, and whether any returned value agrees with the model's result.
  • init_sut and cleanup: specificies how to initialize and clean up after the system under test.
  • precond: specifies preconditions for each command. This is useful, e.g., to prevent the shrinker from breaking invariants when minimizing counterexamples.

In return, the framework provides a generator of cmd lists (incl. a shrinker) as well as an agreement test between the model and system under test.

Installation

With opam this should be as simple as opam install qcstm.

You can also install from source assuming you have ocamlbuild, ocamlfind and a not-too-ancient qcheck installed, by issuing:

  make
  make install

To uninstall with opam just run opam remove qcstm. To uninstall from a source installation run make uninstall from the souce directory.

An example

Consider the following example (available in examples/counter.ml) that tests an int ref against a model consisting of a single int:

  open QCheck
  
  module CConf =
  struct
    type cmd =
      | Incr
      | Decr
      | Set of int
      | Deref [@@deriving show { with_path = false }]
    type state = int
    type sut = int ref
  
    let arb_cmd _ =
      let int_gen = Gen.oneof [Gen.int; Gen.nat] in
      QCheck.make ~print:show_cmd
        (Gen.oneof [Gen.return Incr;
                    Gen.return Decr;
                    Gen.map (fun i -> Set i) int_gen;
                    Gen.return Deref])
  
    let init_state  = 0
    let init_sut () = ref 0
    let cleanup _   = ()
  
    let next_state c s = match c with
      | Incr  -> s+1
      | Decr  -> s-1
      | Set i -> if i<>1213 then i else s (* an artificial fault *)
      | Deref -> s
  
    let run_cmd c s r = match c with
      | Incr  -> (incr r; true)
      | Decr  -> (decr r; true)
      | Set i -> (r := i; true)
      | Deref -> !r = s
        
    let precond _ _ = true
  end
  module CT = QCSTM.Make(CConf)
  ;;
  QCheck_runner.run_tests ~verbose:true [CT.agree_test ~count:10_000 ~name:"ref-model agreement"]

Here we provide a type of four different kinds of commands as well as a generator of these. init_state and init_sut specifies the initial states of both the model and the system under test.

next_state and run_cmd interpret the four different commands over the model and the system under test, respectively. Since we can only observe references through a dereferencing operation, this is the only operation comparing the outputs from the two.

To test whether the testsuite works as expected, we inject a bug in the model that ignores setting the reference when the argument is 1213.

Finally we can compile the state machine model and run the tests. Depending on the underlying random number generator, this may or may not catch the model's bug in a given run:

  $ make counter
  ocamlbuild -use-ocamlfind -package qcheck,qCSTM,ppx_deriving.show examples/counter.cma examples/counter.native
  Finished, 8 targets (3 cached) in 00:00:00.
  $ ./counter.native 
  random seed: 272260055
  generated error  fail  pass / total     time test name
  [✓] 10000     0     0 10000 / 10000     1.0s ref-model agreement
  ================================================================================
  success (ran 1 tests)
  $ ./counter.native 
  random seed: 36511368
  generated error  fail  pass / total     time test name
  [✗]  2032     0     1  2031 / 10000     1.2s ref-model agreement
  
  --- Failure --------------------------------------------------------------------
  
  Test ref-model agreement failed (14 shrink steps):
  
  [(Set 1213); Deref]
  ================================================================================
  failure (1 tests failed, 0 tests errored, ran 1 tests)

A number of additional examples are provided in the examples directory, including examples of testing OCaml code:

There are also examples of testing C code:

Finally there are a few puzzle examples where the command generator is (mis)used to search for a solution:

qcstm's People

Contributors

c-cube avatar jmid avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

qcstm's Issues

Document how C-code examples can be explored interactively from `utop` or a custom top-level

On a related note, I just remembered how I spent some effort trying to mimic the kind of interaction that John Hughes does during live coding in his recorded talks.

For putget.ml which is dynamically linked, after a make putget (or make examples) the old Makefile allowed me to start up utop for the following interaction:

utop # #require "qcheck";;
utop # #require "qcstm";;
utop # #require "ppx_deriving";;
utop # #require "ppx_deriving.show";;
utop # #directory "_build/examples";;
utop # #load "putget.cmo";;

utop # let (stub,put,get) = Putget.PGConf.init_sut();;
val stub : Dl.library = <abstr>
val put : int -> unit = <fun>
val get : unit -> int = <fun>
utop # get ();;
- : int = 0
utop # put 42;;
- : unit = ()
utop # get ();;
- : int = 42

(** Now trigger the C-side bug: *)
utop # put 5538;;
- : unit = ()
utop # get ();;
- : int = 5538
utop # put 0;;
- : unit = ()
utop # get ();;
- : int = 5538

(** Unload library and reload one with a clean state: *)
utop # Putget.PGConf.cleanup (stub,put,get);;
- : unit = ()
utop # let (stub,put,get) = Putget.PGConf.init_sut();;
val stub : Dl.library = <abstr>
val put : int -> unit = <fun>
val get : unit -> int = <fun>
utop # get ();;
- : int = 0

There's something wonderful about exploring C's weirdness from within the comfort of utop with strong types and tab-completiion 😅 (I advice against calling get or put after the unload, but before reloading...)

For statically linked libraries (the stdio and cq examples) I had to resort to a dedicated toplevel, so instead run, e.g, ledit ./cq.top:

#use "topfind";;
#require "ppx_deriving";;
#require "ppx_deriving.show";;
#require "qcstm";;
#directory "_build/examples";;

# let q = Cq.alloc 2;;
val q : Cq.queue = (void*) 0x55fce1945f80     (* strap yourself in... *)
# Cq.put;;
- : Cq.queue -> int -> unit = <fun>
# Cq.put q 5;;
- : unit = ()
# Cq.size q;;
- : int = 1
# Cq.get q;;
- : int = 5
# Cq.get q;;
- : int = 22012      (* ooops, uninitialized memory... *)
# Cq.get q;;
- : int = 0              (* hmmm *)
# Cq.get q;;
- : int = 5

As I hope you can see that this can be useful, e.g., to explore some observed behaviour more in depth.
However, that this was the intention with the obscure Makefile rules was not apparent for anyone but me... 😅

I therefore think

  • the above should be documented (this comment is at least a start)
  • the dune-port should preferably allow the same kind of interaction.

Can you recreate the putget interaction after a dune build (a path should probably be adjusted)?
Dune supports dune utop which would allow a much nicer UX in the stdio and cq examples.
I don't know how much work it will be to make it work though... 🤷‍♂️

Originally posted by @jmid in #2 (comment)

API triggers `unerasable-optional-argument` warning in OCaml 4.12.0

@tmcgilchrist reported how the API triggers unerasable-optional-argument errors/warnings in OCaml 4.12.0
here #2 (comment)

File "src/qCSTM.ml", line 94, characters 25-35:
15
94 |   let consistency_test ?(count=1000) ~name =
16
                              ^^^^^^^^^^
17
Error (warning 16 [unerasable-optional-argument]): this optional argument cannot be erased.
18
File "src/qCSTM.ml", line 120, characters 19-29:
19
120 |   let agree_test ?(count=1000) ~name =
20
                         ^^^^^^^^^^
21
Error (warning 16 [unerasable-optional-argument]): this optional argument cannot be erased.

One potential fix is to swap both count and name to both be labelled arguments (#2 (review)):

Edit: as for the unerasable-optional-argument and OCaml 4.12, I think the API should just make count labelled like name, but that change is probably also best handled separately...

@tmcgilchrist suggests another option of letting the functions take a unit as the last parameter (#2 (comment)):

@jmid one option for fixing 4.12 is adding unit to the functions see
tmcgilchrist/qcstm@fb226ad

I haven't used this enough to tell whether making ~count a label, requiring the user to supply a value, vs appending () is more ergonomic.

I suppose a third option is to disable the error/warning (-w -16?)... 😅

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.