Skip to content

Commit 364b200

Browse files
committed
add fakeclint in etcdctl, add UT for alarm command
Signed-off-by: hwdef <[email protected]>
1 parent 115efcb commit 364b200

File tree

3 files changed

+230
-1
lines changed

3 files changed

+230
-1
lines changed
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
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+
"bytes"
19+
"context"
20+
"os"
21+
"strings"
22+
"testing"
23+
"time"
24+
25+
"github.com/spf13/cobra"
26+
27+
pb "go.etcd.io/etcd/api/v3/etcdserverpb"
28+
clientv3 "go.etcd.io/etcd/client/v3"
29+
"go.etcd.io/etcd/etcdctl/v3/ctlv3/command/fakeclient"
30+
)
31+
32+
func withStdoutCapture(t *testing.T, fn func()) string {
33+
t.Helper()
34+
old := os.Stdout
35+
r, w, _ := os.Pipe()
36+
os.Stdout = w
37+
defer func() { os.Stdout = old }()
38+
39+
fn()
40+
41+
w.Close()
42+
var buf bytes.Buffer
43+
_, _ = buf.ReadFrom(r)
44+
return buf.String()
45+
}
46+
47+
func setupFake(t *testing.T, fc *fakeclient.Client) func() {
48+
t.Helper()
49+
oldNew := newClientFunc
50+
newClientFunc = func(cfg clientv3.Config) (*clientv3.Client, error) {
51+
return fakeclient.WrapAsClientV3(fc), nil
52+
}
53+
return func() { newClientFunc = oldNew }
54+
}
55+
56+
func newTestRoot() *cobra.Command {
57+
root := &cobra.Command{Use: "etcdctl"}
58+
root.AddGroup(NewKVGroup(), NewClusterMaintenanceGroup(), NewConcurrencyGroup(), NewAuthenticationGroup(), NewUtilityGroup())
59+
root.PersistentFlags().StringSlice("endpoints", []string{"127.0.0.1:2379"}, "")
60+
root.PersistentFlags().Bool("debug", false, "")
61+
root.PersistentFlags().String("write-out", "simple", "")
62+
root.PersistentFlags().Bool("hex", false, "")
63+
root.PersistentFlags().Duration("dial-timeout", time.Second, "")
64+
root.PersistentFlags().Duration("command-timeout", 5*time.Second, "")
65+
root.PersistentFlags().Duration("keepalive-time", time.Second, "")
66+
root.PersistentFlags().Duration("keepalive-timeout", 2*time.Second, "")
67+
root.PersistentFlags().Int("max-request-bytes", 0, "")
68+
root.PersistentFlags().Int("max-recv-bytes", 0, "")
69+
root.PersistentFlags().Bool("insecure-transport", true, "")
70+
root.PersistentFlags().Bool("insecure-discovery", true, "")
71+
root.PersistentFlags().Bool("insecure-skip-tls-verify", false, "")
72+
root.PersistentFlags().String("cert", "", "")
73+
root.PersistentFlags().String("key", "", "")
74+
root.PersistentFlags().String("cacert", "", "")
75+
root.PersistentFlags().String("auth-jwt-token", "", "")
76+
root.PersistentFlags().String("user", "", "")
77+
root.PersistentFlags().String("password", "", "")
78+
root.PersistentFlags().String("discovery-srv", "", "")
79+
root.PersistentFlags().String("discovery-srv-name", "", "")
80+
return root
81+
}
82+
83+
func TestAlarmList_PrintsAlarms(t *testing.T) {
84+
var gotReq int
85+
fc := &fakeclient.Client{
86+
AlarmListFn: func(ctx context.Context) (*clientv3.AlarmResponse, error) {
87+
gotReq++
88+
resp := clientv3.AlarmResponse{
89+
Alarms: []*pb.AlarmMember{
90+
{MemberID: 1, Alarm: pb.AlarmType_NOSPACE},
91+
{MemberID: 2, Alarm: pb.AlarmType_CORRUPT},
92+
},
93+
}
94+
return &resp, nil
95+
},
96+
}
97+
restore := setupFake(t, fc)
98+
defer restore()
99+
100+
root := newTestRoot()
101+
root.AddCommand(NewAlarmCommand())
102+
root.SetArgs([]string{"alarm", "list"})
103+
104+
out := withStdoutCapture(t, func() { _ = root.Execute() })
105+
106+
if gotReq != 1 {
107+
t.Fatalf("expected AlarmList to be called once, got %d", gotReq)
108+
}
109+
if !strings.Contains(out, "NOSPACE") || !strings.Contains(out, "CORRUPT") {
110+
t.Fatalf("unexpected output: %q", out)
111+
}
112+
}
113+
114+
func TestAlarmDisarm_DisarmsAll(t *testing.T) {
115+
var disarmCalled int
116+
fc := &fakeclient.Client{
117+
AlarmDisarmFn: func(ctx context.Context, m *clientv3.AlarmMember) (*clientv3.AlarmResponse, error) {
118+
disarmCalled++
119+
return &clientv3.AlarmResponse{Alarms: []*pb.AlarmMember{}}, nil
120+
},
121+
}
122+
restore := setupFake(t, fc)
123+
defer restore()
124+
125+
root := newTestRoot()
126+
root.AddCommand(NewAlarmCommand())
127+
root.SetArgs([]string{"alarm", "disarm"})
128+
129+
_ = withStdoutCapture(t, func() { _ = root.Execute() })
130+
131+
if disarmCalled != 1 {
132+
t.Fatalf("expected disarm to be called, got %d", disarmCalled)
133+
}
134+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
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+
"errors"
21+
"io"
22+
23+
clientv3 "go.etcd.io/etcd/client/v3"
24+
)
25+
26+
// Client is a lightweight fake that satisfies the small subset of etcdctl usage.
27+
// It embeds function fields for behaviors; unimplemented methods return error.
28+
// This keeps the fake reusable across most commands by stubbing only used calls.
29+
type Client struct {
30+
AlarmListFn func(ctx context.Context) (*clientv3.AlarmResponse, error)
31+
AlarmDisarmFn func(ctx context.Context, m *clientv3.AlarmMember) (*clientv3.AlarmResponse, error)
32+
}
33+
34+
// New returns a fake client instance. Config is ignored.
35+
func New(_ clientv3.Config) (*clientv3.Client, error) {
36+
return nil, errors.New("fakeclient.New must be used through command-level wrapper in tests")
37+
}
38+
39+
// WrapAsClientV3 constructs a minimal *clientv3.Client using NewCtxClient and attaches fake Maintenance.
40+
func WrapAsClientV3(fc *Client) *clientv3.Client {
41+
c := clientv3.NewCtxClient(context.Background())
42+
c.Maintenance = &maintenance{fake: fc}
43+
return c
44+
}
45+
46+
type maintenance struct{ fake *Client }
47+
48+
func (m *maintenance) AlarmList(ctx context.Context) (*clientv3.AlarmResponse, error) {
49+
if m.fake.AlarmListFn != nil {
50+
return m.fake.AlarmListFn(ctx)
51+
}
52+
return &clientv3.AlarmResponse{}, nil
53+
}
54+
55+
func (m *maintenance) AlarmDisarm(ctx context.Context, am *clientv3.AlarmMember) (*clientv3.AlarmResponse, error) {
56+
if m.fake.AlarmDisarmFn != nil {
57+
return m.fake.AlarmDisarmFn(ctx, am)
58+
}
59+
return &clientv3.AlarmResponse{}, nil
60+
}
61+
62+
func (m *maintenance) Defragment(ctx context.Context, endpoint string) (*clientv3.DefragmentResponse, error) {
63+
var resp clientv3.DefragmentResponse
64+
return &resp, nil
65+
}
66+
67+
func (m *maintenance) Status(ctx context.Context, endpoint string) (*clientv3.StatusResponse, error) {
68+
var resp clientv3.StatusResponse
69+
return &resp, nil
70+
}
71+
72+
func (m *maintenance) HashKV(ctx context.Context, endpoint string, rev int64) (*clientv3.HashKVResponse, error) {
73+
var resp clientv3.HashKVResponse
74+
return &resp, nil
75+
}
76+
77+
func (m *maintenance) SnapshotWithVersion(ctx context.Context) (*clientv3.SnapshotResponse, error) {
78+
return &clientv3.SnapshotResponse{Snapshot: io.NopCloser(bytes.NewReader(nil))}, nil
79+
}
80+
81+
func (m *maintenance) Snapshot(ctx context.Context) (io.ReadCloser, error) {
82+
return io.NopCloser(bytes.NewReader(nil)), nil
83+
}
84+
85+
func (m *maintenance) MoveLeader(ctx context.Context, transfereeID uint64) (*clientv3.MoveLeaderResponse, error) {
86+
var resp clientv3.MoveLeaderResponse
87+
return &resp, nil
88+
}
89+
90+
func (m *maintenance) Downgrade(ctx context.Context, action clientv3.DowngradeAction, version string) (*clientv3.DowngradeResponse, error) {
91+
var resp clientv3.DowngradeResponse
92+
return &resp, nil
93+
}

etcdctl/ctlv3/command/global.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@ type discoveryCfg struct {
7171

7272
var display printer = &simplePrinter{}
7373

74+
var newClientFunc = clientv3.New
75+
7476
func initDisplayFromCmd(cmd *cobra.Command) {
7577
isHex, err := cmd.Flags().GetBool("hex")
7678
if err != nil {
@@ -163,7 +165,7 @@ func mustClient(cc *clientv3.ConfigSpec) *clientv3.Client {
163165
cobrautl.ExitWithError(cobrautl.ExitBadArgs, err)
164166
}
165167

166-
client, err := clientv3.New(*cfg)
168+
client, err := newClientFunc(*cfg)
167169
if err != nil {
168170
cobrautl.ExitWithError(cobrautl.ExitBadConnection, err)
169171
}

0 commit comments

Comments
 (0)