Browse Source

etcdserver: move recorder to testutil

Xiang Li 11 years ago
parent
commit
27d47977d9
3 changed files with 226 additions and 207 deletions
  1. 7 6
      etcdserver/cluster_test.go
  2. 177 201
      etcdserver/server_test.go
  3. 42 0
      pkg/testutil/recorder.go

+ 7 - 6
etcdserver/cluster_test.go

@@ -23,6 +23,7 @@ import (
 	"reflect"
 	"testing"
 
+	"github.com/coreos/etcd/pkg/testutil"
 	"github.com/coreos/etcd/pkg/types"
 	"github.com/coreos/etcd/raft/raftpb"
 	"github.com/coreos/etcd/store"
@@ -535,10 +536,10 @@ func TestClusterAddMember(t *testing.T) {
 	c.SetStore(st)
 	c.AddMember(newTestMember(1, nil, "node1", nil))
 
-	wactions := []action{
+	wactions := []testutil.Action{
 		{
-			name: "Create",
-			params: []interface{}{
+			Name: "Create",
+			Params: []interface{}{
 				path.Join(storeMembersPrefix, "1", "raftAttributes"),
 				false,
 				`{"peerURLs":null}`,
@@ -623,9 +624,9 @@ func TestClusterRemoveMember(t *testing.T) {
 	c.SetStore(st)
 	c.RemoveMember(1)
 
-	wactions := []action{
-		{name: "Delete", params: []interface{}{memberStoreKey(1), true, true}},
-		{name: "Create", params: []interface{}{removedMemberStoreKey(1), false, "", false, store.Permanent}},
+	wactions := []testutil.Action{
+		{Name: "Delete", Params: []interface{}{memberStoreKey(1), true, true}},
+		{Name: "Create", Params: []interface{}{removedMemberStoreKey(1), false, "", false, store.Permanent}},
 	}
 	if !reflect.DeepEqual(st.Action(), wactions) {
 		t.Errorf("actions = %v, want %v", st.Action(), wactions)

+ 177 - 201
etcdserver/server_test.go

@@ -26,7 +26,6 @@ import (
 	"path"
 	"reflect"
 	"strconv"
-	"sync"
 	"testing"
 	"time"
 
@@ -49,35 +48,35 @@ func TestDoLocalAction(t *testing.T) {
 
 		wresp    Response
 		werr     error
-		wactions []action
+		wactions []testutil.Action
 	}{
 		{
 			pb.Request{Method: "GET", ID: 1, Wait: true},
-			Response{Watcher: &nopWatcher{}}, nil, []action{action{name: "Watch"}},
+			Response{Watcher: &nopWatcher{}}, nil, []testutil.Action{{Name: "Watch"}},
 		},
 		{
 			pb.Request{Method: "GET", ID: 1},
 			Response{Event: &store.Event{}}, nil,
-			[]action{
-				action{
-					name:   "Get",
-					params: []interface{}{"", false, false},
+			[]testutil.Action{
+				{
+					Name:   "Get",
+					Params: []interface{}{"", false, false},
 				},
 			},
 		},
 		{
 			pb.Request{Method: "HEAD", ID: 1},
 			Response{Event: &store.Event{}}, nil,
-			[]action{
-				action{
-					name:   "Get",
-					params: []interface{}{"", false, false},
+			[]testutil.Action{
+				{
+					Name:   "Get",
+					Params: []interface{}{"", false, false},
 				},
 			},
 		},
 		{
 			pb.Request{Method: "BADMETHOD", ID: 1},
-			Response{}, ErrUnknownMethod, []action{},
+			Response{}, ErrUnknownMethod, []testutil.Action{},
 		},
 	}
 	for i, tt := range tests {
@@ -108,25 +107,29 @@ func TestDoBadLocalAction(t *testing.T) {
 	tests := []struct {
 		req pb.Request
 
-		wactions []action
+		wactions []testutil.Action
 	}{
 		{
 			pb.Request{Method: "GET", ID: 1, Wait: true},
-			[]action{action{name: "Watch"}},
+			[]testutil.Action{{Name: "Watch"}},
 		},
 		{
 			pb.Request{Method: "GET", ID: 1},
-			[]action{action{
-				name:   "Get",
-				params: []interface{}{"", false, false},
-			}},
+			[]testutil.Action{
+				{
+					Name:   "Get",
+					Params: []interface{}{"", false, false},
+				},
+			},
 		},
 		{
 			pb.Request{Method: "HEAD", ID: 1},
-			[]action{action{
-				name:   "Get",
-				params: []interface{}{"", false, false},
-			}},
+			[]testutil.Action{
+				{
+					Name:   "Get",
+					Params: []interface{}{"", false, false},
+				},
+			},
 		},
 	}
 	for i, tt := range tests {
@@ -155,16 +158,16 @@ func TestApplyRequest(t *testing.T) {
 		req pb.Request
 
 		wresp    Response
-		wactions []action
+		wactions []testutil.Action
 	}{
 		// POST ==> Create
 		{
 			pb.Request{Method: "POST", ID: 1},
 			Response{Event: &store.Event{}},
-			[]action{
-				action{
-					name:   "Create",
-					params: []interface{}{"", false, "", true, time.Time{}},
+			[]testutil.Action{
+				{
+					Name:   "Create",
+					Params: []interface{}{"", false, "", true, time.Time{}},
 				},
 			},
 		},
@@ -172,10 +175,10 @@ func TestApplyRequest(t *testing.T) {
 		{
 			pb.Request{Method: "POST", ID: 1, Expiration: 1337},
 			Response{Event: &store.Event{}},
-			[]action{
-				action{
-					name:   "Create",
-					params: []interface{}{"", false, "", true, time.Unix(0, 1337)},
+			[]testutil.Action{
+				{
+					Name:   "Create",
+					Params: []interface{}{"", false, "", true, time.Unix(0, 1337)},
 				},
 			},
 		},
@@ -183,10 +186,10 @@ func TestApplyRequest(t *testing.T) {
 		{
 			pb.Request{Method: "POST", ID: 1, Dir: true},
 			Response{Event: &store.Event{}},
-			[]action{
-				action{
-					name:   "Create",
-					params: []interface{}{"", true, "", true, time.Time{}},
+			[]testutil.Action{
+				{
+					Name:   "Create",
+					Params: []interface{}{"", true, "", true, time.Time{}},
 				},
 			},
 		},
@@ -194,10 +197,10 @@ func TestApplyRequest(t *testing.T) {
 		{
 			pb.Request{Method: "PUT", ID: 1},
 			Response{Event: &store.Event{}},
-			[]action{
-				action{
-					name:   "Set",
-					params: []interface{}{"", false, "", time.Time{}},
+			[]testutil.Action{
+				{
+					Name:   "Set",
+					Params: []interface{}{"", false, "", time.Time{}},
 				},
 			},
 		},
@@ -205,10 +208,10 @@ func TestApplyRequest(t *testing.T) {
 		{
 			pb.Request{Method: "PUT", ID: 1, Dir: true},
 			Response{Event: &store.Event{}},
-			[]action{
-				action{
-					name:   "Set",
-					params: []interface{}{"", true, "", time.Time{}},
+			[]testutil.Action{
+				{
+					Name:   "Set",
+					Params: []interface{}{"", true, "", time.Time{}},
 				},
 			},
 		},
@@ -216,10 +219,10 @@ func TestApplyRequest(t *testing.T) {
 		{
 			pb.Request{Method: "PUT", ID: 1, PrevExist: pbutil.Boolp(true)},
 			Response{Event: &store.Event{}},
-			[]action{
-				action{
-					name:   "Update",
-					params: []interface{}{"", "", time.Time{}},
+			[]testutil.Action{
+				{
+					Name:   "Update",
+					Params: []interface{}{"", "", time.Time{}},
 				},
 			},
 		},
@@ -227,10 +230,10 @@ func TestApplyRequest(t *testing.T) {
 		{
 			pb.Request{Method: "PUT", ID: 1, PrevExist: pbutil.Boolp(false)},
 			Response{Event: &store.Event{}},
-			[]action{
-				action{
-					name:   "Create",
-					params: []interface{}{"", false, "", false, time.Time{}},
+			[]testutil.Action{
+				{
+					Name:   "Create",
+					Params: []interface{}{"", false, "", false, time.Time{}},
 				},
 			},
 		},
@@ -239,10 +242,10 @@ func TestApplyRequest(t *testing.T) {
 		{
 			pb.Request{Method: "PUT", ID: 1, PrevExist: pbutil.Boolp(true), PrevIndex: 1},
 			Response{Event: &store.Event{}},
-			[]action{
-				action{
-					name:   "Update",
-					params: []interface{}{"", "", time.Time{}},
+			[]testutil.Action{
+				{
+					Name:   "Update",
+					Params: []interface{}{"", "", time.Time{}},
 				},
 			},
 		},
@@ -251,10 +254,10 @@ func TestApplyRequest(t *testing.T) {
 		{
 			pb.Request{Method: "PUT", ID: 1, PrevExist: pbutil.Boolp(false), PrevIndex: 1},
 			Response{Event: &store.Event{}},
-			[]action{
-				action{
-					name:   "Create",
-					params: []interface{}{"", false, "", false, time.Time{}},
+			[]testutil.Action{
+				{
+					Name:   "Create",
+					Params: []interface{}{"", false, "", false, time.Time{}},
 				},
 			},
 		},
@@ -262,10 +265,10 @@ func TestApplyRequest(t *testing.T) {
 		{
 			pb.Request{Method: "PUT", ID: 1, PrevIndex: 1},
 			Response{Event: &store.Event{}},
-			[]action{
-				action{
-					name:   "CompareAndSwap",
-					params: []interface{}{"", "", uint64(1), "", time.Time{}},
+			[]testutil.Action{
+				{
+					Name:   "CompareAndSwap",
+					Params: []interface{}{"", "", uint64(1), "", time.Time{}},
 				},
 			},
 		},
@@ -273,10 +276,10 @@ func TestApplyRequest(t *testing.T) {
 		{
 			pb.Request{Method: "PUT", ID: 1, PrevValue: "bar"},
 			Response{Event: &store.Event{}},
-			[]action{
-				action{
-					name:   "CompareAndSwap",
-					params: []interface{}{"", "bar", uint64(0), "", time.Time{}},
+			[]testutil.Action{
+				{
+					Name:   "CompareAndSwap",
+					Params: []interface{}{"", "bar", uint64(0), "", time.Time{}},
 				},
 			},
 		},
@@ -284,10 +287,10 @@ func TestApplyRequest(t *testing.T) {
 		{
 			pb.Request{Method: "PUT", ID: 1, PrevIndex: 1, PrevValue: "bar"},
 			Response{Event: &store.Event{}},
-			[]action{
-				action{
-					name:   "CompareAndSwap",
-					params: []interface{}{"", "bar", uint64(1), "", time.Time{}},
+			[]testutil.Action{
+				{
+					Name:   "CompareAndSwap",
+					Params: []interface{}{"", "bar", uint64(1), "", time.Time{}},
 				},
 			},
 		},
@@ -295,10 +298,10 @@ func TestApplyRequest(t *testing.T) {
 		{
 			pb.Request{Method: "DELETE", ID: 1},
 			Response{Event: &store.Event{}},
-			[]action{
-				action{
-					name:   "Delete",
-					params: []interface{}{"", false, false},
+			[]testutil.Action{
+				{
+					Name:   "Delete",
+					Params: []interface{}{"", false, false},
 				},
 			},
 		},
@@ -306,10 +309,10 @@ func TestApplyRequest(t *testing.T) {
 		{
 			pb.Request{Method: "DELETE", ID: 1, PrevIndex: 1},
 			Response{Event: &store.Event{}},
-			[]action{
-				action{
-					name:   "CompareAndDelete",
-					params: []interface{}{"", "", uint64(1)},
+			[]testutil.Action{
+				{
+					Name:   "CompareAndDelete",
+					Params: []interface{}{"", "", uint64(1)},
 				},
 			},
 		},
@@ -317,10 +320,10 @@ func TestApplyRequest(t *testing.T) {
 		{
 			pb.Request{Method: "DELETE", ID: 1, PrevValue: "bar"},
 			Response{Event: &store.Event{}},
-			[]action{
-				action{
-					name:   "CompareAndDelete",
-					params: []interface{}{"", "bar", uint64(0)},
+			[]testutil.Action{
+				{
+					Name:   "CompareAndDelete",
+					Params: []interface{}{"", "bar", uint64(0)},
 				},
 			},
 		},
@@ -328,10 +331,10 @@ func TestApplyRequest(t *testing.T) {
 		{
 			pb.Request{Method: "DELETE", ID: 1, PrevIndex: 5, PrevValue: "bar"},
 			Response{Event: &store.Event{}},
-			[]action{
-				action{
-					name:   "CompareAndDelete",
-					params: []interface{}{"", "bar", uint64(5)},
+			[]testutil.Action{
+				{
+					Name:   "CompareAndDelete",
+					Params: []interface{}{"", "bar", uint64(5)},
 				},
 			},
 		},
@@ -339,10 +342,10 @@ func TestApplyRequest(t *testing.T) {
 		{
 			pb.Request{Method: "QGET", ID: 1},
 			Response{Event: &store.Event{}},
-			[]action{
-				action{
-					name:   "Get",
-					params: []interface{}{"", false, false},
+			[]testutil.Action{
+				{
+					Name:   "Get",
+					Params: []interface{}{"", false, false},
 				},
 			},
 		},
@@ -350,20 +353,20 @@ func TestApplyRequest(t *testing.T) {
 		{
 			pb.Request{Method: "SYNC", ID: 1},
 			Response{},
-			[]action{
-				action{
-					name:   "DeleteExpiredKeys",
-					params: []interface{}{time.Unix(0, 0)},
+			[]testutil.Action{
+				{
+					Name:   "DeleteExpiredKeys",
+					Params: []interface{}{time.Unix(0, 0)},
 				},
 			},
 		},
 		{
 			pb.Request{Method: "SYNC", ID: 1, Time: 12345},
 			Response{},
-			[]action{
-				action{
-					name:   "DeleteExpiredKeys",
-					params: []interface{}{time.Unix(0, 12345)},
+			[]testutil.Action{
+				{
+					Name:   "DeleteExpiredKeys",
+					Params: []interface{}{time.Unix(0, 12345)},
 				},
 			},
 		},
@@ -371,7 +374,7 @@ func TestApplyRequest(t *testing.T) {
 		{
 			pb.Request{Method: "BADMETHOD", ID: 1},
 			Response{err: ErrUnknownMethod},
-			[]action{},
+			[]testutil.Action{},
 		},
 	}
 
@@ -461,10 +464,10 @@ func TestApplyConfChangeError(t *testing.T) {
 			t.Errorf("#%d: applyConfChange error = %v, want %v", i, err, tt.werr)
 		}
 		cc := raftpb.ConfChange{Type: tt.cc.Type, NodeID: raft.None}
-		w := []action{
+		w := []testutil.Action{
 			{
-				name:   "ApplyConfChange",
-				params: []interface{}{cc},
+				Name:   "ApplyConfChange",
+				Params: []interface{}{cc},
 			},
 		}
 		if g := n.Action(); !reflect.DeepEqual(g, w) {
@@ -558,7 +561,7 @@ func TestDoProposalCancelled(t *testing.T) {
 	if err != ErrCanceled {
 		t.Fatalf("err = %v, want %v", err, ErrCanceled)
 	}
-	w := []action{action{name: "Register"}, action{name: "Trigger"}}
+	w := []testutil.Action{{Name: "Register"}, {Name: "Trigger"}}
 	if !reflect.DeepEqual(wait.action, w) {
 		t.Errorf("wait.action = %+v, want %+v", wait.action, w)
 	}
@@ -610,10 +613,10 @@ func TestSync(t *testing.T) {
 	if len(action) != 1 {
 		t.Fatalf("len(action) = %d, want 1", len(action))
 	}
-	if action[0].name != "Propose" {
-		t.Fatalf("action = %s, want Propose", action[0].name)
+	if action[0].Name != "Propose" {
+		t.Fatalf("action = %s, want Propose", action[0].Name)
 	}
-	data := action[0].params[0].([]byte)
+	data := action[0].Params[0].([]byte)
 	var r pb.Request
 	if err := r.Unmarshal(data); err != nil {
 		t.Fatalf("unmarshal request error: %v", err)
@@ -640,7 +643,7 @@ func TestSyncTimeout(t *testing.T) {
 
 	// give time for goroutine in sync to cancel
 	testutil.ForceGosched()
-	w := []action{action{name: "Propose blocked"}}
+	w := []testutil.Action{{Name: "Propose blocked"}}
 	if g := n.Action(); !reflect.DeepEqual(g, w) {
 		t.Errorf("action = %v, want %v", g, w)
 	}
@@ -677,10 +680,10 @@ func TestSyncTrigger(t *testing.T) {
 	if len(action) != 1 {
 		t.Fatalf("len(action) = %d, want 1", len(action))
 	}
-	if action[0].name != "Propose" {
-		t.Fatalf("action = %s, want Propose", action[0].name)
+	if action[0].Name != "Propose" {
+		t.Fatalf("action = %s, want Propose", action[0].Name)
 	}
-	data := action[0].params[0].([]byte)
+	data := action[0].Params[0].([]byte)
 	var req pb.Request
 	if err := req.Unmarshal(data); err != nil {
 		t.Fatalf("error unmarshalling data: %v", err)
@@ -707,17 +710,17 @@ func TestSnapshot(t *testing.T) {
 	if len(gaction) != 1 {
 		t.Fatalf("len(action) = %d, want 1", len(gaction))
 	}
-	if !reflect.DeepEqual(gaction[0], action{name: "Save"}) {
+	if !reflect.DeepEqual(gaction[0], testutil.Action{Name: "Save"}) {
 		t.Errorf("action = %s, want Save", gaction[0])
 	}
 	gaction = p.Action()
 	if len(gaction) != 2 {
 		t.Fatalf("len(action) = %d, want 2", len(gaction))
 	}
-	if !reflect.DeepEqual(gaction[0], action{name: "Cut"}) {
+	if !reflect.DeepEqual(gaction[0], testutil.Action{Name: "Cut"}) {
 		t.Errorf("action = %s, want Cut", gaction[0])
 	}
-	if !reflect.DeepEqual(gaction[1], action{name: "SaveSnap"}) {
+	if !reflect.DeepEqual(gaction[1], testutil.Action{Name: "SaveSnap"}) {
 		t.Errorf("action = %s, want SaveSnap", gaction[1])
 	}
 }
@@ -749,7 +752,7 @@ func TestTriggerSnap(t *testing.T) {
 	if len(gaction) != wcnt {
 		t.Fatalf("len(action) = %d, want %d", len(gaction), wcnt)
 	}
-	if !reflect.DeepEqual(gaction[wcnt-1], action{name: "SaveSnap"}) {
+	if !reflect.DeepEqual(gaction[wcnt-1], testutil.Action{Name: "SaveSnap"}) {
 		t.Errorf("action = %s, want SaveSnap", gaction[wcnt-1])
 	}
 }
@@ -777,11 +780,11 @@ func TestRecvSnapshot(t *testing.T) {
 	testutil.ForceGosched()
 	s.Stop()
 
-	wactions := []action{action{name: "Recovery"}}
+	wactions := []testutil.Action{{Name: "Recovery"}}
 	if g := st.Action(); !reflect.DeepEqual(g, wactions) {
 		t.Errorf("store action = %v, want %v", g, wactions)
 	}
-	wactions = []action{action{name: "SaveSnap"}, action{name: "Save"}}
+	wactions = []testutil.Action{{Name: "SaveSnap"}, {Name: "Save"}}
 	if g := p.Action(); !reflect.DeepEqual(g, wactions) {
 		t.Errorf("storage action = %v, want %v", g, wactions)
 	}
@@ -853,11 +856,11 @@ func TestApplySnapshotAndCommittedEntries(t *testing.T) {
 	if len(actions) != 2 {
 		t.Fatalf("len(action) = %d, want 2", len(actions))
 	}
-	if actions[0].name != "Recovery" {
-		t.Errorf("actions[0] = %s, want %s", actions[0].name, "Recovery")
+	if actions[0].Name != "Recovery" {
+		t.Errorf("actions[0] = %s, want %s", actions[0].Name, "Recovery")
 	}
-	if actions[1].name != "Get" {
-		t.Errorf("actions[1] = %s, want %s", actions[1].name, "Get")
+	if actions[1].Name != "Get" {
+		t.Errorf("actions[1] = %s, want %s", actions[1].Name, "Get")
 	}
 }
 
@@ -887,7 +890,7 @@ func TestAddMember(t *testing.T) {
 	if err != nil {
 		t.Fatalf("AddMember error: %v", err)
 	}
-	wactions := []action{action{name: "ProposeConfChange:ConfChangeAddNode"}, action{name: "ApplyConfChange:ConfChangeAddNode"}}
+	wactions := []testutil.Action{{Name: "ProposeConfChange:ConfChangeAddNode"}, {Name: "ApplyConfChange:ConfChangeAddNode"}}
 	if !reflect.DeepEqual(gaction, wactions) {
 		t.Errorf("action = %v, want %v", gaction, wactions)
 	}
@@ -922,7 +925,7 @@ func TestRemoveMember(t *testing.T) {
 	if err != nil {
 		t.Fatalf("RemoveMember error: %v", err)
 	}
-	wactions := []action{action{name: "ProposeConfChange:ConfChangeRemoveNode"}, action{name: "ApplyConfChange:ConfChangeRemoveNode"}}
+	wactions := []testutil.Action{{Name: "ProposeConfChange:ConfChangeRemoveNode"}, {Name: "ApplyConfChange:ConfChangeRemoveNode"}}
 	if !reflect.DeepEqual(gaction, wactions) {
 		t.Errorf("action = %v, want %v", gaction, wactions)
 	}
@@ -958,7 +961,7 @@ func TestUpdateMember(t *testing.T) {
 	if err != nil {
 		t.Fatalf("UpdateMember error: %v", err)
 	}
-	wactions := []action{action{name: "ProposeConfChange:ConfChangeUpdateNode"}, action{name: "ApplyConfChange:ConfChangeUpdateNode"}}
+	wactions := []testutil.Action{{Name: "ProposeConfChange:ConfChangeUpdateNode"}, {Name: "ApplyConfChange:ConfChangeUpdateNode"}}
 	if !reflect.DeepEqual(gaction, wactions) {
 		t.Errorf("action = %v, want %v", gaction, wactions)
 	}
@@ -989,10 +992,10 @@ func TestPublish(t *testing.T) {
 	if len(action) != 1 {
 		t.Fatalf("len(action) = %d, want 1", len(action))
 	}
-	if action[0].name != "Propose" {
-		t.Fatalf("action = %s, want Propose", action[0].name)
+	if action[0].Name != "Propose" {
+		t.Fatalf("action = %s, want Propose", action[0].Name)
 	}
-	data := action[0].params[0].([]byte)
+	data := action[0].Params[0].([]byte)
 	var r pb.Request
 	if err := r.Unmarshal(data); err != nil {
 		t.Fatalf("unmarshal request error: %v", err)
@@ -1114,102 +1117,79 @@ func TestGetOtherPeerURLs(t *testing.T) {
 	}
 }
 
-type action struct {
-	name   string
-	params []interface{}
-}
-
-type recorder struct {
-	sync.Mutex
-	actions []action
-}
-
-func (r *recorder) record(a action) {
-	r.Lock()
-	r.actions = append(r.actions, a)
-	r.Unlock()
-}
-func (r *recorder) Action() []action {
-	r.Lock()
-	cpy := make([]action, len(r.actions))
-	copy(cpy, r.actions)
-	r.Unlock()
-	return cpy
-}
-
 // storeRecorder records all the methods it receives.
 // storeRecorder DOES NOT work as a actual store.
 // It always returns invaild empty response and no error.
-type storeRecorder struct{ recorder }
+type storeRecorder struct{ testutil.Recorder }
 
 func (s *storeRecorder) Version() int  { return 0 }
 func (s *storeRecorder) Index() uint64 { return 0 }
 func (s *storeRecorder) Get(path string, recursive, sorted bool) (*store.Event, error) {
-	s.record(action{
-		name:   "Get",
-		params: []interface{}{path, recursive, sorted},
+	s.Record(testutil.Action{
+		Name:   "Get",
+		Params: []interface{}{path, recursive, sorted},
 	})
 	return &store.Event{}, nil
 }
 func (s *storeRecorder) Set(path string, dir bool, val string, expr time.Time) (*store.Event, error) {
-	s.record(action{
-		name:   "Set",
-		params: []interface{}{path, dir, val, expr},
+	s.Record(testutil.Action{
+		Name:   "Set",
+		Params: []interface{}{path, dir, val, expr},
 	})
 	return &store.Event{}, nil
 }
 func (s *storeRecorder) Update(path, val string, expr time.Time) (*store.Event, error) {
-	s.record(action{
-		name:   "Update",
-		params: []interface{}{path, val, expr},
+	s.Record(testutil.Action{
+		Name:   "Update",
+		Params: []interface{}{path, val, expr},
 	})
 	return &store.Event{}, nil
 }
 func (s *storeRecorder) Create(path string, dir bool, val string, uniq bool, exp time.Time) (*store.Event, error) {
-	s.record(action{
-		name:   "Create",
-		params: []interface{}{path, dir, val, uniq, exp},
+	s.Record(testutil.Action{
+		Name:   "Create",
+		Params: []interface{}{path, dir, val, uniq, exp},
 	})
 	return &store.Event{}, nil
 }
 func (s *storeRecorder) CompareAndSwap(path, prevVal string, prevIdx uint64, val string, expr time.Time) (*store.Event, error) {
-	s.record(action{
-		name:   "CompareAndSwap",
-		params: []interface{}{path, prevVal, prevIdx, val, expr},
+	s.Record(testutil.Action{
+		Name:   "CompareAndSwap",
+		Params: []interface{}{path, prevVal, prevIdx, val, expr},
 	})
 	return &store.Event{}, nil
 }
 func (s *storeRecorder) Delete(path string, dir, recursive bool) (*store.Event, error) {
-	s.record(action{
-		name:   "Delete",
-		params: []interface{}{path, dir, recursive},
+	s.Record(testutil.Action{
+		Name:   "Delete",
+		Params: []interface{}{path, dir, recursive},
 	})
 	return &store.Event{}, nil
 }
 func (s *storeRecorder) CompareAndDelete(path, prevVal string, prevIdx uint64) (*store.Event, error) {
-	s.record(action{
-		name:   "CompareAndDelete",
-		params: []interface{}{path, prevVal, prevIdx},
+	s.Record(testutil.Action{
+		Name:   "CompareAndDelete",
+		Params: []interface{}{path, prevVal, prevIdx},
 	})
 	return &store.Event{}, nil
 }
 func (s *storeRecorder) Watch(_ string, _, _ bool, _ uint64) (store.Watcher, error) {
-	s.record(action{name: "Watch"})
+	s.Record(testutil.Action{Name: "Watch"})
 	return &nopWatcher{}, nil
 }
 func (s *storeRecorder) Save() ([]byte, error) {
-	s.record(action{name: "Save"})
+	s.Record(testutil.Action{Name: "Save"})
 	return nil, nil
 }
 func (s *storeRecorder) Recovery(b []byte) error {
-	s.record(action{name: "Recovery"})
+	s.Record(testutil.Action{Name: "Recovery"})
 	return nil
 }
 func (s *storeRecorder) JsonStats() []byte { return nil }
 func (s *storeRecorder) DeleteExpiredKeys(cutoff time.Time) {
-	s.record(action{
-		name:   "DeleteExpiredKeys",
-		params: []interface{}{cutoff},
+	s.Record(testutil.Action{
+		Name:   "DeleteExpiredKeys",
+		Params: []interface{}{cutoff},
 	})
 }
 
@@ -1236,15 +1216,15 @@ func (s *errStoreRecorder) Watch(path string, recursive, sorted bool, index uint
 }
 
 type waitRecorder struct {
-	action []action
+	action []testutil.Action
 }
 
 func (w *waitRecorder) Register(id uint64) <-chan interface{} {
-	w.action = append(w.action, action{name: "Register"})
+	w.action = append(w.action, testutil.Action{Name: "Register"})
 	return nil
 }
 func (w *waitRecorder) Trigger(id uint64, x interface{}) {
-	w.action = append(w.action, action{name: "Trigger"})
+	w.action = append(w.action, testutil.Action{Name: "Trigger"})
 }
 
 type waitWithResponse struct {
@@ -1256,58 +1236,54 @@ func (w *waitWithResponse) Register(id uint64) <-chan interface{} {
 }
 func (w *waitWithResponse) Trigger(id uint64, x interface{}) {}
 
-type storageRecorder struct {
-	recorder
-}
+type storageRecorder struct{ testutil.Recorder }
 
 func (p *storageRecorder) Save(st raftpb.HardState, ents []raftpb.Entry) error {
-	p.record(action{name: "Save"})
+	p.Record(testutil.Action{Name: "Save"})
 	return nil
 }
 func (p *storageRecorder) Cut() error {
-	p.record(action{name: "Cut"})
+	p.Record(testutil.Action{Name: "Cut"})
 	return nil
 }
 func (p *storageRecorder) SaveSnap(st raftpb.Snapshot) error {
 	if !raft.IsEmptySnap(st) {
-		p.record(action{name: "SaveSnap"})
+		p.Record(testutil.Action{Name: "SaveSnap"})
 	}
 	return nil
 }
 func (p *storageRecorder) Close() error { return nil }
 
-type nodeRecorder struct {
-	recorder
-}
+type nodeRecorder struct{ testutil.Recorder }
 
-func (n *nodeRecorder) Tick() { n.record(action{name: "Tick"}) }
+func (n *nodeRecorder) Tick() { n.Record(testutil.Action{Name: "Tick"}) }
 func (n *nodeRecorder) Campaign(ctx context.Context) error {
-	n.record(action{name: "Campaign"})
+	n.Record(testutil.Action{Name: "Campaign"})
 	return nil
 }
 func (n *nodeRecorder) Propose(ctx context.Context, data []byte) error {
-	n.record(action{name: "Propose", params: []interface{}{data}})
+	n.Record(testutil.Action{Name: "Propose", Params: []interface{}{data}})
 	return nil
 }
 func (n *nodeRecorder) ProposeConfChange(ctx context.Context, conf raftpb.ConfChange) error {
-	n.record(action{name: "ProposeConfChange"})
+	n.Record(testutil.Action{Name: "ProposeConfChange"})
 	return nil
 }
 func (n *nodeRecorder) Step(ctx context.Context, msg raftpb.Message) error {
-	n.record(action{name: "Step"})
+	n.Record(testutil.Action{Name: "Step"})
 	return nil
 }
 func (n *nodeRecorder) Ready() <-chan raft.Ready { return nil }
 func (n *nodeRecorder) Advance()                 {}
 func (n *nodeRecorder) ApplyConfChange(conf raftpb.ConfChange) *raftpb.ConfState {
-	n.record(action{name: "ApplyConfChange", params: []interface{}{conf}})
+	n.Record(testutil.Action{Name: "ApplyConfChange", Params: []interface{}{conf}})
 	return &raftpb.ConfState{}
 }
 func (n *nodeRecorder) Stop() {
-	n.record(action{name: "Stop"})
+	n.Record(testutil.Action{Name: "Stop"})
 }
 func (n *nodeRecorder) Compact(index uint64, nodes []uint64, d []byte) {
-	n.record(action{name: "Compact"})
+	n.Record(testutil.Action{Name: "Compact"})
 }
 
 type nodeProposalBlockerRecorder struct {
@@ -1316,7 +1292,7 @@ type nodeProposalBlockerRecorder struct {
 
 func (n *nodeProposalBlockerRecorder) Propose(ctx context.Context, data []byte) error {
 	<-ctx.Done()
-	n.record(action{name: "Propose blocked"})
+	n.Record(testutil.Action{Name: "Propose blocked"})
 	return nil
 }
 
@@ -1337,14 +1313,14 @@ func (n *nodeConfChangeCommitterRecorder) ProposeConfChange(ctx context.Context,
 	}
 	n.index++
 	n.readyc <- raft.Ready{CommittedEntries: []raftpb.Entry{{Index: n.index, Type: raftpb.EntryConfChange, Data: data}}}
-	n.record(action{name: "ProposeConfChange:" + conf.Type.String()})
+	n.Record(testutil.Action{Name: "ProposeConfChange:" + conf.Type.String()})
 	return nil
 }
 func (n *nodeConfChangeCommitterRecorder) Ready() <-chan raft.Ready {
 	return n.readyc
 }
 func (n *nodeConfChangeCommitterRecorder) ApplyConfChange(conf raftpb.ConfChange) *raftpb.ConfState {
-	n.record(action{name: "ApplyConfChange:" + conf.Type.String()})
+	n.Record(testutil.Action{Name: "ApplyConfChange:" + conf.Type.String()})
 	return &raftpb.ConfState{}
 }
 

+ 42 - 0
pkg/testutil/recorder.go

@@ -0,0 +1,42 @@
+/*
+   Copyright 2015 CoreOS, Inc.
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+*/
+
+package testutil
+
+import "sync"
+
+type Action struct {
+	Name   string
+	Params []interface{}
+}
+
+type Recorder struct {
+	sync.Mutex
+	actions []Action
+}
+
+func (r *Recorder) Record(a Action) {
+	r.Lock()
+	r.actions = append(r.actions, a)
+	r.Unlock()
+}
+func (r *Recorder) Action() []Action {
+	r.Lock()
+	cpy := make([]Action, len(r.actions))
+	copy(cpy, r.actions)
+	r.Unlock()
+	return cpy
+}