diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml index 14c7410fc..dd26c3568 100644 --- a/.github/workflows/sonarqube.yml +++ b/.github/workflows/sonarqube.yml @@ -27,7 +27,7 @@ jobs: - 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 @@ -39,7 +39,7 @@ jobs: run: | # Build the project ./gradlew assembleDebug --stacktrace - + # Run tests with coverage - allow test failures ./gradlew testDebugUnitTest jacocoTestReport --stacktrace TEST_RESULT=$? @@ -48,17 +48,17 @@ jobs: # 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..." - + # Create the target directory mkdir -p build/intermediates/runtime_library_classes_dir/debug - + # 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..." @@ -66,7 +66,7 @@ jobs: 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 @@ -74,12 +74,12 @@ jobs: 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" fi - + # Update sonar-project.properties with all found class directories echo "" >> sonar-project.properties echo "# Additional binary paths found during build" >> sonar-project.properties @@ -90,14 +90,14 @@ jobs: else echo "sonar.java.binaries=build/intermediates/runtime_library_classes_dir/debug" >> sonar-project.properties 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:" TEST_RESULT_FILES=$(find build -name "TEST-*.xml" 2>/dev/null) @@ -112,7 +112,7 @@ jobs: else echo "No test result files found" fi - + echo "" echo "Checking JaCoCo report content:" if [ -f build/reports/jacoco/jacocoTestReport/jacocoTestReport.xml ]; then @@ -130,22 +130,22 @@ jobs: else echo "JaCoCo report file not found" fi - + echo "" echo "Checking binary directories specified in sonar-project.properties:" echo "build/intermediates/runtime_library_classes_dir/debug:" ls -la build/intermediates/runtime_library_classes_dir/debug || echo "Directory not found" - + echo "" echo "Checking all available class directories:" find build -path "*/build/*" -name "*.class" | head -n 10 || echo "No class files found" - + echo "" echo "Final sonar-project.properties content:" cat sonar-project.properties - name: SonarCloud Scan - uses: SonarSource/sonarqube-scan-action@v5 + uses: SonarSource/sonarqube-scan-action@v6 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information SONAR_TOKEN: ${{ secrets.SONARQUBE_TOKEN }} diff --git a/.harness/instrumented.yaml b/.harness/instrumented.yaml new file mode 100644 index 000000000..a979b5389 --- /dev/null +++ b/.harness/instrumented.yaml @@ -0,0 +1,93 @@ +pipeline: + name: android-client instrumented tests + identifier: androidclient_instrumented_tests + projectIdentifier: Harness_Split + orgIdentifier: PROD + tags: {} + properties: + ci: + codebase: + connectorRef: fmegithubrunnersci + repoName: android-client + build: <+input> + stages: + - stage: + name: Instrumented tests + identifier: Instrumented_tests + description: "" + type: CI + spec: + cloneCodebase: true + caching: + enabled: true + override: true + platform: + os: Linux + arch: Amd64 + runtime: + type: Cloud + spec: + size: large + nestedVirtualization: true + variables: + - name: ANDROID_HOME + type: String + value: /opt/android-sdk + execution: + steps: + - step: + type: Run + name: Install Dependencies + identifier: install_dependencies + spec: + shell: Sh + command: | + chmod +x .harness/scripts/install-deps.sh + .harness/scripts/install-deps.sh + - step: + type: RunTests + name: Start Emulator and Run Tests + identifier: start_emulator_and_run_tests + spec: + language: Java + buildTool: Gradle + args: connectedCheck --continue --stacktrace --info + shell: Sh + preCommand: | + # Set Android SDK environment variables to point to the same location + export ANDROID_SDK_ROOT=$ANDROID_HOME + + export JAVA_HOME=$(dirname $(dirname $(readlink -f $(which java)))) + export PATH="$PATH:$ANDROID_HOME/platform-tools:$ANDROID_HOME/emulator:$ANDROID_HOME/cmdline-tools/latest/bin" + + # Start emulator + chmod +x .harness/scripts/start-emulator.sh + .harness/scripts/start-emulator.sh + testIntelligence: + enabled: true + reports: + type: JUnit + spec: + paths: + - "**/build/outputs/androidTest-results/connected/**/*.xml" + - "**/build/test-results/**/*.xml" + - step: + type: Test + name: Publish Test Results + identifier: publish_test_results + spec: + reports: + type: JUnit + spec: + paths: + - "**/build/outputs/androidTest-results/connected/**/*.xml" + - "**/build/test-results/**/*.xml" + when: + stageStatus: All + timeout: 1h + failureStrategies: + - onFailure: + errors: + - AllErrors + action: + type: MarkAsSuccess diff --git a/.harness/orgs/PROD/projects/Harness_Split/pipelines/androidclient_instrumented_tests/input_sets/inputset.yaml b/.harness/orgs/PROD/projects/Harness_Split/pipelines/androidclient_instrumented_tests/input_sets/inputset.yaml new file mode 100644 index 000000000..0d007c6f6 --- /dev/null +++ b/.harness/orgs/PROD/projects/Harness_Split/pipelines/androidclient_instrumented_tests/input_sets/inputset.yaml @@ -0,0 +1,14 @@ +inputSet: + name: " inputset" + identifier: inputset + orgIdentifier: PROD + projectIdentifier: Harness_Split + pipeline: + identifier: androidclient_instrumented_tests + properties: + ci: + codebase: + build: + type: PR + spec: + number: <+trigger.prNumber> diff --git a/.harness/scripts/install-deps.sh b/.harness/scripts/install-deps.sh new file mode 100755 index 000000000..bbe6e6795 --- /dev/null +++ b/.harness/scripts/install-deps.sh @@ -0,0 +1,184 @@ +#!/usr/bin/env bash +set -euo pipefail + +echo "========================================" +echo " Android Platform Dependencies Setup" +echo "========================================" +echo "" + +# ============================================ +# 1. Install Java 17 +# ============================================ +echo "=== Installing Java 17 ===" + +# Install Java 17 +echo "Installing OpenJDK 17..." +sudo apt-get update -qq +sudo apt-get install -y openjdk-17-jdk + +# Set JAVA_HOME to Java 17 explicitly +export JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64 + +# Update alternatives to use Java 17 as default +sudo update-alternatives --set java /usr/lib/jvm/java-17-openjdk-amd64/bin/java +sudo update-alternatives --set javac /usr/lib/jvm/java-17-openjdk-amd64/bin/javac + +echo "" +echo "Java 17 installation complete:" +java -version +echo "JAVA_HOME: $JAVA_HOME" + +echo "✓ Java 17 installation complete" +echo "" + +# ============================================ +# 2. Setup Android SDK Tools +# ============================================ +echo "=== Setting up Android SDK Tools ===" + +# Ensure SDK directory exists +mkdir -p "$ANDROID_HOME" + +# Look for command-line tools in common locations +CMDLINE_TOOLS_BIN="" +if [ -d "$ANDROID_HOME/cmdline-tools/latest/bin" ]; then + CMDLINE_TOOLS_BIN="$ANDROID_HOME/cmdline-tools/latest/bin" + echo "✓ Found command-line tools at: $CMDLINE_TOOLS_BIN" +elif [ -d "$ANDROID_HOME/tools/bin" ]; then + CMDLINE_TOOLS_BIN="$ANDROID_HOME/tools/bin" + echo "✓ Found legacy tools at: $CMDLINE_TOOLS_BIN" +else + echo "⚠ Command-line tools not found, installing..." + cd "$ANDROID_HOME" + + # Download Linux command-line tools for AMD64 + CMDLINE_TOOLS_URL="https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip" + + echo "Downloading from: $CMDLINE_TOOLS_URL" + curl -fSL -o cmdline-tools.zip "$CMDLINE_TOOLS_URL" + + # Extract and organize + unzip -q cmdline-tools.zip + mkdir -p cmdline-tools/latest + mv cmdline-tools/* cmdline-tools/latest/ 2>/dev/null || true + rm cmdline-tools.zip + + CMDLINE_TOOLS_BIN="$ANDROID_HOME/cmdline-tools/latest/bin" + echo "✓ Installed command-line tools at: $CMDLINE_TOOLS_BIN" +fi + +# Update PATH to include all Android tools +export PATH="$PATH:$ANDROID_HOME/platform-tools:$ANDROID_HOME/emulator:$CMDLINE_TOOLS_BIN" + +# Test that sdkmanager is accessible +echo "" +echo "Testing SDK Manager access..." +sdkmanager --version + +echo "✓ Android SDK tools setup complete" +echo "" + +# ============================================ +# 3. Install Android Components +# ============================================ +echo "=== Installing required Android components ===" + +# Update PATH to include Android tools +if [ -d "$ANDROID_HOME/cmdline-tools/latest/bin" ]; then + export PATH="$PATH:$ANDROID_HOME/cmdline-tools/latest/bin" +elif [ -d "$ANDROID_HOME/tools/bin" ]; then + export PATH="$PATH:$ANDROID_HOME/tools/bin" +fi +export PATH="$PATH:$ANDROID_HOME/platform-tools:$ANDROID_HOME/emulator" + +# Verify SDK Manager is accessible +if ! command -v sdkmanager >/dev/null 2>&1; then + echo "ERROR: SDK Manager not found in PATH" + echo "PATH: $PATH" + exit 1 +fi + +echo "SDK Manager version: $(sdkmanager --version)" +echo "" + +# Accept licenses first +echo "Accepting Android SDK licenses..." +yes | sdkmanager --licenses 2>&1 || echo "Licenses already accepted" +echo "" + +# Get list of installed packages +INSTALLED_PACKAGES=$(sdkmanager --list_installed 2>/dev/null) + +# Install platform-tools +if ! echo "$INSTALLED_PACKAGES" | grep -q "platform-tools"; then + echo "Installing platform-tools..." + sdkmanager "platform-tools" +else + echo "✓ platform-tools already installed" +fi + +# Install build-tools +if ! echo "$INSTALLED_PACKAGES" | grep -q "build-tools;"; then + echo "Installing build-tools..." + sdkmanager "build-tools;34.0.0" +else + echo "✓ build-tools already installed" +fi + +# Install Android platform +if ! echo "$INSTALLED_PACKAGES" | grep -q "platforms;android-29"; then + echo "Installing Android platform 29..." + sdkmanager "platforms;android-29" +else + echo "✓ Android platform 29 already installed" +fi + +# Install x86_64 system image for AMD64 architecture +SYSTEM_IMAGE="system-images;android-29;default;x86_64" +echo "" +echo "Using system image: $SYSTEM_IMAGE" +if ! echo "$INSTALLED_PACKAGES" | grep -q "$SYSTEM_IMAGE"; then + echo "Installing system image: $SYSTEM_IMAGE" + yes | sdkmanager "$SYSTEM_IMAGE" 2>&1 || true +else + echo "✓ System image already installed: $SYSTEM_IMAGE" +fi + +# Install/update emulator (force update to ensure we have a working version) +echo "Installing/updating Android emulator..." +yes | sdkmanager "emulator" 2>&1 || true + +# Verify emulator binary is accessible and working +export PATH="$PATH:$ANDROID_HOME/emulator" +if ! command -v emulator >/dev/null 2>&1; then + echo "ERROR: emulator binary not found after installation" + echo "Contents of ANDROID_HOME/emulator:" + ls -la "$ANDROID_HOME/emulator" || echo "Directory does not exist" + exit 1 +fi + +# Test emulator can show version (quick check it's not corrupted) +echo "Testing emulator binary..." +if emulator -version 2>&1 | head -1 | grep -q "emulator"; then + echo "✓ Emulator binary is working:" + emulator -version | head -3 +else + echo "WARNING: Emulator binary may have issues" + emulator -version 2>&1 | head -5 || echo "Failed to get emulator version" +fi + +echo "" +echo "Refreshing SDK package cache..." +# Force package list refresh so avdmanager recognizes newly installed packages +sdkmanager --list > /dev/null 2>&1 || echo "Warning: Failed to refresh package list" + +echo "" +echo "=== Android SDK components installed ===" +echo "" +echo "Installed packages:" +sdkmanager --list_installed + +echo "" +echo "========================================" +echo " ✓ Android Dependencies Complete" +echo "========================================" diff --git a/.harness/scripts/start-emulator.sh b/.harness/scripts/start-emulator.sh new file mode 100755 index 000000000..8e9986e85 --- /dev/null +++ b/.harness/scripts/start-emulator.sh @@ -0,0 +1,163 @@ +#!/usr/bin/env bash +set -euo pipefail + +echo "========================================" +echo " Starting Android Emulator" +echo "========================================" +echo "" + +# Update PATH to include Android tools +export PATH="$PATH:$ANDROID_HOME/platform-tools:$ANDROID_HOME/emulator:$ANDROID_HOME/cmdline-tools/latest/bin" + +# Verify required tools are available +if ! command -v avdmanager >/dev/null 2>&1; then + echo "ERROR: avdmanager not found in PATH" + exit 1 +fi + +if ! command -v emulator >/dev/null 2>&1; then + echo "ERROR: emulator not found in PATH" + exit 1 +fi + +# Use x86_64 system image for AMD64 architecture +AVD_NAME="test_device_api29" +SYSTEM_IMAGE="system-images;android-29;default;x86_64" + +echo "Creating AVD: $AVD_NAME" +echo "System image: $SYSTEM_IMAGE" +echo "" + +# Create AVD (force recreate if exists) +echo no | avdmanager create avd \ + -n "$AVD_NAME" \ + -k "$SYSTEM_IMAGE" \ + --force \ + --device "pixel_5" + +echo "✓ AVD created successfully" +echo "" + +# Verify AVD was created and get its actual path +echo "Verifying AVD creation..." +echo "Listing available AVDs:" +avdmanager list avd + +# Extract the actual AVD path from avdmanager output +AVD_PATH=$(avdmanager list avd | grep -A 3 "Name: ${AVD_NAME}" | grep "Path:" | sed 's/.*Path: //') + +if [ -z "$AVD_PATH" ]; then + echo "ERROR: Could not find AVD path for: $AVD_NAME" + exit 1 +fi + +echo "" +echo "✓ AVD found at: $AVD_PATH" + +# The .ini file should be in the parent directory +AVD_DIR=$(dirname "$AVD_PATH") +AVD_INI_FILE="${AVD_DIR}/${AVD_NAME}.ini" +AVD_CONFIG_PATH="${AVD_PATH}/config.ini" + +echo "AVD ini file: $AVD_INI_FILE" +echo "AVD config file: $AVD_CONFIG_PATH" +echo "" + +# Configure AVD for CI environment (disable animations, reduce resources) +if [ -f "$AVD_CONFIG_PATH" ]; then + echo "Optimizing AVD configuration for CI..." + # Increase RAM and heap for better performance + echo "hw.ramSize=2048" >> "$AVD_CONFIG_PATH" + echo "vm.heapSize=256" >> "$AVD_CONFIG_PATH" + echo "✓ AVD configuration optimized" +else + echo "WARNING: AVD config file not found at: $AVD_CONFIG_PATH" +fi + +echo "" +echo "Starting emulator in background..." +echo "" + +# Set ANDROID_AVD_HOME so emulator can find the AVD +export ANDROID_AVD_HOME="$AVD_DIR" +echo "ANDROID_AVD_HOME set to: $ANDROID_AVD_HOME" +echo "" + +# Start emulator with CI-friendly flags +nohup emulator -avd "$AVD_NAME" \ + -no-window \ + -no-audio \ + -no-boot-anim \ + -gpu swiftshader_indirect \ + -no-snapshot \ + -wipe-data \ + -camera-back none \ + -camera-front none \ + > /tmp/emulator.log 2>&1 & + +EMULATOR_PID=$! +echo "Emulator started with PID: $EMULATOR_PID" +echo "" + +# Wait for emulator to be detected by adb +echo "Waiting for emulator to be detected by adb..." +TIMEOUT=300 # 5 minutes timeout +ELAPSED=0 +until adb devices | grep -q "emulator"; do + if [ $ELAPSED -ge $TIMEOUT ]; then + echo "ERROR: Emulator not detected after ${TIMEOUT}s" + echo "Last 50 lines of emulator log:" + tail -50 /tmp/emulator.log + exit 1 + fi + sleep 2 + ELAPSED=$((ELAPSED + 2)) + echo -n "." +done + +echo "" +echo "✓ Emulator detected by adb" +echo "" + +# Wait for device to be ready +echo "Waiting for device to boot (this may take a few minutes)..." +adb wait-for-device + +# Wait for boot to complete +BOOT_TIMEOUT=600 # 10 minutes timeout +BOOT_ELAPSED=0 +until adb shell getprop sys.boot_completed 2>/dev/null | grep -q "1"; do + if [ $BOOT_ELAPSED -ge $BOOT_TIMEOUT ]; then + echo "ERROR: Device boot not completed after ${BOOT_TIMEOUT}s" + echo "Last 50 lines of emulator log:" + tail -50 /tmp/emulator.log + adb shell getprop + exit 1 + fi + sleep 5 + BOOT_ELAPSED=$((BOOT_ELAPSED + 5)) + echo -n "." +done + +echo "" +echo "✓ Device boot completed" +echo "" + +# Disable animations for faster test execution +echo "Disabling animations..." +adb shell settings put global window_animation_scale 0 +adb shell settings put global transition_animation_scale 0 +adb shell settings put global animator_duration_scale 0 +echo "✓ Animations disabled" +echo "" + +# Show device info +echo "Device information:" +echo " API Level: $(adb shell getprop ro.build.version.sdk)" +echo " Model: $(adb shell getprop ro.product.model)" +echo " Android Version: $(adb shell getprop ro.build.version.release)" +echo "" + +echo "========================================" +echo " ✓ Emulator Ready for Testing" +echo "========================================" diff --git a/CHANGES.txt b/CHANGES.txt index 9864bd165..84eaa9379 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,6 @@ +5.4.2 (Oct 30, 2025) +- Added support for Fallback Treatments in LOCALHOST mode. + 5.4.1 (Oct 14, 2025) - Optimized SDK_READY performance. diff --git a/build.gradle b/build.gradle index 4b497cc06..5a398e16b 100644 --- a/build.gradle +++ b/build.gradle @@ -19,7 +19,7 @@ apply from: 'spec.gradle' apply from: 'jacoco.gradle' ext { - splitVersion = '5.4.1' + splitVersion = '5.4.2' jacocoVersion = '0.8.8' } @@ -143,7 +143,6 @@ dependencies { def apacheCommonsVersion = '3.12.0' def kotlinVer = '1.5.31' def mockWebServerVersion = '4.12.0' - def robolectricVersion = '4.16' def testRulesVersion = '1.4.0' def jUnitExtVersion = '1.1.3' @@ -178,8 +177,6 @@ dependencies { testImplementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVer" testImplementation "org.jetbrains.kotlin:kotlin-test-junit:$kotlinVer" testImplementation "com.google.guava:guava:$guavaVersion" - testImplementation "org.robolectric:robolectric:$robolectricVersion" - testImplementation "androidx.work:work-testing:$workVersion" androidTestImplementation "androidx.test:rules:$testRulesVersion" androidTestImplementation "androidx.test.ext:junit:$jUnitExtVersion" diff --git a/gradle.properties b/gradle.properties index 353a62a02..aa8a41b37 100644 --- a/gradle.properties +++ b/gradle.properties @@ -8,6 +8,9 @@ android.defaults.buildfeatures.buildconfig=true android.nonFinalResIds=false kotlin.stdlib.default.dependency=false +# Increase heap size for DEX merging to avoid OOM errors +org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError + # Last 8 digits of the key's ID. Can be obtained with gpg --list-secret-keys signing.keyId= diff --git a/src/test/java/io/split/android/client/SplitFactoryImplFreshInstallTest.java b/src/androidTest/java/tests/integration/SplitFactoryFreshInstallTest.java similarity index 93% rename from src/test/java/io/split/android/client/SplitFactoryImplFreshInstallTest.java rename to src/androidTest/java/tests/integration/SplitFactoryFreshInstallTest.java index fe385cee6..e7c182103 100644 --- a/src/test/java/io/split/android/client/SplitFactoryImplFreshInstallTest.java +++ b/src/androidTest/java/tests/integration/SplitFactoryFreshInstallTest.java @@ -1,4 +1,4 @@ -package io.split.android.client; +package tests.integration; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; @@ -6,23 +6,22 @@ import android.content.Context; import androidx.test.platform.app.InstrumentationRegistry; -import androidx.work.Configuration; -import androidx.work.testing.WorkManagerTestInitHelper; import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.Timeout; -import org.junit.runner.RunWith; -import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; import java.io.File; import java.util.ArrayList; import java.util.Collections; import java.util.concurrent.TimeUnit; +import io.split.android.client.ServiceEndpoints; +import io.split.android.client.SplitClientConfig; +import io.split.android.client.SplitFactory; +import io.split.android.client.SplitFactoryBuilder; import io.split.android.client.api.Key; import io.split.android.client.dtos.Excluded; import io.split.android.client.dtos.RuleBasedSegment; @@ -39,9 +38,7 @@ /** * Tests for SplitFactoryImpl fresh install prefetch functionality. */ -@RunWith(RobolectricTestRunner.class) -@Config(sdk = 33, manifest = Config.NONE) -public class SplitFactoryImplFreshInstallTest { +public class SplitFactoryFreshInstallTest { @Rule public Timeout globalTimeout = Timeout.seconds(30); @@ -56,11 +53,6 @@ public class SplitFactoryImplFreshInstallTest { public void setUp() throws Exception { mContext = InstrumentationRegistry.getInstrumentation().getContext(); - // Initialize WorkManager - Configuration config = new Configuration.Builder() - .build(); - WorkManagerTestInitHelper.initializeTestWorkManager(mContext, config); - mMockWebServer = new MockWebServer(); mMockWebServer.start(); @@ -235,8 +227,8 @@ private void cleanupDatabases() { "test-fresh", "testfres", mApiToken != null && mApiToken.length() >= 4 - ? mApiToken.substring(0, 4) + "resh" - : "testresh" + ? mApiToken.substring(0, 4) + "resh" + : "testresh" }; for (String dbName : possibleDbNames) { diff --git a/src/main/java/io/split/android/client/localhost/LocalhostSplitClient.java b/src/main/java/io/split/android/client/localhost/LocalhostSplitClient.java index dacead934..00c6a4f51 100644 --- a/src/main/java/io/split/android/client/localhost/LocalhostSplitClient.java +++ b/src/main/java/io/split/android/client/localhost/LocalhostSplitClient.java @@ -4,6 +4,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import java.lang.ref.WeakReference; import java.util.Collections; @@ -20,28 +21,28 @@ import io.split.android.client.SplitClientConfig; import io.split.android.client.SplitFactory; import io.split.android.client.SplitResult; +import io.split.android.client.TreatmentLabels; import io.split.android.client.api.Key; import io.split.android.client.attributes.AttributesManager; import io.split.android.client.attributes.AttributesMerger; import io.split.android.client.events.SplitEvent; import io.split.android.client.events.SplitEventTask; import io.split.android.client.events.SplitEventsManager; +import io.split.android.client.fallback.FallbackTreatmentsCalculator; +import io.split.android.client.fallback.FallbackTreatmentsCalculatorImpl; +import io.split.android.client.fallback.FallbackTreatmentsConfiguration; import io.split.android.client.impressions.ImpressionListener; import io.split.android.client.shared.SplitClientContainer; import io.split.android.client.storage.splits.SplitsStorage; import io.split.android.client.telemetry.storage.TelemetryStorageProducer; import io.split.android.client.utils.logger.Logger; import io.split.android.client.validators.FlagSetsValidatorImpl; -import io.split.android.client.fallback.FallbackTreatmentsCalculator; -import io.split.android.client.fallback.FallbackTreatmentsCalculatorImpl; -import io.split.android.client.fallback.FallbackTreatmentsConfiguration; import io.split.android.client.validators.KeyValidatorImpl; import io.split.android.client.validators.SplitValidatorImpl; import io.split.android.client.validators.TreatmentManager; import io.split.android.client.validators.TreatmentManagerImpl; import io.split.android.client.validators.ValidationMessageLoggerImpl; import io.split.android.engine.experiments.SplitParser; -import io.split.android.grammar.Treatments; /** * An implementation of SplitClient that considers all partitions @@ -54,9 +55,10 @@ public final class LocalhostSplitClient implements SplitClient { private final WeakReference mClientContainer; private final Key mKey; private final SplitEventsManager mEventsManager; - private final TreatmentManager mTreatmentManager; + private TreatmentManager mTreatmentManager; private boolean mIsClientDestroyed = false; private final SplitsStorage mSplitsStorage; + private FallbackTreatmentsCalculator mFallbackTreatmentsCalculator; public LocalhostSplitClient(@NonNull LocalhostSplitFactory container, @NonNull SplitClientContainer clientContainer, @@ -75,13 +77,16 @@ public LocalhostSplitClient(@NonNull LocalhostSplitFactory container, mKey = checkNotNull(key); mEventsManager = checkNotNull(eventsManager); mSplitsStorage = splitsStorage; - FallbackTreatmentsCalculator calculator = new FallbackTreatmentsCalculatorImpl(FallbackTreatmentsConfiguration.builder().build()); + FallbackTreatmentsConfiguration fallbackTreatmentsConfig = splitClientConfig.fallbackTreatments(); + FallbackTreatmentsConfiguration fallbackTreatmentsConfiguration = fallbackTreatmentsConfig != null ? + fallbackTreatmentsConfig : FallbackTreatmentsConfiguration.builder().build(); + mFallbackTreatmentsCalculator = new FallbackTreatmentsCalculatorImpl(fallbackTreatmentsConfiguration); mTreatmentManager = new TreatmentManagerImpl(mKey.matchingKey(), mKey.bucketingKey(), - new EvaluatorImpl(splitsStorage, splitParser, calculator), new KeyValidatorImpl(), + new EvaluatorImpl(splitsStorage, splitParser, mFallbackTreatmentsCalculator), new KeyValidatorImpl(), new SplitValidatorImpl(), getImpressionsListener(splitClientConfig), splitClientConfig.labelsEnabled(), eventsManager, attributesManager, attributesMerger, telemetryStorageProducer, flagSetsFilter, splitsStorage, new ValidationMessageLoggerImpl(), new FlagSetsValidatorImpl(), - new PropertyValidatorImpl(), calculator); + new PropertyValidatorImpl(), mFallbackTreatmentsCalculator); } @Override @@ -101,7 +106,7 @@ public String getTreatment(String featureFlagName, Map attribute } catch (Exception exception) { Logger.e(exception); - return Treatments.CONTROL; + return mFallbackTreatmentsCalculator.resolve(featureFlagName, TreatmentLabels.EXCEPTION).getTreatment(); } } @@ -117,7 +122,7 @@ public SplitResult getTreatmentWithConfig(String featureFlagName, Map getTreatments(List featureFlagNames, Map result = new HashMap<>(); for (String featureFlagName : featureFlagNames) { - result.put(featureFlagName, Treatments.CONTROL); + result.put(featureFlagName, mFallbackTreatmentsCalculator.resolve(featureFlagName, TreatmentLabels.EXCEPTION).getTreatment()); } return result; @@ -158,7 +163,7 @@ public Map getTreatmentsWithConfig(List featureFlag Map result = new HashMap<>(); for (String featureFlagName : featureFlagNames) { - result.put(featureFlagName, new SplitResult(Treatments.CONTROL)); + result.put(featureFlagName, new SplitResult(mFallbackTreatmentsCalculator.resolve(featureFlagName, TreatmentLabels.EXCEPTION).getTreatment())); } return result; @@ -348,7 +353,7 @@ private Map buildExceptionResult(List flagSets) { Map result = new HashMap<>(); Set namesByFlagSets = mSplitsStorage.getNamesByFlagSets(flagSets); for (String featureFlagName : namesByFlagSets) { - result.put(featureFlagName, Treatments.CONTROL); + result.put(featureFlagName, mFallbackTreatmentsCalculator.resolve(featureFlagName, TreatmentLabels.EXCEPTION).getTreatment()); } return result; @@ -358,9 +363,19 @@ private Map buildExceptionResultWithConfig(List fla Map result = new HashMap<>(); Set namesByFlagSets = mSplitsStorage.getNamesByFlagSets(flagSets); for (String featureFlagName : namesByFlagSets) { - result.put(featureFlagName, new SplitResult(Treatments.CONTROL)); + result.put(featureFlagName, new SplitResult(mFallbackTreatmentsCalculator.resolve(featureFlagName, TreatmentLabels.EXCEPTION).getTreatment())); } return result; } + + @VisibleForTesting + void setTreatmentManagerForTesting(@NonNull TreatmentManager treatmentManager) { + mTreatmentManager = checkNotNull(treatmentManager); + } + + @VisibleForTesting + void setFallbackTreatmentsCalculatorForTesting(@NonNull FallbackTreatmentsCalculator fallbackTreatmentsCalculator) { + mFallbackTreatmentsCalculator = checkNotNull(fallbackTreatmentsCalculator); + } } diff --git a/src/test/java/io/split/android/client/localhost/LocalhostSplitClientTest.java b/src/test/java/io/split/android/client/localhost/LocalhostSplitClientTest.java new file mode 100644 index 000000000..8ff8cf368 --- /dev/null +++ b/src/test/java/io/split/android/client/localhost/LocalhostSplitClientTest.java @@ -0,0 +1,678 @@ +package io.split.android.client.localhost; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyMap; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import io.split.android.client.EvaluationOptions; +import io.split.android.client.FlagSetsFilter; +import io.split.android.client.SplitClientConfig; +import io.split.android.client.SplitResult; +import io.split.android.client.TreatmentLabels; +import io.split.android.client.api.Key; +import io.split.android.client.attributes.AttributesManager; +import io.split.android.client.attributes.AttributesMerger; +import io.split.android.client.events.SplitEvent; +import io.split.android.client.events.SplitEventTask; +import io.split.android.client.events.SplitEventsManager; +import io.split.android.client.fallback.FallbackTreatment; +import io.split.android.client.fallback.FallbackTreatmentsCalculator; +import io.split.android.client.shared.SplitClientContainer; +import io.split.android.client.storage.splits.SplitsStorage; +import io.split.android.client.telemetry.storage.TelemetryStorageProducer; +import io.split.android.client.validators.TreatmentManager; +import io.split.android.engine.experiments.SplitParser; + +public class LocalhostSplitClientTest { + + @Mock + private LocalhostSplitFactory mockFactory; + @Mock + private SplitClientContainer mockClientContainer; + @Mock + private SplitClientConfig mockConfig; + @Mock + private SplitsStorage mockSplitsStorage; + @Mock + private SplitEventsManager mockEventsManager; + @Mock + private SplitParser mockSplitParser; + @Mock + private AttributesManager mockAttributesManager; + @Mock + private AttributesMerger mockAttributesMerger; + @Mock + private TelemetryStorageProducer mockTelemetryStorageProducer; + @Mock + private FlagSetsFilter mockFlagSetsFilter; + @Mock + private TreatmentManager mockTreatmentManager; + + private Key testKey; + private LocalhostSplitClient client; + + @Before + public void setUp() { + MockitoAnnotations.openMocks(this); + testKey = new Key("test_matching_key", "test_bucketing_key"); + + when(mockConfig.labelsEnabled()).thenReturn(true); + when(mockConfig.impressionListener()).thenReturn(null); + + client = new LocalhostSplitClient( + mockFactory, + mockClientContainer, + mockConfig, + testKey, + mockSplitsStorage, + mockEventsManager, + mockSplitParser, + mockAttributesManager, + mockAttributesMerger, + mockTelemetryStorageProducer, + mockFlagSetsFilter + ); + } + + @Test + public void getTreatmentWithoutAttributesUsesEmptyMap() { + when(mockTreatmentManager.getTreatment(eq("feature"), anyMap(), eq(null), anyBoolean())) + .thenReturn("off"); + client.setTreatmentManagerForTesting(mockTreatmentManager); + + String result = client.getTreatment("feature"); + + assertEquals("off", result); + verify(mockTreatmentManager).getTreatment("feature", Collections.emptyMap(), null, false); + } + + @Test + public void getTreatmentWithAttributesCallsWithNullEvaluationOptions() { + Map attributes = new HashMap<>(); + when(mockTreatmentManager.getTreatment(eq("feature"), eq(attributes), eq(null), anyBoolean())) + .thenReturn("on"); + client.setTreatmentManagerForTesting(mockTreatmentManager); + + String result = client.getTreatment("feature", attributes); + + assertEquals("on", result); + verify(mockTreatmentManager).getTreatment("feature", attributes, null, false); + } + + @Test + public void getTreatmentWithEvaluationOptionsDelegatesToTreatmentManager() { + Map attributes = new HashMap<>(); + EvaluationOptions options = mock(EvaluationOptions.class); + when(mockTreatmentManager.getTreatment(eq("feature"), eq(attributes), eq(options), anyBoolean())) + .thenReturn("on"); + client.setTreatmentManagerForTesting(mockTreatmentManager); + + String result = client.getTreatment("feature", attributes, options); + + assertEquals("on", result); + verify(mockTreatmentManager).getTreatment("feature", attributes, options, false); + } + + @Test + public void getTreatmentWithConfigWithAttributesCallsWithNullEvaluationOptions() { + Map attributes = new HashMap<>(); + SplitResult expectedResult = new SplitResult("on", "{\"color\":\"red\"}"); + when(mockTreatmentManager.getTreatmentWithConfig(eq("feature"), eq(attributes), eq(null), anyBoolean())) + .thenReturn(expectedResult); + client.setTreatmentManagerForTesting(mockTreatmentManager); + + SplitResult result = client.getTreatmentWithConfig("feature", attributes); + + assertEquals("on", result.treatment()); + assertEquals("{\"color\":\"red\"}", result.config()); + verify(mockTreatmentManager).getTreatmentWithConfig("feature", attributes, null, false); + } + + @Test + public void getTreatmentWithConfigDelegatesToTreatmentManager() { + Map attributes = new HashMap<>(); + EvaluationOptions options = mock(EvaluationOptions.class); + SplitResult expectedResult = new SplitResult("on", "{\"color\":\"blue\"}"); + when(mockTreatmentManager.getTreatmentWithConfig(eq("feature"), eq(attributes), eq(options), anyBoolean())) + .thenReturn(expectedResult); + client.setTreatmentManagerForTesting(mockTreatmentManager); + + SplitResult result = client.getTreatmentWithConfig("feature", attributes, options); + + assertEquals("on", result.treatment()); + assertEquals("{\"color\":\"blue\"}", result.config()); + verify(mockTreatmentManager).getTreatmentWithConfig("feature", attributes, options, false); + } + + @Test + public void getTreatmentsWithAttributesCallsWithNullEvaluationOptions() { + List features = Arrays.asList("feature1", "feature2"); + Map attributes = new HashMap<>(); + Map expected = new HashMap<>(); + expected.put("feature1", "on"); + expected.put("feature2", "off"); + when(mockTreatmentManager.getTreatments(eq(features), eq(attributes), eq(null), anyBoolean())) + .thenReturn(expected); + client.setTreatmentManagerForTesting(mockTreatmentManager); + + Map results = client.getTreatments(features, attributes); + + assertEquals(2, results.size()); + assertEquals("on", results.get("feature1")); + assertEquals("off", results.get("feature2")); + verify(mockTreatmentManager).getTreatments(features, attributes, null, false); + } + + @Test + public void getTreatmentsDelegatesToTreatmentManager() { + List features = Arrays.asList("feature1", "feature2"); + Map attributes = new HashMap<>(); + EvaluationOptions options = mock(EvaluationOptions.class); + Map expected = new HashMap<>(); + expected.put("feature1", "on"); + expected.put("feature2", "off"); + when(mockTreatmentManager.getTreatments(eq(features), eq(attributes), eq(options), anyBoolean())) + .thenReturn(expected); + client.setTreatmentManagerForTesting(mockTreatmentManager); + + Map results = client.getTreatments(features, attributes, options); + + assertEquals(2, results.size()); + assertEquals("on", results.get("feature1")); + assertEquals("off", results.get("feature2")); + verify(mockTreatmentManager).getTreatments(features, attributes, options, false); + } + + @Test + public void getTreatmentsWithConfigWithAttributesCallsWithNullEvaluationOptions() { + List features = Arrays.asList("feature1", "feature2"); + Map attributes = new HashMap<>(); + Map expected = new HashMap<>(); + expected.put("feature1", new SplitResult("on", "{\"test\":true}")); + expected.put("feature2", new SplitResult("off")); + when(mockTreatmentManager.getTreatmentsWithConfig(eq(features), eq(attributes), eq(null), anyBoolean())) + .thenReturn(expected); + client.setTreatmentManagerForTesting(mockTreatmentManager); + + Map results = client.getTreatmentsWithConfig(features, attributes); + + assertEquals(2, results.size()); + assertEquals("on", results.get("feature1").treatment()); + assertEquals("{\"test\":true}", results.get("feature1").config()); + verify(mockTreatmentManager).getTreatmentsWithConfig(features, attributes, null, false); + } + + @Test + public void getTreatmentsWithConfigDelegatesToTreatmentManager() { + List features = Arrays.asList("feature1", "feature2"); + Map attributes = new HashMap<>(); + EvaluationOptions options = mock(EvaluationOptions.class); + Map expected = new HashMap<>(); + expected.put("feature1", new SplitResult("on", "{\"size\":10}")); + expected.put("feature2", new SplitResult("off")); + when(mockTreatmentManager.getTreatmentsWithConfig(eq(features), eq(attributes), eq(options), anyBoolean())) + .thenReturn(expected); + client.setTreatmentManagerForTesting(mockTreatmentManager); + + Map results = client.getTreatmentsWithConfig(features, attributes, options); + + assertEquals(2, results.size()); + assertEquals("on", results.get("feature1").treatment()); + assertEquals("{\"size\":10}", results.get("feature1").config()); + verify(mockTreatmentManager).getTreatmentsWithConfig(features, attributes, options, false); + } + + @Test + public void getTreatmentsByFlagSetWithAttributesCallsWithNullEvaluationOptions() { + Map attributes = new HashMap<>(); + Map expected = new HashMap<>(); + expected.put("feature1", "on"); + when(mockTreatmentManager.getTreatmentsByFlagSet(eq("backend"), eq(attributes), eq(null), anyBoolean())) + .thenReturn(expected); + client.setTreatmentManagerForTesting(mockTreatmentManager); + + Map results = client.getTreatmentsByFlagSet("backend", attributes); + + assertEquals(1, results.size()); + assertEquals("on", results.get("feature1")); + verify(mockTreatmentManager).getTreatmentsByFlagSet("backend", attributes, null, false); + } + + @Test + public void getTreatmentsByFlagSetDelegatesToTreatmentManager() { + Map attributes = new HashMap<>(); + EvaluationOptions options = mock(EvaluationOptions.class); + Map expected = new HashMap<>(); + expected.put("feature1", "on"); + when(mockTreatmentManager.getTreatmentsByFlagSet(eq("backend"), eq(attributes), eq(options), anyBoolean())) + .thenReturn(expected); + client.setTreatmentManagerForTesting(mockTreatmentManager); + + Map results = client.getTreatmentsByFlagSet("backend", attributes, options); + + assertEquals(1, results.size()); + assertEquals("on", results.get("feature1")); + verify(mockTreatmentManager).getTreatmentsByFlagSet("backend", attributes, options, false); + } + + @Test + public void getTreatmentsByFlagSetsWithAttributesCallsWithNullEvaluationOptions() { + List flagSets = Arrays.asList("backend", "frontend"); + Map attributes = new HashMap<>(); + Map expected = new HashMap<>(); + expected.put("feature1", "on"); + expected.put("feature2", "off"); + when(mockTreatmentManager.getTreatmentsByFlagSets(eq(flagSets), eq(attributes), eq(null), anyBoolean())) + .thenReturn(expected); + client.setTreatmentManagerForTesting(mockTreatmentManager); + + Map results = client.getTreatmentsByFlagSets(flagSets, attributes); + + assertEquals(2, results.size()); + verify(mockTreatmentManager).getTreatmentsByFlagSets(flagSets, attributes, null, false); + } + + @Test + public void getTreatmentsByFlagSetsDelegatesToTreatmentManager() { + List flagSets = Arrays.asList("backend", "frontend"); + Map attributes = new HashMap<>(); + EvaluationOptions options = mock(EvaluationOptions.class); + Map expected = new HashMap<>(); + expected.put("feature1", "on"); + expected.put("feature2", "off"); + when(mockTreatmentManager.getTreatmentsByFlagSets(eq(flagSets), eq(attributes), eq(options), anyBoolean())) + .thenReturn(expected); + client.setTreatmentManagerForTesting(mockTreatmentManager); + + Map results = client.getTreatmentsByFlagSets(flagSets, attributes, options); + + assertEquals(2, results.size()); + verify(mockTreatmentManager).getTreatmentsByFlagSets(flagSets, attributes, options, false); + } + + @Test + public void getTreatmentsWithConfigByFlagSetWithAttributesCallsWithNullEvaluationOptions() { + Map attributes = new HashMap<>(); + Map expected = new HashMap<>(); + expected.put("feature1", new SplitResult("on", "{\"version\":1}")); + when(mockTreatmentManager.getTreatmentsWithConfigByFlagSet(eq("backend"), eq(attributes), eq(null), anyBoolean())) + .thenReturn(expected); + client.setTreatmentManagerForTesting(mockTreatmentManager); + + Map results = client.getTreatmentsWithConfigByFlagSet("backend", attributes); + + assertEquals(1, results.size()); + assertEquals("on", results.get("feature1").treatment()); + verify(mockTreatmentManager).getTreatmentsWithConfigByFlagSet("backend", attributes, null, false); + } + + @Test + public void getTreatmentsWithConfigByFlagSetDelegatesToTreatmentManager() { + Map attributes = new HashMap<>(); + EvaluationOptions options = mock(EvaluationOptions.class); + Map expected = new HashMap<>(); + expected.put("feature1", new SplitResult("on", "{\"version\":2}")); + when(mockTreatmentManager.getTreatmentsWithConfigByFlagSet(eq("backend"), eq(attributes), eq(options), anyBoolean())) + .thenReturn(expected); + client.setTreatmentManagerForTesting(mockTreatmentManager); + + Map results = client.getTreatmentsWithConfigByFlagSet("backend", attributes, options); + + assertEquals(1, results.size()); + assertEquals("on", results.get("feature1").treatment()); + verify(mockTreatmentManager).getTreatmentsWithConfigByFlagSet("backend", attributes, options, false); + } + + @Test + public void getTreatmentsWithConfigByFlagSetsWithAttributesCallsWithNullEvaluationOptions() { + List flagSets = Arrays.asList("backend", "frontend"); + Map attributes = new HashMap<>(); + Map expected = new HashMap<>(); + expected.put("feature1", new SplitResult("on")); + expected.put("feature2", new SplitResult("off")); + when(mockTreatmentManager.getTreatmentsWithConfigByFlagSets(eq(flagSets), eq(attributes), eq(null), anyBoolean())) + .thenReturn(expected); + client.setTreatmentManagerForTesting(mockTreatmentManager); + + Map results = client.getTreatmentsWithConfigByFlagSets(flagSets, attributes); + + assertEquals(2, results.size()); + verify(mockTreatmentManager).getTreatmentsWithConfigByFlagSets(flagSets, attributes, null, false); + } + + @Test + public void getTreatmentsWithConfigByFlagSetsDelegatesToTreatmentManager() { + List flagSets = Arrays.asList("backend", "frontend"); + Map attributes = new HashMap<>(); + EvaluationOptions options = mock(EvaluationOptions.class); + Map expected = new HashMap<>(); + expected.put("feature1", new SplitResult("on")); + expected.put("feature2", new SplitResult("off")); + when(mockTreatmentManager.getTreatmentsWithConfigByFlagSets(eq(flagSets), eq(attributes), eq(options), anyBoolean())) + .thenReturn(expected); + client.setTreatmentManagerForTesting(mockTreatmentManager); + + Map results = client.getTreatmentsWithConfigByFlagSets(flagSets, attributes, options); + + assertEquals(2, results.size()); + verify(mockTreatmentManager).getTreatmentsWithConfigByFlagSets(flagSets, attributes, options, false); + } + + @Test + public void destroyRemovesClientFromContainerAndDestroysFactory() { + client.destroy(); + + verify(mockClientContainer).remove(testKey); + verify(mockFactory).destroy(); + } + + @Test + public void isReadyReturnsTrueWhenSdkReadyEventTriggered() { + when(mockEventsManager.eventAlreadyTriggered(SplitEvent.SDK_READY)).thenReturn(true); + + assertTrue(client.isReady()); + verify(mockEventsManager).eventAlreadyTriggered(SplitEvent.SDK_READY); + } + + @Test + public void isReadyReturnsFalseWhenSdkReadyEventNotTriggered() { + when(mockEventsManager.eventAlreadyTriggered(SplitEvent.SDK_READY)).thenReturn(false); + + assertFalse(client.isReady()); + verify(mockEventsManager).eventAlreadyTriggered(SplitEvent.SDK_READY); + } + + @Test + public void onRegistersEventTaskWhenEventNotTriggered() { + SplitEvent event = SplitEvent.SDK_READY; + SplitEventTask task = mock(SplitEventTask.class); + + when(mockEventsManager.eventAlreadyTriggered(event)).thenReturn(false); + + client.on(event, task); + + verify(mockEventsManager).register(event, task); + } + + @Test + public void onRegistersTaskForSdkReadyFromCacheEvenIfAlreadyTriggered() { + SplitEvent event = SplitEvent.SDK_READY_FROM_CACHE; + SplitEventTask task = mock(SplitEventTask.class); + + when(mockEventsManager.eventAlreadyTriggered(event)).thenReturn(true); + + client.on(event, task); + + verify(mockEventsManager).register(event, task); + } + + @Test + public void onDoesNotRegisterEventTaskWhenEventAlreadyTriggered() { + SplitEvent event = SplitEvent.SDK_READY; + SplitEventTask task = mock(SplitEventTask.class); + + when(mockEventsManager.eventAlreadyTriggered(event)).thenReturn(true); + + client.on(event, task); + + verify(mockEventsManager, never()).register(any(), any()); + } + + @Test + public void trackMethodsReturnFalse() { + assertFalse(client.track("user", "event_type")); + assertFalse(client.track("user", "event_type", 10.5)); + assertFalse(client.track("event_type")); + assertFalse(client.track("event_type", 10.5)); + assertFalse(client.track("user", "event_type", new HashMap<>())); + assertFalse(client.track("user", "event_type", 10.5, new HashMap<>())); + assertFalse(client.track("event_type", new HashMap<>())); + assertFalse(client.track("event_type", 10.5, new HashMap<>())); + } + + @Test + public void flushDoesNothing() { + // This should not throw any exception + client.flush(); + } + + @Test + public void setAttributeReturnsTrue() { + assertTrue(client.setAttribute("age", 25)); + } + + @Test + public void getAttributeReturnsNull() { + assertNull(client.getAttribute("age")); + } + + @Test + public void setAttributesReturnsTrue() { + Map attributes = new HashMap<>(); + attributes.put("age", 25); + attributes.put("name", "John"); + + assertTrue(client.setAttributes(attributes)); + } + + @Test + public void getAllAttributesReturnsEmptyMap() { + Map attributes = client.getAllAttributes(); + + assertNotNull(attributes); + assertTrue(attributes.isEmpty()); + } + + @Test + public void removeAttributeReturnsTrue() { + assertTrue(client.removeAttribute("age")); + } + + @Test + public void clearAttributesReturnsTrue() { + assertTrue(client.clearAttributes()); + } + + @Test + public void getTreatmentAfterDestroyPassesDestroyedFlag() { + when(mockTreatmentManager.getTreatment(eq("feature"), anyMap(), eq(null), eq(true))) + .thenReturn("control"); + client.setTreatmentManagerForTesting(mockTreatmentManager); + + client.destroy(); + String result = client.getTreatment("feature"); + + verify(mockTreatmentManager).getTreatment("feature", Collections.emptyMap(), null, true); + } + + @Test + public void getTreatmentWhenTreatmentManagerThrowsReturnsFallback() { + FallbackTreatmentsCalculator mockCalculator = mock(FallbackTreatmentsCalculator.class); + when(mockCalculator.resolve(eq("feature"), eq(TreatmentLabels.EXCEPTION))) + .thenReturn(new FallbackTreatment("control")); + when(mockTreatmentManager.getTreatment(eq("feature"), anyMap(), eq(null), anyBoolean())) + .thenThrow(new RuntimeException("Test exception")); + client.setTreatmentManagerForTesting(mockTreatmentManager); + client.setFallbackTreatmentsCalculatorForTesting(mockCalculator); + + String result = client.getTreatment("feature"); + + assertEquals("control", result); + verify(mockCalculator).resolve("feature", TreatmentLabels.EXCEPTION); + } + + @Test + public void getTreatmentWithConfigWhenTreatmentManagerThrowsReturnsFallback() { + FallbackTreatmentsCalculator mockCalculator = mock(FallbackTreatmentsCalculator.class); + when(mockCalculator.resolve(eq("feature"), eq(TreatmentLabels.EXCEPTION))) + .thenReturn(new FallbackTreatment("control")); + Map attributes = new HashMap<>(); + when(mockTreatmentManager.getTreatmentWithConfig(eq("feature"), eq(attributes), eq(null), anyBoolean())) + .thenThrow(new RuntimeException("Test exception")); + client.setTreatmentManagerForTesting(mockTreatmentManager); + client.setFallbackTreatmentsCalculatorForTesting(mockCalculator); + + SplitResult result = client.getTreatmentWithConfig("feature", attributes); + + assertEquals("control", result.treatment()); + verify(mockCalculator).resolve("feature", TreatmentLabels.EXCEPTION); + } + + @Test + public void getTreatmentsWhenTreatmentManagerThrowsReturnsFallbacks() { + FallbackTreatmentsCalculator mockCalculator = mock(FallbackTreatmentsCalculator.class); + when(mockCalculator.resolve(eq("feature1"), eq(TreatmentLabels.EXCEPTION))) + .thenReturn(new FallbackTreatment("control")); + when(mockCalculator.resolve(eq("feature2"), eq(TreatmentLabels.EXCEPTION))) + .thenReturn(new FallbackTreatment("control")); + List features = Arrays.asList("feature1", "feature2"); + Map attributes = new HashMap<>(); + when(mockTreatmentManager.getTreatments(eq(features), eq(attributes), eq(null), anyBoolean())) + .thenThrow(new RuntimeException("Test exception")); + client.setTreatmentManagerForTesting(mockTreatmentManager); + client.setFallbackTreatmentsCalculatorForTesting(mockCalculator); + + Map results = client.getTreatments(features, attributes); + + assertEquals(2, results.size()); + assertEquals("control", results.get("feature1")); + assertEquals("control", results.get("feature2")); + verify(mockCalculator).resolve("feature1", TreatmentLabels.EXCEPTION); + verify(mockCalculator).resolve("feature2", TreatmentLabels.EXCEPTION); + } + + @Test + public void getTreatmentsWithConfigWhenTreatmentManagerThrowsReturnsFallbacks() { + FallbackTreatmentsCalculator mockCalculator = mock(FallbackTreatmentsCalculator.class); + when(mockCalculator.resolve(eq("feature1"), eq(TreatmentLabels.EXCEPTION))) + .thenReturn(new FallbackTreatment("control")); + when(mockCalculator.resolve(eq("feature2"), eq(TreatmentLabels.EXCEPTION))) + .thenReturn(new FallbackTreatment("control")); + List features = Arrays.asList("feature1", "feature2"); + Map attributes = new HashMap<>(); + when(mockTreatmentManager.getTreatmentsWithConfig(eq(features), eq(attributes), eq(null), anyBoolean())) + .thenThrow(new RuntimeException("Test exception")); + client.setTreatmentManagerForTesting(mockTreatmentManager); + client.setFallbackTreatmentsCalculatorForTesting(mockCalculator); + + Map results = client.getTreatmentsWithConfig(features, attributes); + + assertEquals(2, results.size()); + assertEquals("control", results.get("feature1").treatment()); + assertEquals("control", results.get("feature2").treatment()); + verify(mockCalculator).resolve("feature1", TreatmentLabels.EXCEPTION); + verify(mockCalculator).resolve("feature2", TreatmentLabels.EXCEPTION); + } + + @Test + public void getTreatmentsByFlagSetWhenTreatmentManagerThrowsReturnsFallbacks() { + FallbackTreatmentsCalculator mockCalculator = mock(FallbackTreatmentsCalculator.class); + when(mockCalculator.resolve(eq("feature1"), eq(TreatmentLabels.EXCEPTION))) + .thenReturn(new FallbackTreatment("control")); + when(mockSplitsStorage.getNamesByFlagSets(eq(Collections.singletonList("backend")))) + .thenReturn(Collections.singleton("feature1")); + Map attributes = new HashMap<>(); + when(mockTreatmentManager.getTreatmentsByFlagSet(eq("backend"), eq(attributes), eq(null), anyBoolean())) + .thenThrow(new RuntimeException("Test exception")); + client.setTreatmentManagerForTesting(mockTreatmentManager); + client.setFallbackTreatmentsCalculatorForTesting(mockCalculator); + + Map results = client.getTreatmentsByFlagSet("backend", attributes); + + assertEquals(1, results.size()); + assertEquals("control", results.get("feature1")); + verify(mockCalculator).resolve("feature1", TreatmentLabels.EXCEPTION); + } + + @Test + public void getTreatmentsByFlagSetsWhenTreatmentManagerThrowsReturnsFallbacks() { + FallbackTreatmentsCalculator mockCalculator = mock(FallbackTreatmentsCalculator.class); + when(mockCalculator.resolve(eq("feature1"), eq(TreatmentLabels.EXCEPTION))) + .thenReturn(new FallbackTreatment("control")); + when(mockCalculator.resolve(eq("feature2"), eq(TreatmentLabels.EXCEPTION))) + .thenReturn(new FallbackTreatment("control")); + List flagSets = Arrays.asList("backend", "frontend"); + when(mockSplitsStorage.getNamesByFlagSets(eq(flagSets))) + .thenReturn(Set.of("feature1", "feature2")); + Map attributes = new HashMap<>(); + when(mockTreatmentManager.getTreatmentsByFlagSets(eq(flagSets), eq(attributes), eq(null), anyBoolean())) + .thenThrow(new RuntimeException("Test exception")); + client.setTreatmentManagerForTesting(mockTreatmentManager); + client.setFallbackTreatmentsCalculatorForTesting(mockCalculator); + + Map results = client.getTreatmentsByFlagSets(flagSets, attributes); + + assertEquals(2, results.size()); + assertEquals("control", results.get("feature1")); + assertEquals("control", results.get("feature2")); + verify(mockCalculator).resolve("feature1", TreatmentLabels.EXCEPTION); + verify(mockCalculator).resolve("feature2", TreatmentLabels.EXCEPTION); + } + + @Test + public void getTreatmentsWithConfigByFlagSetWhenTreatmentManagerThrowsReturnsFallbacks() { + FallbackTreatmentsCalculator mockCalculator = mock(FallbackTreatmentsCalculator.class); + when(mockCalculator.resolve(eq("feature1"), eq(TreatmentLabels.EXCEPTION))) + .thenReturn(new FallbackTreatment("control")); + when(mockSplitsStorage.getNamesByFlagSets(eq(Collections.singletonList("backend")))) + .thenReturn(Collections.singleton("feature1")); + Map attributes = new HashMap<>(); + when(mockTreatmentManager.getTreatmentsWithConfigByFlagSet(eq("backend"), eq(attributes), eq(null), anyBoolean())) + .thenThrow(new RuntimeException("Test exception")); + client.setTreatmentManagerForTesting(mockTreatmentManager); + client.setFallbackTreatmentsCalculatorForTesting(mockCalculator); + + Map results = client.getTreatmentsWithConfigByFlagSet("backend", attributes); + + assertEquals(1, results.size()); + assertEquals("control", results.get("feature1").treatment()); + verify(mockCalculator).resolve("feature1", TreatmentLabels.EXCEPTION); + } + + @Test + public void getTreatmentsWithConfigByFlagSetsWhenTreatmentManagerThrowsReturnsFallbacks() { + FallbackTreatmentsCalculator mockCalculator = mock(FallbackTreatmentsCalculator.class); + when(mockCalculator.resolve(eq("feature1"), eq(TreatmentLabels.EXCEPTION))) + .thenReturn(new FallbackTreatment("control")); + when(mockCalculator.resolve(eq("feature2"), eq(TreatmentLabels.EXCEPTION))) + .thenReturn(new FallbackTreatment("control")); + List flagSets = Arrays.asList("backend", "frontend"); + when(mockSplitsStorage.getNamesByFlagSets(eq(flagSets))) + .thenReturn(Set.of("feature1", "feature2")); + Map attributes = new HashMap<>(); + when(mockTreatmentManager.getTreatmentsWithConfigByFlagSets(eq(flagSets), eq(attributes), eq(null), anyBoolean())) + .thenThrow(new RuntimeException("Test exception")); + client.setTreatmentManagerForTesting(mockTreatmentManager); + client.setFallbackTreatmentsCalculatorForTesting(mockCalculator); + + Map results = client.getTreatmentsWithConfigByFlagSets(flagSets, attributes); + + assertEquals(2, results.size()); + assertEquals("control", results.get("feature1").treatment()); + assertEquals("control", results.get("feature2").treatment()); + verify(mockCalculator).resolve("feature1", TreatmentLabels.EXCEPTION); + verify(mockCalculator).resolve("feature2", TreatmentLabels.EXCEPTION); + } +} diff --git a/src/test/java/io/split/android/client/network/ProxySslSocketFactoryProviderImplTest.java b/src/test/java/io/split/android/client/network/ProxySslSocketFactoryProviderImplTest.java index fb69a87c0..b4d88868a 100644 --- a/src/test/java/io/split/android/client/network/ProxySslSocketFactoryProviderImplTest.java +++ b/src/test/java/io/split/android/client/network/ProxySslSocketFactoryProviderImplTest.java @@ -4,7 +4,6 @@ import androidx.annotation.NonNull; -import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; @@ -44,7 +43,6 @@ public void creatingWithValidCaCertCreatesSocketFactory() throws Exception { } } - @Ignore("Robolectric conflic in CI") @Test(expected = Exception.class) public void creatingWithInvalidCaCertThrows() throws Exception { File caCertFile = tempFolder.newFile("invalid-ca.pem"); diff --git a/src/test/java/io/split/android/client/network/SslProxyTunnelEstablisherTest.java b/src/test/java/io/split/android/client/network/SslProxyTunnelEstablisherTest.java index 08cea238b..6172535c3 100644 --- a/src/test/java/io/split/android/client/network/SslProxyTunnelEstablisherTest.java +++ b/src/test/java/io/split/android/client/network/SslProxyTunnelEstablisherTest.java @@ -10,7 +10,6 @@ import org.junit.After; import org.junit.Before; -import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; @@ -39,7 +38,6 @@ import okhttp3.tls.HeldCertificate; -@Ignore("Robolectric conflict in CI") public class SslProxyTunnelEstablisherTest { @Rule