From ab16bcc15b0f606989cfede7eda1a8a9a5a5e126 Mon Sep 17 00:00:00 2001 From: Sergejs Luhmirins Date: Tue, 3 Dec 2024 18:40:29 +0200 Subject: [PATCH] MS-806 Basic worker info log --- .../TroubleshootingPagerAdapter.kt | 2 + .../workers/WorkerLogFragment.kt | 30 ++++++ .../workers/WorkerLogViewModel.kt | 52 +++++++++ .../workers/WorkerLogViewModelTest.kt | 101 ++++++++++++++++++ 4 files changed, 185 insertions(+) create mode 100644 feature/dashboard/src/main/java/com/simprints/feature/dashboard/settings/troubleshooting/workers/WorkerLogFragment.kt create mode 100644 feature/dashboard/src/main/java/com/simprints/feature/dashboard/settings/troubleshooting/workers/WorkerLogViewModel.kt create mode 100644 feature/dashboard/src/test/java/com/simprints/feature/dashboard/settings/troubleshooting/workers/WorkerLogViewModelTest.kt diff --git a/feature/dashboard/src/main/java/com/simprints/feature/dashboard/settings/troubleshooting/TroubleshootingPagerAdapter.kt b/feature/dashboard/src/main/java/com/simprints/feature/dashboard/settings/troubleshooting/TroubleshootingPagerAdapter.kt index 186ba46158..10c4c1b742 100644 --- a/feature/dashboard/src/main/java/com/simprints/feature/dashboard/settings/troubleshooting/TroubleshootingPagerAdapter.kt +++ b/feature/dashboard/src/main/java/com/simprints/feature/dashboard/settings/troubleshooting/TroubleshootingPagerAdapter.kt @@ -5,6 +5,7 @@ import androidx.fragment.app.FragmentActivity import androidx.viewpager2.adapter.FragmentStateAdapter import com.simprints.feature.dashboard.settings.troubleshooting.events.EventScopeLogFragment import com.simprints.feature.dashboard.settings.troubleshooting.overview.OverviewFragment +import com.simprints.feature.dashboard.settings.troubleshooting.workers.WorkerLogFragment import com.simprints.infra.uibase.annotations.ExcludedFromGeneratedTestCoverageReports @ExcludedFromGeneratedTestCoverageReports("UI code") @@ -23,5 +24,6 @@ internal class TroubleshootingPagerAdapter( Overview("Overview", { OverviewFragment() }), Events("Event scopes", { EventScopeLogFragment() }), + Workers("Worker log", { WorkerLogFragment() }), } } diff --git a/feature/dashboard/src/main/java/com/simprints/feature/dashboard/settings/troubleshooting/workers/WorkerLogFragment.kt b/feature/dashboard/src/main/java/com/simprints/feature/dashboard/settings/troubleshooting/workers/WorkerLogFragment.kt new file mode 100644 index 0000000000..4599eefe20 --- /dev/null +++ b/feature/dashboard/src/main/java/com/simprints/feature/dashboard/settings/troubleshooting/workers/WorkerLogFragment.kt @@ -0,0 +1,30 @@ +package com.simprints.feature.dashboard.settings.troubleshooting.workers + +import android.os.Bundle +import android.view.View +import androidx.core.view.isGone +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import com.simprints.feature.dashboard.R +import com.simprints.feature.dashboard.databinding.FragmentTroubleshootingListBinding +import com.simprints.feature.dashboard.settings.troubleshooting.adapter.TroubleshootingListAdapter +import com.simprints.infra.uibase.viewbinding.viewBinding +import dagger.hilt.android.AndroidEntryPoint +import kotlin.getValue + +@AndroidEntryPoint +class WorkerLogFragment : Fragment(R.layout.fragment_troubleshooting_list) { + + private val viewModel by viewModels() + private val binding by viewBinding(FragmentTroubleshootingListBinding::bind) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + viewModel.workers.observe(viewLifecycleOwner) { + binding.troubleshootingListProgress.isGone = it.isNotEmpty() + binding.troubleshootingList.adapter = TroubleshootingListAdapter(it) + } + viewModel.collectWorkerData() + } +} diff --git a/feature/dashboard/src/main/java/com/simprints/feature/dashboard/settings/troubleshooting/workers/WorkerLogViewModel.kt b/feature/dashboard/src/main/java/com/simprints/feature/dashboard/settings/troubleshooting/workers/WorkerLogViewModel.kt new file mode 100644 index 0000000000..14db2716ce --- /dev/null +++ b/feature/dashboard/src/main/java/com/simprints/feature/dashboard/settings/troubleshooting/workers/WorkerLogViewModel.kt @@ -0,0 +1,52 @@ +package com.simprints.feature.dashboard.settings.troubleshooting.workers + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import androidx.work.WorkInfo +import androidx.work.WorkManager +import androidx.work.WorkQuery +import com.simprints.feature.dashboard.settings.troubleshooting.adapter.TroubleshootingItemViewData +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import java.util.Date +import javax.inject.Inject + +@HiltViewModel +internal class WorkerLogViewModel @Inject constructor( + private val workManager: WorkManager, +) : ViewModel() { + + private val _workers = MutableLiveData>(emptyList()) + val workers: LiveData> + get() = _workers + + fun collectWorkerData() { + viewModelScope.launch { + workManager.getWorkInfosFlow(WorkQuery.fromStates(WorkInfo.State.entries)) + .collect { infos -> + infos.map { formatWorkInfo(it) } + .take(50) + .ifEmpty { listOf(TroubleshootingItemViewData("No data")) } + .let { _workers.postValue(it) } + } + } + } + + private fun formatWorkInfo(info: WorkInfo) = TroubleshootingItemViewData( + // One of the work info tags is worker's full class name + title = info.tags + .find { it.startsWith("com.simprints") } + ?.substringAfterLast(".") + ?: info.id.toString(), + subtitle = info.state.toString(), + body = if (info.state == WorkInfo.State.ENQUEUED) { + "ID: ${info.id}\nNext run: ${Date(info.nextScheduleTimeMillis)}" + } else { + "ID: ${info.id}\nOutput: ${info.outputData}" + } + + + ) +} diff --git a/feature/dashboard/src/test/java/com/simprints/feature/dashboard/settings/troubleshooting/workers/WorkerLogViewModelTest.kt b/feature/dashboard/src/test/java/com/simprints/feature/dashboard/settings/troubleshooting/workers/WorkerLogViewModelTest.kt new file mode 100644 index 0000000000..39b8c92939 --- /dev/null +++ b/feature/dashboard/src/test/java/com/simprints/feature/dashboard/settings/troubleshooting/workers/WorkerLogViewModelTest.kt @@ -0,0 +1,101 @@ +package com.simprints.feature.dashboard.settings.troubleshooting.workers + +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import androidx.work.WorkInfo +import androidx.work.WorkManager +import com.google.common.truth.Truth.assertThat +import com.jraska.livedata.test +import com.simprints.testtools.common.coroutines.TestCoroutineRule +import io.mockk.MockKAnnotations +import io.mockk.coEvery +import io.mockk.every +import io.mockk.impl.annotations.MockK +import io.mockk.mockk +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import java.util.UUID + +class WorkerLogViewModelTest { + + @get:Rule + val rule = InstantTaskExecutorRule() + + @get:Rule + val testCoroutineRule = TestCoroutineRule() + + @MockK + private lateinit var workManager: WorkManager + + private lateinit var viewModel: WorkerLogViewModel + + @Before + fun setUp() { + MockKAnnotations.init(this, relaxed = true) + + viewModel = WorkerLogViewModel( + workManager = workManager, + ) + } + + @Test + fun `sets list of scopes on request`() = runTest { + coEvery { workManager.getWorkInfosFlow(any()) } returns flowOf(listOf( + mockk(relaxed = true) { + every { id } returns UUID.fromString("c92d4da1-dc9a-4e25-9fcd-a9aca78a4cf3") + every { tags } returns setOf("com.simprints.Worker") + every { state } returns WorkInfo.State.SUCCEEDED + } + )) + + val workers = viewModel.workers.test() + viewModel.collectWorkerData() + + assertThat(workers.value()).isNotEmpty() + assertThat(workers.value().first().title).isEqualTo("Worker") + assertThat(workers.value().first().body).contains("Output") + } + + @Test + fun `sets list of scopes placeholder if no scopes`() = runTest { + coEvery { workManager.getWorkInfosFlow(any()) } returns flowOf(emptyList()) + + val workers = viewModel.workers.test() + viewModel.collectWorkerData() + + assertThat(workers.value()).isNotEmpty() + } + + @Test + fun `sets id to UUID if no tag`() = runTest { + coEvery { workManager.getWorkInfosFlow(any()) } returns flowOf(listOf( + mockk(relaxed = true) { + every { id } returns UUID.fromString("c92d4da1-dc9a-4e25-9fcd-a9aca78a4cf3") + every { tags } returns setOf("Worker") + } + )) + + val workers = viewModel.workers.test() + viewModel.collectWorkerData() + + assertThat(workers.value()).isNotEmpty() + assertThat(workers.value().first().title).isEqualTo("c92d4da1-dc9a-4e25-9fcd-a9aca78a4cf3") + } + + @Test + fun `sets takes next scheduled time if worked is enqueued`() = runTest { + coEvery { workManager.getWorkInfosFlow(any()) } returns flowOf(listOf( + mockk(relaxed = true) { + every { state } returns WorkInfo.State.ENQUEUED + } + )) + + val workers = viewModel.workers.test() + viewModel.collectWorkerData() + + assertThat(workers.value()).isNotEmpty() + assertThat(workers.value().first().body).contains("Next run:") + } +}