Skip to content

Commit

Permalink
Merge pull request #16 from NextThought/issue15
Browse files Browse the repository at this point in the history
Add Python 3.7, handle time.gmtime() and add a layer helper.
  • Loading branch information
jamadden committed Aug 23, 2018
2 parents 6c1e11e + 9afe93a commit 58e5658
Show file tree
Hide file tree
Showing 7 changed files with 145 additions and 37 deletions.
5 changes: 5 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ python:
- 3.6
- pypy
- pypy3
matrix:
include:
- python: 3.7
dist: xenial
sudo: true
script:
- coverage run -m zope.testrunner --test-path=src --auto-color --auto-progress

Expand Down
7 changes: 5 additions & 2 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@
=========


2.1.1 (unreleased)
2.2.0 (unreleased)
==================

- Nothing changed yet.
- Add support for Python 3.7.

- Make ``time_monotonically_increases`` also handle ``time.gmtime``
and add a helper for using it in layers.


2.1.0 (2017-10-23)
Expand Down
3 changes: 2 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from setuptools import setup, find_packages


version = '2.1.1.dev0'
version = '2.2.0.dev0'

entry_points = {
}
Expand Down Expand Up @@ -38,6 +38,7 @@ def _read(fname):
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: Implementation :: CPython',
'Programming Language :: Python :: Implementation :: PyPy',
'Topic :: Software Development :: Testing',
Expand Down
8 changes: 6 additions & 2 deletions src/nti/testing/matchers.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@
from __future__ import print_function

# stdlib imports
import collections
try:
from collections.abc import Sequence, Mapping
except ImportError:
# Python 2
from collections import Sequence, Mapping
import pprint

import six
Expand Down Expand Up @@ -304,7 +308,7 @@ def aq_inContextOf(parent):
_orig_append_description_of = BaseDescription.append_description_of
def _append_description_of_map(self, value):
if not hasattr(value, 'describe_to'):
if isinstance(value, (collections.Mapping, collections.Sequence)):
if isinstance(value, (Mapping, Sequence)):
sio = StringIO()
pprint.pprint(value, sio)
self.append(sio.getvalue())
Expand Down
51 changes: 45 additions & 6 deletions src/nti/testing/tests/test_time.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,15 @@ def _check_time(self, granularity=1.0):
before = time.time()
after = time.time()
assert_that(after, is_(greater_than_or_equal_to(before + granularity)))


assert_that(time.gmtime(after), is_(time.gmtime(after)))
assert_that(time.gmtime(after), is_(greater_than_or_equal_to(time.gmtime(before))))

if granularity >= 1.0:
gm_before = time.gmtime()
gm_after = time.gmtime()
assert_that(gm_after, is_(greater_than(gm_before)))
return after

@time_monotonically_increases
Expand Down Expand Up @@ -70,6 +79,7 @@ def test_increases_across_funcs(self):
import time
before_all = time.time()


@time_monotonically_increases(granularity)
def f1():
return self._check_time(granularity)
Expand All @@ -78,12 +88,20 @@ def f1():
def f2():
return self._check_time(granularity)

@time_monotonically_increases(granularity)
def f3():
return time.time()

self._check_across_funcs(before_all, f1, f2, f3)

def _check_across_funcs(self, before_all, f1, f2, f3):
from nti.testing.time import _real_time
after_first = f1()

assert_that(after_first, is_(greater_than(before_all)))

after_second = f2()
current_real = time.time()
current_real = _real_time()

# The loop in self._check_time incremented the clock by a full second.
# That function should have taken far less time to actually run than that, though,
Expand All @@ -94,7 +112,7 @@ def f2():
# And immediately running it again will continue to produce values that
# are ever larger.
after_second = f2()
current_real = time.time()
current_real = _real_time()

assert_that(current_real, is_(less_than(after_second)))

Expand All @@ -106,13 +124,34 @@ def f2():
# apparently are calling time.time() too, which makes it unpredictable.
# We don't want to sleep for a long time, so we
# reset the clock again to prove that we can go back to real time.
@time_monotonically_increases(granularity)
def f3():
return time.time()

# Use a value in the distant past to account for the calls that get made.
reset_monotonic_time(-50)

after_third = f3()
current_real = time.time()
current_real = _real_time()
assert_that(current_real, is_(greater_than_or_equal_to(after_third)))


def test_layer(self):
from nti.testing.time import MonotonicallyIncreasingTimeLayerMixin
import time
granularity = 0.1

def f1():
return self._check_time(granularity)

def f2():
return self._check_time(granularity)

def f3():
return time.time()

before_all = time.time()

layer = MonotonicallyIncreasingTimeLayerMixin(granularity)
layer.testSetUp()
try:
self._check_across_funcs(before_all, f1, f2, f3)
finally:
layer.testTearDown()
106 changes: 81 additions & 25 deletions src/nti/testing/time.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@

# stdlib imports
import functools
from threading import Lock
import time
from time import time as _real_time
from time import gmtime as _real_gmtime

try:
from unittest import mock
Expand All @@ -23,45 +26,71 @@

_current_time = _real_time()


class _TimeWrapper(object):

def __init__(self, granularity=1.0):
self._granularity = granularity
self._lock = Lock()
self.fake_gmtime = mock.Mock()
self.fake_time = mock.Mock()
self._configure_fakes()

def _configure_fakes(self):
def incr():
global _current_time # pylint:disable=global-statement
with self._lock:
_current_time = max(_real_time(), _current_time + self._granularity)
return _current_time
self.fake_time.side_effect = incr

def __call__(self, func):
@mock.patch('time.time')
@functools.wraps(func)
def wrapper(*args, **kwargs):
def incr_gmtime(*seconds):
if seconds:
assert len(seconds) == 1
now = seconds[0]
else:
now = incr()
return _real_gmtime(now)
self.fake_gmtime.side_effect = incr_gmtime

fake_time = args[-1]
assert isinstance(fake_time, mock.Mock), args
args = args[:-1]
def install_fakes(self):
time.time = self.fake_time
time.gmtime = self.fake_gmtime

# make time monotonically increasing
def incr():
global _current_time # pylint:disable=global-statement
_current_time = max(_real_time(), _current_time + self._granularity)
return _current_time
fake_time.side_effect = incr
__enter__ = install_fakes

def close(self, *args):
time.time = _real_time
time.gmtime = _real_gmtime

return func(*args, **kwargs)
__exit__ = close

def __call__(self, func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
with self:
return func(*args, **kwargs)
return wrapper



def time_monotonically_increases(func_or_granularity):
"""
Decorate a unittest method with this function to cause the value
of :func:`time.time` to monotonically increase by one each time it
is called. This ensures things like last modified dates always
increase.
of :func:`time.time` (and :func:`time.gmtime`) to monotonically
increase by one each time it is called. This ensures things like
last modified dates always increase.
We make three guarantees about the value of :func:`time.time`
returned while the decorated function is running:
1. It is always *at least* the value of the *real* :func:`time.time`;
2. Each call returns a value greater than the previous call;
3. Those two constraints hold across different invocations of functions
decorated.
1. It is always *at least* the value of the *real*
:func:`time.time`;
2. Each call returns a value greater than the previous call;
3. Those two constraints hold across different invocations of
functions decorated.
This decorator can be applied to a method in a test case::
Expand All @@ -71,23 +100,28 @@ def test_method(self):
t = time.time()
...
It can also be applied to a bare function taking any number of arguments::
It can also be applied to a bare function taking any number of
arguments::
@time_monotonically_increases
def utility_function(a, b, c=1):
t = time.time()
...
By default, the time will be incremented in 1.0 second intervals. You can
specify a particular granularity as an argument; this is useful to keep from
running too far ahead of the real clock::
By default, the time will be incremented in 1.0 second intervals.
You can specify a particular granularity as an argument; this is
useful to keep from running too far ahead of the real clock::
@time_monotonically_increases(0.1)
def smaller_increment():
t1 = time.time()
t2 = time.time()
assrt t2 == t1 + 0.1
.. versionchanged:: 2.1
Add support for ``time.gmtime``.
.. versionchanged:: 2.1
Now thread safe.
.. versionchanged:: 2.0
The decorated function returns whatever the passed function returns.
.. versionchanged:: 2.0
Expand All @@ -112,6 +146,7 @@ def smaller_increment():
wrapper_factory = _TimeWrapper()
return wrapper_factory(func_or_granularity)


def reset_monotonic_time(value=0.0):
"""
Make the monotonic clock return the real time on its next
Expand All @@ -122,3 +157,24 @@ def reset_monotonic_time(value=0.0):

global _current_time # pylint:disable=global-statement
_current_time = value


class MonotonicallyIncreasingTimeLayerMixin(object):
"""
A helper for layers that need time to increase monotonically.
You can either mix this in to a layer object, or instantiate it
and call the methods directly.
.. versionadded:: 2.2
"""

def __init__(self, granularity=1.0):
self.time_manager = _TimeWrapper(granularity)

def testSetUp(self):
self.time_manager.install_fakes()

def testTearDown(self):
self.time_manager.close()
reset_monotonic_time()
2 changes: 1 addition & 1 deletion tox.ini
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[tox]
envlist = pypy,py27,py35,py36,pypy3,coverage,docs
envlist = pypy,py27,py35,py36,py37,pypy3,coverage,docs

[testenv]
deps =
Expand Down

0 comments on commit 58e5658

Please sign in to comment.