diff --git a/tests/robustness/model/describe.go b/tests/robustness/model/describe.go index 8f708a4551b1..deb25761c4f2 100644 --- a/tests/robustness/model/describe.go +++ b/tests/robustness/model/describe.go @@ -85,7 +85,15 @@ func describeEtcdState(state EtcdState) string { descHTML = append(descHTML, fmt.Sprintf("
state, rev: %d, compactRev: %d
", state.Revision, state.CompactRevision)) - if len(state.KeyValues) > 0 { + keys := []string{} + for i, v := range state.KeyValues { + if v == nil { + continue + } + keys = append(keys, state.Keys[i]) + } + + if len(keys) > 0 { descHTML = append(descHTML, "keys:" + html.EscapeString(string(data)) + "" - }, +var DeterministicModel = func(keys []string) porcupine.Model { + return porcupine.Model{ + Init: func() any { + return freshEtcdState(keys) + }, + Step: func(st any, in any, out any) (bool, any) { + return st.(EtcdState).apply(in.(EtcdRequest), out.(EtcdResponse)) + }, + Equal: func(st1, st2 any) bool { + return st1.(EtcdState).Equal(st2.(EtcdState)) + }, + DescribeOperation: func(in, out any) string { + return fmt.Sprintf("%s -> %s", describeEtcdRequest(in.(EtcdRequest)), describeEtcdResponse(in.(EtcdRequest), MaybeEtcdResponse{EtcdResponse: out.(EtcdResponse)})) + }, + DescribeOperationMetadata: func(info any) string { + if info == nil { + return "" + } + return DescribeOperationMetadata(MaybeEtcdResponse{EtcdResponse: info.(EtcdResponse)}) + }, + DescribeState: func(st any) string { + data, err := json.MarshalIndent(st, "", " ") + if err != nil { + panic(err) + } + return "
" + html.EscapeString(string(data)) + "" + }, + } } type EtcdState struct { - Revision int64 `json:",omitempty"` - CompactRevision int64 `json:",omitempty"` - KeyValues map[string]ValueRevision `json:",omitempty"` - KeyLeases map[string]int64 `json:",omitempty"` - Leases map[int64]EtcdLease `json:",omitempty"` + Revision int64 `json:",omitempty"` + CompactRevision int64 `json:",omitempty"` + // Slices below are positionally aligned. If KeyValue is nil on index i, + // it means the key `Keys[i]` doesn't exist. + Keys []string `json:",omitempty"` + KeyValues []*ValueRevision `json:",omitempty"` + KeyLeases []*int64 `json:",omitempty"` + // All leases sorted by LeaseID. + Leases []int64 `json:",omitempty"` } func (s EtcdState) Equal(other EtcdState) bool { @@ -83,13 +88,22 @@ func (s EtcdState) Equal(other EtcdState) bool { if s.CompactRevision != other.CompactRevision { return false } - if !reflect.DeepEqual(s.KeyValues, other.KeyValues) { - return false + if unsafe.SliceData(s.Keys) != unsafe.SliceData(other.Keys) { + panic("Can only compare states created from the same key slice") + } + return slices.EqualFunc(s.KeyValues, other.KeyValues, equalPtr) && + slices.EqualFunc(s.KeyLeases, other.KeyLeases, equalPtr) && + slices.Equal(s.Leases, other.Leases) +} + +func equalPtr[T comparable](a, b *T) bool { + if a == b { + return true } - if !reflect.DeepEqual(s.KeyLeases, other.KeyLeases) { + if a == nil || b == nil { return false } - return reflect.DeepEqual(s.Leases, other.Leases) + return *a == *b } func (s EtcdState) apply(request EtcdRequest, response EtcdResponse) (bool, EtcdState) { @@ -103,25 +117,22 @@ func (s EtcdState) DeepCopy() EtcdState { CompactRevision: s.CompactRevision, } - newState.KeyValues = maps.Clone(s.KeyValues) - newState.KeyLeases = maps.Clone(s.KeyLeases) - - newLeases := map[int64]EtcdLease{} - for key, val := range s.Leases { - newLeases[key] = val.DeepCopy() - } - newState.Leases = newLeases + newState.Keys = s.Keys + newState.KeyValues = slices.Clone(s.KeyValues) + newState.KeyLeases = slices.Clone(s.KeyLeases) + newState.Leases = slices.Clone(s.Leases) return newState } -func freshEtcdState() EtcdState { +func freshEtcdState(keys []string) EtcdState { return EtcdState{ Revision: 1, // Start from CompactRevision equal -1 as etcd allows client to compact revision 0 for some reason. CompactRevision: -1, - KeyValues: map[string]ValueRevision{}, - KeyLeases: map[string]int64{}, - Leases: map[int64]EtcdLease{}, + Keys: keys, + KeyValues: make([]*ValueRevision, len(keys)), + KeyLeases: make([]*int64, len(keys)), + Leases: make([]int64, 0), } } @@ -199,16 +210,14 @@ func (s EtcdState) stepTxn(request EtcdRequest) (EtcdState, MaybeEtcdResponse) { leaseID = &op.Put.LeaseID } ver := int64(1) - valPtr, exists := newState.GetValue(op.Put.Key) - if exists && valPtr.Version > 0 { - ver = valPtr.Version + 1 + if val, exists := newState.GetValue(op.Put.Key); exists && val.Version > 0 { + ver = val.Version + 1 } - val := ValueRevision{ + newState.setValueLease(op.Put.Key, ValueRevision{ Value: op.Put.Value, ModRevision: newState.Revision + 1, Version: ver, - } - newState.setValueLease(op.Put.Key, val, leaseID) + }, leaseID) increaseRevision = true case DeleteOperation: if _, ok := newState.GetValue(op.Delete.Key); ok { @@ -232,30 +241,27 @@ func (s EtcdState) stepLeaseGrant(request EtcdRequest) (EtcdState, MaybeEtcdResp if request.LeaseGrant.LeaseID == 0 { return newState, MaybeEtcdResponse{EtcdResponse: EtcdResponse{Revision: newState.Revision, LeaseGrant: &LeaseGrantResponse{}}} } - lease := EtcdLease{ - LeaseID: request.LeaseGrant.LeaseID, - Keys: map[string]struct{}{}, - } - newState.Leases[request.LeaseGrant.LeaseID] = lease + newState.Leases = append(newState.Leases, request.LeaseGrant.LeaseID) + sort.Slice(newState.Leases, func(i, j int) bool { return newState.Leases[i] < newState.Leases[j] }) return newState, MaybeEtcdResponse{EtcdResponse: EtcdResponse{Revision: newState.Revision, LeaseGrant: &LeaseGrantResponse{}}} } func (s EtcdState) stepLeaseRevoke(request EtcdRequest) (EtcdState, MaybeEtcdResponse) { newState := s.DeepCopy() - // Delete the keys attached to the lease keyDeleted := false - for key := range newState.Leases[request.LeaseRevoke.LeaseID].Keys { - // same as delete. - if _, ok := newState.KeyValues[key]; ok { - if !keyDeleted { - keyDeleted = true - } - delete(newState.KeyValues, key) - delete(newState.KeyLeases, key) + for i, l := range newState.KeyLeases { + if l != nil && *l == request.LeaseRevoke.LeaseID { + keyDeleted = true + newState.KeyValues[i] = nil + newState.KeyLeases[i] = nil + } + } + for i, l := range newState.Leases { + if l == request.LeaseRevoke.LeaseID { + newState.Leases = append(newState.Leases[:i], newState.Leases[i+1:]...) + break } } - // delete the lease - delete(newState.Leases, request.LeaseRevoke.LeaseID) if keyDeleted { newState.Revision++ } @@ -284,9 +290,13 @@ func (s EtcdState) getRange(options RangeOptions) RangeResponse { } if options.End != "" { var count int64 - for k, v := range s.KeyValues { + for i, v := range s.KeyValues { + if v == nil { + continue + } + k := s.Keys[i] if k >= options.Start && k < options.End { - response.KVs = append(response.KVs, KeyValue{Key: k, ValueRevision: v}) + response.KVs = append(response.KVs, KeyValue{Key: k, ValueRevision: *v}) count++ } } @@ -298,11 +308,11 @@ func (s EtcdState) getRange(options RangeOptions) RangeResponse { } response.Count = count } else { - valPtr, ok := s.GetValue(options.Start) + value, ok := s.GetValue(options.Start) if ok { response.KVs = append(response.KVs, KeyValue{ Key: options.Start, - ValueRevision: *valPtr, + ValueRevision: *value, }) response.Count = 1 } @@ -315,52 +325,71 @@ func (s EtcdState) KeysValueLeases() (keys []string, values []ValueRevision, lea values = make([]ValueRevision, 0, len(s.KeyValues)) leases = make([]int64, 0, len(s.KeyLeases)) - for k, v := range s.KeyValues { - keys = append(keys, k) - values = append(values, v) - leases = append(leases, s.KeyLeases[k]) + for i, v := range s.KeyValues { + if v == nil { + continue + } + keys = append(keys, s.Keys[i]) + values = append(values, *v) + lease := int64(0) + if s.KeyLeases[i] != nil { + lease = *s.KeyLeases[i] + } + leases = append(leases, lease) } return keys, values, leases } func (s EtcdState) leases() []int64 { - return slices.Collect(maps.Keys(s.Leases)) + return slices.Clone(s.Leases) } func (s EtcdState) GetValue(key string) (*ValueRevision, bool) { - val, ok := s.KeyValues[key] - if !ok { - return nil, false + for i, k := range s.Keys { + if k == key { + return s.KeyValues[i], s.KeyValues[i] != nil + } } - return &val, true + return nil, false } -func (s EtcdState) setValueLease(key string, val ValueRevision, lease *int64) { - s.KeyValues[key] = val - if oldLeaseID, ok := s.KeyLeases[key]; ok { - delete(s.Leases[oldLeaseID].Keys, key) - } - if lease != nil { - s.KeyLeases[key] = *lease - s.Leases[*lease].Keys[key] = leased - } else { - delete(s.KeyLeases, key) +func (s EtcdState) leaseExists(lease int64) bool { + for _, l := range s.Leases { + if l == lease { + return true + } } + return false } -func (s EtcdState) leaseExists(lease int64) bool { - _, ok := s.Leases[lease] - return ok +func (s EtcdState) setValueLease(key string, val ValueRevision, lease *int64) { + for i, k := range s.Keys { + if k == key { + s.KeyValues[i] = &val + s.KeyLeases[i] = lease + return + } + } + panic(fmt.Sprintf("couldn't find key %s in EtcdState (%v) when calling setValue", key, s.Keys)) } func (s EtcdState) deleteKey(key string) { - delete(s.KeyValues, key) - if oldLeaseID, ok := s.KeyLeases[key]; ok { - delete(s.Leases[oldLeaseID].Keys, key) + for i, k := range s.Keys { + if k == key { + s.KeyValues[i] = nil + s.KeyLeases[i] = nil + return + } } - delete(s.KeyLeases, key) + panic(fmt.Sprintf("couldn't find key %s in EtcdState (%v) when calling setValue", key, s.Keys)) } func (s EtcdState) leaseKeys(leaseID int64) []string { - return slices.Sorted(maps.Keys(s.Leases[leaseID].Keys)) + keys := []string{} + for i, l := range s.KeyLeases { + if l != nil && *l == leaseID { + keys = append(keys, s.Keys[i]) + } + } + return keys } diff --git a/tests/robustness/model/deterministic_test.go b/tests/robustness/model/deterministic_test.go index 3e3c50e458df..f5737e8859d0 100644 --- a/tests/robustness/model/deterministic_test.go +++ b/tests/robustness/model/deterministic_test.go @@ -15,13 +15,11 @@ package model import ( - "encoding/json" "math/rand" "slices" "testing" "github.com/google/go-cmp/cmp" - "github.com/stretchr/testify/require" "go.etcd.io/etcd/api/v3/mvccpb" ) @@ -30,15 +28,15 @@ func TestModelDeterministic(t *testing.T) { for _, tc := range commonTestScenarios { tc := tc t.Run(tc.name, func(t *testing.T) { - state := DeterministicModel.Init() + keys := keysFromTestOperations(tc.operations) + model := DeterministicModel(keys) + state := model.Init() for _, op := range tc.operations { - ok, newState := DeterministicModel.Step(state, op.req, op.resp.EtcdResponse) + ok, newState := model.Step(state, op.req, op.resp.EtcdResponse) if op.expectFailure == ok { t.Logf("state: %v", state) - t.Errorf("Unexpected operation result, expect: %v, got: %v, operation: %s", !op.expectFailure, ok, DeterministicModel.DescribeOperation(op.req, op.resp.EtcdResponse)) - var loadedState EtcdState - err := json.Unmarshal([]byte(state.(string)), &loadedState) - require.NoErrorf(t, err, "Failed to load state") + t.Errorf("Unexpected operation result, expect: %v, got: %v, operation: %s", !op.expectFailure, ok, model.DescribeOperation(op.req, op.resp.EtcdResponse)) + loadedState := state.(EtcdState) _, resp := loadedState.Step(op.req) t.Errorf("Response diff: %s", cmp.Diff(op.resp, resp)) break @@ -53,6 +51,7 @@ func TestModelDeterministic(t *testing.T) { } func TestEtcdStateEqual(t *testing.T) { + keys := []string{"key"} testCases := []struct { name string s1 EtcdState @@ -61,20 +60,20 @@ func TestEtcdStateEqual(t *testing.T) { }{ { name: "Fresh states should be equal", - s1: freshEtcdState(), - s2: freshEtcdState(), + s1: freshEtcdState(keys), + s2: freshEtcdState(keys), equal: true, }, { name: "States from identical history should be equal", s1: func() EtcdState { - s := freshEtcdState() + s := freshEtcdState(keys) s, _ = s.Step(putRequest("key", "1")) s, _ = s.Step(putRequest("key", "2")) return s }(), s2: func() EtcdState { - s := freshEtcdState() + s := freshEtcdState(keys) s, _ = s.Step(putRequest("key", "1")) s, _ = s.Step(putRequest("key", "2")) return s @@ -84,13 +83,13 @@ func TestEtcdStateEqual(t *testing.T) { { name: "States from different history should not be equal", s1: func() EtcdState { - s := freshEtcdState() + s := freshEtcdState(keys) s, _ = s.Step(putRequest("key", "1")) s, _ = s.Step(putRequest("key", "2")) return s }(), s2: func() EtcdState { - s := freshEtcdState() + s := freshEtcdState(keys) s, _ = s.Step(putRequest("key", "2")) s, _ = s.Step(putRequest("key", "1")) return s @@ -100,14 +99,14 @@ func TestEtcdStateEqual(t *testing.T) { { name: "Empty states with higher revision should be equal", s1: func() EtcdState { - s := freshEtcdState() + s := freshEtcdState(keys) s, _ = s.Step(putRequest("key", "1")) s, _ = s.Step(putRequest("key", "2")) s, _ = s.Step(deleteRequest("key")) return s }(), s2: func() EtcdState { - s := freshEtcdState() + s := freshEtcdState(keys) s, _ = s.Step(putRequest("key", "2")) s, _ = s.Step(putRequest("key", "1")) s, _ = s.Step(deleteRequest("key")) @@ -117,9 +116,9 @@ func TestEtcdStateEqual(t *testing.T) { }, { name: "Grant and Revoke empty lease should be equal to fresh state", - s1: freshEtcdState(), + s1: freshEtcdState(keys), s2: func() EtcdState { - s := freshEtcdState() + s := freshEtcdState(keys) s, _ = s.Step(leaseGrantRequest(1)) s, _ = s.Step(leaseRevokeRequest(1)) return s @@ -129,14 +128,14 @@ func TestEtcdStateEqual(t *testing.T) { { name: "Delete via Revoke vs Delete directly should be equal", s1: func() EtcdState { - s := freshEtcdState() + s := freshEtcdState(keys) s, _ = s.Step(leaseGrantRequest(1)) s, _ = s.Step(putWithLeaseRequest("key", "val", 1)) s, _ = s.Step(leaseRevokeRequest(1)) return s }(), s2: func() EtcdState { - s := freshEtcdState() + s := freshEtcdState(keys) s, _ = s.Step(putRequest("key", "val")) s, _ = s.Step(deleteRequest("key")) return s @@ -146,12 +145,12 @@ func TestEtcdStateEqual(t *testing.T) { { name: "Put via Txn vs Put directly should be equal", s1: func() EtcdState { - s := freshEtcdState() + s := freshEtcdState(keys) s, _ = s.Step(compareRevisionAndPutRequest("key", 0, "val")) return s }(), s2: func() EtcdState { - s := freshEtcdState() + s := freshEtcdState(keys) s, _ = s.Step(putRequest("key", "val")) return s }(), @@ -182,15 +181,16 @@ func TestEtcdStateEqualCommutativeRequests(t *testing.T) { compactRequest(1), compactRequest(2), } + keys := []string{"key1", "key2"} - baseState := applyRequests(commutativeRequests) + baseState := applyRequests(keys, commutativeRequests) for i := 0; i < 10_000; i++ { perm := slices.Clone(commutativeRequests) rand.Shuffle(len(perm), func(i, j int) { perm[i], perm[j] = perm[j], perm[i] }) - s2 := applyRequests(perm) + s2 := applyRequests(keys, perm) if !baseState.Equal(s2) { t.Errorf("Expected states to be equal after random reordering, but they are not") @@ -198,14 +198,22 @@ func TestEtcdStateEqualCommutativeRequests(t *testing.T) { } } -func applyRequests(reqs []EtcdRequest) EtcdState { - state := freshEtcdState() +func applyRequests(keys []string, reqs []EtcdRequest) EtcdState { + state := freshEtcdState(keys) for _, req := range reqs { state, _ = state.Step(req) } return state } +func keysFromTestOperations(ops []testOperation) []string { + requests := make([]EtcdRequest, 0, len(ops)) + for _, op := range ops { + requests = append(requests, op.req) + } + return keysFromRequests(requests) +} + type modelTestCase struct { name string operations []testOperation diff --git a/tests/robustness/model/non_deterministic.go b/tests/robustness/model/non_deterministic.go index 65706ecd9e85..3fbdca7b3995 100644 --- a/tests/robustness/model/non_deterministic.go +++ b/tests/robustness/model/non_deterministic.go @@ -17,7 +17,6 @@ package model import ( "cmp" "fmt" - "reflect" "slices" "strings" @@ -28,47 +27,44 @@ import ( // An unknown/error response doesn't inform whether the request was persisted or not, so the model // considers both cases. This is represented as multiple, equally possible deterministic states. // Failed requests fork the possible states, while successful requests merge and filter them. -var NonDeterministicModel = porcupine.Model{ - Init: func() any { - return nonDeterministicState{freshEtcdState()} - }, - Step: func(st any, in any, out any) (bool, any) { - return st.(nonDeterministicState).apply(in.(EtcdRequest), out.(MaybeEtcdResponse)) - }, - Equal: func(st1, st2 any) bool { - return st1.(nonDeterministicState).Equal(st2.(nonDeterministicState)) - }, - DescribeOperation: func(in, out any) string { - return fmt.Sprintf("%s -> %s", describeEtcdRequest(in.(EtcdRequest)), describeEtcdResponse(in.(EtcdRequest), out.(MaybeEtcdResponse))) - }, - DescribeOperationMetadata: func(info any) string { - if info == nil { - return "" - } - return DescribeOperationMetadata(info.(MaybeEtcdResponse)) - }, - DescribeState: func(st any) string { - etcdStates := st.(nonDeterministicState) - desc := make([]string, 0, len(etcdStates)) - - slices.SortFunc(etcdStates, func(i, j EtcdState) int { - if c := cmp.Compare(i.Revision, j.Revision); c != 0 { - return c +var NonDeterministicModel = func(keys []string) porcupine.Model { + return porcupine.Model{ + Init: func() any { + return nonDeterministicState{freshEtcdState(keys)} + }, + Step: func(st any, in any, out any) (bool, any) { + return st.(nonDeterministicState).apply(in.(EtcdRequest), out.(MaybeEtcdResponse)) + }, + Equal: func(st1, st2 any) bool { + return st1.(nonDeterministicState).Equal(st2.(nonDeterministicState)) + }, + DescribeOperation: func(in, out any) string { + return fmt.Sprintf("%s -> %s", describeEtcdRequest(in.(EtcdRequest)), describeEtcdResponse(in.(EtcdRequest), out.(MaybeEtcdResponse))) + }, + DescribeOperationMetadata: func(info any) string { + if info == nil { + return "" } - return cmp.Compare(i.CompactRevision, j.CompactRevision) - }) - - for i, s := range etcdStates { - // Describe just 3 first states before truncating - if i >= 3 { - desc = append(desc, "...truncated...") - break + return DescribeOperationMetadata(info.(MaybeEtcdResponse)) + }, + DescribeState: func(st any) string { + etcdStates := st.(nonDeterministicState) + desc := make([]string, 0, len(etcdStates)) + + slices.SortFunc(etcdStates, compareStates) + + for i, s := range etcdStates { + // Describe just 3 first states before truncating + if i >= 3 { + desc = append(desc, "...truncated...") + break + } + desc = append(desc, describeEtcdState(s)) } - desc = append(desc, describeEtcdState(s)) - } - return strings.Join(desc, "\n") - }, + return strings.Join(desc, "\n") + }, + } } type nonDeterministicState []EtcdState @@ -77,24 +73,78 @@ func (states nonDeterministicState) Equal(other nonDeterministicState) bool { if len(states) != len(other) { return false } + slices.SortFunc(states, compareStates) + slices.SortFunc(other, compareStates) - otherMatched := make([]bool, len(other)) - for _, sItem := range states { - foundMatchInOther := false - for j, otherItem := range other { - if !otherMatched[j] && sItem.Equal(otherItem) { - otherMatched[j] = true - foundMatchInOther = true - break - } - } - if !foundMatchInOther { + for i := range states { + if !states[i].Equal(other[i]) { return false } } return true } +func compareStates(first, second EtcdState) int { + if c := cmp.Compare(first.Revision, second.Revision); c != 0 { + return c + } + if c := cmp.Compare(first.CompactRevision, second.CompactRevision); c != 0 { + return c + } + if c := cmp.Compare(len(first.KeyValues), len(second.KeyValues)); c != 0 { + return c + } + for i := range first.KeyValues { + if (first.KeyValues[i] == nil) != (second.KeyValues[i] == nil) { + if first.KeyValues[i] == nil { + return -1 + } + return 1 + } + if first.KeyValues[i] == nil { + continue + } + if c := cmp.Compare(first.KeyValues[i].ModRevision, second.KeyValues[i].ModRevision); c != 0 { + return c + } + if c := cmp.Compare(first.KeyValues[i].Version, second.KeyValues[i].Version); c != 0 { + return c + } + if c := cmp.Compare(first.KeyValues[i].Value.Value, second.KeyValues[i].Value.Value); c != 0 { + return c + } + if c := cmp.Compare(first.KeyValues[i].Value.Hash, second.KeyValues[i].Value.Hash); c != 0 { + return c + } + } + if c := cmp.Compare(len(first.KeyLeases), len(second.KeyLeases)); c != 0 { + return c + } + for i := range first.KeyLeases { + if (first.KeyLeases[i] == nil) != (second.KeyLeases[i] == nil) { + if first.KeyLeases[i] == nil { + return -1 + } + return 1 + } + if first.KeyLeases[i] == nil { + continue + } + if c := cmp.Compare(*first.KeyLeases[i], *second.KeyLeases[i]); c != 0 { + return c + } + } + if c := cmp.Compare(len(first.Leases), len(second.Leases)); c != 0 { + return c + } + for i := range first.Leases { + if c := cmp.Compare(first.Leases[i], second.Leases[i]); c != 0 { + return c + } + } + return 0 +} + func (states nonDeterministicState) apply(request EtcdRequest, response MaybeEtcdResponse) (bool, nonDeterministicState) { var newStates nonDeterministicState switch { @@ -116,7 +166,7 @@ func (states nonDeterministicState) applyFailedRequest(request EtcdRequest) nonD for _, s := range states { newStates = append(newStates, s) newState, _ := s.Step(request) - if !reflect.DeepEqual(newState, s) { + if !newState.Equal(s) { newStates = append(newStates, newState) } } diff --git a/tests/robustness/model/non_deterministic_test.go b/tests/robustness/model/non_deterministic_test.go index c53c013265bc..1960f75e9c7c 100644 --- a/tests/robustness/model/non_deterministic_test.go +++ b/tests/robustness/model/non_deterministic_test.go @@ -15,13 +15,11 @@ package model import ( - "encoding/json" "errors" "testing" "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" "go.etcd.io/etcd/api/v3/mvccpb" ) @@ -328,15 +326,15 @@ func TestModelNonDeterministic(t *testing.T) { for _, tc := range nonDeterministicTestScenarios { tc := tc t.Run(tc.name, func(t *testing.T) { - state := NonDeterministicModel.Init() + keys := keysFromTestOperations(tc.operations) + model := NonDeterministicModel(keys) + state := model.Init() for _, op := range tc.operations { - ok, newState := NonDeterministicModel.Step(state, op.req, op.resp) + ok, newState := model.Step(state, op.req, op.resp) if ok != !op.expectFailure { t.Logf("state: %v", state) - t.Errorf("Unexpected operation result, expect: %v, got: %v, operation: %s", !op.expectFailure, ok, NonDeterministicModel.DescribeOperation(op.req, op.resp)) - var loadedState nonDeterministicState - err := json.Unmarshal([]byte(state.(string)), &loadedState) - require.NoErrorf(t, err, "Failed to load state") + t.Errorf("Unexpected operation result, expect: %v, got: %v, operation: %s", !op.expectFailure, ok, model.DescribeOperation(op.req, op.resp)) + loadedState := state.(nonDeterministicState) for i, s := range loadedState { _, resp := s.Step(op.req) t.Errorf("For state %d, response diff: %s", i, cmp.Diff(op.resp, resp)) diff --git a/tests/robustness/model/replay.go b/tests/robustness/model/replay.go index 8edd121b6fab..a9f3653b1db5 100644 --- a/tests/robustness/model/replay.go +++ b/tests/robustness/model/replay.go @@ -16,6 +16,8 @@ package model import ( "fmt" + "maps" + "slices" "strings" "github.com/anishathalye/porcupine" @@ -30,7 +32,8 @@ func NewReplayFromOperations(ops []porcupine.Operation) *EtcdReplay { } func NewReplay(persistedRequests []EtcdRequest) *EtcdReplay { - state := freshEtcdState() + keys := keysFromRequests(persistedRequests) + state := freshEtcdState(keys) // Padding for index 0 and 1, so the index matches the revision. revisionToEtcdState := []EtcdState{state, state} var events []PersistedEvent @@ -165,3 +168,41 @@ type WatchRequest struct { WithProgressNotify bool WithPrevKV bool } + +func ModelKeys(operations []porcupine.Operation) []string { + requests := []EtcdRequest{} + for _, op := range operations { + requests = append(requests, op.Input.(EtcdRequest)) + } + return keysFromRequests(requests) +} + +func keysFromRequests(requests []EtcdRequest) []string { + keysMap := map[string]bool{} + for _, request := range requests { + switch request.Type { + case Range: + keysMap[request.Range.Start] = true + if request.Range.End != "" { + keysMap[request.Range.End] = true + } + case Txn: + for _, op := range slices.Concat(request.Txn.OperationsOnSuccess, request.Txn.OperationsOnFailure) { + switch op.Type { + case RangeOperation: + keysMap[op.Range.Start] = true + if op.Range.End != "" { + keysMap[op.Range.End] = true + } + case PutOperation: + keysMap[op.Put.Key] = true + case DeleteOperation: + keysMap[op.Delete.Key] = true + } + } + } + } + keys := slices.Collect(maps.Keys(keysMap)) + slices.Sort(keys) + return keys +} diff --git a/tests/robustness/model/types.go b/tests/robustness/model/types.go index e52e40568086..74eaa71a14d1 100644 --- a/tests/robustness/model/types.go +++ b/tests/robustness/model/types.go @@ -18,7 +18,6 @@ import ( "encoding/json" "errors" "hash/fnv" - "maps" "reflect" "slices" @@ -221,20 +220,6 @@ type KeyValue struct { ValueRevision } -var leased = struct{}{} - -type EtcdLease struct { - LeaseID int64 - Keys map[string]struct{} -} - -func (el EtcdLease) DeepCopy() EtcdLease { - return EtcdLease{ - LeaseID: el.LeaseID, - Keys: maps.Clone(el.Keys), - } -} - type ValueRevision struct { Value ValueOrHash `json:",omitempty"` ModRevision int64 `json:",omitempty"` diff --git a/tests/robustness/validate/operations.go b/tests/robustness/validate/operations.go index 900127216bbd..419e2f3ef56f 100644 --- a/tests/robustness/validate/operations.go +++ b/tests/robustness/validate/operations.go @@ -30,13 +30,15 @@ var ( errFutureRevRespRequested = errors.New("request about a future rev with response") ) -func validateLinearizableOperationsAndVisualize(lg *zap.Logger, operations []porcupine.Operation, timeout time.Duration) LinearizationResult { +func validateLinearizableOperationsAndVisualize(lg *zap.Logger, keys []string, operations []porcupine.Operation, timeout time.Duration) LinearizationResult { lg.Info("Validating linearizable operations", zap.Duration("timeout", timeout)) start := time.Now() - check, info := porcupine.CheckOperationsVerbose(model.NonDeterministicModel, operations, timeout) + + model := model.NonDeterministicModel(keys) + check, info := porcupine.CheckOperationsVerbose(model, operations, timeout) result := LinearizationResult{ Info: info, - Model: model.NonDeterministicModel, + Model: model, } switch check { case porcupine.Ok: diff --git a/tests/robustness/validate/operations_test.go b/tests/robustness/validate/operations_test.go index 9033e9a616c0..ab7fce592777 100644 --- a/tests/robustness/validate/operations_test.go +++ b/tests/robustness/validate/operations_test.go @@ -317,9 +317,10 @@ func BenchmarkValidateLinearizableOperations(b *testing.B) { b.Run("BacktrackingHeavy", func(b *testing.B) { history := backtrackingHeavy(b) shuffles := shuffleHistory(history, b.N) + keys := model.ModelKeys(history) b.ResetTimer() for i := 0; i < len(shuffles); i++ { - validateLinearizableOperationsAndVisualize(lg, shuffles[i], time.Second) + validateLinearizableOperationsAndVisualize(lg, keys, shuffles[i], time.Second) } }) } @@ -449,8 +450,9 @@ func shuffleHistory(history []porcupine.Operation, shuffleCount int) [][]porcupi } func validateShuffles(b *testing.B, lg *zap.Logger, shuffles [][]porcupine.Operation, duration time.Duration) { + keys := model.ModelKeys(shuffles[0]) for i := 0; i < len(shuffles); i++ { - result := validateLinearizableOperationsAndVisualize(lg, shuffles[i], duration) + result := validateLinearizableOperationsAndVisualize(lg, keys, shuffles[i], duration) if err := result.Error(); err != nil { b.Fatalf("Not linearizable: %v", err) } diff --git a/tests/robustness/validate/validate.go b/tests/robustness/validate/validate.go index 775eb0d62c52..0a8b370c2abb 100644 --- a/tests/robustness/validate/validate.go +++ b/tests/robustness/validate/validate.go @@ -41,8 +41,8 @@ func ValidateAndReturnVisualize(lg *zap.Logger, cfg Config, reports []report.Cli if len(persistedRequests) != 0 { linearizableOperations = patchLinearizableOperations(linearizableOperations, reports, persistedRequests) } - - result.Linearization = validateLinearizableOperationsAndVisualize(lg, linearizableOperations, timeout) + keys := model.ModelKeys(linearizableOperations) + result.Linearization = validateLinearizableOperationsAndVisualize(lg, keys, linearizableOperations, timeout) result.Linearization.AddToVisualization(operationsForVisualization) // Skip other validations if model is not linearizable, as they are expected to fail too and obfuscate the logs. if result.Linearization.Error() != nil {