427 lines
8.6 KiB
Go
427 lines
8.6 KiB
Go
package mdb
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
)
|
|
|
|
func TestCollection(t *testing.T) {
|
|
type Item struct {
|
|
ID uint64
|
|
Name string // Full map.
|
|
ExtID string // Partial map.
|
|
}
|
|
|
|
sanitize := func(item *Item) {
|
|
item.Name = strings.TrimSpace(item.Name)
|
|
item.ExtID = strings.TrimSpace(item.ExtID)
|
|
}
|
|
|
|
ErrInvalidExtID := errors.New("InvalidExtID")
|
|
|
|
validate := func(item *Item) error {
|
|
if len(item.ExtID) != 0 && !strings.HasPrefix(item.ExtID, "x") {
|
|
return ErrInvalidExtID
|
|
}
|
|
return nil
|
|
}
|
|
|
|
run := func(name string, inner func(t *testing.T, c *Collection[Item])) {
|
|
|
|
t.Run(name, func(t *testing.T) {
|
|
root := filepath.Join(os.TempDir(), randString())
|
|
//defer os.RemoveAll(root)
|
|
|
|
db := NewPrimary(root)
|
|
defer db.Close()
|
|
|
|
c := NewCollection(db, randString(), func(i *Item) uint64 { return i.ID })
|
|
c.SetSanitize(sanitize)
|
|
c.SetValidate(validate)
|
|
|
|
NewMapIndex(c,
|
|
"Name",
|
|
func(i *Item) string { return i.Name },
|
|
nil)
|
|
|
|
NewMapIndex(c,
|
|
"ExtID",
|
|
func(i *Item) string { return i.ExtID },
|
|
func(i *Item) bool { return i.ExtID != "" })
|
|
|
|
inner(t, c)
|
|
})
|
|
}
|
|
|
|
verifyCollectionOnce := func(c *Collection[Item], expected ...Item) error {
|
|
if len(c.items.m) != len(expected) {
|
|
return fmt.Errorf("Expected %d items, but got %d.", len(expected), len(c.items.m))
|
|
}
|
|
|
|
for _, item := range expected {
|
|
i, ok := c.Get(item.ID)
|
|
if !ok {
|
|
return fmt.Errorf("Missing expected item: %v", item)
|
|
}
|
|
if !reflect.DeepEqual(i, item) {
|
|
return fmt.Errorf("Items aren't equal: %v != %v", i, item)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
verifyCollection := func(c *Collection[Item], expected ...Item) error {
|
|
if err := verifyCollectionOnce(c, expected...); err != nil {
|
|
return fmt.Errorf("%w: original", err)
|
|
}
|
|
|
|
// Reload the collection and verify again.
|
|
c.db.Close()
|
|
|
|
db := NewSecondary(c.db.root)
|
|
c2 := NewCollection(db, c.name, func(i *Item) uint64 { return i.ID })
|
|
db.Start()
|
|
defer db.Close()
|
|
return verifyCollectionOnce(c2, expected...)
|
|
}
|
|
|
|
run("empty", func(t *testing.T, c *Collection[Item]) {
|
|
err := verifyCollection(c)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
})
|
|
|
|
run("check NextID", func(t *testing.T, c *Collection[Item]) {
|
|
id := c.NextID()
|
|
for i := 0; i < 100; i++ {
|
|
next := c.NextID()
|
|
if next <= id {
|
|
t.Fatal(next, id)
|
|
}
|
|
id = next
|
|
}
|
|
})
|
|
|
|
run("insert", func(t *testing.T, c *Collection[Item]) {
|
|
item, err := c.Insert(Item{1, "Name", "xid"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
err = verifyCollection(c, item)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
})
|
|
|
|
run("insert concurrent differnt items", func(t *testing.T, c *Collection[Item]) {
|
|
wg := sync.WaitGroup{}
|
|
for i := 0; i < 100; i++ {
|
|
wg.Add(1)
|
|
go func(i int) {
|
|
defer wg.Done()
|
|
c.Insert(Item{
|
|
ID: c.NextID(),
|
|
Name: fmt.Sprintf("Name.%03d", i),
|
|
ExtID: fmt.Sprintf("x.%03d", i),
|
|
})
|
|
}(i)
|
|
}
|
|
|
|
wg.Wait()
|
|
})
|
|
|
|
run("insert concurrent same item", func(t *testing.T, c *Collection[Item]) {
|
|
wg := sync.WaitGroup{}
|
|
for i := 0; i < 100; i++ {
|
|
wg.Add(1)
|
|
go func(i int) {
|
|
defer wg.Done()
|
|
c.Insert(Item{
|
|
ID: 1,
|
|
Name: "My name",
|
|
})
|
|
}(i)
|
|
}
|
|
|
|
wg.Wait()
|
|
|
|
if err := verifyCollection(c, Item{1, "My name", ""}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
})
|
|
|
|
run("insert invalid", func(t *testing.T, c *Collection[Item]) {
|
|
item, err := c.Insert(Item{
|
|
ID: c.NextID(),
|
|
Name: "Hello",
|
|
ExtID: "123"})
|
|
if !errors.Is(err, ErrInvalidExtID) {
|
|
t.Fatal(item, err)
|
|
}
|
|
})
|
|
|
|
run("insert duplicate ID", func(t *testing.T, c *Collection[Item]) {
|
|
item, err := c.Insert(Item{
|
|
ID: c.NextID(),
|
|
Name: "Hello",
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
item2, err := c.Insert(Item{ID: item.ID, Name: "Item"})
|
|
if !errors.Is(err, ErrDuplicate) {
|
|
t.Fatal(err, item2)
|
|
}
|
|
})
|
|
|
|
run("insert duplicate name", func(t *testing.T, c *Collection[Item]) {
|
|
_, err := c.Insert(Item{
|
|
ID: c.NextID(),
|
|
Name: "Hello",
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
item2, err := c.Insert(Item{ID: c.NextID(), Name: "Hello"})
|
|
if !errors.Is(err, ErrDuplicate) {
|
|
t.Fatal(err, item2)
|
|
}
|
|
})
|
|
|
|
run("insert duplicate ext ID", func(t *testing.T, c *Collection[Item]) {
|
|
_, err := c.Insert(Item{
|
|
ID: c.NextID(),
|
|
Name: "Hello",
|
|
ExtID: "x1",
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
item2, err := c.Insert(Item{ID: c.NextID(), Name: "name", ExtID: "x1"})
|
|
if !errors.Is(err, ErrDuplicate) {
|
|
t.Fatal(err, item2)
|
|
}
|
|
})
|
|
|
|
run("get not found", func(t *testing.T, c *Collection[Item]) {
|
|
item, err := c.Insert(Item{
|
|
ID: c.NextID(),
|
|
Name: "Hello",
|
|
ExtID: "x1",
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if i, ok := c.Get(item.ID + 1); ok {
|
|
t.Fatal(i)
|
|
}
|
|
})
|
|
|
|
run("update", func(t *testing.T, c *Collection[Item]) {
|
|
item1, err := c.Insert(Item{
|
|
ID: c.NextID(),
|
|
Name: "Hello",
|
|
ExtID: "x1",
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
err = c.Update(item1.ID, func(item Item) (Item, error) {
|
|
item.Name = "name"
|
|
item.ExtID = "x88"
|
|
return item, nil
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
err = verifyCollection(c, Item{ID: item1.ID, Name: "name", ExtID: "x88"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
})
|
|
|
|
run("update concurrent different items", func(t *testing.T, c *Collection[Item]) {
|
|
items := make([]Item, 10)
|
|
for i := range items {
|
|
item, err := c.Insert(Item{ID: c.NextID(), Name: randString()})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
items[i] = item
|
|
}
|
|
|
|
wg := sync.WaitGroup{}
|
|
wg.Add(10)
|
|
for i := range items {
|
|
item := items[i]
|
|
go func() {
|
|
defer wg.Done()
|
|
for x := 0; x < 100; x++ {
|
|
err := c.Update(item.ID, func(i Item) (Item, error) {
|
|
i.Name = randString()
|
|
return i, nil
|
|
})
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
wg.Wait()
|
|
})
|
|
|
|
run("update concurrent same item", func(t *testing.T, c *Collection[Item]) {
|
|
item, err := c.Insert(Item{ID: c.NextID(), Name: randString()})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
wg := sync.WaitGroup{}
|
|
wg.Add(10)
|
|
for i := 0; i < 10; i++ {
|
|
go func() {
|
|
defer wg.Done()
|
|
for x := 0; x < 100; x++ {
|
|
err := c.Update(item.ID, func(i Item) (Item, error) {
|
|
i.Name = randString()
|
|
return i, nil
|
|
})
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
wg.Wait()
|
|
})
|
|
|
|
run("update not found", func(t *testing.T, c *Collection[Item]) {
|
|
item, err := c.Insert(Item{ID: c.NextID(), Name: randString()})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
err = c.Update(item.ID+1, func(i Item) (Item, error) {
|
|
i.Name = randString()
|
|
return i, nil
|
|
})
|
|
if !errors.Is(err, ErrNotFound) {
|
|
t.Fatal(err)
|
|
}
|
|
})
|
|
|
|
run("update mismatched IDs", func(t *testing.T, c *Collection[Item]) {
|
|
item, err := c.Insert(Item{ID: c.NextID(), Name: randString()})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
err = c.Update(item.ID, func(i Item) (Item, error) {
|
|
i.ID++
|
|
return i, nil
|
|
})
|
|
|
|
if !errors.Is(err, ErrMismatchedIDs) {
|
|
t.Fatal(err)
|
|
}
|
|
})
|
|
|
|
run("update invalid", func(t *testing.T, c *Collection[Item]) {
|
|
item, err := c.Insert(Item{ID: c.NextID(), Name: randString()})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
err = c.Update(item.ID, func(i Item) (Item, error) {
|
|
i.ExtID = "a"
|
|
return i, nil
|
|
})
|
|
if !errors.Is(err, ErrInvalidExtID) {
|
|
t.Fatal(err)
|
|
}
|
|
})
|
|
|
|
run("delete", func(t *testing.T, c *Collection[Item]) {
|
|
item1, err := c.Insert(Item{c.NextID(), "name1", "x1"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
item2, err := c.Insert(Item{c.NextID(), "name2", "x2"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
item3, err := c.Insert(Item{c.NextID(), "name3", "x3"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
c.Delete(item2.ID)
|
|
if err := verifyCollection(c, item1, item3); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
})
|
|
|
|
run("delete not found", func(t *testing.T, c *Collection[Item]) {
|
|
item1, err := c.Insert(Item{c.NextID(), "name1", "x1"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
item2, err := c.Insert(Item{c.NextID(), "name2", "x2"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
item3, err := c.Insert(Item{c.NextID(), "name3", "x3"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
c.Delete(c.NextID())
|
|
if err := verifyCollection(c, item1, item2, item3); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
})
|
|
}
|
|
|
|
func BenchmarkLoad(b *testing.B) {
|
|
type Item struct {
|
|
ID uint64
|
|
Name string
|
|
}
|
|
|
|
root := filepath.Join("test-files", randString())
|
|
db := NewPrimary(root)
|
|
getID := func(item *Item) uint64 { return item.ID }
|
|
|
|
c := NewCollection(db, "items", getID)
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
item := Item{ID: c.NextID(), Name: fmt.Sprintf("Name %04d", i)}
|
|
c.Insert(item)
|
|
}
|
|
|
|
b.ResetTimer()
|
|
|
|
c2 := NewCollection(db, "items", getID)
|
|
log.Print(len(c2.items.m))
|
|
if len(c2.items.m) != b.N {
|
|
panic("What?")
|
|
}
|
|
}
|