Skip to content

Commit 4e2be22

Browse files
committed
etcdctl: add fakeclient in etcdctl, add UT for alarm command
Signed-off-by: hwdef <[email protected]>
1 parent 12a93a3 commit 4e2be22

File tree

7 files changed

+335
-96
lines changed

7 files changed

+335
-96
lines changed
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
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 command
16+
17+
import (
18+
"context"
19+
"strings"
20+
"testing"
21+
22+
"github.com/spf13/cobra"
23+
24+
pb "go.etcd.io/etcd/api/v3/etcdserverpb"
25+
clientv3 "go.etcd.io/etcd/client/v3"
26+
"go.etcd.io/etcd/etcdctl/v3/ctlv3/command/fakeclient"
27+
)
28+
29+
func newTestRoot() *cobra.Command {
30+
root := &cobra.Command{Use: "etcdctl"}
31+
root.AddGroup(NewKVGroup(), NewClusterMaintenanceGroup(), NewConcurrencyGroup(), NewAuthenticationGroup(), NewUtilityGroup())
32+
RegisterGlobalFlags(root)
33+
return root
34+
}
35+
36+
func TestAlarmList_PrintsAlarms(t *testing.T) {
37+
var gotReq int
38+
fc := &fakeclient.Client{
39+
AlarmListFn: func(ctx context.Context) (*clientv3.AlarmResponse, error) {
40+
gotReq++
41+
resp := clientv3.AlarmResponse{
42+
Alarms: []*pb.AlarmMember{
43+
{MemberID: 1, Alarm: pb.AlarmType_NOSPACE},
44+
{MemberID: 2, Alarm: pb.AlarmType_CORRUPT},
45+
},
46+
}
47+
return &resp, nil
48+
},
49+
}
50+
root := newTestRoot()
51+
root.SetContext(WithClient(context.Background(), fakeclient.WrapAsClientV3(fc)))
52+
root.AddCommand(NewAlarmCommand())
53+
root.SetArgs([]string{"alarm", "list"})
54+
55+
out := withStdoutCapture(t, func() { _ = root.Execute() })
56+
57+
if gotReq != 1 {
58+
t.Fatalf("expected AlarmList to be called once, got %d", gotReq)
59+
}
60+
if !strings.Contains(out, "NOSPACE") || !strings.Contains(out, "CORRUPT") {
61+
t.Fatalf("unexpected output: %q", out)
62+
}
63+
}
64+
65+
func TestAlarmDisarm_DisarmsAll(t *testing.T) {
66+
var disarmCalled int
67+
fc := &fakeclient.Client{
68+
AlarmDisarmFn: func(ctx context.Context, m *clientv3.AlarmMember) (*clientv3.AlarmResponse, error) {
69+
disarmCalled++
70+
return &clientv3.AlarmResponse{Alarms: []*pb.AlarmMember{}}, nil
71+
},
72+
}
73+
root := newTestRoot()
74+
root.SetContext(WithClient(context.Background(), fakeclient.WrapAsClientV3(fc)))
75+
root.AddCommand(NewAlarmCommand())
76+
root.SetArgs([]string{"alarm", "disarm"})
77+
78+
_ = withStdoutCapture(t, func() { _ = root.Execute() })
79+
80+
if disarmCalled != 1 {
81+
t.Fatalf("expected disarm to be called, got %d", disarmCalled)
82+
}
83+
}

etcdctl/ctlv3/command/ep_command.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,7 @@ func epHashKVCommandFunc(cmd *cobra.Command, args []string) {
245245

246246
func endpointsFromCluster(cmd *cobra.Command) []string {
247247
if !epClusterEndpoints {
248-
endpoints, err := cmd.Flags().GetStringSlice("endpoints")
248+
endpoints, err := cmd.Flags().GetStringSlice(FlagEndpoints)
249249
if err != nil {
250250
cobrautl.ExitWithError(cobrautl.ExitError, err)
251251
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
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 fakeclient
16+
17+
import (
18+
"bytes"
19+
"context"
20+
"io"
21+
22+
clientv3 "go.etcd.io/etcd/client/v3"
23+
)
24+
25+
// Client is a lightweight fake that satisfies the small subset of etcdctl usage.
26+
// It embeds function fields for behaviors; unimplemented methods return error.
27+
// This keeps the fake reusable across most commands by stubbing only used calls.
28+
type Client struct {
29+
AlarmListFn func(ctx context.Context) (*clientv3.AlarmResponse, error)
30+
AlarmDisarmFn func(ctx context.Context, m *clientv3.AlarmMember) (*clientv3.AlarmResponse, error)
31+
}
32+
33+
// WrapAsClientV3 constructs a minimal *clientv3.Client using NewCtxClient and attaches fake Maintenance.
34+
func WrapAsClientV3(fc *Client) *clientv3.Client {
35+
c := clientv3.NewCtxClient(context.Background())
36+
c.Maintenance = &maintenance{fake: fc}
37+
return c
38+
}
39+
40+
type maintenance struct{ fake *Client }
41+
42+
func (m *maintenance) AlarmList(ctx context.Context) (*clientv3.AlarmResponse, error) {
43+
if m.fake.AlarmListFn != nil {
44+
return m.fake.AlarmListFn(ctx)
45+
}
46+
return &clientv3.AlarmResponse{}, nil
47+
}
48+
49+
func (m *maintenance) AlarmDisarm(ctx context.Context, am *clientv3.AlarmMember) (*clientv3.AlarmResponse, error) {
50+
if m.fake.AlarmDisarmFn != nil {
51+
return m.fake.AlarmDisarmFn(ctx, am)
52+
}
53+
return &clientv3.AlarmResponse{}, nil
54+
}
55+
56+
func (m *maintenance) Defragment(ctx context.Context, endpoint string) (*clientv3.DefragmentResponse, error) {
57+
var resp clientv3.DefragmentResponse
58+
return &resp, nil
59+
}
60+
61+
func (m *maintenance) Status(ctx context.Context, endpoint string) (*clientv3.StatusResponse, error) {
62+
var resp clientv3.StatusResponse
63+
return &resp, nil
64+
}
65+
66+
func (m *maintenance) HashKV(ctx context.Context, endpoint string, rev int64) (*clientv3.HashKVResponse, error) {
67+
var resp clientv3.HashKVResponse
68+
return &resp, nil
69+
}
70+
71+
func (m *maintenance) SnapshotWithVersion(ctx context.Context) (*clientv3.SnapshotResponse, error) {
72+
return &clientv3.SnapshotResponse{Snapshot: io.NopCloser(bytes.NewReader(nil))}, nil
73+
}
74+
75+
func (m *maintenance) Snapshot(ctx context.Context) (io.ReadCloser, error) {
76+
return io.NopCloser(bytes.NewReader(nil)), nil
77+
}
78+
79+
func (m *maintenance) MoveLeader(ctx context.Context, transfereeID uint64) (*clientv3.MoveLeaderResponse, error) {
80+
var resp clientv3.MoveLeaderResponse
81+
return &resp, nil
82+
}
83+
84+
func (m *maintenance) Downgrade(ctx context.Context, action clientv3.DowngradeAction, version string) (*clientv3.DowngradeResponse, error) {
85+
var resp clientv3.DowngradeResponse
86+
return &resp, nil
87+
}

0 commit comments

Comments
 (0)