-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
11 changed files
with
542 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,34 @@ | ||
# Log-based testing | ||
# Log-based testing 🪵 | ||
|
||
[![Build](https://github.com/etianen/logot/actions/workflows/build.yml/badge.svg)](https://github.com/etianen/logot/actions/workflows/build.yml) | ||
[![Docs](https://readthedocs.org/projects/logot/badge/)](https://logot.readthedocs.io) | ||
|
||
📖 [Read the docs](https://logot.readthedocs.io) 📖 | ||
`logot` makes it easy to test your application is logging correctly: | ||
|
||
``` python | ||
from logot import Logot, logged | ||
|
||
def test_my_app(logot: Logot) -> None: | ||
app.start() | ||
logot.wait_for(logged.info("App started")) | ||
``` | ||
|
||
|
||
## Documentation 📖 | ||
|
||
Full documentation is published on [Read the Docs](https://logot.readthedocs.io). | ||
|
||
|
||
## Bugs / feedback 🐛 | ||
|
||
Issue tracking is hosted on [GitHub](https://github.com/etianen/logot/issues). | ||
|
||
|
||
## Changelog 🏗️ | ||
|
||
Release notes are published on [GitHub](https://github.com/etianen/logot/releases). | ||
|
||
|
||
## License ⚖️ | ||
|
||
``logot`` is published as open-source software under the [MIT license](https://github.com/etianen/logot/blob/main/LICENSE). |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
API reference | ||
============= | ||
|
||
.. currentmodule:: logot | ||
|
||
Import the :mod:`logot` API in your tests: | ||
|
||
.. code:: python | ||
from logot import Logot, logged | ||
:mod:`logot` | ||
------------ | ||
|
||
.. module:: logot | ||
|
||
.. autoclass:: logot.Logot | ||
|
||
.. automethod:: capturing | ||
|
||
.. automethod:: wait_for | ||
|
||
.. automethod:: await_for | ||
|
||
.. automethod:: assert_logged | ||
|
||
.. automethod:: assert_not_logged | ||
|
||
.. autoattribute:: DEFAULT_LEVEL | ||
|
||
.. autoattribute:: DEFAULT_LOGGER | ||
|
||
.. autoattribute:: DEFAULT_TIMEOUT | ||
|
||
.. autoclass:: logot.Logged | ||
|
||
|
||
:mod:`logot.logged` | ||
------------------- | ||
|
||
.. module:: logot.logged | ||
|
||
.. autofunction:: logot.logged.log | ||
|
||
.. autofunction:: logot.logged.debug | ||
|
||
.. autofunction:: logot.logged.info | ||
|
||
.. autofunction:: logot.logged.warning | ||
|
||
.. autofunction:: logot.logged.error | ||
|
||
.. autofunction:: logot.logged.critical |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,11 +1,155 @@ | ||
Log-based testing | ||
================= | ||
Log-based testing 🪵 | ||
==================== | ||
|
||
.. automodule:: logot | ||
.. currentmodule:: logot | ||
|
||
:mod:`logot` makes it easy to test your application is logging correctly: | ||
|
||
.. code:: python | ||
from logot import Logot, logged | ||
def test_my_app(logot: Logot) -> None: | ||
app.start() | ||
logot.wait_for(logged.info("App started")) | ||
.. note:: | ||
|
||
These examples all show using :mod:`logot` with :mod:`pytest`. See :doc:`unittest` to learn about about using | ||
:mod:`logot` with other testing frameworks. | ||
|
||
|
||
Why test logging? 🤔 | ||
-------------------- | ||
|
||
Good logging ensures your application is debuggable at runtime, but why bother actually *testing* your logs? After | ||
all... surely the worst that can happen is your logs are a bit *wonky*? 🥴 | ||
|
||
Sometimes, testing logs is the only *reasonable* way to known your code has actually run correctly! This is particularly | ||
the case in *threaded* or *asynchronous* applications where work is carried out at unpredictable times by background | ||
workers. | ||
|
||
For example, imagine the following code running in a thread: | ||
|
||
.. code:: python | ||
def poll_daemon(app: App) -> None: | ||
while not app.stopping: | ||
sleep(app.poll_interval) | ||
logger.debug("Poll started") | ||
try: | ||
app.data = app.get("http://is-everything-ok.com/") | ||
except HTTPError: | ||
logger.exception("Poll error") | ||
else: | ||
logger.debug("Poll finished") | ||
.. note:: | ||
|
||
It's certainly *possible* to rewrite this code in a way that can be tested without :mod:`logot`, but that often makes | ||
the code less clear or more verbose. For complex threaded or asynchronous applications, this can quickly become | ||
burdensome. | ||
👎 | ||
|
||
Testing this code with :mod:`logot` is easy! | ||
|
||
.. code:: python | ||
from logot import Logot, logged | ||
def test_poll_daemon(logot: Logot) -> None: | ||
app.start_poll() | ||
for _ in range(3): | ||
logot.wait_for(logged.info("Poll started")) | ||
logot.wait_for(logged.info("Poll finished")) | ||
Testing threaded code | ||
--------------------- | ||
|
||
Use :meth:`Logot.wait_for` to pause your test until the expected logs arrive or the timeout expires: | ||
|
||
.. code:: python | ||
from logot import Logot, logged | ||
def test_my_app(logot: Logot) -> None: | ||
app.start() | ||
logot.wait_for(logged.info("App started")) | ||
.. note:: | ||
|
||
Use the ``timeout`` argument to :meth:`Logot.wait_for` to configure how long to wait before the test fails. This can | ||
be configured globally with the ``timeout`` argument to :class:`Logot`, defaulting to :attr:`Logot.DEFAULT_TIMEOUT`. | ||
|
||
.. seealso:: | ||
|
||
See :doc:`logged` for examples of how to wait for logs that may arrive in an unpredictable order. | ||
|
||
|
||
Testing asynchronous code | ||
------------------------- | ||
|
||
Use :meth:`Logot.await_for` to pause your test until the expected logs arrive or the timeout expires: | ||
|
||
.. code:: python | ||
from logot import Logot, logged | ||
async def test_my_app(logot: Logot) -> None: | ||
app.start() | ||
await logot.await_for(logged.info("App started")) | ||
.. note:: | ||
|
||
Use the ``timeout`` argument to :meth:`Logot.await_for` to configure how long to wait before the test fails. This can | ||
be configured globally with the ``timeout`` argument to :class:`Logot`, defaulting to :attr:`Logot.DEFAULT_TIMEOUT`. | ||
|
||
.. seealso:: | ||
|
||
See :doc:`logged` for examples of how to wait for logs that may arrive in an unpredictable order. | ||
|
||
|
||
Testing synchronous code | ||
------------------------ | ||
|
||
Use :meth:`Logot.assert_logged` to fail *immediately* if the expected logs have not arrived: | ||
|
||
.. code:: python | ||
from logot import Logot, logged | ||
def test_my_app(logot: Logot) -> None: | ||
app.run() | ||
logot.assert_logged(logged.info("App started")) | ||
.. note:: | ||
|
||
You can also use :meth:`Logot.wait_for` to test for expected logs, but since this only fails after a ``timeout``, | ||
using :meth:`Logot.assert_logged` will give more immediate feedback if your test fails. | ||
|
||
.. seealso:: | ||
|
||
Use :meth:`Logot.assert_not_logged` to fail *immediately* if the expected logs *do* arrive. | ||
|
||
|
||
Further reading | ||
--------------- | ||
|
||
Learn more about :mod:`logot` with the following guides: | ||
|
||
.. toctree:: | ||
:caption: Contents | ||
:hidden: | ||
:maxdepth: 1 | ||
|
||
self | ||
|
||
|
||
.. toctree:: | ||
:maxdepth: 1 | ||
|
||
match | ||
logged | ||
pytest | ||
unittest | ||
api |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
Log pattern matching | ||
==================== | ||
|
||
.. currentmodule:: logot | ||
|
||
:mod:`logot` makes it easy to match logs that may arrive in an unpredictable order. This is especially useful in | ||
*threaded* or *asynchronous* applications! | ||
|
||
Compose your :mod:`logot.logged` calls with special *log pattern operators*: | ||
|
||
.. code:: python | ||
from logot import Logot, logged | ||
def test_my_app(logot: Logot) -> None: | ||
app.start() | ||
logot.wait_for( | ||
# Wait for the app to start... | ||
logged.info("App started") | ||
# ...then wait for the app to stop *or* crash! | ||
>> ( | ||
logged.info("App stopped") | ||
| logged.error("App crashed!") | ||
) | ||
) | ||
.. note:: | ||
|
||
Log pattern operators are *infinitely* composable! Use ``()`` brackets when needed to define complex log patterns. | ||
|
||
|
||
Available operators | ||
------------------- | ||
|
||
Sequential logs | ||
~~~~~~~~~~~~~~~ | ||
|
||
Use the ``>>`` operator to wait for logs that must arrive in a *sequential* order: | ||
|
||
.. code:: python | ||
from logot import Logot, logged | ||
def test_my_app(logot: Logot) -> None: | ||
app.start() | ||
logot.wait_for( | ||
logged.info("App started") | ||
>> logged.info("App stopped") | ||
) | ||
Parallel logs | ||
~~~~~~~~~~~~~ | ||
|
||
Use the ``&`` operator to wait for logs that must arrive in *any* order: | ||
|
||
.. code:: python | ||
from logot import Logot, logged | ||
def test_my_app(logot: Logot) -> None: | ||
app.start() | ||
other_app.start() | ||
logot.wait_for( | ||
logged.info("App started") | ||
& logged.info("Other app started") | ||
) | ||
Any logs | ||
~~~~~~~~ | ||
|
||
Use the ``|`` operator to wait for *any* matching log pattern: | ||
|
||
.. code:: python | ||
from logot import Logot, logged | ||
def test_my_app(logot: Logot) -> None: | ||
app.start() | ||
logot.wait_for( | ||
logged.info("App stopped") | ||
| logged.error("App crashed!") | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
Log message matching | ||
==================== | ||
|
||
.. currentmodule:: logot | ||
|
||
:mod:`logot` makes it easy to match log messages using ``%``-style placeholders: | ||
|
||
.. code:: python | ||
from logot import Logot, logged | ||
def test_my_app(logot: Logot) -> None: | ||
app.start() | ||
logot.wait_for(logged.info("App %s")) | ||
In this case, the ``%s`` placeholder will match *any* string! | ||
|
||
|
||
Available placeholders | ||
---------------------- | ||
|
||
The following placeholders are available, each corresponding to a formatting option available in the stdlib | ||
:mod:`logging` module: | ||
|
||
=========== =========================================================================================================== | ||
Placeholder Matches | ||
=========== =========================================================================================================== | ||
``%d`` Signed integer decimal. | ||
``%i`` Signed integer decimal. | ||
``%o`` Signed octal value. | ||
``%u`` Signed integer decimal. | ||
``%x`` Signed hexadecimal (lowercase). | ||
``%X`` Signed hexadecimal (uppercase). | ||
``%e`` Floating point exponential format (lowercase). | ||
``%E`` Floating point exponential format (uppercase). | ||
``%f`` Floating point decimal format (lowercase). | ||
``%F`` Floating point decimal format (uppercase). | ||
``%g`` Floating point format. Uses lowercase exponential format if exponent is less than -4 or not less than precision, decimal format otherwise. | ||
``%G`` Floating point format. Uses uppercase exponential format if exponent is less than -4 or not less than precision, decimal format otherwise. | ||
``%c`` Single character. | ||
``%r`` Any string (non-greedy). | ||
``%s`` Any string (non-greedy). | ||
``%a`` Any string (non-greedy). | ||
``%%`` Escape sequence, results in a ``%`` character in the result. | ||
=========== =========================================================================================================== |
Oops, something went wrong.