mirror of
https://github.com/etcd-io/etcd.git
synced 2024-09-27 06:25:44 +00:00

ExpectProcess's Stop method uses 'strings.Contains' to check the returned err, however, this can be avoided. os.ErrProcessDone's error message is the same as the hardcoded string. So I think this explicit error is what this method wants to compare. Signed-off-by: Jes Cok <xigua67damn@gmail.com>
373 lines
8.5 KiB
Go
373 lines
8.5 KiB
Go
// Copyright 2016 The etcd Authors
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
// Package expect implements a small expect-style interface
|
|
package expect
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"strings"
|
|
"sync"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/creack/pty"
|
|
)
|
|
|
|
const DEBUG_LINES_TAIL = 40
|
|
|
|
var (
|
|
ErrProcessRunning = fmt.Errorf("process is still running")
|
|
)
|
|
|
|
type ExpectProcess struct {
|
|
cfg expectConfig
|
|
|
|
cmd *exec.Cmd
|
|
fpty *os.File
|
|
wg sync.WaitGroup
|
|
|
|
readCloseCh chan struct{} // close it if async read goroutine exits
|
|
|
|
mu sync.Mutex // protects lines, count, cur, exitErr and exitCode
|
|
lines []string
|
|
count int // increment whenever new line gets added
|
|
cur int // current read position
|
|
exitErr error // process exit error
|
|
exitCode int
|
|
}
|
|
|
|
// NewExpect creates a new process for expect testing.
|
|
func NewExpect(name string, arg ...string) (ep *ExpectProcess, err error) {
|
|
// if env[] is nil, use current system env and the default command as name
|
|
return NewExpectWithEnv(name, arg, nil, name)
|
|
}
|
|
|
|
// NewExpectWithEnv creates a new process with user defined env variables for expect testing.
|
|
func NewExpectWithEnv(name string, args []string, env []string, serverProcessConfigName string) (ep *ExpectProcess, err error) {
|
|
ep = &ExpectProcess{
|
|
cfg: expectConfig{
|
|
name: serverProcessConfigName,
|
|
cmd: name,
|
|
args: args,
|
|
env: env,
|
|
},
|
|
readCloseCh: make(chan struct{}),
|
|
}
|
|
ep.cmd = commandFromConfig(ep.cfg)
|
|
|
|
if ep.fpty, err = pty.Start(ep.cmd); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
ep.wg.Add(2)
|
|
go ep.read()
|
|
go ep.waitSaveExitErr()
|
|
return ep, nil
|
|
}
|
|
|
|
type expectConfig struct {
|
|
name string
|
|
cmd string
|
|
args []string
|
|
env []string
|
|
}
|
|
|
|
func commandFromConfig(config expectConfig) *exec.Cmd {
|
|
cmd := exec.Command(config.cmd, config.args...)
|
|
cmd.Env = config.env
|
|
cmd.Stderr = cmd.Stdout
|
|
cmd.Stdin = nil
|
|
return cmd
|
|
}
|
|
|
|
func (ep *ExpectProcess) Pid() int {
|
|
return ep.cmd.Process.Pid
|
|
}
|
|
|
|
func (ep *ExpectProcess) read() {
|
|
defer func() {
|
|
ep.wg.Done()
|
|
close(ep.readCloseCh)
|
|
}()
|
|
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)
|
|
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')
|
|
|
|
ep.mu.Lock()
|
|
defer ep.mu.Unlock()
|
|
|
|
if l != "" {
|
|
if printDebugLines {
|
|
fmt.Printf("%s (%s) (%d): %s", ep.cmd.Path, ep.cfg.name, ep.cmd.Process.Pid, l)
|
|
}
|
|
ep.lines = append(ep.lines, l)
|
|
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 {
|
|
ep.exitErr = err
|
|
}
|
|
}
|
|
|
|
// ExpectFunc returns the first line satisfying the function f.
|
|
func (ep *ExpectProcess) ExpectFunc(ctx context.Context, f func(string) bool) (string, error) {
|
|
i := 0
|
|
for {
|
|
line, errsFound := func() (string, bool) {
|
|
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) {
|
|
line := ep.lines[i]
|
|
i++
|
|
if f(line) {
|
|
return line, false
|
|
}
|
|
}
|
|
return "", ep.exitErr != nil
|
|
}()
|
|
|
|
if line != "" {
|
|
return line, nil
|
|
}
|
|
|
|
if errsFound {
|
|
break
|
|
}
|
|
|
|
select {
|
|
case <-ctx.Done():
|
|
return "", fmt.Errorf("failed to find match string: %w", ctx.Err())
|
|
case <-time.After(time.Millisecond * 10):
|
|
// continue loop
|
|
}
|
|
}
|
|
|
|
select {
|
|
// NOTE: we wait readCloseCh for ep.read() to complete draining the log before acquring the lock.
|
|
case <-ep.readCloseCh:
|
|
case <-ctx.Done():
|
|
return "", fmt.Errorf("failed to find match string: %w", ctx.Err())
|
|
}
|
|
|
|
ep.mu.Lock()
|
|
defer ep.mu.Unlock()
|
|
|
|
// retry it since we get all the log data
|
|
for i < len(ep.lines) {
|
|
line := ep.lines[i]
|
|
i++
|
|
if f(line) {
|
|
return line, nil
|
|
}
|
|
}
|
|
|
|
lastLinesIndex := len(ep.lines) - DEBUG_LINES_TAIL
|
|
if lastLinesIndex < 0 {
|
|
lastLinesIndex = 0
|
|
}
|
|
lastLines := strings.Join(ep.lines[lastLinesIndex:], "")
|
|
return "", fmt.Errorf("match not found. "+
|
|
" Set EXPECT_DEBUG for more info Errs: [%v], last lines:\n%s",
|
|
ep.exitErr, lastLines)
|
|
}
|
|
|
|
// 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) })
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
// LineCount returns the number of recorded lines since
|
|
// the beginning of the process.
|
|
func (ep *ExpectProcess) LineCount() int {
|
|
ep.mu.Lock()
|
|
defer ep.mu.Unlock()
|
|
return ep.count
|
|
}
|
|
|
|
// ExitCode returns the exit code of this process.
|
|
// 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
|
|
}
|
|
|
|
if ep.exitErr != nil {
|
|
// If the child process panics or is killed, for instance, the
|
|
// goFailpoint triggers the exit event, the ep.cmd isn't nil and
|
|
// the exitCode will describe the case.
|
|
if ep.exitCode != 0 {
|
|
return ep.exitCode, nil
|
|
}
|
|
|
|
// If the wait4(2) in waitProcess returns error, the child
|
|
// process might be reaped if the process handles the SIGCHILD
|
|
// in other goroutine. It's unlikely in this repo. But we
|
|
// should return the error for log even if the child process
|
|
// is still running.
|
|
return 0, ep.exitErr
|
|
}
|
|
|
|
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 && errors.Is(err, os.ErrProcessDone) {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
|
|
// Signal sends a signal to the expect process
|
|
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)
|
|
}
|
|
|
|
func (ep *ExpectProcess) waitProcess() error {
|
|
state, err := ep.cmd.Process.Wait()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ep.mu.Lock()
|
|
defer ep.mu.Unlock()
|
|
ep.exitCode = exitCode(state)
|
|
|
|
if !state.Success() {
|
|
return fmt.Errorf("unexpected exit code [%d] after running [%s]", ep.exitCode, ep.cmd.String())
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// exitCode returns correct exit code for a process based on signaled or exited.
|
|
func exitCode(state *os.ProcessState) int {
|
|
status := state.Sys().(syscall.WaitStatus)
|
|
|
|
if status.Signaled() {
|
|
return 128 + int(status.Signal())
|
|
}
|
|
return status.ExitStatus()
|
|
}
|
|
|
|
// Wait waits for the process to finish.
|
|
func (ep *ExpectProcess) Wait() {
|
|
ep.wg.Wait()
|
|
}
|
|
|
|
// Close waits for the expect process to exit and return its error.
|
|
func (ep *ExpectProcess) Close() error {
|
|
ep.wg.Wait()
|
|
|
|
ep.mu.Lock()
|
|
defer ep.mu.Unlock()
|
|
|
|
// this signals to other funcs that the process has finished
|
|
ep.cmd = nil
|
|
return ep.exitErr
|
|
}
|
|
|
|
func (ep *ExpectProcess) Send(command string) error {
|
|
_, err := io.WriteString(ep.fpty, command)
|
|
return err
|
|
}
|
|
|
|
func (ep *ExpectProcess) Lines() []string {
|
|
ep.mu.Lock()
|
|
defer ep.mu.Unlock()
|
|
return ep.lines
|
|
}
|
|
|
|
// ReadLine returns line by line.
|
|
func (ep *ExpectProcess) ReadLine() string {
|
|
ep.mu.Lock()
|
|
defer ep.mu.Unlock()
|
|
if ep.count > ep.cur {
|
|
line := ep.lines[ep.cur]
|
|
ep.cur++
|
|
return line
|
|
}
|
|
return ""
|
|
}
|