From 2632b4d8a13e2a9ab8a009e1f033164dede0a9ff Mon Sep 17 00:00:00 2001 From: Andre Weber Date: Thu, 7 Dec 2023 07:55:18 +0100 Subject: [PATCH 1/6] feature: Remove "Custom Wildcard Subscription"-Logic for Specs Closes: #60 Signed-off-by: Andre Weber --- .../eclipse/kuksa/DataBrokerTransporter.kt | 2 +- .../subscription/DataBrokerSubscriber.kt | 26 +++++++++---------- .../SpecificationPropertyListener.kt | 1 - .../kuksa/subscription/Subscription.kt | 2 +- 4 files changed, 15 insertions(+), 16 deletions(-) diff --git a/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/DataBrokerTransporter.kt b/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/DataBrokerTransporter.kt index c06c43d6..ecea7043 100644 --- a/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/DataBrokerTransporter.kt +++ b/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/DataBrokerTransporter.kt @@ -153,7 +153,7 @@ internal class DataBrokerTransporter( val entry = entryUpdate.entry subscription.listeners.forEach { observer -> - observer.onPropertyChanged(vssPath, field, entry) + observer.onPropertyChanged(entry.path, field, entry) } } diff --git a/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/subscription/DataBrokerSubscriber.kt b/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/subscription/DataBrokerSubscriber.kt index a53b159f..f2e26c08 100644 --- a/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/subscription/DataBrokerSubscriber.kt +++ b/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/subscription/DataBrokerSubscriber.kt @@ -25,11 +25,11 @@ import org.eclipse.kuksa.DataBrokerTransporter import org.eclipse.kuksa.PropertyListener import org.eclipse.kuksa.VssSpecificationListener import org.eclipse.kuksa.extension.TAG -import org.eclipse.kuksa.extension.createProperties import org.eclipse.kuksa.proto.v1.Types import org.eclipse.kuksa.proto.v1.Types.Field import org.eclipse.kuksa.vsscore.model.VssProperty import org.eclipse.kuksa.vsscore.model.VssSpecification +import org.eclipse.kuksa.vsscore.model.vssProperties /** * Creates [Subscription]s to the DataBroker to get notified about changes on the underlying vssPaths and fields. @@ -91,13 +91,13 @@ internal class DataBrokerSubscriber(private val dataBrokerTransporter: DataBroke field: Field = Field.FIELD_VALUE, listener: VssSpecificationListener, ) { - val leafProperties = specification.createProperties(field) - val vssPaths = leafProperties.map { it.vssPath } + val vssPath = specification.vssPath - val specificationPropertyListener = SpecificationPropertyListener(specification, vssPaths, listener) - vssPaths.forEach { vssPath -> - subscribe(vssPath, field, specificationPropertyListener) - } + val leafProperties = specification.vssProperties + val childPropertiesPaths = leafProperties.map { it.vssPath } + + val specificationPropertyListener = SpecificationPropertyListener(specification, childPropertiesPaths, listener) + subscribe(vssPath, field, specificationPropertyListener) } /** @@ -111,13 +111,13 @@ internal class DataBrokerSubscriber(private val dataBrokerTransporter: DataBroke field: Field = Field.FIELD_VALUE, listener: VssSpecificationListener, ) { - val leafProperties = specification.createProperties(field) - val vssPaths = leafProperties.map { it.vssPath } + val vssPath = specification.vssPath - val specificationPropertyListener = SpecificationPropertyListener(specification, vssPaths, listener) - vssPaths.forEach { vssPath -> - unsubscribe(vssPath, field, specificationPropertyListener) - } + val leafProperties = specification.vssProperties + val childPropertiesPaths = leafProperties.map { it.vssPath } + + val specificationPropertyListener = SpecificationPropertyListener(specification, childPropertiesPaths, listener) + unsubscribe(vssPath, field, specificationPropertyListener) } private companion object { diff --git a/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/subscription/SpecificationPropertyListener.kt b/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/subscription/SpecificationPropertyListener.kt index 4de50f5d..7c95ec67 100644 --- a/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/subscription/SpecificationPropertyListener.kt +++ b/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/subscription/SpecificationPropertyListener.kt @@ -35,7 +35,6 @@ internal class SpecificationPropertyListener( vssPaths: Collection, private val listener: VssSpecificationListener, ) : PropertyListener { - // TODO: Remove as soon as the server supports subscribing to vssPaths which are not VssProperties // Reduces the load on the observer for big VssSpecifications. We wait for the initial update // of all VssProperties before notifying the observer about the first batch private val initialSubscriptionUpdates = vssPaths.associateWith { false }.toMutableMap() diff --git a/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/subscription/Subscription.kt b/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/subscription/Subscription.kt index 56419740..4d4cd6e9 100644 --- a/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/subscription/Subscription.kt +++ b/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/subscription/Subscription.kt @@ -50,7 +50,7 @@ internal class Subscription( for (entryUpdate in lastSubscribeResponse.updatesList) { val entry = entryUpdate.entry - observer.onPropertyChanged(vssPath, field, entry) + observer.onPropertyChanged(entry.path, field, entry) } } }, From b571fde8ec0fabe75d96769e65aaa53b9550d51a Mon Sep 17 00:00:00 2001 From: Andre Weber Date: Thu, 7 Dec 2023 08:42:25 +0100 Subject: [PATCH 2/6] test: Fix Tests --- .../eclipse/kuksa/DataBrokerConnectionTest.kt | 8 +- .../kuksa/vssSpecification/VssDriver.kt | 355 ++++++++++++------ .../VssSpecificationCopyTest.kt | 2 +- .../kuksa/vssSpecification/VssVehicle.kt | 120 ++++++ 4 files changed, 368 insertions(+), 117 deletions(-) create mode 100644 kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/vssSpecification/VssVehicle.kt diff --git a/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/DataBrokerConnectionTest.kt b/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/DataBrokerConnectionTest.kt index 9a936ebd..e00a7dff 100644 --- a/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/DataBrokerConnectionTest.kt +++ b/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/DataBrokerConnectionTest.kt @@ -159,10 +159,10 @@ class DataBrokerConnectionTest : BehaviorSpec({ dataBrokerConnection.subscribe(specification, listener = specificationListener) then("The #onSpecificationChanged method is triggered") { - verify( - timeout = 100L, - exactly = 1, - ) { specificationListener.onSpecificationChanged(any()) } + val capturingList = mutableListOf() + verify(timeout = 100L, exactly = 1) { + specificationListener.onSpecificationChanged(capture(capturingList)) + } } and("The initial value is different from the default for a child") { diff --git a/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/vssSpecification/VssDriver.kt b/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/vssSpecification/VssDriver.kt index 8d87ab8f..6d3f6999 100644 --- a/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/vssSpecification/VssDriver.kt +++ b/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/vssSpecification/VssDriver.kt @@ -23,137 +23,268 @@ import org.eclipse.kuksa.vsscore.model.VssProperty import org.eclipse.kuksa.vsscore.model.VssSpecification import kotlin.reflect.KClass -data class VssVehicle( - val driver: VssDriver = VssDriver(), - val passenger: VssPassenger = VssPassenger(), - val body: VssBody = VssBody(), - override val uuid: String = "Vehicle", - override val vssPath: String = "Vehicle", - override val description: String = "High-level vehicle data.", - override val type: String = "branch", - override val comment: String = "", +data class VssDriver @JvmOverloads constructor( + val attentiveProbability: VssAttentiveProbability = VssAttentiveProbability(), + val distractionLevel: VssDistractionLevel = VssDistractionLevel(), + val fatigueLevel: VssFatigueLevel = VssFatigueLevel(), + val heartRate: VssHeartRate = VssHeartRate(), + val identifier: VssIdentifier = VssIdentifier(), + val isEyesOnRoad: VssIsEyesOnRoad = VssIsEyesOnRoad(), + val isHandsOnWheel: VssIsHandsOnWheel = VssIsHandsOnWheel(), ) : VssSpecification { + override val comment: String + get() = "" + + override val description: String + get() = "Driver data." + + override val type: String + get() = "branch" + + override val uuid: String + get() = "1cac57e7b7e756dc8a154eaacbce6426" + + override val vssPath: String + get() = "Vehicle.Driver" + override val children: Set - get() = setOf(driver, passenger, body) + get() = setOf( + attentiveProbability, + distractionLevel, + fatigueLevel, + heartRate, + identifier, + isEyesOnRoad, + isHandsOnWheel, + ) + + override val parentClass: KClass<*>? + get() = null + + data class VssHeartRate @JvmOverloads constructor( + override val `value`: Int = 0, + ) : VssProperty { + override val comment: String + get() = "" + + override val description: String + get() = "Heart rate of the driver." + + override val type: String + get() = "sensor" + + override val uuid: String + get() = "d71516905f785c4da867a2f86e774d93" + + override val vssPath: String + get() = "Vehicle.Driver.HeartRate" + + override val children: Set + get() = setOf() + + override val parentClass: KClass<*> + get() = VssVehicle::class + } } -data class VssBody( - override val uuid: String = "Body", - override val vssPath: String = "Vehicle.Body", - override val description: String = "All body components.", - override val type: String = "branch", - override val comment: String = "", -) : VssSpecification { +data class VssAttentiveProbability @JvmOverloads constructor( + override val `value`: Float = 0f, +) : VssProperty { + override val comment: String + get() = "" + + override val description: String + get() = "Probability of attentiveness of the driver." + + override val type: String + get() = "sensor" + + override val uuid: String + get() = "fcd202467afb533fbbf9e7da89cc1cee" + + override val vssPath: String + get() = "Vehicle.Driver.AttentiveProbability" + + override val children: Set + get() = setOf() + override val parentClass: KClass<*> get() = VssVehicle::class } -data class VssDriver( - val heartRate: VssHeartRate = VssHeartRate(), - val isEyesOnRoad: VssIsEyesOnRoad = VssIsEyesOnRoad(), - override val uuid: String = "Driver", - override val vssPath: String = "Vehicle.Driver", - override val description: String = "Driver data.", - override val type: String = "branch", - override val comment: String = "", -) : VssSpecification { +data class VssDistractionLevel @JvmOverloads constructor( + override val `value`: Float = 0f, +) : VssProperty { + override val comment: String + get() = "" + + override val description: String + get() = "Distraction level of the driver" + + override val type: String + get() = "sensor" + + override val uuid: String + get() = "cb35ec0b924e58979e1469146d65c3fa" + + override val vssPath: String + get() = "Vehicle.Driver.DistractionLevel" + override val children: Set - get() = setOf(heartRate, isEyesOnRoad) + get() = setOf() + override val parentClass: KClass<*> get() = VssVehicle::class +} - data class VssHeartRate( - override val uuid: String = "Driver HeartRate", - override val vssPath: String = "Vehicle.Driver.HeartRate", - override val description: String = "Heart rate of the driver.", - override val type: String = "sensor", - override val comment: String = "", - override val value: Int = 100, - ) : VssProperty { - override val parentClass: KClass<*> - get() = VssDriver::class - } +data class VssFatigueLevel @JvmOverloads constructor( + override val `value`: Float = 0f, +) : VssProperty { + override val comment: String + get() = "" - data class VssIsEyesOnRoad( - override val uuid: String = "Driver IsEyesOnRoad", - override val vssPath: String = "Vehicle.Driver.IsEyesOnRoad", - override val description: String = "Has driver the eyes on road or not?", - override val type: String = "sensor", - override val comment: String = "", - override val value: Boolean = true, - ) : VssProperty { - override val parentClass: KClass<*> - get() = VssDriver::class - } + override val description: String + get() = "Fatigue level of the driver" + + override val type: String + get() = "sensor" + + override val uuid: String + get() = "49b1626295705a79ae20d8a270c48b6b" + + override val vssPath: String + get() = "Vehicle.Driver.FatigueLevel" + + override val children: Set + get() = setOf() + + override val parentClass: KClass<*> + get() = VssVehicle::class } -data class VssPassenger( - val heartRate: VssHeartRate = VssHeartRate(), - override val uuid: String = "Passenger", - override val vssPath: String = "Vehicle.Passenger", - override val description: String = "Passenger data", - override val type: String = "branch", - override val comment: String = "", +data class VssIdentifier @JvmOverloads constructor( + val issuer: VssIssuer = VssIssuer(), + val subject: VssSubject = VssSubject(), ) : VssSpecification { + override val comment: String + get() = "" + + override val description: String + get() = "Identifier attributes based on OAuth 2.0." + + override val type: String + get() = "branch" + + override val uuid: String + get() = "89705397069c5ec58d607318f2ff0ea8" + + override val vssPath: String + get() = "Vehicle.Driver.Identifier" + override val children: Set - get() = setOf(heartRate) + get() = setOf(issuer, subject) + override val parentClass: KClass<*> get() = VssVehicle::class +} - data class VssHeartRate( - override val uuid: String = "Passenger HeartRate", - override val vssPath: String = "Vehicle.Passenger.HeartRate", - override val description: String = "Heart rate of the Passenger.", - override val type: String = "sensor", - override val comment: String = "", - override val value: Int = 80, - ) : VssProperty { - override val parentClass: KClass<*> - get() = VssPassenger::class - } +data class VssIssuer @JvmOverloads constructor( + override val `value`: String = "", +) : VssProperty { + override val comment: String + get() = "" + + override val description: String + get() = + "Unique Issuer for the authentication of the occupant e.g. https://accounts.funcorp.com." + + override val type: String + get() = "sensor" + + override val uuid: String + get() = "ee7988d26d7156d2a030ecc506ea97e7" + + override val vssPath: String + get() = "Vehicle.Driver.Identifier.Issuer" + + override val children: Set + get() = setOf() + + override val parentClass: KClass<*> + get() = VssIdentifier::class } -data class VssValueInt( - override val uuid: String = "Value", - override val vssPath: String = "Value", - override val description: String = "", - override val type: String = "sensor", - override val comment: String = "", - override val value: Int = 0, -) : VssProperty - -data class VssValueFloat( - override val uuid: String = "Value", - override val vssPath: String = "Value", - override val description: String = "", - override val type: String = "sensor", - override val comment: String = "", - override val value: Float = 0f, -) : VssProperty - -data class VssValueDouble( - override val uuid: String = "Value", - override val vssPath: String = "Value", - override val description: String = "", - override val type: String = "sensor", - override val comment: String = "", - override val value: Double = 0.0, -) : VssProperty - -data class VssValueLong( - override val uuid: String = "Value", - override val vssPath: String = "Value", - override val description: String = "", - override val type: String = "sensor", - override val comment: String = "", - override val value: Long = 0L, -) : VssProperty - -data class VssInvalid( - override val uuid: String = "Invalid", - override val vssPath: String = "Invalid", - override val description: String = "", - override val type: String = "", - override val comment: String = "", - override val value: Exception = Exception(), -) : VssProperty +data class VssSubject @JvmOverloads constructor( + override val `value`: String = "", +) : VssProperty { + override val comment: String + get() = "" + + override val description: String + get() = "Subject for the authentication of the occupant e.g. UserID 7331677." + + override val type: String + get() = "sensor" + + override val uuid: String + get() = "b41ec688af265f10824bc9635989ac55" + + override val vssPath: String + get() = "Vehicle.Driver.Identifier.Subject" + + override val children: Set + get() = setOf() + + override val parentClass: KClass<*> + get() = VssIdentifier::class +} + +data class VssIsEyesOnRoad @JvmOverloads constructor( + override val `value`: Boolean = false, +) : VssProperty { + override val comment: String + get() = "" + + override val description: String + get() = "Has driver the eyes on road or not?" + + override val type: String + get() = "sensor" + + override val uuid: String + get() = "625e5009f1145aa0b797ee6c335ca2fe" + + override val vssPath: String + get() = "Vehicle.Driver.IsEyesOnRoad" + + override val children: Set + get() = setOf() + + override val parentClass: KClass<*> + get() = VssVehicle::class +} + +data class VssIsHandsOnWheel @JvmOverloads constructor( + override val `value`: Boolean = false, +) : VssProperty { + override val comment: String + get() = "" + + override val description: String + get() = "Are the driver's hands on the steering wheel or not?" + + override val type: String + get() = "sensor" + + override val uuid: String + get() = "90d7dc2c408c528d941829ff88075f24" + + override val vssPath: String + get() = "Vehicle.Driver.IsHandsOnWheel" + + override val children: Set + get() = setOf() + + override val parentClass: KClass<*> + get() = VssVehicle::class +} diff --git a/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/vssSpecification/VssSpecificationCopyTest.kt b/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/vssSpecification/VssSpecificationCopyTest.kt index 7965782f..8499acd2 100644 --- a/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/vssSpecification/VssSpecificationCopyTest.kt +++ b/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/vssSpecification/VssSpecificationCopyTest.kt @@ -58,7 +58,7 @@ class VssSpecificationCopyTest : BehaviorSpec({ val invertedSpecification = !vehicle.driver.isEyesOnRoad then("it should return a copy with the inverted value") { - invertedSpecification.value shouldBe false + invertedSpecification.value shouldBe true } } diff --git a/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/vssSpecification/VssVehicle.kt b/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/vssSpecification/VssVehicle.kt new file mode 100644 index 00000000..6abebf3d --- /dev/null +++ b/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/vssSpecification/VssVehicle.kt @@ -0,0 +1,120 @@ +/* + * Copyright (c) 2023 Contributors to the Eclipse Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * + */ + +package org.eclipse.kuksa.vssSpecification + +import org.eclipse.kuksa.vsscore.model.VssProperty +import org.eclipse.kuksa.vsscore.model.VssSpecification +import kotlin.reflect.KClass + +data class VssVehicle( + val driver: VssDriver = VssDriver(), + val passenger: VssPassenger = VssPassenger(), + val body: VssBody = VssBody(), + override val uuid: String = "Vehicle", + override val vssPath: String = "Vehicle", + override val description: String = "High-level vehicle data.", + override val type: String = "branch", + override val comment: String = "", +) : VssSpecification { + override val children: Set + get() = setOf(driver, passenger, body) +} + +data class VssBody( + override val uuid: String = "Body", + override val vssPath: String = "Vehicle.Body", + override val description: String = "All body components.", + override val type: String = "branch", + override val comment: String = "", +) : VssSpecification { + override val parentClass: KClass<*> + get() = VssVehicle::class +} + +data class VssPassenger( + val heartRate: VssHeartRate = VssHeartRate(), + override val uuid: String = "Passenger", + override val vssPath: String = "Vehicle.Passenger", + override val description: String = "Passenger data", + override val type: String = "branch", + override val comment: String = "", +) : VssSpecification { + override val children: Set + get() = setOf(heartRate) + override val parentClass: KClass<*> + get() = VssVehicle::class + + data class VssHeartRate( + override val uuid: String = "Passenger HeartRate", + override val vssPath: String = "Vehicle.Passenger.HeartRate", + override val description: String = "Heart rate of the Passenger.", + override val type: String = "sensor", + override val comment: String = "", + override val value: Int = 80, + ) : VssProperty { + override val parentClass: KClass<*> + get() = VssPassenger::class + } +} + +data class VssValueInt( + override val uuid: String = "Value", + override val vssPath: String = "Value", + override val description: String = "", + override val type: String = "sensor", + override val comment: String = "", + override val value: Int = 0, +) : VssProperty + +data class VssValueFloat( + override val uuid: String = "Value", + override val vssPath: String = "Value", + override val description: String = "", + override val type: String = "sensor", + override val comment: String = "", + override val value: Float = 0f, +) : VssProperty + +data class VssValueDouble( + override val uuid: String = "Value", + override val vssPath: String = "Value", + override val description: String = "", + override val type: String = "sensor", + override val comment: String = "", + override val value: Double = 0.0, +) : VssProperty + +data class VssValueLong( + override val uuid: String = "Value", + override val vssPath: String = "Value", + override val description: String = "", + override val type: String = "sensor", + override val comment: String = "", + override val value: Long = 0L, +) : VssProperty + +data class VssInvalid( + override val uuid: String = "Invalid", + override val vssPath: String = "Invalid", + override val description: String = "", + override val type: String = "", + override val comment: String = "", + override val value: Exception = Exception(), +) : VssProperty From bb9247692ee021c439f076cb80fd955f66ce9a8e Mon Sep 17 00:00:00 2001 From: Andre Weber Date: Wed, 31 Jan 2024 11:51:21 +0100 Subject: [PATCH 3/6] chore: Improve PropertyListener for Wildcard Subscribes BREAKING CHANGE: - Method PropertyListener#onPropertyChanged(String, Field, DataEntry) was removed - Method PropertyListener#onPropertyChanged(List) was added - Previous DataEntry can be retrieved via EntryUpdate#entry - Subscribing to a branch will initially send out an update containing a list of all leafs --- .../kuksa/testapp/KuksaDataBrokerActivity.kt | 7 +- gradle.properties | 1 + .../eclipse/kuksa/DataBrokerTransporter.kt | 8 +- .../org/eclipse/kuksa/PropertyListener.kt | 11 ++- .../subscription/DataBrokerSubscriber.kt | 11 +-- .../SpecificationPropertyListener.kt | 33 ++----- .../kuksa/subscription/Subscription.kt | 5 +- .../eclipse/kuksa/DataBrokerConnectionTest.kt | 19 ++-- .../kuksa/DataBrokerTransporterTest.kt | 2 +- .../DataBrokerTransporterExtensions.kt | 19 ++++ .../subscription/DataBrokerSubscriberTest.kt | 95 ++++++++++++++++--- .../java/com/example/sample/JavaActivity.java | 18 +++- .../com/example/sample/KotlinActivity.kt | 15 ++- 13 files changed, 157 insertions(+), 87 deletions(-) diff --git a/app/src/main/kotlin/org/eclipse/kuksa/testapp/KuksaDataBrokerActivity.kt b/app/src/main/kotlin/org/eclipse/kuksa/testapp/KuksaDataBrokerActivity.kt index 5f72b44b..8c28a6bd 100644 --- a/app/src/main/kotlin/org/eclipse/kuksa/testapp/KuksaDataBrokerActivity.kt +++ b/app/src/main/kotlin/org/eclipse/kuksa/testapp/KuksaDataBrokerActivity.kt @@ -40,7 +40,6 @@ import org.eclipse.kuksa.model.Property import org.eclipse.kuksa.proto.v1.KuksaValV1 import org.eclipse.kuksa.proto.v1.KuksaValV1.GetResponse import org.eclipse.kuksa.proto.v1.Types -import org.eclipse.kuksa.proto.v1.Types.DataEntry import org.eclipse.kuksa.proto.v1.Types.Datapoint import org.eclipse.kuksa.proto.v1.Types.Field import org.eclipse.kuksa.testapp.databroker.DataBrokerEngine @@ -94,9 +93,9 @@ class KuksaDataBrokerActivity : ComponentActivity() { } private val propertyListener = object : PropertyListener { - override fun onPropertyChanged(vssPath: String, field: Field, updatedValue: DataEntry) { - Log.d(TAG, "onPropertyChanged path: vssPath = $vssPath, field = $field, changedValue = $updatedValue") - outputViewModel.addOutputEntry("Updated value: $updatedValue") + override fun onPropertyChanged(entryUpdates: List) { + Log.d(TAG, "onPropertyChanged() called with: updatedValues = $entryUpdates") + outputViewModel.addOutputEntry("Updated value: $entryUpdates") } override fun onError(throwable: Throwable) { diff --git a/gradle.properties b/gradle.properties index 7572ceb8..2574d3c5 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,6 +6,7 @@ android.useAndroidX=true kotlin.code.style=official org.gradle.jvmargs=-Xmx2048m +kotlin.daemon.jvmargs=-Xmx2048m # When using compose + ksp the incremental compiler should be disabled: https://issuetracker.google.com/issues/207185051 ksp.incremental=false diff --git a/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/DataBrokerTransporter.kt b/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/DataBrokerTransporter.kt index ecea7043..554ca496 100644 --- a/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/DataBrokerTransporter.kt +++ b/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/DataBrokerTransporter.kt @@ -149,12 +149,8 @@ internal class DataBrokerTransporter( val subscription = Subscription(vssPath, field, cancellableContext) val streamObserver = object : StreamObserver { override fun onNext(value: SubscribeResponse) { - for (entryUpdate in value.updatesList) { - val entry = entryUpdate.entry - - subscription.listeners.forEach { observer -> - observer.onPropertyChanged(entry.path, field, entry) - } + subscription.listeners.forEach { observer -> + observer.onPropertyChanged(value.updatesList) } subscription.lastSubscribeResponse = value diff --git a/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/PropertyListener.kt b/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/PropertyListener.kt index 951ffb4d..50f0af6a 100644 --- a/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/PropertyListener.kt +++ b/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/PropertyListener.kt @@ -20,18 +20,19 @@ package org.eclipse.kuksa import org.eclipse.kuksa.pattern.listener.Listener -import org.eclipse.kuksa.proto.v1.Types.DataEntry -import org.eclipse.kuksa.proto.v1.Types.Field +import org.eclipse.kuksa.proto.v1.KuksaValV1 import org.eclipse.kuksa.vsscore.model.VssSpecification /** - * The Listener is used to notify about changes to subscribed properties. + * The Listener is used to notify about changes to subscribed properties. When registering the listener to + * Vehicle.ADAS.ABS this listener will also be notified about changes of sub-properties e.g. Vehicle.ADAS.ABS.IsEnabled + * or Vehicle.ADAS.ABS.IsEngaged. */ interface PropertyListener : Listener { /** - * Will be triggered with the [updatedValue] when the underlying [field] of the [vssPath] changed it's value. + * Will be triggered with a list of [entryUpdates] of the corresponding field. */ - fun onPropertyChanged(vssPath: String, field: Field, updatedValue: DataEntry) + fun onPropertyChanged(entryUpdates: List) /** * Will be triggered when an error happens during subscription and forwards the [throwable]. diff --git a/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/subscription/DataBrokerSubscriber.kt b/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/subscription/DataBrokerSubscriber.kt index f2e26c08..f95678fe 100644 --- a/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/subscription/DataBrokerSubscriber.kt +++ b/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/subscription/DataBrokerSubscriber.kt @@ -29,7 +29,6 @@ import org.eclipse.kuksa.proto.v1.Types import org.eclipse.kuksa.proto.v1.Types.Field import org.eclipse.kuksa.vsscore.model.VssProperty import org.eclipse.kuksa.vsscore.model.VssSpecification -import org.eclipse.kuksa.vsscore.model.vssProperties /** * Creates [Subscription]s to the DataBroker to get notified about changes on the underlying vssPaths and fields. @@ -93,10 +92,7 @@ internal class DataBrokerSubscriber(private val dataBrokerTransporter: DataBroke ) { val vssPath = specification.vssPath - val leafProperties = specification.vssProperties - val childPropertiesPaths = leafProperties.map { it.vssPath } - - val specificationPropertyListener = SpecificationPropertyListener(specification, childPropertiesPaths, listener) + val specificationPropertyListener = SpecificationPropertyListener(specification, listener) subscribe(vssPath, field, specificationPropertyListener) } @@ -113,10 +109,7 @@ internal class DataBrokerSubscriber(private val dataBrokerTransporter: DataBroke ) { val vssPath = specification.vssPath - val leafProperties = specification.vssProperties - val childPropertiesPaths = leafProperties.map { it.vssPath } - - val specificationPropertyListener = SpecificationPropertyListener(specification, childPropertiesPaths, listener) + val specificationPropertyListener = SpecificationPropertyListener(specification, listener) unsubscribe(vssPath, field, specificationPropertyListener) } diff --git a/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/subscription/SpecificationPropertyListener.kt b/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/subscription/SpecificationPropertyListener.kt index 7c95ec67..b93dfe65 100644 --- a/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/subscription/SpecificationPropertyListener.kt +++ b/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/subscription/SpecificationPropertyListener.kt @@ -19,48 +19,27 @@ package org.eclipse.kuksa.subscription -import android.util.Log -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.runBlocking import org.eclipse.kuksa.PropertyListener import org.eclipse.kuksa.VssSpecificationListener -import org.eclipse.kuksa.extension.TAG import org.eclipse.kuksa.extension.copy -import org.eclipse.kuksa.proto.v1.Types +import org.eclipse.kuksa.proto.v1.KuksaValV1 import org.eclipse.kuksa.vsscore.model.VssSpecification internal class SpecificationPropertyListener( specification: T, - vssPaths: Collection, private val listener: VssSpecificationListener, ) : PropertyListener { - // Reduces the load on the observer for big VssSpecifications. We wait for the initial update - // of all VssProperties before notifying the observer about the first batch - private val initialSubscriptionUpdates = vssPaths.associateWith { false }.toMutableMap() - // This is currently needed because we get multiple subscribe responses for every heir. Otherwise we // would override the last heir value with every new response. private var updatedVssSpecification: T = specification - // Multiple onPropertyChanged updates from different threads may be called. The updatedVssSpecification must be - // in sync however. Calling the .copy in a blocking context is necessary for this. - @OptIn(ExperimentalCoroutinesApi::class) - private val specificationUpdateContext = Dispatchers.IO.limitedParallelism(1) - - override fun onPropertyChanged(vssPath: String, field: Types.Field, updatedValue: Types.DataEntry) { - Log.d(TAG, "Update from subscribed property: $vssPath - $field: ${updatedValue.value}") - - runBlocking(specificationUpdateContext) { - updatedVssSpecification = updatedVssSpecification.copy(vssPath, updatedValue.value) + override fun onPropertyChanged(entryUpdates: List) { + entryUpdates.forEach { entryUpdate -> + val dataEntry = entryUpdate.entry + updatedVssSpecification = updatedVssSpecification.copy(dataEntry.path, dataEntry.value) } - initialSubscriptionUpdates[vssPath] = true - val isInitialSubscriptionComplete = initialSubscriptionUpdates.values.all { it } - if (isInitialSubscriptionComplete) { - Log.d(TAG, "Update for subscribed specification complete: ${updatedVssSpecification.vssPath}") - listener.onSpecificationChanged(updatedVssSpecification) - } + listener.onSpecificationChanged(updatedVssSpecification) } override fun onError(throwable: Throwable) { diff --git a/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/subscription/Subscription.kt b/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/subscription/Subscription.kt index 4d4cd6e9..6318bfab 100644 --- a/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/subscription/Subscription.kt +++ b/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/subscription/Subscription.kt @@ -48,10 +48,7 @@ internal class Subscription( } else { val lastSubscribeResponse = lastSubscribeResponse ?: return@MultiListener - for (entryUpdate in lastSubscribeResponse.updatesList) { - val entry = entryUpdate.entry - observer.onPropertyChanged(entry.path, field, entry) - } + observer.onPropertyChanged(lastSubscribeResponse.updatesList) } }, ) diff --git a/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/DataBrokerConnectionTest.kt b/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/DataBrokerConnectionTest.kt index e00a7dff..e2ae4bd2 100644 --- a/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/DataBrokerConnectionTest.kt +++ b/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/DataBrokerConnectionTest.kt @@ -32,6 +32,7 @@ import io.mockk.verify import kotlinx.coroutines.runBlocking import org.eclipse.kuksa.databroker.DataBrokerConnectorProvider import org.eclipse.kuksa.model.Property +import org.eclipse.kuksa.proto.v1.KuksaValV1 import org.eclipse.kuksa.proto.v1.Types import org.eclipse.kuksa.proto.v1.Types.Datapoint import org.eclipse.kuksa.test.kotest.Integration @@ -48,17 +49,23 @@ class DataBrokerConnectionTest : BehaviorSpec({ val dataBrokerConnection = connectToDataBrokerBlocking() and("A Property with a valid VSS Path") { + val vssPath = "Vehicle.Acceleration.Lateral" val fields = listOf(Types.Field.FIELD_VALUE) - val property = Property("Vehicle.Acceleration.Lateral", fields) + val property = Property(vssPath, fields) `when`("Subscribing to the Property") { val propertyListener = mockk(relaxed = true) dataBrokerConnection.subscribe(property, propertyListener) then("The #onPropertyChanged method is triggered") { + val capturingSlot = slot>() verify(timeout = 100L) { - propertyListener.onPropertyChanged(any(), any(), any()) + propertyListener.onPropertyChanged(capture(capturingSlot)) } + + val entryUpdates = capturingSlot.captured + entryUpdates.size shouldBe 1 + entryUpdates[0].entry.path shouldBe vssPath } `when`("The observed Property changes") { @@ -70,14 +77,14 @@ class DataBrokerConnectionTest : BehaviorSpec({ dataBrokerConnection.update(property, datapoint) then("The #onPropertyChanged callback is triggered with the new value") { - val capturingSlot = slot() + val capturingSlot = slot>() verify(timeout = 100) { - propertyListener.onPropertyChanged(any(), any(), capture(capturingSlot)) + propertyListener.onPropertyChanged(capture(capturingSlot)) } - val dataEntry = capturingSlot.captured - val capturedDatapoint = dataEntry.value + val entryUpdates = capturingSlot.captured + val capturedDatapoint = entryUpdates[0].entry.value val float = capturedDatapoint.float assertEquals(newValue, float, 0.0001f) diff --git a/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/DataBrokerTransporterTest.kt b/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/DataBrokerTransporterTest.kt index 519b260b..ee2d9191 100644 --- a/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/DataBrokerTransporterTest.kt +++ b/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/DataBrokerTransporterTest.kt @@ -111,7 +111,7 @@ class DataBrokerTransporterTest : BehaviorSpec({ then("The PropertyListener should be notified") { verify { - propertyListener.onPropertyChanged(vssPath, Types.Field.FIELD_VALUE, any()) + propertyListener.onPropertyChanged(any()) } } } diff --git a/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/extensions/DataBrokerTransporterExtensions.kt b/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/extensions/DataBrokerTransporterExtensions.kt index 00678135..8fc91105 100644 --- a/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/extensions/DataBrokerTransporterExtensions.kt +++ b/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/extensions/DataBrokerTransporterExtensions.kt @@ -60,3 +60,22 @@ internal suspend fun DataBrokerTransporter.updateRandomUint32Value( return randomValue } + +internal suspend fun DataBrokerTransporter.toggleBoolean(vssPath: String): Boolean { + val fields = listOf(Types.Field.FIELD_VALUE) + + var newBoolean: Boolean? = null + try { + val response = fetch(vssPath, fields) + val currentBool = response.entriesList[0].value.bool + + newBoolean = !currentBool + val newDatapoint = Types.Datapoint.newBuilder().setBool(newBoolean).build() + + update(vssPath, fields, newDatapoint) + } catch (e: Exception) { + fail("Updating $vssPath to $newBoolean failed: $e") + } + + return newBoolean == true +} diff --git a/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/subscription/DataBrokerSubscriberTest.kt b/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/subscription/DataBrokerSubscriberTest.kt index 44c86f03..bcc08763 100644 --- a/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/subscription/DataBrokerSubscriberTest.kt +++ b/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/subscription/DataBrokerSubscriberTest.kt @@ -30,12 +30,13 @@ import org.eclipse.kuksa.DataBrokerTransporter import org.eclipse.kuksa.PropertyListener import org.eclipse.kuksa.VssSpecificationListener import org.eclipse.kuksa.databroker.DataBrokerConnectorProvider +import org.eclipse.kuksa.extensions.toggleBoolean import org.eclipse.kuksa.extensions.updateRandomFloatValue import org.eclipse.kuksa.extensions.updateRandomUint32Value import org.eclipse.kuksa.pattern.listener.MultiListener import org.eclipse.kuksa.pattern.listener.count +import org.eclipse.kuksa.proto.v1.KuksaValV1 import org.eclipse.kuksa.proto.v1.Types -import org.eclipse.kuksa.proto.v1.Types.DataEntry import org.eclipse.kuksa.test.kotest.Insecure import org.eclipse.kuksa.test.kotest.Integration import org.eclipse.kuksa.vssSpecification.VssDriver @@ -53,6 +54,55 @@ class DataBrokerSubscriberTest : BehaviorSpec({ DataBrokerTransporter(dataBrokerConnectorProvider.managedChannel) val classUnderTest = DataBrokerSubscriber(databrokerTransporter) + `when`("Subscribing to VSS_PATH (Branch) 'Vehicle.ADAS.ABS'") { + val vssPath = "Vehicle.ADAS.ABS" + val fieldValue = Types.Field.FIELD_VALUE + val propertyListener = mockk(relaxed = true) + classUnderTest.subscribe(vssPath, fieldValue, propertyListener) + + then("The PropertyListener should send out ONE update containing ALL children") { + val entryUpdatesList = mutableListOf>() + verify(timeout = 100, exactly = 1) { + propertyListener.onPropertyChanged(capture(entryUpdatesList)) + } + + val entryUpdates = entryUpdatesList[0] + entryUpdatesList.size shouldBe 1 // ONE update + entryUpdates.size shouldBe 3 // all children: IsEnabled, IsEngaged, IsError + entryUpdates.all { it.entry.path.startsWith(vssPath) } shouldBe true + } + + `when`("Any child changes it's value") { + clearMocks(propertyListener) + val vssPathIsError = "Vehicle.ADAS.ABS.IsError" + val newValueIsError = databrokerTransporter.toggleBoolean(vssPathIsError) + val vssPathIsEngaged = "Vehicle.ADAS.ABS.IsEngaged" + val newValueIsEngaged = databrokerTransporter.toggleBoolean(vssPathIsEngaged) + delay(50) + + then("The PropertyListener should be notified about it") { + val entryUpdatesList = mutableListOf>() + verify(timeout = 100, exactly = 2) { + propertyListener.onPropertyChanged(capture(entryUpdatesList)) + } + entryUpdatesList.size shouldBe 2 + val entryUpdates = entryUpdatesList.flatten() + entryUpdates.count { + val path = it.entry.path + val entry = it.entry + val value = entry.value + path == vssPathIsError && value.bool == newValueIsError + } shouldBe 1 + entryUpdates.count { + val path = it.entry.path + val entry = it.entry + val value = entry.value + path == vssPathIsEngaged && value.bool == newValueIsEngaged + } shouldBe 1 + } + } + } + `when`("Subscribing using VSS_PATH to Vehicle.Speed with FIELD_VALUE") { val vssPath = "Vehicle.Speed" val fieldValue = Types.Field.FIELD_VALUE @@ -64,26 +114,41 @@ class DataBrokerSubscriberTest : BehaviorSpec({ then("The PropertyListener is notified about the change") { verify(timeout = 100L, exactly = 2) { - propertyListener.onPropertyChanged(vssPath, fieldValue, any()) + propertyListener.onPropertyChanged(any()) } } } `when`("Subscribing the same PropertyListener to a different vssPath") { + clearMocks(propertyListener) val otherVssPath = "Vehicle.ADAS.CruiseControl.SpeedSet" classUnderTest.subscribe(otherVssPath, fieldValue, propertyListener) and("Both values are updated") { - databrokerTransporter.updateRandomFloatValue(vssPath) - databrokerTransporter.updateRandomFloatValue(otherVssPath) + val updatedValueVssPath = databrokerTransporter.updateRandomFloatValue(vssPath) + val updatedValueOtherVssPath = databrokerTransporter.updateRandomFloatValue(otherVssPath) + delay(50) then("The Observer is notified about both changes") { + val entryUpdatesList = mutableListOf>() verify(timeout = 100L, exactly = 3) { - propertyListener.onPropertyChanged(vssPath, fieldValue, any()) - } - verify(timeout = 100L, exactly = 2) { - propertyListener.onPropertyChanged(otherVssPath, fieldValue, any()) + propertyListener.onPropertyChanged(capture(entryUpdatesList)) } + val entryUpdates = entryUpdatesList.flatten() + entryUpdates + .count { + val path = it.entry.path + val entry = it.entry + val value = entry.value + path == vssPath && value.float == updatedValueVssPath + } shouldBe 1 + entryUpdates + .count { + val path = it.entry.path + val entry = it.entry + val value = entry.value + path == otherVssPath && value.float == updatedValueOtherVssPath + } shouldBe 1 } } } @@ -102,13 +167,13 @@ class DataBrokerSubscriberTest : BehaviorSpec({ then("Each PropertyListener is only notified once") { propertyListenerMocks.forEach { propertyListenerMock -> - val dataEntries = mutableListOf() + val dataEntries = mutableListOf>() verify(timeout = 100L, exactly = 2) { - propertyListenerMock.onPropertyChanged(vssPath, fieldValue, capture(dataEntries)) + propertyListenerMock.onPropertyChanged(capture(dataEntries)) } - val count = dataEntries.count { it.value.float == randomFloatValue } + val count = dataEntries.count { it[0].entry.value.float == randomFloatValue } count shouldBe 1 } } @@ -125,7 +190,7 @@ class DataBrokerSubscriberTest : BehaviorSpec({ then("The PropertyListener is not notified") { verify(exactly = 0) { - propertyListener.onPropertyChanged(vssPath, fieldValue, any()) + propertyListener.onPropertyChanged(any()) } } } @@ -143,13 +208,13 @@ class DataBrokerSubscriberTest : BehaviorSpec({ val randomFloatValue = databrokerTransporter.updateRandomFloatValue(vssPath) then("The PropertyListener is only notified once") { - val dataEntries = mutableListOf() + val dataEntries = mutableListOf>() verify(timeout = 100L, exactly = 2) { - propertyListenerMock.onPropertyChanged(vssPath, fieldValue, capture(dataEntries)) + propertyListenerMock.onPropertyChanged(capture(dataEntries)) } - val count = dataEntries.count { it.value.float == randomFloatValue } + val count = dataEntries.count { it[0].entry.value.float == randomFloatValue } count shouldBe 1 } } diff --git a/samples/src/main/java/com/example/sample/JavaActivity.java b/samples/src/main/java/com/example/sample/JavaActivity.java index 1ac41418..01acaf54 100644 --- a/samples/src/main/java/com/example/sample/JavaActivity.java +++ b/samples/src/main/java/com/example/sample/JavaActivity.java @@ -39,6 +39,7 @@ import java.io.InputStream; import java.util.Collection; import java.util.Collections; +import java.util.List; import javax.annotation.Nullable; @@ -165,11 +166,18 @@ public void subscribeProperty(Property property) { dataBrokerConnection.subscribe(property, new PropertyListener() { @Override - public void onPropertyChanged( - @NonNull String vssPath, - @NonNull Types.Field field, - @NonNull Types.DataEntry updatedValue) { - // handle result + public void onPropertyChanged(@NonNull List entryUpdates) { + for (KuksaValV1.EntryUpdate entryUpdate : entryUpdates) { + Types.DataEntry updatedValue = entryUpdate.getEntry(); + + // handle property change + //noinspection SwitchStatementWithTooFewBranches + switch (updatedValue.getPath()) { + case "VSS.Speed": + float speed = updatedValue.getValue().getFloat(); + + } + } } @Override diff --git a/samples/src/main/kotlin/com/example/sample/KotlinActivity.kt b/samples/src/main/kotlin/com/example/sample/KotlinActivity.kt index 4d4c3061..297e08cf 100644 --- a/samples/src/main/kotlin/com/example/sample/KotlinActivity.kt +++ b/samples/src/main/kotlin/com/example/sample/KotlinActivity.kt @@ -33,6 +33,7 @@ import org.eclipse.kuksa.DisconnectListener import org.eclipse.kuksa.PropertyListener import org.eclipse.kuksa.VssSpecificationListener import org.eclipse.kuksa.model.Property +import org.eclipse.kuksa.proto.v1.KuksaValV1 import org.eclipse.kuksa.proto.v1.Types import org.eclipse.kuksa.proto.v1.Types.Datapoint import org.eclipse.kuksa.vss.VssVehicle @@ -131,11 +132,15 @@ class KotlinActivity : AppCompatActivity() { fun subscribeProperty(property: Property) { val propertyListener = object : PropertyListener { - override fun onPropertyChanged(vssPath: String, field: Types.Field, updatedValue: Types.DataEntry) { - // handle property change - when (vssPath) { - "Vehicle.Speed" -> { - val speed = updatedValue.value.float + override fun onPropertyChanged(entryUpdates: List) { + entryUpdates.forEach { entryUpdate -> + val updatedValue = entryUpdate.entry + + // handle property change + when (updatedValue.path) { + "Vehicle.Speed" -> { + val speed = updatedValue.value.float + } } } } From 35f7e5063a1ee50a3058534702f9bce82f3492de Mon Sep 17 00:00:00 2001 From: Andre Weber Date: Wed, 31 Jan 2024 13:34:08 +0100 Subject: [PATCH 4/6] chore: Use FriendlyListener, eventually and continually --- .../subscription/DataBrokerSubscriberTest.kt | 78 ++++++++++++------- 1 file changed, 52 insertions(+), 26 deletions(-) diff --git a/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/subscription/DataBrokerSubscriberTest.kt b/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/subscription/DataBrokerSubscriberTest.kt index bcc08763..ef9dd5fe 100644 --- a/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/subscription/DataBrokerSubscriberTest.kt +++ b/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/subscription/DataBrokerSubscriberTest.kt @@ -19,6 +19,8 @@ package org.eclipse.kuksa.subscription +import io.kotest.assertions.nondeterministic.continually +import io.kotest.assertions.nondeterministic.eventually import io.kotest.core.spec.style.BehaviorSpec import io.kotest.matchers.shouldBe import io.mockk.clearMocks @@ -40,6 +42,8 @@ import org.eclipse.kuksa.proto.v1.Types import org.eclipse.kuksa.test.kotest.Insecure import org.eclipse.kuksa.test.kotest.Integration import org.eclipse.kuksa.vssSpecification.VssDriver +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.seconds class DataBrokerSubscriberTest : BehaviorSpec({ tags(Integration, Insecure) @@ -57,36 +61,34 @@ class DataBrokerSubscriberTest : BehaviorSpec({ `when`("Subscribing to VSS_PATH (Branch) 'Vehicle.ADAS.ABS'") { val vssPath = "Vehicle.ADAS.ABS" val fieldValue = Types.Field.FIELD_VALUE - val propertyListener = mockk(relaxed = true) + val propertyListener = FriendlyPropertyListener() classUnderTest.subscribe(vssPath, fieldValue, propertyListener) then("The PropertyListener should send out ONE update containing ALL children") { - val entryUpdatesList = mutableListOf>() - verify(timeout = 100, exactly = 1) { - propertyListener.onPropertyChanged(capture(entryUpdatesList)) + eventually(1.seconds) { + propertyListener.updates.size shouldBe 1 } - val entryUpdates = entryUpdatesList[0] - entryUpdatesList.size shouldBe 1 // ONE update + val entryUpdates = propertyListener.updates[0] + propertyListener.updates.size shouldBe 1 // ONE update entryUpdates.size shouldBe 3 // all children: IsEnabled, IsEngaged, IsError entryUpdates.all { it.entry.path.startsWith(vssPath) } shouldBe true } `when`("Any child changes it's value") { - clearMocks(propertyListener) + propertyListener.reset() + val vssPathIsError = "Vehicle.ADAS.ABS.IsError" val newValueIsError = databrokerTransporter.toggleBoolean(vssPathIsError) val vssPathIsEngaged = "Vehicle.ADAS.ABS.IsEngaged" val newValueIsEngaged = databrokerTransporter.toggleBoolean(vssPathIsEngaged) - delay(50) then("The PropertyListener should be notified about it") { - val entryUpdatesList = mutableListOf>() - verify(timeout = 100, exactly = 2) { - propertyListener.onPropertyChanged(capture(entryUpdatesList)) + eventually(1.seconds) { + propertyListener.updates.size shouldBe 2 } - entryUpdatesList.size shouldBe 2 - val entryUpdates = entryUpdatesList.flatten() + + val entryUpdates = propertyListener.updates.flatten() entryUpdates.count { val path = it.entry.path val entry = it.entry @@ -106,35 +108,42 @@ class DataBrokerSubscriberTest : BehaviorSpec({ `when`("Subscribing using VSS_PATH to Vehicle.Speed with FIELD_VALUE") { val vssPath = "Vehicle.Speed" val fieldValue = Types.Field.FIELD_VALUE - val propertyListener = mockk(relaxed = true) + val propertyListener = FriendlyPropertyListener() classUnderTest.subscribe(vssPath, fieldValue, propertyListener) and("When the FIELD_VALUE of Vehicle.Speed is updated") { - databrokerTransporter.updateRandomFloatValue(vssPath) + val updateRandomFloatValue = databrokerTransporter.updateRandomFloatValue(vssPath) then("The PropertyListener is notified about the change") { - verify(timeout = 100L, exactly = 2) { - propertyListener.onPropertyChanged(any()) + eventually(1.seconds) { + propertyListener.updates.size shouldBe 2 } + propertyListener.updates.flatten() + .count { + val dataEntry = it.entry + val datapoint = dataEntry.value + dataEntry.path == vssPath && datapoint.float == updateRandomFloatValue + } shouldBe 1 + + propertyListener.updates.clear() } } `when`("Subscribing the same PropertyListener to a different vssPath") { - clearMocks(propertyListener) + val otherVssPath = "Vehicle.ADAS.CruiseControl.SpeedSet" classUnderTest.subscribe(otherVssPath, fieldValue, propertyListener) and("Both values are updated") { val updatedValueVssPath = databrokerTransporter.updateRandomFloatValue(vssPath) val updatedValueOtherVssPath = databrokerTransporter.updateRandomFloatValue(otherVssPath) - delay(50) then("The Observer is notified about both changes") { - val entryUpdatesList = mutableListOf>() - verify(timeout = 100L, exactly = 3) { - propertyListener.onPropertyChanged(capture(entryUpdatesList)) + eventually(1.seconds) { + propertyListener.updates.size shouldBe 3 // 1 from subscribe(otherVssPath) + 2 updates } - val entryUpdates = entryUpdatesList.flatten() + + val entryUpdates = propertyListener.updates.flatten() entryUpdates .count { val path = it.entry.path @@ -181,7 +190,7 @@ class DataBrokerSubscriberTest : BehaviorSpec({ } `when`("Unsubscribing the previously registered PropertyListener") { - clearMocks(propertyListener) + propertyListener.reset() classUnderTest.unsubscribe(vssPath, fieldValue, propertyListener) and("When the FIELD_VALUE of Vehicle.Speed is updated") { @@ -189,8 +198,8 @@ class DataBrokerSubscriberTest : BehaviorSpec({ delay(100) then("The PropertyListener is not notified") { - verify(exactly = 0) { - propertyListener.onPropertyChanged(any()) + continually(100.milliseconds) { + propertyListener.updates.size shouldBe 0 } } } @@ -340,3 +349,20 @@ class DataBrokerSubscriberTest : BehaviorSpec({ } } }) + +class FriendlyPropertyListener : PropertyListener { + val updates = mutableListOf>() + val errors = mutableListOf() + override fun onPropertyChanged(entryUpdates: List) { + updates.add(entryUpdates) + } + + override fun onError(throwable: Throwable) { + errors.add(throwable) + } + + fun reset() { + updates.clear() + errors.clear() + } +} From 6fa50d36d7a6279ad7b5362a57e31bc05a5646d7 Mon Sep 17 00:00:00 2001 From: Andre Weber Date: Thu, 1 Feb 2024 14:28:54 +0100 Subject: [PATCH 5/6] chore: Fix Crash when subscribing to Vehicle --- .../org/eclipse/kuksa/testapp/KuksaDataBrokerActivity.kt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/src/main/kotlin/org/eclipse/kuksa/testapp/KuksaDataBrokerActivity.kt b/app/src/main/kotlin/org/eclipse/kuksa/testapp/KuksaDataBrokerActivity.kt index 8c28a6bd..1f562550 100644 --- a/app/src/main/kotlin/org/eclipse/kuksa/testapp/KuksaDataBrokerActivity.kt +++ b/app/src/main/kotlin/org/eclipse/kuksa/testapp/KuksaDataBrokerActivity.kt @@ -95,7 +95,13 @@ class KuksaDataBrokerActivity : ComponentActivity() { private val propertyListener = object : PropertyListener { override fun onPropertyChanged(entryUpdates: List) { Log.d(TAG, "onPropertyChanged() called with: updatedValues = $entryUpdates") - outputViewModel.addOutputEntry("Updated value: $entryUpdates") + + val entries = mutableListOf().apply { + add("Updated Entries") + addAll(entryUpdates.map { it.entry.toString() }) + } + val outputEntry = OutputEntry(messages = entries) + outputViewModel.addOutputEntry(outputEntry) } override fun onError(throwable: Throwable) { From 80d0f75ca56664a1887bdd08cc7506ed2bf40b51 Mon Sep 17 00:00:00 2001 From: Andre Weber Date: Thu, 1 Feb 2024 15:01:07 +0100 Subject: [PATCH 6/6] chore: Fix Flickering Tests --- .../eclipse/kuksa/DataBrokerConnectionTest.kt | 27 +++---- .../kuksa/mocking/FriendlyPropertyListener.kt | 40 ++++++++++ .../FriendlyVssSpecificationListener.kt | 40 ++++++++++ .../subscription/DataBrokerSubscriberTest.kt | 77 +++++++------------ 4 files changed, 119 insertions(+), 65 deletions(-) create mode 100644 kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/mocking/FriendlyPropertyListener.kt create mode 100644 kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/mocking/FriendlyVssSpecificationListener.kt diff --git a/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/DataBrokerConnectionTest.kt b/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/DataBrokerConnectionTest.kt index e2ae4bd2..46f572d3 100644 --- a/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/DataBrokerConnectionTest.kt +++ b/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/DataBrokerConnectionTest.kt @@ -21,6 +21,7 @@ package org.eclipse.kuksa import io.grpc.ConnectivityState import io.grpc.ManagedChannel +import io.kotest.assertions.nondeterministic.eventually import io.kotest.core.spec.style.BehaviorSpec import io.kotest.matchers.shouldBe import io.kotest.matchers.string.shouldContain @@ -31,6 +32,7 @@ import io.mockk.slot import io.mockk.verify import kotlinx.coroutines.runBlocking import org.eclipse.kuksa.databroker.DataBrokerConnectorProvider +import org.eclipse.kuksa.mocking.FriendlyVssSpecificationListener import org.eclipse.kuksa.model.Property import org.eclipse.kuksa.proto.v1.KuksaValV1 import org.eclipse.kuksa.proto.v1.Types @@ -41,6 +43,7 @@ import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertFalse import org.junit.jupiter.api.Assertions.assertTrue import kotlin.random.Random +import kotlin.time.Duration.Companion.seconds class DataBrokerConnectionTest : BehaviorSpec({ tags(Integration) @@ -161,14 +164,12 @@ class DataBrokerConnectionTest : BehaviorSpec({ } `when`("Subscribing to the specification") { - val specificationListener = - mockk>(relaxed = true) + val specificationListener = FriendlyVssSpecificationListener() dataBrokerConnection.subscribe(specification, listener = specificationListener) then("The #onSpecificationChanged method is triggered") { - val capturingList = mutableListOf() - verify(timeout = 100L, exactly = 1) { - specificationListener.onSpecificationChanged(capture(capturingList)) + eventually(1.seconds) { + specificationListener.updatedSpecifications.size shouldBe 1 } } @@ -179,13 +180,11 @@ class DataBrokerConnectionTest : BehaviorSpec({ dataBrokerConnection.update(property, datapoint) then("Every child property has been updated with the correct value") { - val capturingList = mutableListOf() - - verify(timeout = 100, exactly = 2) { - specificationListener.onSpecificationChanged(capture(capturingList)) + eventually(1.seconds) { + specificationListener.updatedSpecifications.size shouldBe 2 } - val updatedDriver = capturingList.last() + val updatedDriver = specificationListener.updatedSpecifications.last() val heartRate = updatedDriver.heartRate heartRate.value shouldBe newHeartRateValue @@ -199,13 +198,11 @@ class DataBrokerConnectionTest : BehaviorSpec({ dataBrokerConnection.update(property, datapoint) then("The subscribed Specification should be updated") { - val capturingSlots = mutableListOf() - - verify(timeout = 100, exactly = 3) { - specificationListener.onSpecificationChanged(capture(capturingSlots)) + eventually(1.seconds) { + specificationListener.updatedSpecifications.size shouldBe 3 } - val updatedDriver = capturingSlots.last() + val updatedDriver = specificationListener.updatedSpecifications.last() val heartRate = updatedDriver.heartRate heartRate.value shouldBe newHeartRateValue diff --git a/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/mocking/FriendlyPropertyListener.kt b/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/mocking/FriendlyPropertyListener.kt new file mode 100644 index 00000000..514f1f5b --- /dev/null +++ b/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/mocking/FriendlyPropertyListener.kt @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * + */ + +package org.eclipse.kuksa.mocking + +import org.eclipse.kuksa.PropertyListener +import org.eclipse.kuksa.proto.v1.KuksaValV1 + +class FriendlyPropertyListener : PropertyListener { + val updates = mutableListOf>() + val errors = mutableListOf() + override fun onPropertyChanged(entryUpdates: List) { + updates.add(entryUpdates) + } + + override fun onError(throwable: Throwable) { + errors.add(throwable) + } + + fun reset() { + updates.clear() + errors.clear() + } +} diff --git a/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/mocking/FriendlyVssSpecificationListener.kt b/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/mocking/FriendlyVssSpecificationListener.kt new file mode 100644 index 00000000..e9a45990 --- /dev/null +++ b/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/mocking/FriendlyVssSpecificationListener.kt @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * + */ + +package org.eclipse.kuksa.mocking + +import org.eclipse.kuksa.VssSpecificationListener +import org.eclipse.kuksa.vsscore.model.VssSpecification + +class FriendlyVssSpecificationListener : VssSpecificationListener { + val updatedSpecifications = mutableListOf() + val errors = mutableListOf() + override fun onSpecificationChanged(vssSpecification: T) { + updatedSpecifications.add(vssSpecification) + } + + override fun onError(throwable: Throwable) { + errors.add(throwable) + } + + fun reset() { + updatedSpecifications.clear() + errors.clear() + } +} diff --git a/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/subscription/DataBrokerSubscriberTest.kt b/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/subscription/DataBrokerSubscriberTest.kt index ef9dd5fe..3e2f976a 100644 --- a/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/subscription/DataBrokerSubscriberTest.kt +++ b/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/subscription/DataBrokerSubscriberTest.kt @@ -30,14 +30,14 @@ import io.mockk.verify import kotlinx.coroutines.delay import org.eclipse.kuksa.DataBrokerTransporter import org.eclipse.kuksa.PropertyListener -import org.eclipse.kuksa.VssSpecificationListener import org.eclipse.kuksa.databroker.DataBrokerConnectorProvider import org.eclipse.kuksa.extensions.toggleBoolean import org.eclipse.kuksa.extensions.updateRandomFloatValue import org.eclipse.kuksa.extensions.updateRandomUint32Value +import org.eclipse.kuksa.mocking.FriendlyPropertyListener +import org.eclipse.kuksa.mocking.FriendlyVssSpecificationListener import org.eclipse.kuksa.pattern.listener.MultiListener import org.eclipse.kuksa.pattern.listener.count -import org.eclipse.kuksa.proto.v1.KuksaValV1 import org.eclipse.kuksa.proto.v1.Types import org.eclipse.kuksa.test.kotest.Insecure import org.eclipse.kuksa.test.kotest.Integration @@ -163,10 +163,10 @@ class DataBrokerSubscriberTest : BehaviorSpec({ } `when`("Subscribing multiple (different) PropertyListener to $vssPath") { - val propertyListenerMocks = mutableListOf() + val friendlyPropertyListeners = mutableListOf() repeat(10) { - val otherPropertyListenerMock = mockk(relaxed = true) - propertyListenerMocks.add(otherPropertyListenerMock) + val otherPropertyListenerMock = FriendlyPropertyListener() + friendlyPropertyListeners.add(otherPropertyListenerMock) classUnderTest.subscribe(vssPath, fieldValue, otherPropertyListenerMock) } @@ -175,14 +175,13 @@ class DataBrokerSubscriberTest : BehaviorSpec({ val randomFloatValue = databrokerTransporter.updateRandomFloatValue(vssPath) then("Each PropertyListener is only notified once") { - propertyListenerMocks.forEach { propertyListenerMock -> - val dataEntries = mutableListOf>() - - verify(timeout = 100L, exactly = 2) { - propertyListenerMock.onPropertyChanged(capture(dataEntries)) + friendlyPropertyListeners.forEach { friendlyPropertyListener -> + eventually(1.seconds) { + friendlyPropertyListener.updates.size shouldBe 2 } - val count = dataEntries.count { it[0].entry.value.float == randomFloatValue } + val count = friendlyPropertyListener.updates + .count { it[0].entry.value.float == randomFloatValue } count shouldBe 1 } } @@ -209,21 +208,20 @@ class DataBrokerSubscriberTest : BehaviorSpec({ `when`("Subscribing the same PropertyListener twice using VSS_PATH to Vehicle.Speed with FIELD_VALUE") { val vssPath = "Vehicle.Speed" val fieldValue = Types.Field.FIELD_VALUE - val propertyListenerMock = mockk(relaxed = true) - classUnderTest.subscribe(vssPath, fieldValue, propertyListenerMock) - classUnderTest.subscribe(vssPath, fieldValue, propertyListenerMock) + val friendlyPropertyListener = FriendlyPropertyListener() + classUnderTest.subscribe(vssPath, fieldValue, friendlyPropertyListener) + classUnderTest.subscribe(vssPath, fieldValue, friendlyPropertyListener) and("When the FIELD_VALUE of Vehicle.Speed is updated") { val randomFloatValue = databrokerTransporter.updateRandomFloatValue(vssPath) then("The PropertyListener is only notified once") { - val dataEntries = mutableListOf>() - - verify(timeout = 100L, exactly = 2) { - propertyListenerMock.onPropertyChanged(capture(dataEntries)) + eventually(1.seconds) { + friendlyPropertyListener.updates.size shouldBe 2 } - val count = dataEntries.count { it[0].entry.value.float == randomFloatValue } + val count = friendlyPropertyListener.updates + .count { it[0].entry.value.float == randomFloatValue } count shouldBe 1 } } @@ -232,12 +230,11 @@ class DataBrokerSubscriberTest : BehaviorSpec({ val specification = VssDriver.VssHeartRate() `when`("Subscribing using VssSpecification to Vehicle.Driver.HeartRate with Field FIELD_VALUE") { - val specificationObserverMock = - mockk>(relaxed = true) + val friendlyVssSpecificationListener = FriendlyVssSpecificationListener() classUnderTest.subscribe( specification, Types.Field.FIELD_VALUE, - specificationObserverMock, + friendlyVssSpecificationListener, ) and("The value of Vehicle.Driver.HeartRate changes") { @@ -245,20 +242,19 @@ class DataBrokerSubscriberTest : BehaviorSpec({ databrokerTransporter.updateRandomUint32Value(specification.vssPath) then("The Observer should be triggered") { - val vssHeartRates = mutableListOf() - verify(timeout = 100, exactly = 2) { - specificationObserverMock.onSpecificationChanged(capture(vssHeartRates)) + eventually(1.seconds) { + friendlyVssSpecificationListener.updatedSpecifications.size shouldBe 2 } - val count = vssHeartRates.count { it.value == randomIntValue } + val count = friendlyVssSpecificationListener.updatedSpecifications + .count { it.value == randomIntValue } count shouldBe 1 } } } `when`("Subscribing the same SpecificationObserver twice to Vehicle.Driver.HeartRate") { - val specificationObserverMock = - mockk>(relaxed = true) + val specificationObserverMock = FriendlyVssSpecificationListener() classUnderTest.subscribe( specification, Types.Field.FIELD_VALUE, @@ -275,13 +271,11 @@ class DataBrokerSubscriberTest : BehaviorSpec({ databrokerTransporter.updateRandomUint32Value(specification.vssPath) then("The Observer is only notified once") { - val heartRates = mutableListOf() - - verify(timeout = 100, exactly = 2) { - specificationObserverMock.onSpecificationChanged(capture(heartRates)) + eventually(1.seconds) { + specificationObserverMock.updatedSpecifications.size shouldBe 2 } - val count = heartRates.count { it.value == randomIntValue } + val count = specificationObserverMock.updatedSpecifications.count { it.value == randomIntValue } count shouldBe 1 } } @@ -349,20 +343,3 @@ class DataBrokerSubscriberTest : BehaviorSpec({ } } }) - -class FriendlyPropertyListener : PropertyListener { - val updates = mutableListOf>() - val errors = mutableListOf() - override fun onPropertyChanged(entryUpdates: List) { - updates.add(entryUpdates) - } - - override fun onError(throwable: Throwable) { - errors.add(throwable) - } - - fun reset() { - updates.clear() - errors.clear() - } -}