diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index f1e9664d..fac2bcfe 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -33,14 +33,14 @@ jobs: - name: Build Binary run: make bin - - name: Run Go Tests - run: make test - - name: Check format run: | make gofmt if [[ ! -z $(git status -s) ]] then - echo "not well formatted sources are found" + echo "not well formatted sources are found : $(git status -s)" exit 1 fi + + - name: Run Go Tests + run: make test diff --git a/.gitignore b/.gitignore index 83bf67aa..2e12ef21 100644 --- a/.gitignore +++ b/.gitignore @@ -18,4 +18,7 @@ main # Files used for debugging .vscode/ +# File created running tests +tests/tmp/ + .DS_Store diff --git a/go.mod b/go.mod index 7537dcf1..7d41e82c 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/fatih/color v1.7.0 github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32 github.com/gobwas/glob v0.2.3 + github.com/google/go-cmp v0.4.0 github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348 github.com/mattn/go-colorable v0.1.2 // indirect diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 00000000..9f51e59f --- /dev/null +++ b/tests/README.md @@ -0,0 +1,235 @@ +# Devfile Parser Library Tests + +## About + +The tests use the go language and are intended to test every apsect of the parser for every schema attribute. Some basic aspects of the tests: + +* A first test (parser_v200_schema_test.go) feeds pre-created devfiles to the parser to ensure the parser can parse all attribues and return an appropriate error when the devfile contains an error. This test is not currently available. +* A second set of tests (parser_v200_verify_test.go) create devfile content at runtime: + * Devfile content is randomly generated and as a result the tests are designed to run multiple times. + * Parser functions covered: + * Read an existing devfile. + * Write a new devfile. + * Modify content of a devfile. + * Multi-threaded access to the parser. + * The tests use the devfile schema to create a structure containing expected content for a devfile. These structures are compared with those returned by the parser. + * sigs.k8s.io/yaml is used to write out devfiles. + * github.com/google/go-cmp/cmp is used to compare structures. + +## Current tests: + +The tests using pre-created devfiles are not currently available (update in progress due to schema changes) + +The tests which generate devfiles with random content at run time currently cover the following properties and items. + +* Commands: + * Exec + * Composite +* Components + * Container + * Volume + +## Running the tests + +1. Go to directory tests/src/tests +1. Run ```go test``` or ```go test -v``` +1. The test creates the following files: + 1. ```./tmp/test.log``` contains log output from the tests. + 1. ```./tmp/parser_v200_verify_test/Test_*.yaml``` are the devfiles which are randomly generated at runtime. The file name matches the name of the test function which resulted in them being created. + 1. ```./tmp/parser_v200_schema_test/*.yaml``` are the pre-created devfiles. + 1. If a test detects an error when compariing properties returned by the parser with expected properties + * ```./tmp/parser_v200_schema_test/Test_*__Parser.yaml``` - property as returned by the parser + * ```./tmp/parser_v200_schema_test/Test_*__Test.yaml``` - property as expected by the test + +Note: each run of the test removes the existing conents of the ```./tmp``` directory + +## Anatomy of parser_v200_verify_test.go test + +Each test in ```parser_v200_verify_test.go``` sets values in a test structure which defines the test to run (additions will new made for new properties as support is added): + + type TestContent struct { + CommandTypes []schema.CommandType + ComponentTypes []schema.ComponentType + FileName string + CreateWithParser bool + EditContent bool + } + + +The test then uses one (or both) of two functions to run a test + +* For a single thread test: + * ```func runTest(testContent TestContent, t *testing.T)``` +* For a multi-thread test: + * ```func runMultiThreadTest(testContent TestContent, t *testing.T)``` + +An example test: + + func Test_MultiCommand(t *testing.T) { + testContent := TestContent{} + testContent.CommandTypes = []schema.CommandType{schema.ExecCommandType, schema.CompositeCommandType} + testContent.CreateWithParser = true + testContent.EditContent = true + testContent.FileName = GetDevFileName() + runTest(testContent, t) + runMultiThreadTest(testContent, t) + } + +Note: ```GetDevFileName()``` is a function which returns the name of a temporary file to create which uses the name of the test file as a subdirectory and the name of test function function as file name. In this example it returns ```./tmp/parser_v200_verify_test/Test_MultiCommand``` + +There are also some constants which control execution of the tests: + + const numThreads = 5 // Number of threads used by multi-thread tests + const maxCommands = 10 // The maximum number of commands to include in a generated devfile + const maxComponents = 10 // The maximum number of components to include in a generated devfile + +## Basic principles of the tests which randomly generates devfiles + +* Each devfile is created in a schema structure. +* Which attributes are set and the values used are randomized. + * For example, the number of commands included in a devfile is randomly generated. + * For example, attribute values are set to randomized strings, numbers or binary. + * For example, a particular optional attribute has a 50% chance of being uncluded in a devfiles. + * Repeated tests give more variety and wider coverage. +* Once the schema structure is complete it is written in one of two ways. + * using the sigs.k8s.io/yaml. + * using the parser. +* Once the devfile is created on disk the parser is used to read and validate it. +* If editing the devfile + * each object is retrieved, modified and written back to the parser + * the parser is used to write the devfile to disk + * the parser is then used to read and validate the modified devfile. +* Each array of objects in then devfile are then retrieved from the parser and compared. If this fails: + * Each object returned by the parser is compared to the equivalent object tracked in the test. + * if the obejcts do not match the test fails + * Files are output with the content of each object. + * If the parser returns more or fewer objects than expected, the test fails. + +## Updating tests + +### Files +* ```parser_v200_verify_test.go``` contains the tests +* ```test-utils.go``` provides property agnostic functions for use by the tests and other utils +* ```-test-utils.go``` for example ```command-test-utils.go```, provides property related functions + +### Adding, modifying attributes of existing properties. + +In the ```-test-utils.go``` files there are: +* ```setValues``` functions. + * for example in ```command-test-utils.go``` : + * ```func setExecCommandValues(execCommand *schema.ExecCommand)``` + * ```func setCompositeCommandValues(compositeCommand *schema.CompositeCommand)``` +* These may use utility functions to set more complex attributes. +* Modify these functions to add/modify test for new/changed attributes. + +### Add new item to an existing property. + +For example add support for apply command to existing command support: + +1. In ```command-test-utils.go``` + * add functions: + * ```func setApplyCommandValues(applyCommand *schema.ApplyCommand)``` + * randomly set attribute values in the provided apply command object + * ```func createApplyCommand() *schema.ApplyCommand``` + * creates the apply command object and calls setApplyCommandValues to add attribute values + * follow the implementation of other similar functions. + * modify: + * ```func generateCommand(command *schema.Command, genericCommand *GenericCommand)``` + * add logic to call createApplyCommand if commandType indicates such. + * ```func (devfile *TestDevfile) UpdateCommand(command *schema.Command) error``` + * add logic to call setApplyCommandValues if commandType indicates such. +1. In ```parser_v200_verify_test.go``` + * add new tests. for example: + * Test_ApplyCommand - CreateWithParser set to false, EditContent set to false + * Test_CreateApplyCommand - CreateWithParser set to true, EditContent set to false + * Test_EditApplyCommand - CreateWithParser set to false, EditContent set to true + * Test_CreateEditApplyCommand - CreateWithParser set to true, EditContent set to true + * modify existing test to include Apply commands + * Test_MultiCommand + * Test_Everything + +### Add new property + +Using existing support for commands as an illustration, any new property support added should follow the same structure: + +1. ```command-test-utils.go```: + * Specific to commands + * Commands require support for 5 different command types: + * Exec + * Appy (to be implemented) + * Composite + * VSCodeLaunch (to be implemented) + * VSCodeTask (to be implemented) + * Each of these command-types have equivalent functions: + * ```func createCommand() *schema.``` + * creates the command object and calls ```setCommandValues``` to add attribute values + * for example see: ```func createExecCommand(execCommand *schema.ExecCommand)``` + * ```func setCommandValues(project-sourceProject *schema.)``` + * sets random attributes into the provided object + * for example see: ```func setExecCommandValues(execCommand *schema.ExecCommand)``` + * Functions general to all commands + * ```func generateCommand(command *schema.Command, genericCommand *GenericCommand)``` + * includes logic to call the ```createCommand``` function for the command-Type of the supplied command object. + * ```func (devfile *TestDevfile) addCommand(commandType schema.CommandType) string``` + * main entry point for a test to add a command + * maintains the array of commands in the schema structure + * calls generateCommand() + * ```func (devfile *TestDevfile) UpdateCommand(command *schema.Command) error``` + * includes logic to call setCommandValues for each commandType. + * ```func (devfile TestDevfile) VerifyCommands(parserCommands []schema.Command) error``` + * includes logic to compare the array of commands obtained from the parser with those created by the test. if the compare fails: + * each individual command is compared. + * if a command compare fails, the parser version and test version of the command are oputput as yaml files to the tmp directory + * a check is made to determine if the parser returned a command not known to the test or the pasrer omitted a command expected by the test. +1. ```test-utils.go``` + * ```func (devfile TestDevfile) Verify()``` + * includes code to get object from the paser and verify their content. + * for commands code is required to: + 1. Retrieve each command from the parser + 1. Use command Id to obtain the GenericCommand object which matches + 1. Compare the command structure returned by the parser with the command structure saved in the GenericCommand object. + * ```func (devfile TestDevfile) EditCommands() error``` + * specific to command objects. + 1. Ensure devfile is written to disk + 1. Use parser to read devfile and get all command object + 1. For each command call: + * ```func (devfile *TestDevfile) UpdateCommand(command *schema.Command) error``` + 1. When all commands have been updated, use parser to write the updated devfile to disk +1. ```parser-v200-test.go``` + * ```type TestContent struct``` + * includes an array of command types: ```CommandTypes []schema.CommandType``` + * ```func Test_ExecCommand(t *testing.T)``` + 1. Creates a TestContent object + 1. Adds a single entry array containg schema.ExecCommandType to the array of command types + 1. Calls runTest for a single thread test + 1. Calls runMultiThreadTest for a multi-thread test. + * See also + * ```func Test_ExecCommand(t *testing.T)``` + * ```func Test_MultiCommand(t *testing.T)``` + * ```func Test_Everything(t *testing.T)``` + * Add logic to ```func runTest(testContent TestContent, t *testing.T)``` + 1. Add commands to the test. + 2. Start edits of commands if required. + + +#### Code flow + +Create, modify and verify an exec command: +1. parser_v200_verify_test.Test_ExecCommand + 1. parser-v200-test.runTest + 1. command-test-utils.AddCommand + 1. command-test-utils.GenerateCommand + 1. command-test-utils.createExecCommand + 1. command-test-utils.setExecCommandValues + 1. test-utils.CreateDevfile + 1. test-utils.EditCommands + 1. command-test-utils.UpdateCommand + 1. command-test-utils.setExecCommandValues + 1. test-utils.Verify + 1. command-test-utils.VerifyCommands + + + + + + diff --git a/tests/api/parser_v200_verify_test.go b/tests/api/parser_v200_verify_test.go new file mode 100644 index 00000000..9275642f --- /dev/null +++ b/tests/api/parser_v200_verify_test.go @@ -0,0 +1,288 @@ +package api + +import ( + "fmt" + "strconv" + "testing" + "time" + + "github.com/devfile/library/tests/utils" + + schema "github.com/devfile/api/pkg/apis/workspaces/v1alpha2" +) + +const ( + // numThreads : Number of threads used by multi-thread tests + numThreads = 5 + // maxCommands : The maximum number of commands to include in a generated devfile + maxCommands = 10 + // maxComponents : The maximum number of components to include in a generated devfile + maxComponents = 10 +) + +// TestContent - structure used by a test to configure the tests to run +type TestContent struct { + CommandTypes []schema.CommandType + ComponentTypes []schema.ComponentType + FileName string + CreateWithParser bool + EditContent bool +} + +func Test_ExecCommand(t *testing.T) { + testContent := TestContent{} + testContent.CommandTypes = []schema.CommandType{schema.ExecCommandType} + testContent.CreateWithParser = false + testContent.EditContent = false + testContent.FileName = utils.GetDevFileName() + runTest(testContent, t) + runMultiThreadTest(testContent, t) +} +func Test_ExecCommandEdit(t *testing.T) { + testContent := TestContent{} + testContent.CommandTypes = []schema.CommandType{schema.ExecCommandType} + testContent.CreateWithParser = false + testContent.EditContent = true + testContent.FileName = utils.GetDevFileName() + runTest(testContent, t) + runMultiThreadTest(testContent, t) +} + +func Test_ExecCommandParserCreate(t *testing.T) { + testContent := TestContent{} + testContent.CommandTypes = []schema.CommandType{schema.ExecCommandType} + testContent.CreateWithParser = true + testContent.EditContent = false + testContent.FileName = utils.GetDevFileName() + runTest(testContent, t) + runMultiThreadTest(testContent, t) +} + +func Test_ExecCommandEditParserCreate(t *testing.T) { + testContent := TestContent{} + testContent.CommandTypes = []schema.CommandType{schema.ExecCommandType} + testContent.CreateWithParser = true + testContent.EditContent = true + testContent.FileName = utils.GetDevFileName() + runTest(testContent, t) + runMultiThreadTest(testContent, t) +} + +func Test_CompositeCommand(t *testing.T) { + testContent := TestContent{} + testContent.CommandTypes = []schema.CommandType{schema.CompositeCommandType} + testContent.CreateWithParser = false + testContent.EditContent = false + testContent.FileName = utils.GetDevFileName() + runTest(testContent, t) + runMultiThreadTest(testContent, t) +} +func Test_CompositeCommandEdit(t *testing.T) { + testContent := TestContent{} + testContent.CommandTypes = []schema.CommandType{schema.CompositeCommandType} + testContent.CreateWithParser = false + testContent.EditContent = true + testContent.FileName = utils.GetDevFileName() + runTest(testContent, t) + runMultiThreadTest(testContent, t) +} + +func Test_CompositeCommandParserCreate(t *testing.T) { + testContent := TestContent{} + testContent.CommandTypes = []schema.CommandType{schema.CompositeCommandType} + testContent.CreateWithParser = true + testContent.EditContent = false + testContent.FileName = utils.GetDevFileName() + runTest(testContent, t) + runMultiThreadTest(testContent, t) +} + +func Test_CompositeCommandEditParserCreate(t *testing.T) { + testContent := TestContent{} + testContent.CommandTypes = []schema.CommandType{schema.CompositeCommandType} + testContent.CreateWithParser = true + testContent.EditContent = true + testContent.FileName = utils.GetDevFileName() + runTest(testContent, t) + runMultiThreadTest(testContent, t) +} + +func Test_MultiCommand(t *testing.T) { + testContent := TestContent{} + testContent.CommandTypes = []schema.CommandType{schema.ExecCommandType, schema.CompositeCommandType} + testContent.CreateWithParser = true + testContent.EditContent = true + testContent.FileName = utils.GetDevFileName() + runTest(testContent, t) + runMultiThreadTest(testContent, t) +} + +func Test_ContainerComponent(t *testing.T) { + testContent := TestContent{} + testContent.ComponentTypes = []schema.ComponentType{schema.ContainerComponentType} + testContent.CreateWithParser = false + testContent.EditContent = false + testContent.FileName = utils.GetDevFileName() + runTest(testContent, t) + runMultiThreadTest(testContent, t) +} + +func Test_ContainerComponentEdit(t *testing.T) { + testContent := TestContent{} + testContent.ComponentTypes = []schema.ComponentType{schema.ContainerComponentType} + testContent.CreateWithParser = false + testContent.EditContent = true + testContent.FileName = utils.GetDevFileName() + runTest(testContent, t) + runMultiThreadTest(testContent, t) +} + +func Test_ContainerComponentCreateWithParser(t *testing.T) { + testContent := TestContent{} + testContent.ComponentTypes = []schema.ComponentType{schema.ContainerComponentType} + testContent.CreateWithParser = true + testContent.EditContent = false + testContent.FileName = utils.GetDevFileName() + runTest(testContent, t) + runMultiThreadTest(testContent, t) +} + +func Test_ContainerComponentEditCreateWithParser(t *testing.T) { + testContent := TestContent{} + testContent.ComponentTypes = []schema.ComponentType{schema.ContainerComponentType} + testContent.CreateWithParser = true + testContent.EditContent = true + testContent.FileName = utils.GetDevFileName() + runTest(testContent, t) + runMultiThreadTest(testContent, t) +} + +func Test_VolumeComponent(t *testing.T) { + testContent := TestContent{} + testContent.ComponentTypes = []schema.ComponentType{schema.VolumeComponentType} + testContent.CreateWithParser = false + testContent.EditContent = false + testContent.FileName = utils.GetDevFileName() + runTest(testContent, t) + runMultiThreadTest(testContent, t) +} + +func Test_VolumeComponentEdit(t *testing.T) { + testContent := TestContent{} + testContent.ComponentTypes = []schema.ComponentType{schema.VolumeComponentType} + testContent.CreateWithParser = false + testContent.EditContent = true + testContent.FileName = utils.GetDevFileName() + runTest(testContent, t) + runMultiThreadTest(testContent, t) +} + +func Test_VolumeComponentCreateWithParser(t *testing.T) { + testContent := TestContent{} + testContent.ComponentTypes = []schema.ComponentType{schema.VolumeComponentType} + testContent.CreateWithParser = true + testContent.EditContent = false + testContent.FileName = utils.GetDevFileName() + runTest(testContent, t) + runMultiThreadTest(testContent, t) +} + +func Test_VolumeComponentEditCreateWithParser(t *testing.T) { + testContent := TestContent{} + testContent.ComponentTypes = []schema.ComponentType{schema.VolumeComponentType} + testContent.CreateWithParser = true + testContent.EditContent = true + testContent.FileName = utils.GetDevFileName() + runTest(testContent, t) + runMultiThreadTest(testContent, t) +} + +func Test_MultiComponent(t *testing.T) { + testContent := TestContent{} + testContent.ComponentTypes = []schema.ComponentType{schema.ContainerComponentType, schema.VolumeComponentType} + testContent.CreateWithParser = true + testContent.EditContent = true + testContent.FileName = utils.GetDevFileName() + runTest(testContent, t) + runMultiThreadTest(testContent, t) +} + +func Test_Everything(t *testing.T) { + testContent := TestContent{} + testContent.CommandTypes = []schema.CommandType{schema.ExecCommandType, schema.CompositeCommandType} + testContent.ComponentTypes = []schema.ComponentType{schema.ContainerComponentType, schema.VolumeComponentType} + testContent.CreateWithParser = true + testContent.EditContent = true + testContent.FileName = utils.GetDevFileName() + runTest(testContent, t) + runMultiThreadTest(testContent, t) +} + +// runMultiThreadTest : Runs the same test on multiple threads, the test is based on the content of the specified TestContent +func runMultiThreadTest(testContent TestContent, t *testing.T) { + + utils.LogMessage(fmt.Sprintf("Start Threaded test for %s", testContent.FileName)) + + devfileName := testContent.FileName + var i int + for i = 1; i < numThreads; i++ { + testContent.FileName = utils.AddSuffixToFileName(devfileName, strconv.Itoa(i)) + go runTest(testContent, t) + } + testContent.FileName = utils.AddSuffixToFileName(devfileName, strconv.Itoa(i)) + runTest(testContent, t) + + utils.LogMessage(fmt.Sprintf("Sleep 2 seconds to allow all threads to complete : %s", devfileName)) + time.Sleep(2 * time.Second) + utils.LogMessage(fmt.Sprintf("Sleep complete : %s", devfileName)) + +} + +// runTest : Runs a test beased on the content of the specified TestContent +func runTest(testContent TestContent, t *testing.T) { + + utils.LogMessage(fmt.Sprintf("Start test for %s", testContent.FileName)) + testDevfile := utils.GetDevfile(testContent.FileName) + + if len(testContent.CommandTypes) > 0 { + numCommands := utils.GetRandomNumber(maxCommands) + for i := 0; i < numCommands; i++ { + commandIndex := utils.GetRandomNumber(len(testContent.CommandTypes)) + testDevfile.AddCommand(testContent.CommandTypes[commandIndex-1]) + } + } + + if len(testContent.ComponentTypes) > 0 { + numComponents := utils.GetRandomNumber(maxComponents) + for i := 0; i < numComponents; i++ { + componentIndex := utils.GetRandomNumber(len(testContent.ComponentTypes)) + testDevfile.AddComponent(testContent.ComponentTypes[componentIndex-1]) + } + } + + err := testDevfile.CreateDevfile(testContent.CreateWithParser) + if err != nil { + t.Fatalf(utils.LogErrorMessage(fmt.Sprintf("ERROR creating devfile : %s : %v", testContent.FileName, err))) + } + + if testContent.EditContent { + if len(testContent.CommandTypes) > 0 { + err = testDevfile.EditCommands() + if err != nil { + t.Fatalf(utils.LogErrorMessage(fmt.Sprintf("ERROR editing commands : %s : %v", testContent.FileName, err))) + } + } + if len(testContent.ComponentTypes) > 0 { + err = testDevfile.EditComponents() + if err != nil { + t.Fatalf(utils.LogErrorMessage(fmt.Sprintf("ERROR editing components : %s : %v", testContent.FileName, err))) + } + } + } + + err = testDevfile.Verify() + if err != nil { + t.Fatalf(utils.LogErrorMessage(fmt.Sprintf("ERROR verifying devfile content : %s : %v", testContent.FileName, err))) + } + +} diff --git a/tests/utils/command_test_utils.go b/tests/utils/command_test_utils.go new file mode 100644 index 00000000..23d6400a --- /dev/null +++ b/tests/utils/command_test_utils.go @@ -0,0 +1,253 @@ +package utils + +import ( + "errors" + "fmt" + "io/ioutil" + + "github.com/google/go-cmp/cmp" + "sigs.k8s.io/yaml" + + schema "github.com/devfile/api/pkg/apis/workspaces/v1alpha2" +) + +// addEnv creates and returns a specifed number of env attributes in a schema structure +func addEnv(numEnv int) []schema.EnvVar { + commandEnvs := make([]schema.EnvVar, numEnv) + for i := 0; i < numEnv; i++ { + commandEnvs[i].Name = "Name_" + GetRandomString(5, false) + commandEnvs[i].Value = "Value_" + GetRandomString(5, false) + LogInfoMessage(fmt.Sprintf("Add Env: %s", commandEnvs[i])) + } + return commandEnvs +} + +// addAttributes creates returns a specifed number of attributes in a schema structure +func addAttributes(numAtrributes int) map[string]string { + attributes := make(map[string]string) + for i := 0; i < numAtrributes; i++ { + AttributeName := "Name_" + GetRandomString(6, false) + attributes[AttributeName] = "Value_" + GetRandomString(6, false) + LogInfoMessage(fmt.Sprintf("Add attribute : %s = %s", AttributeName, attributes[AttributeName])) + } + return attributes +} + +// addGroup creates and returns a group in a schema structure +func addGroup() *schema.CommandGroup { + + commandGroup := schema.CommandGroup{} + commandGroup.Kind = GetRandomGroupKind() + LogInfoMessage(fmt.Sprintf("group Kind: %s", commandGroup.Kind)) + commandGroup.IsDefault = GetBinaryDecision() + LogInfoMessage(fmt.Sprintf("group isDefault: %t", commandGroup.IsDefault)) + return &commandGroup +} + +// AddCommand adds a command of the specified type, with random attributes, to the devfile schema +func (devfile *TestDevfile) AddCommand(commandType schema.CommandType) string { + command := generateCommand(commandType) + devfile.SchemaDevFile.Commands = append(devfile.SchemaDevFile.Commands, command) + return command.Id +} + +// generateCommand creates a command of a specified type in a schema structure +func generateCommand(commandType schema.CommandType) schema.Command { + command := schema.Command{} + command.Id = GetRandomUniqueString(8, true) + LogInfoMessage(fmt.Sprintf("command Id: %s", command.Id)) + + if commandType == schema.ExecCommandType { + command.Exec = createExecCommand() + } else if commandType == schema.CompositeCommandType { + command.Composite = createCompositeCommand() + } + return command +} + +// UpdateCommand randomly updates attribute values of a specified command in the devfile schema +func (devfile *TestDevfile) UpdateCommand(parserCommand *schema.Command) error { + + var err error + testCommand, found := getSchemaCommand(devfile.SchemaDevFile.Commands, parserCommand.Id) + if found { + LogInfoMessage(fmt.Sprintf("Updating command id: %s", parserCommand.Id)) + if testCommand.Exec != nil { + setExecCommandValues(parserCommand.Exec) + } else if testCommand.Composite != nil { + setCompositeCommandValues(parserCommand.Composite) + } + devfile.replaceSchemaCommand(*parserCommand) + } else { + err = errors.New(LogErrorMessage(fmt.Sprintf("Command not found in test : %s", parserCommand.Id))) + } + return err +} + +// createExecCommand creates and returns an exec command in a schema structure +func createExecCommand() *schema.ExecCommand { + + LogInfoMessage("Create an exec command :") + execCommand := schema.ExecCommand{} + setExecCommandValues(&execCommand) + return &execCommand + +} + +// setExecCommandValues randomly sets exec command attribute to random values +func setExecCommandValues(execCommand *schema.ExecCommand) { + + execCommand.Component = GetRandomString(8, false) + LogInfoMessage(fmt.Sprintf("....... component: %s", execCommand.Component)) + + execCommand.CommandLine = GetRandomString(4, false) + " " + GetRandomString(4, false) + LogInfoMessage(fmt.Sprintf("....... commandLine: %s", execCommand.CommandLine)) + + if GetRandomDecision(2, 1) { + execCommand.Group = addGroup() + } else { + execCommand.Group = nil + } + + if GetBinaryDecision() { + execCommand.Label = GetRandomString(12, false) + LogInfoMessage(fmt.Sprintf("....... label: %s", execCommand.Label)) + } else { + execCommand.Label = "" + } + + if GetBinaryDecision() { + execCommand.WorkingDir = "./tmp" + LogInfoMessage(fmt.Sprintf("....... WorkingDir: %s", execCommand.WorkingDir)) + } else { + execCommand.WorkingDir = "" + } + + execCommand.HotReloadCapable = GetBinaryDecision() + LogInfoMessage(fmt.Sprintf("....... HotReloadCapable: %t", execCommand.HotReloadCapable)) + + if GetBinaryDecision() { + execCommand.Env = addEnv(GetRandomNumber(4)) + } else { + execCommand.Env = nil + } + +} + +// replaceSchemaCommand uses the specified command to replace the command in the schema structure with the same Id. +func (devfile TestDevfile) replaceSchemaCommand(command schema.Command) { + for i := 0; i < len(devfile.SchemaDevFile.Commands); i++ { + if devfile.SchemaDevFile.Commands[i].Id == command.Id { + devfile.SchemaDevFile.Commands[i] = command + break + } + } +} + +// getSchemaCommand get a command from the devfile schema structure +func getSchemaCommand(commands []schema.Command, id string) (*schema.Command, bool) { + found := false + var schemaCommand schema.Command + for _, command := range commands { + if command.Id == id { + schemaCommand = command + found = true + break + } + } + return &schemaCommand, found +} + +// createCompositeCommand creates a composite command in a schema structure +func createCompositeCommand() *schema.CompositeCommand { + + LogInfoMessage("Create a composite command :") + compositeCommand := schema.CompositeCommand{} + setCompositeCommandValues(&compositeCommand) + return &compositeCommand +} + +// setCompositeCommandValues randomly sets composite command attribute to random values +func setCompositeCommandValues(compositeCommand *schema.CompositeCommand) { + numCommands := GetRandomNumber(3) + + compositeCommand.Commands = make([]string, numCommands) + for i := 0; i < numCommands; i++ { + compositeCommand.Commands[i] = GetRandomUniqueString(8, false) + LogInfoMessage(fmt.Sprintf("....... command %d of %d : %s", i, numCommands, compositeCommand.Commands[i])) + } + + if GetRandomDecision(2, 1) { + compositeCommand.Group = addGroup() + } + + if GetBinaryDecision() { + compositeCommand.Label = GetRandomString(12, false) + LogInfoMessage(fmt.Sprintf("....... label: %s", compositeCommand.Label)) + } + + if GetBinaryDecision() { + compositeCommand.Parallel = true + LogInfoMessage(fmt.Sprintf("....... Parallel: %t", compositeCommand.Parallel)) + } +} + +// VerifyCommands verifies commands returned by the parser are the same as those saved in the devfile schema +func (devfile TestDevfile) VerifyCommands(parserCommands []schema.Command) error { + + LogInfoMessage("Enter VerifyCommands") + var errorString []string + + // Compare entire array of commands + if !cmp.Equal(parserCommands, devfile.SchemaDevFile.Commands) { + errorString = append(errorString, LogErrorMessage(fmt.Sprintf("Command array compare failed."))) + // Array compare failed. Narrow down by comparing indivdual commands + for _, command := range parserCommands { + if testCommand, found := getSchemaCommand(devfile.SchemaDevFile.Commands, command.Id); found { + if !cmp.Equal(command, *testCommand) { + parserFilename := AddSuffixToFileName(devfile.FileName, "_"+command.Id+"_Parser") + testFilename := AddSuffixToFileName(devfile.FileName, "_"+command.Id+"_Test") + LogInfoMessage(fmt.Sprintf(".......marshall and write devfile %s", devfile.FileName)) + c, err := yaml.Marshal(command) + if err != nil { + errorString = append(errorString, LogErrorMessage(fmt.Sprintf(".......marshall devfile %s", parserFilename))) + } else { + err = ioutil.WriteFile(parserFilename, c, 0644) + if err != nil { + errorString = append(errorString, LogErrorMessage(fmt.Sprintf(".......write devfile %s", parserFilename))) + } + } + LogInfoMessage(fmt.Sprintf(".......marshall and write devfile %s", testFilename)) + c, err = yaml.Marshal(testCommand) + if err != nil { + errorString = append(errorString, LogErrorMessage(fmt.Sprintf(".......marshall devfile %s", testFilename))) + } else { + err = ioutil.WriteFile(testFilename, c, 0644) + if err != nil { + errorString = append(errorString, LogErrorMessage(fmt.Sprintf(".......write devfile %s", testFilename))) + } + } + errorString = append(errorString, LogInfoMessage(fmt.Sprintf("Command %s did not match, see files : %s and %s", command.Id, parserFilename, testFilename))) + } else { + LogInfoMessage(fmt.Sprintf(" --> Command matched : %s", command.Id)) + } + } else { + errorString = append(errorString, LogErrorMessage(fmt.Sprintf("Command from parser not known to test - id : %s ", command.Id))) + } + + } + for _, command := range devfile.SchemaDevFile.Commands { + if _, found := getSchemaCommand(parserCommands, command.Id); !found { + errorString = append(errorString, LogErrorMessage(fmt.Sprintf("Command from test not returned by parser : %s ", command.Id))) + } + } + } else { + LogInfoMessage(fmt.Sprintf(" --> Command structures matched")) + } + + var err error + if len(errorString) > 0 { + err = errors.New(fmt.Sprint(errorString)) + } + return err +} diff --git a/tests/utils/component_test_utils.go b/tests/utils/component_test_utils.go new file mode 100644 index 00000000..43440f13 --- /dev/null +++ b/tests/utils/component_test_utils.go @@ -0,0 +1,233 @@ +package utils + +import ( + "errors" + "fmt" + "io/ioutil" + "strconv" + + schema "github.com/devfile/api/pkg/apis/workspaces/v1alpha2" + "github.com/google/go-cmp/cmp" + "sigs.k8s.io/yaml" +) + +// addVolume returns volumeMounts in a schema structure based on a specified number of volumes +func addVolume(numVols int) []schema.VolumeMount { + commandVols := make([]schema.VolumeMount, numVols) + for i := 0; i < numVols; i++ { + commandVols[i].Name = "Name_" + GetRandomString(5, false) + commandVols[i].Path = "/Path_" + GetRandomString(5, false) + LogInfoMessage(fmt.Sprintf("....... Add Volume: %s", commandVols[i])) + } + return commandVols +} + +// getSchemaComponent returns a named component from an array of components +func getSchemaComponent(components []schema.Component, name string) (*schema.Component, bool) { + found := false + var schemaComponent schema.Component + for _, component := range components { + if component.Name == name { + schemaComponent = component + found = true + break + } + } + return &schemaComponent, found +} + +// AddComponent adds a component of the specified type, with random attributes, to the devfile schema +func (devfile *TestDevfile) AddComponent(componentType schema.ComponentType) string { + component := generateComponent(componentType) + devfile.SchemaDevFile.Components = append(devfile.SchemaDevFile.Components, component) + return component.Name +} + +// generateComponent generates a component in a schema structure of the specified type +func generateComponent(componentType schema.ComponentType) schema.Component { + + component := schema.Component{} + component.Name = GetRandomUniqueString(8, true) + LogInfoMessage(fmt.Sprintf("....... Name: %s", component.Name)) + + if componentType == schema.ContainerComponentType { + component.Container = createContainerComponent() + } else if componentType == schema.VolumeComponentType { + component.Volume = createVolumeComponent() + } + return component +} + +// createContainerComponent creates a container component and set its attribute values +func createContainerComponent() *schema.ContainerComponent { + + LogInfoMessage("Create a container component :") + + containerComponent := schema.ContainerComponent{} + setContainerComponentValues(&containerComponent) + + return &containerComponent + +} + +// createVolumeComponent creates a volume component and set its attribute values +func createVolumeComponent() *schema.VolumeComponent { + + LogInfoMessage("Create a volume component :") + + volumeComponent := schema.VolumeComponent{} + setVolumeComponentValues(&volumeComponent) + + return &volumeComponent + +} + +// setContainerComponentValues randomly sets container component attributes to random values +func setContainerComponentValues(containerComponent *schema.ContainerComponent) { + + containerComponent.Image = GetRandomUniqueString(8+GetRandomNumber(10), false) + + if GetBinaryDecision() { + numCommands := GetRandomNumber(3) + containerComponent.Command = make([]string, numCommands) + for i := 0; i < numCommands; i++ { + containerComponent.Command[i] = GetRandomString(4+GetRandomNumber(10), false) + LogInfoMessage(fmt.Sprintf("....... command %d of %d : %s", i, numCommands, containerComponent.Command[i])) + } + } + + if GetBinaryDecision() { + numArgs := GetRandomNumber(3) + containerComponent.Args = make([]string, numArgs) + for i := 0; i < numArgs; i++ { + containerComponent.Args[i] = GetRandomString(8+GetRandomNumber(10), false) + LogInfoMessage(fmt.Sprintf("....... arg %d of %d : %s", i, numArgs, containerComponent.Args[i])) + } + } + + containerComponent.DedicatedPod = GetBinaryDecision() + LogInfoMessage(fmt.Sprintf("....... DedicatedPod: %t", containerComponent.DedicatedPod)) + + if GetBinaryDecision() { + containerComponent.MemoryLimit = strconv.Itoa(4+GetRandomNumber(124)) + "M" + LogInfoMessage(fmt.Sprintf("....... MemoryLimit: %s", containerComponent.MemoryLimit)) + } + + if GetBinaryDecision() { + setMountSources := GetBinaryDecision() + containerComponent.MountSources = &setMountSources + LogInfoMessage(fmt.Sprintf("....... MountSources: %t", *containerComponent.MountSources)) + + if setMountSources { + containerComponent.SourceMapping = "/" + GetRandomString(8, false) + LogInfoMessage(fmt.Sprintf("....... SourceMapping: %s", containerComponent.SourceMapping)) + } + } + + if GetBinaryDecision() { + containerComponent.Env = addEnv(GetRandomNumber(4)) + } else { + containerComponent.Env = nil + } + + if GetBinaryDecision() { + containerComponent.VolumeMounts = addVolume(GetRandomNumber(4)) + } else { + containerComponent.VolumeMounts = nil + } + + if GetBinaryDecision() { + containerComponent.Endpoints = CreateEndpoints() + } + +} + +// setVolumeComponentValues randomly sets volume component attributes to random values +func setVolumeComponentValues(volumeComponent *schema.VolumeComponent) { + + if GetRandomDecision(5, 1) { + volumeComponent.Size = strconv.Itoa(4+GetRandomNumber(252)) + "G" + LogInfoMessage(fmt.Sprintf("....... volumeComponent.Size: %s", volumeComponent.Size)) + } + +} + +// UpdateComponent randomly updates the attribute values of a specified component +func (devfile *TestDevfile) UpdateComponent(component *schema.Component) error { + + var errorString []string + testComponent, found := getSchemaComponent(devfile.SchemaDevFile.Components, component.Name) + if found { + LogInfoMessage(fmt.Sprintf("....... Updating component name: %s", component.Name)) + if testComponent.ComponentType == schema.ContainerComponentType { + setContainerComponentValues(component.Container) + } else if testComponent.ComponentType == schema.VolumeComponentType { + setVolumeComponentValues(component.Volume) + } + } else { + errorString = append(errorString, LogInfoMessage(fmt.Sprintf("....... Component not found in test : %s", component.Name))) + } + var err error + if len(errorString) > 0 { + err = errors.New(fmt.Sprint(errorString)) + } + return err +} + +// VerifyComponents verifies components returned by the parser are the same as those saved in the devfile schema +func (devfile TestDevfile) VerifyComponents(parserComponents []schema.Component) error { + + LogInfoMessage("Enter VerifyComponents") + var errorString []string + + // Compare entire array of components + if !cmp.Equal(parserComponents, devfile.SchemaDevFile.Components) { + errorString = append(errorString, LogErrorMessage(fmt.Sprintf("Component array compare failed."))) + for _, component := range parserComponents { + if testComponent, found := getSchemaComponent(devfile.SchemaDevFile.Components, component.Name); found { + if !cmp.Equal(component, *testComponent) { + parserFilename := AddSuffixToFileName(devfile.FileName, "_"+component.Name+"_Parser") + testFilename := AddSuffixToFileName(devfile.FileName, "_"+component.Name+"_Test") + LogInfoMessage(fmt.Sprintf(".......marshall and write devfile %s", parserFilename)) + c, err := yaml.Marshal(component) + if err != nil { + errorString = append(errorString, LogErrorMessage(fmt.Sprintf(".......marshall devfile %s", parserFilename))) + } else { + err = ioutil.WriteFile(parserFilename, c, 0644) + if err != nil { + errorString = append(errorString, LogErrorMessage(fmt.Sprintf(".......write devfile %s", parserFilename))) + } + } + LogInfoMessage(fmt.Sprintf(".......marshall and write devfile %s", testFilename)) + c, err = yaml.Marshal(testComponent) + if err != nil { + errorString = append(errorString, LogErrorMessage(fmt.Sprintf(".......marshall devfile %s", testFilename))) + } else { + err = ioutil.WriteFile(testFilename, c, 0644) + if err != nil { + errorString = append(errorString, LogErrorMessage(fmt.Sprintf(".......write devfile %s", testFilename))) + } + } + errorString = append(errorString, LogErrorMessage(fmt.Sprintf("Component %s did not match, see files : %s and %s", component.Name, parserFilename, testFilename))) + } else { + LogInfoMessage(fmt.Sprintf(" --> Component matched : %s", component.Name)) + } + } else { + errorString = append(errorString, LogErrorMessage(fmt.Sprintf("Component from parser not known to test - id : %s ", component.Name))) + } + } + for _, component := range devfile.SchemaDevFile.Components { + if _, found := getSchemaComponent(parserComponents, component.Name); !found { + errorString = append(errorString, LogErrorMessage(fmt.Sprintf("Component from test not returned by parser : %s ", component.Name))) + } + } + } else { + LogInfoMessage(fmt.Sprintf("Component structures matched")) + } + + var err error + if len(errorString) > 0 { + err = errors.New(fmt.Sprint(errorString)) + } + return err +} diff --git a/tests/utils/endpoint-test-utils.go b/tests/utils/endpoint-test-utils.go new file mode 100644 index 00000000..b8ca55a9 --- /dev/null +++ b/tests/utils/endpoint-test-utils.go @@ -0,0 +1,63 @@ +package utils + +import ( + "fmt" + + schema "github.com/devfile/api/pkg/apis/workspaces/v1alpha2" +) + +var Exposures = [...]schema.EndpointExposure{schema.PublicEndpointExposure, schema.InternalEndpointExposure, schema.NoneEndpointExposure} + +// getRandomExposure returns a random exposure value +func getRandomExposure() schema.EndpointExposure { + return Exposures[GetRandomNumber(len(Exposures))-1] +} + +//var Protocols = [...]schema.EndpointProtocol{schema.HTTPEndpointProtocol, schema.HTTPSEndpointProtocol, schema.WSEndpointProtocol, schema.WSSEndpointProtocol, schema.TCPEndpointProtocol, schema.UDPEndpointProtocol} +var Protocols = [...]schema.EndpointProtocol{schema.HTTPEndpointProtocol, schema.WSEndpointProtocol, schema.TCPEndpointProtocol, schema.UDPEndpointProtocol} + +// getRandomProtocol returns a random protocol value +func getRandomProtocol() schema.EndpointProtocol { + return Protocols[GetRandomNumber(len(Protocols))-1] +} + +// CreateEndpoints createa and returns a randon numebr of endpoints in a schema structure +func CreateEndpoints() []schema.Endpoint { + + numEndpoints := GetRandomNumber(5) + endpoints := make([]schema.Endpoint, numEndpoints) + + for i := 0; i < numEndpoints; i++ { + + endpoint := schema.Endpoint{} + + endpoint.Name = GetRandomString(GetRandomNumber(15)+5, false) + LogInfoMessage(fmt.Sprintf(" ....... add endpoint %d name : %s", i, endpoint.Name)) + + endpoint.TargetPort = GetRandomNumber(9999) + LogInfoMessage(fmt.Sprintf(" ....... add endpoint %d targetPort: %d", i, endpoint.TargetPort)) + + if GetBinaryDecision() { + endpoint.Exposure = getRandomExposure() + LogInfoMessage(fmt.Sprintf(" ....... add endpoint %d exposure: %s", i, endpoint.Exposure)) + } + + if GetBinaryDecision() { + endpoint.Protocol = getRandomProtocol() + LogInfoMessage(fmt.Sprintf(" ....... add endpoint %d protocol: %s", i, endpoint.Protocol)) + } + + endpoint.Secure = GetBinaryDecision() + LogInfoMessage(fmt.Sprintf(" ....... add endpoint %d secure: %t", i, endpoint.Secure)) + + if GetBinaryDecision() { + endpoint.Path = "/Path_" + GetRandomString(GetRandomNumber(10)+3, false) + LogInfoMessage(fmt.Sprintf(" ....... add endpoint %d path: %s", i, endpoint.Path)) + } + + endpoints[i] = endpoint + + } + + return endpoints +} diff --git a/tests/utils/test_utils.go b/tests/utils/test_utils.go new file mode 100644 index 00000000..c99098f9 --- /dev/null +++ b/tests/utils/test_utils.go @@ -0,0 +1,375 @@ +package utils + +import ( + "errors" + "fmt" + "io" + "io/ioutil" + "log" + "math/rand" + "os" + "path/filepath" + "runtime" + "strings" + "time" + + schema "github.com/devfile/api/pkg/apis/workspaces/v1alpha2" + devfilepkg "github.com/devfile/library/pkg/devfile" + "github.com/devfile/library/pkg/devfile/parser" + devfileCtx "github.com/devfile/library/pkg/devfile/parser/context" + devfileData "github.com/devfile/library/pkg/devfile/parser/data" + "github.com/devfile/library/pkg/devfile/parser/data/v2/common" + "sigs.k8s.io/yaml" +) + +const ( + tmpDir = "../tmp/" + logErrorOnly = false + logFileName = "test.log" + // logToFileOnly - If set to false the log output will also be output to the console + logToFileOnly = true // If set to false the log output will also be output to the console +) + +var ( + testLogger *log.Logger +) + +// init creates: +// - the temporary directory used by the test to store logs and generated devfiles. +// - the log file +func init() { + if _, err := os.Stat(tmpDir); !os.IsNotExist(err) { + os.RemoveAll(tmpDir) + } + os.Mkdir(tmpDir, 0755) + + f, err := os.OpenFile(filepath.Join(tmpDir, logFileName), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + fmt.Printf("Error creating Log file : %v", err) + } else { + if logToFileOnly { + testLogger = log.New(f, "", log.LstdFlags|log.Lmicroseconds) + } else { + writer := io.MultiWriter(f, os.Stdout) + testLogger = log.New(writer, "", log.LstdFlags|log.Lmicroseconds) + } + testLogger.Println("Test Starting:") + } +} + +// createTempDir creates a specified sub directory under the temp directory if it does not exist. +// Returns the name of the created directory. +func createTempDir(subdir string) string { + tempDir := tmpDir + subdir + "/" + if _, err := os.Stat(tempDir); os.IsNotExist(err) { + os.Mkdir(tempDir, 0755) + } + return tempDir +} + +// GetDevFileName returns a qualified name of a devfile for use in a test. +// The devfile will be in a temporary directory and is named using the calling function's name. +func GetDevFileName() string { + pc, fn, _, ok := runtime.Caller(1) + if !ok { + return tmpDir + "DefaultDevfile" + } + + testFile := filepath.Base(fn) + testFileExtension := filepath.Ext(testFile) + subdir := testFile[0 : len(testFile)-len(testFileExtension)] + destDir := createTempDir(subdir) + callerName := runtime.FuncForPC(pc).Name() + pos1 := strings.LastIndex(callerName, "/tests/api.") + len("/tests/api.") + devfileName := destDir + callerName[pos1:len(callerName)] + ".yaml" + + LogInfoMessage(fmt.Sprintf("GetDevFileName : %s", devfileName)) + + return devfileName +} + +// AddSuffixToFileName adds a specified suffix to the name of a specified file. +// For example if the file is devfile.yaml and the suffix is 1, the result is devfile1.yaml +func AddSuffixToFileName(fileName string, suffix string) string { + pos1 := strings.LastIndex(fileName, ".yaml") + newFileName := fileName[0:pos1] + suffix + ".yaml" + LogInfoMessage(fmt.Sprintf("Add suffix %s to fileName %s : %s", suffix, fileName, newFileName)) + return newFileName +} + +// LogMessage logs the specified message and returns the message logged +func LogMessage(message string) string { + testLogger.Println(message) + return message +} + +var errorPrefix = "..... ERROR : " +var infoPrefix = "INFO :" + +// LogErrorMessage logs the specified message as an error message and returns the message logged +func LogErrorMessage(message string) string { + var errMessage []string + errMessage = append(errMessage, errorPrefix, message) + return LogMessage(fmt.Sprint(errMessage)) +} + +// LogInfoMessage logs the specified message as an info message and returns the message logged +func LogInfoMessage(message string) string { + var infoMessage []string + infoMessage = append(infoMessage, infoPrefix, message) + return LogMessage(fmt.Sprint(infoMessage)) +} + +// TestDevfile is a structure used to track a test devfile and its contents +type TestDevfile struct { + SchemaDevFile schema.Devfile + FileName string + ParsedSchemaObj parser.DevfileObj + SchemaParsed bool +} + +var StringCount int = 0 + +var RndSeed int64 = time.Now().UnixNano() + +// GetRandomUniqueString returns a unique random string which is n characters long plus an integer to ensure uniqueness +// If lower is set to true a lower case string is returned. +func GetRandomUniqueString(n int, lower bool) string { + StringCount++ + return fmt.Sprintf("%s%04d", GetRandomString(n, lower), StringCount) +} + +// Creates a unique seed for the randon generation. +func setRandSeed() { + RndSeed++ + rand.Seed(RndSeed) +} + +const schemaBytes = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + +// GetRandomString returns a random string which is n characters long. +// If lower is set to true a lower case string is returned. +func GetRandomString(n int, lower bool) string { + setRandSeed() + b := make([]byte, n) + for i := range b { + b[i] = schemaBytes[rand.Intn(len(schemaBytes)-1)] + } + randomString := string(b) + if lower { + randomString = strings.ToLower(randomString) + } + return randomString +} + +var GroupKinds = [...]schema.CommandGroupKind{schema.BuildCommandGroupKind, schema.RunCommandGroupKind, schema.TestCommandGroupKind, schema.DebugCommandGroupKind} + +// GetRandomGroupKind return random group kind. One of "build", "run", "test" or "debug" +func GetRandomGroupKind() schema.CommandGroupKind { + return GroupKinds[GetRandomNumber(len(GroupKinds))-1] +} + +// GetBinaryDecision randomly returns true or false +func GetBinaryDecision() bool { + return GetRandomDecision(1, 1) +} + +// GetRandomDecision randomly returns true or false, but weighted to one or the other. +// For example if success is set to 2 and failure to 1, true is twice as likely to be returned. +func GetRandomDecision(success int, failure int) bool { + setRandSeed() + return rand.Intn(success+failure) > failure-1 +} + +// GetRandomNumber randomly returns an integer between 1 and the number specified. +func GetRandomNumber(max int) int { + setRandSeed() + return rand.Intn(max) + 1 +} + +// GetDevfile returns a structure used to represent a specific devfile in a test +func GetDevfile(fileName string) TestDevfile { + testDevfile := TestDevfile{} + testDevfile.SchemaDevFile = schema.Devfile{} + testDevfile.FileName = fileName + testDevfile.SchemaDevFile.SchemaVersion = "2.0.0" + testDevfile.SchemaParsed = false + return testDevfile +} + +// CreateDevfile create a devifle on disk for use in a test. +// If useParser is true the parser library is used to generate the file, otherwise "sigs.k8s.io/yaml" is used. +func (devfile *TestDevfile) CreateDevfile(useParser bool) error { + var err error + + fileName := devfile.FileName + if !strings.HasSuffix(fileName, ".yaml") { + fileName += ".yaml" + } + + if useParser { + LogInfoMessage(fmt.Sprintf("Use Parser to write devfile %s", fileName)) + newDevfile, createErr := devfileData.NewDevfileData(devfile.SchemaDevFile.SchemaVersion) + if createErr != nil { + err = errors.New(LogErrorMessage(fmt.Sprintf("Creating new devfile : %v", createErr))) + } else { + newDevfile.SetSchemaVersion(devfile.SchemaDevFile.SchemaVersion) + + // add the commands to new devfile + for _, command := range devfile.SchemaDevFile.Commands { + newDevfile.AddCommands(command) + } + // add components to the new devfile + newDevfile.AddComponents(devfile.SchemaDevFile.Components) + + ctx := devfileCtx.NewDevfileCtx(fileName) + + err = ctx.SetAbsPath() + if err != nil { + LogErrorMessage(fmt.Sprintf("Setting devfile path : %v", err)) + } else { + devObj := parser.DevfileObj{ + Ctx: ctx, + Data: newDevfile, + } + err = devObj.WriteYamlDevfile() + if err != nil { + LogErrorMessage(fmt.Sprintf("Writing devfile : %v", err)) + } else { + devfile.SchemaParsed = false + } + } + + } + } else { + LogInfoMessage(fmt.Sprintf("Marshall and write devfile %s", devfile.FileName)) + c, marshallErr := yaml.Marshal(&(devfile.SchemaDevFile)) + + if marshallErr != nil { + err = errors.New(LogErrorMessage(fmt.Sprintf("Marshall devfile %s : %v", devfile.FileName, marshallErr))) + } else { + err = ioutil.WriteFile(fileName, c, 0644) + if err != nil { + LogErrorMessage(fmt.Sprintf("Write devfile %s : %v", devfile.FileName, err)) + } else { + devfile.SchemaParsed = false + } + } + } + return err +} + +// Use the parser to parse a devfile on disk +func (devfile *TestDevfile) parseSchema() error { + + var err error + if !devfile.SchemaParsed { + LogInfoMessage(fmt.Sprintf("Parse and Validate %s : ", devfile.FileName)) + devfile.ParsedSchemaObj, err = devfilepkg.ParseAndValidate(devfile.FileName) + if err != nil { + LogErrorMessage(fmt.Sprintf("From ParseAndValidate %v : ", err)) + } + devfile.SchemaParsed = true + } + return err +} + +// Verify verifies the contents of the specified devfile with the expected content +func (devfile TestDevfile) Verify() error { + + LogInfoMessage(fmt.Sprintf("Verify %s : ", devfile.FileName)) + + var errorString []string + + err := devfile.parseSchema() + + if err != nil { + errorString = append(errorString, LogErrorMessage(fmt.Sprintf("parsing schema %s : %v", devfile.FileName, err))) + } else { + LogInfoMessage(fmt.Sprintf("Get commands %s : ", devfile.FileName)) + commands, _ := devfile.ParsedSchemaObj.Data.GetCommands(common.DevfileOptions{}) + if commands != nil && len(commands) > 0 { + err = devfile.VerifyCommands(commands) + if err != nil { + errorString = append(errorString, LogErrorMessage(fmt.Sprintf("Verfify Commands %s : %v", devfile.FileName, err))) + } + } else { + LogInfoMessage(fmt.Sprintf("No command found in %s : ", devfile.FileName)) + } + + LogInfoMessage(fmt.Sprintf("Get components %s : ", devfile.FileName)) + components, _ := devfile.ParsedSchemaObj.Data.GetComponents(common.DevfileOptions{}) + if components != nil && len(components) > 0 { + err = devfile.VerifyComponents(components) + if err != nil { + errorString = append(errorString, LogErrorMessage(fmt.Sprintf("Verfify Commands %s : %v", devfile.FileName, err))) + } + } else { + LogInfoMessage(fmt.Sprintf("No components found in %s : ", devfile.FileName)) + } + } + var returnError error + if len(errorString) > 0 { + returnError = errors.New(fmt.Sprint(errorString)) + } + return returnError + +} + +// EditCommands modifies random attributes for each of the commands in the devfile. +func (devfile TestDevfile) EditCommands() error { + + LogInfoMessage(fmt.Sprintf("Edit %s : ", devfile.FileName)) + + err := devfile.parseSchema() + if err != nil { + LogErrorMessage(fmt.Sprintf("From parser : %v", err)) + } else { + LogInfoMessage(fmt.Sprintf(" -> Get commands %s : ", devfile.FileName)) + commands, _ := devfile.ParsedSchemaObj.Data.GetCommands(common.DevfileOptions{}) + for _, command := range commands { + err = devfile.UpdateCommand(&command) + if err != nil { + LogErrorMessage(fmt.Sprintf("Updating command : %v", err)) + } else { + LogInfoMessage(fmt.Sprintf("Update command in Parser : %s", command.Id)) + devfile.ParsedSchemaObj.Data.UpdateCommand(command) + } + } + LogInfoMessage(fmt.Sprintf("Write updated file to yaml : %s", devfile.FileName)) + devfile.ParsedSchemaObj.WriteYamlDevfile() + devfile.SchemaParsed = false + } + return err +} + +// EditComponents modifies random attributes for each of the components in the devfile. +func (devfile TestDevfile) EditComponents() error { + + LogInfoMessage(fmt.Sprintf("Edit %s : ", devfile.FileName)) + + err := devfile.parseSchema() + if err != nil { + LogErrorMessage(fmt.Sprintf("From parser : %v", err)) + } else { + LogInfoMessage(fmt.Sprintf(" -> Get commands %s : ", devfile.FileName)) + components, _ := devfile.ParsedSchemaObj.Data.GetComponents(common.DevfileOptions{}) + for _, component := range components { + err = devfile.UpdateComponent(&component) + if err != nil { + LogErrorMessage(fmt.Sprintf("Updating component : %v", err)) + } else { + LogInfoMessage(fmt.Sprintf("Update component in Parser : %s", component.Name)) + devfile.ParsedSchemaObj.Data.UpdateComponent(component) + } + } + LogInfoMessage(fmt.Sprintf("Write updated file to yaml : %s", devfile.FileName)) + devfile.ParsedSchemaObj.WriteYamlDevfile() + devfile.SchemaParsed = false + } + return err +} + +func getError(message string) (string, error) { + return message, errors.New(message) +}