20
20
from _pytest import fixtures as pytest_fixtures
21
21
except ImportError :
22
22
from _pytest import python as pytest_fixtures
23
- import six
24
23
25
24
from . import exceptions
26
25
from .feature import (
27
26
Feature ,
28
- force_encode ,
29
27
force_unicode ,
30
28
get_features ,
31
29
)
32
30
from .steps import (
33
- execute ,
34
- get_caller_function ,
35
31
get_caller_module ,
36
32
get_step_fixture_name ,
37
33
inject_fixture ,
38
- recreate_function ,
39
34
)
40
35
from .types import GIVEN
41
36
from .utils import CONFIG_STACK , get_args
42
37
43
- if six .PY3 : # pragma: no cover
44
- import runpy
45
-
46
- def execfile (filename , init_globals ):
47
- """Execute given file as a python script in given globals environment."""
48
- result = runpy .run_path (filename , init_globals = init_globals )
49
- init_globals .update (result )
50
-
51
38
52
39
PYTHON_REPLACE_REGEX = re .compile (r"\W" )
53
40
ALPHA_REGEX = re .compile (r"^\d+_*" )
54
41
55
42
43
+ # We have to keep track of the invocation of @scenario() so that we can reorder test item accordingly.
44
+ # In python 3.6+ this is no longer necessary, as the order is automatically retained.
45
+ _py2_scenario_creation_counter = 0
46
+
47
+
56
48
def find_argumented_step_fixture_name (name , type_ , fixturemanager , request = None ):
57
49
"""Find argumented step fixture name."""
58
50
# happens to be that _arg2fixturedefs is changed during the iteration so we use a copy
@@ -88,9 +80,11 @@ def _find_step_function(request, step, scenario, encoding):
88
80
"""
89
81
name = step .name
90
82
try :
83
+ # Simple case where no parser is used for the step
91
84
return request .getfixturevalue (get_step_fixture_name (name , step .type , encoding ))
92
85
except pytest_fixtures .FixtureLookupError :
93
86
try :
87
+ # Could not find a fixture with the same name, let's see if there is a parser involved
94
88
name = find_argumented_step_fixture_name (name , step .type , request ._fixturemanager , request )
95
89
if name :
96
90
return request .getfixturevalue (name )
@@ -204,63 +198,53 @@ def _execute_scenario(feature, scenario, request, encoding):
204
198
FakeRequest = collections .namedtuple ("FakeRequest" , ["module" ])
205
199
206
200
207
- def _get_scenario_decorator (feature , feature_name , scenario , scenario_name , caller_module , caller_function , encoding ):
208
- """Get scenario decorator."""
209
- g = locals ()
210
- g ["_execute_scenario" ] = _execute_scenario
201
+ def _get_scenario_decorator (feature , feature_name , scenario , scenario_name , encoding ):
202
+ global _py2_scenario_creation_counter
211
203
212
- scenario_name = force_encode (scenario_name , encoding )
204
+ counter = _py2_scenario_creation_counter
205
+ _py2_scenario_creation_counter += 1
213
206
214
- def decorator (_pytestbdd_function ):
215
- if isinstance (_pytestbdd_function , pytest_fixtures .FixtureRequest ):
207
+ # HACK: Ideally we would use `def decorator(fn)`, but we want to return a custom exception
208
+ # when the decorator is misused.
209
+ # Pytest inspect the signature to determine the required fixtures, and in that case it would look
210
+ # for a fixture called "fn" that doesn't exist (if it exists then it's even worse).
211
+ # It will error with a "fixture 'fn' not found" message instead.
212
+ # We can avoid this hack by using a pytest hook and check for misuse instead.
213
+ def decorator (* args ):
214
+ if not args :
216
215
raise exceptions .ScenarioIsDecoratorOnly (
217
216
"scenario function can only be used as a decorator. Refer to the documentation." ,
218
217
)
219
-
220
- g .update (locals ())
221
-
222
- args = get_args (_pytestbdd_function )
218
+ [fn ] = args
219
+ args = get_args (fn )
223
220
function_args = list (args )
224
221
for arg in scenario .get_example_params ():
225
222
if arg not in function_args :
226
223
function_args .append (arg )
227
- if "request" not in function_args :
228
- function_args .append ("request" )
229
224
230
- code = """def {name}({function_args}):
225
+ @pytest .mark .usefixtures (* function_args )
226
+ def scenario_wrapper (request ):
231
227
_execute_scenario (feature , scenario , request , encoding )
232
- _pytestbdd_function({args})""" .format (
233
- name = _pytestbdd_function .__name__ ,
234
- function_args = ", " .join (function_args ),
235
- args = ", " .join (args ))
236
-
237
- execute (code , g )
238
-
239
- _scenario = recreate_function (
240
- g [_pytestbdd_function .__name__ ],
241
- module = caller_module ,
242
- firstlineno = caller_function .f_lineno ,
243
- )
228
+ return fn (* [request .getfixturevalue (arg ) for arg in args ])
244
229
245
230
for param_set in scenario .get_params ():
246
231
if param_set :
247
- _scenario = pytest .mark .parametrize (* param_set )(_scenario )
248
-
232
+ scenario_wrapper = pytest .mark .parametrize (* param_set )(scenario_wrapper )
249
233
for tag in scenario .tags .union (feature .tags ):
250
234
config = CONFIG_STACK [- 1 ]
251
- config .hook .pytest_bdd_apply_tag (tag = tag , function = _scenario )
235
+ config .hook .pytest_bdd_apply_tag (tag = tag , function = scenario_wrapper )
252
236
253
- _scenario .__doc__ = "{feature_name}: {scenario_name}" .format (
237
+ scenario_wrapper .__doc__ = u "{feature_name}: {scenario_name}" .format (
254
238
feature_name = feature_name , scenario_name = scenario_name )
255
- _scenario .__scenario__ = scenario
256
- scenario . test_function = _scenario
257
- return _scenario
258
-
259
- return recreate_function ( decorator , module = caller_module , firstlineno = caller_function . f_lineno )
239
+ scenario_wrapper .__scenario__ = scenario
240
+ scenario_wrapper . __pytest_bdd_counter__ = counter
241
+ scenario . test_function = scenario_wrapper
242
+ return scenario_wrapper
243
+ return decorator
260
244
261
245
262
246
def scenario (feature_name , scenario_name , encoding = "utf-8" , example_converters = None ,
263
- caller_module = None , caller_function = None , features_base_dir = None , strict_gherkin = None ):
247
+ caller_module = None , features_base_dir = None , strict_gherkin = None ):
264
248
"""Scenario decorator.
265
249
266
250
:param str feature_name: Feature file name. Absolute or relative to the configured feature base path.
@@ -269,9 +253,9 @@ def scenario(feature_name, scenario_name, encoding="utf-8", example_converters=N
269
253
:param dict example_converters: optional `dict` of example converter function, where key is the name of the
270
254
example parameter, and value is the converter function.
271
255
"""
256
+
272
257
scenario_name = force_unicode (scenario_name , encoding )
273
258
caller_module = caller_module or get_caller_module ()
274
- caller_function = caller_function or get_caller_function ()
275
259
276
260
# Get the feature
277
261
if features_base_dir is None :
@@ -280,7 +264,7 @@ def scenario(feature_name, scenario_name, encoding="utf-8", example_converters=N
280
264
strict_gherkin = get_strict_gherkin ()
281
265
feature = Feature .get_feature (features_base_dir , feature_name , encoding = encoding , strict_gherkin = strict_gherkin )
282
266
283
- # Get the sc_enario
267
+ # Get the scenario
284
268
try :
285
269
scenario = feature .scenarios [scenario_name ]
286
270
except KeyError :
@@ -298,13 +282,11 @@ def scenario(feature_name, scenario_name, encoding="utf-8", example_converters=N
298
282
scenario .validate ()
299
283
300
284
return _get_scenario_decorator (
301
- feature ,
302
- feature_name ,
303
- scenario ,
304
- scenario_name ,
305
- caller_module ,
306
- caller_function ,
307
- encoding ,
285
+ feature = feature ,
286
+ feature_name = feature_name ,
287
+ scenario = scenario ,
288
+ scenario_name = scenario_name ,
289
+ encoding = encoding ,
308
290
)
309
291
310
292
@@ -375,7 +357,6 @@ def scenarios(*feature_paths, **kwargs):
375
357
(attr .__scenario__ .feature .filename , attr .__scenario__ .name )
376
358
for name , attr in module .__dict__ .items () if hasattr (attr , '__scenario__' ))
377
359
378
- index = 10
379
360
for feature in get_features (abs_feature_paths , strict_gherkin = strict_gherkin ):
380
361
for scenario_name , scenario_object in feature .scenarios .items ():
381
362
# skip already bound scenarios
@@ -386,9 +367,6 @@ def _scenario():
386
367
for test_name in get_python_name_generator (scenario_name ):
387
368
if test_name not in module .__dict__ :
388
369
# found an unique test name
389
- # recreate function to set line number
390
- _scenario = recreate_function (_scenario , module = module , firstlineno = index * 4 )
391
- index += 1
392
370
module .__dict__ [test_name ] = _scenario
393
371
break
394
372
found = True
0 commit comments