From cbf6a470b0dc38da2dbee67901c81947983a238a Mon Sep 17 00:00:00 2001 From: Tomas Chladek Date: Tue, 3 Jun 2025 13:27:26 +0200 Subject: [PATCH 1/2] MutableAttributes unit tests Signed-off-by: Tomas Chladek --- integration/agent/api/build.gradle.kts | 2 + .../attributes/MutableAttributesTest.kt | 306 ++++++++++++++++++ .../common/attributes/MutableAttributes.kt | 44 ++- 3 files changed, 346 insertions(+), 6 deletions(-) create mode 100644 integration/agent/api/src/test/java/com/splunk/rum/integration/agent/common/attributes/MutableAttributesTest.kt diff --git a/integration/agent/api/build.gradle.kts b/integration/agent/api/build.gradle.kts index 71850f807..518ace17d 100644 --- a/integration/agent/api/build.gradle.kts +++ b/integration/agent/api/build.gradle.kts @@ -42,4 +42,6 @@ dependencies { implementation(project(":common:utils")) compileOnly(Dependencies.Android.Compose.ui) + + testImplementation(Dependencies.Test.junit) } diff --git a/integration/agent/api/src/test/java/com/splunk/rum/integration/agent/common/attributes/MutableAttributesTest.kt b/integration/agent/api/src/test/java/com/splunk/rum/integration/agent/common/attributes/MutableAttributesTest.kt new file mode 100644 index 000000000..bd8415bd3 --- /dev/null +++ b/integration/agent/api/src/test/java/com/splunk/rum/integration/agent/common/attributes/MutableAttributesTest.kt @@ -0,0 +1,306 @@ +/* + * Copyright 2025 Splunk Inc. + * + * 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. + */ + +package com.splunk.rum.integration.agent.common.attributes + +import com.cisco.android.common.utils.extensions.forEachFast +import io.opentelemetry.api.common.AttributeKey +import io.opentelemetry.api.common.Attributes +import io.opentelemetry.api.common.AttributesBuilder +import java.util.concurrent.CountDownLatch +import java.util.concurrent.Executors +import org.junit.Assert +import org.junit.Test + +class MutableAttributesTest { + + @Test + fun `get and set with String key`() { + val mutableAttributes = MutableAttributes() + + mutableAttributes["key"] = "value" + Assert.assertEquals("value", mutableAttributes["key"]) + } + + @Test + fun `get and set with AttributeKey`() { + val mutableAttributes = MutableAttributes() + + mutableAttributes[AttributeKey.stringKey("key")] = "value" + Assert.assertEquals("value", mutableAttributes[AttributeKey.stringKey("key")]) + } + + @Test + fun `get non-existent key returns null`() { + val mutableAttributes = MutableAttributes() + Assert.assertNull(mutableAttributes["key"]) + Assert.assertNull(mutableAttributes[AttributeKey.stringKey("key")]) + } + + @Test + fun `set multiple types`() { + val mutableAttributes = MutableAttributes() + + mutableAttributes["stringKey"] = "hello" + mutableAttributes["longKey"] = 123L + mutableAttributes["doubleKey"] = 45.67 + mutableAttributes["booleanKey"] = true + + Assert.assertEquals("hello", mutableAttributes["stringKey"]) + Assert.assertEquals(123L, mutableAttributes["longKey"]) + Assert.assertEquals(45.67, mutableAttributes["doubleKey"]) + Assert.assertEquals(true, mutableAttributes["booleanKey"]) + } + + @Test + fun `remove with String key`() { + val mutableAttributes = MutableAttributes() + + mutableAttributes["key"] = "value" + Assert.assertEquals("value", mutableAttributes["key"]) + + mutableAttributes.remove("key") + Assert.assertNull(mutableAttributes["key"]) + } + + @Test + fun `remove with AttributeKey`() { + val mutableAttributes = MutableAttributes() + + mutableAttributes[AttributeKey.longKey("keyToRemove")] = 100L + Assert.assertEquals(100L, mutableAttributes[AttributeKey.longKey("keyToRemove")]) + + mutableAttributes.remove(AttributeKey.longKey("keyToRemove")) + Assert.assertNull(mutableAttributes[AttributeKey.longKey("keyToRemove")]) + } + + @Test + fun `removeAll clears all attributes`() { + val mutableAttributes = MutableAttributes() + + mutableAttributes["key1"] = "value1" + mutableAttributes["key2"] = 123L + Assert.assertEquals(2, mutableAttributes.size()) + + mutableAttributes.removeAll() + + Assert.assertTrue(mutableAttributes.isEmpty()) + Assert.assertEquals(0, mutableAttributes.size()) + Assert.assertNull(mutableAttributes["key1"]) + Assert.assertNull(mutableAttributes["key2"]) + } + + @Test + fun `setAll adds attributes from another Attributes instance`() { + val mutableAttributes = MutableAttributes() + mutableAttributes["existingKey"] = "existingValue" + + val attributesToAdd = Attributes.builder() + .put("newKey1", "newValue1") + .put(AttributeKey.longKey("newKey2"), 99L) + .build() + + mutableAttributes.setAll(attributesToAdd) + + Assert.assertEquals("existingValue", mutableAttributes["existingKey"]) + Assert.assertEquals("newValue1", mutableAttributes["newKey1"]) + Assert.assertEquals(99L, mutableAttributes[AttributeKey.longKey("newKey2")]) + Assert.assertEquals(3, mutableAttributes.size()) + } + + @Test + fun `getAll returns current attributes`() { + val mutableAttributes = MutableAttributes() + + mutableAttributes[AttributeKey.stringKey("key1")] = "value1" + mutableAttributes["key2"] = true + + val allAttributes = mutableAttributes.getAll() + Assert.assertEquals(2, allAttributes.size()) + Assert.assertEquals("value1", allAttributes[AttributeKey.stringKey("key1")]) + Assert.assertEquals(true, allAttributes[AttributeKey.booleanKey("key2")]) + } + + @Test + fun `update modifies attributes`() { + val mutableAttributes = MutableAttributes() + mutableAttributes["initialKey"] = "initialValue" + + mutableAttributes.update { + put("updatedKey", "updatedValue") + remove(AttributeKey.stringKey("initialKey")) + put("anotherKey", 12345L) + } + + Assert.assertNull(mutableAttributes["initialKey"]) + Assert.assertEquals("updatedValue", mutableAttributes["updatedKey"]) + Assert.assertEquals(12345L, mutableAttributes["anotherKey"]) + Assert.assertEquals(2, mutableAttributes.size()) + } + + @Test + fun `forEach iterates over attributes`() { + val mutableAttributes = MutableAttributes() + + mutableAttributes[AttributeKey.stringKey("name")] = "test" + mutableAttributes[AttributeKey.longKey("count")] = 10L + + val collectedAttributes = mutableMapOf, Any>() + mutableAttributes.forEach { key, value -> collectedAttributes[key] = value } + + Assert.assertEquals(2, collectedAttributes.size) + Assert.assertEquals("test", collectedAttributes[AttributeKey.stringKey("name")]) + Assert.assertEquals(10L, collectedAttributes[AttributeKey.longKey("count")]) + } + + @Test + fun `size returns correct number of attributes`() { + val mutableAttributes = MutableAttributes() + Assert.assertEquals(0, mutableAttributes.size()) + mutableAttributes["a"] = "1" + Assert.assertEquals(1, mutableAttributes.size()) + mutableAttributes["b"] = "2" + Assert.assertEquals(2, mutableAttributes.size()) + mutableAttributes.remove("a") + Assert.assertEquals(1, mutableAttributes.size()) + } + + @Test + fun `isEmpty checks if attributes are empty`() { + val mutableAttributes = MutableAttributes() + Assert.assertTrue(mutableAttributes.isEmpty()) + mutableAttributes["a"] = "1" + Assert.assertTrue(!mutableAttributes.isEmpty()) + mutableAttributes.removeAll() + Assert.assertTrue(mutableAttributes.isEmpty()) + } + + @Test + fun `asMap returns a map representation`() { + val mutableAttributes = MutableAttributes() + + mutableAttributes[AttributeKey.stringKey("city")] = "Prague" + mutableAttributes[AttributeKey.booleanKey("isActive")] = true + + val map = mutableAttributes.asMap() + Assert.assertEquals(2, map.size) + Assert.assertEquals("Prague", map[AttributeKey.stringKey("city")]) + Assert.assertEquals(true, map[AttributeKey.booleanKey("isActive")]) + } + + @Test + fun `toBuilder creates a builder with current attributes`() { + val mutableAttributes = MutableAttributes() + mutableAttributes["name"] = "testBuilder" + mutableAttributes["version"] = 1.0 + + val builder = mutableAttributes.toBuilder() + val newAttributes = builder.put("extra", true).build() + + Assert.assertEquals("testBuilder", newAttributes[AttributeKey.stringKey("name")]) + Assert.assertEquals(1.0, newAttributes[AttributeKey.doubleKey("version")]) + Assert.assertEquals(true, newAttributes[AttributeKey.booleanKey("extra")]) + Assert.assertNull(mutableAttributes["extra"]) + Assert.assertEquals(2, mutableAttributes.size()) + } + + @Test + fun `thread safety test - concurrent writes and reads`() { + val mutableAttributes = MutableAttributes() + + val numberOfThreads = 10 + val operationsPerThread = 100 + val executor = Executors.newFixedThreadPool(numberOfThreads) + val latch = CountDownLatch(numberOfThreads) + + val createKey = { threadIndex: Int, keyIndex: Int -> "thread_${threadIndex}_key_$keyIndex" } + + for (i in 0 until numberOfThreads) { + executor.submit { + try { + for (j in 0 until operationsPerThread) { + mutableAttributes[createKey(i, j)] = "value_$j" + } + + for (j in 0 until operationsPerThread) { + Assert.assertEquals("value_$j", mutableAttributes[createKey(i, j)]) + } + + for (j in 0 until operationsPerThread) { + mutableAttributes.remove("thread_${i}_key_$j") + } + } finally { + latch.countDown() + } + } + } + + latch.await() + executor.shutdown() + + Assert.assertEquals(0, mutableAttributes.size()) + } + + @Test + fun `thread safety test - concurrent setAll and removeAll`() { + val mutableAttributes = MutableAttributes() + val numberOfThreads = 100 + val executor = Executors.newFixedThreadPool(numberOfThreads) + val latch = CountDownLatch(numberOfThreads) + + val attributesList = ArrayList(numberOfThreads) + + for (i in 0 until numberOfThreads) { + executor.submit { + try { + val attributes = Attributes.builder() + .put("key_${i}_0", "value") + .put("key_${i}_1", 123) + .put("key_${i}_2", true) + .put("key_${i}_3", 123.45) + .build() + + mutableAttributes.setAll(attributes) + + if (i % 10 == 0) { + mutableAttributes.removeAll() + synchronized(attributesList) { attributesList.clear() } + } else { + synchronized(attributesList) { attributesList += attributes } + } + } finally { + latch.countDown() + } + } + } + + latch.await() + executor.shutdown() + + val attributes = mutableAttributes.getAll() + + val allAttributes = Attributes.builder() + .putAll(attributesList) + .build() + + Assert.assertEquals(allAttributes, attributes) + } + + private fun AttributesBuilder.putAll(list: Collection): AttributesBuilder { + list.forEachFast { putAll(it) } + return this + } +} diff --git a/integration/agent/common/src/main/java/com/splunk/rum/integration/agent/common/attributes/MutableAttributes.kt b/integration/agent/common/src/main/java/com/splunk/rum/integration/agent/common/attributes/MutableAttributes.kt index 81ffc319e..3057c8fa8 100644 --- a/integration/agent/common/src/main/java/com/splunk/rum/integration/agent/common/attributes/MutableAttributes.kt +++ b/integration/agent/common/src/main/java/com/splunk/rum/integration/agent/common/attributes/MutableAttributes.kt @@ -24,10 +24,8 @@ import java.util.function.BiConsumer /** * A utility class for managing custom RUM attributes. */ -class MutableAttributes @JvmOverloads constructor( - @Volatile - private var attributes: Attributes = Attributes.empty() -) : Attributes { +class MutableAttributes @JvmOverloads constructor(private var attributes: Attributes = Attributes.empty()) : + Attributes { /** * Retrieves the value associated with the given [AttributeKey]. @@ -35,6 +33,7 @@ class MutableAttributes @JvmOverloads constructor( * @param key the attribute key to retrieve * @return the value if present, or null */ + @Synchronized override operator fun get(key: AttributeKey): T? = key.let { attributes.get(it) } /** @@ -42,10 +41,25 @@ class MutableAttributes @JvmOverloads constructor( * * @param key the string key to retrieve * @return the value if present, or null - * @throws ClassCastException if the stored value is not of the expected type */ + @Suppress("UNCHECKED_CAST") - operator fun get(key: String): T? = attributes.get(AttributeKey.stringKey(key)) as (T) + @Synchronized + operator fun get(key: String): T? { + var value: Any? = null + + try { + attributes.forEach { aKey, aValue -> + if (aKey.key == key) { + value = aValue as? T + throw StopException() + } + } + } catch (_: StopException) { + } + + return value as? T + } /** * Sets a String value for the given key. @@ -53,6 +67,7 @@ class MutableAttributes @JvmOverloads constructor( * @param key the key to set * @param value the value to associate */ + @Synchronized operator fun set(key: String, value: String) { attributes = attributes.edit { put(AttributeKey.stringKey(key), value) } } @@ -63,6 +78,7 @@ class MutableAttributes @JvmOverloads constructor( * @param key the key to set * @param value the value to associate */ + @Synchronized operator fun set(key: String, value: Long) { attributes = attributes.edit { put(AttributeKey.longKey(key), value) } } @@ -73,6 +89,7 @@ class MutableAttributes @JvmOverloads constructor( * @param key the key to set * @param value the value to associate */ + @Synchronized operator fun set(key: String, value: Double) { attributes = attributes.edit { put(AttributeKey.doubleKey(key), value) } } @@ -83,6 +100,7 @@ class MutableAttributes @JvmOverloads constructor( * @param key the key to set * @param value the value to associate */ + @Synchronized operator fun set(key: String, value: Boolean) { attributes = attributes.edit { put(AttributeKey.booleanKey(key), value) } } @@ -93,6 +111,7 @@ class MutableAttributes @JvmOverloads constructor( * @param key the attribute key * @param value the value to associate */ + @Synchronized operator fun set(key: AttributeKey, value: T) { attributes = attributes.edit { put(key, value) } } @@ -102,6 +121,7 @@ class MutableAttributes @JvmOverloads constructor( * * @param key the attribute key */ + @Synchronized fun remove(key: AttributeKey) { attributes = attributes.edit { remove(key) } } @@ -111,6 +131,7 @@ class MutableAttributes @JvmOverloads constructor( * * @param key the string key to remove */ + @Synchronized fun remove(key: String) { attributes = attributes.edit { removeIf { it.key == key } } } @@ -118,6 +139,7 @@ class MutableAttributes @JvmOverloads constructor( /** * Removes all attributes. */ + @Synchronized fun removeAll() { attributes = Attributes.empty() } @@ -127,6 +149,7 @@ class MutableAttributes @JvmOverloads constructor( * * @param attributesToAdd the attributes to merge */ + @Synchronized fun setAll(attributesToAdd: Attributes) { attributes = attributes.edit { putAll(attributesToAdd) } } @@ -136,6 +159,7 @@ class MutableAttributes @JvmOverloads constructor( * * @return the current attributes */ + @Synchronized fun getAll(): Attributes = attributes /** @@ -144,20 +168,28 @@ class MutableAttributes @JvmOverloads constructor( * * @param updateAttributes a lambda to modify the current attributes. The lambda receives an [AttributesBuilder]. */ + @Synchronized fun update(updateAttributes: AttributesBuilder.() -> Unit) { attributes = attributes.edit(updateAttributes) } + @Synchronized override fun forEach(consumer: BiConsumer, in Any>) = attributes.forEach(consumer) + @Synchronized override fun size(): Int = attributes.size() + @Synchronized override fun isEmpty(): Boolean = attributes.isEmpty + @Synchronized override fun asMap(): Map, Any> = attributes.asMap() + @Synchronized override fun toBuilder(): AttributesBuilder = attributes.toBuilder() private inline fun Attributes.edit(block: AttributesBuilder.() -> Unit): Attributes = toBuilder().apply(block).build() + + private class StopException : Exception() } From ccfc3bb4b1a6096cf52a880dcda0594e1348c8b7 Mon Sep 17 00:00:00 2001 From: Tomas Chladek Date: Wed, 4 Jun 2025 00:32:08 +0200 Subject: [PATCH 2/2] MutableAttributes unit tests Signed-off-by: Tomas Chladek --- .../common/attributes/MutableAttributesTest.kt | 14 ++++++++++++++ .../agent/common/attributes/MutableAttributes.kt | 5 ++--- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/integration/agent/api/src/test/java/com/splunk/rum/integration/agent/common/attributes/MutableAttributesTest.kt b/integration/agent/api/src/test/java/com/splunk/rum/integration/agent/common/attributes/MutableAttributesTest.kt index bd8415bd3..4939f0553 100644 --- a/integration/agent/api/src/test/java/com/splunk/rum/integration/agent/common/attributes/MutableAttributesTest.kt +++ b/integration/agent/api/src/test/java/com/splunk/rum/integration/agent/common/attributes/MutableAttributesTest.kt @@ -35,6 +35,20 @@ class MutableAttributesTest { Assert.assertEquals("value", mutableAttributes["key"]) } + @Test + fun `modify value with String key`() { + val mutableAttributes = MutableAttributes() + + mutableAttributes["key"] = "value" + Assert.assertEquals("value", mutableAttributes["key"]) + + mutableAttributes["key"] = "value1" + Assert.assertEquals("value1", mutableAttributes["key"]) + + mutableAttributes["key"] = 0L + Assert.assertEquals(0L, mutableAttributes["key"]) + } + @Test fun `get and set with AttributeKey`() { val mutableAttributes = MutableAttributes() diff --git a/integration/agent/common/src/main/java/com/splunk/rum/integration/agent/common/attributes/MutableAttributes.kt b/integration/agent/common/src/main/java/com/splunk/rum/integration/agent/common/attributes/MutableAttributes.kt index 3057c8fa8..dc74bc9cc 100644 --- a/integration/agent/common/src/main/java/com/splunk/rum/integration/agent/common/attributes/MutableAttributes.kt +++ b/integration/agent/common/src/main/java/com/splunk/rum/integration/agent/common/attributes/MutableAttributes.kt @@ -42,11 +42,10 @@ class MutableAttributes @JvmOverloads constructor(private var attributes: Attrib * @param key the string key to retrieve * @return the value if present, or null */ - @Suppress("UNCHECKED_CAST") @Synchronized operator fun get(key: String): T? { - var value: Any? = null + var value: T? = null try { attributes.forEach { aKey, aValue -> @@ -58,7 +57,7 @@ class MutableAttributes @JvmOverloads constructor(private var attributes: Attrib } catch (_: StopException) { } - return value as? T + return value } /**