|
""" |
|
This is our testing framework. |
|
|
|
Goals: |
|
|
|
* it should be compatible with py.test and operate very similarly |
|
(or identically) |
|
* does not require any external dependencies |
|
* preferably all the functionality should be in this file only |
|
* no magic, just import the test file and execute the test functions, that's it |
|
* portable |
|
|
|
""" |
|
|
|
import os |
|
import sys |
|
import platform |
|
import inspect |
|
import traceback |
|
import pdb |
|
import re |
|
import linecache |
|
import time |
|
from fnmatch import fnmatch |
|
from timeit import default_timer as clock |
|
import doctest as pdoctest |
|
from doctest import DocTestFinder, DocTestRunner |
|
import random |
|
import subprocess |
|
import shutil |
|
import signal |
|
import stat |
|
import tempfile |
|
import warnings |
|
from contextlib import contextmanager |
|
from inspect import unwrap |
|
from pathlib import Path |
|
|
|
from sympy.core.cache import clear_cache |
|
from sympy.external import import_module |
|
from sympy.external.gmpy import GROUND_TYPES |
|
|
|
IS_WINDOWS = (os.name == 'nt') |
|
ON_CI = os.getenv('CI', None) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
SPLIT_DENSITY = [ |
|
0.0059, 0.0027, 0.0068, 0.0011, 0.0006, |
|
0.0058, 0.0047, 0.0046, 0.004, 0.0257, |
|
0.0017, 0.0026, 0.004, 0.0032, 0.0016, |
|
0.0015, 0.0004, 0.0011, 0.0016, 0.0014, |
|
0.0077, 0.0137, 0.0217, 0.0074, 0.0043, |
|
0.0067, 0.0236, 0.0004, 0.1189, 0.0142, |
|
0.0234, 0.0003, 0.0003, 0.0047, 0.0006, |
|
0.0013, 0.0004, 0.0008, 0.0007, 0.0006, |
|
0.0139, 0.0013, 0.0007, 0.0051, 0.002, |
|
0.0004, 0.0005, 0.0213, 0.0048, 0.0016, |
|
0.0012, 0.0014, 0.0024, 0.0015, 0.0004, |
|
0.0005, 0.0007, 0.011, 0.0062, 0.0015, |
|
0.0021, 0.0049, 0.0006, 0.0006, 0.0011, |
|
0.0006, 0.0019, 0.003, 0.0044, 0.0054, |
|
0.0057, 0.0049, 0.0016, 0.0006, 0.0009, |
|
0.0006, 0.0012, 0.0006, 0.0149, 0.0532, |
|
0.0076, 0.0041, 0.0024, 0.0135, 0.0081, |
|
0.2209, 0.0459, 0.0438, 0.0488, 0.0137, |
|
0.002, 0.0003, 0.0008, 0.0039, 0.0024, |
|
0.0005, 0.0004, 0.003, 0.056, 0.0026] |
|
SPLIT_DENSITY_SLOW = [0.0086, 0.0004, 0.0568, 0.0003, 0.0032, 0.0005, 0.0004, 0.0013, 0.0016, 0.0648, 0.0198, 0.1285, 0.098, 0.0005, 0.0064, 0.0003, 0.0004, 0.0026, 0.0007, 0.0051, 0.0089, 0.0024, 0.0033, 0.0057, 0.0005, 0.0003, 0.001, 0.0045, 0.0091, 0.0006, 0.0005, 0.0321, 0.0059, 0.1105, 0.216, 0.1489, 0.0004, 0.0003, 0.0006, 0.0483] |
|
|
|
class Skipped(Exception): |
|
pass |
|
|
|
class TimeOutError(Exception): |
|
pass |
|
|
|
class DependencyError(Exception): |
|
pass |
|
|
|
|
|
def _indent(s, indent=4): |
|
""" |
|
Add the given number of space characters to the beginning of |
|
every non-blank line in ``s``, and return the result. |
|
If the string ``s`` is Unicode, it is encoded using the stdout |
|
encoding and the ``backslashreplace`` error handler. |
|
""" |
|
|
|
return re.sub('(?m)^(?!$)', indent*' ', s) |
|
|
|
|
|
pdoctest._indent = _indent |
|
|
|
|
|
|
|
|
|
def _report_failure(self, out, test, example, got): |
|
""" |
|
Report that the given example failed. |
|
""" |
|
s = self._checker.output_difference(example, got, self.optionflags) |
|
s = s.encode('raw_unicode_escape').decode('utf8', 'ignore') |
|
out(self._failure_header(test, example) + s) |
|
|
|
|
|
if IS_WINDOWS: |
|
DocTestRunner.report_failure = _report_failure |
|
|
|
|
|
def convert_to_native_paths(lst): |
|
""" |
|
Converts a list of '/' separated paths into a list of |
|
native (os.sep separated) paths and converts to lowercase |
|
if the system is case insensitive. |
|
""" |
|
newlst = [] |
|
for rv in lst: |
|
rv = os.path.join(*rv.split("/")) |
|
|
|
if sys.platform == "win32": |
|
pos = rv.find(':') |
|
if pos != -1: |
|
if rv[pos + 1] != '\\': |
|
rv = rv[:pos + 1] + '\\' + rv[pos + 1:] |
|
newlst.append(os.path.normcase(rv)) |
|
return newlst |
|
|
|
|
|
def get_sympy_dir(): |
|
""" |
|
Returns the root SymPy directory and set the global value |
|
indicating whether the system is case sensitive or not. |
|
""" |
|
this_file = os.path.abspath(__file__) |
|
sympy_dir = os.path.join(os.path.dirname(this_file), "..", "..") |
|
sympy_dir = os.path.normpath(sympy_dir) |
|
return os.path.normcase(sympy_dir) |
|
|
|
|
|
def setup_pprint(disable_line_wrap=True): |
|
from sympy.interactive.printing import init_printing |
|
from sympy.printing.pretty.pretty import pprint_use_unicode |
|
import sympy.interactive.printing as interactive_printing |
|
from sympy.printing.pretty import stringpict |
|
|
|
|
|
interactive_printing.NO_GLOBAL = True |
|
|
|
|
|
use_unicode_prev = pprint_use_unicode(False) |
|
|
|
|
|
wrap_line_prev = stringpict._GLOBAL_WRAP_LINE |
|
if disable_line_wrap: |
|
stringpict._GLOBAL_WRAP_LINE = False |
|
|
|
|
|
init_printing(pretty_print=False) |
|
|
|
return use_unicode_prev, wrap_line_prev |
|
|
|
|
|
@contextmanager |
|
def raise_on_deprecated(): |
|
"""Context manager to make DeprecationWarning raise an error |
|
|
|
This is to catch SymPyDeprecationWarning from library code while running |
|
tests and doctests. It is important to use this context manager around |
|
each individual test/doctest in case some tests modify the warning |
|
filters. |
|
""" |
|
with warnings.catch_warnings(): |
|
warnings.filterwarnings('error', '.*', DeprecationWarning, module='sympy.*') |
|
yield |
|
|
|
|
|
def run_in_subprocess_with_hash_randomization( |
|
function, function_args=(), |
|
function_kwargs=None, command=sys.executable, |
|
module='sympy.testing.runtests', force=False): |
|
""" |
|
Run a function in a Python subprocess with hash randomization enabled. |
|
|
|
If hash randomization is not supported by the version of Python given, it |
|
returns False. Otherwise, it returns the exit value of the command. The |
|
function is passed to sys.exit(), so the return value of the function will |
|
be the return value. |
|
|
|
The environment variable PYTHONHASHSEED is used to seed Python's hash |
|
randomization. If it is set, this function will return False, because |
|
starting a new subprocess is unnecessary in that case. If it is not set, |
|
one is set at random, and the tests are run. Note that if this |
|
environment variable is set when Python starts, hash randomization is |
|
automatically enabled. To force a subprocess to be created even if |
|
PYTHONHASHSEED is set, pass ``force=True``. This flag will not force a |
|
subprocess in Python versions that do not support hash randomization (see |
|
below), because those versions of Python do not support the ``-R`` flag. |
|
|
|
``function`` should be a string name of a function that is importable from |
|
the module ``module``, like "_test". The default for ``module`` is |
|
"sympy.testing.runtests". ``function_args`` and ``function_kwargs`` |
|
should be a repr-able tuple and dict, respectively. The default Python |
|
command is sys.executable, which is the currently running Python command. |
|
|
|
This function is necessary because the seed for hash randomization must be |
|
set by the environment variable before Python starts. Hence, in order to |
|
use a predetermined seed for tests, we must start Python in a separate |
|
subprocess. |
|
|
|
Hash randomization was added in the minor Python versions 2.6.8, 2.7.3, |
|
3.1.5, and 3.2.3, and is enabled by default in all Python versions after |
|
and including 3.3.0. |
|
|
|
Examples |
|
======== |
|
|
|
>>> from sympy.testing.runtests import ( |
|
... run_in_subprocess_with_hash_randomization) |
|
>>> # run the core tests in verbose mode |
|
>>> run_in_subprocess_with_hash_randomization("_test", |
|
... function_args=("core",), |
|
... function_kwargs={'verbose': True}) # doctest: +SKIP |
|
# Will return 0 if sys.executable supports hash randomization and tests |
|
# pass, 1 if they fail, and False if it does not support hash |
|
# randomization. |
|
|
|
""" |
|
cwd = get_sympy_dir() |
|
|
|
|
|
|
|
|
|
|
|
p = subprocess.Popen([command, "-RV"], stdout=subprocess.PIPE, |
|
stderr=subprocess.STDOUT, cwd=cwd) |
|
p.communicate() |
|
if p.returncode != 0: |
|
return False |
|
|
|
hash_seed = os.getenv("PYTHONHASHSEED") |
|
if not hash_seed: |
|
os.environ["PYTHONHASHSEED"] = str(random.randrange(2**32)) |
|
else: |
|
if not force: |
|
return False |
|
|
|
function_kwargs = function_kwargs or {} |
|
|
|
|
|
commandstring = ("import sys; from %s import %s;sys.exit(%s(*%s, **%s))" % |
|
(module, function, function, repr(function_args), |
|
repr(function_kwargs))) |
|
|
|
try: |
|
p = subprocess.Popen([command, "-R", "-c", commandstring], cwd=cwd) |
|
p.communicate() |
|
except KeyboardInterrupt: |
|
p.wait() |
|
finally: |
|
|
|
|
|
if hash_seed is None: |
|
del os.environ["PYTHONHASHSEED"] |
|
else: |
|
os.environ["PYTHONHASHSEED"] = hash_seed |
|
return p.returncode |
|
|
|
|
|
def run_all_tests(test_args=(), test_kwargs=None, |
|
doctest_args=(), doctest_kwargs=None, |
|
examples_args=(), examples_kwargs=None): |
|
""" |
|
Run all tests. |
|
|
|
Right now, this runs the regular tests (bin/test), the doctests |
|
(bin/doctest), and the examples (examples/all.py). |
|
|
|
This is what ``setup.py test`` uses. |
|
|
|
You can pass arguments and keyword arguments to the test functions that |
|
support them (for now, test, doctest, and the examples). See the |
|
docstrings of those functions for a description of the available options. |
|
|
|
For example, to run the solvers tests with colors turned off: |
|
|
|
>>> from sympy.testing.runtests import run_all_tests |
|
>>> run_all_tests(test_args=("solvers",), |
|
... test_kwargs={"colors:False"}) # doctest: +SKIP |
|
|
|
""" |
|
tests_successful = True |
|
|
|
test_kwargs = test_kwargs or {} |
|
doctest_kwargs = doctest_kwargs or {} |
|
examples_kwargs = examples_kwargs or {'quiet': True} |
|
|
|
try: |
|
|
|
if not test(*test_args, **test_kwargs): |
|
|
|
|
|
tests_successful = False |
|
|
|
|
|
print() |
|
if not doctest(*doctest_args, **doctest_kwargs): |
|
tests_successful = False |
|
|
|
|
|
print() |
|
sys.path.append("examples") |
|
from all import run_examples |
|
if not run_examples(*examples_args, **examples_kwargs): |
|
tests_successful = False |
|
|
|
if tests_successful: |
|
return |
|
else: |
|
|
|
sys.exit(1) |
|
except KeyboardInterrupt: |
|
print() |
|
print("DO *NOT* COMMIT!") |
|
sys.exit(1) |
|
|
|
|
|
def test(*paths, subprocess=True, rerun=0, **kwargs): |
|
""" |
|
Run tests in the specified test_*.py files. |
|
|
|
Tests in a particular test_*.py file are run if any of the given strings |
|
in ``paths`` matches a part of the test file's path. If ``paths=[]``, |
|
tests in all test_*.py files are run. |
|
|
|
Notes: |
|
|
|
- If sort=False, tests are run in random order (not default). |
|
- Paths can be entered in native system format or in unix, |
|
forward-slash format. |
|
- Files that are on the blacklist can be tested by providing |
|
their path; they are only excluded if no paths are given. |
|
|
|
**Explanation of test results** |
|
|
|
====== =============================================================== |
|
Output Meaning |
|
====== =============================================================== |
|
. passed |
|
F failed |
|
X XPassed (expected to fail but passed) |
|
f XFAILed (expected to fail and indeed failed) |
|
s skipped |
|
w slow |
|
T timeout (e.g., when ``--timeout`` is used) |
|
K KeyboardInterrupt (when running the slow tests with ``--slow``, |
|
you can interrupt one of them without killing the test runner) |
|
====== =============================================================== |
|
|
|
|
|
Colors have no additional meaning and are used just to facilitate |
|
interpreting the output. |
|
|
|
Examples |
|
======== |
|
|
|
>>> import sympy |
|
|
|
Run all tests: |
|
|
|
>>> sympy.test() # doctest: +SKIP |
|
|
|
Run one file: |
|
|
|
>>> sympy.test("sympy/core/tests/test_basic.py") # doctest: +SKIP |
|
>>> sympy.test("_basic") # doctest: +SKIP |
|
|
|
Run all tests in sympy/functions/ and some particular file: |
|
|
|
>>> sympy.test("sympy/core/tests/test_basic.py", |
|
... "sympy/functions") # doctest: +SKIP |
|
|
|
Run all tests in sympy/core and sympy/utilities: |
|
|
|
>>> sympy.test("/core", "/util") # doctest: +SKIP |
|
|
|
Run specific test from a file: |
|
|
|
>>> sympy.test("sympy/core/tests/test_basic.py", |
|
... kw="test_equality") # doctest: +SKIP |
|
|
|
Run specific test from any file: |
|
|
|
>>> sympy.test(kw="subs") # doctest: +SKIP |
|
|
|
Run the tests with verbose mode on: |
|
|
|
>>> sympy.test(verbose=True) # doctest: +SKIP |
|
|
|
Do not sort the test output: |
|
|
|
>>> sympy.test(sort=False) # doctest: +SKIP |
|
|
|
Turn on post-mortem pdb: |
|
|
|
>>> sympy.test(pdb=True) # doctest: +SKIP |
|
|
|
Turn off colors: |
|
|
|
>>> sympy.test(colors=False) # doctest: +SKIP |
|
|
|
Force colors, even when the output is not to a terminal (this is useful, |
|
e.g., if you are piping to ``less -r`` and you still want colors) |
|
|
|
>>> sympy.test(force_colors=False) # doctest: +SKIP |
|
|
|
The traceback verboseness can be set to "short" or "no" (default is |
|
"short") |
|
|
|
>>> sympy.test(tb='no') # doctest: +SKIP |
|
|
|
The ``split`` option can be passed to split the test run into parts. The |
|
split currently only splits the test files, though this may change in the |
|
future. ``split`` should be a string of the form 'a/b', which will run |
|
part ``a`` of ``b``. For instance, to run the first half of the test suite: |
|
|
|
>>> sympy.test(split='1/2') # doctest: +SKIP |
|
|
|
The ``time_balance`` option can be passed in conjunction with ``split``. |
|
If ``time_balance=True`` (the default for ``sympy.test``), SymPy will attempt |
|
to split the tests such that each split takes equal time. This heuristic |
|
for balancing is based on pre-recorded test data. |
|
|
|
>>> sympy.test(split='1/2', time_balance=True) # doctest: +SKIP |
|
|
|
You can disable running the tests in a separate subprocess using |
|
``subprocess=False``. This is done to support seeding hash randomization, |
|
which is enabled by default in the Python versions where it is supported. |
|
If subprocess=False, hash randomization is enabled/disabled according to |
|
whether it has been enabled or not in the calling Python process. |
|
However, even if it is enabled, the seed cannot be printed unless it is |
|
called from a new Python process. |
|
|
|
Hash randomization was added in the minor Python versions 2.6.8, 2.7.3, |
|
3.1.5, and 3.2.3, and is enabled by default in all Python versions after |
|
and including 3.3.0. |
|
|
|
If hash randomization is not supported ``subprocess=False`` is used |
|
automatically. |
|
|
|
>>> sympy.test(subprocess=False) # doctest: +SKIP |
|
|
|
To set the hash randomization seed, set the environment variable |
|
``PYTHONHASHSEED`` before running the tests. This can be done from within |
|
Python using |
|
|
|
>>> import os |
|
>>> os.environ['PYTHONHASHSEED'] = '42' # doctest: +SKIP |
|
|
|
Or from the command line using |
|
|
|
$ PYTHONHASHSEED=42 ./bin/test |
|
|
|
If the seed is not set, a random seed will be chosen. |
|
|
|
Note that to reproduce the same hash values, you must use both the same seed |
|
as well as the same architecture (32-bit vs. 64-bit). |
|
|
|
""" |
|
|
|
print_counter = lambda i : (print("rerun %d" % (rerun-i)) |
|
if rerun-i else None) |
|
|
|
if subprocess: |
|
|
|
for i in range(rerun, -1, -1): |
|
print_counter(i) |
|
ret = run_in_subprocess_with_hash_randomization("_test", |
|
function_args=paths, function_kwargs=kwargs) |
|
if ret is False: |
|
break |
|
val = not bool(ret) |
|
|
|
if not val or i == 0: |
|
return val |
|
|
|
|
|
for i in range(rerun, -1, -1): |
|
print_counter(i) |
|
val = not bool(_test(*paths, **kwargs)) |
|
if not val or i == 0: |
|
return val |
|
|
|
|
|
def _test(*paths, |
|
verbose=False, tb="short", kw=None, pdb=False, colors=True, |
|
force_colors=False, sort=True, seed=None, timeout=False, |
|
fail_on_timeout=False, slow=False, enhance_asserts=False, split=None, |
|
time_balance=True, blacklist=(), |
|
fast_threshold=None, slow_threshold=None): |
|
""" |
|
Internal function that actually runs the tests. |
|
|
|
All keyword arguments from ``test()`` are passed to this function except for |
|
``subprocess``. |
|
|
|
Returns 0 if tests passed and 1 if they failed. See the docstring of |
|
``test()`` for more information. |
|
""" |
|
kw = kw or () |
|
|
|
if isinstance(kw, str): |
|
kw = (kw,) |
|
post_mortem = pdb |
|
if seed is None: |
|
seed = random.randrange(100000000) |
|
if ON_CI and timeout is False: |
|
timeout = 595 |
|
fail_on_timeout = True |
|
if ON_CI: |
|
blacklist = list(blacklist) + ['sympy/plotting/pygletplot/tests'] |
|
blacklist = convert_to_native_paths(blacklist) |
|
r = PyTestReporter(verbose=verbose, tb=tb, colors=colors, |
|
force_colors=force_colors, split=split) |
|
|
|
|
|
_paths = [] |
|
for path in paths: |
|
if '::' in path: |
|
path, _kw = path.split('::', 1) |
|
kw += (_kw,) |
|
_paths.append(path) |
|
paths = _paths |
|
|
|
t = SymPyTests(r, kw, post_mortem, seed, |
|
fast_threshold=fast_threshold, |
|
slow_threshold=slow_threshold) |
|
|
|
test_files = t.get_test_files('sympy') |
|
|
|
not_blacklisted = [f for f in test_files |
|
if not any(b in f for b in blacklist)] |
|
|
|
if len(paths) == 0: |
|
matched = not_blacklisted |
|
else: |
|
paths = convert_to_native_paths(paths) |
|
matched = [] |
|
for f in not_blacklisted: |
|
basename = os.path.basename(f) |
|
for p in paths: |
|
if p in f or fnmatch(basename, p): |
|
matched.append(f) |
|
break |
|
|
|
density = None |
|
if time_balance: |
|
if slow: |
|
density = SPLIT_DENSITY_SLOW |
|
else: |
|
density = SPLIT_DENSITY |
|
|
|
if split: |
|
matched = split_list(matched, split, density=density) |
|
|
|
t._testfiles.extend(matched) |
|
|
|
return int(not t.test(sort=sort, timeout=timeout, slow=slow, |
|
enhance_asserts=enhance_asserts, fail_on_timeout=fail_on_timeout)) |
|
|
|
|
|
def doctest(*paths, subprocess=True, rerun=0, **kwargs): |
|
r""" |
|
Runs doctests in all \*.py files in the SymPy directory which match |
|
any of the given strings in ``paths`` or all tests if paths=[]. |
|
|
|
Notes: |
|
|
|
- Paths can be entered in native system format or in unix, |
|
forward-slash format. |
|
- Files that are on the blacklist can be tested by providing |
|
their path; they are only excluded if no paths are given. |
|
|
|
Examples |
|
======== |
|
|
|
>>> import sympy |
|
|
|
Run all tests: |
|
|
|
>>> sympy.doctest() # doctest: +SKIP |
|
|
|
Run one file: |
|
|
|
>>> sympy.doctest("sympy/core/basic.py") # doctest: +SKIP |
|
>>> sympy.doctest("polynomial.rst") # doctest: +SKIP |
|
|
|
Run all tests in sympy/functions/ and some particular file: |
|
|
|
>>> sympy.doctest("/functions", "basic.py") # doctest: +SKIP |
|
|
|
Run any file having polynomial in its name, doc/src/modules/polynomial.rst, |
|
sympy/functions/special/polynomials.py, and sympy/polys/polynomial.py: |
|
|
|
>>> sympy.doctest("polynomial") # doctest: +SKIP |
|
|
|
The ``split`` option can be passed to split the test run into parts. The |
|
split currently only splits the test files, though this may change in the |
|
future. ``split`` should be a string of the form 'a/b', which will run |
|
part ``a`` of ``b``. Note that the regular doctests and the Sphinx |
|
doctests are split independently. For instance, to run the first half of |
|
the test suite: |
|
|
|
>>> sympy.doctest(split='1/2') # doctest: +SKIP |
|
|
|
The ``subprocess`` and ``verbose`` options are the same as with the function |
|
``test()`` (see the docstring of that function for more information) except |
|
that ``verbose`` may also be set equal to ``2`` in order to print |
|
individual doctest lines, as they are being tested. |
|
""" |
|
|
|
print_counter = lambda i : (print("rerun %d" % (rerun-i)) |
|
if rerun-i else None) |
|
|
|
if subprocess: |
|
|
|
for i in range(rerun, -1, -1): |
|
print_counter(i) |
|
ret = run_in_subprocess_with_hash_randomization("_doctest", |
|
function_args=paths, function_kwargs=kwargs) |
|
if ret is False: |
|
break |
|
val = not bool(ret) |
|
|
|
if not val or i == 0: |
|
return val |
|
|
|
|
|
for i in range(rerun, -1, -1): |
|
print_counter(i) |
|
val = not bool(_doctest(*paths, **kwargs)) |
|
if not val or i == 0: |
|
return val |
|
|
|
|
|
def _get_doctest_blacklist(): |
|
'''Get the default blacklist for the doctests''' |
|
blacklist = [] |
|
|
|
blacklist.extend([ |
|
"doc/src/modules/plotting.rst", |
|
"doc/src/modules/physics/mechanics/autolev_parser.rst", |
|
"sympy/codegen/array_utils.py", |
|
"sympy/core/compatibility.py", |
|
"sympy/core/trace.py", |
|
"sympy/galgebra.py", |
|
"sympy/parsing/autolev/_antlr/autolevlexer.py", |
|
"sympy/parsing/autolev/_antlr/autolevlistener.py", |
|
"sympy/parsing/autolev/_antlr/autolevparser.py", |
|
"sympy/parsing/latex/_antlr/latexlexer.py", |
|
"sympy/parsing/latex/_antlr/latexparser.py", |
|
"sympy/plotting/pygletplot/__init__.py", |
|
"sympy/plotting/pygletplot/plot.py", |
|
"sympy/printing/ccode.py", |
|
"sympy/printing/cxxcode.py", |
|
"sympy/printing/fcode.py", |
|
"sympy/testing/randtest.py", |
|
"sympy/this.py", |
|
]) |
|
|
|
num = 12 |
|
for i in range (1, num+1): |
|
blacklist.append("sympy/parsing/autolev/test-examples/ruletest" + str(i) + ".py") |
|
blacklist.extend(["sympy/parsing/autolev/test-examples/pydy-example-repo/mass_spring_damper.py", |
|
"sympy/parsing/autolev/test-examples/pydy-example-repo/chaos_pendulum.py", |
|
"sympy/parsing/autolev/test-examples/pydy-example-repo/double_pendulum.py", |
|
"sympy/parsing/autolev/test-examples/pydy-example-repo/non_min_pendulum.py"]) |
|
|
|
if import_module('numpy') is None: |
|
blacklist.extend([ |
|
"sympy/plotting/experimental_lambdify.py", |
|
"sympy/plotting/plot_implicit.py", |
|
"examples/advanced/autowrap_integrators.py", |
|
"examples/advanced/autowrap_ufuncify.py", |
|
"examples/intermediate/sample.py", |
|
"examples/intermediate/mplot2d.py", |
|
"examples/intermediate/mplot3d.py", |
|
"doc/src/modules/numeric-computation.rst", |
|
"doc/src/explanation/best-practices.md", |
|
"doc/src/tutorials/physics/biomechanics/biomechanical-model-example.rst", |
|
"doc/src/tutorials/physics/biomechanics/biomechanics.rst", |
|
]) |
|
else: |
|
if import_module('matplotlib') is None: |
|
blacklist.extend([ |
|
"examples/intermediate/mplot2d.py", |
|
"examples/intermediate/mplot3d.py" |
|
]) |
|
else: |
|
|
|
import matplotlib |
|
matplotlib.use('Agg') |
|
|
|
if ON_CI or import_module('pyglet') is None: |
|
blacklist.extend(["sympy/plotting/pygletplot"]) |
|
|
|
if import_module('aesara') is None: |
|
blacklist.extend([ |
|
"sympy/printing/aesaracode.py", |
|
"doc/src/modules/numeric-computation.rst", |
|
]) |
|
|
|
if import_module('cupy') is None: |
|
blacklist.extend([ |
|
"doc/src/modules/numeric-computation.rst", |
|
]) |
|
|
|
if import_module('jax') is None: |
|
blacklist.extend([ |
|
"doc/src/modules/numeric-computation.rst", |
|
]) |
|
|
|
if import_module('antlr4') is None: |
|
blacklist.extend([ |
|
"sympy/parsing/autolev/__init__.py", |
|
"sympy/parsing/latex/_parse_latex_antlr.py", |
|
]) |
|
|
|
if import_module('lfortran') is None: |
|
|
|
blacklist.extend([ |
|
"sympy/parsing/sym_expr.py", |
|
]) |
|
|
|
if import_module("scipy") is None: |
|
|
|
blacklist.extend([ |
|
"doc/src/guides/solving/solve-numerically.md", |
|
"doc/src/guides/solving/solve-ode.md", |
|
]) |
|
|
|
if import_module("numpy") is None: |
|
|
|
blacklist.extend([ |
|
"doc/src/guides/solving/solve-ode.md", |
|
"doc/src/guides/solving/solve-numerically.md", |
|
]) |
|
|
|
|
|
blacklist.extend([ |
|
"sympy/utilities/autowrap.py", |
|
"examples/advanced/autowrap_integrators.py", |
|
"examples/advanced/autowrap_ufuncify.py" |
|
]) |
|
|
|
blacklist.extend([ |
|
"sympy/conftest.py", |
|
]) |
|
|
|
|
|
blacklist.extend([ |
|
"sympy/utilities/tmpfiles.py", |
|
"sympy/utilities/pytest.py", |
|
"sympy/utilities/runtests.py", |
|
"sympy/utilities/quality_unicode.py", |
|
"sympy/utilities/randtest.py", |
|
]) |
|
|
|
blacklist = convert_to_native_paths(blacklist) |
|
return blacklist |
|
|
|
|
|
def _doctest(*paths, **kwargs): |
|
""" |
|
Internal function that actually runs the doctests. |
|
|
|
All keyword arguments from ``doctest()`` are passed to this function |
|
except for ``subprocess``. |
|
|
|
Returns 0 if tests passed and 1 if they failed. See the docstrings of |
|
``doctest()`` and ``test()`` for more information. |
|
""" |
|
from sympy.printing.pretty.pretty import pprint_use_unicode |
|
from sympy.printing.pretty import stringpict |
|
|
|
normal = kwargs.get("normal", False) |
|
verbose = kwargs.get("verbose", False) |
|
colors = kwargs.get("colors", True) |
|
force_colors = kwargs.get("force_colors", False) |
|
blacklist = kwargs.get("blacklist", []) |
|
split = kwargs.get('split', None) |
|
|
|
blacklist.extend(_get_doctest_blacklist()) |
|
|
|
|
|
if import_module('matplotlib') is not None: |
|
import matplotlib |
|
matplotlib.use('Agg') |
|
|
|
|
|
import sympy.external |
|
sympy.external.importtools.WARN_OLD_VERSION = False |
|
sympy.external.importtools.WARN_NOT_INSTALLED = False |
|
|
|
|
|
from sympy.plotting.plot import unset_show |
|
unset_show() |
|
|
|
r = PyTestReporter(verbose, split=split, colors=colors,\ |
|
force_colors=force_colors) |
|
t = SymPyDocTests(r, normal) |
|
|
|
test_files = t.get_test_files('sympy') |
|
test_files.extend(t.get_test_files('examples', init_only=False)) |
|
|
|
not_blacklisted = [f for f in test_files |
|
if not any(b in f for b in blacklist)] |
|
if len(paths) == 0: |
|
matched = not_blacklisted |
|
else: |
|
|
|
|
|
paths = convert_to_native_paths(paths) |
|
matched = [] |
|
for f in not_blacklisted: |
|
basename = os.path.basename(f) |
|
for p in paths: |
|
if p in f or fnmatch(basename, p): |
|
matched.append(f) |
|
break |
|
|
|
matched.sort() |
|
|
|
if split: |
|
matched = split_list(matched, split) |
|
|
|
t._testfiles.extend(matched) |
|
|
|
|
|
if t._testfiles: |
|
failed = not t.test() |
|
else: |
|
failed = False |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
test_files_rst = t.get_test_files('doc/src', '*.rst', init_only=False) |
|
test_files_md = t.get_test_files('doc/src', '*.md', init_only=False) |
|
test_files = test_files_rst + test_files_md |
|
test_files.sort() |
|
|
|
not_blacklisted = [f for f in test_files |
|
if not any(b in f for b in blacklist)] |
|
|
|
if len(paths) == 0: |
|
matched = not_blacklisted |
|
else: |
|
|
|
|
|
|
|
|
|
matched = [] |
|
for f in not_blacklisted: |
|
basename = os.path.basename(f) |
|
for p in paths: |
|
if p in f or fnmatch(basename, p): |
|
matched.append(f) |
|
break |
|
|
|
if split: |
|
matched = split_list(matched, split) |
|
|
|
first_report = True |
|
for rst_file in matched: |
|
if not os.path.isfile(rst_file): |
|
continue |
|
old_displayhook = sys.displayhook |
|
try: |
|
use_unicode_prev, wrap_line_prev = setup_pprint() |
|
out = sympytestfile( |
|
rst_file, module_relative=False, encoding='utf-8', |
|
optionflags=pdoctest.ELLIPSIS | pdoctest.NORMALIZE_WHITESPACE | |
|
pdoctest.IGNORE_EXCEPTION_DETAIL) |
|
finally: |
|
|
|
|
|
sys.displayhook = old_displayhook |
|
|
|
|
|
import sympy.interactive.printing as interactive_printing |
|
interactive_printing.NO_GLOBAL = False |
|
pprint_use_unicode(use_unicode_prev) |
|
stringpict._GLOBAL_WRAP_LINE = wrap_line_prev |
|
|
|
rstfailed, tested = out |
|
if tested: |
|
failed = rstfailed or failed |
|
if first_report: |
|
first_report = False |
|
msg = 'rst/md doctests start' |
|
if not t._testfiles: |
|
r.start(msg=msg) |
|
else: |
|
r.write_center(msg) |
|
print() |
|
|
|
file_id = rst_file[rst_file.find('sympy') + len('sympy') + 1:] |
|
print(file_id, end=" ") |
|
|
|
wid = r.terminal_width - len(file_id) - 1 |
|
test_file = '[%s]' % (tested) |
|
report = '[%s]' % (rstfailed or 'OK') |
|
print(''.join( |
|
[test_file, ' '*(wid - len(test_file) - len(report)), report]) |
|
) |
|
|
|
|
|
|
|
|
|
if not first_report and failed: |
|
print() |
|
print("DO *NOT* COMMIT!") |
|
|
|
return int(failed) |
|
|
|
sp = re.compile(r'([0-9]+)/([1-9][0-9]*)') |
|
|
|
def split_list(l, split, density=None): |
|
""" |
|
Splits a list into part a of b |
|
|
|
split should be a string of the form 'a/b'. For instance, '1/3' would give |
|
the split one of three. |
|
|
|
If the length of the list is not divisible by the number of splits, the |
|
last split will have more items. |
|
|
|
`density` may be specified as a list. If specified, |
|
tests will be balanced so that each split has as equal-as-possible |
|
amount of mass according to `density`. |
|
|
|
>>> from sympy.testing.runtests import split_list |
|
>>> a = list(range(10)) |
|
>>> split_list(a, '1/3') |
|
[0, 1, 2] |
|
>>> split_list(a, '2/3') |
|
[3, 4, 5] |
|
>>> split_list(a, '3/3') |
|
[6, 7, 8, 9] |
|
""" |
|
m = sp.match(split) |
|
if not m: |
|
raise ValueError("split must be a string of the form a/b where a and b are ints") |
|
i, t = map(int, m.groups()) |
|
|
|
if not density: |
|
return l[(i - 1)*len(l)//t : i*len(l)//t] |
|
|
|
|
|
tot = sum(density) |
|
density = [x / tot for x in density] |
|
|
|
def density_inv(x): |
|
"""Interpolate the inverse to the cumulative |
|
distribution function given by density""" |
|
if x <= 0: |
|
return 0 |
|
if x >= sum(density): |
|
return 1 |
|
|
|
|
|
|
|
cumm = 0 |
|
for i, d in enumerate(density): |
|
cumm += d |
|
if cumm >= x: |
|
break |
|
frac = (d - (cumm - x)) / d |
|
return (i + frac) / len(density) |
|
|
|
lower_frac = density_inv((i - 1) / t) |
|
higher_frac = density_inv(i / t) |
|
return l[int(lower_frac*len(l)) : int(higher_frac*len(l))] |
|
|
|
from collections import namedtuple |
|
SymPyTestResults = namedtuple('SymPyTestResults', 'failed attempted') |
|
|
|
def sympytestfile(filename, module_relative=True, name=None, package=None, |
|
globs=None, verbose=None, report=True, optionflags=0, |
|
extraglobs=None, raise_on_error=False, |
|
parser=pdoctest.DocTestParser(), encoding=None): |
|
|
|
""" |
|
Test examples in the given file. Return (#failures, #tests). |
|
|
|
Optional keyword arg ``module_relative`` specifies how filenames |
|
should be interpreted: |
|
|
|
- If ``module_relative`` is True (the default), then ``filename`` |
|
specifies a module-relative path. By default, this path is |
|
relative to the calling module's directory; but if the |
|
``package`` argument is specified, then it is relative to that |
|
package. To ensure os-independence, ``filename`` should use |
|
"/" characters to separate path segments, and should not |
|
be an absolute path (i.e., it may not begin with "/"). |
|
|
|
- If ``module_relative`` is False, then ``filename`` specifies an |
|
os-specific path. The path may be absolute or relative (to |
|
the current working directory). |
|
|
|
Optional keyword arg ``name`` gives the name of the test; by default |
|
use the file's basename. |
|
|
|
Optional keyword argument ``package`` is a Python package or the |
|
name of a Python package whose directory should be used as the |
|
base directory for a module relative filename. If no package is |
|
specified, then the calling module's directory is used as the base |
|
directory for module relative filenames. It is an error to |
|
specify ``package`` if ``module_relative`` is False. |
|
|
|
Optional keyword arg ``globs`` gives a dict to be used as the globals |
|
when executing examples; by default, use {}. A copy of this dict |
|
is actually used for each docstring, so that each docstring's |
|
examples start with a clean slate. |
|
|
|
Optional keyword arg ``extraglobs`` gives a dictionary that should be |
|
merged into the globals that are used to execute examples. By |
|
default, no extra globals are used. |
|
|
|
Optional keyword arg ``verbose`` prints lots of stuff if true, prints |
|
only failures if false; by default, it's true iff "-v" is in sys.argv. |
|
|
|
Optional keyword arg ``report`` prints a summary at the end when true, |
|
else prints nothing at the end. In verbose mode, the summary is |
|
detailed, else very brief (in fact, empty if all tests passed). |
|
|
|
Optional keyword arg ``optionflags`` or's together module constants, |
|
and defaults to 0. Possible values (see the docs for details): |
|
|
|
- DONT_ACCEPT_TRUE_FOR_1 |
|
- DONT_ACCEPT_BLANKLINE |
|
- NORMALIZE_WHITESPACE |
|
- ELLIPSIS |
|
- SKIP |
|
- IGNORE_EXCEPTION_DETAIL |
|
- REPORT_UDIFF |
|
- REPORT_CDIFF |
|
- REPORT_NDIFF |
|
- REPORT_ONLY_FIRST_FAILURE |
|
|
|
Optional keyword arg ``raise_on_error`` raises an exception on the |
|
first unexpected exception or failure. This allows failures to be |
|
post-mortem debugged. |
|
|
|
Optional keyword arg ``parser`` specifies a DocTestParser (or |
|
subclass) that should be used to extract tests from the files. |
|
|
|
Optional keyword arg ``encoding`` specifies an encoding that should |
|
be used to convert the file to unicode. |
|
|
|
Advanced tomfoolery: testmod runs methods of a local instance of |
|
class doctest.Tester, then merges the results into (or creates) |
|
global Tester instance doctest.master. Methods of doctest.master |
|
can be called directly too, if you want to do something unusual. |
|
Passing report=0 to testmod is especially useful then, to delay |
|
displaying a summary. Invoke doctest.master.summarize(verbose) |
|
when you're done fiddling. |
|
""" |
|
if package and not module_relative: |
|
raise ValueError("Package may only be specified for module-" |
|
"relative paths.") |
|
|
|
|
|
text, filename = pdoctest._load_testfile( |
|
filename, package, module_relative, encoding) |
|
|
|
|
|
if name is None: |
|
name = os.path.basename(filename) |
|
|
|
|
|
if globs is None: |
|
globs = {} |
|
else: |
|
globs = globs.copy() |
|
if extraglobs is not None: |
|
globs.update(extraglobs) |
|
if '__name__' not in globs: |
|
globs['__name__'] = '__main__' |
|
|
|
if raise_on_error: |
|
runner = pdoctest.DebugRunner(verbose=verbose, optionflags=optionflags) |
|
else: |
|
runner = SymPyDocTestRunner(verbose=verbose, optionflags=optionflags) |
|
runner._checker = SymPyOutputChecker() |
|
|
|
|
|
test = parser.get_doctest(text, globs, name, filename, 0) |
|
runner.run(test) |
|
|
|
if report: |
|
runner.summarize() |
|
|
|
if pdoctest.master is None: |
|
pdoctest.master = runner |
|
else: |
|
pdoctest.master.merge(runner) |
|
|
|
return SymPyTestResults(runner.failures, runner.tries) |
|
|
|
|
|
class SymPyTests: |
|
|
|
def __init__(self, reporter, kw="", post_mortem=False, |
|
seed=None, fast_threshold=None, slow_threshold=None): |
|
self._post_mortem = post_mortem |
|
self._kw = kw |
|
self._count = 0 |
|
self._root_dir = get_sympy_dir() |
|
self._reporter = reporter |
|
self._reporter.root_dir(self._root_dir) |
|
self._testfiles = [] |
|
self._seed = seed if seed is not None else random.random() |
|
|
|
|
|
|
|
|
|
|
|
|
|
if fast_threshold: |
|
self._fast_threshold = float(fast_threshold) |
|
else: |
|
self._fast_threshold = 8 |
|
if slow_threshold: |
|
self._slow_threshold = float(slow_threshold) |
|
else: |
|
self._slow_threshold = 10 |
|
|
|
def test(self, sort=False, timeout=False, slow=False, |
|
enhance_asserts=False, fail_on_timeout=False): |
|
""" |
|
Runs the tests returning True if all tests pass, otherwise False. |
|
|
|
If sort=False run tests in random order. |
|
""" |
|
if sort: |
|
self._testfiles.sort() |
|
elif slow: |
|
pass |
|
else: |
|
random.seed(self._seed) |
|
random.shuffle(self._testfiles) |
|
self._reporter.start(self._seed) |
|
for f in self._testfiles: |
|
try: |
|
self.test_file(f, sort, timeout, slow, |
|
enhance_asserts, fail_on_timeout) |
|
except KeyboardInterrupt: |
|
print(" interrupted by user") |
|
self._reporter.finish() |
|
raise |
|
return self._reporter.finish() |
|
|
|
def _enhance_asserts(self, source): |
|
from ast import (NodeTransformer, Compare, Name, Store, Load, Tuple, |
|
Assign, BinOp, Str, Mod, Assert, parse, fix_missing_locations) |
|
|
|
ops = {"Eq": '==', "NotEq": '!=', "Lt": '<', "LtE": '<=', |
|
"Gt": '>', "GtE": '>=', "Is": 'is', "IsNot": 'is not', |
|
"In": 'in', "NotIn": 'not in'} |
|
|
|
class Transform(NodeTransformer): |
|
def visit_Assert(self, stmt): |
|
if isinstance(stmt.test, Compare): |
|
compare = stmt.test |
|
values = [compare.left] + compare.comparators |
|
names = [ "_%s" % i for i, _ in enumerate(values) ] |
|
names_store = [ Name(n, Store()) for n in names ] |
|
names_load = [ Name(n, Load()) for n in names ] |
|
target = Tuple(names_store, Store()) |
|
value = Tuple(values, Load()) |
|
assign = Assign([target], value) |
|
new_compare = Compare(names_load[0], compare.ops, names_load[1:]) |
|
msg_format = "\n%s " + "\n%s ".join([ ops[op.__class__.__name__] for op in compare.ops ]) + "\n%s" |
|
msg = BinOp(Str(msg_format), Mod(), Tuple(names_load, Load())) |
|
test = Assert(new_compare, msg, lineno=stmt.lineno, col_offset=stmt.col_offset) |
|
return [assign, test] |
|
else: |
|
return stmt |
|
|
|
tree = parse(source) |
|
new_tree = Transform().visit(tree) |
|
return fix_missing_locations(new_tree) |
|
|
|
def test_file(self, filename, sort=True, timeout=False, slow=False, |
|
enhance_asserts=False, fail_on_timeout=False): |
|
reporter = self._reporter |
|
funcs = [] |
|
try: |
|
gl = {'__file__': filename} |
|
try: |
|
open_file = lambda: open(filename, encoding="utf8") |
|
|
|
with open_file() as f: |
|
source = f.read() |
|
if self._kw: |
|
for l in source.splitlines(): |
|
if l.lstrip().startswith('def '): |
|
if any(l.lower().find(k.lower()) != -1 for k in self._kw): |
|
break |
|
else: |
|
return |
|
|
|
if enhance_asserts: |
|
try: |
|
source = self._enhance_asserts(source) |
|
except ImportError: |
|
pass |
|
|
|
code = compile(source, filename, "exec", flags=0, dont_inherit=True) |
|
exec(code, gl) |
|
except (SystemExit, KeyboardInterrupt): |
|
raise |
|
except ImportError: |
|
reporter.import_error(filename, sys.exc_info()) |
|
return |
|
except Exception: |
|
reporter.test_exception(sys.exc_info()) |
|
|
|
clear_cache() |
|
self._count += 1 |
|
random.seed(self._seed) |
|
disabled = gl.get("disabled", False) |
|
if not disabled: |
|
|
|
|
|
|
|
funcs = [] |
|
for f in gl: |
|
if (f.startswith("test_") and (inspect.isfunction(gl[f]) |
|
or inspect.ismethod(gl[f]))): |
|
func = gl[f] |
|
|
|
while hasattr(func, '__wrapped__'): |
|
func = func.__wrapped__ |
|
|
|
if inspect.getsourcefile(func) == filename: |
|
funcs.append(gl[f]) |
|
if slow: |
|
funcs = [f for f in funcs if getattr(f, '_slow', False)] |
|
|
|
funcs.sort(key=lambda x: inspect.getsourcelines(x)[1]) |
|
i = 0 |
|
while i < len(funcs): |
|
if inspect.isgeneratorfunction(funcs[i]): |
|
|
|
|
|
f = funcs.pop(i) |
|
for fg in f(): |
|
func = fg[0] |
|
args = fg[1:] |
|
fgw = lambda: func(*args) |
|
funcs.insert(i, fgw) |
|
i += 1 |
|
else: |
|
i += 1 |
|
|
|
funcs = [x for x in funcs if self.matches(x)] |
|
|
|
if not funcs: |
|
return |
|
except Exception: |
|
reporter.entering_filename(filename, len(funcs)) |
|
raise |
|
|
|
reporter.entering_filename(filename, len(funcs)) |
|
if not sort: |
|
random.shuffle(funcs) |
|
|
|
for f in funcs: |
|
start = time.time() |
|
reporter.entering_test(f) |
|
try: |
|
if getattr(f, '_slow', False) and not slow: |
|
raise Skipped("Slow") |
|
with raise_on_deprecated(): |
|
if timeout: |
|
self._timeout(f, timeout, fail_on_timeout) |
|
else: |
|
random.seed(self._seed) |
|
f() |
|
except KeyboardInterrupt: |
|
if getattr(f, '_slow', False): |
|
reporter.test_skip("KeyboardInterrupt") |
|
else: |
|
raise |
|
except Exception: |
|
if timeout: |
|
signal.alarm(0) |
|
t, v, tr = sys.exc_info() |
|
if t is AssertionError: |
|
reporter.test_fail((t, v, tr)) |
|
if self._post_mortem: |
|
pdb.post_mortem(tr) |
|
elif t.__name__ == "Skipped": |
|
reporter.test_skip(v) |
|
elif t.__name__ == "XFail": |
|
reporter.test_xfail() |
|
elif t.__name__ == "XPass": |
|
reporter.test_xpass(v) |
|
else: |
|
reporter.test_exception((t, v, tr)) |
|
if self._post_mortem: |
|
pdb.post_mortem(tr) |
|
else: |
|
reporter.test_pass() |
|
taken = time.time() - start |
|
if taken > self._slow_threshold: |
|
filename = os.path.relpath(filename, reporter._root_dir) |
|
reporter.slow_test_functions.append( |
|
(filename + "::" + f.__name__, taken)) |
|
if getattr(f, '_slow', False) and slow: |
|
if taken < self._fast_threshold: |
|
filename = os.path.relpath(filename, reporter._root_dir) |
|
reporter.fast_test_functions.append( |
|
(filename + "::" + f.__name__, taken)) |
|
reporter.leaving_filename() |
|
|
|
def _timeout(self, function, timeout, fail_on_timeout): |
|
def callback(x, y): |
|
signal.alarm(0) |
|
if fail_on_timeout: |
|
raise TimeOutError("Timed out after %d seconds" % timeout) |
|
else: |
|
raise Skipped("Timeout") |
|
signal.signal(signal.SIGALRM, callback) |
|
signal.alarm(timeout) |
|
function() |
|
signal.alarm(0) |
|
|
|
def matches(self, x): |
|
""" |
|
Does the keyword expression self._kw match "x"? Returns True/False. |
|
|
|
Always returns True if self._kw is "". |
|
""" |
|
if not self._kw: |
|
return True |
|
for kw in self._kw: |
|
if x.__name__.lower().find(kw.lower()) != -1: |
|
return True |
|
return False |
|
|
|
def get_test_files(self, dir, pat='test_*.py'): |
|
""" |
|
Returns the list of test_*.py (default) files at or below directory |
|
``dir`` relative to the SymPy home directory. |
|
""" |
|
dir = os.path.join(self._root_dir, convert_to_native_paths([dir])[0]) |
|
|
|
g = [] |
|
for path, folders, files in os.walk(dir): |
|
g.extend([os.path.join(path, f) for f in files if fnmatch(f, pat)]) |
|
|
|
return sorted([os.path.normcase(gi) for gi in g]) |
|
|
|
|
|
class SymPyDocTests: |
|
|
|
def __init__(self, reporter, normal): |
|
self._count = 0 |
|
self._root_dir = get_sympy_dir() |
|
self._reporter = reporter |
|
self._reporter.root_dir(self._root_dir) |
|
self._normal = normal |
|
|
|
self._testfiles = [] |
|
|
|
def test(self): |
|
""" |
|
Runs the tests and returns True if all tests pass, otherwise False. |
|
""" |
|
self._reporter.start() |
|
for f in self._testfiles: |
|
try: |
|
self.test_file(f) |
|
except KeyboardInterrupt: |
|
print(" interrupted by user") |
|
self._reporter.finish() |
|
raise |
|
return self._reporter.finish() |
|
|
|
def test_file(self, filename): |
|
clear_cache() |
|
|
|
from io import StringIO |
|
import sympy.interactive.printing as interactive_printing |
|
from sympy.printing.pretty.pretty import pprint_use_unicode |
|
from sympy.printing.pretty import stringpict |
|
|
|
rel_name = filename[len(self._root_dir) + 1:] |
|
dirname, file = os.path.split(filename) |
|
module = rel_name.replace(os.sep, '.')[:-3] |
|
|
|
if rel_name.startswith("examples"): |
|
|
|
|
|
sys.path.insert(0, dirname) |
|
module = file[:-3] |
|
try: |
|
module = pdoctest._normalize_module(module) |
|
tests = SymPyDocTestFinder().find(module) |
|
except (SystemExit, KeyboardInterrupt): |
|
raise |
|
except ImportError: |
|
self._reporter.import_error(filename, sys.exc_info()) |
|
return |
|
finally: |
|
if rel_name.startswith("examples"): |
|
del sys.path[0] |
|
|
|
tests = [test for test in tests if len(test.examples) > 0] |
|
|
|
|
|
|
|
|
|
|
|
tests.sort(key=lambda x: -x.lineno) |
|
|
|
if not tests: |
|
return |
|
self._reporter.entering_filename(filename, len(tests)) |
|
for test in tests: |
|
assert len(test.examples) != 0 |
|
|
|
if self._reporter._verbose: |
|
self._reporter.write("\n{} ".format(test.name)) |
|
|
|
|
|
if '_doctest_depends_on' in test.globs: |
|
try: |
|
self._check_dependencies(**test.globs['_doctest_depends_on']) |
|
except DependencyError as e: |
|
self._reporter.test_skip(v=str(e)) |
|
continue |
|
|
|
runner = SymPyDocTestRunner(verbose=self._reporter._verbose==2, |
|
optionflags=pdoctest.ELLIPSIS | |
|
pdoctest.NORMALIZE_WHITESPACE | |
|
pdoctest.IGNORE_EXCEPTION_DETAIL) |
|
runner._checker = SymPyOutputChecker() |
|
old = sys.stdout |
|
new = old if self._reporter._verbose==2 else StringIO() |
|
sys.stdout = new |
|
|
|
|
|
|
|
|
|
|
|
|
|
if not self._normal: |
|
test.globs = {} |
|
|
|
|
|
|
|
old_displayhook = sys.displayhook |
|
use_unicode_prev, wrap_line_prev = setup_pprint() |
|
|
|
try: |
|
f, t = runner.run(test, |
|
out=new.write, clear_globs=False) |
|
except KeyboardInterrupt: |
|
raise |
|
finally: |
|
sys.stdout = old |
|
if f > 0: |
|
self._reporter.doctest_fail(test.name, new.getvalue()) |
|
else: |
|
self._reporter.test_pass() |
|
sys.displayhook = old_displayhook |
|
interactive_printing.NO_GLOBAL = False |
|
pprint_use_unicode(use_unicode_prev) |
|
stringpict._GLOBAL_WRAP_LINE = wrap_line_prev |
|
|
|
self._reporter.leaving_filename() |
|
|
|
def get_test_files(self, dir, pat='*.py', init_only=True): |
|
r""" |
|
Returns the list of \*.py files (default) from which docstrings |
|
will be tested which are at or below directory ``dir``. By default, |
|
only those that have an __init__.py in their parent directory |
|
and do not start with ``test_`` will be included. |
|
""" |
|
def importable(x): |
|
""" |
|
Checks if given pathname x is an importable module by checking for |
|
__init__.py file. |
|
|
|
Returns True/False. |
|
|
|
Currently we only test if the __init__.py file exists in the |
|
directory with the file "x" (in theory we should also test all the |
|
parent dirs). |
|
""" |
|
init_py = os.path.join(os.path.dirname(x), "__init__.py") |
|
return os.path.exists(init_py) |
|
|
|
dir = os.path.join(self._root_dir, convert_to_native_paths([dir])[0]) |
|
|
|
g = [] |
|
for path, folders, files in os.walk(dir): |
|
g.extend([os.path.join(path, f) for f in files |
|
if not f.startswith('test_') and fnmatch(f, pat)]) |
|
if init_only: |
|
|
|
g = [x for x in g if importable(x)] |
|
|
|
return [os.path.normcase(gi) for gi in g] |
|
|
|
def _check_dependencies(self, |
|
executables=(), |
|
modules=(), |
|
disable_viewers=(), |
|
python_version=(3, 5), |
|
ground_types=None): |
|
""" |
|
Checks if the dependencies for the test are installed. |
|
|
|
Raises ``DependencyError`` it at least one dependency is not installed. |
|
""" |
|
|
|
for executable in executables: |
|
if not shutil.which(executable): |
|
raise DependencyError("Could not find %s" % executable) |
|
|
|
for module in modules: |
|
if module == 'matplotlib': |
|
matplotlib = import_module( |
|
'matplotlib', |
|
import_kwargs={'fromlist': |
|
['pyplot', 'cm', 'collections']}, |
|
min_module_version='1.0.0', catch=(RuntimeError,)) |
|
if matplotlib is None: |
|
raise DependencyError("Could not import matplotlib") |
|
else: |
|
if not import_module(module): |
|
raise DependencyError("Could not import %s" % module) |
|
|
|
if disable_viewers: |
|
tempdir = tempfile.mkdtemp() |
|
os.environ['PATH'] = '%s:%s' % (tempdir, os.environ['PATH']) |
|
|
|
vw = ('#!/usr/bin/env python3\n' |
|
'import sys\n' |
|
'if len(sys.argv) <= 1:\n' |
|
' exit("wrong number of args")\n') |
|
|
|
for viewer in disable_viewers: |
|
Path(os.path.join(tempdir, viewer)).write_text(vw) |
|
|
|
|
|
os.chmod(os.path.join(tempdir, viewer), |
|
stat.S_IREAD | stat.S_IWRITE | stat.S_IXUSR) |
|
|
|
if python_version: |
|
if sys.version_info < python_version: |
|
raise DependencyError("Requires Python >= " + '.'.join(map(str, python_version))) |
|
|
|
if ground_types is not None: |
|
if GROUND_TYPES not in ground_types: |
|
raise DependencyError("Requires ground_types in " + str(ground_types)) |
|
|
|
if 'pyglet' in modules: |
|
|
|
|
|
import pyglet |
|
class DummyWindow: |
|
def __init__(self, *args, **kwargs): |
|
self.has_exit = True |
|
self.width = 600 |
|
self.height = 400 |
|
|
|
def set_vsync(self, x): |
|
pass |
|
|
|
def switch_to(self): |
|
pass |
|
|
|
def push_handlers(self, x): |
|
pass |
|
|
|
def close(self): |
|
pass |
|
|
|
pyglet.window.Window = DummyWindow |
|
|
|
|
|
class SymPyDocTestFinder(DocTestFinder): |
|
""" |
|
A class used to extract the DocTests that are relevant to a given |
|
object, from its docstring and the docstrings of its contained |
|
objects. Doctests can currently be extracted from the following |
|
object types: modules, functions, classes, methods, staticmethods, |
|
classmethods, and properties. |
|
|
|
Modified from doctest's version to look harder for code that |
|
appears comes from a different module. For example, the @vectorize |
|
decorator makes it look like functions come from multidimensional.py |
|
even though their code exists elsewhere. |
|
""" |
|
|
|
def _find(self, tests, obj, name, module, source_lines, globs, seen): |
|
""" |
|
Find tests for the given object and any contained objects, and |
|
add them to ``tests``. |
|
""" |
|
if self._verbose: |
|
print('Finding tests in %s' % name) |
|
|
|
|
|
if id(obj) in seen: |
|
return |
|
seen[id(obj)] = 1 |
|
|
|
|
|
|
|
if inspect.isclass(obj): |
|
if obj.__module__.split('.')[0] != 'sympy': |
|
return |
|
|
|
|
|
test = self._get_test(obj, name, module, globs, source_lines) |
|
if test is not None: |
|
tests.append(test) |
|
|
|
if not self._recurse: |
|
return |
|
|
|
|
|
if inspect.ismodule(obj): |
|
for rawname, val in obj.__dict__.items(): |
|
|
|
if inspect.isfunction(val) or inspect.isclass(val): |
|
|
|
|
|
if val.__module__ != module.__name__: |
|
continue |
|
|
|
assert self._from_module(module, val), \ |
|
"%s is not in module %s (rawname %s)" % (val, module, rawname) |
|
|
|
try: |
|
valname = '%s.%s' % (name, rawname) |
|
self._find(tests, val, valname, module, |
|
source_lines, globs, seen) |
|
except KeyboardInterrupt: |
|
raise |
|
|
|
|
|
for valname, val in getattr(obj, '__test__', {}).items(): |
|
if not isinstance(valname, str): |
|
raise ValueError("SymPyDocTestFinder.find: __test__ keys " |
|
"must be strings: %r" % |
|
(type(valname),)) |
|
if not (inspect.isfunction(val) or inspect.isclass(val) or |
|
inspect.ismethod(val) or inspect.ismodule(val) or |
|
isinstance(val, str)): |
|
raise ValueError("SymPyDocTestFinder.find: __test__ values " |
|
"must be strings, functions, methods, " |
|
"classes, or modules: %r" % |
|
(type(val),)) |
|
valname = '%s.__test__.%s' % (name, valname) |
|
self._find(tests, val, valname, module, source_lines, |
|
globs, seen) |
|
|
|
|
|
|
|
if inspect.isclass(obj): |
|
for valname, val in obj.__dict__.items(): |
|
|
|
if isinstance(val, staticmethod): |
|
val = getattr(obj, valname) |
|
if isinstance(val, classmethod): |
|
val = getattr(obj, valname).__func__ |
|
|
|
|
|
|
|
if ((inspect.isfunction(unwrap(val)) or |
|
inspect.isclass(val) or |
|
isinstance(val, property)) and |
|
self._from_module(module, val)): |
|
|
|
|
|
if isinstance(val, property): |
|
if hasattr(val.fget, '__module__'): |
|
if val.fget.__module__ != module.__name__: |
|
continue |
|
else: |
|
if val.__module__ != module.__name__: |
|
continue |
|
|
|
assert self._from_module(module, val), \ |
|
"%s is not in module %s (valname %s)" % ( |
|
val, module, valname) |
|
|
|
valname = '%s.%s' % (name, valname) |
|
self._find(tests, val, valname, module, source_lines, |
|
globs, seen) |
|
|
|
def _get_test(self, obj, name, module, globs, source_lines): |
|
""" |
|
Return a DocTest for the given object, if it defines a docstring; |
|
otherwise, return None. |
|
""" |
|
|
|
lineno = None |
|
|
|
|
|
|
|
if isinstance(obj, str): |
|
|
|
|
|
|
|
|
|
|
|
docstring = obj |
|
|
|
matches = re.findall(r"line \d+", name) |
|
assert len(matches) == 1, \ |
|
"string '%s' does not contain lineno " % name |
|
|
|
|
|
|
|
lineno = int(matches[0][5:]) |
|
|
|
else: |
|
docstring = getattr(obj, '__doc__', '') |
|
if docstring is None: |
|
docstring = '' |
|
if not isinstance(docstring, str): |
|
docstring = str(docstring) |
|
|
|
|
|
if self._exclude_empty and not docstring: |
|
return None |
|
|
|
|
|
|
|
if isinstance(obj, property): |
|
if obj.fget.__doc__ is None: |
|
return None |
|
|
|
|
|
if lineno is None: |
|
obj = unwrap(obj) |
|
|
|
|
|
if hasattr(obj, 'func_closure') and obj.func_closure is not None: |
|
tobj = obj.func_closure[0].cell_contents |
|
elif isinstance(obj, property): |
|
tobj = obj.fget |
|
else: |
|
tobj = obj |
|
lineno = self._find_lineno(tobj, source_lines) |
|
|
|
if lineno is None: |
|
return None |
|
|
|
|
|
if module is None: |
|
filename = None |
|
else: |
|
filename = getattr(module, '__file__', module.__name__) |
|
if filename[-4:] in (".pyc", ".pyo"): |
|
filename = filename[:-1] |
|
|
|
globs['_doctest_depends_on'] = getattr(obj, '_doctest_depends_on', {}) |
|
|
|
return self._parser.get_doctest(docstring, globs, name, |
|
filename, lineno) |
|
|
|
|
|
class SymPyDocTestRunner(DocTestRunner): |
|
""" |
|
A class used to run DocTest test cases, and accumulate statistics. |
|
The ``run`` method is used to process a single DocTest case. It |
|
returns a tuple ``(f, t)``, where ``t`` is the number of test cases |
|
tried, and ``f`` is the number of test cases that failed. |
|
|
|
Modified from the doctest version to not reset the sys.displayhook (see |
|
issue 5140). |
|
|
|
See the docstring of the original DocTestRunner for more information. |
|
""" |
|
|
|
def run(self, test, compileflags=None, out=None, clear_globs=True): |
|
""" |
|
Run the examples in ``test``, and display the results using the |
|
writer function ``out``. |
|
|
|
The examples are run in the namespace ``test.globs``. If |
|
``clear_globs`` is true (the default), then this namespace will |
|
be cleared after the test runs, to help with garbage |
|
collection. If you would like to examine the namespace after |
|
the test completes, then use ``clear_globs=False``. |
|
|
|
``compileflags`` gives the set of flags that should be used by |
|
the Python compiler when running the examples. If not |
|
specified, then it will default to the set of future-import |
|
flags that apply to ``globs``. |
|
|
|
The output of each example is checked using |
|
``SymPyDocTestRunner.check_output``, and the results are |
|
formatted by the ``SymPyDocTestRunner.report_*`` methods. |
|
""" |
|
self.test = test |
|
|
|
|
|
|
|
for example in test.examples: |
|
example.want = example.want.replace('```\n', '') |
|
example.exc_msg = example.exc_msg and example.exc_msg.replace('```\n', '') |
|
|
|
|
|
if compileflags is None: |
|
compileflags = pdoctest._extract_future_flags(test.globs) |
|
|
|
save_stdout = sys.stdout |
|
if out is None: |
|
out = save_stdout.write |
|
sys.stdout = self._fakeout |
|
|
|
|
|
|
|
|
|
|
|
|
|
save_set_trace = pdb.set_trace |
|
self.debugger = pdoctest._OutputRedirectingPdb(save_stdout) |
|
self.debugger.reset() |
|
pdb.set_trace = self.debugger.set_trace |
|
|
|
|
|
|
|
self.save_linecache_getlines = pdoctest.linecache.getlines |
|
linecache.getlines = self.__patched_linecache_getlines |
|
|
|
|
|
with raise_on_deprecated(): |
|
try: |
|
return self.__run(test, compileflags, out) |
|
finally: |
|
sys.stdout = save_stdout |
|
pdb.set_trace = save_set_trace |
|
linecache.getlines = self.save_linecache_getlines |
|
if clear_globs: |
|
test.globs.clear() |
|
|
|
|
|
|
|
monkeypatched_methods = [ |
|
'patched_linecache_getlines', |
|
'run', |
|
'record_outcome' |
|
] |
|
for method in monkeypatched_methods: |
|
oldname = '_DocTestRunner__' + method |
|
newname = '_SymPyDocTestRunner__' + method |
|
setattr(SymPyDocTestRunner, newname, getattr(DocTestRunner, oldname)) |
|
|
|
|
|
class SymPyOutputChecker(pdoctest.OutputChecker): |
|
""" |
|
Compared to the OutputChecker from the stdlib our OutputChecker class |
|
supports numerical comparison of floats occurring in the output of the |
|
doctest examples |
|
""" |
|
|
|
def __init__(self): |
|
|
|
|
|
|
|
got_floats = r'(\d+\.\d*|\.\d+)' |
|
|
|
|
|
want_floats = got_floats + r'(\.{3})?' |
|
|
|
front_sep = r'\s|\+|\-|\*|,' |
|
back_sep = front_sep + r'|j|e' |
|
|
|
fbeg = r'^%s(?=%s|$)' % (got_floats, back_sep) |
|
fmidend = r'(?<=%s)%s(?=%s|$)' % (front_sep, got_floats, back_sep) |
|
self.num_got_rgx = re.compile(r'(%s|%s)' %(fbeg, fmidend)) |
|
|
|
fbeg = r'^%s(?=%s|$)' % (want_floats, back_sep) |
|
fmidend = r'(?<=%s)%s(?=%s|$)' % (front_sep, want_floats, back_sep) |
|
self.num_want_rgx = re.compile(r'(%s|%s)' %(fbeg, fmidend)) |
|
|
|
def check_output(self, want, got, optionflags): |
|
""" |
|
Return True iff the actual output from an example (`got`) |
|
matches the expected output (`want`). These strings are |
|
always considered to match if they are identical; but |
|
depending on what option flags the test runner is using, |
|
several non-exact match types are also possible. See the |
|
documentation for `TestRunner` for more information about |
|
option flags. |
|
""" |
|
|
|
|
|
if got == want: |
|
return True |
|
|
|
|
|
|
|
|
|
matches = self.num_got_rgx.finditer(got) |
|
numbers_got = [match.group(1) for match in matches] |
|
matches = self.num_want_rgx.finditer(want) |
|
numbers_want = [match.group(1) for match in matches] |
|
if len(numbers_got) != len(numbers_want): |
|
return False |
|
|
|
if len(numbers_got) > 0: |
|
nw_ = [] |
|
for ng, nw in zip(numbers_got, numbers_want): |
|
if '...' in nw: |
|
nw_.append(ng) |
|
continue |
|
else: |
|
nw_.append(nw) |
|
|
|
if abs(float(ng)-float(nw)) > 1e-5: |
|
return False |
|
|
|
got = self.num_got_rgx.sub(r'%s', got) |
|
got = got % tuple(nw_) |
|
|
|
|
|
|
|
if not (optionflags & pdoctest.DONT_ACCEPT_BLANKLINE): |
|
|
|
want = re.sub(r'(?m)^%s\s*?$' % re.escape(pdoctest.BLANKLINE_MARKER), |
|
'', want) |
|
|
|
|
|
got = re.sub(r'(?m)^\s*?$', '', got) |
|
if got == want: |
|
return True |
|
|
|
|
|
|
|
|
|
if optionflags & pdoctest.NORMALIZE_WHITESPACE: |
|
got = ' '.join(got.split()) |
|
want = ' '.join(want.split()) |
|
if got == want: |
|
return True |
|
|
|
|
|
|
|
if optionflags & pdoctest.ELLIPSIS: |
|
if pdoctest._ellipsis_match(want, got): |
|
return True |
|
|
|
|
|
return False |
|
|
|
|
|
class Reporter: |
|
""" |
|
Parent class for all reporters. |
|
""" |
|
pass |
|
|
|
|
|
class PyTestReporter(Reporter): |
|
""" |
|
Py.test like reporter. Should produce output identical to py.test. |
|
""" |
|
|
|
def __init__(self, verbose=False, tb="short", colors=True, |
|
force_colors=False, split=None): |
|
self._verbose = verbose |
|
self._tb_style = tb |
|
self._colors = colors |
|
self._force_colors = force_colors |
|
self._xfailed = 0 |
|
self._xpassed = [] |
|
self._failed = [] |
|
self._failed_doctest = [] |
|
self._passed = 0 |
|
self._skipped = 0 |
|
self._exceptions = [] |
|
self._terminal_width = None |
|
self._default_width = 80 |
|
self._split = split |
|
self._active_file = '' |
|
self._active_f = None |
|
|
|
|
|
self.slow_test_functions = [] |
|
self.fast_test_functions = [] |
|
|
|
|
|
|
|
self._write_pos = 0 |
|
self._line_wrap = False |
|
|
|
def root_dir(self, dir): |
|
self._root_dir = dir |
|
|
|
@property |
|
def terminal_width(self): |
|
if self._terminal_width is not None: |
|
return self._terminal_width |
|
|
|
def findout_terminal_width(): |
|
if sys.platform == "win32": |
|
|
|
|
|
|
|
|
|
|
|
from ctypes import windll, create_string_buffer |
|
|
|
h = windll.kernel32.GetStdHandle(-12) |
|
csbi = create_string_buffer(22) |
|
res = windll.kernel32.GetConsoleScreenBufferInfo(h, csbi) |
|
|
|
if res: |
|
import struct |
|
(_, _, _, _, _, left, _, right, _, _, _) = \ |
|
struct.unpack("hhhhHhhhhhh", csbi.raw) |
|
return right - left |
|
else: |
|
return self._default_width |
|
|
|
if hasattr(sys.stdout, 'isatty') and not sys.stdout.isatty(): |
|
return self._default_width |
|
|
|
try: |
|
process = subprocess.Popen(['stty', '-a'], |
|
stdout=subprocess.PIPE, |
|
stderr=subprocess.PIPE) |
|
stdout, stderr = process.communicate() |
|
stdout = stdout.decode("utf-8") |
|
except OSError: |
|
pass |
|
else: |
|
|
|
|
|
|
|
|
|
|
|
|
|
re_linux = r"columns\s+(?P<columns>\d+);" |
|
re_osx = r"(?P<columns>\d+)\s*columns;" |
|
re_solaris = r"columns\s+=\s+(?P<columns>\d+);" |
|
|
|
for regex in (re_linux, re_osx, re_solaris): |
|
match = re.search(regex, stdout) |
|
|
|
if match is not None: |
|
columns = match.group('columns') |
|
|
|
try: |
|
width = int(columns) |
|
except ValueError: |
|
pass |
|
if width != 0: |
|
return width |
|
|
|
return self._default_width |
|
|
|
width = findout_terminal_width() |
|
self._terminal_width = width |
|
|
|
return width |
|
|
|
def write(self, text, color="", align="left", width=None, |
|
force_colors=False): |
|
""" |
|
Prints a text on the screen. |
|
|
|
It uses sys.stdout.write(), so no readline library is necessary. |
|
|
|
Parameters |
|
========== |
|
|
|
color : choose from the colors below, "" means default color |
|
align : "left"/"right", "left" is a normal print, "right" is aligned on |
|
the right-hand side of the screen, filled with spaces if |
|
necessary |
|
width : the screen width |
|
|
|
""" |
|
color_templates = ( |
|
("Black", "0;30"), |
|
("Red", "0;31"), |
|
("Green", "0;32"), |
|
("Brown", "0;33"), |
|
("Blue", "0;34"), |
|
("Purple", "0;35"), |
|
("Cyan", "0;36"), |
|
("LightGray", "0;37"), |
|
("DarkGray", "1;30"), |
|
("LightRed", "1;31"), |
|
("LightGreen", "1;32"), |
|
("Yellow", "1;33"), |
|
("LightBlue", "1;34"), |
|
("LightPurple", "1;35"), |
|
("LightCyan", "1;36"), |
|
("White", "1;37"), |
|
) |
|
|
|
colors = {} |
|
|
|
for name, value in color_templates: |
|
colors[name] = value |
|
c_normal = '\033[0m' |
|
c_color = '\033[%sm' |
|
|
|
if width is None: |
|
width = self.terminal_width |
|
|
|
if align == "right": |
|
if self._write_pos + len(text) > width: |
|
|
|
self.write("\n") |
|
self.write(" "*(width - self._write_pos - len(text))) |
|
|
|
if not self._force_colors and hasattr(sys.stdout, 'isatty') and not \ |
|
sys.stdout.isatty(): |
|
|
|
|
|
|
|
|
|
color = "" |
|
elif sys.platform == "win32": |
|
|
|
color = "" |
|
elif not self._colors: |
|
color = "" |
|
|
|
if self._line_wrap: |
|
if text[0] != "\n": |
|
sys.stdout.write("\n") |
|
|
|
|
|
if IS_WINDOWS: |
|
text = text.encode('raw_unicode_escape').decode('utf8', 'ignore') |
|
elif not sys.stdout.encoding.lower().startswith('utf'): |
|
text = text.encode(sys.stdout.encoding, 'backslashreplace' |
|
).decode(sys.stdout.encoding) |
|
|
|
if color == "": |
|
sys.stdout.write(text) |
|
else: |
|
sys.stdout.write("%s%s%s" % |
|
(c_color % colors[color], text, c_normal)) |
|
sys.stdout.flush() |
|
l = text.rfind("\n") |
|
if l == -1: |
|
self._write_pos += len(text) |
|
else: |
|
self._write_pos = len(text) - l - 1 |
|
self._line_wrap = self._write_pos >= width |
|
self._write_pos %= width |
|
|
|
def write_center(self, text, delim="="): |
|
width = self.terminal_width |
|
if text != "": |
|
text = " %s " % text |
|
idx = (width - len(text)) // 2 |
|
t = delim*idx + text + delim*(width - idx - len(text)) |
|
self.write(t + "\n") |
|
|
|
def write_exception(self, e, val, tb): |
|
|
|
tb = tb.tb_next |
|
t = traceback.format_exception(e, val, tb) |
|
self.write("".join(t)) |
|
|
|
def start(self, seed=None, msg="test process starts"): |
|
self.write_center(msg) |
|
executable = sys.executable |
|
v = tuple(sys.version_info) |
|
python_version = "%s.%s.%s-%s-%s" % v |
|
implementation = platform.python_implementation() |
|
if implementation == 'PyPy': |
|
implementation += " %s.%s.%s-%s-%s" % sys.pypy_version_info |
|
self.write("executable: %s (%s) [%s]\n" % |
|
(executable, python_version, implementation)) |
|
from sympy.utilities.misc import ARCH |
|
self.write("architecture: %s\n" % ARCH) |
|
from sympy.core.cache import USE_CACHE |
|
self.write("cache: %s\n" % USE_CACHE) |
|
version = '' |
|
if GROUND_TYPES =='gmpy': |
|
import gmpy2 as gmpy |
|
version = gmpy.version() |
|
self.write("ground types: %s %s\n" % (GROUND_TYPES, version)) |
|
numpy = import_module('numpy') |
|
self.write("numpy: %s\n" % (None if not numpy else numpy.__version__)) |
|
if seed is not None: |
|
self.write("random seed: %d\n" % seed) |
|
from sympy.utilities.misc import HASH_RANDOMIZATION |
|
self.write("hash randomization: ") |
|
hash_seed = os.getenv("PYTHONHASHSEED") or '0' |
|
if HASH_RANDOMIZATION and (hash_seed == "random" or int(hash_seed)): |
|
self.write("on (PYTHONHASHSEED=%s)\n" % hash_seed) |
|
else: |
|
self.write("off\n") |
|
if self._split: |
|
self.write("split: %s\n" % self._split) |
|
self.write('\n') |
|
self._t_start = clock() |
|
|
|
def finish(self): |
|
self._t_end = clock() |
|
self.write("\n") |
|
global text, linelen |
|
text = "tests finished: %d passed, " % self._passed |
|
linelen = len(text) |
|
|
|
def add_text(mytext): |
|
global text, linelen |
|
"""Break new text if too long.""" |
|
if linelen + len(mytext) > self.terminal_width: |
|
text += '\n' |
|
linelen = 0 |
|
text += mytext |
|
linelen += len(mytext) |
|
|
|
if len(self._failed) > 0: |
|
add_text("%d failed, " % len(self._failed)) |
|
if len(self._failed_doctest) > 0: |
|
add_text("%d failed, " % len(self._failed_doctest)) |
|
if self._skipped > 0: |
|
add_text("%d skipped, " % self._skipped) |
|
if self._xfailed > 0: |
|
add_text("%d expected to fail, " % self._xfailed) |
|
if len(self._xpassed) > 0: |
|
add_text("%d expected to fail but passed, " % len(self._xpassed)) |
|
if len(self._exceptions) > 0: |
|
add_text("%d exceptions, " % len(self._exceptions)) |
|
add_text("in %.2f seconds" % (self._t_end - self._t_start)) |
|
|
|
if self.slow_test_functions: |
|
self.write_center('slowest tests', '_') |
|
sorted_slow = sorted(self.slow_test_functions, key=lambda r: r[1]) |
|
for slow_func_name, taken in sorted_slow: |
|
print('%s - Took %.3f seconds' % (slow_func_name, taken)) |
|
|
|
if self.fast_test_functions: |
|
self.write_center('unexpectedly fast tests', '_') |
|
sorted_fast = sorted(self.fast_test_functions, |
|
key=lambda r: r[1]) |
|
for fast_func_name, taken in sorted_fast: |
|
print('%s - Took %.3f seconds' % (fast_func_name, taken)) |
|
|
|
if len(self._xpassed) > 0: |
|
self.write_center("xpassed tests", "_") |
|
for e in self._xpassed: |
|
self.write("%s: %s\n" % (e[0], e[1])) |
|
self.write("\n") |
|
|
|
if self._tb_style != "no" and len(self._exceptions) > 0: |
|
for e in self._exceptions: |
|
filename, f, (t, val, tb) = e |
|
self.write_center("", "_") |
|
if f is None: |
|
s = "%s" % filename |
|
else: |
|
s = "%s:%s" % (filename, f.__name__) |
|
self.write_center(s, "_") |
|
self.write_exception(t, val, tb) |
|
self.write("\n") |
|
|
|
if self._tb_style != "no" and len(self._failed) > 0: |
|
for e in self._failed: |
|
filename, f, (t, val, tb) = e |
|
self.write_center("", "_") |
|
self.write_center("%s::%s" % (filename, f.__name__), "_") |
|
self.write_exception(t, val, tb) |
|
self.write("\n") |
|
|
|
if self._tb_style != "no" and len(self._failed_doctest) > 0: |
|
for e in self._failed_doctest: |
|
filename, msg = e |
|
self.write_center("", "_") |
|
self.write_center("%s" % filename, "_") |
|
self.write(msg) |
|
self.write("\n") |
|
|
|
self.write_center(text) |
|
ok = len(self._failed) == 0 and len(self._exceptions) == 0 and \ |
|
len(self._failed_doctest) == 0 |
|
if not ok: |
|
self.write("DO *NOT* COMMIT!\n") |
|
return ok |
|
|
|
def entering_filename(self, filename, n): |
|
rel_name = filename[len(self._root_dir) + 1:] |
|
self._active_file = rel_name |
|
self._active_file_error = False |
|
self.write(rel_name) |
|
self.write("[%d] " % n) |
|
|
|
def leaving_filename(self): |
|
self.write(" ") |
|
if self._active_file_error: |
|
self.write("[FAIL]", "Red", align="right") |
|
else: |
|
self.write("[OK]", "Green", align="right") |
|
self.write("\n") |
|
if self._verbose: |
|
self.write("\n") |
|
|
|
def entering_test(self, f): |
|
self._active_f = f |
|
if self._verbose: |
|
self.write("\n" + f.__name__ + " ") |
|
|
|
def test_xfail(self): |
|
self._xfailed += 1 |
|
self.write("f", "Green") |
|
|
|
def test_xpass(self, v): |
|
message = str(v) |
|
self._xpassed.append((self._active_file, message)) |
|
self.write("X", "Green") |
|
|
|
def test_fail(self, exc_info): |
|
self._failed.append((self._active_file, self._active_f, exc_info)) |
|
self.write("F", "Red") |
|
self._active_file_error = True |
|
|
|
def doctest_fail(self, name, error_msg): |
|
|
|
error_msg = "\n".join(error_msg.split("\n")[1:]) |
|
self._failed_doctest.append((name, error_msg)) |
|
self.write("F", "Red") |
|
self._active_file_error = True |
|
|
|
def test_pass(self, char="."): |
|
self._passed += 1 |
|
if self._verbose: |
|
self.write("ok", "Green") |
|
else: |
|
self.write(char, "Green") |
|
|
|
def test_skip(self, v=None): |
|
char = "s" |
|
self._skipped += 1 |
|
if v is not None: |
|
message = str(v) |
|
if message == "KeyboardInterrupt": |
|
char = "K" |
|
elif message == "Timeout": |
|
char = "T" |
|
elif message == "Slow": |
|
char = "w" |
|
if self._verbose: |
|
if v is not None: |
|
self.write(message + ' ', "Blue") |
|
else: |
|
self.write(" - ", "Blue") |
|
self.write(char, "Blue") |
|
|
|
def test_exception(self, exc_info): |
|
self._exceptions.append((self._active_file, self._active_f, exc_info)) |
|
if exc_info[0] is TimeOutError: |
|
self.write("T", "Red") |
|
else: |
|
self.write("E", "Red") |
|
self._active_file_error = True |
|
|
|
def import_error(self, filename, exc_info): |
|
self._exceptions.append((filename, None, exc_info)) |
|
rel_name = filename[len(self._root_dir) + 1:] |
|
self.write(rel_name) |
|
self.write("[?] Failed to import", "Red") |
|
self.write(" ") |
|
self.write("[FAIL]", "Red", align="right") |
|
self.write("\n") |
|
|