diff --git a/.github/workflows/anchore-sbom-evidence-example.yaml b/.github/workflows/anchore-sbom-evidence-example.yml similarity index 100% rename from .github/workflows/anchore-sbom-evidence-example.yaml rename to .github/workflows/anchore-sbom-evidence-example.yml diff --git a/.github/workflows/anchore-scan-evidence-example.yaml b/.github/workflows/anchore-scan-evidence-example.yml similarity index 100% rename from .github/workflows/anchore-scan-evidence-example.yaml rename to .github/workflows/anchore-scan-evidence-example.yml diff --git a/.github/workflows/junit-evidence-example.yml b/.github/workflows/junit-evidence-example.yml new file mode 100644 index 0000000..fde9e8c --- /dev/null +++ b/.github/workflows/junit-evidence-example.yml @@ -0,0 +1,103 @@ +name: "JUnit evidence integration example" +on: + workflow_dispatch: + +permissions: + id-token: write + contents: read + +jobs: + package-java-project-with-junit-evidence: + runs-on: ubuntu-latest + env: + REGISTRY_DOMAIN: ${{ vars.JF_URL }} + REPO_NAME: 'maven-junit-local' + PACKAGE_NAME: 'simple-java-maven' + VERSION: ${{ github.run_number }} + BUILD_NAME: 'junit-maven-build' + ATTACH_OPTIONAL_CUSTOM_MARKDOWN_TO_EVIDENCE: true + + steps: + # Setup JFrog CLI + - name: Setup jfrog cli + uses: jfrog/setup-jfrog-cli@v4 + env: + JF_URL: ${{ vars.ARTIFACTORY_URL }} + JF_ACCESS_TOKEN: ${{ secrets.ARTIFACTORY_ACCESS_TOKEN }} + + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + + - name: Cache Maven dependencies + uses: actions/cache@v4 + with: + path: ~/.m2 + key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} + restore-keys: ${{ runner.os }}-m2 + + # Build and publish the Java package to JFrog Artifactory + - name: Build and publish Java package to Artifactory + run: | + cd examples/junit/src + mvn clean compile + mvn package + echo "Deploying package to Artifactory" + jf rt upload target/*.jar $REPO_NAME/$PACKAGE_NAME/$VERSION/ --build-name=$BUILD_NAME --build-number=${{ github.run_number }} + echo "Publishing build info" + jf rt build-publish $BUILD_NAME ${{ github.run_number }} + + # Run JUnit tests and generate test reports + - name: Run JUnit tests + run: | + cd examples/junit/src + mvn test + + - name: Generate consolidated test report + run: | + cd examples/junit/src + mvn surefire-report:report-only + mvn site:site + + - name: Create unified test summary + if: always() + run: | + cd examples/junit/src + chmod +x merge-test-reports.sh + chmod +x xml-to-json.sh + ./merge-test-reports.sh + ./xml-to-json.sh + + # This is an optional step to generate a custom markdown report + - name: Generate optional custom markdown report + if: env.ATTACH_OPTIONAL_CUSTOM_MARKDOWN_TO_EVIDENCE == 'true' + run: | + cd examples/junit/src + if [ -f "target/consolidated-test-report.json" ]; then + python ../junit_json_to_markdown_helper.py target/consolidated-test-report.json + else + echo "# JUnit Test Results" > junit-results.md + echo "No test results found." >> junit-results.md + fi + + # Attaching the evidence to associated build + - name: Attach evidence using jfrog cli + run: | + cd examples/junit/src + if [ -f "target/consolidated-test-report.json" ]; then + jf evd create \ + --build-name $BUILD_NAME \ + --build-number ${{ github.run_number }} \ + --key "${{ secrets.PRIVATE_KEY }}" \ + --key-alias "${{ vars.EVIDENCE_KEY_ALIAS }}" \ + --predicate ./target/consolidated-test-report.json \ + --predicate-type https://jfrog.com/evidence/junit-test-results/v1 \ + ${{ env.ATTACH_OPTIONAL_CUSTOM_MARKDOWN_TO_EVIDENCE == 'true' && '--markdown "junit-results.md"' || '' }} + else + echo "No test results found to attach as evidence" + fi diff --git a/examples/junit/README.md b/examples/junit/README.md new file mode 100644 index 0000000..edab34d --- /dev/null +++ b/examples/junit/README.md @@ -0,0 +1,202 @@ +# **JUnit Test Evidence Example** + +This repository provides a working example of a GitHub Actions workflow that automates Java Maven project testing using **JUnit**. It then attaches the resulting test report as signed, verifiable evidence to the package in **JFrog Artifactory**. + +This workflow is an essential pattern for DevSecOps, creating a traceable, compliant, and secure software supply chain with comprehensive test coverage validation. + +### **Key Features** + +* **Automated Build & Deploy**: Builds a Java Maven project and deploys it to Artifactory. +* **JUnit Test Execution**: Runs comprehensive JUnit tests using Maven Surefire plugin. +* **Test Report Consolidation**: Merges multiple test reports into a single consolidated JSON format. +* **Optional Markdown Report**: Includes a helper script to generate a human-readable Markdown summary from the JUnit JSON results. +* **Signed Evidence Attachment**: Attaches the test results to the corresponding package version in Artifactory using jf evd create, cryptographically signing it for integrity. +* **JUnit**: [JUnit 5 testing framework](https://junit.org/junit5/) + +### **Workflow** + +The following diagram illustrates the sequence of operations performed by the GitHub Actions workflow. + +```mermaid +graph TD + A[Workflow Dispatch Trigger] --> B[Setup JFrog CLI] + B --> C[Checkout Repository] + C --> D[Setup JDK 17] + D --> E[Build and Publish Java Package to Artifactory] + E --> F[Run JUnit Tests] + F --> G[Generate Consolidated Test Report] + G --> H[Create Unified Test Summary] + H --> I{Attach Optional Custom Markdown Report?} + I -->|Yes| J[Generate Custom Markdown Report] + I -->|No| K[Skip Markdown Report] + J --> L[Attach Evidence to Package] + K --> L[Attach Evidence to Package] +``` + +--- + +### **1. Prerequisites** + +Before running this workflow, you must have: + +* JFrog CLI 2.65.0 or above (installed automatically in the workflow) +* An Artifactory repository of type maven (e.g., maven-junit-repo). +* A private key and a corresponding key alias configured in your JFrog Platform for signing evidence. +* The following GitHub repository variables: + * `JF_URL` (Artifactory base URL, e.g. `https://mycompany.jfrog.io`) + * `EVIDENCE_KEY_ALIAS` (Key alias for signing evidence) +* The following GitHub repository secrets: + * `ARTIFACTORY_ACCESS_TOKEN` (Artifactory access token) + * `PRIVATE_KEY` (Private key for signing evidence) + +### Environment Variables Used + +* `REGISTRY_DOMAIN` - Maven registry domain +* `REPO_NAME` - Maven repository name +* `PACKAGE_NAME` - Maven artifact name + +### **2. Configuration** + +To use this workflow, you must configure the following GitHub Repository Secrets and Variables. + +#### **GitHub Secrets** + +Navigate to Settings > Secrets and variables > Actions and create the following secrets: + +| Secret Name | Description | +| :---- | :---- | +| ARTIFACTORY_ACCESS_TOKEN | A valid JFrog Access Token with permissions to read, write, and annotate in your target repository. | +| PRIVATE_KEY | The private key used to sign the evidence. This key corresponds to the alias configured in JFrog Platform. | + +#### **GitHub Variables** + +Navigate to Settings > Secrets and variables > Actions and create the following variables: + +| Variable Name | Description | Example Value | +| :---- | :---- | :---- | +| JF_URL | The base URL of your JFrog Platform instance. | https://mycompany.jfrog.io | +| EVIDENCE_KEY_ALIAS | The alias for the public key in JFrog Platform used to verify the evidence signature. | my-signing-key-alias | + +#### **Workflow Environment Variables** + +You can also customize the workflow's behavior by modifying the env block in the .github/workflows/junit-evidence-example.yml file: + +| Variable Name | Description | Default Value | +| :---- | :---- | :---- | +| REPO_NAME | The name of the target Maven repository in Artifactory. | maven-junit-repo | +| PACKAGE_NAME | The name of the Maven artifact to be built and deployed. | simple-java-maven | +| BUILD_NAME | The name assigned to the build information in Artifactory. | junit-maven-build | +| ATTACH_OPTIONAL_CUSTOM_MARKDOWN_TO_EVIDENCE | Set to true to generate and attach a Markdown report alongside the JSON evidence. Set to false to skip this step. | true | + +--- + +### **3. Usage** + +This workflow is triggered manually. + +1. Navigate to the **Actions** tab of your forked repository. +2. In the left sidebar, click on the **JUnit evidence integration example** workflow. +3. Click the **Run workflow** dropdown button. You can leave the default branch selected. +4. Click the green **Run workflow** button. + +Once the workflow completes successfully, you can navigate to your repository in Artifactory (maven-junit-repo) and view the simple-java-maven artifact. Under the **Evidence** tab for the latest version, you will find the signed JUnit test results. + +### **How It Works: A Step-by-Step Breakdown** + +1. **Setup and Checkout**: The workflow begins by setting up the JFrog CLI and checking out the repository code. +2. **Java Environment Setup**: Configures JDK 17 and caches Maven dependencies for optimal performance. +3. **Build and Deploy Maven Package**: Uses Maven to compile and deploy the Java package to Artifactory, associating it with build information. +4. **Run JUnit Tests**: Executes JUnit tests using Maven Surefire plugin, generating XML test reports. +5. **Generate Consolidated Test Report**: Runs Maven site generation and Surefire reporting to create comprehensive test documentation. +6. **Create Unified Test Summary**: Executes custom scripts to merge multiple test reports into a single consolidated JSON format. +7. **Generate Optional Markdown Report**: If ATTACH_OPTIONAL_CUSTOM_MARKDOWN_TO_EVIDENCE is true, a Python helper script parses the JSON output and creates a human-readable junit-results.md file. +8. **Attach Signed Evidence**: The final step uses the jf evd create command. It takes the consolidated-test-report.json file as the official "predicate" and attaches it as evidence to the specific package version in Artifactory. The evidence is signed using the provided PRIVATE_KEY, ensuring its authenticity and integrity. + +### **Key Commands Used** + +* **Build and Deploy Maven Package:** + +```bash +cd examples/junit/src +mvn clean compile +mvn deploy -DaltDeploymentRepository=artifactory::default::$REGISTRY_DOMAIN/$REPO_NAME +``` + +* **Run JUnit Tests:** + +```bash +cd examples/junit/src +mvn test +``` + +* **Generate Test Reports:** + +```bash +cd examples/junit/src +mvn surefire-report:report-only +mvn site:site +``` + +* **Consolidate Test Results:** + +```bash +cd examples/junit/src +./merge-test-reports.sh +./xml-to-json.sh +``` + +* **Generate Markdown Report:** + +```bash +cd examples/junit/src +python ../junit_json_to_markdown_helper.py target/consolidated-test-report.json +``` + +* **Attach Evidence:** + +```bash +jf evd create \ + --package-name $PACKAGE_NAME \ + --package-version $VERSION \ + --package-repo-name $REPO_NAME \ + --key "${{ secrets.PRIVATE_KEY }}" \ + --key-alias "${{ vars.EVIDENCE_KEY_ALIAS }}" \ + --predicate ./target/consolidated-test-report.json \ + --predicate-type http://junit.org/test-results \ + --markdown "junit-results.md" +``` + +### **Test Results Format** + +The workflow generates a consolidated JSON test report with the following structure: + +```json +{ + "testReport": { + "summary": { + "totalTests": 6, + "totalFailures": 0, + "totalErrors": 0, + "totalSkipped": 0, + "successRate": 100, + "totalTime": "0.123", + "timestamp": "2024-01-15T10:30:00Z" + }, + "testSuites": [ + { + "name": "testAddition", + "class": "com.example.AdditionTest", + "time": "0.045", + "status": "passed" + } + ] + } +} +``` + +### **References** + +* [JUnit 5 Documentation](https://junit.org/junit5/docs/current/user-guide/) +* [Maven Surefire Plugin](https://maven.apache.org/surefire/maven-surefire-plugin/) +* [JFrog Evidence Management](https://jfrog.com/help/r/jfrog-artifactory-documentation/evidence-management) +* [JFrog CLI Documentation](https://jfrog.com/getcli/) \ No newline at end of file diff --git a/examples/junit/junit_json_to_markdown_helper.py b/examples/junit/junit_json_to_markdown_helper.py new file mode 100644 index 0000000..ebac6fb --- /dev/null +++ b/examples/junit/junit_json_to_markdown_helper.py @@ -0,0 +1,198 @@ +import json +import sys +from datetime import datetime + + +def calculate_test_statistics(test_report): + """Calculate test statistics from the test report""" + summary = test_report.get('testReport', {}).get('summary', {}) + + total_tests = summary.get('totalTests', 0) + total_failures = summary.get('totalFailures', 0) + total_errors = summary.get('totalErrors', 0) + total_skipped = summary.get('totalSkipped', 0) + success_rate = summary.get('successRate', 0) + total_time = summary.get('totalTime', 0) + + return { + 'total_tests': total_tests, + 'total_failures': total_failures, + 'total_errors': total_errors, + 'total_skipped': total_skipped, + 'success_rate': success_rate, + 'total_time': total_time + } + + +def get_test_status_color(status): + """Get color indicator for test status""" + if status == 'passed': + return 'PASS' + elif status == 'failed': + return 'FAIL' + elif status == 'skipped': + return 'SKIP' + else: + return 'UNKNOWN' + + +def group_tests_by_class(test_suites): + """Group tests by their class name""" + grouped_tests = {} + + for test in test_suites: + class_name = test.get('class', 'Unknown') + if class_name not in grouped_tests: + grouped_tests[class_name] = [] + grouped_tests[class_name].append(test) + + return grouped_tests + + +def generate_markdown_report(junit_output): + """Generate a comprehensive markdown report from JUnit test results""" + + # Extract test report data + test_report = junit_output.get('testReport', {}) + summary = test_report.get('summary', {}) + test_suites = test_report.get('testSuites', []) + + # Calculate statistics + stats = calculate_test_statistics(junit_output) + + # Get timestamp + timestamp = summary.get('timestamp', datetime.now().isoformat()) + + # Generate the markdown report + markdown_report = f"""# JUnit Test Results Report + +**Generated:** `{timestamp}` + +**Total Tests:** `{stats['total_tests']}` + +**Total Failures:** `{stats['total_failures']}` + +**Total Errors:** `{stats['total_errors']}` + +**Total Skipped:** `{stats['total_skipped']}` + +**Success Rate:** `{stats['success_rate']}%` + +**Total Execution Time:** `{stats['total_time']}s` + +--- + +## Test Summary + +| Metric | Count | +| :----- | :---- | +| Total Tests | {stats['total_tests']} | +| Passed | {stats['total_tests'] - stats['total_failures'] - stats['total_errors']} | +| Failed | {stats['total_failures']} | +| Errors | {stats['total_errors']} | +| Skipped | {stats['total_skipped']} | +| Success Rate | {stats['success_rate']}% | + +--- + +## Test Results by Class + +""" + + # Group tests by class + grouped_tests = group_tests_by_class(test_suites) + + for class_name, tests in grouped_tests.items(): + markdown_report += f"### {class_name}\n\n" + markdown_report += "| Test Name | Status | Execution Time |\n" + markdown_report += "| :-------- | :----- | :------------- |\n" + + for test in tests: + test_name = test.get('name', 'Unknown') + status = test.get('status', 'unknown') + time = test.get('time', '0') + status_icon = get_test_status_color(status) + + markdown_report += f"| {test_name} | {status_icon} {status} | {time}s |\n" + + markdown_report += "\n" + + # Add overall status summary + if stats['success_rate'] == 100: + overall_status = "All tests passed successfully!" + elif stats['success_rate'] >= 80: + overall_status = "Most tests passed with some failures." + else: + overall_status = "Significant test failures detected." + + markdown_report += f""" +--- + +## Overall Status + +{overall_status} + +**Recommendations:** +""" + + if stats['total_failures'] > 0 or stats['total_errors'] > 0: + markdown_report += f""" +- Review and fix {stats['total_failures'] + stats['total_errors']} failing tests +- Investigate test failures in the affected classes +- Consider adding more test coverage for failing scenarios +""" + + if stats['total_skipped'] > 0: + markdown_report += f""" +- Review {stats['total_skipped']} skipped tests to ensure they are intentionally skipped +- Consider enabling skipped tests if conditions are met +""" + + if stats['success_rate'] == 100: + markdown_report += """ +- All tests are passing! Consider adding more test coverage for edge cases +- Review test execution time for optimization opportunities +""" + + markdown_report += "\n---\n" + return markdown_report + + +def main(input_file): + """Main function to process JUnit JSON and generate markdown report""" + try: + # Read JSON input from a file + with open(input_file, 'r') as file: + junit_output = json.load(file) + + # Generate the Markdown report + markdown_report = generate_markdown_report(junit_output) + + # Define the output file path + output_file = 'junit-results.md' + + # Write the Markdown report to a file + with open(output_file, 'w') as file: + file.write(markdown_report) + + print(f"Markdown report generated successfully and saved to {output_file}!") + + except FileNotFoundError: + print(f"Error: Input file '{input_file}' not found.") + sys.exit(1) + except json.JSONDecodeError as e: + print(f"Error: Invalid JSON format in '{input_file}': {e}") + sys.exit(1) + except Exception as e: + print(f"Error: An unexpected error occurred: {e}") + sys.exit(1) + + +if __name__ == '__main__': + if len(sys.argv) != 2: + print("Usage: python junit_json_to_markdown_helper.py ") + print("Example: python junit_json_to_markdown_helper.py target/consolidated-test-report.json") + sys.exit(1) + + input_file = sys.argv[1] + main(input_file) \ No newline at end of file diff --git a/examples/junit/src/main/java/com/example/Addition.java b/examples/junit/src/main/java/com/example/Addition.java new file mode 100644 index 0000000..b090620 --- /dev/null +++ b/examples/junit/src/main/java/com/example/Addition.java @@ -0,0 +1,30 @@ +package com.example; + +/** + * A class dedicated to addition operations. + */ +public class Addition { + + /** + * Adds two integers. + * @param a The first integer. + * @param b The second integer. + * @return The sum of a and b. + */ + public int add(int a, int b) { + return a + b; + } + + /** + * Adds multiple integers. + * @param numbers The integers to add. + * @return The sum of all numbers. + */ + public int addMultiple(int... numbers) { + int sum = 0; + for (int number : numbers) { + sum += number; + } + return sum; + } +} \ No newline at end of file diff --git a/examples/junit/src/main/java/com/example/Calculator.java b/examples/junit/src/main/java/com/example/Calculator.java new file mode 100644 index 0000000..5daa9ad --- /dev/null +++ b/examples/junit/src/main/java/com/example/Calculator.java @@ -0,0 +1,59 @@ +package com.example; + +/** + * A calculator class that uses separate Addition and Subtraction classes. + */ +public class Calculator { + + private final Addition addition; + private final Subtraction subtraction; + + public Calculator() { + this.addition = new Addition(); + this.subtraction = new Subtraction(); + } + + public Calculator(Addition addition, Subtraction subtraction) { + this.addition = addition; + this.subtraction = subtraction; + } + + /** + * Adds two integers using the Addition class. + * @param a The first integer. + * @param b The second integer. + * @return The sum of a and b. + */ + public int add(int a, int b) { + return addition.add(a, b); + } + + /** + * Subtracts two integers using the Subtraction class. + * @param a The first integer (minuend). + * @param b The second integer (subtrahend). + * @return The difference of a and b. + */ + public int subtract(int a, int b) { + return subtraction.subtract(a, b); + } + + /** + * Adds multiple integers using the Addition class. + * @param numbers The integers to add. + * @return The sum of all numbers. + */ + public int addMultiple(int... numbers) { + return addition.addMultiple(numbers); + } + + /** + * Subtracts multiple integers from the first number using the Subtraction class. + * @param first The first number to subtract from. + * @param numbers The integers to subtract. + * @return The result after subtracting all numbers from the first. + */ + public int subtractMultiple(int first, int... numbers) { + return subtraction.subtractMultiple(first, numbers); + } +} \ No newline at end of file diff --git a/examples/junit/src/main/java/com/example/Subtraction.java b/examples/junit/src/main/java/com/example/Subtraction.java new file mode 100644 index 0000000..151a493 --- /dev/null +++ b/examples/junit/src/main/java/com/example/Subtraction.java @@ -0,0 +1,31 @@ +package com.example; + +/** + * A class dedicated to subtraction operations. + */ +public class Subtraction { + + /** + * Subtracts two integers. + * @param a The first integer (minuend). + * @param b The second integer (subtrahend). + * @return The difference of a and b. + */ + public int subtract(int a, int b) { + return a - b; + } + + /** + * Subtracts multiple integers from the first number. + * @param first The first number to subtract from. + * @param numbers The integers to subtract. + * @return The result after subtracting all numbers from the first. + */ + public int subtractMultiple(int first, int... numbers) { + int result = first; + for (int number : numbers) { + result -= number; + } + return result; + } +} \ No newline at end of file diff --git a/examples/junit/src/merge-test-reports.sh b/examples/junit/src/merge-test-reports.sh new file mode 100755 index 0000000..d85f384 --- /dev/null +++ b/examples/junit/src/merge-test-reports.sh @@ -0,0 +1,152 @@ +#!/bin/bash + +# Script to merge all JUnit test reports into a single consolidated report +# Usage: ./merge-test-reports.sh + +set -e + +echo "=== JUnit Test Report Merger ===" +echo "" + +# Check if we're in the right directory +if [ ! -d "target/surefire-reports" ]; then + echo "Error: target/surefire-reports directory not found. Please run 'mvn test' first." + exit 1 +fi + +# Initialize counters +total_tests=0 +total_failures=0 +total_errors=0 +total_skipped=0 +total_time=0 + +echo "Processing test reports..." + +# Process each XML report file +for file in target/surefire-reports/*.xml; do + if [ -f "$file" ]; then + filename=$(basename "$file") + echo " Processing: $filename" + + # Extract test statistics + tests=$(grep -o 'tests="[0-9]*"' "$file" | sed 's/tests="\([0-9]*\)"/\1/') + failures=$(grep -o 'failures="[0-9]*"' "$file" | sed 's/failures="\([0-9]*\)"/\1/') + errors=$(grep -o 'errors="[0-9]*"' "$file" | sed 's/errors="\([0-9]*\)"/\1/') + skipped=$(grep -o 'skipped="[0-9]*"' "$file" | sed 's/skipped="\([0-9]*\)"/\1/') + time=$(grep -o 'time="[0-9.]*"' "$file" | head -1 | sed 's/time="\([0-9.]*\)"/\1/') + + # Handle empty values + tests=${tests:-0} + failures=${failures:-0} + errors=${errors:-0} + skipped=${skipped:-0} + time=${time:-0} + + # Accumulate totals + total_tests=$((total_tests + tests)) + total_failures=$((total_failures + failures)) + total_errors=$((total_errors + errors)) + total_skipped=$((total_skipped + skipped)) + + # Handle time calculation more robustly + if command -v bc >/dev/null 2>&1; then + total_time=$(echo "$total_time + $time" | bc -l) + else + total_time=$(awk "BEGIN {print $total_time + $time}") + fi + + echo " Tests: $tests, Failures: $failures, Errors: $errors, Skipped: $skipped, Time: ${time}s" + fi +done + +echo "" +echo "=== CONSOLIDATED TEST RESULTS ===" +echo "Total Tests: $total_tests" +echo "Total Failures: $total_failures" +echo "Total Errors: $total_errors" +echo "Total Skipped: $total_skipped" +echo "Total Time: ${total_time}s" + +# Calculate success rate +if [ $total_tests -gt 0 ]; then + success_rate=$(( (total_tests - total_failures - total_errors) * 100 / total_tests )) + echo "Success Rate: ${success_rate}%" +else + echo "Success Rate: N/A (no tests found)" +fi + +echo "" +echo "=== TEST CLASSES EXECUTED ===" +for file in target/surefire-reports/*.xml; do + if [ -f "$file" ]; then + classname=$(grep -o 'name="[^"]*Test"' "$file" | head -1 | sed 's/name="\([^"]*\)"/\1/') + if [ -n "$classname" ]; then + echo "- $classname" + fi + fi +done + +echo "" +echo "=== DETAILED TEST RESULTS ===" +for file in target/surefire-reports/*.xml; do + if [ -f "$file" ]; then + filename=$(basename "$file" .xml) + echo "--- $filename ---" + + # Extract and display test case results + grep -o ']*name="[^"]*"[^>]*>' "$file" | while read line; do + testname=$(echo "$line" | grep -o 'name="[^"]*"' | sed 's/name="\([^"]*\)"/\1/') + classname=$(echo "$line" | grep -o 'classname="[^"]*"' | sed 's/classname="\([^"]*\)"/\1/') + time=$(echo "$line" | grep -o 'time="[0-9.]*"' | sed 's/time="\([0-9.]*\)"/\1/') + + if [ -n "$testname" ] && [ -n "$classname" ]; then + time_display=${time:-0} + echo " ✓ $classname.$testname (${time_display}s)" + fi + done + + # Check for failures + if grep -q ' "$consolidated_file" << EOF + + + +EOF + +# Merge all test cases into the consolidated report +for file in target/surefire-reports/*.xml; do + if [ -f "$file" ]; then + # Extract testcase elements and append to consolidated report + grep '> "$consolidated_file" + fi +done + +echo " " >> "$consolidated_file" +echo "" >> "$consolidated_file" + +echo "Consolidated report saved to: $consolidated_file" +echo "" +echo "=== MERGE COMPLETE ===" \ No newline at end of file diff --git a/examples/junit/src/pom.xml b/examples/junit/src/pom.xml new file mode 100644 index 0000000..3e205ed --- /dev/null +++ b/examples/junit/src/pom.xml @@ -0,0 +1,57 @@ + + + 4.0.0 + + com.example + simple-java-maven + 1.0-SNAPSHOT + + + UTF-8 + 17 + 17 + + + + + org.junit.jupiter + junit-jupiter + 5.10.2 + test + + + + + main/java + test/java + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + + org.apache.maven.plugins + maven-surefire-plugin + 3.2.5 + + + **/*Test.java + **/*Tests.java + + xml + true + 1 + false + + + + org.apache.maven.plugins + maven-surefire-report-plugin + 3.2.5 + + + + \ No newline at end of file diff --git a/examples/junit/src/test/java/com/example/AdditionTest.java b/examples/junit/src/test/java/com/example/AdditionTest.java new file mode 100644 index 0000000..5447b91 --- /dev/null +++ b/examples/junit/src/test/java/com/example/AdditionTest.java @@ -0,0 +1,67 @@ +package com.example; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Unit tests for the Addition class. + */ +public class AdditionTest { + + private Addition addition; + + @BeforeEach + void setUp() { + addition = new Addition(); + } + + @Test + void testSimpleAddition() { + // Test a simple addition case + int expected = 2; + int actual = addition.add(1, 1); + assertEquals(expected, actual, "1 + 1 should equal 2"); + } + + @Test + void testAdditionWithNegativeNumbers() { + // Test addition with negative numbers + assertEquals(-2, addition.add(-1, -1), "Adding two negative numbers"); + assertEquals(0, addition.add(-1, 1), "Adding negative and positive numbers"); + } + + @Test + void testAdditionWithZero() { + // Test addition with zero + assertEquals(5, addition.add(5, 0), "Adding zero should return the original number"); + assertEquals(-3, addition.add(0, -3), "Adding zero to negative number"); + } + + @Test + void testLargeNumbers() { + // Test with larger numbers + assertEquals(1000, addition.add(500, 500), "Adding large positive numbers"); + assertEquals(-1000, addition.add(-500, -500), "Adding large negative numbers"); + } + + @Test + void testMultipleAddition() { + // Test adding multiple numbers + assertEquals(10, addition.addMultiple(1, 2, 3, 4), "Adding multiple positive numbers"); + assertEquals(0, addition.addMultiple(5, -3, -2), "Adding mixed positive and negative numbers"); + assertEquals(-6, addition.addMultiple(-1, -2, -3), "Adding multiple negative numbers"); + } + + @Test + void testSingleNumberAddition() { + // Test adding a single number + assertEquals(5, addition.addMultiple(5), "Adding single number should return the number itself"); + } + + @Test + void testEmptyAddition() { + // Test adding no numbers + assertEquals(0, addition.addMultiple(), "Adding no numbers should return 0"); + } +} \ No newline at end of file diff --git a/examples/junit/src/test/java/com/example/CalculatorTest.java b/examples/junit/src/test/java/com/example/CalculatorTest.java new file mode 100644 index 0000000..918bbbe --- /dev/null +++ b/examples/junit/src/test/java/com/example/CalculatorTest.java @@ -0,0 +1,73 @@ +package com.example; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Unit test for the Calculator class that uses Addition and Subtraction classes. + */ +public class CalculatorTest { + + private Calculator calculator; + + @BeforeEach + void setUp() { + calculator = new Calculator(); + } + + @Test + void testAddition() { + // Test a simple addition case + int expected = 2; + int actual = calculator.add(1, 1); + assertEquals(expected, actual, "1 + 1 should equal 2"); + } + + @Test + void testAdditionWithNegativeNumbers() { + // Test addition with negative numbers + assertEquals(-2, calculator.add(-1, -1), "Adding two negative numbers"); + assertEquals(0, calculator.add(-1, 1), "Adding negative and positive numbers"); + } + + @Test + void testSubtraction() { + // Test a simple subtraction case + int expected = 2; + int actual = calculator.subtract(5, 3); + assertEquals(expected, actual, "5 - 3 should equal 2"); + } + + @Test + void testSubtractionWithNegativeNumbers() { + // Test subtraction with negative numbers + assertEquals(4, calculator.subtract(2, -2), "Subtracting negative number"); + assertEquals(-4, calculator.subtract(-2, 2), "Subtracting from negative number"); + } + + @Test + void testMultipleAddition() { + // Test adding multiple numbers + assertEquals(10, calculator.addMultiple(1, 2, 3, 4), "Adding multiple positive numbers"); + assertEquals(0, calculator.addMultiple(5, -3, -2), "Adding mixed positive and negative numbers"); + } + + @Test + void testMultipleSubtraction() { + // Test subtracting multiple numbers + assertEquals(1, calculator.subtractMultiple(10, 2, 3, 4), "Subtracting multiple positive numbers"); + assertEquals(10, calculator.subtractMultiple(5, -3, -2), "Subtracting negative numbers (adding)"); + } + + @Test + void testCalculatorWithDependencyInjection() { + // Test calculator with injected dependencies + Addition mockAddition = new Addition(); + Subtraction mockSubtraction = new Subtraction(); + Calculator calculatorWithDeps = new Calculator(mockAddition, mockSubtraction); + + assertEquals(4, calculatorWithDeps.add(2, 2), "Addition through dependency injection"); + assertEquals(1, calculatorWithDeps.subtract(3, 2), "Subtraction through dependency injection"); + } +} \ No newline at end of file diff --git a/examples/junit/src/test/java/com/example/SubtractionTest.java b/examples/junit/src/test/java/com/example/SubtractionTest.java new file mode 100644 index 0000000..141b1b4 --- /dev/null +++ b/examples/junit/src/test/java/com/example/SubtractionTest.java @@ -0,0 +1,75 @@ +package com.example; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Unit tests for the Subtraction class. + */ +public class SubtractionTest { + + private Subtraction subtraction; + + @BeforeEach + void setUp() { + subtraction = new Subtraction(); + } + + @Test + void testSimpleSubtraction() { + // Test a simple subtraction case + int expected = 2; + int actual = subtraction.subtract(5, 3); + assertEquals(expected, actual, "5 - 3 should equal 2"); + } + + @Test + void testSubtractionWithNegativeNumbers() { + // Test subtraction with negative numbers + assertEquals(4, subtraction.subtract(2, -2), "Subtracting negative number"); + assertEquals(-4, subtraction.subtract(-2, 2), "Subtracting from negative number"); + assertEquals(0, subtraction.subtract(-2, -2), "Subtracting negative from negative"); + } + + @Test + void testSubtractionWithZero() { + // Test subtraction with zero + assertEquals(5, subtraction.subtract(5, 0), "Subtracting zero should return the original number"); + assertEquals(-3, subtraction.subtract(0, 3), "Subtracting from zero"); + } + + @Test + void testLargeNumbers() { + // Test with larger numbers + assertEquals(500, subtraction.subtract(1000, 500), "Subtracting large positive numbers"); + assertEquals(-500, subtraction.subtract(-1000, -500), "Subtracting large negative numbers"); + } + + @Test + void testMultipleSubtraction() { + // Test subtracting multiple numbers + assertEquals(1, subtraction.subtractMultiple(10, 2, 3, 4), "Subtracting multiple positive numbers"); + assertEquals(10, subtraction.subtractMultiple(5, -3, -2), "Subtracting negative numbers (adding)"); + assertEquals(6, subtraction.subtractMultiple(0, -1, -2, -3), "Subtracting negative numbers from zero"); + } + + @Test + void testSingleNumberSubtraction() { + // Test subtracting a single number + assertEquals(5, subtraction.subtractMultiple(5, 0), "Subtracting zero should return the original number"); + } + + @Test + void testNoSubtraction() { + // Test subtracting no numbers + assertEquals(10, subtraction.subtractMultiple(10), "Subtracting no numbers should return the original number"); + } + + @Test + void testSubtractionResultingInZero() { + // Test cases where subtraction results in zero + assertEquals(0, subtraction.subtract(5, 5), "Subtracting same number should equal zero"); + assertEquals(0, subtraction.subtractMultiple(10, 3, 4, 3), "Multiple subtraction resulting in zero"); + } +} \ No newline at end of file diff --git a/examples/junit/src/xml-to-json.sh b/examples/junit/src/xml-to-json.sh new file mode 100755 index 0000000..f689076 --- /dev/null +++ b/examples/junit/src/xml-to-json.sh @@ -0,0 +1,111 @@ +#!/bin/bash + +# Script to convert consolidated XML test report to JSON +# Usage: ./xml-to-json.sh + +set -e + +echo "=== XML to JSON Test Report Converter ===" +echo "" + +# Check if consolidated XML report exists +xml_file="target/consolidated-test-report.xml" +json_file="target/consolidated-test-report.json" + +if [ ! -f "$xml_file" ]; then + echo "Error: Consolidated XML report not found at $xml_file" + echo "Please run './merge-test-reports.sh' first to generate the XML report." + exit 1 +fi + +echo "Converting $xml_file to $json_file..." + +# Create JSON structure +cat > "$json_file" << 'EOF' +{ + "testReport": { + "summary": { +EOF + +# Extract test suite attributes +tests=$(grep -o 'tests="[0-9]*"' "$xml_file" | head -1 | sed 's/tests="\([0-9]*\)"/\1/') +failures=$(grep -o 'failures="[0-9]*"' "$xml_file" | head -1 | sed 's/failures="\([0-9]*\)"/\1/') +errors=$(grep -o 'errors="[0-9]*"' "$xml_file" | head -1 | sed 's/errors="\([0-9]*\)"/\1/') +skipped=$(grep -o 'skipped="[0-9]*"' "$xml_file" | head -1 | sed 's/skipped="\([0-9]*\)"/\1/') +time=$(grep -o 'time="[0-9.]*"' "$xml_file" | head -1 | sed 's/time="\([0-9.]*\)"/\1/') + +# Handle empty values +tests=${tests:-0} +failures=${failures:-0} +errors=${errors:-0} +skipped=${skipped:-0} +time=${time:-0} + +# Calculate success rate +if [ $tests -gt 0 ]; then + success_rate=$(( (tests - failures - errors) * 100 / tests )) +else + success_rate=0 +fi + +# Write summary +cat >> "$json_file" << EOF + "totalTests": $tests, + "totalFailures": $failures, + "totalErrors": $errors, + "totalSkipped": $skipped, + "totalTime": "$time", + "successRate": $success_rate, + "timestamp": "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" + }, + "testSuites": [ +EOF + +# Process each test case +first_test=true +grep '> "$json_file" + fi + + # Write test case + cat >> "$json_file" << EOF + { + "name": "$testname", + "class": "$classname", + "time": "$time", + "status": "passed" + } +EOF +done + +# Close JSON structure +cat >> "$json_file" << 'EOF' + ] + } +} +EOF + +echo "Conversion complete! JSON report saved to: $json_file" +echo "" + +# Display the JSON content +echo "=== CONVERTED JSON REPORT ===" +cat "$json_file" +echo "" +echo "=== JSON REPORT END ===" \ No newline at end of file