From 2226dd547fe221e82cdc37578d1bf43efb636035 Mon Sep 17 00:00:00 2001 From: Ilya Konstantinov Date: Mon, 8 Sep 2025 13:19:40 +0000 Subject: [PATCH] PY-58751 Unable to jump to location of "synthetic" test item Merge-request: IJ-MR-174410 Merged-by: Egor Eliseev GitOrigin-RevId: 858bccafb9ae0343fafa5bac4cc0f88c0778898d --- python/helpers/pycharm/_jb_runner_tools.py | 29 +++++++++++++------ python/helpers/pycharm/teamcity/messages.py | 4 +-- .../helpers/pycharm/teamcity/pytest_plugin.py | 14 ++++++--- 3 files changed, 32 insertions(+), 15 deletions(-) diff --git a/python/helpers/pycharm/_jb_runner_tools.py b/python/helpers/pycharm/_jb_runner_tools.py index f47d4a151fdd..02ee58de28fc 100644 --- a/python/helpers/pycharm/_jb_runner_tools.py +++ b/python/helpers/pycharm/_jb_runner_tools.py @@ -130,12 +130,20 @@ class NewTeamcityServiceMessages(_old_service_messages): # tests with docstrings are reported in format "test.name (some test here)". # text should be part of name, but not location. - possible_location = str(full_name) - loc = possible_location.find("(") - if loc > 0: - possible_location = possible_location[:loc].strip() - properties["locationHint"] = "python<{0}>://{1}".format(PROJECT_DIR, - possible_location) + path = properties.get('path') + if not path or path.endswith('.py'): + possible_location = str(full_name) + loc = possible_location.find("(") + if loc > 0: + possible_location = possible_location[:loc].strip() + properties["locationHint"] = "python<{0}>://{1}".format(PROJECT_DIR, possible_location) + else: + # For data-driven tests, we reference the test by file and line number. + location_hint = "file:/" + path + lineno = properties.get('lineno') + if lineno: + location_hint += ":{}".format(lineno) + properties["locationHint"] = location_hint except KeyError: # If message does not have name, then it is not test # Simply pass it @@ -205,13 +213,16 @@ class NewTeamcityServiceMessages(_old_service_messages): self.testStarted(".".join(_TREE_MANAGER_HOLDER.manager.current_branch + [name])) self._latest_subtest_result = subTestResult - def testStarted(self, testName, captureStandardOutput=None, flowId=None, is_suite=False, metainfo=None): + def testStarted(self, testName, captureStandardOutput=None, flowId=None, is_suite=False, metainfo=None, path=None, lineno=None): test_name_as_list = _jb_utils.test_to_list(testName) testName = ".".join(test_name_as_list) def _write_start_message(): # testName, captureStandardOutput, flowId args = {"name": testName, "captureStandardOutput": captureStandardOutput, "metainfo": metainfo} + if path is not None and lineno is not None: + args["path"] = path + args["lineno"] = str(lineno) if is_suite: self.message("testSuiteStarted", **args) else: @@ -220,7 +231,7 @@ class NewTeamcityServiceMessages(_old_service_messages): commands = _TREE_MANAGER_HOLDER.manager.level_opened(test_name_as_list, _write_start_message) if commands: self.do_commands(commands) - self.testStarted(testName, captureStandardOutput, is_suite=is_suite, metainfo=metainfo) + self.testStarted(testName, captureStandardOutput, is_suite=is_suite, metainfo=metainfo, path=path, lineno=lineno) def testFailed(self, testName, message='', details='', flowId=None, comparison_failure=None): testName = ".".join(_jb_utils.test_to_list(testName)) @@ -343,7 +354,7 @@ def jb_patch_targets(targets, fs_glue, old_python_glue, new_python_glue, fs_to_p def jb_patch_separator(targets, fs_glue, python_glue, fs_to_python_glue): """ - Converts python target if format "/path/foo.py::parts.to.python" provided by Java to + Converts python target if format "/path/foo.py::parts.to.python" provided by Java to python-specific format :param targets: list of dot-separated targets diff --git a/python/helpers/pycharm/teamcity/messages.py b/python/helpers/pycharm/teamcity/messages.py index 9ebb3d95714b..8295d4310df4 100644 --- a/python/helpers/pycharm/teamcity/messages.py +++ b/python/helpers/pycharm/teamcity/messages.py @@ -116,12 +116,12 @@ class TeamcityServiceMessages(object): import teamcity.context_managers as cm return cm.testSuite(self, name=name) - def testStarted(self, testName, captureStandardOutput=None, flowId=None, metainfo=None): + def testStarted(self, testName, captureStandardOutput=None, flowId=None, metainfo=None, **properties): """ :param metainfo: Used to pass any payload from test runner to Intellij. See IDEA-176950 """ - self.message('testStarted', name=testName, captureStandardOutput=captureStandardOutput, flowId=flowId, metainfo=metainfo) + self.message('testStarted', name=testName, captureStandardOutput=captureStandardOutput, flowId=flowId, metainfo=metainfo, **properties) def testFinished(self, testName, testDuration=None, flowId=None): if testDuration is not None: diff --git a/python/helpers/pycharm/teamcity/pytest_plugin.py b/python/helpers/pycharm/teamcity/pytest_plugin.py index 2e5ca3775f58..a79ca068da0c 100644 --- a/python/helpers/pycharm/teamcity/pytest_plugin.py +++ b/python/helpers/pycharm/teamcity/pytest_plugin.py @@ -94,6 +94,7 @@ class EchoTeamCityMessages(object): output_handler = TeamCityMessagesPrinter(context_manager=context_manager) self.teamcity = TeamcityServiceMessages(output_handler=output_handler) self.test_start_reported_mark = set() + self.rootdir = None self.current_test_item = None self.max_reported_output_size = 1 * 1024 * 1024 @@ -178,28 +179,33 @@ class EchoTeamCityMessages(object): self.report_test_finished(test_id) def pytest_collection_finish(self, session): + if hasattr(session.config, 'rootpath'): # pytest>=6 + self.rootdir = str(session.config.rootpath) + else: + self.rootdir = str(session.config.rootdir) self.teamcity.testCount(len(session.items)) def pytest_runtest_logstart(self, nodeid, location): # test name fetched from location passed as metainfo to PyCharm # it will be used to run specific test # See IDEA-176950, PY-31836 - test_name = location[2] + path, lineno, test_name = location if test_name: test_name = str(test_name).split(".")[-1] - self.ensure_test_start_reported(self.format_test_id(nodeid, location), test_name) + path = os.path.join(self.rootdir, path) + self.ensure_test_start_reported(self.format_test_id(nodeid, location), test_name, path=path, lineno=lineno + 1) def pytest_runtest_protocol(self, item): self.current_test_item = item return None # continue to next hook - def ensure_test_start_reported(self, test_id, metainfo=None): + def ensure_test_start_reported(self, test_id, metainfo=None, path=None, lineno=None): if test_id not in self.test_start_reported_mark: if self.output_capture_enabled: capture_standard_output = "false" else: capture_standard_output = "true" - self.teamcity.testStarted(test_id, flowId=test_id, captureStandardOutput=capture_standard_output, metainfo=metainfo) + self.teamcity.testStarted(test_id, flowId=test_id, captureStandardOutput=capture_standard_output, metainfo=metainfo, path=path, lineno=lineno) self.test_start_reported_mark.add(test_id) def report_has_output(self, report):