Skip to content

Commit

Permalink
Merge pull request #268 from danielrichman/logs
Browse files Browse the repository at this point in the history
Email errors and exceptions to us. Closes #197
  • Loading branch information
adamgreig committed Sep 13, 2012
2 parents 06761e6 + 0df11d6 commit 37327fa
Show file tree
Hide file tree
Showing 4 changed files with 315 additions and 23 deletions.
12 changes: 10 additions & 2 deletions habitat.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
couch_uri: "http://localhost:5984"
couch_db: habitat
log_stderr_level: DEBUG
log_file_level: NONE
log_levels:
stderr: DEBUG
file: NONE
email: ERROR
log_emails:
to:
- "[email protected]"
- "[email protected]"
from: "[email protected]"
server: localhost
parserdaemon:
log_file:
parser:
Expand Down
271 changes: 265 additions & 6 deletions habitat/tests/test_utils/test_startup.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright 2011 (C) Daniel Richman
# Copyright 2012 (C) Daniel Richman
#
# This file is part of habitat.
#
Expand All @@ -17,9 +17,268 @@

"""
Tests for habitat.utils.startup
There are no tests here, since it's probably not useful to have them.
Tests for the startup functions would largely just be the startup functions
re-written with Mox, as they just call other modules rather than do
anything really useful themselves.
"""

import sys
import tempfile
import mox
import smtplib
import copy
import logging
import os
import os.path

from ...utils import startup

_example_yaml = \
"""d: 4
blah: moo
o:
listy:
- 2
- cow"""

_example_parsed = {"d": 4, "blah": "moo", "o": {"listy": [2, "cow"]}}

class TestLoadConfig(object):
def setup(self):
self.config = tempfile.NamedTemporaryFile()
self.old_argv = sys.argv
sys.argv = ["bin/something", self.config.name]

def teardown(self):
sys.argv = self.old_argv
self.config.close()

def test_works(self):
self.config.write(_example_yaml)
self.config.flush()
assert startup.load_config() == _example_parsed

_logging_config = {
"log_levels": {},
"log_emails": {"to": ["addr_1", "addr_2"], "from": "from_bob",
"server": "email_server"},
"mydaemon": {"log_file": "somewhere"}
}

class EqIfIn(mox.Comparator):
"""
A mox comparator that is 'equal' to something if each of the provided
arguments to the constructor is 'in' (as in python operator in) the
right hand side of the equality
"""
def __init__(self, *objs):
self._objs = objs
def equals(self, rhs):
for obj in self._objs:
if obj not in rhs:
return False
return True
def __repr__(self):
return "EqIfIn({0})".format(self._objs)

class TestSetupLogging(object):
# Rationale for weird tests:
# Mocking out the handlers and loggers would be a bit silly, since it
# would just be rewriting startup.py line for line with mox. So these,
# while a little dodgy in places, actually serve to test something.

def setup(self):
self.mocker = mox.Mox()
self.config = copy.deepcopy(_logging_config)

self.old_handlers = logging.root.handlers
# nose creates its own handler
logging.root.handlers = []

# manual cleanup needed for check_file's tests
self.temp_dir = None
self.temp_files = []

def teardown(self):
self.mocker.UnsetStubs()

# Reset logging
for h in logging.root.handlers:
h.close()
logging.root.handlers = self.old_handlers

if self.temp_files:
for f in self.temp_files:
try:
os.unlink(f)
except:
pass

if self.temp_dir:
os.rmdir(self.temp_dir)

def generate_log_messages(self):
l = logging.getLogger("test_source")
l.debug("Debug message TEST1")
l.info("Info message TEST2")
l.warning("Warning message TEST3")
l.error("Error message TEST4")
l.critical("Error message TEST5")
def function_TEST6():
raise ValueError("TEST7")
try:
function_TEST6()
except:
l.exception("Exception TEST8")

def expected_messages(self, level, initialised=True):
"""
returns, for each expected message, a tuple of strings that should
be found in that message
"""
if level <= logging.INFO and initialised:
yield "Log initialised",
if level <= logging.DEBUG:
yield "TEST1",
if level <= logging.INFO:
yield "TEST2",
if level <= logging.WARNING:
yield "TEST3",
if level <= logging.ERROR:
yield "TEST4",
if level <= logging.CRITICAL:
yield "TEST5",
if level <= logging.ERROR:
# message must contain function name, exception arg, log msg
yield "TEST6", "TEST7", "TEST8"

levels = \
[logging.CRITICAL, logging.ERROR, logging.WARNING,
logging.INFO, logging.DEBUG]
level_names = ["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"]

checks = zip(levels, level_names)

def test_stderr(self):
for a, b in self.checks:
yield self.check_stderr, a, b

def test_file(self):
for a, b in self.checks:
yield self.check_file, a, b

def test_email(self):
for a, b in self.checks:
yield self.check_email, a, b

def check_stderr(self, level, level_name):
self.config["log_levels"]["stderr"] = level_name

self.mocker.StubOutWithMock(sys, 'stdout')
self.mocker.StubOutWithMock(sys, 'stderr')

for things in self.expected_messages(level):
sys.stderr.write(EqIfIn(*things))
sys.stderr.flush()

self.mocker.ReplayAll()

startup.setup_logging(self.config, "mydaemon")
self.generate_log_messages()

self.mocker.VerifyAll()

def check_file(self, level, level_name):
# Mocks not used.
self.mocker.ReplayAll()

self.temp_dir = tempfile.mkdtemp()
filename_a = os.path.join(self.temp_dir, "log_a")
filename_b = os.path.join(self.temp_dir, "log_b")
self.temp_files = [filename_a, filename_b]

self.config["log_levels"]["file"] = level_name
self.config["mydaemon"]["log_file"] = filename_a

startup.setup_logging(self.config, "mydaemon")
self.generate_log_messages()
logging.getLogger("tests").critical("This should be in file b")

# WatchedFileHandler, so should be able to move log file and it will
# automatically write to new file at old name.
os.rename(filename_a, filename_b)
self.generate_log_messages()
logging.getLogger("tests").critical("This should be in file a")

# N.B.: It should flush after each message, so no need to close/flush
with open(filename_a) as f:
data_a = f.read()
with open(filename_b) as f:
data_b = f.read()

for things in self.expected_messages(level, initialised=False):
for match in things:
assert match in data_a
for things in self.expected_messages(level):
for match in things:
assert match in data_b

assert "should be in file a" in data_a
assert "should be in file b" in data_b

self.mocker.VerifyAll()

def check_email(self, level, level_name):
self.config["log_levels"]["email"] = level_name

# SMTPHandler uses smtplib
self.mocker.StubOutClassWithMocks(smtplib, 'SMTP')

for things in self.expected_messages(level):
smtp = smtplib.SMTP("email_server", 25)
smtp.sendmail("from_bob", ["addr_1", "addr_2"], EqIfIn(*things))
smtp.quit()

self.mocker.ReplayAll()

startup.setup_logging(self.config, "mydaemon")
self.generate_log_messages()

self.mocker.VerifyAll()

def test_silent(self):
self.mocker.StubOutWithMock(sys, 'stdout')
self.mocker.StubOutWithMock(sys, 'stderr')
# No output on std{err,out}

self.mocker.ReplayAll()

startup.setup_logging(self.config, "mydaemon")
self.generate_log_messages()

self.mocker.VerifyAll()

class TestMain(object):
def setup(self):
self.mocker = mox.Mox()

def teardown(self):
self.mocker.UnsetStubs()

def test_works(self):
self.mocker.StubOutWithMock(startup, 'load_config')
self.mocker.StubOutWithMock(startup, 'setup_logging')

main_class = self.mocker.CreateMockAnything()
main_class.__name__ = "ExampleDaemon"

main_object = self.mocker.CreateMockAnything()

startup.load_config().AndReturn({"the_config": True})
startup.setup_logging({"the_config": True}, "exampledaemon")
main_class({"the_config": True}, "exampledaemon")\
.AndReturn(main_object)
main_object.run()

self.mocker.ReplayAll()

startup.main(main_class)

self.mocker.VerifyAll()
1 change: 0 additions & 1 deletion habitat/tests/test_views/test_flight.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,5 +176,4 @@ def test_view_end_start_without_payloads(self):
def test_view_name(self):
mydoc = deepcopy(doc)
result = list(flight.all_name_map(mydoc))
print result
assert result == [("Test Launch", None)]
Loading

0 comments on commit 37327fa

Please sign in to comment.