Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 33298765 authored by Massimo Carli's avatar Massimo Carli Committed by Android (Google) Code Review
Browse files

Merge "[51/n] Generic Repository definition" into main

parents 45d6e93f cd659568
Loading
Loading
Loading
Loading
+94 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.wm.shell.repository

/**
 * Simple repository abstraction for common use.
 * @param Key     The generic type for the key.
 * @param Item    The generic type for the value stored in the repository.
 */
interface GenericRepository<Key, Item> {

    /**
     * @return The Item for a given [key] if present.
     */
    fun find(key: Key): Item?

    /**
     * Inserts the given [item] for the given [key].
     *
     * @param key   The Key for the new item
     * @param item  The Item for the given key.
     * @param overrideIfPresent If {@code true} the value is updated when already present.
     * @return `true` if the value has been updated and `false` otherwise.
     */
    fun insert(key: Key, item: Item, overrideIfPresent: Boolean = true): Boolean

    /**
     * Inserts the given [item] for the given [key]. The [item] is created only in the case
     * when [overrideIfPresent] is [true] or it's not present. This prevents the [item] from
     * being created if not necessary.
     *
     * @param key   The Key for the new item
     * @param item  The Item for the given key.
     * @param overrideIfPresent If {@code true} the value is updated when already present.
     * @return `true` if the value has been updated and `false` otherwise.
     */
    fun insert(key: Key, itemFactory: () -> Item, overrideIfPresent: Boolean = true): Boolean

    /**
     * Deletes the Item for the given [key].
     *
     * @param key   The Key of the item to delete.
     * @return `true` if the item has been removed and `false` otherwise.
     */
    fun delete(key: Key): Boolean

    /**
     * Search for the Item for the given [key] and invoked the [onItem] on it if
     * present.
     *
     * @param key   The Key for the Item in the repository.
     * @param defaultResult The value to return in case there's no value for the key
     * @param onItem    The function to invoke in case the Item is present.
     * @return The result of [onItem] if the item is present or [defaultResult] otherwise.
     */
    fun executeOn(
        key: Key,
        defaultResult: Boolean = false,
        onItem: (Item) -> Boolean
    ): Boolean = find(key)?.let { onItem(it) } ?: defaultResult

    /**
     * Updates the value if present.
     *
     * @param key   The Key for the Item in the repository.
     * @param updateItem The function which updated the Item if present.
     * @return [true] if the item has been updated and [false] if not present.
     */
    fun update(key: Key, updateItem: (Item) -> Item): Boolean

    /**
     * Finds the items for a given [predicate].
     */
    fun find(predicate: (Key, Item) -> Boolean): List<Item>

    /**
     * Shows what's in the repository for debugging purpose.
     */
    fun dump()
}
+70 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.wm.shell.repository

/**
 * In memory [GenericRepository] implementation.
 */
class MemoryRepositoryImpl<Key, Item>(private val logger: (String) -> Unit = { _ -> }) :
    GenericRepository<Key, Item> {

    private var memoryStore = mutableMapOf<Key, Item>()

    override fun find(key: Key): Item? = memoryStore[key]

    override fun insert(
        key: Key,
        item: Item,
        overrideIfPresent: Boolean
    ): Boolean {
        if (find(key) != null && !overrideIfPresent) {
            return false
        }
        memoryStore[key] = item
        return true
    }

    override fun insert(
        key: Key,
        itemFactory: () -> Item,
        overrideIfPresent: Boolean
    ): Boolean {
        if (find(key) != null && !overrideIfPresent) {
            return false
        }
        memoryStore[key] = itemFactory()
        return true
    }

    override fun delete(key: Key): Boolean = memoryStore.remove(key) != null

    override fun find(predicate: (Key, Item) -> Boolean): List<Item> =
        memoryStore.entries
            .filter { element -> predicate(element.key, element.value) }
            .map { element -> element.value }

    override fun update(key: Key, updateItem: (Item) -> Item): Boolean {
        find(key)?.let { item ->
            return insert(key, updateItem(item))
        }
        return false
    }

    override fun dump() {
        logger("Repository dump: $memoryStore")
    }
}
+33 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.wm.shell.repository

// Key for testing
data class FakeKey(val id1: Int, val id2: String)

// Item for testing
data class FakeItem(val value1: Int, val value2: String)

// Fake Factory for the Item for testing
class FakeItemFactory(val value1: Int, val value2: String) : () -> FakeItem {

    var fakeFactoryInvokedTimes = 0

    override fun invoke(): FakeItem = FakeItem(value1, value2).also {
        fakeFactoryInvokedTimes++
    }
}
+109 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.wm.shell.repository

import android.testing.AndroidTestingRunner
import androidx.test.filters.SmallTest
import com.android.wm.shell.ShellTestCase
import org.junit.Test
import org.junit.runner.RunWith
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertNull
import kotlin.test.assertTrue

/**
 * Tests for [GenericRepository].
 *
 * Build/Install/Run:
 *  atest WMShellUnitTests:GenericRepositoryTest
 */
@RunWith(AndroidTestingRunner::class)
@SmallTest
class GenericRepositoryTest : ShellTestCase() {

    @Test
    fun `callback not invoked when item is not found and default is true`() {
        val repository = MemoryRepositoryImpl<FakeKey, FakeItem>()
        var returnedItem: FakeItem? = null
        val result =
            repository.executeOn(
                FakeKey(id1 = 0, id2 = ""),
                defaultResult = true
            ) { item ->
                returnedItem = item
                true
            }
        assertNull(returnedItem)
        assertTrue(result)
    }

    @Test
    fun `callback not invoked when item is not found and default is false`() {
        val repository = MemoryRepositoryImpl<FakeKey, FakeItem>()
        var returnedItem: FakeItem? = null
        val result =
            repository.executeOn(
                FakeKey(id1 = 0, id2 = "")
            ) { item ->
                returnedItem = item
                true
            }
        assertNull(returnedItem)
        assertFalse(result)
    }

    @Test
    fun `callback invoked when item is found and returns true`() {
        val repository = MemoryRepositoryImpl<FakeKey, FakeItem>().apply {
            insert(
                key = FakeKey(id1 = 1, id2 = "test"),
                item = FakeItem(value1 = 1, value2 = "item")
            )
        }
        var returnedItem: FakeItem? = null
        val result =
            repository.executeOn(
                FakeKey(id1 = 1, id2 = "test")
            ) { item ->
                returnedItem = item
                true
            }
        assertEquals(FakeItem(value1 = 1, value2 = "item"), returnedItem)
        assertTrue(result)
    }

    @Test
    fun `callback invoked when item is found and returns false`() {
        val repository = MemoryRepositoryImpl<FakeKey, FakeItem>().apply {
            insert(
                key = FakeKey(id1 = 1, id2 = "test"),
                item = FakeItem(value1 = 1, value2 = "item")
            )
        }
        var returnedItem: FakeItem? = null
        val result =
            repository.executeOn(
                FakeKey(id1 = 1, id2 = "test")
            ) { item ->
                returnedItem = item
                false
            }
        assertEquals(FakeItem(value1 = 1, value2 = "item"), returnedItem!!)
        assertFalse(result)
    }
}
+174 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.wm.shell.repository

import android.testing.AndroidTestingRunner
import androidx.test.filters.SmallTest
import com.android.wm.shell.ShellTestCase
import com.android.wm.shell.util.FakeLogger
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertNotNull
import kotlin.test.assertNull
import kotlin.test.assertTrue
import kotlin.test.fail

/**
 * Tests for [MemoryRepositoryImpl].
 *
 * Build/Install/Run:
 *  atest WMShellUnitTests:MemoryRepositoryImplTest
 */
@RunWith(AndroidTestingRunner::class)
@SmallTest
class MemoryRepositoryImplTest : ShellTestCase() {

    private lateinit var repository: MemoryRepositoryImpl<FakeKey, FakeItem>
    private lateinit var fakeLogger: FakeLogger

    @Before
    fun setUp() {
        fakeLogger = FakeLogger()
        repository = MemoryRepositoryImpl(fakeLogger.logger)
    }

    @Test
    fun `itemFactory invoked when item not present`() {
        val itemFactory = FakeItemFactory(0, "test")

        // The item is initially not present.
        assertTrue(repository.insert(FakeKey(1, "test1"), itemFactory))
        assertTrue(itemFactory.fakeFactoryInvokedTimes == 1)

        // The item is then present.
        assertNotNull(repository.find(FakeKey(1, "test1")))

        // Insert again with overrideIfPresent == true so the factory is invoked again.
        assertTrue(repository.insert(FakeKey(1, "test1"), itemFactory))
        assertTrue(itemFactory.fakeFactoryInvokedTimes == 2)

        // Insert again with overrideIfPresent == false so the factory is not invoked again and
        // the item not inserted.
        assertFalse(repository.insert(FakeKey(1, "test1"), itemFactory, overrideIfPresent = false))
        assertTrue(itemFactory.fakeFactoryInvokedTimes == 2)
    }

    @Test
    fun `find returns the item when present`() {
        val item = FakeItem(0, "test")

        // The item is initially not present.
        assertNull(repository.find(FakeKey(1, "test1")))
        assertTrue(repository.insert(FakeKey(1, "test1"), item))

        // The item is then present.
        assertNotNull(repository.find(FakeKey(1, "test1")))
        assertEquals(item, repository.find(FakeKey(1, "test1")))
    }

    @Test
    fun `delete removes the item and returns if the item was present`() {
        val item = FakeItem(0, "test")

        // The item cannot be removed for a not existing key.
        assertFalse(repository.delete(FakeKey(1, "test1")))

        // We insert the item.
        assertTrue(repository.insert(FakeKey(1, "test1"), item))
        assertNotNull(repository.find(FakeKey(1, "test1")))

        // We try to remove a different key and the item is still present.
        assertFalse(repository.delete(FakeKey(0, "test1")))
        assertNotNull(repository.find(FakeKey(1, "test1")))

        // We remove the existing key and the item is not present.
        assertTrue(repository.delete(FakeKey(1, "test1")))
        assertNull(repository.find(FakeKey(1, "test1")))
    }

    @Test
    fun `insert adds the items and return the right value`() {
        val item1 = FakeItem(1, "test1")
        val item2 = FakeItem(2, "test2")

        // Item is not present
        assertNull(repository.find(FakeKey(1, "test1")))

        // Insert when the item is not present. Then it's added.
        assertTrue(repository.insert(FakeKey(1, "test1"), item1))
        assertNotNull(repository.find(FakeKey(1, "test1")))
        assertEquals(item1, repository.find(FakeKey(1, "test1")))

        // Insert when the item is already present and overrideIfPresent is true by default.
        assertTrue(repository.insert(FakeKey(1, "test1"), item2))
        assertNotNull(repository.find(FakeKey(1, "test1")))
        assertEquals(item2, repository.find(FakeKey(1, "test1")))

        // Insert when the item is already present but overrideIfPresent is false.
        assertFalse(
            repository.insert(FakeKey(1, "test1"), item1, overrideIfPresent = false)
        )
        assertNotNull(repository.find(FakeKey(1, "test1")))
        assertEquals(item2, repository.find(FakeKey(1, "test1")))
    }

    @Test
    fun `Predicate is invoked on items`() {
        val item1 = FakeItem(1, "test1")
        val item2 = FakeItem(2, "test2")

        repository.insert(FakeKey(1, "test1"), item1)
        repository.insert(FakeKey(1, "test2"), item2)

        assertTrue(repository.find { key, _ -> key.id1 == 1 }.size == 2)
        assertTrue(repository.find { key, _ -> key.id2 == "test2" }.size == 1)
        assertTrue(repository.find { key, _ -> key.id1 == 2 && key.id2 == "test" }.isEmpty())
    }

    @Test
    fun `Item is not updated if not present`() {
        val result = repository.update(FakeKey(id1 = 1, id2 = "test")) { item ->
            fail("This should not be invoked")
        }
        assertFalse(result)
    }

    @Test
    fun `Item is updated if present`() {
        val item1 = FakeItem(1, "test1")
        val item2 = FakeItem(2, "test2")
        repository.insert(FakeKey(id1 = 1, id2 = "test"), item1)

        val result = repository.update(FakeKey(id1 = 1, id2 = "test")) { item ->
            assert(item == item1)
            item2
        }
        assertTrue(result)
        assertTrue(repository.find { key, _ -> key.id1 == 1 }.size == 1)
        assertTrue(repository.find { key, _ -> key.id1 == 1 }[0] == item2)
    }

    @Test
    fun `Logger is used on dump`() {
        fakeLogger.assertLoggerInvocation(0)
        repository.dump()
        fakeLogger.assertLoggerInvocation(1)
    }
}
Loading