Skip to content

Commit 3fcfaa6

Browse files
committed
Add HashTable coverage tests and fix try! usage
Add CustomTableTestHelper with string-based hash/compare to test non-pointer code paths: custom constructor, string_hash, duplicate insert/replace, non-pointer lookup/remove, found_key_out parameter, destructor callbacks, and remove callbacks. Replace try! #require with try #require across all UtilitiesTests.
1 parent 69bd561 commit 3fcfaa6

4 files changed

Lines changed: 307 additions & 13 deletions

File tree

Sources/Utilities/include/Utilities/TestingSupport.hpp

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
#include <Utilities/ForwardList.hpp>
1212
#include <Utilities/Heap.hpp>
13+
#include <Utilities/HashTable.hpp>
1314
#include <Utilities/cf_ptr.hpp>
1415
#include <CoreFoundation/CFData.h>
1516

@@ -55,6 +56,72 @@ OAG_INLINE uint64_t *_Nonnull heap_alloc_uint64(Heap *heap, size_t count = 1) SW
5556
return heap->alloc<uint64_t>(count);
5657
}
5758

59+
// MARK: - HashTable
60+
61+
OAG_INLINE uint64_t test_string_hash(const char *str) {
62+
return string_hash(str);
63+
}
64+
65+
class CustomTableTestHelper {
66+
UntypedTable _table;
67+
68+
static uint64_t custom_hash(void const *key) {
69+
return string_hash(static_cast<char const *>(key));
70+
}
71+
72+
static bool custom_compare(void const *a, void const *b) {
73+
return strcmp(static_cast<char const *>(a), static_cast<char const *>(b)) == 0;
74+
}
75+
76+
static void on_remove_key(const void *_Nonnull key) {
77+
_remove_key_count++;
78+
}
79+
80+
static void on_remove_value(const void *_Nullable value) {
81+
_remove_value_count++;
82+
}
83+
84+
static int _remove_key_count;
85+
static int _remove_value_count;
86+
87+
public:
88+
static CustomTableTestHelper *create() {
89+
return new CustomTableTestHelper();
90+
}
91+
92+
static void destroy(CustomTableTestHelper *value) { delete value; }
93+
94+
static int remove_key_count() { return _remove_key_count; }
95+
static int remove_value_count() { return _remove_value_count; }
96+
static void reset_counters() { _remove_key_count = 0; _remove_value_count = 0; }
97+
98+
bool empty() const { return _table.empty(); }
99+
uint64_t count() const { return _table.count(); }
100+
101+
bool insert(const char *key, const void *_Nullable value) {
102+
return _table.insert(key, value);
103+
}
104+
105+
const void *_Nullable lookup(const char *key, const char *_Nullable *_Nullable found_key) {
106+
return _table.lookup(key, (UntypedTable::nullable_key_type *)found_key);
107+
}
108+
109+
bool remove(const char *key) {
110+
return _table.remove(key);
111+
}
112+
113+
void for_each(UntypedTable::entry_callback body, void *_Nullable context) const {
114+
_table.for_each(body, context);
115+
}
116+
117+
private:
118+
CustomTableTestHelper()
119+
: _table(custom_hash, custom_compare, on_remove_key, on_remove_value, nullptr) {}
120+
} SWIFT_UNSAFE_REFERENCE;
121+
122+
int CustomTableTestHelper::_remove_key_count = 0;
123+
int CustomTableTestHelper::_remove_value_count = 0;
124+
58125
// MARK: - cf_ptr
59126

60127
using cf_data_ptr = cf_ptr<CFDataRef>;

Tests/UtilitiesTests/CFPtrTests.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ struct CFPtrTests {
3939

4040
@Test("Reset clears value")
4141
@available(iOS 16.4, *)
42-
func resetClears() {
42+
func resetClears() throws {
4343
let bytes: [UInt8] = [10, 20]
4444
let helper = bytes.withUnsafeBufferPointer { buf in
4545
util.CFPtrTestHelper.create(buf.baseAddress, Int(buf.count))
@@ -48,7 +48,7 @@ struct CFPtrTests {
4848
util.CFPtrTestHelper.destroy(helper)
4949
}
5050

51-
try! #require(helper.has_value() == true)
51+
try #require(helper.has_value() == true)
5252

5353
helper.reset()
5454

@@ -110,7 +110,7 @@ struct CFPtrTests {
110110

111111
@Test("Move transfers ownership")
112112
@available(iOS 16.4, *)
113-
func moveTransfersOwnership() {
113+
func moveTransfersOwnership() throws {
114114
let bytes: [UInt8] = [1, 2, 3, 4]
115115
let source = bytes.withUnsafeBufferPointer { buf in
116116
util.CFPtrTestHelper.create(buf.baseAddress, Int(buf.count))
@@ -119,7 +119,7 @@ struct CFPtrTests {
119119
util.CFPtrTestHelper.destroy(source)
120120
}
121121

122-
try! #require(source.has_value() == true)
122+
try #require(source.has_value() == true)
123123

124124
let dest = source.move_to_new()
125125
defer {

Tests/UtilitiesTests/HashTableTests.swift

Lines changed: 232 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,12 @@
55
import Utilities
66
import Testing
77

8+
// CustomTableTestHelper uses static counters for C function pointer callbacks
9+
// (C callbacks can't capture per-instance state). @MainActor ensures tests
10+
// touching those shared counters run serially on the main actor, preventing
11+
// data races while keeping the rest of the suite concurrent-safe.
812
@Suite("HashTable tests")
13+
@MainActor
914
struct HashTableTests {
1015

1116
@Test("Initialize empty table")
@@ -108,7 +113,7 @@ struct HashTableTests {
108113

109114
@Test("Remove entry")
110115
@available(iOS 16.4, *)
111-
func removeEntry() {
116+
func removeEntry() throws {
112117
class Value {
113118
let prop: String
114119
init(prop: String) {
@@ -123,12 +128,12 @@ struct HashTableTests {
123128

124129
let key = "key1"
125130
let value = Value(prop: "valueProp")
126-
withUnsafePointer(to: key) { keyPointer in
127-
withUnsafePointer(to: value) { valuePointer in
131+
try withUnsafePointer(to: key) { keyPointer in
132+
try withUnsafePointer(to: value) { valuePointer in
128133
let inserted = table.insert(keyPointer, valuePointer)
129134

130-
try! #require(inserted == true)
131-
try! #require(table.count() == 1)
135+
try #require(inserted == true)
136+
try #require(table.count() == 1)
132137

133138
let removed = table.remove(keyPointer)
134139

@@ -291,6 +296,228 @@ struct HashTableTests {
291296
#expect(iterationCount == 2, "for_each should visit exactly 2 items after reinsertion")
292297
}
293298

299+
// MARK: - Custom table tests (non-pointer compare path)
300+
301+
@Test("Custom table: insert and lookup by string content")
302+
@available(iOS 16.4, *)
303+
func customTableInsertAndLookup() throws {
304+
util.CustomTableTestHelper.reset_counters()
305+
let table = util.CustomTableTestHelper.create()
306+
defer {
307+
util.CustomTableTestHelper.destroy(table)
308+
}
309+
310+
#expect(table.empty())
311+
312+
// Use strdup for persistent C strings the table can store safely
313+
let key1 = strdup("hello")!
314+
let inserted = table.insert(key1, nil)
315+
#expect(inserted == true)
316+
#expect(table.count() == 1)
317+
318+
// Lookup with a different pointer but same string content
319+
let key2 = strdup("hello")!
320+
defer { free(key2) }
321+
322+
var foundKey: UnsafePointer<CChar>? = nil
323+
_ = table.__lookupUnsafe(key2, &foundKey)
324+
let foundStr = try #require(foundKey, "Key should be found via custom compare")
325+
#expect(String(cString: foundStr) == "hello")
326+
}
327+
328+
@Test("Custom table: insert duplicate replaces value")
329+
@available(iOS 16.4, *)
330+
func customTableInsertDuplicate() {
331+
util.CustomTableTestHelper.reset_counters()
332+
let table = util.CustomTableTestHelper.create()
333+
defer {
334+
util.CustomTableTestHelper.destroy(table)
335+
}
336+
337+
var value1 = 100
338+
var value2 = 200
339+
340+
let key1 = strdup("key1")!
341+
let inserted1 = withUnsafePointer(to: &value1) { v in
342+
table.insert(key1, v)
343+
}
344+
#expect(inserted1 == true)
345+
#expect(table.count() == 1)
346+
347+
let keyCountBefore = util.CustomTableTestHelper.remove_key_count()
348+
let valCountBefore = util.CustomTableTestHelper.remove_value_count()
349+
350+
// Insert same key content again - should replace, not add
351+
let key1dup = strdup("key1")!
352+
let inserted2 = withUnsafePointer(to: &value2) { v in
353+
table.insert(key1dup, v)
354+
}
355+
#expect(inserted2 == false, "Duplicate insert should return false")
356+
#expect(table.count() == 1, "Count should not change on replace")
357+
358+
// Callbacks should have been called for the old key/value
359+
#expect(util.CustomTableTestHelper.remove_key_count() == keyCountBefore + 1)
360+
#expect(util.CustomTableTestHelper.remove_value_count() == valCountBefore + 1)
361+
free(key1) // old key was replaced, free it
362+
}
363+
364+
@Test("Custom table: remove calls callbacks")
365+
@available(iOS 16.4, *)
366+
func customTableRemoveCallbacks() {
367+
util.CustomTableTestHelper.reset_counters()
368+
let table = util.CustomTableTestHelper.create()
369+
defer {
370+
util.CustomTableTestHelper.destroy(table)
371+
}
372+
373+
var value1 = 42
374+
let key1 = strdup("removeMe")!
375+
withUnsafePointer(to: &value1) { v in
376+
_ = table.insert(key1, v)
377+
}
378+
#expect(table.count() == 1)
379+
380+
let keyCountBefore = util.CustomTableTestHelper.remove_key_count()
381+
let valCountBefore = util.CustomTableTestHelper.remove_value_count()
382+
383+
// Remove using custom compare (non-pointer path) with different pointer
384+
let key2 = strdup("removeMe")!
385+
defer { free(key2) }
386+
let removed = table.remove(key2)
387+
#expect(removed == true)
388+
#expect(table.count() == 0)
389+
390+
#expect(util.CustomTableTestHelper.remove_key_count() == keyCountBefore + 1)
391+
#expect(util.CustomTableTestHelper.remove_value_count() == valCountBefore + 1)
392+
free(key1) // removed key, free it
393+
}
394+
395+
@Test("Custom table: remove non-existent key returns false")
396+
@available(iOS 16.4, *)
397+
func customTableRemoveNonExistent() {
398+
util.CustomTableTestHelper.reset_counters()
399+
let table = util.CustomTableTestHelper.create()
400+
defer {
401+
util.CustomTableTestHelper.destroy(table)
402+
}
403+
404+
var value = 1
405+
let key1 = strdup("existing")!
406+
withUnsafePointer(to: &value) { v in
407+
_ = table.insert(key1, v)
408+
}
409+
410+
let key2 = strdup("nonexistent")!
411+
defer { free(key2) }
412+
let removed = table.remove(key2)
413+
#expect(removed == false)
414+
#expect(table.count() == 1)
415+
}
416+
417+
@Test("Custom table: destructor calls callbacks for remaining entries")
418+
@available(iOS 16.4, *)
419+
func customTableDestructorCallbacks() {
420+
util.CustomTableTestHelper.reset_counters()
421+
422+
let table = util.CustomTableTestHelper.create()
423+
var v1 = 10
424+
var v2 = 20
425+
let keyA = strdup("a")!
426+
let keyB = strdup("b")!
427+
withUnsafePointer(to: &v1) { v in
428+
_ = table.insert(keyA, v)
429+
}
430+
withUnsafePointer(to: &v2) { v in
431+
_ = table.insert(keyB, v)
432+
}
433+
#expect(table.count() == 2)
434+
435+
// Destroy the table - should trigger callbacks for remaining 2 entries
436+
util.CustomTableTestHelper.destroy(table)
437+
438+
#expect(util.CustomTableTestHelper.remove_key_count() == 2)
439+
#expect(util.CustomTableTestHelper.remove_value_count() == 2)
440+
}
441+
442+
@Test("Custom table: lookup with found_key_out")
443+
@available(iOS 16.4, *)
444+
func customTableLookupFoundKeyOut() {
445+
util.CustomTableTestHelper.reset_counters()
446+
let table = util.CustomTableTestHelper.create()
447+
defer {
448+
util.CustomTableTestHelper.destroy(table)
449+
}
450+
451+
var value = 99
452+
let key1 = strdup("theKey")!
453+
withUnsafePointer(to: &value) { v in
454+
_ = table.insert(key1, v)
455+
}
456+
457+
// Lookup with found_key_out using different pointer with same content
458+
let key2 = strdup("theKey")!
459+
defer { free(key2) }
460+
var foundKey: UnsafePointer<CChar>? = nil
461+
let result = table.__lookupUnsafe(key2, &foundKey)
462+
#expect(foundKey != nil, "found_key_out should be set on match")
463+
#expect(result != nil, "Value should be returned")
464+
465+
// Lookup non-existent with found_key_out
466+
let key3 = strdup("missing")!
467+
defer { free(key3) }
468+
var foundKey2: UnsafePointer<CChar>? = nil
469+
let result2 = table.__lookupUnsafe(key3, &foundKey2)
470+
#expect(foundKey2 == nil, "found_key_out should be nil for miss")
471+
#expect(result2 == nil)
472+
}
473+
474+
@Test("string_hash produces non-zero results")
475+
@available(iOS 16.4, *)
476+
func stringHashFunction() {
477+
let hash1 = "hello".withCString { util.test_string_hash($0) }
478+
let hash2 = "world".withCString { util.test_string_hash($0) }
479+
let hash3 = "hello".withCString { util.test_string_hash($0) }
480+
481+
#expect(hash1 != 0, "Hash should be non-zero for non-empty string")
482+
#expect(hash2 != 0, "Hash should be non-zero for non-empty string")
483+
#expect(hash1 == hash3, "Same string should produce same hash")
484+
#expect(hash1 != hash2, "Different strings should likely produce different hashes")
485+
}
486+
487+
// MARK: - Pointer table tests (additional coverage)
488+
489+
@Test("Pointer table: lookup with found_key_out")
490+
@available(iOS 16.4, *)
491+
func pointerTableLookupFoundKeyOut() {
492+
let table = util.UntypedTable.create()
493+
defer {
494+
util.UntypedTable.destroy(table)
495+
}
496+
497+
var key = 42
498+
var value = 100
499+
withUnsafePointer(to: &key) { k in
500+
withUnsafePointer(to: &value) { v in
501+
_ = table.insert(k, v)
502+
}
503+
}
504+
505+
// Lookup with found_key_out on pointer table
506+
var foundKey: UnsafeRawPointer? = nil
507+
withUnsafePointer(to: &key) { k in
508+
_ = table.__lookupUnsafe(k, &foundKey)
509+
}
510+
#expect(foundKey != nil, "found_key_out should be set on match")
511+
512+
// Miss case
513+
var otherKey = 999
514+
var foundKey2: UnsafeRawPointer? = nil
515+
withUnsafePointer(to: &otherKey) { k in
516+
_ = table.__lookupUnsafe(k, &foundKey2)
517+
}
518+
#expect(foundKey2 == nil, "found_key_out should be nil for miss")
519+
}
520+
294521
@Test("Grow buckets should not lose nodes")
295522
@available(iOS 16.4, *)
296523
func growBucketsPreservesAllNodes() {

0 commit comments

Comments
 (0)