diff --git a/tests/common/kv_test.go b/tests/common/kv_test.go index 68f3205b3823..4a8ab7f40094 100644 --- a/tests/common/kv_test.go +++ b/tests/common/kv_test.go @@ -163,3 +163,42 @@ func TestKVGet(t *testing.T) { }) } } + +func TestKVGetNoQuorum(t *testing.T) { + testRunner.BeforeTest(t) + tcs := []struct { + name string + options config.GetOptions + + wantError bool + }{ + { + name: "Serializable", + options: config.GetOptions{Serializable: true}, + }, + { + name: "Linearizable", + options: config.GetOptions{Serializable: false, Timeout: time.Second}, + wantError: true, + }, + } + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + clus := testRunner.NewCluster(t, config.ClusterConfig{ClusterSize: 3}) + defer clus.Close() + + clus.Members()[0].Stop() + clus.Members()[1].Stop() + + cc := clus.Members()[2].Client() + testutils.ExecuteWithTimeout(t, 10*time.Second, func() { + key := "foo" + _, err := cc.Get(key, tc.options) + gotError := err != nil + if gotError != tc.wantError { + t.Fatalf("Unexpeted result, wantError: %v, gotErr: %v, err: %s", tc.wantError, gotError, err) + } + }) + }) + } +} diff --git a/tests/e2e/ctl_v3_kv_no_quorum_test.go b/tests/e2e/ctl_v3_kv_no_quorum_test.go deleted file mode 100644 index dbc599b49765..000000000000 --- a/tests/e2e/ctl_v3_kv_no_quorum_test.go +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright 2021 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. - -// When the quorum isn't satisfied, then each etcd member isn't able to -// publish/register server information(i.e., clientURL) into the cluster. -// Accordingly, the v2 proxy can't get any member's clientURL, so this -// case will fail for sure in this case. -// -// todo(ahrtr): When v2 proxy is removed, then we can remove the go build -// lines below. -//go:build !cluster_proxy -// +build !cluster_proxy - -package e2e - -import ( - "testing" - - "go.etcd.io/etcd/tests/v3/framework/e2e" -) - -func TestSerializableReadWithoutQuorum(t *testing.T) { - tcs := []struct { - name string - testFunc func(cx ctlCtx) - }{ - { - name: "serializableReadTest", - testFunc: serializableReadTest, - }, - { - name: "linearizableReadTest", - testFunc: linearizableReadTest, - }, - } - for _, tc := range tcs { - t.Run(tc.name, func(t *testing.T) { - // Initialize a cluster with 3 members - epc, err := e2e.InitEtcdProcessCluster(t, e2e.NewConfigAutoTLS()) - if err != nil { - t.Fatalf("Failed to initilize the etcd cluster: %v", err) - } - - // Remove two members, so that only one etcd will get started - epc.Procs = epc.Procs[:1] - - // Start the etcd cluster with only one member - if err := epc.Start(); err != nil { - t.Fatalf("Failed to start the etcd cluster: %v", err) - } - - // construct the ctl context - cx := getDefaultCtlCtx(t) - cx.epc = epc - runCtlTest(t, tc.testFunc, nil, cx) - }) - } -} - -func serializableReadTest(cx ctlCtx) { - cx.quorum = false - if err := ctlV3Get(cx, []string{"key1"}, []kv{}...); err != nil { - cx.t.Errorf("serializableReadTest failed: %v", err) - } -} - -func linearizableReadTest(cx ctlCtx) { - cx.quorum = true - if err := ctlV3GetWithErr(cx, []string{"key"}, []string{"retrying of unary invoker failed"}); err != nil { // expect errors - cx.t.Fatalf("ctlV3GetWithErr error (%v)", err) - } -} diff --git a/tests/framework/config/client.go b/tests/framework/config/client.go index 47c7ba13864c..61301e7a7a98 100644 --- a/tests/framework/config/client.go +++ b/tests/framework/config/client.go @@ -14,7 +14,11 @@ package config -import clientv3 "go.etcd.io/etcd/client/v3" +import ( + "time" + + clientv3 "go.etcd.io/etcd/client/v3" +) type GetOptions struct { Revision int @@ -26,4 +30,5 @@ type GetOptions struct { Limit int Order clientv3.SortOrder SortBy clientv3.SortTarget + Timeout time.Duration } diff --git a/tests/framework/e2e.go b/tests/framework/e2e.go index d0e799163e6f..ce068d37b991 100644 --- a/tests/framework/e2e.go +++ b/tests/framework/e2e.go @@ -83,6 +83,30 @@ func (c *e2eCluster) Client() Client { return e2eClient{e2e.NewEtcdctl(c.Cfg, c.EndpointsV3())} } +func (c *e2eCluster) Members() (ms []Member) { + for _, proc := range c.EtcdProcessCluster.Procs { + ms = append(ms, e2eMember{EtcdProcess: proc, Cfg: c.Cfg}) + } + return ms +} + type e2eClient struct { *e2e.EtcdctlV3 } + +type e2eMember struct { + e2e.EtcdProcess + Cfg *e2e.EtcdProcessClusterConfig +} + +func (m e2eMember) Client() Client { + return e2eClient{e2e.NewEtcdctl(m.Cfg, m.EndpointsV3())} +} + +func (m e2eMember) Start() error { + return m.Restart() +} + +func (m e2eMember) Stop() { + m.EtcdProcess.Stop() +} diff --git a/tests/framework/e2e/etcdctl.go b/tests/framework/e2e/etcdctl.go index 5d79bbfbba5f..385000cba483 100644 --- a/tests/framework/e2e/etcdctl.go +++ b/tests/framework/e2e/etcdctl.go @@ -41,6 +41,9 @@ func (ctl *EtcdctlV3) DowngradeEnable(version string) error { func (ctl *EtcdctlV3) Get(key string, o config.GetOptions) (*clientv3.GetResponse, error) { args := ctl.cmdArgs() + if o.Timeout != 0 { + args = append(args, fmt.Sprintf("--command-timeout=%s", o.Timeout)) + } if o.Serializable { args = append(args, "--consistency", "s") } @@ -98,7 +101,7 @@ func (ctl *EtcdctlV3) Get(key string, o config.GetOptions) (*clientv3.GetRespons _, err := cmd.Expect("Count") return &resp, err } - line, err := cmd.Expect("kvs") + line, err := cmd.Expect("header") if err != nil { return nil, err } diff --git a/tests/framework/integration.go b/tests/framework/integration.go index d484fe580462..4452eaf68ce8 100644 --- a/tests/framework/integration.go +++ b/tests/framework/integration.go @@ -77,6 +77,30 @@ type integrationCluster struct { t testing.TB } +func (c *integrationCluster) Members() (ms []Member) { + for _, m := range c.Cluster.Members { + ms = append(ms, integrationMember{m, c.t}) + } + return ms +} + +type integrationMember struct { + *integration.Member + t testing.TB +} + +func (m integrationMember) Client() Client { + return integrationClient{m.Member.Client} +} + +func (m integrationMember) Start() error { + return m.Member.Restart(m.t) +} + +func (m integrationMember) Stop() { + m.Member.Stop(m.t) +} + func (c *integrationCluster) Close() error { c.Terminate(c.t) return nil @@ -87,7 +111,7 @@ func (c *integrationCluster) Client() Client { if err != nil { c.t.Fatal(err) } - return &integrationClient{cc} + return integrationClient{cc} } type integrationClient struct { @@ -95,6 +119,12 @@ type integrationClient struct { } func (c integrationClient) Get(key string, o config.GetOptions) (*clientv3.GetResponse, error) { + ctx := context.Background() + if o.Timeout != 0 { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, o.Timeout) + defer cancel() + } clientOpts := []clientv3.OpOption{} if o.Revision != 0 { clientOpts = append(clientOpts, clientv3.WithRev(int64(o.Revision))) @@ -120,7 +150,7 @@ func (c integrationClient) Get(key string, o config.GetOptions) (*clientv3.GetRe if o.SortBy != clientv3.SortByKey || o.Order != clientv3.SortNone { clientOpts = append(clientOpts, clientv3.WithSort(o.SortBy, o.Order)) } - return c.Client.Get(context.Background(), key, clientOpts...) + return c.Client.Get(ctx, key, clientOpts...) } func (c integrationClient) Put(key, value string) error { diff --git a/tests/framework/interface.go b/tests/framework/interface.go index fb57b92ec267..15d0d30ffeb6 100644 --- a/tests/framework/interface.go +++ b/tests/framework/interface.go @@ -28,8 +28,15 @@ type testRunner interface { } type Cluster interface { + Members() []Member + Client() Client Close() error +} + +type Member interface { Client() Client + Start() error + Stop() } type Client interface {