diff --git a/launchable/test_runners/maven.py b/launchable/test_runners/maven.py index df7211a9b..9f27eaf32 100644 --- a/launchable/test_runners/maven.py +++ b/launchable/test_runners/maven.py @@ -1,6 +1,7 @@ import glob import os import re +import xml.etree.ElementTree as ET from typing import Dict, List, Optional, Tuple import click @@ -46,6 +47,31 @@ def is_file(f: str) -> bool: return False +def parse_surefire_reports() -> List[Dict[str, str]]: + """Collect the test classes Surefire discovered from its TEST-*.xml reports. + + Intended to be run against reports produced by a JUnit 5 dry run + (`mvn test -Djunit.platform.execution.dryRun.enabled=true`): Surefire applies + all of its own filtering (excludedGroups, includedGroups, excludes, profiles, + ...) before the dry run, so only the tests Maven would actually execute get a + report. Each report's root `name` attribute is the fully-qualified class name. + """ + test_paths = [] + report_pattern = os.path.join('**', 'target', 'surefire-reports', 'TEST-*.xml') + report_files = glob.glob(report_pattern, recursive=True) + + for report_file in report_files: + try: + classname = ET.parse(report_file).getroot().get('name') + if classname: + test_paths.append({"type": "class", "name": classname}) + except ET.ParseError as e: + click.echo(click.style( + "Warning: Could not parse {}: {}".format(report_file, e), fg="yellow"), err=True) + + return test_paths + + @click.option( '--test-compile-created-file', 'test_compile_created_file', @@ -61,6 +87,13 @@ def is_file(f: str) -> bool: is_flag=True, help="Scan testCompile/default-testCompile/createdFiles.lst for *.lst files generated by `mvn compile` and use them as test inputs.", # noqa: E501 ) +@click.option( + '--scan-dryrun-results', + 'is_scan_dryrun_results', + required=False, + is_flag=True, + help="Scan surefire reports generated by `mvn test -Djunit.platform.execution.dryRun.enabled=true`. Only the tests Maven would actually run (after applying pom.xml excludedGroups/excludes/etc.) are sent to the subset. Run the dry run before this command. JUnit 5 only.", # noqa: E501 +) @click.option( '--exclude', 'exclude_rules', @@ -70,7 +103,19 @@ def is_file(f: str) -> bool: ) @click.argument('source_roots', required=False, nargs=-1) @launchable.subset -def subset(client, source_roots, test_compile_created_file, is_scan_test_compile_lst, exclude_rules: Tuple[str, ...]): +def subset(client, source_roots, test_compile_created_file, is_scan_test_compile_lst, is_scan_dryrun_results, + exclude_rules: Tuple[str, ...]): + + if is_scan_dryrun_results: + tests = parse_surefire_reports() + if not tests: + raise click.UsageError( + "No surefire reports found under **/target/surefire-reports/. " + "Run `mvn test -Djunit.platform.execution.dryRun.enabled=true` before this command.") + for test in tests: + client.test_paths.append([test]) + client.run() + return # Compile exclude rules compiled_exclude_rules = [] diff --git a/tests/data/maven/dryrun-test/target/surefire-reports/TEST-com.example.launchable.CalculatorTest.xml b/tests/data/maven/dryrun-test/target/surefire-reports/TEST-com.example.launchable.CalculatorTest.xml new file mode 100644 index 000000000..6c5cbce8c --- /dev/null +++ b/tests/data/maven/dryrun-test/target/surefire-reports/TEST-com.example.launchable.CalculatorTest.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/tests/data/maven/dryrun-test/target/surefire-reports/TEST-com.example.launchable.UserTest.xml b/tests/data/maven/dryrun-test/target/surefire-reports/TEST-com.example.launchable.UserTest.xml new file mode 100644 index 000000000..87c8b3512 --- /dev/null +++ b/tests/data/maven/dryrun-test/target/surefire-reports/TEST-com.example.launchable.UserTest.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/tests/data/maven/subset_scan_dryrun_results_result.json b/tests/data/maven/subset_scan_dryrun_results_result.json new file mode 100644 index 000000000..77adb8e6e --- /dev/null +++ b/tests/data/maven/subset_scan_dryrun_results_result.json @@ -0,0 +1,12 @@ +{ + "testPaths": [ + [{"type": "class", "name": "com.example.launchable.CalculatorTest"}], + [{"type": "class", "name": "com.example.launchable.UserTest"}] + ], + "testRunner": "maven", + "goal": {"type": "subset-by-percentage", "percentage": 0.1}, + "ignoreNewTests": false, + "session": {"id": "16"}, + "getTestsFromGuess": false, + "getTestsFromPreviousSessions": false +} diff --git a/tests/test_runners/test_maven.py b/tests/test_runners/test_maven.py index aa6669e6a..c3421d9c1 100644 --- a/tests/test_runners/test_maven.py +++ b/tests/test_runners/test_maven.py @@ -227,6 +227,36 @@ def test_subset_with_exclude(self): self.assert_success(result) self.assert_subset_payload('subset_with_exclude_rules_result.json') + @responses.activate + @mock.patch.dict(os.environ, {"LAUNCHABLE_TOKEN": CliTestCase.launchable_token}) + def test_scan_dryrun_results(self): + # Reports live under /target/surefire-reports/. The flag globs + # **/target/surefire-reports/TEST-*.xml relative to the cwd, so run from there. + test_data_dir = str(self.test_files_dir.joinpath('dryrun-test').resolve()) + original_dir = os.getcwd() + try: + os.chdir(test_data_dir) + result = self.cli('subset', '--target', '10%', '--session', + self.session, 'maven', '--scan-dryrun-results') + self.assert_success(result) + self.assert_subset_payload('subset_scan_dryrun_results_result.json') + finally: + os.chdir(original_dir) + + @responses.activate + @mock.patch.dict(os.environ, {"LAUNCHABLE_TOKEN": CliTestCase.launchable_token}) + def test_scan_dryrun_results_no_reports(self): + original_dir = os.getcwd() + with tempfile.TemporaryDirectory() as temp_dir: + try: + os.chdir(temp_dir) + result = self.cli('subset', '--target', '10%', '--session', + self.session, 'maven', '--scan-dryrun-results') + self.assertNotEqual(result.exit_code, 0) + self.assertIn("No surefire reports found", result.output) + finally: + os.chdir(original_dir) + def test_glob(self): for x in [ 'foo/BarTest.java',