Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 14 additions & 7 deletions pkg/runner/test_steps_variables.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,14 +126,21 @@ func newStepVariablesAccessor(stepLabel string, tsv *testStepsVariables) *stepVa
}

func (sva *stepVariablesAccessor) Add(tgtID string, name string, in interface{}) error {
if len(sva.stepLabel) == 0 {
return nil
}
b, err := json.Marshal(in)
if err != nil {
return fmt.Errorf("failed to serialize variable: %v", in)
var marshalled []byte
if raw, ok := in.(json.RawMessage); ok {
var v interface{}
if err := json.Unmarshal(raw, &v); err != nil {
return fmt.Errorf("invalid input, failed to unmarshal: %v", err)
}
marshalled = raw
} else {
var err error
marshalled, err = json.Marshal(in)
if err != nil {
return fmt.Errorf("failed to serialize variable: %v", in)
}
}
return sva.tsv.Add(tgtID, sva.stepLabel, name, b)
return sva.tsv.Add(tgtID, sva.stepLabel, name, marshalled)
}

func (sva *stepVariablesAccessor) Get(tgtID string, stepLabel, name string, out interface{}) error {
Expand Down
33 changes: 33 additions & 0 deletions plugins/teststeps/variables/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Variables plugin

The *variables* plugin adds its input parameters as step plugin variables that could be later referred to by other plugins.

## Parameters

Any parameter should be a single-value parameter that will be added as a test-variable.
For example:

{
"name": "variables",
"label": "variablesstep"
"parameters": {
"string_variable": ["Hello"],
"int_variable": [123],
"complex_variable": [{"hello": "world"}]
}
}

Will generate a string, int and a json-object variables respectively.

These parameters could be later accessed in the following manner in accordance with step variables guidance:

{
"name": "cmd",
"label": "cmdstep",
"parameters": {
"executable": [echo],
"args": ["{{ StringVar \"variablesstep.string_variable\" }} world number {{ IntVar \"variablesstep.int_variable\" }}"],
"emit_stdout": [true],
"emit_stderr": [true]
}
}
78 changes: 78 additions & 0 deletions plugins/teststeps/variables/variables.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package variables
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please add docs md (see exec plugin for example))


import (
"encoding/json"
"fmt"
"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"
"github.com/linuxboot/contest/plugins/teststeps"
)

// Name is the name used to look this plugin up.
const Name = "variables"

// Events defines the events that a TestStep is allowed to emit
var Events []event.Name

// Variables creates variables that can be used by other test steps
type Variables struct {
}

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

// Run executes the cmd step.
func (ts *Variables) Run(
ctx xcontext.Context,
ch test.TestStepChannels,
ev testevent.Emitter,
stepsVars test.StepsVariables,
inputParams test.TestStepParameters,
resumeState json.RawMessage,
) (json.RawMessage, error) {
if err := ts.ValidateParameters(ctx, inputParams); err != nil {
return nil, err
}
return teststeps.ForEachTarget(Name, ctx, ch, func(ctx xcontext.Context, target *target.Target) error {
for name, ps := range inputParams {
ctx.Debugf("add variable %s, value: %s", name, ps[0])
if err := stepsVars.Add(target.ID, name, ps[0].RawMessage); err != nil {
return err
}
}
return nil
})
}

// ValidateParameters validates the parameters associated to the TestStep
func (ts *Variables) ValidateParameters(ctx xcontext.Context, params test.TestStepParameters) error {
for name, ps := range params {
if err := test.CheckIdentifier(name); err != nil {
return fmt.Errorf("invalid variable name: '%s': %w", name, err)
}
if len(ps) != 1 {
return fmt.Errorf("invalid number of parameter '%s' values: %d (expected 1)", name, len(ps))
}

var res interface{}
if err := json.Unmarshal(ps[0].RawMessage, &res); err != nil {
return fmt.Errorf("invalid json '%s': %w", ps[0].RawMessage, err)
}
}
return nil
}

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

// 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
}
175 changes: 175 additions & 0 deletions plugins/teststeps/variables/variables_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
package variables

import (
"encoding/json"
"fmt"
"sync"
"testing"

"github.com/linuxboot/contest/pkg/event/testevent"
"github.com/linuxboot/contest/pkg/storage"
"github.com/linuxboot/contest/pkg/target"
"github.com/linuxboot/contest/pkg/test"
"github.com/linuxboot/contest/pkg/xcontext"
"github.com/linuxboot/contest/plugins/storage/memory"
"github.com/stretchr/testify/require"
)

func TestCreation(t *testing.T) {
obj := New()
require.NotNil(t, obj)
require.Equal(t, Name, obj.Name())
}

func TestValidateParameters(t *testing.T) {
obj := New()
require.NotNil(t, obj)

require.NoError(t, obj.ValidateParameters(xcontext.Background(), nil))
require.NoError(t, obj.ValidateParameters(xcontext.Background(), test.TestStepParameters{
"var1": []test.Param{
{
RawMessage: json.RawMessage("123"),
},
},
}))
// invalid variable name
require.Error(t, obj.ValidateParameters(xcontext.Background(), test.TestStepParameters{
"var var": []test.Param{
{
RawMessage: json.RawMessage("123"),
},
},
}))
// invalid value
require.Error(t, obj.ValidateParameters(xcontext.Background(), test.TestStepParameters{
"var1": []test.Param{
{
RawMessage: json.RawMessage("ALALALALA[}"),
},
},
}))
}

func TestVariablesEmission(t *testing.T) {
ctx, cancel := xcontext.WithCancel(xcontext.Background())
defer cancel()

obj := New()
require.NotNil(t, obj)

in := make(chan *target.Target, 1)
out := make(chan test.TestStepResult, 1)

m, err := memory.New()
if err != nil {
t.Fatalf("could not initialize memory storage: '%v'", err)
}
storageEngineVault := storage.NewSimpleEngineVault()
if err := storageEngineVault.StoreEngine(m, storage.SyncEngine); err != nil {
t.Fatalf("Failed to set memory storage: '%v'", err)
}
ev := storage.NewTestEventEmitterFetcher(storageEngineVault, testevent.Header{
JobID: 12345,
TestName: "variables_tests",
TestStepLabel: "variables",
})

svm := newStepsVariablesMock()

tgt := target.Target{ID: "id1"}
in <- &tgt
close(in)

state, err := obj.Run(ctx, test.TestStepChannels{In: in, Out: out}, ev, svm, test.TestStepParameters{
"str_variable": []test.Param{
{
RawMessage: json.RawMessage("\"dummy\""),
},
},
"int_variable": []test.Param{
{
RawMessage: json.RawMessage("123"),
},
},
"complex_variable": []test.Param{
{
RawMessage: json.RawMessage("{\"name\":\"value\"}"),
},
},
}, nil)
require.NoError(t, err)
require.Empty(t, state)

stepResult := <-out
require.Equal(t, tgt, *stepResult.Target)
require.NoError(t, stepResult.Err)

var strVar string
require.NoError(t, svm.get(tgt.ID, "str_variable", &strVar))
require.Equal(t, "dummy", strVar)

var intVar int
require.NoError(t, svm.get(tgt.ID, "int_variable", &intVar))
require.Equal(t, 123, intVar)

var complexVar dummyStruct
require.NoError(t, svm.get(tgt.ID, "complex_variable", &complexVar))
require.Equal(t, dummyStruct{Name: "value"}, complexVar)
}

type dummyStruct struct {
Name string `json:"name"`
}

type stepsVariablesMock struct {
mu sync.Mutex
variables map[string]map[string]json.RawMessage
}

func newStepsVariablesMock() *stepsVariablesMock {
return &stepsVariablesMock{
variables: make(map[string]map[string]json.RawMessage),
}
}

func (svm *stepsVariablesMock) AddRaw(tgtID string, name string, b json.RawMessage) error {
svm.mu.Lock()
defer svm.mu.Unlock()

targetVars := svm.variables[tgtID]
if targetVars == nil {
targetVars = make(map[string]json.RawMessage)
svm.variables[tgtID] = targetVars
}
targetVars[name] = b
return nil
}

func (svm *stepsVariablesMock) Add(tgtID string, name string, v interface{}) error {
b, err := json.Marshal(v)
if err != nil {
return err
}

return svm.AddRaw(tgtID, name, b)
}

func (svm *stepsVariablesMock) Get(tgtID string, stepLabel, name string, out interface{}) error {
panic("not implemented")
}

func (svm *stepsVariablesMock) get(tgtID string, name string, out interface{}) error {
svm.mu.Lock()
defer svm.mu.Unlock()

targetVars := svm.variables[tgtID]
if targetVars == nil {
return fmt.Errorf("no target: %s", tgtID)
}
b, found := targetVars[name]
if !found {
return fmt.Errorf("no variable: %s", name)
}
return json.Unmarshal(b, out)
}
Loading