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

1# coding=utf-8

2"""

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

4"""

5 import os

6 import re

7 import sys

8 from collections import OrderedDict

9

10 import _jb_utils

11 from teamcity import teamcity_presence_env_var, messages

12

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

14 if 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

20 JB_DISABLE_BUFFERING = "JB_DISABLE_BUFFERING" in os.environ

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

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

23

24

25 def _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: 37 ↛ 40line 37 didn't jump to line 40 because the condition on line 37 was always true

38 return [part]

39 else:

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

41

42

43 class _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: 56 ↛ 57line 56 didn't jump to line 57 because the condition on line 56 was never true

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

67 def set_parallel_mode():

68 _TREE_MANAGER_HOLDER.parallel = True

69

70

71 def is_parallel_mode():

72 return _TREE_MANAGER_HOLDER.parallel

73

74

75# Monkeypatching TC

76 _old_service_messages = messages.TeamcityServiceMessages

77

78 PARSE_FUNC = None

79

80

81 class 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: 93 ↛ 95line 93 didn't jump to line 95 because the condition on line 93 was always true

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: 105 ↛ 106line 105 didn't jump to line 106 because the condition on line 105 was never true

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: 116 ↛ 117line 116 didn't jump to line 117 because the condition on line 116 was never true

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: 210 ↛ 211line 210 didn't jump to line 211 because the condition on line 210 was never true

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: 215 ↛ 221line 215 didn't jump to line 221 because the condition on line 215 was always true

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: 221 ↛ 222line 221 didn't jump to line 222 because the condition on line 221 was never true

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: 232 ↛ 233line 232 didn't jump to line 233 because the condition on line 232 was never true

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": 244 ↛ 247line 244 didn't jump to line 247 because the condition on line 244 was always true

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

270 messages.TeamcityServiceMessages = NewTeamcityServiceMessages

271

272

273# Monkeypatched

274

275 def jb_patch_targets(targets, fs_glue, old_python_glue, new_python_glue, fs_to_python_glue, python_parts_action=None):

276 """

277 Converts python targets format provided by Java to python-specific format

278

279 :param targets: list of separated by [old_python_glue] or dots targets

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

281 :param new_python_glue: how to glue python parts (glue between class and function etc)

282 :param old_python_glue: which symbols need to be replaced by [new_python_glue]

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

284 :param python_parts_action: additional action for python parts

285 :return: list of targets with patched separators

286 """

287 if not targets:

288 return []

289

290 def _patch_target(target):

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

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

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

294 fs_part = match.group(2)

295 python_part = match.group(3).replace(old_python_glue, new_python_glue) + match.group(4)

296 if python_parts_action is not None:

297 python_part = python_parts_action(fs_part, python_part)

298 if fs_part:

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

300 else:

301 return python_part

302

303 return map(_patch_target, targets)

304

305

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

307 """

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

309 python-specific format

310

311 :param targets: list of dot-separated targets

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

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

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

315 :return: list of targets with patched separators

316 """

317 return jb_patch_targets(targets, fs_glue, '.', python_glue, fs_to_python_glue)

318

319

320 def jb_start_tests():

321 """

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

323 """

324 path, targets, additional_args = parse_arguments()

325 start_protocol()

326 return path, targets, additional_args

327

328

329 def jb_finish_tests():

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

331 instance = NewTeamcityServiceMessages.INSTANCE

332

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

334 if instance:

335 instance.close_suites()

336

337

338 def start_protocol():

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

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

341

342

343 def parse_arguments():

344 """

345 Parses arguments, fixes syspath and returns tuple of arguments

346

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

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

349 """

350 # Handle additional args after --

351 additional_args = []

352 try:

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

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

355 del sys.argv[index:]

356 except ValueError:

357 pass

358 utils = _jb_utils.VersionAgnosticUtils()

359 namespace = utils.get_options(

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

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

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

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

364

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

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

367 try:

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

369 os.environ["PYCHARM_HELPERS_DIR"]):

370 path = sys.path.pop(0)

371 if path not in sys.path:

372 sys.path.append(path)

373 except KeyError:

374 pass

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

376 return namespace.path, namespace.target, additional_args

377

378

379 def jb_doc_args(framework_name, args):

380 """

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

382

383 """

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

385 " ".join(args),

386 PROJECT_DIR))