Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions etcdctl/ctlv3/command/alarm_command_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// 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 command

import (
"context"
"strings"
"testing"

"github.com/spf13/cobra"

pb "go.etcd.io/etcd/api/v3/etcdserverpb"
clientv3 "go.etcd.io/etcd/client/v3"
"go.etcd.io/etcd/etcdctl/v3/ctlv3/command/fakeclient"
)

func newTestRoot() *cobra.Command {
root := &cobra.Command{Use: "etcdctl"}
root.AddGroup(NewKVGroup(), NewClusterMaintenanceGroup(), NewConcurrencyGroup(), NewAuthenticationGroup(), NewUtilityGroup())
RegisterGlobalFlags(root)
return root
}

func TestAlarmList_PrintsAlarms(t *testing.T) {
var gotReq int
fc := &fakeclient.Client{
AlarmListFn: func(ctx context.Context) (*clientv3.AlarmResponse, error) {
gotReq++
resp := clientv3.AlarmResponse{
Alarms: []*pb.AlarmMember{
{MemberID: 1, Alarm: pb.AlarmType_NOSPACE},
{MemberID: 2, Alarm: pb.AlarmType_CORRUPT},
},
}
return &resp, nil
},
}
root := newTestRoot()
root.SetContext(WithClient(context.Background(), fakeclient.WrapAsClientV3(fc)))
root.AddCommand(NewAlarmCommand())
root.SetArgs([]string{"alarm", "list"})

out := withStdoutCapture(t, func() { _ = root.Execute() })

if gotReq != 1 {
t.Fatalf("expected AlarmList to be called once, got %d", gotReq)
}
if !strings.Contains(out, "NOSPACE") || !strings.Contains(out, "CORRUPT") {
t.Fatalf("unexpected output: %q", out)
}
}

func TestAlarmDisarm_DisarmsAll(t *testing.T) {
var disarmCalled int
fc := &fakeclient.Client{
AlarmDisarmFn: func(ctx context.Context, m *clientv3.AlarmMember) (*clientv3.AlarmResponse, error) {
disarmCalled++
return &clientv3.AlarmResponse{Alarms: []*pb.AlarmMember{}}, nil
},
}
root := newTestRoot()
root.SetContext(WithClient(context.Background(), fakeclient.WrapAsClientV3(fc)))
root.AddCommand(NewAlarmCommand())
root.SetArgs([]string{"alarm", "disarm"})

_ = withStdoutCapture(t, func() { _ = root.Execute() })

if disarmCalled != 1 {
t.Fatalf("expected disarm to be called, got %d", disarmCalled)
}
}
2 changes: 1 addition & 1 deletion etcdctl/ctlv3/command/ep_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ func epHashKVCommandFunc(cmd *cobra.Command, args []string) {

func endpointsFromCluster(cmd *cobra.Command) []string {
if !epClusterEndpoints {
endpoints, err := cmd.Flags().GetStringSlice("endpoints")
endpoints, err := cmd.Flags().GetStringSlice(FlagEndpoints)
if err != nil {
cobrautl.ExitWithError(cobrautl.ExitError, err)
}
Expand Down
87 changes: 87 additions & 0 deletions etcdctl/ctlv3/command/fakeclient/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// 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 fakeclient

import (
"bytes"
"context"
"io"

clientv3 "go.etcd.io/etcd/client/v3"
)

// Client is a lightweight fake that satisfies the small subset of etcdctl usage.
// It embeds function fields for behaviors; unimplemented methods return error.
// This keeps the fake reusable across most commands by stubbing only used calls.
type Client struct {
AlarmListFn func(ctx context.Context) (*clientv3.AlarmResponse, error)
AlarmDisarmFn func(ctx context.Context, m *clientv3.AlarmMember) (*clientv3.AlarmResponse, error)
}

// WrapAsClientV3 constructs a minimal *clientv3.Client using NewCtxClient and attaches fake Maintenance.
func WrapAsClientV3(fc *Client) *clientv3.Client {
c := clientv3.NewCtxClient(context.Background())
c.Maintenance = &maintenance{fake: fc}
return c
}

type maintenance struct{ fake *Client }

func (m *maintenance) AlarmList(ctx context.Context) (*clientv3.AlarmResponse, error) {
if m.fake.AlarmListFn != nil {
return m.fake.AlarmListFn(ctx)
}
return &clientv3.AlarmResponse{}, nil
}

func (m *maintenance) AlarmDisarm(ctx context.Context, am *clientv3.AlarmMember) (*clientv3.AlarmResponse, error) {
if m.fake.AlarmDisarmFn != nil {
return m.fake.AlarmDisarmFn(ctx, am)
}
return &clientv3.AlarmResponse{}, nil
}

func (m *maintenance) Defragment(ctx context.Context, endpoint string) (*clientv3.DefragmentResponse, error) {
var resp clientv3.DefragmentResponse
return &resp, nil
}

func (m *maintenance) Status(ctx context.Context, endpoint string) (*clientv3.StatusResponse, error) {
var resp clientv3.StatusResponse
return &resp, nil
}

func (m *maintenance) HashKV(ctx context.Context, endpoint string, rev int64) (*clientv3.HashKVResponse, error) {
var resp clientv3.HashKVResponse
return &resp, nil
}

func (m *maintenance) SnapshotWithVersion(ctx context.Context) (*clientv3.SnapshotResponse, error) {
return &clientv3.SnapshotResponse{Snapshot: io.NopCloser(bytes.NewReader(nil))}, nil
}

func (m *maintenance) Snapshot(ctx context.Context) (io.ReadCloser, error) {
return io.NopCloser(bytes.NewReader(nil)), nil
}

func (m *maintenance) MoveLeader(ctx context.Context, transfereeID uint64) (*clientv3.MoveLeaderResponse, error) {
var resp clientv3.MoveLeaderResponse
return &resp, nil
}

func (m *maintenance) Downgrade(ctx context.Context, action clientv3.DowngradeAction, version string) (*clientv3.DowngradeResponse, error) {
var resp clientv3.DowngradeResponse
return &resp, nil
}
Loading