Coverage for Applications/PyCharm.app/Contents/plugins/python/helpers/pycharm/_jb_runner_tools.py: 36%

201 statements  

« prev     ^ index     » next       coverage.py v7.3.1, created at 2023-09-19 10:04 +0530

1# coding=utf-8 

2""" 

3Tools to implement runners (https://confluence.jetbrains.com/display/~link/PyCharm+test+runners+protocol) 

4""" 

5import os 

6import re 

7import sys 

8from collections import OrderedDict 

9 

10import _jb_utils 

11from teamcity import teamcity_presence_env_var, messages 

12 

13# Some runners need it to "detect" TC and start protocol 

14if teamcity_presence_env_var not in os.environ: 

15 os.environ[teamcity_presence_env_var] = "LOCAL" 

16 

17# Providing this env variable disables output buffering. 

18# anything sent to stdout/stderr goes to IDE directly, not after test is over like it is done by default. 

19# out and err are not in sync, so output may go to wrong test 

20JB_DISABLE_BUFFERING = "JB_DISABLE_BUFFERING" in os.environ 

21# getcwd resolves symlinks, but PWD is not supported by some shells 

22PROJECT_DIR = os.getenv('PWD', os.getcwd()) 

23 

24 

25def _parse_parametrized(part): 

26 """ 

27 

28 Support nose generators / pytest parameters and other functions that provides names like foo(1,2) 

29 Until https://github.com/JetBrains/teamcity-messages/issues/121, all such tests are provided 

30 with parentheses. 

31  

32 Tests with docstring are reported in similar way but they have space before parenthesis and should be ignored 

33 by this function 

34  

35 """ 

36 match = re.match("^([^\\s)(]+)(\\(.+\\))$", part) 

37 if not match: 

38 return [part] 

39 else: 

40 return [match.group(1), match.group(2)] 

41 

42 

43class _TreeManagerHolder(object): 

44 def __init__(self): 

45 self.parallel = "JB_USE_PARALLEL_TREE_MANAGER" in os.environ 

46 self.offset = 0 

47 self._manager_imp = None 

48 

49 @property 

50 def manager(self): 

51 if not self._manager_imp: 

52 self._fill_manager() 

53 return self._manager_imp 

54 

55 def _fill_manager(self): 

56 if self.parallel: 

57 from _jb_parallel_tree_manager import ParallelTreeManager 

58 self._manager_imp = ParallelTreeManager(self.offset) 

59 else: 

60 from _jb_serial_tree_manager import SerialTreeManager 

61 self._manager_imp = SerialTreeManager(self.offset) 

62 

63 

64_TREE_MANAGER_HOLDER = _TreeManagerHolder() 

65 

66 

67def set_parallel_mode(): 

68 _TREE_MANAGER_HOLDER.parallel = True 

69 

70 

71def is_parallel_mode(): 

72 return _TREE_MANAGER_HOLDER.parallel 

73 

74 

75# Monkeypatching TC 

76_old_service_messages = messages.TeamcityServiceMessages 

77 

78PARSE_FUNC = None 

79 

80 

81class NewTeamcityServiceMessages(_old_service_messages): 

82 _latest_subtest_result = None 

83 # [full_test_name] = (test_name, node_id, parent_node_id) 

84 _test_suites = OrderedDict() 

85 INSTANCE = None 

86 

87 def __init__(self, *args, **kwargs): 

88 super(NewTeamcityServiceMessages, self).__init__(*args, **kwargs) 

89 NewTeamcityServiceMessages.INSTANCE = self 

90 

91 def message(self, messageName, **properties): 

92 if messageName in {"enteredTheMatrix", "testCount"}: 

93 if "_jb_do_not_call_enter_matrix" not in os.environ: 

94 _old_service_messages.message(self, messageName, **properties) 

95 return 

96 

97 full_name = properties["name"] 

98 try: 

99 # Report directory so Java site knows which folder to resolve names against 

100 

101 # tests with docstrings are reported in format "test.name (some test here)". 

102 # text should be part of name, but not location. 

103 possible_location = str(full_name) 

104 loc = possible_location.find("(") 

105 if loc > 0: 

106 possible_location = possible_location[:loc].strip() 

107 properties["locationHint"] = "python<{0}>://{1}".format(PROJECT_DIR, 

108 possible_location) 

109 except KeyError: 

110 # If message does not have name, then it is not test 

111 # Simply pass it 

112 _old_service_messages.message(self, messageName, **properties) 

113 return 

114 

115 current, parent = _TREE_MANAGER_HOLDER.manager.get_node_ids(full_name) 

116 if not current and not parent: 

117 return 

118 # Shortcut for name 

119 try: 

120 properties["name"] = str(full_name).split(".")[-1] 

121 except IndexError: 

122 pass 

123 

124 properties["nodeId"] = str(current) 

125 properties["parentNodeId"] = str(parent) 

126 

127 is_test = messageName == "testStarted" 

128 if messageName == "testSuiteStarted" or is_test: 

129 self._test_suites[full_name] = (full_name, current, parent, is_test) 

130 _old_service_messages.message(self, messageName, **properties) 

131 

132 def _test_to_list(self, test_name): 

133 """ 

134 Splits test name to parts to use it as list. 

135 It most cases dot is used, but runner may provide custom function 

136 """ 

137 parts = test_name.split(".") 

138 result = [] 

139 for part in parts: 

140 result += _parse_parametrized(part) 

141 return result 

142 

143 def _fix_setup_teardown_name(self, test_name): 

144 """ 

145 

146 Hack to rename setup and teardown methods to much real python signatures 

147 """ 

148 try: 

149 return {"test setup": "setUpClass", "test teardown": "tearDownClass"}[test_name] 

150 except KeyError: 

151 return test_name 

152 

153 # Blocks are used for 2 cases now: 

154 # 1) Unittest subtests (only closed, opened by subTestBlockOpened) 

155 # 2) setup/teardown (does not work, see https://github.com/JetBrains/teamcity-messages/issues/114) 

156 # def blockOpened(self, name, flowId=None): 

157 # self.testStarted(".".join(TREE_MANAGER.current_branch + [self._fix_setup_teardown_name(name)])) 

158 

159 def blockClosed(self, name, flowId=None): 

160 

161 # If _latest_subtest_result is not set or does not exist we closing setup method, not a subtest 

162 try: 

163 if not self._latest_subtest_result: 

164 return 

165 except AttributeError: 

166 return 

167 

168 # closing subtest 

169 test_name = ".".join(_TREE_MANAGER_HOLDER.manager.current_branch) 

170 if self._latest_subtest_result in {"Failure", "Error"}: 

171 self.testFailed(test_name) 

172 if self._latest_subtest_result == "Skip": 

173 self.testIgnored(test_name) 

174 

175 self.testFinished(test_name) 

176 self._latest_subtest_result = None 

177 

178 def subTestBlockOpened(self, name, subTestResult, flowId=None): 

179 self.testStarted(".".join(_TREE_MANAGER_HOLDER.manager.current_branch + [name])) 

180 self._latest_subtest_result = subTestResult 

181 

182 def testStarted(self, testName, captureStandardOutput=None, flowId=None, is_suite=False, metainfo=None): 

183 test_name_as_list = self._test_to_list(testName) 

184 testName = ".".join(test_name_as_list) 

185 

186 def _write_start_message(): 

187 # testName, captureStandardOutput, flowId 

188 args = {"name": testName, "captureStandardOutput": captureStandardOutput, "metainfo": metainfo} 

189 if is_suite: 

190 self.message("testSuiteStarted", **args) 

191 else: 

192 self.message("testStarted", **args) 

193 

194 commands = _TREE_MANAGER_HOLDER.manager.level_opened(self._test_to_list(testName), _write_start_message) 

195 if commands: 

196 self.do_commands(commands) 

197 self.testStarted(testName, captureStandardOutput, metainfo=metainfo) 

198 

199 def testFailed(self, testName, message='', details='', flowId=None, comparison_failure=None): 

200 testName = ".".join(self._test_to_list(testName)) 

201 _old_service_messages.testFailed(self, testName, message, details, comparison_failure=comparison_failure) 

202 

203 def testFinished(self, testName, testDuration=None, flowId=None, is_suite=False): 

204 test_parts = self._test_to_list(testName) 

205 testName = ".".join(test_parts) 

206 

207 def _write_finished_message(): 

208 # testName, captureStandardOutput, flowId 

209 current, parent = _TREE_MANAGER_HOLDER.manager.get_node_ids(testName) 

210 if not current and not parent: 

211 return 

212 args = {"nodeId": current, "parentNodeId": parent, "name": testName} 

213 

214 # TODO: Doc copy/paste with parent, extract 

215 if testDuration is not None: 

216 duration_ms = testDuration.days * 86400000 + \ 

217 testDuration.seconds * 1000 + \ 

218 int(testDuration.microseconds / 1000) 

219 args["duration"] = str(duration_ms) 

220 

221 if is_suite: 

222 del self._test_suites[testName] 

223 if is_parallel_mode(): 

224 del args["duration"] 

225 self.message("testSuiteFinished", **args) 

226 else: 

227 self.message("testFinished", **args) 

228 del self._test_suites[testName] 

229 

230 commands = _TREE_MANAGER_HOLDER.manager.level_closed( 

231 self._test_to_list(testName), _write_finished_message) 

232 if commands: 

233 self.do_commands(commands) 

234 self.testFinished(testName, testDuration) 

235 

236 def do_commands(self, commands): 

237 """ 

238 

239 Executes commands, returned by level_closed and level_opened 

240 """ 

241 for command, test in commands: 

242 test_name = ".".join(test) 

243 # By executing commands we open or close suites(branches) since tests(leaves) are always reported by runner 

244 if command == "open": 

245 self.testStarted(test_name, is_suite=True) 

246 else: 

247 self.testFinished(test_name, is_suite=True) 

248 

249 def _repose_suite_closed(self, suite): 

250 name = suite.full_name 

251 if name: 

252 _old_service_messages.testSuiteFinished(self, ".".join(name)) 

253 for child in suite.children.values(): 

254 self._repose_suite_closed(child) 

255 

256 def close_suites(self): 

257 # Go in reverse order and close all suites 

258 for (test_suite, node_id, parent_node_id, is_test) in \ 

259 reversed(list(self._test_suites.values())): 

260 # suits are explicitly closed, but if test can't been finished, it is skipped 

261 message = "testIgnored" if is_test else "testSuiteFinished" 

262 _old_service_messages.message(self, message, **{ 

263 "name": test_suite, 

264 "nodeId": str(node_id), 

265 "parentNodeId": str(parent_node_id) 

266 }) 

267 self._test_suites = OrderedDict() 

268 

269 

270messages.TeamcityServiceMessages = NewTeamcityServiceMessages 

271 

272 

273# Monkeypatched 

274 

275def jb_patch_separator(targets, fs_glue, python_glue, fs_to_python_glue): 

276 """ 

277 Converts python target if format "/path/foo.py::parts.to.python" provided by Java to  

278 python specific format 

279 

280 :param targets: list of dot-separated targets 

281 :param fs_glue: how to glue fs parts of target. I.e.: module "eggs" in "spam" package is "spam[fs_glue]eggs" 

282 :param python_glue: how to glue python parts (glue between class and function etc) 

283 :param fs_to_python_glue: between last fs-part and first python part 

284 :return: list of targets with patched separators 

285 """ 

286 if not targets: 

287 return [] 

288 

289 def _patch_target(target): 

290 # /path/foo.py::parts.to.python 

291 match = re.match("^(:?(.+)[.]py::)?(.+)$", target) 

292 assert match, "unexpected string: {0}".format(target) 

293 fs_part = match.group(2) 

294 python_part = match.group(3).replace(".", python_glue) 

295 if fs_part: 

296 return fs_part.replace("/", fs_glue) + fs_to_python_glue + python_part 

297 else: 

298 return python_part 

299 

300 return map(_patch_target, targets) 

301 

302 

303def jb_start_tests(): 

304 """ 

305 Parses arguments, starts protocol and fixes syspath and returns tuple of arguments 

306 """ 

307 path, targets, additional_args = parse_arguments() 

308 start_protocol() 

309 return path, targets, additional_args 

310 

311 

312def jb_finish_tests(): 

313 # To be called before process exist to close all suites 

314 instance = NewTeamcityServiceMessages.INSTANCE 

315 

316 # instance may not be set if you run like pytest --version 

317 if instance: 

318 instance.close_suites() 

319 

320 

321def start_protocol(): 

322 properties = {"durationStrategy": "manual"} if is_parallel_mode() else dict() 

323 NewTeamcityServiceMessages().message('enteredTheMatrix', **properties) 

324 

325 

326def parse_arguments(): 

327 """ 

328 Parses arguments, fixes syspath and returns tuple of arguments 

329 

330 :return: (string with path or None, list of targets or None, list of additional arguments) 

331 It may return list with only one element (name itself) if name is the same or split names to several parts 

332 """ 

333 # Handle additional args after -- 

334 additional_args = [] 

335 try: 

336 index = sys.argv.index("--") 

337 additional_args = sys.argv[index + 1:] 

338 del sys.argv[index:] 

339 except ValueError: 

340 pass 

341 utils = _jb_utils.VersionAgnosticUtils() 

342 namespace = utils.get_options( 

343 _jb_utils.OptionDescription('--path', 'Path to file or folder to run'), 

344 _jb_utils.OptionDescription('--offset', 'Root node offset'), 

345 _jb_utils.OptionDescription('--target', 'Python target to run', "append")) 

346 del sys.argv[1:] # Remove all args 

347 

348 # PyCharm helpers dir is first dir in sys.path because helper is launched. 

349 # But sys.path should be same as when launched with test runner directly 

350 try: 

351 if os.path.abspath(sys.path[0]) == os.path.abspath( 

352 os.environ["PYCHARM_HELPERS_DIR"]): 

353 path = sys.path.pop(0) 

354 if path not in sys.path: 

355 sys.path.append(path) 

356 except KeyError: 

357 pass 

358 _TREE_MANAGER_HOLDER.offset = int(namespace.offset if namespace.offset else 0) 

359 return namespace.path, namespace.target, additional_args 

360 

361 

362def jb_doc_args(framework_name, args): 

363 """ 

364 Runner encouraged to report its arguments to user with aid of this function 

365 

366 """ 

367 print("Launching {0} with arguments {1} in {2}\n".format(framework_name, 

368 " ".join(args), 

369 PROJECT_DIR))