1
0
Fork 0
You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

748 lines
21 KiB
Go

package store
import (
"bytes"
"encoding/json"
"io/ioutil"
"net"
"os"
"path/filepath"
"reflect"
"sort"
"testing"
"time"
"github.com/rqlite/rqlite/testdata/chinook"
)
type mockSnapshotSink struct {
*os.File
}
func (m *mockSnapshotSink) ID() string {
return "1"
}
func (m *mockSnapshotSink) Cancel() error {
return nil
}
func Test_ClusterMeta(t *testing.T) {
8 years ago
c := newClusterMeta()
c.APIPeers["localhost:4002"] = "localhost:4001"
if c.AddrForPeer("localhost:4002") != "localhost:4001" {
t.Fatalf("wrong address returned for localhost:4002")
}
if c.AddrForPeer("127.0.0.1:4002") != "localhost:4001" {
t.Fatalf("wrong address returned for 127.0.0.1:4002")
}
if c.AddrForPeer("127.0.0.1:4004") != "" {
t.Fatalf("wrong address returned for 127.0.0.1:4003")
}
}
func Test_OpenStoreSingleNode(t *testing.T) {
s := mustNewStore(true)
defer os.RemoveAll(s.Path())
if err := s.Open(true); err != nil {
t.Fatalf("failed to open single-node store: %s", err.Error())
}
}
func Test_OpenStoreCloseSingleNode(t *testing.T) {
s := mustNewStore(true)
defer os.RemoveAll(s.Path())
if err := s.Open(true); err != nil {
t.Fatalf("failed to open single-node store: %s", err.Error())
}
if err := s.Close(true); err != nil {
t.Fatalf("failed to close single-node store: %s", err.Error())
}
}
func Test_SingleNodeInMemExecuteQuery(t *testing.T) {
s := mustNewStore(true)
defer os.RemoveAll(s.Path())
if err := s.Open(true); err != nil {
t.Fatalf("failed to open single-node store: %s", err.Error())
}
defer s.Close(true)
s.WaitForLeader(10 * time.Second)
queries := []string{
`CREATE TABLE foo (id INTEGER NOT NULL PRIMARY KEY, name TEXT)`,
`INSERT INTO foo(id, name) VALUES(1, "fiona")`,
}
_, err := s.Execute(queries, false, false)
if err != nil {
t.Fatalf("failed to execute on single node: %s", err.Error())
}
r, err := s.Query([]string{`SELECT * FROM foo`}, false, false, None)
if err != nil {
t.Fatalf("failed to query single node: %s", err.Error())
}
r, err = s.Query([]string{`SELECT * FROM foo`}, false, false, None)
if err != nil {
t.Fatalf("failed to query single node: %s", err.Error())
}
r, err = s.Query([]string{`SELECT * FROM foo`}, false, false, None)
if err != nil {
t.Fatalf("failed to query single node: %s", err.Error())
}
if exp, got := `["id","name"]`, asJSON(r[0].Columns); exp != got {
t.Fatalf("unexpected results for query\nexp: %s\ngot: %s", exp, got)
}
if exp, got := `[[1,"fiona"]]`, asJSON(r[0].Values); exp != got {
t.Fatalf("unexpected results for query\nexp: %s\ngot: %s", exp, got)
}
}
// Test_SingleNodeInMemExecuteQueryFail ensures database level errors are presented by the store.
func Test_SingleNodeInMemExecuteQueryFail(t *testing.T) {
s := mustNewStore(true)
defer os.RemoveAll(s.Path())
if err := s.Open(true); err != nil {
t.Fatalf("failed to open single-node store: %s", err.Error())
}
defer s.Close(true)
s.WaitForLeader(10 * time.Second)
queries := []string{
`INSERT INTO foo(id, name) VALUES(1, "fiona")`,
}
r, err := s.Execute(queries, false, false)
if err != nil {
t.Fatalf("failed to execute on single node: %s", err.Error())
}
if exp, got := "no such table: foo", r[0].Error; exp != got {
t.Fatalf("unexpected results for query\nexp: %s\ngot: %s", exp, got)
}
}
func Test_SingleNodeFileExecuteQuery(t *testing.T) {
9 years ago
s := mustNewStore(false)
defer os.RemoveAll(s.Path())
if err := s.Open(true); err != nil {
t.Fatalf("failed to open single-node store: %s", err.Error())
}
defer s.Close(true)
s.WaitForLeader(10 * time.Second)
queries := []string{
`CREATE TABLE foo (id INTEGER NOT NULL PRIMARY KEY, name TEXT)`,
`INSERT INTO foo(id, name) VALUES(1, "fiona")`,
}
_, err := s.Execute(queries, false, false)
if err != nil {
t.Fatalf("failed to execute on single node: %s", err.Error())
}
r, err := s.Query([]string{`SELECT * FROM foo`}, false, false, None)
if err != nil {
t.Fatalf("failed to query single node: %s", err.Error())
}
r, err = s.Query([]string{`SELECT * FROM foo`}, false, false, None)
if err != nil {
t.Fatalf("failed to query single node: %s", err.Error())
}
r, err = s.Query([]string{`SELECT * FROM foo`}, false, false, None)
if err != nil {
t.Fatalf("failed to query single node: %s", err.Error())
}
if exp, got := `["id","name"]`, asJSON(r[0].Columns); exp != got {
t.Fatalf("unexpected results for query\nexp: %s\ngot: %s", exp, got)
}
if exp, got := `[[1,"fiona"]]`, asJSON(r[0].Values); exp != got {
t.Fatalf("unexpected results for query\nexp: %s\ngot: %s", exp, got)
}
}
func Test_SingleNodeExecuteQueryTx(t *testing.T) {
s := mustNewStore(true)
defer os.RemoveAll(s.Path())
if err := s.Open(true); err != nil {
t.Fatalf("failed to open single-node store: %s", err.Error())
}
defer s.Close(true)
s.WaitForLeader(10 * time.Second)
queries := []string{
`CREATE TABLE foo (id INTEGER NOT NULL PRIMARY KEY, name TEXT)`,
`INSERT INTO foo(id, name) VALUES(1, "fiona")`,
}
_, err := s.Execute(queries, false, true)
if err != nil {
t.Fatalf("failed to execute on single node: %s", err.Error())
}
r, err := s.Query([]string{`SELECT * FROM foo`}, false, true, None)
if err != nil {
t.Fatalf("failed to query single node: %s", err.Error())
}
r, err = s.Query([]string{`SELECT * FROM foo`}, false, true, Weak)
if err != nil {
t.Fatalf("failed to query single node: %s", err.Error())
}
r, err = s.Query([]string{`SELECT * FROM foo`}, false, true, Strong)
if err != nil {
t.Fatalf("failed to query single node: %s", err.Error())
}
if exp, got := `["id","name"]`, asJSON(r[0].Columns); exp != got {
t.Fatalf("unexpected results for query\nexp: %s\ngot: %s", exp, got)
}
if exp, got := `[[1,"fiona"]]`, asJSON(r[0].Values); exp != got {
t.Fatalf("unexpected results for query\nexp: %s\ngot: %s", exp, got)
}
_, err = s.Execute(queries, false, true)
if err != nil {
t.Fatalf("failed to execute on single node: %s", err.Error())
}
}
func Test_SingleNodeLoad(t *testing.T) {
s := mustNewStore(true)
defer os.RemoveAll(s.Path())
if err := s.Open(true); err != nil {
t.Fatalf("failed to open single-node store: %s", err.Error())
}
defer s.Close(true)
s.WaitForLeader(10 * time.Second)
dump := `PRAGMA foreign_keys=OFF;
BEGIN TRANSACTION;
CREATE TABLE foo (id integer not null primary key, name text);
INSERT INTO "foo" VALUES(1,'fiona');
COMMIT;
`
buf := bytes.NewBufferString(dump)
n, err := s.Load(buf)
if err != nil {
t.Fatalf("failed to load dump: %s", err.Error())
}
if n != 5 {
t.Fatal("wrong number of statements loaded")
}
// Check that data were loaded correctly.
r, err := s.Query([]string{`SELECT * FROM foo`}, false, true, Strong)
if err != nil {
t.Fatalf("failed to query single node: %s", err.Error())
}
if exp, got := `["id","name"]`, asJSON(r[0].Columns); exp != got {
t.Fatalf("unexpected results for query\nexp: %s\ngot: %s", exp, got)
}
if exp, got := `[[1,"fiona"]]`, asJSON(r[0].Values); exp != got {
t.Fatalf("unexpected results for query\nexp: %s\ngot: %s", exp, got)
}
}
func Test_SingleNodeLoadBatchLargeBlank(t *testing.T) {
s := mustNewStore(true)
defer os.RemoveAll(s.Path())
if err := s.Open(true); err != nil {
t.Fatalf("failed to open single-node store: %s", err.Error())
}
defer s.Close(true)
s.WaitForLeader(10 * time.Second)
dump := `PRAGMA foreign_keys=OFF;
BEGIN TRANSACTION;
CREATE TABLE foo (id integer not null primary key, name text);
INSERT INTO "foo" VALUES(1,'fiona');
COMMIT;
`
buf := bytes.NewBufferString(dump)
n, err := s.Load(buf)
if err != nil {
t.Fatalf("failed to load dump: %s", err.Error())
}
if n != 5 {
t.Fatal("wrong number of statements loaded, exp: 2, got: ", n)
}
// Check that data were loaded correctly.
r, err := s.Query([]string{`SELECT * FROM foo`}, false, true, Strong)
if err != nil {
t.Fatalf("failed to query single node: %s", err.Error())
}
if exp, got := `["id","name"]`, asJSON(r[0].Columns); exp != got {
t.Fatalf("unexpected results for query\nexp: %s\ngot: %s", exp, got)
}
if exp, got := `[[1,"fiona"]]`, asJSON(r[0].Values); exp != got {
t.Fatalf("unexpected results for query\nexp: %s\ngot: %s", exp, got)
}
}
func Test_SingleNodeLoadNoStatements(t *testing.T) {
s := mustNewStore(true)
defer os.RemoveAll(s.Path())
if err := s.Open(true); err != nil {
t.Fatalf("failed to open single-node store: %s", err.Error())
}
defer s.Close(true)
s.WaitForLeader(10 * time.Second)
dump := `PRAGMA foreign_keys=OFF;
BEGIN TRANSACTION;
COMMIT;
`
buf := bytes.NewBufferString(dump)
n, err := s.Load(buf)
if err != nil {
t.Fatalf("failed to load dump: %s", err.Error())
}
if n != 3 {
t.Fatal("wrong number of statements loaded, exp: 1, got: ", n)
}
}
func Test_SingleNodeLoadEmpty(t *testing.T) {
s := mustNewStore(true)
defer os.RemoveAll(s.Path())
if err := s.Open(true); err != nil {
t.Fatalf("failed to open single-node store: %s", err.Error())
}
defer s.Close(true)
s.WaitForLeader(10 * time.Second)
buf := bytes.NewBufferString("")
n, err := s.Load(buf)
if err != nil {
t.Fatalf("failed to load dump: %s", err.Error())
}
if n != 0 {
t.Fatal("wrong number of statements loaded")
}
}
func Test_SingleNodeLoadChinook(t *testing.T) {
s := mustNewStore(true)
defer os.RemoveAll(s.Path())
if err := s.Open(true); err != nil {
t.Fatalf("failed to open single-node store: %s", err.Error())
}
defer s.Close(true)
s.WaitForLeader(10 * time.Second)
buf := bytes.NewBufferString(chinook.DB)
_, err := s.Load(buf)
if err != nil {
t.Fatalf("failed to load dump: %s", err.Error())
}
// Check that data were loaded correctly.
r, err := s.Query([]string{`SELECT count(*) FROM track`}, false, true, Strong)
if err != nil {
t.Fatalf("failed to query single node: %s", err.Error())
}
if exp, got := `["count(*)"]`, asJSON(r[0].Columns); exp != got {
t.Fatalf("unexpected results for query\nexp: %s\ngot: %s", exp, got)
}
if exp, got := `[[3503]]`, asJSON(r[0].Values); exp != got {
t.Fatalf("unexpected results for query\nexp: %s\ngot: %s", exp, got)
}
r, err = s.Query([]string{`SELECT count(*) FROM album`}, false, true, Strong)
if err != nil {
t.Fatalf("failed to query single node: %s", err.Error())
}
if exp, got := `["count(*)"]`, asJSON(r[0].Columns); exp != got {
t.Fatalf("unexpected results for query\nexp: %s\ngot: %s", exp, got)
}
if exp, got := `[[347]]`, asJSON(r[0].Values); exp != got {
t.Fatalf("unexpected results for query\nexp: %s\ngot: %s", exp, got)
}
r, err = s.Query([]string{`SELECT count(*) FROM artist`}, false, true, Strong)
if err != nil {
t.Fatalf("failed to query single node: %s", err.Error())
}
if exp, got := `["count(*)"]`, asJSON(r[0].Columns); exp != got {
t.Fatalf("unexpected results for query\nexp: %s\ngot: %s", exp, got)
}
if exp, got := `[[275]]`, asJSON(r[0].Values); exp != got {
t.Fatalf("unexpected results for query\nexp: %s\ngot: %s", exp, got)
}
}
func Test_MultiNodeJoinRemove(t *testing.T) {
s0 := mustNewStore(true)
defer os.RemoveAll(s0.Path())
if err := s0.Open(true); err != nil {
t.Fatalf("failed to open node for multi-node test: %s", err.Error())
}
defer s0.Close(true)
s0.WaitForLeader(10 * time.Second)
s1 := mustNewStore(true)
defer os.RemoveAll(s1.Path())
if err := s1.Open(false); err != nil {
t.Fatalf("failed to open node for multi-node test: %s", err.Error())
}
defer s1.Close(true)
// Get sorted list of cluster nodes.
storeNodes := []string{s0.Addr().String(), s1.Addr().String()}
sort.StringSlice(storeNodes).Sort()
// Join the second node to the first.
if err := s0.Join(s1.Addr().String()); err != nil {
t.Fatalf("failed to join to node at %s: %s", s0.Addr().String(), err.Error())
}
nodes, err := s0.Nodes()
if err != nil {
t.Fatalf("failed to get nodes: %s", err.Error())
}
sort.StringSlice(nodes).Sort()
if len(nodes) != len(storeNodes) {
t.Fatalf("size of cluster is not correct")
}
if storeNodes[0] != nodes[0] && storeNodes[1] != nodes[1] {
t.Fatalf("cluster does not have correct nodes")
}
// Remove a node.
if err := s0.Remove(s1.Addr().String()); err != nil {
t.Fatalf("failed to remove %s from cluster: %s", s1.Addr().String(), err.Error())
}
nodes, err = s0.Nodes()
if err != nil {
t.Fatalf("failed to get nodes post remove: %s", err.Error())
}
if len(nodes) != 1 {
t.Fatalf("size of cluster is not correct post remove")
}
if s0.Addr().String() != nodes[0] {
t.Fatalf("cluster does not have correct nodes post remove")
}
}
func Test_MultiNodeExecuteQuery(t *testing.T) {
s0 := mustNewStore(true)
defer os.RemoveAll(s0.Path())
if err := s0.Open(true); err != nil {
t.Fatalf("failed to open node for multi-node test: %s", err.Error())
}
defer s0.Close(true)
s0.WaitForLeader(10 * time.Second)
s1 := mustNewStore(true)
defer os.RemoveAll(s1.Path())
if err := s1.Open(false); err != nil {
t.Fatalf("failed to open node for multi-node test: %s", err.Error())
}
defer s1.Close(true)
// Join the second node to the first.
if err := s0.Join(s1.Addr().String()); err != nil {
t.Fatalf("failed to join to node at %s: %s", s0.Addr().String(), err.Error())
}
queries := []string{
`CREATE TABLE foo (id INTEGER NOT NULL PRIMARY KEY, name TEXT)`,
`INSERT INTO foo(id, name) VALUES(1, "fiona")`,
}
_, err := s0.Execute(queries, false, false)
if err != nil {
t.Fatalf("failed to execute on single node: %s", err.Error())
}
r, err := s0.Query([]string{`SELECT * FROM foo`}, false, false, None)
if err != nil {
t.Fatalf("failed to query single node: %s", err.Error())
}
if exp, got := `["id","name"]`, asJSON(r[0].Columns); exp != got {
t.Fatalf("unexpected results for query\nexp: %s\ngot: %s", exp, got)
}
if exp, got := `[[1,"fiona"]]`, asJSON(r[0].Values); exp != got {
t.Fatalf("unexpected results for query\nexp: %s\ngot: %s", exp, got)
}
// Wait until the 3 log entries have been applied to the follower,
// and then query.
if err := s1.WaitForAppliedIndex(3, 5*time.Second); err != nil {
t.Fatalf("error waiting for follower to apply index: %s:", err.Error())
}
r, err = s1.Query([]string{`SELECT * FROM foo`}, false, false, Weak)
if err == nil {
t.Fatalf("successfully queried non-leader node")
}
r, err = s1.Query([]string{`SELECT * FROM foo`}, false, false, Strong)
if err == nil {
t.Fatalf("successfully queried non-leader node")
}
r, err = s1.Query([]string{`SELECT * FROM foo`}, false, false, None)
if err != nil {
t.Fatalf("failed to query single node: %s", err.Error())
}
if exp, got := `["id","name"]`, asJSON(r[0].Columns); exp != got {
t.Fatalf("unexpected results for query\nexp: %s\ngot: %s", exp, got)
}
if exp, got := `[[1,"fiona"]]`, asJSON(r[0].Values); exp != got {
t.Fatalf("unexpected results for query\nexp: %s\ngot: %s", exp, got)
}
}
func Test_SingleNodeSnapshotOnDisk(t *testing.T) {
s := mustNewStore(false)
defer os.RemoveAll(s.Path())
if err := s.Open(true); err != nil {
t.Fatalf("failed to open single-node store: %s", err.Error())
}
defer s.Close(true)
s.WaitForLeader(10 * time.Second)
queries := []string{
`CREATE TABLE foo (id INTEGER NOT NULL PRIMARY KEY, name TEXT)`,
`INSERT INTO foo(id, name) VALUES(1, "fiona")`,
}
_, err := s.Execute(queries, false, false)
if err != nil {
t.Fatalf("failed to execute on single node: %s", err.Error())
}
_, err = s.Query([]string{`SELECT * FROM foo`}, false, false, None)
if err != nil {
t.Fatalf("failed to query single node: %s", err.Error())
}
// Snap the node and write to disk.
f, err := s.Snapshot()
if err != nil {
t.Fatalf("failed to snapshot node: %s", err.Error())
}
snapDir := mustTempDir()
defer os.RemoveAll(snapDir)
snapFile, err := os.Create(filepath.Join(snapDir, "snapshot"))
if err != nil {
t.Fatalf("failed to create snapshot file: %s", err.Error())
}
sink := &mockSnapshotSink{snapFile}
if err := f.Persist(sink); err != nil {
t.Fatalf("failed to persist snapshot to disk: %s", err.Error())
}
// Check restoration.
snapFile, err = os.Open(filepath.Join(snapDir, "snapshot"))
if err != nil {
t.Fatalf("failed to open snapshot file: %s", err.Error())
}
if err := s.Restore(snapFile); err != nil {
t.Fatalf("failed to restore snapshot from disk: %s", err.Error())
}
// Ensure database is back in the correct state.
r, err := s.Query([]string{`SELECT * FROM foo`}, false, false, None)
if err != nil {
t.Fatalf("failed to query single node: %s", err.Error())
}
if exp, got := `["id","name"]`, asJSON(r[0].Columns); exp != got {
t.Fatalf("unexpected results for query\nexp: %s\ngot: %s", exp, got)
}
if exp, got := `[[1,"fiona"]]`, asJSON(r[0].Values); exp != got {
t.Fatalf("unexpected results for query\nexp: %s\ngot: %s", exp, got)
}
}
func Test_SingleNodeSnapshotInMem(t *testing.T) {
s := mustNewStore(true)
defer os.RemoveAll(s.Path())
if err := s.Open(true); err != nil {
t.Fatalf("failed to open single-node store: %s", err.Error())
}
defer s.Close(true)
s.WaitForLeader(10 * time.Second)
queries := []string{
`CREATE TABLE foo (id INTEGER NOT NULL PRIMARY KEY, name TEXT)`,
`INSERT INTO foo(id, name) VALUES(1, "fiona")`,
}
_, err := s.Execute(queries, false, false)
if err != nil {
t.Fatalf("failed to execute on single node: %s", err.Error())
}
_, err = s.Query([]string{`SELECT * FROM foo`}, false, false, None)
if err != nil {
t.Fatalf("failed to query single node: %s", err.Error())
}
// Snap the node and write to disk.
f, err := s.Snapshot()
if err != nil {
t.Fatalf("failed to snapshot node: %s", err.Error())
}
snapDir := mustTempDir()
defer os.RemoveAll(snapDir)
snapFile, err := os.Create(filepath.Join(snapDir, "snapshot"))
if err != nil {
t.Fatalf("failed to create snapshot file: %s", err.Error())
}
sink := &mockSnapshotSink{snapFile}
if err := f.Persist(sink); err != nil {
t.Fatalf("failed to persist snapshot to disk: %s", err.Error())
}
// Check restoration.
snapFile, err = os.Open(filepath.Join(snapDir, "snapshot"))
if err != nil {
t.Fatalf("failed to open snapshot file: %s", err.Error())
}
if err := s.Restore(snapFile); err != nil {
t.Fatalf("failed to restore snapshot from disk: %s", err.Error())
}
// Ensure database is back in the correct state.
r, err := s.Query([]string{`SELECT * FROM foo`}, false, false, None)
if err != nil {
t.Fatalf("failed to query single node: %s", err.Error())
}
if exp, got := `["id","name"]`, asJSON(r[0].Columns); exp != got {
t.Fatalf("unexpected results for query\nexp: %s\ngot: %s", exp, got)
}
if exp, got := `[[1,"fiona"]]`, asJSON(r[0].Values); exp != got {
t.Fatalf("unexpected results for query\nexp: %s\ngot: %s", exp, got)
}
}
func Test_APIPeers(t *testing.T) {
s := mustNewStore(false)
defer os.RemoveAll(s.Path())
if err := s.Open(true); err != nil {
t.Fatalf("failed to open single-node store: %s", err.Error())
}
defer s.Close(true)
s.WaitForLeader(10 * time.Second)
peers := map[string]string{
"localhost:4002": "localhost:4001",
"localhost:4004": "localhost:4003",
}
if err := s.UpdateAPIPeers(peers); err != nil {
t.Fatalf("failed to update API peers: %s", err.Error())
}
// Retrieve peers and verify them.
apiPeers, err := s.APIPeers()
if err != nil {
t.Fatalf("failed to retrieve API peers: %s", err.Error())
}
if !reflect.DeepEqual(peers, apiPeers) {
t.Fatalf("set and retrieved API peers not identical, got %v, exp %v",
apiPeers, peers)
}
if s.Peer("localhost:4002") != "localhost:4001" ||
s.Peer("localhost:4004") != "localhost:4003" ||
s.Peer("not exist") != "" {
t.Fatalf("failed to retrieve correct single API peer")
}
}
func Test_IsLeader(t *testing.T) {
s := mustNewStore(true)
defer os.RemoveAll(s.Path())
if err := s.Open(true); err != nil {
t.Fatalf("failed to open single-node store: %s", err.Error())
}
defer s.Close(true)
s.WaitForLeader(10 * time.Second)
if !s.IsLeader() {
t.Fatalf("single node is not leader!")
}
}
func Test_State(t *testing.T) {
s := mustNewStore(true)
defer os.RemoveAll(s.Path())
if err := s.Open(true); err != nil {
t.Fatalf("failed to open single-node store: %s", err.Error())
}
defer s.Close(true)
s.WaitForLeader(10 * time.Second)
state := s.State()
if state != Leader {
t.Fatalf("single node returned incorrect state (not Leader): %v", s)
}
}
func mustNewStore(inmem bool) *Store {
path := mustTempDir()
defer os.RemoveAll(path)
cfg := NewDBConfig("", inmem)
s := New(&StoreConfig{
DBConf: cfg,
Dir: path,
Tn: mustMockTransport("localhost:0"),
})
if s == nil {
panic("failed to create new store")
}
return s
}
func mustTempDir() string {
var err error
path, err := ioutil.TempDir("", "rqlilte-test-")
if err != nil {
panic("failed to create temp dir")
}
return path
}
type mockTransport struct {
ln net.Listener
}
func mustMockTransport(addr string) Transport {
ln, err := net.Listen("tcp", addr)
if err != nil {
panic("failed to create new transport")
}
return &mockTransport{ln}
}
func (m *mockTransport) Dial(addr string, timeout time.Duration) (net.Conn, error) {
return net.DialTimeout("tcp", addr, timeout)
}
func (m *mockTransport) Accept() (net.Conn, error) { return m.ln.Accept() }
func (m *mockTransport) Close() error { return m.ln.Close() }
func (m *mockTransport) Addr() net.Addr { return m.ln.Addr() }
func asJSON(v interface{}) string {
b, err := json.Marshal(v)
if err != nil {
panic("failed to JSON marshal value")
}
return string(b)
}