This repository has been archived by the owner on Jan 30, 2023. It is now read-only.
-
-
Notifications
You must be signed in to change notification settings - Fork 7
/
forker.py
2385 lines (2022 loc) · 95.3 KB
/
forker.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
"""
Processes for running doctests
This module controls the processes started by Sage that actually run
the doctests.
EXAMPLES:
The following examples are used in doctesting this file::
sage: doctest_var = 42; doctest_var^2
1764
sage: R.<a> = ZZ[]
sage: a + doctest_var
a + 42
AUTHORS:
- David Roe (2012-03-27) -- initial version, based on Robert Bradshaw's code.
- Jeroen Demeyer (2013 and 2015) -- major improvements to forking and logging
"""
#*****************************************************************************
# Copyright (C) 2012 David Roe <roed.math@gmail.com>
# Robert Bradshaw <robertwb@gmail.com>
# William Stein <wstein@gmail.com>
# Copyright (C) 2013-2015 Jeroen Demeyer <jdemeyer@cage.ugent.be>
#
# Distributed under the terms of the GNU General Public License (GPL)
# as published by the Free Software Foundation; either version 2 of
# the License, or (at your option) any later version.
# http://www.gnu.org/licenses/
#*****************************************************************************
from __future__ import print_function
from __future__ import absolute_import
import __future__
import hashlib, multiprocessing, os, sys, time, warnings, signal, linecache
import errno
import doctest, traceback
import tempfile
import six
import sage.misc.randstate as randstate
from .util import Timer, RecordingDict, count_noun
from .sources import DictAsObject
from .parsing import OriginalSource, reduce_hex
from sage.structure.sage_object import SageObject
from .parsing import SageOutputChecker, pre_hash, get_source
from sage.repl.user_globals import set_globals
from sage.interfaces.process import ContainChildren
from sage.cpython.atexit import restore_atexit
from sage.cpython.string import bytes_to_str, str_to_bytes
# All doctests run as if the following future imports are present
MANDATORY_COMPILE_FLAGS = __future__.print_function.compiler_flag
def init_sage():
"""
Import the Sage library.
This function is called once at the beginning of a doctest run
(rather than once for each file). It imports the Sage library,
sets DOCTEST_MODE to True, and invalidates any interfaces.
EXAMPLES::
sage: from sage.doctest.forker import init_sage
sage: sage.doctest.DOCTEST_MODE = False
sage: init_sage()
sage: sage.doctest.DOCTEST_MODE
True
Check that pexpect interfaces are invalidated, but still work::
sage: gap.eval("my_test_var := 42;")
'42'
sage: gap.eval("my_test_var;")
'42'
sage: init_sage()
sage: gap('Group((1,2,3)(4,5), (3,4))')
Group( [ (1,2,3)(4,5), (3,4) ] )
sage: gap.eval("my_test_var;")
Traceback (most recent call last):
...
RuntimeError: Gap produced error output...
Check that SymPy equation pretty printer is limited in doctest
mode to default width (80 chars)::
sage: from sympy import sympify
sage: from sympy.printing.pretty.pretty import PrettyPrinter
sage: s = sympify('+x^'.join(str(i) for i in range(30)))
sage: print(PrettyPrinter(settings={'wrap_line':True}).doprint(s))
29 28 27 26 25 24 23 22 21 20 19 18 17
x + x + x + x + x + x + x + x + x + x + x + x + x +
<BLANKLINE>
16 15 14 13 12 11 10 9 8 7 6 5 4 3
x + x + x + x + x + x + x + x + x + x + x + x + x + x + x
<BLANKLINE>
2
+ x
The displayhook sorts dictionary keys to simplify doctesting of
dictionary output::
sage: {'a':23, 'b':34, 'au':56, 'bbf':234, 'aaa':234}
{'a': 23, 'aaa': 234, 'au': 56, 'b': 34, 'bbf': 234}
"""
# We need to ensure that the Matplotlib font cache is built to
# avoid spurious warnings (see Trac #20222).
import matplotlib.font_manager
# Make sure that the agg backend is selected during doctesting.
# This needs to be done before any other matplotlib calls.
matplotlib.use('agg')
# Do this once before forking off child processes running the tests.
# This is more efficient because we only need to wait once for the
# Sage imports.
import sage.doctest
sage.doctest.DOCTEST_MODE=True
import sage.all_cmdline
sage.interfaces.quit.invalidate_all()
# Disable cysignals debug messages in doctests: this is needed to
# make doctests pass when cysignals was built with debugging enabled
from cysignals.signals import set_debug_level
set_debug_level(0)
# Use the rich output backend for doctest
from sage.repl.rich_output import get_display_manager
dm = get_display_manager()
from sage.repl.rich_output.backend_doctest import BackendDoctest
dm.switch_backend(BackendDoctest())
# Switch on extra debugging
from sage.structure.debug_options import debug
debug.refine_category_hash_check = True
# We import readline before forking, otherwise Pdb doesn't work
# os OS X: http://trac.sagemath.org/14289
import readline
# Disable SymPy terminal width detection
from sympy.printing.pretty.stringpict import stringPict
stringPict.terminal_width = lambda self:0
def showwarning_with_traceback(message, category, filename, lineno, file=None, line=None):
r"""
Displays a warning message with a traceback.
INPUT: see :func:`warnings.showwarning`.
OUTPUT: None
EXAMPLES::
sage: from sage.doctest.forker import showwarning_with_traceback
sage: showwarning_with_traceback("bad stuff", UserWarning, "myfile.py", 0)
doctest:warning
...
File "<doctest sage.doctest.forker.showwarning_with_traceback[1]>", line 1, in <module>
showwarning_with_traceback("bad stuff", UserWarning, "myfile.py", Integer(0))
:
UserWarning: bad stuff
"""
# Get traceback to display in warning
tb = traceback.extract_stack()
tb = tb[:-1] # Drop this stack frame for showwarning_with_traceback()
# Format warning
lines = ["doctest:warning\n"] # Match historical warning messages in doctests
lines.extend(traceback.format_list(tb))
lines.append(":\n") # Match historical warning messages in doctests
lines.extend(traceback.format_exception_only(category, category(message)))
if file is None:
file = sys.stderr
try:
file.writelines(lines)
file.flush()
except IOError:
pass # the file is invalid
class SageSpoofInOut(SageObject):
r"""
We replace the standard :class:`doctest._SpoofOut` for three reasons:
- we need to divert the output of C programs that don't print
through sys.stdout,
- we want the ability to recover partial output from doctest
processes that segfault.
- we also redirect stdin (usually from /dev/null) during doctests.
This class defines streams ``self.real_stdin``, ``self.real_stdout``
and ``self.real_stderr`` which refer to the original streams.
INPUT:
- ``outfile`` -- (default: ``tempfile.TemporaryFile()``) a seekable open file
object to which stdout and stderr should be redirected.
- ``infile`` -- (default: ``open(os.devnull)``) an open file object
from which stdin should be redirected.
EXAMPLES::
sage: import subprocess, tempfile
sage: from sage.doctest.forker import SageSpoofInOut
sage: O = tempfile.TemporaryFile()
sage: S = SageSpoofInOut(O)
sage: try:
....: S.start_spoofing()
....: print("hello world")
....: finally:
....: S.stop_spoofing()
....:
sage: S.getvalue()
'hello world\n'
sage: _ = O.seek(0)
sage: S = SageSpoofInOut(outfile=sys.stdout, infile=O)
sage: try:
....: S.start_spoofing()
....: _ = subprocess.check_call("cat")
....: finally:
....: S.stop_spoofing()
....:
hello world
sage: O.close()
"""
def __init__(self, outfile=None, infile=None):
"""
Initialization.
TESTS::
sage: from tempfile import TemporaryFile
sage: from sage.doctest.forker import SageSpoofInOut
sage: with TemporaryFile() as outfile:
....: with TemporaryFile() as infile:
....: SageSpoofInOut(outfile, infile)
<sage.doctest.forker.SageSpoofInOut object at ...>
"""
if infile is None:
self.infile = open(os.devnull)
self._close_infile = True
else:
self.infile = infile
self._close_infile = False
if outfile is None:
self.outfile = tempfile.TemporaryFile()
self._close_outfile = True
else:
self.outfile = outfile
self._close_outfile = False
self.spoofing = False
self.real_stdin = os.fdopen(os.dup(sys.stdin.fileno()), "r")
self.real_stdout = os.fdopen(os.dup(sys.stdout.fileno()), "w")
self.real_stderr = os.fdopen(os.dup(sys.stderr.fileno()), "w")
self.position = 0
def __del__(self):
"""
Stop spoofing.
TESTS::
sage: from sage.doctest.forker import SageSpoofInOut
sage: spoof = SageSpoofInOut()
sage: spoof.start_spoofing()
sage: print("Spoofed!") # No output
sage: del spoof
sage: print("Not spoofed!")
Not spoofed!
"""
self.stop_spoofing()
if self._close_infile:
try:
self.infile.close()
except OSError:
pass
if self._close_outfile:
try:
self.outfile.close()
except OSError:
pass
for stream in ('stdin', 'stdout', 'stderr'):
try:
getattr(self, 'real_' + stream).close()
except OSError:
pass
def start_spoofing(self):
r"""
Set stdin to read from ``self.infile`` and stdout to print to
``self.outfile``.
EXAMPLES::
sage: import os, tempfile
sage: from sage.doctest.forker import SageSpoofInOut
sage: O = tempfile.TemporaryFile()
sage: S = SageSpoofInOut(O)
sage: try:
....: S.start_spoofing()
....: print("this is not printed")
....: finally:
....: S.stop_spoofing()
....:
sage: S.getvalue()
'this is not printed\n'
sage: _ = O.seek(0)
sage: S = SageSpoofInOut(infile=O)
sage: try:
....: S.start_spoofing()
....: v = sys.stdin.read()
....: finally:
....: S.stop_spoofing()
....:
sage: v
'this is not printed\n'
We also catch non-Python output::
sage: try:
....: S.start_spoofing()
....: retval = os.system('''echo "Hello there"\nif [ $? -eq 0 ]; then\necho "good"\nfi''')
....: finally:
....: S.stop_spoofing()
....:
sage: S.getvalue()
'Hello there\ngood\n'
sage: O.close()
"""
if not self.spoofing:
sys.stdout.flush()
sys.stderr.flush()
self.outfile.flush()
os.dup2(self.infile.fileno(), sys.stdin.fileno())
os.dup2(self.outfile.fileno(), sys.stdout.fileno())
os.dup2(self.outfile.fileno(), sys.stderr.fileno())
self.spoofing = True
def stop_spoofing(self):
"""
Reset stdin and stdout to their original values.
EXAMPLES::
sage: from sage.doctest.forker import SageSpoofInOut
sage: S = SageSpoofInOut()
sage: try:
....: S.start_spoofing()
....: print("this is not printed")
....: finally:
....: S.stop_spoofing()
....:
sage: print("this is now printed")
this is now printed
"""
if self.spoofing:
sys.stdout.flush()
sys.stderr.flush()
self.real_stdout.flush()
self.real_stderr.flush()
os.dup2(self.real_stdin.fileno(), sys.stdin.fileno())
os.dup2(self.real_stdout.fileno(), sys.stdout.fileno())
os.dup2(self.real_stderr.fileno(), sys.stderr.fileno())
self.spoofing = False
def getvalue(self):
r"""
Gets the value that has been printed to ``outfile`` since the
last time this function was called.
EXAMPLES::
sage: from sage.doctest.forker import SageSpoofInOut
sage: S = SageSpoofInOut()
sage: try:
....: S.start_spoofing()
....: print("step 1")
....: finally:
....: S.stop_spoofing()
....:
sage: S.getvalue()
'step 1\n'
sage: try:
....: S.start_spoofing()
....: print("step 2")
....: finally:
....: S.stop_spoofing()
....:
sage: S.getvalue()
'step 2\n'
"""
sys.stdout.flush()
self.outfile.seek(self.position)
result = self.outfile.read()
self.position = self.outfile.tell()
if not result.endswith(b"\n"):
result += b"\n"
return bytes_to_str(result)
class SageDocTestRunner(doctest.DocTestRunner):
def __init__(self, *args, **kwds):
"""
A customized version of DocTestRunner that tracks dependencies
of doctests.
INPUT:
- ``stdout`` -- an open file to restore for debugging
- ``checker`` -- None, or an instance of
:class:`doctest.OutputChecker`
- ``verbose`` -- boolean, determines whether verbose printing
is enabled.
- ``optionflags`` -- Controls the comparison with the expected
output. See :mod:`testmod` for more information.
EXAMPLES::
sage: from sage.doctest.parsing import SageOutputChecker
sage: from sage.doctest.forker import SageDocTestRunner
sage: from sage.doctest.control import DocTestDefaults; DD = DocTestDefaults()
sage: import doctest, sys, os
sage: DTR = SageDocTestRunner(SageOutputChecker(), verbose=False, sage_options=DD, optionflags=doctest.NORMALIZE_WHITESPACE|doctest.ELLIPSIS)
sage: DTR
<sage.doctest.forker.SageDocTestRunner instance at ...>
"""
O = kwds.pop('outtmpfile', None)
self.msgfile = kwds.pop('msgfile', None)
self.options = kwds.pop('sage_options')
doctest.DocTestRunner.__init__(self, *args, **kwds)
self._fakeout = SageSpoofInOut(O)
if self.msgfile is None:
self.msgfile = self._fakeout.real_stdout
self.history = []
self.references = []
self.setters = {}
self.running_global_digest = hashlib.md5()
def _run(self, test, compileflags, out):
"""
This function replaces :meth:`doctest.DocTestRunner.__run`.
It changes the following behavior:
- We call :meth:`SageDocTestRunner.execute` rather than just
exec
- We don't truncate _fakeout after each example since we want
the output file to be readable by the calling
:class:`SageWorker`.
Since it needs to be able to read stdout, it should be called
while spoofing using :class:`SageSpoofInOut`.
EXAMPLES::
sage: from sage.doctest.parsing import SageOutputChecker
sage: from sage.doctest.forker import SageDocTestRunner
sage: from sage.doctest.sources import FileDocTestSource
sage: from sage.doctest.control import DocTestDefaults; DD = DocTestDefaults()
sage: from sage.env import SAGE_SRC
sage: import doctest, sys, os
sage: DTR = SageDocTestRunner(SageOutputChecker(), verbose=False, sage_options=DD, optionflags=doctest.NORMALIZE_WHITESPACE|doctest.ELLIPSIS)
sage: filename = os.path.join(SAGE_SRC,'sage','doctest','forker.py')
sage: FDS = FileDocTestSource(filename,DD)
sage: doctests, extras = FDS.create_doctests(globals())
sage: DTR.run(doctests[0], clear_globs=False) # indirect doctest
TestResults(failed=0, attempted=4)
"""
# Ensure that injecting globals works as expected in doctests
set_globals(test.globs)
# Keep track of the number of failures and tries.
failures = tries = 0
quiet = False
# Save the option flags (since option directives can be used
# to modify them).
original_optionflags = self.optionflags
SUCCESS, FAILURE, BOOM = range(3) # `outcome` state
check = self._checker.check_output
# Process each example.
for examplenum, example in enumerate(test.examples):
if failures:
# If exitfirst is set, abort immediately after a
# failure.
if self.options.exitfirst:
break
# If REPORT_ONLY_FIRST_FAILURE is set, then suppress
# reporting after the first failure (but continue
# running the tests).
quiet |= (self.optionflags & doctest.REPORT_ONLY_FIRST_FAILURE)
# Merge in the example's options.
self.optionflags = original_optionflags
if example.options:
for (optionflag, val) in example.options.items():
if val:
self.optionflags |= optionflag
else:
self.optionflags &= ~optionflag
# If 'SKIP' is set, then skip this example.
if self.optionflags & doctest.SKIP:
continue
# Record that we started this example.
tries += 1
# We print the example we're running for easier debugging
# if this file times out or crashes.
with OriginalSource(example):
print("sage: " + example.source[:-1] + " ## line %s ##"%(test.lineno + example.lineno + 1))
# Update the position so that result comparison works
throwaway = self._fakeout.getvalue()
if not quiet:
self.report_start(out, test, example)
# Flush files before running the example, so we know for
# sure that everything is reported properly if the test
# crashes.
sys.stdout.flush()
sys.stderr.flush()
self.msgfile.flush()
# Use a special filename for compile(), so we can retrieve
# the source code during interactive debugging (see
# __patched_linecache_getlines).
filename = '<doctest %s[%d]>' % (test.name, examplenum)
# Run the example in the given context (globs), and record
# any exception that gets raised. But for SystemExit, we
# simply propagate the exception.
exception = None
try:
compiler = lambda example:compile(
example.source, filename, "single", compileflags, 1)
# Don't blink! This is where the user's code gets run.
self.compile_and_execute(example, compiler, test.globs)
except SystemExit:
raise
except BaseException:
exception = sys.exc_info()
finally:
if self.debugger is not None:
self.debugger.set_continue() # ==== Example Finished ====
got = self._fakeout.getvalue()
if not isinstance(got, six.text_type):
# On Python 3 got should already be unicode text, but on Python
# 2 it is not. For comparison's sake we want the unicode text
# decoded from UTF-8. If there was some error such that the
# output is so malformed that it does not even decode from
# UTF-8 at all there will be an error in the test framework
# here. But this shouldn't happen at all, so we want it to be
# understood as an error in the test framework, and not some
# subtle error in the code under test.
got = got.decode('utf-8')
outcome = FAILURE # guilty until proved innocent or insane
# If the example executed without raising any exceptions,
# verify its output.
if exception is None:
if check(example.want, got, self.optionflags):
outcome = SUCCESS
# The example raised an exception: check if it was expected.
else:
exc_info = exception
exc_msg = traceback.format_exception_only(*exc_info[:2])[-1]
if six.PY3 and example.exc_msg is not None:
# On Python 3 the exception repr often includes the
# exception's full module name (for non-builtin
# exceptions), whereas on Python 2 does not, so we
# normalize Python 3 exceptions to match tests written to
# Python 2
# See https://trac.sagemath.org/ticket/24271
exc_cls = exc_info[0]
exc_name = exc_cls.__name__
if exc_cls.__module__:
exc_fullname = (exc_cls.__module__ + '.' +
exc_cls.__qualname__)
else:
exc_fullname = exc_cls.__qualname__
# See
# https://docs.python.org/3/library/exceptions.html#OSError
oserror_aliases = ['IOError', 'EnvironmentError',
'socket.error', 'select.error',
'mmap.error']
if (example.exc_msg.startswith(exc_name) and
exc_msg.startswith(exc_fullname)):
exc_msg = exc_msg.replace(exc_fullname, exc_name, 1)
else:
# Special case: On Python 3 these exceptions are all
# just aliases for OSError
for alias in oserror_aliases:
if example.exc_msg.startswith(alias + ':'):
exc_msg = exc_msg.replace('OSError', alias, 1)
break
if not quiet:
got += doctest._exception_traceback(exc_info)
# If `example.exc_msg` is None, then we weren't expecting
# an exception.
if example.exc_msg is None:
outcome = BOOM
# We expected an exception: see whether it matches.
elif check(example.exc_msg, exc_msg, self.optionflags):
outcome = SUCCESS
# Another chance if they didn't care about the detail.
elif self.optionflags & doctest.IGNORE_EXCEPTION_DETAIL:
m1 = re.match(r'(?:[^:]*\.)?([^:]*:)', example.exc_msg)
m2 = re.match(r'(?:[^:]*\.)?([^:]*:)', exc_msg)
if m1 and m2 and check(m1.group(1), m2.group(1),
self.optionflags):
outcome = SUCCESS
# Report the outcome.
if outcome is SUCCESS:
if self.options.warn_long and example.walltime > self.options.warn_long:
self.report_overtime(out, test, example, got)
elif not quiet:
self.report_success(out, test, example, got)
elif outcome is FAILURE:
if not quiet:
self.report_failure(out, test, example, got, test.globs)
failures += 1
elif outcome is BOOM:
if not quiet:
self.report_unexpected_exception(out, test, example,
exc_info)
failures += 1
else:
assert False, ("unknown outcome", outcome)
# Restore the option flags (in case they were modified)
self.optionflags = original_optionflags
# Record and return the number of failures and tries.
self._DocTestRunner__record_outcome(test, failures, tries)
return doctest.TestResults(failures, tries)
def run(self, test, compileflags=None, out=None, clear_globs=True):
"""
Runs the examples in a given doctest.
This function replaces :class:`doctest.DocTestRunner.run`
since it needs to handle spoofing. It also leaves the display
hook in place.
INPUT:
- ``test`` -- an instance of :class:`doctest.DocTest`
- ``compileflags`` -- the set of compiler flags used to
execute examples (passed in to the :func:`compile`). If
None, they are filled in from the result of
:func:`doctest._extract_future_flags` applied to
``test.globs``.
- ``out`` -- a function for writing the output (defaults to
:func:`sys.stdout.write`).
- ``clear_globs`` -- boolean (default True): whether to clear
the namespace after running this doctest.
OUTPUT:
- ``f`` -- integer, the number of examples that failed
- ``t`` -- the number of examples tried
EXAMPLES::
sage: from sage.doctest.parsing import SageOutputChecker
sage: from sage.doctest.forker import SageDocTestRunner
sage: from sage.doctest.sources import FileDocTestSource
sage: from sage.doctest.control import DocTestDefaults; DD = DocTestDefaults()
sage: from sage.env import SAGE_SRC
sage: import doctest, sys, os
sage: DTR = SageDocTestRunner(SageOutputChecker(), verbose=False, sage_options=DD, optionflags=doctest.NORMALIZE_WHITESPACE|doctest.ELLIPSIS)
sage: filename = os.path.join(SAGE_SRC,'sage','doctest','forker.py')
sage: FDS = FileDocTestSource(filename,DD)
sage: doctests, extras = FDS.create_doctests(globals())
sage: DTR.run(doctests[0], clear_globs=False)
TestResults(failed=0, attempted=4)
"""
self.setters = {}
randstate.set_random_seed(0)
warnings.showwarning = showwarning_with_traceback
self.running_doctest_digest = hashlib.md5()
self.test = test
if compileflags is None:
compileflags = doctest._extract_future_flags(test.globs)
compileflags |= MANDATORY_COMPILE_FLAGS
# We use this slightly modified version of Pdb because it
# interacts better with the doctesting framework (like allowing
# doctests for sys.settrace()). Since we already have output
# spoofing in place, there is no need for redirection.
if self.options.debug:
self.debugger = doctest._OutputRedirectingPdb(sys.stdout)
self.debugger.reset()
else:
self.debugger = None
self.save_linecache_getlines = linecache.getlines
linecache.getlines = self._DocTestRunner__patched_linecache_getlines
if out is None:
def out(s):
self.msgfile.write(s)
self.msgfile.flush()
self._fakeout.start_spoofing()
# If self.options.initial is set, we show only the first failure in each doctest block.
self.no_failure_yet = True
try:
return self._run(test, compileflags, out)
finally:
self._fakeout.stop_spoofing()
linecache.getlines = self.save_linecache_getlines
if clear_globs:
test.globs.clear()
def summarize(self, verbose=None):
"""
Print results of testing to ``self.msgfile`` and return number
of failures and tests run.
INPUT:
- ``verbose`` -- whether to print lots of stuff
OUTPUT:
- returns ``(f, t)``, a :class:`doctest.TestResults` instance
giving the number of failures and the total number of tests
run.
EXAMPLES::
sage: from sage.doctest.parsing import SageOutputChecker
sage: from sage.doctest.forker import SageDocTestRunner
sage: from sage.doctest.control import DocTestDefaults; DD = DocTestDefaults()
sage: import doctest, sys, os
sage: DTR = SageDocTestRunner(SageOutputChecker(), verbose=False, sage_options=DD, optionflags=doctest.NORMALIZE_WHITESPACE|doctest.ELLIPSIS)
sage: DTR._name2ft['sage.doctest.forker'] = (1,120)
sage: results = DTR.summarize()
**********************************************************************
1 item had failures:
1 of 120 in sage.doctest.forker
sage: results
TestResults(failed=1, attempted=120)
"""
if verbose is None:
verbose = self._verbose
m = self.msgfile
notests = []
passed = []
failed = []
totalt = totalf = 0
for x in self._name2ft.items():
name, (f, t) = x
assert f <= t
totalt += t
totalf += f
if not t:
notests.append(name)
elif not f:
passed.append( (name, t) )
else:
failed.append(x)
if verbose:
if notests:
print(count_noun(len(notests), "item"), "had no tests:", file=m)
notests.sort()
for thing in notests:
print(" %s"%thing, file=m)
if passed:
print(count_noun(len(passed), "item"), "passed all tests:", file=m)
passed.sort()
for thing, count in passed:
print(" %s in %s"%(count_noun(count, "test", pad_number=3, pad_noun=True), thing), file=m)
if failed:
print(self.DIVIDER, file=m)
print(count_noun(len(failed), "item"), "had failures:", file=m)
failed.sort()
for thing, (f, t) in failed:
print(" %3d of %3d in %s"%(f, t, thing), file=m)
if verbose:
print(count_noun(totalt, "test") + " in " + count_noun(len(self._name2ft), "item") + ".", file=m)
print("%s passed and %s failed."%(totalt - totalf, totalf), file=m)
if totalf:
print("***Test Failed***", file=m)
else:
print("Test passed.", file=m)
m.flush()
return doctest.TestResults(totalf, totalt)
def update_digests(self, example):
"""
Update global and doctest digests.
Sage's doctest runner tracks the state of doctests so that
their dependencies are known. For example, in the following
two lines ::
sage: R.<x> = ZZ[]
sage: f = x^2 + 1
it records that the second line depends on the first since the
first INSERTS ``x`` into the global namespace and the second
line RETRIEVES ``x`` from the global namespace.
This function updates the hashes that record these
dependencies.
INPUT:
- ``example`` -- a :class:`doctest.Example` instance
EXAMPLES::
sage: from sage.doctest.parsing import SageOutputChecker
sage: from sage.doctest.forker import SageDocTestRunner
sage: from sage.doctest.sources import FileDocTestSource
sage: from sage.doctest.control import DocTestDefaults; DD = DocTestDefaults()
sage: from sage.env import SAGE_SRC
sage: import doctest, sys, os, hashlib
sage: DTR = SageDocTestRunner(SageOutputChecker(), verbose=False, sage_options=DD, optionflags=doctest.NORMALIZE_WHITESPACE|doctest.ELLIPSIS)
sage: filename = os.path.join(SAGE_SRC,'sage','doctest','forker.py')
sage: FDS = FileDocTestSource(filename,DD)
sage: doctests, extras = FDS.create_doctests(globals())
sage: DTR.running_global_digest.hexdigest()
'd41d8cd98f00b204e9800998ecf8427e'
sage: DTR.running_doctest_digest = hashlib.md5()
sage: ex = doctests[0].examples[0]; ex.predecessors = None
sage: DTR.update_digests(ex)
sage: DTR.running_global_digest.hexdigest()
'3cb44104292c3a3ab4da3112ce5dc35c'
"""
s = str_to_bytes(pre_hash(get_source(example)), 'utf-8')
self.running_global_digest.update(s)
self.running_doctest_digest.update(s)
if example.predecessors is not None:
digest = hashlib.md5(s)
gen = (e.running_state for e in example.predecessors)
digest.update(str_to_bytes(reduce_hex(gen), 'ascii'))
example.running_state = digest.hexdigest()
def compile_and_execute(self, example, compiler, globs):
"""
Runs the given example, recording dependencies.
Rather than using a basic dictionary, Sage's doctest runner
uses a :class:`sage.doctest.util.RecordingDict`, which records
every time a value is set or retrieved. Executing the given
code with this recording dictionary as the namespace allows
Sage to track dependencies between doctest lines. For
example, in the following two lines ::
sage: R.<x> = ZZ[]
sage: f = x^2 + 1
the recording dictionary records that the second line depends
on the first since the first INSERTS ``x`` into the global
namespace and the second line RETRIEVES ``x`` from the global
namespace.
INPUT:
- ``example`` -- a :class:`doctest.Example` instance.
- ``compiler`` -- a callable that, applied to example,
produces a code object
- ``globs`` -- a dictionary in which to execute the code.
OUTPUT:
- the output of the compiled code snippet.
EXAMPLES::
sage: from sage.doctest.parsing import SageOutputChecker
sage: from sage.doctest.forker import SageDocTestRunner
sage: from sage.doctest.sources import FileDocTestSource
sage: from sage.doctest.util import RecordingDict
sage: from sage.doctest.control import DocTestDefaults; DD = DocTestDefaults()
sage: from sage.env import SAGE_SRC
sage: import doctest, sys, os, hashlib
sage: DTR = SageDocTestRunner(SageOutputChecker(), verbose=False, sage_options=DD, optionflags=doctest.NORMALIZE_WHITESPACE|doctest.ELLIPSIS)
sage: DTR.running_doctest_digest = hashlib.md5()
sage: filename = os.path.join(SAGE_SRC,'sage','doctest','forker.py')
sage: FDS = FileDocTestSource(filename,DD)
sage: globs = RecordingDict(globals())
sage: 'doctest_var' in globs
False
sage: doctests, extras = FDS.create_doctests(globs)
sage: ex0 = doctests[0].examples[0]
sage: compiler = lambda ex: compile(ex.source, '<doctest sage.doctest.forker[0]>', 'single', 32768, 1)
sage: DTR.compile_and_execute(ex0, compiler, globs)
1764
sage: globs['doctest_var']
42
sage: globs.set
{'doctest_var'}
sage: globs.got
{'Integer'}
Now we can execute some more doctests to see the dependencies. ::
sage: ex1 = doctests[0].examples[1]
sage: compiler = lambda ex:compile(ex.source, '<doctest sage.doctest.forker[1]>', 'single', 32768, 1)
sage: DTR.compile_and_execute(ex1, compiler, globs)
sage: sorted(list(globs.set))
['R', 'a']
sage: globs.got
{'ZZ'}
sage: ex1.predecessors
[]
::
sage: ex2 = doctests[0].examples[2]
sage: compiler = lambda ex:compile(ex.source, '<doctest sage.doctest.forker[2]>', 'single', 32768, 1)
sage: DTR.compile_and_execute(ex2, compiler, globs)
a + 42
sage: list(globs.set)
[]
sage: sorted(list(globs.got))
['a', 'doctest_var']
sage: set(ex2.predecessors) == set([ex0,ex1])
True
"""
if isinstance(globs, RecordingDict):
globs.start()
example.sequence_number = len(self.history)
self.history.append(example)
timer = Timer().start()
try:
compiled = compiler(example)
timer.start() # reset timer
exec(compiled, globs)
finally:
timer.stop().annotate(example)
if isinstance(globs, RecordingDict):
example.predecessors = []
for name in globs.got:
ref = self.setters.get(name)
if ref is not None:
example.predecessors.append(ref)
for name in globs.set:
self.setters[name] = example
else:
example.predecessors = None
self.update_digests(example)
example.total_state = self.running_global_digest.hexdigest()
example.doctest_state = self.running_doctest_digest.hexdigest()
def _failure_header(self, test, example, message='Failed example:'):
"""
We strip out ``sage:`` prompts, so we override
:meth:`doctest.DocTestRunner._failure_header` for better
reporting.
INPUT:
- ``test`` -- a :class:`doctest.DocTest` instance
- ``example`` -- a :class:`doctest.Example` instance in ``test``.
OUTPUT:
- a string used for reporting that the given example failed.
EXAMPLES::