Skip to content

Commit

Permalink
Merge branch 'main' into renovate/all-minor-patch-digest-pin
Browse files Browse the repository at this point in the history
  • Loading branch information
AlaricWhitney authored Apr 25, 2024
2 parents 9f6d08e + 69d635a commit b82efd1
Show file tree
Hide file tree
Showing 5 changed files with 396 additions and 156 deletions.
20 changes: 10 additions & 10 deletions cmd/vela-k6/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,34 +8,34 @@ import (

"github.com/go-vela/vela-k6/plugin"
"github.com/go-vela/vela-k6/version"
"github.com/sirupsen/logrus"
)

func main() {
// capture application version information
v := version.New()

// serialize the version information as pretty JSON
bytes, err := json.MarshalIndent(v, "", " ")
if err != nil {
logrus.Fatal(err)
var bytes []byte

var err error

if bytes, err = json.MarshalIndent(v, "", " "); err != nil {
log.Fatal(err)
}

// output the version information to stdout
fmt.Fprintf(os.Stdout, "%s\n", string(bytes))

cfg, err := plugin.ConfigFromEnv()
if err != nil {
p := plugin.New()
if err = p.ConfigFromEnv(); err != nil {
log.Fatalf("FATAL: %s\n", err)
}

err = plugin.RunSetupScript(cfg)
if err != nil {
if err = p.RunSetupScript(); err != nil {
log.Fatalf("FATAL: %s\n", err)
}

err = plugin.RunPerfTests(cfg)
if err != nil {
if err = p.RunPerfTests(); err != nil {
log.Fatalf("FATAL: %s\n", err)
}
}
22 changes: 14 additions & 8 deletions plugin/mock/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,15 @@ import (
const thresholdsBreachedExitCode = 99

type Command struct {
args []string
waitErr error
args []string
waitErr error
stdoutPipeErr error
stderrPipeErr error
startErr error
}

func (m *Command) Start() error {
return nil
return m.startErr
}

func (m *Command) Wait() error {
Expand All @@ -29,21 +32,24 @@ func (m *Command) String() (str string) {

func (m *Command) StdoutPipe() (io.ReadCloser, error) {
dummyReader := strings.NewReader("")
return io.NopCloser(dummyReader), nil
return io.NopCloser(dummyReader), m.stdoutPipeErr
}

func (m *Command) StderrPipe() (io.ReadCloser, error) {
dummyReader := strings.NewReader("")
return io.NopCloser(dummyReader), nil
return io.NopCloser(dummyReader), m.stderrPipeErr
}

// CommandBuilderWithError returns a function that will return a mock.Command
// which will return the specified waitErr on cmd.Wait().
func CommandBuilderWithError(waitErr error) func(string, ...string) types.ShellCommand {
func CommandBuilderWithError(waitErr error, stdoutPipeErr error, stderrPipeErr error, startErr error) func(string, ...string) types.ShellCommand {
return func(name string, args ...string) types.ShellCommand {
return &Command{
args: append([]string{name}, args...),
waitErr: waitErr,
args: append([]string{name}, args...),
waitErr: waitErr,
stdoutPipeErr: stdoutPipeErr,
stderrPipeErr: stderrPipeErr,
startErr: startErr,
}
}
}
Expand Down
61 changes: 61 additions & 0 deletions plugin/mock/plugin_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package mock

import (
"errors"
"io"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestStart(t *testing.T) {
c := &Command{}
assert.NoError(t, c.Start())
}

func TestWait(t *testing.T) {
t.Run("No Error", func(t *testing.T) {
c := &Command{}
assert.NoError(t, c.Wait())
})
t.Run("An Error", func(t *testing.T) {
c := &Command{waitErr: errors.New("some error")}
assert.ErrorContains(t, c.Wait(), "some error")
})
}

func TestString(t *testing.T) {
c := &Command{}
assert.Empty(t, c.String())
}

func TestStdoutPipe(t *testing.T) {
c := &Command{}
result, err := c.StdoutPipe()
require.NoError(t, err)

b, err := io.ReadAll(result)
assert.NoError(t, err)
assert.Empty(t, b)
}
func TestStderrPipe(t *testing.T) {
c := &Command{}
result, err := c.StderrPipe()
require.NoError(t, err)

b, err := io.ReadAll(result)
assert.NoError(t, err)
assert.Empty(t, b)
}

func TestCommandBuilderWithError(t *testing.T) {
result := CommandBuilderWithError(errors.New("some error"), nil, nil, nil)
assert.ErrorContains(t, result("start").Wait(), "some error")
}

func TestThresholdError(t *testing.T) {
th := &ThresholdError{}
assert.Equal(t, th.ExitCode(), thresholdsBreachedExitCode)
assert.Contains(t, th.Error(), "mock")
}
97 changes: 55 additions & 42 deletions plugin/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,25 @@ import (

const thresholdsBreachedExitCode = 99

type pluginType struct {
config config
buildCommand func(name string, args ...string) types.ShellCommand // buildCommand can be swapped out for a mock function for unit testing.
verifyFileExists func(path string) error // verifyFileExists can be swapped out for a mock function for unit testing.
}

type Plugin interface {
ConfigFromEnv() error
RunSetupScript() error
RunPerfTests() error
}

func New() Plugin {
return &pluginType{
buildCommand: buildExecCommand,
verifyFileExists: checkOSStat,
}
}

// buildExecCommand returns a ShellCommand with the given arguments. The
// return type of ShellCommand is for mocking purposes.
func buildExecCommand(name string, args ...string) types.ShellCommand {
Expand All @@ -35,30 +54,26 @@ var (
validJSFilePattern = regexp.MustCompile(`^(\./|(\.\./)+)?[a-zA-Z0-9-_/]*[a-zA-Z0-9]\.js$`)
validJSONFilePattern = regexp.MustCompile(`^(\./|(\.\./)+)?[a-zA-Z0-9-_/]*[a-zA-Z0-9]\.json$`)
validShellFilePattern = regexp.MustCompile(`^(\./|(\.\./)+)?[a-zA-Z0-9-_/]*[a-zA-Z0-9]\.sh$`)
// buildCommand can be swapped out for a mock function for unit testing.
buildCommand = buildExecCommand
// verifyFileExists can be swapped out for a mock function for unit testing.
verifyFileExists = checkOSStat
)

// ConfigFromEnv returns a Config populated with the values of the Vela
// parameters. Script and output paths will be sanitized/validated, and
// an error is returned if the script path is empty or invalid. If the
// output path is invalid, OutputPath is set to "".
func ConfigFromEnv() (*Config, error) {
cfg := &Config{}
cfg.ScriptPath = sanitizeScriptPath(os.Getenv("PARAMETER_SCRIPT_PATH"))
cfg.OutputPath = sanitizeOutputPath(os.Getenv("PARAMETER_OUTPUT_PATH"))
cfg.SetupScriptPath = sanitizeSetupPath(os.Getenv("PARAMETER_SETUP_SCRIPT_PATH"))
cfg.FailOnThresholdBreach = !strings.EqualFold(os.Getenv("PARAMETER_FAIL_ON_THRESHOLD_BREACH"), "false")
cfg.ProjektorCompatMode = strings.EqualFold(os.Getenv("PARAMETER_PROJEKTOR_COMPAT_MODE"), "true")
cfg.LogProgress = strings.EqualFold(os.Getenv("PARAMETER_LOG_PROGRESS"), "true")
func (p *pluginType) ConfigFromEnv() error {
p.config.ScriptPath = sanitizeScriptPath(os.Getenv("PARAMETER_SCRIPT_PATH"))
p.config.OutputPath = sanitizeOutputPath(os.Getenv("PARAMETER_OUTPUT_PATH"))
p.config.SetupScriptPath = sanitizeSetupPath(os.Getenv("PARAMETER_SETUP_SCRIPT_PATH"))
p.config.FailOnThresholdBreach = !strings.EqualFold(os.Getenv("PARAMETER_FAIL_ON_THRESHOLD_BREACH"), "false")
p.config.ProjektorCompatMode = strings.EqualFold(os.Getenv("PARAMETER_PROJEKTOR_COMPAT_MODE"), "true")
p.config.LogProgress = strings.EqualFold(os.Getenv("PARAMETER_LOG_PROGRESS"), "true")

if cfg.ScriptPath == "" || !strings.HasSuffix(cfg.ScriptPath, ".js") {
return nil, fmt.Errorf("invalid script file. provide the filepath to a JavaScript file in plugin parameter 'script_path' (e.g. 'script_path: \"/k6-test/script.js\"'). the filepath must follow the regular expression `%s`", validJSFilePattern)
if p.config.ScriptPath == "" || !strings.HasSuffix(p.config.ScriptPath, ".js") {
p.config = config{} // reset config
return fmt.Errorf("invalid script file. provide the filepath to a JavaScript file in plugin parameter 'script_path' (e.g. 'script_path: \"/k6-test/script.js\"'). the filepath must follow the regular expression `%s`", validJSFilePattern)
}

return cfg, nil
return nil
}

// sanitizeScriptPath returns the input string if it satisfies the pattern
Expand All @@ -81,47 +96,45 @@ func sanitizeSetupPath(input string) string {

// buildK6Command returns a ShellCommand that will execute K6 tests
// using the script path, output path, and output type in cfg.
func buildK6Command(cfg *Config) (cmd types.ShellCommand, err error) {
func (p *pluginType) buildK6Command() (cmd types.ShellCommand, err error) {
commandArgs := []string{"run"}
if !cfg.LogProgress {
if !p.config.LogProgress {
commandArgs = append(commandArgs, "-q")
}

if cfg.OutputPath != "" {
outputDir := filepath.Dir(cfg.OutputPath)
err = os.MkdirAll(outputDir, os.FileMode(0755))

if err != nil {
if p.config.OutputPath != "" {
outputDir := filepath.Dir(p.config.OutputPath)
if err = os.MkdirAll(outputDir, os.FileMode(0755)); err != nil {
return
}

if cfg.ProjektorCompatMode {
commandArgs = append(commandArgs, fmt.Sprintf("--summary-export=%s", cfg.OutputPath))
if p.config.ProjektorCompatMode {
commandArgs = append(commandArgs, fmt.Sprintf("--summary-export=%s", p.config.OutputPath))
} else {
commandArgs = append(commandArgs, "--out", fmt.Sprintf("json=%s", cfg.OutputPath))
commandArgs = append(commandArgs, "--out", fmt.Sprintf("json=%s", p.config.OutputPath))
}
}

commandArgs = append(commandArgs, cfg.ScriptPath)
cmd = buildCommand("k6", commandArgs...)
commandArgs = append(commandArgs, p.config.ScriptPath)
cmd = p.buildCommand("k6", commandArgs...)

return
}

// RunSetupScript runs the setup script located at the cfg.SetupScriptPath
// if the path is not empty.
func RunSetupScript(cfg *Config) error {
if cfg.SetupScriptPath == "" {
func (p *pluginType) RunSetupScript() error {
if p.config.SetupScriptPath == "" {
log.Println("No setup script specified, skipping.")
return nil
}

err := verifyFileExists(cfg.SetupScriptPath)
err := p.verifyFileExists(p.config.SetupScriptPath)
if err != nil {
return fmt.Errorf("read setup script file at %s: %w", cfg.SetupScriptPath, err)
return fmt.Errorf("read setup script file at %s: %w", p.config.SetupScriptPath, err)
}

cmd := buildCommand(cfg.SetupScriptPath)
cmd := p.buildCommand(p.config.SetupScriptPath)

stdout, err := cmd.StdoutPipe()
if err != nil {
Expand Down Expand Up @@ -155,15 +168,15 @@ func RunSetupScript(cfg *Config) error {
}

// RunPerfTests runs the K6 performance test script located at the
// cfg.ScriptPath and saves the output to cfg.OutputPath if it is present
// p.config.ScriptPath and saves the output to p.config.OutputPath if it is present
// and a valid filepath.
func RunPerfTests(cfg *Config) error {
err := verifyFileExists(cfg.ScriptPath)
func (p *pluginType) RunPerfTests() error {
err := p.verifyFileExists(p.config.ScriptPath)
if err != nil {
return fmt.Errorf("read script file at %s: %w", cfg.ScriptPath, err)
return fmt.Errorf("read script file at %s: %w", p.config.ScriptPath, err)
}

cmd, err := buildK6Command(cfg)
cmd, err := p.buildK6Command()
if err != nil {
return fmt.Errorf("create output directory: %w", err)
}
Expand Down Expand Up @@ -198,18 +211,18 @@ func RunPerfTests(cfg *Config) error {
ok := errors.As(execError, &exitError)

if ok && exitError.ExitCode() == thresholdsBreachedExitCode {
if cfg.FailOnThresholdBreach {
if p.config.FailOnThresholdBreach {
return fmt.Errorf("thresholds breached")
}
} else {
return execError
}
}

if cfg.OutputPath != "" {
path, err := filepath.Abs(cfg.OutputPath)
if p.config.OutputPath != "" {
path, err := filepath.Abs(p.config.OutputPath)
if err != nil {
log.Printf("save output to %s: %s\n", cfg.OutputPath, err)
log.Printf("save output to %s: %s\n", p.config.OutputPath, err)
} else {
log.Printf("Output file saved at %s\n", path)
}
Expand All @@ -232,7 +245,7 @@ func readLinesFromPipe(pipe io.ReadCloser, wg *sync.WaitGroup) {
}
}

type Config struct {
type config struct {
ScriptPath string
OutputPath string
SetupScriptPath string
Expand Down
Loading

0 comments on commit b82efd1

Please sign in to comment.