blob: 4a1b2f6557c56ba0629bec582b280d21ae4bebbf [file] [log] [blame]
Tor Norbye02cf98d2014-08-19 12:53:10 -07001# coding=utf-8
2"""
3Behave BDD runner.
4*FIRST* param now: folder to search "features" for.
5Each "features" folder should have features and "steps" subdir.
6
7Other args are tag expressionsin format (--tags=.. --tags=..).
8See https://pythonhosted.org/behave/behave.html#tag-expression
9"""
10import functools
11import sys
12import os
13import traceback
14
15from behave.formatter.base import Formatter
16from behave.model import Step, ScenarioOutline, Feature, Scenario
17from behave.tag_expression import TagExpression
18
19import _bdd_utils
20
21
22_MAX_STEPS_SEARCH_FEATURES = 5000 # Do not look for features in folder that has more that this number of children
23_FEATURES_FOLDER = 'features' # "features" folder name.
24
25__author__ = 'Ilya.Kazakevich'
26
27from behave import configuration, runner
28from behave.formatter import formatters
29
30
31def _get_dirs_to_run(base_dir_to_search):
32 """
33 Searches for "features" dirs in some base_dir
34 :return: list of feature dirs to run
35 :rtype: list
36 :param base_dir_to_search root directory to search (should not have too many children!)
37 :type base_dir_to_search str
38
39 """
40 result = set()
41 for (step, (folder, sub_folders, files)) in enumerate(os.walk(base_dir_to_search)):
42 if os.path.basename(folder) == _FEATURES_FOLDER and os.path.isdir(folder):
43 result.add(os.path.abspath(folder))
44 if step == _MAX_STEPS_SEARCH_FEATURES: # Guard
45 err = "Folder {} is too deep to find any features folder. Please provider concrete folder".format(
46 base_dir_to_search)
47 raise Exception(err)
48 return list(result)
49
50
51def _merge_hooks_wrapper(*hooks):
52 """
53 Creates wrapper that runs provided behave hooks sequentally
54 :param hooks: hooks to run
55 :return: wrapper
56 """
57 # TODO: Wheel reinvented!!!!
58 def wrapper(*args, **kwargs):
59 for hook in hooks:
60 hook(*args, **kwargs)
61
62 return wrapper
63
64
65class _RunnerWrapper(runner.Runner):
66 """
67 Wrapper around behave native wrapper. Has nothing todo with BddRunner!
68 We need it to support dry runs (to fetch data from scenarios) and hooks api
69 """
70
71 def __init__(self, config, hooks):
72 """
73 :type config configuration.Configuration
74 :param config behave configuration
75 :type hooks dict
76 :param hooks hooks in format "before_scenario" => f(context, scenario) to load after/before hooks, provided by user
77 """
78 super(_RunnerWrapper, self).__init__(config)
79 self.dry_run = False
80 """
81 Does not run tests (only fetches "self.features") if true. Runs tests otherwise.
82 """
83 self.__hooks = hooks
84
85 def load_hooks(self, filename='environment.py'):
86 """
87 Overrides parent "load_hooks" to add "self.__hooks"
88 :param filename: env. file name
89 """
90 super(_RunnerWrapper, self).load_hooks(filename)
91 for (hook_name, hook) in self.__hooks.items():
92 hook_to_add = hook
93 if hook_name in self.hooks:
94 user_hook = self.hooks[hook_name]
95 if hook_name.startswith("before"):
96 user_and_custom_hook = [user_hook, hook]
97 else:
98 user_and_custom_hook = [hook, user_hook]
99 hook_to_add = _merge_hooks_wrapper(*user_and_custom_hook)
100 self.hooks[hook_name] = hook_to_add
101
102 def run_model(self, features=None):
103 """
104 Overrides parent method to stop (do nothing) in case of "dry_run"
105 :param features: features to run
106 :return:
107 """
108 if self.dry_run: # To stop further execution
109 return
110 return super(_RunnerWrapper, self).run_model(features)
111
112 def clean(self):
113 """
114 Cleans runner after dry run (clears hooks, features etc). To be called before real run!
115 """
116 self.dry_run = False
117 self.hooks.clear()
118 self.features = []
119
120
121class _BehaveRunner(_bdd_utils.BddRunner):
122 """
123 BddRunner for behave
124 """
125
126
127 def __process_hook(self, is_started, context, element):
128 """
129 Hook to be installed. Reports steps, features etc.
130 :param is_started true if test/feature/scenario is started
131 :type is_started bool
132 :param context behave context
133 :type context behave.runner.Context
134 :param element feature/suite/step
135 """
136 element.location.file = element.location.filename # To preserve _bdd_utils contract
137 if isinstance(element, Step):
138 # Process step
139 if is_started:
140 self._test_started(element.name, element.location)
141 elif element.status == 'passed':
142 self._test_passed(element.name, element.duration)
143 elif element.status == 'failed':
144 try:
145 trace = traceback.format_exc()
146 except Exception:
147 trace = "".join(traceback.format_tb(element.exc_traceback))
148 self._test_failed(element.name, element.error_message, trace)
149 elif element.status == 'undefined':
150 self._test_undefined(element.name, element.location)
151 else:
152 self._test_skipped(element.name, element.status, element.location)
153 elif not is_started and isinstance(element, Scenario) and element.status == 'failed':
154 # To process scenarios with undefined/skipped tests
155 for step in element.steps:
156 assert isinstance(step, Step), step
157 if step.status not in ['passed', 'failed']: # Something strange, probably skipped or undefined
158 self.__process_hook(False, context, step)
159 self._feature_or_scenario(is_started, element.name, element.location)
160 elif isinstance(element, ScenarioOutline):
161 self._feature_or_scenario(is_started, str(element.examples), element.location)
162 else:
163 self._feature_or_scenario(is_started, element.name, element.location)
164
165 def __init__(self, config, base_dir):
166 """
167 :type config configuration.Configuration
168 """
169 super(_BehaveRunner, self).__init__(base_dir)
170 self.__config = config
171 # Install hooks
172 self.__real_runner = _RunnerWrapper(config, {
173 "before_feature": functools.partial(self.__process_hook, True),
174 "after_feature": functools.partial(self.__process_hook, False),
175 "before_scenario": functools.partial(self.__process_hook, True),
176 "after_scenario": functools.partial(self.__process_hook, False),
177 "before_step": functools.partial(self.__process_hook, True),
178 "after_step": functools.partial(self.__process_hook, False)
179 })
180
181 def _run_tests(self):
182 self.__real_runner.run()
183
184
185 def __filter_scenarios_by_tag(self, scenario):
186 """
187 Filters out scenarios that should be skipped by tags
188 :param scenario scenario to check
189 :return true if should pass
190 """
191 assert isinstance(scenario, Scenario), scenario
192 expected_tags = self.__config.tags
193 if not expected_tags:
194 return True # No tags are required
195 return isinstance(expected_tags, TagExpression) and expected_tags.check(scenario.tags)
196
197
198 def _get_features_to_run(self):
199 self.__real_runner.dry_run = True
200 self.__real_runner.run()
201 features_to_run = self.__real_runner.features
202 self.__real_runner.clean() # To make sure nothing left after dry run
203
204 # Change outline scenario skeletons with real scenarios
205 for feature in features_to_run:
206 assert isinstance(feature, Feature), feature
207 scenarios = []
208 for scenario in feature.scenarios:
209 if isinstance(scenario, ScenarioOutline):
210 scenarios.extend(scenario.scenarios)
211 else:
212 scenarios.append(scenario)
213 feature.scenarios = filter(self.__filter_scenarios_by_tag, scenarios)
214
215 return features_to_run
216
217
218if __name__ == "__main__":
219 # TODO: support all other params instead
220
221 class _Null(Formatter):
222 """
223 Null formater to prevent stdout output
224 """
225 pass
226
227 command_args = list(filter(None, sys.argv[1:]))
228 my_config = configuration.Configuration(command_args=command_args)
229 formatters.register_as(_Null, "com.intellij.python.null")
230 my_config.format = ["com.intellij.python.null"] # To prevent output to stdout
231 my_config.reporters = [] # To prevent summary to stdout
232 my_config.stdout_capture = False # For test output
233 my_config.stderr_capture = False # For test output
234 (base_dir, what_to_run) = _bdd_utils.get_path_by_args(sys.argv)
235 if not my_config.paths: # No path provided, trying to load dit manually
236 if os.path.isfile(what_to_run): # File is provided, load it
237 my_config.paths = [what_to_run]
238 else: # Dir is provided, find subdirs ro run
239 my_config.paths = _get_dirs_to_run(base_dir)
240 _BehaveRunner(my_config, base_dir).run()
241
242