Skip to content

Commit

Permalink
test: support regular expression matching on the response
Browse files Browse the repository at this point in the history
Signed-off-by: Benjamin Wang <[email protected]>
  • Loading branch information
ahrtr committed Aug 25, 2023
1 parent f377db2 commit 14ef651
Show file tree
Hide file tree
Showing 30 changed files with 171 additions and 137 deletions.
21 changes: 18 additions & 3 deletions pkg/expect/expect.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"io"
"os"
"os/exec"
"regexp"
"strings"
"sync"
"syscall"
Expand All @@ -37,6 +38,11 @@ var (
ErrProcessRunning = fmt.Errorf("process is still running")
)

type ExpectedResponse struct {
Value string
IsRegularExpr bool
}

type ExpectProcess struct {
cfg expectConfig

Expand Down Expand Up @@ -223,14 +229,23 @@ func (ep *ExpectProcess) ExpectFunc(ctx context.Context, f func(string) bool) (s
}

// ExpectWithContext returns the first line containing the given string.
func (ep *ExpectProcess) ExpectWithContext(ctx context.Context, s string) (string, error) {
return ep.ExpectFunc(ctx, func(txt string) bool { return strings.Contains(txt, s) })
func (ep *ExpectProcess) ExpectWithContext(ctx context.Context, s ExpectedResponse) (string, error) {
return ep.ExpectFunc(ctx, func(txt string) bool {
if s.IsRegularExpr {
r, err := regexp.Compile(s.Value)
if err != nil {
return false
}
return r.MatchString(txt)
}
return strings.Contains(txt, s.Value)
})
}

// Expect returns the first line containing the given string.
// Deprecated: please use ExpectWithContext instead.
func (ep *ExpectProcess) Expect(s string) (string, error) {
return ep.ExpectWithContext(context.Background(), s)
return ep.ExpectWithContext(context.Background(), ExpectedResponse{Value: s})
}

// LineCount returns the number of recorded lines since
Expand Down
8 changes: 4 additions & 4 deletions pkg/expect/expect_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ func TestEcho(t *testing.T) {
t.Fatal(err)
}
ctx := context.Background()
l, eerr := ep.ExpectWithContext(ctx, "world")
l, eerr := ep.ExpectWithContext(ctx, ExpectedResponse{Value: "world"})
if eerr != nil {
t.Fatal(eerr)
}
Expand All @@ -138,7 +138,7 @@ func TestEcho(t *testing.T) {
if cerr := ep.Close(); cerr != nil {
t.Fatal(cerr)
}
if _, eerr = ep.ExpectWithContext(ctx, "..."); eerr == nil {
if _, eerr = ep.ExpectWithContext(ctx, ExpectedResponse{Value: "..."}); eerr == nil {
t.Fatalf("expected error on closed expect process")
}
}
Expand All @@ -149,7 +149,7 @@ func TestLineCount(t *testing.T) {
t.Fatal(err)
}
wstr := "3"
l, eerr := ep.ExpectWithContext(context.Background(), wstr)
l, eerr := ep.ExpectWithContext(context.Background(), ExpectedResponse{Value: wstr})
if eerr != nil {
t.Fatal(eerr)
}
Expand All @@ -172,7 +172,7 @@ func TestSend(t *testing.T) {
if err := ep.Send("a\r"); err != nil {
t.Fatal(err)
}
if _, err := ep.ExpectWithContext(context.Background(), "b"); err != nil {
if _, err := ep.ExpectWithContext(context.Background(), ExpectedResponse{Value: "b"}); err != nil {
t.Fatal(err)
}
if err := ep.Stop(); err != nil {
Expand Down
5 changes: 3 additions & 2 deletions tests/e2e/corrupt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (

"go.etcd.io/etcd/api/v3/etcdserverpb"
clientv3 "go.etcd.io/etcd/client/v3"
"go.etcd.io/etcd/pkg/v3/expect"
"go.etcd.io/etcd/server/v3/storage/datadir"
"go.etcd.io/etcd/server/v3/storage/mvcc/testutil"
"go.etcd.io/etcd/tests/v3/framework/config"
Expand Down Expand Up @@ -334,11 +335,11 @@ func TestCompactHashCheckDetectCorruptionInterrupt(t *testing.T) {
err = epc.Procs[slowCompactionNodeIndex].Restart(ctx)

// Wait until the node finished compaction and the leader finished compaction hash check
_, err = epc.Procs[slowCompactionNodeIndex].Logs().ExpectWithContext(ctx, "finished scheduled compaction")
_, err = epc.Procs[slowCompactionNodeIndex].Logs().ExpectWithContext(ctx, expect.ExpectedResponse{Value: "finished scheduled compaction"})
require.NoError(t, err, "can't get log indicating finished scheduled compaction")

leaderIndex := epc.WaitLeader(t)
_, err = epc.Procs[leaderIndex].Logs().ExpectWithContext(ctx, "finished compaction hash check")
_, err = epc.Procs[leaderIndex].Logs().ExpectWithContext(ctx, expect.ExpectedResponse{Value: "finished compaction hash check"})
require.NoError(t, err, "can't get log indicating finished compaction hash check")

alarmResponse, err := cc.AlarmList(ctx)
Expand Down
17 changes: 9 additions & 8 deletions tests/e2e/ctl_v3_auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (

"github.com/stretchr/testify/require"

"go.etcd.io/etcd/pkg/v3/expect"
"go.etcd.io/etcd/tests/v3/framework/e2e"
)

Expand Down Expand Up @@ -58,25 +59,25 @@ func authEnable(cx ctlCtx) error {

func ctlV3AuthEnable(cx ctlCtx) error {
cmdArgs := append(cx.PrefixArgs(), "auth", "enable")
return e2e.SpawnWithExpectWithEnv(cmdArgs, cx.envMap, "Authentication Enabled")
return e2e.SpawnWithExpectWithEnv(cmdArgs, cx.envMap, expect.ExpectedResponse{Value: "Authentication Enabled"})
}

func ctlV3PutFailPerm(cx ctlCtx, key, val string) error {
return e2e.SpawnWithExpectWithEnv(append(cx.PrefixArgs(), "put", key, val), cx.envMap, "permission denied")
return e2e.SpawnWithExpectWithEnv(append(cx.PrefixArgs(), "put", key, val), cx.envMap, expect.ExpectedResponse{Value: "permission denied"})
}

func authSetupTestUser(cx ctlCtx) {
if err := ctlV3User(cx, []string{"add", "test-user", "--interactive=false"}, "User test-user created", []string{"pass"}); err != nil {
cx.t.Fatal(err)
}
if err := e2e.SpawnWithExpectWithEnv(append(cx.PrefixArgs(), "role", "add", "test-role"), cx.envMap, "Role test-role created"); err != nil {
if err := e2e.SpawnWithExpectWithEnv(append(cx.PrefixArgs(), "role", "add", "test-role"), cx.envMap, expect.ExpectedResponse{Value: "Role test-role created"}); err != nil {
cx.t.Fatal(err)
}
if err := ctlV3User(cx, []string{"grant-role", "test-user", "test-role"}, "Role test-role is granted to user test-user", nil); err != nil {
cx.t.Fatal(err)
}
cmd := append(cx.PrefixArgs(), "role", "grant-permission", "test-role", "readwrite", "foo")
if err := e2e.SpawnWithExpectWithEnv(cmd, cx.envMap, "Role test-role updated"); err != nil {
if err := e2e.SpawnWithExpectWithEnv(cmd, cx.envMap, expect.ExpectedResponse{Value: "Role test-role updated"}); err != nil {
cx.t.Fatal(err)
}
}
Expand Down Expand Up @@ -118,7 +119,7 @@ func authTestCertCN(cx ctlCtx) {
if err := ctlV3User(cx, []string{"add", "example.com", "--interactive=false"}, "User example.com created", []string{""}); err != nil {
cx.t.Fatal(err)
}
if err := e2e.SpawnWithExpectWithEnv(append(cx.PrefixArgs(), "role", "add", "test-role"), cx.envMap, "Role test-role created"); err != nil {
if err := e2e.SpawnWithExpectWithEnv(append(cx.PrefixArgs(), "role", "add", "test-role"), cx.envMap, expect.ExpectedResponse{Value: "Role test-role created"}); err != nil {
cx.t.Fatal(err)
}
if err := ctlV3User(cx, []string{"grant-role", "example.com", "test-role"}, "Role test-role is granted to user example.com", nil); err != nil {
Expand Down Expand Up @@ -379,7 +380,7 @@ func certCNAndUsername(cx ctlCtx, noPassword bool) {
cx.t.Fatal(err)
}
}
if err := e2e.SpawnWithExpectWithEnv(append(cx.PrefixArgs(), "role", "add", "test-role-cn"), cx.envMap, "Role test-role-cn created"); err != nil {
if err := e2e.SpawnWithExpectWithEnv(append(cx.PrefixArgs(), "role", "add", "test-role-cn"), cx.envMap, expect.ExpectedResponse{Value: "Role test-role-cn created"}); err != nil {
cx.t.Fatal(err)
}
if err := ctlV3User(cx, []string{"grant-role", "example.com", "test-role-cn"}, "Role test-role-cn is granted to user example.com", nil); err != nil {
Expand Down Expand Up @@ -428,9 +429,9 @@ func authTestCertCNAndUsernameNoPassword(cx ctlCtx) {

func ctlV3EndpointHealth(cx ctlCtx) error {
cmdArgs := append(cx.PrefixArgs(), "endpoint", "health")
lines := make([]string, cx.epc.Cfg.ClusterSize)
lines := make([]expect.ExpectedResponse, cx.epc.Cfg.ClusterSize)
for i := range lines {
lines[i] = "is healthy"
lines[i] = expect.ExpectedResponse{Value: "is healthy"}
}
return e2e.SpawnWithExpects(cmdArgs, cx.envMap, lines...)
}
Expand Down
7 changes: 4 additions & 3 deletions tests/e2e/ctl_v3_defrag_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package e2e
import (
"testing"

"go.etcd.io/etcd/pkg/v3/expect"
"go.etcd.io/etcd/tests/v3/framework/e2e"
)

Expand All @@ -35,16 +36,16 @@ func maintenanceInitKeys(cx ctlCtx) {

func ctlV3OnlineDefrag(cx ctlCtx) error {
cmdArgs := append(cx.PrefixArgs(), "defrag")
lines := make([]string, cx.epc.Cfg.ClusterSize)
lines := make([]expect.ExpectedResponse, cx.epc.Cfg.ClusterSize)
for i := range lines {
lines[i] = "Finished defragmenting etcd member"
lines[i] = expect.ExpectedResponse{Value: "Finished defragmenting etcd member"}
}
return e2e.SpawnWithExpects(cmdArgs, cx.envMap, lines...)
}

func ctlV3OfflineDefrag(cx ctlCtx) error {
cmdArgs := append(cx.PrefixArgsUtl(), "defrag", "--data-dir", cx.dataDir)
lines := []string{"finished defragmenting directory"}
lines := []expect.ExpectedResponse{{Value: "finished defragmenting directory"}}
return e2e.SpawnWithExpects(cmdArgs, cx.envMap, lines...)
}

Expand Down
3 changes: 2 additions & 1 deletion tests/e2e/ctl_v3_grpc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (

"github.com/stretchr/testify/assert"

"go.etcd.io/etcd/pkg/v3/expect"
"go.etcd.io/etcd/tests/v3/framework/config"
"go.etcd.io/etcd/tests/v3/framework/e2e"
"go.etcd.io/etcd/tests/v3/framework/testutils"
Expand Down Expand Up @@ -160,7 +161,7 @@ func templateEndpoints(t *testing.T, pattern string, clus *e2e.EtcdProcessCluste

func assertAuthority(t *testing.T, expectAuthorityPattern string, clus *e2e.EtcdProcessCluster) {
for i := range clus.Procs {
line, _ := clus.Procs[i].Logs().ExpectWithContext(context.TODO(), `http2: decoded hpack field header field ":authority"`)
line, _ := clus.Procs[i].Logs().ExpectWithContext(context.TODO(), expect.ExpectedResponse{Value: `http2: decoded hpack field header field ":authority"`})
line = strings.TrimSuffix(line, "\n")
line = strings.TrimSuffix(line, "\r")

Expand Down
35 changes: 13 additions & 22 deletions tests/e2e/ctl_v3_kv_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (

"github.com/stretchr/testify/require"

"go.etcd.io/etcd/pkg/v3/expect"
"go.etcd.io/etcd/tests/v3/framework/e2e"
)

Expand Down Expand Up @@ -178,7 +179,7 @@ func getFormatTest(cx ctlCtx) {
cmdArgs = append(cmdArgs, "--print-value-only")
}
cmdArgs = append(cmdArgs, "abc")
if err := e2e.SpawnWithExpectWithEnv(cmdArgs, cx.envMap, tt.wstr); err != nil {
if err := e2e.SpawnWithExpectWithEnv(cmdArgs, cx.envMap, expect.ExpectedResponse{Value: tt.wstr}); err != nil {
cx.t.Errorf("#%d: error (%v), wanted %v", i, err, tt.wstr)
}
}
Expand Down Expand Up @@ -216,28 +217,28 @@ func getKeysOnlyTest(cx ctlCtx) {
cx.t.Fatal(err)
}
cmdArgs := append(cx.PrefixArgs(), []string{"get", "--keys-only", "key"}...)
if err := e2e.SpawnWithExpectWithEnv(cmdArgs, cx.envMap, "key"); err != nil {
if err := e2e.SpawnWithExpectWithEnv(cmdArgs, cx.envMap, expect.ExpectedResponse{Value: "key"}); err != nil {
cx.t.Fatal(err)
}

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

lines, err := e2e.SpawnWithExpectLines(ctx, cmdArgs, cx.envMap, "key")
lines, err := e2e.SpawnWithExpectLines(ctx, cmdArgs, cx.envMap, expect.ExpectedResponse{Value: "key"})
require.NoError(cx.t, err)
require.NotContains(cx.t, lines, "val", "got value but passed --keys-only")
}

func getCountOnlyTest(cx ctlCtx) {
cmdArgs := append(cx.PrefixArgs(), []string{"get", "--count-only", "key", "--prefix", "--write-out=fields"}...)
if err := e2e.SpawnWithExpects(cmdArgs, cx.envMap, "\"Count\" : 0"); err != nil {
if err := e2e.SpawnWithExpects(cmdArgs, cx.envMap, expect.ExpectedResponse{Value: "\"Count\" : 0"}); err != nil {
cx.t.Fatal(err)
}
if err := ctlV3Put(cx, "key", "val", ""); err != nil {
cx.t.Fatal(err)
}
cmdArgs = append(cx.PrefixArgs(), []string{"get", "--count-only", "key", "--prefix", "--write-out=fields"}...)
if err := e2e.SpawnWithExpects(cmdArgs, cx.envMap, "\"Count\" : 1"); err != nil {
if err := e2e.SpawnWithExpects(cmdArgs, cx.envMap, expect.ExpectedResponse{Value: "\"Count\" : 1"}); err != nil {
cx.t.Fatal(err)
}
if err := ctlV3Put(cx, "key1", "val", ""); err != nil {
Expand All @@ -247,22 +248,22 @@ func getCountOnlyTest(cx ctlCtx) {
cx.t.Fatal(err)
}
cmdArgs = append(cx.PrefixArgs(), []string{"get", "--count-only", "key", "--prefix", "--write-out=fields"}...)
if err := e2e.SpawnWithExpects(cmdArgs, cx.envMap, "\"Count\" : 2"); err != nil {
if err := e2e.SpawnWithExpects(cmdArgs, cx.envMap, expect.ExpectedResponse{Value: "\"Count\" : 2"}); err != nil {
cx.t.Fatal(err)
}
if err := ctlV3Put(cx, "key2", "val", ""); err != nil {
cx.t.Fatal(err)
}
cmdArgs = append(cx.PrefixArgs(), []string{"get", "--count-only", "key", "--prefix", "--write-out=fields"}...)
if err := e2e.SpawnWithExpects(cmdArgs, cx.envMap, "\"Count\" : 3"); err != nil {
if err := e2e.SpawnWithExpects(cmdArgs, cx.envMap, expect.ExpectedResponse{Value: "\"Count\" : 3"}); err != nil {
cx.t.Fatal(err)
}

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

cmdArgs = append(cx.PrefixArgs(), []string{"get", "--count-only", "key3", "--prefix", "--write-out=fields"}...)
lines, err := e2e.SpawnWithExpectLines(ctx, cmdArgs, cx.envMap, "\"Count\"")
lines, err := e2e.SpawnWithExpectLines(ctx, cmdArgs, cx.envMap, expect.ExpectedResponse{Value: "\"Count\""})
require.NoError(cx.t, err)
require.NotContains(cx.t, lines, "\"Count\" : 3")
}
Expand Down Expand Up @@ -341,7 +342,7 @@ func ctlV3Put(cx ctlCtx, key, value, leaseID string, flags ...string) error {
if len(flags) != 0 {
cmdArgs = append(cmdArgs, flags...)
}
return e2e.SpawnWithExpectWithEnv(cmdArgs, cx.envMap, "OK")
return e2e.SpawnWithExpectWithEnv(cmdArgs, cx.envMap, expect.ExpectedResponse{Value: "OK"})
}

type kv struct {
Expand All @@ -354,25 +355,15 @@ func ctlV3Get(cx ctlCtx, args []string, kvs ...kv) error {
if !cx.quorum {
cmdArgs = append(cmdArgs, "--consistency", "s")
}
var lines []string
var lines []expect.ExpectedResponse
for _, elem := range kvs {
lines = append(lines, elem.key, elem.val)
lines = append(lines, expect.ExpectedResponse{Value: elem.key}, expect.ExpectedResponse{Value: elem.val})
}
return e2e.SpawnWithExpects(cmdArgs, cx.envMap, lines...)
}

// ctlV3GetWithErr runs "get" command expecting no output but error
func ctlV3GetWithErr(cx ctlCtx, args []string, errs []string) error {
cmdArgs := append(cx.PrefixArgs(), "get")
cmdArgs = append(cmdArgs, args...)
if !cx.quorum {
cmdArgs = append(cmdArgs, "--consistency", "s")
}
return e2e.SpawnWithExpects(cmdArgs, cx.envMap, errs...)
}

func ctlV3Del(cx ctlCtx, args []string, num int) error {
cmdArgs := append(cx.PrefixArgs(), "del")
cmdArgs = append(cmdArgs, args...)
return e2e.SpawnWithExpects(cmdArgs, cx.envMap, fmt.Sprintf("%d", num))
return e2e.SpawnWithExpects(cmdArgs, cx.envMap, expect.ExpectedResponse{Value: fmt.Sprintf("%d", num)})
}
5 changes: 3 additions & 2 deletions tests/e2e/ctl_v3_lease_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"strings"
"testing"

"go.etcd.io/etcd/pkg/v3/expect"
"go.etcd.io/etcd/tests/v3/framework/e2e"
)

Expand Down Expand Up @@ -93,13 +94,13 @@ func ctlV3LeaseKeepAlive(cx ctlCtx, leaseID string) error {

func ctlV3LeaseRevoke(cx ctlCtx, leaseID string) error {
cmdArgs := append(cx.PrefixArgs(), "lease", "revoke", leaseID)
return e2e.SpawnWithExpectWithEnv(cmdArgs, cx.envMap, fmt.Sprintf("lease %s revoked", leaseID))
return e2e.SpawnWithExpectWithEnv(cmdArgs, cx.envMap, expect.ExpectedResponse{Value: fmt.Sprintf("lease %s revoked", leaseID)})
}

func ctlV3LeaseTimeToLive(cx ctlCtx, leaseID string, withKeys bool) error {
cmdArgs := append(cx.PrefixArgs(), "lease", "timetolive", leaseID)
if withKeys {
cmdArgs = append(cmdArgs, "--keys")
}
return e2e.SpawnWithExpectWithEnv(cmdArgs, cx.envMap, fmt.Sprintf("lease %s granted with", leaseID))
return e2e.SpawnWithExpectWithEnv(cmdArgs, cx.envMap, expect.ExpectedResponse{Value: fmt.Sprintf("lease %s granted with", leaseID)})
}
8 changes: 4 additions & 4 deletions tests/e2e/ctl_v3_lock_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,16 +114,16 @@ func testLock(cx ctlCtx) {
func testLockWithCmd(cx ctlCtx) {
// exec command with zero exit code
echoCmd := []string{"echo"}
if err := ctlV3LockWithCmd(cx, echoCmd, ""); err != nil {
if err := ctlV3LockWithCmd(cx, echoCmd, expect.ExpectedResponse{Value: ""}); err != nil {
cx.t.Fatal(err)
}

// exec command with non-zero exit code
code := 3
awkCmd := []string{"awk", fmt.Sprintf("BEGIN{exit %d}", code)}
expect := fmt.Sprintf("Error: exit status %d", code)
expect := expect.ExpectedResponse{Value: fmt.Sprintf("Error: exit status %d", code)}
err := ctlV3LockWithCmd(cx, awkCmd, expect)
require.ErrorContains(cx.t, err, expect)
require.ErrorContains(cx.t, err, expect.Value)
}

// ctlV3Lock creates a lock process with a channel listening for when it acquires the lock.
Expand All @@ -149,7 +149,7 @@ func ctlV3Lock(cx ctlCtx, name string) (*expect.ExpectProcess, <-chan string, er
}

// ctlV3LockWithCmd creates a lock process to exec command.
func ctlV3LockWithCmd(cx ctlCtx, execCmd []string, as ...string) error {
func ctlV3LockWithCmd(cx ctlCtx, execCmd []string, as ...expect.ExpectedResponse) error {
// use command as lock name
cmdArgs := append(cx.PrefixArgs(), "lock", execCmd[0])
cmdArgs = append(cmdArgs, execCmd...)
Expand Down
Loading

0 comments on commit 14ef651

Please sign in to comment.