Skip to content

Commit cd318f7

Browse files
committed
Adds support for MatchXML
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
1 parent 9b8c753 commit cd318f7

15 files changed

+477
-0
lines changed

.travis.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ language: go
22
go:
33
- 1.6
44
- 1.7
5+
- 1.8
56

67
install:
78
- go get -v ./...

matchers.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,15 @@ func MatchJSON(json interface{}) types.GomegaMatcher {
214214
}
215215
}
216216

217+
//MatchXML succeeds if actual is a string or stringer of XML that matches
218+
//the expected XML. The XMLs are decoded and the resulting objects are compared via
219+
//reflect.DeepEqual so things like key-ordering and whitespace shouldn't matter.
220+
func MatchXML(xml interface{}) types.GomegaMatcher {
221+
return &matchers.MatchXMLMatcher{
222+
XMLToMatch: xml,
223+
}
224+
}
225+
217226
//MatchYAML succeeds if actual is a string or stringer of YAML that matches
218227
//the expected YAML. The YAML's are decoded and the resulting objects are compared via
219228
//reflect.DeepEqual so things like key-ordering and whitespace shouldn't matter.

matchers/match_xml_matcher.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package matchers
2+
3+
import (
4+
"encoding/xml"
5+
"fmt"
6+
"reflect"
7+
8+
"github.com/onsi/gomega/format"
9+
)
10+
11+
type MatchXMLMatcher struct {
12+
XMLToMatch interface{}
13+
}
14+
15+
func (matcher *MatchXMLMatcher) Match(actual interface{}) (success bool, err error) {
16+
actualString, expectedString, err := matcher.formattedPrint(actual)
17+
if err != nil {
18+
return false, err
19+
}
20+
21+
aval := &xmlNode{}
22+
eval := &xmlNode{}
23+
24+
if err := xml.Unmarshal([]byte(actualString), aval); err != nil {
25+
return false, fmt.Errorf("Actual '%s' should be valid XML, but it is not.\nUnderlying error:%s", actualString, err)
26+
}
27+
if err := xml.Unmarshal([]byte(expectedString), eval); err != nil {
28+
return false, fmt.Errorf("Expected '%s' should be valid XML, but it is not.\nUnderlying error:%s", expectedString, err)
29+
}
30+
31+
aval.Clean()
32+
eval.Clean()
33+
34+
return reflect.DeepEqual(aval, eval), nil
35+
}
36+
37+
func (matcher *MatchXMLMatcher) FailureMessage(actual interface{}) (message string) {
38+
actualString, expectedString, _ := matcher.formattedPrint(actual)
39+
return fmt.Sprintf("Expected\n%s\nto match XML of\n%s", actualString, expectedString)
40+
}
41+
42+
func (matcher *MatchXMLMatcher) NegatedFailureMessage(actual interface{}) (message string) {
43+
actualString, expectedString, _ := matcher.formattedPrint(actual)
44+
return fmt.Sprintf("Expected\n%s\nnot to match XML of\n%s", actualString, expectedString)
45+
}
46+
47+
func (matcher *MatchXMLMatcher) formattedPrint(actual interface{}) (actualString, expectedString string, err error) {
48+
var ok bool
49+
actualString, ok = toString(actual)
50+
if !ok {
51+
return "", "", fmt.Errorf("MatchXMLMatcher matcher requires a string, stringer, or []byte. Got actual:\n%s", format.Object(actual, 1))
52+
}
53+
expectedString, ok = toString(matcher.XMLToMatch)
54+
if !ok {
55+
return "", "", fmt.Errorf("MatchXMLMatcher matcher requires a string, stringer, or []byte. Got expected:\n%s", format.Object(matcher.XMLToMatch, 1))
56+
}
57+
return actualString, expectedString, nil
58+
}
59+
60+
type xmlNode struct {
61+
XMLName xml.Name
62+
XMLAttr []xml.Attr `xml:",any,attr"`
63+
Content string `xml:",innerxml"`
64+
Nodes []*xmlNode `xml:",any"`
65+
}
66+
67+
func (n *xmlNode) Clean() {
68+
if len(n.Nodes) == 0 {
69+
return
70+
}
71+
n.Content = ""
72+
for _, child := range n.Nodes {
73+
child.Clean()
74+
}
75+
}

matchers/match_xml_matcher_test.go

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
package matchers_test
2+
3+
import (
4+
. "github.com/onsi/ginkgo"
5+
. "github.com/onsi/gomega"
6+
. "github.com/onsi/gomega/matchers"
7+
)
8+
9+
var _ = Describe("MatchXMLMatcher", func() {
10+
11+
var (
12+
sample_01 = readFileContents("test_data/xml/sample_01.xml")
13+
sample_02 = readFileContents("test_data/xml/sample_02.xml")
14+
sample_03 = readFileContents("test_data/xml/sample_03.xml")
15+
sample_04 = readFileContents("test_data/xml/sample_04.xml")
16+
sample_05 = readFileContents("test_data/xml/sample_05.xml")
17+
sample_06 = readFileContents("test_data/xml/sample_06.xml")
18+
sample_07 = readFileContents("test_data/xml/sample_07.xml")
19+
sample_08 = readFileContents("test_data/xml/sample_08.xml")
20+
sample_09 = readFileContents("test_data/xml/sample_09.xml")
21+
sample_10 = readFileContents("test_data/xml/sample_10.xml")
22+
)
23+
24+
Context("When passed stringifiables", func() {
25+
It("should succeed if the XML matches", func() {
26+
Ω(sample_01).Should(MatchXML(sample_01))
27+
Ω(sample_01).Should(MatchXML(sample_02))
28+
Ω(sample_01).Should(MatchXML(sample_03))
29+
Ω(sample_09).Should(MatchXML(sample_09))
30+
31+
Ω(sample_01).ShouldNot(MatchXML(sample_04))
32+
Ω(sample_01).ShouldNot(MatchXML(sample_05))
33+
Ω(sample_06).ShouldNot(MatchXML(sample_07))
34+
Ω(sample_07).ShouldNot(MatchXML(sample_08))
35+
Ω(sample_09).ShouldNot(MatchXML(sample_10))
36+
37+
Ω(sample_08).Should(MatchXML(sample_08))
38+
})
39+
40+
It("should work with byte arrays", func() {
41+
Ω([]byte(sample_01)).Should(MatchXML([]byte(sample_01)))
42+
Ω([]byte(sample_01)).Should(MatchXML(sample_01))
43+
Ω(sample_01).Should(MatchXML([]byte(sample_01)))
44+
})
45+
})
46+
47+
Context("when the expected is not valid XML", func() {
48+
It("should error and explain why", func() {
49+
success, err := (&MatchXMLMatcher{XMLToMatch: sample_01}).Match(`oops`)
50+
Ω(success).Should(BeFalse())
51+
Ω(err).Should(HaveOccurred())
52+
Ω(err.Error()).Should(ContainSubstring("Actual 'oops' should be valid XML"))
53+
})
54+
})
55+
56+
Context("when the actual is not valid XML", func() {
57+
It("should error and explain why", func() {
58+
success, err := (&MatchXMLMatcher{XMLToMatch: `oops`}).Match(sample_01)
59+
Ω(success).Should(BeFalse())
60+
Ω(err).Should(HaveOccurred())
61+
Ω(err.Error()).Should(ContainSubstring("Expected 'oops' should be valid XML"))
62+
})
63+
})
64+
65+
Context("when the expected is neither a string nor a stringer nor a byte array", func() {
66+
It("should error", func() {
67+
success, err := (&MatchXMLMatcher{XMLToMatch: 2}).Match(sample_01)
68+
Ω(success).Should(BeFalse())
69+
Ω(err).Should(HaveOccurred())
70+
Ω(err.Error()).Should(ContainSubstring("MatchXMLMatcher matcher requires a string, stringer, or []byte. Got expected:\n <int>: 2"))
71+
72+
success, err = (&MatchXMLMatcher{XMLToMatch: nil}).Match(sample_01)
73+
Ω(success).Should(BeFalse())
74+
Ω(err).Should(HaveOccurred())
75+
Ω(err.Error()).Should(ContainSubstring("MatchXMLMatcher matcher requires a string, stringer, or []byte. Got expected:\n <nil>: nil"))
76+
})
77+
})
78+
79+
Context("when the actual is neither a string nor a stringer nor a byte array", func() {
80+
It("should error", func() {
81+
success, err := (&MatchXMLMatcher{XMLToMatch: sample_01}).Match(2)
82+
Ω(success).Should(BeFalse())
83+
Ω(err).Should(HaveOccurred())
84+
Ω(err.Error()).Should(ContainSubstring("MatchXMLMatcher matcher requires a string, stringer, or []byte. Got actual:\n <int>: 2"))
85+
86+
success, err = (&MatchXMLMatcher{XMLToMatch: sample_01}).Match(nil)
87+
Ω(success).Should(BeFalse())
88+
Ω(err).Should(HaveOccurred())
89+
Ω(err.Error()).Should(ContainSubstring("MatchXMLMatcher matcher requires a string, stringer, or []byte. Got actual:\n <nil>: nil"))
90+
})
91+
})
92+
})

matchers/matcher_tests_suite_test.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package matchers_test
22

33
import (
4+
"fmt"
5+
"io/ioutil"
6+
"os"
47
"testing"
58

69
. "github.com/onsi/ginkgo"
@@ -28,3 +31,20 @@ func Test(t *testing.T) {
2831
RegisterFailHandler(Fail)
2932
RunSpecs(t, "Gomega Matchers")
3033
}
34+
35+
func readFileContents(filePath string) []byte {
36+
f := openFile(filePath)
37+
b, err := ioutil.ReadAll(f)
38+
if err != nil {
39+
panic(fmt.Errorf("failed to read file contents: %v", err))
40+
}
41+
return b
42+
}
43+
44+
func openFile(filePath string) *os.File {
45+
f, err := os.Open(filePath)
46+
if err != nil {
47+
panic(fmt.Errorf("failed to open file: %v", err))
48+
}
49+
return f
50+
}

matchers/test_data/xml/sample_01.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<note>
2+
<to>Tove</to>
3+
<from>Jani</from>
4+
<heading>Reminder</heading>
5+
<body>Don't forget me this weekend!</body>
6+
</note>

matchers/test_data/xml/sample_02.xml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
2+
3+
<note>
4+
<to>Tove</to>
5+
<from>Jani</from>
6+
<heading>Reminder</heading>
7+
<body>Don't forget me this weekend!</body>
8+
</note>
9+

matchers/test_data/xml/sample_03.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<note> <to>Tove</to> <from>Jani</from> <heading>Reminder</heading> <body>Don't forget me this weekend!</body> </note>

matchers/test_data/xml/sample_04.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<note>
2+
<to>Tove</to>
3+
<from>John</from>
4+
<heading>Doe</heading>
5+
<body>Don't forget me this weekend!</body>
6+
</note>

0 commit comments

Comments
 (0)