futoase/PyQCheck

Propose new features with refactoring

Gab-km opened this issue · 5 comments

Target: Extensions for PyQCheck

I'd like to add new features from another QuickCheck libraries (ex. scalacheck, etc) into PyQCheck. The features are like following:

for_all function

for_all function allows us to create property a little easier than we are writing now.

from pyqcheck import PyQCheck, for_all

PyQCheck(verbose=True).add(
  for_all(
    ('boolean', 'boolean'),
    '!(x || y) == !x && !y',
    lambda x, y: (not(x or y)) == ((not x) and (not y))
  )
).run(10).result()

This is equivalent to the code below:

from pyqcheck import PyQCheck, Arbitrary

PyQCheck(verbose=True).add(
  Arbitrary('boolean', 'boolean').property(
    '!(x || y) == !x && !y', lambda x, y: (not(x or y)) == ((not x) and (not y))
  )
).run(10).result()

from_gen function

Gen class and from_gen function allows us to create our own arbitrary with generator syntax in Python.

import random
import sys
from pyqcheck import PyQCheck, Gen, from_gen

def gen_int(min_int=1, max_int=None):
  min_int = min_int if isinstance(min_int, int) else 1
  max_int = max_int if max_int is not None and isinstance(max_int, int) else sys.maxsize
  while True:
    yield random.randint(min_int, max_int)

PyQCheck(verbose=True).add(
  from_gen([Gen(gen_int), Gen(gen_int)]).property(
    'x * y == y * x and x + y == y + x',
    lambda x, y: x * y == y * x and x + y == y + x
  )
).run(10).result()

We can limit these ranges.

from pyqcheck import PyQCheck, Gen, from_gen

def gen_int(min_int=1, max_int=None):
  min_int = min_int if isinstance(min_int, int) else 1
  max_int = max_int if max_int is not None and isinstance(max_int, int) else sys.maxsize
  while True:
    yield random.randint(min_int, max_int)

PyQCheck().add(
  from_gen(
    [Gen(gen_int, {"min": 10, "max": 100}), # range of 10 - 100
     Gen(gen_int, {"min": 30})]             # range of 30 - max of default
  ).property(
    '10 <= x <= 100 and y >= 30',
    lambda x, y : 10 <= x <= 100 and y >= 30
  )
).run(10).result()

from_gen_and_shrink function

from_gen_and_shrink function allows us to create our own arbitrary like from_gen, which can set a shrinker.
The shrinker is a procedure to minimize our test cases as we want.

import random
import sys
from pyqcheck import PyQCheck, Gen, from_gen_and_shrink

def gen_int(min_int=1, max_int=None):
  min_int = min_int if isinstance(min_int, int) else 1
  max_int = max_int if max_int is not None and isinstance(max_int, int) else sys.maxsize
  while True:
    yield random.randint(min_int, max_int)

PyQCheck(verbose=True).add(
  from_gen_and_shrink(
    [Gen(gen_int, {"min": 10, "max": 100})],
    lambda x: [x-2, x-1, x, x+1, x+2]        # limit test cases within 2 if failed
  ).property(
    '15 <= x <= 95,
    lambda x: 15 <= x <= 95
  )
).run(10).result()

It may result like this:

----- PyQCheck test results... -----
label: 15 <= x <= 95
success: 7
failure: 3
shrinks: 2 times
verbose:
  ☀  <lambda>(19)
  ☀  <lambda>(29)
  ☀  <lambda>(59)
  ☀  <lambda>(85)
  ☀  <lambda>(72)
  ☀  <lambda>(40)
  ☂  <lambda>(14)
  ☀  <lambda>(16)
  ☂  <lambda>(13)
  ☂  <lambda>(12)
-----

How to go: step by step

The features above are, however difficult to implement because of PyQCheck's current structure. For example, arbitrary module grabs almost all like a dictator or God.

So I would like to resolve this proposition step by step as following:

  1. Refactoring (or something to redesign) PyQCheck modules without changing PyQCheck APIs as much as we can
  2. Then, implementing the features

If you like it, I will try to do the job.

@futoase If I made you confused, I will apologize you... 😢
I have coded the task above by 70%, so all you need is to judge my proposition and pull requests.
You don't have to be bothered to struggle with them. 😉

👍

Thanks a lot! 😉

This change will be large, so may I have your opinion for the way to send Pull Requests?

  1. To send 1 PR with all changes
  2. To send some PRs for each features, i.e. refactoring, introducing for_all, ...
  3. Or else

@futoase If you don't have any special instruction for PR, I will send you 4 PRs in order (for refactoring, for_all, from_gen, and from_gen_and_shrink) to make each of them reviewed. Thank you. 😃

Now the first step of refactoring and redesigning PyQCheck is done.
I will implement the rest of the tasks, new features of for_all, from_gen and from_gen_and_shrink, in sequence.