Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ class ChatViewModelCompactionTest {

private val configScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)

/** Every ViewModel a test made, cleared before Main is reset; see [clearAndAwait]. */
private val viewModels = mutableListOf<ChatViewModel>()

/** The history each model turn was sent with, in order. */
private val sentHistories = CopyOnWriteArrayList<List<LlmInferenceService.ChatMessage>>()

Expand Down Expand Up @@ -99,6 +102,7 @@ class ChatViewModelCompactionTest {

@After
fun tearDown() {
clearAndAwait(viewModels)
SharedServices.clear()
Dispatchers.resetMain()
sharedPromptConfig.clear()
Expand Down Expand Up @@ -381,7 +385,7 @@ class ChatViewModelCompactionTest {
initializeStorage(context, TEST_PROJECT_KEY)
checkBackendAvailability()
awaitCondition { isBackendAvailable.value }
}
}.also(viewModels::add)
return viewModel to service
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ class ChatViewModelPromptEditTest {

private val configScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)

/** Every ViewModel a test made, cleared before Main is reset; see [clearAndAwait]. */
private val viewModels = mutableListOf<ChatViewModel>()

@Before
fun setUp() {
// ChatViewModel's stateIn() calls run on viewModelScope, i.e. Dispatchers.Main.
Expand All @@ -87,6 +90,7 @@ class ChatViewModelPromptEditTest {

@After
fun tearDown() {
clearAndAwait(viewModels)
SharedServices.clear()
Dispatchers.resetMain()
sharedPromptConfig.clear()
Expand Down Expand Up @@ -384,7 +388,7 @@ class ChatViewModelPromptEditTest {

private fun newViewModel() = ChatViewModel { null }.apply {
initializeStorage(context, TEST_PROJECT_KEY)
}
}.also(viewModels::add)

/**
* A ViewModel whose selected backend is ready and generates by calling [reply] with the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ class ChatViewModelTitleQueueTest {
/** Runs the prompt config load that activation starts on device; the title is worded from it. */
private val configScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)

/** Every ViewModel a test made, cleared before Main is reset; see [clearAndAwait]. */
private val viewModels = mutableListOf<ChatViewModel>()

@Before
fun setUp() {
// ChatViewModel's stateIn() calls run on viewModelScope, i.e. Dispatchers.Main.
Expand Down Expand Up @@ -90,6 +93,7 @@ class ChatViewModelTitleQueueTest {

@After
fun tearDown() {
clearAndAwait(viewModels)
Dispatchers.resetMain()
SharedServices.clear()
sharedPromptConfig.clear()
Expand All @@ -98,7 +102,7 @@ class ChatViewModelTitleQueueTest {

private fun newViewModel() = ChatViewModel { null }.apply {
initializeStorage(context, TEST_PROJECT_KEY)
}
}.also(viewModels::add)

/** A ViewModel whose selected backend is ready; its chat turns never answer. */
private fun newViewModelWithBackend(): ChatViewModel {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.itsaky.androidide.plugins.aicore.viewmodel

import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelStore
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.job
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeout

/** Upper bound on waiting for a cleared ViewModel's coroutines to end. */
private const val CLEAR_TIMEOUT_MS = 5_000L

/**
* Clears each of [viewModels] as its owner would, then waits until every coroutine it started has
* ended.
*
* Call it before `Dispatchers.resetMain()`. A stopped run unwinds on an IO thread and switches to
* Main for its cleanup; when that switch overlaps resetMain(), kotlinx-coroutines-test throws
* "Dispatchers.Main is used concurrently with setting it" and fails whichever test is tearing
* down at that moment.
*/
internal fun clearAndAwait(viewModels: Iterable<ViewModel>) {
viewModels.forEach { viewModel ->
val scopeJob = viewModel.viewModelScope.coroutineContext.job
val store = ViewModelStore()
ViewModelProvider(store, Owning(viewModel))[viewModel.javaClass]
store.clear()
runBlocking { withTimeout(CLEAR_TIMEOUT_MS) { scopeJob.join() } }
}
}

/** Hands an existing ViewModel to a store, the public way to put one there. */
private class Owning(private val viewModel: ViewModel) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T = viewModel as T
}
Loading