diff --git a/pkg/expect/expect.go b/pkg/expect/expect.go index b7e2978af5c2..a9a46555d173 100644 --- a/pkg/expect/expect.go +++ b/pkg/expect/expect.go @@ -23,6 +23,7 @@ import ( "io" "os" "os/exec" + "regexp" "strings" "sync" "syscall" @@ -37,6 +38,11 @@ var ( ErrProcessRunning = fmt.Errorf("process is still running") ) +type ExpectedResponse struct { + Value string + IsRegularExpr bool +} + type ExpectProcess struct { cfg expectConfig @@ -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 diff --git a/tests/e2e/corrupt_test.go b/tests/e2e/corrupt_test.go index 7a463c68949b..273a7019be0c 100644 --- a/tests/e2e/corrupt_test.go +++ b/tests/e2e/corrupt_test.go @@ -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" @@ -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) diff --git a/tests/e2e/ctl_v3_auth_test.go b/tests/e2e/ctl_v3_auth_test.go index 1b01303e9ca3..a7ca4c15ac10 100644 --- a/tests/e2e/ctl_v3_auth_test.go +++ b/tests/e2e/ctl_v3_auth_test.go @@ -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" ) @@ -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) } } @@ -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 { @@ -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 { @@ -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...) } diff --git a/tests/e2e/ctl_v3_defrag_test.go b/tests/e2e/ctl_v3_defrag_test.go index 07f6bd443439..ca1d23540cb9 100644 --- a/tests/e2e/ctl_v3_defrag_test.go +++ b/tests/e2e/ctl_v3_defrag_test.go @@ -17,6 +17,7 @@ package e2e import ( "testing" + "go.etcd.io/etcd/pkg/v3/expect" "go.etcd.io/etcd/tests/v3/framework/e2e" ) @@ -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...) } diff --git a/tests/e2e/ctl_v3_grpc_test.go b/tests/e2e/ctl_v3_grpc_test.go index e00526a38dba..30567782e875 100644 --- a/tests/e2e/ctl_v3_grpc_test.go +++ b/tests/e2e/ctl_v3_grpc_test.go @@ -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" @@ -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") diff --git a/tests/e2e/ctl_v3_kv_test.go b/tests/e2e/ctl_v3_kv_test.go index 9df307ba0734..a24103e57a27 100644 --- a/tests/e2e/ctl_v3_kv_test.go +++ b/tests/e2e/ctl_v3_kv_test.go @@ -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" ) @@ -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) } } @@ -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 { @@ -247,14 +248,14 @@ 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) } @@ -262,7 +263,7 @@ func getCountOnlyTest(cx ctlCtx) { 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") } @@ -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 { @@ -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)}) } diff --git a/tests/e2e/ctl_v3_lease_test.go b/tests/e2e/ctl_v3_lease_test.go index f5bc382b6dba..43a66bf2f2c7 100644 --- a/tests/e2e/ctl_v3_lease_test.go +++ b/tests/e2e/ctl_v3_lease_test.go @@ -20,6 +20,7 @@ import ( "strings" "testing" + "go.etcd.io/etcd/pkg/v3/expect" "go.etcd.io/etcd/tests/v3/framework/e2e" ) @@ -93,7 +94,7 @@ 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 { @@ -101,5 +102,5 @@ func ctlV3LeaseTimeToLive(cx ctlCtx, leaseID string, withKeys bool) error { 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)}) } diff --git a/tests/e2e/ctl_v3_lock_test.go b/tests/e2e/ctl_v3_lock_test.go index cc0822f2644d..326c0b7e48a9 100644 --- a/tests/e2e/ctl_v3_lock_test.go +++ b/tests/e2e/ctl_v3_lock_test.go @@ -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. @@ -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...) diff --git a/tests/e2e/ctl_v3_member_test.go b/tests/e2e/ctl_v3_member_test.go index 463ab2ca5787..47af0cf6b234 100644 --- a/tests/e2e/ctl_v3_member_test.go +++ b/tests/e2e/ctl_v3_member_test.go @@ -25,6 +25,7 @@ import ( "github.com/stretchr/testify/require" "go.etcd.io/etcd/api/v3/etcdserverpb" + "go.etcd.io/etcd/pkg/v3/expect" "go.etcd.io/etcd/tests/v3/framework/e2e" ) @@ -75,9 +76,9 @@ func memberListSerializableTest(cx ctlCtx) { func ctlV3MemberList(cx ctlCtx) error { cmdArgs := append(cx.PrefixArgs(), "member", "list") - lines := make([]string, cx.cfg.ClusterSize) + lines := make([]expect.ExpectedResponse, cx.cfg.ClusterSize) for i := range lines { - lines[i] = "started" + lines[i] = expect.ExpectedResponse{Value: "started"} } return e2e.SpawnWithExpects(cmdArgs, cx.envMap, lines...) } @@ -162,7 +163,7 @@ func memberListWithHexTest(cx ctlCtx) { func ctlV3MemberRemove(cx ctlCtx, ep, memberID, clusterID string) error { cmdArgs := append(cx.prefixArgs([]string{ep}), "member", "remove", memberID) - return e2e.SpawnWithExpectWithEnv(cmdArgs, cx.envMap, fmt.Sprintf("%s removed from cluster %s", memberID, clusterID)) + return e2e.SpawnWithExpectWithEnv(cmdArgs, cx.envMap, expect.ExpectedResponse{Value: fmt.Sprintf("%s removed from cluster %s", memberID, clusterID)}) } func memberAddTest(cx ctlCtx) { @@ -186,7 +187,7 @@ func ctlV3MemberAdd(cx ctlCtx, peerURL string, isLearner bool) error { cmdArgs = append(cmdArgs, "--learner") asLearner = " as learner " } - return e2e.SpawnWithExpectWithEnv(cmdArgs, cx.envMap, fmt.Sprintf(" added%sto cluster ", asLearner)) + return e2e.SpawnWithExpectWithEnv(cmdArgs, cx.envMap, expect.ExpectedResponse{Value: fmt.Sprintf(" added%sto cluster ", asLearner)}) } func memberUpdateTest(cx ctlCtx) { @@ -204,5 +205,5 @@ func memberUpdateTest(cx ctlCtx) { func ctlV3MemberUpdate(cx ctlCtx, memberID, peerURL string) error { cmdArgs := append(cx.PrefixArgs(), "member", "update", memberID, fmt.Sprintf("--peer-urls=%s", peerURL)) - return e2e.SpawnWithExpectWithEnv(cmdArgs, cx.envMap, " updated in cluster ") + return e2e.SpawnWithExpectWithEnv(cmdArgs, cx.envMap, expect.ExpectedResponse{Value: " updated in cluster "}) } diff --git a/tests/e2e/ctl_v3_move_leader_test.go b/tests/e2e/ctl_v3_move_leader_test.go index 6a05c43211ef..0410d9e2de57 100644 --- a/tests/e2e/ctl_v3_move_leader_test.go +++ b/tests/e2e/ctl_v3_move_leader_test.go @@ -26,6 +26,7 @@ import ( "go.etcd.io/etcd/client/pkg/v3/transport" "go.etcd.io/etcd/client/pkg/v3/types" clientv3 "go.etcd.io/etcd/client/v3" + "go.etcd.io/etcd/pkg/v3/expect" "go.etcd.io/etcd/tests/v3/framework/e2e" ) @@ -135,7 +136,7 @@ func testCtlV3MoveLeader(t *testing.T, cfg e2e.EtcdProcessClusterConfig, envVars for i, tc := range tests { prefix := cx.prefixArgs(tc.eps) cmdArgs := append(prefix, "move-leader", types.ID(transferee).String()) - err := e2e.SpawnWithExpectWithEnv(cmdArgs, cx.envMap, tc.expect) + err := e2e.SpawnWithExpectWithEnv(cmdArgs, cx.envMap, expect.ExpectedResponse{Value: tc.expect}) if tc.expectErr { require.ErrorContains(t, err, tc.expect) } else { diff --git a/tests/e2e/ctl_v3_role_test.go b/tests/e2e/ctl_v3_role_test.go index 7bdd7459804e..47aaf655efc4 100644 --- a/tests/e2e/ctl_v3_role_test.go +++ b/tests/e2e/ctl_v3_role_test.go @@ -18,6 +18,7 @@ import ( "fmt" "testing" + "go.etcd.io/etcd/pkg/v3/expect" "go.etcd.io/etcd/tests/v3/framework/e2e" ) @@ -55,7 +56,7 @@ func ctlV3Role(cx ctlCtx, args []string, expStr string) error { cmdArgs := append(cx.PrefixArgs(), "role") cmdArgs = append(cmdArgs, args...) - return e2e.SpawnWithExpectWithEnv(cmdArgs, cx.envMap, expStr) + return e2e.SpawnWithExpectWithEnv(cmdArgs, cx.envMap, expect.ExpectedResponse{Value: expStr}) } func ctlV3RoleGrantPermission(cx ctlCtx, rolename string, perm grantingPerm) error { diff --git a/tests/e2e/ctl_v3_snapshot_test.go b/tests/e2e/ctl_v3_snapshot_test.go index 1a3620bd6bbc..fadb989ae410 100644 --- a/tests/e2e/ctl_v3_snapshot_test.go +++ b/tests/e2e/ctl_v3_snapshot_test.go @@ -94,7 +94,7 @@ func snapshotCorruptTest(cx ctlCtx) { "--data-dir", datadir, fpath), cx.envMap, - "expected sha256") + expect.ExpectedResponse{Value: "expected sha256"}) require.ErrorContains(cx.t, serr, "Error: expected sha256") } @@ -125,7 +125,7 @@ func snapshotStatusBeforeRestoreTest(cx ctlCtx) { "--data-dir", dataDir, fpath), cx.envMap, - "added member") + expect.ExpectedResponse{Value: "added member"}) if serr != nil { cx.t.Fatal(serr) } @@ -133,7 +133,7 @@ func snapshotStatusBeforeRestoreTest(cx ctlCtx) { func ctlV3SnapshotSave(cx ctlCtx, fpath string) error { cmdArgs := append(cx.PrefixArgs(), "snapshot", "save", fpath) - return e2e.SpawnWithExpectWithEnv(cmdArgs, cx.envMap, fmt.Sprintf("Snapshot saved at %s", fpath)) + return e2e.SpawnWithExpectWithEnv(cmdArgs, cx.envMap, expect.ExpectedResponse{Value: fmt.Sprintf("Snapshot saved at %s", fpath)}) } func getSnapshotStatus(cx ctlCtx, fpath string) (snapshot.Status, error) { @@ -194,7 +194,7 @@ func testIssue6361(t *testing.T) { t.Log("Writing some keys...") kvs := []kv{{"foo1", "val1"}, {"foo2", "val2"}, {"foo3", "val3"}} for i := range kvs { - if err = e2e.SpawnWithExpect(append(prefixArgs, "put", kvs[i].key, kvs[i].val), "OK"); err != nil { + if err = e2e.SpawnWithExpect(append(prefixArgs, "put", kvs[i].key, kvs[i].val), expect.ExpectedResponse{Value: "OK"}); err != nil { t.Fatal(err) } } @@ -204,7 +204,7 @@ func testIssue6361(t *testing.T) { t.Log("etcdctl saving snapshot...") if err = e2e.SpawnWithExpects(append(prefixArgs, "snapshot", "save", fpath), nil, - fmt.Sprintf("Snapshot saved at %s", fpath), + expect.ExpectedResponse{Value: fmt.Sprintf("Snapshot saved at %s", fpath)}, ); err != nil { t.Fatal(err) } @@ -216,7 +216,14 @@ func testIssue6361(t *testing.T) { newDataDir := filepath.Join(t.TempDir(), "test.data") t.Log("etcdctl restoring the snapshot...") - err = e2e.SpawnWithExpect([]string{e2e.BinPath.Etcdutl, "snapshot", "restore", fpath, "--name", epc.Procs[0].Config().Name, "--initial-cluster", epc.Procs[0].Config().InitialCluster, "--initial-cluster-token", epc.Procs[0].Config().InitialToken, "--initial-advertise-peer-urls", epc.Procs[0].Config().PeerURL.String(), "--data-dir", newDataDir}, "added member") + err = e2e.SpawnWithExpect([]string{ + e2e.BinPath.Etcdutl, "snapshot", "restore", fpath, + "--name", epc.Procs[0].Config().Name, + "--initial-cluster", epc.Procs[0].Config().InitialCluster, + "--initial-cluster-token", epc.Procs[0].Config().InitialToken, + "--initial-advertise-peer-urls", epc.Procs[0].Config().PeerURL.String(), + "--data-dir", newDataDir}, + expect.ExpectedResponse{Value: "added member"}) if err != nil { t.Fatal(err) } @@ -234,7 +241,7 @@ func testIssue6361(t *testing.T) { t.Log("Ensuring the restored member has the correct data...") for i := range kvs { - if err = e2e.SpawnWithExpect(append(prefixArgs, "get", kvs[i].key), kvs[i].val); err != nil { + if err = e2e.SpawnWithExpect(append(prefixArgs, "get", kvs[i].key), expect.ExpectedResponse{Value: kvs[i].val}); err != nil { t.Fatal(err) } } @@ -242,7 +249,7 @@ func testIssue6361(t *testing.T) { t.Log("Adding new member into the cluster") clientURL := fmt.Sprintf("http://localhost:%d", e2e.EtcdProcessBasePort+30) peerURL := fmt.Sprintf("http://localhost:%d", e2e.EtcdProcessBasePort+31) - err = e2e.SpawnWithExpect(append(prefixArgs, "member", "add", "newmember", fmt.Sprintf("--peer-urls=%s", peerURL)), " added to cluster ") + err = e2e.SpawnWithExpect(append(prefixArgs, "member", "add", "newmember", fmt.Sprintf("--peer-urls=%s", peerURL)), expect.ExpectedResponse{Value: " added to cluster "}) if err != nil { t.Fatal(err) } @@ -271,7 +278,7 @@ func testIssue6361(t *testing.T) { t.Log("Ensuring added member has data from incoming snapshot...") for i := range kvs { - if err = e2e.SpawnWithExpect(append(prefixArgs, "get", kvs[i].key), kvs[i].val); err != nil { + if err = e2e.SpawnWithExpect(append(prefixArgs, "get", kvs[i].key), expect.ExpectedResponse{Value: kvs[i].val}); err != nil { t.Fatal(err) } } @@ -350,7 +357,7 @@ func TestRestoreCompactionRevBump(t *testing.T) { t.Log("etcdctl saving snapshot...") cmdPrefix := []string{e2e.BinPath.Etcdctl, "--endpoints", strings.Join(epc.EndpointsGRPC(), ",")} - require.NoError(t, e2e.SpawnWithExpects(append(cmdPrefix, "snapshot", "save", fpath), nil, fmt.Sprintf("Snapshot saved at %s", fpath))) + require.NoError(t, e2e.SpawnWithExpects(append(cmdPrefix, "snapshot", "save", fpath), nil, expect.ExpectedResponse{Value: fmt.Sprintf("Snapshot saved at %s", fpath)})) // add some more kvs that are not in the snapshot that will be lost after restore unsnappedKVs := []testutils.KV{{Key: "unsnapped1", Val: "one"}, {Key: "unsnapped2", Val: "two"}, {Key: "unsnapped3", Val: "three"}} @@ -378,7 +385,7 @@ func TestRestoreCompactionRevBump(t *testing.T) { "--bump-revision", fmt.Sprintf("%d", bumpAmount), "--mark-compacted", "--data-dir", newDataDir, - }, "added member") + }, expect.ExpectedResponse{Value: "added member"}) require.NoError(t, err) t.Log("(Re)starting the etcd member using the restored snapshot...") diff --git a/tests/e2e/ctl_v3_test.go b/tests/e2e/ctl_v3_test.go index 405aa150622f..07158a734fe3 100644 --- a/tests/e2e/ctl_v3_test.go +++ b/tests/e2e/ctl_v3_test.go @@ -26,6 +26,7 @@ import ( "go.etcd.io/etcd/api/v3/version" "go.etcd.io/etcd/client/pkg/v3/testutil" + "go.etcd.io/etcd/pkg/v3/expect" "go.etcd.io/etcd/pkg/v3/flags" "go.etcd.io/etcd/tests/v3/framework/e2e" ) @@ -88,7 +89,7 @@ func versionTest(cx ctlCtx) { func clusterVersionTest(cx ctlCtx, expected string) { var err error for i := 0; i < 35; i++ { - if err = e2e.CURLGet(cx.epc, e2e.CURLReq{Endpoint: "/version", Expected: expected}); err != nil { + if err = e2e.CURLGet(cx.epc, e2e.CURLReq{Endpoint: "/version", Expected: expect.ExpectedResponse{Value: expected}}); err != nil { cx.t.Logf("#%d: v3 is not ready yet (%v)", i, err) time.Sleep(200 * time.Millisecond) continue @@ -102,7 +103,7 @@ func clusterVersionTest(cx ctlCtx, expected string) { func ctlV3Version(cx ctlCtx) error { cmdArgs := append(cx.PrefixArgs(), "version") - return e2e.SpawnWithExpectWithEnv(cmdArgs, cx.envMap, version.Version) + return e2e.SpawnWithExpectWithEnv(cmdArgs, cx.envMap, expect.ExpectedResponse{Value: version.Version}) } // TestCtlV3DialWithHTTPScheme ensures that client handles Endpoints with HTTPS scheme. @@ -112,7 +113,7 @@ func TestCtlV3DialWithHTTPScheme(t *testing.T) { func dialWithSchemeTest(cx ctlCtx) { cmdArgs := append(cx.prefixArgs(cx.epc.EndpointsGRPC()), "put", "foo", "bar") - if err := e2e.SpawnWithExpectWithEnv(cmdArgs, cx.envMap, "OK"); err != nil { + if err := e2e.SpawnWithExpectWithEnv(cmdArgs, cx.envMap, expect.ExpectedResponse{Value: "OK"}); err != nil { cx.t.Fatal(err) } } diff --git a/tests/e2e/discovery_test.go b/tests/e2e/discovery_test.go index d7ed28bfc50e..576fd752c07c 100644 --- a/tests/e2e/discovery_test.go +++ b/tests/e2e/discovery_test.go @@ -26,6 +26,7 @@ import ( "go.etcd.io/etcd/client/pkg/v3/testutil" "go.etcd.io/etcd/client/pkg/v3/transport" "go.etcd.io/etcd/client/v2" + "go.etcd.io/etcd/pkg/v3/expect" "go.etcd.io/etcd/server/v3/etcdserver/api/rafthttp" "go.etcd.io/etcd/tests/v3/framework/e2e" "go.etcd.io/etcd/tests/v3/framework/integration" @@ -73,10 +74,10 @@ func testClusterUsingDiscovery(t *testing.T, size int, peerTLS bool) { defer c.Close() kubectl := []string{e2e.BinPath.Etcdctl, "--endpoints", strings.Join(c.EndpointsGRPC(), ",")} - if err := e2e.SpawnWithExpect(append(kubectl, "put", "key", "value"), "OK"); err != nil { + if err := e2e.SpawnWithExpect(append(kubectl, "put", "key", "value"), expect.ExpectedResponse{Value: "OK"}); err != nil { t.Fatal(err) } - if err := e2e.SpawnWithExpect(append(kubectl, "get", "key"), "value"); err != nil { + if err := e2e.SpawnWithExpect(append(kubectl, "get", "key"), expect.ExpectedResponse{Value: "value"}); err != nil { t.Fatal(err) } } diff --git a/tests/e2e/discovery_v3_test.go b/tests/e2e/discovery_v3_test.go index 17596c9be9bb..3302634418f6 100644 --- a/tests/e2e/discovery_v3_test.go +++ b/tests/e2e/discovery_v3_test.go @@ -21,6 +21,7 @@ import ( "strings" "testing" + "go.etcd.io/etcd/pkg/v3/expect" "go.etcd.io/etcd/tests/v3/framework/e2e" ) @@ -76,10 +77,10 @@ func testClusterUsingV3Discovery(t *testing.T, discoveryClusterSize, targetClust // step 4: sanity test on the etcd cluster etcdctl := []string{e2e.BinPath.Etcdctl, "--endpoints", strings.Join(epc.EndpointsGRPC(), ",")} - if err := e2e.SpawnWithExpect(append(etcdctl, "put", "key", "value"), "OK"); err != nil { + if err := e2e.SpawnWithExpect(append(etcdctl, "put", "key", "value"), expect.ExpectedResponse{Value: "OK"}); err != nil { t.Fatal(err) } - if err := e2e.SpawnWithExpect(append(etcdctl, "get", "key"), "value"); err != nil { + if err := e2e.SpawnWithExpect(append(etcdctl, "get", "key"), expect.ExpectedResponse{Value: "value"}); err != nil { t.Fatal(err) } } diff --git a/tests/e2e/etcd_config_test.go b/tests/e2e/etcd_config_test.go index b3df0eeaee70..2afe228006b8 100644 --- a/tests/e2e/etcd_config_test.go +++ b/tests/e2e/etcd_config_test.go @@ -296,7 +296,7 @@ func TestGrpcproxyAndCommonName(t *testing.T) { "--cacert", e2e.CaPath, } - err := e2e.SpawnWithExpect(argsWithNonEmptyCN, "cert has non empty Common Name") + err := e2e.SpawnWithExpect(argsWithNonEmptyCN, expect.ExpectedResponse{Value: "cert has non empty Common Name"}) require.ErrorContains(t, err, "cert has non empty Common Name") p, err := e2e.SpawnCmd(argsWithEmptyCN, nil) diff --git a/tests/e2e/etcd_release_upgrade_test.go b/tests/e2e/etcd_release_upgrade_test.go index c51240c26c4e..bbd2ff455623 100644 --- a/tests/e2e/etcd_release_upgrade_test.go +++ b/tests/e2e/etcd_release_upgrade_test.go @@ -23,6 +23,7 @@ import ( "go.etcd.io/etcd/api/v3/version" "go.etcd.io/etcd/client/pkg/v3/fileutil" + "go.etcd.io/etcd/pkg/v3/expect" "go.etcd.io/etcd/tests/v3/framework/e2e" ) @@ -97,7 +98,7 @@ func TestReleaseUpgrade(t *testing.T) { // new cluster version needs more time to upgrade ver := version.Cluster(version.Version) for i := 0; i < 7; i++ { - if err = e2e.CURLGet(epc, e2e.CURLReq{Endpoint: "/version", Expected: `"etcdcluster":"` + ver}); err != nil { + if err = e2e.CURLGet(epc, e2e.CURLReq{Endpoint: "/version", Expected: expect.ExpectedResponse{Value: `"etcdcluster":"` + ver}}); err != nil { t.Logf("#%d: %v is not ready yet (%v)", i, ver, err) time.Sleep(time.Second) continue diff --git a/tests/e2e/gateway_test.go b/tests/e2e/gateway_test.go index 078942832ac7..aefe3d4a713f 100644 --- a/tests/e2e/gateway_test.go +++ b/tests/e2e/gateway_test.go @@ -42,7 +42,7 @@ func TestGateway(t *testing.T) { p.Close() }() - err = e2e.SpawnWithExpect([]string{e2e.BinPath.Etcdctl, "--endpoints=" + defaultGatewayEndpoint, "put", "foo", "bar"}, "OK\r\n") + err = e2e.SpawnWithExpect([]string{e2e.BinPath.Etcdctl, "--endpoints=" + defaultGatewayEndpoint, "put", "foo", "bar"}, expect.ExpectedResponse{Value: "OK\r\n"}) if err != nil { t.Errorf("failed to finish put request through gateway: %v", err) } diff --git a/tests/e2e/metrics_test.go b/tests/e2e/metrics_test.go index 5a6199323877..312b5aab5e27 100644 --- a/tests/e2e/metrics_test.go +++ b/tests/e2e/metrics_test.go @@ -19,6 +19,7 @@ import ( "testing" "go.etcd.io/etcd/api/v3/version" + "go.etcd.io/etcd/pkg/v3/expect" "go.etcd.io/etcd/tests/v3/framework/e2e" ) @@ -63,7 +64,7 @@ func metricsTest(cx ctlCtx) { if err := ctlV3Watch(cx, []string{"k", "--rev", "1"}, []kvExec{{key: "k", val: "v"}}...); err != nil { cx.t.Fatal(err) } - if err := e2e.CURLGet(cx.epc, e2e.CURLReq{Endpoint: test.endpoint, Expected: test.expected}); err != nil { + if err := e2e.CURLGet(cx.epc, e2e.CURLReq{Endpoint: test.endpoint, Expected: expect.ExpectedResponse{Value: test.expected}}); err != nil { cx.t.Fatalf("failed get with curl (%v)", err) } } diff --git a/tests/e2e/utl_migrate_test.go b/tests/e2e/utl_migrate_test.go index 1513d34b4e1b..a58d69a24c74 100644 --- a/tests/e2e/utl_migrate_test.go +++ b/tests/e2e/utl_migrate_test.go @@ -29,6 +29,7 @@ import ( "go.etcd.io/etcd/api/v3/version" "go.etcd.io/etcd/client/pkg/v3/fileutil" + "go.etcd.io/etcd/pkg/v3/expect" "go.etcd.io/etcd/server/v3/storage/backend" "go.etcd.io/etcd/server/v3/storage/schema" "go.etcd.io/etcd/tests/v3/framework/e2e" @@ -139,7 +140,7 @@ func TestEtctlutlMigrate(t *testing.T) { t.Log("Write keys to ensure wal snapshot is created and all v3.5 fields are set...") for i := 0; i < 10; i++ { - if err = e2e.SpawnWithExpect(append(prefixArgs, "put", fmt.Sprintf("%d", i), "value"), "OK"); err != nil { + if err = e2e.SpawnWithExpect(append(prefixArgs, "put", fmt.Sprintf("%d", i), "value"), expect.ExpectedResponse{Value: "OK"}); err != nil { t.Fatal(err) } } @@ -155,7 +156,7 @@ func TestEtctlutlMigrate(t *testing.T) { if tc.force { args = append(args, "--force") } - err = e2e.SpawnWithExpect(args, tc.expectLogsSubString) + err = e2e.SpawnWithExpect(args, expect.ExpectedResponse{Value: tc.expectLogsSubString}) if err != nil { if tc.expectLogsSubString != "" { require.ErrorContains(t, err, tc.expectLogsSubString) diff --git a/tests/e2e/v2store_deprecation_test.go b/tests/e2e/v2store_deprecation_test.go index f85763e9c688..cbb407ebb7de 100644 --- a/tests/e2e/v2store_deprecation_test.go +++ b/tests/e2e/v2store_deprecation_test.go @@ -29,6 +29,7 @@ import ( "go.uber.org/zap/zaptest" "go.etcd.io/etcd/client/pkg/v3/fileutil" + "go.etcd.io/etcd/pkg/v3/expect" "go.etcd.io/etcd/server/v3/etcdserver" "go.etcd.io/etcd/server/v3/etcdserver/api/membership" "go.etcd.io/etcd/server/v3/etcdserver/api/snap" @@ -58,7 +59,7 @@ func createV2store(t testing.TB, dataDirPath string) string { for i := 0; i < 10; i++ { if err := e2e.CURLPut(epc, e2e.CURLReq{ Endpoint: "/v2/keys/foo", Value: "bar" + fmt.Sprint(i), - Expected: `{"action":"set","node":{"key":"/foo","value":"bar` + fmt.Sprint(i)}); err != nil { + Expected: expect.ExpectedResponse{Value: `{"action":"set","node":{"key":"/foo","value":"bar` + fmt.Sprint(i)}}); err != nil { t.Fatalf("failed put with curl (%v)", err) } } diff --git a/tests/e2e/v3_cipher_suite_test.go b/tests/e2e/v3_cipher_suite_test.go index 074dcfe50240..7a2b37a098f1 100644 --- a/tests/e2e/v3_cipher_suite_test.go +++ b/tests/e2e/v3_cipher_suite_test.go @@ -23,6 +23,7 @@ import ( "github.com/stretchr/testify/require" "go.etcd.io/etcd/api/v3/version" + "go.etcd.io/etcd/pkg/v3/expect" "go.etcd.io/etcd/tests/v3/framework/e2e" ) @@ -49,7 +50,7 @@ func testV3CurlCipherSuites(t *testing.T, valid bool) { func cipherSuiteTestValid(cx ctlCtx) { if err := e2e.CURLGet(cx.epc, e2e.CURLReq{ Endpoint: "/metrics", - Expected: fmt.Sprintf(`etcd_server_version{server_version="%s"} 1`, version.Version), + Expected: expect.ExpectedResponse{Value: fmt.Sprintf(`etcd_server_version{server_version="%s"} 1`, version.Version)}, Ciphers: "ECDHE-RSA-AES128-GCM-SHA256", // TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 }); err != nil { require.ErrorContains(cx.t, err, fmt.Sprintf(`etcd_server_version{server_version="%s"} 1`, version.Version)) @@ -59,7 +60,7 @@ func cipherSuiteTestValid(cx ctlCtx) { func cipherSuiteTestMismatch(cx ctlCtx) { err := e2e.CURLGet(cx.epc, e2e.CURLReq{ Endpoint: "/metrics", - Expected: "failed setting cipher list", + Expected: expect.ExpectedResponse{Value: "failed setting cipher list"}, Ciphers: "ECDHE-RSA-DES-CBC3-SHA", // TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA }) require.ErrorContains(cx.t, err, "curl: (59) failed setting cipher list") diff --git a/tests/e2e/v3_curl_maxstream_test.go b/tests/e2e/v3_curl_maxstream_test.go index 2e8dfe287375..662c5d8eb39f 100644 --- a/tests/e2e/v3_curl_maxstream_test.go +++ b/tests/e2e/v3_curl_maxstream_test.go @@ -29,6 +29,7 @@ import ( pb "go.etcd.io/etcd/api/v3/etcdserverpb" "go.etcd.io/etcd/client/pkg/v3/testutil" + "go.etcd.io/etcd/pkg/v3/expect" "go.etcd.io/etcd/tests/v3/framework/e2e" "go.etcd.io/etcd/tests/v3/framework/testutils" ) @@ -166,7 +167,7 @@ func submitConcurrentWatch(cx ctlCtx, number int, wgDone *sync.WaitGroup, closeC // make sure that watch request has been created expectedLine := `"created":true}}` - _, lerr := proc.ExpectWithContext(context.TODO(), expectedLine) + _, lerr := proc.ExpectWithContext(context.TODO(), expect.ExpectedResponse{Value: expectedLine}) if lerr != nil { return fmt.Errorf("%v %v (expected %q). Try EXPECT_DEBUG=TRUE", args, lerr, expectedLine) } @@ -213,7 +214,7 @@ func submitRangeAfterConcurrentWatch(cx ctlCtx, expectedValue string) { } cx.t.Log("Submitting range request...") - if err := e2e.CURLPost(cx.epc, e2e.CURLReq{Endpoint: "/v3/kv/range", Value: string(rangeData), Expected: expectedValue, Timeout: 5}); err != nil { + if err := e2e.CURLPost(cx.epc, e2e.CURLReq{Endpoint: "/v3/kv/range", Value: string(rangeData), Expected: expect.ExpectedResponse{Value: expectedValue}, Timeout: 5}); err != nil { require.ErrorContains(cx.t, err, expectedValue) } cx.t.Log("range request done") diff --git a/tests/e2e/v3_curl_test.go b/tests/e2e/v3_curl_test.go index b994b48973c7..455e9492ac1d 100644 --- a/tests/e2e/v3_curl_test.go +++ b/tests/e2e/v3_curl_test.go @@ -30,6 +30,7 @@ import ( pb "go.etcd.io/etcd/api/v3/etcdserverpb" "go.etcd.io/etcd/api/v3/v3rpc/rpctypes" "go.etcd.io/etcd/client/pkg/v3/testutil" + "go.etcd.io/etcd/pkg/v3/expect" epb "go.etcd.io/etcd/server/v3/etcdserver/api/v3election/v3electionpb" "go.etcd.io/etcd/tests/v3/framework/e2e" @@ -108,14 +109,14 @@ func testV3CurlPutGet(cx ctlCtx) { p := cx.apiPrefix - if err := e2e.CURLPost(cx.epc, e2e.CURLReq{Endpoint: path.Join(p, "/kv/put"), Value: string(putData), Expected: expectPut}); err != nil { + if err := e2e.CURLPost(cx.epc, e2e.CURLReq{Endpoint: path.Join(p, "/kv/put"), Value: string(putData), Expected: expect.ExpectedResponse{Value: expectPut}}); err != nil { cx.t.Fatalf("failed testV3CurlPutGet put with curl using prefix (%s) (%v)", p, err) } - if err := e2e.CURLPost(cx.epc, e2e.CURLReq{Endpoint: path.Join(p, "/kv/range"), Value: string(rangeData), Expected: expectGet}); err != nil { + if err := e2e.CURLPost(cx.epc, e2e.CURLReq{Endpoint: path.Join(p, "/kv/range"), Value: string(rangeData), Expected: expect.ExpectedResponse{Value: expectGet}}); err != nil { cx.t.Fatalf("failed testV3CurlPutGet get with curl using prefix (%s) (%v)", p, err) } if cx.cfg.Client.ConnectionType == e2e.ClientTLSAndNonTLS { - if err := e2e.CURLPost(cx.epc, e2e.CURLReq{Endpoint: path.Join(p, "/kv/range"), Value: string(rangeData), Expected: expectGet, IsTLS: true}); err != nil { + if err := e2e.CURLPost(cx.epc, e2e.CURLReq{Endpoint: path.Join(p, "/kv/range"), Value: string(rangeData), Expected: expect.ExpectedResponse{Value: expectGet}, IsTLS: true}); err != nil { cx.t.Fatalf("failed testV3CurlPutGet get with curl using prefix (%s) (%v)", p, err) } } @@ -139,11 +140,11 @@ func testV3CurlWatch(cx ctlCtx) { wstr := `{"create_request" : ` + string(wreq) + "}" p := cx.apiPrefix - if err = e2e.CURLPost(cx.epc, e2e.CURLReq{Endpoint: path.Join(p, "/kv/put"), Value: string(putreq), Expected: "revision"}); err != nil { + if err = e2e.CURLPost(cx.epc, e2e.CURLReq{Endpoint: path.Join(p, "/kv/put"), Value: string(putreq), Expected: expect.ExpectedResponse{Value: "revision"}}); err != nil { cx.t.Fatalf("failed testV3CurlWatch put with curl using prefix (%s) (%v)", p, err) } // expects "bar", timeout after 2 seconds since stream waits forever - err = e2e.CURLPost(cx.epc, e2e.CURLReq{Endpoint: path.Join(p, "/watch"), Value: wstr, Expected: `"YmFy"`, Timeout: 2}) + err = e2e.CURLPost(cx.epc, e2e.CURLReq{Endpoint: path.Join(p, "/watch"), Value: wstr, Expected: expect.ExpectedResponse{Value: `"YmFy"`}, Timeout: 2}) require.ErrorContains(cx.t, err, "unexpected exit code") } @@ -175,13 +176,13 @@ func testV3CurlTxn(cx ctlCtx) { } expected := `"succeeded":true,"responses":[{"response_put":{"header":{"revision":"2"}}}]` p := cx.apiPrefix - if err := e2e.CURLPost(cx.epc, e2e.CURLReq{Endpoint: path.Join(p, "/kv/txn"), Value: string(jsonDat), Expected: expected}); err != nil { + if err := e2e.CURLPost(cx.epc, e2e.CURLReq{Endpoint: path.Join(p, "/kv/txn"), Value: string(jsonDat), Expected: expect.ExpectedResponse{Value: expected}}); err != nil { cx.t.Fatalf("failed testV3CurlTxn txn with curl using prefix (%s) (%v)", p, err) } // was crashing etcd server malformed := `{"compare":[{"result":0,"target":1,"key":"Zm9v","TargetUnion":null}],"success":[{"Request":{"RequestPut":{"key":"Zm9v","value":"YmFy"}}}]}` - if err := e2e.CURLPost(cx.epc, e2e.CURLReq{Endpoint: path.Join(p, "/kv/txn"), Value: malformed, Expected: "error"}); err != nil { + if err := e2e.CURLPost(cx.epc, e2e.CURLReq{Endpoint: path.Join(p, "/kv/txn"), Value: malformed, Expected: expect.ExpectedResponse{Value: "error"}}); err != nil { cx.t.Fatalf("failed testV3CurlTxn put with curl using prefix (%s) (%v)", p, err) } @@ -198,7 +199,7 @@ func testV3CurlAuth(cx ctlCtx) { user, err := json.Marshal(&pb.AuthUserAddRequest{Name: usernames[i], Password: pwds[i], Options: options[i]}) testutil.AssertNil(cx.t, err) - if err = e2e.CURLPost(cx.epc, e2e.CURLReq{Endpoint: path.Join(p, "/auth/user/add"), Value: string(user), Expected: "revision"}); err != nil { + if err = e2e.CURLPost(cx.epc, e2e.CURLReq{Endpoint: path.Join(p, "/auth/user/add"), Value: string(user), Expected: expect.ExpectedResponse{Value: "revision"}}); err != nil { cx.t.Fatalf("failed testV3CurlAuth add user %v with curl (%v)", usernames[i], err) } } @@ -207,7 +208,7 @@ func testV3CurlAuth(cx ctlCtx) { rolereq, err := json.Marshal(&pb.AuthRoleAddRequest{Name: "root"}) testutil.AssertNil(cx.t, err) - if err = e2e.CURLPost(cx.epc, e2e.CURLReq{Endpoint: path.Join(p, "/auth/role/add"), Value: string(rolereq), Expected: "revision"}); err != nil { + if err = e2e.CURLPost(cx.epc, e2e.CURLReq{Endpoint: path.Join(p, "/auth/role/add"), Value: string(rolereq), Expected: expect.ExpectedResponse{Value: "revision"}}); err != nil { cx.t.Fatalf("failed testV3CurlAuth create role with curl using prefix (%s) (%v)", p, err) } @@ -216,13 +217,13 @@ func testV3CurlAuth(cx ctlCtx) { grantroleroot, err := json.Marshal(&pb.AuthUserGrantRoleRequest{User: usernames[i], Role: "root"}) testutil.AssertNil(cx.t, err) - if err = e2e.CURLPost(cx.epc, e2e.CURLReq{Endpoint: path.Join(p, "/auth/user/grant"), Value: string(grantroleroot), Expected: "revision"}); err != nil { + if err = e2e.CURLPost(cx.epc, e2e.CURLReq{Endpoint: path.Join(p, "/auth/user/grant"), Value: string(grantroleroot), Expected: expect.ExpectedResponse{Value: "revision"}}); err != nil { cx.t.Fatalf("failed testV3CurlAuth grant role with curl using prefix (%s) (%v)", p, err) } } // enable auth - if err = e2e.CURLPost(cx.epc, e2e.CURLReq{Endpoint: path.Join(p, "/auth/enable"), Value: "{}", Expected: "revision"}); err != nil { + if err = e2e.CURLPost(cx.epc, e2e.CURLReq{Endpoint: path.Join(p, "/auth/enable"), Value: "{}", Expected: expect.ExpectedResponse{Value: "revision"}}); err != nil { cx.t.Fatalf("failed testV3CurlAuth enable auth with curl using prefix (%s) (%v)", p, err) } @@ -232,7 +233,7 @@ func testV3CurlAuth(cx ctlCtx) { testutil.AssertNil(cx.t, err) // fail put no auth - if err = e2e.CURLPost(cx.epc, e2e.CURLReq{Endpoint: path.Join(p, "/kv/put"), Value: string(putreq), Expected: "error"}); err != nil { + if err = e2e.CURLPost(cx.epc, e2e.CURLReq{Endpoint: path.Join(p, "/kv/put"), Value: string(putreq), Expected: expect.ExpectedResponse{Value: "error"}}); err != nil { cx.t.Fatalf("failed testV3CurlAuth no auth put with curl using prefix (%s) (%v)", p, err) } @@ -265,7 +266,7 @@ func testV3CurlAuth(cx ctlCtx) { authHeader = "Authorization: " + token // put with auth - if err = e2e.CURLPost(cx.epc, e2e.CURLReq{Endpoint: path.Join(p, "/kv/put"), Value: string(putreq), Header: authHeader, Expected: "revision"}); err != nil { + if err = e2e.CURLPost(cx.epc, e2e.CURLReq{Endpoint: path.Join(p, "/kv/put"), Value: string(putreq), Header: authHeader, Expected: expect.ExpectedResponse{Value: "revision"}}); err != nil { cx.t.Fatalf("failed testV3CurlAuth auth put with curl using prefix (%s) and user (%v) (%v)", p, usernames[i], err) } } @@ -289,7 +290,7 @@ func testV3CurlCampaign(cx ctlCtx) { Endpoint: path.Join(cx.apiPrefix, "/election/campaign"), Value: string(cdata), }) - lines, err := e2e.SpawnWithExpectLines(context.TODO(), cargs, cx.envMap, `"leader":{"name":"`) + lines, err := e2e.SpawnWithExpectLines(context.TODO(), cargs, cx.envMap, expect.ExpectedResponse{Value: `"leader":{"name":"`}) if err != nil { cx.t.Fatalf("failed post campaign request (%s) (%v)", cx.apiPrefix, err) } @@ -327,7 +328,7 @@ func testV3CurlCampaign(cx ctlCtx) { if err = e2e.CURLPost(cx.epc, e2e.CURLReq{ Endpoint: path.Join(cx.apiPrefix, "/election/proclaim"), Value: string(pdata), - Expected: `"revision":`, + Expected: expect.ExpectedResponse{Value: `"revision":`}, }); err != nil { cx.t.Fatalf("failed post proclaim request (%s) (%v)", cx.apiPrefix, err) } @@ -347,7 +348,7 @@ func testV3CurlProclaimMissiongLeaderKey(cx ctlCtx) { if err = e2e.CURLPost(cx.epc, e2e.CURLReq{ Endpoint: path.Join(cx.apiPrefix, "/election/proclaim"), Value: string(pdata), - Expected: `{"error":"\"leader\" field must be provided","code":2,"message":"\"leader\" field must be provided"}`, + Expected: expect.ExpectedResponse{Value: `{"error":"\"leader\" field must be provided","code":2,"message":"\"leader\" field must be provided"}`}, }); err != nil { cx.t.Fatalf("failed post proclaim request (%s) (%v)", cx.apiPrefix, err) } @@ -363,7 +364,7 @@ func testV3CurlResignMissiongLeaderKey(cx ctlCtx) { if err := e2e.CURLPost(cx.epc, e2e.CURLReq{ Endpoint: path.Join(cx.apiPrefix, "/election/resign"), Value: `{}`, - Expected: `{"error":"\"leader\" field must be provided","code":2,"message":"\"leader\" field must be provided"}`, + Expected: expect.ExpectedResponse{Value: `{"error":"\"leader\" field must be provided","code":2,"message":"\"leader\" field must be provided"}`}, }); err != nil { cx.t.Fatalf("failed post resign request (%s) (%v)", cx.apiPrefix, err) } @@ -399,7 +400,7 @@ func CURLWithExpected(cx ctlCtx, tests []v3cURLTest) error { p := cx.apiPrefix for _, t := range tests { value := fmt.Sprintf("%v", t.value) - if err := e2e.CURLPost(cx.epc, e2e.CURLReq{Endpoint: path.Join(p, t.endpoint), Value: value, Expected: t.expected}); err != nil { + if err := e2e.CURLPost(cx.epc, e2e.CURLReq{Endpoint: path.Join(p, t.endpoint), Value: value, Expected: expect.ExpectedResponse{Value: t.expected}}); err != nil { return fmt.Errorf("prefix (%s) endpoint (%s): error (%v), wanted %v", p, t.endpoint, err, t.expected) } } diff --git a/tests/framework/e2e/curl.go b/tests/framework/e2e/curl.go index de2e0e82bfb1..3639bc3a9700 100644 --- a/tests/framework/e2e/curl.go +++ b/tests/framework/e2e/curl.go @@ -20,6 +20,8 @@ import ( "math/rand" "strings" "time" + + "go.etcd.io/etcd/pkg/v3/expect" ) type CURLReq struct { @@ -32,7 +34,7 @@ type CURLReq struct { Endpoint string Value string - Expected string + Expected expect.ExpectedResponse Header string Ciphers string diff --git a/tests/framework/e2e/etcd_process.go b/tests/framework/e2e/etcd_process.go index b832a517cb82..d6c4a3b3f9eb 100644 --- a/tests/framework/e2e/etcd_process.go +++ b/tests/framework/e2e/etcd_process.go @@ -62,7 +62,7 @@ type EtcdProcess interface { } type LogsExpect interface { - ExpectWithContext(context.Context, string) (string, error) + ExpectWithContext(context.Context, expect.ExpectedResponse) (string, error) Lines() []string LineCount() int } @@ -313,7 +313,7 @@ func AssertProcessLogs(t *testing.T, ep EtcdProcess, expectLog string) { var err error ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() - _, err = ep.Logs().ExpectWithContext(ctx, expectLog) + _, err = ep.Logs().ExpectWithContext(ctx, expect.ExpectedResponse{Value: expectLog}) if err != nil { t.Fatal(err) } diff --git a/tests/framework/e2e/etcdctl.go b/tests/framework/e2e/etcdctl.go index 2b1b1690f2bd..62f8a48d9d51 100644 --- a/tests/framework/e2e/etcdctl.go +++ b/tests/framework/e2e/etcdctl.go @@ -28,6 +28,7 @@ import ( "go.etcd.io/etcd/api/v3/authpb" "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/tests/v3/framework/config" ) @@ -80,7 +81,7 @@ func WithEndpoints(endpoints []string) config.ClientOption { } func (ctl *EtcdctlV3) DowngradeEnable(ctx context.Context, version string) error { - _, err := SpawnWithExpectLines(ctx, ctl.cmdArgs("downgrade", "enable", version), nil, "Downgrade enable success") + _, err := SpawnWithExpectLines(ctx, ctl.cmdArgs("downgrade", "enable", version), nil, expect.ExpectedResponse{Value: "Downgrade enable success"}) return err } @@ -144,7 +145,7 @@ func (ctl *EtcdctlV3) Get(ctx context.Context, key string, o config.GetOptions) return nil, err } defer cmd.Close() - _, err = cmd.ExpectWithContext(ctx, "Count") + _, err = cmd.ExpectWithContext(ctx, expect.ExpectedResponse{Value: "Count"}) return &resp, err } err := ctl.spawnJsonCmd(ctx, &resp, args...) @@ -157,7 +158,7 @@ func (ctl *EtcdctlV3) Put(ctx context.Context, key, value string, opts config.Pu if opts.LeaseID != 0 { args = append(args, "--lease", strconv.FormatInt(int64(opts.LeaseID), 16)) } - _, err := SpawnWithExpectLines(ctx, args, nil, "OK") + _, err := SpawnWithExpectLines(ctx, args, nil, expect.ExpectedResponse{Value: "OK"}) return err } @@ -189,7 +190,7 @@ func (ctl *EtcdctlV3) Txn(ctx context.Context, compares, ifSucess, ifFail []stri return nil, err } defer cmd.Close() - _, err = cmd.ExpectWithContext(ctx, "compares:") + _, err = cmd.ExpectWithContext(ctx, expect.ExpectedResponse{Value: "compares:"}) if err != nil { return nil, err } @@ -201,7 +202,7 @@ func (ctl *EtcdctlV3) Txn(ctx context.Context, compares, ifSucess, ifFail []stri if err := cmd.Send("\r"); err != nil { return nil, err } - _, err = cmd.ExpectWithContext(ctx, "success requests (get, put, del):") + _, err = cmd.ExpectWithContext(ctx, expect.ExpectedResponse{Value: "success requests (get, put, del):"}) if err != nil { return nil, err } @@ -214,7 +215,7 @@ func (ctl *EtcdctlV3) Txn(ctx context.Context, compares, ifSucess, ifFail []stri return nil, err } - _, err = cmd.ExpectWithContext(ctx, "failure requests (get, put, del):") + _, err = cmd.ExpectWithContext(ctx, expect.ExpectedResponse{Value: "failure requests (get, put, del):"}) if err != nil { return nil, err } @@ -227,7 +228,7 @@ func (ctl *EtcdctlV3) Txn(ctx context.Context, compares, ifSucess, ifFail []stri return nil, err } var line string - line, err = cmd.ExpectWithContext(ctx, "header") + line, err = cmd.ExpectWithContext(ctx, expect.ExpectedResponse{Value: "header"}) if err != nil { return nil, err } @@ -348,7 +349,7 @@ func (ctl *EtcdctlV3) Compact(ctx context.Context, rev int64, o config.CompactOp args = append(args, "--physical") } - _, err := SpawnWithExpectLines(ctx, args, nil, fmt.Sprintf("compacted revision %v", rev)) + _, err := SpawnWithExpectLines(ctx, args, nil, expect.ExpectedResponse{Value: fmt.Sprintf("compacted revision %v", rev)}) return nil, err } @@ -387,9 +388,9 @@ func (ctl *EtcdctlV3) HashKV(ctx context.Context, rev int64) ([]*clientv3.HashKV func (ctl *EtcdctlV3) Health(ctx context.Context) error { args := ctl.cmdArgs() args = append(args, "endpoint", "health") - lines := make([]string, len(ctl.endpoints)) + lines := make([]expect.ExpectedResponse, len(ctl.endpoints)) for i := range lines { - lines[i] = "is healthy" + lines[i] = expect.ExpectedResponse{Value: "is healthy"} } _, err := SpawnWithExpectLines(ctx, args, nil, lines...) return err @@ -404,7 +405,7 @@ func (ctl *EtcdctlV3) Grant(ctx context.Context, ttl int64) (*clientv3.LeaseGran } defer cmd.Close() var resp clientv3.LeaseGrantResponse - line, err := cmd.ExpectWithContext(ctx, "ID") + line, err := cmd.ExpectWithContext(ctx, expect.ExpectedResponse{Value: "ID"}) if err != nil { return nil, err } @@ -424,7 +425,7 @@ func (ctl *EtcdctlV3) TimeToLive(ctx context.Context, id clientv3.LeaseID, o con } defer cmd.Close() var resp clientv3.LeaseTimeToLiveResponse - line, err := cmd.ExpectWithContext(ctx, "id") + line, err := cmd.ExpectWithContext(ctx, expect.ExpectedResponse{Value: "id"}) if err != nil { return nil, err } @@ -437,9 +438,9 @@ func (ctl *EtcdctlV3) Defragment(ctx context.Context, o config.DefragOption) err if o.Timeout != 0 { args = append(args, fmt.Sprintf("--command-timeout=%s", o.Timeout)) } - lines := make([]string, len(ctl.endpoints)) + lines := make([]expect.ExpectedResponse, len(ctl.endpoints)) for i := range lines { - lines[i] = "Finished defragmenting etcd member" + lines[i] = expect.ExpectedResponse{Value: "Finished defragmenting etcd member"} } _, err := SpawnWithExpectLines(ctx, args, map[string]string{}, lines...) return err @@ -453,7 +454,7 @@ func (ctl *EtcdctlV3) Leases(ctx context.Context) (*clientv3.LeaseLeasesResponse } defer cmd.Close() var resp clientv3.LeaseLeasesResponse - line, err := cmd.ExpectWithContext(ctx, "id") + line, err := cmd.ExpectWithContext(ctx, expect.ExpectedResponse{Value: "id"}) if err != nil { return nil, err } @@ -469,7 +470,7 @@ func (ctl *EtcdctlV3) KeepAliveOnce(ctx context.Context, id clientv3.LeaseID) (* } defer cmd.Close() var resp clientv3.LeaseKeepAliveResponse - line, err := cmd.ExpectWithContext(ctx, "ID") + line, err := cmd.ExpectWithContext(ctx, expect.ExpectedResponse{Value: "ID"}) if err != nil { return nil, err } @@ -498,7 +499,7 @@ func (ctl *EtcdctlV3) AlarmDisarm(ctx context.Context, _ *clientv3.AlarmMember) } defer ep.Close() var resp clientv3.AlarmResponse - line, err := ep.ExpectWithContext(ctx, "alarm") + line, err := ep.ExpectWithContext(ctx, expect.ExpectedResponse{Value: "alarm"}) if err != nil { return nil, err } @@ -514,7 +515,7 @@ func (ctl *EtcdctlV3) AuthEnable(ctx context.Context) error { } defer cmd.Close() - _, err = cmd.ExpectWithContext(ctx, "Authentication Enabled") + _, err = cmd.ExpectWithContext(ctx, expect.ExpectedResponse{Value: "Authentication Enabled"}) return err } @@ -526,7 +527,7 @@ func (ctl *EtcdctlV3) AuthDisable(ctx context.Context) error { } defer cmd.Close() - _, err = cmd.ExpectWithContext(ctx, "Authentication Disabled") + _, err = cmd.ExpectWithContext(ctx, expect.ExpectedResponse{Value: "Authentication Disabled"}) return err } @@ -567,7 +568,7 @@ func (ctl *EtcdctlV3) UserAdd(ctx context.Context, name, password string, opts c } var resp clientv3.AuthUserAddResponse - line, err := cmd.ExpectWithContext(ctx, "header") + line, err := cmd.ExpectWithContext(ctx, expect.ExpectedResponse{Value: "header"}) if err != nil { return nil, err } @@ -606,7 +607,7 @@ func (ctl *EtcdctlV3) UserChangePass(ctx context.Context, user, newPass string) return err } - _, err = cmd.ExpectWithContext(ctx, "Password updated") + _, err = cmd.ExpectWithContext(ctx, expect.ExpectedResponse{Value: "Password updated"}) return err } @@ -666,7 +667,7 @@ func (ctl *EtcdctlV3) spawnJsonCmd(ctx context.Context, output interface{}, args return err } defer cmd.Close() - line, err := cmd.ExpectWithContext(ctx, "header") + line, err := cmd.ExpectWithContext(ctx, expect.ExpectedResponse{Value: "header"}) if err != nil { return err } diff --git a/tests/framework/e2e/lazyfs.go b/tests/framework/e2e/lazyfs.go index 874c82252dc4..fb17b203b341 100644 --- a/tests/framework/e2e/lazyfs.go +++ b/tests/framework/e2e/lazyfs.go @@ -65,7 +65,7 @@ func (fs *LazyFS) Start(ctx context.Context) (err error) { if err != nil { return err } - _, err = fs.ep.ExpectWithContext(ctx, "waiting for fault commands") + _, err = fs.ep.ExpectWithContext(ctx, expect.ExpectedResponse{Value: "waiting for fault commands"}) return err } @@ -109,6 +109,6 @@ func (fs *LazyFS) ClearCache(ctx context.Context) error { } // TODO: Wait for response on socket instead of reading logs to get command completion. // Set `fifo_path_completed` config for LazyFS to create separate socket to write when it has completed command. - _, err = fs.ep.ExpectWithContext(ctx, "cache is cleared") + _, err = fs.ep.ExpectWithContext(ctx, expect.ExpectedResponse{Value: "cache is cleared"}) return err } diff --git a/tests/framework/e2e/util.go b/tests/framework/e2e/util.go index 8e4604ad3ca4..b529e3d0b719 100644 --- a/tests/framework/e2e/util.go +++ b/tests/framework/e2e/util.go @@ -41,24 +41,24 @@ func WaitReadyExpectProc(ctx context.Context, exproc *expect.ExpectProcess, read return err } -func SpawnWithExpect(args []string, expected string) error { - return SpawnWithExpects(args, nil, []string{expected}...) +func SpawnWithExpect(args []string, expected expect.ExpectedResponse) error { + return SpawnWithExpects(args, nil, []expect.ExpectedResponse{expected}...) } -func SpawnWithExpectWithEnv(args []string, envVars map[string]string, expected string) error { - return SpawnWithExpects(args, envVars, []string{expected}...) +func SpawnWithExpectWithEnv(args []string, envVars map[string]string, expected expect.ExpectedResponse) error { + return SpawnWithExpects(args, envVars, []expect.ExpectedResponse{expected}...) } -func SpawnWithExpects(args []string, envVars map[string]string, xs ...string) error { +func SpawnWithExpects(args []string, envVars map[string]string, xs ...expect.ExpectedResponse) error { return SpawnWithExpectsContext(context.TODO(), args, envVars, xs...) } -func SpawnWithExpectsContext(ctx context.Context, args []string, envVars map[string]string, xs ...string) error { +func SpawnWithExpectsContext(ctx context.Context, args []string, envVars map[string]string, xs ...expect.ExpectedResponse) error { _, err := SpawnWithExpectLines(ctx, args, envVars, xs...) return err } -func SpawnWithExpectLines(ctx context.Context, args []string, envVars map[string]string, xs ...string) ([]string, error) { +func SpawnWithExpectLines(ctx context.Context, args []string, envVars map[string]string, xs ...expect.ExpectedResponse) ([]string, error) { proc, err := SpawnCmd(args, envVars) if err != nil { return nil, err