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

Fix day-of-month overflow in datetimes() #2001

Merged
merged 2 commits into from Jun 26, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions hypothesis-python/RELEASE.rst
@@ -0,0 +1,4 @@
RELEASE_TYPE: patch

This patch makes :func:`~hypothesis.strategies.datetimes` more efficient,
as it now handles short months correctly by construction instead of filtering.
22 changes: 8 additions & 14 deletions hypothesis-python/src/hypothesis/searchstrategy/datetime.py
Expand Up @@ -18,6 +18,7 @@
from __future__ import absolute_import, division, print_function

import datetime as dt
from calendar import monthrange

from hypothesis.internal.conjecture import utils
from hypothesis.searchstrategy.strategies import SearchStrategy
Expand All @@ -44,39 +45,32 @@ def __init__(self, min_value, max_value, timezones_strat):
self.max_dt = max_value
self.tz_strat = timezones_strat

def _attempt_one_draw(self, data):
def do_draw(self, data):
result = dict()
cap_low, cap_high = True, True
for name in ("year", "month", "day", "hour", "minute", "second", "microsecond"):
low = getattr(self.min_dt if cap_low else dt.datetime.min, name)
high = getattr(self.max_dt if cap_high else dt.datetime.max, name)
if name == "day" and not cap_high:
_, high = monthrange(**result)
if name == "year":
val = utils.integer_range(data, low, high, 2000)
else:
val = utils.integer_range(data, low, high)
result[name] = val
cap_low = cap_low and val == low
cap_high = cap_high and val == high
result = dt.datetime(**result)
tz = data.draw(self.tz_strat)
try:
result = dt.datetime(**result)
if is_pytz_timezone(tz):
# Can't just construct; see http://pytz.sourceforge.net
return tz.normalize(tz.localize(result))
return result.replace(tzinfo=tz)
except (ValueError, OverflowError):
return None

def do_draw(self, data):
for _ in range(3):
result = self._attempt_one_draw(data)
if result is not None:
return result
data.note_event(
"3 attempts to create a datetime between %r and %r "
"with timezone from %r failed." % (self.min_dt, self.max_dt, self.tz_strat)
)
data.mark_invalid()
msg = "Failed to draw a datetime between %r and %r with timezone from %r."
data.note_event(msg % (self.min_dt, self.max_dt, self.tz_strat))
data.mark_invalid()


class DateStrategy(SearchStrategy):
Expand Down
25 changes: 4 additions & 21 deletions hypothesis-python/tests/cover/test_datetimes.py
Expand Up @@ -23,9 +23,7 @@

from hypothesis import given
from hypothesis.internal.compat import hrange
from hypothesis.internal.conjecture.data import ConjectureData, Status, StopTest
from hypothesis.searchstrategy.datetime import DatetimeStrategy
from hypothesis.strategies import binary, dates, datetimes, none, timedeltas, times
from hypothesis.strategies import dates, datetimes, timedeltas, times
from tests.common.debug import find_any, minimal


Expand Down Expand Up @@ -88,21 +86,6 @@ def test_bordering_on_a_leap_year():
assert x.year == 2004


def test_DatetimeStrategy_draw_may_fail():
def is_failure_inducing(b):
try:
return strat._attempt_one_draw(ConjectureData.for_buffer(b)) is None
except StopTest:
return False

strat = DatetimeStrategy(dt.datetime.min, dt.datetime.max, none())
failure_inducing = minimal(binary(), is_failure_inducing, timeout_after=30)
data = ConjectureData.for_buffer(failure_inducing * 100)
with pytest.raises(StopTest):
data.draw(strat)
assert data.status == Status.INVALID


def test_can_find_after_the_year_2000():
assert minimal(dates(), lambda x: x.year > 2000).year == 2001

Expand All @@ -111,9 +94,9 @@ def test_can_find_before_the_year_2000():
assert minimal(dates(), lambda x: x.year < 2000).year == 1999


def test_can_find_each_month():
for month in hrange(1, 13):
find_any(dates(), lambda x: x.month == month)
@pytest.mark.parametrize("month", hrange(1, 13))
def test_can_find_each_month(month):
find_any(dates(), lambda x: x.month == month)


def test_min_year_is_respected():
Expand Down
16 changes: 6 additions & 10 deletions hypothesis-python/tests/cover/test_verbosity.py
Expand Up @@ -23,6 +23,7 @@
from hypothesis._settings import Verbosity, settings
from hypothesis.reporting import default as default_reporter, with_reporter
from hypothesis.strategies import booleans, integers, lists
from tests.common.debug import minimal
from tests.common.utils import capture_out, fails


Expand Down Expand Up @@ -60,16 +61,11 @@ def test_foo(x):

def test_includes_progress_in_verbose_mode():
with capture_verbosity() as o:

def foo():
find(
lists(integers()),
lambda x: sum(x) >= 100,
settings=settings(verbosity=Verbosity.verbose, database=None),
)

foo()

minimal(
lists(integers(), min_size=1),
lambda x: sum(x) >= 100,
settings(verbosity=Verbosity.verbose),
)
out = o.getvalue()
assert out
assert u"Shrunk example" in out
Expand Down