From 65f08e9f9501c52eb8d4cf47b7e79bc707aeff95 Mon Sep 17 00:00:00 2001 From: Andre Weber Date: Fri, 20 Oct 2023 14:40:17 +0200 Subject: [PATCH] test: Adapt Tests --- .../kuksa/DataBrokerApiInteractionTest.kt | 134 ++++++++++ .../eclipse/kuksa/DataBrokerConnectionTest.kt | 11 +- .../DataBrokerApiInteractionExtensions.kt | 54 ++++ .../subscription/SubscriptionManagerTest.kt | 253 ++++++++++++++++++ 4 files changed, 444 insertions(+), 8 deletions(-) create mode 100644 kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/DataBrokerApiInteractionTest.kt create mode 100644 kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/extensions/DataBrokerApiInteractionExtensions.kt create mode 100644 kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/subscription/SubscriptionManagerTest.kt diff --git a/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/DataBrokerApiInteractionTest.kt b/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/DataBrokerApiInteractionTest.kt new file mode 100644 index 00000000..8ec92e16 --- /dev/null +++ b/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/DataBrokerApiInteractionTest.kt @@ -0,0 +1,134 @@ +/* + * 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 + +import io.kotest.core.spec.style.BehaviorSpec +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import io.kotest.matchers.types.instanceOf +import io.mockk.mockk +import io.mockk.verify +import org.eclipse.kuksa.databroker.DataBrokerConnectorProvider +import org.eclipse.kuksa.extensions.setRandomFloatValue +import org.eclipse.kuksa.proto.v1.KuksaValV1 +import org.eclipse.kuksa.proto.v1.KuksaValV1.SetResponse +import org.eclipse.kuksa.proto.v1.Types +import org.eclipse.kuksa.proto.v1.Types.Datapoint +import org.eclipse.kuksa.test.kotest.Insecure +import org.eclipse.kuksa.test.kotest.Integration +import kotlin.random.Random + +class DataBrokerApiInteractionTest : BehaviorSpec({ + tags(Integration, Insecure) + + given("An active Connection to the DataBroker") { + val dataBrokerConnectorProvider = DataBrokerConnectorProvider() + val connector = dataBrokerConnectorProvider.createInsecure() + connector.connect() + + and("An Instance of DataBrokerApiInteraction") { + val classUnderTest = DataBrokerApiInteraction(dataBrokerConnectorProvider.managedChannel) + + and("Some VSS-related data") { + val vssPath = "Vehicle.ADAS.CruiseControl.SpeedSet" + val fields = listOf(Types.Field.FIELD_VALUE) + val random = Random(System.currentTimeMillis()) + val valueToSet = random.nextInt(250).toFloat() + + `when`("Updating the $fields of $vssPath to $valueToSet km/h") { + val updatedDatapoint = Datapoint.newBuilder().setFloat(valueToSet).build() + val result = kotlin.runCatching { + classUnderTest.updateProperty(vssPath, fields, updatedDatapoint) + } + + then("No Exception should be thrown") { + result.exceptionOrNull() shouldBe null + } + + then("It should return a valid SetResponse") { + val response = result.getOrNull() + response shouldNotBe null + response shouldBe instanceOf(SetResponse::class) + } + } + + `when`("Fetching the Value of Vehicle.ADAS.CruiseControl.SpeedSet") { + val property = classUnderTest.fetchProperty(vssPath, fields) + + then("It should return the correct value") { + val dataEntry = property.getEntries(0) + val value = dataEntry.value.float + value shouldBe valueToSet + } + } + + `when`("Trying to fetch the $fields from an invalid VSS Path") { + val invalidVssPath = "Vehicle.This.Path.Is.Invalid" + + val result = kotlin.runCatching { + classUnderTest.fetchProperty(invalidVssPath, fields) + } + + then("No Exception should be thrown") { + result.exceptionOrNull() shouldBe null + } + + then("It should return a GetResponse with no entries and one error") { + val response = result.getOrNull() + response shouldNotBe null + response shouldBe instanceOf(KuksaValV1.GetResponse::class) + response?.entriesList?.size shouldBe 0 + response?.errorsList?.size shouldBe 1 + } + } + + `when`("Subscribing to the vssPath using FIELD_VALUE") { + val subscription = classUnderTest.subscribe(vssPath, Types.Field.FIELD_VALUE) + + val propertyObserver = mockk(relaxed = true) + subscription.observers.register(propertyObserver) + + and("The value of the vssPath is updated") { + classUnderTest.setRandomFloatValue(vssPath) + + then("The PropertyObserver should be notified") { + verify { + propertyObserver.onPropertyChanged(vssPath, Types.Field.FIELD_VALUE, any()) + } + } + } + } + + `when`("Subscribing to an invalid vssPath") { + val subscription = classUnderTest.subscribe("Vehicle.Some.Invalid.Path", Types.Field.FIELD_VALUE) + + val propertyObserver = mockk(relaxed = true) + subscription.observers.register(propertyObserver) + + then("An Error should be triggered") { + verify(timeout = 100L) { + propertyObserver.onError(any()) + } + } + } + } + } + } +}) 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 d6c84fc6..360b35ae 100644 --- a/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/DataBrokerConnectionTest.kt +++ b/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/DataBrokerConnectionTest.kt @@ -27,7 +27,6 @@ import io.mockk.clearMocks import io.mockk.mockk import io.mockk.slot import io.mockk.verify -import kotlinx.coroutines.delay import kotlinx.coroutines.runBlocking import org.eclipse.kuksa.databroker.DataBrokerConnectorProvider import org.eclipse.kuksa.model.Property @@ -55,7 +54,7 @@ class DataBrokerConnectionTest : BehaviorSpec({ dataBrokerConnection.subscribe(property, propertyObserver) then("The #onPropertyChanged method is triggered") { - verify { propertyObserver.onPropertyChanged(any(), any(), any()) } + verify(timeout = 100L) { propertyObserver.onPropertyChanged(any(), any(), any()) } } `when`("The observed Property changes") { @@ -162,10 +161,8 @@ class DataBrokerConnectionTest : BehaviorSpec({ val propertyObserver = mockk>(relaxed = true) dataBrokerConnection.subscribe(specification, observer = propertyObserver) - delay(100) - then("The #onSpecificationChanged method is triggered") { - verify { propertyObserver.onSpecificationChanged(any()) } + verify(timeout = 100L) { propertyObserver.onSpecificationChanged(any()) } } and("The initial value is different from the default for a child") { @@ -214,11 +211,9 @@ class DataBrokerConnectionTest : BehaviorSpec({ val propertyObserver = mockk(relaxed = true) dataBrokerConnection.subscribe(property, propertyObserver) - delay(100) - then("The PropertyObserver#onError method should be triggered with 'NOT_FOUND' (Path not found)") { val capturingSlot = slot() - verify { propertyObserver.onError(capture(capturingSlot)) } + verify(timeout = 100L) { propertyObserver.onError(capture(capturingSlot)) } val capturedThrowable = capturingSlot.captured capturedThrowable.message shouldContain "NOT_FOUND" } diff --git a/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/extensions/DataBrokerApiInteractionExtensions.kt b/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/extensions/DataBrokerApiInteractionExtensions.kt new file mode 100644 index 00000000..d47859b0 --- /dev/null +++ b/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/extensions/DataBrokerApiInteractionExtensions.kt @@ -0,0 +1,54 @@ +/* + * 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.extensions + +import io.kotest.assertions.fail +import org.eclipse.kuksa.DataBrokerApiInteraction +import org.eclipse.kuksa.proto.v1.Types +import kotlin.random.Random + +internal suspend fun DataBrokerApiInteraction.setRandomFloatValue(vssPath: String, maxValue: Int = 300): Float { + val random = Random(System.nanoTime()) + val randomValue = random.nextInt(maxValue) + val randomFloat = randomValue.toFloat() + val updatedDatapoint = Types.Datapoint.newBuilder().setFloat(randomFloat).build() + + try { + updateProperty(vssPath, listOf(Types.Field.FIELD_VALUE), updatedDatapoint) + } catch (e: Exception) { + fail("Updating $vssPath to $randomFloat failed: $e") + } + + return randomFloat +} + +internal suspend fun DataBrokerApiInteraction.setRandomUint32Value(vssPath: String, maxValue: Int = 300): Int { + val random = Random(System.nanoTime()) + val randomValue = random.nextInt(maxValue) + val updatedDatapoint = Types.Datapoint.newBuilder().setUint32(randomValue).build() + + try { + updateProperty(vssPath, listOf(Types.Field.FIELD_VALUE), updatedDatapoint) + } catch (e: Exception) { + fail("Updating $vssPath to $randomValue failed: $e") + } + + return randomValue +} diff --git a/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/subscription/SubscriptionManagerTest.kt b/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/subscription/SubscriptionManagerTest.kt new file mode 100644 index 00000000..056ddb28 --- /dev/null +++ b/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/subscription/SubscriptionManagerTest.kt @@ -0,0 +1,253 @@ +/* + * 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.subscription + +import io.kotest.core.spec.style.BehaviorSpec +import io.kotest.matchers.shouldBe +import io.mockk.clearMocks +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import org.eclipse.kuksa.DataBrokerApiInteraction +import org.eclipse.kuksa.PropertyObserver +import org.eclipse.kuksa.VssSpecificationObserver +import org.eclipse.kuksa.databroker.DataBrokerConnectorProvider +import org.eclipse.kuksa.extensions.setRandomFloatValue +import org.eclipse.kuksa.extensions.setRandomUint32Value +import org.eclipse.kuksa.pattern.listener.MultiListener +import org.eclipse.kuksa.pattern.listener.count +import org.eclipse.kuksa.proto.v1.Types +import org.eclipse.kuksa.proto.v1.Types.DataEntry +import org.eclipse.kuksa.test.kotest.Integration +import org.eclipse.kuksa.vssSpecification.VssHeartRate + +class SubscriptionManagerTest : BehaviorSpec({ + tags(Integration) + + given("An active Connection to the DataBroker") { + val dataBrokerConnectorProvider = DataBrokerConnectorProvider() + val connector = dataBrokerConnectorProvider.createInsecure() + connector.connect() + + and("An Instance of SubscriptionManager") { + val databrokerApiInteraction = DataBrokerApiInteraction(dataBrokerConnectorProvider.managedChannel) + val classUnderTest = SubscriptionManager(databrokerApiInteraction) + + `when`("Subscribing using VSS_PATH to Vehicle.Speed with FIELD_VALUE") { + val vssPath = "Vehicle.Speed" + val fieldValue = Types.Field.FIELD_VALUE + val propertyObserverMock = mockk(relaxed = true) + classUnderTest.subscribe(vssPath, fieldValue, propertyObserverMock) + + and("When the FIELD_VALUE of Vehicle.Speed is updated") { + databrokerApiInteraction.setRandomFloatValue(vssPath) + + then("The PropertyObserver is notified about the change") { + verify(timeout = 100L) { + propertyObserverMock.onPropertyChanged(vssPath, fieldValue, any()) + } + } + } + + `when`("Subscribing the same PropertyObserver to a different vssPath") { + clearMocks(propertyObserverMock) + + val otherVssPath = "Vehicle.ADAS.CruiseControl.SpeedSet" + classUnderTest.subscribe(otherVssPath, fieldValue, propertyObserverMock) + + and("Both values are updated") { + databrokerApiInteraction.setRandomFloatValue(vssPath) + databrokerApiInteraction.setRandomFloatValue(otherVssPath) + + then("The Observer is notified about both changes") { + verify(timeout = 100L) { + propertyObserverMock.onPropertyChanged(vssPath, fieldValue, any()) + } + verify(timeout = 100L) { + propertyObserverMock.onPropertyChanged(otherVssPath, fieldValue, any()) + } + } + } + } + + `when`("Subscribing multiple (different) PropertyObserver to $vssPath") { + clearMocks(propertyObserverMock) + + val propertyObserverMocks = mutableListOf() + repeat(10) { + val otherPropertyObserverMock = mockk(relaxed = true) + propertyObserverMocks.add(otherPropertyObserverMock) + + classUnderTest.subscribe(vssPath, fieldValue, otherPropertyObserverMock) + } + + and("When the FIELD_VALUE of Vehicle.Speed is updated") { + val randomFloatValue = databrokerApiInteraction.setRandomFloatValue(vssPath) + + then("The PropertyObserver is only notified once") { + propertyObserverMocks.forEach { propertyObserverMock -> + val dataEntries = mutableListOf() + + verify(timeout = 100L) { + propertyObserverMock.onPropertyChanged(vssPath, fieldValue, capture(dataEntries)) + } + + val count = dataEntries.count { it.value.float == randomFloatValue } + count shouldBe 1 + } + } + } + } + + `when`("Unsubscribing the previously registered PropertyObserver") { + clearMocks(propertyObserverMock) + classUnderTest.unsubscribe(vssPath, fieldValue, propertyObserverMock) + + and("When the FIELD_VALUE of Vehicle.Speed is updated") { + databrokerApiInteraction.setRandomFloatValue(vssPath) + + then("The PropertyObserver is not notified") { + verify(timeout = 100L, exactly = 0) { + propertyObserverMock.onPropertyChanged(vssPath, fieldValue, any()) + } + } + } + } + } + + `when`("Subscribing the same PropertyObserver twice using VSS_PATH to Vehicle.Speed with FIELD_VALUE") { + val vssPath = "Vehicle.Speed" + val fieldValue = Types.Field.FIELD_VALUE + val propertyObserverMock = mockk(relaxed = true) + classUnderTest.subscribe(vssPath, fieldValue, propertyObserverMock) + classUnderTest.subscribe(vssPath, fieldValue, propertyObserverMock) + + and("When the FIELD_VALUE of Vehicle.Speed is updated") { + val randomFloatValue = databrokerApiInteraction.setRandomFloatValue(vssPath) + + then("The PropertyObserver is only notified once") { + val dataEntries = mutableListOf() + + verify(timeout = 100L) { + propertyObserverMock.onPropertyChanged(vssPath, fieldValue, capture(dataEntries)) + } + + val count = dataEntries.count { it.value.float == randomFloatValue } + count shouldBe 1 + } + } + } + + val specification = VssHeartRate() + + `when`("Subscribing using VssSpecification to Vehicle.Driver.HeartRate with Field FIELD_VALUE") { + val specificationObserverMock = mockk>(relaxed = true) + classUnderTest.subscribe(specification, Types.Field.FIELD_VALUE, specificationObserverMock) + + and("The value of Vehicle.Driver.HeartRate changes") { + databrokerApiInteraction.setRandomFloatValue(specification.vssPath) + + then("The Observer should be triggered") { + verify(timeout = 100) { specificationObserverMock.onSpecificationChanged(any()) } + } + } + } + + `when`("Subscribing the same SpecificationObserver twice to Vehicle.Driver.HeartRate") { + val specificationObserverMock = mockk>(relaxed = true) + classUnderTest.subscribe(specification, Types.Field.FIELD_VALUE, specificationObserverMock) + classUnderTest.subscribe(specification, Types.Field.FIELD_VALUE, specificationObserverMock) + + and("The value of Vehicle.Driver.HeartRate changes") { + val randomIntValue = databrokerApiInteraction.setRandomUint32Value(specification.vssPath) + + then("The Observer is only notified once") { + val heartRates = mutableListOf() + + verify(timeout = 100) { specificationObserverMock.onSpecificationChanged(capture(heartRates)) } + + val count = heartRates.count { it.value == randomIntValue } + count shouldBe 1 + } + } + } + } + } + + given("An Instance of SubscriptionManager with a mocked DataBrokerApiInteraction") { + val subscriptionMock = mockk(relaxed = true) + val dataBrokerApiInteractionMock = mockk(relaxed = true) + val multiListener = MultiListener() + every { dataBrokerApiInteractionMock.subscribe(any(), any()) } returns subscriptionMock + every { subscriptionMock.observers } returns multiListener + val classUnderTest = SubscriptionManager(dataBrokerApiInteractionMock) + + `when`("Subscribing for the first time to a vssPath and field") { + val vssPath = "Vehicle.Speed" + val field = Types.Field.FIELD_VALUE + val propertyObserverMock1 = mockk() + val propertyObserverMock2 = mockk() + classUnderTest.subscribe(vssPath, field, propertyObserverMock1) + + then("A new Subscription is created and the PropertyObserver is added to the list of Observers") { + verify { + dataBrokerApiInteractionMock.subscribe(vssPath, field) + } + multiListener.count() shouldBe 1 + } + + `when`("Another PropertyObserver subscribes to the same vssPath and field") { + clearMocks(dataBrokerApiInteractionMock) + + classUnderTest.subscribe(vssPath, field, propertyObserverMock2) + + then("No new Subscription is created and the PropertyObserver is added to the list of Observers") { + verify(exactly = 0) { + dataBrokerApiInteractionMock.subscribe(vssPath, field) + } + multiListener.count() shouldBe 2 + } + } + + `when`("One of two PropertyObservers unsubscribes") { + classUnderTest.unsubscribe(vssPath, field, propertyObserverMock1) + + then("The Subscription is not canceled") { + verify(exactly = 0) { + subscriptionMock.cancel() + } + multiListener.count() shouldBe 1 + } + + `when`("The last PropertyObserver unsubscribes as well") { + classUnderTest.unsubscribe(vssPath, field, propertyObserverMock2) + + then("There should be no more observers registered") { + multiListener.count() shouldBe 0 + } + + then("The Subscription is canceled") { + verify { subscriptionMock.cancel() } + } + } + } + } + } +})