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.
This commit is contained in:
Lance Stout 2010-11-17 13:37:03 -05:00
parent 45991e47ee
commit b8114b25ed
3 changed files with 146 additions and 94 deletions

View file

@ -14,6 +14,8 @@ from sleekxmpp.stanza import Message, Iq, Presence
from sleekxmpp.test import TestSocket, TestLiveSocket from sleekxmpp.test import TestSocket, TestLiveSocket
from sleekxmpp.xmlstream import StanzaBase, ET, register_stanza_plugin from sleekxmpp.xmlstream import StanzaBase, ET, register_stanza_plugin
from sleekxmpp.xmlstream.tostring import tostring from sleekxmpp.xmlstream.tostring import tostring
from sleekxmpp.xmlstream.matcher import StanzaPath, MatcherId
from sleekxmpp.xmlstream.matcher import MatchXMLMask, MatchXPath
class SleekTest(unittest.TestCase): class SleekTest(unittest.TestCase):
@ -140,7 +142,7 @@ class SleekTest(unittest.TestCase):
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# Methods for comparing stanza objects to XML strings # 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): defaults=None, use_values=True):
""" """
Create and compare several stanza objects to a correct XML string. Create and compare several stanza objects to a correct XML string.
@ -161,7 +163,10 @@ class SleekTest(unittest.TestCase):
Arguments: Arguments:
stanza -- The stanza object to test. 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 defaults -- A list of stanza interfaces that have default
values. These interfaces will be set to their values. These interfaces will be set to their
defaults for the given and generated stanzas to defaults for the given and generated stanzas to
@ -170,57 +175,74 @@ class SleekTest(unittest.TestCase):
setStanzaValues() should be used. Defaults to setStanzaValues() should be used. Defaults to
True. True.
""" """
stanza_class = stanza.__class__ if method is None and hasattr(self, 'match_method'):
xml = self.parse_xml(xml_string) method = getattr(self, 'match_method')
# Ensure that top level namespaces are used, even if they if method != 'exact':
# were not provided. matchers = {'stanzapath': StanzaPath,
self.fix_namespaces(stanza.xml, 'jabber:client') 'xpath': MatchXPath,
self.fix_namespaces(xml, 'jabber:client') 'mask': MatchXMLMask,
'id': MatcherId}
stanza2 = stanza_class(xml=xml) Matcher = matchers.get(method, None)
if Matcher is None:
if use_values: raise ValueError("Unknown matching method.")
# Using getStanzaValues() and setStanzaValues() will add test = Matcher(criteria)
# XML for any interface that has a default value. We need self.failUnless(test.match(stanza),
# to set those defaults on the existing stanzas and XML "Stanza did not match using %s method:\n" % method + \
# so that they will compare correctly. "Criteria:\n%s\n" % str(criteria) + \
default_stanza = stanza_class() "Stanza:\n%s" % str(stanza))
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: else:
debug = "Two methods for creating stanzas do not match.\n" stanza_class = stanza.__class__
debug += "Given XML:\n%s\n" % tostring(xml) xml = self.parse_xml(criteria)
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) # 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. # Methods for simulating stanza streams.
@ -320,20 +342,23 @@ class SleekTest(unittest.TestCase):
parts.append('xmlns="%s"' % default_ns) parts.append('xmlns="%s"' % default_ns)
return header % ' '.join(parts) return header % ' '.join(parts)
def recv(self, data, stanza_class=StanzaBase, defaults=[], def recv(self, data, defaults=[], method='exact',
use_values=True, timeout=1): use_values=True, timeout=1):
""" """
Pass data to the dummy XMPP client as if it came from an XMPP server. 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. If using a live connection, verify what the server has sent.
Arguments: Arguments:
data -- String stanza XML to be received and processed by data -- If a dummy socket is being used, the XML that is to
the XMPP client or component. be received next. Otherwise it is the criteria used
stanza_class -- The stanza object class for verifying data received to match against live data that is received.
by a live connection. Defaults to StanzaBase.
defaults -- A list of stanza interfaces with default values that defaults -- A list of stanza interfaces with default values that
may interfere with comparisons. 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 use_values -- Indicates if stanza comparisons should test using
getStanzaValues() and setStanzaValues(). getStanzaValues() and setStanzaValues().
Defaults to True. Defaults to True.
@ -347,10 +372,13 @@ class SleekTest(unittest.TestCase):
recv_data = self.xmpp.socket.next_recv(timeout) recv_data = self.xmpp.socket.next_recv(timeout)
if recv_data is None: if recv_data is None:
return False return False
stanza = stanza_class(xml=self.parse_xml(recv_data)) xml = self.parse_xml(recv_data)
return self.check(stanza_class, stanza, data, self.fix_namespaces(xml, 'jabber:client')
defaults=defaults, stanza = self.xmpp._build_stanza(xml, 'jabber:client')
use_values=use_values) self.check(stanza, data,
method=method,
defaults=defaults,
use_values=use_values)
else: else:
# place the data in the dummy socket receiving queue. # place the data in the dummy socket receiving queue.
data = str(data) data = str(data)
@ -424,21 +452,33 @@ class SleekTest(unittest.TestCase):
'%s %s' % (xml.tag, xml.attrib), '%s %s' % (xml.tag, xml.attrib),
'%s %s' % (recv_xml.tag, recv_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: if self.xmpp.socket.is_live:
# we are working with a live connection, so we should # we are working with a live connection, so we should
# verify what has been received instead of simulating # verify what has been received instead of simulating
# receiving data. # receiving data.
recv_data = self.xmpp.socket.next_recv(timeout) recv_data = self.xmpp.socket.next_recv(timeout)
if recv_data is None:
return False
xml = self.parse_xml(data) xml = self.parse_xml(data)
recv_xml = self.parse_xml(recv_data) recv_xml = self.parse_xml(recv_data)
self.failUnless(self.compare(xml, recv_xml), if recv_data is None:
"Features do not match.\nDesired:\n%s\nReceived:\n%s" % ( return False
tostring(xml), tostring(recv_xml))) 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: else:
# place the data in the dummy socket receiving queue. # place the data in the dummy socket receiving queue.
data = str(data) data = str(data)
@ -489,20 +529,29 @@ class SleekTest(unittest.TestCase):
"Stream headers do not match:\nDesired:\n%s\nSent:\n%s" % ( "Stream headers do not match:\nDesired:\n%s\nSent:\n%s" % (
header, sent_header)) 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) sent_data = self.xmpp.socket.next_sent(timeout)
if sent_data is None:
return False
xml = self.parse_xml(data) xml = self.parse_xml(data)
sent_xml = self.parse_xml(sent_data) sent_xml = self.parse_xml(sent_data)
self.failUnless(self.compare(xml, sent_xml), if sent_data is None:
"Features do not match.\nDesired:\n%s\nSent:\n%s" % ( return False
tostring(xml), tostring(sent_xml))) 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, def send(self, data, defaults=None, use_values=True,
use_values=True, timeout=.1): timeout=.1, method='exact'):
""" """
Check that the XMPP client sent the given stanza XML. Check that the XMPP client sent the given stanza XML.
@ -518,15 +567,20 @@ class SleekTest(unittest.TestCase):
values which may interfere with comparisons. values which may interfere with comparisons.
timeout -- Time in seconds to wait for a stanza before timeout -- Time in seconds to wait for a stanza before
failing the check. 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): if isinstance(data, str):
xml = self.parse_xml(data) xml = self.parse_xml(data)
self.fix_namespaces(xml, 'jabber:client') self.fix_namespaces(xml, 'jabber:client')
data = self.xmpp._build_stanza(xml, 'jabber:client') data = self.xmpp._build_stanza(xml, 'jabber:client')
sent = self.xmpp.socket.next_sent(timeout)
self.check(data, sent, self.check(data, sent,
defaults=defaults, method=method,
use_values=use_values) defaults=defaults,
use_values=use_values)
def stream_close(self): def stream_close(self):
""" """

View file

@ -117,7 +117,7 @@ class MatchXMLMask(MatcherBase):
return False return False
# If the mask includes text, compare it. # 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 return False
# Compare attributes. The stanza must include the attributes # Compare attributes. The stanza must include the attributes
@ -127,10 +127,17 @@ class MatchXMLMask(MatcherBase):
return False return False
# Recursively check subelements. # Recursively check subelements.
matched_elements = {}
for subelement in mask: for subelement in mask:
if use_ns: if use_ns:
if not self._mask_cmp(source.find(subelement.tag), matched = False
subelement, use_ns): 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 return False
else: else:
if not self._mask_cmp(self._get_child(source, subelement.tag), if not self._mask_cmp(self._get_child(source, subelement.tag),

View file

@ -1,3 +1,5 @@
import logging
from sleekxmpp.test import * from sleekxmpp.test import *
import sleekxmpp.plugins.xep_0033 as xep_0033 import sleekxmpp.plugins.xep_0033 as xep_0033
@ -29,10 +31,6 @@ class TestLiveStream(SleekTest):
<mechanism>DIGEST-MD5</mechanism> <mechanism>DIGEST-MD5</mechanism>
<mechanism>PLAIN</mechanism> <mechanism>PLAIN</mechanism>
</mechanisms> </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> </stream:features>
""") """)
self.send_feature(""" self.send_feature("""
@ -49,11 +47,6 @@ class TestLiveStream(SleekTest):
<mechanism>DIGEST-MD5</mechanism> <mechanism>DIGEST-MD5</mechanism>
<mechanism>PLAIN</mechanism> <mechanism>PLAIN</mechanism>
</mechanisms> </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> </stream:features>
""") """)
self.send_feature(""" self.send_feature("""
@ -69,11 +62,6 @@ class TestLiveStream(SleekTest):
<stream:features> <stream:features>
<bind xmlns="urn:ietf:params:xml:ns:xmpp-bind" /> <bind xmlns="urn:ietf:params:xml:ns:xmpp-bind" />
<session xmlns="urn:ietf:params:xml:ns:xmpp-session" /> <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> </stream:features>
""") """)
@ -99,6 +87,9 @@ class TestLiveStream(SleekTest):
suite = unittest.TestLoader().loadTestsFromTestCase(TestLiveStream) suite = unittest.TestLoader().loadTestsFromTestCase(TestLiveStream)
if __name__ == '__main__': if __name__ == '__main__':
logging.basicConfig(level=logging.DEBUG,
format='%(levelname)-8s %(message)s')
tests = unittest.TestSuite([suite]) tests = unittest.TestSuite([suite])
result = unittest.TextTestRunner(verbosity=2).run(tests) result = unittest.TextTestRunner(verbosity=2).run(tests)
test_ns = 'http://andyet.net/protocol/tests' test_ns = 'http://andyet.net/protocol/tests'