diff --git a/launchable/test_runners/maven.py b/launchable/test_runners/maven.py
index df7211a9b..6cdf05645 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,33 @@ def is_file(f: str) -> bool:
return False
+def parse_surefire_reports() -> List[Dict[str, str]]:
+ """Parse Maven Surefire XML reports from target/surefire-reports/."""
+ test_paths = []
+ report_pattern = '**/target/surefire-reports/TEST-*.xml'
+ report_files = glob.glob(report_pattern, recursive=True)
+
+ if not report_files:
+ return []
+
+ click.echo(f"Found {len(report_files)} surefire report(s)", err=True)
+
+ for report_file in report_files:
+ try:
+ tree = ET.parse(report_file)
+ root = tree.getroot()
+ classname = root.get('name')
+
+ if classname:
+ test_paths.append({"type": "class", "name": classname})
+
+ except ET.ParseError as e:
+ click.secho(f"Warning: Could not parse {report_file}: {e}", fg='yellow', err=True)
+
+ click.echo(f"Total tests discovered: {len(test_paths)}", err=True)
+ return test_paths
+
+
@click.option(
'--test-compile-created-file',
'test_compile_created_file',
@@ -61,6 +89,12 @@ 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',
+ is_flag=True,
+ help="Scan surefire reports generated by mvn test -DdryRun=true",
+)
@click.option(
'--exclude',
'exclude_rules',
@@ -70,7 +104,14 @@ 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, ...]
+):
# Compile exclude rules
compiled_exclude_rules = []
@@ -100,23 +141,45 @@ def file2test(f: str) -> Optional[List]:
else:
return None
- files_to_read = list(test_compile_created_file)
- if is_scan_test_compile_lst:
- if len(test_compile_created_file) > 0:
- click.echo(click.style(
- "Warning: --test-compile-created-file is overridden by --scan-test-compile-lst", fg="yellow"),
- err=True)
+ if is_scan_dryrun_results:
+ click.echo("Scanning Maven dry-run results...", err=True)
+ tests = parse_surefire_reports()
+
+ if not tests:
+ click.secho(
+ "Warning: No surefire reports found. Did you run 'mvn test -DdryRun=true'?",
+ fg='yellow',
+ err=True
+ )
+ click.secho(
+ "Please run: mvn test -DdryRun=true",
+ fg='cyan',
+ err=True
+ )
+ return
+
+ # Add tests to client (each test path must be a list)
+ for test in tests:
+ client.test_paths.append([test])
- pattern = os.path.join('**', 'createdFiles.lst')
- files_to_read = glob.glob(pattern, recursive=True)
+ elif is_scan_test_compile_lst or test_compile_created_file:
+ if is_scan_test_compile_lst:
+ if len(test_compile_created_file) > 0:
+ click.echo(click.style(
+ "Warning: --test-compile-created-file is overridden by --scan-test-compile-lst", fg="yellow"),
+ err=True)
- if not files_to_read:
- click.echo(click.style(
- "Warning: No .lst files. Please run after executing `mvn test-compile`", fg="yellow"),
- err=True)
- return
+ pattern = os.path.join('**', 'createdFiles.lst')
+ files_to_read = glob.glob(pattern, recursive=True)
+
+ if not files_to_read:
+ click.echo(click.style(
+ "Warning: No .lst files. Please run after executing `mvn test-compile`", fg="yellow"),
+ err=True)
+ return
+ elif test_compile_created_file:
+ files_to_read = list(test_compile_created_file)
- if files_to_read:
for file in files_to_read:
with open(file, 'r') as f:
lines = f.readlines()
@@ -132,6 +195,7 @@ def file2test(f: str) -> Optional[List]:
path = file2test(l)
if path:
client.test_paths.append(path)
+
else:
for root in source_roots:
client.scan(root, '**/*', file2test)
diff --git a/tests/data/maven/README.md b/tests/data/maven/README.md
new file mode 100644
index 000000000..33cf259c8
--- /dev/null
+++ b/tests/data/maven/README.md
@@ -0,0 +1,75 @@
+# Maven Test Data
+
+This directory contains test fixtures for the Maven test runner, including sample Maven Surefire reports used to test the `--scan-dryrun-results` feature.
+
+## Directory Structure
+
+- `dryrun-test/` - Test data for the `--scan-dryrun-results` feature
+ - `target/surefire-reports/` - Sample Maven Surefire reports (XML and TXT formats)
+- `reports/` - Sample test result XML files for various test scenarios
+
+## How to Test Manually with Your Own Project
+
+To manually test the `--scan-dryrun-results` feature with your own Maven project:
+
+```bash
+# 1. Navigate to your Maven project
+cd /path/to/your/maven-project
+
+# 2. Run Maven dry-run to generate reports
+mvn test -DdryRun=true
+
+# 3. Verify reports were created
+ls -la target/surefire-reports/TEST-*.xml
+
+# 4. Set up environment (if not already set)
+export LAUNCHABLE_TOKEN='v1:your-org/your-workspace:your-actual-token'
+
+# 5. Record build
+launchable record build --name 'test-build-name'
+
+# 6. Create a session
+launchable record session --build 'test-build-name'
+# Copy the session ID from output, e.g., builds/test-build-name/test_sessions/123
+
+# 7. Run subset with --scan-dryrun-results
+launchable subset maven \
+ --scan-dryrun-results \
+ --session builds/test-build-name/test_sessions/123 \
+ --target 10%
+```
+
+## Creating Test Files for `--scan-dryrun-results`
+
+The test files in `dryrun-test/target/surefire-reports/` are fixtures that simulate Maven Surefire reports generated by running tests with Maven's dry-run mode.
+
+**Source:** These fixtures were generated from the [smart-tests-integration-examples](https://github.com/cloudbees-oss/smart-tests-integration-examples) repository, specifically from `maven/test-exclusion` project.
+
+### How to Create/Update These Files
+
+The `dryrun-test/` directory contains only the generated reports as test fixtures. To create or update these files:
+
+```bash
+# 1. Clone the integration examples repository
+git clone https://github.com/cloudbees-oss/smart-tests-integration-examples.git
+cd smart-tests-integration-examples/maven/test-exclusion
+
+# 2. Run Maven dry-run to generate Surefire reports
+mvn test -DdryRun=true
+
+# 3. Copy the generated reports to this repository
+cp target/surefire-reports/TEST-*.xml /path/to/smart-tests-cli/tests/data/maven/dryrun-test/target/surefire-reports/
+cp target/surefire-reports/*.txt /path/to/smart-tests-cli/tests/data/maven/dryrun-test/target/surefire-reports/
+```
+
+The reports include:
+- XML files: `TEST-*.xml` (detailed test execution results with test class names)
+- TXT files: `*.txt` (summary information for each test class)
+
+### Purpose
+
+These fixtures are used to test:
+- Parsing of Maven Surefire XML reports
+- The `--scan-dryrun-results` flag functionality
+- Integration with Launchable's subset API
+- Handling of filtered/excluded tests
diff --git a/tests/data/maven/dryrun-test/target/surefire-reports/TEST-com.example.excludetestapplication.ExcludeTestApplicationTests.xml b/tests/data/maven/dryrun-test/target/surefire-reports/TEST-com.example.excludetestapplication.ExcludeTestApplicationTests.xml
new file mode 100644
index 000000000..14ed22172
--- /dev/null
+++ b/tests/data/maven/dryrun-test/target/surefire-reports/TEST-com.example.excludetestapplication.ExcludeTestApplicationTests.xml
@@ -0,0 +1,87 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/data/maven/dryrun-test/target/surefire-reports/TEST-com.launchable.demo.CalculatorTest.xml b/tests/data/maven/dryrun-test/target/surefire-reports/TEST-com.launchable.demo.CalculatorTest.xml
new file mode 100644
index 000000000..f841f4cc7
--- /dev/null
+++ b/tests/data/maven/dryrun-test/target/surefire-reports/TEST-com.launchable.demo.CalculatorTest.xml
@@ -0,0 +1,91 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/data/maven/dryrun-test/target/surefire-reports/TEST-com.launchable.demo.MixedTagsTest.xml b/tests/data/maven/dryrun-test/target/surefire-reports/TEST-com.launchable.demo.MixedTagsTest.xml
new file mode 100644
index 000000000..46f5bdf94
--- /dev/null
+++ b/tests/data/maven/dryrun-test/target/surefire-reports/TEST-com.launchable.demo.MixedTagsTest.xml
@@ -0,0 +1,79 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/data/maven/dryrun-test/target/surefire-reports/com.example.excludetestapplication.ExcludeTestApplicationTests.txt b/tests/data/maven/dryrun-test/target/surefire-reports/com.example.excludetestapplication.ExcludeTestApplicationTests.txt
new file mode 100644
index 000000000..e0fcabc56
--- /dev/null
+++ b/tests/data/maven/dryrun-test/target/surefire-reports/com.example.excludetestapplication.ExcludeTestApplicationTests.txt
@@ -0,0 +1,4 @@
+-------------------------------------------------------------------------------
+Test set: com.example.excludetestapplication.ExcludeTestApplicationTests
+-------------------------------------------------------------------------------
+Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.898 s -- in com.example.excludetestapplication.ExcludeTestApplicationTests
diff --git a/tests/data/maven/dryrun-test/target/surefire-reports/com.launchable.demo.CalculatorTest.txt b/tests/data/maven/dryrun-test/target/surefire-reports/com.launchable.demo.CalculatorTest.txt
new file mode 100644
index 000000000..e075f8c64
--- /dev/null
+++ b/tests/data/maven/dryrun-test/target/surefire-reports/com.launchable.demo.CalculatorTest.txt
@@ -0,0 +1,4 @@
+-------------------------------------------------------------------------------
+Test set: com.launchable.demo.CalculatorTest
+-------------------------------------------------------------------------------
+Tests run: 6, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.026 s -- in com.launchable.demo.CalculatorTest
diff --git a/tests/data/maven/dryrun-test/target/surefire-reports/com.launchable.demo.MixedTagsTest.txt b/tests/data/maven/dryrun-test/target/surefire-reports/com.launchable.demo.MixedTagsTest.txt
new file mode 100644
index 000000000..caa26cb94
--- /dev/null
+++ b/tests/data/maven/dryrun-test/target/surefire-reports/com.launchable.demo.MixedTagsTest.txt
@@ -0,0 +1,4 @@
+-------------------------------------------------------------------------------
+Test set: com.launchable.demo.MixedTagsTest
+-------------------------------------------------------------------------------
+Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.004 s -- in com.launchable.demo.MixedTagsTest
diff --git a/tests/test_runners/test_maven.py b/tests/test_runners/test_maven.py
index aa6669e6a..fc2d998ca 100644
--- a/tests/test_runners/test_maven.py
+++ b/tests/test_runners/test_maven.py
@@ -242,3 +242,109 @@ def test_glob(self):
'foo/Util.class',
]:
self.assertFalse(maven.is_file(x))
+
+ def test_parse_surefire_reports_valid(self):
+ """Test parsing valid surefire reports from test data directory"""
+ # Change to test data directory with surefire reports
+ original_dir = os.getcwd()
+ test_data_dir = str(self.test_files_dir.joinpath('dryrun-test').resolve())
+
+ try:
+ os.chdir(test_data_dir)
+ tests = maven.parse_surefire_reports()
+
+ # Should find test reports (at least 2)
+ self.assertGreaterEqual(len(tests), 2)
+
+ # Verify test class names
+ test_names = {test['name'] for test in tests}
+ self.assertIn('com.launchable.demo.CalculatorTest', test_names)
+ self.assertIn('com.launchable.demo.MixedTagsTest', test_names)
+
+ # Verify format
+ for test in tests:
+ self.assertEqual(test['type'], 'class')
+ self.assertIsInstance(test['name'], str)
+ finally:
+ os.chdir(original_dir)
+
+ def test_parse_surefire_reports_no_files(self):
+ """Test parsing when no surefire reports exist"""
+ # Create empty temp directory
+ with tempfile.TemporaryDirectory() as temp_dir:
+ original_dir = os.getcwd()
+ try:
+ os.chdir(temp_dir)
+ tests = maven.parse_surefire_reports()
+
+ # Should return empty list when no reports found
+ self.assertEqual(tests, [])
+ finally:
+ os.chdir(original_dir)
+
+ def test_parse_surefire_reports_invalid_xml(self):
+ """Test parsing handles invalid XML gracefully"""
+ with tempfile.TemporaryDirectory() as temp_dir:
+ original_dir = os.getcwd()
+ try:
+ os.chdir(temp_dir)
+
+ # Create target/surefire-reports directory
+ os.makedirs('target/surefire-reports', exist_ok=True)
+
+ # Create invalid XML file
+ invalid_xml_path = os.path.join('target/surefire-reports', 'TEST-Invalid.xml')
+ with open(invalid_xml_path, 'w') as f:
+ f.write('')
+
+ # Should handle parse error gracefully
+ tests = maven.parse_surefire_reports()
+
+ # Should return empty list (invalid file skipped)
+ self.assertEqual(tests, [])
+ finally:
+ os.chdir(original_dir)
+
+ @responses.activate
+ @mock.patch.dict(os.environ, {"LAUNCHABLE_TOKEN": CliTestCase.launchable_token})
+ def test_subset_with_scan_dryrun_results(self):
+ """Test subset command with --scan-dryrun-results flag"""
+ # Change to test data directory with surefire reports
+ original_dir = os.getcwd()
+ test_data_dir = str(self.test_files_dir.joinpath('dryrun-test').resolve())
+
+ try:
+ os.chdir(test_data_dir)
+
+ result = self.cli('subset', '--target', '10%', '--session',
+ self.session, 'maven', '--scan-dryrun-results')
+
+ self.assert_success(result)
+
+ # Verify output mentions scanning dry-run results
+ self.assertIn('Scanning Maven dry-run results', result.output)
+ self.assertIn('Found', result.output)
+ self.assertIn('surefire report', result.output)
+ finally:
+ os.chdir(original_dir)
+
+ @responses.activate
+ @mock.patch.dict(os.environ, {"LAUNCHABLE_TOKEN": CliTestCase.launchable_token})
+ def test_subset_with_scan_dryrun_results_no_reports(self):
+ """Test subset command with --scan-dryrun-results when no reports exist"""
+ with tempfile.TemporaryDirectory() as temp_dir:
+ original_dir = os.getcwd()
+ try:
+ os.chdir(temp_dir)
+
+ result = self.cli('subset', '--target', '10%', '--session',
+ self.session, 'maven', '--scan-dryrun-results')
+
+ # Should still succeed but show warning
+ self.assert_success(result)
+
+ # Verify warning message displayed
+ self.assertIn('Warning: No surefire reports found', result.output)
+ self.assertIn('mvn test -DdryRun=true', result.output)
+ finally:
+ os.chdir(original_dir)