def parse_configuration(node, base_config=None): """ Parse input config to configuration information """ test_config = base_config if not test_config: test_config = TestSetConfig() node = lowercase_keys(flatten_dictionaries(node)) # Make it usable if "testset" in node: test_config.testset_name = node['testset'] # ahead process variable_binds, other key can use it in template if not test_config.variable_binds: test_config.variable_binds = dict() if 'variable_binds' in node: value = node['variable_binds'] test_config.variable_binds.update(flatten_dictionaries(value)) # set default_base_url to global variable_binds if 'default_base_url' in node: value = node['default_base_url'] default_base_url = templated_var(value, test_config.variable_binds) test_config.variable_binds['default_base_url'] = default_base_url for key, value in node.items(): if key == 'timeout': test_config.timeout = int(value) elif key == 'retries': test_config.retries = int(value) elif key == 'collect_import_result': if isinstance(value, str): value = True if value.lower() == 'true' else False test_config.collect_import_result = value elif key == 'variable_binds': pass elif key == 'request_client': test_config.request_client = str(value) elif key == 'generators': flat = flatten_dictionaries(value) gen_map = dict() for generator_name, generator_config in flat.items(): gen = parse_generator( configuration=generator_config, variable_binds={ **test_config.variable_binds, 'working_directory': test_config.working_directory }) gen_map[str(generator_name)] = gen test_config.generators = gen_map if 'data_driven' in node: value = node['data_driven'] generator_name = value.get('generator', None) generator_obj = test_config.generators[generator_name] test_config.data_driven_generator = generator_obj test_config.data_driven_generator_name = generator_name return test_config
def parse(config): """ Create a validator that does an extract from body and applies a comparator, Then does comparison vs expected value Syntax sample: { jsonpath_mini: 'node.child', operator: 'eq', expected: 'myValue' } """ output = ComparatorValidator() config = parsing.lowercase_keys(parsing.flatten_dictionaries(config)) output.config = config # Extract functions are called by using defined extractor names output.extractor = _get_extractor(config) if output.extractor is None: raise ValueError( "Extract function for comparison is not valid or not found!") if 'comparator' not in config: # Equals comparator if unspecified output.comparator_name = 'eq' else: output.comparator_name = config['comparator'].lower() output.comparator = COMPARATORS[output.comparator_name] if not output.comparator: raise ValueError("Invalid comparator given!") try: expected = config['expected'] except KeyError: raise ValueError( "No expected value found in comparator validator config, one must be!" ) # Expected value can be another extractor query, or a single value, or # a templated value if isinstance(expected, str) or isinstance( expected, (int, long, float, complex)): output.expected = expected elif isinstance(expected, dict): expected = parsing.lowercase_keys(expected) template = expected.get('template') if template: # Templated string if not isinstance(template, str): raise ValueError( "Can't template a comparator-validator unless template value is a string" ) output.expected = template else: # Extractor to compare against output.expected = _get_extractor(expected) if not output.expected: raise ValueError( "Can't supply a non-template, non-extract dictionary to comparator-validator" ) return output
def parse(config): output = ExtractTestValidator() config = parsing.lowercase_keys(parsing.flatten_dictionaries(config)) output.config = config extractor = _get_extractor(config) output.extractor = extractor test_name = config['test'] output.test_name = test_name test_fn = VALIDATOR_TESTS[test_name] output.test_fn = test_fn return output
def parse_generator(configuration, variable_binds=None): """ Parses a configuration built from yaml and returns a generator Configuration should be a map """ configuration = lowercase_keys(flatten_dictionaries(configuration)) gen_type = str(configuration.get('type')).lower() if gen_type not in GENERATOR_TYPES: raise ValueError( 'Generator type given {0} is not valid '.format(gen_type)) # Do the easy parsing, delegate more complex logic to parsing functions if gen_type == 'env_variable': return factory_env_variable(configuration['variable_name'])() elif gen_type == 'env_string': return factory_env_string(configuration['string'])() elif gen_type == 'number_sequence': start = configuration.get('start') increment = configuration.get('increment') if not start: start = 1 else: start = int(start) if not increment: increment = 1 else: increment = int(increment) return factory_generate_ids(start, increment)() elif gen_type == 'random_int': return generator_random_int32() elif gen_type == 'random_text': return parse_random_text_generator(configuration) elif gen_type in GENERATOR_TYPES: return GENERATOR_PARSING[gen_type](configuration, variable_binds) else: raise Exception("Unknown generator type: {0}".format('gen_type'))
def parse_from_dict(cls, node, input_test=None, test_path=None): """ Create or modify a test, input_test, using configuration in node, and base_url If no input_test is given, creates a new one Test_path gives path to test file, used for setting working directory in setting up input bodies Uses explicitly specified elements from the test input structure to make life *extra* fun, we need to handle list <-- > dict transformations. This is to say: list(dict(),dict()) or dict(key,value) --> dict() for some elements Accepted structure must be a single dictionary of key-value pairs for test configuration """ mytest = input_test if not mytest: mytest = HttpTest() # Clean up for easy parsing node = lowercase_keys(flatten_dictionaries(node)) # Simple table of variable name, coerce function, and optionally special store function CONFIG_ELEMENTS = { # Simple variables u'auth_username': [coerce_string_to_ascii], u'auth_password': [coerce_string_to_ascii], u'method': [coerce_http_method], # HTTP METHOD u'delay': [lambda x: int(x)], # Delay before running u'group': [coerce_to_string], # Test group name u'name': [coerce_to_string], # Test name u'expected_status': [coerce_list_of_ints], u'stop_on_failure': [safe_to_bool], # Templated / special handling # u'body': [ContentHandler.parse_content] # COMPLEX PARSE OPTIONS # u'extract_binds':[], # Context variable-to-extractor output binding # u'variable_binds': [], # Context variable to value binding # u'generator_binds': [], # Context variable to generator output binding # u'validators': [], # Validation functions to run } def use_config_parser(configobject, configelement, configvalue): """ Try to use parser bindings to find an option for parsing and storing config element :configobject: Object to store configuration :configelement: Configuratione element name :configvalue: Value to use to set configuration :returns: True if found match for config element, False if didn't """ myparsing = CONFIG_ELEMENTS.get(configelement) if myparsing: converted = myparsing[0](configvalue) setattr(configobject, configelement, converted) return True return False # Copy/convert input elements into appropriate form for a test object for configelement, configvalue in node.items(): if use_config_parser(mytest, configelement, configvalue): continue # Configure test using configuration elements if configelement == 'url': if isinstance(configvalue, dict): configvalue = configvalue.get("template") mytest.url = coerce_to_string(configvalue) elif configelement == 'body': if isinstance(configvalue, dict): configvalue = configvalue.get("template") mytest.http_body = configvalue elif configelement == 'extract_binds': # Add a list of extractors, of format: # {variable_name: {extractor_type: extractor_config}, ... } binds = flatten_dictionaries(configvalue) if mytest.extract_binds is None: mytest.extract_binds = dict() for variable_name, extractor in binds.items(): if not isinstance(extractor, dict) or len( extractor) == 0: raise TypeError( "Extractors must be defined as maps of extractorType:{configs} with 1 entry") if len(extractor) > 1: raise ValueError( "Cannot define multiple extractors for given variable name") # Safe because length can only be 1 for extractor_type, extractor_config in extractor.items(): mytest.extract_binds[ variable_name] = validators.parse_extractor( extractor_type, extractor_config) elif configelement == 'validators': # Add a list of validators if not isinstance(configvalue, list): raise Exception( 'Misconfigured validator section, must be a list of validators') if mytest.validators is None: mytest.validators = list() # create validator and add to list of validators for var in configvalue: if not isinstance(var, dict): raise TypeError( "Validators must be defined as validatorType:{configs} ") for validator_type, validator_config in var.items(): validator = validators.parse_validator( validator_type, validator_config) mytest.validators.append(validator) elif configelement == 'headers': # HTTP headers to use, flattened to a single string-string dictionary configvalue = flatten_dictionaries(configvalue) if isinstance(configvalue, dict): mytest.headers = configvalue elif isinstance(configvalue, str): try: mytest.headers = json.loads(configvalue) except Exception as e: logger.error(str(e)) raise Exception("header must be dict or json str") else: raise TypeError( "Illegal header type: headers must be a dictionary or list of dictionary keys") elif configelement == 'variable_binds': mytest.variable_binds = flatten_dictionaries(configvalue) elif configelement == 'generator_binds': output = flatten_dictionaries(configvalue) output2 = dict() for key, value in output.items(): output2[str(key)] = str(value) mytest.generator_binds = output2 # For non-GET requests, accept additional response codes indicating success # (but only if not expected statuses are not explicitly specified) # this is per HTTP spec: # http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.5 if 'expected_status' not in node.keys(): if mytest.method == 'POST': mytest.expected_status = [200, 201, 204] elif mytest.method == 'PUT': mytest.expected_status = [200, 201, 204] elif mytest.method == 'DELETE': mytest.expected_status = [200, 202, 204] # Fallthrough default is simply [200] return mytest
def parse_testsets(test_structure, test_files=set(), working_directory=None): """ Convert a Python data structure read from validated YAML to a set of structured testsets The data structure is assumed to be a list of dictionaries, each of which describes: - a tests (test structure) - a simple test (just a URL, and a minimal test is created) - or overall test configuration for this testset - an import (load another set of tests into this one, from a separate file) - For imports, these are recursive, and will use the parent config if none is present Note: test_files is used to track tests that import other tests, to avoid recursive loops This returns a list of testsets, corresponding to imported testsets and in-line multi-document sets """ tests_list = list() test_config = TestSetConfig() testsets = list() if working_directory is None: working_directory = os.path.abspath(os.getcwd()) test_config.working_directory = working_directory # returns a testconfig and collection of testsets assert isinstance(test_structure, list) testset = TestSet() index = 0 while index < len(test_structure): # Iterate through lists of test and configuration elements node = test_structure[index] if isinstance(node, dict): # Each config element is a miniature key-value dictionary node = lowercase_keys(node) for key in node: if key == 'include': includefile = node[key] # include another file as module/modules if includefile[0] != "/": includefile = os.path.join(working_directory, includefile) # load include module file and delete its config, add its test/operation to local flow subnodes = read_test_file(includefile) assert isinstance(subnodes, list) i = index + 1 for sn in subnodes: keys = sn.keys() assert len(keys) == 1 if list(keys)[0] in ['config']: continue test_structure.insert(i, sn) i += 1 if key == 'import': # import another testset file if isinstance(node[key], dict): importfile = node[key]['file'] input = node[key].get('input') extract = node[key].get('extract') elif isinstance(node[key], str): importfile = node[key] input = None extract = None else: raise Exception("Wrong Import Format: {}".format(node[key])) if importfile[0] != "/": importfile = os.path.join(working_directory, importfile) if importfile not in test_files: logger.info("Importing test sets: " + importfile) test_files.add(importfile) import_test_structure = read_test_file(importfile) with CD(os.path.dirname( os.path.realpath(importfile))): try: import_testsets = parse_testsets( import_test_structure, test_files) except Exception as e: error_info = "Import SubTestSet {} ERROR, msg: {}".format(importfile, str(e)) logger.error(error_info) raise Exception(error_info) assert len(import_testsets) == 1 import_testsets[0].config.extract = extract testset.subtestsets[importfile] = import_testsets[0] subtestset = TestSet() subtestset.input = input subtestset.extract = extract subtestset.file_path = importfile tests_list.append(subtestset) # call sub testset is also a test step # elif key == 'url': # Simple test, just a GET to a URL # mytest = HttpTest() # val = node[key] # assert isinstance(val, str) # mytest.url = val # tests_list.append(mytest) elif key == 'test': # Complex test with additional parameters with CD(working_directory): child = node[key] test_type = child.get('test_type', 'http_test') # mytest = HttpTest.parse_from_dict(child) runner_parser_func = get_test_runner_parser(test_type) mytest = runner_parser_func(child) mytest.original_node = child tests_list.append(mytest) elif key == 'operation': # Complex test with additional parameters operation = Operation() operation.config = flatten_dictionaries(node[key]) tests_list.append(operation) elif key == 'config' or key == 'configuration': test_config = parse_configuration( node[key], base_config=test_config) index += 1 testset.tests = tests_list testset.config = test_config testset.name = test_config.testset_name for t in tests_list: t.testset_config = test_config testsets.append(testset) return testsets