1 # Copyright (c) Twisted Matrix Laboratories.
2 # See LICENSE for details.
5 Tests for L{twisted.python.release} and L{twisted.python._release}.
7 All of these tests are skipped on platforms other than Linux, as the release is
8 only ever performed on Linux.
14 import os, sys, signal
15 from StringIO import StringIO
17 from xml.dom import minidom as dom
19 from datetime import date
21 from twisted.trial.unittest import TestCase
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
44 if os.name != 'posix':
45 skip = "Release toolchain only supported on POSIX."
50 # Check a bunch of dependencies to skip tests if necessary.
52 from twisted.lore.scripts import lore
54 loreSkip = "Lore is not present."
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."
66 if getattr(pydoctor, "version_info", (0,)) < (0, 1):
67 pydoctorSkip = "Pydoctor is too old."
72 if which("latex") and which("dvips") and which("ps2pdf13"):
75 latexSkip = "LaTeX is not available."
78 if which("svn") and which("svnadmin"):
81 svnSkip = "svn or svnadmin is not present."
84 def genVersion(*args, **kwargs):
86 A convenience for generating _version.py data.
88 @param args: Arguments to pass to L{Version}.
89 @param kwargs: Keyword arguments to pass to L{Version}.
91 return generateVersionFileData(Version(*args, **kwargs))
95 class StructureAssertingMixin(object):
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.
100 def createStructure(self, root, dirDict):
102 Create a set of directories and files given a dict defining their
105 @param root: The directory in which to create the structure. It must
107 @type root: L{FilePath}
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::
116 {"foofile": "foocontents",
117 "bardir": {"barfile": "bar\ncontents"}}
118 @type dirDict: C{dict}
121 child = root.child(x)
122 if isinstance(dirDict[x], dict):
123 child.createDirectory()
124 self.createStructure(child, dirDict[x])
126 child.setContent(dirDict[x].replace('\n', os.linesep))
128 def assertStructure(self, root, dirDict):
130 Assert that a directory is equivalent to one described by a dict.
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}
139 children = [x.basename() for x in root.children()]
141 child = root.child(x)
142 if isinstance(dirDict[x], dict):
143 self.assertTrue(child.isdir(), "%s is not a dir!"
145 self.assertStructure(child, dirDict[x])
147 a = child.getContent().replace(os.linesep, '\n')
148 self.assertEqual(a, dirDict[x], child.path)
151 self.fail("There were extra children in %s: %s"
152 % (root.path, children))
155 def assertExtractedStructure(self, outputFile, dirDict):
157 Assert that a tarfile content is equivalent to one described by a dict.
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}
166 tarFile = tarfile.TarFile.open(outputFile.path, "r:bz2")
167 extracted = FilePath(self.mktemp())
168 extracted.createDirectory()
170 tarFile.extract(info, path=extracted.path)
171 self.assertStructure(extracted.children()[0], dirDict)
175 class ChangeVersionTest(TestCase, StructureAssertingMixin):
177 Twisted has the ability to change versions.
180 def makeFile(self, relativePath, content):
182 Create a file with the given content relative to a temporary directory.
184 @param relativePath: The basename of the file to create.
185 @param content: The content that the file will have.
186 @return: The filename.
188 baseDirectory = FilePath(self.mktemp())
189 directory, filename = os.path.split(relativePath)
190 directory = baseDirectory.preauthChild(directory)
192 file = directory.child(filename)
193 directory.child(filename).setContent(content)
197 def test_getNextVersion(self):
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.
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))
210 def test_getNextVersionAfterYearChange(self):
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.
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))
222 def test_changeVersionInFile(self):
224 _changeVersionInFile replaces the old version information in a file
225 with the given new version information.
227 # The version numbers are arbitrary, the name is only kind of
230 oldVersion = Version(packageName, 2, 5, 0)
231 file = self.makeFile('README',
232 "Hello and welcome to %s." % oldVersion.base())
234 newVersion = Version(packageName, 7, 6, 0)
235 _changeVersionInFile(oldVersion, newVersion, file.path)
237 self.assertEqual(file.getContent(),
238 "Hello and welcome to %s." % newVersion.base())
241 def test_changeAllProjectVersions(self):
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
247 root = FilePath(self.mktemp())
248 root.createDirectory()
250 "README": "Hi this is 1.0.0.",
253 "README": "Hi this is 1.0.0"},
255 genVersion("twisted", 1, 0, 0),
258 "README": "Hi this is 1.0.0"},
259 "_version.py": genVersion("twisted.web", 1, 0, 0)
261 self.createStructure(root, structure)
262 changeAllProjectVersions(root, Version("lol", 1, 0, 2))
264 "README": "Hi this is 1.0.2.",
267 "README": "Hi this is 1.0.2"},
269 genVersion("twisted", 1, 0, 2),
272 "README": "Hi this is 1.0.2"},
273 "_version.py": genVersion("twisted.web", 1, 0, 2),
275 self.assertStructure(root, outStructure)
278 def test_changeAllProjectVersionsPreRelease(self):
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.
285 root = FilePath(self.mktemp())
286 root.createDirectory()
287 coreNews = ("Twisted Core 1.0.0 (2009-12-25)\n"
288 "===============================\n"
290 webNews = ("Twisted Web 1.0.0pre1 (2009-12-25)\n"
291 "==================================\n"
294 "README": "Hi this is 1.0.0.",
295 "NEWS": coreNews + webNews,
298 "README": "Hi this is 1.0.0",
301 genVersion("twisted", 1, 0, 0),
304 "README": "Hi this is 1.0.0pre1",
306 "_version.py": genVersion("twisted.web", 1, 0, 0, 1)
308 self.createStructure(root, structure)
309 changeAllProjectVersions(root, Version("lol", 1, 0, 2), '2010-01-01')
311 "Twisted Core 1.0.0 (2009-12-25)\n"
312 "===============================\n"
314 webNews = ("Twisted Web 1.0.2 (2010-01-01)\n"
315 "==============================\n"
318 "README": "Hi this is 1.0.2.",
319 "NEWS": coreNews + webNews,
322 "README": "Hi this is 1.0.2",
325 genVersion("twisted", 1, 0, 2),
328 "README": "Hi this is 1.0.2",
330 "_version.py": genVersion("twisted.web", 1, 0, 2),
332 self.assertStructure(root, outStructure)
336 class ProjectTest(TestCase):
338 There is a first-class representation of a project.
341 def assertProjectsEqual(self, observedProjects, expectedProjects):
343 Assert that two lists of L{Project}s are equal.
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)
354 def makeProject(self, version, baseDirectory=None):
356 Make a Twisted-style project in the given base directory.
358 @param baseDirectory: The directory to create files in
360 @param version: The version information for the project.
361 @return: L{Project} pointing to the created project.
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)
380 def makeProjects(self, *versions):
382 Create a series of projects underneath a temporary base directory.
384 @return: A L{FilePath} for the base directory.
386 baseDirectory = FilePath(self.mktemp())
387 baseDirectory.createDirectory()
388 for version in versions:
389 self.makeProject(version, baseDirectory)
393 def test_getVersion(self):
395 Project objects know their version.
397 version = Version('foo', 2, 1, 0)
398 project = self.makeProject(version)
399 self.assertEqual(project.getVersion(), version)
402 def test_updateVersion(self):
404 Project objects know how to update the version numbers in those
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)
412 project.directory.child("topfiles").child("README").getContent(),
418 The representation of a Project is Project(directory).
420 foo = Project(FilePath('bar'))
422 repr(foo), 'Project(%r)' % (foo.directory))
425 def test_findTwistedStyleProjects(self):
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.
431 baseDirectory = self.makeProjects(
432 Version('foo', 2, 3, 0), Version('foo.bar', 0, 7, 4))
433 projects = findTwistedProjects(baseDirectory)
434 self.assertProjectsEqual(
436 [Project(baseDirectory.child('foo')),
437 Project(baseDirectory.child('foo').child('bar'))])
440 def test_updateTwistedVersionInformation(self):
442 Update Twisted version information in the top-level project and all of
445 baseDirectory = FilePath(self.mktemp())
446 baseDirectory.createDirectory()
450 oldVersion = Version(projectName, 2, 5, 0)
451 newVersion = getNextVersion(oldVersion, now=now)
453 project = self.makeProject(oldVersion, baseDirectory)
455 updateTwistedVersionInformation(baseDirectory, now=now)
457 self.assertEqual(project.getVersion(), newVersion)
459 project.directory.child('topfiles').child('README').getContent(),
464 class UtilityTest(TestCase):
466 Tests for various utility functions for releasing.
469 def test_chdir(self):
471 Test that the runChdirSafe is actually safe, i.e., it still
472 changes back to the original directory even if an error is
477 os.mkdir('releaseCh')
478 os.chdir('releaseCh')
480 self.assertRaises(ZeroDivisionError,
481 release.runChdirSafe, chAndBreak)
482 self.assertEqual(cwd, os.getcwd())
486 def test_replaceInFile(self):
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
492 in_ = 'foo\nhey hey $VER\nbar\n'
493 outf = open('release.replace', 'w')
497 expected = in_.replace('$VER', '2.0.0')
498 replaceInFile('release.replace', {'$VER': '2.0.0'})
499 self.assertEqual(open('release.replace').read(), expected)
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)
508 class VersionWritingTest(TestCase):
510 Tests for L{replaceProjectVersion}.
513 def test_replaceProjectVersion(self):
515 L{replaceProjectVersion} writes a Python file that defines a
516 C{version} variable that corresponds to the given name and version
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")
526 def test_replaceProjectVersionWithPrerelease(self):
528 L{replaceProjectVersion} will write a Version instantiation that
529 includes a prerelease parameter if necessary.
531 replaceProjectVersion("test_project",
532 Version("twisted.test_project", 0, 82, 7,
534 ns = {'__name___': 'twisted.test_project'}
535 execfile("test_project", ns)
536 self.assertEqual(ns["version"].base(), "0.82.7pre8")
540 class BuilderTestsMixin(object):
542 A mixin class which provides various methods for creating sample Lore input
545 @cvar template: The lore template that will be used to prepare sample
547 @type template: C{str}
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}
555 <head><title>Yo:</title></head>
558 <a href="index.html">Index</a>
559 <span class="version">Version: </span>
566 Initialize the doc counter which ensures documents are unique.
571 def assertXMLEqual(self, first, second):
573 Verify that two strings represent the same XML document.
576 dom.parseString(first).toxml(),
577 dom.parseString(second).toxml())
580 def getArbitraryOutput(self, version, counter, prefix="", apiBaseURL="%s"):
582 Get the correct HTML output for the arbitrary input returned by
583 L{getArbitraryLoreInput} for the given parameters.
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}
591 <?xml version="1.0"?><html>
592 <head><title>Yo:Hi! Title: %(count)d</title></head>
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>
599 # Try to normalize irrelevant whitespace.
600 return dom.parseString(
601 document % {"count": counter, "prefix": prefix,
603 "foobarLink": apiBaseURL % ("foobar",)}).toxml('utf-8')
606 def getArbitraryLoreInput(self, counter):
608 Get an arbitrary, unique (for this test case) string of lore input.
610 @param counter: A counter to include in the input.
611 @type counter: C{int}
615 '<head><title>Hi! Title: %(count)s</title></head>'
618 '<div class="API">foobar</div>'
621 return template % {"count": counter}
624 def getArbitraryLoreInputAndOutput(self, version, prefix="",
627 Get an input document along with expected output for lore run on that
628 output document, assuming an appropriately-specified C{self.template}.
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.
635 @return: A two-tuple of input and expected output.
636 @rtype: C{(str, str)}.
639 return (self.getArbitraryLoreInput(self.docCounter),
640 self.getArbitraryOutput(version, self.docCounter,
641 prefix=prefix, apiBaseURL=apiBaseURL))
644 def getArbitraryManInput(self):
646 Get an arbitrary man page content.
648 return """.TH MANHOLE "1" "August 2001" "" ""
650 manhole \- Connect to a Twisted Manhole service
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
659 def getArbitraryManLoreOutput(self):
661 Get an arbitrary lore input document which represents man-to-lore
662 output based on the man page returned from L{getArbitraryManInput}
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">
669 <title>MANHOLE.1</title></head>
676 <p>manhole - Connect to a Twisted Manhole service
681 <p><strong>manhole</strong> </p>
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
693 def getArbitraryManHTMLOutput(self, version, prefix=""):
695 Get an arbitrary lore output document which represents the lore HTML
696 output based on the input document returned from
697 L{getArbitraryManLoreOutput}.
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.
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>
709 <div class="content">
713 <h2>NAME<a name="auto0"/></h2>
715 <p>manhole - Connect to a Twisted Manhole service
718 <h2>SYNOPSIS<a name="auto1"/></h2>
720 <p><strong>manhole</strong> </p>
722 <h2>DESCRIPTION<a name="auto2"/></h2>
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
729 <a href="%(prefix)sindex.html">Index</a>
730 <span class="version">Version: %(version)s</span>
733 'prefix': prefix, 'version': version}).toxml("utf-8")
737 class DocBuilderTestCase(TestCase, BuilderTestsMixin):
739 Tests for L{DocBuilder}.
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
751 Set up a few instance variables that will be useful.
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.
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)
769 def test_build(self):
771 The L{DocBuilder} runs lore on all .xhtml files within a directory.
774 input1, output1 = self.getArbitraryLoreInputAndOutput(version)
775 input2, output2 = self.getArbitraryLoreInputAndOutput(version)
777 self.howtoDir.child("one.xhtml").setContent(input1)
778 self.howtoDir.child("two.xhtml").setContent(input2)
780 self.builder.build(version, self.howtoDir, self.howtoDir,
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)
788 def test_noDocumentsFound(self):
790 The C{build} method raises L{NoDocumentsFound} if there are no
791 .xhtml files in the given directory.
795 self.builder.build, "1.2.3", self.howtoDir, self.howtoDir,
799 def test_parentDocumentLinking(self):
801 The L{DocBuilder} generates correct links from documents to
802 template-generated links like stylesheets and index backreferences.
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())
814 def test_siblingDirectoryDocumentLinking(self):
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.
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())
831 def test_apiLinking(self):
833 The L{DocBuilder} generates correct links from documents to API
837 input, output = self.getArbitraryLoreInputAndOutput(version)
838 self.howtoDir.child("one.xhtml").setContent(input)
840 self.builder.build(version, self.howtoDir, self.howtoDir,
841 self.templateFile, "scheme:apilinks/%s.ext")
842 out = self.howtoDir.child('one.html')
844 '<a href="scheme:apilinks/foobar.ext" title="foobar">foobar</a>',
848 def test_deleteInput(self):
850 L{DocBuilder.build} can be instructed to delete the input files after
851 generating the output based on them.
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())
861 def test_doNotDeleteInput(self):
863 Input will not be deleted by default.
865 input1 = self.getArbitraryLoreInput(0)
866 self.howtoDir.child("one.xhtml").setContent(input1)
867 self.builder.build("whatever", self.howtoDir, self.howtoDir,
869 self.assertTrue(self.howtoDir.child('one.html').exists())
870 self.assertTrue(self.howtoDir.child('one.xhtml').exists())
873 def test_getLinkrelToSameDirectory(self):
875 If the doc and resource directories are the same, the linkrel should be
878 linkrel = self.builder.getLinkrel(FilePath("/foo/bar"),
879 FilePath("/foo/bar"))
880 self.assertEqual(linkrel, "")
883 def test_getLinkrelToParentDirectory(self):
885 If the doc directory is a child of the resource directory, the linkrel
886 should make use of '..'.
888 linkrel = self.builder.getLinkrel(FilePath("/foo"),
889 FilePath("/foo/bar"))
890 self.assertEqual(linkrel, "../")
893 def test_getLinkrelToSibling(self):
895 If the doc directory is a sibling of the resource directory, the
896 linkrel should make use of '..' and a named segment.
898 linkrel = self.builder.getLinkrel(FilePath("/foo/howto"),
899 FilePath("/foo/examples"))
900 self.assertEqual(linkrel, "../howto/")
903 def test_getLinkrelToUncle(self):
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
909 linkrel = self.builder.getLinkrel(FilePath("/foo/howto"),
910 FilePath("/foo/examples/quotes"))
911 self.assertEqual(linkrel, "../../howto/")
915 class APIBuilderTestCase(TestCase):
917 Tests for L{APIBuilder}.
921 def test_build(self):
923 L{APIBuilder.build} writes an index file which includes the name of the
927 self.patch(sys, 'stdout', stdout)
929 projectName = "Foobar"
931 projectURL = "scheme:project"
932 sourceURL = "scheme:source"
933 docstring = "text in docstring"
934 privateDocstring = "should also appear in output"
936 inputPath = FilePath(self.mktemp()).child(packageName)
938 inputPath.child("__init__.py").setContent(
942 " '%s'" % (docstring, privateDocstring))
944 outputPath = FilePath(self.mktemp())
945 outputPath.makedirs()
947 builder = APIBuilder()
948 builder.build(projectName, projectURL, sourceURL, inputPath, outputPath)
950 indexPath = outputPath.child("index.html")
953 "API index %r did not exist." % (outputPath.path,))
955 '<a href="%s">%s</a>' % (projectURL, projectName),
956 indexPath.getContent(),
957 "Project name/location not in file contents.")
959 quuxPath = outputPath.child("quux.html")
962 "Package documentation file %r did not exist." % (quuxPath.path,))
964 docstring, quuxPath.getContent(),
965 "Docstring not in package documentation file.")
967 '<a href="%s/%s">View Source</a>' % (sourceURL, packageName),
968 quuxPath.getContent())
970 '<a href="%s/%s/__init__.py#L1" class="functionSourceLink">' % (
971 sourceURL, packageName),
972 quuxPath.getContent())
973 self.assertIn(privateDocstring, quuxPath.getContent())
975 # There should also be a page for the foo function in quux.
976 self.assertTrue(quuxPath.sibling('quux.foo.html').exists())
978 self.assertEqual(stdout.getvalue(), '')
981 def test_buildWithPolicy(self):
983 L{BuildAPIDocsScript.buildAPIDocs} builds the API docs with values
984 appropriate for the Twisted project.
987 self.patch(sys, 'stdout', stdout)
988 docstring = "text in docstring"
990 projectRoot = FilePath(self.mktemp())
991 packagePath = projectRoot.child("twisted")
992 packagePath.makedirs()
993 packagePath.child("__init__.py").setContent(
995 " '%s'\n" % (docstring,))
996 packagePath.child("_version.py").setContent(
997 genVersion("twisted", 1, 0, 0))
998 outputPath = FilePath(self.mktemp())
1000 script = BuildAPIDocsScript()
1001 script.buildAPIDocs(projectRoot, outputPath)
1003 indexPath = outputPath.child("index.html")
1006 "API index %r did not exist." % (outputPath.path,))
1008 '<a href="http://twistedmatrix.com/">Twisted</a>',
1009 indexPath.getContent(),
1010 "Project name/location not in file contents.")
1012 twistedPath = outputPath.child("twisted.html")
1014 twistedPath.exists(),
1015 "Package documentation file %r did not exist."
1016 % (twistedPath.path,))
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
1023 '<a href="http://twistedmatrix.com/trac/browser/tags/releases/'
1024 'twisted-1.0.0/twisted">View Source</a>',
1025 twistedPath.getContent())
1027 self.assertEqual(stdout.getvalue(), '')
1030 def test_apiBuilderScriptMainRequiresTwoArguments(self):
1032 SystemExit is raised when the incorrect number of command line
1033 arguments are passed to the API building script.
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"])
1041 def test_apiBuilderScriptMain(self):
1043 The API building script invokes the same code that
1044 L{test_buildWithPolicy} tests.
1046 script = BuildAPIDocsScript()
1048 script.buildAPIDocs = lambda a, b: calls.append((a, b))
1049 script.main(["hello", "there"])
1050 self.assertEqual(calls, [(FilePath("hello"), FilePath("there"))])
1054 class ManBuilderTestCase(TestCase, BuilderTestsMixin):
1056 Tests for L{ManBuilder}.
1062 Set up a few instance variables that will be useful.
1064 @ivar builder: A plain L{ManBuilder}.
1065 @ivar manDir: A L{FilePath} representing a directory to be used for
1066 containing man pages.
1068 BuilderTestsMixin.setUp(self)
1069 self.builder = ManBuilder()
1070 self.manDir = FilePath(self.mktemp())
1071 self.manDir.createDirectory()
1074 def test_noDocumentsFound(self):
1076 L{ManBuilder.build} raises L{NoDocumentsFound} if there are no
1077 .1 files in the given directory.
1079 self.assertRaises(NoDocumentsFound, self.builder.build, self.manDir)
1082 def test_build(self):
1084 Check that L{ManBuilder.build} find the man page in the directory, and
1085 successfully produce a Lore content.
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)
1097 def test_toHTML(self):
1099 Check that the content output by C{build} is compatible as input of
1100 L{DocBuilder.build}.
1102 manContent = self.getArbitraryManInput()
1103 self.manDir.child('test1.1').setContent(manContent)
1104 self.builder.build(self.manDir)
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,
1111 output = self.manDir.child('test1-man.html').getContent()
1113 self.assertXMLEqual(
1116 <?xml version="1.0" ?><html>
1117 <head><title>Yo:MANHOLE.1</title></head>
1119 <div class="content">
1123 <h2>NAME<a name="auto0"/></h2>
1125 <p>manhole - Connect to a Twisted Manhole service
1128 <h2>SYNOPSIS<a name="auto1"/></h2>
1130 <p><strong>manhole</strong> </p>
1132 <h2>DESCRIPTION<a name="auto2"/></h2>
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
1139 <a href="index.html">Index</a>
1140 <span class="version">Version: 1.2.3</span>
1146 class BookBuilderTests(TestCase, BuilderTestsMixin):
1148 Tests for L{BookBuilder}.
1150 skip = latexSkip or loreSkip
1154 Make a directory into which to place temporary files.
1157 self.howtoDir = FilePath(self.mktemp())
1158 self.howtoDir.makedirs()
1159 self.oldHandler = signal.signal(signal.SIGCHLD, signal.SIG_DFL)
1163 signal.signal(signal.SIGCHLD, self.oldHandler)
1166 def getArbitraryOutput(self, version, counter, prefix="", apiBaseURL=None):
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)}.
1172 path = self.howtoDir.child("%d.xhtml" % (counter,)).path
1174 r'\section{Hi! Title: %(count)s\label{%(path)s}}'
1176 r'Hi! %(count)sfoobar') % {'count': counter, 'path': path}
1179 def test_runSuccess(self):
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
1185 builder = BookBuilder()
1188 sys.executable, '-c',
1190 'sys.stdout.write("hi\\n"); '
1191 'sys.stdout.flush(); '
1192 'sys.stderr.write("bye\\n"); '
1193 'sys.stderr.flush()']),
1197 def test_runFailed(self):
1199 L{BookBuilder.run} executes the command it is passed and raises
1200 L{CommandFailed} if it completes unsuccessfully.
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")
1211 def test_runSignaled(self):
1213 L{BookBuilder.run} executes the command it is passed and raises
1214 L{CommandFailed} if it exits due to a signal.
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")
1227 def test_buildTeX(self):
1229 L{BookBuilder.buildTeX} writes intermediate TeX files for all lore
1230 input files in a directory.
1233 input1, output1 = self.getArbitraryLoreInputAndOutput(version)
1234 input2, output2 = self.getArbitraryLoreInputAndOutput(version)
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)
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)
1247 def test_buildTeXRejectsInvalidDirectory(self):
1249 L{BookBuilder.buildTeX} raises L{ValueError} if passed a directory
1250 which does not exist.
1252 builder = BookBuilder()
1254 ValueError, builder.buildTeX, self.howtoDir.temporarySibling())
1257 def test_buildTeXOnlyBuildsXHTML(self):
1259 L{BookBuilder.buildTeX} ignores files which which don't end with
1262 # Hopefully ">" is always a parse error from microdom!
1263 self.howtoDir.child("not-input.dat").setContent(">")
1264 self.test_buildTeX()
1267 def test_stdout(self):
1269 L{BookBuilder.buildTeX} does not write to stdout.
1272 self.patch(sys, 'stdout', stdout)
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(), '')
1281 def test_buildPDFRejectsInvalidBookFilename(self):
1283 L{BookBuilder.buildPDF} raises L{ValueError} if the book filename does
1284 not end with ".tex".
1286 builder = BookBuilder()
1290 FilePath(self.mktemp()).child("foo"),
1295 def _setupTeXFiles(self):
1297 self._setupTeXSections(sections)
1298 return self._setupTeXBook(sections)
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))
1308 def _setupTeXBook(self, sections):
1309 bookTeX = self.howtoDir.child("book.tex")
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")
1318 def test_buildPDF(self):
1320 L{BookBuilder.buildPDF} creates a PDF given an index tex file and a
1321 directory containing .tex files.
1323 bookPath = self._setupTeXFiles()
1324 outputPath = FilePath(self.mktemp())
1326 builder = BookBuilder()
1327 builder.buildPDF(bookPath, self.howtoDir, outputPath)
1329 self.assertTrue(outputPath.exists())
1332 def test_buildPDFLongPath(self):
1334 L{BookBuilder.buildPDF} succeeds even if the paths it is operating on
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
1342 self.howtoDir = self.howtoDir.child("x" * 128).child("x" * 128).child("x" * 128)
1343 self.howtoDir.makedirs()
1345 # This will use the above long path.
1346 bookPath = self._setupTeXFiles()
1347 outputPath = FilePath(self.mktemp())
1349 builder = BookBuilder()
1350 builder.buildPDF(bookPath, self.howtoDir, outputPath)
1352 self.assertTrue(outputPath.exists())
1355 def test_buildPDFRunsLaTeXThreeTimes(self):
1357 L{BookBuilder.buildPDF} runs C{latex} three times.
1359 class InspectableBookBuilder(BookBuilder):
1361 BookBuilder.__init__(self)
1364 def run(self, command):
1366 Record the command and then execute it.
1368 self.commands.append(command)
1369 return BookBuilder.run(self, command)
1371 bookPath = self._setupTeXFiles()
1372 outputPath = FilePath(self.mktemp())
1374 builder = InspectableBookBuilder()
1375 builder.buildPDF(bookPath, self.howtoDir, outputPath)
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)
1384 latex1[:1], ["latex"],
1385 "LaTeX command %r does not seem right." % (latex1,))
1387 latex1[-1:], [bookPath.path],
1388 "LaTeX command %r does not end with the book path (%r)." % (
1389 latex1, bookPath.path))
1392 dvips[:1], ["dvips"],
1393 "dvips command %r does not seem right." % (dvips,))
1395 ps2pdf13[:1], ["ps2pdf13"],
1396 "ps2pdf13 command %r does not seem right." % (ps2pdf13,))
1399 def test_noSideEffects(self):
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.
1405 startDir = os.getcwd()
1406 bookTeX = self._setupTeXFiles()
1407 startTeXSiblings = bookTeX.parent().children()
1408 startHowtoChildren = self.howtoDir.children()
1410 builder = BookBuilder()
1411 builder.buildPDF(bookTeX, self.howtoDir, FilePath(self.mktemp()))
1413 self.assertEqual(startDir, os.getcwd())
1414 self.assertEqual(startTeXSiblings, bookTeX.parent().children())
1415 self.assertEqual(startHowtoChildren, self.howtoDir.children())
1418 def test_failedCommandProvidesOutput(self):
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.
1424 bookTeX = FilePath(self.mktemp() + ".tex")
1425 builder = BookBuilder()
1426 inputState = bookTeX.parent().children()
1427 exc = self.assertRaises(
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()
1437 "Expected work path %r was not a directory." % (workPath.path,))
1440 def test_build(self):
1442 L{BookBuilder.build} generates a pdf book file from some lore input
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())
1452 builder = BookBuilder()
1453 builder.build(self.howtoDir, [self.howtoDir], bookTeX, bookPDF)
1455 self.assertTrue(bookPDF.exists())
1458 def test_buildRemovesTemporaryLaTeXFiles(self):
1460 L{BookBuilder.build} removes the intermediate LaTeX files it creates.
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())
1469 builder = BookBuilder()
1470 builder.build(self.howtoDir, [self.howtoDir], bookTeX, bookPDF)
1473 set(self.howtoDir.listdir()),
1474 set([bookTeX.basename()] + ["%d.xhtml" % (n,) for n in sections]))
1478 class FilePathDeltaTest(TestCase):
1480 Tests for L{filePathDelta}.
1483 def test_filePathDeltaSubdir(self):
1485 L{filePathDelta} can create a simple relative path to a child path.
1487 self.assertEqual(filePathDelta(FilePath("/foo/bar"),
1488 FilePath("/foo/bar/baz")),
1492 def test_filePathDeltaSiblingDir(self):
1494 L{filePathDelta} can traverse upwards to create relative paths to
1497 self.assertEqual(filePathDelta(FilePath("/foo/bar"),
1498 FilePath("/foo/baz")),
1502 def test_filePathNoCommonElements(self):
1504 L{filePathDelta} can create relative paths to totally unrelated paths
1505 for maximum portability.
1507 self.assertEqual(filePathDelta(FilePath("/foo/bar"),
1508 FilePath("/baz/quux")),
1509 ["..", "..", "baz", "quux"])
1512 def test_filePathDeltaSimilarEndElements(self):
1514 L{filePathDelta} doesn't take into account final elements when
1515 comparing 2 paths, but stops at the first difference.
1517 self.assertEqual(filePathDelta(FilePath("/foo/bar/bar/spam"),
1518 FilePath("/foo/bar/baz/spam")),
1519 ["..", "..", "baz", "spam"])
1523 class NewsBuilderTests(TestCase, StructureAssertingMixin):
1525 Tests for L{NewsBuilder}.
1529 Create a fake project and stuff some basic structure and content into
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',
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'),
1546 'A simpler feature\ndescribed on multiple lines\n'
1548 '23.bugfix': 'Broken stuff was fixed.\n',
1549 '25.removal': 'Stupid stuff was deprecated.\n',
1552 '40.doc': 'foo.bar.Baz.quux',
1553 '41.doc': 'writing Foo servers'})
1556 def test_today(self):
1558 L{NewsBuilder._today} returns today's date in YYYY-MM-DD form.
1561 self.builder._today(), date.today().strftime('%Y-%m-%d'))
1564 def test_findFeatures(self):
1566 When called with L{NewsBuilder._FEATURE}, L{NewsBuilder._findChanges}
1567 returns a list of bugfix ticket numbers and descriptions as a list of
1570 features = self.builder._findChanges(
1571 self.project, self.builder._FEATURE)
1574 [(5, "We now support the web."),
1575 (12, "The widget is more robust."),
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.")])
1583 def test_findBugfixes(self):
1585 When called with L{NewsBuilder._BUGFIX}, L{NewsBuilder._findChanges}
1586 returns a list of bugfix ticket numbers and descriptions as a list of
1589 bugfixes = self.builder._findChanges(
1590 self.project, self.builder._BUGFIX)
1593 [(23, 'Broken stuff was fixed.')])
1596 def test_findRemovals(self):
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.
1602 removals = self.builder._findChanges(
1603 self.project, self.builder._REMOVAL)
1606 [(25, 'Stupid stuff was deprecated.')])
1609 def test_findDocumentation(self):
1611 When called with L{NewsBuilder._DOC}, L{NewsBuilder._findChanges}
1612 returns a list of documentation ticket numbers and descriptions as a
1615 doc = self.builder._findChanges(
1616 self.project, self.builder._DOC)
1619 [(40, 'foo.bar.Baz.quux'),
1620 (41, 'writing Foo servers')])
1623 def test_findMiscellaneous(self):
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.
1629 misc = self.builder._findChanges(
1630 self.project, self.builder._MISC)
1637 def test_writeHeader(self):
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.
1643 self.builder._writeHeader(output, "Super Awesometastic 32.16")
1646 "Super Awesometastic 32.16\n"
1647 "=========================\n"
1651 def test_writeSection(self):
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.
1659 self.builder._writeSection(
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.")])
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"
1674 def test_writeMisc(self):
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.
1682 self.builder._writeMisc(
1684 [(x, "") for x in range(2, 50, 3)])
1689 " - #2, #5, #8, #11, #14, #17, #20, #23, #26, #29, #32, #35, #38, #41,\n"
1694 def test_build(self):
1696 L{NewsBuilder.build} updates a NEWS file with new features based on the
1697 I{<ticket>.feature} files found in the directory specified.
1700 self.project, self.project.child('NEWS'),
1701 "Super Awesometastic 32.16")
1703 results = self.project.child('NEWS').getContent()
1706 'Super Awesometastic 32.16\n'
1707 '=========================\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'
1720 ' - Broken stuff was fixed. (#23)\n'
1722 'Improved Documentation\n'
1723 '----------------------\n'
1724 ' - foo.bar.Baz.quux (#40)\n'
1725 ' - writing Foo servers (#41)\n'
1727 'Deprecations and Removals\n'
1728 '-------------------------\n'
1729 ' - Stupid stuff was deprecated. (#25)\n'
1734 '\n\n' + self.existingText)
1737 def test_emptyProjectCalledOut(self):
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.
1743 project = FilePath(self.mktemp()).child("twisted")
1745 self.createStructure(project, {
1746 'NEWS': self.existingText })
1749 project, project.child('NEWS'),
1750 "Super Awesometastic 32.16")
1751 results = project.child('NEWS').getContent()
1754 'Super Awesometastic 32.16\n'
1755 '=========================\n'
1757 self.builder._NO_CHANGES +
1758 '\n\n' + self.existingText)
1761 def test_preserveTicketHint(self):
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.
1766 news = self.project.child('NEWS')
1768 'Ticket numbers in this file can be looked up by visiting\n'
1769 'http://twistedmatrix.com/trac/ticket/<number>\n'
1771 'Blah blah other stuff.\n')
1773 self.builder.build(self.project, news, "Super Awesometastic 32.16")
1777 'Ticket numbers in this file can be looked up by visiting\n'
1778 'http://twistedmatrix.com/trac/ticket/<number>\n'
1780 'Super Awesometastic 32.16\n'
1781 '=========================\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'
1794 ' - Broken stuff was fixed. (#23)\n'
1796 'Improved Documentation\n'
1797 '----------------------\n'
1798 ' - foo.bar.Baz.quux (#40)\n'
1799 ' - writing Foo servers (#41)\n'
1801 'Deprecations and Removals\n'
1802 '-------------------------\n'
1803 ' - Stupid stuff was deprecated. (#25)\n'
1809 'Blah blah other stuff.\n')
1812 def test_emptySectionsOmitted(self):
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}.
1817 for ticket in self.project.children():
1818 if ticket.splitext()[1] in ('.feature', '.misc', '.doc'):
1822 self.project, self.project.child('NEWS'),
1826 self.project.child('NEWS').getContent(),
1832 ' - Broken stuff was fixed. (#23)\n'
1834 'Deprecations and Removals\n'
1835 '-------------------------\n'
1836 ' - Stupid stuff was deprecated. (#25)\n'
1838 'Here is stuff which was present previously.\n')
1841 def test_duplicatesMerged(self):
1843 If two change files have the same contents, they are merged in the
1844 generated news entry.
1847 return self.project.child(s + '.feature')
1848 feature('5').copyTo(feature('15'))
1849 feature('5').copyTo(feature('16'))
1852 self.project, self.project.child('NEWS'),
1856 self.project.child('NEWS').getContent(),
1857 'Project Name 5.0\n'
1858 '================\n'
1862 ' - We now support the web. (#5, #15, #16)\n'
1863 ' - The widget is more robust. (#12)\n'
1867 ' - Broken stuff was fixed. (#23)\n'
1869 'Improved Documentation\n'
1870 '----------------------\n'
1871 ' - foo.bar.Baz.quux (#40)\n'
1872 ' - writing Foo servers (#41)\n'
1874 'Deprecations and Removals\n'
1875 '-------------------------\n'
1876 ' - Stupid stuff was deprecated. (#25)\n'
1882 'Here is stuff which was present previously.\n')
1885 def createFakeTwistedProject(self):
1887 Create a fake-looking Twisted project to build from.
1889 project = FilePath(self.mktemp()).child("twisted")
1891 self.createStructure(project, {
1892 'NEWS': 'Old boring stuff from the past.\n',
1893 '_version.py': genVersion("twisted", 1, 2, 3),
1895 'NEWS': 'Old core news.\n',
1896 '3.feature': 'Third feature addition.\n',
1899 '_version.py': genVersion("twisted.conch", 3, 4, 5),
1901 'NEWS': 'Old conch news.\n',
1902 '7.bugfix': 'Fixed that bug.\n'}},
1907 def test_buildAll(self):
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
1917 builder = NewsBuilder()
1918 builder.build = lambda path, output, header: builds.append((
1919 path, output, header))
1920 builder._today = lambda: '2009-12-01'
1922 project = self.createFakeTwistedProject()
1923 builder.buildAll(project)
1925 coreTopfiles = project.child("topfiles")
1926 coreNews = coreTopfiles.child("NEWS")
1927 coreHeader = "Twisted Core 1.2.3 (2009-12-01)"
1929 conchTopfiles = project.child("conch").child("topfiles")
1930 conchNews = conchTopfiles.child("NEWS")
1931 conchHeader = "Twisted Conch 3.4.5 (2009-12-01)"
1933 aggregateNews = project.child("NEWS")
1937 [(conchTopfiles, conchNews, conchHeader),
1938 (coreTopfiles, coreNews, coreHeader),
1939 (conchTopfiles, aggregateNews, conchHeader),
1940 (coreTopfiles, aggregateNews, coreHeader)])
1943 def test_changeVersionInNews(self):
1945 L{NewsBuilder._changeVersions} gets the release date for a given
1946 version of a project as a string.
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')
1959 'Twisted Core 7.7.14 (2010-01-01)\n'
1960 '================================\n'
1964 ' - Third feature addition. (#3)\n'
1970 expectedCore + 'Old core news.\n', coreNews.getContent())
1974 class DistributionBuilderTestBase(BuilderTestsMixin, StructureAssertingMixin,
1977 Base for tests of L{DistributionBuilder}.
1982 BuilderTestsMixin.setUp(self)
1984 self.rootDir = FilePath(self.mktemp())
1985 self.rootDir.createDirectory()
1987 self.outputDir = FilePath(self.mktemp())
1988 self.outputDir.createDirectory()
1989 self.builder = DistributionBuilder(self.rootDir, self.outputDir)
1993 class DistributionBuilderTest(DistributionBuilderTestBase):
1995 def test_twistedDistribution(self):
1997 The Twisted tarball contains everything in the source checkout, with
1998 built documentation.
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/")
2009 "README": "Twisted",
2011 "LICENSE": "copyright!",
2012 "setup.py": "import toplevel",
2013 "bin": {"web": {"websetroot": "SET ROOT"},
2014 "twistd": "TWISTD"},
2017 {"__init__.py": "import WEB",
2018 "topfiles": {"setup.py": "import WEBINSTALL",
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}}}
2030 "README": "Twisted",
2032 "LICENSE": "copyright!",
2033 "setup.py": "import toplevel",
2034 "bin": {"web": {"websetroot": "SET ROOT"},
2035 "twistd": "TWISTD"},
2037 {"web": {"__init__.py": "import WEB",
2038 "topfiles": {"setup.py": "import WEBINSTALL",
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}}}
2051 self.createStructure(self.rootDir, structure)
2053 outputFile = self.builder.buildTwisted("10.0.0")
2055 self.assertExtractedStructure(outputFile, outStructure)
2058 def test_subProjectLayout(self):
2060 The subproject tarball includes files like so:
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
2073 "LICENSE": "copyright!",
2074 "setup.py": "import toplevel",
2075 "bin": {"web": {"websetroot": "SET ROOT"},
2076 "words": {"im": "#!im"}},
2079 {"__init__.py": "import WEB",
2080 "topfiles": {"setup.py": "import WEBINSTALL",
2082 "words": {"__init__.py": "import WORDS"},
2083 "plugins": {"twisted_web.py": "import WEBPLUG",
2084 "twisted_words.py": "import WORDPLUG"}}}
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"}}}
2094 self.createStructure(self.rootDir, structure)
2096 outputFile = self.builder.buildSubProject("web", "0.3.0")
2098 self.assertExtractedStructure(outputFile, outStructure)
2101 def test_minimalSubProjectLayout(self):
2103 buildSubProject should work with minimal subprojects.
2106 "LICENSE": "copyright!",
2109 {"web": {"__init__.py": "import WEB",
2110 "topfiles": {"setup.py": "import WEBINSTALL"}},
2114 "setup.py": "import WEBINSTALL",
2115 "LICENSE": "copyright!",
2116 "twisted": {"web": {"__init__.py": "import WEB"}}}
2118 self.createStructure(self.rootDir, structure)
2120 outputFile = self.builder.buildSubProject("web", "0.3.0")
2122 self.assertExtractedStructure(outputFile, outStructure)
2125 def test_subProjectDocBuilding(self):
2127 When building a subproject release, documentation should be built with
2130 loreInput, loreOutput = self.getArbitraryLoreInputAndOutput("0.3.0")
2131 manInput = self.getArbitraryManInput()
2132 manOutput = self.getArbitraryManHTMLOutput("0.3.0", "../howto/")
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}}
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}}}
2151 self.createStructure(self.rootDir, structure)
2153 outputFile = self.builder.buildSubProject("web", "0.3.0")
2155 self.assertExtractedStructure(outputFile, outStructure)
2158 def test_coreProjectLayout(self):
2160 The core tarball looks a lot like a subproject tarball, except it
2163 - Python packages from other subprojects
2164 - plugins from other subprojects
2165 - scripts from other subprojects
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="../")
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,
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"}}
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"},
2219 self.createStructure(self.rootDir, structure)
2220 outputFile = self.builder.buildCore("8.0.0")
2221 self.assertExtractedStructure(outputFile, outStructure)
2224 def test_apiBaseURL(self):
2226 DistributionBuilder builds documentation with the specified
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)
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}}
2244 "LICENSE": "copyright!",
2245 "setup.py": "import WEBINST",
2246 "twisted": {"web": {"__init__.py": "import WEB"}},
2247 "doc": {"howto": {"index.html": loreOutput}}}
2249 self.createStructure(self.rootDir, structure)
2250 outputFile = builder.buildSubProject("web", "0.3.0")
2251 self.assertExtractedStructure(outputFile, outStructure)
2255 class BuildAllTarballsTest(DistributionBuilderTestBase):
2257 Tests for L{DistributionBuilder.buildAllTarballs}.
2262 self.oldHandler = signal.signal(signal.SIGCHLD, signal.SIG_DFL)
2263 DistributionBuilderTestBase.setUp(self)
2267 signal.signal(signal.SIGCHLD, self.oldHandler)
2268 DistributionBuilderTestBase.tearDown(self)
2271 def test_buildAllTarballs(self):
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.
2278 repositoryPath = self.mktemp()
2279 repository = FilePath(repositoryPath)
2280 checkoutPath = self.mktemp()
2281 checkout = FilePath(checkoutPath)
2282 self.outputDir.remove()
2284 runCommand(["svnadmin", "create", repositoryPath])
2285 runCommand(["svn", "checkout", "file://" + repository.path,
2287 coreIndexInput, coreIndexOutput = self.getArbitraryLoreInputAndOutput(
2288 "1.2.0", prefix="howto/",
2289 apiBaseURL="http://twistedmatrix.com/documents/1.2.0/api/%s.html")
2292 "README": "Twisted",
2294 "LICENSE": "copyright!",
2295 "setup.py": "import toplevel",
2296 "bin": {"words": {"im": "import im"},
2297 "twistd": "TWISTD"},
2300 "topfiles": {"setup.py": "import TOPINSTALL",
2302 "_version.py": genVersion("twisted", 1, 2, 0),
2303 "words": {"__init__.py": "import WORDS",
2305 genVersion("twisted.words", 1, 2, 0),
2306 "topfiles": {"setup.py": "import WORDSINSTALL",
2307 "README": "WORDS!"},
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}}}
2315 twistedStructure = {
2316 "README": "Twisted",
2318 "LICENSE": "copyright!",
2319 "setup.py": "import toplevel",
2320 "bin": {"twistd": "TWISTD",
2321 "words": {"im": "import im"}},
2324 "topfiles": {"setup.py": "import TOPINSTALL",
2326 "_version.py": genVersion("twisted", 1, 2, 0),
2327 "words": {"__init__.py": "import WORDS",
2329 genVersion("twisted.words", 1, 2, 0),
2330 "topfiles": {"setup.py": "import WORDSINSTALL",
2331 "README": "WORDS!"},
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}}}
2340 "setup.py": "import TOPINSTALL",
2342 "LICENSE": "copyright!",
2343 "bin": {"twistd": "TWISTD"},
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}}
2352 "LICENSE": "copyright!",
2353 "setup.py": "import WORDSINSTALL",
2354 "bin": {"im": "import im"},
2357 "words": {"__init__.py": "import WORDS",
2359 genVersion("twisted.words", 1, 2, 0),
2361 "plugins": {"twisted_words.py": "import WORDPLUG"}}}
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"])
2368 buildAllTarballs(checkout, self.outputDir)
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")]))
2375 self.assertExtractedStructure(
2376 self.outputDir.child("Twisted-1.2.0.tar.bz2"),
2378 self.assertExtractedStructure(
2379 self.outputDir.child("TwistedCore-1.2.0.tar.bz2"),
2381 self.assertExtractedStructure(
2382 self.outputDir.child("TwistedWords-1.2.0.tar.bz2"),
2386 def test_buildAllTarballsEnsuresCleanCheckout(self):
2388 L{UncleanWorkingDirectory} is raised by L{buildAllTarballs} when the
2389 SVN checkout provided has uncommitted changes.
2391 repositoryPath = self.mktemp()
2392 repository = FilePath(repositoryPath)
2393 checkoutPath = self.mktemp()
2394 checkout = FilePath(checkoutPath)
2396 runCommand(["svnadmin", "create", repositoryPath])
2397 runCommand(["svn", "checkout", "file://" + repository.path,
2400 checkout.child("foo").setContent("whatever")
2401 self.assertRaises(UncleanWorkingDirectory,
2402 buildAllTarballs, checkout, FilePath(self.mktemp()))
2405 def test_buildAllTarballsEnsuresExistingCheckout(self):
2407 L{NotWorkingDirectory} is raised by L{buildAllTarballs} when the
2408 checkout passed does not exist or is not an SVN checkout.
2410 checkout = FilePath(self.mktemp())
2411 self.assertRaises(NotWorkingDirectory,
2413 checkout, FilePath(self.mktemp()))
2414 checkout.createDirectory()
2415 self.assertRaises(NotWorkingDirectory,
2417 checkout, FilePath(self.mktemp()))
2421 class ScriptTests(BuilderTestsMixin, StructureAssertingMixin, TestCase):
2423 Tests for the release script functionality.
2426 def _testVersionChanging(self, major, minor, micro, prerelease=None):
2428 Check that L{ChangeVersionsScript.main} calls the version-changing
2429 function with the appropriate version data and filesystem path.
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)
2448 def test_changeVersions(self):
2450 L{ChangeVersionsScript.main} changes version numbers for all Twisted
2453 self._testVersionChanging(8, 2, 3)
2456 def test_changeVersionsWithPrerelease(self):
2458 A prerelease can be specified to L{changeVersionsScript}.
2460 self._testVersionChanging(9, 2, 7, 38)
2463 def test_defaultChangeVersionsVersionChanger(self):
2465 The default implementation of C{changeAllProjectVersions} is
2466 L{changeAllProjectVersions}.
2468 versionChanger = ChangeVersionsScript()
2469 self.assertEqual(versionChanger.changeAllProjectVersions,
2470 changeAllProjectVersions)
2473 def test_badNumberOfArgumentsToChangeVersionsScript(self):
2475 L{changeVersionsScript} raises SystemExit when the wrong number of
2476 arguments are passed.
2478 versionChanger = ChangeVersionsScript()
2479 self.assertRaises(SystemExit, versionChanger.main, [])
2482 def test_tooManyDotsToChangeVersionsScript(self):
2484 L{changeVersionsScript} raises SystemExit when there are the wrong
2485 number of segments in the version number passed.
2487 versionChanger = ChangeVersionsScript()
2488 self.assertRaises(SystemExit, versionChanger.main,
2492 def test_nonIntPartsToChangeVersionsScript(self):
2494 L{changeVersionsScript} raises SystemExit when the version number isn't
2495 made out of numbers.
2497 versionChanger = ChangeVersionsScript()
2498 self.assertRaises(SystemExit, versionChanger.main,
2499 ["my united.states.of prewhatever"])
2502 def test_buildTarballsScript(self):
2504 L{BuildTarballsScript.main} invokes L{buildAllTarballs} with
2505 2 or 3 L{FilePath} instances representing the paths passed to it.
2508 def myBuilder(checkout, destination, template=None):
2509 builds.append((checkout, destination, template))
2510 tarballBuilder = BuildTarballsScript()
2511 tarballBuilder.buildAllTarballs = myBuilder
2513 tarballBuilder.main(["checkoutDir", "destinationDir"])
2516 [(FilePath("checkoutDir"), FilePath("destinationDir"), None)])
2519 tarballBuilder.main(["checkoutDir", "destinationDir", "templatePath"])
2522 [(FilePath("checkoutDir"), FilePath("destinationDir"),
2523 FilePath("templatePath"))])
2526 def test_defaultBuildTarballsScriptBuilder(self):
2528 The default implementation of L{BuildTarballsScript.buildAllTarballs}
2529 is L{buildAllTarballs}.
2531 tarballBuilder = BuildTarballsScript()
2532 self.assertEqual(tarballBuilder.buildAllTarballs, buildAllTarballs)
2535 def test_badNumberOfArgumentsToBuildTarballs(self):
2537 L{BuildTarballsScript.main} raises SystemExit when the wrong number of
2538 arguments are passed.
2540 tarballBuilder = BuildTarballsScript()
2541 self.assertRaises(SystemExit, tarballBuilder.main, [])
2542 self.assertRaises(SystemExit, tarballBuilder.main, ["a", "b", "c", "d"])
2545 def test_badNumberOfArgumentsToBuildNews(self):
2547 L{NewsBuilder.main} raises L{SystemExit} when other than 1 argument is
2550 newsBuilder = NewsBuilder()
2551 self.assertRaises(SystemExit, newsBuilder.main, [])
2552 self.assertRaises(SystemExit, newsBuilder.main, ["hello", "world"])
2555 def test_buildNews(self):
2557 L{NewsBuilder.main} calls L{NewsBuilder.buildAll} with a L{FilePath}
2558 instance constructed from the path passed to it.
2561 newsBuilder = NewsBuilder()
2562 newsBuilder.buildAll = builds.append
2563 newsBuilder.main(["/foo/bar/baz"])
2564 self.assertEqual(builds, [FilePath("/foo/bar/baz")])