package runner
import (
"fmt"
"testing"
"github.com/stretchr/testify/require"
)
func Test_hashMap(t *testing.T) {
t.Run("tasks can be added", func(t *testing.T) {
hm := newHashMap(0)
task := newBasicMockTask(1)
require.True(t, hm.Add(task), "Add should have returned true on the first call")
require.False(t, hm.Add(task), "Add should have returned false on the second call of the same task")
otherTask := newBasicMockTask(2)
require.True(t, hm.Add(otherTask), "Add should have returned true on the first call")
require.False(t, hm.Add(otherTask), "Add should have returned false on the second call of the same task")
tasks := collectMockTasks(hm)
require.ElementsMatch(t, tasks, []*mockTask{task, otherTask})
})
t.Run("tasks can be searched", func(t *testing.T) {
hm := newHashMap(0)
var (
task1 = newBasicMockTask(1)
task2 = newBasicMockTask(2)
)
require.True(t, hm.Add(task1))
require.True(t, hm.Has(task1), "task1 should be in the hashMap")
require.False(t, hm.Has(task2), "task2 should not be in the hashMap")
})
t.Run("tasks can be deleted", func(t *testing.T) {
hm := newHashMap(0)
var tasks []*mockTask
for i := 0; i < 10; i++ {
tasks = append(tasks, newBasicMockTask(uint64(i)))
}
for i := 0; i < 10; i++ {
tasks = append(tasks, newBasicMockTask(uint64(i)))
}
for _, task := range tasks {
require.True(t, hm.Add(task))
}
expectCount := 20
for _, task := range tasks {
require.Len(t, collectMockTasks(hm), expectCount)
expectCount--
require.True(t, hm.Delete(task))
}
require.Len(t, collectMockTasks(hm), 0)
})
t.Run("tasks are deduplicated", func(t *testing.T) {
hm := newHashMap(0)
var (
task1 = &mockTask{
HashFunc: func() uint64 { return 1 },
EqualsFunc: func(t Task) bool { return true },
}
task2 = &mockTask{
HashFunc: func() uint64 { return 1 },
EqualsFunc: func(t Task) bool { return true },
}
)
require.True(t, hm.Add(task1))
require.False(t, hm.Add(task2))
require.True(t, hm.Has(task1))
require.True(t, hm.Has(task2))
tasks := collectMockTasks(hm)
require.ElementsMatch(t, tasks, []*mockTask{task1})
})
t.Run("hash collisions are handled", func(t *testing.T) {
hm := newHashMap(0)
var (
task1 = newBasicMockTask(1)
task2 = newBasicMockTask(1)
)
require.True(t, hm.Add(task1))
require.True(t, hm.Add(task2))
require.True(t, hm.Has(task1))
require.True(t, hm.Has(task2))
tasks := collectMockTasks(hm)
require.ElementsMatch(t, tasks, []*mockTask{task1, task2})
})
}
func Benchmark_hashMap(b *testing.B) {
b.Run("Add element", func(b *testing.B) {
task := newBasicMockTask(0)
for i := 0; i < b.N; i++ {
hm := newHashMap(1)
hm.Add(task)
}
})
b.Run("Remove element", func(b *testing.B) {
task := newBasicMockTask(0)
var hashMaps []*hashMap
for i := 0; i < b.N; i++ {
hm := newHashMap(1)
hm.Add(task)
hashMaps = append(hashMaps, hm)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
hashMaps[i].Delete(task)
}
})
b.Run("Lookup element", func(b *testing.B) {
task := newBasicMockTask(0)
hm := newHashMap(0)
hm.Add(task)
b.ResetTimer()
for i := 0; i < b.N; i++ {
hm.Has(task)
}
})
b.Run("Add existing element", func(b *testing.B) {
task := newBasicMockTask(0)
hm := newHashMap(0)
hm.Add(task)
b.ResetTimer()
for i := 0; i < b.N; i++ {
hm.Add(task)
}
})
}
func Benchmark_hashMap_collisions(b *testing.B) {
collisionChecks := []int{1, 2, 5, 10, 100}
for _, collisions := range collisionChecks {
b.Run(fmt.Sprintf("%d collisions", collisions), func(b *testing.B) {
var tasks []*mockTask
for i := 0; i < collisions+1; i++ {
tasks = append(tasks, newBasicMockTask(0))
}
b.Run("Add", func(b *testing.B) {
for i := 0; i < b.N; i++ {
hm := newHashMap(0)
for _, task := range tasks {
hm.Add(task)
}
}
})
b.Run("Remove", func(b *testing.B) {
hm := newHashMap(0)
for _, task := range tasks {
hm.Add(task)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
for i := len(tasks) - 1; i >= 0; i-- {
hm.Delete(tasks[i])
}
}
})
b.Run("Lookup", func(b *testing.B) {
hm := newHashMap(0)
for _, task := range tasks {
hm.Add(task)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
for _, task := range tasks {
hm.Has(task)
}
}
})
})
}
}
func collectMockTasks(hm *hashMap) []*mockTask {
var res []*mockTask
for t := range hm.Iterate() {
res = append(res, t.(*mockTask))
}
return res
}
type mockTask struct {
HashFunc func() uint64
EqualsFunc func(t Task) bool
}
func newBasicMockTask(hash uint64) *mockTask {
return &mockTask{
HashFunc: func() uint64 { return hash },
}
}
var _ Task = (*mockTask)(nil)
func (mt *mockTask) Hash() uint64 {
if mt.HashFunc == nil {
return 0
}
return mt.HashFunc()
}
func (mt *mockTask) Equals(t Task) bool {
if mt.EqualsFunc == nil {
return mt == t.(*mockTask)
}
return mt.EqualsFunc(t)
}