diff --git a/.travis.yml b/.travis.yml
index 809f0501..8ec7f455 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,8 +1,8 @@
language: go
go:
- - 1.13.x
- 1.14.x
+ - 1.15.x
- tip
os:
@@ -19,7 +19,7 @@ matrix:
- go: tip
env:
- - SHELLCHECK_VERSION=0.7.1 HADOLINT_VERSION=1.17.6
+ - SHELLCHECK_VERSION=0.7.1 HADOLINT_VERSION=1.18.0
before_install:
- wget https://github.com/koalaman/shellcheck/releases/download/v${SHELLCHECK_VERSION}/shellcheck-v${SHELLCHECK_VERSION}.linux.x86_64.tar.xz
diff --git a/COOKBOOK.md b/COOKBOOK.md
index f82f66c9..d33d6071 100644
--- a/COOKBOOK.md
+++ b/COOKBOOK.md
@@ -8,6 +8,7 @@
* [`require-root`](#require-root)
* [`fast-finish`](#fast-finish)
* [`lock-workdir`](#lock-workdir)
+ * [`unbuffer`](#unbuffer)
* [`delay`](#delay)
* [`command`](#command)
* [Variables](#variables)
@@ -17,6 +18,8 @@
* [`wait`](#wait)
* [Input/Output](#inputoutput)
* [`expect`](#expect)
+ * [`expect-stdout`](#expect-stdout)
+ * [`expect-stderr`](#expect-stderr)
* [`print`](#print)
* [`wait-output`](#wait-output)
* [`output-match`](#output-match)
@@ -97,7 +100,7 @@ dir "/home/john"
```
-
+
### Global
@@ -118,7 +121,7 @@ pkg php nginx libhttp2 libhttp2-devel
```
-
+
#### `unsafe-actions`
@@ -137,7 +140,7 @@ unsafe-actions yes
```
-
+
#### `require-root`
@@ -158,7 +161,7 @@ require-root yes
```
-
+
#### `fast-finish`
@@ -177,7 +180,7 @@ fast-finish yes
```
-
+
#### `lock-workdir`
@@ -196,7 +199,26 @@ lock-workdir no
```
-
+
+
+#### `unbuffer`
+
+Disables I/O stream buffering.
+
+**Syntax:** `unbuffer `
+
+**Arguments:**
+
+* `flag` - Flag (_Boolean_) [`no` by default]
+
+**Example:**
+
+```yang
+unbuffer yes
+
+```
+
+
#### `delay`
@@ -215,7 +237,7 @@ delay 1.5
```
-
+
#### `command`
@@ -262,7 +284,7 @@ command:init "my app initdb" "Init database"
```
-
+
### Variables
@@ -302,7 +324,7 @@ command "service start {service}" "Starting service"
```
-
+
### Actions
@@ -339,7 +361,7 @@ command "git clone git@github.com:user/repo.git" "Repository clone"
```
-
+
##### `wait`
@@ -361,7 +383,7 @@ command "echo 'ABCD'" "Simple echo command"
```
-
+
#### Input/Output
@@ -396,7 +418,53 @@ command "echo 'ABCD'" "Simple echo command with 1 seconds timeout"
```
-
+
+
+##### `expect-stdout`
+
+Expects some substring in command [standard output](http://www.linfo.org/standard_output.html).
+
+**Syntax:** `expect-stdout [max-wait]`
+
+**Arguments:**
+
+* `substr` - Substring for search (_String_)
+* `max-wait` - Max wait time in seconds (_Float_) [Optional | 5 seconds]
+
+**Negative form:** No
+
+**Example:**
+
+```yang
+command "myApp 1" "Simple command"
+ expect-stdout "Everything fine"
+
+```
+
+
+
+##### `expect-stderr`
+
+Expects some substring in command [standard error](http://www.linfo.org/standard_error.html).
+
+**Syntax:** `expect-stderr [max-wait]`
+
+**Arguments:**
+
+* `substr` - Substring for search (_String_)
+* `max-wait` - Max wait time in seconds (_Float_) [Optional | 5 seconds]
+
+**Negative form:** No
+
+**Example:**
+
+```yang
+command "myApp ABCD" "Simple command"
+ expect-stderr "Error!"
+
+```
+
+
##### `print`
@@ -418,7 +486,7 @@ command "echo 'ABCD'" "Simple echo command"
```
-
+
##### `wait-output`
@@ -460,7 +528,7 @@ command "echo 'ABCD'" "Simple echo command"
```
-
+
##### `output-contains`
@@ -482,7 +550,7 @@ command "echo 'ABCD'" "Simple echo command"
```
-
+
##### `output-trim`
@@ -500,7 +568,7 @@ command "echo 'ABCD'" "Simple echo command"
```
-
+
#### Filesystem
@@ -527,7 +595,7 @@ command "-" "Check environment"
```
-
+
##### `mode`
@@ -550,7 +618,7 @@ command "-" "Check environment"
```
-
+
##### `owner`
@@ -576,7 +644,7 @@ command "-" "Check environment"
```
-
+
##### `exist`
@@ -598,7 +666,7 @@ command "-" "Check environment"
```
-
+
##### `readable`
@@ -621,7 +689,7 @@ command "-" "Check environment"
```
-
+
##### `writable`
@@ -644,7 +712,7 @@ command "-" "Check environment"
```
-
+
##### `executable`
@@ -667,7 +735,7 @@ command "-" "Check environment"
```
-
+
##### `dir`
@@ -689,7 +757,7 @@ command "-" "Check environment"
```
-
+
##### `empty`
@@ -711,7 +779,7 @@ command "-" "Check environment"
```
-
+
##### `empty-dir`
@@ -733,7 +801,7 @@ command "-" "Check environment"
```
-
+
##### `checksum`
@@ -756,7 +824,7 @@ command "-" "Check environment"
```
-
+
##### `checksum-read`
@@ -779,7 +847,7 @@ command "-" "Check environment"
```
-
+
##### `file-contains`
@@ -802,7 +870,7 @@ command "-" "Check environment"
```
-
+
##### `copy`
@@ -825,7 +893,7 @@ command "-" "Check environment"
```
-
+
##### `move`
@@ -848,7 +916,7 @@ command "-" "Check environment"
```
-
+
##### `touch`
@@ -870,7 +938,7 @@ command "-" "Check environment"
```
-
+
##### `mkdir`
@@ -892,7 +960,7 @@ command "-" "Check environment"
```
-
+
##### `remove`
@@ -916,7 +984,7 @@ command "-" "Check environment"
```
-
+
##### `chmod`
@@ -939,7 +1007,7 @@ command "-" "Check environment"
```
-
+
##### `backup`
@@ -961,7 +1029,7 @@ command "-" "Configure environment"
```
-
+
##### `backup-restore`
@@ -984,7 +1052,7 @@ command "-" "Configure environment"
```
-
+
#### System
@@ -1008,7 +1076,7 @@ command "-" "Check environment"
```
-
+
##### `wait-pid`
@@ -1037,7 +1105,7 @@ command "-" "Check environment"
```
-
+
##### `wait-fs`
@@ -1066,7 +1134,7 @@ command "service myapp start" "Starting MyApp"
```
-
+
##### `connect`
@@ -1091,7 +1159,7 @@ command "-" "Check environment"
```
-
+
##### `app`
@@ -1113,7 +1181,7 @@ command "-" "Check environment"
```
-
+
##### `signal`
@@ -1150,7 +1218,7 @@ command "myapp --daemon" "Check my app"
```
-
+
##### `env`
@@ -1173,7 +1241,7 @@ command "-" "Check environment"
```
-
+
##### `env-set`
@@ -1196,7 +1264,7 @@ command "-" "Prepare environment"
```
-
+
#### Users/Groups
@@ -1220,7 +1288,7 @@ command "-" "Check environment"
```
-
+
##### `user-id`
@@ -1243,7 +1311,7 @@ command "-" "Check environment"
```
-
+
##### `user-gid`
@@ -1266,7 +1334,7 @@ command "-" "Check environment"
```
-
+
##### `user-group`
@@ -1289,7 +1357,7 @@ command "-" "Check environment"
```
-
+
##### `user-shell`
@@ -1312,7 +1380,7 @@ command "-" "Check environment"
```
-
+
##### `user-home`
@@ -1335,7 +1403,7 @@ command "-" "Check environment"
```
-
+
##### `group-exist`
@@ -1357,7 +1425,7 @@ command "-" "Check environment"
```
-
+
##### `group-id`
@@ -1380,7 +1448,7 @@ command "-" "Check environment"
```
-
+
#### Services
@@ -1404,7 +1472,7 @@ command "-" "Check environment"
```
-
+
##### `service-enabled`
@@ -1426,7 +1494,7 @@ command "-" "Check environment"
```
-
+
##### `service-works`
@@ -1448,7 +1516,7 @@ command "-" "Check environment"
```
-
+
#### HTTP
@@ -1481,7 +1549,7 @@ command "-" "Make HTTP request"
```
-
+
##### `http-header`
@@ -1513,7 +1581,7 @@ command "-" "Make HTTP request"
```
-
+
##### `http-contains`
@@ -1538,7 +1606,7 @@ command "-" "Make HTTP request"
```
-
+
##### `http-json`
@@ -1563,7 +1631,7 @@ command "-" "Make HTTP request and check domain info"
```
-
+
##### `http-set-auth`
@@ -1592,7 +1660,7 @@ command "-" "Make HTTP request without auth"
```
-
+
##### `http-set-header`
@@ -1618,7 +1686,7 @@ command "-" "Make HTTP request"
```
-
+
#### Libraries
@@ -1642,7 +1710,7 @@ command "-" "Check environment"
```
-
+
##### `lib-header`
@@ -1664,7 +1732,7 @@ command "-" "Check environment"
```
-
+
##### `lib-config`
@@ -1686,7 +1754,7 @@ command "-" "Check environment"
```
-
+
##### `lib-exist`
@@ -1709,7 +1777,7 @@ command "-" "Check environment"
```
-
+
##### `lib-linked`
@@ -1732,7 +1800,7 @@ command "-" "Check environment"
```
-
+
#### Python
@@ -1756,7 +1824,7 @@ command "-" "Check Python module loading"
```
-
+
##### `python3-module`
@@ -1778,54 +1846,58 @@ command "-" "Check Python 3 module loading"
```
-
+
## Examples
```yang
-# Simple recipe for mkcryptpasswd utility
+# Bibop recipe for MkCryptPasswd
-command "mkcryptpasswd" "Generate basic hash for password"
- expect "Please enter password"
- print "MyPassword1234"
+pkg mkcryptpasswd
+
+fast-finish yes
+
+var password MyPassword1234
+var salt SALT1234
+var salt_length 9
+
+command "mkcryptpasswd -s" "Generate basic hash for password"
+ expect "Please enter password:"
+ print "{password}"
expect "Hash: "
exit 0
-command "mkcryptpasswd -sa SALT1234" "Generate hash for password with predefined salt"
+command "mkcryptpasswd -s -sa {salt}" "Generate hash for password with predefined salt"
expect "Please enter password"
- print "MyPassword1234"
- wait 1
- output-contains "$6$SALT1234$lTxNu4.6r/j81sirgJ.s9ai8AA3tJdp67XBWLFiE10tIharVYtzRJ9eJ9YEtQsiLzVtg94GrXAYjf40pWEEg7/"
+ print "{password}"
+ expect "$6${salt}$lTxNu4.6r/j81sirgJ.s9ai8AA3tJdp67XBWLFiE10tIharVYtzRJ9eJ9YEtQsiLzVtg94GrXAYjf40pWEEg7/"
exit 0
-command "mkcryptpasswd -sa SALT1234 -1" "Generate MD5 hash for password with predefined salt"
+command "mkcryptpasswd -s -sa {salt} -1" "Generate MD5 hash for password with predefined salt"
expect "Please enter password"
- print "MyPassword1234"
- wait 1
- output-contains "$1$SALT1234$zIPLJYODoLlesdP3bf95S1"
+ print "{password}"
+ expect "$1${salt}$zIPLJYODoLlesdP3bf95S1"
exit 0
-command "mkcryptpasswd -sa SALT1234 -5" "Generate SHA256 hash for password with predefined salt"
+command "mkcryptpasswd -s -sa {salt} -5" "Generate SHA256 hash for password with predefined salt"
expect "Please enter password"
- print "MyPassword1234"
- wait 1
- output-contains "$5$SALT1234$HOV.9Dkp4HSDzcfizNDG7x5ST4e74zcezvCJ8BWHuK8"
+ print "{password}"
+ expect "$5${salt}$HOV.9Dkp4HSDzcfizNDG7x5ST4e74zcezvCJ8BWHuK8"
exit 0
-command "mkcryptpasswd -S" "Return error if password is too weak"
+command "mkcryptpasswd -s -S" "Return error if password is too weak"
expect "Please enter password"
print "password"
expect "Password is too weak: it is based on a dictionary word"
print "password"
- wait 0.5
+ expect "Password is too weak: it is based on a dictionary word"
print "password"
- wait 0.5
- exit 1
+ expect "Password is too weak: it is based on a dictionary word"
+ !exit 0
command "mkcryptpasswd --abcd" "Return error about unsupported argument"
expect "Error! You used unsupported argument --abcd. Please check command syntax."
- exit 1
-
+ !exit 0
```
diff --git a/README.md b/README.md
index 853ff3c9..bd9ded62 100644
--- a/README.md
+++ b/README.md
@@ -101,13 +101,15 @@ Usage: bibop {options} recipe
Options
+ --dry-run, -D Parse and validate recipe
+ --list-packages, -L List required packages
--dir, -d dir Path to working directory
+ --path, -p path Path to directory with binaries
--error-dir, -e dir Path to directory for errors data
--tag, -t tag Command tag
--quiet, -q Quiet mode
- --ignore-packages, -ip Skip packages check
- --dry-run, -D Parse and validate recipe
- --list-packages, -L List required packages
+ --ignore-packages, -ip Do not check system for installed packages
+ --no-cleanup, -nl Disable deleting files created during tests
--no-color, -nc Disable colors in output
--help, -h Show this help message
--version, -v Show version
diff --git a/action/io.go b/action/io.go
index 61e61667..3934d5fc 100644
--- a/action/io.go
+++ b/action/io.go
@@ -56,7 +56,7 @@ func Expect(action *recipe.Action, outputStore *output.Store) error {
for range time.NewTicker(_DATA_READ_PERIOD).C {
if bytes.Contains(stdout.Bytes(), []byte(substr)) || bytes.Contains(stderr.Bytes(), []byte(substr)) {
- outputStore.Clear = true
+ outputStore.Purge()
return nil
}
@@ -65,7 +65,87 @@ func Expect(action *recipe.Action, outputStore *output.Store) error {
}
}
- outputStore.Clear = true
+ outputStore.Purge()
+
+ return fmt.Errorf("Timeout (%g sec) reached", timeout)
+}
+
+// ExpectStdout is action processor for "expect-stdout"
+func ExpectStdout(action *recipe.Action, outputStore *output.Store) error {
+ var timeout float64
+
+ substr, err := action.GetS(0)
+
+ if err != nil {
+ return err
+ }
+
+ if action.Has(1) {
+ timeout, err = action.GetF(1)
+
+ if err != nil {
+ return err
+ }
+ } else {
+ timeout = 5.0
+ }
+
+ start := time.Now()
+ timeout = mathutil.BetweenF64(timeout, 0.01, 3600.0)
+ timeoutDur := secondsToDuration(timeout)
+
+ for range time.NewTicker(_DATA_READ_PERIOD).C {
+ if bytes.Contains(outputStore.Stderr.Bytes(), []byte(substr)) {
+ outputStore.Stderr.Purge()
+ return nil
+ }
+
+ if time.Since(start) >= timeoutDur {
+ break
+ }
+ }
+
+ outputStore.Stderr.Purge()
+
+ return fmt.Errorf("Timeout (%g sec) reached", timeout)
+}
+
+// ExpectStderr is action processor for "expect-stderr"
+func ExpectStderr(action *recipe.Action, outputStore *output.Store) error {
+ var timeout float64
+
+ substr, err := action.GetS(0)
+
+ if err != nil {
+ return err
+ }
+
+ if action.Has(1) {
+ timeout, err = action.GetF(1)
+
+ if err != nil {
+ return err
+ }
+ } else {
+ timeout = 5.0
+ }
+
+ start := time.Now()
+ timeout = mathutil.BetweenF64(timeout, 0.01, 3600.0)
+ timeoutDur := secondsToDuration(timeout)
+
+ for range time.NewTicker(_DATA_READ_PERIOD).C {
+ if bytes.Contains(outputStore.Stdout.Bytes(), []byte(substr)) {
+ outputStore.Stdout.Purge()
+ return nil
+ }
+
+ if time.Since(start) >= timeoutDur {
+ break
+ }
+ }
+
+ outputStore.Stdout.Purge()
return fmt.Errorf("Timeout (%g sec) reached", timeout)
}
@@ -106,9 +186,9 @@ func Input(action *recipe.Action, input io.Writer, outputStore *output.Store) er
text = text + "\n"
}
- _, err = input.Write([]byte(text))
+ outputStore.Purge()
- outputStore.Clear = true
+ _, err = input.Write([]byte(text))
return err
}
@@ -159,6 +239,6 @@ func OutputContains(action *recipe.Action, outputStore *output.Store) error {
// OutputTrim is action processor for "output-trim"
func OutputTrim(action *recipe.Action, outputStore *output.Store) error {
- outputStore.Clear = true
+ outputStore.Purge()
return nil
}
diff --git a/cli/cli.go b/cli/cli.go
index 687a057c..ea278ca4 100644
--- a/cli/cli.go
+++ b/cli/cli.go
@@ -34,7 +34,7 @@ import (
// Application info
const (
APP = "bibop"
- VER = "2.4.0"
+ VER = "2.5.0"
DESC = "Utility for testing command-line tools"
)
@@ -42,16 +42,18 @@ const (
// Options
const (
- OPT_DIR = "d:dir"
- OPT_ERROR_DIR = "e:error-dir"
- OPT_TAG = "t:tag"
- OPT_QUIET = "q:quiet"
- OPT_DRY_RUN = "D:dry-run"
- OPT_LIST_PACKAGES = "L:list-packages"
- OPT_NO_CLEANUP = "NC:no-cleanup"
- OPT_NO_COLOR = "nc:no-color"
- OPT_HELP = "h:help"
- OPT_VER = "v:version"
+ OPT_DRY_RUN = "D:dry-run"
+ OPT_LIST_PACKAGES = "L:list-packages"
+ OPT_DIR = "d:dir"
+ OPT_PATH = "p:path"
+ OPT_ERROR_DIR = "e:error-dir"
+ OPT_TAG = "t:tag"
+ OPT_QUIET = "q:quiet"
+ OPT_INGORE_PACKAGES = "ip:ignore-packages"
+ OPT_NO_CLEANUP = "nl:no-cleanup"
+ OPT_NO_COLOR = "nc:no-color"
+ OPT_HELP = "h:help"
+ OPT_VER = "v:version"
OPT_COMPLETION = "completion"
)
@@ -59,16 +61,18 @@ const (
// ////////////////////////////////////////////////////////////////////////////////// //
var optMap = options.Map{
- OPT_DIR: {},
- OPT_ERROR_DIR: {},
- OPT_TAG: {Mergeble: true},
- OPT_QUIET: {Type: options.BOOL},
- OPT_DRY_RUN: {Type: options.BOOL},
- OPT_LIST_PACKAGES: {Type: options.BOOL},
- OPT_NO_CLEANUP: {Type: options.BOOL},
- OPT_NO_COLOR: {Type: options.BOOL},
- OPT_HELP: {Type: options.BOOL, Alias: "u:usage"},
- OPT_VER: {Type: options.BOOL, Alias: "ver"},
+ OPT_DRY_RUN: {Type: options.BOOL},
+ OPT_LIST_PACKAGES: {Type: options.BOOL},
+ OPT_DIR: {},
+ OPT_PATH: {},
+ OPT_ERROR_DIR: {},
+ OPT_TAG: {Mergeble: true},
+ OPT_QUIET: {Type: options.BOOL},
+ OPT_INGORE_PACKAGES: {Type: options.BOOL},
+ OPT_NO_CLEANUP: {Type: options.BOOL},
+ OPT_NO_COLOR: {Type: options.BOOL},
+ OPT_HELP: {Type: options.BOOL, Alias: "u:usage"},
+ OPT_VER: {Type: options.BOOL, Alias: "ver"},
OPT_COMPLETION: {},
}
@@ -126,6 +130,15 @@ func configureUI() {
// configureSubsystems configures bibop subsystems
func configureSubsystems() {
req.Global.SetUserAgent(APP, VER)
+
+ if options.Has(OPT_PATH) {
+ newPath := os.Getenv("PATH") + ":" + options.GetS(OPT_PATH)
+ err := os.Setenv("PATH", newPath)
+
+ if err != nil {
+ printErrorAndExit(err.Error())
+ }
+ }
}
// validateOptions validates options
@@ -199,14 +212,7 @@ func process(file string) {
// validate validates recipe and print validation errors
func validate(e *executor.Executor, r *recipe.Recipe, tags []string) {
- vc := &executor.ValidationConfig{Tags: tags}
-
- if options.GetB(OPT_DRY_RUN) {
- vc.IgnoreDependencies = true
- vc.IgnorePrivileges = true
- }
-
- errs := e.Validate(r, vc)
+ errs := e.Validate(r, getValidationConfig(tags))
if len(errs) == 0 {
if options.GetB(OPT_DRY_RUN) {
@@ -239,6 +245,22 @@ func listPackages(pkgs []string) {
os.Exit(0)
}
+// getValidationConfig generates validation config
+func getValidationConfig(tags []string) *executor.ValidationConfig {
+ vc := &executor.ValidationConfig{Tags: tags}
+
+ if options.GetB(OPT_DRY_RUN) {
+ vc.IgnoreDependencies = true
+ vc.IgnorePrivileges = true
+ }
+
+ if options.GetB(OPT_INGORE_PACKAGES) {
+ vc.IgnoreDependencies = true
+ }
+
+ return vc
+}
+
// printError prints error message to console
func printError(f string, a ...interface{}) {
fmtc.Fprintf(os.Stderr, "{r}"+f+"{!}\n", a...)
@@ -266,12 +288,14 @@ func showUsage() {
func genUsage() *usage.Info {
info := usage.NewInfo("", "recipe")
+ info.AddOption(OPT_DRY_RUN, "Parse and validate recipe")
+ info.AddOption(OPT_LIST_PACKAGES, "List required packages")
info.AddOption(OPT_DIR, "Path to working directory", "dir")
+ info.AddOption(OPT_PATH, "Path to directory with binaries", "path")
info.AddOption(OPT_ERROR_DIR, "Path to directory for errors data", "dir")
info.AddOption(OPT_TAG, "Command tag", "tag")
info.AddOption(OPT_QUIET, "Quiet mode")
- info.AddOption(OPT_DRY_RUN, "Parse and validate recipe")
- info.AddOption(OPT_LIST_PACKAGES, "List required packages")
+ info.AddOption(OPT_INGORE_PACKAGES, "Do not check system for installed packages")
info.AddOption(OPT_NO_CLEANUP, "Disable deleting files created during tests")
info.AddOption(OPT_NO_COLOR, "Disable colors in output")
info.AddOption(OPT_HELP, "Show this help message")
diff --git a/cli/executor/executor.go b/cli/executor/executor.go
index b973239a..c3c9bda1 100644
--- a/cli/executor/executor.go
+++ b/cli/executor/executor.go
@@ -37,7 +37,7 @@ import (
// ////////////////////////////////////////////////////////////////////////////////// //
-const MAX_STORAGE_SIZE = 2 * 1024 * 1024 // 2 MB
+const MAX_STORAGE_SIZE = 8 * 1024 * 1024 // 2 MB
// ////////////////////////////////////////////////////////////////////////////////// //
@@ -201,10 +201,6 @@ func processRecipe(e *Executor, r *recipe.Recipe, tags []string) {
printCommandHeader(e, command)
ok := runCommand(e, command)
- if index+1 != len(r.Commands) && !e.config.Quiet {
- fmtc.NewLine()
- }
-
e.skipped--
if !ok {
@@ -217,6 +213,10 @@ func processRecipe(e *Executor, r *recipe.Recipe, tags []string) {
e.passes++
}
+ if index+1 != len(r.Commands) && !e.config.Quiet {
+ fmtc.NewLine()
+ }
+
if r.Delay > 0 {
time.Sleep(timeutil.SecondsToDuration(r.Delay))
}
@@ -282,24 +282,21 @@ func runCommand(e *Executor, c *recipe.Command) bool {
// execCommand executes command
func execCommand(c *recipe.Command, outputStore *output.Store) (*exec.Cmd, io.Writer, error) {
- var cmd *exec.Cmd
+ cmd, err := createCommand(c)
- if c.User == "" {
- cmdArgs := c.GetCmdlineArgs()
- cmd = exec.Command(cmdArgs[0], cmdArgs[1:]...)
- } else {
- if !system.IsUserExist(c.User) {
- return nil, nil, fmt.Errorf("Can't execute the command: user %s doesn't exist on the system", c.User)
- }
-
- cmd = exec.Command("/sbin/runuser", "-s", "/bin/bash", c.User, "-c", c.GetCmdline())
+ if err != nil {
+ return nil, nil, err
}
- input, _ := cmd.StdinPipe()
+ input, err := cmd.StdinPipe()
+
+ if err != nil {
+ return nil, nil, err
+ }
connectOutputStore(cmd, outputStore)
- err := cmd.Start()
+ err = cmd.Start()
if err != nil {
return nil, nil, err
@@ -310,6 +307,33 @@ func execCommand(c *recipe.Command, outputStore *output.Store) (*exec.Cmd, io.Wr
return cmd, input, nil
}
+// createCommand creates command
+func createCommand(c *recipe.Command) (*exec.Cmd, error) {
+ var cmdSlice []string
+
+ if c.User != "" {
+ if !system.IsUserExist(c.User) {
+ return nil, fmt.Errorf("Can't execute the command: user %s doesn't exist on the system", c.User)
+ }
+
+ cmdSlice = append(cmdSlice, "/sbin/runuser", "-s", "/bin/bash", c.User, "-c")
+
+ if c.Recipe.Unbuffer {
+ cmdSlice = append(cmdSlice, "stdbuf -o0 -e0 -i0 "+c.GetCmdline())
+ } else {
+ cmdSlice = append(cmdSlice, c.GetCmdline())
+ }
+ } else {
+ if c.Recipe.Unbuffer {
+ cmdSlice = append(cmdSlice, "stdbuf", "-o0", "-e0", "-i0")
+ }
+
+ cmdSlice = append(cmdSlice, c.GetCmdlineArgs()...)
+ }
+
+ return exec.Command(cmdSlice[0], cmdSlice[1:]...), nil
+}
+
// printBasicRecipeInfo print path to recipe and working dir
func printBasicRecipeInfo(e *Executor, r *recipe.Recipe) {
if e.config.Quiet {
@@ -328,6 +352,7 @@ func printBasicRecipeInfo(e *Executor, r *recipe.Recipe) {
printRecipeOptionFlag("Require root", r.RequireRoot)
printRecipeOptionFlag("Fast finish", r.FastFinish)
printRecipeOptionFlag("Lock workdir", r.LockWorkdir)
+ printRecipeOptionFlag("Unbuffered IO", r.Unbuffer)
}
// printResultInfo print info about finished test
@@ -411,6 +436,10 @@ func runAction(a *recipe.Action, cmd *exec.Cmd, input io.Writer, outputStore *ou
return action.Exit(a, cmd)
case recipe.ACTION_EXPECT:
return action.Expect(a, outputStore)
+ case recipe.ACTION_EXPECT_STDOUT:
+ return action.ExpectStdout(a, outputStore)
+ case recipe.ACTION_EXPECT_STDERR:
+ return action.ExpectStderr(a, outputStore)
case recipe.ACTION_PRINT:
return action.Input(a, input, outputStore)
case recipe.ACTION_WAIT_OUTPUT:
@@ -443,20 +472,25 @@ func connectOutputStore(cmd *exec.Cmd, outputStore *output.Store) {
stdoutReader, _ := cmd.StdoutPipe()
stderrReader, _ := cmd.StderrPipe()
- go func(stdout, stderr io.Reader, outputStore *output.Store) {
- for {
- if outputStore.Clear {
- outputStore.Purge()
- }
+ go outputIOLoop(cmd, stdoutReader, outputStore.Stdout)
+ go outputIOLoop(cmd, stderrReader, outputStore.Stderr)
+}
- outputStore.Stdout.Write(ioutil.ReadAll(stdout))
- outputStore.Stderr.Write(ioutil.ReadAll(stderr))
+// outputIOLoop reads data from reader and writes it to output store container
+func outputIOLoop(cmd *exec.Cmd, r io.Reader, c *output.Container) {
+ buf := make([]byte, 16384)
- if cmd.ProcessState != nil && cmd.ProcessState.Exited() {
- return
- }
+ for {
+ n, _ := r.Read(buf[:cap(buf)])
+
+ if n > 0 {
+ c.Write(buf[:n])
}
- }(stdoutReader, stderrReader, outputStore)
+
+ if cmd.ProcessState != nil && cmd.ProcessState.Exited() {
+ return
+ }
+ }
}
// formatActionName format action name
diff --git a/cli/executor/validators.go b/cli/executor/validators.go
index 4de7f4b7..6aed5cc3 100644
--- a/cli/executor/validators.go
+++ b/cli/executor/validators.go
@@ -132,7 +132,7 @@ func checkPackages(r *recipe.Recipe) []error {
}
switch systemInfo.Distribution {
- case system.LINUX_CENTOS:
+ case system.LINUX_CENTOS, system.LINUX_RHEL, system.LINUX_FEDORA:
return checkRPMPackages(r.Packages)
default:
return nil
diff --git a/output/output_store.go b/output/output_store.go
index 6b86ad12..563e1285 100644
--- a/output/output_store.go
+++ b/output/output_store.go
@@ -18,7 +18,6 @@ import (
type Store struct {
Stdout *Container
Stderr *Container
- Clear bool
}
type Container struct {
@@ -37,14 +36,13 @@ func NewStore(size int) *Store {
return &Store{
&Container{size: size},
&Container{size: size},
- false,
}
}
// ////////////////////////////////////////////////////////////////////////////////// //
// Write writes data into buffer
-func (c *Container) Write(data []byte, _ error) {
+func (c *Container) Write(data []byte) {
if len(data) == 0 {
return
}
diff --git a/parser/parser.go b/parser/parser.go
index 56dcb415..bcf176f2 100644
--- a/parser/parser.go
+++ b/parser/parser.go
@@ -208,6 +208,9 @@ func applyGlobalOptions(r *recipe.Recipe, e *entity, line uint16) error {
case recipe.OPTION_LOCK_WORKDIR:
r.LockWorkdir, err = getOptionBoolValue(e.info.Keyword, e.args[0])
+ case recipe.OPTION_UNBUFFER:
+ r.Unbuffer, err = getOptionBoolValue(e.info.Keyword, e.args[0])
+
case recipe.OPTION_DELAY:
r.Delay, err = getOptionFloatValue(e.info.Keyword, e.args[0])
}
diff --git a/parser/parser_test.go b/parser/parser_test.go
index 91e448b0..050b9cb2 100644
--- a/parser/parser_test.go
+++ b/parser/parser_test.go
@@ -81,6 +81,7 @@ func (s *ParseSuite) TestBasicParsing(c *C) {
c.Assert(recipe.RequireRoot, Equals, true)
c.Assert(recipe.FastFinish, Equals, true)
c.Assert(recipe.LockWorkdir, Equals, false)
+ c.Assert(recipe.Unbuffer, Equals, true)
c.Assert(recipe.Delay, Equals, 1.23)
c.Assert(recipe.Commands, HasLen, 2)
c.Assert(recipe.Packages, DeepEquals, []string{"package1", "package2"})
diff --git a/recipe/recipe.go b/recipe/recipe.go
index 58978468..f504bbb7 100644
--- a/recipe/recipe.go
+++ b/recipe/recipe.go
@@ -31,6 +31,7 @@ type Recipe struct {
RequireRoot bool // Require root privileges
FastFinish bool // Fast finish flag
LockWorkdir bool // Locking workdir flag
+ Unbuffer bool // Disabled IO buffering
Delay float64 // Delay between commands
Packages []string // Package list
Commands []*Command // Commands
diff --git a/recipe/tokens.go b/recipe/tokens.go
index 6c634114..03632258 100644
--- a/recipe/tokens.go
+++ b/recipe/tokens.go
@@ -16,12 +16,15 @@ const (
OPTION_REQUIRE_ROOT = "require-root"
OPTION_FAST_FINISH = "fast-finish"
OPTION_LOCK_WORKDIR = "lock-workdir"
+ OPTION_UNBUFFER = "unbuffer"
OPTION_DELAY = "delay"
ACTION_EXIT = "exit"
ACTION_WAIT = "wait"
ACTION_EXPECT = "expect"
+ ACTION_EXPECT_STDOUT = "expect-stdout"
+ ACTION_EXPECT_STDERR = "expect-stderr"
ACTION_WAIT_OUTPUT = "wait-output"
ACTION_OUTPUT_MATCH = "output-match"
ACTION_OUTPUT_CONTAINS = "output-contains"
@@ -115,12 +118,15 @@ var Tokens = []TokenInfo{
{OPTION_REQUIRE_ROOT, 1, 1, true, false},
{OPTION_FAST_FINISH, 1, 1, true, false},
{OPTION_LOCK_WORKDIR, 1, 1, true, false},
+ {OPTION_UNBUFFER, 1, 1, true, false},
{OPTION_DELAY, 1, 1, true, false},
{ACTION_EXIT, 1, 2, false, true},
{ACTION_WAIT, 1, 1, false, false},
{ACTION_EXPECT, 1, 2, false, false},
+ {ACTION_EXPECT_STDOUT, 1, 2, false, false},
+ {ACTION_EXPECT_STDERR, 1, 2, false, false},
{ACTION_WAIT_OUTPUT, 1, 1, false, false},
{ACTION_OUTPUT_MATCH, 1, 1, false, true},
{ACTION_OUTPUT_CONTAINS, 1, 1, false, true},
diff --git a/testdata/test1.recipe b/testdata/test1.recipe
index 458ba9ef..b1bc7b5f 100644
--- a/testdata/test1.recipe
+++ b/testdata/test1.recipe
@@ -6,6 +6,7 @@ unsafe-actions yes
require-root no
fast-finish yes
lock-workdir no
+unbuffer yes
delay 1.23
var user nobody