Giter VIP home page Giter VIP logo

qcheck's Introduction

QCheck

QuickCheck inspired property-based testing for OCaml, and combinators to generate random values to run tests on.

build

The documentation can be found here. This library spent some time in qtest, but is now standalone again!

To construct advanced random generators, the following libraries might be of interest:

Jan Midtgaard (@jmid) has a lecture about property-based testing that relies on QCheck.

Use

See the documentation. I also wrote a blog post that explains how to use it and some design choices; however, be warned that the API changed in lots of small ways (in the right direction, I hope) so the code will not work any more. An Introduction to the Library is an updated version of the blog post’s examples.

Build

$ make

You can use opam:

$ opam install qcheck

License

The code is now released under the BSD license.

An Introduction to the Library

First, let’s see a few tests. Let’s open a toplevel (e.g. utop) and type the following to load QCheck:

#require "qcheck";;
Note
alternatively, it is now possible to locally do: dune utop src to load qcheck.

List Reverse is Involutive

We write a random test for checking that List.rev (List.rev l) = l for any list l:

let test =
  QCheck.Test.make ~count:1000 ~name:"list_rev_is_involutive"
   QCheck.(list small_nat)
   (fun l -> List.rev (List.rev l) = l);;

(* we can check right now the property... *)
QCheck.Test.check_exn test;;

In the above example, we applied the combinator list to the random generator small_nat (ints between 0 and 100), to create a new generator of lists of random integers. These builtin generators come with printers and shrinkers which are handy for outputting and minimizing a counterexample when a test fails.

Consider the buggy property List.rev l = l:

let test =
  QCheck.Test.make ~count:1000 ~name:"my_buggy_test"
   QCheck.(list small_nat)
   (fun l -> List.rev l = l);;

When we run this test we are presented with a counterexample:

# QCheck.Test.check_exn test;;
Exception:
QCheck.Test.Test_fail ("my_buggy_test", ["[0; 1] (after 23 shrink steps)"]).

In this case QCheck found the minimal counterexample [0;1] to the property List.rev l = l and it spent 23 steps shrinking it.

Now, let’s run the buggy test with a decent runner that will print the results nicely (the exact output will change at each run, because of the random seed):

# QCheck_runner.run_tests [test];;

--- Failure --------------------------------------------------------------------

Test my_buggy_test failed (10 shrink steps):

[0; 1]
================================================================================
failure (1 tests failed, 0 tests errored, ran 1 tests)
- : int = 1

For an even nicer output QCheck_runner.run_tests also accepts an optional parameter ~verbose:true.

Mirrors and Trees

QCheck provides many useful combinators to write generators, especially for recursive types, algebraic types, and tuples.

Let’s see how to generate random trees:

type tree = Leaf of int | Node of tree * tree

let leaf x = Leaf x
let node x y = Node (x,y)

let tree_gen = QCheck.Gen.(sized @@ fix
  (fun self n -> match n with
    | 0 -> map leaf nat
    | n ->
      frequency
        [1, map leaf nat;
         2, map2 node (self (n/2)) (self (n/2))]
    ));;

(* generate a few trees, just to check what they look like: *)
QCheck.Gen.generate ~n:20 tree_gen;;

let arbitrary_tree =
  let open QCheck.Iter in
  let rec print_tree = function
    | Leaf i -> "Leaf " ^ (string_of_int i)
    | Node (a,b) -> "Node (" ^ (print_tree a) ^ "," ^ (print_tree b) ^ ")"
  in
  let rec shrink_tree = function
    | Leaf i -> QCheck.Shrink.int i >|= leaf
    | Node (a,b) ->
      of_list [a;b]
      <+>
      (shrink_tree a >|= fun a' -> node a' b)
      <+>
      (shrink_tree b >|= fun b' -> node a b')
  in
  QCheck.make tree_gen ~print:print_tree ~shrink:shrink_tree;;

Here we write a generator of random trees, tree_gen, using the fix combinator. fix is sized (it is a function from int to a random generator; in particular for size 0 it returns only leaves). The sized combinator first generates a random size, and then applies its argument to this size.

Other combinators include monadic abstraction, lifting functions, generation of lists, arrays, and a choice function.

Then, we define arbitrary_tree, a tree QCheck.arbitrary value, which contains everything needed for testing on trees:

  • a random generator (mandatory), weighted with frequency to increase the chance of generating deep trees

  • a printer (optional), very useful for printing counterexamples

  • a shrinker (optional), very useful for trying to reduce big counterexamples to small counterexamples that are usually more easy to understand.

The above shrinker strategy is to

  • reduce the integer leaves, and

  • substitute an internal Node with either of its subtrees or by splicing in a recursively shrunk subtree.

A range of combinators in QCheck.Shrink and QCheck.Iter are available for building shrinking functions.

We can write a failing test using this generator to see the printer and shrinker in action:

let rec mirror_tree (t:tree) : tree = match t with
  | Leaf _ -> t
  | Node (a,b) -> node (mirror_tree b) (mirror_tree a);;

let test_buggy =
  QCheck.Test.make ~name:"buggy_mirror" ~count:200
    arbitrary_tree (fun t -> t = mirror_tree t);;

QCheck_runner.run_tests [test_buggy];;

This test fails with:

--- Failure --------------------------------------------------------------------

Test mirror_buggy failed (6 shrink steps):

Node (Leaf 0,Leaf 1)
================================================================================
failure (1 tests failed, 0 tests errored, ran 1 tests)
- : int = 1

With the (new found) understanding that mirroring a tree changes its structure, we can formulate another property that involves sequentializing its elements in a traversal:

let tree_infix (t:tree): int list =
  let rec aux acc t = match t with
    | Leaf i -> i :: acc
    | Node (a,b) ->
      aux (aux acc b) a
  in
  aux [] t;;

let test_mirror =
  QCheck.Test.make ~name:"mirror_tree" ~count:200
    arbitrary_tree
    (fun t -> List.rev (tree_infix t) = tree_infix (mirror_tree t));;

QCheck_runner.run_tests [test_mirror];;

Preconditions

The functions QCheck.assume and QCheck.(=⇒) can be used for tests with preconditions. For instance, List.hd l :: List.tl l = l only holds for non-empty lists. Without the precondition, the property is false and will even raise an exception in some cases.

let test_hd_tl =
  QCheck.(Test.make
    (list int) (fun l ->
      assume (l <> []);
      l = List.hd l :: List.tl l));;

QCheck_runner.run_tests [test_hd_tl];;

Long tests

It is often useful to have two version of a testsuite: a short one that runs reasonably fast (so that it is effectively run each time a projet is built), and a long one that might be more exhaustive (but whose running time makes it impossible to run at each build). To that end, each test has a 'long' version. In the long version of a test, the number of tests to run is multiplied by the ~long_factor argument of QCheck.Test.make.

Runners

The module QCheck_runner defines several functions to run tests, including compatibility with OUnit. The easiest one is probably run_tests, but if you write your tests in a separate executable you can also use run_tests_main which parses command line arguments and exits with 0 in case of success, or an error number otherwise.

Integration within OUnit

OUnit is a popular unit-testing framework for OCaml. QCheck provides a sub-library qcheck-ounit with some helpers, in QCheck_ounit, to convert its random tests into OUnit tests that can be part of a wider test-suite.

let passing =
  QCheck.Test.make ~count:1000
    ~name:"list_rev_is_involutive"
    QCheck.(list small_nat)
    (fun l -> List.rev (List.rev l) = l);;

let failing =
  QCheck.Test.make ~count:10
    ~name:"fail_sort_id"
    QCheck.(list small_nat)
    (fun l -> l = List.sort compare l);;

let _ =
  let open OUnit in
  run_test_tt_main
    ("tests" >:::
       List.map QCheck_ounit.to_ounit_test [passing; failing])
Note
the package qcheck contains the module QCheck_runner which contains both custom runners and OUnit-based runners.

Integration within alcotest

Alcotest is a simple and colorful test framework for OCaml. QCheck now provides a sub-library qcheck-alcotest to easily integrate into an alcotest test suite:

let passing =
  QCheck.Test.make ~count:1000
    ~name:"list_rev_is_involutive"
    QCheck.(list small_int)
    (fun l -> List.rev (List.rev l) = l);;

let failing =
  QCheck.Test.make ~count:10
    ~name:"fail_sort_id"
    QCheck.(list small_int)
    (fun l -> l = List.sort compare l);;


let () =
  let suite =
    List.map QCheck_alcotest.to_alcotest
      [ passing; failing]
  in
  Alcotest.run "my test" [
    "suite", suite
  ]

Integration within Rely

Rely is a Jest-inspire native reason testing framework. @reason-native/qcheck-rely is available via NPM and provides matchers for the easy use of qCheck within Rely.

open TestFramework;
open QCheckRely;

let {describe} = extendDescribe(QCheckRely.Matchers.matchers);

describe("qcheck-rely", ({test}) => {
  test("passing test", ({expect}) => {
    let passing =
      QCheck.Test.make(
        ~count=1000,
        ~name="list_rev_is_involutive",
        QCheck.(list(small_int)),
        l =>
        List.rev(List.rev(l)) == l
      );
    expect.ext.qCheckTest(passing);
    ();
  });
  test("failing test", ({expect}) => {
    let failing =
      QCheck.Test.make(
        ~count=10, ~name="fail_sort_id", QCheck.(list(small_int)), l =>
        l == List.sort(compare, l)
      );

    expect.ext.qCheckTest(failing);
    ();
  });
});

Deriver

A ppx_deriver is provided to derive QCheck generators from a type declaration.

type tree = Leaf of int | Node of tree * tree
[@@deriving qcheck]

See the according README for more information and examples.

Compatibility notes

Starting with 0.9, the library is split into several components:

  • qcheck-core depends only on unix and bytes. It contains the module QCheck and a QCheck_base_runner module with our custom runners.

  • qcheck-ounit provides an integration layer for OUnit

  • qcheck provides a compatibility API with older versions of qcheck, using both qcheck-core and qcheck-ounit. It provides QCheck_runner which is similar to older versions and contains both custom and Ounit-based runners.

  • qcheck-alcotest provides an integration layer with alcotest

Normally, for contributors, opam pin https://github.com/c-cube/qcheck will pin all these packages.

qcheck's People

Contributors

alopezz avatar bandersongit avatar bobot avatar c-cube avatar favonia avatar fourchaux avatar gasche avatar gbury avatar ghilesz avatar hasantouma avatar jamesjer avatar jmid avatar kit-ty-kate avatar n-osborne avatar oliviernicole avatar rgrinberg avatar shym avatar sir4ur0n avatar tmcgilchrist avatar ulugbekna avatar vch9 avatar xvilka avatar yawaramin 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  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  avatar  avatar  avatar  avatar

qcheck's Issues

A positive feature request/idea: a trophy chest

To underline to QCheck newcomers that the library is practically useful, it would be nice to document a (growing) list of bugs found using it. Ideally this could be integrated into the README file on the front page. This idea is heavily inspired by Stephen Dolan's "Bugs found with Crowbar" stedolan/crowbar#2 (which is itself a very interesting fuzz-testing/QuickCheck hybrid).
The above is limited to OPAM packages, which is perhaps a natural requirement to list entries.

I can personally contribute with the ptrees bug and a number of OCaml compiler bugs to such a list, but I am sure there are many other QCheck trophies out there :-)

(On a related note: I plan to contribute a bit of polishing of the README documentation over the next couple of days)

Allow short and long tests

When writing automated tests, is is recommended to have two version of a testsuite: a short one that runs reasonably fast (so that it is effectively run each time a projet is built), and a long one that might be more exhaustive (but whose running time makes it impossible to run at each build).

To that end, it would be useful to have a mechanism to allow that for qcheck tests (particularly for the functions that generate ounit tests). From the qcheck user point of view, maybe something like ~long_count argument to provide with ~count when creating tests ?

Should pint and neg_int include zero?

My understanding is that 0 should be excluded from both sets, but the current implementations include them. The documentation is unfortunately not explicit, so we should either fix or specify the documentation?

Reduce output lines for statistics

I believe we can reduce the number of redundant lines printed for statistics.
Consider for example:

# let t gen name = Test.make ~name:name (add_stat ("mod10", fun i -> i mod 10) (add_stat ("size", fun i -> i) gen)) (fun _ -> true);;
val t : int arbitrary -> string -> Test.t = <fun>
# QCheck_runner.run_tests [t small_int "small_int dist"; t int "int dist"];;
random seed: 106933173

+++ Stat ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Stat for test small_int dist:

stats mod10:
  num: 100, avg: 4.70, stddev: 2.73, median 4, min 0, max 9
   0: #######################                                           8
   1: #######################                                           8
   2: #######################                                           8
   3: ####################                                              7
   4: #######################################################          19
   5: ###############################                                  11
   6: ##################################                               12
   7: #################                                                 6
   8: ##########################                                        9
   9: ##################################                               12

+++ Stat ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Stat for test small_int dist:

stats size:
  num: 100, avg: 13.30, stddev: 22.10, median 5, min 0, max 99
    0..  4: #######################################################          42
    5..  9: #####################################################            41
   10.. 14: #                                                                 1
   15.. 19:                                                                   0
   20.. 24: #                                                                 1
   25.. 29:                                                                   0
   30.. 34: ###                                                               3
   35.. 39: ###                                                               3
   40.. 44:                                                                   0
   45.. 49:                                                                   0
   50.. 54:                                                                   0
   55.. 59: #                                                                 1
   60.. 64: #                                                                 1
   65.. 69:                                                                   0
   70.. 74: ##                                                                2
   75.. 79: ##                                                                2
   80.. 84:                                                                   0
   85.. 89: #                                                                 1
   90.. 94:                                                                   0
   95.. 99: ##                                                                2

+++ Stat ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Stat for test int dist:

stats mod10:
  num: 100, avg: 0.18, stddev: 5.53, median 0, min -9, max 9
  -9: ##############################                                    6
  -8: ##############################                                    6
  -7: ##############################                                    6
  -6: ##########                                                        2
  -5: ###############                                                   3
  -4: ##############################                                    6
  -3: ##############################                                    6
  -2: #########################                                         5
  -1: ###############                                                   3
   0: #######################################################          11
   1: ###############                                                   3
   2: ##############################                                    6
   3: ####################                                              4
   4: ###############                                                   3
   5: ###################################                               7
   6: #############################################                     9
   7: ##########                                                        2
   8: ##############################                                    6
   9: ##############################                                    6

+++ Stat ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Stat for test int dist:

stats size:
  num: 100, avg: 99373710335244560.00, stddev: 2551325850420775936.00, median 168859703835258516, min -4547337154261407245, max 4545991166451554445
  -4547337154261407245..-4092670738225759118: ########################                                          4
  -4092670738225759117..-3638004322190110990: ####################################                              6
  -3638004322190110989..-3183337906154462862: ##############################                                    5
  -3183337906154462861..-2728671490118814734: ##############################                                    5
  -2728671490118814733..-2274005074083166606: ############                                                      2
  -2274005074083166605..-1819338658047518478: ##################                                                3
  -1819338658047518477..-1364672242011870350: ############                                                      2
  -1364672242011870349.. -910005825976222222: #######################################################           9
   -910005825976222221.. -455339409940574094: ##########################################                        7
   -455339409940574093..    -672993904925966: ########################                                          4
      -672993904925965..  453993422130722162: #######################################################           9
    453993422130722163..  908659838166370290: ####################################                              6
    908659838166370291.. 1363326254202018418: ##########################################                        7
   1363326254202018419.. 1817992670237666546: ##################                                                3
   1817992670237666547.. 2272659086273314674: ########################                                          4
   2272659086273314675.. 2727325502308962802: ############                                                      2
   2727325502308962803.. 3181991918344610930: ########################                                          4
   3181991918344610931.. 3636658334380259058: ################################################                  8
   3636658334380259059.. 4091324750415907186: ####################################                              6
   4091324750415907187.. 4545991166451555314: ########################                                          4
================================================================================
success (ran 2 tests)

Notice how the test name is (somewhat redundantly) printed for each test and how +++ Stat +++... is printed for each of them. For a complex generator with multiple statistics this adds up. Ideally I would like to see Stats for test foo followed by a list of stats for test foo (suitably repeated for each test).

Mention preconditions in doc

In the related blog post, preconditions are mentioned using Prop.assume, which seems deprecated, the right way to use pre-conditions being now to use to use ==>. It might be interesting to mention that in the examples in the README.

Small list generator

I've found that QCheck.list can generate relatively large list of thousands of elements, which I found to slow down my tests. It would be nice to have a generator that actually generates small lists (with, say, less than 100 elements) with a size distribution similar to list. The range generated by small_signed_int, nat, etc. should also be documented.

Side note: There's a minor naming inconsistency: QCheck.list_of_size vs QCheck.Gen.list_size.

Interrupt long shrinking

Sometimes shrinking can take a long time, so a way to stop shrinking after some time and/or a number of failed tests would be nice to have. The new events added in my last PR should help do that in QCheck_runner without having to modify the code in QCheck.

Add Rely backend

I think making a Rely backend for qcheck would be a useful. I've seen a few people express interest in creating one, so I thought I would reach out here.

Based on looking at the alcotest and oUnit implementations I think it should be fairly straightforward and I am happy to help with the translation.

Fail when no assumes pass

I was stubbing out functions, and was surprised when one of my properties started passing. I've figured out that it's simply failing the assume every time, and since it never fails, that counts as succeeding.

If not enough assumptions pass, that should be a test failure (or at the very least a warning), since it indicates that your property probably isn't testing the thing you think it's testing very well.

Feature wish: statistics over classifiers

To observe the distribution of generators (a probability distribution) it would be great to have a bit of statistics. For example, for a term generator I'm currently hacking I can map each term to its size, which gives rise to the following output:

random seed: 252524381
law anon_test_1: 20 relevant cases (20 total)
  10: 1 cases
  7: 2 cases
  94: 1 cases
  117: 1 cases
  4: 1 cases
  3: 3 cases
  114: 1 cases
  42: 1 cases
  208: 1 cases
  1: 3 cases
  24: 1 cases
  37: 1 cases
  14: 3 cases
success (ran 1 tests)

(where I limit the tests to 20 since much more overflows me with information).
With a bit of shell script hacking I can strip the output and pass it, e.g., to ministat:

x stat_numbers.txt
+------------------------------------------------------------------------------------------------------------+
|             x                                                                                              |
|             x                                                                                              |
|             x                                                                                              |
|             x                                                                                              |
|          x xx                                                                                              |
|          x xx                                                                                              |
|          xxxx                                                                                              |
|          xxxx  x                                                                                           |
|          xxxx  x                                                                 x                         |
|          xxxxxxx              x                                                  x                         |
|          xxxxxxx     x x    x x   x  x                   x                       x                        x|
||____________M________A_____________________|                                                               |
+------------------------------------------------------------------------------------------------------------+
    N           Min           Max        Median           Avg        Stddev
 x  50             1           335           9.5         40.76     75.589266

(where I bumped up the count to 50 to get a prettier histogram).

This gives a better idea of the distribution (min,max,avg,...) and even a histogram.
I'm not suggestion we add an external dependency on ministat but just some numbers, e.g.,
like Jesper's custom Erlang output, see slide 8 (bottom) of http://janmidtgaard.dk/quickcheck/slides/jlouis-dtu-2017.pdf would be a very nice addition. For starters, this would probably involve making a collect-variant which is integer valued.

Some shrinkers are incomplete

Some shrinkers only try a few possibles values: for instance the default shrinker for integers try to divide the integer by 2, but if that fails, it doesn't try further. One example of problematic behavior is the following:

let test =
  QCheck.Test.make ~count:1000 ~name:"mod3"
   QCheck.int (fun i -> i mod 3 <> 0);;

which results in:

QCheck_runner.run_tests [test];;
random seed: 275954313
--- Failure --------------------------------------------------------------------
Test mod3 failed (0 shrink steps):

-4393318161250674747
================================================================================
failure (1 tests failed, 0 tests errored, ran 1 tests)

giving a very non-minimal counter-example, instead of the obvious 0, because shrinking failed at the first step.

I think shrinkers should always cover the totality of smaller values, using a 'smart' ordering, much like what is done in the case of lists and arrays (where all sub-lists are tried). So I think the integer shrinker (and possibly others) should probably be fixed, unless there is a compelling argument not to do it ?

generation of functions

  • read more about JST's solution, and the original quickcheck paper
  • something along: to generate random int -> foo, generate a random decision tree on integers (based on intervals and, say, special cases like 0), then generate a random foo in each leaf

What's the workflow to observe/debug shrinking choices? (with proposals inside)

Here is my current scenario.

I am using a known-bug case to improve my shrinkers. I run the QCheck tests from the command-line, at the fixed seed that is known to run into the bug, and I look at the shrunk counter-example that is displayed. It doesn't look very minimal to me, so I tweak the shrinkers to get a better counter-example. When that works, I get a better counterexample and I iterate. When that does not work, I would like to understand why, in particular to understand the shrinking choices that were made.

So: I would like to use the QCheck API to debug shrinking. In particular, I would like to print (to stderr or a log file) every successful shrinking step (not all shrinking steps, which I can do by adding a debug print to my shrinker, but is too verbose). How can I do this with QCheck?

It looks like what I want is to pass my own 'a Test.handler value, which observes all events, and add a debug print on the Shrunk event. But how can I pass my own handler using the API? As far as I can tell, there is no easy way to do this. QCheck.Test.check_cell accepts a handler as parameter, but there is no call to check_cell in my code, I am only using QCheck_runner.run_tests_main. This function internally calls check_cell, but there is no way for the user to pass a custom handler parameter to that call.

I started building a PR to do this, but then I noticed that, at this point in the test pipeline, the type of counterexamples is an existential variable (packed in the Test or TestResult constructor). Not only would an additional parameter to run_tests_main need to provide a polymorphic handler (noise and type complexity incoming), it's simply not the right place to add an event handler -- I want to do this while I still know what the type of my counterexample is.

In my code, the type information is known at the callsite of QCheck.Test.make. I could add an extra ?handler parameter to this function, or I could easily turn my call into a lower-level call to Test.make_cell and add an extra ?handler parameter there.

The problem is that cells do not carry handlers with the current API, cell checkers do. (And they, again, in the current pipeline, are called after the type parameter of the cell was forgotten.) So I thought I would add a handler field to the cells themselves; at checker-creation time, check_cell would compose the existing handlers (if any) with its own handler (if any). (The same reasoning might work for step and callback, but let's put this aside for now.)

The problem is that the handler type is defined as follows:

type 'a handler = string -> 'a cell -> 'a event -> unit

the handler gets passed the cell itself. If the handler needs access to the cell, and we decide that the cell should be able to carry handler, we have a cycle in type dependencies. It can be made to work, but it's ugly/complex, and it suggests that it is not the right design.

Why do handlers, steppers and callbacks have access to the cell itself? I don't see anything of value in the 'a cell record that I would like to have access to in my handler (except maybe the law and arbitrary fields). In fact, the handler defined in QCheck_runner (which I suspect is the only one most QCheck users can use, due to the difficulty in hooking at the check_cell cell) exactly ignores the cell parameter.

Is there a better way to solve my need (debugging the shrinker behaviors on a specific CLI run) that I haven't thought about?

In the short term, I might try to send a PR using a dependency cycle between cells and handlers/steppers/callbacks; it's ugly and I think it's the wrong design, but it's non-invasive and backward-compatible.

In the medium term, could we consider dropping the 'a cell parameter from handlers? (Is it reasonable for steppers and callbacks to do the same? I don't know what typical use-cases for those are.)? Should we pass nothing to replace it, or pass the law and arbitrary? Is this breakage of API something that QCheck developers are okay with?

In the long term, maybe there is a way to break the default runner, a big monolithic beast, so that this kind of flexibility can be obtained by some other approach than "let's add three more optional parameters to this function"? As a user, here is the experience I would like to have: given a specific CLI invocation of my tester, I would like to replace the call to run_tests_main in my program by a simple composition of calls of a lower-level API that (1) initializes various state in the same way, so that the program behavior is reproduced, (2) gives me more flexibility to operate on my tests, cells, checkers etc., and (3) gives me the opportunity to report the behavior in the same way, so that the interface doesn't change too much. In short, I'm suggesting that it would be useful to invert the control of run_test_main, so that I can access its easy-initialization and easy-reporting facilities, but be the one driving the logic in the middle.

Improve message of failed properties

Some context: I have created a small SQL query parser/serializer and created a property that a randomly generated query should be the same as serializing it and then re-parsing it.

However the only information that is shown when a counter example is found is the randomly generated query that was created. I would prefer to be able to provide a more informative such as "The query Q was reparsed as Q'" to make it easier to see exactly why the property failed. This is possible when writing specific test cases using OUnit in the form of a named msg parameter. However currently qcheck properties seem to be simply 'a -> bool.

Long integer shrinking from within toplevel/utop

Consider the following code:

open QCheck

let rec fac n = match n with
  | 0 -> 1
  | n -> n * fac (n - 1)

let test_fac =
  Test.make ~name:"fac mod'"
    (small_int_corners ())
    (fun n -> try (fac n) mod n = 0
              with Division_by_zero -> (n=0))
;;
QCheck_runner.run_tests ~verbose:true [test_fac]

Assume you save the above to a file bug.ml.

If I compile the example test with the bytecode backend I quickly (0.5s) find the bound (262049) for blowing the bytecode stack:

$ ocamlbuild -use-ocamlfind -package qcheck bug.byte
Finished, 3 targets (0 cached) in 00:00:00.
$ ./bug.byte 
random seed: 317373207
generated error fail pass / total     time test name
[✗]    4    1    0    3 /  100     0.5s fac mod'

=== Error ======================================================================

Test fac mod' errored on (138 shrink steps):

262049

exception Stack overflow

================================================================================
failure (0 tests failed, 1 tests errored, ran 1 tests)

Similarly I can quickly (0.4s) find the bound (524115) for blowing the native-code stack:

$ ocamlbuild -use-ocamlfind -package qcheck bug.native
Finished, 4 targets (2 cached) in 00:00:00.
$ ./bug.native 
random seed: 448617552
generated error fail pass / total     time test name
[✗]    4    1    0    3 /  100     0.4s fac mod'

=== Error ======================================================================

Test fac mod' errored on (215 shrink steps):

524115

exception Stack overflow

================================================================================
failure (0 tests failed, 1 tests errored, ran 1 tests)

However, if I try the same thing within the OCaml toplevel or utop -- I consistently get very long shrinking times (237.9s) with many (52578!) shrinking steps - to the point that I thought I had hit an infinite loop:

utop # #require "qcheck";;
─( 17:51:25 )─< command 1 >────────────────────────────────────────────{ counter: 0 }─
utop # #use "bug.ml";;
val fac : int -> int = <fun>
val test_fac : Test.t = QCheck.Test.Test <abstr>
random seed: 175072808
generated error fail pass / total     time test name
[✗]    4    1    0    3 /  100   237.9s fac mod'

=== Error ======================================================================

Test fac mod' errored on (52578 shrink steps):

209609

exception Stack overflow

================================================================================
failure (0 tests failed, 1 tests errored, ran 1 tests)
- : int = 1

Is this example triggering a sore point in the built-in integer shrinker, or is something else going on?
This is on Mac OS X with OCaml 4.07.1 and QCheck 0.9.

Improve the list shrinker

The current list shrinker cuts off elements one by one:

# Iter.find (fun _ -> true) (Shrink.list [1;2;3;4;5]);;
- : int list option = Some [2; 3; 4; 5]

In comparison, e.g., the Haskell shrinker (defined as shrinkList here:
https://hackage.haskell.org/package/QuickCheck-2.9.2/docs/src/Test-QuickCheck-Arbitrary.html ) will repeatedly try to remove a range of elements (before proceeding to per-entry shrinking).
It would be nice if QCheck's list shrinker could do something similar.

alcotest backend

it seems sufficient to produce a unit -> unit function. I suppose it'd be something like

val mk_alcotest: ?seed:int -> QCheck.Test.t -> (unit -> unit)

this should be an optional sub-library.

Add quartile statistics

Consider our statistics output to understand a generator's distribution:

# let t = Test.make (add_stat ("size", fun i -> i) small_int) (fun _ -> true);;
val t : Test.t = QCheck.Test.Test <abstr>
# QCheck_runner.run_tests [t];;

+++ Stat ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Stat for test anon_test_3:

stats size:
  num: 100, avg: 15.10, stddev: 25.34, median 5, min 0, max 98
    0..  4: #######################################################          41
    5..  9: #####################################################            40
   10.. 14: ##                                                                2
   15.. 19: #                                                                 1
   20.. 24:                                                                   0
   25.. 29:                                                                   0
   30.. 34:                                                                   0
   35.. 39: #                                                                 1
   40.. 44: ##                                                                2
   45.. 49: #                                                                 1
   50.. 54:                                                                   0
   55.. 59: ##                                                                2
   60.. 64:                                                                   0
   65.. 69:                                                                   0
   70.. 74: #                                                                 1
   75.. 79:                                                                   0
   80.. 84: #####                                                             4
   85.. 89: #####                                                             4
   90.. 94:                                                                   0
   95.. 99: #                                                                 1
================================================================================
success (ran 1 tests)
- : int = 0

It could be a nice feature addition to have 'quartile statistics', saying that, e.g., 99% of the output is below 89. I admit this is probably simpler for positive int stats...

`dune install` installs empty META files

Context: MacOS.

Version 0.10, after a successful make && dune install -p qcheck-ounit (kind of mimicking the opam file for the package qcheck-ounit), dune installs 4 META files (along other files), 2 of which are empty.
For instance:

$ ls -l $(ocamlc -where)../qcheck-ounit/
total 12
-rw-r--r-- 1 rixed staff   1 Oct  3 13:10 META
-rw-r--r-- 1 rixed staff  52 Oct  3 13:10 dune-package
-rw-r--r-- 1 rixed staff 700 Oct  3 13:10 opam

Notice also the short dune-package file, limited to:

(lang dune 1.11)
(name qcheck-ounit)
(version 0.10)

Knowing nothing about dune I don't know how what to look for.
Although I remember being able to install this package just fine in the past, so this might be due to some local condition.

Any idea how I could investigate this further?

Adding a guard in Gen

I was trying to get a custom arbitrary (in a similar way as for the arbitrary tree example provided in Readme) and I found myself in need for something like the guard (or cond function) that can be found in the gasche random generator API that you mentioned.
Shortly put, in QCheck.Gen I would need something like

val guard:  ('a -> bool) -> 'a Gen.t -> 'a Gen.t

that generates values satisfying the precondition given as argument.
For my specific case, I just need to generate non-zero small integers, which I guess can be done in another way (although I did not manage to find the right way yet, besides generating integer in a range excluding 0) but I am confident that more complicated use-cases could arise.

Such a function already exists for Shrink with filter, but I did not find anything for Gen.

If something like that already exists, or if there is another way I would be grateful if you would tell me where it can be found in the documentation :)

License filename

I don't know whether there is a reason for naming the license .header. Perhaps this is to include it automatically in some form of generated documentation?

Github however prefers that it is named LICENSE or LICENSE.txt (or something like that) - and more importantly: with this kind of name, the project is automatically highlighted with a logo representing the chosen license. For example, at https://github.com/ocaml/ocaml next to the '116 contributors' there is a little graphics that says LGPL-2.1, making it easy to spot the chosen license.

I am not a lawyer, but the text in .header (modulo the all rights reserved and some whitespace issues) seems to match the 2-clause BSD license as presented here: https://opensource.org/licenses/BSD-2-Clause

I (politely) suggest to rename the license file (and perhaps adjust the typesetting to make it recognizable to github) based on the above.

Gen.int_bound and Gen.int_range may fail with Invalid_argument

Random.State.int fails for any argument greater than 0x3FFFFFFF on my 64bit machine:

# Random.State.int (Random.State.make_self_init ()) 0x3FFFFFFF;;
- : int = 271624853
# Random.State.int (Random.State.make_self_init ()) 0x40000000;;
Exception: Invalid_argument "Random.int".

Concretely this affects Gen.int_range and Gen.int_bound:

# open QCheck;;
# Gen.generate1 (Gen.int_bound 0x3FFFFFFF);;                            
Exception: Invalid_argument "Random.int".
# Gen.generate1 (Gen.int_bound 0x3FFFFFFE);;
- : int = 367212674
# Gen.generate1 (Gen.int_range 0 0x3FFFFFFF);;
Exception: Invalid_argument "Random.int".
# Gen.generate1 (Gen.int_range 0 0x3FFFFFFE);;
- : int = 829922949

One should consider stitching together the outcome of two Random.State.int calls as in the pint generator (or document the behaviour).

Char generator misses '\255'

Two students (Kenneth and Philip) found this one:

# open QCheck;;
# let t = Test.make ~count:1_000_000 char (fun c -> c <> '\255');;
val t : QCheck.Test.t = QCheck.Test.Test <abstr>
# QCheck_runner.run_tests ~verbose:true [t];;                     
law <test>: 1000000 relevant cases (1000000 total)
success (ran 1 tests)

The problem is line 201 in src/QCheck.ml reading: let char st = char_of_int (RS.int st 255)
The last constant should be 256 as Random.State.int generates numbers excluding the given upper bound. If one changes the above to test for another character, e.g., '\254', a counterexample is found.

make ounit backend optional

as qcheck.ounit sub-library.

This is potentially breaking since the ounit test generation should be split from QCheck_runner and into QCheck_ounit

Regression on collect/classification?

With previous versions collect would print a report of the distribution of test inputs:

# let t = Test.make ~count:100_000 (make ~collect:string_of_int (Gen.int_bound 4)) (fun _ -> true);;
val t : QCheck.Test.t = QCheck.Test.Test <abstr>
# QCheck_runner.run_tests ~verbose:true [t];;                           
law <test>: 100000 relevant cases (100000 total)
  4: 20274 cases
  3: 19815 cases
  2: 19834 cases
  0: 20068 cases
  1: 20009 cases
success (ran 1 tests)
- : int = 0

But with the current master I cannot inspect the generation anymore:

# let t = Test.make ~count:100_000 (make ~collect:string_of_int (Gen.int_bound 4)) (fun _ -> true);;
val t : QCheck.Test.t = QCheck.Test.Test <abstr>
# QCheck_runner.run_tests ~verbose:true [t];;                           
random seed: 392026798
generated  error;  fail; pass / total       time -- test name
[✓] (100000)    0 ;    0 ; 100000 / 100000 --     1.2s -- anon_test_1
================================================================================
success (ran 1 tests)
- : int = 0

What's going on? A regression because of OUnit integration or an intentional change?

Progress feedback to user

Currently, an interactive counter in the UI counts up the number of generated inputs. This works beautifully for succeeding tests and fast shrinkers.
For occasional slow tests (complex properties and exponential(?) shrinkers) the UI provides less feedback. For example, for the check-fun tests when they hang on fold_left fold_right I just get this:

generated; error;  fail; pass / total -     time -- test name
[✗] (   1)    0 ;    1 ;    0 / 1000 --     0.0s -- false bool
[✓] (1000)    0 ;    0 ; 1000 / 1000 --     0.0s -- thrice bool
[✗] (   1)    0 ;    1 ;    0 / 1000 --     0.0s -- map filter

Because of the order of the tests I know that fold_left fold_right is next, and hence that it must be causing it, but from the UI alone I do not know

  • that this is the case
  • that it found a counterexample immediately and now is taking a long time shrinking.

Perhaps the first can simply be addressed by a suitable flush of the output channel.
For the second just printing Found counterexample, shrinking (or something like) would be nice to get a feel for the framework's progress. A fancier version could even display some progress measure (or bar) of the shrinking phase.

List shrinker performance

Following up to the revision of the integer shrinker, I somehow
ended up looking into the list shrinker.

First, consider these attempts of the built-in list shrinker:

# QCheck_runner.set_seed 158975878;;
# let print_list xs = print_endline (Print.list Print.int xs) in
  Test.check_exn (Test.make (list small_int) (fun xs -> print_list xs; xs = []));;
[9; 73; 794; 4603]
[]
[794; 4603]
[]
[4603]
[]
[2301]
[]
[1150]
[]
[575]
[]
[287]
[]
[143]
[]
[71]
[]
[35]
[]
[17]
[]
[8]
[]
[4]
[]
[2]
[]
[1]
[]
[0]
[]
Exception:
QCheck.Test.Test_fail ("anon_test_13", ["[0] (after 15 shrink steps)"]).

For every succesful shrink+restart we repeatedly test whether [] is a
counterexample. Without some form of memorization this is hard to
avoid, but one could argue whether these redundant attempts are
needed. For any false property that happens to hold for the empty
list, half of QCheck's shrinking attempts are in vain.

Secondly, the code for the built-in list shrinker falls back on array
shrinking:

let array ?shrink a yield =
  let n = Array.length a in
  let chunk_size = ref (n/2) in
  while !chunk_size > 0 do
    for i=0 to n - !chunk_size do
      (* remove elements in [i .. i+!chunk_size] *)
      let a' = Array.init (n - !chunk_size)
          (fun j -> if j < i then a.(j) else a.(j + !chunk_size))
      in
      yield a'
    done;
    chunk_size := !chunk_size / 2;
  done;
  match shrink with
  | None -> ()
  | Some f ->
    (* try to shrink each element of the array *)
    for i = 0 to Array.length a - 1 do
      f a.(i) (fun x ->
          let b = Array.copy a in
          b.(i) <- x;
          yield b
        )
    done
    
let list ?shrink l yield =
  array_shk ?shrink (Array.of_list l)
    (fun a -> yield (Array.to_list a))

This stresses the garbage collector needlessly by the converting back
and forth to arrays. It could be beneficial to share some of the list
tails instead of allocating a new array repeatedly only to convert it
back to a freshly allocated list afterwards (no sharing guaranteed).

As far as I can tell, this has O(n^2 log n) time complexity: the
repeatedly-halving while-loop runs at most O(log n) times, the
for-loop is upper bounded by O(n) and so is Array.init.

By using a queue for holding the elements of the 'omitted list chunk',
we can achive slightly better complexity, without changing the overall
shrinking strategy.

Here's a patched version to that effect. It also starts from a
chunk_size of n/2, to avoid the repeated [] testing mentioned
initially:

let list ?shrink l yield =
  let n = List.length l in
  let chunk_size = ref (n/2) in

  let rec fill_queue n l q = match n,l with
    | 0,_ -> l
    | _,x::xs -> (Queue.push x q; fill_queue (n-1) xs q)
    | _,_ -> failwith "size mismatch in fill_queue"
  in
 
  while !chunk_size > 0 do
    let q = Queue.create () in
    let l' = fill_queue !chunk_size l q in
    (* remove chunk_size elements in queue *)
    let rec pos_loop rev_prefix suffix =
      begin
        yield (List.rev_append rev_prefix suffix);
        match suffix with
        | [] -> ()
        | x::xs ->
          (Queue.push x q;
           pos_loop ((Queue.pop q)::rev_prefix) xs)
      end
    in
    pos_loop [] l';
    chunk_size := !chunk_size / 2;
  done;
  match shrink with
  | None -> ()
  | Some f ->
    (* try to shrink each element of the list *)
    let rec elem_loop rev_prefix suffix = match suffix with
      | [] -> ()
      | x::xs ->
        begin
          f x (fun x' -> yield (List.rev_append rev_prefix (x'::xs)));
          elem_loop (x::rev_prefix) xs
        end
    in
    elem_loop [] l

I tried to compare the two, and as far as I can tell it is a win. Here
are a few numbers to back up these claims. They've come from simply
using #use "filename.ml" from within utop 3 times. With native code
compilation I however get the same speed-up effect.

let size_gen =
  Gen.oneof [Gen.small_nat;
             Gen.int_bound 750_000]

let length_printer xs =
  Printf.sprintf "[...] list length: %i" (List.length xs)

let list_gen_no_print_no_elem_shrink =
  (set_print length_printer
    (list_of_size size_gen small_int))
let shorter432 =
  Test.make ~name:"all lists shorter than 432"
    list_gen_no_print_no_elem_shrink
    (fun xs -> List.length xs < 432)
;;
QCheck_runner.set_seed 158975878;;
QCheck_runner.run_tests ~verbose:true [shorter432; shorter432];;

original, run 1: 14 shrink steps, 0.5s,0.5s,0.5s
new, run 1: 14 shrink steps, 0.3s,0.3s,0.3s

original, run 2: 13 shrink steps, 0.4s,0.4s,0.4s
new, run 2: 13 shrink steps, 0.2s,0.2s,0.3s

let shorter4332 =
  Test.make ~name:"all lists smaller than 4332"
    list_gen_no_print_no_elem_shrink
    (fun xs -> List.length xs < 4332)
;;
QCheck_runner.set_seed 158975878;;
QCheck_runner.run_tests ~verbose:true [shorter4332; shorter4332];;

original, run 1: 9 shrink steps, 24.0s,24.0s,24.2s
new, run 1: 9 shrink steps, 7.2s,7.1s,7.1s

original, run 2: 10 shrink steps, 42.1s,42.2s,41.9s
new, run 2: 10 shrink steps, 11.9s,12.2s,11.9s

For the following example, I see no improvement (but no regression
either):

let equal_dupl =
  Test.make ~name:"all lists equal to duplication"
    list_gen_no_print_no_elem_shrink
    (fun xs -> try xs = xs @ xs
               with Stack_overflow -> false)
;;
QCheck_runner.set_seed 158975878;;
QCheck_runner.run_tests ~verbose:true [equal_dupl; equal_dupl];;

original, run 1: 19 shrink steps, 0.3s,0.3s,0.2s
new, run 1: 19 shrink steps, 0.3s,0.3s,0.2s

original, run 2: 19 shrink steps, 0.2s,0.2s,0.2s
new, run 2: 19 shrink steps, 0.2s,0.2s,0.2s

There are also derived improvements, e.g., for the function shrinkers
(which depend on list shrinking):

let fold_test =
  Test.make ~name:"false fold, fun first"
    (quad  (* string -> int -> string *)
       (fun2 Observable.string Observable.int small_string)
       small_string
       (list small_int)
       (list small_int))
    (fun (f,acc,is,js) ->
       let f = Fn.apply f in
       List.fold_left f acc (is @ js)
       = List.fold_left f (List.fold_left f acc is) is) (*Typo*)
;;
QCheck_runner.set_seed 15587;;
QCheck_runner.run_tests ~verbose:true [fold_test];;

original: 159 shrink steps, 624.8s
new: 159 shrink steps, 444.8s

(this last one indicates room for improvement in the function
shrinker, but that is a separate issue entirely)

Document QCheck_runner.run_tests_main options

When building a test suite with QCheck_runner.run_tests_main as the entry point
the resulting executable accepts a number of command line options: --verbose / -v, --seed / -v, ...
It would be great to mention these in the API doc.

Complexity of `add_stat`

This can be considered a separate issue to #40 hence the separate report (but feel free to merge).

If I try to inspect the (unsigned) int generator (by wrapping it with abs) it takes a very long time
(I still haven't waited it out) to compute statistics of a sample of (the default) 100 integers:

# let t = Test.make (add_stat ("dist",abs) int) (fun _ -> true);;
val t : QCheck.Test.t = QCheck.Test.Test <abstr>
# QCheck_runner.run_tests ~verbose:true [t];;                    
generated error  fail pass / total  time(s) name
[✓]  100    0     0    100 /  100      0.0  anon_test_12                                            

(top-level hangs)

This behaviour indicates that the 100 true tests completed immediately, while computing/drawing statistics take substantial time. I suspect if one of these routines algorithmically "count up" from 0, then it will take a long time to reach the entries produced by int (wrapped in abs) - but that is just a guess without having studied the code thoroughly.

Add a splittable random number generator

An increasing number QuickCheck implementations build on splittable random number generators to avoid (pseudo-random) dependencies between the seeds passed to, e.g., a pair generator's two component generators. For example,

In this light QCheck should consider building on one as well. For OCaml a few splittable random number generator implementations are already available.
Here's an OCaml implementation from Core: https://github.com/janestreet/splittable_random
Here's another one for OCaml by Xavier: https://github.com/xavierleroy/pringo
There may however be different reasons (dependencies, license, opam-package availability, ...) to write one from scratch instead of going with one of these.

Note, apparently there was a bug in the original SplitMix paper
Guy L. Steele Jr., Doug Lea, and Christine H. Flood
Fast Splittable Pseudorandom Number Generators, OOPSLA 2014.
that made it into a number of implementations - and described in more detail in this blog post: http://www.pcg-random.org/posts/bugs-in-splitmix.html
A kind soul has since filed bug-reports to the different implementations, e.g., here:
xavierleroy/pringo#2

Support stats with negative integers

Consider the following interaction to observe the distribution of the builtin integer generators:

# let t = Test.make (add_stat ("dist", fun x -> x) small_nat) (fun _ -> true);;
val t : QCheck.Test.t = QCheck.Test.Test <abstr>
generated error  fail pass / total  time(s) name
[✓]  100    0     0    100 /  100      0.0  anon_test_8                                             
stats dist:
  num: 100, avg: 332.98, stddev: 1220.61, median 9, min 0, max 8845
            0-442: #######################################################          89
          443-885: ###                                                               5
         886-1328: #                                                                 2
        1329-1771:                                                                   0
        1772-2214:                                                                   0
        2215-2657:                                                                   0
        2658-3100:                                                                   0
        3101-3543:                                                                   1
        3544-3986:                                                                   0
        3987-4429:                                                                   0
        4430-4872:                                                                   0
        4873-5315:                                                                   1
        5316-5758:                                                                   0
        5759-6201:                                                                   0
        6202-6644:                                                                   1
        6645-7087:                                                                   0
        7088-7530:                                                                   0
        7531-7973:                                                                   0
        7974-8416:                                                                   0
        8417-8859:                                                                   1
        8860-9302:                                                                   0
================================================================================
success (ran 1 tests)
- : int = 0

Now, it would be nice to similarly inspect a sample of small_signed_int (or other signed generators for that matter). However if one attempts to do so you get:

# let t = Test.make (add_stat ("dist",fun x -> x) small_signed_int) (fun _ -> true);;
val t : QCheck.Test.t = QCheck.Test.Test <abstr>
# QCheck_runner.run_tests ~verbose:true [t];;                           
generated error  fail pass / total  time(s) name
[ ]    0    0     0      0 /  100      0.0  anon_test_10 (collecting)Exception: Assert_failure ("src/QCheck.ml", 1188, 9).

(where the linenumber naturally differs with the particular QCheck version), which indicates that statistics are limited to non-negative integers. In the above case, I would love to see a nice bell-curve centered around 0 as output :-)

This will require some more program logic (for non-negative stats only split into "positive buckets", while mixed positive and negative stats should split into "mixed buckets", ...).

Add missing int32+64 shrinkers

Title says it all: the Shrink module is missing int32+64 shrinkers.

(Realizing I probably won't get around to adressing my growing list of issues and extensions I'll add them here... 😮)

Documentation contains NEXT_RELEASE

The documentation contains comments such as

  (** @since NEXT_RELEASE *)

I believe that NEXT_RELEASE should be substituted by a version number.

system for reporting messages to the user

Sometimes I would like to perform some computation in the property itself (given some inputs), and, in case the property does not hold, print intermediate values, informative things, etc. It's also useful to report values found via find_example to give an idea of what is happening.

Last commits added such an API (with Test.make_full taking an extended property ctx -> 'a -> bool
and a ctx that can be used to report messages). I'd like the opinion of @Gbury and @jmid on this API.

odoc dependency is missing

Installing quickcheck on a switch without odoc installed, but with OPAMBUILDDOC set results in this error:

#=== ERROR while compiling qcheck.0.8 =========================================#
# context     2.0.0~rc3 | linux/x86_64 | ocaml-variants.4.07.0+flambda | git+https://github.com/ocaml/opam-repository.git
# path        /media/samsung/opam2/4.07.0+flambda/.opam-switch/build/qcheck.0.8
# command     /media/samsung/opam2/opam-init/hooks/sandbox.sh build jbuilder build @doc
# exit-code   1
# env-file    /media/samsung/opam2/log/qcheck-20049-84ae40.env
# output-file /media/samsung/opam2/log/qcheck-20049-84ae40.out
### output ###
# Error: Program odoc not found in the tree or in PATH (context: default)
# Hint: opam install odoc

Optional labelled parameter in functions

I've come across a couple of functions in QCheck.Gen which have only optional parameters:

val string : ?⁠gen:char t> string t
val small_string : ?⁠gen:char t> string t

Not sure if there are more throughout the library. Functions with only optional labelled parameters are tricky to use :-) would there be some receptiveness to adding a 'bookend' () parameter to them for 1.0? If we're going by SemVer, backward-incompatible changes are allowed in a major version...

Understanding the implementation of Gen.float

I needed a float_range (perhaps a reasonable PR?), and came up with this:

  let float_range a b =
    if b < a then invalid_arg "Gen.float_range";
    let range = b -. a in
    fun st -> a +. RS.float st range
  let (--.) = float_range

Seems fine, and appears to work well in my use, but along the way I happened to look at the implementation of Gen.float:

qcheck/src/core/QCheck.ml

Lines 117 to 121 in 0dab657

let float st =
exp (RS.float st 15. *. (if RS.float st 1. < 0.5 then 1. else -1.))
*. (if RS.float st 1. < 0.5 then 1. else -1.)

Some questions came to mind:

  1. Why is this implemented in terms of exp, rather than just calling through to RS.float?
  2. Why not use RS.bool to determine whether to negate, rather than generating floats and doing a comparison?
  3. Why not use ~-. instead of performing a multiplication?

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.