Skip to content

Commit f6f8936

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

File tree

3 files changed

+246
-315
lines changed

3 files changed

+246
-315
lines changed

tests/common/hashkv_test.go

Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
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+
// TestVerifyHashKVAfterTwoCompactionsOnTombstone tests that HashKV is consistent
79+
// across all members after two physical compactions on tombstone revisions.
80+
func TestVerifyHashKVAfterTwoCompactionsOnTombstone(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+
// TestVerifyHashKVAfterCompactionOnLastTombstone tests that HashKV is consistent
115+
// across all members after a physical compaction on the last tombstone revision.
116+
func TestVerifyHashKVAfterCompactionOnLastTombstone(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+
ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second)
130+
defer cancel()
131+
132+
clus := testRunner.NewCluster(ctx, t, config.WithClusterConfig(tc.config))
133+
defer clus.Close()
134+
135+
cc := testutils.MustClient(clus.Client())
136+
tombstoneRevs, latestRev := populateDataForHashKV(t, cc, keys)
137+
require.NotEmpty(t, tombstoneRevs)
138+
139+
compactOnRev := tombstoneRevs[len(tombstoneRevs)-1]
140+
t.Logf("COMPACT rev=%d", compactOnRev)
141+
_, err := cc.Compact(ctx, compactOnRev, config.CompactOption{Physical: true})
142+
require.NoError(t, err)
143+
144+
for rev := compactOnRev; rev <= latestRev; rev++ {
145+
verifyConsistentHashKVAcrossAllMembers(t, cc, rev)
146+
}
147+
})
148+
}
149+
})
150+
}
151+
}
152+
153+
// TestVerifyHashKVAfterCompactAndDefrag tests that HashKV is consistent
154+
// within a member before and after physical compaction and defragmentation.
155+
func TestVerifyHashKVAfterCompactAndDefrag(t *testing.T) {
156+
testRunner.BeforeTest(t)
157+
158+
for _, tc := range clusterTestCases() {
159+
t.Run(tc.name, func(t *testing.T) {
160+
ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second)
161+
defer cancel()
162+
163+
clus := testRunner.NewCluster(ctx, t, config.WithClusterConfig(tc.config))
164+
defer clus.Close()
165+
166+
cc := testutils.MustClient(clus.Client())
167+
tombstoneRevs, _ := populateDataForHashKV(t, cc, []string{"key0"})
168+
require.NotEmpty(t, tombstoneRevs)
169+
170+
compactOnRev := tombstoneRevs[0]
171+
t.Logf("COMPACT rev=%d", compactOnRev)
172+
173+
before, err := cc.HashKV(ctx, compactOnRev)
174+
require.NoError(t, err)
175+
176+
_, err = cc.Compact(ctx, compactOnRev, config.CompactOption{Physical: true})
177+
require.NoError(t, err)
178+
179+
err = cc.Defragment(ctx, config.DefragOption{})
180+
require.NoError(t, err)
181+
182+
after, err := cc.HashKV(ctx, compactOnRev)
183+
require.NoError(t, err)
184+
185+
require.Equal(t, len(before), len(after))
186+
for i := range before {
187+
require.Equal(t, before[i].Hash, after[i].Hash)
188+
}
189+
})
190+
}
191+
}
192+
193+
// populateDataForHashKV populates some sample data, and return a slice of tombstone
194+
// revisions and the latest revision
195+
func populateDataForHashKV(t *testing.T, cc intf.Client, keys []string) ([]int64, int64) {
196+
t.Helper()
197+
ctx := t.Context()
198+
totalOperations := 40
199+
200+
var (
201+
tombStoneRevs []int64
202+
latestRev int64
203+
)
204+
205+
deleteStep := 10 // submit a delete operation on every 10 operations
206+
for i := 1; i <= totalOperations; i++ {
207+
if i%deleteStep == 0 {
208+
t.Logf("Deleting key=%s", keys[0]) // Only delete the first key for simplicity
209+
resp, derr := cc.Delete(ctx, keys[0], config.DeleteOptions{})
210+
require.NoError(t, derr)
211+
latestRev = resp.Header.Revision
212+
tombStoneRevs = append(tombStoneRevs, resp.Header.Revision)
213+
continue
214+
}
215+
216+
value := fmt.Sprintf("%d", i)
217+
var ops []string
218+
for _, key := range keys {
219+
ops = append(ops, fmt.Sprintf("put %s %s", key, value))
220+
}
221+
t.Logf("Writing keys: %v, value: %s", keys, value)
222+
resp, terr := cc.Txn(ctx, nil, ops, nil, config.TxnOptions{Interactive: true})
223+
require.NoError(t, terr)
224+
require.True(t, resp.Succeeded)
225+
require.Len(t, resp.Responses, len(ops))
226+
latestRev = resp.Header.Revision
227+
}
228+
return tombStoneRevs, latestRev
229+
}
230+
231+
func verifyConsistentHashKVAcrossAllMembers(t *testing.T, cc intf.Client, hashKVOnRev int64) {
232+
t.Helper()
233+
ctx := t.Context()
234+
235+
t.Logf("HashKV on rev=%d", hashKVOnRev)
236+
resp, err := cc.HashKV(ctx, hashKVOnRev)
237+
require.NoError(t, err)
238+
239+
require.Greater(t, len(resp), 1)
240+
require.NotEqual(t, 0, resp[0].Hash)
241+
t.Logf("One Hash value is %d", resp[0].Hash)
242+
243+
for i := 1; i < len(resp); i++ {
244+
assert.Equal(t, resp[0].Hash, resp[i].Hash)
245+
}
246+
}

0 commit comments

Comments
 (0)