From 4163fbc2238e8b89506fe598a1e3ed9fbd1b289a Mon Sep 17 00:00:00 2001 From: Christopher Jefferson Date: Thu, 1 Oct 2026 14:08:02 +0100 Subject: [PATCH 1/8] Fix QC_CheckEqual ignoring QC_Skip from its second function The skip check tested the left-hand result twice, so a QC_Skip from the right-hand function was compared as an ordinary value and reported as a failure. Co-Authored-By: Claude Opus 5.5 --- gap/quickcheck.gi | 2 +- tst/tst-gap/failures.tst | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 tst/tst-gap/failures.tst diff --git a/gap/quickcheck.gi b/gap/quickcheck.gi index 2608178..d21dd16 100644 --- a/gap/quickcheck.gi +++ b/gap/quickcheck.gi @@ -167,7 +167,7 @@ InstallGlobalFunction(QC_CheckEqual, local retL, retR; retL := CallFuncListWrap(funcL, StructuralCopy(args)); retR := CallFuncListWrap(funcR, args); - if retL = [QC_Skip] or retL = [QC_Skip] then + if retL = [QC_Skip] or retR = [QC_Skip] then return QC_Skip; fi; diff --git a/tst/tst-gap/failures.tst b/tst/tst-gap/failures.tst new file mode 100644 index 0000000..166089a --- /dev/null +++ b/tst/tst-gap/failures.tst @@ -0,0 +1,11 @@ +gap> START_TEST("failures.tst"); + +# Either function in QC_CheckEqual may skip +gap> skipZero := function(x) if x = 0 then return QC_Skip; fi; return x; end;; +gap> QC_CheckEqual([IsInt], skipZero, x -> x); +true +gap> QC_CheckEqual([IsInt], x -> x, skipZero); +true + +# +gap> STOP_TEST("failures.tst", 1); From ab3e7baa5a93e639b47315a0c4fc2105c1e942a5 Mon Sep 17 00:00:00 2001 From: Christopher Jefferson Date: Thu, 1 Oct 2026 14:08:05 +0100 Subject: [PATCH 2/8] Fix typo in QC_CheckEqual message for functions returning nothing Co-Authored-By: Claude Opus 5.5 --- gap/quickcheck.gi | 2 +- tst/tst-gap/failures.tst | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/gap/quickcheck.gi b/gap/quickcheck.gi index d21dd16..dd4ebf1 100644 --- a/gap/quickcheck.gi +++ b/gap/quickcheck.gi @@ -172,7 +172,7 @@ InstallGlobalFunction(QC_CheckEqual, 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]; diff --git a/tst/tst-gap/failures.tst b/tst/tst-gap/failures.tst index 166089a..a9a21dd 100644 --- a/tst/tst-gap/failures.tst +++ b/tst/tst-gap/failures.tst @@ -6,6 +6,11 @@ gap> QC_CheckEqual([IsInt], skipZero, x -> x); true gap> QC_CheckEqual([IsInt], x -> x, skipZero); true +gap> QC_CheckEqual([IsInt], x -> 1, function(x) end, rec(tests := 5)); +Test 0 of 5 failed: + Input: [ 0 ] + Output: At least one function did not return a value +false # gap> STOP_TEST("failures.tst", 1); From 7fdf42f384da991f6aa74ec2135688ced9dabd76 Mon Sep 17 00:00:00 2001 From: Christopher Jefferson Date: Thu, 1 Oct 2026 14:08:18 +0100 Subject: [PATCH 3/8] Make generated values depend only on the configured seed QC_ListOf and QC_SetOf chose their length with the global random source, and the IsDigraph generator used RandomDigraph, which cannot be given a random source. Changing the global random state therefore changed the values QC_Check generated for a fixed seed. The IsDigraph generator now builds its edges from the given random source. Co-Authored-By: Claude Opus 5.5 --- gap/digraphs.g | 8 +++++++- gap/generators.g | 4 ++-- tst/digraphs.tst | 8 +++++++- tst/tst-gap/failures.tst | 10 ++++++++++ 4 files changed, 26 insertions(+), 4 deletions(-) diff --git a/gap/digraphs.g b/gap/digraphs.g index c895bd9..0fb79d3 100644 --- a/gap/digraphs.g +++ b/gap/digraphs.g @@ -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); diff --git a/gap/generators.g b/gap/generators.g index ad29cb1..b2eabad 100644 --- a/gap/generators.g +++ b/gap/generators.g @@ -42,7 +42,7 @@ end); 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; @@ -58,7 +58,7 @@ QC_PairOf := {o} -> QC_FixedLengthListOf(o, 2); 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; diff --git a/tst/digraphs.tst b/tst/digraphs.tst index 0f60e2e..125660a 100644 --- a/tst/digraphs.tst +++ b/tst/digraphs.tst @@ -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 \ No newline at end of file +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 diff --git a/tst/tst-gap/failures.tst b/tst/tst-gap/failures.tst index a9a21dd..029732e 100644 --- a/tst/tst-gap/failures.tst +++ b/tst/tst-gap/failures.tst @@ -12,5 +12,15 @@ Test 0 of 5 failed: Output: At least one function did not return a value false +# Generators only use the given random source +gap> rs := RandomSource(IsMersenneTwister, 3);; +gap> a := QC_ListOf(IsInt)(rs, 9);; b := QC_SetOf(IsInt)(rs, 9);; +gap> Reset(GlobalMersenneTwister, 77);; Random([1..10]);; +gap> rs := RandomSource(IsMersenneTwister, 3);; +gap> a = QC_ListOf(IsInt)(rs, 9); +true +gap> b = QC_SetOf(IsInt)(rs, 9); +true + # gap> STOP_TEST("failures.tst", 1); From b7ea0b2628d838b3c914cb2d51053068d111a33d Mon Sep 17 00:00:00 2001 From: Christopher Jefferson Date: Thu, 1 Oct 2026 14:08:21 +0100 Subject: [PATCH 4/8] Make QC_LastFailure describe the most recent check QC_LastFailure returned the arguments of the last test run, even when that test passed, and returned the arguments after the tested function had a chance to modify them. It returned fail rather than the documented false when nothing had failed, and after QC_CheckEqual it returned an internal wrapper rather than the documented funcs. Each call to QC_Check or QC_CheckEqual now clears it, and sets it only if the check fails on a particular input, recording the unmodified arguments. QC_RerunLastFailure reruns on a copy of those arguments. Co-Authored-By: Claude Opus 5.5 --- gap/quickcheck.gd | 12 ++++++----- gap/quickcheck.gi | 40 +++++++++++++++-------------------- tst/tst-gap/failures.tst | 45 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 69 insertions(+), 28 deletions(-) diff --git a/gap/quickcheck.gd b/gap/quickcheck.gd index b6ca462..f2ab1ba 100644 --- a/gap/quickcheck.gd +++ b/gap/quickcheck.gd @@ -49,18 +49,20 @@ 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 args (the arguments) and a function `func` #! (if `QC_Check` failed) or a list of functions `funcs` (if `QC_CheckEqual` failed). -#! Returns false if no test has failed. +#! Returns false otherwise. 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 . +#! This is most useful if a test in a '.tst' file failed, as this will allow the +#! test to enter the break loop. Returns fail if +#! returns false. DeclareGlobalFunction("QC_RerunLastFailure"); #! @Description diff --git a/gap/quickcheck.gi b/gap/quickcheck.gi index dd4ebf1..7b8d4f3 100644 --- a/gap/quickcheck.gi +++ b/gap/quickcheck.gi @@ -73,12 +73,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); @@ -114,11 +115,11 @@ _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; @@ -130,6 +131,7 @@ _QC.Check := function(argtypes, func, configarg...) 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; @@ -145,12 +147,12 @@ 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); @@ -158,10 +160,7 @@ 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; @@ -183,27 +182,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); diff --git a/tst/tst-gap/failures.tst b/tst/tst-gap/failures.tst index 029732e..c28c3a3 100644 --- a/tst/tst-gap/failures.tst +++ b/tst/tst-gap/failures.tst @@ -22,5 +22,50 @@ true gap> b = QC_SetOf(IsInt)(rs, 9); true +# QC_LastFailure records the unmodified arguments of the last failure +gap> f := function(l) Add(l, 1000); return Length(l) < 5; end;; +gap> QC_Check([QC_ListOf(IsPosInt)], f); +Test 90 of 500 failed: + Input: [ [ 1, 4, 3, 3 ] ] + Output: false +false +gap> QC_LastFailure().args; +[ [ 1, 4, 3, 3 ] ] +gap> QC_LastFailure().func = f; +true +gap> QC_RerunLastFailure(); +false +gap> QC_LastFailure().args; +[ [ 1, 4, 3, 3 ] ] + +# Every check clears it, including passing ones and those with too many skips +gap> QC_Check([IsInt], x -> true); +true +gap> QC_LastFailure(); +false +gap> QC_RerunLastFailure(); +fail +gap> QC_Check([QC_ListOf(IsPosInt)], f);; +Test 90 of 500 failed: + Input: [ [ 1, 4, 3, 3 ] ] + Output: false +gap> QC_Check([IsInt], x -> QC_Skip, rec(tests := 2)); +Too many tests skipped. Only managed 0 out of 2 tests +false +gap> QC_LastFailure(); +false + +# QC_CheckEqual records both functions +gap> sq := x -> x^2;; +gap> QC_CheckEqual([IsPosInt], IdFunc, sq); +Test 31 of 500 failed: + Input: [ 2 ] + Output: Return values differ: 2 and 4 +false +gap> QC_LastFailure() = rec(args := [ 2 ], funcs := [ IdFunc, sq ]); +true +gap> QC_RerunLastFailure(); +"Return values differ: 2 and 4" + # gap> STOP_TEST("failures.tst", 1); From 610045030825e285f0343fc6dbcc1c7e41702225 Mon Sep 17 00:00:00 2001 From: Christopher Jefferson Date: Thu, 1 Oct 2026 14:12:38 +0100 Subject: [PATCH 5/8] Make public generator functions and QC_Skip read-only QC_Skip, QC_RegisterFilterGen and the QC_ListOf family were plain global assignments, so user code could overwrite them by accident. QC_Skip is now also immutable, as a test compares results against it. Co-Authored-By: Claude Opus 5.5 --- gap/generators.g | 18 +++++++++--------- gap/quickcheck.gd | 8 ++++++++ gap/quickcheck.gi | 6 ++---- 3 files changed, 19 insertions(+), 13 deletions(-) diff --git a/gap/generators.g b/gap/generators.g index b2eabad..38a63e6 100644 --- a/gap/generators.g +++ b/gap/generators.g @@ -40,22 +40,22 @@ end); -QC_ListOf := function(object) +InstallGlobalFunction(QC_ListOf, function(object) return function(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(rg, [0..limit]); @@ -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]))); diff --git a/gap/quickcheck.gd b/gap/quickcheck.gd index f2ab1ba..01a3473 100644 --- a/gap/quickcheck.gd +++ b/gap/quickcheck.gd @@ -83,5 +83,13 @@ DeclareGlobalFunction("QC_SetConfig"); DeclareGlobalFunction("QC_GetConfig"); +BindGlobal("QC_Skip", Immutable("SKIP TEST")); +DeclareGlobalFunction("QC_RegisterFilterGen"); +DeclareGlobalFunction("QC_ListOf"); +DeclareGlobalFunction("QC_FixedLengthListOf"); +DeclareGlobalFunction("QC_PairOf"); +DeclareGlobalFunction("QC_SetOf"); +DeclareGlobalFunction("QC_ElementOf"); + ## For private data BindGlobal("_QC", rec()); diff --git a/gap/quickcheck.gi b/gap/quickcheck.gi index 7b8d4f3..4e065a9 100644 --- a/gap/quickcheck.gi +++ b/gap/quickcheck.gi @@ -4,14 +4,12 @@ # Implementations # -QC_Skip := "SKIP TEST"; - # 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, From 3638f3010e328adf8c83513b6764f3a95da57225 Mon Sep 17 00:00:00 2001 From: Christopher Jefferson Date: Thu, 1 Oct 2026 14:14:45 +0100 Subject: [PATCH 6/8] Make QC_Skip an object no test result can equal QC_Skip was the string "SKIP TEST" and was compared with =, so a tested function which genuinely returned that string was treated as skipped. It is now the only object of its own family, and is compared with IsIdenticalObj. Co-Authored-By: Claude Opus 5.5 --- gap/quickcheck.gd | 4 +++- gap/quickcheck.gi | 7 +++++-- tst/tst-gap/failures.tst | 7 +++++++ 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/gap/quickcheck.gd b/gap/quickcheck.gd index 01a3473..eb6310b 100644 --- a/gap/quickcheck.gd +++ b/gap/quickcheck.gd @@ -83,7 +83,9 @@ DeclareGlobalFunction("QC_SetConfig"); DeclareGlobalFunction("QC_GetConfig"); -BindGlobal("QC_Skip", Immutable("SKIP TEST")); +# A unique object, compared with IsIdenticalObj +DeclareCategory("IsQCSkip", IsObject); +BindGlobal("QC_Skip", Objectify(NewType(NewFamily("QCSkipFamily"), IsQCSkip and IsPositionalObjectRep), [])); DeclareGlobalFunction("QC_RegisterFilterGen"); DeclareGlobalFunction("QC_ListOf"); DeclareGlobalFunction("QC_FixedLengthListOf"); diff --git a/gap/quickcheck.gi b/gap/quickcheck.gi index 4e065a9..30e72d3 100644 --- a/gap/quickcheck.gi +++ b/gap/quickcheck.gi @@ -4,6 +4,8 @@ # Implementations # +InstallMethod(PrintObj, [IsQCSkip], function(x) Print("QC_Skip"); end); + # Store the generators we support DeclareOperation("QC_Filters", [IsObject]); @@ -123,7 +125,7 @@ _QC.Check := function(argtypes, func, configarg...) 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); @@ -164,7 +166,8 @@ InstallGlobalFunction(QC_CheckEqual, local retL, retR; retL := CallFuncListWrap(funcL, StructuralCopy(args)); retR := CallFuncListWrap(funcR, args); - if retL = [QC_Skip] or retR = [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; diff --git a/tst/tst-gap/failures.tst b/tst/tst-gap/failures.tst index c28c3a3..33d188d 100644 --- a/tst/tst-gap/failures.tst +++ b/tst/tst-gap/failures.tst @@ -6,6 +6,13 @@ gap> QC_CheckEqual([IsInt], skipZero, x -> x); true gap> QC_CheckEqual([IsInt], x -> x, skipZero); true +gap> QC_Check([IsInt], x -> "SKIP TEST", rec(tests := 5)); +Test 0 of 5 failed: + Input: [ 0 ] + Output: SKIP TEST +false +gap> QC_Skip; +QC_Skip gap> QC_CheckEqual([IsInt], x -> 1, function(x) end, rec(tests := 5)); Test 0 of 5 failed: Input: [ 0 ] From 9647164144fc5f1a68f44bd4d616b92a8920192a Mon Sep 17 00:00:00 2001 From: Christopher Jefferson Date: Thu, 1 Oct 2026 14:17:45 +0100 Subject: [PATCH 7/8] Document QC_Skip, QC_RegisterFilterGen and the argument descriptions These were only mentioned in the tutorial. QC_Skip is now declared with DeclareGlobalName so AutoDoc documents it as a variable, which needs AutoDoc 2026.03.17. Co-Authored-By: Claude Opus 5.5 --- gap/quickcheck.gd | 56 ++++++++++++++++++++++++++++++++++++++++++-- gap/quickcheck.gi | 2 ++ makedoc.g | 4 ++-- tst/quickcheck01.tst | 14 +++++------ tst/quickcheck02.tst | 19 +++++++++++++++ 5 files changed, 84 insertions(+), 11 deletions(-) create mode 100644 tst/quickcheck02.tst diff --git a/gap/quickcheck.gd b/gap/quickcheck.gd index eb6310b..1706aca 100644 --- a/gap/quickcheck.gd +++ b/gap/quickcheck.gd @@ -83,14 +83,66 @@ DeclareGlobalFunction("QC_SetConfig"); DeclareGlobalFunction("QC_GetConfig"); -# A unique object, compared with IsIdenticalObj DeclareCategory("IsQCSkip", IsObject); -BindGlobal("QC_Skip", Objectify(NewType(NewFamily("QCSkipFamily"), IsQCSkip and IsPositionalObjectRep), [])); + +#! @Description +#! A function tested by or +#! can return QC_Skip 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 false. +DeclareGlobalName("QC_Skip"); + +#! @Description +#! Register gen as a generator for arguments described by the filter +#! filter. gen is called as gen(rs, limit), where rs is a +#! random source and limit a positive integer bounding the size of the value. +#! It is up to gen to decide how to interpret limit. +#! Smaller values of limit are used first, so simple inputs are tested +#! before complex ones. +#! Values not in filter 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 gen 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 ), or a function gen(rs, limit). +#! The functions below build descriptions from other descriptions. + +#! @Description +#! Describes a list of between 0 and limit values described by desc. +#! @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 len values described by desc. +#! @Arguments desc, len DeclareGlobalFunction("QC_FixedLengthListOf"); + +#! @Description +#! Describes a list of length two, whose entries are described by desc. +#! @Arguments desc DeclareGlobalFunction("QC_PairOf"); + +#! @Description +#! Describes a set of between 0 and limit values described by desc. +#! @Arguments desc DeclareGlobalFunction("QC_SetOf"); + +#! @Description +#! Describes a random element of the collection coll. This ignores +#! limit. +#! @Arguments coll DeclareGlobalFunction("QC_ElementOf"); ## For private data diff --git a/gap/quickcheck.gi b/gap/quickcheck.gi index 30e72d3..92747b9 100644 --- a/gap/quickcheck.gi +++ b/gap/quickcheck.gi @@ -4,6 +4,8 @@ # Implementations # +# 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 diff --git a/makedoc.g b/makedoc.g index ad40f0c..40e133a 100644 --- a/makedoc.g +++ b/makedoc.g @@ -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 ) ); diff --git a/tst/quickcheck01.tst b/tst/quickcheck01.tst index fe9eb91..58b8c47 100644 --- a/tst/quickcheck01.tst +++ b/tst/quickcheck01.tst @@ -17,25 +17,25 @@ gap> testFunc := function(a,b) gap> QC_Check([IsInt, IsInt], testFunc); true -# doc/_Chapter_Tutorial.xml:30-36 +# doc/_Chapter_Tutorial.xml:29-35 gap> QC_Check([IsPerm, IsPerm], testFunc); Test 60 of 500 failed: Input: [ (1,3), (1,3,2) ] Output: false false -# doc/_Chapter_Tutorial.xml:44-47 +# doc/_Chapter_Tutorial.xml:42-45 gap> QC_LastFailure(); rec( args := [ (1,3), (1,3,2) ], func := function( a, b ) ... end ) -# doc/_Chapter_Tutorial.xml:53-59 +# doc/_Chapter_Tutorial.xml:50-56 gap> slowIntersection := function(g,h) > return Group(Filtered(g, p -> p in h)); > end;; gap> QC_CheckEqual([IsPermGroup, IsPermGroup], Intersection, slowIntersection); true -# doc/_Chapter_Tutorial.xml:82-92 +# doc/_Chapter_Tutorial.xml:77-87 gap> checkDiv := function(a,b) > if b = 0 then return QC_Skip; fi; > return b*(a/b); @@ -46,7 +46,7 @@ gap> justA := function(a,b) gap> QC_CheckEqual([IsInt, IsInt], checkDiv, justA); true -# doc/_Chapter_Tutorial.xml:183-197 +# doc/_Chapter_Tutorial.xml:176-190 gap> func := function(x) > local a, s; > if x < 2 then @@ -61,7 +61,7 @@ gap> func := function(x) gap> QC_Check([IsInt], func); true -# doc/_Chapter_Tutorial.xml:214-221 +# doc/_Chapter_Tutorial.xml:205-212 gap> makePosInt := function(rs, limit) > return Random(rs, [1..limit]); > end;; @@ -69,7 +69,7 @@ gap> makePerm := function(rs, limit) > return Random(rs, SymmetricGroup(limit)); > end;; -# doc/_Chapter_Tutorial.xml:227-230 +# doc/_Chapter_Tutorial.xml:217-220 gap> QC_Check([makePosInt, makePerm, makePerm], {r,p1,p2} -> (r^p1)^p2 = r^(p1*p2)); true diff --git a/tst/quickcheck02.tst b/tst/quickcheck02.tst new file mode 100644 index 0000000..7f1d34b --- /dev/null +++ b/tst/quickcheck02.tst @@ -0,0 +1,19 @@ +# quickcheck, chapter 3 +# +# DO NOT EDIT THIS FILE - EDIT EXAMPLES IN THE SOURCE INSTEAD! +# +# This file has been generated by AutoDoc. It contains examples extracted from +# the package documentation. Each example is preceded by a comment which gives +# the name of a GAPDoc XML file and a line range from which the example were +# taken. Note that the XML file in turn may have been generated by AutoDoc +# from some other input. +# +gap> START_TEST("quickcheck02.tst"); + +# doc/_Chapter_Functionality.xml:131-135 +gap> QC_Check([QC_ListOf(QC_PairOf(IsPosInt))], +> l -> ForAll(l, p -> Length(p) = 2 and ForAll(p, IsPosInt))); +true + +# +gap> STOP_TEST("quickcheck02.tst", 1); From 9b30f10d23ff1147c9365076766bfb3dadccf598 Mon Sep 17 00:00:00 2001 From: Christopher Jefferson Date: Thu, 1 Oct 2026 14:28:39 +0100 Subject: [PATCH 8/8] Fix argument lists and missing config options in the manual QC_LastFailure, QC_RerunLastFailure and QC_GetConfig were shown as taking an argument, QC_SetConfig did not list the ramp and catchErrors options, and the tutorial referred to a nonexistent QC_Equal. Co-Authored-By: Claude Opus 5.5 --- examples/using.autodoc | 2 +- gap/quickcheck.gd | 13 ++++++++++--- tst/quickcheck02.tst | 2 +- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/examples/using.autodoc b/examples/using.autodoc index 8bf9ea4..9c559c7 100644 --- a/examples/using.autodoc +++ b/examples/using.autodoc @@ -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 diff --git a/gap/quickcheck.gd b/gap/quickcheck.gd index 1706aca..e362982 100644 --- a/gap/quickcheck.gd +++ b/gap/quickcheck.gd @@ -55,6 +55,7 @@ DeclareGlobalFunction("QC_CheckEqual"); #! Returns a record containing args (the arguments) and a function `func` #! (if `QC_Check` failed) or a list of functions `funcs` (if `QC_CheckEqual` failed). #! Returns false otherwise. +#! @Arguments DeclareGlobalFunction("QC_LastFailure"); @@ -63,6 +64,7 @@ DeclareGlobalFunction("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 fail if #! returns false. +#! @Arguments DeclareGlobalFunction("QC_RerunLastFailure"); #! @Description @@ -70,9 +72,13 @@ DeclareGlobalFunction("QC_RerunLastFailure"); #! to set all options. #! #! Current options are: -#! * tests: Number of tests to run -#! * limit: The size of the largest object to create -#! * seed: Initial random seed +#! * tests: Number of tests to run (default 500) +#! * limit: The size of the largest object to create (default 9) +#! * ramp: Number of tests, including skipped ones, to run at each size +#! before increasing it (default 30) +#! * seed: Initial random seed (default 1) +#! * catchErrors: If true, an error in the tested function counts +#! as a failure; if false, it enters the break loop (default true) #! #! @Arguments config DeclareGlobalFunction("QC_SetConfig"); @@ -80,6 +86,7 @@ DeclareGlobalFunction("QC_SetConfig"); #! @Description #! Get the current global configuration for QuickCheck, as a record +#! @Arguments DeclareGlobalFunction("QC_GetConfig"); diff --git a/tst/quickcheck02.tst b/tst/quickcheck02.tst index 7f1d34b..8d92676 100644 --- a/tst/quickcheck02.tst +++ b/tst/quickcheck02.tst @@ -10,7 +10,7 @@ # gap> START_TEST("quickcheck02.tst"); -# doc/_Chapter_Functionality.xml:131-135 +# doc/_Chapter_Functionality.xml:139-143 gap> QC_Check([QC_ListOf(QC_PairOf(IsPosInt))], > l -> ForAll(l, p -> Length(p) = 2 and ForAll(p, IsPosInt))); true