You've already forked sap-jenkins-library
							
							
				mirror of
				https://github.com/SAP/jenkins-library.git
				synced 2025-10-30 23:57:50 +02:00 
			
		
		
		
	Add golang implementation for karma tests (#919)
* Provide golang based karma step
This commit is contained in:
		| @@ -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 | ||||
|   | ||||
| @@ -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
									
								
							
							
						
						
									
										6
									
								
								cmd/interfaces.go
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										35
									
								
								cmd/karmaExecuteTests.go
									
									
									
									
									
										Normal 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, " ") | ||||
| } | ||||
							
								
								
									
										87
									
								
								cmd/karmaExecuteTests_generated.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								cmd/karmaExecuteTests_generated.go
									
									
									
									
									
										Normal 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 | ||||
| } | ||||
							
								
								
									
										16
									
								
								cmd/karmaExecuteTests_generated_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								cmd/karmaExecuteTests_generated_test.go
									
									
									
									
									
										Normal 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") | ||||
|  | ||||
| } | ||||
							
								
								
									
										65
									
								
								cmd/karmaExecuteTests_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								cmd/karmaExecuteTests_test.go
									
									
									
									
									
										Normal 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") | ||||
| 	}) | ||||
| } | ||||
							
								
								
									
										55
									
								
								cmd/piper.go
									
									
									
									
									
								
							
							
						
						
									
										55
									
								
								cmd/piper.go
									
									
									
									
									
								
							| @@ -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
									
								
							
							
						
						
									
										110
									
								
								cmd/piper_test.go
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										134
									
								
								pkg/command/command.go
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										182
									
								
								pkg/command/command_test.go
									
									
									
									
									
										Normal 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) | ||||
|  | ||||
| 	} | ||||
| } | ||||
| @@ -372,6 +372,5 @@ func TestGetContextDefaults(t *testing.T) { | ||||
|  | ||||
| 			//no assert since we just want to make sure that no panic occurs | ||||
| 		}) | ||||
|  | ||||
| 	}) | ||||
| } | ||||
|   | ||||
							
								
								
									
										312
									
								
								pkg/generator/step-metadata.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										312
									
								
								pkg/generator/step-metadata.go
									
									
									
									
									
										Normal 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 | ||||
| } | ||||
							
								
								
									
										227
									
								
								pkg/generator/step-metadata_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										227
									
								
								pkg/generator/step-metadata_test.go
									
									
									
									
									
										Normal 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)) | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										76
									
								
								pkg/generator/testdata/TestProcessMetaFiles/step_code_generated.golden
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								pkg/generator/testdata/TestProcessMetaFiles/step_code_generated.golden
									
									
									
									
										vendored
									
									
										Normal 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 | ||||
| } | ||||
							
								
								
									
										16
									
								
								pkg/generator/testdata/TestProcessMetaFiles/test_code_generated.golden
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								pkg/generator/testdata/TestProcessMetaFiles/test_code_generated.golden
									
									
									
									
										vendored
									
									
										Normal 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") | ||||
|  | ||||
| } | ||||
							
								
								
									
										67
									
								
								resources/metadata/karma.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								resources/metadata/karma.yaml
									
									
									
									
									
										Normal 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 | ||||
		Reference in New Issue
	
	Block a user