An Eliot-adjacent testing helper

This commit is contained in:
Jean-Paul Calderone 2019-02-20 12:01:38 -05:00
parent 6f7e1250e8
commit 9ad8e21530
2 changed files with 136 additions and 0 deletions

View File

@ -0,0 +1,101 @@
"""
Tools aimed at the interaction between tests and Eliot.
"""
from functools import wraps
from eliot import (
ActionType,
Field,
)
from eliot.testing import capture_logging
from twisted.internet.defer import maybeDeferred
_NAME = Field.for_types(
u"name",
[unicode],
u"The name of the test.",
)
RUN_TEST = ActionType(
u"run-test",
[_NAME],
[],
u"A test is run.",
)
def with_eliot(f):
"""
Decorate a test method to run in a dedicated Eliot action context.
The action will finish after the test is done (after the returned Deferred
fires, if a Deferred is returned). It will note the name of the test
being run.
All messages emitted by the test will be validated. They will still be
delivered to the global logger.
"""
# A convenient, mutable container into which nested functions can write
# state to be shared among them.
class storage:
pass
@wraps(f)
def run_and_republish(self):
def republish():
# This is called as a cleanup function after capture_logging has
# restored the global/default logger to its original state. We
# can now emit messages that go to whatever global destinations
# are installed.
# Unfortunately the only way to get at the global/default
# logger...
from eliot._output import _DEFAULT_LOGGER as logger
# storage.logger.serialize() seems like it would make more sense
# than storage.logger.messages here. However, serialize()
# explodes, seemingly as a result of double-serializing the logged
# messages. I don't understand this.
for msg in storage.logger.messages:
logger.write(msg)
# And now that we've re-published all of the test's messages, we
# can finish the test's action.
storage.action.finish()
@capture_logging(None)
def run(self, logger):
# Record the MemoryLogger for later message extraction.
storage.logger = logger
return f(self)
# Arrange for all messages written to the memory logger that
# `capture_logging` installs to be re-written to the global/default
# logger so they might end up in a log file somewhere, if someone
# wants. This has to be done in a cleanup function (or later) because
# capture_logging restores the original logger in a cleanup function.
# We install our cleanup function here, before we call run, so that it
# runs *after* the cleanup function capture_logging installs (cleanup
# functions are a stack).
self.addCleanup(republish)
# Begin an action that should comprise all messages from the decorated
# test method.
with RUN_TEST(name=self.id().decode("utf-8")).context() as action:
# Support both Deferred-returning and non-Deferred-returning
# tests.
d = maybeDeferred(run, self)
# When the test method Deferred fires, the RUN_TEST action is
# done. However, we won't have re-published the MemoryLogger
# messages into the global/default logger when this Deferred
# fires. So we need to delay finishing the action until that has
# happened. Record the action so we can do that.
storage.action = action
# Let the test runner do its thing.
return d
return run_and_republish

View File

@ -0,0 +1,35 @@
"""
Tests for ``allmydata.test.eliotutil``.
"""
from eliot import (
Message,
)
from eliot.twisted import DeferredContext
from twisted.trial.unittest import TestCase
from twisted.internet.defer import succeed
from twisted.internet.task import deferLater
from twisted.internet import reactor
from .eliotutil import with_eliot
class WithEliotTests(TestCase):
@with_eliot
def test_returns_none(self):
Message.log(hello="world")
@with_eliot
def test_returns_fired_deferred(self):
Message.log(hello="world")
return succeed(None)
@with_eliot
def test_returns_unfired_deferred(self):
Message.log(hello="world")
# @with_eliot automatically gives us an action context but it's still
# our responsibility to maintain it across stack-busting operations.
d = DeferredContext(deferLater(reactor, 0.0, lambda: None))
d.addCallback(lambda ignored: Message.log(goodbye="world"))
# We didn't start an action. We're not finishing an action.
return d.result