Skip to content

Instantly share code, notes, and snippets.

@Sarthak2601
Last active September 1, 2020 18:55
Show Gist options
  • Save Sarthak2601/ac1fa692831d5a3679ae2fc0f1915a90 to your computer and use it in GitHub Desktop.
Save Sarthak2601/ac1fa692831d5a3679ae2fc0f1915a90 to your computer and use it in GitHub Desktop.
Work Manager
package org.oppia.domain.oppialogger.loguploader
import android.content.Context
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext
import org.oppia.app.model.EventLog
import org.oppia.app.model.ExceptionLog
import org.oppia.app.model.OppiaEventLogs
import org.oppia.app.model.OppiaExceptionLogs
import org.oppia.domain.oppialogger.analytics.AnalyticsController
import org.oppia.domain.oppialogger.exceptions.ExceptionsController
import org.oppia.domain.oppialogger.exceptions.toException
import org.oppia.util.logging.ConsoleLogger
import org.oppia.util.logging.EventLogger
import org.oppia.util.logging.ExceptionLogger
import org.oppia.util.threading.BackgroundDispatcher
import javax.inject.Inject
/** Worker class that extracts log reports from the cache store and logs them to the remote service. */
class LogUploadWorker private constructor(
context: Context,
params: WorkerParameters,
private val analyticsController: AnalyticsController,
private val exceptionsController: ExceptionsController,
private val exceptionLogger: ExceptionLogger,
private val eventLogger: EventLogger,
private val consoleLogger: ConsoleLogger,
@BackgroundDispatcher private val backgroundDispatcher: CoroutineDispatcher
) : CoroutineWorker(context, params) {
companion object {
const val WORKER_CASE_KEY = "worker_case_key"
const val TAG = "LogUploadWorker.tag"
const val EVENT_WORKER = "event_worker"
const val EXCEPTION_WORKER = "exception_worker"
}
override val coroutineContext = backgroundDispatcher
override suspend fun doWork(): Result {
return when (inputData.getString(WORKER_CASE_KEY)) {
EVENT_WORKER -> {
withContext(backgroundDispatcher) { uploadEvents() }
}
EXCEPTION_WORKER -> {
withContext(backgroundDispatcher) { uploadExceptions() }
}
else -> Result.failure()
}
}
/** Extracts exception logs from the cache store and logs them to the remote service. */
private suspend fun uploadExceptions(): Result {
return try {
val exceptionLogs =
exceptionsController.getExceptionLogStore().retrieveData()
.getOrDefault(OppiaExceptionLogs.getDefaultInstance()).exceptionLogList
exceptionLogs?.let {
for (exceptionLog in it) {
exceptionLogger.logException(exceptionLog.toException())
it.remove(exceptionLog)
}
}
Result.success()
} catch (e: Exception) {
consoleLogger.e(TAG, e.toString(), e)
System.err.println(e)
Result.failure()
}
}
/** Extracts event logs from the cache store and logs them to the remote service. */
private suspend fun uploadEvents(): Result {
return try {
val eventLogs =
analyticsController.getEventLogStore().retrieveData()
.getOrDefault(OppiaEventLogs.getDefaultInstance()).eventLogList
eventLogs?.let {
for (eventLog in it) {
eventLogger.logEvent(eventLog)
it.remove(eventLog)
}
}
Result.success()
} catch (e: Exception) {
consoleLogger.e(TAG, e.toString(), e)
Result.failure()
}
}
/** Creates an instance of [LogUploadWorker] by properly injecting dependencies. */
class FactoryLogUpload @Inject constructor(
private val analyticsController: AnalyticsController,
private val exceptionsController: ExceptionsController,
private val exceptionLogger: ExceptionLogger,
private val eventLogger: EventLogger,
private val consoleLogger: ConsoleLogger,
@BackgroundDispatcher private val backgroundDispatcher: CoroutineDispatcher
) : LogUploadChildWorkerFactory {
override fun create(context: Context, params: WorkerParameters): CoroutineWorker {
return LogUploadWorker(
context,
params,
analyticsController,
exceptionsController,
exceptionLogger,
eventLogger,
consoleLogger,
backgroundDispatcher
)
}
}
}
package org.oppia.domain.oppialogger.loguploader
import android.app.Application
import android.content.Context
import android.os.Looper.getMainLooper
import androidx.lifecycle.Observer
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import androidx.work.Configuration
import androidx.work.Data
import androidx.work.ListenableWorker
import androidx.work.OneTimeWorkRequest
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkInfo
import androidx.work.WorkManager
import androidx.work.testing.SynchronousExecutor
import androidx.work.testing.TestListenableWorkerBuilder
import androidx.work.testing.TestWorkerBuilder
import androidx.work.testing.WorkManagerTestInitHelper
import com.google.common.truth.Truth.assertThat
import dagger.BindsInstance
import dagger.Component
import dagger.Module
import dagger.Provides
import kotlinx.coroutines.CoroutineScope
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor
import org.mockito.Captor
import org.mockito.Mock
import org.mockito.Mockito
import org.mockito.junit.MockitoJUnit
import org.mockito.junit.MockitoRule
import org.oppia.app.model.EventLog
import org.oppia.app.model.OppiaEventLogs
import org.oppia.app.model.OppiaExceptionLogs
import org.oppia.domain.oppialogger.EventLogStorageCacheSize
import org.oppia.domain.oppialogger.ExceptionLogStorageCacheSize
import org.oppia.domain.oppialogger.OppiaLogger
import org.oppia.domain.oppialogger.analytics.AnalyticsController
import org.oppia.domain.oppialogger.analytics.TEST_TIMESTAMP
import org.oppia.domain.oppialogger.analytics.TEST_TOPIC_ID
import org.oppia.domain.oppialogger.exceptions.ExceptionsController
import org.oppia.testing.BackgroundTestDispatcher
import org.oppia.testing.FakeEventLogger
import org.oppia.testing.FakeExceptionLogger
import org.oppia.testing.TestCoroutineDispatcher
import org.oppia.testing.TestCoroutineDispatchers
import org.oppia.testing.TestDispatcherModule
import org.oppia.testing.TestLogReportingModule
import org.oppia.util.data.AsyncResult
import org.oppia.util.data.DataProvider
import org.oppia.util.data.DataProviders
import org.oppia.util.logging.EnableConsoleLog
import org.oppia.util.logging.EnableFileLog
import org.oppia.util.logging.GlobalLogLevel
import org.oppia.util.logging.LogLevel
import org.oppia.util.networking.NetworkConnectionUtil
import org.robolectric.Shadows.shadowOf
import org.robolectric.annotation.Config
import org.robolectric.annotation.LooperMode
import javax.inject.Inject
import javax.inject.Singleton
@RunWith(AndroidJUnit4::class)
@LooperMode(LooperMode.Mode.PAUSED)
@Config(manifest = Config.NONE)
class LogUploadWorkerTest {
@Rule
@JvmField
val mockitoRule: MockitoRule = MockitoJUnit.rule()
@Inject
lateinit var networkConnectionUtil: NetworkConnectionUtil
@Inject
lateinit var fakeEventLogger: FakeEventLogger
@Inject
lateinit var fakeExceptionLogger: FakeExceptionLogger
@Inject
lateinit var oppiaLogger: OppiaLogger
@Inject
lateinit var analyticsController: AnalyticsController
@Inject
lateinit var exceptionsController: ExceptionsController
@Inject
lateinit var logUploadWorkerFactory: LogUploadWorkerFactory
@Inject
lateinit var dataProviders: DataProviders
@Inject
lateinit var testCoroutineDispatchers: TestCoroutineDispatchers
private lateinit var context: Context
@Mock
lateinit var mockOppiaEventLogsObserver: Observer<AsyncResult<OppiaEventLogs>>
@Captor
lateinit var oppiaEventLogsResultCaptor: ArgumentCaptor<AsyncResult<OppiaEventLogs>>
@Before
fun setUp() {
networkConnectionUtil = NetworkConnectionUtil(ApplicationProvider.getApplicationContext())
setUpTestApplicationComponent()
context = InstrumentationRegistry.getInstrumentation().targetContext
val config = Configuration.Builder()
.setExecutor(SynchronousExecutor())
.setWorkerFactory(logUploadWorkerFactory)
.build()
WorkManagerTestInitHelper.initializeTestWorkManager(context, config)
}
@Test
fun testWorker_logEvent_withoutNetwork_enqueueRequest_verifySuccess() {
val eventLogTopicContext = EventLog.newBuilder()
.setActionName(EventLog.EventAction.EVENT_ACTION_UNSPECIFIED)
.setContext(
EventLog.Context.newBuilder()
.setTopicContext(
EventLog.TopicContext.newBuilder()
.setTopicId(TEST_TOPIC_ID)
.build()
)
.build()
)
.setPriority(EventLog.Priority.ESSENTIAL)
.setTimestamp(TEST_TIMESTAMP)
.build()
networkConnectionUtil.setCurrentConnectionStatus(NetworkConnectionUtil.ConnectionStatus.NONE)
analyticsController.logTransitionEvent(
eventLogTopicContext.timestamp,
eventLogTopicContext.actionName,
oppiaLogger.createTopicContext(TEST_TOPIC_ID)
)
val eventLogs = dataProviders.convertToLiveData(analyticsController.getEventLogStore())
eventLogs.observeForever(mockOppiaEventLogsObserver)
testCoroutineDispatchers.advanceUntilIdle()
Mockito.verify(
mockOppiaEventLogsObserver,
Mockito.atLeastOnce()
).onChanged(oppiaEventLogsResultCaptor.capture())
val workManager = WorkManager.getInstance(ApplicationProvider.getApplicationContext())
val inputData = Data.Builder().putString(
LogUploadWorker.WORKER_CASE_KEY,
LogUploadWorker.EVENT_WORKER
).build()
val request: OneTimeWorkRequest = OneTimeWorkRequestBuilder<LogUploadWorker>()
.setInputData(inputData)
.build()
workManager.enqueue(request)
testCoroutineDispatchers.runCurrent()
val workInfo = workManager.getWorkInfoById(request.id)
assertThat(workInfo.get().state).isEqualTo(WorkInfo.State.SUCCEEDED)
}
@Test
fun testWorker_logException_withoutNetwork_enqueueRequest_verifySuccess() {
val exceptionThrown = Exception("TEST", Throwable("TEST THROWABLE"))
networkConnectionUtil.setCurrentConnectionStatus(NetworkConnectionUtil.ConnectionStatus.NONE)
exceptionsController.logNonFatalException(exceptionThrown, TEST_TIMESTAMP)
val workManager = WorkManager.getInstance(ApplicationProvider.getApplicationContext())
val inputData = Data.Builder().putString(
LogUploadWorker.WORKER_CASE_KEY,
LogUploadWorker.EXCEPTION_WORKER
).build()
val request: OneTimeWorkRequest = OneTimeWorkRequestBuilder<LogUploadWorker>()
.setInputData(inputData)
.build()
workManager.enqueue(request)
testCoroutineDispatchers.runCurrent()
val workInfo = workManager.getWorkInfoById(request.id)
assertThat(workInfo.get().state).isEqualTo(WorkInfo.State.SUCCEEDED)
}
private fun setUpTestApplicationComponent() {
DaggerLogUploadWorkerTest_TestApplicationComponent.builder()
.setApplication(ApplicationProvider.getApplicationContext())
.build()
.inject(this)
}
// TODO(#89): Move this to a common test application component.
@Module
class TestModule {
@Provides
@Singleton
fun provideContext(application: Application): Context {
return application
}
// TODO(#59): Either isolate these to their own shared test module, or use the real logging
// module in tests to avoid needing to specify these settings for tests.
@EnableConsoleLog
@Provides
fun provideEnableConsoleLog(): Boolean = true
@EnableFileLog
@Provides
fun provideEnableFileLog(): Boolean = false
@GlobalLogLevel
@Provides
fun provideGlobalLogLevel(): LogLevel = LogLevel.VERBOSE
}
@Module
class TestLogStorageModule {
@Provides
@EventLogStorageCacheSize
fun provideEventLogStorageCacheSize(): Int = 2
@Provides
@ExceptionLogStorageCacheSize
fun provideExceptionLogStorageSize(): Int = 2
}
// TODO(#89): Move this to a common test application component.
@Singleton
@Component(
modules = [
TestModule::class,
TestLogReportingModule::class,
TestLogStorageModule::class,
TestDispatcherModule::class,
LogUploadWorkerModule::class
]
)
interface TestApplicationComponent {
@Component.Builder
interface Builder {
@BindsInstance
fun setApplication(application: Application): Builder
fun build(): TestApplicationComponent
}
fun inject(logUploadWorkerTest: LogUploadWorkerTest)
}
}
package org.oppia.domain.oppialogger.loguploader
import android.content.Context
import androidx.work.Constraints
import androidx.work.Data
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.NetworkType
import androidx.work.PeriodicWorkRequest
import androidx.work.WorkManager
import org.oppia.domain.oppialogger.ApplicationStartupListener
import java.util.concurrent.TimeUnit
import javax.inject.Inject
import javax.inject.Singleton
private const val OPPIA_EXCEPTION_WORK = "OPPIA_EXCEPTION_WORK_REQUEST"
private const val OPPIA_EVENT_WORK = "OPPIA_EVENT_WORK_REQUEST"
/** Enqueues unique periodic work requests for uploading events and exceptions to the remote service on application creation. */
@Singleton
class LogUploadWorkManagerInitializer @Inject constructor(
private val context: Context
) : ApplicationStartupListener {
private val logUploadWorkerConstraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.setRequiresBatteryNotLow(true)
.build()
override fun onCreate() {
val workManager = WorkManager.getInstance(context)
enqueueWorkRequestForEvents(workManager)
enqueueWorkRequestForExceptions(workManager)
}
/** Enqueues a unique periodic work request for uploading events to the remote service. */
private fun enqueueWorkRequestForEvents(workManager: WorkManager) {
val workerCase =
Data.Builder()
.putString(
LogUploadWorker.WORKER_CASE_KEY,
LogUploadWorker.EVENT_WORKER
)
.build()
val eventWorkRequest =
PeriodicWorkRequest
.Builder(LogUploadWorker::class.java, 6, TimeUnit.HOURS)
.setInputData(workerCase)
.setConstraints(logUploadWorkerConstraints)
.build()
workManager.enqueueUniquePeriodicWork(
OPPIA_EVENT_WORK,
ExistingPeriodicWorkPolicy.KEEP,
eventWorkRequest
)
}
/** Enqueues a unique periodic work request for uploading exceptions to the remote service. */
private fun enqueueWorkRequestForExceptions(workManager: WorkManager) {
val workerCase =
Data.Builder()
.putString(
LogUploadWorker.WORKER_CASE_KEY,
LogUploadWorker.EXCEPTION_WORKER
)
.build()
val exceptionWorkRequest =
PeriodicWorkRequest
.Builder(LogUploadWorker::class.java, 6, TimeUnit.HOURS)
.setInputData(workerCase)
.setConstraints(logUploadWorkerConstraints)
.build()
workManager.enqueueUniquePeriodicWork(
OPPIA_EXCEPTION_WORK,
ExistingPeriodicWorkPolicy.KEEP,
exceptionWorkRequest
)
}
/** Returns the worker constraints set for the log uploading work requests. */
fun getLogUploadWorkerConstraints(): Constraints = logUploadWorkerConstraints
}
package org.oppia.domain.oppialogger.loguploader
import android.app.Application
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import androidx.work.Configuration
import androidx.work.Constraints
import androidx.work.NetworkType
import androidx.work.testing.SynchronousExecutor
import androidx.work.testing.WorkManagerTestInitHelper
import com.google.common.truth.Truth.assertThat
import dagger.BindsInstance
import dagger.Component
import dagger.Module
import dagger.Provides
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.oppia.app.model.EventLog
import org.oppia.domain.oppialogger.EventLogStorageCacheSize
import org.oppia.domain.oppialogger.ExceptionLogStorageCacheSize
import org.oppia.domain.oppialogger.OppiaLogger
import org.oppia.domain.oppialogger.analytics.AnalyticsController
import org.oppia.domain.oppialogger.analytics.TEST_TIMESTAMP
import org.oppia.domain.oppialogger.analytics.TEST_TOPIC_ID
import org.oppia.domain.oppialogger.exceptions.ExceptionsController
import org.oppia.testing.FakeEventLogger
import org.oppia.testing.FakeExceptionLogger
import org.oppia.testing.TestDispatcherModule
import org.oppia.testing.TestLogReportingModule
import org.oppia.util.logging.EnableConsoleLog
import org.oppia.util.logging.EnableFileLog
import org.oppia.util.logging.GlobalLogLevel
import org.oppia.util.logging.LogLevel
import org.oppia.util.networking.NetworkConnectionUtil
import org.robolectric.annotation.Config
import javax.inject.Inject
import javax.inject.Singleton
@RunWith(AndroidJUnit4::class)
@Config(manifest = Config.NONE)
class LogUploadWorkManagerInitializerTest {
@Inject
lateinit var logUploadWorkerFactory: LogUploadWorkerFactory
@Inject
lateinit var logUploadWorkManagerInitializer: LogUploadWorkManagerInitializer
@Inject
lateinit var analyticsController: AnalyticsController
@Inject
lateinit var exceptionsController: ExceptionsController
@Inject
lateinit var networkConnectionUtil: NetworkConnectionUtil
@Inject
lateinit var fakeEventLogger: FakeEventLogger
@Inject
lateinit var fakeExceptionLogger: FakeExceptionLogger
@Inject
lateinit var oppiaLogger: OppiaLogger
private lateinit var context: Context
private val eventLogTopicContext = EventLog.newBuilder()
.setActionName(EventLog.EventAction.EVENT_ACTION_UNSPECIFIED)
.setContext(
EventLog.Context.newBuilder()
.setTopicContext(
EventLog.TopicContext.newBuilder()
.setTopicId(TEST_TOPIC_ID)
.build()
)
.build()
)
.setPriority(EventLog.Priority.ESSENTIAL)
.setTimestamp(TEST_TIMESTAMP)
.build()
private val exception = Exception("TEST")
@Before
fun setUp() {
networkConnectionUtil = NetworkConnectionUtil(ApplicationProvider.getApplicationContext())
setUpTestApplicationComponent()
context = InstrumentationRegistry.getInstrumentation().targetContext
val config = Configuration.Builder()
.setExecutor(SynchronousExecutor())
.setWorkerFactory(logUploadWorkerFactory)
.build()
WorkManagerTestInitHelper.initializeTestWorkManager(context, config)
}
@Test
fun testWorkRequest_logEvent_withoutNetwork_callOnCreate_verifyLogToRemoteService() {
networkConnectionUtil.setCurrentConnectionStatus(NetworkConnectionUtil.ConnectionStatus.NONE)
analyticsController.logTransitionEvent(
eventLogTopicContext.timestamp,
eventLogTopicContext.actionName,
oppiaLogger.createTopicContext(TEST_TOPIC_ID)
)
networkConnectionUtil.setCurrentConnectionStatus(NetworkConnectionUtil.ConnectionStatus.LOCAL)
setUp()
logUploadWorkManagerInitializer.onCreate()
val eventLog = fakeEventLogger.getMostRecentEvent()
assertThat(eventLog).isEqualTo(eventLogTopicContext)
}
@Test
fun testWorkRequest_logExcepton_withoutNetwork_callOnCreate_verifyLogToRemoteService() {
networkConnectionUtil.setCurrentConnectionStatus(NetworkConnectionUtil.ConnectionStatus.NONE)
exceptionsController.logNonFatalException(exception, TEST_TIMESTAMP)
networkConnectionUtil.setCurrentConnectionStatus(NetworkConnectionUtil.ConnectionStatus.LOCAL)
setUp()
logUploadWorkManagerInitializer.onCreate()
val exceptionLogged = fakeExceptionLogger.getMostRecentException()
assertThat(exceptionLogged).isEqualTo(exception)
}
@Test
fun testWorkRequest_logExceptionAndEvent_withoutNetwork_callOnCreate_verifyLogToRemoteService() {
networkConnectionUtil.setCurrentConnectionStatus(NetworkConnectionUtil.ConnectionStatus.NONE)
analyticsController.logTransitionEvent(
eventLogTopicContext.timestamp,
eventLogTopicContext.actionName,
oppiaLogger.createTopicContext(TEST_TOPIC_ID)
)
exceptionsController.logNonFatalException(exception, TEST_TIMESTAMP)
networkConnectionUtil.setCurrentConnectionStatus(NetworkConnectionUtil.ConnectionStatus.LOCAL)
setUp()
logUploadWorkManagerInitializer.onCreate()
val eventLog = fakeEventLogger.getMostRecentEvent()
val exceptionLogged = fakeExceptionLogger.getMostRecentException()
assertThat(eventLog).isEqualTo(eventLogTopicContext)
assertThat(exceptionLogged).isEqualTo(exception)
}
@Test
fun testWorkRequest_verifyWorkerConstraints() {
val workerConstraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.setRequiresBatteryNotLow(true)
.build()
val logUploadingWorkRequestConstraints =
logUploadWorkManagerInitializer.getLogUploadWorkerConstraints()
assertThat(logUploadingWorkRequestConstraints).isEqualTo(workerConstraints)
}
private fun setUpTestApplicationComponent() {
DaggerLogUploadWorkManagerInitializerTest_TestApplicationComponent.builder()
.setApplication(ApplicationProvider.getApplicationContext())
.build()
.inject(this)
}
// TODO(#89): Move this to a common test application component.
@Module
class TestModule {
@Provides
@Singleton
fun provideContext(application: Application): Context {
return application
}
// TODO(#59): Either isolate these to their own shared test module, or use the real logging
// module in tests to avoid needing to specify these settings for tests.
@EnableConsoleLog
@Provides
fun provideEnableConsoleLog(): Boolean = true
@EnableFileLog
@Provides
fun provideEnableFileLog(): Boolean = false
@GlobalLogLevel
@Provides
fun provideGlobalLogLevel(): LogLevel = LogLevel.VERBOSE
}
@Module
class TestLogStorageModule {
@Provides
@EventLogStorageCacheSize
fun provideEventLogStorageCacheSize(): Int = 2
@Provides
@ExceptionLogStorageCacheSize
fun provideExceptionLogStorageSize(): Int = 2
}
// TODO(#89): Move this to a common test application component.
@Singleton
@Component(
modules = [
TestModule::class,
TestLogReportingModule::class,
TestLogStorageModule::class,
TestDispatcherModule::class,
LogUploadWorkerModule::class
]
)
interface TestApplicationComponent {
@Component.Builder
interface Builder {
@BindsInstance
fun setApplication(application: Application): Builder
fun build(): TestApplicationComponent
}
fun inject(logUploadWorkRequestTest: LogUploadWorkManagerInitializerTest)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment