dte test coverage


Directory: ./
File: src/util/hashmap.c
Date: 2025-12-03 13:13:24
Coverage Exec Excl Total
Lines: 93.8% 121 0 129
Functions: 100.0% 11 0 11
Branches: 81.2% 52 0 64

Line Branch Exec Source
1 #include <errno.h>
2 #include <stdlib.h>
3 #include <string.h>
4 #include "hashmap.h"
5 #include "arith.h"
6 #include "bit.h"
7 #include "errorcode.h"
8 #include "hash.h"
9 #include "xstring.h"
10
11 enum {
12 MIN_CAPACITY = 8,
13 TOMBSTONE = 0xdead,
14 };
15
16 WARN_UNUSED_RESULT
17 500 static SystemErrno hashmap_resize(HashMap *map, size_t capacity)
18 {
19 500 BUG_ON(capacity < MIN_CAPACITY);
20 500 BUG_ON(capacity <= map->count);
21 500 BUG_ON(!IS_POWER_OF_2(capacity));
22
23 500 const size_t entrysize = sizeof(map->entries[0]);
24
1/2
✓ Branch 8 → 9 taken 500 times.
✗ Branch 8 → 21 not taken.
500 if (unlikely(calloc_args_have_ub_overflow(capacity, entrysize))) {
25 return EOVERFLOW;
26 }
27
28 500 HashMapEntry *newtab = calloc(capacity, entrysize); // NOLINT(*-unsafe-functions)
29
1/2
✓ Branch 9 → 10 taken 500 times.
✗ Branch 9 → 21 not taken.
500 if (unlikely(!newtab)) {
30 return ENOMEM;
31 }
32
33 500 HashMapEntry *oldtab = map->entries;
34 500 size_t oldlen = map->mask + 1;
35 500 map->entries = newtab;
36 500 map->mask = capacity - 1;
37 500 map->tombstones = 0;
38
39
2/2
✓ Branch 10 → 11 taken 222 times.
✓ Branch 10 → 21 taken 278 times.
500 if (!oldtab) {
40 return 0;
41 }
42
43 // Copy the entries to the new table
44
2/2
✓ Branch 19 → 12 taken 19224 times.
✓ Branch 19 → 20 taken 222 times.
19446 for (const HashMapEntry *e = oldtab, *end = e + oldlen; e < end; e++) {
45
2/2
✓ Branch 12 → 13 taken 10991 times.
✓ Branch 12 → 14 taken 8233 times.
19224 if (!e->key) {
46 10991 continue;
47 }
48 8233 HashMapEntry *newe;
49 9855 for (size_t i = e->hash, j = 1; ; i += j++) {
50 9855 newe = newtab + (i & map->mask);
51
2/2
✓ Branch 15 → 16 taken 1622 times.
✓ Branch 15 → 17 taken 8233 times.
9855 if (!newe->key) {
52 break;
53 }
54 }
55 8233 *newe = *e;
56 }
57
58 222 free(oldtab);
59 222 return 0;
60 }
61
62 WARN_UNUSED_RESULT
63 278 static SystemErrno hashmap_do_init(HashMap *map, size_t capacity)
64 {
65 // Accommodate the 75% load factor in the table size, to allow
66 // filling to the requested size without needing to resize()
67 278 capacity += capacity / 3;
68
69
2/2
✓ Branch 2 → 3 taken 263 times.
✓ Branch 2 → 4 taken 15 times.
278 if (unlikely(capacity < MIN_CAPACITY)) {
70 263 capacity = MIN_CAPACITY;
71 }
72
73 // Round up the size to the next power of 2, to allow using simple
74 // bitwise ops (instead of modulo) to wrap the hash value and also
75 // to allow quadratic probing with triangular numbers (`i += j++`
76 // in the `for` loops below)
77 278 capacity = next_pow2(capacity);
78
1/2
✓ Branch 4 → 5 taken 278 times.
✗ Branch 4 → 6 not taken.
278 if (unlikely(capacity == 0)) {
79 return EOVERFLOW;
80 }
81
82 278 return hashmap_resize(map, capacity);
83 }
84
85 16 void hashmap_init(HashMap *map, size_t capacity, HashMapFlags flags)
86 {
87 16 *map = (HashMap){.flags = flags};
88 16 SystemErrno err = hashmap_do_init(map, capacity);
89
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 16 times.
16 FATAL_ERROR_ON(err, err);
90 16 }
91
92 42670 HashMapEntry *hashmap_find(const HashMap *map, const char *key)
93 {
94
2/2
✓ Branch 2 → 3 taken 42317 times.
✓ Branch 2 → 10 taken 353 times.
42670 if (unlikely(!map->entries)) {
95 return NULL;
96 }
97
98 42317 size_t hash = fnv_1a_hash(key, strlen(key));
99 42317 HashMapEntry *e;
100 79705 for (size_t i = hash, j = 1; ; i += j++) {
101 79705 e = map->entries + (i & map->mask);
102
2/2
✓ Branch 4 → 5 taken 31333 times.
✓ Branch 4 → 7 taken 48372 times.
79705 if (!e->key) {
103
2/2
✓ Branch 5 → 6 taken 12182 times.
✓ Branch 5 → 10 taken 19151 times.
31333 if (e->hash == TOMBSTONE) {
104 12182 continue;
105 }
106 return NULL;
107 }
108
3/4
✓ Branch 7 → 8 taken 23166 times.
✓ Branch 7 → 9 taken 25206 times.
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 23166 times.
48372 if (e->hash == hash && streq(e->key, key)) {
109 return e;
110 }
111 }
112
113 BUG("unexpected loop break");
114 }
115
116 15800 static void hashmap_free_key(const HashMap *map, char *key)
117 {
118
2/2
✓ Branch 2 → 3 taken 15765 times.
✓ Branch 2 → 4 taken 35 times.
15800 if (!(map->flags & HMAP_BORROWED_KEYS)) {
119 15765 free(key);
120 }
121 15800 }
122
123 11562 void *hashmap_remove(HashMap *map, const char *key)
124 {
125 11562 HashMapEntry *e = hashmap_find(map, key);
126
2/2
✓ Branch 3 → 4 taken 11543 times.
✓ Branch 3 → 6 taken 19 times.
11562 if (!e) {
127 return NULL;
128 }
129
130 11543 hashmap_free_key(map, e->key);
131 11543 e->key = NULL;
132 11543 e->hash = TOMBSTONE;
133 11543 map->count--;
134 11543 map->tombstones++;
135 11543 return e->value;
136 }
137
138 WARN_UNUSED_RESULT
139 15800 static SystemErrno hashmap_do_insert(HashMap *map, char *key, void *value, void **old_value)
140 {
141 15800 SystemErrno err = 0;
142
2/2
✓ Branch 2 → 3 taken 262 times.
✓ Branch 2 → 5 taken 15538 times.
15800 if (unlikely(!map->entries)) {
143 262 err = hashmap_do_init(map, 0);
144
1/2
✓ Branch 4 → 5 taken 262 times.
✗ Branch 4 → 31 not taken.
262 if (unlikely(err)) {
145 return err;
146 }
147 }
148
149 15800 size_t hash = fnv_1a_hash(key, strlen(key));
150 15800 bool replacing_tombstone_or_existing_value = false;
151 15800 HashMapEntry *e;
152 23418 for (size_t i = hash, j = 1; ; i += j++) {
153 23418 e = map->entries + (i & map->mask);
154
2/2
✓ Branch 6 → 7 taken 15799 times.
✓ Branch 6 → 11 taken 7619 times.
23418 if (!e->key) {
155
2/2
✓ Branch 7 → 8 taken 4770 times.
✓ Branch 7 → 20 taken 11029 times.
15799 if (e->hash == TOMBSTONE) {
156 4770 replacing_tombstone_or_existing_value = true;
157 4770 BUG_ON(map->tombstones == 0);
158 4770 map->tombstones--;
159 }
160 break;
161 }
162
3/4
✓ Branch 11 → 12 taken 1 time.
✓ Branch 11 → 19 taken 7618 times.
✓ Branch 12 → 13 taken 1 time.
✗ Branch 12 → 19 not taken.
7619 if (unlikely(e->hash == hash && streq(e->key, key))) {
163 1 replacing_tombstone_or_existing_value = true;
164 // When a caller passes NULL as the "old_value" return param,
165 // it implies there can be no existing entry with the same key
166 // as the one to be inserted.
167 1 BUG_ON(!old_value);
168 1 BUG_ON(!e->value);
169 1 *old_value = e->value;
170 1 hashmap_free_key(map, key);
171 1 key = e->key;
172 1 map->count--;
173 1 break;
174 }
175 }
176
177 15800 const size_t max_load = map->mask - (map->mask / 4);
178 15800 e->key = key;
179 15800 e->value = value;
180 15800 e->hash = hash;
181 15800 map->count++;
182
183
2/2
✓ Branch 20 → 21 taken 222 times.
✓ Branch 20 → 31 taken 15578 times.
15800 if (unlikely(map->count + map->tombstones > max_load)) {
184 222 BUG_ON(replacing_tombstone_or_existing_value);
185 222 size_t new_size = map->mask + 1;
186
3/4
✓ Branch 23 → 24 taken 6 times.
✓ Branch 23 → 25 taken 216 times.
✗ Branch 24 → 25 not taken.
✓ Branch 24 → 27 taken 6 times.
222 if (map->count > map->tombstones || new_size <= 256) {
187 // Only increase the size of the table when the number of
188 // real entries is higher than the number of tombstones.
189 // If the number of real entries is lower, the table is
190 // most likely being filled with tombstones from repeated
191 // insert/remove churn; so we just rehash at the same size
192 // to clean out the tombstones.
193 216 new_size <<= 1;
194
1/2
✗ Branch 25 → 26 not taken.
✓ Branch 25 → 27 taken 216 times.
216 if (unlikely(new_size == 0)) {
195 err = EOVERFLOW;
196 goto error;
197 }
198 }
199 222 err = hashmap_resize(map, new_size);
200
1/2
✗ Branch 28 → 29 not taken.
✓ Branch 28 → 31 taken 222 times.
222 if (unlikely(err)) {
201 goto error;
202 }
203 }
204
205 return 0;
206
207 error:
208 map->count--;
209 e->key = NULL;
210 e->hash = 0;
211 return err;
212 }
213
214 15505 void *hashmap_insert(HashMap *map, char *key, void *value)
215 {
216 15505 SystemErrno err = hashmap_do_insert(map, key, value, NULL);
217
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 15505 times.
15505 FATAL_ERROR_ON(err, err);
218 15505 return value;
219 }
220
221 295 void *hashmap_insert_or_replace(HashMap *map, char *key, void *value)
222 {
223 295 void *replaced_value = NULL;
224 295 SystemErrno err = hashmap_do_insert(map, key, value, &replaced_value);
225
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 295 times.
295 FATAL_ERROR_ON(err, err);
226 295 return replaced_value;
227 }
228
229 // Remove all entries without freeing the table
230 586 void hashmap_clear(HashMap *map, FreeFunction free_value)
231 {
232
2/2
✓ Branch 2 → 3 taken 278 times.
✓ Branch 2 → 13 taken 308 times.
586 if (unlikely(!map->entries)) {
233 return;
234 }
235
236 278 size_t count = 0;
237
2/2
✓ Branch 9 → 4 taken 4256 times.
✓ Branch 9 → 10 taken 278 times.
4534 for (HashMapIter it = hashmap_iter(map); hashmap_next(&it); count++) {
238 4256 hashmap_free_key(map, it.entry->key);
239
2/2
✓ Branch 5 → 6 taken 3794 times.
✓ Branch 5 → 7 taken 462 times.
4256 if (free_value) {
240 3794 do_free_value(free_value, it.entry->value);
241 }
242 }
243
244 278 BUG_ON(count != map->count);
245 278 size_t len = map->mask + 1;
246 278 map->count = 0;
247 278 memset(map->entries, 0, len * sizeof(*map->entries));
248 }
249
250 568 void hashmap_free(HashMap *map, FreeFunction free_value)
251 {
252 568 hashmap_clear(map, free_value);
253 568 free(map->entries);
254 568 *map = (HashMap){.flags = map->flags};
255 568 }
256