mirror of
https://github.com/tahoe-lafs/tahoe-lafs.git
synced 2025-01-03 03:36:44 +00:00
An Eliot-adjacent testing helper
This commit is contained in:
parent
6f7e1250e8
commit
9ad8e21530
101
src/allmydata/test/eliotutil.py
Normal file
101
src/allmydata/test/eliotutil.py
Normal 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
|
35
src/allmydata/test/test_eliotutil.py
Normal file
35
src/allmydata/test/test_eliotutil.py
Normal 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
|
Loading…
Reference in New Issue
Block a user