From 4740464620e4579f6a816ef2ebcfe16cd42be6ee Mon Sep 17 00:00:00 2001 From: gthea Date: Tue, 24 Jun 2025 15:21:11 -0300 Subject: [PATCH 1/4] SonarQube config (#778) --- .github/workflows/sonarqube.yml | 171 ++++++++++++++++++++++++++++++++ build.gradle | 25 +++++ jacoco.gradle | 115 +++++++++++++++++++++ sonar-project.properties | 25 +++++ 4 files changed, 336 insertions(+) create mode 100644 .github/workflows/sonarqube.yml create mode 100644 jacoco.gradle create mode 100644 sonar-project.properties diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml new file mode 100644 index 000000000..88a42ab4d --- /dev/null +++ b/.github/workflows/sonarqube.yml @@ -0,0 +1,171 @@ +name: SonarCloud Analysis + +on: + pull_request: + branches: + - '*' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + sonarcloud: + name: SonarCloud Scan + runs-on: ubuntu-latest + steps: + - name: Enable KVM group perms + run: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm + + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 # Shallow clones should be disabled for better relevancy of analysis + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: 17 + + - name: Gradle cache + uses: gradle/actions/setup-gradle@v3 + + - name: Configure Gradle memory settings + run: | + echo "org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=1g -XX:+HeapDumpOnOutOfMemoryError" >> gradle.properties + echo "android.enableJetifier=false" >> gradle.properties + echo "android.enableR8.fullMode=false" >> gradle.properties + cat gradle.properties + + - name: Build project and run tests with coverage + run: | + # 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 + + # Check if the report was generated with content + if [ -f build/reports/jacoco/jacocoTestReport/jacocoTestReport.xml ]; then + # Use stat command compatible with both Linux and macOS + if [[ "$(uname)" == "Darwin" ]]; then + # macOS syntax + REPORT_SIZE=$(stat -f%z build/reports/jacoco/jacocoTestReport/jacocoTestReport.xml) + else + # Linux syntax + REPORT_SIZE=$(stat -c%s build/reports/jacoco/jacocoTestReport/jacocoTestReport.xml) + fi + + if [ "$REPORT_SIZE" -lt 1000 ]; then + echo "JaCoCo report is too small ($REPORT_SIZE bytes), likely empty. Trying to generate from test execution data..." + # Try to generate the report directly from the exec file + if [ -f build/jacoco/testDebugUnitTest.exec ]; then + java -jar ~/.gradle/caches/modules-2/files-2.1/org.jacoco/org.jacoco.cli/0.8.8/*/org.jacoco.cli-0.8.8.jar report build/jacoco/testDebugUnitTest.exec \ + --classfiles build/intermediates/runtime_library_classes_dir/debug \ + --sourcefiles src/main/java \ + --xml build/reports/jacoco/jacocoTestReport/jacocoTestReport.xml + + # Check if the report was successfully generated + if [[ "$(uname)" == "Darwin" ]]; then + # macOS syntax + NEW_REPORT_SIZE=$(stat -f%z build/reports/jacoco/jacocoTestReport/jacocoTestReport.xml) + else + # Linux syntax + NEW_REPORT_SIZE=$(stat -c%s build/reports/jacoco/jacocoTestReport/jacocoTestReport.xml) + fi + + if [ "$NEW_REPORT_SIZE" -lt 1000 ]; then + echo "ERROR: Failed to generate a valid JaCoCo report with coverage data" + exit 1 + else + echo "JaCoCo report successfully generated with size $NEW_REPORT_SIZE bytes" + fi + else + echo "ERROR: No JaCoCo execution data file found. Tests may not have run correctly." + exit 1 + fi + else + echo "JaCoCo report generated successfully with size $REPORT_SIZE bytes" + fi + else + echo "ERROR: JaCoCo report file not found. Coverage data is missing." + exit 1 + fi + + - name: Prepare class files for SonarQube analysis + run: | + echo "Searching for compiled class files..." + + # Create the target directory + mkdir -p build/intermediates/runtime_library_classes_dir/debug + + # Find all directories containing class files + CLASS_DIRS=$(find build -name "*.class" -type f -exec dirname {} \; | sort -u) + + if [ -z "$CLASS_DIRS" ]; then + echo "WARNING: No class files found in the build directory!" + else + echo "Found class files in the following directories:" + echo "$CLASS_DIRS" + + # Copy classes from the first directory with classes + FIRST_CLASS_DIR=$(echo "$CLASS_DIRS" | head -1) + echo "Copying classes from $FIRST_CLASS_DIR to build/intermediates/runtime_library_classes_dir/debug" + cp -r $FIRST_CLASS_DIR/* build/intermediates/runtime_library_classes_dir/debug/ || echo "Failed to copy from $FIRST_CLASS_DIR" + + # 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" + fi + + # Update sonar-project.properties with all found class directories as a fallback + echo "\n# Additional binary paths found during build" >> sonar-project.properties + echo "sonar.java.binaries=build/intermediates/runtime_library_classes_dir/debug,$CLASS_DIRS" >> sonar-project.properties + + echo "Checking for JaCoCo report files..." + find build -name "*.xml" | grep jacoco || echo "No XML files found" + find build -name "*.exec" | grep jacoco || echo "No exec files found" + echo "Contents of JaCoCo report directory:" + ls -la build/reports/jacoco/jacocoTestReport/ || echo "Directory not found" + + echo "\nChecking test execution results:" + find build -name "TEST-*.xml" | xargs cat | grep -E 'tests|failures|errors|skipped' || echo "No test result files found" + + echo "\nChecking 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 "\n\nCounting coverage elements:" + grep -c " + if (file.exists()) { + logger.lifecycle("Found JaCoCo execution data file: $file") + } + } + } + } +} + +// Task to ensure JaCoCo XML report is created in the exact location SonarQube expects +task generateJacocoXmlReport { + doLast { + // Create the directory if it doesn't exist + def reportDir = file("${buildDir}/reports/jacoco/jacocoTestReport") + reportDir.mkdirs() + + // Check if we have a JaCoCo exec file + def execFiles = fileTree(dir: "${buildDir}", includes: ['**/*.exec']) + if (execFiles.isEmpty()) { + // Create an empty report file if no exec files found + def reportFile = new File(reportDir, "jacocoTestReport.xml") + reportFile.text = "\n\n" + println "Created empty JaCoCo report at ${reportFile.absolutePath}" + } else { + println "Found JaCoCo exec files: ${execFiles.files}" + // If we have exec files but no report, we could use JaCoCo's ant task to generate one + // This is a simplified version - in a real scenario you'd use the JaCoCo ant task + } + + // Check if the report exists and log its content + def reportFile = new File(reportDir, "jacocoTestReport.xml") + if (reportFile.exists()) { + println "\n==== JaCoCo Report Content ====" + println "Report file size: ${reportFile.length()} bytes" + + if (reportFile.length() > 0) { + def xmlContent = reportFile.text + println "First 500 chars of report: ${xmlContent.take(500)}..." + + // Count packages, classes, and methods + def packageCount = (xmlContent =~ / Date: Tue, 1 Jul 2025 11:53:53 -0300 Subject: [PATCH 2/4] Sonatype Central flow (#779) --- build.gradle | 94 ++++-------------------- deps.txt | 2 +- gradle.properties | 17 ++++- gradle/wrapper/gradle-wrapper.properties | 2 +- 4 files changed, 33 insertions(+), 82 deletions(-) diff --git a/build.gradle b/build.gradle index 3a305de58..bad02ed05 100644 --- a/build.gradle +++ b/build.gradle @@ -5,19 +5,19 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:7.4.2' + classpath 'com.android.tools.build:gradle:8.7.3' classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.0' + classpath "com.vanniktech:gradle-maven-publish-plugin:0.33.0" } } apply plugin: 'com.android.library' -apply plugin: 'maven-publish' apply plugin: 'signing' apply plugin: 'kotlin-android' +apply plugin: 'com.vanniktech.maven.publish' apply from: 'spec.gradle' apply from: 'jacoco.gradle' - ext { splitVersion = '5.3.0' jacocoVersion = '0.8.8' @@ -119,6 +119,12 @@ repositories { mavenCentral() } +tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach { + kotlinOptions { + jvmTarget = "1.8" + } +} + dependencies { def roomVersion = '2.4.3' @@ -189,7 +195,6 @@ dependencies { def splitPOM = { name = 'Split Android SDK' - packaging = 'aar' description = 'Official Split Android SDK' url = 'https://github.com/splitio/android-client' @@ -202,15 +207,8 @@ def splitPOM = { developers { developer { - id = 'sarrubia' - name = 'Sebastian Arrubia' - email = 'sebastian@split.io' - } - - developer { - id = 'fernando' - name = 'Fernando Martin' - email = 'fernando@split.io' + id = 'sdks' + email = 'sdks@split.io' } } @@ -221,17 +219,6 @@ def splitPOM = { } } -def releaseRepo = { - name = "ReleaseRepo" - def releasesRepoUrl = "https://oss.sonatype.org/service/local/staging/deploy/maven2/" - def snapshotsRepoUrl = "https://oss.sonatype.org/content/repositories/snapshots/" - url = version.endsWith('SNAPSHOT') ? snapshotsRepoUrl : releasesRepoUrl - credentials { - username = ossrhUsername - password = ossrhPassword - } -} - def devRepo = { name = "DevelopmentRepo" url = 'https://splitio.jfrog.io/artifactory/maven-all-virtual' @@ -241,66 +228,17 @@ def devRepo = { } } -afterEvaluate { - android.sourceSets.all { sourceSet -> - if (!sourceSet.name.startsWith("test")) { - sourceSet.kotlin.setSrcDirs([]) - } - } - - publishing { - publications { - release(MavenPublication) { - from components.release - - artifactId = 'android-client' - groupId = 'io.split.client' - version = splitVersion - artifact sourcesJar - artifact javadocJar +mavenPublishing { + coordinates("io.split.client", "android-client", splitVersion) + pom(splitPOM) - pom splitPOM - - repositories { - maven releaseRepo - maven devRepo - } - } - - development(MavenPublication) { - from components.release - - artifactId = 'android-client' - groupId = 'io.split.client' - version = splitVersion - artifact sourcesJar - artifact javadocJar - - pom splitPOM - } - } - } - - task publishRelease(type: PublishToMavenRepository) { - publication = publishing.publications.getByName("release") - repository = publishing.repositories.ReleaseRepo - } - - task publishDev(type: PublishToMavenRepository) { - publication = publishing.publications.getByName("development") - repository = publishing.repositories.DevelopmentRepo - } - - signing { - sign publishing.publications.getByName("release") - } + publishToMavenCentral(false) + signAllPublications() } - task sourcesJar(type: Jar) { archiveClassifier.set("sources") from android.sourceSets.main.java.srcDirs -// archiveClassifier = "sources" } task javadoc(type: Javadoc) { diff --git a/deps.txt b/deps.txt index 48ab16c45..a680eefb8 100644 --- a/deps.txt +++ b/deps.txt @@ -123,6 +123,6 @@ releaseRuntimeClasspath - Runtime classpath of compilation 'release' (target (a | \--- com.google.android.gms:play-services-basement:18.1.0 (*) \--- androidx.multidex:multidex:2.0.1 -(*) - dependencies omitted (listed previously) +(*) - Indicates repeated occurrences of a transitive dependency subtree. Gradle expands transitive dependency subtrees only once per project; repeat occurrences only display the root of the subtree, followed by this annotation. A web-based, searchable dependency report is available by adding the --scan option. diff --git a/gradle.properties b/gradle.properties index 19a21ffb5..353a62a02 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,18 @@ -ossrhUsername= -ossrhPassword= +mavenCentralUsername= +mavenCentralPassword= + android.useAndroidX=true +android.nonTransitiveRClass=true +android.defaults.buildfeatures.buildconfig=true +android.nonFinalResIds=false kotlin.stdlib.default.dependency=false + +# Last 8 digits of the key's ID. Can be obtained with gpg --list-secret-keys +signing.keyId= + +# Password that was set on key creation. +signing.password= + +# Path to the file created in the previous step. +signing.secretKeyRingFile= \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 52f11743c..3d8313c42 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip From 76801b84d358bd2b70327e4830b1c81cd16177b6 Mon Sep 17 00:00:00 2001 From: gthea Date: Wed, 30 Jul 2025 09:39:45 -0300 Subject: [PATCH 3/4] Fix race condition in DB initialization (#793) --- .github/workflows/deploy.yml | 35 ----- .github/workflows/instrumented.yml | 6 - .github/workflows/sonarqube.yml | 53 ------- build.gradle | 24 +-- .../client/storage/db/SplitRoomDatabase.java | 8 +- .../storage/db/SplitRoomDatabaseTest.java | 141 ++++++++++++++++++ 6 files changed, 155 insertions(+), 112 deletions(-) delete mode 100644 .github/workflows/deploy.yml create mode 100644 src/test/java/io/split/android/client/storage/db/SplitRoomDatabaseTest.java diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml deleted file mode 100644 index c2ee017b0..000000000 --- a/.github/workflows/deploy.yml +++ /dev/null @@ -1,35 +0,0 @@ -name: Internal deploy - -on: - push: - branches: - - 'development' - - '*_baseline' - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - build-app: - name: Build App - runs-on: ubuntu-latest - env: - ARTIFACTORY_USER: ${{ secrets.ARTIFACTORY_USER }} - ARTIFACTORY_TOKEN: ${{ secrets.ARTIFACTORY_TOKEN }} - steps: - - name: checkout - uses: actions/checkout@v4 - - - name: Gradle cache - uses: gradle/gradle-build-action@v3 - - - name: Set up JDK 17 - uses: actions/setup-java@v3 - with: - distribution: 'temurin' - java-version: 17 - cache: 'gradle' - - - name: Publish - run: ./gradlew publishDev diff --git a/.github/workflows/instrumented.yml b/.github/workflows/instrumented.yml index 6ec106f2e..b43cc2832 100644 --- a/.github/workflows/instrumented.yml +++ b/.github/workflows/instrumented.yml @@ -1,11 +1,5 @@ name: Instrumented tests -on: - pull_request: - branches: - - 'development' - - '*_baseline' - concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml index 88a42ab4d..0bf07cec2 100644 --- a/.github/workflows/sonarqube.yml +++ b/.github/workflows/sonarqube.yml @@ -14,12 +14,6 @@ jobs: name: SonarCloud Scan runs-on: ubuntu-latest steps: - - name: Enable KVM group perms - run: | - echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules - sudo udevadm control --reload-rules - sudo udevadm trigger --name-match=kvm - - name: Checkout uses: actions/checkout@v4 with: @@ -54,53 +48,6 @@ jobs: # Even if tests fail, JaCoCo should generate a report with partial coverage # from the tests that did pass fi - - # Check if the report was generated with content - if [ -f build/reports/jacoco/jacocoTestReport/jacocoTestReport.xml ]; then - # Use stat command compatible with both Linux and macOS - if [[ "$(uname)" == "Darwin" ]]; then - # macOS syntax - REPORT_SIZE=$(stat -f%z build/reports/jacoco/jacocoTestReport/jacocoTestReport.xml) - else - # Linux syntax - REPORT_SIZE=$(stat -c%s build/reports/jacoco/jacocoTestReport/jacocoTestReport.xml) - fi - - if [ "$REPORT_SIZE" -lt 1000 ]; then - echo "JaCoCo report is too small ($REPORT_SIZE bytes), likely empty. Trying to generate from test execution data..." - # Try to generate the report directly from the exec file - if [ -f build/jacoco/testDebugUnitTest.exec ]; then - java -jar ~/.gradle/caches/modules-2/files-2.1/org.jacoco/org.jacoco.cli/0.8.8/*/org.jacoco.cli-0.8.8.jar report build/jacoco/testDebugUnitTest.exec \ - --classfiles build/intermediates/runtime_library_classes_dir/debug \ - --sourcefiles src/main/java \ - --xml build/reports/jacoco/jacocoTestReport/jacocoTestReport.xml - - # Check if the report was successfully generated - if [[ "$(uname)" == "Darwin" ]]; then - # macOS syntax - NEW_REPORT_SIZE=$(stat -f%z build/reports/jacoco/jacocoTestReport/jacocoTestReport.xml) - else - # Linux syntax - NEW_REPORT_SIZE=$(stat -c%s build/reports/jacoco/jacocoTestReport/jacocoTestReport.xml) - fi - - if [ "$NEW_REPORT_SIZE" -lt 1000 ]; then - echo "ERROR: Failed to generate a valid JaCoCo report with coverage data" - exit 1 - else - echo "JaCoCo report successfully generated with size $NEW_REPORT_SIZE bytes" - fi - else - echo "ERROR: No JaCoCo execution data file found. Tests may not have run correctly." - exit 1 - fi - else - echo "JaCoCo report generated successfully with size $REPORT_SIZE bytes" - fi - else - echo "ERROR: JaCoCo report file not found. Coverage data is missing." - exit 1 - fi - name: Prepare class files for SonarQube analysis run: | diff --git a/build.gradle b/build.gradle index bad02ed05..7b2950bd8 100644 --- a/build.gradle +++ b/build.gradle @@ -25,12 +25,12 @@ ext { // Define exclusions for JaCoCo coverage def coverageExclusions = [ - '**/R.class', - '**/R$*.class', - '**/BuildConfig.*', - '**/Manifest*.*', - '**/*Test*.*', - 'android/**/*.*' + '**/R.class', + '**/R$*.class', + '**/BuildConfig.*', + '**/Manifest*.*', + '**/*Test*.*', + 'android/**/*.*' ] android { @@ -76,7 +76,7 @@ android { testOptions { unitTests.returnDefaultValues = true - + // Configure JaCoCo for all test tasks unitTests.all { jacoco { @@ -219,15 +219,6 @@ def splitPOM = { } } -def devRepo = { - name = "DevelopmentRepo" - url = 'https://splitio.jfrog.io/artifactory/maven-all-virtual' - credentials { - username = System.getenv('ARTIFACTORY_USER') - password = System.getenv('ARTIFACTORY_TOKEN') - } -} - mavenPublishing { coordinates("io.split.client", "android-client", splitVersion) pom(splitPOM) @@ -296,4 +287,3 @@ tasks.withType(Test) { - diff --git a/src/main/java/io/split/android/client/storage/db/SplitRoomDatabase.java b/src/main/java/io/split/android/client/storage/db/SplitRoomDatabase.java index e0c54d7dd..0d9d7cc66 100644 --- a/src/main/java/io/split/android/client/storage/db/SplitRoomDatabase.java +++ b/src/main/java/io/split/android/client/storage/db/SplitRoomDatabase.java @@ -99,8 +99,14 @@ public static SplitRoomDatabase getDatabase(final Context context, final String Logger.i("Failed to set optimized pragma"); } - mInstances.put(databaseName, instance); + + // Ensure Room is fully initialized before starting preload thread + try { + instance.getOpenHelper().getWritableDatabase(); // This blocks until validations happen + } catch (Exception e) { + Logger.i("Failed to force Room initialization: " + e.getMessage()); + } new Thread(() -> { try { mInstances.get(databaseName).getSplitQueryDao(); diff --git a/src/test/java/io/split/android/client/storage/db/SplitRoomDatabaseTest.java b/src/test/java/io/split/android/client/storage/db/SplitRoomDatabaseTest.java new file mode 100644 index 000000000..e6ab65dd3 --- /dev/null +++ b/src/test/java/io/split/android/client/storage/db/SplitRoomDatabaseTest.java @@ -0,0 +1,141 @@ +package io.split.android.client.storage.db; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.Context; + +import androidx.room.Room; +import androidx.room.RoomDatabase; +import androidx.sqlite.db.SupportSQLiteDatabase; +import androidx.sqlite.db.SupportSQLiteOpenHelper; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.MockitoAnnotations; + +public class SplitRoomDatabaseTest { + + @Mock + private Context mockContext; + + @Mock + private Context mockApplicationContext; + + @Mock + private RoomDatabase.Builder mockBuilder; + + @Mock + private SplitRoomDatabase mockDatabase; + + @Mock + private SupportSQLiteOpenHelper mockOpenHelper; + + @Mock + private SupportSQLiteDatabase mockSqliteDatabase; + + @Before + public void setUp() { + MockitoAnnotations.openMocks(this); + + when(mockContext.getApplicationContext()).thenReturn(mockApplicationContext); + when(mockDatabase.getOpenHelper()).thenReturn(mockOpenHelper); + when(mockOpenHelper.getWritableDatabase()).thenReturn(mockSqliteDatabase); + } + + @Test + public void shouldCallGetWritableDatabaseDuringInitialization() { + String databaseName = "test_database_unique_1"; + + try (MockedStatic mockedRoom = mockStatic(Room.class)) { + mockRoom(mockedRoom, databaseName); + + SplitRoomDatabase result = SplitRoomDatabase.getDatabase(mockContext, databaseName); + + assertNotNull("Database instance should not be null", result); + // Verify that getWritableDatabase is called twice: + // 1. Once for pragma setup + // 2. Once for ensuring Room initialization before preload thread + verify(mockOpenHelper, times(2)).getWritableDatabase(); + } + } + + @Test + public void shouldHandleExceptionDuringRoomInitializationGracefully() { + String databaseName = "test_database_unique_2"; + + try (MockedStatic mockedRoom = mockStatic(Room.class)) { + mockRoom(mockedRoom, databaseName); + + // Make the second call to getWritableDatabase throw an exception + when(mockOpenHelper.getWritableDatabase()) + .thenReturn(mockSqliteDatabase) + .thenThrow(new RuntimeException("Database initialization failed")); // Second call fails + + SplitRoomDatabase result = SplitRoomDatabase.getDatabase(mockContext, databaseName); + + verify(mockOpenHelper, times(2)).getWritableDatabase(); + // Should still return the database instance despite initialization failure + assertSame("Should return the same database instance", mockDatabase, result); + } + } + + @Test + public void shouldReturnSameInstanceForSameDatabaseName() { + String databaseName = "test_database_unique_3"; + + try (MockedStatic mockedRoom = mockStatic(Room.class)) { + mockRoom(mockedRoom, databaseName); + + SplitRoomDatabase instance1 = SplitRoomDatabase.getDatabase(mockContext, databaseName); + SplitRoomDatabase instance2 = SplitRoomDatabase.getDatabase(mockContext, databaseName); + + assertSame("Should return the same instance for same database name", instance1, instance2); + // getWritableDatabase should only be called during first initialization + verify(mockOpenHelper, times(2)).getWritableDatabase(); + } + } + + @Test + public void shouldCallGetWritableDatabaseBeforeStartingPreloadThread() { + String databaseName = "test_database_unique_4"; + + try (MockedStatic mockedRoom = mockStatic(Room.class)) { + mockRoom(mockedRoom, databaseName); + + SplitRoomDatabase.getDatabase(mockContext, databaseName); + + // Verify that getWritableDatabase is called twice: + // 1. Once for pragma setup + // 2. Once for ensuring Room initialization before preload thread + verify(mockOpenHelper, times(2)).getWritableDatabase(); + + // Verify the database instance is returned + verify(mockBuilder).build(); + } + } + + private void mockRoom(MockedStatic mockedRoom, String databaseName) { + mockedRoom.when(() -> Room.databaseBuilder( + eq(mockApplicationContext), + eq(SplitRoomDatabase.class), + eq(databaseName) + )).thenReturn(mockBuilder); + + mockReturn(); + } + + private void mockReturn() { + when(mockBuilder.setJournalMode(any())).thenReturn(mockBuilder); + when(mockBuilder.fallbackToDestructiveMigration()).thenReturn(mockBuilder); + when(mockBuilder.build()).thenReturn(mockDatabase); + } +} From 9162e2ed348f84bb2c77bf983df07b8d94952cb3 Mon Sep 17 00:00:00 2001 From: gthea Date: Wed, 30 Jul 2025 11:33:21 -0300 Subject: [PATCH 4/4] Prepare release 5.3.1 (#794) --- CHANGES.txt | 4 ++++ build.gradle | 2 +- .../client/service/executor/SplitBaseTaskExecutor.java | 4 ---- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index acb561c06..cbbb0f56e 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,7 @@ +5.3.1 (Jul 30, 2025) +- Fixed race condition in database initialization. +- Fixed increased ANRs when pausing task executor. + 5.3.0 (May 28, 2025) - Added support for rule-based segments. These segments determine membership at runtime by evaluating their configured rules against the user attributes provided to the SDK. - Added support for feature flag prerequisites. This allows customers to define dependency conditions between flags, which are evaluated before any allowlists or targeting rules. diff --git a/build.gradle b/build.gradle index 7b2950bd8..19cae2132 100644 --- a/build.gradle +++ b/build.gradle @@ -19,7 +19,7 @@ apply from: 'spec.gradle' apply from: 'jacoco.gradle' ext { - splitVersion = '5.3.0' + splitVersion = '5.3.1' jacocoVersion = '0.8.8' } diff --git a/src/main/java/io/split/android/client/service/executor/SplitBaseTaskExecutor.java b/src/main/java/io/split/android/client/service/executor/SplitBaseTaskExecutor.java index b45737a8b..997cfbbab 100644 --- a/src/main/java/io/split/android/client/service/executor/SplitBaseTaskExecutor.java +++ b/src/main/java/io/split/android/client/service/executor/SplitBaseTaskExecutor.java @@ -125,10 +125,6 @@ public void submitOnMainThread(SplitTask splitTask) { } public void pause() { - long start = System.currentTimeMillis(); - while (!mScheduledTasks.isEmpty() && (System.currentTimeMillis() - start) < 500L) { - try { Thread.sleep(50); } catch (InterruptedException e) { break; } - } mScheduler.pause(); }