Merge pull request #14672 from tjungblu/etcd-14638

Expect exit code enhancement
This commit is contained in:
Marek Siarkowicz 2022-11-14 12:34:43 +01:00 committed by GitHub
commit 4cdcb91fac
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 453 additions and 279 deletions

View File

@ -19,6 +19,7 @@ package expect
import ( import (
"bufio" "bufio"
"context" "context"
"errors"
"fmt" "fmt"
"io" "io"
"os" "os"
@ -33,6 +34,10 @@ import (
const DEBUG_LINES_TAIL = 40 const DEBUG_LINES_TAIL = 40
var (
ErrProcessRunning = fmt.Errorf("process is still running")
)
type ExpectProcess struct { type ExpectProcess struct {
cfg expectConfig cfg expectConfig
@ -40,11 +45,12 @@ type ExpectProcess struct {
fpty *os.File fpty *os.File
wg sync.WaitGroup wg sync.WaitGroup
mu sync.Mutex // protects lines and err mu sync.Mutex // protects lines, count, cur, exitErr and exitCode
lines []string lines []string
count int // increment whenever new line gets added count int // increment whenever new line gets added
cur int // current read position cur int // current read position
err error exitErr error // process exit error
exitCode int
} }
// NewExpect creates a new process for expect testing. // NewExpect creates a new process for expect testing.
@ -69,8 +75,9 @@ func NewExpectWithEnv(name string, args []string, env []string, serverProcessCon
return nil, err return nil, err
} }
ep.wg.Add(1) ep.wg.Add(2)
go ep.read() go ep.read()
go ep.waitSaveExitErr()
return ep, nil return ep, nil
} }
@ -95,11 +102,30 @@ func (ep *ExpectProcess) Pid() int {
func (ep *ExpectProcess) read() { func (ep *ExpectProcess) read() {
defer ep.wg.Done() defer ep.wg.Done()
printDebugLines := os.Getenv("EXPECT_DEBUG") != "" defer func(fpty *os.File) {
err := fpty.Close()
if err != nil {
// we deliberately only log the error here, closing the PTY should mostly be (expected) broken pipes
fmt.Printf("error while closing fpty: %v", err)
}
}(ep.fpty)
r := bufio.NewReader(ep.fpty) r := bufio.NewReader(ep.fpty)
for { for {
err := ep.tryReadNextLine(r)
if err != nil {
break
}
}
}
func (ep *ExpectProcess) tryReadNextLine(r *bufio.Reader) error {
printDebugLines := os.Getenv("EXPECT_DEBUG") != ""
l, err := r.ReadString('\n') l, err := r.ReadString('\n')
ep.mu.Lock() ep.mu.Lock()
defer ep.mu.Unlock()
if l != "" { if l != "" {
if printDebugLines { if printDebugLines {
fmt.Printf("%s (%s) (%d): %s", ep.cmd.Path, ep.cfg.name, ep.cmd.Process.Pid, l) fmt.Printf("%s (%s) (%d): %s", ep.cmd.Path, ep.cfg.name, ep.cmd.Process.Pid, l)
@ -107,34 +133,52 @@ func (ep *ExpectProcess) read() {
ep.lines = append(ep.lines, l) ep.lines = append(ep.lines, l)
ep.count++ ep.count++
} }
// we're checking the error here at the bottom to ensure any leftover reads are still taken into account
return err
}
func (ep *ExpectProcess) waitSaveExitErr() {
defer ep.wg.Done()
err := ep.waitProcess()
ep.mu.Lock()
defer ep.mu.Unlock()
if err != nil { if err != nil {
ep.err = err ep.exitErr = err
ep.mu.Unlock()
break
}
ep.mu.Unlock()
} }
} }
// ExpectFunc returns the first line satisfying the function f. // ExpectFunc returns the first line satisfying the function f.
func (ep *ExpectProcess) ExpectFunc(ctx context.Context, f func(string) bool) (string, error) { func (ep *ExpectProcess) ExpectFunc(ctx context.Context, f func(string) bool) (string, error) {
i := 0 i := 0
for { for {
line, errsFound := func() (string, bool) {
ep.mu.Lock() ep.mu.Lock()
defer ep.mu.Unlock()
// check if this expect has been already closed
if ep.cmd == nil {
return "", true
}
for i < len(ep.lines) { for i < len(ep.lines) {
line := ep.lines[i] line := ep.lines[i]
i++ i++
if f(line) { if f(line) {
ep.mu.Unlock() return line, false
}
}
return "", ep.exitErr != nil
}()
if line != "" {
return line, nil return line, nil
} }
}
if ep.err != nil { if errsFound {
ep.mu.Unlock()
break break
} }
ep.mu.Unlock()
select { select {
case <-ctx.Done(): case <-ctx.Done():
@ -143,16 +187,18 @@ func (ep *ExpectProcess) ExpectFunc(ctx context.Context, f func(string) bool) (s
// continue loop // continue loop
} }
} }
ep.mu.Lock() ep.mu.Lock()
defer ep.mu.Unlock()
lastLinesIndex := len(ep.lines) - DEBUG_LINES_TAIL lastLinesIndex := len(ep.lines) - DEBUG_LINES_TAIL
if lastLinesIndex < 0 { if lastLinesIndex < 0 {
lastLinesIndex = 0 lastLinesIndex = 0
} }
lastLines := strings.Join(ep.lines[lastLinesIndex:], "") lastLines := strings.Join(ep.lines[lastLinesIndex:], "")
ep.mu.Unlock() return "", fmt.Errorf("match not found. "+
return "", fmt.Errorf("match not found."+ " Set EXPECT_DEBUG for more info Errs: [%v], last lines:\n%s",
" Set EXPECT_DEBUG for more info Err: %v, last lines:\n%s", ep.exitErr, lastLines)
ep.err, lastLines)
} }
// ExpectWithContext returns the first line containing the given string. // ExpectWithContext returns the first line containing the given string.
@ -174,47 +220,85 @@ func (ep *ExpectProcess) LineCount() int {
return ep.count return ep.count
} }
// Stop kills the expect process and waits for it to exit. // ExitCode returns the exit code of this process.
func (ep *ExpectProcess) Stop() error { return ep.close(true) } // If the process is still running, it returns exit code 0 and ErrProcessRunning.
func (ep *ExpectProcess) ExitCode() (int, error) {
ep.mu.Lock()
defer ep.mu.Unlock()
if ep.cmd == nil {
return ep.exitCode, nil
}
return 0, ErrProcessRunning
}
// ExitError returns the exit error of this process (if any).
// If the process is still running, it returns ErrProcessRunning instead.
func (ep *ExpectProcess) ExitError() error {
ep.mu.Lock()
defer ep.mu.Unlock()
if ep.cmd == nil {
return ep.exitErr
}
return ErrProcessRunning
}
// Stop signals the process to terminate via SIGTERM
func (ep *ExpectProcess) Stop() error {
err := ep.Signal(syscall.SIGTERM)
if err != nil && strings.Contains(err.Error(), "os: process already finished") {
return nil
}
return err
}
// Signal sends a signal to the expect process // Signal sends a signal to the expect process
func (ep *ExpectProcess) Signal(sig os.Signal) error { func (ep *ExpectProcess) Signal(sig os.Signal) error {
ep.mu.Lock()
defer ep.mu.Unlock()
if ep.cmd == nil {
return errors.New("expect process already closed")
}
return ep.cmd.Process.Signal(sig) return ep.cmd.Process.Signal(sig)
} }
func (ep *ExpectProcess) Wait() error { func (ep *ExpectProcess) waitProcess() error {
_, err := ep.cmd.Process.Wait() state, err := ep.cmd.Process.Wait()
if err != nil {
return err return err
}
ep.mu.Lock()
defer ep.mu.Unlock()
ep.exitCode = state.ExitCode()
if !state.Success() {
return fmt.Errorf("unexpected exit code [%d] after running [%s]", ep.exitCode, ep.cmd.String())
}
return nil
} }
// Close waits for the expect process to exit. // Wait waits for the process to finish.
// Close currently does not return error if process exited with !=0 status. func (ep *ExpectProcess) Wait() {
// TODO: Close should expose underlying process failure by default. ep.wg.Wait()
func (ep *ExpectProcess) Close() error { return ep.close(false) } }
func (ep *ExpectProcess) close(kill bool) error { // Close waits for the expect process to exit and return its error.
if ep.cmd == nil { func (ep *ExpectProcess) Close() error {
return ep.err
}
if kill {
ep.Signal(syscall.SIGTERM)
}
err := ep.cmd.Wait()
ep.fpty.Close()
ep.wg.Wait() ep.wg.Wait()
if err != nil { ep.mu.Lock()
if !kill && strings.Contains(err.Error(), "exit status") { defer ep.mu.Unlock()
// non-zero exit code
err = nil
} else if kill && strings.Contains(err.Error(), "signal:") {
err = nil
}
}
// this signals to other funcs that the process has finished
ep.cmd = nil ep.cmd = nil
return err return ep.exitErr
} }
func (ep *ExpectProcess) Send(command string) error { func (ep *ExpectProcess) Send(command string) error {
@ -222,15 +306,6 @@ func (ep *ExpectProcess) Send(command string) error {
return err return err
} }
func (ep *ExpectProcess) ProcessError() error {
if strings.Contains(ep.err.Error(), "input/output error") {
// TODO: The expect library should not return
// `/dev/ptmx: input/output error` when process just exits.
return nil
}
return ep.err
}
func (ep *ExpectProcess) Lines() []string { func (ep *ExpectProcess) Lines() []string {
ep.mu.Lock() ep.mu.Lock()
defer ep.mu.Unlock() defer ep.mu.Unlock()

View File

@ -19,9 +19,11 @@ package expect
import ( import (
"context" "context"
"os" "os"
"strings"
"testing" "testing"
"time" "time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -65,9 +67,57 @@ func TestExpectFuncTimeout(t *testing.T) {
require.ErrorAs(t, err, &context.DeadlineExceeded) require.ErrorAs(t, err, &context.DeadlineExceeded)
if err = ep.Stop(); err != nil { if err := ep.Stop(); err != nil {
t.Fatal(err) t.Fatal(err)
} }
err = ep.Close()
require.ErrorContains(t, err, "unexpected exit code [-1] after running [/usr/bin/tail -f /dev/null]")
require.Equal(t, -1, ep.exitCode)
}
func TestExpectFuncExitFailure(t *testing.T) {
// tail -x should not exist and return a non-zero exit code
ep, err := NewExpect("tail", "-x")
if err != nil {
t.Fatal(err)
}
ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
defer cancel()
_, err = ep.ExpectFunc(ctx, func(s string) bool {
return strings.Contains(s, "something entirely unexpected")
})
require.ErrorContains(t, err, "unexpected exit code [1] after running [/usr/bin/tail -x]")
require.Equal(t, 1, ep.exitCode)
}
func TestExpectFuncExitFailureStop(t *testing.T) {
// tail -x should not exist and return a non-zero exit code
ep, err := NewExpect("tail", "-x")
if err != nil {
t.Fatal(err)
}
ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
defer cancel()
_, err = ep.ExpectFunc(ctx, func(s string) bool {
return strings.Contains(s, "something entirely unexpected")
})
require.ErrorContains(t, err, "unexpected exit code [1] after running [/usr/bin/tail -x]")
exitCode, err := ep.ExitCode()
require.Equal(t, 0, exitCode)
require.Equal(t, err, ErrProcessRunning)
if err := ep.Stop(); err != nil {
t.Fatal(err)
}
err = ep.Close()
require.ErrorContains(t, err, "unexpected exit code [1] after running [/usr/bin/tail -x]")
exitCode, err = ep.ExitCode()
require.Equal(t, 1, exitCode)
require.NoError(t, err)
} }
func TestEcho(t *testing.T) { func TestEcho(t *testing.T) {
@ -138,10 +188,8 @@ func TestSignal(t *testing.T) {
donec := make(chan struct{}) donec := make(chan struct{})
go func() { go func() {
defer close(donec) defer close(donec)
werr := "signal: interrupt" err = ep.Close()
if cerr := ep.Close(); cerr == nil || cerr.Error() != werr { assert.ErrorContains(t, err, "unexpected exit code [-1] after running [/usr/bin/sleep 100]")
t.Errorf("got error %v, wanted error %s", cerr, werr)
}
}() }()
select { select {
case <-time.After(5 * time.Second): case <-time.After(5 * time.Second):

View File

@ -29,7 +29,7 @@ import (
type txnReq struct { type txnReq struct {
compare []string compare []string
ifSucess []string ifSuccess []string
ifFail []string ifFail []string
results []string results []string
} }
@ -39,18 +39,18 @@ func TestTxnSucc(t *testing.T) {
reqs := []txnReq{ reqs := []txnReq{
{ {
compare: []string{`value("key1") != "value2"`, `value("key2") != "value1"`}, compare: []string{`value("key1") != "value2"`, `value("key2") != "value1"`},
ifSucess: []string{"get key1", "get key2"}, ifSuccess: []string{"get key1", "get key2"},
results: []string{"SUCCESS", "key1", "value1", "key2", "value2"}, results: []string{"SUCCESS", "key1", "value1", "key2", "value2"},
}, },
{ {
compare: []string{`version("key1") = "1"`, `version("key2") = "1"`}, compare: []string{`version("key1") = "1"`, `version("key2") = "1"`},
ifSucess: []string{"get key1", "get key2", `put "key \"with\" space" "value \x23"`}, ifSuccess: []string{"get key1", "get key2", `put "key \"with\" space" "value \x23"`},
ifFail: []string{`put key1 "fail"`, `put key2 "fail"`}, ifFail: []string{`put key1 "fail"`, `put key2 "fail"`},
results: []string{"SUCCESS", "key1", "value1", "key2", "value2", "OK"}, results: []string{"SUCCESS", "key1", "value1", "key2", "value2", "OK"},
}, },
{ {
compare: []string{`version("key \"with\" space") = "1"`}, compare: []string{`version("key \"with\" space") = "1"`},
ifSucess: []string{`get "key \"with\" space"`}, ifSuccess: []string{`get "key \"with\" space"`},
results: []string{"SUCCESS", `key "with" space`, "value \x23"}, results: []string{"SUCCESS", `key "with" space`, "value \x23"},
}, },
} }
@ -69,7 +69,7 @@ func TestTxnSucc(t *testing.T) {
t.Fatalf("could not create key:%s, value:%s", "key2", "value2") t.Fatalf("could not create key:%s, value:%s", "key2", "value2")
} }
for _, req := range reqs { for _, req := range reqs {
resp, err := cc.Txn(ctx, req.compare, req.ifSucess, req.ifFail, config.TxnOptions{ resp, err := cc.Txn(ctx, req.compare, req.ifSuccess, req.ifFail, config.TxnOptions{
Interactive: true, Interactive: true,
}) })
if err != nil { if err != nil {
@ -87,13 +87,13 @@ func TestTxnFail(t *testing.T) {
reqs := []txnReq{ reqs := []txnReq{
{ {
compare: []string{`version("key") < "0"`}, compare: []string{`version("key") < "0"`},
ifSucess: []string{`put key "success"`}, ifSuccess: []string{`put key "success"`},
ifFail: []string{`put key "fail"`}, ifFail: []string{`put key "fail"`},
results: []string{"FAILURE", "OK"}, results: []string{"FAILURE", "OK"},
}, },
{ {
compare: []string{`value("key1") != "value1"`}, compare: []string{`value("key1") != "value1"`},
ifSucess: []string{`put key1 "success"`}, ifSuccess: []string{`put key1 "success"`},
ifFail: []string{`put key1 "fail"`}, ifFail: []string{`put key1 "fail"`},
results: []string{"FAILURE", "OK"}, results: []string{"FAILURE", "OK"},
}, },
@ -110,7 +110,7 @@ func TestTxnFail(t *testing.T) {
t.Fatalf("could not create key:%s, value:%s", "key1", "value1") t.Fatalf("could not create key:%s, value:%s", "key1", "value1")
} }
for _, req := range reqs { for _, req := range reqs {
resp, err := cc.Txn(ctx, req.compare, req.ifSucess, req.ifFail, config.TxnOptions{ resp, err := cc.Txn(ctx, req.compare, req.ifSuccess, req.ifFail, config.TxnOptions{
Interactive: true, Interactive: true,
}) })
if err != nil { if err != nil {

View File

@ -21,6 +21,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/stretchr/testify/require"
clientv3 "go.etcd.io/etcd/client/v3" clientv3 "go.etcd.io/etcd/client/v3"
"go.etcd.io/etcd/tests/v3/framework/e2e" "go.etcd.io/etcd/tests/v3/framework/e2e"
) )
@ -118,9 +119,8 @@ func authDisableTest(cx ctlCtx) {
// test-user doesn't have the permission, it must fail // test-user doesn't have the permission, it must fail
cx.user, cx.pass = "test-user", "pass" cx.user, cx.pass = "test-user", "pass"
if err := ctlV3PutFailPerm(cx, "hoo", "bar"); err != nil { err := ctlV3PutFailPerm(cx, "hoo", "bar")
cx.t.Fatal(err) require.ErrorContains(cx.t, err, "permission denied")
}
cx.user, cx.pass = "root", "root" cx.user, cx.pass = "root", "root"
if err := ctlV3AuthDisable(cx); err != nil { if err := ctlV3AuthDisable(cx); err != nil {
@ -241,9 +241,9 @@ func authCredWriteKeyTest(cx ctlCtx) {
// try invalid user // try invalid user
cx.user, cx.pass = "a", "b" cx.user, cx.pass = "a", "b"
if err := ctlV3PutFailAuth(cx, "foo", "bar"); err != nil { err := ctlV3PutFailAuth(cx, "foo", "bar")
cx.t.Fatal(err) require.ErrorContains(cx.t, err, "authentication failed")
}
// confirm put failed // confirm put failed
cx.user, cx.pass = "test-user", "pass" cx.user, cx.pass = "test-user", "pass"
if err := ctlV3Get(cx, []string{"foo"}, []kv{{"foo", "bar"}}...); err != nil { if err := ctlV3Get(cx, []string{"foo"}, []kv{{"foo", "bar"}}...); err != nil {
@ -262,9 +262,9 @@ func authCredWriteKeyTest(cx ctlCtx) {
// try bad password // try bad password
cx.user, cx.pass = "test-user", "badpass" cx.user, cx.pass = "test-user", "badpass"
if err := ctlV3PutFailAuth(cx, "foo", "baz"); err != nil { err = ctlV3PutFailAuth(cx, "foo", "baz")
cx.t.Fatal(err) require.ErrorContains(cx.t, err, "authentication failed")
}
// confirm put failed // confirm put failed
cx.user, cx.pass = "test-user", "pass" cx.user, cx.pass = "test-user", "pass"
if err := ctlV3Get(cx, []string{"foo"}, []kv{{"foo", "bar2"}}...); err != nil { if err := ctlV3Get(cx, []string{"foo"}, []kv{{"foo", "bar2"}}...); err != nil {
@ -286,9 +286,8 @@ func authRoleUpdateTest(cx ctlCtx) {
// try put to not granted key // try put to not granted key
cx.user, cx.pass = "test-user", "pass" cx.user, cx.pass = "test-user", "pass"
if err := ctlV3PutFailPerm(cx, "hoo", "bar"); err != nil { err := ctlV3PutFailPerm(cx, "hoo", "bar")
cx.t.Fatal(err) require.ErrorContains(cx.t, err, "permission denied")
}
// grant a new key // grant a new key
cx.user, cx.pass = "root", "root" cx.user, cx.pass = "root", "root"
@ -314,9 +313,8 @@ func authRoleUpdateTest(cx ctlCtx) {
// try put to the revoked key // try put to the revoked key
cx.user, cx.pass = "test-user", "pass" cx.user, cx.pass = "test-user", "pass"
if err := ctlV3PutFailPerm(cx, "hoo", "bar"); err != nil { err = ctlV3PutFailPerm(cx, "hoo", "bar")
cx.t.Fatal(err) require.ErrorContains(cx.t, err, "permission denied")
}
// confirm a key still granted can be accessed // confirm a key still granted can be accessed
if err := ctlV3Get(cx, []string{"foo"}, []kv{{"foo", "bar"}}...); err != nil { if err := ctlV3Get(cx, []string{"foo"}, []kv{{"foo", "bar"}}...); err != nil {
@ -355,9 +353,8 @@ func authUserDeleteDuringOpsTest(cx ctlCtx) {
// check the user is deleted // check the user is deleted
cx.user, cx.pass = "test-user", "pass" cx.user, cx.pass = "test-user", "pass"
if err := ctlV3PutFailAuth(cx, "foo", "baz"); err != nil { err = ctlV3PutFailAuth(cx, "foo", "baz")
cx.t.Fatal(err) require.ErrorContains(cx.t, err, "authentication failed")
}
} }
func authRoleRevokeDuringOpsTest(cx ctlCtx) { func authRoleRevokeDuringOpsTest(cx ctlCtx) {
@ -415,9 +412,8 @@ func authRoleRevokeDuringOpsTest(cx ctlCtx) {
// check the role is revoked and permission is lost from the user // check the role is revoked and permission is lost from the user
cx.user, cx.pass = "test-user", "pass" cx.user, cx.pass = "test-user", "pass"
if err := ctlV3PutFailPerm(cx, "foo", "baz"); err != nil { err = ctlV3PutFailPerm(cx, "foo", "baz")
cx.t.Fatal(err) require.ErrorContains(cx.t, err, "permission denied")
}
// try a key that can be accessed from the remaining role // try a key that can be accessed from the remaining role
cx.user, cx.pass = "test-user", "pass" cx.user, cx.pass = "test-user", "pass"
@ -493,44 +489,44 @@ func authTestTxn(cx ctlCtx) {
rqs := txnRequests{ rqs := txnRequests{
compare: []string{`version("c2") = "1"`}, compare: []string{`version("c2") = "1"`},
ifSucess: []string{"get s2"}, ifSuccess: []string{"get s2"},
ifFail: []string{"get f2"}, ifFail: []string{"get f2"},
results: []string{"SUCCESS", "s2", "v"}, results: []string{"SUCCESS", "s2", "v"},
} }
if err := ctlV3Txn(cx, rqs); err != nil { if err := ctlV3Txn(cx, rqs, false); err != nil {
cx.t.Fatal(err) cx.t.Fatal(err)
} }
// a key of compare case isn't granted // a key of compare case isn't granted
rqs = txnRequests{ rqs = txnRequests{
compare: []string{`version("c1") = "1"`}, compare: []string{`version("c1") = "1"`},
ifSucess: []string{"get s2"}, ifSuccess: []string{"get s2"},
ifFail: []string{"get f2"}, ifFail: []string{"get f2"},
results: []string{"Error: etcdserver: permission denied"}, results: []string{"Error: etcdserver: permission denied"},
} }
if err := ctlV3Txn(cx, rqs); err != nil { if err := ctlV3Txn(cx, rqs, true); err != nil {
cx.t.Fatal(err) cx.t.Fatal(err)
} }
// a key of success case isn't granted // a key of success case isn't granted
rqs = txnRequests{ rqs = txnRequests{
compare: []string{`version("c2") = "1"`}, compare: []string{`version("c2") = "1"`},
ifSucess: []string{"get s1"}, ifSuccess: []string{"get s1"},
ifFail: []string{"get f2"}, ifFail: []string{"get f2"},
results: []string{"Error: etcdserver: permission denied"}, results: []string{"Error: etcdserver: permission denied"},
} }
if err := ctlV3Txn(cx, rqs); err != nil { if err := ctlV3Txn(cx, rqs, true); err != nil {
cx.t.Fatal(err) cx.t.Fatal(err)
} }
// a key of failure case isn't granted // a key of failure case isn't granted
rqs = txnRequests{ rqs = txnRequests{
compare: []string{`version("c2") = "1"`}, compare: []string{`version("c2") = "1"`},
ifSucess: []string{"get s2"}, ifSuccess: []string{"get s2"},
ifFail: []string{"get f1"}, ifFail: []string{"get f1"},
results: []string{"Error: etcdserver: permission denied"}, results: []string{"Error: etcdserver: permission denied"},
} }
if err := ctlV3Txn(cx, rqs); err != nil { if err := ctlV3Txn(cx, rqs, true); err != nil {
cx.t.Fatal(err) cx.t.Fatal(err)
} }
} }
@ -559,9 +555,8 @@ func authTestPrefixPerm(cx ctlCtx) {
} }
} }
if err := ctlV3PutFailPerm(cx, clientv3.GetPrefixRangeEnd(prefix), "baz"); err != nil { err := ctlV3PutFailPerm(cx, clientv3.GetPrefixRangeEnd(prefix), "baz")
cx.t.Fatal(err) require.ErrorContains(cx.t, err, "permission denied")
}
// grant the entire keys to test-user // grant the entire keys to test-user
cx.user, cx.pass = "root", "root" cx.user, cx.pass = "root", "root"
@ -679,11 +674,10 @@ func authTestCertCN(cx ctlCtx) {
cx.t.Error(err) cx.t.Error(err)
} }
// try a non granted key // try a non-granted key
cx.user, cx.pass = "", "" cx.user, cx.pass = "", ""
if err := ctlV3PutFailPerm(cx, "baz", "bar"); err != nil { err := ctlV3PutFailPerm(cx, "baz", "bar")
cx.t.Error(err) require.ErrorContains(cx.t, err, "permission denied")
}
} }
func authTestRevokeWithDelete(cx ctlCtx) { func authTestRevokeWithDelete(cx ctlCtx) {
@ -766,9 +760,8 @@ func authTestFromKeyPerm(cx ctlCtx) {
} }
// try a non granted key // try a non granted key
if err := ctlV3PutFailPerm(cx, "x", "baz"); err != nil { err := ctlV3PutFailPerm(cx, "x", "baz")
cx.t.Fatal(err) require.ErrorContains(cx.t, err, "permission denied")
}
// revoke the open ended permission // revoke the open ended permission
cx.user, cx.pass = "root", "root" cx.user, cx.pass = "root", "root"
@ -780,9 +773,8 @@ func authTestFromKeyPerm(cx ctlCtx) {
cx.user, cx.pass = "test-user", "pass" cx.user, cx.pass = "test-user", "pass"
for i := 0; i < 10; i++ { for i := 0; i < 10; i++ {
key := fmt.Sprintf("z%d", i) key := fmt.Sprintf("z%d", i)
if err := ctlV3PutFailPerm(cx, key, "val"); err != nil { err := ctlV3PutFailPerm(cx, key, "val")
cx.t.Fatal(err) require.ErrorContains(cx.t, err, "permission denied")
}
} }
// grant the entire keys // grant the entire keys
@ -810,9 +802,8 @@ func authTestFromKeyPerm(cx ctlCtx) {
cx.user, cx.pass = "test-user", "pass" cx.user, cx.pass = "test-user", "pass"
for i := 0; i < 10; i++ { for i := 0; i < 10; i++ {
key := fmt.Sprintf("z%d", i) key := fmt.Sprintf("z%d", i)
if err := ctlV3PutFailPerm(cx, key, "val"); err != nil { err := ctlV3PutFailPerm(cx, key, "val")
cx.t.Fatal(err) require.ErrorContains(cx.t, err, "permission denied")
}
} }
} }
@ -848,9 +839,8 @@ func authLeaseTestTimeToLiveExpired(cx ctlCtx) {
authSetupTestUser(cx) authSetupTestUser(cx)
ttl := 3 ttl := 3
if err := leaseTestTimeToLiveExpire(cx, ttl); err != nil { err := leaseTestTimeToLiveExpire(cx, ttl)
cx.t.Fatalf("leaseTestTimeToLiveExpire: error (%v)", err) require.NoError(cx.t, err)
}
} }
func leaseTestTimeToLiveExpire(cx ctlCtx, ttl int) error { func leaseTestTimeToLiveExpire(cx ctlCtx, ttl int) error {
@ -984,14 +974,13 @@ func authTestWatch(cx ctlCtx) {
var err error var err error
if tt.want { if tt.want {
err = ctlV3Watch(cx, tt.args, tt.wkv...) err = ctlV3Watch(cx, tt.args, tt.wkv...)
} else { if err != nil && cx.dialTimeout > 0 && !isGRPCTimedout(err) {
err = ctlV3WatchFailPerm(cx, tt.args)
}
if err != nil {
if cx.dialTimeout > 0 && !isGRPCTimedout(err) {
cx.t.Errorf("watchTest #%d: ctlV3Watch error (%v)", i, err) cx.t.Errorf("watchTest #%d: ctlV3Watch error (%v)", i, err)
} }
} else {
err = ctlV3WatchFailPerm(cx, tt.args)
// this will not have any meaningful error output, but the process fails due to the cancellation
require.ErrorContains(cx.t, err, "unexpected exit code")
} }
<-donec <-donec
@ -1025,9 +1014,8 @@ func authTestRoleGet(cx ctlCtx) {
expected = []string{ expected = []string{
"Error: etcdserver: permission denied", "Error: etcdserver: permission denied",
} }
if err := e2e.SpawnWithExpects(append(cx.PrefixArgs(), "role", "get", "root"), cx.envMap, expected...); err != nil { err := e2e.SpawnWithExpects(append(cx.PrefixArgs(), "role", "get", "root"), cx.envMap, expected...)
cx.t.Fatal(err) require.ErrorContains(cx.t, err, "permission denied")
}
} }
func authTestUserGet(cx ctlCtx) { func authTestUserGet(cx ctlCtx) {
@ -1056,9 +1044,8 @@ func authTestUserGet(cx ctlCtx) {
expected = []string{ expected = []string{
"Error: etcdserver: permission denied", "Error: etcdserver: permission denied",
} }
if err := e2e.SpawnWithExpects(append(cx.PrefixArgs(), "user", "get", "root"), cx.envMap, expected...); err != nil { err := e2e.SpawnWithExpects(append(cx.PrefixArgs(), "user", "get", "root"), cx.envMap, expected...)
cx.t.Fatal(err) require.ErrorContains(cx.t, err, "permission denied")
}
} }
func authTestRoleList(cx ctlCtx) { func authTestRoleList(cx ctlCtx) {
@ -1207,16 +1194,14 @@ func certCNAndUsername(cx ctlCtx, noPassword bool) {
cx.t.Error(err) cx.t.Error(err)
} }
// try a non granted key for both of them // try a non-granted key for both of them
cx.user, cx.pass = "", "" cx.user, cx.pass = "", ""
if err := ctlV3PutFailPerm(cx, "baz", "bar"); err != nil { err := ctlV3PutFailPerm(cx, "baz", "bar")
cx.t.Error(err) require.ErrorContains(cx.t, err, "permission denied")
}
cx.user, cx.pass = "test-user", "pass" cx.user, cx.pass = "test-user", "pass"
if err := ctlV3PutFailPerm(cx, "baz", "bar"); err != nil { err = ctlV3PutFailPerm(cx, "baz", "bar")
cx.t.Error(err) require.ErrorContains(cx.t, err, "permission denied")
}
} }
func authTestCertCNAndUsername(cx ctlCtx) { func authTestCertCNAndUsername(cx ctlCtx) {

View File

@ -21,6 +21,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/stretchr/testify/require"
"go.etcd.io/etcd/pkg/v3/expect" "go.etcd.io/etcd/pkg/v3/expect"
"go.etcd.io/etcd/tests/v3/framework/e2e" "go.etcd.io/etcd/tests/v3/framework/e2e"
) )
@ -32,7 +33,7 @@ func TestCtlV3Elect(t *testing.T) {
func testElect(cx ctlCtx) { func testElect(cx ctlCtx) {
name := "a" name := "a"
holder, ch, err := ctlV3Elect(cx, name, "p1") holder, ch, err := ctlV3Elect(cx, name, "p1", false)
if err != nil { if err != nil {
cx.t.Fatal(err) cx.t.Fatal(err)
} }
@ -48,7 +49,7 @@ func testElect(cx ctlCtx) {
} }
// blocked process that won't win the election // blocked process that won't win the election
blocked, ch, err := ctlV3Elect(cx, name, "p2") blocked, ch, err := ctlV3Elect(cx, name, "p2", true)
if err != nil { if err != nil {
cx.t.Fatal(err) cx.t.Fatal(err)
} }
@ -59,7 +60,7 @@ func testElect(cx ctlCtx) {
} }
// overlap with a blocker that will win the election // overlap with a blocker that will win the election
blockAcquire, ch, err := ctlV3Elect(cx, name, "p2") blockAcquire, ch, err := ctlV3Elect(cx, name, "p2", false)
if err != nil { if err != nil {
cx.t.Fatal(err) cx.t.Fatal(err)
} }
@ -74,8 +75,10 @@ func testElect(cx ctlCtx) {
if err = blocked.Signal(os.Interrupt); err != nil { if err = blocked.Signal(os.Interrupt); err != nil {
cx.t.Fatal(err) cx.t.Fatal(err)
} }
if err = e2e.CloseWithTimeout(blocked, time.Second); err != nil { err = e2e.CloseWithTimeout(blocked, time.Second)
cx.t.Fatal(err) if err != nil {
// due to being blocked, this can potentially get killed and thus exit non-zero sometimes
require.ErrorContains(cx.t, err, "unexpected exit code")
} }
// kill the holder with clean shutdown // kill the holder with clean shutdown
@ -98,7 +101,7 @@ func testElect(cx ctlCtx) {
} }
// ctlV3Elect creates a elect process with a channel listening for when it wins the election. // ctlV3Elect creates a elect process with a channel listening for when it wins the election.
func ctlV3Elect(cx ctlCtx, name, proposal string) (*expect.ExpectProcess, <-chan string, error) { func ctlV3Elect(cx ctlCtx, name, proposal string, expectFailure bool) (*expect.ExpectProcess, <-chan string, error) {
cmdArgs := append(cx.PrefixArgs(), "elect", name, proposal) cmdArgs := append(cx.PrefixArgs(), "elect", name, proposal)
proc, err := e2e.SpawnCmd(cmdArgs, cx.envMap) proc, err := e2e.SpawnCmd(cmdArgs, cx.envMap)
outc := make(chan string, 1) outc := make(chan string, 1)
@ -109,8 +112,10 @@ func ctlV3Elect(cx ctlCtx, name, proposal string) (*expect.ExpectProcess, <-chan
go func() { go func() {
s, xerr := proc.ExpectFunc(context.TODO(), func(string) bool { return true }) s, xerr := proc.ExpectFunc(context.TODO(), func(string) bool { return true })
if xerr != nil { if xerr != nil {
if !expectFailure {
cx.t.Errorf("expect failed (%v)", xerr) cx.t.Errorf("expect failed (%v)", xerr)
} }
}
outc <- s outc <- s
}() }()
return proc, outc, err return proc, outc, err

View File

@ -15,10 +15,12 @@
package e2e package e2e
import ( import (
"context"
"fmt" "fmt"
"strings"
"testing" "testing"
"time"
"github.com/stretchr/testify/require"
"go.etcd.io/etcd/tests/v3/framework/e2e" "go.etcd.io/etcd/tests/v3/framework/e2e"
) )
@ -50,9 +52,9 @@ func TestCtlV3GetRevokedCRL(t *testing.T) {
func testGetRevokedCRL(cx ctlCtx) { func testGetRevokedCRL(cx ctlCtx) {
// test reject // test reject
if err := ctlV3Put(cx, "k", "v", ""); err == nil || !strings.Contains(err.Error(), "Error:") { err := ctlV3Put(cx, "k", "v", "")
cx.t.Fatalf("expected reset connection on put, got %v", err) require.ErrorContains(cx.t, err, "context deadline exceeded")
}
// test accept // test accept
cx.epc.Cfg.IsClientCRL = false cx.epc.Cfg.IsClientCRL = false
if err := ctlV3Put(cx, "k", "v", ""); err != nil { if err := ctlV3Put(cx, "k", "v", ""); err != nil {
@ -216,9 +218,13 @@ func getKeysOnlyTest(cx ctlCtx) {
if err := e2e.SpawnWithExpectWithEnv(cmdArgs, cx.envMap, "key"); err != nil { if err := e2e.SpawnWithExpectWithEnv(cmdArgs, cx.envMap, "key"); err != nil {
cx.t.Fatal(err) cx.t.Fatal(err)
} }
if err := e2e.SpawnWithExpects(cmdArgs, cx.envMap, "val"); err == nil {
cx.t.Fatalf("got value but passed --keys-only") ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
} defer cancel()
lines, err := e2e.SpawnWithExpectLines(ctx, cmdArgs, cx.envMap, "key")
require.NoError(cx.t, err)
require.NotContains(cx.t, lines, "val", "got value but passed --keys-only")
} }
func getCountOnlyTest(cx ctlCtx) { func getCountOnlyTest(cx ctlCtx) {
@ -250,13 +256,14 @@ func getCountOnlyTest(cx ctlCtx) {
if err := e2e.SpawnWithExpects(cmdArgs, cx.envMap, "\"Count\" : 3"); err != nil { if err := e2e.SpawnWithExpects(cmdArgs, cx.envMap, "\"Count\" : 3"); err != nil {
cx.t.Fatal(err) cx.t.Fatal(err)
} }
expected := []string{
"\"Count\" : 3", ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
} defer cancel()
cmdArgs = append(cx.PrefixArgs(), []string{"get", "--count-only", "key3", "--prefix", "--write-out=fields"}...) cmdArgs = append(cx.PrefixArgs(), []string{"get", "--count-only", "key3", "--prefix", "--write-out=fields"}...)
if err := e2e.SpawnWithExpects(cmdArgs, cx.envMap, expected...); err == nil { lines, err := e2e.SpawnWithExpectLines(ctx, cmdArgs, cx.envMap, "\"Count\"")
cx.t.Fatal(err) require.NoError(cx.t, err)
} require.NotContains(cx.t, lines, "\"Count\" : 3")
} }
func delTest(cx ctlCtx) { func delTest(cx ctlCtx) {

View File

@ -22,6 +22,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/stretchr/testify/require"
"go.etcd.io/etcd/pkg/v3/expect" "go.etcd.io/etcd/pkg/v3/expect"
"go.etcd.io/etcd/tests/v3/framework/e2e" "go.etcd.io/etcd/tests/v3/framework/e2e"
) )
@ -79,8 +80,10 @@ func testLock(cx ctlCtx) {
if err = blocked.Signal(os.Interrupt); err != nil { if err = blocked.Signal(os.Interrupt); err != nil {
cx.t.Fatal(err) cx.t.Fatal(err)
} }
if err = e2e.CloseWithTimeout(blocked, time.Second); err != nil { err = e2e.CloseWithTimeout(blocked, time.Second)
cx.t.Fatal(err) if err != nil {
// due to being blocked, this can potentially get killed and thus exit non-zero sometimes
require.ErrorContains(cx.t, err, "unexpected exit code")
} }
// kill the holder with clean shutdown // kill the holder with clean shutdown
@ -113,9 +116,8 @@ func testLockWithCmd(cx ctlCtx) {
code := 3 code := 3
awkCmd := []string{"awk", fmt.Sprintf("BEGIN{exit %d}", code)} awkCmd := []string{"awk", fmt.Sprintf("BEGIN{exit %d}", code)}
expect := fmt.Sprintf("Error: exit status %d", code) expect := fmt.Sprintf("Error: exit status %d", code)
if err := ctlV3LockWithCmd(cx, awkCmd, expect); err != nil { err := ctlV3LockWithCmd(cx, awkCmd, expect)
cx.t.Fatal(err) require.ErrorContains(cx.t, err, expect)
}
} }
// ctlV3Lock creates a lock process with a channel listening for when it acquires the lock. // ctlV3Lock creates a lock process with a channel listening for when it acquires the lock.
@ -130,7 +132,7 @@ func ctlV3Lock(cx ctlCtx, name string) (*expect.ExpectProcess, <-chan string, er
go func() { go func() {
s, xerr := proc.ExpectFunc(context.TODO(), func(string) bool { return true }) s, xerr := proc.ExpectFunc(context.TODO(), func(string) bool { return true })
if xerr != nil { if xerr != nil {
cx.t.Errorf("expect failed (%v)", xerr) require.ErrorContains(cx.t, xerr, "Error: context canceled")
} }
outc <- s outc <- s
}() }()
@ -142,5 +144,7 @@ func ctlV3LockWithCmd(cx ctlCtx, execCmd []string, as ...string) error {
// use command as lock name // use command as lock name
cmdArgs := append(cx.PrefixArgs(), "lock", execCmd[0]) cmdArgs := append(cx.PrefixArgs(), "lock", execCmd[0])
cmdArgs = append(cmdArgs, execCmd...) cmdArgs = append(cmdArgs, execCmd...)
return e2e.SpawnWithExpects(cmdArgs, cx.envMap, as...) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
return e2e.SpawnWithExpectsContext(ctx, cmdArgs, cx.envMap, as...)
} }

View File

@ -21,6 +21,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/stretchr/testify/require"
"go.etcd.io/etcd/client/pkg/v3/transport" "go.etcd.io/etcd/client/pkg/v3/transport"
"go.etcd.io/etcd/client/pkg/v3/types" "go.etcd.io/etcd/client/pkg/v3/types"
"go.etcd.io/etcd/client/v3" "go.etcd.io/etcd/client/v3"
@ -112,25 +113,32 @@ func testCtlV3MoveLeader(t *testing.T, cfg e2e.EtcdProcessClusterConfig, envVars
tests := []struct { tests := []struct {
eps []string eps []string
expect string expect string
expectErr bool
}{ }{
{ // request to non-leader { // request to non-leader
[]string{cx.epc.EndpointsV3()[(leadIdx+1)%3]}, []string{cx.epc.EndpointsV3()[(leadIdx+1)%3]},
"no leader endpoint given at ", "no leader endpoint given at ",
true,
}, },
{ // request to leader { // request to leader
[]string{cx.epc.EndpointsV3()[leadIdx]}, []string{cx.epc.EndpointsV3()[leadIdx]},
fmt.Sprintf("Leadership transferred from %s to %s", types.ID(leaderID), types.ID(transferee)), fmt.Sprintf("Leadership transferred from %s to %s", types.ID(leaderID), types.ID(transferee)),
false,
}, },
{ // request to all endpoints { // request to all endpoints
cx.epc.EndpointsV3(), cx.epc.EndpointsV3(),
fmt.Sprintf("Leadership transferred"), fmt.Sprintf("Leadership transferred"),
false,
}, },
} }
for i, tc := range tests { for i, tc := range tests {
prefix := cx.prefixArgs(tc.eps) prefix := cx.prefixArgs(tc.eps)
cmdArgs := append(prefix, "move-leader", types.ID(transferee).String()) cmdArgs := append(prefix, "move-leader", types.ID(transferee).String())
if err := e2e.SpawnWithExpectWithEnv(cmdArgs, cx.envMap, tc.expect); err != nil { err := e2e.SpawnWithExpectWithEnv(cmdArgs, cx.envMap, tc.expect)
t.Fatalf("#%d: %v", i, err) if tc.expectErr {
require.ErrorContains(t, err, tc.expect)
} else {
require.Nilf(t, err, "#%d: %v", i, err)
} }
} }
} }

View File

@ -25,6 +25,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/stretchr/testify/require"
"go.etcd.io/etcd/etcdutl/v3/snapshot" "go.etcd.io/etcd/etcdutl/v3/snapshot"
"go.etcd.io/etcd/pkg/v3/expect" "go.etcd.io/etcd/pkg/v3/expect"
"go.etcd.io/etcd/tests/v3/framework/e2e" "go.etcd.io/etcd/tests/v3/framework/e2e"
@ -90,10 +91,7 @@ func snapshotCorruptTest(cx ctlCtx) {
fpath), fpath),
cx.envMap, cx.envMap,
"expected sha256") "expected sha256")
require.ErrorContains(cx.t, serr, "Error: expected sha256")
if serr != nil {
cx.t.Fatal(serr)
}
} }
// This test ensures that the snapshot status does not modify the snapshot file // This test ensures that the snapshot status does not modify the snapshot file

View File

@ -248,9 +248,8 @@ func runCtlTest(t *testing.T, testFunc func(ctlCtx), testOfflineFunc func(ctlCtx
cx.envMap = make(map[string]string) cx.envMap = make(map[string]string)
} }
if cx.epc != nil { if cx.epc != nil {
if errC := cx.epc.Close(); errC != nil { cx.epc.Stop()
t.Fatalf("error closing etcd processes (%v)", errC) cx.epc.Close()
}
} }
}() }()
@ -270,6 +269,7 @@ func runCtlTest(t *testing.T, testFunc func(ctlCtx), testOfflineFunc func(ctlCtx
} }
t.Log("closing test cluster...") t.Log("closing test cluster...")
assert.NoError(t, cx.epc.Stop())
assert.NoError(t, cx.epc.Close()) assert.NoError(t, cx.epc.Close())
cx.epc = nil cx.epc = nil
t.Log("closed test cluster...") t.Log("closed test cluster...")

View File

@ -20,12 +20,12 @@ import (
type txnRequests struct { type txnRequests struct {
compare []string compare []string
ifSucess []string ifSuccess []string
ifFail []string ifFail []string
results []string results []string
} }
func ctlV3Txn(cx ctlCtx, rqs txnRequests) error { func ctlV3Txn(cx ctlCtx, rqs txnRequests, expectedExitErr bool) error {
// TODO: support non-interactive mode // TODO: support non-interactive mode
cmdArgs := append(cx.PrefixArgs(), "txn") cmdArgs := append(cx.PrefixArgs(), "txn")
if cx.interactive { if cx.interactive {
@ -52,7 +52,7 @@ func ctlV3Txn(cx ctlCtx, rqs txnRequests) error {
if err != nil { if err != nil {
return err return err
} }
for _, req := range rqs.ifSucess { for _, req := range rqs.ifSuccess {
if err = proc.Send(req + "\r"); err != nil { if err = proc.Send(req + "\r"); err != nil {
return err return err
} }
@ -80,5 +80,11 @@ func ctlV3Txn(cx ctlCtx, rqs txnRequests) error {
return err return err
} }
} }
return proc.Close()
err = proc.Close()
if expectedExitErr {
return nil
}
return err
} }

View File

@ -21,6 +21,7 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/stretchr/testify/require"
"go.etcd.io/etcd/pkg/v3/expect" "go.etcd.io/etcd/pkg/v3/expect"
"go.etcd.io/etcd/tests/v3/framework/e2e" "go.etcd.io/etcd/tests/v3/framework/e2e"
) )
@ -57,6 +58,7 @@ func TestEtcdMultiPeer(t *testing.T) {
for i := range procs { for i := range procs {
if procs[i] != nil { if procs[i] != nil {
procs[i].Stop() procs[i].Stop()
procs[i].Close()
} }
} }
}() }()
@ -128,6 +130,7 @@ func TestEtcdPeerCNAuth(t *testing.T) {
for i := range procs { for i := range procs {
if procs[i] != nil { if procs[i] != nil {
procs[i].Stop() procs[i].Stop()
procs[i].Close()
} }
} }
}() }()
@ -206,6 +209,7 @@ func TestEtcdPeerNameAuth(t *testing.T) {
for i := range procs { for i := range procs {
if procs[i] != nil { if procs[i] != nil {
procs[i].Stop() procs[i].Stop()
procs[i].Close()
} }
os.RemoveAll(tmpdirs[i]) os.RemoveAll(tmpdirs[i])
} }
@ -287,9 +291,7 @@ func TestGrpcproxyAndCommonName(t *testing.T) {
} }
err := e2e.SpawnWithExpect(argsWithNonEmptyCN, "cert has non empty Common Name") err := e2e.SpawnWithExpect(argsWithNonEmptyCN, "cert has non empty Common Name")
if err != nil { require.ErrorContains(t, err, "cert has non empty Common Name")
t.Errorf("Unexpected error: %s", err)
}
p, err := e2e.SpawnCmd(argsWithEmptyCN, nil) p, err := e2e.SpawnCmd(argsWithEmptyCN, nil)
defer func() { defer func() {

View File

@ -37,7 +37,10 @@ func TestGateway(t *testing.T) {
eps := strings.Join(ec.EndpointsV3(), ",") eps := strings.Join(ec.EndpointsV3(), ",")
p := startGateway(t, eps) p := startGateway(t, eps)
defer p.Stop() defer func() {
p.Stop()
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"}, "OK\r\n")
if err != nil { if err != nil {

View File

@ -28,6 +28,8 @@ func TestInitDaemonNotifyWithoutQuorum(t *testing.T) {
t.Fatalf("Failed to initilize the etcd cluster: %v", err) t.Fatalf("Failed to initilize the etcd cluster: %v", err)
} }
defer epc.Close()
// Remove two members, so that only one etcd will get started // Remove two members, so that only one etcd will get started
epc.Procs = epc.Procs[:1] epc.Procs = epc.Procs[:1]
@ -40,6 +42,4 @@ func TestInitDaemonNotifyWithoutQuorum(t *testing.T) {
e2e.AssertProcessLogs(t, epc.Procs[0], "startEtcd: timed out waiting for the ready notification") e2e.AssertProcessLogs(t, epc.Procs[0], "startEtcd: timed out waiting for the ready notification")
// Expect log message indicating systemd notify message has been sent // Expect log message indicating systemd notify message has been sent
e2e.AssertProcessLogs(t, epc.Procs[0], "notifying init daemon") e2e.AssertProcessLogs(t, epc.Procs[0], "notifying init daemon")
epc.Close()
} }

View File

@ -24,6 +24,7 @@ import (
"github.com/coreos/go-semver/semver" "github.com/coreos/go-semver/semver"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.etcd.io/etcd/api/v3/version" "go.etcd.io/etcd/api/v3/version"
"go.etcd.io/etcd/client/pkg/v3/fileutil" "go.etcd.io/etcd/client/pkg/v3/fileutil"
"go.etcd.io/etcd/server/v3/storage/backend" "go.etcd.io/etcd/server/v3/storage/backend"
@ -155,8 +156,12 @@ func TestEtctlutlMigrate(t *testing.T) {
} }
err = e2e.SpawnWithExpect(args, tc.expectLogsSubString) err = e2e.SpawnWithExpect(args, tc.expectLogsSubString)
if err != nil { if err != nil {
if tc.expectLogsSubString != "" {
require.ErrorContains(t, err, tc.expectLogsSubString)
} else {
t.Fatal(err) t.Fatal(err)
} }
}
t.Log("etcdutl migrate...") t.Log("etcdutl migrate...")
be := backend.NewDefaultBackend(lg, filepath.Join(memberDataDir, "member/snap/db")) be := backend.NewDefaultBackend(lg, filepath.Join(memberDataDir, "member/snap/db"))

View File

@ -21,6 +21,7 @@ import (
"fmt" "fmt"
"testing" "testing"
"github.com/stretchr/testify/require"
"go.etcd.io/etcd/api/v3/version" "go.etcd.io/etcd/api/v3/version"
"go.etcd.io/etcd/tests/v3/framework/e2e" "go.etcd.io/etcd/tests/v3/framework/e2e"
) )
@ -52,24 +53,16 @@ func cipherSuiteTestValid(cx ctlCtx) {
MetricsURLScheme: cx.cfg.MetricsURLScheme, MetricsURLScheme: cx.cfg.MetricsURLScheme,
Ciphers: "ECDHE-RSA-AES128-GCM-SHA256", // TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 Ciphers: "ECDHE-RSA-AES128-GCM-SHA256", // TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
}); err != nil { }); err != nil {
cx.t.Fatalf("failed get with curl (%v)", err) require.ErrorContains(cx.t, err, fmt.Sprintf(`etcd_server_version{server_version="%s"} 1`, version.Version))
} }
} }
func cipherSuiteTestMismatch(cx ctlCtx) { func cipherSuiteTestMismatch(cx ctlCtx) {
var err error err := e2e.CURLGet(cx.epc, e2e.CURLReq{
for _, exp := range []string{"alert handshake failure", "failed setting cipher list"} {
err = e2e.CURLGet(cx.epc, e2e.CURLReq{
Endpoint: "/metrics", Endpoint: "/metrics",
Expected: exp, Expected: "failed setting cipher list",
MetricsURLScheme: cx.cfg.MetricsURLScheme, MetricsURLScheme: cx.cfg.MetricsURLScheme,
Ciphers: "ECDHE-RSA-DES-CBC3-SHA", // TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA Ciphers: "ECDHE-RSA-DES-CBC3-SHA", // TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA
}) })
if err == nil { require.ErrorContains(cx.t, err, "curl: (59) failed setting cipher list")
break
}
}
if err != nil {
cx.t.Fatalf("failed get with curl (%v)", err)
}
} }

View File

@ -25,6 +25,7 @@ import (
"time" "time"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
pb "go.etcd.io/etcd/api/v3/etcdserverpb" pb "go.etcd.io/etcd/api/v3/etcdserverpb"
"go.etcd.io/etcd/client/pkg/v3/testutil" "go.etcd.io/etcd/client/pkg/v3/testutil"
"go.etcd.io/etcd/tests/v3/framework/e2e" "go.etcd.io/etcd/tests/v3/framework/e2e"
@ -212,7 +213,7 @@ func submitRangeAfterConcurrentWatch(cx ctlCtx, expectedValue string) {
cx.t.Log("Submitting range request...") 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: expectedValue, Timeout: 5}); err != nil {
cx.t.Fatalf("testV3CurlMaxStream get failed, error: %v", err) require.ErrorContains(cx.t, err, expectedValue)
} }
cx.t.Log("range request done") cx.t.Log("range request done")
} }

View File

@ -24,6 +24,7 @@ import (
"strconv" "strconv"
"testing" "testing"
"github.com/stretchr/testify/require"
"go.etcd.io/etcd/api/v3/authpb" "go.etcd.io/etcd/api/v3/authpb"
pb "go.etcd.io/etcd/api/v3/etcdserverpb" pb "go.etcd.io/etcd/api/v3/etcdserverpb"
"go.etcd.io/etcd/api/v3/v3rpc/rpctypes" "go.etcd.io/etcd/api/v3/v3rpc/rpctypes"
@ -141,9 +142,8 @@ func testV3CurlWatch(cx ctlCtx) {
cx.t.Fatalf("failed testV3CurlWatch put with curl using prefix (%s) (%v)", p, err) cx.t.Fatalf("failed testV3CurlWatch put with curl using prefix (%s) (%v)", p, err)
} }
// expects "bar", timeout after 2 seconds since stream waits forever // expects "bar", timeout after 2 seconds since stream waits forever
if err = e2e.CURLPost(cx.epc, e2e.CURLReq{Endpoint: path.Join(p, "/watch"), Value: wstr, Expected: `"YmFy"`, Timeout: 2}); err != nil { err = e2e.CURLPost(cx.epc, e2e.CURLReq{Endpoint: path.Join(p, "/watch"), Value: wstr, Expected: `"YmFy"`, Timeout: 2})
cx.t.Fatalf("failed testV3CurlWatch watch with curl using prefix (%s) (%v)", p, err) require.ErrorContains(cx.t, err, "unexpected exit code")
}
} }
func testV3CurlTxn(cx ctlCtx) { func testV3CurlTxn(cx ctlCtx) {

View File

@ -16,6 +16,7 @@ package e2e
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"net/url" "net/url"
"path" "path"
@ -29,7 +30,6 @@ import (
"go.uber.org/zap/zaptest" "go.uber.org/zap/zaptest"
"go.etcd.io/etcd/api/v3/etcdserverpb" "go.etcd.io/etcd/api/v3/etcdserverpb"
"go.etcd.io/etcd/api/v3/v3rpc/rpctypes"
"go.etcd.io/etcd/server/v3/etcdserver" "go.etcd.io/etcd/server/v3/etcdserver"
"go.etcd.io/etcd/tests/v3/framework/config" "go.etcd.io/etcd/tests/v3/framework/config"
) )
@ -664,18 +664,23 @@ func (epc *EtcdProcessCluster) CloseProc(ctx context.Context, finder func(EtcdPr
return fmt.Errorf("failed to find member ID: %w", err) return fmt.Errorf("failed to find member ID: %w", err)
} }
memberRemoved := false
for i := 0; i < 10; i++ { for i := 0; i < 10; i++ {
_, err = memberCtl.MemberRemove(ctx, memberID) _, err := memberCtl.MemberRemove(ctx, memberID)
if err != nil && strings.Contains(err.Error(), rpctypes.ErrGRPCUnhealthy.Error()) { if err != nil && strings.Contains(err.Error(), "member not found") {
time.Sleep(500 * time.Millisecond) memberRemoved = true
continue
}
break break
} }
if err != nil {
return fmt.Errorf("failed to remove member: %w", err) time.Sleep(500 * time.Millisecond)
} }
if !memberRemoved {
return errors.New("failed to remove member after 10 tries")
}
epc.lg.Info("successfully removed member", zap.String("acurl", proc.Config().Acurl))
// Then stop process // Then stop process
return proc.Close() return proc.Close()
} }

View File

@ -15,9 +15,11 @@
package e2e package e2e
import ( import (
"context"
"fmt" "fmt"
"math/rand" "math/rand"
"strings" "strings"
"time"
) )
type CURLReq struct { type CURLReq struct {
@ -38,6 +40,15 @@ type CURLReq struct {
Ciphers string Ciphers string
} }
func (r CURLReq) timeoutDuration() time.Duration {
if r.Timeout != 0 {
return time.Duration(r.Timeout) * time.Second
}
// assume a sane default to finish a curl request
return 5 * time.Second
}
// CURLPrefixArgs builds the beginning of a curl command for a given key // CURLPrefixArgs builds the beginning of a curl command for a given key
// addressed to a random URL in the given cluster. // addressed to a random URL in the given cluster.
func CURLPrefixArgs(cfg *EtcdProcessClusterConfig, member EtcdProcess, method string, req CURLReq) []string { func CURLPrefixArgs(cfg *EtcdProcessClusterConfig, member EtcdProcess, method string, req CURLReq) []string {
@ -94,13 +105,20 @@ func CURLPrefixArgs(cfg *EtcdProcessClusterConfig, member EtcdProcess, method st
} }
func CURLPost(clus *EtcdProcessCluster, req CURLReq) error { func CURLPost(clus *EtcdProcessCluster, req CURLReq) error {
return SpawnWithExpect(CURLPrefixArgs(clus.Cfg, clus.Procs[rand.Intn(clus.Cfg.ClusterSize)], "POST", req), req.Expected) ctx, cancel := context.WithTimeout(context.Background(), req.timeoutDuration())
defer cancel()
return SpawnWithExpectsContext(ctx, CURLPrefixArgs(clus.Cfg, clus.Procs[rand.Intn(clus.Cfg.ClusterSize)], "POST", req), nil, req.Expected)
} }
func CURLPut(clus *EtcdProcessCluster, req CURLReq) error { func CURLPut(clus *EtcdProcessCluster, req CURLReq) error {
return SpawnWithExpect(CURLPrefixArgs(clus.Cfg, clus.Procs[rand.Intn(clus.Cfg.ClusterSize)], "PUT", req), req.Expected) ctx, cancel := context.WithTimeout(context.Background(), req.timeoutDuration())
defer cancel()
return SpawnWithExpectsContext(ctx, CURLPrefixArgs(clus.Cfg, clus.Procs[rand.Intn(clus.Cfg.ClusterSize)], "PUT", req), nil, req.Expected)
} }
func CURLGet(clus *EtcdProcessCluster, req CURLReq) error { func CURLGet(clus *EtcdProcessCluster, req CURLReq) error {
return SpawnWithExpect(CURLPrefixArgs(clus.Cfg, clus.Procs[rand.Intn(clus.Cfg.ClusterSize)], "GET", req), req.Expected) ctx, cancel := context.WithTimeout(context.Background(), req.timeoutDuration())
defer cancel()
return SpawnWithExpectsContext(ctx, CURLPrefixArgs(clus.Cfg, clus.Procs[rand.Intn(clus.Cfg.ClusterSize)], "GET", req), nil, req.Expected)
} }

View File

@ -19,6 +19,7 @@ import (
"fmt" "fmt"
"net/url" "net/url"
"os" "os"
"strings"
"syscall" "syscall"
"testing" "testing"
"time" "time"
@ -134,11 +135,18 @@ func (ep *EtcdServerProcess) Stop() (err error) {
if ep == nil || ep.proc == nil { if ep == nil || ep.proc == nil {
return nil return nil
} }
err = ep.proc.Stop() defer func() {
ep.proc = nil ep.proc = nil
}()
err = ep.proc.Stop()
if err != nil { if err != nil {
return err return err
} }
err = ep.proc.Close()
if err != nil && !strings.Contains(err.Error(), "unexpected exit code") {
return err
}
<-ep.donec <-ep.donec
ep.donec = make(chan struct{}) ep.donec = make(chan struct{})
if ep.cfg.Purl.Scheme == "unix" || ep.cfg.Purl.Scheme == "unixs" { if ep.cfg.Purl.Scheme == "unix" || ep.cfg.Purl.Scheme == "unixs" {
@ -183,11 +191,7 @@ func (ep *EtcdServerProcess) Kill() error {
} }
func (ep *EtcdServerProcess) Wait() error { func (ep *EtcdServerProcess) Wait() error {
err := ep.proc.Wait() ep.proc.Wait()
if err != nil {
ep.cfg.lg.Error("failed to wait for server exit", zap.String("name", ep.cfg.Name))
return err
}
ep.cfg.lg.Info("server exited", zap.String("name", ep.cfg.Name)) ep.cfg.lg.Info("server exited", zap.String("name", ep.cfg.Name))
ep.proc = nil ep.proc = nil
return nil return nil

View File

@ -50,7 +50,11 @@ func SpawnWithExpectWithEnv(args []string, envVars map[string]string, expected s
} }
func SpawnWithExpects(args []string, envVars map[string]string, xs ...string) error { func SpawnWithExpects(args []string, envVars map[string]string, xs ...string) error {
_, err := SpawnWithExpectLines(context.TODO(), args, envVars, xs...) return SpawnWithExpectsContext(context.TODO(), args, envVars, xs...)
}
func SpawnWithExpectsContext(ctx context.Context, args []string, envVars map[string]string, xs ...string) error {
_, err := SpawnWithExpectLines(ctx, args, envVars, xs...)
return err return err
} }
@ -74,26 +78,29 @@ func SpawnWithExpectLines(ctx context.Context, args []string, envVars map[string
lines = append(lines, l) lines = append(lines, l)
} }
perr := proc.Close() perr := proc.Close()
if perr != nil {
return lines, fmt.Errorf("err: %w, with output lines %v", perr, proc.Lines())
}
l := proc.LineCount() l := proc.LineCount()
if len(xs) == 0 && l != noOutputLineCount { // expect no output if len(xs) == 0 && l != noOutputLineCount { // expect no output
return nil, fmt.Errorf("unexpected output from %v (got lines %q, line count %d) %v. Try EXPECT_DEBUG=TRUE", args, lines, l, l != noOutputLineCount) return nil, fmt.Errorf("unexpected output from %v (got lines %q, line count %d) %v. Try EXPECT_DEBUG=TRUE", args, lines, l, l != noOutputLineCount)
} }
return lines, perr return lines, nil
} }
func RunUtilCompletion(args []string, envVars map[string]string) ([]string, error) { func RunUtilCompletion(args []string, envVars map[string]string) ([]string, error) {
proc, err := SpawnCmd(args, envVars) proc, err := SpawnCmd(args, envVars)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to spawn command: %w", err) return nil, fmt.Errorf("failed to spawn command %v with error: %w", args, err)
} }
defer proc.Stop()
perr := proc.Wait() proc.Wait()
// make sure that all the outputs are received err = proc.Close()
proc.Close() if err != nil {
if perr != nil { return nil, fmt.Errorf("failed to close command %v with error: %w", args, err)
return nil, fmt.Errorf("unexpected error from command %v: %w", args, perr)
} }
return proc.Lines(), nil return proc.Lines(), nil
} }

View File

@ -69,7 +69,7 @@ func (f killFailpoint) Trigger(ctx context.Context, clus *e2e.EtcdProcessCluster
return err return err
} }
err = member.Wait() err = member.Wait()
if err != nil { if err != nil && !strings.Contains(err.Error(), "unexpected exit code") {
return err return err
} }
err = member.Start(ctx) err = member.Start(ctx)
@ -103,7 +103,7 @@ func (f goFailpoint) Trigger(ctx context.Context, clus *e2e.EtcdProcessCluster)
} }
} }
err = member.Wait() err = member.Wait()
if err != nil { if err != nil && !strings.Contains(err.Error(), "unexpected exit code") {
return err return err
} }
err = member.Start(ctx) err = member.Start(ctx)