passagemath-repl 10.5.1__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.5.1.data/scripts/sage-cachegrind +25 -0
- passagemath_repl-10.5.1.data/scripts/sage-callgrind +16 -0
- passagemath_repl-10.5.1.data/scripts/sage-cleaner +230 -0
- passagemath_repl-10.5.1.data/scripts/sage-coverage +327 -0
- passagemath_repl-10.5.1.data/scripts/sage-eval +14 -0
- passagemath_repl-10.5.1.data/scripts/sage-fixdoctests +710 -0
- passagemath_repl-10.5.1.data/scripts/sage-inline-fortran +12 -0
- passagemath_repl-10.5.1.data/scripts/sage-ipynb2rst +50 -0
- passagemath_repl-10.5.1.data/scripts/sage-ipython +16 -0
- passagemath_repl-10.5.1.data/scripts/sage-massif +25 -0
- passagemath_repl-10.5.1.data/scripts/sage-notebook +267 -0
- passagemath_repl-10.5.1.data/scripts/sage-omega +25 -0
- passagemath_repl-10.5.1.data/scripts/sage-preparse +302 -0
- passagemath_repl-10.5.1.data/scripts/sage-run +27 -0
- passagemath_repl-10.5.1.data/scripts/sage-run-cython +10 -0
- passagemath_repl-10.5.1.data/scripts/sage-runtests +9 -0
- passagemath_repl-10.5.1.data/scripts/sage-startuptime.py +163 -0
- passagemath_repl-10.5.1.data/scripts/sage-valgrind +34 -0
- passagemath_repl-10.5.1.dist-info/METADATA +77 -0
- passagemath_repl-10.5.1.dist-info/RECORD +162 -0
- passagemath_repl-10.5.1.dist-info/WHEEL +5 -0
- passagemath_repl-10.5.1.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 +134 -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 +249 -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/all.py +0 -0
- sage/tests/all__sagemath_repl.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 +1925 -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 +796 -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/forker.py
ADDED
@@ -0,0 +1,2665 @@
|
|
1
|
+
# sage_setup: distribution = sagemath-repl
|
2
|
+
"""
|
3
|
+
Processes for running doctests
|
4
|
+
|
5
|
+
This module controls the processes started by Sage that actually run
|
6
|
+
the doctests.
|
7
|
+
|
8
|
+
EXAMPLES:
|
9
|
+
|
10
|
+
The following examples are used in doctesting this file::
|
11
|
+
|
12
|
+
sage: doctest_var = 42; doctest_var^2
|
13
|
+
1764
|
14
|
+
sage: R.<a> = ZZ[]
|
15
|
+
sage: a + doctest_var
|
16
|
+
a + 42
|
17
|
+
|
18
|
+
AUTHORS:
|
19
|
+
|
20
|
+
- David Roe (2012-03-27) -- initial version, based on Robert Bradshaw's code.
|
21
|
+
|
22
|
+
- Jeroen Demeyer (2013 and 2015) -- major improvements to forking and logging
|
23
|
+
"""
|
24
|
+
|
25
|
+
# ****************************************************************************
|
26
|
+
# Copyright (C) 2012-2013 David Roe <roed.math@gmail.com>
|
27
|
+
# 2012 Robert Bradshaw <robertwb@gmail.com>
|
28
|
+
# 2012 William Stein <wstein@gmail.com>
|
29
|
+
# 2013 R. Andrew Ohana
|
30
|
+
# 2013-2018 Jeroen Demeyer <jdemeyer@cage.ugent.be>
|
31
|
+
# 2013-2020 John H. Palmieri
|
32
|
+
# 2013-2017 Volker Braun
|
33
|
+
# 2014 André Apitzsch
|
34
|
+
# 2014 Darij Grinberg
|
35
|
+
# 2016-2021 Frédéric Chapoton
|
36
|
+
# 2017-2019 Erik M. Bray
|
37
|
+
# 2018 Julian Rüth
|
38
|
+
# 2020 Jonathan Kliem
|
39
|
+
# 2020-2023 Matthias Koeppe
|
40
|
+
# 2022 Markus Wageringel
|
41
|
+
# 2022 Michael Orlitzky
|
42
|
+
#
|
43
|
+
# Distributed under the terms of the GNU General Public License (GPL)
|
44
|
+
# as published by the Free Software Foundation; either version 2 of
|
45
|
+
# the License, or (at your option) any later version.
|
46
|
+
# https://www.gnu.org/licenses/
|
47
|
+
# ****************************************************************************
|
48
|
+
|
49
|
+
|
50
|
+
import os
|
51
|
+
import platform
|
52
|
+
import sys
|
53
|
+
import time
|
54
|
+
import signal
|
55
|
+
import linecache
|
56
|
+
import hashlib
|
57
|
+
import multiprocessing
|
58
|
+
import warnings
|
59
|
+
import re
|
60
|
+
import errno
|
61
|
+
import doctest
|
62
|
+
import traceback
|
63
|
+
import tempfile
|
64
|
+
from collections import defaultdict
|
65
|
+
from dis import findlinestarts
|
66
|
+
from queue import Empty
|
67
|
+
import gc
|
68
|
+
import IPython.lib.pretty
|
69
|
+
|
70
|
+
import sage.misc.randstate as randstate
|
71
|
+
from sage.misc.timing import walltime
|
72
|
+
from .util import Timer, RecordingDict, count_noun
|
73
|
+
from .sources import DictAsObject
|
74
|
+
from .parsing import OriginalSource, reduce_hex
|
75
|
+
from sage.structure.sage_object import SageObject
|
76
|
+
from .parsing import SageOutputChecker, pre_hash, get_source, unparse_optional_tags
|
77
|
+
from sage.repl.user_globals import set_globals
|
78
|
+
from sage.cpython.atexit import restore_atexit
|
79
|
+
from sage.cpython.string import bytes_to_str, str_to_bytes
|
80
|
+
|
81
|
+
# With OS X, Python 3.8 defaults to use 'spawn' instead of 'fork' in
|
82
|
+
# multiprocessing, and Sage doctesting doesn't work with 'spawn'. See
|
83
|
+
# trac #27754.
|
84
|
+
if platform.system() == 'Darwin':
|
85
|
+
multiprocessing.set_start_method('fork', force=True)
|
86
|
+
|
87
|
+
|
88
|
+
def _sorted_dict_pprinter_factory(start, end):
|
89
|
+
"""
|
90
|
+
Modified version of :func:`IPython.lib.pretty._dict_pprinter_factory`
|
91
|
+
that sorts the keys of dictionaries for printing.
|
92
|
+
|
93
|
+
EXAMPLES::
|
94
|
+
|
95
|
+
sage: {2: 0, 1: 0} # indirect doctest
|
96
|
+
{1: 0, 2: 0}
|
97
|
+
"""
|
98
|
+
def inner(obj, p, cycle):
|
99
|
+
if cycle:
|
100
|
+
return p.text('{...}')
|
101
|
+
step = len(start)
|
102
|
+
p.begin_group(step, start)
|
103
|
+
keys = obj.keys()
|
104
|
+
keys = IPython.lib.pretty._sorted_for_pprint(keys)
|
105
|
+
for idx, key in p._enumerate(keys):
|
106
|
+
if idx:
|
107
|
+
p.text(',')
|
108
|
+
p.breakable()
|
109
|
+
p.pretty(key)
|
110
|
+
p.text(': ')
|
111
|
+
p.pretty(obj[key])
|
112
|
+
p.end_group(step, end)
|
113
|
+
return inner
|
114
|
+
|
115
|
+
|
116
|
+
def init_sage(controller=None):
|
117
|
+
"""
|
118
|
+
Import the Sage library.
|
119
|
+
|
120
|
+
This function is called once at the beginning of a doctest run
|
121
|
+
(rather than once for each file). It imports the Sage library,
|
122
|
+
sets DOCTEST_MODE to True, and invalidates any interfaces.
|
123
|
+
|
124
|
+
EXAMPLES::
|
125
|
+
|
126
|
+
sage: # needs sage.all
|
127
|
+
sage: from sage.doctest.forker import init_sage
|
128
|
+
sage: sage.doctest.DOCTEST_MODE = False
|
129
|
+
sage: init_sage()
|
130
|
+
sage: sage.doctest.DOCTEST_MODE
|
131
|
+
True
|
132
|
+
|
133
|
+
Check that pexpect interfaces are invalidated, but still work::
|
134
|
+
|
135
|
+
sage: # needs sage.all
|
136
|
+
sage: gap.eval("my_test_var := 42;")
|
137
|
+
'42'
|
138
|
+
sage: gap.eval("my_test_var;")
|
139
|
+
'42'
|
140
|
+
sage: init_sage()
|
141
|
+
sage: gap('Group((1,2,3)(4,5), (3,4))')
|
142
|
+
Group( [ (1,2,3)(4,5), (3,4) ] )
|
143
|
+
sage: gap.eval("my_test_var;")
|
144
|
+
Traceback (most recent call last):
|
145
|
+
...
|
146
|
+
RuntimeError: Gap produced error output...
|
147
|
+
|
148
|
+
Check that SymPy equation pretty printer is limited in doctest
|
149
|
+
mode to default width (80 chars)::
|
150
|
+
|
151
|
+
sage: # needs sympy
|
152
|
+
sage: from sympy import sympify
|
153
|
+
sage: from sympy.printing.pretty.pretty import PrettyPrinter
|
154
|
+
sage: s = sympify('+x^'.join(str(i) for i in range(30)))
|
155
|
+
sage: print(PrettyPrinter(settings={'wrap_line': True}).doprint(s))
|
156
|
+
29 28 27 26 25 24 23 22 21 20 19 18 17...
|
157
|
+
x + x + x + x + x + x + x + x + x + x + x + x + x...
|
158
|
+
<BLANKLINE>
|
159
|
+
... 16 15 14 13 12 11 10 9 8 7 6 5 4 3...
|
160
|
+
...x + x + x + x + x + x + x + x + x + x + x + x + x + x...
|
161
|
+
<BLANKLINE>
|
162
|
+
...
|
163
|
+
|
164
|
+
The displayhook sorts dictionary keys to simplify doctesting of
|
165
|
+
dictionary output::
|
166
|
+
|
167
|
+
sage: {'a':23, 'b':34, 'au':56, 'bbf':234, 'aaa':234}
|
168
|
+
{'a': 23, 'aaa': 234, 'au': 56, 'b': 34, 'bbf': 234}
|
169
|
+
"""
|
170
|
+
try:
|
171
|
+
# We need to ensure that the Matplotlib font cache is built to
|
172
|
+
# avoid spurious warnings (see Issue #20222).
|
173
|
+
import matplotlib.font_manager
|
174
|
+
except ImportError:
|
175
|
+
# Do not require matplotlib for running doctests (Issue #25106).
|
176
|
+
pass
|
177
|
+
else:
|
178
|
+
# Make sure that the agg backend is selected during doctesting.
|
179
|
+
# This needs to be done before any other matplotlib calls.
|
180
|
+
matplotlib.use('agg')
|
181
|
+
|
182
|
+
# Do this once before forking off child processes running the tests.
|
183
|
+
# This is more efficient because we only need to wait once for the
|
184
|
+
# Sage imports.
|
185
|
+
import sage.doctest
|
186
|
+
sage.doctest.DOCTEST_MODE = True
|
187
|
+
|
188
|
+
# Set the Python PRNG class to the Python 2 implementation for consistency
|
189
|
+
# of 'random' test results that use it; see
|
190
|
+
# https://github.com/sagemath/sage/issues/24508
|
191
|
+
# We use the baked in copy of the random module for both Python 2 and 3
|
192
|
+
# since, although the upstream copy is unlikely to change, this further
|
193
|
+
# ensures consistency of test results
|
194
|
+
import sage.misc.randstate
|
195
|
+
from sage.cpython._py2_random import Random
|
196
|
+
sage.misc.randstate.DEFAULT_PYTHON_RANDOM = Random
|
197
|
+
|
198
|
+
# IPython's pretty printer sorts the repr of dicts by their keys by default
|
199
|
+
# (or their keys' str() if they are not otherwise orderable). However, it
|
200
|
+
# disables this for CPython 3.6+ opting to instead display dicts' "natural"
|
201
|
+
# insertion order, which is preserved in those versions).
|
202
|
+
# However, this order is random in some instances.
|
203
|
+
# Also modifications of code may affect the order.
|
204
|
+
# So here we fore sorted dict printing.
|
205
|
+
IPython.lib.pretty.for_type(dict, _sorted_dict_pprinter_factory('{', '}'))
|
206
|
+
|
207
|
+
if controller is None:
|
208
|
+
import sage.repl.ipython_kernel.all_jupyter
|
209
|
+
else:
|
210
|
+
controller.load_environment()
|
211
|
+
|
212
|
+
try:
|
213
|
+
from sage.interfaces.quit import invalidate_all
|
214
|
+
invalidate_all()
|
215
|
+
except ModuleNotFoundError:
|
216
|
+
pass
|
217
|
+
|
218
|
+
# Disable cysignals debug messages in doctests: this is needed to
|
219
|
+
# make doctests pass when cysignals was built with debugging enabled
|
220
|
+
from cysignals.signals import set_debug_level
|
221
|
+
set_debug_level(0)
|
222
|
+
|
223
|
+
# Use the rich output backend for doctest
|
224
|
+
from sage.repl.rich_output import get_display_manager
|
225
|
+
dm = get_display_manager()
|
226
|
+
from sage.repl.rich_output.backend_doctest import BackendDoctest
|
227
|
+
dm.switch_backend(BackendDoctest())
|
228
|
+
|
229
|
+
# Switch on extra debugging
|
230
|
+
from sage.structure.debug_options import debug
|
231
|
+
debug.refine_category_hash_check = True
|
232
|
+
|
233
|
+
# We import readline before forking, otherwise Pdb doesn't work
|
234
|
+
# on OS X: https://github.com/sagemath/sage/issues/14289
|
235
|
+
try:
|
236
|
+
import readline
|
237
|
+
except ModuleNotFoundError:
|
238
|
+
# Do not require readline for running doctests (Issue #31160).
|
239
|
+
pass
|
240
|
+
|
241
|
+
try:
|
242
|
+
import sympy
|
243
|
+
except ImportError:
|
244
|
+
# Do not require sympy for running doctests (Issue #25106).
|
245
|
+
pass
|
246
|
+
else:
|
247
|
+
# Disable SymPy terminal width detection
|
248
|
+
from sympy.printing.pretty.stringpict import stringPict
|
249
|
+
stringPict.terminal_width = lambda self: 0
|
250
|
+
|
251
|
+
|
252
|
+
def showwarning_with_traceback(message, category, filename, lineno, file=None, line=None):
|
253
|
+
r"""
|
254
|
+
Displays a warning message with a traceback.
|
255
|
+
|
256
|
+
INPUT: see :func:`warnings.showwarning`.
|
257
|
+
|
258
|
+
OUTPUT: none
|
259
|
+
|
260
|
+
EXAMPLES::
|
261
|
+
|
262
|
+
sage: from sage.doctest.forker import showwarning_with_traceback
|
263
|
+
sage: showwarning_with_traceback("bad stuff", UserWarning, "myfile.py", 0)
|
264
|
+
doctest:warning...
|
265
|
+
File "<doctest sage.doctest.forker.showwarning_with_traceback[1]>", line 1, in <module>
|
266
|
+
showwarning_with_traceback("bad stuff", UserWarning, "myfile.py", Integer(0))
|
267
|
+
:
|
268
|
+
UserWarning: bad stuff
|
269
|
+
"""
|
270
|
+
# Flush stdout to get predictable ordering of output and warnings
|
271
|
+
sys.stdout.flush()
|
272
|
+
|
273
|
+
# Get traceback to display in warning
|
274
|
+
tb = traceback.extract_stack()
|
275
|
+
tb = tb[:-1] # Drop this stack frame for showwarning_with_traceback()
|
276
|
+
for i, frame_summary in enumerate(tb):
|
277
|
+
if frame_summary.filename.endswith('sage/doctest/forker.py') and frame_summary.name == 'compile_and_execute':
|
278
|
+
tb = tb[i + 1:]
|
279
|
+
break
|
280
|
+
|
281
|
+
# Format warning
|
282
|
+
lines = ["doctest:warning\n"] # Match historical warning messages in doctests
|
283
|
+
lines.extend(traceback.format_list(tb))
|
284
|
+
lines.append(":\n") # Match historical warning messages in doctests
|
285
|
+
lines.extend(traceback.format_exception_only(category, category(message)))
|
286
|
+
|
287
|
+
if file is None:
|
288
|
+
file = sys.stderr
|
289
|
+
try:
|
290
|
+
file.writelines(lines)
|
291
|
+
file.flush()
|
292
|
+
except OSError:
|
293
|
+
pass # the file is invalid
|
294
|
+
|
295
|
+
|
296
|
+
class SageSpoofInOut(SageObject):
|
297
|
+
r"""
|
298
|
+
We replace the standard :class:`doctest._SpoofOut` for three reasons:
|
299
|
+
|
300
|
+
- we need to divert the output of C programs that don't print
|
301
|
+
through sys.stdout,
|
302
|
+
- we want the ability to recover partial output from doctest
|
303
|
+
processes that segfault.
|
304
|
+
- we also redirect stdin (usually from /dev/null) during doctests.
|
305
|
+
|
306
|
+
This class defines streams ``self.real_stdin``, ``self.real_stdout``
|
307
|
+
and ``self.real_stderr`` which refer to the original streams.
|
308
|
+
|
309
|
+
INPUT:
|
310
|
+
|
311
|
+
- ``outfile`` -- (default: ``tempfile.TemporaryFile()``) a seekable open file
|
312
|
+
object to which stdout and stderr should be redirected
|
313
|
+
|
314
|
+
- ``infile`` -- (default: ``open(os.devnull)``) an open file object
|
315
|
+
from which stdin should be redirected
|
316
|
+
|
317
|
+
EXAMPLES::
|
318
|
+
|
319
|
+
sage: import subprocess, tempfile
|
320
|
+
sage: from sage.doctest.forker import SageSpoofInOut
|
321
|
+
sage: O = tempfile.TemporaryFile()
|
322
|
+
sage: S = SageSpoofInOut(O)
|
323
|
+
sage: try:
|
324
|
+
....: S.start_spoofing()
|
325
|
+
....: print("hello world")
|
326
|
+
....: finally:
|
327
|
+
....: S.stop_spoofing()
|
328
|
+
....:
|
329
|
+
sage: S.getvalue()
|
330
|
+
'hello world\n'
|
331
|
+
sage: _ = O.seek(0)
|
332
|
+
sage: S = SageSpoofInOut(outfile=sys.stdout, infile=O)
|
333
|
+
sage: try:
|
334
|
+
....: S.start_spoofing()
|
335
|
+
....: _ = subprocess.check_call("cat")
|
336
|
+
....: finally:
|
337
|
+
....: S.stop_spoofing()
|
338
|
+
....:
|
339
|
+
hello world
|
340
|
+
sage: O.close()
|
341
|
+
"""
|
342
|
+
def __init__(self, outfile=None, infile=None):
|
343
|
+
"""
|
344
|
+
Initialization.
|
345
|
+
|
346
|
+
TESTS::
|
347
|
+
|
348
|
+
sage: from tempfile import TemporaryFile
|
349
|
+
sage: from sage.doctest.forker import SageSpoofInOut
|
350
|
+
sage: with TemporaryFile() as outfile:
|
351
|
+
....: with TemporaryFile() as infile:
|
352
|
+
....: SageSpoofInOut(outfile, infile)
|
353
|
+
<sage.doctest.forker.SageSpoofInOut object at ...>
|
354
|
+
"""
|
355
|
+
if infile is None:
|
356
|
+
self.infile = open(os.devnull)
|
357
|
+
self._close_infile = True
|
358
|
+
else:
|
359
|
+
self.infile = infile
|
360
|
+
self._close_infile = False
|
361
|
+
if outfile is None:
|
362
|
+
self.outfile = tempfile.TemporaryFile()
|
363
|
+
self._close_outfile = True
|
364
|
+
else:
|
365
|
+
self.outfile = outfile
|
366
|
+
self._close_outfile = False
|
367
|
+
self.spoofing = False
|
368
|
+
self.real_stdin = os.fdopen(os.dup(sys.stdin.fileno()), "r")
|
369
|
+
self.real_stdout = os.fdopen(os.dup(sys.stdout.fileno()), "w")
|
370
|
+
self.real_stderr = os.fdopen(os.dup(sys.stderr.fileno()), "w")
|
371
|
+
self.position = 0
|
372
|
+
|
373
|
+
def __del__(self):
|
374
|
+
"""
|
375
|
+
Stop spoofing.
|
376
|
+
|
377
|
+
TESTS::
|
378
|
+
|
379
|
+
sage: from sage.doctest.forker import SageSpoofInOut
|
380
|
+
sage: spoof = SageSpoofInOut()
|
381
|
+
sage: spoof.start_spoofing()
|
382
|
+
sage: print("Spoofed!") # No output
|
383
|
+
sage: del spoof
|
384
|
+
sage: print("Not spoofed!")
|
385
|
+
Not spoofed!
|
386
|
+
"""
|
387
|
+
self.stop_spoofing()
|
388
|
+
if self._close_infile:
|
389
|
+
self.infile.close()
|
390
|
+
if self._close_outfile:
|
391
|
+
self.outfile.close()
|
392
|
+
for stream in ('stdin', 'stdout', 'stderr'):
|
393
|
+
getattr(self, 'real_' + stream).close()
|
394
|
+
|
395
|
+
def start_spoofing(self):
|
396
|
+
r"""
|
397
|
+
Set stdin to read from ``self.infile`` and stdout to print to
|
398
|
+
``self.outfile``.
|
399
|
+
|
400
|
+
EXAMPLES::
|
401
|
+
|
402
|
+
sage: import os, tempfile
|
403
|
+
sage: from sage.doctest.forker import SageSpoofInOut
|
404
|
+
sage: O = tempfile.TemporaryFile()
|
405
|
+
sage: S = SageSpoofInOut(O)
|
406
|
+
sage: try:
|
407
|
+
....: S.start_spoofing()
|
408
|
+
....: print("this is not printed")
|
409
|
+
....: finally:
|
410
|
+
....: S.stop_spoofing()
|
411
|
+
....:
|
412
|
+
sage: S.getvalue()
|
413
|
+
'this is not printed\n'
|
414
|
+
sage: _ = O.seek(0)
|
415
|
+
sage: S = SageSpoofInOut(infile=O)
|
416
|
+
sage: try:
|
417
|
+
....: S.start_spoofing()
|
418
|
+
....: v = sys.stdin.read()
|
419
|
+
....: finally:
|
420
|
+
....: S.stop_spoofing()
|
421
|
+
....:
|
422
|
+
sage: v
|
423
|
+
'this is not printed\n'
|
424
|
+
|
425
|
+
We also catch non-Python output::
|
426
|
+
|
427
|
+
sage: try:
|
428
|
+
....: S.start_spoofing()
|
429
|
+
....: retval = os.system('''echo "Hello there"\nif [ $? -eq 0 ]; then\necho "good"\nfi''')
|
430
|
+
....: finally:
|
431
|
+
....: S.stop_spoofing()
|
432
|
+
....:
|
433
|
+
sage: S.getvalue()
|
434
|
+
'Hello there\ngood\n'
|
435
|
+
sage: O.close()
|
436
|
+
"""
|
437
|
+
if not self.spoofing:
|
438
|
+
sys.stdout.flush()
|
439
|
+
sys.stderr.flush()
|
440
|
+
self.outfile.flush()
|
441
|
+
os.dup2(self.infile.fileno(), sys.stdin.fileno())
|
442
|
+
os.dup2(self.outfile.fileno(), sys.stdout.fileno())
|
443
|
+
os.dup2(self.outfile.fileno(), sys.stderr.fileno())
|
444
|
+
self.spoofing = True
|
445
|
+
|
446
|
+
def stop_spoofing(self):
|
447
|
+
"""
|
448
|
+
Reset stdin and stdout to their original values.
|
449
|
+
|
450
|
+
EXAMPLES::
|
451
|
+
|
452
|
+
sage: from sage.doctest.forker import SageSpoofInOut
|
453
|
+
sage: S = SageSpoofInOut()
|
454
|
+
sage: try:
|
455
|
+
....: S.start_spoofing()
|
456
|
+
....: print("this is not printed")
|
457
|
+
....: finally:
|
458
|
+
....: S.stop_spoofing()
|
459
|
+
....:
|
460
|
+
sage: print("this is now printed")
|
461
|
+
this is now printed
|
462
|
+
"""
|
463
|
+
if self.spoofing:
|
464
|
+
sys.stdout.flush()
|
465
|
+
sys.stderr.flush()
|
466
|
+
self.real_stdout.flush()
|
467
|
+
self.real_stderr.flush()
|
468
|
+
os.dup2(self.real_stdin.fileno(), sys.stdin.fileno())
|
469
|
+
os.dup2(self.real_stdout.fileno(), sys.stdout.fileno())
|
470
|
+
os.dup2(self.real_stderr.fileno(), sys.stderr.fileno())
|
471
|
+
self.spoofing = False
|
472
|
+
|
473
|
+
def getvalue(self):
|
474
|
+
r"""
|
475
|
+
Get the value that has been printed to ``outfile`` since the
|
476
|
+
last time this function was called.
|
477
|
+
|
478
|
+
EXAMPLES::
|
479
|
+
|
480
|
+
sage: from sage.doctest.forker import SageSpoofInOut
|
481
|
+
sage: S = SageSpoofInOut()
|
482
|
+
sage: try:
|
483
|
+
....: S.start_spoofing()
|
484
|
+
....: print("step 1")
|
485
|
+
....: finally:
|
486
|
+
....: S.stop_spoofing()
|
487
|
+
....:
|
488
|
+
sage: S.getvalue()
|
489
|
+
'step 1\n'
|
490
|
+
sage: try:
|
491
|
+
....: S.start_spoofing()
|
492
|
+
....: print("step 2")
|
493
|
+
....: finally:
|
494
|
+
....: S.stop_spoofing()
|
495
|
+
....:
|
496
|
+
sage: S.getvalue()
|
497
|
+
'step 2\n'
|
498
|
+
"""
|
499
|
+
sys.stdout.flush()
|
500
|
+
self.outfile.seek(self.position)
|
501
|
+
result = self.outfile.read()
|
502
|
+
self.position = self.outfile.tell()
|
503
|
+
if not result.endswith(b"\n"):
|
504
|
+
result += b"\n"
|
505
|
+
return bytes_to_str(result)
|
506
|
+
|
507
|
+
|
508
|
+
from collections import namedtuple
|
509
|
+
TestResults = namedtuple('TestResults', 'failed attempted')
|
510
|
+
|
511
|
+
|
512
|
+
class SageDocTestRunner(doctest.DocTestRunner):
|
513
|
+
def __init__(self, *args, **kwds):
|
514
|
+
"""
|
515
|
+
A customized version of DocTestRunner that tracks dependencies
|
516
|
+
of doctests.
|
517
|
+
|
518
|
+
INPUT:
|
519
|
+
|
520
|
+
- ``stdout`` -- an open file to restore for debugging
|
521
|
+
|
522
|
+
- ``checker`` -- ``None``, or an instance of
|
523
|
+
:class:`doctest.OutputChecker`
|
524
|
+
|
525
|
+
- ``verbose`` -- boolean, determines whether verbose printing
|
526
|
+
is enabled
|
527
|
+
|
528
|
+
- ``optionflags`` -- controls the comparison with the expected
|
529
|
+
output. See :mod:`testmod` for more information
|
530
|
+
|
531
|
+
- ``baseline`` -- dictionary, the ``baseline_stats`` value
|
532
|
+
|
533
|
+
EXAMPLES::
|
534
|
+
|
535
|
+
sage: from sage.doctest.parsing import SageOutputChecker
|
536
|
+
sage: from sage.doctest.forker import SageDocTestRunner
|
537
|
+
sage: from sage.doctest.control import DocTestDefaults; DD = DocTestDefaults()
|
538
|
+
sage: import doctest, sys, os
|
539
|
+
sage: DTR = SageDocTestRunner(SageOutputChecker(), verbose=False, sage_options=DD, optionflags=doctest.NORMALIZE_WHITESPACE|doctest.ELLIPSIS)
|
540
|
+
sage: DTR
|
541
|
+
<sage.doctest.forker.SageDocTestRunner object at ...>
|
542
|
+
"""
|
543
|
+
O = kwds.pop('outtmpfile', None)
|
544
|
+
self.msgfile = kwds.pop('msgfile', None)
|
545
|
+
self.options = kwds.pop('sage_options')
|
546
|
+
self.baseline = kwds.pop('baseline', {})
|
547
|
+
doctest.DocTestRunner.__init__(self, *args, **kwds)
|
548
|
+
self._fakeout = SageSpoofInOut(O)
|
549
|
+
if self.msgfile is None:
|
550
|
+
self.msgfile = self._fakeout.real_stdout
|
551
|
+
self.history = []
|
552
|
+
self.references = []
|
553
|
+
self.setters = defaultdict(dict)
|
554
|
+
self.running_global_digest = hashlib.md5()
|
555
|
+
self.total_walltime_skips = 0
|
556
|
+
self.total_performed_tests = 0
|
557
|
+
self.total_walltime = 0
|
558
|
+
|
559
|
+
def _run(self, test, compileflags, out):
|
560
|
+
"""
|
561
|
+
This function replaces :meth:`doctest.DocTestRunner.__run`.
|
562
|
+
|
563
|
+
It changes the following behavior:
|
564
|
+
|
565
|
+
- We call :meth:`SageDocTestRunner.execute` rather than just
|
566
|
+
exec
|
567
|
+
|
568
|
+
- We don't truncate _fakeout after each example since we want
|
569
|
+
the output file to be readable by the calling
|
570
|
+
:class:`SageWorker`.
|
571
|
+
|
572
|
+
Since it needs to be able to read stdout, it should be called
|
573
|
+
while spoofing using :class:`SageSpoofInOut`.
|
574
|
+
|
575
|
+
EXAMPLES::
|
576
|
+
|
577
|
+
sage: from sage.doctest.parsing import SageOutputChecker
|
578
|
+
sage: from sage.doctest.forker import SageDocTestRunner
|
579
|
+
sage: from sage.doctest.sources import FileDocTestSource
|
580
|
+
sage: from sage.doctest.control import DocTestDefaults; DD = DocTestDefaults()
|
581
|
+
sage: import doctest, sys, os
|
582
|
+
sage: DTR = SageDocTestRunner(SageOutputChecker(), verbose=False, sage_options=DD, optionflags=doctest.NORMALIZE_WHITESPACE|doctest.ELLIPSIS)
|
583
|
+
sage: filename = sage.doctest.forker.__file__
|
584
|
+
sage: FDS = FileDocTestSource(filename, DD)
|
585
|
+
sage: doctests, extras = FDS.create_doctests(globals())
|
586
|
+
sage: DTR.run(doctests[0], clear_globs=False) # indirect doctest
|
587
|
+
TestResults(failed=0, attempted=4)
|
588
|
+
|
589
|
+
TESTS:
|
590
|
+
|
591
|
+
Check that :issue:`26038` is fixed::
|
592
|
+
|
593
|
+
sage: a = 1
|
594
|
+
....: b = 2
|
595
|
+
Traceback (most recent call last):
|
596
|
+
...
|
597
|
+
SyntaxError: multiple statements found while compiling a single statement
|
598
|
+
sage: a = 1
|
599
|
+
....: @syntax error
|
600
|
+
Traceback (most recent call last):
|
601
|
+
...
|
602
|
+
SyntaxError: multiple statements found while compiling a single statement
|
603
|
+
"""
|
604
|
+
# Ensure that injecting globals works as expected in doctests
|
605
|
+
set_globals(test.globs)
|
606
|
+
|
607
|
+
# Keep track of the number of failures and tries.
|
608
|
+
failures = tries = walltime_skips = 0
|
609
|
+
quiet = False
|
610
|
+
|
611
|
+
# Save the option flags (since option directives can be used
|
612
|
+
# to modify them).
|
613
|
+
original_optionflags = self.optionflags
|
614
|
+
|
615
|
+
SUCCESS, FAILURE, BOOM = range(3) # `outcome` state
|
616
|
+
|
617
|
+
check = self._checker.check_output
|
618
|
+
|
619
|
+
# Process each example.
|
620
|
+
for examplenum, example in enumerate(test.examples):
|
621
|
+
if failures:
|
622
|
+
# If exitfirst is set, abort immediately after a
|
623
|
+
# failure.
|
624
|
+
if self.options.exitfirst:
|
625
|
+
break
|
626
|
+
|
627
|
+
# If REPORT_ONLY_FIRST_FAILURE is set, then suppress
|
628
|
+
# reporting after the first failure (but continue
|
629
|
+
# running the tests).
|
630
|
+
quiet |= (self.optionflags & doctest.REPORT_ONLY_FIRST_FAILURE)
|
631
|
+
|
632
|
+
# Merge in the example's options.
|
633
|
+
self.optionflags = original_optionflags
|
634
|
+
if example.options:
|
635
|
+
for (optionflag, val) in example.options.items():
|
636
|
+
if val:
|
637
|
+
self.optionflags |= optionflag
|
638
|
+
else:
|
639
|
+
self.optionflags &= ~optionflag
|
640
|
+
|
641
|
+
# Skip this test if we exceeded our --short budget of walltime for
|
642
|
+
# this doctest
|
643
|
+
if self.options.target_walltime != -1 and self.total_walltime >= self.options.target_walltime:
|
644
|
+
walltime_skips += 1
|
645
|
+
self.optionflags |= doctest.SKIP
|
646
|
+
|
647
|
+
# If 'SKIP' is set, then skip this example.
|
648
|
+
if self.optionflags & doctest.SKIP:
|
649
|
+
continue
|
650
|
+
|
651
|
+
# Record that we started this example.
|
652
|
+
tries += 1
|
653
|
+
|
654
|
+
# We print the example we're running for easier debugging
|
655
|
+
# if this file times out or crashes.
|
656
|
+
with OriginalSource(example):
|
657
|
+
print("sage: " + example.source[:-1] + " ## line %s ##" % (test.lineno + example.lineno + 1))
|
658
|
+
# Update the position so that result comparison works
|
659
|
+
self._fakeout.getvalue()
|
660
|
+
if not quiet:
|
661
|
+
self.report_start(out, test, example)
|
662
|
+
|
663
|
+
# Flush files before running the example, so we know for
|
664
|
+
# sure that everything is reported properly if the test
|
665
|
+
# crashes.
|
666
|
+
sys.stdout.flush()
|
667
|
+
sys.stderr.flush()
|
668
|
+
self.msgfile.flush()
|
669
|
+
|
670
|
+
# Use a special filename for compile(), so we can retrieve
|
671
|
+
# the source code during interactive debugging (see
|
672
|
+
# __patched_linecache_getlines).
|
673
|
+
filename = '<doctest %s[%d]>' % (test.name, examplenum)
|
674
|
+
|
675
|
+
# Run the example in the given context (globs), and record
|
676
|
+
# any exception that gets raised. But for SystemExit, we
|
677
|
+
# simply propagate the exception.
|
678
|
+
exception = None
|
679
|
+
|
680
|
+
def compiler(example):
|
681
|
+
# Compile mode "single" is meant for running a single
|
682
|
+
# statement like on the Python command line. It implies
|
683
|
+
# in particular that the resulting value will be printed.
|
684
|
+
code = compile(example.source, filename, "single",
|
685
|
+
compileflags, 1)
|
686
|
+
|
687
|
+
# Python 2 ignores everything after the first complete
|
688
|
+
# statement in the source code. To verify that we really
|
689
|
+
# have just a single statement and nothing more, we also
|
690
|
+
# compile in "exec" mode and verify that the line
|
691
|
+
# numbers are the same.
|
692
|
+
execcode = compile(example.source, filename, "exec",
|
693
|
+
compileflags, 1)
|
694
|
+
|
695
|
+
# findlinestarts() returns pairs (index, lineno) where
|
696
|
+
# "index" is the index in the bytecode where the line
|
697
|
+
# number changes to "lineno".
|
698
|
+
linenumbers1 = {lineno for (index, lineno)
|
699
|
+
in findlinestarts(code)}
|
700
|
+
linenumbers2 = {lineno for (index, lineno)
|
701
|
+
in findlinestarts(execcode)}
|
702
|
+
if linenumbers1 != linenumbers2:
|
703
|
+
raise SyntaxError("doctest is not a single statement")
|
704
|
+
|
705
|
+
return code
|
706
|
+
|
707
|
+
if not self.options.gc:
|
708
|
+
pass
|
709
|
+
elif self.options.gc > 0:
|
710
|
+
if gc.isenabled():
|
711
|
+
gc.collect()
|
712
|
+
elif self.options.gc < 0:
|
713
|
+
gc.disable()
|
714
|
+
|
715
|
+
from cysignals.signals import SignalError
|
716
|
+
try:
|
717
|
+
# Don't blink! This is where the user's code gets run.
|
718
|
+
self.compile_and_execute(example, compiler, test.globs)
|
719
|
+
except (SignalError, SystemExit):
|
720
|
+
# Tests can be killed by signals in unexpected places.
|
721
|
+
raise
|
722
|
+
except BaseException:
|
723
|
+
exception = sys.exc_info()
|
724
|
+
finally:
|
725
|
+
if self.debugger is not None:
|
726
|
+
self.debugger.set_continue() # ==== Example Finished ====
|
727
|
+
check_timer = Timer().start()
|
728
|
+
got = self._fakeout.getvalue()
|
729
|
+
|
730
|
+
outcome = FAILURE # guilty until proved innocent or insane
|
731
|
+
|
732
|
+
probed_tags = getattr(example, 'probed_tags', False)
|
733
|
+
|
734
|
+
# If the example executed without raising any exceptions,
|
735
|
+
# verify its output.
|
736
|
+
if exception is None:
|
737
|
+
if check(example.want, got, self.optionflags):
|
738
|
+
if probed_tags and probed_tags is not True:
|
739
|
+
example.warnings.append(
|
740
|
+
f"The tag '{unparse_optional_tags(probed_tags)}' "
|
741
|
+
f"may no longer be needed; these features are not present, "
|
742
|
+
f"but we ran the doctest anyway as requested by --probe, "
|
743
|
+
f"and it succeeded.")
|
744
|
+
outcome = SUCCESS
|
745
|
+
|
746
|
+
# The example raised an exception: check if it was expected.
|
747
|
+
else:
|
748
|
+
exc_msg = traceback.format_exception_only(*exception[:2])[-1]
|
749
|
+
|
750
|
+
if example.exc_msg is not None:
|
751
|
+
# On Python 3 the exception repr often includes the
|
752
|
+
# exception's full module name (for non-builtin
|
753
|
+
# exceptions), whereas on Python 2 does not, so we
|
754
|
+
# normalize Python 3 exceptions to match tests written to
|
755
|
+
# Python 2
|
756
|
+
# See https://github.com/sagemath/sage/issues/24271
|
757
|
+
exc_cls = exception[0]
|
758
|
+
exc_name = exc_cls.__name__
|
759
|
+
if exc_cls.__module__:
|
760
|
+
exc_fullname = (exc_cls.__module__ + '.' +
|
761
|
+
exc_cls.__qualname__)
|
762
|
+
else:
|
763
|
+
exc_fullname = exc_cls.__qualname__
|
764
|
+
|
765
|
+
if (example.exc_msg.startswith(exc_name) and
|
766
|
+
exc_msg.startswith(exc_fullname)):
|
767
|
+
exc_msg = exc_msg.replace(exc_fullname, exc_name, 1)
|
768
|
+
|
769
|
+
if not quiet:
|
770
|
+
got += doctest._exception_traceback(exception)
|
771
|
+
|
772
|
+
# If `example.exc_msg` is None, then we weren't expecting
|
773
|
+
# an exception.
|
774
|
+
if example.exc_msg is None:
|
775
|
+
outcome = BOOM
|
776
|
+
|
777
|
+
# We expected an exception: see whether it matches.
|
778
|
+
elif check(example.exc_msg, exc_msg, self.optionflags):
|
779
|
+
if probed_tags and probed_tags is not True:
|
780
|
+
example.warnings.append(
|
781
|
+
f"The tag '{unparse_optional_tags(example.probed_tags)}' "
|
782
|
+
f"may no longer be needed; these features are not present, "
|
783
|
+
f"but we ran the doctest anyway as requested by --probe, "
|
784
|
+
f"and it succeeded (raised the expected exception).")
|
785
|
+
outcome = SUCCESS
|
786
|
+
|
787
|
+
# Another chance if they didn't care about the detail.
|
788
|
+
elif self.optionflags & doctest.IGNORE_EXCEPTION_DETAIL:
|
789
|
+
m1 = re.match(r'(?:[^:]*\.)?([^:]*:)', example.exc_msg)
|
790
|
+
m2 = re.match(r'(?:[^:]*\.)?([^:]*:)', exc_msg)
|
791
|
+
if m1 and m2 and check(m1.group(1), m2.group(1),
|
792
|
+
self.optionflags):
|
793
|
+
if probed_tags and probed_tags is not True:
|
794
|
+
example.warnings.append(
|
795
|
+
f"The tag '{unparse_optional_tags(example.probed_tags)}' "
|
796
|
+
f"may no longer be needed; these features are not present, "
|
797
|
+
f"but we ran the doctest anyway as requested by --probe, "
|
798
|
+
f"and it succeeded (raised an exception as expected).")
|
799
|
+
outcome = SUCCESS
|
800
|
+
|
801
|
+
check_timer.stop()
|
802
|
+
self.total_walltime += example.walltime + check_timer.walltime
|
803
|
+
|
804
|
+
# Report the outcome.
|
805
|
+
if example.warnings:
|
806
|
+
for warning in example.warnings:
|
807
|
+
out(self._failure_header(test, example, f'Warning: {warning}'))
|
808
|
+
if outcome is SUCCESS:
|
809
|
+
if self.options.warn_long > 0 and example.cputime + check_timer.cputime > self.options.warn_long:
|
810
|
+
self.report_overtime(out, test, example, got,
|
811
|
+
check_timer=check_timer)
|
812
|
+
elif example.warnings:
|
813
|
+
pass
|
814
|
+
elif not quiet:
|
815
|
+
self.report_success(out, test, example, got,
|
816
|
+
check_timer=check_timer)
|
817
|
+
elif probed_tags:
|
818
|
+
pass
|
819
|
+
elif outcome is FAILURE:
|
820
|
+
if not quiet:
|
821
|
+
self.report_failure(out, test, example, got, test.globs)
|
822
|
+
failures += 1
|
823
|
+
elif outcome is BOOM:
|
824
|
+
if not quiet:
|
825
|
+
self.report_unexpected_exception(out, test, example,
|
826
|
+
exception)
|
827
|
+
failures += 1
|
828
|
+
else:
|
829
|
+
assert False, ("unknown outcome", outcome)
|
830
|
+
|
831
|
+
# Restore the option flags (in case they were modified)
|
832
|
+
self.optionflags = original_optionflags
|
833
|
+
|
834
|
+
# Record and return the number of failures and tries.
|
835
|
+
self._DocTestRunner__record_outcome(test, failures, tries)
|
836
|
+
self.total_walltime_skips += walltime_skips
|
837
|
+
self.total_performed_tests += tries
|
838
|
+
return TestResults(failures, tries)
|
839
|
+
|
840
|
+
def run(self, test, compileflags=0, out=None, clear_globs=True):
|
841
|
+
"""
|
842
|
+
Run the examples in a given doctest.
|
843
|
+
|
844
|
+
This function replaces :class:`doctest.DocTestRunner.run`
|
845
|
+
since it needs to handle spoofing. It also leaves the display
|
846
|
+
hook in place.
|
847
|
+
|
848
|
+
INPUT:
|
849
|
+
|
850
|
+
- ``test`` -- an instance of :class:`doctest.DocTest`
|
851
|
+
|
852
|
+
- ``compileflags`` -- integer (default: 0) the set of compiler flags
|
853
|
+
used to execute examples (passed in to the :func:`compile`)
|
854
|
+
|
855
|
+
- ``out`` -- a function for writing the output (defaults to
|
856
|
+
:func:`sys.stdout.write`)
|
857
|
+
|
858
|
+
- ``clear_globs`` -- boolean (default: ``True``); whether to clear
|
859
|
+
the namespace after running this doctest
|
860
|
+
|
861
|
+
OUTPUT:
|
862
|
+
|
863
|
+
- ``f`` -- integer, the number of examples that failed
|
864
|
+
|
865
|
+
- ``t`` -- the number of examples tried
|
866
|
+
|
867
|
+
EXAMPLES::
|
868
|
+
|
869
|
+
sage: from sage.doctest.parsing import SageOutputChecker
|
870
|
+
sage: from sage.doctest.forker import SageDocTestRunner
|
871
|
+
sage: from sage.doctest.sources import FileDocTestSource
|
872
|
+
sage: from sage.doctest.control import DocTestDefaults; DD = DocTestDefaults()
|
873
|
+
sage: import doctest, sys, os
|
874
|
+
sage: DTR = SageDocTestRunner(SageOutputChecker(), verbose=False, sage_options=DD,
|
875
|
+
....: optionflags=doctest.NORMALIZE_WHITESPACE|doctest.ELLIPSIS)
|
876
|
+
sage: filename = sage.doctest.forker.__file__
|
877
|
+
sage: FDS = FileDocTestSource(filename, DD)
|
878
|
+
sage: doctests, extras = FDS.create_doctests(globals())
|
879
|
+
sage: DTR.run(doctests[0], clear_globs=False)
|
880
|
+
TestResults(failed=0, attempted=4)
|
881
|
+
"""
|
882
|
+
self.setters = defaultdict(dict)
|
883
|
+
randstate.set_random_seed(self.options.random_seed)
|
884
|
+
warnings.showwarning = showwarning_with_traceback
|
885
|
+
self.running_doctest_digest = hashlib.md5()
|
886
|
+
self.test = test
|
887
|
+
# We use this slightly modified version of Pdb because it
|
888
|
+
# interacts better with the doctesting framework (like allowing
|
889
|
+
# doctests for sys.settrace()). Since we already have output
|
890
|
+
# spoofing in place, there is no need for redirection.
|
891
|
+
if self.options.debug:
|
892
|
+
self.debugger = doctest._OutputRedirectingPdb(sys.stdout)
|
893
|
+
self.debugger.reset()
|
894
|
+
else:
|
895
|
+
self.debugger = None
|
896
|
+
self.save_linecache_getlines = linecache.getlines
|
897
|
+
linecache.getlines = self._DocTestRunner__patched_linecache_getlines
|
898
|
+
if out is None:
|
899
|
+
def out(s):
|
900
|
+
self.msgfile.write(s)
|
901
|
+
self.msgfile.flush()
|
902
|
+
|
903
|
+
self._fakeout.start_spoofing()
|
904
|
+
# If self.options.initial is set, we show only the first failure in each doctest block.
|
905
|
+
self.no_failure_yet = True
|
906
|
+
try:
|
907
|
+
return self._run(test, compileflags, out)
|
908
|
+
finally:
|
909
|
+
self._fakeout.stop_spoofing()
|
910
|
+
linecache.getlines = self.save_linecache_getlines
|
911
|
+
if clear_globs:
|
912
|
+
test.globs.clear()
|
913
|
+
|
914
|
+
def summarize(self, verbose=None):
|
915
|
+
"""
|
916
|
+
Print results of testing to ``self.msgfile`` and return number
|
917
|
+
of failures and tests run.
|
918
|
+
|
919
|
+
INPUT:
|
920
|
+
|
921
|
+
- ``verbose`` -- whether to print lots of stuff
|
922
|
+
|
923
|
+
OUTPUT:
|
924
|
+
|
925
|
+
- returns ``(f, t)``, a :class:`doctest.TestResults` instance
|
926
|
+
giving the number of failures and the total number of tests
|
927
|
+
run.
|
928
|
+
|
929
|
+
EXAMPLES::
|
930
|
+
|
931
|
+
sage: from sage.doctest.parsing import SageOutputChecker
|
932
|
+
sage: from sage.doctest.forker import SageDocTestRunner
|
933
|
+
sage: from sage.doctest.control import DocTestDefaults; DD = DocTestDefaults()
|
934
|
+
sage: import doctest, sys, os
|
935
|
+
sage: DTR = SageDocTestRunner(SageOutputChecker(), verbose=False, sage_options=DD, optionflags=doctest.NORMALIZE_WHITESPACE|doctest.ELLIPSIS)
|
936
|
+
sage: DTR._name2ft['sage.doctest.forker'] = (1,120)
|
937
|
+
sage: results = DTR.summarize()
|
938
|
+
**********************************************************************
|
939
|
+
1 item had failures:
|
940
|
+
1 of 120 in sage.doctest.forker
|
941
|
+
sage: results
|
942
|
+
TestResults(failed=1, attempted=120)
|
943
|
+
"""
|
944
|
+
if verbose is None:
|
945
|
+
verbose = self._verbose
|
946
|
+
m = self.msgfile
|
947
|
+
notests = []
|
948
|
+
passed = []
|
949
|
+
failed = []
|
950
|
+
totalt = totalf = 0
|
951
|
+
for x in self._name2ft.items():
|
952
|
+
name, (f, t) = x
|
953
|
+
assert f <= t
|
954
|
+
totalt += t
|
955
|
+
totalf += f
|
956
|
+
if not t:
|
957
|
+
notests.append(name)
|
958
|
+
elif not f:
|
959
|
+
passed.append((name, t))
|
960
|
+
else:
|
961
|
+
failed.append(x)
|
962
|
+
if verbose:
|
963
|
+
if notests:
|
964
|
+
print(count_noun(len(notests), "item"), "had no tests:", file=m)
|
965
|
+
notests.sort()
|
966
|
+
for thing in notests:
|
967
|
+
print(" %s" % thing, file=m)
|
968
|
+
if passed:
|
969
|
+
print(count_noun(len(passed), "item"), "passed all tests:", file=m)
|
970
|
+
passed.sort()
|
971
|
+
for thing, count in passed:
|
972
|
+
print(" %s in %s" % (count_noun(count, "test", pad_number=3, pad_noun=True), thing), file=m)
|
973
|
+
if failed:
|
974
|
+
print(self.DIVIDER, file=m)
|
975
|
+
print(count_noun(len(failed), "item"), "had failures:", file=m)
|
976
|
+
failed.sort()
|
977
|
+
for thing, (f, t) in failed:
|
978
|
+
print(" %3d of %3d in %s" % (f, t, thing), file=m)
|
979
|
+
if verbose:
|
980
|
+
print(count_noun(totalt, "test") + " in " + count_noun(len(self._name2ft), "item") + ".", file=m)
|
981
|
+
print("%s passed and %s failed." % (totalt - totalf, totalf), file=m)
|
982
|
+
if totalf:
|
983
|
+
print("***Test Failed***", file=m)
|
984
|
+
else:
|
985
|
+
print("Test passed.", file=m)
|
986
|
+
m.flush()
|
987
|
+
return doctest.TestResults(totalf, totalt)
|
988
|
+
|
989
|
+
def update_digests(self, example):
|
990
|
+
"""
|
991
|
+
Update global and doctest digests.
|
992
|
+
|
993
|
+
Sage's doctest runner tracks the state of doctests so that
|
994
|
+
their dependencies are known. For example, in the following
|
995
|
+
two lines ::
|
996
|
+
|
997
|
+
sage: R.<x> = ZZ[]
|
998
|
+
sage: f = x^2 + 1
|
999
|
+
|
1000
|
+
it records that the second line depends on the first since the
|
1001
|
+
first INSERTS ``x`` into the global namespace and the second
|
1002
|
+
line RETRIEVES ``x`` from the global namespace.
|
1003
|
+
|
1004
|
+
This function updates the hashes that record these
|
1005
|
+
dependencies.
|
1006
|
+
|
1007
|
+
INPUT:
|
1008
|
+
|
1009
|
+
- ``example`` -- a :class:`doctest.Example` instance
|
1010
|
+
|
1011
|
+
EXAMPLES::
|
1012
|
+
|
1013
|
+
sage: from sage.doctest.parsing import SageOutputChecker
|
1014
|
+
sage: from sage.doctest.forker import SageDocTestRunner
|
1015
|
+
sage: from sage.doctest.sources import FileDocTestSource
|
1016
|
+
sage: from sage.doctest.control import DocTestDefaults; DD = DocTestDefaults()
|
1017
|
+
sage: import doctest, sys, os, hashlib
|
1018
|
+
sage: DTR = SageDocTestRunner(SageOutputChecker(), verbose=False, sage_options=DD, optionflags=doctest.NORMALIZE_WHITESPACE|doctest.ELLIPSIS)
|
1019
|
+
sage: filename = sage.doctest.forker.__file__
|
1020
|
+
sage: FDS = FileDocTestSource(filename, DD)
|
1021
|
+
sage: doctests, extras = FDS.create_doctests(globals())
|
1022
|
+
sage: DTR.running_global_digest.hexdigest()
|
1023
|
+
'd41d8cd98f00b204e9800998ecf8427e'
|
1024
|
+
sage: DTR.running_doctest_digest = hashlib.md5()
|
1025
|
+
sage: ex = doctests[0].examples[0]; ex.predecessors = None
|
1026
|
+
sage: DTR.update_digests(ex)
|
1027
|
+
sage: DTR.running_global_digest.hexdigest()
|
1028
|
+
'3cb44104292c3a3ab4da3112ce5dc35c'
|
1029
|
+
"""
|
1030
|
+
s = str_to_bytes(pre_hash(get_source(example)), 'utf-8')
|
1031
|
+
self.running_global_digest.update(s)
|
1032
|
+
self.running_doctest_digest.update(s)
|
1033
|
+
if example.predecessors is not None:
|
1034
|
+
digest = hashlib.md5(s)
|
1035
|
+
gen = (e.running_state for e in example.predecessors)
|
1036
|
+
digest.update(str_to_bytes(reduce_hex(gen), 'ascii'))
|
1037
|
+
example.running_state = digest.hexdigest()
|
1038
|
+
|
1039
|
+
def compile_and_execute(self, example, compiler, globs):
|
1040
|
+
"""
|
1041
|
+
Run the given example, recording dependencies.
|
1042
|
+
|
1043
|
+
Rather than using a basic dictionary, Sage's doctest runner
|
1044
|
+
uses a :class:`sage.doctest.util.RecordingDict`, which records
|
1045
|
+
every time a value is set or retrieved. Executing the given
|
1046
|
+
code with this recording dictionary as the namespace allows
|
1047
|
+
Sage to track dependencies between doctest lines. For
|
1048
|
+
example, in the following two lines ::
|
1049
|
+
|
1050
|
+
sage: R.<x> = ZZ[]
|
1051
|
+
sage: f = x^2 + 1
|
1052
|
+
|
1053
|
+
the recording dictionary records that the second line depends
|
1054
|
+
on the first since the first INSERTS ``x`` into the global
|
1055
|
+
namespace and the second line RETRIEVES ``x`` from the global
|
1056
|
+
namespace.
|
1057
|
+
|
1058
|
+
INPUT:
|
1059
|
+
|
1060
|
+
- ``example`` -- a :class:`doctest.Example` instance
|
1061
|
+
|
1062
|
+
- ``compiler`` -- a callable that, applied to example,
|
1063
|
+
produces a code object
|
1064
|
+
|
1065
|
+
- ``globs`` -- dictionary in which to execute the code
|
1066
|
+
|
1067
|
+
OUTPUT: the output of the compiled code snippet
|
1068
|
+
|
1069
|
+
EXAMPLES::
|
1070
|
+
|
1071
|
+
sage: from sage.doctest.parsing import SageOutputChecker
|
1072
|
+
sage: from sage.doctest.forker import SageDocTestRunner
|
1073
|
+
sage: from sage.doctest.sources import FileDocTestSource
|
1074
|
+
sage: from sage.doctest.util import RecordingDict
|
1075
|
+
sage: from sage.doctest.control import DocTestDefaults; DD = DocTestDefaults()
|
1076
|
+
sage: import doctest, sys, os, hashlib
|
1077
|
+
sage: DTR = SageDocTestRunner(SageOutputChecker(), verbose=False, sage_options=DD,
|
1078
|
+
....: optionflags=doctest.NORMALIZE_WHITESPACE|doctest.ELLIPSIS)
|
1079
|
+
sage: DTR.running_doctest_digest = hashlib.md5()
|
1080
|
+
sage: filename = sage.doctest.forker.__file__
|
1081
|
+
sage: FDS = FileDocTestSource(filename, DD)
|
1082
|
+
sage: globs = RecordingDict(globals())
|
1083
|
+
sage: 'doctest_var' in globs
|
1084
|
+
False
|
1085
|
+
sage: doctests, extras = FDS.create_doctests(globs)
|
1086
|
+
sage: ex0 = doctests[0].examples[0]
|
1087
|
+
sage: flags = 32768 if sys.version_info.minor < 8 else 524288
|
1088
|
+
sage: def compiler(ex):
|
1089
|
+
....: return compile(ex.source, '<doctest sage.doctest.forker[0]>',
|
1090
|
+
....: 'single', flags, 1)
|
1091
|
+
sage: DTR.compile_and_execute(ex0, compiler, globs)
|
1092
|
+
1764
|
1093
|
+
sage: globs['doctest_var']
|
1094
|
+
42
|
1095
|
+
sage: globs.set
|
1096
|
+
{'doctest_var'}
|
1097
|
+
sage: globs.got
|
1098
|
+
{'Integer'}
|
1099
|
+
|
1100
|
+
Now we can execute some more doctests to see the dependencies. ::
|
1101
|
+
|
1102
|
+
sage: ex1 = doctests[0].examples[1]
|
1103
|
+
sage: def compiler(ex):
|
1104
|
+
....: return compile(ex.source, '<doctest sage.doctest.forker[1]>',
|
1105
|
+
....: 'single', flags, 1)
|
1106
|
+
sage: DTR.compile_and_execute(ex1, compiler, globs)
|
1107
|
+
sage: sorted(list(globs.set))
|
1108
|
+
['R', 'a']
|
1109
|
+
sage: globs.got
|
1110
|
+
{'ZZ'}
|
1111
|
+
sage: ex1.predecessors
|
1112
|
+
[]
|
1113
|
+
|
1114
|
+
::
|
1115
|
+
|
1116
|
+
sage: ex2 = doctests[0].examples[2]
|
1117
|
+
sage: def compiler(ex):
|
1118
|
+
....: return compile(ex.source, '<doctest sage.doctest.forker[2]>',
|
1119
|
+
....: 'single', flags, 1)
|
1120
|
+
sage: DTR.compile_and_execute(ex2, compiler, globs)
|
1121
|
+
a + 42
|
1122
|
+
sage: list(globs.set)
|
1123
|
+
[]
|
1124
|
+
sage: sorted(list(globs.got))
|
1125
|
+
['a', 'doctest_var']
|
1126
|
+
sage: set(ex2.predecessors) == set([ex0,ex1])
|
1127
|
+
True
|
1128
|
+
"""
|
1129
|
+
if isinstance(globs, RecordingDict):
|
1130
|
+
globs.start()
|
1131
|
+
example.sequence_number = len(self.history)
|
1132
|
+
if not hasattr(example, 'warnings'):
|
1133
|
+
example.warnings = []
|
1134
|
+
self.history.append(example)
|
1135
|
+
timer = Timer().start()
|
1136
|
+
try:
|
1137
|
+
compiled = compiler(example)
|
1138
|
+
timer.start() # reset timer
|
1139
|
+
exec(compiled, globs)
|
1140
|
+
finally:
|
1141
|
+
timer.stop().annotate(example)
|
1142
|
+
if isinstance(globs, RecordingDict):
|
1143
|
+
example.predecessors = []
|
1144
|
+
for name in globs.got:
|
1145
|
+
setters_dict = self.setters.get(name) # setter_optional_tags -> setter
|
1146
|
+
if setters_dict:
|
1147
|
+
was_set = False
|
1148
|
+
for setter_optional_tags, setter in setters_dict.items():
|
1149
|
+
if setter_optional_tags.issubset(example.optional_tags): # was set in a less constrained doctest
|
1150
|
+
was_set = True
|
1151
|
+
example.predecessors.append(setter)
|
1152
|
+
if not was_set:
|
1153
|
+
if example.probed_tags:
|
1154
|
+
# Probing confusion.
|
1155
|
+
# Do not issue the "was set only in doctest marked" warning;
|
1156
|
+
# and also do not issue the "may no longer be needed" notice
|
1157
|
+
example.probed_tags = True
|
1158
|
+
else:
|
1159
|
+
f_setter_optional_tags = "; ".join("'"
|
1160
|
+
+ unparse_optional_tags(setter_optional_tags)
|
1161
|
+
+ "'"
|
1162
|
+
for setter_optional_tags in setters_dict)
|
1163
|
+
example.warnings.append(f"Variable '{name}' referenced here "
|
1164
|
+
f"was set only in doctest marked {f_setter_optional_tags}")
|
1165
|
+
for name in globs.set:
|
1166
|
+
self.setters[name][example.optional_tags] = example
|
1167
|
+
else:
|
1168
|
+
example.predecessors = None
|
1169
|
+
self.update_digests(example)
|
1170
|
+
example.total_state = self.running_global_digest.hexdigest()
|
1171
|
+
example.doctest_state = self.running_doctest_digest.hexdigest()
|
1172
|
+
|
1173
|
+
def _failure_header(self, test, example, message='Failed example:'):
|
1174
|
+
"""
|
1175
|
+
We strip out ``sage:`` prompts, so we override
|
1176
|
+
:meth:`doctest.DocTestRunner._failure_header` for better
|
1177
|
+
reporting.
|
1178
|
+
|
1179
|
+
INPUT:
|
1180
|
+
|
1181
|
+
- ``test`` -- a :class:`doctest.DocTest` instance
|
1182
|
+
|
1183
|
+
- ``example`` -- a :class:`doctest.Example` instance in ``test``
|
1184
|
+
|
1185
|
+
OUTPUT: string used for reporting that the given example failed
|
1186
|
+
|
1187
|
+
EXAMPLES::
|
1188
|
+
|
1189
|
+
sage: from sage.doctest.parsing import SageOutputChecker
|
1190
|
+
sage: from sage.doctest.forker import SageDocTestRunner
|
1191
|
+
sage: from sage.doctest.sources import FileDocTestSource
|
1192
|
+
sage: from sage.doctest.control import DocTestDefaults; DD = DocTestDefaults()
|
1193
|
+
sage: import doctest, sys, os
|
1194
|
+
sage: DTR = SageDocTestRunner(SageOutputChecker(), verbose=False, sage_options=DD, optionflags=doctest.NORMALIZE_WHITESPACE|doctest.ELLIPSIS)
|
1195
|
+
sage: filename = sage.doctest.forker.__file__
|
1196
|
+
sage: FDS = FileDocTestSource(filename, DD)
|
1197
|
+
sage: doctests, extras = FDS.create_doctests(globals())
|
1198
|
+
sage: ex = doctests[0].examples[0]
|
1199
|
+
sage: print(DTR._failure_header(doctests[0], ex))
|
1200
|
+
**********************************************************************
|
1201
|
+
File ".../sage/doctest/forker.py", line 12, in sage.doctest.forker
|
1202
|
+
Failed example:
|
1203
|
+
doctest_var = 42; doctest_var^2
|
1204
|
+
<BLANKLINE>
|
1205
|
+
|
1206
|
+
Without the source swapping::
|
1207
|
+
|
1208
|
+
sage: import doctest
|
1209
|
+
sage: print(doctest.DocTestRunner._failure_header(DTR, doctests[0], ex))
|
1210
|
+
**********************************************************************
|
1211
|
+
File ".../sage/doctest/forker.py", line 12, in sage.doctest.forker
|
1212
|
+
Failed example:
|
1213
|
+
doctest_var = Integer(42); doctest_var**Integer(2)
|
1214
|
+
<BLANKLINE>
|
1215
|
+
|
1216
|
+
The ``'Failed example:'`` message can be customized::
|
1217
|
+
|
1218
|
+
sage: print(DTR._failure_header(doctests[0], ex, message='Hello there!'))
|
1219
|
+
**********************************************************************
|
1220
|
+
File ".../sage/doctest/forker.py", line 12, in sage.doctest.forker
|
1221
|
+
Hello there!
|
1222
|
+
doctest_var = 42; doctest_var^2
|
1223
|
+
<BLANKLINE>
|
1224
|
+
"""
|
1225
|
+
out = [self.DIVIDER]
|
1226
|
+
with OriginalSource(example):
|
1227
|
+
if self.options.format == 'sage':
|
1228
|
+
if test.filename:
|
1229
|
+
if test.lineno is not None and example.lineno is not None:
|
1230
|
+
lineno = test.lineno + example.lineno + 1
|
1231
|
+
else:
|
1232
|
+
lineno = '?'
|
1233
|
+
out.append('File "%s", line %s, in %s' %
|
1234
|
+
(test.filename, lineno, test.name))
|
1235
|
+
else:
|
1236
|
+
out.append('Line %s, in %s' % (example.lineno + 1, test.name))
|
1237
|
+
out.append(message)
|
1238
|
+
elif self.options.format == 'github':
|
1239
|
+
# https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#using-workflow-commands-to-access-toolkit-functions
|
1240
|
+
if message.startswith('Warning: '):
|
1241
|
+
command = f'::warning title={message}'
|
1242
|
+
message = message[len('Warning: '):]
|
1243
|
+
elif self.baseline.get('failed', False):
|
1244
|
+
command = f'::notice title={message}'
|
1245
|
+
message += ' [failed in baseline]'
|
1246
|
+
else:
|
1247
|
+
command = f'::error title={message}'
|
1248
|
+
if extra := getattr(example, 'extra', None):
|
1249
|
+
message += f': {extra}'
|
1250
|
+
if test.filename:
|
1251
|
+
command += f',file={test.filename}'
|
1252
|
+
if test.lineno is not None and example.lineno is not None:
|
1253
|
+
lineno = test.lineno + example.lineno + 1
|
1254
|
+
command += f',line={lineno}'
|
1255
|
+
lineno = None
|
1256
|
+
else:
|
1257
|
+
command += f',line={example.lineno + 1}'
|
1258
|
+
#
|
1259
|
+
# Urlencoding trick for multi-line annotations
|
1260
|
+
# https://github.com/actions/starter-workflows/issues/68#issuecomment-581479448
|
1261
|
+
#
|
1262
|
+
# This only affects the display in the workflow Summary, after clicking "Show more";
|
1263
|
+
# the message needs to be long enough so that "Show more" becomes available.
|
1264
|
+
# https://github.com/actions/toolkit/issues/193#issuecomment-1867084340
|
1265
|
+
#
|
1266
|
+
# Unfortunately, this trick does not make the annotations in the diff view multi-line.
|
1267
|
+
#
|
1268
|
+
if '\n' in message:
|
1269
|
+
message = message.replace('\n', '%0A')
|
1270
|
+
# The actual threshold for "Show more" to appear depends on the window size.
|
1271
|
+
show_more_threshold = 500
|
1272
|
+
if (pad := show_more_threshold - len(message)) > 0:
|
1273
|
+
message += ' ' * pad
|
1274
|
+
command += f'::{message}'
|
1275
|
+
out.append(command)
|
1276
|
+
else:
|
1277
|
+
raise ValueError(f'unknown format option: {self.options.format}')
|
1278
|
+
source = example.source
|
1279
|
+
out.append(doctest._indent(source))
|
1280
|
+
return '\n'.join(out)
|
1281
|
+
|
1282
|
+
def report_start(self, out, test, example):
|
1283
|
+
"""
|
1284
|
+
Called when an example starts.
|
1285
|
+
|
1286
|
+
INPUT:
|
1287
|
+
|
1288
|
+
- ``out`` -- a function for printing
|
1289
|
+
|
1290
|
+
- ``test`` -- a :class:`doctest.DocTest` instance
|
1291
|
+
|
1292
|
+
- ``example`` -- a :class:`doctest.Example` instance in ``test``
|
1293
|
+
|
1294
|
+
OUTPUT: prints a report to ``out``
|
1295
|
+
|
1296
|
+
EXAMPLES::
|
1297
|
+
|
1298
|
+
sage: from sage.doctest.parsing import SageOutputChecker
|
1299
|
+
sage: from sage.doctest.forker import SageDocTestRunner
|
1300
|
+
sage: from sage.doctest.sources import FileDocTestSource
|
1301
|
+
sage: from sage.doctest.control import DocTestDefaults; DD = DocTestDefaults()
|
1302
|
+
sage: import doctest, sys, os
|
1303
|
+
sage: DTR = SageDocTestRunner(SageOutputChecker(), verbose=True, sage_options=DD, optionflags=doctest.NORMALIZE_WHITESPACE|doctest.ELLIPSIS)
|
1304
|
+
sage: filename = sage.doctest.forker.__file__
|
1305
|
+
sage: FDS = FileDocTestSource(filename, DD)
|
1306
|
+
sage: doctests, extras = FDS.create_doctests(globals())
|
1307
|
+
sage: ex = doctests[0].examples[0]
|
1308
|
+
sage: DTR.report_start(sys.stdout.write, doctests[0], ex)
|
1309
|
+
Trying (line 12): doctest_var = 42; doctest_var^2
|
1310
|
+
Expecting:
|
1311
|
+
1764
|
1312
|
+
"""
|
1313
|
+
# We completely replace doctest.DocTestRunner.report_start so that we can include line numbers
|
1314
|
+
with OriginalSource(example):
|
1315
|
+
if self._verbose:
|
1316
|
+
start_txt = ('Trying (line %s):' % (test.lineno + example.lineno + 1)
|
1317
|
+
+ doctest._indent(example.source))
|
1318
|
+
if example.want:
|
1319
|
+
start_txt += 'Expecting:\n' + doctest._indent(example.want)
|
1320
|
+
else:
|
1321
|
+
start_txt += 'Expecting nothing\n'
|
1322
|
+
out(start_txt)
|
1323
|
+
|
1324
|
+
def report_success(self, out, test, example, got, *, check_timer=None):
|
1325
|
+
"""
|
1326
|
+
Called when an example succeeds.
|
1327
|
+
|
1328
|
+
INPUT:
|
1329
|
+
|
1330
|
+
- ``out`` -- a function for printing
|
1331
|
+
|
1332
|
+
- ``test`` -- a :class:`doctest.DocTest` instance
|
1333
|
+
|
1334
|
+
- ``example`` -- a :class:`doctest.Example` instance in ``test``
|
1335
|
+
|
1336
|
+
- ``got`` -- string; the result of running ``example``
|
1337
|
+
|
1338
|
+
- ``check_timer`` -- a :class:`sage.doctest.util.Timer` (default:
|
1339
|
+
``None``) that measures the time spent checking whether or not
|
1340
|
+
the output was correct
|
1341
|
+
|
1342
|
+
OUTPUT: prints a report to ``out``; if in debugging mode, starts an
|
1343
|
+
IPython prompt at the point of the failure
|
1344
|
+
|
1345
|
+
EXAMPLES::
|
1346
|
+
|
1347
|
+
sage: from sage.doctest.parsing import SageOutputChecker
|
1348
|
+
sage: from sage.doctest.forker import SageDocTestRunner
|
1349
|
+
sage: from sage.doctest.sources import FileDocTestSource
|
1350
|
+
sage: from sage.doctest.control import DocTestDefaults; DD = DocTestDefaults()
|
1351
|
+
sage: from sage.doctest.util import Timer
|
1352
|
+
sage: import doctest, sys, os
|
1353
|
+
sage: DTR = SageDocTestRunner(SageOutputChecker(), verbose=True, sage_options=DD, optionflags=doctest.NORMALIZE_WHITESPACE|doctest.ELLIPSIS)
|
1354
|
+
sage: filename = sage.doctest.forker.__file__
|
1355
|
+
sage: FDS = FileDocTestSource(filename, DD)
|
1356
|
+
sage: doctests, extras = FDS.create_doctests(globals())
|
1357
|
+
sage: ex = doctests[0].examples[0]
|
1358
|
+
sage: ex.cputime = 1.01
|
1359
|
+
sage: ex.walltime = 1.12
|
1360
|
+
sage: check = Timer()
|
1361
|
+
sage: check.cputime = 2.14
|
1362
|
+
sage: check.walltime = 2.71
|
1363
|
+
sage: DTR.report_success(sys.stdout.write, doctests[0], ex, '1764',
|
1364
|
+
....: check_timer=check)
|
1365
|
+
ok [3.83s wall]
|
1366
|
+
"""
|
1367
|
+
# We completely replace doctest.DocTestRunner.report_success
|
1368
|
+
# so that we can include time taken for the test
|
1369
|
+
if self._verbose:
|
1370
|
+
out("ok [%.2fs wall]\n" %
|
1371
|
+
(example.walltime + check_timer.walltime))
|
1372
|
+
|
1373
|
+
def report_failure(self, out, test, example, got, globs):
|
1374
|
+
r"""
|
1375
|
+
Called when a doctest fails.
|
1376
|
+
|
1377
|
+
INPUT:
|
1378
|
+
|
1379
|
+
- ``out`` -- a function for printing
|
1380
|
+
|
1381
|
+
- ``test`` -- a :class:`doctest.DocTest` instance
|
1382
|
+
|
1383
|
+
- ``example`` -- a :class:`doctest.Example` instance in ``test``
|
1384
|
+
|
1385
|
+
- ``got`` -- string, the result of running ``example``
|
1386
|
+
|
1387
|
+
- ``globs`` -- dictionary of globals, used if in debugging mode
|
1388
|
+
|
1389
|
+
OUTPUT: prints a report to ``out``
|
1390
|
+
|
1391
|
+
EXAMPLES::
|
1392
|
+
|
1393
|
+
sage: from sage.doctest.parsing import SageOutputChecker
|
1394
|
+
sage: from sage.doctest.forker import SageDocTestRunner
|
1395
|
+
sage: from sage.doctest.sources import FileDocTestSource
|
1396
|
+
sage: from sage.doctest.control import DocTestDefaults; DD = DocTestDefaults()
|
1397
|
+
sage: import doctest, sys, os
|
1398
|
+
sage: DTR = SageDocTestRunner(SageOutputChecker(), verbose=True, sage_options=DD, optionflags=doctest.NORMALIZE_WHITESPACE|doctest.ELLIPSIS)
|
1399
|
+
sage: filename = sage.doctest.forker.__file__
|
1400
|
+
sage: FDS = FileDocTestSource(filename, DD)
|
1401
|
+
sage: doctests, extras = FDS.create_doctests(globals())
|
1402
|
+
sage: ex = doctests[0].examples[0]
|
1403
|
+
sage: DTR.no_failure_yet = True
|
1404
|
+
sage: DTR.report_failure(sys.stdout.write, doctests[0], ex, 'BAD ANSWER\n', {})
|
1405
|
+
**********************************************************************
|
1406
|
+
File ".../sage/doctest/forker.py", line 12, in sage.doctest.forker
|
1407
|
+
Failed example:
|
1408
|
+
doctest_var = 42; doctest_var^2
|
1409
|
+
Expected:
|
1410
|
+
1764
|
1411
|
+
Got:
|
1412
|
+
BAD ANSWER
|
1413
|
+
|
1414
|
+
If debugging is turned on this function starts an IPython
|
1415
|
+
prompt when a test returns an incorrect answer::
|
1416
|
+
|
1417
|
+
sage: # needs sage.symbolic (actually sage.all)
|
1418
|
+
sage: sage0.quit()
|
1419
|
+
sage: _ = sage0.eval("import doctest, sys, os, multiprocessing, subprocess")
|
1420
|
+
sage: _ = sage0.eval("from sage.doctest.parsing import SageOutputChecker")
|
1421
|
+
sage: _ = sage0.eval("import sage.doctest.forker as sdf")
|
1422
|
+
sage: _ = sage0.eval("from sage.doctest.control import DocTestDefaults")
|
1423
|
+
sage: _ = sage0.eval("DD = DocTestDefaults(debug=True)")
|
1424
|
+
sage: _ = sage0.eval("ex1 = doctest.Example('a = 17', '')")
|
1425
|
+
sage: _ = sage0.eval("ex2 = doctest.Example('2*a', '1')")
|
1426
|
+
sage: _ = sage0.eval("DT = doctest.DocTest([ex1,ex2], globals(), 'doubling', None, 0, None)")
|
1427
|
+
sage: _ = sage0.eval("DTR = sdf.SageDocTestRunner(SageOutputChecker(), verbose=False, sage_options=DD, optionflags=doctest.NORMALIZE_WHITESPACE|doctest.ELLIPSIS)")
|
1428
|
+
sage: print(sage0.eval("sdf.init_sage(); DTR.run(DT, clear_globs=False)")) # indirect doctest
|
1429
|
+
**********************************************************************
|
1430
|
+
Line 1, in doubling
|
1431
|
+
Failed example:
|
1432
|
+
2*a
|
1433
|
+
Expected:
|
1434
|
+
1
|
1435
|
+
Got:
|
1436
|
+
34
|
1437
|
+
**********************************************************************
|
1438
|
+
Previously executed commands:
|
1439
|
+
sage: sage0._expect.expect('sage: ') # sage0 just mis-identified the output as prompt, synchronize
|
1440
|
+
0
|
1441
|
+
sage: sage0.eval("a")
|
1442
|
+
'...17'
|
1443
|
+
sage: sage0.eval("quit")
|
1444
|
+
'Returning to doctests...TestResults(failed=1, attempted=2)'
|
1445
|
+
"""
|
1446
|
+
if not self.options.initial or self.no_failure_yet:
|
1447
|
+
self.no_failure_yet = False
|
1448
|
+
example.extra = f'Got: {got}'
|
1449
|
+
returnval = doctest.DocTestRunner.report_failure(self, out, test, example, got)
|
1450
|
+
if self.options.debug:
|
1451
|
+
self._fakeout.stop_spoofing()
|
1452
|
+
restore_tcpgrp = None
|
1453
|
+
try:
|
1454
|
+
if os.isatty(0):
|
1455
|
+
# In order to read from the terminal, we need
|
1456
|
+
# to make the current process group the
|
1457
|
+
# foreground group.
|
1458
|
+
restore_tcpgrp = os.tcgetpgrp(0)
|
1459
|
+
signal.signal(signal.SIGTTIN, signal.SIG_IGN)
|
1460
|
+
signal.signal(signal.SIGTTOU, signal.SIG_IGN)
|
1461
|
+
os.tcsetpgrp(0, os.getpgrp())
|
1462
|
+
print("*" * 70)
|
1463
|
+
print("Previously executed commands:")
|
1464
|
+
for ex in test.examples:
|
1465
|
+
if ex is example:
|
1466
|
+
break
|
1467
|
+
if hasattr(ex, 'sage_source'):
|
1468
|
+
src = ' sage: ' + ex.sage_source
|
1469
|
+
else:
|
1470
|
+
src = ' sage: ' + ex.source
|
1471
|
+
if src[-1] == '\n':
|
1472
|
+
src = src[:-1]
|
1473
|
+
src = src.replace('\n', '\n ....: ')
|
1474
|
+
print(src)
|
1475
|
+
if ex.want:
|
1476
|
+
print(doctest._indent(ex.want[:-1]))
|
1477
|
+
from sage.repl.configuration import sage_ipython_config
|
1478
|
+
from IPython.terminal.embed import InteractiveShellEmbed
|
1479
|
+
cfg = sage_ipython_config.default()
|
1480
|
+
# Currently this doesn't work: prompts only work in pty
|
1481
|
+
# We keep simple_prompt=True, prompts will be "In [0]:"
|
1482
|
+
# cfg.InteractiveShell.prompts_class = DebugPrompts
|
1483
|
+
# cfg.InteractiveShell.simple_prompt = False
|
1484
|
+
shell = InteractiveShellEmbed(config=cfg, banner1='', user_ns=dict(globs))
|
1485
|
+
shell(header='', stack_depth=2)
|
1486
|
+
except KeyboardInterrupt:
|
1487
|
+
# Assume this is a *real* interrupt. We need to
|
1488
|
+
# escalate this to the master doctesting process.
|
1489
|
+
if not self.options.serial:
|
1490
|
+
os.kill(os.getppid(), signal.SIGINT)
|
1491
|
+
raise
|
1492
|
+
finally:
|
1493
|
+
# Restore the foreground process group.
|
1494
|
+
if restore_tcpgrp is not None:
|
1495
|
+
os.tcsetpgrp(0, restore_tcpgrp)
|
1496
|
+
signal.signal(signal.SIGTTIN, signal.SIG_DFL)
|
1497
|
+
signal.signal(signal.SIGTTOU, signal.SIG_DFL)
|
1498
|
+
print("Returning to doctests...")
|
1499
|
+
self._fakeout.start_spoofing()
|
1500
|
+
return returnval
|
1501
|
+
|
1502
|
+
def report_overtime(self, out, test, example, got, *, check_timer=None):
|
1503
|
+
r"""
|
1504
|
+
Called when the ``warn_long`` option flag is set and a doctest
|
1505
|
+
runs longer than the specified time.
|
1506
|
+
|
1507
|
+
INPUT:
|
1508
|
+
|
1509
|
+
- ``out`` -- a function for printing
|
1510
|
+
|
1511
|
+
- ``test`` -- a :class:`doctest.DocTest` instance
|
1512
|
+
|
1513
|
+
- ``example`` -- a :class:`doctest.Example` instance in ``test``
|
1514
|
+
|
1515
|
+
- ``got`` -- string; the result of running ``example``
|
1516
|
+
|
1517
|
+
- ``check_timer`` -- a :class:`sage.doctest.util.Timer` (default:
|
1518
|
+
``None``) that measures the time spent checking whether or not
|
1519
|
+
the output was correct
|
1520
|
+
|
1521
|
+
OUTPUT: prints a report to ``out``
|
1522
|
+
|
1523
|
+
EXAMPLES::
|
1524
|
+
|
1525
|
+
sage: from sage.doctest.parsing import SageOutputChecker
|
1526
|
+
sage: from sage.doctest.forker import SageDocTestRunner
|
1527
|
+
sage: from sage.doctest.sources import FileDocTestSource
|
1528
|
+
sage: from sage.doctest.control import DocTestDefaults; DD = DocTestDefaults()
|
1529
|
+
sage: from sage.doctest.util import Timer
|
1530
|
+
sage: import doctest, sys, os
|
1531
|
+
sage: DTR = SageDocTestRunner(SageOutputChecker(), verbose=True, sage_options=DD, optionflags=doctest.NORMALIZE_WHITESPACE|doctest.ELLIPSIS)
|
1532
|
+
sage: filename = sage.doctest.forker.__file__
|
1533
|
+
sage: FDS = FileDocTestSource(filename, DD)
|
1534
|
+
sage: doctests, extras = FDS.create_doctests(globals())
|
1535
|
+
sage: ex = doctests[0].examples[0]
|
1536
|
+
sage: ex.cputime = 1.23
|
1537
|
+
sage: ex.walltime = 2.50
|
1538
|
+
sage: check = Timer()
|
1539
|
+
sage: check.cputime = 2.34
|
1540
|
+
sage: check.walltime = 3.12
|
1541
|
+
sage: DTR.report_overtime(sys.stdout.write, doctests[0], ex, 'BAD ANSWER\n', check_timer=check)
|
1542
|
+
**********************************************************************
|
1543
|
+
File ".../sage/doctest/forker.py", line 12, in sage.doctest.forker
|
1544
|
+
Warning: slow doctest:
|
1545
|
+
doctest_var = 42; doctest_var^2
|
1546
|
+
Test ran for 1.23s cpu, 2.50s wall
|
1547
|
+
Check ran for 2.34s cpu, 3.12s wall
|
1548
|
+
"""
|
1549
|
+
out(self._failure_header(test, example, 'Warning: slow doctest:') +
|
1550
|
+
('Test ran for %.2fs cpu, %.2fs wall\nCheck ran for %.2fs cpu, %.2fs wall\n'
|
1551
|
+
% (example.cputime,
|
1552
|
+
example.walltime,
|
1553
|
+
check_timer.cputime,
|
1554
|
+
check_timer.walltime)))
|
1555
|
+
|
1556
|
+
def report_unexpected_exception(self, out, test, example, exc_info):
|
1557
|
+
r"""
|
1558
|
+
Called when a doctest raises an exception that's not matched by the expected output.
|
1559
|
+
|
1560
|
+
If debugging has been turned on, starts an interactive debugger.
|
1561
|
+
|
1562
|
+
INPUT:
|
1563
|
+
|
1564
|
+
- ``out`` -- a function for printing
|
1565
|
+
|
1566
|
+
- ``test`` -- a :class:`doctest.DocTest` instance
|
1567
|
+
|
1568
|
+
- ``example`` -- a :class:`doctest.Example` instance in ``test``
|
1569
|
+
|
1570
|
+
- ``exc_info`` -- the result of ``sys.exc_info()``
|
1571
|
+
|
1572
|
+
OUTPUT: prints a report to ``out``
|
1573
|
+
|
1574
|
+
- if in debugging mode, starts PDB with the given traceback
|
1575
|
+
|
1576
|
+
EXAMPLES::
|
1577
|
+
|
1578
|
+
sage: # needs sage.symbolic (actually sage.all)
|
1579
|
+
sage: from sage.interfaces.sage0 import sage0
|
1580
|
+
sage: sage0.quit()
|
1581
|
+
sage: _ = sage0.eval("import doctest, sys, os, multiprocessing, subprocess")
|
1582
|
+
sage: _ = sage0.eval("from sage.doctest.parsing import SageOutputChecker")
|
1583
|
+
sage: _ = sage0.eval("import sage.doctest.forker as sdf")
|
1584
|
+
sage: _ = sage0.eval("from sage.doctest.control import DocTestDefaults")
|
1585
|
+
sage: _ = sage0.eval("DD = DocTestDefaults(debug=True)")
|
1586
|
+
sage: _ = sage0.eval("ex = doctest.Example('E = EllipticCurve([0,0]); E', 'A singular Elliptic Curve')")
|
1587
|
+
sage: _ = sage0.eval("DT = doctest.DocTest([ex], globals(), 'singular_curve', None, 0, None)")
|
1588
|
+
sage: _ = sage0.eval("DTR = sdf.SageDocTestRunner(SageOutputChecker(), verbose=False, sage_options=DD, optionflags=doctest.NORMALIZE_WHITESPACE|doctest.ELLIPSIS)")
|
1589
|
+
sage: old_prompt = sage0._prompt
|
1590
|
+
sage: sage0._prompt = r"\(Pdb\) "
|
1591
|
+
sage: sage0.eval("DTR.run(DT, clear_globs=False)") # indirect doctest
|
1592
|
+
'... ArithmeticError(self._equation_string() + " defines a singular curve")'
|
1593
|
+
sage: sage0.eval("l")
|
1594
|
+
'...if self.discriminant() == 0:...raise ArithmeticError...'
|
1595
|
+
sage: sage0.eval("u")
|
1596
|
+
'...-> super().__init__(R, data, category=category)'
|
1597
|
+
sage: sage0.eval("u")
|
1598
|
+
'...EllipticCurve_field.__init__(self, K, ainvs)'
|
1599
|
+
sage: sage0.eval("p ainvs")
|
1600
|
+
'(0, 0, 0, 0, 0)'
|
1601
|
+
sage: sage0._prompt = old_prompt
|
1602
|
+
sage: sage0.eval("quit")
|
1603
|
+
'TestResults(failed=1, attempted=1)'
|
1604
|
+
"""
|
1605
|
+
if not self.options.initial or self.no_failure_yet:
|
1606
|
+
self.no_failure_yet = False
|
1607
|
+
|
1608
|
+
example.extra = "Exception raised:\n" + "".join(traceback.format_exception(*exc_info))
|
1609
|
+
returnval = doctest.DocTestRunner.report_unexpected_exception(self, out, test, example, exc_info)
|
1610
|
+
if self.options.debug:
|
1611
|
+
self._fakeout.stop_spoofing()
|
1612
|
+
restore_tcpgrp = None
|
1613
|
+
try:
|
1614
|
+
if os.isatty(0):
|
1615
|
+
# In order to read from the terminal, we need
|
1616
|
+
# to make the current process group the
|
1617
|
+
# foreground group.
|
1618
|
+
restore_tcpgrp = os.tcgetpgrp(0)
|
1619
|
+
signal.signal(signal.SIGTTIN, signal.SIG_IGN)
|
1620
|
+
signal.signal(signal.SIGTTOU, signal.SIG_IGN)
|
1621
|
+
os.tcsetpgrp(0, os.getpgrp())
|
1622
|
+
|
1623
|
+
exc_type, exc_val, exc_tb = exc_info
|
1624
|
+
if exc_tb is None:
|
1625
|
+
raise RuntimeError(
|
1626
|
+
"could not start the debugger for an unexpected "
|
1627
|
+
"exception, probably due to an unhandled error "
|
1628
|
+
"in a C extension module")
|
1629
|
+
self.debugger.reset()
|
1630
|
+
self.debugger.interaction(None, exc_tb)
|
1631
|
+
except KeyboardInterrupt:
|
1632
|
+
# Assume this is a *real* interrupt. We need to
|
1633
|
+
# escalate this to the master doctesting process.
|
1634
|
+
if not self.options.serial:
|
1635
|
+
os.kill(os.getppid(), signal.SIGINT)
|
1636
|
+
raise
|
1637
|
+
finally:
|
1638
|
+
# Restore the foreground process group.
|
1639
|
+
if restore_tcpgrp is not None:
|
1640
|
+
os.tcsetpgrp(0, restore_tcpgrp)
|
1641
|
+
signal.signal(signal.SIGTTIN, signal.SIG_DFL)
|
1642
|
+
signal.signal(signal.SIGTTOU, signal.SIG_DFL)
|
1643
|
+
self._fakeout.start_spoofing()
|
1644
|
+
return returnval
|
1645
|
+
|
1646
|
+
def update_results(self, D):
|
1647
|
+
"""
|
1648
|
+
When returning results we pick out the results of interest
|
1649
|
+
since many attributes are not pickleable.
|
1650
|
+
|
1651
|
+
INPUT:
|
1652
|
+
|
1653
|
+
- ``D`` -- dictionary to update with cputime and walltime
|
1654
|
+
|
1655
|
+
OUTPUT: the number of failures (or ``False`` if there is no failure
|
1656
|
+
attribute)
|
1657
|
+
|
1658
|
+
EXAMPLES::
|
1659
|
+
|
1660
|
+
sage: from sage.doctest.parsing import SageOutputChecker
|
1661
|
+
sage: from sage.doctest.forker import SageDocTestRunner
|
1662
|
+
sage: from sage.doctest.sources import FileDocTestSource, DictAsObject
|
1663
|
+
sage: from sage.doctest.control import DocTestDefaults; DD = DocTestDefaults()
|
1664
|
+
sage: import doctest, sys, os
|
1665
|
+
sage: DTR = SageDocTestRunner(SageOutputChecker(), verbose=False, sage_options=DD, optionflags=doctest.NORMALIZE_WHITESPACE|doctest.ELLIPSIS)
|
1666
|
+
sage: filename = sage.doctest.forker.__file__
|
1667
|
+
sage: FDS = FileDocTestSource(filename, DD)
|
1668
|
+
sage: doctests, extras = FDS.create_doctests(globals())
|
1669
|
+
sage: from sage.doctest.util import Timer
|
1670
|
+
sage: T = Timer().start()
|
1671
|
+
sage: DTR.run(doctests[0])
|
1672
|
+
TestResults(failed=0, attempted=4)
|
1673
|
+
sage: T.stop().annotate(DTR)
|
1674
|
+
sage: D = DictAsObject({'cputime': [], 'walltime': [], 'err': None})
|
1675
|
+
sage: DTR.update_results(D)
|
1676
|
+
0
|
1677
|
+
sage: sorted(list(D.items()))
|
1678
|
+
[('cputime', [...]), ('err', None), ('failures', 0), ('tests', 4),
|
1679
|
+
('walltime', [...]), ('walltime_skips', 0)]
|
1680
|
+
"""
|
1681
|
+
for key in ["cputime", "walltime"]:
|
1682
|
+
if key not in D:
|
1683
|
+
D[key] = []
|
1684
|
+
if hasattr(self, key):
|
1685
|
+
D[key].append(self.__dict__[key])
|
1686
|
+
D['tests'] = self.total_performed_tests
|
1687
|
+
D['walltime_skips'] = self.total_walltime_skips
|
1688
|
+
if hasattr(self, 'failures'):
|
1689
|
+
D['failures'] = self.failures
|
1690
|
+
return self.failures
|
1691
|
+
else:
|
1692
|
+
return False
|
1693
|
+
|
1694
|
+
|
1695
|
+
def dummy_handler(sig, frame):
|
1696
|
+
"""
|
1697
|
+
Dummy signal handler for SIGCHLD (just to ensure the signal
|
1698
|
+
isn't ignored).
|
1699
|
+
|
1700
|
+
TESTS::
|
1701
|
+
|
1702
|
+
sage: import signal
|
1703
|
+
sage: from sage.doctest.forker import dummy_handler
|
1704
|
+
sage: _ = signal.signal(signal.SIGUSR1, dummy_handler)
|
1705
|
+
sage: os.kill(os.getpid(), signal.SIGUSR1)
|
1706
|
+
sage: signal.signal(signal.SIGUSR1, signal.SIG_DFL)
|
1707
|
+
<function dummy_handler at ...>
|
1708
|
+
"""
|
1709
|
+
pass
|
1710
|
+
|
1711
|
+
|
1712
|
+
class DocTestDispatcher(SageObject):
|
1713
|
+
"""
|
1714
|
+
Create parallel :class:`DocTestWorker` processes and dispatches
|
1715
|
+
doctesting tasks.
|
1716
|
+
"""
|
1717
|
+
def __init__(self, controller):
|
1718
|
+
"""
|
1719
|
+
INPUT:
|
1720
|
+
|
1721
|
+
- ``controller`` -- a :class:`sage.doctest.control.DocTestController` instance
|
1722
|
+
|
1723
|
+
EXAMPLES::
|
1724
|
+
|
1725
|
+
sage: # needs sage.all
|
1726
|
+
sage: from sage.doctest.control import DocTestController, DocTestDefaults
|
1727
|
+
sage: from sage.doctest.forker import DocTestDispatcher
|
1728
|
+
sage: DocTestDispatcher(DocTestController(DocTestDefaults(), []))
|
1729
|
+
<sage.doctest.forker.DocTestDispatcher object at ...>
|
1730
|
+
"""
|
1731
|
+
self.controller = controller
|
1732
|
+
init_sage(controller)
|
1733
|
+
|
1734
|
+
def serial_dispatch(self):
|
1735
|
+
"""
|
1736
|
+
Run the doctests from the controller's specified sources in series.
|
1737
|
+
|
1738
|
+
There is no graceful handling for signals, no possibility of
|
1739
|
+
interrupting tests and no timeout.
|
1740
|
+
|
1741
|
+
EXAMPLES::
|
1742
|
+
|
1743
|
+
sage: # needs sage.all
|
1744
|
+
sage: from sage.doctest.control import DocTestController, DocTestDefaults
|
1745
|
+
sage: from sage.doctest.forker import DocTestDispatcher
|
1746
|
+
sage: from sage.doctest.reporting import DocTestReporter
|
1747
|
+
sage: from sage.doctest.util import Timer
|
1748
|
+
sage: import os
|
1749
|
+
sage: homset = os.path.join(SAGE_SRC, 'sage', 'rings', 'homset.py')
|
1750
|
+
sage: ideal = os.path.join(SAGE_SRC, 'sage', 'rings', 'ideal.py')
|
1751
|
+
sage: DC = DocTestController(DocTestDefaults(), [homset, ideal])
|
1752
|
+
sage: DC.expand_files_into_sources()
|
1753
|
+
sage: DD = DocTestDispatcher(DC)
|
1754
|
+
sage: DR = DocTestReporter(DC)
|
1755
|
+
sage: DC.reporter = DR
|
1756
|
+
sage: DC.dispatcher = DD
|
1757
|
+
sage: DC.timer = Timer().start()
|
1758
|
+
sage: DD.serial_dispatch()
|
1759
|
+
sage -t .../rings/homset.py
|
1760
|
+
[... tests, ...s wall]
|
1761
|
+
sage -t .../rings/ideal.py
|
1762
|
+
[... tests, ...s wall]
|
1763
|
+
"""
|
1764
|
+
for source in self.controller.sources:
|
1765
|
+
heading = self.controller.reporter.report_head(source)
|
1766
|
+
baseline = self.controller.source_baseline(source)
|
1767
|
+
if not self.controller.options.only_errors:
|
1768
|
+
self.controller.log(heading)
|
1769
|
+
|
1770
|
+
with tempfile.TemporaryFile() as outtmpfile:
|
1771
|
+
result = DocTestTask(source)(self.controller.options,
|
1772
|
+
outtmpfile, self.controller.logger,
|
1773
|
+
baseline=baseline)
|
1774
|
+
outtmpfile.seek(0)
|
1775
|
+
output = bytes_to_str(outtmpfile.read())
|
1776
|
+
|
1777
|
+
self.controller.reporter.report(source, False, 0, result, output)
|
1778
|
+
if self.controller.options.exitfirst and result[1].failures:
|
1779
|
+
break
|
1780
|
+
|
1781
|
+
def parallel_dispatch(self):
|
1782
|
+
r"""
|
1783
|
+
Run the doctests from the controller's specified sources in parallel.
|
1784
|
+
|
1785
|
+
This creates :class:`DocTestWorker` subprocesses, while the master
|
1786
|
+
process checks for timeouts and collects and displays the results.
|
1787
|
+
|
1788
|
+
EXAMPLES::
|
1789
|
+
|
1790
|
+
sage: # needs sage.all
|
1791
|
+
sage: from sage.doctest.control import DocTestController, DocTestDefaults
|
1792
|
+
sage: from sage.doctest.forker import DocTestDispatcher
|
1793
|
+
sage: from sage.doctest.reporting import DocTestReporter
|
1794
|
+
sage: from sage.doctest.util import Timer
|
1795
|
+
sage: import os
|
1796
|
+
sage: crem = os.path.join(SAGE_SRC, 'sage', 'databases', 'cremona.py')
|
1797
|
+
sage: bigo = os.path.join(SAGE_SRC, 'sage', 'rings', 'big_oh.py')
|
1798
|
+
sage: DC = DocTestController(DocTestDefaults(), [crem, bigo])
|
1799
|
+
sage: DC.expand_files_into_sources()
|
1800
|
+
sage: DD = DocTestDispatcher(DC)
|
1801
|
+
sage: DR = DocTestReporter(DC)
|
1802
|
+
sage: DC.reporter = DR
|
1803
|
+
sage: DC.dispatcher = DD
|
1804
|
+
sage: DC.timer = Timer().start()
|
1805
|
+
sage: DD.parallel_dispatch()
|
1806
|
+
sage -t .../databases/cremona.py
|
1807
|
+
[... tests, ...s wall]
|
1808
|
+
sage -t .../rings/big_oh.py
|
1809
|
+
[... tests, ...s wall]
|
1810
|
+
|
1811
|
+
If the ``exitfirst=True`` option is given, the results for a failing
|
1812
|
+
module will be immediately printed and any other ongoing tests
|
1813
|
+
canceled::
|
1814
|
+
|
1815
|
+
sage: # needs sage.all
|
1816
|
+
sage: from tempfile import NamedTemporaryFile as NTF
|
1817
|
+
sage: with NTF(suffix='.py', mode='w+t') as f1, \
|
1818
|
+
....: NTF(suffix='.py', mode='w+t') as f2:
|
1819
|
+
....: _ = f1.write("'''\nsage: import time; time.sleep(60)\n'''")
|
1820
|
+
....: f1.flush()
|
1821
|
+
....: _ = f2.write("'''\nsage: True\nFalse\n'''")
|
1822
|
+
....: f2.flush()
|
1823
|
+
....: DC = DocTestController(DocTestDefaults(exitfirst=True,
|
1824
|
+
....: nthreads=2),
|
1825
|
+
....: [f1.name, f2.name])
|
1826
|
+
....: DC.expand_files_into_sources()
|
1827
|
+
....: DD = DocTestDispatcher(DC)
|
1828
|
+
....: DR = DocTestReporter(DC)
|
1829
|
+
....: DC.reporter = DR
|
1830
|
+
....: DC.dispatcher = DD
|
1831
|
+
....: DC.timer = Timer().start()
|
1832
|
+
....: DD.parallel_dispatch()
|
1833
|
+
sage -t ...
|
1834
|
+
**********************************************************************
|
1835
|
+
File "...", line 2, in ...
|
1836
|
+
Failed example:
|
1837
|
+
True
|
1838
|
+
Expected:
|
1839
|
+
False
|
1840
|
+
Got:
|
1841
|
+
True
|
1842
|
+
**********************************************************************
|
1843
|
+
1 item had failures:
|
1844
|
+
1 of 1 in ...
|
1845
|
+
[1 test, 1 failure, ...s wall]
|
1846
|
+
Killing test ...
|
1847
|
+
"""
|
1848
|
+
opt = self.controller.options
|
1849
|
+
|
1850
|
+
job_client = None
|
1851
|
+
try:
|
1852
|
+
from gnumake_tokenpool import JobClient, NoJobServer
|
1853
|
+
except ImportError:
|
1854
|
+
pass
|
1855
|
+
else:
|
1856
|
+
try:
|
1857
|
+
job_client = JobClient(use_cysignals=True)
|
1858
|
+
except NoJobServer:
|
1859
|
+
pass
|
1860
|
+
|
1861
|
+
source_iter = iter(self.controller.sources)
|
1862
|
+
|
1863
|
+
# If timeout was 0, simply set a very long time
|
1864
|
+
if opt.timeout <= 0:
|
1865
|
+
opt.timeout = 2**60
|
1866
|
+
# Timeout we give a process to die (after it received a SIGQUIT
|
1867
|
+
# signal). If it doesn't exit by itself in this many seconds, we
|
1868
|
+
# SIGKILL it. This is 5% of doctest timeout, with a maximum of
|
1869
|
+
# 10 minutes and a minimum of 60 seconds.
|
1870
|
+
die_timeout = opt.timeout * 0.05
|
1871
|
+
if die_timeout > 600:
|
1872
|
+
die_timeout = 600
|
1873
|
+
elif die_timeout < 60:
|
1874
|
+
die_timeout = 60
|
1875
|
+
# allow override via cmdline option
|
1876
|
+
if opt.die_timeout >= 0:
|
1877
|
+
die_timeout = opt.die_timeout
|
1878
|
+
|
1879
|
+
# If we think that we can not finish running all tests until
|
1880
|
+
# target_endtime, we skip individual tests. (Only enabled with
|
1881
|
+
# --short.)
|
1882
|
+
if opt.target_walltime == -1:
|
1883
|
+
target_endtime = None
|
1884
|
+
else:
|
1885
|
+
target_endtime = time.time() + opt.target_walltime
|
1886
|
+
pending_tests = len(self.controller.sources)
|
1887
|
+
|
1888
|
+
# List of alive DocTestWorkers (child processes). Workers which
|
1889
|
+
# are done but whose messages have not been read are also
|
1890
|
+
# considered alive.
|
1891
|
+
workers = []
|
1892
|
+
|
1893
|
+
# List of DocTestWorkers which have finished running but
|
1894
|
+
# whose results have not been reported yet.
|
1895
|
+
finished = []
|
1896
|
+
|
1897
|
+
# If exitfirst is set and we got a failure.
|
1898
|
+
abort_now = False
|
1899
|
+
|
1900
|
+
# One particular worker that we are "following": we report the
|
1901
|
+
# messages while it's running. For other workers, we report the
|
1902
|
+
# messages if there is no followed worker.
|
1903
|
+
follow = None
|
1904
|
+
|
1905
|
+
# Install signal handler for SIGCHLD
|
1906
|
+
signal.signal(signal.SIGCHLD, dummy_handler)
|
1907
|
+
|
1908
|
+
# Logger
|
1909
|
+
log = self.controller.log
|
1910
|
+
|
1911
|
+
from cysignals.pselect import PSelecter
|
1912
|
+
try:
|
1913
|
+
# Block SIGCHLD and SIGINT except during the pselect() call
|
1914
|
+
with PSelecter([signal.SIGCHLD, signal.SIGINT]) as sel:
|
1915
|
+
# Function to execute in the child process which exits
|
1916
|
+
# this "with" statement (which restores the signal mask)
|
1917
|
+
# and resets to SIGCHLD handler to default.
|
1918
|
+
# Since multiprocessing.Process is implemented using
|
1919
|
+
# fork(), signals would otherwise remain blocked in the
|
1920
|
+
# child process.
|
1921
|
+
def sel_exit():
|
1922
|
+
signal.signal(signal.SIGCHLD, signal.SIG_DFL)
|
1923
|
+
sel.__exit__(None, None, None)
|
1924
|
+
|
1925
|
+
while True:
|
1926
|
+
# To avoid calling time.time() all the time while
|
1927
|
+
# checking for timeouts, we call it here, once per
|
1928
|
+
# loop. It's not a problem if this isn't very
|
1929
|
+
# precise, doctest timeouts don't need millisecond
|
1930
|
+
# precision.
|
1931
|
+
now = time.time()
|
1932
|
+
|
1933
|
+
# If there were any substantial changes in the state
|
1934
|
+
# (new worker started or finished worker reported),
|
1935
|
+
# restart this while loop instead of calling pselect().
|
1936
|
+
# This ensures internal consistency and a reasonably
|
1937
|
+
# accurate value for "now".
|
1938
|
+
restart = False
|
1939
|
+
|
1940
|
+
# Process all workers. Check for timeouts on active
|
1941
|
+
# workers and move finished/crashed workers to the
|
1942
|
+
# "finished" list.
|
1943
|
+
# Create a new list "new_workers" containing the active
|
1944
|
+
# workers (to avoid updating "workers" in place).
|
1945
|
+
new_workers = []
|
1946
|
+
for w in workers:
|
1947
|
+
if w.rmessages is not None or w.is_alive():
|
1948
|
+
if now >= w.deadline:
|
1949
|
+
# Timeout => (try to) kill the process
|
1950
|
+
# group (which normally includes
|
1951
|
+
# grandchildren) and close the message
|
1952
|
+
# pipe.
|
1953
|
+
# We don't report the timeout yet, we wait
|
1954
|
+
# until the process has actually died.
|
1955
|
+
w.kill()
|
1956
|
+
w.deadline = now + die_timeout
|
1957
|
+
if not w.is_alive():
|
1958
|
+
# Worker is done but we haven't read all
|
1959
|
+
# messages (possibly a grandchild still
|
1960
|
+
# has the messages pipe open).
|
1961
|
+
# Adjust deadline to read all messages:
|
1962
|
+
newdeadline = now + die_timeout
|
1963
|
+
w.deadline = min(w.deadline, newdeadline)
|
1964
|
+
new_workers.append(w)
|
1965
|
+
else:
|
1966
|
+
# Save the result and output of the worker
|
1967
|
+
# and close the associated file descriptors.
|
1968
|
+
# It is important to do this now. If we
|
1969
|
+
# would leave them open until we call
|
1970
|
+
# report(), parallel testing can easily fail
|
1971
|
+
# with a "Too many open files" error.
|
1972
|
+
w.save_result_output()
|
1973
|
+
# In python3 multiprocessing.Process also
|
1974
|
+
# opens a pipe internally, which has to be
|
1975
|
+
# closed here, as well.
|
1976
|
+
# But afterwards, exitcode and pid are
|
1977
|
+
# no longer available.
|
1978
|
+
w.copied_exitcode = w.exitcode
|
1979
|
+
w.copied_pid = w.pid
|
1980
|
+
w.close()
|
1981
|
+
finished.append(w)
|
1982
|
+
if job_client:
|
1983
|
+
job_client.release()
|
1984
|
+
|
1985
|
+
workers = new_workers
|
1986
|
+
|
1987
|
+
# Similarly, process finished workers.
|
1988
|
+
new_finished = []
|
1989
|
+
for w in finished:
|
1990
|
+
if opt.exitfirst and w.result[1].failures:
|
1991
|
+
abort_now = True
|
1992
|
+
elif follow is not None and follow is not w:
|
1993
|
+
# We are following a different worker, so
|
1994
|
+
# we cannot report now.
|
1995
|
+
new_finished.append(w)
|
1996
|
+
continue
|
1997
|
+
|
1998
|
+
# Report the completion of this worker
|
1999
|
+
log(w.messages, end="")
|
2000
|
+
self.controller.reporter.report(
|
2001
|
+
w.source,
|
2002
|
+
w.killed,
|
2003
|
+
w.copied_exitcode,
|
2004
|
+
w.result,
|
2005
|
+
w.output,
|
2006
|
+
pid=w.copied_pid)
|
2007
|
+
|
2008
|
+
pending_tests -= 1
|
2009
|
+
|
2010
|
+
restart = True
|
2011
|
+
follow = None
|
2012
|
+
|
2013
|
+
finished = new_finished
|
2014
|
+
|
2015
|
+
if abort_now:
|
2016
|
+
break
|
2017
|
+
|
2018
|
+
# Start new workers if possible
|
2019
|
+
while (source_iter is not None and len(workers) < opt.nthreads
|
2020
|
+
and (not job_client or job_client.acquire())):
|
2021
|
+
try:
|
2022
|
+
source = next(source_iter)
|
2023
|
+
except StopIteration:
|
2024
|
+
source_iter = None
|
2025
|
+
if job_client:
|
2026
|
+
job_client.release()
|
2027
|
+
else:
|
2028
|
+
# Start a new worker.
|
2029
|
+
import copy
|
2030
|
+
worker_options = copy.copy(opt)
|
2031
|
+
baseline = self.controller.source_baseline(source)
|
2032
|
+
if target_endtime is not None:
|
2033
|
+
worker_options.target_walltime = (target_endtime - now) / (max(1, pending_tests / opt.nthreads))
|
2034
|
+
w = DocTestWorker(source, options=worker_options, funclist=[sel_exit], baseline=baseline)
|
2035
|
+
heading = self.controller.reporter.report_head(w.source)
|
2036
|
+
if not self.controller.options.only_errors:
|
2037
|
+
w.messages = heading + "\n"
|
2038
|
+
# Store length of heading to detect if the
|
2039
|
+
# worker has something interesting to report.
|
2040
|
+
w.heading_len = len(w.messages)
|
2041
|
+
w.start() # This might take some time
|
2042
|
+
w.deadline = time.time() + opt.timeout
|
2043
|
+
workers.append(w)
|
2044
|
+
restart = True
|
2045
|
+
|
2046
|
+
# Recompute state if needed
|
2047
|
+
if restart:
|
2048
|
+
continue
|
2049
|
+
|
2050
|
+
# We are finished if there are no DocTestWorkers left
|
2051
|
+
if len(workers) == 0:
|
2052
|
+
# If there are no active workers, we should have
|
2053
|
+
# reported all finished workers.
|
2054
|
+
assert len(finished) == 0
|
2055
|
+
break
|
2056
|
+
|
2057
|
+
# The master pselect() call
|
2058
|
+
rlist = [w.rmessages for w in workers if w.rmessages is not None]
|
2059
|
+
tmout = min(w.deadline for w in workers) - now
|
2060
|
+
tmout = min(tmout, 5)
|
2061
|
+
rlist, _, _, _ = sel.pselect(rlist, timeout=tmout)
|
2062
|
+
|
2063
|
+
# Read messages
|
2064
|
+
for w in workers:
|
2065
|
+
if w.rmessages is not None and w.rmessages in rlist:
|
2066
|
+
w.read_messages()
|
2067
|
+
|
2068
|
+
# Find a worker to follow: if there is only one worker,
|
2069
|
+
# always follow it. Otherwise, take the worker with
|
2070
|
+
# the earliest deadline of all workers whose
|
2071
|
+
# messages are more than just the heading.
|
2072
|
+
if follow is None:
|
2073
|
+
if len(workers) == 1:
|
2074
|
+
follow = workers[0]
|
2075
|
+
else:
|
2076
|
+
for w in workers:
|
2077
|
+
if len(w.messages) > w.heading_len:
|
2078
|
+
if follow is None or w.deadline < follow.deadline:
|
2079
|
+
follow = w
|
2080
|
+
|
2081
|
+
# Write messages of followed worker
|
2082
|
+
if follow is not None:
|
2083
|
+
log(follow.messages, end="")
|
2084
|
+
follow.messages = ""
|
2085
|
+
finally:
|
2086
|
+
# Restore SIGCHLD handler (which is to ignore the signal)
|
2087
|
+
signal.signal(signal.SIGCHLD, signal.SIG_DFL)
|
2088
|
+
|
2089
|
+
# Kill all remaining workers (in case we got interrupted)
|
2090
|
+
for w in workers:
|
2091
|
+
if w.kill():
|
2092
|
+
log("Killing test %s" % w.source.printpath)
|
2093
|
+
# Fork a child process with the specific purpose of
|
2094
|
+
# killing the remaining workers.
|
2095
|
+
if len(workers) > 0 and os.fork() == 0:
|
2096
|
+
# Block these signals
|
2097
|
+
with PSelecter([signal.SIGQUIT, signal.SIGINT]):
|
2098
|
+
try:
|
2099
|
+
from time import sleep
|
2100
|
+
sleep(die_timeout)
|
2101
|
+
for w in workers:
|
2102
|
+
w.kill()
|
2103
|
+
if job_client:
|
2104
|
+
job_client.release()
|
2105
|
+
finally:
|
2106
|
+
os._exit(0)
|
2107
|
+
|
2108
|
+
# Hack to ensure multiprocessing leaves these processes
|
2109
|
+
# alone (in particular, it doesn't wait for them when we
|
2110
|
+
# exit).
|
2111
|
+
p = multiprocessing.process
|
2112
|
+
assert hasattr(p, '_children')
|
2113
|
+
p._children = set()
|
2114
|
+
|
2115
|
+
def dispatch(self):
|
2116
|
+
"""
|
2117
|
+
Run the doctests for the controller's specified sources,
|
2118
|
+
by calling :meth:`parallel_dispatch` or :meth:`serial_dispatch`
|
2119
|
+
according to the ``--serial`` option.
|
2120
|
+
|
2121
|
+
EXAMPLES::
|
2122
|
+
|
2123
|
+
sage: # needs sage.modules
|
2124
|
+
sage: from sage.doctest.control import DocTestController, DocTestDefaults
|
2125
|
+
sage: from sage.doctest.forker import DocTestDispatcher
|
2126
|
+
sage: from sage.doctest.reporting import DocTestReporter
|
2127
|
+
sage: from sage.doctest.util import Timer
|
2128
|
+
sage: import os
|
2129
|
+
sage: freehom = os.path.join(SAGE_SRC, 'sage', 'modules', 'free_module_homspace.py')
|
2130
|
+
sage: bigo = os.path.join(SAGE_SRC, 'sage', 'rings', 'big_oh.py')
|
2131
|
+
sage: DC = DocTestController(DocTestDefaults(), [freehom, bigo])
|
2132
|
+
sage: DC.expand_files_into_sources()
|
2133
|
+
sage: DD = DocTestDispatcher(DC)
|
2134
|
+
sage: DR = DocTestReporter(DC)
|
2135
|
+
sage: DC.reporter = DR
|
2136
|
+
sage: DC.dispatcher = DD
|
2137
|
+
sage: DC.timer = Timer().start()
|
2138
|
+
sage: DD.dispatch()
|
2139
|
+
sage -t .../sage/modules/free_module_homspace.py
|
2140
|
+
[... tests, ...s wall]
|
2141
|
+
sage -t .../sage/rings/big_oh.py
|
2142
|
+
[... tests, ...s wall]
|
2143
|
+
"""
|
2144
|
+
if self.controller.options.serial:
|
2145
|
+
self.serial_dispatch()
|
2146
|
+
else:
|
2147
|
+
self.parallel_dispatch()
|
2148
|
+
|
2149
|
+
|
2150
|
+
class DocTestWorker(multiprocessing.Process):
|
2151
|
+
"""
|
2152
|
+
The DocTestWorker process runs one :class:`DocTestTask` for a given
|
2153
|
+
source. It returns messages about doctest failures (or all tests if
|
2154
|
+
verbose doctesting) through a pipe and returns results through a
|
2155
|
+
``multiprocessing.Queue`` instance (both these are created in the
|
2156
|
+
:meth:`start` method).
|
2157
|
+
|
2158
|
+
It runs the task in its own process-group, such that killing the
|
2159
|
+
process group kills this process together with its child processes.
|
2160
|
+
|
2161
|
+
The class has additional methods and attributes for bookkeeping
|
2162
|
+
by the master process. Except in :meth:`run`, nothing from this
|
2163
|
+
class should be accessed by the child process.
|
2164
|
+
|
2165
|
+
INPUT:
|
2166
|
+
|
2167
|
+
- ``source`` -- a :class:`DocTestSource` instance
|
2168
|
+
|
2169
|
+
- ``options`` -- an object representing doctest options
|
2170
|
+
|
2171
|
+
- ``funclist`` -- list of callables to be called at the start of
|
2172
|
+
the child process
|
2173
|
+
|
2174
|
+
- ``baseline`` -- dictionary, the ``baseline_stats`` value
|
2175
|
+
|
2176
|
+
EXAMPLES::
|
2177
|
+
|
2178
|
+
sage: # needs sage.all
|
2179
|
+
sage: from sage.doctest.forker import DocTestWorker, DocTestTask
|
2180
|
+
sage: from sage.doctest.sources import FileDocTestSource
|
2181
|
+
sage: from sage.doctest.reporting import DocTestReporter
|
2182
|
+
sage: from sage.doctest.control import DocTestController, DocTestDefaults
|
2183
|
+
sage: filename = sage.doctest.util.__file__
|
2184
|
+
sage: DD = DocTestDefaults()
|
2185
|
+
sage: FDS = FileDocTestSource(filename, DD)
|
2186
|
+
sage: W = DocTestWorker(FDS, DD)
|
2187
|
+
sage: W.start()
|
2188
|
+
sage: DC = DocTestController(DD, filename)
|
2189
|
+
sage: reporter = DocTestReporter(DC)
|
2190
|
+
sage: W.join() # Wait for worker to finish
|
2191
|
+
sage: result = W.result_queue.get()
|
2192
|
+
sage: reporter.report(FDS, False, W.exitcode, result, "")
|
2193
|
+
[... tests, ...s wall]
|
2194
|
+
"""
|
2195
|
+
def __init__(self, source, options, funclist=[], baseline=None):
|
2196
|
+
"""
|
2197
|
+
Initialization.
|
2198
|
+
|
2199
|
+
TESTS::
|
2200
|
+
|
2201
|
+
sage: # needs sage.all
|
2202
|
+
sage: run_doctests(sage.rings.big_oh) # indirect doctest
|
2203
|
+
Running doctests with ID ...
|
2204
|
+
Doctesting 1 file.
|
2205
|
+
sage -t .../sage/rings/big_oh.py
|
2206
|
+
[... tests, ...s wall]
|
2207
|
+
----------------------------------------------------------------------
|
2208
|
+
All tests passed!
|
2209
|
+
----------------------------------------------------------------------
|
2210
|
+
Total time for all tests: ... seconds
|
2211
|
+
cpu time: ... seconds
|
2212
|
+
cumulative wall time: ... seconds
|
2213
|
+
Features detected...
|
2214
|
+
"""
|
2215
|
+
multiprocessing.Process.__init__(self)
|
2216
|
+
|
2217
|
+
self.source = source
|
2218
|
+
self.options = options
|
2219
|
+
self.funclist = funclist
|
2220
|
+
self.baseline = baseline
|
2221
|
+
|
2222
|
+
# Open pipe for messages. These are raw file descriptors,
|
2223
|
+
# not Python file objects!
|
2224
|
+
self.rmessages, self.wmessages = os.pipe()
|
2225
|
+
|
2226
|
+
# Create Queue for the result. Since we're running only one
|
2227
|
+
# doctest, this "queue" will contain only 1 element.
|
2228
|
+
self.result_queue = multiprocessing.Queue(1)
|
2229
|
+
|
2230
|
+
# Temporary file for stdout/stderr of the child process.
|
2231
|
+
# Normally, this isn't used in the master process except to
|
2232
|
+
# debug timeouts/crashes.
|
2233
|
+
self.outtmpfile = tempfile.NamedTemporaryFile(delete=False)
|
2234
|
+
|
2235
|
+
# Create string for the master process to store the messages
|
2236
|
+
# (usually these are the doctest failures) of the child.
|
2237
|
+
# These messages are read through the pipe created above.
|
2238
|
+
self.messages = ""
|
2239
|
+
|
2240
|
+
# Has this worker been killed (because of a time out)?
|
2241
|
+
self.killed = False
|
2242
|
+
|
2243
|
+
def run(self):
|
2244
|
+
"""
|
2245
|
+
Run the :class:`DocTestTask` under its own PGID.
|
2246
|
+
|
2247
|
+
TESTS::
|
2248
|
+
|
2249
|
+
sage: run_doctests(sage.symbolic.units) # indirect doctest # needs sage.symbolic
|
2250
|
+
Running doctests with ID ...
|
2251
|
+
Doctesting 1 file.
|
2252
|
+
sage -t .../sage/symbolic/units.py
|
2253
|
+
[... tests, ...s wall]
|
2254
|
+
----------------------------------------------------------------------
|
2255
|
+
All tests passed!
|
2256
|
+
----------------------------------------------------------------------
|
2257
|
+
Total time for all tests: ... seconds
|
2258
|
+
cpu time: ... seconds
|
2259
|
+
cumulative wall time: ... seconds
|
2260
|
+
Features detected...
|
2261
|
+
"""
|
2262
|
+
os.setpgid(os.getpid(), os.getpid())
|
2263
|
+
|
2264
|
+
# Run functions
|
2265
|
+
for f in self.funclist:
|
2266
|
+
f()
|
2267
|
+
|
2268
|
+
# Write one byte to the pipe to signal to the master process
|
2269
|
+
# that we have started properly.
|
2270
|
+
os.write(self.wmessages, b"X")
|
2271
|
+
|
2272
|
+
task = DocTestTask(self.source)
|
2273
|
+
|
2274
|
+
# Ensure the Python stdin is the actual stdin
|
2275
|
+
# (multiprocessing redirects this).
|
2276
|
+
# We will do a more proper redirect of stdin in SageSpoofInOut.
|
2277
|
+
try:
|
2278
|
+
sys.stdin = os.fdopen(0, "r")
|
2279
|
+
except OSError:
|
2280
|
+
# We failed to open stdin for reading, this might happen
|
2281
|
+
# for example when running under "nohup" (Issue #14307).
|
2282
|
+
# Simply redirect stdin from /dev/null and try again.
|
2283
|
+
with open(os.devnull) as f:
|
2284
|
+
os.dup2(f.fileno(), 0)
|
2285
|
+
sys.stdin = os.fdopen(0, "r")
|
2286
|
+
|
2287
|
+
# Close the reading end of the pipe (only the master should
|
2288
|
+
# read from the pipe) and open the writing end.
|
2289
|
+
os.close(self.rmessages)
|
2290
|
+
msgpipe = os.fdopen(self.wmessages, "w")
|
2291
|
+
try:
|
2292
|
+
task(self.options, self.outtmpfile, msgpipe, self.result_queue,
|
2293
|
+
baseline=self.baseline)
|
2294
|
+
finally:
|
2295
|
+
msgpipe.close()
|
2296
|
+
self.outtmpfile.close()
|
2297
|
+
|
2298
|
+
def start(self):
|
2299
|
+
"""
|
2300
|
+
Start the worker and close the writing end of the message pipe.
|
2301
|
+
|
2302
|
+
TESTS::
|
2303
|
+
|
2304
|
+
sage: from sage.doctest.forker import DocTestWorker, DocTestTask
|
2305
|
+
sage: from sage.doctest.sources import FileDocTestSource
|
2306
|
+
sage: from sage.doctest.reporting import DocTestReporter
|
2307
|
+
sage: from sage.doctest.control import DocTestController, DocTestDefaults
|
2308
|
+
sage: filename = sage.doctest.util.__file__
|
2309
|
+
sage: DD = DocTestDefaults()
|
2310
|
+
sage: FDS = FileDocTestSource(filename, DD)
|
2311
|
+
sage: W = DocTestWorker(FDS, DD)
|
2312
|
+
sage: W.start()
|
2313
|
+
sage: try:
|
2314
|
+
....: os.fstat(W.wmessages)
|
2315
|
+
....: except OSError:
|
2316
|
+
....: print("Write end of pipe successfully closed")
|
2317
|
+
Write end of pipe successfully closed
|
2318
|
+
sage: W.join() # Wait for worker to finish
|
2319
|
+
"""
|
2320
|
+
super().start()
|
2321
|
+
|
2322
|
+
# Close the writing end of the pipe (only the child should
|
2323
|
+
# write to the pipe).
|
2324
|
+
os.close(self.wmessages)
|
2325
|
+
|
2326
|
+
# Read one byte from the pipe as a sign that the child process
|
2327
|
+
# has properly started (to avoid race conditions). In particular,
|
2328
|
+
# it will have its process group changed.
|
2329
|
+
os.read(self.rmessages, 1)
|
2330
|
+
|
2331
|
+
def read_messages(self):
|
2332
|
+
"""
|
2333
|
+
In the master process, read from the pipe and store the data
|
2334
|
+
read in the ``messages`` attribute.
|
2335
|
+
|
2336
|
+
.. NOTE::
|
2337
|
+
|
2338
|
+
This function may need to be called multiple times in
|
2339
|
+
order to read all of the messages.
|
2340
|
+
|
2341
|
+
EXAMPLES::
|
2342
|
+
|
2343
|
+
sage: # needs sage.all
|
2344
|
+
sage: from sage.doctest.forker import DocTestWorker, DocTestTask
|
2345
|
+
sage: from sage.doctest.sources import FileDocTestSource
|
2346
|
+
sage: from sage.doctest.reporting import DocTestReporter
|
2347
|
+
sage: from sage.doctest.control import DocTestController, DocTestDefaults
|
2348
|
+
sage: filename = sage.doctest.util.__file__
|
2349
|
+
sage: DD = DocTestDefaults(verbose=True,nthreads=2)
|
2350
|
+
sage: FDS = FileDocTestSource(filename, DD)
|
2351
|
+
sage: W = DocTestWorker(FDS, DD)
|
2352
|
+
sage: W.start()
|
2353
|
+
sage: while W.rmessages is not None:
|
2354
|
+
....: W.read_messages()
|
2355
|
+
sage: W.join()
|
2356
|
+
sage: len(W.messages) > 0
|
2357
|
+
True
|
2358
|
+
"""
|
2359
|
+
# It's absolutely important to execute only one read() system
|
2360
|
+
# call, more might block. Assuming that we used pselect()
|
2361
|
+
# correctly, one read() will not block.
|
2362
|
+
if self.rmessages is not None:
|
2363
|
+
s = os.read(self.rmessages, 4096)
|
2364
|
+
self.messages += bytes_to_str(s)
|
2365
|
+
if len(s) == 0: # EOF
|
2366
|
+
os.close(self.rmessages)
|
2367
|
+
self.rmessages = None
|
2368
|
+
|
2369
|
+
def save_result_output(self):
|
2370
|
+
"""
|
2371
|
+
Annotate ``self`` with ``self.result`` (the result read through
|
2372
|
+
the ``result_queue`` and with ``self.output``, the complete
|
2373
|
+
contents of ``self.outtmpfile``. Then close the Queue and
|
2374
|
+
``self.outtmpfile``.
|
2375
|
+
|
2376
|
+
EXAMPLES::
|
2377
|
+
|
2378
|
+
sage: # needs sage.all
|
2379
|
+
sage: from sage.doctest.forker import DocTestWorker, DocTestTask
|
2380
|
+
sage: from sage.doctest.sources import FileDocTestSource
|
2381
|
+
sage: from sage.doctest.reporting import DocTestReporter
|
2382
|
+
sage: from sage.doctest.control import DocTestController, DocTestDefaults
|
2383
|
+
sage: filename = sage.doctest.util.__file__
|
2384
|
+
sage: DD = DocTestDefaults()
|
2385
|
+
sage: FDS = FileDocTestSource(filename, DD)
|
2386
|
+
sage: W = DocTestWorker(FDS, DD)
|
2387
|
+
sage: W.start()
|
2388
|
+
sage: W.join()
|
2389
|
+
sage: W.save_result_output()
|
2390
|
+
sage: sorted(W.result[1].keys())
|
2391
|
+
['cputime', 'err', 'failures', 'optionals', 'tests', 'walltime', 'walltime_skips']
|
2392
|
+
sage: len(W.output) > 0
|
2393
|
+
True
|
2394
|
+
|
2395
|
+
.. NOTE::
|
2396
|
+
|
2397
|
+
This method is called from the parent process, not from the
|
2398
|
+
subprocess.
|
2399
|
+
"""
|
2400
|
+
try:
|
2401
|
+
self.result = self.result_queue.get(block=False)
|
2402
|
+
except Empty:
|
2403
|
+
self.result = (0, DictAsObject({'err': 'noresult'}))
|
2404
|
+
del self.result_queue
|
2405
|
+
|
2406
|
+
self.outtmpfile.seek(0)
|
2407
|
+
self.output = bytes_to_str(self.outtmpfile.read())
|
2408
|
+
self.outtmpfile.close()
|
2409
|
+
try:
|
2410
|
+
# Now it is safe to delete the outtmpfile; we manage this manually
|
2411
|
+
# so that the file does not get deleted via TemporaryFile.__del__
|
2412
|
+
# in the worker process
|
2413
|
+
os.unlink(self.outtmpfile.name)
|
2414
|
+
except OSError as exc:
|
2415
|
+
if exc.errno != errno.ENOENT:
|
2416
|
+
raise
|
2417
|
+
|
2418
|
+
del self.outtmpfile
|
2419
|
+
|
2420
|
+
def kill(self):
|
2421
|
+
"""
|
2422
|
+
Kill this worker. Return ``True`` if the signal(s) are sent
|
2423
|
+
successfully or ``False`` if the worker process no longer exists.
|
2424
|
+
|
2425
|
+
This method is only called if there is something wrong with the
|
2426
|
+
worker. Under normal circumstances, the worker is supposed to
|
2427
|
+
exit by itself after finishing.
|
2428
|
+
|
2429
|
+
The first time this is called, use ``SIGQUIT``. This will trigger
|
2430
|
+
the cysignals ``SIGQUIT`` handler and try to print an enhanced
|
2431
|
+
traceback.
|
2432
|
+
|
2433
|
+
Subsequent times, use ``SIGKILL``. Also close the message pipe
|
2434
|
+
if it was still open.
|
2435
|
+
|
2436
|
+
EXAMPLES::
|
2437
|
+
|
2438
|
+
sage: import time
|
2439
|
+
sage: from sage.doctest.forker import DocTestWorker, DocTestTask
|
2440
|
+
sage: from sage.doctest.sources import FileDocTestSource
|
2441
|
+
sage: from sage.doctest.reporting import DocTestReporter
|
2442
|
+
sage: from sage.doctest.control import DocTestController, DocTestDefaults
|
2443
|
+
sage: filename = os.path.join(SAGE_SRC,'sage','doctest','tests','99seconds.rst')
|
2444
|
+
sage: DD = DocTestDefaults()
|
2445
|
+
sage: FDS = FileDocTestSource(filename, DD)
|
2446
|
+
|
2447
|
+
We set up the worker to start by blocking ``SIGQUIT``, such that
|
2448
|
+
killing will fail initially::
|
2449
|
+
|
2450
|
+
sage: # needs sage.all
|
2451
|
+
sage: from cysignals.pselect import PSelecter
|
2452
|
+
sage: import signal
|
2453
|
+
sage: def block_hup():
|
2454
|
+
....: # We never __exit__()
|
2455
|
+
....: PSelecter([signal.SIGQUIT]).__enter__()
|
2456
|
+
sage: W = DocTestWorker(FDS, DD, [block_hup])
|
2457
|
+
sage: W.start()
|
2458
|
+
sage: W.killed
|
2459
|
+
False
|
2460
|
+
sage: W.kill()
|
2461
|
+
True
|
2462
|
+
sage: W.killed
|
2463
|
+
True
|
2464
|
+
sage: time.sleep(float(0.2)) # Worker doesn't die
|
2465
|
+
sage: W.kill() # Worker dies now
|
2466
|
+
True
|
2467
|
+
sage: time.sleep(1)
|
2468
|
+
sage: W.is_alive()
|
2469
|
+
False
|
2470
|
+
"""
|
2471
|
+
|
2472
|
+
if self.rmessages is not None:
|
2473
|
+
os.close(self.rmessages)
|
2474
|
+
self.rmessages = None
|
2475
|
+
|
2476
|
+
try:
|
2477
|
+
if not self.killed:
|
2478
|
+
self.killed = True
|
2479
|
+
os.killpg(self.pid, signal.SIGQUIT)
|
2480
|
+
else:
|
2481
|
+
os.killpg(self.pid, signal.SIGKILL)
|
2482
|
+
except OSError as exc:
|
2483
|
+
# Handle a race condition where the process has exited on
|
2484
|
+
# its own by the time we get here, and ESRCH is returned
|
2485
|
+
# indicating no processes in the specified process group
|
2486
|
+
if exc.errno != errno.ESRCH:
|
2487
|
+
raise
|
2488
|
+
|
2489
|
+
return False
|
2490
|
+
|
2491
|
+
return True
|
2492
|
+
|
2493
|
+
|
2494
|
+
class DocTestTask:
|
2495
|
+
"""
|
2496
|
+
This class encapsulates the tests from a single source.
|
2497
|
+
|
2498
|
+
This class does not insulate from problems in the source
|
2499
|
+
(e.g. entering an infinite loop or causing a segfault), that has to
|
2500
|
+
be dealt with at a higher level.
|
2501
|
+
|
2502
|
+
INPUT:
|
2503
|
+
|
2504
|
+
- ``source`` -- a :class:`sage.doctest.sources.DocTestSource` instance
|
2505
|
+
|
2506
|
+
- ``verbose`` -- boolean, controls reporting of progress by :class:`doctest.DocTestRunner`
|
2507
|
+
|
2508
|
+
EXAMPLES::
|
2509
|
+
|
2510
|
+
sage: # needs sage.all
|
2511
|
+
sage: from sage.doctest.forker import DocTestTask
|
2512
|
+
sage: from sage.doctest.sources import FileDocTestSource
|
2513
|
+
sage: from sage.doctest.control import DocTestDefaults, DocTestController
|
2514
|
+
sage: import os
|
2515
|
+
sage: filename = sage.doctest.sources.__file__
|
2516
|
+
sage: DD = DocTestDefaults()
|
2517
|
+
sage: FDS = FileDocTestSource(filename, DD)
|
2518
|
+
sage: DTT = DocTestTask(FDS)
|
2519
|
+
sage: DC = DocTestController(DD,[filename])
|
2520
|
+
sage: ntests, results = DTT(options=DD)
|
2521
|
+
sage: ntests >= 300 or ntests
|
2522
|
+
True
|
2523
|
+
sage: sorted(results.keys())
|
2524
|
+
['cputime', 'err', 'failures', 'optionals', 'tests', 'walltime', 'walltime_skips']
|
2525
|
+
"""
|
2526
|
+
|
2527
|
+
def __init__(self, source):
|
2528
|
+
"""
|
2529
|
+
Initialization.
|
2530
|
+
|
2531
|
+
TESTS::
|
2532
|
+
|
2533
|
+
sage: from sage.doctest.forker import DocTestTask
|
2534
|
+
sage: from sage.doctest.sources import FileDocTestSource
|
2535
|
+
sage: from sage.doctest.control import DocTestDefaults
|
2536
|
+
sage: import os
|
2537
|
+
sage: filename = sage.doctest.sources.__file__
|
2538
|
+
sage: FDS = FileDocTestSource(filename, DocTestDefaults())
|
2539
|
+
sage: DocTestTask(FDS)
|
2540
|
+
<sage.doctest.forker.DocTestTask object at ...>
|
2541
|
+
"""
|
2542
|
+
self.source = source
|
2543
|
+
|
2544
|
+
def __call__(self, options, outtmpfile=None, msgfile=None, result_queue=None, *,
|
2545
|
+
baseline=None):
|
2546
|
+
"""
|
2547
|
+
Calling the task does the actual work of running the doctests.
|
2548
|
+
|
2549
|
+
INPUT:
|
2550
|
+
|
2551
|
+
- ``options`` -- an object representing doctest options
|
2552
|
+
|
2553
|
+
- ``outtmpfile`` -- a seekable file that's used by the doctest
|
2554
|
+
runner to redirect stdout and stderr of the doctests
|
2555
|
+
|
2556
|
+
- ``msgfile`` -- a file or pipe to send doctest messages about
|
2557
|
+
doctest failures (or all tests in verbose mode)
|
2558
|
+
|
2559
|
+
- ``result_queue`` -- an instance of :class:`multiprocessing.Queue`
|
2560
|
+
to store the doctest result. For testing, this can also be ``None``
|
2561
|
+
|
2562
|
+
- ``baseline`` -- dictionary, the ``baseline_stats`` value
|
2563
|
+
|
2564
|
+
OUTPUT:
|
2565
|
+
|
2566
|
+
- ``(doctests, result_dict)`` where ``doctests`` is the number of
|
2567
|
+
doctests and ``result_dict`` is a dictionary annotated with
|
2568
|
+
timings and error information.
|
2569
|
+
|
2570
|
+
- Also put ``(doctests, result_dict)`` onto the ``result_queue``
|
2571
|
+
if the latter isn't None.
|
2572
|
+
|
2573
|
+
EXAMPLES::
|
2574
|
+
|
2575
|
+
sage: # needs sage.all
|
2576
|
+
sage: from sage.doctest.forker import DocTestTask
|
2577
|
+
sage: from sage.doctest.sources import FileDocTestSource
|
2578
|
+
sage: from sage.doctest.control import DocTestDefaults, DocTestController
|
2579
|
+
sage: import os
|
2580
|
+
sage: filename = sage.doctest.parsing.__file__
|
2581
|
+
sage: DD = DocTestDefaults()
|
2582
|
+
sage: FDS = FileDocTestSource(filename, DD)
|
2583
|
+
sage: DTT = DocTestTask(FDS)
|
2584
|
+
sage: DC = DocTestController(DD, [filename])
|
2585
|
+
sage: ntests, runner = DTT(options=DD)
|
2586
|
+
sage: runner.failures
|
2587
|
+
0
|
2588
|
+
sage: ntests >= 200 or ntests
|
2589
|
+
True
|
2590
|
+
"""
|
2591
|
+
result = None
|
2592
|
+
try:
|
2593
|
+
runner = SageDocTestRunner(
|
2594
|
+
SageOutputChecker(),
|
2595
|
+
verbose=options.verbose,
|
2596
|
+
outtmpfile=outtmpfile,
|
2597
|
+
msgfile=msgfile,
|
2598
|
+
sage_options=options,
|
2599
|
+
optionflags=doctest.NORMALIZE_WHITESPACE | doctest.ELLIPSIS,
|
2600
|
+
baseline=baseline)
|
2601
|
+
runner.basename = self.source.basename
|
2602
|
+
runner.filename = self.source.path
|
2603
|
+
N = options.file_iterations
|
2604
|
+
results = DictAsObject({'walltime': [], 'cputime': [],
|
2605
|
+
'err': None, 'walltime_skips': 0})
|
2606
|
+
|
2607
|
+
# multiprocessing.Process instances don't run exit
|
2608
|
+
# functions, so we run the functions added by doctests
|
2609
|
+
# when exiting this context.
|
2610
|
+
with restore_atexit(run=True):
|
2611
|
+
for it in range(N):
|
2612
|
+
doctests, extras = self._run(runner, options, results)
|
2613
|
+
runner.summarize(options.verbose)
|
2614
|
+
if runner.update_results(results):
|
2615
|
+
break
|
2616
|
+
|
2617
|
+
if extras['tab']:
|
2618
|
+
results.err = 'tab'
|
2619
|
+
results.tab_linenos = extras['tab']
|
2620
|
+
if extras['line_number']:
|
2621
|
+
results.err = 'line_number'
|
2622
|
+
results.optionals = extras['optionals']
|
2623
|
+
# We subtract 1 to remove the sig_on_count() tests
|
2624
|
+
result = (sum(max(0, len(test.examples) - 1) for test in doctests),
|
2625
|
+
results)
|
2626
|
+
|
2627
|
+
except BaseException:
|
2628
|
+
exc_info = sys.exc_info()
|
2629
|
+
tb = "".join(traceback.format_exception(*exc_info))
|
2630
|
+
result = (0, DictAsObject({'err': exc_info[0], 'tb': tb}))
|
2631
|
+
|
2632
|
+
if result_queue is not None:
|
2633
|
+
result_queue.put(result, False)
|
2634
|
+
|
2635
|
+
return result
|
2636
|
+
|
2637
|
+
def _run(self, runner, options, results):
|
2638
|
+
"""
|
2639
|
+
Actually run the doctests with the right set of globals
|
2640
|
+
"""
|
2641
|
+
# Import Jupyter globals to doctest the Jupyter
|
2642
|
+
# implementation of widgets and interacts
|
2643
|
+
from importlib import import_module
|
2644
|
+
sage_all = import_module(options.environment)
|
2645
|
+
dict_all = sage_all.__dict__
|
2646
|
+
# When using global environments other than sage.all,
|
2647
|
+
# make sure startup is finished so we don't get "Resolving lazy import"
|
2648
|
+
# warnings.
|
2649
|
+
from sage.misc.lazy_import import ensure_startup_finished
|
2650
|
+
ensure_startup_finished()
|
2651
|
+
# Remove '__package__' item from the globals since it is not
|
2652
|
+
# always in the globals in an actual Sage session.
|
2653
|
+
dict_all.pop('__package__', None)
|
2654
|
+
sage_namespace = RecordingDict(dict_all)
|
2655
|
+
sage_namespace['__name__'] = '__main__'
|
2656
|
+
doctests, extras = self.source.create_doctests(sage_namespace)
|
2657
|
+
timer = Timer().start()
|
2658
|
+
|
2659
|
+
for test in doctests:
|
2660
|
+
result = runner.run(test)
|
2661
|
+
if options.exitfirst and result.failed:
|
2662
|
+
break
|
2663
|
+
|
2664
|
+
timer.stop().annotate(runner)
|
2665
|
+
return doctests, extras
|