Merge remote-tracking branch 'origin/pr/163'
diff --git a/.gitignore b/.gitignore index 5514532..720c13c 100644 --- a/.gitignore +++ b/.gitignore
@@ -1,3 +1,5 @@ .DS_Store *.test . +.idea +gomega.iml
diff --git a/.travis.yml b/.travis.yml index 79780ec..8ed1e44 100644 --- a/.travis.yml +++ b/.travis.yml
@@ -1,7 +1,8 @@ language: go go: - - 1.4 - 1.5 + - 1.6.2 + - stable install: - go get -v ./...
diff --git a/format/format_test.go b/format/format_test.go index fd926f5..59517fe 100644 --- a/format/format_test.go +++ b/format/format_test.go
@@ -2,11 +2,11 @@ import ( "fmt" - "strings" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" . "github.com/onsi/gomega/format" "github.com/onsi/gomega/types" + "strings" ) //recursive struct
diff --git a/gbytes/say_matcher_test.go b/gbytes/say_matcher_test.go index d0ddf1f..63fb3b3 100644 --- a/gbytes/say_matcher_test.go +++ b/gbytes/say_matcher_test.go
@@ -1,8 +1,8 @@ package gbytes_test import ( - "time" . "github.com/onsi/gomega/gbytes" + "time" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega"
diff --git a/gexec/build.go b/gexec/build.go index 3e9bf9f..220c8c4 100644 --- a/gexec/build.go +++ b/gexec/build.go
@@ -9,9 +9,13 @@ "path" "path/filepath" "runtime" + "sync" ) -var tmpDir string +var ( + mu sync.Mutex + tmpDir string +) /* Build uses go build to compile the package at packagePath. The resulting binary is saved off in a temporary directory. @@ -60,13 +64,18 @@ gexec. In Ginkgo this is typically done in an AfterSuite callback. */ func CleanupBuildArtifacts() { + mu.Lock() + defer mu.Unlock() if tmpDir != "" { os.RemoveAll(tmpDir) + tmpDir = "" } } func temporaryDirectory() (string, error) { var err error + mu.Lock() + defer mu.Unlock() if tmpDir == "" { tmpDir, err = ioutil.TempDir("", "gexec_artifacts") if err != nil {
diff --git a/gexec/build_test.go b/gexec/build_test.go new file mode 100644 index 0000000..7bd62fe --- /dev/null +++ b/gexec/build_test.go
@@ -0,0 +1,37 @@ +package gexec_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/onsi/gomega/gexec" +) + +var _ = Describe(".Build", func() { + var packagePath = "./_fixture/firefly" + + Context("when there have been previous calls to Build", func() { + BeforeEach(func() { + _, err := gexec.Build(packagePath) + Ω(err).ShouldNot(HaveOccurred()) + }) + + It("compiles the specified package", func() { + compiledPath, err := gexec.Build(packagePath) + Ω(err).ShouldNot(HaveOccurred()) + Ω(compiledPath).Should(BeAnExistingFile()) + }) + + Context("and CleanupBuildArtifacts has been called", func() { + BeforeEach(func() { + gexec.CleanupBuildArtifacts() + }) + + It("compiles the specified package", func() { + var err error + fireflyPath, err = gexec.Build(packagePath) + Ω(err).ShouldNot(HaveOccurred()) + Ω(fireflyPath).Should(BeAnExistingFile()) + }) + }) + }) +})
diff --git a/gexec/exit_matcher_test.go b/gexec/exit_matcher_test.go index 9f18e2d..79615dd 100644 --- a/gexec/exit_matcher_test.go +++ b/gexec/exit_matcher_test.go
@@ -1,9 +1,9 @@ package gexec_test import ( + . "github.com/onsi/gomega/gexec" "os/exec" "time" - . "github.com/onsi/gomega/gexec" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega"
diff --git a/ghttp/test_server.go b/ghttp/test_server.go index fde65be..093550d 100644 --- a/ghttp/test_server.go +++ b/ghttp/test_server.go
@@ -202,7 +202,9 @@ server := s.HTTPTestServer s.HTTPTestServer = nil - server.Close() + if server != nil { + server.Close() + } } //ServeHTTP() makes Server an http.Handler @@ -223,7 +225,7 @@ //If the handler panics GHTTP will silently succeed. This is bad™. //To catch this case we need to fail the test if the handler has panicked. - //However, if the handler is panicking because Ginkgo's causing it to panic (i.e. an asswertion failed) + //However, if the handler is panicking because Ginkgo's causing it to panic (i.e. an assertion failed) //then we shouldn't double-report the error as this will confuse people. //So: step 1, if this is a Ginkgo panic - do nothing, Ginkgo's aware of the failure @@ -349,6 +351,17 @@ return s.requestHandlers[index] } +func (s *Server) Reset() { + s.writeLock.Lock() + defer s.writeLock.Unlock() + + s.HTTPTestServer.CloseClientConnections() + s.calls = 0 + s.receivedRequests = nil + s.requestHandlers = nil + s.routedHandlers = nil +} + //WrapHandler combines the passed in handler with the handler registered at the passed in index. //This is useful, for example, when a server has been set up in a shared context but must be tweaked //for a particular test.
diff --git a/ghttp/test_server_test.go b/ghttp/test_server_test.go index 292b51d..88b3246 100644 --- a/ghttp/test_server_test.go +++ b/ghttp/test_server_test.go
@@ -2,6 +2,7 @@ import ( "bytes" + "io" "io/ioutil" "net/http" "net/url" @@ -31,29 +32,56 @@ s.Close() }) + Describe("Resetting the server", func() { + BeforeEach(func() { + s.RouteToHandler("GET", "/", func(w http.ResponseWriter, req *http.Request) {}) + s.AppendHandlers(func(w http.ResponseWriter, req *http.Request) {}) + http.Get(s.URL() + "/") + + Ω(s.ReceivedRequests()).Should(HaveLen(1)) + }) + + It("clears all handlers and call counts", func() { + s.Reset() + Ω(s.ReceivedRequests()).Should(HaveLen(0)) + Ω(func() { s.GetHandler(0) }).Should(Panic()) + }) + }) + Describe("closing client connections", func() { It("closes", func() { - s.AppendHandlers( + s.RouteToHandler("GET", "/", func(w http.ResponseWriter, req *http.Request) { - w.Write([]byte("hello")) - }, - func(w http.ResponseWriter, req *http.Request) { - s.CloseClientConnections() + io.WriteString(w, req.RemoteAddr) }, ) - - resp, err := http.Get(s.URL()) + client := http.Client{Transport: &http.Transport{DisableKeepAlives: true}} + resp, err := client.Get(s.URL()) Ω(err).ShouldNot(HaveOccurred()) Ω(resp.StatusCode).Should(Equal(200)) body, err := ioutil.ReadAll(resp.Body) resp.Body.Close() Ω(err).ShouldNot(HaveOccurred()) - Ω(body).Should(Equal([]byte("hello"))) - resp, err = http.Get(s.URL()) - Ω(err).Should(HaveOccurred()) - Ω(resp).Should(BeNil()) + s.CloseClientConnections() + + resp, err = client.Get(s.URL()) + Ω(err).ShouldNot(HaveOccurred()) + Ω(resp.StatusCode).Should(Equal(200)) + + body2, err := ioutil.ReadAll(resp.Body) + resp.Body.Close() + Ω(err).ShouldNot(HaveOccurred()) + + Ω(body2).ShouldNot(Equal(body)) + }) + }) + + Describe("closing server mulitple times", func() { + It("should not fail", func() { + s.Close() + Ω(s.Close).ShouldNot(Panic()) }) })
diff --git a/matchers.go b/matchers.go index b6110c4..0c30aa1 100644 --- a/matchers.go +++ b/matchers.go
@@ -26,6 +26,15 @@ } } +//BeIdenticalTo uses the == operator to compare actual with expected. +//BeIdenticalTo is strict about types when performing comparisons. +//It is an error for both actual and expected to be nil. Use BeNil() instead. +func BeIdenticalTo(expected interface{}) types.GomegaMatcher { + return &matchers.BeIdenticalToMatcher{ + Expected: expected, + } +} + //BeNil succeeds if actual is nil func BeNil() types.GomegaMatcher { return &matchers.BeNilMatcher{} @@ -205,6 +214,15 @@ } } +//MatchYAML succeeds if actual is a string or stringer of YAML that matches +//the expected YAML. The YAML's are decoded and the resulting objects are compared via +//reflect.DeepEqual so things like key-ordering and whitespace shouldn't matter. +func MatchYAML(yaml interface{}) types.GomegaMatcher { + return &matchers.MatchYAMLMatcher{ + YAMLToMatch: yaml, + } +} + //BeEmpty succeeds if actual is empty. Actual must be of type string, array, map, chan, or slice. func BeEmpty() types.GomegaMatcher { return &matchers.BeEmptyMatcher{} @@ -217,6 +235,13 @@ } } +//HaveCap succeeds if actual has the passed-in capacity. Actual must be of type array, chan, or slice. +func HaveCap(count int) types.GomegaMatcher { + return &matchers.HaveCapMatcher{ + Count: count, + } +} + //BeZero succeeds if actual is the zero value for its type or if actual is nil. func BeZero() types.GomegaMatcher { return &matchers.BeZeroMatcher{}
diff --git a/matchers/be_identical_to.go b/matchers/be_identical_to.go new file mode 100644 index 0000000..fdcda4d --- /dev/null +++ b/matchers/be_identical_to.go
@@ -0,0 +1,37 @@ +package matchers + +import ( + "fmt" + "runtime" + + "github.com/onsi/gomega/format" +) + +type BeIdenticalToMatcher struct { + Expected interface{} +} + +func (matcher *BeIdenticalToMatcher) Match(actual interface{}) (success bool, matchErr error) { + if actual == nil && matcher.Expected == nil { + return false, fmt.Errorf("Refusing to compare <nil> to <nil>.\nBe explicit and use BeNil() instead. This is to avoid mistakes where both sides of an assertion are erroneously uninitialized.") + } + + defer func() { + if r := recover(); r != nil { + if _, ok := r.(runtime.Error); ok { + success = false + matchErr = nil + } + } + }() + + return actual == matcher.Expected, nil +} + +func (matcher *BeIdenticalToMatcher) FailureMessage(actual interface{}) string { + return format.Message(actual, "to be identical to", matcher.Expected) +} + +func (matcher *BeIdenticalToMatcher) NegatedFailureMessage(actual interface{}) string { + return format.Message(actual, "not to be identical to", matcher.Expected) +}
diff --git a/matchers/be_identical_to_test.go b/matchers/be_identical_to_test.go new file mode 100644 index 0000000..8b90a1a --- /dev/null +++ b/matchers/be_identical_to_test.go
@@ -0,0 +1,61 @@ +package matchers_test + +import ( + "errors" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + . "github.com/onsi/gomega/matchers" +) + +var _ = Describe("BeIdenticalTo", func() { + Context("when asserting that nil equals nil", func() { + It("should error", func() { + success, err := (&BeIdenticalToMatcher{Expected: nil}).Match(nil) + + Ω(success).Should(BeFalse()) + Ω(err).Should(HaveOccurred()) + }) + }) + + It("should treat the same pointer to a struct as identical", func() { + mySpecialStruct := myCustomType{} + Ω(&mySpecialStruct).Should(BeIdenticalTo(&mySpecialStruct)) + Ω(&myCustomType{}).ShouldNot(BeIdenticalTo(&mySpecialStruct)) + }) + + It("should be strict about types", func() { + Ω(5).ShouldNot(BeIdenticalTo("5")) + Ω(5).ShouldNot(BeIdenticalTo(5.0)) + Ω(5).ShouldNot(BeIdenticalTo(3)) + }) + + It("should treat primtives as identical", func() { + Ω("5").Should(BeIdenticalTo("5")) + Ω("5").ShouldNot(BeIdenticalTo("55")) + + Ω(5.55).Should(BeIdenticalTo(5.55)) + Ω(5.55).ShouldNot(BeIdenticalTo(6.66)) + + Ω(5).Should(BeIdenticalTo(5)) + Ω(5).ShouldNot(BeIdenticalTo(55)) + }) + + It("should treat the same pointers to a slice as identical", func() { + mySlice := []int{1, 2} + Ω(&mySlice).Should(BeIdenticalTo(&mySlice)) + Ω(&mySlice).ShouldNot(BeIdenticalTo(&[]int{1, 2})) + }) + + It("should treat the same pointers to a map as identical", func() { + myMap := map[string]string{"a": "b", "c": "d"} + Ω(&myMap).Should(BeIdenticalTo(&myMap)) + Ω(myMap).ShouldNot(BeIdenticalTo(map[string]string{"a": "b", "c": "d"})) + }) + + It("should treat the same pointers to an error as identical", func() { + myError := errors.New("foo") + Ω(&myError).Should(BeIdenticalTo(&myError)) + Ω(errors.New("foo")).ShouldNot(BeIdenticalTo(errors.New("bar"))) + }) +})
diff --git a/matchers/be_sent_matcher_test.go b/matchers/be_sent_matcher_test.go index 381c2b4..205d71f 100644 --- a/matchers/be_sent_matcher_test.go +++ b/matchers/be_sent_matcher_test.go
@@ -1,8 +1,8 @@ package matchers_test import ( - "time" . "github.com/onsi/gomega/matchers" + "time" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega"
diff --git a/matchers/consist_of_test.go b/matchers/consist_of_test.go index 0b230e3..dcd1afe 100644 --- a/matchers/consist_of_test.go +++ b/matchers/consist_of_test.go
@@ -54,10 +54,10 @@ Ω([]string{"foo", "bar", "baz"}).ShouldNot(ConsistOf("foo", MatchRegexp("^ba"), MatchRegexp("turducken"))) }) - It("should not depend on the order of the matchers", func() { + It("should not depend on the order of the matchers", func() { Ω([][]int{[]int{1, 2}, []int{2}}).Should(ConsistOf(ContainElement(1), ContainElement(2))) Ω([][]int{[]int{1, 2}, []int{2}}).Should(ConsistOf(ContainElement(2), ContainElement(1))) - }) + }) Context("when a matcher errors", func() { It("should soldier on", func() {
diff --git a/matchers/have_cap_matcher.go b/matchers/have_cap_matcher.go new file mode 100644 index 0000000..7ace93d --- /dev/null +++ b/matchers/have_cap_matcher.go
@@ -0,0 +1,28 @@ +package matchers + +import ( + "fmt" + + "github.com/onsi/gomega/format" +) + +type HaveCapMatcher struct { + Count int +} + +func (matcher *HaveCapMatcher) Match(actual interface{}) (success bool, err error) { + length, ok := capOf(actual) + if !ok { + return false, fmt.Errorf("HaveCap matcher expects a array/channel/slice. Got:\n%s", format.Object(actual, 1)) + } + + return length == matcher.Count, nil +} + +func (matcher *HaveCapMatcher) FailureMessage(actual interface{}) (message string) { + return fmt.Sprintf("Expected\n%s\nto have capacity %d", format.Object(actual, 1), matcher.Count) +} + +func (matcher *HaveCapMatcher) NegatedFailureMessage(actual interface{}) (message string) { + return fmt.Sprintf("Expected\n%s\nnot to have capacity %d", format.Object(actual, 1), matcher.Count) +}
diff --git a/matchers/have_cap_matcher_test.go b/matchers/have_cap_matcher_test.go new file mode 100644 index 0000000..a92a177 --- /dev/null +++ b/matchers/have_cap_matcher_test.go
@@ -0,0 +1,50 @@ +package matchers_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + . "github.com/onsi/gomega/matchers" +) + +var _ = Describe("HaveCap", func() { + Context("when passed a supported type", func() { + It("should do the right thing", func() { + Ω([0]int{}).Should(HaveCap(0)) + Ω([2]int{1}).Should(HaveCap(2)) + + Ω([]int{}).Should(HaveCap(0)) + Ω([]int{1, 2, 3, 4, 5}[:2]).Should(HaveCap(5)) + Ω(make([]int, 0, 5)).Should(HaveCap(5)) + + c := make(chan bool, 3) + Ω(c).Should(HaveCap(3)) + c <- true + c <- true + Ω(c).Should(HaveCap(3)) + + Ω(make(chan bool)).Should(HaveCap(0)) + }) + }) + + Context("when passed a correctly typed nil", func() { + It("should operate succesfully on the passed in value", func() { + var nilSlice []int + Ω(nilSlice).Should(HaveCap(0)) + + var nilChan chan int + Ω(nilChan).Should(HaveCap(0)) + }) + }) + + Context("when passed an unsupported type", func() { + It("should error", func() { + success, err := (&HaveCapMatcher{Count: 0}).Match(0) + Ω(success).Should(BeFalse()) + Ω(err).Should(HaveOccurred()) + + success, err = (&HaveCapMatcher{Count: 0}).Match(nil) + Ω(success).Should(BeFalse()) + Ω(err).Should(HaveOccurred()) + }) + }) +})
diff --git a/matchers/have_occurred_matcher.go b/matchers/have_occurred_matcher.go index cdc1d54..ebdd717 100644 --- a/matchers/have_occurred_matcher.go +++ b/matchers/have_occurred_matcher.go
@@ -10,15 +10,18 @@ } func (matcher *HaveOccurredMatcher) Match(actual interface{}) (success bool, err error) { - if isNil(actual) { + // is purely nil? + if actual == nil { return false, nil } - if isError(actual) { - return true, nil + // must be an 'error' type + if !isError(actual) { + return false, fmt.Errorf("Expected an error-type. Got:\n%s", format.Object(actual, 1)) } - return false, fmt.Errorf("Expected an error. Got:\n%s", format.Object(actual, 1)) + // must be non-nil (or a pointer to a non-nil) + return !isNil(actual), nil } func (matcher *HaveOccurredMatcher) FailureMessage(actual interface{}) (message string) {
diff --git a/matchers/have_occurred_matcher_test.go b/matchers/have_occurred_matcher_test.go index 0fc35a9..009e23e 100644 --- a/matchers/have_occurred_matcher_test.go +++ b/matchers/have_occurred_matcher_test.go
@@ -34,6 +34,18 @@ Ω(err).Should(HaveOccurred()) }) + It("doesn't support non-error type", func() { + success, err := (&HaveOccurredMatcher{}).Match(AnyType{}) + Ω(success).Should(BeFalse()) + Ω(err).Should(MatchError("Expected an error-type. Got:\n <matchers_test.AnyType>: {}")) + }) + + It("doesn't support non-error pointer type", func() { + success, err := (&HaveOccurredMatcher{}).Match(&AnyType{}) + Ω(success).Should(BeFalse()) + Ω(err).Should(MatchError(MatchRegexp(`Expected an error-type. Got:\n <*matchers_test.AnyType | 0x[[:xdigit:]]+>: {}`))) + }) + It("should succeed with pointer types that conform to error interface", func() { err := &CustomErr{"ohai"} Ω(err).Should(HaveOccurred())
diff --git a/matchers/have_suffix_matcher.go b/matchers/have_suffix_matcher.go index eb1b284..afc78fc 100644 --- a/matchers/have_suffix_matcher.go +++ b/matchers/have_suffix_matcher.go
@@ -16,7 +16,7 @@ return false, fmt.Errorf("HaveSuffix matcher requires a string or stringer. Got:\n%s", format.Object(actual, 1)) } suffix := matcher.suffix() - return len(actualString) >= len(suffix) && actualString[len(actualString) - len(suffix):] == suffix, nil + return len(actualString) >= len(suffix) && actualString[len(actualString)-len(suffix):] == suffix, nil } func (matcher *HaveSuffixMatcher) suffix() string {
diff --git a/matchers/match_json_matcher.go b/matchers/match_json_matcher.go index efc5e15..e61978a 100644 --- a/matchers/match_json_matcher.go +++ b/matchers/match_json_matcher.go
@@ -4,8 +4,9 @@ "bytes" "encoding/json" "fmt" - "github.com/onsi/gomega/format" "reflect" + + "github.com/onsi/gomega/format" ) type MatchJSONMatcher struct { @@ -39,22 +40,24 @@ } func (matcher *MatchJSONMatcher) prettyPrint(actual interface{}) (actualFormatted, expectedFormatted string, err error) { - actualString, aok := toString(actual) - expectedString, eok := toString(matcher.JSONToMatch) - - if !(aok && eok) { - return "", "", fmt.Errorf("MatchJSONMatcher matcher requires a string or stringer. Got:\n%s", format.Object(actual, 1)) + actualString, ok := toString(actual) + if !ok { + return "", "", fmt.Errorf("MatchJSONMatcher matcher requires a string, stringer, or []byte. Got actual:\n%s", format.Object(actual, 1)) + } + expectedString, ok := toString(matcher.JSONToMatch) + if !ok { + return "", "", fmt.Errorf("MatchJSONMatcher matcher requires a string, stringer, or []byte. Got expected:\n%s", format.Object(matcher.JSONToMatch, 1)) } abuf := new(bytes.Buffer) ebuf := new(bytes.Buffer) if err := json.Indent(abuf, []byte(actualString), "", " "); err != nil { - return "", "", err + return "", "", fmt.Errorf("Actual '%s' should be valid JSON, but it is not.\nUnderlying error:%s", actualString, err) } if err := json.Indent(ebuf, []byte(expectedString), "", " "); err != nil { - return "", "", err + return "", "", fmt.Errorf("Expected '%s' should be valid JSON, but it is not.\nUnderlying error:%s", expectedString, err) } return abuf.String(), ebuf.String(), nil
diff --git a/matchers/match_json_matcher_test.go b/matchers/match_json_matcher_test.go index c1924ba..755c4ad 100644 --- a/matchers/match_json_matcher_test.go +++ b/matchers/match_json_matcher_test.go
@@ -25,35 +25,49 @@ }) }) - Context("when either side is not valid JSON", func() { - It("should error", func() { - success, err := (&MatchJSONMatcher{JSONToMatch: `oops`}).Match(`{}`) + Context("when the expected is not valid JSON", func() { + It("should error and explain why", func() { + success, err := (&MatchJSONMatcher{JSONToMatch: `{}`}).Match(`oops`) Ω(success).Should(BeFalse()) Ω(err).Should(HaveOccurred()) - - success, err = (&MatchJSONMatcher{JSONToMatch: `{}`}).Match(`oops`) - Ω(success).Should(BeFalse()) - Ω(err).Should(HaveOccurred()) + Ω(err.Error()).Should(ContainSubstring("Actual 'oops' should be valid JSON")) }) }) - Context("when either side is neither a string nor a stringer", func() { - It("should error", func() { - success, err := (&MatchJSONMatcher{JSONToMatch: "{}"}).Match(2) + Context("when the actual is not valid JSON", func() { + It("should error and explain why", func() { + success, err := (&MatchJSONMatcher{JSONToMatch: `oops`}).Match(`{}`) Ω(success).Should(BeFalse()) Ω(err).Should(HaveOccurred()) + Ω(err.Error()).Should(ContainSubstring("Expected 'oops' should be valid JSON")) + }) + }) - success, err = (&MatchJSONMatcher{JSONToMatch: 2}).Match("{}") + Context("when the expected is neither a string nor a stringer nor a byte array", func() { + It("should error", func() { + success, err := (&MatchJSONMatcher{JSONToMatch: 2}).Match("{}") Ω(success).Should(BeFalse()) Ω(err).Should(HaveOccurred()) + Ω(err.Error()).Should(ContainSubstring("MatchJSONMatcher matcher requires a string, stringer, or []byte. Got expected:\n <int>: 2")) success, err = (&MatchJSONMatcher{JSONToMatch: nil}).Match("{}") Ω(success).Should(BeFalse()) Ω(err).Should(HaveOccurred()) + Ω(err.Error()).Should(ContainSubstring("MatchJSONMatcher matcher requires a string, stringer, or []byte. Got expected:\n <nil>: nil")) + }) + }) - success, err = (&MatchJSONMatcher{JSONToMatch: 2}).Match(nil) + Context("when the actual is neither a string nor a stringer nor a byte array", func() { + It("should error", func() { + success, err := (&MatchJSONMatcher{JSONToMatch: "{}"}).Match(2) Ω(success).Should(BeFalse()) Ω(err).Should(HaveOccurred()) + Ω(err.Error()).Should(ContainSubstring("MatchJSONMatcher matcher requires a string, stringer, or []byte. Got actual:\n <int>: 2")) + + success, err = (&MatchJSONMatcher{JSONToMatch: "{}"}).Match(nil) + Ω(success).Should(BeFalse()) + Ω(err).Should(HaveOccurred()) + Ω(err.Error()).Should(ContainSubstring("MatchJSONMatcher matcher requires a string, stringer, or []byte. Got actual:\n <nil>: nil")) }) }) })
diff --git a/matchers/match_yaml_matcher.go b/matchers/match_yaml_matcher.go new file mode 100644 index 0000000..69fb51a --- /dev/null +++ b/matchers/match_yaml_matcher.go
@@ -0,0 +1,74 @@ +package matchers + +import ( + "fmt" + "reflect" + "strings" + + "github.com/onsi/gomega/format" + "gopkg.in/yaml.v2" +) + +type MatchYAMLMatcher struct { + YAMLToMatch interface{} +} + +func (matcher *MatchYAMLMatcher) Match(actual interface{}) (success bool, err error) { + actualString, expectedString, err := matcher.toStrings(actual) + if err != nil { + return false, err + } + + var aval interface{} + var eval interface{} + + if err := yaml.Unmarshal([]byte(actualString), &aval); err != nil { + return false, fmt.Errorf("Actual '%s' should be valid YAML, but it is not.\nUnderlying error:%s", actualString, err) + } + if err := yaml.Unmarshal([]byte(expectedString), &eval); err != nil { + return false, fmt.Errorf("Expected '%s' should be valid YAML, but it is not.\nUnderlying error:%s", expectedString, err) + } + + return reflect.DeepEqual(aval, eval), nil +} + +func (matcher *MatchYAMLMatcher) FailureMessage(actual interface{}) (message string) { + actualString, expectedString, _ := matcher.toNormalisedStrings(actual) + return format.Message(actualString, "to match YAML of", expectedString) +} + +func (matcher *MatchYAMLMatcher) NegatedFailureMessage(actual interface{}) (message string) { + actualString, expectedString, _ := matcher.toNormalisedStrings(actual) + return format.Message(actualString, "not to match YAML of", expectedString) +} + +func (matcher *MatchYAMLMatcher) toNormalisedStrings(actual interface{}) (actualFormatted, expectedFormatted string, err error) { + actualString, expectedString, err := matcher.toStrings(actual) + return normalise(actualString), normalise(expectedString), err +} + +func normalise(input string) string { + var val interface{} + err := yaml.Unmarshal([]byte(input), &val) + if err != nil { + panic(err) // guarded by Match + } + output, err := yaml.Marshal(val) + if err != nil { + panic(err) // guarded by Unmarshal + } + return strings.TrimSpace(string(output)) +} + +func (matcher *MatchYAMLMatcher) toStrings(actual interface{}) (actualFormatted, expectedFormatted string, err error) { + actualString, ok := toString(actual) + if !ok { + return "", "", fmt.Errorf("MatchYAMLMatcher matcher requires a string, stringer, or []byte. Got actual:\n%s", format.Object(actual, 1)) + } + expectedString, ok := toString(matcher.YAMLToMatch) + if !ok { + return "", "", fmt.Errorf("MatchYAMLMatcher matcher requires a string, stringer, or []byte. Got expected:\n%s", format.Object(matcher.YAMLToMatch, 1)) + } + + return actualString, expectedString, nil +}
diff --git a/matchers/match_yaml_matcher_test.go b/matchers/match_yaml_matcher_test.go new file mode 100644 index 0000000..8e63de1 --- /dev/null +++ b/matchers/match_yaml_matcher_test.go
@@ -0,0 +1,94 @@ +package matchers_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + . "github.com/onsi/gomega/matchers" +) + +var _ = Describe("MatchYAMLMatcher", func() { + Context("When passed stringifiables", func() { + It("should succeed if the YAML matches", func() { + Expect("---").Should(MatchYAML("")) + Expect("a: 1").Should(MatchYAML(`{"a":1}`)) + Expect("a: 1\nb: 2").Should(MatchYAML(`{"b":2, "a":1}`)) + }) + + It("should explain if the YAML does not match when it should", func() { + message := (&MatchYAMLMatcher{YAMLToMatch: "a: 1"}).FailureMessage("b: 2") + Expect(message).To(MatchRegexp(`Expected\s+<string>: b: 2\s+to match YAML of\s+<string>: a: 1`)) + }) + + It("should normalise the expected and actual when explaining if the YAML does not match when it should", func() { + message := (&MatchYAMLMatcher{YAMLToMatch: "a: 'one'"}).FailureMessage("{b: two}") + Expect(message).To(MatchRegexp(`Expected\s+<string>: b: two\s+to match YAML of\s+<string>: a: one`)) + }) + + It("should explain if the YAML matches when it should not", func() { + message := (&MatchYAMLMatcher{YAMLToMatch: "a: 1"}).NegatedFailureMessage("a: 1") + Expect(message).To(MatchRegexp(`Expected\s+<string>: a: 1\s+not to match YAML of\s+<string>: a: 1`)) + }) + + It("should normalise the expected and actual when explaining if the YAML matches when it should not", func() { + message := (&MatchYAMLMatcher{YAMLToMatch: "a: 'one'"}).NegatedFailureMessage("{a: one}") + Expect(message).To(MatchRegexp(`Expected\s+<string>: a: one\s+not to match YAML of\s+<string>: a: one`)) + }) + + It("should fail if the YAML does not match", func() { + Expect("a: 1").ShouldNot(MatchYAML(`{"b":2, "a":1}`)) + }) + + It("should work with byte arrays", func() { + Expect([]byte("a: 1")).Should(MatchYAML([]byte("a: 1"))) + Expect("a: 1").Should(MatchYAML([]byte("a: 1"))) + Expect([]byte("a: 1")).Should(MatchYAML("a: 1")) + }) + }) + + Context("when the expected is not valid YAML", func() { + It("should error and explain why", func() { + success, err := (&MatchYAMLMatcher{YAMLToMatch: ""}).Match("good:\nbad") + Expect(success).Should(BeFalse()) + Expect(err).Should(HaveOccurred()) + Expect(err.Error()).Should(ContainSubstring("Actual 'good:\nbad' should be valid YAML")) + }) + }) + + Context("when the actual is not valid YAML", func() { + It("should error and explain why", func() { + success, err := (&MatchYAMLMatcher{YAMLToMatch: "good:\nbad"}).Match("") + Expect(success).Should(BeFalse()) + Expect(err).Should(HaveOccurred()) + Expect(err.Error()).Should(ContainSubstring("Expected 'good:\nbad' should be valid YAML")) + }) + }) + + Context("when the expected is neither a string nor a stringer nor a byte array", func() { + It("should error", func() { + success, err := (&MatchYAMLMatcher{YAMLToMatch: 2}).Match("") + Expect(success).Should(BeFalse()) + Expect(err).Should(HaveOccurred()) + Expect(err.Error()).Should(ContainSubstring("MatchYAMLMatcher matcher requires a string, stringer, or []byte. Got expected:\n <int>: 2")) + + success, err = (&MatchYAMLMatcher{YAMLToMatch: nil}).Match("") + Expect(success).Should(BeFalse()) + Expect(err).Should(HaveOccurred()) + Expect(err.Error()).Should(ContainSubstring("MatchYAMLMatcher matcher requires a string, stringer, or []byte. Got expected:\n <nil>: nil")) + }) + }) + + Context("when the actual is neither a string nor a stringer nor a byte array", func() { + It("should error", func() { + success, err := (&MatchYAMLMatcher{YAMLToMatch: ""}).Match(2) + Expect(success).Should(BeFalse()) + Expect(err).Should(HaveOccurred()) + Expect(err.Error()).Should(ContainSubstring("MatchYAMLMatcher matcher requires a string, stringer, or []byte. Got actual:\n <int>: 2")) + + success, err = (&MatchYAMLMatcher{YAMLToMatch: ""}).Match(nil) + Expect(success).Should(BeFalse()) + Expect(err).Should(HaveOccurred()) + Expect(err.Error()).Should(ContainSubstring("MatchYAMLMatcher matcher requires a string, stringer, or []byte. Got actual:\n <nil>: nil")) + }) + }) +})
diff --git a/matchers/succeed_matcher.go b/matchers/succeed_matcher.go index f7dd853..721ed55 100644 --- a/matchers/succeed_matcher.go +++ b/matchers/succeed_matcher.go
@@ -10,15 +10,18 @@ } func (matcher *SucceedMatcher) Match(actual interface{}) (success bool, err error) { + // is purely nil? if actual == nil { return true, nil } - if isError(actual) { - return false, nil + // must be an 'error' type + if !isError(actual) { + return false, fmt.Errorf("Expected an error-type. Got:\n%s", format.Object(actual, 1)) } - return false, fmt.Errorf("Expected an error-type. Got:\n%s", format.Object(actual, 1)) + // must be nil (or a pointer to a nil) + return isNil(actual), nil } func (matcher *SucceedMatcher) FailureMessage(actual interface{}) (message string) {
diff --git a/matchers/succeed_matcher_test.go b/matchers/succeed_matcher_test.go index 3562e70..6b62c8b 100644 --- a/matchers/succeed_matcher_test.go +++ b/matchers/succeed_matcher_test.go
@@ -34,6 +34,29 @@ It("should not if passed a non-error", func() { success, err := (&SucceedMatcher{}).Match(Invalid()) Ω(success).Should(BeFalse()) - Ω(err).Should(HaveOccurred()) + Ω(err).Should(MatchError("Expected an error-type. Got:\n <*matchers_test.AnyType | 0x0>: nil")) }) + + It("doesn't support non-error type", func() { + success, err := (&SucceedMatcher{}).Match(AnyType{}) + Ω(success).Should(BeFalse()) + Ω(err).Should(MatchError("Expected an error-type. Got:\n <matchers_test.AnyType>: {}")) + }) + + It("doesn't support non-error pointer type", func() { + success, err := (&SucceedMatcher{}).Match(&AnyType{}) + Ω(success).Should(BeFalse()) + Ω(err).Should(MatchError(MatchRegexp(`Expected an error-type. Got:\n <*matchers_test.AnyType | 0x[[:xdigit:]]+>: {}`))) + }) + + It("should not succeed with pointer types that conform to error interface", func() { + err := &CustomErr{"ohai"} + Ω(err).ShouldNot(Succeed()) + }) + + It("should succeed with nil pointers to types that conform to error interface", func() { + var err *CustomErr = nil + Ω(err).Should(Succeed()) + }) + })
diff --git a/matchers/support/goraph/util/util.go b/matchers/support/goraph/util/util.go index a24cd27..d76a1ee 100644 --- a/matchers/support/goraph/util/util.go +++ b/matchers/support/goraph/util/util.go
@@ -3,5 +3,5 @@ import "math" func Odd(n int) bool { - return math.Mod(float64(n), 2.0) == 1.0 -} + return math.Mod(float64(n), 2.0) == 1.0 +}
diff --git a/matchers/type_support.go b/matchers/type_support.go index ef9b448..04020f0 100644 --- a/matchers/type_support.go +++ b/matchers/type_support.go
@@ -150,6 +150,17 @@ return 0, false } } +func capOf(a interface{}) (int, bool) { + if a == nil { + return 0, false + } + switch reflect.TypeOf(a).Kind() { + case reflect.Array, reflect.Chan, reflect.Slice: + return reflect.ValueOf(a).Cap(), true + default: + return 0, false + } +} func isNil(a interface{}) bool { if a == nil {