Repository: SimonBaeumer/commander
Pull request #71 (2019-04-09 11:34:46)

Add retry interval
It is now possible to define intervals between retry attempts.

```yaml
config:
  retries: 2
  interval: 50ms
```

codeclimate[bot]: Code Climate has analyzed commit 4a7aed9b and detected **0 issues** on this pull request.

The test coverage on the diff in this pull request is **100.0%** (**50%** is the threshold).

This pull request will bring the total coverage in the repository to **92.1%** (**0.0%** change).

View more on [Code Climate](https://codeclimate.com/github/SimonBaeumer/commander/pull/71).

codeclimate[bot]: Code Climate has analyzed commit 8b22b8ba and detected **0 issues** on this pull request.

The test coverage on the diff in this pull request is **100.0%** (**50%** is the threshold).

This pull request will bring the total coverage in the repository to **92.4%** (**0.1%** change).

View more on [Code Climate](https://codeclimate.com/github/SimonBaeumer/commander/pull/71).


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 39b0113..e77c675 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,7 @@
+# v1.2.0
+
+ - Add `interval` option for `retries` which allows to execute a retry after a given period of time. I.e. `interval: 50ms`
+
 # v1.1.0
 
  - Add `not-contains` assertion on `stdout` and `stderr`
diff --git a/commander_unix.yaml b/commander_unix.yaml
index 6cee58d..5f11da6 100644
--- a/commander_unix.yaml
+++ b/commander_unix.yaml
@@ -52,5 +52,7 @@ tests:
     stdout:
       contains:
         - ✗ echo hello, retries 3
-        - ✓ ./integration/unix/_fixtures/retries.sh, retries 2
+        - ✓ it should retry failed commands, retries 2
+        - ✗ it should retry failed commands with an interval, retries 2
+        - "Duration: 0.1" # Assertion that the interval is working
     exit-code: 1
\ No newline at end of file
diff --git a/docs/manual.md b/docs/manual.md
index f355a36..f5d8e5c 100644
--- a/docs/manual.md
+++ b/docs/manual.md
@@ -68,5 +68,6 @@ tests:
                 ANOTHER: yeah # Add another env variable
             timeout: 1000 # Overwrite timeout
             retries: 5
+            interval: 30ms
         exit-code: 0
 ```
\ No newline at end of file
diff --git a/integration/unix/retries.yaml b/integration/unix/retries.yaml
index 41ed929..e3ecb75 100644
--- a/integration/unix/retries.yaml
+++ b/integration/unix/retries.yaml
@@ -4,7 +4,16 @@ tests:
     config:
       retries: 3
 
-  ./integration/unix/_fixtures/retries.sh:
+  it should retry failed commands:
+    command: ./integration/unix/_fixtures/retries.sh
     exit-code: 0
     config:
+      retries: 2
+
+  it should retry failed commands with an interval:
+    command: echo hello
+    stdout: fail
+    exit-code: 0
+    config:
+      interval: 50ms
       retries: 2
\ No newline at end of file
diff --git a/pkg/runtime/runtime.go b/pkg/runtime/runtime.go
index f52d1b0..29b2d2d 100644
--- a/pkg/runtime/runtime.go
+++ b/pkg/runtime/runtime.go
@@ -1,11 +1,13 @@
 package runtime
 
 import (
+	"fmt"
 	"github.com/SimonBaeumer/commander/pkg/cmd"
 	"log"
 	"runtime"
 	"strings"
 	"sync"
+	"time"
 )
 
 // Constants for defining the various tested properties
@@ -36,10 +38,11 @@ type TestCase struct {
 
 //TestConfig represents the configuration for a test
 type TestConfig struct {
-	Env     map[string]string
-	Dir     string
-	Timeout string
-	Retries int
+	Env      map[string]string
+	Dir      string
+	Timeout  string
+	Retries  int
+	Interval string
 }
 
 // ResultStatus represents the status code of a test result
@@ -74,11 +77,12 @@ type ExpectedOut struct {
 
 // CommandUnderTest represents the command under test
 type CommandUnderTest struct {
-	Cmd     string
-	Env     map[string]string
-	Dir     string
-	Timeout string
-	Retries int
+	Cmd      string
+	Env      map[string]string
+	Dir      string
+	Timeout  string
+	Retries  int
+	Interval string
 }
 
 // TestResult represents the TestCase and the ValidationResult
@@ -120,6 +124,8 @@ func Start(tests []TestCase, maxConcurrent int) <-chan TestResult {
 					if result.ValidationResult.Success {
 						break
 					}
+
+					executeRetryInterval(t)
 				}
 
 				out <- result
@@ -135,6 +141,16 @@ func Start(tests []TestCase, maxConcurrent int) <-chan TestResult {
 	return out
 }
 
+func executeRetryInterval(t TestCase) {
+	if t.Command.GetRetries() > 1 && t.Command.Interval != "" {
+		interval, err := time.ParseDuration(t.Command.Interval)
+		if err != nil {
+			panic(fmt.Sprintf("'%s' interval error: %s", t.Command.Cmd, err))
+		}
+		time.Sleep(interval)
+	}
+}
+
 // runTest executes the current test case
 func runTest(test TestCase) TestResult {
 	// cut = command under test
diff --git a/pkg/suite/yaml_suite.go b/pkg/suite/yaml_suite.go
index 9f367ca..1de03a4 100644
--- a/pkg/suite/yaml_suite.go
+++ b/pkg/suite/yaml_suite.go
@@ -15,10 +15,11 @@ type YAMLConfig struct {
 
 // YAMLTestConfig is a struct to represent the test config
 type YAMLTestConfig struct {
-	Env     map[string]string `yaml:"env,omitempty"`
-	Dir     string            `yaml:"dir,omitempty"`
-	Timeout string            `yaml:"timeout,omitempty"`
-	Retries int               `yaml:"retries,omitempty"`
+	Env      map[string]string `yaml:"env,omitempty"`
+	Dir      string            `yaml:"dir,omitempty"`
+	Timeout  string            `yaml:"timeout,omitempty"`
+	Retries  int               `yaml:"retries,omitempty"`
+	Interval string            `yaml:"interval,omitempty"`
 }
 
 // YAMLTest represents a test in the yaml test suite
@@ -69,10 +70,11 @@ func ParseYAML(content []byte) Suite {
 	return YAMLSuite{
 		TestCases: convertYAMLConfToTestCases(yamlConfig),
 		Config: runtime.TestConfig{
-			Env:     yamlConfig.Config.Env,
-			Dir:     yamlConfig.Config.Dir,
-			Timeout: yamlConfig.Config.Timeout,
-			Retries: yamlConfig.Config.Retries,
+			Env:      yamlConfig.Config.Env,
+			Dir:      yamlConfig.Config.Dir,
+			Timeout:  yamlConfig.Config.Timeout,
+			Retries:  yamlConfig.Config.Retries,
+			Interval: yamlConfig.Config.Interval,
 		},
 	}
 }
@@ -84,11 +86,12 @@ func convertYAMLConfToTestCases(conf YAMLConfig) []runtime.TestCase {
 		tests = append(tests, runtime.TestCase{
 			Title: t.Title,
 			Command: runtime.CommandUnderTest{
-				Cmd:     t.Command,
-				Env:     t.Config.Env,
-				Dir:     t.Config.Dir,
-				Timeout: t.Config.Timeout,
-				Retries: t.Config.Retries,
+				Cmd:      t.Command,
+				Env:      t.Config.Env,
+				Dir:      t.Config.Dir,
+				Timeout:  t.Config.Timeout,
+				Retries:  t.Config.Retries,
+				Interval: t.Config.Interval,
 			},
 			Expected: runtime.Expected{
 				ExitCode: t.ExitCode,
@@ -140,10 +143,11 @@ func (y *YAMLConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
 
 	//Parse global configuration
 	y.Config = YAMLTestConfig{
-		Env:     params.Config.Env,
-		Dir:     params.Config.Dir,
-		Timeout: params.Config.Timeout,
-		Retries: params.Config.Retries,
+		Env:      params.Config.Env,
+		Dir:      params.Config.Dir,
+		Timeout:  params.Config.Timeout,
+		Retries:  params.Config.Retries,
+		Interval: params.Config.Interval,
 	}
 
 	return nil
@@ -239,6 +243,10 @@ func (y *YAMLConfig) mergeConfigs(local YAMLTestConfig, global YAMLTestConfig) Y
 		conf.Retries = local.Retries
 	}
 
+	if local.Interval != "" {
+		conf.Interval = local.Interval
+	}
+
 	return conf
 }
 


Test patch:
diff --git a/pkg/runtime/runtime_test.go b/pkg/runtime/runtime_test.go
index baba5a7..1f69c00 100644
--- a/pkg/runtime/runtime_test.go
+++ b/pkg/runtime/runtime_test.go
@@ -5,6 +5,7 @@ import (
 	"github.com/stretchr/testify/assert"
 	"runtime"
 	"testing"
+	"time"
 )
 
 const SingleConcurrent = 1
@@ -37,7 +38,29 @@ func TestRuntime_WithRetries(t *testing.T) {
 		assert.False(t, r.ValidationResult.Success)
 		assert.Equal(t, 3, r.Tries)
 	}
+
+	assert.Equal(t, 1, counter)
+}
+
+func TestRuntime_WithRetriesAndInterval(t *testing.T) {
+	s := getExampleTestSuite()
+	s[0].Command.Retries = 3
+	s[0].Command.Cmd = "echo fail"
+	s[0].Command.Interval = "50ms"
+
+	start := time.Now()
+	got := Start(s, 1)
+
+	var counter = 0
+	for r := range got {
+		counter++
+		assert.False(t, r.ValidationResult.Success)
+		assert.Equal(t, 3, r.Tries)
+	}
+	duration := time.Since(start)
+
 	assert.Equal(t, 1, counter)
+	assert.True(t, duration.Seconds() > 0.15, "Retry interval did not work")
 }
 
 func TestRuntime_WithEnvVariables(t *testing.T) {
diff --git a/pkg/suite/yaml_suite_test.go b/pkg/suite/yaml_suite_test.go
index d176faa..e004f1f 100644
--- a/pkg/suite/yaml_suite_test.go
+++ b/pkg/suite/yaml_suite_test.go
@@ -184,6 +184,7 @@ config:
     dir: /home/commander/
     timeout: 10ms
     retries: 2
+    interval: 500ms
 
 tests:
     echo hello:
@@ -194,6 +195,7 @@ tests:
            dir: /home/test
            timeout: 1s
            retries: 10
+           interval: 5s
 `)
 
 	got := ParseYAML(yaml)
@@ -201,11 +203,13 @@ tests:
 	assert.Equal(t, "/home/commander/", got.GetGlobalConfig().Dir)
 	assert.Equal(t, "10ms", got.GetGlobalConfig().Timeout)
 	assert.Equal(t, 2, got.GetGlobalConfig().Retries)
+	assert.Equal(t, "500ms", got.GetGlobalConfig().Interval)
 
 	assert.Equal(t, map[string]string{"KEY": "local", "ANOTHER_KEY": "another_global"}, got.GetTests()[0].Command.Env)
 	assert.Equal(t, "/home/test", got.GetTests()[0].Command.Dir)
 	assert.Equal(t, "1s", got.GetTests()[0].Command.Timeout)
 	assert.Equal(t, 10, got.GetTests()[0].Command.Retries)
+	assert.Equal(t, "5s", got.GetTests()[0].Command.Interval)
 }
 
 func TestYAMLSuite_ShouldThrowAnErrorIfFieldIsNotRegistered(t *testing.T) {