1# coding=utf-8
2"""
3BDD lettuce framework runner
4TODO: Support other params (like tags) as well.
5Supports only 2 params now: folder to search "features" for or file and "-s scenario_index"
6"""
7import inspect
8import optparse
9import os
10import _bdd_utils
11
12__author__ = 'Ilya.Kazakevich'
13from lettuce.exceptions import ReasonToFail
14import lettuce
15from lettuce import core
16
17
18class _LettuceRunner(_bdd_utils.BddRunner):
19    """
20    Lettuce runner (BddRunner for lettuce)
21    """
22
23    def __init__(self, base_dir, what_to_run, scenarios, options):
24        """
25
26        :param scenarios scenario numbers to run
27        :type scenarios list
28        :param base_dir base directory to run tests in
29        :type base_dir: str
30        :param what_to_run folder or file to run
31        :type options optparse.Values
32        :param options optparse options passed by user
33        :type what_to_run str
34
35        """
36        super(_LettuceRunner, self).__init__(base_dir)
37        # TODO: Copy/Paste with lettuce.bin, need to reuse somehow
38
39        # Delete args that do not exist in constructor
40        args_to_pass = options.__dict__
41        runner_args = inspect.getargspec(lettuce.Runner.__init__)[0]
42        unknown_args = set(args_to_pass.keys()) - set(runner_args)
43        map(args_to_pass.__delitem__, unknown_args)
44
45        # Tags is special case and need to be preprocessed
46        self.__tags = None  # Store tags in field
47        if 'tags' in args_to_pass.keys() and args_to_pass['tags']:
48            args_to_pass['tags'] = [tag.strip('@') for tag in args_to_pass['tags']]
49            self.__tags = set(args_to_pass['tags'])
50
51        # Special cases we pass directly
52        args_to_pass['base_path'] = what_to_run
53        args_to_pass['scenarios'] = ",".join(scenarios)
54
55        self.__runner = lettuce.Runner(**args_to_pass)
56
57    def _get_features_to_run(self):
58        super(_LettuceRunner, self)._get_features_to_run()
59        features = []
60        if self.__runner.single_feature:  # We need to run one and only one feature
61            features = [core.Feature.from_file(self.__runner.single_feature)]
62        else:
63            # Find all features in dir
64            for feature_file in self.__runner.loader.find_feature_files():
65                feature = core.Feature.from_file(feature_file)
66                assert isinstance(feature, core.Feature), feature
67                if feature.scenarios:
68                    features.append(feature)
69
70        # Choose only selected scenarios
71        if self.__runner.scenarios:
72            for feature in features:
73                filtered_feature_scenarios = []
74                for index in [i - 1 for i in self.__runner.scenarios]:  # decrease index by 1
75                    if index < len(feature.scenarios):
76                        filtered_feature_scenarios.append(feature.scenarios[index])
77                feature.scenarios = filtered_feature_scenarios
78
79        # Filter out tags TODO: Share with behave_runner.py#__filter_scenarios_by_args
80        if self.__tags:
81            for feature in features:
82                feature.scenarios = filter(lambda s: set(s.tags) & self.__tags, feature.scenarios)
83        return features
84
85    def _run_tests(self):
86        super(_LettuceRunner, self)._run_tests()
87        self.__install_hooks()
88        self.__runner.run()
89
90    def __step(self, is_started, step):
91        """
92        Reports step start / stop
93        :type step core.Step
94        :param step: step
95        """
96        test_name = step.sentence
97        if is_started:
98            self._test_started(test_name, step.described_at)
99        elif step.passed:
100            self._test_passed(test_name)
101        elif step.failed:
102            reason = step.why
103            assert isinstance(reason, ReasonToFail), reason
104            self._test_failed(test_name, message=reason.exception.message, details=reason.traceback)
105        elif step.has_definition:
106            self._test_skipped(test_name, "In lettuce, we do know the reason", step.described_at)
107        else:
108            self._test_undefined(test_name, step.described_at)
109
110    def __install_hooks(self):
111        """
112        Installs required hooks
113        """
114
115        # Install hooks
116        lettuce.before.each_feature(
117            lambda f: self._feature_or_scenario(True, f.name, f.described_at))
118        lettuce.after.each_feature(
119            lambda f: self._feature_or_scenario(False, f.name, f.described_at))
120
121        try:
122            lettuce.before.each_outline(lambda s, o: self.__outline(True, s, o))
123            lettuce.after.each_outline(lambda s, o: self.__outline(False, s, o))
124        except AttributeError:
125            import sys
126            sys.stderr.write("WARNING: your lettuce version is outdated and does not support outline hooks. "
127                             "Outline scenarios may not work. Consider upgrade to latest lettuce (0.22 at least)")
128
129        lettuce.before.each_scenario(
130            lambda s: self.__scenario(True, s))
131        lettuce.after.each_scenario(
132            lambda s: self.__scenario(False, s))
133
134        lettuce.before.each_background(
135            lambda b, *args: self._background(True, b.feature.described_at))
136        lettuce.after.each_background(
137            lambda b, *args: self._background(False, b.feature.described_at))
138
139        lettuce.before.each_step(lambda s: self.__step(True, s))
140        lettuce.after.each_step(lambda s: self.__step(False, s))
141
142    def __outline(self, is_started, scenario, outline):
143        """
144        report outline is started or finished
145        """
146        outline_description = ["{0}: {1}".format(k, v) for k, v in outline.items()]
147        self._feature_or_scenario(is_started, "Outline {0}".format(outline_description), scenario.described_at)
148
149    def __scenario(self, is_started, scenario):
150        """
151        Reports scenario launched
152        :type scenario core.Scenario
153        :param scenario: scenario
154        """
155        self._feature_or_scenario(is_started, scenario.name, scenario.described_at)
156
157
158def _get_args():
159    """
160    Get options passed by user
161
162    :return: tuple (options, args), see optparse
163    """
164    # TODO: Copy/Paste with lettuce.bin, need to reuse somehow
165    parser = optparse.OptionParser()
166    parser.add_option("-v", "--verbosity",
167                      dest="verbosity",
168                      default=0,  # We do not need verbosity due to GUI we use (although user may override it)
169                      help='The verbosity level')
170
171    parser.add_option("-s", "--scenarios",
172                      dest="scenarios",
173                      default=None,
174                      help='Comma separated list of scenarios to run')
175
176    parser.add_option("-t", "--tag",
177                      dest="tags",
178                      default=None,
179                      action='append',
180                      help='Tells lettuce to run the specified tags only; '
181                           'can be used multiple times to define more tags'
182                           '(prefixing tags with "-" will exclude them and '
183                           'prefixing with "~" will match approximate words)')
184
185    parser.add_option("-r", "--random",
186                      dest="random",
187                      action="store_true",
188                      default=False,
189                      help="Run scenarios in a more random order to avoid interference")
190
191    parser.add_option("--with-xunit",
192                      dest="enable_xunit",
193                      action="store_true",
194                      default=False,
195                      help='Output JUnit XML test results to a file')
196
197    parser.add_option("--xunit-file",
198                      dest="xunit_file",
199                      default=None,
200                      type="string",
201                      help='Write JUnit XML to this file. Defaults to '
202                           'lettucetests.xml')
203
204    parser.add_option("--with-subunit",
205                      dest="enable_subunit",
206                      action="store_true",
207                      default=False,
208                      help='Output Subunit test results to a file')
209
210    parser.add_option("--subunit-file",
211                      dest="subunit_filename",
212                      default=None,
213                      help='Write Subunit data to this file. Defaults to '
214                           'subunit.bin')
215
216    parser.add_option("--failfast",
217                      dest="failfast",
218                      default=False,
219                      action="store_true",
220                      help='Stop running in the first failure')
221
222    parser.add_option("--pdb",
223                      dest="auto_pdb",
224                      default=False,
225                      action="store_true",
226                      help='Launches an interactive debugger upon error')
227    return parser.parse_args()
228
229
230if __name__ == "__main__":
231    options, args = _get_args()
232    (base_dir, scenarios, what_to_run) = _bdd_utils.get_what_to_run_by_env(os.environ)
233    if len(what_to_run) > 1:
234        raise Exception("Lettuce can't run more than one file now")
235    _bdd_utils.fix_win_drive(what_to_run[0])
236    _LettuceRunner(base_dir, what_to_run[0], scenarios, options).run()