Skip to content

Propose new features with refactoring #10

@Gab-km

Description

@Gab-km

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions