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()