diff --git a/launchable/__main__.py b/launchable/__main__.py index e4307d63b..ef81dfd9a 100644 --- a/launchable/__main__.py +++ b/launchable/__main__.py @@ -9,6 +9,7 @@ from launchable.app import Application +from .commands.compare import compare from .commands.inspect import inspect from .commands.record import record from .commands.split_subset import split_subset @@ -89,6 +90,7 @@ def main(ctx, log_level, plugin_dir, dry_run, skip_cert_verification): main.add_command(verify) main.add_command(inspect) main.add_command(stats) +main.add_command(compare) if __name__ == '__main__': main() diff --git a/launchable/commands/compare/__init__.py b/launchable/commands/compare/__init__.py new file mode 100644 index 000000000..3dc6280ba --- /dev/null +++ b/launchable/commands/compare/__init__.py @@ -0,0 +1,13 @@ +import click + +from launchable.utils.click import GroupWithAlias + +from .subsets import subsets + + +@click.group(cls=GroupWithAlias) +def compare(): + pass + + +compare.add_command(subsets) diff --git a/launchable/commands/compare/subsets.py b/launchable/commands/compare/subsets.py new file mode 100644 index 000000000..d55047256 --- /dev/null +++ b/launchable/commands/compare/subsets.py @@ -0,0 +1,50 @@ +from typing import List, Tuple, Union + +import click +from tabulate import tabulate + + +@click.command() +@click.argument('file_before', type=click.Path(exists=True)) +@click.argument('file_after', type=click.Path(exists=True)) +def subsets(file_before, file_after): + """ + Compare two subset files and display changes in test order positions + """ + + # Read files and map test paths to their indices + with open(file_before, 'r') as f: + before_tests = f.read().splitlines() + before_index_map = {test: idx for idx, test in enumerate(before_tests)} + + with open(file_after, 'r') as f: + after_tests = f.read().splitlines() + after_index_map = {test: idx for idx, test in enumerate(after_tests)} + + # List of tuples representing test order changes (before, after, diff, test) + rows: List[Tuple[Union[int, str], Union[int, str], Union[int, str], str]] = [] + + # Calculate order difference and add each test in file_after to changes + for after_idx, test in enumerate(after_tests): + if test in before_index_map: + before_idx = before_index_map[test] + diff = after_idx - before_idx + rows.append((before_idx + 1, after_idx + 1, diff, test)) + else: + rows.append(('-', after_idx + 1, 'NEW', test)) + + # Add all deleted tests to changes + for before_idx, test in enumerate(before_tests): + if test not in after_index_map: + rows.append((before_idx + 1, '-', 'DELETED', test)) + + # Sort changes by the order diff + rows.sort(key=lambda x: (0 if isinstance(x[2], str) else 1, x[2])) + + # Display results in a tabular format + headers = ["Before", "After", "After - Before", "Test"] + tabular_data = [ + (before, after, f"{diff:+}" if isinstance(diff, int) else diff, test) + for before, after, diff, test in rows + ] + click.echo(tabulate(tabular_data, headers=headers, tablefmt="github")) diff --git a/tests/commands/inspect/__init__.py b/tests/commands/compare/__init__.py similarity index 100% rename from tests/commands/inspect/__init__.py rename to tests/commands/compare/__init__.py diff --git a/tests/commands/compare/test_subsets.py b/tests/commands/compare/test_subsets.py new file mode 100644 index 000000000..df0676bbc --- /dev/null +++ b/tests/commands/compare/test_subsets.py @@ -0,0 +1,155 @@ +import os +from unittest import mock + +from tests.cli_test_case import CliTestCase + + +class SubsetsTest(CliTestCase): + + @mock.patch.dict(os.environ, {"LAUNCHABLE_TOKEN": CliTestCase.launchable_token}) + def test_subsets(self): + # Create subset-before.txt + with open("subset-before.txt", "w") as f: + f.write("\n".join([ + "src/test/java/example/DivTest.java", + "src/test/java/example/DB1Test.java", + "src/test/java/example/MulTest.java", + "src/test/java/example/Add2Test.java", + "src/test/java/example/File1Test.java", + "src/test/java/example/File0Test.java", + "src/test/java/example/SubTest.java", + "src/test/java/example/DB0Test.java", + "src/test/java/example/AddTest.java", + ])) + + # Create subset-after.txt + with open("subset-after.txt", "w") as f: + f.write("\n".join([ + "src/test/java/example/Add2Test.java", + "src/test/java/example/MulTest.java", + "src/test/java/example/AddTest.java", + "src/test/java/example/File1Test.java", + "src/test/java/example/DivTest.java", + "src/test/java/example/File0Test.java", + "src/test/java/example/DB1Test.java", + "src/test/java/example/DB0Test.java", + "src/test/java/example/SubTest.java", + ])) + + result = self.cli('compare', 'subsets', "subset-before.txt", "subset-after.txt", mix_stderr=False) + expect = """| Before | After | After - Before | Test | +|----------|---------|------------------|--------------------------------------| +| 9 | 3 | -6 | src/test/java/example/AddTest.java | +| 4 | 1 | -3 | src/test/java/example/Add2Test.java | +| 3 | 2 | -1 | src/test/java/example/MulTest.java | +| 5 | 4 | -1 | src/test/java/example/File1Test.java | +| 6 | 6 | +0 | src/test/java/example/File0Test.java | +| 8 | 8 | +0 | src/test/java/example/DB0Test.java | +| 7 | 9 | +2 | src/test/java/example/SubTest.java | +| 1 | 5 | +4 | src/test/java/example/DivTest.java | +| 2 | 7 | +5 | src/test/java/example/DB1Test.java | +""" + + self.assertEqual(result.stdout, expect) + + @mock.patch.dict(os.environ, {"LAUNCHABLE_TOKEN": CliTestCase.launchable_token}) + def test_subsets_when_new_tests(self): + # Create subset-before.txt + with open("subset-before.txt", "w") as f: + f.write("\n".join([ + "src/test/java/example/SubTest.java", + "src/test/java/example/DivTest.java", + "src/test/java/example/Add2Test.java", + "src/test/java/example/File0Test.java", + "src/test/java/example/AddTest.java", + "src/test/java/example/File1Test.java", + "src/test/java/example/MulTest.java", + "src/test/java/example/DB0Test.java", + "src/test/java/example/DB1Test.java" + ])) + + # Create subset-after.txt (which includes additional test path NewTest.java) + with open("subset-after.txt", "w") as f: + f.write("\n".join([ + "src/test/java/example/NewTest.java", + "src/test/java/example/SubTest.java", + "src/test/java/example/File0Test.java", + "src/test/java/example/DB1Test.java", + "src/test/java/example/DivTest.java", + "src/test/java/example/MulTest.java", + "src/test/java/example/File1Test.java", + "src/test/java/example/DB0Test.java", + "src/test/java/example/Add2Test.java", + "src/test/java/example/AddTest.java" + ])) + + result = self.cli('compare', 'subsets', "subset-before.txt", "subset-after.txt", mix_stderr=False) + expect = """| Before | After | After - Before | Test | +|----------|---------|------------------|--------------------------------------| +| - | 1 | NEW | src/test/java/example/NewTest.java | +| 9 | 4 | -5 | src/test/java/example/DB1Test.java | +| 4 | 3 | -1 | src/test/java/example/File0Test.java | +| 7 | 6 | -1 | src/test/java/example/MulTest.java | +| 8 | 8 | +0 | src/test/java/example/DB0Test.java | +| 1 | 2 | +1 | src/test/java/example/SubTest.java | +| 6 | 7 | +1 | src/test/java/example/File1Test.java | +| 2 | 5 | +3 | src/test/java/example/DivTest.java | +| 5 | 10 | +5 | src/test/java/example/AddTest.java | +| 3 | 9 | +6 | src/test/java/example/Add2Test.java | +""" + + self.assertEqual(result.stdout, expect) + + @mock.patch.dict(os.environ, {"LAUNCHABLE_TOKEN": CliTestCase.launchable_token}) + def test_subsets_when_deleted_tests(self): + # Create subset-before.txt + with open("subset-before.txt", "w") as f: + f.write("\n".join([ + "src/test/java/example/NewTest.java", + "src/test/java/example/SubTest.java", + "src/test/java/example/File0Test.java", + "src/test/java/example/DB1Test.java", + "src/test/java/example/DivTest.java", + "src/test/java/example/MulTest.java", + "src/test/java/example/File1Test.java", + "src/test/java/example/DB0Test.java", + "src/test/java/example/Add2Test.java", + "src/test/java/example/AddTest.java" + ])) + + # Create subset-after.txt (which doesn't include NewTest.java) + with open("subset-after.txt", "w") as f: + f.write("\n".join([ + "src/test/java/example/DB1Test.java", + "src/test/java/example/DB0Test.java", + "src/test/java/example/File1Test.java", + "src/test/java/example/SubTest.java", + "src/test/java/example/AddTest.java", + "src/test/java/example/MulTest.java", + "src/test/java/example/File0Test.java", + "src/test/java/example/Add2Test.java", + "src/test/java/example/DivTest.java" + ])) + + result = self.cli('compare', 'subsets', "subset-before.txt", "subset-after.txt", mix_stderr=False) + expect = """| Before | After | After - Before | Test | +|----------|---------|------------------|--------------------------------------| +| 1 | - | DELETED | src/test/java/example/NewTest.java | +| 8 | 2 | -6 | src/test/java/example/DB0Test.java | +| 10 | 5 | -5 | src/test/java/example/AddTest.java | +| 7 | 3 | -4 | src/test/java/example/File1Test.java | +| 4 | 1 | -3 | src/test/java/example/DB1Test.java | +| 9 | 8 | -1 | src/test/java/example/Add2Test.java | +| 6 | 6 | +0 | src/test/java/example/MulTest.java | +| 2 | 4 | +2 | src/test/java/example/SubTest.java | +| 3 | 7 | +4 | src/test/java/example/File0Test.java | +| 5 | 9 | +4 | src/test/java/example/DivTest.java | +""" + + self.assertEqual(result.stdout, expect) + + def tearDown(self): + if os.path.exists("subset-before.txt"): + os.remove("subset-before.txt") + if os.path.exists("subset-after.txt"): + os.remove("subset-after.txt")