Merge pull request #15059 from serathius/linearizability-operations

tests: Rewrite etcd requests to use operations
dependabot/go_modules/go.uber.org/atomic-1.10.0
Marek Siarkowicz 2023-01-09 15:12:16 +01:00 committed by GitHub
commit a992fb5e92
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 447 additions and 275 deletions

View File

@ -46,17 +46,21 @@ func (h *appendableHistory) AppendGet(key string, start, end time.Time, resp *cl
if len(resp.Kvs) == 1 {
readData = string(resp.Kvs[0].Value)
}
var revision int64
if resp != nil && resp.Header != nil {
revision = resp.Header.Revision
}
h.successful = append(h.successful, porcupine.Operation{
ClientId: h.id,
Input: EtcdRequest{Op: Get, Key: key},
Input: getRequest(key),
Call: start.UnixNano(),
Output: EtcdResponse{GetData: readData, Revision: resp.Header.Revision},
Output: getResponse(readData, revision),
Return: end.UnixNano(),
})
}
func (h *appendableHistory) AppendPut(key, value string, start, end time.Time, resp *clientv3.PutResponse, err error) {
request := EtcdRequest{Op: Put, Key: key, PutData: value}
request := putRequest(key, value)
if err != nil {
h.appendFailed(request, start, err)
return
@ -67,15 +71,15 @@ func (h *appendableHistory) AppendPut(key, value string, start, end time.Time, r
}
h.successful = append(h.successful, porcupine.Operation{
ClientId: h.id,
Input: EtcdRequest{Op: Put, Key: key, PutData: value},
Input: request,
Call: start.UnixNano(),
Output: EtcdResponse{Err: err, Revision: revision},
Output: putResponse(revision),
Return: end.UnixNano(),
})
}
func (h *appendableHistory) AppendDelete(key string, start, end time.Time, resp *clientv3.DeleteResponse, err error) {
request := EtcdRequest{Op: Delete, Key: key}
request := deleteRequest(key)
if err != nil {
h.appendFailed(request, start, err)
return
@ -90,13 +94,13 @@ func (h *appendableHistory) AppendDelete(key string, start, end time.Time, resp
ClientId: h.id,
Input: request,
Call: start.UnixNano(),
Output: EtcdResponse{Revision: revision, Deleted: deleted, Err: err},
Output: deleteResponse(deleted, revision),
Return: end.UnixNano(),
})
}
func (h *appendableHistory) AppendTxn(key, expectValue, newValue string, start, end time.Time, resp *clientv3.TxnResponse, err error) {
request := EtcdRequest{Op: Txn, Key: key, TxnExpectData: expectValue, TxnNewData: newValue}
request := txnRequest(key, expectValue, newValue)
if err != nil {
h.appendFailed(request, start, err)
return
@ -109,7 +113,7 @@ func (h *appendableHistory) AppendTxn(key, expectValue, newValue string, start,
ClientId: h.id,
Input: request,
Call: start.UnixNano(),
Output: EtcdResponse{Err: err, Revision: revision, TxnSucceeded: resp.Succeeded},
Output: txnResponse(resp.Succeeded, revision),
Return: end.UnixNano(),
})
}
@ -119,7 +123,7 @@ func (h *appendableHistory) appendFailed(request EtcdRequest, start time.Time, e
ClientId: h.id,
Input: request,
Call: start.UnixNano(),
Output: EtcdResponse{Err: err},
Output: failedResponse(err),
Return: 0, // For failed writes we don't know when request has really finished.
})
// Operations of single client needs to be sequential.
@ -127,6 +131,46 @@ func (h *appendableHistory) appendFailed(request EtcdRequest, start time.Time, e
h.id = h.idProvider.ClientId()
}
func getRequest(key string) EtcdRequest {
return EtcdRequest{Ops: []EtcdOperation{{Type: Get, Key: key}}}
}
func getResponse(value string, revision int64) EtcdResponse {
return EtcdResponse{Result: []EtcdOperationResult{{Value: value}}, Revision: revision}
}
func failedResponse(err error) EtcdResponse {
return EtcdResponse{Err: err}
}
func putRequest(key, value string) EtcdRequest {
return EtcdRequest{Ops: []EtcdOperation{{Type: Put, Key: key, Value: value}}}
}
func putResponse(revision int64) EtcdResponse {
return EtcdResponse{Result: []EtcdOperationResult{{}}, Revision: revision}
}
func deleteRequest(key string) EtcdRequest {
return EtcdRequest{Ops: []EtcdOperation{{Type: Delete, Key: key}}}
}
func deleteResponse(deleted int64, revision int64) EtcdResponse {
return EtcdResponse{Result: []EtcdOperationResult{{Deleted: deleted}}, Revision: revision}
}
func txnRequest(key, expectValue, newValue string) EtcdRequest {
return EtcdRequest{Conds: []EtcdCondition{{Key: key, ExpectedValue: expectValue}}, Ops: []EtcdOperation{{Type: Put, Key: key, Value: newValue}}}
}
func txnResponse(succeeded bool, revision int64) EtcdResponse {
var result []EtcdOperationResult
if succeeded {
result = []EtcdOperationResult{{}}
}
return EtcdResponse{Result: result, TxnFailure: !succeeded, Revision: revision}
}
type history struct {
successful []porcupine.Operation
// failed requests are kept separate as we don't know return time of failed operations.

View File

@ -17,33 +17,47 @@ package linearizability
import (
"encoding/json"
"fmt"
"reflect"
"strings"
"github.com/anishathalye/porcupine"
)
type Operation string
type OperationType string
const (
Get Operation = "get"
Put Operation = "put"
Delete Operation = "delete"
Txn Operation = "txn"
Get OperationType = "get"
Put OperationType = "put"
Delete OperationType = "delete"
Txn OperationType = "txn"
)
type EtcdRequest struct {
Op Operation
Conds []EtcdCondition
Ops []EtcdOperation
}
type EtcdCondition struct {
Key string
PutData string
TxnExpectData string
TxnNewData string
ExpectedValue string
}
type EtcdOperation struct {
Type OperationType
Key string
Value string
}
type EtcdResponse struct {
GetData string
Revision int64
Deleted int64
TxnSucceeded bool
Err error
Err error
Revision int64
TxnFailure bool
Result []EtcdOperationResult
}
type EtcdOperationResult struct {
Value string
Deleted int64
}
type PossibleStates []EtcdState
@ -71,39 +85,83 @@ var etcdModel = porcupine.Model{
return ok, string(data)
},
DescribeOperation: func(in, out interface{}) string {
request := in.(EtcdRequest)
response := out.(EtcdResponse)
switch request.Op {
case Get:
if response.Err != nil {
return fmt.Sprintf("get(%q) -> %q", request.Key, response.Err)
} else {
return fmt.Sprintf("get(%q) -> %q, rev: %d", request.Key, response.GetData, response.Revision)
}
case Put:
if response.Err != nil {
return fmt.Sprintf("put(%q, %q) -> %s", request.Key, request.PutData, response.Err)
} else {
return fmt.Sprintf("put(%q, %q) -> ok, rev: %d", request.Key, request.PutData, response.Revision)
}
case Delete:
if response.Err != nil {
return fmt.Sprintf("delete(%q) -> %s", request.Key, response.Err)
} else {
return fmt.Sprintf("delete(%q) -> ok, rev: %d deleted:%d", request.Key, response.Revision, response.Deleted)
}
case Txn:
if response.Err != nil {
return fmt.Sprintf("txn(if(value(%q)=%q).then(put(%q, %q)) -> %s", request.Key, request.TxnExpectData, request.Key, request.TxnNewData, response.Err)
} else {
return fmt.Sprintf("txn(if(value(%q)=%q).then(put(%q, %q)) -> %v, rev: %d", request.Key, request.TxnExpectData, request.Key, request.TxnNewData, response.TxnSucceeded, response.Revision)
}
default:
return "<invalid>"
}
return describeEtcdRequestResponse(in.(EtcdRequest), out.(EtcdResponse))
},
}
func describeEtcdRequestResponse(request EtcdRequest, response EtcdResponse) string {
prefix := describeEtcdOperations(request.Ops)
if len(request.Conds) != 0 {
prefix = fmt.Sprintf("if(%s).then(%s)", describeEtcdConditions(request.Conds), prefix)
}
return fmt.Sprintf("%s -> %s", prefix, describeEtcdResponse(request.Ops, response))
}
func describeEtcdConditions(conds []EtcdCondition) string {
opsDescription := make([]string, len(conds))
for i := range conds {
opsDescription[i] = fmt.Sprintf("%s==%q", conds[i].Key, conds[i].ExpectedValue)
}
return strings.Join(opsDescription, " && ")
}
func describeEtcdOperations(ops []EtcdOperation) string {
opsDescription := make([]string, len(ops))
for i := range ops {
opsDescription[i] = describeEtcdOperation(ops[i])
}
return strings.Join(opsDescription, ", ")
}
func describeEtcdResponse(ops []EtcdOperation, response EtcdResponse) string {
if response.Err != nil {
return fmt.Sprintf("err: %q", response.Err)
}
if response.TxnFailure {
return fmt.Sprintf("txn failed, rev: %d", response.Revision)
}
respDescription := make([]string, len(response.Result))
for i := range response.Result {
respDescription[i] = describeEtcdOperationResponse(ops[i].Type, response.Result[i])
}
respDescription = append(respDescription, fmt.Sprintf("rev: %d", response.Revision))
return strings.Join(respDescription, ", ")
}
func describeEtcdOperation(op EtcdOperation) string {
switch op.Type {
case Get:
return fmt.Sprintf("get(%q)", op.Key)
case Put:
return fmt.Sprintf("put(%q, %q)", op.Key, op.Value)
case Delete:
return fmt.Sprintf("delete(%q)", op.Key)
case Txn:
return "<! unsupported: nested transaction !>"
default:
return fmt.Sprintf("<! unknown op: %q !>", op.Type)
}
}
func describeEtcdOperationResponse(op OperationType, resp EtcdOperationResult) string {
switch op {
case Get:
if resp.Value == "" {
return "nil"
}
return fmt.Sprintf("%q", resp.Value)
case Put:
return fmt.Sprintf("ok")
case Delete:
return fmt.Sprintf("deleted: %d", resp.Deleted)
case Txn:
return "<! unsupported: nested transaction !>"
default:
return fmt.Sprintf("<! unknown op: %q !>", op)
}
}
func step(states PossibleStates, request EtcdRequest, response EtcdResponse) (bool, PossibleStates) {
if len(states) == 0 {
// states were not initialized
@ -126,20 +184,22 @@ func initState(request EtcdRequest, response EtcdResponse) EtcdState {
Revision: response.Revision,
KeyValues: map[string]string{},
}
switch request.Op {
case Get:
if response.GetData != "" {
state.KeyValues[request.Key] = response.GetData
if response.TxnFailure {
return state
}
for i, op := range request.Ops {
opResp := response.Result[i]
switch op.Type {
case Get:
if opResp.Value != "" {
state.KeyValues[op.Key] = opResp.Value
}
case Put:
state.KeyValues[op.Key] = op.Value
case Delete:
default:
panic("Unknown operation")
}
case Put:
state.KeyValues[request.Key] = request.PutData
case Delete:
case Txn:
if response.TxnSucceeded {
state.KeyValues[request.Key] = request.TxnNewData
}
default:
panic("Unknown operation")
}
return state
}
@ -158,7 +218,7 @@ func applyRequest(states PossibleStates, request EtcdRequest, response EtcdRespo
newStates := make(PossibleStates, 0, len(states))
for _, s := range states {
newState, expectResponse := applyRequestToSingleState(s, request)
if expectResponse == response {
if reflect.DeepEqual(expectResponse, response) {
newStates = append(newStates, newState)
}
}
@ -167,33 +227,42 @@ func applyRequest(states PossibleStates, request EtcdRequest, response EtcdRespo
// applyRequestToSingleState handles a successful request, returning updated state and response it would generate.
func applyRequestToSingleState(s EtcdState, request EtcdRequest) (EtcdState, EtcdResponse) {
success := true
for _, cond := range request.Conds {
if val := s.KeyValues[cond.Key]; val != cond.ExpectedValue {
success = false
break
}
}
if !success {
return s, EtcdResponse{Revision: s.Revision, TxnFailure: true}
}
newKVs := map[string]string{}
for k, v := range s.KeyValues {
newKVs[k] = v
}
s.KeyValues = newKVs
resp := EtcdResponse{}
switch request.Op {
case Get:
resp.GetData = s.KeyValues[request.Key]
case Put:
s.KeyValues[request.Key] = request.PutData
s.Revision += 1
case Delete:
if _, ok := s.KeyValues[request.Key]; ok {
delete(s.KeyValues, request.Key)
s.Revision += 1
resp.Deleted = 1
opResp := make([]EtcdOperationResult, len(request.Ops))
increaseRevision := false
for i, op := range request.Ops {
switch op.Type {
case Get:
opResp[i].Value = s.KeyValues[op.Key]
case Put:
s.KeyValues[op.Key] = op.Value
increaseRevision = true
case Delete:
if _, ok := s.KeyValues[op.Key]; ok {
delete(s.KeyValues, op.Key)
increaseRevision = true
opResp[i].Deleted = 1
}
default:
panic("unsupported operation")
}
case Txn:
if val := s.KeyValues[request.Key]; val == request.TxnExpectData {
s.KeyValues[request.Key] = request.TxnNewData
s.Revision += 1
resp.TxnSucceeded = true
}
default:
panic("unsupported operation")
}
resp.Revision = s.Revision
return s, resp
if increaseRevision {
s.Revision += 1
}
return s, EtcdResponse{Result: opResp, Revision: s.Revision}
}

View File

@ -17,9 +17,11 @@ package linearizability
import (
"errors"
"testing"
"github.com/stretchr/testify/assert"
)
func TestModel(t *testing.T) {
func TestModelStep(t *testing.T) {
tcs := []struct {
name string
operations []testOperation
@ -27,89 +29,89 @@ func TestModel(t *testing.T) {
{
name: "First Get can start from non-empty value and non-zero revision",
operations: []testOperation{
{req: EtcdRequest{Op: Get, Key: "key"}, resp: EtcdResponse{GetData: "2", Revision: 42}},
{req: getRequest("key"), resp: getResponse("", 42)},
},
},
{
name: "First Put can start from non-zero revision",
operations: []testOperation{
{req: EtcdRequest{Op: Put, Key: "key", PutData: "2"}, resp: EtcdResponse{Revision: 42}},
{req: putRequest("key", "1"), resp: putResponse(42)},
},
},
{
name: "First delete can start from non-zero revision",
operations: []testOperation{
{req: EtcdRequest{Op: Delete, Key: "key"}, resp: EtcdResponse{Revision: 42}},
{req: deleteRequest("key"), resp: deleteResponse(0, 42)},
},
},
{
name: "First Txn can start from non-zero revision",
operations: []testOperation{
{req: EtcdRequest{Op: Txn, Key: "key", TxnExpectData: "", TxnNewData: "42"}, resp: EtcdResponse{Revision: 42}},
{req: txnRequest("key", "", "42"), resp: txnResponse(false, 42)},
},
},
{
name: "Get response data should match put",
operations: []testOperation{
{req: EtcdRequest{Op: Put, Key: "key1", PutData: "11"}, resp: EtcdResponse{Revision: 1}},
{req: EtcdRequest{Op: Put, Key: "key2", PutData: "12"}, resp: EtcdResponse{Revision: 2}},
{req: EtcdRequest{Op: Get, Key: "key1"}, resp: EtcdResponse{GetData: "11", Revision: 1}, failure: true},
{req: EtcdRequest{Op: Get, Key: "key1"}, resp: EtcdResponse{GetData: "12", Revision: 1}, failure: true},
{req: EtcdRequest{Op: Get, Key: "key1"}, resp: EtcdResponse{GetData: "12", Revision: 2}, failure: true},
{req: EtcdRequest{Op: Get, Key: "key1"}, resp: EtcdResponse{GetData: "11", Revision: 2}},
{req: EtcdRequest{Op: Get, Key: "key2"}, resp: EtcdResponse{GetData: "11", Revision: 2}, failure: true},
{req: EtcdRequest{Op: Get, Key: "key2"}, resp: EtcdResponse{GetData: "12", Revision: 1}, failure: true},
{req: EtcdRequest{Op: Get, Key: "key2"}, resp: EtcdResponse{GetData: "11", Revision: 1}, failure: true},
{req: EtcdRequest{Op: Get, Key: "key2"}, resp: EtcdResponse{GetData: "12", Revision: 2}},
{req: putRequest("key1", "11"), resp: putResponse(1)},
{req: putRequest("key2", "12"), resp: putResponse(2)},
{req: getRequest("key1"), resp: getResponse("11", 1), failure: true},
{req: getRequest("key1"), resp: getResponse("12", 1), failure: true},
{req: getRequest("key1"), resp: getResponse("12", 2), failure: true},
{req: getRequest("key1"), resp: getResponse("11", 2)},
{req: getRequest("key2"), resp: getResponse("11", 2), failure: true},
{req: getRequest("key2"), resp: getResponse("12", 1), failure: true},
{req: getRequest("key2"), resp: getResponse("11", 1), failure: true},
{req: getRequest("key2"), resp: getResponse("12", 2)},
},
},
{
name: "Put must increase revision by 1",
operations: []testOperation{
{req: EtcdRequest{Op: Get, Key: "key"}, resp: EtcdResponse{Revision: 1}},
{req: EtcdRequest{Op: Put, Key: "key", PutData: "1"}, resp: EtcdResponse{Revision: 1}, failure: true},
{req: EtcdRequest{Op: Put, Key: "key", PutData: "1"}, resp: EtcdResponse{Revision: 3}, failure: true},
{req: EtcdRequest{Op: Put, Key: "key", PutData: "2"}, resp: EtcdResponse{Revision: 2}},
{req: getRequest("key"), resp: getResponse("", 1)},
{req: putRequest("key", "1"), resp: putResponse(1), failure: true},
{req: putRequest("key", "1"), resp: putResponse(3), failure: true},
{req: putRequest("key", "1"), resp: putResponse(2)},
},
},
{
name: "Put can fail and be lost before get",
operations: []testOperation{
{req: EtcdRequest{Op: Put, Key: "key", PutData: "1"}, resp: EtcdResponse{Revision: 1}},
{req: EtcdRequest{Op: Put, Key: "key", PutData: "2"}, resp: EtcdResponse{Err: errors.New("failed")}},
{req: EtcdRequest{Op: Get, Key: "key"}, resp: EtcdResponse{GetData: "1", Revision: 1}},
{req: EtcdRequest{Op: Get, Key: "key"}, resp: EtcdResponse{GetData: "2", Revision: 1}, failure: true},
{req: EtcdRequest{Op: Get, Key: "key"}, resp: EtcdResponse{GetData: "1", Revision: 2}, failure: true},
{req: EtcdRequest{Op: Get, Key: "key"}, resp: EtcdResponse{GetData: "2", Revision: 2}, failure: true},
{req: putRequest("key", "1"), resp: putResponse(1)},
{req: putRequest("key", "1"), resp: failedResponse(errors.New("failed"))},
{req: getRequest("key"), resp: getResponse("1", 1)},
{req: getRequest("key"), resp: getResponse("2", 1), failure: true},
{req: getRequest("key"), resp: getResponse("1", 2), failure: true},
{req: getRequest("key"), resp: getResponse("2", 2), failure: true},
},
},
{
name: "Put can fail and be lost before put",
operations: []testOperation{
{req: EtcdRequest{Op: Get, Key: "key"}, resp: EtcdResponse{Revision: 1}},
{req: EtcdRequest{Op: Put, Key: "key", PutData: "2"}, resp: EtcdResponse{Err: errors.New("failed")}},
{req: EtcdRequest{Op: Put, Key: "key", PutData: "3"}, resp: EtcdResponse{Revision: 2}},
{req: getRequest("key"), resp: getResponse("", 1)},
{req: putRequest("key", "1"), resp: failedResponse(errors.New("failed"))},
{req: putRequest("key", "3"), resp: getResponse("", 2)},
},
},
{
name: "Put can fail and be lost before delete",
operations: []testOperation{
{req: EtcdRequest{Op: Delete, Key: "key"}, resp: EtcdResponse{Revision: 1}},
{req: EtcdRequest{Op: Put, Key: "key", PutData: "2"}, resp: EtcdResponse{Err: errors.New("failed")}},
{req: EtcdRequest{Op: Delete, Key: "key"}, resp: EtcdResponse{Revision: 1}},
{req: deleteRequest("key"), resp: deleteResponse(0, 1)},
{req: putRequest("key", "1"), resp: failedResponse(errors.New("failed"))},
{req: deleteRequest("key"), resp: deleteResponse(0, 1)},
},
},
{
name: "Put can fail and be lost before txn failed",
name: "Put can fail and be lost before txn",
operations: []testOperation{
// Txn failure
{req: EtcdRequest{Op: Get, Key: "key"}, resp: EtcdResponse{Revision: 1}},
{req: EtcdRequest{Op: Put, Key: "key", PutData: "2"}, resp: EtcdResponse{Err: errors.New("failed")}},
{req: EtcdRequest{Op: Txn, Key: "key", TxnExpectData: "2", TxnNewData: "3"}, resp: EtcdResponse{Revision: 1}},
{req: getRequest("key"), resp: getResponse("", 1)},
{req: putRequest("key", "1"), resp: failedResponse(errors.New("failed"))},
{req: txnRequest("key", "2", "3"), resp: txnResponse(false, 1)},
// Txn success
{req: EtcdRequest{Op: Put, Key: "key", PutData: "2"}, resp: EtcdResponse{Revision: 2}},
{req: EtcdRequest{Op: Put, Key: "key", PutData: "4"}, resp: EtcdResponse{Err: errors.New("failed")}},
{req: EtcdRequest{Op: Txn, Key: "key", TxnExpectData: "2", TxnNewData: "3"}, resp: EtcdResponse{TxnSucceeded: true, Revision: 3}},
{req: putRequest("key", "2"), resp: putResponse(2)},
{req: putRequest("key", "4"), resp: failedResponse(errors.New("failed"))},
{req: txnRequest("key", "2", "5"), resp: txnResponse(true, 3)},
},
},
{
@ -120,284 +122,284 @@ func TestModel(t *testing.T) {
name: "Put can fail but be persisted and increase revision before get",
operations: []testOperation{
// One failed request, one persisted.
{req: EtcdRequest{Op: Put, Key: "key", PutData: "1"}, resp: EtcdResponse{Revision: 1}},
{req: EtcdRequest{Op: Put, Key: "key", PutData: "2"}, resp: EtcdResponse{Err: errors.New("failed")}},
{req: EtcdRequest{Op: Get, Key: "key"}, resp: EtcdResponse{GetData: "3", Revision: 2}, failure: true},
{req: EtcdRequest{Op: Get, Key: "key"}, resp: EtcdResponse{GetData: "2", Revision: 1}, failure: true},
{req: EtcdRequest{Op: Get, Key: "key"}, resp: EtcdResponse{GetData: "2", Revision: 2}},
{req: putRequest("key", "1"), resp: putResponse(1)},
{req: putRequest("key", "2"), resp: failedResponse(errors.New("failed"))},
{req: getRequest("key"), resp: getResponse("3", 2), failure: true},
{req: getRequest("key"), resp: getResponse("2", 1), failure: true},
{req: getRequest("key"), resp: getResponse("2", 2)},
// Two failed request, two persisted.
{req: EtcdRequest{Op: Put, Key: "key", PutData: "3"}, resp: EtcdResponse{Err: errors.New("failed")}},
{req: EtcdRequest{Op: Put, Key: "key", PutData: "4"}, resp: EtcdResponse{Err: errors.New("failed")}},
{req: EtcdRequest{Op: Get, Key: "key"}, resp: EtcdResponse{GetData: "4", Revision: 4}},
{req: putRequest("key", "3"), resp: failedResponse(errors.New("failed"))},
{req: putRequest("key", "4"), resp: failedResponse(errors.New("failed"))},
{req: getRequest("key"), resp: getResponse("4", 4)},
},
},
{
name: "Put can fail but be persisted and increase revision before delete",
operations: []testOperation{
// One failed request, one persisted.
{req: EtcdRequest{Op: Delete, Key: "key"}, resp: EtcdResponse{Revision: 1}},
{req: EtcdRequest{Op: Put, Key: "key", PutData: "2"}, resp: EtcdResponse{Err: errors.New("failed")}},
{req: EtcdRequest{Op: Delete, Key: "key"}, resp: EtcdResponse{Deleted: 1, Revision: 1}, failure: true},
{req: EtcdRequest{Op: Delete, Key: "key"}, resp: EtcdResponse{Deleted: 1, Revision: 2}, failure: true},
{req: EtcdRequest{Op: Delete, Key: "key"}, resp: EtcdResponse{Deleted: 1, Revision: 3}},
{req: deleteRequest("key"), resp: deleteResponse(0, 1)},
{req: putRequest("key", "1"), resp: failedResponse(errors.New("failed"))},
{req: deleteRequest("key"), resp: deleteResponse(1, 1), failure: true},
{req: deleteRequest("key"), resp: deleteResponse(1, 2), failure: true},
{req: deleteRequest("key"), resp: deleteResponse(1, 3)},
// Two failed request, two persisted.
{req: EtcdRequest{Op: Put, Key: "key", PutData: "4"}, resp: EtcdResponse{Revision: 4}},
{req: EtcdRequest{Op: Put, Key: "key", PutData: "5"}, resp: EtcdResponse{Err: errors.New("failed")}},
{req: EtcdRequest{Op: Put, Key: "key", PutData: "6"}, resp: EtcdResponse{Err: errors.New("failed")}},
{req: EtcdRequest{Op: Delete, Key: "key"}, resp: EtcdResponse{Deleted: 1, Revision: 7}},
{req: putRequest("key", "4"), resp: putResponse(4)},
{req: putRequest("key", "5"), resp: failedResponse(errors.New("failed"))},
{req: putRequest("key", "6"), resp: failedResponse(errors.New("failed"))},
{req: deleteRequest("key"), resp: deleteResponse(1, 7)},
// Two failed request, one persisted.
{req: EtcdRequest{Op: Put, Key: "key", PutData: "8"}, resp: EtcdResponse{Revision: 8}},
{req: EtcdRequest{Op: Put, Key: "key", PutData: "9"}, resp: EtcdResponse{Err: errors.New("failed")}},
{req: EtcdRequest{Op: Put, Key: "key", PutData: "10"}, resp: EtcdResponse{Err: errors.New("failed")}},
{req: EtcdRequest{Op: Delete, Key: "key"}, resp: EtcdResponse{Deleted: 1, Revision: 10}},
{req: putRequest("key", "8"), resp: putResponse(8)},
{req: putRequest("key", "9"), resp: failedResponse(errors.New("failed"))},
{req: putRequest("key", "10"), resp: failedResponse(errors.New("failed"))},
{req: deleteRequest("key"), resp: deleteResponse(1, 10)},
},
},
{
name: "Put can fail but be persisted before txn",
operations: []testOperation{
// Txn success
{req: EtcdRequest{Op: Get, Key: "key"}, resp: EtcdResponse{Revision: 1}},
{req: EtcdRequest{Op: Put, Key: "key", PutData: "2"}, resp: EtcdResponse{Err: errors.New("failed")}},
{req: EtcdRequest{Op: Txn, Key: "key", TxnExpectData: "2"}, resp: EtcdResponse{TxnSucceeded: true, Revision: 2}, failure: true},
{req: EtcdRequest{Op: Txn, Key: "key", TxnExpectData: "2"}, resp: EtcdResponse{TxnSucceeded: true, Revision: 3}},
{req: getRequest("key"), resp: getResponse("", 1)},
{req: putRequest("key", "2"), resp: failedResponse(errors.New("failed"))},
{req: txnRequest("key", "2", ""), resp: txnResponse(true, 2), failure: true},
{req: txnRequest("key", "2", ""), resp: txnResponse(true, 3)},
// Txn failure
{req: EtcdRequest{Op: Put, Key: "key", PutData: "4"}, resp: EtcdResponse{Revision: 4}},
{req: EtcdRequest{Op: Txn, Key: "key", TxnExpectData: "5"}, resp: EtcdResponse{Revision: 4}},
{req: EtcdRequest{Op: Put, Key: "key", PutData: "5"}, resp: EtcdResponse{Err: errors.New("failed")}},
{req: EtcdRequest{Op: Get, Key: "key"}, resp: EtcdResponse{Revision: 5, GetData: "5"}},
{req: putRequest("key", "4"), resp: putResponse(4)},
{req: txnRequest("key", "5", ""), resp: txnResponse(false, 4)},
{req: putRequest("key", "5"), resp: failedResponse(errors.New("failed"))},
{req: getRequest("key"), resp: getResponse("5", 5)},
},
},
{
name: "Delete only increases revision on success",
operations: []testOperation{
{req: EtcdRequest{Op: Put, Key: "key1", PutData: "11"}, resp: EtcdResponse{Revision: 1}},
{req: EtcdRequest{Op: Put, Key: "key2", PutData: "12"}, resp: EtcdResponse{Revision: 2}},
{req: EtcdRequest{Op: Delete, Key: "key1"}, resp: EtcdResponse{Deleted: 1, Revision: 2}, failure: true},
{req: EtcdRequest{Op: Delete, Key: "key1"}, resp: EtcdResponse{Deleted: 1, Revision: 3}},
{req: EtcdRequest{Op: Delete, Key: "key1"}, resp: EtcdResponse{Deleted: 0, Revision: 4}, failure: true},
{req: EtcdRequest{Op: Delete, Key: "key1"}, resp: EtcdResponse{Deleted: 0, Revision: 3}},
{req: putRequest("key1", "11"), resp: putResponse(1)},
{req: putRequest("key2", "12"), resp: putResponse(2)},
{req: deleteRequest("key1"), resp: deleteResponse(1, 2), failure: true},
{req: deleteRequest("key1"), resp: deleteResponse(1, 3)},
{req: deleteRequest("key1"), resp: deleteResponse(0, 4), failure: true},
{req: deleteRequest("key1"), resp: deleteResponse(0, 3)},
},
},
{
name: "Delete not existing key",
operations: []testOperation{
{req: EtcdRequest{Op: Get, Key: "key"}, resp: EtcdResponse{Revision: 1}},
{req: EtcdRequest{Op: Delete, Key: "key"}, resp: EtcdResponse{Deleted: 1, Revision: 2}, failure: true},
{req: EtcdRequest{Op: Delete, Key: "key"}, resp: EtcdResponse{Deleted: 0, Revision: 1}},
{req: getRequest("key"), resp: getResponse("", 1)},
{req: deleteRequest("key"), resp: deleteResponse(1, 2), failure: true},
{req: deleteRequest("key"), resp: deleteResponse(0, 1)},
},
},
{
name: "Delete clears value",
operations: []testOperation{
{req: EtcdRequest{Op: Get, Key: "key"}, resp: EtcdResponse{GetData: "1", Revision: 1}},
{req: EtcdRequest{Op: Delete, Key: "key"}, resp: EtcdResponse{Deleted: 1, Revision: 2}},
{req: EtcdRequest{Op: Get, Key: "key"}, resp: EtcdResponse{GetData: "1", Revision: 1}, failure: true},
{req: EtcdRequest{Op: Get, Key: "key"}, resp: EtcdResponse{GetData: "1", Revision: 2}, failure: true},
{req: EtcdRequest{Op: Get, Key: "key"}, resp: EtcdResponse{Revision: 2}},
{req: getRequest("key"), resp: getResponse("1", 1)},
{req: deleteRequest("key"), resp: deleteResponse(1, 2)},
{req: getRequest("key"), resp: getResponse("1", 1), failure: true},
{req: getRequest("key"), resp: getResponse("1", 2), failure: true},
{req: getRequest("key"), resp: getResponse("", 2)},
},
},
{
name: "Delete can fail and be lost before get",
operations: []testOperation{
{req: EtcdRequest{Op: Put, Key: "key", PutData: "1"}, resp: EtcdResponse{Revision: 1}},
{req: EtcdRequest{Op: Delete, Key: "key"}, resp: EtcdResponse{Err: errors.New("failed")}},
{req: EtcdRequest{Op: Get, Key: "key"}, resp: EtcdResponse{GetData: "1", Revision: 1}},
{req: EtcdRequest{Op: Get, Key: "key"}, resp: EtcdResponse{Revision: 2}, failure: true},
{req: putRequest("key", "1"), resp: putResponse(1)},
{req: deleteRequest("key"), resp: failedResponse(errors.New("failed"))},
{req: getRequest("key"), resp: getResponse("1", 1)},
{req: getRequest("key"), resp: getResponse("", 2), failure: true},
},
},
{
name: "Delete can fail and be lost before delete",
operations: []testOperation{
{req: EtcdRequest{Op: Put, Key: "key", PutData: "1"}, resp: EtcdResponse{Revision: 1}},
{req: EtcdRequest{Op: Delete, Key: "key"}, resp: EtcdResponse{Err: errors.New("failed")}},
{req: EtcdRequest{Op: Delete, Key: "key"}, resp: EtcdResponse{Deleted: 1, Revision: 1}, failure: true},
{req: EtcdRequest{Op: Delete, Key: "key"}, resp: EtcdResponse{Deleted: 1, Revision: 2}},
{req: putRequest("key", "1"), resp: putResponse(1)},
{req: deleteRequest("key"), resp: failedResponse(errors.New("failed"))},
{req: deleteRequest("key"), resp: deleteResponse(1, 1), failure: true},
{req: deleteRequest("key"), resp: deleteResponse(1, 2)},
},
},
{
name: "Delete can fail and be lost before put",
operations: []testOperation{
{req: EtcdRequest{Op: Put, Key: "key", PutData: "1"}, resp: EtcdResponse{Revision: 1}},
{req: EtcdRequest{Op: Delete, Key: "key"}, resp: EtcdResponse{Err: errors.New("failed")}},
{req: EtcdRequest{Op: Put, Key: "key", PutData: "2"}, resp: EtcdResponse{Revision: 2}},
{req: putRequest("key", "1"), resp: putResponse(1)},
{req: deleteRequest("key"), resp: failedResponse(errors.New("failed"))},
{req: putRequest("key", "1"), resp: putResponse(2)},
},
},
{
name: "Delete can fail but be persisted before get",
operations: []testOperation{
// One failed request, one persisted.
{req: EtcdRequest{Op: Put, Key: "key", PutData: "1"}, resp: EtcdResponse{Revision: 1}},
{req: EtcdRequest{Op: Delete, Key: "key"}, resp: EtcdResponse{Err: errors.New("failed")}},
{req: EtcdRequest{Op: Get, Key: "key"}, resp: EtcdResponse{Revision: 2}},
{req: putRequest("key", "1"), resp: putResponse(1)},
{req: deleteRequest("key"), resp: failedResponse(errors.New("failed"))},
{req: getRequest("key"), resp: getResponse("", 2)},
// Two failed request, one persisted.
{req: EtcdRequest{Op: Put, Key: "key", PutData: "3"}, resp: EtcdResponse{Revision: 3}},
{req: EtcdRequest{Op: Delete, Key: "key"}, resp: EtcdResponse{Err: errors.New("failed")}},
{req: EtcdRequest{Op: Delete, Key: "key"}, resp: EtcdResponse{Err: errors.New("failed")}},
{req: EtcdRequest{Op: Get, Key: "key"}, resp: EtcdResponse{Revision: 4}},
{req: putRequest("key", "3"), resp: putResponse(3)},
{req: deleteRequest("key"), resp: failedResponse(errors.New("failed"))},
{req: deleteRequest("key"), resp: failedResponse(errors.New("failed"))},
{req: getRequest("key"), resp: getResponse("", 4)},
},
},
{
name: "Delete can fail but be persisted before put",
operations: []testOperation{
// One failed request, one persisted.
{req: EtcdRequest{Op: Put, Key: "key", PutData: "1"}, resp: EtcdResponse{Revision: 1}},
{req: EtcdRequest{Op: Delete, Key: "key"}, resp: EtcdResponse{Err: errors.New("failed")}},
{req: EtcdRequest{Op: Put, Key: "key", PutData: "3"}, resp: EtcdResponse{Revision: 3}},
{req: putRequest("key", "1"), resp: putResponse(1)},
{req: deleteRequest("key"), resp: failedResponse(errors.New("failed"))},
{req: putRequest("key", "3"), resp: putResponse(3)},
// Two failed request, one persisted.
{req: EtcdRequest{Op: Delete, Key: "key"}, resp: EtcdResponse{Err: errors.New("failed")}},
{req: EtcdRequest{Op: Delete, Key: "key"}, resp: EtcdResponse{Err: errors.New("failed")}},
{req: EtcdRequest{Op: Put, Key: "key", PutData: "5"}, resp: EtcdResponse{Revision: 5}},
{req: deleteRequest("key"), resp: failedResponse(errors.New("failed"))},
{req: deleteRequest("key"), resp: failedResponse(errors.New("failed"))},
{req: putRequest("key", "5"), resp: putResponse(5)},
},
},
{
name: "Delete can fail but be persisted before delete",
operations: []testOperation{
// One failed request, one persisted.
{req: EtcdRequest{Op: Put, Key: "key", PutData: "1"}, resp: EtcdResponse{Revision: 1}},
{req: EtcdRequest{Op: Delete, Key: "key"}, resp: EtcdResponse{Err: errors.New("failed")}},
{req: EtcdRequest{Op: Delete, Key: "key"}, resp: EtcdResponse{Revision: 2}},
{req: EtcdRequest{Op: Put, Key: "key", PutData: "3"}, resp: EtcdResponse{Revision: 3}},
{req: putRequest("key", "1"), resp: putResponse(1)},
{req: deleteRequest("key"), resp: failedResponse(errors.New("failed"))},
{req: deleteRequest("key"), resp: deleteResponse(0, 2)},
{req: putRequest("key", "3"), resp: putResponse(3)},
// Two failed request, one persisted.
{req: EtcdRequest{Op: Delete, Key: "key"}, resp: EtcdResponse{Err: errors.New("failed")}},
{req: EtcdRequest{Op: Delete, Key: "key"}, resp: EtcdResponse{Err: errors.New("failed")}},
{req: EtcdRequest{Op: Delete, Key: "key"}, resp: EtcdResponse{Revision: 4}},
{req: deleteRequest("key"), resp: failedResponse(errors.New("failed"))},
{req: deleteRequest("key"), resp: failedResponse(errors.New("failed"))},
{req: deleteRequest("key"), resp: deleteResponse(0, 4)},
},
},
{
name: "Delete can fail but be persisted before txn",
operations: []testOperation{
// Txn success
{req: EtcdRequest{Op: Get, Key: "key"}, resp: EtcdResponse{GetData: "1", Revision: 1}},
{req: EtcdRequest{Op: Delete, Key: "key"}, resp: EtcdResponse{Err: errors.New("failed")}},
{req: EtcdRequest{Op: Txn, Key: "key", TxnExpectData: "", TxnNewData: "1"}, resp: EtcdResponse{TxnSucceeded: true, Revision: 3}},
{req: getRequest("key"), resp: getResponse("1", 1)},
{req: deleteRequest("key"), resp: failedResponse(errors.New("failed"))},
{req: txnRequest("key", "", "3"), resp: txnResponse(true, 3)},
// Txn failure
{req: EtcdRequest{Op: Put, Key: "key", PutData: "4"}, resp: EtcdResponse{Revision: 4}},
{req: EtcdRequest{Op: Delete, Key: "key"}, resp: EtcdResponse{Err: errors.New("failed")}},
{req: EtcdRequest{Op: Txn, Key: "key", TxnExpectData: "4", TxnNewData: "5"}, resp: EtcdResponse{TxnSucceeded: false, Revision: 5}},
{req: putRequest("key", "4"), resp: putResponse(4)},
{req: deleteRequest("key"), resp: failedResponse(errors.New("failed"))},
{req: txnRequest("key", "4", "5"), resp: txnResponse(false, 5)},
},
},
{
name: "Txn sets new value if value matches expected",
operations: []testOperation{
{req: EtcdRequest{Op: Get, Key: "key"}, resp: EtcdResponse{GetData: "1", Revision: 1}},
{req: EtcdRequest{Op: Txn, Key: "key", TxnExpectData: "1", TxnNewData: "2"}, resp: EtcdResponse{Revision: 1, TxnSucceeded: true}, failure: true},
{req: EtcdRequest{Op: Txn, Key: "key", TxnExpectData: "1", TxnNewData: "2"}, resp: EtcdResponse{Revision: 2, TxnSucceeded: false}, failure: true},
{req: EtcdRequest{Op: Txn, Key: "key", TxnExpectData: "1", TxnNewData: "2"}, resp: EtcdResponse{Revision: 1, TxnSucceeded: false}, failure: true},
{req: EtcdRequest{Op: Txn, Key: "key", TxnExpectData: "1", TxnNewData: "2"}, resp: EtcdResponse{Revision: 2, TxnSucceeded: true}},
{req: EtcdRequest{Op: Get, Key: "key"}, resp: EtcdResponse{GetData: "1", Revision: 1}, failure: true},
{req: EtcdRequest{Op: Get, Key: "key"}, resp: EtcdResponse{GetData: "1", Revision: 2}, failure: true},
{req: EtcdRequest{Op: Get, Key: "key"}, resp: EtcdResponse{GetData: "2", Revision: 1}, failure: true},
{req: EtcdRequest{Op: Get, Key: "key"}, resp: EtcdResponse{GetData: "2", Revision: 2}},
{req: getRequest("key"), resp: getResponse("1", 1)},
{req: txnRequest("key", "1", "2"), resp: txnResponse(true, 1), failure: true},
{req: txnRequest("key", "1", "2"), resp: txnResponse(false, 2), failure: true},
{req: txnRequest("key", "1", "2"), resp: txnResponse(false, 1), failure: true},
{req: txnRequest("key", "1", "2"), resp: txnResponse(true, 2)},
{req: getRequest("key"), resp: getResponse("1", 1), failure: true},
{req: getRequest("key"), resp: getResponse("1", 2), failure: true},
{req: getRequest("key"), resp: getResponse("2", 1), failure: true},
{req: getRequest("key"), resp: getResponse("2", 2)},
},
},
{
name: "Txn can expect on empty key",
operations: []testOperation{
{req: EtcdRequest{Op: Get, Key: "key1"}, resp: EtcdResponse{Revision: 1}},
{req: EtcdRequest{Op: Txn, Key: "key1", TxnExpectData: "", TxnNewData: "2"}, resp: EtcdResponse{Revision: 2, TxnSucceeded: true}},
{req: EtcdRequest{Op: Txn, Key: "key2", TxnExpectData: "", TxnNewData: "3"}, resp: EtcdResponse{Revision: 3, TxnSucceeded: true}},
{req: EtcdRequest{Op: Txn, Key: "key3", TxnExpectData: "4", TxnNewData: "4"}, resp: EtcdResponse{Revision: 4}, failure: true},
{req: getRequest("key1"), resp: getResponse("", 1)},
{req: txnRequest("key1", "", "2"), resp: txnResponse(true, 2)},
{req: txnRequest("key2", "", "3"), resp: txnResponse(true, 3)},
{req: txnRequest("key3", "4", "4"), resp: txnResponse(false, 4), failure: true},
},
},
{
name: "Txn doesn't do anything if value doesn't match expected",
operations: []testOperation{
{req: EtcdRequest{Op: Get, Key: "key"}, resp: EtcdResponse{GetData: "1", Revision: 1}},
{req: EtcdRequest{Op: Txn, Key: "key", TxnExpectData: "2", TxnNewData: "3"}, resp: EtcdResponse{Revision: 2, TxnSucceeded: true}, failure: true},
{req: EtcdRequest{Op: Txn, Key: "key", TxnExpectData: "2", TxnNewData: "3"}, resp: EtcdResponse{Revision: 1, TxnSucceeded: true}, failure: true},
{req: EtcdRequest{Op: Txn, Key: "key", TxnExpectData: "2", TxnNewData: "3"}, resp: EtcdResponse{Revision: 2, TxnSucceeded: false}, failure: true},
{req: EtcdRequest{Op: Txn, Key: "key", TxnExpectData: "2", TxnNewData: "3"}, resp: EtcdResponse{Revision: 1, TxnSucceeded: false}},
{req: EtcdRequest{Op: Get, Key: "key"}, resp: EtcdResponse{GetData: "2", Revision: 1}, failure: true},
{req: EtcdRequest{Op: Get, Key: "key"}, resp: EtcdResponse{GetData: "2", Revision: 2}, failure: true},
{req: EtcdRequest{Op: Get, Key: "key"}, resp: EtcdResponse{GetData: "3", Revision: 1}, failure: true},
{req: EtcdRequest{Op: Get, Key: "key"}, resp: EtcdResponse{GetData: "3", Revision: 2}, failure: true},
{req: EtcdRequest{Op: Get, Key: "key"}, resp: EtcdResponse{GetData: "1", Revision: 1}},
{req: getRequest("key"), resp: getResponse("1", 1)},
{req: txnRequest("key", "2", "3"), resp: txnResponse(true, 2), failure: true},
{req: txnRequest("key", "2", "3"), resp: txnResponse(true, 1), failure: true},
{req: txnRequest("key", "2", "3"), resp: txnResponse(false, 2), failure: true},
{req: txnRequest("key", "2", "3"), resp: txnResponse(false, 1)},
{req: getRequest("key"), resp: getResponse("2", 1), failure: true},
{req: getRequest("key"), resp: getResponse("2", 2), failure: true},
{req: getRequest("key"), resp: getResponse("3", 1), failure: true},
{req: getRequest("key"), resp: getResponse("3", 2), failure: true},
{req: getRequest("key"), resp: getResponse("1", 1)},
},
},
{
name: "Txn can fail and be lost before get",
operations: []testOperation{
{req: EtcdRequest{Op: Get, Key: "key"}, resp: EtcdResponse{GetData: "1", Revision: 1}},
{req: EtcdRequest{Op: Txn, Key: "key", TxnExpectData: "1", TxnNewData: "2"}, resp: EtcdResponse{Err: errors.New("failed")}},
{req: EtcdRequest{Op: Get, Key: "key"}, resp: EtcdResponse{GetData: "1", Revision: 1}},
{req: EtcdRequest{Op: Get, Key: "key"}, resp: EtcdResponse{Revision: 2, GetData: "2"}, failure: true},
{req: getRequest("key"), resp: getResponse("1", 1)},
{req: txnRequest("key", "1", "2"), resp: failedResponse(errors.New("failed"))},
{req: getRequest("key"), resp: getResponse("1", 1)},
{req: getRequest("key"), resp: getResponse("2", 2), failure: true},
},
},
{
name: "Txn can fail and be lost before delete",
operations: []testOperation{
{req: EtcdRequest{Op: Get, Key: "key"}, resp: EtcdResponse{GetData: "1", Revision: 1}},
{req: EtcdRequest{Op: Txn, Key: "key", TxnExpectData: "1", TxnNewData: "2"}, resp: EtcdResponse{Err: errors.New("failed")}},
{req: EtcdRequest{Op: Delete, Key: "key"}, resp: EtcdResponse{Deleted: 1, Revision: 2}},
{req: getRequest("key"), resp: getResponse("1", 1)},
{req: txnRequest("key", "1", "2"), resp: failedResponse(errors.New("failed"))},
{req: deleteRequest("key"), resp: deleteResponse(1, 2)},
},
},
{
name: "Txn can fail and be lost before put",
operations: []testOperation{
{req: EtcdRequest{Op: Get, Key: "key"}, resp: EtcdResponse{GetData: "1", Revision: 1}},
{req: EtcdRequest{Op: Txn, Key: "key", TxnExpectData: "1", TxnNewData: "2"}, resp: EtcdResponse{Err: errors.New("failed")}},
{req: EtcdRequest{Op: Put, Key: "key", PutData: "3"}, resp: EtcdResponse{Revision: 2}},
{req: getRequest("key"), resp: getResponse("1", 1)},
{req: txnRequest("key", "1", "2"), resp: failedResponse(errors.New("failed"))},
{req: putRequest("key", "3"), resp: putResponse(2)},
},
},
{
name: "Txn can fail but be persisted before get",
operations: []testOperation{
// One failed request, one persisted.
{req: EtcdRequest{Op: Get, Key: "key"}, resp: EtcdResponse{GetData: "1", Revision: 1}},
{req: EtcdRequest{Op: Txn, Key: "key", TxnExpectData: "1", TxnNewData: "2"}, resp: EtcdResponse{Err: errors.New("failed")}},
{req: EtcdRequest{Op: Get, Key: "key"}, resp: EtcdResponse{Revision: 1, GetData: "2"}, failure: true},
{req: EtcdRequest{Op: Get, Key: "key"}, resp: EtcdResponse{Revision: 2, GetData: "2"}},
{req: getRequest("key"), resp: getResponse("1", 1)},
{req: txnRequest("key", "1", "2"), resp: failedResponse(errors.New("failed"))},
{req: getRequest("key"), resp: getResponse("2", 1), failure: true},
{req: getRequest("key"), resp: getResponse("2", 2)},
// Two failed request, two persisted.
{req: EtcdRequest{Op: Put, Key: "key", PutData: "3"}, resp: EtcdResponse{Revision: 3}},
{req: EtcdRequest{Op: Txn, Key: "key", TxnExpectData: "3", TxnNewData: "4"}, resp: EtcdResponse{Err: errors.New("failed")}},
{req: EtcdRequest{Op: Txn, Key: "key", TxnExpectData: "4", TxnNewData: "5"}, resp: EtcdResponse{Err: errors.New("failed")}},
{req: EtcdRequest{Op: Get, Key: "key"}, resp: EtcdResponse{Revision: 5, GetData: "5"}},
{req: putRequest("key", "3"), resp: putResponse(3)},
{req: txnRequest("key", "3", "4"), resp: failedResponse(errors.New("failed"))},
{req: txnRequest("key", "4", "5"), resp: failedResponse(errors.New("failed"))},
{req: getRequest("key"), resp: getResponse("5", 5)},
},
},
{
name: "Txn can fail but be persisted before put",
operations: []testOperation{
// One failed request, one persisted.
{req: EtcdRequest{Op: Get, Key: "key"}, resp: EtcdResponse{GetData: "1", Revision: 1}},
{req: EtcdRequest{Op: Txn, Key: "key", TxnExpectData: "1", TxnNewData: "2"}, resp: EtcdResponse{Err: errors.New("failed")}},
{req: EtcdRequest{Op: Put, Key: "key", PutData: "3"}, resp: EtcdResponse{Revision: 3}},
{req: getRequest("key"), resp: getResponse("1", 1)},
{req: txnRequest("key", "1", "2"), resp: failedResponse(errors.New("failed"))},
{req: putRequest("key", "3"), resp: putResponse(3)},
// Two failed request, two persisted.
{req: EtcdRequest{Op: Put, Key: "key", PutData: "4"}, resp: EtcdResponse{Revision: 4}},
{req: EtcdRequest{Op: Txn, Key: "key", TxnExpectData: "4", TxnNewData: "5"}, resp: EtcdResponse{Err: errors.New("failed")}},
{req: EtcdRequest{Op: Txn, Key: "key", TxnExpectData: "5", TxnNewData: "6"}, resp: EtcdResponse{Err: errors.New("failed")}},
{req: EtcdRequest{Op: Put, Key: "key", PutData: "7"}, resp: EtcdResponse{Revision: 7}},
{req: putRequest("key", "4"), resp: putResponse(4)},
{req: txnRequest("key", "4", "5"), resp: failedResponse(errors.New("failed"))},
{req: txnRequest("key", "5", "6"), resp: failedResponse(errors.New("failed"))},
{req: putRequest("key", "7"), resp: putResponse(7)},
},
},
{
name: "Txn can fail but be persisted before delete",
operations: []testOperation{
// One failed request, one persisted.
{req: EtcdRequest{Op: Get, Key: "key"}, resp: EtcdResponse{GetData: "1", Revision: 1}},
{req: EtcdRequest{Op: Txn, Key: "key", TxnExpectData: "1", TxnNewData: "2"}, resp: EtcdResponse{Err: errors.New("failed")}},
{req: EtcdRequest{Op: Delete, Key: "key"}, resp: EtcdResponse{Deleted: 1, Revision: 3}},
{req: getRequest("key"), resp: getResponse("1", 1)},
{req: txnRequest("key", "1", "2"), resp: failedResponse(errors.New("failed"))},
{req: deleteRequest("key"), resp: deleteResponse(1, 3)},
// Two failed request, two persisted.
{req: EtcdRequest{Op: Put, Key: "key", PutData: "4"}, resp: EtcdResponse{Revision: 4}},
{req: EtcdRequest{Op: Txn, Key: "key", TxnExpectData: "4", TxnNewData: "5"}, resp: EtcdResponse{Err: errors.New("failed")}},
{req: EtcdRequest{Op: Txn, Key: "key", TxnExpectData: "5", TxnNewData: "6"}, resp: EtcdResponse{Err: errors.New("failed")}},
{req: EtcdRequest{Op: Delete, Key: "key"}, resp: EtcdResponse{Deleted: 1, Revision: 7}},
{req: putRequest("key", "4"), resp: putResponse(4)},
{req: txnRequest("key", "4", "5"), resp: failedResponse(errors.New("failed"))},
{req: txnRequest("key", "5", "6"), resp: failedResponse(errors.New("failed"))},
{req: deleteRequest("key"), resp: deleteResponse(1, 7)},
},
},
{
name: "Txn can fail but be persisted before txn",
operations: []testOperation{
// One failed request, one persisted with success.
{req: EtcdRequest{Op: Get, Key: "key"}, resp: EtcdResponse{GetData: "1", Revision: 1}},
{req: EtcdRequest{Op: Txn, Key: "key", TxnExpectData: "1", TxnNewData: "2"}, resp: EtcdResponse{Err: errors.New("failed")}},
{req: EtcdRequest{Op: Txn, Key: "key", TxnExpectData: "2", TxnNewData: "3"}, resp: EtcdResponse{Revision: 3, TxnSucceeded: true}},
{req: getRequest("key"), resp: getResponse("1", 1)},
{req: txnRequest("key", "1", "2"), resp: failedResponse(errors.New("failed"))},
{req: txnRequest("key", "2", "3"), resp: txnResponse(true, 3)},
// Two failed request, two persisted with success.
{req: EtcdRequest{Op: Put, Key: "key", PutData: "4"}, resp: EtcdResponse{Revision: 4}},
{req: EtcdRequest{Op: Txn, Key: "key", TxnExpectData: "4", TxnNewData: "5"}, resp: EtcdResponse{Err: errors.New("failed")}},
{req: EtcdRequest{Op: Txn, Key: "key", TxnExpectData: "5", TxnNewData: "6"}, resp: EtcdResponse{Err: errors.New("failed")}},
{req: EtcdRequest{Op: Txn, Key: "key", TxnExpectData: "6", TxnNewData: "7"}, resp: EtcdResponse{Revision: 7, TxnSucceeded: true}},
{req: putRequest("key", "4"), resp: putResponse(4)},
{req: txnRequest("key", "4", "5"), resp: failedResponse(errors.New("failed"))},
{req: txnRequest("key", "5", "6"), resp: failedResponse(errors.New("failed"))},
{req: txnRequest("key", "6", "7"), resp: txnResponse(true, 7)},
// One failed request, one persisted with failure.
{req: EtcdRequest{Op: Put, Key: "key", PutData: "8"}, resp: EtcdResponse{Revision: 8}},
{req: EtcdRequest{Op: Txn, Key: "key", TxnExpectData: "8", TxnNewData: "9"}, resp: EtcdResponse{Err: errors.New("failed")}},
{req: EtcdRequest{Op: Txn, Key: "key", TxnExpectData: "8", TxnNewData: "10"}, resp: EtcdResponse{Revision: 9}},
{req: putRequest("key", "8"), resp: putResponse(8)},
{req: txnRequest("key", "8", "9"), resp: failedResponse(errors.New("failed"))},
{req: txnRequest("key", "8", "10"), resp: txnResponse(false, 9)},
},
},
}
@ -424,3 +426,60 @@ type testOperation struct {
resp EtcdResponse
failure bool
}
func TestModelDescribe(t *testing.T) {
tcs := []struct {
req EtcdRequest
resp EtcdResponse
expectDescribe string
}{
{
req: getRequest("key1"),
resp: getResponse("", 1),
expectDescribe: `get("key1") -> nil, rev: 1`,
},
{
req: getRequest("key2"),
resp: getResponse("2", 2),
expectDescribe: `get("key2") -> "2", rev: 2`,
},
{
req: putRequest("key3", "3"),
resp: putResponse(3),
expectDescribe: `put("key3", "3") -> ok, rev: 3`,
},
{
req: putRequest("key4", "4"),
resp: failedResponse(errors.New("failed")),
expectDescribe: `put("key4", "4") -> err: "failed"`,
},
{
req: deleteRequest("key5"),
resp: deleteResponse(1, 5),
expectDescribe: `delete("key5") -> deleted: 1, rev: 5`,
},
{
req: deleteRequest("key6"),
resp: failedResponse(errors.New("failed")),
expectDescribe: `delete("key6") -> err: "failed"`,
},
{
req: txnRequest("key7", "7", "77"),
resp: txnResponse(false, 7),
expectDescribe: `if(key7=="7").then(put("key7", "77")) -> txn failed, rev: 7`,
},
{
req: txnRequest("key8", "8", "88"),
resp: txnResponse(true, 8),
expectDescribe: `if(key8=="8").then(put("key8", "88")) -> ok, rev: 8`,
},
{
req: txnRequest("key9", "9", "99"),
resp: failedResponse(errors.New("failed")),
expectDescribe: `if(key9=="9").then(put("key9", "99")) -> err: "failed"`,
},
}
for _, tc := range tcs {
assert.Equal(t, tc.expectDescribe, etcdModel.DescribeOperation(tc.req, tc.resp))
}
}

View File

@ -39,7 +39,7 @@ type readWriteSingleKey struct {
}
type opChance struct {
operation Operation
operation OperationType
chance int
}
@ -97,7 +97,7 @@ func (t readWriteSingleKey) Write(ctx context.Context, c *recordingClient, limit
return err
}
func (t readWriteSingleKey) pickWriteOperation() Operation {
func (t readWriteSingleKey) pickWriteOperation() OperationType {
sum := 0
for _, op := range t.writes {
sum += op.chance