Commit 0968f9ca authored by Ilias Tsitsimpis's avatar Ilias Tsitsimpis
Browse files

burnin: Add new logger

parent 61d56f74
# Copyright 2013 GRNET S.A. All rights reserved.
#
# Redistribution and use in source and binary forms, with or
# without modification, are permitted provided that the following
# conditions are met:
#
# 1. Redistributions of source code must retain the above
# copyright notice, this list of conditions and the following
# disclaimer.
#
# 2. Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials
# provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# The views and conclusions contained in the software and
# documentation are those of the authors and should not be
# interpreted as representing official policies, either expressed
# or implied, of GRNET S.A.
"""
This is the burnin class that tests the Astakos functionality
"""
from kamaki.clients.compute import ComputeClient
from kamaki.clients import ClientError
import common
# Too many public methods (47/20). pylint: disable-msg=R0904
class AstakosTestCase(common.BurninTests):
"""Test Astakos functionality"""
def test_unauthorized_access(self):
"""Test access without a valid token fails"""
false_token = "12345"
client = ComputeClient(self.compute_url, false_token)
client.CONNECTION_RETRY_LIMIT = self.connection_retry_limit
with self.assertRaises(ClientError) as cl_error:
client.list_servers()
self.assertEqual(cl_error.exception.status, 401)
class AstakosFoo(common.BurninTests):
"""Just Fail"""
def test_just_foo(self):
"""A test that just fails"""
self.fail("just fail")
# Copyright 2013 GRNET S.A. All rights reserved.
#
# Redistribution and use in source and binary forms, with or
# without modification, are permitted provided that the following
# conditions are met:
#
# 1. Redistributions of source code must retain the above
# copyright notice, this list of conditions and the following
# disclaimer.
#
# 2. Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials
# provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# The views and conclusions contained in the software and
# documentation are those of the authors and should not be
# interpreted as representing official policies, either expressed
# or implied, of GRNET S.A.
"""
Common utils for burnin tests
"""
import sys
import traceback
# Use backported unittest functionality if Python < 2.7
try:
import unittest2 as unittest
except ImportError:
if sys.version_info < (2, 7):
raise Exception("The unittest2 package is required for Python < 2.7")
import unittest
from kamaki.clients.astakos import AstakosClient
from kamaki.clients.compute import ComputeClient
from logger import Log
# --------------------------------------------------------------------
# Global variables
logger = None # Invalid constant name. pylint: disable-msg=C0103
AUTH_URL = "https://accounts.okeanos.grnet.gr/identity/v2.0/"
TOKEN = ""
CONNECTION_RETRY_LIMIT = 2
# --------------------------------------------------------------------
# BurninTestResult class
class BurninTestResult(unittest.TestResult):
"""Modify the TextTestResult class"""
def __init__(self):
super(BurninTestResult, self).__init__()
# Test parameters
self.failfast = True
def startTest(self, test): # noqa
"""Called when the test case test is about to be run"""
super(BurninTestResult, self).startTest(test)
# Access to a protected member. pylint: disable-msg=W0212
logger.log(test.__class__.__name__, test._testMethodDoc)
# Method could be a function. pylint: disable-msg=R0201
def _test_failed(self, test, err):
"""Test failed"""
# Access to a protected member. pylint: disable-msg=W0212
err_msg = test._testMethodDoc + "... failed."
logger.error(test.__class__.__name__, err_msg)
(err_type, err_value, err_trace) = err
trcback = traceback.format_exception(err_type, err_value, err_trace)
logger.info(test.__class__.__name__, trcback)
def addError(self, test, err): # noqa
"""Called when the test case test raises an unexpected exception"""
super(BurninTestResult, self).addError(test, err)
self._test_failed(test, err)
def addFailure(self, test, err): # noqa
"""Called when the test case test signals a failure"""
super(BurninTestResult, self).addFailure(test, err)
self._test_failed(test, err)
# --------------------------------------------------------------------
# BurninTests class
# Too many public methods (45/20). pylint: disable-msg=R0904
class BurninTests(unittest.TestCase):
"""Common class that all burnin tests should implement"""
@classmethod
def setUpClass(cls): # noqa
"""Initialize BurninTests"""
cls.suite_name = cls.__name__
cls.connection_retry_limit = CONNECTION_RETRY_LIMIT
logger.testsuite_start(cls.suite_name)
# Set test parameters
cls.longMessage = True
def test_clients_setup(self):
"""Initializing astakos/cyclades/pithos clients"""
# Update class attributes
cls = type(self)
self.info("Astakos auth url is %s", AUTH_URL)
cls.astakos = AstakosClient(AUTH_URL, TOKEN)
cls.astakos.CONNECTION_RETRY_LIMIT = CONNECTION_RETRY_LIMIT
cls.compute_url = \
cls.astakos.get_service_endpoints('compute')['publicURL']
self.info("Cyclades url is %s", cls.compute_url)
cls.compute = ComputeClient(cls.compute_url, TOKEN)
cls.compute.CONNECTION_RETRY_LIMIT = CONNECTION_RETRY_LIMIT
def log(self, msg, *args):
"""Pass the section value to logger"""
logger.log(self.suite_name, msg, *args)
def info(self, msg, *args):
"""Pass the section value to logger"""
logger.info(self.suite_name, msg, *args)
def debug(self, msg, *args):
"""Pass the section value to logger"""
logger.debug(self.suite_name, msg, *args)
def warning(self, msg, *args):
"""Pass the section value to logger"""
logger.warning(self.suite_name, msg, *args)
def error(self, msg, *args):
"""Pass the section value to logger"""
logger.error(self.suite_name, msg, *args)
# --------------------------------------------------------------------
# Helper functions
def was_succesful(tsuite, success):
"""Handle whether a testsuite was succesful or not"""
if success:
logger.testsuite_success(tsuite)
else:
logger.testsuite_failure(tsuite)
def setup_logger(output_dir, verbose=1, use_colors=True, in_parallel=False):
"""Setup our logger"""
global logger # Using global statement. pylint: disable-msg=C0103,W0603
logger = Log(output_dir, verbose=verbose,
use_colors=use_colors, in_parallel=in_parallel)
# filelocker.py - Cross-platform (posix/nt) API for flock-style file locking.
# Requires python 1.5.2 or better.
"""Cross-platform (posix/nt) API for flock-style file locking.
Synopsis:
import filelocker
with filelocker.lock("lockfile", filelocker.LOCK_EX):
print "Got it"
Methods:
lock ( file, flags, tries=10 )
Constants:
LOCK_EX
LOCK_SH
LOCK_NB
Exceptions:
LockException
Notes:
For the 'nt' platform, this module requires the Python Extensions for Windows.
Be aware that this may not work as expected on Windows 95/98/ME.
History:
I learned the win32 technique for locking files from sample code
provided by John Nielsen <nielsenjf@my-deja.com> in the documentation
that accompanies the win32 modules.
Author: Jonathan Feinberg <jdf@pobox.com>,
Lowell Alleman <lalleman@mfps.com>
Version: $Id: filelocker.py 5474 2008-05-16 20:53:50Z lowell $
Modified to work as a contextmanager
"""
import os
from contextlib import contextmanager
class LockException(Exception):
# Error codes:
LOCK_FAILED = 1
# Import modules for each supported platform
if os.name == 'nt':
import win32con
import win32file
import pywintypes
LOCK_EX = win32con.LOCKFILE_EXCLUSIVE_LOCK
LOCK_SH = 0 # the default
LOCK_NB = win32con.LOCKFILE_FAIL_IMMEDIATELY
# is there any reason not to reuse the following structure?
__overlapped = pywintypes.OVERLAPPED()
elif os.name == 'posix':
import fcntl
LOCK_EX = fcntl.LOCK_EX
LOCK_SH = fcntl.LOCK_SH
LOCK_NB = fcntl.LOCK_NB
else:
raise RuntimeError("FileLocker only defined for nt and posix platforms")
# --------------------------------------
# Implementation for NT
if os.name == 'nt':
@contextmanager
def lock(filename, flags):
file = open(filename, "w+")
hfile = win32file._get_osfhandle(file.fileno())
try:
win32file.LockFileEx(hfile, flags, 0, -0x10000, __overlapped)
try:
yield
finally:
file.close()
except pywintypes.error, exc_value:
# error: (33, 'LockFileEx',
# 'The process cannot access the file because another
# process has locked a portion of the file.')
file.close()
if exc_value[0] == 33:
raise LockException(LockException.LOCK_FAILED, exc_value[2])
else:
# Q: Are there exceptions/codes we should be dealing with?
raise
# --------------------------------------
# Implementation for Posix
elif os.name == 'posix':
@contextmanager
def lock(filename, flags):
file = open(filename, "w+")
try:
fcntl.flock(file.fileno(), flags)
try:
yield
finally:
file.close()
except IOError, exc_value:
# IOError: [Errno 11] Resource temporarily unavailable
file.close()
if exc_value[0] == 11:
raise LockException(LockException.LOCK_FAILED, exc_value[1])
else:
raise
# Copyright 2013 GRNET S.A. All rights reserved.
#
# Redistribution and use in source and binary forms, with or
# without modification, are permitted provided that the following
# conditions are met:
#
# 1. Redistributions of source code must retain the above
# copyright notice, this list of conditions and the following
# disclaimer.
#
# 2. Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials
# provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# The views and conclusions contained in the software and
# documentation are those of the authors and should not be
# interpreted as representing official policies, either expressed
# or implied, of GRNET S.A.
"""
This is the logging class for burnin
It supports logging both for the stdout/stderr as well as file logging at the
same time.
The stdout/stderr logger supports verbose levels and colors but the file
logging doesn't (we use the info verbose level for our file logger).
Our loggers have primitive support for handling parallel execution (even though
burnin doesn't support it yet). To do so the stdout/stderr logger prepends the
name of the test under execution to every line it prints. On the other hand the
file logger waits to lock the file, then reads it, prints the message to the
corresponding line and closes the file.
"""
import os
import sys
import os.path
import datetime
import filelocker
# --------------------------------------------------------------------
# Constant variables
LOCK_EXT = ".lock"
SECTION_SEPARATOR = \
"-- -------------------------------------------------------------------"
SECTION_PREFIX = "-- "
SECTION_RUNNED = "Tests Runned"
SECTION_RESULTS = "Results"
SECTION_NEW = "__ADD_NEW_SECTION__"
SECTION_PASSED = " * Passed:"
SECTION_FAILED = " * Failed:"
# --------------------------------------------------------------------
# Helper functions
def _blue(msg):
"""Blue color"""
return "\x1b[1;34m" + str(msg) + "\x1b[0m"
def _yellow(msg):
"""Yellow color"""
return "\x1b[33m" + str(msg) + "\x1b[0m"
def _red(msg):
"""Yellow color"""
return "\x1b[31m" + str(msg) + "\x1b[0m"
def _ts_start(msg):
"""New testsuite color"""
return "\x1b[35m" + str(msg) + "\x1b[0m"
def _ts_success(msg):
"""Testsuite passed color"""
return "\x1b[42m" + str(msg) + "\x1b[0m"
def _ts_failure(msg):
"""Testsuite failed color"""
return "\x1b[41m" + str(msg) + "\x1b[0m"
def _format_message(msg, *args):
"""Format the message using the args"""
return (msg % args) + "\n"
def _list_to_string(lst, append=""):
"""Convert a list of strings to string
Append the value given in L{append} in front of all lines
(except of the first line).
"""
if isinstance(lst, list):
return append.join(lst).rstrip('\n')
else:
return lst.rstrip('\n')
# --------------------------------------
def _locate_sections(contents):
"""Locate the sections inside the logging file"""
i = 0
res = []
for cnt in contents:
if SECTION_SEPARATOR in cnt:
res.append(i+1)
i += 1
return res
def _locate_input(contents, section):
"""Locate position to insert text
Given a section location the next possition to insert text inside that
section.
"""
sect_locs = _locate_sections(contents)
if section == SECTION_NEW:
# We want to add a new section
# Just return the position of SECTION_RESULTS
for obj in sect_locs:
if SECTION_RESULTS in contents[obj]:
return obj - 1
else:
# We will add our message in this location
for (index, obj) in enumerate(sect_locs):
if section in contents[obj]:
return sect_locs[index+1] - 3
# We didn't find our section??
sys.stderr.write("Section %s could not be found in logging file\n"
% section)
sys.exit(1)
def _add_testsuite_results(contents, section, testsuite):
"""Add the given testsuite to results
Well we know that SECTION_FAILED is the last line and SECTION_PASSED is the
line before, so we are going to cheat here and use this information.
"""
if section == SECTION_PASSED:
line = contents[-2]
new_line = line.rstrip() + " " + testsuite + ",\n"
contents[-2] = new_line
elif section == SECTION_FAILED:
line = contents[-1]
new_line = line.rstrip() + " " + testsuite + ",\n"
contents[-1] = new_line
else:
sys.stderr.write("Unknown section %s in _add_testsuite_results\n"
% section)
sys.exit(1)
return contents
def _write_log_file(file_location, section, message):
"""Write something to our log file
For this we have to get the lock, read and parse the file add the new
message and re-write the file.
"""
# Get the lock
file_lock = os.path.splitext(file_location)[0] + LOCK_EXT
with filelocker.lock(file_lock, filelocker.LOCK_EX):
with open(file_location, "r+") as log_file:
contents = log_file.readlines()
if section == SECTION_PASSED or section == SECTION_FAILED:
# Add testsuite to results
new_contents = \
_add_testsuite_results(contents, section, message)
else:
# Add message to its line
input_loc = _locate_input(contents, section)
new_contents = \
contents[:input_loc] + [message] + contents[input_loc:]
log_file.seek(0)
log_file.write("".join(new_contents))
# --------------------------------------------------------------------
# The Log class
class Log(object):
"""Burnin logger
"""
# ----------------------------------
def __init__(self, output_dir, verbose=1, use_colors=True,
in_parallel=False):
"""Initialize our loggers
The file to be used by our file logger will be created inside
the L{output_dir} with name the current timestamp.
@type output_dir: string
@param output_dir: the directory to save the output file
@type verbose: int
@param verbose: the verbose level to use for stdout/stderr logger
0: verbose at minimum level (only which test we are running now)
1: verbose at info level (information about our running test)
2: verbose at debug level
@type use_colors: boolean
@param use_colors: use colors for out stdout/stderr logger
@type in_parallel: boolean
@param in_parallel: this signifies that burnin is running in parallel
"""
self.verbose = verbose
self.use_colors = use_colors
self.in_parallel = in_parallel
# Create file for logging
output_dir = os.path.expanduser(output_dir)
if not os.path.exists(output_dir):
self.debug(None, "Creating directory %s", output_dir)
os.makedirs(output_dir)
timestamp = datetime.datetime.strftime(
datetime.datetime.now(), "%Y%m%d%H%M%S (%a %b %d %Y %H:%M)")
file_name = timestamp + ".log"
self.file_location = os.path.join(output_dir, file_name)
timestamp = datetime.datetime.strftime(
datetime.datetime.now(), "%a %b %d %Y %H:%M")
sys.stdout.write("Starting burnin (%s)\n" % timestamp)
# Create the logging file
self._create_logging_file(timestamp)
def _create_logging_file(self, timestamp):
"""Create the logging file"""
self.debug(None, "Using \"%s\" file for logging", self.file_location)