diff --git a/maven/basic/.gitignore b/maven/basic/.gitignore new file mode 100644 index 00000000..cf9c443c --- /dev/null +++ b/maven/basic/.gitignore @@ -0,0 +1,39 @@ +# Java + +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + +# Maven + +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next +release.properties +dependency-reduced-pom.xml +buildNumber.properties +.mvn/timing.properties +# https://github.com/takari/maven-wrapper#usage-without-binary-jar +.mvn/wrapper/maven-wrapper.jar diff --git a/maven/basic/README.md b/maven/basic/README.md new file mode 100644 index 00000000..56b2120a --- /dev/null +++ b/maven/basic/README.md @@ -0,0 +1,55 @@ +# Maven Basic Example + +This is a simple Maven project using JUnit 4 to demonstrate basic Launchable integration. + +## Project Structure + +- Simple Maven project with JUnit 4 +- Two test classes: `AppTest.java` and `App2Test.java` +- Basic "Hello World" application + +## Running Tests + +```sh +# Run all tests +mvn test +``` + +## Recording Test Results + +```sh +# Record build +BUILD_NAME=my-build-$(date +%s) +launchable record build --name ${BUILD_NAME} --source . + +# Run tests +mvn test + +# Record test results +launchable record tests --build ${BUILD_NAME} maven surefire ./target/surefire-reports +``` + +## Subsetting Test Runs + +```sh +# Set up variables +BUILD_NAME=my-build-$(date +%s) +TARGET="80%" + +# Record build +launchable record build --name ${BUILD_NAME} --source . + +# Request subset +launchable subset --target ${TARGET} --build ${BUILD_NAME} maven surefire ./target/surefire-reports > subset.txt + +# Run only the subset +mvn test -Dtest=$(cat subset.txt | tr '\n' ',') + +# Record the subset results +launchable record tests --build ${BUILD_NAME} maven surefire ./target/surefire-reports +``` + +## Learn More + +- [Launchable Documentation](https://www.launchableinc.com/docs/) +- [Maven Surefire Plugin](https://maven.apache.org/surefire/maven-surefire-plugin/) diff --git a/maven/pom.xml b/maven/basic/pom.xml similarity index 100% rename from maven/pom.xml rename to maven/basic/pom.xml diff --git a/maven/src/main/java/com/launchableinc/rocket_car_maven/App.java b/maven/basic/src/main/java/com/launchableinc/rocket_car_maven/App.java similarity index 100% rename from maven/src/main/java/com/launchableinc/rocket_car_maven/App.java rename to maven/basic/src/main/java/com/launchableinc/rocket_car_maven/App.java diff --git a/maven/src/test/java/com/launchableinc/rocket_car_maven/App2Test.java b/maven/basic/src/test/java/com/launchableinc/rocket_car_maven/App2Test.java similarity index 100% rename from maven/src/test/java/com/launchableinc/rocket_car_maven/App2Test.java rename to maven/basic/src/test/java/com/launchableinc/rocket_car_maven/App2Test.java diff --git a/maven/src/test/java/com/launchableinc/rocket_car_maven/AppTest.java b/maven/basic/src/test/java/com/launchableinc/rocket_car_maven/AppTest.java similarity index 100% rename from maven/src/test/java/com/launchableinc/rocket_car_maven/AppTest.java rename to maven/basic/src/test/java/com/launchableinc/rocket_car_maven/AppTest.java diff --git a/maven/test-exclusion/.gitignore b/maven/test-exclusion/.gitignore new file mode 100644 index 00000000..cf9c443c --- /dev/null +++ b/maven/test-exclusion/.gitignore @@ -0,0 +1,39 @@ +# Java + +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + +# Maven + +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next +release.properties +dependency-reduced-pom.xml +buildNumber.properties +.mvn/timing.properties +# https://github.com/takari/maven-wrapper#usage-without-binary-jar +.mvn/wrapper/maven-wrapper.jar diff --git a/maven/test-exclusion/README.md b/maven/test-exclusion/README.md new file mode 100644 index 00000000..ec2893b7 --- /dev/null +++ b/maven/test-exclusion/README.md @@ -0,0 +1,280 @@ +# Maven Test Exclusion Example with Launchable + +This example demonstrates how to use the `--scan-dryrun-results` feature with Maven to discover **all** tests, including those excluded by JUnit 5 tags, for intelligent subset selection with Launchable. + +## Problem Statement + +When using Maven's `` configuration to filter tests, traditional test discovery only finds tests that will actually run. This creates a problem: + +- **Normal discovery**: Discovers only 10 tests (tests that will run) +- **Excluded tests**: 11 tests are filtered out and invisible to Launchable +- **Result**: Launchable cannot optimize subset selection for the full test suite + +The `--scan-dryrun-results` feature solves this by discovering **all 21 tests** (including excluded ones), enabling Launchable to: +- Understand your complete test inventory +- Make better subset optimization decisions +- Track historical data for all tests, even when exclusions change + +## Test Organization + +This project contains **21 tests across 5 test classes**: + +### Tests That Run (10 tests) +- `CalculatorTest` - 6 untagged tests (basic calculator operations) +- `MixedTagsTest` - 3 untagged tests (will run) +- `ExcludeTestApplicationTests` - 1 Spring Boot context test + +### Tests That Are Excluded (11 tests) +- `UserServiceIntegrationTest` - 4 tests with `@Tag("IntegrationTest")` (excluded) +- `ApplicationSmokeTest` - 5 tests with `@Tag("SmokeTest")` (excluded) +- `MixedTagsTest` - 2 tests with exclusion tags (excluded) + +### Maven Configuration + +The `pom.xml` configures test exclusions: + +```xml + + org.apache.maven.plugins + maven-surefire-plugin + 3.0.0-M5 + + IntegrationTest,SmokeTest + + +``` + +## Running Tests + +### Run all non-excluded tests (10 tests) +```sh +mvn test +``` + +### Run with dry-run mode to discover all tests (21 tests) +```sh +mvn test -DdryRun=true +``` + +This generates Surefire reports in `target/surefire-reports/` for all tests, including excluded ones. + +### Run all tests (ignoring exclusions) +```sh +mvn test -Dtest.excludedGroups= +``` + +## Launchable Integration + +### Normal Discovery Mode (Without `--scan-dryrun-results`) + +When using standard test discovery, Launchable only sees tests that actually run: + +```sh +# Record build +BUILD_NAME=my-build-$(date +%s) +launchable record build --name ${BUILD_NAME} --source . + +# Run tests normally +mvn test + +# Record test results - only discovers 9 tests +launchable record tests --build ${BUILD_NAME} maven surefire ./target/surefire-reports +``` + +**Result**: Launchable discovers **10 tests** (excluded tests are invisible) + +### With `--scan-dryrun-results` (Recommended) + +Using dry-run mode, Launchable discovers all tests including excluded ones: + +```sh +# Record build +BUILD_NAME=my-build-$(date +%s) +launchable record build --name ${BUILD_NAME} --source . + +# Generate dry-run reports (discovers all 20 tests) +mvn test -DdryRun=true + +# Scan dry-run results to discover all tests +launchable subset maven \ + --scan-dryrun-results \ + --build ${BUILD_NAME} \ + --target 10% \ + ./target/surefire-reports + +# Run the subset +mvn test -Dtest=$(cat subset.txt | tr '\n' ',') + +# Record actual test results +launchable record tests --build ${BUILD_NAME} maven surefire ./target/surefire-reports +``` + +**Result**: Launchable discovers **all 21 tests** and makes better optimization decisions + +## Demo Scripts + +This example includes three demo scripts to explore test discovery: + +### 1. `discover-tests-maven.sh` +Demonstrates Maven-based test discovery showing the difference between normal and dry-run modes. + +```sh +./discover-tests-maven.sh +``` + +**Output**: +- Extracts Maven exclusion configuration +- Discovers all 21 tests (without exclusions) +- Discovers 10 tests that will run (with exclusions) +- Shows which 11 tests are excluded and why + +### 2. `compare-tests-demo.sh` +Quick comparison showing how test discovery differs with and without exclusions. + +```sh +./compare-tests-demo.sh +``` + +**Output**: +- Creates `all-tests.txt` (21 tests) +- Creates `included-tests.txt` (10 tests) +- Creates `excluded-tests.txt` (11 tests) +- Side-by-side comparison + +### 3. `ci-pipeline-example.sh` +Complete CI/CD pipeline example integrating with Launchable CLI. + +```sh +# Set environment variables +export LAUNCHABLE_TOKEN='v1:your-org/your-workspace:your-token' +export BUILD_ID='my-build-123' +export SUBSET_TARGET='60%' + +./ci-pipeline-example.sh +``` + +**Pipeline steps**: +1. Compile tests +2. Extract Maven exclusion configuration +3. Discover tests and identify exclusions +4. Record build with Launchable +5. Request test subset +6. Run subset +7. Record results + +## Manual Testing with Your Own Project + +To manually test the `--scan-dryrun-results` feature with your own Maven project: + +```sh +# 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' --source . + +# 6. Create a session and scan dry-run results +launchable subset maven \ + --scan-dryrun-results \ + --build 'test-build-name' \ + --target 10% \ + ./target/surefire-reports + +# 7. Compare with normal discovery (optional) +mvn test +launchable record tests --build 'test-build-name' maven surefire ./target/surefire-reports +# Check: How many tests were discovered? Only non-excluded tests! +``` + +## Key Differences + +| Discovery Mode | Tests Discovered | Use Case | +|---|---|---| +| **Normal** (`mvn test`) | 10 tests (only non-excluded) | Standard test execution | +| **Dry-run** (`mvn test -DdryRun=true`) | 21 tests (all tests) | Discovery for Launchable subset optimization | +| **No exclusions** (`mvn test -Dtest.excludedGroups=`) | 21 tests (all tests run) | Running complete test suite | + +## Test Files + +### Main Source Files +- `Calculator.java` - Simple calculator with basic operations +- `UserService.java` - User service for integration tests +- `TestDiscoveryUtil.java` - Utility for test discovery +- `ExcludeTestApplication.java` - Spring Boot application entry point + +### Test Files +- `CalculatorTest.java` - 6 tests (untagged, always run) +- `MixedTagsTest.java` - 5 tests (3 untagged + 2 with exclusion tags) +- `UserServiceIntegrationTest.java` - 4 tests (`@Tag("IntegrationTest")`) +- `ApplicationSmokeTest.java` - 5 tests (`@Tag("SmokeTest")`) +- `ExcludeTestApplicationTests.java` - Spring Boot context test + +## Understanding Surefire Reports + +When running `mvn test -DdryRun=true`, Maven generates two types of reports: + +### XML Reports (`TEST-*.xml`) +```xml + + + ... + +``` + +### Text Reports (`*.txt`) +``` +Test set: com.launchable.demo.CalculatorTest +Tests run: 6, Failures: 0, Errors: 0, Skipped: 0 +``` + +Both formats include metadata for **all discovered tests**, making them ideal for the `--scan-dryrun-results` feature. + +## Troubleshooting + +### "No POM in this directory" Error +```sh +$ mvn test -DdryRun=true +[ERROR] The goal you specified requires a project to execute but there is no POM in this directory +``` + +**Solution**: Make sure you're running the command from the project root directory containing `pom.xml`. + +### Launchable Not Discovering All Tests +If Launchable still shows only 10 tests instead of 21: + +1. Verify dry-run reports were generated: + ```sh + ls -la target/surefire-reports/TEST-*.xml | wc -l + # Should show reports for all test classes + ``` + +2. Make sure you're using `--scan-dryrun-results` flag: + ```sh + launchable subset maven --scan-dryrun-results ... + ``` + +3. Check the Surefire plugin version in `pom.xml` (should be 3.0.0-M5 or later for best results). + +## Learn More + +- [Launchable Documentation](https://www.launchableinc.com/docs/) +- [Maven Surefire Plugin](https://maven.apache.org/surefire/maven-surefire-plugin/) +- [JUnit 5 Tagging and Filtering](https://junit.org/junit5/docs/current/user-guide/#writing-tests-tagging-and-filtering) + +## Summary + +This example demonstrates: +- ✅ How Maven's `` filters tests at runtime +- ✅ Why standard discovery misses excluded tests (9 discovered vs 20 total) +- ✅ How `--scan-dryrun-results` discovers all tests for better optimization +- ✅ Complete Launchable integration workflow with test exclusions +- ✅ Scripts to explore and compare test discovery methods diff --git a/maven/test-exclusion/pom.xml b/maven/test-exclusion/pom.xml new file mode 100644 index 00000000..63cba30b --- /dev/null +++ b/maven/test-exclusion/pom.xml @@ -0,0 +1,125 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 4.0.5 + + + com.example + excludeTestApplication + 0.0.1-SNAPSHOT + excludeTestApplication + excludeTestApplication + + + + + + + + + + + + + + + 17 + + + IntegrationTest,SmokeTest + + + + + org.springframework.boot + spring-boot-starter + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + org.assertj + assertj-core + 3.25.3 + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.2.5 + + + ${test.excludedGroups} + + + true + + + ${project.build.directory}/surefire-reports + + + + + + org.apache.maven.plugins + maven-help-plugin + 3.4.0 + + + + + + + + all-tests + + + + org.apache.maven.plugins + maven-surefire-plugin + + + + + + + + + + + integration-tests + + + + org.apache.maven.plugins + maven-surefire-plugin + + + IntegrationTest + + + + + + + + diff --git a/maven/test-exclusion/src/main/java/com/example/excludetestapplication/ExcludeTestApplication.java b/maven/test-exclusion/src/main/java/com/example/excludetestapplication/ExcludeTestApplication.java new file mode 100644 index 00000000..7bcf8c29 --- /dev/null +++ b/maven/test-exclusion/src/main/java/com/example/excludetestapplication/ExcludeTestApplication.java @@ -0,0 +1,13 @@ +package com.example.excludetestapplication; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class ExcludeTestApplication { + + public static void main(String[] args) { + SpringApplication.run(ExcludeTestApplication.class, args); + } + +} diff --git a/maven/test-exclusion/src/main/java/com/launchable/demo/Calculator.java b/maven/test-exclusion/src/main/java/com/launchable/demo/Calculator.java new file mode 100644 index 00000000..d6988d15 --- /dev/null +++ b/maven/test-exclusion/src/main/java/com/launchable/demo/Calculator.java @@ -0,0 +1,31 @@ +package com.launchable.demo; + +/** + * Simple calculator class for demonstration purposes. + * This is the production code being tested. + */ +public class Calculator { + + public int add(int a, int b) { + return a + b; + } + + public int subtract(int a, int b) { + return a - b; + } + + public int multiply(int a, int b) { + return a * b; + } + + public int divide(int a, int b) { + if (b == 0) { + throw new IllegalArgumentException("Cannot divide by zero"); + } + return a / b; + } + + public double power(double base, double exponent) { + return Math.pow(base, exponent); + } +} diff --git a/maven/test-exclusion/src/main/java/com/launchable/demo/TestDiscoveryUtil.java b/maven/test-exclusion/src/main/java/com/launchable/demo/TestDiscoveryUtil.java new file mode 100644 index 00000000..1ba0256e --- /dev/null +++ b/maven/test-exclusion/src/main/java/com/launchable/demo/TestDiscoveryUtil.java @@ -0,0 +1,270 @@ +package com.launchable.demo; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Utility to discover test methods and their @Tag annotations. + * + * This demonstrates how to programmatically identify which tests + * will be excluded by Maven's configuration. + * + * Usage: + * mvn compile exec:java -Dexec.mainClass="com.launchable.demo.TestDiscoveryUtil" \ + * -Dexec.args="IntegrationTest,SmokeTest" + */ +public class TestDiscoveryUtil { + + private static class TestInfo { + String className; + String methodName; + Set tags = new HashSet<>(); + + String getFullName() { + return className + "#" + methodName; + } + + boolean hasAnyTag(Set excludedTags) { + return !Collections.disjoint(tags, excludedTags); + } + } + + public static void main(String[] args) { + String excludedGroupsArg = args.length > 0 ? args[0] : "IntegrationTest,SmokeTest"; + Set excludedGroups = Arrays.stream(excludedGroupsArg.split(",")) + .map(String::trim) + .collect(Collectors.toSet()); + + System.out.println("=".repeat(70)); + System.out.println("Test Discovery Utility"); + System.out.println("=".repeat(70)); + System.out.println(); + System.out.println("Excluded Groups: " + excludedGroups); + System.out.println(); + + try { + List allTests = discoverTests(); + + List includedTests = allTests.stream() + .filter(test -> !test.hasAnyTag(excludedGroups)) + .collect(Collectors.toList()); + + List excludedTests = allTests.stream() + .filter(test -> test.hasAnyTag(excludedGroups)) + .collect(Collectors.toList()); + + System.out.println("TESTS THAT WILL RUN (" + includedTests.size() + " tests)"); + System.out.println("-".repeat(70)); + for (TestInfo test : includedTests) { + String tagsStr = test.tags.isEmpty() ? "no tags" : "tags: " + test.tags; + System.out.println(" ✓ " + test.getFullName() + " (" + tagsStr + ")"); + } + + System.out.println(); + System.out.println("TESTS THAT WILL BE EXCLUDED (" + excludedTests.size() + " tests)"); + System.out.println("-".repeat(70)); + for (TestInfo test : excludedTests) { + System.out.println(" ✗ " + test.getFullName() + " (tags: " + test.tags + ")"); + } + + System.out.println(); + System.out.println("=".repeat(70)); + System.out.println("SUMMARY"); + System.out.println("=".repeat(70)); + System.out.println("Total Tests: " + allTests.size()); + System.out.println("Tests to Run: " + includedTests.size()); + System.out.println("Tests to Exclude: " + excludedTests.size()); + System.out.println(); + + // Generate JSON output + generateJsonOutput(includedTests, excludedTests, excludedGroups); + + } catch (IOException e) { + System.err.println("Error discovering tests: " + e.getMessage()); + e.printStackTrace(); + System.exit(1); + } + } + + private static List discoverTests() throws IOException { + List tests = new ArrayList<>(); + + Path testSourceRoot = Paths.get("src/test/java"); + if (!Files.exists(testSourceRoot)) { + System.err.println("Test source directory not found: " + testSourceRoot); + return tests; + } + + try (Stream paths = Files.walk(testSourceRoot)) { + List testFiles = paths + .filter(Files::isRegularFile) + .filter(p -> p.toString().endsWith("Test.java") || p.toString().endsWith("Tests.java")) + .collect(Collectors.toList()); + + for (Path file : testFiles) { + tests.addAll(parseTestFile(file)); + } + } + + return tests; + } + + private static List parseTestFile(Path file) throws IOException { + List tests = new ArrayList<>(); + List lines = Files.readAllLines(file); + + // Extract package and class name + String packageName = ""; + String className = ""; + + for (String line : lines) { + if (line.trim().startsWith("package ")) { + packageName = line.trim() + .substring(8) + .replace(";", "") + .trim(); + } + } + + className = file.getFileName().toString().replace(".java", ""); + String fullClassName = packageName.isEmpty() ? className : packageName + "." + className; + + // Extract class-level tags + Set classTags = new HashSet<>(); + for (int i = 0; i < lines.size(); i++) { + String line = lines.get(i).trim(); + if (line.startsWith("@Tag(")) { + String tag = extractTag(line); + if (tag != null) { + classTags.add(tag); + } + } + if (line.startsWith("public class ") || line.startsWith("class ")) { + break; // Found class declaration + } + } + + // Extract test methods and their tags + for (int i = 0; i < lines.size(); i++) { + String line = lines.get(i).trim(); + + if (line.equals("@Test") || line.startsWith("@Test(")) { + // Look ahead for method name + Set methodTags = new HashSet<>(classTags); + + // Look backwards for method-level tags + for (int j = i - 1; j >= 0 && j > i - 10; j--) { + String prevLine = lines.get(j).trim(); + if (prevLine.startsWith("@Tag(")) { + String tag = extractTag(prevLine); + if (tag != null) { + methodTags.add(tag); + } + } + if (prevLine.isEmpty() || prevLine.startsWith("//") || prevLine.startsWith("/*")) { + continue; + } + if (!prevLine.startsWith("@")) { + break; + } + } + + // Find method name + for (int j = i + 1; j < lines.size() && j < i + 5; j++) { + String nextLine = lines.get(j).trim(); + if (nextLine.contains("void ") && nextLine.contains("(")) { + String methodName = extractMethodName(nextLine); + if (methodName != null) { + TestInfo test = new TestInfo(); + test.className = fullClassName; + test.methodName = methodName; + test.tags = methodTags; + tests.add(test); + break; + } + } + } + } + } + + return tests; + } + + private static String extractTag(String line) { + // Extract tag from @Tag("TagName") + int start = line.indexOf("\""); + int end = line.lastIndexOf("\""); + if (start != -1 && end != -1 && start < end) { + return line.substring(start + 1, end); + } + return null; + } + + private static String extractMethodName(String line) { + // Extract method name from "public void methodName()" + int voidIndex = line.indexOf("void "); + int parenIndex = line.indexOf("("); + if (voidIndex != -1 && parenIndex != -1 && voidIndex < parenIndex) { + return line.substring(voidIndex + 5, parenIndex).trim(); + } + return null; + } + + private static void generateJsonOutput(List includedTests, + List excludedTests, + Set excludedGroups) { + try { + Path outputPath = Paths.get("target/test-discovery.json"); + Files.createDirectories(outputPath.getParent()); + + StringBuilder json = new StringBuilder(); + json.append("{\n"); + json.append(" \"exclusionRules\": {\n"); + json.append(" \"excludedGroups\": \"").append(String.join(",", excludedGroups)).append("\"\n"); + json.append(" },\n"); + json.append(" \"testSummary\": {\n"); + json.append(" \"totalTests\": ").append(includedTests.size() + excludedTests.size()).append(",\n"); + json.append(" \"includedTests\": ").append(includedTests.size()).append(",\n"); + json.append(" \"excludedTests\": ").append(excludedTests.size()).append("\n"); + json.append(" },\n"); + json.append(" \"excludedTestList\": [\n"); + + for (int i = 0; i < excludedTests.size(); i++) { + TestInfo test = excludedTests.get(i); + json.append(" {\"test\": \"").append(test.getFullName()).append("\", "); + json.append("\"excludedBy\": \"").append(String.join(",", test.tags)).append("\"}"); + if (i < excludedTests.size() - 1) { + json.append(","); + } + json.append("\n"); + } + + json.append(" ],\n"); + json.append(" \"includedTestList\": [\n"); + + for (int i = 0; i < includedTests.size(); i++) { + TestInfo test = includedTests.get(i); + json.append(" {\"test\": \"").append(test.getFullName()).append("\"}"); + if (i < includedTests.size() - 1) { + json.append(","); + } + json.append("\n"); + } + + json.append(" ]\n"); + json.append("}\n"); + + Files.write(outputPath, json.toString().getBytes()); + System.out.println("✓ JSON output written to: " + outputPath); + + } catch (IOException e) { + System.err.println("Error writing JSON output: " + e.getMessage()); + } + } +} diff --git a/maven/test-exclusion/src/main/java/com/launchable/demo/UserService.java b/maven/test-exclusion/src/main/java/com/launchable/demo/UserService.java new file mode 100644 index 00000000..1f34674e --- /dev/null +++ b/maven/test-exclusion/src/main/java/com/launchable/demo/UserService.java @@ -0,0 +1,23 @@ +package com.launchable.demo; + +/** + * User service class that would typically interact with a database. + * Used for integration test demonstrations. + */ +public class UserService { + + public String getUserById(String userId) { + // In real implementation, this would query a database + return "User_" + userId; + } + + public boolean saveUser(String userId, String name) { + // In real implementation, this would save to database + return true; + } + + public boolean deleteUser(String userId) { + // In real implementation, this would delete from database + return true; + } +} diff --git a/maven/test-exclusion/src/main/resources/application.properties b/maven/test-exclusion/src/main/resources/application.properties new file mode 100644 index 00000000..01898c94 --- /dev/null +++ b/maven/test-exclusion/src/main/resources/application.properties @@ -0,0 +1 @@ +spring.application.name=excludeTestApplication diff --git a/maven/test-exclusion/src/test/java/com/example/excludetestapplication/ExcludeTestApplicationTests.java b/maven/test-exclusion/src/test/java/com/example/excludetestapplication/ExcludeTestApplicationTests.java new file mode 100644 index 00000000..0c8406db --- /dev/null +++ b/maven/test-exclusion/src/test/java/com/example/excludetestapplication/ExcludeTestApplicationTests.java @@ -0,0 +1,13 @@ +package com.example.excludetestapplication; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class ExcludeTestApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/maven/test-exclusion/src/test/java/com/launchable/demo/ApplicationSmokeTest.java b/maven/test-exclusion/src/test/java/com/launchable/demo/ApplicationSmokeTest.java new file mode 100644 index 00000000..be8cab33 --- /dev/null +++ b/maven/test-exclusion/src/test/java/com/launchable/demo/ApplicationSmokeTest.java @@ -0,0 +1,74 @@ +package com.launchable.demo; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.DisplayName; +import static org.assertj.core.api.Assertions.*; + +/** + * Smoke tests for the application. + * Tagged with @Tag("SmokeTest") - these will be EXCLUDED by Maven default config. + * + * Smoke tests are typically: + * - Quick sanity checks after deployment + * - Basic connectivity/health checks + * - Run in production-like environments + * - Separate from unit test pipeline + */ +@Tag("SmokeTest") +@DisplayName("Application Smoke Tests") +public class ApplicationSmokeTest { + + @Test + @DisplayName("Application should start successfully") + public void testApplicationStarts() { + // In real scenario, this would check if the app is running + assertThat(true).isTrue(); + + System.out.println("✗ ApplicationSmokeTest.testApplicationStarts() - SHOULD BE EXCLUDED BY MAVEN"); + } + + @Test + @DisplayName("Health endpoint should respond") + public void testHealthEndpoint() { + // In real scenario, this would call /health endpoint + String healthStatus = "OK"; + + assertThat(healthStatus).isEqualTo("OK"); + + System.out.println("✗ ApplicationSmokeTest.testHealthEndpoint() - SHOULD BE EXCLUDED BY MAVEN"); + } + + @Test + @DisplayName("Database connection should be available") + public void testDatabaseConnectivity() { + // In real scenario, this would try to connect to database + boolean isConnected = true; + + assertThat(isConnected).isTrue(); + + System.out.println("✗ ApplicationSmokeTest.testDatabaseConnectivity() - SHOULD BE EXCLUDED BY MAVEN"); + } + + @Test + @DisplayName("External API should be reachable") + public void testExternalApiConnectivity() { + // In real scenario, this would ping external APIs + boolean isReachable = true; + + assertThat(isReachable).isTrue(); + + System.out.println("✗ ApplicationSmokeTest.testExternalApiConnectivity() - SHOULD BE EXCLUDED BY MAVEN"); + } + + @Test + @DisplayName("Cache service should be responsive") + public void testCacheService() { + // In real scenario, this would check Redis/Memcached + boolean isCacheAvailable = true; + + assertThat(isCacheAvailable).isTrue(); + + System.out.println("✗ ApplicationSmokeTest.testCacheService() - SHOULD BE EXCLUDED BY MAVEN"); + } +} diff --git a/maven/test-exclusion/src/test/java/com/launchable/demo/CalculatorTest.java b/maven/test-exclusion/src/test/java/com/launchable/demo/CalculatorTest.java new file mode 100644 index 00000000..7474d4a5 --- /dev/null +++ b/maven/test-exclusion/src/test/java/com/launchable/demo/CalculatorTest.java @@ -0,0 +1,71 @@ +package com.launchable.demo; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.DisplayName; +import static org.assertj.core.api.Assertions.*; + +/** + * Unit tests for Calculator class. + * These tests have NO @Tag annotation, so they WILL RUN with default Maven config. + * + * These represent normal unit tests that customers want to run in their CI pipeline. + */ +@DisplayName("Calculator Unit Tests") +public class CalculatorTest { + + private final Calculator calculator = new Calculator(); + + @Test + @DisplayName("Addition should work correctly") + public void testAddition() { + assertThat(calculator.add(2, 3)).isEqualTo(5); + assertThat(calculator.add(-1, 1)).isEqualTo(0); + assertThat(calculator.add(0, 0)).isEqualTo(0); + System.out.println("✓ CalculatorTest.testAddition() EXECUTED"); + } + + @Test + @DisplayName("Subtraction should work correctly") + public void testSubtraction() { + assertThat(calculator.subtract(5, 3)).isEqualTo(2); + assertThat(calculator.subtract(0, 5)).isEqualTo(-5); + assertThat(calculator.subtract(10, 10)).isEqualTo(0); + System.out.println("✓ CalculatorTest.testSubtraction() EXECUTED"); + } + + @Test + @DisplayName("Multiplication should work correctly") + public void testMultiplication() { + assertThat(calculator.multiply(2, 3)).isEqualTo(6); + assertThat(calculator.multiply(-2, 3)).isEqualTo(-6); + assertThat(calculator.multiply(0, 100)).isEqualTo(0); + System.out.println("✓ CalculatorTest.testMultiplication() EXECUTED"); + } + + @Test + @DisplayName("Division should work correctly") + public void testDivision() { + assertThat(calculator.divide(10, 2)).isEqualTo(5); + assertThat(calculator.divide(9, 3)).isEqualTo(3); + assertThat(calculator.divide(-10, 2)).isEqualTo(-5); + System.out.println("✓ CalculatorTest.testDivision() EXECUTED"); + } + + @Test + @DisplayName("Division by zero should throw exception") + public void testDivisionByZero() { + assertThatThrownBy(() -> calculator.divide(10, 0)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Cannot divide by zero"); + System.out.println("✓ CalculatorTest.testDivisionByZero() EXECUTED"); + } + + @Test + @DisplayName("Power function should work correctly") + public void testPower() { + assertThat(calculator.power(2, 3)).isEqualTo(8.0); + assertThat(calculator.power(5, 2)).isEqualTo(25.0); + assertThat(calculator.power(10, 0)).isEqualTo(1.0); + System.out.println("✓ CalculatorTest.testPower() EXECUTED"); + } +} diff --git a/maven/test-exclusion/src/test/java/com/launchable/demo/MixedTagsTest.java b/maven/test-exclusion/src/test/java/com/launchable/demo/MixedTagsTest.java new file mode 100644 index 00000000..2f0b8f15 --- /dev/null +++ b/maven/test-exclusion/src/test/java/com/launchable/demo/MixedTagsTest.java @@ -0,0 +1,55 @@ +package com.launchable.demo; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.DisplayName; +import static org.assertj.core.api.Assertions.*; + +/** + * Test class demonstrating mixed tagging scenarios. + * Some tests are excluded, some are not. + * + * This shows that exclusion works at the METHOD level, not just CLASS level. + */ +@DisplayName("Mixed Tags Test Suite") +public class MixedTagsTest { + + @Test + @DisplayName("Regular unit test - will run") + public void testRegularUnitTest() { + assertThat(1 + 1).isEqualTo(2); + System.out.println("✓ MixedTagsTest.testRegularUnitTest() EXECUTED"); + } + + @Test + @Tag("IntegrationTest") + @DisplayName("Integration test in mixed class - will be excluded") + public void testIntegrationInMixedClass() { + assertThat(true).isTrue(); + System.out.println("✗ MixedTagsTest.testIntegrationInMixedClass() - SHOULD BE EXCLUDED"); + } + + @Test + @Tag("SmokeTest") + @DisplayName("Smoke test in mixed class - will be excluded") + public void testSmokeInMixedClass() { + assertThat(true).isTrue(); + System.out.println("✗ MixedTagsTest.testSmokeInMixedClass() - SHOULD BE EXCLUDED"); + } + + @Test + @DisplayName("Another regular test - will run") + public void testAnotherRegularTest() { + assertThat("hello").isNotEmpty(); + System.out.println("✓ MixedTagsTest.testAnotherRegularTest() EXECUTED"); + } + + @Test + @Tag("Performance") + @DisplayName("Performance test - NOT excluded (not in excludedGroups)") + public void testPerformance() { + // This tag is not in excludedGroups, so it will run + assertThat(System.currentTimeMillis()).isPositive(); + System.out.println("✓ MixedTagsTest.testPerformance() EXECUTED (Performance tag not excluded)"); + } +} diff --git a/maven/test-exclusion/src/test/java/com/launchable/demo/UserServiceIntegrationTest.java b/maven/test-exclusion/src/test/java/com/launchable/demo/UserServiceIntegrationTest.java new file mode 100644 index 00000000..d44d47c6 --- /dev/null +++ b/maven/test-exclusion/src/test/java/com/launchable/demo/UserServiceIntegrationTest.java @@ -0,0 +1,71 @@ +package com.launchable.demo; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.DisplayName; +import static org.assertj.core.api.Assertions.*; + +/** + * Integration tests for UserService. + * Tagged with @Tag("IntegrationTest") - these will be EXCLUDED by Maven default config. + * + * These tests would typically require: + * - Database connection + * - Test data setup/teardown + * - Longer execution time + * - External dependencies + * + * Customers exclude these from regular CI runs and run them separately. + */ +@Tag("IntegrationTest") +@DisplayName("User Service Integration Tests") +public class UserServiceIntegrationTest { + + private final UserService userService = new UserService(); + + @Test + @DisplayName("Should retrieve user from database") + public void testGetUserById() { + // In real test, this would set up test data in database + String userId = "123"; + String result = userService.getUserById(userId); + + assertThat(result).isNotNull(); + assertThat(result).contains(userId); + + System.out.println("✗ UserServiceIntegrationTest.testGetUserById() - SHOULD BE EXCLUDED BY MAVEN"); + } + + @Test + @DisplayName("Should save user to database") + public void testSaveUser() { + // In real test, this would insert into database and verify + boolean result = userService.saveUser("456", "John Doe"); + + assertThat(result).isTrue(); + + System.out.println("✗ UserServiceIntegrationTest.testSaveUser() - SHOULD BE EXCLUDED BY MAVEN"); + } + + @Test + @DisplayName("Should delete user from database") + public void testDeleteUser() { + // In real test, this would delete from database and verify + boolean result = userService.deleteUser("789"); + + assertThat(result).isTrue(); + + System.out.println("✗ UserServiceIntegrationTest.testDeleteUser() - SHOULD BE EXCLUDED BY MAVEN"); + } + + @Test + @DisplayName("Should handle concurrent user operations") + @Tag("Slow") // Multiple tags on same test + public void testConcurrentOperations() { + // This test might take several seconds + assertThat(userService.saveUser("999", "Concurrent User")).isTrue(); + assertThat(userService.getUserById("999")).isNotNull(); + + System.out.println("✗ UserServiceIntegrationTest.testConcurrentOperations() - SHOULD BE EXCLUDED BY MAVEN"); + } +}