time-travel’s documentation¶
time-travel is a python library that allows users to write deterministic tests for time sensitive and I/O intensive code.
time-travel supports python 2.7, 3.5, 3.6, 3.7 and pypy on both Linux and Windows.
Quick start¶
Install¶
$ pip install time_travel
Usage¶
Here are two examples of how to use time-travel
. See the full tutorial for
more tips and tricks.
Mocking time-sensitive code¶
with TimeTravel():
start = time.time()
time.sleep(200)
assert time.time() == start + 200
Mocking I/O code¶
with TimeTravel() as tt:
sock = socket.socket()
tt.add_future_event(time_from_now=2, sock, t.event_types.select.WRITE)
now = time.time()
assert select.select([], [sock], []) == ([], [sock], [])
assert time.time() == now + 2
Tutorial¶
Why should I use time-travel?¶
Writing good tests can sometimes be a bit tricky, especially when you are testing code that uses a lot of I/O and has hard timing constraints.
The naïve approach for testing such code is to actually wait for the time to pass. This is bad. Horribly bad. Why?
- Tests shouldn’t take long.
- Time is not accurate. When you wait for timeouts, there’s always a threshold.
If your code expects exactly 5 seconds to pass, there’s no guarantee that
time.sleep
will wait exactly that long.
If you rely on timing in your tests - your build will never be reliable.
How does it work?¶
When loaded, the library mocks modules that access the machine’s time
(e.g. time
, datetime
) and I/O event handlers (e.g. poll
, select
)
and replaces them with an internal event-pool implementation that lets the user
choose when time moves forward and which I/O event will happen next.
The TimeTravel Context Manager¶
-
class
time_travel.
TimeTravel
(start_time=86400.0, **kwargs)[source]¶ Context-manager for patching time and I/O libraries.
Note
The initial time for the clock is set to 86,400 seconds since epoc. This is because Windows does not support any lower values. Sorry UNIX users!
Performance¶
The way the context manager works is that it changes references to patched
objects in loaded modules. By default time-travel
searches through
every loaded module in sys.modules
. This takes around 2 seconds.
Wait!! Don’t leave yet!! We managed to solve this!!!
To minimize search time, time_travel.TimeTravel
gets a keyword argument
named modules_to_patch
, which is a list of module names to search in.
For example, let’s say you’re testing a module named foobar:
import foobar
with TimeTravel(modules_to_patch=['foobar']) as t:
foobar.dostuff()
This will reduce the replace time to the bare minimum.
Note
When the default search method is used (without the modules_to_patch
argument) the following modules are skipped and not patched:
pytest
unittest
mock
threading
Moving Through Time¶
-
time.
time
()¶ Return the time stored in
time-travel
’s internal clock.
-
time.
sleep
(secs)¶ Move
time-travel
’s internal clock forward by secs seconds.
-
datetime.date.
today
()¶ Return a
datetime.date
object initialized to the day thattime-travel
’s internal clock is set to.
-
datetime.datetime.
today
()¶ Return a
datetime.datetime
object initialized to the day thattime-travel
’s internal clock is set to.
-
datetime.datetime.
now
()¶ Return a
datetime.datetime
object initialized to the time thattime-travel
’s internal clock is set to.
-
datetime.datetime.
utcnow
()¶ Return a
datetime.datetime
object initialized to the time thattime-travel
’s internal clock is set to (timezone naive).
Faking I/O Events¶
To mock I/O events, the user must tell time-travel
which event will happen,
for which file descriptor, and when. For that we have:
-
TimeTravel.
add_future_event
(time_from_now, fd, event)[source]¶ Add an event to the event pool.
Parameters: - time_from_now – When will the event happen.
- fd – The descriptor (usually a socket object) that the event will happen for.
- event – The event that will happen (implementation specific).
For example:
with TimeTravel() as t:
sock = socket.socket()
t.add_future_event(2, sock, EVENT)
Note
EVENT
is implementation specific for every event handler (select
,
poll
, etc.) and will be described in the corresponding handler’s
documentation.
-
select.
select
(rlist, wlist, xlist, timeout=None)¶ Mimics the behaviour of
select.select
.select
has no event types, it uses positional lists in order to distinguish between read-ready, write-ready and exception.TimeTravel.add_future_event()
requires an event type, so the following consts are provided:TimeTravel.event_types.select.READ
TimeTravel.event_types.select.WRITE
TimeTravel.event_types.select.EXCEPTIONAL
The mock returns the first event(s) that expire in the event pool and moves time forward to that point in time. For example, if the user added 2 events:
t.add_future_event(1, sock1, t.event_types.select.READ) t.add_future_event(2, sock2, t.event_types.select.READ)
Calling
select.select([sock1, sock2], [], [])
will return an rlist containing onlysock1
and the time will move forward by 1 second.
-
select.
poll
()¶ Return a
MockPollObject
that behaves exactly like the realPoll
object.Note
This patcher is not supported on Windows.
-
class
MockPollObject
(clock, event_pool)¶ A mock poll object.
-
MockPollObject.
modify
(fd, eventmask)¶ Modify an already registered fd’s event mask.
-
MockPollObject.
poll
(timeout=None)¶ Poll the set of registered file descriptors.
timeout is a value in milliseconds.
-
MockPollObject.
register
(fd, eventmask=None)¶ Register a file descriptor with the fake polling object.
-
MockPollObject.
unregister
(fd)¶ Remove a file descriptor tracked by the fake polling object.
-
The event type supplied to
TimeTravel.add_future_event()
is the event mask that is required by poll.poll() (select.POLLIN
,select.POLLOUT
, etc.).-
class
Examples¶
Skip timeouts¶
Tests are deterministic and take no time with time travel. For example:
with TimeTravel():
assert time.time() == 86400
time.sleep(200)
assert time.time() == 86600
with TimeTravel(modules_to_patch=__name__):
assert datetime.today() == datetime.fromtimestamp(86400)
time.sleep(250)
assert datetime.today() == datetime.fromtimestamp(86650)
import module1
import module2
with TimeTravel(modules_to_patch=['module1', 'module2']) as time_machine:
time_machine.set_time(100000)
module1.very_long_method()
module2.time_sensitive_method()
Patching I/O events modules¶
With time-travel
you can fake future events for I/O modules:
with TimeTravel() as t:
sock = socket.socket()
t.add_future_event(2, sock, t.event_types.select.WRITE)
now = t.clock.time
assert select.select([], [sock], []) == ([], [sock], [])
assert time.time() == now + 2
assert datetime_cls.today() == datetime_cls.fromtimestamp(now + 2)
Or using poll
(for supported platforms only):
with TimeTravel() as t:
sock = socket.socket()
t.add_future_event(2, sock, select.POLLIN)
poll = select.poll()
poll.register(sock, select.POLLIN | select.POLLOUT)
now = t.clock.time
assert poll.poll() == [(sock, select.POLLIN)]
assert time.time() == now + 2
Implementation Details¶
Internally, time-travel
has 2 main objects: a clock
, and
an event pool
.
The Clock¶
The clock is an object that holds the current time (as a float), and has
listeners
that are registered to it.
Whenever the time changes, the listeners’s callback is called with the new
time so they can react to it.
The Event Pool¶
The event pool keeps a set of events for different file descriptors, in different timestamps. The pool’s job is to keep those events and to retrieve them for different patchers.
Writing a Patcher¶
Lets create a new patcher that patches the time
module.
Your patcher should inherit from BasePatcher
and implement 2 methods:
- get_patched_module should return the actual module being patched by the patcher.
- get_patch_actions should return a list containing 3-tuples with the following information: (object_name, the_real_object, fake_object)
import time
from time_travel.patchers.basic_patcher import BasicPatcher
class MyNewPatcher(BasicPatcher):
def get_patched_module(self):
return time
def get_patch_actions(self):
return ('time.time', time.time, self._mock_time)
def _mock_time(self):
return 4 # Decided by a fair dice roll.
Adding the patcher to time-travel¶
time-travel
uses entry points to add external patchers to it.
For example let’s imagine that our MyNewPatcher
class is located in a file
named my_new_patcher.py
. In order to add the new patcher to time-travel
just add the new class to the time_travel.patchers entry point in
setup.py
:
from setuptools import setup
setup(
...,
entry_points={
'time_travel.patchers' : [
'my_new_patcher = my_new_patcher:MyNewPatcher',
],
}
)
Event Types Hooks¶
If you need to hook event types to TimeTravel.event_types
(like
select.select()
does) your patcher should override 2 methods:
- get_events_namespace should return a string that identifies the “namespace”
of the event types. For example, if this returns “foo”, your events will be
registered under
TimeTravel.event_types.foo
. - get_event_types should return an
Enum
object that contains the events.
For example:
from time_travel.patchers.basic_patcher import BasicPatcher
class MyNewPatcher(BasicPatcher):
@staticmethod
def get_events_namespace():
return "foo"
@staticmethod
def get_event_types():
return Enum("events", ['READ', 'WRITE'])