Skip to content

Commit c6268d1

Browse files
author
Release Manager
committed
Trac #29935: implicitly fuzz RNG-dependent doctests with a random random seed
Our documentation and tests include a variety of examples and tests involving randomness. Those depend on a randomness seed, and up to Sage 9.1 they always used the same one: 0. Thus every run of `sage -t` or `make [p]test[all][long]` would run those "random" doctests deterministically. In many cases, the output of those tests even relied on that. As a result, random examples and tests were actually testing that they were run non-randomly! This is reminiscent of an [https://xkcd.com/221/ xkcd comic illustration of random number generators]. Based on these considerations and the related [https://groups.google.com/d/msg/sage- devel/c4UbKSdt3Aw/UQAo1iYoAAAJ sage-devel discussion], we propose to: - allow specifying a randomness when running tests (#29962), - adapt tests involving randomness, making sure they test mathematical functionality independent of the randomness seed used to run them (see roadmap), - default to a random randomness seed when none is specified (present ticket), thus making those tests more robust and more useful, by becoming more likely to reveal bugs in a variety of cases including corner cases. The first step (see #29962, merged in Sage 9.2) adds a `--random-seed` flag to `sage -t`, allowing: {{{ sage -t --long --random-seed=9876543210 src/sage/ }}} Still, when no randomness seed is specified, the default seed 0 is used. This means most testers test with the same randomness seed, making "random" doctests still mostly deterministic in practice. Here is a way to pick a random randomness seed and run tests with it (can be used to work on the tickets in the roadmap): {{{ $ randseed() { sage -c "import sage.misc.randstate as randstate; \ randstate.set_random_seed(); \ print(randstate.initial_seed())"; } $ SEED=$(randseed) $ DIR=src/sage $ echo "$ sage -t --long --random-seed=${SEED} ${DIR}" \ && ./sage -t --long --random-seed=${SEED} ${DIR} }}} Once examples and tests involving randomness have been adapted, the present ticket puts the final touch by making it so that when running tests with no seed specified, a random one will be used: {{{ sage -t src/sage/all.py Running doctests with ID 2020-06-23-23-19-03-8003eea5. ... sage -t --warn-long 89.5 --random- seed=273987373473433732996760115183658447263 src/sage/all.py [16 tests, 0.73 s] ---------------------------------------------------------------------- All tests passed! ---------------------------------------------------------------------- Total time for all tests: 0.8 seconds cpu time: 0.7 seconds cumulative wall time: 0.7 seconds }}} Being displayed in the output, the seed used can be used again if needed: {{{ sage -t --warn-long 89.5 --random- seed=273987373473433732996760115183658447263 src/sage/all.py }}} allowing to explore any problematic case revealed by running the tests with that seed. Roadmap: - Allow fuzzing: #29962 - Make all parts of sage ready for default fuzzing: - #29945: coding - #29963: geometry - #29964: libs - #29965: graphs - #32107: groups - #29967: interfaces - #29968: algebras - #29969: misc - #29970: arith - #29971: categories - #29972: stats - #29973: sets - #29974: combinat - #29975: numerical, probability - #29976: matrix - #29977: modular - #29978: modules - #29979: rings - #29980: crypto - #29981: documentation - #29982: dynamics - #29983: finance - #29984: symbolic - #29985: schemes - #29986: plot - #32188: various missed tests along the way - #32216: Update the developers guide for implicitly fuzzing doctests - Finally make fuzzing default with this ticket. Follow-up: - #32544: Meta-ticket: Fix unstable doctests detected after #29935 - Remove `set_random_seed` in doctests where possible. Errors discovered by this ticket: - #29936: Hyperbolic geometry bugs revealed by fuzzing - #29945: Failing doctest in `src/sage/coding/linear_code.py` (just a wrong doctest, will be fixed here) - #29954: Unstable plotting - #29956: Bug in `KlyachkoBundle_class.random_deformation` - #29957: Bug in `ContinuedFraction` rounding - #29958: Too many strong articulation points - #29961: Random symbolic expression is completely unstable - #30045: Bug in Reed-Solomon encoder and error-erasure decoder - Index error with random derangement (fixed in #29974) - #31890: simplify_hypergeometric is unstable - #31891: `ZeroDivisonError` when creating polynomial system - #31892: Conic parametrization broken - #32075: Polynomial generic power trunk broken - #32083: Various errors with polybori including segmentation fault - #32084 `_nth_root_naive` fails for integer mod - #32085: Errors when computing norms of padic elements - #32086: apply_homography unstable for continue fraction - #32095: DiFUB algorithm fails on some random graph - #32108: Fix random tree on one or less vertices - #32109: Fix 0/0 in ore function field - #32111: Unstable minimal polynomial for element of 2-adic Eisenstein Extension Field in pi defined by x^4 - 2*a - #32117: Random relative number field checks only irreducibility over QQ - #32118: AlgebraicForm checks invariance with random matrix that can be the identity - #32124: SL2Z.random_element unstable - #32125: random Ore polynomials do not respect minimum degree bound - #32126: padic QpLC.random_element is broken - #32127: gosper_iterator of continued fractions is unstable - #32129: sage_input is unreliable for elements of ComplexField - #32131: Cut width of graph with one edge incorrect - #32132: Wrong gyration orbit length - #32138: is_groebner fails over fraction fields - #32141: Unstable doctest involving permutation groups - #32169: Bug in edge disjoint spanning trees - #32185: Failing weak order assertion on random symbolic expression - #32186: Random bounded tolerance graph - #32187: permutation group generated by list perms in L of degree n incorrect when compared to GAP - #32657: `plot_vector_field` unstable URL: https://trac.sagemath.org/29935 Reported by: dimpase Ticket author(s): Jonathan Kliem Reviewer(s): Michael Orlitzky
2 parents ec501d3 + 047379c commit c6268d1

File tree

18 files changed

+84
-27
lines changed

18 files changed

+84
-27
lines changed

src/bin/sage

+1-1
Original file line numberDiff line numberDiff line change
@@ -466,7 +466,7 @@ usage_advanced() {
466466
echo " labeled \"# optional\" or labeled"
467467
echo " \"# optional tag\" for any of the tags given."
468468
echo " --randorder[=seed] -- randomize order of tests"
469-
echo " --random-seed[=seed] -- random seed for fuzzing doctests"
469+
echo " --random-seed[=seed] -- random seed (integer) for fuzzing doctests"
470470
echo " --new -- only test files modified since last commit"
471471
echo " --initial -- only show the first failure per block"
472472
echo " --debug -- drop into PDB after an unexpected error"

src/bin/sage-runtests

+1-1
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ if __name__ == "__main__":
5656
'if "build" is listed, will also run tests specific to Sage\'s build/packaging system; '
5757
'if set to "all", then all tests will be run')
5858
parser.add_argument("--randorder", type=int, metavar="SEED", help="randomize order of tests")
59-
parser.add_argument("--random-seed", dest="random_seed", type=int, metavar="SEED", help="random seed for fuzzing doctests")
59+
parser.add_argument("--random-seed", dest="random_seed", type=int, metavar="SEED", help="random seed (integer) for fuzzing doctests")
6060
parser.add_argument("--global-iterations", "--global_iterations", type=int, default=0, help="repeat the whole testing process this many times")
6161
parser.add_argument("--file-iterations", "--file_iterations", type=int, default=0, help="repeat each file this many times, stopping on the first failure")
6262
parser.add_argument("--environment", type=str, default="sage.repl.ipython_kernel.all_jupyter", help="name of a module that provides the global environment for tests")

src/doc/en/developer/doctesting.rst

+27-1
Original file line numberDiff line numberDiff line change
@@ -803,7 +803,33 @@ You can also pass in an explicit amount of time::
803803
Finally, you can disable any warnings about long tests with
804804
``--warn-long 0``.
805805

806-
Doctests may start from a random seed::
806+
Doctests start from a random seed::
807+
808+
[kliem@sage sage-9.2]$ sage -t src/sage/doctest/tests/random_seed.rst
809+
Running doctests with ID 2020-06-23-23-22-59-49f37a55.
810+
...
811+
Doctesting 1 file.
812+
sage -t --warn-long 89.5 --random-seed=112986622569797306072457879734474628454 src/sage/doctest/tests/random_seed.rst
813+
**********************************************************************
814+
File "src/sage/doctest/tests/random_seed.rst", line 3, in sage.doctest.tests.random_seed
815+
Failed example:
816+
randint(5, 10)
817+
Expected:
818+
9
819+
Got:
820+
8
821+
**********************************************************************
822+
1 item had failures:
823+
1 of 2 in sage.doctest.tests.random_seed
824+
[1 test, 1 failure, 0.00 s]
825+
----------------------------------------------------------------------
826+
sage -t --warn-long 89.5 --random-seed=112986622569797306072457879734474628454 src/sage/doctest/tests/random_seed.rst # 1 doctest failed
827+
----------------------------------------------------------------------
828+
Total time for all tests: 0.0 seconds
829+
cpu time: 0.0 seconds
830+
cumulative wall time: 0.0 seconds
831+
832+
This seed can be set explicitly to reproduce possible failures::
807833

808834
[kliem@sage sage-9.2]$ sage -t --warn-long 89.5 --random-seed=112986622569797306072457879734474628454 src/sage/doctest/tests/random_seed.rst
809835
Running doctests with ID 2020-06-23-23-24-28-14a52269.

src/sage/arith/misc.py

+19-6
Original file line numberDiff line numberDiff line change
@@ -5667,12 +5667,25 @@ def sort_complex_numbers_for_display(nums):
56675667
....: RDF.random_element()))
56685668
sage: shuffle(nums)
56695669
sage: nums = sort_c(nums)
5670-
sage: nums[:3]
5671-
[0.0, 1.0, 2.0]
5672-
sage: for i in range(3, len(nums)-1):
5673-
....: assert nums[i].real() <= nums[i+1].real() + 1e-10
5674-
....: if abs(nums[i].real() - nums[i+1].real()) < 1e-10:
5675-
....: assert nums[i].imag() <= nums[i+1].imag() + 1e-10
5670+
sage: for i in range(len(nums)):
5671+
....: if nums[i].imag():
5672+
....: first_non_real = i
5673+
....: break
5674+
....: else:
5675+
....: first_non_real = len(nums)
5676+
sage: assert first_non_real >= 3
5677+
sage: for i in range(first_non_real - 1):
5678+
....: assert nums[i].real() <= nums[i + 1].real()
5679+
5680+
sage: def truncate(n):
5681+
....: if n.real() < 1e-10:
5682+
....: return 0
5683+
....: else:
5684+
....: return n.real().n(digits=9)
5685+
sage: for i in range(first_non_real, len(nums)-1):
5686+
....: assert truncate(nums[i]) <= truncate(nums[i + 1])
5687+
....: if truncate(nums[i]) == truncate(nums[i + 1]):
5688+
....: assert nums[i].imag() <= nums[i+1].imag()
56765689
"""
56775690
if not nums:
56785691
return nums

src/sage/crypto/util.py

+2
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,8 @@ def carmichael_lambda(n):
334334
....: L = coprime(n)
335335
....: return list(map(power_mod, L, [k]*len(L), [n]*len(L)))
336336
sage: def my_carmichael(n):
337+
....: if n == 1:
338+
....: return 1
337339
....: for k in range(1, n):
338340
....: L = znpower(n, k)
339341
....: ones = [1] * len(L)

src/sage/doctest/control.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import re
2929
import types
3030
import sage.misc.flatten
31+
import sage.misc.randstate as randstate
3132
from sage.structure.sage_object import SageObject
3233
from sage.env import DOT_SAGE, SAGE_LIB, SAGE_SRC, SAGE_VENV, SAGE_EXTCODE
3334
from sage.misc.temporary_file import tmp_dir
@@ -461,7 +462,8 @@ def __init__(self, options, args):
461462
self._init_warn_long()
462463

463464
if self.options.random_seed is None:
464-
self.options.random_seed = 0
465+
randstate.set_random_seed()
466+
self.options.random_seed = randstate.initial_seed()
465467

466468
def __del__(self):
467469
if getattr(self, 'logfile', None) is not None:

src/sage/functions/exp_integral.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1497,7 +1497,7 @@ def exponential_integral_1(x, n=0):
14971497
....: n = 2^ZZ.random_element(14)
14981498
....: x = exponential_integral_1(a, n)
14991499
....: y = exponential_integral_1(S(a), n)
1500-
....: c = RDF(2 * max(1.0, y[0]))
1500+
....: c = RDF(4 * max(1.0, y[0]))
15011501
....: for i in range(n):
15021502
....: e = float(abs(S(x[i]) - y[i]) << prec)
15031503
....: if e >= c:

src/sage/functions/orthogonal_polys.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -2083,7 +2083,7 @@ class Func_ultraspherical(GinacFunction):
20832083
32*t^3 - 12*t
20842084
sage: _ = var('x')
20852085
sage: for N in range(100):
2086-
....: n = ZZ.random_element().abs() + 5
2086+
....: n = ZZ.random_element(5, 5001)
20872087
....: a = QQ.random_element().abs() + 5
20882088
....: assert ((n+1)*ultraspherical(n+1,a,x) - 2*x*(n+a)*ultraspherical(n,a,x) + (n+2*a-1)*ultraspherical(n-1,a,x)).expand().is_zero()
20892089
sage: ultraspherical(5,9/10,3.1416)

src/sage/graphs/generic_graph.py

+7-2
Original file line numberDiff line numberDiff line change
@@ -6331,8 +6331,11 @@ def edge_disjoint_spanning_trees(self, k, root=None, solver=None, verbose=0):
63316331
By Edmond's theorem, a graph which is `k`-connected always has `k`
63326332
edge-disjoint arborescences, regardless of the root we pick::
63336333

6334-
sage: g = digraphs.RandomDirectedGNP(28, .3) # reduced from 30 to 28, cf. #9584
6334+
sage: g = digraphs.RandomDirectedGNP(11, .3) # reduced from 30 to 11, cf. #32169
63356335
sage: k = Integer(g.edge_connectivity())
6336+
sage: while not k:
6337+
....: g = digraphs.RandomDirectedGNP(11, .3)
6338+
....: k = Integer(g.edge_connectivity())
63366339
sage: arborescences = g.edge_disjoint_spanning_trees(k) # long time (up to 15s on sage.math, 2011)
63376340
sage: all(a.is_directed_acyclic() for a in arborescences) # long time
63386341
True
@@ -6341,7 +6344,9 @@ def edge_disjoint_spanning_trees(self, k, root=None, solver=None, verbose=0):
63416344

63426345
In the undirected case, we can only ensure half of it::
63436346

6344-
sage: g = graphs.RandomGNP(30, .3)
6347+
sage: g = graphs.RandomGNP(14, .3) # reduced from 30 to 14, see #32169
6348+
sage: while not g.is_biconnected():
6349+
....: g = graphs.RandomGNP(14, .3)
63456350
sage: k = Integer(g.edge_connectivity()) // 2
63466351
sage: trees = g.edge_disjoint_spanning_trees(k)
63476352
sage: all(t.is_tree() for t in trees)

src/sage/graphs/tutte_polynomial.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -544,10 +544,12 @@ def tutte_polynomial(G, edge_selector=None, cache=None):
544544
+ 105*x^2*y^2 + 65*x*y^3 + 35*y^4 + 180*x^3 + 240*x^2*y + 171*x*y^2
545545
+ 75*y^3 + 120*x^2 + 168*x*y + 84*y^2 + 36*x + 36*y
546546
547-
The Tutte polynomial of `G` evaluated at (1,1) is the number of
547+
The Tutte polynomial of a connected graph `G` evaluated at (1,1) is the number of
548548
spanning trees of `G`::
549549
550550
sage: G = graphs.RandomGNP(10,0.6)
551+
sage: while not G.is_connected():
552+
....: G = graphs.RandomGNP(10,0.6)
551553
sage: G.tutte_polynomial()(1,1) == G.spanning_trees_count()
552554
True
553555

src/sage/groups/perm_gps/permgroup_morphism.py

+4-5
Original file line numberDiff line numberDiff line change
@@ -117,11 +117,10 @@ def image(self, J):
117117
sage: G = L.galois_group()
118118
sage: D4 = DihedralGroup(4)
119119
sage: h = D4.isomorphism_to(G)
120-
sage: h.image(D4)
121-
Subgroup generated by [(1,2)(3,4)(5,7)(6,8), (1,6,4,7)(2,5,3,8)] of (Galois group 8T4 ([4]2) with order 8 of x^8 + 4*x^7 + 12*x^6 + 22*x^5 + 23*x^4 + 14*x^3 + 28*x^2 + 24*x + 16)
122-
sage: r, s = D4.gens()
123-
sage: h.image(r)
124-
(1,6,4,7)(2,5,3,8)
120+
sage: h.image(D4).is_isomorphic(G)
121+
True
122+
sage: all(h.image(g) in G for g in D4.gens())
123+
True
125124
"""
126125
H = self.codomain()
127126
if J in self.domain():

src/sage/matrix/matrix0.pyx

+4-1
Original file line numberDiff line numberDiff line change
@@ -4830,7 +4830,10 @@ cdef class Matrix(sage.structure.element.Matrix):
48304830
sage: B.multiplicative_order()
48314831
1
48324832
4833-
sage: E = MatrixSpace(GF(11^2,'e'),5).random_element()
4833+
sage: M = MatrixSpace(GF(11^2,'e'),5)
4834+
sage: E = M.random_element()
4835+
sage: while E.det() == 0:
4836+
....: E = M.random_element()
48344837
sage: (E^E.multiplicative_order()).is_one()
48354838
True
48364839

src/sage/misc/misc.py

+2
Original file line numberDiff line numberDiff line change
@@ -803,6 +803,8 @@ def __rmul__(self, left):
803803
EXAMPLES::
804804
805805
sage: A = random_matrix(ZZ, 4)
806+
sage: while A.rank() != 4:
807+
....: A = random_matrix(ZZ, 4)
806808
sage: B = random_matrix(ZZ, 4)
807809
sage: temp = A * BackslashOperator()
808810
sage: temp.left is A

src/sage/modular/modform/numerical.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -444,7 +444,7 @@ def systems_of_eigenvalues(self, bound):
444444
445445
EXAMPLES::
446446
447-
sage: numerical_eigenforms(61).systems_of_eigenvalues(10) # rel tol 1e-12
447+
sage: numerical_eigenforms(61).systems_of_eigenvalues(10) # rel tol 1e-11
448448
[
449449
[-1.4811943040920152, 0.8060634335253695, 3.1563251746586642, 0.6751308705666477],
450450
[-1.0, -2.0000000000000027, -3.000000000000003, 1.0000000000000044],
@@ -471,7 +471,7 @@ def systems_of_abs(self, bound):
471471
472472
EXAMPLES::
473473
474-
sage: numerical_eigenforms(61).systems_of_abs(10) # rel tol 1e-12
474+
sage: numerical_eigenforms(61).systems_of_abs(10) # rel tol 1e-11
475475
[
476476
[0.3111078174659775, 2.903211925911551, 2.525427560843529, 3.214319743377552],
477477
[1.0, 2.0000000000000027, 3.000000000000003, 1.0000000000000044],

src/sage/stats/hmm/chmm.pyx

+1-1
Original file line numberDiff line numberDiff line change
@@ -903,7 +903,7 @@ cdef class GaussianHiddenMarkovModel(HiddenMarkovModel):
903903
sage: m = hmm.GaussianHiddenMarkovModel([[.1,.9],[.5,.5]], [(1,.5), (-1,3)], [.1,.9])
904904
sage: v = m.sample(10)
905905
sage: l = stats.TimeSeries([m.baum_welch(v,max_iter=1)[0] for _ in range(len(v))])
906-
sage: all(l[i] <= l[i+1] for i in range(9))
906+
sage: all(l[i] <= l[i+1] + 0.0001 for i in range(9))
907907
True
908908
sage: l # random
909909
[-20.1167, -17.7611, -16.9814, -16.9364, -16.9314, -16.9309, -16.9309, -16.9309, -16.9309, -16.9309]

src/sage/tests/book_stein_ent.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -267,8 +267,9 @@
267267
....: if g != 1 and g != n:
268268
....: return g
269269
sage: n=32295194023343; e=29468811804857; d=11127763319273
270-
sage: crack_given_decrypt(n, e*d - 1)
271-
737531
270+
sage: p = crack_given_decrypt(n, e*d - 1)
271+
sage: p in (737531, n/737531) # could be other prime divisor
272+
True
272273
sage: factor(n)
273274
737531 * 43788253
274275
sage: e = 22601762315966221465875845336488389513

src/sage/tests/books/computational-mathematics-with-sagemath/linalg_doctest.py

+2
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,8 @@
269269
sage: A = random_matrix(R,2,3); A # random
270270
[ 3*x^2 + x x^2 + 2*x 2*x^2 + 2]
271271
[ x^2 + x + 2 2*x^2 + 4*x + 3 x^2 + 4*x + 3]
272+
sage: while A.rank() < 2:
273+
....: A = random_matrix(R,2,3)
272274
273275
Sage example in ./linalg.tex, line 1830::
274276

src/sage/tests/books/computational-mathematics-with-sagemath/sol/graphique_doctest.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
sage: g = plot([c*e^(-1/x) for c in srange(-8, 8, 0.4)], (x, -3, 3))
4343
sage: y = var('y')
4444
sage: g += plot_vector_field((x^2, y), (x,-3,3), (y,-5,5))
45-
sage: g.show()
45+
sage: g.show() # not tested, known bug, see :trac:`32657`
4646
4747
Sage example in ./sol/graphique.tex, line 124::
4848

0 commit comments

Comments
 (0)