Skip to content

Commit

Permalink
Support time.monotonic() and time.monotonic_ns() (#382)
Browse files Browse the repository at this point in the history
Co-authored-by: Adam Johnson <[email protected]>
  • Loading branch information
asottile-sentry and adamchainz authored Sep 18, 2023
1 parent c8e1ccf commit e2766de
Show file tree
Hide file tree
Showing 5 changed files with 103 additions and 4 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@
Changelog
=========

* Mock ``time.monotonic()`` and ``time.monotonic_ns()``.
They return the values of ``time.time()`` and ``time.time_ns()`` respectively, rather than real monotonic clocks.

Thanks to Anthony Sottile in `PR 382 <https://github.com/adamchainz/time-machine/pull/382>`__.

2.12.0 (2023-08-14)
-------------------

Expand Down
6 changes: 4 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -101,10 +101,12 @@ All datetime functions in the standard library are mocked to move to the destina

* ``datetime.datetime.now()``
* ``datetime.datetime.utcnow()``
* ``time.gmtime()``
* ``time.localtime()``
* ``time.clock_gettime()`` (only for ``CLOCK_REALTIME``)
* ``time.clock_gettime_ns()`` (only for ``CLOCK_REALTIME``)
* ``time.gmtime()``
* ``time.localtime()``
* ``time.monotonic()`` (not a real monotonic clock, returns ``time.time()``)
* ``time.monotonic_ns()`` (not a real monotonic clock, returns ``time.time_ns()``)
* ``time.strftime()``
* ``time.time()``
* ``time.time_ns()``
Expand Down
58 changes: 56 additions & 2 deletions src/_time_machine.c
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ typedef struct {
PyCFunction original_clock_gettime_ns;
PyCFunction original_gmtime;
PyCFunction original_localtime;
PyCFunction original_monotonic;
PyCFunction original_monotonic_ns;
PyCFunction original_strftime;
PyCFunction original_time;
PyCFunction original_time_ns;
Expand Down Expand Up @@ -162,7 +164,7 @@ _time_machine_original_clock_gettime_ns(PyObject *module, PyObject *args)
return result;
}
PyDoc_STRVAR(original_clock_gettime_ns_doc,
"original_clock_gettime_ns() -> floating point number\n\
"original_clock_gettime_ns() -> int\n\
\n\
Call time.clock_gettime_ns() after patching.");

Expand Down Expand Up @@ -234,6 +236,46 @@ PyDoc_STRVAR(original_localtime_doc,
\n\
Call time.localtime() after patching.");

/* time.monotonic() */

static PyObject*
_time_machine_original_monotonic(PyObject* module, PyObject* args)
{
_time_machine_state *state = get_time_machine_state(module);

PyObject *time_module = PyImport_ImportModule("time");

PyObject* result = state->original_monotonic(time_module, args);

Py_DECREF(time_module);

return result;
}
PyDoc_STRVAR(original_monotonic_doc,
"original_monotonic() -> floating point number\n\
\n\
Call time.monotonic() after patching.");

/* time.monotonic_ns() */

static PyObject*
_time_machine_original_monotonic_ns(PyObject* module, PyObject* args)
{
_time_machine_state *state = get_time_machine_state(module);

PyObject *time_module = PyImport_ImportModule("time");

PyObject* result = state->original_monotonic_ns(time_module, args);

Py_DECREF(time_module);

return result;
}
PyDoc_STRVAR(original_monotonic_ns_doc,
"original_monotonic_ns() -> int\n\
\n\
Call time.monotonic_ns() after patching.");

/* time.strftime() */

static PyObject*
Expand Down Expand Up @@ -332,7 +374,7 @@ _time_machine_original_time_ns(PyObject *module, PyObject *args)
return result;
}
PyDoc_STRVAR(original_time_ns_doc,
"original_time_ns() -> floating point number\n\
"original_time_ns() -> int\n\
\n\
Call time.time_ns() after patching.");

Expand Down Expand Up @@ -395,6 +437,16 @@ _time_machine_patch_if_needed(PyObject *module, PyObject *unused)
time_localtime->m_ml->ml_meth = _time_machine_localtime;
Py_DECREF(time_localtime);

PyCFunctionObject *time_monotonic = (PyCFunctionObject *) PyObject_GetAttrString(time_module, "monotonic");
state->original_monotonic = time_monotonic->m_ml->ml_meth;
time_monotonic->m_ml->ml_meth = _time_machine_time;
Py_DECREF(time_monotonic);

PyCFunctionObject *time_monotonic_ns = (PyCFunctionObject *) PyObject_GetAttrString(time_module, "monotonic_ns");
state->original_monotonic_ns = time_monotonic_ns->m_ml->ml_meth;
time_monotonic_ns->m_ml->ml_meth = _time_machine_time_ns;
Py_DECREF(time_monotonic_ns);

PyCFunctionObject *time_strftime = (PyCFunctionObject *) PyObject_GetAttrString(time_module, "strftime");
state->original_strftime = time_strftime->m_ml->ml_meth;
time_strftime->m_ml->ml_meth = _time_machine_strftime;
Expand Down Expand Up @@ -430,6 +482,8 @@ static PyMethodDef module_functions[] = {
{"original_clock_gettime_ns", (PyCFunction)_time_machine_original_clock_gettime_ns, METH_VARARGS, original_clock_gettime_ns_doc},
{"original_gmtime", (PyCFunction)_time_machine_original_gmtime, METH_VARARGS, original_gmtime_doc},
{"original_localtime", (PyCFunction)_time_machine_original_localtime, METH_VARARGS, original_localtime_doc},
{"original_monotonic", (PyCFunction)_time_machine_original_monotonic, METH_NOARGS, original_monotonic_doc},
{"original_monotonic_ns", (PyCFunction)_time_machine_original_monotonic_ns, METH_NOARGS, original_monotonic_ns_doc},
{"original_strftime", (PyCFunction)_time_machine_original_strftime, METH_VARARGS, original_strftime_doc},
{"original_time", (PyCFunction)_time_machine_original_time, METH_NOARGS, original_time_doc},
{"original_time_ns", (PyCFunction)_time_machine_original_time_ns, METH_NOARGS, original_time_ns_doc},
Expand Down
8 changes: 8 additions & 0 deletions src/time_machine/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,14 @@ def localtime(self, secs: float | None = None) -> struct_time:
result: struct_time = _time_machine.original_localtime(secs)
return result

def monotonic(self) -> float:
result: float = _time_machine.original_monotonic()
return result

def monotonic_ns(self) -> int:
result: int = _time_machine.original_monotonic_ns()
return result

def strftime(self, format: str, t: _TimeTuple | struct_time | None = None) -> str:
result: str
if t is not None:
Expand Down
30 changes: 30 additions & 0 deletions tests/test_time_machine.py
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,23 @@ def test_time_localtime_arg():
assert local_time.tm_mday == 1


def test_time_montonic():
with time_machine.travel(EPOCH, tick=False) as t:
assert time.monotonic() == EPOCH
t.shift(1)
assert time.monotonic() == EPOCH + 1


def test_time_monotonic_ns():
with time_machine.travel(EPOCH, tick=False) as t:
assert time.monotonic_ns() == int(EPOCH * NANOSECONDS_PER_SECOND)
t.shift(1)
assert (
time.monotonic_ns()
== int(EPOCH * NANOSECONDS_PER_SECOND) + NANOSECONDS_PER_SECOND
)


def test_time_strftime_format():
with time_machine.travel(EPOCH):
assert time.strftime("%Y-%m-%d") == "1970-01-01"
Expand Down Expand Up @@ -772,6 +789,19 @@ def test_time_localtime(self):
eh_now = time_machine.escape_hatch.time.localtime()
assert eh_now >= now

def test_time_monotonic(self):
with time_machine.travel(LIBRARY_EPOCH):
# real monotonic time counts from a small number
assert time_machine.escape_hatch.time.monotonic() < LIBRARY_EPOCH

def test_time_monotonic_ns(self):
with time_machine.travel(LIBRARY_EPOCH):
# real monotonic time counts from a small number
assert (
time_machine.escape_hatch.time.monotonic_ns()
< LIBRARY_EPOCH * NANOSECONDS_PER_SECOND
)

def test_time_strftime_no_arg(self):
today = dt.date.today()

Expand Down

0 comments on commit e2766de

Please sign in to comment.