Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Float strategy can generate infinite values, even if explicitly excluded #1859

Closed
asmodehn opened this issue Mar 9, 2019 · 4 comments
Closed
Assignees
Labels
bug something is clearly wrong here

Comments

@asmodehn
Copy link

asmodehn commented Mar 9, 2019

When specifying infinity as the max of min value for a float, and then excluding this value, it can still be generated.

This is easy to fix with simple strategies, but when generating min and max values from another strategy, and relying on the exclude parameter to specify open or closed intervals to draw a float, it may cause problems.

Here is a sample code to reproduce the problem :

import pytest
from hypothesis import given, settings, Verbosity
from hypothesis.strategies import composite, floats, one_of

@given(val=floats(max_value=float('-inf'), allow_nan=False, exclude_max=True))
@settings(verbosity=Verbosity.verbose)
def test_should_be_always_true(val):
    assert val != float('-inf')

@given(val=floats(min_value=float('inf'), allow_nan=False, exclude_min=True))
@settings(verbosity=Verbosity.verbose)
def test_should_be_always_true_aswell(val):
    assert val != float('inf')
        
if __name__ == "__main__":
    pytest.main(["-s", __file__])

I would have expected the strategy to fail somehow... but it succeeds and generate only infinity, even tho it should be excluded.

So running this script I get :

================================================================================================================================= FAILURES =================================================================================================================================
________________________________________________________________________________________________________________________ test_should_be_always_true ________________________________________________________________________________________________________________________

    @given(val=floats(max_value=float('-inf'), allow_nan=False, exclude_max=True))
>   @settings(verbosity=Verbosity.verbose)
    def test_should_be_always_true(val):

float_inf_strat.py:6: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

val = -inf

    @given(val=floats(max_value=float('-inf'), allow_nan=False, exclude_max=True))
    @settings(verbosity=Verbosity.verbose)
    def test_should_be_always_true(val):
>       assert val != float('-inf')
E       AssertionError: assert -inf != -inf
E        +  where -inf = float('-inf')

float_inf_strat.py:8: AssertionError
-------------------------------------------------------------------------------------------------------------------------------- Hypothesis --------------------------------------------------------------------------------------------------------------------------------
Trying example: test_should_be_always_true(val=-inf)
Traceback (most recent call last):
  File "/home/alexv/Projects/tmp/float_inf_strat.py", line 8, in test_should_be_always_true
    assert val != float('-inf')
AssertionError: assert -inf != -inf
 +  where -inf = float('-inf')

Trying example: test_should_be_always_true(val=-inf)
Traceback (most recent call last):
  File "/home/alexv/Projects/tmp/float_inf_strat.py", line 8, in test_should_be_always_true
    assert val != float('-inf')
AssertionError: assert -inf != -inf
 +  where -inf = float('-inf')

Falsifying example: test_should_be_always_true(val=-inf)
____________________________________________________________________________________________________________________ test_should_be_always_true_aswell _____________________________________________________________________________________________________________________

    @given(val=floats(min_value=float('inf'), allow_nan=False, exclude_min=True))
>   @settings(verbosity=Verbosity.verbose)
    def test_should_be_always_true_aswell(val):

float_inf_strat.py:11: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

val = inf

    @given(val=floats(min_value=float('inf'), allow_nan=False, exclude_min=True))
    @settings(verbosity=Verbosity.verbose)
    def test_should_be_always_true_aswell(val):
>       assert val != float('inf')
E       AssertionError: assert inf != inf
E        +  where inf = float('inf')

float_inf_strat.py:13: AssertionError
-------------------------------------------------------------------------------------------------------------------------------- Hypothesis --------------------------------------------------------------------------------------------------------------------------------
Trying example: test_should_be_always_true_aswell(val=inf)
Traceback (most recent call last):
  File "/home/alexv/Projects/tmp/float_inf_strat.py", line 13, in test_should_be_always_true_aswell
    assert val != float('inf')
AssertionError: assert inf != inf
 +  where inf = float('inf')

Trying example: test_should_be_always_true_aswell(val=inf)
Traceback (most recent call last):
  File "/home/alexv/Projects/tmp/float_inf_strat.py", line 13, in test_should_be_always_true_aswell
    assert val != float('inf')
AssertionError: assert inf != inf
 +  where inf = float('inf')

Falsifying example: test_should_be_always_true_aswell(val=inf)
============================================================================================================================= warnings summary =============================================================================================================================
/home/alexv/.virtualenvs/tmptesthyp/lib/python3.6/site-packages/_pytest/config/__init__.py:752
  /home/alexv/.virtualenvs/tmptesthyp/lib/python3.6/site-packages/_pytest/config/__init__.py:752: PytestWarning: Module already imported so cannot be rewritten: hypothesis
    self._mark_plugins_for_rewrite(hook)

-- Docs: https://docs.pytest.org/en/latest/warnings.html
=================================================================================================================== 2 failed, 1 warnings in 0.08 seconds ===================================================================================================================

@Zac-HD Zac-HD added the bug something is clearly wrong here label Mar 9, 2019
@Zac-HD
Copy link
Member

Zac-HD commented Mar 9, 2019

Gah, I'd handled the max_value=-inf, allow_infinity=False cases in #1720, but didn't see the similar problem when adding exclude_max.

Unfortunately the solution will involve raise InvalidArgument(...), so while the invariants will be upheld you'll still have to ensure that you don't call floats() with such a combination of arguments.

@asmodehn
Copy link
Author

There is therefore a related question :
How can I avoid this :

E           KeyError: builds(MPFloat, floats(max_value=-1.7976931348623157e+308, allow_nan=False, allow_infinity=False, exclude_max=True))
-- snip --
E           hypothesis.errors.InvalidArgument: allow_infinity=False excludes max_value=-inf

It seems somewhere along the way my large float (likely around 64bits representable float limitations) has been reinterpreted as infinite.
Is there anyway to anticipate that ? preventively with an assume() call for example ?
FWIW, the float was generated with a simple draw(hypothesis.strategies.floats(allow_nan=False))

@Zac-HD
Copy link
Member

Zac-HD commented Mar 12, 2019

Your problem here is that max_value=-1.7976931348623157e+308 is the most-negative finite number representable as a 64-bit float. exclude_max=True therefore means that there are no allowed finite numbers, and allow_infinity=False means that nothing is allowed. (We should really improve this error message)

If you want a finite float that is always valid as an exclusive finite bound, you can draw one from floats(sys.float_info.min, sys.float_info.max, exclude_min=True, exclude_max=True). A filter would also work, but is tricky to implement without e.g. Numpy's nextafter function and slightly less efficient.

@asmodehn
Copy link
Author

Thanks I managed to cover my use case for now.
I am trying to test some mpmath related code with hypothesis.

In case it helps someone later, I first generate a mpmath.mpi (multiprecision interval) with :

@composite
def mpi_strat(draw)
        low_bound = draw(hypothesis.strategies.floats(allow_nan=False))
        high_bound = draw(
            hypothesis.strategies.floats(allow_nan=False, min_value=low_bound)
        )
        return mpmath.mpi(low_bound, high_bound)

And then to draw a float outside that interval when possible (with potentially infinite bounds).
a is the lower bound of the mpi :

@composite
def underbounds(draw,):
    b = draw(mpi_strat())
    assume(isfinite(b.a) and sys.float_info.min < float(b.a) < sys.float_info.max)
    v = draw(floats(max_value=float(b.a), exclude_max=True, allow_infinity=False))
    return v, b

I cannot test outside the representable float range, but it should be enough for my use-case (I m using mpmath for precision tracking purposes, not large numbers) so I'm fine with that.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug something is clearly wrong here
Projects
None yet
Development

No branches or pull requests

2 participants