1
# Copyright (c) 2008-2009 Twisted Matrix Laboratories.
2
# See LICENSE for details.
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
13
# ++ group index entries by indexed term
14
# ++ sort index entries by indexed term
15
# __ hierarchical index entries (e.g. language!programming)
17
# ++ add parameter for what the index filename should be
18
# ++ add (default) ability to NOT index (if index not specified)
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!
24
# __ make index look nice
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
30
# __ put all of our test files someplace neat and tidy
33
import os, shutil, errno, time
34
from StringIO import StringIO
35
from xml.dom import minidom as dom
37
from twisted.trial import unittest
38
from twisted.python.filepath import FilePath
39
from twisted.python.versions import Version
41
from twisted.lore import tree, process, indexer, numberer, htmlbook, default
42
from twisted.lore.default import factory
43
from twisted.lore.latex import LatexSpitter
45
from twisted.python.util import sibpath
47
from twisted.lore.scripts import lore
49
from twisted.web import domhelpers
51
def sp(originalFileName):
52
return sibpath(__file__, originalFileName)
54
options = {"template" : sp("template.tpl"), 'baseurl': '%s', 'ext': '.xhtml' }
58
class _XMLAssertionMixin:
60
Test mixin defining a method for comparing serialized XML documents.
62
def assertXMLEqual(self, first, second):
64
Verify that two strings represent the same XML document.
67
dom.parseString(first).toxml(),
68
dom.parseString(second).toxml())
71
class TestFactory(unittest.TestCase, _XMLAssertionMixin):
73
file = sp('simple.html')
76
def assertEqualFiles1(self, exp, act):
77
if (exp == act): return True
79
self.assertEqualsFile(exp, fact.read())
81
def assertEqualFiles(self, exp, act):
82
if (exp == act): return True
84
self.assertEqualsFile(exp, fact.read())
86
def assertEqualsFile(self, exp, act):
87
expected = open(sp(exp)).read()
88
self.assertEqual(expected, act)
90
def makeTemp(self, *filenames):
93
for filename in filenames:
94
tmpFile = os.path.join(tmp, filename)
95
shutil.copyfile(sp(filename), tmpFile)
98
########################################
104
def testProcessingFunctionFactory(self):
105
base = FilePath(self.mktemp())
108
simple = base.child('simple.html')
109
FilePath(__file__).sibling('simple.html').copyTo(simple)
111
htmlGenerator = factory.generate_html(options)
112
htmlGenerator(simple.path, self.linkrel)
116
<?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">
117
<head><title>Twisted Documentation: My Test Lore Input</title></head>
118
<body bgcolor="white">
119
<h1 class="title">My Test Lore Input</h1>
120
<div class="content">
124
<a href="index.xhtml">Index</a>
127
simple.sibling('simple.xhtml').getContent())
130
def testProcessingFunctionFactoryWithFilenameGenerator(self):
131
base = FilePath(self.mktemp())
134
def filenameGenerator(originalFileName, outputExtension):
135
name = os.path.splitext(FilePath(originalFileName).basename())[0]
136
return base.child(name + outputExtension).path
138
htmlGenerator = factory.generate_html(options, filenameGenerator)
139
htmlGenerator(self.file, self.linkrel)
142
<?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">
143
<head><title>Twisted Documentation: My Test Lore Input</title></head>
144
<body bgcolor="white">
145
<h1 class="title">My Test Lore Input</h1>
146
<div class="content">
150
<a href="index.xhtml">Index</a>
153
base.child("simple.xhtml").getContent())
156
def test_doFile(self):
157
base = FilePath(self.mktemp())
160
simple = base.child('simple.html')
161
FilePath(__file__).sibling('simple.html').copyTo(simple)
163
templ = dom.parse(open(d['template']))
165
tree.doFile(simple.path, self.linkrel, d['ext'], d['baseurl'], templ, d)
168
<?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">
169
<head><title>Twisted Documentation: My Test Lore Input</title></head>
170
<body bgcolor="white">
171
<h1 class="title">My Test Lore Input</h1>
172
<div class="content">
176
<a href="index.xhtml">Index</a>
179
base.child("simple.xhtml").getContent())
182
def test_doFile_withFilenameGenerator(self):
183
base = FilePath(self.mktemp())
186
def filenameGenerator(originalFileName, outputExtension):
187
name = os.path.splitext(FilePath(originalFileName).basename())[0]
188
return base.child(name + outputExtension).path
190
templ = dom.parse(open(d['template']))
191
tree.doFile(self.file, self.linkrel, d['ext'], d['baseurl'], templ, d, filenameGenerator)
195
<?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">
196
<head><title>Twisted Documentation: My Test Lore Input</title></head>
197
<body bgcolor="white">
198
<h1 class="title">My Test Lore Input</h1>
199
<div class="content">
203
<a href="index.xhtml">Index</a>
206
base.child("simple.xhtml").getContent())
209
def test_munge(self):
210
indexer.setIndexFilename("lore_index_file.html")
211
doc = dom.parse(open(self.file))
212
node = dom.parse(open(d['template']))
213
tree.munge(doc, node, self.linkrel,
214
os.path.dirname(self.file),
216
d['ext'], d['baseurl'], d)
220
<?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">
221
<head><title>Twisted Documentation: My Test Lore Input</title></head>
222
<body bgcolor="white">
223
<h1 class="title">My Test Lore Input</h1>
224
<div class="content">
228
<a href="lore_index_file.html">Index</a>
234
def test_mungeAuthors(self):
236
If there is a node with a I{class} attribute set to C{"authors"},
237
L{tree.munge} adds anchors as children to it, takeing the necessary
238
information from any I{link} nodes in the I{head} with their I{rel}
239
attribute set to C{"author"}.
241
document = dom.parseString(
245
<title>munge authors</title>
246
<link rel="author" title="foo" href="bar"/>
247
<link rel="author" title="baz" href="quux"/>
248
<link rel="author" title="foobar" href="barbaz"/>
251
<h1>munge authors</h1>
254
template = dom.parseString(
256
<html xmlns="http://www.w3.org/1999/xhtml" lang="en">
263
<div class="authors" />
268
document, template, self.linkrel, os.path.dirname(self.file),
269
self.file, d['ext'], d['baseurl'], d)
274
<?xml version="1.0" ?><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
276
<title>munge authors</title>
277
<link href="bar" rel="author" title="foo"/><link href="quux" rel="author" title="baz"/><link href="barbaz" rel="author" title="foobar"/></head>
280
<div class="content">
283
<div class="authors"><span><a href="bar">foo</a>, <a href="quux">baz</a>, and <a href="barbaz">foobar</a></span></div>
288
def test_getProcessor(self):
290
base = FilePath(self.mktemp())
292
input = base.child("simple3.html")
293
FilePath(__file__).sibling("simple3.html").copyTo(input)
295
options = { 'template': sp('template.tpl'), 'ext': '.xhtml', 'baseurl': 'burl',
296
'filenameMapping': None }
297
p = process.getProcessor(default, "html", options)
298
p(input.path, self.linkrel)
301
<?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">
302
<head><title>Twisted Documentation: My Test Lore Input</title></head>
303
<body bgcolor="white">
304
<h1 class="title">My Test Lore Input</h1>
305
<div class="content">
309
<a href="index.xhtml">Index</a>
312
base.child("simple3.xhtml").getContent())
314
def test_outputdirGenerator(self):
315
normp = os.path.normpath; join = os.path.join
316
inputdir = normp(join("/", 'home', 'joe'))
317
outputdir = normp(join("/", 'away', 'joseph'))
318
actual = process.outputdirGenerator(join("/", 'home', 'joe', "myfile.html"),
319
'.xhtml', inputdir, outputdir)
320
expected = normp(join("/", 'away', 'joseph', 'myfile.xhtml'))
321
self.assertEquals(expected, actual)
323
def test_outputdirGeneratorBadInput(self):
324
options = {'outputdir': '/away/joseph/', 'inputdir': '/home/joe/' }
325
self.assertRaises(ValueError, process.outputdirGenerator, '.html', '.xhtml', **options)
327
def test_makeSureDirectoryExists(self):
328
dirname = os.path.join("tmp", 'nonexistentdir')
329
if os.path.exists(dirname):
331
self.failIf(os.path.exists(dirname), "Hey: someone already created the dir")
332
filename = os.path.join(dirname, 'newfile')
333
tree.makeSureDirectoryExists(filename)
334
self.failUnless(os.path.exists(dirname), 'should have created dir')
338
def test_indexAnchorsAdded(self):
339
indexer.setIndexFilename('theIndexFile.html')
340
# generate the output file
341
templ = dom.parse(open(d['template']))
342
tmp = self.makeTemp('lore_index_test.xhtml')
344
tree.doFile(os.path.join(tmp, 'lore_index_test.xhtml'),
345
self.linkrel, '.html', d['baseurl'], templ, d)
349
<?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">
350
<head><title>Twisted Documentation: The way of the program</title></head>
351
<body bgcolor="white">
352
<h1 class="title">The way of the program</h1>
353
<div class="content">
357
<p>The first paragraph.</p>
360
<h2>The Python programming language<a name="auto0"/></h2>
364
<p>The second paragraph.</p>
368
<a href="theIndexFile.html">Index</a>
371
FilePath(tmp).child("lore_index_test.html").getContent())
374
def test_indexEntriesAdded(self):
375
indexer.addEntry('lore_index_test.html', 'index02', 'language of programming', '1.3')
376
indexer.addEntry('lore_index_test.html', 'index01', 'programming language', '1.2')
377
indexer.setIndexFilename("lore_index_file.html")
378
indexer.generateIndex()
379
self.assertEqualFiles1("lore_index_file_out.html", "lore_index_file.html")
382
tmp = self.makeTemp()
383
inputFilename = sp('lore_index_test.xhtml')
385
bookFilename = os.path.join(tmp, 'lore_test_book.book')
386
bf = open(bookFilename, 'w')
387
bf.write('Chapter(r"%s", None)\r\n' % inputFilename)
390
book = htmlbook.Book(bookFilename)
391
expected = {'indexFilename': None,
392
'chapters': [(inputFilename, None)],
396
self.assertEquals(dct[k], expected[k])
398
def test_runningLore(self):
399
options = lore.Options()
400
tmp = self.makeTemp('lore_index_test.xhtml')
402
templateFilename = sp('template.tpl')
403
inputFilename = os.path.join(tmp, 'lore_index_test.xhtml')
404
indexFilename = 'theIndexFile'
406
bookFilename = os.path.join(tmp, 'lore_test_book.book')
407
bf = open(bookFilename, 'w')
408
bf.write('Chapter(r"%s", None)\n' % inputFilename)
411
options.parseOptions(['--null', '--book=%s' % bookFilename,
412
'--config', 'template=%s' % templateFilename,
413
'--index=%s' % indexFilename
415
result = lore.runGivenOptions(options)
416
self.assertEquals(None, result)
417
self.assertEqualFiles1("lore_index_file_unnumbered_out.html", indexFilename + ".html")
420
def test_runningLoreMultipleFiles(self):
421
tmp = self.makeTemp('lore_index_test.xhtml', 'lore_index_test2.xhtml')
422
templateFilename = sp('template.tpl')
423
inputFilename = os.path.join(tmp, 'lore_index_test.xhtml')
424
inputFilename2 = os.path.join(tmp, 'lore_index_test2.xhtml')
425
indexFilename = 'theIndexFile'
427
bookFilename = os.path.join(tmp, 'lore_test_book.book')
428
bf = open(bookFilename, 'w')
429
bf.write('Chapter(r"%s", None)\n' % inputFilename)
430
bf.write('Chapter(r"%s", None)\n' % inputFilename2)
433
options = lore.Options()
434
options.parseOptions(['--null', '--book=%s' % bookFilename,
435
'--config', 'template=%s' % templateFilename,
436
'--index=%s' % indexFilename
438
result = lore.runGivenOptions(options)
439
self.assertEquals(None, result)
442
# XXX This doesn't seem like a very good index file.
444
aahz: <a href="lore_index_test2.html#index03">link</a><br />
445
aahz2: <a href="lore_index_test2.html#index02">link</a><br />
446
language of programming: <a href="lore_index_test.html#index02">link</a>, <a href="lore_index_test2.html#index01">link</a><br />
447
programming language: <a href="lore_index_test.html#index01">link</a><br />
449
file(FilePath(indexFilename + ".html").path).read())
453
<?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">
454
<head><title>Twisted Documentation: The way of the program</title></head>
455
<body bgcolor="white">
456
<h1 class="title">The way of the program</h1>
457
<div class="content">
461
<p>The first paragraph.</p>
464
<h2>The Python programming language<a name="auto0"/></h2>
468
<p>The second paragraph.</p>
472
<a href="theIndexFile.html">Index</a>
475
FilePath(tmp).child("lore_index_test.html").getContent())
479
<?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">
480
<head><title>Twisted Documentation: The second page to index</title></head>
481
<body bgcolor="white">
482
<h1 class="title">The second page to index</h1>
483
<div class="content">
487
<p>The first paragraph of the second page.</p>
490
<h2>The Jython programming language<a name="auto0"/></h2>
495
<p>The second paragraph of the second page.</p>
499
<a href="theIndexFile.html">Index</a>
502
FilePath(tmp).child("lore_index_test2.html").getContent())
506
def XXXtest_NumberedSections(self):
507
# run two files through lore, with numbering turned on
508
# every h2 should be numbered:
509
# first file's h2s should be 1.1, 1.2
510
# second file's h2s should be 2.1, 2.2
511
templateFilename = sp('template.tpl')
512
inputFilename = sp('lore_numbering_test.xhtml')
513
inputFilename2 = sp('lore_numbering_test2.xhtml')
514
indexFilename = 'theIndexFile'
516
# you can number without a book:
517
options = lore.Options()
518
options.parseOptions(['--null',
519
'--index=%s' % indexFilename,
520
'--config', 'template=%s' % templateFilename,
521
'--config', 'ext=%s' % ".tns",
523
inputFilename, inputFilename2])
524
result = lore.runGivenOptions(options)
526
self.assertEquals(None, result)
527
#self.assertEqualFiles1("lore_index_file_out_multiple.html", indexFilename + ".tns")
528
# VVV change to new, numbered files
529
self.assertEqualFiles("lore_numbering_test_out.html", "lore_numbering_test.tns")
530
self.assertEqualFiles("lore_numbering_test_out2.html", "lore_numbering_test2.tns")
533
def test_setTitle(self):
535
L{tree.setTitle} inserts the given title into the first I{title}
536
element and the first element with the I{title} class in the given
539
parent = dom.Element('div')
540
firstTitle = dom.Element('title')
541
parent.appendChild(firstTitle)
542
secondTitle = dom.Element('span')
543
secondTitle.setAttribute('class', 'title')
544
parent.appendChild(secondTitle)
546
titleNodes = [dom.Text()]
547
# minidom has issues with cloning documentless-nodes. See Python issue
549
titleNodes[0].ownerDocument = dom.Document()
550
titleNodes[0].data = 'foo bar'
552
tree.setTitle(parent, titleNodes, None)
553
self.assertEqual(firstTitle.toxml(), '<title>foo bar</title>')
555
secondTitle.toxml(), '<span class="title">foo bar</span>')
558
def test_setTitleWithChapter(self):
560
L{tree.setTitle} includes a chapter number if it is passed one.
562
document = dom.Document()
564
parent = dom.Element('div')
565
parent.ownerDocument = document
567
title = dom.Element('title')
568
parent.appendChild(title)
570
titleNodes = [dom.Text()]
571
titleNodes[0].ownerDocument = document
572
titleNodes[0].data = 'foo bar'
574
# Oh yea. The numberer has to agree to put the chapter number in, too.
575
numberer.setNumberSections(True)
577
tree.setTitle(parent, titleNodes, '13')
578
self.assertEqual(title.toxml(), '<title>13. foo bar</title>')
581
def test_setIndexLink(self):
583
Tests to make sure that index links are processed when an index page
584
exists and removed when there is not.
586
templ = dom.parse(open(d['template']))
587
indexFilename = 'theIndexFile'
588
numLinks = len(domhelpers.findElementsWithAttribute(templ,
592
# if our testing template has no index-link nodes, complain about it
593
self.assertNotEquals(
595
domhelpers.findElementsWithAttribute(templ,
599
tree.setIndexLink(templ, indexFilename)
603
domhelpers.findElementsWithAttribute(templ,
607
indexLinks = domhelpers.findElementsWithAttribute(templ,
610
self.assertTrue(len(indexLinks) >= numLinks)
612
templ = dom.parse(open(d['template']))
613
self.assertNotEquals(
615
domhelpers.findElementsWithAttribute(templ,
620
tree.setIndexLink(templ, indexFilename)
624
domhelpers.findElementsWithAttribute(templ,
629
def test_addMtime(self):
631
L{tree.addMtime} inserts a text node giving the last modification time
632
of the specified file wherever it encounters an element with the
635
path = FilePath(self.mktemp())
637
when = time.ctime(path.getModificationTime())
639
parent = dom.Element('div')
640
mtime = dom.Element('span')
641
mtime.setAttribute('class', 'mtime')
642
parent.appendChild(mtime)
644
tree.addMtime(parent, path.path)
646
mtime.toxml(), '<span class="mtime">' + when + '</span>')
649
def test_makeLineNumbers(self):
651
L{tree._makeLineNumbers} takes an integer and returns a I{p} tag with
652
that number of line numbers in it.
654
numbers = tree._makeLineNumbers(1)
655
self.assertEqual(numbers.tagName, 'p')
656
self.assertEqual(numbers.getAttribute('class'), 'py-linenumber')
657
self.assertIsInstance(numbers.firstChild, dom.Text)
658
self.assertEqual(numbers.firstChild.nodeValue, '1\n')
660
numbers = tree._makeLineNumbers(10)
661
self.assertEqual(numbers.tagName, 'p')
662
self.assertEqual(numbers.getAttribute('class'), 'py-linenumber')
663
self.assertIsInstance(numbers.firstChild, dom.Text)
665
numbers.firstChild.nodeValue,
666
' 1\n 2\n 3\n 4\n 5\n'
667
' 6\n 7\n 8\n 9\n10\n')
670
def test_fontifyPythonNode(self):
672
L{tree.fontifyPythonNode} accepts a text node and replaces it in its
673
parent with a syntax colored and line numbered version of the Python
676
parent = dom.Element('div')
678
source.data = 'def foo():\n pass\n'
679
parent.appendChild(source)
681
tree.fontifyPythonNode(source)
684
<div><pre class="python"><p class="py-linenumber">1
686
</p><span class="py-src-keyword">def</span> <span class="py-src-identifier">foo</span>():
687
<span class="py-src-keyword">pass</span>
690
self.assertEqual(parent.toxml(), expected)
693
def test_addPyListings(self):
695
L{tree.addPyListings} accepts a document with nodes with their I{class}
696
attribute set to I{py-listing} and replaces those nodes with Python
697
source listings from the file given by the node's I{href} attribute.
699
listingPath = FilePath(self.mktemp())
700
listingPath.setContent('def foo():\n pass\n')
702
parent = dom.Element('div')
703
listing = dom.Element('a')
704
listing.setAttribute('href', listingPath.basename())
705
listing.setAttribute('class', 'py-listing')
706
parent.appendChild(listing)
708
tree.addPyListings(parent, listingPath.dirname())
711
<div><div class="py-listing"><pre><p class="py-linenumber">1
713
</p><span class="py-src-keyword">def</span> <span class="py-src-identifier">foo</span>():
714
<span class="py-src-keyword">pass</span>
715
</pre><div class="caption"> - <a href="temp"><span class="filename">temp</span></a></div></div></div>"""
717
self.assertEqual(parent.toxml(), expected)
720
def test_addPyListingsSkipLines(self):
722
If a node with the I{py-listing} class also has a I{skipLines}
723
attribute, that number of lines from the beginning of the source
726
listingPath = FilePath(self.mktemp())
727
listingPath.setContent('def foo():\n pass\n')
729
parent = dom.Element('div')
730
listing = dom.Element('a')
731
listing.setAttribute('href', listingPath.basename())
732
listing.setAttribute('class', 'py-listing')
733
listing.setAttribute('skipLines', 1)
734
parent.appendChild(listing)
736
tree.addPyListings(parent, listingPath.dirname())
739
<div><div class="py-listing"><pre><p class="py-linenumber">1
740
</p> <span class="py-src-keyword">pass</span>
741
</pre><div class="caption"> - <a href="temp"><span class="filename">temp</span></a></div></div></div>"""
743
self.assertEqual(parent.toxml(), expected)
746
def test_fixAPI(self):
748
The element passed to L{tree.fixAPI} has all of its children with the
749
I{API} class rewritten to contain links to the API which is referred to
750
by the text they contain.
752
parent = dom.Element('div')
753
link = dom.Element('span')
754
link.setAttribute('class', 'API')
757
link.appendChild(text)
758
parent.appendChild(link)
760
tree.fixAPI(parent, 'http://example.com/%s')
763
'<div><span class="API">'
764
'<a href="http://example.com/foo" title="foo">foo</a>'
768
def test_fixAPIBase(self):
770
If a node with the I{API} class and a value for the I{base} attribute
771
is included in the DOM passed to L{tree.fixAPI}, the link added to that
772
node refers to the API formed by joining the value of the I{base}
773
attribute to the text contents of the node.
775
parent = dom.Element('div')
776
link = dom.Element('span')
777
link.setAttribute('class', 'API')
778
link.setAttribute('base', 'bar')
781
link.appendChild(text)
782
parent.appendChild(link)
784
tree.fixAPI(parent, 'http://example.com/%s')
788
'<div><span class="API">'
789
'<a href="http://example.com/bar.baz" title="bar.baz">baz</a>'
793
def test_fixLinks(self):
795
Links in the nodes of the DOM passed to L{tree.fixLinks} have their
796
extensions rewritten to the given extension.
798
parent = dom.Element('div')
799
link = dom.Element('a')
800
link.setAttribute('href', 'foo.html')
801
parent.appendChild(link)
803
tree.fixLinks(parent, '.xhtml')
805
self.assertEqual(parent.toxml(), '<div><a href="foo.xhtml"/></div>')
808
def test_setVersion(self):
810
Nodes of the DOM passed to L{tree.setVersion} which have the I{version}
811
class have the given version added to them a child.
813
parent = dom.Element('div')
814
version = dom.Element('span')
815
version.setAttribute('class', 'version')
816
parent.appendChild(version)
818
tree.setVersion(parent, '1.2.3')
821
parent.toxml(), '<div><span class="version">1.2.3</span></div>')
824
def test_footnotes(self):
826
L{tree.footnotes} finds all of the nodes with the I{footnote} class in
827
the DOM passed to it and adds a footnotes section to the end of the
828
I{body} element which includes them. It also inserts links to those
829
footnotes from the original definition location.
831
parent = dom.Element('div')
832
body = dom.Element('body')
833
footnote = dom.Element('span')
834
footnote.setAttribute('class', 'footnote')
836
text.data = 'this is the footnote'
837
footnote.appendChild(text)
838
body.appendChild(footnote)
839
body.appendChild(dom.Element('p'))
840
parent.appendChild(body)
842
tree.footnotes(parent)
847
'<a href="#footnote-1" title="this is the footnote">'
852
'<ol><li><a name="footnote-1">'
853
'<span class="footnote">this is the footnote</span>'
858
def test_generateTableOfContents(self):
860
L{tree.generateToC} returns an element which contains a table of
861
contents generated from the headers in the document passed to it.
863
parent = dom.Element('body')
864
header = dom.Element('h2')
866
text.data = u'header & special character'
867
header.appendChild(text)
868
parent.appendChild(header)
869
subheader = dom.Element('h3')
871
text.data = 'subheader'
872
subheader.appendChild(text)
873
parent.appendChild(subheader)
875
tableOfContents = tree.generateToC(parent)
877
tableOfContents.toxml(),
878
'<ol><li><a href="#auto0">header & special character</a></li><ul><li><a href="#auto1">subheader</a></li></ul></ol>')
882
'<h2>header & special character<a name="auto0"/></h2>')
886
'<h3>subheader<a name="auto1"/></h3>')
889
def test_putInToC(self):
891
L{tree.putInToC} replaces all of the children of the first node with
892
the I{toc} class with the given node representing a table of contents.
894
parent = dom.Element('div')
895
toc = dom.Element('span')
896
toc.setAttribute('class', 'toc')
897
toc.appendChild(dom.Element('foo'))
898
parent.appendChild(toc)
900
tree.putInToC(parent, dom.Element('toc'))
902
self.assertEqual(toc.toxml(), '<span class="toc"><toc/></span>')
905
def test_invalidTableOfContents(self):
907
If passed a document with I{h3} elements before any I{h2} element,
908
L{tree.generateToC} raises L{ValueError} explaining that this is not a
911
parent = dom.Element('body')
912
parent.appendChild(dom.Element('h3'))
913
err = self.assertRaises(ValueError, tree.generateToC, parent)
915
str(err), "No H3 element is allowed until after an H2 element")
918
def test_notes(self):
920
L{tree.notes} inserts some additional markup before the first child of
921
any node with the I{note} class.
923
parent = dom.Element('div')
924
noteworthy = dom.Element('span')
925
noteworthy.setAttribute('class', 'note')
926
noteworthy.appendChild(dom.Element('foo'))
927
parent.appendChild(noteworthy)
933
'<span class="note"><strong>Note: </strong><foo/></span>')
936
def test_findNodeJustBefore(self):
938
L{tree.findNodeJustBefore} returns the previous sibling of the node it
939
is passed. The list of nodes passed in is ignored.
941
parent = dom.Element('div')
942
result = dom.Element('foo')
943
target = dom.Element('bar')
944
parent.appendChild(result)
945
parent.appendChild(target)
947
self.assertIdentical(
948
tree.findNodeJustBefore(target, [parent, result]),
951
# Also, support other configurations. This is a really not nice API.
952
newTarget = dom.Element('baz')
953
target.appendChild(newTarget)
954
self.assertIdentical(
955
tree.findNodeJustBefore(newTarget, [parent, result]),
959
def test_getSectionNumber(self):
961
L{tree.getSectionNumber} accepts an I{H2} element and returns its text
964
header = dom.Element('foo')
967
header.appendChild(text)
968
self.assertEqual(tree.getSectionNumber(header), 'foobar')
971
def test_numberDocument(self):
973
L{tree.numberDocument} inserts section numbers into the text of each
976
parent = dom.Element('foo')
977
section = dom.Element('h2')
980
section.appendChild(text)
981
parent.appendChild(section)
983
tree.numberDocument(parent, '7')
985
self.assertEqual(section.toxml(), '<h2>7.1 foo</h2>')
988
def test_parseFileAndReport(self):
990
L{tree.parseFileAndReport} parses the contents of the filename passed
991
to it and returns the corresponding DOM.
993
path = FilePath(self.mktemp())
994
path.setContent('<foo bar="baz">hello</foo>\n')
996
document = tree.parseFileAndReport(path.path)
999
'<?xml version="1.0" ?><foo bar="baz">hello</foo>')
1002
def test_parseFileAndReportMismatchedTags(self):
1004
If the contents of the file passed to L{tree.parseFileAndReport}
1005
contain a mismatched tag, L{process.ProcessingFailure} is raised
1006
indicating the location of the open and close tags which were
1009
path = FilePath(self.mktemp())
1010
path.setContent(' <foo>\n\n </bar>')
1012
err = self.assertRaises(
1013
process.ProcessingFailure, tree.parseFileAndReport, path.path)
1016
"mismatched close tag at line 3, column 4; expected </foo> "
1017
"(from line 1, column 2)")
1019
# Test a case which requires involves proper close tag handling.
1020
path.setContent('<foo><bar></bar>\n </baz>')
1022
err = self.assertRaises(
1023
process.ProcessingFailure, tree.parseFileAndReport, path.path)
1026
"mismatched close tag at line 2, column 4; expected </foo> "
1027
"(from line 1, column 0)")
1030
def test_parseFileAndReportParseError(self):
1032
If the contents of the file passed to L{tree.parseFileAndReport} cannot
1033
be parsed for a reason other than mismatched tags,
1034
L{process.ProcessingFailure} is raised with a string describing the
1037
path = FilePath(self.mktemp())
1038
path.setContent('\n foo')
1040
err = self.assertRaises(
1041
process.ProcessingFailure, tree.parseFileAndReport, path.path)
1042
self.assertEqual(str(err), 'syntax error at line 2, column 3')
1045
def test_parseFileAndReportIOError(self):
1047
If an L{IOError} is raised while reading from the file specified to
1048
L{tree.parseFileAndReport}, a L{process.ProcessingFailure} is raised
1049
indicating what the error was. The file should be closed by the
1050
time the exception is raised to the caller.
1054
def read(self, bytes=None):
1055
raise IOError(errno.ENOTCONN, 'socket not connected')
1060
theFile = FakeFile()
1061
def fakeOpen(filename):
1064
err = self.assertRaises(
1065
process.ProcessingFailure, tree.parseFileAndReport, "foo", fakeOpen)
1066
self.assertEqual(str(err), "socket not connected, filename was 'foo'")
1067
self.assertFalse(theFile._open)
1071
class XMLParsingTests(unittest.TestCase):
1073
Tests for various aspects of parsing a Lore XML input document using
1074
L{tree.parseFileAndReport}.
1076
def _parseTest(self, xml):
1077
path = FilePath(self.mktemp())
1078
path.setContent(xml)
1079
return tree.parseFileAndReport(path.path)
1082
def test_withoutDocType(self):
1084
A Lore XML input document may omit a I{DOCTYPE} declaration. If it
1085
does so, the XHTML1 Strict DTD is used.
1087
# Parsing should succeed.
1088
document = self._parseTest("<foo>uses an xhtml entity: ©</foo>")
1089
# But even more than that, the © entity should be turned into the
1090
# appropriate unicode codepoint.
1092
domhelpers.gatherTextNodes(document.documentElement),
1093
u"uses an xhtml entity: \N{COPYRIGHT SIGN}")
1096
def test_withTransitionalDocType(self):
1098
A Lore XML input document may include a I{DOCTYPE} declaration
1099
referring to the XHTML1 Transitional DTD.
1101
# Parsing should succeed.
1102
document = self._parseTest("""\
1103
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
1104
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
1105
<foo>uses an xhtml entity: ©</foo>
1107
# But even more than that, the © entity should be turned into the
1108
# appropriate unicode codepoint.
1110
domhelpers.gatherTextNodes(document.documentElement),
1111
u"uses an xhtml entity: \N{COPYRIGHT SIGN}")
1114
def test_withStrictDocType(self):
1116
A Lore XML input document may include a I{DOCTYPE} declaration
1117
referring to the XHTML1 Strict DTD.
1119
# Parsing should succeed.
1120
document = self._parseTest("""\
1121
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
1122
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
1123
<foo>uses an xhtml entity: ©</foo>
1125
# But even more than that, the © entity should be turned into the
1126
# appropriate unicode codepoint.
1128
domhelpers.gatherTextNodes(document.documentElement),
1129
u"uses an xhtml entity: \N{COPYRIGHT SIGN}")
1132
def test_withDisallowedDocType(self):
1134
A Lore XML input document may not include a I{DOCTYPE} declaration
1135
referring to any DTD other than XHTML1 Transitional or XHTML1 Strict.
1138
process.ProcessingFailure,
1141
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
1142
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">
1143
<foo>uses an xhtml entity: ©</foo>
1148
class XMLSerializationTests(unittest.TestCase, _XMLAssertionMixin):
1150
Tests for L{tree._writeDocument}.
1152
def test_nonASCIIData(self):
1154
A document which contains non-ascii characters is serialized to a
1157
document = dom.Document()
1158
parent = dom.Element('foo')
1160
text.data = u'\N{SNOWMAN}'
1161
parent.appendChild(text)
1162
document.appendChild(parent)
1163
outFile = self.mktemp()
1164
tree._writeDocument(outFile, document)
1165
self.assertXMLEqual(
1166
FilePath(outFile).getContent(),
1167
u'<foo>\N{SNOWMAN}</foo>'.encode('utf-8'))
1171
class LatexSpitterTestCase(unittest.TestCase):
1173
Tests for the Latex output plugin.
1175
def test_indexedSpan(self):
1177
Test processing of a span tag with an index class results in a latex
1178
\\index directive the correct value.
1180
doc = dom.parseString('<span class="index" value="name" />').documentElement
1182
spitter = LatexSpitter(out.write)
1183
spitter.visitNode(doc)
1184
self.assertEqual(out.getvalue(), u'\\index{name}\n')
1188
class ScriptTests(unittest.TestCase):
1190
Tests for L{twisted.lore.scripts.lore}, the I{lore} command's
1193
def test_getProcessor(self):
1195
L{lore.getProcessor} loads the specified output plugin from the
1196
specified input plugin.
1198
processor = lore.getProcessor("lore", "html", options)
1199
self.assertNotIdentical(processor, None)
1203
class DeprecationTests(unittest.TestCase):
1205
Tests for deprecated APIs in L{twisted.lore.tree}.
1207
def test_comparePosition(self):
1209
L{tree.comparePosition} is deprecated.
1211
from twisted.web.microdom import parseString
1212
element = parseString('<foo/>').documentElement
1214
self.callDeprecated(
1215
Version('Twisted', 9, 0, 0),
1216
tree.comparePosition, element, element),
1220
def test_compareMarkPos(self):
1222
L{tree.compareMarkPos} is deprecated.
1225
self.callDeprecated(
1226
Version('Twisted', 9, 0, 0),
1227
tree.compareMarkPos, [0, 1], [1, 2]),