blob: f4b43fede83ee74d939c2570da6746f297c463d8 [file] [log] [blame]
Lars Gustäbel96753b32000-09-24 12:24:24 +00001# regression test for SAX 2.0
2# $Id$
3
Martin v. Löwis80670bc2000-10-06 21:13:23 +00004from xml.sax import make_parser, ContentHandler, \
5 SAXException, SAXReaderNotAvailable, SAXParseException
Martin v. Löwis962c9e72000-10-06 17:41:52 +00006try:
7 make_parser()
Martin v. Löwis80670bc2000-10-06 21:13:23 +00008except SAXReaderNotAvailable:
Martin v. Löwis962c9e72000-10-06 17:41:52 +00009 # don't try to test this module if we cannot create a parser
10 raise ImportError("no XML parsers available")
Lars Gustäbel96753b32000-09-24 12:24:24 +000011from xml.sax.saxutils import XMLGenerator, escape, XMLFilterBase
12from xml.sax.expatreader import create_parser
Lars Gustäbelb7536d52000-09-24 18:53:56 +000013from xml.sax.xmlreader import InputSource, AttributesImpl, AttributesNSImpl
Lars Gustäbel96753b32000-09-24 12:24:24 +000014from cStringIO import StringIO
Marc-André Lemburg36619082001-01-17 19:11:13 +000015from test_support import verify, verbose, TestFailed, findfile
Lars Gustäbel96753b32000-09-24 12:24:24 +000016
17# ===== Utilities
18
19tests = 0
20fails = 0
21
22def confirm(outcome, name):
23 global tests, fails
24
25 tests = tests + 1
26 if outcome:
27 print "Passed", name
28 else:
29 print "Failed", name
30 fails = fails + 1
31
Lars Gustäbel2fc52942000-10-24 15:35:07 +000032def test_make_parser2():
Tim Petersd2bf3b72001-01-18 02:22:22 +000033 try:
Lars Gustäbel2fc52942000-10-24 15:35:07 +000034 # Creating parsers several times in a row should succeed.
35 # Testing this because there have been failures of this kind
36 # before.
37 from xml.sax import make_parser
38 p = make_parser()
39 from xml.sax import make_parser
40 p = make_parser()
41 from xml.sax import make_parser
42 p = make_parser()
43 from xml.sax import make_parser
44 p = make_parser()
45 from xml.sax import make_parser
46 p = make_parser()
47 from xml.sax import make_parser
48 p = make_parser()
49 except:
50 return 0
51 else:
52 return p
Tim Petersd2bf3b72001-01-18 02:22:22 +000053
54
Lars Gustäbel96753b32000-09-24 12:24:24 +000055# ===========================================================================
56#
57# saxutils tests
58#
59# ===========================================================================
60
61# ===== escape
62
63def test_escape_basic():
64 return escape("Donald Duck & Co") == "Donald Duck & Co"
65
66def test_escape_all():
67 return escape("<Donald Duck & Co>") == "&lt;Donald Duck &amp; Co&gt;"
68
69def test_escape_extra():
70 return escape("Hei på deg", {"å" : "&aring;"}) == "Hei p&aring; deg"
71
Martin v. Löwis962c9e72000-10-06 17:41:52 +000072def test_make_parser():
73 try:
74 # Creating a parser should succeed - it should fall back
75 # to the expatreader
76 p = make_parser(['xml.parsers.no_such_parser'])
77 except:
78 return 0
79 else:
80 return p
81
82
Lars Gustäbel96753b32000-09-24 12:24:24 +000083# ===== XMLGenerator
84
85start = '<?xml version="1.0" encoding="iso-8859-1"?>\n'
86
87def test_xmlgen_basic():
88 result = StringIO()
89 gen = XMLGenerator(result)
90 gen.startDocument()
91 gen.startElement("doc", {})
92 gen.endElement("doc")
93 gen.endDocument()
94
95 return result.getvalue() == start + "<doc></doc>"
96
97def test_xmlgen_content():
98 result = StringIO()
99 gen = XMLGenerator(result)
Fred Drake004d5e62000-10-23 17:22:08 +0000100
Lars Gustäbel96753b32000-09-24 12:24:24 +0000101 gen.startDocument()
102 gen.startElement("doc", {})
103 gen.characters("huhei")
104 gen.endElement("doc")
105 gen.endDocument()
106
107 return result.getvalue() == start + "<doc>huhei</doc>"
108
109def test_xmlgen_pi():
110 result = StringIO()
111 gen = XMLGenerator(result)
Fred Drake004d5e62000-10-23 17:22:08 +0000112
Lars Gustäbel96753b32000-09-24 12:24:24 +0000113 gen.startDocument()
114 gen.processingInstruction("test", "data")
115 gen.startElement("doc", {})
116 gen.endElement("doc")
117 gen.endDocument()
118
119 return result.getvalue() == start + "<?test data?><doc></doc>"
120
121def test_xmlgen_content_escape():
122 result = StringIO()
123 gen = XMLGenerator(result)
Fred Drake004d5e62000-10-23 17:22:08 +0000124
Lars Gustäbel96753b32000-09-24 12:24:24 +0000125 gen.startDocument()
126 gen.startElement("doc", {})
127 gen.characters("<huhei&")
128 gen.endElement("doc")
129 gen.endDocument()
130
131 return result.getvalue() == start + "<doc>&lt;huhei&amp;</doc>"
132
133def test_xmlgen_ignorable():
134 result = StringIO()
135 gen = XMLGenerator(result)
Fred Drake004d5e62000-10-23 17:22:08 +0000136
Lars Gustäbel96753b32000-09-24 12:24:24 +0000137 gen.startDocument()
138 gen.startElement("doc", {})
139 gen.ignorableWhitespace(" ")
140 gen.endElement("doc")
141 gen.endDocument()
142
143 return result.getvalue() == start + "<doc> </doc>"
144
145ns_uri = "http://www.python.org/xml-ns/saxtest/"
146
147def test_xmlgen_ns():
148 result = StringIO()
149 gen = XMLGenerator(result)
Fred Drake004d5e62000-10-23 17:22:08 +0000150
Lars Gustäbel96753b32000-09-24 12:24:24 +0000151 gen.startDocument()
152 gen.startPrefixMapping("ns1", ns_uri)
Lars Gustäbel6a7768a2000-09-27 08:12:17 +0000153 gen.startElementNS((ns_uri, "doc"), "ns1:doc", {})
Martin v. Löwiscf0a1cc2000-10-03 22:35:29 +0000154 # add an unqualified name
155 gen.startElementNS((None, "udoc"), None, {})
156 gen.endElementNS((None, "udoc"), None)
Lars Gustäbel6a7768a2000-09-27 08:12:17 +0000157 gen.endElementNS((ns_uri, "doc"), "ns1:doc")
Lars Gustäbel96753b32000-09-24 12:24:24 +0000158 gen.endPrefixMapping("ns1")
159 gen.endDocument()
160
Martin v. Löwiscf0a1cc2000-10-03 22:35:29 +0000161 return result.getvalue() == start + \
162 ('<ns1:doc xmlns:ns1="%s"><udoc></udoc></ns1:doc>' %
Lars Gustäbel96753b32000-09-24 12:24:24 +0000163 ns_uri)
164
165# ===== XMLFilterBase
166
167def test_filter_basic():
168 result = StringIO()
169 gen = XMLGenerator(result)
170 filter = XMLFilterBase()
171 filter.setContentHandler(gen)
Fred Drake004d5e62000-10-23 17:22:08 +0000172
Lars Gustäbel96753b32000-09-24 12:24:24 +0000173 filter.startDocument()
174 filter.startElement("doc", {})
175 filter.characters("content")
176 filter.ignorableWhitespace(" ")
177 filter.endElement("doc")
178 filter.endDocument()
179
180 return result.getvalue() == start + "<doc>content </doc>"
181
182# ===========================================================================
183#
184# expatreader tests
185#
186# ===========================================================================
187
Lars Gustäbel07025072000-10-24 16:00:22 +0000188# ===== XMLReader support
189
190def test_expat_file():
191 parser = create_parser()
192 result = StringIO()
193 xmlgen = XMLGenerator(result)
194
195 parser.setContentHandler(xmlgen)
196 parser.parse(open(findfile("test.xml")))
197
198 return result.getvalue() == xml_test_out
199
Lars Gustäbel96753b32000-09-24 12:24:24 +0000200# ===== DTDHandler support
201
202class TestDTDHandler:
203
204 def __init__(self):
205 self._notations = []
206 self._entities = []
Fred Drake004d5e62000-10-23 17:22:08 +0000207
Lars Gustäbel96753b32000-09-24 12:24:24 +0000208 def notationDecl(self, name, publicId, systemId):
209 self._notations.append((name, publicId, systemId))
210
211 def unparsedEntityDecl(self, name, publicId, systemId, ndata):
212 self._entities.append((name, publicId, systemId, ndata))
213
Lars Gustäbele292a242000-09-24 20:19:45 +0000214def test_expat_dtdhandler():
215 parser = create_parser()
216 handler = TestDTDHandler()
217 parser.setDTDHandler(handler)
Lars Gustäbel96753b32000-09-24 12:24:24 +0000218
Lars Gustäbele292a242000-09-24 20:19:45 +0000219 parser.feed('<!DOCTYPE doc [\n')
220 parser.feed(' <!ENTITY img SYSTEM "expat.gif" NDATA GIF>\n')
221 parser.feed(' <!NOTATION GIF PUBLIC "-//CompuServe//NOTATION Graphics Interchange Format 89a//EN">\n')
222 parser.feed(']>\n')
223 parser.feed('<doc></doc>')
224 parser.close()
Lars Gustäbel96753b32000-09-24 12:24:24 +0000225
Lars Gustäbele292a242000-09-24 20:19:45 +0000226 return handler._notations == [("GIF", "-//CompuServe//NOTATION Graphics Interchange Format 89a//EN", None)] and \
227 handler._entities == [("img", None, "expat.gif", "GIF")]
Lars Gustäbel96753b32000-09-24 12:24:24 +0000228
229# ===== EntityResolver support
230
Lars Gustäbele292a242000-09-24 20:19:45 +0000231class TestEntityResolver:
Lars Gustäbel96753b32000-09-24 12:24:24 +0000232
Lars Gustäbele292a242000-09-24 20:19:45 +0000233 def resolveEntity(self, publicId, systemId):
234 inpsrc = InputSource()
235 inpsrc.setByteStream(StringIO("<entity/>"))
236 return inpsrc
237
238def test_expat_entityresolver():
Lars Gustäbele292a242000-09-24 20:19:45 +0000239 parser = create_parser()
240 parser.setEntityResolver(TestEntityResolver())
241 result = StringIO()
242 parser.setContentHandler(XMLGenerator(result))
243
244 parser.feed('<!DOCTYPE doc [\n')
245 parser.feed(' <!ENTITY test SYSTEM "whatever">\n')
246 parser.feed(']>\n')
247 parser.feed('<doc>&test;</doc>')
248 parser.close()
249
250 return result.getvalue() == start + "<doc><entity></entity></doc>"
Fred Drake004d5e62000-10-23 17:22:08 +0000251
Lars Gustäbelab647872000-09-24 18:40:52 +0000252# ===== Attributes support
253
254class AttrGatherer(ContentHandler):
255
256 def startElement(self, name, attrs):
257 self._attrs = attrs
258
259 def startElementNS(self, name, qname, attrs):
260 self._attrs = attrs
Fred Drake004d5e62000-10-23 17:22:08 +0000261
Lars Gustäbelab647872000-09-24 18:40:52 +0000262def test_expat_attrs_empty():
263 parser = create_parser()
264 gather = AttrGatherer()
265 parser.setContentHandler(gather)
266
267 parser.feed("<doc/>")
268 parser.close()
269
270 return verify_empty_attrs(gather._attrs)
271
272def test_expat_attrs_wattr():
273 parser = create_parser()
274 gather = AttrGatherer()
275 parser.setContentHandler(gather)
276
277 parser.feed("<doc attr='val'/>")
278 parser.close()
279
280 return verify_attrs_wattr(gather._attrs)
281
282def test_expat_nsattrs_empty():
283 parser = create_parser(1)
284 gather = AttrGatherer()
285 parser.setContentHandler(gather)
286
287 parser.feed("<doc/>")
288 parser.close()
289
290 return verify_empty_nsattrs(gather._attrs)
291
292def test_expat_nsattrs_wattr():
293 parser = create_parser(1)
294 gather = AttrGatherer()
295 parser.setContentHandler(gather)
296
297 parser.feed("<doc xmlns:ns='%s' ns:attr='val'/>" % ns_uri)
298 parser.close()
299
300 attrs = gather._attrs
Fred Drake004d5e62000-10-23 17:22:08 +0000301
Lars Gustäbelab647872000-09-24 18:40:52 +0000302 return attrs.getLength() == 1 and \
303 attrs.getNames() == [(ns_uri, "attr")] and \
304 attrs.getQNames() == [] and \
305 len(attrs) == 1 and \
306 attrs.has_key((ns_uri, "attr")) and \
307 attrs.keys() == [(ns_uri, "attr")] and \
308 attrs.get((ns_uri, "attr")) == "val" and \
309 attrs.get((ns_uri, "attr"), 25) == "val" and \
310 attrs.items() == [((ns_uri, "attr"), "val")] and \
311 attrs.values() == ["val"] and \
312 attrs.getValue((ns_uri, "attr")) == "val" and \
313 attrs[(ns_uri, "attr")] == "val"
314
Lars Gustäbelb7536d52000-09-24 18:53:56 +0000315# ===== InputSource support
316
Martin v. Löwis33315b12000-09-24 20:30:24 +0000317xml_test_out = open(findfile("test.xml.out")).read()
Lars Gustäbelb7536d52000-09-24 18:53:56 +0000318
319def test_expat_inpsource_filename():
320 parser = create_parser()
321 result = StringIO()
322 xmlgen = XMLGenerator(result)
323
324 parser.setContentHandler(xmlgen)
Martin v. Löwis33315b12000-09-24 20:30:24 +0000325 parser.parse(findfile("test.xml"))
Lars Gustäbelb7536d52000-09-24 18:53:56 +0000326
327 return result.getvalue() == xml_test_out
328
329def test_expat_inpsource_sysid():
330 parser = create_parser()
331 result = StringIO()
332 xmlgen = XMLGenerator(result)
333
334 parser.setContentHandler(xmlgen)
Martin v. Löwis33315b12000-09-24 20:30:24 +0000335 parser.parse(InputSource(findfile("test.xml")))
Lars Gustäbelb7536d52000-09-24 18:53:56 +0000336
337 return result.getvalue() == xml_test_out
338
339def test_expat_inpsource_stream():
340 parser = create_parser()
341 result = StringIO()
342 xmlgen = XMLGenerator(result)
343
344 parser.setContentHandler(xmlgen)
345 inpsrc = InputSource()
Martin v. Löwis33315b12000-09-24 20:30:24 +0000346 inpsrc.setByteStream(open(findfile("test.xml")))
Lars Gustäbelb7536d52000-09-24 18:53:56 +0000347 parser.parse(inpsrc)
348
349 return result.getvalue() == xml_test_out
350
Lars Gustäbel2fc52942000-10-24 15:35:07 +0000351# ===== IncrementalParser support
352
353def test_expat_incremental():
354 result = StringIO()
355 xmlgen = XMLGenerator(result)
356 parser = create_parser()
357 parser.setContentHandler(xmlgen)
358
359 parser.feed("<doc>")
360 parser.feed("</doc>")
361 parser.close()
362
363 return result.getvalue() == start + "<doc></doc>"
364
365def test_expat_incremental_reset():
366 result = StringIO()
367 xmlgen = XMLGenerator(result)
368 parser = create_parser()
369 parser.setContentHandler(xmlgen)
370
371 parser.feed("<doc>")
372 parser.feed("text")
373
374 result = StringIO()
375 xmlgen = XMLGenerator(result)
376 parser.setContentHandler(xmlgen)
377 parser.reset()
378
379 parser.feed("<doc>")
380 parser.feed("text")
381 parser.feed("</doc>")
382 parser.close()
383
384 return result.getvalue() == start + "<doc>text</doc>"
385
386# ===== Locator support
387
388def test_expat_locator_noinfo():
389 result = StringIO()
390 xmlgen = XMLGenerator(result)
391 parser = create_parser()
392 parser.setContentHandler(xmlgen)
393
394 parser.feed("<doc>")
395 parser.feed("</doc>")
396 parser.close()
397
Fred Drake132dce22000-12-12 23:11:42 +0000398 return parser.getSystemId() is None and \
399 parser.getPublicId() is None and \
Tim Petersd2bf3b72001-01-18 02:22:22 +0000400 parser.getLineNumber() == 1
Lars Gustäbel2fc52942000-10-24 15:35:07 +0000401
402def test_expat_locator_withinfo():
403 result = StringIO()
404 xmlgen = XMLGenerator(result)
405 parser = create_parser()
406 parser.setContentHandler(xmlgen)
407 parser.parse(findfile("test.xml"))
408
409 return parser.getSystemId() == findfile("test.xml") and \
Fred Drake132dce22000-12-12 23:11:42 +0000410 parser.getPublicId() is None
Lars Gustäbel2fc52942000-10-24 15:35:07 +0000411
Martin v. Löwis80670bc2000-10-06 21:13:23 +0000412
413# ===========================================================================
414#
415# error reporting
416#
417# ===========================================================================
418
419def test_expat_inpsource_location():
420 parser = create_parser()
421 parser.setContentHandler(ContentHandler()) # do nothing
422 source = InputSource()
423 source.setByteStream(StringIO("<foo bar foobar>")) #ill-formed
424 name = "a file name"
425 source.setSystemId(name)
426 try:
427 parser.parse(source)
428 except SAXException, e:
429 return e.getSystemId() == name
430
431def test_expat_incomplete():
432 parser = create_parser()
433 parser.setContentHandler(ContentHandler()) # do nothing
434 try:
435 parser.parse(StringIO("<foo>"))
436 except SAXParseException:
437 return 1 # ok, error found
438 else:
439 return 0
440
441
Lars Gustäbelab647872000-09-24 18:40:52 +0000442# ===========================================================================
443#
444# xmlreader tests
445#
446# ===========================================================================
447
448# ===== AttributesImpl
449
450def verify_empty_attrs(attrs):
451 try:
452 attrs.getValue("attr")
453 gvk = 0
454 except KeyError:
455 gvk = 1
456
457 try:
458 attrs.getValueByQName("attr")
459 gvqk = 0
460 except KeyError:
461 gvqk = 1
462
463 try:
464 attrs.getNameByQName("attr")
465 gnqk = 0
466 except KeyError:
467 gnqk = 1
468
469 try:
470 attrs.getQNameByName("attr")
471 gqnk = 0
472 except KeyError:
473 gqnk = 1
Fred Drake004d5e62000-10-23 17:22:08 +0000474
Lars Gustäbelab647872000-09-24 18:40:52 +0000475 try:
476 attrs["attr"]
477 gik = 0
478 except KeyError:
479 gik = 1
Fred Drake004d5e62000-10-23 17:22:08 +0000480
Lars Gustäbelab647872000-09-24 18:40:52 +0000481 return attrs.getLength() == 0 and \
482 attrs.getNames() == [] and \
483 attrs.getQNames() == [] and \
484 len(attrs) == 0 and \
485 not attrs.has_key("attr") and \
486 attrs.keys() == [] and \
Fred Drake132dce22000-12-12 23:11:42 +0000487 attrs.get("attrs") is None and \
Lars Gustäbelab647872000-09-24 18:40:52 +0000488 attrs.get("attrs", 25) == 25 and \
489 attrs.items() == [] and \
490 attrs.values() == [] and \
491 gvk and gvqk and gnqk and gik and gqnk
492
493def verify_attrs_wattr(attrs):
494 return attrs.getLength() == 1 and \
495 attrs.getNames() == ["attr"] and \
496 attrs.getQNames() == ["attr"] and \
497 len(attrs) == 1 and \
498 attrs.has_key("attr") and \
499 attrs.keys() == ["attr"] and \
500 attrs.get("attr") == "val" and \
501 attrs.get("attr", 25) == "val" and \
502 attrs.items() == [("attr", "val")] and \
503 attrs.values() == ["val"] and \
504 attrs.getValue("attr") == "val" and \
505 attrs.getValueByQName("attr") == "val" and \
506 attrs.getNameByQName("attr") == "attr" and \
507 attrs["attr"] == "val" and \
508 attrs.getQNameByName("attr") == "attr"
509
510def test_attrs_empty():
511 return verify_empty_attrs(AttributesImpl({}))
512
513def test_attrs_wattr():
514 return verify_attrs_wattr(AttributesImpl({"attr" : "val"}))
515
516# ===== AttributesImpl
517
518def verify_empty_nsattrs(attrs):
519 try:
520 attrs.getValue((ns_uri, "attr"))
521 gvk = 0
522 except KeyError:
523 gvk = 1
524
525 try:
526 attrs.getValueByQName("ns:attr")
527 gvqk = 0
528 except KeyError:
529 gvqk = 1
530
531 try:
532 attrs.getNameByQName("ns:attr")
533 gnqk = 0
534 except KeyError:
535 gnqk = 1
536
537 try:
538 attrs.getQNameByName((ns_uri, "attr"))
539 gqnk = 0
540 except KeyError:
541 gqnk = 1
Fred Drake004d5e62000-10-23 17:22:08 +0000542
Lars Gustäbelab647872000-09-24 18:40:52 +0000543 try:
544 attrs[(ns_uri, "attr")]
545 gik = 0
546 except KeyError:
547 gik = 1
Fred Drake004d5e62000-10-23 17:22:08 +0000548
Lars Gustäbelab647872000-09-24 18:40:52 +0000549 return attrs.getLength() == 0 and \
550 attrs.getNames() == [] and \
551 attrs.getQNames() == [] and \
552 len(attrs) == 0 and \
553 not attrs.has_key((ns_uri, "attr")) and \
554 attrs.keys() == [] and \
Fred Drake132dce22000-12-12 23:11:42 +0000555 attrs.get((ns_uri, "attr")) is None and \
Lars Gustäbelab647872000-09-24 18:40:52 +0000556 attrs.get((ns_uri, "attr"), 25) == 25 and \
557 attrs.items() == [] and \
558 attrs.values() == [] and \
559 gvk and gvqk and gnqk and gik and gqnk
560
561def test_nsattrs_empty():
562 return verify_empty_nsattrs(AttributesNSImpl({}, {}))
563
564def test_nsattrs_wattr():
565 attrs = AttributesNSImpl({(ns_uri, "attr") : "val"},
566 {(ns_uri, "attr") : "ns:attr"})
Fred Drake004d5e62000-10-23 17:22:08 +0000567
Lars Gustäbelab647872000-09-24 18:40:52 +0000568 return attrs.getLength() == 1 and \
569 attrs.getNames() == [(ns_uri, "attr")] and \
570 attrs.getQNames() == ["ns:attr"] and \
571 len(attrs) == 1 and \
572 attrs.has_key((ns_uri, "attr")) and \
573 attrs.keys() == [(ns_uri, "attr")] and \
574 attrs.get((ns_uri, "attr")) == "val" and \
575 attrs.get((ns_uri, "attr"), 25) == "val" and \
576 attrs.items() == [((ns_uri, "attr"), "val")] and \
577 attrs.values() == ["val"] and \
578 attrs.getValue((ns_uri, "attr")) == "val" and \
579 attrs.getValueByQName("ns:attr") == "val" and \
580 attrs.getNameByQName("ns:attr") == (ns_uri, "attr") and \
581 attrs[(ns_uri, "attr")] == "val" and \
582 attrs.getQNameByName((ns_uri, "attr")) == "ns:attr"
Fred Drake004d5e62000-10-23 17:22:08 +0000583
Lars Gustäbelab647872000-09-24 18:40:52 +0000584
Lars Gustäbel96753b32000-09-24 12:24:24 +0000585# ===== Main program
586
Lars Gustäbelb7536d52000-09-24 18:53:56 +0000587def make_test_output():
588 parser = create_parser()
589 result = StringIO()
590 xmlgen = XMLGenerator(result)
591
592 parser.setContentHandler(xmlgen)
Martin v. Löwis33315b12000-09-24 20:30:24 +0000593 parser.parse(findfile("test.xml"))
Lars Gustäbelb7536d52000-09-24 18:53:56 +0000594
Martin v. Löwis33315b12000-09-24 20:30:24 +0000595 outf = open(findfile("test.xml.out"), "w")
Lars Gustäbelb7536d52000-09-24 18:53:56 +0000596 outf.write(result.getvalue())
597 outf.close()
598
Lars Gustäbel96753b32000-09-24 12:24:24 +0000599items = locals().items()
600items.sort()
601for (name, value) in items:
602 if name[ : 5] == "test_":
603 confirm(value(), name)
604
605print "%d tests, %d failures" % (tests, fails)
606if fails != 0:
607 raise TestFailed, "%d of %d tests failed" % (fails, tests)