| // Copyright 2025 The etcd Authors |
| // |
| // 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 cache |
| |
| import ( |
| "errors" |
| "testing" |
| |
| "github.com/google/go-cmp/cmp" |
| "google.golang.org/protobuf/testing/protocmp" |
| |
| mvccpb "go.etcd.io/etcd/api/v3/mvccpb" |
| clientv3 "go.etcd.io/etcd/client/v3" |
| ) |
| |
| func TestStoreGet(t *testing.T) { |
| tests := []struct { |
| name string |
| initialKVs []*mvccpb.KeyValue |
| initialRev int64 |
| |
| start []byte |
| end []byte |
| |
| expectedKVs []*mvccpb.KeyValue |
| expectedRev int64 |
| expectedErr error |
| }{ |
| { |
| name: "empty_store_returns_ErrNotReady", |
| initialKVs: nil, |
| start: []byte("a"), |
| expectedErr: ErrNotReady, |
| }, |
| { |
| name: "Get_single_key_hit", |
| initialKVs: []*mvccpb.KeyValue{makeKV("/b", "2", 5), makeKV("/a", "1", 5), makeKV("/c", "3", 5)}, |
| initialRev: 5, |
| start: []byte("/b"), |
| expectedKVs: []*mvccpb.KeyValue{makeKV("/b", "2", 5)}, |
| expectedRev: 5, |
| }, |
| { |
| name: "Get_single_key_miss_returns_empty", |
| initialKVs: []*mvccpb.KeyValue{makeKV("/b", "2", 5), makeKV("/a", "1", 5), makeKV("/c", "3", 5)}, |
| initialRev: 5, |
| start: []byte("/zzz"), |
| expectedKVs: nil, |
| expectedRev: 5, |
| }, |
| { |
| name: "Get_explicit_range", |
| initialKVs: []*mvccpb.KeyValue{makeKV("/a", "1", 10), makeKV("/b", "2", 10), makeKV("/c", "3", 10), makeKV("/d", "4", 10)}, |
| initialRev: 10, |
| start: []byte("/b"), |
| end: []byte("/d"), |
| expectedKVs: []*mvccpb.KeyValue{makeKV("/b", "2", 10), makeKV("/c", "3", 10)}, |
| expectedRev: 10, |
| }, |
| { |
| name: "Get_range_includes_prefix_excludes_end", |
| initialKVs: []*mvccpb.KeyValue{makeKV("/a", "1", 4), makeKV("/aa", "2", 4), makeKV("/ab", "3", 4), makeKV("/b", "4", 4)}, |
| initialRev: 4, |
| start: []byte("/a"), |
| end: []byte("/b"), |
| expectedKVs: []*mvccpb.KeyValue{makeKV("/a", "1", 4), makeKV("/aa", "2", 4), makeKV("/ab", "3", 4)}, |
| expectedRev: 4, |
| }, |
| { |
| name: "Get_empty_range_returns_empty", |
| initialKVs: []*mvccpb.KeyValue{makeKV("/a", "1", 2), makeKV("/b", "2", 2)}, |
| initialRev: 2, |
| start: []byte("/a"), |
| end: []byte("/a"), |
| expectedKVs: nil, |
| expectedRev: 2, |
| }, |
| { |
| name: "Get_invalid_range_returns_empty", |
| initialKVs: []*mvccpb.KeyValue{makeKV("/a", "1", 6), makeKV("/z", "9", 6)}, |
| initialRev: 6, |
| start: []byte("/z"), |
| end: []byte("/a"), |
| expectedKVs: nil, |
| expectedRev: 6, |
| }, |
| { |
| name: "Get_fromKey_scans_ordered", |
| initialKVs: []*mvccpb.KeyValue{makeKV("/a", "1", 7), makeKV("/b", "2", 7), makeKV("/c", "3", 7), makeKV("/d", "4", 7)}, |
| initialRev: 7, |
| start: []byte("/b"), |
| end: []byte{0}, |
| expectedKVs: []*mvccpb.KeyValue{makeKV("/b", "2", 7), makeKV("/c", "3", 7), makeKV("/d", "4", 7)}, |
| expectedRev: 7, |
| }, |
| { |
| name: "Get_fromKey_with_no_results", |
| initialKVs: []*mvccpb.KeyValue{makeKV("/a", "1", 9), makeKV("/b", "2", 9)}, |
| initialRev: 9, |
| start: []byte("/zzz"), |
| end: []byte{0}, |
| expectedKVs: nil, |
| expectedRev: 9, |
| }, |
| } |
| |
| for _, tt := range tests { |
| test := tt |
| t.Run(test.name, func(t *testing.T) { |
| s := newStore(8, 32) |
| if test.initialKVs != nil { |
| s.Restore(test.initialKVs, test.initialRev) |
| } |
| |
| kvs, rev, err := s.Get(test.start, test.end, 0) |
| |
| if test.expectedErr != nil { |
| if !errors.Is(err, test.expectedErr) { |
| t.Fatalf("Get error = %v; want %v", err, test.expectedErr) |
| } |
| return |
| } |
| if err != nil { |
| t.Fatalf("Get returned unexpected error: %v", err) |
| } |
| if rev != test.expectedRev { |
| t.Fatalf("revision=%d; want %d", rev, test.expectedRev) |
| } |
| |
| if diff := cmp.Diff(test.expectedKVs, kvs, protocmp.Transform()); diff != "" { |
| t.Fatalf("Get mismatch (-want +got):\n%s", diff) |
| } |
| }) |
| } |
| } |
| |
| func TestStoreApply(t *testing.T) { |
| type testCase struct { |
| name string |
| initialKVs []*mvccpb.KeyValue |
| initialRev int64 |
| eventBatches [][]*clientv3.Event |
| |
| expectedLatestRev int64 |
| expectedSnapshot []*mvccpb.KeyValue |
| expectErr bool |
| } |
| tests := []testCase{ |
| { |
| name: "put_overwrites_key", |
| initialKVs: []*mvccpb.KeyValue{makeKV("/k", "v1", 10)}, |
| initialRev: 10, |
| eventBatches: [][]*clientv3.Event{{makePutEvent("/k", "v2", 11)}}, |
| expectedLatestRev: 11, |
| expectedSnapshot: []*mvccpb.KeyValue{makeKV("/k", "v2", 11)}, |
| }, |
| { |
| name: "put_contiguous_revision", |
| initialKVs: []*mvccpb.KeyValue{makeKV("/a", "A1", 20)}, |
| initialRev: 20, |
| eventBatches: [][]*clientv3.Event{ |
| {makePutEvent("/a", "A2", 21)}, |
| {makePutEvent("/b", "B1", 22)}, |
| {makePutEvent("/c", "C1", 23)}, |
| }, |
| expectedLatestRev: 23, |
| expectedSnapshot: []*mvccpb.KeyValue{makeKV("/a", "A2", 21), makeKV("/b", "B1", 22), makeKV("/c", "C1", 23)}, |
| }, |
| { |
| name: "put_single_non_contiguous_batch", |
| initialKVs: []*mvccpb.KeyValue{makeKV("/a", "A1", 20)}, |
| initialRev: 20, |
| eventBatches: [][]*clientv3.Event{{makePutEvent("/a", "A2", 25)}}, |
| expectedLatestRev: 25, |
| expectedSnapshot: []*mvccpb.KeyValue{makeKV("/a", "A2", 25)}, |
| }, |
| { |
| name: "put_multiple_non_contiguous_batches", |
| initialKVs: []*mvccpb.KeyValue{makeKV("/a", "A1", 21), makeKV("/b", "B1", 22)}, |
| initialRev: 22, |
| eventBatches: [][]*clientv3.Event{ |
| {makePutEvent("/a", "A2", 25)}, |
| {makePutEvent("/b", "B2", 27)}, |
| }, |
| expectedLatestRev: 27, |
| expectedSnapshot: []*mvccpb.KeyValue{makeKV("/a", "A2", 25), makeKV("/b", "B2", 27)}, |
| }, |
| { |
| name: "apply_mixed_operations", |
| initialKVs: []*mvccpb.KeyValue{makeKV("/a", "A1", 20)}, |
| initialRev: 20, |
| eventBatches: [][]*clientv3.Event{ |
| {makePutEvent("/a", "A2", 21), makePutEvent("/b", "B1", 21), makePutEvent("/c", "C1", 21)}, |
| {makePutEvent("/b", "B2", 22)}, |
| {makeDelEvent("/c", 23), makePutEvent("/a", "A3", 23)}, |
| {makePutEvent("/b", "B3", 24)}, |
| }, |
| expectedLatestRev: 24, |
| expectedSnapshot: []*mvccpb.KeyValue{makeKV("/a", "A3", 23), makeKV("/b", "B3", 24)}, |
| }, |
| { |
| name: "delete_same_key", |
| initialKVs: []*mvccpb.KeyValue{makeKV("/a", "X", 10)}, |
| initialRev: 10, |
| eventBatches: [][]*clientv3.Event{ |
| {makeDelEvent("/a", 11)}, |
| }, |
| expectedLatestRev: 11, |
| expectedSnapshot: nil, |
| }, |
| { |
| name: "delete_nonexistent_returns_error", |
| initialKVs: []*mvccpb.KeyValue{makeKV("/p", "X", 5)}, |
| initialRev: 5, |
| eventBatches: [][]*clientv3.Event{{makeDelEvent("/zzz", 6)}}, |
| expectedLatestRev: 5, |
| expectedSnapshot: []*mvccpb.KeyValue{makeKV("/p", "X", 5)}, |
| expectErr: true, |
| }, |
| { |
| name: "mixed_delete_nonexistent_returns_error", |
| initialKVs: []*mvccpb.KeyValue{makeKV("/p", "X", 5)}, |
| initialRev: 5, |
| eventBatches: [][]*clientv3.Event{{makeDelEvent("/zzz", 6), makePutEvent("/r", "Y", 6)}}, |
| expectedLatestRev: 5, |
| expectedSnapshot: []*mvccpb.KeyValue{makeKV("/p", "X", 5)}, |
| expectErr: true, |
| }, |
| { |
| name: "delete_then_delete_again_returns_error", |
| initialKVs: []*mvccpb.KeyValue{makeKV("/p", "X", 5)}, |
| initialRev: 5, |
| eventBatches: [][]*clientv3.Event{ |
| {makeDelEvent("/p", 6)}, |
| {makeDelEvent("/p", 7)}, |
| }, |
| expectedLatestRev: 6, |
| expectedSnapshot: nil, |
| expectErr: true, |
| }, |
| { |
| name: "stale_batch_rejected", |
| initialKVs: []*mvccpb.KeyValue{makeKV("/x", "1", 20)}, |
| initialRev: 20, |
| eventBatches: [][]*clientv3.Event{{makePutEvent("/x", "2", 19)}}, |
| expectedLatestRev: 20, |
| expectedSnapshot: []*mvccpb.KeyValue{makeKV("/x", "1", 20)}, |
| expectErr: true, |
| }, |
| { |
| name: "mixed_stale_batch_returns_error", |
| initialKVs: []*mvccpb.KeyValue{makeKV("/x", "1", 20)}, |
| initialRev: 20, |
| eventBatches: [][]*clientv3.Event{ |
| {makePutEvent("/x", "should-not-apply", 19)}, |
| {makeDelEvent("/x", 21), makePutEvent("/y", "new", 21)}, |
| {makeDelEvent("/y", 22)}, |
| }, |
| expectedLatestRev: 20, |
| expectedSnapshot: []*mvccpb.KeyValue{makeKV("/x", "1", 20)}, |
| expectErr: true, |
| }, |
| } |
| |
| for _, tt := range tests { |
| test := tt |
| t.Run(test.name, func(t *testing.T) { |
| s := newStore(4, 32) |
| s.Restore(test.initialKVs, test.initialRev) |
| |
| var gotErr error |
| for batchIndex, batch := range test.eventBatches { |
| resp := clientv3.WatchResponse{Events: batch} |
| if err := s.Apply(resp); err != nil { |
| gotErr = err |
| if !test.expectErr { |
| t.Fatalf("Apply(batch %d) unexpected error: %v", batchIndex, err) |
| } |
| break |
| } |
| } |
| if test.expectErr && gotErr == nil { |
| t.Fatalf("expected Apply() to error, but got nil") |
| } |
| if latest := s.LatestRev(); latest != test.expectedLatestRev { |
| t.Fatalf("LatestRev=%d; want %d", latest, test.expectedLatestRev) |
| } |
| verifyStoreSnapshot(t, s, test.expectedSnapshot, test.expectedLatestRev, 0) |
| }) |
| } |
| } |
| |
| func TestStoreRestore(t *testing.T) { |
| type restoreSeq struct { |
| kvs []*mvccpb.KeyValue |
| rev int64 |
| } |
| tests := []struct { |
| name string |
| seq []restoreSeq |
| expectedSnap []*mvccpb.KeyValue |
| expectedRev int64 |
| }{ |
| { |
| name: "rebuilds_tree_and_resets_rev", |
| seq: []restoreSeq{ |
| {[]*mvccpb.KeyValue{makeKV("/a", "1", 3), makeKV("/b", "2", 3)}, 3}, |
| {[]*mvccpb.KeyValue{makeKV("/c", "3", 15)}, 15}, |
| }, |
| expectedSnap: []*mvccpb.KeyValue{makeKV("/c", "3", 15)}, |
| expectedRev: 15, |
| }, |
| { |
| name: "restore_to_revision_zero_returns_ErrNotReady", |
| seq: []restoreSeq{ |
| {[]*mvccpb.KeyValue{makeKV("/a", "1", 5)}, 5}, |
| {nil, 0}, |
| }, |
| expectedSnap: nil, |
| expectedRev: 0, |
| }, |
| { |
| name: "restore_empty_ready", |
| seq: []restoreSeq{{nil, 5}}, |
| expectedSnap: nil, |
| expectedRev: 5, |
| }, |
| } |
| |
| for _, tt := range tests { |
| t.Run(tt.name, func(t *testing.T) { |
| s := newStore(8, 32) |
| for _, step := range tt.seq { |
| s.Restore(step.kvs, step.rev) |
| } |
| if tt.expectedRev == 0 { |
| if _, _, err := s.Get([]byte("/"), []byte{0}, 0); !errors.Is(err, ErrNotReady) { |
| t.Fatalf("Get after restore to rev=0 err=%v; want %v", err, ErrNotReady) |
| } |
| return |
| } |
| verifyStoreSnapshot(t, s, tt.expectedSnap, tt.expectedRev, 0) |
| }) |
| } |
| } |
| |
| func TestRestoreAppendCloneImmutability(t *testing.T) { |
| tests := []struct { |
| name string |
| initialKVs []*mvccpb.KeyValue |
| initialRev int64 |
| events []*clientv3.Event |
| requestedRev int64 |
| expectedSnap []*mvccpb.KeyValue |
| expectedLatestSnap []*mvccpb.KeyValue |
| expectedLatestRev int64 |
| }{ |
| { |
| name: "put_overwrites_key", |
| initialKVs: []*mvccpb.KeyValue{makeKV("/k", "v1", 5)}, |
| initialRev: 5, |
| events: []*clientv3.Event{makePutEvent("/k", "v2", 6)}, |
| |
| requestedRev: 5, |
| expectedSnap: []*mvccpb.KeyValue{makeKV("/k", "v1", 5)}, |
| expectedLatestSnap: []*mvccpb.KeyValue{makeKV("/k", "v2", 6)}, |
| expectedLatestRev: 6, |
| }, |
| { |
| name: "delete_key", |
| initialKVs: []*mvccpb.KeyValue{makeKV("/k", "v1", 5)}, |
| initialRev: 5, |
| events: []*clientv3.Event{makeDelEvent("/k", 6)}, |
| |
| requestedRev: 5, |
| expectedSnap: []*mvccpb.KeyValue{makeKV("/k", "v1", 5)}, |
| expectedLatestSnap: nil, |
| expectedLatestRev: 6, |
| }, |
| } |
| |
| for _, tt := range tests { |
| test := tt |
| t.Run(test.name, func(t *testing.T) { |
| s := newStore(8, 32) |
| if test.initialKVs != nil { |
| s.Restore(test.initialKVs, test.initialRev) |
| } |
| if len(test.events) > 0 { |
| resp := clientv3.WatchResponse{Events: test.events} |
| if err := s.Apply(resp); err != nil { |
| t.Fatalf("Apply failed: %v", err) |
| } |
| } |
| |
| if test.requestedRev != 0 { |
| verifyStoreSnapshot(t, s, test.expectedSnap, test.expectedLatestRev, test.requestedRev) |
| } |
| verifyStoreSnapshot(t, s, test.expectedLatestSnap, test.expectedLatestRev, test.expectedLatestRev) |
| }) |
| } |
| } |
| |
| func makeKV(key, val string, rev int64) *mvccpb.KeyValue { |
| return &mvccpb.KeyValue{Key: []byte(key), Value: []byte(val), ModRevision: rev, CreateRevision: rev, Version: 1} |
| } |
| |
| func makePutEvent(key, val string, rev int64) *clientv3.Event { |
| return &clientv3.Event{Type: clientv3.EventTypePut, Kv: &mvccpb.KeyValue{Key: []byte(key), Value: []byte(val), ModRevision: rev, CreateRevision: rev, Version: 1}} |
| } |
| |
| func makeDelEvent(key string, rev int64) *clientv3.Event { |
| return &clientv3.Event{Type: clientv3.EventTypeDelete, Kv: &mvccpb.KeyValue{Key: []byte(key), ModRevision: rev}} |
| } |
| |
| func verifyStoreSnapshot(t *testing.T, s *store, want []*mvccpb.KeyValue, wantRev int64, requestedRev int64) { |
| kvs, headerRev, err := s.Get([]byte("/"), []byte{0}, requestedRev) |
| if err != nil { |
| t.Fatalf("Get all keys (rev=%d): got error: %v", requestedRev, err) |
| } |
| latestRev := s.LatestRev() |
| if headerRev != latestRev { |
| t.Fatalf("header rev=%d; want latest %d (requestedRev=%d)", latestRev, wantRev, requestedRev) |
| } |
| if diff := cmp.Diff(want, kvs, protocmp.Transform()); diff != "" { |
| t.Fatalf("snapshot mismatch (requestedRev=%d) (-want +got):\n%s", requestedRev, diff) |
| } |
| } |