Add tests for LRUCache

Adds some simple unittests to ensure that LRUCache works the way it
should.

Change-Id: I49a6229509cd1ab00fa076d7987ec81e87af6138
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/255563
Commit-Queue: Brandon Jones <bajones@chromium.org>
Reviewed-by: Shrek Shao <shrekshao@google.com>
Reviewed-by: Loko Kung <lokokung@google.com>
diff --git a/src/dawn/tests/BUILD.gn b/src/dawn/tests/BUILD.gn
index 176a5d5..8f5b8d6 100644
--- a/src/dawn/tests/BUILD.gn
+++ b/src/dawn/tests/BUILD.gn
@@ -335,6 +335,7 @@
     "unittests/ITypBitsetTests.cpp",
     "unittests/ITypSpanTests.cpp",
     "unittests/ITypVectorTests.cpp",
+    "unittests/LRUCacheTests.cpp",
     "unittests/LinkedListTests.cpp",
     "unittests/MathTests.cpp",
     "unittests/MutexProtectedTests.cpp",
diff --git a/src/dawn/tests/unittests/LRUCacheTests.cpp b/src/dawn/tests/unittests/LRUCacheTests.cpp
new file mode 100644
index 0000000..d58642f
--- /dev/null
+++ b/src/dawn/tests/unittests/LRUCacheTests.cpp
@@ -0,0 +1,208 @@
+// Copyright 2025 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "dawn/common/HashUtils.h"
+#include "dawn/common/LRUCache.h"
+#include "dawn/native/Error.h"
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+
+namespace dawn::native {
+namespace {
+
+class CacheKey {
+  public:
+    explicit CacheKey(uint32_t value, bool isError = false) : mValue(value), mIsError(isError) {}
+
+    const uint32_t mValue;
+    const bool mIsError;
+};
+
+class CacheValue {
+  public:
+    explicit CacheValue(uint32_t value) : mValue(value), mId(CacheValue::mNextId++) {}
+
+    uint32_t mValue;
+    uint32_t mId;
+
+  private:
+    static uint32_t mNextId;
+};
+
+uint32_t CacheValue::mNextId = 1;
+
+struct CacheFuncs {
+    size_t operator()(const CacheKey& key) const {
+        size_t hash = Hash(key.mValue);
+        HashCombine(&hash, key.mIsError);
+        return hash;
+    }
+    bool operator()(const CacheKey& a, const CacheKey& b) const {
+        return a.mValue == b.mValue && a.mIsError == b.mIsError;
+    }
+};
+
+class TestCache final : public LRUCache<CacheKey, CacheValue, ErrorData, CacheFuncs> {
+    using Base = LRUCache<CacheKey, CacheValue, ErrorData, CacheFuncs>;
+
+  public:
+    static const size_t kDefaultCapacity = 4;
+    explicit TestCache(size_t capacity = kDefaultCapacity) : Base(capacity) {}
+    ~TestCache() override = default;
+
+    MOCK_METHOD(void, EvictedFromCache, (const CacheValue&), (override));
+
+    // Calls GetOrCreate, assuming no error.
+    CacheValue GetOrCreateNoError(CacheKey& key) {
+        auto result = GetOrCreate(key, &TestCache::CreateFn);
+        EXPECT_FALSE(result.IsError());
+        return result.AcquireSuccess();
+    }
+
+    static Result<CacheValue, ErrorData> CreateFn(const CacheKey& key) {
+        DAWN_INTERNAL_ERROR_IF(key.mIsError, "CacheKey was an error key");
+        return CacheValue(key.mValue);
+    }
+};
+
+// A cache size of 0 disables caching, but still creates the value.
+TEST(LRUCache, ZeroSizedCache) {
+    TestCache cache(0);
+
+    CacheKey key(1);
+    uint32_t cacheId;
+    {
+        // The constructed values should be evicted immediately.
+        EXPECT_CALL(cache, EvictedFromCache(testing::_));
+        CacheValue value = cache.GetOrCreateNoError(key);
+        cacheId = value.mId;
+    }
+
+    {
+        EXPECT_CALL(cache, EvictedFromCache(testing::_));
+        CacheValue value = cache.GetOrCreateNoError(key);
+
+        // A new cached value should be created.
+        EXPECT_NE(cacheId, value.mId);
+    }
+}
+
+// A cache with a non-zero capacity should return the same value for equivalent keys
+TEST(LRUCache, Basic) {
+    TestCache cache;
+
+    uint32_t cacheId;
+    {
+        CacheKey key(1);
+        CacheValue value = cache.GetOrCreateNoError(key);
+        cacheId = value.mId;
+    }
+
+    {
+        CacheKey key(1);
+        CacheValue value = cache.GetOrCreateNoError(key);
+
+        // A new CacheValue should be returned.
+        EXPECT_EQ(cacheId, value.mId);
+    }
+}
+
+// A cache with a non-zero capacity should begin evicting old values after it reached capacity.
+TEST(LRUCache, CacheEviction) {
+    TestCache cache;
+
+    std::array<uint32_t, TestCache::kDefaultCapacity> keyIds;
+    // Fill the cache with values.
+    for (uint32_t i = 0; i < TestCache::kDefaultCapacity; ++i) {
+        CacheKey key(i);
+        auto value = cache.GetOrCreateNoError(key);
+        keyIds[i] = value.mId;
+    }
+
+    // Calling GetOrCreate with an existing key won't cause a cache eviction
+    {
+        CacheKey key(1);
+        EXPECT_CALL(cache, EvictedFromCache(testing::_)).Times(0);
+        cache.GetOrCreateNoError(key);
+    }
+
+    // Calling GetOrCreate with a new key should evict an older value.
+    {
+        CacheKey key(TestCache::kDefaultCapacity);
+        EXPECT_CALL(cache, EvictedFromCache(testing::_));
+        cache.GetOrCreateNoError(key);
+    }
+
+    // The oldest value in the cache (key 0) should have been the one evicted,
+    // as evidenced by us receiving a new value when querying it again.
+    {
+        CacheKey key(0);
+        EXPECT_CALL(cache, EvictedFromCache(testing::_));
+        auto value = cache.GetOrCreateNoError(key);
+        EXPECT_NE(value.mId, keyIds[0]);
+    }
+
+    // The value for key 1 should not have been the one evicted after the last
+    // call, because it was queried more recently than the other keys.
+    {
+        CacheKey key(1);
+        EXPECT_CALL(cache, EvictedFromCache(testing::_)).Times(0);
+        auto value = cache.GetOrCreateNoError(key);
+        EXPECT_EQ(value.mId, keyIds[1]);
+    }
+}
+
+// The Clear method should evict all values from the cache.
+TEST(LRUCache, CacheClear) {
+    TestCache cache;
+
+    std::array<uint32_t, TestCache::kDefaultCapacity> keyIds;
+    // Fill the cache with values.
+    for (uint32_t i = 0; i < TestCache::kDefaultCapacity; ++i) {
+        CacheKey key(i);
+        auto value = cache.GetOrCreateNoError(key);
+        keyIds[i] = value.mId;
+    }
+
+    // Calling Clear should evict all values from the cache.
+    {
+        EXPECT_CALL(cache, EvictedFromCache(testing::_)).Times(TestCache::kDefaultCapacity);
+        cache.Clear();
+    }
+
+    // Calling GetOrCreate with a previously created key should return a new
+    // value and cause no evictions
+    {
+        CacheKey key(0);
+        EXPECT_CALL(cache, EvictedFromCache(testing::_)).Times(0);
+        auto value = cache.GetOrCreateNoError(key);
+        EXPECT_NE(value.mId, keyIds[0]);
+    }
+}
+
+}  // anonymous namespace
+}  // namespace dawn::native