diff --git a/buildSrc/src/main/kotlin/otel.android-library-conventions.gradle.kts b/buildSrc/src/main/kotlin/otel.android-library-conventions.gradle.kts index e65bb337b..dfe72449d 100644 --- a/buildSrc/src/main/kotlin/otel.android-library-conventions.gradle.kts +++ b/buildSrc/src/main/kotlin/otel.android-library-conventions.gradle.kts @@ -32,6 +32,7 @@ android { jvmTarget = javaVersion.toString() apiVersion = minKotlinVersion.version languageVersion = minKotlinVersion.version + freeCompilerArgs = listOf("-Xjvm-default=all") } } @@ -43,7 +44,7 @@ val libs = extensions.getByType().named("libs") dependencies { implementation(libs.findLibrary("findbugs-jsr305").get()) testImplementation(libs.findLibrary("assertj-core").get()) - testImplementation(libs.findBundle("mockito").get()) + testImplementation(libs.findBundle("mocking").get()) testImplementation(libs.findBundle("junit").get()) testImplementation(libs.findLibrary("opentelemetry-sdk-testing").get()) coreLibraryDesugaring(libs.findLibrary("desugarJdkLibs").get()) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 09e9e4777..b0177c6c6 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -37,6 +37,7 @@ androidx-test-core = "androidx.test:core:1.5.0" androidx-test-runner = "androidx.test:runner:1.5.2" mockito-core = { module = "org.mockito:mockito-core", version.ref = "mockito" } mockito-junit-jupiter = { module = "org.mockito:mockito-junit-jupiter", version.ref = "mockito" } +mockk = "io.mockk:mockk:1.13.7" junit-jupiter-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "junit" } junit-jupiter-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junit" } junit-vintage-engine = { module = "org.junit.vintage:junit-vintage-engine", version.ref = "junit" } @@ -59,7 +60,7 @@ byteBuddy-plugin = { module = "net.bytebuddy:byte-buddy-gradle-plugin", version. kotlin-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } [bundles] -mockito = ["mockito-core", "mockito-junit-jupiter"] +mocking = ["mockito-core", "mockito-junit-jupiter", "mockk"] junit = ["junit-jupiter-api", "junit-jupiter-engine", "junit-vintage-engine"] [plugins] diff --git a/instrumentation/src/main/java/io/opentelemetry/android/OpenTelemetryRumBuilder.java b/instrumentation/src/main/java/io/opentelemetry/android/OpenTelemetryRumBuilder.java index a18d68605..8147be50a 100644 --- a/instrumentation/src/main/java/io/opentelemetry/android/OpenTelemetryRumBuilder.java +++ b/instrumentation/src/main/java/io/opentelemetry/android/OpenTelemetryRumBuilder.java @@ -9,8 +9,8 @@ import android.app.Application; import android.util.Log; -import io.opentelemetry.android.config.DiskBufferingConfiguration; import io.opentelemetry.android.config.OtelRumConfig; +import io.opentelemetry.android.features.diskbuffering.DiskBufferingConfiguration; import io.opentelemetry.android.instrumentation.InstrumentedApplication; import io.opentelemetry.android.instrumentation.activity.VisibleScreenTracker; import io.opentelemetry.android.instrumentation.network.CurrentNetworkProvider; diff --git a/instrumentation/src/main/java/io/opentelemetry/android/config/OtelRumConfig.java b/instrumentation/src/main/java/io/opentelemetry/android/config/OtelRumConfig.java index 7051c79db..6b771c119 100644 --- a/instrumentation/src/main/java/io/opentelemetry/android/config/OtelRumConfig.java +++ b/instrumentation/src/main/java/io/opentelemetry/android/config/OtelRumConfig.java @@ -6,6 +6,7 @@ package io.opentelemetry.android.config; import io.opentelemetry.android.ScreenAttributesSpanProcessor; +import io.opentelemetry.android.features.diskbuffering.DiskBufferingConfiguration; import io.opentelemetry.android.instrumentation.network.CurrentNetworkProvider; import io.opentelemetry.api.common.Attributes; import java.util.function.Supplier; diff --git a/instrumentation/src/main/java/io/opentelemetry/android/config/DiskBufferingConfiguration.java b/instrumentation/src/main/java/io/opentelemetry/android/features/diskbuffering/DiskBufferingConfiguration.java similarity index 96% rename from instrumentation/src/main/java/io/opentelemetry/android/config/DiskBufferingConfiguration.java rename to instrumentation/src/main/java/io/opentelemetry/android/features/diskbuffering/DiskBufferingConfiguration.java index 393ce6067..a4629e96c 100644 --- a/instrumentation/src/main/java/io/opentelemetry/android/config/DiskBufferingConfiguration.java +++ b/instrumentation/src/main/java/io/opentelemetry/android/features/diskbuffering/DiskBufferingConfiguration.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.android.config; +package io.opentelemetry.android.features.diskbuffering; /** Configuration for disk buffering. */ public final class DiskBufferingConfiguration { diff --git a/instrumentation/src/main/java/io/opentelemetry/android/features/diskbuffering/scheduler/DefaultExportScheduleHandler.kt b/instrumentation/src/main/java/io/opentelemetry/android/features/diskbuffering/scheduler/DefaultExportScheduleHandler.kt new file mode 100644 index 000000000..ba5820bda --- /dev/null +++ b/instrumentation/src/main/java/io/opentelemetry/android/features/diskbuffering/scheduler/DefaultExportScheduleHandler.kt @@ -0,0 +1,22 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.android.features.diskbuffering.scheduler + +import io.opentelemetry.android.internal.services.ServiceManager +import io.opentelemetry.android.internal.services.periodicwork.PeriodicWorkService +import java.util.concurrent.atomic.AtomicBoolean + +class DefaultExportScheduleHandler(private val exportScheduler: DefaultExportScheduler) : + ExportScheduleHandler { + private val enabled = AtomicBoolean(false) + + override fun enable() { + if (!enabled.getAndSet(true)) { + ServiceManager.get().getService(PeriodicWorkService::class.java) + .enqueue(exportScheduler) + } + } +} diff --git a/instrumentation/src/main/java/io/opentelemetry/android/features/diskbuffering/scheduler/DefaultExportScheduler.kt b/instrumentation/src/main/java/io/opentelemetry/android/features/diskbuffering/scheduler/DefaultExportScheduler.kt new file mode 100644 index 000000000..029a97f16 --- /dev/null +++ b/instrumentation/src/main/java/io/opentelemetry/android/features/diskbuffering/scheduler/DefaultExportScheduler.kt @@ -0,0 +1,27 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.android.features.diskbuffering.scheduler + +import io.opentelemetry.android.internal.services.periodicwork.PeriodicRunnable +import java.util.concurrent.TimeUnit + +class DefaultExportScheduler : PeriodicRunnable() { + companion object { + private val DELAY_BEFORE_NEXT_EXPORT_IN_MILLIS = TimeUnit.SECONDS.toMillis(10) + } + + override fun onRun() { + // TODO for next PR. + } + + override fun shouldStopRunning(): Boolean { + return false + } + + override fun minimumDelayUntilNextRunInMillis(): Long { + return DELAY_BEFORE_NEXT_EXPORT_IN_MILLIS + } +} diff --git a/instrumentation/src/main/java/io/opentelemetry/android/features/diskbuffering/scheduler/ExportScheduleHandler.kt b/instrumentation/src/main/java/io/opentelemetry/android/features/diskbuffering/scheduler/ExportScheduleHandler.kt new file mode 100644 index 000000000..017a54771 --- /dev/null +++ b/instrumentation/src/main/java/io/opentelemetry/android/features/diskbuffering/scheduler/ExportScheduleHandler.kt @@ -0,0 +1,23 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.android.features.diskbuffering.scheduler + +/** + * Sets up a scheduling mechanism to read and export previously stored signals in disk. + */ +interface ExportScheduleHandler { + /** + * Start/Set up the exporting schedule. Called when the disk buffering feature gets enabled. + */ + fun enable() + + /** + * Don't start (or stop if something was previously started) the exporting schedule, no look up + * for data stored in the disk to export will be carried over if this function is called. + * This will be called if the disk buffering feature gets disabled. + */ + fun disable() {} +} diff --git a/instrumentation/src/main/java/io/opentelemetry/android/internal/features/persistence/DiskManager.java b/instrumentation/src/main/java/io/opentelemetry/android/internal/features/persistence/DiskManager.java index f6739c673..47ba752de 100644 --- a/instrumentation/src/main/java/io/opentelemetry/android/internal/features/persistence/DiskManager.java +++ b/instrumentation/src/main/java/io/opentelemetry/android/internal/features/persistence/DiskManager.java @@ -7,7 +7,7 @@ import android.util.Log; import io.opentelemetry.android.RumConstants; -import io.opentelemetry.android.config.DiskBufferingConfiguration; +import io.opentelemetry.android.features.diskbuffering.DiskBufferingConfiguration; import io.opentelemetry.android.internal.services.CacheStorageService; import io.opentelemetry.android.internal.services.PreferencesService; import io.opentelemetry.android.internal.services.ServiceManager; diff --git a/instrumentation/src/main/java/io/opentelemetry/android/internal/services/periodicwork/PeriodicRunnable.kt b/instrumentation/src/main/java/io/opentelemetry/android/internal/services/periodicwork/PeriodicRunnable.kt new file mode 100644 index 000000000..9122fc9e6 --- /dev/null +++ b/instrumentation/src/main/java/io/opentelemetry/android/internal/services/periodicwork/PeriodicRunnable.kt @@ -0,0 +1,57 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.android.internal.services.periodicwork + +import io.opentelemetry.android.internal.services.ServiceManager +import io.opentelemetry.android.internal.tools.time.SystemTime + +/** + * Utility for creating a Runnable that needs to run multiple times. + * + *

This class is internal and not for public use. Its APIs are unstable and can change at any + * time. + */ +abstract class PeriodicRunnable : Runnable { + private var lastTimeItRan: Long? = null + + final override fun run() { + if (isReadyToRun()) { + onRun() + lastTimeItRan = getCurrentTimeMillis() + } + if (!shouldStopRunning()) { + enqueueForNextLoop() + } + } + + private fun isReadyToRun(): Boolean { + return lastTimeItRan?.let { + getCurrentTimeMillis() >= (it + minimumDelayUntilNextRunInMillis()) + } ?: true + } + + private fun enqueueForNextLoop() { + ServiceManager.get().getService(PeriodicWorkService::class.java).enqueue(this) + } + + private fun getCurrentTimeMillis() = SystemTime.get().getCurrentTimeMillis() + + /** + * Called only if a) The runnable has never run before, OR b) The minimum amount of time delay has passed after the last run. + */ + abstract fun onRun() + + /** + * Should return FALSE when further runs are needed, TRUE if no need for this task to ever run again. + */ + abstract fun shouldStopRunning(): Boolean + + /** + * The minimum amount of time to wait between runs, it might take longer than what's defined here + * to run this task again depending on when the next batch of background work will get submitted. + */ + abstract fun minimumDelayUntilNextRunInMillis(): Long +} diff --git a/instrumentation/src/main/java/io/opentelemetry/android/internal/services/periodicwork/PeriodicWorkService.kt b/instrumentation/src/main/java/io/opentelemetry/android/internal/services/periodicwork/PeriodicWorkService.kt new file mode 100644 index 000000000..2725870d4 --- /dev/null +++ b/instrumentation/src/main/java/io/opentelemetry/android/internal/services/periodicwork/PeriodicWorkService.kt @@ -0,0 +1,75 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.android.internal.services.periodicwork + +import android.os.Handler +import android.os.Looper +import io.opentelemetry.android.internal.services.Service +import java.util.concurrent.ConcurrentLinkedQueue +import java.util.concurrent.LinkedBlockingQueue +import java.util.concurrent.ThreadPoolExecutor +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean + +/** + * Utility to run periodic background work. + * + *

This class is internal and not for public use. Its APIs are unstable and can change at any + * time. + */ +class PeriodicWorkService : Service { + private val delegator = WorkerDelegator() + private val started = AtomicBoolean(false) + + override fun start() { + if (!started.getAndSet(true)) { + delegator.run() + } + } + + fun enqueue(runnable: Runnable) { + delegator.enqueue(runnable) + } + + private class WorkerDelegator : Runnable { + companion object { + private const val SECONDS_TO_KILL_IDLE_THREADS = 30L + private const val SECONDS_FOR_NEXT_LOOP = 10L + private const val MAX_AMOUNT_OF_WORKER_THREADS = 1 + private const val NUMBER_OF_PERMANENT_WORKER_THREADS = 0 + } + + private val queue = ConcurrentLinkedQueue() + private val handler = Handler(Looper.getMainLooper()) + private val executor = + ThreadPoolExecutor( + NUMBER_OF_PERMANENT_WORKER_THREADS, + MAX_AMOUNT_OF_WORKER_THREADS, + SECONDS_TO_KILL_IDLE_THREADS, + TimeUnit.SECONDS, + LinkedBlockingQueue(), + ) + + fun enqueue(runnable: Runnable) { + queue.add(runnable) + } + + override fun run() { + delegateToWorkerThread() + scheduleNextLookUp() + } + + private fun delegateToWorkerThread() { + while (queue.isNotEmpty()) { + executor.execute(queue.poll()) + } + } + + private fun scheduleNextLookUp() { + handler.postDelayed(this, TimeUnit.SECONDS.toMillis(SECONDS_FOR_NEXT_LOOP)) + } + } +} diff --git a/instrumentation/src/main/java/io/opentelemetry/android/internal/tools/time/SystemTime.kt b/instrumentation/src/main/java/io/opentelemetry/android/internal/tools/time/SystemTime.kt new file mode 100644 index 000000000..d5a76164f --- /dev/null +++ b/instrumentation/src/main/java/io/opentelemetry/android/internal/tools/time/SystemTime.kt @@ -0,0 +1,34 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.android.internal.tools.time + +/** + * Utility to be able to mock the current system time for testing purposes. + * + *

This class is internal and not for public use. Its APIs are unstable and can change at any + * time. + */ +internal interface SystemTime { + companion object { + private var instance: SystemTime = DefaultSystemTime() + + fun get(): SystemTime { + return instance + } + + fun setForTest(instance: SystemTime) { + this.instance = instance + } + } + + fun getCurrentTimeMillis(): Long + + class DefaultSystemTime : SystemTime { + override fun getCurrentTimeMillis(): Long { + return System.currentTimeMillis() + } + } +} diff --git a/instrumentation/src/test/java/io/opentelemetry/android/OpenTelemetryRumBuilderTest.java b/instrumentation/src/test/java/io/opentelemetry/android/OpenTelemetryRumBuilderTest.java index af8540c8b..3cfcea795 100644 --- a/instrumentation/src/test/java/io/opentelemetry/android/OpenTelemetryRumBuilderTest.java +++ b/instrumentation/src/test/java/io/opentelemetry/android/OpenTelemetryRumBuilderTest.java @@ -22,8 +22,8 @@ import android.app.Activity; import android.app.Application; import androidx.annotation.NonNull; -import io.opentelemetry.android.config.DiskBufferingConfiguration; import io.opentelemetry.android.config.OtelRumConfig; +import io.opentelemetry.android.features.diskbuffering.DiskBufferingConfiguration; import io.opentelemetry.android.instrumentation.ApplicationStateListener; import io.opentelemetry.android.internal.services.CacheStorageService; import io.opentelemetry.android.internal.services.PreferencesService; diff --git a/instrumentation/src/test/java/io/opentelemetry/android/features/diskbuffering/scheduler/DefaultExportScheduleHandlerTest.kt b/instrumentation/src/test/java/io/opentelemetry/android/features/diskbuffering/scheduler/DefaultExportScheduleHandlerTest.kt new file mode 100644 index 000000000..b0000c54f --- /dev/null +++ b/instrumentation/src/test/java/io/opentelemetry/android/features/diskbuffering/scheduler/DefaultExportScheduleHandlerTest.kt @@ -0,0 +1,60 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.android.features.diskbuffering.scheduler + +import io.mockk.Runs +import io.mockk.clearAllMocks +import io.mockk.every +import io.mockk.just +import io.mockk.mockk +import io.mockk.slot +import io.mockk.verify +import io.opentelemetry.android.internal.services.ServiceManager +import io.opentelemetry.android.internal.services.periodicwork.PeriodicWorkService +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test + +class DefaultExportScheduleHandlerTest { + private lateinit var handler: DefaultExportScheduleHandler + + @BeforeEach + fun setUp() { + handler = DefaultExportScheduleHandler(DefaultExportScheduler()) + } + + @Test + fun `Start scheduler once when enabled`() { + val periodicWorkService = createMock() + val captor = slot() + + // Calling enable the first time (should work) + handler.enable() + verify { + periodicWorkService.enqueue(capture(captor)) + } + assertThat(captor.captured).isInstanceOf(DefaultExportScheduler::class.java) + clearAllMocks() + + // Calling enable a second time (should not work) + handler.enable() + verify(exactly = 0) { + periodicWorkService.enqueue(any()) + } + } + + private fun createMock(): PeriodicWorkService { + val periodicWorkService = mockk() + val manager = mockk() + every { + manager.getService(PeriodicWorkService::class.java) + }.returns(periodicWorkService) + every { periodicWorkService.enqueue(any()) } just Runs + ServiceManager.setForTest(manager) + + return periodicWorkService + } +} diff --git a/instrumentation/src/test/java/io/opentelemetry/android/features/diskbuffering/scheduler/DefaultExportSchedulerTest.kt b/instrumentation/src/test/java/io/opentelemetry/android/features/diskbuffering/scheduler/DefaultExportSchedulerTest.kt new file mode 100644 index 000000000..d9fe23169 --- /dev/null +++ b/instrumentation/src/test/java/io/opentelemetry/android/features/diskbuffering/scheduler/DefaultExportSchedulerTest.kt @@ -0,0 +1,29 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.android.features.diskbuffering.scheduler + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import java.util.concurrent.TimeUnit + +class DefaultExportSchedulerTest { + private lateinit var scheduler: DefaultExportScheduler + + @BeforeEach + fun setUp() { + scheduler = DefaultExportScheduler() + } + + @Test + fun `Verify minimum delay`() { + assertThat(scheduler.minimumDelayUntilNextRunInMillis()).isEqualTo( + TimeUnit.SECONDS.toMillis( + 10, + ), + ) + } +} diff --git a/instrumentation/src/test/java/io/opentelemetry/android/internal/features/persistence/DiskManagerTest.java b/instrumentation/src/test/java/io/opentelemetry/android/internal/features/persistence/DiskManagerTest.java index 70af58045..baeac5463 100644 --- a/instrumentation/src/test/java/io/opentelemetry/android/internal/features/persistence/DiskManagerTest.java +++ b/instrumentation/src/test/java/io/opentelemetry/android/internal/features/persistence/DiskManagerTest.java @@ -13,7 +13,7 @@ import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.verifyNoMoreInteractions; -import io.opentelemetry.android.config.DiskBufferingConfiguration; +import io.opentelemetry.android.features.diskbuffering.DiskBufferingConfiguration; import io.opentelemetry.android.internal.services.CacheStorageService; import io.opentelemetry.android.internal.services.PreferencesService; import io.opentelemetry.android.internal.services.ServiceManager; diff --git a/instrumentation/src/test/java/io/opentelemetry/android/internal/services/periodicwork/PeriodicRunnableTest.kt b/instrumentation/src/test/java/io/opentelemetry/android/internal/services/periodicwork/PeriodicRunnableTest.kt new file mode 100644 index 000000000..fe6316cbd --- /dev/null +++ b/instrumentation/src/test/java/io/opentelemetry/android/internal/services/periodicwork/PeriodicRunnableTest.kt @@ -0,0 +1,139 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.android.internal.services.periodicwork + +import io.mockk.Runs +import io.mockk.every +import io.mockk.just +import io.mockk.mockk +import io.mockk.verify +import io.opentelemetry.android.internal.services.Service +import io.opentelemetry.android.internal.services.ServiceManager +import io.opentelemetry.android.internal.tools.time.SystemTime +import org.assertj.core.api.Assertions.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class PeriodicRunnableTest { + private lateinit var periodicWorkService: PeriodicWorkService + private lateinit var testSystemTime: TestSystemTime + + @Before + fun setUp() { + periodicWorkService = createPeriodicWorkServiceMock() + mockServiceManager(periodicWorkService) + testSystemTime = TestSystemTime() + SystemTime.setForTest(testSystemTime) + } + + @Test + fun `Run the first time right away`() { + val minimumDelayInMillis = 1000L + val runnable = TestRunnable(minimumDelayInMillis) + + runnable.run() + + assertThat(runnable.timesRun).isEqualTo(1) + } + + @Test + fun `Wait minimum delay time before running again`() { + val minimumDelayInMillis = 10_000L + val runnable = TestRunnable(minimumDelayInMillis) + + runnable.run() + assertThat(runnable.timesRun).isEqualTo(1) + + // Try again right away (should not work) + runnable.run() + assertThat(runnable.timesRun).isEqualTo(1) + + // Wait for minimum delay + testSystemTime.advanceTimeByMillis(testSystemTime.getCurrentTimeMillis() + minimumDelayInMillis) + + // Try again after the delay (should work) + runnable.run() + assertThat(runnable.timesRun).isEqualTo(2) + } + + @Test + fun `When needed to run again, enqueue for next loop`() { + val runnable = TestRunnable(1000) + + runnable.run() + + assertThat(runnable.timesRun).isEqualTo(1) + verify { + periodicWorkService.enqueue(runnable) + } + } + + @Test + fun `When no need to run again, do not enqueue for next loop`() { + val runnable = TestRunnable(1000) + runnable.stopAfterRun = true + + runnable.run() + + assertThat(runnable.timesRun).isEqualTo(1) + verify(exactly = 0) { + periodicWorkService.enqueue(runnable) + } + } + + private fun createPeriodicWorkServiceMock(): PeriodicWorkService { + val periodicWorkService = mockk() + every { periodicWorkService.enqueue(any()) } just Runs + + return periodicWorkService + } + + private fun mockServiceManager(vararg services: Service) { + val manager = mockk() + services.forEach { service -> + every { manager.getService(service.javaClass) }.returns( + service, + ) + } + ServiceManager.setForTest(manager) + } + + private class TestRunnable(val minimumDelayInMillis: Long) : PeriodicRunnable() { + var timesRun = 0 + var stopAfterRun = false + private var stopRunning = false + + override fun onRun() { + timesRun++ + if (stopAfterRun) { + stopRunning = true + } + } + + override fun shouldStopRunning(): Boolean { + return stopRunning + } + + override fun minimumDelayUntilNextRunInMillis(): Long { + return minimumDelayInMillis + } + } + + private class TestSystemTime : SystemTime { + var currentTime = 1000L + + override fun getCurrentTimeMillis(): Long { + return currentTime + } + + fun advanceTimeByMillis(millis: Long) { + currentTime += millis + } + } +} diff --git a/instrumentation/src/test/java/io/opentelemetry/android/internal/services/periodicwork/PeriodicWorkServiceTest.kt b/instrumentation/src/test/java/io/opentelemetry/android/internal/services/periodicwork/PeriodicWorkServiceTest.kt new file mode 100644 index 000000000..9f62000f6 --- /dev/null +++ b/instrumentation/src/test/java/io/opentelemetry/android/internal/services/periodicwork/PeriodicWorkServiceTest.kt @@ -0,0 +1,128 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.android.internal.services.periodicwork + +import org.assertj.core.api.Assertions.assertThat +import org.junit.Assert.fail +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.shadows.ShadowLooper +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit + +@RunWith(RobolectricTestRunner::class) +class PeriodicWorkServiceTest { + companion object { + private const val DELAY_BETWEEN_EXECUTIONS_IN_SECONDS = 10L + } + + private lateinit var service: PeriodicWorkService + + @Before + fun setUp() { + service = PeriodicWorkService() + } + + @Test + fun `Execute enqueued work on start`() { + val numberOfTasks = 5 + val latch = CountDownLatch(numberOfTasks) + val threadIds = mutableSetOf() + repeat(numberOfTasks) { + service.enqueue { + threadIds.add(Thread.currentThread().id) + latch.countDown() + } + } + + service.start() + latch.await() + + // All ran in a single worker thread + assertThat(threadIds.size).isEqualTo(1) + + // The worker thread is not the same as the main thread + assertThat(threadIds.first()).isNotEqualTo(Thread.currentThread().id) + } + + @Test + fun `Start only once`() { + val latch = CountDownLatch(1) + + // First start (should work) + service.enqueue { + latch.countDown() + } + service.start() + latch.await() + + // Trying to re-start (should not work) + service.enqueue { + fail("Must not execute this right away.") + } + service.start() + + Thread.sleep(200) // Giving some time for the test to fail. + } + + @Test + fun `Check for pending work after a delay`() { + val firstRunLatch = CountDownLatch(1) + val secondRunLatch = CountDownLatch(1) + var secondRunExecuted = false + + // First run right away + service.enqueue { + firstRunLatch.countDown() + } + service.start() + service.enqueue { + secondRunExecuted = true + secondRunLatch.countDown() + } + firstRunLatch.await() + assertThat(secondRunExecuted).isFalse() + + // Second run after delay + fastForwardBySeconds(DELAY_BETWEEN_EXECUTIONS_IN_SECONDS) + secondRunLatch.await(1, TimeUnit.SECONDS) + assertThat(secondRunExecuted).isTrue() + } + + @Test + fun `Remove delegated work from further executions`() { + val firstRunLatch = CountDownLatch(1) + val secondRunLatch = CountDownLatch(1) + var timesExecutedFirstWork = 0 + var timesExecutedSecondWork = 0 + + // First run right away + service.enqueue { + timesExecutedFirstWork++ + firstRunLatch.countDown() + } + service.start() + service.enqueue { + timesExecutedSecondWork++ + secondRunLatch.countDown() + } + firstRunLatch.await() + assertThat(timesExecutedFirstWork).isEqualTo(1) + assertThat(timesExecutedSecondWork).isEqualTo(0) + + // Second run after delay + fastForwardBySeconds(DELAY_BETWEEN_EXECUTIONS_IN_SECONDS) + secondRunLatch.await(1, TimeUnit.SECONDS) + assertThat(timesExecutedFirstWork).isEqualTo(1) + assertThat(timesExecutedSecondWork).isEqualTo(1) + } + + private fun fastForwardBySeconds(seconds: Long) { + ShadowLooper.idleMainLooper(seconds, TimeUnit.SECONDS) + } +}