root/src/tests/kits/support/BlockCacheExerciseTest.cpp
/*
 * Copyright 2003-2026, Haiku, Inc. All rights reserved.
 * Distributed under the terms of the MIT License.
 */


#include <math.h>
#include <string.h>

#include <BlockCache.h>
#include <List.h>

#include <TestSuiteAddon.h>
#include <cppunit/TestFixture.h>
#include <cppunit/TestSuite.h>
#include <cppunit/extensions/HelperMacros.h>


class BlockCacheExerciseTest : public CppUnit::TestCase {
private:
        BBlockCache* theCache;
        int numBlocksInCache;
        size_t sizeOfBlocksInCache;
        size_t sizeOfNonCacheBlocks;

        bool isMallocTest;

        BList freeList;
        BList usedList;
        BList nonCacheList;

        void BuildLists(void);
        void* GetBlock(size_t blockSize);
        void SaveBlock(void*, size_t blockSize);
        void FreeBlock(void*, size_t blockSize);
        void TestBlockCache(void);

public:
        static CppUnit::Test* suite(void);
        BlockCacheExerciseTest(std::string = "");
        virtual ~BlockCacheExerciseTest();
        virtual void PerformTest(void);
};


/*
 *  Method: BlockCacheExerciseTest::BlockCacheExerciseTest()
 *   Descr: This method is the only constructor for the BlockCacheExerciseTest
 *          class.
 */
BlockCacheExerciseTest::BlockCacheExerciseTest(std::string name)
        :
        TestCase(name),
        theCache(NULL),
        numBlocksInCache(0),
        sizeOfBlocksInCache(0),
        sizeOfNonCacheBlocks(0),
        isMallocTest(false)
{
}


/*
 *  Method: BlockCacheExerciseTest::~BlockCacheExerciseTest()
 *   Descr: This method is the destructor for the BlockCacheExerciseTest class.
 */
BlockCacheExerciseTest::~BlockCacheExerciseTest()
{
}


/*
 *  Method:  BlockCacheExerciseTest::TestBlockCache()
 *   Descr:  This method performs the tests on a particular BBlockCache.
 *           The goal of the method is to perform the following basic
 *           sequence:
 *             1. Get (numBlocksInCache + 10) blocks of "cache size" from the
 *                BBlockCache.  By doing this, we can guarantee that the cache
 *                has been exhausted and some elements are allocated to
 *                satisfy the caller.  Then, they are all returned to the
 *                cache in most recent block to oldest block order.  This
 *                confirms that regardless whether the block was initially
 *                part of the cache or created because the cache was empty,
 *                once it is returned to the cache, it is just placed on the
 *                cache.  Imagine a cache of 4 blocks.  Initally, the cache
 *                has the following blocks:
 *                   A, B, C, D
 *                If five blocks are gotten from the cache, the caller will get
 *                   A, B, C, D, E
 *                However, E wasn't initially part of the cache.  It was
 *                allocated dynamically to satisfy the caller's request because
 *                the cache was empty.  Now, if they are returned in the order
 *                E, D, C, B, A the cache will have the following blocks
 *                available in it: B, C, D, E When A is returned, the cache will
 *                find there is no more room for more free blocks and it will be
 *                freed.  This is the behaviour which is confirmed initially.
 *
 *             2. After this is done, the cache is just "exercised".  The
 *                following is done "numBlocksInCache" times:
 *                  - 4 "cache sized" blocks are gotten from the cache
 *                  - 4 "non-cache sized" blocks are gotten from the cache
 *                  - 1 "cache sized" block is returned back to the cache
 *                  - 1 "non-cache sized" block is returned back to the cache
 *                  - 1 "cache sized" block is freed and not returned to the
 *                    cache
 *                  - 1 "non-cache sized" block is freed and not returned to the
 *                    cache (but even if given to the BBlockCache, it would just
 *                    be freed anyhow)
 *                What this means is that everytime through the loop, 2 "cache
 *                sized" and 2 "non-cache sized" blocks are kept in memory.  At
 *                the end, 2 * numBlocksInCache items of size "cache size" and
 *                "non cache size" will exist.
 *
 *                Then, numBlocksInCache / 4 items are returned to the cache
 *                and numBlocksInCache / 4 are freed.  This ensures at the end
 *                of the test that there are some available blocks in the cache.
 *
 *           The sum total of these actions test the BBlockCache.
 */
void
BlockCacheExerciseTest::TestBlockCache(void)
{
        // First get all items from the cache plus ten more
        for (int i = 0; i < numBlocksInCache + 10; i++)
                GetBlock(sizeOfBlocksInCache);

        // Put them all back in reverse order to confirm 1 from above
        while (!usedList.IsEmpty())
                SaveBlock(usedList.LastItem(), sizeOfBlocksInCache);

        // Get a bunch of blocks and send some back to the cache
        // to confirm 2 from above.
        for (int i = 0; i < numBlocksInCache; i++) {
                GetBlock(sizeOfBlocksInCache);
                GetBlock(sizeOfBlocksInCache);
                GetBlock(sizeOfNonCacheBlocks);
                GetBlock(sizeOfNonCacheBlocks);

                // We send one back from the middle of the lists so
                // the most recent block is not the one returned.
                SaveBlock(usedList.ItemAt(usedList.CountItems() / 2), sizeOfBlocksInCache);
                SaveBlock(nonCacheList.ItemAt(nonCacheList.CountItems() / 2), sizeOfNonCacheBlocks);

                GetBlock(sizeOfBlocksInCache);
                GetBlock(sizeOfBlocksInCache);
                GetBlock(sizeOfNonCacheBlocks);
                GetBlock(sizeOfNonCacheBlocks);

                // We free one from the middle of the lists so the
                // most recent block is not the one freed.
                FreeBlock(usedList.ItemAt(usedList.CountItems() / 2), sizeOfBlocksInCache);
                FreeBlock(nonCacheList.ItemAt(nonCacheList.CountItems() / 2), sizeOfNonCacheBlocks);
        }

        // Now, send some blocks back to the cache and free some blocks
        // so the cache is not empty at the end of the test.
        for (int i = 0; i < numBlocksInCache / 4; i++) {
                // Return the blocks which are 2/3s of the way through the
                // lists.
                SaveBlock(usedList.ItemAt(usedList.CountItems() * 2 / 3), sizeOfBlocksInCache);
                SaveBlock(nonCacheList.ItemAt(nonCacheList.CountItems() * 2 / 3), sizeOfNonCacheBlocks);

                // Free the blocks which are 1/3 of the way through the lists.
                FreeBlock(usedList.ItemAt(usedList.CountItems() / 3), sizeOfBlocksInCache);
                FreeBlock(nonCacheList.ItemAt(nonCacheList.CountItems() / 3), sizeOfNonCacheBlocks);
        }
}


/*
 *  Method:  BlockCacheExerciseTest::BuildLists()
 *   Descr:  This method gets all of the blocks from the cache in order to
 *           track them for future tests.  The assumption here is that the
 *           blocks are not deallocated and reallocated in the implementation
 *           of BBlockCache.  Given the purpose is to provide a cheap way to
 *           access allocated memory without resorting to malloc()'s or new's,
 *           this should be a fair assumption.
 */
void
BlockCacheExerciseTest::BuildLists()
{
        freeList.MakeEmpty();
        usedList.MakeEmpty();
        nonCacheList.MakeEmpty();

        for (int i = 0; i < numBlocksInCache; i++)
                freeList.AddItem(theCache->Get(sizeOfBlocksInCache));
        for (int i = 0; i < numBlocksInCache; i++)
                theCache->Save(freeList.ItemAt(i), sizeOfBlocksInCache);
}


/*
 *  Method:  BlockCacheExerciseTest::GetBlock()
 *   Descr:  This method returns a pointer from the BBlockCache, checking
 *           the value before passing it to the caller.
 */
void*
BlockCacheExerciseTest::GetBlock(size_t blockSize)
{
        void* thePtr = theCache->Get(blockSize);

        // This new pointer should not be one which we already
        // have from the BBlockCache which we haven't given back
        // yet.
        CPPUNIT_ASSERT(!usedList.HasItem(thePtr));
        CPPUNIT_ASSERT(!nonCacheList.HasItem(thePtr));

        if (blockSize == sizeOfBlocksInCache) {
                // If this block was one which could have come from the
                // cache and there are free items on the cache, it
                // should be one of those free blocks.
                if (freeList.CountItems() > 0)
                        CPPUNIT_ASSERT(freeList.RemoveItem(thePtr));
                CPPUNIT_ASSERT(usedList.AddItem(thePtr));
        } else {
                // A "non-cache sized" block should never come from the
                // free list.
                CPPUNIT_ASSERT(!freeList.HasItem(thePtr));
                CPPUNIT_ASSERT(nonCacheList.AddItem(thePtr));
        }
        return thePtr;
}


/*
 *  Method:  BlockCacheExerciseTest::SavedCacheBlock()
 *   Descr:  This method passes the pointer back to the BBlockCache
 *           and checks the sanity of the lists.
 */
void
BlockCacheExerciseTest::SaveBlock(void* thePtr, size_t blockSize)
{
        // The memory block being returned to the cache should
        // not already be free.
        CPPUNIT_ASSERT(!freeList.HasItem(thePtr));

        if (blockSize == sizeOfBlocksInCache) {
                // If there is room on the free list, when this block
                // is returned to the cache, it will be put on the
                // free list.  Therefore we will also track it as
                // a free block on the cache.
                if (freeList.CountItems() < numBlocksInCache)
                        CPPUNIT_ASSERT(freeList.AddItem(thePtr));

                // This block should not be on the non-cache list but it
                // should be on the used list.
                CPPUNIT_ASSERT(!nonCacheList.HasItem(thePtr));
                CPPUNIT_ASSERT(usedList.RemoveItem(thePtr));
        } else {
                // This block should not be on the used list but it should
                // be on the non-cache list.
                CPPUNIT_ASSERT(!usedList.HasItem(thePtr));
                CPPUNIT_ASSERT(nonCacheList.RemoveItem(thePtr));
        }
        theCache->Save(thePtr, blockSize);
}


/*
 *  Method:  BlockCacheExerciseTest::FreeBlock()
 *   Descr:  This method frees the block directly using delete[] or free(),
 *           checking the sanity of the lists as it does the operation.
 */
void
BlockCacheExerciseTest::FreeBlock(void* thePtr, size_t blockSize)
{
        // The block being freed should not already have been
        // returned to the cache.
        CPPUNIT_ASSERT(!freeList.HasItem(thePtr));

        if (blockSize == sizeOfBlocksInCache) {
                // This block should not be on the non-cache list but it
                // should be on the used list.
                CPPUNIT_ASSERT(!nonCacheList.HasItem(thePtr));
                CPPUNIT_ASSERT(usedList.RemoveItem(thePtr));
        } else {
                // This block should not be on the used list but it should
                // be on the non-cache list.
                CPPUNIT_ASSERT(!usedList.HasItem(thePtr));
                CPPUNIT_ASSERT(nonCacheList.RemoveItem(thePtr));
        }
        if (isMallocTest)
                free(thePtr);
        else
                delete[] (uint8*)thePtr;
}


/*
 *  Method:  BlockCacheExerciseTest::PerformTest()
 *   Descr:  This method performs the tests on a series of BBlockCache
 *           instances.  It performs the tests with a series of
 *           block sizes and numbers of blocks.  Also, it does the
 *           test using a B_OBJECT_CACHE and B_MALLOC_CACHE type
 *           cache.  For each individual BBlockCache to be tested, it:
 *             1. Queries the cache to find out the blocks which are
 *                on it.
 *             2. Performs the test.
 *             3. Frees all blocks left after the test to prevent
 *                memory leaks.
 */
void
BlockCacheExerciseTest::PerformTest(void)
{
        for (numBlocksInCache = 8; numBlocksInCache < 513; numBlocksInCache *= 2) {
                for (sizeOfBlocksInCache = 13; sizeOfBlocksInCache < 9478; sizeOfBlocksInCache *= 3) {

                        // To test getting blocks which are not from the cache,
                        // we will get blocks of 6 bytes less than the size of
                        // the blocks on the cache.
                        sizeOfNonCacheBlocks = sizeOfBlocksInCache - 6;

                        isMallocTest = false;
                        theCache = new BBlockCache(numBlocksInCache, sizeOfBlocksInCache, B_OBJECT_CACHE);
                        CPPUNIT_ASSERT(theCache != NULL);

                        // Query the cache and determine the blocks in it.
                        BuildLists();
                        // Perform the test on this instance.
                        TestBlockCache();
                        delete theCache;
                        // Clean up remaining memory.
                        while (!usedList.IsEmpty())
                                FreeBlock(usedList.LastItem(), sizeOfBlocksInCache);
                        while (!nonCacheList.IsEmpty())
                                FreeBlock(nonCacheList.LastItem(), sizeOfNonCacheBlocks);

                        isMallocTest = true;
                        theCache = new BBlockCache(numBlocksInCache, sizeOfBlocksInCache, B_MALLOC_CACHE);
                        CPPUNIT_ASSERT(theCache != NULL);

                        // Query the cache and determine the blocks in it.
                        BuildLists();
                        // Perform the test on this instance.
                        TestBlockCache();
                        delete theCache;
                        // Clean up remaining memory.
                        while (!usedList.IsEmpty())
                                FreeBlock(usedList.LastItem(), sizeOfBlocksInCache);
                        while (!nonCacheList.IsEmpty())
                                FreeBlock(nonCacheList.LastItem(), sizeOfNonCacheBlocks);
                }
        }
}


/*
 *  Method:  BlockCacheExerciseTest::suite()
 *   Descr:  This static member function returns a test caller for performing
 *           the "BlockCacheExerciseTest" test.
 */
CppUnit::Test*
BlockCacheExerciseTest::suite()
{
        typedef CppUnit::TestCaller<BlockCacheExerciseTest> BlockCacheExerciseTestCaller;

        return new BlockCacheExerciseTestCaller("BBlockCache::Exercise Test",
                &BlockCacheExerciseTest::PerformTest);
}


CPPUNIT_TEST_SUITE_NAMED_REGISTRATION(BlockCacheExerciseTest, getTestSuiteName());