root/src/tests/system/kernel/cache/block_cache_test.cpp
/*
 * Copyright 2008-2012, Axel Dörfler, axeld@pinc-software.de.
 * Distributed under the terms of the MIT License.
 */


#define write_pos       block_cache_write_pos
#define read_pos        block_cache_read_pos

#include "block_cache.cpp"

#undef write_pos
#undef read_pos


#define MAX_BLOCKS                                      100
#define BLOCK_CHANGED_IN_MAIN           (1L << 16)
#define BLOCK_CHANGED_IN_SUB            (2L << 16)
#define BLOCK_CHANGED_IN_PREVIOUS       (4L << 16)

#define TEST_BLOCKS(number, count) \
        test_blocks(number, count, __LINE__)
#define TEST_TRANSACTION(id, num, mainNum, subNum) \
        test_transaction(id, num, mainNum, subNum, __LINE__)

#define TEST_BLOCK_DATA(block, number, type) \
        if ((block)->type ## _data != NULL && gBlocks[(number)]. type == 0) \
                error(line, "Block %lld: " #type " should be NULL!", (number)); \
        if ((block)->type ## _data != NULL && gBlocks[(number)]. type != 0 \
                && *(int32*)(block)->type ## _data != gBlocks[(number)]. type) { \
                error(line, "Block %lld: " #type " wrong (0x%lx should be 0x%lx)!", \
                        (number), *(int32*)(block)->type ## _data, \
                        gBlocks[(number)]. type); \
        }

#define TEST_ASSERT(statement) \
        if (!(statement)) { \
                error(__LINE__, "Assertion failed: " #statement); \
        }


struct test_block {
        int32   current;
        int32   original;
        int32   parent;
        int32   previous_transaction;
        int32   transaction;
        bool    unused;
        bool    is_dirty;
        bool    discard;

        bool    write;

        bool    read;
        bool    written;
        bool    present;
};

test_block gBlocks[MAX_BLOCKS];
block_cache* gCache;
size_t gBlockSize;
int32 gTest;
int32 gSubTest;
const char* gTestName;


void
dump_cache()
{
        char cacheString[32];
        sprintf(cacheString, "%p", gCache);
        char* argv[4];
        argv[0] = "dump";
        argv[1] = "-bt";
        argv[2] = cacheString;
        argv[3] = NULL;
        dump_cache(3, argv);
}


void
error(int32 line, const char* format, ...)
{
        va_list args;
        va_start(args, format);

        fprintf(stderr, "ERROR IN TEST LINE %ld: ", line);
        vfprintf(stderr, format, args);
        fprintf(stderr, "\n");

        va_end(args);

        dump_cache();

        exit(1);
}


void
or_block(void* block, int32 value)
{
        int32* data = (int32*)block;
        *data |= value;
}


void
set_block(void* block, int32 value)
{
        int32* data = (int32*)block;
        *data = value;
}


void
reset_block(void* block, int32 index)
{
        int32* data = (int32*)block;
        *data = index + 1;
}


ssize_t
block_cache_write_pos(int fd, off_t offset, const void* buffer, size_t size)
{
        int32 index = offset / gBlockSize;

        gBlocks[index].written = true;
        if (!gBlocks[index].write)
                error(__LINE__, "Block %ld should not be written!\n", index);

        return size;
}


ssize_t
block_cache_read_pos(int fd, off_t offset, void* buffer, size_t size)
{
        int32 index = offset / gBlockSize;

        memset(buffer, 0xcc, size);
        reset_block(buffer, index);
        if (!gBlocks[index].read)
                error(__LINE__, "Block %ld should not be read!\n", index);

        return size;
}


void
init_test_blocks()
{
        memset(gBlocks, 0, sizeof(test_block) * MAX_BLOCKS);

        for (uint32 i = 0; i < MAX_BLOCKS; i++) {
                gBlocks[i].current = i + 1;
                gBlocks[i].unused = true;
        }
}


void
test_transaction(int32 id, int32 numBlocks, int32 numMainBlocks,
        int32 numSubBlocks, int32 line)
{
        MutexLocker locker(&gCache->lock);
        cache_transaction* transaction = lookup_transaction(gCache, id);

        if (numBlocks != transaction->num_blocks) {
                error(line, "Transaction %d has wrong num_blocks (is %d, should be "
                        "%d)!", id, transaction->num_blocks, numBlocks);
        }
        if (numMainBlocks != transaction->main_num_blocks) {
                error(line, "Transaction %d has wrong num_blocks (is %d, should be "
                        "%d)!", id, transaction->main_num_blocks, numMainBlocks);
        }
        if (numSubBlocks != transaction->sub_num_blocks) {
                error(line, "Transaction %d has wrong num_blocks (is %d, should be "
                        "%d)!", id, transaction->sub_num_blocks, numSubBlocks);
        }
}


void
test_blocks(off_t number, int32 count, int32 line)
{
        printf("  %ld\n", gSubTest++);

        for (int32 i = 0; i < count; i++, number++) {
                MutexLocker locker(&gCache->lock);

                cached_block* block = (cached_block*)hash_lookup(gCache->hash, &number);
                if (block == NULL) {
                        if (gBlocks[number].present)
                                error(line, "Block %lld not found!", number);
                        continue;
                }
                if (!gBlocks[number].present)
                        error(line, "Block %lld is present, but should not!", number);

                if (block->is_dirty != gBlocks[number].is_dirty) {
                        error(line, "Block %lld: dirty bit differs (is %d should be %d)!",
                                number, block->is_dirty, gBlocks[number].is_dirty);
                }
#if 0
                if (block->unused != gBlocks[number].unused) {
                        error("Block %ld: unused bit differs (%d should be %d)!", number,
                                block->unused, gBlocks[number].unused);
                }
#endif
                if (block->discard != gBlocks[number].discard) {
                        error(line, "Block %lld: discard bit differs (is %d should be %d)!",
                                number, block->discard, gBlocks[number].discard);
                }
                if (gBlocks[number].write && !gBlocks[number].written)
                        error(line, "Block %lld: has not been written yet!", number);

                TEST_BLOCK_DATA(block, number, current);
                TEST_BLOCK_DATA(block, number, original);
                TEST_BLOCK_DATA(block, number, parent);
        }
}


void
stop_test(void)
{
        if (gCache == NULL)
                return;
        TEST_BLOCKS(0, MAX_BLOCKS);

//      dump_cache();
        block_cache_delete(gCache, true);
}


void
start_test(const char* name, bool init = true)
{
        if (init) {
                stop_test();

                gBlockSize = 2048;
                gCache = (block_cache*)block_cache_create(-1, MAX_BLOCKS, gBlockSize,
                        false);

                init_test_blocks();
        }

        gTest++;
        gTestName = name;
        gSubTest = 1;

        printf("----------- Test %ld%s%s -----------\n", gTest,
                gTestName[0] ? " - " : "", gTestName);
}


/*!     Changes block 1 in main, block 2 if touchedInMain is \c true.
        Changes block 0 in sub, discards block 2.
        Performs two block tests.
*/
void
basic_test_discard_in_sub(int32 id, bool touchedInMain)
{
        gBlocks[1].present = true;
        gBlocks[1].read = true;
        gBlocks[1].is_dirty = true;
        gBlocks[1].original = gBlocks[1].current;
        gBlocks[1].current |= BLOCK_CHANGED_IN_MAIN;

        void* block = block_cache_get_writable(gCache, 1, id);
        or_block(block, BLOCK_CHANGED_IN_MAIN);
        block_cache_put(gCache, 1);

        TEST_BLOCKS(0, 2);

        if (touchedInMain) {
                gBlocks[2].present = true;
                gBlocks[2].is_dirty = true;
                gBlocks[2].current |= BLOCK_CHANGED_IN_MAIN;

                block = block_cache_get_empty(gCache, 2, id);
                reset_block(block, 2);
                or_block(block, BLOCK_CHANGED_IN_MAIN);
                block_cache_put(gCache, 2);
        }

        cache_start_sub_transaction(gCache, id);

        gBlocks[0].present = true;
        gBlocks[0].read = true;
        gBlocks[0].is_dirty = true;
        if ((gBlocks[0].current & BLOCK_CHANGED_IN_MAIN) != 0)
                gBlocks[0].parent = gBlocks[0].current;
        else
                gBlocks[0].original = gBlocks[0].current;
        gBlocks[0].current |= BLOCK_CHANGED_IN_SUB;

        gBlocks[1].parent = gBlocks[1].current;
        if (touchedInMain)
                gBlocks[2].parent = gBlocks[2].current;

        block = block_cache_get_writable(gCache, 0, id);
        or_block(block, BLOCK_CHANGED_IN_SUB);
        block_cache_put(gCache, 0);

        gBlocks[2].discard = true;

        block_cache_discard(gCache, 2, 1);

        TEST_BLOCKS(0, 2);

        gBlocks[0].is_dirty = false;
        gBlocks[0].write = true;
        gBlocks[1].is_dirty = false;
        gBlocks[1].write = true;
        gBlocks[2].present = false;
        gBlocks[2].write = touchedInMain;
        gBlocks[2].is_dirty = false;
}


// #pragma mark - Tests


void
test_abort_transaction()
{
        start_test("Abort main");

        int32 id = cache_start_transaction(gCache);

        gBlocks[0].present = true;
        gBlocks[0].read = false;
        gBlocks[0].write = false;
        gBlocks[0].is_dirty = true;
        gBlocks[1].present = true;
        gBlocks[1].read = true;
        gBlocks[1].write = false;
        gBlocks[1].is_dirty = true;

        void* block = block_cache_get_empty(gCache, 0, id);
        or_block(block, BLOCK_CHANGED_IN_PREVIOUS);
        gBlocks[0].current = BLOCK_CHANGED_IN_PREVIOUS;

        block = block_cache_get_writable(gCache, 1, id);
        or_block(block, BLOCK_CHANGED_IN_PREVIOUS);
        gBlocks[1].original = gBlocks[1].current;
        gBlocks[1].current |= BLOCK_CHANGED_IN_PREVIOUS;

        block_cache_put(gCache, 0);
        block_cache_put(gCache, 1);

        cache_end_transaction(gCache, id, NULL, NULL);
        TEST_BLOCKS(0, 2);

        id = cache_start_transaction(gCache);

        block = block_cache_get_writable(gCache, 0, id);
        or_block(block, BLOCK_CHANGED_IN_MAIN);

        block = block_cache_get_writable(gCache, 1, id);
        or_block(block, BLOCK_CHANGED_IN_MAIN);

        block_cache_put(gCache, 0);
        block_cache_put(gCache, 1);

        cache_abort_transaction(gCache, id);

        gBlocks[0].write = true;
        gBlocks[0].is_dirty = false;
        gBlocks[1].write = true;
        gBlocks[1].is_dirty = false;
        cache_sync_transaction(gCache, id);
}


void
test_abort_sub_transaction()
{
        start_test("Abort sub");

        int32 id = cache_start_transaction(gCache);

        gBlocks[0].present = true;
        gBlocks[0].read = false;
        gBlocks[0].write = false;
        gBlocks[0].is_dirty = true;
        gBlocks[1].present = true;
        gBlocks[1].read = true;
        gBlocks[1].write = false;
        gBlocks[1].is_dirty = true;

        void* block = block_cache_get_empty(gCache, 0, id);
        or_block(block, BLOCK_CHANGED_IN_PREVIOUS);
        gBlocks[0].current = BLOCK_CHANGED_IN_PREVIOUS;

        block = block_cache_get_writable(gCache, 1, id);
        or_block(block, BLOCK_CHANGED_IN_PREVIOUS);
        gBlocks[1].original = gBlocks[1].current;
        gBlocks[1].current |= BLOCK_CHANGED_IN_PREVIOUS;

        block_cache_put(gCache, 0);
        block_cache_put(gCache, 1);

        cache_start_sub_transaction(gCache, id);

        gBlocks[0].parent = gBlocks[0].current;
        gBlocks[1].parent = gBlocks[1].current;
        TEST_BLOCKS(0, 2);

        block = block_cache_get_writable(gCache, 1, id);
        or_block(block, BLOCK_CHANGED_IN_MAIN);

        block_cache_put(gCache, 1);

        TEST_TRANSACTION(id, 2, 2, 1);
        cache_abort_sub_transaction(gCache, id);
        TEST_TRANSACTION(id, 2, 2, 0);

        gBlocks[0].write = true;
        gBlocks[0].is_dirty = false;
        gBlocks[1].write = true;
        gBlocks[1].is_dirty = false;

        cache_end_transaction(gCache, id, NULL, NULL);
        cache_sync_transaction(gCache, id);

        start_test("Abort sub with empty block");
        id = cache_start_transaction(gCache);

        gBlocks[1].present = true;
        gBlocks[1].read = true;

        block = block_cache_get_writable(gCache, 1, id);
        or_block(block, BLOCK_CHANGED_IN_PREVIOUS);
        gBlocks[1].original = gBlocks[1].current;
        gBlocks[1].current |= BLOCK_CHANGED_IN_PREVIOUS;

        block_cache_put(gCache, 1);

        gBlocks[1].is_dirty = true;
        TEST_BLOCKS(1, 1);

        TEST_TRANSACTION(id, 1, 0, 0);
        cache_start_sub_transaction(gCache, id);
        TEST_TRANSACTION(id, 1, 1, 0);

        gBlocks[0].present = true;

        block = block_cache_get_empty(gCache, 0, id);
        or_block(block, BLOCK_CHANGED_IN_SUB);
        gBlocks[0].current = BLOCK_CHANGED_IN_SUB;

        block_cache_put(gCache, 0);

        TEST_TRANSACTION(id, 2, 1, 1);
        cache_abort_sub_transaction(gCache, id);
        TEST_TRANSACTION(id, 1, 1, 0);

        gBlocks[0].write = false;
        gBlocks[0].is_dirty = false;
        gBlocks[0].parent = 0;
        gBlocks[0].original = 0;
        TEST_BLOCKS(0, 1);

        gBlocks[1].write = true;
        gBlocks[1].is_dirty = false;
        cache_end_transaction(gCache, id, NULL, NULL);
        cache_sync_transaction(gCache, id);
}


void
test_block_cache_discard()
{
        // Test transactions and block caches

        start_test("Discard in main");

        int32 id = cache_start_transaction(gCache);

        gBlocks[0].present = true;
        gBlocks[0].read = true;

        block_cache_get(gCache, 0);
        block_cache_put(gCache, 0);

        gBlocks[1].present = true;
        gBlocks[1].read = true;
        gBlocks[1].write = true;

        void* block = block_cache_get_writable(gCache, 1, id);
        block_cache_put(gCache, 1);

        gBlocks[2].present = false;

        TEST_TRANSACTION(id, 1, 0, 0);
        block = block_cache_get_empty(gCache, 2, id);
        TEST_TRANSACTION(id, 2, 0, 0);
        block_cache_discard(gCache, 2, 1);
        block_cache_put(gCache, 2);

        cache_end_transaction(gCache, id, NULL, NULL);
        TEST_TRANSACTION(id, 1, 0, 0);
        cache_sync_transaction(gCache, id);

        start_test("Discard in sub");

        id = cache_start_transaction(gCache);

        basic_test_discard_in_sub(id, false);
        TEST_ASSERT(cache_blocks_in_sub_transaction(gCache, id) == 1);

        cache_end_transaction(gCache, id, NULL, NULL);
        cache_sync_transaction(gCache, id);

        gBlocks[0].is_dirty = false;
        gBlocks[1].is_dirty = false;

        start_test("Discard in sub, present in main");

        id = cache_start_transaction(gCache);

        basic_test_discard_in_sub(id, true);

        cache_end_transaction(gCache, id, NULL, NULL);
        cache_sync_transaction(gCache, id);

        gBlocks[0].is_dirty = false;
        gBlocks[1].is_dirty = false;

        start_test("Discard in sub, changed in main, abort sub");

        id = cache_start_transaction(gCache);

        basic_test_discard_in_sub(id, true);
        TEST_ASSERT(cache_blocks_in_sub_transaction(gCache, id) == 1);

        gBlocks[0].current &= ~BLOCK_CHANGED_IN_SUB;
        gBlocks[0].is_dirty = false;
        gBlocks[0].write = false;
        gBlocks[1].write = false;
        gBlocks[1].is_dirty = true;
        gBlocks[2].present = true;
        gBlocks[2].is_dirty = true;
        gBlocks[2].write = false;
        gBlocks[2].discard = false;
        cache_abort_sub_transaction(gCache, id);
        TEST_BLOCKS(0, 2);

        gBlocks[1].is_dirty = false;
        gBlocks[1].write = true;
        gBlocks[2].is_dirty = false;
        gBlocks[2].write = true;
        cache_end_transaction(gCache, id, NULL, NULL);
        cache_sync_transaction(gCache, id);

        start_test("Discard in sub, changed in main, new sub");

        id = cache_start_transaction(gCache);

        basic_test_discard_in_sub(id, true);

        cache_start_sub_transaction(gCache, id);
        cache_end_transaction(gCache, id, NULL, NULL);
        cache_sync_transaction(gCache, id);

        start_test("Discard in sub, changed in main, detach sub");

        id = cache_start_transaction(gCache);

        basic_test_discard_in_sub(id, true);

        id = cache_detach_sub_transaction(gCache, id, NULL, NULL);

        gBlocks[0].is_dirty = true;
        gBlocks[0].write = false;
        gBlocks[1].is_dirty = true;
        gBlocks[1].write = false;

        TEST_BLOCKS(0, 2);

        gBlocks[0].is_dirty = false;
        gBlocks[0].write = true;
        gBlocks[1].is_dirty = false;
        gBlocks[1].write = true;

        cache_end_transaction(gCache, id, NULL, NULL);
        cache_sync_transaction(gCache, id);

        start_test("Discard in sub, all changed in main, detach sub");

        id = cache_start_transaction(gCache);

        gBlocks[0].present = true;
        gBlocks[0].read = true;
        gBlocks[0].is_dirty = true;
        gBlocks[0].original = gBlocks[0].current;
        gBlocks[0].current |= BLOCK_CHANGED_IN_MAIN;

        block = block_cache_get_writable(gCache, 0, id);
        or_block(block, BLOCK_CHANGED_IN_MAIN);
        block_cache_put(gCache, 0);

        basic_test_discard_in_sub(id, true);

        id = cache_detach_sub_transaction(gCache, id, NULL, NULL);

        gBlocks[0].is_dirty = true;
        gBlocks[0].write = false;
        gBlocks[0].original |= BLOCK_CHANGED_IN_MAIN;
        gBlocks[1].is_dirty = true;
        gBlocks[1].write = false;

        TEST_BLOCKS(0, 2);

        gBlocks[0].is_dirty = false;
        gBlocks[0].write = true;
        gBlocks[1].is_dirty = false;
        gBlocks[1].write = true;

        cache_end_transaction(gCache, id, NULL, NULL);
        cache_sync_transaction(gCache, id);

        stop_test();
}


// #pragma mark -


int
main(int argc, char** argv)
{
        block_cache_init();

        // TODO: test transaction-less block caches
        // TODO: test read-only block caches
        test_abort_transaction();
        test_abort_sub_transaction();
        test_block_cache_discard();
        return 0;
}