From b0ce2d6d62a5d675bee5bc902a257c0c2eb0b65f Mon Sep 17 00:00:00 2001 From: David Teigland Date: Aug 14 2012 21:32:48 +0000 Subject: tests: add test-recovery script to run recovery tests also remove old unused stuff Signed-off-by: David Teigland --- diff --git a/tests/confUtils.py b/tests/confUtils.py deleted file mode 100644 index a4a986e..0000000 --- a/tests/confUtils.py +++ /dev/null @@ -1,309 +0,0 @@ -""" -ConfUtils is a general purpose configuration infrastructure. - -This module contains various classes and functions to help with the creation of a structured and robust configuration for your application. -ConfUtils extends the general functionality of python's idea of a configuration file and uses the same file format for saving configurations. -Thus making it's configuration 100% compatible with python's simpler configuration parsers and utilities. -The main difference is that ConfUtils treats sections and options as **case sensitive** while python's own config parsers are generally **case insensitive**. - -Configuration Templates -======================= -ConfigUtils uses Configuration Templates as the basis of all of it's advanced functions. -Configuration Template is a way of representing what you expect the configuration to look like and how you want to use it. - -A template is a specially crafted mishmash of python dictionaries. This is an example of a configuration template: - - #The template is a dict - configurationTemplate = { - #Each key is a section with another dict as the value - "Section1" : { - #Each key in the sub dict is an option, and a value is a dict containing the option's metadata. - "Option1" : {"default" : "Default Value", "comment" : "Comment", "validator" : Validate.int} - "Option2" : {} # Note that if you don't want to set any restrictions you still need to supply an empty dict. - } - "Section2" : { - "Option3" : {"default" : "Bob"} # You can optionally fill in only a subset of the metadata. - } - } - -This template validates this config: - - [Section1] - Option1 = Bill - Option2 = 3 - [Section2] - Option3 = Ted - -Option Meta Data ----------------- -Every option can have added attributes that define it. - -* default - The default value of this option. If the config is missing this option this value will be used. -* comment - Used when generating a sample configuration. If this exists a comment above the option will be written. -* validator - A method that validates that the value in the configuration is correct. This can be any method that: - 1. Accepts 1 argument. - 2. Raises an exception in case validation fails. - 3. Return the value as a python native type -""" - -from ConfigParser import ConfigParser, RawConfigParser -import os - -class AdvancedConfigParser(RawConfigParser): - """ - A configuration parser that supports the advance features of ConfUtils. - Specifically case sensitivity and writing comments. - """ - def __init__(self): - RawConfigParser.__init__(self) - self._comments = {} - - def set_option_comment(self, section, option, comment): - """ - Set the comment that will appear if the config is written to a file. - """ - if not self.has_option(section, option): - raise KeyError("No such option '%s.%s'." %(section, option)) - if not section in self._comments: - self._comments[section] = {} - - self._comments[section][option] = comment - - def optionxform(self, option): - """ - Changes the behaviour so that it keeps the case of the option. - """ - return option - - def write(self, fileobject): - """ - Write the config file to an object **including** comments - """ - comments = self._comments - for section in self.sections(): - #write section - fileobject.write("[%s]\n" % section) - for option in self.options(section): - hasComment = (section in comments and - option in comments[section] and - comments[section][option] != None) - - if hasComment: - comment = comments[section][option] - comment = "#" + "\n#".join(comment.splitlines()) - fileobject.write(comment + "\n") - - value = str(self.get(section, option)) - # If option contains multiple lines - if "\n" in value: - value = "\n\t".join(value.splitlines()) - - fileobject.write("%s: %s\n" % (option, value)) - else: - fileobject.write("%s = %s\n" % (option, value)) - #pad section - fileobject.write("\n\n") - -class TemplateMergeError(RuntimeError) : pass - -class ConfigurateionValidationError(RuntimeError) : pass - -def mergeTemplates(templates): - """ - A logical way to merege template. - .. note:: - Templates a merged in the way they were recieved. - - .. warning:: - In any option arg conflict the new will override the old. - - :param templates: a list of templates to merge. - """ - finalTemplate = {} - for template in templates: - for section, options in template.iteritems(): - if not section in finalTemplate: - finalTemplate[section] = {} - - for option, args in options.iteritems(): - if not option in finalTemplate[section]: - finalTemplate[section][option] = args - elif finalTemplate[section][option] != args: - raise TemplateMergeError("Option '%s.%s' exists in two templates but doesn't have the same definition." % (section, option)) - - return finalTemplate - -class Validate(object): - """ - A class with common validators. - """ - #TBD: make thread safe? - _innerConfig = ConfigParser() - - @classmethod - def _genericGetValue(cls, methodName, value): - innerConfig = cls._innerConfig - - if not innerConfig.has_section("tmp"): - innerConfig.add_section("tmp") - - innerConfig.set("tmp", "tmp", value) - validationMethod = getattr(innerConfig, methodName) - return validationMethod("tmp", "tmp") - - @classmethod - def int(cls, value): - if isinstance(value, int): - return value - return cls._genericGetValue("getint", value) - - @classmethod - def bool(cls, value): - if isinstance(value, bool): - return value - - return cls._genericGetValue("getboolean", value) - - @classmethod - def float(cls, value): - if isinstance(value, float): - return value - return cls._genericGetValue("getfloat", value) - - @classmethod - def list(cls, value): - if isinstance(value, list): - return value - return [i.strip() for i in value.split(",")] - - @classmethod - def dict(cls, value): - if isinstance(value, dict): - return value - value = value.strip() - if not (value.startswith("{") and value.endswith("}")): - raise ValueError("String doesn't represent a dict.") - res = eval(value) - if not isinstance(res, dict): - raise ValueError("String doesn't represent a dict.") - return res - @classmethod - def pathExists(cls, value): - if os.path.exists(value): - return value - - raise ValueError("Path doesn't exist.") - -def generateSampleConfigFile(template, targetFile): - """ - Generates a sample config file from a template. - - :param template: A config template. - :param tergetfile: A file path or a writable file-like object. - """ - cfg = AdvancedConfigParser() - if not isinstance(template, dict): - raise TypeError("Template must be a dict") - - for section, options in template.iteritems(): - #Create the section - cfg.add_section(section) - - #Compile the options - if not isinstance(options, dict): - raise TypeError("Template options must be a dict") - - for option, args in options.iteritems(): - if not isinstance(args, dict): - raise TypeError("Options metadata must be a dict") - - defaultValue = "" - if args.has_key("default"): - defaultValue = args["default"] - cfg.set(section, option, defaultValue) - - if "comment" in args: - cfg.set_option_comment(section, option, args["comment"]) - - # Write the generated config file - if type(targetFile) in (str, unicode): - cfg.write(open(targetFile, "w")) - elif hasattr(targetFile, "write"): - targetFile.write(targetFile) - else: - raise TypeError("targetFile: Expected a path or a file-like object") - -def validateConfigFile(template, cfg): - """ - Validate that config file conforms with template. - - :param cfg: The path to the config file or a :class:`~ConfigParser.ConfigParser` instance. - :param template: A config template. - - :returns: A touple in the format of ``(result, message)``. - *result* will be :keyword:`True` if validation - was seccessful. - """ - #Make sure cfg is a config object. - if type(cfg) in (str, unicode): - if not os.path.exists(cfg): - raise ConfigurateionValidationError("File '%s' doesn't exist." % cfg) - path = cfg - cfg = ConfigParser() - cfg.read(path) - elif not isinstance(cfg, RawConfigParser): - raise TypeError("Parameter 'cfg' must be a path or a config object") - - #Test if sections exist - for section, options in template.iteritems(): - if not cfg.has_section(section): - raise ConfigurateionValidationError("Section %s is missing." % section) - - #Validate that options exist and are valid. - for option, args in options.iteritems(): - hasDefaultValue = ("default" in args) - - exists = cfg.has_option(section, option) - if not exists and not hasDefaultValue: - raise ConfigurateionValidationError("Option %s.%s is missing." % (section, option)) - - if exists: - optionValue = cfg.get(section, option) - else: - optionValue = args["default"] - - if args.has_key("validator"): - try: - args["validator"](optionValue) - except Exception, ex: - raise ConfigurateionValidationError("Parsing of option %s.%s with the value '%s' failed (%s: %s)." % - (section, option, optionValue, ex.__class__.__name__, ex)) - - return True - -def conf2dict(template, cfg): - """ - Converts a config file to a dict using the template to convert types from - strings to native data types. - - .. note:: - * Assumes template is validated. - * Extracts only the field declared in the templates. - """ - outputDict = {} - for section, options in template.iteritems(): - outputDict[section] = {} - - for option, args in options.iteritems(): - if cfg.has_option(section, option): - rawOptionValue = cfg.get(section, option) - elif "default" in args: - rawOptionValue = args["default"] - - hasValidator = ("validator" in args) - if hasValidator: - outputDict[section][option] = args["validator"](rawOptionValue) - else: - outputDict[section][option] = rawOptionValue - return outputDict - diff --git a/tests/enum.py b/tests/enum.py deleted file mode 100644 index 34e00e8..0000000 --- a/tests/enum.py +++ /dev/null @@ -1,41 +0,0 @@ -class Enum(object): - """ - A nice class to handle Enums gracefullly. - """ - def __init__(self, **pairs): - #Generate reverse dict - self._reverse = dict([(b, a) for a, b in pairs.iteritems()]) - - #Generate attributes - for key, value in pairs.iteritems(): - setattr(self, key, value) - - def __getitem__(self, index): - return self._reverse[index] - - def __iter__(self): - return self._reverse.itervalues() - - def parse(self, value): - #If value is enum name convert to value - if isinstance(value, str): - if hasattr(self, value): - return getattr(self, value) - #If value is a number assume parsing meant converting the value to int - #if you can think of a more generic way feel free to change - if value.isdigit(): - value = int(value) - - #If not check if value is a value of the enum - if value in self._reverse: - return value - - #Enum doesn't know this value - raise ValueError("Value '%s' is not in the Enum." % value) - -if __name__ == "__main__": - eColors = Enum( - Red = 1, - Blue = 2 - ) - print eColors.Red, eColors.Blue, eColors[1] diff --git a/tests/paxosState.py b/tests/paxosState.py deleted file mode 100755 index cf55d4e..0000000 --- a/tests/paxosState.py +++ /dev/null @@ -1,54 +0,0 @@ -#!/usr/bin/python -from testUtils import readState, DBlock, nullTerminated -from StringIO import StringIO -import time -import sys - -USAGE = "usage: paxosState.py : [:]" - -def formatPaxoState(disk, offset): - with open(disk, "rb") as f: - f.seek(offset) - leader, dblocks = readState(f) - - res = StringIO() - res.write("LEADER\n------\n") - for key in leader._fields: - val = getattr(leader, key) - if key == "timestamp": - val = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(val)) - elif isinstance(val, str): - val = nullTerminated(val) - res.write("%s:\t%s%s\n" % (key, '\t' if len(key) < 7 else '', val)) - - res.write("\nBLOCKS\n------\n") - for field in DBlock._fields: - res.write("%s:" % field) - for dblock in dblocks: - res.write("\t%s" % getattr(dblock, field)) - res.write("\n") - - res.seek(0) - return res.read() - - -if __name__ == "__main__": - if len(sys.argv) < 2: - print USAGE - sys.exit(1) - - disks = [] - try: - for arg in sys.argv[1:]: - disk, offset = arg.split(":") - offset = int(offset) - disks.append((disk, offset)) - except: - print USAGE - sys.exit(1) - - for disk, offset in disks: - print "**** %s:%d ****" % (disk, offset) - print formatPaxoState(disk, offset) - - diff --git a/tests/ruth.py b/tests/ruth.py deleted file mode 100755 index 9c3151f..0000000 --- a/tests/ruth.py +++ /dev/null @@ -1,359 +0,0 @@ -#!/usr/bin/python -# Copyright 2009 Red Hat, Inc. and/or its affiliates. -# -# Licensed to you under the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. See the files README and -# LICENSE_GPL_v2 which accompany this distribution. -# - -import sys -from optparse import OptionParser -import os -from ConfigParser import ConfigParser -import logging -from copy import deepcopy -import unittest as ut - -import confUtils -from confUtils import Validate -from testRunner import TestRunner -#To use the same instance as everyone else -import ruth - -LOG_LEVELS = {'d': logging.DEBUG, - 'i': logging.INFO, - 'w': logging.WARNING, - 'e': logging.ERROR, - 'c': logging.CRITICAL, - 'debug': logging.DEBUG, - 'info': logging.INFO, - 'warning': logging.WARNING, - 'error': logging.ERROR, - 'critical': logging.CRITICAL} - -DEFAULT_CONFIG_PATH = "~/.ruthrc" - -USAGE = '''usage: %%prog [options] [conf1 [conf2 [...] ] ] -Loads the configuration from '%s', unless other 'config' files are specified -in command line. For more help read \"README.1st\".''' % (DEFAULT_CONFIG_PATH) - -CONFIG_TEMPLATE = { - "global" : { - "verbose" : {"default" : 1, "validator" : Validate.int}, - "modules" : {"validator" : Validate.list}, - } -} - -def generateTemplateFromSuite(suite): - """ - Generate a config template from a test suit. - """ - templates = [] - for testCase in suite: - if hasattr(testCase, "getConfigTemplate"): - templates.append(testCase.getConfigTemplate()) - - return confUtils.mergeTemplates(templates) - -def validateSuiteConfig(suite, cfg): - """ - Validate a config against a suite. Validates that all the test cases in the suite have - all the options they need in the configuration file. - - To be used by most ruth modules. - - :returns: a touple of ``(result, message)``. - """ - masterTemplate = generateTemplateFromSuite(suite) - cfg = _expandGlobalCfg(masterTemplate, cfg) - return confUtils.validateConfigFile(masterTemplate, cfg) - -def _expandGlobalCfg(template, cfg): - """ - Distribute the options defined in 'global' to all the sections. - - :returns: a new config file with the global section distributed - to all the sections defined in the template. - """ - #Work on a copy - cfg = deepcopy(cfg) - - #Do we even need to work - if not cfg.has_section("global"): - return cfg - - for section in template: - if not cfg.has_section(section): - cfg.add_section(section) - for option in template[section]: - if not cfg.has_option("global", option): - continue - - globalValue = cfg.get("global", option) - cfg.set(section, option, globalValue) - - return cfg - -class RuthTestCase(ut.TestCase): - mycfg = property(lambda self: self.cfg[self.__class__.__name__]) - def _getConfig(self): - """ - Manages the configuration wrapper of a test case - """ - a = self.__class__ - if not hasattr(self, "_confDict"): - template = self.getConfigTemplate() - expandedCfg = _expandGlobalCfg(template, self._cfg) - confDict = confUtils.conf2dict(template, expandedCfg) - setattr(self, "_confDict", confDict) - return self._confDict - - def _getLog(self): - if (not hasattr(self, "_log")) or self._log == None: - self._log = logging.getLogger("test." + self.id()) - - return self._log - - def _setLog(self, value): - self._log = value - - log = property(lambda self: self._getLog(), lambda self, value: self._setLog(value)) - - #Dynamic because the base class get the conf directly from Ruth. - #If it were static everyone would have the basic classes wrapper. - cfg = property(lambda self : self._getConfig()) - - @classmethod - def getConfigTemplate(cls): - """ - Returns a config template that announces what the - test case expects from the config file. - - .. note:: - Should be overrided by subclasses. - """ - return {} - - @classmethod - def setConfig(cls, cfg): - cls._cfg = cfg - - def setUp(self): - pass - - def tearDown(self): - pass - - -def parseArguments(): - """ - Prepares the options parser and parses the cmd line args. - """ - usage = USAGE - parser = OptionParser(usage=usage) - - #Prepare generation configuration option - parser.add_option("-g", "--generate-configuration", - action="store", dest="moduleToGenerate", type="string", metavar="MODULE", default=None, - help="Instead of running the suite. Creates a sample configuration from MODULE)") - - #prepare quiet option - parser.add_option("-q", "--quiet", - action="store_true", dest="quiet", default=False, - help="Should I bother you with unnecessary niceties. (Hello message and end quote).") - - #prepare verbose option - parser.add_option("-v", - action="count", dest="verbosity", default=0, - help="Override configurations' verbose level.") - - #prepare filter option - parser.add_option("-f", "--filter-tests", - action="store", dest="filter", default="*", metavar="GLOB", - help="Only tests that match this glob filter will run." + \ - "Using '^' in the beginning of the glob means: Match opposite of GLOB.") - - #prepare debug option - parser.add_option("-d", "--debug", - action="store_true", dest="debug", default=False, - help="Should I print a lot of output in case of internal errors.") - - #prepare log option - parser.add_option("-l", "--logging", - action="store", dest="logLevel", default=None, metavar="LEVEL", - help="Turn on test logging of the level LEVEL") - - #parse args - options, args = parser.parse_args() - if len(args) == 0: - args = [DEFAULT_CONFIG_PATH] - - return (options, args) - -def generateSampleConfigFile(defaultModule, suite, targetFile): - #Generate template - template = generateTemplateFromSuite(suite) - - #Add default module - if not "global" in template: - template["global"] = {} - globalSection = template["global"] - - if not "modules" in globalSection: - globalSection["modules"] = {} - - template["global"]["modules"]["default"] = defaultModule - - #Write it all to disk - confUtils.generateSampleConfigFile(template, targetFile) - -def handleSampleConfigFileGeneration(moduleToGenerate, targetFile): - """ - Takes care of sample config generation. - - :param moduleToGenerate: The name of the python module. - Should be the same as in and :keyword:`import` statement. - :param targetFile: The path to where to sample config file will be generated. - - :returns: **0** if successful, **400** on import error or **500** on config generation error. - """ - #Import module - try: - print "Importing module '%s'..." % (moduleToGenerate) - suiteModule = __import__(moduleToGenerate) - except Exception, ex: - print "Could not import module '%s'. (%s)" % (moduleToGenerate, ex) - return 400 - - #Get suite and generate config file - try: - print "Generating sample config file at '%s'..." % (targetFile) - generateSampleConfigFile(moduleToGenerate, suiteModule.suite(), targetFile) - except Exception, ex: - print "Could not generate sample config file from module '%s'. (%s: %s)" % (moduleToGenerate, ex.__class__.__name__, ex) - return 500 - - return 0 - -def _printHeader(header, marker="="): - sep = marker * len(header) - print sep - print header - print sep - -def runBatch(confFile, options): - """ - Run a batch test as stated in a config file. - """ - # Try to load config file - mycfg = {} - batchcfg = {} - output = sys.stdout - try: - output.write("Validating configuration file '%s'.\n" % (os.path.split(confFile)[1])) - output.flush() - confUtils.validateConfigFile(CONFIG_TEMPLATE, confFile) - output.write("Loading RUTH configuration.\n") - batchcfg = ConfigParser() - batchcfg.read(confFile) - mycfg = confUtils.conf2dict(CONFIG_TEMPLATE, batchcfg) - except Exception, ex: - raise Exception("Could not load config file '%s'. Bailing out from batch. (%s: %s)" % (confFile, ex.__class__.__name__, ex)) - - #Get modules to test - modules = mycfg["global"]["modules"] - output.write("Running tests from modules: %s.\n" % (", ".join(modules))) - output.flush() - - #test modules - batch = {} - for mod in modules[:]: - #import module - imported_module = __import__(mod) - - try: - if hasattr(imported_module, "validateConfig"): - imported_module.validateConfig(batchcfg) - else: - validateSuiteConfig(imported_module.suite(), batchcfg) - batch[mod] = imported_module - output.write("Module '%s' is READY\n" % (mod)) - except Exception, ex: - output.write("Module '%s' is NOT READY (%s: %s)\n" % (mod, ex.__class__.__name__, ex)) - modules.remove(mod) - #set configuration - ruth.RuthTestCase.setConfig(batchcfg) - - results = [] - #run tests - for mod in batch: - output.write("Exercising module '%s'\n" % mod) - output.flush() - suite = batch[mod].suite() - verbose = mycfg["global"]["verbose"] - - if options.verbosity > 0: - verbose = options.verbosity - - logging = True - if options.logLevel is None: - logging = False - - results.append(TestRunner(verbosity=verbose, stream=output, filter=options.filter, logging=logging).run(suite)) - - return results - -def main(): - hello = """ - Hello, nice to meet you. I am RUTH - "Regression and Unit Test Harness". - I am going to run a comprehensive test suite in order to validate vdsm - functionality. However, I may require some assistance from you in order - to correctly bootstrap the whole procedure. - Use --help to see what you can do with me. - """ - - options, args = parseArguments() - if options.logLevel is None: - logging.basicConfig(filename='/dev/null') - else: - if not options.logLevel in LOG_LEVELS: - print "Invalid logging level, possible values are %s." % ", ".join(options.keys()) - return - - logging.basicConfig(filename='/dev/stdout', filemode='w+',level=LOG_LEVELS[options.logLevel], - format="\t\t%(asctime)s %(levelname)-8s%(message)s", datefmt='%H:%M:%S') - - if not options.quiet: - print hello - - if options.moduleToGenerate: - return handleSampleConfigFileGeneration(options.moduleToGenerate, args[0]) - - #iterate config files and run their tests - configFiles = args - i = 0 - results = [] - isMultipleConfigMode = len(configFiles) > 1 - for confFile in configFiles: - i += 1 - if isMultipleConfigMode: - _printHeader("Processing batch %d of %d. Configuration is '%s'." % (i, len(configFiles), os.path.split(confFile)[1])) - try: - results.extend(runBatch(os.path.expanduser(confFile), options)) - except Exception, ex: - if options.debug: - import traceback - print traceback.format_exc() - print ex - - if isMultipleConfigMode: - totalFailures = sum([len(result.failures) for result in results]) - totalErrors = sum([len(result.errors) for result in results]) - _printHeader("Totals: Failures %d, Errors %d." % (totalFailures, totalErrors)) - - if not options.quiet: - print 'All Done!\nremember:\n\t"To Err is Human, To Test is Divine!"' - -if __name__ == '__main__': - main() diff --git a/tests/syncManager.py b/tests/syncManager.py deleted file mode 120000 index 99f718b..0000000 --- a/tests/syncManager.py +++ /dev/null @@ -1 +0,0 @@ -../../python/syncManager.py \ No newline at end of file diff --git a/tests/syncManagerTests.py b/tests/syncManagerTests.py deleted file mode 100644 index b2801e8..0000000 --- a/tests/syncManagerTests.py +++ /dev/null @@ -1,185 +0,0 @@ -import unittest as ut -import time - -from ruth import RuthTestCase -import syncManager -from syncManager import SyncManager -from confUtils import Validate -from testUtils import LeaderRecord, readState, nullTerminated, leasesValidator, getResources -from testUtils import Dummy - -DEFAULT_NUMBER_OF_HOSTS = 10 -MAXIMUM_NUMBER_OF_HOSTS = 10 #2000 -DEFAULT_NAME = "RUTH" -DEFAULT_LEASES = "::[:], ..." -LEASES_CONFIG_DEFINITION = {"validator": leasesValidator, "default" : DEFAULT_LEASES} -SYNCMANAGER_PATH="../sync_manager" - -syncManager.SYNCMANAGER_PATH = SYNCMANAGER_PATH - -class DriveInitialization(RuthTestCase): - @classmethod - def getConfigTemplate(cls): - return { cls.__name__ : { - "Leases" : LEASES_CONFIG_DEFINITION, - "NumberOfHosts" : {"validator" : Validate.int, "default" : DEFAULT_NUMBER_OF_HOSTS} - } - } - - def test(self): - mgr = SyncManager(DEFAULT_NAME) - leases = self.mycfg["Leases"] - mgr.initStorage(leases, self.mycfg["NumberOfHosts"], MAXIMUM_NUMBER_OF_HOSTS) - for lease, drives in leases: - for drive, offset in drives: - with open(drive, "rb") as f: - f.seek(offset) - leader, blocks = readState(f, MAXIMUM_NUMBER_OF_HOSTS) - self.assertEquals(nullTerminated(leader.resourceID), lease) - self.assertEquals(leader.numHosts, self.mycfg["NumberOfHosts"]) - self.assertEquals(leader.maxHosts, MAXIMUM_NUMBER_OF_HOSTS) - for block in blocks: - self.assertEquals(block.bal, 0) - self.assertEquals(block.mbal, 0) - self.assertEquals(block.inp, 0) - self.assertEquals(block.lver, 0) - -class InitPerformanceTest(RuthTestCase): - @classmethod - def getConfigTemplate(cls): - return { cls.__name__ : { - "AcceptableTimeSpan" : {"validator" : Validate.float, "default" : 60.0}, - "Leases" : LEASES_CONFIG_DEFINITION, - "NumberOfHosts" : {"validator" : Validate.int, "default" : DEFAULT_NUMBER_OF_HOSTS} - } - } - - def test(self): - mgr = SyncManager(DEFAULT_NAME) - start = time.time() - mgr.initStorage(self.mycfg["Leases"], self.mycfg["NumberOfHosts"], MAXIMUM_NUMBER_OF_HOSTS) - end = time.time() - self.assertTrue((end - start) <= self.mycfg["AcceptableTimeSpan"]) - -class AcquireLease(RuthTestCase): - @classmethod - def getConfigTemplate(cls): - return { cls.__name__ : { - "Leases" : LEASES_CONFIG_DEFINITION, - "NumberOfHosts" : {"validator" : Validate.int, "default" : DEFAULT_NUMBER_OF_HOSTS} - } - } - - def setUp(self): - self.mgr = SyncManager(DEFAULT_NAME) - self.log.debug("Initializing disks") - self.mgr.initStorage(self.mycfg["Leases"], self.mycfg["NumberOfHosts"], MAXIMUM_NUMBER_OF_HOSTS) - self.log.debug("Starting Dummy Process") - self.dummy = Dummy(DEFAULT_NAME, 1) - - def testGood(self): - self.log.debug("Acquiring leases") - self.mgr.acquireLeases(self.mycfg["Leases"]) - self.mgr.releaseLeases(getResources(self.mycfg["Leases"])) - - def testWithBadDrive(self): - self.log.debug("Acquiring leases") - # Adding fake lease - leases = list(self.mycfg["Leases"]) + [("Sense-Sphere", [("./disk.fake", 0)])] - self.assertRaises(Exception, self.mgr.acquireLeases, leases); - - def tearDown(self): - self.dummy.stop() - -class ReleaseLease(RuthTestCase): - @classmethod - def getConfigTemplate(cls): - return { cls.__name__ : { - "Leases" : LEASES_CONFIG_DEFINITION, - "NumberOfHosts" : {"validator" : Validate.int, "default" : DEFAULT_NUMBER_OF_HOSTS} - } - } - - def setUp(self): - self.mgr = SyncManager(DEFAULT_NAME) - self.log.debug("Initializing disks") - self.mgr.initStorage(self.mycfg["Leases"], self.mycfg["NumberOfHosts"], MAXIMUM_NUMBER_OF_HOSTS) - self.log.debug("Starting Dummy Process") - self.dummy = Dummy(DEFAULT_NAME, 1) - self.log.debug("Acquiring leases") - self.mgr.acquireLeases(self.mycfg["Leases"]) - - def testGood(self): - self.mgr.releaseLeases(getResources(self.mycfg["Leases"])) - - def testUnacquired(self): - resources = getResources(self.mycfg["Leases"]) - self.assertRaises(Exception, self.mgr.releaseLeases, resources + ["Sense-Sphere"]) - self.mgr.releaseLeases(resources) - - def tearDown(self): - self.dummy.stop() - -class InitialLeasesTests(RuthTestCase): - @classmethod - def getConfigTemplate(cls): - return { cls.__name__ : { - "Leases" : LEASES_CONFIG_DEFINITION, - "NumberOfHosts" : {"validator" : Validate.int, "default" : DEFAULT_NUMBER_OF_HOSTS} - } - } - - def setUp(self): - self.mgr = SyncManager(DEFAULT_NAME) - self.log.debug("Initializing disks") - self.mgr.initStorage(self.mycfg["Leases"], self.mycfg["NumberOfHosts"], MAXIMUM_NUMBER_OF_HOSTS) - - def acquireInitialLeases(self): - self.dummy = Dummy(DEFAULT_NAME, 1, self.mycfg["Leases"]) - self.mgr.releaseLeases(getResources(self.mycfg["Leases"])) - - def acquireInitialLeasesWithoutHostID(self): - try: - self.dummy = Dummy(DEFAULT_NAME, -1, self.mycfg["Leases"]) - except: - return - self.fail("Managed to start sync_manager daemon without a host ID") - - def acquireLeasesFromDaemonizedSyncManagerWithoutSettingHostID(self): - self.dummy = Dummy(DEFAULT_NAME) - self.assertRaises(Exception, self.mgr.acquireLeases, self.mycfg["Leases"]) - - def acquireLeasesFromDaemonizedSyncManagerAfterSettingHostID(self): - self.dummy = Dummy(DEFAULT_NAME) - self.mgr.setHostID(1); - self.mgr.acquireLeases(self.mycfg["Leases"]) - - def resetHostID(self): - self.dummy = Dummy(DEFAULT_NAME) - self.mgr.setHostID(1); - self.assertRaises(Exception, self.mgr.setHostID, 2); - self.mgr.acquireLeases(self.mycfg["Leases"]) - - def tearDown(self): - if hasattr(self, "dummy"): - self.dummy.stop() - -def suite(): - tests = { - DriveInitialization : ["test"], - InitPerformanceTest : ["test"], - AcquireLease : ["testGood", "testWithBadDrive"], - ReleaseLease : ["testGood", "testUnacquired"], - InitialLeasesTests : ["acquireInitialLeases", - "acquireInitialLeasesWithoutHostID", - "acquireLeasesFromDaemonizedSyncManagerWithoutSettingHostID", - "acquireLeasesFromDaemonizedSyncManagerAfterSettingHostID", - "resetHostID"] - } - - resSuite = ut.TestSuite() - for testcase, methods in tests.iteritems(): - resSuite.addTests(map(testcase, methods)) - - return resSuite - diff --git a/tests/test-recovery.sh b/tests/test-recovery.sh new file mode 100755 index 0000000..b5d0f28 --- /dev/null +++ b/tests/test-recovery.sh @@ -0,0 +1,247 @@ +#!/bin/bash + +# +# recovery tests based on 10 sec io timeout +# + + +dev=$1 + +echo test lockspace storage loss, recovery by lease release using killpath +echo messages: sanlock check lease warn/fail, kill 100, all pids clear +echo messages: wdmd warn, close, fail +echo messages: killpath_pause +date +set -x +./clientn 4 start $dev 1 /root/killpath_pause +sleep 5 +./clientn 4 error $dev +sleep 150 +./clientn 4 resume $dev 1 +sleep 5 +killall -9 sanlk_client +sleep 5 +set +x + + +echo test lockspace storage loss, recovery by escalation from killpath to sigkill +echo messages: sanlock check lease warn/fail, kill 100, kill 9, dead, all pids clear +echo messages: wdmd warn, close, fail +echo messages: killpath_args +date +set -x +./clientn 4 start $dev 1 /root/killpath_args +sleep 5 +./clientn 4 error $dev +sleep 150 +./clientn 4 linear $dev 1 +sleep 5 +set +x + + +echo test lockspace storage loss, recovery by pid exit using killpath +echo messages: sanlock check lease warn/fail, kill 100, dead, all pids clear +echo messages: wdmd warn, close, fail +echo messages: killpath_term +date +set -x +./clientn 4 start $dev 1 /root/killpath_term +sleep 5 +./clientn 4 error $dev +sleep 150 +./clientn 4 linear $dev 1 +sleep 5 +set +x + + +echo test lockspace storage loss, recovery by pid sigterm without killpath +echo messages: sanlock check lease warn/fail, kill 15, dead, all pids clear +echo messages: wdmd warn, close, fail +date +set -x +./clientn 4 start $dev 1 none +sleep 5 +./clientn 4 error $dev +sleep 150 +./clientn 4 linear $dev 1 +sleep 5 +set +x + + +echo test lockspace storage delay, small enough to have no effect +echo messages: none +date +set -x +./clientn 4 start $dev 1 none +sleep 22 +./clientn 4 iodelay $dev 57 +sleep 5 +killall -9 sanlk_client +sleep 5 +set +x + + +echo test lockspace storage delay, long enough to produce sanlock warning, +echo but not failure, not long enough for wdmd warn or close +echo messages: sanlock check lease warn +date +set -x +./clientn 4 start $dev 1 none +sleep 22 +./clientn 4 iodelay $dev 67 +sleep 5 +killall -9 sanlk_client +sleep 5 +set +x + + +echo test lockspace storage delay, long enough to produce sanlock warning, +echo but not failure/recovery, long enough for wdmd warn and close +echo messages: sanlock check lease warn +echo messages: wdmd warn, close +date +set -x +./clientn 4 start $dev 1 none +sleep 22 +./clientn 4 iodelay $dev 77 +sleep 5 +killall -9 sanlk_client +sleep 5 +set +x + + +echo test lockspace storage delay, long enough to produce sanlock warning, +echo failure/recovery, recovery by lease release using killpath +echo messages: sanlock check lease warn/fail, kill 100, all pids clear +echo messages: killpath_pause +echo messages: wdmd warn, close, fail +date +set -x +./clientn 4 start $dev 1 /root/killpath_pause +sleep 22 +./clientn 4 iodelay $dev 87 +sleep 5 +set +x + + +echo test lockspace storage delay, long enough to produce sanlock warning, +echo failure/recovery, recovery by pid sigterm without killpath +echo messages: sanlock check lease warn/fail, kill 15, dead, all pids clear +echo messages: wdmd warn, close, fail +date +set -x +./clientn 4 start $dev 1 none +sleep 22 +./clientn 4 iodelay $dev 87 +sleep 5 +set +x + + +echo test daemon run delay, small enough to have no effect +echo messages: none +date +set -x +./clientn 4 start $dev 1 none +sleep 22 +./clientn 4 delay 58 +sleep 5 +killall -9 sanlk_client +sleep 5 +set +x + + +echo test daemon run delay, long enough to produce sanlock warning, +echo but not failure, not long enough for wdmd warn or close +echo messages: sanlock check lease warn +date +set -x +./clientn 4 start $dev 1 none +sleep 22 +./clientn 4 delay 68 +sleep 5 +killall -9 sanlk_client +sleep 5 +set +x + + +echo test daemon run delay, long enough to produce sanlock warning, +echo but not failure, long enough for wdmd warn and close +echo messages: sanlock check lease warn +echo messages: wdmd warn, close +date +set -x +./clientn 4 start $dev 1 none +sleep 22 +./clientn 4 delay 78 +sleep 5 +killall -9 sanlk_client +sleep 5 +set +x + + +echo test daemon run delay, long enough to produce sanlock +echo failure/recovery, recovery by lease release using killpath +echo messages: sanlock check lease fail, kill 100, all pids clear +echo messages: wdmd warn, close, fail +echo messages: killpath_pause +date +set -x +./clientn 4 start $dev 1 /root/killpath_pause +sleep 22 +./clientn 4 delay 88 +sleep 5 +./clientn 4 resume $dev 1 +sleep 5 +killall -9 sanlk_client +sleep 5 +set +x + + +echo test daemon run delay, long enough to produce sanlock +echo failure/recovery, recovery by pid sigterm without killpath +echo messages: sanlock check lease fail, kill 15, dead, all pids clear +echo messages: wdmd warn, close, fail +date +set -x +./clientn 4 start $dev 1 none +sleep 22 +./clientn 4 delay 88 +sleep 5 +set +x + + +echo test daemon run delay, long enough to produce sanlock +echo failure/recovery, recovery by pid sigkill after skipping killpath +echo messages: sanlock check lease fail, kill 9, dead, all pids clear +echo messages: wdmd warn, close, fail +date +set -x +./clientn 4 start $dev 1 /root/killpath_pause +sleep 22 +./clientn 4 delay 130 +sleep 5 +set +x + + +echo test daemon run delay, long enough to produce sanlock +echo failure/recovery, recovery by pid sigkill without killpath +echo messages: sanlock check lease fail, kill 9, dead, all pids clear +echo messages: wdmd warn, close, fail +date +set -x +./clientn 4 start $dev 1 none +sleep 22 +./clientn 4 delay 130 +sleep 5 +set +x + + +echo test daemon run delay, long enough to produce watchdog firing +echo messages: wdmd warn, close, fail +date +set -x +./clientn 4 start $dev 1 none +sleep 22 +./clientn 4 delay 140 +echo should not get here + diff --git a/tests/testRunner.py b/tests/testRunner.py deleted file mode 100644 index 1459f72..0000000 --- a/tests/testRunner.py +++ /dev/null @@ -1,171 +0,0 @@ -import sys -from unittest import TestResult -from fnmatch import fnmatch -import traceback -from itertools import chain, ifilter - -from enum import Enum - -eColors = Enum( - Green = '\033[92m', - Yellow = '\033[93m', - Red = '\033[91m', - ENDC = '\033[0m' - ) - -_faultSeperator = "-" * 80 - -def _formatTestFault(test, err, faultTypeName): - res = "%s\n%s: %s :\n%s\n" % (_faultSeperator, faultTypeName, test.id(), err) - return res - - -class _TextTestResult(TestResult): - """ - A better way to display test results in in the terminal. - - Assumes correct an linear execution per test. - """ - def __init__(self, stream, verbosity = 1, logging=False): - TestResult.__init__(self) - self._stream = stream - self._verbosity = verbosity - self._logging = logging - - def _writeToStream(self, msg, color=None): - stream = self._stream - - #Make sure color is a color - if color != None: - color = eColors.parse(color) - - writeColor = False - try: - writeColor = (color != None and stream.isatty()) - except AttributeError: #A strem might no implement isatty - pass - - if writeColor: - msg = color + msg + eColors.ENDC - - stream.write(msg) - stream.flush() - - def startTest(self, test): - TestResult.startTest(self, test) - self._writeToStream("\t%s: " % test.id()) - if self._logging: - self._writeToStream("\n") - - def addSuccess(self, test): - TestResult.addSuccess(self, test) - if self._logging: - self._writeToStream("\tResult: ") - self._writeToStream("OK", eColors.Green) - - def addError(self, test, err): - testname = test.id().split(".")[-1] - tb = err[2] - stack = traceback.extract_tb(tb) - for frame in stack: - fname = frame[2] - if fname == testname: - if self._logging: - self._writeToStream("\tResult: ") - self._writeToStream("Test ERROR", eColors.Yellow) - break - if fname == "setUp": - if self._logging: - self._writeToStream("\tResult: ") - self._writeToStream("SetUp ERROR", eColors.Yellow) - break - if fname == "tearDown": - #If test succeded but tear down failed the result should - #still be that the test failed. So it's my resposibility - #to display thet only the 'test' part of the test passed. (Confused yet?) - faults = chain(self.failures, self.errors) - testFaults = ifilter(lambda item: item[0] == test, faults) - hasFailed = (sum(1 for u in testFaults) > 0) - if not hasFailed: - if self._logging: - self._writeToStream("\tResult: ") - self._writeToStream("PASSED", eColors.Green) - - self._writeToStream(", ") - self._writeToStream("Tear Down ERROR", eColors.Yellow) - break - - TestResult.addError(self, test, err) - - def addFailure(self, test, err): - if self._logging: - self._writeToStream("\tResult: ") - TestResult.addFailure(self, test, err) - self._writeToStream("FAIL", eColors.Red) - - def stopTest(self, test): - TestResult.stopTest(self, test) - self._writeToStream("\n") - self.printTestErrLog(test, 3) - - def printTestErrLog(self, test, minVerbosity): - if self._verbosity < minVerbosity: - return - - for fTest, err in self.failures: - if test == fTest: - self._writeToStream( _formatTestFault(test, err, "FAILURE")) - for eTest, err in self.errors: - if test == eTest: - self._writeToStream( _formatTestFault(test, err, "ERROR")) - -class TestRunner(object): - """ - A test runner that is better then the default :class:`unittest.TextTestRunner`. - Gives prettier output. - """ - def __init__(self, stream = sys.stderr, verbosity=1, filter="*", logging=False): - self._verbosity = verbosity - self._stream = stream - self._filter = filter - self._logging = logging - - def run(self, suite): - """ - Run a test. - """ - stream = self._stream - results = _TextTestResult(stream, self._verbosity, self._logging) - - #Parse filter - filter = self._filter - filterIfMatchIs = True - if filter.startswith("^"): - filterIfMatchIs = False - filter = filter[1:] - filter = filter.replace("\\^", "^") #So you could escape ^. For completeness. - filter = filter.replace("\\\\", "\\") - - for test in suite: - if not (fnmatch(test.id(), filter) == filterIfMatchIs): - continue - - test.run(result = results) - - if results.wasSuccessful(): - msg = "All Good!" - else: - msg = "Failed (failures=%d, errors=%d)." % (len(results.failures), len(results.errors)) - sep = "*" * (len(msg) + 4) + "\n" - stream.write(sep) - stream.write("* " + msg + " *" + "\n") - stream.write(sep) - - if self._verbosity == 2: - for test, err in results.failures: - stream.write(_formatTestFault(test, err, "FAILURE")) - for test, err in results.errors: - stream.write(_formatTestFault(test, err, "ERROR")) - - stream.flush() - return results diff --git a/tests/testUtils.py b/tests/testUtils.py deleted file mode 100644 index 64e5e9a..0000000 --- a/tests/testUtils.py +++ /dev/null @@ -1,157 +0,0 @@ -from struct import Struct -from functools import partial -from collections import namedtuple -from confUtils import Validate -import new -import signal -import subprocess -import logging -from select import select -from threading import Thread, Event -import re -import os -import pwd -import time - -def _makeFromStream(ntClass, struct, cls, stream): - size = struct.size - buf = stream.read(size) - if len(buf) < size: - raise RuntimeError("Stream is not long enough") - - return _makeFromBuffer(ntClass, struct, cls, buf) - -def _makeFromBuffer(ntClass, struct, cls, buffer): - return ntClass._make(struct.unpack(buffer)) - -def aligneStruct(struct, blockSize=512): - return Struct("%s%dx" % (struct.format, (blockSize - (struct.size % blockSize)))) - -dblockStruct = aligneStruct(Struct("QQQQ")) -DBlock = namedtuple("DBlock", "mbal bal inp lver") - -DBlock.fromStream = new.instancemethod(partial(_makeFromStream, DBlock, dblockStruct), DBlock, DBlock.__class__) -DBlock.fromBuffer = new.instancemethod(partial(_makeFromBuffer, DBlock, dblockStruct), DBlock, DBlock.__class__) - -leaderRecordStruct = aligneStruct(Struct("III4xQQQQ32sQI4x")) -LeaderRecord = namedtuple('LeaderRecord', 'magic version clusterMode numHosts maxHosts ownerID lver resourceID timestamp checksum') - -LeaderRecord.fromStream = new.instancemethod(partial(_makeFromStream, LeaderRecord, leaderRecordStruct), LeaderRecord, LeaderRecord.__class__) -LeaderRecord.fromBuffer = new.instancemethod(partial(_makeFromBuffer, LeaderRecord, leaderRecordStruct), LeaderRecord, LeaderRecord.__class__) - -def leasesValidator(value): - rawLeases = Validate.list(value) - leases = [] - for lease in rawLeases: - parts = lease.split(":") - resourceID = parts[0] - disks = [] - for i in range(1, len(parts), 2): - disks.append((parts[i], int(parts[i + 1]))) - - leases.append((resourceID, tuple(disks))) - - return tuple(leases) - -getResources = lambda leases : [resource for resource, disks in leases] - -nullTerminated = lambda str : str[:str.find("\0")] - -def readState(stream, numOfHosts = 0): - lrSize = leaderRecordStruct.size - leader = LeaderRecord.fromStream(stream) - - if numOfHosts < 1: - numOfHosts = leader.numHosts - - dblockSize = dblockStruct.size - totalSize = dblockSize * numOfHosts - - buf = stream.read(totalSize) - - if len(buf) < totalSize: - raise RuntimeError("Stream is not long enough") - - dblocks = [] - for start in range(0, totalSize, dblockSize): - minibuf = buf[start: (start + dblockSize)] - dblocks.append(DBlock.fromBuffer(minibuf)) - - return (leader, tuple(dblocks)) - - -#DUMMY_CMD = ["/usr/bin/sudo", "-u", pwd.getpwuid(os.geteuid())[0], os.path.abspath("./dummy.py")] -DUMMY_CMD = [os.path.abspath("./dummy.py")] - -class Dummy(object): - _log = logging.getLogger("Dummy"); - _pidRegex = re.compile(r".*supervise_pid\s+(\d+).*") - def __init__(self, name, hostID = -1, leases = []): - cmd = ["sudo", "-n", "../sync_manager", "daemon", "-D", "-n", name, "-i", str(hostID)] - cmd.extend(self._compileLeaseArgs(leases)) - cmd.append("-c") - cmd.extend(DUMMY_CMD) - self._log.debug("CMD: %s" % subprocess.list2cmdline(cmd)) - self.process = subprocess.Popen(cmd, - stdin = subprocess.PIPE, - stdout = subprocess.PIPE, - stderr = subprocess.PIPE - ) - self._wrappedPid = 0 - self._pidStarted = Event() - self._logThread = Thread(target = self._logOutputThread) - self._logThread.start() - self._pidStarted.wait() - #Wait for dummy to set up - time.sleep(1) - if self._wrappedPid == 0: - raise Exception("Probelm running dummy") - - def _logOutputThread(self): - while self.process.poll() is None: - readyObjects = select([self.process.stdout, self.process.stderr], [], [], 1)[0] - for obj in readyObjects: - line = obj.readline().replace("\n", "") - if line == "": - continue - if self._wrappedPid == 0: - m = self._pidRegex.match(line) - if m: - self._wrappedPid = int(m.groups()[0]) - self._pidStarted.set() - self._log.debug("Daemon - %s" % line) - - self._pidStarted.set() - - def _compileLeaseArgs(self, leases): - args = [] - for lease, disks in leases: - mangledDisks = ["%s:%d" % (os.path.abspath(disk), offset) for (disk, offset) in disks] - args.extend(["-l", "%s:%s" % (lease, ":".join(mangledDisks))]) - - return args - - def stop(self): - if not self.process.poll() is None: - return - self._log.debug("Stopping dummy") - os.kill(self._wrappedPid, signal.SIGUSR1) - - try: - self.process.wait() - except OSError, ex: - if ex.errno != 10: - raise - self._logThread.join() - - def __del__(self): - self.stop() - -if __name__ == "__main__": - with open("drive.img" , "rb") as f: - t = LeaderRecord.fromStream(f) - print t.tokenName - - with open("drive.img" , "rb") as f: - l = readState(f, 200) - print len(l)