Skip to content

Commit

Permalink
rework the InitialContentImporter to not load bundled translations un…
Browse files Browse the repository at this point in the history
…til tools and languages are loaded
  • Loading branch information
frett committed May 8, 2024
1 parent 053674a commit 04f3046
Show file tree
Hide file tree
Showing 3 changed files with 192 additions and 40 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,38 @@ package org.cru.godtools.init.content

import javax.inject.Inject
import javax.inject.Singleton
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import org.cru.godtools.init.content.task.Tasks

@Singleton
class InitialContentImporter @Inject internal constructor(tasks: Tasks) {
private val coroutineScope = CoroutineScope(Dispatchers.IO)
class InitialContentImporter internal constructor(tasks: Tasks, dispatcher: CoroutineDispatcher) {
@Inject
internal constructor(tasks: Tasks) : this(tasks, Dispatchers.IO)

private val coroutineScope = CoroutineScope(dispatcher)

init {
coroutineScope.launch {
launch { tasks.loadBundledLanguages() }
val tools = async { tasks.loadBundledTools() }
val languages = launch { tasks.loadBundledLanguages() }

tasks.loadBundledResources()
launch { tasks.initFavoriteTools() }
launch { tasks.importBundledAttachments() }
launch { tasks.importBundledTranslations() }
launch {
tools.join()
tasks.initFavoriteTools()
}
launch {
tasks.loadBundledAttachments(tools.await())
tasks.importBundledAttachments()
}
launch {
languages.join()
tasks.loadBundledTranslations(tools.await())
tasks.importBundledTranslations()
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,17 +62,22 @@ internal class Tasks @Inject constructor(
// endregion Language Initial Content Tasks

// region Tool Initial Content Tasks
suspend fun loadBundledResources() = withContext(Dispatchers.IO) {
suspend fun loadBundledTools(): List<Tool> {
// short-circuit if we already have any resources loaded
if (toolsRepository.getAllTools().isNotEmpty()) return@withContext

bundledTools.let { resources ->
toolsRepository.storeInitialTools(resources)
translationsRepository.storeInitialTranslations(
resources.flatMap { it.translations.orEmpty().filter { it.isValid } }
)
attachmentsRepository.storeInitialAttachments(resources.flatMap { it.attachments.orEmpty() })
}
if (toolsRepository.getAllTools().isNotEmpty()) return emptyList()

return readBundledTools()
.also { toolsRepository.storeInitialTools(it) }
}

suspend fun loadBundledAttachments(tools: List<Tool>) {
attachmentsRepository.storeInitialAttachments(tools.flatMap { it.attachments.orEmpty() })
}

suspend fun loadBundledTranslations(tools: List<Tool>) {
translationsRepository.storeInitialTranslations(
tools.flatMap { it.translations.orEmpty().filter { it.isValid } }
)
}

suspend fun initFavoriteTools() {
Expand All @@ -82,7 +87,7 @@ internal class Tasks @Inject constructor(

coroutineScope {
val preferred = async {
bundledTools.sortedBy { it.initialFavoritesPriority ?: Int.MAX_VALUE }.mapNotNull { it.code }
readBundledTools().sortedBy { it.initialFavoritesPriority ?: Int.MAX_VALUE }.mapNotNull { it.code }
}
val available = translationsRepository.getTranslationsForLanguages(listOf(settings.appLanguage))
.mapNotNullTo(mutableSetOf()) { it.toolCode }
Expand All @@ -97,8 +102,8 @@ internal class Tasks @Inject constructor(
lastSyncTimeRepository.updateLastSyncTime(SYNC_TIME_DEFAULT_TOOLS)
}

private val bundledTools: List<Tool>
get() = try {
private suspend fun readBundledTools(): List<Tool> = withContext(Dispatchers.IO) {
try {
context.assets.open("tools.json").reader().use { it.readText() }
.let { jsonApiConverter.fromJson(it, Tool::class.java) }
.data
Expand All @@ -108,6 +113,7 @@ internal class Tasks @Inject constructor(
Timber.tag(TAG).e(e, "Error parsing bundled tools")
emptyList()
}
}
// endregion Tool Initial Content Tasks

suspend fun importBundledAttachments() = withContext(Dispatchers.IO) {
Expand All @@ -118,7 +124,7 @@ internal class Tasks @Inject constructor(
attachmentsRepository.getAttachments()
.filter { !it.isDownloaded && it.localFilename in files }
.forEach { attachment ->
launch(Dispatchers.IO) {
launch {
context.assets.open("attachments/${attachment.localFilename}").use {
downloadManager.importAttachment(attachment.id, data = it)
}
Expand All @@ -129,23 +135,24 @@ internal class Tasks @Inject constructor(
}
}

suspend fun importBundledTranslations() = try {
withContext(Dispatchers.IO) {
context.assets.list("translations")?.forEach { file ->
launch {
// load the translation unless it's downloaded already
val id = file.substring(0, file.lastIndexOf('.')).toLongOrNull()
val translation = id?.let { translationsRepository.findTranslation(id) }
?.takeUnless { it.isDownloaded } ?: return@launch

// short-circuit if a newer translation is already downloaded
val toolCode = translation.toolCode ?: return@launch
val languageCode = translation.languageCode
val latestTranslation =
translationsRepository.findLatestTranslation(toolCode, languageCode, downloadedOnly = true)
if (latestTranslation != null && latestTranslation.version >= translation.version) return@launch

withContext(Dispatchers.IO) {
suspend fun importBundledTranslations() {
try {
withContext(Dispatchers.IO) {
context.assets.list("translations")?.forEach { file ->
launch {
// load the translation unless it's downloaded already
val id = file.substring(0, file.lastIndexOf('.')).toLongOrNull()
val translation = id?.let { translationsRepository.findTranslation(id) }
?.takeUnless { it.isDownloaded } ?: return@launch

// short-circuit if a newer translation is already downloaded
val toolCode = translation.toolCode ?: return@launch
val languageCode = translation.languageCode
val latestTranslation =
translationsRepository.findLatestTranslation(toolCode, languageCode, downloadedOnly = true)
if (latestTranslation != null && latestTranslation.version >= translation.version) return@launch

// actually open and import the translation
try {
context.assets.open("translations/$file")
.use { downloadManager.importTranslation(translation, it, -1) }
Expand All @@ -162,8 +169,8 @@ internal class Tasks @Inject constructor(
}
}
}
} catch (e: Exception) {
Timber.tag(TAG).e(e, "Error importing bundled translations")
}
} catch (e: Exception) {
Timber.tag(TAG).e(e, "Error importing bundled translations")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
package org.cru.godtools.init.content

import io.mockk.Runs
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.coVerifyAll
import io.mockk.coVerifyOrder
import io.mockk.excludeRecords
import io.mockk.just
import io.mockk.mockk
import kotlin.test.Test
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
import org.cru.godtools.init.content.task.Tasks

@OptIn(ExperimentalCoroutinesApi::class)
class InitialContentImporterTest {
private val toolsSemaphore = Semaphore(1, 1)
private val languagesSemaphore = Semaphore(1, 1)

private val tasks: Tasks = mockk {
coEvery { loadBundledTools() } coAnswers {
toolsSemaphore.acquire()
emptyList()
}
coEvery { loadBundledLanguages() } coAnswers { languagesSemaphore.acquire() }
coEvery { loadBundledAttachments(any()) } just Runs
coEvery { loadBundledTranslations(any()) } just Runs
coEvery { initFavoriteTools() } just Runs
coEvery { importBundledAttachments() } just Runs
coEvery { importBundledTranslations() } just Runs

excludeRecords { this@mockk.toString() }
}

@Test
fun `Verify All Tasks run`() = runTest {
toolsSemaphore.release()
languagesSemaphore.release()
InitialContentImporter(tasks, UnconfinedTestDispatcher(testScheduler))

coVerifyAll {
tasks.loadBundledLanguages()
tasks.loadBundledTools()
tasks.initFavoriteTools()
tasks.loadBundledAttachments(any())
tasks.loadBundledTranslations(any())
tasks.importBundledAttachments()
tasks.importBundledTranslations()
}
}

@Test
fun `Favorite Tools - Dependent on Tools being loaded`() = runTest {
InitialContentImporter(tasks, UnconfinedTestDispatcher(testScheduler))

coVerify { tasks.loadBundledTools() }
coVerify(exactly = 0) { tasks.initFavoriteTools() }

toolsSemaphore.release()
coVerifyOrder {
tasks.loadBundledTools()
tasks.initFavoriteTools()
}
}

@Test
fun `Bundled Attachments - Dependent on Tools being loaded`() = runTest {
InitialContentImporter(tasks, UnconfinedTestDispatcher(testScheduler))

coVerify { tasks.loadBundledTools() }
coVerify(exactly = 0) {
tasks.loadBundledAttachments(any())
tasks.importBundledAttachments()
}

toolsSemaphore.release()
coVerifyOrder {
tasks.loadBundledTools()
tasks.loadBundledAttachments(any())
tasks.importBundledAttachments()
}
}

@Test
fun `Bundled Translations - Dependent on Tools being loaded`() = runTest {
languagesSemaphore.release()
InitialContentImporter(tasks, UnconfinedTestDispatcher(testScheduler))

coVerify {
tasks.loadBundledTools()
tasks.loadBundledLanguages()
}
coVerify(exactly = 0) {
tasks.loadBundledTranslations(any())
tasks.importBundledTranslations()
}

toolsSemaphore.release()
coVerifyOrder {
tasks.loadBundledTools()
tasks.loadBundledTranslations(any())
tasks.importBundledTranslations()
}
}

@Test
fun `Bundled Translations - Dependent on Languages being loaded`() = runTest {
toolsSemaphore.release()
InitialContentImporter(tasks, UnconfinedTestDispatcher(testScheduler))

coVerify {
tasks.loadBundledTools()
tasks.loadBundledLanguages()
}
coVerify(exactly = 0) {
tasks.loadBundledTranslations(any())
tasks.importBundledTranslations()
}

languagesSemaphore.release()
coVerifyOrder {
tasks.loadBundledTools()
tasks.loadBundledTranslations(any())
tasks.importBundledTranslations()
}
}
}

0 comments on commit 04f3046

Please sign in to comment.