1# coding=utf-8 2""" 3Tools for running BDD frameworks in python. 4You probably need to extend BddRunner (see its doc). 5 6You may also need "get_what_to_run_by_env" that gets folder (current or passed as first argument) 7""" 8import os 9import time 10import abc 11import tcmessages 12from _jb_utils import VersionAgnosticUtils 13 14__author__ = 'Ilya.Kazakevich' 15 16 17def fix_win_drive(feature_path): 18 """ 19 Workaround to fix issues like http://bugs.python.org/issue7195 on windows. 20 Pass feature dir or file path as argument. 21 This function does nothing on non-windows platforms, so it could be run safely. 22 23 :param feature_path: path to feature (c:/fe.feature or /my/features) 24 """ 25 current_disk = (os.path.splitdrive(os.getcwd()))[0] 26 feature_disk = (os.path.splitdrive(feature_path))[0] 27 if current_disk and feature_disk and current_disk != feature_disk: 28 os.chdir(feature_disk) 29 30 31def get_what_to_run_by_env(environment): 32 """ 33 :type environment dict 34 :param environment: os.environment (files and folders should be separated with | and passed to PY_STUFF_TO_RUN). 35 Scenarios optionally could be passed as SCENARIOS (names or order numbers, depends on runner) 36 :return: tuple (base_dir, scenarios[], what_to_run(list of feature files or folders))) where dir is current or first argument from env, checking it exists 37 :rtype tuple of (str, iterable) 38 """ 39 if "PY_STUFF_TO_RUN" not in environment: 40 what_to_run = ["."] 41 else: 42 what_to_run = str(environment["PY_STUFF_TO_RUN"]).split("|") 43 44 scenarios = [] 45 if "SCENARIOS" in environment: 46 scenarios = str(environment["SCENARIOS"]).split("|") 47 48 if not what_to_run: 49 what_to_run = ["."] 50 51 for path in what_to_run: 52 assert os.path.exists(path), "{0} does not exist".format(path) 53 54 base_dir = what_to_run[0] 55 if os.path.isfile(what_to_run[0]): 56 base_dir = os.path.dirname(what_to_run[0]) # User may point to the file directly 57 return base_dir, scenarios, what_to_run 58 59 60def get_location(base_dir, location_file, location_line): 61 """ 62 Generates location that PyCharm resolves to file 63 :param base_dir: base directory to resolve relative path against 64 :param location_file: path to file 65 :param location_line: line number 66 """ 67 my_file = str(location_file).lstrip("/\\") 68 return "file:///{0}:{1}".format(os.path.normpath(os.path.join(base_dir, my_file)), location_line) 69 70 71class BddRunner(object): 72 """ 73 Extends this class, implement abstract methods and use its API to implement new BDD frameworks. 74 Call "run()" to launch it. 75 This class does the following: 76 * Gets features to run (using "_get_features_to_run()") and calculates steps in it 77 * Reports steps to Intellij or TC 78 * Calls "_run_tests()" where *you* should install all hooks you need into your BDD and use "self._" functions 79 to report tests and features. It actually wraps tcmessages but adds some stuff like duration count etc 80 :param base_dir: 81 """ 82 __metaclass__ = abc.ABCMeta 83 84 def __init__(self, base_dir): 85 """ 86 :type base_dir str 87 :param base_dir base directory of your project 88 """ 89 super(BddRunner, self).__init__() 90 self.tc_messages = tcmessages.TeamcityServiceMessages() 91 """ 92 tcmessages TeamCity/Intellij test API. See TeamcityServiceMessages 93 """ 94 self.__base_dir = base_dir 95 self.__last_test_start_time = None # TODO: Doc when use 96 self.__last_test_name = None 97 98 def run(self): 99 """" 100 Runs runner. To be called right after constructor. 101 """ 102 number_of_tests = self._get_number_of_tests() 103 self.tc_messages.testCount(number_of_tests) 104 self.tc_messages.testMatrixEntered() 105 if number_of_tests == 0: # Nothing to run, so no need to report even feature/scenario start. (See PY-13623) 106 return 107 self._run_tests() 108 109 def __gen_location(self, location): 110 """ 111 Generates location in format, supported by tcmessages 112 :param location object with "file" (relative to base_dir) and "line" fields. 113 :return: location in format file:line (as supported in tcmessages) 114 """ 115 return get_location(self.__base_dir, location.file, location.line) 116 117 def _test_undefined(self, test_name, location): 118 """ 119 Mark test as undefined 120 :param test_name: name of test 121 :type test_name str 122 :param location its location 123 124 """ 125 if test_name != self.__last_test_name: 126 self._test_started(test_name, location) 127 self._test_failed(test_name, message="Test undefined", details="Please define test") 128 129 def _test_skipped(self, test_name, reason, location): 130 """ 131 Mark test as skipped 132 :param test_name: name of test 133 :param reason: why test was skipped 134 :type reason str 135 :type test_name str 136 :param location its location 137 138 """ 139 if test_name != self.__last_test_name: 140 self._test_started(test_name, location) 141 self.tc_messages.testIgnored(test_name, "Skipped: {0}".format(reason)) 142 self.__last_test_name = None 143 pass 144 145 146 def _test_failed(self, name, message, details, duration=None): 147 """ 148 Report test failure 149 :param name: test name 150 :type name str 151 :param message: failure message 152 :type message basestring 153 :param details: failure details (probably stacktrace) 154 :type details str 155 :param duration how long test took 156 :type duration int 157 """ 158 self.tc_messages.testFailed(name, 159 message=VersionAgnosticUtils().to_unicode(message), 160 details=details, 161 duration=duration) 162 self.__last_test_name = None 163 164 def _test_passed(self, name, duration=None): 165 """ 166 Reports test passed 167 :param name: test name 168 :type name str 169 :param duration: time (in seconds) test took. Pass None if you do not know (we'll try to calculate it) 170 :type duration int 171 :return: 172 """ 173 duration_to_report = duration 174 if self.__last_test_start_time and not duration: # And not provided 175 duration_to_report = int(time.time() - self.__last_test_start_time) 176 self.tc_messages.testFinished(name, duration=int(duration_to_report)) 177 self.__last_test_start_time = None 178 self.__last_test_name = None 179 180 def _test_started(self, name, location): 181 """ 182 Reports test launched 183 :param name: test name 184 :param location object with "file" (relative to base_dir) and "line" fields. 185 :type name str 186 """ 187 self.__last_test_start_time = time.time() 188 self.__last_test_name = name 189 self.tc_messages.testStarted(name, self.__gen_location(location)) 190 191 def _feature_or_scenario(self, is_started, name, location): 192 """ 193 Reports feature or scenario launched or stopped 194 :param is_started: started or finished? 195 :type is_started bool 196 :param name: scenario or feature name 197 :param location object with "file" (relative to base_dir) and "line" fields. 198 """ 199 if is_started: 200 self.tc_messages.testSuiteStarted(name, self.__gen_location(location)) 201 else: 202 self.tc_messages.testSuiteFinished(name) 203 204 def _background(self, is_started, location): 205 """ 206 Reports background or stopped 207 :param is_started: started or finished? 208 :type is_started bool 209 :param location object with "file" (relative to base_dir) and "line" fields. 210 """ 211 self._feature_or_scenario(is_started, "Background", location) 212 213 def _get_number_of_tests(self): 214 """" 215 Gets number of tests using "_get_features_to_run()" to obtain number of features to calculate. 216 Supports backgrounds as well. 217 :return number of steps 218 :rtype int 219 """ 220 num_of_steps = 0 221 for feature in self._get_features_to_run(): 222 if feature.background: 223 num_of_steps += len(list(feature.background.steps)) * len(list(feature.scenarios)) 224 for scenario in feature.scenarios: 225 num_of_steps += len(list(scenario.steps)) 226 return num_of_steps 227 228 @abc.abstractmethod 229 def _get_features_to_run(self): 230 """ 231 Implement it! Return list of features to run. Each "feature" should have "scenarios". 232 Each "scenario" should have "steps". Each "feature" may have "background" and each "background" should have 233 "steps". Duck typing. 234 :rtype list 235 :returns list of features 236 """ 237 return [] 238 239 @abc.abstractmethod 240 def _run_tests(self): 241 """ 242 Implement it! It should launch tests using your BDD. Use "self._" functions to report results. 243 """ 244 pass 245