55import Utilities
66import 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
914struct 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