diff -Nru golang-gomega-1.0/CHANGELOG.md golang-gomega-1.0+git20160910.d59fa0a/CHANGELOG.md --- golang-gomega-1.0/CHANGELOG.md 2014-08-02 20:42:16.000000000 +0000 +++ golang-gomega-1.0+git20160910.d59fa0a/CHANGELOG.md 2016-09-11 05:10:23.000000000 +0000 @@ -1,3 +1,24 @@ +## HEAD + +Improvements: + +- Added `BeSent` which attempts to send a value down a channel and fails if the attempt blocks. Can be paired with `Eventually` to safely send a value down a channel with a timeout. +- `Ω`, `Expect`, `Eventually`, and `Consistently` now immediately `panic` if there is no registered fail handler. This is always a mistake that can hide failing tests. +- `Receive()` no longer errors when passed a closed channel, it's perfectly fine to attempt to read from a closed channel so Ω(c).Should(Receive()) always fails and Ω(c).ShoudlNot(Receive()) always passes with a closed channel. +- Added `HavePrefix` and `HaveSuffix` matchers. +- `ghttp` can now handle concurrent requests. +- Added `Succeed` which allows one to write `Ω(MyFunction()).Should(Succeed())`. +- Improved `ghttp`'s behavior around failing assertions and panics: + - If a registered handler makes a failing assertion `ghttp` will return `500`. + - If a registered handler panics, `ghttp` will return `500` *and* fail the test. This is new behavior that may cause existing code to break. This code is almost certainly incorrect and creating a false positive. +- `ghttp` servers can take an `io.Writer`. `ghttp` will write a line to the writer when each request arrives. +- Added `WithTransform` matcher to allow munging input data before feeding into the relevant matcher +- Added boolean `And`, `Or`, and `Not` matchers to allow creating composite matchers + +Bug Fixes: +- gexec: `session.Wait` now uses `EventuallyWithOffset` to get the right line number in the failure. +- `ContainElement` no longer bails if a passed-in matcher errors. + ## 1.0 (8/2/2014) No changes. Dropping "beta" from the version number. diff -Nru golang-gomega-1.0/debian/changelog golang-gomega-1.0+git20160910.d59fa0a/debian/changelog --- golang-gomega-1.0/debian/changelog 2015-08-21 21:01:09.000000000 +0000 +++ golang-gomega-1.0+git20160910.d59fa0a/debian/changelog 2016-09-26 15:30:00.000000000 +0000 @@ -1,3 +1,21 @@ +golang-gomega (1.0+git20160910.d59fa0a-1) unstable; urgency=medium + + [ Paul Tagliamonte ] + * Team upload. + * Use a secure transport for the Vcs-Git and Vcs-Browser URL + + [ Martín Ferrari ] + * Upstream has communicated that they won't be making releases unless there + is a breaking change. Making a new release out of a git snapshot. + * Packaging refresh: + - Updated Standards-Version (no changes). + - Use new dh-golang features. + - Use golang-any. + - Remove Built-Using. + - Added new build dependencies. + + -- Martín Ferrari Mon, 26 Sep 2016 15:30:00 +0000 + golang-gomega (1.0-2) unstable; urgency=medium * debian/control: Migration to pkg-go team. diff -Nru golang-gomega-1.0/debian/control golang-gomega-1.0+git20160910.d59fa0a/debian/control --- golang-gomega-1.0/debian/control 2015-08-21 21:01:09.000000000 +0000 +++ golang-gomega-1.0+git20160910.d59fa0a/debian/control 2016-09-26 15:30:00.000000000 +0000 @@ -3,16 +3,20 @@ Priority: extra Maintainer: Debian Go Packaging Team Uploaders: Martín Ferrari -Build-Depends: debhelper (>= 9.0.0), dh-golang, golang-go -Standards-Version: 3.9.6 -Vcs-Git: git://anonscm.debian.org/pkg-go/packages/golang-gomega.git -Vcs-Browser: http://anonscm.debian.org/cgit/pkg-go/packages/golang-gomega.git/ +Build-Depends: debhelper (>= 9.0.0), dh-golang (>= 1.17~), golang-any, + golang-gopkg-yaml.v2-dev, + golang-goprotobuf-dev, +Standards-Version: 3.9.8 +Vcs-Git: https://anonscm.debian.org/git/pkg-go/packages/golang-gomega.git +Vcs-Browser: https://anonscm.debian.org/cgit/pkg-go/packages/golang-gomega.git/ Homepage: https://github.com/onsi/gomega +XS-Go-Import-Path: github.com/onsi/gomega Package: golang-gomega-dev Architecture: all -Depends: ${shlibs:Depends}, ${misc:Depends} -Built-Using: ${misc:Built-Using} +Depends: ${shlibs:Depends}, ${misc:Depends}, + golang-gopkg-yaml.v2-dev, + golang-goprotobuf-dev, Description: Matcher/assertion library for the Go programming language Gomega is a matcher/assertion library. It is best paired with the Ginkgo BDD test framework, but can be adapted for use in other contexts too. diff -Nru golang-gomega-1.0/debian/rules golang-gomega-1.0+git20160910.d59fa0a/debian/rules --- golang-gomega-1.0/debian/rules 2015-08-21 21:01:09.000000000 +0000 +++ golang-gomega-1.0+git20160910.d59fa0a/debian/rules 2016-09-26 15:30:00.000000000 +0000 @@ -1,15 +1,21 @@ #!/usr/bin/make -f -# Uncomment this to turn on verbose mode. export DH_VERBOSE=1 -# DH_GOPKG is the upstream path which you would normally “go get”. -# Using it allows us to build applications without patching locations. -export DH_GOPKG := github.com/onsi/gomega - %: dh $@ --buildsystem=golang --with=golang # There is a circular dependency between gomega and ginkgo during testing, so # skip tests. override_dh_auto_test: + + +DEBVERS ?= $(shell dpkg-parsechangelog -SVersion) +VERSION ?= $(shell echo '$(DEBVERS)' | sed 's/^[[:digit:]]*://; s/[-].*//') +DEBPKGNAME ?= $(shell dpkg-parsechangelog -SSource) + +gen-orig-tgz: + if [ ! -f ../$(DEBPKGNAME)_$(VERSION).orig.tar.gz ] ; then \ + git archive --format=tar.gz --prefix=$(DEBPKGNAME)-$(VERSION)/ \ + upstream/$(VERSION) >../$(DEBPKGNAME)_$(VERSION).orig.tar.gz; \ + fi diff -Nru golang-gomega-1.0/format/format.go golang-gomega-1.0+git20160910.d59fa0a/format/format.go --- golang-gomega-1.0/format/format.go 2014-08-02 20:42:16.000000000 +0000 +++ golang-gomega-1.0+git20160910.d59fa0a/format/format.go 2016-09-11 05:10:23.000000000 +0000 @@ -7,6 +7,7 @@ "fmt" "reflect" "strings" + "strconv" ) // Use MaxDepth to set the maximum recursion depth when printing deeply nested objects @@ -143,9 +144,6 @@ case reflect.Ptr: return formatValue(value.Elem(), indentation) case reflect.Slice: - if value.Type().Elem().Kind() == reflect.Uint8 { - return formatString(value.Bytes(), indentation) - } return formatSlice(value, indentation) case reflect.String: return formatString(value.String(), indentation) @@ -189,6 +187,10 @@ } func formatSlice(v reflect.Value, indentation uint) string { + if v.Kind() == reflect.Slice && v.Type().Elem().Kind() == reflect.Uint8 && isPrintableString(string(v.Bytes())){ + return formatString(v.Bytes(), indentation) + } + l := v.Len() result := make([]string, l) longest := 0 @@ -262,15 +264,14 @@ return false } -func isNil(a interface{}) bool { - if a == nil { - return true - } - - switch reflect.TypeOf(a).Kind() { - case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice: - return reflect.ValueOf(a).IsNil() +/* +Returns true when the string is entirely made of printable runes, false otherwise. +*/ +func isPrintableString(str string) bool { + for _, runeValue := range str { + if !strconv.IsPrint(runeValue) { + return false + } } - - return false + return true } diff -Nru golang-gomega-1.0/format/format_test.go golang-gomega-1.0+git20160910.d59fa0a/format/format_test.go --- golang-gomega-1.0/format/format_test.go 2014-08-02 20:42:16.000000000 +0000 +++ golang-gomega-1.0+git20160910.d59fa0a/format/format_test.go 2016-09-11 05:10:23.000000000 +0000 @@ -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 @@ -162,13 +162,19 @@ }) Describe("formatting []byte slices", func() { - It("should present them as strings", func() { - b := []byte("a\nb\nc") - Ω(Object(b, 1)).Should(matchRegexp(`\[\]uint8 \| len:5, cap:\d+`, `a - b - c`)) - }) - }) + Context("when the slice is made of printable bytes", func () { + It("should present it as string", func() { + b := []byte("a b c") + Ω(Object(b, 1)).Should(matchRegexp(`\[\]uint8 \| len:5, cap:\d+`, `a b c`)) + }) + }) + Context("when the slice contains non-printable bytes", func () { + It("should present it as slice", func() { + b := []byte("a b c\n\x01\x02\x03\xff\x1bH") + Ω(Object(b, 1)).Should(matchRegexp(`\[\]uint8 \| len:12, cap:\d+`, `\[97, 32, 98, 32, 99, 10, 1, 2, 3, 255, 27, 72\]`)) + }) + }) + }) Describe("formatting functions", func() { It("should give the type and format values correctly", func() { diff -Nru golang-gomega-1.0/gbytes/buffer.go golang-gomega-1.0+git20160910.d59fa0a/gbytes/buffer.go --- golang-gomega-1.0/gbytes/buffer.go 2014-08-02 20:42:16.000000000 +0000 +++ golang-gomega-1.0+git20160910.d59fa0a/gbytes/buffer.go 2016-09-11 05:10:23.000000000 +0000 @@ -14,6 +14,7 @@ import ( "errors" "fmt" + "io" "regexp" "sync" "time" @@ -67,6 +68,30 @@ } /* +Read implements the io.Reader interface. It advances the +cursor as it reads. + +Returns an error if called after Close. +*/ +func (b *Buffer) Read(d []byte) (int, error) { + b.lock.Lock() + defer b.lock.Unlock() + + if b.closed { + return 0, errors.New("attempt to read from closed buffer") + } + + if uint64(len(b.contents)) <= b.readCursor { + return 0, io.EOF + } + + n := copy(d, b.contents[b.readCursor:]) + b.readCursor += uint64(n) + + return n, nil +} + +/* Close signifies that the buffer will no longer be written to */ func (b *Buffer) Close() error { diff -Nru golang-gomega-1.0/gbytes/buffer_test.go golang-gomega-1.0+git20160910.d59fa0a/gbytes/buffer_test.go --- golang-gomega-1.0/gbytes/buffer_test.go 2014-08-02 20:42:16.000000000 +0000 +++ golang-gomega-1.0+git20160910.d59fa0a/gbytes/buffer_test.go 2016-09-11 05:10:23.000000000 +0000 @@ -1,7 +1,9 @@ package gbytes_test import ( + "io" "time" + . "github.com/onsi/gomega/gbytes" . "github.com/onsi/ginkgo" @@ -36,6 +38,41 @@ }) }) + Describe("reading from a buffer", func() { + It("should read the current contents of the buffer", func() { + buffer := BufferWithBytes([]byte("abcde")) + + dest := make([]byte, 3) + n, err := buffer.Read(dest) + Ω(err).ShouldNot(HaveOccurred()) + Ω(n).Should(Equal(3)) + Ω(string(dest)).Should(Equal("abc")) + + dest = make([]byte, 3) + n, err = buffer.Read(dest) + Ω(err).ShouldNot(HaveOccurred()) + Ω(n).Should(Equal(2)) + Ω(string(dest[:n])).Should(Equal("de")) + + n, err = buffer.Read(dest) + Ω(err).Should(Equal(io.EOF)) + Ω(n).Should(Equal(0)) + }) + + Context("after the buffer has been closed", func() { + It("returns an error", func() { + buffer := BufferWithBytes([]byte("abcde")) + + buffer.Close() + + dest := make([]byte, 3) + n, err := buffer.Read(dest) + Ω(err).Should(HaveOccurred()) + Ω(n).Should(Equal(0)) + }) + }) + }) + Describe("detecting regular expressions", func() { It("should fire the appropriate channel when the passed in pattern matches, then close it", func(done Done) { go func() { diff -Nru golang-gomega-1.0/gbytes/say_matcher.go golang-gomega-1.0+git20160910.d59fa0a/gbytes/say_matcher.go --- golang-gomega-1.0/gbytes/say_matcher.go 2014-08-02 20:42:16.000000000 +0000 +++ golang-gomega-1.0+git20160910.d59fa0a/gbytes/say_matcher.go 2016-09-11 05:10:23.000000000 +0000 @@ -22,7 +22,7 @@ When Say succeeds, it fast forwards the gbytes.Buffer's read cursor to just after the succesful match. Thus, subsequent calls to Say will only match against the unread portion of the buffer -Say pairs very well with Eventually. To asser that a buffer eventually receives data matching "[123]-star" within 3 seconds you can: +Say pairs very well with Eventually. To assert that a buffer eventually receives data matching "[123]-star" within 3 seconds you can: Eventually(buffer, 3).Should(Say("[123]-star")) diff -Nru golang-gomega-1.0/gbytes/say_matcher_test.go golang-gomega-1.0+git20160910.d59fa0a/gbytes/say_matcher_test.go --- golang-gomega-1.0/gbytes/say_matcher_test.go 2014-08-02 20:42:16.000000000 +0000 +++ golang-gomega-1.0+git20160910.d59fa0a/gbytes/say_matcher_test.go 2016-09-11 05:10:23.000000000 +0000 @@ -1,8 +1,8 @@ package gbytes_test import ( - "time" . "github.com/onsi/gomega/gbytes" + "time" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" diff -Nru golang-gomega-1.0/gexec/build.go golang-gomega-1.0+git20160910.d59fa0a/gexec/build.go --- golang-gomega-1.0/gexec/build.go 2014-08-02 20:42:16.000000000 +0000 +++ golang-gomega-1.0+git20160910.d59fa0a/gexec/build.go 2016-09-11 05:10:23.000000000 +0000 @@ -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 -Nru golang-gomega-1.0/gexec/build_test.go golang-gomega-1.0+git20160910.d59fa0a/gexec/build_test.go --- golang-gomega-1.0/gexec/build_test.go 1970-01-01 00:00:00.000000000 +0000 +++ golang-gomega-1.0+git20160910.d59fa0a/gexec/build_test.go 2016-09-11 05:10:23.000000000 +0000 @@ -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 -Nru golang-gomega-1.0/gexec/exit_matcher_test.go golang-gomega-1.0+git20160910.d59fa0a/gexec/exit_matcher_test.go --- golang-gomega-1.0/gexec/exit_matcher_test.go 2014-08-02 20:42:16.000000000 +0000 +++ golang-gomega-1.0+git20160910.d59fa0a/gexec/exit_matcher_test.go 2016-09-11 05:10:23.000000000 +0000 @@ -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 -Nru golang-gomega-1.0/gexec/prefixed_writer.go golang-gomega-1.0+git20160910.d59fa0a/gexec/prefixed_writer.go --- golang-gomega-1.0/gexec/prefixed_writer.go 2014-08-02 20:42:16.000000000 +0000 +++ golang-gomega-1.0+git20160910.d59fa0a/gexec/prefixed_writer.go 2016-09-11 05:10:23.000000000 +0000 @@ -1,7 +1,6 @@ package gexec import ( - "bytes" "io" "sync" ) @@ -14,19 +13,18 @@ gexec.Start(cmd, NewPrefixedWriter("[my-cmd] ", GinkgoWriter), NewPrefixedWriter("[my-cmd] ", GinkgoWriter)) */ type PrefixedWriter struct { - prefix []byte - writer io.Writer - lock *sync.Mutex - isNewLine bool - isFirstWrite bool + prefix []byte + writer io.Writer + lock *sync.Mutex + atStartOfLine bool } func NewPrefixedWriter(prefix string, writer io.Writer) *PrefixedWriter { return &PrefixedWriter{ - prefix: []byte(prefix), - writer: writer, - lock: &sync.Mutex{}, - isFirstWrite: true, + prefix: []byte(prefix), + writer: writer, + lock: &sync.Mutex{}, + atStartOfLine: true, } } @@ -34,46 +32,21 @@ w.lock.Lock() defer w.lock.Unlock() - newLine := []byte("\n") - segments := bytes.Split(b, newLine) + toWrite := []byte{} - if len(segments) != 0 { - toWrite := []byte{} - if w.isFirstWrite { + for _, c := range b { + if w.atStartOfLine { toWrite = append(toWrite, w.prefix...) - toWrite = append(toWrite, segments[0]...) - w.isFirstWrite = false - } else if w.isNewLine { - toWrite = append(toWrite, newLine...) - toWrite = append(toWrite, w.prefix...) - toWrite = append(toWrite, segments[0]...) - } else { - toWrite = append(toWrite, segments[0]...) - } - - for i := 1; i < len(segments)-1; i++ { - toWrite = append(toWrite, newLine...) - toWrite = append(toWrite, w.prefix...) - toWrite = append(toWrite, segments[i]...) } - if len(segments) > 1 { - lastSegment := segments[len(segments)-1] + toWrite = append(toWrite, c) - if len(lastSegment) == 0 { - w.isNewLine = true - } else { - toWrite = append(toWrite, newLine...) - toWrite = append(toWrite, w.prefix...) - toWrite = append(toWrite, lastSegment...) - w.isNewLine = false - } - } + w.atStartOfLine = c == '\n' + } - _, err := w.writer.Write(toWrite) - if err != nil { - return 0, err - } + _, err := w.writer.Write(toWrite) + if err != nil { + return 0, err } return len(b), nil diff -Nru golang-gomega-1.0/gexec/prefixed_writer_test.go golang-gomega-1.0+git20160910.d59fa0a/gexec/prefixed_writer_test.go --- golang-gomega-1.0/gexec/prefixed_writer_test.go 2014-08-02 20:42:16.000000000 +0000 +++ golang-gomega-1.0+git20160910.d59fa0a/gexec/prefixed_writer_test.go 2016-09-11 05:10:23.000000000 +0000 @@ -2,6 +2,7 @@ import ( "bytes" + . "github.com/onsi/gomega/gexec" . "github.com/onsi/ginkgo" @@ -36,6 +37,7 @@ [p]nopqrs [p]tuv [p]wxyz -[p]`)) +[p] +`)) }) }) diff -Nru golang-gomega-1.0/gexec/session.go golang-gomega-1.0+git20160910.d59fa0a/gexec/session.go --- golang-gomega-1.0/gexec/session.go 2014-08-02 20:42:16.000000000 +0000 +++ golang-gomega-1.0+git20160910.d59fa0a/gexec/session.go 2016-09-11 05:10:23.000000000 +0000 @@ -60,7 +60,7 @@ Ω(session).Should(gexec.Exit()) When the session exits it closes the stdout and stderr gbytes buffers. This will short circuit any -Eventuallys waiting fo the buffers to Say something. +Eventuallys waiting for the buffers to Say something. */ func Start(command *exec.Cmd, outWriter io.Writer, errWriter io.Writer) (*Session, error) { exited := make(chan struct{}) @@ -137,7 +137,7 @@ Wait uses eventually under the hood and accepts the same timeout/polling intervals that eventually does. */ func (s *Session) Wait(timeout ...interface{}) *Session { - Eventually(s, timeout...).Should(Exit()) + EventuallyWithOffset(1, s, timeout...).Should(Exit()) return s } diff -Nru golang-gomega-1.0/gexec/session_test.go golang-gomega-1.0+git20160910.d59fa0a/gexec/session_test.go --- golang-gomega-1.0/gexec/session_test.go 2014-08-02 20:42:16.000000000 +0000 +++ golang-gomega-1.0+git20160910.d59fa0a/gexec/session_test.go 2016-09-11 05:10:23.000000000 +0000 @@ -4,6 +4,7 @@ "os/exec" "syscall" "time" + . "github.com/onsi/gomega/gbytes" . "github.com/onsi/gomega/gexec" diff -Nru golang-gomega-1.0/ghttp/handlers.go golang-gomega-1.0+git20160910.d59fa0a/ghttp/handlers.go --- golang-gomega-1.0/ghttp/handlers.go 2014-08-02 20:42:16.000000000 +0000 +++ golang-gomega-1.0+git20160910.d59fa0a/ghttp/handlers.go 2016-09-11 05:10:23.000000000 +0000 @@ -6,6 +6,10 @@ "fmt" "io/ioutil" "net/http" + "net/url" + "reflect" + + "github.com/golang/protobuf/proto" . "github.com/onsi/gomega" "github.com/onsi/gomega/types" ) @@ -35,7 +39,10 @@ Ω(req.URL.Path).Should(Equal(path), "Path mismatch") } if len(rawQuery) > 0 { - Ω(req.URL.RawQuery).Should(Equal(rawQuery[0]), "RawQuery mismatch") + values, err := url.ParseQuery(rawQuery[0]) + Ω(err).ShouldNot(HaveOccurred(), "Expected RawQuery is malformed") + + Ω(req.URL.Query()).Should(Equal(values), "RawQuery mismatch") } } } @@ -53,6 +60,8 @@ func VerifyBasicAuth(username string, password string) http.HandlerFunc { return func(w http.ResponseWriter, req *http.Request) { auth := req.Header.Get("Authorization") + Ω(auth).ShouldNot(Equal(""), "Authorization header must be specified") + decoded, err := base64.StdEncoding.DecodeString(auth[6:]) Ω(err).ShouldNot(HaveOccurred()) @@ -81,6 +90,19 @@ return VerifyHeader(http.Header{key: values}) } +//VerifyBody returns a handler that verifies that the body of the request matches the passed in byte array. +//It does this using Equal(). +func VerifyBody(expectedBody []byte) http.HandlerFunc { + return CombineHandlers( + func(w http.ResponseWriter, req *http.Request) { + body, err := ioutil.ReadAll(req.Body) + req.Body.Close() + Ω(err).ShouldNot(HaveOccurred()) + Ω(body).Should(Equal(expectedBody), "Body Mismatch") + }, + ) +} + //VerifyJSON returns a handler that verifies that the body of the request is a valid JSON representation //matching the passed in JSON string. It does this using Gomega's MatchJSON method // @@ -109,6 +131,53 @@ ) } +//VerifyForm returns a handler that verifies a request contains the specified form values. +// +//The request must contain *all* of the specified values, but it is allowed to have additional +//form values beyond the passed in set. +func VerifyForm(values url.Values) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + err := r.ParseForm() + Ω(err).ShouldNot(HaveOccurred()) + for key, vals := range values { + Ω(r.Form[key]).Should(Equal(vals), "Form mismatch for key: %s", key) + } + } +} + +//VerifyFormKV returns a handler that verifies a request contains a form key with the specified values. +// +//It is a convenience wrapper around `VerifyForm` that lets you avoid having to create a `url.Values` object. +func VerifyFormKV(key string, values ...string) http.HandlerFunc { + return VerifyForm(url.Values{key: values}) +} + +//VerifyProtoRepresenting returns a handler that verifies that the body of the request is a valid protobuf +//representation of the passed message. +// +//VerifyProtoRepresenting also verifies that the request's content type is application/x-protobuf +func VerifyProtoRepresenting(expected proto.Message) http.HandlerFunc { + return CombineHandlers( + VerifyContentType("application/x-protobuf"), + func(w http.ResponseWriter, req *http.Request) { + body, err := ioutil.ReadAll(req.Body) + Ω(err).ShouldNot(HaveOccurred()) + req.Body.Close() + + expectedType := reflect.TypeOf(expected) + actualValuePtr := reflect.New(expectedType.Elem()) + + actual, ok := actualValuePtr.Interface().(proto.Message) + Ω(ok).Should(BeTrue(), "Message value is not a proto.Message") + + err = proto.Unmarshal(body, actual) + Ω(err).ShouldNot(HaveOccurred(), "Failed to unmarshal protobuf") + + Ω(actual).Should(Equal(expected), "ProtoBuf Mismatch") + }, + ) +} + func copyHeader(src http.Header, dst http.Header) { for key, value := range src { dst[key] = value @@ -176,7 +245,17 @@ func RespondWithJSONEncoded(statusCode int, object interface{}, optionalHeader ...http.Header) http.HandlerFunc { data, err := json.Marshal(object) Ω(err).ShouldNot(HaveOccurred()) - return RespondWith(statusCode, string(data), optionalHeader...) + + var headers http.Header + if len(optionalHeader) == 1 { + headers = optionalHeader[0] + } else { + headers = make(http.Header) + } + if _, found := headers["Content-Type"]; !found { + headers["Content-Type"] = []string{"application/json"} + } + return RespondWith(statusCode, string(data), headers) } /* @@ -189,14 +268,46 @@ Also, RespondWithJSONEncodedPtr can be given an optional http.Header. The headers defined therein will be added to the response headers. Since the http.Header can be mutated after the fact you don't need to pass in a pointer. */ -func RespondWithJSONEncodedPtr(statusCode *int, object *interface{}, optionalHeader ...http.Header) http.HandlerFunc { +func RespondWithJSONEncodedPtr(statusCode *int, object interface{}, optionalHeader ...http.Header) http.HandlerFunc { return func(w http.ResponseWriter, req *http.Request) { - data, err := json.Marshal(*object) + data, err := json.Marshal(object) Ω(err).ShouldNot(HaveOccurred()) + var headers http.Header if len(optionalHeader) == 1 { - copyHeader(optionalHeader[0], w.Header()) + headers = optionalHeader[0] + } else { + headers = make(http.Header) + } + if _, found := headers["Content-Type"]; !found { + headers["Content-Type"] = []string{"application/json"} } + copyHeader(headers, w.Header()) w.WriteHeader(*statusCode) w.Write(data) } } + +//RespondWithProto returns a handler that responds to a request with the specified status code and a body +//containing the protobuf serialization of the provided message. +// +//Also, RespondWithProto can be given an optional http.Header. The headers defined therein will be added to the response headers. +func RespondWithProto(statusCode int, message proto.Message, optionalHeader ...http.Header) http.HandlerFunc { + return func(w http.ResponseWriter, req *http.Request) { + data, err := proto.Marshal(message) + Ω(err).ShouldNot(HaveOccurred()) + + var headers http.Header + if len(optionalHeader) == 1 { + headers = optionalHeader[0] + } else { + headers = make(http.Header) + } + if _, found := headers["Content-Type"]; !found { + headers["Content-Type"] = []string{"application/x-protobuf"} + } + copyHeader(headers, w.Header()) + + w.WriteHeader(statusCode) + w.Write(data) + } +} diff -Nru golang-gomega-1.0/ghttp/protobuf/protobuf.go golang-gomega-1.0+git20160910.d59fa0a/ghttp/protobuf/protobuf.go --- golang-gomega-1.0/ghttp/protobuf/protobuf.go 1970-01-01 00:00:00.000000000 +0000 +++ golang-gomega-1.0+git20160910.d59fa0a/ghttp/protobuf/protobuf.go 2016-09-11 05:10:23.000000000 +0000 @@ -0,0 +1,3 @@ +package protobuf + +//go:generate protoc --go_out=. simple_message.proto diff -Nru golang-gomega-1.0/ghttp/protobuf/simple_message.pb.go golang-gomega-1.0+git20160910.d59fa0a/ghttp/protobuf/simple_message.pb.go --- golang-gomega-1.0/ghttp/protobuf/simple_message.pb.go 1970-01-01 00:00:00.000000000 +0000 +++ golang-gomega-1.0+git20160910.d59fa0a/ghttp/protobuf/simple_message.pb.go 2016-09-11 05:10:23.000000000 +0000 @@ -0,0 +1,55 @@ +// Code generated by protoc-gen-go. +// source: simple_message.proto +// DO NOT EDIT! + +/* +Package protobuf is a generated protocol buffer package. + +It is generated from these files: + simple_message.proto + +It has these top-level messages: + SimpleMessage +*/ +package protobuf + +import proto "github.com/golang/protobuf/proto" +import fmt "fmt" +import math "math" + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +type SimpleMessage struct { + Description *string `protobuf:"bytes,1,req,name=description" json:"description,omitempty"` + Id *int32 `protobuf:"varint,2,req,name=id" json:"id,omitempty"` + Metadata *string `protobuf:"bytes,3,opt,name=metadata" json:"metadata,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *SimpleMessage) Reset() { *m = SimpleMessage{} } +func (m *SimpleMessage) String() string { return proto.CompactTextString(m) } +func (*SimpleMessage) ProtoMessage() {} + +func (m *SimpleMessage) GetDescription() string { + if m != nil && m.Description != nil { + return *m.Description + } + return "" +} + +func (m *SimpleMessage) GetId() int32 { + if m != nil && m.Id != nil { + return *m.Id + } + return 0 +} + +func (m *SimpleMessage) GetMetadata() string { + if m != nil && m.Metadata != nil { + return *m.Metadata + } + return "" +} diff -Nru golang-gomega-1.0/ghttp/protobuf/simple_message.proto golang-gomega-1.0+git20160910.d59fa0a/ghttp/protobuf/simple_message.proto --- golang-gomega-1.0/ghttp/protobuf/simple_message.proto 1970-01-01 00:00:00.000000000 +0000 +++ golang-gomega-1.0+git20160910.d59fa0a/ghttp/protobuf/simple_message.proto 2016-09-11 05:10:23.000000000 +0000 @@ -0,0 +1,9 @@ +syntax = "proto2"; + +package protobuf; + +message SimpleMessage { + required string description = 1; + required int32 id = 2; + optional string metadata = 3; +} diff -Nru golang-gomega-1.0/ghttp/test_server.go golang-gomega-1.0+git20160910.d59fa0a/ghttp/test_server.go --- golang-gomega-1.0/ghttp/test_server.go 2014-08-02 20:42:16.000000000 +0000 +++ golang-gomega-1.0+git20160910.d59fa0a/ghttp/test_server.go 2016-09-11 05:10:23.000000000 +0000 @@ -106,12 +106,16 @@ package ghttp import ( + "fmt" + "io" "io/ioutil" "net/http" "net/http/httptest" "reflect" "regexp" + "strings" "sync" + . "github.com/onsi/gomega" ) @@ -137,6 +141,13 @@ return s } +// NewUnstartedServer return a new, unstarted, `*ghttp.Server`. Useful for specifying a custom listener on `server.HTTPTestServer`. +func NewUnstartedServer() *Server { + s := new() + s.HTTPTestServer = httptest.NewUnstartedServer(s) + return s +} + // NewTLSServer returns a new `*ghttp.Server` that wraps an `httptest` TLS server. The server is started automatically. func NewTLSServer() *Server { s := new() @@ -156,6 +167,11 @@ //Only applies if AllowUnhandledRequests is true UnhandledRequestStatusCode int + //If provided, ghttp will log about each request received to the provided io.Writer + //Defaults to nil + //If you're using Ginkgo, set this to GinkgoWriter to get improved output during failures + Writer io.Writer + receivedRequests []*http.Request requestHandlers []http.HandlerFunc routedHandlers []routedHandler @@ -164,16 +180,31 @@ calls int } +//Start() starts an unstarted ghttp server. It is a catastrophic error to call Start more than once (thanks, httptest). +func (s *Server) Start() { + s.HTTPTestServer.Start() +} + //URL() returns a url that will hit the server func (s *Server) URL() string { return s.HTTPTestServer.URL } +//Addr() returns the address on which the server is listening. +func (s *Server) Addr() string { + return s.HTTPTestServer.Listener.Addr().String() +} + //Close() should be called at the end of each test. It spins down and cleans up the test server. func (s *Server) Close() { + s.writeLock.Lock() + defer s.writeLock.Unlock() + server := s.HTTPTestServer s.HTTPTestServer = nil - server.Close() + if server != nil { + server.Close() + } } //ServeHTTP() makes Server an http.Handler @@ -186,17 +217,47 @@ // b) If AllowUnhandledRequests is false, the request will not be handled and the current test will be marked as failed. func (s *Server) ServeHTTP(w http.ResponseWriter, req *http.Request) { s.writeLock.Lock() - defer s.writeLock.Unlock() defer func() { - recover() + e := recover() + if e != nil { + w.WriteHeader(http.StatusInternalServerError) + } + + //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 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 + eAsString, ok := e.(string) + if ok && strings.Contains(eAsString, "defer GinkgoRecover()") { + return + } + + //If we're here, we have to do step 2: assert that the error is nil. This assertion will + //allow us to fail the test suite (note: we can't call Fail since Gomega is not allowed to import Ginkgo). + //Since a failed assertion throws a panic, and we are likely in a goroutine, we need to defer within our defer! + defer func() { + recover() + }() + Ω(e).Should(BeNil(), "Handler Panicked") }() + if s.Writer != nil { + s.Writer.Write([]byte(fmt.Sprintf("GHTTP Received Request: %s - %s\n", req.Method, req.URL))) + } + + s.receivedRequests = append(s.receivedRequests, req) if routedHandler, ok := s.handlerForRoute(req.Method, req.URL.Path); ok { + s.writeLock.Unlock() routedHandler(w, req) } else if s.calls < len(s.requestHandlers) { - s.requestHandlers[s.calls](w, req) + h := s.requestHandlers[s.calls] s.calls++ + s.writeLock.Unlock() + h(w, req) } else { + s.writeLock.Unlock() if s.AllowUnhandledRequests { ioutil.ReadAll(req.Body) req.Body.Close() @@ -205,7 +266,6 @@ Ω(req).Should(BeNil(), "Received Unhandled Request") } } - s.receivedRequests = append(s.receivedRequests, req) } //ReceivedRequests is an array containing all requests received by the server (both handled and unhandled requests) @@ -291,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. @@ -301,3 +372,10 @@ existingHandler := s.GetHandler(index) s.SetHandler(index, CombineHandlers(existingHandler, handler)) } + +func (s *Server) CloseClientConnections() { + s.writeLock.Lock() + defer s.writeLock.Unlock() + + s.HTTPTestServer.CloseClientConnections() +} diff -Nru golang-gomega-1.0/ghttp/test_server_test.go golang-gomega-1.0+git20160910.d59fa0a/ghttp/test_server_test.go --- golang-gomega-1.0/ghttp/test_server_test.go 2014-08-02 20:42:16.000000000 +0000 +++ golang-gomega-1.0+git20160910.d59fa0a/ghttp/test_server_test.go 2016-09-11 05:10:23.000000000 +0000 @@ -2,9 +2,16 @@ import ( "bytes" + "io" "io/ioutil" "net/http" + "net/url" "regexp" + + "github.com/golang/protobuf/proto" + "github.com/onsi/gomega/gbytes" + "github.com/onsi/gomega/ghttp/protobuf" + . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" . "github.com/onsi/gomega/ghttp" @@ -25,6 +32,59 @@ 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.RouteToHandler("GET", "/", + func(w http.ResponseWriter, req *http.Request) { + io.WriteString(w, req.RemoteAddr) + }, + ) + 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()) + + 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()) + }) + }) + Describe("allowing unhandled requests", func() { Context("when true", func() { BeforeEach(func() { @@ -164,6 +224,69 @@ }) }) + Describe("When a handler fails", func() { + BeforeEach(func() { + s.UnhandledRequestStatusCode = http.StatusForbidden //just to be clear that 500s aren't coming from unhandled requests + }) + + Context("because the handler has panicked", func() { + BeforeEach(func() { + s.AppendHandlers(func(w http.ResponseWriter, req *http.Request) { + panic("bam") + }) + }) + + It("should respond with a 500 and make a failing assertion", func() { + var resp *http.Response + var err error + + failures := InterceptGomegaFailures(func() { + resp, err = http.Get(s.URL()) + }) + + Ω(err).ShouldNot(HaveOccurred()) + Ω(resp.StatusCode).Should(Equal(http.StatusInternalServerError)) + Ω(failures).Should(ConsistOf(ContainSubstring("Handler Panicked"))) + }) + }) + + Context("because an assertion has failed", func() { + BeforeEach(func() { + s.AppendHandlers(func(w http.ResponseWriter, req *http.Request) { + // Ω(true).Should(BeFalse()) <-- would be nice to do it this way, but the test just can't be written this way + + By("We're cheating a bit here -- we're throwing a GINKGO_PANIC which simulates a failed assertion") + panic(GINKGO_PANIC) + }) + }) + + It("should respond with a 500 and *not* make a failing assertion, instead relying on Ginkgo to have already been notified of the error", func() { + resp, err := http.Get(s.URL()) + + Ω(err).ShouldNot(HaveOccurred()) + Ω(resp.StatusCode).Should(Equal(http.StatusInternalServerError)) + }) + }) + }) + + Describe("Logging to the Writer", func() { + var buf *gbytes.Buffer + BeforeEach(func() { + buf = gbytes.NewBuffer() + s.Writer = buf + s.AppendHandlers(func(w http.ResponseWriter, req *http.Request) {}) + s.AppendHandlers(func(w http.ResponseWriter, req *http.Request) {}) + }) + + It("should write to the buffer when a request comes in", func() { + http.Get(s.URL() + "/foo") + Ω(buf).Should(gbytes.Say("GHTTP Received Request: GET - /foo\n")) + + http.Post(s.URL()+"/bar", "", nil) + Ω(buf).Should(gbytes.Say("GHTTP Received Request: POST - /bar\n")) + }) + }) + Describe("Request Handlers", func() { Describe("VerifyRequest", func() { BeforeEach(func() { @@ -195,6 +318,18 @@ resp, err = http.Get(s.URL() + "/foo?baz=bar") Ω(err).ShouldNot(HaveOccurred()) }) + + It("should match irregardless of query parameter ordering", func() { + s.SetHandler(0, VerifyRequest("GET", "/foo", "type=get&name=money")) + u, _ := url.Parse(s.URL() + "/foo") + u.RawQuery = url.Values{ + "type": []string{"get"}, + "name": []string{"money"}, + }.Encode() + + resp, err = http.Get(u.String()) + Ω(err).ShouldNot(HaveOccurred()) + }) }) Context("when passed a matcher for path", func() { @@ -263,6 +398,15 @@ Ω(failures).Should(HaveLen(1)) }) + It("should require basic auth header", func() { + req, err := http.NewRequest("GET", s.URL()+"/foo", nil) + Ω(err).ShouldNot(HaveOccurred()) + + failures := InterceptGomegaFailures(func() { + http.DefaultClient.Do(req) + }) + Ω(failures).Should(ContainElement(ContainSubstring("Authorization header must be specified"))) + }) }) Describe("VerifyHeader", func() { @@ -340,6 +484,27 @@ }) }) + Describe("VerifyBody", func() { + BeforeEach(func() { + s.AppendHandlers(CombineHandlers( + VerifyRequest("POST", "/foo"), + VerifyBody([]byte("some body")), + )) + }) + + It("should verify the body", func() { + resp, err = http.Post(s.URL()+"/foo", "", bytes.NewReader([]byte("some body"))) + Ω(err).ShouldNot(HaveOccurred()) + }) + + It("should verify the body", func() { + failures := InterceptGomegaFailures(func() { + http.Post(s.URL()+"/foo", "", bytes.NewReader([]byte("wrong body"))) + }) + Ω(failures).Should(HaveLen(1)) + }) + }) + Describe("VerifyJSON", func() { BeforeEach(func() { s.AppendHandlers(CombineHandlers( @@ -389,6 +554,186 @@ }) }) + Describe("VerifyForm", func() { + var formValues url.Values + + BeforeEach(func() { + formValues = make(url.Values) + formValues.Add("users", "user1") + formValues.Add("users", "user2") + formValues.Add("group", "users") + }) + + Context("when encoded in the URL", func() { + BeforeEach(func() { + s.AppendHandlers(CombineHandlers( + VerifyRequest("GET", "/foo"), + VerifyForm(url.Values{ + "users": []string{"user1", "user2"}, + "group": []string{"users"}, + }), + )) + }) + + It("should verify form values", func() { + resp, err = http.Get(s.URL() + "/foo?" + formValues.Encode()) + Ω(err).ShouldNot(HaveOccurred()) + }) + + It("should ignore extra values", func() { + formValues.Add("extra", "value") + resp, err = http.Get(s.URL() + "/foo?" + formValues.Encode()) + Ω(err).ShouldNot(HaveOccurred()) + }) + + It("fail on missing values", func() { + formValues.Del("group") + failures := InterceptGomegaFailures(func() { + resp, err = http.Get(s.URL() + "/foo?" + formValues.Encode()) + }) + Ω(failures).Should(HaveLen(1)) + }) + + It("fail on incorrect values", func() { + formValues.Set("group", "wheel") + failures := InterceptGomegaFailures(func() { + resp, err = http.Get(s.URL() + "/foo?" + formValues.Encode()) + }) + Ω(failures).Should(HaveLen(1)) + }) + }) + + Context("when present in the body", func() { + BeforeEach(func() { + s.AppendHandlers(CombineHandlers( + VerifyRequest("POST", "/foo"), + VerifyForm(url.Values{ + "users": []string{"user1", "user2"}, + "group": []string{"users"}, + }), + )) + }) + + It("should verify form values", func() { + resp, err = http.PostForm(s.URL()+"/foo", formValues) + Ω(err).ShouldNot(HaveOccurred()) + }) + + It("should ignore extra values", func() { + formValues.Add("extra", "value") + resp, err = http.PostForm(s.URL()+"/foo", formValues) + Ω(err).ShouldNot(HaveOccurred()) + }) + + It("fail on missing values", func() { + formValues.Del("group") + failures := InterceptGomegaFailures(func() { + resp, err = http.PostForm(s.URL()+"/foo", formValues) + }) + Ω(failures).Should(HaveLen(1)) + }) + + It("fail on incorrect values", func() { + formValues.Set("group", "wheel") + failures := InterceptGomegaFailures(func() { + resp, err = http.PostForm(s.URL()+"/foo", formValues) + }) + Ω(failures).Should(HaveLen(1)) + }) + }) + }) + + Describe("VerifyFormKV", func() { + Context("when encoded in the URL", func() { + BeforeEach(func() { + s.AppendHandlers(CombineHandlers( + VerifyRequest("GET", "/foo"), + VerifyFormKV("users", "user1", "user2"), + )) + }) + + It("verifies the form value", func() { + resp, err = http.Get(s.URL() + "/foo?users=user1&users=user2") + Ω(err).ShouldNot(HaveOccurred()) + }) + + It("verifies the form value", func() { + failures := InterceptGomegaFailures(func() { + resp, err = http.Get(s.URL() + "/foo?users=user1") + }) + Ω(failures).Should(HaveLen(1)) + }) + }) + + Context("when present in the body", func() { + BeforeEach(func() { + s.AppendHandlers(CombineHandlers( + VerifyRequest("POST", "/foo"), + VerifyFormKV("users", "user1", "user2"), + )) + }) + + It("verifies the form value", func() { + resp, err = http.PostForm(s.URL()+"/foo", url.Values{"users": []string{"user1", "user2"}}) + Ω(err).ShouldNot(HaveOccurred()) + }) + + It("verifies the form value", func() { + failures := InterceptGomegaFailures(func() { + resp, err = http.PostForm(s.URL()+"/foo", url.Values{"users": []string{"user1"}}) + }) + Ω(failures).Should(HaveLen(1)) + }) + }) + }) + + Describe("VerifyProtoRepresenting", func() { + var message *protobuf.SimpleMessage + + BeforeEach(func() { + message = new(protobuf.SimpleMessage) + message.Description = proto.String("A description") + message.Id = proto.Int32(0) + + s.AppendHandlers(CombineHandlers( + VerifyRequest("POST", "/proto"), + VerifyProtoRepresenting(message), + )) + }) + + It("verifies the proto body and the content type", func() { + serialized, err := proto.Marshal(message) + Ω(err).ShouldNot(HaveOccurred()) + + resp, err = http.Post(s.URL()+"/proto", "application/x-protobuf", bytes.NewReader(serialized)) + Ω(err).ShouldNot(HaveOccurred()) + }) + + It("should verify the proto body and the content type", func() { + serialized, err := proto.Marshal(&protobuf.SimpleMessage{ + Description: proto.String("A description"), + Id: proto.Int32(0), + Metadata: proto.String("some metadata"), + }) + Ω(err).ShouldNot(HaveOccurred()) + + failures := InterceptGomegaFailures(func() { + http.Post(s.URL()+"/proto", "application/x-protobuf", bytes.NewReader(serialized)) + }) + Ω(failures).Should(HaveLen(1)) + }) + + It("should verify the proto body and the content type", func() { + serialized, err := proto.Marshal(message) + Ω(err).ShouldNot(HaveOccurred()) + + failures := InterceptGomegaFailures(func() { + http.Post(s.URL()+"/proto", "application/not-x-protobuf", bytes.NewReader(serialized)) + }) + Ω(failures).Should(HaveLen(1)) + }) + }) + Describe("RespondWith", func() { Context("without headers", func() { BeforeEach(func() { @@ -506,49 +851,238 @@ }) Describe("RespondWithJSON", func() { - BeforeEach(func() { - s.AppendHandlers(CombineHandlers( - VerifyRequest("POST", "/foo"), - RespondWithJSONEncoded(http.StatusCreated, []int{1, 2, 3}), - )) + Context("when no optional headers are set", func() { + BeforeEach(func() { + s.AppendHandlers(CombineHandlers( + VerifyRequest("POST", "/foo"), + RespondWithJSONEncoded(http.StatusCreated, []int{1, 2, 3}), + )) + }) + + It("should return the response", func() { + resp, err = http.Post(s.URL()+"/foo", "application/json", nil) + Ω(err).ShouldNot(HaveOccurred()) + + Ω(resp.StatusCode).Should(Equal(http.StatusCreated)) + + body, err := ioutil.ReadAll(resp.Body) + Ω(err).ShouldNot(HaveOccurred()) + Ω(body).Should(MatchJSON("[1,2,3]")) + }) + + It("should set the Content-Type header to application/json", func() { + resp, err = http.Post(s.URL()+"/foo", "application/json", nil) + Ω(err).ShouldNot(HaveOccurred()) + + Ω(resp.Header["Content-Type"]).Should(Equal([]string{"application/json"})) + }) }) - It("should return the response", func() { - resp, err = http.Post(s.URL()+"/foo", "application/json", nil) - Ω(err).ShouldNot(HaveOccurred()) + Context("when optional headers are set", func() { + var headers http.Header + BeforeEach(func() { + headers = http.Header{"Stuff": []string{"things"}} + }) - Ω(resp.StatusCode).Should(Equal(http.StatusCreated)) + JustBeforeEach(func() { + s.AppendHandlers(CombineHandlers( + VerifyRequest("POST", "/foo"), + RespondWithJSONEncoded(http.StatusCreated, []int{1, 2, 3}, headers), + )) + }) - body, err := ioutil.ReadAll(resp.Body) - Ω(err).ShouldNot(HaveOccurred()) - Ω(body).Should(MatchJSON("[1,2,3]")) + It("should preserve those headers", func() { + resp, err = http.Post(s.URL()+"/foo", "application/json", nil) + Ω(err).ShouldNot(HaveOccurred()) + + Ω(resp.Header["Stuff"]).Should(Equal([]string{"things"})) + }) + + It("should set the Content-Type header to application/json", func() { + resp, err = http.Post(s.URL()+"/foo", "application/json", nil) + Ω(err).ShouldNot(HaveOccurred()) + + Ω(resp.Header["Content-Type"]).Should(Equal([]string{"application/json"})) + }) + + Context("when setting the Content-Type explicitly", func() { + BeforeEach(func() { + headers["Content-Type"] = []string{"not-json"} + }) + + It("should use the Content-Type header that was explicitly set", func() { + resp, err = http.Post(s.URL()+"/foo", "application/json", nil) + Ω(err).ShouldNot(HaveOccurred()) + + Ω(resp.Header["Content-Type"]).Should(Equal([]string{"not-json"})) + }) + }) }) }) Describe("RespondWithJSONPtr", func() { + type testObject struct { + Key string + Value string + } + var code int - var object interface{} + var object testObject + + Context("when no optional headers are set", func() { + BeforeEach(func() { + code = http.StatusOK + object = testObject{} + s.AppendHandlers(CombineHandlers( + VerifyRequest("POST", "/foo"), + RespondWithJSONEncodedPtr(&code, &object), + )) + }) + + It("should return the response", func() { + code = http.StatusCreated + object = testObject{ + Key: "Jim", + Value: "Codes", + } + resp, err = http.Post(s.URL()+"/foo", "application/json", nil) + Ω(err).ShouldNot(HaveOccurred()) + + Ω(resp.StatusCode).Should(Equal(http.StatusCreated)) + + body, err := ioutil.ReadAll(resp.Body) + Ω(err).ShouldNot(HaveOccurred()) + Ω(body).Should(MatchJSON(`{"Key": "Jim", "Value": "Codes"}`)) + }) + + It("should set the Content-Type header to application/json", func() { + resp, err = http.Post(s.URL()+"/foo", "application/json", nil) + Ω(err).ShouldNot(HaveOccurred()) + + Ω(resp.Header["Content-Type"]).Should(Equal([]string{"application/json"})) + }) + }) + + Context("when optional headers are set", func() { + var headers http.Header + BeforeEach(func() { + headers = http.Header{"Stuff": []string{"things"}} + }) + + JustBeforeEach(func() { + code = http.StatusOK + object = testObject{} + s.AppendHandlers(CombineHandlers( + VerifyRequest("POST", "/foo"), + RespondWithJSONEncodedPtr(&code, &object, headers), + )) + }) + + It("should preserve those headers", func() { + resp, err = http.Post(s.URL()+"/foo", "application/json", nil) + Ω(err).ShouldNot(HaveOccurred()) + + Ω(resp.Header["Stuff"]).Should(Equal([]string{"things"})) + }) + + It("should set the Content-Type header to application/json", func() { + resp, err = http.Post(s.URL()+"/foo", "application/json", nil) + Ω(err).ShouldNot(HaveOccurred()) + + Ω(resp.Header["Content-Type"]).Should(Equal([]string{"application/json"})) + }) + + Context("when setting the Content-Type explicitly", func() { + BeforeEach(func() { + headers["Content-Type"] = []string{"not-json"} + }) + + It("should use the Content-Type header that was explicitly set", func() { + resp, err = http.Post(s.URL()+"/foo", "application/json", nil) + Ω(err).ShouldNot(HaveOccurred()) + + Ω(resp.Header["Content-Type"]).Should(Equal([]string{"not-json"})) + }) + }) + }) + }) + + Describe("RespondWithProto", func() { + var message *protobuf.SimpleMessage + BeforeEach(func() { - code = http.StatusOK - object = []int{1, 2, 3} + message = new(protobuf.SimpleMessage) + message.Description = proto.String("A description") + message.Id = proto.Int32(99) + }) - s.AppendHandlers(CombineHandlers( - VerifyRequest("POST", "/foo"), - RespondWithJSONEncodedPtr(&code, &object), - )) + Context("when no optional headers are set", func() { + BeforeEach(func() { + s.AppendHandlers(CombineHandlers( + VerifyRequest("POST", "/proto"), + RespondWithProto(http.StatusCreated, message), + )) + }) + + It("should return the response", func() { + resp, err = http.Post(s.URL()+"/proto", "application/x-protobuf", nil) + Ω(err).ShouldNot(HaveOccurred()) + + Ω(resp.StatusCode).Should(Equal(http.StatusCreated)) + + var received protobuf.SimpleMessage + body, err := ioutil.ReadAll(resp.Body) + err = proto.Unmarshal(body, &received) + Ω(err).ShouldNot(HaveOccurred()) + }) + + It("should set the Content-Type header to application/x-protobuf", func() { + resp, err = http.Post(s.URL()+"/proto", "application/x-protobuf", nil) + Ω(err).ShouldNot(HaveOccurred()) + + Ω(resp.Header["Content-Type"]).Should(Equal([]string{"application/x-protobuf"})) + }) }) - It("should return the response", func() { - code = http.StatusCreated - object = []int{4, 5, 6} - resp, err = http.Post(s.URL()+"/foo", "application/json", nil) - Ω(err).ShouldNot(HaveOccurred()) + Context("when optional headers are set", func() { + var headers http.Header + BeforeEach(func() { + headers = http.Header{"Stuff": []string{"things"}} + }) - Ω(resp.StatusCode).Should(Equal(http.StatusCreated)) + JustBeforeEach(func() { + s.AppendHandlers(CombineHandlers( + VerifyRequest("POST", "/proto"), + RespondWithProto(http.StatusCreated, message, headers), + )) + }) - body, err := ioutil.ReadAll(resp.Body) - Ω(err).ShouldNot(HaveOccurred()) - Ω(body).Should(MatchJSON("[4,5,6]")) + It("should preserve those headers", func() { + resp, err = http.Post(s.URL()+"/proto", "application/x-protobuf", nil) + Ω(err).ShouldNot(HaveOccurred()) + + Ω(resp.Header["Stuff"]).Should(Equal([]string{"things"})) + }) + + It("should set the Content-Type header to application/x-protobuf", func() { + resp, err = http.Post(s.URL()+"/proto", "application/x-protobuf", nil) + Ω(err).ShouldNot(HaveOccurred()) + + Ω(resp.Header["Content-Type"]).Should(Equal([]string{"application/x-protobuf"})) + }) + + Context("when setting the Content-Type explicitly", func() { + BeforeEach(func() { + headers["Content-Type"] = []string{"not-x-protobuf"} + }) + + It("should use the Content-Type header that was explicitly set", func() { + resp, err = http.Post(s.URL()+"/proto", "application/x-protobuf", nil) + Ω(err).ShouldNot(HaveOccurred()) + + Ω(resp.Header["Content-Type"]).Should(Equal([]string{"not-x-protobuf"})) + }) + }) }) }) }) diff -Nru golang-gomega-1.0/.gitignore golang-gomega-1.0+git20160910.d59fa0a/.gitignore --- golang-gomega-1.0/.gitignore 2014-08-02 20:42:16.000000000 +0000 +++ golang-gomega-1.0+git20160910.d59fa0a/.gitignore 2016-09-11 05:10:23.000000000 +0000 @@ -1,3 +1,5 @@ .DS_Store *.test . +.idea +gomega.iml diff -Nru golang-gomega-1.0/gomega_dsl.go golang-gomega-1.0+git20160910.d59fa0a/gomega_dsl.go --- golang-gomega-1.0/gomega_dsl.go 2014-08-02 20:42:16.000000000 +0000 +++ golang-gomega-1.0+git20160910.d59fa0a/gomega_dsl.go 2016-09-11 05:10:23.000000000 +0000 @@ -26,6 +26,11 @@ const GOMEGA_VERSION = "1.0" +const nilFailHandlerPanic = `You are trying to make an assertion, but Gomega's fail handler is nil. +If you're using Ginkgo then you probably forgot to put your assertion in an It(). +Alternatively, you may have forgotten to register a fail handler with RegisterFailHandler() or RegisterTestingT(). +` + var globalFailHandler types.GomegaFailHandler var defaultEventuallyTimeout = time.Second @@ -130,6 +135,9 @@ //error message to refer to the calling line in the test (as opposed to the line in the helper function) //set the first argument of `ExpectWithOffset` appropriately. func ExpectWithOffset(offset int, actual interface{}, extra ...interface{}) GomegaAssertion { + if globalFailHandler == nil { + panic(nilFailHandlerPanic) + } return assertion.New(actual, globalFailHandler, offset, extra...) } @@ -177,6 +185,9 @@ //initial argument to indicate an offset in the call stack. This is useful when building helper //functions that contain matchers. To learn more, read about `ExpectWithOffset`. func EventuallyWithOffset(offset int, actual interface{}, intervals ...interface{}) GomegaAsyncAssertion { + if globalFailHandler == nil { + panic(nilFailHandlerPanic) + } timeoutInterval := defaultEventuallyTimeout pollingInterval := defaultEventuallyPollingInterval if len(intervals) > 0 { @@ -219,6 +230,9 @@ //initial argument to indicate an offset in the call stack. This is useful when building helper //functions that contain matchers. To learn more, read about `ExpectWithOffset`. func ConsistentlyWithOffset(offset int, actual interface{}, intervals ...interface{}) GomegaAsyncAssertion { + if globalFailHandler == nil { + panic(nilFailHandlerPanic) + } timeoutInterval := defaultConsistentlyDuration pollingInterval := defaultConsistentlyPollingInterval if len(intervals) > 0 { diff -Nru golang-gomega-1.0/gstruct/elements.go golang-gomega-1.0+git20160910.d59fa0a/gstruct/elements.go --- golang-gomega-1.0/gstruct/elements.go 1970-01-01 00:00:00.000000000 +0000 +++ golang-gomega-1.0+git20160910.d59fa0a/gstruct/elements.go 2016-09-11 05:10:23.000000000 +0000 @@ -0,0 +1,141 @@ +package gstruct + +import ( + "errors" + "fmt" + "reflect" + "runtime/debug" + + "github.com/onsi/gomega/format" + errorsutil "github.com/onsi/gomega/gstruct/errors" + "github.com/onsi/gomega/types" +) + +//MatchAllElements succeeds if every element of a slice matches the element matcher it maps to +//through the id function, and every element matcher is matched. +// Expect([]string{"a", "b"}).To(MatchAllElements(idFn, matchers.Elements{ +// "a": BeEqual("a"), +// "b": BeEqual("b"), +// }) +func MatchAllElements(identifier Identifier, elements Elements) types.GomegaMatcher { + return &ElementsMatcher{ + Identifier: identifier, + Elements: elements, + } +} + +//MatchElements succeeds if each element of a slice matches the element matcher it maps to +//through the id function. It can ignore extra elements and/or missing elements. +// Expect([]string{"a", "c"}).To(MatchElements(idFn, IgnoreMissing|IgnoreExtra, matchers.Elements{ +// "a": BeEqual("a") +// "b": BeEqual("b"), +// }) +func MatchElements(identifier Identifier, options Options, elements Elements) types.GomegaMatcher { + return &ElementsMatcher{ + Identifier: identifier, + Elements: elements, + IgnoreExtras: options&IgnoreExtras != 0, + IgnoreMissing: options&IgnoreMissing != 0, + } +} + +// ElementsMatcher is a NestingMatcher that applies custom matchers to each element of a slice mapped +// by the Identifier function. +// TODO: Extend this to work with arrays & maps (map the key) as well. +type ElementsMatcher struct { + // Matchers for each element. + Elements Elements + // Function mapping an element to the string key identifying its matcher. + Identifier Identifier + + // Whether to ignore extra elements or consider it an error. + IgnoreExtras bool + // Whether to ignore missing elements or consider it an error. + IgnoreMissing bool + + // State. + failures []error +} + +// Element ID to matcher. +type Elements map[string]types.GomegaMatcher + +// Function for identifying (mapping) elements. +type Identifier func(element interface{}) string + +func (m *ElementsMatcher) Match(actual interface{}) (success bool, err error) { + if reflect.TypeOf(actual).Kind() != reflect.Slice { + return false, fmt.Errorf("%v is type %T, expected slice", actual, actual) + } + + m.failures = m.matchElements(actual) + if len(m.failures) > 0 { + return false, nil + } + return true, nil +} + +func (m *ElementsMatcher) matchElements(actual interface{}) (errs []error) { + // Provide more useful error messages in the case of a panic. + defer func() { + if err := recover(); err != nil { + errs = append(errs, fmt.Errorf("panic checking %+v: %v\n%s", actual, err, debug.Stack())) + } + }() + + val := reflect.ValueOf(actual) + elements := map[string]bool{} + for i := 0; i < val.Len(); i++ { + element := val.Index(i).Interface() + id := m.Identifier(element) + // TODO: Add options to ignore & match duplicates. + if elements[id] { + errs = append(errs, fmt.Errorf("found duplicate element ID %s", id)) + continue + } + elements[id] = true + + matcher, expected := m.Elements[id] + if !expected { + if !m.IgnoreExtras { + errs = append(errs, fmt.Errorf("unexpected element %s", id)) + } + continue + } + + match, err := matcher.Match(element) + if match { + continue + } + + if err == nil { + if nesting, ok := matcher.(errorsutil.NestingMatcher); ok { + err = errorsutil.AggregateError(nesting.Failures()) + } else { + err = errors.New(matcher.FailureMessage(element)) + } + } + errs = append(errs, errorsutil.Nest(fmt.Sprintf("[%s]", id), err)) + } + + for id := range m.Elements { + if !elements[id] && !m.IgnoreMissing { + errs = append(errs, fmt.Errorf("missing expected element %s", id)) + } + } + + return errs +} + +func (m *ElementsMatcher) FailureMessage(actual interface{}) (message string) { + failure := errorsutil.AggregateError(m.failures) + return format.Message(actual, fmt.Sprintf("to match elements: %v", failure)) +} + +func (m *ElementsMatcher) NegatedFailureMessage(actual interface{}) (message string) { + return format.Message(actual, "not to match elements") +} + +func (m *ElementsMatcher) Failures() []error { + return m.failures +} diff -Nru golang-gomega-1.0/gstruct/elements_test.go golang-gomega-1.0+git20160910.d59fa0a/gstruct/elements_test.go --- golang-gomega-1.0/gstruct/elements_test.go 1970-01-01 00:00:00.000000000 +0000 +++ golang-gomega-1.0+git20160910.d59fa0a/gstruct/elements_test.go 2016-09-11 05:10:23.000000000 +0000 @@ -0,0 +1,85 @@ +package gstruct_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + . "github.com/onsi/gomega/gstruct" +) + +var _ = Describe("Slice", func() { + allElements := []string{"a", "b"} + missingElements := []string{"a"} + extraElements := []string{"a", "b", "c"} + duplicateElements := []string{"a", "a", "b"} + empty := []string{} + var nils []string + + It("should strictly match all elements", func() { + m := MatchAllElements(id, Elements{ + "b": Equal("b"), + "a": Equal("a"), + }) + Ω(allElements).Should(m, "should match all elements") + Ω(missingElements).ShouldNot(m, "should fail with missing elements") + Ω(extraElements).ShouldNot(m, "should fail with extra elements") + Ω(duplicateElements).ShouldNot(m, "should fail with duplicate elements") + Ω(nils).ShouldNot(m, "should fail with an uninitialized slice") + + m = MatchAllElements(id, Elements{ + "a": Equal("a"), + "b": Equal("fail"), + }) + Ω(allElements).ShouldNot(m, "should run nested matchers") + + m = MatchAllElements(id, Elements{}) + Ω(empty).Should(m, "should handle empty slices") + Ω(allElements).ShouldNot(m, "should handle only empty slices") + Ω(nils).Should(m, "should handle nil slices") + }) + + It("should ignore extra elements", func() { + m := MatchElements(id, IgnoreExtras, Elements{ + "b": Equal("b"), + "a": Equal("a"), + }) + Ω(allElements).Should(m, "should match all elements") + Ω(missingElements).ShouldNot(m, "should fail with missing elements") + Ω(extraElements).Should(m, "should ignore extra elements") + Ω(duplicateElements).ShouldNot(m, "should fail with duplicate elements") + Ω(nils).ShouldNot(m, "should fail with an uninitialized slice") + }) + + It("should ignore missing elements", func() { + m := MatchElements(id, IgnoreMissing, Elements{ + "a": Equal("a"), + "b": Equal("b"), + }) + Ω(allElements).Should(m, "should match all elements") + Ω(missingElements).Should(m, "should ignore missing elements") + Ω(extraElements).ShouldNot(m, "should fail with extra elements") + Ω(duplicateElements).ShouldNot(m, "should fail with duplicate elements") + Ω(nils).Should(m, "should ignore an uninitialized slice") + }) + + It("should ignore missing and extra elements", func() { + m := MatchElements(id, IgnoreMissing|IgnoreExtras, Elements{ + "a": Equal("a"), + "b": Equal("b"), + }) + Ω(allElements).Should(m, "should match all elements") + Ω(missingElements).Should(m, "should ignore missing elements") + Ω(extraElements).Should(m, "should ignore extra elements") + Ω(duplicateElements).ShouldNot(m, "should fail with duplicate elements") + Ω(nils).Should(m, "should ignore an uninitialized slice") + + m = MatchElements(id, IgnoreExtras|IgnoreMissing, Elements{ + "a": Equal("a"), + "b": Equal("fail"), + }) + Ω(allElements).ShouldNot(m, "should run nested matchers") + }) +}) + +func id(element interface{}) string { + return element.(string) +} diff -Nru golang-gomega-1.0/gstruct/errors/nested_types.go golang-gomega-1.0+git20160910.d59fa0a/gstruct/errors/nested_types.go --- golang-gomega-1.0/gstruct/errors/nested_types.go 1970-01-01 00:00:00.000000000 +0000 +++ golang-gomega-1.0+git20160910.d59fa0a/gstruct/errors/nested_types.go 2016-09-11 05:10:23.000000000 +0000 @@ -0,0 +1,72 @@ +package errors + +import ( + "fmt" + "strings" + + "github.com/onsi/gomega/types" +) + +// A stateful matcher that nests other matchers within it and preserves the error types of the +// nested matcher failures. +type NestingMatcher interface { + types.GomegaMatcher + + // Returns the failures of nested matchers. + Failures() []error +} + +// An error type for labeling errors on deeply nested matchers. +type NestedError struct { + Path string + Err error +} + +func (e *NestedError) Error() string { + // Indent Errors. + indented := strings.Replace(e.Err.Error(), "\n", "\n\t", -1) + return fmt.Sprintf("%s:\n\t%v", e.Path, indented) +} + +// Create a NestedError with the given path. +// If err is a NestedError, prepend the path to it. +// If err is an AggregateError, recursively Nest each error. +func Nest(path string, err error) error { + if ag, ok := err.(AggregateError); ok { + var errs AggregateError + for _, e := range ag { + errs = append(errs, Nest(path, e)) + } + return errs + } + if ne, ok := err.(*NestedError); ok { + return &NestedError{ + Path: path + ne.Path, + Err: ne.Err, + } + } + return &NestedError{ + Path: path, + Err: err, + } +} + +// An error type for treating multiple errors as a single error. +type AggregateError []error + +// Error is part of the error interface. +func (err AggregateError) Error() string { + if len(err) == 0 { + // This should never happen, really. + return "" + } + if len(err) == 1 { + return err[0].Error() + } + result := fmt.Sprintf("[%s", err[0].Error()) + for i := 1; i < len(err); i++ { + result += fmt.Sprintf(", %s", err[i].Error()) + } + result += "]" + return result +} diff -Nru golang-gomega-1.0/gstruct/fields.go golang-gomega-1.0+git20160910.d59fa0a/gstruct/fields.go --- golang-gomega-1.0/gstruct/fields.go 1970-01-01 00:00:00.000000000 +0000 +++ golang-gomega-1.0+git20160910.d59fa0a/gstruct/fields.go 2016-09-11 05:10:23.000000000 +0000 @@ -0,0 +1,141 @@ +package gstruct + +import ( + "errors" + "fmt" + "reflect" + "runtime/debug" + "strings" + + "github.com/onsi/gomega/format" + errorsutil "github.com/onsi/gomega/gstruct/errors" + "github.com/onsi/gomega/types" +) + +//MatchAllFields succeeds if every field of a struct matches the field matcher associated with +//it, and every element matcher is matched. +// Expect([]string{"a", "b"}).To(MatchAllFields(idFn, gstruct.Fields{ +// "a": BeEqual("a"), +// "b": BeEqual("b"), +// }) +func MatchAllFields(fields Fields) types.GomegaMatcher { + return &FieldsMatcher{ + Fields: fields, + } +} + +//MatchFields succeeds if each element of a struct matches the field matcher associated with +//it. It can ignore extra fields and/or missing fields. +// Expect([]string{"a", "c"}).To(MatchFields(idFn, IgnoreMissing|IgnoreExtra, gstruct.Fields{ +// "a": BeEqual("a") +// "b": BeEqual("b"), +// }) +func MatchFields(options Options, fields Fields) types.GomegaMatcher { + return &FieldsMatcher{ + Fields: fields, + IgnoreExtras: options&IgnoreExtras != 0, + IgnoreMissing: options&IgnoreMissing != 0, + } +} + +type FieldsMatcher struct { + // Matchers for each field. + Fields Fields + + // Whether to ignore extra elements or consider it an error. + IgnoreExtras bool + // Whether to ignore missing elements or consider it an error. + IgnoreMissing bool + + // State. + failures []error +} + +// Field name to matcher. +type Fields map[string]types.GomegaMatcher + +func (m *FieldsMatcher) Match(actual interface{}) (success bool, err error) { + if reflect.TypeOf(actual).Kind() != reflect.Struct { + return false, fmt.Errorf("%v is type %T, expected struct", actual, actual) + } + + m.failures = m.matchFields(actual) + if len(m.failures) > 0 { + return false, nil + } + return true, nil +} + +func (m *FieldsMatcher) matchFields(actual interface{}) (errs []error) { + val := reflect.ValueOf(actual) + typ := val.Type() + fields := map[string]bool{} + for i := 0; i < val.NumField(); i++ { + fieldName := typ.Field(i).Name + fields[fieldName] = true + + err := func() (err error) { + // This test relies heavily on reflect, which tends to panic. + // Recover here to provide more useful error messages in that case. + defer func() { + if r := recover(); r != nil { + err = fmt.Errorf("panic checking %+v: %v\n%s", actual, r, debug.Stack()) + } + }() + + matcher, expected := m.Fields[fieldName] + if !expected { + if !m.IgnoreExtras { + return fmt.Errorf("unexpected field %s: %+v", fieldName, actual) + } + return nil + } + + var field interface{} + if val.Field(i).IsValid() { + field = val.Field(i).Interface() + } else { + field = reflect.Zero(typ.Field(i).Type) + } + + match, err := matcher.Match(field) + if err != nil { + return err + } else if !match { + if nesting, ok := matcher.(errorsutil.NestingMatcher); ok { + return errorsutil.AggregateError(nesting.Failures()) + } + return errors.New(matcher.FailureMessage(field)) + } + return nil + }() + if err != nil { + errs = append(errs, errorsutil.Nest("."+fieldName, err)) + } + } + + for field := range m.Fields { + if !fields[field] && !m.IgnoreMissing { + errs = append(errs, fmt.Errorf("missing expected field %s", field)) + } + } + + return errs +} + +func (m *FieldsMatcher) FailureMessage(actual interface{}) (message string) { + failures := make([]string, len(m.failures)) + for i := range m.failures { + failures[i] = m.failures[i].Error() + } + return format.Message(reflect.TypeOf(actual).Name(), + fmt.Sprintf("to match fields: {\n%v\n}\n", strings.Join(failures, "\n"))) +} + +func (m *FieldsMatcher) NegatedFailureMessage(actual interface{}) (message string) { + return format.Message(actual, "not to match fields") +} + +func (m *FieldsMatcher) Failures() []error { + return m.failures +} diff -Nru golang-gomega-1.0/gstruct/fields_test.go golang-gomega-1.0+git20160910.d59fa0a/gstruct/fields_test.go --- golang-gomega-1.0/gstruct/fields_test.go 1970-01-01 00:00:00.000000000 +0000 +++ golang-gomega-1.0+git20160910.d59fa0a/gstruct/fields_test.go 2016-09-11 05:10:23.000000000 +0000 @@ -0,0 +1,76 @@ +package gstruct_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + . "github.com/onsi/gomega/gstruct" +) + +var _ = Describe("Struct", func() { + allFields := struct{ A, B string }{"a", "b"} + missingFields := struct{ A string }{"a"} + extraFields := struct{ A, B, C string }{"a", "b", "c"} + emptyFields := struct{ A, B string }{} + + It("should strictly match all fields", func() { + m := MatchAllFields(Fields{ + "B": Equal("b"), + "A": Equal("a"), + }) + Ω(allFields).Should(m, "should match all fields") + Ω(missingFields).ShouldNot(m, "should fail with missing fields") + Ω(extraFields).ShouldNot(m, "should fail with extra fields") + Ω(emptyFields).ShouldNot(m, "should fail with empty fields") + + m = MatchAllFields(Fields{ + "A": Equal("a"), + "B": Equal("fail"), + }) + Ω(allFields).ShouldNot(m, "should run nested matchers") + }) + + It("should handle empty structs", func() { + m := MatchAllFields(Fields{}) + Ω(struct{}{}).Should(m, "should handle empty structs") + Ω(allFields).ShouldNot(m, "should fail with extra fields") + }) + + It("should ignore missing fields", func() { + m := MatchFields(IgnoreMissing, Fields{ + "B": Equal("b"), + "A": Equal("a"), + }) + Ω(allFields).Should(m, "should match all fields") + Ω(missingFields).Should(m, "should ignore missing fields") + Ω(extraFields).ShouldNot(m, "should fail with extra fields") + Ω(emptyFields).ShouldNot(m, "should fail with empty fields") + }) + + It("should ignore extra fields", func() { + m := MatchFields(IgnoreExtras, Fields{ + "B": Equal("b"), + "A": Equal("a"), + }) + Ω(allFields).Should(m, "should match all fields") + Ω(missingFields).ShouldNot(m, "should fail with missing fields") + Ω(extraFields).Should(m, "should ignore extra fields") + Ω(emptyFields).ShouldNot(m, "should fail with empty fields") + }) + + It("should ignore missing and extra fields", func() { + m := MatchFields(IgnoreMissing|IgnoreExtras, Fields{ + "B": Equal("b"), + "A": Equal("a"), + }) + Ω(allFields).Should(m, "should match all fields") + Ω(missingFields).Should(m, "should ignore missing fields") + Ω(extraFields).Should(m, "should ignore extra fields") + Ω(emptyFields).ShouldNot(m, "should fail with empty fields") + + m = MatchFields(IgnoreMissing|IgnoreExtras, Fields{ + "A": Equal("a"), + "B": Equal("fail"), + }) + Ω(allFields).ShouldNot(m, "should run nested matchers") + }) +}) diff -Nru golang-gomega-1.0/gstruct/gstruct_tests_suite_test.go golang-gomega-1.0+git20160910.d59fa0a/gstruct/gstruct_tests_suite_test.go --- golang-gomega-1.0/gstruct/gstruct_tests_suite_test.go 1970-01-01 00:00:00.000000000 +0000 +++ golang-gomega-1.0+git20160910.d59fa0a/gstruct/gstruct_tests_suite_test.go 2016-09-11 05:10:23.000000000 +0000 @@ -0,0 +1,13 @@ +package gstruct_test + +import ( + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +func Test(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Gstruct Suite") +} diff -Nru golang-gomega-1.0/gstruct/ignore.go golang-gomega-1.0+git20160910.d59fa0a/gstruct/ignore.go --- golang-gomega-1.0/gstruct/ignore.go 1970-01-01 00:00:00.000000000 +0000 +++ golang-gomega-1.0+git20160910.d59fa0a/gstruct/ignore.go 2016-09-11 05:10:23.000000000 +0000 @@ -0,0 +1,37 @@ +package gstruct + +import ( + "github.com/onsi/gomega/types" +) + +//Ignore ignores the actual value and always succeeds. +// Expect(nil).To(Ignore()) +// Expect(true).To(Ignore()) +func Ignore() types.GomegaMatcher { + return &IgnoreMatcher{true} +} + +//Reject ignores the actual value and always fails. It can be used in conjunction with IgnoreMissing +//to catch problematic elements, or to verify tests are running. +// Expect(nil).NotTo(Reject()) +// Expect(true).NotTo(Reject()) +func Reject() types.GomegaMatcher { + return &IgnoreMatcher{false} +} + +// A matcher that either always succeeds or always fails. +type IgnoreMatcher struct { + Succeed bool +} + +func (m *IgnoreMatcher) Match(actual interface{}) (bool, error) { + return m.Succeed, nil +} + +func (m *IgnoreMatcher) FailureMessage(_ interface{}) (message string) { + return "Unconditional failure" +} + +func (m *IgnoreMatcher) NegatedFailureMessage(_ interface{}) (message string) { + return "Unconditional success" +} diff -Nru golang-gomega-1.0/gstruct/ignore_test.go golang-gomega-1.0+git20160910.d59fa0a/gstruct/ignore_test.go --- golang-gomega-1.0/gstruct/ignore_test.go 1970-01-01 00:00:00.000000000 +0000 +++ golang-gomega-1.0+git20160910.d59fa0a/gstruct/ignore_test.go 2016-09-11 05:10:23.000000000 +0000 @@ -0,0 +1,23 @@ +package gstruct_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + . "github.com/onsi/gomega/gstruct" +) + +var _ = Describe("Ignore", func() { + It("should always succeed", func() { + Ω(nil).Should(Ignore()) + Ω(struct{}{}).Should(Ignore()) + Ω(0).Should(Ignore()) + Ω(false).Should(Ignore()) + }) + + It("should always fail", func() { + Ω(nil).ShouldNot(Reject()) + Ω(struct{}{}).ShouldNot(Reject()) + Ω(1).ShouldNot(Reject()) + Ω(true).ShouldNot(Reject()) + }) +}) diff -Nru golang-gomega-1.0/gstruct/pointer.go golang-gomega-1.0+git20160910.d59fa0a/gstruct/pointer.go --- golang-gomega-1.0/gstruct/pointer.go 1970-01-01 00:00:00.000000000 +0000 +++ golang-gomega-1.0+git20160910.d59fa0a/gstruct/pointer.go 2016-09-11 05:10:23.000000000 +0000 @@ -0,0 +1,56 @@ +package gstruct + +import ( + "fmt" + "reflect" + + "github.com/onsi/gomega/format" + "github.com/onsi/gomega/types" +) + +//PointTo applies the given matcher to the value pointed to by actual. It fails if the pointer is +//nil. +// actual := 5 +// Expect(&actual).To(PointTo(Equal(5))) +func PointTo(matcher types.GomegaMatcher) types.GomegaMatcher { + return &PointerMatcher{ + Matcher: matcher, + } +} + +type PointerMatcher struct { + Matcher types.GomegaMatcher + + // Failure message. + failure string +} + +func (m *PointerMatcher) Match(actual interface{}) (bool, error) { + val := reflect.ValueOf(actual) + + // return error if actual type is not a pointer + if val.Kind() != reflect.Ptr { + return false, fmt.Errorf("PointerMatcher expects a pointer but we have '%s'", val.Kind()) + } + + if !val.IsValid() || val.IsNil() { + m.failure = format.Message(actual, "not to be ") + return false, nil + } + + // Forward the value. + elem := val.Elem().Interface() + match, err := m.Matcher.Match(elem) + if !match { + m.failure = m.Matcher.FailureMessage(elem) + } + return match, err +} + +func (m *PointerMatcher) FailureMessage(_ interface{}) (message string) { + return m.failure +} + +func (m *PointerMatcher) NegatedFailureMessage(actual interface{}) (message string) { + return m.Matcher.NegatedFailureMessage(actual) +} diff -Nru golang-gomega-1.0/gstruct/pointer_test.go golang-gomega-1.0+git20160910.d59fa0a/gstruct/pointer_test.go --- golang-gomega-1.0/gstruct/pointer_test.go 1970-01-01 00:00:00.000000000 +0000 +++ golang-gomega-1.0+git20160910.d59fa0a/gstruct/pointer_test.go 2016-09-11 05:10:23.000000000 +0000 @@ -0,0 +1,33 @@ +package gstruct_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + . "github.com/onsi/gomega/gstruct" +) + +var _ = Describe("PointTo", func() { + It("should fail when passed nil", func() { + var p *struct{} + Ω(p).Should(BeNil()) + }) + + It("should succeed when passed non-nil pointer", func() { + var s struct{} + Ω(&s).Should(PointTo(Ignore())) + }) + + It("should unwrap the pointee value", func() { + i := 1 + Ω(&i).Should(PointTo(Equal(1))) + Ω(&i).ShouldNot(PointTo(Equal(2))) + }) + + It("should work with nested pointers", func() { + i := 1 + ip := &i + ipp := &ip + Ω(ipp).Should(PointTo(PointTo(Equal(1)))) + Ω(ipp).ShouldNot(PointTo(PointTo(Equal(2)))) + }) +}) diff -Nru golang-gomega-1.0/gstruct/types.go golang-gomega-1.0+git20160910.d59fa0a/gstruct/types.go --- golang-gomega-1.0/gstruct/types.go 1970-01-01 00:00:00.000000000 +0000 +++ golang-gomega-1.0+git20160910.d59fa0a/gstruct/types.go 2016-09-11 05:10:23.000000000 +0000 @@ -0,0 +1,11 @@ +package gstruct + +//Options is the type for options passed to some matchers. +type Options int + +const ( + //IgnoreExtras tells the matcher to ignore extra elements or fields, rather than triggering a failure. + IgnoreExtras Options = 1 << iota + //IgnoreMissing tells the matcher to ignore missing elements or fields, rather than triggering a failure. + IgnoreMissing +) diff -Nru golang-gomega-1.0/internal/assertion/assertion_test.go golang-gomega-1.0+git20160910.d59fa0a/internal/assertion/assertion_test.go --- golang-gomega-1.0/internal/assertion/assertion_test.go 2014-08-02 20:42:16.000000000 +0000 +++ golang-gomega-1.0+git20160910.d59fa0a/internal/assertion/assertion_test.go 2016-09-11 05:10:23.000000000 +0000 @@ -2,6 +2,7 @@ import ( "errors" + . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" . "github.com/onsi/gomega/internal/assertion" @@ -233,4 +234,19 @@ }) }) }) + + Context("Making an assertion without a registered fail handler", func() { + It("should panic", func() { + defer func() { + e := recover() + RegisterFailHandler(Fail) + if e == nil { + Fail("expected a panic to have occurred") + } + }() + + RegisterFailHandler(nil) + Ω(true).Should(BeTrue()) + }) + }) }) diff -Nru golang-gomega-1.0/internal/asyncassertion/async_assertion.go golang-gomega-1.0+git20160910.d59fa0a/internal/asyncassertion/async_assertion.go --- golang-gomega-1.0/internal/asyncassertion/async_assertion.go 2014-08-02 20:42:16.000000000 +0000 +++ golang-gomega-1.0+git20160910.d59fa0a/internal/asyncassertion/async_assertion.go 2016-09-11 05:10:23.000000000 +0000 @@ -6,6 +6,7 @@ "reflect" "time" + "github.com/onsi/gomega/internal/oraclematcher" "github.com/onsi/gomega/types" ) @@ -86,21 +87,12 @@ return assertion.actualInput, nil } -type oracleMatcher interface { - MatchMayChangeInTheFuture(actual interface{}) bool -} - func (assertion *AsyncAssertion) matcherMayChange(matcher types.GomegaMatcher, value interface{}) bool { if assertion.actualInputIsAFunction() { return true } - oracleMatcher, ok := matcher.(oracleMatcher) - if !ok { - return true - } - - return oracleMatcher.MatchMayChangeInTheFuture(value) + return oraclematcher.MatchMayChangeInTheFuture(matcher, value) } func (assertion *AsyncAssertion) match(matcher types.GomegaMatcher, desiredMatch bool, optionalDescription ...interface{}) bool { diff -Nru golang-gomega-1.0/internal/asyncassertion/async_assertion_test.go golang-gomega-1.0+git20160910.d59fa0a/internal/asyncassertion/async_assertion_test.go --- golang-gomega-1.0/internal/asyncassertion/async_assertion_test.go 2014-08-02 20:42:16.000000000 +0000 +++ golang-gomega-1.0+git20160910.d59fa0a/internal/asyncassertion/async_assertion_test.go 2016-09-11 05:10:23.000000000 +0000 @@ -3,6 +3,7 @@ import ( "errors" "time" + . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" . "github.com/onsi/gomega/internal/asyncassertion" @@ -27,29 +28,27 @@ Describe("Eventually", func() { Context("the positive case", func() { It("should poll the function and matcher", func() { - arr := []int{} - a := New(AsyncAssertionTypeEventually, func() []int { - arr = append(arr, 1) - return arr + counter := 0 + a := New(AsyncAssertionTypeEventually, func() int { + counter++ + return counter }, fakeFailHandler, time.Duration(0.2*float64(time.Second)), time.Duration(0.02*float64(time.Second)), 1) - a.Should(HaveLen(10)) - - Ω(arr).Should(HaveLen(10)) + a.Should(BeNumerically("==", 5)) Ω(failureMessage).Should(BeZero()) }) It("should continue when the matcher errors", func() { - var arr = []int{} + counter := 0 a := New(AsyncAssertionTypeEventually, func() interface{} { - arr = append(arr, 1) - if len(arr) == 4 { - return 0 //this should cause the matcher to error + counter++ + if counter == 5 { + return "not-a-number" //this should cause the matcher to error } - return arr + return counter }, fakeFailHandler, time.Duration(0.2*float64(time.Second)), time.Duration(0.02*float64(time.Second)), 1) - a.Should(HaveLen(4), "My description %d", 2) + a.Should(BeNumerically("==", 5), "My description %d", 2) Ω(failureMessage).Should(ContainSubstring("Timed out after")) Ω(failureMessage).Should(ContainSubstring("My description 2")) @@ -57,17 +56,18 @@ }) It("should be able to timeout", func() { - arr := []int{} - a := New(AsyncAssertionTypeEventually, func() []int { - arr = append(arr, 1) - return arr + counter := 0 + a := New(AsyncAssertionTypeEventually, func() int { + counter++ + return counter }, fakeFailHandler, time.Duration(0.2*float64(time.Second)), time.Duration(0.02*float64(time.Second)), 1) - a.Should(HaveLen(11), "My description %d", 2) + a.Should(BeNumerically(">", 100), "My description %d", 2) - Ω(arr).Should(HaveLen(10)) + Ω(counter).Should(BeNumerically(">", 8)) + Ω(counter).Should(BeNumerically("<=", 10)) Ω(failureMessage).Should(ContainSubstring("Timed out after")) - Ω(failureMessage).Should(ContainSubstring("<[]int | len:10"), "Should pass the correct value to the matcher message formatter.") + Ω(failureMessage).Should(MatchRegexp(`\: \d`), "Should pass the correct value to the matcher message formatter.") Ω(failureMessage).Should(ContainSubstring("My description 2")) Ω(callerSkip).Should(Equal(4)) }) @@ -76,18 +76,14 @@ Context("the negative case", func() { It("should poll the function and matcher", func() { counter := 0 - arr := []int{} - a := New(AsyncAssertionTypeEventually, func() []int { + a := New(AsyncAssertionTypeEventually, func() int { counter += 1 - if counter >= 10 { - arr = append(arr, 1) - } - return arr + return counter }, fakeFailHandler, time.Duration(0.2*float64(time.Second)), time.Duration(0.02*float64(time.Second)), 1) - a.ShouldNot(HaveLen(0)) + a.ShouldNot(BeNumerically("<", 3)) - Ω(arr).Should(HaveLen(1)) + Ω(counter).Should(Equal(3)) Ω(failureMessage).Should(BeZero()) }) @@ -105,14 +101,14 @@ }) It("should be able to timeout", func() { - a := New(AsyncAssertionTypeEventually, func() []int { - return []int{} - }, fakeFailHandler, time.Duration(0.2*float64(time.Second)), time.Duration(0.02*float64(time.Second)), 1) + a := New(AsyncAssertionTypeEventually, func() int { + return 0 + }, fakeFailHandler, time.Duration(0.1*float64(time.Second)), time.Duration(0.02*float64(time.Second)), 1) - a.ShouldNot(HaveLen(0), "My description %d", 2) + a.ShouldNot(Equal(0), "My description %d", 2) Ω(failureMessage).Should(ContainSubstring("Timed out after")) - Ω(failureMessage).Should(ContainSubstring("<[]int | len:0"), "Should pass the correct value to the matcher message formatter.") + Ω(failureMessage).Should(ContainSubstring(": 0"), "Should pass the correct value to the matcher message formatter.") Ω(failureMessage).Should(ContainSubstring("My description 2")) Ω(callerSkip).Should(Equal(4)) }) @@ -141,6 +137,23 @@ Ω(callerSkip).Should(Equal(4)) }) }) + + Context("Making an assertion without a registered fail handler", func() { + It("should panic", func() { + defer func() { + e := recover() + RegisterFailHandler(Fail) + if e == nil { + Fail("expected a panic to have occurred") + } + }() + + RegisterFailHandler(nil) + c := make(chan bool, 1) + c <- true + Eventually(c).Should(Receive()) + }) + }) }) Describe("Consistently", func() { @@ -154,7 +167,8 @@ }, fakeFailHandler, time.Duration(0.2*float64(time.Second)), time.Duration(0.02*float64(time.Second)), 1) a.Should(Equal("foo")) - Ω(calls).Should(Equal(10)) + Ω(calls).Should(BeNumerically(">", 8)) + Ω(calls).Should(BeNumerically("<=", 10)) Ω(failureMessage).Should(BeZero()) }) }) @@ -164,7 +178,7 @@ calls := 0 a := New(AsyncAssertionTypeConsistently, func() interface{} { calls++ - if calls > 9 { + if calls > 5 { return "bar" } return "foo" @@ -257,6 +271,22 @@ Ω(callerSkip).Should(Equal(4)) }) }) + + Context("Making an assertion without a registered fail handler", func() { + It("should panic", func() { + defer func() { + e := recover() + RegisterFailHandler(Fail) + if e == nil { + Fail("expected a panic to have occurred") + } + }() + + RegisterFailHandler(nil) + c := make(chan bool) + Consistently(c).ShouldNot(Receive()) + }) + }) }) Context("when passed a function with the wrong # or arguments & returns", func() { diff -Nru golang-gomega-1.0/internal/oraclematcher/oracle_matcher.go golang-gomega-1.0+git20160910.d59fa0a/internal/oraclematcher/oracle_matcher.go --- golang-gomega-1.0/internal/oraclematcher/oracle_matcher.go 1970-01-01 00:00:00.000000000 +0000 +++ golang-gomega-1.0+git20160910.d59fa0a/internal/oraclematcher/oracle_matcher.go 2016-09-11 05:10:23.000000000 +0000 @@ -0,0 +1,25 @@ +package oraclematcher + +import "github.com/onsi/gomega/types" + +/* +GomegaMatchers that also match the OracleMatcher interface can convey information about +whether or not their result will change upon future attempts. + +This allows `Eventually` and `Consistently` to short circuit if success becomes impossible. + +For example, a process' exit code can never change. So, gexec's Exit matcher returns `true` +for `MatchMayChangeInTheFuture` until the process exits, at which point it returns `false` forevermore. +*/ +type OracleMatcher interface { + MatchMayChangeInTheFuture(actual interface{}) bool +} + +func MatchMayChangeInTheFuture(matcher types.GomegaMatcher, value interface{}) bool { + oracleMatcher, ok := matcher.(OracleMatcher) + if !ok { + return true + } + + return oracleMatcher.MatchMayChangeInTheFuture(value) +} diff -Nru golang-gomega-1.0/LICENSE golang-gomega-1.0+git20160910.d59fa0a/LICENSE --- golang-gomega-1.0/LICENSE 1970-01-01 00:00:00.000000000 +0000 +++ golang-gomega-1.0+git20160910.d59fa0a/LICENSE 2016-09-11 05:10:23.000000000 +0000 @@ -0,0 +1,20 @@ +Copyright (c) 2013-2014 Onsi Fakhouri + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff -Nru golang-gomega-1.0/matchers/and.go golang-gomega-1.0+git20160910.d59fa0a/matchers/and.go --- golang-gomega-1.0/matchers/and.go 1970-01-01 00:00:00.000000000 +0000 +++ golang-gomega-1.0+git20160910.d59fa0a/matchers/and.go 2016-09-11 05:10:23.000000000 +0000 @@ -0,0 +1,64 @@ +package matchers + +import ( + "fmt" + + "github.com/onsi/gomega/format" + "github.com/onsi/gomega/internal/oraclematcher" + "github.com/onsi/gomega/types" +) + +type AndMatcher struct { + Matchers []types.GomegaMatcher + + // state + firstFailedMatcher types.GomegaMatcher +} + +func (m *AndMatcher) Match(actual interface{}) (success bool, err error) { + m.firstFailedMatcher = nil + for _, matcher := range m.Matchers { + success, err := matcher.Match(actual) + if !success || err != nil { + m.firstFailedMatcher = matcher + return false, err + } + } + return true, nil +} + +func (m *AndMatcher) FailureMessage(actual interface{}) (message string) { + return m.firstFailedMatcher.FailureMessage(actual) +} + +func (m *AndMatcher) NegatedFailureMessage(actual interface{}) (message string) { + // not the most beautiful list of matchers, but not bad either... + return format.Message(actual, fmt.Sprintf("To not satisfy all of these matchers: %s", m.Matchers)) +} + +func (m *AndMatcher) MatchMayChangeInTheFuture(actual interface{}) bool { + /* + Example with 3 matchers: A, B, C + + Match evaluates them: T, F, => F + So match is currently F, what should MatchMayChangeInTheFuture() return? + Seems like it only depends on B, since currently B MUST change to allow the result to become T + + Match eval: T, T, T => T + So match is currently T, what should MatchMayChangeInTheFuture() return? + Seems to depend on ANY of them being able to change to F. + */ + + if m.firstFailedMatcher == nil { + // so all matchers succeeded.. Any one of them changing would change the result. + for _, matcher := range m.Matchers { + if oraclematcher.MatchMayChangeInTheFuture(matcher, actual) { + return true + } + } + return false // none of were going to change + } else { + // one of the matchers failed.. it must be able to change in order to affect the result + return oraclematcher.MatchMayChangeInTheFuture(m.firstFailedMatcher, actual) + } +} diff -Nru golang-gomega-1.0/matchers/and_test.go golang-gomega-1.0+git20160910.d59fa0a/matchers/and_test.go --- golang-gomega-1.0/matchers/and_test.go 1970-01-01 00:00:00.000000000 +0000 +++ golang-gomega-1.0+git20160910.d59fa0a/matchers/and_test.go 2016-09-11 05:10:23.000000000 +0000 @@ -0,0 +1,103 @@ +package matchers_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + . "github.com/onsi/gomega/matchers" + "github.com/onsi/gomega/types" +) + +// sample data +var ( + // example input + input = "hi" + // some matchers that succeed against the input + true1 = HaveLen(2) + true2 = Equal("hi") + true3 = MatchRegexp("hi") + // some matchers that fail against the input. + false1 = HaveLen(1) + false2 = Equal("hip") + false3 = MatchRegexp("hope") +) + +// verifyFailureMessage expects the matcher to fail with the given input, and verifies the failure message. +func verifyFailureMessage(m types.GomegaMatcher, input string, expectedFailureMsgFragment string) { + Expect(m.Match(input)).To(BeFalse()) + Expect(m.FailureMessage(input)).To(Equal( + "Expected\n : " + input + "\n" + expectedFailureMsgFragment)) +} + +var _ = Describe("AndMatcher", func() { + It("works with positive cases", func() { + Expect(input).To(And()) + Expect(input).To(And(true1)) + Expect(input).To(And(true1, true2)) + Expect(input).To(And(true1, true2, true3)) + + // use alias + Expect(input).To(SatisfyAll(true1, true2, true3)) + }) + + It("works with negative cases", func() { + Expect(input).ToNot(And(false1, false2)) + Expect(input).ToNot(And(true1, true2, false3)) + Expect(input).ToNot(And(true1, false2, false3)) + Expect(input).ToNot(And(false1, true1, true2)) + }) + + Context("failure messages", func() { + Context("when match fails", func() { + It("gives a descriptive message", func() { + verifyFailureMessage(And(false1, true1), input, "to have length 1") + verifyFailureMessage(And(true1, false2), input, "to equal\n : hip") + verifyFailureMessage(And(true1, true2, false3), input, "to match regular expression\n : hope") + }) + }) + + Context("when match succeeds, but expected it to fail", func() { + It("gives a descriptive message", func() { + verifyFailureMessage(Not(And(true1, true2)), input, + `To not satisfy all of these matchers: [%!s(*matchers.HaveLenMatcher=&{2}) %!s(*matchers.EqualMatcher=&{hi})]`) + }) + }) + }) + + Context("MatchMayChangeInTheFuture", func() { + Context("Match returned false", func() { + Context("returns value of the failed matcher", func() { + It("false if failed matcher not going to change", func() { + // 3 matchers: 1st returns true, 2nd returns false and is not going to change, 3rd is never called + m := And(Not(BeNil()), Or(), Equal(1)) + Expect(m.Match("hi")).To(BeFalse()) + Expect(m.(*AndMatcher).MatchMayChangeInTheFuture("hi")).To(BeFalse()) // empty Or() indicates not going to change + }) + It("true if failed matcher indicates it might change", func() { + // 3 matchers: 1st returns true, 2nd returns false and "might" change, 3rd is never called + m := And(Not(BeNil()), Equal(5), Equal(1)) + Expect(m.Match("hi")).To(BeFalse()) + Expect(m.(*AndMatcher).MatchMayChangeInTheFuture("hi")).To(BeTrue()) // Equal(5) indicates it might change + }) + }) + }) + Context("Match returned true", func() { + It("returns true if any of the matchers could change", func() { + // 3 matchers, all return true, and all could change + m := And(Not(BeNil()), Equal("hi"), HaveLen(2)) + Expect(m.Match("hi")).To(BeTrue()) + Expect(m.(*AndMatcher).MatchMayChangeInTheFuture("hi")).To(BeTrue()) // all 3 of these matchers default to 'true' + }) + It("returns false if none of the matchers could change", func() { + // empty And() has the property of always matching, and never can change since there are no sub-matchers that could change + m := And() + Expect(m.Match("anything")).To(BeTrue()) + Expect(m.(*AndMatcher).MatchMayChangeInTheFuture("anything")).To(BeFalse()) + + // And() with 3 sub-matchers that return true, and can't change + m = And(And(), And(), And()) + Expect(m.Match("hi")).To(BeTrue()) + Expect(m.(*AndMatcher).MatchMayChangeInTheFuture("hi")).To(BeFalse()) // the 3 empty And()'s won't change + }) + }) + }) +}) diff -Nru golang-gomega-1.0/matchers/assignable_to_type_of_matcher.go golang-gomega-1.0+git20160910.d59fa0a/matchers/assignable_to_type_of_matcher.go --- golang-gomega-1.0/matchers/assignable_to_type_of_matcher.go 2014-08-02 20:42:16.000000000 +0000 +++ golang-gomega-1.0+git20160910.d59fa0a/matchers/assignable_to_type_of_matcher.go 2016-09-11 05:10:23.000000000 +0000 @@ -2,8 +2,9 @@ import ( "fmt" - "github.com/onsi/gomega/format" "reflect" + + "github.com/onsi/gomega/format" ) type AssignableToTypeOfMatcher struct { @@ -12,7 +13,7 @@ func (matcher *AssignableToTypeOfMatcher) Match(actual interface{}) (success bool, err error) { if actual == nil || matcher.Expected == nil { - return false, fmt.Errorf("Refusing to compare to .") + return false, fmt.Errorf("Refusing to compare to .\nBe explicit and use BeNil() instead. This is to avoid mistakes where both sides of an assertion are erroneously uninitialized.") } actualType := reflect.TypeOf(actual) diff -Nru golang-gomega-1.0/matchers/be_a_directory.go golang-gomega-1.0+git20160910.d59fa0a/matchers/be_a_directory.go --- golang-gomega-1.0/matchers/be_a_directory.go 1970-01-01 00:00:00.000000000 +0000 +++ golang-gomega-1.0+git20160910.d59fa0a/matchers/be_a_directory.go 2016-09-11 05:10:23.000000000 +0000 @@ -0,0 +1,54 @@ +package matchers + +import ( + "fmt" + "os" + + "github.com/onsi/gomega/format" +) + +type notADirectoryError struct { + os.FileInfo +} + +func (t notADirectoryError) Error() string { + fileInfo := os.FileInfo(t) + switch { + case fileInfo.Mode().IsRegular(): + return "file is a regular file" + default: + return fmt.Sprintf("file mode is: %s", fileInfo.Mode().String()) + } +} + +type BeADirectoryMatcher struct { + expected interface{} + err error +} + +func (matcher *BeADirectoryMatcher) Match(actual interface{}) (success bool, err error) { + actualFilename, ok := actual.(string) + if !ok { + return false, fmt.Errorf("BeADirectoryMatcher matcher expects a file path") + } + + fileInfo, err := os.Stat(actualFilename) + if err != nil { + matcher.err = err + return false, nil + } + + if !fileInfo.Mode().IsDir() { + matcher.err = notADirectoryError{fileInfo} + return false, nil + } + return true, nil +} + +func (matcher *BeADirectoryMatcher) FailureMessage(actual interface{}) (message string) { + return format.Message(actual, fmt.Sprintf("to be a directory: %s", matcher.err)) +} + +func (matcher *BeADirectoryMatcher) NegatedFailureMessage(actual interface{}) (message string) { + return format.Message(actual, fmt.Sprintf("not be a directory")) +} diff -Nru golang-gomega-1.0/matchers/be_a_directory_test.go golang-gomega-1.0+git20160910.d59fa0a/matchers/be_a_directory_test.go --- golang-gomega-1.0/matchers/be_a_directory_test.go 1970-01-01 00:00:00.000000000 +0000 +++ golang-gomega-1.0+git20160910.d59fa0a/matchers/be_a_directory_test.go 2016-09-11 05:10:23.000000000 +0000 @@ -0,0 +1,40 @@ +package matchers_test + +import ( + "io/ioutil" + "os" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + . "github.com/onsi/gomega/matchers" +) + +var _ = Describe("BeADirectoryMatcher", func() { + Context("when passed a string", func() { + It("should do the right thing", func() { + Ω("/dne/test").ShouldNot(BeADirectory()) + + tmpFile, err := ioutil.TempFile("", "gomega-test-tempfile") + Ω(err).ShouldNot(HaveOccurred()) + defer os.Remove(tmpFile.Name()) + Ω(tmpFile.Name()).ShouldNot(BeADirectory()) + + tmpDir, err := ioutil.TempDir("", "gomega-test-tempdir") + Ω(err).ShouldNot(HaveOccurred()) + defer os.Remove(tmpDir) + Ω(tmpDir).Should(BeADirectory()) + }) + }) + + Context("when passed something else", func() { + It("should error", func() { + success, err := (&BeADirectoryMatcher{}).Match(nil) + Ω(success).Should(BeFalse()) + Ω(err).Should(HaveOccurred()) + + success, err = (&BeADirectoryMatcher{}).Match(true) + Ω(success).Should(BeFalse()) + Ω(err).Should(HaveOccurred()) + }) + }) +}) diff -Nru golang-gomega-1.0/matchers/be_an_existing_file.go golang-gomega-1.0+git20160910.d59fa0a/matchers/be_an_existing_file.go --- golang-gomega-1.0/matchers/be_an_existing_file.go 1970-01-01 00:00:00.000000000 +0000 +++ golang-gomega-1.0+git20160910.d59fa0a/matchers/be_an_existing_file.go 2016-09-11 05:10:23.000000000 +0000 @@ -0,0 +1,38 @@ +package matchers + +import ( + "fmt" + "os" + + "github.com/onsi/gomega/format" +) + +type BeAnExistingFileMatcher struct { + expected interface{} +} + +func (matcher *BeAnExistingFileMatcher) Match(actual interface{}) (success bool, err error) { + actualFilename, ok := actual.(string) + if !ok { + return false, fmt.Errorf("BeAnExistingFileMatcher matcher expects a file path") + } + + if _, err = os.Stat(actualFilename); err != nil { + switch { + case os.IsNotExist(err): + return false, nil + default: + return false, err + } + } + + return true, nil +} + +func (matcher *BeAnExistingFileMatcher) FailureMessage(actual interface{}) (message string) { + return format.Message(actual, fmt.Sprintf("to exist")) +} + +func (matcher *BeAnExistingFileMatcher) NegatedFailureMessage(actual interface{}) (message string) { + return format.Message(actual, fmt.Sprintf("not to exist")) +} diff -Nru golang-gomega-1.0/matchers/be_an_existing_file_test.go golang-gomega-1.0+git20160910.d59fa0a/matchers/be_an_existing_file_test.go --- golang-gomega-1.0/matchers/be_an_existing_file_test.go 1970-01-01 00:00:00.000000000 +0000 +++ golang-gomega-1.0+git20160910.d59fa0a/matchers/be_an_existing_file_test.go 2016-09-11 05:10:23.000000000 +0000 @@ -0,0 +1,40 @@ +package matchers_test + +import ( + "io/ioutil" + "os" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + . "github.com/onsi/gomega/matchers" +) + +var _ = Describe("BeAnExistingFileMatcher", func() { + Context("when passed a string", func() { + It("should do the right thing", func() { + Ω("/dne/test").ShouldNot(BeAnExistingFile()) + + tmpFile, err := ioutil.TempFile("", "gomega-test-tempfile") + Ω(err).ShouldNot(HaveOccurred()) + defer os.Remove(tmpFile.Name()) + Ω(tmpFile.Name()).Should(BeAnExistingFile()) + + tmpDir, err := ioutil.TempDir("", "gomega-test-tempdir") + Ω(err).ShouldNot(HaveOccurred()) + defer os.Remove(tmpDir) + Ω(tmpDir).Should(BeAnExistingFile()) + }) + }) + + Context("when passed something else", func() { + It("should error", func() { + success, err := (&BeAnExistingFileMatcher{}).Match(nil) + Ω(success).Should(BeFalse()) + Ω(err).Should(HaveOccurred()) + + success, err = (&BeAnExistingFileMatcher{}).Match(true) + Ω(success).Should(BeFalse()) + Ω(err).Should(HaveOccurred()) + }) + }) +}) diff -Nru golang-gomega-1.0/matchers/be_a_regular_file.go golang-gomega-1.0+git20160910.d59fa0a/matchers/be_a_regular_file.go --- golang-gomega-1.0/matchers/be_a_regular_file.go 1970-01-01 00:00:00.000000000 +0000 +++ golang-gomega-1.0+git20160910.d59fa0a/matchers/be_a_regular_file.go 2016-09-11 05:10:23.000000000 +0000 @@ -0,0 +1,54 @@ +package matchers + +import ( + "fmt" + "os" + + "github.com/onsi/gomega/format" +) + +type notARegularFileError struct { + os.FileInfo +} + +func (t notARegularFileError) Error() string { + fileInfo := os.FileInfo(t) + switch { + case fileInfo.IsDir(): + return "file is a directory" + default: + return fmt.Sprintf("file mode is: %s", fileInfo.Mode().String()) + } +} + +type BeARegularFileMatcher struct { + expected interface{} + err error +} + +func (matcher *BeARegularFileMatcher) Match(actual interface{}) (success bool, err error) { + actualFilename, ok := actual.(string) + if !ok { + return false, fmt.Errorf("BeARegularFileMatcher matcher expects a file path") + } + + fileInfo, err := os.Stat(actualFilename) + if err != nil { + matcher.err = err + return false, nil + } + + if !fileInfo.Mode().IsRegular() { + matcher.err = notARegularFileError{fileInfo} + return false, nil + } + return true, nil +} + +func (matcher *BeARegularFileMatcher) FailureMessage(actual interface{}) (message string) { + return format.Message(actual, fmt.Sprintf("to be a regular file: %s", matcher.err)) +} + +func (matcher *BeARegularFileMatcher) NegatedFailureMessage(actual interface{}) (message string) { + return format.Message(actual, fmt.Sprintf("not be a regular file")) +} diff -Nru golang-gomega-1.0/matchers/be_a_regular_file_test.go golang-gomega-1.0+git20160910.d59fa0a/matchers/be_a_regular_file_test.go --- golang-gomega-1.0/matchers/be_a_regular_file_test.go 1970-01-01 00:00:00.000000000 +0000 +++ golang-gomega-1.0+git20160910.d59fa0a/matchers/be_a_regular_file_test.go 2016-09-11 05:10:23.000000000 +0000 @@ -0,0 +1,40 @@ +package matchers_test + +import ( + "io/ioutil" + "os" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + . "github.com/onsi/gomega/matchers" +) + +var _ = Describe("BeARegularFileMatcher", func() { + Context("when passed a string", func() { + It("should do the right thing", func() { + Ω("/dne/test").ShouldNot(BeARegularFile()) + + tmpFile, err := ioutil.TempFile("", "gomega-test-tempfile") + Ω(err).ShouldNot(HaveOccurred()) + defer os.Remove(tmpFile.Name()) + Ω(tmpFile.Name()).Should(BeARegularFile()) + + tmpDir, err := ioutil.TempDir("", "gomega-test-tempdir") + Ω(err).ShouldNot(HaveOccurred()) + defer os.Remove(tmpDir) + Ω(tmpDir).ShouldNot(BeARegularFile()) + }) + }) + + Context("when passed something else", func() { + It("should error", func() { + success, err := (&BeARegularFileMatcher{}).Match(nil) + Ω(success).Should(BeFalse()) + Ω(err).Should(HaveOccurred()) + + success, err = (&BeARegularFileMatcher{}).Match(true) + Ω(success).Should(BeFalse()) + Ω(err).Should(HaveOccurred()) + }) + }) +}) diff -Nru golang-gomega-1.0/matchers/be_identical_to.go golang-gomega-1.0+git20160910.d59fa0a/matchers/be_identical_to.go --- golang-gomega-1.0/matchers/be_identical_to.go 1970-01-01 00:00:00.000000000 +0000 +++ golang-gomega-1.0+git20160910.d59fa0a/matchers/be_identical_to.go 2016-09-11 05:10:23.000000000 +0000 @@ -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 to .\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 -Nru golang-gomega-1.0/matchers/be_identical_to_test.go golang-gomega-1.0+git20160910.d59fa0a/matchers/be_identical_to_test.go --- golang-gomega-1.0/matchers/be_identical_to_test.go 1970-01-01 00:00:00.000000000 +0000 +++ golang-gomega-1.0+git20160910.d59fa0a/matchers/be_identical_to_test.go 2016-09-11 05:10:23.000000000 +0000 @@ -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 -Nru golang-gomega-1.0/matchers/be_sent_matcher_test.go golang-gomega-1.0+git20160910.d59fa0a/matchers/be_sent_matcher_test.go --- golang-gomega-1.0/matchers/be_sent_matcher_test.go 2014-08-02 20:42:16.000000000 +0000 +++ golang-gomega-1.0+git20160910.d59fa0a/matchers/be_sent_matcher_test.go 2016-09-11 05:10:23.000000000 +0000 @@ -1,8 +1,8 @@ package matchers_test import ( - "time" . "github.com/onsi/gomega/matchers" + "time" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" diff -Nru golang-gomega-1.0/matchers/consist_of_test.go golang-gomega-1.0+git20160910.d59fa0a/matchers/consist_of_test.go --- golang-gomega-1.0/matchers/consist_of_test.go 2014-08-02 20:42:16.000000000 +0000 +++ golang-gomega-1.0+git20160910.d59fa0a/matchers/consist_of_test.go 2016-09-11 05:10:23.000000000 +0000 @@ -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 -Nru golang-gomega-1.0/matchers/contain_element_matcher.go golang-gomega-1.0+git20160910.d59fa0a/matchers/contain_element_matcher.go --- golang-gomega-1.0/matchers/contain_element_matcher.go 2014-08-02 20:42:16.000000000 +0000 +++ golang-gomega-1.0+git20160910.d59fa0a/matchers/contain_element_matcher.go 2016-09-11 05:10:23.000000000 +0000 @@ -2,8 +2,9 @@ import ( "fmt" - "github.com/onsi/gomega/format" "reflect" + + "github.com/onsi/gomega/format" ) type ContainElementMatcher struct { @@ -25,6 +26,7 @@ if isMap(actual) { keys = value.MapKeys() } + var lastError error for i := 0; i < value.Len(); i++ { var success bool var err error @@ -34,14 +36,15 @@ success, err = elemMatcher.Match(value.Index(i).Interface()) } if err != nil { - return false, fmt.Errorf("ContainElement's element matcher failed with:\n\t%s", err.Error()) + lastError = err + continue } if success { return true, nil } } - return false, nil + return false, lastError } func (matcher *ContainElementMatcher) FailureMessage(actual interface{}) (message string) { diff -Nru golang-gomega-1.0/matchers/contain_element_matcher_test.go golang-gomega-1.0+git20160910.d59fa0a/matchers/contain_element_matcher_test.go --- golang-gomega-1.0/matchers/contain_element_matcher_test.go 2014-08-02 20:42:16.000000000 +0000 +++ golang-gomega-1.0+git20160910.d59fa0a/matchers/contain_element_matcher_test.go 2016-09-11 05:10:23.000000000 +0000 @@ -35,8 +35,12 @@ Ω(map[string]int{"foo": 1, "bar": 2}).ShouldNot(ContainElement(BeNumerically(">", 2))) }) - It("should fail if the matcher ever fails", func() { - actual := []interface{}{1, 2, "3", 4} + It("should power through even if the matcher ever fails", func() { + Ω([]interface{}{1, 2, "3", 4}).Should(ContainElement(BeNumerically(">=", 3))) + }) + + It("should fail if the matcher fails", func() { + actual := []interface{}{1, 2, "3", "4"} success, err := (&ContainElementMatcher{Element: BeNumerically(">=", 3)}).Match(actual) Ω(success).Should(BeFalse()) Ω(err).Should(HaveOccurred()) diff -Nru golang-gomega-1.0/matchers/equal_matcher.go golang-gomega-1.0+git20160910.d59fa0a/matchers/equal_matcher.go --- golang-gomega-1.0/matchers/equal_matcher.go 2014-08-02 20:42:16.000000000 +0000 +++ golang-gomega-1.0+git20160910.d59fa0a/matchers/equal_matcher.go 2016-09-11 05:10:23.000000000 +0000 @@ -2,8 +2,9 @@ import ( "fmt" - "github.com/onsi/gomega/format" "reflect" + + "github.com/onsi/gomega/format" ) type EqualMatcher struct { @@ -12,7 +13,7 @@ func (matcher *EqualMatcher) Match(actual interface{}) (success bool, err error) { if actual == nil && matcher.Expected == nil { - return false, fmt.Errorf("Refusing to compare to .") + return false, fmt.Errorf("Refusing to compare to .\nBe explicit and use BeNil() instead. This is to avoid mistakes where both sides of an assertion are erroneously uninitialized.") } return reflect.DeepEqual(actual, matcher.Expected), nil } diff -Nru golang-gomega-1.0/matchers/have_cap_matcher.go golang-gomega-1.0+git20160910.d59fa0a/matchers/have_cap_matcher.go --- golang-gomega-1.0/matchers/have_cap_matcher.go 1970-01-01 00:00:00.000000000 +0000 +++ golang-gomega-1.0+git20160910.d59fa0a/matchers/have_cap_matcher.go 2016-09-11 05:10:23.000000000 +0000 @@ -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 -Nru golang-gomega-1.0/matchers/have_cap_matcher_test.go golang-gomega-1.0+git20160910.d59fa0a/matchers/have_cap_matcher_test.go --- golang-gomega-1.0/matchers/have_cap_matcher_test.go 1970-01-01 00:00:00.000000000 +0000 +++ golang-gomega-1.0+git20160910.d59fa0a/matchers/have_cap_matcher_test.go 2016-09-11 05:10:23.000000000 +0000 @@ -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 -Nru golang-gomega-1.0/matchers/have_occurred_matcher.go golang-gomega-1.0+git20160910.d59fa0a/matchers/have_occurred_matcher.go --- golang-gomega-1.0/matchers/have_occurred_matcher.go 2014-08-02 20:42:16.000000000 +0000 +++ golang-gomega-1.0+git20160910.d59fa0a/matchers/have_occurred_matcher.go 2016-09-11 05:10:23.000000000 +0000 @@ -2,6 +2,7 @@ import ( "fmt" + "github.com/onsi/gomega/format" ) @@ -9,19 +10,22 @@ } func (matcher *HaveOccurredMatcher) Match(actual interface{}) (success bool, err error) { + // 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) { - return fmt.Sprintf("Expected an error to have occured. Got:\n%s", format.Object(actual, 1)) + return fmt.Sprintf("Expected an error to have occurred. Got:\n%s", format.Object(actual, 1)) } func (matcher *HaveOccurredMatcher) NegatedFailureMessage(actual interface{}) (message string) { diff -Nru golang-gomega-1.0/matchers/have_occurred_matcher_test.go golang-gomega-1.0+git20160910.d59fa0a/matchers/have_occurred_matcher_test.go --- golang-gomega-1.0/matchers/have_occurred_matcher_test.go 2014-08-02 20:42:16.000000000 +0000 +++ golang-gomega-1.0+git20160910.d59fa0a/matchers/have_occurred_matcher_test.go 2016-09-11 05:10:23.000000000 +0000 @@ -7,6 +7,14 @@ . "github.com/onsi/gomega/matchers" ) +type CustomErr struct { + msg string +} + +func (e *CustomErr) Error() string { + return e.msg +} + var _ = Describe("HaveOccurred", func() { It("should succeed if matching an error", func() { Ω(errors.New("Foo")).Should(HaveOccurred()) @@ -25,4 +33,26 @@ Ω(success).Should(BeFalse()) Ω(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 : {}")) + }) + + 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()) + }) + + It("should not succeed with nil pointers to types that conform to error interface", func() { + var err *CustomErr = nil + Ω(err).ShouldNot(HaveOccurred()) + }) }) diff -Nru golang-gomega-1.0/matchers/have_prefix_matcher.go golang-gomega-1.0+git20160910.d59fa0a/matchers/have_prefix_matcher.go --- golang-gomega-1.0/matchers/have_prefix_matcher.go 1970-01-01 00:00:00.000000000 +0000 +++ golang-gomega-1.0+git20160910.d59fa0a/matchers/have_prefix_matcher.go 2016-09-11 05:10:23.000000000 +0000 @@ -0,0 +1,35 @@ +package matchers + +import ( + "fmt" + "github.com/onsi/gomega/format" +) + +type HavePrefixMatcher struct { + Prefix string + Args []interface{} +} + +func (matcher *HavePrefixMatcher) Match(actual interface{}) (success bool, err error) { + actualString, ok := toString(actual) + if !ok { + return false, fmt.Errorf("HavePrefix matcher requires a string or stringer. Got:\n%s", format.Object(actual, 1)) + } + prefix := matcher.prefix() + return len(actualString) >= len(prefix) && actualString[0:len(prefix)] == prefix, nil +} + +func (matcher *HavePrefixMatcher) prefix() string { + if len(matcher.Args) > 0 { + return fmt.Sprintf(matcher.Prefix, matcher.Args...) + } + return matcher.Prefix +} + +func (matcher *HavePrefixMatcher) FailureMessage(actual interface{}) (message string) { + return format.Message(actual, "to have prefix", matcher.prefix()) +} + +func (matcher *HavePrefixMatcher) NegatedFailureMessage(actual interface{}) (message string) { + return format.Message(actual, "not to have prefix", matcher.prefix()) +} diff -Nru golang-gomega-1.0/matchers/have_prefix_matcher_test.go golang-gomega-1.0+git20160910.d59fa0a/matchers/have_prefix_matcher_test.go --- golang-gomega-1.0/matchers/have_prefix_matcher_test.go 1970-01-01 00:00:00.000000000 +0000 +++ golang-gomega-1.0+git20160910.d59fa0a/matchers/have_prefix_matcher_test.go 2016-09-11 05:10:23.000000000 +0000 @@ -0,0 +1,36 @@ +package matchers_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + . "github.com/onsi/gomega/matchers" +) + +var _ = Describe("HavePrefixMatcher", func() { + Context("when actual is a string", func() { + It("should match a string prefix", func() { + Ω("Ab").Should(HavePrefix("A")) + Ω("A").ShouldNot(HavePrefix("Ab")) + }) + }) + + Context("when the matcher is called with multiple arguments", func() { + It("should pass the string and arguments to sprintf", func() { + Ω("C3PO").Should(HavePrefix("C%dP", 3)) + }) + }) + + Context("when actual is a stringer", func() { + It("should call the stringer and match against the returned string", func() { + Ω(&myStringer{a: "Ab"}).Should(HavePrefix("A")) + }) + }) + + Context("when actual is neither a string nor a stringer", func() { + It("should error", func() { + success, err := (&HavePrefixMatcher{Prefix: "2"}).Match(2) + Ω(success).Should(BeFalse()) + Ω(err).Should(HaveOccurred()) + }) + }) +}) diff -Nru golang-gomega-1.0/matchers/have_suffix_matcher.go golang-gomega-1.0+git20160910.d59fa0a/matchers/have_suffix_matcher.go --- golang-gomega-1.0/matchers/have_suffix_matcher.go 1970-01-01 00:00:00.000000000 +0000 +++ golang-gomega-1.0+git20160910.d59fa0a/matchers/have_suffix_matcher.go 2016-09-11 05:10:23.000000000 +0000 @@ -0,0 +1,35 @@ +package matchers + +import ( + "fmt" + "github.com/onsi/gomega/format" +) + +type HaveSuffixMatcher struct { + Suffix string + Args []interface{} +} + +func (matcher *HaveSuffixMatcher) Match(actual interface{}) (success bool, err error) { + actualString, ok := toString(actual) + if !ok { + 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 +} + +func (matcher *HaveSuffixMatcher) suffix() string { + if len(matcher.Args) > 0 { + return fmt.Sprintf(matcher.Suffix, matcher.Args...) + } + return matcher.Suffix +} + +func (matcher *HaveSuffixMatcher) FailureMessage(actual interface{}) (message string) { + return format.Message(actual, "to have suffix", matcher.suffix()) +} + +func (matcher *HaveSuffixMatcher) NegatedFailureMessage(actual interface{}) (message string) { + return format.Message(actual, "not to have suffix", matcher.suffix()) +} diff -Nru golang-gomega-1.0/matchers/have_suffix_matcher_test.go golang-gomega-1.0+git20160910.d59fa0a/matchers/have_suffix_matcher_test.go --- golang-gomega-1.0/matchers/have_suffix_matcher_test.go 1970-01-01 00:00:00.000000000 +0000 +++ golang-gomega-1.0+git20160910.d59fa0a/matchers/have_suffix_matcher_test.go 2016-09-11 05:10:23.000000000 +0000 @@ -0,0 +1,36 @@ +package matchers_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + . "github.com/onsi/gomega/matchers" +) + +var _ = Describe("HaveSuffixMatcher", func() { + Context("when actual is a string", func() { + It("should match a string suffix", func() { + Ω("Ab").Should(HaveSuffix("b")) + Ω("A").ShouldNot(HaveSuffix("Ab")) + }) + }) + + Context("when the matcher is called with multiple arguments", func() { + It("should pass the string and arguments to sprintf", func() { + Ω("C3PO").Should(HaveSuffix("%dPO", 3)) + }) + }) + + Context("when actual is a stringer", func() { + It("should call the stringer and match against the returned string", func() { + Ω(&myStringer{a: "Ab"}).Should(HaveSuffix("b")) + }) + }) + + Context("when actual is neither a string nor a stringer", func() { + It("should error", func() { + success, err := (&HaveSuffixMatcher{Suffix: "2"}).Match(2) + Ω(success).Should(BeFalse()) + Ω(err).Should(HaveOccurred()) + }) + }) +}) diff -Nru golang-gomega-1.0/matchers/match_error_matcher.go golang-gomega-1.0+git20160910.d59fa0a/matchers/match_error_matcher.go --- golang-gomega-1.0/matchers/match_error_matcher.go 2014-08-02 20:42:16.000000000 +0000 +++ golang-gomega-1.0+git20160910.d59fa0a/matchers/match_error_matcher.go 2016-09-11 05:10:23.000000000 +0000 @@ -29,7 +29,16 @@ return reflect.DeepEqual(actualErr, matcher.Expected), nil } - return false, fmt.Errorf("MatchError must be passed an error or string. Got:\n%s", format.Object(matcher.Expected, 1)) + var subMatcher omegaMatcher + var hasSubMatcher bool + if matcher.Expected != nil { + subMatcher, hasSubMatcher = (matcher.Expected).(omegaMatcher) + if hasSubMatcher { + return subMatcher.Match(actualErr.Error()) + } + } + + return false, fmt.Errorf("MatchError must be passed an error, string, or Matcher that can match on strings. Got:\n%s", format.Object(matcher.Expected, 1)) } func (matcher *MatchErrorMatcher) FailureMessage(actual interface{}) (message string) { diff -Nru golang-gomega-1.0/matchers/match_error_matcher_test.go golang-gomega-1.0+git20160910.d59fa0a/matchers/match_error_matcher_test.go --- golang-gomega-1.0/matchers/match_error_matcher_test.go 2014-08-02 20:42:16.000000000 +0000 +++ golang-gomega-1.0+git20160910.d59fa0a/matchers/match_error_matcher_test.go 2016-09-11 05:10:23.000000000 +0000 @@ -41,6 +41,19 @@ Ω(customErr).Should(MatchError("an error")) }) + Context("when passed a matcher", func() { + It("should pass if the matcher passes against the error string", func() { + err := errors.New("error 123 abc") + + Ω(err).Should(MatchError(MatchRegexp(`\d{3}`))) + }) + + It("should fail if the matcher fails against the error string", func() { + err := errors.New("no digits") + Ω(err).ShouldNot(MatchError(MatchRegexp(`\d`))) + }) + }) + It("should fail when passed anything else", func() { actualErr := errors.New("an error") _, err := (&MatchErrorMatcher{ diff -Nru golang-gomega-1.0/matchers/matcher_tests_suite_test.go golang-gomega-1.0+git20160910.d59fa0a/matchers/matcher_tests_suite_test.go --- golang-gomega-1.0/matchers/matcher_tests_suite_test.go 2014-08-02 20:42:16.000000000 +0000 +++ golang-gomega-1.0+git20160910.d59fa0a/matchers/matcher_tests_suite_test.go 2016-09-11 05:10:23.000000000 +0000 @@ -2,6 +2,7 @@ import ( "testing" + . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" ) @@ -25,5 +26,5 @@ func Test(t *testing.T) { RegisterFailHandler(Fail) - RunSpecs(t, "Gomega") + RunSpecs(t, "Gomega Matchers") } diff -Nru golang-gomega-1.0/matchers/match_json_matcher.go golang-gomega-1.0+git20160910.d59fa0a/matchers/match_json_matcher.go --- golang-gomega-1.0/matchers/match_json_matcher.go 2014-08-02 20:42:16.000000000 +0000 +++ golang-gomega-1.0+git20160910.d59fa0a/matchers/match_json_matcher.go 2016-09-11 05:10:23.000000000 +0000 @@ -4,8 +4,9 @@ "bytes" "encoding/json" "fmt" - "github.com/onsi/gomega/format" "reflect" + + "github.com/onsi/gomega/format" ) type MatchJSONMatcher struct { @@ -39,23 +40,25 @@ } 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 actualString, expectedString, nil + return abuf.String(), ebuf.String(), nil } diff -Nru golang-gomega-1.0/matchers/match_json_matcher_test.go golang-gomega-1.0+git20160910.d59fa0a/matchers/match_json_matcher_test.go --- golang-gomega-1.0/matchers/match_json_matcher_test.go 2014-08-02 20:42:16.000000000 +0000 +++ golang-gomega-1.0+git20160910.d59fa0a/matchers/match_json_matcher_test.go 2016-09-11 05:10:23.000000000 +0000 @@ -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()) + Ω(err.Error()).Should(ContainSubstring("Actual 'oops' should be valid JSON")) + }) + }) - success, err = (&MatchJSONMatcher{JSONToMatch: `{}`}).Match(`oops`) + 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")) }) }) - Context("when either side is neither a string nor a stringer", func() { + Context("when the expected is neither a string nor a stringer nor a byte array", func() { It("should error", func() { - success, err := (&MatchJSONMatcher{JSONToMatch: "{}"}).Match(2) + 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 : 2")) - success, err = (&MatchJSONMatcher{JSONToMatch: 2}).Match("{}") + 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")) + }) + }) - success, err = (&MatchJSONMatcher{JSONToMatch: nil}).Match("{}") + 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 : 2")) - success, err = (&MatchJSONMatcher{JSONToMatch: 2}).Match(nil) + 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")) }) }) }) diff -Nru golang-gomega-1.0/matchers/match_yaml_matcher.go golang-gomega-1.0+git20160910.d59fa0a/matchers/match_yaml_matcher.go --- golang-gomega-1.0/matchers/match_yaml_matcher.go 1970-01-01 00:00:00.000000000 +0000 +++ golang-gomega-1.0+git20160910.d59fa0a/matchers/match_yaml_matcher.go 2016-09-11 05:10:23.000000000 +0000 @@ -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 -Nru golang-gomega-1.0/matchers/match_yaml_matcher_test.go golang-gomega-1.0+git20160910.d59fa0a/matchers/match_yaml_matcher_test.go --- golang-gomega-1.0/matchers/match_yaml_matcher_test.go 1970-01-01 00:00:00.000000000 +0000 +++ golang-gomega-1.0+git20160910.d59fa0a/matchers/match_yaml_matcher_test.go 2016-09-11 05:10:23.000000000 +0000 @@ -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+: b: 2\s+to match YAML of\s+: 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+: b: two\s+to match YAML of\s+: 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+: a: 1\s+not to match YAML of\s+: 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+: a: one\s+not to match YAML of\s+: 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 : 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")) + }) + }) + + 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 : 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")) + }) + }) +}) diff -Nru golang-gomega-1.0/matchers/not.go golang-gomega-1.0+git20160910.d59fa0a/matchers/not.go --- golang-gomega-1.0/matchers/not.go 1970-01-01 00:00:00.000000000 +0000 +++ golang-gomega-1.0+git20160910.d59fa0a/matchers/not.go 2016-09-11 05:10:23.000000000 +0000 @@ -0,0 +1,30 @@ +package matchers + +import ( + "github.com/onsi/gomega/internal/oraclematcher" + "github.com/onsi/gomega/types" +) + +type NotMatcher struct { + Matcher types.GomegaMatcher +} + +func (m *NotMatcher) Match(actual interface{}) (bool, error) { + success, err := m.Matcher.Match(actual) + if err != nil { + return false, err + } + return !success, nil +} + +func (m *NotMatcher) FailureMessage(actual interface{}) (message string) { + return m.Matcher.NegatedFailureMessage(actual) // works beautifully +} + +func (m *NotMatcher) NegatedFailureMessage(actual interface{}) (message string) { + return m.Matcher.FailureMessage(actual) // works beautifully +} + +func (m *NotMatcher) MatchMayChangeInTheFuture(actual interface{}) bool { + return oraclematcher.MatchMayChangeInTheFuture(m.Matcher, actual) // just return m.Matcher's value +} diff -Nru golang-gomega-1.0/matchers/not_test.go golang-gomega-1.0+git20160910.d59fa0a/matchers/not_test.go --- golang-gomega-1.0/matchers/not_test.go 1970-01-01 00:00:00.000000000 +0000 +++ golang-gomega-1.0+git20160910.d59fa0a/matchers/not_test.go 2016-09-11 05:10:23.000000000 +0000 @@ -0,0 +1,57 @@ +package matchers_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + . "github.com/onsi/gomega/matchers" +) + +var _ = Describe("NotMatcher", func() { + Context("basic examples", func() { + It("works", func() { + Expect(input).To(Not(false1)) + Expect(input).To(Not(Not(true2))) + Expect(input).ToNot(Not(true3)) + Expect(input).ToNot(Not(Not(false1))) + Expect(input).To(Not(Not(Not(false2)))) + }) + }) + + Context("De Morgan's laws", func() { + It("~(A && B) == ~A || ~B", func() { + Expect(input).To(Not(And(false1, false2))) + Expect(input).To(Or(Not(false1), Not(false2))) + }) + It("~(A || B) == ~A && ~B", func() { + Expect(input).To(Not(Or(false1, false2))) + Expect(input).To(And(Not(false1), Not(false2))) + }) + }) + + Context("failure messages are opposite of original matchers' failure messages", func() { + Context("when match fails", func() { + It("gives a descriptive message", func() { + verifyFailureMessage(Not(HaveLen(2)), input, "not to have length 2") + }) + }) + + Context("when match succeeds, but expected it to fail", func() { + It("gives a descriptive message", func() { + verifyFailureMessage(Not(Not(HaveLen(3))), input, "to have length 3") + }) + }) + }) + + Context("MatchMayChangeInTheFuture()", func() { + It("Propagates value from wrapped matcher", func() { + m := Not(Or()) // an empty Or() always returns false, and indicates it cannot change + Expect(m.Match("anything")).To(BeTrue()) + Expect(m.(*NotMatcher).MatchMayChangeInTheFuture("anything")).To(BeFalse()) + }) + It("Defaults to true", func() { + m := Not(Equal(1)) // Equal does not have this method + Expect(m.Match(2)).To(BeTrue()) + Expect(m.(*NotMatcher).MatchMayChangeInTheFuture(2)).To(BeTrue()) // defaults to true + }) + }) +}) diff -Nru golang-gomega-1.0/matchers/or.go golang-gomega-1.0+git20160910.d59fa0a/matchers/or.go --- golang-gomega-1.0/matchers/or.go 1970-01-01 00:00:00.000000000 +0000 +++ golang-gomega-1.0+git20160910.d59fa0a/matchers/or.go 2016-09-11 05:10:23.000000000 +0000 @@ -0,0 +1,67 @@ +package matchers + +import ( + "fmt" + + "github.com/onsi/gomega/format" + "github.com/onsi/gomega/internal/oraclematcher" + "github.com/onsi/gomega/types" +) + +type OrMatcher struct { + Matchers []types.GomegaMatcher + + // state + firstSuccessfulMatcher types.GomegaMatcher +} + +func (m *OrMatcher) Match(actual interface{}) (success bool, err error) { + m.firstSuccessfulMatcher = nil + for _, matcher := range m.Matchers { + success, err := matcher.Match(actual) + if err != nil { + return false, err + } + if success { + m.firstSuccessfulMatcher = matcher + return true, nil + } + } + return false, nil +} + +func (m *OrMatcher) FailureMessage(actual interface{}) (message string) { + // not the most beautiful list of matchers, but not bad either... + return format.Message(actual, fmt.Sprintf("To satisfy at least one of these matchers: %s", m.Matchers)) +} + +func (m *OrMatcher) NegatedFailureMessage(actual interface{}) (message string) { + return m.firstSuccessfulMatcher.NegatedFailureMessage(actual) +} + +func (m *OrMatcher) MatchMayChangeInTheFuture(actual interface{}) bool { + /* + Example with 3 matchers: A, B, C + + Match evaluates them: F, T, => T + So match is currently T, what should MatchMayChangeInTheFuture() return? + Seems like it only depends on B, since currently B MUST change to allow the result to become F + + Match eval: F, F, F => F + So match is currently F, what should MatchMayChangeInTheFuture() return? + Seems to depend on ANY of them being able to change to T. + */ + + if m.firstSuccessfulMatcher != nil { + // one of the matchers succeeded.. it must be able to change in order to affect the result + return oraclematcher.MatchMayChangeInTheFuture(m.firstSuccessfulMatcher, actual) + } else { + // so all matchers failed.. Any one of them changing would change the result. + for _, matcher := range m.Matchers { + if oraclematcher.MatchMayChangeInTheFuture(matcher, actual) { + return true + } + } + return false // none of were going to change + } +} diff -Nru golang-gomega-1.0/matchers/or_test.go golang-gomega-1.0+git20160910.d59fa0a/matchers/or_test.go --- golang-gomega-1.0/matchers/or_test.go 1970-01-01 00:00:00.000000000 +0000 +++ golang-gomega-1.0+git20160910.d59fa0a/matchers/or_test.go 2016-09-11 05:10:23.000000000 +0000 @@ -0,0 +1,85 @@ +package matchers_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + . "github.com/onsi/gomega/matchers" +) + +var _ = Describe("OrMatcher", func() { + It("works with positive cases", func() { + Expect(input).To(Or(true1)) + Expect(input).To(Or(true1, true2)) + Expect(input).To(Or(true1, false1)) + Expect(input).To(Or(false1, true2)) + Expect(input).To(Or(true1, true2, true3)) + Expect(input).To(Or(true1, true2, false3)) + Expect(input).To(Or(true1, false2, true3)) + Expect(input).To(Or(false1, true2, true3)) + Expect(input).To(Or(true1, false2, false3)) + Expect(input).To(Or(false1, false2, true3)) + + // use alias + Expect(input).To(SatisfyAny(false1, false2, true3)) + }) + + It("works with negative cases", func() { + Expect(input).ToNot(Or()) + Expect(input).ToNot(Or(false1)) + Expect(input).ToNot(Or(false1, false2)) + Expect(input).ToNot(Or(false1, false2, false3)) + }) + + Context("failure messages", func() { + Context("when match fails", func() { + It("gives a descriptive message", func() { + verifyFailureMessage(Or(false1, false2), input, + "To satisfy at least one of these matchers: [%!s(*matchers.HaveLenMatcher=&{1}) %!s(*matchers.EqualMatcher=&{hip})]") + }) + }) + + Context("when match succeeds, but expected it to fail", func() { + It("gives a descriptive message", func() { + verifyFailureMessage(Not(Or(true1, true2)), input, `not to have length 2`) + }) + }) + }) + + Context("MatchMayChangeInTheFuture", func() { + Context("Match returned false", func() { + It("returns true if any of the matchers could change", func() { + // 3 matchers, all return false, and all could change + m := Or(BeNil(), Equal("hip"), HaveLen(1)) + Expect(m.Match("hi")).To(BeFalse()) + Expect(m.(*OrMatcher).MatchMayChangeInTheFuture("hi")).To(BeTrue()) // all 3 of these matchers default to 'true' + }) + It("returns false if none of the matchers could change", func() { + // empty Or() has the property of never matching, and never can change since there are no sub-matchers that could change + m := Or() + Expect(m.Match("anything")).To(BeFalse()) + Expect(m.(*OrMatcher).MatchMayChangeInTheFuture("anything")).To(BeFalse()) + + // Or() with 3 sub-matchers that return false, and can't change + m = Or(Or(), Or(), Or()) + Expect(m.Match("hi")).To(BeFalse()) + Expect(m.(*OrMatcher).MatchMayChangeInTheFuture("hi")).To(BeFalse()) // the 3 empty Or()'s won't change + }) + }) + Context("Match returned true", func() { + Context("returns value of the successful matcher", func() { + It("false if successful matcher not going to change", func() { + // 3 matchers: 1st returns false, 2nd returns true and is not going to change, 3rd is never called + m := Or(BeNil(), And(), Equal(1)) + Expect(m.Match("hi")).To(BeTrue()) + Expect(m.(*OrMatcher).MatchMayChangeInTheFuture("hi")).To(BeFalse()) + }) + It("true if successful matcher indicates it might change", func() { + // 3 matchers: 1st returns false, 2nd returns true and "might" change, 3rd is never called + m := Or(Not(BeNil()), Equal("hi"), Equal(1)) + Expect(m.Match("hi")).To(BeTrue()) + Expect(m.(*OrMatcher).MatchMayChangeInTheFuture("hi")).To(BeTrue()) // Equal("hi") indicates it might change + }) + }) + }) + }) +}) diff -Nru golang-gomega-1.0/matchers/receive_matcher.go golang-gomega-1.0+git20160910.d59fa0a/matchers/receive_matcher.go --- golang-gomega-1.0/matchers/receive_matcher.go 2014-08-02 20:42:16.000000000 +0000 +++ golang-gomega-1.0+git20160910.d59fa0a/matchers/receive_matcher.go 2016-09-11 05:10:23.000000000 +0000 @@ -57,7 +57,7 @@ matcher.channelClosed = closed if closed { - return false, fmt.Errorf("ReceiveMatcher was given a closed channel:\n%s", format.Object(actual, 1)) + return false, nil } if hasSubMatcher { @@ -84,26 +84,36 @@ func (matcher *ReceiveMatcher) FailureMessage(actual interface{}) (message string) { subMatcher, hasSubMatcher := (matcher.Arg).(omegaMatcher) + closedAddendum := "" + if matcher.channelClosed { + closedAddendum = " The channel is closed." + } + if hasSubMatcher { if matcher.receivedValue.IsValid() { return subMatcher.FailureMessage(matcher.receivedValue.Interface()) } return "When passed a matcher, ReceiveMatcher's channel *must* receive something." } else { - return format.Message(actual, "to receive something") + return format.Message(actual, "to receive something."+closedAddendum) } } func (matcher *ReceiveMatcher) NegatedFailureMessage(actual interface{}) (message string) { subMatcher, hasSubMatcher := (matcher.Arg).(omegaMatcher) + closedAddendum := "" + if matcher.channelClosed { + closedAddendum = " The channel is closed." + } + if hasSubMatcher { if matcher.receivedValue.IsValid() { return subMatcher.NegatedFailureMessage(matcher.receivedValue.Interface()) } return "When passed a matcher, ReceiveMatcher's channel *must* receive something." } else { - return format.Message(actual, "not to receive anything") + return format.Message(actual, "not to receive anything."+closedAddendum) } } diff -Nru golang-gomega-1.0/matchers/receive_matcher_test.go golang-gomega-1.0+git20160910.d59fa0a/matchers/receive_matcher_test.go --- golang-gomega-1.0/matchers/receive_matcher_test.go 2014-08-02 20:42:16.000000000 +0000 +++ golang-gomega-1.0+git20160910.d59fa0a/matchers/receive_matcher_test.go 2016-09-11 05:10:23.000000000 +0000 @@ -2,6 +2,7 @@ import ( "time" + . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" . "github.com/onsi/gomega/matchers" @@ -192,21 +193,16 @@ close(channel) Ω(channel).Should(Receive()) - - success, err := (&ReceiveMatcher{}).Match(channel) - Ω(success).Should(BeFalse()) - Ω(err).Should(HaveOccurred()) + Ω(channel).ShouldNot(Receive()) }) }) Context("for an unbuffered channel", func() { - It("should error", func() { + It("should always fail", func() { channel := make(chan bool) close(channel) - success, err := (&ReceiveMatcher{}).Match(channel) - Ω(success).Should(BeFalse()) - Ω(err).Should(HaveOccurred()) + Ω(channel).ShouldNot(Receive()) }) }) }) diff -Nru golang-gomega-1.0/matchers/succeed_matcher.go golang-gomega-1.0+git20160910.d59fa0a/matchers/succeed_matcher.go --- golang-gomega-1.0/matchers/succeed_matcher.go 1970-01-01 00:00:00.000000000 +0000 +++ golang-gomega-1.0+git20160910.d59fa0a/matchers/succeed_matcher.go 2016-09-11 05:10:23.000000000 +0000 @@ -0,0 +1,33 @@ +package matchers + +import ( + "fmt" + + "github.com/onsi/gomega/format" +) + +type SucceedMatcher struct { +} + +func (matcher *SucceedMatcher) Match(actual interface{}) (success bool, err error) { + // is purely nil? + if actual == nil { + 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)) + } + + // must be nil (or a pointer to a nil) + return isNil(actual), nil +} + +func (matcher *SucceedMatcher) FailureMessage(actual interface{}) (message string) { + return fmt.Sprintf("Expected success, but got an error:\n%s\n%s", format.Object(actual, 1), format.IndentString(actual.(error).Error(), 1)) +} + +func (matcher *SucceedMatcher) NegatedFailureMessage(actual interface{}) (message string) { + return "Expected failure, but got no error." +} diff -Nru golang-gomega-1.0/matchers/succeed_matcher_test.go golang-gomega-1.0+git20160910.d59fa0a/matchers/succeed_matcher_test.go --- golang-gomega-1.0/matchers/succeed_matcher_test.go 1970-01-01 00:00:00.000000000 +0000 +++ golang-gomega-1.0+git20160910.d59fa0a/matchers/succeed_matcher_test.go 2016-09-11 05:10:23.000000000 +0000 @@ -0,0 +1,62 @@ +package matchers_test + +import ( + "errors" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + . "github.com/onsi/gomega/matchers" +) + +func Erroring() error { + return errors.New("bam") +} + +func NotErroring() error { + return nil +} + +type AnyType struct{} + +func Invalid() *AnyType { + return nil +} + +var _ = Describe("Succeed", func() { + It("should succeed if the function succeeds", func() { + Ω(NotErroring()).Should(Succeed()) + }) + + It("should succeed (in the negated) if the function errored", func() { + Ω(Erroring()).ShouldNot(Succeed()) + }) + + It("should not if passed a non-error", func() { + success, err := (&SucceedMatcher{}).Match(Invalid()) + Ω(success).Should(BeFalse()) + Ω(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 : {}")) + }) + + 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 -Nru golang-gomega-1.0/matchers/support/goraph/util/util.go golang-gomega-1.0+git20160910.d59fa0a/matchers/support/goraph/util/util.go --- golang-gomega-1.0/matchers/support/goraph/util/util.go 2014-08-02 20:42:16.000000000 +0000 +++ golang-gomega-1.0+git20160910.d59fa0a/matchers/support/goraph/util/util.go 2016-09-11 05:10:23.000000000 +0000 @@ -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 -Nru golang-gomega-1.0/matchers/type_support.go golang-gomega-1.0+git20160910.d59fa0a/matchers/type_support.go --- golang-gomega-1.0/matchers/type_support.go 2014-08-02 20:42:16.000000000 +0000 +++ golang-gomega-1.0+git20160910.d59fa0a/matchers/type_support.go 2016-09-11 05:10:23.000000000 +0000 @@ -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 { diff -Nru golang-gomega-1.0/matchers/with_transform.go golang-gomega-1.0+git20160910.d59fa0a/matchers/with_transform.go --- golang-gomega-1.0/matchers/with_transform.go 1970-01-01 00:00:00.000000000 +0000 +++ golang-gomega-1.0+git20160910.d59fa0a/matchers/with_transform.go 2016-09-11 05:10:23.000000000 +0000 @@ -0,0 +1,72 @@ +package matchers + +import ( + "fmt" + "reflect" + + "github.com/onsi/gomega/internal/oraclematcher" + "github.com/onsi/gomega/types" +) + +type WithTransformMatcher struct { + // input + Transform interface{} // must be a function of one parameter that returns one value + Matcher types.GomegaMatcher + + // cached value + transformArgType reflect.Type + + // state + transformedValue interface{} +} + +func NewWithTransformMatcher(transform interface{}, matcher types.GomegaMatcher) *WithTransformMatcher { + if transform == nil { + panic("transform function cannot be nil") + } + txType := reflect.TypeOf(transform) + if txType.NumIn() != 1 { + panic("transform function must have 1 argument") + } + if txType.NumOut() != 1 { + panic("transform function must have 1 return value") + } + + return &WithTransformMatcher{ + Transform: transform, + Matcher: matcher, + transformArgType: reflect.TypeOf(transform).In(0), + } +} + +func (m *WithTransformMatcher) Match(actual interface{}) (bool, error) { + // return error if actual's type is incompatible with Transform function's argument type + actualType := reflect.TypeOf(actual) + if !actualType.AssignableTo(m.transformArgType) { + return false, fmt.Errorf("Transform function expects '%s' but we have '%s'", m.transformArgType, actualType) + } + + // call the Transform function with `actual` + fn := reflect.ValueOf(m.Transform) + result := fn.Call([]reflect.Value{reflect.ValueOf(actual)}) + m.transformedValue = result[0].Interface() // expect exactly one value + + return m.Matcher.Match(m.transformedValue) +} + +func (m *WithTransformMatcher) FailureMessage(_ interface{}) (message string) { + return m.Matcher.FailureMessage(m.transformedValue) +} + +func (m *WithTransformMatcher) NegatedFailureMessage(_ interface{}) (message string) { + return m.Matcher.NegatedFailureMessage(m.transformedValue) +} + +func (m *WithTransformMatcher) MatchMayChangeInTheFuture(_ interface{}) bool { + // TODO: Maybe this should always just return true? (Only an issue for non-deterministic transformers.) + // + // Querying the next matcher is fine if the transformer always will return the same value. + // But if the transformer is non-deterministic and returns a different value each time, then there + // is no point in querying the next matcher, since it can only comment on the last transformed value. + return oraclematcher.MatchMayChangeInTheFuture(m.Matcher, m.transformedValue) +} diff -Nru golang-gomega-1.0/matchers/with_transform_test.go golang-gomega-1.0+git20160910.d59fa0a/matchers/with_transform_test.go --- golang-gomega-1.0/matchers/with_transform_test.go 1970-01-01 00:00:00.000000000 +0000 +++ golang-gomega-1.0+git20160910.d59fa0a/matchers/with_transform_test.go 2016-09-11 05:10:23.000000000 +0000 @@ -0,0 +1,102 @@ +package matchers_test + +import ( + "errors" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + . "github.com/onsi/gomega/matchers" +) + +var _ = Describe("WithTransformMatcher", func() { + + var plus1 = func(i int) int { return i + 1 } + + Context("Panic if transform function invalid", func() { + panicsWithTransformer := func(transform interface{}) { + ExpectWithOffset(1, func() { WithTransform(transform, nil) }).To(Panic()) + } + It("nil", func() { + panicsWithTransformer(nil) + }) + Context("Invalid number of args, but correct return value count", func() { + It("zero", func() { + panicsWithTransformer(func() int { return 5 }) + }) + It("two", func() { + panicsWithTransformer(func(i, j int) int { return 5 }) + }) + }) + Context("Invalid number of return values, but correct number of arguments", func() { + It("zero", func() { + panicsWithTransformer(func(i int) {}) + }) + It("two", func() { + panicsWithTransformer(func(i int) (int, int) { return 5, 6 }) + }) + }) + }) + + It("works with positive cases", func() { + Expect(1).To(WithTransform(plus1, Equal(2))) + Expect(1).To(WithTransform(plus1, WithTransform(plus1, Equal(3)))) + Expect(1).To(WithTransform(plus1, And(Equal(2), BeNumerically(">", 1)))) + + // transform expects custom type + type S struct { + A int + B string + } + transformer := func(s S) string { return s.B } + Expect(S{1, "hi"}).To(WithTransform(transformer, Equal("hi"))) + + // transform expects interface + errString := func(e error) string { return e.Error() } + Expect(errors.New("abc")).To(WithTransform(errString, Equal("abc"))) + }) + + It("works with negative cases", func() { + Expect(1).ToNot(WithTransform(plus1, Equal(3))) + Expect(1).ToNot(WithTransform(plus1, WithTransform(plus1, Equal(2)))) + }) + + Context("failure messages", func() { + Context("when match fails", func() { + It("gives a descriptive message", func() { + m := WithTransform(plus1, Equal(3)) + Expect(m.Match(1)).To(BeFalse()) + Expect(m.FailureMessage(1)).To(Equal("Expected\n : 2\nto equal\n : 3")) + }) + }) + + Context("when match succeeds, but expected it to fail", func() { + It("gives a descriptive message", func() { + m := Not(WithTransform(plus1, Equal(3))) + Expect(m.Match(2)).To(BeFalse()) + Expect(m.FailureMessage(2)).To(Equal("Expected\n : 3\nnot to equal\n : 3")) + }) + }) + + Context("actual value is incompatible with transform function's argument type", func() { + It("gracefully fails if transform cannot be performed", func() { + m := WithTransform(plus1, Equal(3)) + result, err := m.Match("hi") // give it a string but transform expects int; doesn't panic + Expect(result).To(BeFalse()) + Expect(err).To(MatchError("Transform function expects 'int' but we have 'string'")) + }) + }) + }) + + Context("MatchMayChangeInTheFuture()", func() { + It("Propagates value from wrapped matcher on the transformed value", func() { + m := WithTransform(plus1, Or()) // empty Or() always returns false, and indicates it cannot change + Expect(m.Match(1)).To(BeFalse()) + Expect(m.(*WithTransformMatcher).MatchMayChangeInTheFuture(1)).To(BeFalse()) // empty Or() indicates cannot change + }) + It("Defaults to true", func() { + m := WithTransform(plus1, Equal(2)) // Equal does not have this method + Expect(m.Match(1)).To(BeTrue()) + Expect(m.(*WithTransformMatcher).MatchMayChangeInTheFuture(1)).To(BeTrue()) // defaults to true + }) + }) +}) diff -Nru golang-gomega-1.0/matchers.go golang-gomega-1.0+git20160910.d59fa0a/matchers.go --- golang-gomega-1.0/matchers.go 2014-08-02 20:42:16.000000000 +0000 +++ golang-gomega-1.0+git20160910.d59fa0a/matchers.go 2016-09-11 05:10:23.000000000 +0000 @@ -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{} @@ -49,6 +58,21 @@ return &matchers.HaveOccurredMatcher{} } +//Succeed passes if actual is a nil error +//Succeed is intended to be used with functions that return a single error value. Instead of +// err := SomethingThatMightFail() +// Ω(err).ShouldNot(HaveOccurred()) +// +//You can write: +// Ω(SomethingThatMightFail()).Should(Succeed()) +// +//It is a mistake to use Succeed with a function that has multiple return values. Gomega's Ω and Expect +//functions automatically trigger failure if any return values after the first return value are non-zero/non-nil. +//This means that Ω(MultiReturnFunc()).ShouldNot(Succeed()) can never pass. +func Succeed() types.GomegaMatcher { + return &matchers.SucceedMatcher{} +} + //MatchError succeeds if actual is a non-nil error that matches the passed in string/error. // //These are valid use-cases: @@ -84,7 +108,7 @@ // //- If there is nothing on the channel `c` then Ω(c).Should(Receive()) will fail and Ω(c).ShouldNot(Receive()) will pass. // -//- If the channel `c` is closed then *both* Ω(c).Should(Receive()) and Ω(c).ShouldNot(Receive()) will error. +//- If the channel `c` is closed then Ω(c).Should(Receive()) will fail and Ω(c).ShouldNot(Receive()) will pass. // //- If there is something on the channel `c` ready to be read, then Ω(c).Should(Receive()) will pass and Ω(c).ShouldNot(Receive()) will fail. // @@ -161,6 +185,26 @@ } } +//HavePrefix succeeds if actual is a string or stringer that contains the +//passed-in string as a prefix. Optional arguments can be provided to construct +//via fmt.Sprintf(). +func HavePrefix(prefix string, args ...interface{}) types.GomegaMatcher { + return &matchers.HavePrefixMatcher{ + Prefix: prefix, + Args: args, + } +} + +//HaveSuffix succeeds if actual is a string or stringer that contains the +//passed-in string as a suffix. Optional arguments can be provided to construct +//via fmt.Sprintf(). +func HaveSuffix(suffix string, args ...interface{}) types.GomegaMatcher { + return &matchers.HaveSuffixMatcher{ + Suffix: suffix, + Args: args, + } +} + //MatchJSON succeeds if actual is a string or stringer of JSON that matches //the expected JSON. The JSONs are decoded and the resulting objects are compared via //reflect.DeepEqual so things like key-ordering and whitespace shouldn't matter. @@ -170,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{} @@ -182,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{} @@ -215,7 +275,6 @@ // Ω([]string{"Foo", "FooBar"}).Should(ConsistOf([]string{"FooBar", "Foo"})) // //Note that Go's type system does not allow you to write this as ConsistOf([]string{"FooBar", "Foo"}...) as []string and []interface{} are different types - hence the need for this special rule. - func ConsistOf(elements ...interface{}) types.GomegaMatcher { return &matchers.ConsistOfMatcher{ Elements: elements, @@ -291,3 +350,69 @@ func Panic() types.GomegaMatcher { return &matchers.PanicMatcher{} } + +//BeAnExistingFile succeeds if a file exists. +//Actual must be a string representing the abs path to the file being checked. +func BeAnExistingFile() types.GomegaMatcher { + return &matchers.BeAnExistingFileMatcher{} +} + +//BeARegularFile succeeds iff a file exists and is a regular file. +//Actual must be a string representing the abs path to the file being checked. +func BeARegularFile() types.GomegaMatcher { + return &matchers.BeARegularFileMatcher{} +} + +//BeADirectory succeeds iff a file exists and is a directory. +//Actual must be a string representing the abs path to the file being checked. +func BeADirectory() types.GomegaMatcher { + return &matchers.BeADirectoryMatcher{} +} + +//And succeeds only if all of the given matchers succeed. +//The matchers are tried in order, and will fail-fast if one doesn't succeed. +// Expect("hi").To(And(HaveLen(2), Equal("hi")) +// +//And(), Or(), Not() and WithTransform() allow matchers to be composed into complex expressions. +func And(ms ...types.GomegaMatcher) types.GomegaMatcher { + return &matchers.AndMatcher{Matchers: ms} +} + +//SatisfyAll is an alias for And(). +// Ω("hi").Should(SatisfyAll(HaveLen(2), Equal("hi"))) +func SatisfyAll(matchers ...types.GomegaMatcher) types.GomegaMatcher { + return And(matchers...) +} + +//Or succeeds if any of the given matchers succeed. +//The matchers are tried in order and will return immediately upon the first successful match. +// Expect("hi").To(Or(HaveLen(3), HaveLen(2)) +// +//And(), Or(), Not() and WithTransform() allow matchers to be composed into complex expressions. +func Or(ms ...types.GomegaMatcher) types.GomegaMatcher { + return &matchers.OrMatcher{Matchers: ms} +} + +//SatisfyAny is an alias for Or(). +// Expect("hi").SatisfyAny(Or(HaveLen(3), HaveLen(2)) +func SatisfyAny(matchers ...types.GomegaMatcher) types.GomegaMatcher { + return Or(matchers...) +} + +//Not negates the given matcher; it succeeds if the given matcher fails. +// Expect(1).To(Not(Equal(2)) +// +//And(), Or(), Not() and WithTransform() allow matchers to be composed into complex expressions. +func Not(matcher types.GomegaMatcher) types.GomegaMatcher { + return &matchers.NotMatcher{Matcher: matcher} +} + +//WithTransform applies the `transform` to the actual value and matches it against `matcher`. +//The given transform must be a function of one parameter that returns one value. +// var plus1 = func(i int) int { return i + 1 } +// Expect(1).To(WithTransform(plus1, Equal(2)) +// +//And(), Or(), Not() and WithTransform() allow matchers to be composed into complex expressions. +func WithTransform(transform interface{}, matcher types.GomegaMatcher) types.GomegaMatcher { + return matchers.NewWithTransformMatcher(transform, matcher) +} diff -Nru golang-gomega-1.0/MIT.LICENSE golang-gomega-1.0+git20160910.d59fa0a/MIT.LICENSE --- golang-gomega-1.0/MIT.LICENSE 2014-08-02 20:42:16.000000000 +0000 +++ golang-gomega-1.0+git20160910.d59fa0a/MIT.LICENSE 1970-01-01 00:00:00.000000000 +0000 @@ -1,20 +0,0 @@ -Copyright (c) 2013 Onsi Fakhouri - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff -Nru golang-gomega-1.0/README.md golang-gomega-1.0+git20160910.d59fa0a/README.md --- golang-gomega-1.0/README.md 2014-08-02 20:42:16.000000000 +0000 +++ golang-gomega-1.0+git20160910.d59fa0a/README.md 2016-09-11 05:10:23.000000000 +0000 @@ -2,7 +2,7 @@ [![Build Status](https://travis-ci.org/onsi/gomega.png)](https://travis-ci.org/onsi/gomega) -Jump straight to the [docs](http://onsi.github.io/gomega/) to learn about Gomega, including a list of [all available matchers](http://onsi.github.io/gomega/#provided_matchers). +Jump straight to the [docs](http://onsi.github.io/gomega/) to learn about Gomega, including a list of [all available matchers](http://onsi.github.io/gomega/#provided-matchers). To discuss Gomega and get updates, join the [google group](https://groups.google.com/d/forum/ginkgo-and-gomega). @@ -10,6 +10,10 @@ Learn more about Ginkgo [here](http://onsi.github.io/ginkgo/) +## Community Matchers + +A collection of community matchers is available on the [wiki](https://github.com/onsi/gomega/wiki). + ## License Gomega is MIT-Licensed diff -Nru golang-gomega-1.0/.travis.yml golang-gomega-1.0+git20160910.d59fa0a/.travis.yml --- golang-gomega-1.0/.travis.yml 2014-08-02 20:42:16.000000000 +0000 +++ golang-gomega-1.0+git20160910.d59fa0a/.travis.yml 2016-09-11 05:10:23.000000000 +0000 @@ -1,7 +1,9 @@ language: go go: - - 1.3 - + - 1.5 + - 1.6.2 + - stable + install: - go get -v ./... - go get github.com/onsi/ginkgo