passagemath-repl 10.4.62__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (162) hide show
  1. passagemath_repl-10.4.62.data/scripts/sage-cachegrind +25 -0
  2. passagemath_repl-10.4.62.data/scripts/sage-callgrind +16 -0
  3. passagemath_repl-10.4.62.data/scripts/sage-cleaner +230 -0
  4. passagemath_repl-10.4.62.data/scripts/sage-coverage +327 -0
  5. passagemath_repl-10.4.62.data/scripts/sage-eval +14 -0
  6. passagemath_repl-10.4.62.data/scripts/sage-fixdoctests +708 -0
  7. passagemath_repl-10.4.62.data/scripts/sage-inline-fortran +12 -0
  8. passagemath_repl-10.4.62.data/scripts/sage-ipynb2rst +50 -0
  9. passagemath_repl-10.4.62.data/scripts/sage-ipython +16 -0
  10. passagemath_repl-10.4.62.data/scripts/sage-massif +25 -0
  11. passagemath_repl-10.4.62.data/scripts/sage-notebook +267 -0
  12. passagemath_repl-10.4.62.data/scripts/sage-omega +25 -0
  13. passagemath_repl-10.4.62.data/scripts/sage-preparse +302 -0
  14. passagemath_repl-10.4.62.data/scripts/sage-run +27 -0
  15. passagemath_repl-10.4.62.data/scripts/sage-run-cython +10 -0
  16. passagemath_repl-10.4.62.data/scripts/sage-runtests +9 -0
  17. passagemath_repl-10.4.62.data/scripts/sage-startuptime.py +163 -0
  18. passagemath_repl-10.4.62.data/scripts/sage-valgrind +34 -0
  19. passagemath_repl-10.4.62.dist-info/METADATA +77 -0
  20. passagemath_repl-10.4.62.dist-info/RECORD +162 -0
  21. passagemath_repl-10.4.62.dist-info/WHEEL +5 -0
  22. passagemath_repl-10.4.62.dist-info/top_level.txt +1 -0
  23. sage/all__sagemath_repl.py +119 -0
  24. sage/doctest/__init__.py +4 -0
  25. sage/doctest/__main__.py +236 -0
  26. sage/doctest/all.py +4 -0
  27. sage/doctest/check_tolerance.py +261 -0
  28. sage/doctest/control.py +1727 -0
  29. sage/doctest/external.py +534 -0
  30. sage/doctest/fixtures.py +383 -0
  31. sage/doctest/forker.py +2665 -0
  32. sage/doctest/marked_output.py +102 -0
  33. sage/doctest/parsing.py +1708 -0
  34. sage/doctest/parsing_test.py +79 -0
  35. sage/doctest/reporting.py +733 -0
  36. sage/doctest/rif_tol.py +124 -0
  37. sage/doctest/sources.py +1657 -0
  38. sage/doctest/test.py +584 -0
  39. sage/doctest/tests/1second.rst +4 -0
  40. sage/doctest/tests/99seconds.rst +4 -0
  41. sage/doctest/tests/abort.rst +5 -0
  42. sage/doctest/tests/atexit.rst +7 -0
  43. sage/doctest/tests/fail_and_die.rst +6 -0
  44. sage/doctest/tests/initial.rst +15 -0
  45. sage/doctest/tests/interrupt.rst +7 -0
  46. sage/doctest/tests/interrupt_diehard.rst +14 -0
  47. sage/doctest/tests/keyboardinterrupt.rst +11 -0
  48. sage/doctest/tests/longtime.rst +5 -0
  49. sage/doctest/tests/nodoctest +5 -0
  50. sage/doctest/tests/random_seed.rst +4 -0
  51. sage/doctest/tests/show_skipped.rst +18 -0
  52. sage/doctest/tests/sig_on.rst +9 -0
  53. sage/doctest/tests/simple_failure.rst +8 -0
  54. sage/doctest/tests/sleep_and_raise.rst +106 -0
  55. sage/doctest/tests/tolerance.rst +31 -0
  56. sage/doctest/util.py +750 -0
  57. sage/interfaces/cleaner.py +48 -0
  58. sage/interfaces/quit.py +163 -0
  59. sage/misc/all__sagemath_repl.py +51 -0
  60. sage/misc/banner.py +235 -0
  61. sage/misc/benchmark.py +221 -0
  62. sage/misc/classgraph.py +131 -0
  63. sage/misc/copying.py +22 -0
  64. sage/misc/cython.py +694 -0
  65. sage/misc/dev_tools.py +745 -0
  66. sage/misc/edit_module.py +304 -0
  67. sage/misc/explain_pickle.py +3079 -0
  68. sage/misc/gperftools.py +361 -0
  69. sage/misc/inline_fortran.py +212 -0
  70. sage/misc/messaging.py +86 -0
  71. sage/misc/pager.py +21 -0
  72. sage/misc/profiler.py +179 -0
  73. sage/misc/python.py +70 -0
  74. sage/misc/remote_file.py +53 -0
  75. sage/misc/sage_eval.py +246 -0
  76. sage/misc/sage_input.py +3621 -0
  77. sage/misc/sagedoc.py +1742 -0
  78. sage/misc/sh.py +38 -0
  79. sage/misc/trace.py +90 -0
  80. sage/repl/__init__.py +16 -0
  81. sage/repl/all.py +15 -0
  82. sage/repl/attach.py +625 -0
  83. sage/repl/configuration.py +186 -0
  84. sage/repl/display/__init__.py +1 -0
  85. sage/repl/display/fancy_repr.py +354 -0
  86. sage/repl/display/formatter.py +318 -0
  87. sage/repl/display/jsmol_iframe.py +290 -0
  88. sage/repl/display/pretty_print.py +153 -0
  89. sage/repl/display/util.py +163 -0
  90. sage/repl/image.py +302 -0
  91. sage/repl/inputhook.py +91 -0
  92. sage/repl/interface_magic.py +298 -0
  93. sage/repl/interpreter.py +854 -0
  94. sage/repl/ipython_extension.py +593 -0
  95. sage/repl/ipython_kernel/__init__.py +1 -0
  96. sage/repl/ipython_kernel/__main__.py +4 -0
  97. sage/repl/ipython_kernel/all_jupyter.py +10 -0
  98. sage/repl/ipython_kernel/install.py +301 -0
  99. sage/repl/ipython_kernel/interact.py +278 -0
  100. sage/repl/ipython_kernel/kernel.py +217 -0
  101. sage/repl/ipython_kernel/widgets.py +466 -0
  102. sage/repl/ipython_kernel/widgets_sagenb.py +587 -0
  103. sage/repl/ipython_tests.py +163 -0
  104. sage/repl/load.py +326 -0
  105. sage/repl/preparse.py +2218 -0
  106. sage/repl/prompts.py +90 -0
  107. sage/repl/rich_output/__init__.py +4 -0
  108. sage/repl/rich_output/backend_base.py +648 -0
  109. sage/repl/rich_output/backend_doctest.py +316 -0
  110. sage/repl/rich_output/backend_emacs.py +151 -0
  111. sage/repl/rich_output/backend_ipython.py +596 -0
  112. sage/repl/rich_output/buffer.py +311 -0
  113. sage/repl/rich_output/display_manager.py +829 -0
  114. sage/repl/rich_output/example.avi +0 -0
  115. sage/repl/rich_output/example.canvas3d +1 -0
  116. sage/repl/rich_output/example.dvi +0 -0
  117. sage/repl/rich_output/example.flv +0 -0
  118. sage/repl/rich_output/example.gif +0 -0
  119. sage/repl/rich_output/example.jpg +0 -0
  120. sage/repl/rich_output/example.mkv +0 -0
  121. sage/repl/rich_output/example.mov +0 -0
  122. sage/repl/rich_output/example.mp4 +0 -0
  123. sage/repl/rich_output/example.ogv +0 -0
  124. sage/repl/rich_output/example.pdf +0 -0
  125. sage/repl/rich_output/example.png +0 -0
  126. sage/repl/rich_output/example.svg +54 -0
  127. sage/repl/rich_output/example.webm +0 -0
  128. sage/repl/rich_output/example.wmv +0 -0
  129. sage/repl/rich_output/example_jmol.spt.zip +0 -0
  130. sage/repl/rich_output/example_wavefront_scene.mtl +7 -0
  131. sage/repl/rich_output/example_wavefront_scene.obj +17 -0
  132. sage/repl/rich_output/output_basic.py +391 -0
  133. sage/repl/rich_output/output_browser.py +103 -0
  134. sage/repl/rich_output/output_catalog.py +54 -0
  135. sage/repl/rich_output/output_graphics.py +320 -0
  136. sage/repl/rich_output/output_graphics3d.py +345 -0
  137. sage/repl/rich_output/output_video.py +231 -0
  138. sage/repl/rich_output/preferences.py +432 -0
  139. sage/repl/rich_output/pretty_print.py +339 -0
  140. sage/repl/rich_output/test_backend.py +201 -0
  141. sage/repl/user_globals.py +214 -0
  142. sage/tests/__init__.py +1 -0
  143. sage/tests/all.py +3 -0
  144. sage/tests/article_heuberger_krenn_kropf_fsm-in-sage.py +630 -0
  145. sage/tests/arxiv_0812_2725.py +351 -0
  146. sage/tests/benchmark.py +1923 -0
  147. sage/tests/book_schilling_zabrocki_kschur_primer.py +795 -0
  148. sage/tests/book_stein_ent.py +651 -0
  149. sage/tests/book_stein_modform.py +558 -0
  150. sage/tests/cmdline.py +790 -0
  151. sage/tests/combinatorial_hopf_algebras.py +52 -0
  152. sage/tests/finite_poset.py +623 -0
  153. sage/tests/functools_partial_src.py +27 -0
  154. sage/tests/gosper-sum.py +218 -0
  155. sage/tests/lazy_imports.py +28 -0
  156. sage/tests/modular_group_cohomology.py +80 -0
  157. sage/tests/numpy.py +21 -0
  158. sage/tests/parigp.py +76 -0
  159. sage/tests/startup.py +27 -0
  160. sage/tests/symbolic-series.py +76 -0
  161. sage/tests/sympy.py +16 -0
  162. 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