Initial import to Tizen
[profile/ivi/python-twisted.git] / twisted / python / test / test_release.py
1 # Copyright (c) Twisted Matrix Laboratories.
2 # See LICENSE for details.
3
4 """
5 Tests for L{twisted.python.release} and L{twisted.python._release}.
6
7 All of these tests are skipped on platforms other than Linux, as the release is
8 only ever performed on Linux.
9 """
10
11
12 import warnings
13 import operator
14 import os, sys, signal
15 from StringIO import StringIO
16 import tarfile
17 from xml.dom import minidom as dom
18
19 from datetime import date
20
21 from twisted.trial.unittest import TestCase
22
23 from twisted.python.compat import set
24 from twisted.python.procutils import which
25 from twisted.python import release
26 from twisted.python.filepath import FilePath
27 from twisted.python.versions import Version
28 from twisted.python._release import _changeVersionInFile, getNextVersion
29 from twisted.python._release import findTwistedProjects, replaceInFile
30 from twisted.python._release import replaceProjectVersion
31 from twisted.python._release import updateTwistedVersionInformation, Project
32 from twisted.python._release import generateVersionFileData
33 from twisted.python._release import changeAllProjectVersions
34 from twisted.python._release import VERSION_OFFSET, DocBuilder, ManBuilder
35 from twisted.python._release import NoDocumentsFound, filePathDelta
36 from twisted.python._release import CommandFailed, BookBuilder
37 from twisted.python._release import DistributionBuilder, APIBuilder
38 from twisted.python._release import BuildAPIDocsScript
39 from twisted.python._release import buildAllTarballs, runCommand
40 from twisted.python._release import UncleanWorkingDirectory, NotWorkingDirectory
41 from twisted.python._release import ChangeVersionsScript, BuildTarballsScript
42 from twisted.python._release import NewsBuilder
43
44 if os.name != 'posix':
45     skip = "Release toolchain only supported on POSIX."
46 else:
47     skip = None
48
49
50 # Check a bunch of dependencies to skip tests if necessary.
51 try:
52     from twisted.lore.scripts import lore
53 except ImportError:
54     loreSkip = "Lore is not present."
55 else:
56     loreSkip = skip
57
58
59 try:
60     import pydoctor.driver
61     # it might not be installed, or it might use syntax not available in
62     # this version of Python.
63 except (ImportError, SyntaxError):
64     pydoctorSkip = "Pydoctor is not present."
65 else:
66     if getattr(pydoctor, "version_info", (0,)) < (0, 1):
67         pydoctorSkip = "Pydoctor is too old."
68     else:
69         pydoctorSkip = skip
70
71
72 if which("latex") and which("dvips") and which("ps2pdf13"):
73     latexSkip = skip
74 else:
75     latexSkip = "LaTeX is not available."
76
77
78 if which("svn") and which("svnadmin"):
79     svnSkip = skip
80 else:
81     svnSkip = "svn or svnadmin is not present."
82
83
84 def genVersion(*args, **kwargs):
85     """
86     A convenience for generating _version.py data.
87
88     @param args: Arguments to pass to L{Version}.
89     @param kwargs: Keyword arguments to pass to L{Version}.
90     """
91     return generateVersionFileData(Version(*args, **kwargs))
92
93
94
95 class StructureAssertingMixin(object):
96     """
97     A mixin for L{TestCase} subclasses which provides some methods for asserting
98     the structure and contents of directories and files on the filesystem.
99     """
100     def createStructure(self, root, dirDict):
101         """
102         Create a set of directories and files given a dict defining their
103         structure.
104
105         @param root: The directory in which to create the structure.  It must
106             already exist.
107         @type root: L{FilePath}
108
109         @param dirDict: The dict defining the structure. Keys should be strings
110             naming files, values should be strings describing file contents OR
111             dicts describing subdirectories.  All files are written in binary
112             mode.  Any string values are assumed to describe text files and
113             will have their newlines replaced with the platform-native newline
114             convention.  For example::
115
116                 {"foofile": "foocontents",
117                  "bardir": {"barfile": "bar\ncontents"}}
118         @type dirDict: C{dict}
119         """
120         for x in dirDict:
121             child = root.child(x)
122             if isinstance(dirDict[x], dict):
123                 child.createDirectory()
124                 self.createStructure(child, dirDict[x])
125             else:
126                 child.setContent(dirDict[x].replace('\n', os.linesep))
127
128     def assertStructure(self, root, dirDict):
129         """
130         Assert that a directory is equivalent to one described by a dict.
131
132         @param root: The filesystem directory to compare.
133         @type root: L{FilePath}
134         @param dirDict: The dict that should describe the contents of the
135             directory. It should be the same structure as the C{dirDict}
136             parameter to L{createStructure}.
137         @type dirDict: C{dict}
138         """
139         children = [x.basename() for x in root.children()]
140         for x in dirDict:
141             child = root.child(x)
142             if isinstance(dirDict[x], dict):
143                 self.assertTrue(child.isdir(), "%s is not a dir!"
144                                 % (child.path,))
145                 self.assertStructure(child, dirDict[x])
146             else:
147                 a = child.getContent().replace(os.linesep, '\n')
148                 self.assertEqual(a, dirDict[x], child.path)
149             children.remove(x)
150         if children:
151             self.fail("There were extra children in %s: %s"
152                       % (root.path, children))
153
154
155     def assertExtractedStructure(self, outputFile, dirDict):
156         """
157         Assert that a tarfile content is equivalent to one described by a dict.
158
159         @param outputFile: The tar file built by L{DistributionBuilder}.
160         @type outputFile: L{FilePath}.
161         @param dirDict: The dict that should describe the contents of the
162             directory. It should be the same structure as the C{dirDict}
163             parameter to L{createStructure}.
164         @type dirDict: C{dict}
165         """
166         tarFile = tarfile.TarFile.open(outputFile.path, "r:bz2")
167         extracted = FilePath(self.mktemp())
168         extracted.createDirectory()
169         for info in tarFile:
170             tarFile.extract(info, path=extracted.path)
171         self.assertStructure(extracted.children()[0], dirDict)
172
173
174
175 class ChangeVersionTest(TestCase, StructureAssertingMixin):
176     """
177     Twisted has the ability to change versions.
178     """
179
180     def makeFile(self, relativePath, content):
181         """
182         Create a file with the given content relative to a temporary directory.
183
184         @param relativePath: The basename of the file to create.
185         @param content: The content that the file will have.
186         @return: The filename.
187         """
188         baseDirectory = FilePath(self.mktemp())
189         directory, filename = os.path.split(relativePath)
190         directory = baseDirectory.preauthChild(directory)
191         directory.makedirs()
192         file = directory.child(filename)
193         directory.child(filename).setContent(content)
194         return file
195
196
197     def test_getNextVersion(self):
198         """
199         When calculating the next version to release when a release is
200         happening in the same year as the last release, the minor version
201         number is incremented.
202         """
203         now = date.today()
204         major = now.year - VERSION_OFFSET
205         version = Version("twisted", major, 9, 0)
206         self.assertEqual(getNextVersion(version, now=now),
207                           Version("twisted", major, 10, 0))
208
209
210     def test_getNextVersionAfterYearChange(self):
211         """
212         When calculating the next version to release when a release is
213         happening in a later year, the minor version number is reset to 0.
214         """
215         now = date.today()
216         major = now.year - VERSION_OFFSET
217         version = Version("twisted", major - 1, 9, 0)
218         self.assertEqual(getNextVersion(version, now=now),
219                           Version("twisted", major, 0, 0))
220
221
222     def test_changeVersionInFile(self):
223         """
224         _changeVersionInFile replaces the old version information in a file
225         with the given new version information.
226         """
227         # The version numbers are arbitrary, the name is only kind of
228         # arbitrary.
229         packageName = 'foo'
230         oldVersion = Version(packageName, 2, 5, 0)
231         file = self.makeFile('README',
232                              "Hello and welcome to %s." % oldVersion.base())
233
234         newVersion = Version(packageName, 7, 6, 0)
235         _changeVersionInFile(oldVersion, newVersion, file.path)
236
237         self.assertEqual(file.getContent(),
238                          "Hello and welcome to %s." % newVersion.base())
239
240
241     def test_changeAllProjectVersions(self):
242         """
243         L{changeAllProjectVersions} changes all version numbers in _version.py
244         and README files for all projects as well as in the the top-level
245         README file.
246         """
247         root = FilePath(self.mktemp())
248         root.createDirectory()
249         structure = {
250             "README": "Hi this is 1.0.0.",
251             "twisted": {
252                 "topfiles": {
253                     "README": "Hi this is 1.0.0"},
254                 "_version.py":
255                     genVersion("twisted", 1, 0, 0),
256                 "web": {
257                     "topfiles": {
258                         "README": "Hi this is 1.0.0"},
259                     "_version.py": genVersion("twisted.web", 1, 0, 0)
260                     }}}
261         self.createStructure(root, structure)
262         changeAllProjectVersions(root, Version("lol", 1, 0, 2))
263         outStructure = {
264             "README": "Hi this is 1.0.2.",
265             "twisted": {
266                 "topfiles": {
267                     "README": "Hi this is 1.0.2"},
268                 "_version.py":
269                     genVersion("twisted", 1, 0, 2),
270                 "web": {
271                     "topfiles": {
272                         "README": "Hi this is 1.0.2"},
273                     "_version.py": genVersion("twisted.web", 1, 0, 2),
274                     }}}
275         self.assertStructure(root, outStructure)
276
277
278     def test_changeAllProjectVersionsPreRelease(self):
279         """
280         L{changeAllProjectVersions} changes all version numbers in _version.py
281         and README files for all projects as well as in the the top-level
282         README file. If the old version was a pre-release, it will change the
283         version in NEWS files as well.
284         """
285         root = FilePath(self.mktemp())
286         root.createDirectory()
287         coreNews = ("Twisted Core 1.0.0 (2009-12-25)\n"
288                     "===============================\n"
289                     "\n")
290         webNews = ("Twisted Web 1.0.0pre1 (2009-12-25)\n"
291                    "==================================\n"
292                    "\n")
293         structure = {
294             "README": "Hi this is 1.0.0.",
295             "NEWS": coreNews + webNews,
296             "twisted": {
297                 "topfiles": {
298                     "README": "Hi this is 1.0.0",
299                     "NEWS": coreNews},
300                 "_version.py":
301                     genVersion("twisted", 1, 0, 0),
302                 "web": {
303                     "topfiles": {
304                         "README": "Hi this is 1.0.0pre1",
305                         "NEWS": webNews},
306                     "_version.py": genVersion("twisted.web", 1, 0, 0, 1)
307                     }}}
308         self.createStructure(root, structure)
309         changeAllProjectVersions(root, Version("lol", 1, 0, 2), '2010-01-01')
310         coreNews = (
311             "Twisted Core 1.0.0 (2009-12-25)\n"
312             "===============================\n"
313             "\n")
314         webNews = ("Twisted Web 1.0.2 (2010-01-01)\n"
315                    "==============================\n"
316                    "\n")
317         outStructure = {
318             "README": "Hi this is 1.0.2.",
319             "NEWS": coreNews + webNews,
320             "twisted": {
321                 "topfiles": {
322                     "README": "Hi this is 1.0.2",
323                     "NEWS": coreNews},
324                 "_version.py":
325                     genVersion("twisted", 1, 0, 2),
326                 "web": {
327                     "topfiles": {
328                         "README": "Hi this is 1.0.2",
329                         "NEWS": webNews},
330                     "_version.py": genVersion("twisted.web", 1, 0, 2),
331                     }}}
332         self.assertStructure(root, outStructure)
333
334
335
336 class ProjectTest(TestCase):
337     """
338     There is a first-class representation of a project.
339     """
340
341     def assertProjectsEqual(self, observedProjects, expectedProjects):
342         """
343         Assert that two lists of L{Project}s are equal.
344         """
345         self.assertEqual(len(observedProjects), len(expectedProjects))
346         observedProjects = sorted(observedProjects,
347                                   key=operator.attrgetter('directory'))
348         expectedProjects = sorted(expectedProjects,
349                                   key=operator.attrgetter('directory'))
350         for observed, expected in zip(observedProjects, expectedProjects):
351             self.assertEqual(observed.directory, expected.directory)
352
353
354     def makeProject(self, version, baseDirectory=None):
355         """
356         Make a Twisted-style project in the given base directory.
357
358         @param baseDirectory: The directory to create files in
359             (as a L{FilePath).
360         @param version: The version information for the project.
361         @return: L{Project} pointing to the created project.
362         """
363         if baseDirectory is None:
364             baseDirectory = FilePath(self.mktemp())
365             baseDirectory.createDirectory()
366         segments = version.package.split('.')
367         directory = baseDirectory
368         for segment in segments:
369             directory = directory.child(segment)
370             if not directory.exists():
371                 directory.createDirectory()
372             directory.child('__init__.py').setContent('')
373         directory.child('topfiles').createDirectory()
374         directory.child('topfiles').child('README').setContent(version.base())
375         replaceProjectVersion(
376             directory.child('_version.py').path, version)
377         return Project(directory)
378
379
380     def makeProjects(self, *versions):
381         """
382         Create a series of projects underneath a temporary base directory.
383
384         @return: A L{FilePath} for the base directory.
385         """
386         baseDirectory = FilePath(self.mktemp())
387         baseDirectory.createDirectory()
388         for version in versions:
389             self.makeProject(version, baseDirectory)
390         return baseDirectory
391
392
393     def test_getVersion(self):
394         """
395         Project objects know their version.
396         """
397         version = Version('foo', 2, 1, 0)
398         project = self.makeProject(version)
399         self.assertEqual(project.getVersion(), version)
400
401
402     def test_updateVersion(self):
403         """
404         Project objects know how to update the version numbers in those
405         projects.
406         """
407         project = self.makeProject(Version("bar", 2, 1, 0))
408         newVersion = Version("bar", 3, 2, 9)
409         project.updateVersion(newVersion)
410         self.assertEqual(project.getVersion(), newVersion)
411         self.assertEqual(
412             project.directory.child("topfiles").child("README").getContent(),
413             "3.2.9")
414
415
416     def test_repr(self):
417         """
418         The representation of a Project is Project(directory).
419         """
420         foo = Project(FilePath('bar'))
421         self.assertEqual(
422             repr(foo), 'Project(%r)' % (foo.directory))
423
424
425     def test_findTwistedStyleProjects(self):
426         """
427         findTwistedStyleProjects finds all projects underneath a particular
428         directory. A 'project' is defined by the existence of a 'topfiles'
429         directory and is returned as a Project object.
430         """
431         baseDirectory = self.makeProjects(
432             Version('foo', 2, 3, 0), Version('foo.bar', 0, 7, 4))
433         projects = findTwistedProjects(baseDirectory)
434         self.assertProjectsEqual(
435             projects,
436             [Project(baseDirectory.child('foo')),
437              Project(baseDirectory.child('foo').child('bar'))])
438
439
440     def test_updateTwistedVersionInformation(self):
441         """
442         Update Twisted version information in the top-level project and all of
443         the subprojects.
444         """
445         baseDirectory = FilePath(self.mktemp())
446         baseDirectory.createDirectory()
447         now = date.today()
448
449         projectName = 'foo'
450         oldVersion = Version(projectName, 2, 5, 0)
451         newVersion = getNextVersion(oldVersion, now=now)
452
453         project = self.makeProject(oldVersion, baseDirectory)
454
455         updateTwistedVersionInformation(baseDirectory, now=now)
456
457         self.assertEqual(project.getVersion(), newVersion)
458         self.assertEqual(
459             project.directory.child('topfiles').child('README').getContent(),
460             newVersion.base())
461
462
463
464 class UtilityTest(TestCase):
465     """
466     Tests for various utility functions for releasing.
467     """
468
469     def test_chdir(self):
470         """
471         Test that the runChdirSafe is actually safe, i.e., it still
472         changes back to the original directory even if an error is
473         raised.
474         """
475         cwd = os.getcwd()
476         def chAndBreak():
477             os.mkdir('releaseCh')
478             os.chdir('releaseCh')
479             1/0
480         self.assertRaises(ZeroDivisionError,
481                           release.runChdirSafe, chAndBreak)
482         self.assertEqual(cwd, os.getcwd())
483
484
485
486     def test_replaceInFile(self):
487         """
488         L{replaceInFile} replaces data in a file based on a dict. A key from
489         the dict that is found in the file is replaced with the corresponding
490         value.
491         """
492         in_ = 'foo\nhey hey $VER\nbar\n'
493         outf = open('release.replace', 'w')
494         outf.write(in_)
495         outf.close()
496
497         expected = in_.replace('$VER', '2.0.0')
498         replaceInFile('release.replace', {'$VER': '2.0.0'})
499         self.assertEqual(open('release.replace').read(), expected)
500
501
502         expected = expected.replace('2.0.0', '3.0.0')
503         replaceInFile('release.replace', {'2.0.0': '3.0.0'})
504         self.assertEqual(open('release.replace').read(), expected)
505
506
507
508 class VersionWritingTest(TestCase):
509     """
510     Tests for L{replaceProjectVersion}.
511     """
512
513     def test_replaceProjectVersion(self):
514         """
515         L{replaceProjectVersion} writes a Python file that defines a
516         C{version} variable that corresponds to the given name and version
517         number.
518         """
519         replaceProjectVersion("test_project",
520                               Version("twisted.test_project", 0, 82, 7))
521         ns = {'__name___': 'twisted.test_project'}
522         execfile("test_project", ns)
523         self.assertEqual(ns["version"].base(), "0.82.7")
524
525
526     def test_replaceProjectVersionWithPrerelease(self):
527         """
528         L{replaceProjectVersion} will write a Version instantiation that
529         includes a prerelease parameter if necessary.
530         """
531         replaceProjectVersion("test_project",
532                               Version("twisted.test_project", 0, 82, 7,
533                                       prerelease=8))
534         ns = {'__name___': 'twisted.test_project'}
535         execfile("test_project", ns)
536         self.assertEqual(ns["version"].base(), "0.82.7pre8")
537
538
539
540 class BuilderTestsMixin(object):
541     """
542     A mixin class which provides various methods for creating sample Lore input
543     and output.
544
545     @cvar template: The lore template that will be used to prepare sample
546     output.
547     @type template: C{str}
548
549     @ivar docCounter: A counter which is incremented every time input is
550         generated and which is included in the documents.
551     @type docCounter: C{int}
552     """
553     template = '''
554     <html>
555     <head><title>Yo:</title></head>
556     <body>
557     <div class="body" />
558     <a href="index.html">Index</a>
559     <span class="version">Version: </span>
560     </body>
561     </html>
562     '''
563
564     def setUp(self):
565         """
566         Initialize the doc counter which ensures documents are unique.
567         """
568         self.docCounter = 0
569
570
571     def assertXMLEqual(self, first, second):
572         """
573         Verify that two strings represent the same XML document.
574         """
575         self.assertEqual(
576             dom.parseString(first).toxml(),
577             dom.parseString(second).toxml())
578
579
580     def getArbitraryOutput(self, version, counter, prefix="", apiBaseURL="%s"):
581         """
582         Get the correct HTML output for the arbitrary input returned by
583         L{getArbitraryLoreInput} for the given parameters.
584
585         @param version: The version string to include in the output.
586         @type version: C{str}
587         @param counter: A counter to include in the output.
588         @type counter: C{int}
589         """
590         document = """\
591 <?xml version="1.0"?><html>
592     <head><title>Yo:Hi! Title: %(count)d</title></head>
593     <body>
594     <div class="content">Hi! %(count)d<div class="API"><a href="%(foobarLink)s" title="foobar">foobar</a></div></div>
595     <a href="%(prefix)sindex.html">Index</a>
596     <span class="version">Version: %(version)s</span>
597     </body>
598     </html>"""
599         # Try to normalize irrelevant whitespace.
600         return dom.parseString(
601             document % {"count": counter, "prefix": prefix,
602                         "version": version,
603                         "foobarLink": apiBaseURL % ("foobar",)}).toxml('utf-8')
604
605
606     def getArbitraryLoreInput(self, counter):
607         """
608         Get an arbitrary, unique (for this test case) string of lore input.
609
610         @param counter: A counter to include in the input.
611         @type counter: C{int}
612         """
613         template = (
614             '<html>'
615             '<head><title>Hi! Title: %(count)s</title></head>'
616             '<body>'
617             'Hi! %(count)s'
618             '<div class="API">foobar</div>'
619             '</body>'
620             '</html>')
621         return template % {"count": counter}
622
623
624     def getArbitraryLoreInputAndOutput(self, version, prefix="",
625                                        apiBaseURL="%s"):
626         """
627         Get an input document along with expected output for lore run on that
628         output document, assuming an appropriately-specified C{self.template}.
629
630         @param version: A version string to include in the input and output.
631         @type version: C{str}
632         @param prefix: The prefix to include in the link to the index.
633         @type prefix: C{str}
634
635         @return: A two-tuple of input and expected output.
636         @rtype: C{(str, str)}.
637         """
638         self.docCounter += 1
639         return (self.getArbitraryLoreInput(self.docCounter),
640                 self.getArbitraryOutput(version, self.docCounter,
641                                         prefix=prefix, apiBaseURL=apiBaseURL))
642
643
644     def getArbitraryManInput(self):
645         """
646         Get an arbitrary man page content.
647         """
648         return """.TH MANHOLE "1" "August 2001" "" ""
649 .SH NAME
650 manhole \- Connect to a Twisted Manhole service
651 .SH SYNOPSIS
652 .B manhole
653 .SH DESCRIPTION
654 manhole is a GTK interface to Twisted Manhole services. You can execute python
655 code as if at an interactive Python console inside a running Twisted process
656 with this."""
657
658
659     def getArbitraryManLoreOutput(self):
660         """
661         Get an arbitrary lore input document which represents man-to-lore
662         output based on the man page returned from L{getArbitraryManInput}
663         """
664         return """\
665 <?xml version="1.0"?>
666 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
667     "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
668 <html><head>
669 <title>MANHOLE.1</title></head>
670 <body>
671
672 <h1>MANHOLE.1</h1>
673
674 <h2>NAME</h2>
675
676 <p>manhole - Connect to a Twisted Manhole service
677 </p>
678
679 <h2>SYNOPSIS</h2>
680
681 <p><strong>manhole</strong> </p>
682
683 <h2>DESCRIPTION</h2>
684
685 <p>manhole is a GTK interface to Twisted Manhole services. You can execute python
686 code as if at an interactive Python console inside a running Twisted process
687 with this.</p>
688
689 </body>
690 </html>
691 """
692
693     def getArbitraryManHTMLOutput(self, version, prefix=""):
694         """
695         Get an arbitrary lore output document which represents the lore HTML
696         output based on the input document returned from
697         L{getArbitraryManLoreOutput}.
698
699         @param version: A version string to include in the document.
700         @type version: C{str}
701         @param prefix: The prefix to include in the link to the index.
702         @type prefix: C{str}
703         """
704         # Try to normalize the XML a little bit.
705         return dom.parseString("""\
706 <?xml version="1.0" ?><html>
707     <head><title>Yo:MANHOLE.1</title></head>
708     <body>
709     <div class="content">
710
711 <span/>
712
713 <h2>NAME<a name="auto0"/></h2>
714
715 <p>manhole - Connect to a Twisted Manhole service
716 </p>
717
718 <h2>SYNOPSIS<a name="auto1"/></h2>
719
720 <p><strong>manhole</strong> </p>
721
722 <h2>DESCRIPTION<a name="auto2"/></h2>
723
724 <p>manhole is a GTK interface to Twisted Manhole services. You can execute python
725 code as if at an interactive Python console inside a running Twisted process
726 with this.</p>
727
728 </div>
729     <a href="%(prefix)sindex.html">Index</a>
730     <span class="version">Version: %(version)s</span>
731     </body>
732     </html>""" % {
733             'prefix': prefix, 'version': version}).toxml("utf-8")
734
735
736
737 class DocBuilderTestCase(TestCase, BuilderTestsMixin):
738     """
739     Tests for L{DocBuilder}.
740
741     Note for future maintainers: The exact byte equality assertions throughout
742     this suite may need to be updated due to minor differences in lore. They
743     should not be taken to mean that Lore must maintain the same byte format
744     forever. Feel free to update the tests when Lore changes, but please be
745     careful.
746     """
747     skip = loreSkip
748
749     def setUp(self):
750         """
751         Set up a few instance variables that will be useful.
752
753         @ivar builder: A plain L{DocBuilder}.
754         @ivar docCounter: An integer to be used as a counter by the
755             C{getArbitrary...} methods.
756         @ivar howtoDir: A L{FilePath} representing a directory to be used for
757             containing Lore documents.
758         @ivar templateFile: A L{FilePath} representing a file with
759             C{self.template} as its content.
760         """
761         BuilderTestsMixin.setUp(self)
762         self.builder = DocBuilder()
763         self.howtoDir = FilePath(self.mktemp())
764         self.howtoDir.createDirectory()
765         self.templateFile = self.howtoDir.child("template.tpl")
766         self.templateFile.setContent(self.template)
767
768
769     def test_build(self):
770         """
771         The L{DocBuilder} runs lore on all .xhtml files within a directory.
772         """
773         version = "1.2.3"
774         input1, output1 = self.getArbitraryLoreInputAndOutput(version)
775         input2, output2 = self.getArbitraryLoreInputAndOutput(version)
776
777         self.howtoDir.child("one.xhtml").setContent(input1)
778         self.howtoDir.child("two.xhtml").setContent(input2)
779
780         self.builder.build(version, self.howtoDir, self.howtoDir,
781                            self.templateFile)
782         out1 = self.howtoDir.child('one.html')
783         out2 = self.howtoDir.child('two.html')
784         self.assertXMLEqual(out1.getContent(), output1)
785         self.assertXMLEqual(out2.getContent(), output2)
786
787
788     def test_noDocumentsFound(self):
789         """
790         The C{build} method raises L{NoDocumentsFound} if there are no
791         .xhtml files in the given directory.
792         """
793         self.assertRaises(
794             NoDocumentsFound,
795             self.builder.build, "1.2.3", self.howtoDir, self.howtoDir,
796             self.templateFile)
797
798
799     def test_parentDocumentLinking(self):
800         """
801         The L{DocBuilder} generates correct links from documents to
802         template-generated links like stylesheets and index backreferences.
803         """
804         input = self.getArbitraryLoreInput(0)
805         tutoDir = self.howtoDir.child("tutorial")
806         tutoDir.createDirectory()
807         tutoDir.child("child.xhtml").setContent(input)
808         self.builder.build("1.2.3", self.howtoDir, tutoDir, self.templateFile)
809         outFile = tutoDir.child('child.html')
810         self.assertIn('<a href="../index.html">Index</a>',
811                       outFile.getContent())
812
813
814     def test_siblingDirectoryDocumentLinking(self):
815         """
816         It is necessary to generate documentation in a directory foo/bar where
817         stylesheet and indexes are located in foo/baz. Such resources should be
818         appropriately linked to.
819         """
820         input = self.getArbitraryLoreInput(0)
821         resourceDir = self.howtoDir.child("resources")
822         docDir = self.howtoDir.child("docs")
823         docDir.createDirectory()
824         docDir.child("child.xhtml").setContent(input)
825         self.builder.build("1.2.3", resourceDir, docDir, self.templateFile)
826         outFile = docDir.child('child.html')
827         self.assertIn('<a href="../resources/index.html">Index</a>',
828                       outFile.getContent())
829
830
831     def test_apiLinking(self):
832         """
833         The L{DocBuilder} generates correct links from documents to API
834         documentation.
835         """
836         version = "1.2.3"
837         input, output = self.getArbitraryLoreInputAndOutput(version)
838         self.howtoDir.child("one.xhtml").setContent(input)
839
840         self.builder.build(version, self.howtoDir, self.howtoDir,
841                            self.templateFile, "scheme:apilinks/%s.ext")
842         out = self.howtoDir.child('one.html')
843         self.assertIn(
844             '<a href="scheme:apilinks/foobar.ext" title="foobar">foobar</a>',
845             out.getContent())
846
847
848     def test_deleteInput(self):
849         """
850         L{DocBuilder.build} can be instructed to delete the input files after
851         generating the output based on them.
852         """
853         input1 = self.getArbitraryLoreInput(0)
854         self.howtoDir.child("one.xhtml").setContent(input1)
855         self.builder.build("whatever", self.howtoDir, self.howtoDir,
856                            self.templateFile, deleteInput=True)
857         self.assertTrue(self.howtoDir.child('one.html').exists())
858         self.assertFalse(self.howtoDir.child('one.xhtml').exists())
859
860
861     def test_doNotDeleteInput(self):
862         """
863         Input will not be deleted by default.
864         """
865         input1 = self.getArbitraryLoreInput(0)
866         self.howtoDir.child("one.xhtml").setContent(input1)
867         self.builder.build("whatever", self.howtoDir, self.howtoDir,
868                            self.templateFile)
869         self.assertTrue(self.howtoDir.child('one.html').exists())
870         self.assertTrue(self.howtoDir.child('one.xhtml').exists())
871
872
873     def test_getLinkrelToSameDirectory(self):
874         """
875         If the doc and resource directories are the same, the linkrel should be
876         an empty string.
877         """
878         linkrel = self.builder.getLinkrel(FilePath("/foo/bar"),
879                                           FilePath("/foo/bar"))
880         self.assertEqual(linkrel, "")
881
882
883     def test_getLinkrelToParentDirectory(self):
884         """
885         If the doc directory is a child of the resource directory, the linkrel
886         should make use of '..'.
887         """
888         linkrel = self.builder.getLinkrel(FilePath("/foo"),
889                                           FilePath("/foo/bar"))
890         self.assertEqual(linkrel, "../")
891
892
893     def test_getLinkrelToSibling(self):
894         """
895         If the doc directory is a sibling of the resource directory, the
896         linkrel should make use of '..' and a named segment.
897         """
898         linkrel = self.builder.getLinkrel(FilePath("/foo/howto"),
899                                           FilePath("/foo/examples"))
900         self.assertEqual(linkrel, "../howto/")
901
902
903     def test_getLinkrelToUncle(self):
904         """
905         If the doc directory is a sibling of the parent of the resource
906         directory, the linkrel should make use of multiple '..'s and a named
907         segment.
908         """
909         linkrel = self.builder.getLinkrel(FilePath("/foo/howto"),
910                                           FilePath("/foo/examples/quotes"))
911         self.assertEqual(linkrel, "../../howto/")
912
913
914
915 class APIBuilderTestCase(TestCase):
916     """
917     Tests for L{APIBuilder}.
918     """
919     skip = pydoctorSkip
920
921     def test_build(self):
922         """
923         L{APIBuilder.build} writes an index file which includes the name of the
924         project specified.
925         """
926         stdout = StringIO()
927         self.patch(sys, 'stdout', stdout)
928
929         projectName = "Foobar"
930         packageName = "quux"
931         projectURL = "scheme:project"
932         sourceURL = "scheme:source"
933         docstring = "text in docstring"
934         privateDocstring = "should also appear in output"
935
936         inputPath = FilePath(self.mktemp()).child(packageName)
937         inputPath.makedirs()
938         inputPath.child("__init__.py").setContent(
939             "def foo():\n"
940             "    '%s'\n"
941             "def _bar():\n"
942             "    '%s'" % (docstring, privateDocstring))
943
944         outputPath = FilePath(self.mktemp())
945         outputPath.makedirs()
946
947         builder = APIBuilder()
948         builder.build(projectName, projectURL, sourceURL, inputPath, outputPath)
949
950         indexPath = outputPath.child("index.html")
951         self.assertTrue(
952             indexPath.exists(),
953             "API index %r did not exist." % (outputPath.path,))
954         self.assertIn(
955             '<a href="%s">%s</a>' % (projectURL, projectName),
956             indexPath.getContent(),
957             "Project name/location not in file contents.")
958
959         quuxPath = outputPath.child("quux.html")
960         self.assertTrue(
961             quuxPath.exists(),
962             "Package documentation file %r did not exist." % (quuxPath.path,))
963         self.assertIn(
964             docstring, quuxPath.getContent(),
965             "Docstring not in package documentation file.")
966         self.assertIn(
967             '<a href="%s/%s">View Source</a>' % (sourceURL, packageName),
968             quuxPath.getContent())
969         self.assertIn(
970             '<a href="%s/%s/__init__.py#L1" class="functionSourceLink">' % (
971                 sourceURL, packageName),
972             quuxPath.getContent())
973         self.assertIn(privateDocstring, quuxPath.getContent())
974
975         # There should also be a page for the foo function in quux.
976         self.assertTrue(quuxPath.sibling('quux.foo.html').exists())
977
978         self.assertEqual(stdout.getvalue(), '')
979
980
981     def test_buildWithPolicy(self):
982         """
983         L{BuildAPIDocsScript.buildAPIDocs} builds the API docs with values
984         appropriate for the Twisted project.
985         """
986         stdout = StringIO()
987         self.patch(sys, 'stdout', stdout)
988         docstring = "text in docstring"
989
990         projectRoot = FilePath(self.mktemp())
991         packagePath = projectRoot.child("twisted")
992         packagePath.makedirs()
993         packagePath.child("__init__.py").setContent(
994             "def foo():\n"
995             "    '%s'\n" % (docstring,))
996         packagePath.child("_version.py").setContent(
997             genVersion("twisted", 1, 0, 0))
998         outputPath = FilePath(self.mktemp())
999
1000         script = BuildAPIDocsScript()
1001         script.buildAPIDocs(projectRoot, outputPath)
1002
1003         indexPath = outputPath.child("index.html")
1004         self.assertTrue(
1005             indexPath.exists(),
1006             "API index %r did not exist." % (outputPath.path,))
1007         self.assertIn(
1008             '<a href="http://twistedmatrix.com/">Twisted</a>',
1009             indexPath.getContent(),
1010             "Project name/location not in file contents.")
1011
1012         twistedPath = outputPath.child("twisted.html")
1013         self.assertTrue(
1014             twistedPath.exists(),
1015             "Package documentation file %r did not exist."
1016             % (twistedPath.path,))
1017         self.assertIn(
1018             docstring, twistedPath.getContent(),
1019             "Docstring not in package documentation file.")
1020         #Here we check that it figured out the correct version based on the
1021         #source code.
1022         self.assertIn(
1023             '<a href="http://twistedmatrix.com/trac/browser/tags/releases/'
1024             'twisted-1.0.0/twisted">View Source</a>',
1025             twistedPath.getContent())
1026
1027         self.assertEqual(stdout.getvalue(), '')
1028
1029
1030     def test_apiBuilderScriptMainRequiresTwoArguments(self):
1031         """
1032         SystemExit is raised when the incorrect number of command line
1033         arguments are passed to the API building script.
1034         """
1035         script = BuildAPIDocsScript()
1036         self.assertRaises(SystemExit, script.main, [])
1037         self.assertRaises(SystemExit, script.main, ["foo"])
1038         self.assertRaises(SystemExit, script.main, ["foo", "bar", "baz"])
1039
1040
1041     def test_apiBuilderScriptMain(self):
1042         """
1043         The API building script invokes the same code that
1044         L{test_buildWithPolicy} tests.
1045         """
1046         script = BuildAPIDocsScript()
1047         calls = []
1048         script.buildAPIDocs = lambda a, b: calls.append((a, b))
1049         script.main(["hello", "there"])
1050         self.assertEqual(calls, [(FilePath("hello"), FilePath("there"))])
1051
1052
1053
1054 class ManBuilderTestCase(TestCase, BuilderTestsMixin):
1055     """
1056     Tests for L{ManBuilder}.
1057     """
1058     skip = loreSkip
1059
1060     def setUp(self):
1061         """
1062         Set up a few instance variables that will be useful.
1063
1064         @ivar builder: A plain L{ManBuilder}.
1065         @ivar manDir: A L{FilePath} representing a directory to be used for
1066             containing man pages.
1067         """
1068         BuilderTestsMixin.setUp(self)
1069         self.builder = ManBuilder()
1070         self.manDir = FilePath(self.mktemp())
1071         self.manDir.createDirectory()
1072
1073
1074     def test_noDocumentsFound(self):
1075         """
1076         L{ManBuilder.build} raises L{NoDocumentsFound} if there are no
1077         .1 files in the given directory.
1078         """
1079         self.assertRaises(NoDocumentsFound, self.builder.build, self.manDir)
1080
1081
1082     def test_build(self):
1083         """
1084         Check that L{ManBuilder.build} find the man page in the directory, and
1085         successfully produce a Lore content.
1086         """
1087         manContent = self.getArbitraryManInput()
1088         self.manDir.child('test1.1').setContent(manContent)
1089         self.builder.build(self.manDir)
1090         output = self.manDir.child('test1-man.xhtml').getContent()
1091         expected = self.getArbitraryManLoreOutput()
1092         # No-op on *nix, fix for windows
1093         expected = expected.replace('\n', os.linesep)
1094         self.assertEqual(output, expected)
1095
1096
1097     def test_toHTML(self):
1098         """
1099         Check that the content output by C{build} is compatible as input of
1100         L{DocBuilder.build}.
1101         """
1102         manContent = self.getArbitraryManInput()
1103         self.manDir.child('test1.1').setContent(manContent)
1104         self.builder.build(self.manDir)
1105
1106         templateFile = self.manDir.child("template.tpl")
1107         templateFile.setContent(DocBuilderTestCase.template)
1108         docBuilder = DocBuilder()
1109         docBuilder.build("1.2.3", self.manDir, self.manDir,
1110                          templateFile)
1111         output = self.manDir.child('test1-man.html').getContent()
1112
1113         self.assertXMLEqual(
1114             output,
1115             """\
1116 <?xml version="1.0" ?><html>
1117     <head><title>Yo:MANHOLE.1</title></head>
1118     <body>
1119     <div class="content">
1120
1121 <span/>
1122
1123 <h2>NAME<a name="auto0"/></h2>
1124
1125 <p>manhole - Connect to a Twisted Manhole service
1126 </p>
1127
1128 <h2>SYNOPSIS<a name="auto1"/></h2>
1129
1130 <p><strong>manhole</strong> </p>
1131
1132 <h2>DESCRIPTION<a name="auto2"/></h2>
1133
1134 <p>manhole is a GTK interface to Twisted Manhole services. You can execute python
1135 code as if at an interactive Python console inside a running Twisted process
1136 with this.</p>
1137
1138 </div>
1139     <a href="index.html">Index</a>
1140     <span class="version">Version: 1.2.3</span>
1141     </body>
1142     </html>""")
1143
1144
1145
1146 class BookBuilderTests(TestCase, BuilderTestsMixin):
1147     """
1148     Tests for L{BookBuilder}.
1149     """
1150     skip = latexSkip or loreSkip
1151
1152     def setUp(self):
1153         """
1154         Make a directory into which to place temporary files.
1155         """
1156         self.docCounter = 0
1157         self.howtoDir = FilePath(self.mktemp())
1158         self.howtoDir.makedirs()
1159         self.oldHandler = signal.signal(signal.SIGCHLD, signal.SIG_DFL)
1160
1161
1162     def tearDown(self):
1163         signal.signal(signal.SIGCHLD, self.oldHandler)
1164
1165
1166     def getArbitraryOutput(self, version, counter, prefix="", apiBaseURL=None):
1167         """
1168         Create and return a C{str} containing the LaTeX document which is
1169         expected as the output for processing the result of the document
1170         returned by C{self.getArbitraryLoreInput(counter)}.
1171         """
1172         path = self.howtoDir.child("%d.xhtml" % (counter,)).path
1173         return (
1174             r'\section{Hi! Title: %(count)s\label{%(path)s}}'
1175             '\n'
1176             r'Hi! %(count)sfoobar') % {'count': counter, 'path': path}
1177
1178
1179     def test_runSuccess(self):
1180         """
1181         L{BookBuilder.run} executes the command it is passed and returns a
1182         string giving the stdout and stderr of the command if it completes
1183         successfully.
1184         """
1185         builder = BookBuilder()
1186         self.assertEqual(
1187                 builder.run([
1188                     sys.executable, '-c',
1189                     'import sys; '
1190                     'sys.stdout.write("hi\\n"); '
1191                     'sys.stdout.flush(); '
1192                     'sys.stderr.write("bye\\n"); '
1193                     'sys.stderr.flush()']),
1194                 "hi\nbye\n")
1195
1196
1197     def test_runFailed(self):
1198         """
1199         L{BookBuilder.run} executes the command it is passed and raises
1200         L{CommandFailed} if it completes unsuccessfully.
1201         """
1202         builder = BookBuilder()
1203         exc = self.assertRaises(
1204             CommandFailed, builder.run,
1205             [sys.executable, '-c', 'print "hi"; raise SystemExit(1)'])
1206         self.assertEqual(exc.exitStatus, 1)
1207         self.assertEqual(exc.exitSignal, None)
1208         self.assertEqual(exc.output, "hi\n")
1209
1210
1211     def test_runSignaled(self):
1212         """
1213         L{BookBuilder.run} executes the command it is passed and raises
1214         L{CommandFailed} if it exits due to a signal.
1215         """
1216         builder = BookBuilder()
1217         exc = self.assertRaises(
1218             CommandFailed, builder.run,
1219             [sys.executable, '-c',
1220             'import sys; print "hi"; sys.stdout.flush(); '
1221             'import os; os.kill(os.getpid(), 9)'])
1222         self.assertEqual(exc.exitSignal, 9)
1223         self.assertEqual(exc.exitStatus, None)
1224         self.assertEqual(exc.output, "hi\n")
1225
1226
1227     def test_buildTeX(self):
1228         """
1229         L{BookBuilder.buildTeX} writes intermediate TeX files for all lore
1230         input files in a directory.
1231         """
1232         version = "3.2.1"
1233         input1, output1 = self.getArbitraryLoreInputAndOutput(version)
1234         input2, output2 = self.getArbitraryLoreInputAndOutput(version)
1235
1236         # Filenames are chosen by getArbitraryOutput to match the counter used
1237         # by getArbitraryLoreInputAndOutput.
1238         self.howtoDir.child("1.xhtml").setContent(input1)
1239         self.howtoDir.child("2.xhtml").setContent(input2)
1240
1241         builder = BookBuilder()
1242         builder.buildTeX(self.howtoDir)
1243         self.assertEqual(self.howtoDir.child("1.tex").getContent(), output1)
1244         self.assertEqual(self.howtoDir.child("2.tex").getContent(), output2)
1245
1246
1247     def test_buildTeXRejectsInvalidDirectory(self):
1248         """
1249         L{BookBuilder.buildTeX} raises L{ValueError} if passed a directory
1250         which does not exist.
1251         """
1252         builder = BookBuilder()
1253         self.assertRaises(
1254             ValueError, builder.buildTeX, self.howtoDir.temporarySibling())
1255
1256
1257     def test_buildTeXOnlyBuildsXHTML(self):
1258         """
1259         L{BookBuilder.buildTeX} ignores files which which don't end with
1260         ".xhtml".
1261         """
1262         # Hopefully ">" is always a parse error from microdom!
1263         self.howtoDir.child("not-input.dat").setContent(">")
1264         self.test_buildTeX()
1265
1266
1267     def test_stdout(self):
1268         """
1269         L{BookBuilder.buildTeX} does not write to stdout.
1270         """
1271         stdout = StringIO()
1272         self.patch(sys, 'stdout', stdout)
1273
1274         # Suppress warnings so that if there are any old-style plugins that
1275         # lore queries for don't confuse the assertion below.  See #3070.
1276         self.patch(warnings, 'warn', lambda *a, **kw: None)
1277         self.test_buildTeX()
1278         self.assertEqual(stdout.getvalue(), '')
1279
1280
1281     def test_buildPDFRejectsInvalidBookFilename(self):
1282         """
1283         L{BookBuilder.buildPDF} raises L{ValueError} if the book filename does
1284         not end with ".tex".
1285         """
1286         builder = BookBuilder()
1287         self.assertRaises(
1288             ValueError,
1289             builder.buildPDF,
1290             FilePath(self.mktemp()).child("foo"),
1291             None,
1292             None)
1293
1294
1295     def _setupTeXFiles(self):
1296         sections = range(3)
1297         self._setupTeXSections(sections)
1298         return self._setupTeXBook(sections)
1299
1300
1301     def _setupTeXSections(self, sections):
1302         for texSectionNumber in sections:
1303             texPath = self.howtoDir.child("%d.tex" % (texSectionNumber,))
1304             texPath.setContent(self.getArbitraryOutput(
1305                     "1.2.3", texSectionNumber))
1306
1307
1308     def _setupTeXBook(self, sections):
1309         bookTeX = self.howtoDir.child("book.tex")
1310         bookTeX.setContent(
1311             r"\documentclass{book}" "\n"
1312             r"\begin{document}" "\n" +
1313             "\n".join([r"\input{%d.tex}" % (n,) for n in sections]) +
1314             r"\end{document}" "\n")
1315         return bookTeX
1316
1317
1318     def test_buildPDF(self):
1319         """
1320         L{BookBuilder.buildPDF} creates a PDF given an index tex file and a
1321         directory containing .tex files.
1322         """
1323         bookPath = self._setupTeXFiles()
1324         outputPath = FilePath(self.mktemp())
1325
1326         builder = BookBuilder()
1327         builder.buildPDF(bookPath, self.howtoDir, outputPath)
1328
1329         self.assertTrue(outputPath.exists())
1330
1331
1332     def test_buildPDFLongPath(self):
1333         """
1334         L{BookBuilder.buildPDF} succeeds even if the paths it is operating on
1335         are very long.
1336
1337         C{ps2pdf13} seems to have problems when path names are long.  This test
1338         verifies that even if inputs have long paths, generation still
1339         succeeds.
1340         """
1341         # Make it long.
1342         self.howtoDir = self.howtoDir.child("x" * 128).child("x" * 128).child("x" * 128)
1343         self.howtoDir.makedirs()
1344
1345         # This will use the above long path.
1346         bookPath = self._setupTeXFiles()
1347         outputPath = FilePath(self.mktemp())
1348
1349         builder = BookBuilder()
1350         builder.buildPDF(bookPath, self.howtoDir, outputPath)
1351
1352         self.assertTrue(outputPath.exists())
1353
1354
1355     def test_buildPDFRunsLaTeXThreeTimes(self):
1356         """
1357         L{BookBuilder.buildPDF} runs C{latex} three times.
1358         """
1359         class InspectableBookBuilder(BookBuilder):
1360             def __init__(self):
1361                 BookBuilder.__init__(self)
1362                 self.commands = []
1363
1364             def run(self, command):
1365                 """
1366                 Record the command and then execute it.
1367                 """
1368                 self.commands.append(command)
1369                 return BookBuilder.run(self, command)
1370
1371         bookPath = self._setupTeXFiles()
1372         outputPath = FilePath(self.mktemp())
1373
1374         builder = InspectableBookBuilder()
1375         builder.buildPDF(bookPath, self.howtoDir, outputPath)
1376
1377         # These string comparisons are very fragile.  It would be better to
1378         # have a test which asserted the correctness of the contents of the
1379         # output files.  I don't know how one could do that, though. -exarkun
1380         latex1, latex2, latex3, dvips, ps2pdf13 = builder.commands
1381         self.assertEqual(latex1, latex2)
1382         self.assertEqual(latex2, latex3)
1383         self.assertEqual(
1384             latex1[:1], ["latex"],
1385             "LaTeX command %r does not seem right." % (latex1,))
1386         self.assertEqual(
1387             latex1[-1:], [bookPath.path],
1388             "LaTeX command %r does not end with the book path (%r)." % (
1389                 latex1, bookPath.path))
1390
1391         self.assertEqual(
1392             dvips[:1], ["dvips"],
1393             "dvips command %r does not seem right." % (dvips,))
1394         self.assertEqual(
1395             ps2pdf13[:1], ["ps2pdf13"],
1396             "ps2pdf13 command %r does not seem right." % (ps2pdf13,))
1397
1398
1399     def test_noSideEffects(self):
1400         """
1401         The working directory is the same before and after a call to
1402         L{BookBuilder.buildPDF}.  Also the contents of the directory containing
1403         the input book are the same before and after the call.
1404         """
1405         startDir = os.getcwd()
1406         bookTeX = self._setupTeXFiles()
1407         startTeXSiblings = bookTeX.parent().children()
1408         startHowtoChildren = self.howtoDir.children()
1409
1410         builder = BookBuilder()
1411         builder.buildPDF(bookTeX, self.howtoDir, FilePath(self.mktemp()))
1412
1413         self.assertEqual(startDir, os.getcwd())
1414         self.assertEqual(startTeXSiblings, bookTeX.parent().children())
1415         self.assertEqual(startHowtoChildren, self.howtoDir.children())
1416
1417
1418     def test_failedCommandProvidesOutput(self):
1419         """
1420         If a subprocess fails, L{BookBuilder.buildPDF} raises L{CommandFailed}
1421         with the subprocess's output and leaves the temporary directory as a
1422         sibling of the book path.
1423         """
1424         bookTeX = FilePath(self.mktemp() + ".tex")
1425         builder = BookBuilder()
1426         inputState = bookTeX.parent().children()
1427         exc = self.assertRaises(
1428             CommandFailed,
1429             builder.buildPDF,
1430             bookTeX, self.howtoDir, FilePath(self.mktemp()))
1431         self.assertTrue(exc.output)
1432         newOutputState = set(bookTeX.parent().children()) - set(inputState)
1433         self.assertEqual(len(newOutputState), 1)
1434         workPath = newOutputState.pop()
1435         self.assertTrue(
1436             workPath.isdir(),
1437             "Expected work path %r was not a directory." % (workPath.path,))
1438
1439
1440     def test_build(self):
1441         """
1442         L{BookBuilder.build} generates a pdf book file from some lore input
1443         files.
1444         """
1445         sections = range(1, 4)
1446         for sectionNumber in sections:
1447             self.howtoDir.child("%d.xhtml" % (sectionNumber,)).setContent(
1448                 self.getArbitraryLoreInput(sectionNumber))
1449         bookTeX = self._setupTeXBook(sections)
1450         bookPDF = FilePath(self.mktemp())
1451
1452         builder = BookBuilder()
1453         builder.build(self.howtoDir, [self.howtoDir], bookTeX, bookPDF)
1454
1455         self.assertTrue(bookPDF.exists())
1456
1457
1458     def test_buildRemovesTemporaryLaTeXFiles(self):
1459         """
1460         L{BookBuilder.build} removes the intermediate LaTeX files it creates.
1461         """
1462         sections = range(1, 4)
1463         for sectionNumber in sections:
1464             self.howtoDir.child("%d.xhtml" % (sectionNumber,)).setContent(
1465                 self.getArbitraryLoreInput(sectionNumber))
1466         bookTeX = self._setupTeXBook(sections)
1467         bookPDF = FilePath(self.mktemp())
1468
1469         builder = BookBuilder()
1470         builder.build(self.howtoDir, [self.howtoDir], bookTeX, bookPDF)
1471
1472         self.assertEqual(
1473             set(self.howtoDir.listdir()),
1474             set([bookTeX.basename()] + ["%d.xhtml" % (n,) for n in sections]))
1475
1476
1477
1478 class FilePathDeltaTest(TestCase):
1479     """
1480     Tests for L{filePathDelta}.
1481     """
1482
1483     def test_filePathDeltaSubdir(self):
1484         """
1485         L{filePathDelta} can create a simple relative path to a child path.
1486         """
1487         self.assertEqual(filePathDelta(FilePath("/foo/bar"),
1488                                         FilePath("/foo/bar/baz")),
1489                           ["baz"])
1490
1491
1492     def test_filePathDeltaSiblingDir(self):
1493         """
1494         L{filePathDelta} can traverse upwards to create relative paths to
1495         siblings.
1496         """
1497         self.assertEqual(filePathDelta(FilePath("/foo/bar"),
1498                                         FilePath("/foo/baz")),
1499                           ["..", "baz"])
1500
1501
1502     def test_filePathNoCommonElements(self):
1503         """
1504         L{filePathDelta} can create relative paths to totally unrelated paths
1505         for maximum portability.
1506         """
1507         self.assertEqual(filePathDelta(FilePath("/foo/bar"),
1508                                         FilePath("/baz/quux")),
1509                           ["..", "..", "baz", "quux"])
1510
1511
1512     def test_filePathDeltaSimilarEndElements(self):
1513         """
1514         L{filePathDelta} doesn't take into account final elements when
1515         comparing 2 paths, but stops at the first difference.
1516         """
1517         self.assertEqual(filePathDelta(FilePath("/foo/bar/bar/spam"),
1518                                         FilePath("/foo/bar/baz/spam")),
1519                           ["..", "..", "baz", "spam"])
1520
1521
1522
1523 class NewsBuilderTests(TestCase, StructureAssertingMixin):
1524     """
1525     Tests for L{NewsBuilder}.
1526     """
1527     def setUp(self):
1528         """
1529         Create a fake project and stuff some basic structure and content into
1530         it.
1531         """
1532         self.builder = NewsBuilder()
1533         self.project = FilePath(self.mktemp())
1534         self.project.createDirectory()
1535         self.existingText = 'Here is stuff which was present previously.\n'
1536         self.createStructure(self.project, {
1537                 'NEWS': self.existingText,
1538                 '5.feature': 'We now support the web.\n',
1539                 '12.feature': 'The widget is more robust.\n',
1540                 '15.feature': (
1541                     'A very long feature which takes many words to '
1542                     'describe with any accuracy was introduced so that '
1543                     'the line wrapping behavior of the news generating '
1544                     'code could be verified.\n'),
1545                 '16.feature': (
1546                     'A simpler feature\ndescribed on multiple lines\n'
1547                     'was added.\n'),
1548                 '23.bugfix': 'Broken stuff was fixed.\n',
1549                 '25.removal': 'Stupid stuff was deprecated.\n',
1550                 '30.misc': '',
1551                 '35.misc': '',
1552                 '40.doc': 'foo.bar.Baz.quux',
1553                 '41.doc': 'writing Foo servers'})
1554
1555
1556     def test_today(self):
1557         """
1558         L{NewsBuilder._today} returns today's date in YYYY-MM-DD form.
1559         """
1560         self.assertEqual(
1561             self.builder._today(), date.today().strftime('%Y-%m-%d'))
1562
1563
1564     def test_findFeatures(self):
1565         """
1566         When called with L{NewsBuilder._FEATURE}, L{NewsBuilder._findChanges}
1567         returns a list of bugfix ticket numbers and descriptions as a list of
1568         two-tuples.
1569         """
1570         features = self.builder._findChanges(
1571             self.project, self.builder._FEATURE)
1572         self.assertEqual(
1573             features,
1574             [(5, "We now support the web."),
1575              (12, "The widget is more robust."),
1576              (15,
1577               "A very long feature which takes many words to describe with "
1578               "any accuracy was introduced so that the line wrapping behavior "
1579               "of the news generating code could be verified."),
1580              (16, "A simpler feature described on multiple lines was added.")])
1581
1582
1583     def test_findBugfixes(self):
1584         """
1585         When called with L{NewsBuilder._BUGFIX}, L{NewsBuilder._findChanges}
1586         returns a list of bugfix ticket numbers and descriptions as a list of
1587         two-tuples.
1588         """
1589         bugfixes = self.builder._findChanges(
1590             self.project, self.builder._BUGFIX)
1591         self.assertEqual(
1592             bugfixes,
1593             [(23, 'Broken stuff was fixed.')])
1594
1595
1596     def test_findRemovals(self):
1597         """
1598         When called with L{NewsBuilder._REMOVAL}, L{NewsBuilder._findChanges}
1599         returns a list of removal/deprecation ticket numbers and descriptions
1600         as a list of two-tuples.
1601         """
1602         removals = self.builder._findChanges(
1603             self.project, self.builder._REMOVAL)
1604         self.assertEqual(
1605             removals,
1606             [(25, 'Stupid stuff was deprecated.')])
1607
1608
1609     def test_findDocumentation(self):
1610         """
1611         When called with L{NewsBuilder._DOC}, L{NewsBuilder._findChanges}
1612         returns a list of documentation ticket numbers and descriptions as a
1613         list of two-tuples.
1614         """
1615         doc = self.builder._findChanges(
1616             self.project, self.builder._DOC)
1617         self.assertEqual(
1618             doc,
1619             [(40, 'foo.bar.Baz.quux'),
1620              (41, 'writing Foo servers')])
1621
1622
1623     def test_findMiscellaneous(self):
1624         """
1625         When called with L{NewsBuilder._MISC}, L{NewsBuilder._findChanges}
1626         returns a list of removal/deprecation ticket numbers and descriptions
1627         as a list of two-tuples.
1628         """
1629         misc = self.builder._findChanges(
1630             self.project, self.builder._MISC)
1631         self.assertEqual(
1632             misc,
1633             [(30, ''),
1634              (35, '')])
1635
1636
1637     def test_writeHeader(self):
1638         """
1639         L{NewsBuilder._writeHeader} accepts a file-like object opened for
1640         writing and a header string and writes out a news file header to it.
1641         """
1642         output = StringIO()
1643         self.builder._writeHeader(output, "Super Awesometastic 32.16")
1644         self.assertEqual(
1645             output.getvalue(),
1646             "Super Awesometastic 32.16\n"
1647             "=========================\n"
1648             "\n")
1649
1650
1651     def test_writeSection(self):
1652         """
1653         L{NewsBuilder._writeSection} accepts a file-like object opened for
1654         writing, a section name, and a list of ticket information (as returned
1655         by L{NewsBuilder._findChanges}) and writes out a section header and all
1656         of the given ticket information.
1657         """
1658         output = StringIO()
1659         self.builder._writeSection(
1660             output, "Features",
1661             [(3, "Great stuff."),
1662              (17, "Very long line which goes on and on and on, seemingly "
1663               "without end until suddenly without warning it does end.")])
1664         self.assertEqual(
1665             output.getvalue(),
1666             "Features\n"
1667             "--------\n"
1668             " - Great stuff. (#3)\n"
1669             " - Very long line which goes on and on and on, seemingly without end\n"
1670             "   until suddenly without warning it does end. (#17)\n"
1671             "\n")
1672
1673
1674     def test_writeMisc(self):
1675         """
1676         L{NewsBuilder._writeMisc} accepts a file-like object opened for
1677         writing, a section name, and a list of ticket information (as returned
1678         by L{NewsBuilder._findChanges} and writes out a section header and all
1679         of the ticket numbers, but excludes any descriptions.
1680         """
1681         output = StringIO()
1682         self.builder._writeMisc(
1683             output, "Other",
1684             [(x, "") for x in range(2, 50, 3)])
1685         self.assertEqual(
1686             output.getvalue(),
1687             "Other\n"
1688             "-----\n"
1689             " - #2, #5, #8, #11, #14, #17, #20, #23, #26, #29, #32, #35, #38, #41,\n"
1690             "   #44, #47\n"
1691             "\n")
1692
1693
1694     def test_build(self):
1695         """
1696         L{NewsBuilder.build} updates a NEWS file with new features based on the
1697         I{<ticket>.feature} files found in the directory specified.
1698         """
1699         self.builder.build(
1700             self.project, self.project.child('NEWS'),
1701             "Super Awesometastic 32.16")
1702
1703         results = self.project.child('NEWS').getContent()
1704         self.assertEqual(
1705             results,
1706             'Super Awesometastic 32.16\n'
1707             '=========================\n'
1708             '\n'
1709             'Features\n'
1710             '--------\n'
1711             ' - We now support the web. (#5)\n'
1712             ' - The widget is more robust. (#12)\n'
1713             ' - A very long feature which takes many words to describe with any\n'
1714             '   accuracy was introduced so that the line wrapping behavior of the\n'
1715             '   news generating code could be verified. (#15)\n'
1716             ' - A simpler feature described on multiple lines was added. (#16)\n'
1717             '\n'
1718             'Bugfixes\n'
1719             '--------\n'
1720             ' - Broken stuff was fixed. (#23)\n'
1721             '\n'
1722             'Improved Documentation\n'
1723             '----------------------\n'
1724             ' - foo.bar.Baz.quux (#40)\n'
1725             ' - writing Foo servers (#41)\n'
1726             '\n'
1727             'Deprecations and Removals\n'
1728             '-------------------------\n'
1729             ' - Stupid stuff was deprecated. (#25)\n'
1730             '\n'
1731             'Other\n'
1732             '-----\n'
1733             ' - #30, #35\n'
1734             '\n\n' + self.existingText)
1735
1736
1737     def test_emptyProjectCalledOut(self):
1738         """
1739         If no changes exist for a project, I{NEWS} gains a new section for
1740         that project that includes some helpful text about how there were no
1741         interesting changes.
1742         """
1743         project = FilePath(self.mktemp()).child("twisted")
1744         project.makedirs()
1745         self.createStructure(project, {
1746                 'NEWS': self.existingText })
1747
1748         self.builder.build(
1749             project, project.child('NEWS'),
1750             "Super Awesometastic 32.16")
1751         results = project.child('NEWS').getContent()
1752         self.assertEqual(
1753             results,
1754             'Super Awesometastic 32.16\n'
1755             '=========================\n'
1756             '\n' +
1757             self.builder._NO_CHANGES +
1758             '\n\n' + self.existingText)
1759
1760
1761     def test_preserveTicketHint(self):
1762         """
1763         If a I{NEWS} file begins with the two magic lines which point readers
1764         at the issue tracker, those lines are kept at the top of the new file.
1765         """
1766         news = self.project.child('NEWS')
1767         news.setContent(
1768             'Ticket numbers in this file can be looked up by visiting\n'
1769             'http://twistedmatrix.com/trac/ticket/<number>\n'
1770             '\n'
1771             'Blah blah other stuff.\n')
1772
1773         self.builder.build(self.project, news, "Super Awesometastic 32.16")
1774
1775         self.assertEqual(
1776             news.getContent(),
1777             'Ticket numbers in this file can be looked up by visiting\n'
1778             'http://twistedmatrix.com/trac/ticket/<number>\n'
1779             '\n'
1780             'Super Awesometastic 32.16\n'
1781             '=========================\n'
1782             '\n'
1783             'Features\n'
1784             '--------\n'
1785             ' - We now support the web. (#5)\n'
1786             ' - The widget is more robust. (#12)\n'
1787             ' - A very long feature which takes many words to describe with any\n'
1788             '   accuracy was introduced so that the line wrapping behavior of the\n'
1789             '   news generating code could be verified. (#15)\n'
1790             ' - A simpler feature described on multiple lines was added. (#16)\n'
1791             '\n'
1792             'Bugfixes\n'
1793             '--------\n'
1794             ' - Broken stuff was fixed. (#23)\n'
1795             '\n'
1796             'Improved Documentation\n'
1797             '----------------------\n'
1798             ' - foo.bar.Baz.quux (#40)\n'
1799             ' - writing Foo servers (#41)\n'
1800             '\n'
1801             'Deprecations and Removals\n'
1802             '-------------------------\n'
1803             ' - Stupid stuff was deprecated. (#25)\n'
1804             '\n'
1805             'Other\n'
1806             '-----\n'
1807             ' - #30, #35\n'
1808             '\n\n'
1809             'Blah blah other stuff.\n')
1810
1811
1812     def test_emptySectionsOmitted(self):
1813         """
1814         If there are no changes of a particular type (feature, bugfix, etc), no
1815         section for that type is written by L{NewsBuilder.build}.
1816         """
1817         for ticket in self.project.children():
1818             if ticket.splitext()[1] in ('.feature', '.misc', '.doc'):
1819                 ticket.remove()
1820
1821         self.builder.build(
1822             self.project, self.project.child('NEWS'),
1823             'Some Thing 1.2')
1824
1825         self.assertEqual(
1826             self.project.child('NEWS').getContent(),
1827             'Some Thing 1.2\n'
1828             '==============\n'
1829             '\n'
1830             'Bugfixes\n'
1831             '--------\n'
1832             ' - Broken stuff was fixed. (#23)\n'
1833             '\n'
1834             'Deprecations and Removals\n'
1835             '-------------------------\n'
1836             ' - Stupid stuff was deprecated. (#25)\n'
1837             '\n\n'
1838             'Here is stuff which was present previously.\n')
1839
1840
1841     def test_duplicatesMerged(self):
1842         """
1843         If two change files have the same contents, they are merged in the
1844         generated news entry.
1845         """
1846         def feature(s):
1847             return self.project.child(s + '.feature')
1848         feature('5').copyTo(feature('15'))
1849         feature('5').copyTo(feature('16'))
1850
1851         self.builder.build(
1852             self.project, self.project.child('NEWS'),
1853             'Project Name 5.0')
1854
1855         self.assertEqual(
1856             self.project.child('NEWS').getContent(),
1857             'Project Name 5.0\n'
1858             '================\n'
1859             '\n'
1860             'Features\n'
1861             '--------\n'
1862             ' - We now support the web. (#5, #15, #16)\n'
1863             ' - The widget is more robust. (#12)\n'
1864             '\n'
1865             'Bugfixes\n'
1866             '--------\n'
1867             ' - Broken stuff was fixed. (#23)\n'
1868             '\n'
1869             'Improved Documentation\n'
1870             '----------------------\n'
1871             ' - foo.bar.Baz.quux (#40)\n'
1872             ' - writing Foo servers (#41)\n'
1873             '\n'
1874             'Deprecations and Removals\n'
1875             '-------------------------\n'
1876             ' - Stupid stuff was deprecated. (#25)\n'
1877             '\n'
1878             'Other\n'
1879             '-----\n'
1880             ' - #30, #35\n'
1881             '\n\n'
1882             'Here is stuff which was present previously.\n')
1883
1884
1885     def createFakeTwistedProject(self):
1886         """
1887         Create a fake-looking Twisted project to build from.
1888         """
1889         project = FilePath(self.mktemp()).child("twisted")
1890         project.makedirs()
1891         self.createStructure(project, {
1892                 'NEWS': 'Old boring stuff from the past.\n',
1893                 '_version.py': genVersion("twisted", 1, 2, 3),
1894                 'topfiles': {
1895                     'NEWS': 'Old core news.\n',
1896                     '3.feature': 'Third feature addition.\n',
1897                     '5.misc': ''},
1898                 'conch': {
1899                     '_version.py': genVersion("twisted.conch", 3, 4, 5),
1900                     'topfiles': {
1901                         'NEWS': 'Old conch news.\n',
1902                         '7.bugfix': 'Fixed that bug.\n'}},
1903                 })
1904         return project
1905
1906
1907     def test_buildAll(self):
1908         """
1909         L{NewsBuilder.buildAll} calls L{NewsBuilder.build} once for each
1910         subproject, passing that subproject's I{topfiles} directory as C{path},
1911         the I{NEWS} file in that directory as C{output}, and the subproject's
1912         name as C{header}, and then again for each subproject with the
1913         top-level I{NEWS} file for C{output}. Blacklisted subprojects are
1914         skipped.
1915         """
1916         builds = []
1917         builder = NewsBuilder()
1918         builder.build = lambda path, output, header: builds.append((
1919                 path, output, header))
1920         builder._today = lambda: '2009-12-01'
1921
1922         project = self.createFakeTwistedProject()
1923         builder.buildAll(project)
1924
1925         coreTopfiles = project.child("topfiles")
1926         coreNews = coreTopfiles.child("NEWS")
1927         coreHeader = "Twisted Core 1.2.3 (2009-12-01)"
1928
1929         conchTopfiles = project.child("conch").child("topfiles")
1930         conchNews = conchTopfiles.child("NEWS")
1931         conchHeader = "Twisted Conch 3.4.5 (2009-12-01)"
1932
1933         aggregateNews = project.child("NEWS")
1934
1935         self.assertEqual(
1936             builds,
1937             [(conchTopfiles, conchNews, conchHeader),
1938              (coreTopfiles, coreNews, coreHeader),
1939              (conchTopfiles, aggregateNews, conchHeader),
1940              (coreTopfiles, aggregateNews, coreHeader)])
1941
1942
1943     def test_changeVersionInNews(self):
1944         """
1945         L{NewsBuilder._changeVersions} gets the release date for a given
1946         version of a project as a string.
1947         """
1948         builder = NewsBuilder()
1949         builder._today = lambda: '2009-12-01'
1950         project = self.createFakeTwistedProject()
1951         builder.buildAll(project)
1952         newVersion = Version('TEMPLATE', 7, 7, 14)
1953         coreNews = project.child('topfiles').child('NEWS')
1954         # twisted 1.2.3 is the old version.
1955         builder._changeNewsVersion(
1956             coreNews, "Core", Version("twisted", 1, 2, 3),
1957             newVersion, '2010-01-01')
1958         expectedCore = (
1959             'Twisted Core 7.7.14 (2010-01-01)\n'
1960             '================================\n'
1961             '\n'
1962             'Features\n'
1963             '--------\n'
1964             ' - Third feature addition. (#3)\n'
1965             '\n'
1966             'Other\n'
1967             '-----\n'
1968             ' - #5\n\n\n')
1969         self.assertEqual(
1970             expectedCore + 'Old core news.\n', coreNews.getContent())
1971
1972
1973
1974 class DistributionBuilderTestBase(BuilderTestsMixin, StructureAssertingMixin,
1975                                    TestCase):
1976     """
1977     Base for tests of L{DistributionBuilder}.
1978     """
1979     skip = loreSkip
1980
1981     def setUp(self):
1982         BuilderTestsMixin.setUp(self)
1983
1984         self.rootDir = FilePath(self.mktemp())
1985         self.rootDir.createDirectory()
1986
1987         self.outputDir = FilePath(self.mktemp())
1988         self.outputDir.createDirectory()
1989         self.builder = DistributionBuilder(self.rootDir, self.outputDir)
1990
1991
1992
1993 class DistributionBuilderTest(DistributionBuilderTestBase):
1994
1995     def test_twistedDistribution(self):
1996         """
1997         The Twisted tarball contains everything in the source checkout, with
1998         built documentation.
1999         """
2000         loreInput, loreOutput = self.getArbitraryLoreInputAndOutput("10.0.0")
2001         manInput1 = self.getArbitraryManInput()
2002         manOutput1 = self.getArbitraryManHTMLOutput("10.0.0", "../howto/")
2003         manInput2 = self.getArbitraryManInput()
2004         manOutput2 = self.getArbitraryManHTMLOutput("10.0.0", "../howto/")
2005         coreIndexInput, coreIndexOutput = self.getArbitraryLoreInputAndOutput(
2006             "10.0.0", prefix="howto/")
2007
2008         structure = {
2009             "README": "Twisted",
2010             "unrelated": "x",
2011             "LICENSE": "copyright!",
2012             "setup.py": "import toplevel",
2013             "bin": {"web": {"websetroot": "SET ROOT"},
2014                     "twistd": "TWISTD"},
2015             "twisted":
2016                 {"web":
2017                      {"__init__.py": "import WEB",
2018                       "topfiles": {"setup.py": "import WEBINSTALL",
2019                                    "README": "WEB!"}},
2020                  "words": {"__init__.py": "import WORDS"},
2021                  "plugins": {"twisted_web.py": "import WEBPLUG",
2022                              "twisted_words.py": "import WORDPLUG"}},
2023             "doc": {"web": {"howto": {"index.xhtml": loreInput},
2024                             "man": {"websetroot.1": manInput2}},
2025                     "core": {"howto": {"template.tpl": self.template},
2026                              "man": {"twistd.1": manInput1},
2027                              "index.xhtml": coreIndexInput}}}
2028
2029         outStructure = {
2030             "README": "Twisted",
2031             "unrelated": "x",
2032             "LICENSE": "copyright!",
2033             "setup.py": "import toplevel",
2034             "bin": {"web": {"websetroot": "SET ROOT"},
2035                     "twistd": "TWISTD"},
2036             "twisted":
2037                 {"web": {"__init__.py": "import WEB",
2038                          "topfiles": {"setup.py": "import WEBINSTALL",
2039                                       "README": "WEB!"}},
2040                  "words": {"__init__.py": "import WORDS"},
2041                  "plugins": {"twisted_web.py": "import WEBPLUG",
2042                              "twisted_words.py": "import WORDPLUG"}},
2043             "doc": {"web": {"howto": {"index.html": loreOutput},
2044                             "man": {"websetroot.1": manInput2,
2045                                     "websetroot-man.html": manOutput2}},
2046                     "core": {"howto": {"template.tpl": self.template},
2047                              "man": {"twistd.1": manInput1,
2048                                      "twistd-man.html": manOutput1},
2049                              "index.html": coreIndexOutput}}}
2050
2051         self.createStructure(self.rootDir, structure)
2052
2053         outputFile = self.builder.buildTwisted("10.0.0")
2054
2055         self.assertExtractedStructure(outputFile, outStructure)
2056
2057
2058     def test_subProjectLayout(self):
2059         """
2060         The subproject tarball includes files like so:
2061
2062         1. twisted/<subproject>/topfiles defines the files that will be in the
2063            top level in the tarball, except LICENSE, which comes from the real
2064            top-level directory.
2065         2. twisted/<subproject> is included, but without the topfiles entry
2066            in that directory. No other twisted subpackages are included.
2067         3. twisted/plugins/twisted_<subproject>.py is included, but nothing
2068            else in plugins is.
2069         """
2070         structure = {
2071             "README": "HI!@",
2072             "unrelated": "x",
2073             "LICENSE": "copyright!",
2074             "setup.py": "import toplevel",
2075             "bin": {"web": {"websetroot": "SET ROOT"},
2076                     "words": {"im": "#!im"}},
2077             "twisted":
2078                 {"web":
2079                      {"__init__.py": "import WEB",
2080                       "topfiles": {"setup.py": "import WEBINSTALL",
2081                                    "README": "WEB!"}},
2082                  "words": {"__init__.py": "import WORDS"},
2083                  "plugins": {"twisted_web.py": "import WEBPLUG",
2084                              "twisted_words.py": "import WORDPLUG"}}}
2085
2086         outStructure = {
2087             "README": "WEB!",
2088             "LICENSE": "copyright!",
2089             "setup.py": "import WEBINSTALL",
2090             "bin": {"websetroot": "SET ROOT"},
2091             "twisted": {"web": {"__init__.py": "import WEB"},
2092                         "plugins": {"twisted_web.py": "import WEBPLUG"}}}
2093
2094         self.createStructure(self.rootDir, structure)
2095
2096         outputFile = self.builder.buildSubProject("web", "0.3.0")
2097
2098         self.assertExtractedStructure(outputFile, outStructure)
2099
2100
2101     def test_minimalSubProjectLayout(self):
2102         """
2103         buildSubProject should work with minimal subprojects.
2104         """
2105         structure = {
2106             "LICENSE": "copyright!",
2107             "bin": {},
2108             "twisted":
2109                 {"web": {"__init__.py": "import WEB",
2110                          "topfiles": {"setup.py": "import WEBINSTALL"}},
2111                  "plugins": {}}}
2112
2113         outStructure = {
2114             "setup.py": "import WEBINSTALL",
2115             "LICENSE": "copyright!",
2116             "twisted": {"web": {"__init__.py": "import WEB"}}}
2117
2118         self.createStructure(self.rootDir, structure)
2119
2120         outputFile = self.builder.buildSubProject("web", "0.3.0")
2121
2122         self.assertExtractedStructure(outputFile, outStructure)
2123
2124
2125     def test_subProjectDocBuilding(self):
2126         """
2127         When building a subproject release, documentation should be built with
2128         lore.
2129         """
2130         loreInput, loreOutput = self.getArbitraryLoreInputAndOutput("0.3.0")
2131         manInput = self.getArbitraryManInput()
2132         manOutput = self.getArbitraryManHTMLOutput("0.3.0", "../howto/")
2133         structure = {
2134             "LICENSE": "copyright!",
2135             "twisted": {"web": {"__init__.py": "import WEB",
2136                                 "topfiles": {"setup.py": "import WEBINST"}}},
2137             "doc": {"web": {"howto": {"index.xhtml": loreInput},
2138                             "man": {"twistd.1": manInput}},
2139                     "core": {"howto": {"template.tpl": self.template}}
2140                     }
2141             }
2142
2143         outStructure = {
2144             "LICENSE": "copyright!",
2145             "setup.py": "import WEBINST",
2146             "twisted": {"web": {"__init__.py": "import WEB"}},
2147             "doc": {"howto": {"index.html": loreOutput},
2148                     "man": {"twistd.1": manInput,
2149                             "twistd-man.html": manOutput}}}
2150
2151         self.createStructure(self.rootDir, structure)
2152
2153         outputFile = self.builder.buildSubProject("web", "0.3.0")
2154
2155         self.assertExtractedStructure(outputFile, outStructure)
2156
2157
2158     def test_coreProjectLayout(self):
2159         """
2160         The core tarball looks a lot like a subproject tarball, except it
2161         doesn't include:
2162
2163         - Python packages from other subprojects
2164         - plugins from other subprojects
2165         - scripts from other subprojects
2166         """
2167         indexInput, indexOutput = self.getArbitraryLoreInputAndOutput(
2168             "8.0.0", prefix="howto/")
2169         howtoInput, howtoOutput = self.getArbitraryLoreInputAndOutput("8.0.0")
2170         specInput, specOutput = self.getArbitraryLoreInputAndOutput(
2171             "8.0.0", prefix="../howto/")
2172         tutorialInput, tutorialOutput = self.getArbitraryLoreInputAndOutput(
2173             "8.0.0", prefix="../")
2174
2175         structure = {
2176             "LICENSE": "copyright!",
2177             "twisted": {"__init__.py": "twisted",
2178                         "python": {"__init__.py": "python",
2179                                    "roots.py": "roots!"},
2180                         "conch": {"__init__.py": "conch",
2181                                   "unrelated.py": "import conch"},
2182                         "plugin.py": "plugin",
2183                         "plugins": {"twisted_web.py": "webplug",
2184                                     "twisted_whatever.py": "include!",
2185                                     "cred.py": "include!"},
2186                         "topfiles": {"setup.py": "import CORE",
2187                                      "README": "core readme"}},
2188             "doc": {"core": {"howto": {"template.tpl": self.template,
2189                                        "index.xhtml": howtoInput,
2190                                        "tutorial":
2191                                            {"index.xhtml": tutorialInput}},
2192                              "specifications": {"index.xhtml": specInput},
2193                              "examples": {"foo.py": "foo.py"},
2194                              "index.xhtml": indexInput},
2195                     "web": {"howto": {"index.xhtml": "webindex"}}},
2196             "bin": {"twistd": "TWISTD",
2197                     "web": {"websetroot": "websetroot"}}
2198             }
2199
2200         outStructure = {
2201             "LICENSE": "copyright!",
2202             "setup.py": "import CORE",
2203             "README": "core readme",
2204             "twisted": {"__init__.py": "twisted",
2205                         "python": {"__init__.py": "python",
2206                                    "roots.py": "roots!"},
2207                         "plugin.py": "plugin",
2208                         "plugins": {"twisted_whatever.py": "include!",
2209                                     "cred.py": "include!"}},
2210             "doc": {"howto": {"template.tpl": self.template,
2211                               "index.html": howtoOutput,
2212                               "tutorial": {"index.html": tutorialOutput}},
2213                     "specifications": {"index.html": specOutput},
2214                     "examples": {"foo.py": "foo.py"},
2215                     "index.html": indexOutput},
2216             "bin": {"twistd": "TWISTD"},
2217             }
2218
2219         self.createStructure(self.rootDir, structure)
2220         outputFile = self.builder.buildCore("8.0.0")
2221         self.assertExtractedStructure(outputFile, outStructure)
2222
2223
2224     def test_apiBaseURL(self):
2225         """
2226         DistributionBuilder builds documentation with the specified
2227         API base URL.
2228         """
2229         apiBaseURL = "http://%s"
2230         builder = DistributionBuilder(self.rootDir, self.outputDir,
2231                                       apiBaseURL=apiBaseURL)
2232         loreInput, loreOutput = self.getArbitraryLoreInputAndOutput(
2233             "0.3.0", apiBaseURL=apiBaseURL)
2234         structure = {
2235             "LICENSE": "copyright!",
2236             "twisted": {"web": {"__init__.py": "import WEB",
2237                                 "topfiles": {"setup.py": "import WEBINST"}}},
2238             "doc": {"web": {"howto": {"index.xhtml": loreInput}},
2239                     "core": {"howto": {"template.tpl": self.template}}
2240                     }
2241             }
2242
2243         outStructure = {
2244             "LICENSE": "copyright!",
2245             "setup.py": "import WEBINST",
2246             "twisted": {"web": {"__init__.py": "import WEB"}},
2247             "doc": {"howto": {"index.html": loreOutput}}}
2248
2249         self.createStructure(self.rootDir, structure)
2250         outputFile = builder.buildSubProject("web", "0.3.0")
2251         self.assertExtractedStructure(outputFile, outStructure)
2252
2253
2254
2255 class BuildAllTarballsTest(DistributionBuilderTestBase):
2256     """
2257     Tests for L{DistributionBuilder.buildAllTarballs}.
2258     """
2259     skip = svnSkip
2260
2261     def setUp(self):
2262         self.oldHandler = signal.signal(signal.SIGCHLD, signal.SIG_DFL)
2263         DistributionBuilderTestBase.setUp(self)
2264
2265
2266     def tearDown(self):
2267         signal.signal(signal.SIGCHLD, self.oldHandler)
2268         DistributionBuilderTestBase.tearDown(self)
2269
2270
2271     def test_buildAllTarballs(self):
2272         """
2273         L{buildAllTarballs} builds tarballs for Twisted and all of its
2274         subprojects based on an SVN checkout; the resulting tarballs contain
2275         no SVN metadata.  This involves building documentation, which it will
2276         build with the correct API documentation reference base URL.
2277         """
2278         repositoryPath = self.mktemp()
2279         repository = FilePath(repositoryPath)
2280         checkoutPath = self.mktemp()
2281         checkout = FilePath(checkoutPath)
2282         self.outputDir.remove()
2283
2284         runCommand(["svnadmin", "create", repositoryPath])
2285         runCommand(["svn", "checkout", "file://" + repository.path,
2286                     checkout.path])
2287         coreIndexInput, coreIndexOutput = self.getArbitraryLoreInputAndOutput(
2288             "1.2.0", prefix="howto/",
2289             apiBaseURL="http://twistedmatrix.com/documents/1.2.0/api/%s.html")
2290
2291         structure = {
2292             "README": "Twisted",
2293             "unrelated": "x",
2294             "LICENSE": "copyright!",
2295             "setup.py": "import toplevel",
2296             "bin": {"words": {"im": "import im"},
2297                     "twistd": "TWISTD"},
2298             "twisted":
2299                 {
2300                     "topfiles": {"setup.py": "import TOPINSTALL",
2301                                  "README": "CORE!"},
2302                     "_version.py": genVersion("twisted", 1, 2, 0),
2303                     "words": {"__init__.py": "import WORDS",
2304                               "_version.py":
2305                                   genVersion("twisted.words", 1, 2, 0),
2306                               "topfiles": {"setup.py": "import WORDSINSTALL",
2307                                            "README": "WORDS!"},
2308                               },
2309                     "plugins": {"twisted_web.py": "import WEBPLUG",
2310                                 "twisted_words.py": "import WORDPLUG",
2311                                 "twisted_yay.py": "import YAY"}},
2312             "doc": {"core": {"howto": {"template.tpl": self.template},
2313                              "index.xhtml": coreIndexInput}}}
2314
2315         twistedStructure = {
2316             "README": "Twisted",
2317             "unrelated": "x",
2318             "LICENSE": "copyright!",
2319             "setup.py": "import toplevel",
2320             "bin": {"twistd": "TWISTD",
2321                     "words": {"im": "import im"}},
2322             "twisted":
2323                 {
2324                     "topfiles": {"setup.py": "import TOPINSTALL",
2325                                  "README": "CORE!"},
2326                     "_version.py": genVersion("twisted", 1, 2, 0),
2327                     "words": {"__init__.py": "import WORDS",
2328                               "_version.py":
2329                                   genVersion("twisted.words", 1, 2, 0),
2330                               "topfiles": {"setup.py": "import WORDSINSTALL",
2331                                            "README": "WORDS!"},
2332                               },
2333                     "plugins": {"twisted_web.py": "import WEBPLUG",
2334                                 "twisted_words.py": "import WORDPLUG",
2335                                 "twisted_yay.py": "import YAY"}},
2336             "doc": {"core": {"howto": {"template.tpl": self.template},
2337                              "index.html": coreIndexOutput}}}
2338
2339         coreStructure = {
2340             "setup.py": "import TOPINSTALL",
2341             "README": "CORE!",
2342             "LICENSE": "copyright!",
2343             "bin": {"twistd": "TWISTD"},
2344             "twisted": {
2345                 "_version.py": genVersion("twisted", 1, 2, 0),
2346                 "plugins": {"twisted_yay.py": "import YAY"}},
2347             "doc": {"howto": {"template.tpl": self.template},
2348                     "index.html": coreIndexOutput}}
2349
2350         wordsStructure = {
2351             "README": "WORDS!",
2352             "LICENSE": "copyright!",
2353             "setup.py": "import WORDSINSTALL",
2354             "bin": {"im": "import im"},
2355             "twisted":
2356                 {
2357                     "words": {"__init__.py": "import WORDS",
2358                               "_version.py":
2359                                   genVersion("twisted.words", 1, 2, 0),
2360                               },
2361                     "plugins": {"twisted_words.py": "import WORDPLUG"}}}
2362
2363         self.createStructure(checkout, structure)
2364         childs = [x.path for x in checkout.children()]
2365         runCommand(["svn", "add"] + childs)
2366         runCommand(["svn", "commit", checkout.path, "-m", "yay"])
2367
2368         buildAllTarballs(checkout, self.outputDir)
2369         self.assertEqual(
2370             set(self.outputDir.children()),
2371             set([self.outputDir.child("Twisted-1.2.0.tar.bz2"),
2372                  self.outputDir.child("TwistedCore-1.2.0.tar.bz2"),
2373                  self.outputDir.child("TwistedWords-1.2.0.tar.bz2")]))
2374
2375         self.assertExtractedStructure(
2376             self.outputDir.child("Twisted-1.2.0.tar.bz2"),
2377             twistedStructure)
2378         self.assertExtractedStructure(
2379             self.outputDir.child("TwistedCore-1.2.0.tar.bz2"),
2380             coreStructure)
2381         self.assertExtractedStructure(
2382             self.outputDir.child("TwistedWords-1.2.0.tar.bz2"),
2383             wordsStructure)
2384
2385
2386     def test_buildAllTarballsEnsuresCleanCheckout(self):
2387         """
2388         L{UncleanWorkingDirectory} is raised by L{buildAllTarballs} when the
2389         SVN checkout provided has uncommitted changes.
2390         """
2391         repositoryPath = self.mktemp()
2392         repository = FilePath(repositoryPath)
2393         checkoutPath = self.mktemp()
2394         checkout = FilePath(checkoutPath)
2395
2396         runCommand(["svnadmin", "create", repositoryPath])
2397         runCommand(["svn", "checkout", "file://" + repository.path,
2398                     checkout.path])
2399
2400         checkout.child("foo").setContent("whatever")
2401         self.assertRaises(UncleanWorkingDirectory,
2402                           buildAllTarballs, checkout, FilePath(self.mktemp()))
2403
2404
2405     def test_buildAllTarballsEnsuresExistingCheckout(self):
2406         """
2407         L{NotWorkingDirectory} is raised by L{buildAllTarballs} when the
2408         checkout passed does not exist or is not an SVN checkout.
2409         """
2410         checkout = FilePath(self.mktemp())
2411         self.assertRaises(NotWorkingDirectory,
2412                           buildAllTarballs,
2413                           checkout, FilePath(self.mktemp()))
2414         checkout.createDirectory()
2415         self.assertRaises(NotWorkingDirectory,
2416                           buildAllTarballs,
2417                           checkout, FilePath(self.mktemp()))
2418
2419
2420
2421 class ScriptTests(BuilderTestsMixin, StructureAssertingMixin, TestCase):
2422     """
2423     Tests for the release script functionality.
2424     """
2425
2426     def _testVersionChanging(self, major, minor, micro, prerelease=None):
2427         """
2428         Check that L{ChangeVersionsScript.main} calls the version-changing
2429         function with the appropriate version data and filesystem path.
2430         """
2431         versionUpdates = []
2432         def myVersionChanger(sourceTree, versionTemplate):
2433             versionUpdates.append((sourceTree, versionTemplate))
2434         versionChanger = ChangeVersionsScript()
2435         versionChanger.changeAllProjectVersions = myVersionChanger
2436         version = "%d.%d.%d" % (major, minor, micro)
2437         if prerelease is not None:
2438             version += "pre%d" % (prerelease,)
2439         versionChanger.main([version])
2440         self.assertEqual(len(versionUpdates), 1)
2441         self.assertEqual(versionUpdates[0][0], FilePath("."))
2442         self.assertEqual(versionUpdates[0][1].major, major)
2443         self.assertEqual(versionUpdates[0][1].minor, minor)
2444         self.assertEqual(versionUpdates[0][1].micro, micro)
2445         self.assertEqual(versionUpdates[0][1].prerelease, prerelease)
2446
2447
2448     def test_changeVersions(self):
2449         """
2450         L{ChangeVersionsScript.main} changes version numbers for all Twisted
2451         projects.
2452         """
2453         self._testVersionChanging(8, 2, 3)
2454
2455
2456     def test_changeVersionsWithPrerelease(self):
2457         """
2458         A prerelease can be specified to L{changeVersionsScript}.
2459         """
2460         self._testVersionChanging(9, 2, 7, 38)
2461
2462
2463     def test_defaultChangeVersionsVersionChanger(self):
2464         """
2465         The default implementation of C{changeAllProjectVersions} is
2466         L{changeAllProjectVersions}.
2467         """
2468         versionChanger = ChangeVersionsScript()
2469         self.assertEqual(versionChanger.changeAllProjectVersions,
2470                           changeAllProjectVersions)
2471
2472
2473     def test_badNumberOfArgumentsToChangeVersionsScript(self):
2474         """
2475         L{changeVersionsScript} raises SystemExit when the wrong number of
2476         arguments are passed.
2477         """
2478         versionChanger = ChangeVersionsScript()
2479         self.assertRaises(SystemExit, versionChanger.main, [])
2480
2481
2482     def test_tooManyDotsToChangeVersionsScript(self):
2483         """
2484         L{changeVersionsScript} raises SystemExit when there are the wrong
2485         number of segments in the version number passed.
2486         """
2487         versionChanger = ChangeVersionsScript()
2488         self.assertRaises(SystemExit, versionChanger.main,
2489                           ["3.2.1.0"])
2490
2491
2492     def test_nonIntPartsToChangeVersionsScript(self):
2493         """
2494         L{changeVersionsScript} raises SystemExit when the version number isn't
2495         made out of numbers.
2496         """
2497         versionChanger = ChangeVersionsScript()
2498         self.assertRaises(SystemExit, versionChanger.main,
2499                           ["my united.states.of prewhatever"])
2500
2501
2502     def test_buildTarballsScript(self):
2503         """
2504         L{BuildTarballsScript.main} invokes L{buildAllTarballs} with
2505         2 or 3 L{FilePath} instances representing the paths passed to it.
2506         """
2507         builds = []
2508         def myBuilder(checkout, destination, template=None):
2509             builds.append((checkout, destination, template))
2510         tarballBuilder = BuildTarballsScript()
2511         tarballBuilder.buildAllTarballs = myBuilder
2512
2513         tarballBuilder.main(["checkoutDir", "destinationDir"])
2514         self.assertEqual(
2515             builds,
2516             [(FilePath("checkoutDir"), FilePath("destinationDir"), None)])
2517
2518         builds = []
2519         tarballBuilder.main(["checkoutDir", "destinationDir", "templatePath"])
2520         self.assertEqual(
2521             builds,
2522             [(FilePath("checkoutDir"), FilePath("destinationDir"),
2523               FilePath("templatePath"))])
2524
2525
2526     def test_defaultBuildTarballsScriptBuilder(self):
2527         """
2528         The default implementation of L{BuildTarballsScript.buildAllTarballs}
2529         is L{buildAllTarballs}.
2530         """
2531         tarballBuilder = BuildTarballsScript()
2532         self.assertEqual(tarballBuilder.buildAllTarballs, buildAllTarballs)
2533
2534
2535     def test_badNumberOfArgumentsToBuildTarballs(self):
2536         """
2537         L{BuildTarballsScript.main} raises SystemExit when the wrong number of
2538         arguments are passed.
2539         """
2540         tarballBuilder = BuildTarballsScript()
2541         self.assertRaises(SystemExit, tarballBuilder.main, [])
2542         self.assertRaises(SystemExit, tarballBuilder.main, ["a", "b", "c", "d"])
2543
2544
2545     def test_badNumberOfArgumentsToBuildNews(self):
2546         """
2547         L{NewsBuilder.main} raises L{SystemExit} when other than 1 argument is
2548         passed to it.
2549         """
2550         newsBuilder = NewsBuilder()
2551         self.assertRaises(SystemExit, newsBuilder.main, [])
2552         self.assertRaises(SystemExit, newsBuilder.main, ["hello", "world"])
2553
2554
2555     def test_buildNews(self):
2556         """
2557         L{NewsBuilder.main} calls L{NewsBuilder.buildAll} with a L{FilePath}
2558         instance constructed from the path passed to it.
2559         """
2560         builds = []
2561         newsBuilder = NewsBuilder()
2562         newsBuilder.buildAll = builds.append
2563         newsBuilder.main(["/foo/bar/baz"])
2564         self.assertEqual(builds, [FilePath("/foo/bar/baz")])