From b8114b25ed28437248322aad50209f737faa392c Mon Sep 17 00:00:00 2001
From: Lance Stout <lancestout@gmail.com>
Date: Wed, 17 Nov 2010 13:37:03 -0500
Subject: Make live stream tests work better.

SleekTest can now use matchers when checking stanzas, using
the method parameter for self.check(), self.recv(), and self.send():
    method='exact'      - Same behavior as before
           'xpath'      - Use xpath matcher
           'id'         - Use ID matcher
           'mask'       - Use XML mask matcher
           'stanzapath' - Use StanzaPath matcher

recv_feature and send_feature only accept 'exact' and 'mask' for now.
---
 sleekxmpp/test/sleektest.py            | 210 +++++++++++++++++++++------------
 sleekxmpp/xmlstream/matcher/xmlmask.py |  13 +-
 tests/live_test.py                     |  19 +--
 3 files changed, 147 insertions(+), 95 deletions(-)

diff --git a/sleekxmpp/test/sleektest.py b/sleekxmpp/test/sleektest.py
index f8b4b546..d7a6147b 100644
--- a/sleekxmpp/test/sleektest.py
+++ b/sleekxmpp/test/sleektest.py
@@ -14,6 +14,8 @@ from sleekxmpp.stanza import Message, Iq, Presence
 from sleekxmpp.test import TestSocket, TestLiveSocket
 from sleekxmpp.xmlstream import StanzaBase, ET, register_stanza_plugin
 from sleekxmpp.xmlstream.tostring import tostring
+from sleekxmpp.xmlstream.matcher import StanzaPath, MatcherId
+from sleekxmpp.xmlstream.matcher import MatchXMLMask, MatchXPath
 
 
 class SleekTest(unittest.TestCase):
@@ -140,7 +142,7 @@ class SleekTest(unittest.TestCase):
     # ------------------------------------------------------------------
     # Methods for comparing stanza objects to XML strings
 
-    def check(self, stanza, xml_string,
+    def check(self, stanza, criteria, method='exact',
               defaults=None, use_values=True):
         """
         Create and compare several stanza objects to a correct XML string.
@@ -161,7 +163,10 @@ class SleekTest(unittest.TestCase):
 
         Arguments:
             stanza       -- The stanza object to test.
-            xml_string   -- A string version of the correct XML expected.
+            criteria     -- An expression the stanza must match against.
+            method       -- The type of matching to use; one of: 
+                            'exact', 'mask', 'id', 'xpath', and 'stanzapath'.
+                            Defaults to the value of self.match_method.
             defaults     -- A list of stanza interfaces that have default
                             values. These interfaces will be set to their
                             defaults for the given and generated stanzas to
@@ -170,57 +175,74 @@ class SleekTest(unittest.TestCase):
                             setStanzaValues() should be used. Defaults to
                             True.
         """
-        stanza_class = stanza.__class__
-        xml = self.parse_xml(xml_string)
-
-        # Ensure that top level namespaces are used, even if they
-        # were not provided.
-        self.fix_namespaces(stanza.xml, 'jabber:client')
-        self.fix_namespaces(xml, 'jabber:client')
-
-        stanza2 = stanza_class(xml=xml)
-
-        if use_values:
-            # Using getStanzaValues() and setStanzaValues() will add
-            # XML for any interface that has a default value. We need
-            # to set those defaults on the existing stanzas and XML
-            # so that they will compare correctly.
-            default_stanza = stanza_class()
-            if defaults is None:
-                known_defaults = {
-                    Message: ['type'],
-                    Presence: ['priority']
-                }
-                defaults = known_defaults.get(stanza_class, [])
-            for interface in defaults:
-                stanza[interface] = stanza[interface]
-                stanza2[interface] = stanza2[interface]
-                # Can really only automatically add defaults for top
-                # level attribute values. Anything else must be accounted
-                # for in the provided XML string.
-                if interface not in xml.attrib:
-                    if interface in default_stanza.xml.attrib:
-                        value = default_stanza.xml.attrib[interface]
-                        xml.attrib[interface] = value
-
-            values = stanza2.getStanzaValues()
-            stanza3 = stanza_class()
-            stanza3.setStanzaValues(values)
-
-            debug = "Three methods for creating stanzas do not match.\n"
-            debug += "Given XML:\n%s\n" % tostring(xml)
-            debug += "Given stanza:\n%s\n" % tostring(stanza.xml)
-            debug += "Generated stanza:\n%s\n" % tostring(stanza2.xml)
-            debug += "Second generated stanza:\n%s\n" % tostring(stanza3.xml)
-            result = self.compare(xml, stanza.xml, stanza2.xml, stanza3.xml)
+        if method is None and hasattr(self, 'match_method'):
+            method = getattr(self, 'match_method')
+
+        if method != 'exact':
+            matchers = {'stanzapath': StanzaPath,
+                        'xpath': MatchXPath,
+                        'mask': MatchXMLMask,
+                        'id': MatcherId}
+            Matcher = matchers.get(method, None)
+            if Matcher is None:
+                raise ValueError("Unknown matching method.")
+            test = Matcher(criteria)
+            self.failUnless(test.match(stanza),
+                    "Stanza did not match using %s method:\n" % method + \
+                    "Criteria:\n%s\n" % str(criteria) + \
+                    "Stanza:\n%s" % str(stanza))
         else:
-            debug = "Two methods for creating stanzas do not match.\n"
-            debug += "Given XML:\n%s\n" % tostring(xml)
-            debug += "Given stanza:\n%s\n" % tostring(stanza.xml)
-            debug += "Generated stanza:\n%s\n" % tostring(stanza2.xml)
-            result = self.compare(xml, stanza.xml, stanza2.xml)
+            stanza_class = stanza.__class__
+            xml = self.parse_xml(criteria)
 
-        self.failUnless(result, debug)
+            # Ensure that top level namespaces are used, even if they
+            # were not provided.
+            self.fix_namespaces(stanza.xml, 'jabber:client')
+            self.fix_namespaces(xml, 'jabber:client')
+
+            stanza2 = stanza_class(xml=xml)
+
+            if use_values:
+                # Using getStanzaValues() and setStanzaValues() will add
+                # XML for any interface that has a default value. We need
+                # to set those defaults on the existing stanzas and XML
+                # so that they will compare correctly.
+                default_stanza = stanza_class()
+                if defaults is None:
+                    known_defaults = {
+                        Message: ['type'],
+                        Presence: ['priority']
+                    }
+                    defaults = known_defaults.get(stanza_class, [])
+                for interface in defaults:
+                    stanza[interface] = stanza[interface]
+                    stanza2[interface] = stanza2[interface]
+                    # Can really only automatically add defaults for top
+                    # level attribute values. Anything else must be accounted
+                    # for in the provided XML string.
+                    if interface not in xml.attrib:
+                        if interface in default_stanza.xml.attrib:
+                            value = default_stanza.xml.attrib[interface]
+                            xml.attrib[interface] = value
+
+                values = stanza2.getStanzaValues()
+                stanza3 = stanza_class()
+                stanza3.setStanzaValues(values)
+
+                debug = "Three methods for creating stanzas do not match.\n"
+                debug += "Given XML:\n%s\n" % tostring(xml)
+                debug += "Given stanza:\n%s\n" % tostring(stanza.xml)
+                debug += "Generated stanza:\n%s\n" % tostring(stanza2.xml)
+                debug += "Second generated stanza:\n%s\n" % tostring(stanza3.xml)
+                result = self.compare(xml, stanza.xml, stanza2.xml, stanza3.xml)
+            else:
+                debug = "Two methods for creating stanzas do not match.\n"
+                debug += "Given XML:\n%s\n" % tostring(xml)
+                debug += "Given stanza:\n%s\n" % tostring(stanza.xml)
+                debug += "Generated stanza:\n%s\n" % tostring(stanza2.xml)
+                result = self.compare(xml, stanza.xml, stanza2.xml)
+
+            self.failUnless(result, debug)
 
     # ------------------------------------------------------------------
     # Methods for simulating stanza streams.
@@ -320,20 +342,23 @@ class SleekTest(unittest.TestCase):
         parts.append('xmlns="%s"' % default_ns)
         return header % ' '.join(parts)
 
-    def recv(self, data, stanza_class=StanzaBase, defaults=[],
-             use_values=True, timeout=1):
+    def recv(self, data, defaults=[], method='exact',
+             use_values=True, timeout=1): 
         """
         Pass data to the dummy XMPP client as if it came from an XMPP server.
 
         If using a live connection, verify what the server has sent.
 
         Arguments:
-            data         -- String stanza XML to be received and processed by
-                            the XMPP client or component.
-            stanza_class -- The stanza object class for verifying data received
-                            by a live connection. Defaults to StanzaBase.
+            data         -- If a dummy socket is being used, the XML that is to
+                            be received next. Otherwise it is the criteria used
+                            to match against live data that is received.
             defaults     -- A list of stanza interfaces with default values that
                             may interfere with comparisons.
+            method       -- Select the type of comparison to use for
+                            verifying the received stanza. Options are 'exact',
+                            'id', 'stanzapath', 'xpath', and 'mask'.
+                            Defaults to the value of self.match_method.
             use_values   -- Indicates if stanza comparisons should test using
                             getStanzaValues() and setStanzaValues().
                             Defaults to True.
@@ -347,10 +372,13 @@ class SleekTest(unittest.TestCase):
             recv_data = self.xmpp.socket.next_recv(timeout)
             if recv_data is None:
                 return False
-            stanza = stanza_class(xml=self.parse_xml(recv_data))
-            return self.check(stanza_class, stanza, data,
-                                     defaults=defaults,
-                                     use_values=use_values)
+            xml = self.parse_xml(recv_data)
+            self.fix_namespaces(xml, 'jabber:client')
+            stanza = self.xmpp._build_stanza(xml, 'jabber:client')
+            self.check(stanza, data,
+                       method=method,
+                       defaults=defaults,
+                       use_values=use_values)
         else:
             # place the data in the dummy socket receiving queue.
             data = str(data)
@@ -424,21 +452,33 @@ class SleekTest(unittest.TestCase):
                 '%s %s' % (xml.tag, xml.attrib),
                 '%s %s' % (recv_xml.tag, recv_xml.attrib)))
 
-    def recv_feature(self, data, use_values=True, timeout=1):
+    def recv_feature(self, data, method='mask', use_values=True, timeout=1):
         """
         """
+        if method is None and hasattr(self, 'match_method'):
+            method = getattr(self, 'match_method')
+
         if self.xmpp.socket.is_live:
             # we are working with a live connection, so we should
             # verify what has been received instead of simulating
             # receiving data.
             recv_data = self.xmpp.socket.next_recv(timeout)
-            if recv_data is None:
-                return False
             xml = self.parse_xml(data)
             recv_xml = self.parse_xml(recv_data)
-            self.failUnless(self.compare(xml, recv_xml),
-                "Features do not match.\nDesired:\n%s\nReceived:\n%s" % (
-                    tostring(xml), tostring(recv_xml)))
+            if recv_data is None:
+                return False
+            if method == 'exact':
+                self.failUnless(self.compare(xml, recv_xml),
+                    "Features do not match.\nDesired:\n%s\nReceived:\n%s" % (
+                        tostring(xml), tostring(recv_xml)))
+            elif method == 'mask':
+                matcher = MatchXMLMask(xml)
+                self.failUnless(matcher.match(recv_xml), 
+                    "Stanza did not match using %s method:\n" % method + \
+                    "Criteria:\n%s\n" % tostring(xml) + \
+                    "Stanza:\n%s" % tostring(recv_xml))
+            else:
+                raise ValueError("Uknown matching method: %s" % method)
         else:
             # place the data in the dummy socket receiving queue.
             data = str(data)
@@ -489,20 +529,29 @@ class SleekTest(unittest.TestCase):
             "Stream headers do not match:\nDesired:\n%s\nSent:\n%s" % (
                 header, sent_header))
 
-    def send_feature(self, data, use_values=True, timeout=1):
+    def send_feature(self, data, method='mask', use_values=True, timeout=1):
         """
         """
         sent_data = self.xmpp.socket.next_sent(timeout)
-        if sent_data is None:
-            return False
         xml = self.parse_xml(data)
         sent_xml = self.parse_xml(sent_data)
-        self.failUnless(self.compare(xml, sent_xml),
-            "Features do not match.\nDesired:\n%s\nSent:\n%s" % (
-                tostring(xml), tostring(sent_xml)))
+        if sent_data is None:
+            return False
+        if method == 'exact':
+            self.failUnless(self.compare(xml, sent_xml),
+                "Features do not match.\nDesired:\n%s\nReceived:\n%s" % (
+                    tostring(xml), tostring(sent_xml)))
+        elif method == 'mask':
+            matcher = MatchXMLMask(xml)
+            self.failUnless(matcher.match(sent_xml), 
+                "Stanza did not match using %s method:\n" % method + \
+                "Criteria:\n%s\n" % tostring(xml) + \
+                "Stanza:\n%s" % tostring(sent_xml))
+        else:
+            raise ValueError("Uknown matching method: %s" % method)
 
-    def send(self, data, defaults=None,
-             use_values=True, timeout=.1):
+    def send(self, data, defaults=None, use_values=True,
+             timeout=.1, method='exact'):
         """
         Check that the XMPP client sent the given stanza XML.
 
@@ -518,15 +567,20 @@ class SleekTest(unittest.TestCase):
                             values which may interfere with comparisons.
             timeout      -- Time in seconds to wait for a stanza before
                             failing the check.
+            method       -- Select the type of comparison to use for
+                            verifying the sent stanza. Options are 'exact',
+                            'id', 'stanzapath', 'xpath', and 'mask'.
+                            Defaults to the value of self.match_method.
         """
+        sent = self.xmpp.socket.next_sent(timeout)
         if isinstance(data, str):
             xml = self.parse_xml(data)
             self.fix_namespaces(xml, 'jabber:client')
             data = self.xmpp._build_stanza(xml, 'jabber:client')
-        sent = self.xmpp.socket.next_sent(timeout)
         self.check(data, sent,
-                          defaults=defaults,
-                          use_values=use_values)
+                   method=method,
+                   defaults=defaults,
+                   use_values=use_values)
 
     def stream_close(self):
         """
diff --git a/sleekxmpp/xmlstream/matcher/xmlmask.py b/sleekxmpp/xmlstream/matcher/xmlmask.py
index 6ebb437d..60e19495 100644
--- a/sleekxmpp/xmlstream/matcher/xmlmask.py
+++ b/sleekxmpp/xmlstream/matcher/xmlmask.py
@@ -117,7 +117,7 @@ class MatchXMLMask(MatcherBase):
                 return False
 
         # If the mask includes text, compare it.
-        if mask.text and source.text != mask.text:
+        if mask.text and source.text and source.text.strip() != mask.text.strip():
             return False
 
         # Compare attributes. The stanza must include the attributes
@@ -127,10 +127,17 @@ class MatchXMLMask(MatcherBase):
                 return False
 
         # Recursively check subelements.
+        matched_elements = {}
         for subelement in mask:
             if use_ns:
-                if not self._mask_cmp(source.find(subelement.tag),
-                                      subelement, use_ns):
+                matched = False
+                for other in source.findall(subelement.tag):
+                    matched_elements[other] = False
+                    if self._mask_cmp(other, subelement, use_ns):
+                        if not matched_elements.get(other, False):
+                            matched_elements[other] = True
+                            matched = True
+                if not matched:
                     return False
             else:
                 if not self._mask_cmp(self._get_child(source, subelement.tag),
diff --git a/tests/live_test.py b/tests/live_test.py
index 4b4394e9..16b6f1cc 100644
--- a/tests/live_test.py
+++ b/tests/live_test.py
@@ -1,3 +1,5 @@
+import logging
+
 from sleekxmpp.test import *
 import sleekxmpp.plugins.xep_0033 as xep_0033
 
@@ -29,10 +31,6 @@ class TestLiveStream(SleekTest):
               <mechanism>DIGEST-MD5</mechanism>
               <mechanism>PLAIN</mechanism>
             </mechanisms>
-            <c xmlns="http://jabber.org/protocol/caps"
-               node="http://www.process-one.net/en/ejabberd/"
-               ver="TQ2JFyRoSa70h2G1bpgjzuXb2sU=" hash="sha-1" />
-            <register xmlns="http://jabber.org/features/iq-register" />
           </stream:features>
         """)
         self.send_feature("""
@@ -49,11 +47,6 @@ class TestLiveStream(SleekTest):
               <mechanism>DIGEST-MD5</mechanism>
               <mechanism>PLAIN</mechanism>
             </mechanisms>
-            <c xmlns="http://jabber.org/protocol/caps"
-               node="http://www.process-one.net/en/ejabberd/"
-               ver="TQ2JFyRoSa70h2G1bpgjzuXb2sU="
-               hash="sha-1" />
-            <register xmlns="http://jabber.org/features/iq-register" />
           </stream:features>
         """)
         self.send_feature("""
@@ -69,11 +62,6 @@ class TestLiveStream(SleekTest):
           <stream:features>
             <bind xmlns="urn:ietf:params:xml:ns:xmpp-bind" />
             <session xmlns="urn:ietf:params:xml:ns:xmpp-session" />
-            <c xmlns="http://jabber.org/protocol/caps"
-               node="http://www.process-one.net/en/ejabberd/"
-               ver="TQ2JFyRoSa70h2G1bpgjzuXb2sU="
-               hash="sha-1" />
-            <register xmlns="http://jabber.org/features/iq-register" />
           </stream:features>
         """)
 
@@ -99,6 +87,9 @@ class TestLiveStream(SleekTest):
 suite = unittest.TestLoader().loadTestsFromTestCase(TestLiveStream)
 
 if __name__ == '__main__':
+    logging.basicConfig(level=logging.DEBUG,
+                        format='%(levelname)-8s %(message)s')
+
     tests = unittest.TestSuite([suite])
     result = unittest.TextTestRunner(verbosity=2).run(tests)
     test_ns = 'http://andyet.net/protocol/tests'
-- 
cgit v1.2.3