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}