diff --git a/index.go b/index.go index 7cc7477..730a015 100644 --- a/index.go +++ b/index.go @@ -98,6 +98,8 @@ func (idx *index) bucketIndex(hash uint32) uint32 { return bidx } +// TODO: deeply nested callbacks are hard to reason about. +// TODO: rewrite the function to return an iterator if there is no performance implications. func (idx *index) forEachBucket(startBucketIdx uint32, cb func(bucketHandle) (bool, error)) error { off := bucketOffset(startBucketIdx) f := idx.main.MmapFile diff --git a/internal/assert/assert.go b/internal/assert/assert.go index dcd3bcf..6025516 100644 --- a/internal/assert/assert.go +++ b/internal/assert/assert.go @@ -14,14 +14,31 @@ func Equal(t testing.TB, expected interface{}, actual interface{}) { } } -// Nil fails the test when actual is not nil. -func Nil(t testing.TB, actual interface{}) { - if actual != nil && !reflect.ValueOf(actual).IsNil() { +// https://github.com/golang/go/blob/go1.15/src/reflect/value.go#L1071 +var nillableKinds = map[reflect.Kind]bool{ + reflect.Chan: true, + reflect.Func: true, + reflect.Map: true, + reflect.Ptr: true, + reflect.UnsafePointer: true, + reflect.Interface: true, + reflect.Slice: true, +} + +// Nil fails the test when obj is not nil. +func Nil(t testing.TB, obj interface{}) { + if obj == nil { + return + } + val := reflect.ValueOf(obj) + if !nillableKinds[val.Kind()] || !val.IsNil() { t.Helper() - t.Fatalf("expected nil; got %+v", actual) + t.Fatalf("expected nil; got %+v", obj) } } +const pollingInterval = time.Millisecond * 10 // How often CompleteWithin polls the cond function. + // CompleteWithin fails the test when cond doesn't succeed within waitDur. func CompleteWithin(t testing.TB, waitDur time.Duration, cond func() bool) { start := time.Now() @@ -29,13 +46,13 @@ func CompleteWithin(t testing.TB, waitDur time.Duration, cond func() bool) { if cond() { return } - time.Sleep(time.Millisecond * 10) + time.Sleep(pollingInterval) } t.Helper() t.Fatalf("expected to complete within %v", waitDur) } -// Panic fails the test when the test doesn't panic. +// Panic fails the test when the test doesn't panic with the expected message. func Panic(t testing.TB, expectedMessage string, f func()) { t.Helper() var message interface{} diff --git a/internal/assert/assert_test.go b/internal/assert/assert_test.go new file mode 100644 index 0000000..47b8d0a --- /dev/null +++ b/internal/assert/assert_test.go @@ -0,0 +1,257 @@ +package assert + +import ( + "fmt" + "sync" + "testing" + "time" +) + +func TestEqual(t *testing.T) { + testCases := []struct { + first interface{} + second interface{} + expectedFailed bool + }{ + { + first: 1, + second: 1, + expectedFailed: false, + }, + + { + first: nil, + second: nil, + expectedFailed: false, + }, + { + first: "1", + second: "1", + expectedFailed: false, + }, + { + first: struct{}{}, + second: struct{}{}, + expectedFailed: false, + }, + { + first: struct{ x int }{x: 1}, + second: struct{ x int }{x: 1}, + expectedFailed: false, + }, + { + first: 1, + second: 2, + expectedFailed: true, + }, + { + first: 1, + second: "1", + expectedFailed: true, + }, + { + first: 1, + second: 1.0, + expectedFailed: true, + }, + { + first: struct{ x int }{x: 1}, + second: struct{ x int }{x: 2}, + expectedFailed: true, + }, + { + first: struct{ x int }{x: 1}, + second: struct{ y int }{y: 1}, + expectedFailed: true, + }, + } + + for i, tc := range testCases { + t.Run(fmt.Sprintf("case %d %+v", i, tc), func(t *testing.T) { + mock := &testing.T{} + wg := &sync.WaitGroup{} + wg.Add(1) + // Run the asserting in a goroutine. t.Fatal calls runtime.Goexit. + go func() { + defer wg.Done() + Equal(mock, tc.first, tc.second) + }() + wg.Wait() + failed := mock.Failed() + if tc.expectedFailed != failed { + t.Fatalf("expected to fail: %t; failed: %t", tc.expectedFailed, failed) + } + }) + } +} + +func TestNil(t *testing.T) { + var nilIntPtr *int + var nilStructPtr *struct{ x int } + var nilSlice []string + + testCases := []struct { + obj interface{} + expectedFailed bool + }{ + { + obj: nil, + expectedFailed: false, + }, + { + obj: nilIntPtr, + expectedFailed: false, + }, + { + obj: nilStructPtr, + expectedFailed: false, + }, + { + obj: nilSlice, + expectedFailed: false, + }, + { + obj: 1, + expectedFailed: true, + }, + { + obj: "1", + expectedFailed: true, + }, + { + obj: []string{}, + expectedFailed: true, + }, + { + obj: [2]int{1, 1}, + expectedFailed: true, + }, + } + + for i, tc := range testCases { + t.Run(fmt.Sprintf("case %d %+v", i, tc.obj), func(t *testing.T) { + mock := &testing.T{} + wg := &sync.WaitGroup{} + wg.Add(1) + go func() { + defer wg.Done() + Nil(mock, tc.obj) + }() + wg.Wait() + if tc.expectedFailed != mock.Failed() { + t.Fatalf("expected to fail: %t; failed: %t", tc.expectedFailed, mock.Failed()) + } + }) + } +} + +func TestPanic(t *testing.T) { + testCases := []struct { + name string + f func() + expectedFailed bool + }{ + { + name: "panic", + f: func() { + panic("message123") + }, + expectedFailed: false, + }, + { + name: "panic: wrong message", + f: func() { + panic("message456") + }, + expectedFailed: true, + }, + { + name: "no panic", + f: func() {}, + expectedFailed: true, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + mock := &testing.T{} + wg := &sync.WaitGroup{} + wg.Add(1) + go func() { + defer wg.Done() + Panic(mock, "message123", tc.f) + }() + wg.Wait() + if tc.expectedFailed != mock.Failed() { + t.Fatalf("expected to fail: %t; failed: %t", tc.expectedFailed, mock.Failed()) + } + }) + } +} + +func TestCompleteWithin(t *testing.T) { + var tc2Tries int + var tc4Tries int + testCases := []struct { + name string + dur time.Duration + cond func() bool + expectedFailed bool + }{ + { + name: "completed: first try", + dur: time.Hour, + cond: func() bool { + return true + }, + expectedFailed: false, + }, + { + name: "completed: second try", + dur: time.Hour, + cond: func() bool { + if tc2Tries == 0 { + tc2Tries++ + return false + } + return true + }, + expectedFailed: false, + }, + { + name: "not completed", + dur: time.Nanosecond, + cond: func() bool { + return false + }, + expectedFailed: true, + }, + { + name: "not completed: timeout", + dur: time.Nanosecond, + cond: func() bool { + if tc4Tries == 0 { + tc4Tries++ + time.Sleep(pollingInterval * 2) + return false + } + return true + }, + expectedFailed: true, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + mock := &testing.T{} + wg := &sync.WaitGroup{} + wg.Add(1) + go func() { + defer wg.Done() + CompleteWithin(mock, tc.dur, tc.cond) + }() + wg.Wait() + if tc.expectedFailed != mock.Failed() { + t.Fatalf("expected to fail: %t; failed: %t", tc.expectedFailed, mock.Failed()) + } + }) + } +}