From 8bd9bcc6a6a310fd765c4ad592f947597c538b8c Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Thu, 15 Feb 2024 21:04:59 +0300 Subject: [PATCH] Fix bizzare NPE in context generation for DeviceClient. Add test for remote client --- controls-magix/build.gradle.kts | 1 + .../kscience/controls/client/DeviceClient.kt | 6 +- .../controls/client/RemoteDeviceConnect.kt | 87 +++++++++++++++++++ 3 files changed, 91 insertions(+), 3 deletions(-) create mode 100644 controls-magix/src/commonTest/kotlin/space/kscience/controls/client/RemoteDeviceConnect.kt diff --git a/controls-magix/build.gradle.kts b/controls-magix/build.gradle.kts index 0296b8c..3a819bf 100644 --- a/controls-magix/build.gradle.kts +++ b/controls-magix/build.gradle.kts @@ -12,6 +12,7 @@ description = """ kscience { jvm() js() + useCoroutines("1.8.0") useSerialization { json() } diff --git a/controls-magix/src/commonMain/kotlin/space/kscience/controls/client/DeviceClient.kt b/controls-magix/src/commonMain/kotlin/space/kscience/controls/client/DeviceClient.kt index 474f86a..924fad9 100644 --- a/controls-magix/src/commonMain/kotlin/space/kscience/controls/client/DeviceClient.kt +++ b/controls-magix/src/commonMain/kotlin/space/kscience/controls/client/DeviceClient.kt @@ -1,8 +1,8 @@ package space.kscience.controls.client import com.benasher44.uuid.uuid4 +import kotlinx.coroutines.Job import kotlinx.coroutines.flow.* -import kotlinx.coroutines.newCoroutineContext import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import space.kscience.controls.api.* @@ -28,8 +28,8 @@ public class DeviceClient( private val send: suspend (DeviceMessage) -> Unit, ) : CachingDevice { - @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class) - override val coroutineContext: CoroutineContext = newCoroutineContext(context.coroutineContext) + + override val coroutineContext: CoroutineContext = context.coroutineContext + Job(context.coroutineContext[Job]) private val mutex = Mutex() diff --git a/controls-magix/src/commonTest/kotlin/space/kscience/controls/client/RemoteDeviceConnect.kt b/controls-magix/src/commonTest/kotlin/space/kscience/controls/client/RemoteDeviceConnect.kt new file mode 100644 index 0000000..f0f65fa --- /dev/null +++ b/controls-magix/src/commonTest/kotlin/space/kscience/controls/client/RemoteDeviceConnect.kt @@ -0,0 +1,87 @@ +package space.kscience.controls.client + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.test.runTest +import kotlinx.serialization.json.Json +import space.kscience.controls.api.Device +import space.kscience.controls.manager.DeviceManager +import space.kscience.controls.manager.install +import space.kscience.controls.manager.respondMessage +import space.kscience.controls.spec.* +import space.kscience.dataforge.context.Context +import space.kscience.dataforge.context.Factory +import space.kscience.dataforge.context.request +import space.kscience.dataforge.meta.Meta +import space.kscience.dataforge.meta.get +import space.kscience.dataforge.meta.int +import space.kscience.dataforge.names.Name +import space.kscience.magix.api.MagixEndpoint +import space.kscience.magix.api.MagixMessage +import space.kscience.magix.api.MagixMessageFilter +import kotlin.random.Random +import kotlin.test.Test +import kotlin.test.assertContains +import kotlin.time.Duration.Companion.milliseconds + + +public suspend fun Device.readUnsafe(propertySpec: DevicePropertySpec<*, T>): T = + propertySpec.converter.metaToObject(readProperty(propertySpec.name)) ?: error("Property read result is not valid") + +internal class RemoteDeviceConnect { + + class TestDevice(context: Context, meta: Meta) : DeviceBySpec(TestDevice, context, meta) { + private val rng = Random(meta["seed"].int ?: 0) + + private val randomValue get() = rng.nextDouble() + + companion object : DeviceSpec(), Factory { + + override fun build(context: Context, meta: Meta): TestDevice = TestDevice(context, meta) + + val value by doubleProperty { randomValue } + + override suspend fun TestDevice.onOpen() { + doRecurring((meta["delay"].int ?: 10).milliseconds) { + read(value) + } + } + } + } + + @Test + fun wrapper() = runTest { + val context = Context { + plugin(DeviceManager) + } + + val device = context.request(DeviceManager).install("test", TestDevice) + + val virtualMagixEndpoint = object : MagixEndpoint { + + + override fun subscribe(filter: MagixMessageFilter): Flow = device.messageFlow.map { + MagixMessage( + format = DeviceManager.magixFormat.defaultFormat, + payload = MagixEndpoint.magixJson.encodeToJsonElement(DeviceManager.magixFormat.serializer, it), + sourceEndpoint = "test", + ) + } + + override suspend fun broadcast(message: MagixMessage) { + device.respondMessage( + Name.EMPTY, + Json.decodeFromJsonElement(DeviceManager.magixFormat.serializer, message.payload) + ) + } + + override fun close() { + // + } + } + + val remoteDevice = virtualMagixEndpoint.remoteDevice(context, "test", Name.EMPTY) + + assertContains(0.0..1.0, remoteDevice.readUnsafe(TestDevice.value)) + } +} \ No newline at end of file