Skip to content

Commit

Permalink
Merge pull request etcd-io#16472 from ahrtr/expr_expect_20230825
Browse files Browse the repository at this point in the history
test: support regular expression matching on the response
  • Loading branch information
ahrtr committed Aug 25, 2023
2 parents 7ea5ee8 + 7cbab60 commit b6935cf
Show file tree
Hide file tree
Showing 30 changed files with 234 additions and 138 deletions.
27 changes: 24 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,29 @@ 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) {
var (
expr *regexp.Regexp
err error
)
if s.IsRegularExpr {
expr, err = regexp.Compile(s.Value)
if err != nil {
return "", err
}
}
return ep.ExpectFunc(ctx, func(txt string) bool {
if expr != nil {
return expr.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
64 changes: 60 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 Expand Up @@ -218,3 +218,59 @@ func TestExpectForFailFastCommand(t *testing.T) {
_, err = ep.Expect("failed setting cipher list")
require.NoError(t, err)
}

func TestResponseMatchRegularExpr(t *testing.T) {
testCases := []struct {
name string
mockOutput string
expectedResp ExpectedResponse
expectMatch bool
}{
{
name: "exact match",
mockOutput: "hello world",
expectedResp: ExpectedResponse{Value: "hello world"},
expectMatch: true,
},
{
name: "not exact match",
mockOutput: "hello world",
expectedResp: ExpectedResponse{Value: "hello wld"},
expectMatch: false,
},
{
name: "match regular expression",
mockOutput: "hello world",
expectedResp: ExpectedResponse{Value: `.*llo\sworld`, IsRegularExpr: true},
expectMatch: true,
},
{
name: "not match regular expression",
mockOutput: "hello world",
expectedResp: ExpectedResponse{Value: `.*llo wrld`, IsRegularExpr: true},
expectMatch: false,
},
}

for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {

ep, err := NewExpect("echo", "-n", tc.mockOutput)
require.NoError(t, err)

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
l, err := ep.ExpectWithContext(ctx, tc.expectedResp)

if tc.expectMatch {
require.Equal(t, tc.mockOutput, l)
} else {
require.Error(t, err)
}

cerr := ep.Close()
require.NoError(t, cerr)
})
}
}
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)})
}
Loading

0 comments on commit b6935cf

Please sign in to comment.