Beispiel #1
0
def parse_test_file(test_root, test_path):
    ''' Parse a Python file to find test cases '''
    logger = LogManager().getLogger('parse_test_file')
    test_root = os.path.abspath(test_root)
    test_path = os.path.abspath(test_path)
    python_path = test_path[len(test_root)+1:-3].replace(os.path.sep, '.')
    file_name = python_path.split('.')[-1]

    if test_root not in sys.path:
        sys.path.insert(0, test_root)
    old_path = os.getcwd()
    os.chdir(test_root)
    reload_modules(test_root)

    try:
        module = __import__(python_path, globals(),
            locals(), fromlist=[file_name])
        return find_test_methods(module)
    except:
        logger.warn("Could not load test %s, \n%s"
            %(test_path, traceback.format_exc()))
    finally:
        os.chdir(old_path)

    return []
Beispiel #2
0
class RecoveryWorker(Process):
    def __init__(self, peer, rebootAttempts = 1, removeLock = False):
        self.peer = peer
        self.removeLock = removeLock
        self.stages = [self.__rebootSSH, self.__rebootPWR] * rebootAttempts
        self.logger = LogManager().getLogger('RecoveryWorker-%s' % peer.ipAddr)
        Process.__init__(self, target = self.__restartPeer)
        self.start()

    def __restartPeer(self):
        """
        Reboot a given peer
        @param peer: Peer to reboot
        @return: Process.exitcode will be 0 if successful else -99
        """
        for stage in self.stages:
            try: stage()
            except KeyError, e:
                self.logger.warn("Ignoring stage %s, invalid config: %s"
                    % (stage.__name__, e))

            time.sleep(10)
        sys.exit(-99)
Beispiel #3
0
class Peer(object):
    HEARTBEAT_TIMEOUT = 300
    DEATH_TIMEOUT = 600
    TEST_TIMEOUT = 60
    TEST_GRACE = 3

    DEAD_STATES = [PeerState.UNRESPONSIVE(), PeerState.DEAD()]
    DONE_STATES = [PeerState.ACTIVE(), PeerState.DEAD()]
    REBOOT_STATES = [PeerState.REBOOTING(), PeerState.DEAD()]
    LOCKED_STATES = [PeerState.LOCKED(), PeerState.PENDING()]
    REQUIRED_CONF = ['user', 'password', 'tmpdir', 'envserver', 'envserverport', 'rebootcmd']

    IGNORED_PEER_EVENT = get_event_handler('ignored_peer')
    NEW_PEER_EVENT = get_event_handler('new_peer')
    PEER_STATE_CHANGE_EVENT = get_event_handler('peer_state_change')
    PEER_STAGE_CHANGE_EVENT = get_event_handler('peer_stage_change')
    TEST_RESULT_EVENT = get_event_handler('test_result')
    PEER_SLEEPING_EVENT = get_event_handler('peer_sleeping')

    @staticmethod
    def createPeer(ipAddr, port, macAddr, randomBits, testDistributor, resultWorker):
        config = ConfigurationManager().getConfiguration('routes').configuration
        routes = config.route if isinstance(config.route, list) else [config.route]

        for route in routes:
            if route.macAddr.PCDATA != macAddr or route.enabled == 'false':
                continue
            return Peer(ipAddr, port, macAddr, randomBits, testDistributor, resultWorker)
        Peer.IGNORED_PEER_EVENT(ipAddr, port, macAddr, randomBits)

    def __init__(self, ipAddr, port, macAddr, randomBits, testDistributor, resultWorker):
        self.STAGES = [
            self.__retrieveConfigFile,
            self.__processConfigFile,
            self.__checkForLockedBox,
            self.__initRebootPeer,
            self.__syncCode,
            self.__executeTest,
            self.__archiveLogFiles,
            self.__retrieveLogFiles,
            self.__defineResults,
            self.__reportResults,
            self.__reaction,
            self.__gracePeriod,
            self.__postRebootPeer,
        ]

        self.recoverIndex = self.STAGES.index(self.__archiveLogFiles)
        self.graceIndex = self.STAGES.index(self.__gracePeriod)
        self.STAG_LEN = len(self.STAGES)

        self.testDistributor = testDistributor
        self.resultWorker = resultWorker

        self.config = {}
        self.ipAddr = ipAddr
        self.macAddr = macAddr
        self.randomBits = randomBits
        self.capabilities = ''
        self.gracePeriod = self.recoveries = 0
        self.timeJoined = time.time()
        self.envServer = ''
        self.envServerPort = 0

        resConf = ConfigurationManager().getConfiguration('resultWorker').configuration

        self.customLogFilters = resConf.customLogFilters.customLogFilter \
            if hasattr(resConf.customLogFilters, 'customLogFilter') else []
        self.customLogFilters = self.customLogFilters  \
            if isinstance(self.customLogFilters, list) else [self.customLogFilters]

        self.logger = LogManager().getLogger('Peer-%s' % self.macAddr)
        self.hostIP = getIPAddressByInterface()
        self.masterHTTPLoc = 'http://%s:%s/' % (self.hostIP, port)
        self.httpLoc = 'http://%s:5005/' % ipAddr
        self.tmpDir = os.environ['TAS_TMP']
        self.peerDir = '%s/peer-%s' % (self.tmpDir, self.macAddr)
        self.resultLoc = '%s/results' % self.peerDir

        if not os.path.exists(self.resultLoc):
            os.makedirs(self.resultLoc)

        execution = ConfigurationManager()\
            .getConfiguration('execution').configuration
        self.initReboot = execution.rebootNewBoxes.PCDATA == 'true'
        self.postReboot = execution.rebootPerTest.PCDATA == 'true'
        self.state = PeerState.ACTIVE()
        self.__changeState(PeerState.PENDING())
        self.stage = 0

        self.lastHeartBeat = time.time()
        self.currentTest = self.testTimeout = None
        self.failureCodes = []
        self.processResults = []
        self.longRunningProcesses = []

        Peer.NEW_PEER_EVENT(self)

    def __retrieveConfigFile(self):
        """ Retrieve the configuration file from the peer """
        if not os.path.exists(self.peerDir):
            os.makedirs(self.peerDir)

        self.longRunningProcesses.append(syncGetHTTPFile(
            '%sconfig' % self.httpLoc, self.peerDir))

    def __processConfigFile(self):
        """ Process the configuration file as a key/value pair """
        configLoc = '%s/config' % self.peerDir
        if not os.path.exists(configLoc):
            self.logger.warn('Could not find a configuration file')
            return self.__changeState(PeerState.DEAD())

        with open(configLoc, 'r') as lf:
            for line in lf.readlines():
                key, value = line.strip().split('=', 1)
                self.config[key.lower()] = value

        self.envServer = self.config['envserver'].strip()
        self.envServerPort = self.config['envserverport'].strip()
        if self.envServerPort.isdigit():
            self.envServerPort = int(self.envServerPort)

        if self.config['rebootcmd'] == '':
            self.postReboot = self.initReboot = False

        self.capabilities = yates.Utils.Envcat.capabilities(
            self.envServer, self.envServerPort)

        self.log_index = yates.Utils.Envcat.latest_id(
            self.envServer, self.envServerPort)

        if self.__configIsMissingKeys():
            return self.__changeState(PeerState.DEAD(), 'Invalid config')

        cmd = 'mkdir -p %(T)s; cd %(T)s; if [ ! -f locked ]; then echo "%(I)s" > locked; sleep 2; fi; ' \
              'if [ "%(I)s" != `cat locked` ]; then echo `cat locked`; else echo "UNLOCKED"; fi;' \
              % {'T': self.config['tmpdir'], 'I': self.hostIP}

        self.longRunningProcesses.append(SSHClient(
            self.config['user'], self.config['password'],
            self.ipAddr, cmd))

    def __checkForLockedBox(self):
        """ Check for a locked box """
        output = self.processResults[0].updateOutput().strip()

        if 'UNLOCKED' in output:
            self.lastHeartBeat = time.time()
        elif not self.testDistributor.peekTest(self):
            self.stage = 0
            self.__changeState(PeerState.ACTIVE())
        else:
            self.gracePeriod = 5
            self.stage = self.graceIndex - 1
            self.__changeState(PeerState.LOCKED(),
                               'current user %s' % output)

    def __initRebootPeer(self):
        """ Pre testing reboot if required """
        if not self.initReboot:
            return False
        self.initReboot = False

        self.__changeState(PeerState.REBOOTING())
        self.longRunningProcesses.append(RecoveryWorker(self))

    def __syncCode(self):
        """ Sync the source code the the peer """
        self.status = self.__changeState(PeerState.SYNC_CODE())
        cmd = 'mkdir -p %(T)s; for x in `ls %(T)s | grep -vE "(config|locked|scripts.%(J)s.tar.gz)"`; do rm -rf %(T)s/$x; done; ' \
              'if [ ! -f %(T)s/scripts.%(J)s.tar.gz ]; then wget %(H)sscripts.tar.gz -O %(T)s/scripts.%(J)s.tar.gz -q; fi; ' \
              'tar -xzf %(T)s/scripts.%(J)s.tar.gz -C %(T)s' % {'T': self.config['tmpdir'], 'J': self.timeJoined, 'H': self.masterHTTPLoc}

        self.longRunningProcesses.append(SSHClient(
            self.config['user'], self.config['password'],
            self.ipAddr, cmd))

    def __executeTest(self):
        """ Execute a test script on the peer """
        test = self.testDistributor.getTest(self)
        if test is None:
            self.stage = 0
            self.longRunningProcesses.append(self.__unlockBox())
            return self.__changeState(PeerState.ACTIVE())

        test.testTimeout += Peer.TEST_GRACE
        self.__changeState(PeerState.TESTING(), 'while running test %s' % test.testId)
        self.currentTest = test

        cmd = 'cd %s; bash -x %s >execution.log 2>&1' % (self.config['tmpdir'], self.currentTest.getExecutionString())
        self.longRunningProcesses.append(SSHClient(
            self.config['user'], self.config['password'],
            self.ipAddr, cmd))

        self.currentTest.startTime = time.time()

    def __archiveLogFiles(self):
        """ Archive logs and cleanup on the peer """
        endTime = time.time()
        startTime = self.currentTest.startTime
        if len(self.processResults) > 0:
            endTime = self.processResults[0].endTime.value
            startTime = self.processResults[0].startTime.value
        self.currentTest.duration = endTime - startTime
        self.__changeState(PeerState.SYNC_LOGS())

        cmd = ''
        for customLogFilter in self.customLogFilters:
            cmd += 'cp -r %s %s/logs; ' % (customLogFilter.PCDATA, self.config['tmpdir'])
        cmd += 'cd %s/logs; tar -czf ../logs.tar.gz *;' % self.config['tmpdir']

        self.longRunningProcesses.append(SSHClient(
            self.config['user'], self.config['password'],
            self.ipAddr, cmd))

    def __retrieveLogFiles(self):
        """ Retrieve the log files from the peer """
        self.lastResultLoc = "%s-%s-%s" % (self.resultLoc, time.time(), self.currentTest.testId)
        os.makedirs(self.lastResultLoc)

        self.longRunningProcesses.append(syncGetHTTPFile(
            '%slogs.tar.gz' % self.httpLoc, self.lastResultLoc, True))

        self.longRunningProcesses.append(yates.Utils.Envcat.store_log(
            self.envServer, self.envServerPort,
            '%s/envlog.txt' % self.lastResultLoc, self.log_index))

    def __defineResults(self):
        """ Define the result for the test result """
        self.lastResultFiles = [os.path.join(self.lastResultLoc, fileName)
                                for fileName in os.listdir(self.lastResultLoc)]

        self.longRunningProcesses.append(ResultDefiner(
            self.lastResultFiles, self.currentTest))

    def __reportResults(self):
        """ From the log files define a result """
        testState, errorMsg = self.processResults[0].getResult()
        self.currentTest.state = testState
        self.currentTest.error = errorMsg
        Peer.TEST_RESULT_EVENT(self.currentTest, self.lastResultFiles, self)
        #TODO: replace me, self.resultWorker.report(self.currentTest, self.lastResultFiles, self)

        self.longRunningProcesses.append(
            ReactionDefiner(self.lastResultFiles, self.currentTest))

    def __reaction(self):
        """ React appon the current test results """
        peerState, gracePeriod = self.processResults[0].getResult()
        self.__changeState(peerState)
        self.gracePeriod = gracePeriod
        self.currentTest = None

    def __gracePeriod(self):
        """ Allow for a grace period """
        if self.gracePeriod == 0:
            return False

        Peer.PEER_SLEEPING_EVENT(self)
        self.longRunningProcesses.append(Popen(
            'sleep %d' % self.gracePeriod, shell=True))
        self.gracePeriod = 0

    def __postRebootPeer(self):
        """ Pre testing reboot if required """
        if not self.postReboot or self.state in Peer.LOCKED_STATES:
            return False

        self.initReboot = False
        self.__changeState(PeerState.REBOOTING())
        self.longRunningProcesses.append(RecoveryWorker(
            self, removeLock=True))

    def __changeState(self, state, comment=None):
        """ Change the peer state and report """
        if self.state == state or not state:
            return

        self.state = state
        Peer.PEER_STATE_CHANGE_EVENT(self, comment)

    def __hasDied(self, heartBeatFelt):
        """
        States if the peer hasn't been sending heartbeats.
        The state will be set to either DEAD or UNRESPONSIVE
        depending on how much time has passed.
        @return True if heartbeat timeout has not occured else false
        """
        now = time.time()
        timePassed = now - self.lastHeartBeat

        if timePassed > Peer.DEATH_TIMEOUT and \
                self.state != PeerState.DEAD():
            self.__changeState(PeerState.DEAD())

        elif timePassed > Peer.HEARTBEAT_TIMEOUT and \
                self.state not in Peer.DEAD_STATES:
            self.__changeState(PeerState.UNRESPONSIVE(), 'within stage %d' % self.stage)

        else:
            if heartBeatFelt:
                self.lastHeartBeat = time.time()
            return False

        return True

    def checkState(self, heartBeatFelt):
        """
        Check the overall state
        @param heartBeat: True if a heart was felt else False
        @return: Result, State. Either can be None if they have not changed
        """
        self.failureCodes = [
            p for p in self.longRunningProcesses
            if self.__getExitCode(p) not in [None, 0]
            and not isinstance(p, RecoveryWorker)
        ] + self.failureCodes

        self.processResults = [
            p for p in self.longRunningProcesses
            if not self.__isAlive(p)
            and not isinstance(p, RecoveryWorker)
        ] + self.processResults

        self.longRunningProcesses = [
            p for p in self.longRunningProcesses
            if self.__isAlive(p)
        ]

        # Box doesn't belong to us yet. Try again
        if len(self.failureCodes) > 0 and self.state == PeerState.PENDING():
            self.gracePeriod = 5
            self.stage = self.graceIndex
            self.__clearProcessHistory()

        if self.state not in Peer.LOCKED_STATES:
            hasDied = self.__hasDied(heartBeatFelt)
            if len(self.longRunningProcesses) > 0:
                return

            if len(self.failureCodes) > 0 or hasDied:
                hasConfig = not any(x not in self.config.keys() for x in Peer.REQUIRED_CONF)
                if self.state not in Peer.DEAD_STATES:
                    objNames = ','.join([x.__class__.__name__ for x in self.failureCodes])
                    funcName = self.STAGES[self.stage].im_func.func_name
                    self.__changeState(
                        PeerState.UNRESPONSIVE(),
                        'on stage %s, %s' % (funcName, objNames))
                if hasConfig and self.recoveries < 5:
                    self.longRunningProcesses.append(RecoveryWorker(self))
                    self.recoveries += 1
                return

            if self.state == PeerState.TESTING() \
                    and time.time() - self.currentTest.startTime > self.currentTest.testTimeout \
                    and self.currentTest.state != TestState.TIMEOUT():
                self.currentTest.state = TestState.TIMEOUT()
                for process in self.longRunningProcesses + self.processResults:
                    if hasattr(process, 'shutdown'):
                        process.shutdown()

        if len(self.longRunningProcesses) > 0 or \
                self.state in Peer.REBOOT_STATES + Peer.DONE_STATES:
            # TODO: timeout?
            return  # Still running processes

        if self.STAGES[self.stage]() is False:
           # Go to the next stage if asked to skip
            self.__incStage()
            self.checkState(heartBeatFelt)
        else:
            self.recoveries = 0
            self.__incStage()
            self.__clearProcessHistory()

    def __incStage(self):
        """ Increment the current stage """
        if self.state == PeerState.ACTIVE():
            return

        self.stage = (self.stage + 1) * (self.stage + 1 < self.STAG_LEN)
        Peer.PEER_STAGE_CHANGE_EVENT(self)

    def __getExitCode(self, process):
        """
        Returns the exit code from a process
        @param process: Popen or Process object
        @return: exit code of process
        """
        if isinstance(process, Popen):
            return process.poll()
        return process.exitcode

    def __isAlive(self, process):
        """
        Returns true if the process or Popen is
        still executing code else false
        @param process: Process or Popen
        @return True if executing else false
        """
        if isinstance(process, Popen):
            return process.poll() is None
        return process.is_alive()

    def __killAllProcesses(self):
        """ Kill all running processes """
        for process in self.longRunningProcesses:
            try:
                process.terminate()
            except OSError:
                pass

        self.longRunningProcesses = []
        self.__clearProcessHistory()

    def __clearProcessHistory(self):
        """ Clear all process history """
        processes = self.failureCodes + self.processResults
        for process in processes:
            if not hasattr(process, 'cleanup'):
                continue
            process.cleanup()

        self.failureCodes = []
        self.processResults = []

    def checkIP(self, ipAddr, randomBits):
        """
        Check if the IP address has changed
        @param ipAddr: New IP address
        """
        # IP address has changed
        if ipAddr != self.ipAddr:
            # TODO: restart SSH ? What does this mean? Recovery?
            self.logger.warn('IP address for peer %s has changed to %s from %s!'
                             % (self.macAddr, ipAddr, self.ipAddr))

        # Box rebooted randomly
        elif randomBits != self.randomBits and self.currentTest:
            self.logger.warn(
                'Peer (%s,%s) has rebooted! Random bits have changed: %s - %s'
                % (self.ipAddr, self.macAddr, self.randomBits, randomBits))

            self.__changeState(PeerState.RECOVER_LOGS())
            self.__killAllProcesses()
            self.stage = self.recoverIndex
            self.currentTest.state = TestState.CRASH()
            self.lastHeartBeat = time.time()

        # Expected and controlled rebooted
        if self.randomBits != randomBits and self.state in Peer.REBOOT_STATES:
            self.__changeState(PeerState.PENDING())

        self.ipAddr = ipAddr
        self.randomBits = randomBits

    def __configIsMissingKeys(self):
        """ Configuration is missing keys """
        return any(k not in self.config.keys() for k in Peer.REQUIRED_CONF)

    def __unlockBox(self):
        """ Remove the lock from the box and reboot """
        if self.__configIsMissingKeys():
            return

        return SSHClient(
            self.config['user'], self.config['password'], self.ipAddr,
            'if [ "`cat %(T)s/locked`" == "%(H)s" ]; then touch %(T)s/unlock; %(R)s; fi'
            % {'H': self.hostIP, 'T': self.config['tmpdir'], 'R': self.config['rebootcmd']},
            timeout=2)

    def shutdown(self):
        """ Shutdown all operations that are related to this peer """
        self.__killAllProcesses()
        if self.currentTest and self.state == PeerState.DEAD():
            self.currentTest.state = TestState.DEADBOX()
            self.resultWorker.report(self.currentTest, [], self)
            self.currentTest = None

        processes = [yates.Utils.Envcat.stop_all(self.envServer, self.envServerPort)]

        if self.state not in Peer.LOCKED_STATES:
            processes.append(self.__unlockBox())
        return processes

    def isDone(self):
        """
        State if the peer has finished all required work
        @return: True if all work has been completed else false
        """
        return self.state in Peer.DONE_STATES
Beispiel #4
0
class TestDiscoveryWorker(object):
    CONFIG_ENABLED_TRUE = 'true'

    def __init__(self, config, defaultTimeout, iterations, srcLoc):
        self.test = 0
        self.config = config
        self.logger = LogManager().getLogger('TestDiscoveryWorker')

        self.enabled = self.config.DiscoveryWorker.enabled == 'true'
        self.testRoot = os.path.abspath(self.config.SourceLocation.testRoot.PCDATA)
        self.testPath = self.config.SourceLocation.testPath.PCDATA or ""
        self.defaultTestTimeout = defaultTimeout
        self.iterations = iterations
        self.srcLoc = srcLoc

    def createTests(self):
        '''
        Method creates the Source object, default group and the list
        of tests. If the discovery worker is disabled, then the
        empty list of tests is stored in the Source object
        @return: the source object with the list of tests
        '''
        if not self.enabled:
            return []

        tests = []
        output_Q, input_Q = Queue(), Queue()
        processes, alive, work_avail = [], 0, True
        fullPath = os.path.join(self.testRoot, self.testPath)

        for path in find_python_files(fullPath):
            input_Q.put(path, block=False)

        while work_avail or alive > 0 or not output_Q.empty():
            time.sleep(0.1)

            work_avail = not input_Q.empty()
            alive = [p.is_alive() for p in processes].count(True)
            if alive == cpu_count() or not work_avail:
                while not output_Q.empty():
                    tests.append(output_Q.get())
                continue

            process = Process(
                target=self.__processTest,
                args=(self.testRoot, output_Q, input_Q))
            process.daemon = True
            process.start()
            processes.append(process)

        input_Q.close()
        output_Q.close()
        return tests

    def __processTest(self, test_root, output_Q, input_Q):
        ''' Convert a given file location into test objects '''
        os.chdir(test_root)
        if test_root not in sys.path:
            sys.path.insert(0, test_root)

        while not input_Q.empty():
            file_path = input_Q.get(timeout=0.5)
            test_methods = parse_test_file(test_root, file_path)

            for cls, method in test_methods:
                raw_doc = gather_doc_str(cls, method)
                doc_dict = process_docstr(raw_doc)

                try:
                    newTest = self.__createTestCase(
                        cls, method, file_path, raw_doc,
                        doc_dict, self.iterations)
                    if newTest:
                        output_Q.put(newTest)
                except:
                    self.logger.warn(
                        "Cannot create object from %s, \n%s\n%s\n\n"
                        % (file_path, doc_dict, traceback.format_exc()))

        output_Q.close()
        input_Q.close()

    def __createTestCase(self, cls, method, test_file, raw_doc, doc_dict, iterations):
        description = doc_dict.get('summary', '')
        testId = doc_dict.get('test', '')
        environment = doc_dict.get('environment', '')
        testStatus = doc_dict.get('status', '')
        testTimeout = doc_dict.get('timeout', self.defaultTestTimeout)

        # Check whether the test is function, or it is within class
        testFile = test_file.replace('%s/' % os.getcwd(), '')
        test_method = method.im_func.func_name if cls else method.func_name
        test_class = cls.__name__ if cls else None

        missing_data = []
        manditory_data = {'test method': test_method,
                          'test file': testFile,
                          'test id': testId}

        for key in manditory_data.keys():
            if manditory_data[key] in ['', None]:
                missing_data.append(key)

        if len(missing_data) > 0:
            raise Exception('Cannot create test for %s:%s.%s as %s is missing'
                % (testFile, test_class, test_method, ', '.join(missing_data)))

        return PythonNoseTest(
            description, iterations, environment, test_class,
            testMethod=test_method, testFile=testFile,
            testId=testId, testStatus=testStatus,
            testTimeout=testTimeout, docstrings=raw_doc,
            srcLoc=self.srcLoc)
class AbstractDistributor(object):
    def __init__(self, source):
        self.execMap = {}
        self.logger = LogManager().getLogger(self.__class__.__name__)

    def recordExecution(self, peer, test):
        """
        Add the test to the map, which contains the history of
        the test distribution to the peers
        """
        if not peer.macAddr in self.execMap.keys():
            self.execMap[peer.macAddr] = []
        self.execMap[peer.macAddr].append(test)

    def assignSuitableTest(self, peer, peers, source):
        """
        Method, which finds the suitable test for the given peer
        @param peer: the peer, which is active and awaits the test
        @param souce: the source object with groups of tests
        @return: found test and modified source
        """
        for gi in range(0, len(source.groups)):
            for ti in range(0, len(source.groups[gi].tests)):
                test = source.groups[gi].tests[ti]

                if not self.useTest(peer, peers, source, source.groups[gi], test, True):
                    continue

                self.recordExecution(peer, test)
                return copy.deepcopy(test), source

        self.logger.warn("Could not find any test for peer %s" % peer.ipAddr)
        return None, source

    def peekSuitableTest(self, peer, peers, source):
        """
        Method, which finds the suitable test for the given peer
        @param peer: the peer, which is active and awaits the test
        @param souce: the source object with groups of tests
        @return: found test and modified source
        """
        for gi in range(0, len(source.groups)):
            for ti in range(0, len(source.groups[gi].tests)):
                test = source.groups[gi].tests[ti]

                if not self.useTest(peer, peers, source, source.groups[gi], test, False):
                    continue
                return copy.deepcopy(test)

        return None

    def isSuitable(self, has, requires):
        """
        Method copied from envutils/caps.py module
        @param requires: modulators required by test
        @param has: modulators provided by the box
        @return: True or False depending on the box capability
        """
        has = [set(c.split("/")) for c in has.split()]
        requires = [set((r,)) for r in requires.split()]
        if len(has) < len(requires):
            return False

        return any(all(h >= r for r, h in izip(requires, perm)) for perm in permutations(has, len(requires)))

    def whatsMissing(self, has, requires):
        has = [set(c.split("/")) for c in has.split()]
        requires = [set((r,)) for r in requires.split()]

        for r in requires[:]:
            for h in has:
                if not any(x in h for x in r):
                    continue

                has.remove(h)
                requires.remove(r)
                break

        strBuffer = StringIO()
        for r in requires:
            strBuffer.write(" %s" % "/".join(list(r)))

        result = strBuffer.getvalue()
        strBuffer.close()
        return result

    def useTest(self, peer, peers, source, group, test, alterSource):
        raise NotImplementedError()

    def executed(self, peers, test):
        raise NotImplementedError()