Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemath
GitHub Repository: sagemath/sagesmc
Path: blob/master/src/sage/doctest/control.py
8817 views
1
"""
2
Classes involved in doctesting
3
4
This module controls the various classes involved in doctesting.
5
6
AUTHORS:
7
8
- David Roe (2012-03-27) -- initial version, based on Robert Bradshaw's code.
9
"""
10
11
#*****************************************************************************
12
# Copyright (C) 2012 David Roe <[email protected]>
13
# Robert Bradshaw <[email protected]>
14
# William Stein <[email protected]>
15
#
16
# Distributed under the terms of the GNU General Public License (GPL)
17
# as published by the Free Software Foundation; either version 2 of
18
# the License, or (at your option) any later version.
19
# http://www.gnu.org/licenses/
20
#*****************************************************************************
21
22
import random, os, sys, time, json, re, types
23
import sage.misc.flatten
24
from sage.structure.sage_object import SageObject
25
from sage.env import DOT_SAGE, SAGE_LIB, SAGE_SRC
26
from sage.ext.c_lib import AlarmInterrupt, _init_csage
27
28
from sources import FileDocTestSource, DictAsObject
29
from forker import DocTestDispatcher
30
from reporting import DocTestReporter
31
from util import NestedName, Timer, count_noun, dict_difference
32
33
nodoctest_regex = re.compile(r'\s*(#+|%+|r"+|"+|\.\.)\s*nodoctest')
34
optionaltag_regex = re.compile(r'^\w+$')
35
36
class DocTestDefaults(SageObject):
37
"""
38
This class is used for doctesting the Sage doctest module.
39
40
It fills in attributes to be the same as the defaults defined in
41
``SAGE_LOCAL/bin/sage-runtests``, expect for a few places,
42
which is mostly to make doctesting more predictable.
43
44
EXAMPLES::
45
46
sage: from sage.doctest.control import DocTestDefaults
47
sage: D = DocTestDefaults()
48
sage: D
49
DocTestDefaults()
50
sage: D.timeout
51
-1
52
53
Keyword arguments become attributes::
54
55
sage: D = DocTestDefaults(timeout=100)
56
sage: D
57
DocTestDefaults(timeout=100)
58
sage: D.timeout
59
100
60
"""
61
def __init__(self, **kwds):
62
"""
63
Edit these parameters after creating an instance.
64
65
EXAMPLES::
66
67
sage: from sage.doctest.control import DocTestDefaults
68
sage: D = DocTestDefaults(); D.optional
69
set(['sage'])
70
"""
71
self.nthreads = 1
72
self.serial = False
73
self.timeout = -1
74
self.all = False
75
self.logfile = None
76
self.sagenb = False
77
self.long = False
78
self.warn_long = None
79
self.optional = set(["sage"])
80
self.randorder = None
81
self.global_iterations = 1 # sage-runtests default is 0
82
self.file_iterations = 1 # sage-runtests default is 0
83
self.initial = False
84
self.force_lib = False
85
self.abspath = True # sage-runtests default is False
86
self.verbose = False
87
self.debug = False
88
self.gdb = False
89
self.valgrind = False
90
self.massif = False
91
self.cachegrind = False
92
self.omega = False
93
self.failed = False
94
self.new = False
95
self.show_skipped = False
96
# We don't want to use the real stats file by default so that
97
# we don't overwrite timings for the actual running doctests.
98
self.stats_path = os.path.join(DOT_SAGE, "timings_dt_test.json")
99
self.__dict__.update(kwds)
100
101
def _repr_(self):
102
"""
103
Return the print representation.
104
105
EXAMPLES::
106
107
sage: from sage.doctest.control import DocTestDefaults
108
sage: DocTestDefaults(timeout=100, foobar="hello")
109
DocTestDefaults(foobar='hello', timeout=100)
110
"""
111
s = "DocTestDefaults("
112
for k in sorted(dict_difference(self.__dict__, DocTestDefaults().__dict__).keys()):
113
if s[-1] != "(":
114
s += ", "
115
s += str(k) + "=" + repr(getattr(self,k))
116
s += ")"
117
return s
118
119
def __cmp__(self, other):
120
"""
121
Comparison by __dict__.
122
123
EXAMPLES::
124
125
sage: from sage.doctest.control import DocTestDefaults
126
sage: DD1 = DocTestDefaults(long=True)
127
sage: DD2 = DocTestDefaults(long=True)
128
sage: DD1 == DD2
129
True
130
"""
131
c = cmp(type(self), type(other))
132
if c: return c
133
return cmp(self.__dict__,other.__dict__)
134
135
136
def skipdir(dirname):
137
"""
138
Return True if and only if the directory ``dirname`` should not be
139
doctested.
140
141
EXAMPLES::
142
143
sage: from sage.doctest.control import skipdir
144
sage: skipdir(sage.env.SAGE_SRC)
145
False
146
sage: skipdir(os.path.join(sage.env.SAGE_SRC, "sage", "doctest", "tests"))
147
True
148
"""
149
if os.path.exists(os.path.join(dirname, "nodoctest.py")):
150
return True
151
return False
152
153
def skipfile(filename):
154
"""
155
Return True if and only if the file ``filename`` should not be
156
doctested.
157
158
EXAMPLES::
159
160
sage: from sage.doctest.control import skipfile
161
sage: skipfile("skipme.c")
162
True
163
sage: f = tmp_filename(ext=".pyx")
164
sage: skipfile(f)
165
False
166
sage: open(f, "w").write("# nodoctest")
167
sage: skipfile(f)
168
True
169
"""
170
base, ext = os.path.splitext(filename)
171
if ext not in ('.py', '.pyx', '.pxi', '.sage', '.spyx', '.rst', '.tex'):
172
return True
173
with open(filename) as F:
174
line_count = 0
175
for line in F:
176
if nodoctest_regex.match(line):
177
return True
178
line_count += 1
179
if line_count >= 10:
180
break
181
return False
182
183
184
class DocTestController(SageObject):
185
"""
186
This class controls doctesting of files.
187
188
After creating it with appropriate options, call the :meth:`run` method to run the doctests.
189
"""
190
def __init__(self, options, args):
191
"""
192
Initialization.
193
194
INPUT:
195
196
- options -- either options generated from the command line by SAGE_LOCAL/bin/sage-runtests
197
or a DocTestDefaults object (possibly with some entries modified)
198
- args -- a list of filenames to doctest
199
200
EXAMPLES::
201
202
sage: from sage.doctest.control import DocTestDefaults, DocTestController
203
sage: DC = DocTestController(DocTestDefaults(), [])
204
sage: DC
205
DocTest Controller
206
"""
207
# First we modify options to take environment variables into
208
# account and check compatibility of the user's specified
209
# options.
210
if options.timeout < 0:
211
if options.gdb or options.debug:
212
# Interactive debuggers: "infinite" timeout
213
options.timeout = 0
214
elif options.valgrind or options.massif or options.cachegrind or options.omega:
215
# Non-interactive debuggers: 48 hours
216
options.timeout = int(os.getenv('SAGE_TIMEOUT_VALGRIND', 48 * 60 * 60))
217
elif options.long:
218
options.timeout = int(os.getenv('SAGE_TIMEOUT_LONG', 30 * 60))
219
else:
220
options.timeout = int(os.getenv('SAGE_TIMEOUT', 5 * 60))
221
if options.nthreads == 0:
222
options.nthreads = int(os.getenv('SAGE_NUM_THREADS_PARALLEL',1))
223
if options.failed and not (args or options.new or options.sagenb):
224
# If the user doesn't specify any files then we rerun all failed files.
225
options.all = True
226
if options.global_iterations == 0:
227
options.global_iterations = int(os.environ.get('SAGE_TEST_GLOBAL_ITER', 1))
228
if options.file_iterations == 0:
229
options.file_iterations = int(os.environ.get('SAGE_TEST_ITER', 1))
230
if options.debug and options.nthreads > 1:
231
print("Debugging requires single-threaded operation, setting number of threads to 1.")
232
options.nthreads = 1
233
if options.serial:
234
options.nthreads = 1
235
if options.verbose:
236
options.show_skipped = True
237
238
if isinstance(options.optional, basestring):
239
s = options.optional.lower()
240
if s in ['all', 'true']:
241
options.optional = True
242
else:
243
options.optional = set(s.split(','))
244
# Check that all tags are valid
245
for o in options.optional:
246
if not optionaltag_regex.search(o):
247
raise ValueError('invalid optional tag %s'%repr(o))
248
249
self.options = options
250
self.files = args
251
if options.logfile:
252
try:
253
self.logfile = open(options.logfile, 'a')
254
except IOError:
255
print "Unable to open logfile at %s\nProceeding without logging."%(options.logfile)
256
self.logfile = None
257
else:
258
self.logfile = None
259
self.stats = {}
260
self.load_stats(options.stats_path)
261
262
def _repr_(self):
263
"""
264
String representation.
265
266
EXAMPLES::
267
268
sage: from sage.doctest.control import DocTestDefaults, DocTestController
269
sage: DC = DocTestController(DocTestDefaults(), [])
270
sage: repr(DC) # indirect doctest
271
'DocTest Controller'
272
"""
273
return "DocTest Controller"
274
275
def load_stats(self, filename):
276
"""
277
Load stats from the most recent run(s).
278
279
Stats are stored as a JSON file, and include information on
280
which files failed tests and the walltime used for execution
281
of the doctests.
282
283
EXAMPLES::
284
285
sage: from sage.doctest.control import DocTestDefaults, DocTestController
286
sage: DC = DocTestController(DocTestDefaults(), [])
287
sage: import json
288
sage: filename = tmp_filename()
289
sage: with open(filename, 'w') as stats_file:
290
... json.dump({'sage.doctest.control':{u'walltime':1.0r}}, stats_file)
291
sage: DC.load_stats(filename)
292
sage: DC.stats['sage.doctest.control']
293
{u'walltime': 1.0}
294
295
If the file doesn't exist, nothing happens. If there is an
296
error, print a message. In any case, leave the stats alone::
297
298
sage: d = tmp_dir()
299
sage: DC.load_stats(os.path.join(d)) # Cannot read a directory
300
Error loading stats from ...
301
sage: DC.load_stats(os.path.join(d, "no_such_file"))
302
sage: DC.stats['sage.doctest.control']
303
{u'walltime': 1.0}
304
"""
305
# Simply ignore non-existing files
306
if not os.path.exists(filename):
307
return
308
309
try:
310
with open(filename) as stats_file:
311
self.stats.update(json.load(stats_file))
312
except Exception:
313
self.log("Error loading stats from %s"%filename)
314
315
def save_stats(self, filename):
316
"""
317
Save stats from the most recent run as a JSON file.
318
319
WARNING: This function overwrites the file.
320
321
EXAMPLES::
322
323
sage: from sage.doctest.control import DocTestDefaults, DocTestController
324
sage: DC = DocTestController(DocTestDefaults(), [])
325
sage: DC.stats['sage.doctest.control'] = {u'walltime':1.0r}
326
sage: filename = tmp_filename()
327
sage: DC.save_stats(filename)
328
sage: import json
329
sage: D = json.load(open(filename))
330
sage: D['sage.doctest.control']
331
{u'walltime': 1.0}
332
"""
333
from sage.misc.temporary_file import atomic_write
334
with atomic_write(filename) as stats_file:
335
json.dump(self.stats, stats_file)
336
337
338
def log(self, s, end="\n"):
339
"""
340
Logs the string ``s + end`` (where ``end`` is a newline by default)
341
to the logfile and prints it to the standard output.
342
343
EXAMPLES::
344
345
sage: from sage.doctest.control import DocTestDefaults, DocTestController
346
sage: DD = DocTestDefaults(logfile=tmp_filename())
347
sage: DC = DocTestController(DD, [])
348
sage: DC.log("hello world")
349
hello world
350
sage: DC.logfile.close()
351
sage: print open(DD.logfile).read()
352
hello world
353
354
Check that no duplicate logs appear, even when forking (:trac:`15244`)::
355
356
sage: DD = DocTestDefaults(logfile=tmp_filename())
357
sage: DC = DocTestController(DD, [])
358
sage: DC.log("hello world")
359
hello world
360
sage: if os.fork() == 0:
361
....: DC.logfile.close()
362
....: os._exit(0)
363
sage: DC.logfile.close()
364
sage: print open(DD.logfile).read()
365
hello world
366
367
"""
368
s += end
369
if self.logfile is not None:
370
self.logfile.write(s)
371
self.logfile.flush()
372
sys.stdout.write(s)
373
sys.stdout.flush()
374
375
def test_safe_directory(self, dir=None):
376
"""
377
Test that the given directory is safe to run Python code from.
378
379
We use the check added to Python for this, which gives a
380
warning when the current directory is considered unsafe. We promote
381
this warning to an error with ``-Werror``. See
382
``sage/tests/cmdline.py`` for a doctest that this works, see
383
also :trac:`13579`.
384
385
TESTS::
386
387
sage: from sage.doctest.control import DocTestDefaults, DocTestController
388
sage: DD = DocTestDefaults()
389
sage: DC = DocTestController(DD, [])
390
sage: DC.test_safe_directory()
391
sage: d = os.path.join(tmp_dir(), "test")
392
sage: os.mkdir(d)
393
sage: os.chmod(d, 0o777)
394
sage: DC.test_safe_directory(d)
395
Traceback (most recent call last):
396
...
397
RuntimeError: refusing to run doctests...
398
"""
399
import subprocess
400
with open(os.devnull, 'w') as dev_null:
401
if subprocess.call(['python', '-Werror', '-c', ''],
402
stdout=dev_null, stderr=dev_null, cwd=dir) != 0:
403
raise RuntimeError(
404
"refusing to run doctests from the current "
405
"directory '{}' since untrusted users could put files in "
406
"this directory, making it unsafe to run Sage code from"
407
.format(os.getcwd()))
408
409
def create_run_id(self):
410
"""
411
Creates the run id.
412
413
EXAMPLES::
414
415
sage: from sage.doctest.control import DocTestDefaults, DocTestController
416
sage: DC = DocTestController(DocTestDefaults(), [])
417
sage: DC.create_run_id()
418
Running doctests with ID ...
419
"""
420
self.run_id = time.strftime('%Y-%m-%d-%H-%M-%S-') + "%08x" % random.getrandbits(32)
421
from sage.version import version
422
self.log("Running doctests with ID %s."%self.run_id)
423
424
def add_files(self):
425
r"""
426
Checks for the flags '--all', '--new' and '--sagenb'.
427
428
For each one present, this function adds the appropriate directories and files to the todo list.
429
430
EXAMPLES::
431
432
sage: from sage.doctest.control import DocTestDefaults, DocTestController
433
sage: from sage.env import SAGE_SRC
434
sage: import os
435
sage: log_location = os.path.join(SAGE_TMP, 'control_dt_log.log')
436
sage: DD = DocTestDefaults(all=True, logfile=log_location)
437
sage: DC = DocTestController(DD, [])
438
sage: DC.add_files()
439
Doctesting entire Sage library.
440
sage: os.path.join(SAGE_SRC, 'sage') in DC.files
441
True
442
443
::
444
445
sage: DD = DocTestDefaults(new = True)
446
sage: DC = DocTestController(DD, [])
447
sage: DC.add_files()
448
Doctesting ...
449
450
::
451
452
sage: DD = DocTestDefaults(sagenb = True)
453
sage: DC = DocTestController(DD, [])
454
sage: DC.add_files()
455
Doctesting the Sage notebook.
456
sage: DC.files[0][-6:]
457
'sagenb'
458
"""
459
opj = os.path.join
460
from sage.env import SAGE_SRC, SAGE_ROOT
461
def all_files():
462
from glob import glob
463
self.files.append(opj(SAGE_SRC, 'sage'))
464
self.files.append(opj(SAGE_SRC, 'doc', 'common'))
465
self.files.extend(glob(opj(SAGE_SRC, 'doc', '[a-z][a-z]')))
466
self.options.sagenb = True
467
DOT_GIT= opj(SAGE_ROOT, '.git')
468
have_git = os.path.exists(DOT_GIT)
469
if self.options.all or (self.options.new and not have_git):
470
self.log("Doctesting entire Sage library.")
471
all_files()
472
elif self.options.new and have_git:
473
# Get all files changed in the working repo.
474
self.log("Doctesting files changed since last git commit")
475
import subprocess
476
change = subprocess.check_output(["git",
477
"--git-dir=" + DOT_GIT,
478
"--work-tree=" + SAGE_ROOT,
479
"status",
480
"--porcelain"])
481
for line in change.split("\n"):
482
if not line:
483
continue
484
data = line.strip().split(' ')
485
status, filename = data[0], data[-1]
486
if (set(status).issubset("MARCU")
487
and filename.startswith("src/sage")
488
and (filename.endswith(".py") or filename.endswith(".pyx"))):
489
self.files.append(os.path.relpath(opj(SAGE_ROOT,filename)))
490
if self.options.sagenb:
491
if not self.options.all:
492
self.log("Doctesting the Sage notebook.")
493
from pkg_resources import Requirement, working_set
494
sagenb_loc = working_set.find(Requirement.parse('sagenb')).location
495
self.files.append(opj(sagenb_loc, 'sagenb'))
496
497
def expand_files_into_sources(self):
498
r"""
499
Expands ``self.files``, which may include directories, into a
500
list of :class:`sage.doctest.FileDocTestSource`
501
502
This function also handles the optional command line option.
503
504
EXAMPLES::
505
506
sage: from sage.doctest.control import DocTestDefaults, DocTestController
507
sage: from sage.env import SAGE_SRC
508
sage: import os
509
sage: dirname = os.path.join(SAGE_SRC, 'sage', 'doctest')
510
sage: DD = DocTestDefaults(optional='all')
511
sage: DC = DocTestController(DD, [dirname])
512
sage: DC.expand_files_into_sources()
513
sage: len(DC.sources)
514
9
515
sage: DC.sources[0].options.optional
516
True
517
518
::
519
520
sage: DD = DocTestDefaults(optional='magma,guava')
521
sage: DC = DocTestController(DD, [dirname])
522
sage: DC.expand_files_into_sources()
523
sage: sorted(list(DC.sources[0].options.optional))
524
['guava', 'magma']
525
526
We check that files are skipped appropriately::
527
528
sage: dirname = tmp_dir()
529
sage: filename = os.path.join(dirname, 'not_tested.py')
530
sage: with open(filename, 'w') as F:
531
....: F.write("#"*80 + "\n\n\n\n## nodoctest\n sage: 1+1\n 4")
532
sage: DC = DocTestController(DD, [dirname])
533
sage: DC.expand_files_into_sources()
534
sage: DC.sources
535
[]
536
537
The directory ``sage/doctest/tests`` contains ``nodoctest.py``
538
but the files should still be tested when that directory is
539
explicitly given (as opposed to being recursed into)::
540
541
sage: DC = DocTestController(DD, [os.path.join(SAGE_SRC, 'sage', 'doctest', 'tests')])
542
sage: DC.expand_files_into_sources()
543
sage: len(DC.sources) >= 10
544
True
545
"""
546
def expand():
547
for path in self.files:
548
if os.path.isdir(path):
549
for root, dirs, files in os.walk(path):
550
for dir in list(dirs):
551
if dir[0] == "." or skipdir(os.path.join(root,dir)):
552
dirs.remove(dir)
553
for file in files:
554
if not skipfile(os.path.join(root,file)):
555
yield os.path.join(root, file)
556
else:
557
# the user input this file explicitly, so we don't skip it
558
yield path
559
self.sources = [FileDocTestSource(path, self.options) for path in expand()]
560
561
def filter_sources(self):
562
"""
563
564
EXAMPLES::
565
566
sage: from sage.doctest.control import DocTestDefaults, DocTestController
567
sage: from sage.env import SAGE_SRC
568
sage: import os
569
sage: dirname = os.path.join(SAGE_SRC, 'sage', 'doctest')
570
sage: DD = DocTestDefaults(failed=True)
571
sage: DC = DocTestController(DD, [dirname])
572
sage: DC.expand_files_into_sources()
573
sage: for i, source in enumerate(DC.sources):
574
... DC.stats[source.basename] = {'walltime': 0.1*(i+1)}
575
sage: DC.stats['sage.doctest.control'] = {'failed':True,'walltime':1.0}
576
sage: DC.filter_sources()
577
Only doctesting files that failed last test.
578
sage: len(DC.sources)
579
1
580
"""
581
# Filter the sources to only include those with failing doctests if the --failed option is passed
582
if self.options.failed:
583
self.log("Only doctesting files that failed last test.")
584
def is_failure(source):
585
basename = source.basename
586
return basename not in self.stats or self.stats[basename].get('failed')
587
self.sources = filter(is_failure, self.sources)
588
589
def sort_sources(self):
590
r"""
591
This function sorts the sources so that slower doctests are run first.
592
593
EXAMPLES::
594
595
sage: from sage.doctest.control import DocTestDefaults, DocTestController
596
sage: from sage.env import SAGE_SRC
597
sage: import os
598
sage: dirname = os.path.join(SAGE_SRC, 'sage', 'doctest')
599
sage: DD = DocTestDefaults(nthreads=2)
600
sage: DC = DocTestController(DD, [dirname])
601
sage: DC.expand_files_into_sources()
602
sage: DC.sources.sort(key=lambda s:s.basename)
603
sage: for i, source in enumerate(DC.sources):
604
... DC.stats[source.basename] = {'walltime': 0.1*(i+1)}
605
sage: DC.sort_sources()
606
Sorting sources by runtime so that slower doctests are run first....
607
sage: print "\n".join([source.basename for source in DC.sources])
608
sage.doctest.util
609
sage.doctest.test
610
sage.doctest.sources
611
sage.doctest.reporting
612
sage.doctest.parsing
613
sage.doctest.forker
614
sage.doctest.control
615
sage.doctest.all
616
sage.doctest
617
"""
618
if self.options.nthreads > 1 and len(self.sources) > self.options.nthreads:
619
self.log("Sorting sources by runtime so that slower doctests are run first....")
620
default = dict(walltime=0)
621
def sort_key(source):
622
basename = source.basename
623
return -self.stats.get(basename, default).get('walltime'), basename
624
self.sources = [x[1] for x in sorted((sort_key(source), source) for source in self.sources)]
625
626
def run_doctests(self):
627
"""
628
Actually runs the doctests.
629
630
This function is called by :meth:`run`.
631
632
EXAMPLES::
633
634
sage: from sage.doctest.control import DocTestDefaults, DocTestController
635
sage: from sage.env import SAGE_SRC
636
sage: import os
637
sage: dirname = os.path.join(SAGE_SRC, 'sage', 'rings', 'homset.py')
638
sage: DD = DocTestDefaults()
639
sage: DC = DocTestController(DD, [dirname])
640
sage: DC.expand_files_into_sources()
641
sage: DC.run_doctests()
642
Doctesting 1 file.
643
sage -t .../sage/rings/homset.py
644
[... tests, ... s]
645
----------------------------------------------------------------------
646
All tests passed!
647
----------------------------------------------------------------------
648
Total time for all tests: ... seconds
649
cpu time: ... seconds
650
cumulative wall time: ... seconds
651
"""
652
nfiles = 0
653
nother = 0
654
for F in self.sources:
655
if isinstance(F, FileDocTestSource):
656
nfiles += 1
657
else:
658
nother += 1
659
if self.sources:
660
filestr = ", ".join(([count_noun(nfiles, "file")] if nfiles else []) +
661
([count_noun(nother, "other source")] if nother else []))
662
threads = " using %s threads"%(self.options.nthreads) if self.options.nthreads > 1 else ""
663
iterations = []
664
if self.options.global_iterations > 1:
665
iterations.append("%s global iterations"%(self.options.global_iterations))
666
if self.options.file_iterations > 1:
667
iterations.append("%s file iterations"%(self.options.file_iterations))
668
iterations = ", ".join(iterations)
669
if iterations:
670
iterations = " (%s)"%(iterations)
671
self.log("Doctesting %s%s%s."%(filestr, threads, iterations))
672
self.reporter = DocTestReporter(self)
673
self.dispatcher = DocTestDispatcher(self)
674
N = self.options.global_iterations
675
for it in range(N):
676
try:
677
self.timer = Timer().start()
678
self.dispatcher.dispatch()
679
except KeyboardInterrupt:
680
it = N - 1
681
break
682
finally:
683
self.timer.stop()
684
self.reporter.finalize()
685
self.cleanup(it == N - 1)
686
else:
687
self.log("No files to doctest")
688
self.reporter = DictAsObject(dict(error_status=0))
689
690
def cleanup(self, final=True):
691
"""
692
Runs cleanup activities after actually running doctests.
693
694
In particular, saves the stats to disk and closes the logfile.
695
696
INPUT:
697
698
- ``final`` -- whether to close the logfile
699
700
EXAMPLES::
701
702
sage: from sage.doctest.control import DocTestDefaults, DocTestController
703
sage: from sage.env import SAGE_SRC
704
sage: import os
705
sage: dirname = os.path.join(SAGE_SRC, 'sage', 'rings', 'infinity.py')
706
sage: DD = DocTestDefaults()
707
708
sage: DC = DocTestController(DD, [dirname])
709
sage: DC.expand_files_into_sources()
710
sage: DC.sources.sort(key=lambda s:s.basename)
711
712
sage: for i, source in enumerate(DC.sources):
713
....: DC.stats[source.basename] = {'walltime': 0.1*(i+1)}
714
....:
715
716
sage: DC.run()
717
Running doctests with ID ...
718
Doctesting 1 file.
719
sage -t .../rings/infinity.py
720
[... tests, ... s]
721
----------------------------------------------------------------------
722
All tests passed!
723
----------------------------------------------------------------------
724
Total time for all tests: ... seconds
725
cpu time: ... seconds
726
cumulative wall time: ... seconds
727
0
728
sage: DC.cleanup()
729
"""
730
self.stats.update(self.reporter.stats)
731
self.save_stats(self.options.stats_path)
732
# Close the logfile
733
if final and self.logfile is not None:
734
self.logfile.close()
735
self.logfile = None
736
737
def _assemble_cmd(self):
738
"""
739
Assembles a shell command used in running tests under gdb or valgrind.
740
741
EXAMPLES::
742
743
sage: from sage.doctest.control import DocTestDefaults, DocTestController
744
sage: DC = DocTestController(DocTestDefaults(timeout=123), ["hello_world.py"])
745
sage: print DC._assemble_cmd()
746
python "$SAGE_LOCAL/bin/sage-runtests" --serial --timeout=123 hello_world.py
747
"""
748
cmd = '''python "%s" --serial '''%(os.path.join("$SAGE_LOCAL","bin","sage-runtests"))
749
opt = dict_difference(self.options.__dict__, DocTestDefaults().__dict__)
750
for o in ("all", "sagenb"):
751
if o in opt:
752
raise ValueError("You cannot run gdb/valgrind on the whole sage%s library"%("" if o == "all" else "nb"))
753
for o in ("all", "sagenb", "long", "force_lib", "verbose", "failed", "new"):
754
if o in opt:
755
cmd += "--%s "%o
756
for o in ("timeout", "optional", "randorder", "stats_path"):
757
if o in opt:
758
cmd += "--%s=%s "%(o, opt[o])
759
return cmd + " ".join(self.files)
760
761
def run_val_gdb(self, testing=False):
762
"""
763
Spawns a subprocess to run tests under the control of gdb or valgrind.
764
765
INPUT:
766
767
- ``testing`` -- boolean; if True then the command to be run
768
will be printed rather than a subprocess started.
769
770
EXAMPLES:
771
772
Note that the command lines include unexpanded environment
773
variables. It is safer to let the shell expand them than to
774
expand them here and risk insufficient quoting. ::
775
776
sage: from sage.doctest.control import DocTestDefaults, DocTestController
777
sage: DD = DocTestDefaults(gdb=True)
778
sage: DC = DocTestController(DD, ["hello_world.py"])
779
sage: DC.run_val_gdb(testing=True)
780
exec gdb -x "$SAGE_LOCAL/bin/sage-gdb-commands" --args python "$SAGE_LOCAL/bin/sage-runtests" --serial --timeout=0 hello_world.py
781
782
::
783
784
sage: DD = DocTestDefaults(valgrind=True, optional="all", timeout=172800)
785
sage: DC = DocTestController(DD, ["hello_world.py"])
786
sage: DC.run_val_gdb(testing=True)
787
exec valgrind --tool=memcheck --leak-resolution=high --leak-check=full --num-callers=25 --suppressions="$SAGE_LOCAL/lib/valgrind/sage.supp" --log-file=".../valgrind/sage-memcheck.%p" python "$SAGE_LOCAL/bin/sage-runtests" --serial --timeout=172800 --optional=True hello_world.py
788
"""
789
try:
790
sage_cmd = self._assemble_cmd()
791
except ValueError:
792
self.log(sys.exc_info()[1])
793
return 2
794
opt = self.options
795
if opt.gdb:
796
cmd = '''exec gdb -x "$SAGE_LOCAL/bin/sage-gdb-commands" --args '''
797
flags = ""
798
if opt.logfile:
799
sage_cmd += " --logfile %s"%(opt.logfile)
800
else:
801
if opt.logfile is None:
802
default_log = os.path.join(DOT_SAGE, "valgrind")
803
if os.path.exists(default_log):
804
if not os.path.isdir(default_log):
805
self.log("%s must be a directory"%default_log)
806
return 2
807
else:
808
os.makedirs(default_log)
809
logfile = os.path.join(default_log, "sage-%s")
810
else:
811
logfile = opt.logfile
812
if opt.valgrind:
813
toolname = "memcheck"
814
flags = os.getenv("SAGE_MEMCHECK_FLAGS")
815
if flags is None:
816
flags = "--leak-resolution=high --leak-check=full --num-callers=25 "
817
flags += '''--suppressions="%s" '''%(os.path.join("$SAGE_LOCAL","lib","valgrind","sage.supp"))
818
elif opt.massif:
819
toolname = "massif"
820
flags = os.getenv("SAGE_MASSIF_FLAGS", "--depth=6 ")
821
elif opt.cachegrind:
822
toolname = "cachegrind"
823
flags = os.getenv("SAGE_CACHEGRIND_FLAGS", "")
824
elif opt.omega:
825
toolname = "exp-omega"
826
flags = os.getenv("SAGE_OMEGA_FLAGS", "")
827
cmd = "exec valgrind --tool=%s "%(toolname)
828
flags += ''' --log-file="%s" ''' % logfile
829
if opt.omega:
830
toolname = "omega"
831
if "%s" in flags:
832
flags %= toolname + ".%p" # replace %s with toolname
833
cmd += flags + sage_cmd
834
835
self.log(cmd)
836
sys.stdout.flush()
837
sys.stderr.flush()
838
if self.logfile is not None:
839
self.logfile.flush()
840
841
if testing:
842
return
843
844
# Setup Sage signal handler
845
_init_csage()
846
847
import signal, subprocess
848
p = subprocess.Popen(cmd, shell=True)
849
if opt.timeout > 0:
850
signal.alarm(opt.timeout)
851
try:
852
return p.wait()
853
except AlarmInterrupt:
854
self.log(" Timed out")
855
return 4
856
except KeyboardInterrupt:
857
self.log(" Interrupted")
858
return 128
859
finally:
860
signal.alarm(0)
861
if p.returncode is None:
862
p.terminate()
863
864
def run(self):
865
"""
866
This function is called after initialization to set up and run all doctests.
867
868
EXAMPLES::
869
870
sage: from sage.doctest.control import DocTestDefaults, DocTestController
871
sage: from sage.env import SAGE_SRC
872
sage: import os
873
sage: DD = DocTestDefaults()
874
sage: filename = os.path.join(SAGE_SRC, "sage", "sets", "non_negative_integers.py")
875
sage: DC = DocTestController(DD, [filename])
876
sage: DC.run()
877
Running doctests with ID ...
878
Doctesting 1 file.
879
sage -t .../sage/sets/non_negative_integers.py
880
[... tests, ... s]
881
----------------------------------------------------------------------
882
All tests passed!
883
----------------------------------------------------------------------
884
Total time for all tests: ... seconds
885
cpu time: ... seconds
886
cumulative wall time: ... seconds
887
0
888
"""
889
opt = self.options
890
L = (opt.gdb, opt.valgrind, opt.massif, opt.cachegrind, opt.omega)
891
if any(L):
892
if L.count(True) > 1:
893
self.log("You may only specify one of gdb, valgrind/memcheck, massif, cachegrind, omega")
894
return 2
895
return self.run_val_gdb()
896
else:
897
self.test_safe_directory()
898
self.create_run_id()
899
self.add_files()
900
self.expand_files_into_sources()
901
self.filter_sources()
902
self.sort_sources()
903
self.run_doctests()
904
return self.reporter.error_status
905
906
def run_doctests(module, options=None):
907
"""
908
Runs the doctests in a given file.
909
910
INPUTS:
911
912
- ``module`` -- a Sage module, a string, or a list of such.
913
914
- ``options`` -- a DocTestDefaults object or None.
915
916
EXAMPLES::
917
918
sage: run_doctests(sage.rings.infinity)
919
Running doctests with ID ...
920
Doctesting 1 file.
921
sage -t .../sage/rings/infinity.py
922
[... tests, ... s]
923
----------------------------------------------------------------------
924
All tests passed!
925
----------------------------------------------------------------------
926
Total time for all tests: ... seconds
927
cpu time: ... seconds
928
cumulative wall time: ... seconds
929
"""
930
import sys
931
sys.stdout.flush()
932
def stringify(x):
933
if isinstance(x, (list, tuple)):
934
F = [stringify(a) for a in x]
935
return sage.misc.flatten.flatten(F)
936
elif isinstance(x, types.ModuleType):
937
F = x.__file__.replace(SAGE_LIB, SAGE_SRC)
938
base, pyfile = os.path.split(F)
939
file, ext = os.path.splitext(pyfile)
940
if ext == ".pyc":
941
ext = ".py"
942
elif ext == ".so":
943
ext = ".pyx"
944
if file == "__init__":
945
return [base]
946
else:
947
return [os.path.join(base, file) + ext]
948
elif isinstance(x, basestring):
949
return [os.path.abspath(x)]
950
F = stringify(module)
951
if options is None:
952
options = DocTestDefaults()
953
DC = DocTestController(options, F)
954
955
# Determine whether we're in doctest mode
956
save_dtmode = sage.doctest.DOCTEST_MODE
957
958
# We need the following if we're not in DOCTEST_MODE
959
# Tell IPython to avoid colors: it screws up the output checking.
960
if not save_dtmode:
961
if options.debug:
962
raise ValueError("You should not try to run doctests with a debugger from within Sage: IPython objects to embedded shells")
963
IP = get_ipython()
964
old_color = IP.colors
965
IP.run_line_magic('colors', 'NoColor')
966
old_config_color = IP.config.TerminalInteractiveShell.colors
967
IP.config.TerminalInteractiveShell.colors = 'NoColor'
968
969
try:
970
DC.run()
971
finally:
972
sage.doctest.DOCTEST_MODE = save_dtmode
973
if not save_dtmode:
974
IP.run_line_magic('colors', old_color)
975
IP.config.TerminalInteractiveShell.colors = old_config_color
976
977