Implements infrastructure for doctesting Pint

The most important aspect is a custom OutputChecker used to circumvent
certain aspects of an easy to read documentation. It performs a series
of conversions to the actual and expected output string of each doctest.
If the standard checker fails, the following steps are taken stopping
when the first is successful:

  1. eval the strings and compare the results.
     (this is useful to compare unordered collections such as dicts)
  2. parse the repr of a Quantity object and compare values and units
     Importantly, values are compared allowing rounding errors (0.1% difference)
  3. parse the str repr of a Quantity object and compare values and units
     Again, values are compared allowing rounding errors (0.1% difference)
  4. Replace the repr of a Unit by the corresponding string and compare
     the resulting strings.
  5. If at least one replacement was done in 4, then start in 1 again.
This commit is contained in:
Hernan Grecco
2016-02-13 22:58:03 -03:00
parent 3939cba313
commit be1ed57e67
2 changed files with 109 additions and 1 deletions

View File

@@ -2,6 +2,7 @@
from __future__ import division, unicode_literals, print_function, absolute_import
import doctest
import os
import logging
from contextlib import contextmanager
@@ -10,6 +11,7 @@ from pint.compat import ndarray, unittest, np
from pint import logger, UnitRegistry
from pint.quantity import _Quantity
from pint.testsuite.helpers import PintOutputChecker
from logging.handlers import BufferingHandler
@@ -117,7 +119,9 @@ class QuantityTestCase(BaseTestCase):
def testsuite():
"""A testsuite that has all the pint tests.
"""
return unittest.TestLoader().discover(os.path.dirname(__file__))
suite = unittest.TestLoader().discover(os.path.dirname(__file__))
#add_docs(suite)
return suite
def main():
@@ -137,3 +141,41 @@ def run():
test_runner = unittest.TextTestRunner()
return test_runner.run(testsuite())
import math
_GLOBS = {
'wrapping.rst': {
'pendulum_period': lambda length: 2*math.pi*math.sqrt(length/9.806650),
'pendulum_period2': lambda length, swing_amplitude: 1.,
'pendulum_period_maxspeed': lambda length, swing_amplitude: (1., 2.),
'pendulum_period_error': lambda length: (1., False),
}
}
def add_docs(suite):
"""Add docs to suite
:type suite: unittest.TestSuite
"""
docpath = os.path.join(os.path.dirname(__file__), '..', '..', 'docs')
docpath = os.path.abspath(docpath)
if os.path.exists(docpath):
checker = PintOutputChecker()
for name in (name for name in os.listdir(docpath) if name.endswith('.rst')):
file = os.path.join(docpath, name)
suite.addTest(doctest.DocFileSuite(file,
module_relative=False,
checker=checker,
globs=_GLOBS.get(name, None)))
def test_docs():
suite = unittest.TestSuite()
add_docs(suite)
runner = unittest.TextTestRunner()
return runner.run(suite)

View File

@@ -2,7 +2,10 @@
from __future__ import division, unicode_literals, print_function, absolute_import
import doctest
from distutils.version import StrictVersion
import re
from pint.compat import unittest, HAS_NUMPY, HAS_UNCERTAINTIES, NUMPY_VER, PYTHON3
@@ -41,3 +44,66 @@ def requires_python2():
def requires_python3():
return unittest.skipUnless(PYTHON3, 'Requires Python 3.X.')
_number_re = '([-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?)'
_q_re = re.compile('<Quantity\(' + '\s*' + '(?P<magnitude>%s)' % _number_re +
'\s*,\s*' + "'(?P<unit>.*)'" + '\s*' + '\)>')
_sq_re = re.compile('\s*' + '(?P<magnitude>%s)' % _number_re +
'\s' + "(?P<unit>.*)")
_unit_re = re.compile('<Unit\((.*)\)>')
class PintOutputChecker(doctest.OutputChecker):
def check_output(self, want, got, optionflags):
check = super(PintOutputChecker, self).check_output(want, got, optionflags)
if check:
return check
try:
if eval(want) == eval(got):
return True
except:
pass
for regex in (_q_re, _sq_re):
try:
parsed_got = regex.match(got.replace(r'\\', '')).groupdict()
parsed_want = regex.match(want.replace(r'\\', '')).groupdict()
v1 = float(parsed_got['magnitude'])
v2 = float(parsed_want['magnitude'])
if abs(v1 - v2) > abs(v1) / 1000:
return False
if parsed_got['unit'] != parsed_want['unit']:
return False
return True
except:
pass
cnt = 0
for regex in (_unit_re, ):
try:
parsed_got, tmp = regex.subn('\1', got)
cnt += tmp
parsed_want, temp = regex.subn('\1', want)
cnt += tmp
if parsed_got == parsed_want:
return True
except:
pass
if cnt:
# If there was any replacement, we try again the previous methods.
return self.check_output(parsed_want, parsed_got, optionflags)
return False