Skip to content

Commit

Permalink
Adds support for MatchXML
Browse files Browse the repository at this point in the history
Actual and expected can not be pretty printed in the test output because
there are issues in the encoding/xml package related to deplicated
namespaces. See golang/go#7535
  • Loading branch information
aubm committed Apr 29, 2017
1 parent 9b8c753 commit cd318f7
Show file tree
Hide file tree
Showing 15 changed files with 477 additions and 0 deletions.
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ language: go
go:
- 1.6
- 1.7
- 1.8

install:
- go get -v ./...
Expand Down
9 changes: 9 additions & 0 deletions matchers.go
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,15 @@ func MatchJSON(json interface{}) types.GomegaMatcher {
}
}

//MatchXML succeeds if actual is a string or stringer of XML that matches
//the expected XML. The XMLs are decoded and the resulting objects are compared via
//reflect.DeepEqual so things like key-ordering and whitespace shouldn't matter.
func MatchXML(xml interface{}) types.GomegaMatcher {
return &matchers.MatchXMLMatcher{
XMLToMatch: xml,
}
}

//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.
Expand Down
75 changes: 75 additions & 0 deletions matchers/match_xml_matcher.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package matchers

import (
"encoding/xml"
"fmt"
"reflect"

"github.com/onsi/gomega/format"
)

type MatchXMLMatcher struct {
XMLToMatch interface{}
}

func (matcher *MatchXMLMatcher) Match(actual interface{}) (success bool, err error) {
actualString, expectedString, err := matcher.formattedPrint(actual)
if err != nil {
return false, err
}

aval := &xmlNode{}
eval := &xmlNode{}

if err := xml.Unmarshal([]byte(actualString), aval); err != nil {
return false, fmt.Errorf("Actual '%s' should be valid XML, but it is not.\nUnderlying error:%s", actualString, err)
}
if err := xml.Unmarshal([]byte(expectedString), eval); err != nil {
return false, fmt.Errorf("Expected '%s' should be valid XML, but it is not.\nUnderlying error:%s", expectedString, err)
}

aval.Clean()
eval.Clean()

return reflect.DeepEqual(aval, eval), nil
}

func (matcher *MatchXMLMatcher) FailureMessage(actual interface{}) (message string) {
actualString, expectedString, _ := matcher.formattedPrint(actual)
return fmt.Sprintf("Expected\n%s\nto match XML of\n%s", actualString, expectedString)
}

func (matcher *MatchXMLMatcher) NegatedFailureMessage(actual interface{}) (message string) {
actualString, expectedString, _ := matcher.formattedPrint(actual)
return fmt.Sprintf("Expected\n%s\nnot to match XML of\n%s", actualString, expectedString)
}

func (matcher *MatchXMLMatcher) formattedPrint(actual interface{}) (actualString, expectedString string, err error) {
var ok bool
actualString, ok = toString(actual)
if !ok {
return "", "", fmt.Errorf("MatchXMLMatcher matcher requires a string, stringer, or []byte. Got actual:\n%s", format.Object(actual, 1))
}
expectedString, ok = toString(matcher.XMLToMatch)
if !ok {
return "", "", fmt.Errorf("MatchXMLMatcher matcher requires a string, stringer, or []byte. Got expected:\n%s", format.Object(matcher.XMLToMatch, 1))
}
return actualString, expectedString, nil
}

type xmlNode struct {
XMLName xml.Name
XMLAttr []xml.Attr `xml:",any,attr"`
Content string `xml:",innerxml"`
Nodes []*xmlNode `xml:",any"`
}

func (n *xmlNode) Clean() {
if len(n.Nodes) == 0 {
return
}
n.Content = ""
for _, child := range n.Nodes {
child.Clean()
}
}
92 changes: 92 additions & 0 deletions matchers/match_xml_matcher_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package matchers_test

import (
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
. "github.com/onsi/gomega/matchers"
)

var _ = Describe("MatchXMLMatcher", func() {

var (
sample_01 = readFileContents("test_data/xml/sample_01.xml")
sample_02 = readFileContents("test_data/xml/sample_02.xml")
sample_03 = readFileContents("test_data/xml/sample_03.xml")
sample_04 = readFileContents("test_data/xml/sample_04.xml")
sample_05 = readFileContents("test_data/xml/sample_05.xml")
sample_06 = readFileContents("test_data/xml/sample_06.xml")
sample_07 = readFileContents("test_data/xml/sample_07.xml")
sample_08 = readFileContents("test_data/xml/sample_08.xml")
sample_09 = readFileContents("test_data/xml/sample_09.xml")
sample_10 = readFileContents("test_data/xml/sample_10.xml")
)

Context("When passed stringifiables", func() {
It("should succeed if the XML matches", func() {
Ω(sample_01).Should(MatchXML(sample_01))
Ω(sample_01).Should(MatchXML(sample_02))
Ω(sample_01).Should(MatchXML(sample_03))
Ω(sample_09).Should(MatchXML(sample_09))

Ω(sample_01).ShouldNot(MatchXML(sample_04))
Ω(sample_01).ShouldNot(MatchXML(sample_05))
Ω(sample_06).ShouldNot(MatchXML(sample_07))
Ω(sample_07).ShouldNot(MatchXML(sample_08))
Ω(sample_09).ShouldNot(MatchXML(sample_10))

Ω(sample_08).Should(MatchXML(sample_08))
})

It("should work with byte arrays", func() {
Ω([]byte(sample_01)).Should(MatchXML([]byte(sample_01)))
Ω([]byte(sample_01)).Should(MatchXML(sample_01))
Ω(sample_01).Should(MatchXML([]byte(sample_01)))
})
})

Context("when the expected is not valid XML", func() {
It("should error and explain why", func() {
success, err := (&MatchXMLMatcher{XMLToMatch: sample_01}).Match(`oops`)
Ω(success).Should(BeFalse())
Ω(err).Should(HaveOccurred())
Ω(err.Error()).Should(ContainSubstring("Actual 'oops' should be valid XML"))
})
})

Context("when the actual is not valid XML", func() {
It("should error and explain why", func() {
success, err := (&MatchXMLMatcher{XMLToMatch: `oops`}).Match(sample_01)
Ω(success).Should(BeFalse())
Ω(err).Should(HaveOccurred())
Ω(err.Error()).Should(ContainSubstring("Expected 'oops' should be valid XML"))
})
})

Context("when the expected is neither a string nor a stringer nor a byte array", func() {
It("should error", func() {
success, err := (&MatchXMLMatcher{XMLToMatch: 2}).Match(sample_01)
Ω(success).Should(BeFalse())
Ω(err).Should(HaveOccurred())
Ω(err.Error()).Should(ContainSubstring("MatchXMLMatcher matcher requires a string, stringer, or []byte. Got expected:\n <int>: 2"))

success, err = (&MatchXMLMatcher{XMLToMatch: nil}).Match(sample_01)
Ω(success).Should(BeFalse())
Ω(err).Should(HaveOccurred())
Ω(err.Error()).Should(ContainSubstring("MatchXMLMatcher matcher requires a string, stringer, or []byte. Got expected:\n <nil>: nil"))
})
})

Context("when the actual is neither a string nor a stringer nor a byte array", func() {
It("should error", func() {
success, err := (&MatchXMLMatcher{XMLToMatch: sample_01}).Match(2)
Ω(success).Should(BeFalse())
Ω(err).Should(HaveOccurred())
Ω(err.Error()).Should(ContainSubstring("MatchXMLMatcher matcher requires a string, stringer, or []byte. Got actual:\n <int>: 2"))

success, err = (&MatchXMLMatcher{XMLToMatch: sample_01}).Match(nil)
Ω(success).Should(BeFalse())
Ω(err).Should(HaveOccurred())
Ω(err.Error()).Should(ContainSubstring("MatchXMLMatcher matcher requires a string, stringer, or []byte. Got actual:\n <nil>: nil"))
})
})
})
20 changes: 20 additions & 0 deletions matchers/matcher_tests_suite_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package matchers_test

import (
"fmt"
"io/ioutil"
"os"
"testing"

. "github.com/onsi/ginkgo"
Expand Down Expand Up @@ -28,3 +31,20 @@ func Test(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Gomega Matchers")
}

func readFileContents(filePath string) []byte {
f := openFile(filePath)
b, err := ioutil.ReadAll(f)
if err != nil {
panic(fmt.Errorf("failed to read file contents: %v", err))
}
return b
}

func openFile(filePath string) *os.File {
f, err := os.Open(filePath)
if err != nil {
panic(fmt.Errorf("failed to open file: %v", err))
}
return f
}
6 changes: 6 additions & 0 deletions matchers/test_data/xml/sample_01.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<note>
<to>Tove</to>
<from>Jani</from>
<heading>Reminder</heading>
<body>Don't forget me this weekend!</body>
</note>
9 changes: 9 additions & 0 deletions matchers/test_data/xml/sample_02.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@


<note>
<to>Tove</to>
<from>Jani</from>
<heading>Reminder</heading>
<body>Don't forget me this weekend!</body>
</note>

1 change: 1 addition & 0 deletions matchers/test_data/xml/sample_03.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<note> <to>Tove</to> <from>Jani</from> <heading>Reminder</heading> <body>Don't forget me this weekend!</body> </note>
6 changes: 6 additions & 0 deletions matchers/test_data/xml/sample_04.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<note>
<to>Tove</to>
<from>John</from>
<heading>Doe</heading>
<body>Don't forget me this weekend!</body>
</note>
Loading

0 comments on commit cd318f7

Please sign in to comment.