From 66b6a200a33f3c3ecfd59daf1f8d124fdecc2b37 Mon Sep 17 00:00:00 2001 From: Anton Novojilov Date: Fri, 19 Nov 2021 02:30:15 +0300 Subject: [PATCH 1/5] Command grouping --- Makefile | 6 +-- cli/cli.go | 2 +- cli/executor/executor.go | 13 ++++-- parser/parser.go | 78 +++++++++++++++++++++++------------ parser/parser_test.go | 32 +++++++++------ recipe/recipe.go | 87 +++++++++++++++++++++++++++++++--------- recipe/recipe_test.go | 32 +++++++++++---- recipe/tokens.go | 4 ++ testdata/test1.recipe | 10 ++++- testdata/test2.recipe | 4 +- testdata/test3.recipe | 2 +- testdata/test4.recipe | 2 +- testdata/test5.recipe | 2 +- testdata/test9.recipe | 4 ++ 14 files changed, 198 insertions(+), 80 deletions(-) create mode 100644 testdata/test9.recipe diff --git a/Makefile b/Makefile index 21252921..b4e1dd9b 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ ################################################################################ -# This Makefile generated by GoMakeGen 1.3.1 using next command: +# This Makefile generated by GoMakeGen 1.3.2 using next command: # gomakegen . # # More info: https://kaos.sh/gomakegen @@ -32,7 +32,7 @@ deps: git-config ## Download dependencies go get -d -v pkg.re/essentialkaos/ek.v12 deps-test: git-config ## Download dependencies for tests - go get -d -v pkg.re/check.v1 + go get -d -v pkg.re/essentialkaos/check.v1 test: ## Run tests go test -covermode=count ./parser ./recipe @@ -55,6 +55,6 @@ help: ## Show this info @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) \ | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[33m%-12s\033[0m %s\n", $$1, $$2}' @echo -e '' - @echo -e '\033[90mGenerated by GoMakeGen 1.3.1\033[0m\n' + @echo -e '\033[90mGenerated by GoMakeGen 1.3.2\033[0m\n' ################################################################################ diff --git a/cli/cli.go b/cli/cli.go index fe4be1bc..8eae8184 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -37,7 +37,7 @@ import ( // Application info const ( APP = "bibop" - VER = "4.9.0" + VER = "5.0.0" DESC = "Utility for testing command-line tools" ) diff --git a/cli/executor/executor.go b/cli/executor/executor.go index bc8671e1..392ca434 100644 --- a/cli/executor/executor.go +++ b/cli/executor/executor.go @@ -204,17 +204,18 @@ func applyRecipeOptions(e *Executor, rr render.Renderer, r *recipe.Recipe) { // processRecipe execute commands in recipe func processRecipe(e *Executor, rr render.Renderer, r *recipe.Recipe, tags []string) { + var lastFailedGroupID uint8 + var finished bool + e.start = time.Now() e.skipped = len(r.Commands) - finished := false - for index, command := range r.Commands { if r.LockWorkdir && r.Dir != "" { os.Chdir(r.Dir) // Set current dir to working dir for every command } - if skipCommand(command, tags, finished) { + if skipCommand(command, tags, lastFailedGroupID, finished) { e.skipped-- continue } @@ -228,6 +229,8 @@ func processRecipe(e *Executor, rr render.Renderer, r *recipe.Recipe, tags []str if !ok { e.fails++ + lastFailedGroupID = command.GroupID + if r.FastFinish { rr.CommandDone(command, true) finished = true @@ -435,10 +438,12 @@ func outputIOLoop(cmdEnv *CommandEnv) { } // skipCommand returns true if command should be skipped -func skipCommand(c *recipe.Command, tags []string, finished bool) bool { +func skipCommand(c *recipe.Command, tags []string, lastFailedGroupID uint8, finished bool) bool { switch { case c.Tag == recipe.TEARDOWN_TAG: return false + case c.GroupID == lastFailedGroupID: + return true case finished == true: return true case c.Tag == "": diff --git a/parser/parser.go b/parser/parser.go index a1314026..87d1798e 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -29,6 +29,7 @@ type entity struct { args []string tag string isNegative bool + isGroup bool } // ////////////////////////////////////////////////////////////////////////////////// // @@ -127,31 +128,35 @@ func parseLine(line string) (*entity, error) { isGlobal = true } - cmd := strutil.Fields(line) + fields := strutil.Fields(line) - if len(cmd) == 0 { + if len(fields) == 0 { return nil, fmt.Errorf("Can't parse token data") } - info := getTokenInfo(cmd[0]) - tag := extractTag(cmd[0]) + keyword := fields[0] + + info := getTokenInfo(keyword) + tag := extractTag(keyword) if info.Keyword == "" || info.Global != isGlobal { switch isGlobal { case true: - return nil, fmt.Errorf("Global keyword \"%s\" is not supported", cmd[0]) + return nil, fmt.Errorf("Global keyword \"%s\" is not supported", keyword) case false: - return nil, fmt.Errorf("Keyword \"%s\" is not supported", cmd[0]) + return nil, fmt.Errorf("Keyword \"%s\" is not supported", keyword) } } - isNegative := strings.HasPrefix(cmd[0], "!") + isNegative := strings.HasPrefix(keyword, recipe.SYMBOL_NEGATIVE_ACTION) if isNegative && !info.AllowNegative { - return nil, fmt.Errorf("Action \"%s\" does not support negative results", cmd[0]) + return nil, fmt.Errorf("Action \"%s\" does not support negative results", keyword) } - argsNum := len(cmd) - 1 + isGroup := strings.HasPrefix(keyword, recipe.SYMBOL_COMMAND_GROUP) + + argsNum := len(fields) - 1 switch { case argsNum > info.MaxArgs: @@ -160,30 +165,30 @@ func parseLine(line string) (*entity, error) { return nil, fmt.Errorf("Action \"%s\" has too few arguments (minimum is %d)", info.Keyword, info.MinArgs) } - return &entity{info, cmd[1:], tag, isNegative}, nil + return &entity{info, fields[1:], tag, isNegative, isGroup}, nil } // appendData append data to recipe struct func appendData(r *recipe.Recipe, e *entity, line uint16) error { if e.info.Global { - return applyGlobalOptions(r, e, line) - } - - action := &recipe.Action{ - Name: e.info.Keyword, - Arguments: e.args, - Negative: e.isNegative, - Line: line, + return processGlobalEntity(r, e, line) } - lastCommand := r.Commands[len(r.Commands)-1] - lastCommand.AddAction(action) + r.Commands.Last().AddAction( + &recipe.Action{ + Name: e.info.Keyword, + Arguments: e.args, + Negative: e.isNegative, + Line: line, + }, + ) return nil } -// applyGlobalOptions applies global options to recipe -func applyGlobalOptions(r *recipe.Recipe, e *entity, line uint16) error { +// processGlobalEntity creates new global entity (variable/command) or appplies +// global option +func processGlobalEntity(r *recipe.Recipe, e *entity, line uint16) error { var err error switch e.info.Keyword { @@ -191,11 +196,27 @@ func applyGlobalOptions(r *recipe.Recipe, e *entity, line uint16) error { r.AddVariable(e.args[0], e.args[1]) case recipe.KEYWORD_COMMAND: - r.AddCommand(recipe.NewCommand(e.args, line), e.tag) + if e.isGroup && len(r.Commands) == 0 { + return fmt.Errorf("Group command (with prefix +) cannot be defined as first in a recipe") + } + + r.AddCommand(recipe.NewCommand(e.args, line), e.tag, e.isGroup) case recipe.KEYWORD_PACKAGE: r.Packages = e.args + default: + err = applyGlobalOption(r, e, line) + } + + return err +} + +// applyGlobalOption applies global options to the recipe +func applyGlobalOption(r *recipe.Recipe, e *entity, line uint16) error { + var err error + + switch e.info.Keyword { case recipe.OPTION_UNSAFE_ACTIONS: r.UnsafeActions, err = getOptionBoolValue(e.info.Keyword, e.args[0]) @@ -246,12 +267,17 @@ func getOptionFloatValue(keyword, value string) (float64, error) { // getTokenInfo return token info by keyword func getTokenInfo(keyword string) recipe.TokenInfo { - if strings.HasPrefix(keyword, recipe.KEYWORD_COMMAND+":") { + switch { + case strings.HasPrefix(keyword, recipe.KEYWORD_COMMAND+recipe.SYMBOL_SEPARATOR), + strings.HasPrefix(keyword, recipe.SYMBOL_COMMAND_GROUP+recipe.KEYWORD_COMMAND), + strings.HasPrefix(keyword, recipe.SYMBOL_COMMAND_GROUP+recipe.KEYWORD_COMMAND+recipe.SYMBOL_SEPARATOR): keyword = recipe.KEYWORD_COMMAND } for _, token := range recipe.Tokens { - if token.Keyword == keyword || "!"+token.Keyword == keyword { + switch { + case token.Keyword == keyword, + recipe.SYMBOL_NEGATIVE_ACTION+token.Keyword == keyword: return token } } @@ -276,7 +302,7 @@ func isUselessRecipeLine(line string) bool { // extractTag extracts tag from command func extractTag(data string) string { - if !strings.HasPrefix(data, recipe.KEYWORD_COMMAND+":") { + if !strings.HasPrefix(data, recipe.KEYWORD_COMMAND+recipe.SYMBOL_SEPARATOR) { return "" } diff --git a/parser/parser_test.go b/parser/parser_test.go index a18262c2..5bb075da 100644 --- a/parser/parser_test.go +++ b/parser/parser_test.go @@ -8,9 +8,10 @@ package parser // ////////////////////////////////////////////////////////////////////////////////// // import ( + "errors" "testing" - . "pkg.re/check.v1" + . "pkg.re/essentialkaos/check.v1" ) // ////////////////////////////////////////////////////////////////////////////////// // @@ -30,42 +31,47 @@ var _ = Suite(&ParseSuite{}) func (s *ParseSuite) TestGlobalErrors(c *C) { recipe, err := Parse("../testdata/test0.recipe") - c.Assert(err, NotNil) + c.Assert(err, DeepEquals, errors.New("File ../testdata/test0.recipe doesn't exist or not readable")) c.Assert(recipe, IsNil) recipe, err = Parse("../testdata/test2.recipe") - c.Assert(err, NotNil) + c.Assert(err, DeepEquals, errors.New("Parsing error in line 3: Action \"unsafe-actions\" has too many arguments (maximum is 1)")) c.Assert(recipe, IsNil) recipe, err = Parse("../testdata/test3.recipe") - c.Assert(err, NotNil) + c.Assert(err, DeepEquals, errors.New("Parsing error in line 3: \"123\" is not allowed as value for unsafe-actions")) c.Assert(recipe, IsNil) recipe, err = Parse("../testdata/test4.recipe") - c.Assert(err, NotNil) + c.Assert(err, DeepEquals, errors.New("Parsing error in line 4: \"123\" is not allowed as value for require-root")) c.Assert(recipe, IsNil) recipe, err = Parse("../testdata/test5.recipe") - c.Assert(err, NotNil) + c.Assert(err, DeepEquals, errors.New("Parsing error in line 6: keyword \"exist\" is not allowed there")) c.Assert(recipe, IsNil) recipe, err = Parse("../testdata/test6.recipe") - c.Assert(err, NotNil) + c.Assert(err, DeepEquals, errors.New("File ../testdata/test6.recipe is empty")) c.Assert(recipe, IsNil) recipe, err = Parse("../testdata/test7.recipe") - c.Assert(err, NotNil) + c.Assert(err, DeepEquals, errors.New("Parsing error in line 3: \"123\" is not allowed as value for unsafe-actions")) c.Assert(recipe, IsNil) recipe, err = Parse("../testdata/test8.recipe") - c.Assert(err, NotNil) + c.Assert(err, DeepEquals, errors.New("Parsing error in line 5: keyword \"expect\" is not allowed there")) + c.Assert(recipe, IsNil) + + recipe, err = Parse("../testdata/test9.recipe") + + c.Assert(err, DeepEquals, errors.New("Parsing error in line 3: Group command (with prefix +) cannot be defined as first in a recipe")) c.Assert(recipe, IsNil) } @@ -84,12 +90,12 @@ func (s *ParseSuite) TestBasicParsing(c *C) { c.Assert(recipe.Unbuffer, Equals, true) c.Assert(recipe.HTTPSSkipVerify, Equals, true) c.Assert(recipe.Delay, Equals, 1.23) - c.Assert(recipe.Commands, HasLen, 2) + c.Assert(recipe.Commands, HasLen, 4) c.Assert(recipe.Packages, DeepEquals, []string{"package1", "package2"}) c.Assert(recipe.Commands[0].User, Equals, "nobody") c.Assert(recipe.Commands[0].Tag, Equals, "") - c.Assert(recipe.Commands[0].Cmdline, Equals, "echo") + c.Assert(recipe.Commands[0].Cmdline, Equals, "echo test") c.Assert(recipe.Commands[0].Description, Equals, "Simple echo command") c.Assert(recipe.Commands[0].Actions, HasLen, 3) @@ -98,9 +104,11 @@ func (s *ParseSuite) TestBasicParsing(c *C) { c.Assert(recipe.Commands[1].User, Equals, "") c.Assert(recipe.Commands[1].Tag, Equals, "special") - c.Assert(recipe.Commands[1].Cmdline, Equals, "echo") + c.Assert(recipe.Commands[1].Cmdline, Equals, "echo test") c.Assert(recipe.Commands[1].Description, Equals, "Simple echo command") c.Assert(recipe.Commands[1].Actions, HasLen, 1) + + c.Assert(recipe.Commands[2].GroupID, Equals, recipe.Commands[3].GroupID) } func (s *ParseSuite) TestOptionsParsing(c *C) { diff --git a/recipe/recipe.go b/recipe/recipe.go index 221b84ff..8bf3a428 100644 --- a/recipe/recipe.go +++ b/recipe/recipe.go @@ -29,35 +29,43 @@ const TEARDOWN_TAG = "teardown" // Recipe contains recipe data // aligo:ignore type Recipe struct { - Packages []string // Package list - Commands []*Command // Commands - File string // Path to recipe - Dir string // Working dir - Delay float64 // Delay between commands - UnsafeActions bool // Allow unsafe actions - RequireRoot bool // Require root privileges - FastFinish bool // Fast finish flag - LockWorkdir bool // Locking workdir flag - Unbuffer bool // Disabled IO buffering - HTTPSSkipVerify bool // Disable certificate verification + Packages []string // Package list + Commands Commands // Commands + File string // Path to recipe + Dir string // Working dir + Delay float64 // Delay between commands + UnsafeActions bool // Allow unsafe actions + RequireRoot bool // Require root privileges + FastFinish bool // Fast finish flag + LockWorkdir bool // Locking workdir flag + Unbuffer bool // Disabled IO buffering + HTTPSSkipVerify bool // Disable certificate verification variables map[string]*Variable // Variables } +// Commands is a slice with commands +type Commands []*Command + // Command contains command with all actions // aligo:ignore type Command struct { - Actions []*Action // Slice with actions - User string // User name - Tag string // Tag - Cmdline string // Command line - Description string // Description - Recipe *Recipe // Link to recipe - Line uint16 // Line in recipe + Actions Actions // Slice with actions + User string // User name + Tag string // Tag + Cmdline string // Command line + Description string // Description + Recipe *Recipe // Link to recipe + Line uint16 // Line in recipe + + GroupID uint8 // Unique command group ID props map[string]interface{} // Properties } +// Actions is a slice with actions +type Actions []*Action + // Action contains action name and slice with arguments type Action struct { Arguments []string // Arguments @@ -65,7 +73,6 @@ type Action struct { Command *Command // Link to command Line uint16 // Line in recipe Negative bool // Negative check flag - } type Variable struct { @@ -99,7 +106,7 @@ func NewCommand(args []string, line uint16) *Command { // ////////////////////////////////////////////////////////////////////////////////// // // AddCommand appends command to command slice -func (r *Recipe) AddCommand(cmd *Command, tag string) { +func (r *Recipe) AddCommand(cmd *Command, tag string, isNested bool) { cmd.Recipe = r cmd.Tag = tag @@ -115,6 +122,14 @@ func (r *Recipe) AddCommand(cmd *Command, tag string) { cmd.Description = renderVars(r, cmd.Description) } + if len(r.Commands) != 0 { + if isNested { + cmd.GroupID = r.Commands.Last().GroupID + } else { + cmd.GroupID = r.Commands.Last().GroupID + 1 + } + } + r.Commands = append(r.Commands, cmd) } @@ -187,6 +202,22 @@ func (r *Recipe) HasTeardown() bool { // ////////////////////////////////////////////////////////////////////////////////// // +// Last returns the last command from slice +func (c Commands) Last() *Command { + if len(c) == 0 { + return nil + } + + return c[len(c)-1] +} + +// Has returns true if slice contains command with given index +func (c Commands) Has(index int) bool { + return c != nil && index < len(c) +} + +// ////////////////////////////////////////////////////////////////////////////////// // + // AddAction appends command to actions slice func (c *Command) AddAction(action *Action) { action.Command = c @@ -249,6 +280,22 @@ func (c *Command) HasProp(name string) bool { // ////////////////////////////////////////////////////////////////////////////////// // +// Last returns the last action from slice +func (a Actions) Last() *Action { + if len(a) == 0 { + return nil + } + + return a[len(a)-1] +} + +// Has returns true if slice contains action with given index +func (a Actions) Has(index int) bool { + return a != nil && index < len(a) +} + +// ////////////////////////////////////////////////////////////////////////////////// // + // Index returns action index func (a *Action) Index() int { if a.Command == nil { diff --git a/recipe/recipe_test.go b/recipe/recipe_test.go index a653a11d..d9bcdbb4 100644 --- a/recipe/recipe_test.go +++ b/recipe/recipe_test.go @@ -11,7 +11,7 @@ import ( "os" "testing" - . "pkg.re/check.v1" + . "pkg.re/essentialkaos/check.v1" ) // ////////////////////////////////////////////////////////////////////////////////// // @@ -64,17 +64,29 @@ func (s *RecipeSuite) TestBasicRecipe(c *C) { r.AddVariable("service", "nginx") r.AddVariable("user", "nginx") + c.Assert(r.Commands.Last(), IsNil) + c1 := NewCommand([]string{"{user}:echo {service}"}, 0) c2 := NewCommand([]string{"echo ABCD 1.53 4000", "Echo command for service {service}"}, 0) + c3 := NewCommand([]string{"echo 1234"}, 0) + c4 := NewCommand([]string{"echo test"}, 0) + + r.AddCommand(c1, "", false) + r.AddCommand(c2, "special", false) + r.AddCommand(c3, "", false) + r.AddCommand(c4, "", true) - r.AddCommand(c1, "") - r.AddCommand(c2, "special") + c.Assert(r.Commands.Last(), Equals, c4) + c.Assert(r.Commands.Has(3), Equals, true) + c.Assert(r.Commands.Has(99), Equals, false) c.Assert(r.RequireRoot, Equals, true) c.Assert(c1.User, Equals, "nginx") c.Assert(c2.Tag, Equals, "special") c.Assert(c2.Description, Equals, "Echo command for service nginx") + c.Assert(c3.GroupID, Equals, c4.GroupID) + a1 := &Action{Name: "copy", Arguments: []string{"file1", "file2"}, Negative: true, Line: 0, Command: nil, @@ -92,12 +104,18 @@ func (s *RecipeSuite) TestBasicRecipe(c *C) { Negative: false, Line: 0, Command: nil, } + c.Assert(c1.Actions.Last(), IsNil) + c1.AddAction(a1) c2.AddAction(a2) c.Assert(c1.GetCmdlineArgs(), DeepEquals, []string{"echo", "nginx"}) c.Assert(c2.GetCmdlineArgs(), DeepEquals, []string{"echo", "ABCD", "1.53", "4000"}) + c.Assert(c1.Actions.Last(), Equals, a1) + c.Assert(c1.Actions.Has(0), Equals, true) + c.Assert(c1.Actions.Has(99), Equals, false) + vs, err := a1.GetS(0) c.Assert(vs, Equals, "file1") c.Assert(err, IsNil) @@ -216,7 +234,7 @@ func (s *RecipeSuite) TestIndex(c *C) { c.Assert(c1.Index(), Equals, -1) - r.AddCommand(c1, "") + r.AddCommand(c1, "", false) c.Assert(c1.Index(), Equals, 0) @@ -297,7 +315,7 @@ func (s *RecipeSuite) TestAux(c *C) { k := &Command{} - r.AddCommand(k, "") + r.AddCommand(k, "", false) c.Assert(renderVars(nil, "{abcd}"), Equals, "{abcd}") c.Assert(renderVars(r, "{abcd}"), Equals, "{abcd}") @@ -314,12 +332,12 @@ func (s *RecipeSuite) TestAux(c *C) { func (s *RecipeSuite) TestTags(c *C) { r, k := &Recipe{}, &Command{} - r.AddCommand(k, "teardown") + r.AddCommand(k, "teardown", false) c.Assert(r.HasTeardown(), Equals, true) r, k = &Recipe{}, &Command{} - r.AddCommand(k, "") + r.AddCommand(k, "", false) c.Assert(r.HasTeardown(), Equals, false) } diff --git a/recipe/tokens.go b/recipe/tokens.go index d57c051d..75adc685 100644 --- a/recipe/tokens.go +++ b/recipe/tokens.go @@ -8,6 +8,10 @@ package recipe // ////////////////////////////////////////////////////////////////////////////////// // const ( + SYMBOL_COMMAND_GROUP = "+" + SYMBOL_NEGATIVE_ACTION = "!" + SYMBOL_SEPARATOR = ":" + KEYWORD_VAR = "var" KEYWORD_COMMAND = "command" KEYWORD_PACKAGE = "pkg" diff --git a/testdata/test1.recipe b/testdata/test1.recipe index 77ec9ea9..92a21c13 100644 --- a/testdata/test1.recipe +++ b/testdata/test1.recipe @@ -12,10 +12,16 @@ delay 1.23 var user nobody -command "{user}:echo" "Simple echo command" +command "{user}:echo test" "Simple echo command" !exist "/etc/unknown.txt" expect '{"id": "test"}' exit 1 -command:special "echo" "Simple echo command" +command:special "echo test" "Simple echo command" + exit 1 + +command "echo test" "Simple echo command" + exit 1 + ++command "echo test" "Simple echo command" exit 1 diff --git a/testdata/test2.recipe b/testdata/test2.recipe index cbbec345..dca0e33f 100644 --- a/testdata/test2.recipe +++ b/testdata/test2.recipe @@ -1,6 +1,6 @@ # This is comment -dir "/tmp" asd -unsafe-actions true + +unsafe-actions true abcd require-root true command "echo" "Simple echo command" diff --git a/testdata/test3.recipe b/testdata/test3.recipe index 18dcc51e..ad1efa7c 100644 --- a/testdata/test3.recipe +++ b/testdata/test3.recipe @@ -1,5 +1,5 @@ # This is comment -dir "/tmp" + unsafe-actions 123 require-root true diff --git a/testdata/test4.recipe b/testdata/test4.recipe index a4f854c5..c7fb82d3 100644 --- a/testdata/test4.recipe +++ b/testdata/test4.recipe @@ -1,5 +1,5 @@ # This is comment -dir "/tmp" + unsafe-actions true require-root 123 diff --git a/testdata/test5.recipe b/testdata/test5.recipe index 6602c03d..787e7e7a 100644 --- a/testdata/test5.recipe +++ b/testdata/test5.recipe @@ -1,5 +1,5 @@ # This is comment -dir "/tmp" + unsafe-actions true require-root true diff --git a/testdata/test9.recipe b/testdata/test9.recipe new file mode 100644 index 00000000..1dfea9a7 --- /dev/null +++ b/testdata/test9.recipe @@ -0,0 +1,4 @@ +# This is comment + ++command "echo test" "Simple command" + exit 1 From b45f3f3f4903982e658100753c0291a1f857eb1a Mon Sep 17 00:00:00 2001 From: Anton Novojilov Date: Fri, 19 Nov 2021 03:46:19 +0300 Subject: [PATCH 2/5] Add protection from cyclic variable redefinition --- parser/parser.go | 8 +++----- recipe/recipe.go | 28 ++++++++++++++++++++++------ recipe/recipe_test.go | 18 ++++++++++++++++++ 3 files changed, 43 insertions(+), 11 deletions(-) diff --git a/parser/parser.go b/parser/parser.go index 87d1798e..015aa2a3 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -174,7 +174,7 @@ func appendData(r *recipe.Recipe, e *entity, line uint16) error { return processGlobalEntity(r, e, line) } - r.Commands.Last().AddAction( + return r.Commands.Last().AddAction( &recipe.Action{ Name: e.info.Keyword, Arguments: e.args, @@ -182,8 +182,6 @@ func appendData(r *recipe.Recipe, e *entity, line uint16) error { Line: line, }, ) - - return nil } // processGlobalEntity creates new global entity (variable/command) or appplies @@ -193,14 +191,14 @@ func processGlobalEntity(r *recipe.Recipe, e *entity, line uint16) error { switch e.info.Keyword { case recipe.KEYWORD_VAR: - r.AddVariable(e.args[0], e.args[1]) + err = r.AddVariable(e.args[0], e.args[1]) case recipe.KEYWORD_COMMAND: if e.isGroup && len(r.Commands) == 0 { return fmt.Errorf("Group command (with prefix +) cannot be defined as first in a recipe") } - r.AddCommand(recipe.NewCommand(e.args, line), e.tag, e.isGroup) + err = r.AddCommand(recipe.NewCommand(e.args, line), e.tag, e.isGroup) case recipe.KEYWORD_PACKAGE: r.Packages = e.args diff --git a/recipe/recipe.go b/recipe/recipe.go index 8bf3a428..187a9437 100644 --- a/recipe/recipe.go +++ b/recipe/recipe.go @@ -18,10 +18,13 @@ import ( // ////////////////////////////////////////////////////////////////////////////////// // -// MAX_VAR_NESTING maximum variables nesting +// MAX_VAR_NESTING is maximum variables nesting const MAX_VAR_NESTING = 32 -// TEARDOWN_TAG contains teardown tag +// MAX_VARIABLE_SIZE is maximum length of variable value +const MAX_VARIABLE_SIZE = 256 + +// TEARDOWN_TAG is teardown tag const TEARDOWN_TAG = "teardown" // ////////////////////////////////////////////////////////////////////////////////// // @@ -106,7 +109,7 @@ func NewCommand(args []string, line uint16) *Command { // ////////////////////////////////////////////////////////////////////////////////// // // AddCommand appends command to command slice -func (r *Recipe) AddCommand(cmd *Command, tag string, isNested bool) { +func (r *Recipe) AddCommand(cmd *Command, tag string, isNested bool) error { cmd.Recipe = r cmd.Tag = tag @@ -131,15 +134,23 @@ func (r *Recipe) AddCommand(cmd *Command, tag string, isNested bool) { } r.Commands = append(r.Commands, cmd) + + return nil } // AddVariable adds new RO variable -func (r *Recipe) AddVariable(name, value string) { +func (r *Recipe) AddVariable(name, value string) error { if r.variables == nil { r.variables = make(map[string]*Variable) } + if strings.Contains(value, "{"+name+"}") { + return fmt.Errorf("Can't define variable \"%s\": variable contains itself as a part of value", name) + } + r.variables[name] = &Variable{value, true} + + return nil } // SetVariable sets RW variable @@ -219,9 +230,10 @@ func (c Commands) Has(index int) bool { // ////////////////////////////////////////////////////////////////////////////////// // // AddAction appends command to actions slice -func (c *Command) AddAction(action *Action) { +func (c *Command) AddAction(action *Action) error { action.Command = c c.Actions = append(c.Actions, action) + return nil } // GetCmdline returns command line with rendered variables @@ -412,7 +424,11 @@ func renderVars(r *Recipe, data string) string { continue } - data = strings.Replace(data, found[0], varValue, -1) + data = strings.ReplaceAll(data, found[0], varValue) + + if len(data) > MAX_VARIABLE_SIZE { + return data + } } } diff --git a/recipe/recipe_test.go b/recipe/recipe_test.go index d9bcdbb4..6abd5e2a 100644 --- a/recipe/recipe_test.go +++ b/recipe/recipe_test.go @@ -8,6 +8,7 @@ package recipe // ////////////////////////////////////////////////////////////////////////////////// // import ( + "errors" "os" "testing" @@ -66,6 +67,10 @@ func (s *RecipeSuite) TestBasicRecipe(c *C) { c.Assert(r.Commands.Last(), IsNil) + err := r.AddVariable("group", "{group}1") + + c.Assert(err, DeepEquals, errors.New("Can't define variable \"group\": variable contains itself as a part of value")) + c1 := NewCommand([]string{"{user}:echo {service}"}, 0) c2 := NewCommand([]string{"echo ABCD 1.53 4000", "Echo command for service {service}"}, 0) c3 := NewCommand([]string{"echo 1234"}, 0) @@ -342,4 +347,17 @@ func (s *RecipeSuite) TestTags(c *C) { c.Assert(r.HasTeardown(), Equals, false) } +func (s *RecipeSuite) TestNesting(c *C) { + r := NewRecipe("/home/user/test.recipe") + + r.AddVariable("a", "{d}{d}{d}{d}{d}{d}{d}{d}{d}{d}{d}{d}") + r.AddVariable("d", "{a}{a}{a}{a}{a}{a}{a}{a}{a}{a}{a}{a}") + + c1 := NewCommand([]string{"echo 1", "My command {d}"}, 0) + + r.AddCommand(c1, "", false) + + c.Assert(len(c1.Description) < 1024, Equals, true) +} + // ////////////////////////////////////////////////////////////////////////////////// // From ddcf92dacd2c57ca24ec9785e4c72086782e8ac2 Mon Sep 17 00:00:00 2001 From: Anton Novojilov Date: Fri, 19 Nov 2021 03:53:15 +0300 Subject: [PATCH 3/5] Increase max variable value size --- recipe/recipe.go | 2 +- recipe/recipe_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/recipe/recipe.go b/recipe/recipe.go index 187a9437..63949ab4 100644 --- a/recipe/recipe.go +++ b/recipe/recipe.go @@ -22,7 +22,7 @@ import ( const MAX_VAR_NESTING = 32 // MAX_VARIABLE_SIZE is maximum length of variable value -const MAX_VARIABLE_SIZE = 256 +const MAX_VARIABLE_SIZE = 512 // TEARDOWN_TAG is teardown tag const TEARDOWN_TAG = "teardown" diff --git a/recipe/recipe_test.go b/recipe/recipe_test.go index 6abd5e2a..ed6de7d2 100644 --- a/recipe/recipe_test.go +++ b/recipe/recipe_test.go @@ -357,7 +357,7 @@ func (s *RecipeSuite) TestNesting(c *C) { r.AddCommand(c1, "", false) - c.Assert(len(c1.Description) < 1024, Equals, true) + c.Assert(len(c1.Description) < 8192, Equals, true) } // ////////////////////////////////////////////////////////////////////////////////// // From 0d148f8f7555be1b0531bc8af922dfeb352b9d44 Mon Sep 17 00:00:00 2001 From: Anton Novojilov Date: Sun, 21 Nov 2021 01:01:33 +0300 Subject: [PATCH 4/5] Render skipped commands in terminal renderer --- cli/executor/executor.go | 3 ++- recipe/recipe.go | 7 +++++-- render/render.go | 18 ++++++++++++++++++ render/renderer_json.go | 5 +++++ render/renderer_quiet.go | 5 +++++ render/renderer_tap13.go | 5 +++++ render/renderer_terminal.go | 32 ++++++++++++++++++++++++++++++++ render/renderer_xml.go | 5 +++++ 8 files changed, 77 insertions(+), 3 deletions(-) diff --git a/cli/executor/executor.go b/cli/executor/executor.go index 392ca434..f93218f5 100644 --- a/cli/executor/executor.go +++ b/cli/executor/executor.go @@ -204,7 +204,7 @@ func applyRecipeOptions(e *Executor, rr render.Renderer, r *recipe.Recipe) { // processRecipe execute commands in recipe func processRecipe(e *Executor, rr render.Renderer, r *recipe.Recipe, tags []string) { - var lastFailedGroupID uint8 + var lastFailedGroupID uint8 = recipe.MAX_GROUP_ID var finished bool e.start = time.Now() @@ -216,6 +216,7 @@ func processRecipe(e *Executor, rr render.Renderer, r *recipe.Recipe, tags []str } if skipCommand(command, tags, lastFailedGroupID, finished) { + rr.CommandSkipped(command) e.skipped-- continue } diff --git a/recipe/recipe.go b/recipe/recipe.go index 63949ab4..a65a3a3f 100644 --- a/recipe/recipe.go +++ b/recipe/recipe.go @@ -18,11 +18,14 @@ import ( // ////////////////////////////////////////////////////////////////////////////////// // +// MAX_GROUP_ID is maximum group ID +const MAX_GROUP_ID uint8 = 255 + // MAX_VAR_NESTING is maximum variables nesting -const MAX_VAR_NESTING = 32 +const MAX_VAR_NESTING int = 32 // MAX_VARIABLE_SIZE is maximum length of variable value -const MAX_VARIABLE_SIZE = 512 +const MAX_VARIABLE_SIZE int = 512 // TEARDOWN_TAG is teardown tag const TEARDOWN_TAG = "teardown" diff --git a/render/render.go b/render/render.go index 52b62a7b..87c81830 100644 --- a/render/render.go +++ b/render/render.go @@ -15,13 +15,31 @@ import ( // Renderer is interface for renderers type Renderer interface { + // Start prints info about started test Start(r *recipe.Recipe) + + // CommandStarted prints info about started command CommandStarted(c *recipe.Command) + + // CommandSkipped prints info about skipped command + CommandSkipped(c *recipe.Command) + + // CommandFailed prints info about failed command CommandFailed(c *recipe.Command, err error) + + // CommandFailed prints info about executed command CommandDone(c *recipe.Command, isLast bool) + + // ActionStarted prints info about action in progress ActionStarted(a *recipe.Action) + + // ActionFailed prints info about failed action ActionFailed(a *recipe.Action, err error) + + // ActionDone prints info about successfully finished action ActionDone(a *recipe.Action, isLast bool) + + // Result prints info about test results Result(passes, fails int) } diff --git a/render/renderer_json.go b/render/renderer_json.go index 762ff60d..645c42e8 100644 --- a/render/renderer_json.go +++ b/render/renderer_json.go @@ -79,6 +79,11 @@ func (rr *JSONRenderer) CommandStarted(c *recipe.Command) { rr.curCommand = rr.convertCommand(c) } +// CommandSkipped prints info about skipped command +func (rr *JSONRenderer) CommandSkipped(c *recipe.Command) { + return +} + // CommandFailed prints info about failed command func (rr *JSONRenderer) CommandFailed(c *recipe.Command, err error) { rr.curCommand.IsFailed = true diff --git a/render/renderer_quiet.go b/render/renderer_quiet.go index db44125e..d6a71bac 100644 --- a/render/renderer_quiet.go +++ b/render/renderer_quiet.go @@ -28,6 +28,11 @@ func (rr *QuietRenderer) CommandStarted(c *recipe.Command) { return } +// CommandSkipped prints info about skipped command +func (rr *QuietRenderer) CommandSkipped(c *recipe.Command) { + return +} + // CommandFailed prints info about failed command func (rr *QuietRenderer) CommandFailed(c *recipe.Command, err error) { return diff --git a/render/renderer_tap13.go b/render/renderer_tap13.go index 6380176d..5e1acf8a 100644 --- a/render/renderer_tap13.go +++ b/render/renderer_tap13.go @@ -65,6 +65,11 @@ func (rr *TAPRenderer) CommandStarted(c *recipe.Command) { } } +// CommandSkipped prints info about skipped command +func (rr *TAPRenderer) CommandSkipped(c *recipe.Command) { + return +} + // CommandFailed prints info about failed command func (rr *TAPRenderer) CommandFailed(c *recipe.Command, err error) { fmt.Printf("Bail out! %v\n", err) diff --git a/render/renderer_terminal.go b/render/renderer_terminal.go index 3f671ff7..36d73173 100644 --- a/render/renderer_terminal.go +++ b/render/renderer_terminal.go @@ -92,6 +92,38 @@ func (rr *TerminalRenderer) CommandStarted(c *recipe.Command) { fmtc.NewLine() } +// CommandSkipped prints info about skipped command +func (rr *TerminalRenderer) CommandSkipped(c *recipe.Command) { + var info string + + if c.Tag != "" { + info += fmt.Sprintf("(%s) ", c.Tag) + } + + switch { + case c.Cmdline == "-" && c.Description == "": + info += "- Empty command -" + case c.Cmdline == "-" && c.Description != "": + info += c.Description + case c.Cmdline != "-" && c.Description == "": + info += c.Cmdline + case c.Cmdline != "-" && c.Description == "" && c.User != "": + info += fmt.Sprintf("[%s] %s", c.User, c.Cmdline) + case c.Cmdline != "-" && c.Description != "" && c.User != "": + info += fmt.Sprintf("%s → [%s] %s", c.Description, c.User, c.GetCmdline()) + default: + info += fmt.Sprintf("%s → %s", c.Description, c.GetCmdline()) + } + + if fmtc.DisableColors { + fmtc.Printf(" [SKIPPED] %s\n", info) + } else { + fmtc.Printf(" {s-}%s{!}\n", info) + } + + fmtc.NewLine() +} + // CommandFailed prints info about failed command func (rr *TerminalRenderer) CommandFailed(c *recipe.Command, err error) { fmtc.Printf(" {r}%v{!}\n", err) diff --git a/render/renderer_xml.go b/render/renderer_xml.go index 021249b1..b1ed043f 100644 --- a/render/renderer_xml.go +++ b/render/renderer_xml.go @@ -62,6 +62,11 @@ func (rr *XMLRenderer) CommandStarted(c *recipe.Command) { rr.data += " \n" } +// CommandSkipped prints info about skipped command +func (rr *XMLRenderer) CommandSkipped(c *recipe.Command) { + return +} + // CommandFailed prints info about failed command func (rr *XMLRenderer) CommandFailed(c *recipe.Command, err error) { rr.data += " \n" From 37821ac1e1230a01c73c9f94f01256d4658eaadb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=B4=80=C9=B4=E1=B4=9B=E1=B4=8F=C9=B4=20=C9=B4=E1=B4=8F?= =?UTF-8?q?=E1=B4=A0=E1=B4=8F=E1=B4=8A=C9=AA=CA=9F=E1=B4=8F=E1=B4=A0?= Date: Mon, 22 Nov 2021 16:10:16 +0300 Subject: [PATCH 5/5] Update COOKBOOK.md --- COOKBOOK.md | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/COOKBOOK.md b/COOKBOOK.md index f42aef2e..57a7867d 100644 --- a/COOKBOOK.md +++ b/COOKBOOK.md @@ -268,6 +268,8 @@ Executes command. If you want to do some actions and checks without executing an You can execute the command as another user. For using this feature, you should define user name at the start of the command, e.g. `nobody:echo 'ABCD'`. This feature requires that bibop utility was executed with super user privileges (e.g. `root`). +Commands could be combined into groups. By default, every command has its own group. If you want to add a command to the group, use `+` as a prefix (e.g., `+command`). See the example below. If any command from the group fails, all the following commands in the group will be skipped. + You can define tag and execute the command with a tag on demand (using `-t` /` --tag` option of CLI). By default, all commands with tags are ignored. Also, there is a special tag — `teardown`. If a command has this tag, this command will be executed even if `fast-finish` is set to true. @@ -279,7 +281,7 @@ Also, there is a special tag — `teardown`. If a command has this tag, this com * `cmd-line` - Full command with all arguments * `descriprion` - Command description [Optional] -**Example:** +**Examples:** ```yang command "echo 'ABCD'" "Simple echo command" @@ -309,6 +311,38 @@ command:init "my app initdb" "Init database" ``` +```yang +command "-" "Replace configuration file" + backup {redis_config} + copy redis.conf {redis_config} + +command "systemctl start {service_name}" "Start Redis service" + wait {delay} + service-works {service_name} + connect tcp :6379 + ++command "systemctl status {service_name}" "Check status of Redis service" + expect "active (running)" + ++command "systemctl restart {service_name}" "Restart Redis service" + wait {delay} + service-works {service_name} + connect tcp :6379 + ++command "redis-cli CONFIG GET logfile" "Check Redis Client" + exit 0 + output-contains "/var/log/redis/redis.log" + ++command "systemctl stop {service_name}" "Stop Redis service" + wait {delay} + !service-works {service_name} + !connect tcp :6379 + +command "-" "Configuration file restore" + backup-restore {redis_config} + +``` + ### Variables