Skip to content

Commit 56231c5

Browse files
committed
tests: migrate hashkv tests(e2e/integration) to common test framework
Signed-off-by: Kota <[email protected]>
1 parent 57f21e2 commit 56231c5

File tree

3 files changed

+248
-315
lines changed

3 files changed

+248
-315
lines changed

tests/common/hashkv_test.go

Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
// Copyright 2025 The etcd Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package common
16+
17+
import (
18+
"context"
19+
"fmt"
20+
"testing"
21+
"time"
22+
23+
"github.com/stretchr/testify/assert"
24+
"github.com/stretchr/testify/require"
25+
26+
"go.etcd.io/etcd/tests/v3/framework/config"
27+
intf "go.etcd.io/etcd/tests/v3/framework/interfaces"
28+
"go.etcd.io/etcd/tests/v3/framework/testutils"
29+
)
30+
31+
// TestVerifyHashKVAfterCompact tests that HashKV is consistent across all members after a physical compaction.
32+
// It tests both cases where the compaction is on a tombstone revision and where it is not.
33+
func TestVerifyHashKVAfterCompact(t *testing.T) {
34+
testRunner.BeforeTest(t)
35+
for _, keys := range [][]string{
36+
{"key0"},
37+
{"key0", "key1"},
38+
} {
39+
for _, compactedOnTombstoneRev := range []bool{false, true} {
40+
t.Run(fmt.Sprintf("Keys=%v/CompactedOnTombstone=%v", keys, compactedOnTombstoneRev), func(t *testing.T) {
41+
for _, tc := range clusterTestCases() {
42+
t.Run(tc.name, func(t *testing.T) {
43+
if tc.config.ClusterSize < 2 {
44+
t.Skip("Skipping test for single-member cluster")
45+
}
46+
ctx, cancel := context.WithTimeout(t.Context(), 30*time.Second)
47+
defer cancel()
48+
49+
clus := testRunner.NewCluster(ctx, t, config.WithClusterConfig(tc.config))
50+
defer clus.Close()
51+
52+
cc := testutils.MustClient(clus.Client())
53+
tombstoneRevs, latestRev := populateDataForHashKV(t, cc, keys)
54+
55+
compactedOnRev := tombstoneRevs[0]
56+
57+
// If compaction revision isn't a tombstone, select a revision in the middle of two tombstones.
58+
if !compactedOnTombstoneRev {
59+
require.Greater(t, len(tombstoneRevs), 1)
60+
compactedOnRev = (tombstoneRevs[0] + tombstoneRevs[1]) / 2
61+
require.Greater(t, compactedOnRev, tombstoneRevs[0])
62+
require.Greater(t, tombstoneRevs[1], compactedOnRev)
63+
}
64+
65+
_, err := cc.Compact(ctx, compactedOnRev, config.CompactOption{Physical: true})
66+
require.NoError(t, err)
67+
68+
for rev := compactedOnRev; rev <= latestRev; rev++ {
69+
verifyConsistentHashKVAcrossAllMembers(t, cc, rev)
70+
}
71+
})
72+
}
73+
})
74+
}
75+
}
76+
}
77+
78+
// TestVerifyHashKVAfterTwoCompactsOnTombstone tests that HashKV is consistent
79+
// across all members after two physical compactions on tombstone revisions.
80+
func TestVerifyHashKVAfterTwoCompactsOnTombstone(t *testing.T) {
81+
testRunner.BeforeTest(t)
82+
for _, tc := range clusterTestCases() {
83+
t.Run(tc.name, func(t *testing.T) {
84+
if tc.config.ClusterSize < 2 {
85+
t.Skip("Skipping test for single-member cluster")
86+
}
87+
ctx, cancel := context.WithTimeout(t.Context(), 30*time.Second)
88+
defer cancel()
89+
90+
clus := testRunner.NewCluster(ctx, t, config.WithClusterConfig(tc.config))
91+
defer clus.Close()
92+
93+
cc := testutils.MustClient(clus.Client())
94+
tombstoneRevs, latestRev := populateDataForHashKV(t, cc, []string{"key0"})
95+
require.GreaterOrEqual(t, len(tombstoneRevs), 2)
96+
97+
firstCompactOnRev := tombstoneRevs[0]
98+
t.Logf("COMPACT rev=%d", firstCompactOnRev)
99+
_, err := cc.Compact(ctx, firstCompactOnRev, config.CompactOption{Physical: true})
100+
require.NoError(t, err)
101+
102+
secondCompactOnRev := tombstoneRevs[1]
103+
t.Logf("COMPACT rev=%d", secondCompactOnRev)
104+
_, err = cc.Compact(ctx, secondCompactOnRev, config.CompactOption{Physical: true})
105+
require.NoError(t, err)
106+
107+
for rev := secondCompactOnRev; rev <= latestRev; rev++ {
108+
verifyConsistentHashKVAcrossAllMembers(t, cc, rev)
109+
}
110+
})
111+
}
112+
}
113+
114+
// TestVerifyHashKVAfterCompactOnLastTombstone tests that HashKV is consistent
115+
// across all members after a physical compaction on the last tombstone revision.
116+
func TestVerifyHashKVAfterCompactOnLastTombstone(t *testing.T) {
117+
testRunner.BeforeTest(t)
118+
119+
for _, keys := range [][]string{
120+
{"key0"},
121+
{"key0", "key1"},
122+
} {
123+
t.Run(fmt.Sprintf("Keys=%v", keys), func(t *testing.T) {
124+
for _, tc := range clusterTestCases() {
125+
t.Run(tc.name, func(t *testing.T) {
126+
if tc.config.ClusterSize < 2 {
127+
t.Skip("Skipping test for single-member cluster")
128+
}
129+
130+
ctx, cancel := context.WithTimeout(t.Context(), 30*time.Second)
131+
defer cancel()
132+
133+
clus := testRunner.NewCluster(ctx, t, config.WithClusterConfig(tc.config))
134+
defer clus.Close()
135+
136+
cc := testutils.MustClient(clus.Client())
137+
tombstoneRevs, latestRev := populateDataForHashKV(t, cc, keys)
138+
require.NotEmpty(t, tombstoneRevs)
139+
140+
compactOnRev := tombstoneRevs[len(tombstoneRevs)-1]
141+
t.Logf("COMPACT rev=%d", compactOnRev)
142+
_, err := cc.Compact(ctx, compactOnRev, config.CompactOption{Physical: true})
143+
require.NoError(t, err)
144+
145+
for rev := compactOnRev; rev <= latestRev; rev++ {
146+
verifyConsistentHashKVAcrossAllMembers(t, cc, rev)
147+
}
148+
})
149+
}
150+
})
151+
}
152+
}
153+
154+
// TestVerifyHashKVAfterCompactAndDefrag tests that HashKV is consistent
155+
// within a member before and after a physical compaction and defragmentation.
156+
func TestVerifyHashKVAfterCompactAndDefrag(t *testing.T) {
157+
testRunner.BeforeTest(t)
158+
159+
for _, tc := range clusterTestCases() {
160+
t.Run(tc.name, func(t *testing.T) {
161+
ctx, cancel := context.WithTimeout(t.Context(), 30*time.Second)
162+
defer cancel()
163+
164+
clus := testRunner.NewCluster(ctx, t, config.WithClusterConfig(tc.config))
165+
defer clus.Close()
166+
167+
cc := testutils.MustClient(clus.Client())
168+
tombstoneRevs, _ := populateDataForHashKV(t, cc, []string{"key0"})
169+
require.NotEmpty(t, tombstoneRevs)
170+
171+
compactOnRev := tombstoneRevs[0]
172+
t.Logf("COMPACT rev=%d", compactOnRev)
173+
174+
before, err := cc.HashKV(ctx, compactOnRev)
175+
require.NoError(t, err)
176+
177+
_, err = cc.Compact(ctx, compactOnRev, config.CompactOption{Physical: true})
178+
require.NoError(t, err)
179+
180+
err = cc.Defragment(ctx, config.DefragOption{})
181+
require.NoError(t, err)
182+
183+
after, err := cc.HashKV(ctx, compactOnRev)
184+
require.NoError(t, err)
185+
186+
require.Equal(t, len(before), len(after))
187+
for i := range before {
188+
assert.Equal(t, before[i].Hash, after[i].Hash)
189+
}
190+
})
191+
}
192+
}
193+
194+
// populateDataForHashKV populates some sample data, and return a slice of tombstone
195+
// revisions and the latest revision.
196+
func populateDataForHashKV(t *testing.T, cc intf.Client, keys []string) ([]int64, int64) {
197+
t.Helper()
198+
ctx := t.Context()
199+
totalOperations := 40
200+
201+
var (
202+
tombStoneRevs []int64
203+
latestRev int64
204+
)
205+
206+
deleteStep := 10 // submit a delete operation on every 10 operations
207+
for i := 1; i <= totalOperations; i++ {
208+
if i%deleteStep == 0 {
209+
t.Logf("Deleting key=%s", keys[0]) // Only delete the first key for simplicity
210+
resp, derr := cc.Delete(ctx, keys[0], config.DeleteOptions{})
211+
require.NoError(t, derr)
212+
latestRev = resp.Header.Revision
213+
tombStoneRevs = append(tombStoneRevs, resp.Header.Revision)
214+
continue
215+
}
216+
217+
value := fmt.Sprintf("%d", i)
218+
var ops []string
219+
for _, key := range keys {
220+
ops = append(ops, fmt.Sprintf("put %s %s", key, value))
221+
}
222+
t.Logf("Writing keys: %v, value: %s", keys, value)
223+
resp, terr := cc.Txn(ctx, nil, ops, nil, config.TxnOptions{Interactive: true})
224+
require.NoError(t, terr)
225+
require.True(t, resp.Succeeded)
226+
require.Len(t, resp.Responses, len(ops))
227+
latestRev = resp.Header.Revision
228+
}
229+
return tombStoneRevs, latestRev
230+
}
231+
232+
func verifyConsistentHashKVAcrossAllMembers(t *testing.T, cc intf.Client, hashKVOnRev int64) {
233+
t.Helper()
234+
ctx := t.Context()
235+
236+
t.Logf("HashKV on rev=%d", hashKVOnRev)
237+
resp, err := cc.HashKV(ctx, hashKVOnRev)
238+
require.NoError(t, err)
239+
240+
// Ensure that there are multiple members in the cluster.
241+
require.Greater(t, len(resp), 1)
242+
require.NotEqual(t, 0, resp[0].Hash)
243+
t.Logf("One Hash value is %d", resp[0].Hash)
244+
245+
for i := 1; i < len(resp); i++ {
246+
assert.Equal(t, resp[0].Hash, resp[i].Hash)
247+
}
248+
}

0 commit comments

Comments
 (0)