diff --git a/pkg/crdt/g_map.go b/pkg/crdt/g_map.go index 791c8e9..67f1587 100644 --- a/pkg/crdt/g_map.go +++ b/pkg/crdt/g_map.go @@ -1,4 +1,4 @@ -// crdt is a golang implementation of a crdt +// crdt provides go implementations for crdts package crdt import ( @@ -65,10 +65,19 @@ func (g *GMap[K, D]) get(key uint64) Bucket[D] { } func (g *GMap[K, D]) Get(key K) D { + if !g.Contains(key) { + var def D + return def + } + return g.get(g.clock.hashFunc(key)).Contents } func (g *GMap[K, D]) Mark(key K) { + if !g.Contains(key) { + return + } + g.lock.Lock() bucket := g.contents[g.clock.hashFunc(key)] bucket.Gravestone = true @@ -89,7 +98,6 @@ func (g *GMap[K, D]) IsMarked(key K) bool { } g.lock.RUnlock() - return marked } diff --git a/pkg/crdt/g_map_test.go b/pkg/crdt/g_map_test.go new file mode 100644 index 0000000..4cd05aa --- /dev/null +++ b/pkg/crdt/g_map_test.go @@ -0,0 +1,224 @@ +// crdt_test unit tests the crdt implementations +package crdt + +import ( + "hash/fnv" + "slices" + "testing" + "time" + + "github.com/tim-beatham/wgmesh/pkg/lib" +) + +func NewGmap() *GMap[string, bool] { + vectorClock := NewVectorClock("a", func(key string) uint64 { + hash := fnv.New64a() + hash.Write([]byte(key)) + return hash.Sum64() + }, 1) // 1 second stale time + + gMap := NewGMap[string, bool](vectorClock) + return gMap +} + +func TestGMapPutInsertsItems(t *testing.T) { + gMap := NewGmap() + gMap.Put("bruh1234", true) + + if !gMap.Contains("bruh1234") { + t.Fatalf(`value not added to map`) + } +} + +func TestGMapPutReplacesItems(t *testing.T) { + gMap := NewGmap() + gMap.Put("bruh1234", true) + gMap.Put("bruh1234", false) + + value := gMap.Get("bruh1234") + + if value { + t.Fatalf(`value should ahve been replaced to false`) + } +} + +func TestContainsValueNotPresent(t *testing.T) { + gMap := NewGmap() + + if gMap.Contains("sdhjsdhsdj") { + t.Fatalf(`value should not be present in the map`) + } +} + +func TestContainsValuePresent(t *testing.T) { + gMap := NewGmap() + key := "hehehehe" + gMap.Put(key, false) + + if !gMap.Contains(key) { + t.Fatalf(`%s should not be present in the map`, key) + } +} + +func TestGMapGetNotPresentReturnsError(t *testing.T) { + gMap := NewGmap() + value := gMap.Get("bruh123") + + if value != false { + t.Fatalf(`value should be default type false`) + } +} + +func TestGMapGetReturnsValue(t *testing.T) { + gMap := NewGmap() + gMap.Put("bobdylan", true) + + value := gMap.Get("bobdylan") + + if !value { + t.Fatalf("value should be true but was false") + } +} + +func TestMarkMarksTheValue(t *testing.T) { + gMap := NewGmap() + gMap.Put("hello123", true) + + gMap.Mark("hello123") + + if !gMap.IsMarked("hello123") { + t.Fatal(`hello123 should be marked`) + } +} + +func TestMarkValueNotPresent(t *testing.T) { + gMap := NewGmap() + gMap.Mark("ok123456") +} + +func TestKeysMapEmpty(t *testing.T) { + gMap := NewGmap() + + keys := gMap.Keys() + + if len(keys) != 0 { + t.Fatal(`list of keys was not empty but should be empty`) + } +} + +func TestKeysMapReturnsKeysInMap(t *testing.T) { + gMap := NewGmap() + + gMap.Put("a", false) + gMap.Put("b", false) + gMap.Put("c", false) + + keys := gMap.Keys() + + if len(keys) != 3 { + t.Fatal(`key length should be 3`) + } +} + +func TestSaveMapEmptyReturnsEmptyMap(t *testing.T) { + gMap := NewGmap() + + saveMap := gMap.Save() + + if len(saveMap) != 0 { + t.Fatal(`saves should be empty`) + } +} + +func TestSaveMapReturnsMapOfBuckets(t *testing.T) { + gMap := NewGmap() + gMap.Put("a", false) + gMap.Put("b", false) + gMap.Put("c", false) + + saveMap := gMap.Save() + + if len(saveMap) != 3 { + t.Fatalf(`save length should be 3`) + } +} + +func TestSaveWithKeysNoKeysReturnsEmptyBucket(t *testing.T) { + gMap := NewGmap() + gMap.Put("a", false) + gMap.Put("b", false) + gMap.Put("c", false) + + saveMap := gMap.SaveWithKeys([]uint64{}) + + if len(saveMap) != 0 { + t.Fatalf(`save map should be empty`) + } +} + +func TestSaveWithKeysReturnsIntersection(t *testing.T) { + gMap := NewGmap() + gMap.Put("a", false) + gMap.Put("b", false) + gMap.Put("c", false) + + clock := lib.MapKeys(gMap.GetClock()) + clock = clock[:len(clock)-1] + + values := gMap.SaveWithKeys(clock) + if len(values) != len(clock) { + t.Fatalf(`intersection not returned`) + } +} + +func TestGetClockMapEmptyReturnsEmptyClock(t *testing.T) { + gMap := NewGmap() + + clocks := gMap.GetClock() + + if len(clocks) != 0 { + t.Fatalf(`vector clock is not empty`) + } +} + +func TestGetClockReturnsAllCLocks(t *testing.T) { + gMap := NewGmap() + gMap.Put("a", false) + gMap.Put("b", false) + gMap.Put("c", false) + + clocks := lib.MapValues(gMap.GetClock()) + slices.Sort(clocks) + + if !slices.Equal([]uint64{0, 1, 2}, clocks) { + t.Fatalf(`clocks are invalid`) + } +} + +func TestGetHashChangesHashOnValueAdded(t *testing.T) { + gMap := NewGmap() + gMap.Put("a", false) + prevHash := gMap.GetHash() + + gMap.Put("b", true) + + if prevHash == gMap.GetHash() { + t.Fatalf(`hash should be different`) + } +} + +func TestPruneGarbageCollectsValuesThatHaveNotBeenUpdated(t *testing.T) { + gMap := NewGmap() + gMap.clock.Put("c", 12) + gMap.Put("c", false) + gMap.Put("a", false) + + time.Sleep(4 * time.Second) + gMap.Put("a", true) + + gMap.Prune() + + if gMap.Contains("c") { + t.Fatalf(`a should have been pruned`) + } +}