QuickCheck

# QuickCheck Tutorial 2

Files: use20.m use21.m use22.m mymax.m
Back to main

## Conditional law

In general many laws hold only under certain conditions. Quickcheck provides an implication combinator to represent such conditional laws. Eg, the law

            X =< Y  => max x y == y

can be represented by the definition :

            :- func law(int, int) = property.
            law(X, Y) = (X =< Y) `===>` (mymax(X, Y) `===` Y).

or alternatively can be represented as :

            :- func law2(int, int) = property.
            law2(X, Y) = is_less_equal(X, Y) `===>` (mymax(X, Y) `===` Y).

            :- func is_less_equal(int, int) = bool.
            is_less_equal(X, Y) = Bool :-
                    (if     X =< Y
                     then
                            Bool = yes
                     else
                            Bool = no
                    ).

The difference between law1 and law2 is the left argument of `===>`. `(X =< Y)` is of type (pred) whereas `is_less_equal(X, Y)` evaluates to type bool. `===>` is overloaded to take both types.

The complete use20.m:

    :- module use20.

    :- interface.

    :- use_module io.

    :- pred main(io__state, io__state).
    :- mode main(di, uo) is det.

    %---------------------------------------------------------------------------%

    :- implementation.

    :- import_module int, list, bool.
    :- import_module qcheck, mymax.

    %---------------------------------------------------------------------------%

    main -->
            qcheck(qcheck__f(law), "testing mymax using `===>` for (pred)"),
            qcheck(qcheck__f(law2), "testing mymax using `===>` for bool").

    :- func law(int, int) = property.
    law(X, Y) = (X =< Y) `===>` (mymax(X, Y) `===` Y).

    :- func law2(int, int) = property.
    law2(X, Y) = is_less_equal(X, Y) `===>`
                                    (mymax(X, Y) `===` Y).

    :- func is_less_equal(int, int) = bool.
    is_less_equal(X, Y) = Bool :-
            (if     X =< Y
             then
                    Bool = yes
             else
                    Bool = no
            ).

After running the program, test statistics will be something like:

            Test Description : testing mymax using `===>` for (pred)
            Number of test cases that succeeded : 52
            Number of trivial tests : 0
            Number of tests cases which failed the pre-condition : 48
            Distributions of selected argument(s) :

            Test Description : testing mymax using `===>` for bool
            Number of test cases that succeeded : 52
            Number of trivial tests : 0
            Number of tests cases which failed the pre-condition : 48
            Distributions of selected argument(s) :

The default number of tests to run is 100. In the above test, 48/100 cases passed the invariant function, and none failed. However, there are 52/100 cases where the inputs failed the pre-condition.

Note that both test cases succeeded 52/100 and failed pre-condition 48/100. qcheck.m seeds the random generator on local time, if qcheck is called twice within the same second, the number generated will be the same.

The implication combinator can be compounded. For example, suppose mymax is designed that if mymax(X, Y) is called, Y will never be zero. Thus test cases with Y=0 should also be disregarded (use21.m) :

            :- func law(int, int) = property.
            law(X, Y) =  notzero(Y) `===>` ((X =< Y) `===>` (mymax(X, Y) `===` Y)).

            :- pred notzero(int).
            :- mode notzero(in) is semidet.
            notzero(X) :- X \= 0.

The right argument of `===>` is also overload, as shown in use22.m:

    :- module use22.

    :- interface.

    :- use_module io.

    :- pred main(io__state, io__state).
    :- mode main(di, uo) is det.

    %---------------------------------------------------------------------------%

    :- implementation.

    :- import_module int, bool.
    :- import_module qcheck.

    %---------------------------------------------------------------------------%

    main -->
            qcheck(qcheck__f(law1), "passing property"),
            qcheck(qcheck__f(law2), "passing (func) = property").

    :- func law1(int, int) = property.
    law1(X, Y) =  notzero(Y) `===>` qcheck__f( (func) = ((X // Y) `===` (X // Y)) ).

    :- func law2(int, int) = property.
    law2(X, Y) =  notzero(Y) `===>` ( (X // Y) `===` (X // Y) ).

    :- pred notzero(int).
    :- mode notzero(in) is semidet.
    notzero(X) :- X \= 0.

The difference between law1/2 and law2/2 is the right argument of `===>`. law1/2 passed (func), while law2/2 passed property. In law2/2, (X // Y) is always evaluated, which will cause error if Y is zero. However, in law1/2 (X // Y) is not evaluated if `===>`'s left argument is 'bool:no' or '(pred):failure'.

The implication combinator `===>` marks a test that has failed pre-condition by inserting 'flag:condition' into the property in cases where the right argument is a property; in cases where the right argument is (func), then `===>` will just return 'property:[condition]'. If the property list contains one or more 'condition', the test result is ignored.