OLD | NEW |
| (Empty) |
1 # Copyright (c) 2007-2008 Twisted Matrix Laboratories. | |
2 # See LICENSE for details. | |
3 | |
4 """ | |
5 Tests for L{twisted.python.release} and L{twisted.python._release}. | |
6 """ | |
7 | |
8 import warnings | |
9 import operator | |
10 import os, sys, signal | |
11 from StringIO import StringIO | |
12 import tarfile | |
13 | |
14 from datetime import date | |
15 | |
16 try: | |
17 import pydoctor.driver | |
18 # it might not be installed, or it might use syntax not available in | |
19 # this version of Python. | |
20 except (ImportError, SyntaxError): | |
21 pydoctor = None | |
22 | |
23 from twisted.trial.unittest import TestCase | |
24 | |
25 from twisted.python.compat import set | |
26 from twisted.python.procutils import which | |
27 from twisted.python import release | |
28 from twisted.python.filepath import FilePath | |
29 from twisted.python.util import dsu | |
30 from twisted.python.versions import Version | |
31 from twisted.python._release import _changeVersionInFile, getNextVersion | |
32 from twisted.python._release import findTwistedProjects, replaceInFile | |
33 from twisted.python._release import replaceProjectVersion | |
34 from twisted.python._release import updateTwistedVersionInformation, Project | |
35 from twisted.python._release import VERSION_OFFSET, DocBuilder, ManBuilder | |
36 from twisted.python._release import NoDocumentsFound, filePathDelta | |
37 from twisted.python._release import CommandFailed, BookBuilder | |
38 from twisted.python._release import DistributionBuilder, APIBuilder | |
39 | |
40 try: | |
41 from twisted.lore.scripts import lore | |
42 except ImportError: | |
43 lore = None | |
44 | |
45 | |
46 | |
47 class ChangeVersionTest(TestCase): | |
48 """ | |
49 Twisted has the ability to change versions. | |
50 """ | |
51 | |
52 def makeFile(self, relativePath, content): | |
53 """ | |
54 Create a file with the given content relative to a temporary directory. | |
55 | |
56 @param relativePath: The basename of the file to create. | |
57 @param content: The content that the file will have. | |
58 @return: The filename. | |
59 """ | |
60 baseDirectory = FilePath(self.mktemp()) | |
61 directory, filename = os.path.split(relativePath) | |
62 directory = baseDirectory.preauthChild(directory) | |
63 directory.makedirs() | |
64 file = directory.child(filename) | |
65 directory.child(filename).setContent(content) | |
66 return file | |
67 | |
68 | |
69 def test_getNextVersion(self): | |
70 """ | |
71 When calculating the next version to release when a release is | |
72 happening in the same year as the last release, the minor version | |
73 number is incremented. | |
74 """ | |
75 now = date.today() | |
76 major = now.year - VERSION_OFFSET | |
77 version = Version("twisted", major, 9, 0) | |
78 self.assertEquals(getNextVersion(version, now=now), | |
79 Version("twisted", major, 10, 0)) | |
80 | |
81 | |
82 def test_getNextVersionAfterYearChange(self): | |
83 """ | |
84 When calculating the next version to release when a release is | |
85 happening in a later year, the minor version number is reset to 0. | |
86 """ | |
87 now = date.today() | |
88 major = now.year - VERSION_OFFSET | |
89 version = Version("twisted", major - 1, 9, 0) | |
90 self.assertEquals(getNextVersion(version, now=now), | |
91 Version("twisted", major, 0, 0)) | |
92 | |
93 | |
94 def test_changeVersionInFile(self): | |
95 """ | |
96 _changeVersionInFile replaces the old version information in a file | |
97 with the given new version information. | |
98 """ | |
99 # The version numbers are arbitrary, the name is only kind of | |
100 # arbitrary. | |
101 packageName = 'foo' | |
102 oldVersion = Version(packageName, 2, 5, 0) | |
103 file = self.makeFile('README', | |
104 "Hello and welcome to %s." % oldVersion.base()) | |
105 | |
106 newVersion = Version(packageName, 7, 6, 0) | |
107 _changeVersionInFile(oldVersion, newVersion, file.path) | |
108 | |
109 self.assertEqual(file.getContent(), | |
110 "Hello and welcome to %s." % newVersion.base()) | |
111 | |
112 | |
113 | |
114 class ProjectTest(TestCase): | |
115 """ | |
116 There is a first-class representation of a project. | |
117 """ | |
118 | |
119 def assertProjectsEqual(self, observedProjects, expectedProjects): | |
120 """ | |
121 Assert that two lists of L{Project}s are equal. | |
122 """ | |
123 self.assertEqual(len(observedProjects), len(expectedProjects)) | |
124 observedProjects = dsu(observedProjects, | |
125 key=operator.attrgetter('directory')) | |
126 expectedProjects = dsu(expectedProjects, | |
127 key=operator.attrgetter('directory')) | |
128 for observed, expected in zip(observedProjects, expectedProjects): | |
129 self.assertEqual(observed.directory, expected.directory) | |
130 | |
131 | |
132 def makeProject(self, version, baseDirectory=None): | |
133 """ | |
134 Make a Twisted-style project in the given base directory. | |
135 | |
136 @param baseDirectory: The directory to create files in | |
137 (as a L{FilePath). | |
138 @param version: The version information for the project. | |
139 @return: L{Project} pointing to the created project. | |
140 """ | |
141 if baseDirectory is None: | |
142 baseDirectory = FilePath(self.mktemp()) | |
143 baseDirectory.createDirectory() | |
144 segments = version.package.split('.') | |
145 directory = baseDirectory | |
146 for segment in segments: | |
147 directory = directory.child(segment) | |
148 if not directory.exists(): | |
149 directory.createDirectory() | |
150 directory.child('__init__.py').setContent('') | |
151 directory.child('topfiles').createDirectory() | |
152 directory.child('topfiles').child('README').setContent(version.base()) | |
153 replaceProjectVersion( | |
154 version.package, directory.child('_version.py').path, | |
155 version) | |
156 return Project(directory) | |
157 | |
158 | |
159 def makeProjects(self, *versions): | |
160 """ | |
161 Create a series of projects underneath a temporary base directory. | |
162 | |
163 @return: A L{FilePath} for the base directory. | |
164 """ | |
165 baseDirectory = FilePath(self.mktemp()) | |
166 baseDirectory.createDirectory() | |
167 for version in versions: | |
168 self.makeProject(version, baseDirectory) | |
169 return baseDirectory | |
170 | |
171 | |
172 def test_getVersion(self): | |
173 """ | |
174 Project objects know their version. | |
175 """ | |
176 version = Version('foo', 2, 1, 0) | |
177 project = self.makeProject(version) | |
178 self.assertEquals(project.getVersion(), version) | |
179 | |
180 | |
181 def test_updateVersion(self): | |
182 """ | |
183 Project objects know how to update the version numbers in those | |
184 projects. | |
185 """ | |
186 project = self.makeProject(Version("bar", 2, 1, 0)) | |
187 newVersion = Version("bar", 3, 2, 9) | |
188 project.updateVersion(newVersion) | |
189 self.assertEquals(project.getVersion(), newVersion) | |
190 self.assertEquals( | |
191 project.directory.child("topfiles").child("README").getContent(), | |
192 "3.2.9") | |
193 | |
194 | |
195 def test_repr(self): | |
196 """ | |
197 The representation of a Project is Project(directory). | |
198 """ | |
199 foo = Project(FilePath('bar')) | |
200 self.assertEqual( | |
201 repr(foo), 'Project(%r)' % (foo.directory)) | |
202 | |
203 | |
204 def test_findTwistedStyleProjects(self): | |
205 """ | |
206 findTwistedStyleProjects finds all projects underneath a particular | |
207 directory. A 'project' is defined by the existence of a 'topfiles' | |
208 directory and is returned as a Project object. | |
209 """ | |
210 baseDirectory = self.makeProjects( | |
211 Version('foo', 2, 3, 0), Version('foo.bar', 0, 7, 4)) | |
212 projects = findTwistedProjects(baseDirectory) | |
213 self.assertProjectsEqual( | |
214 projects, | |
215 [Project(baseDirectory.child('foo')), | |
216 Project(baseDirectory.child('foo').child('bar'))]) | |
217 | |
218 | |
219 def test_updateTwistedVersionInformation(self): | |
220 """ | |
221 Update Twisted version information in the top-level project and all of | |
222 the subprojects. | |
223 """ | |
224 baseDirectory = FilePath(self.mktemp()) | |
225 baseDirectory.createDirectory() | |
226 now = date.today() | |
227 | |
228 projectName = 'foo' | |
229 oldVersion = Version(projectName, 2, 5, 0) | |
230 newVersion = getNextVersion(oldVersion, now=now) | |
231 | |
232 project = self.makeProject(oldVersion, baseDirectory) | |
233 | |
234 updateTwistedVersionInformation(baseDirectory, now=now) | |
235 | |
236 self.assertEqual(project.getVersion(), newVersion) | |
237 self.assertEqual( | |
238 project.directory.child('topfiles').child('README').getContent(), | |
239 newVersion.base()) | |
240 | |
241 | |
242 | |
243 class UtilityTest(TestCase): | |
244 """ | |
245 Tests for various utility functions for releasing. | |
246 """ | |
247 | |
248 def test_chdir(self): | |
249 """ | |
250 Test that the runChdirSafe is actually safe, i.e., it still | |
251 changes back to the original directory even if an error is | |
252 raised. | |
253 """ | |
254 cwd = os.getcwd() | |
255 def chAndBreak(): | |
256 os.mkdir('releaseCh') | |
257 os.chdir('releaseCh') | |
258 1/0 | |
259 self.assertRaises(ZeroDivisionError, | |
260 release.runChdirSafe, chAndBreak) | |
261 self.assertEquals(cwd, os.getcwd()) | |
262 | |
263 | |
264 | |
265 def test_replaceInFile(self): | |
266 """ | |
267 L{replaceInFile} replaces data in a file based on a dict. A key from | |
268 the dict that is found in the file is replaced with the corresponding | |
269 value. | |
270 """ | |
271 in_ = 'foo\nhey hey $VER\nbar\n' | |
272 outf = open('release.replace', 'w') | |
273 outf.write(in_) | |
274 outf.close() | |
275 | |
276 expected = in_.replace('$VER', '2.0.0') | |
277 replaceInFile('release.replace', {'$VER': '2.0.0'}) | |
278 self.assertEquals(open('release.replace').read(), expected) | |
279 | |
280 | |
281 expected = expected.replace('2.0.0', '3.0.0') | |
282 replaceInFile('release.replace', {'2.0.0': '3.0.0'}) | |
283 self.assertEquals(open('release.replace').read(), expected) | |
284 | |
285 | |
286 | |
287 class VersionWritingTest(TestCase): | |
288 """ | |
289 Tests for L{replaceProjectVersion}. | |
290 """ | |
291 | |
292 def test_replaceProjectVersion(self): | |
293 """ | |
294 L{replaceProjectVersion} writes a Python file that defines a | |
295 C{version} variable that corresponds to the given name and version | |
296 number. | |
297 """ | |
298 replaceProjectVersion("twisted.test_project", | |
299 "test_project", Version("whatever", 0, 82, 7)) | |
300 ns = {'__name___': 'twisted.test_project'} | |
301 execfile("test_project", ns) | |
302 self.assertEquals(ns["version"].base(), "0.82.7") | |
303 | |
304 | |
305 def test_replaceProjectVersionWithPrerelease(self): | |
306 """ | |
307 L{replaceProjectVersion} will write a Version instantiation that | |
308 includes a prerelease parameter if necessary. | |
309 """ | |
310 replaceProjectVersion("twisted.test_project", | |
311 "test_project", Version("whatever", 0, 82, 7, | |
312 prerelease=8)) | |
313 ns = {'__name___': 'twisted.test_project'} | |
314 execfile("test_project", ns) | |
315 self.assertEquals(ns["version"].base(), "0.82.7pre8") | |
316 | |
317 | |
318 | |
319 class BuilderTestsMixin(object): | |
320 """ | |
321 A mixin class which provides various methods for creating sample Lore input | |
322 and output. | |
323 | |
324 @cvar template: The lore template that will be used to prepare sample | |
325 output. | |
326 @type template: C{str} | |
327 | |
328 @ivar docCounter: A counter which is incremented every time input is | |
329 generated and which is included in the documents. | |
330 @type docCounter: C{int} | |
331 """ | |
332 template = ''' | |
333 <html> | |
334 <head><title>Yo:</title></head> | |
335 <body> | |
336 <div class="body" /> | |
337 <a href="index.html">Index</a> | |
338 <span class="version">Version: </span> | |
339 </body> | |
340 </html> | |
341 ''' | |
342 | |
343 def setUp(self): | |
344 """ | |
345 Initialize the doc counter which ensures documents are unique. | |
346 """ | |
347 self.docCounter = 0 | |
348 | |
349 | |
350 def getArbitraryOutput(self, version, counter, prefix=""): | |
351 """ | |
352 Get the correct HTML output for the arbitrary input returned by | |
353 L{getArbitraryLoreInput} for the given parameters. | |
354 | |
355 @param version: The version string to include in the output. | |
356 @type version: C{str} | |
357 @param counter: A counter to include in the output. | |
358 @type counter: C{int} | |
359 """ | |
360 document = ('<?xml version="1.0"?><html><head>' | |
361 '<title>Yo:Hi! Title: %(count)s</title></head>' | |
362 '<body><div class="content">Hi! %(count)s' | |
363 '<div class="API"><a href="foobar" title="foobar">' | |
364 'foobar</a></div></div><a href="%(prefix)sindex.html">' | |
365 'Index</a><span class="version">Version: %(version)s' | |
366 '</span></body></html>') | |
367 return document % {"count": counter, "prefix": prefix, | |
368 "version": version} | |
369 | |
370 | |
371 def getArbitraryLoreInput(self, counter): | |
372 """ | |
373 Get an arbitrary, unique (for this test case) string of lore input. | |
374 | |
375 @param counter: A counter to include in the input. | |
376 @type counter: C{int} | |
377 """ | |
378 template = ( | |
379 '<html>' | |
380 '<head><title>Hi! Title: %(count)s</title></head>' | |
381 '<body>' | |
382 'Hi! %(count)s' | |
383 '<div class="API">foobar</div>' | |
384 '</body>' | |
385 '</html>') | |
386 return template % {"count": counter} | |
387 | |
388 | |
389 def getArbitraryLoreInputAndOutput(self, version, prefix=""): | |
390 """ | |
391 Get an input document along with expected output for lore run on that | |
392 output document, assuming an appropriately-specified C{self.template}. | |
393 | |
394 @param version: A version string to include in the input and output. | |
395 @type version: C{str} | |
396 @param prefix: The prefix to include in the link to the index. | |
397 @type prefix: C{str} | |
398 | |
399 @return: A two-tuple of input and expected output. | |
400 @rtype: C{(str, str)}. | |
401 """ | |
402 self.docCounter += 1 | |
403 return (self.getArbitraryLoreInput(self.docCounter), | |
404 self.getArbitraryOutput(version, self.docCounter, | |
405 prefix=prefix)) | |
406 | |
407 | |
408 def getArbitraryManInput(self): | |
409 """ | |
410 Get an arbitrary man page content. | |
411 """ | |
412 return """.TH MANHOLE "1" "August 2001" "" "" | |
413 .SH NAME | |
414 manhole \- Connect to a Twisted Manhole service | |
415 .SH SYNOPSIS | |
416 .B manhole | |
417 .SH DESCRIPTION | |
418 manhole is a GTK interface to Twisted Manhole services. You can execute python | |
419 code as if at an interactive Python console inside a running Twisted process | |
420 with this.""" | |
421 | |
422 | |
423 def getArbitraryManLoreOutput(self): | |
424 """ | |
425 Get an arbitrary lore input document which represents man-to-lore | |
426 output based on the man page returned from L{getArbitraryManInput} | |
427 """ | |
428 return ("<html><head>\n<title>MANHOLE.1</title>" | |
429 "</head>\n<body>\n\n<h1>MANHOLE.1</h1>\n\n<h2>NAME</h2>\n\n" | |
430 "<p>manhole - Connect to a Twisted Manhole service\n</p>\n\n" | |
431 "<h2>SYNOPSIS</h2>\n\n<p><strong>manhole</strong> </p>\n\n" | |
432 "<h2>DESCRIPTION</h2>\n\n<p>manhole is a GTK interface to Twisted " | |
433 "Manhole services. You can execute python\ncode as if at an " | |
434 "interactive Python console inside a running Twisted process\nwith" | |
435 " this.</p>\n\n</body>\n</html>\n") | |
436 | |
437 | |
438 def getArbitraryManHTMLOutput(self, version, prefix=""): | |
439 """ | |
440 Get an arbitrary lore output document which represents the lore HTML | |
441 output based on the input document returned from | |
442 L{getArbitraryManLoreOutput}. | |
443 | |
444 @param version: A version string to include in the document. | |
445 @type version: C{str} | |
446 @param prefix: The prefix to include in the link to the index. | |
447 @type prefix: C{str} | |
448 """ | |
449 return ('<?xml version="1.0"?><html><head>' | |
450 '<title>Yo:MANHOLE.1</title></head><body><div class="content">' | |
451 '<span></span><h2>NAME<a name="auto0"></a></h2><p>manhole - ' | |
452 'Connect to a Twisted Manhole service\n</p><h2>SYNOPSIS<a ' | |
453 'name="auto1"></a></h2><p><strong>manhole</strong></p><h2>' | |
454 'DESCRIPTION<a name="auto2"></a></h2><p>manhole is a GTK ' | |
455 'interface to Twisted Manhole services. You can execute ' | |
456 'python\ncode as if at an interactive Python console inside a ' | |
457 'running Twisted process\nwith this.</p></div><a ' | |
458 'href="%sindex.html">Index</a><span class="version">Version: ' | |
459 '%s</span></body></html>' % (prefix, version)) | |
460 | |
461 | |
462 | |
463 | |
464 class DocBuilderTestCase(TestCase, BuilderTestsMixin): | |
465 """ | |
466 Tests for L{DocBuilder}. | |
467 | |
468 Note for future maintainers: The exact byte equality assertions throughout | |
469 this suite may need to be updated due to minor differences in lore. They | |
470 should not be taken to mean that Lore must maintain the same byte format | |
471 forever. Feel free to update the tests when Lore changes, but please be | |
472 careful. | |
473 """ | |
474 | |
475 def setUp(self): | |
476 """ | |
477 Set up a few instance variables that will be useful. | |
478 | |
479 @ivar builder: A plain L{DocBuilder}. | |
480 @ivar docCounter: An integer to be used as a counter by the | |
481 C{getArbitrary...} methods. | |
482 @ivar howtoDir: A L{FilePath} representing a directory to be used for | |
483 containing Lore documents. | |
484 @ivar templateFile: A L{FilePath} representing a file with | |
485 C{self.template} as its content. | |
486 """ | |
487 BuilderTestsMixin.setUp(self) | |
488 self.builder = DocBuilder() | |
489 self.howtoDir = FilePath(self.mktemp()) | |
490 self.howtoDir.createDirectory() | |
491 self.templateFile = self.howtoDir.child("template.tpl") | |
492 self.templateFile.setContent(self.template) | |
493 | |
494 | |
495 def test_build(self): | |
496 """ | |
497 The L{DocBuilder} runs lore on all .xhtml files within a directory. | |
498 """ | |
499 version = "1.2.3" | |
500 input1, output1 = self.getArbitraryLoreInputAndOutput(version) | |
501 input2, output2 = self.getArbitraryLoreInputAndOutput(version) | |
502 | |
503 self.howtoDir.child("one.xhtml").setContent(input1) | |
504 self.howtoDir.child("two.xhtml").setContent(input2) | |
505 | |
506 self.builder.build(version, self.howtoDir, self.howtoDir, | |
507 self.templateFile) | |
508 out1 = self.howtoDir.child('one.html') | |
509 out2 = self.howtoDir.child('two.html') | |
510 self.assertEquals(out1.getContent(), output1) | |
511 self.assertEquals(out2.getContent(), output2) | |
512 | |
513 | |
514 def test_noDocumentsFound(self): | |
515 """ | |
516 The C{build} method raises L{NoDocumentsFound} if there are no | |
517 .xhtml files in the given directory. | |
518 """ | |
519 self.assertRaises( | |
520 NoDocumentsFound, | |
521 self.builder.build, "1.2.3", self.howtoDir, self.howtoDir, | |
522 self.templateFile) | |
523 | |
524 | |
525 def test_parentDocumentLinking(self): | |
526 """ | |
527 The L{DocBuilder} generates correct links from documents to | |
528 template-generated links like stylesheets and index backreferences. | |
529 """ | |
530 input = self.getArbitraryLoreInput(0) | |
531 tutoDir = self.howtoDir.child("tutorial") | |
532 tutoDir.createDirectory() | |
533 tutoDir.child("child.xhtml").setContent(input) | |
534 self.builder.build("1.2.3", self.howtoDir, tutoDir, self.templateFile) | |
535 outFile = tutoDir.child('child.html') | |
536 self.assertIn('<a href="../index.html">Index</a>', | |
537 outFile.getContent()) | |
538 | |
539 | |
540 def test_siblingDirectoryDocumentLinking(self): | |
541 """ | |
542 It is necessary to generate documentation in a directory foo/bar where | |
543 stylesheet and indexes are located in foo/baz. Such resources should be | |
544 appropriately linked to. | |
545 """ | |
546 input = self.getArbitraryLoreInput(0) | |
547 resourceDir = self.howtoDir.child("resources") | |
548 docDir = self.howtoDir.child("docs") | |
549 docDir.createDirectory() | |
550 docDir.child("child.xhtml").setContent(input) | |
551 self.builder.build("1.2.3", resourceDir, docDir, self.templateFile) | |
552 outFile = docDir.child('child.html') | |
553 self.assertIn('<a href="../resources/index.html">Index</a>', | |
554 outFile.getContent()) | |
555 | |
556 | |
557 def test_apiLinking(self): | |
558 """ | |
559 The L{DocBuilder} generates correct links from documents to API | |
560 documentation. | |
561 """ | |
562 version = "1.2.3" | |
563 input, output = self.getArbitraryLoreInputAndOutput(version) | |
564 self.howtoDir.child("one.xhtml").setContent(input) | |
565 | |
566 self.builder.build(version, self.howtoDir, self.howtoDir, | |
567 self.templateFile, "scheme:apilinks/%s.ext") | |
568 out = self.howtoDir.child('one.html') | |
569 self.assertIn( | |
570 '<a href="scheme:apilinks/foobar.ext" title="foobar">foobar</a>', | |
571 out.getContent()) | |
572 | |
573 | |
574 def test_deleteInput(self): | |
575 """ | |
576 L{DocBuilder.build} can be instructed to delete the input files after | |
577 generating the output based on them. | |
578 """ | |
579 input1 = self.getArbitraryLoreInput(0) | |
580 self.howtoDir.child("one.xhtml").setContent(input1) | |
581 self.builder.build("whatever", self.howtoDir, self.howtoDir, | |
582 self.templateFile, deleteInput=True) | |
583 self.assertTrue(self.howtoDir.child('one.html').exists()) | |
584 self.assertFalse(self.howtoDir.child('one.xhtml').exists()) | |
585 | |
586 | |
587 def test_doNotDeleteInput(self): | |
588 """ | |
589 Input will not be deleted by default. | |
590 """ | |
591 input1 = self.getArbitraryLoreInput(0) | |
592 self.howtoDir.child("one.xhtml").setContent(input1) | |
593 self.builder.build("whatever", self.howtoDir, self.howtoDir, | |
594 self.templateFile) | |
595 self.assertTrue(self.howtoDir.child('one.html').exists()) | |
596 self.assertTrue(self.howtoDir.child('one.xhtml').exists()) | |
597 | |
598 | |
599 def test_getLinkrelToSameDirectory(self): | |
600 """ | |
601 If the doc and resource directories are the same, the linkrel should be | |
602 an empty string. | |
603 """ | |
604 linkrel = self.builder.getLinkrel(FilePath("/foo/bar"), | |
605 FilePath("/foo/bar")) | |
606 self.assertEquals(linkrel, "") | |
607 | |
608 | |
609 def test_getLinkrelToParentDirectory(self): | |
610 """ | |
611 If the doc directory is a child of the resource directory, the linkrel | |
612 should make use of '..'. | |
613 """ | |
614 linkrel = self.builder.getLinkrel(FilePath("/foo"), | |
615 FilePath("/foo/bar")) | |
616 self.assertEquals(linkrel, "../") | |
617 | |
618 | |
619 def test_getLinkrelToSibling(self): | |
620 """ | |
621 If the doc directory is a sibling of the resource directory, the | |
622 linkrel should make use of '..' and a named segment. | |
623 """ | |
624 linkrel = self.builder.getLinkrel(FilePath("/foo/howto"), | |
625 FilePath("/foo/examples")) | |
626 self.assertEquals(linkrel, "../howto/") | |
627 | |
628 | |
629 def test_getLinkrelToUncle(self): | |
630 """ | |
631 If the doc directory is a sibling of the parent of the resource | |
632 directory, the linkrel should make use of multiple '..'s and a named | |
633 segment. | |
634 """ | |
635 linkrel = self.builder.getLinkrel(FilePath("/foo/howto"), | |
636 FilePath("/foo/examples/quotes")) | |
637 self.assertEquals(linkrel, "../../howto/") | |
638 | |
639 | |
640 | |
641 class APIBuilderTestCase(TestCase): | |
642 """ | |
643 Tests for L{APIBuilder}. | |
644 """ | |
645 if pydoctor is None or getattr(pydoctor, "version_info", (0,)) < (0, 1): | |
646 skip = "APIBuilder requires Pydoctor 0.1 or newer" | |
647 | |
648 def test_build(self): | |
649 """ | |
650 L{APIBuilder.build} writes an index file which includes the name of the | |
651 project specified. | |
652 """ | |
653 stdout = StringIO() | |
654 self.patch(sys, 'stdout', stdout) | |
655 | |
656 projectName = "Foobar" | |
657 packageName = "quux" | |
658 projectURL = "scheme:project" | |
659 sourceURL = "scheme:source" | |
660 docstring = "text in docstring" | |
661 badDocstring = "should not appear in output" | |
662 | |
663 inputPath = FilePath(self.mktemp()).child(packageName) | |
664 inputPath.makedirs() | |
665 inputPath.child("__init__.py").setContent( | |
666 "def foo():\n" | |
667 " '%s'\n" | |
668 "def _bar():\n" | |
669 " '%s'" % (docstring, badDocstring)) | |
670 | |
671 outputPath = FilePath(self.mktemp()) | |
672 outputPath.makedirs() | |
673 | |
674 builder = APIBuilder() | |
675 builder.build(projectName, projectURL, sourceURL, inputPath, outputPath) | |
676 | |
677 indexPath = outputPath.child("index.html") | |
678 self.assertTrue( | |
679 indexPath.exists(), | |
680 "API index %r did not exist." % (outputPath.path,)) | |
681 self.assertIn( | |
682 '<a href="%s">%s</a>' % (projectURL, projectName), | |
683 indexPath.getContent(), | |
684 "Project name/location not in file contents.") | |
685 | |
686 quuxPath = outputPath.child("quux.html") | |
687 self.assertTrue( | |
688 quuxPath.exists(), | |
689 "Package documentation file %r did not exist." % (quuxPath.path,)) | |
690 self.assertIn( | |
691 docstring, quuxPath.getContent(), | |
692 "Docstring not in package documentation file.") | |
693 self.assertIn( | |
694 '<a href="%s/%s">View Source</a>' % (sourceURL, packageName), | |
695 quuxPath.getContent()) | |
696 self.assertIn( | |
697 '<a href="%s/%s">View Source</a>' % (sourceURL, packageName), | |
698 quuxPath.getContent()) | |
699 self.assertIn( | |
700 '<a href="%s/%s/__init__.py#L1" class="functionSourceLink">' % ( | |
701 sourceURL, packageName), | |
702 quuxPath.getContent()) | |
703 self.assertNotIn(badDocstring, quuxPath.getContent()) | |
704 | |
705 self.assertEqual(stdout.getvalue(), '') | |
706 | |
707 | |
708 | |
709 class ManBuilderTestCase(TestCase, BuilderTestsMixin): | |
710 """ | |
711 Tests for L{ManBuilder}. | |
712 """ | |
713 | |
714 def setUp(self): | |
715 """ | |
716 Set up a few instance variables that will be useful. | |
717 | |
718 @ivar builder: A plain L{ManBuilder}. | |
719 @ivar manDir: A L{FilePath} representing a directory to be used for | |
720 containing man pages. | |
721 """ | |
722 BuilderTestsMixin.setUp(self) | |
723 self.builder = ManBuilder() | |
724 self.manDir = FilePath(self.mktemp()) | |
725 self.manDir.createDirectory() | |
726 | |
727 | |
728 def test_noDocumentsFound(self): | |
729 """ | |
730 L{ManBuilder.build} raises L{NoDocumentsFound} if there are no | |
731 .1 files in the given directory. | |
732 """ | |
733 self.assertRaises(NoDocumentsFound, self.builder.build, self.manDir) | |
734 | |
735 | |
736 def test_build(self): | |
737 """ | |
738 Check that L{ManBuilder.build} find the man page in the directory, and | |
739 successfully produce a Lore content. | |
740 """ | |
741 manContent = self.getArbitraryManInput() | |
742 self.manDir.child('test1.1').setContent(manContent) | |
743 self.builder.build(self.manDir) | |
744 output = self.manDir.child('test1-man.xhtml').getContent() | |
745 expected = self.getArbitraryManLoreOutput() | |
746 # No-op on *nix, fix for windows | |
747 expected = expected.replace('\n', os.linesep) | |
748 self.assertEquals(output, expected) | |
749 | |
750 | |
751 def test_toHTML(self): | |
752 """ | |
753 Check that the content output by C{build} is compatible as input of | |
754 L{DocBuilder.build}. | |
755 """ | |
756 manContent = self.getArbitraryManInput() | |
757 self.manDir.child('test1.1').setContent(manContent) | |
758 self.builder.build(self.manDir) | |
759 | |
760 templateFile = self.manDir.child("template.tpl") | |
761 templateFile.setContent(DocBuilderTestCase.template) | |
762 docBuilder = DocBuilder() | |
763 docBuilder.build("1.2.3", self.manDir, self.manDir, | |
764 templateFile) | |
765 output = self.manDir.child('test1-man.html').getContent() | |
766 self.assertEquals(output, '<?xml version="1.0"?><html><head>' | |
767 '<title>Yo:MANHOLE.1</title></head><body><div class="content">' | |
768 '<span></span><h2>NAME<a name="auto0"></a></h2><p>manhole - ' | |
769 'Connect to a Twisted Manhole service\n</p><h2>SYNOPSIS<a ' | |
770 'name="auto1"></a></h2><p><strong>manhole</strong></p><h2>' | |
771 'DESCRIPTION<a name="auto2"></a></h2><p>manhole is a GTK ' | |
772 'interface to Twisted Manhole services. You can execute ' | |
773 'python\ncode as if at an interactive Python console inside a ' | |
774 'running Twisted process\nwith this.</p></div><a ' | |
775 'href="index.html">Index</a><span class="version">Version: ' | |
776 '1.2.3</span></body></html>') | |
777 | |
778 | |
779 | |
780 class BookBuilderTests(TestCase, BuilderTestsMixin): | |
781 """ | |
782 Tests for L{BookBuilder}. | |
783 """ | |
784 if not (which("latex") and which("dvips") and which("ps2pdf13")): | |
785 skip = "Book Builder tests require latex." | |
786 try: | |
787 from popen2 import Popen4 | |
788 except ImportError: | |
789 skip = "Book Builder requires popen2.Popen4." | |
790 else: | |
791 del Popen4 | |
792 | |
793 def setUp(self): | |
794 """ | |
795 Make a directory into which to place temporary files. | |
796 """ | |
797 self.docCounter = 0 | |
798 self.howtoDir = FilePath(self.mktemp()) | |
799 self.howtoDir.makedirs() | |
800 | |
801 | |
802 def getArbitraryOutput(self, version, counter, prefix=""): | |
803 """ | |
804 Create and return a C{str} containing the LaTeX document which is | |
805 expected as the output for processing the result of the document | |
806 returned by C{self.getArbitraryLoreInput(counter)}. | |
807 """ | |
808 path = self.howtoDir.child("%d.xhtml" % (counter,)).path | |
809 path = path[len(os.getcwd()) + 1:] | |
810 return ( | |
811 r'\section{Hi! Title: %(count)s\label{%(path)s}}' | |
812 '\n' | |
813 r'Hi! %(count)sfoobar') % {'count': counter, 'path': path} | |
814 | |
815 | |
816 def test_runSuccess(self): | |
817 """ | |
818 L{BookBuilder.run} executes the command it is passed and returns a | |
819 string giving the stdout and stderr of the command if it completes | |
820 successfully. | |
821 """ | |
822 builder = BookBuilder() | |
823 self.assertEqual(builder.run("echo hi; echo bye 1>&2"), "hi\nbye\n") | |
824 | |
825 | |
826 def test_runFailed(self): | |
827 """ | |
828 L{BookBuilder.run} executes the command it is passed and raises | |
829 L{CommandFailed} if it completes unsuccessfully. | |
830 """ | |
831 builder = BookBuilder() | |
832 exc = self.assertRaises(CommandFailed, builder.run, "echo hi; false") | |
833 self.assertNotEqual(os.WEXITSTATUS(exc.exitCode), 0) | |
834 self.assertEqual(exc.output, "hi\n") | |
835 | |
836 | |
837 def test_runSignaled(self): | |
838 """ | |
839 L{BookBuilder.run} executes the command it is passed and raises | |
840 L{CommandFailed} if it exits due to a signal. | |
841 """ | |
842 builder = BookBuilder() | |
843 exc = self.assertRaises( | |
844 # This is only a little bit too tricky. | |
845 CommandFailed, builder.run, "echo hi; exec kill -9 $$") | |
846 self.assertTrue(os.WIFSIGNALED(exc.exitCode)) | |
847 self.assertEqual(os.WTERMSIG(exc.exitCode), signal.SIGKILL) | |
848 self.assertEqual(exc.output, "hi\n") | |
849 | |
850 | |
851 def test_buildTeX(self): | |
852 """ | |
853 L{BookBuilder.buildTeX} writes intermediate TeX files for all lore | |
854 input files in a directory. | |
855 """ | |
856 version = "3.2.1" | |
857 input1, output1 = self.getArbitraryLoreInputAndOutput(version) | |
858 input2, output2 = self.getArbitraryLoreInputAndOutput(version) | |
859 | |
860 # Filenames are chosen by getArbitraryOutput to match the counter used | |
861 # by getArbitraryLoreInputAndOutput. | |
862 self.howtoDir.child("1.xhtml").setContent(input1) | |
863 self.howtoDir.child("2.xhtml").setContent(input2) | |
864 | |
865 builder = BookBuilder() | |
866 builder.buildTeX(self.howtoDir) | |
867 self.assertEqual(self.howtoDir.child("1.tex").getContent(), output1) | |
868 self.assertEqual(self.howtoDir.child("2.tex").getContent(), output2) | |
869 | |
870 | |
871 def test_buildTeXRejectsInvalidDirectory(self): | |
872 """ | |
873 L{BookBuilder.buildTeX} raises L{ValueError} if passed a directory | |
874 which does not exist. | |
875 """ | |
876 builder = BookBuilder() | |
877 self.assertRaises( | |
878 ValueError, builder.buildTeX, self.howtoDir.temporarySibling()) | |
879 | |
880 | |
881 def test_buildTeXOnlyBuildsXHTML(self): | |
882 """ | |
883 L{BookBuilder.buildTeX} ignores files which which don't end with | |
884 ".xhtml". | |
885 """ | |
886 # Hopefully ">" is always a parse error from microdom! | |
887 self.howtoDir.child("not-input.dat").setContent(">") | |
888 self.test_buildTeX() | |
889 | |
890 | |
891 def test_stdout(self): | |
892 """ | |
893 L{BookBuilder.buildTeX} does not write to stdout. | |
894 """ | |
895 stdout = StringIO() | |
896 self.patch(sys, 'stdout', stdout) | |
897 | |
898 # Suppress warnings so that if there are any old-style plugins that | |
899 # lore queries for don't confuse the assertion below. See #3070. | |
900 self.patch(warnings, 'warn', lambda *a, **kw: None) | |
901 self.test_buildTeX() | |
902 self.assertEqual(stdout.getvalue(), '') | |
903 | |
904 | |
905 def test_buildPDFRejectsInvalidBookFilename(self): | |
906 """ | |
907 L{BookBuilder.buildPDF} raises L{ValueError} if the book filename does | |
908 not end with ".tex". | |
909 """ | |
910 builder = BookBuilder() | |
911 self.assertRaises( | |
912 ValueError, | |
913 builder.buildPDF, | |
914 FilePath(self.mktemp()).child("foo"), | |
915 None, | |
916 None) | |
917 | |
918 | |
919 def _setupTeXFiles(self): | |
920 sections = range(3) | |
921 self._setupTeXSections(sections) | |
922 return self._setupTeXBook(sections) | |
923 | |
924 | |
925 def _setupTeXSections(self, sections): | |
926 for texSectionNumber in sections: | |
927 texPath = self.howtoDir.child("%d.tex" % (texSectionNumber,)) | |
928 texPath.setContent(self.getArbitraryOutput( | |
929 "1.2.3", texSectionNumber)) | |
930 | |
931 | |
932 def _setupTeXBook(self, sections): | |
933 bookTeX = self.howtoDir.child("book.tex") | |
934 bookTeX.setContent( | |
935 r"\documentclass{book}" "\n" | |
936 r"\begin{document}" "\n" + | |
937 "\n".join([r"\input{%d.tex}" % (n,) for n in sections]) + | |
938 r"\end{document}" "\n") | |
939 return bookTeX | |
940 | |
941 | |
942 def test_buildPDF(self): | |
943 """ | |
944 L{BookBuilder.buildPDF} creates a PDF given an index tex file and a | |
945 directory containing .tex files. | |
946 """ | |
947 bookPath = self._setupTeXFiles() | |
948 outputPath = FilePath(self.mktemp()) | |
949 | |
950 builder = BookBuilder() | |
951 builder.buildPDF(bookPath, self.howtoDir, outputPath) | |
952 | |
953 self.assertTrue(outputPath.exists()) | |
954 | |
955 | |
956 def test_buildPDFLongPath(self): | |
957 """ | |
958 L{BookBuilder.buildPDF} succeeds even if the paths it is operating on | |
959 are very long. | |
960 | |
961 C{ps2pdf13} seems to have problems when path names are long. This test | |
962 verifies that even if inputs have long paths, generation still | |
963 succeeds. | |
964 """ | |
965 # Make it long. | |
966 self.howtoDir = self.howtoDir.child("x" * 128).child("x" * 128).child("x
" * 128) | |
967 self.howtoDir.makedirs() | |
968 | |
969 # This will use the above long path. | |
970 bookPath = self._setupTeXFiles() | |
971 outputPath = FilePath(self.mktemp()) | |
972 | |
973 builder = BookBuilder() | |
974 builder.buildPDF(bookPath, self.howtoDir, outputPath) | |
975 | |
976 self.assertTrue(outputPath.exists()) | |
977 | |
978 | |
979 def test_buildPDFRunsLaTeXThreeTimes(self): | |
980 """ | |
981 L{BookBuilder.buildPDF} runs C{latex} three times. | |
982 """ | |
983 class InspectableBookBuilder(BookBuilder): | |
984 def __init__(self): | |
985 BookBuilder.__init__(self) | |
986 self.commands = [] | |
987 | |
988 def run(self, command): | |
989 """ | |
990 Record the command and then execute it. | |
991 """ | |
992 self.commands.append(command) | |
993 return BookBuilder.run(self, command) | |
994 | |
995 bookPath = self._setupTeXFiles() | |
996 outputPath = FilePath(self.mktemp()) | |
997 | |
998 builder = InspectableBookBuilder() | |
999 builder.buildPDF(bookPath, self.howtoDir, outputPath) | |
1000 | |
1001 # These string comparisons are very fragile. It would be better to | |
1002 # have a test which asserted the correctness of the contents of the | |
1003 # output files. I don't know how one could do that, though. -exarkun | |
1004 latex1, latex2, latex3, dvips, ps2pdf13 = builder.commands | |
1005 self.assertEqual(latex1, latex2) | |
1006 self.assertEqual(latex2, latex3) | |
1007 self.assertTrue( | |
1008 latex1.startswith("latex "), | |
1009 "LaTeX command %r does not start with 'latex '" % (latex1,)) | |
1010 self.assertTrue( | |
1011 latex1.endswith(" " + bookPath.path), | |
1012 "LaTeX command %r does not end with the book path (%r)." % ( | |
1013 latex1, bookPath.path)) | |
1014 | |
1015 self.assertTrue( | |
1016 dvips.startswith("dvips "), | |
1017 "dvips command %r does not start with 'dvips '" % (dvips,)) | |
1018 self.assertTrue( | |
1019 ps2pdf13.startswith("ps2pdf13 "), | |
1020 "ps2pdf13 command %r does not start with 'ps2pdf13 '" % ( | |
1021 ps2pdf13,)) | |
1022 | |
1023 | |
1024 def test_noSideEffects(self): | |
1025 """ | |
1026 The working directory is the same before and after a call to | |
1027 L{BookBuilder.buildPDF}. Also the contents of the directory containing | |
1028 the input book are the same before and after the call. | |
1029 """ | |
1030 startDir = os.getcwd() | |
1031 bookTeX = self._setupTeXFiles() | |
1032 startTeXSiblings = bookTeX.parent().children() | |
1033 startHowtoChildren = self.howtoDir.children() | |
1034 | |
1035 builder = BookBuilder() | |
1036 builder.buildPDF(bookTeX, self.howtoDir, FilePath(self.mktemp())) | |
1037 | |
1038 self.assertEqual(startDir, os.getcwd()) | |
1039 self.assertEqual(startTeXSiblings, bookTeX.parent().children()) | |
1040 self.assertEqual(startHowtoChildren, self.howtoDir.children()) | |
1041 | |
1042 | |
1043 def test_failedCommandProvidesOutput(self): | |
1044 """ | |
1045 If a subprocess fails, L{BookBuilder.buildPDF} raises L{CommandFailed} | |
1046 with the subprocess's output and leaves the temporary directory as a | |
1047 sibling of the book path. | |
1048 """ | |
1049 bookTeX = FilePath(self.mktemp() + ".tex") | |
1050 builder = BookBuilder() | |
1051 inputState = bookTeX.parent().children() | |
1052 exc = self.assertRaises( | |
1053 CommandFailed, | |
1054 builder.buildPDF, | |
1055 bookTeX, self.howtoDir, FilePath(self.mktemp())) | |
1056 self.assertTrue(exc.output) | |
1057 newOutputState = set(bookTeX.parent().children()) - set(inputState) | |
1058 self.assertEqual(len(newOutputState), 1) | |
1059 workPath = newOutputState.pop() | |
1060 self.assertTrue( | |
1061 workPath.isdir(), | |
1062 "Expected work path %r was not a directory." % (workPath.path,)) | |
1063 | |
1064 | |
1065 def test_build(self): | |
1066 """ | |
1067 L{BookBuilder.build} generates a pdf book file from some lore input | |
1068 files. | |
1069 """ | |
1070 sections = range(1, 4) | |
1071 for sectionNumber in sections: | |
1072 self.howtoDir.child("%d.xhtml" % (sectionNumber,)).setContent( | |
1073 self.getArbitraryLoreInput(sectionNumber)) | |
1074 bookTeX = self._setupTeXBook(sections) | |
1075 bookPDF = FilePath(self.mktemp()) | |
1076 | |
1077 builder = BookBuilder() | |
1078 builder.build(self.howtoDir, [self.howtoDir], bookTeX, bookPDF) | |
1079 | |
1080 self.assertTrue(bookPDF.exists()) | |
1081 | |
1082 | |
1083 def test_buildRemovesTemporaryLaTeXFiles(self): | |
1084 """ | |
1085 L{BookBuilder.build} removes the intermediate LaTeX files it creates. | |
1086 """ | |
1087 sections = range(1, 4) | |
1088 for sectionNumber in sections: | |
1089 self.howtoDir.child("%d.xhtml" % (sectionNumber,)).setContent( | |
1090 self.getArbitraryLoreInput(sectionNumber)) | |
1091 bookTeX = self._setupTeXBook(sections) | |
1092 bookPDF = FilePath(self.mktemp()) | |
1093 | |
1094 builder = BookBuilder() | |
1095 builder.build(self.howtoDir, [self.howtoDir], bookTeX, bookPDF) | |
1096 | |
1097 self.assertEqual( | |
1098 set(self.howtoDir.listdir()), | |
1099 set([bookTeX.basename()] + ["%d.xhtml" % (n,) for n in sections])) | |
1100 | |
1101 | |
1102 | |
1103 class FilePathDeltaTest(TestCase): | |
1104 """ | |
1105 Tests for L{filePathDelta}. | |
1106 """ | |
1107 | |
1108 def test_filePathDeltaSubdir(self): | |
1109 """ | |
1110 L{filePathDelta} can create a simple relative path to a child path. | |
1111 """ | |
1112 self.assertEquals(filePathDelta(FilePath("/foo/bar"), | |
1113 FilePath("/foo/bar/baz")), | |
1114 ["baz"]) | |
1115 | |
1116 | |
1117 def test_filePathDeltaSiblingDir(self): | |
1118 """ | |
1119 L{filePathDelta} can traverse upwards to create relative paths to | |
1120 siblings. | |
1121 """ | |
1122 self.assertEquals(filePathDelta(FilePath("/foo/bar"), | |
1123 FilePath("/foo/baz")), | |
1124 ["..", "baz"]) | |
1125 | |
1126 | |
1127 def test_filePathNoCommonElements(self): | |
1128 """ | |
1129 L{filePathDelta} can create relative paths to totally unrelated paths | |
1130 for maximum portability. | |
1131 """ | |
1132 self.assertEquals(filePathDelta(FilePath("/foo/bar"), | |
1133 FilePath("/baz/quux")), | |
1134 ["..", "..", "baz", "quux"]) | |
1135 | |
1136 | |
1137 | |
1138 class DistributionBuilderTests(BuilderTestsMixin, TestCase): | |
1139 """ | |
1140 Tests for L{DistributionBuilder}. | |
1141 """ | |
1142 | |
1143 def setUp(self): | |
1144 BuilderTestsMixin.setUp(self) | |
1145 | |
1146 self.rootDir = FilePath(self.mktemp()) | |
1147 self.rootDir.createDirectory() | |
1148 | |
1149 outputDir = FilePath(self.mktemp()) | |
1150 outputDir.createDirectory() | |
1151 self.builder = DistributionBuilder(self.rootDir, outputDir) | |
1152 | |
1153 | |
1154 def createStructure(self, root, dirDict): | |
1155 """ | |
1156 Create a set of directories and files given a dict defining their | |
1157 structure. | |
1158 | |
1159 @param root: The directory in which to create the structure. | |
1160 @type root: L{FilePath} | |
1161 | |
1162 @param dirDict: The dict defining the structure. Keys should be strings | |
1163 naming files, values should be strings describing file contents OR | |
1164 dicts describing subdirectories. For example: C{{"foofile": | |
1165 "foocontents", "bardir": {"barfile": "barcontents"}}} | |
1166 @type dirDict: C{dict} | |
1167 """ | |
1168 for x in dirDict: | |
1169 child = root.child(x) | |
1170 if isinstance(dirDict[x], dict): | |
1171 child.createDirectory() | |
1172 self.createStructure(child, dirDict[x]) | |
1173 else: | |
1174 child.setContent(dirDict[x]) | |
1175 | |
1176 | |
1177 def assertExtractedStructure(self, outputFile, dirDict): | |
1178 """ | |
1179 Assert that a tarfile content is equivalent to one described by a dict. | |
1180 | |
1181 @param outputFile: The tar file built by L{DistributionBuilder}. | |
1182 @type outputFile: L{FilePath}. | |
1183 @param dirDict: The dict that should describe the contents of the | |
1184 directory. It should be the same structure as the C{dirDict} | |
1185 parameter to L{createStructure}. | |
1186 @type dirDict: C{dict} | |
1187 """ | |
1188 tarFile = tarfile.TarFile.open(outputFile.path, "r:bz2") | |
1189 extracted = FilePath(self.mktemp()) | |
1190 extracted.createDirectory() | |
1191 for info in tarFile: | |
1192 tarFile.extract(info, path=extracted.path) | |
1193 self.assertStructure(extracted.children()[0], dirDict) | |
1194 | |
1195 | |
1196 def assertStructure(self, root, dirDict): | |
1197 """ | |
1198 Assert that a directory is equivalent to one described by a dict. | |
1199 | |
1200 @param root: The filesystem directory to compare. | |
1201 @type root: L{FilePath} | |
1202 @param dirDict: The dict that should describe the contents of the | |
1203 directory. It should be the same structure as the C{dirDict} | |
1204 parameter to L{createStructure}. | |
1205 @type dirDict: C{dict} | |
1206 """ | |
1207 children = [x.basename() for x in root.children()] | |
1208 for x in dirDict: | |
1209 child = root.child(x) | |
1210 if isinstance(dirDict[x], dict): | |
1211 self.assertTrue(child.isdir(), "%s is not a dir!" | |
1212 % (child.path,)) | |
1213 self.assertStructure(child, dirDict[x]) | |
1214 else: | |
1215 a = child.getContent() | |
1216 self.assertEquals(a, dirDict[x], child.path) | |
1217 children.remove(x) | |
1218 if children: | |
1219 self.fail("There were extra children in %s: %s" | |
1220 % (root.path, children)) | |
1221 | |
1222 | |
1223 def test_twistedDistribution(self): | |
1224 """ | |
1225 The Twisted tarball contains everything in the source checkout, with | |
1226 built documentation. | |
1227 """ | |
1228 loreInput, loreOutput = self.getArbitraryLoreInputAndOutput("10.0.0") | |
1229 manInput1 = self.getArbitraryManInput() | |
1230 manOutput1 = self.getArbitraryManHTMLOutput("10.0.0", "../howto/") | |
1231 manInput2 = self.getArbitraryManInput() | |
1232 manOutput2 = self.getArbitraryManHTMLOutput("10.0.0", "../howto/") | |
1233 coreIndexInput, coreIndexOutput = self.getArbitraryLoreInputAndOutput( | |
1234 "10.0.0", prefix="howto/") | |
1235 | |
1236 structure = { | |
1237 "README": "Twisted", | |
1238 "unrelated": "x", | |
1239 "LICENSE": "copyright!", | |
1240 "setup.py": "import toplevel", | |
1241 "bin": {"web": {"websetroot": "SET ROOT"}, | |
1242 "twistd": "TWISTD"}, | |
1243 "twisted": | |
1244 {"web": | |
1245 {"__init__.py": "import WEB", | |
1246 "topfiles": {"setup.py": "import WEBINSTALL", | |
1247 "README": "WEB!"}}, | |
1248 "words": {"__init__.py": "import WORDS"}, | |
1249 "plugins": {"twisted_web.py": "import WEBPLUG", | |
1250 "twisted_words.py": "import WORDPLUG"}}, | |
1251 "doc": {"web": {"howto": {"index.xhtml": loreInput}, | |
1252 "man": {"websetroot.1": manInput2}}, | |
1253 "core": {"howto": {"template.tpl": self.template}, | |
1254 "man": {"twistd.1": manInput1}, | |
1255 "index.xhtml": coreIndexInput}}} | |
1256 | |
1257 outStructure = { | |
1258 "README": "Twisted", | |
1259 "unrelated": "x", | |
1260 "LICENSE": "copyright!", | |
1261 "setup.py": "import toplevel", | |
1262 "bin": {"web": {"websetroot": "SET ROOT"}, | |
1263 "twistd": "TWISTD"}, | |
1264 "twisted": | |
1265 {"web": {"__init__.py": "import WEB", | |
1266 "topfiles": {"setup.py": "import WEBINSTALL", | |
1267 "README": "WEB!"}}, | |
1268 "words": {"__init__.py": "import WORDS"}, | |
1269 "plugins": {"twisted_web.py": "import WEBPLUG", | |
1270 "twisted_words.py": "import WORDPLUG"}}, | |
1271 "doc": {"web": {"howto": {"index.html": loreOutput}, | |
1272 "man": {"websetroot.1": manInput2, | |
1273 "websetroot-man.html": manOutput2}}, | |
1274 "core": {"howto": {"template.tpl": self.template}, | |
1275 "man": {"twistd.1": manInput1, | |
1276 "twistd-man.html": manOutput1}, | |
1277 "index.html": coreIndexOutput}}} | |
1278 | |
1279 self.createStructure(self.rootDir, structure) | |
1280 | |
1281 outputFile = self.builder.buildTwisted("10.0.0") | |
1282 | |
1283 self.assertExtractedStructure(outputFile, outStructure) | |
1284 | |
1285 def test_twistedDistributionExcludesWeb2AndVFS(self): | |
1286 """ | |
1287 The main Twisted distribution does not include web2 or vfs. | |
1288 """ | |
1289 loreInput, loreOutput = self.getArbitraryLoreInputAndOutput("10.0.0") | |
1290 coreIndexInput, coreIndexOutput = self.getArbitraryLoreInputAndOutput( | |
1291 "10.0.0", prefix="howto/") | |
1292 | |
1293 structure = { | |
1294 "README": "Twisted", | |
1295 "unrelated": "x", | |
1296 "LICENSE": "copyright!", | |
1297 "setup.py": "import toplevel", | |
1298 "bin": {"web2": {"websetroot": "SET ROOT"}, | |
1299 "vfs": {"vfsitup": "hee hee"}, | |
1300 "twistd": "TWISTD"}, | |
1301 "twisted": | |
1302 {"web2": | |
1303 {"__init__.py": "import WEB", | |
1304 "topfiles": {"setup.py": "import WEBINSTALL", | |
1305 "README": "WEB!"}}, | |
1306 "vfs": | |
1307 {"__init__.py": "import VFS", | |
1308 "blah blah": "blah blah"}, | |
1309 "words": {"__init__.py": "import WORDS"}, | |
1310 "plugins": {"twisted_web.py": "import WEBPLUG", | |
1311 "twisted_words.py": "import WORDPLUG", | |
1312 "twisted_web2.py": "import WEB2", | |
1313 "twisted_vfs.py": "import VFS"}}, | |
1314 "doc": {"web2": {"excluded!": "yay"}, | |
1315 "vfs": {"unrelated": "whatever"}, | |
1316 "core": {"howto": {"template.tpl": self.template}, | |
1317 "index.xhtml": coreIndexInput}}} | |
1318 | |
1319 outStructure = { | |
1320 "README": "Twisted", | |
1321 "unrelated": "x", | |
1322 "LICENSE": "copyright!", | |
1323 "setup.py": "import toplevel", | |
1324 "bin": {"twistd": "TWISTD"}, | |
1325 "twisted": | |
1326 {"words": {"__init__.py": "import WORDS"}, | |
1327 "plugins": {"twisted_web.py": "import WEBPLUG", | |
1328 "twisted_words.py": "import WORDPLUG"}}, | |
1329 "doc": {"core": {"howto": {"template.tpl": self.template}, | |
1330 "index.html": coreIndexOutput}}} | |
1331 self.createStructure(self.rootDir, structure) | |
1332 | |
1333 outputFile = self.builder.buildTwisted("10.0.0") | |
1334 | |
1335 self.assertExtractedStructure(outputFile, outStructure) | |
1336 | |
1337 | |
1338 def test_subProjectLayout(self): | |
1339 """ | |
1340 The subproject tarball includes files like so: | |
1341 | |
1342 1. twisted/<subproject>/topfiles defines the files that will be in the | |
1343 top level in the tarball, except LICENSE, which comes from the real | |
1344 top-level directory. | |
1345 2. twisted/<subproject> is included, but without the topfiles entry | |
1346 in that directory. No other twisted subpackages are included. | |
1347 3. twisted/plugins/twisted_<subproject>.py is included, but nothing | |
1348 else in plugins is. | |
1349 """ | |
1350 structure = { | |
1351 "README": "HI!@", | |
1352 "unrelated": "x", | |
1353 "LICENSE": "copyright!", | |
1354 "setup.py": "import toplevel", | |
1355 "bin": {"web": {"websetroot": "SET ROOT"}, | |
1356 "words": {"im": "#!im"}}, | |
1357 "twisted": | |
1358 {"web": | |
1359 {"__init__.py": "import WEB", | |
1360 "topfiles": {"setup.py": "import WEBINSTALL", | |
1361 "README": "WEB!"}}, | |
1362 "words": {"__init__.py": "import WORDS"}, | |
1363 "plugins": {"twisted_web.py": "import WEBPLUG", | |
1364 "twisted_words.py": "import WORDPLUG"}}} | |
1365 | |
1366 outStructure = { | |
1367 "README": "WEB!", | |
1368 "LICENSE": "copyright!", | |
1369 "setup.py": "import WEBINSTALL", | |
1370 "bin": {"websetroot": "SET ROOT"}, | |
1371 "twisted": {"web": {"__init__.py": "import WEB"}, | |
1372 "plugins": {"twisted_web.py": "import WEBPLUG"}}} | |
1373 | |
1374 self.createStructure(self.rootDir, structure) | |
1375 | |
1376 outputFile = self.builder.buildSubProject("web", "0.3.0") | |
1377 | |
1378 self.assertExtractedStructure(outputFile, outStructure) | |
1379 | |
1380 | |
1381 def test_minimalSubProjectLayout(self): | |
1382 """ | |
1383 buildSubProject should work with minimal subprojects. | |
1384 """ | |
1385 structure = { | |
1386 "LICENSE": "copyright!", | |
1387 "bin": {}, | |
1388 "twisted": | |
1389 {"web": {"__init__.py": "import WEB", | |
1390 "topfiles": {"setup.py": "import WEBINSTALL"}}, | |
1391 "plugins": {}}} | |
1392 | |
1393 outStructure = { | |
1394 "setup.py": "import WEBINSTALL", | |
1395 "LICENSE": "copyright!", | |
1396 "twisted": {"web": {"__init__.py": "import WEB"}}} | |
1397 | |
1398 self.createStructure(self.rootDir, structure) | |
1399 | |
1400 outputFile = self.builder.buildSubProject("web", "0.3.0") | |
1401 | |
1402 self.assertExtractedStructure(outputFile, outStructure) | |
1403 | |
1404 | |
1405 def test_subProjectDocBuilding(self): | |
1406 """ | |
1407 When building a subproject release, documentation should be built with | |
1408 lore. | |
1409 """ | |
1410 loreInput, loreOutput = self.getArbitraryLoreInputAndOutput("0.3.0") | |
1411 manInput = self.getArbitraryManInput() | |
1412 manOutput = self.getArbitraryManHTMLOutput("0.3.0", "../howto/") | |
1413 structure = { | |
1414 "LICENSE": "copyright!", | |
1415 "twisted": {"web": {"__init__.py": "import WEB", | |
1416 "topfiles": {"setup.py": "import WEBINST"}}}, | |
1417 "doc": {"web": {"howto": {"index.xhtml": loreInput}, | |
1418 "man": {"twistd.1": manInput}}, | |
1419 "core": {"howto": {"template.tpl": self.template}} | |
1420 } | |
1421 } | |
1422 | |
1423 outStructure = { | |
1424 "LICENSE": "copyright!", | |
1425 "setup.py": "import WEBINST", | |
1426 "twisted": {"web": {"__init__.py": "import WEB"}}, | |
1427 "doc": {"howto": {"index.html": loreOutput}, | |
1428 "man": {"twistd.1": manInput, | |
1429 "twistd-man.html": manOutput}}} | |
1430 | |
1431 self.createStructure(self.rootDir, structure) | |
1432 | |
1433 outputFile = self.builder.buildSubProject("web", "0.3.0") | |
1434 | |
1435 self.assertExtractedStructure(outputFile, outStructure) | |
1436 | |
1437 | |
1438 def test_coreProjectLayout(self): | |
1439 """ | |
1440 The core tarball looks a lot like a subproject tarball, except it | |
1441 doesn't include: | |
1442 | |
1443 - Python packages from other subprojects | |
1444 - plugins from other subprojects | |
1445 - scripts from other subprojects | |
1446 """ | |
1447 indexInput, indexOutput = self.getArbitraryLoreInputAndOutput( | |
1448 "8.0.0", prefix="howto/") | |
1449 howtoInput, howtoOutput = self.getArbitraryLoreInputAndOutput("8.0.0") | |
1450 specInput, specOutput = self.getArbitraryLoreInputAndOutput( | |
1451 "8.0.0", prefix="../howto/") | |
1452 upgradeInput, upgradeOutput = self.getArbitraryLoreInputAndOutput( | |
1453 "8.0.0", prefix="../howto/") | |
1454 tutorialInput, tutorialOutput = self.getArbitraryLoreInputAndOutput( | |
1455 "8.0.0", prefix="../") | |
1456 | |
1457 structure = { | |
1458 "LICENSE": "copyright!", | |
1459 "twisted": {"__init__.py": "twisted", | |
1460 "python": {"__init__.py": "python", | |
1461 "roots.py": "roots!"}, | |
1462 "conch": {"__init__.py": "conch", | |
1463 "unrelated.py": "import conch"}, | |
1464 "plugin.py": "plugin", | |
1465 "plugins": {"twisted_web.py": "webplug", | |
1466 "twisted_whatever.py": "include!", | |
1467 "cred.py": "include!"}, | |
1468 "topfiles": {"setup.py": "import CORE", | |
1469 "README": "core readme"}}, | |
1470 "doc": {"core": {"howto": {"template.tpl": self.template, | |
1471 "index.xhtml": howtoInput, | |
1472 "tutorial": | |
1473 {"index.xhtml": tutorialInput}}, | |
1474 "specifications": {"index.xhtml": specInput}, | |
1475 "upgrades": {"index.xhtml": upgradeInput}, | |
1476 "examples": {"foo.py": "foo.py"}, | |
1477 "index.xhtml": indexInput}, | |
1478 "web": {"howto": {"index.xhtml": "webindex"}}}, | |
1479 "bin": {"twistd": "TWISTD", | |
1480 "web": {"websetroot": "websetroot"}} | |
1481 } | |
1482 | |
1483 outStructure = { | |
1484 "LICENSE": "copyright!", | |
1485 "setup.py": "import CORE", | |
1486 "README": "core readme", | |
1487 "twisted": {"__init__.py": "twisted", | |
1488 "python": {"__init__.py": "python", | |
1489 "roots.py": "roots!"}, | |
1490 "plugin.py": "plugin", | |
1491 "plugins": {"twisted_whatever.py": "include!", | |
1492 "cred.py": "include!"}}, | |
1493 "doc": {"howto": {"template.tpl": self.template, | |
1494 "index.html": howtoOutput, | |
1495 "tutorial": {"index.html": tutorialOutput}}, | |
1496 "specifications": {"index.html": specOutput}, | |
1497 "upgrades": {"index.html": upgradeOutput}, | |
1498 "examples": {"foo.py": "foo.py"}, | |
1499 "index.html": indexOutput}, | |
1500 "bin": {"twistd": "TWISTD"}, | |
1501 } | |
1502 | |
1503 self.createStructure(self.rootDir, structure) | |
1504 | |
1505 outputFile = self.builder.buildCore("8.0.0") | |
1506 | |
1507 self.assertExtractedStructure(outputFile, outStructure) | |
1508 | |
1509 | |
1510 | |
1511 if lore is None: | |
1512 skipMessage = "Lore is not present." | |
1513 BookBuilderTests.skip = skipMessage | |
1514 DocBuilderTestCase.skip = skipMessage | |
1515 ManBuilderTestCase.skip = skipMessage | |
1516 DistributionBuilderTests.skip = skipMessage | |
OLD | NEW |