diff --git a/smart_tests/commands/inspect/subset.py b/smart_tests/commands/inspect/subset.py index b8491b300..312bcfd5f 100644 --- a/smart_tests/commands/inspect/subset.py +++ b/smart_tests/commands/inspect/subset.py @@ -20,6 +20,8 @@ def __init__(self, result: dict, is_subset: bool): self._test_path = "#".join([path["type"] + "=" + path["name"] for path in result["testPath"] if path.keys() >= {"type", "name"}]) self._is_subset = is_subset + self._density = result.get("density", 0.0) + self._is_new = result.get("numNewTests", 0) > 0 class SubsetResults(object): @@ -57,19 +59,22 @@ class SubsetResultTableDisplay(SubsetResultAbstractDisplay): def __init__(self, results: SubsetResults): super().__init__(results) - def display(self): - header = ["Order", "Test Path", "In Subset", "Estimated duration (sec)"] + def display(self, new_tests_only: bool = False): + header = ["Order", "Test Path", "In Subset", "Density", "Duration", "New"] rows = [] - for idx, result in enumerate(self._results.list()): + results = [r for r in self._results.list() if r._is_new] if new_tests_only else self._results.list() + for idx, result in enumerate(results): rows.append( [ idx + 1, result._test_path, "✔" if result._is_subset else "", - result._estimated_duration_sec, + result._density, + "{:.3f}s".format(result._estimated_duration_sec), + "Yes" if result._is_new else "No", ] ) - click.echo_via_pager(tabulate(rows, header, tablefmt="github", floatfmt=".2f")) + click.echo_via_pager(tabulate(rows, header, tablefmt="github", floatfmt=".3f")) class SubsetResultJSONDisplay(SubsetResultAbstractDisplay): @@ -105,6 +110,10 @@ def subset( json: Annotated[bool, typer.Option( help="Display JSON format" )] = False, + new_tests_only: Annotated[bool, typer.Option( + "--new-tests-only", + help="Show only tests that have no duration history" + )] = False, ): is_json_format = json # Map parameter name @@ -135,4 +144,7 @@ def subset( else: displayer = SubsetResultTableDisplay(results) - displayer.display() + if isinstance(displayer, SubsetResultTableDisplay): + displayer.display(new_tests_only=new_tests_only) + else: + displayer.display() diff --git a/smart_tests/commands/subset.py b/smart_tests/commands/subset.py index df62f9f8b..f7bb0fbd2 100644 --- a/smart_tests/commands/subset.py +++ b/smart_tests/commands/subset.py @@ -821,12 +821,20 @@ def run(self): org, workspace = get_org_workspace() + new_test_count = summary["subset"].get("newTestCount", 0) + subset_label = "Subset (New Tests)" if new_test_count > 0 else "Subset" + subset_candidates = ( + "{} ({})".format(len(original_subset), new_test_count) + if new_test_count > 0 + else len(original_subset) + ) + header = ["", "Candidates", "Estimated duration (%)", "Estimated duration (min)"] rows = [ [ - "Subset", - len(original_subset), + subset_label, + subset_candidates, summary["subset"].get("rate", 0.0), summary["subset"].get("duration", 0.0), ], diff --git a/tests/commands/inspect/test_subset.py b/tests/commands/inspect/test_subset.py index 1c144f431..44212e4c7 100644 --- a/tests/commands/inspect/test_subset.py +++ b/tests/commands/inspect/test_subset.py @@ -11,19 +11,32 @@ class SubsetTest(CliTestCase): mock_json = { "testPaths": [ {"testPath": [ - {"type": "file", "name": "test_file1.py"}], "duration": 1200}, + {"type": "file", "name": "test_file1.py"}], "duration": 1200, "density": 0.8, "numNewTests": 0}, {"testPath": [ - {"type": "file", "name": "test_file3.py"}], "duration": 600}, + {"type": "file", "name": "test_file3.py"}], "duration": 600, "density": 0.5, "numNewTests": 0}, ], "rest": [ {"testPath": [ - {"type": "file", "name": "test_file4.py"}], "duration": 1800}, + {"type": "file", "name": "test_file4.py"}], "duration": 1800, "density": 0.3, "numNewTests": 0}, {"testPath": [ - {"type": "file", "name": "test_file2.py"}], "duration": 100} + {"type": "file", "name": "test_file2.py"}], "duration": 100, "density": 0.1, "numNewTests": 0} ] } + mock_json_with_new_tests = { + "testPaths": [ + {"testPath": [ + {"type": "file", "name": "test_new1.py"}], "duration": 0, "density": 0.0, "numNewTests": 1}, + {"testPath": [ + {"type": "file", "name": "test_known.py"}], "duration": 1200, "density": 0.8, "numNewTests": 0}, + ], + "rest": [ + {"testPath": [ + {"type": "file", "name": "test_new2.py"}], "duration": 0, "density": 0.0, "numNewTests": 1}, + ] + } + @responses.activate @mock.patch.dict(os.environ, {"SMART_TESTS_TOKEN": CliTestCase.smart_tests_token}) def test_subset(self): @@ -35,12 +48,81 @@ def test_subset(self): status=200) result = self.cli('inspect', 'subset', '--subset-id', self.subsetting_id, mix_stderr=False) - expect = """| Order | Test Path | In Subset | Estimated duration (sec) | -|---------|--------------------|-------------|----------------------------| -| 1 | file=test_file1.py | ✔ | 1.20 | -| 2 | file=test_file3.py | ✔ | 0.60 | -| 3 | file=test_file4.py | | 1.80 | -| 4 | file=test_file2.py | | 0.10 | + expect = """| Order | Test Path | In Subset | Density | Duration | New | +|---------|--------------------|-------------|-----------|------------|-------| +| 1 | file=test_file1.py | ✔ | 0.800 | 1.200s | No | +| 2 | file=test_file3.py | ✔ | 0.500 | 0.600s | No | +| 3 | file=test_file4.py | | 0.300 | 1.800s | No | +| 4 | file=test_file2.py | | 0.100 | 0.100s | No | +""" + + self.assertEqual(result.stdout, expect) + + @responses.activate + @mock.patch.dict(os.environ, {"SMART_TESTS_TOKEN": CliTestCase.smart_tests_token}) + def test_subset_shows_new_column(self): + responses.replace( + responses.GET, + f"{get_base_url()}/intake/organizations/{self.organization}/workspaces/" + f"{self.workspace}/subset/{self.subsetting_id}", + json=self.mock_json_with_new_tests, + status=200) + + result = self.cli('inspect', 'subset', '--subset-id', self.subsetting_id, mix_stderr=False) + expect = """| Order | Test Path | In Subset | Density | Duration | New | +|---------|--------------------|-------------|-----------|------------|-------| +| 1 | file=test_new1.py | ✔ | 0.000 | 0.000s | Yes | +| 2 | file=test_known.py | ✔ | 0.800 | 1.200s | No | +| 3 | file=test_new2.py | | 0.000 | 0.000s | Yes | +""" + + self.assertEqual(result.stdout, expect) + + @responses.activate + @mock.patch.dict(os.environ, {"SMART_TESTS_TOKEN": CliTestCase.smart_tests_token}) + def test_subset_new_tests_only(self): + responses.replace( + responses.GET, + f"{get_base_url()}/intake/organizations/{self.organization}/workspaces/" + f"{self.workspace}/subset/{self.subsetting_id}", + json=self.mock_json_with_new_tests, + status=200) + + result = self.cli( + 'inspect', 'subset', '--subset-id', self.subsetting_id, '--new-tests-only', + mix_stderr=False) + expect = """| Order | Test Path | In Subset | Density | Duration | New | +|---------|-------------------|-------------|-----------|------------|-------| +| 1 | file=test_new1.py | ✔ | 0.000 | 0.000s | Yes | +| 2 | file=test_new2.py | | 0.000 | 0.000s | Yes | +""" + + self.assertEqual(result.stdout, expect) + + @responses.activate + @mock.patch.dict(os.environ, {"SMART_TESTS_TOKEN": CliTestCase.smart_tests_token}) + def test_subset_new_tests_only_empty_in_brainless_mode(self): + brainless_mock_json = { + "testPaths": [ + {"testPath": [ + {"type": "file", "name": "test_file1.py"}], "duration": 0, "density": 0.0, "numNewTests": 0}, + {"testPath": [ + {"type": "file", "name": "test_file2.py"}], "duration": 0, "density": 0.0, "numNewTests": 0}, + ], + "rest": [] + } + responses.replace( + responses.GET, + f"{get_base_url()}/intake/organizations/{self.organization}/workspaces/" + f"{self.workspace}/subset/{self.subsetting_id}", + json=brainless_mock_json, + status=200) + + result = self.cli( + 'inspect', 'subset', '--subset-id', self.subsetting_id, '--new-tests-only', + mix_stderr=False) + expect = """| Order | Test Path | In Subset | Density | Duration | New | +|---------|-------------|-------------|-----------|------------|-------| """ self.assertEqual(result.stdout, expect) diff --git a/tests/commands/test_subset.py b/tests/commands/test_subset.py index 179910409..fc1b428f3 100644 --- a/tests/commands/test_subset.py +++ b/tests/commands/test_subset.py @@ -1083,3 +1083,74 @@ def test_subset_id_file_round_trip(self): self.assertEqual(payload.get('subsettingId'), self.subsetting_id) finally: os.unlink(id_file_path) + + @responses.activate + @mock.patch.dict(os.environ, {"SMART_TESTS_TOKEN": CliTestCase.smart_tests_token}) + def test_subset_shows_new_test_count_in_table(self): + pipe = "test_new.py\ntest_known.py\ntest_known2.py" + responses.replace( + responses.POST, + f"{get_base_url()}/intake/organizations/{self.organization}/workspaces/{self.workspace}/subset", + json={ + "testPaths": [ + [{"type": "file", "name": "test_new.py"}], + [{"type": "file", "name": "test_known.py"}], + ], + "rest": [ + [{"type": "file", "name": "test_known2.py"}], + ], + "subsettingId": 123, + "summary": { + "subset": {"duration": 0, "candidates": 2, "rate": 67, "newTestCount": 1}, + "rest": {"duration": 5, "candidates": 1, "rate": 33, "newTestCount": 0}, + }, + "isObservation": False, + }, + status=200, + ) + + result = self.cli( + "subset", "file", + "--target", "70%", + "--session", self.session, + mix_stderr=False, + input=pipe, + ) + self.assert_success(result) + self.assertIn("Subset (New Tests)", result.stderr) + self.assertIn("2 (1)", result.stderr) + + @responses.activate + @mock.patch.dict(os.environ, {"SMART_TESTS_TOKEN": CliTestCase.smart_tests_token}) + def test_subset_shows_plain_subset_label_when_no_new_tests(self): + pipe = "test_known.py\ntest_known2.py" + responses.replace( + responses.POST, + f"{get_base_url()}/intake/organizations/{self.organization}/workspaces/{self.workspace}/subset", + json={ + "testPaths": [ + [{"type": "file", "name": "test_known.py"}], + ], + "rest": [ + [{"type": "file", "name": "test_known2.py"}], + ], + "subsettingId": 123, + "summary": { + "subset": {"duration": 10, "candidates": 1, "rate": 50}, + "rest": {"duration": 10, "candidates": 1, "rate": 50}, + }, + "isObservation": False, + }, + status=200, + ) + + result = self.cli( + "subset", "file", + "--target", "50%", + "--session", self.session, + mix_stderr=False, + input=pipe, + ) + self.assert_success(result) + self.assertIn("| Subset", result.stderr) + self.assertNotIn("Subset (New Tests)", result.stderr)