Skip to content
Merged
2 changes: 1 addition & 1 deletion examples/using.autodoc
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ true
@EndExampleSession


In some cases we might want to return an explanation of why a test failed. Rather than returning `false` from `QC_Equal`, you can instead return a string. Returning a string is treated as a failure, and the string is printed out to the user.
In some cases we might want to return an explanation of why a test failed. Rather than returning `false` from a function tested by `QC_Check`, you can instead return a string. Returning a string is treated as a failure, and the string is printed out to the user.

@Section Valid argument types for QuickCheck

Expand Down
8 changes: 7 additions & 1 deletion gap/digraphs.g
Original file line number Diff line number Diff line change
@@ -1 +1,7 @@
QC_RegisterFilterGen(IsDigraph, {rg, limit} -> RandomDigraph(Random([1..limit])));
# RandomDigraph cannot take a random source, so we build the edges from rg
QC_RegisterFilterGen(IsDigraph, function(rg, limit)
local n, p;
n := Random(rg, [1..limit]);
p := Random(rg, [0..100]);
return Digraph(List([1..n], i -> Filtered([1..n], j -> Random(rg, [1..100]) <= p)));
end);
22 changes: 11 additions & 11 deletions gap/generators.g
Original file line number Diff line number Diff line change
Expand Up @@ -40,25 +40,25 @@ end);



QC_ListOf := function(object)
InstallGlobalFunction(QC_ListOf, function(object)
return function(rg, limit)
return List([1..Random([0..limit])], {x} -> QC_MakeRandomArgument(object, rg, limit));
return List([1..Random(rg, [0..limit])], {x} -> QC_MakeRandomArgument(object, rg, limit));
end;
end;
end);

QC_FixedLengthListOf := function(object, length)
InstallGlobalFunction(QC_FixedLengthListOf, function(object, length)
return function(rg, limit)
return List([1..length], {x} -> QC_MakeRandomArgument(object, rg, limit));
end;
end;
end);

QC_PairOf := {o} -> QC_FixedLengthListOf(o, 2);
InstallGlobalFunction(QC_PairOf, {o} -> QC_FixedLengthListOf(o, 2));


QC_SetOf := function(object)
InstallGlobalFunction(QC_SetOf, function(object)
return function(rg, limit)
local set, targetsize, testlimit;
targetsize := Random([0..limit]);
targetsize := Random(rg, [0..limit]);
set := [];
# In case we have trouble filling the set, put a limit on number of tests
testlimit := targetsize * limit + 10;
Expand All @@ -68,13 +68,13 @@ QC_SetOf := function(object)
od;
return set;
end;
end;
end);

QC_ElementOf := function(object)
InstallGlobalFunction(QC_ElementOf, function(object)
return function(rg, limit)
return Random(rg, object);
end;
end;
end);


QC_RegisterFilterGen(IsCyclic, {rg, limit} -> CyclicGroup(Random(rg, [1..limit])));
Expand Down
87 changes: 79 additions & 8 deletions gap/quickcheck.gd
Original file line number Diff line number Diff line change
Expand Up @@ -49,37 +49,108 @@ DeclareGlobalFunction("QC_Check");
DeclareGlobalFunction("QC_CheckEqual");

#! @Description
#! Return the function called, and arguments given, last time a QuickCheck
#! test failed.
#! Return the function called, and arguments given, if the most recent call to
#! `QC_Check` or `QC_CheckEqual` failed on a particular input.
#!
#! Returns a record containing <A>args</A> (the arguments) and a function `func`
#! (if `QC_Check` failed) or a list of functions `funcs` (if `QC_CheckEqual` failed).
#! Returns <K>false</K> if no test has failed.
#! Returns <K>false</K> otherwise.
#! @Arguments
DeclareGlobalFunction("QC_LastFailure");


#! @Description
#! Rerun the last test which failed. This is most useful if a test in a '.tst' file
#! failed, as this will allow the test to enter the break loop.
#! Rerun the last test which failed, as given by <Ref Func="QC_LastFailure"/>.
#! This is most useful if a test in a '.tst' file failed, as this will allow the
#! test to enter the break loop. Returns <K>fail</K> if <Ref Func="QC_LastFailure"/>
#! returns <K>false</K>.
#! @Arguments
DeclareGlobalFunction("QC_RerunLastFailure");

#! @Description
#! Set config options for QuickCheck globally, by passing a record. It is not required
#! to set all options.
#!
#! Current options are:
#! * <C>tests</C>: Number of tests to run
#! * <C>limit</C>: The size of the largest object to create
#! * <C>seed</C>: Initial random seed
#! * <C>tests</C>: Number of tests to run (default 500)
#! * <C>limit</C>: The size of the largest object to create (default 9)
#! * <C>ramp</C>: Number of tests, including skipped ones, to run at each size
#! before increasing it (default 30)
#! * <C>seed</C>: Initial random seed (default 1)
#! * <C>catchErrors</C>: If <K>true</K>, an error in the tested function counts
#! as a failure; if <K>false</K>, it enters the break loop (default <K>true</K>)
#!
#! @Arguments config
DeclareGlobalFunction("QC_SetConfig");


#! @Description
#! Get the current global configuration for QuickCheck, as a record
#! @Arguments
DeclareGlobalFunction("QC_GetConfig");


DeclareCategory("IsQCSkip", IsObject);

#! @Description
#! A function tested by <Ref Func="QC_Check"/> or <Ref Func="QC_CheckEqual"/>
#! can return <C>QC_Skip</C> if its arguments do not satisfy its requirements
#! (for example, if it needs an intransitive group, or an integer which is not prime).
#! Skipped tests do not count as failures or towards the number of tests.
#! To avoid infinite loops, if 100 times the requested number of tests
#! are skipped, the check stops and returns <K>false</K>.
DeclareGlobalName("QC_Skip");

#! @Description
#! Register <A>gen</A> as a generator for arguments described by the filter
#! <A>filter</A>. <A>gen</A> is called as <C>gen(rs, limit)</C>, where <A>rs</A> is a
#! random source and <A>limit</A> a positive integer bounding the size of the value.
#! It is up to <A>gen</A> to decide how to interpret <A>limit</A>.
#! Smaller values of <A>limit</A> are used first, so simple inputs are tested
#! before complex ones.
#! Values not in <A>filter</A> are discarded and regenerated, with an error
#! after 100 attempts. This lets a generator serve a more specific filter, for
#! example an abelian permutation group when only a <A>gen</A> for permutation
#! groups has been installed.
#! @Arguments filter, gen
DeclareGlobalFunction("QC_RegisterFilterGen");

#! @Section Argument descriptions
#!
#! An argument description is either a filter with a registered generator
#! (see <Ref Func="QC_RegisterFilterGen"/>), or a function <C>gen(rs, limit)</C>.
#! The functions below build descriptions from other descriptions.

#! @Description
#! Describes a list of between 0 and <C>limit</C> values described by <A>desc</A>.
#! @Arguments desc
#! @BeginExampleSession
#! gap> QC_Check([QC_ListOf(QC_PairOf(IsPosInt))],
#! > l -> ForAll(l, p -> Length(p) = 2 and ForAll(p, IsPosInt)));
#! true
#! @EndExampleSession
DeclareGlobalFunction("QC_ListOf");

#! @Description
#! Describes a list of <A>len</A> values described by <A>desc</A>.
#! @Arguments desc, len
DeclareGlobalFunction("QC_FixedLengthListOf");

#! @Description
#! Describes a list of length two, whose entries are described by <A>desc</A>.
#! @Arguments desc
DeclareGlobalFunction("QC_PairOf");

#! @Description
#! Describes a set of between 0 and <C>limit</C> values described by <A>desc</A>.
#! @Arguments desc
DeclareGlobalFunction("QC_SetOf");

#! @Description
#! Describes a random element of the collection <A>coll</A>. This ignores
#! <C>limit</C>.
#! @Arguments coll
DeclareGlobalFunction("QC_ElementOf");

## For private data
BindGlobal("_QC", rec());
55 changes: 26 additions & 29 deletions gap/quickcheck.gi
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,16 @@
# Implementations
#

QC_Skip := "SKIP TEST";
# A unique object, compared with IsIdenticalObj
BindGlobal("QC_Skip", Objectify(NewType(NewFamily("QCSkipFamily"), IsQCSkip and IsPositionalObjectRep), []));
InstallMethod(PrintObj, [IsQCSkip], function(x) Print("QC_Skip"); end);

# Store the generators we support
DeclareOperation("QC_Filters", [IsObject]);

QC_RegisterFilterGen := function(filt, func)
InstallGlobalFunction(QC_RegisterFilterGen, function(filt, func)
InstallMethod(QC_Filters, [filt], function(x) return func; end);
end;
end);


InstallGlobalFunction(QC_MakeRandomArgument,
Expand Down Expand Up @@ -73,12 +75,13 @@ InstallGlobalFunction(QC_SetConfig,

InstallGlobalFunction(QC_GetConfig, {} -> ShallowCopy(_QC.defaultConfig));

_QC.LastFailure := rec();
_QC.LastFailure := false;

_QC.Check := function(argtypes, func, configarg...)
local testCount, skipCount, args, rg, call, ret, config, testSize, breakOnError, instream;

config := _QC.fillConfig(configarg);
Unbind(_QC.Counterexample);

rg := RandomSource(IsMersenneTwister, config.seed);

Expand Down Expand Up @@ -114,22 +117,23 @@ _QC.Check := function(argtypes, func, configarg...)
fi;
CloseStream(instream);
Unbind(_QC.Ret);
# We leave _QC.Args and _QC.Function, so they can be returned by QC_LastFailure

if IsEmpty(ret) then
PrintFormatted("Test {} of {} did not return a value\n", testCount, config.tests);
Print(" Input: ", args, "\n");
_QC.Counterexample := args;
return false;
fi;

ret := ret[1];

if ret = QC_Skip then
if IsIdenticalObj(ret, QC_Skip) then
skipCount := skipCount + 1;
elif ret <> true then
PrintFormatted("Test {} of {} failed:\n", testCount, config.tests);
Print(" Input: ", args,"\n");
Print(" Output: ", ret,"\n");
_QC.Counterexample := args;
return false;
else
testCount := testCount + 1;
Expand All @@ -145,34 +149,32 @@ end;

InstallGlobalFunction(QC_Check,
function(argtypes, func, configarg...)
local ret, savefailure;
savefailure := _QC.LastFailure;
_QC.LastFailure := rec(func := func);
local ret;
_QC.LastFailure := false;
ret := CallFuncList(_QC.Check, Concatenation([argtypes, func], configarg));
if ret then
_QC.LastFailure := savefailure;
if IsBound(_QC.Counterexample) then
_QC.LastFailure := rec(func := func, args := _QC.Counterexample);
_QC.LastFailureRerun := func;
fi;
return ret;
end);


InstallGlobalFunction(QC_CheckEqual,
function(argtypes, funcL, funcR, configarg...)
local funccheck, ret, savefailure;

savefailure := _QC.LastFailure;
_QC.LastFailure := rec(funcs := [funcL, funcR]);
local funccheck, ret;

funccheck := function(args...)
local retL, retR;
retL := CallFuncListWrap(funcL, StructuralCopy(args));
retR := CallFuncListWrap(funcR, args);
if retL = [QC_Skip] or retL = [QC_Skip] then
if (not IsEmpty(retL) and IsIdenticalObj(retL[1], QC_Skip)) or
(not IsEmpty(retR) and IsIdenticalObj(retR[1], QC_Skip)) then
return QC_Skip;
fi;

if IsEmpty(retL) or IsEmpty(retR) then
return "A least one function did no return a value";
return "At least one function did not return a value";
fi;

retL := retL[1];
Expand All @@ -183,27 +185,22 @@ InstallGlobalFunction(QC_CheckEqual,
fi;
return StringFormatted("Return values differ: {} and {}", retL, retR);
end;
_QC.LastFailure := false;
ret := CallFuncList(_QC.Check, Concatenation([argtypes, funccheck], configarg));
if ret then
_QC.LastFailure := savefailure;
if IsBound(_QC.Counterexample) then
_QC.LastFailure := rec(funcs := [funcL, funcR], args := _QC.Counterexample);
_QC.LastFailureRerun := funccheck;
fi;
return ret;
end);

InstallGlobalFunction(QC_LastFailure,
function()
if IsBound(_QC.Args) then
return rec(func := _QC.Function, args := _QC.Args);
else
return fail;
fi;
end);
{} -> _QC.LastFailure);

InstallGlobalFunction(QC_RerunLastFailure,
function()
if IsBound(_QC.Args) then
return CallFuncList(_QC.Function, _QC.Args);
else
if _QC.LastFailure = false then
return fail;
fi;
return CallFuncList(_QC.LastFailureRerun, StructuralCopy(_QC.LastFailure.args));
end);
4 changes: 2 additions & 2 deletions makedoc.g
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
#
# This file is a script which compiles the package manual.
#
if fail = LoadPackage("AutoDoc", "2018.02.14") then
Error("AutoDoc version 2018.02.14 or newer is required.");
if fail = LoadPackage("AutoDoc", "2026.03.17") then
Error("AutoDoc version 2026.03.17 or newer is required.");
fi;

AutoDoc( rec( scaffold := true, autodoc := true, extract_examples := true ) );
8 changes: 7 additions & 1 deletion tst/digraphs.tst
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,10 @@ true
gap> QC_Check([IsDigraph], function(d) local a; a := DigraphDiameter(d); return a = fail or a <= DigraphNrVertices(d); end);
true
gap> QC_Check([IsDigraph], {d} -> IsList(DigraphMaximalClique(d)));
true
true
gap> rs := RandomSource(IsMersenneTwister, 3);;
gap> l := List([1..20], i -> QC_MakeRandomArgument(IsDigraph, rs, 9));;
gap> Reset(GlobalMersenneTwister, 77);; Random([1..10]);;
gap> rs := RandomSource(IsMersenneTwister, 3);;
gap> l = List([1..20], i -> QC_MakeRandomArgument(IsDigraph, rs, 9));
true
Loading
Loading