def __load_macro_instance(MacroModule, MacroFactoryFunction, args=None): ''' Dynamically call a factory function of a python module The factory function should return an instance of a local class of this module. :param MacroModule: python module from the macro library :param MacroFactoryFunction: function we want to call to get the macro object :param args: String arguments passed to the MacroFactoryFunction ''' instance = None # the arguments of the MacroFactoryFunction can be a grinder placeholder if args: # New: default value management props = loadProps() args = CachedMacros.__manageDefaultValue(args, props) # First try, ignore any substitution error (safe_substitute does the job) args = ExtendedTemplate(args).safe_substitute(props) # Still a template variable ? if args.find('${') >= 0: try: # Any not substituted key raise an error (paranoid mode) args = ExtendedTemplate(args).substitute(props) except KeyError, e: logger.error( '__load_macro_instance() - KeyError raise, reason: %s' % str(e)) raise SyntaxError( '__load_macro_instance() - KeyError raise, reason: %s' % str(e))
def __maySubstituteTemplate(cls, param, templateDict): if (cls.placeholderPattern.matcher(param)).find(): try: return (ExtendedTemplate(param).substitute(templateDict)) except KeyError: raise SyntaxError('Unable to find "%s" in the context' % (param)) return param
def getNextScenario(self): ''' Switch to the next scenario. Localization is done by scenario and indexScenario attributes TODO: check if self.stored is still used ... ''' self.indexScenario += 1 self.macros = None # Check we don't reach the last scenario if self.indexScenario >= self.scenarioListSize: logger.debug( 'getNextScenario() - End Of Scenario - indexScenario=%s, Number of scenario=%d' % (self.indexScenario, self.scenarioListSize)) self.scenario = None self.indexScenario = -1 return # An Unique Identifier aimed at giving an identification of the UC self.uid = '%01d%02d%03d%05d%03d' % ( grinder.agentNumber, Configuration.processNumber, grinder.threadNumber, grinder.runNumber, self.indexScenario) MDC.put('uid', '[uid=%s]' % (self.uid)) # Manage current scenario variables self.scenario = self.scenarioList[self.indexScenario] self.scenarioSize = len(self.scenario.lines) if self.macrosAllScenario: if self.indexScenario in self.macrosAllScenario: self.macros = self.macrosAllScenario[self.indexScenario] # get immutable information from scenario object self.__initial_context = self.scenario.getContextDefinition() logger.debug('getNextScenario() - [scenario:%s] [initialContext:%s] ' % (self.scenario, self.__initial_context)) self.stored = dict(self.__initial_context) self.indexLine = -1 # Update the Context cache for the new scenario self.cacheKeys.reset() # add the unique identifier for implementation relying on a static session cache self.cacheKeys.set('grindertool.transactionId', self.uid) # Add all the context evaluated keys if len(self.__initial_context) > 0: if logger.isTraceEnabled(): logger.trace( '-- Begin context evaluation [context="%s"][size=%d]' % (self, len(self.__initial_context))) # Evaluate macros in context temp_dict = dict(self.__initial_context) # TODO : recursive nbRetryLoop = 4 for i in range(1, nbRetryLoop): needSubstitutionCount = 0 for k, v in dict(temp_dict).iteritems(): if logger.isTraceEnabled(): logger.trace('[loop=%d][key=%s][value=%s]' % (i, k, v)) v = ExtendedTemplate(v).safe_substitute( self.cacheKeys.get()) (stillPlaceholder, newValue) = self.__substitute_in_value(v) if logger.isTraceEnabled(): logger.trace( '.........[key=%s][value=%s][stillPlaceholder=%s][returnValue=%s]' % (k, v, stillPlaceholder, newValue)) needSubstitutionCount += int(stillPlaceholder) temp_dict[k] = newValue if not stillPlaceholder and newValue: if type(newValue) is dict: for key, val in newValue.iteritems(): if logger.isTraceEnabled(): logger.trace( 'Adding key "%s.%s" value %s to scenario context' % (k, key, val)) self.cacheKeys.set('%s.%s' % (k, key), val) else: self.cacheKeys.set(k, newValue) if logger.isTraceEnabled(): logger.trace( 'Removing key from temporary context: %s' % (k)) temp_dict.pop(k) if not needSubstitutionCount: if logger.isTraceEnabled(): logger.trace( 'No more substitution needed [Exit at loop %d/%d]' % (i, nbRetryLoop)) break # We define the context cache keys self.cacheKeys.setContextMarker() if logger.isTraceEnabled(): logger.trace('-- End context evaluation --')
def processNew(self): ''' Process a ***template**** line with dynamic substitutions and memorization ''' # To be sure that we won't have some side effect with fields and memory cache self.cacheKeys.resetToContextMarker() if logger.isTraceEnabled(): logger.trace('>>> processNew(): cachekeys: %s' % (self.cacheKeys.dictRuntime)) #======================= # Necessary to have a thread local copy of immutable line object #========================== localFields = copy.deepcopy(self.line.fieldsNew) # Update the cacheKeys with <<<literal values>>> (values without function and without template ${template} ) subsDict = {} for elem in localFields.getLiterral(): subsDict[elem.name] = elem.value logger.trace('********** literal field:%s' % (elem)) self.cacheKeys.update_keys(subsDict) # Then substitute all <<<template strings>>> found in the fields to allow function execution for elem in localFields.getTemplate(): elem.setValue( ExtendedTemplate(elem.value).safe_substitute( self.cacheKeys.get())) # Dynamic function call in the field itself is marked for the whole line (optimization) if localFields.isFunction: logger.debug('processLine() - Dynamic fields substitution') for elem in localFields.getFunction(): m = (GlobalPattern.dynFieldPattern).matcher(elem.value) while m.find(): (module, method, param) = (m.group(1), m.group(2), m.group(3) or None) if logger.isTraceEnabled(): logger.trace('%s[module=%s][method=%s][param=%s]' % (elem, module, method, param)) try: valueToReplace = getattr( self.macros[module], method)(param) if param else getattr( self.macros[module], method)() if logger.isDebugEnabled(): logger.debug('valueToReplace=%s' % (valueToReplace)) if not valueToReplace: logger.error( 'valueToReplace is null. REVIEW YOUR TEST DEFINITION. Context is [module=%s][method=%s][param=%s]' % (module, method, param)) raise SyntaxError( 'valueToReplace is null. REVIEW YOUR TEST DEFINITION. Context is [module=%s][method=%s][param=%s]' % (module, method, param)) except KeyError: logger.error( 'FATAL: Macro call "%s.%s(%s)" is not defined in your scenario "%s" macros section' % (module, method, param, self.scenario.getName())) raise SyntaxError( 'FATAL: Macro call "%s.%s(%s)" is not defined in your scenario "%s" macros section' % (module, method, param, self.scenario.getName())) except Exception, x: if not self.macros: logger.error( 'FATAL: your forget to define the macros section in your scenario "%s"' % (self.scenario.getName())) raise SyntaxError( 'FATAL: your forget to define the macros section in your scenario "%s"' % (self.scenario.getName())) else: logger.error( 'FATAL: when calling macro: %s, cause=%s' % (elem, x)) raise SyntaxError( 'FATAL: when calling macro: %s, cause=%s' % (elem, x)) elem.setValue(m.replaceFirst(valueToReplace)) m = (GlobalPattern.dynFieldPattern).matcher(elem.value)
class Context: ''' A context maintains the : - Yaml context execution value between RUN - all the cached values during a scenario lifecycle Remark: keys are ** unknown ** - they are pushed in the context environment and reused in the scenario ** coherence of keys ** are the sole responsibility of the tester ''' def __init__(self, macrosAllScenario): # =========== IMMUTABLE attributes ================= # we reverse because we will use a pop() to traverse the scenario self.scenarioList = Configuration.getScenarioList() self.scenarioListSize = len(self.scenarioList) self.macrosAllScenario = macrosAllScenario # self.templateManager = Configuration.cmdMgr # =========== MUTABLE attributes ================= self.uid = None self.macros = None # expiration time = infinite by default self.expirationTime = -1 self.indexScenario = -1 self.scenario = None self.line = None self.indexLine = -1 self.scenarioSize = 0 # For async hardening self.locked = False self.flagged = False self.contextKey = None self.contextValue = None # A flag to indicates that the scenario must be continued on resume # Bug in Rev373 - the default value was True self.scenarioContinue = False # The initial context is the "meta" definition of the context self.__initial_context = None # This is the living context with substitution self.stored = None # A thread context cache self.cacheKeys = CacheKey() def copy(self, __ctx): ''' ATTENTION: must copy all the mutable attributes (see above) :param __ctx: the context object to copy ''' self.uid = __ctx.uid self.macros = __ctx.macros self.expirationTime = __ctx.expirationTime self.indexScenario = __ctx.indexScenario self.scenario = __ctx.scenario # take care ... Line must be immutable ! self.line = __ctx.line self.indexLine = __ctx.indexLine self.scenarioSize = __ctx.scenarioSize # self.contextKey = __ctx.contextKey self.contextValue = __ctx.contextValue self.scenarioContinue = __ctx.scenarioContinue # The initial context is the "meta" definition of the context self.__initial_context = __ctx.__initial_context # This is the living context with substitution self.stored = __ctx.stored # A thread context cache self.cacheKeys = copy.deepcopy(__ctx.cacheKeys) ''' #### BEGIN - applicative locks used for asynchronous processing ''' def unlock(self): self.locked = False def setLock(self): self.locked = True def isLocked(self): return self.locked def setFlag(self): self.flagged = True def isFlagged(self): return self.flagged ''' #### END - applicative locks used for asynchronous processing ''' def __repr__(self): s = '' s += '\n<%s.%s object at %s>\n' % ( self.__class__.__module__, self.__class__.__name__, hex(id(self))) s += 'scenario: %s\n' % (hex(id(self.scenario)) if self.scenario else 'None') s += 'line(%d): %s\n' % (self.indexLine, hex(id(self.line)) if self.line else 'None') s += 'scenarioList(%d): %s\n' % (self.indexScenario, hex(id(self.scenarioList)) if self.scenarioList else 'None') return s def getIndexScenario(self): return self.indexScenario def getNextScenario(self): ''' Switch to the next scenario. Localization is done by scenario and indexScenario attributes TODO: check if self.stored is still used ... ''' self.indexScenario += 1 self.macros = None # Check we don't reach the last scenario if self.indexScenario >= self.scenarioListSize: logger.debug( 'getNextScenario() - End Of Scenario - indexScenario=%s, Number of scenario=%d' % (self.indexScenario, self.scenarioListSize)) self.scenario = None self.indexScenario = -1 return # An Unique Identifier aimed at giving an identification of the UC self.uid = '%01d%02d%03d%05d%03d' % ( grinder.agentNumber, Configuration.processNumber, grinder.threadNumber, grinder.runNumber, self.indexScenario) MDC.put('uid', '[uid=%s]' % (self.uid)) # Manage current scenario variables self.scenario = self.scenarioList[self.indexScenario] self.scenarioSize = len(self.scenario.lines) if self.macrosAllScenario: if self.indexScenario in self.macrosAllScenario: self.macros = self.macrosAllScenario[self.indexScenario] # get immutable information from scenario object self.__initial_context = self.scenario.getContextDefinition() logger.debug('getNextScenario() - [scenario:%s] [initialContext:%s] ' % (self.scenario, self.__initial_context)) self.stored = dict(self.__initial_context) self.indexLine = -1 # Update the Context cache for the new scenario self.cacheKeys.reset() # add the unique identifier for implementation relying on a static session cache self.cacheKeys.set('grindertool.transactionId', self.uid) # Add all the context evaluated keys if len(self.__initial_context) > 0: if logger.isTraceEnabled(): logger.trace( '-- Begin context evaluation [context="%s"][size=%d]' % (self, len(self.__initial_context))) # Evaluate macros in context temp_dict = dict(self.__initial_context) # TODO : recursive nbRetryLoop = 4 for i in range(1, nbRetryLoop): needSubstitutionCount = 0 for k, v in dict(temp_dict).iteritems(): if logger.isTraceEnabled(): logger.trace('[loop=%d][key=%s][value=%s]' % (i, k, v)) v = ExtendedTemplate(v).safe_substitute( self.cacheKeys.get()) (stillPlaceholder, newValue) = self.__substitute_in_value(v) if logger.isTraceEnabled(): logger.trace( '.........[key=%s][value=%s][stillPlaceholder=%s][returnValue=%s]' % (k, v, stillPlaceholder, newValue)) needSubstitutionCount += int(stillPlaceholder) temp_dict[k] = newValue if not stillPlaceholder and newValue: if type(newValue) is dict: for key, val in newValue.iteritems(): if logger.isTraceEnabled(): logger.trace( 'Adding key "%s.%s" value %s to scenario context' % (k, key, val)) self.cacheKeys.set('%s.%s' % (k, key), val) else: self.cacheKeys.set(k, newValue) if logger.isTraceEnabled(): logger.trace( 'Removing key from temporary context: %s' % (k)) temp_dict.pop(k) if not needSubstitutionCount: if logger.isTraceEnabled(): logger.trace( 'No more substitution needed [Exit at loop %d/%d]' % (i, nbRetryLoop)) break # We define the context cache keys self.cacheKeys.setContextMarker() if logger.isTraceEnabled(): logger.trace('-- End context evaluation --') def getScenario(self, asyncCallback=False): return self.scenario if asyncCallback else self.getNextScenario() def setExpirationTime(self, expirationTime): ''' Expiration is set when executing at runtime an async step :param expirationTime: a future date in second for expiration including the timeout initialized in Line class ''' self.expirationTime = expirationTime def getCacheKeys(self): return self.cacheKeys def getNextLine(self): ''' go to the scenario next line (step) of it exists ''' self.indexLine += 1 if self.indexLine >= self.scenarioSize: # VIRTUALRUN END # self.indexLine = -1 self.line = None return False self.line = self.scenario.lines[self.indexLine] # To avoid side effect if self.line.async_step: self.line.contextValue = self.line.initialContextValue if logger.isTraceEnabled(): logger.trace( '[ASYNC] restoring initialContextValue definition: %s' % (self.line.contextValue)) return True def getCurrentLines(self): return self.scenario.lines def getCurrentLineIndex(self): return self.indexLine def getCurrentLine(self): return self.line def endAsyncScenarioTimeout(self): ''' Case where we consider a timeout as a normal behavior timeout_success: True in the "async" tag default ( timeout_success: False ) ''' # Special case : timeout EXPIRED is a success if self.line.isTimeoutSuccess(): logger.trace( 'endAsyncScenarioTimeout - considering a timeout as a SUCCESS') self.scenarioContinue = True GrinderQueue.put(self) return # # Nominal UC: timeout is a failure. # ome,29/02: removed the last parameter of report_step_status to raise the flag error in the grindertool GUI # ome, 10/07/17: removing last parameter was not a good idea as it raise a "InvalidContextException" # reporting.report_step_status(success=False, context=self, cause='Timeout error triggered', state='stepTimeoutKO', synchronous=False) # reporting.report_step_status(success=False, context=self, cause='Timeout error triggered', state='stepTimeoutKO') self.endAsyncScenario() def endAsyncScenario(self): # There is another scenario behind, we increase the scenario index and put it in the queue if self.indexScenario < (self.scenarioListSize - 1): logger.debug( 'endAsyncScenario: case indexScenario[%d] < number of scenario minus one[%d]' % (self.indexScenario, self.scenarioListSize - 1)) # We take care to remove any promised async call of the stopped scenario for i, line in enumerate(self.scenario.lines[self.indexLine:]): # ignore the current expired one if i == 0 and line.isAsynchronous(): continue if line.isAsynchronous() and line.asyncBlocking(): logger.trace( 'endAsyncScenario: adding extra AbortRunToken for scenario[%d]:%s' % (self.indexScenario, self.scenario.getName())) GrinderQueue.put(AbortRunToken()) logger.trace( 'endAsyncScenario: adding context in the GrinderQueue()') GrinderQueue.put(self) return # There is no more scenario logger.trace( 'endAsyncScenario: Last scenario - Adding AbortRuntoken when removing context' ) loggerProxy.end_run('end run') [ GrinderQueue.put(AbortRunToken()) for line in self.scenario.lines[self.indexLine:] if line.isAsynchronous() and line.asyncBlocking() ] def processNew(self): ''' Process a ***template**** line with dynamic substitutions and memorization ''' # To be sure that we won't have some side effect with fields and memory cache self.cacheKeys.resetToContextMarker() if logger.isTraceEnabled(): logger.trace('>>> processNew(): cachekeys: %s' % (self.cacheKeys.dictRuntime)) #======================= # Necessary to have a thread local copy of immutable line object #========================== localFields = copy.deepcopy(self.line.fieldsNew) # Update the cacheKeys with <<<literal values>>> (values without function and without template ${template} ) subsDict = {} for elem in localFields.getLiterral(): subsDict[elem.name] = elem.value logger.trace('********** literal field:%s' % (elem)) self.cacheKeys.update_keys(subsDict) # Then substitute all <<<template strings>>> found in the fields to allow function execution for elem in localFields.getTemplate(): elem.setValue( ExtendedTemplate(elem.value).safe_substitute( self.cacheKeys.get())) # Dynamic function call in the field itself is marked for the whole line (optimization) if localFields.isFunction: logger.debug('processLine() - Dynamic fields substitution') for elem in localFields.getFunction(): m = (GlobalPattern.dynFieldPattern).matcher(elem.value) while m.find(): (module, method, param) = (m.group(1), m.group(2), m.group(3) or None) if logger.isTraceEnabled(): logger.trace('%s[module=%s][method=%s][param=%s]' % (elem, module, method, param)) try: valueToReplace = getattr( self.macros[module], method)(param) if param else getattr( self.macros[module], method)() if logger.isDebugEnabled(): logger.debug('valueToReplace=%s' % (valueToReplace)) if not valueToReplace: logger.error( 'valueToReplace is null. REVIEW YOUR TEST DEFINITION. Context is [module=%s][method=%s][param=%s]' % (module, method, param)) raise SyntaxError( 'valueToReplace is null. REVIEW YOUR TEST DEFINITION. Context is [module=%s][method=%s][param=%s]' % (module, method, param)) except KeyError: logger.error( 'FATAL: Macro call "%s.%s(%s)" is not defined in your scenario "%s" macros section' % (module, method, param, self.scenario.getName())) raise SyntaxError( 'FATAL: Macro call "%s.%s(%s)" is not defined in your scenario "%s" macros section' % (module, method, param, self.scenario.getName())) except Exception, x: if not self.macros: logger.error( 'FATAL: your forget to define the macros section in your scenario "%s"' % (self.scenario.getName())) raise SyntaxError( 'FATAL: your forget to define the macros section in your scenario "%s"' % (self.scenario.getName())) else: logger.error( 'FATAL: when calling macro: %s, cause=%s' % (elem, x)) raise SyntaxError( 'FATAL: when calling macro: %s, cause=%s' % (elem, x)) elem.setValue(m.replaceFirst(valueToReplace)) m = (GlobalPattern.dynFieldPattern).matcher(elem.value) # Add all the line fields in the cacheKey cache subsDict = {} for elem in localFields.getAllFields(): subsDict[elem.name] = elem.value self.cacheKeys.update_keys(subsDict) str2Use = None if self.line.template: # template could be dynamic (depending on context) template = self.line.getDynamicTemplate( self.cacheKeys.get(), self.macros) if self.line.dynTemplate else self.line.template # return the cached template (or store it the first time) command = self.templateManager.get_or_load(self.line.template_type, template, self.macros) # the common case is to have Yaml format if command.getStr(): str2Use = ExtendedTemplate(command.getStr()).safe_substitute( self.cacheKeys.get()) logger.debug('[Yaml Template after substitution:%s]' % (str2Use)) return str2Use # No template, return just the fields with substitution applied str2Use = localFields.update(self.cacheKeys.get()) logger.debug('[NoTemplate] str2Use=%s' % (str2Use)) return str2Use