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?") } }