master ohmu/fastfetch-openbsd / tests / cache.c
ohmu  ·  2026-10-15
  1#include "common/FFcache.h"
  2#include "common/textModifier.h"
  3
  4#include <stdio.h>
  5#include <stdlib.h>
  6#include <stdnoreturn.h>
  7
  8typedef struct TestStorage {
  9    int value;
 10    int initCalls;
 11    int destroyCalls;
 12} TestStorage;
 13
 14static TestStorage storageA, storageB, storageC, storageD, storageUnused;
 15
 16static void initStorage(void* storage) {
 17    TestStorage* test = storage;
 18    ++test->initCalls;
 19    test->value = 42;
 20}
 21
 22static void destroyStorage(void* storage) {
 23    TestStorage* test = storage;
 24    ++test->destroyCalls;
 25    test->value = -1;
 26}
 27
 28static FFcacheEntry entryA = { .name = "A", .storage = &storageA, .init = initStorage, .destroy = destroyStorage };
 29static FFcacheEntry entryB = { .name = "B", .storage = &storageB, .init = initStorage, .destroy = destroyStorage };
 30static FFcacheEntry entryC = { .name = "C", .storage = &storageC, .init = initStorage, .destroy = destroyStorage };
 31static FFcacheEntry entryD = { .name = "D", .storage = &storageD };
 32static FFcacheEntry entryNoStorage = { .name = "no storage" };
 33static FFcacheEntry entryUnused = { .name = "unused", .storage = &storageUnused, .init = initStorage, .destroy = destroyStorage };
 34
 35noreturn static void testFailed(const char* expression, int lineNo) {
 36    fprintf(stderr, FASTFETCH_TEXT_MODIFIER_ERROR "[%d] %s\n" FASTFETCH_TEXT_MODIFIER_RESET, lineNo, expression);
 37    exit(1);
 38}
 39
 40noreturn static void testFailedInt(int expected, int actual, const char* what, int lineNo) {
 41    fprintf(stderr, FASTFETCH_TEXT_MODIFIER_ERROR "[%d] %s: expected %d, got %d\n" FASTFETCH_TEXT_MODIFIER_RESET, lineNo, what, expected, actual);
 42    exit(1);
 43}
 44
 45#define VERIFY(expression) \
 46    if (!(expression)) testFailed(#expression, __LINE__)
 47
 48#define VERIFY_INT(expected, actual, what) \
 49    if ((expected) != (actual)) testFailedInt((expected), (actual), (what), __LINE__)
 50
 51int main(void) {
 52    // A stale entry is built on the first get, and only then
 53    VERIFY(ffCacheGet(&entryA) == &storageA);
 54    VERIFY_INT(1, storageA.initCalls, "A init calls after the first get");
 55    VERIFY_INT(42, storageA.value, "A value after the first get");
 56
 57    VERIFY(ffCacheGet(&entryA) == &storageA);
 58    VERIFY_INT(1, storageA.initCalls, "A init calls after the second get");
 59
 60    // Nothing happens to the entry that has not been used yet
 61    VERIFY_INT(0, storageUnused.initCalls, "unused init calls");
 62
 63    // Invalidating drops the value and rearms the entry, and doing it twice is a no-op
 64    ffCacheInvalidate(&entryA);
 65    VERIFY_INT(1, storageA.destroyCalls, "A destroy calls after invalidating");
 66    ffCacheInvalidate(&entryA);
 67    VERIFY_INT(1, storageA.destroyCalls, "A destroy calls after invalidating twice");
 68
 69    VERIFY(ffCacheGet(&entryA) == &storageA);
 70    VERIFY_INT(2, storageA.initCalls, "A init calls after rebuilding");
 71    VERIFY_INT(42, storageA.value, "A value after rebuilding");
 72
 73    // An entry that has never been used has nothing to drop
 74    ffCacheInvalidate(&entryC);
 75    VERIFY_INT(0, storageC.destroyCalls, "C destroy calls after invalidating an unused entry");
 76    VERIFY(ffCacheGet(&entryC) == &storageC);
 77    VERIFY_INT(1, storageC.initCalls, "C init calls");
 78
 79    // An entry without an initializer tracks staleness on its own
 80    VERIFY(ffCacheGet(&entryD) == &storageD);
 81    VERIFY(!ffCacheBeginInit(&entryD));
 82    ffCacheInvalidate(&entryD);
 83    VERIFY(ffCacheBeginInit(&entryD));
 84    VERIFY(!ffCacheBeginInit(&entryD));
 85
 86    // An entry without storage and without callbacks is tolerated
 87    VERIFY(ffCacheGet(&entryNoStorage) == nullptr);
 88    ffCacheInvalidate(&entryNoStorage);
 89    VERIFY(ffCacheGet(&entryNoStorage) == nullptr);
 90
 91    // An entry whose owner builds the value itself (which is what `ffCacheBeginInit` is for) is only
 92    // marked, never built by the cache; and it must survive a `destroy` on a storage that the owner
 93    // never filled in
 94    VERIFY(ffCacheBeginInit(&entryB));
 95    VERIFY_INT(0, storageB.initCalls, "B init calls (the owner builds the value itself)");
 96
 97    // Invalidating everything drops exactly the entries that have been used so far
 98    ffCacheInvalidateAll();
 99    VERIFY_INT(2, storageA.destroyCalls, "A destroy calls after invalidating all");
100    VERIFY_INT(1, storageC.destroyCalls, "C destroy calls after invalidating all");
101    VERIFY_INT(0, storageD.destroyCalls, "D destroy calls"); // D has no callback
102    VERIFY_INT(1, storageB.destroyCalls, "B destroy calls after invalidating a value that was never built");
103    VERIFY_INT(0, entryB.initialized, "B must be stale again after invalidating all");
104    VERIFY_INT(0, storageUnused.initCalls, "unused init calls after invalidating all");
105
106    // Rebuilding after invalidating all
107    VERIFY(ffCacheGet(&entryA) == &storageA);
108    VERIFY_INT(3, storageA.initCalls, "A init calls after invalidating all");
109    VERIFY_INT(42, storageA.value, "A value after invalidating all");
110
111    // An entry joins the registry when it is first used, not when it is created
112    VERIFY(ffCacheGet(&entryUnused) == &storageUnused);
113    VERIFY_INT(1, storageUnused.initCalls, "unused init calls after its first get");
114    ffCacheInvalidateAll();
115    VERIFY_INT(1, storageUnused.destroyCalls, "unused destroy calls after invalidating all");
116    VERIFY_INT(3, storageA.destroyCalls, "A destroy calls after the second invalidating all");
117
118    // Success
119    puts("\033[32mAll tests passed!" FASTFETCH_TEXT_MODIFIER_RESET);
120}