From a6b7b3d409015b8d92ccd6038517bb9e9279358c Mon Sep 17 00:00:00 2001 From: kendo5731 Date: Sat, 29 Apr 2017 17:16:31 +0200 Subject: [PATCH] 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 https://github.com/golang/go/issues/7535 Because Go has no support for collecting all XML attributes before 1.8 (see [here](https://github.com/golang/go/commit/c1a1328c5f004c62b8c08faf0d0d2845e0be5d37)), the two following XMLs will be equal for 1.7 and before: --- .travis.yml | 1 + matchers.go | 14 ++ matchers/match_xml_matcher.go | 58 +++++++ matchers/match_xml_matcher_go1.8_test.go | 22 +++ matchers/match_xml_matcher_test.go | 87 ++++++++++ matchers/matcher_tests_suite_test.go | 20 +++ matchers/test_data/xml/sample_01.xml | 6 + matchers/test_data/xml/sample_02.xml | 9 + matchers/test_data/xml/sample_03.xml | 1 + matchers/test_data/xml/sample_04.xml | 6 + matchers/test_data/xml/sample_05.xml | 211 +++++++++++++++++++++++ matchers/test_data/xml/sample_06.xml | 13 ++ matchers/test_data/xml/sample_07.xml | 13 ++ matchers/test_data/xml/sample_08.xml | 13 ++ matchers/test_data/xml/sample_09.xml | 4 + matchers/test_data/xml/sample_10.xml | 4 + matchers/xml_node_before_go1.8.go | 21 +++ matchers/xml_node_go1.8.go | 22 +++ 18 files changed, 525 insertions(+) create mode 100644 matchers/match_xml_matcher.go create mode 100644 matchers/match_xml_matcher_go1.8_test.go create mode 100644 matchers/match_xml_matcher_test.go create mode 100644 matchers/test_data/xml/sample_01.xml create mode 100644 matchers/test_data/xml/sample_02.xml create mode 100644 matchers/test_data/xml/sample_03.xml create mode 100644 matchers/test_data/xml/sample_04.xml create mode 100644 matchers/test_data/xml/sample_05.xml create mode 100644 matchers/test_data/xml/sample_06.xml create mode 100644 matchers/test_data/xml/sample_07.xml create mode 100644 matchers/test_data/xml/sample_08.xml create mode 100644 matchers/test_data/xml/sample_09.xml create mode 100644 matchers/test_data/xml/sample_10.xml create mode 100644 matchers/xml_node_before_go1.8.go create mode 100644 matchers/xml_node_go1.8.go diff --git a/.travis.yml b/.travis.yml index 9bc3fd027..61d0f41fa 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,6 +2,7 @@ language: go go: - 1.6 - 1.7 + - 1.8 install: - go get -v ./... diff --git a/matchers.go b/matchers.go index ad04ab657..eede1ad82 100644 --- a/matchers.go +++ b/matchers.go @@ -214,6 +214,20 @@ 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. +//The comparison of attribute values is not supported for go1.7 and before. +//For go1.7, the two following XMLs are equal: +// +// +// +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. diff --git a/matchers/match_xml_matcher.go b/matchers/match_xml_matcher.go new file mode 100644 index 000000000..17aa971f0 --- /dev/null +++ b/matchers/match_xml_matcher.go @@ -0,0 +1,58 @@ +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 +} diff --git a/matchers/match_xml_matcher_go1.8_test.go b/matchers/match_xml_matcher_go1.8_test.go new file mode 100644 index 000000000..694d8db1a --- /dev/null +++ b/matchers/match_xml_matcher_go1.8_test.go @@ -0,0 +1,22 @@ +// +build go1.8 + +package matchers_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("MatchXMLMatcher Go 1.8", func() { + + var ( + 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_09).ShouldNot(MatchXML(sample_10)) // same structures with different attribute values + }) + }) +}) diff --git a/matchers/match_xml_matcher_test.go b/matchers/match_xml_matcher_test.go new file mode 100644 index 000000000..c5479d079 --- /dev/null +++ b/matchers/match_xml_matcher_test.go @@ -0,0 +1,87 @@ +// +build !go1.8 + +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") + ) + + Context("When passed stringifiables", func() { + It("should succeed if the XML matches", func() { + Ω(sample_01).Should(MatchXML(sample_01)) // same XML + Ω(sample_01).Should(MatchXML(sample_02)) // same XML with blank lines + Ω(sample_01).Should(MatchXML(sample_03)) // same XML with different formatting + Ω(sample_01).ShouldNot(MatchXML(sample_04)) // same structures with different values + Ω(sample_01).ShouldNot(MatchXML(sample_05)) // different structures + Ω(sample_06).ShouldNot(MatchXML(sample_07)) // same xml names with different namespaces + Ω(sample_07).ShouldNot(MatchXML(sample_08)) // same structures with different values + }) + + 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 : 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")) + }) + }) + + 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 : 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")) + }) + }) +}) diff --git a/matchers/matcher_tests_suite_test.go b/matchers/matcher_tests_suite_test.go index 01b11b97d..b5f76c995 100644 --- a/matchers/matcher_tests_suite_test.go +++ b/matchers/matcher_tests_suite_test.go @@ -1,6 +1,9 @@ package matchers_test import ( + "fmt" + "io/ioutil" + "os" "testing" . "github.com/onsi/ginkgo" @@ -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 +} diff --git a/matchers/test_data/xml/sample_01.xml b/matchers/test_data/xml/sample_01.xml new file mode 100644 index 000000000..90f0a1b45 --- /dev/null +++ b/matchers/test_data/xml/sample_01.xml @@ -0,0 +1,6 @@ + + Tove + Jani + Reminder + Don't forget me this weekend! + \ No newline at end of file diff --git a/matchers/test_data/xml/sample_02.xml b/matchers/test_data/xml/sample_02.xml new file mode 100644 index 000000000..3863b83c3 --- /dev/null +++ b/matchers/test_data/xml/sample_02.xml @@ -0,0 +1,9 @@ + + + + Tove + Jani + Reminder + Don't forget me this weekend! + + diff --git a/matchers/test_data/xml/sample_03.xml b/matchers/test_data/xml/sample_03.xml new file mode 100644 index 000000000..a491c213c --- /dev/null +++ b/matchers/test_data/xml/sample_03.xml @@ -0,0 +1 @@ + Tove Jani Reminder Don't forget me this weekend! diff --git a/matchers/test_data/xml/sample_04.xml b/matchers/test_data/xml/sample_04.xml new file mode 100644 index 000000000..dcfd3db03 --- /dev/null +++ b/matchers/test_data/xml/sample_04.xml @@ -0,0 +1,6 @@ + + Tove + John + Doe + Don't forget me this weekend! + \ No newline at end of file diff --git a/matchers/test_data/xml/sample_05.xml b/matchers/test_data/xml/sample_05.xml new file mode 100644 index 000000000..de15a6a55 --- /dev/null +++ b/matchers/test_data/xml/sample_05.xml @@ -0,0 +1,211 @@ + + + + Empire Burlesque + Bob Dylan + USA + Columbia + 10.90 + 1985 + + + Hide your heart + Bonnie Tyler + UK + CBS Records + 9.90 + 1988 + + + Greatest Hits + Dolly Parton + USA + RCA + 9.90 + 1982 + + + Still got the blues + Gary Moore + UK + Virgin records + 10.20 + 1990 + + + Eros + Eros Ramazzotti + EU + BMG + 9.90 + 1997 + + + One night only + Bee Gees + UK + Polydor + 10.90 + 1998 + + + Sylvias Mother + Dr.Hook + UK + CBS + 8.10 + 1973 + + + Maggie May + Rod Stewart + UK + Pickwick + 8.50 + 1990 + + + Romanza + Andrea Bocelli + EU + Polydor + 10.80 + 1996 + + + When a man loves a woman + Percy Sledge + USA + Atlantic + 8.70 + 1987 + + + Black angel + Savage Rose + EU + Mega + 10.90 + 1995 + + + 1999 Grammy Nominees + Many + USA + Grammy + 10.20 + 1999 + + + For the good times + Kenny Rogers + UK + Mucik Master + 8.70 + 1995 + + + Big Willie style + Will Smith + USA + Columbia + 9.90 + 1997 + + + Tupelo Honey + Van Morrison + UK + Polydor + 8.20 + 1971 + + + Soulsville + Jorn Hoel + Norway + WEA + 7.90 + 1996 + + + The very best of + Cat Stevens + UK + Island + 8.90 + 1990 + + + Stop + Sam Brown + UK + A and M + 8.90 + 1988 + + + Bridge of Spies + T'Pau + UK + Siren + 7.90 + 1987 + + + Private Dancer + Tina Turner + UK + Capitol + 8.90 + 1983 + + + Midt om natten + Kim Larsen + EU + Medley + 7.80 + 1983 + + + Pavarotti Gala Concert + Luciano Pavarotti + UK + DECCA + 9.90 + 1991 + + + The dock of the bay + Otis Redding + USA + Stax Records + 7.90 + 1968 + + + Picture book + Simply Red + EU + Elektra + 7.20 + 1985 + + + Red + The Communards + UK + London + 7.80 + 1987 + + + Unchain my heart + Joe Cocker + USA + EMI + 8.20 + 1987 + + diff --git a/matchers/test_data/xml/sample_06.xml b/matchers/test_data/xml/sample_06.xml new file mode 100644 index 000000000..4ba90fb97 --- /dev/null +++ b/matchers/test_data/xml/sample_06.xml @@ -0,0 +1,13 @@ + + + + + + +
ApplesBananas
+ + African Coffee Table + 80 + 120 +
+
\ No newline at end of file diff --git a/matchers/test_data/xml/sample_07.xml b/matchers/test_data/xml/sample_07.xml new file mode 100644 index 000000000..34b9e9775 --- /dev/null +++ b/matchers/test_data/xml/sample_07.xml @@ -0,0 +1,13 @@ + + + + Apples + Bananas + + + + African Coffee Table + 80 + 120 + + \ No newline at end of file diff --git a/matchers/test_data/xml/sample_08.xml b/matchers/test_data/xml/sample_08.xml new file mode 100644 index 000000000..ccaee4e1a --- /dev/null +++ b/matchers/test_data/xml/sample_08.xml @@ -0,0 +1,13 @@ + + + + Apples + Oranges + + + + African Coffee Table + 80 + 120 + + \ No newline at end of file diff --git a/matchers/test_data/xml/sample_09.xml b/matchers/test_data/xml/sample_09.xml new file mode 100644 index 000000000..531f84d3f --- /dev/null +++ b/matchers/test_data/xml/sample_09.xml @@ -0,0 +1,4 @@ + + Foo + Bar + \ No newline at end of file diff --git a/matchers/test_data/xml/sample_10.xml b/matchers/test_data/xml/sample_10.xml new file mode 100644 index 000000000..b1e1e1fbe --- /dev/null +++ b/matchers/test_data/xml/sample_10.xml @@ -0,0 +1,4 @@ + + Foo + Bar + \ No newline at end of file diff --git a/matchers/xml_node_before_go1.8.go b/matchers/xml_node_before_go1.8.go new file mode 100644 index 000000000..7da4e23e1 --- /dev/null +++ b/matchers/xml_node_before_go1.8.go @@ -0,0 +1,21 @@ +// +build !go1.8 + +package matchers + +import "encoding/xml" + +type xmlNode struct { + XMLName xml.Name + Content []byte `xml:",innerxml"` + Nodes []*xmlNode `xml:",any"` +} + +func (n *xmlNode) Clean() { + if len(n.Nodes) == 0 { + return + } + n.Content = nil + for _, child := range n.Nodes { + child.Clean() + } +} diff --git a/matchers/xml_node_go1.8.go b/matchers/xml_node_go1.8.go new file mode 100644 index 000000000..fac182903 --- /dev/null +++ b/matchers/xml_node_go1.8.go @@ -0,0 +1,22 @@ +// +build go1.8 + +package matchers + +import "encoding/xml" + +type xmlNode struct { + XMLName xml.Name + XMLAttr []xml.Attr `xml:",any,attr"` + Content []byte `xml:",innerxml"` + Nodes []*xmlNode `xml:",any"` +} + +func (n *xmlNode) Clean() { + if len(n.Nodes) == 0 { + return + } + n.Content = nil + for _, child := range n.Nodes { + child.Clean() + } +}