Initial import to Tizen
[profile/ivi/python-twisted.git] / twisted / lore / test / test_lore.py
1 # Copyright (c) Twisted Matrix Laboratories.
2 # See LICENSE for details.
3
4 # ++ single anchor added to individual output file
5 # ++ two anchors added to individual output file
6 # ++ anchors added to individual output files
7 # ++ entry added to index
8 # ++ index entry pointing to correct file and anchor
9 # ++ multiple entries added to index
10 # ++ multiple index entries pointing to correct files and anchors
11 # __ all of above for files in deep directory structure
12 #
13 # ++ group index entries by indexed term
14 # ++ sort index entries by indexed term
15 # __ hierarchical index entries (e.g. language!programming)
16 #
17 # ++ add parameter for what the index filename should be
18 # ++ add (default) ability to NOT index (if index not specified)
19 #
20 # ++ put actual index filename into INDEX link (if any) in the template
21 # __ make index links RELATIVE!
22 # __ make index pay attention to the outputdir!
23 #
24 # __ make index look nice
25 #
26 # ++ add section numbers to headers in lore output
27 # ++ make text of index entry links be chapter numbers
28 # ++ make text of index entry links be section numbers
29 #
30 # __ put all of our test files someplace neat and tidy
31 #
32
33 import os, shutil, errno, time
34 from StringIO import StringIO
35 from xml.dom import minidom as dom
36
37 from twisted.trial import unittest
38 from twisted.python.filepath import FilePath
39
40 from twisted.lore import tree, process, indexer, numberer, htmlbook, default
41 from twisted.lore.default import factory
42 from twisted.lore.latex import LatexSpitter
43
44 from twisted.python.util import sibpath
45
46 from twisted.lore.scripts import lore
47
48 from twisted.web import domhelpers
49
50 def sp(originalFileName):
51     return sibpath(__file__, originalFileName)
52
53 options = {"template" : sp("template.tpl"), 'baseurl': '%s', 'ext': '.xhtml' }
54 d = options
55
56
57 class _XMLAssertionMixin:
58     """
59     Test mixin defining a method for comparing serialized XML documents.
60     """
61     def assertXMLEqual(self, first, second):
62         """
63         Verify that two strings represent the same XML document.
64         """
65         self.assertEqual(
66             dom.parseString(first).toxml(),
67             dom.parseString(second).toxml())
68
69
70 class TestFactory(unittest.TestCase, _XMLAssertionMixin):
71
72     file = sp('simple.html')
73     linkrel = ""
74
75     def assertEqualFiles1(self, exp, act):
76         if (exp == act): return True
77         fact = open(act)
78         self.assertEqualsFile(exp, fact.read())
79
80     def assertEqualFiles(self, exp, act):
81         if (exp == act): return True
82         fact = open(sp(act))
83         self.assertEqualsFile(exp, fact.read())
84
85     def assertEqualsFile(self, exp, act):
86         expected = open(sp(exp)).read()
87         self.assertEqual(expected, act)
88
89     def makeTemp(self, *filenames):
90         tmp = self.mktemp()
91         os.mkdir(tmp)
92         for filename in filenames:
93             tmpFile = os.path.join(tmp, filename)
94             shutil.copyfile(sp(filename), tmpFile)
95         return tmp
96
97 ########################################
98
99     def setUp(self):
100         indexer.reset()
101         numberer.reset()
102
103     def testProcessingFunctionFactory(self):
104         base = FilePath(self.mktemp())
105         base.makedirs()
106
107         simple = base.child('simple.html')
108         FilePath(__file__).sibling('simple.html').copyTo(simple)
109
110         htmlGenerator = factory.generate_html(options)
111         htmlGenerator(simple.path, self.linkrel)
112
113         self.assertXMLEqual(
114             """\
115 <?xml version="1.0" ?><!DOCTYPE html  PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN'  'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
116   <head><title>Twisted Documentation: My Test Lore Input</title></head>
117   <body bgcolor="white">
118     <h1 class="title">My Test Lore Input</h1>
119     <div class="content">
120 <span/>
121 <p>A Body.</p>
122 </div>
123     <a href="index.xhtml">Index</a>
124   </body>
125 </html>""",
126             simple.sibling('simple.xhtml').getContent())
127
128
129     def testProcessingFunctionFactoryWithFilenameGenerator(self):
130         base = FilePath(self.mktemp())
131         base.makedirs()
132
133         def filenameGenerator(originalFileName, outputExtension):
134             name = os.path.splitext(FilePath(originalFileName).basename())[0]
135             return base.child(name + outputExtension).path
136
137         htmlGenerator = factory.generate_html(options, filenameGenerator)
138         htmlGenerator(self.file, self.linkrel)
139         self.assertXMLEqual(
140             """\
141 <?xml version="1.0" ?><!DOCTYPE html  PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN'  'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
142   <head><title>Twisted Documentation: My Test Lore Input</title></head>
143   <body bgcolor="white">
144     <h1 class="title">My Test Lore Input</h1>
145     <div class="content">
146 <span/>
147 <p>A Body.</p>
148 </div>
149     <a href="index.xhtml">Index</a>
150   </body>
151 </html>""",
152             base.child("simple.xhtml").getContent())
153
154
155     def test_doFile(self):
156         base = FilePath(self.mktemp())
157         base.makedirs()
158
159         simple = base.child('simple.html')
160         FilePath(__file__).sibling('simple.html').copyTo(simple)
161
162         templ = dom.parse(open(d['template']))
163
164         tree.doFile(simple.path, self.linkrel, d['ext'], d['baseurl'], templ, d)
165         self.assertXMLEqual(
166             """\
167 <?xml version="1.0" ?><!DOCTYPE html  PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN'  'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
168   <head><title>Twisted Documentation: My Test Lore Input</title></head>
169   <body bgcolor="white">
170     <h1 class="title">My Test Lore Input</h1>
171     <div class="content">
172 <span/>
173 <p>A Body.</p>
174 </div>
175     <a href="index.xhtml">Index</a>
176   </body>
177 </html>""",
178             base.child("simple.xhtml").getContent())
179
180
181     def test_doFile_withFilenameGenerator(self):
182         base = FilePath(self.mktemp())
183         base.makedirs()
184
185         def filenameGenerator(originalFileName, outputExtension):
186             name = os.path.splitext(FilePath(originalFileName).basename())[0]
187             return base.child(name + outputExtension).path
188
189         templ = dom.parse(open(d['template']))
190         tree.doFile(self.file, self.linkrel, d['ext'], d['baseurl'], templ, d, filenameGenerator)
191
192         self.assertXMLEqual(
193             """\
194 <?xml version="1.0" ?><!DOCTYPE html  PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN'  'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
195   <head><title>Twisted Documentation: My Test Lore Input</title></head>
196   <body bgcolor="white">
197     <h1 class="title">My Test Lore Input</h1>
198     <div class="content">
199 <span/>
200 <p>A Body.</p>
201 </div>
202     <a href="index.xhtml">Index</a>
203   </body>
204 </html>""",
205             base.child("simple.xhtml").getContent())
206
207
208     def test_munge(self):
209         indexer.setIndexFilename("lore_index_file.html")
210         doc = dom.parse(open(self.file))
211         node = dom.parse(open(d['template']))
212         tree.munge(doc, node, self.linkrel,
213                    os.path.dirname(self.file),
214                    self.file,
215                    d['ext'], d['baseurl'], d)
216
217         self.assertXMLEqual(
218             """\
219 <?xml version="1.0" ?><!DOCTYPE html  PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN'  'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
220   <head><title>Twisted Documentation: My Test Lore Input</title></head>
221   <body bgcolor="white">
222     <h1 class="title">My Test Lore Input</h1>
223     <div class="content">
224 <span/>
225 <p>A Body.</p>
226 </div>
227     <a href="lore_index_file.html">Index</a>
228   </body>
229 </html>""",
230             node.toxml())
231
232
233     def test_mungeAuthors(self):
234         """
235         If there is a node with a I{class} attribute set to C{"authors"},
236         L{tree.munge} adds anchors as children to it, takeing the necessary
237         information from any I{link} nodes in the I{head} with their I{rel}
238         attribute set to C{"author"}.
239         """
240         document = dom.parseString(
241             """\
242 <html>
243   <head>
244     <title>munge authors</title>
245     <link rel="author" title="foo" href="bar"/>
246     <link rel="author" title="baz" href="quux"/>
247     <link rel="author" title="foobar" href="barbaz"/>
248   </head>
249   <body>
250     <h1>munge authors</h1>
251   </body>
252 </html>""")
253         template = dom.parseString(
254             """\
255 <html xmlns="http://www.w3.org/1999/xhtml" lang="en">
256   <head>
257     <title />
258   </head>
259
260   <body>
261     <div class="body" />
262     <div class="authors" />
263   </body>
264 </html>
265 """)
266         tree.munge(
267             document, template, self.linkrel, os.path.dirname(self.file),
268             self.file, d['ext'], d['baseurl'], d)
269
270         self.assertXMLEqual(
271             template.toxml(),
272             """\
273 <?xml version="1.0" ?><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
274   <head>
275     <title>munge authors</title>
276   <link href="bar" rel="author" title="foo"/><link href="quux" rel="author" title="baz"/><link href="barbaz" rel="author" title="foobar"/></head>
277
278   <body>
279     <div class="content">
280     <span/>
281   </div>
282     <div class="authors"><span><a href="bar">foo</a>, <a href="quux">baz</a>, and <a href="barbaz">foobar</a></span></div>
283   </body>
284 </html>""")
285
286
287     def test_getProcessor(self):
288
289         base = FilePath(self.mktemp())
290         base.makedirs()
291         input = base.child("simple3.html")
292         FilePath(__file__).sibling("simple3.html").copyTo(input)
293
294         options = { 'template': sp('template.tpl'), 'ext': '.xhtml', 'baseurl': 'burl',
295                     'filenameMapping': None }
296         p = process.getProcessor(default, "html", options)
297         p(input.path, self.linkrel)
298         self.assertXMLEqual(
299             """\
300 <?xml version="1.0" ?><!DOCTYPE html  PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN'  'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
301   <head><title>Twisted Documentation: My Test Lore Input</title></head>
302   <body bgcolor="white">
303     <h1 class="title">My Test Lore Input</h1>
304     <div class="content">
305 <span/>
306 <p>A Body.</p>
307 </div>
308     <a href="index.xhtml">Index</a>
309   </body>
310 </html>""",
311             base.child("simple3.xhtml").getContent())
312
313     def test_outputdirGenerator(self):
314         normp = os.path.normpath; join = os.path.join
315         inputdir  = normp(join("/", 'home', 'joe'))
316         outputdir = normp(join("/", 'away', 'joseph'))
317         actual = process.outputdirGenerator(join("/", 'home', 'joe', "myfile.html"),
318                                             '.xhtml', inputdir, outputdir)
319         expected = normp(join("/", 'away', 'joseph', 'myfile.xhtml'))
320         self.assertEqual(expected, actual)
321
322     def test_outputdirGeneratorBadInput(self):
323         options = {'outputdir': '/away/joseph/', 'inputdir': '/home/joe/' }
324         self.assertRaises(ValueError, process.outputdirGenerator, '.html', '.xhtml', **options)
325
326     def test_makeSureDirectoryExists(self):
327         dirname = os.path.join("tmp", 'nonexistentdir')
328         if os.path.exists(dirname):
329             os.rmdir(dirname)
330         self.failIf(os.path.exists(dirname), "Hey: someone already created the dir")
331         filename = os.path.join(dirname, 'newfile')
332         tree.makeSureDirectoryExists(filename)
333         self.failUnless(os.path.exists(dirname), 'should have created dir')
334         os.rmdir(dirname)
335
336
337     def test_indexAnchorsAdded(self):
338         indexer.setIndexFilename('theIndexFile.html')
339         # generate the output file
340         templ = dom.parse(open(d['template']))
341         tmp = self.makeTemp('lore_index_test.xhtml')
342
343         tree.doFile(os.path.join(tmp, 'lore_index_test.xhtml'),
344                     self.linkrel, '.html', d['baseurl'], templ, d)
345
346         self.assertXMLEqual(
347             """\
348 <?xml version="1.0" ?><!DOCTYPE html  PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN'  'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
349   <head><title>Twisted Documentation: The way of the program</title></head>
350   <body bgcolor="white">
351     <h1 class="title">The way of the program</h1>
352     <div class="content">
353
354 <span/>
355
356 <p>The first paragraph.</p>
357
358
359 <h2>The Python programming language<a name="auto0"/></h2>
360 <a name="index01"/>
361 <a name="index02"/>
362
363 <p>The second paragraph.</p>
364
365
366 </div>
367     <a href="theIndexFile.html">Index</a>
368   </body>
369 </html>""",
370             FilePath(tmp).child("lore_index_test.html").getContent())
371
372
373     def test_indexEntriesAdded(self):
374         indexer.addEntry('lore_index_test.html', 'index02', 'language of programming', '1.3')
375         indexer.addEntry('lore_index_test.html', 'index01', 'programming language', '1.2')
376         indexer.setIndexFilename("lore_index_file.html")
377         indexer.generateIndex()
378         self.assertEqualFiles1("lore_index_file_out.html", "lore_index_file.html")
379
380     def test_book(self):
381         tmp = self.makeTemp()
382         inputFilename = sp('lore_index_test.xhtml')
383
384         bookFilename = os.path.join(tmp, 'lore_test_book.book')
385         bf = open(bookFilename, 'w')
386         bf.write('Chapter(r"%s", None)\r\n' % inputFilename)
387         bf.close()
388
389         book = htmlbook.Book(bookFilename)
390         expected = {'indexFilename': None,
391                     'chapters': [(inputFilename, None)],
392                     }
393         dct = book.__dict__
394         for k in dct:
395             self.assertEqual(dct[k], expected[k])
396
397     def test_runningLore(self):
398         options = lore.Options()
399         tmp = self.makeTemp('lore_index_test.xhtml')
400
401         templateFilename = sp('template.tpl')
402         inputFilename = os.path.join(tmp, 'lore_index_test.xhtml')
403         indexFilename = 'theIndexFile'
404
405         bookFilename = os.path.join(tmp, 'lore_test_book.book')
406         bf = open(bookFilename, 'w')
407         bf.write('Chapter(r"%s", None)\n' % inputFilename)
408         bf.close()
409
410         options.parseOptions(['--null', '--book=%s' % bookFilename,
411                               '--config', 'template=%s' % templateFilename,
412                               '--index=%s' % indexFilename
413                               ])
414         result = lore.runGivenOptions(options)
415         self.assertEqual(None, result)
416         self.assertEqualFiles1("lore_index_file_unnumbered_out.html", indexFilename + ".html")
417
418
419     def test_runningLoreMultipleFiles(self):
420         tmp = self.makeTemp('lore_index_test.xhtml', 'lore_index_test2.xhtml')
421         templateFilename = sp('template.tpl')
422         inputFilename = os.path.join(tmp, 'lore_index_test.xhtml')
423         inputFilename2 = os.path.join(tmp, 'lore_index_test2.xhtml')
424         indexFilename = 'theIndexFile'
425
426         bookFilename = os.path.join(tmp, 'lore_test_book.book')
427         bf = open(bookFilename, 'w')
428         bf.write('Chapter(r"%s", None)\n' % inputFilename)
429         bf.write('Chapter(r"%s", None)\n' % inputFilename2)
430         bf.close()
431
432         options = lore.Options()
433         options.parseOptions(['--null', '--book=%s' % bookFilename,
434                               '--config', 'template=%s' % templateFilename,
435                               '--index=%s' % indexFilename
436                               ])
437         result = lore.runGivenOptions(options)
438         self.assertEqual(None, result)
439
440         self.assertEqual(
441             # XXX This doesn't seem like a very good index file.
442             """\
443 aahz: <a href="lore_index_test2.html#index03">link</a><br />
444 aahz2: <a href="lore_index_test2.html#index02">link</a><br />
445 language of programming: <a href="lore_index_test.html#index02">link</a>, <a href="lore_index_test2.html#index01">link</a><br />
446 programming language: <a href="lore_index_test.html#index01">link</a><br />
447 """,
448             file(FilePath(indexFilename + ".html").path).read())
449
450         self.assertXMLEqual(
451             """\
452 <?xml version="1.0" ?><!DOCTYPE html  PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN'  'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
453   <head><title>Twisted Documentation: The way of the program</title></head>
454   <body bgcolor="white">
455     <h1 class="title">The way of the program</h1>
456     <div class="content">
457
458 <span/>
459
460 <p>The first paragraph.</p>
461
462
463 <h2>The Python programming language<a name="auto0"/></h2>
464 <a name="index01"/>
465 <a name="index02"/>
466
467 <p>The second paragraph.</p>
468
469
470 </div>
471     <a href="theIndexFile.html">Index</a>
472   </body>
473 </html>""",
474             FilePath(tmp).child("lore_index_test.html").getContent())
475
476         self.assertXMLEqual(
477             """\
478 <?xml version="1.0" ?><!DOCTYPE html  PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN'  'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
479   <head><title>Twisted Documentation: The second page to index</title></head>
480   <body bgcolor="white">
481     <h1 class="title">The second page to index</h1>
482     <div class="content">
483
484 <span/>
485
486 <p>The first paragraph of the second page.</p>
487
488
489 <h2>The Jython programming language<a name="auto0"/></h2>
490 <a name="index01"/>
491 <a name="index02"/>
492 <a name="index03"/>
493
494 <p>The second paragraph of the second page.</p>
495
496
497 </div>
498     <a href="theIndexFile.html">Index</a>
499   </body>
500 </html>""",
501             FilePath(tmp).child("lore_index_test2.html").getContent())
502
503
504
505     def XXXtest_NumberedSections(self):
506         # run two files through lore, with numbering turned on
507         # every h2 should be numbered:
508         # first  file's h2s should be 1.1, 1.2
509         # second file's h2s should be 2.1, 2.2
510         templateFilename = sp('template.tpl')
511         inputFilename = sp('lore_numbering_test.xhtml')
512         inputFilename2 = sp('lore_numbering_test2.xhtml')
513         indexFilename = 'theIndexFile'
514
515         # you can number without a book:
516         options = lore.Options()
517         options.parseOptions(['--null',
518                               '--index=%s' % indexFilename,
519                               '--config', 'template=%s' % templateFilename,
520                               '--config', 'ext=%s' % ".tns",
521                               '--number',
522                               inputFilename, inputFilename2])
523         result = lore.runGivenOptions(options)
524
525         self.assertEqual(None, result)
526         #self.assertEqualFiles1("lore_index_file_out_multiple.html", indexFilename + ".tns")
527         #                       VVV change to new, numbered files
528         self.assertEqualFiles("lore_numbering_test_out.html", "lore_numbering_test.tns")
529         self.assertEqualFiles("lore_numbering_test_out2.html", "lore_numbering_test2.tns")
530
531
532     def test_setTitle(self):
533         """
534         L{tree.setTitle} inserts the given title into the first I{title}
535         element and the first element with the I{title} class in the given
536         template.
537         """
538         parent = dom.Element('div')
539         firstTitle = dom.Element('title')
540         parent.appendChild(firstTitle)
541         secondTitle = dom.Element('span')
542         secondTitle.setAttribute('class', 'title')
543         parent.appendChild(secondTitle)
544
545         titleNodes = [dom.Text()]
546         # minidom has issues with cloning documentless-nodes.  See Python issue
547         # 4851.
548         titleNodes[0].ownerDocument = dom.Document()
549         titleNodes[0].data = 'foo bar'
550
551         tree.setTitle(parent, titleNodes, None)
552         self.assertEqual(firstTitle.toxml(), '<title>foo bar</title>')
553         self.assertEqual(
554             secondTitle.toxml(), '<span class="title">foo bar</span>')
555
556
557     def test_setTitleWithChapter(self):
558         """
559         L{tree.setTitle} includes a chapter number if it is passed one.
560         """
561         document = dom.Document()
562
563         parent = dom.Element('div')
564         parent.ownerDocument = document
565
566         title = dom.Element('title')
567         parent.appendChild(title)
568
569         titleNodes = [dom.Text()]
570         titleNodes[0].ownerDocument = document
571         titleNodes[0].data = 'foo bar'
572
573         # Oh yea.  The numberer has to agree to put the chapter number in, too.
574         numberer.setNumberSections(True)
575
576         tree.setTitle(parent, titleNodes, '13')
577         self.assertEqual(title.toxml(), '<title>13. foo bar</title>')
578
579
580     def test_setIndexLink(self):
581         """
582         Tests to make sure that index links are processed when an index page
583         exists and removed when there is not.
584         """
585         templ = dom.parse(open(d['template']))
586         indexFilename = 'theIndexFile'
587         numLinks = len(domhelpers.findElementsWithAttribute(templ,
588                                                             "class",
589                                                             "index-link"))
590
591         # if our testing template has no index-link nodes, complain about it
592         self.assertNotEquals(
593             [],
594             domhelpers.findElementsWithAttribute(templ,
595                                                  "class",
596                                                  "index-link"))
597
598         tree.setIndexLink(templ, indexFilename)
599
600         self.assertEqual(
601             [],
602             domhelpers.findElementsWithAttribute(templ,
603                                                  "class",
604                                                  "index-link"))
605
606         indexLinks = domhelpers.findElementsWithAttribute(templ,
607                                                           "href",
608                                                           indexFilename)
609         self.assertTrue(len(indexLinks) >= numLinks)
610
611         templ = dom.parse(open(d['template']))
612         self.assertNotEquals(
613             [],
614             domhelpers.findElementsWithAttribute(templ,
615                                                  "class",
616                                                  "index-link"))
617         indexFilename = None
618
619         tree.setIndexLink(templ, indexFilename)
620
621         self.assertEqual(
622             [],
623             domhelpers.findElementsWithAttribute(templ,
624                                                  "class",
625                                                  "index-link"))
626
627
628     def test_addMtime(self):
629         """
630         L{tree.addMtime} inserts a text node giving the last modification time
631         of the specified file wherever it encounters an element with the
632         I{mtime} class.
633         """
634         path = FilePath(self.mktemp())
635         path.setContent('')
636         when = time.ctime(path.getModificationTime())
637
638         parent = dom.Element('div')
639         mtime = dom.Element('span')
640         mtime.setAttribute('class', 'mtime')
641         parent.appendChild(mtime)
642
643         tree.addMtime(parent, path.path)
644         self.assertEqual(
645             mtime.toxml(), '<span class="mtime">' + when + '</span>')
646
647
648     def test_makeLineNumbers(self):
649         """
650         L{tree._makeLineNumbers} takes an integer and returns a I{p} tag with
651         that number of line numbers in it.
652         """
653         numbers = tree._makeLineNumbers(1)
654         self.assertEqual(numbers.tagName, 'p')
655         self.assertEqual(numbers.getAttribute('class'), 'py-linenumber')
656         self.assertIsInstance(numbers.firstChild, dom.Text)
657         self.assertEqual(numbers.firstChild.nodeValue, '1\n')
658
659         numbers = tree._makeLineNumbers(10)
660         self.assertEqual(numbers.tagName, 'p')
661         self.assertEqual(numbers.getAttribute('class'), 'py-linenumber')
662         self.assertIsInstance(numbers.firstChild, dom.Text)
663         self.assertEqual(
664             numbers.firstChild.nodeValue,
665             ' 1\n 2\n 3\n 4\n 5\n'
666             ' 6\n 7\n 8\n 9\n10\n')
667
668
669     def test_fontifyPythonNode(self):
670         """
671         L{tree.fontifyPythonNode} accepts a text node and replaces it in its
672         parent with a syntax colored and line numbered version of the Python
673         source it contains.
674         """
675         parent = dom.Element('div')
676         source = dom.Text()
677         source.data = 'def foo():\n    pass\n'
678         parent.appendChild(source)
679
680         tree.fontifyPythonNode(source)
681
682         expected = """\
683 <div><pre class="python"><p class="py-linenumber">1
684 2
685 </p><span class="py-src-keyword">def</span> <span class="py-src-identifier">foo</span>():
686     <span class="py-src-keyword">pass</span>
687 </pre></div>"""
688
689         self.assertEqual(parent.toxml(), expected)
690
691
692     def test_addPyListings(self):
693         """
694         L{tree.addPyListings} accepts a document with nodes with their I{class}
695         attribute set to I{py-listing} and replaces those nodes with Python
696         source listings from the file given by the node's I{href} attribute.
697         """
698         listingPath = FilePath(self.mktemp())
699         listingPath.setContent('def foo():\n    pass\n')
700
701         parent = dom.Element('div')
702         listing = dom.Element('a')
703         listing.setAttribute('href', listingPath.basename())
704         listing.setAttribute('class', 'py-listing')
705         parent.appendChild(listing)
706
707         tree.addPyListings(parent, listingPath.dirname())
708
709         expected = """\
710 <div><div class="py-listing"><pre><p class="py-linenumber">1
711 2
712 </p><span class="py-src-keyword">def</span> <span class="py-src-identifier">foo</span>():
713     <span class="py-src-keyword">pass</span>
714 </pre><div class="caption"> - <a href="temp"><span class="filename">temp</span></a></div></div></div>"""
715
716         self.assertEqual(parent.toxml(), expected)
717
718
719     def test_addPyListingsSkipLines(self):
720         """
721         If a node with the I{py-listing} class also has a I{skipLines}
722         attribute, that number of lines from the beginning of the source
723         listing are omitted.
724         """
725         listingPath = FilePath(self.mktemp())
726         listingPath.setContent('def foo():\n    pass\n')
727
728         parent = dom.Element('div')
729         listing = dom.Element('a')
730         listing.setAttribute('href', listingPath.basename())
731         listing.setAttribute('class', 'py-listing')
732         listing.setAttribute('skipLines', 1)
733         parent.appendChild(listing)
734
735         tree.addPyListings(parent, listingPath.dirname())
736
737         expected = """\
738 <div><div class="py-listing"><pre><p class="py-linenumber">1
739 </p>    <span class="py-src-keyword">pass</span>
740 </pre><div class="caption"> - <a href="temp"><span class="filename">temp</span></a></div></div></div>"""
741
742         self.assertEqual(parent.toxml(), expected)
743
744
745     def test_fixAPI(self):
746         """
747         The element passed to L{tree.fixAPI} has all of its children with the
748         I{API} class rewritten to contain links to the API which is referred to
749         by the text they contain.
750         """
751         parent = dom.Element('div')
752         link = dom.Element('span')
753         link.setAttribute('class', 'API')
754         text = dom.Text()
755         text.data = 'foo'
756         link.appendChild(text)
757         parent.appendChild(link)
758
759         tree.fixAPI(parent, 'http://example.com/%s')
760         self.assertEqual(
761             parent.toxml(),
762             '<div><span class="API">'
763             '<a href="http://example.com/foo" title="foo">foo</a>'
764             '</span></div>')
765
766
767     def test_fixAPIBase(self):
768         """
769         If a node with the I{API} class and a value for the I{base} attribute
770         is included in the DOM passed to L{tree.fixAPI}, the link added to that
771         node refers to the API formed by joining the value of the I{base}
772         attribute to the text contents of the node.
773         """
774         parent = dom.Element('div')
775         link = dom.Element('span')
776         link.setAttribute('class', 'API')
777         link.setAttribute('base', 'bar')
778         text = dom.Text()
779         text.data = 'baz'
780         link.appendChild(text)
781         parent.appendChild(link)
782
783         tree.fixAPI(parent, 'http://example.com/%s')
784
785         self.assertEqual(
786             parent.toxml(),
787             '<div><span class="API">'
788             '<a href="http://example.com/bar.baz" title="bar.baz">baz</a>'
789             '</span></div>')
790
791
792     def test_fixLinks(self):
793         """
794         Links in the nodes of the DOM passed to L{tree.fixLinks} have their
795         extensions rewritten to the given extension.
796         """
797         parent = dom.Element('div')
798         link = dom.Element('a')
799         link.setAttribute('href', 'foo.html')
800         parent.appendChild(link)
801
802         tree.fixLinks(parent, '.xhtml')
803
804         self.assertEqual(parent.toxml(), '<div><a href="foo.xhtml"/></div>')
805
806
807     def test_setVersion(self):
808         """
809         Nodes of the DOM passed to L{tree.setVersion} which have the I{version}
810         class have the given version added to them a child.
811         """
812         parent = dom.Element('div')
813         version = dom.Element('span')
814         version.setAttribute('class', 'version')
815         parent.appendChild(version)
816
817         tree.setVersion(parent, '1.2.3')
818
819         self.assertEqual(
820             parent.toxml(), '<div><span class="version">1.2.3</span></div>')
821
822
823     def test_footnotes(self):
824         """
825         L{tree.footnotes} finds all of the nodes with the I{footnote} class in
826         the DOM passed to it and adds a footnotes section to the end of the
827         I{body} element which includes them.  It also inserts links to those
828         footnotes from the original definition location.
829         """
830         parent = dom.Element('div')
831         body = dom.Element('body')
832         footnote = dom.Element('span')
833         footnote.setAttribute('class', 'footnote')
834         text = dom.Text()
835         text.data = 'this is the footnote'
836         footnote.appendChild(text)
837         body.appendChild(footnote)
838         body.appendChild(dom.Element('p'))
839         parent.appendChild(body)
840
841         tree.footnotes(parent)
842
843         self.assertEqual(
844             parent.toxml(),
845             '<div><body>'
846             '<a href="#footnote-1" title="this is the footnote">'
847             '<super>1</super>'
848             '</a>'
849             '<p/>'
850             '<h2>Footnotes</h2>'
851             '<ol><li><a name="footnote-1">'
852             '<span class="footnote">this is the footnote</span>'
853             '</a></li></ol>'
854             '</body></div>')
855
856
857     def test_generateTableOfContents(self):
858         """
859         L{tree.generateToC} returns an element which contains a table of
860         contents generated from the headers in the document passed to it.
861         """
862         parent = dom.Element('body')
863         header = dom.Element('h2')
864         text = dom.Text()
865         text.data = u'header & special character'
866         header.appendChild(text)
867         parent.appendChild(header)
868         subheader = dom.Element('h3')
869         text = dom.Text()
870         text.data = 'subheader'
871         subheader.appendChild(text)
872         parent.appendChild(subheader)
873
874         tableOfContents = tree.generateToC(parent)
875         self.assertEqual(
876             tableOfContents.toxml(),
877             '<ol><li><a href="#auto0">header &amp; special character</a></li><ul><li><a href="#auto1">subheader</a></li></ul></ol>')
878
879         self.assertEqual(
880             header.toxml(),
881             '<h2>header &amp; special character<a name="auto0"/></h2>')
882
883         self.assertEqual(
884             subheader.toxml(),
885             '<h3>subheader<a name="auto1"/></h3>')
886
887
888     def test_putInToC(self):
889         """
890         L{tree.putInToC} replaces all of the children of the first node with
891         the I{toc} class with the given node representing a table of contents.
892         """
893         parent = dom.Element('div')
894         toc = dom.Element('span')
895         toc.setAttribute('class', 'toc')
896         toc.appendChild(dom.Element('foo'))
897         parent.appendChild(toc)
898
899         tree.putInToC(parent, dom.Element('toc'))
900
901         self.assertEqual(toc.toxml(), '<span class="toc"><toc/></span>')
902
903
904     def test_invalidTableOfContents(self):
905         """
906         If passed a document with I{h3} elements before any I{h2} element,
907         L{tree.generateToC} raises L{ValueError} explaining that this is not a
908         valid document.
909         """
910         parent = dom.Element('body')
911         parent.appendChild(dom.Element('h3'))
912         err = self.assertRaises(ValueError, tree.generateToC, parent)
913         self.assertEqual(
914             str(err), "No H3 element is allowed until after an H2 element")
915
916
917     def test_notes(self):
918         """
919         L{tree.notes} inserts some additional markup before the first child of
920         any node with the I{note} class.
921         """
922         parent = dom.Element('div')
923         noteworthy = dom.Element('span')
924         noteworthy.setAttribute('class', 'note')
925         noteworthy.appendChild(dom.Element('foo'))
926         parent.appendChild(noteworthy)
927
928         tree.notes(parent)
929
930         self.assertEqual(
931             noteworthy.toxml(),
932             '<span class="note"><strong>Note: </strong><foo/></span>')
933
934
935     def test_findNodeJustBefore(self):
936         """
937         L{tree.findNodeJustBefore} returns the previous sibling of the node it
938         is passed.  The list of nodes passed in is ignored.
939         """
940         parent = dom.Element('div')
941         result = dom.Element('foo')
942         target = dom.Element('bar')
943         parent.appendChild(result)
944         parent.appendChild(target)
945
946         self.assertIdentical(
947             tree.findNodeJustBefore(target, [parent, result]),
948             result)
949
950         # Also, support other configurations.  This is a really not nice API.
951         newTarget = dom.Element('baz')
952         target.appendChild(newTarget)
953         self.assertIdentical(
954             tree.findNodeJustBefore(newTarget, [parent, result]),
955             result)
956
957
958     def test_getSectionNumber(self):
959         """
960         L{tree.getSectionNumber} accepts an I{H2} element and returns its text
961         content.
962         """
963         header = dom.Element('foo')
964         text = dom.Text()
965         text.data = 'foobar'
966         header.appendChild(text)
967         self.assertEqual(tree.getSectionNumber(header), 'foobar')
968
969
970     def test_numberDocument(self):
971         """
972         L{tree.numberDocument} inserts section numbers into the text of each
973         header.
974         """
975         parent = dom.Element('foo')
976         section = dom.Element('h2')
977         text = dom.Text()
978         text.data = 'foo'
979         section.appendChild(text)
980         parent.appendChild(section)
981
982         tree.numberDocument(parent, '7')
983
984         self.assertEqual(section.toxml(), '<h2>7.1 foo</h2>')
985
986
987     def test_parseFileAndReport(self):
988         """
989         L{tree.parseFileAndReport} parses the contents of the filename passed
990         to it and returns the corresponding DOM.
991         """
992         path = FilePath(self.mktemp())
993         path.setContent('<foo bar="baz">hello</foo>\n')
994
995         document = tree.parseFileAndReport(path.path)
996         self.assertXMLEqual(
997             document.toxml(),
998             '<?xml version="1.0" ?><foo bar="baz">hello</foo>')
999
1000
1001     def test_parseFileAndReportMismatchedTags(self):
1002         """
1003         If the contents of the file passed to L{tree.parseFileAndReport}
1004         contain a mismatched tag, L{process.ProcessingFailure} is raised
1005         indicating the location of the open and close tags which were
1006         mismatched.
1007         """
1008         path = FilePath(self.mktemp())
1009         path.setContent('  <foo>\n\n  </bar>')
1010
1011         err = self.assertRaises(
1012             process.ProcessingFailure, tree.parseFileAndReport, path.path)
1013         self.assertEqual(
1014             str(err),
1015             "mismatched close tag at line 3, column 4; expected </foo> "
1016             "(from line 1, column 2)")
1017
1018         # Test a case which requires involves proper close tag handling.
1019         path.setContent('<foo><bar></bar>\n  </baz>')
1020
1021         err = self.assertRaises(
1022             process.ProcessingFailure, tree.parseFileAndReport, path.path)
1023         self.assertEqual(
1024             str(err),
1025             "mismatched close tag at line 2, column 4; expected </foo> "
1026             "(from line 1, column 0)")
1027
1028
1029     def test_parseFileAndReportParseError(self):
1030         """
1031         If the contents of the file passed to L{tree.parseFileAndReport} cannot
1032         be parsed for a reason other than mismatched tags,
1033         L{process.ProcessingFailure} is raised with a string describing the
1034         parse error.
1035         """
1036         path = FilePath(self.mktemp())
1037         path.setContent('\n   foo')
1038
1039         err = self.assertRaises(
1040             process.ProcessingFailure, tree.parseFileAndReport, path.path)
1041         self.assertEqual(str(err), 'syntax error at line 2, column 3')
1042
1043
1044     def test_parseFileAndReportIOError(self):
1045         """
1046         If an L{IOError} is raised while reading from the file specified to
1047         L{tree.parseFileAndReport}, a L{process.ProcessingFailure} is raised
1048         indicating what the error was.  The file should be closed by the
1049         time the exception is raised to the caller.
1050         """
1051         class FakeFile:
1052             _open = True
1053             def read(self, bytes=None):
1054                 raise IOError(errno.ENOTCONN, 'socket not connected')
1055
1056             def close(self):
1057                 self._open = False
1058
1059         theFile = FakeFile()
1060         def fakeOpen(filename):
1061             return theFile
1062
1063         err = self.assertRaises(
1064             process.ProcessingFailure, tree.parseFileAndReport, "foo", fakeOpen)
1065         self.assertEqual(str(err), "socket not connected, filename was 'foo'")
1066         self.assertFalse(theFile._open)
1067
1068
1069
1070 class XMLParsingTests(unittest.TestCase):
1071     """
1072     Tests for various aspects of parsing a Lore XML input document using
1073     L{tree.parseFileAndReport}.
1074     """
1075     def _parseTest(self, xml):
1076         path = FilePath(self.mktemp())
1077         path.setContent(xml)
1078         return tree.parseFileAndReport(path.path)
1079
1080
1081     def test_withoutDocType(self):
1082         """
1083         A Lore XML input document may omit a I{DOCTYPE} declaration.  If it
1084         does so, the XHTML1 Strict DTD is used.
1085         """
1086         # Parsing should succeed.
1087         document = self._parseTest("<foo>uses an xhtml entity: &copy;</foo>")
1088         # But even more than that, the &copy; entity should be turned into the
1089         # appropriate unicode codepoint.
1090         self.assertEqual(
1091             domhelpers.gatherTextNodes(document.documentElement),
1092             u"uses an xhtml entity: \N{COPYRIGHT SIGN}")
1093
1094
1095     def test_withTransitionalDocType(self):
1096         """
1097         A Lore XML input document may include a I{DOCTYPE} declaration
1098         referring to the XHTML1 Transitional DTD.
1099         """
1100         # Parsing should succeed.
1101         document = self._parseTest("""\
1102 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
1103     "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
1104 <foo>uses an xhtml entity: &copy;</foo>
1105 """)
1106         # But even more than that, the &copy; entity should be turned into the
1107         # appropriate unicode codepoint.
1108         self.assertEqual(
1109             domhelpers.gatherTextNodes(document.documentElement),
1110             u"uses an xhtml entity: \N{COPYRIGHT SIGN}")
1111
1112
1113     def test_withStrictDocType(self):
1114         """
1115         A Lore XML input document may include a I{DOCTYPE} declaration
1116         referring to the XHTML1 Strict DTD.
1117         """
1118         # Parsing should succeed.
1119         document = self._parseTest("""\
1120 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
1121     "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
1122 <foo>uses an xhtml entity: &copy;</foo>
1123 """)
1124         # But even more than that, the &copy; entity should be turned into the
1125         # appropriate unicode codepoint.
1126         self.assertEqual(
1127             domhelpers.gatherTextNodes(document.documentElement),
1128             u"uses an xhtml entity: \N{COPYRIGHT SIGN}")
1129
1130
1131     def test_withDisallowedDocType(self):
1132         """
1133         A Lore XML input document may not include a I{DOCTYPE} declaration
1134         referring to any DTD other than XHTML1 Transitional or XHTML1 Strict.
1135         """
1136         self.assertRaises(
1137             process.ProcessingFailure,
1138             self._parseTest,
1139             """\
1140 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
1141     "http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">
1142 <foo>uses an xhtml entity: &copy;</foo>
1143 """)
1144
1145
1146
1147 class XMLSerializationTests(unittest.TestCase, _XMLAssertionMixin):
1148     """
1149     Tests for L{tree._writeDocument}.
1150     """
1151     def test_nonASCIIData(self):
1152         """
1153         A document which contains non-ascii characters is serialized to a
1154         file using UTF-8.
1155         """
1156         document = dom.Document()
1157         parent = dom.Element('foo')
1158         text = dom.Text()
1159         text.data = u'\N{SNOWMAN}'
1160         parent.appendChild(text)
1161         document.appendChild(parent)
1162         outFile = self.mktemp()
1163         tree._writeDocument(outFile, document)
1164         self.assertXMLEqual(
1165             FilePath(outFile).getContent(),
1166             u'<foo>\N{SNOWMAN}</foo>'.encode('utf-8'))
1167
1168
1169
1170 class LatexSpitterTestCase(unittest.TestCase):
1171     """
1172     Tests for the Latex output plugin.
1173     """
1174     def test_indexedSpan(self):
1175         """
1176         Test processing of a span tag with an index class results in a latex
1177         \\index directive the correct value.
1178         """
1179         doc = dom.parseString('<span class="index" value="name" />').documentElement
1180         out = StringIO()
1181         spitter = LatexSpitter(out.write)
1182         spitter.visitNode(doc)
1183         self.assertEqual(out.getvalue(), u'\\index{name}\n')
1184
1185
1186
1187 class ScriptTests(unittest.TestCase):
1188     """
1189     Tests for L{twisted.lore.scripts.lore}, the I{lore} command's
1190     implementation,
1191     """
1192     def test_getProcessor(self):
1193         """
1194         L{lore.getProcessor} loads the specified output plugin from the
1195         specified input plugin.
1196         """
1197         processor = lore.getProcessor("lore", "html", options)
1198         self.assertNotIdentical(processor, None)