diff --git a/app/src/main/java/eu/darken/myperm/common/network/NetworkRequestBuilderProvider.kt b/app/src/main/java/eu/darken/myperm/common/network/NetworkRequestBuilderProvider.kt deleted file mode 100644 index cd23204b..00000000 --- a/app/src/main/java/eu/darken/myperm/common/network/NetworkRequestBuilderProvider.kt +++ /dev/null @@ -1,12 +0,0 @@ -package eu.darken.myperm.common.network - -import android.net.NetworkRequest -import javax.inject.Inject -import javax.inject.Provider - -/** - * Indirection to allow this to run in unit tests, without requiring framework classes - */ -class NetworkRequestBuilderProvider @Inject constructor() : Provider { - override fun get(): NetworkRequest.Builder = NetworkRequest.Builder() -} diff --git a/app/src/main/java/eu/darken/myperm/common/network/NetworkStateProvider.kt b/app/src/main/java/eu/darken/myperm/common/network/NetworkStateProvider.kt deleted file mode 100644 index de286d7e..00000000 --- a/app/src/main/java/eu/darken/myperm/common/network/NetworkStateProvider.kt +++ /dev/null @@ -1,198 +0,0 @@ -package eu.darken.myperm.common.network - -import android.annotation.SuppressLint -import android.content.Context -import android.net.ConnectivityManager -import android.net.LinkProperties -import android.net.Network -import android.net.NetworkCapabilities -import android.net.NetworkCapabilities.* -import android.os.Build -import androidx.annotation.RequiresApi -import androidx.core.net.ConnectivityManagerCompat -import dagger.hilt.android.qualifiers.ApplicationContext -import eu.darken.myperm.BuildConfig -import eu.darken.myperm.common.coroutine.AppScope -import eu.darken.myperm.common.debug.logging.Logging.Priority.ERROR -import eu.darken.myperm.common.debug.logging.Logging.Priority.VERBOSE -import eu.darken.myperm.common.debug.logging.asLog -import eu.darken.myperm.common.debug.logging.log -import eu.darken.myperm.common.debug.logging.logTag -import eu.darken.myperm.common.flow.replayingShare -import eu.darken.myperm.common.hasApiLevel -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.channels.awaitClose -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.callbackFlow -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.launch -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class NetworkStateProvider @Inject constructor( - @ApplicationContext private val context: Context, - @AppScope private val appScope: CoroutineScope, - private val networkRequestBuilderProvider: NetworkRequestBuilderProvider -) { - private val manager: ConnectivityManager - get() = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager - - val networkState: Flow = callbackFlow { - send(currentState) - - var registeredCallback: ConnectivityManager.NetworkCallback? = null - - fun callbackRefresh(delayValue: Long = 0) { - appScope.launch { - delay(delayValue) - send(currentState) - } - } - - try { - val callback = object : ConnectivityManager.NetworkCallback() { - override fun onAvailable(network: Network) { - log(TAG, VERBOSE) { "onAvailable(network=$network)" } - callbackRefresh() - } - - /** - * Some devices don't update the active network fast enough. - * 200ms gave good results on a Pixel 2. - */ - override fun onLost(network: Network) { - log(TAG, VERBOSE) { "onLost(network=$network)" } - callbackRefresh(200) - } - - /** - * Not consistently called in all Android versions - * https://issuetracker.google.com/issues/144891976 - */ - override fun onUnavailable() { - log(TAG, VERBOSE) { "onUnavailable()" } - callbackRefresh() - } - - override fun onLosing(network: Network, maxMsToLive: Int) { - log(TAG, VERBOSE) { "onLosing(network=$network, maxMsToLive=$maxMsToLive)" } - callbackRefresh() - } - - override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) { - log(TAG, VERBOSE) { "onCapabilitiesChanged(network=$network, capabilities=$networkCapabilities)" } - callbackRefresh() - } - - override fun onLinkPropertiesChanged(network: Network, linkProperties: LinkProperties) { - log(TAG, VERBOSE) { "onLinkPropertiesChanged(network=$network, linkProperties=$linkProperties)" } - callbackRefresh() - } - - override fun onBlockedStatusChanged(network: Network, blocked: Boolean) { - log(TAG, VERBOSE) { "onBlockedStatusChanged(network=$network, blocked=$blocked)" } - callbackRefresh() - } - } - - val request = networkRequestBuilderProvider.get() - .addCapability(NET_CAPABILITY_INTERNET) - .build() - - /** - * This may throw java.lang.SecurityException on Samsung devices - * java.lang.SecurityException: - * at android.os.Parcel.createExceptionOrNull (Parcel.java:2385) - * at android.net.ConnectivityManager.registerNetworkCallback (ConnectivityManager.java:4564) - */ - manager.registerNetworkCallback(request, callback) - registeredCallback = callback - } catch (e: SecurityException) { - log(TAG, ERROR) { - "registerNetworkCallback() threw an undocumented SecurityException, Just Samsung Things™️:${e.asLog()}" - } - send(State.Fallback) - } - - awaitClose { - log(TAG, VERBOSE) { "unregisterNetworkCallback($registeredCallback)" } - registeredCallback?.let { manager.unregisterNetworkCallback(it) } - } - } - .distinctUntilChanged() - .replayingShare(scope = appScope) - - private val currentState: State - @SuppressLint("NewApi") - get() = try { - when { - hasApiLevel(Build.VERSION_CODES.M) -> modernNetworkState() - else -> legacyNetworkState() - } - } catch (e: Exception) { - if (BuildConfig.DEBUG) throw e - // Don't crash on appScope in prod - log(TAG, ERROR) { "Failed to determine current network state, using fallback:${e.asLog()}" } - State.Fallback - } - - @RequiresApi(Build.VERSION_CODES.M) - private fun modernNetworkState(): State = manager.activeNetwork.let { network -> - State.Modern( - activeNetwork = network, - capabilities = network?.let { - try { - manager.getNetworkCapabilities(it) - } catch (e: SecurityException) { - log(TAG, ERROR) { "Failed to determine network capabilities:${e.asLog()}" } - null - } - }, - ) - } - - @Suppress("DEPRECATION") - private fun legacyNetworkState(): State = State.LegacyAPI21( - isInternetAvailable = manager.activeNetworkInfo?.isConnected ?: false, - isMeteredConnection = ConnectivityManagerCompat.isActiveNetworkMetered(manager) - ) - - interface State { - val isMeteredConnection: Boolean - val isInternetAvailable: Boolean - - data class LegacyAPI21( - override val isMeteredConnection: Boolean, - override val isInternetAvailable: Boolean - ) : State - - data class Modern( - val activeNetwork: Network?, - val capabilities: NetworkCapabilities?, - ) : State { - override val isInternetAvailable: Boolean - get() = capabilities?.hasCapability(NET_CAPABILITY_VALIDATED) ?: false - - override val isMeteredConnection: Boolean - get() { - val unMetered = if (hasApiLevel(Build.VERSION_CODES.N)) { - capabilities?.hasCapability(NET_CAPABILITY_NOT_METERED) ?: false - } else { - capabilities?.hasTransport(TRANSPORT_WIFI) ?: false - } - return !unMetered - } - } - - object Fallback : State { - override val isMeteredConnection: Boolean = true - override val isInternetAvailable: Boolean = true - } - } - - companion object { - private val TAG = logTag("NetworkStateProvider") - } -} diff --git a/app/src/test/java/eu/darken/myperm/common/flow/DynamicStateFlowTest.kt b/app/src/test/java/eu/darken/myperm/common/flow/DynamicStateFlowTest.kt index ef3a0b79..7b419014 100644 --- a/app/src/test/java/eu/darken/myperm/common/flow/DynamicStateFlowTest.kt +++ b/app/src/test/java/eu/darken/myperm/common/flow/DynamicStateFlowTest.kt @@ -3,53 +3,47 @@ package eu.darken.myperm.common.flow import eu.darken.myperm.common.collections.mutate import io.kotest.assertions.throwables.shouldThrow import io.kotest.matchers.shouldBe -import io.kotest.matchers.types.instanceOf import io.kotest.matchers.types.shouldBeInstanceOf import io.mockk.coEvery import io.mockk.coVerify import io.mockk.mockk -import kotlinx.coroutines.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.firstOrNull -import kotlinx.coroutines.test.* +import kotlinx.coroutines.test.advanceUntilIdle import org.junit.jupiter.api.Test import testhelper.BaseTest import testhelper.coroutine.runTest2 import testhelper.flow.test import java.io.IOException -import java.lang.Thread.sleep import kotlin.concurrent.thread -import kotlin.coroutines.EmptyCoroutineContext + class DynamicStateFlowTest : BaseTest() { // Without an init value, there isn't a way to keep using the flow @Test - fun `exceptions on initialization are rethrown`() { - val testScope = - createTestCoroutineScope(TestCoroutineDispatcher() + TestCoroutineExceptionHandler() + EmptyCoroutineContext) + fun `exceptions on initialization are rethrown`() = runTest2(expectedError = IOException::class) { + val testScope = this + val hotData = DynamicStateFlow( loggingTag = "tag", parentScope = testScope, coroutineContext = Dispatchers.Unconfined, startValueProvider = { throw IOException() } ) - runBlocking { - withTimeoutOrNull(500) { - // This blocking scope gets the init exception as the first caller - hotData.flow.firstOrNull() - } shouldBe null - } - - testScope.advanceUntilIdle() - testScope.uncaughtExceptions.single() shouldBe instanceOf(IOException::class) + // This blocking scope gets the init exception as the first caller + hotData.flow.firstOrNull() } + @Test - fun `subscription doesn't end when no subscriber is collecting, mode Lazily`() { - val testScope = - createTestCoroutineScope(TestCoroutineDispatcher() + TestCoroutineExceptionHandler() + EmptyCoroutineContext) + fun `subscription doesn't end when no subscriber is collecting, mode Lazily`() = runTest2(autoCancel = true) { + val testScope = this + val valueProvider = mockk String>() coEvery { valueProvider.invoke(any()) } returns "Test" @@ -60,19 +54,16 @@ class DynamicStateFlowTest : BaseTest() { startValueProvider = valueProvider, ) - testScope.apply { - runTest2(autoCancel = true) { - hotData.flow.first() shouldBe "Test" - hotData.flow.first() shouldBe "Test" - } - coVerify(exactly = 1) { valueProvider.invoke(any()) } - } + hotData.flow.first() shouldBe "Test" + hotData.flow.first() shouldBe "Test" + + coVerify(exactly = 1) { valueProvider.invoke(any()) } } @Test - fun `value updates`() { - val testScope = - createTestCoroutineScope(TestCoroutineDispatcher() + TestCoroutineExceptionHandler() + EmptyCoroutineContext) + fun `value updates`() = runTest2(autoCancel = true) { + val testScope = this + val valueProvider = mockk Long>() coEvery { valueProvider.invoke(any()) } returns 1 @@ -88,7 +79,6 @@ class DynamicStateFlowTest : BaseTest() { (1..16).forEach { _ -> thread { (1..200).forEach { _ -> - sleep(10) hotData.updateAsync( onUpdate = { this + 1L }, onError = { throw it } @@ -97,12 +87,16 @@ class DynamicStateFlowTest : BaseTest() { } } - runBlocking { - testCollector.await { list, _ -> list.size == 3201 } - testCollector.latestValues shouldBe (1..3201).toList() - } + advanceUntilIdle() + + testCollector.await { list, _ -> list.size == 3201 } + testCollector.latestValues shouldBe (1..3201).toList() + + advanceUntilIdle() coVerify(exactly = 1) { valueProvider.invoke(any()) } + + testCollector.cancelAndJoin() } data class TestData( @@ -110,9 +104,9 @@ class DynamicStateFlowTest : BaseTest() { ) @Test - fun `check multi threading value updates with more complex data`() { - val testScope = - createTestCoroutineScope(TestCoroutineDispatcher() + TestCoroutineExceptionHandler() + EmptyCoroutineContext) + fun `check multi threading value updates with more complex data`() = runTest2(autoCancel = true) { + val testScope = this + val valueProvider = mockk Map>() coEvery { valueProvider.invoke(any()) } returns mapOf("data" to TestData()) @@ -139,18 +133,21 @@ class DynamicStateFlowTest : BaseTest() { } } - runBlocking { - testCollector.await { list, _ -> list.size == 4001 } - testCollector.latestValues.map { it.values.single().number } shouldBe (1L..4001L).toList() - } + advanceUntilIdle() + + testCollector.await { list, _ -> list.size == 4001 } + testCollector.latestValues.map { it.values.single().number } shouldBe (1L..4001L).toList() + + advanceUntilIdle() coVerify(exactly = 1) { valueProvider.invoke(any()) } + + testCollector.cancelAndJoin() } @Test - fun `only emit new values if they actually changed updates`() { - val testScope = - createTestCoroutineScope(TestCoroutineDispatcher() + TestCoroutineExceptionHandler() + EmptyCoroutineContext) + fun `only emit new values if they actually changed updates`() = runTest2(autoCancel = true) { + val testScope = this val hotData = DynamicStateFlow( loggingTag = "tag", @@ -166,26 +163,28 @@ class DynamicStateFlowTest : BaseTest() { hotData.updateAsync { "2" } hotData.updateAsync { "1" } - runBlocking { - testCollector.await { list, _ -> list.size == 3 } - testCollector.latestValues shouldBe listOf("1", "2", "1") - } + advanceUntilIdle() + + testCollector.await { list, _ -> list.size == 3 } + testCollector.latestValues shouldBe listOf("1", "2", "1") } @Test fun `multiple subscribers share the flow`() = runTest2(autoCancel = true) { + val testScope = this + val valueProvider = mockk String>() coEvery { valueProvider.invoke(any()) } returns "Test" val hotData = DynamicStateFlow( loggingTag = "tag", - parentScope = this, + parentScope = testScope, startValueProvider = valueProvider, ) - val sub1 = hotData.flow.test(tag = "sub1", scope = this) - val sub2 = hotData.flow.test(tag = "sub2", scope = this) - val sub3 = hotData.flow.test(tag = "sub3", scope = this) + val sub1 = hotData.flow.test(tag = "sub1", scope = testScope) + val sub2 = hotData.flow.test(tag = "sub2", scope = testScope) + val sub3 = hotData.flow.test(tag = "sub3", scope = testScope) hotData.updateAsync { "A" } hotData.updateAsync { "B" } @@ -206,17 +205,19 @@ class DynamicStateFlowTest : BaseTest() { @Test fun `value is persisted between unsubscribes`() = runTest2(autoCancel = true) { + val testScope = this + val valueProvider = mockk Long>() coEvery { valueProvider.invoke(any()) } returns 1 val hotData = DynamicStateFlow( loggingTag = "tag", - parentScope = this, + parentScope = testScope, coroutineContext = this.coroutineContext, startValueProvider = valueProvider, ) - val testCollector1 = hotData.flow.test(tag = "collector1", scope = this) + val testCollector1 = hotData.flow.test(tag = "collector1", scope = testScope) testCollector1.silent = false (1..10).forEach { _ -> @@ -232,7 +233,7 @@ class DynamicStateFlowTest : BaseTest() { testCollector1.cancelAndJoin() - val testCollector2 = hotData.flow.test(tag = "collector2", scope = this) + val testCollector2 = hotData.flow.test(tag = "collector2", scope = testScope) testCollector2.silent = false advanceUntilIdle() @@ -245,9 +246,9 @@ class DynamicStateFlowTest : BaseTest() { } @Test - fun `blocking update is actually blocking`() = runBlocking { - val testScope = - createTestCoroutineScope(TestCoroutineDispatcher() + TestCoroutineExceptionHandler() + EmptyCoroutineContext) + fun `blocking update is actually blocking`() = runTest2(autoCancel = true) { + val testScope = this + val hotData = DynamicStateFlow( loggingTag = "tag", parentScope = testScope, @@ -269,6 +270,8 @@ class DynamicStateFlowTest : BaseTest() { hotData.updateBlocking { this - 3 } shouldBe 0 + advanceUntilIdle() + testCollector.await { _, i -> i == 3 } testCollector.latestValues shouldBe listOf(2, 3, 0) @@ -276,9 +279,9 @@ class DynamicStateFlowTest : BaseTest() { } @Test - fun `blocking update rethrows error`() = runBlocking { - val testScope = - createTestCoroutineScope(TestCoroutineDispatcher() + TestCoroutineExceptionHandler() + EmptyCoroutineContext) + fun `blocking update rethrows error`() = runTest2(autoCancel = true) { + val testScope = this + val hotData = DynamicStateFlow( loggingTag = "tag", parentScope = testScope, @@ -299,9 +302,10 @@ class DynamicStateFlowTest : BaseTest() { hotData.flow.first() shouldBe 2 hotData.updateBlocking { 3 } shouldBe 3 - hotData.flow.first() shouldBe 3 - testScope.uncaughtExceptions.singleOrNull() shouldBe null + advanceUntilIdle() + + hotData.flow.first() shouldBe 3 testCollector.cancelAndJoin() } @@ -320,12 +324,13 @@ class DynamicStateFlowTest : BaseTest() { hotData.updateAsync { throw IOException("Surprise") } advanceUntilIdle() + + testCollector.cancelAndJoin() } @Test - fun `async updates rethrow errors on HotDataFlow scope if no error handler is set`() = runBlocking { - val testScope = - createTestCoroutineScope(TestCoroutineDispatcher() + TestCoroutineExceptionHandler() + EmptyCoroutineContext) + fun `async updates rethrow errors on HotDataFlow scope if no error handler is set`() = runTest2(autoCancel = true) { + val testScope = this val hotData = DynamicStateFlow( loggingTag = "tag", @@ -345,31 +350,29 @@ class DynamicStateFlowTest : BaseTest() { testScope.advanceUntilIdle() thrownError!!.shouldBeInstanceOf() - testScope.uncaughtExceptions.singleOrNull() shouldBe null testCollector.cancelAndJoin() } @Test - fun `clean up function is called when parent scope is cancelled`() = runTest { - val testScope = - createTestCoroutineScope(TestCoroutineDispatcher() + TestCoroutineExceptionHandler() + EmptyCoroutineContext) - + fun `clean up function is called when parent scope is cancelled`() { var onReleaseValue: String? = null - val hotData = DynamicStateFlow( - loggingTag = "tag", - parentScope = testScope, - coroutineContext = Dispatchers.Unconfined, - startValueProvider = { "Test" }, - onRelease = { - onReleaseValue = it - } - ) + runTest2(autoCancel = true) { + val testScope = this - hotData.flow.first() shouldBe "Test" + val hotData = DynamicStateFlow( + loggingTag = "tag", + parentScope = testScope, + coroutineContext = Dispatchers.Unconfined, + startValueProvider = { "Test" }, + onRelease = { + onReleaseValue = it + } + ) - testScope.cancel() + hotData.flow.first() shouldBe "Test" + } onReleaseValue shouldBe "Test" } diff --git a/app/src/test/java/eu/darken/myperm/common/network/NetworkStateProviderTest.kt b/app/src/test/java/eu/darken/myperm/common/network/NetworkStateProviderTest.kt deleted file mode 100644 index 85b7d888..00000000 --- a/app/src/test/java/eu/darken/myperm/common/network/NetworkStateProviderTest.kt +++ /dev/null @@ -1,283 +0,0 @@ -@file:Suppress("DEPRECATION") - -package eu.darken.myperm.common.network - -import android.content.Context -import android.net.* -import android.net.ConnectivityManager.* -import eu.darken.myperm.common.BuildWrap -import io.kotest.assertions.throwables.shouldNotThrowAny -import io.kotest.matchers.shouldBe -import io.mockk.* -import io.mockk.impl.annotations.MockK -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.test.TestCoroutineScope -import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.advanceUntilIdle -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import testhelper.BaseTest -import testhelper.coroutine.runTest2 -import testhelper.flow.test - -class NetworkStateProviderTest : BaseTest() { - - @MockK lateinit var context: Context - @MockK lateinit var connectivityManager: ConnectivityManager - - @MockK lateinit var network: Network - @MockK lateinit var networkInfo: NetworkInfo - @MockK lateinit var networkRequest: NetworkRequest - @MockK lateinit var networkRequestBuilder: NetworkRequest.Builder - @MockK lateinit var networkRequestBuilderProvider: NetworkRequestBuilderProvider - @MockK lateinit var capabilities: NetworkCapabilities - @MockK lateinit var linkProperties: LinkProperties - - private var lastRequest: NetworkRequest? = null - private var lastCallback: NetworkCallback? = null - - @BeforeEach - fun setup() { - MockKAnnotations.init(this) - - mockkObject(BuildWrap.VersionWrap) - every { BuildWrap.VersionWrap.SDK_INT } returns 24 - - every { context.getSystemService(Context.CONNECTIVITY_SERVICE) } returns connectivityManager - - every { networkRequestBuilderProvider.get() } returns networkRequestBuilder - networkRequestBuilder.apply { - every { addCapability(any()) } returns networkRequestBuilder - every { build() } returns networkRequest - } - - connectivityManager.apply { - every { activeNetwork } returns network - every { activeNetworkInfo } answers { networkInfo } - every { unregisterNetworkCallback(any()) } just Runs - - every { getNetworkCapabilities(network) } answers { capabilities } - every { getLinkProperties(network) } answers { linkProperties } - - every { - registerNetworkCallback(any(), any()) - } answers { - lastRequest = arg(0) - lastCallback = arg(1) - mockk() - } - } - - networkInfo.apply { - every { type } returns TYPE_WIFI - every { isConnected } returns true - } - - capabilities.apply { - // The happy path is an unmetered internet connection being available - every { hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) } returns true - every { hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) } returns true - every { hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED) } returns true - every { hasTransport(NetworkCapabilities.TRANSPORT_WIFI) } returns true - } - } - - private fun createInstance(scope: CoroutineScope) = NetworkStateProvider( - context = context, - appScope = scope, - networkRequestBuilderProvider = networkRequestBuilderProvider, - ) - - @Test - fun `init is sideeffect free and lazy`() { - shouldNotThrowAny { - createInstance(TestCoroutineScope()) - } - verify { connectivityManager wasNot Called } - } - - @Test - fun `initial state is emitted correctly without callback`() = runTest2(autoCancel = true) { - val instance = createInstance(this) - - instance.networkState.first().apply { - isMeteredConnection shouldBe false - isInternetAvailable shouldBe true - } - - advanceUntilIdle() - - verifySequence { - connectivityManager.activeNetwork - connectivityManager.getNetworkCapabilities(network) - connectivityManager.registerNetworkCallback(networkRequest, any()) - connectivityManager.unregisterNetworkCallback(lastCallback!!) - } - } - - @Test - fun `we can handle null networks`() = runTest2(autoCancel = true) { - every { connectivityManager.activeNetwork } returns null - val instance = createInstance(this) - - instance.networkState.first().apply { - isInternetAvailable shouldBe false - isMeteredConnection shouldBe true - } - verify { connectivityManager.activeNetwork } - } - - @Test - fun `system callbacks lead to new emissions with an updated state`() = runTest2(autoCancel = true) { - val instance = createInstance(this) - - val testCollector = instance.networkState.test(scope = this) - advanceUntilIdle() - - lastCallback!!.onAvailable(mockk()) - advanceUntilIdle() - - every { connectivityManager.activeNetwork } returns null - lastCallback!!.onUnavailable() - advanceUntilIdle() - - every { connectivityManager.activeNetwork } returns network - lastCallback!!.onAvailable(mockk()) - advanceUntilIdle() - - // 3 not 4 as first onAvailable call doesn't change the value (stateIn behavior) - testCollector.latestValues.size shouldBe 3 - - testCollector.awaitFinal(cancel = true) - advanceUntilIdle() - - verifySequence { - // Start value - connectivityManager.activeNetwork - connectivityManager.getNetworkCapabilities(network) - connectivityManager.registerNetworkCallback(networkRequest, any()) - - // onAvailable - connectivityManager.activeNetwork - connectivityManager.getNetworkCapabilities(network) - - // onUnavailable - connectivityManager.activeNetwork - - // onAvailable - connectivityManager.activeNetwork - connectivityManager.getNetworkCapabilities(network) - connectivityManager.unregisterNetworkCallback(lastCallback!!) - } - } - - @Test - fun `metered connection state checks capabilities`() = runTest2(autoCancel = true) { - createInstance(this).apply { - networkState.first().isMeteredConnection shouldBe false - - every { capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED) } returns false - networkState.first().isMeteredConnection shouldBe true - - every { connectivityManager.getNetworkCapabilities(any()) } returns null - networkState.first().isMeteredConnection shouldBe true - } - } - - @Test - fun `Android 6 not metered on wifi`() = runTest2(autoCancel = true) { - every { BuildWrap.VERSION.SDK_INT } returns 23 - val instance = createInstance(this) - - every { capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) } returns false - instance.networkState.first().isMeteredConnection shouldBe true - advanceUntilIdle() - - every { capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) } returns true - instance.networkState.first().isMeteredConnection shouldBe false - advanceUntilIdle() - - every { connectivityManager.getNetworkCapabilities(any()) } returns null - instance.networkState.first().isMeteredConnection shouldBe true - } - - @Test - fun `do not try to unregister the callback if it was never registered`() = runTest2(autoCancel = true) { - every { - connectivityManager.registerNetworkCallback( - any(), - any() - ) - } throws SecurityException() - val testScope2 = TestScope() - val testObs = createInstance(this).networkState.test(scope = this).start(testScope2) - - advanceUntilIdle() - - verifySequence { - connectivityManager.activeNetwork - connectivityManager.getNetworkCapabilities(network) - connectivityManager.registerNetworkCallback(networkRequest, any()) - } - verify(exactly = 0) { - connectivityManager.unregisterNetworkCallback(any()) - } - } - - @Test - fun `send the fallback state on exceptions`() = runTest2(autoCancel = true) { - every { - connectivityManager.registerNetworkCallback( - any(), - any() - ) - } throws SecurityException() - - val instance = createInstance(this) - - val testObs = instance.networkState.test(scope = this).start(this) - - advanceUntilIdle() - - testObs.latestValues[0].apply { - isInternetAvailable shouldBe true - isMeteredConnection shouldBe false - } - - testObs.latestValues[1].apply { - isInternetAvailable shouldBe true - isMeteredConnection shouldBe true - } - testObs.cancelAndJoin() - } - - @Test - fun `current state is correctly determined below API 23`() = runTest2(autoCancel = true) { - every { BuildWrap.VERSION.SDK_INT } returns 22 - - createInstance(this).apply { - networkState.first().apply { - isInternetAvailable shouldBe true - isMeteredConnection shouldBe false - } - advanceUntilIdle() - - every { networkInfo.type } returns TYPE_MOBILE - networkState.first().apply { - isInternetAvailable shouldBe true - isMeteredConnection shouldBe true - } - advanceUntilIdle() - - every { networkInfo.isConnected } returns false - networkState.first().apply { - isInternetAvailable shouldBe false - isMeteredConnection shouldBe true - } - } - - verify { connectivityManager.activeNetworkInfo } - } -} - diff --git a/app/src/test/java/testhelper/coroutine/CoroutinesTestExtension.kt b/app/src/test/java/testhelper/coroutine/CoroutinesTestExtension.kt deleted file mode 100644 index 3364bbd9..00000000 --- a/app/src/test/java/testhelper/coroutine/CoroutinesTestExtension.kt +++ /dev/null @@ -1,24 +0,0 @@ -package testhelper.coroutine - -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.* -import org.junit.jupiter.api.extension.AfterEachCallback -import org.junit.jupiter.api.extension.BeforeEachCallback -import org.junit.jupiter.api.extension.ExtensionContext - -@ExperimentalCoroutinesApi -class CoroutinesTestExtension( - private val dispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher() -) : BeforeEachCallback, AfterEachCallback, - TestCoroutineScope by createTestCoroutineScope(TestCoroutineDispatcher() + TestCoroutineExceptionHandler() + dispatcher) { - - override fun beforeEach(context: ExtensionContext?) { - Dispatchers.setMain(dispatcher) - } - - override fun afterEach(context: ExtensionContext?) { - cleanupTestCoroutines() - Dispatchers.resetMain() - } -}