# encoding: utf-8 import operator import sys import pytest import doctest from pytest import approx from operator import eq, ne from decimal import Decimal from fractions import Fraction inf, nan = float('inf'), float('nan') class MyDocTestRunner(doctest.DocTestRunner): def __init__(self): doctest.DocTestRunner.__init__(self) def report_failure(self, out, test, example, got): raise AssertionError("'{}' evaluates to '{}', not '{}'".format( example.source.strip(), got.strip(), example.want.strip())) class TestApprox(object): def test_repr_string(self): plus_minus = u'\u00b1' if sys.version_info[0] > 2 else u'+-' tol1, tol2, infr = '1.0e-06', '2.0e-06', 'inf' assert repr(approx(1.0)) == '1.0 {pm} {tol1}'.format(pm=plus_minus, tol1=tol1) assert repr(approx([1.0, 2.0])) == 'approx([1.0 {pm} {tol1}, 2.0 {pm} {tol2}])'.format( pm=plus_minus, tol1=tol1, tol2=tol2) assert repr(approx((1.0, 2.0))) == 'approx((1.0 {pm} {tol1}, 2.0 {pm} {tol2}))'.format( pm=plus_minus, tol1=tol1, tol2=tol2) assert repr(approx(inf)) == 'inf' assert repr(approx(1.0, rel=nan)) == '1.0 {pm} ???'.format(pm=plus_minus) assert repr(approx(1.0, rel=inf)) == '1.0 {pm} {infr}'.format(pm=plus_minus, infr=infr) assert repr(approx(1.0j, rel=inf)) == '1j' # Dictionaries aren't ordered, so we need to check both orders. assert repr(approx({'a': 1.0, 'b': 2.0})) in ( "approx({{'a': 1.0 {pm} {tol1}, 'b': 2.0 {pm} {tol2}}})".format(pm=plus_minus, tol1=tol1, tol2=tol2), "approx({{'b': 2.0 {pm} {tol2}, 'a': 1.0 {pm} {tol1}}})".format(pm=plus_minus, tol1=tol1, tol2=tol2), ) def test_operator_overloading(self): assert 1 == approx(1, rel=1e-6, abs=1e-12) assert not (1 != approx(1, rel=1e-6, abs=1e-12)) assert 10 != approx(1, rel=1e-6, abs=1e-12) assert not (10 == approx(1, rel=1e-6, abs=1e-12)) def test_exactly_equal(self): examples = [ (2.0, 2.0), (0.1e200, 0.1e200), (1.123e-300, 1.123e-300), (12345, 12345.0), (0.0, -0.0), (345678, 345678), (Decimal('1.0001'), Decimal('1.0001')), (Fraction(1, 3), Fraction(-1, -3)), ] for a, x in examples: assert a == approx(x) def test_opposite_sign(self): examples = [ (eq, 1e-100, -1e-100), (ne, 1e100, -1e100), ] for op, a, x in examples: assert op(a, approx(x)) def test_zero_tolerance(self): within_1e10 = [ (1.1e-100, 1e-100), (-1.1e-100, -1e-100), ] for a, x in within_1e10: assert x == approx(x, rel=0.0, abs=0.0) assert a != approx(x, rel=0.0, abs=0.0) assert a == approx(x, rel=0.0, abs=5e-101) assert a != approx(x, rel=0.0, abs=5e-102) assert a == approx(x, rel=5e-1, abs=0.0) assert a != approx(x, rel=5e-2, abs=0.0) def test_negative_tolerance(self): # Negative tolerances are not allowed. illegal_kwargs = [ dict(rel=-1e100), dict(abs=-1e100), dict(rel=1e100, abs=-1e100), dict(rel=-1e100, abs=1e100), dict(rel=-1e100, abs=-1e100), ] for kwargs in illegal_kwargs: with pytest.raises(ValueError): 1.1 == approx(1, **kwargs) def test_inf_tolerance(self): # Everything should be equal if the tolerance is infinite. large_diffs = [ (1, 1000), (1e-50, 1e50), (-1.0, -1e300), (0.0, 10), ] for a, x in large_diffs: assert a != approx(x, rel=0.0, abs=0.0) assert a == approx(x, rel=inf, abs=0.0) assert a == approx(x, rel=0.0, abs=inf) assert a == approx(x, rel=inf, abs=inf) def test_inf_tolerance_expecting_zero(self): # If the relative tolerance is zero but the expected value is infinite, # the actual tolerance is a NaN, which should be an error. illegal_kwargs = [ dict(rel=inf, abs=0.0), dict(rel=inf, abs=inf), ] for kwargs in illegal_kwargs: with pytest.raises(ValueError): 1 == approx(0, **kwargs) def test_nan_tolerance(self): illegal_kwargs = [ dict(rel=nan), dict(abs=nan), dict(rel=nan, abs=nan), ] for kwargs in illegal_kwargs: with pytest.raises(ValueError): 1.1 == approx(1, **kwargs) def test_reasonable_defaults(self): # Whatever the defaults are, they should work for numbers close to 1 # than have a small amount of floating-point error. assert 0.1 + 0.2 == approx(0.3) def test_default_tolerances(self): # This tests the defaults as they are currently set. If you change the # defaults, this test will fail but you should feel free to change it. # None of the other tests (except the doctests) should be affected by # the choice of defaults. examples = [ # Relative tolerance used. (eq, 1e100 + 1e94, 1e100), (ne, 1e100 + 2e94, 1e100), (eq, 1e0 + 1e-6, 1e0), (ne, 1e0 + 2e-6, 1e0), # Absolute tolerance used. (eq, 1e-100, + 1e-106), (eq, 1e-100, + 2e-106), (eq, 1e-100, 0), ] for op, a, x in examples: assert op(a, approx(x)) def test_custom_tolerances(self): assert 1e8 + 1e0 == approx(1e8, rel=5e-8, abs=5e0) assert 1e8 + 1e0 == approx(1e8, rel=5e-9, abs=5e0) assert 1e8 + 1e0 == approx(1e8, rel=5e-8, abs=5e-1) assert 1e8 + 1e0 != approx(1e8, rel=5e-9, abs=5e-1) assert 1e0 + 1e-8 == approx(1e0, rel=5e-8, abs=5e-8) assert 1e0 + 1e-8 == approx(1e0, rel=5e-9, abs=5e-8) assert 1e0 + 1e-8 == approx(1e0, rel=5e-8, abs=5e-9) assert 1e0 + 1e-8 != approx(1e0, rel=5e-9, abs=5e-9) assert 1e-8 + 1e-16 == approx(1e-8, rel=5e-8, abs=5e-16) assert 1e-8 + 1e-16 == approx(1e-8, rel=5e-9, abs=5e-16) assert 1e-8 + 1e-16 == approx(1e-8, rel=5e-8, abs=5e-17) assert 1e-8 + 1e-16 != approx(1e-8, rel=5e-9, abs=5e-17) def test_relative_tolerance(self): within_1e8_rel = [ (1e8 + 1e0, 1e8), (1e0 + 1e-8, 1e0), (1e-8 + 1e-16, 1e-8), ] for a, x in within_1e8_rel: assert a == approx(x, rel=5e-8, abs=0.0) assert a != approx(x, rel=5e-9, abs=0.0) def test_absolute_tolerance(self): within_1e8_abs = [ (1e8 + 9e-9, 1e8), (1e0 + 9e-9, 1e0), (1e-8 + 9e-9, 1e-8), ] for a, x in within_1e8_abs: assert a == approx(x, rel=0, abs=5e-8) assert a != approx(x, rel=0, abs=5e-9) def test_expecting_zero(self): examples = [ (ne, 1e-6, 0.0), (ne, -1e-6, 0.0), (eq, 1e-12, 0.0), (eq, -1e-12, 0.0), (ne, 2e-12, 0.0), (ne, -2e-12, 0.0), (ne, inf, 0.0), (ne, nan, 0.0), ] for op, a, x in examples: assert op(a, approx(x, rel=0.0, abs=1e-12)) assert op(a, approx(x, rel=1e-6, abs=1e-12)) def test_expecting_inf(self): examples = [ (eq, inf, inf), (eq, -inf, -inf), (ne, inf, -inf), (ne, 0.0, inf), (ne, nan, inf), ] for op, a, x in examples: assert op(a, approx(x)) def test_expecting_nan(self): examples = [ (eq, nan, nan), (eq, -nan, -nan), (eq, nan, -nan), (ne, 0.0, nan), (ne, inf, nan), ] for op, a, x in examples: # Nothing is equal to NaN by default. assert a != approx(x) # If ``nan_ok=True``, then NaN is equal to NaN. assert op(a, approx(x, nan_ok=True)) def test_int(self): within_1e6 = [ (1000001, 1000000), (-1000001, -1000000), ] for a, x in within_1e6: assert a == approx(x, rel=5e-6, abs=0) assert a != approx(x, rel=5e-7, abs=0) assert approx(x, rel=5e-6, abs=0) == a assert approx(x, rel=5e-7, abs=0) != a def test_decimal(self): within_1e6 = [ (Decimal('1.000001'), Decimal('1.0')), (Decimal('-1.000001'), Decimal('-1.0')), ] for a, x in within_1e6: assert a == approx(x, rel=Decimal('5e-6'), abs=0) assert a != approx(x, rel=Decimal('5e-7'), abs=0) assert approx(x, rel=Decimal('5e-6'), abs=0) == a assert approx(x, rel=Decimal('5e-7'), abs=0) != a def test_fraction(self): within_1e6 = [ (1 + Fraction(1, 1000000), Fraction(1)), (-1 - Fraction(-1, 1000000), Fraction(-1)), ] for a, x in within_1e6: assert a == approx(x, rel=5e-6, abs=0) assert a != approx(x, rel=5e-7, abs=0) assert approx(x, rel=5e-6, abs=0) == a assert approx(x, rel=5e-7, abs=0) != a def test_complex(self): within_1e6 = [ (1.000001 + 1.0j, 1.0 + 1.0j), (1.0 + 1.000001j, 1.0 + 1.0j), (-1.000001 + 1.0j, -1.0 + 1.0j), (1.0 - 1.000001j, 1.0 - 1.0j), ] for a, x in within_1e6: assert a == approx(x, rel=5e-6, abs=0) assert a != approx(x, rel=5e-7, abs=0) assert approx(x, rel=5e-6, abs=0) == a assert approx(x, rel=5e-7, abs=0) != a def test_list(self): actual = [1 + 1e-7, 2 + 1e-8] expected = [1, 2] # Return false if any element is outside the tolerance. assert actual == approx(expected, rel=5e-7, abs=0) assert actual != approx(expected, rel=5e-8, abs=0) assert approx(expected, rel=5e-7, abs=0) == actual assert approx(expected, rel=5e-8, abs=0) != actual def test_list_wrong_len(self): assert [1, 2] != approx([1]) assert [1, 2] != approx([1, 2, 3]) def test_tuple(self): actual = (1 + 1e-7, 2 + 1e-8) expected = (1, 2) # Return false if any element is outside the tolerance. assert actual == approx(expected, rel=5e-7, abs=0) assert actual != approx(expected, rel=5e-8, abs=0) assert approx(expected, rel=5e-7, abs=0) == actual assert approx(expected, rel=5e-8, abs=0) != actual def test_tuple_wrong_len(self): assert (1, 2) != approx((1,)) assert (1, 2) != approx((1, 2, 3)) def test_dict(self): actual = {'a': 1 + 1e-7, 'b': 2 + 1e-8} # Dictionaries became ordered in python3.6, so switch up the order here # to make sure it doesn't matter. expected = {'b': 2, 'a': 1} # Return false if any element is outside the tolerance. assert actual == approx(expected, rel=5e-7, abs=0) assert actual != approx(expected, rel=5e-8, abs=0) assert approx(expected, rel=5e-7, abs=0) == actual assert approx(expected, rel=5e-8, abs=0) != actual def test_dict_wrong_len(self): assert {'a': 1, 'b': 2} != approx({'a': 1}) assert {'a': 1, 'b': 2} != approx({'a': 1, 'c': 2}) assert {'a': 1, 'b': 2} != approx({'a': 1, 'b': 2, 'c': 3}) def test_numpy_array(self): np = pytest.importorskip('numpy') actual = np.array([1 + 1e-7, 2 + 1e-8]) expected = np.array([1, 2]) # Return false if any element is outside the tolerance. assert actual == approx(expected, rel=5e-7, abs=0) assert actual != approx(expected, rel=5e-8, abs=0) assert approx(expected, rel=5e-7, abs=0) == expected assert approx(expected, rel=5e-8, abs=0) != actual # Should be able to compare lists with numpy arrays. assert list(actual) == approx(expected, rel=5e-7, abs=0) assert list(actual) != approx(expected, rel=5e-8, abs=0) assert actual == approx(list(expected), rel=5e-7, abs=0) assert actual != approx(list(expected), rel=5e-8, abs=0) def test_numpy_array_wrong_shape(self): np = pytest.importorskip('numpy') a12 = np.array([[1, 2]]) a21 = np.array([[1], [2]]) assert a12 != approx(a21) assert a21 != approx(a12) def test_doctests(self): parser = doctest.DocTestParser() test = parser.get_doctest( approx.__doc__, {'approx': approx}, approx.__name__, None, None, ) runner = MyDocTestRunner() runner.run(test) def test_unicode_plus_minus(self, testdir): """ Comparing approx instances inside lists should not produce an error in the detailed diff. Integration test for issue #2111. """ testdir.makepyfile(""" import pytest def test_foo(): assert [3] == [pytest.approx(4)] """) expected = '4.0e-06' result = testdir.runpytest() result.stdout.fnmatch_lines([ '*At index 0 diff: 3 != 4 * {0}'.format(expected), '=* 1 failed in *=', ]) @pytest.mark.parametrize('op', [ pytest.param(operator.le, id='<='), pytest.param(operator.lt, id='<'), pytest.param(operator.ge, id='>='), pytest.param(operator.gt, id='>'), ]) def test_comparison_operator_type_error(self, op): """ pytest.approx should raise TypeError for operators other than == and != (#2003). """ with pytest.raises(TypeError): op(1, approx(1, rel=1e-6, abs=1e-12))