Skip to content

Commit

Permalink
Documentation (#19)
Browse files Browse the repository at this point in the history
  • Loading branch information
etianen authored Jan 23, 2024
1 parent 7a10a40 commit 12cc011
Show file tree
Hide file tree
Showing 11 changed files with 542 additions and 7 deletions.
33 changes: 31 additions & 2 deletions README.md
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).
54 changes: 54 additions & 0 deletions docs/api.rst
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
1 change: 1 addition & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,5 @@

intersphinx_mapping = {
"python": ("https://docs.python.org/3", None),
"pytest": ("https://docs.pytest.org/en/latest/", None),
}
152 changes: 148 additions & 4 deletions docs/index.rst
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
84 changes: 84 additions & 0 deletions docs/logged.rst
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!")
)
45 changes: 45 additions & 0 deletions docs/match.rst
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.
=========== ===========================================================================================================
Loading

0 comments on commit 12cc011

Please sign in to comment.