Repository: SimonBaeumer/commander
Pull request #23 (2019-03-16 10:25:27)

Refactoring cmd/command


Patch:
diff --git a/.travis.yml b/.travis.yml
index d64e07f..f93b82b 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -21,6 +21,8 @@ services:
 before_install:
   - go get -u golang.org/x/lint/golint
   - make deps
+  - curl -L https://github.com/SimonBaeumer/commander/releases/download/v0.1.0/commander-linux-amd64 -o ~/bin/commander
+  - chmod +x ~/bin/commander
 
 jobs:
   include:
diff --git a/Makefile b/Makefile
index 411ed72..6daa7e4 100644
--- a/Makefile
+++ b/Makefile
@@ -26,7 +26,7 @@ test-coverage: build
 
 test-integration: build
 	$(info INFO: Starting build $@)
-	./commander test
+	commander test
 
 release-amd64:
 	$(info INFO: Starting build $@)
diff --git a/README.md b/README.md
index bc70bfb..578d9fa 100644
--- a/README.md
+++ b/README.md
@@ -2,6 +2,7 @@
 [![Go Report Card](https://goreportcard.com/badge/github.com/SimonBaeumer/commander)](https://goreportcard.com/report/github.com/SimonBaeumer/commander)
 [![Maintainability](https://api.codeclimate.com/v1/badges/cc848165784e0f809a51/maintainability)](https://codeclimate.com/github/SimonBaeumer/commander/maintainability)
 [![Test Coverage](https://api.codeclimate.com/v1/badges/cc848165784e0f809a51/test_coverage)](https://codeclimate.com/github/SimonBaeumer/commander/test_coverage)
+[![Github All Releases](https://img.shields.io/github/downloads/SimonBaeumer/commander/total.svg)](https://github.com/SimonBaeumer/commander/releases)
 
 # Commander
 
diff --git a/cmd/commander/commander.go b/cmd/commander/commander.go
index 69850fe..eb17038 100644
--- a/cmd/commander/commander.go
+++ b/cmd/commander/commander.go
@@ -6,7 +6,7 @@ import (
 	"log"
 	"os"
 	"github.com/urfave/cli"
-	"github.com/SimonBaeumer/commander/pkg"
+	"github.com/SimonBaeumer/commander/pkg/output"
 	"github.com/SimonBaeumer/commander/pkg/runtime"
 	"github.com/SimonBaeumer/commander/pkg/suite"
 )
@@ -78,7 +78,8 @@ func testCommand(c *cli.Context) {
 	}
 
 	results := runtime.Start(tests)
-	if !commander.Start(results) {
+	out := output.NewCliOutput()
+	if !out.Start(results) {
 		os.Exit(1)
 	}
 }
diff --git a/commander.yaml b/commander.yaml
index c9506ee..78c3243 100644
--- a/commander.yaml
+++ b/commander.yaml
@@ -26,4 +26,12 @@ tests:
       contains:
         - ✓ cat ./integration/_fixtures/big_out.txt
         - "Count: 1, Failed: 0"
+    exit-code: 0
+
+  test global and local configurations:
+    command: ./commander test ./integration/config_test.yaml
+    stdout:
+      contains:
+        - ✓ should print global env value
+        - ✓ should print local env value
     exit-code: 0
\ No newline at end of file
diff --git a/pkg/cmd/command.go b/pkg/cmd/command.go
index 3ecbd55..184d0e9 100644
--- a/pkg/cmd/command.go
+++ b/pkg/cmd/command.go
@@ -7,6 +7,7 @@ import (
     "syscall"
 )
 
+//Command represents a single command which can be executed
 type Command struct {
     Cmd      string
     Env      []string
@@ -17,38 +18,44 @@ type Command struct {
     exitCode int
 }
 
+//NewCommand creates a new command
 func NewCommand(cmd string) *Command {
     return &Command{
-        Cmd: cmd,
+        Cmd:      cmd,
         executed: false,
     }
 }
 
+//Stdout returns the output to stdout
 func (c *Command) Stdout() string {
-    c.evaluateExecuted("Stdout")
+    c.isExecuted("Stdout")
     return c.stdout
 }
 
+//Stderr returns the output to stderr
 func (c *Command) Stderr() string {
-    c.evaluateExecuted("Stderr")
+    c.isExecuted("Stderr")
     return c.stderr
 }
 
+//ExitCode returns the exit code of the command
 func (c *Command) ExitCode() int {
-    c.evaluateExecuted("ExitCode")
+    c.isExecuted("ExitCode")
     return c.exitCode
 }
 
+//Executed returns if the command was already executed
 func (c *Command) Executed() bool {
     return c.executed
 }
 
-func (c *Command) evaluateExecuted(property string) {
+func (c *Command) isExecuted(property string) {
     if !c.executed {
         panic("Can not read " + property + " if command was not executed.")
     }
 }
 
+//Execute executes the command
 func (c *Command) Execute() error {
     cmd := exec.Command("sh", "-c", c.Cmd)
     cmd.Env = c.Env
diff --git a/pkg/commander.go b/pkg/commander.go
deleted file mode 100644
index 14e4ba4..0000000
--- a/pkg/commander.go
+++ /dev/null
@@ -1,61 +0,0 @@
-package commander
-
-import (
-    "fmt"
-    "time"
-
-    "github.com/SimonBaeumer/commander/pkg/runtime"
-    "github.com/logrusorgru/aurora"
-)
-
-var au aurora.Aurora
-
-func Start(results <-chan runtime.TestResult) bool {
-    au = aurora.NewAurora(true)
-    failed := 0
-    testResults := []runtime.TestResult{}
-    start := time.Now()
-
-    for r := range results {
-        testResults = append(testResults, r)
-        if r.ValidationResult.Success {
-            fmt.Println("✓ " + r.TestCase.Title)
-        }
-
-        if !r.ValidationResult.Success {
-            failed++
-            fmt.Println(au.Red("✗ " + r.TestCase.Title))
-        }
-    }
-
-    duration := time.Since(start)
-
-    if failed > 0 {
-        printFailures(testResults)
-    }
-
-    fmt.Println("")
-    fmt.Println(fmt.Sprintf("Duration: %.3fs", duration.Seconds()))
-    summary := fmt.Sprintf("Count: %d, Failed: %d", len(testResults), failed)
-    if failed > 0 {
-        fmt.Println(au.Red(summary))
-    } else {
-        fmt.Println(au.Green(summary))
-    }
-
-    return failed == 0
-}
-
-func printFailures(results []runtime.TestResult) {
-    fmt.Println("")
-    fmt.Println(au.Bold("Results"))
-    fmt.Println(au.Bold(""))
-
-    for _, r := range results {
-        if !r.ValidationResult.Success {
-            fmt.Println(au.Bold(au.Red("✗ " + r.TestCase.Title + ", on property '" + r.FailedProperty + "'")))
-            fmt.Println(r.ValidationResult.Diff)
-        }
-    }
-
-}
diff --git a/pkg/output/cli.go b/pkg/output/cli.go
new file mode 100644
index 0000000..ce75547
--- /dev/null
+++ b/pkg/output/cli.go
@@ -0,0 +1,79 @@
+package output
+
+import (
+    "fmt"
+    "io"
+    "log"
+    "os"
+    "time"
+
+    "github.com/SimonBaeumer/commander/pkg/runtime"
+    "github.com/logrusorgru/aurora"
+)
+
+var au aurora.Aurora
+
+type OutputWriter struct {
+    out io.Writer
+}
+
+func NewCliOutput() OutputWriter {
+    return OutputWriter{
+        out: os.Stdout,
+    }
+}
+
+func (w *OutputWriter) Start(results <-chan runtime.TestResult) bool {
+    au = aurora.NewAurora(true)
+    failed := 0
+    testResults := []runtime.TestResult{}
+    start := time.Now()
+
+    for r := range results {
+        testResults = append(testResults, r)
+        if r.ValidationResult.Success {
+            w.fprintf("✓ " + r.TestCase.Title)
+        }
+
+        if !r.ValidationResult.Success {
+            failed++
+            w.fprintf(au.Red("✗ " + r.TestCase.Title))
+        }
+    }
+
+    duration := time.Since(start)
+
+    if failed > 0 {
+        w.printFailures(testResults)
+    }
+
+    w.fprintf("")
+    w.fprintf(fmt.Sprintf("Duration: %.3fs", duration.Seconds()))
+    summary := fmt.Sprintf("Count: %d, Failed: %d", len(testResults), failed)
+    if failed > 0 {
+        w.fprintf(au.Red(summary))
+    } else {
+        w.fprintf(au.Green(summary))
+    }
+
+    return failed == 0
+}
+
+func (w *OutputWriter) printFailures(results []runtime.TestResult) {
+    w.fprintf("")
+    w.fprintf(au.Bold("Results"))
+    w.fprintf(au.Bold(""))
+
+    for _, r := range results {
+        if !r.ValidationResult.Success {
+            w.fprintf(au.Bold(au.Red("✗ " + r.TestCase.Title + ", on property '" + r.FailedProperty + "'")))
+            w.fprintf(r.ValidationResult.Diff)
+        }
+    }
+}
+
+func (w *OutputWriter) fprintf(a ...interface{}) {
+    if _, err := fmt.Fprintln(w.out, a...); err != nil {
+        log.Fatal(err)
+    }
+}
diff --git a/pkg/runtime/runtime.go b/pkg/runtime/runtime.go
index cb09f69..3511b80 100644
--- a/pkg/runtime/runtime.go
+++ b/pkg/runtime/runtime.go
@@ -30,6 +30,12 @@ type TestCase struct {
     Result   CommandResult
 }
 
+//Config
+type TestConfig struct {
+    Env []string
+    Dir string
+}
+
 // ResultStatus represents the status code of a test result
 type ResultStatus int
 
@@ -107,6 +113,8 @@ func Start(tests []TestCase) <-chan TestResult {
 func runTest(test TestCase) TestResult {
     // cut = command under test
     cut := cmd.NewCommand(test.Command.Cmd)
+    cut.Env = test.Command.Env
+    cut.Dir = test.Command.Dir
 
     if err := cut.Execute(); err != nil {
         log.Fatal(err)
diff --git a/pkg/suite/suite.go b/pkg/suite/suite.go
index 246f1fd..d8be619 100644
--- a/pkg/suite/suite.go
+++ b/pkg/suite/suite.go
@@ -5,4 +5,5 @@ import "github.com/SimonBaeumer/commander/pkg/runtime"
 type Suite interface {
 	GetTests()					 []runtime.TestCase
 	GetTestByTitle(title string) (runtime.TestCase, error)
+	GetGlobalConfig()            runtime.TestConfig
 }
diff --git a/pkg/suite/yaml_suite.go b/pkg/suite/yaml_suite.go
index 5d9410c..a3303d7 100644
--- a/pkg/suite/yaml_suite.go
+++ b/pkg/suite/yaml_suite.go
@@ -10,21 +10,29 @@ import (
 
 // YAMLConfig will be used for unmarshalling the yaml test suite
 type YAMLConfig struct {
-	Tests map[string]YAMLTest `yaml:"tests"`
+	Tests   map[string]YAMLTest       `yaml:"tests"`
+	Config  YAMLTestConfig            `yaml:"config"`
+}
+
+type YAMLTestConfig struct {
+	Env []string `yaml:"env"`
+	Dir string   `yaml:"dir"`
 }
 
 // YAMLTest represents a test in the yaml test suite
 type YAMLTest struct {
-	Title    string      `yaml:"-"`
-	Command  string      `yaml:"command"`
-	ExitCode int         `yaml:"exit-code"`
-	Stdout   interface{} `yaml:"stdout,omitempty"`
-	Stderr   interface{} `yaml:"stderr,omitempty"`
+	Title    string         `yaml:"-"`
+	Command  string         `yaml:"command"`
+	ExitCode int            `yaml:"exit-code"`
+	Stdout   interface{}    `yaml:"stdout,omitempty"`
+	Stderr   interface{}    `yaml:"stderr,omitempty"`
+	Config   YAMLTestConfig `yaml:"config,omitempty"`
 }
 
 //YAMLSuite represents a test suite which was configured in yaml
 type YAMLSuite struct {
 	TestCases  []runtime.TestCase
+	Config     runtime.TestConfig
 }
 
 // GetTests returns all tests of the test suite
@@ -42,6 +50,10 @@ func (s YAMLSuite) GetTestByTitle(title string) (runtime.TestCase, error) {
 	return runtime.TestCase{}, fmt.Errorf("Could not find test " + title)
 }
 
+func (s YAMLSuite) GetGlobalConfig() runtime.TestConfig {
+	return s.Config
+}
+
 // ParseYAML parses the Suite from a yaml byte slice
 func ParseYAML(content []byte) Suite {
 	yamlConfig := YAMLConfig{}
@@ -53,6 +65,10 @@ func ParseYAML(content []byte) Suite {
 
 	return YAMLSuite{
 		TestCases: convertYAMLConfToTestCases(yamlConfig),
+		Config:    runtime.TestConfig{
+		    Env: yamlConfig.Config.Env,
+		    Dir: yamlConfig.Config.Dir,
+        },
 	}
 }
 
@@ -64,6 +80,8 @@ func convertYAMLConfToTestCases(conf YAMLConfig) []runtime.TestCase {
 			Title:    t.Title,
 			Command:  runtime.CommandUnderTest{
 				Cmd: t.Command,
+				Env: t.Config.Env,
+				Dir: t.Config.Dir,
 			},
 			Expected: runtime.Expected{
 				ExitCode: t.ExitCode,
@@ -84,7 +102,8 @@ func toString(s interface{}) string {
 // UnmarshalYAML unmarshals the yaml
 func (y *YAMLConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
 	var params struct {
-		Tests map[string]YAMLTest `yaml:"tests"`
+		Tests  map[string]YAMLTest       `yaml:"tests"`
+		Config YAMLTestConfig            `yaml:"config"`
 	}
 
 	err := unmarshal(&params)
@@ -101,6 +120,7 @@ func (y *YAMLConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
 			ExitCode: v.ExitCode,
 			Stdout: y.convertToExpectedOut(v.Stdout),
 			Stderr: y.convertToExpectedOut(v.Stderr),
+			Config: y.mergeConfigs(v.Config, params.Config),
 		}
 
 		// Set key as command, if command property was empty
@@ -111,6 +131,12 @@ func (y *YAMLConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
 		y.Tests[k] = test
 	}
 
+	//Parse global configuration
+	y.Config = YAMLTestConfig{
+		Env: params.Config.Env,
+		Dir: params.Config.Dir,
+	}
+
 	return nil
 }
 
@@ -172,3 +198,16 @@ func (y *YAMLConfig) convertToExpectedOut(value interface{}) runtime.ExpectedOut
 
     return exp
 }
+
+func (y *YAMLConfig) mergeConfigs(local YAMLTestConfig, global YAMLTestConfig) YAMLTestConfig {
+	conf := global
+	if len(local.Env) > 0 {
+		conf.Env = local.Env
+	}
+
+	if local.Dir != "" {
+		conf.Dir = local.Dir
+	}
+
+	return conf
+}


Test patch:
diff --git a/integration/config_test.yaml b/integration/config_test.yaml
new file mode 100644
index 0000000..cb9e4cf
--- /dev/null
+++ b/integration/config_test.yaml
@@ -0,0 +1,23 @@
+config:
+  env:
+    - KEY=value
+tests:
+  should print global env value:
+    command: echo $KEY
+    stdout: value
+    exit-code: 0
+
+  should print local env value:
+    command: echo $KEY
+    config:
+      env:
+        - KEY=local
+    stdout: local
+    exit-code: 0
+
+  should execute in given dir:
+    command: pwd
+    config:
+      dir: /tmp
+    stdout: /tmp
+    exit-code: 0
\ No newline at end of file
diff --git a/pkg/cmd/command_test.go b/pkg/cmd/command_test.go
index 6476faa..6156af3 100644
--- a/pkg/cmd/command_test.go
+++ b/pkg/cmd/command_test.go
@@ -36,4 +36,13 @@ func TestCommand_ExitCode(t *testing.T) {
 
     assert.Nil(t, err)
     assert.Equal(t, 120, cmd.ExitCode())
+}
+
+func TestCommand_WithEnvVariables(t *testing.T) {
+    cmd := NewCommand("echo $TEST")
+    cmd.Env = []string{"TEST=hey"}
+
+    _ = cmd.Execute()
+
+    assert.Equal(t, "hey", cmd.Stdout())
 }
\ No newline at end of file
diff --git a/pkg/output/cli_test.go b/pkg/output/cli_test.go
new file mode 100644
index 0000000..4d1154d
--- /dev/null
+++ b/pkg/output/cli_test.go
@@ -0,0 +1,54 @@
+package output
+
+import (
+    "bytes"
+    "github.com/SimonBaeumer/commander/pkg/runtime"
+    "github.com/stretchr/testify/assert"
+    "strings"
+    "sync"
+    "testing"
+)
+
+func Test_Start(t *testing.T) {
+    var buf bytes.Buffer
+    var wg sync.WaitGroup
+    results := make(chan runtime.TestResult)
+
+    wg.Add(1)
+    go func() {
+        defer wg.Done()
+
+        writer := OutputWriter{out: &buf}
+        got := writer.Start(results)
+
+        assert.False(t, got)
+    }()
+
+    results <- runtime.TestResult{
+        TestCase: runtime.TestCase{
+            Title: "Successful test",
+            Command: runtime.CommandUnderTest{},
+        },
+        ValidationResult: runtime.ValidationResult{
+            Success: true,
+        },
+        FailedProperty: "",
+    }
+
+    results <- runtime.TestResult{
+        TestCase: runtime.TestCase{
+            Title: "Failed test",
+            Command: runtime.CommandUnderTest{},
+        },
+        ValidationResult: runtime.ValidationResult{
+            Success: false,
+        },
+        FailedProperty: "Stdout",
+    }
+
+    close(results)
+    wg.Wait()
+    assert.True(t, true)
+    assert.True(t, strings.Contains(buf.String(), "✓ Successful test"))
+    assert.True(t, strings.Contains(buf.String(), "✗ Failed test"))
+}
\ No newline at end of file
diff --git a/pkg/runtime/runtime_test.go b/pkg/runtime/runtime_test.go
index 6e10000..9ce028f 100644
--- a/pkg/runtime/runtime_test.go
+++ b/pkg/runtime/runtime_test.go
@@ -20,6 +20,26 @@ func TestRuntime_Start(t *testing.T) {
     assert.Equal(t, 1, count)
 }
 
+func TestRuntime_WithEnvVariables(t *testing.T) {
+    s := getExampleTestSuite()
+    s[0].Command.Env = []string{"KEY=value"}
+    s[0].Command.Cmd = "echo $KEY"
+    s[0].Expected.Stdout.Exactly = "value"
+    s[0].Title = "Output env variable"
+
+    got := Start(s)
+
+    assert.IsType(t, make(<-chan TestResult), got)
+
+    count := 0
+    for r := range got {
+        assert.Equal(t, "value", r.TestCase.Result.Stdout)
+        assert.True(t, r.ValidationResult.Success)
+        count++
+    }
+    assert.Equal(t, 1, count)
+}
+
 func getExampleTestSuite() []TestCase {
     tests := []TestCase{
         {
diff --git a/pkg/suite/yaml_suite_test.go b/pkg/suite/yaml_suite_test.go
index 89f2c5a..df83caa 100644
--- a/pkg/suite/yaml_suite_test.go
+++ b/pkg/suite/yaml_suite_test.go
@@ -154,3 +154,44 @@ tests:
 
 	assert.Equal(t, "Could not find test does not exist", err.Error())
 }
+
+func TestYAMLSuite_ShouldParseGlobalConfig(t *testing.T) {
+	yaml := []byte(`
+config:
+    env:
+    - KEY=value
+    dir: /home/commander/
+tests:
+    echo hello:
+       exit-code: 0
+`)
+
+	got := ParseYAML(yaml)
+	assert.Equal(t, []string{"KEY=value"}, got.GetGlobalConfig().Env)
+	assert.Equal(t, []string{"KEY=value"}, got.GetTests()[0].Command.Env)
+	assert.Equal(t, "/home/commander/", got.GetTests()[0].Command.Dir)
+	assert.Equal(t, "/home/commander/", got.GetGlobalConfig().Dir)
+}
+
+func TestYAMLSuite_ShouldPreferLocalTestConfigs(t *testing.T) {
+	yaml := []byte(`
+config:
+    env:
+    - KEY=global
+    dir: /home/commander/
+tests:
+    echo hello:
+       exit-code: 0
+       config:
+           env:
+           - KEY=local
+           dir: /home/test
+`)
+
+	got := ParseYAML(yaml)
+	assert.Equal(t, []string{"KEY=global"}, got.GetGlobalConfig().Env)
+	assert.Equal(t, "/home/commander/", got.GetGlobalConfig().Dir)
+
+	assert.Equal(t, []string{"KEY=local"}, got.GetTests()[0].Command.Env)
+	assert.Equal(t, "/home/test", got.GetTests()[0].Command.Dir)
+}
\ No newline at end of file