1
0
mirror of https://github.com/SAP/jenkins-library.git synced 2025-09-16 09:26:22 +02:00

Add golang implementation for karma tests (#919)

* Provide golang based karma step
This commit is contained in:
Oliver Nocon
2019-10-25 14:58:59 +02:00
committed by GitHub
parent 1c5df085d5
commit d053653a93
17 changed files with 1388 additions and 7 deletions

View File

@@ -22,6 +22,6 @@ indent_size = none
[cfg/id_rsa.enc]
indent_style = none
indent_size = none
[{go.mod,go.sum,*.go}]
[{go.mod,go.sum,*.go,*.golden}]
indent_style = tab
indent_size = 8

View File

@@ -12,7 +12,7 @@ import (
"github.com/stretchr/testify/assert"
)
func openFileMock(name string) (io.ReadCloser, error) {
func configOpenFileMock(name string) (io.ReadCloser, error) {
var r string
switch name {
case "TestAddCustomDefaults_default1":
@@ -52,7 +52,7 @@ func TestConfigCommand(t *testing.T) {
t.Run("Run", func(t *testing.T) {
t.Run("Success case", func(t *testing.T) {
configOptions.openFile = openFileMock
configOptions.openFile = configOpenFileMock
err := cmd.RunE(cmd, []string{})
assert.NoError(t, err, "error occured but none expected")
})

6
cmd/interfaces.go Normal file
View File

@@ -0,0 +1,6 @@
package cmd
type execRunner interface {
RunExecutable(e string, p ...string) error
Dir(d string)
}

35
cmd/karmaExecuteTests.go Normal file
View File

@@ -0,0 +1,35 @@
package cmd
import (
"strings"
"github.com/SAP/jenkins-library/pkg/command"
"github.com/pkg/errors"
)
func karmaExecuteTests(myKarmaExecuteTestsOptions karmaExecuteTestsOptions) error {
c := command.Command{}
return runKarma(myKarmaExecuteTestsOptions, &c)
}
func runKarma(myKarmaExecuteTestsOptions karmaExecuteTestsOptions, command execRunner) error {
installCommandTokens := tokenize(myKarmaExecuteTestsOptions.InstallCommand)
command.Dir(myKarmaExecuteTestsOptions.ModulePath)
err := command.RunExecutable(installCommandTokens[0], installCommandTokens[1:]...)
if err != nil {
return errors.Wrapf(err, "failed to execute install command '%v'", myKarmaExecuteTestsOptions.InstallCommand)
}
runCommandTokens := tokenize(myKarmaExecuteTestsOptions.RunCommand)
command.Dir(myKarmaExecuteTestsOptions.ModulePath)
err = command.RunExecutable(runCommandTokens[0], runCommandTokens[1:]...)
if err != nil {
return errors.Wrapf(err, "failed to execute run command '%v'", myKarmaExecuteTestsOptions.RunCommand)
}
return nil
}
func tokenize(command string) []string {
return strings.Split(command, " ")
}

View File

@@ -0,0 +1,87 @@
package cmd
import (
//"os"
"github.com/SAP/jenkins-library/pkg/config"
"github.com/spf13/cobra"
)
type karmaExecuteTestsOptions struct {
InstallCommand string `json:"installCommand,omitempty"`
ModulePath string `json:"modulePath,omitempty"`
RunCommand string `json:"runCommand,omitempty"`
}
var myKarmaExecuteTestsOptions karmaExecuteTestsOptions
var karmaExecuteTestsStepConfigJSON string
// KarmaExecuteTestsCommand Executes the Karma test runner
func KarmaExecuteTestsCommand() *cobra.Command {
metadata := karmaExecuteTestsMetadata()
var createKarmaExecuteTestsCmd = &cobra.Command{
Use: "karmaExecuteTests",
Short: "Executes the Karma test runner",
Long: `In this step the ([Karma test runner](http://karma-runner.github.io)) is executed.
The step is using the ` + "`" + `seleniumExecuteTest` + "`" + ` step to spin up two containers in a Docker network:
* a Selenium/Chrome container (` + "`" + `selenium/standalone-chrome` + "`" + `)
* a NodeJS container (` + "`" + `node:8-stretch` + "`" + `)
In the Docker network, the containers can be referenced by the values provided in ` + "`" + `dockerName` + "`" + ` and ` + "`" + `sidecarName` + "`" + `, the default values are ` + "`" + `karma` + "`" + ` and ` + "`" + `selenium` + "`" + `. These values must be used in the ` + "`" + `hostname` + "`" + ` properties of the test configuration ([Karma](https://karma-runner.github.io/1.0/config/configuration-file.html) and [WebDriver](https://github.com/karma-runner/karma-webdriver-launcher#usage)).
!!! note
In a Kubernetes environment, the containers both need to be referenced with ` + "`" + `localhost` + "`" + `.`,
PreRunE: func(cmd *cobra.Command, args []string) error {
return PrepareConfig(cmd, &metadata, "karmaExecuteTests", &myKarmaExecuteTestsOptions, openPiperFile)
},
RunE: func(cmd *cobra.Command, args []string) error {
return karmaExecuteTests(myKarmaExecuteTestsOptions)
},
}
addKarmaExecuteTestsFlags(createKarmaExecuteTestsCmd)
return createKarmaExecuteTestsCmd
}
func addKarmaExecuteTestsFlags(cmd *cobra.Command) {
cmd.Flags().StringVar(&myKarmaExecuteTestsOptions.InstallCommand, "installCommand", "npm install --quiet", "The command that is executed to install the test tool.")
cmd.Flags().StringVar(&myKarmaExecuteTestsOptions.ModulePath, "modulePath", ".", "Define the path of the module to execute tests on.")
cmd.Flags().StringVar(&myKarmaExecuteTestsOptions.RunCommand, "runCommand", "npm run karma", "The command that is executed to start the tests.")
cmd.MarkFlagRequired("installCommand")
cmd.MarkFlagRequired("modulePath")
cmd.MarkFlagRequired("runCommand")
}
// retrieve step metadata
func karmaExecuteTestsMetadata() config.StepData {
var theMetaData = config.StepData{
Spec: config.StepSpec{
Inputs: config.StepInputs{
Parameters: []config.StepParameters{
{
Name: "installCommand",
Scope: []string{"GENERAL", "PARAMETERS", "STAGES", "STEPS"},
Type: "string",
Mandatory: true,
},
{
Name: "modulePath",
Scope: []string{"PARAMETERS", "STAGES", "STEPS"},
Type: "string",
Mandatory: true,
},
{
Name: "runCommand",
Scope: []string{"GENERAL", "PARAMETERS", "STAGES", "STEPS"},
Type: "string",
Mandatory: true,
},
},
},
},
}
return theMetaData
}

View File

@@ -0,0 +1,16 @@
package cmd
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestKarmaExecuteTestsCommand(t *testing.T) {
testCmd := KarmaExecuteTestsCommand()
// only high level testing performed - details are tested in step generation procudure
assert.Equal(t, "karmaExecuteTests", testCmd.Use, "command name incorrect")
}

View File

@@ -0,0 +1,65 @@
package cmd
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
)
type mockRunner struct {
dir []string
calls []execCall
}
type execCall struct {
exec string
params []string
}
func (m *mockRunner) Dir(d string) {
m.dir = append(m.dir, d)
}
func (m *mockRunner) RunExecutable(e string, p ...string) error {
if e == "fail" {
return fmt.Errorf("error case")
}
exec := execCall{exec: e, params: p}
m.calls = append(m.calls, exec)
return nil
}
func TestRunKarma(t *testing.T) {
t.Run("success case", func(t *testing.T) {
opts := karmaExecuteTestsOptions{ModulePath: "./test", InstallCommand: "npm install test", RunCommand: "npm run test"}
e := mockRunner{}
err := runKarma(opts, &e)
assert.NoError(t, err, "error occured but no error expected")
assert.Equal(t, e.dir[0], "./test", "install command dir incorrect")
assert.Equal(t, e.calls[0], execCall{exec: "npm", params: []string{"install", "test"}}, "install command/params incorrect")
assert.Equal(t, e.dir[1], "./test", "run command dir incorrect")
assert.Equal(t, e.calls[1], execCall{exec: "npm", params: []string{"run", "test"}}, "run command/params incorrect")
})
t.Run("error case install command", func(t *testing.T) {
opts := karmaExecuteTestsOptions{ModulePath: "./test", InstallCommand: "fail install test", RunCommand: "npm run test"}
e := mockRunner{}
err := runKarma(opts, &e)
assert.Error(t, err, "error expected but none occcured")
})
t.Run("error case run command", func(t *testing.T) {
opts := karmaExecuteTestsOptions{ModulePath: "./test", InstallCommand: "npm install test", RunCommand: "fail run test"}
e := mockRunner{}
err := runKarma(opts, &e)
assert.Error(t, err, "error expected but none occcured")
})
}

View File

@@ -1,11 +1,14 @@
package cmd
import (
"encoding/json"
"fmt"
"io"
"os"
"strings"
"github.com/SAP/jenkins-library/pkg/config"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)
@@ -36,6 +39,16 @@ var generalConfig generalConfigOptions
func Execute() {
rootCmd.AddCommand(ConfigCommand())
addRootFlags(rootCmd)
if err := rootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}
func addRootFlags(rootCmd *cobra.Command) {
rootCmd.PersistentFlags().StringVar(&generalConfig.customConfig, "customConfig", ".pipeline/config.yml", "Path to the pipeline configuration file")
rootCmd.PersistentFlags().StringSliceVar(&generalConfig.defaultConfig, "defaultConfig", nil, "Default configurations, passed as path to yaml file")
rootCmd.PersistentFlags().StringVar(&generalConfig.parametersJSON, "parametersJSON", os.Getenv("PIPER_parametersJSON"), "Parameters to be considered in JSON format")
@@ -43,10 +56,46 @@ func Execute() {
rootCmd.PersistentFlags().StringVar(&generalConfig.stepConfigJSON, "stepConfigJSON", os.Getenv("PIPER_stepConfigJSON"), "Step configuration in JSON format")
rootCmd.PersistentFlags().BoolVarP(&generalConfig.verbose, "verbose", "v", false, "verbose output")
if err := rootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
// PrepareConfig reads step configuration from various sources and merges it (defaults, config file, flags, ...)
func PrepareConfig(cmd *cobra.Command, metadata *config.StepData, stepName string, options interface{}, openFile func(s string) (io.ReadCloser, error)) error {
filters := metadata.GetParameterFilters()
flagValues := config.AvailableFlagValues(cmd, &filters)
var myConfig config.Config
var stepConfig config.StepConfig
if len(generalConfig.stepConfigJSON) != 0 {
// ignore config & defaults in favor of passed stepConfigJSON
stepConfig = config.GetStepConfigWithJSON(flagValues, generalConfig.stepConfigJSON, filters)
} else {
// use config & defaults
//accept that config file and defaults cannot be loaded since both are not mandatory here
customConfig, _ := openFile(generalConfig.customConfig)
var defaultConfig []io.ReadCloser
for _, f := range generalConfig.defaultConfig {
//ToDo: support also https as source
fc, _ := openFile(f)
defaultConfig = append(defaultConfig, fc)
}
var err error
stepConfig, err = myConfig.GetStepConfig(flagValues, generalConfig.parametersJSON, customConfig, defaultConfig, filters, generalConfig.stageName, stepName)
if err != nil {
return errors.Wrap(err, "retrieving step configuration failed")
}
}
confJSON, _ := json.Marshal(stepConfig.Config)
json.Unmarshal(confJSON, &options)
config.MarkFlagsWithValue(cmd, stepConfig)
return nil
}
func openPiperFile(name string) (io.ReadCloser, error) {

110
cmd/piper_test.go Normal file
View File

@@ -0,0 +1,110 @@
package cmd
import (
"io"
"io/ioutil"
"strings"
"testing"
"github.com/SAP/jenkins-library/pkg/config"
"github.com/spf13/cobra"
flag "github.com/spf13/pflag"
"github.com/stretchr/testify/assert"
)
type stepOptions struct {
TestParam string `json:"testParam,omitempty"`
}
func openFileMock(name string) (io.ReadCloser, error) {
var r string
switch name {
case "testDefaults.yml":
r = "general:\n testParam: testValue"
case "testDefaultsInvalid.yml":
r = "invalid yaml"
default:
r = ""
}
return ioutil.NopCloser(strings.NewReader(r)), nil
}
func TestAddRootFlags(t *testing.T) {
var testRootCmd = &cobra.Command{Use: "test", Short: "This is just a test"}
addRootFlags(testRootCmd)
assert.NotNil(t, testRootCmd.Flag("customConfig"), "expected flag not available")
assert.NotNil(t, testRootCmd.Flag("defaultConfig"), "expected flag not available")
assert.NotNil(t, testRootCmd.Flag("parametersJSON"), "expected flag not available")
assert.NotNil(t, testRootCmd.Flag("stageName"), "expected flag not available")
assert.NotNil(t, testRootCmd.Flag("stepConfigJSON"), "expected flag not available")
assert.NotNil(t, testRootCmd.Flag("verbose"), "expected flag not available")
}
func TestPrepareConfig(t *testing.T) {
defaultsBak := generalConfig.defaultConfig
generalConfig.defaultConfig = []string{"testDefaults.yml"}
defer func() { generalConfig.defaultConfig = defaultsBak }()
t.Run("using stepConfigJSON", func(t *testing.T) {
stepConfigJSONBak := generalConfig.stepConfigJSON
generalConfig.stepConfigJSON = `{"testParam": "testValueJSON"}`
defer func() { generalConfig.stepConfigJSON = stepConfigJSONBak }()
testOptions := stepOptions{}
var testCmd = &cobra.Command{Use: "test", Short: "This is just a test"}
testCmd.Flags().StringVar(&testOptions.TestParam, "testParam", "", "test usage")
metadata := config.StepData{
Spec: config.StepSpec{
Inputs: config.StepInputs{
Parameters: []config.StepParameters{
{Name: "testParam", Scope: []string{"GENERAL"}},
},
},
},
}
PrepareConfig(testCmd, &metadata, "testStep", &testOptions, openFileMock)
assert.Equal(t, "testValueJSON", testOptions.TestParam, "wrong value retrieved from config")
})
t.Run("using config files", func(t *testing.T) {
t.Run("success case", func(t *testing.T) {
testOptions := stepOptions{}
var testCmd = &cobra.Command{Use: "test", Short: "This is just a test"}
testCmd.Flags().StringVar(&testOptions.TestParam, "testParam", "", "test usage")
metadata := config.StepData{
Spec: config.StepSpec{
Inputs: config.StepInputs{
Parameters: []config.StepParameters{
{Name: "testParam", Scope: []string{"GENERAL"}},
},
},
},
}
err := PrepareConfig(testCmd, &metadata, "testStep", &testOptions, openFileMock)
assert.NoError(t, err, "no error expected but error occured")
//assert config
assert.Equal(t, "testValue", testOptions.TestParam, "wrong value retrieved from config")
//assert that flag has been marked as changed
testCmd.Flags().VisitAll(func(pflag *flag.Flag) {
if pflag.Name == "testParam" {
assert.True(t, pflag.Changed, "flag should be marked as changed")
}
})
})
t.Run("error case", func(t *testing.T) {
generalConfig.defaultConfig = []string{"testDefaultsInvalid.yml"}
testOptions := stepOptions{}
var testCmd = &cobra.Command{Use: "test", Short: "This is just a test"}
metadata := config.StepData{}
err := PrepareConfig(testCmd, &metadata, "testStep", &testOptions, openFileMock)
assert.Error(t, err, "error expected but none occured")
})
})
}

134
pkg/command/command.go Normal file
View File

@@ -0,0 +1,134 @@
package command
import (
"bytes"
"fmt"
"io"
"os"
"os/exec"
"sync"
"github.com/pkg/errors"
)
// Command defines the information required for executing a call to any executable
type Command struct {
dir string
Stdout io.Writer
Stderr io.Writer
}
// Dir sets the working directory for the execution
func (c *Command) Dir(d string) {
c.dir = d
}
// ExecCommand defines how to execute os commands
var ExecCommand = exec.Command
// RunShell runs the specified command on the shell
func (c *Command) RunShell(shell, script string) error {
_out, _err := prepareOut(c.Stdout, c.Stderr)
cmd := ExecCommand(shell)
cmd.Dir = c.dir
in := bytes.Buffer{}
in.Write([]byte(script))
cmd.Stdin = &in
if err := runCmd(cmd, _out, _err); err != nil {
return errors.Wrapf(err, "running shell script failed with %v", shell)
}
return nil
}
// RunExecutable runs the specified executable with parameters
func (c *Command) RunExecutable(executable string, params ...string) error {
_out, _err := prepareOut(c.Stdout, c.Stderr)
cmd := ExecCommand(executable, params...)
if len(c.dir) > 0 {
cmd.Dir = c.dir
}
if err := runCmd(cmd, _out, _err); err != nil {
return errors.Wrapf(err, "running command '%v' failed", executable)
}
return nil
}
func runCmd(cmd *exec.Cmd, _out, _err io.Writer) error {
stdout, stderr, err := cmdPipes(cmd)
if err != nil {
return errors.Wrap(err, "getting commmand pipes failed")
}
err = cmd.Start()
if err != nil {
return errors.Wrap(err, "starting command failed")
}
var wg sync.WaitGroup
wg.Add(2)
var errStdout, errStderr error
go func() {
_, errStdout = io.Copy(_out, stdout)
wg.Done()
}()
go func() {
_, errStderr = io.Copy(_err, stderr)
wg.Done()
}()
wg.Wait()
err = cmd.Wait()
if err != nil {
return errors.Wrap(err, "cmd.Run() failed")
}
if errStdout != nil || errStderr != nil {
return fmt.Errorf("failed to capture stdout/stderr: '%v'/'%v'", errStdout, errStderr)
}
return nil
}
func prepareOut(stdout, stderr io.Writer) (io.Writer, io.Writer) {
//ToDo: check use of multiwriter instead to always write into os.Stdout and os.Stdin?
//stdout := io.MultiWriter(os.Stdout, &stdoutBuf)
//stderr := io.MultiWriter(os.Stderr, &stderrBuf)
if stdout == nil {
stdout = os.Stdout
}
if stderr == nil {
stderr = os.Stderr
}
return stdout, stderr
}
func cmdPipes(cmd *exec.Cmd) (io.ReadCloser, io.ReadCloser, error) {
stdout, err := cmd.StdoutPipe()
if err != nil {
return nil, nil, errors.Wrap(err, "getting Stdout pipe failed")
}
stderr, err := cmd.StderrPipe()
if err != nil {
return nil, nil, errors.Wrap(err, "getting Stderr pipe failed")
}
return stdout, stderr, nil
}

182
pkg/command/command_test.go Normal file
View File

@@ -0,0 +1,182 @@
package command
import (
"bytes"
"fmt"
"io/ioutil"
"os"
"os/exec"
"testing"
)
//based on https://golang.org/src/os/exec/exec_test.go
func helperCommand(command string, s ...string) (cmd *exec.Cmd) {
cs := []string{"-test.run=TestHelperProcess", "--", command}
cs = append(cs, s...)
cmd = exec.Command(os.Args[0], cs...)
cmd.Env = []string{"GO_WANT_HELPER_PROCESS=1"}
return cmd
}
func TestShellRun(t *testing.T) {
t.Run("test shell", func(t *testing.T) {
ExecCommand = helperCommand
defer func() { ExecCommand = exec.Command }()
o := new(bytes.Buffer)
e := new(bytes.Buffer)
s := Command{Stdout: o, Stderr: e}
s.RunShell("/bin/bash", "myScript")
t.Run("success case", func(t *testing.T) {
t.Run("stdin-stdout", func(t *testing.T) {
expectedOut := "Stdout: command /bin/bash - Stdin: myScript\n"
if oStr := o.String(); oStr != expectedOut {
t.Errorf("expected: %v got: %v", expectedOut, oStr)
}
})
t.Run("stderr", func(t *testing.T) {
expectedErr := "Stderr: command /bin/bash\n"
if eStr := e.String(); eStr != expectedErr {
t.Errorf("expected: %v got: %v", expectedErr, eStr)
}
})
})
})
}
func TestExecutableRun(t *testing.T) {
t.Run("test shell", func(t *testing.T) {
ExecCommand = helperCommand
defer func() { ExecCommand = exec.Command }()
o := new(bytes.Buffer)
e := new(bytes.Buffer)
ex := Command{Stdout: o, Stderr: e}
ex.RunExecutable("echo", []string{"foo bar", "baz"}...)
t.Run("success case", func(t *testing.T) {
t.Run("stdin", func(t *testing.T) {
expectedOut := "foo bar baz\n"
if oStr := o.String(); oStr != expectedOut {
t.Errorf("expected: %v got: %v", expectedOut, oStr)
}
})
t.Run("stderr", func(t *testing.T) {
expectedErr := "Stderr: command echo\n"
if eStr := e.String(); eStr != expectedErr {
t.Errorf("expected: %v got: %v", expectedErr, eStr)
}
})
})
})
}
func TestPrepareOut(t *testing.T) {
t.Run("os", func(t *testing.T) {
s := Command{}
_out, _err := prepareOut(s.Stdout, s.Stderr)
if _out != os.Stdout {
t.Errorf("expected out to be os.Stdout")
}
if _err != os.Stderr {
t.Errorf("expected err to be os.Stderr")
}
})
t.Run("custom", func(t *testing.T) {
o := bytes.NewBufferString("")
e := bytes.NewBufferString("")
s := Command{Stdout: o, Stderr: e}
_out, _err := prepareOut(s.Stdout, s.Stderr)
expectOut := "Test out"
expectErr := "Test err"
_out.Write([]byte(expectOut))
_err.Write([]byte(expectErr))
t.Run("out", func(t *testing.T) {
if o.String() != expectOut {
t.Errorf("expected: %v got: %v", expectOut, o.String())
}
})
t.Run("err", func(t *testing.T) {
if e.String() != expectErr {
t.Errorf("expected: %v got: %v", expectErr, e.String())
}
})
})
}
func TestCmdPipes(t *testing.T) {
//cmd := helperCommand(t, "echo", "foo bar", "baz")
cmd := helperCommand("echo", "foo bar", "baz")
defer func() { ExecCommand = exec.Command }()
t.Run("success case", func(t *testing.T) {
o, e, err := cmdPipes(cmd)
t.Run("no error", func(t *testing.T) {
if err != nil {
t.Errorf("error occured but no error expected")
}
})
t.Run("out pipe", func(t *testing.T) {
if o == nil {
t.Errorf("no pipe received")
}
})
t.Run("err pipe", func(t *testing.T) {
if e == nil {
t.Errorf("no pipe received")
}
})
})
}
//based on https://golang.org/src/os/exec/exec_test.go
//this is not directly executed
func TestHelperProcess(*testing.T) {
if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" {
return
}
defer os.Exit(0)
args := os.Args
for len(args) > 0 {
if args[0] == "--" {
args = args[1:]
break
}
args = args[1:]
}
if len(args) == 0 {
fmt.Fprintf(os.Stderr, "No command\n")
os.Exit(2)
}
cmd, args := args[0], args[1:]
switch cmd {
case "/bin/bash":
o, _ := ioutil.ReadAll(os.Stdin)
fmt.Fprintf(os.Stdout, "Stdout: command %v - Stdin: %v\n", cmd, string(o))
fmt.Fprintf(os.Stderr, "Stderr: command %v\n", cmd)
case "echo":
iargs := []interface{}{}
for _, s := range args {
iargs = append(iargs, s)
}
fmt.Println(iargs...)
fmt.Fprintf(os.Stderr, "Stderr: command %v\n", cmd)
default:
fmt.Fprintf(os.Stderr, "Unknown command %q\n", cmd)
os.Exit(2)
}
}

View File

@@ -372,6 +372,5 @@ func TestGetContextDefaults(t *testing.T) {
//no assert since we just want to make sure that no panic occurs
})
})
}

View File

@@ -0,0 +1,312 @@
package main
import (
"bytes"
"fmt"
"io"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"strings"
"text/template"
"github.com/SAP/jenkins-library/pkg/config"
)
type stepInfo struct {
CobraCmdFuncName string
CreateCmdVar string
FlagsFunc string
Long string
Metadata []config.StepParameters
OSImport bool
Short string
StepFunc string
StepName string
}
//StepGoTemplate ...
const stepGoTemplate = `package cmd
import (
//"os"
"github.com/SAP/jenkins-library/pkg/config"
"github.com/spf13/cobra"
)
type {{ .StepName }}Options struct {
{{- range $key, $value := .Metadata }}
{{ $value.Name | golangName }} {{ $value.Type }} ` + "`json:\"{{$value.Name}},omitempty\"`" + `{{end}}
}
var my{{ .StepName | title}}Options {{.StepName}}Options
var {{ .StepName }}StepConfigJSON string
// {{.CobraCmdFuncName}} {{.Short}}
func {{.CobraCmdFuncName}}() *cobra.Command {
metadata := {{ .StepName }}Metadata()
var {{.CreateCmdVar}} = &cobra.Command{
Use: "{{.StepName}}",
Short: "{{.Short}}",
Long: {{ $tick := "` + "`" + `" }}{{ $tick }}{{.Long | longName }}{{ $tick }},
PreRunE: func(cmd *cobra.Command, args []string) error {
return PrepareConfig(cmd, &metadata, "{{ .StepName }}", &my{{ .StepName | title}}Options, openPiperFile)
},
RunE: func(cmd *cobra.Command, args []string) error {
return {{.StepName}}(my{{ .StepName | title }}Options)
},
}
{{.FlagsFunc}}({{.CreateCmdVar}})
return {{.CreateCmdVar}}
}
func {{.FlagsFunc}}(cmd *cobra.Command) {
{{- range $key, $value := .Metadata }}
cmd.Flags().{{ $value.Type | flagType }}(&my{{ $.StepName | title }}Options.{{ $value.Name | golangName }}, "{{ $value.Name }}", {{ $value.Default }}, "{{ $value.Description }}"){{ end }}
{{- printf "\n" }}
{{- range $key, $value := .Metadata }}{{ if $value.Mandatory }}
cmd.MarkFlagRequired("{{ $value.Name }}"){{ end }}{{ end }}
}
// retrieve step metadata
func {{ .StepName }}Metadata() config.StepData {
var theMetaData = config.StepData{
Spec: config.StepSpec{
Inputs: config.StepInputs{
Parameters: []config.StepParameters{
{{- range $key, $value := .Metadata }}
{
Name: "{{ $value.Name }}",
Scope: []string{{ "{" }}{{ range $notused, $scope := $value.Scope }}"{{ $scope }}",{{ end }}{{ "}" }},
Type: "{{ $value.Type }}",
Mandatory: {{ $value.Mandatory }},
},{{ end }}
},
},
},
}
return theMetaData
}
`
//StepTestGoTemplate ...
const stepTestGoTemplate = `package cmd
import (
"testing"
"github.com/stretchr/testify/assert"
)
func Test{{.CobraCmdFuncName}}(t *testing.T) {
testCmd := {{.CobraCmdFuncName}}()
// only high level testing performed - details are tested in step generation procudure
assert.Equal(t, "{{.StepName}}", testCmd.Use, "command name incorrect")
}
`
func main() {
metadataPath := "./resources/metadata"
metadataFiles, err := metadataFiles(metadataPath)
checkError(err)
err = processMetaFiles(metadataFiles, openMetaFile, fileWriter)
checkError(err)
cmd := exec.Command("go", "fmt", "./cmd")
err = cmd.Run()
checkError(err)
}
func processMetaFiles(metadataFiles []string, openFile func(s string) (io.ReadCloser, error), writeFile func(filename string, data []byte, perm os.FileMode) error) error {
for key := range metadataFiles {
var stepData config.StepData
configFilePath := metadataFiles[key]
metadataFile, err := openFile(configFilePath)
checkError(err)
defer metadataFile.Close()
fmt.Printf("Reading file %v\n", configFilePath)
err = stepData.ReadPipelineStepData(metadataFile)
checkError(err)
fmt.Printf("Step name: %v\n", stepData.Metadata.Name)
err = setDefaultParameters(&stepData)
checkError(err)
myStepInfo := getStepInfo(&stepData)
step := stepTemplate(myStepInfo)
err = writeFile(fmt.Sprintf("cmd/%v_generated.go", stepData.Metadata.Name), step, 0644)
checkError(err)
test := stepTestTemplate(myStepInfo)
err = writeFile(fmt.Sprintf("cmd/%v_generated_test.go", stepData.Metadata.Name), test, 0644)
checkError(err)
}
return nil
}
func openMetaFile(name string) (io.ReadCloser, error) {
return os.Open(name)
}
func fileWriter(filename string, data []byte, perm os.FileMode) error {
return ioutil.WriteFile(filename, data, perm)
}
func setDefaultParameters(stepData *config.StepData) error {
//ToDo: custom function for default handling, support all relevant parameter types
for k, param := range stepData.Spec.Inputs.Parameters {
if param.Default == nil {
switch param.Type {
case "string":
param.Default = fmt.Sprintf("os.Getenv(\"PIPER_%v\")", param.Name)
case "bool":
// ToDo: Check if default should be read from env
param.Default = "false"
case "[]string":
// ToDo: Check if default should be read from env
param.Default = "[]string{}"
default:
return fmt.Errorf("Meta data type not set or not known: '%v'", param.Type)
}
} else {
switch param.Type {
case "string":
param.Default = fmt.Sprintf("\"%v\"", param.Default)
case "bool":
boolVal := "false"
if param.Default.(bool) == true {
boolVal = "true"
}
param.Default = boolVal
case "[]string":
param.Default = fmt.Sprintf("[]string{\"%v\"}", strings.Join(param.Default.([]string), "\", \""))
default:
return fmt.Errorf("Meta data type not set or not known: '%v'", param.Type)
}
}
stepData.Spec.Inputs.Parameters[k] = param
}
return nil
}
func getStepInfo(stepData *config.StepData) stepInfo {
return stepInfo{
StepName: stepData.Metadata.Name,
CobraCmdFuncName: fmt.Sprintf("%vCommand", strings.Title(stepData.Metadata.Name)),
CreateCmdVar: fmt.Sprintf("create%vCmd", strings.Title(stepData.Metadata.Name)),
Short: stepData.Metadata.Description,
Long: stepData.Metadata.LongDescription,
Metadata: stepData.Spec.Inputs.Parameters,
FlagsFunc: fmt.Sprintf("add%vFlags", strings.Title(stepData.Metadata.Name)),
}
}
func checkError(err error) {
if err != nil {
fmt.Printf("Error occured: %v\n", err)
os.Exit(1)
}
}
func metadataFiles(sourceDirectory string) ([]string, error) {
var metadataFiles []string
err := filepath.Walk(sourceDirectory, func(path string, info os.FileInfo, err error) error {
if filepath.Ext(path) == ".yaml" {
metadataFiles = append(metadataFiles, path)
}
return nil
})
if err != nil {
return metadataFiles, nil
}
return metadataFiles, nil
}
func stepTemplate(myStepInfo stepInfo) []byte {
funcMap := template.FuncMap{
"flagType": flagType,
"golangName": golangName,
"title": strings.Title,
"longName": longName,
}
tmpl, err := template.New("step").Funcs(funcMap).Parse(stepGoTemplate)
checkError(err)
var generatedCode bytes.Buffer
err = tmpl.Execute(&generatedCode, myStepInfo)
checkError(err)
return generatedCode.Bytes()
}
func stepTestTemplate(myStepInfo stepInfo) []byte {
funcMap := template.FuncMap{
"flagType": flagType,
"golangName": golangName,
"title": strings.Title,
}
tmpl, err := template.New("stepTest").Funcs(funcMap).Parse(stepTestGoTemplate)
checkError(err)
var generatedCode bytes.Buffer
err = tmpl.Execute(&generatedCode, myStepInfo)
checkError(err)
return generatedCode.Bytes()
}
func longName(long string) string {
l := strings.ReplaceAll(long, "`", "` + \"`\" + `")
l = strings.TrimSpace(l)
return l
}
func golangName(name string) string {
properName := strings.Replace(name, "Api", "API", -1)
properName = strings.Replace(properName, "Url", "URL", -1)
properName = strings.Replace(properName, "Id", "ID", -1)
properName = strings.Replace(properName, "Json", "JSON", -1)
properName = strings.Replace(properName, "json", "JSON", -1)
return strings.Title(properName)
}
func flagType(paramType string) string {
var theFlagType string
switch paramType {
case "bool":
theFlagType = "BoolVar"
case "string":
theFlagType = "StringVar"
case "[]string":
theFlagType = "StringSliceVar"
default:
fmt.Printf("Meta data type not set or not known: '%v'\n", paramType)
os.Exit(1)
}
return theFlagType
}

View File

@@ -0,0 +1,227 @@
package main
import (
//"bytes"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"
"testing"
"github.com/SAP/jenkins-library/pkg/config"
"github.com/stretchr/testify/assert"
)
func configOpenFileMock(name string) (io.ReadCloser, error) {
meta1 := `metadata:
name: testStep
description: Test description
longDescription: |
Long Test description
spec:
inputs:
params:
- name: param0
type: string
description: param0 description
default: val0
scope:
- GENERAL
- PARAMETERS
mandatory: true
- name: param1
type: string
description: param1 description
scope:
- PARAMETERS
- name: param2
type: string
description: param1 description
scope:
- PARAMETERS
mandatory: true
`
var r string
switch name {
case "test.yaml":
r = meta1
default:
r = ""
}
return ioutil.NopCloser(strings.NewReader(r)), nil
}
var files map[string][]byte
func writeFileMock(filename string, data []byte, perm os.FileMode) error {
if files == nil {
files = make(map[string][]byte)
}
files[filename] = data
return nil
}
func TestProcessMetaFiles(t *testing.T) {
processMetaFiles([]string{"test.yaml"}, configOpenFileMock, writeFileMock)
t.Run("step code", func(t *testing.T) {
goldenFilePath := filepath.Join("testdata", t.Name()+"_generated.golden")
expected, err := ioutil.ReadFile(goldenFilePath)
if err != nil {
t.Fatalf("failed reading %v", goldenFilePath)
}
assert.Equal(t, expected, files["cmd/testStep_generated.go"])
})
t.Run("test code", func(t *testing.T) {
goldenFilePath := filepath.Join("testdata", t.Name()+"_generated.golden")
expected, err := ioutil.ReadFile(goldenFilePath)
if err != nil {
t.Fatalf("failed reading %v", goldenFilePath)
}
assert.Equal(t, expected, files["cmd/testStep_generated_test.go"])
})
}
func TestSetDefaultParameters(t *testing.T) {
t.Run("success case", func(t *testing.T) {
stepData := config.StepData{
Spec: config.StepSpec{
Inputs: config.StepInputs{
Parameters: []config.StepParameters{
{Name: "param0", Scope: []string{"GENERAL"}, Type: "string", Default: "val0"},
{Name: "param1", Scope: []string{"STEPS"}, Type: "string"},
{Name: "param2", Scope: []string{"STAGES"}, Type: "bool", Default: true},
{Name: "param3", Scope: []string{"PARAMETERS"}, Type: "bool"},
{Name: "param4", Scope: []string{"ENV"}, Type: "[]string", Default: []string{"val4_1", "val4_2"}},
{Name: "param5", Scope: []string{"ENV"}, Type: "[]string"},
},
},
},
}
expected := []string{
"\"val0\"",
"os.Getenv(\"PIPER_param1\")",
"true",
"false",
"[]string{\"val4_1\", \"val4_2\"}",
"[]string{}",
}
err := setDefaultParameters(&stepData)
assert.NoError(t, err, "error occured but none expected")
for k, v := range expected {
assert.Equal(t, v, stepData.Spec.Inputs.Parameters[k].Default, fmt.Sprintf("default not correct for parameter %v", k))
}
})
t.Run("error case", func(t *testing.T) {
stepData := []config.StepData{
{
Spec: config.StepSpec{
Inputs: config.StepInputs{
Parameters: []config.StepParameters{
{Name: "param0", Scope: []string{"GENERAL"}, Type: "int", Default: 10},
{Name: "param1", Scope: []string{"GENERAL"}, Type: "int"},
},
},
},
},
{
Spec: config.StepSpec{
Inputs: config.StepInputs{
Parameters: []config.StepParameters{
{Name: "param1", Scope: []string{"GENERAL"}, Type: "int"},
},
},
},
},
}
for k, v := range stepData {
err := setDefaultParameters(&v)
assert.Error(t, err, fmt.Sprintf("error expected but none occured for parameter %v", k))
}
})
}
func TestGetStepInfo(t *testing.T) {
stepData := config.StepData{
Metadata: config.StepMetadata{
Name: "testStep",
Description: "Test description",
LongDescription: "Long Test description",
},
Spec: config.StepSpec{
Inputs: config.StepInputs{
Parameters: []config.StepParameters{
{Name: "param0", Scope: []string{"GENERAL"}, Type: "string", Default: "test"},
},
},
},
}
myStepInfo := getStepInfo(&stepData)
assert.Equal(t, "testStep", myStepInfo.StepName, "StepName incorrect")
assert.Equal(t, "TestStepCommand", myStepInfo.CobraCmdFuncName, "CobraCmdFuncName incorrect")
assert.Equal(t, "createTestStepCmd", myStepInfo.CreateCmdVar, "CreateCmdVar incorrect")
assert.Equal(t, "Test description", myStepInfo.Short, "Short incorrect")
assert.Equal(t, "Long Test description", myStepInfo.Long, "Long incorrect")
assert.Equal(t, stepData.Spec.Inputs.Parameters, myStepInfo.Metadata, "Metadata incorrect")
assert.Equal(t, "addTestStepFlags", myStepInfo.FlagsFunc, "FlagsFunc incorrect")
}
func TestLongName(t *testing.T) {
tt := []struct {
input string
expected string
}{
{input: "my long name with no ticks", expected: "my long name with no ticks"},
{input: "my long name with `ticks`", expected: "my long name with ` + \"`\" + `ticks` + \"`\" + `"},
}
for k, v := range tt {
assert.Equal(t, v.expected, longName(v.input), fmt.Sprintf("wrong long name for run %v", k))
}
}
func TestGolangName(t *testing.T) {
tt := []struct {
input string
expected string
}{
{input: "testApi", expected: "TestAPI"},
{input: "testUrl", expected: "TestURL"},
{input: "testId", expected: "TestID"},
{input: "testJson", expected: "TestJSON"},
{input: "jsonTest", expected: "JSONTest"},
}
for k, v := range tt {
assert.Equal(t, v.expected, golangName(v.input), fmt.Sprintf("wrong golang name for run %v", k))
}
}
func TestFlagType(t *testing.T) {
tt := []struct {
input string
expected string
}{
{input: "bool", expected: "BoolVar"},
{input: "string", expected: "StringVar"},
{input: "[]string", expected: "StringSliceVar"},
}
for k, v := range tt {
assert.Equal(t, v.expected, flagType(v.input), fmt.Sprintf("wrong flag type for run %v", k))
}
}

View File

@@ -0,0 +1,76 @@
package cmd
import (
//"os"
"github.com/SAP/jenkins-library/pkg/config"
"github.com/spf13/cobra"
)
type testStepOptions struct {
Param0 string `json:"param0,omitempty"`
Param1 string `json:"param1,omitempty"`
Param2 string `json:"param2,omitempty"`
}
var myTestStepOptions testStepOptions
var testStepStepConfigJSON string
// TestStepCommand Test description
func TestStepCommand() *cobra.Command {
metadata := testStepMetadata()
var createTestStepCmd = &cobra.Command{
Use: "testStep",
Short: "Test description",
Long: `Long Test description`,
PreRunE: func(cmd *cobra.Command, args []string) error {
return PrepareConfig(cmd, &metadata, "testStep", &myTestStepOptions, openPiperFile)
},
RunE: func(cmd *cobra.Command, args []string) error {
return testStep(myTestStepOptions)
},
}
addTestStepFlags(createTestStepCmd)
return createTestStepCmd
}
func addTestStepFlags(cmd *cobra.Command) {
cmd.Flags().StringVar(&myTestStepOptions.Param0, "param0", "val0", "param0 description")
cmd.Flags().StringVar(&myTestStepOptions.Param1, "param1", os.Getenv("PIPER_param1"), "param1 description")
cmd.Flags().StringVar(&myTestStepOptions.Param2, "param2", os.Getenv("PIPER_param2"), "param1 description")
cmd.MarkFlagRequired("param0")
cmd.MarkFlagRequired("param2")
}
// retrieve step metadata
func testStepMetadata() config.StepData {
var theMetaData = config.StepData{
Spec: config.StepSpec{
Inputs: config.StepInputs{
Parameters: []config.StepParameters{
{
Name: "param0",
Scope: []string{"GENERAL","PARAMETERS",},
Type: "string",
Mandatory: true,
},
{
Name: "param1",
Scope: []string{"PARAMETERS",},
Type: "string",
Mandatory: false,
},
{
Name: "param2",
Scope: []string{"PARAMETERS",},
Type: "string",
Mandatory: true,
},
},
},
},
}
return theMetaData
}

View File

@@ -0,0 +1,16 @@
package cmd
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestTestStepCommand(t *testing.T) {
testCmd := TestStepCommand()
// only high level testing performed - details are tested in step generation procudure
assert.Equal(t, "testStep", testCmd.Use, "command name incorrect")
}

View File

@@ -0,0 +1,67 @@
metadata:
name: karmaExecuteTests
description: Executes the Karma test runner
longDescription: |
In this step the ([Karma test runner](http://karma-runner.github.io)) is executed.
The step is using the `seleniumExecuteTest` step to spin up two containers in a Docker network:
* a Selenium/Chrome container (`selenium/standalone-chrome`)
* a NodeJS container (`node:8-stretch`)
In the Docker network, the containers can be referenced by the values provided in `dockerName` and `sidecarName`, the default values are `karma` and `selenium`. These values must be used in the `hostname` properties of the test configuration ([Karma](https://karma-runner.github.io/1.0/config/configuration-file.html) and [WebDriver](https://github.com/karma-runner/karma-webdriver-launcher#usage)).
!!! note
In a Kubernetes environment, the containers both need to be referenced with `localhost`.
spec:
inputs:
resources:
- name: buildDescriptor
type: stash
- name: tests
type: stash
params:
- name: installCommand
type: string
description: The command that is executed to install the test tool.
default: npm install --quiet
scope:
- GENERAL
- PARAMETERS
- STAGES
- STEPS
mandatory: true
- name: modulePath
type: string
description: Define the path of the module to execute tests on.
default: '.'
scope:
- PARAMETERS
- STAGES
- STEPS
mandatory: true
- name: runCommand
type: string
description: The command that is executed to start the tests.
default: npm run karma
scope:
- GENERAL
- PARAMETERS
- STAGES
- STEPS
mandatory: true
#outputs:
containers:
- name: maven
image: maven:3.5-jdk-8
volumeMounts:
- mountPath: /dev/shm
name: dev-shm
sidecars:
- image: selenium/standalone-chrome
name: selenium
securityContext:
privileged: true
volumeMounts:
- mountPath: /dev/shm
name: dev-shm