diff --git a/composeApp/src/commonMain/kotlin/org/ooni/engine/Engine.kt b/composeApp/src/commonMain/kotlin/org/ooni/engine/Engine.kt index 3cac9418..498f4715 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/engine/Engine.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/engine/Engine.kt @@ -6,15 +6,16 @@ import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.withContext import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import org.ooni.engine.OonimkallBridge.SubmitMeasurementResults +import org.ooni.engine.models.Result import org.ooni.engine.models.TaskEvent import org.ooni.engine.models.TaskEventResult import org.ooni.engine.models.TaskLogLevel import org.ooni.engine.models.TaskOrigin import org.ooni.engine.models.TaskSettings +import org.ooni.engine.models.resultOf import org.ooni.probe.config.Config import org.ooni.probe.shared.Platform import org.ooni.probe.shared.PlatformInfo @@ -62,57 +63,45 @@ class Engine( suspend fun submitMeasurements( measurement: String, taskOrigin: TaskOrigin = TaskOrigin.OoniRun, - ): SubmitMeasurementResults = - withContext(backgroundDispatcher) { - try { - val sessionConfig = buildSessionConfig(taskOrigin) - session(sessionConfig).submitMeasurement(measurement) - } catch (e: Exception) { - throw MkException(e) - } - } + ): Result = + resultOf(backgroundDispatcher) { + val sessionConfig = buildSessionConfig(taskOrigin) + session(sessionConfig).submitMeasurement(measurement) + }.mapError { MkException(it) } suspend fun checkIn( categories: List, taskOrigin: TaskOrigin, - ): OonimkallBridge.CheckInResults = - withContext(backgroundDispatcher) { - try { - val sessionConfig = buildSessionConfig(taskOrigin) - session(sessionConfig).checkIn( - OonimkallBridge.CheckInConfig( - charging = true, - onWiFi = true, - platform = platformInfo.platform.value, - runType = taskOrigin.value, - softwareName = sessionConfig.softwareName, - softwareVersion = sessionConfig.softwareVersion, - webConnectivityCategories = categories, - ), - ) - } catch (e: Exception) { - throw MkException(e) - } - } + ): Result = + resultOf(backgroundDispatcher) { + val sessionConfig = buildSessionConfig(taskOrigin) + session(sessionConfig).checkIn( + OonimkallBridge.CheckInConfig( + charging = true, + onWiFi = true, + platform = platformInfo.platform.value, + runType = taskOrigin.value, + softwareName = sessionConfig.softwareName, + softwareVersion = sessionConfig.softwareVersion, + webConnectivityCategories = categories, + ), + ) + }.mapError { MkException(it) } suspend fun httpDo( method: String, url: String, taskOrigin: TaskOrigin = TaskOrigin.OoniRun, - ): String? = - withContext(backgroundDispatcher) { - try { - session(buildSessionConfig(taskOrigin)) - .httpDo( - OonimkallBridge.HTTPRequest( - method = method, - url = url, - ), - ).body - } catch (e: Exception) { - throw MkException(e) - } - } + ): Result = + resultOf(backgroundDispatcher) { + session(buildSessionConfig(taskOrigin)) + .httpDo( + OonimkallBridge.HTTPRequest( + method = method, + url = url, + ), + ).body + }.mapError { MkException(it) } private fun session(sessionConfig: OonimkallBridge.SessionConfig): OonimkallBridge.Session = bridge.newSession(sessionConfig) @@ -199,5 +188,5 @@ class Engine( } } - class MkException(e: Exception) : Exception(e) + class MkException(t: Throwable) : Exception(t) } diff --git a/composeApp/src/commonMain/kotlin/org/ooni/engine/models/Result.kt b/composeApp/src/commonMain/kotlin/org/ooni/engine/models/Result.kt new file mode 100644 index 00000000..27742be7 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/ooni/engine/models/Result.kt @@ -0,0 +1,59 @@ +package org.ooni.engine.models + +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.withContext +import kotlin.coroutines.coroutineContext + +sealed class Result { + inline infix fun map(mapper: (S) -> S2): Result = + when (this) { + is Success -> Success(mapper(value)) + is Failure -> Failure(reason) + } + + inline infix fun mapError(mapper: (F) -> F2): Result = + when (this) { + is Success -> Success(value) + is Failure -> Failure(mapper(reason)) + } + + inline infix fun flatMap(mapper: (S) -> Result): Result = + when (this) { + is Success -> mapper(value) + is Failure -> Failure(reason) + } + + inline infix fun onSuccess(action: (S) -> Unit): Result { + if (this is Success) { + action(value) + } + return this + } + + inline infix fun onFailure(action: (F) -> Unit): Result { + if (this is Failure) { + action(reason) + } + return this + } + + fun get(): S? = (this as? Success)?.value + + fun getOrThrow(): S = get() ?: throw IllegalStateException("Result is not successful") +} + +data class Success(val value: S) : Result() + +data class Failure(val reason: F) : Result() + +suspend fun resultOf( + coroutineDispatcher: CoroutineDispatcher? = null, + action: suspend () -> S, +): Result = + withContext(coroutineDispatcher ?: coroutineContext) { + try { + Success(action()) + } catch (throwable: Throwable) { + Failure(throwable) + } + } diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/dashboard/DashboardViewModel.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/dashboard/DashboardViewModel.kt index 1a89bb22..76d788bc 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/dashboard/DashboardViewModel.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/dashboard/DashboardViewModel.kt @@ -34,27 +34,20 @@ class DashboardViewModel( _state.value = _state.value.copy(isRunning = true) - try { - val response = - engine.httpDo( - method = "GET", - url = "https://api.dev.ooni.io/api/v2/oonirun/links/10426", - ) - response?.let { Logger.d(it) } - } catch (e: Engine.MkException) { - Logger.e("httpDo failed", e) - } + engine.httpDo( + method = "GET", + url = "https://api.dev.ooni.io/api/v2/oonirun/links/10426", + ) + .onSuccess { Logger.d(it.orEmpty()) } + .onFailure { Logger.e("httpDo failed", it) } val checkInResults = - try { - engine.checkIn( - categories = listOf("NEWS"), - taskOrigin = TaskOrigin.OoniRun, - ) - } catch (e: Engine.MkException) { - Logger.e("checkIn failed", e) - return@flatMapLatest emptyFlow() - } + engine.checkIn( + categories = listOf("NEWS"), + taskOrigin = TaskOrigin.OoniRun, + ) + .onFailure { Logger.e("checkIn failed", it) } + .get() ?: return@flatMapLatest emptyFlow() engine .startTask( diff --git a/composeApp/src/commonTest/kotlin/org/ooni/engine/EngineTest.kt b/composeApp/src/commonTest/kotlin/org/ooni/engine/EngineTest.kt index bcc2e03d..0478b393 100644 --- a/composeApp/src/commonTest/kotlin/org/ooni/engine/EngineTest.kt +++ b/composeApp/src/commonTest/kotlin/org/ooni/engine/EngineTest.kt @@ -3,6 +3,7 @@ package org.ooni.engine import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.toList import kotlinx.coroutines.test.runTest +import org.ooni.engine.models.Failure import org.ooni.engine.models.NetworkType import org.ooni.engine.models.TaskEvent import org.ooni.engine.models.TaskOrigin @@ -12,6 +13,7 @@ import org.ooni.probe.shared.Platform import org.ooni.probe.shared.PlatformInfo import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertTrue class EngineTest { private val json = Dependencies.buildJson() @@ -41,6 +43,20 @@ class EngineTest { assertEquals(NetworkType.NoInternet, settings.annotations.networkType) } + @Test + fun httpDoWithException() = + runTest { + val bridge = TestOonimkallBridge() + val exception = IllegalStateException("failure") + bridge.httpDoMock = { throw exception } + val engine = buildEngine(bridge) + + val result = engine.httpDo("GET", "https://example.org") + + assertTrue(result is Failure) + assertEquals(exception, result.reason.cause) + } + private fun buildEngine(bridge: OonimkallBridge) = Engine( bridge = bridge, @@ -56,6 +72,6 @@ class EngineTest { override val osVersion = "1" override val model = "test" }, - backgroundDispatcher = Dispatchers.Default, + backgroundDispatcher = Dispatchers.Unconfined, ) } diff --git a/composeApp/src/commonTest/kotlin/org/ooni/engine/TestOonimkallBridge.kt b/composeApp/src/commonTest/kotlin/org/ooni/engine/TestOonimkallBridge.kt index ba2145ac..250dd1b1 100644 --- a/composeApp/src/commonTest/kotlin/org/ooni/engine/TestOonimkallBridge.kt +++ b/composeApp/src/commonTest/kotlin/org/ooni/engine/TestOonimkallBridge.kt @@ -18,9 +18,9 @@ class TestOonimkallBridge : OonimkallBridge { var lastSessionConfig: OonimkallBridge.SessionConfig? = null private set - var submitMeasurement: ((String) -> OonimkallBridge.SubmitMeasurementResults)? = null - var checkIn: ((OonimkallBridge.CheckInConfig) -> OonimkallBridge.CheckInResults)? = null - var httpDo: ((OonimkallBridge.HTTPRequest) -> OonimkallBridge.HTTPResponse)? = null + var submitMeasurementMock: ((String) -> OonimkallBridge.SubmitMeasurementResults)? = null + var checkInMock: ((OonimkallBridge.CheckInConfig) -> OonimkallBridge.CheckInResults)? = null + var httpDoMock: ((OonimkallBridge.HTTPRequest) -> OonimkallBridge.HTTPResponse)? = null // Base implementation @@ -40,11 +40,11 @@ class TestOonimkallBridge : OonimkallBridge { return Session() } - class Session : OonimkallBridge.Session { - override fun submitMeasurement(measurement: String): OonimkallBridge.SubmitMeasurementResults = submitMeasurement(measurement) + inner class Session : OonimkallBridge.Session { + override fun submitMeasurement(measurement: String): OonimkallBridge.SubmitMeasurementResults = submitMeasurementMock!!(measurement) - override fun checkIn(config: OonimkallBridge.CheckInConfig): OonimkallBridge.CheckInResults = checkIn(config) + override fun checkIn(config: OonimkallBridge.CheckInConfig): OonimkallBridge.CheckInResults = checkInMock!!(config) - override fun httpDo(request: OonimkallBridge.HTTPRequest): OonimkallBridge.HTTPResponse = httpDo(request) + override fun httpDo(request: OonimkallBridge.HTTPRequest): OonimkallBridge.HTTPResponse = httpDoMock!!(request) } }