def get_execution_monitor(self, pytest_node, args, kwargs): """ Returns the StepsMonitor in charge of monitoring execution of the provided pytest node. The same StepsMonitor will be used to execute all steps of the generator function. If there is no monitor yet (first function call with this combination of parameters), then one is created, that will be used subsequently. :param pytest_node: :param args: :param kwargs: :return: """ # Get the unique id that is shared between the steps of the same execution, by removing the step parameter # Note: when the id was using not only param values but also fixture values we had to discard # 'request' and maybe some fixtures here. But that's not the case anymore,simply discard the "test step" param id_without_steps = get_pytest_node_hash_id( pytest_node, params_to_ignore=(GENERATOR_MODE_STEP_ARGNAME, )) if id_without_steps not in self: # First time we call the function with this combination of parameters # print("DEBUG - creating StepsMonitor for %s" % id_without_steps) # create the monitor, in charge of managing the execution flow self[id_without_steps] = StepsMonitor(self.step_ids, self.test_func, args, kwargs) return self[id_without_steps]
def wrapped_test_function(*args, **kwargs): """Executes the current step only if its dependencies are correct, and registers its execution result""" request = kwargs[ 'request'] if func_needs_request else kwargs.pop('request') if request is None: # manual call (maybe for pre-loading?), no dependency management, ability to execute several steps _execute_manually(test_func, s, test_step_argname, step_ids, steps, args, kwargs) else: # (a) retrieve the "current step" function current_step_fun = get_fixture_or_param_value( request, test_step_argname) # Get the unique id that is shared between the steps of the same execution # Note: when the id was using not only param values but also fixture values we had to discard # steps_data_holder_name and 'request'. But that's not the case anymore, simply discard "test step" test_id_without_steps = get_pytest_node_hash_id( request.node, params_to_ignore={test_step_argname}) # Make sure that it has a field to store its execution success if not hasattr(current_step_fun, STEP_SUCCESS_FIELD): # this is a dict where the key is the `test_id_without_steps` and the value is a boolean setattr(current_step_fun, STEP_SUCCESS_FIELD, dict()) # (b) skip or fail it if needed dependencies, should_fail = getattr( current_step_fun, DEPENDS_ON_FIELD, ([], False)) # -- check that dependencies have all run (execution order is correct) if not all( hasattr(step, STEP_SUCCESS_FIELD) for step in dependencies): raise ValueError( "Test step {} depends on another step that has not yet been executed. In " "current version the steps execution order is manual, make sure it is correct." "".format(current_step_fun.__name__)) # -- check that dependencies all ran with success deps_successess = { step: getattr(step, STEP_SUCCESS_FIELD).get( test_id_without_steps, False) for step in dependencies } failed_deps = [ d.__name__ for d, res in deps_successess.items() if res is False ] if not all(deps_successess.values()): msg = "This test step depends on other steps, and the following have failed: %s" % failed_deps if should_fail: pytest.fail(msg) else: pytest.skip(msg) # (c) execute the test function for this step res = test_func(*args, **kwargs) # (d) declare execution as a success getattr(current_step_fun, STEP_SUCCESS_FIELD)[test_id_without_steps] = True return res
def results(request): """ The fixture for the StepsDataHolder :param request: :return: """ test_id = get_pytest_node_hash_id(request.node, params_to_ignore={'test_step'}) return get_results_holder(id=test_id)
def _init_and_check(request): """ Checks that the current request is not session but a specific node. :param request: :return: """ scope = get_scope(request) if scope == 'function': # function-scope: ok id_without_steps = get_pytest_node_hash_id( request.node, params_to_ignore=_get_step_param_names_or_default( step_param_names)) return id_without_steps else: # session- or module-scope raise Exception( "The `@cross_steps_fixture` decorator is only useful for function-scope fixtures. `%s`" " seems to have scope='%s'. Consider removing `@cross_steps_fixture` or changing " "the scope to 'function'." % (fixture_fun, scope))
def results(request): """ The fixture for the StepsDataHolder. It is function-scoped (so oit is called for each step of each param combination) but it implements an intelligent cache so that the same StepsDataHolder object is returned across all test steps belonging to the same param combination. :param request: :return: """ # Get a good unique identifier of the test. # The id should be different everytime anything changes, except when the test step changes # Note: when the id was using not only param values but also fixture values we had to discard # steps_data_holder_name and 'request'. But that's not the case anymore,simply discard "test step" param test_id = get_pytest_node_hash_id( request.node, params_to_ignore={test_step_argname}) # Get or create the cached Result holder for this combination of parameters return get_results_holder(id=test_id)