Bump to 1.3.1
[slixmpp.git] / sleekxmpp / test / sleektest.py
blobd28f77e290b8b458b3457ff6cc2f13988c54e071
1 """
2 SleekXMPP: The Sleek XMPP Library
3 Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
4 This file is part of SleekXMPP.
6 See the file LICENSE for copying permission.
7 """
9 import unittest
10 from xml.parsers.expat import ExpatError
12 from sleekxmpp import ClientXMPP, ComponentXMPP
13 from sleekxmpp.util import Queue
14 from sleekxmpp.stanza import Message, Iq, Presence
15 from sleekxmpp.test import TestSocket, TestLiveSocket
16 from sleekxmpp.xmlstream import ET
17 from sleekxmpp.xmlstream import ElementBase
18 from sleekxmpp.xmlstream.tostring import tostring
19 from sleekxmpp.xmlstream.matcher import StanzaPath, MatcherId, MatchIDSender
20 from sleekxmpp.xmlstream.matcher import MatchXMLMask, MatchXPath
23 class SleekTest(unittest.TestCase):
25 """
26 A SleekXMPP specific TestCase class that provides
27 methods for comparing message, iq, and presence stanzas.
29 Methods:
30 Message -- Create a Message stanza object.
31 Iq -- Create an Iq stanza object.
32 Presence -- Create a Presence stanza object.
33 check_jid -- Check a JID and its component parts.
34 check -- Compare a stanza against an XML string.
35 stream_start -- Initialize a dummy XMPP client.
36 stream_close -- Disconnect the XMPP client.
37 make_header -- Create a stream header.
38 send_header -- Check that the given header has been sent.
39 send_feature -- Send a raw XML element.
40 send -- Check that the XMPP client sent the given
41 generic stanza.
42 recv -- Queue data for XMPP client to receive, or
43 verify the data that was received from a
44 live connection.
45 recv_header -- Check that a given stream header
46 was received.
47 recv_feature -- Check that a given, raw XML element
48 was recveived.
49 fix_namespaces -- Add top-level namespace to an XML object.
50 compare -- Compare XML objects against each other.
51 """
53 def __init__(self, *args, **kwargs):
54 unittest.TestCase.__init__(self, *args, **kwargs)
55 self.xmpp = None
57 def parse_xml(self, xml_string):
58 try:
59 xml = ET.fromstring(xml_string)
60 return xml
61 except (SyntaxError, ExpatError) as e:
62 msg = e.msg if hasattr(e, 'msg') else e.message
63 if 'unbound' in msg:
64 known_prefixes = {
65 'stream': 'http://etherx.jabber.org/streams'}
67 prefix = xml_string.split('<')[1].split(':')[0]
68 if prefix in known_prefixes:
69 xml_string = '<fixns xmlns:%s="%s">%s</fixns>' % (
70 prefix,
71 known_prefixes[prefix],
72 xml_string)
73 xml = self.parse_xml(xml_string)
74 xml = list(xml)[0]
75 return xml
76 else:
77 self.fail("XML data was mal-formed:\n%s" % xml_string)
79 # ------------------------------------------------------------------
80 # Shortcut methods for creating stanza objects
82 def Message(self, *args, **kwargs):
83 """
84 Create a Message stanza.
86 Uses same arguments as StanzaBase.__init__
88 Arguments:
89 xml -- An XML object to use for the Message's values.
90 """
91 return Message(self.xmpp, *args, **kwargs)
93 def Iq(self, *args, **kwargs):
94 """
95 Create an Iq stanza.
97 Uses same arguments as StanzaBase.__init__
99 Arguments:
100 xml -- An XML object to use for the Iq's values.
102 return Iq(self.xmpp, *args, **kwargs)
104 def Presence(self, *args, **kwargs):
106 Create a Presence stanza.
108 Uses same arguments as StanzaBase.__init__
110 Arguments:
111 xml -- An XML object to use for the Iq's values.
113 return Presence(self.xmpp, *args, **kwargs)
115 def check_jid(self, jid, user=None, domain=None, resource=None,
116 bare=None, full=None, string=None):
118 Verify the components of a JID.
120 Arguments:
121 jid -- The JID object to test.
122 user -- Optional. The user name portion of the JID.
123 domain -- Optional. The domain name portion of the JID.
124 resource -- Optional. The resource portion of the JID.
125 bare -- Optional. The bare JID.
126 full -- Optional. The full JID.
127 string -- Optional. The string version of the JID.
129 if user is not None:
130 self.assertEqual(jid.user, user,
131 "User does not match: %s" % jid.user)
132 if domain is not None:
133 self.assertEqual(jid.domain, domain,
134 "Domain does not match: %s" % jid.domain)
135 if resource is not None:
136 self.assertEqual(jid.resource, resource,
137 "Resource does not match: %s" % jid.resource)
138 if bare is not None:
139 self.assertEqual(jid.bare, bare,
140 "Bare JID does not match: %s" % jid.bare)
141 if full is not None:
142 self.assertEqual(jid.full, full,
143 "Full JID does not match: %s" % jid.full)
144 if string is not None:
145 self.assertEqual(str(jid), string,
146 "String does not match: %s" % str(jid))
148 def check_roster(self, owner, jid, name=None, subscription=None,
149 afrom=None, ato=None, pending_out=None, pending_in=None,
150 groups=None):
151 roster = self.xmpp.roster[owner][jid]
152 if name is not None:
153 self.assertEqual(roster['name'], name,
154 "Incorrect name value: %s" % roster['name'])
155 if subscription is not None:
156 self.assertEqual(roster['subscription'], subscription,
157 "Incorrect subscription: %s" % roster['subscription'])
158 if afrom is not None:
159 self.assertEqual(roster['from'], afrom,
160 "Incorrect from state: %s" % roster['from'])
161 if ato is not None:
162 self.assertEqual(roster['to'], ato,
163 "Incorrect to state: %s" % roster['to'])
164 if pending_out is not None:
165 self.assertEqual(roster['pending_out'], pending_out,
166 "Incorrect pending_out state: %s" % roster['pending_out'])
167 if pending_in is not None:
168 self.assertEqual(roster['pending_in'], pending_out,
169 "Incorrect pending_in state: %s" % roster['pending_in'])
170 if groups is not None:
171 self.assertEqual(roster['groups'], groups,
172 "Incorrect groups: %s" % roster['groups'])
174 # ------------------------------------------------------------------
175 # Methods for comparing stanza objects to XML strings
177 def check(self, stanza, criteria, method='exact',
178 defaults=None, use_values=True):
180 Create and compare several stanza objects to a correct XML string.
182 If use_values is False, tests using stanza.values will not be used.
184 Some stanzas provide default values for some interfaces, but
185 these defaults can be problematic for testing since they can easily
186 be forgotten when supplying the XML string. A list of interfaces that
187 use defaults may be provided and the generated stanzas will use the
188 default values for those interfaces if needed.
190 However, correcting the supplied XML is not possible for interfaces
191 that add or remove XML elements. Only interfaces that map to XML
192 attributes may be set using the defaults parameter. The supplied XML
193 must take into account any extra elements that are included by default.
195 Arguments:
196 stanza -- The stanza object to test.
197 criteria -- An expression the stanza must match against.
198 method -- The type of matching to use; one of:
199 'exact', 'mask', 'id', 'xpath', and 'stanzapath'.
200 Defaults to the value of self.match_method.
201 defaults -- A list of stanza interfaces that have default
202 values. These interfaces will be set to their
203 defaults for the given and generated stanzas to
204 prevent unexpected test failures.
205 use_values -- Indicates if testing using stanza.values should
206 be used. Defaults to True.
208 if method is None and hasattr(self, 'match_method'):
209 method = getattr(self, 'match_method')
211 if method != 'exact':
212 matchers = {'stanzapath': StanzaPath,
213 'xpath': MatchXPath,
214 'mask': MatchXMLMask,
215 'idsender': MatchIDSender,
216 'id': MatcherId}
217 Matcher = matchers.get(method, None)
218 if Matcher is None:
219 raise ValueError("Unknown matching method.")
220 test = Matcher(criteria)
221 self.failUnless(test.match(stanza),
222 "Stanza did not match using %s method:\n" % method + \
223 "Criteria:\n%s\n" % str(criteria) + \
224 "Stanza:\n%s" % str(stanza))
225 else:
226 stanza_class = stanza.__class__
227 if not isinstance(criteria, ElementBase):
228 xml = self.parse_xml(criteria)
229 else:
230 xml = criteria.xml
232 # Ensure that top level namespaces are used, even if they
233 # were not provided.
234 self.fix_namespaces(stanza.xml, 'jabber:client')
235 self.fix_namespaces(xml, 'jabber:client')
237 stanza2 = stanza_class(xml=xml)
239 if use_values:
240 # Using stanza.values will add XML for any interface that
241 # has a default value. We need to set those defaults on
242 # the existing stanzas and XML so that they will compare
243 # correctly.
244 default_stanza = stanza_class()
245 if defaults is None:
246 known_defaults = {
247 Message: ['type'],
248 Presence: ['priority']
250 defaults = known_defaults.get(stanza_class, [])
251 for interface in defaults:
252 stanza[interface] = stanza[interface]
253 stanza2[interface] = stanza2[interface]
254 # Can really only automatically add defaults for top
255 # level attribute values. Anything else must be accounted
256 # for in the provided XML string.
257 if interface not in xml.attrib:
258 if interface in default_stanza.xml.attrib:
259 value = default_stanza.xml.attrib[interface]
260 xml.attrib[interface] = value
262 values = stanza2.values
263 stanza3 = stanza_class()
264 stanza3.values = values
266 debug = "Three methods for creating stanzas do not match.\n"
267 debug += "Given XML:\n%s\n" % tostring(xml)
268 debug += "Given stanza:\n%s\n" % tostring(stanza.xml)
269 debug += "Generated stanza:\n%s\n" % tostring(stanza2.xml)
270 debug += "Second generated stanza:\n%s\n" % tostring(stanza3.xml)
271 result = self.compare(xml, stanza.xml, stanza2.xml, stanza3.xml)
272 else:
273 debug = "Two methods for creating stanzas do not match.\n"
274 debug += "Given XML:\n%s\n" % tostring(xml)
275 debug += "Given stanza:\n%s\n" % tostring(stanza.xml)
276 debug += "Generated stanza:\n%s\n" % tostring(stanza2.xml)
277 result = self.compare(xml, stanza.xml, stanza2.xml)
279 self.failUnless(result, debug)
281 # ------------------------------------------------------------------
282 # Methods for simulating stanza streams.
284 def stream_disconnect(self):
286 Simulate a stream disconnection.
288 if self.xmpp:
289 self.xmpp.socket.disconnect_error()
291 def stream_start(self, mode='client', skip=True, header=None,
292 socket='mock', jid='tester@localhost',
293 password='test', server='localhost',
294 port=5222, sasl_mech=None,
295 plugins=None, plugin_config={}):
297 Initialize an XMPP client or component using a dummy XML stream.
299 Arguments:
300 mode -- Either 'client' or 'component'. Defaults to 'client'.
301 skip -- Indicates if the first item in the sent queue (the
302 stream header) should be removed. Tests that wish
303 to test initializing the stream should set this to
304 False. Otherwise, the default of True should be used.
305 socket -- Either 'mock' or 'live' to indicate if the socket
306 should be a dummy, mock socket or a live, functioning
307 socket. Defaults to 'mock'.
308 jid -- The JID to use for the connection.
309 Defaults to 'tester@localhost'.
310 password -- The password to use for the connection.
311 Defaults to 'test'.
312 server -- The name of the XMPP server. Defaults to 'localhost'.
313 port -- The port to use when connecting to the server.
314 Defaults to 5222.
315 plugins -- List of plugins to register. By default, all plugins
316 are loaded.
318 if mode == 'client':
319 self.xmpp = ClientXMPP(jid, password,
320 sasl_mech=sasl_mech,
321 plugin_config=plugin_config)
322 elif mode == 'component':
323 self.xmpp = ComponentXMPP(jid, password,
324 server, port,
325 plugin_config=plugin_config)
326 else:
327 raise ValueError("Unknown XMPP connection mode.")
329 # Remove unique ID prefix to make it easier to test
330 self.xmpp._id_prefix = ''
331 self.xmpp._disconnect_wait_for_threads = False
332 self.xmpp.default_lang = None
333 self.xmpp.peer_default_lang = None
335 # We will use this to wait for the session_start event
336 # for live connections.
337 skip_queue = Queue()
339 if socket == 'mock':
340 self.xmpp.set_socket(TestSocket())
342 # Simulate connecting for mock sockets.
343 self.xmpp.auto_reconnect = False
344 self.xmpp.state._set_state('connected')
346 # Must have the stream header ready for xmpp.process() to work.
347 if not header:
348 header = self.xmpp.stream_header
349 self.xmpp.socket.recv_data(header)
350 elif socket == 'live':
351 self.xmpp.socket_class = TestLiveSocket
353 def wait_for_session(x):
354 self.xmpp.socket.clear()
355 skip_queue.put('started')
357 self.xmpp.add_event_handler('session_start', wait_for_session)
358 if server is not None:
359 self.xmpp.connect((server, port))
360 else:
361 self.xmpp.connect()
362 else:
363 raise ValueError("Unknown socket type.")
365 if plugins is None:
366 self.xmpp.register_plugins()
367 else:
368 for plugin in plugins:
369 self.xmpp.register_plugin(plugin)
371 # Some plugins require messages to have ID values. Set
372 # this to True in tests related to those plugins.
373 self.xmpp.use_message_ids = False
375 self.xmpp.process(threaded=True)
376 if skip:
377 if socket != 'live':
378 # Mark send queue as usable
379 self.xmpp.session_bind_event.set()
380 self.xmpp.session_started_event.set()
381 # Clear startup stanzas
382 self.xmpp.socket.next_sent(timeout=1)
383 if mode == 'component':
384 self.xmpp.socket.next_sent(timeout=1)
385 else:
386 skip_queue.get(block=True, timeout=10)
388 def make_header(self, sto='',
389 sfrom='',
390 sid='',
391 stream_ns="http://etherx.jabber.org/streams",
392 default_ns="jabber:client",
393 default_lang="en",
394 version="1.0",
395 xml_header=True):
397 Create a stream header to be received by the test XMPP agent.
399 The header must be saved and passed to stream_start.
401 Arguments:
402 sto -- The recipient of the stream header.
403 sfrom -- The agent sending the stream header.
404 sid -- The stream's id.
405 stream_ns -- The namespace of the stream's root element.
406 default_ns -- The default stanza namespace.
407 version -- The stream version.
408 xml_header -- Indicates if the XML version header should be
409 appended before the stream header.
411 header = '<stream:stream %s>'
412 parts = []
413 if xml_header:
414 header = '<?xml version="1.0"?>' + header
415 if sto:
416 parts.append('to="%s"' % sto)
417 if sfrom:
418 parts.append('from="%s"' % sfrom)
419 if sid:
420 parts.append('id="%s"' % sid)
421 if default_lang:
422 parts.append('xml:lang="%s"' % default_lang)
423 parts.append('version="%s"' % version)
424 parts.append('xmlns:stream="%s"' % stream_ns)
425 parts.append('xmlns="%s"' % default_ns)
426 return header % ' '.join(parts)
428 def recv(self, data, defaults=[], method='exact',
429 use_values=True, timeout=1):
431 Pass data to the dummy XMPP client as if it came from an XMPP server.
433 If using a live connection, verify what the server has sent.
435 Arguments:
436 data -- If a dummy socket is being used, the XML that is to
437 be received next. Otherwise it is the criteria used
438 to match against live data that is received.
439 defaults -- A list of stanza interfaces with default values that
440 may interfere with comparisons.
441 method -- Select the type of comparison to use for
442 verifying the received stanza. Options are 'exact',
443 'id', 'stanzapath', 'xpath', and 'mask'.
444 Defaults to the value of self.match_method.
445 use_values -- Indicates if stanza comparisons should test using
446 stanza.values. Defaults to True.
447 timeout -- Time to wait in seconds for data to be received by
448 a live connection.
450 if self.xmpp.socket.is_live:
451 # we are working with a live connection, so we should
452 # verify what has been received instead of simulating
453 # receiving data.
454 recv_data = self.xmpp.socket.next_recv(timeout)
455 if recv_data is None:
456 self.fail("No stanza was received.")
457 xml = self.parse_xml(recv_data)
458 self.fix_namespaces(xml, 'jabber:client')
459 stanza = self.xmpp._build_stanza(xml, 'jabber:client')
460 self.check(stanza, data,
461 method=method,
462 defaults=defaults,
463 use_values=use_values)
464 else:
465 # place the data in the dummy socket receiving queue.
466 data = str(data)
467 self.xmpp.socket.recv_data(data)
469 def recv_header(self, sto='',
470 sfrom='',
471 sid='',
472 stream_ns="http://etherx.jabber.org/streams",
473 default_ns="jabber:client",
474 version="1.0",
475 xml_header=False,
476 timeout=1):
478 Check that a given stream header was received.
480 Arguments:
481 sto -- The recipient of the stream header.
482 sfrom -- The agent sending the stream header.
483 sid -- The stream's id. Set to None to ignore.
484 stream_ns -- The namespace of the stream's root element.
485 default_ns -- The default stanza namespace.
486 version -- The stream version.
487 xml_header -- Indicates if the XML version header should be
488 appended before the stream header.
489 timeout -- Length of time to wait in seconds for a
490 response.
492 header = self.make_header(sto, sfrom, sid,
493 stream_ns=stream_ns,
494 default_ns=default_ns,
495 version=version,
496 xml_header=xml_header)
497 recv_header = self.xmpp.socket.next_recv(timeout)
498 if recv_header is None:
499 raise ValueError("Socket did not return data.")
501 # Apply closing elements so that we can construct
502 # XML objects for comparison.
503 header2 = header + '</stream:stream>'
504 recv_header2 = recv_header + '</stream:stream>'
506 xml = self.parse_xml(header2)
507 recv_xml = self.parse_xml(recv_header2)
509 if sid is None:
510 # Ignore the id sent by the server since
511 # we can't know in advance what it will be.
512 if 'id' in recv_xml.attrib:
513 del recv_xml.attrib['id']
515 # Ignore the xml:lang attribute for now.
516 if 'xml:lang' in recv_xml.attrib:
517 del recv_xml.attrib['xml:lang']
518 xml_ns = 'http://www.w3.org/XML/1998/namespace'
519 if '{%s}lang' % xml_ns in recv_xml.attrib:
520 del recv_xml.attrib['{%s}lang' % xml_ns]
522 if list(recv_xml):
523 # We received more than just the header
524 for xml in recv_xml:
525 self.xmpp.socket.recv_data(tostring(xml))
527 attrib = recv_xml.attrib
528 recv_xml.clear()
529 recv_xml.attrib = attrib
531 self.failUnless(
532 self.compare(xml, recv_xml),
533 "Stream headers do not match:\nDesired:\n%s\nReceived:\n%s" % (
534 '%s %s' % (xml.tag, xml.attrib),
535 '%s %s' % (recv_xml.tag, recv_xml.attrib)))
537 def recv_feature(self, data, method='mask', use_values=True, timeout=1):
540 if method is None and hasattr(self, 'match_method'):
541 method = getattr(self, 'match_method')
543 if self.xmpp.socket.is_live:
544 # we are working with a live connection, so we should
545 # verify what has been received instead of simulating
546 # receiving data.
547 recv_data = self.xmpp.socket.next_recv(timeout)
548 xml = self.parse_xml(data)
549 recv_xml = self.parse_xml(recv_data)
550 if recv_data is None:
551 self.fail("No stanza was received.")
552 if method == 'exact':
553 self.failUnless(self.compare(xml, recv_xml),
554 "Features do not match.\nDesired:\n%s\nReceived:\n%s" % (
555 tostring(xml), tostring(recv_xml)))
556 elif method == 'mask':
557 matcher = MatchXMLMask(xml)
558 self.failUnless(matcher.match(recv_xml),
559 "Stanza did not match using %s method:\n" % method + \
560 "Criteria:\n%s\n" % tostring(xml) + \
561 "Stanza:\n%s" % tostring(recv_xml))
562 else:
563 raise ValueError("Uknown matching method: %s" % method)
564 else:
565 # place the data in the dummy socket receiving queue.
566 data = str(data)
567 self.xmpp.socket.recv_data(data)
569 def send_header(self, sto='',
570 sfrom='',
571 sid='',
572 stream_ns="http://etherx.jabber.org/streams",
573 default_ns="jabber:client",
574 default_lang="en",
575 version="1.0",
576 xml_header=False,
577 timeout=1):
579 Check that a given stream header was sent.
581 Arguments:
582 sto -- The recipient of the stream header.
583 sfrom -- The agent sending the stream header.
584 sid -- The stream's id.
585 stream_ns -- The namespace of the stream's root element.
586 default_ns -- The default stanza namespace.
587 version -- The stream version.
588 xml_header -- Indicates if the XML version header should be
589 appended before the stream header.
590 timeout -- Length of time to wait in seconds for a
591 response.
593 header = self.make_header(sto, sfrom, sid,
594 stream_ns=stream_ns,
595 default_ns=default_ns,
596 default_lang=default_lang,
597 version=version,
598 xml_header=xml_header)
599 sent_header = self.xmpp.socket.next_sent(timeout)
600 if sent_header is None:
601 raise ValueError("Socket did not return data.")
603 # Apply closing elements so that we can construct
604 # XML objects for comparison.
605 header2 = header + '</stream:stream>'
606 sent_header2 = sent_header + b'</stream:stream>'
608 xml = self.parse_xml(header2)
609 sent_xml = self.parse_xml(sent_header2)
611 self.failUnless(
612 self.compare(xml, sent_xml),
613 "Stream headers do not match:\nDesired:\n%s\nSent:\n%s" % (
614 header, sent_header))
616 def send_feature(self, data, method='mask', use_values=True, timeout=1):
619 sent_data = self.xmpp.socket.next_sent(timeout)
620 xml = self.parse_xml(data)
621 sent_xml = self.parse_xml(sent_data)
622 if sent_data is None:
623 self.fail("No stanza was sent.")
624 if method == 'exact':
625 self.failUnless(self.compare(xml, sent_xml),
626 "Features do not match.\nDesired:\n%s\nReceived:\n%s" % (
627 tostring(xml), tostring(sent_xml)))
628 elif method == 'mask':
629 matcher = MatchXMLMask(xml)
630 self.failUnless(matcher.match(sent_xml),
631 "Stanza did not match using %s method:\n" % method + \
632 "Criteria:\n%s\n" % tostring(xml) + \
633 "Stanza:\n%s" % tostring(sent_xml))
634 else:
635 raise ValueError("Uknown matching method: %s" % method)
637 def send(self, data, defaults=None, use_values=True,
638 timeout=.5, method='exact'):
640 Check that the XMPP client sent the given stanza XML.
642 Extracts the next sent stanza and compares it with the given
643 XML using check.
645 Arguments:
646 stanza_class -- The class of the sent stanza object.
647 data -- The XML string of the expected Message stanza,
648 or an equivalent stanza object.
649 use_values -- Modifies the type of tests used by check_message.
650 defaults -- A list of stanza interfaces that have defaults
651 values which may interfere with comparisons.
652 timeout -- Time in seconds to wait for a stanza before
653 failing the check.
654 method -- Select the type of comparison to use for
655 verifying the sent stanza. Options are 'exact',
656 'id', 'stanzapath', 'xpath', and 'mask'.
657 Defaults to the value of self.match_method.
659 sent = self.xmpp.socket.next_sent(timeout)
660 if data is None and sent is None:
661 return
662 if data is None and sent is not None:
663 self.fail("Stanza data was sent: %s" % sent)
664 if sent is None:
665 self.fail("No stanza was sent.")
667 xml = self.parse_xml(sent)
668 self.fix_namespaces(xml, 'jabber:client')
669 sent = self.xmpp._build_stanza(xml, 'jabber:client')
670 self.check(sent, data,
671 method=method,
672 defaults=defaults,
673 use_values=use_values)
675 def stream_close(self):
677 Disconnect the dummy XMPP client.
679 Can be safely called even if stream_start has not been called.
681 Must be placed in the tearDown method of a test class to ensure
682 that the XMPP client is disconnected after an error.
684 if hasattr(self, 'xmpp') and self.xmpp is not None:
685 self.xmpp.socket.recv_data(self.xmpp.stream_footer)
686 self.xmpp.disconnect()
688 # ------------------------------------------------------------------
689 # XML Comparison and Cleanup
691 def fix_namespaces(self, xml, ns):
693 Assign a namespace to an element and any children that
694 don't have a namespace.
696 Arguments:
697 xml -- The XML object to fix.
698 ns -- The namespace to add to the XML object.
700 if xml.tag.startswith('{'):
701 return
702 xml.tag = '{%s}%s' % (ns, xml.tag)
703 for child in xml:
704 self.fix_namespaces(child, ns)
706 def compare(self, xml, *other):
708 Compare XML objects.
710 Arguments:
711 xml -- The XML object to compare against.
712 *other -- The list of XML objects to compare.
714 if not other:
715 return False
717 # Compare multiple objects
718 if len(other) > 1:
719 for xml2 in other:
720 if not self.compare(xml, xml2):
721 return False
722 return True
724 other = other[0]
726 # Step 1: Check tags
727 if xml.tag != other.tag:
728 return False
730 # Step 2: Check attributes
731 if xml.attrib != other.attrib:
732 return False
734 # Step 3: Check text
735 if xml.text is None:
736 xml.text = ""
737 if other.text is None:
738 other.text = ""
739 xml.text = xml.text.strip()
740 other.text = other.text.strip()
742 if xml.text != other.text:
743 return False
745 # Step 4: Check children count
746 if len(list(xml)) != len(list(other)):
747 return False
749 # Step 5: Recursively check children
750 for child in xml:
751 child2s = other.findall("%s" % child.tag)
752 if child2s is None:
753 return False
754 for child2 in child2s:
755 if self.compare(child, child2):
756 break
757 else:
758 return False
760 # Step 6: Recursively check children the other way.
761 for child in other:
762 child2s = xml.findall("%s" % child.tag)
763 if child2s is None:
764 return False
765 for child2 in child2s:
766 if self.compare(child, child2):
767 break
768 else:
769 return False
771 # Everything matches
772 return True