From dc7a4b69bca8be4fe1068d7bc7d5cd94430a86d5 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Sat, 1 Jul 2017 21:40:46 +0800 Subject: [PATCH] refactor context: 1, testset and testcase are in different context level; 2, testset context will be initialized when a file is loaded, and testcase level context initializes when each testcase starts; 3, testcase context should inherit from testset context configs, and the testcase context has high priority. --- ate/context.py | 129 +++++++++++++++++++++++++------ ate/main.py | 2 +- ate/runner.py | 45 +++++------ test/data/demo_binds.yml | 14 +--- test/data/demo_template_sets.yml | 2 +- test/test_context.py | 89 +++++++++------------ 6 files changed, 164 insertions(+), 117 deletions(-) diff --git a/ate/context.py b/ate/context.py index aeef98db..977af7a0 100644 --- a/ate/context.py +++ b/ate/context.py @@ -1,23 +1,45 @@ +import copy import importlib import re import types +from collections import OrderedDict -from ate import exception, utils +from ate import exception, testcase, utils def is_function(tup): - """ - Takes (name, object) tuple, returns True if it is a function. + """ Takes (name, object) tuple, returns True if it is a function. """ name, item = tup return isinstance(item, types.FunctionType) class Context(object): - """ Manages binding of variables + """ Manages context functions and variables. + context has two levels, testset and testcase. """ def __init__(self): - self.functions = dict() - self.variables = dict() # Maps variable name to value + self.testset_config = {} + self.testset_shared_variables_mapping = dict() + + self.testcase_config = {} + self.testcase_variables_mapping = dict() + self.init_context() + + def init_context(self, level='testset'): + """ + testset level context initializes when a file is loaded, + testcase level context initializes when each testcase starts. + """ + if level == "testset": + self.testset_config["functions"] = {} + self.testset_config["variables"] = OrderedDict() + self.testset_config["request"] = {} + self.testset_shared_variables_mapping = {} + + self.testcase_config["functions"] = {} + self.testcase_config["variables"] = OrderedDict() + self.testcase_config["request"] = {} + self.testcase_variables_mapping = copy.deepcopy(self.testset_shared_variables_mapping) def import_requires(self, modules): """ import required modules dynamicly @@ -25,54 +47,111 @@ class Context(object): for module_name in modules: globals()[module_name] = importlib.import_module(module_name) - def bind_functions(self, function_binds): + def bind_functions(self, function_binds, level="testcase"): """ Bind named functions within the context This allows for passing in self-defined functions in testing. e.g. function_binds: { - "add_one": lambda x: x + 1, - "add_two_nums": "lambda x, y: x + y" + "add_one": lambda x: x + 1, # lambda function + "add_two_nums": "lambda x, y: x + y" # lambda function in string } """ + eval_function_binds = {} for func_name, function in function_binds.items(): if isinstance(function, str): function = eval(function) - self.functions[func_name] = function + eval_function_binds[func_name] = function - def import_module_functions(self, modules): + self.__update_context_config(level, "functions", eval_function_binds) + + def import_module_functions(self, modules, level="testcase"): """ import modules and bind all functions within the context """ for module_name in modules: imported = importlib.import_module(module_name) imported_functions_dict = dict(filter(is_function, vars(imported).items())) - self.functions.update(imported_functions_dict) + self.__update_context_config(level, "functions", imported_functions_dict) - def bind_variables(self, variable_binds): - """ Bind named variables to value within the context. - This allows for passing in variables or functions. - e.g. variable_binds: + def register_variables_config(self, variable_binds, level="testcase"): + """ register variable configs + @param (list) variable_binds, variable can be value or custom function + e.g. [ {"TOKEN": "debugtalk"}, {"random": {"func": "gen_random_string", "args": [5]}}, {"json": {'name': 'user', 'password': '123456'}}, - {"md5": {"func": "gen_md5", "args": ["$TOKEN", "$json", "$random"]}} + {"md5": {"func": "gen_md5", "args": ["${TOKEN}", "${json}", "${random}"]}} ] """ - for variable_bind_map in variable_binds: - for var_name, var_value in variable_bind_map.items(): - self.variables[var_name] = self.get_eval_value(var_value) + if level == "testset": + for variable_bind in variable_binds: + self.testset_config["variables"].update(variable_bind) + elif level == "testcase": + self.testcase_config["variables"] = copy.deepcopy(self.testset_config["variables"]) + for variable_bind in variable_binds: + self.testcase_config["variables"].update(variable_bind) - def update_variables(self, variables_mapping): - """ update context variables binds with new variables mapping + def register_request(self, request_dict, level="testcase"): + self.__update_context_config(level, "request", request_dict) + + def __update_context_config(self, level, config_type, config_mapping): """ - self.variables.update(variables_mapping) + @param level: testset or testcase + @param config_type: functions, variables or request + @param config_mapping: functions config mapping or variables config mapping + """ + if level == "testset": + self.testset_config[config_type].update(config_mapping) + elif level == "testcase": + self.testcase_config[config_type].update(config_mapping) + + def get_parsed_request(self): + """ get parsed request, with each variable replaced by bind value. + testcase request shall inherit from testset request configs, + but can not change testset configs, that's why we use copy.deepcopy here. + """ + testcase_request_config = copy.deepcopy(self.testset_config["request"]) + testcase_request_config.update(self.testcase_config["request"]) + + parsed_request = testcase.parse_template( + testcase_request_config, + self._get_evaluated_testcase_variables() + ) + + return parsed_request + + def bind_extracted_variables(self, variables_mapping): + """ bind extracted variable to current testcase context and testset context. + since extracted variable maybe used in current testcase and next testcases. + """ + self.testset_shared_variables_mapping.update(variables_mapping) + self.testcase_variables_mapping.update(variables_mapping) + + def get_testcase_variables_mapping(self): + return self.testcase_variables_mapping + + def _get_evaluated_testcase_variables(self): + """ variables in variables_config will be evaluated each time + """ + testcase_functions_config = copy.deepcopy(self.testset_config["functions"]) + testcase_functions_config.update(self.testcase_config["functions"]) + self.testcase_config["functions"] = testcase_functions_config + + testcase_variables_config = copy.deepcopy(self.testset_config["variables"]) + testcase_variables_config.update(self.testcase_config["variables"]) + self.testcase_config["variables"] = testcase_variables_config + + for var_name, var_value in self.testcase_config["variables"].items(): + self.testcase_variables_mapping[var_name] = self.get_eval_value(var_value) + + return self.testcase_variables_mapping def get_eval_value(self, data): """ evaluate data recursively, each variable in data will be evaluated. variables marker: ${variable}. """ if isinstance(data, str): - return utils.parse_content_with_variables(data, self.variables) + return utils.parse_content_with_variables(data, self.testcase_variables_mapping) if isinstance(data, list): return [self.get_eval_value(item) for item in data] @@ -85,7 +164,7 @@ class Context(object): func_name = data['func'] args = self.get_eval_value(data.get('args', [])) kargs = self.get_eval_value(data.get('kargs', {})) - return self.functions[func_name](*args, **kargs) + return self.testcase_config["functions"][func_name](*args, **kargs) else: evaluated_data = {} for key, value in data.items(): diff --git a/ate/main.py b/ate/main.py index 47626a8f..c2f1a123 100644 --- a/ate/main.py +++ b/ate/main.py @@ -26,7 +26,7 @@ def create_suite(testset): test_runner = runner.TestRunner() config_dict = testset.get("config", {}) - test_runner.update_context(config_dict, level="testset") + test_runner.init_context(config_dict, level="testset") testcases = testset.get("testcases", []) for testcase in testcases: diff --git a/ate/runner.py b/ate/runner.py index 730fa04e..08d6a003 100644 --- a/ate/runner.py +++ b/ate/runner.py @@ -1,9 +1,7 @@ -import copy import requests from ate import exception, response from ate.context import Context -from ate.testcase import parse_template class TestRunner(object): @@ -11,9 +9,8 @@ class TestRunner(object): def __init__(self): self.client = requests.Session() self.context = Context() - self.testset_req_overall_configs = {} - def update_context(self, config_dict, level="testcase"): + def init_context(self, config_dict, level): """ create/update context variables binds @param (dict) config_dict { @@ -27,28 +24,28 @@ class TestRunner(object): "lambda *str_args: hashlib.md5(''.join(str_args).\ encode('utf-8')).hexdigest()" }, + "import_module_functions": ["test.data.custom_functions"], "variable_binds": [ {"TOKEN": "debugtalk"}, {"random": {"func": "gen_random_string", "args": [5]}}, ] } @param (str) context level, testcase or testset - only when level is testset, shall we update testset_req_overall_configs """ requires = config_dict.get('requires', []) self.context.import_requires(requires) function_binds = config_dict.get('function_binds', {}) - self.context.bind_functions(function_binds) + self.context.bind_functions(function_binds, level) module_functions = config_dict.get('import_module_functions', []) - self.context.import_module_functions(module_functions) + self.context.import_module_functions(module_functions, level) variable_binds = config_dict.get('variable_binds', []) - self.context.bind_variables(variable_binds) + self.context.register_variables_config(variable_binds, level) - if level == "testset": - self.testset_req_overall_configs = config_dict.get('request', {}) + request_config = config_dict.get('request', {}) + self.context.register_request(request_config, level) def run_test(self, testcase): """ run single testcase. @@ -68,21 +65,14 @@ class TestRunner(object): }, "body": '{"name": "user", "password": "123456"}' }, - "extract_binds": {}, - "validators": [] + "extract_binds": {}, # optional + "validators": [] # optional } @return (tuple) test result of single testcase (success, diff_content_list) """ - self.update_context(testcase) - - # each testcase shall inherit from testset request configs, - # but can not override testset configs, - # that's why we use copy.deepcopy here. - testcase_request = copy.deepcopy(self.testset_req_overall_configs) - testcase_request.update(testcase["request"]) - - parsed_request = parse_template(testcase_request, self.context.variables) + self.init_context(testcase, level="testcase") + parsed_request = self.context.get_parsed_request() try: url = parsed_request.pop('url') method = parsed_request.pop('method') @@ -100,10 +90,11 @@ class TestRunner(object): extract_binds = testcase.get("extract_binds", {}) extracted_variables_mapping = resp_obj.extract_response(extract_binds) - self.context.update_variables(extracted_variables_mapping) + self.context.bind_extracted_variables(extracted_variables_mapping) validators = testcase.get("validators", []) - diff_content_list = resp_obj.validate(validators, self.context.variables) + diff_content_list = resp_obj.validate( + validators, self.context.get_testcase_variables_mapping()) return resp_obj.success, diff_content_list @@ -122,10 +113,10 @@ class TestRunner(object): "testcases": [ { "name": "testcase description", - "variable_binds": {}, # override + "variable_binds": {}, # optional, override "request": {}, - "extract_binds": {}, - "validators": {} + "extract_binds": {}, # optional + "validators": {} # optional }, testcase12 ] @@ -139,7 +130,7 @@ class TestRunner(object): results = [] config_dict = testset.get("config", {}) - self.update_context(config_dict) + self.init_context(config_dict, level="testset") testcases = testset.get("testcases", []) for testcase in testcases: result = self.run_test(testcase) diff --git a/test/data/demo_binds.yml b/test/data/demo_binds.yml index 0ac48942..d11d7621 100644 --- a/test/data/demo_binds.yml +++ b/test/data/demo_binds.yml @@ -1,21 +1,15 @@ -- +register_variables: variable_binds: - TOKEN: "debugtalk" - -- - variable_binds: - var: [1, 2, 3] - -- - variable_binds: - data: {'name': 'user', 'password': '123456'} -- +register_template_variables: variable_binds: - TOKEN: "debugtalk" - token: ${TOKEN} -- +bind_lambda_functions: function_binds: add_one: "lambda x: x + 1" add_two_nums: "lambda x, y: x + y" @@ -23,7 +17,7 @@ - add1: {"func": "add_one", "args": [2]} - sum2nums: {"func": "add_two_nums", "args": [2, 3]} -- +bind_lambda_functions_with_import: requires: - random - string diff --git a/test/data/demo_template_sets.yml b/test/data/demo_template_sets.yml index 1036e7bd..92f8427d 100644 --- a/test/data/demo_template_sets.yml +++ b/test/data/demo_template_sets.yml @@ -9,8 +9,8 @@ gen_md5: "lambda *str_args: hashlib.md5(''.join(str_args).encode('utf-8')).hexdigest()" variable_binds: - TOKEN: debugtalk + - data: "" - random: {"func": "gen_random_string", "args": [5]} - - data: '{"name": "user", "password": "123456"}' - authorization: {"func": "gen_md5", "args": ["${TOKEN}", "${data}", "${random}"]} - test: diff --git a/test/test_context.py b/test/test_context.py index fe6cbf40..d27d648d 100644 --- a/test/test_context.py +++ b/test/test_context.py @@ -12,79 +12,53 @@ class VariableBindsUnittest(unittest.TestCase): testcase_file_path = os.path.join(os.getcwd(), 'test/data/demo_binds.yml') self.testcases = utils.load_testcases(testcase_file_path) - def test_context_variable_string(self): + def test_context_register_variables(self): # testcase in JSON format testcase1 = { "variable_binds": [ - {"TOKEN": "debugtalk"} - ] - } - # testcase in YAML format - testcase2 = self.testcases[0] - - for testcase in [testcase1, testcase2]: - variable_binds = testcase['variable_binds'] - self.context.bind_variables(variable_binds) - - context_variables = self.context.variables - self.assertIn("TOKEN", context_variables) - self.assertEqual(context_variables["TOKEN"], "debugtalk") - - def test_context_variable_list(self): - testcase1 = { - "variable_binds": [ - {"var": [1, 2, 3]} - ] - } - testcase2 = self.testcases[1] - - for testcase in [testcase1, testcase2]: - variable_binds = testcase['variable_binds'] - self.context.bind_variables(variable_binds) - - context_variables = self.context.variables - self.assertIn("var", context_variables) - self.assertEqual(context_variables["var"], [1, 2, 3]) - - def test_context_variable_json(self): - testcase1 = { - "variable_binds": [ + {"TOKEN": "debugtalk"}, + {"var": [1, 2, 3]}, {"data": {'name': 'user', 'password': '123456'}} ] } - testcase2 = self.testcases[2] + # testcase in YAML format + testcase2 = self.testcases["register_variables"] for testcase in [testcase1, testcase2]: variable_binds = testcase['variable_binds'] - self.context.bind_variables(variable_binds) + self.context.register_variables_config(variable_binds) - context_variables = self.context.variables + context_variables = self.context._get_evaluated_testcase_variables() + self.assertIn("TOKEN", context_variables) + self.assertEqual(context_variables["TOKEN"], "debugtalk") + self.assertIn("var", context_variables) + self.assertEqual(context_variables["var"], [1, 2, 3]) self.assertIn("data", context_variables) self.assertEqual( context_variables["data"], {'name': 'user', 'password': '123456'} ) - def test_context_variable_variable(self): + def test_context_register_template_variables(self): testcase1 = { "variable_binds": [ {"GLOBAL_TOKEN": "debugtalk"}, {"token": "${GLOBAL_TOKEN}"} ] } - testcase2 = self.testcases[3] + testcase2 = self.testcases["register_template_variables"] for testcase in [testcase1, testcase2]: variable_binds = testcase['variable_binds'] - self.context.bind_variables(variable_binds) + self.context.register_variables_config(variable_binds) - context_variables = self.context.variables + context_variables = self.context._get_evaluated_testcase_variables() self.assertIn("GLOBAL_TOKEN", context_variables) self.assertEqual(context_variables["GLOBAL_TOKEN"], "debugtalk") self.assertIn("token", context_variables) self.assertEqual(context_variables["token"], "debugtalk") - def test_context_variable_function_lambda(self): + def test_context_bind_lambda_functions(self): testcase1 = { "function_binds": { "add_one": lambda x: x + 1, @@ -95,22 +69,22 @@ class VariableBindsUnittest(unittest.TestCase): {"sum2nums": {"func": "add_two_nums", "args": [2, 3]}} ] } - testcase2 = self.testcases[4] + testcase2 = self.testcases["bind_lambda_functions"] for testcase in [testcase1, testcase2]: function_binds = testcase.get('function_binds', {}) self.context.bind_functions(function_binds) variable_binds = testcase['variable_binds'] - self.context.bind_variables(variable_binds) + self.context.register_variables_config(variable_binds) - context_variables = self.context.variables + context_variables = self.context._get_evaluated_testcase_variables() self.assertIn("add1", context_variables) self.assertEqual(context_variables["add1"], 3) self.assertIn("sum2nums", context_variables) self.assertEqual(context_variables["sum2nums"], 5) - def test_context_variable_function_lambda_with_import(self): + def test_context_bind_lambda_functions_with_import(self): testcase1 = { "requires": ["random", "string", "hashlib"], "function_binds": { @@ -120,11 +94,11 @@ class VariableBindsUnittest(unittest.TestCase): "variable_binds": [ {"TOKEN": "debugtalk"}, {"random": {"func": "gen_random_string", "args": [5]}}, - {"data": "{'name': 'user', 'password': '123456'}"}, - {"md5": {"func": "gen_md5", "args": ["$TOKEN", "$data", "$random"]}} + {"data": '{"name": "user", "password": "123456"}'}, + {"authorization": {"func": "gen_md5", "args": ["${TOKEN}", "${data}", "${random}"]}} ] } - testcase2 = self.testcases[5] + testcase2 = self.testcases["bind_lambda_functions_with_import"] for testcase in [testcase1, testcase2]: requires = testcase.get('requires', []) @@ -134,11 +108,20 @@ class VariableBindsUnittest(unittest.TestCase): self.context.bind_functions(function_binds) variable_binds = testcase['variable_binds'] - self.context.bind_variables(variable_binds) + self.context.register_variables_config(variable_binds) + context_variables = self.context._get_evaluated_testcase_variables() - context_variables = self.context.variables + self.assertIn("TOKEN", context_variables) + TOKEN = context_variables["TOKEN"] + self.assertEqual(TOKEN, "debugtalk") self.assertIn("random", context_variables) self.assertIsInstance(context_variables["random"], str) self.assertEqual(len(context_variables["random"]), 5) - self.assertIn("md5", context_variables) - self.assertEqual(len(context_variables["md5"]), 32) + random = context_variables["random"] + self.assertIn("data", context_variables) + data = context_variables["data"] + self.assertIn("authorization", context_variables) + self.assertEqual(len(context_variables["authorization"]), 32) + authorization = context_variables["authorization"] + self.assertEqual(utils.gen_md5(TOKEN, data, random), authorization) +