diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml index dd26c3568..4d16c87f5 100644 --- a/.github/workflows/sonarqube.yml +++ b/.github/workflows/sonarqube.yml @@ -40,109 +40,91 @@ jobs: # Build the project ./gradlew assembleDebug --stacktrace - # Run tests with coverage - allow test failures - ./gradlew testDebugUnitTest jacocoTestReport --stacktrace - TEST_RESULT=$? - if [ $TEST_RESULT -ne 0 ]; then - echo "Some tests failed, but continuing to check for coverage data..." - # Even if tests fail, JaCoCo should generate a report with partial coverage - # from the tests that did pass - fi - - - name: Prepare class files for SonarQube analysis - run: | - echo "Searching for compiled class files..." + # Run tests - continue even if some tests fail + ./gradlew testDebugUnitTest --stacktrace || echo "Some tests failed, but continuing to generate coverage report..." - # Create the target directory - mkdir -p build/intermediates/runtime_library_classes_dir/debug + # Generate JaCoCo aggregate report separately (ensures it runs even if tests failed) + ./gradlew jacocoRootReport --stacktrace - # Find all directories containing class files with better patterns - CLASS_DIRS=$(find build -name "*.class" -type f -exec dirname {} \; | sort -u | grep -E "(javac|kotlin-classes|runtime_library)" | head -10) - - if [ -z "$CLASS_DIRS" ]; then - echo "WARNING: No class files found in the build directory!" - echo "Searching in all build subdirectories..." - find build -name "*.class" -type f | head -20 + # Log report location for debugging + REPORT_PATH="build/reports/jacoco/jacocoRootReport/jacocoRootReport.xml" + if [ -f "$REPORT_PATH" ]; then + echo "✓ JaCoCo report generated at: $REPORT_PATH ($(wc -c < "$REPORT_PATH") bytes)" else - echo "Found class files in the following directories:" - echo "$CLASS_DIRS" - - # Copy classes from all relevant directories, not just the first one - for CLASS_DIR in $CLASS_DIRS; do - if [ -d "$CLASS_DIR" ] && [ "$(find "$CLASS_DIR" -name "*.class" | wc -l)" -gt 0 ]; then - echo "Copying classes from $CLASS_DIR" - cp -r "$CLASS_DIR"/* build/intermediates/runtime_library_classes_dir/debug/ 2>/dev/null || echo "Failed to copy from $CLASS_DIR" - fi - done - - # Verify the target directory now has class files - CLASS_COUNT=$(find build/intermediates/runtime_library_classes_dir/debug -name "*.class" | wc -l) - echo "Target directory now contains $CLASS_COUNT class files" + echo "✗ JaCoCo report was NOT generated at: $REPORT_PATH" fi - # Update sonar-project.properties with all found class directories - echo "" >> sonar-project.properties - echo "# Additional binary paths found during build" >> sonar-project.properties - if [ -n "$CLASS_DIRS" ]; then - # Convert newlines to commas for sonar.java.binaries - BINARY_PATHS=$(echo "$CLASS_DIRS" | tr '\n' ',' | sed 's/,$//') - echo "sonar.java.binaries=build/intermediates/runtime_library_classes_dir/debug,$BINARY_PATHS" >> sonar-project.properties + - name: Verify class files and coverage data for SonarQube analysis + run: | + echo "=== Verifying Build Artifacts for SonarQube ===" + echo "" + + echo "Checking compiled class files for each module:" + for module in main events logger; do + MODULE_CLASSES_DIR="${module}/build/intermediates/javac/debug/compileDebugJavaWithJavac/classes" + if [ -d "$MODULE_CLASSES_DIR" ]; then + CLASS_COUNT=$(find "$MODULE_CLASSES_DIR" -name "*.class" | wc -l) + echo " ✓ ${module}: Found $CLASS_COUNT class files in $MODULE_CLASSES_DIR" + else + echo " ✗ ${module}: Class directory not found: $MODULE_CLASSES_DIR" + fi + done + + echo "" + echo "Checking JaCoCo coverage report:" + if [ -f build/reports/jacoco/jacocoRootReport/jacocoRootReport.xml ]; then + REPORT_SIZE=$(wc -c < build/reports/jacoco/jacocoRootReport/jacocoRootReport.xml) + PACKAGE_COUNT=$(grep -c "> sonar-project.properties + echo " ✗ JaCoCo report file not found" fi - - echo "Checking for JaCoCo report files..." - find build -name "*.xml" | grep jacoco || echo "No JaCoCo XML files found" - find build -name "*.exec" | grep jacoco || echo "No JaCoCo exec files found" - - echo "Contents of JaCoCo report directory:" - ls -la build/reports/jacoco/jacocoTestReport/ || echo "Directory not found" - + echo "" - echo "Checking test execution results:" + echo "Checking JaCoCo execution data for each module:" + for module in main events logger; do + EXEC_FILE="${module}/build/jacoco/testDebugUnitTest.exec" + if [ -f "$EXEC_FILE" ]; then + EXEC_SIZE=$(wc -c < "$EXEC_FILE") + echo " ✓ ${module}: Found execution data ($EXEC_SIZE bytes) in $EXEC_FILE" + else + echo " ✗ ${module}: Execution data not found: $EXEC_FILE" + fi + done + + echo "" + echo "Test execution summary:" TEST_RESULT_FILES=$(find build -name "TEST-*.xml" 2>/dev/null) if [ -n "$TEST_RESULT_FILES" ]; then - echo "Found test result files:" - echo "$TEST_RESULT_FILES" - # Count total tests, failures, errors TOTAL_TESTS=$(cat $TEST_RESULT_FILES | grep -o 'tests="[0-9]*"' | cut -d'"' -f2 | awk '{sum+=$1} END {print sum}') TOTAL_FAILURES=$(cat $TEST_RESULT_FILES | grep -o 'failures="[0-9]*"' | cut -d'"' -f2 | awk '{sum+=$1} END {print sum}') TOTAL_ERRORS=$(cat $TEST_RESULT_FILES | grep -o 'errors="[0-9]*"' | cut -d'"' -f2 | awk '{sum+=$1} END {print sum}') - echo "Test summary: $TOTAL_TESTS tests, $TOTAL_FAILURES failures, $TOTAL_ERRORS errors" - else - echo "No test result files found" - fi - - echo "" - echo "Checking JaCoCo report content:" - if [ -f build/reports/jacoco/jacocoTestReport/jacocoTestReport.xml ]; then - echo "Report file size: $(wc -c < build/reports/jacoco/jacocoTestReport/jacocoTestReport.xml) bytes" - echo "First 500 chars of report:" - head -c 500 build/reports/jacoco/jacocoTestReport/jacocoTestReport.xml - echo "" - echo "" - echo "Counting coverage elements:" - grep -c " + try { + def proc = new ProcessBuilder(path, '--version').redirectErrorStream(true).start() + return proc.waitFor() == 0 + } catch (Exception e) { return false } + } + + if (!scannerPath) { + throw new GradleException('sonar-scanner not found. Install with: brew install sonar-scanner') + } + + println "Running sonar-scanner..." + def cmd = [scannerPath, "-Dsonar.token=${sonarToken}", "-Dsonar.host.url=${sonarHost}"] + if (sonarOrg) cmd.add("-Dsonar.organization=${sonarOrg}") + cmd.add("-Dsonar.projectVersion=${splitVersion}") + + def proc = new ProcessBuilder(cmd).directory(rootDir).inheritIO().start() + if (proc.waitFor() != 0) { + throw new GradleException("sonar-scanner failed") + } + } +} androidFusedLibrary { namespace = 'io.split.android.android_client' @@ -106,6 +140,7 @@ repositories { dependencies { include project(':main') include project(':logger') + include project(':events') } def splitPOM = { diff --git a/events/.gitignore b/events/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/events/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/events/build.gradle b/events/build.gradle new file mode 100644 index 000000000..89ae1775c --- /dev/null +++ b/events/build.gradle @@ -0,0 +1,21 @@ +plugins { + id 'com.android.library' +} + +apply from: "$rootDir/gradle/common-android-library.gradle" + +android { + namespace 'io.harness.events' + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } +} + +dependencies { + implementation libs.annotation + + testImplementation libs.junit4 + testImplementation libs.mockitoCore +} diff --git a/events/consumer-rules.pro b/events/consumer-rules.pro new file mode 100644 index 000000000..e69de29bb diff --git a/events/proguard-rules.pro b/events/proguard-rules.pro new file mode 100644 index 000000000..481bb4348 --- /dev/null +++ b/events/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/events/src/main/AndroidManifest.xml b/events/src/main/AndroidManifest.xml new file mode 100644 index 000000000..8bdb7e14b --- /dev/null +++ b/events/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + diff --git a/events/src/main/java/io/harness/events/EventDelivery.java b/events/src/main/java/io/harness/events/EventDelivery.java new file mode 100644 index 000000000..1ad9e6565 --- /dev/null +++ b/events/src/main/java/io/harness/events/EventDelivery.java @@ -0,0 +1,12 @@ +package io.harness.events; + +/** + * Interface for event delivery. + * + * @param event type + * @param metadata type + */ +public interface EventDelivery { + + void deliver(EventHandler eventHandler, E event, M metadata); +} diff --git a/events/src/main/java/io/harness/events/EventHandler.java b/events/src/main/java/io/harness/events/EventHandler.java new file mode 100644 index 000000000..d12c73f24 --- /dev/null +++ b/events/src/main/java/io/harness/events/EventHandler.java @@ -0,0 +1,13 @@ +package io.harness.events; + +/** + * Interface for event handlers. This represents a callback + * that will be executed when an event is triggered. + * + * @param event type + * @param metadata type + */ +public interface EventHandler { + + void handle(E event, M metadata); +} diff --git a/events/src/main/java/io/harness/events/EventsManager.java b/events/src/main/java/io/harness/events/EventsManager.java new file mode 100644 index 000000000..70f915ff5 --- /dev/null +++ b/events/src/main/java/io/harness/events/EventsManager.java @@ -0,0 +1,50 @@ +package io.harness.events; + +import androidx.annotation.Nullable; + +/** + * Interface for events manager. + * + * @param external events type + * @param internal events type + * @param metadata type + */ +public interface EventsManager { + + /** + * Registers a handler to be executed when the event is triggered. + * + * @param event event to register + * @param handler handler to execute when the event is triggered + */ + void register(E event, EventHandler handler); + + /** + * Unregisters all registered handlers for an event. + * + * @param event event to unregister handlers for + */ + void unregister(E event); + + /** + * Notifies an internal event has occurred. + * + * @param event internal event to notify + * @param metadata optional metadata + */ + void notifyInternalEvent(I event, @Nullable M metadata); + + /** + * Checks if the event has already been triggered. + * + * @param event event to check + * @return whether event has been triggered + */ + boolean eventAlreadyTriggered(E event); + + /** + * Destroys the events manager. + * This should be called when the events manager is no longer needed. + */ + void destroy(); +} diff --git a/events/src/main/java/io/harness/events/EventsManagerConfig.java b/events/src/main/java/io/harness/events/EventsManagerConfig.java new file mode 100644 index 000000000..5b9fe38af --- /dev/null +++ b/events/src/main/java/io/harness/events/EventsManagerConfig.java @@ -0,0 +1,91 @@ +package io.harness.events; + +import androidx.annotation.NonNull; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +/** + * Contains the interdependencies between events and internal events. + * + * @param external events type + * @param internal events type + */ +public final class EventsManagerConfig { + // External events that require ALL listed internals (AND) + private final Map> mRequireAll; + // External events triggered by ANY of the listed internals (OR) + private final Map> mRequireAny; + // External-event guards: prerequisites that must have fired before External can emit + private final Map> mPrerequisites; + // External-event guards: if any of these have fired, suppress E + private final Map> mSuppressedBy; + // Execution policy: max executions per external event (-1 = unlimited) + private final Map mExecutionLimits; + + /** + * Creates a new EventsManagerConfig. + * + * @param requireAll External events that require ALL listed internals (AND) + * @param requireAny External events triggered by ANY of the listed internals (OR) + * @param prerequisites External-event guards: prerequisites that must have fired before External can emit + * @param suppressedBy External-event guards: if any of these have fired, suppress E + * @param executionLimits Execution policy: max executions per external event (-1 = unlimited) + */ + public EventsManagerConfig(Map> requireAll, + Map> requireAny, + Map> prerequisites, + Map> suppressedBy, + Map executionLimits) { + mRequireAll = requireAll == null + ? Collections.emptyMap() + : Collections.unmodifiableMap(new HashMap<>(requireAll)); + mRequireAny = requireAny == null + ? Collections.emptyMap() + : Collections.unmodifiableMap(new HashMap<>(requireAny)); + mPrerequisites = prerequisites == null + ? Collections.emptyMap() + : Collections.unmodifiableMap(new HashMap<>(prerequisites)); + mSuppressedBy = suppressedBy == null + ? Collections.emptyMap() + : Collections.unmodifiableMap(new HashMap<>(suppressedBy)); + mExecutionLimits = executionLimits == null + ? Collections.emptyMap() + : Collections.unmodifiableMap(new HashMap<>(executionLimits)); + } + + public static EventsManagerConfig empty() { + return new EventsManagerConfig<>(Collections.emptyMap(), + Collections.emptyMap(), + Collections.emptyMap(), + Collections.emptyMap(), + Collections.emptyMap()); + } + + @NonNull + public Map> getRequireAll() { + return mRequireAll; + } + + @NonNull + public Map> getRequireAny() { + return mRequireAny; + } + + @NonNull + public Map> getPrerequisites() { + return mPrerequisites; + } + + @NonNull + public Map> getSuppressedBy() { + return mSuppressedBy; + } + + @NonNull + public Map getExecutionLimits() { + return mExecutionLimits; + } +} diff --git a/events/src/test/java/io/harness/events/EventsManagerConfigTest.java b/events/src/test/java/io/harness/events/EventsManagerConfigTest.java new file mode 100644 index 000000000..84db4b209 --- /dev/null +++ b/events/src/test/java/io/harness/events/EventsManagerConfigTest.java @@ -0,0 +1,155 @@ +package io.harness.events; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.junit.Assert; +import org.junit.Test; + +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +public class EventsManagerConfigTest { + + @Test + public void nullInputMapsCreateEmptyMaps() { + EventsManagerConfig config = + new EventsManagerConfig<>(null, null, null, null, null); + + assertTrue(config.getRequireAll().isEmpty()); + assertTrue(config.getRequireAny().isEmpty()); + assertTrue(config.getPrerequisites().isEmpty()); + assertTrue(config.getSuppressedBy().isEmpty()); + assertTrue(config.getExecutionLimits().isEmpty()); + } + + @Test + public void mutationsToInputMapsDoNotModifyConfig() { + Map> requireAll = new HashMap<>(); + Map> requireAny = new HashMap<>(); + Map> prerequisites = new HashMap<>(); + Map> suppressedBy = new HashMap<>(); + Map executionLimits = new HashMap<>(); + + Set internals = new HashSet<>(); + internals.add("I1"); + + requireAll.put("E1", internals); + requireAny.put("E1", internals); + prerequisites.put("E1", Collections.singleton("E0")); + suppressedBy.put("E1", Collections.singleton("E2")); + executionLimits.put("E1", 3); + + EventsManagerConfig config = + new EventsManagerConfig<>(requireAll, requireAny, prerequisites, suppressedBy, executionLimits); + + // Mutate the original maps after construction + requireAll.put("E2", Collections.singleton("I2")); + requireAny.clear(); + prerequisites.put("E2", Collections.singleton("E3")); + suppressedBy.remove("E1"); + executionLimits.put("E2", 5); + + Map> requireAllFromConfig = config.getRequireAll(); + Map> requireAnyFromConfig = config.getRequireAny(); + Map> prerequisitesFromConfig = config.getPrerequisites(); + Map> suppressedByFromConfig = config.getSuppressedBy(); + Map executionLimitsFromConfig = config.getExecutionLimits(); + + assertEquals(1, requireAllFromConfig.size()); + assertTrue(requireAllFromConfig.containsKey("E1")); + assertFalse(requireAllFromConfig.containsKey("E2")); + + assertEquals(1, requireAnyFromConfig.size()); + assertTrue(requireAnyFromConfig.containsKey("E1")); + + assertEquals(1, prerequisitesFromConfig.size()); + assertTrue(prerequisitesFromConfig.containsKey("E1")); + assertFalse(prerequisitesFromConfig.containsKey("E2")); + + assertEquals(1, suppressedByFromConfig.size()); + assertTrue(suppressedByFromConfig.containsKey("E1")); + + assertEquals(1, executionLimitsFromConfig.size()); + assertTrue(executionLimitsFromConfig.containsKey("E1")); + assertFalse(executionLimitsFromConfig.containsKey("E2")); + } + + @Test + public void returnedMapsAreUnmodifiable() { + Map> requireAll = new HashMap<>(); + requireAll.put("E1", Collections.singleton("I1")); + + Map> requireAny = new HashMap<>(); + requireAny.put("E1", Collections.singleton("I1")); + + Map> prerequisites = new HashMap<>(); + prerequisites.put("E1", Collections.singleton("E0")); + + Map> suppressedBy = new HashMap<>(); + suppressedBy.put("E1", Collections.singleton("E2")); + + Map executionLimits = new HashMap<>(); + executionLimits.put("E1", 3); + + EventsManagerConfig config = + new EventsManagerConfig<>(requireAll, requireAny, prerequisites, suppressedBy, executionLimits); + + try { + config.getRequireAll().put("E2", Collections.singleton("I2")); + Assert.fail("getRequireAll() should return an unmodifiable map"); + } catch (UnsupportedOperationException expected) { + // expected + } + + try { + config.getRequireAny().put("E2", Collections.singleton("I2")); + Assert.fail("getRequireAny() should return an unmodifiable map"); + } catch (UnsupportedOperationException expected) { + // expected + } + + try { + config.getPrerequisites().put("E2", Collections.singleton("E3")); + Assert.fail("getPrerequisites() should return an unmodifiable map"); + } catch (UnsupportedOperationException expected) { + // expected + } + + try { + config.getSuppressedBy().put("E2", Collections.singleton("E3")); + Assert.fail("getSuppressedBy() should return an unmodifiable map"); + } catch (UnsupportedOperationException expected) { + // expected + } + + try { + config.getExecutionLimits().put("E2", 5); + Assert.fail("getExecutionLimits() should return an unmodifiable map"); + } catch (UnsupportedOperationException expected) { + // expected + } + } + + @Test + public void emptyMethodReturnsEmptyUnmodifiableConfig() { + EventsManagerConfig config = EventsManagerConfig.empty(); + + assertTrue(config.getRequireAll().isEmpty()); + assertTrue(config.getRequireAny().isEmpty()); + assertTrue(config.getPrerequisites().isEmpty()); + assertTrue(config.getSuppressedBy().isEmpty()); + assertTrue(config.getExecutionLimits().isEmpty()); + + try { + config.getRequireAll().put("E1", Collections.singleton("I1")); + Assert.fail("getRequireAll() from empty() should be unmodifiable"); + } catch (UnsupportedOperationException expected) { + // expected + } + } +} diff --git a/gradle/common-android-library.gradle b/gradle/common-android-library.gradle index de5ee9713..93f2c7962 100644 --- a/gradle/common-android-library.gradle +++ b/gradle/common-android-library.gradle @@ -24,3 +24,6 @@ if (kotlinCompileClass != null) { } } +// Enable Jacoco coverage configuration for all Android library modules +apply from: "$rootDir/gradle/jacoco-android.gradle" + diff --git a/gradle/jacoco-android.gradle b/gradle/jacoco-android.gradle index e594c5d18..68103d1a8 100644 --- a/gradle/jacoco-android.gradle +++ b/gradle/jacoco-android.gradle @@ -25,19 +25,20 @@ tasks.register('jacocoTestReport', JacocoReport) { def fileFilter = ['**/R.class', '**/R$*.class', '**/BuildConfig.*', '**/Manifest*.*', '**/*Test*.*', 'android/**/*.*'] def classDirectoriesFiles = [] + + // Try multiple possible class directory locations for different AGP versions + // Android Gradle Plugin may compile classes to different locations + // NOTE: If new modules use different compilation output directories, add them here def possibleClassDirs = [ - "${buildDir}/intermediates/javac/debug/classes", - "${buildDir}/intermediates/javac/debug/compileDebugJavaWithJavac/classes", - "${buildDir}/intermediates/runtime_library_classes_dir/debug", - "${buildDir}/intermediates/classes/debug", - "${buildDir}/classes/java/main", - "${buildDir}/tmp/kotlin-classes/debug" + "${buildDir}/intermediates/javac/debug/compileDebugJavaWithJavac/classes", // AGP 9.0+ location (primary) + "${buildDir}/classes/java/main" // Standard fallback location ] + possibleClassDirs.each { dirPath -> def dir = file(dirPath) if (dir.exists() && dir.isDirectory()) { def classDir = fileTree(dir: dirPath, excludes: fileFilter) - if (classDir.files.size() > 0) { + if (!classDir.isEmpty()) { classDirectoriesFiles.add(classDir) } } diff --git a/gradle/jacoco-root.gradle b/gradle/jacoco-root.gradle new file mode 100644 index 000000000..c4913b1e6 --- /dev/null +++ b/gradle/jacoco-root.gradle @@ -0,0 +1,100 @@ +import org.gradle.testing.jacoco.tasks.JacocoReport +import org.gradle.api.tasks.testing.Test + +// Apply Jacoco at the root to support aggregate reporting +apply plugin: 'jacoco' + +// Define exclusions for JaCoCo coverage +def coverageExclusions = [ + '**/R.class', + '**/R$*.class', + '**/BuildConfig.*', + '**/Manifest*.*', + '**/*Test*.*', + 'android/**/*.*' +] + +// Aggregate Jacoco coverage report for all Android library modules +tasks.register('jacocoRootReport', JacocoReport) { + group = 'verification' + description = 'Generates an aggregate JaCoCo coverage report for all modules' + + def fileFilter = coverageExclusions + + // Collect class directories from all subprojects + def classDirs = subprojects.collectMany { proj -> + def b = proj.buildDir + + // Try multiple possible class directory locations for different AGP versions + // NOTE: If new modules use different compilation output directories, add them here + def possibleClassDirs = [ + new File(b, "intermediates/javac/debug/compileDebugJavaWithJavac/classes"), // AGP 9.0+ location (primary) + new File(b, "classes/java/main") // Standard fallback location + ] + + possibleClassDirs.findAll { it.exists() && it.isDirectory() }.collect { dir -> + proj.fileTree(dir: dir, excludes: fileFilter) + } + } + classDirectories.from = files(classDirs) + + // Collect source directories from all subprojects + def srcDirs = subprojects.collectMany { proj -> + [proj.file("src/main/java"), proj.file("src/main/kotlin")] + }.findAll { it.exists() } + sourceDirectories.from = files(srcDirs) + + // Collect execution data from all subprojects + def execFiles = subprojects.collectMany { proj -> + def b = proj.buildDir + [ + new File(b, "jacoco/testDebugUnitTest.exec"), + new File(b, "outputs/unit_test_code_coverage/debugUnitTest/testDebugUnitTest.exec") + ].findAll { it.exists() } + } + executionData.from = files(execFiles) + + doFirst { + logger.lifecycle("=== JaCoCo Root Report Generation ===") + logger.lifecycle("Execution data files:") + def execDataFiles = executionData.files + if (execDataFiles.isEmpty() || !execDataFiles.any { it.exists() }) { + logger.warn(" - No execution data files found - coverage report will be empty") + } else { + execDataFiles.each { file -> + if (file.exists()) { + logger.lifecycle(" - Found: $file (${file.length()} bytes)") + } else { + logger.lifecycle(" - Missing: $file") + } + } + } + logger.lifecycle("=======================================") + } + + reports { + xml.required = true + html.required = true + csv.required = false + + xml.outputLocation = file("$buildDir/reports/jacoco/jacocoRootReport/jacocoRootReport.xml") + html.outputLocation = file("$buildDir/reports/jacoco/jacocoRootReport/html") + } + + // Always regenerate the report + outputs.upToDateWhen { false } +} + +// Wire all module unit tests into the aggregate Jacoco report +subprojects { proj -> + // Only consider projects that apply the Jacoco plugin + plugins.withId('jacoco') { + tasks.withType(Test).matching { it.name == 'testDebugUnitTest' }.configureEach { testTask -> + rootProject.tasks.named('jacocoRootReport') { + dependsOn(testTask) + } + } + } +} + + diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 98011c573..c35a03090 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -46,4 +46,3 @@ androidxTestOrchestrator = { module = "androidx.test:orchestrator", version.ref kotlinStdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" } kotlinTest = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } kotlinTestJunit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" } - diff --git a/main/build.gradle b/main/build.gradle index 3654b45fe..241ba786e 100644 --- a/main/build.gradle +++ b/main/build.gradle @@ -3,7 +3,6 @@ plugins { } apply from: "$rootDir/gradle/common-android-library.gradle" -apply from: "$rootDir/gradle/jacoco-android.gradle" android { namespace 'io.split.android.client.main' @@ -52,6 +51,7 @@ android { dependencies { // Internal module dependencies api project(':logger') + implementation project(':events') // External dependencies implementation libs.roomRuntime diff --git a/settings.gradle b/settings.gradle index 0f38584f2..246c9b203 100644 --- a/settings.gradle +++ b/settings.gradle @@ -2,3 +2,4 @@ rootProject.name = 'android-client' include ':logger' include ':main' +include ':events' diff --git a/sonar-project.properties b/sonar-project.properties index 930a7d2dc..f598dd559 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -2,20 +2,41 @@ sonar.projectKey=splitio_android-client sonar.projectName=android-client -# Path to source directories -sonar.sources=src/main/java +# Path to source directories (multi-module) +# Root project contains modules: main, events, logger +sonar.sources=main/src/main/java,events/src/main/java,logger/src/main/java -# Path to compiled classes -sonar.java.binaries=build/intermediates/runtime_library_classes_dir/debug +# Path to compiled classes (multi-module) +# Include binary paths for all modules: main, events, logger +sonar.java.binaries=\ + main/build/intermediates/javac/debug/compileDebugJavaWithJavac/classes,\ + events/build/intermediates/javac/debug/compileDebugJavaWithJavac/classes,\ + logger/build/intermediates/javac/debug/compileDebugJavaWithJavac/classes -# Path to test directories -sonar.tests=src/test/java,src/androidTest/java,src/sharedTest/java +# Path to dependency/libraries jars (multi-module) +sonar.java.libraries=\ + main/build/intermediates/compile_library_classes_jar/debug/bundleLibCompileToJarDebug/classes.jar,\ + main/build/intermediates/runtime_library_classes_jar/debug/bundleLibRuntimeToJarDebug/classes.jar,\ + main/build/intermediates/compile_r_class_jar/debug/generateDebugRFile/R.jar,\ + main/build/intermediates/compile_and_runtime_r_class_jar/debugUnitTest/generateDebugUnitTestStubRFile/R.jar,\ + events/build/intermediates/compile_library_classes_jar/debug/bundleLibCompileToJarDebug/classes.jar,\ + events/build/intermediates/runtime_library_classes_jar/debug/bundleLibRuntimeToJarDebug/classes.jar,\ + events/build/intermediates/compile_r_class_jar/debug/generateDebugRFile/R.jar,\ + events/build/intermediates/compile_and_runtime_r_class_jar/debugUnitTest/generateDebugUnitTestStubRFile/R.jar,\ + logger/build/intermediates/compile_library_classes_jar/debug/bundleLibCompileToJarDebug/classes.jar,\ + logger/build/intermediates/runtime_library_classes_jar/debug/bundleLibRuntimeToJarDebug/classes.jar,\ + logger/build/intermediates/compile_r_class_jar/debug/generateDebugRFile/R.jar,\ + logger/build/intermediates/compile_and_runtime_r_class_jar/debugUnitTest/generateDebugUnitTestStubRFile/R.jar + +# Path to test directories (multi-module) +# Only include test source folders that are guaranteed to exist in all environments +sonar.tests=main/src/test/java,main/src/androidTest/java,main/src/sharedTest/java,events/src/test/java,logger/src/test/java # Encoding of the source code sonar.sourceEncoding=UTF-8 -# Include test coverage reports - prioritize combined report -sonar.coverage.jacoco.xmlReportPaths=build/reports/jacoco/jacocoTestReport/jacocoTestReport.xml +# Include aggregate test coverage report from all modules +sonar.coverage.jacoco.xmlReportPaths=build/reports/jacoco/jacocoRootReport/jacocoRootReport.xml # Exclusions sonar.exclusions=**/R.class,**/R$*.class,**/BuildConfig.*,**/Manifest*.*,**/*Test*.*,android/**/*.*