passagemath-repl 10.4.62__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- passagemath_repl-10.4.62.data/scripts/sage-cachegrind +25 -0
- passagemath_repl-10.4.62.data/scripts/sage-callgrind +16 -0
- passagemath_repl-10.4.62.data/scripts/sage-cleaner +230 -0
- passagemath_repl-10.4.62.data/scripts/sage-coverage +327 -0
- passagemath_repl-10.4.62.data/scripts/sage-eval +14 -0
- passagemath_repl-10.4.62.data/scripts/sage-fixdoctests +708 -0
- passagemath_repl-10.4.62.data/scripts/sage-inline-fortran +12 -0
- passagemath_repl-10.4.62.data/scripts/sage-ipynb2rst +50 -0
- passagemath_repl-10.4.62.data/scripts/sage-ipython +16 -0
- passagemath_repl-10.4.62.data/scripts/sage-massif +25 -0
- passagemath_repl-10.4.62.data/scripts/sage-notebook +267 -0
- passagemath_repl-10.4.62.data/scripts/sage-omega +25 -0
- passagemath_repl-10.4.62.data/scripts/sage-preparse +302 -0
- passagemath_repl-10.4.62.data/scripts/sage-run +27 -0
- passagemath_repl-10.4.62.data/scripts/sage-run-cython +10 -0
- passagemath_repl-10.4.62.data/scripts/sage-runtests +9 -0
- passagemath_repl-10.4.62.data/scripts/sage-startuptime.py +163 -0
- passagemath_repl-10.4.62.data/scripts/sage-valgrind +34 -0
- passagemath_repl-10.4.62.dist-info/METADATA +77 -0
- passagemath_repl-10.4.62.dist-info/RECORD +162 -0
- passagemath_repl-10.4.62.dist-info/WHEEL +5 -0
- passagemath_repl-10.4.62.dist-info/top_level.txt +1 -0
- sage/all__sagemath_repl.py +119 -0
- sage/doctest/__init__.py +4 -0
- sage/doctest/__main__.py +236 -0
- sage/doctest/all.py +4 -0
- sage/doctest/check_tolerance.py +261 -0
- sage/doctest/control.py +1727 -0
- sage/doctest/external.py +534 -0
- sage/doctest/fixtures.py +383 -0
- sage/doctest/forker.py +2665 -0
- sage/doctest/marked_output.py +102 -0
- sage/doctest/parsing.py +1708 -0
- sage/doctest/parsing_test.py +79 -0
- sage/doctest/reporting.py +733 -0
- sage/doctest/rif_tol.py +124 -0
- sage/doctest/sources.py +1657 -0
- sage/doctest/test.py +584 -0
- sage/doctest/tests/1second.rst +4 -0
- sage/doctest/tests/99seconds.rst +4 -0
- sage/doctest/tests/abort.rst +5 -0
- sage/doctest/tests/atexit.rst +7 -0
- sage/doctest/tests/fail_and_die.rst +6 -0
- sage/doctest/tests/initial.rst +15 -0
- sage/doctest/tests/interrupt.rst +7 -0
- sage/doctest/tests/interrupt_diehard.rst +14 -0
- sage/doctest/tests/keyboardinterrupt.rst +11 -0
- sage/doctest/tests/longtime.rst +5 -0
- sage/doctest/tests/nodoctest +5 -0
- sage/doctest/tests/random_seed.rst +4 -0
- sage/doctest/tests/show_skipped.rst +18 -0
- sage/doctest/tests/sig_on.rst +9 -0
- sage/doctest/tests/simple_failure.rst +8 -0
- sage/doctest/tests/sleep_and_raise.rst +106 -0
- sage/doctest/tests/tolerance.rst +31 -0
- sage/doctest/util.py +750 -0
- sage/interfaces/cleaner.py +48 -0
- sage/interfaces/quit.py +163 -0
- sage/misc/all__sagemath_repl.py +51 -0
- sage/misc/banner.py +235 -0
- sage/misc/benchmark.py +221 -0
- sage/misc/classgraph.py +131 -0
- sage/misc/copying.py +22 -0
- sage/misc/cython.py +694 -0
- sage/misc/dev_tools.py +745 -0
- sage/misc/edit_module.py +304 -0
- sage/misc/explain_pickle.py +3079 -0
- sage/misc/gperftools.py +361 -0
- sage/misc/inline_fortran.py +212 -0
- sage/misc/messaging.py +86 -0
- sage/misc/pager.py +21 -0
- sage/misc/profiler.py +179 -0
- sage/misc/python.py +70 -0
- sage/misc/remote_file.py +53 -0
- sage/misc/sage_eval.py +246 -0
- sage/misc/sage_input.py +3621 -0
- sage/misc/sagedoc.py +1742 -0
- sage/misc/sh.py +38 -0
- sage/misc/trace.py +90 -0
- sage/repl/__init__.py +16 -0
- sage/repl/all.py +15 -0
- sage/repl/attach.py +625 -0
- sage/repl/configuration.py +186 -0
- sage/repl/display/__init__.py +1 -0
- sage/repl/display/fancy_repr.py +354 -0
- sage/repl/display/formatter.py +318 -0
- sage/repl/display/jsmol_iframe.py +290 -0
- sage/repl/display/pretty_print.py +153 -0
- sage/repl/display/util.py +163 -0
- sage/repl/image.py +302 -0
- sage/repl/inputhook.py +91 -0
- sage/repl/interface_magic.py +298 -0
- sage/repl/interpreter.py +854 -0
- sage/repl/ipython_extension.py +593 -0
- sage/repl/ipython_kernel/__init__.py +1 -0
- sage/repl/ipython_kernel/__main__.py +4 -0
- sage/repl/ipython_kernel/all_jupyter.py +10 -0
- sage/repl/ipython_kernel/install.py +301 -0
- sage/repl/ipython_kernel/interact.py +278 -0
- sage/repl/ipython_kernel/kernel.py +217 -0
- sage/repl/ipython_kernel/widgets.py +466 -0
- sage/repl/ipython_kernel/widgets_sagenb.py +587 -0
- sage/repl/ipython_tests.py +163 -0
- sage/repl/load.py +326 -0
- sage/repl/preparse.py +2218 -0
- sage/repl/prompts.py +90 -0
- sage/repl/rich_output/__init__.py +4 -0
- sage/repl/rich_output/backend_base.py +648 -0
- sage/repl/rich_output/backend_doctest.py +316 -0
- sage/repl/rich_output/backend_emacs.py +151 -0
- sage/repl/rich_output/backend_ipython.py +596 -0
- sage/repl/rich_output/buffer.py +311 -0
- sage/repl/rich_output/display_manager.py +829 -0
- sage/repl/rich_output/example.avi +0 -0
- sage/repl/rich_output/example.canvas3d +1 -0
- sage/repl/rich_output/example.dvi +0 -0
- sage/repl/rich_output/example.flv +0 -0
- sage/repl/rich_output/example.gif +0 -0
- sage/repl/rich_output/example.jpg +0 -0
- sage/repl/rich_output/example.mkv +0 -0
- sage/repl/rich_output/example.mov +0 -0
- sage/repl/rich_output/example.mp4 +0 -0
- sage/repl/rich_output/example.ogv +0 -0
- sage/repl/rich_output/example.pdf +0 -0
- sage/repl/rich_output/example.png +0 -0
- sage/repl/rich_output/example.svg +54 -0
- sage/repl/rich_output/example.webm +0 -0
- sage/repl/rich_output/example.wmv +0 -0
- sage/repl/rich_output/example_jmol.spt.zip +0 -0
- sage/repl/rich_output/example_wavefront_scene.mtl +7 -0
- sage/repl/rich_output/example_wavefront_scene.obj +17 -0
- sage/repl/rich_output/output_basic.py +391 -0
- sage/repl/rich_output/output_browser.py +103 -0
- sage/repl/rich_output/output_catalog.py +54 -0
- sage/repl/rich_output/output_graphics.py +320 -0
- sage/repl/rich_output/output_graphics3d.py +345 -0
- sage/repl/rich_output/output_video.py +231 -0
- sage/repl/rich_output/preferences.py +432 -0
- sage/repl/rich_output/pretty_print.py +339 -0
- sage/repl/rich_output/test_backend.py +201 -0
- sage/repl/user_globals.py +214 -0
- sage/tests/__init__.py +1 -0
- sage/tests/all.py +3 -0
- sage/tests/article_heuberger_krenn_kropf_fsm-in-sage.py +630 -0
- sage/tests/arxiv_0812_2725.py +351 -0
- sage/tests/benchmark.py +1923 -0
- sage/tests/book_schilling_zabrocki_kschur_primer.py +795 -0
- sage/tests/book_stein_ent.py +651 -0
- sage/tests/book_stein_modform.py +558 -0
- sage/tests/cmdline.py +790 -0
- sage/tests/combinatorial_hopf_algebras.py +52 -0
- sage/tests/finite_poset.py +623 -0
- sage/tests/functools_partial_src.py +27 -0
- sage/tests/gosper-sum.py +218 -0
- sage/tests/lazy_imports.py +28 -0
- sage/tests/modular_group_cohomology.py +80 -0
- sage/tests/numpy.py +21 -0
- sage/tests/parigp.py +76 -0
- sage/tests/startup.py +27 -0
- sage/tests/symbolic-series.py +76 -0
- sage/tests/sympy.py +16 -0
- sage/tests/test_deprecation.py +31 -0
sage/doctest/control.py
ADDED
@@ -0,0 +1,1727 @@
|
|
1
|
+
# sage_setup: distribution = sagemath-repl
|
2
|
+
# sage.doctest: needs sage.all
|
3
|
+
"""
|
4
|
+
Classes involved in doctesting
|
5
|
+
|
6
|
+
This module controls the various classes involved in doctesting.
|
7
|
+
|
8
|
+
AUTHORS:
|
9
|
+
|
10
|
+
- David Roe (2012-03-27) -- initial version, based on Robert Bradshaw's code.
|
11
|
+
"""
|
12
|
+
# ****************************************************************************
|
13
|
+
# Copyright (C) 2012-2013 David Roe <roed.math@gmail.com>
|
14
|
+
# 2012-2013 Robert Bradshaw <robertwb@gmail.com>
|
15
|
+
# 2012 William Stein <wstein@gmail.com>
|
16
|
+
# 2013 R. Andrew Ohana
|
17
|
+
# 2013-2014 Volker Braun
|
18
|
+
# 2013-2018 Jeroen Demeyer <jdemeyer@cage.ugent.be>
|
19
|
+
# 2013-2021 John H. Palmieri
|
20
|
+
# 2017 Erik M. Bray
|
21
|
+
# 2017-2021 Frédéric Chapoton
|
22
|
+
# 2018 Sébastien Labbé
|
23
|
+
# 2019 François Bissey
|
24
|
+
# 2020-2023 Matthias Koeppe
|
25
|
+
# 2022 Michael Orlitzky
|
26
|
+
# 2022 Sebastian Oehms
|
27
|
+
#
|
28
|
+
# This program is free software: you can redistribute it and/or modify
|
29
|
+
# it under the terms of the GNU General Public License as published by
|
30
|
+
# the Free Software Foundation, either version 2 of the License, or
|
31
|
+
# (at your option) any later version.
|
32
|
+
# https://www.gnu.org/licenses/
|
33
|
+
# ****************************************************************************
|
34
|
+
|
35
|
+
import importlib
|
36
|
+
import random
|
37
|
+
import os
|
38
|
+
import sys
|
39
|
+
import time
|
40
|
+
import json
|
41
|
+
import shlex
|
42
|
+
import types
|
43
|
+
import sage.misc.flatten
|
44
|
+
import sage.misc.randstate as randstate
|
45
|
+
from sage.structure.sage_object import SageObject
|
46
|
+
from sage.env import DOT_SAGE, SAGE_LIB, SAGE_SRC, SAGE_VENV, SAGE_EXTCODE
|
47
|
+
from sage.misc.temporary_file import tmp_dir
|
48
|
+
from cysignals.signals import AlarmInterrupt, init_cysignals
|
49
|
+
|
50
|
+
from .sources import FileDocTestSource, DictAsObject, get_basename
|
51
|
+
from .forker import DocTestDispatcher
|
52
|
+
from .reporting import DocTestReporter
|
53
|
+
from .util import Timer, count_noun, dict_difference
|
54
|
+
from .external import available_software
|
55
|
+
from .parsing import parse_optional_tags, parse_file_optional_tags, unparse_optional_tags, \
|
56
|
+
nodoctest_regex, optionaltag_regex, optionalfiledirective_regex
|
57
|
+
|
58
|
+
|
59
|
+
# Optional tags which are always automatically added
|
60
|
+
|
61
|
+
auto_optional_tags = set()
|
62
|
+
|
63
|
+
class DocTestDefaults(SageObject):
|
64
|
+
"""
|
65
|
+
This class is used for doctesting the Sage doctest module.
|
66
|
+
|
67
|
+
INPUT:
|
68
|
+
|
69
|
+
- ``runtest_default`` -- (boolean, default ``False``); if ``True``,
|
70
|
+
fills in attribute to be the same as the defaults defined in
|
71
|
+
``sage-runtests``. If ``False``, change defaults in a few places
|
72
|
+
for use in doctests of the doctester, which is mostly to make
|
73
|
+
doctesting more predictable.
|
74
|
+
|
75
|
+
- ``**kwds`` -- attributes to override defaults
|
76
|
+
|
77
|
+
EXAMPLES::
|
78
|
+
|
79
|
+
sage: from sage.doctest.control import DocTestDefaults
|
80
|
+
sage: D = DocTestDefaults(); D
|
81
|
+
DocTestDefaults()
|
82
|
+
sage: D.timeout
|
83
|
+
-1
|
84
|
+
|
85
|
+
Keyword arguments become attributes::
|
86
|
+
|
87
|
+
sage: D = DocTestDefaults(timeout=100); D
|
88
|
+
DocTestDefaults(timeout=100)
|
89
|
+
sage: D.timeout
|
90
|
+
100
|
91
|
+
|
92
|
+
The defaults for ``sage-runtests``::
|
93
|
+
|
94
|
+
sage: D = DocTestDefaults(runtest_default=True); D
|
95
|
+
DocTestDefaults(abspath=False, file_iterations=0, global_iterations=0,
|
96
|
+
optional='sage,optional', random_seed=None,
|
97
|
+
stats_path='.../timings2.json')
|
98
|
+
"""
|
99
|
+
def __init__(self, runtest_default=False, **kwds):
|
100
|
+
"""
|
101
|
+
Edit these parameters after creating an instance.
|
102
|
+
|
103
|
+
EXAMPLES::
|
104
|
+
|
105
|
+
sage: from sage.doctest.control import DocTestDefaults
|
106
|
+
sage: D = DocTestDefaults()
|
107
|
+
sage: 'sage' in D.optional
|
108
|
+
True
|
109
|
+
"""
|
110
|
+
# NOTE that these are NOT the defaults used by the sage-runtests
|
111
|
+
# script (which is what gets invoked when running `sage -t`).
|
112
|
+
# These are only basic defaults when invoking the doctest runner
|
113
|
+
# from Python, which is not the typical use case.
|
114
|
+
self.nthreads = 1
|
115
|
+
self.serial = False
|
116
|
+
self.timeout = -1
|
117
|
+
self.die_timeout = -1
|
118
|
+
self.all = False
|
119
|
+
self.installed = False
|
120
|
+
self.logfile = None
|
121
|
+
self.long = False
|
122
|
+
self.warn_long = -1.0
|
123
|
+
self.randorder = None
|
124
|
+
self.random_seed = None if runtest_default else 0
|
125
|
+
self.global_iterations = 0 if runtest_default else 1
|
126
|
+
self.file_iterations = 0 if runtest_default else 1
|
127
|
+
self.environment = "sage.repl.ipython_kernel.all_jupyter"
|
128
|
+
self.initial = False
|
129
|
+
self.exitfirst = False
|
130
|
+
self.force_lib = False
|
131
|
+
self.if_installed = False
|
132
|
+
self.abspath = not runtest_default
|
133
|
+
self.verbose = False
|
134
|
+
self.debug = False
|
135
|
+
self.only_errors = False
|
136
|
+
self.gdb = False
|
137
|
+
self.lldb = False
|
138
|
+
self.valgrind = False
|
139
|
+
self.massif = False
|
140
|
+
self.cachegrind = False
|
141
|
+
self.omega = False
|
142
|
+
self.failed = False
|
143
|
+
self.new = False
|
144
|
+
self.show_skipped = False
|
145
|
+
self.target_walltime = -1
|
146
|
+
self.baseline_stats_path = None
|
147
|
+
self.format = "sage"
|
148
|
+
|
149
|
+
# sage-runtests contains more optional tags. Technically, adding
|
150
|
+
# auto_optional_tags here is redundant, since that is added
|
151
|
+
# automatically anyway. However, this default is still used for
|
152
|
+
# displaying user-defined optional tags and we don't want to see
|
153
|
+
# the auto_optional_tags there.
|
154
|
+
if runtest_default:
|
155
|
+
self.optional = ','.join(['sage', 'optional'])
|
156
|
+
else:
|
157
|
+
self.optional = {'sage'} | auto_optional_tags
|
158
|
+
self.hide = ''
|
159
|
+
self.probe = ''
|
160
|
+
|
161
|
+
# > 0: always run GC before every test
|
162
|
+
# < 0: disable GC
|
163
|
+
self.gc = 0
|
164
|
+
|
165
|
+
# We don't want to use the real stats file by default so that
|
166
|
+
# we don't overwrite timings for the actual running doctests.
|
167
|
+
self.stats_path = os.path.join(
|
168
|
+
DOT_SAGE, "timings2.json" if runtest_default else "timings_dt_test.json")
|
169
|
+
self.__dict__.update(kwds)
|
170
|
+
|
171
|
+
def _repr_(self):
|
172
|
+
"""
|
173
|
+
Return the print representation.
|
174
|
+
|
175
|
+
EXAMPLES::
|
176
|
+
|
177
|
+
sage: from sage.doctest.control import DocTestDefaults
|
178
|
+
sage: DocTestDefaults(timeout=100, foobar='hello')
|
179
|
+
DocTestDefaults(foobar='hello', timeout=100)
|
180
|
+
"""
|
181
|
+
s = "DocTestDefaults("
|
182
|
+
for k in sorted(dict_difference(self.__dict__, DocTestDefaults().__dict__).keys()):
|
183
|
+
if s[-1] != "(":
|
184
|
+
s += ", "
|
185
|
+
s += str(k) + "=" + repr(getattr(self, k))
|
186
|
+
s += ")"
|
187
|
+
return s
|
188
|
+
|
189
|
+
def __eq__(self, other):
|
190
|
+
"""
|
191
|
+
Comparison by __dict__.
|
192
|
+
|
193
|
+
EXAMPLES::
|
194
|
+
|
195
|
+
sage: from sage.doctest.control import DocTestDefaults
|
196
|
+
sage: DD1 = DocTestDefaults(long=True)
|
197
|
+
sage: DD2 = DocTestDefaults(long=True)
|
198
|
+
sage: DD1 == DD2
|
199
|
+
True
|
200
|
+
"""
|
201
|
+
if not isinstance(other, DocTestDefaults):
|
202
|
+
return False
|
203
|
+
return self.__dict__ == other.__dict__
|
204
|
+
|
205
|
+
def __ne__(self, other):
|
206
|
+
"""
|
207
|
+
Test for non-equality.
|
208
|
+
|
209
|
+
EXAMPLES::
|
210
|
+
|
211
|
+
sage: from sage.doctest.control import DocTestDefaults
|
212
|
+
sage: DD1 = DocTestDefaults(long=True)
|
213
|
+
sage: DD2 = DocTestDefaults(long=True)
|
214
|
+
sage: DD1 != DD2
|
215
|
+
False
|
216
|
+
"""
|
217
|
+
return not (self == other)
|
218
|
+
|
219
|
+
|
220
|
+
def skipdir(dirname):
|
221
|
+
"""
|
222
|
+
Return ``True`` if and only if the directory ``dirname`` should not be
|
223
|
+
doctested.
|
224
|
+
|
225
|
+
EXAMPLES::
|
226
|
+
|
227
|
+
sage: from sage.doctest.control import skipdir
|
228
|
+
sage: skipdir(sage.env.SAGE_SRC)
|
229
|
+
False
|
230
|
+
sage: skipdir(os.path.join(sage.env.SAGE_SRC, "sage", "doctest", "tests"))
|
231
|
+
True
|
232
|
+
"""
|
233
|
+
if os.path.exists(os.path.join(dirname, "nodoctest.py")) or os.path.exists(os.path.join(dirname, "nodoctest")):
|
234
|
+
return True
|
235
|
+
return False
|
236
|
+
|
237
|
+
|
238
|
+
def skipfile(filename, tested_optional_tags=False, *,
|
239
|
+
if_installed=False, log=None):
|
240
|
+
"""
|
241
|
+
Return ``True`` if and only if the file ``filename`` should not be doctested.
|
242
|
+
|
243
|
+
INPUT:
|
244
|
+
|
245
|
+
- ``filename`` -- name of a file
|
246
|
+
|
247
|
+
- ``tested_optional_tags`` -- list or tuple or set of optional tags to test,
|
248
|
+
or ``False`` (no optional test) or ``True`` (all optional tests)
|
249
|
+
|
250
|
+
- ``if_installed`` -- boolean (default: ``False``); whether to skip Python/Cython files
|
251
|
+
that are not installed as modules
|
252
|
+
|
253
|
+
- ``log`` -- function to call with log messages, or ``None``
|
254
|
+
|
255
|
+
If ``filename`` contains a line of the form ``"# sage.doctest:
|
256
|
+
optional - xyz")``, then this will return ``False`` if "xyz" is in
|
257
|
+
``tested_optional_tags``. Otherwise, it returns the matching tag
|
258
|
+
("optional - xyz").
|
259
|
+
|
260
|
+
EXAMPLES::
|
261
|
+
|
262
|
+
sage: from sage.doctest.control import skipfile
|
263
|
+
sage: skipfile("skipme.c")
|
264
|
+
True
|
265
|
+
sage: filename = tmp_filename(ext='.pyx')
|
266
|
+
sage: skipfile(filename)
|
267
|
+
False
|
268
|
+
sage: with open(filename, "w") as f:
|
269
|
+
....: _ = f.write("# nodoctest")
|
270
|
+
sage: skipfile(filename)
|
271
|
+
True
|
272
|
+
sage: with open(filename, "w") as f:
|
273
|
+
....: _ = f.write("# sage.doctest: " # broken in two source lines to avoid the pattern
|
274
|
+
....: "optional - xyz") # of relint (multiline_doctest_comment)
|
275
|
+
sage: skipfile(filename, False)
|
276
|
+
'optional - xyz'
|
277
|
+
sage: bool(skipfile(filename, False))
|
278
|
+
True
|
279
|
+
sage: skipfile(filename, ['abc'])
|
280
|
+
'optional - xyz'
|
281
|
+
sage: skipfile(filename, ['abc', 'xyz'])
|
282
|
+
False
|
283
|
+
sage: skipfile(filename, True)
|
284
|
+
False
|
285
|
+
"""
|
286
|
+
if filename.endswith('__main__.py'):
|
287
|
+
if log:
|
288
|
+
log(f"Skipping '{filename}' because it is a __main__.py file")
|
289
|
+
return True
|
290
|
+
if filename.endswith('.rst.txt'):
|
291
|
+
ext = '.rst.txt'
|
292
|
+
else:
|
293
|
+
_ , ext = os.path.splitext(filename)
|
294
|
+
# .rst.txt appear in the installed documentation in subdirectories named "_sources"
|
295
|
+
if ext not in ('.py', '.pyx', '.pxd', '.pxi', '.sage', '.spyx', '.rst', '.tex', '.rst.txt'):
|
296
|
+
if log:
|
297
|
+
log(f"Skipping '{filename}' because it does not have one of the recognized file name extensions")
|
298
|
+
return True
|
299
|
+
if if_installed and ext not in ('.py', '.pyx'):
|
300
|
+
if log:
|
301
|
+
log(f"Skipping '{filename}' because it is not the source file of a Python module")
|
302
|
+
return True
|
303
|
+
if "jupyter_execute" in filename:
|
304
|
+
if log:
|
305
|
+
log(f"Skipping '{filename}' because it is created by the jupyter-sphinx extension for internal use and should not be tested")
|
306
|
+
return True
|
307
|
+
if if_installed:
|
308
|
+
module_name = get_basename(filename)
|
309
|
+
try:
|
310
|
+
if not importlib.util.find_spec(module_name): # tries to import the containing package
|
311
|
+
if log:
|
312
|
+
log(f"Skipping '{filename}' because module {module_name} is not present in the venv")
|
313
|
+
return True
|
314
|
+
except ModuleNotFoundError as e:
|
315
|
+
if log:
|
316
|
+
log(f"Skipping '{filename}' because module {e.name} cannot be imported")
|
317
|
+
return True
|
318
|
+
|
319
|
+
with open(filename) as F:
|
320
|
+
file_optional_tags = parse_file_optional_tags(enumerate(F))
|
321
|
+
|
322
|
+
if 'not tested' in file_optional_tags:
|
323
|
+
if log:
|
324
|
+
log(f"Skipping '{filename}' because it is marked 'nodoctest'")
|
325
|
+
return True
|
326
|
+
|
327
|
+
if tested_optional_tags is False:
|
328
|
+
if file_optional_tags:
|
329
|
+
file_tag_string = unparse_optional_tags(file_optional_tags, prefix='')
|
330
|
+
if log:
|
331
|
+
log(f"Skipping '{filename}' because it is marked '# {file_tag_string}'")
|
332
|
+
return file_tag_string
|
333
|
+
|
334
|
+
elif tested_optional_tags is not True:
|
335
|
+
extra = {tag for tag in file_optional_tags
|
336
|
+
if tag not in tested_optional_tags}
|
337
|
+
if extra:
|
338
|
+
file_tag_string = unparse_optional_tags(file_optional_tags, prefix='')
|
339
|
+
if log:
|
340
|
+
log(f"Skipping '{filename}' because it is marked '{file_tag_string}'")
|
341
|
+
return file_tag_string
|
342
|
+
|
343
|
+
return False
|
344
|
+
|
345
|
+
|
346
|
+
class Logger:
|
347
|
+
r"""
|
348
|
+
File-like object which implements writing to multiple files at
|
349
|
+
once.
|
350
|
+
|
351
|
+
EXAMPLES::
|
352
|
+
|
353
|
+
sage: from sage.doctest.control import Logger
|
354
|
+
sage: with open(tmp_filename(), "w+") as t:
|
355
|
+
....: L = Logger(sys.stdout, t)
|
356
|
+
....: _ = L.write("hello world\n")
|
357
|
+
....: _ = t.seek(0)
|
358
|
+
....: t.read()
|
359
|
+
hello world
|
360
|
+
'hello world\n'
|
361
|
+
"""
|
362
|
+
def __init__(self, *files):
|
363
|
+
r"""
|
364
|
+
Initialize the logger for writing to all files in ``files``.
|
365
|
+
|
366
|
+
TESTS::
|
367
|
+
|
368
|
+
sage: from sage.doctest.control import Logger
|
369
|
+
sage: Logger().write("hello world\n") # no-op
|
370
|
+
"""
|
371
|
+
self.files = list(files)
|
372
|
+
|
373
|
+
def write(self, x):
|
374
|
+
r"""
|
375
|
+
Write ``x`` to all files.
|
376
|
+
|
377
|
+
TESTS::
|
378
|
+
|
379
|
+
sage: from sage.doctest.control import Logger
|
380
|
+
sage: Logger(sys.stdout).write("hello world\n")
|
381
|
+
hello world
|
382
|
+
"""
|
383
|
+
for f in self.files:
|
384
|
+
f.write(x)
|
385
|
+
|
386
|
+
def flush(self):
|
387
|
+
"""
|
388
|
+
Flush all files.
|
389
|
+
|
390
|
+
TESTS::
|
391
|
+
|
392
|
+
sage: from sage.doctest.control import Logger
|
393
|
+
sage: Logger(sys.stdout).flush()
|
394
|
+
"""
|
395
|
+
for f in self.files:
|
396
|
+
f.flush()
|
397
|
+
|
398
|
+
|
399
|
+
class DocTestController(SageObject):
|
400
|
+
"""
|
401
|
+
This class controls doctesting of files.
|
402
|
+
|
403
|
+
After creating it with appropriate options, call the :meth:`run` method to run the doctests.
|
404
|
+
"""
|
405
|
+
def __init__(self, options, args):
|
406
|
+
"""
|
407
|
+
Initialization.
|
408
|
+
|
409
|
+
INPUT:
|
410
|
+
|
411
|
+
- ``options`` -- either options generated from the command line by sage-runtests
|
412
|
+
or a DocTestDefaults object (possibly with some entries modified)
|
413
|
+
- ``args`` -- list of filenames to doctest
|
414
|
+
|
415
|
+
EXAMPLES::
|
416
|
+
|
417
|
+
sage: from sage.doctest.control import DocTestDefaults, DocTestController
|
418
|
+
sage: DC = DocTestController(DocTestDefaults(), [])
|
419
|
+
sage: DC
|
420
|
+
DocTest Controller
|
421
|
+
"""
|
422
|
+
# First we modify options to take environment variables into
|
423
|
+
# account and check compatibility of the user's specified
|
424
|
+
# options.
|
425
|
+
if options.timeout < 0:
|
426
|
+
if options.gdb or options.lldb or options.debug:
|
427
|
+
# Interactive debuggers: "infinite" timeout
|
428
|
+
options.timeout = 0
|
429
|
+
elif options.valgrind or options.massif or options.cachegrind or options.omega:
|
430
|
+
# Non-interactive debuggers: 48 hours
|
431
|
+
options.timeout = int(os.getenv('SAGE_TIMEOUT_VALGRIND', 48 * 60 * 60))
|
432
|
+
elif options.long:
|
433
|
+
options.timeout = int(os.getenv('SAGE_TIMEOUT_LONG', 30 * 60))
|
434
|
+
else:
|
435
|
+
options.timeout = int(os.getenv('SAGE_TIMEOUT', 10 * 60))
|
436
|
+
# For non-default GC options, double the timeout
|
437
|
+
if options.gc:
|
438
|
+
options.timeout *= 2
|
439
|
+
if options.nthreads == 0:
|
440
|
+
options.nthreads = int(os.getenv('SAGE_NUM_THREADS_PARALLEL', 1))
|
441
|
+
if options.failed and not (args or options.new):
|
442
|
+
# If the user doesn't specify any files then we rerun all failed files.
|
443
|
+
options.all = True
|
444
|
+
if options.global_iterations == 0:
|
445
|
+
options.global_iterations = int(os.environ.get('SAGE_TEST_GLOBAL_ITER', 1))
|
446
|
+
if options.file_iterations == 0:
|
447
|
+
options.file_iterations = int(os.environ.get('SAGE_TEST_ITER', 1))
|
448
|
+
if options.debug:
|
449
|
+
if options.nthreads > 1:
|
450
|
+
print("Debugging requires single-threaded operation, setting number of threads to 1.")
|
451
|
+
if options.logfile:
|
452
|
+
print("Debugging is not compatible with logging, disabling logfile.")
|
453
|
+
options.serial = True
|
454
|
+
options.logfile = None
|
455
|
+
if options.serial:
|
456
|
+
options.nthreads = 1
|
457
|
+
if options.verbose:
|
458
|
+
options.show_skipped = True
|
459
|
+
|
460
|
+
options.hidden_features = set()
|
461
|
+
if isinstance(options.hide, str):
|
462
|
+
if not len(options.hide):
|
463
|
+
options.hide = set()
|
464
|
+
else:
|
465
|
+
s = options.hide.lower()
|
466
|
+
options.hide = set(s.split(','))
|
467
|
+
for h in options.hide:
|
468
|
+
if not optionaltag_regex.search(h):
|
469
|
+
raise ValueError('invalid optional tag {!r}'.format(h))
|
470
|
+
if 'all' in options.hide:
|
471
|
+
options.hide.discard('all')
|
472
|
+
from sage.features.all import all_features
|
473
|
+
feature_names = {f.name for f in all_features() if not f.is_standard()}
|
474
|
+
options.hide = options.hide.union(feature_names)
|
475
|
+
if 'optional' in options.hide:
|
476
|
+
options.hide.discard('optional')
|
477
|
+
from sage.features.all import all_features
|
478
|
+
feature_names = {f.name for f in all_features() if f.is_optional()}
|
479
|
+
options.hide = options.hide.union(feature_names)
|
480
|
+
|
481
|
+
options.disabled_optional = set()
|
482
|
+
if isinstance(options.optional, str):
|
483
|
+
s = options.optional.lower()
|
484
|
+
options.optional = set(s.split(','))
|
485
|
+
if "all" in options.optional:
|
486
|
+
# Special case to run all optional tests
|
487
|
+
options.optional = True
|
488
|
+
else:
|
489
|
+
# We replace the 'optional' tag by all optional
|
490
|
+
# packages for which the installed version matches the
|
491
|
+
# latest available version (this implies in particular
|
492
|
+
# that the package is actually installed).
|
493
|
+
if 'optional' in options.optional:
|
494
|
+
options.optional.discard('optional')
|
495
|
+
from sage.misc.package import list_packages
|
496
|
+
for pkg in list_packages('optional', local=True).values():
|
497
|
+
if pkg.name in options.hide:
|
498
|
+
continue
|
499
|
+
# Skip features for which we have a more specific runtime feature test.
|
500
|
+
if pkg.name in ['bliss', 'coxeter3', 'ecm', 'fricas', 'frobby', 'gfan', 'giac', 'jmol', 'latte_int', 'macaulay2', 'mcqd', 'meataxe', 'msolve', 'sirocco', 'tdlib']:
|
501
|
+
continue
|
502
|
+
if pkg.is_installed() and pkg.installed_version == pkg.remote_version:
|
503
|
+
options.optional.add(pkg.name)
|
504
|
+
|
505
|
+
from sage.features import package_systems
|
506
|
+
options.optional.update(system.name
|
507
|
+
for system in package_systems())
|
508
|
+
# Check that all tags are valid
|
509
|
+
for o in options.optional:
|
510
|
+
if o.startswith('!'):
|
511
|
+
if not optionaltag_regex.search(o[1:]):
|
512
|
+
raise ValueError('invalid optional tag {!r}'.format(o))
|
513
|
+
options.disabled_optional.add(o[1:])
|
514
|
+
elif not optionaltag_regex.search(o):
|
515
|
+
raise ValueError('invalid optional tag {!r}'.format(o))
|
516
|
+
|
517
|
+
options.optional |= auto_optional_tags
|
518
|
+
options.optional -= options.disabled_optional
|
519
|
+
|
520
|
+
if isinstance(options.probe, str):
|
521
|
+
if options.probe == 'none':
|
522
|
+
options.probe = ''
|
523
|
+
s = options.probe.lower()
|
524
|
+
if not s:
|
525
|
+
options.probe = set()
|
526
|
+
else:
|
527
|
+
options.probe = set(s.split(','))
|
528
|
+
if "all" in options.probe:
|
529
|
+
# Special case to probe all features that are not present
|
530
|
+
options.probe = True
|
531
|
+
else:
|
532
|
+
# Check that all tags are valid
|
533
|
+
for o in options.probe:
|
534
|
+
if not optionaltag_regex.search(o):
|
535
|
+
raise ValueError('invalid optional tag {!r}'.format(o))
|
536
|
+
|
537
|
+
self.options = options
|
538
|
+
|
539
|
+
self.files = args
|
540
|
+
if options.logfile:
|
541
|
+
if not isinstance(options.logfile, str):
|
542
|
+
# file from sage-runtests
|
543
|
+
self.logfile = options.logfile
|
544
|
+
else:
|
545
|
+
# string from DocTestDefaults
|
546
|
+
try:
|
547
|
+
self.logfile = open(options.logfile, 'a')
|
548
|
+
except OSError:
|
549
|
+
print("Unable to open logfile {!r}\nProceeding without logging.".format(options.logfile))
|
550
|
+
self.logfile = None
|
551
|
+
else:
|
552
|
+
self.logfile = None
|
553
|
+
|
554
|
+
# Flush any diagnostic messages we just printed
|
555
|
+
sys.stdout.flush()
|
556
|
+
sys.stderr.flush()
|
557
|
+
|
558
|
+
# In serial mode, we run just one process. Then the doctests
|
559
|
+
# will interfere with the output logging (both use stdout).
|
560
|
+
# To solve this, we create real_stdout which will always
|
561
|
+
# write to the actual standard output, regardless of
|
562
|
+
# redirections.
|
563
|
+
if options.serial:
|
564
|
+
self._real_stdout = os.fdopen(os.dup(sys.stdout.fileno()), "w")
|
565
|
+
self._close_stdout = True
|
566
|
+
else:
|
567
|
+
# Parallel mode: no special tricks needed
|
568
|
+
self._real_stdout = sys.stdout
|
569
|
+
self._close_stdout = False
|
570
|
+
|
571
|
+
if self.logfile is None:
|
572
|
+
self.logger = self._real_stdout
|
573
|
+
else:
|
574
|
+
self.logger = Logger(self._real_stdout, self.logfile)
|
575
|
+
|
576
|
+
self.stats = {}
|
577
|
+
self.load_stats(options.stats_path)
|
578
|
+
self.baseline_stats = {}
|
579
|
+
if options.baseline_stats_path:
|
580
|
+
self.load_baseline_stats(options.baseline_stats_path)
|
581
|
+
self._init_warn_long()
|
582
|
+
|
583
|
+
if self.options.random_seed is None:
|
584
|
+
randstate.set_random_seed()
|
585
|
+
self.options.random_seed = randstate.initial_seed()
|
586
|
+
|
587
|
+
def __del__(self):
|
588
|
+
if getattr(self, 'logfile', None) is not None:
|
589
|
+
self.logfile.close()
|
590
|
+
|
591
|
+
if getattr(self, '_close_stdout', False):
|
592
|
+
self._real_stdout.close()
|
593
|
+
|
594
|
+
def _init_warn_long(self):
|
595
|
+
"""
|
596
|
+
Pick a suitable default for the ``--warn-long`` option if not
|
597
|
+
specified.
|
598
|
+
|
599
|
+
It is desirable to have all tests (even ``# long`` ones)
|
600
|
+
finish in less than about 5 seconds. Longer tests typically
|
601
|
+
don't add coverage, they just make testing slow.
|
602
|
+
|
603
|
+
The default used here is 5 seconds, unless `--long` was used,
|
604
|
+
in which case it is 30 seconds.
|
605
|
+
|
606
|
+
TESTS:
|
607
|
+
|
608
|
+
Ensure that the user's command-line options are not changed::
|
609
|
+
|
610
|
+
sage: from sage.doctest.control import (DocTestDefaults,
|
611
|
+
....: DocTestController)
|
612
|
+
sage: DC = DocTestController(DocTestDefaults(), [])
|
613
|
+
sage: DC.options.warn_long = 5.0
|
614
|
+
sage: DC._init_warn_long()
|
615
|
+
sage: DC.options.warn_long
|
616
|
+
5.00000000000000
|
617
|
+
"""
|
618
|
+
# default is -1.0
|
619
|
+
if self.options.warn_long >= 0: # Specified on the command line
|
620
|
+
return
|
621
|
+
|
622
|
+
# The developer's guide says that even a "long time" test
|
623
|
+
# should ideally complete in under five seconds, so we're
|
624
|
+
# being rather generous here.
|
625
|
+
self.options.warn_long = 5.0
|
626
|
+
if self.options.long:
|
627
|
+
self.options.warn_long = 30.0
|
628
|
+
|
629
|
+
def second_on_modern_computer(self):
|
630
|
+
"""
|
631
|
+
Return the wall time equivalent of a second on a modern computer.
|
632
|
+
|
633
|
+
OUTPUT:
|
634
|
+
|
635
|
+
Float. The wall time on your computer that would be equivalent
|
636
|
+
to one second on a modern computer. Unless you have kick-ass
|
637
|
+
hardware this should always be >= 1.0. This raises a
|
638
|
+
:exc:`RuntimeError` if there are no stored timings to use as
|
639
|
+
benchmark.
|
640
|
+
|
641
|
+
EXAMPLES::
|
642
|
+
|
643
|
+
sage: from sage.doctest.control import DocTestDefaults, DocTestController
|
644
|
+
sage: DC = DocTestController(DocTestDefaults(), [])
|
645
|
+
sage: DC.second_on_modern_computer() # not tested
|
646
|
+
"""
|
647
|
+
from sage.misc.superseded import deprecation
|
648
|
+
deprecation(32981, "this method is no longer used by the sage library and will eventually be removed")
|
649
|
+
|
650
|
+
if len(self.stats) == 0:
|
651
|
+
raise RuntimeError('no stored timings available')
|
652
|
+
success = []
|
653
|
+
failed = []
|
654
|
+
for mod in self.stats.values():
|
655
|
+
if mod.get('failed', False):
|
656
|
+
failed.append(mod['walltime'])
|
657
|
+
else:
|
658
|
+
success.append(mod['walltime'])
|
659
|
+
if len(success) < 2500:
|
660
|
+
raise RuntimeError('too few successful tests, not using stored timings')
|
661
|
+
if len(failed) > 20:
|
662
|
+
raise RuntimeError('too many failed tests, not using stored timings')
|
663
|
+
expected = 12800.0 # Core i7 Quad-Core 2014
|
664
|
+
return sum(success) / expected
|
665
|
+
|
666
|
+
def _repr_(self):
|
667
|
+
"""
|
668
|
+
String representation.
|
669
|
+
|
670
|
+
EXAMPLES::
|
671
|
+
|
672
|
+
sage: from sage.doctest.control import DocTestDefaults, DocTestController
|
673
|
+
sage: DC = DocTestController(DocTestDefaults(), [])
|
674
|
+
sage: repr(DC) # indirect doctest
|
675
|
+
'DocTest Controller'
|
676
|
+
"""
|
677
|
+
return "DocTest Controller"
|
678
|
+
|
679
|
+
def load_environment(self):
|
680
|
+
"""
|
681
|
+
Return the module that provides the global environment.
|
682
|
+
|
683
|
+
EXAMPLES::
|
684
|
+
|
685
|
+
sage: from sage.doctest.control import DocTestDefaults, DocTestController
|
686
|
+
sage: DC = DocTestController(DocTestDefaults(), [])
|
687
|
+
sage: 'BipartiteGraph' in DC.load_environment().__dict__ # needs sage.graphs
|
688
|
+
True
|
689
|
+
sage: DC = DocTestController(DocTestDefaults(environment='sage.doctest.all'), [])
|
690
|
+
sage: 'BipartiteGraph' in DC.load_environment().__dict__ # needs sage.graphs
|
691
|
+
False
|
692
|
+
sage: 'run_doctests' in DC.load_environment().__dict__
|
693
|
+
True
|
694
|
+
"""
|
695
|
+
from importlib import import_module
|
696
|
+
return import_module(self.options.environment)
|
697
|
+
|
698
|
+
def load_baseline_stats(self, filename):
|
699
|
+
"""
|
700
|
+
Load baseline stats.
|
701
|
+
|
702
|
+
This must be a JSON file in the same format that :meth:`load_stats`
|
703
|
+
expects.
|
704
|
+
|
705
|
+
EXAMPLES::
|
706
|
+
|
707
|
+
sage: from sage.doctest.control import DocTestDefaults, DocTestController
|
708
|
+
sage: DC = DocTestController(DocTestDefaults(), [])
|
709
|
+
sage: import json
|
710
|
+
sage: filename = tmp_filename()
|
711
|
+
sage: with open(filename, 'w') as stats_file:
|
712
|
+
....: json.dump({'sage.doctest.control':{'failed':True}}, stats_file)
|
713
|
+
sage: DC.load_baseline_stats(filename)
|
714
|
+
sage: DC.baseline_stats['sage.doctest.control']
|
715
|
+
{'failed': True}
|
716
|
+
|
717
|
+
If the file doesn't exist, nothing happens. If there is an
|
718
|
+
error, print a message. In any case, leave the stats alone::
|
719
|
+
|
720
|
+
sage: d = tmp_dir()
|
721
|
+
sage: DC.load_baseline_stats(os.path.join(d)) # Cannot read a directory
|
722
|
+
Error loading baseline stats from ...
|
723
|
+
sage: DC.load_baseline_stats(os.path.join(d, "no_such_file"))
|
724
|
+
sage: DC.baseline_stats['sage.doctest.control']
|
725
|
+
{'failed': True}
|
726
|
+
"""
|
727
|
+
# Simply ignore non-existing files
|
728
|
+
if not os.path.exists(filename):
|
729
|
+
return
|
730
|
+
|
731
|
+
try:
|
732
|
+
with open(filename) as stats_file:
|
733
|
+
self.baseline_stats.update(json.load(stats_file))
|
734
|
+
except Exception as e:
|
735
|
+
self.log("Error loading baseline stats from %s: %s" % (filename, e))
|
736
|
+
|
737
|
+
def load_stats(self, filename):
|
738
|
+
"""
|
739
|
+
Load stats from the most recent run(s).
|
740
|
+
|
741
|
+
Stats are stored as a JSON file, and include information on
|
742
|
+
which files failed tests and the walltime used for execution
|
743
|
+
of the doctests.
|
744
|
+
|
745
|
+
EXAMPLES::
|
746
|
+
|
747
|
+
sage: from sage.doctest.control import DocTestDefaults, DocTestController
|
748
|
+
sage: DC = DocTestController(DocTestDefaults(), [])
|
749
|
+
sage: import json
|
750
|
+
sage: filename = tmp_filename()
|
751
|
+
sage: with open(filename, 'w') as stats_file:
|
752
|
+
....: json.dump({'sage.doctest.control': {'walltime': 1.0r}}, stats_file)
|
753
|
+
sage: DC.load_stats(filename)
|
754
|
+
sage: DC.stats['sage.doctest.control']
|
755
|
+
{'walltime': 1.0}
|
756
|
+
|
757
|
+
If the file doesn't exist, nothing happens. If there is an
|
758
|
+
error, print a message. In any case, leave the stats alone::
|
759
|
+
|
760
|
+
sage: d = tmp_dir()
|
761
|
+
sage: DC.load_stats(os.path.join(d)) # Cannot read a directory
|
762
|
+
Error loading stats from ...
|
763
|
+
sage: DC.load_stats(os.path.join(d, "no_such_file"))
|
764
|
+
sage: DC.stats['sage.doctest.control']
|
765
|
+
{'walltime': 1.0}
|
766
|
+
"""
|
767
|
+
# Simply ignore non-existing files
|
768
|
+
if not os.path.exists(filename):
|
769
|
+
return
|
770
|
+
|
771
|
+
try:
|
772
|
+
with open(filename) as stats_file:
|
773
|
+
self.stats.update(json.load(stats_file))
|
774
|
+
except Exception:
|
775
|
+
self.log("Error loading stats from %s" % filename)
|
776
|
+
|
777
|
+
def save_stats(self, filename):
|
778
|
+
"""
|
779
|
+
Save stats from the most recent run as a JSON file.
|
780
|
+
|
781
|
+
WARNING: This function overwrites the file.
|
782
|
+
|
783
|
+
EXAMPLES::
|
784
|
+
|
785
|
+
sage: from sage.doctest.control import DocTestDefaults, DocTestController
|
786
|
+
sage: DC = DocTestController(DocTestDefaults(), [])
|
787
|
+
sage: DC.stats['sage.doctest.control'] = {'walltime': 1.0r}
|
788
|
+
sage: filename = tmp_filename()
|
789
|
+
sage: DC.save_stats(filename)
|
790
|
+
sage: import json
|
791
|
+
sage: with open(filename) as f:
|
792
|
+
....: D = json.load(f)
|
793
|
+
sage: D['sage.doctest.control']
|
794
|
+
{'walltime': 1.0}
|
795
|
+
"""
|
796
|
+
from sage.misc.temporary_file import atomic_write
|
797
|
+
with atomic_write(filename) as stats_file:
|
798
|
+
json.dump(self.stats, stats_file, sort_keys=True, indent=4)
|
799
|
+
|
800
|
+
def log(self, s, end='\n'):
|
801
|
+
"""
|
802
|
+
Log the string ``s + end`` (where ``end`` is a newline by default)
|
803
|
+
to the logfile and print it to the standard output.
|
804
|
+
|
805
|
+
EXAMPLES::
|
806
|
+
|
807
|
+
sage: from sage.doctest.control import DocTestDefaults, DocTestController
|
808
|
+
sage: DD = DocTestDefaults(logfile=tmp_filename())
|
809
|
+
sage: DC = DocTestController(DD, [])
|
810
|
+
sage: DC.log("hello world")
|
811
|
+
hello world
|
812
|
+
sage: DC.logfile.close()
|
813
|
+
sage: with open(DD.logfile) as f:
|
814
|
+
....: print(f.read())
|
815
|
+
hello world
|
816
|
+
|
817
|
+
In serial mode, check that logging works even if ``stdout`` is
|
818
|
+
redirected::
|
819
|
+
|
820
|
+
sage: DD = DocTestDefaults(logfile=tmp_filename(), serial=True)
|
821
|
+
sage: DC = DocTestController(DD, [])
|
822
|
+
sage: from sage.doctest.forker import SageSpoofInOut
|
823
|
+
sage: with open(os.devnull, 'w') as devnull:
|
824
|
+
....: S = SageSpoofInOut(devnull)
|
825
|
+
....: S.start_spoofing()
|
826
|
+
....: DC.log("hello world")
|
827
|
+
....: S.stop_spoofing()
|
828
|
+
hello world
|
829
|
+
sage: DC.logfile.close()
|
830
|
+
sage: with open(DD.logfile) as f:
|
831
|
+
....: print(f.read())
|
832
|
+
hello world
|
833
|
+
|
834
|
+
Check that no duplicate logs appear, even when forking (:issue:`15244`)::
|
835
|
+
|
836
|
+
sage: DD = DocTestDefaults(logfile=tmp_filename())
|
837
|
+
sage: DC = DocTestController(DD, [])
|
838
|
+
sage: DC.log("hello world")
|
839
|
+
hello world
|
840
|
+
sage: if os.fork() == 0:
|
841
|
+
....: DC.logfile.close()
|
842
|
+
....: os._exit(0)
|
843
|
+
sage: DC.logfile.close()
|
844
|
+
sage: with open(DD.logfile) as f:
|
845
|
+
....: print(f.read())
|
846
|
+
hello world
|
847
|
+
"""
|
848
|
+
self.logger.write(s + end)
|
849
|
+
self.logger.flush()
|
850
|
+
|
851
|
+
def create_run_id(self):
|
852
|
+
"""
|
853
|
+
Create the run id.
|
854
|
+
|
855
|
+
EXAMPLES::
|
856
|
+
|
857
|
+
sage: from sage.doctest.control import DocTestDefaults, DocTestController
|
858
|
+
sage: DC = DocTestController(DocTestDefaults(), [])
|
859
|
+
sage: DC.create_run_id()
|
860
|
+
Running doctests with ID ...
|
861
|
+
"""
|
862
|
+
self.run_id = time.strftime('%Y-%m-%d-%H-%M-%S-') + "%08x" % random.getrandbits(32)
|
863
|
+
self.log("Running doctests with ID %s." % self.run_id)
|
864
|
+
|
865
|
+
def add_files(self):
|
866
|
+
r"""
|
867
|
+
Check for the flags '--all' and '--new'.
|
868
|
+
|
869
|
+
For each one present, this function adds the appropriate directories and files to the todo list.
|
870
|
+
|
871
|
+
EXAMPLES::
|
872
|
+
|
873
|
+
sage: from sage.doctest.control import (DocTestDefaults,
|
874
|
+
....: DocTestController)
|
875
|
+
sage: from sage.env import SAGE_SRC
|
876
|
+
sage: import tempfile
|
877
|
+
sage: with tempfile.NamedTemporaryFile() as f:
|
878
|
+
....: DD = DocTestDefaults(all=True, logfile=f.name)
|
879
|
+
....: DC = DocTestController(DD, [])
|
880
|
+
....: DC.add_files()
|
881
|
+
Doctesting ...
|
882
|
+
sage: os.path.join(SAGE_SRC, 'sage') in DC.files
|
883
|
+
True
|
884
|
+
|
885
|
+
::
|
886
|
+
|
887
|
+
sage: DD = DocTestDefaults(new = True)
|
888
|
+
sage: DC = DocTestController(DD, [])
|
889
|
+
sage: DC.add_files()
|
890
|
+
Doctesting ...
|
891
|
+
"""
|
892
|
+
opj = os.path.join
|
893
|
+
from sage.env import SAGE_SRC, SAGE_DOC_SRC, SAGE_ROOT, SAGE_ROOT_GIT, SAGE_DOC
|
894
|
+
# SAGE_ROOT_GIT can be None on distributions which typically
|
895
|
+
# only have the SAGE_LOCAL install tree but not SAGE_ROOT
|
896
|
+
if SAGE_ROOT_GIT is not None:
|
897
|
+
have_git = os.path.exists(SAGE_ROOT_GIT)
|
898
|
+
else:
|
899
|
+
have_git = False
|
900
|
+
|
901
|
+
def all_installed_modules():
|
902
|
+
self.log("Doctesting all installed modules of the Sage library.")
|
903
|
+
import sage
|
904
|
+
self.files.extend(sage.__path__)
|
905
|
+
try:
|
906
|
+
import sage_setup
|
907
|
+
self.files.extend(sage_setup.__path__)
|
908
|
+
except ImportError:
|
909
|
+
pass
|
910
|
+
try:
|
911
|
+
import sage_docbuild
|
912
|
+
self.files.extend(sage_docbuild.__path__)
|
913
|
+
except ImportError:
|
914
|
+
pass
|
915
|
+
|
916
|
+
def all_installed_doc():
|
917
|
+
if SAGE_DOC and os.path.isdir(SAGE_DOC):
|
918
|
+
self.log("Doctesting all installed documentation sources.")
|
919
|
+
self.files.append(SAGE_DOC)
|
920
|
+
|
921
|
+
def all_files():
|
922
|
+
if not SAGE_SRC:
|
923
|
+
return all_installed_modules()
|
924
|
+
self.log("Doctesting entire Sage library.")
|
925
|
+
self.files.append(opj(SAGE_SRC, 'sage'))
|
926
|
+
# Only test sage_setup and sage_docbuild if the relevant
|
927
|
+
# imports work. They may not work if not in a build
|
928
|
+
# environment or if the documentation build has been
|
929
|
+
# disabled.
|
930
|
+
try:
|
931
|
+
import sage_setup
|
932
|
+
self.files.append(opj(SAGE_SRC, 'sage_setup'))
|
933
|
+
except ImportError:
|
934
|
+
pass
|
935
|
+
try:
|
936
|
+
import sage_docbuild
|
937
|
+
self.files.append(opj(SAGE_SRC, 'sage_docbuild'))
|
938
|
+
except ImportError:
|
939
|
+
pass
|
940
|
+
|
941
|
+
def all_doc_sources():
|
942
|
+
if SAGE_DOC_SRC and os.path.isdir(SAGE_DOC_SRC):
|
943
|
+
self.log("Doctesting all documentation sources.")
|
944
|
+
self.files.append(SAGE_DOC_SRC)
|
945
|
+
else:
|
946
|
+
all_installed_doc()
|
947
|
+
|
948
|
+
if self.options.installed:
|
949
|
+
all_installed_modules()
|
950
|
+
all_installed_doc()
|
951
|
+
|
952
|
+
elif self.options.all or (self.options.new and not have_git):
|
953
|
+
all_files()
|
954
|
+
all_doc_sources()
|
955
|
+
|
956
|
+
elif self.options.new and have_git:
|
957
|
+
# Get all files changed in the working repo.
|
958
|
+
self.log("Doctesting files changed since last git commit")
|
959
|
+
import subprocess
|
960
|
+
change = subprocess.check_output(["git",
|
961
|
+
"--git-dir=" + SAGE_ROOT_GIT,
|
962
|
+
"--work-tree=" + SAGE_ROOT,
|
963
|
+
"status",
|
964
|
+
"--porcelain"])
|
965
|
+
change = change.decode('utf-8')
|
966
|
+
for line in change.split("\n"):
|
967
|
+
if not line:
|
968
|
+
continue
|
969
|
+
data = line.strip().split(' ')
|
970
|
+
status, filename = data[0], data[-1]
|
971
|
+
if (set(status).issubset("MARCU")
|
972
|
+
and filename.startswith("src/sage")
|
973
|
+
and (filename.endswith(".py") or
|
974
|
+
filename.endswith(".pyx") or
|
975
|
+
filename.endswith(".rst"))
|
976
|
+
and not skipfile(opj(SAGE_ROOT, filename),
|
977
|
+
bool(self.options.optional),
|
978
|
+
if_installed=self.options.if_installed)):
|
979
|
+
self.files.append(os.path.relpath(opj(SAGE_ROOT, filename)))
|
980
|
+
|
981
|
+
def expand_files_into_sources(self):
|
982
|
+
r"""
|
983
|
+
Expand ``self.files``, which may include directories, into a
|
984
|
+
list of :class:`sage.doctest.FileDocTestSource`
|
985
|
+
|
986
|
+
This function also handles the optional command line option.
|
987
|
+
|
988
|
+
EXAMPLES::
|
989
|
+
|
990
|
+
sage: from sage.doctest.control import DocTestDefaults, DocTestController
|
991
|
+
sage: from sage.env import SAGE_SRC
|
992
|
+
sage: import os
|
993
|
+
sage: dirname = os.path.join(SAGE_SRC, 'sage', 'doctest')
|
994
|
+
sage: DD = DocTestDefaults(optional='all')
|
995
|
+
sage: DC = DocTestController(DD, [dirname])
|
996
|
+
sage: DC.expand_files_into_sources()
|
997
|
+
sage: len(DC.sources)
|
998
|
+
15
|
999
|
+
sage: DC.sources[0].options.optional
|
1000
|
+
True
|
1001
|
+
|
1002
|
+
::
|
1003
|
+
|
1004
|
+
sage: DD = DocTestDefaults(optional='magma,guava')
|
1005
|
+
sage: DC = DocTestController(DD, [dirname])
|
1006
|
+
sage: DC.expand_files_into_sources()
|
1007
|
+
sage: all(t in DC.sources[0].options.optional for t in ['magma','guava'])
|
1008
|
+
True
|
1009
|
+
|
1010
|
+
We check that files are skipped appropriately::
|
1011
|
+
|
1012
|
+
sage: dirname = tmp_dir()
|
1013
|
+
sage: filename = os.path.join(dirname, 'not_tested.py')
|
1014
|
+
sage: with open(filename, 'w') as f:
|
1015
|
+
....: _ = f.write("#"*80 + "\n\n\n\n## nodoctest\n sage: 1+1\n 4")
|
1016
|
+
sage: DC = DocTestController(DD, [dirname])
|
1017
|
+
sage: DC.expand_files_into_sources()
|
1018
|
+
sage: DC.sources
|
1019
|
+
[]
|
1020
|
+
|
1021
|
+
The directory ``sage/doctest/tests`` contains ``nodoctest.py``
|
1022
|
+
but the files should still be tested when that directory is
|
1023
|
+
explicitly given (as opposed to being recursed into)::
|
1024
|
+
|
1025
|
+
sage: DC = DocTestController(DD, [os.path.join(SAGE_SRC, 'sage', 'doctest', 'tests')])
|
1026
|
+
sage: DC.expand_files_into_sources()
|
1027
|
+
sage: len(DC.sources) >= 10
|
1028
|
+
True
|
1029
|
+
"""
|
1030
|
+
def expand():
|
1031
|
+
for path in self.files:
|
1032
|
+
if os.path.isdir(path):
|
1033
|
+
for root, dirs, files in os.walk(path):
|
1034
|
+
for dir in list(dirs):
|
1035
|
+
if dir[0] == "." or skipdir(os.path.join(root, dir)):
|
1036
|
+
dirs.remove(dir)
|
1037
|
+
for file in files:
|
1038
|
+
if not skipfile(os.path.join(root, file),
|
1039
|
+
bool(self.options.optional),
|
1040
|
+
if_installed=self.options.if_installed):
|
1041
|
+
yield os.path.join(root, file)
|
1042
|
+
else:
|
1043
|
+
if not skipfile(path, bool(self.options.optional),
|
1044
|
+
if_installed=self.options.if_installed, log=self.log): # log when directly specified filenames are skipped
|
1045
|
+
yield path
|
1046
|
+
self.sources = [FileDocTestSource(path, self.options) for path in expand()]
|
1047
|
+
|
1048
|
+
def filter_sources(self):
|
1049
|
+
"""
|
1050
|
+
|
1051
|
+
EXAMPLES::
|
1052
|
+
|
1053
|
+
sage: from sage.doctest.control import DocTestDefaults, DocTestController
|
1054
|
+
sage: from sage.env import SAGE_SRC
|
1055
|
+
sage: import os
|
1056
|
+
sage: dirname = os.path.join(SAGE_SRC, 'sage', 'doctest')
|
1057
|
+
sage: DD = DocTestDefaults(failed=True)
|
1058
|
+
sage: DC = DocTestController(DD, [dirname])
|
1059
|
+
sage: DC.expand_files_into_sources()
|
1060
|
+
sage: for i, source in enumerate(DC.sources):
|
1061
|
+
....: DC.stats[source.basename] = {'walltime': 0.1r * (i+1)}
|
1062
|
+
sage: DC.stats['sage.doctest.control'] = {'failed': True, 'walltime': 1.0r}
|
1063
|
+
sage: DC.filter_sources()
|
1064
|
+
Only doctesting files that failed last test.
|
1065
|
+
sage: len(DC.sources)
|
1066
|
+
1
|
1067
|
+
"""
|
1068
|
+
# Filter the sources to only include those with failing doctests if the --failed option is passed
|
1069
|
+
if self.options.failed:
|
1070
|
+
self.log("Only doctesting files that failed last test.")
|
1071
|
+
|
1072
|
+
def is_failure(source):
|
1073
|
+
basename = source.basename
|
1074
|
+
return basename not in self.stats or self.stats[basename].get('failed')
|
1075
|
+
self.sources = [x for x in self.sources if is_failure(x)]
|
1076
|
+
|
1077
|
+
def sort_sources(self):
|
1078
|
+
r"""
|
1079
|
+
This function sorts the sources so that slower doctests are run first.
|
1080
|
+
|
1081
|
+
EXAMPLES::
|
1082
|
+
|
1083
|
+
sage: from sage.doctest.control import DocTestDefaults, DocTestController
|
1084
|
+
sage: from sage.env import SAGE_SRC
|
1085
|
+
sage: import os
|
1086
|
+
sage: dirname = os.path.join(SAGE_SRC, 'sage', 'doctest')
|
1087
|
+
sage: DD = DocTestDefaults(nthreads=2)
|
1088
|
+
sage: DC = DocTestController(DD, [dirname])
|
1089
|
+
sage: DC.expand_files_into_sources()
|
1090
|
+
sage: DC.sources.sort(key=lambda s:s.basename)
|
1091
|
+
sage: for i, source in enumerate(DC.sources):
|
1092
|
+
....: DC.stats[source.basename] = {'walltime': 0.1r * (i+1)}
|
1093
|
+
sage: DC.sort_sources()
|
1094
|
+
Sorting sources by runtime so that slower doctests are run first....
|
1095
|
+
sage: print("\n".join(source.basename for source in DC.sources))
|
1096
|
+
sage.doctest.util
|
1097
|
+
sage.doctest.test
|
1098
|
+
sage.doctest.sources
|
1099
|
+
sage.doctest.rif_tol
|
1100
|
+
sage.doctest.reporting
|
1101
|
+
sage.doctest.parsing_test
|
1102
|
+
sage.doctest.parsing
|
1103
|
+
sage.doctest.marked_output
|
1104
|
+
sage.doctest.forker
|
1105
|
+
sage.doctest.fixtures
|
1106
|
+
sage.doctest.external
|
1107
|
+
sage.doctest.control
|
1108
|
+
sage.doctest.check_tolerance
|
1109
|
+
sage.doctest.all
|
1110
|
+
sage.doctest
|
1111
|
+
"""
|
1112
|
+
if self.options.nthreads > 1 and len(self.sources) > self.options.nthreads:
|
1113
|
+
self.log("Sorting sources by runtime so that slower doctests are run first....")
|
1114
|
+
default = {'walltime': 0}
|
1115
|
+
|
1116
|
+
def sort_key(source):
|
1117
|
+
basename = source.basename
|
1118
|
+
return -self.stats.get(basename, default).get('walltime', 0), basename
|
1119
|
+
self.sources = sorted(self.sources, key=sort_key)
|
1120
|
+
|
1121
|
+
def source_baseline(self, source):
|
1122
|
+
r"""
|
1123
|
+
Return the ``baseline_stats`` value of ``source``.
|
1124
|
+
|
1125
|
+
INPUT:
|
1126
|
+
|
1127
|
+
- ``source`` -- a :class:`DocTestSource` instance
|
1128
|
+
|
1129
|
+
OUTPUT: a dictionary
|
1130
|
+
|
1131
|
+
EXAMPLES::
|
1132
|
+
|
1133
|
+
sage: from sage.doctest.control import DocTestDefaults, DocTestController
|
1134
|
+
sage: filename = sage.doctest.util.__file__
|
1135
|
+
sage: DD = DocTestDefaults()
|
1136
|
+
sage: DC = DocTestController(DD, [filename])
|
1137
|
+
sage: DC.expand_files_into_sources()
|
1138
|
+
sage: DC.source_baseline(DC.sources[0])
|
1139
|
+
{}
|
1140
|
+
"""
|
1141
|
+
if self.baseline_stats:
|
1142
|
+
basename = source.basename
|
1143
|
+
return self.baseline_stats.get(basename, {})
|
1144
|
+
return {}
|
1145
|
+
|
1146
|
+
def run_doctests(self):
|
1147
|
+
"""
|
1148
|
+
Actually run the doctests.
|
1149
|
+
|
1150
|
+
This function is called by :meth:`run`.
|
1151
|
+
|
1152
|
+
EXAMPLES::
|
1153
|
+
|
1154
|
+
sage: from sage.doctest.control import DocTestDefaults, DocTestController
|
1155
|
+
sage: from sage.env import SAGE_SRC
|
1156
|
+
sage: import os
|
1157
|
+
sage: dirname = os.path.join(SAGE_SRC, 'sage', 'rings', 'homset.py')
|
1158
|
+
sage: DD = DocTestDefaults()
|
1159
|
+
sage: DC = DocTestController(DD, [dirname])
|
1160
|
+
sage: DC.expand_files_into_sources()
|
1161
|
+
sage: DC.run_doctests()
|
1162
|
+
Doctesting 1 file.
|
1163
|
+
sage -t .../sage/rings/homset.py
|
1164
|
+
[... tests, ...s wall]
|
1165
|
+
----------------------------------------------------------------------
|
1166
|
+
All tests passed!
|
1167
|
+
----------------------------------------------------------------------
|
1168
|
+
Total time for all tests: ... seconds
|
1169
|
+
cpu time: ... seconds
|
1170
|
+
cumulative wall time: ... seconds...
|
1171
|
+
"""
|
1172
|
+
nfiles = 0
|
1173
|
+
nother = 0
|
1174
|
+
for F in self.sources:
|
1175
|
+
if isinstance(F, FileDocTestSource):
|
1176
|
+
nfiles += 1
|
1177
|
+
else:
|
1178
|
+
nother += 1
|
1179
|
+
if self.sources:
|
1180
|
+
filestr = ", ".join(([count_noun(nfiles, "file")] if nfiles else []) +
|
1181
|
+
([count_noun(nother, "other source")] if nother else []))
|
1182
|
+
threads = " using %s threads" % (self.options.nthreads) if self.options.nthreads > 1 else ""
|
1183
|
+
iterations = []
|
1184
|
+
if self.options.global_iterations > 1:
|
1185
|
+
iterations.append("%s global iterations" % (self.options.global_iterations))
|
1186
|
+
if self.options.file_iterations > 1:
|
1187
|
+
iterations.append("%s file iterations" % (self.options.file_iterations))
|
1188
|
+
iterations = ", ".join(iterations)
|
1189
|
+
if iterations:
|
1190
|
+
iterations = " (%s)" % (iterations)
|
1191
|
+
if self.baseline_stats:
|
1192
|
+
self.log(f"Using --baseline-stats-path={self.options.baseline_stats_path}")
|
1193
|
+
self.log("Doctesting %s%s%s." % (filestr, threads, iterations))
|
1194
|
+
self.reporter = DocTestReporter(self)
|
1195
|
+
self.dispatcher = DocTestDispatcher(self)
|
1196
|
+
N = self.options.global_iterations
|
1197
|
+
for _ in range(N):
|
1198
|
+
try:
|
1199
|
+
self.timer = Timer().start()
|
1200
|
+
self.dispatcher.dispatch()
|
1201
|
+
except KeyboardInterrupt:
|
1202
|
+
break
|
1203
|
+
finally:
|
1204
|
+
self.timer.stop()
|
1205
|
+
self.reporter.finalize()
|
1206
|
+
self.cleanup(False)
|
1207
|
+
else:
|
1208
|
+
self.log("No files to doctest")
|
1209
|
+
self.reporter = DictAsObject({'error_status': 0, 'stats': {}})
|
1210
|
+
|
1211
|
+
def cleanup(self, final=True):
|
1212
|
+
"""
|
1213
|
+
Run cleanup activities after actually running doctests.
|
1214
|
+
|
1215
|
+
In particular, saves the stats to disk and closes the logfile.
|
1216
|
+
|
1217
|
+
INPUT:
|
1218
|
+
|
1219
|
+
- ``final`` -- whether to close the logfile
|
1220
|
+
|
1221
|
+
EXAMPLES::
|
1222
|
+
|
1223
|
+
sage: from sage.doctest.control import DocTestDefaults, DocTestController
|
1224
|
+
sage: from sage.env import SAGE_SRC
|
1225
|
+
sage: import os
|
1226
|
+
sage: dirname = os.path.join(SAGE_SRC, 'sage', 'rings', 'all.py')
|
1227
|
+
sage: DD = DocTestDefaults()
|
1228
|
+
|
1229
|
+
sage: DC = DocTestController(DD, [dirname])
|
1230
|
+
sage: DC.expand_files_into_sources()
|
1231
|
+
sage: DC.sources.sort(key=lambda s:s.basename)
|
1232
|
+
|
1233
|
+
sage: for i, source in enumerate(DC.sources):
|
1234
|
+
....: DC.stats[source.basename] = {'walltime': 0.1r * (i+1)}
|
1235
|
+
....:
|
1236
|
+
|
1237
|
+
sage: DC.run()
|
1238
|
+
Running doctests with ID ...
|
1239
|
+
Doctesting 1 file.
|
1240
|
+
sage -t .../rings/all.py
|
1241
|
+
[... tests, ...s wall]
|
1242
|
+
----------------------------------------------------------------------
|
1243
|
+
All tests passed!
|
1244
|
+
----------------------------------------------------------------------
|
1245
|
+
Total time for all tests: ... seconds
|
1246
|
+
cpu time: ... seconds
|
1247
|
+
cumulative wall time: ... seconds
|
1248
|
+
Features detected...
|
1249
|
+
0
|
1250
|
+
sage: DC.cleanup()
|
1251
|
+
"""
|
1252
|
+
self.stats.update(self.reporter.stats)
|
1253
|
+
self.save_stats(self.options.stats_path)
|
1254
|
+
# Close the logfile
|
1255
|
+
if final and self.logfile is not None:
|
1256
|
+
self.logfile.close()
|
1257
|
+
self.logfile = None
|
1258
|
+
|
1259
|
+
def _optional_tags_string(self):
|
1260
|
+
"""
|
1261
|
+
Return a string describing the optional tags used.
|
1262
|
+
|
1263
|
+
OUTPUT: string with comma-separated tags (without spaces, so
|
1264
|
+
it can be used to build a command-line)
|
1265
|
+
|
1266
|
+
EXAMPLES::
|
1267
|
+
|
1268
|
+
sage: from sage.doctest.control import DocTestDefaults, DocTestController
|
1269
|
+
sage: DC = DocTestController(DocTestDefaults(), [])
|
1270
|
+
sage: DC._optional_tags_string()
|
1271
|
+
'sage'
|
1272
|
+
sage: DC = DocTestController(DocTestDefaults(optional='all,and,some,more'), [])
|
1273
|
+
sage: DC._optional_tags_string()
|
1274
|
+
'all'
|
1275
|
+
sage: DC = DocTestController(DocTestDefaults(optional='sage,openssl'), [])
|
1276
|
+
sage: DC._optional_tags_string()
|
1277
|
+
'openssl,sage'
|
1278
|
+
"""
|
1279
|
+
tags = self.options.optional
|
1280
|
+
if tags is True:
|
1281
|
+
return "all"
|
1282
|
+
else:
|
1283
|
+
return ",".join(sorted(tags - auto_optional_tags))
|
1284
|
+
|
1285
|
+
def _assemble_cmd(self):
|
1286
|
+
"""
|
1287
|
+
Assemble a shell command used in running tests under gdb, lldb, or valgrind.
|
1288
|
+
|
1289
|
+
EXAMPLES::
|
1290
|
+
|
1291
|
+
sage: from sage.doctest.control import DocTestDefaults, DocTestController
|
1292
|
+
sage: DC = DocTestController(DocTestDefaults(timeout=123), ["hello_world.py"])
|
1293
|
+
sage: print(DC._assemble_cmd())
|
1294
|
+
...python... -m sage.doctest --serial... --timeout=123... hello_world.py
|
1295
|
+
"""
|
1296
|
+
cmd = f"{shlex.quote(sys.executable)} -m sage.doctest --serial "
|
1297
|
+
opt = dict_difference(self.options.__dict__, DocTestDefaults(runtest_default=True).__dict__)
|
1298
|
+
# Options with no argument
|
1299
|
+
for o in ("all", "installed", "long", "initial", "exitfirst",
|
1300
|
+
"force_lib", "if_installed", "abspath", "verbose",
|
1301
|
+
"debug", "only_errors", "failed", "new",
|
1302
|
+
"show_skipped"):
|
1303
|
+
if o in opt:
|
1304
|
+
cmd += "--%s " % o.replace('_', '-')
|
1305
|
+
# Options with one argument
|
1306
|
+
for o in ("timeout", "die_timeout", "logfile", "warn_long", "randorder",
|
1307
|
+
"random_seed", "global_iterations", "file_iterations",
|
1308
|
+
"environment", "baseline_stats_path", "stats_path"):
|
1309
|
+
if o in opt:
|
1310
|
+
cmd += "--%s=%s " % (o.replace('_', '-'), opt[o])
|
1311
|
+
# One with a different dest
|
1312
|
+
if "target_walltime" in opt:
|
1313
|
+
cmd += "--%s=%s " % ("short", opt[o])
|
1314
|
+
if "optional" in opt:
|
1315
|
+
cmd += "--optional={} ".format(self._optional_tags_string())
|
1316
|
+
return cmd + " ".join(self.files)
|
1317
|
+
|
1318
|
+
def run_val_gdb(self, testing=False):
|
1319
|
+
"""
|
1320
|
+
Spawns a subprocess to run tests under the control of gdb, lldb, or valgrind.
|
1321
|
+
|
1322
|
+
INPUT:
|
1323
|
+
|
1324
|
+
- ``testing`` -- boolean (default: ``False``); if ``True`` then the
|
1325
|
+
command to be run will be printed rather than a subprocess started
|
1326
|
+
|
1327
|
+
EXAMPLES:
|
1328
|
+
|
1329
|
+
Note that the command lines include unexpanded environment
|
1330
|
+
variables. It is safer to let the shell expand them than to
|
1331
|
+
expand them here and risk insufficient quoting. ::
|
1332
|
+
|
1333
|
+
sage: from sage.doctest.control import DocTestDefaults, DocTestController
|
1334
|
+
sage: DD = DocTestDefaults(gdb=True)
|
1335
|
+
sage: DC = DocTestController(DD, ["hello_world.py"])
|
1336
|
+
sage: DC.run_val_gdb(testing=True)
|
1337
|
+
exec gdb --eval-command="run" --args ...python... -m sage.doctest --serial... --timeout=0... hello_world.py
|
1338
|
+
|
1339
|
+
::
|
1340
|
+
|
1341
|
+
sage: DD = DocTestDefaults(valgrind=True, optional='all', timeout=172800)
|
1342
|
+
sage: DC = DocTestController(DD, ["hello_world.py"])
|
1343
|
+
sage: DC.run_val_gdb(testing=True)
|
1344
|
+
exec valgrind --tool=memcheck --leak-resolution=high --leak-check=full --num-callers=25 --suppressions=.../valgrind/pyalloc.supp --suppressions=.../valgrind/sage.supp --suppressions=.../valgrind/sage-additional.supp --suppressions=.../valgrind/valgrind-python.supp --log-file=.../valgrind/sage-memcheck.%p ...python... -m sage.doctest --serial... --timeout=172800... --optional=all hello_world.py
|
1345
|
+
"""
|
1346
|
+
try:
|
1347
|
+
sage_cmd = self._assemble_cmd()
|
1348
|
+
except ValueError:
|
1349
|
+
self.log(sys.exc_info()[1])
|
1350
|
+
return 2
|
1351
|
+
opt = self.options
|
1352
|
+
|
1353
|
+
if opt.gdb:
|
1354
|
+
cmd = f'''exec gdb --eval-command="run" --args '''
|
1355
|
+
flags = ""
|
1356
|
+
if opt.logfile:
|
1357
|
+
sage_cmd += f" --logfile {shlex.quote(opt.logfile)}"
|
1358
|
+
elif opt.lldb:
|
1359
|
+
cmd = f'''exec lldb --one-line "process launch" --one-line "cont" -- '''
|
1360
|
+
flags = ""
|
1361
|
+
else:
|
1362
|
+
if opt.logfile is None:
|
1363
|
+
default_log = os.path.join(DOT_SAGE, "valgrind")
|
1364
|
+
if os.path.exists(default_log):
|
1365
|
+
if not os.path.isdir(default_log):
|
1366
|
+
self.log(f"{default_log} must be a directory")
|
1367
|
+
return 2
|
1368
|
+
else:
|
1369
|
+
os.makedirs(default_log)
|
1370
|
+
logfile = os.path.join(default_log, "sage-%s")
|
1371
|
+
else:
|
1372
|
+
logfile = opt.logfile
|
1373
|
+
if opt.valgrind:
|
1374
|
+
toolname = "memcheck"
|
1375
|
+
flags = os.getenv("SAGE_MEMCHECK_FLAGS")
|
1376
|
+
if flags is None:
|
1377
|
+
flags = "--leak-resolution=high --leak-check=full --num-callers=25 "
|
1378
|
+
for supp in ["pyalloc.supp", "sage.supp", "sage-additional.supp", "valgrind-python.supp"]:
|
1379
|
+
fname = os.path.join(SAGE_EXTCODE, "valgrind", supp)
|
1380
|
+
flags += f"--suppressions={shlex.quote(fname)} "
|
1381
|
+
elif opt.massif:
|
1382
|
+
toolname = "massif"
|
1383
|
+
flags = os.getenv("SAGE_MASSIF_FLAGS", "--depth=6 ")
|
1384
|
+
elif opt.cachegrind:
|
1385
|
+
toolname = "cachegrind"
|
1386
|
+
flags = os.getenv("SAGE_CACHEGRIND_FLAGS", "")
|
1387
|
+
elif opt.omega:
|
1388
|
+
toolname = "exp-omega"
|
1389
|
+
flags = os.getenv("SAGE_OMEGA_FLAGS", "")
|
1390
|
+
cmd = "exec valgrind --tool=%s " % (toolname)
|
1391
|
+
flags += f''' --log-file={shlex.quote(logfile)} '''
|
1392
|
+
if opt.omega:
|
1393
|
+
toolname = "omega"
|
1394
|
+
if "%s" in flags:
|
1395
|
+
flags %= toolname + ".%p" # replace %s with toolname
|
1396
|
+
cmd += flags + sage_cmd
|
1397
|
+
|
1398
|
+
sys.stdout.flush()
|
1399
|
+
sys.stderr.flush()
|
1400
|
+
self.log(cmd)
|
1401
|
+
|
1402
|
+
if testing:
|
1403
|
+
return
|
1404
|
+
|
1405
|
+
# Setup signal handlers.
|
1406
|
+
# Save crash logs in temporary directory.
|
1407
|
+
os.putenv('CYSIGNALS_CRASH_LOGS', tmp_dir("crash_logs_"))
|
1408
|
+
init_cysignals()
|
1409
|
+
|
1410
|
+
import signal
|
1411
|
+
import subprocess
|
1412
|
+
p = subprocess.Popen(cmd, shell=True)
|
1413
|
+
|
1414
|
+
if opt.timeout > 0:
|
1415
|
+
signal.alarm(opt.timeout)
|
1416
|
+
try:
|
1417
|
+
return p.wait()
|
1418
|
+
except AlarmInterrupt:
|
1419
|
+
self.log(" Timed out")
|
1420
|
+
return 4
|
1421
|
+
except KeyboardInterrupt:
|
1422
|
+
self.log(" Interrupted")
|
1423
|
+
return 128
|
1424
|
+
finally:
|
1425
|
+
signal.alarm(0)
|
1426
|
+
if p.returncode is None:
|
1427
|
+
p.terminate()
|
1428
|
+
|
1429
|
+
def run(self):
|
1430
|
+
"""
|
1431
|
+
This function is called after initialization to set up and run all doctests.
|
1432
|
+
|
1433
|
+
EXAMPLES::
|
1434
|
+
|
1435
|
+
sage: from sage.doctest.control import DocTestDefaults, DocTestController
|
1436
|
+
sage: from sage.env import SAGE_SRC
|
1437
|
+
sage: import os
|
1438
|
+
sage: DD = DocTestDefaults()
|
1439
|
+
sage: filename = os.path.join(SAGE_SRC, "sage", "sets", "non_negative_integers.py")
|
1440
|
+
sage: DC = DocTestController(DD, [filename])
|
1441
|
+
sage: DC.run()
|
1442
|
+
Running doctests with ID ...
|
1443
|
+
Doctesting 1 file.
|
1444
|
+
sage -t .../sage/sets/non_negative_integers.py
|
1445
|
+
[... tests, ...s wall]
|
1446
|
+
----------------------------------------------------------------------
|
1447
|
+
All tests passed!
|
1448
|
+
----------------------------------------------------------------------
|
1449
|
+
Total time for all tests: ... seconds
|
1450
|
+
cpu time: ... seconds
|
1451
|
+
cumulative wall time: ... seconds
|
1452
|
+
Features detected...
|
1453
|
+
0
|
1454
|
+
|
1455
|
+
We check that :issue:`25378` is fixed (testing external packages
|
1456
|
+
while providing a logfile does not raise a ValueError: I/O
|
1457
|
+
operation on closed file)::
|
1458
|
+
|
1459
|
+
sage: logfile = tmp_filename(ext='.log')
|
1460
|
+
sage: DD = DocTestDefaults(optional=set(['sage', 'external']), logfile=logfile)
|
1461
|
+
sage: filename = tmp_filename(ext='.py')
|
1462
|
+
sage: DC = DocTestController(DD, [filename])
|
1463
|
+
sage: DC.run()
|
1464
|
+
Running doctests with ID ...
|
1465
|
+
Using --optional=external,sage
|
1466
|
+
Features to be detected: ...
|
1467
|
+
Doctesting 1 file.
|
1468
|
+
sage -t ....py
|
1469
|
+
[0 tests, ...s wall]
|
1470
|
+
----------------------------------------------------------------------
|
1471
|
+
All tests passed!
|
1472
|
+
----------------------------------------------------------------------
|
1473
|
+
Total time for all tests: ... seconds
|
1474
|
+
cpu time: ... seconds
|
1475
|
+
cumulative wall time: ... seconds
|
1476
|
+
Features detected...
|
1477
|
+
0
|
1478
|
+
|
1479
|
+
We test the ``--hide`` option (:issue:`34185`)::
|
1480
|
+
|
1481
|
+
sage: from sage.doctest.control import test_hide
|
1482
|
+
sage: filename = tmp_filename(ext='.py')
|
1483
|
+
sage: with open(filename, 'w') as f:
|
1484
|
+
....: f.write(test_hide)
|
1485
|
+
....: f.close()
|
1486
|
+
714
|
1487
|
+
sage: DF = DocTestDefaults(hide='buckygen,all')
|
1488
|
+
sage: DC = DocTestController(DF, [filename])
|
1489
|
+
sage: DC.run()
|
1490
|
+
Running doctests with ID ...
|
1491
|
+
Using --optional=sage...
|
1492
|
+
Features to be detected: ...
|
1493
|
+
Doctesting 1 file.
|
1494
|
+
sage -t ....py
|
1495
|
+
[4 tests, ...s wall]
|
1496
|
+
----------------------------------------------------------------------
|
1497
|
+
All tests passed!
|
1498
|
+
----------------------------------------------------------------------
|
1499
|
+
Total time for all tests: ... seconds
|
1500
|
+
cpu time: ... seconds
|
1501
|
+
cumulative wall time: ... seconds
|
1502
|
+
Features detected...
|
1503
|
+
0
|
1504
|
+
|
1505
|
+
sage: DF = DocTestDefaults(hide='benzene,optional')
|
1506
|
+
sage: DC = DocTestController(DF, [filename])
|
1507
|
+
sage: DC.run()
|
1508
|
+
Running doctests with ID ...
|
1509
|
+
Using --optional=sage
|
1510
|
+
Features to be detected: ...
|
1511
|
+
Doctesting 1 file.
|
1512
|
+
sage -t ....py
|
1513
|
+
[4 tests, ...s wall]
|
1514
|
+
----------------------------------------------------------------------
|
1515
|
+
All tests passed!
|
1516
|
+
----------------------------------------------------------------------
|
1517
|
+
Total time for all tests: ... seconds
|
1518
|
+
cpu time: ... seconds
|
1519
|
+
cumulative wall time: ... seconds
|
1520
|
+
Features detected...
|
1521
|
+
0
|
1522
|
+
|
1523
|
+
Test *Features that have been hidden* message::
|
1524
|
+
|
1525
|
+
sage: DC.run() # optional - meataxe
|
1526
|
+
Running doctests with ID ...
|
1527
|
+
Using --optional=sage
|
1528
|
+
Features to be detected: ...
|
1529
|
+
Doctesting 1 file.
|
1530
|
+
sage -t ....py
|
1531
|
+
[4 tests, ...s wall]
|
1532
|
+
----------------------------------------------------------------------
|
1533
|
+
All tests passed!
|
1534
|
+
----------------------------------------------------------------------
|
1535
|
+
Total time for all tests: ... seconds
|
1536
|
+
cpu time: ... seconds
|
1537
|
+
cumulative wall time: ... seconds
|
1538
|
+
Features detected...
|
1539
|
+
Features that have been hidden: ...meataxe...
|
1540
|
+
0
|
1541
|
+
"""
|
1542
|
+
opt = self.options
|
1543
|
+
L = (opt.gdb, opt.lldb, opt.valgrind, opt.massif, opt.cachegrind, opt.omega)
|
1544
|
+
if any(L):
|
1545
|
+
if L.count(True) > 1:
|
1546
|
+
self.log("You may only specify one of gdb, valgrind/memcheck, massif, cachegrind, omega")
|
1547
|
+
return 2
|
1548
|
+
return self.run_val_gdb()
|
1549
|
+
else:
|
1550
|
+
self.create_run_id()
|
1551
|
+
from sage.env import SAGE_ROOT_GIT, SAGE_LOCAL, SAGE_VENV
|
1552
|
+
# SAGE_ROOT_GIT can be None on distributions which typically
|
1553
|
+
# only have the SAGE_LOCAL install tree but not SAGE_ROOT
|
1554
|
+
if (SAGE_ROOT_GIT is not None) and os.path.isdir(SAGE_ROOT_GIT):
|
1555
|
+
import subprocess
|
1556
|
+
try:
|
1557
|
+
branch = subprocess.check_output(["git",
|
1558
|
+
"--git-dir=" + SAGE_ROOT_GIT,
|
1559
|
+
"rev-parse",
|
1560
|
+
"--abbrev-ref",
|
1561
|
+
"HEAD"])
|
1562
|
+
branch = branch.decode('utf-8')
|
1563
|
+
self.log("Git branch: " + branch, end="")
|
1564
|
+
except subprocess.CalledProcessError:
|
1565
|
+
pass
|
1566
|
+
try:
|
1567
|
+
ref = subprocess.check_output(["git",
|
1568
|
+
"--git-dir=" + SAGE_ROOT_GIT,
|
1569
|
+
"describe",
|
1570
|
+
"--always",
|
1571
|
+
"--dirty"])
|
1572
|
+
ref = ref.decode('utf-8')
|
1573
|
+
self.log("Git ref: " + ref, end="")
|
1574
|
+
except subprocess.CalledProcessError:
|
1575
|
+
pass
|
1576
|
+
|
1577
|
+
self.log(f"Running with {SAGE_LOCAL=} and {SAGE_VENV=}")
|
1578
|
+
|
1579
|
+
self.log("Using --optional=" + self._optional_tags_string())
|
1580
|
+
available_software._allow_external = self.options.optional is True or 'external' in self.options.optional
|
1581
|
+
|
1582
|
+
for h in self.options.hide:
|
1583
|
+
try:
|
1584
|
+
i = available_software._indices[h]
|
1585
|
+
except KeyError:
|
1586
|
+
pass
|
1587
|
+
else:
|
1588
|
+
f = available_software._features[i]
|
1589
|
+
f.hide()
|
1590
|
+
self.options.hidden_features.add(f)
|
1591
|
+
for g in f.joined_features():
|
1592
|
+
if g.name in self.options.optional:
|
1593
|
+
self.options.optional.discard(g.name)
|
1594
|
+
|
1595
|
+
for o in self.options.disabled_optional:
|
1596
|
+
try:
|
1597
|
+
i = available_software._indices[o]
|
1598
|
+
except KeyError:
|
1599
|
+
pass
|
1600
|
+
else:
|
1601
|
+
available_software._seen[i] = -1
|
1602
|
+
|
1603
|
+
self.log("Features to be detected: " + ','.join(available_software.detectable()))
|
1604
|
+
if self.options.probe:
|
1605
|
+
self.log("Features to be probed: " + ('all' if self.options.probe is True
|
1606
|
+
else ','.join(self.options.probe)))
|
1607
|
+
self.add_files()
|
1608
|
+
self.expand_files_into_sources()
|
1609
|
+
self.filter_sources()
|
1610
|
+
self.sort_sources()
|
1611
|
+
self.run_doctests()
|
1612
|
+
|
1613
|
+
self.log("Features detected for doctesting: "
|
1614
|
+
+ ','.join(available_software.seen()))
|
1615
|
+
if self.options.hidden_features:
|
1616
|
+
for f in self.options.hidden_features:
|
1617
|
+
f.unhide()
|
1618
|
+
self.log("Features that have been hidden: " + ','.join(available_software.hidden()))
|
1619
|
+
self.cleanup()
|
1620
|
+
return self.reporter.error_status
|
1621
|
+
|
1622
|
+
|
1623
|
+
def run_doctests(module, options=None):
|
1624
|
+
"""
|
1625
|
+
Run the doctests in a given file.
|
1626
|
+
|
1627
|
+
INPUT:
|
1628
|
+
|
1629
|
+
- ``module`` -- a Sage module, a string, or a list of such
|
1630
|
+
|
1631
|
+
- ``options`` -- a DocTestDefaults object or ``None``
|
1632
|
+
|
1633
|
+
EXAMPLES::
|
1634
|
+
|
1635
|
+
sage: run_doctests(sage.rings.all)
|
1636
|
+
Running doctests with ID ...
|
1637
|
+
Doctesting 1 file.
|
1638
|
+
sage -t .../sage/rings/all.py
|
1639
|
+
[... tests, ...s wall]
|
1640
|
+
----------------------------------------------------------------------
|
1641
|
+
All tests passed!
|
1642
|
+
----------------------------------------------------------------------
|
1643
|
+
Total time for all tests: ... seconds
|
1644
|
+
cpu time: ... seconds
|
1645
|
+
cumulative wall time: ... seconds
|
1646
|
+
Features detected...
|
1647
|
+
"""
|
1648
|
+
import sys
|
1649
|
+
sys.stdout.flush()
|
1650
|
+
|
1651
|
+
def stringify(x):
|
1652
|
+
if isinstance(x, (list, tuple)):
|
1653
|
+
F = [stringify(a) for a in x]
|
1654
|
+
return sage.misc.flatten.flatten(F)
|
1655
|
+
elif isinstance(x, types.ModuleType):
|
1656
|
+
F = x.__file__.replace(SAGE_LIB, SAGE_SRC)
|
1657
|
+
base, pyfile = os.path.split(F)
|
1658
|
+
file, ext = os.path.splitext(pyfile)
|
1659
|
+
if ext == ".pyc":
|
1660
|
+
ext = ".py"
|
1661
|
+
elif ext == ".so":
|
1662
|
+
ext = ".pyx"
|
1663
|
+
if file == "__init__":
|
1664
|
+
return [base]
|
1665
|
+
else:
|
1666
|
+
return [os.path.join(base, file) + ext]
|
1667
|
+
elif isinstance(x, str):
|
1668
|
+
return [os.path.abspath(x)]
|
1669
|
+
F = stringify(module)
|
1670
|
+
if options is None:
|
1671
|
+
options = DocTestDefaults()
|
1672
|
+
DC = DocTestController(options, F)
|
1673
|
+
|
1674
|
+
# Determine whether we're in doctest mode
|
1675
|
+
save_dtmode = sage.doctest.DOCTEST_MODE
|
1676
|
+
|
1677
|
+
# We need the following if we're not in DOCTEST_MODE
|
1678
|
+
# Tell IPython to avoid colors: it screws up the output checking.
|
1679
|
+
if not save_dtmode:
|
1680
|
+
if options.debug:
|
1681
|
+
raise ValueError("You should not try to run doctests with a debugger from within Sage: IPython objects to embedded shells")
|
1682
|
+
from IPython.core.getipython import get_ipython
|
1683
|
+
IP = get_ipython()
|
1684
|
+
if IP is not None:
|
1685
|
+
old_color = IP.colors
|
1686
|
+
IP.run_line_magic('colors', 'NoColor')
|
1687
|
+
old_config_color = IP.config.TerminalInteractiveShell.colors
|
1688
|
+
IP.config.TerminalInteractiveShell.colors = 'NoColor'
|
1689
|
+
|
1690
|
+
try:
|
1691
|
+
DC.run()
|
1692
|
+
finally:
|
1693
|
+
sage.doctest.DOCTEST_MODE = save_dtmode
|
1694
|
+
if not save_dtmode and IP is not None:
|
1695
|
+
IP.run_line_magic('colors', old_color)
|
1696
|
+
IP.config.TerminalInteractiveShell.colors = old_config_color
|
1697
|
+
|
1698
|
+
|
1699
|
+
###############################################################################
|
1700
|
+
# Declaration of doctest strings
|
1701
|
+
###############################################################################
|
1702
|
+
|
1703
|
+
test_hide = r"""r{quotmark}
|
1704
|
+
{prompt}: next(graphs.fullerenes(20))
|
1705
|
+
Traceback (most recent call last):
|
1706
|
+
...
|
1707
|
+
FeatureNotPresentError: buckygen is not available.
|
1708
|
+
...
|
1709
|
+
{prompt}: next(graphs.fullerenes(20)) # optional - buckygen
|
1710
|
+
Graph on 20 vertices
|
1711
|
+
|
1712
|
+
{prompt}: len(list(graphs.fusenes(2)))
|
1713
|
+
Traceback (most recent call last):
|
1714
|
+
...
|
1715
|
+
FeatureNotPresentError: benzene is not available.
|
1716
|
+
...
|
1717
|
+
{prompt}: len(list(graphs.fusenes(2))) # optional - benzene
|
1718
|
+
1
|
1719
|
+
{prompt}: from sage.matrix.matrix_space import get_matrix_class
|
1720
|
+
{prompt}: get_matrix_class(GF(25,'x'), 4, 4, False, 'meataxe')
|
1721
|
+
Failed lazy import:
|
1722
|
+
meataxe is not available.
|
1723
|
+
...
|
1724
|
+
{prompt}: get_matrix_class(GF(25,'x'), 4, 4, False, 'meataxe') # optional - meataxe
|
1725
|
+
<class 'sage.matrix.matrix_gfpn_dense.Matrix_gfpn_dense'>
|
1726
|
+
{quotmark}
|
1727
|
+
""".format(quotmark='"""', prompt='sage') # using prompt to hide these lines from _test_enough_doctests
|