Skip to content

Commit

Permalink
Add localsetup test step
Browse files Browse the repository at this point in the history
- this is a gather type plugin, holds onto all targets and runs the
  given command once; returns a framework error if the command fails
- add to contest generator and server

Signed-off-by: mimir-d <[email protected]>
  • Loading branch information
mimir-d committed Jul 28, 2022
1 parent 240ce54 commit f4acb36
Show file tree
Hide file tree
Showing 3 changed files with 270 additions and 0 deletions.
1 change: 1 addition & 0 deletions cmds/contest-generator/core-plugins.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ teststeps:
- path: github.com/linuxboot/contest/plugins/teststeps/exec
- path: github.com/linuxboot/contest/plugins/teststeps/echo
- path: github.com/linuxboot/contest/plugins/teststeps/randecho
- path: github.com/linuxboot/contest/plugins/teststeps/localsetup

reporters:
- path: github.com/linuxboot/contest/plugins/reporters/targetsuccess
Expand Down
2 changes: 2 additions & 0 deletions cmds/contest/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import (
cpucmd "github.com/linuxboot/contest/plugins/teststeps/cpucmd"
echo "github.com/linuxboot/contest/plugins/teststeps/echo"
exec "github.com/linuxboot/contest/plugins/teststeps/exec"
localsetup "github.com/linuxboot/contest/plugins/teststeps/localsetup"
randecho "github.com/linuxboot/contest/plugins/teststeps/randecho"
sleep "github.com/linuxboot/contest/plugins/teststeps/sleep"
sshcmd "github.com/linuxboot/contest/plugins/teststeps/sshcmd"
Expand All @@ -51,6 +52,7 @@ func getPluginConfig() *server.PluginConfig {
pc.TestStepLoaders = append(pc.TestStepLoaders, cpucmd.Load)
pc.TestStepLoaders = append(pc.TestStepLoaders, echo.Load)
pc.TestStepLoaders = append(pc.TestStepLoaders, exec.Load)
pc.TestStepLoaders = append(pc.TestStepLoaders, localsetup.Load)
pc.TestStepLoaders = append(pc.TestStepLoaders, randecho.Load)
pc.TestStepLoaders = append(pc.TestStepLoaders, sleep.Load)
pc.TestStepLoaders = append(pc.TestStepLoaders, sshcmd.Load)
Expand Down
267 changes: 267 additions & 0 deletions plugins/teststeps/localsetup/localsetup.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,267 @@
// 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 localsetup

import (
"bytes"
"encoding/json"
"errors"
"fmt"
"os/exec"
"path/filepath"
"syscall"

"github.com/linuxboot/contest/pkg/event"
"github.com/linuxboot/contest/pkg/event/testevent"
"github.com/linuxboot/contest/pkg/target"
"github.com/linuxboot/contest/pkg/test"
"github.com/linuxboot/contest/pkg/xcontext"
)

// Name is the name used to look this plugin up.
var Name = "LocalSetup"

// event names for this plugin
const (
EventSetupStart = event.Name("LocalSetupStart")
EventSetupDone = event.Name("LocalSetupDone")
)

// Events defines the events that a TestStep is allow to emit
var Events = []event.Name{
EventSetupStart,
EventSetupDone,
}

// eventSetupStartPayload is the payload for EventSetupStart
type eventSetupStartPayload struct {
Path string
Args []string
}

// eventSetupDonePayload is the payload for EventSetupDone
type eventSetupDonePayload struct {
ExitCode int
Err string
}

// LocalSetup is used to run arbitrary commands as test steps.
type LocalSetup struct {
// binary to execute; will consult $PATH
binary string

// arguments to pass to the binary on execution
args []string
}

// Name returns the plugin name.
func (ts LocalSetup) Name() string {
return Name
}

func emitEvent(
ctx xcontext.Context,
emitter testevent.Emitter,
target *target.Target,
name event.Name,
payload interface{},
) error {
jsonstr, err := json.Marshal(payload)
if err != nil {
return fmt.Errorf("cannot encode payload for event '%s': %v", name, err)
}

jsonPayload := json.RawMessage(jsonstr)
data := testevent.Data{
EventName: name,
Target: target,
Payload: &jsonPayload,
}
if err := emitter.Emit(ctx, data); err != nil {
return fmt.Errorf("cannot emit event EventCmdStart: %v", err)
}

return nil
}

func truncate(in string, maxsize uint) string {
size := uint(len(in))
if size > maxsize {
size = maxsize
}
return in[:size]
}

func (ts *LocalSetup) acquireTargets(ctx xcontext.Context, ch test.TestStepChannels) ([]*target.Target, error) {
var targets []*target.Target

for {
select {
case target, ok := <-ch.In:
if !ok {
ctx.Debugf("acquired %d targets", len(targets))
return targets, nil
}
targets = append(targets, target)

case <-ctx.Until(xcontext.ErrPaused):
ctx.Debugf("paused during target acquisition, acquired %d", len(targets))
return nil, xcontext.ErrPaused

case <-ctx.Done():
ctx.Debugf("canceled during target acquisition, acquired %d", len(targets))
return nil, ctx.Err()
}
}
}

func (ts *LocalSetup) returnTargets(ctx xcontext.Context, ch test.TestStepChannels, targets []*target.Target) {
for _, target := range targets {
ch.Out <- test.TestStepResult{Target: target}
}
}

// Run executes the cmd step.
func (ts *LocalSetup) Run(ctx xcontext.Context, ch test.TestStepChannels, params test.TestStepParameters, emitter testevent.Emitter, resumeState json.RawMessage) (json.RawMessage, error) {
log := ctx.Logger()

if err := ts.setParams(params); err != nil {
return nil, err
}

// acquire all targets and hold them hostage until the setup is done
targets, err := ts.acquireTargets(ctx, ch)
if err != nil {
return nil, err
}
defer ts.returnTargets(ctx, ch, targets)

// arbitrarily choose first target to associate events with, anyone would work
// but it is unnecessary to have the same event on all targets since this is a
// "gather" type plugin
eventTarget := targets[0]

// used to manually cancel the exec if step becomes paused
ctx, cancel := xcontext.WithCancel(ctx)
defer cancel()

go func() {
select {
case <-ctx.Until(xcontext.ErrPaused):
log.Debugf("step was paused, killing the executing command")
cancel()

case <-ctx.Done():
// does not leak because cancel is deferred
}
}()

cmd := exec.CommandContext(ctx, ts.binary, ts.args...)

var stdout, stderr bytes.Buffer
cmd.Stdout, cmd.Stderr = &stdout, &stderr
cmd.SysProcAttr = &syscall.SysProcAttr{
// Put the command into a separate session (and group) so signals do not propagate directly to it.
Setsid: true,
}

log.Debugf("Running command: %+v", cmd)

err = emitEvent(
ctx,
emitter, eventTarget,
EventSetupStart,
eventSetupStartPayload{Path: cmd.Path, Args: cmd.Args},
)
if err != nil {
log.Warnf("Failed to emit event: %v", err)
}

if cmdErr := cmd.Run(); cmdErr != nil {
// the command may have been canceled as a result of the step pausing
if ctx.IsSignaledWith(xcontext.ErrPaused) {
// nothing to save, just continue pausing
return nil, xcontext.ErrPaused
}

// output the failure event
var payload eventSetupDonePayload

var exitError *exec.ExitError
if errors.As(cmdErr, &exitError) {
// we ran the binary and it had non-zero exit code
payload = eventSetupDonePayload{
ExitCode: exitError.ExitCode(),
Err: truncate(fmt.Sprintf("stdout: %s\nstderr: %s", stdout.String(), stderr.String()), 10240),
}
} else {
// something failed while trying to spawn the process
payload = eventSetupDonePayload{
Err: fmt.Sprintf("unknown error: %v", cmdErr),
}
}

if err := emitEvent(ctx, emitter, eventTarget, EventSetupDone, payload); err != nil {
log.Warnf("Failed to emit event: %v", err)
}

// setup failing is actually a framework error, so crash everything
return nil, cmdErr
}

if err := emitEvent(ctx, emitter, eventTarget, EventSetupDone, nil); err != nil {
log.Warnf("Failed to emit event: %v", err)
}

// TODO: make step output with stdout/err when PR #83 gets merged
return nil, nil
}

func (ts *LocalSetup) setParams(params test.TestStepParameters) error {
param := params.GetOne("binary")
if param.IsEmpty() {
return errors.New("invalid or missing 'binary' parameter")
}

binary := param.String()
if filepath.IsAbs(binary) {
ts.binary = binary
} else {
fullpath, err := exec.LookPath(binary)
if err != nil {
return fmt.Errorf("cannot find '%s' executable in PATH: %v", binary, err)
}
ts.binary = fullpath
}

ts.args = []string{}
args := params.Get("args")
// expand args in case they use functions, but they shouldnt be target aware
for _, arg := range args {
expanded, err := arg.Expand(&target.Target{})
if err != nil {
return fmt.Errorf("failed to expand argument: %s -> %v", arg, err)
}
ts.args = append(ts.args, expanded)
}

return nil
}

// ValidateParameters validates the parameters associated to the TestStep
func (ts *LocalSetup) ValidateParameters(_ xcontext.Context, params test.TestStepParameters) error {
return ts.setParams(params)
}

// New initializes and returns a new Cmd test step.
func New() test.TestStep {
return &LocalSetup{}
}

// 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
}

0 comments on commit f4acb36

Please sign in to comment.