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