372 lines
12 KiB
Python
372 lines
12 KiB
Python
# 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)
|