From 7c7fde5dd0da3df5e0db46e98f6eaeae64a91bbf Mon Sep 17 00:00:00 2001 From: Kayvan Zahiri <123409429+Kayvan-Zahiri@users.noreply.github.com> Date: Sun, 25 Oct 2026 11:40:48 -0700 Subject: [PATCH] fix(pytest): preserve node separators inside parameter IDs --- smart_tests/test_runners/pytest.py | 7 ++++- tests/test_runners/test_pytest.py | 47 ++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/smart_tests/test_runners/pytest.py b/smart_tests/test_runners/pytest.py index edb3c5e0d..5c021d939 100644 --- a/smart_tests/test_runners/pytest.py +++ b/smart_tests/test_runners/pytest.py @@ -84,7 +84,12 @@ def _add_testpaths(lines: Iterable[str]): def _parse_pytest_nodeid(nodeid: str) -> TestPath: - data = nodeid.split("::") + data = nodeid.split("::", 1) + if len(data) > 1: + # Parameter IDs may themselves contain the node separator. + test_name, bracket, parameter_id = data.pop().partition("[") + data.extend(test_name.split("::")) + data[-1] += bracket + parameter_id file = data[0] class_name = _path_to_class_name(file) normalized_file = os.path.normpath(file) diff --git a/tests/test_runners/test_pytest.py b/tests/test_runners/test_pytest.py index efa7f6bdb..91dc8cc2a 100644 --- a/tests/test_runners/test_pytest.py +++ b/tests/test_runners/test_pytest.py @@ -78,6 +78,53 @@ def test_parse_pytest_nodeid(self): {"type": "testcase", "name": "test_func6"}, ]) + @responses.activate + @mock.patch.dict(os.environ, {"SMART_TESTS_TOKEN": CliTestCase.smart_tests_token}) + def test_subset_preserves_parameter_id_containing_separator(self): + nodeid = 'tests/test_mod.py::TestClass::test_value[left::right]' + result = self.cli('subset', 'pytest', '--target', '10%', '--session', self.session, + input=nodeid + '\n\n') + self.assert_success(result) + payload = json.loads(gzip.decompress(self.find_request('/subset').request.body).decode()) + self.assertEqual(payload['testPaths'], [[ + {'type': 'file', 'name': os.path.normpath('tests/test_mod.py')}, + {'type': 'class', 'name': 'tests.test_mod.TestClass'}, + {'type': 'testcase', 'name': 'test_value[left::right]'}, + ]]) + + +class PytestParameterIdTest(TestCase): + def test_function_parameter_id_preserves_separator(self): + self.assertEqual(_parse_pytest_nodeid('tests/test_mod.py::test_value[left::right]'), [ + {'type': 'file', 'name': os.path.normpath('tests/test_mod.py')}, + {'type': 'class', 'name': 'tests.test_mod'}, + {'type': 'testcase', 'name': 'test_value[left::right]'}, + ]) + + def test_class_parameter_id_preserves_separators_and_brackets(self): + self.assertEqual(_parse_pytest_nodeid('tests/test_mod.py::TestClass::test_value[a[::b]::c]'), [ + {'type': 'file', 'name': os.path.normpath('tests/test_mod.py')}, + {'type': 'class', 'name': 'tests.test_mod.TestClass'}, + {'type': 'testcase', 'name': 'test_value[a[::b]::c]'}, + ]) + + def test_brackets_in_file_path_do_not_start_parameter_id(self): + self.assertEqual(_parse_pytest_nodeid('tests/[group]/test_mod.py::TestClass::test_value'), [ + {'type': 'file', 'name': os.path.normpath('tests/[group]/test_mod.py')}, + {'type': 'class', 'name': 'tests.[group].test_mod.TestClass'}, + {'type': 'testcase', 'name': 'test_value'}, + ]) + + def test_json_report_preserves_parameter_id(self): + parser = PytestJSONReportParser(mock.Mock()) + with tempfile.TemporaryDirectory() as directory: + path = os.path.join(directory, 'report.json') + with open(path, 'w') as stream: + stream.write(json.dumps({'nodeid': 'tests/test_mod.py::test_value[left::right]', + 'when': 'call', 'outcome': 'passed'}) + '\n') + events = list(parser.parse_func(path)) + self.assertEqual(events[0]['testPath'][-1], {'type': 'testcase', 'name': 'test_value[left::right]'}) + class PytestJSONReportParserLongreprTest(TestCase): class DummyClient: