blob: 9e25feb0b4d630ac8db86853561869cb2aaa17eb [file] [log] [blame]
# 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)
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 * {}".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))
def test_numpy_array_with_scalar(self):
np = pytest.importorskip("numpy")
actual = np.array([1 + 1e-7, 1 - 1e-8])
expected = 1.0
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_numpy_scalar_with_array(self):
np = pytest.importorskip("numpy")
actual = 1.0
expected = np.array([1 + 1e-7, 1 - 1e-8])
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