Repository: SimonBaeumer/commander
Pull request #12 (2019-03-07 09:37:44)

Refactor matcher and validation


Patch:
diff --git a/README.md b/README.md
index 4351b79..abe8ad5 100644
--- a/README.md
+++ b/README.md
@@ -118,5 +118,4 @@ make deps
       - ubuntu
       - rhel
       - centos
-      - alpine
-      
\ No newline at end of file
+      - alpine
\ No newline at end of file
diff --git a/commander.yaml b/commander.yaml
index 3c96e4a..673e3ea 100644
--- a/commander.yaml
+++ b/commander.yaml
@@ -19,14 +19,3 @@ tests:
         - ✗ it will fail, on property 'ExitCode'
         - "Count: 1, Failed: 1"
     exit-code: 1
-
-  it should assert stderr:
-    command: ">&2 echo hello"
-    stderr: hello
-    exit-code: 0
-
-  it should assert exactly:
-    command: echo hello
-    stdout:
-      exactly: hello
-    exit-code: 0
\ No newline at end of file
diff --git a/pkg/matcher/matcher.go b/pkg/matcher/matcher.go
new file mode 100644
index 0000000..b164384
--- /dev/null
+++ b/pkg/matcher/matcher.go
@@ -0,0 +1,108 @@
+package matcher
+
+import (
+    "fmt"
+    "github.com/pmezard/go-difflib/difflib"
+    "strings"
+)
+
+const (
+    Text     = "text"
+    Contains = "contains"
+    Equal    = "equal"
+)
+
+func NewMatcher(matcher string) Matcher {
+    switch matcher {
+    case Text:
+        return TextMatcher{}
+    case Contains:
+        return ContainsMatcher{}
+    case Equal:
+        return EqualMatcher{}
+    default:
+        panic(fmt.Sprintf("Validator '%s' does not exist!", matcher))
+    }
+}
+
+type Matcher interface {
+    Match(actual interface{}, expected interface{}) MatcherResult
+}
+
+type MatcherResult struct {
+    Success bool
+    Diff    string
+}
+
+type TextMatcher struct {
+}
+
+func (m TextMatcher) Match(got interface{}, expected interface{}) MatcherResult {
+    result := true
+    if got != expected {
+        result = false
+    }
+
+    diff := difflib.UnifiedDiff{
+        A: difflib.SplitLines(got.(string)),
+        B: difflib.SplitLines(expected.(string)),
+        FromFile: "Got",
+        ToFile: "Expected",
+        Context: 3,
+    }
+    diffText, _ := difflib.GetUnifiedDiffString(diff)
+
+    return MatcherResult{
+        Diff:       diffText,
+        Success:    result,
+    }
+}
+
+type ContainsMatcher struct {
+}
+
+func (m ContainsMatcher) Match(got interface{}, expected interface{}) MatcherResult {
+    result := true
+    if !strings.Contains(got.(string), expected.(string)) {
+        result = false
+    }
+
+    diff := difflib.UnifiedDiff{
+        A: difflib.SplitLines(fmt.Sprintf("%s", got.(string))),
+        B: difflib.SplitLines(fmt.Sprintf("%s", expected.(string))),
+        FromFile: "Got",
+        ToFile: "Expected",
+        Context: 3,
+    }
+    diffText, _ := difflib.GetUnifiedDiffString(diff)
+
+    return MatcherResult{
+        Success: result,
+        Diff: diffText,
+    }
+}
+
+type EqualMatcher struct {
+}
+
+func (m EqualMatcher) Match(got interface{}, expected interface{}) MatcherResult {
+    if got == expected {
+        return MatcherResult{
+            Success: true,
+        }
+    }
+
+    diff := difflib.UnifiedDiff{
+        A: difflib.SplitLines(fmt.Sprintf("%v", got)),
+        B: difflib.SplitLines(fmt.Sprintf("%v", expected)),
+        FromFile: "Got",
+        ToFile: "Expected",
+        Context: 3,
+    }
+    diffText, _ := difflib.GetUnifiedDiffString(diff)
+
+    return MatcherResult{
+        Success: false,
+        Diff: diffText,
+    }
+}
diff --git a/pkg/runtime/runtime.go b/pkg/runtime/runtime.go
index 26ad3b3..9192041 100644
--- a/pkg/runtime/runtime.go
+++ b/pkg/runtime/runtime.go
@@ -9,9 +9,10 @@ import (
 
 // Constants for defining the various tested properties
 const (
-    ExitCode = "ExitCode"
-    Stdout   = "Stdout"
-    Stderr   = "Stderr"
+    ExitCode  = "ExitCode"
+    Stdout    = "Stdout"
+    Stderr    = "Stderr"
+    LineCount = "LineCount"
 )
 
 // Result status codes
@@ -43,15 +44,17 @@ type CommandResult struct {
 
 //Expected is the expected output of the command under test
 type Expected struct {
-    Stdout   ExpectedOut
-    Stderr   ExpectedOut
-    ExitCode int
+    Stdout    ExpectedOut
+    Stderr    ExpectedOut
+    LineCount int
+    ExitCode  int
 }
 
 type ExpectedOut struct {
-    Contains    []string
-    Line        map[int]string
-    Exactly     string
+    Contains  []string
+    Lines     map[int]string
+    Exactly   string
+    LineCount int
 }
 
 // CommandUnderTest represents the command under test
@@ -117,60 +120,6 @@ func runTest(test TestCase) TestResult {
         Stderr:   cut.Stderr(),
     }
 
-    validationResult := validateExpectedOut(test.Result.Stdout, test.Expected.Stdout)
-    if !validationResult.Success {
-        return TestResult{
-            ValidationResult: validationResult,
-            TestCase:         test,
-            FailedProperty:   Stdout,
-        }
-    }
-
-    validationResult = validateExpectedOut(test.Result.Stderr, test.Expected.Stderr)
-    if !validationResult.Success {
-        return TestResult{
-            ValidationResult: validationResult,
-            TestCase:         test,
-            FailedProperty: Stderr,
-        }
-    }
-
-    validator := NewValidator(Equal)
-    validationResult = validator.Validate(test.Result.ExitCode, test.Expected.ExitCode)
-    if !validationResult.Success {
-        return TestResult{
-            ValidationResult: validationResult,
-            TestCase: test,
-            FailedProperty: ExitCode,
-        }
-    }
-
-    return TestResult{
-        ValidationResult: validationResult,
-        TestCase:         test,
-    }
+    return Validate(test)
 }
 
-func validateExpectedOut(got string, expected  ExpectedOut) ValidationResult {
-    var v Validator
-    var result ValidationResult
-
-    if expected.Exactly != ""{
-        v = NewValidator(Text)
-        if result = v.Validate(got, expected.Exactly); !result.Success {
-            return result
-        }
-    }
-
-    if len(expected.Contains) > 0 {
-        v = NewValidator(Contains)
-        for _, c := range expected.Contains {
-            if result = v.Validate(got, c); !result.Success {
-                return result
-            }
-        }
-    }
-
-    result.Success = true
-    return result
-}
\ No newline at end of file
diff --git a/pkg/runtime/validator.go b/pkg/runtime/validator.go
index ebf6b5c..121fe97 100644
--- a/pkg/runtime/validator.go
+++ b/pkg/runtime/validator.go
@@ -1,108 +1,100 @@
 package runtime
 
 import (
-	"fmt"
-	"github.com/pmezard/go-difflib/difflib"
-	"strings"
+	"github.com/SimonBaeumer/commander/pkg/matcher"
+    "strings"
 )
 
-const (
-	Text     = "text"
-	Contains = "contains"
-	Equal    = "equal"
-)
-
-func NewValidator(validator string) Validator {
-	switch validator {
-	case Text:
-		return TextValidator{}
-	case Contains:
-		return ContainsValidator{}
-	case Equal:
-		return EqualValidator{}
-	default:
-		panic(fmt.Sprintf("Validator '%s' does not exist!", validator))
-	}
-}
-
-type Validator interface {
-    Validate(got interface{}, expected interface{}) ValidationResult
-}
-
 type ValidationResult struct {
-	Success    bool
-	Diff       string
+	Success bool
+	Diff    string
 }
 
-type TextValidator struct {
-}
-
-func (v TextValidator) Validate(got interface{}, expected interface{}) ValidationResult {
-	result := true
-	if got != expected {
-		result = false
-	}
-
-	diff := difflib.UnifiedDiff{
-		A: difflib.SplitLines(got.(string)),
-		B: difflib.SplitLines(expected.(string)),
-		FromFile: "Got",
-		ToFile: "Expected",
-		Context: 3,
-	}
-	diffText, _ := difflib.GetUnifiedDiffString(diff)
-
+func NewValidationResult(m matcher.MatcherResult) ValidationResult {
 	return ValidationResult{
-		Diff:       diffText,
-		Success:    result,
+		Success: m.Success,
+		Diff:    m.Diff,
 	}
 }
 
-type ContainsValidator struct {
+func Validate(test TestCase) TestResult {
+    equalMatcher := matcher.NewMatcher(matcher.Equal)
+
+    matcherResult := validateExpectedOut(test.Result.Stdout, test.Expected.Stdout)
+    if !matcherResult.Success {
+        return TestResult{
+            ValidationResult: NewValidationResult(matcherResult),
+            TestCase:         test,
+            FailedProperty:   Stdout,
+        }
+    }
+
+    matcherResult = validateExpectedOut(test.Result.Stderr, test.Expected.Stderr)
+    if !matcherResult.Success {
+        return TestResult{
+            ValidationResult: NewValidationResult(matcherResult),
+            TestCase:         test,
+            FailedProperty:   Stderr,
+        }
+    }
+
+    matcherResult = equalMatcher.Match(test.Result.ExitCode, test.Expected.ExitCode)
+    if !matcherResult.Success {
+        return TestResult{
+            ValidationResult: NewValidationResult(matcherResult),
+            TestCase:         test,
+            FailedProperty:   ExitCode,
+        }
+    }
+
+    return TestResult{
+        ValidationResult: NewValidationResult(matcherResult),
+        TestCase:         test,
+    }
 }
 
-func (v ContainsValidator) Validate(got interface{}, expected interface{}) ValidationResult {
-	result := true
-	if !strings.Contains(got.(string), expected.(string)) {
-		result = false
-	}
-
-	diff := difflib.UnifiedDiff{
-		A: difflib.SplitLines(fmt.Sprintf("%s", got.(string))),
-		B: difflib.SplitLines(fmt.Sprintf("%s", expected.(string))),
-		FromFile: "Got",
-		ToFile: "Expected",
-		Context: 3,
-	}
-	diffText, _ := difflib.GetUnifiedDiffString(diff)
-
-	return ValidationResult{
-		Success: result,
-		Diff: diffText,
-	}
-}
+func validateExpectedOut(got string, expected  ExpectedOut) matcher.MatcherResult {
+	var m matcher.Matcher
+	var result matcher.MatcherResult
 
-type EqualValidator struct {
-}
-
-func (v EqualValidator) Validate(got interface{}, expected interface{}) ValidationResult {
-	if got == expected {
-		return ValidationResult{
-			Success: true,
+	if expected.Exactly != "" {
+		m = matcher.NewMatcher(matcher.Text)
+		if result = m.Match(got, expected.Exactly); !result.Success {
+			return result
 		}
 	}
 
-	diff := difflib.UnifiedDiff{
-		A: difflib.SplitLines(fmt.Sprintf("%d", got.(int))),
-		B: difflib.SplitLines(fmt.Sprintf("%d", expected.(int))),
-		FromFile: "Got",
-		ToFile: "Expected",
-		Context: 3,
+	if len(expected.Contains) > 0 {
+		m = matcher.NewMatcher(matcher.Contains)
+		for _, c := range expected.Contains {
+			if result = m.Match(got, c); !result.Success {
+				return result
+			}
+		}
 	}
-	diffText, _ := difflib.GetUnifiedDiffString(diff)
 
-	return ValidationResult{
-		Success: false,
-		Diff: diffText,
-	}
-}
+	if expected.LineCount != 0 {
+        m = matcher.NewMatcher(matcher.Equal)
+        count := strings.Count(got, "\n") + 1
+        if got == "" {
+            count = 0
+        }
+
+        if result = m.Match(count, expected.LineCount); !result.Success {
+            return result
+        }
+    }
+
+	if len(expected.Lines) > 0 {
+	    m = matcher.NewMatcher(matcher.Equal)
+	    actualLines := strings.Split(got, "\n")
+	    for k, expL := range expected.Lines {
+            if result = m.Match(actualLines[k], expL); !result.Success {
+                return result
+            }
+        }
+    }
+
+	result.Success = true
+	return result
+}
\ No newline at end of file
diff --git a/pkg/suite/yaml_suite.go b/pkg/suite/yaml_suite.go
index d1fe620..b23c13d 100644
--- a/pkg/suite/yaml_suite.go
+++ b/pkg/suite/yaml_suite.go
@@ -131,7 +131,9 @@ func (y *YAMLConfig) convertToExpectedOut(value interface{}) runtime.ExpectedOut
 		    switch k {
 		    case
 			    "contains",
-			    "exactly":
+			    "exactly",
+			    "line-count",
+			    "lines":
 		        break;
 		    default:
 			    panic(fmt.Sprintf("Key %s is not allowed.", k))
@@ -148,9 +150,21 @@ func (y *YAMLConfig) convertToExpectedOut(value interface{}) runtime.ExpectedOut
 
 	    //Parse exactly key
         if exactly := v["exactly"]; exactly != nil {
-        	exact := toString(exactly)
-            exp.Exactly = exact
+	        exp.Exactly = toString(exactly)
         }
+
+	    //Parse line-count key
+	    if lc := v["line-count"]; lc != nil {
+		    exp.LineCount = lc.(int)
+	    }
+
+	    // Parse lines
+	    if l := v["lines"]; l != nil {
+	    	exp.Lines = make(map[int]string)
+		    for k, v := range l.(map[interface{}]interface{}) {
+		    	exp.Lines[k.(int)] = toString(v)
+		    }
+	    }
         break
     }
 


Test patch:
diff --git a/integration/commander_test.yaml b/integration/commander_test.yaml
index 08ef30f..3b0ee60 100644
--- a/integration/commander_test.yaml
+++ b/integration/commander_test.yaml
@@ -16,4 +16,25 @@ tests:
   it should assert stderr:
       command: '>&2 echo "error"'
       stderr: error
-      exit-code: 0
\ No newline at end of file
+      exit-code: 0
+
+  it should assert line-count:
+    command: printf "hello\nline2"
+    stdout:
+      line-count: 2
+    exit-code: 0
+
+  it should assert zero line count:
+    command: exit 0
+    stdout:
+      line-count: 0
+    exit-code: 0
+
+  it should assert a specifc line:
+    command: printf "line1\nline2\nline3\nline4"
+    stdout:
+      lines:
+        0: line1
+        1: line2
+        3: line4
+    exit-code: 0
\ No newline at end of file
diff --git a/pkg/matcher/matcher_test.go b/pkg/matcher/matcher_test.go
new file mode 100644
index 0000000..97ac3ef
--- /dev/null
+++ b/pkg/matcher/matcher_test.go
@@ -0,0 +1,50 @@
+package matcher
+
+import (
+    "github.com/stretchr/testify/assert"
+    "testing"
+)
+
+func Test_NewMatcher_Text(t *testing.T) {
+    got := NewMatcher(Text)
+    assert.IsType(t, TextMatcher{}, got)
+}
+
+func Test_NewMatcher_Contains(t *testing.T) {
+    got := NewMatcher(Contains)
+    assert.IsType(t, ContainsMatcher{}, got)
+}
+
+func Test_NewMatcher_Equal(t *testing.T) {
+    got := NewMatcher(Equal)
+    assert.IsType(t, EqualMatcher{}, got)
+}
+
+func TestTextMatcher_Validate(t *testing.T) {
+    m := TextMatcher{}
+    got := m.Match("test", "test")
+    assert.True(t, got.Success)
+}
+
+func TestTextMatcher_ValidateFails(t *testing.T) {
+    m := TextMatcher{}
+    got := m.Match("test", "unequal")
+    assert.False(t, got.Success)
+    assert.Contains(t, got.Diff, "+unequal")
+    assert.Contains(t, got.Diff, "-test")
+}
+
+func TestEqualMatcher_Validate(t *testing.T) {
+    m := EqualMatcher{}
+    got := m.Match(1, 1)
+    assert.True(t, got.Success)
+}
+
+func TestEqualMatcher_ValidateFails(t *testing.T) {
+    m := EqualMatcher{}
+    got := m.Match(1, 0)
+    assert.False(t, got.Success)
+    assert.Contains(t, got.Diff, "+0")
+    assert.Contains(t, got.Diff, "-1")
+}
+
diff --git a/pkg/runtime/validator_test.go b/pkg/runtime/validator_test.go
deleted file mode 100644
index 7673a4e..0000000
--- a/pkg/runtime/validator_test.go
+++ /dev/null
@@ -1,50 +0,0 @@
-package runtime
-
-import (
-    "github.com/stretchr/testify/assert"
-    "testing"
-)
-
-func Test_NewValidator_Text(t *testing.T) {
-    got := NewValidator(Text)
-    assert.IsType(t, TextValidator{}, got)
-}
-
-func Test_NewValidator_Contains(t *testing.T) {
-    got := NewValidator(Contains)
-    assert.IsType(t, ContainsValidator{}, got)
-}
-
-func Test_NewValidator_Equal(t *testing.T) {
-    got := NewValidator(Equal)
-    assert.IsType(t, EqualValidator{}, got)
-}
-
-func TestTextValidator_Validate(t *testing.T) {
-    v := TextValidator{}
-    got := v.Validate("test", "test")
-    assert.True(t, got.Success)
-}
-
-func TestTextValidator_ValidateFails(t *testing.T) {
-    v := TextValidator{}
-    got := v.Validate("test", "unequal")
-    assert.False(t, got.Success)
-    assert.Contains(t, got.Diff, "+unequal")
-    assert.Contains(t, got.Diff, "-test")
-}
-
-func TestEqualValidator_Validate(t *testing.T) {
-    v := EqualValidator{}
-    got := v.Validate(1, 1)
-    assert.True(t, got.Success)
-}
-
-func TestEqualValidator_ValidateFails(t *testing.T) {
-    v := EqualValidator{}
-    got := v.Validate(1, 0)
-    assert.False(t, got.Success)
-    assert.Contains(t, got.Diff, "+0")
-    assert.Contains(t, got.Diff, "-1")
-}
-
diff --git a/pkg/suite/yaml_suite_test.go b/pkg/suite/yaml_suite_test.go
index 6d00723..89f2c5a 100644
--- a/pkg/suite/yaml_suite_test.go
+++ b/pkg/suite/yaml_suite_test.go
@@ -6,6 +6,8 @@ import (
 	"testing"
 )
 
+const ExpectedLineCount = 10
+
 func TestYAMLConfig_UnmarshalYAML(t *testing.T) {
 	yaml := []byte(`
 tests:
@@ -41,6 +43,37 @@ tests:
 	assert.Equal(t, "echo hello", tests[0].Title)
 }
 
+func TestYAMLConfig_UnmarshalYAML_ShouldParseLineCount(t *testing.T) {
+	yaml := []byte(`
+tests:
+    echo hello:
+        exit-code: 0
+        stdout:
+            line-count: 10
+`)
+	tests := ParseYAML(yaml).GetTests()
+
+	assert.Equal(t, ExpectedLineCount, tests[0].Expected.Stdout.LineCount)
+}
+
+func TestYAMLConfig_UnmarshalYAML_ShouldParseLines(t *testing.T) {
+    yaml := []byte(`
+tests:
+    printf "line1\nline2\nline3\nline4":
+        exit-code: 0
+        stdout:
+            lines:
+                0: line1
+                1: line2
+                3: line4
+`)
+    tests := ParseYAML(yaml).GetTests()
+
+    assert.Equal(t, "line1", tests[0].Expected.Stdout.Lines[0])
+    assert.Equal(t, "line2", tests[0].Expected.Stdout.Lines[1])
+    assert.Equal(t, "line4", tests[0].Expected.Stdout.Lines[3])
+}
+
 func TestYAMLConfig_UnmarshalYAML_ShouldConvertStdoutToExpectedOut(t *testing.T) {
 	yaml := []byte(`
 tests:
@@ -120,4 +153,4 @@ tests:
 	_, err := ParseYAML(yaml).GetTestByTitle("does not exist")
 
 	assert.Equal(t, "Could not find test does not exist", err.Error())
-}
\ No newline at end of file
+}