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