/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=8 sts=2 et sw=2 tw=80: */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "gtest/gtest.h" #include "mozilla/SmallArrayLRUCache.h" #include #include #include using Key = unsigned; struct Value { Value() : m(unsigned(-1)) {} explicit Value(unsigned a) : m(a) {} bool operator==(const Value& aOther) const { return m == aOther.m; } bool operator!=(const Value& aOther) const { return m != aOther.m; } unsigned m; }; constexpr static unsigned CacheSize = 8; using TestCache = mozilla::SmallArrayLRUCache; // This struct embeds a given object type between two "guard" objects, to check // if anything is written out of bounds. template struct Boxed { constexpr static size_t GuardSize = std::max(sizeof(T), size_t(256)); // A Guard is a character array with a pre-set content that can be checked for // unwanted changes. struct Guard { char mGuard[GuardSize]; explicit Guard(char aValue) { memset(&mGuard, aValue, GuardSize); } void Check(char aValue) { for (const char& c : mGuard) { ASSERT_EQ(c, aValue); } } }; Guard mGuardBefore; T mObject; Guard mGuardAfter; template explicit Boxed(Ts&&... aTs) : mGuardBefore(0x5a), mObject(std::forward(aTs)...), mGuardAfter(0xa5) { Check(); } ~Boxed() { Check(); } T& Object() { return mObject; } const T& Object() const { return mObject; } void Check() { mGuardBefore.Check(0x5a); mGuardAfter.Check(0xa5); } }; TEST(SmallArrayLRUCache, FetchOrAdd_KeysFitInCache) { // We're going to add-or-fetch between 1 and CacheSize keys, so they all fit // in the cache. for (Key keys = 1; keys <= CacheSize; ++keys) { Boxed boxedCache; TestCache& cache = boxedCache.Object(); for (Key i = 0; i < keys; ++i) { bool valueFunctionCalled = false; Value v = cache.FetchOrAdd(i, [&]() { valueFunctionCalled = true; return Value{i}; }); ASSERT_EQ(v, Value{i}); ASSERT_TRUE(valueFunctionCalled); boxedCache.Check(); } // Fetching any key should never call the value function. for (Key i = 0; i < CacheSize * 3; ++i) { { bool valueFunctionCalled = false; Value v = cache.FetchOrAdd(i % keys, [&]() { valueFunctionCalled = true; return Value{i % keys}; }); ASSERT_EQ(v, Value{i % keys}); ASSERT_FALSE(valueFunctionCalled); boxedCache.Check(); } // Fetching the same key again will never call the function value. { bool valueFunctionCalled = false; Value v = cache.FetchOrAdd(i % keys, [&]() { valueFunctionCalled = true; return Value{i % keys}; }); ASSERT_EQ(v, Value{i % keys}); ASSERT_FALSE(valueFunctionCalled); boxedCache.Check(); } } } } TEST(SmallArrayLRUCache, Add_FetchOrAdd_KeysFitInCache) { // We're going to add between 1 and CacheSize keys, so they all fit in the // cache. for (Key keys = 1; keys <= CacheSize; ++keys) { Boxed boxedCache; TestCache& cache = boxedCache.Object(); for (Key i = 0; i < keys; ++i) { cache.Add(i, Value{i}); boxedCache.Check(); } // Fetching any key should never call the value function. for (Key i = 0; i < CacheSize * 3; ++i) { { bool valueFunctionCalled = false; Value v = cache.FetchOrAdd(i % keys, [&]() { valueFunctionCalled = true; return Value{i % keys}; }); ASSERT_EQ(v, Value{i % keys}); ASSERT_FALSE(valueFunctionCalled); boxedCache.Check(); } // Fetching the same key again will never call the function value. { bool valueFunctionCalled = false; Value v = cache.FetchOrAdd(i % keys, [&]() { valueFunctionCalled = true; return Value{i % keys}; }); ASSERT_EQ(v, Value{i % keys}); ASSERT_FALSE(valueFunctionCalled); boxedCache.Check(); } } } } TEST(SmallArrayLRUCache, FetchOrAdd_KeysDoNotFitInCache) { // We're going to add-or-fetch strictly more than CacheSize keys, so they // cannot fit in the cache, only the last `CacheSize` ones are kept. for (Key keys = CacheSize + 1; keys <= CacheSize * 2; ++keys) { Boxed boxedCache; TestCache& cache = boxedCache.Object(); for (Key i = 0; i < keys; ++i) { bool valueFunctionCalled = false; Value v = cache.FetchOrAdd(i, [&]() { valueFunctionCalled = true; return Value{i}; }); ASSERT_EQ(v, Value{i}); ASSERT_TRUE(valueFunctionCalled); boxedCache.Check(); } // Fetching keys from 0 should always call the function value: // - 0 is the oldest key, it must have been pushed out when `CacheSize` // was added. // - Once we've fetched 0, it's pushed out the old (smallest) key. // Etc. for (Key i = 0; i < CacheSize * 3; ++i) { { bool valueFunctionCalled = false; Value v = cache.FetchOrAdd(i % keys, [&]() { valueFunctionCalled = true; return Value{i % keys}; }); ASSERT_EQ(v, Value{i % keys}); ASSERT_TRUE(valueFunctionCalled); boxedCache.Check(); } // Fetching the same key again will never call the function value. { bool valueFunctionCalled = false; Value v = cache.FetchOrAdd(i % keys, [&]() { valueFunctionCalled = true; return Value{i % keys}; }); ASSERT_EQ(v, Value{i % keys}); ASSERT_FALSE(valueFunctionCalled); boxedCache.Check(); } } } } TEST(SmallArrayLRUCache, Add_FetchOrAdd_KeysDoNotFitInCache) { // We're going to add strictly more than CacheSize keys, so they cannot fit in // the cache, only the last `CacheSize` ones are kept. for (Key keys = CacheSize + 1; keys <= CacheSize * 2; ++keys) { Boxed boxedCache; TestCache& cache = boxedCache.Object(); for (Key i = 0; i < keys; ++i) { cache.Add(i, Value{i}); boxedCache.Check(); } // Fetching keys from 0 should always call the function value: // - 0 is the oldest key, it must have been pushed out when `CacheSize` // was added. // - Once we've fetched 0, it's pushed out the old (smallest) key. // Etc. for (Key i = 0; i < CacheSize * 3; ++i) { { bool valueFunctionCalled = false; Value v = cache.FetchOrAdd(i % keys, [&]() { valueFunctionCalled = true; return Value{i % keys}; }); ASSERT_EQ(v, Value{i % keys}); ASSERT_TRUE(valueFunctionCalled); boxedCache.Check(); } // Fetching the same key again will never call the function value. { bool valueFunctionCalled = false; Value v = cache.FetchOrAdd(i % keys, [&]() { valueFunctionCalled = true; return Value{i % keys}; }); ASSERT_EQ(v, Value{i % keys}); ASSERT_FALSE(valueFunctionCalled); boxedCache.Check(); } } } } TEST(SmallArrayLRUCache, Clear) { Boxed boxedCache; TestCache& cache = boxedCache.Object(); // First fetch will always call the function value. { bool valueFunctionCalled = false; Value v = cache.FetchOrAdd(42, [&]() { valueFunctionCalled = true; return Value{4242}; }); ASSERT_EQ(v, Value{4242}); ASSERT_TRUE(valueFunctionCalled); boxedCache.Check(); } // Second fetch will never call the function value. { bool valueFunctionCalled = false; Value v = cache.FetchOrAdd(42, [&]() { valueFunctionCalled = true; return Value{4242}; }); ASSERT_EQ(v, Value{4242}); ASSERT_FALSE(valueFunctionCalled); boxedCache.Check(); } cache.Clear(); // After Clear(), first fetch will always call the function value. { bool valueFunctionCalled = false; Value v = cache.FetchOrAdd(42, [&]() { valueFunctionCalled = true; return Value{4242}; }); ASSERT_EQ(v, Value{4242}); ASSERT_TRUE(valueFunctionCalled); boxedCache.Check(); } // Next fetch will never call the function value. { bool valueFunctionCalled = false; Value v = cache.FetchOrAdd(42, [&]() { valueFunctionCalled = true; return Value{4242}; }); ASSERT_EQ(v, Value{4242}); ASSERT_FALSE(valueFunctionCalled); boxedCache.Check(); } } TEST(SmallArrayLRUCache, Shutdown) { Boxed boxedCache; TestCache& cache = boxedCache.Object(); // First fetch will always call the function value. { bool valueFunctionCalled = false; Value v = cache.FetchOrAdd(42, [&]() { valueFunctionCalled = true; return Value{4242}; }); ASSERT_EQ(v, Value{4242}); ASSERT_TRUE(valueFunctionCalled); boxedCache.Check(); } // Second fetch will never call the function value. { bool valueFunctionCalled = false; Value v = cache.FetchOrAdd(42, [&]() { valueFunctionCalled = true; return Value{4242}; }); ASSERT_EQ(v, Value{4242}); ASSERT_FALSE(valueFunctionCalled); boxedCache.Check(); } cache.Shutdown(); // After Shutdown(), any fetch will always call the function value. { bool valueFunctionCalled = false; Value v = cache.FetchOrAdd(42, [&]() { valueFunctionCalled = true; return Value{4242}; }); ASSERT_EQ(v, Value{4242}); ASSERT_TRUE(valueFunctionCalled); boxedCache.Check(); } { bool valueFunctionCalled = false; Value v = cache.FetchOrAdd(42, [&]() { valueFunctionCalled = true; return Value{4242}; }); ASSERT_EQ(v, Value{4242}); ASSERT_TRUE(valueFunctionCalled); boxedCache.Check(); } cache.Add(42, Value{4242}); boxedCache.Check(); { bool valueFunctionCalled = false; Value v = cache.FetchOrAdd(42, [&]() { valueFunctionCalled = true; return Value{4242}; }); ASSERT_EQ(v, Value{4242}); ASSERT_TRUE(valueFunctionCalled); boxedCache.Check(); } }