1# -*- coding: UTF-8 -*- 2""" 3This module provides Runner class to run behave feature files (or model elements). 4""" 5 6from __future__ import absolute_import, print_function, with_statement 7 8import contextlib 9import os.path 10import sys 11import warnings 12import weakref 13 14import six 15 16from behave._types import ExceptionUtil 17from behave.capture import CaptureController 18from behave.configuration import ConfigError 19from behave.formatter._registry import make_formatters 20from behave.runner_util import \ 21 collect_feature_locations, parse_features, \ 22 exec_file, load_step_modules, PathManager 23from behave.step_registry import registry as the_step_registry 24 25if six.PY2: 26 # -- USE PYTHON3 BACKPORT: With unicode traceback support. 27 import traceback2 as traceback 28else: 29 import traceback 30 31 32class CleanupError(RuntimeError): 33 pass 34 35 36class ContextMaskWarning(UserWarning): 37 """Raised if a context variable is being overwritten in some situations. 38 39 If the variable was originally set by user code then this will be raised if 40 *behave* overwites the value. 41 42 If the variable was originally set by *behave* then this will be raised if 43 user code overwites the value. 44 """ 45 pass 46 47 48class Context(object): 49 """Hold contextual information during the running of tests. 50 51 This object is a place to store information related to the tests you're 52 running. You may add arbitrary attributes to it of whatever value you need. 53 54 During the running of your tests the object will have additional layers of 55 namespace added and removed automatically. There is a "root" namespace and 56 additional namespaces for features and scenarios. 57 58 Certain names are used by *behave*; be wary of using them yourself as 59 *behave* may overwrite the value you set. These names are: 60 61 .. attribute:: feature 62 63 This is set when we start testing a new feature and holds a 64 :class:`~behave.model.Feature`. It will not be present outside of a 65 feature (i.e. within the scope of the environment before_all and 66 after_all). 67 68 .. attribute:: scenario 69 70 This is set when we start testing a new scenario (including the 71 individual scenarios of a scenario outline) and holds a 72 :class:`~behave.model.Scenario`. It will not be present outside of the 73 scope of a scenario. 74 75 .. attribute:: tags 76 77 The current set of active tags (as a Python set containing instances of 78 :class:`~behave.model.Tag` which are basically just glorified strings) 79 combined from the feature and scenario. This attribute will not be 80 present outside of a feature scope. 81 82 .. attribute:: aborted 83 84 This is set to true in the root namespace when the user aborts a test run 85 (:exc:`KeyboardInterrupt` exception). Initially: False. 86 87 .. attribute:: failed 88 89 This is set to true in the root namespace as soon as a step fails. 90 Initially: False. 91 92 .. attribute:: table 93 94 This is set at the step level and holds any :class:`~behave.model.Table` 95 associated with the step. 96 97 .. attribute:: text 98 99 This is set at the step level and holds any multiline text associated 100 with the step. 101 102 .. attribute:: config 103 104 The configuration of *behave* as determined by configuration files and 105 command-line options. The attributes of this object are the same as the 106 `configuration file section names`_. 107 108 .. attribute:: active_outline 109 110 This is set for each scenario in a scenario outline and references the 111 :class:`~behave.model.Row` that is active for the current scenario. It is 112 present mostly for debugging, but may be useful otherwise. 113 114 .. attribute:: log_capture 115 116 If logging capture is enabled then this attribute contains the captured 117 logging as an instance of :class:`~behave.log_capture.LoggingCapture`. 118 It is not present if logging is not being captured. 119 120 .. attribute:: stdout_capture 121 122 If stdout capture is enabled then this attribute contains the captured 123 output as a StringIO instance. It is not present if stdout is not being 124 captured. 125 126 .. attribute:: stderr_capture 127 128 If stderr capture is enabled then this attribute contains the captured 129 output as a StringIO instance. It is not present if stderr is not being 130 captured. 131 132 If an attempt made by user code to overwrite one of these variables, or 133 indeed by *behave* to overwite a user-set variable, then a 134 :class:`behave.runner.ContextMaskWarning` warning will be raised. 135 136 You may use the "in" operator to test whether a certain value has been set 137 on the context, for example: 138 139 "feature" in context 140 141 checks whether there is a "feature" value in the context. 142 143 Values may be deleted from the context using "del" but only at the level 144 they are set. You can't delete a value set by a feature at a scenario level 145 but you can delete a value set for a scenario in that scenario. 146 147 .. _`configuration file section names`: behave.html#configuration-files 148 """ 149 # pylint: disable=too-many-instance-attributes 150 BEHAVE = "behave" 151 USER = "user" 152 FAIL_ON_CLEANUP_ERRORS = True 153 154 def __init__(self, runner): 155 self._runner = weakref.proxy(runner) 156 self._config = runner.config 157 d = self._root = { 158 "aborted": False, 159 "failed": False, 160 "config": self._config, 161 "active_outline": None, 162 "cleanup_errors": 0, 163 "@cleanups": [], # -- REQUIRED-BY: before_all() hook 164 "@layer": "testrun", 165 } 166 self._stack = [d] 167 self._record = {} 168 self._origin = {} 169 self._mode = self.BEHAVE 170 self.feature = None 171 # -- RECHECK: If needed 172 self.text = None 173 self.table = None 174 self.stdout_capture = None 175 self.stderr_capture = None 176 self.log_capture = None 177 self.fail_on_cleanup_errors = self.FAIL_ON_CLEANUP_ERRORS 178 179 @staticmethod 180 def ignore_cleanup_error(context, cleanup_func, exception): 181 pass 182 183 @staticmethod 184 def print_cleanup_error(context, cleanup_func, exception): 185 cleanup_func_name = getattr(cleanup_func, "__name__", None) 186 if not cleanup_func_name: 187 cleanup_func_name = "%r" % cleanup_func 188 print(u"CLEANUP-ERROR in %s: %s: %s" % 189 (cleanup_func_name, exception.__class__.__name__, exception)) 190 traceback.print_exc(file=sys.stdout) 191 # MAYBE: context._dump(pretty=True, prefix="Context: ") 192 # -- MARK: testrun as FAILED 193 # context._set_root_attribute("failed", True) 194 195 def _do_cleanups(self): 196 """Execute optional cleanup functions when stack frame is popped. 197 A user can add a user-specified handler for cleanup errors. 198 199 .. code-block:: python 200 201 # -- FILE: features/environment.py 202 def cleanup_database(database): 203 pass 204 205 def handle_cleanup_error(context, cleanup_func, exception): 206 pass 207 208 def before_all(context): 209 context.on_cleanup_error = handle_cleanup_error 210 context.add_cleanup(cleanup_database, the_database) 211 """ 212 # -- BEST-EFFORT ALGORITHM: Tries to perform all cleanups. 213 assert self._stack, "REQUIRE: Non-empty stack" 214 current_layer = self._stack[0] 215 cleanup_funcs = current_layer.get("@cleanups", []) 216 on_cleanup_error = getattr(self, "on_cleanup_error", 217 self.print_cleanup_error) 218 context = self 219 cleanup_errors = [] 220 for cleanup_func in reversed(cleanup_funcs): 221 try: 222 cleanup_func() 223 except Exception as e: # pylint: disable=broad-except 224 # pylint: disable=protected-access 225 context._root["cleanup_errors"] += 1 226 cleanup_errors.append(sys.exc_info()) 227 on_cleanup_error(context, cleanup_func, e) 228 229 if self.fail_on_cleanup_errors and cleanup_errors: 230 first_cleanup_erro_info = cleanup_errors[0] 231 del cleanup_errors # -- ENSURE: Release other exception frames. 232 six.reraise(*first_cleanup_erro_info) 233 234 235 def _push(self, layer_name=None): 236 """Push a new layer on the context stack. 237 HINT: Use layer_name values: "scenario", "feature", "testrun". 238 239 :param layer_name: Layer name to use (or None). 240 """ 241 initial_data = {"@cleanups": []} 242 if layer_name: 243 initial_data["@layer"] = layer_name 244 self._stack.insert(0, initial_data) 245 246 def _pop(self): 247 """Pop the current layer from the context stack. 248 Performs any pending cleanups, registered for this layer. 249 """ 250 try: 251 self._do_cleanups() 252 finally: 253 # -- ENSURE: Layer is removed even if cleanup-errors occur. 254 self._stack.pop(0) 255 256 def _use_with_behave_mode(self): 257 """Provides a context manager for using the context in BEHAVE mode.""" 258 return use_context_with_mode(self, Context.BEHAVE) 259 260 def use_with_user_mode(self): 261 """Provides a context manager for using the context in USER mode.""" 262 return use_context_with_mode(self, Context.USER) 263 264 def user_mode(self): 265 warnings.warn("Use 'use_with_user_mode()' instead", 266 PendingDeprecationWarning, stacklevel=2) 267 return self.use_with_user_mode() 268 269 def _set_root_attribute(self, attr, value): 270 for frame in self.__dict__["_stack"]: 271 if frame is self.__dict__["_root"]: 272 continue 273 if attr in frame: 274 record = self.__dict__["_record"][attr] 275 params = { 276 "attr": attr, 277 "filename": record[0], 278 "line": record[1], 279 "function": record[3], 280 } 281 self._emit_warning(attr, params) 282 283 self.__dict__["_root"][attr] = value 284 if attr not in self._origin: 285 self._origin[attr] = self._mode 286 287 def _emit_warning(self, attr, params): 288 msg = "" 289 if self._mode is self.BEHAVE and self._origin[attr] is not self.BEHAVE: 290 msg = "behave runner is masking context attribute '%(attr)s' " \ 291 "originally set in %(function)s (%(filename)s:%(line)s)" 292 elif self._mode is self.USER: 293 if self._origin[attr] is not self.USER: 294 msg = "user code is masking context attribute '%(attr)s' " \ 295 "originally set by behave" 296 elif self._config.verbose: 297 msg = "user code is masking context attribute " \ 298 "'%(attr)s'; see the tutorial for what this means" 299 if msg: 300 msg = msg % params 301 warnings.warn(msg, ContextMaskWarning, stacklevel=3) 302 303 def _dump(self, pretty=False, prefix=" "): 304 for level, frame in enumerate(self._stack): 305 print("%sLevel %d" % (prefix, level)) 306 if pretty: 307 for name in sorted(frame.keys()): 308 value = frame[name] 309 print("%s %-15s = %r" % (prefix, name, value)) 310 else: 311 print(prefix + repr(frame)) 312 313 def __getattr__(self, attr): 314 if attr[0] == "_": 315 return self.__dict__[attr] 316 for frame in self._stack: 317 if attr in frame: 318 return frame[attr] 319 msg = "'{0}' object has no attribute '{1}'" 320 msg = msg.format(self.__class__.__name__, attr) 321 raise AttributeError(msg) 322 323 def __setattr__(self, attr, value): 324 if attr[0] == "_": 325 self.__dict__[attr] = value 326 return 327 328 for frame in self._stack[1:]: 329 if attr in frame: 330 record = self._record[attr] 331 params = { 332 "attr": attr, 333 "filename": record[0], 334 "line": record[1], 335 "function": record[3], 336 } 337 self._emit_warning(attr, params) 338 339 stack_limit = 2 340 if six.PY2: 341 stack_limit += 1 # Due to traceback2 usage. 342 stack_frame = traceback.extract_stack(limit=stack_limit)[0] 343 self._record[attr] = stack_frame 344 frame = self._stack[0] 345 frame[attr] = value 346 if attr not in self._origin: 347 self._origin[attr] = self._mode 348 349 def __delattr__(self, attr): 350 frame = self._stack[0] 351 if attr in frame: 352 del frame[attr] 353 del self._record[attr] 354 else: 355 msg = "'{0}' object has no attribute '{1}' at the current level" 356 msg = msg.format(self.__class__.__name__, attr) 357 raise AttributeError(msg) 358 359 def __contains__(self, attr): 360 if attr[0] == "_": 361 return attr in self.__dict__ 362 for frame in self._stack: 363 if attr in frame: 364 return True 365 return False 366 367 def execute_steps(self, steps_text): 368 """The steps identified in the "steps" text string will be parsed and 369 executed in turn just as though they were defined in a feature file. 370 371 If the execute_steps call fails (either through error or failure 372 assertion) then the step invoking it will need to catch the resulting 373 exceptions. 374 375 :param steps_text: Text with the Gherkin steps to execute (as string). 376 :returns: True, if the steps executed successfully. 377 :raises: AssertionError, if a step failure occurs. 378 :raises: ValueError, if invoked without a feature context. 379 """ 380 assert isinstance(steps_text, six.text_type), "Steps must be unicode." 381 if not self.feature: 382 raise ValueError("execute_steps() called outside of feature") 383 384 # -- PREPARE: Save original context data for current step. 385 # Needed if step definition that called this method uses .table/.text 386 original_table = getattr(self, "table", None) 387 original_text = getattr(self, "text", None) 388 389 self.feature.parser.variant = "steps" 390 steps = self.feature.parser.parse_steps(steps_text) 391 with self._use_with_behave_mode(): 392 for step in steps: 393 passed = step.run(self._runner, quiet=True, capture=False) 394 if not passed: 395 # -- ISSUE #96: Provide more substep info to diagnose problem. 396 step_line = u"%s %s" % (step.keyword, step.name) 397 message = "%s SUB-STEP: %s" % \ 398 (step.status.name.upper(), step_line) 399 if step.error_message: 400 message += "\nSubstep info: %s\n" % step.error_message 401 message += u"Traceback (of failed substep):\n" 402 message += u"".join(traceback.format_tb(step.exc_traceback)) 403 # message += u"\nTraceback (of context.execute_steps()):" 404 assert False, message 405 406 # -- FINALLY: Restore original context data for current step. 407 self.table = original_table 408 self.text = original_text 409 return True 410 411 def add_cleanup(self, cleanup_func, *args, **kwargs): 412 """Adds a cleanup function that is called when :meth:`Context._pop()` 413 is called. This is intended for user-cleanups. 414 415 :param cleanup_func: Callable function 416 :param args: Args for cleanup_func() call (optional). 417 :param kwargs: Kwargs for cleanup_func() call (optional). 418 """ 419 # MAYBE: 420 assert callable(cleanup_func), "REQUIRES: callable(cleanup_func)" 421 assert self._stack 422 if args or kwargs: 423 def internal_cleanup_func(): 424 cleanup_func(*args, **kwargs) 425 else: 426 internal_cleanup_func = cleanup_func 427 428 current_frame = self._stack[0] 429 if cleanup_func not in current_frame["@cleanups"]: 430 # -- AVOID DUPLICATES: 431 current_frame["@cleanups"].append(internal_cleanup_func) 432 433 434@contextlib.contextmanager 435def use_context_with_mode(context, mode): 436 """Switch context to BEHAVE or USER mode. 437 Provides a context manager for switching between the two context modes. 438 439 .. sourcecode:: python 440 441 context = Context() 442 with use_context_with_mode(context, Context.BEHAVE): 443 ... # Do something 444 # -- POSTCONDITION: Original context._mode is restored. 445 446 :param context: Context object to use. 447 :param mode: Mode to apply to context object. 448 """ 449 # pylint: disable=protected-access 450 assert mode in (Context.BEHAVE, Context.USER) 451 current_mode = context._mode 452 try: 453 context._mode = mode 454 yield 455 finally: 456 # -- RESTORE: Initial current_mode 457 # Even if an AssertionError/Exception is raised. 458 context._mode = current_mode 459 460 461@contextlib.contextmanager 462def scoped_context_layer(context, layer_name=None): 463 """Provides context manager for context layer (push/do-something/pop cycle). 464 465 .. code-block:: 466 467 with scoped_context_layer(context): 468 the_fixture = use_fixture(foo, context, name="foo_42") 469 """ 470 # pylint: disable=protected-access 471 try: 472 context._push(layer_name) 473 yield context 474 finally: 475 context._pop() 476 477 478def path_getrootdir(path): 479 """ 480 Extract rootdir from path in a platform independent way. 481 482 POSIX-PATH EXAMPLE: 483 rootdir = path_getrootdir("/foo/bar/one.feature") 484 assert rootdir == "/" 485 486 WINDOWS-PATH EXAMPLE: 487 rootdir = path_getrootdir("D:\\foo\\bar\\one.feature") 488 assert rootdir == r"D:\" 489 """ 490 drive, _ = os.path.splitdrive(path) 491 if drive: 492 # -- WINDOWS: 493 return drive + os.path.sep 494 # -- POSIX: 495 return os.path.sep 496 497 498class ModelRunner(object): 499 """ 500 Test runner for a behave model (features). 501 Provides the core functionality of a test runner and 502 the functional API needed by model elements. 503 504 .. attribute:: aborted 505 506 This is set to true when the user aborts a test run 507 (:exc:`KeyboardInterrupt` exception). Initially: False. 508 Stored as derived attribute in :attr:`Context.aborted`. 509 """ 510 # pylint: disable=too-many-instance-attributes 511 512 def __init__(self, config, features=None, step_registry=None): 513 self.config = config 514 self.features = features or [] 515 self.hooks = {} 516 self.formatters = [] 517 self.undefined_steps = [] 518 self.step_registry = step_registry 519 self.capture_controller = CaptureController(config) 520 521 self.context = None 522 self.feature = None 523 self.hook_failures = 0 524 525 # @property 526 def _get_aborted(self): 527 value = False 528 if self.context: 529 value = self.context.aborted 530 return value 531 532 # @aborted.setter 533 def _set_aborted(self, value): 534 # pylint: disable=protected-access 535 assert self.context, "REQUIRE: context, but context=%r" % self.context 536 self.context._set_root_attribute("aborted", bool(value)) 537 538 aborted = property(_get_aborted, _set_aborted, 539 doc="Indicates that test run is aborted by the user.") 540 541 def run_hook(self, name, context, *args): 542 if not self.config.dry_run and (name in self.hooks): 543 try: 544 with context.use_with_user_mode(): 545 self.hooks[name](context, *args) 546 # except KeyboardInterrupt: 547 # self.aborted = True 548 # if name not in ("before_all", "after_all"): 549 # raise 550 except Exception as e: # pylint: disable=broad-except 551 # -- HANDLE HOOK ERRORS: 552 use_traceback = False 553 if self.config.verbose: 554 use_traceback = True 555 ExceptionUtil.set_traceback(e) 556 extra = u"" 557 if "tag" in name: 558 extra = "(tag=%s)" % args[0] 559 560 error_text = ExceptionUtil.describe(e, use_traceback).rstrip() 561 error_message = u"HOOK-ERROR in %s%s: %s" % (name, extra, error_text) 562 print(error_message) 563 self.hook_failures += 1 564 if "tag" in name: 565 # -- SCENARIO or FEATURE 566 statement = getattr(context, "scenario", context.feature) 567 elif "all" in name: 568 # -- ABORT EXECUTION: For before_all/after_all 569 self.aborted = True 570 statement = None 571 else: 572 # -- CASE: feature, scenario, step 573 statement = args[0] 574 575 if statement: 576 # -- CASE: feature, scenario, step 577 statement.hook_failed = True 578 if statement.error_message: 579 # -- NOTE: One exception/failure is already stored. 580 # Append only error message. 581 statement.error_message += u"\n"+ error_message 582 else: 583 # -- FIRST EXCEPTION/FAILURE: 584 statement.store_exception_context(e) 585 statement.error_message = error_message 586 587 def setup_capture(self): 588 if not self.context: 589 self.context = Context(self) 590 self.capture_controller.setup_capture(self.context) 591 592 def start_capture(self): 593 self.capture_controller.start_capture() 594 595 def stop_capture(self): 596 self.capture_controller.stop_capture() 597 598 def teardown_capture(self): 599 self.capture_controller.teardown_capture() 600 601 def run_model(self, features=None): 602 # pylint: disable=too-many-branches 603 if not self.context: 604 self.context = Context(self) 605 if self.step_registry is None: 606 self.step_registry = the_step_registry 607 if features is None: 608 features = self.features 609 610 # -- ENSURE: context.execute_steps() works in weird cases (hooks, ...) 611 context = self.context 612 self.hook_failures = 0 613 self.setup_capture() 614 self.run_hook("before_all", context) 615 616 run_feature = not self.aborted 617 failed_count = 0 618 undefined_steps_initial_size = len(self.undefined_steps) 619 for feature in features: 620 if run_feature: 621 try: 622 self.feature = feature 623 for formatter in self.formatters: 624 formatter.uri(feature.filename) 625 626 failed = feature.run(self) 627 if failed: 628 failed_count += 1 629 if self.config.stop or self.aborted: 630 # -- FAIL-EARLY: After first failure. 631 run_feature = False 632 except KeyboardInterrupt: 633 self.aborted = True 634 failed_count += 1 635 run_feature = False 636 637 # -- ALWAYS: Report run/not-run feature to reporters. 638 # REQUIRED-FOR: Summary to keep track of untested features. 639 for reporter in self.config.reporters: 640 reporter.feature(feature) 641 642 # -- AFTER-ALL: 643 # pylint: disable=protected-access, broad-except 644 cleanups_failed = False 645 self.run_hook("after_all", self.context) 646 try: 647 self.context._do_cleanups() # Without dropping the last context layer. 648 except Exception: 649 cleanups_failed = True 650 651 if self.aborted: 652 print("\nABORTED: By user.") 653 for formatter in self.formatters: 654 formatter.close() 655 for reporter in self.config.reporters: 656 reporter.end() 657 658 failed = ((failed_count > 0) or self.aborted or (self.hook_failures > 0) 659 or (len(self.undefined_steps) > undefined_steps_initial_size) 660 or cleanups_failed) 661 # XXX-MAYBE: or context.failed) 662 return failed 663 664 def run(self): 665 """ 666 Implements the run method by running the model. 667 """ 668 self.context = Context(self) 669 return self.run_model() 670 671 672class Runner(ModelRunner): 673 """ 674 Standard test runner for behave: 675 676 * setup paths 677 * loads environment hooks 678 * loads step definitions 679 * select feature files, parses them and creates model (elements) 680 """ 681 def __init__(self, config): 682 super(Runner, self).__init__(config) 683 self.path_manager = PathManager() 684 self.base_dir = None 685 686 687 def setup_paths(self): 688 # pylint: disable=too-many-branches, too-many-statements 689 if self.config.paths: 690 if self.config.verbose: 691 print("Supplied path:", \ 692 ", ".join('"%s"' % path for path in self.config.paths)) 693 first_path = self.config.paths[0] 694 if hasattr(first_path, "filename"): 695 # -- BETTER: isinstance(first_path, FileLocation): 696 first_path = first_path.filename 697 base_dir = first_path 698 if base_dir.startswith("@"): 699 # -- USE: behave @features.txt 700 base_dir = base_dir[1:] 701 file_locations = self.feature_locations() 702 if file_locations: 703 base_dir = os.path.dirname(file_locations[0].filename) 704 base_dir = os.path.abspath(base_dir) 705 706 # supplied path might be to a feature file 707 if os.path.isfile(base_dir): 708 if self.config.verbose: 709 print("Primary path is to a file so using its directory") 710 base_dir = os.path.dirname(base_dir) 711 else: 712 if self.config.verbose: 713 print('Using default path "./features"') 714 base_dir = os.path.abspath("features") 715 716 # Get the root. This is not guaranteed to be "/" because Windows. 717 root_dir = path_getrootdir(base_dir) 718 new_base_dir = base_dir 719 steps_dir = self.config.steps_dir 720 environment_file = self.config.environment_file 721 722 while True: 723 if self.config.verbose: 724 print("Trying base directory:", new_base_dir) 725 726 if os.path.isdir(os.path.join(new_base_dir, steps_dir)): 727 break 728 if os.path.isfile(os.path.join(new_base_dir, environment_file)): 729 break 730 if new_base_dir == root_dir: 731 break 732 733 new_base_dir = os.path.dirname(new_base_dir) 734 735 if new_base_dir == root_dir: 736 if self.config.verbose: 737 if not self.config.paths: 738 print('ERROR: Could not find "%s" directory. '\ 739 'Please specify where to find your features.' % \ 740 steps_dir) 741 else: 742 print('ERROR: Could not find "%s" directory in your '\ 743 'specified path "%s"' % (steps_dir, base_dir)) 744 745 message = 'No %s directory in %r' % (steps_dir, base_dir) 746 raise ConfigError(message) 747 748 base_dir = new_base_dir 749 self.config.base_dir = base_dir 750 751 for dirpath, dirnames, filenames in os.walk(base_dir): 752 if [fn for fn in filenames if fn.endswith(".feature")]: 753 break 754 else: 755 if self.config.verbose: 756 if not self.config.paths: 757 print('ERROR: Could not find any "<name>.feature" files. '\ 758 'Please specify where to find your features.') 759 else: 760 print('ERROR: Could not find any "<name>.feature" files '\ 761 'in your specified path "%s"' % base_dir) 762 raise ConfigError('No feature files in %r' % base_dir) 763 764 self.base_dir = base_dir 765 self.path_manager.add(base_dir) 766 if not self.config.paths: 767 self.config.paths = [base_dir] 768 769 if base_dir != os.getcwd(): 770 self.path_manager.add(os.getcwd()) 771 772 def before_all_default_hook(self, context): 773 """ 774 Default implementation for :func:`before_all()` hook. 775 Setup the logging subsystem based on the configuration data. 776 """ 777 # pylint: disable=no-self-use 778 context.config.setup_logging() 779 780 def load_hooks(self, filename=None): 781 filename = filename or self.config.environment_file 782 hooks_path = os.path.join(self.base_dir, filename) 783 if os.path.exists(hooks_path): 784 exec_file(hooks_path, self.hooks) 785 786 if "before_all" not in self.hooks: 787 self.hooks["before_all"] = self.before_all_default_hook 788 789 def load_step_definitions(self, extra_step_paths=None): 790 if extra_step_paths is None: 791 extra_step_paths = [] 792 # -- Allow steps to import other stuff from the steps dir 793 # NOTE: Default matcher can be overridden in "environment.py" hook. 794 steps_dir = os.path.join(self.base_dir, self.config.steps_dir) 795 step_paths = [steps_dir] + list(extra_step_paths) 796 load_step_modules(step_paths) 797 798 def feature_locations(self): 799 return collect_feature_locations(self.config.paths) 800 801 def run(self): 802 with self.path_manager: 803 self.setup_paths() 804 return self.run_with_paths() 805 806 def run_with_paths(self): 807 self.context = Context(self) 808 self.load_hooks() 809 self.load_step_definitions() 810 811 # -- ENSURE: context.execute_steps() works in weird cases (hooks, ...) 812 # self.setup_capture() 813 # self.run_hook("before_all", self.context) 814 815 # -- STEP: Parse all feature files (by using their file location). 816 feature_locations = [filename for filename in self.feature_locations() 817 if not self.config.exclude(filename)] 818 features = parse_features(feature_locations, language=self.config.lang) 819 self.features.extend(features) 820 821 # -- STEP: Run all features. 822 stream_openers = self.config.outputs 823 self.formatters = make_formatters(self.config, stream_openers) 824 return self.run_model() 825