Source code for nti.testing.time
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Test support for working with clocks and time.
"""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
# stdlib imports
import functools
from threading import Lock
import time
from time import time as _real_time
from time import gmtime as _real_gmtime
import six
from nti.testing.mock import Mock
__docformat__ = "restructuredtext en"
_current_time = _real_time()
class _TimeWrapper(object):
def __init__(self, granularity=1.0):
self._granularity = granularity
self._lock = Lock()
self.fake_gmtime = Mock()
self.fake_time = 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 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
def install_fakes(self):
time.time = self.fake_time
time.gmtime = self.fake_gmtime
__enter__ = install_fakes
def close(self, *_args):
time.time = _real_time
time.gmtime = _real_gmtime
__exit__ = close
def __call__(self, func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
with self:
return func(*args, **kwargs)
return wrapper
[docs]
def time_monotonically_increases(func_or_granularity):
"""
Decorate a unittest method with this function to cause the value
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.
This decorator can be applied to a method in a test case::
class TestThing(unittest.TestCase)
@time_monotonically_increases
def test_method(self):
t = time.time()
...
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::
@time_monotonically_increases(0.1)
def smaller_increment():
t1 = time.time()
t2 = time.time()
assrt t2 == t1 + 0.1
.. seealso:: `nti.testing.time.reset_monotonic_time`
.. 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
Properly handle more than one argument to the underlying function.
.. versionchanged:: 2.0
Ensure that the values returned are at least the real time. Previously
they were integers starting at 0.
.. versionchanged:: 2.0
Ensure that the values returned are increasing even across different
invocations of a decorated function or different decorated functions. Previously
different functions would return the same sequence.
.. versionchanged:: 2.0
Allow specifying the granularity of clock increments. Keep at
1.0 seconds for backwards compatibility by default.
"""
if isinstance(func_or_granularity, (six.integer_types, float)):
# We're being used as a factory.
wrapper_factory = _TimeWrapper(func_or_granularity)
return wrapper_factory
# We're being used bare
wrapper_factory = _TimeWrapper()
return wrapper_factory(func_or_granularity)
[docs]
def reset_monotonic_time(value=0.0):
"""
Make the monotonic clock return the real time on its next
call.
.. versionadded:: 2.0
"""
global _current_time # pylint:disable=global-statement
_current_time = value
[docs]
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()