blob: 2ec649ea7c1d7de07279052f10227d2af9d2eddb [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
Tor Norbye1aa2e092014-08-20 17:01:23 -0700139 step_name = "{} {}".format(element.keyword, element.name)
Tor Norbye02cf98d2014-08-19 12:53:10 -0700140 if is_started:
Tor Norbye1aa2e092014-08-20 17:01:23 -0700141 self._test_started(step_name, element.location)
Tor Norbye02cf98d2014-08-19 12:53:10 -0700142 elif element.status == 'passed':
Tor Norbye1aa2e092014-08-20 17:01:23 -0700143 self._test_passed(step_name, element.duration)
Tor Norbye02cf98d2014-08-19 12:53:10 -0700144 elif element.status == 'failed':
145 try:
146 trace = traceback.format_exc()
147 except Exception:
148 trace = "".join(traceback.format_tb(element.exc_traceback))
Tor Norbye1aa2e092014-08-20 17:01:23 -0700149 if trace in str(element.error_message):
150 trace = None # No reason to duplicate output (see PY-13647)
151 self._test_failed(step_name, element.error_message, trace)
Tor Norbye02cf98d2014-08-19 12:53:10 -0700152 elif element.status == 'undefined':
Tor Norbye1aa2e092014-08-20 17:01:23 -0700153 self._test_undefined(step_name, element.location)
Tor Norbye02cf98d2014-08-19 12:53:10 -0700154 else:
Tor Norbye1aa2e092014-08-20 17:01:23 -0700155 self._test_skipped(step_name, element.status, element.location)
Tor Norbye02cf98d2014-08-19 12:53:10 -0700156 elif not is_started and isinstance(element, Scenario) and element.status == 'failed':
157 # To process scenarios with undefined/skipped tests
158 for step in element.steps:
159 assert isinstance(step, Step), step
160 if step.status not in ['passed', 'failed']: # Something strange, probably skipped or undefined
161 self.__process_hook(False, context, step)
162 self._feature_or_scenario(is_started, element.name, element.location)
163 elif isinstance(element, ScenarioOutline):
164 self._feature_or_scenario(is_started, str(element.examples), element.location)
165 else:
166 self._feature_or_scenario(is_started, element.name, element.location)
167
168 def __init__(self, config, base_dir):
169 """
170 :type config configuration.Configuration
171 """
172 super(_BehaveRunner, self).__init__(base_dir)
173 self.__config = config
174 # Install hooks
175 self.__real_runner = _RunnerWrapper(config, {
176 "before_feature": functools.partial(self.__process_hook, True),
177 "after_feature": functools.partial(self.__process_hook, False),
178 "before_scenario": functools.partial(self.__process_hook, True),
179 "after_scenario": functools.partial(self.__process_hook, False),
180 "before_step": functools.partial(self.__process_hook, True),
181 "after_step": functools.partial(self.__process_hook, False)
182 })
183
184 def _run_tests(self):
185 self.__real_runner.run()
186
187
188 def __filter_scenarios_by_tag(self, scenario):
189 """
190 Filters out scenarios that should be skipped by tags
191 :param scenario scenario to check
192 :return true if should pass
193 """
194 assert isinstance(scenario, Scenario), scenario
195 expected_tags = self.__config.tags
196 if not expected_tags:
197 return True # No tags are required
198 return isinstance(expected_tags, TagExpression) and expected_tags.check(scenario.tags)
199
200
201 def _get_features_to_run(self):
202 self.__real_runner.dry_run = True
203 self.__real_runner.run()
204 features_to_run = self.__real_runner.features
205 self.__real_runner.clean() # To make sure nothing left after dry run
206
207 # Change outline scenario skeletons with real scenarios
208 for feature in features_to_run:
209 assert isinstance(feature, Feature), feature
210 scenarios = []
211 for scenario in feature.scenarios:
212 if isinstance(scenario, ScenarioOutline):
213 scenarios.extend(scenario.scenarios)
214 else:
215 scenarios.append(scenario)
216 feature.scenarios = filter(self.__filter_scenarios_by_tag, scenarios)
217
218 return features_to_run
219
220
221if __name__ == "__main__":
222 # TODO: support all other params instead
223
224 class _Null(Formatter):
225 """
226 Null formater to prevent stdout output
227 """
228 pass
229
230 command_args = list(filter(None, sys.argv[1:]))
Tor Norbyec3d3a902014-09-04 13:24:04 -0700231 if command_args:
232 _bdd_utils.fix_win_drive(command_args[0])
Tor Norbye02cf98d2014-08-19 12:53:10 -0700233 my_config = configuration.Configuration(command_args=command_args)
234 formatters.register_as(_Null, "com.intellij.python.null")
235 my_config.format = ["com.intellij.python.null"] # To prevent output to stdout
236 my_config.reporters = [] # To prevent summary to stdout
237 my_config.stdout_capture = False # For test output
238 my_config.stderr_capture = False # For test output
239 (base_dir, what_to_run) = _bdd_utils.get_path_by_args(sys.argv)
240 if not my_config.paths: # No path provided, trying to load dit manually
241 if os.path.isfile(what_to_run): # File is provided, load it
242 my_config.paths = [what_to_run]
243 else: # Dir is provided, find subdirs ro run
244 my_config.paths = _get_dirs_to_run(base_dir)
245 _BehaveRunner(my_config, base_dir).run()
246
247