1# encoding: utf-8
2import operator
3import sys
4import pytest
5import doctest
6
7from pytest import approx
8from operator import eq, ne
9from decimal import Decimal
10from fractions import Fraction
11
12inf, nan = float("inf"), float("nan")
13
14
15class MyDocTestRunner(doctest.DocTestRunner):
16
17    def __init__(self):
18        doctest.DocTestRunner.__init__(self)
19
20    def report_failure(self, out, test, example, got):
21        raise AssertionError(
22            "'{}' evaluates to '{}', not '{}'".format(
23                example.source.strip(), got.strip(), example.want.strip()
24            )
25        )
26
27
28class TestApprox(object):
29
30    def test_repr_string(self):
31        plus_minus = u"\u00b1" if sys.version_info[0] > 2 else u"+-"
32        tol1, tol2, infr = "1.0e-06", "2.0e-06", "inf"
33        assert repr(approx(1.0)) == "1.0 {pm} {tol1}".format(pm=plus_minus, tol1=tol1)
34        assert (
35            repr(approx([1.0, 2.0]))
36            == "approx([1.0 {pm} {tol1}, 2.0 {pm} {tol2}])".format(
37                pm=plus_minus, tol1=tol1, tol2=tol2
38            )
39        )
40        assert (
41            repr(approx((1.0, 2.0)))
42            == "approx((1.0 {pm} {tol1}, 2.0 {pm} {tol2}))".format(
43                pm=plus_minus, tol1=tol1, tol2=tol2
44            )
45        )
46        assert repr(approx(inf)) == "inf"
47        assert repr(approx(1.0, rel=nan)) == "1.0 {pm} ???".format(pm=plus_minus)
48        assert (
49            repr(approx(1.0, rel=inf))
50            == "1.0 {pm} {infr}".format(pm=plus_minus, infr=infr)
51        )
52        assert repr(approx(1.0j, rel=inf)) == "1j"
53
54        # Dictionaries aren't ordered, so we need to check both orders.
55        assert repr(approx({"a": 1.0, "b": 2.0})) in (
56            "approx({{'a': 1.0 {pm} {tol1}, 'b': 2.0 {pm} {tol2}}})".format(
57                pm=plus_minus, tol1=tol1, tol2=tol2
58            ),
59            "approx({{'b': 2.0 {pm} {tol2}, 'a': 1.0 {pm} {tol1}}})".format(
60                pm=plus_minus, tol1=tol1, tol2=tol2
61            ),
62        )
63
64    def test_operator_overloading(self):
65        assert 1 == approx(1, rel=1e-6, abs=1e-12)
66        assert not (1 != approx(1, rel=1e-6, abs=1e-12))
67        assert 10 != approx(1, rel=1e-6, abs=1e-12)
68        assert not (10 == approx(1, rel=1e-6, abs=1e-12))
69
70    def test_exactly_equal(self):
71        examples = [
72            (2.0, 2.0),
73            (0.1e200, 0.1e200),
74            (1.123e-300, 1.123e-300),
75            (12345, 12345.0),
76            (0.0, -0.0),
77            (345678, 345678),
78            (Decimal("1.0001"), Decimal("1.0001")),
79            (Fraction(1, 3), Fraction(-1, -3)),
80        ]
81        for a, x in examples:
82            assert a == approx(x)
83
84    def test_opposite_sign(self):
85        examples = [(eq, 1e-100, -1e-100), (ne, 1e100, -1e100)]
86        for op, a, x in examples:
87            assert op(a, approx(x))
88
89    def test_zero_tolerance(self):
90        within_1e10 = [(1.1e-100, 1e-100), (-1.1e-100, -1e-100)]
91        for a, x in within_1e10:
92            assert x == approx(x, rel=0.0, abs=0.0)
93            assert a != approx(x, rel=0.0, abs=0.0)
94            assert a == approx(x, rel=0.0, abs=5e-101)
95            assert a != approx(x, rel=0.0, abs=5e-102)
96            assert a == approx(x, rel=5e-1, abs=0.0)
97            assert a != approx(x, rel=5e-2, abs=0.0)
98
99    def test_negative_tolerance(self):
100        # Negative tolerances are not allowed.
101        illegal_kwargs = [
102            dict(rel=-1e100),
103            dict(abs=-1e100),
104            dict(rel=1e100, abs=-1e100),
105            dict(rel=-1e100, abs=1e100),
106            dict(rel=-1e100, abs=-1e100),
107        ]
108        for kwargs in illegal_kwargs:
109            with pytest.raises(ValueError):
110                1.1 == approx(1, **kwargs)
111
112    def test_inf_tolerance(self):
113        # Everything should be equal if the tolerance is infinite.
114        large_diffs = [(1, 1000), (1e-50, 1e50), (-1.0, -1e300), (0.0, 10)]
115        for a, x in large_diffs:
116            assert a != approx(x, rel=0.0, abs=0.0)
117            assert a == approx(x, rel=inf, abs=0.0)
118            assert a == approx(x, rel=0.0, abs=inf)
119            assert a == approx(x, rel=inf, abs=inf)
120
121    def test_inf_tolerance_expecting_zero(self):
122        # If the relative tolerance is zero but the expected value is infinite,
123        # the actual tolerance is a NaN, which should be an error.
124        illegal_kwargs = [dict(rel=inf, abs=0.0), dict(rel=inf, abs=inf)]
125        for kwargs in illegal_kwargs:
126            with pytest.raises(ValueError):
127                1 == approx(0, **kwargs)
128
129    def test_nan_tolerance(self):
130        illegal_kwargs = [dict(rel=nan), dict(abs=nan), dict(rel=nan, abs=nan)]
131        for kwargs in illegal_kwargs:
132            with pytest.raises(ValueError):
133                1.1 == approx(1, **kwargs)
134
135    def test_reasonable_defaults(self):
136        # Whatever the defaults are, they should work for numbers close to 1
137        # than have a small amount of floating-point error.
138        assert 0.1 + 0.2 == approx(0.3)
139
140    def test_default_tolerances(self):
141        # This tests the defaults as they are currently set.  If you change the
142        # defaults, this test will fail but you should feel free to change it.
143        # None of the other tests (except the doctests) should be affected by
144        # the choice of defaults.
145        examples = [
146            # Relative tolerance used.
147            (eq, 1e100 + 1e94, 1e100),
148            (ne, 1e100 + 2e94, 1e100),
149            (eq, 1e0 + 1e-6, 1e0),
150            (ne, 1e0 + 2e-6, 1e0),
151            # Absolute tolerance used.
152            (eq, 1e-100, +1e-106),
153            (eq, 1e-100, +2e-106),
154            (eq, 1e-100, 0),
155        ]
156        for op, a, x in examples:
157            assert op(a, approx(x))
158
159    def test_custom_tolerances(self):
160        assert 1e8 + 1e0 == approx(1e8, rel=5e-8, abs=5e0)
161        assert 1e8 + 1e0 == approx(1e8, rel=5e-9, abs=5e0)
162        assert 1e8 + 1e0 == approx(1e8, rel=5e-8, abs=5e-1)
163        assert 1e8 + 1e0 != approx(1e8, rel=5e-9, abs=5e-1)
164
165        assert 1e0 + 1e-8 == approx(1e0, rel=5e-8, abs=5e-8)
166        assert 1e0 + 1e-8 == approx(1e0, rel=5e-9, abs=5e-8)
167        assert 1e0 + 1e-8 == approx(1e0, rel=5e-8, abs=5e-9)
168        assert 1e0 + 1e-8 != approx(1e0, rel=5e-9, abs=5e-9)
169
170        assert 1e-8 + 1e-16 == approx(1e-8, rel=5e-8, abs=5e-16)
171        assert 1e-8 + 1e-16 == approx(1e-8, rel=5e-9, abs=5e-16)
172        assert 1e-8 + 1e-16 == approx(1e-8, rel=5e-8, abs=5e-17)
173        assert 1e-8 + 1e-16 != approx(1e-8, rel=5e-9, abs=5e-17)
174
175    def test_relative_tolerance(self):
176        within_1e8_rel = [(1e8 + 1e0, 1e8), (1e0 + 1e-8, 1e0), (1e-8 + 1e-16, 1e-8)]
177        for a, x in within_1e8_rel:
178            assert a == approx(x, rel=5e-8, abs=0.0)
179            assert a != approx(x, rel=5e-9, abs=0.0)
180
181    def test_absolute_tolerance(self):
182        within_1e8_abs = [(1e8 + 9e-9, 1e8), (1e0 + 9e-9, 1e0), (1e-8 + 9e-9, 1e-8)]
183        for a, x in within_1e8_abs:
184            assert a == approx(x, rel=0, abs=5e-8)
185            assert a != approx(x, rel=0, abs=5e-9)
186
187    def test_expecting_zero(self):
188        examples = [
189            (ne, 1e-6, 0.0),
190            (ne, -1e-6, 0.0),
191            (eq, 1e-12, 0.0),
192            (eq, -1e-12, 0.0),
193            (ne, 2e-12, 0.0),
194            (ne, -2e-12, 0.0),
195            (ne, inf, 0.0),
196            (ne, nan, 0.0),
197        ]
198        for op, a, x in examples:
199            assert op(a, approx(x, rel=0.0, abs=1e-12))
200            assert op(a, approx(x, rel=1e-6, abs=1e-12))
201
202    def test_expecting_inf(self):
203        examples = [
204            (eq, inf, inf),
205            (eq, -inf, -inf),
206            (ne, inf, -inf),
207            (ne, 0.0, inf),
208            (ne, nan, inf),
209        ]
210        for op, a, x in examples:
211            assert op(a, approx(x))
212
213    def test_expecting_nan(self):
214        examples = [
215            (eq, nan, nan),
216            (eq, -nan, -nan),
217            (eq, nan, -nan),
218            (ne, 0.0, nan),
219            (ne, inf, nan),
220        ]
221        for op, a, x in examples:
222            # Nothing is equal to NaN by default.
223            assert a != approx(x)
224
225            # If ``nan_ok=True``, then NaN is equal to NaN.
226            assert op(a, approx(x, nan_ok=True))
227
228    def test_int(self):
229        within_1e6 = [(1000001, 1000000), (-1000001, -1000000)]
230        for a, x in within_1e6:
231            assert a == approx(x, rel=5e-6, abs=0)
232            assert a != approx(x, rel=5e-7, abs=0)
233            assert approx(x, rel=5e-6, abs=0) == a
234            assert approx(x, rel=5e-7, abs=0) != a
235
236    def test_decimal(self):
237        within_1e6 = [
238            (Decimal("1.000001"), Decimal("1.0")),
239            (Decimal("-1.000001"), Decimal("-1.0")),
240        ]
241        for a, x in within_1e6:
242            assert a == approx(x)
243            assert a == approx(x, rel=Decimal("5e-6"), abs=0)
244            assert a != approx(x, rel=Decimal("5e-7"), abs=0)
245            assert approx(x, rel=Decimal("5e-6"), abs=0) == a
246            assert approx(x, rel=Decimal("5e-7"), abs=0) != a
247
248    def test_fraction(self):
249        within_1e6 = [
250            (1 + Fraction(1, 1000000), Fraction(1)),
251            (-1 - Fraction(-1, 1000000), Fraction(-1)),
252        ]
253        for a, x in within_1e6:
254            assert a == approx(x, rel=5e-6, abs=0)
255            assert a != approx(x, rel=5e-7, abs=0)
256            assert approx(x, rel=5e-6, abs=0) == a
257            assert approx(x, rel=5e-7, abs=0) != a
258
259    def test_complex(self):
260        within_1e6 = [
261            (1.000001 + 1.0j, 1.0 + 1.0j),
262            (1.0 + 1.000001j, 1.0 + 1.0j),
263            (-1.000001 + 1.0j, -1.0 + 1.0j),
264            (1.0 - 1.000001j, 1.0 - 1.0j),
265        ]
266        for a, x in within_1e6:
267            assert a == approx(x, rel=5e-6, abs=0)
268            assert a != approx(x, rel=5e-7, abs=0)
269            assert approx(x, rel=5e-6, abs=0) == a
270            assert approx(x, rel=5e-7, abs=0) != a
271
272    def test_list(self):
273        actual = [1 + 1e-7, 2 + 1e-8]
274        expected = [1, 2]
275
276        # Return false if any element is outside the tolerance.
277        assert actual == approx(expected, rel=5e-7, abs=0)
278        assert actual != approx(expected, rel=5e-8, abs=0)
279        assert approx(expected, rel=5e-7, abs=0) == actual
280        assert approx(expected, rel=5e-8, abs=0) != actual
281
282    def test_list_wrong_len(self):
283        assert [1, 2] != approx([1])
284        assert [1, 2] != approx([1, 2, 3])
285
286    def test_tuple(self):
287        actual = (1 + 1e-7, 2 + 1e-8)
288        expected = (1, 2)
289
290        # Return false if any element is outside the tolerance.
291        assert actual == approx(expected, rel=5e-7, abs=0)
292        assert actual != approx(expected, rel=5e-8, abs=0)
293        assert approx(expected, rel=5e-7, abs=0) == actual
294        assert approx(expected, rel=5e-8, abs=0) != actual
295
296    def test_tuple_wrong_len(self):
297        assert (1, 2) != approx((1,))
298        assert (1, 2) != approx((1, 2, 3))
299
300    def test_dict(self):
301        actual = {"a": 1 + 1e-7, "b": 2 + 1e-8}
302        # Dictionaries became ordered in python3.6, so switch up the order here
303        # to make sure it doesn't matter.
304        expected = {"b": 2, "a": 1}
305
306        # Return false if any element is outside the tolerance.
307        assert actual == approx(expected, rel=5e-7, abs=0)
308        assert actual != approx(expected, rel=5e-8, abs=0)
309        assert approx(expected, rel=5e-7, abs=0) == actual
310        assert approx(expected, rel=5e-8, abs=0) != actual
311
312    def test_dict_wrong_len(self):
313        assert {"a": 1, "b": 2} != approx({"a": 1})
314        assert {"a": 1, "b": 2} != approx({"a": 1, "c": 2})
315        assert {"a": 1, "b": 2} != approx({"a": 1, "b": 2, "c": 3})
316
317    def test_numpy_array(self):
318        np = pytest.importorskip("numpy")
319
320        actual = np.array([1 + 1e-7, 2 + 1e-8])
321        expected = np.array([1, 2])
322
323        # Return false if any element is outside the tolerance.
324        assert actual == approx(expected, rel=5e-7, abs=0)
325        assert actual != approx(expected, rel=5e-8, abs=0)
326        assert approx(expected, rel=5e-7, abs=0) == expected
327        assert approx(expected, rel=5e-8, abs=0) != actual
328
329        # Should be able to compare lists with numpy arrays.
330        assert list(actual) == approx(expected, rel=5e-7, abs=0)
331        assert list(actual) != approx(expected, rel=5e-8, abs=0)
332        assert actual == approx(list(expected), rel=5e-7, abs=0)
333        assert actual != approx(list(expected), rel=5e-8, abs=0)
334
335    def test_numpy_array_wrong_shape(self):
336        np = pytest.importorskip("numpy")
337
338        a12 = np.array([[1, 2]])
339        a21 = np.array([[1], [2]])
340
341        assert a12 != approx(a21)
342        assert a21 != approx(a12)
343
344    def test_doctests(self):
345        parser = doctest.DocTestParser()
346        test = parser.get_doctest(
347            approx.__doc__, {"approx": approx}, approx.__name__, None, None
348        )
349        runner = MyDocTestRunner()
350        runner.run(test)
351
352    def test_unicode_plus_minus(self, testdir):
353        """
354        Comparing approx instances inside lists should not produce an error in the detailed diff.
355        Integration test for issue #2111.
356        """
357        testdir.makepyfile(
358            """
359            import pytest
360            def test_foo():
361                assert [3] == [pytest.approx(4)]
362        """
363        )
364        expected = "4.0e-06"
365        result = testdir.runpytest()
366        result.stdout.fnmatch_lines(
367            ["*At index 0 diff: 3 != 4 * {}".format(expected), "=* 1 failed in *="]
368        )
369
370    @pytest.mark.parametrize(
371        "op",
372        [
373            pytest.param(operator.le, id="<="),
374            pytest.param(operator.lt, id="<"),
375            pytest.param(operator.ge, id=">="),
376            pytest.param(operator.gt, id=">"),
377        ],
378    )
379    def test_comparison_operator_type_error(self, op):
380        """
381        pytest.approx should raise TypeError for operators other than == and != (#2003).
382        """
383        with pytest.raises(TypeError):
384            op(1, approx(1, rel=1e-6, abs=1e-12))
385
386    def test_numpy_array_with_scalar(self):
387        np = pytest.importorskip("numpy")
388
389        actual = np.array([1 + 1e-7, 1 - 1e-8])
390        expected = 1.0
391
392        assert actual == approx(expected, rel=5e-7, abs=0)
393        assert actual != approx(expected, rel=5e-8, abs=0)
394        assert approx(expected, rel=5e-7, abs=0) == actual
395        assert approx(expected, rel=5e-8, abs=0) != actual
396
397    def test_numpy_scalar_with_array(self):
398        np = pytest.importorskip("numpy")
399
400        actual = 1.0
401        expected = np.array([1 + 1e-7, 1 - 1e-8])
402
403        assert actual == approx(expected, rel=5e-7, abs=0)
404        assert actual != approx(expected, rel=5e-8, abs=0)
405        assert approx(expected, rel=5e-7, abs=0) == actual
406        assert approx(expected, rel=5e-8, abs=0) != actual
407