plugins/teststeps/terminalexpect/terminalexpect.go (101 lines of code) (raw):

// Copyright (c) Facebook, Inc. and its affiliates. // // This source code is licensed under the MIT license found in the // LICENSE file in the root directory of this source tree. package terminalexpect import ( "encoding/json" "errors" "fmt" "io" "strings" "time" "github.com/facebookincubator/contest/pkg/event" "github.com/facebookincubator/contest/pkg/event/testevent" "github.com/facebookincubator/contest/pkg/target" "github.com/facebookincubator/contest/pkg/test" "github.com/facebookincubator/contest/pkg/xcontext" "github.com/facebookincubator/contest/plugins/teststeps" "github.com/insomniacslk/termhook" ) // Name is the name used to look this plugin up. var Name = "TerminalExpect" // Events defines the events that a TestStep is allow to emit var Events = []event.Name{} // TerminalExpect reads from a terminal and returns when the given Match string // is found on an output line. type TerminalExpect struct { Port string Speed int Match string Timeout time.Duration } // Name returns the plugin name. func (ts TerminalExpect) Name() string { return Name } // match implements termhook.LineHandler func match(match string, log xcontext.Logger) termhook.LineHandler { return func(w io.Writer, line []byte) (bool, error) { if strings.Contains(string(line), match) { log.Infof("%s: found pattern '%s'", Name, match) return true, nil } return false, nil } } // Run executes the terminal step. func (ts *TerminalExpect) Run(ctx xcontext.Context, ch test.TestStepChannels, params test.TestStepParameters, ev testevent.Emitter, resumeState json.RawMessage) (json.RawMessage, error) { log := ctx.Logger() if err := ts.validateAndPopulate(params); err != nil { return nil, err } hook, err := termhook.NewHook(ts.Port, ts.Speed, false, match(ts.Match, log)) if err != nil { return nil, err } // f implements plugins.PerTargetFunc f := func(ctx xcontext.Context, target *target.Target) error { errCh := make(chan error, 1) go func() { errCh <- hook.Run() if closeErr := hook.Close(); closeErr != nil { log.Errorf("Failed to close hook, err: %v", closeErr) } }() select { case err := <-errCh: return err case <-time.After(ts.Timeout): return fmt.Errorf("timed out after %s", ts.Timeout) case <-ctx.Done(): return nil } } log.Debugf("%s: waiting for string '%s' with timeout %s", Name, ts.Match, ts.Timeout) return teststeps.ForEachTarget(Name, ctx, ch, f) } func (ts *TerminalExpect) validateAndPopulate(params test.TestStepParameters) error { // no expression expansion for these parameters port := params.GetOne("port") if port.IsEmpty() { return errors.New("invalid or missing 'port' parameter, must be exactly one string") } ts.Port = port.String() speed, err := params.GetInt("speed") if err != nil { return fmt.Errorf("invalid or missing 'speed' parameter: %v", err) } ts.Speed = int(speed) match := params.GetOne("match") if match.IsEmpty() { return errors.New("invalid or missing 'match' parameter, must be exactly one string") } ts.Match = match.String() timeoutStr := params.GetOne("timeout") if timeoutStr.IsEmpty() { return errors.New("invalid or missing 'timeout' parameter, must be exactly one string") } timeout, err := time.ParseDuration(timeoutStr.String()) if err != nil { return fmt.Errorf("invalid terminal timeout %s", timeoutStr) } ts.Timeout = timeout return nil } // ValidateParameters validates the parameters associated to the TestStep func (ts *TerminalExpect) ValidateParameters(_ xcontext.Context, params test.TestStepParameters) error { return ts.validateAndPopulate(params) } // New initializes and returns a new TerminalExpect test step. func New() test.TestStep { return &TerminalExpect{} } // Load returns the name, factory and events which are needed to register the step. func Load() (string, test.TestStepFactory, []event.Name) { return Name, New, Events }