17.12
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Test cases for L{twisted.logger._buffer}.
|
||||
"""
|
||||
|
||||
from zope.interface.verify import verifyObject, BrokenMethodImplementation
|
||||
|
||||
from twisted.trial import unittest
|
||||
|
||||
from .._observer import ILogObserver
|
||||
from .._buffer import LimitedHistoryLogObserver
|
||||
|
||||
|
||||
|
||||
class LimitedHistoryLogObserverTests(unittest.TestCase):
|
||||
"""
|
||||
Tests for L{LimitedHistoryLogObserver}.
|
||||
"""
|
||||
|
||||
def test_interface(self):
|
||||
"""
|
||||
L{LimitedHistoryLogObserver} provides L{ILogObserver}.
|
||||
"""
|
||||
observer = LimitedHistoryLogObserver(0)
|
||||
try:
|
||||
verifyObject(ILogObserver, observer)
|
||||
except BrokenMethodImplementation as e:
|
||||
self.fail(e)
|
||||
|
||||
|
||||
def test_order(self):
|
||||
"""
|
||||
L{LimitedHistoryLogObserver} saves history in the order it is received.
|
||||
"""
|
||||
size = 4
|
||||
events = [dict(n=n) for n in range(size//2)]
|
||||
observer = LimitedHistoryLogObserver(size)
|
||||
|
||||
for event in events:
|
||||
observer(event)
|
||||
|
||||
outEvents = []
|
||||
observer.replayTo(outEvents.append)
|
||||
self.assertEqual(events, outEvents)
|
||||
|
||||
|
||||
def test_limit(self):
|
||||
"""
|
||||
When more events than a L{LimitedHistoryLogObserver}'s maximum size are
|
||||
buffered, older events will be dropped.
|
||||
"""
|
||||
size = 4
|
||||
events = [dict(n=n) for n in range(size*2)]
|
||||
observer = LimitedHistoryLogObserver(size)
|
||||
|
||||
for event in events:
|
||||
observer(event)
|
||||
outEvents = []
|
||||
observer.replayTo(outEvents.append)
|
||||
self.assertEqual(events[-size:], outEvents)
|
||||
@@ -0,0 +1,39 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Test cases for L{twisted.logger._capture}.
|
||||
"""
|
||||
|
||||
from twisted.logger import Logger, LogLevel
|
||||
from twisted.trial.unittest import TestCase
|
||||
|
||||
from .._capture import capturedLogs
|
||||
|
||||
|
||||
|
||||
class LogCaptureTests(TestCase):
|
||||
"""
|
||||
Tests for L{LogCaptureTests}.
|
||||
"""
|
||||
|
||||
log = Logger()
|
||||
|
||||
|
||||
def test_capture(self):
|
||||
"""
|
||||
Events logged within context are captured.
|
||||
"""
|
||||
foo = object()
|
||||
|
||||
with capturedLogs() as captured:
|
||||
self.log.debug("Capture this, please", foo=foo)
|
||||
self.log.info("Capture this too, please", foo=foo)
|
||||
|
||||
self.assertTrue(len(captured) == 2)
|
||||
self.assertEqual(captured[0]["log_format"], "Capture this, please")
|
||||
self.assertEqual(captured[0]["log_level"], LogLevel.debug)
|
||||
self.assertEqual(captured[0]["foo"], foo)
|
||||
self.assertEqual(captured[1]["log_format"], "Capture this too, please")
|
||||
self.assertEqual(captured[1]["log_level"], LogLevel.info)
|
||||
self.assertEqual(captured[1]["foo"], foo)
|
||||
@@ -0,0 +1,199 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Test cases for L{twisted.logger._file}.
|
||||
"""
|
||||
|
||||
from io import StringIO
|
||||
|
||||
from zope.interface.verify import verifyObject, BrokenMethodImplementation
|
||||
|
||||
from twisted.trial.unittest import TestCase
|
||||
|
||||
from twisted.python.failure import Failure
|
||||
from twisted.python.compat import unicode
|
||||
from .._observer import ILogObserver
|
||||
from .._file import FileLogObserver
|
||||
from .._file import textFileLogObserver
|
||||
|
||||
|
||||
|
||||
class FileLogObserverTests(TestCase):
|
||||
"""
|
||||
Tests for L{FileLogObserver}.
|
||||
"""
|
||||
|
||||
def test_interface(self):
|
||||
"""
|
||||
L{FileLogObserver} is an L{ILogObserver}.
|
||||
"""
|
||||
with StringIO() as fileHandle:
|
||||
observer = FileLogObserver(fileHandle, lambda e: unicode(e))
|
||||
try:
|
||||
verifyObject(ILogObserver, observer)
|
||||
except BrokenMethodImplementation as e:
|
||||
self.fail(e)
|
||||
|
||||
|
||||
def test_observeWrites(self):
|
||||
"""
|
||||
L{FileLogObserver} writes to the given file when it observes events.
|
||||
"""
|
||||
with StringIO() as fileHandle:
|
||||
observer = FileLogObserver(fileHandle, lambda e: unicode(e))
|
||||
event = dict(x=1)
|
||||
observer(event)
|
||||
self.assertEqual(fileHandle.getvalue(), unicode(event))
|
||||
|
||||
|
||||
def _test_observeWrites(self, what, count):
|
||||
"""
|
||||
Verify that observer performs an expected number of writes when the
|
||||
formatter returns a given value.
|
||||
|
||||
@param what: the value for the formatter to return.
|
||||
@type what: L{unicode}
|
||||
|
||||
@param count: the expected number of writes.
|
||||
@type count: L{int}
|
||||
"""
|
||||
with DummyFile() as fileHandle:
|
||||
observer = FileLogObserver(fileHandle, lambda e: what)
|
||||
event = dict(x=1)
|
||||
observer(event)
|
||||
self.assertEqual(fileHandle.writes, count)
|
||||
|
||||
|
||||
def test_observeWritesNone(self):
|
||||
"""
|
||||
L{FileLogObserver} does not write to the given file when it observes
|
||||
events and C{formatEvent} returns L{None}.
|
||||
"""
|
||||
self._test_observeWrites(None, 0)
|
||||
|
||||
|
||||
def test_observeWritesEmpty(self):
|
||||
"""
|
||||
L{FileLogObserver} does not write to the given file when it observes
|
||||
events and C{formatEvent} returns C{u""}.
|
||||
"""
|
||||
self._test_observeWrites(u"", 0)
|
||||
|
||||
|
||||
def test_observeFlushes(self):
|
||||
"""
|
||||
L{FileLogObserver} calles C{flush()} on the output file when it
|
||||
observes an event.
|
||||
"""
|
||||
with DummyFile() as fileHandle:
|
||||
observer = FileLogObserver(fileHandle, lambda e: unicode(e))
|
||||
event = dict(x=1)
|
||||
observer(event)
|
||||
self.assertEqual(fileHandle.flushes, 1)
|
||||
|
||||
|
||||
class TextFileLogObserverTests(TestCase):
|
||||
"""
|
||||
Tests for L{textFileLogObserver}.
|
||||
"""
|
||||
|
||||
def test_returnsFileLogObserver(self):
|
||||
"""
|
||||
L{textFileLogObserver} returns a L{FileLogObserver}.
|
||||
"""
|
||||
with StringIO() as fileHandle:
|
||||
observer = textFileLogObserver(fileHandle)
|
||||
self.assertIsInstance(observer, FileLogObserver)
|
||||
|
||||
|
||||
def test_outFile(self):
|
||||
"""
|
||||
Returned L{FileLogObserver} has the correct outFile.
|
||||
"""
|
||||
with StringIO() as fileHandle:
|
||||
observer = textFileLogObserver(fileHandle)
|
||||
self.assertIs(observer._outFile, fileHandle)
|
||||
|
||||
|
||||
def test_timeFormat(self):
|
||||
"""
|
||||
Returned L{FileLogObserver} has the correct outFile.
|
||||
"""
|
||||
with StringIO() as fileHandle:
|
||||
observer = textFileLogObserver(fileHandle, timeFormat=u"%f")
|
||||
observer(dict(log_format=u"XYZZY", log_time=112345.6))
|
||||
self.assertEqual(fileHandle.getvalue(), u"600000 [-#-] XYZZY\n")
|
||||
|
||||
|
||||
def test_observeFailure(self):
|
||||
"""
|
||||
If the C{"log_failure"} key exists in an event, the observer appends
|
||||
the failure's traceback to the output.
|
||||
"""
|
||||
with StringIO() as fileHandle:
|
||||
observer = textFileLogObserver(fileHandle)
|
||||
|
||||
try:
|
||||
1 / 0
|
||||
except ZeroDivisionError:
|
||||
failure = Failure()
|
||||
|
||||
event = dict(log_failure=failure)
|
||||
observer(event)
|
||||
output = fileHandle.getvalue()
|
||||
self.assertTrue(output.split("\n")[1].startswith("\tTraceback "),
|
||||
msg=repr(output))
|
||||
|
||||
|
||||
def test_observeFailureThatRaisesInGetTraceback(self):
|
||||
"""
|
||||
If the C{"log_failure"} key exists in an event, and contains an object
|
||||
that raises when you call its C{getTraceback()}, then the observer
|
||||
appends a message noting the problem, instead of raising.
|
||||
"""
|
||||
with StringIO() as fileHandle:
|
||||
observer = textFileLogObserver(fileHandle)
|
||||
event = dict(log_failure=object()) # object has no getTraceback()
|
||||
observer(event)
|
||||
output = fileHandle.getvalue()
|
||||
expected = (
|
||||
"(UNABLE TO OBTAIN TRACEBACK FROM EVENT)"
|
||||
)
|
||||
self.assertIn(expected, output)
|
||||
|
||||
|
||||
|
||||
class DummyFile(object):
|
||||
"""
|
||||
File that counts writes and flushes.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.writes = 0
|
||||
self.flushes = 0
|
||||
|
||||
|
||||
def write(self, data):
|
||||
"""
|
||||
Write data.
|
||||
|
||||
@param data: data
|
||||
@type data: L{unicode} or L{bytes}
|
||||
"""
|
||||
self.writes += 1
|
||||
|
||||
|
||||
def flush(self):
|
||||
"""
|
||||
Flush buffers.
|
||||
"""
|
||||
self.flushes += 1
|
||||
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
pass
|
||||
@@ -0,0 +1,372 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Test cases for L{twisted.logger._global}.
|
||||
"""
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import io
|
||||
|
||||
from twisted.trial import unittest
|
||||
|
||||
from .._file import textFileLogObserver
|
||||
from .._observer import LogPublisher
|
||||
from .._logger import Logger
|
||||
from .._global import LogBeginner
|
||||
from .._global import MORE_THAN_ONCE_WARNING
|
||||
from .._levels import LogLevel
|
||||
from ..test.test_stdlib import nextLine
|
||||
from twisted.python.failure import Failure
|
||||
|
||||
|
||||
|
||||
def compareEvents(test, actualEvents, expectedEvents):
|
||||
"""
|
||||
Compare two sequences of log events, examining only the the keys which are
|
||||
present in both.
|
||||
|
||||
@param test: a test case doing the comparison
|
||||
@type test: L{unittest.TestCase}
|
||||
|
||||
@param actualEvents: A list of log events that were emitted by a logger.
|
||||
@type actualEvents: L{list} of L{dict}
|
||||
|
||||
@param expectedEvents: A list of log events that were expected by a test.
|
||||
@type expected: L{list} of L{dict}
|
||||
"""
|
||||
if len(actualEvents) != len(expectedEvents):
|
||||
test.assertEqual(actualEvents, expectedEvents)
|
||||
allMergedKeys = set()
|
||||
|
||||
for event in expectedEvents:
|
||||
allMergedKeys |= set(event.keys())
|
||||
|
||||
def simplify(event):
|
||||
copy = event.copy()
|
||||
for key in event.keys():
|
||||
if key not in allMergedKeys:
|
||||
copy.pop(key)
|
||||
return copy
|
||||
|
||||
simplifiedActual = [simplify(event) for event in actualEvents]
|
||||
test.assertEqual(simplifiedActual, expectedEvents)
|
||||
|
||||
|
||||
|
||||
class LogBeginnerTests(unittest.TestCase):
|
||||
"""
|
||||
Tests for L{LogBeginner}.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.publisher = LogPublisher()
|
||||
self.errorStream = io.StringIO()
|
||||
|
||||
class NotSys(object):
|
||||
stdout = object()
|
||||
stderr = object()
|
||||
|
||||
class NotWarnings(object):
|
||||
def __init__(self):
|
||||
self.warnings = []
|
||||
|
||||
def showwarning(
|
||||
self, message, category, filename, lineno,
|
||||
file=None, line=None
|
||||
):
|
||||
"""
|
||||
Emulate warnings.showwarning.
|
||||
|
||||
@param message: A warning message to emit.
|
||||
@type message: L{str}
|
||||
|
||||
@param category: A warning category to associate with
|
||||
C{message}.
|
||||
@type category: L{warnings.Warning}
|
||||
|
||||
@param filename: A file name for the source code file issuing
|
||||
the warning.
|
||||
@type warning: L{str}
|
||||
|
||||
@param lineno: A line number in the source file where the
|
||||
warning was issued.
|
||||
@type lineno: L{int}
|
||||
|
||||
@param file: A file to write the warning message to. If
|
||||
L{None}, write to L{sys.stderr}.
|
||||
@type file: file-like object
|
||||
|
||||
@param line: A line of source code to include with the warning
|
||||
message. If L{None}, attempt to read the line from
|
||||
C{filename} and C{lineno}.
|
||||
@type line: L{str}
|
||||
"""
|
||||
self.warnings.append(
|
||||
(message, category, filename, lineno, file, line)
|
||||
)
|
||||
|
||||
self.sysModule = NotSys()
|
||||
self.warningsModule = NotWarnings()
|
||||
self.beginner = LogBeginner(
|
||||
self.publisher, self.errorStream, self.sysModule,
|
||||
self.warningsModule
|
||||
)
|
||||
|
||||
|
||||
def test_beginLoggingToAddObservers(self):
|
||||
"""
|
||||
Test that C{beginLoggingTo()} adds observers.
|
||||
"""
|
||||
event = dict(foo=1, bar=2)
|
||||
|
||||
events1 = []
|
||||
events2 = []
|
||||
|
||||
o1 = lambda e: events1.append(e)
|
||||
o2 = lambda e: events2.append(e)
|
||||
|
||||
self.beginner.beginLoggingTo((o1, o2))
|
||||
self.publisher(event)
|
||||
|
||||
self.assertEqual([event], events1)
|
||||
self.assertEqual([event], events2)
|
||||
|
||||
|
||||
def test_beginLoggingToBufferedEvents(self):
|
||||
"""
|
||||
Test that events are buffered until C{beginLoggingTo()} is
|
||||
called.
|
||||
"""
|
||||
event = dict(foo=1, bar=2)
|
||||
|
||||
events1 = []
|
||||
events2 = []
|
||||
|
||||
o1 = lambda e: events1.append(e)
|
||||
o2 = lambda e: events2.append(e)
|
||||
|
||||
self.publisher(event) # Before beginLoggingTo; this is buffered
|
||||
self.beginner.beginLoggingTo((o1, o2))
|
||||
|
||||
self.assertEqual([event], events1)
|
||||
self.assertEqual([event], events2)
|
||||
|
||||
|
||||
def _bufferLimitTest(self, limit, beginner):
|
||||
"""
|
||||
Verify that when more than C{limit} events are logged to L{LogBeginner},
|
||||
only the last C{limit} are replayed by L{LogBeginner.beginLoggingTo}.
|
||||
|
||||
@param limit: The maximum number of events the log beginner should
|
||||
buffer.
|
||||
@type limit: L{int}
|
||||
|
||||
@param beginner: The L{LogBeginner} against which to verify.
|
||||
@type beginner: L{LogBeginner}
|
||||
|
||||
@raise: C{self.failureException} if the wrong events are replayed by
|
||||
C{beginner}.
|
||||
|
||||
@return: L{None}
|
||||
"""
|
||||
for count in range(limit + 1):
|
||||
self.publisher(dict(count=count))
|
||||
events = []
|
||||
beginner.beginLoggingTo([events.append])
|
||||
self.assertEqual(
|
||||
list(range(1, limit + 1)),
|
||||
list(event["count"] for event in events),
|
||||
)
|
||||
|
||||
|
||||
def test_defaultBufferLimit(self):
|
||||
"""
|
||||
Up to C{LogBeginner._DEFAULT_BUFFER_SIZE} log events are buffered for
|
||||
replay by L{LogBeginner.beginLoggingTo}.
|
||||
"""
|
||||
limit = LogBeginner._DEFAULT_BUFFER_SIZE
|
||||
self._bufferLimitTest(limit, self.beginner)
|
||||
|
||||
|
||||
def test_overrideBufferLimit(self):
|
||||
"""
|
||||
The size of the L{LogBeginner} event buffer can be overridden with the
|
||||
C{initialBufferSize} initilizer argument.
|
||||
"""
|
||||
limit = 3
|
||||
beginner = LogBeginner(
|
||||
self.publisher, self.errorStream, self.sysModule,
|
||||
self.warningsModule, initialBufferSize=limit,
|
||||
)
|
||||
self._bufferLimitTest(limit, beginner)
|
||||
|
||||
|
||||
def test_beginLoggingToTwice(self):
|
||||
"""
|
||||
When invoked twice, L{LogBeginner.beginLoggingTo} will emit a log
|
||||
message warning the user that they previously began logging, and add
|
||||
the new log observers.
|
||||
"""
|
||||
events1 = []
|
||||
events2 = []
|
||||
fileHandle = io.StringIO()
|
||||
textObserver = textFileLogObserver(fileHandle)
|
||||
self.publisher(dict(event="prebuffer"))
|
||||
firstFilename, firstLine = nextLine()
|
||||
self.beginner.beginLoggingTo([events1.append, textObserver])
|
||||
self.publisher(dict(event="postbuffer"))
|
||||
secondFilename, secondLine = nextLine()
|
||||
self.beginner.beginLoggingTo([events2.append, textObserver])
|
||||
self.publisher(dict(event="postwarn"))
|
||||
warning = dict(
|
||||
log_format=MORE_THAN_ONCE_WARNING,
|
||||
log_level=LogLevel.warn,
|
||||
fileNow=secondFilename, lineNow=secondLine,
|
||||
fileThen=firstFilename, lineThen=firstLine
|
||||
)
|
||||
|
||||
compareEvents(
|
||||
self, events1,
|
||||
[
|
||||
dict(event="prebuffer"),
|
||||
dict(event="postbuffer"),
|
||||
warning,
|
||||
dict(event="postwarn")
|
||||
]
|
||||
)
|
||||
compareEvents(self, events2, [warning, dict(event="postwarn")])
|
||||
|
||||
output = fileHandle.getvalue()
|
||||
self.assertIn('<{0}:{1}>'.format(firstFilename, firstLine),
|
||||
output)
|
||||
self.assertIn('<{0}:{1}>'.format(secondFilename, secondLine),
|
||||
output)
|
||||
|
||||
|
||||
def test_criticalLogging(self):
|
||||
"""
|
||||
Critical messages will be written as text to the error stream.
|
||||
"""
|
||||
log = Logger(observer=self.publisher)
|
||||
log.info("ignore this")
|
||||
log.critical("a critical {message}", message="message")
|
||||
self.assertEqual(self.errorStream.getvalue(), u"a critical message\n")
|
||||
|
||||
|
||||
def test_criticalLoggingStops(self):
|
||||
"""
|
||||
Once logging has begun with C{beginLoggingTo}, critical messages are no
|
||||
longer written to the output stream.
|
||||
"""
|
||||
log = Logger(observer=self.publisher)
|
||||
self.beginner.beginLoggingTo(())
|
||||
log.critical("another critical message")
|
||||
self.assertEqual(self.errorStream.getvalue(), u"")
|
||||
|
||||
|
||||
def test_beginLoggingToRedirectStandardIO(self):
|
||||
"""
|
||||
L{LogBeginner.beginLoggingTo} will re-direct the standard output and
|
||||
error streams by setting the C{stdio} and C{stderr} attributes on its
|
||||
sys module object.
|
||||
"""
|
||||
x = []
|
||||
self.beginner.beginLoggingTo([x.append])
|
||||
print("Hello, world.", file=self.sysModule.stdout)
|
||||
compareEvents(
|
||||
self, x, [dict(log_namespace="stdout", log_io="Hello, world.")]
|
||||
)
|
||||
del x[:]
|
||||
print("Error, world.", file=self.sysModule.stderr)
|
||||
compareEvents(
|
||||
self, x, [dict(log_namespace="stderr", log_io="Error, world.")]
|
||||
)
|
||||
|
||||
|
||||
def test_beginLoggingToDontRedirect(self):
|
||||
"""
|
||||
L{LogBeginner.beginLoggingTo} will leave the existing stdout/stderr in
|
||||
place if it has been told not to replace them.
|
||||
"""
|
||||
oldOut = self.sysModule.stdout
|
||||
oldErr = self.sysModule.stderr
|
||||
self.beginner.beginLoggingTo((), redirectStandardIO=False)
|
||||
self.assertIs(self.sysModule.stdout, oldOut)
|
||||
self.assertIs(self.sysModule.stderr, oldErr)
|
||||
|
||||
|
||||
def test_beginLoggingToPreservesEncoding(self):
|
||||
"""
|
||||
When L{LogBeginner.beginLoggingTo} redirects stdout/stderr streams, the
|
||||
replacement streams will preserve the encoding of the replaced streams,
|
||||
to minimally disrupt any application relying on a specific encoding.
|
||||
"""
|
||||
|
||||
weird = io.TextIOWrapper(io.BytesIO(), "shift-JIS")
|
||||
weirderr = io.TextIOWrapper(io.BytesIO(), "big5")
|
||||
|
||||
self.sysModule.stdout = weird
|
||||
self.sysModule.stderr = weirderr
|
||||
|
||||
x = []
|
||||
self.beginner.beginLoggingTo([x.append])
|
||||
self.assertEqual(self.sysModule.stdout.encoding, "shift-JIS")
|
||||
self.assertEqual(self.sysModule.stderr.encoding, "big5")
|
||||
|
||||
self.sysModule.stdout.write(b"\x97\x9B\n")
|
||||
self.sysModule.stderr.write(b"\xBC\xFC\n")
|
||||
compareEvents(
|
||||
self, x, [dict(log_io=u"\u674e"), dict(log_io=u"\u7469")]
|
||||
)
|
||||
|
||||
|
||||
def test_warningsModule(self):
|
||||
"""
|
||||
L{LogBeginner.beginLoggingTo} will redirect the warnings of its
|
||||
warnings module into the logging system.
|
||||
"""
|
||||
self.warningsModule.showwarning(
|
||||
"a message", DeprecationWarning, __file__, 1
|
||||
)
|
||||
x = []
|
||||
self.beginner.beginLoggingTo([x.append])
|
||||
self.warningsModule.showwarning(
|
||||
"another message", DeprecationWarning, __file__, 2
|
||||
)
|
||||
f = io.StringIO()
|
||||
self.warningsModule.showwarning(
|
||||
"yet another", DeprecationWarning, __file__, 3, file=f
|
||||
)
|
||||
self.assertEqual(
|
||||
self.warningsModule.warnings,
|
||||
[
|
||||
("a message", DeprecationWarning, __file__, 1, None, None),
|
||||
("yet another", DeprecationWarning, __file__, 3, f, None),
|
||||
]
|
||||
)
|
||||
compareEvents(
|
||||
self, x,
|
||||
[dict(
|
||||
warning="another message",
|
||||
category=(
|
||||
DeprecationWarning.__module__ + "." +
|
||||
DeprecationWarning.__name__
|
||||
),
|
||||
filename=__file__, lineno=2,
|
||||
)]
|
||||
)
|
||||
|
||||
|
||||
def test_failuresAppendTracebacks(self):
|
||||
"""
|
||||
The string resulting from a logged failure contains a traceback.
|
||||
"""
|
||||
f = Failure(Exception("this is not the behavior you are looking for"))
|
||||
log = Logger(observer=self.publisher)
|
||||
log.failure('a failure', failure=f)
|
||||
msg = self.errorStream.getvalue()
|
||||
self.assertIn('a failure', msg)
|
||||
self.assertIn('this is not the behavior you are looking for', msg)
|
||||
self.assertIn('Traceback', msg)
|
||||
@@ -0,0 +1,38 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Test cases for L{twisted.logger._levels}.
|
||||
"""
|
||||
|
||||
from twisted.trial import unittest
|
||||
|
||||
from .._levels import InvalidLogLevelError
|
||||
from .._levels import LogLevel
|
||||
|
||||
|
||||
|
||||
class LogLevelTests(unittest.TestCase):
|
||||
"""
|
||||
Tests for L{LogLevel}.
|
||||
"""
|
||||
|
||||
def test_levelWithName(self):
|
||||
"""
|
||||
Look up log level by name.
|
||||
"""
|
||||
for level in LogLevel.iterconstants():
|
||||
self.assertIs(LogLevel.levelWithName(level.name), level)
|
||||
|
||||
|
||||
def test_levelWithInvalidName(self):
|
||||
"""
|
||||
You can't make up log level names.
|
||||
"""
|
||||
bogus = "*bogus*"
|
||||
try:
|
||||
LogLevel.levelWithName(bogus)
|
||||
except InvalidLogLevelError as e:
|
||||
self.assertIs(e.level, bogus)
|
||||
else:
|
||||
self.fail("Expected InvalidLogLevelError.")
|
||||
@@ -0,0 +1,304 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Test cases for L{twisted.logger._format}.
|
||||
"""
|
||||
|
||||
import sys
|
||||
from io import BytesIO, TextIOWrapper
|
||||
import logging as py_logging
|
||||
from inspect import getsourcefile
|
||||
|
||||
from zope.interface.verify import verifyObject, BrokenMethodImplementation
|
||||
|
||||
from twisted.python.compat import _PY3, currentframe
|
||||
from twisted.python.failure import Failure
|
||||
from twisted.trial import unittest
|
||||
|
||||
from .._levels import LogLevel
|
||||
from .._observer import ILogObserver
|
||||
from .._stdlib import STDLibLogObserver
|
||||
|
||||
|
||||
def nextLine():
|
||||
"""
|
||||
Retrive the file name and line number immediately after where this function
|
||||
is called.
|
||||
|
||||
@return: the file name and line number
|
||||
@rtype: 2-L{tuple} of L{str}, L{int}
|
||||
"""
|
||||
caller = currentframe(1)
|
||||
return (getsourcefile(sys.modules[caller.f_globals['__name__']]),
|
||||
caller.f_lineno + 1)
|
||||
|
||||
|
||||
|
||||
class STDLibLogObserverTests(unittest.TestCase):
|
||||
"""
|
||||
Tests for L{STDLibLogObserver}.
|
||||
"""
|
||||
|
||||
def test_interface(self):
|
||||
"""
|
||||
L{STDLibLogObserver} is an L{ILogObserver}.
|
||||
"""
|
||||
observer = STDLibLogObserver()
|
||||
try:
|
||||
verifyObject(ILogObserver, observer)
|
||||
except BrokenMethodImplementation as e:
|
||||
self.fail(e)
|
||||
|
||||
|
||||
def py_logger(self):
|
||||
"""
|
||||
Create a logging object we can use to test with.
|
||||
|
||||
@return: a stdlib-style logger
|
||||
@rtype: L{StdlibLoggingContainer}
|
||||
"""
|
||||
logger = StdlibLoggingContainer()
|
||||
self.addCleanup(logger.close)
|
||||
return logger
|
||||
|
||||
|
||||
def logEvent(self, *events):
|
||||
"""
|
||||
Send one or more events to Python's logging module, and
|
||||
capture the emitted L{logging.LogRecord}s and output stream as
|
||||
a string.
|
||||
|
||||
@param events: events
|
||||
@type events: L{tuple} of L{dict}
|
||||
|
||||
@return: a tuple: (records, output)
|
||||
@rtype: 2-tuple of (L{list} of L{logging.LogRecord}, L{bytes}.)
|
||||
"""
|
||||
pl = self.py_logger()
|
||||
observer = STDLibLogObserver(
|
||||
# Add 1 to default stack depth to skip *this* frame, since
|
||||
# tests will want to know about their own frames.
|
||||
stackDepth=STDLibLogObserver.defaultStackDepth + 1
|
||||
)
|
||||
for event in events:
|
||||
observer(event)
|
||||
return pl.bufferedHandler.records, pl.outputAsText()
|
||||
|
||||
|
||||
def test_name(self):
|
||||
"""
|
||||
Logger name.
|
||||
"""
|
||||
records, output = self.logEvent({})
|
||||
|
||||
self.assertEqual(len(records), 1)
|
||||
self.assertEqual(records[0].name, "twisted")
|
||||
|
||||
|
||||
def test_levels(self):
|
||||
"""
|
||||
Log levels.
|
||||
"""
|
||||
levelMapping = {
|
||||
None: py_logging.INFO, # Default
|
||||
LogLevel.debug: py_logging.DEBUG,
|
||||
LogLevel.info: py_logging.INFO,
|
||||
LogLevel.warn: py_logging.WARNING,
|
||||
LogLevel.error: py_logging.ERROR,
|
||||
LogLevel.critical: py_logging.CRITICAL,
|
||||
}
|
||||
|
||||
# Build a set of events for each log level
|
||||
events = []
|
||||
for level, pyLevel in levelMapping.items():
|
||||
event = {}
|
||||
|
||||
# Set the log level on the event, except for default
|
||||
if level is not None:
|
||||
event["log_level"] = level
|
||||
|
||||
# Remember the Python log level we expect to see for this
|
||||
# event (as an int)
|
||||
event["py_levelno"] = int(pyLevel)
|
||||
|
||||
events.append(event)
|
||||
|
||||
records, output = self.logEvent(*events)
|
||||
self.assertEqual(len(records), len(levelMapping))
|
||||
|
||||
# Check that each event has the correct level
|
||||
for i in range(len(records)):
|
||||
self.assertEqual(records[i].levelno, events[i]["py_levelno"])
|
||||
|
||||
|
||||
def test_callerInfo(self):
|
||||
"""
|
||||
C{pathname}, C{lineno}, C{exc_info}, C{func} is set properly on
|
||||
records.
|
||||
"""
|
||||
filename, logLine = nextLine()
|
||||
records, output = self.logEvent({})
|
||||
|
||||
self.assertEqual(len(records), 1)
|
||||
self.assertEqual(records[0].pathname, filename)
|
||||
self.assertEqual(records[0].lineno, logLine)
|
||||
self.assertIsNone(records[0].exc_info)
|
||||
|
||||
# Attribute "func" is missing from record, which is weird because it's
|
||||
# documented.
|
||||
# self.assertEqual(records[0].func, "test_callerInfo")
|
||||
|
||||
|
||||
def test_basicFormat(self):
|
||||
"""
|
||||
Basic formattable event passes the format along correctly.
|
||||
"""
|
||||
event = dict(log_format="Hello, {who}!", who="dude")
|
||||
records, output = self.logEvent(event)
|
||||
|
||||
self.assertEqual(len(records), 1)
|
||||
self.assertEqual(str(records[0].msg), u"Hello, dude!")
|
||||
self.assertEqual(records[0].args, ())
|
||||
|
||||
|
||||
def test_basicFormatRendered(self):
|
||||
"""
|
||||
Basic formattable event renders correctly.
|
||||
"""
|
||||
event = dict(log_format="Hello, {who}!", who="dude")
|
||||
records, output = self.logEvent(event)
|
||||
|
||||
self.assertEqual(len(records), 1)
|
||||
self.assertTrue(output.endswith(u":Hello, dude!\n"),
|
||||
repr(output))
|
||||
|
||||
|
||||
def test_noFormat(self):
|
||||
"""
|
||||
Event with no format.
|
||||
"""
|
||||
records, output = self.logEvent({})
|
||||
|
||||
self.assertEqual(len(records), 1)
|
||||
self.assertEqual(str(records[0].msg), "")
|
||||
|
||||
|
||||
def test_failure(self):
|
||||
"""
|
||||
An event with a failure logs the failure details as well.
|
||||
"""
|
||||
def failing_func():
|
||||
1 / 0
|
||||
try:
|
||||
failing_func()
|
||||
except ZeroDivisionError:
|
||||
failure = Failure()
|
||||
event = dict(log_format='Hi mom', who='me', log_failure=failure)
|
||||
records, output = self.logEvent(event)
|
||||
self.assertEqual(len(records), 1)
|
||||
self.assertIn(u'Hi mom', output)
|
||||
self.assertIn(u'in failing_func', output)
|
||||
self.assertIn(u'ZeroDivisionError', output)
|
||||
|
||||
|
||||
def test_cleanedFailure(self):
|
||||
"""
|
||||
A cleaned Failure object has a fake traceback object; make sure that
|
||||
logging such a failure still results in the exception details being
|
||||
logged.
|
||||
"""
|
||||
def failing_func():
|
||||
1 / 0
|
||||
try:
|
||||
failing_func()
|
||||
except ZeroDivisionError:
|
||||
failure = Failure()
|
||||
failure.cleanFailure()
|
||||
event = dict(log_format='Hi mom', who='me', log_failure=failure)
|
||||
records, output = self.logEvent(event)
|
||||
self.assertEqual(len(records), 1)
|
||||
self.assertIn(u'Hi mom', output)
|
||||
self.assertIn(u'in failing_func', output)
|
||||
self.assertIn(u'ZeroDivisionError', output)
|
||||
|
||||
|
||||
|
||||
class StdlibLoggingContainer(object):
|
||||
"""
|
||||
Continer for a test configuration of stdlib logging objects.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.rootLogger = py_logging.getLogger("")
|
||||
|
||||
self.originalLevel = self.rootLogger.getEffectiveLevel()
|
||||
self.rootLogger.setLevel(py_logging.DEBUG)
|
||||
|
||||
self.bufferedHandler = BufferedHandler()
|
||||
self.rootLogger.addHandler(self.bufferedHandler)
|
||||
|
||||
self.streamHandler, self.output = handlerAndBytesIO()
|
||||
self.rootLogger.addHandler(self.streamHandler)
|
||||
|
||||
|
||||
def close(self):
|
||||
"""
|
||||
Close the logger.
|
||||
"""
|
||||
self.rootLogger.setLevel(self.originalLevel)
|
||||
self.rootLogger.removeHandler(self.bufferedHandler)
|
||||
self.rootLogger.removeHandler(self.streamHandler)
|
||||
self.streamHandler.close()
|
||||
self.output.close()
|
||||
|
||||
|
||||
def outputAsText(self):
|
||||
"""
|
||||
Get the output to the underlying stream as text.
|
||||
|
||||
@return: the output text
|
||||
@rtype: L{unicode}
|
||||
"""
|
||||
return self.output.getvalue().decode("utf-8")
|
||||
|
||||
|
||||
|
||||
def handlerAndBytesIO():
|
||||
"""
|
||||
Construct a 2-tuple of C{(StreamHandler, BytesIO)} for testing interaction
|
||||
with the 'logging' module.
|
||||
|
||||
@return: handler and io object
|
||||
@rtype: tuple of L{StreamHandler} and L{io.BytesIO}
|
||||
"""
|
||||
output = BytesIO()
|
||||
stream = output
|
||||
template = py_logging.BASIC_FORMAT
|
||||
if _PY3:
|
||||
stream = TextIOWrapper(output, encoding="utf-8", newline="\n")
|
||||
formatter = py_logging.Formatter(template)
|
||||
handler = py_logging.StreamHandler(stream)
|
||||
handler.setFormatter(formatter)
|
||||
return handler, output
|
||||
|
||||
|
||||
|
||||
class BufferedHandler(py_logging.Handler):
|
||||
"""
|
||||
A L{py_logging.Handler} that remembers all logged records in a list.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""
|
||||
Initialize this L{BufferedHandler}.
|
||||
"""
|
||||
py_logging.Handler.__init__(self)
|
||||
self.records = []
|
||||
|
||||
|
||||
def emit(self, record):
|
||||
"""
|
||||
Remember the record.
|
||||
"""
|
||||
self.records.append(record)
|
||||
Reference in New Issue
Block a user