tests: Refactor etcd model

Signed-off-by: Marek Siarkowicz <siarkowicz@google.com>
This commit is contained in:
Marek Siarkowicz 2022-12-06 13:47:23 +01:00
parent a4c6d1bbce
commit 60200b37d3
2 changed files with 67 additions and 107 deletions

View File

@ -44,9 +44,12 @@ type EtcdResponse struct {
type EtcdState struct { type EtcdState struct {
Key string Key string
PossibleValues []ValueRevision
}
type ValueRevision struct {
Value string Value string
LastRevision int64 Revision int64
FailedWrite *EtcdRequest
} }
var etcdModel = porcupine.Model{ var etcdModel = porcupine.Model{
@ -93,121 +96,77 @@ var etcdModel = porcupine.Model{
} }
func step(state EtcdState, request EtcdRequest, response EtcdResponse) (bool, EtcdState) { func step(state EtcdState, request EtcdRequest, response EtcdResponse) (bool, EtcdState) {
if request.Key == "" { if len(state.PossibleValues) == 0 {
panic("invalid request") state.Key = request.Key
if ok, v := initValueRevision(request, response); ok {
state.PossibleValues = append(state.PossibleValues, v)
} }
if state.Key == "" { return true, state
return true, initState(request, response)
} }
if state.Key != request.Key { if state.Key != request.Key {
panic("Multiple keys not supported") panic("multiple keys not supported")
} }
switch request.Op { if response.Err != nil {
case Get: for _, v := range state.PossibleValues {
return stepGet(state, request, response) newV, _ := stepValue(v, request)
case Put: state.PossibleValues = append(state.PossibleValues, newV)
return stepPut(state, request, response)
case Delete:
return stepDelete(state, request, response)
default:
panic("Unknown operation")
} }
}
func initState(request EtcdRequest, response EtcdResponse) EtcdState {
state := EtcdState{
Key: request.Key,
LastRevision: response.Revision,
}
switch request.Op {
case Get:
state.Value = response.GetData
case Put:
if response.Err == nil {
state.Value = request.PutData
} else { } else {
state.FailedWrite = &request var i = 0
for _, v := range state.PossibleValues {
newV, expectedResponse := stepValue(v, request)
if expectedResponse == response {
state.PossibleValues[i] = newV
i++
} }
case Delete:
if response.Err != nil {
state.FailedWrite = &request
} }
default: state.PossibleValues = state.PossibleValues[:i]
panic("Unknown operation")
} }
return state return len(state.PossibleValues) > 0, state
} }
func stepGet(state EtcdState, request EtcdRequest, response EtcdResponse) (bool, EtcdState) { func initValueRevision(request EtcdRequest, response EtcdResponse) (ok bool, v ValueRevision) {
if state.Value == response.GetData && state.LastRevision == response.Revision { if response.Err != nil {
state.FailedWrite = nil return false, ValueRevision{}
return true, state
} }
if state.FailedWrite != nil && state.LastRevision < response.Revision { switch request.Op {
var ok bool
switch state.FailedWrite.Op {
case Get: case Get:
panic("Expected write") return true, ValueRevision{
Value: response.GetData,
Revision: response.Revision,
}
case Put: case Put:
ok = response.GetData == state.FailedWrite.PutData return true, ValueRevision{
Value: request.PutData,
Revision: response.Revision,
}
case Delete: case Delete:
ok = response.GetData == "" return true, ValueRevision{
Value: "",
Revision: response.Revision,
}
default: default:
panic("Unknown operation") panic("Unknown operation")
} }
if ok {
state.Value = response.GetData
state.LastRevision = response.Revision
state.FailedWrite = nil
return true, state
}
}
return false, state
} }
func stepPut(state EtcdState, request EtcdRequest, response EtcdResponse) (bool, EtcdState) { func stepValue(v ValueRevision, request EtcdRequest) (ValueRevision, EtcdResponse) {
if response.Err != nil { resp := EtcdResponse{}
state.FailedWrite = &request switch request.Op {
return true, state case Get:
resp.GetData = v.Value
case Put:
v.Value = request.PutData
v.Revision += 1
case Delete:
if v.Value != "" {
v.Value = ""
v.Revision += 1
resp.Deleted = 1
} }
if response.Revision <= state.LastRevision { default:
return false, state panic("unsupported operation")
} }
if response.Revision != state.LastRevision+1 && state.FailedWrite == nil { resp.Revision = v.Revision
return false, state return v, resp
}
state.Value = request.PutData
state.LastRevision = response.Revision
state.FailedWrite = nil
return true, state
}
func stepDelete(state EtcdState, request EtcdRequest, response EtcdResponse) (bool, EtcdState) {
if response.Err != nil {
state.FailedWrite = &request
return true, state
}
// revision should never decrease
if response.Revision < state.LastRevision {
return false, state
}
deleteSucceeded := response.Deleted != 0
keySet := state.Value != ""
// non-existent key cannot be deleted.
if deleteSucceeded != keySet && state.FailedWrite == nil {
return false, state
}
//if key was deleted, response revision should increase
if deleteSucceeded && (response.Revision != state.LastRevision+1 || !keySet) && (state.FailedWrite == nil || response.Revision < state.LastRevision+2) {
return false, state
}
//if key was not deleted, response revision should not change
if !deleteSucceeded && state.LastRevision != response.Revision && state.FailedWrite == nil {
return false, state
}
state.Value = ""
state.LastRevision = response.Revision
return true, state
} }

View File

@ -105,8 +105,6 @@ func TestModel(t *testing.T) {
// Two failed request, two persisted. // 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: "3"}, resp: EtcdResponse{Err: errors.New("failed")}},
{req: EtcdRequest{Op: Put, Key: "key", PutData: "4"}, 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: "3", Revision: 3}, failure: true},
{req: EtcdRequest{Op: Get, Key: "key"}, resp: EtcdResponse{GetData: "3", Revision: 4}, failure: true},
{req: EtcdRequest{Op: Get, Key: "key"}, resp: EtcdResponse{GetData: "4", Revision: 4}}, {req: EtcdRequest{Op: Get, Key: "key"}, resp: EtcdResponse{GetData: "4", Revision: 4}},
}, },
}, },
@ -220,15 +218,18 @@ func TestModel(t *testing.T) {
}, },
} }
for _, tc := range tcs { for _, tc := range tcs {
var ok bool
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
state := etcdModel.Init() state := etcdModel.Init()
for _, op := range tc.operations { for _, op := range tc.operations {
t.Logf("state: %v", state) ok, newState := etcdModel.Step(state, op.req, op.resp)
ok, state = etcdModel.Step(state, op.req, op.resp)
if ok != !op.failure { if ok != !op.failure {
t.Logf("state: %v", state)
t.Errorf("Unexpected operation result, expect: %v, got: %v, operation: %s", !op.failure, ok, etcdModel.DescribeOperation(op.req, op.resp)) t.Errorf("Unexpected operation result, expect: %v, got: %v, operation: %s", !op.failure, ok, etcdModel.DescribeOperation(op.req, op.resp))
} }
if ok {
state = newState
t.Logf("state: %v", state)
}
} }
}) })
} }