Merge "Add testcases for KeyedObserver" into main
diff --git a/packages/SettingsLib/DataStore/tests/src/com/android/settingslib/datastore/KeyedObserverTest.kt b/packages/SettingsLib/DataStore/tests/src/com/android/settingslib/datastore/KeyedObserverTest.kt
new file mode 100644
index 0000000..b52586c
--- /dev/null
+++ b/packages/SettingsLib/DataStore/tests/src/com/android/settingslib/datastore/KeyedObserverTest.kt
@@ -0,0 +1,215 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * 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.android.settingslib.datastore
+
+import com.google.common.truth.Truth.assertThat
+import com.google.common.util.concurrent.MoreExecutors.directExecutor
+import java.util.concurrent.Executor
+import java.util.concurrent.atomic.AtomicInteger
+import org.junit.Assert
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+import org.mockito.kotlin.any
+import org.mockito.kotlin.never
+import org.mockito.kotlin.reset
+import org.mockito.kotlin.verify
+import org.robolectric.RobolectricTestRunner
+
+@RunWith(RobolectricTestRunner::class)
+class KeyedObserverTest {
+    @get:Rule
+    val mockitoRule: MockitoRule = MockitoJUnit.rule()
+
+    @Mock
+    private lateinit var observer1: KeyedObserver<Any?>
+
+    @Mock
+    private lateinit var observer2: KeyedObserver<Any?>
+
+    @Mock
+    private lateinit var keyedObserver1: KeyedObserver<Any>
+
+    @Mock
+    private lateinit var keyedObserver2: KeyedObserver<Any>
+
+    @Mock
+    private lateinit var key1: Any
+
+    @Mock
+    private lateinit var key2: Any
+
+    @Mock
+    private lateinit var executor: Executor
+
+    private val keyedObservable = KeyedDataObservable<Any>()
+
+    @Test
+    fun addObserver_sameExecutor() {
+        keyedObservable.addObserver(observer1, executor)
+        keyedObservable.addObserver(observer1, executor)
+    }
+
+    @Test
+    fun addObserver_keyedObserver_sameExecutor() {
+        keyedObservable.addObserver(key1, keyedObserver1, executor)
+        keyedObservable.addObserver(key1, keyedObserver1, executor)
+    }
+
+    @Test
+    fun addObserver_differentExecutor() {
+        keyedObservable.addObserver(observer1, executor)
+        Assert.assertThrows(IllegalStateException::class.java) {
+            keyedObservable.addObserver(observer1, directExecutor())
+        }
+    }
+
+    @Test
+    fun addObserver_keyedObserver_differentExecutor() {
+        keyedObservable.addObserver(key1, keyedObserver1, executor)
+        Assert.assertThrows(IllegalStateException::class.java) {
+            keyedObservable.addObserver(key1, keyedObserver1, directExecutor())
+        }
+    }
+
+    @Test
+    fun addObserver_weaklyReferenced() {
+        val counter = AtomicInteger()
+        var observer: KeyedObserver<Any?>? = KeyedObserver { _, _ -> counter.incrementAndGet() }
+        keyedObservable.addObserver(observer!!, directExecutor())
+
+        keyedObservable.notifyChange(ChangeReason.UPDATE)
+        assertThat(counter.get()).isEqualTo(1)
+
+        // trigger GC, the observer callback should not be invoked
+        null.also { observer = it }
+        System.gc()
+        System.runFinalization()
+
+        keyedObservable.notifyChange(ChangeReason.UPDATE)
+        assertThat(counter.get()).isEqualTo(1)
+    }
+
+    @Test
+    fun addObserver_keyedObserver_weaklyReferenced() {
+        val counter = AtomicInteger()
+        var keyObserver: KeyedObserver<Any>? = KeyedObserver { _, _ -> counter.incrementAndGet() }
+        keyedObservable.addObserver(key1, keyObserver!!, directExecutor())
+
+        keyedObservable.notifyChange(key1, ChangeReason.UPDATE)
+        assertThat(counter.get()).isEqualTo(1)
+
+        // trigger GC, the observer callback should not be invoked
+        null.also { keyObserver = it }
+        System.gc()
+        System.runFinalization()
+
+        keyedObservable.notifyChange(key1, ChangeReason.UPDATE)
+        assertThat(counter.get()).isEqualTo(1)
+    }
+
+    @Test
+    fun addObserver_notifyObservers_removeObserver() {
+        keyedObservable.addObserver(observer1, directExecutor())
+        keyedObservable.addObserver(observer2, executor)
+
+        keyedObservable.notifyChange(ChangeReason.UPDATE)
+        verify(observer1).onKeyChanged(null, ChangeReason.UPDATE)
+        verify(observer2, never()).onKeyChanged(any(), any())
+        verify(executor).execute(any())
+
+        reset(observer1, executor)
+        keyedObservable.removeObserver(observer2)
+
+        keyedObservable.notifyChange(ChangeReason.DELETE)
+        verify(observer1).onKeyChanged(null, ChangeReason.DELETE)
+        verify(executor, never()).execute(any())
+    }
+
+    @Test
+    fun addObserver_keyedObserver_notifyObservers_removeObserver() {
+        keyedObservable.addObserver(key1, keyedObserver1, directExecutor())
+        keyedObservable.addObserver(key2, keyedObserver2, executor)
+
+        keyedObservable.notifyChange(key1, ChangeReason.UPDATE)
+        verify(keyedObserver1).onKeyChanged(key1, ChangeReason.UPDATE)
+        verify(keyedObserver2, never()).onKeyChanged(any(), any())
+        verify(executor, never()).execute(any())
+
+        reset(keyedObserver1, executor)
+        keyedObservable.removeObserver(key2, keyedObserver2)
+
+        keyedObservable.notifyChange(key1, ChangeReason.DELETE)
+        verify(keyedObserver1).onKeyChanged(key1, ChangeReason.DELETE)
+        verify(executor, never()).execute(any())
+    }
+
+    @Test
+    fun notifyChange_addMoreTypeObservers_checkOnKeyChanged() {
+        keyedObservable.addObserver(observer1, directExecutor())
+        keyedObservable.addObserver(key1, keyedObserver1, directExecutor())
+        keyedObservable.addObserver(key2, keyedObserver2, directExecutor())
+
+        keyedObservable.notifyChange(ChangeReason.UPDATE)
+        verify(observer1).onKeyChanged(null, ChangeReason.UPDATE)
+        verify(keyedObserver1).onKeyChanged(key1, ChangeReason.UPDATE)
+        verify(keyedObserver2).onKeyChanged(key2, ChangeReason.UPDATE)
+
+        reset(observer1, keyedObserver1, keyedObserver2)
+        keyedObservable.notifyChange(key1, ChangeReason.UPDATE)
+
+        verify(observer1).onKeyChanged(key1, ChangeReason.UPDATE)
+        verify(keyedObserver1).onKeyChanged(key1, ChangeReason.UPDATE)
+        verify(keyedObserver2, never()).onKeyChanged(key1, ChangeReason.UPDATE)
+
+        reset(observer1, keyedObserver1, keyedObserver2)
+        keyedObservable.notifyChange(key2, ChangeReason.UPDATE)
+
+        verify(observer1).onKeyChanged(key2, ChangeReason.UPDATE)
+        verify(keyedObserver1, never()).onKeyChanged(key2, ChangeReason.UPDATE)
+        verify(keyedObserver2).onKeyChanged(key2, ChangeReason.UPDATE)
+    }
+
+    @Test
+    fun notifyChange_addObserverWithinCallback() {
+        // ConcurrentModificationException is raised if it is not implemented correctly
+        val observer: KeyedObserver<Any?> = KeyedObserver { _, _ ->
+            keyedObservable.addObserver(observer1, executor)
+        }
+
+        keyedObservable.addObserver(observer, directExecutor())
+
+        keyedObservable.notifyChange(ChangeReason.UPDATE)
+        keyedObservable.removeObserver(observer)
+    }
+
+    @Test
+    fun notifyChange_KeyedObserver_addObserverWithinCallback() {
+        // ConcurrentModificationException is raised if it is not implemented correctly
+        val keyObserver: KeyedObserver<Any?> = KeyedObserver { _, _ ->
+            keyedObservable.addObserver(key1, keyedObserver1, executor)
+        }
+
+        keyedObservable.addObserver(key1, keyObserver, directExecutor())
+
+        keyedObservable.notifyChange(key1, ChangeReason.UPDATE)
+        keyedObservable.removeObserver(key1, keyObserver)
+    }
+}
\ No newline at end of file
diff --git a/packages/SettingsLib/DataStore/tests/src/com/android/settingslib/datastore/ObserverTest.kt b/packages/SettingsLib/DataStore/tests/src/com/android/settingslib/datastore/ObserverTest.kt
index bb791dc..f065829 100644
--- a/packages/SettingsLib/DataStore/tests/src/com/android/settingslib/datastore/ObserverTest.kt
+++ b/packages/SettingsLib/DataStore/tests/src/com/android/settingslib/datastore/ObserverTest.kt
@@ -69,8 +69,7 @@
         assertThat(counter.get()).isEqualTo(1)
 
         // trigger GC, the observer callback should not be invoked
-        @Suppress("unused")
-        observer = null
+        null.also { observer = it }
         System.gc()
         System.runFinalization()
 
@@ -100,10 +99,12 @@
     @Test
     fun notifyChange_addObserverWithinCallback() {
         // ConcurrentModificationException is raised if it is not implemented correctly
+        val observer = Observer { observable.addObserver(observer1, executor) }
         observable.addObserver(
-            { observable.addObserver(observer1, executor) },
+            observer,
             MoreExecutors.directExecutor()
         )
         observable.notifyChange(ChangeReason.UPDATE)
+        observable.removeObserver(observer)
     }
 }