sumdb/internal/tkv/tkvtest: add tester and in-memory implementation

This is part of a Go checksum database implementation.
Eventually the code will live outside golang.org/x/exp.

Although the expected implementation of tkv.Storage
is a real database or key-value store, it speeds testing
considerably to avoid needing a full-blown storage system
for writing tests. This CL adds tkvtest.Mem, a trivial in-memory
implementation of tkv.Storage, for testing.

This CL also adds a preliminary tester for tkv.Storage
implementations. It is used here for testing tkvtest.Mem
but can be used for testing other implementations as well.

Change-Id: Ib70bc25a7b519fa4b685f6ce4c91b3ed04e0aa30
Reviewed-on: https://go-review.googlesource.com/c/exp/+/161663
Reviewed-by: Filippo Valsorda <filippo@golang.org>
diff --git a/sumdb/internal/tkv/tkvtest/mem.go b/sumdb/internal/tkv/tkvtest/mem.go
new file mode 100644
index 0000000..780edc4
--- /dev/null
+++ b/sumdb/internal/tkv/tkvtest/mem.go
@@ -0,0 +1,116 @@
+// Copyright 2019 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package tkvtest
+
+import (
+	"context"
+	"errors"
+	"math/rand"
+	"sync"
+
+	"golang.org/x/exp/sumdb/internal/tkv"
+)
+
+// Mem is an in-memory implementation of Storage.
+// It is meant for tests and does not store any data to persistent storage.
+//
+// The zero value is an empty Mem ready for use.
+type Mem struct {
+	mu    sync.RWMutex
+	table map[string]string
+}
+
+// A memTx is a transaction in a Mem.
+type memTx struct {
+	m      *Mem
+	writes []tkv.Write
+}
+
+// errRetry is an internal sentinel indicating that the transaction should be retried.
+// It is never returned to the caller.
+var errRetry = errors.New("retry")
+
+// ReadOnly runs f in a read-only transaction.
+func (m *Mem) ReadOnly(ctx context.Context, f func(context.Context, tkv.Transaction) error) error {
+	tx := &memTx{m: m}
+	for {
+		err := func() error {
+			m.mu.Lock()
+			defer m.mu.Unlock()
+
+			if err := f(ctx, tx); err != nil {
+				return err
+			}
+			// Spurious retry with 10% probability.
+			if rand.Intn(10) == 0 {
+				return errRetry
+			}
+			return nil
+		}()
+		if err != errRetry {
+			return err
+		}
+	}
+}
+
+// ReadWrite runs f in a read-write transaction.
+func (m *Mem) ReadWrite(ctx context.Context, f func(context.Context, tkv.Transaction) error) error {
+	tx := &memTx{m: m}
+	for {
+		err := func() error {
+			m.mu.Lock()
+			defer m.mu.Unlock()
+
+			tx.writes = []tkv.Write{}
+			if err := f(ctx, tx); err != nil {
+				return err
+			}
+			// Spurious retry with 10% probability.
+			if rand.Intn(10) == 0 {
+				return errRetry
+			}
+			if m.table == nil {
+				m.table = make(map[string]string)
+			}
+			for _, w := range tx.writes {
+				if w.Value == "" {
+					delete(m.table, w.Key)
+				} else {
+					m.table[w.Key] = w.Value
+				}
+			}
+			return nil
+		}()
+		if err != errRetry {
+			return err
+		}
+	}
+}
+
+// ReadValues returns the values associated with the given keys.
+func (tx *memTx) ReadValues(ctx context.Context, keys []string) ([]string, error) {
+	vals := make([]string, len(keys))
+	for i, key := range keys {
+		vals[i] = tx.m.table[key]
+	}
+	return vals, nil
+}
+
+// ReadValue returns the value associated with the single key.
+func (tx *memTx) ReadValue(ctx context.Context, key string) (string, error) {
+	return tx.m.table[key], nil
+}
+
+// BufferWrites buffers a list of writes to be applied
+// to the table when the transaction commits.
+// The changes are not visible to reads within the transaction.
+// The map argument is not used after the call returns.
+func (tx *memTx) BufferWrites(list []tkv.Write) error {
+	if tx.writes == nil {
+		panic("BufferWrite on read-only transaction")
+	}
+	tx.writes = append(tx.writes, list...)
+	return nil
+}
diff --git a/sumdb/internal/tkv/tkvtest/mem_test.go b/sumdb/internal/tkv/tkvtest/mem_test.go
new file mode 100644
index 0000000..ac0c48b
--- /dev/null
+++ b/sumdb/internal/tkv/tkvtest/mem_test.go
@@ -0,0 +1,14 @@
+// Copyright 2019 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package tkvtest
+
+import (
+	"context"
+	"testing"
+)
+
+func TestMem(t *testing.T) {
+	TestStorage(t, context.Background(), new(Mem))
+}
diff --git a/sumdb/internal/tkv/tkvtest/test.go b/sumdb/internal/tkv/tkvtest/test.go
new file mode 100644
index 0000000..7f124b1
--- /dev/null
+++ b/sumdb/internal/tkv/tkvtest/test.go
@@ -0,0 +1,77 @@
+// Copyright 2019 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package tkvtest contains a test harness for testing tkv.Storage implementations.
+package tkvtest
+
+import (
+	"context"
+	"fmt"
+	"io"
+	"testing"
+
+	"golang.org/x/exp/sumdb/internal/tkv"
+)
+
+func TestStorage(t *testing.T, ctx context.Context, storage tkv.Storage) {
+	s := storage
+
+	// Insert records.
+	err := s.ReadWrite(ctx, func(ctx context.Context, tx tkv.Transaction) error {
+		for i := 0; i < 10; i++ {
+			err := tx.BufferWrites([]tkv.Write{
+				{Key: fmt.Sprint(i), Value: fmt.Sprint(-i)},
+				{Key: fmt.Sprint(1000 + i), Value: fmt.Sprint(-1000 - i)},
+			})
+			if err != nil {
+				t.Fatal(err)
+			}
+		}
+		return nil
+	})
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// Read the records back.
+	testRead := func() {
+		err := s.ReadOnly(ctx, func(ctx context.Context, tx tkv.Transaction) error {
+			for i := int64(0); i < 1010; i++ {
+				if i == 10 {
+					i = 1000
+				}
+				val, err := tx.ReadValue(ctx, fmt.Sprint(i))
+				if err != nil {
+					t.Fatalf("reading %v: %v", i, err)
+				}
+				if val != fmt.Sprint(-i) {
+					t.Fatalf("ReadValue %v = %q, want %v", i, val, fmt.Sprint(-i))
+				}
+			}
+			return nil
+		})
+		if err != nil {
+			t.Fatal(err)
+		}
+	}
+	testRead()
+
+	// Buffered writes in failed transaction should not be applied.
+	err = s.ReadWrite(ctx, func(ctx context.Context, tx tkv.Transaction) error {
+		tx.BufferWrites([]tkv.Write{
+			{Key: fmt.Sprint(0), Value: ""},          // delete
+			{Key: fmt.Sprint(1), Value: "overwrite"}, // overwrite
+		})
+		if err != nil {
+			t.Fatal(err)
+		}
+		return io.ErrUnexpectedEOF
+	})
+	if err != io.ErrUnexpectedEOF {
+		t.Fatalf("ReadWrite returned %v, want ErrUnexpectedEOF", err)
+	}
+
+	// All same values should still be there.
+	testRead()
+}