class NewsBuilderTests(TestCase, StructureAssertingMixin): """ Tests for L{NewsBuilder}. """ skip = svnSkip def setUp(self): """ Create a fake project and stuff some basic structure and content into it. """ self.builder = NewsBuilder() self.project = FilePath(self.mktemp()) self.project.createDirectory() self.existingText = 'Here is stuff which was present previously.\n' createStructure( self.project, { 'NEWS': self.existingText, '5.feature': 'We now support the web.\n', '12.feature': 'The widget is more robust.\n', '15.feature': ( 'A very long feature which takes many words to ' 'describe with any accuracy was introduced so that ' 'the line wrapping behavior of the news generating ' 'code could be verified.\n'), '16.feature': ( 'A simpler feature\ndescribed on multiple lines\n' 'was added.\n'), '23.bugfix': 'Broken stuff was fixed.\n', '25.removal': 'Stupid stuff was deprecated.\n', '30.misc': '', '35.misc': '', '40.doc': 'foo.bar.Baz.quux', '41.doc': 'writing Foo servers'}) def test_today(self): """ L{NewsBuilder._today} returns today's date in YYYY-MM-DD form. """ self.assertEqual( self.builder._today(), date.today().strftime('%Y-%m-%d')) def test_findFeatures(self): """ When called with L{NewsBuilder._FEATURE}, L{NewsBuilder._findChanges} returns a list of bugfix ticket numbers and descriptions as a list of two-tuples. """ features = self.builder._findChanges( self.project, self.builder._FEATURE) self.assertEqual( features, [(5, "We now support the web."), (12, "The widget is more robust."), (15, "A very long feature which takes many words to describe with " "any accuracy was introduced so that the line wrapping behavior " "of the news generating code could be verified."), (16, "A simpler feature described on multiple lines was added.")]) def test_findBugfixes(self): """ When called with L{NewsBuilder._BUGFIX}, L{NewsBuilder._findChanges} returns a list of bugfix ticket numbers and descriptions as a list of two-tuples. """ bugfixes = self.builder._findChanges( self.project, self.builder._BUGFIX) self.assertEqual( bugfixes, [(23, 'Broken stuff was fixed.')]) def test_findRemovals(self): """ When called with L{NewsBuilder._REMOVAL}, L{NewsBuilder._findChanges} returns a list of removal/deprecation ticket numbers and descriptions as a list of two-tuples. """ removals = self.builder._findChanges( self.project, self.builder._REMOVAL) self.assertEqual( removals, [(25, 'Stupid stuff was deprecated.')]) def test_findDocumentation(self): """ When called with L{NewsBuilder._DOC}, L{NewsBuilder._findChanges} returns a list of documentation ticket numbers and descriptions as a list of two-tuples. """ doc = self.builder._findChanges( self.project, self.builder._DOC) self.assertEqual( doc, [(40, 'foo.bar.Baz.quux'), (41, 'writing Foo servers')]) def test_findMiscellaneous(self): """ When called with L{NewsBuilder._MISC}, L{NewsBuilder._findChanges} returns a list of removal/deprecation ticket numbers and descriptions as a list of two-tuples. """ misc = self.builder._findChanges( self.project, self.builder._MISC) self.assertEqual( misc, [(30, ''), (35, '')]) def test_writeHeader(self): """ L{NewsBuilder._writeHeader} accepts a file-like object opened for writing and a header string and writes out a news file header to it. """ output = StringIO() self.builder._writeHeader(output, "Super Awesometastic 32.16") self.assertEqual( output.getvalue(), "Super Awesometastic 32.16\n" "=========================\n" "\n") def test_writeSection(self): """ L{NewsBuilder._writeSection} accepts a file-like object opened for writing, a section name, and a list of ticket information (as returned by L{NewsBuilder._findChanges}) and writes out a section header and all of the given ticket information. """ output = StringIO() self.builder._writeSection( output, "Features", [(3, "Great stuff."), (17, "Very long line which goes on and on and on, seemingly " "without end until suddenly without warning it does end.")]) self.assertEqual( output.getvalue(), "Features\n" "--------\n" " - Great stuff. (#3)\n" " - Very long line which goes on and on and on, seemingly " "without end\n" " until suddenly without warning it does end. (#17)\n" "\n") def test_writeMisc(self): """ L{NewsBuilder._writeMisc} accepts a file-like object opened for writing, a section name, and a list of ticket information (as returned by L{NewsBuilder._findChanges} and writes out a section header and all of the ticket numbers, but excludes any descriptions. """ output = StringIO() self.builder._writeMisc( output, "Other", [(x, "") for x in range(2, 50, 3)]) self.assertEqual( output.getvalue(), "Other\n" "-----\n" " - #2, #5, #8, #11, #14, #17, #20, #23, #26, #29, #32, #35, " "#38, #41,\n" " #44, #47\n" "\n") def test_build(self): """ L{NewsBuilder.build} updates a NEWS file with new features based on the I{<ticket>.feature} files found in the directory specified. """ self.builder.build( self.project, self.project.child('NEWS'), "Super Awesometastic 32.16") results = self.project.child('NEWS').getContent() self.assertEqual( results, 'Super Awesometastic 32.16\n' '=========================\n' '\n' 'Features\n' '--------\n' ' - We now support the web. (#5)\n' ' - The widget is more robust. (#12)\n' ' - A very long feature which takes many words to describe ' 'with any\n' ' accuracy was introduced so that the line wrapping behavior ' 'of the\n' ' news generating code could be verified. (#15)\n' ' - A simpler feature described on multiple lines was ' 'added. (#16)\n' '\n' 'Bugfixes\n' '--------\n' ' - Broken stuff was fixed. (#23)\n' '\n' 'Improved Documentation\n' '----------------------\n' ' - foo.bar.Baz.quux (#40)\n' ' - writing Foo servers (#41)\n' '\n' 'Deprecations and Removals\n' '-------------------------\n' ' - Stupid stuff was deprecated. (#25)\n' '\n' 'Other\n' '-----\n' ' - #30, #35\n' '\n\n' + self.existingText) def test_emptyProjectCalledOut(self): """ If no changes exist for a project, I{NEWS} gains a new section for that project that includes some helpful text about how there were no interesting changes. """ project = FilePath(self.mktemp()).child("twisted") project.makedirs() createStructure(project, {'NEWS': self.existingText}) self.builder.build( project, project.child('NEWS'), "Super Awesometastic 32.16") results = project.child('NEWS').getContent() self.assertEqual( results, 'Super Awesometastic 32.16\n' '=========================\n' '\n' + self.builder._NO_CHANGES + '\n\n' + self.existingText) def test_preserveTicketHint(self): """ If a I{NEWS} file begins with the two magic lines which point readers at the issue tracker, those lines are kept at the top of the new file. """ news = self.project.child('NEWS') news.setContent( 'Ticket numbers in this file can be looked up by visiting\n' 'http://twistedmatrix.com/trac/ticket/<number>\n' '\n' 'Blah blah other stuff.\n') self.builder.build(self.project, news, "Super Awesometastic 32.16") self.assertEqual( news.getContent(), 'Ticket numbers in this file can be looked up by visiting\n' 'http://twistedmatrix.com/trac/ticket/<number>\n' '\n' 'Super Awesometastic 32.16\n' '=========================\n' '\n' 'Features\n' '--------\n' ' - We now support the web. (#5)\n' ' - The widget is more robust. (#12)\n' ' - A very long feature which takes many words to describe ' 'with any\n' ' accuracy was introduced so that the line wrapping behavior ' 'of the\n' ' news generating code could be verified. (#15)\n' ' - A simpler feature described on multiple lines was ' 'added. (#16)\n' '\n' 'Bugfixes\n' '--------\n' ' - Broken stuff was fixed. (#23)\n' '\n' 'Improved Documentation\n' '----------------------\n' ' - foo.bar.Baz.quux (#40)\n' ' - writing Foo servers (#41)\n' '\n' 'Deprecations and Removals\n' '-------------------------\n' ' - Stupid stuff was deprecated. (#25)\n' '\n' 'Other\n' '-----\n' ' - #30, #35\n' '\n\n' 'Blah blah other stuff.\n') def test_emptySectionsOmitted(self): """ If there are no changes of a particular type (feature, bugfix, etc), no section for that type is written by L{NewsBuilder.build}. """ for ticket in self.project.children(): if ticket.splitext()[1] in ('.feature', '.misc', '.doc'): ticket.remove() self.builder.build( self.project, self.project.child('NEWS'), 'Some Thing 1.2') self.assertEqual( self.project.child('NEWS').getContent(), 'Some Thing 1.2\n' '==============\n' '\n' 'Bugfixes\n' '--------\n' ' - Broken stuff was fixed. (#23)\n' '\n' 'Deprecations and Removals\n' '-------------------------\n' ' - Stupid stuff was deprecated. (#25)\n' '\n\n' 'Here is stuff which was present previously.\n') def test_duplicatesMerged(self): """ If two change files have the same contents, they are merged in the generated news entry. """ def feature(s): return self.project.child(s + '.feature') feature('5').copyTo(feature('15')) feature('5').copyTo(feature('16')) self.builder.build( self.project, self.project.child('NEWS'), 'Project Name 5.0') self.assertEqual( self.project.child('NEWS').getContent(), 'Project Name 5.0\n' '================\n' '\n' 'Features\n' '--------\n' ' - We now support the web. (#5, #15, #16)\n' ' - The widget is more robust. (#12)\n' '\n' 'Bugfixes\n' '--------\n' ' - Broken stuff was fixed. (#23)\n' '\n' 'Improved Documentation\n' '----------------------\n' ' - foo.bar.Baz.quux (#40)\n' ' - writing Foo servers (#41)\n' '\n' 'Deprecations and Removals\n' '-------------------------\n' ' - Stupid stuff was deprecated. (#25)\n' '\n' 'Other\n' '-----\n' ' - #30, #35\n' '\n\n' 'Here is stuff which was present previously.\n')
class NewsBuilderTests(TestCase, StructureAssertingMixin): """ Tests for L{NewsBuilder}. """ skip = svnSkip def setUp(self): """ Create a fake project and stuff some basic structure and content into it. """ self.builder = NewsBuilder() self.project = FilePath(self.mktemp()) self.project.createDirectory() self.existingText = 'Here is stuff which was present previously.\n' self.createStructure( self.project, { 'NEWS': self.existingText, '5.feature': 'We now support the web.\n', '12.feature': 'The widget is more robust.\n', '15.feature': ( 'A very long feature which takes many words to ' 'describe with any accuracy was introduced so that ' 'the line wrapping behavior of the news generating ' 'code could be verified.\n'), '16.feature': ( 'A simpler feature\ndescribed on multiple lines\n' 'was added.\n'), '23.bugfix': 'Broken stuff was fixed.\n', '25.removal': 'Stupid stuff was deprecated.\n', '30.misc': '', '35.misc': '', '40.doc': 'foo.bar.Baz.quux', '41.doc': 'writing Foo servers'}) def svnCommit(self, project=None): """ Make the C{project} directory a valid subversion directory with all files committed. """ if project is None: project = self.project repositoryPath = self.mktemp() repository = FilePath(repositoryPath) runCommand(["svnadmin", "create", repository.path]) runCommand(["svn", "checkout", "file://" + repository.path, project.path]) runCommand(["svn", "add"] + glob.glob(project.path + "/*")) runCommand(["svn", "commit", project.path, "-m", "yay"]) def test_today(self): """ L{NewsBuilder._today} returns today's date in YYYY-MM-DD form. """ self.assertEqual( self.builder._today(), date.today().strftime('%Y-%m-%d')) def test_findFeatures(self): """ When called with L{NewsBuilder._FEATURE}, L{NewsBuilder._findChanges} returns a list of bugfix ticket numbers and descriptions as a list of two-tuples. """ features = self.builder._findChanges( self.project, self.builder._FEATURE) self.assertEqual( features, [(5, "We now support the web."), (12, "The widget is more robust."), (15, "A very long feature which takes many words to describe with " "any accuracy was introduced so that the line wrapping behavior " "of the news generating code could be verified."), (16, "A simpler feature described on multiple lines was added.")]) def test_findBugfixes(self): """ When called with L{NewsBuilder._BUGFIX}, L{NewsBuilder._findChanges} returns a list of bugfix ticket numbers and descriptions as a list of two-tuples. """ bugfixes = self.builder._findChanges( self.project, self.builder._BUGFIX) self.assertEqual( bugfixes, [(23, 'Broken stuff was fixed.')]) def test_findRemovals(self): """ When called with L{NewsBuilder._REMOVAL}, L{NewsBuilder._findChanges} returns a list of removal/deprecation ticket numbers and descriptions as a list of two-tuples. """ removals = self.builder._findChanges( self.project, self.builder._REMOVAL) self.assertEqual( removals, [(25, 'Stupid stuff was deprecated.')]) def test_findDocumentation(self): """ When called with L{NewsBuilder._DOC}, L{NewsBuilder._findChanges} returns a list of documentation ticket numbers and descriptions as a list of two-tuples. """ doc = self.builder._findChanges( self.project, self.builder._DOC) self.assertEqual( doc, [(40, 'foo.bar.Baz.quux'), (41, 'writing Foo servers')]) def test_findMiscellaneous(self): """ When called with L{NewsBuilder._MISC}, L{NewsBuilder._findChanges} returns a list of removal/deprecation ticket numbers and descriptions as a list of two-tuples. """ misc = self.builder._findChanges( self.project, self.builder._MISC) self.assertEqual( misc, [(30, ''), (35, '')]) def test_writeHeader(self): """ L{NewsBuilder._writeHeader} accepts a file-like object opened for writing and a header string and writes out a news file header to it. """ output = StringIO() self.builder._writeHeader(output, "Super Awesometastic 32.16") self.assertEqual( output.getvalue(), "Super Awesometastic 32.16\n" "=========================\n" "\n") def test_writeSection(self): """ L{NewsBuilder._writeSection} accepts a file-like object opened for writing, a section name, and a list of ticket information (as returned by L{NewsBuilder._findChanges}) and writes out a section header and all of the given ticket information. """ output = StringIO() self.builder._writeSection( output, "Features", [(3, "Great stuff."), (17, "Very long line which goes on and on and on, seemingly " "without end until suddenly without warning it does end.")]) self.assertEqual( output.getvalue(), "Features\n" "--------\n" " - Great stuff. (#3)\n" " - Very long line which goes on and on and on, seemingly " "without end\n" " until suddenly without warning it does end. (#17)\n" "\n") def test_writeMisc(self): """ L{NewsBuilder._writeMisc} accepts a file-like object opened for writing, a section name, and a list of ticket information (as returned by L{NewsBuilder._findChanges} and writes out a section header and all of the ticket numbers, but excludes any descriptions. """ output = StringIO() self.builder._writeMisc( output, "Other", [(x, "") for x in range(2, 50, 3)]) self.assertEqual( output.getvalue(), "Other\n" "-----\n" " - #2, #5, #8, #11, #14, #17, #20, #23, #26, #29, #32, #35, " "#38, #41,\n" " #44, #47\n" "\n") def test_build(self): """ L{NewsBuilder.build} updates a NEWS file with new features based on the I{<ticket>.feature} files found in the directory specified. """ self.builder.build( self.project, self.project.child('NEWS'), "Super Awesometastic 32.16") results = self.project.child('NEWS').getContent() self.assertEqual( results, 'Super Awesometastic 32.16\n' '=========================\n' '\n' 'Features\n' '--------\n' ' - We now support the web. (#5)\n' ' - The widget is more robust. (#12)\n' ' - A very long feature which takes many words to describe ' 'with any\n' ' accuracy was introduced so that the line wrapping behavior ' 'of the\n' ' news generating code could be verified. (#15)\n' ' - A simpler feature described on multiple lines was ' 'added. (#16)\n' '\n' 'Bugfixes\n' '--------\n' ' - Broken stuff was fixed. (#23)\n' '\n' 'Improved Documentation\n' '----------------------\n' ' - foo.bar.Baz.quux (#40)\n' ' - writing Foo servers (#41)\n' '\n' 'Deprecations and Removals\n' '-------------------------\n' ' - Stupid stuff was deprecated. (#25)\n' '\n' 'Other\n' '-----\n' ' - #30, #35\n' '\n\n' + self.existingText) def test_emptyProjectCalledOut(self): """ If no changes exist for a project, I{NEWS} gains a new section for that project that includes some helpful text about how there were no interesting changes. """ project = FilePath(self.mktemp()).child("twisted") project.makedirs() self.createStructure(project, {'NEWS': self.existingText}) self.builder.build( project, project.child('NEWS'), "Super Awesometastic 32.16") results = project.child('NEWS').getContent() self.assertEqual( results, 'Super Awesometastic 32.16\n' '=========================\n' '\n' + self.builder._NO_CHANGES + '\n\n' + self.existingText) def test_preserveTicketHint(self): """ If a I{NEWS} file begins with the two magic lines which point readers at the issue tracker, those lines are kept at the top of the new file. """ news = self.project.child('NEWS') news.setContent( 'Ticket numbers in this file can be looked up by visiting\n' 'http://twistedmatrix.com/trac/ticket/<number>\n' '\n' 'Blah blah other stuff.\n') self.builder.build(self.project, news, "Super Awesometastic 32.16") self.assertEqual( news.getContent(), 'Ticket numbers in this file can be looked up by visiting\n' 'http://twistedmatrix.com/trac/ticket/<number>\n' '\n' 'Super Awesometastic 32.16\n' '=========================\n' '\n' 'Features\n' '--------\n' ' - We now support the web. (#5)\n' ' - The widget is more robust. (#12)\n' ' - A very long feature which takes many words to describe ' 'with any\n' ' accuracy was introduced so that the line wrapping behavior ' 'of the\n' ' news generating code could be verified. (#15)\n' ' - A simpler feature described on multiple lines was ' 'added. (#16)\n' '\n' 'Bugfixes\n' '--------\n' ' - Broken stuff was fixed. (#23)\n' '\n' 'Improved Documentation\n' '----------------------\n' ' - foo.bar.Baz.quux (#40)\n' ' - writing Foo servers (#41)\n' '\n' 'Deprecations and Removals\n' '-------------------------\n' ' - Stupid stuff was deprecated. (#25)\n' '\n' 'Other\n' '-----\n' ' - #30, #35\n' '\n\n' 'Blah blah other stuff.\n') def test_emptySectionsOmitted(self): """ If there are no changes of a particular type (feature, bugfix, etc), no section for that type is written by L{NewsBuilder.build}. """ for ticket in self.project.children(): if ticket.splitext()[1] in ('.feature', '.misc', '.doc'): ticket.remove() self.builder.build( self.project, self.project.child('NEWS'), 'Some Thing 1.2') self.assertEqual( self.project.child('NEWS').getContent(), 'Some Thing 1.2\n' '==============\n' '\n' 'Bugfixes\n' '--------\n' ' - Broken stuff was fixed. (#23)\n' '\n' 'Deprecations and Removals\n' '-------------------------\n' ' - Stupid stuff was deprecated. (#25)\n' '\n\n' 'Here is stuff which was present previously.\n') def test_duplicatesMerged(self): """ If two change files have the same contents, they are merged in the generated news entry. """ def feature(s): return self.project.child(s + '.feature') feature('5').copyTo(feature('15')) feature('5').copyTo(feature('16')) self.builder.build( self.project, self.project.child('NEWS'), 'Project Name 5.0') self.assertEqual( self.project.child('NEWS').getContent(), 'Project Name 5.0\n' '================\n' '\n' 'Features\n' '--------\n' ' - We now support the web. (#5, #15, #16)\n' ' - The widget is more robust. (#12)\n' '\n' 'Bugfixes\n' '--------\n' ' - Broken stuff was fixed. (#23)\n' '\n' 'Improved Documentation\n' '----------------------\n' ' - foo.bar.Baz.quux (#40)\n' ' - writing Foo servers (#41)\n' '\n' 'Deprecations and Removals\n' '-------------------------\n' ' - Stupid stuff was deprecated. (#25)\n' '\n' 'Other\n' '-----\n' ' - #30, #35\n' '\n\n' 'Here is stuff which was present previously.\n') def createFakeTwistedProject(self): """ Create a fake-looking Twisted project to build from. """ project = FilePath(self.mktemp()).child("twisted") project.makedirs() self.createStructure( project, { 'NEWS': 'Old boring stuff from the past.\n', '_version.py': genVersion("twisted", 1, 2, 3), 'topfiles': { 'NEWS': 'Old core news.\n', '3.feature': 'Third feature addition.\n', '5.misc': ''}, 'conch': { '_version.py': genVersion("twisted.conch", 3, 4, 5), 'topfiles': { 'NEWS': 'Old conch news.\n', '7.bugfix': 'Fixed that bug.\n'}}}) return project def test_buildAll(self): """ L{NewsBuilder.buildAll} calls L{NewsBuilder.build} once for each subproject, passing that subproject's I{topfiles} directory as C{path}, the I{NEWS} file in that directory as C{output}, and the subproject's name as C{header}, and then again for each subproject with the top-level I{NEWS} file for C{output}. Blacklisted subprojects are skipped. """ builds = [] builder = NewsBuilder() builder.build = lambda path, output, header: builds.append(( path, output, header)) builder._today = lambda: '2009-12-01' project = self.createFakeTwistedProject() self.svnCommit(project) builder.buildAll(project) coreTopfiles = project.child("topfiles") coreNews = coreTopfiles.child("NEWS") coreHeader = "Twisted Core 1.2.3 (2009-12-01)" conchTopfiles = project.child("conch").child("topfiles") conchNews = conchTopfiles.child("NEWS") conchHeader = "Twisted Conch 3.4.5 (2009-12-01)" aggregateNews = project.child("NEWS") self.assertEqual( builds, [(conchTopfiles, conchNews, conchHeader), (conchTopfiles, aggregateNews, conchHeader), (coreTopfiles, coreNews, coreHeader), (coreTopfiles, aggregateNews, coreHeader)]) def test_buildAllAggregate(self): """ L{NewsBuilder.buildAll} aggregates I{NEWS} information into the top files, only deleting fragments once it's done. """ builder = NewsBuilder() project = self.createFakeTwistedProject() self.svnCommit(project) builder.buildAll(project) aggregateNews = project.child("NEWS") aggregateContent = aggregateNews.getContent() self.assertIn("Third feature addition", aggregateContent) self.assertIn("Fixed that bug", aggregateContent) self.assertIn("Old boring stuff from the past", aggregateContent) def test_changeVersionInNews(self): """ L{NewsBuilder._changeVersions} gets the release date for a given version of a project as a string. """ builder = NewsBuilder() builder._today = lambda: '2009-12-01' project = self.createFakeTwistedProject() self.svnCommit(project) builder.buildAll(project) newVersion = Version('TEMPLATE', 7, 7, 14) coreNews = project.child('topfiles').child('NEWS') # twisted 1.2.3 is the old version. builder._changeNewsVersion( coreNews, "Core", Version("twisted", 1, 2, 3), newVersion, '2010-01-01') expectedCore = ( 'Twisted Core 7.7.14 (2010-01-01)\n' '================================\n' '\n' 'Features\n' '--------\n' ' - Third feature addition. (#3)\n' '\n' 'Other\n' '-----\n' ' - #5\n\n\n') self.assertEqual( expectedCore + 'Old core news.\n', coreNews.getContent()) def test_removeNEWSfragments(self): """ L{NewsBuilder.buildALL} removes all the NEWS fragments after the build process, using the C{svn} C{rm} command. """ builder = NewsBuilder() project = self.createFakeTwistedProject() self.svnCommit(project) builder.buildAll(project) self.assertEqual(5, len(project.children())) output = runCommand(["svn", "status", project.path]) removed = [line for line in output.splitlines() if line.startswith("D ")] self.assertEqual(3, len(removed)) def test_checkSVN(self): """ L{NewsBuilder.buildAll} raises L{NotWorkingDirectory} when the given path is not a SVN checkout. """ self.assertRaises( NotWorkingDirectory, self.builder.buildAll, self.project)
class NewsBuilderTests(TestCase, StructureAssertingMixin): """ Tests for L{NewsBuilder}. """ skip = svnSkip def setUp(self): """ Create a fake project and stuff some basic structure and content into it. """ self.builder = NewsBuilder() self.project = FilePath(self.mktemp()) self.project.createDirectory() self.existingText = 'Here is stuff which was present previously.\n' self.createStructure( self.project, { 'NEWS': self.existingText, '5.feature': 'We now support the web.\n', '12.feature': 'The widget is more robust.\n', '15.feature': ('A very long feature which takes many words to ' 'describe with any accuracy was introduced so that ' 'the line wrapping behavior of the news generating ' 'code could be verified.\n'), '16.feature': ('A simpler feature\ndescribed on multiple lines\n' 'was added.\n'), '23.bugfix': 'Broken stuff was fixed.\n', '25.removal': 'Stupid stuff was deprecated.\n', '30.misc': '', '35.misc': '', '40.doc': 'foo.bar.Baz.quux', '41.doc': 'writing Foo servers' }) def svnCommit(self, project=None): """ Make the C{project} directory a valid subversion directory with all files committed. """ if project is None: project = self.project repositoryPath = self.mktemp() repository = FilePath(repositoryPath) runCommand(["svnadmin", "create", repository.path]) runCommand( ["svn", "checkout", "file://" + repository.path, project.path]) runCommand(["svn", "add"] + glob.glob(project.path + "/*")) runCommand(["svn", "commit", project.path, "-m", "yay"]) def test_today(self): """ L{NewsBuilder._today} returns today's date in YYYY-MM-DD form. """ self.assertEqual(self.builder._today(), date.today().strftime('%Y-%m-%d')) def test_findFeatures(self): """ When called with L{NewsBuilder._FEATURE}, L{NewsBuilder._findChanges} returns a list of bugfix ticket numbers and descriptions as a list of two-tuples. """ features = self.builder._findChanges(self.project, self.builder._FEATURE) self.assertEqual(features, [ (5, "We now support the web."), (12, "The widget is more robust."), (15, "A very long feature which takes many words to describe with " "any accuracy was introduced so that the line wrapping behavior " "of the news generating code could be verified."), (16, "A simpler feature described on multiple lines was added.") ]) def test_findBugfixes(self): """ When called with L{NewsBuilder._BUGFIX}, L{NewsBuilder._findChanges} returns a list of bugfix ticket numbers and descriptions as a list of two-tuples. """ bugfixes = self.builder._findChanges(self.project, self.builder._BUGFIX) self.assertEqual(bugfixes, [(23, 'Broken stuff was fixed.')]) def test_findRemovals(self): """ When called with L{NewsBuilder._REMOVAL}, L{NewsBuilder._findChanges} returns a list of removal/deprecation ticket numbers and descriptions as a list of two-tuples. """ removals = self.builder._findChanges(self.project, self.builder._REMOVAL) self.assertEqual(removals, [(25, 'Stupid stuff was deprecated.')]) def test_findDocumentation(self): """ When called with L{NewsBuilder._DOC}, L{NewsBuilder._findChanges} returns a list of documentation ticket numbers and descriptions as a list of two-tuples. """ doc = self.builder._findChanges(self.project, self.builder._DOC) self.assertEqual(doc, [(40, 'foo.bar.Baz.quux'), (41, 'writing Foo servers')]) def test_findMiscellaneous(self): """ When called with L{NewsBuilder._MISC}, L{NewsBuilder._findChanges} returns a list of removal/deprecation ticket numbers and descriptions as a list of two-tuples. """ misc = self.builder._findChanges(self.project, self.builder._MISC) self.assertEqual(misc, [(30, ''), (35, '')]) def test_writeHeader(self): """ L{NewsBuilder._writeHeader} accepts a file-like object opened for writing and a header string and writes out a news file header to it. """ output = StringIO() self.builder._writeHeader(output, "Super Awesometastic 32.16") self.assertEqual( output.getvalue(), "Super Awesometastic 32.16\n" "=========================\n" "\n") def test_writeSection(self): """ L{NewsBuilder._writeSection} accepts a file-like object opened for writing, a section name, and a list of ticket information (as returned by L{NewsBuilder._findChanges}) and writes out a section header and all of the given ticket information. """ output = StringIO() self.builder._writeSection( output, "Features", [(3, "Great stuff."), (17, "Very long line which goes on and on and on, seemingly " "without end until suddenly without warning it does end.")]) self.assertEqual( output.getvalue(), "Features\n" "--------\n" " - Great stuff. (#3)\n" " - Very long line which goes on and on and on, seemingly " "without end\n" " until suddenly without warning it does end. (#17)\n" "\n") def test_writeMisc(self): """ L{NewsBuilder._writeMisc} accepts a file-like object opened for writing, a section name, and a list of ticket information (as returned by L{NewsBuilder._findChanges} and writes out a section header and all of the ticket numbers, but excludes any descriptions. """ output = StringIO() self.builder._writeMisc(output, "Other", [(x, "") for x in range(2, 50, 3)]) self.assertEqual( output.getvalue(), "Other\n" "-----\n" " - #2, #5, #8, #11, #14, #17, #20, #23, #26, #29, #32, #35, " "#38, #41,\n" " #44, #47\n" "\n") def test_build(self): """ L{NewsBuilder.build} updates a NEWS file with new features based on the I{<ticket>.feature} files found in the directory specified. """ self.builder.build(self.project, self.project.child('NEWS'), "Super Awesometastic 32.16") results = self.project.child('NEWS').getContent() self.assertEqual( results, 'Super Awesometastic 32.16\n' '=========================\n' '\n' 'Features\n' '--------\n' ' - We now support the web. (#5)\n' ' - The widget is more robust. (#12)\n' ' - A very long feature which takes many words to describe ' 'with any\n' ' accuracy was introduced so that the line wrapping behavior ' 'of the\n' ' news generating code could be verified. (#15)\n' ' - A simpler feature described on multiple lines was ' 'added. (#16)\n' '\n' 'Bugfixes\n' '--------\n' ' - Broken stuff was fixed. (#23)\n' '\n' 'Improved Documentation\n' '----------------------\n' ' - foo.bar.Baz.quux (#40)\n' ' - writing Foo servers (#41)\n' '\n' 'Deprecations and Removals\n' '-------------------------\n' ' - Stupid stuff was deprecated. (#25)\n' '\n' 'Other\n' '-----\n' ' - #30, #35\n' '\n\n' + self.existingText) def test_emptyProjectCalledOut(self): """ If no changes exist for a project, I{NEWS} gains a new section for that project that includes some helpful text about how there were no interesting changes. """ project = FilePath(self.mktemp()).child("twisted") project.makedirs() self.createStructure(project, {'NEWS': self.existingText}) self.builder.build(project, project.child('NEWS'), "Super Awesometastic 32.16") results = project.child('NEWS').getContent() self.assertEqual( results, 'Super Awesometastic 32.16\n' '=========================\n' '\n' + self.builder._NO_CHANGES + '\n\n' + self.existingText) def test_preserveTicketHint(self): """ If a I{NEWS} file begins with the two magic lines which point readers at the issue tracker, those lines are kept at the top of the new file. """ news = self.project.child('NEWS') news.setContent( 'Ticket numbers in this file can be looked up by visiting\n' 'http://twistedmatrix.com/trac/ticket/<number>\n' '\n' 'Blah blah other stuff.\n') self.builder.build(self.project, news, "Super Awesometastic 32.16") self.assertEqual( news.getContent(), 'Ticket numbers in this file can be looked up by visiting\n' 'http://twistedmatrix.com/trac/ticket/<number>\n' '\n' 'Super Awesometastic 32.16\n' '=========================\n' '\n' 'Features\n' '--------\n' ' - We now support the web. (#5)\n' ' - The widget is more robust. (#12)\n' ' - A very long feature which takes many words to describe ' 'with any\n' ' accuracy was introduced so that the line wrapping behavior ' 'of the\n' ' news generating code could be verified. (#15)\n' ' - A simpler feature described on multiple lines was ' 'added. (#16)\n' '\n' 'Bugfixes\n' '--------\n' ' - Broken stuff was fixed. (#23)\n' '\n' 'Improved Documentation\n' '----------------------\n' ' - foo.bar.Baz.quux (#40)\n' ' - writing Foo servers (#41)\n' '\n' 'Deprecations and Removals\n' '-------------------------\n' ' - Stupid stuff was deprecated. (#25)\n' '\n' 'Other\n' '-----\n' ' - #30, #35\n' '\n\n' 'Blah blah other stuff.\n') def test_emptySectionsOmitted(self): """ If there are no changes of a particular type (feature, bugfix, etc), no section for that type is written by L{NewsBuilder.build}. """ for ticket in self.project.children(): if ticket.splitext()[1] in ('.feature', '.misc', '.doc'): ticket.remove() self.builder.build(self.project, self.project.child('NEWS'), 'Some Thing 1.2') self.assertEqual( self.project.child('NEWS').getContent(), 'Some Thing 1.2\n' '==============\n' '\n' 'Bugfixes\n' '--------\n' ' - Broken stuff was fixed. (#23)\n' '\n' 'Deprecations and Removals\n' '-------------------------\n' ' - Stupid stuff was deprecated. (#25)\n' '\n\n' 'Here is stuff which was present previously.\n') def test_duplicatesMerged(self): """ If two change files have the same contents, they are merged in the generated news entry. """ def feature(s): return self.project.child(s + '.feature') feature('5').copyTo(feature('15')) feature('5').copyTo(feature('16')) self.builder.build(self.project, self.project.child('NEWS'), 'Project Name 5.0') self.assertEqual( self.project.child('NEWS').getContent(), 'Project Name 5.0\n' '================\n' '\n' 'Features\n' '--------\n' ' - We now support the web. (#5, #15, #16)\n' ' - The widget is more robust. (#12)\n' '\n' 'Bugfixes\n' '--------\n' ' - Broken stuff was fixed. (#23)\n' '\n' 'Improved Documentation\n' '----------------------\n' ' - foo.bar.Baz.quux (#40)\n' ' - writing Foo servers (#41)\n' '\n' 'Deprecations and Removals\n' '-------------------------\n' ' - Stupid stuff was deprecated. (#25)\n' '\n' 'Other\n' '-----\n' ' - #30, #35\n' '\n\n' 'Here is stuff which was present previously.\n') def createFakeTwistedProject(self): """ Create a fake-looking Twisted project to build from. """ project = FilePath(self.mktemp()).child("twisted") project.makedirs() self.createStructure( project, { 'NEWS': 'Old boring stuff from the past.\n', '_version.py': genVersion("twisted", 1, 2, 3), 'topfiles': { 'NEWS': 'Old core news.\n', '3.feature': 'Third feature addition.\n', '5.misc': '' }, 'conch': { '_version.py': genVersion("twisted.conch", 3, 4, 5), 'topfiles': { 'NEWS': 'Old conch news.\n', '7.bugfix': 'Fixed that bug.\n' } } }) return project def test_buildAll(self): """ L{NewsBuilder.buildAll} calls L{NewsBuilder.build} once for each subproject, passing that subproject's I{topfiles} directory as C{path}, the I{NEWS} file in that directory as C{output}, and the subproject's name as C{header}, and then again for each subproject with the top-level I{NEWS} file for C{output}. Blacklisted subprojects are skipped. """ builds = [] builder = NewsBuilder() builder.build = lambda path, output, header: builds.append( (path, output, header)) builder._today = lambda: '2009-12-01' project = self.createFakeTwistedProject() self.svnCommit(project) builder.buildAll(project) coreTopfiles = project.child("topfiles") coreNews = coreTopfiles.child("NEWS") coreHeader = "Twisted Core 1.2.3 (2009-12-01)" conchTopfiles = project.child("conch").child("topfiles") conchNews = conchTopfiles.child("NEWS") conchHeader = "Twisted Conch 3.4.5 (2009-12-01)" aggregateNews = project.child("NEWS") self.assertEqual(builds, [(conchTopfiles, conchNews, conchHeader), (conchTopfiles, aggregateNews, conchHeader), (coreTopfiles, coreNews, coreHeader), (coreTopfiles, aggregateNews, coreHeader)]) def test_buildAllAggregate(self): """ L{NewsBuilder.buildAll} aggregates I{NEWS} information into the top files, only deleting fragments once it's done. """ builder = NewsBuilder() project = self.createFakeTwistedProject() self.svnCommit(project) builder.buildAll(project) aggregateNews = project.child("NEWS") aggregateContent = aggregateNews.getContent() self.assertIn("Third feature addition", aggregateContent) self.assertIn("Fixed that bug", aggregateContent) self.assertIn("Old boring stuff from the past", aggregateContent) def test_changeVersionInNews(self): """ L{NewsBuilder._changeVersions} gets the release date for a given version of a project as a string. """ builder = NewsBuilder() builder._today = lambda: '2009-12-01' project = self.createFakeTwistedProject() self.svnCommit(project) builder.buildAll(project) newVersion = Version('TEMPLATE', 7, 7, 14) coreNews = project.child('topfiles').child('NEWS') # twisted 1.2.3 is the old version. builder._changeNewsVersion(coreNews, "Core", Version("twisted", 1, 2, 3), newVersion, '2010-01-01') expectedCore = ('Twisted Core 7.7.14 (2010-01-01)\n' '================================\n' '\n' 'Features\n' '--------\n' ' - Third feature addition. (#3)\n' '\n' 'Other\n' '-----\n' ' - #5\n\n\n') self.assertEqual(expectedCore + 'Old core news.\n', coreNews.getContent()) def test_removeNEWSfragments(self): """ L{NewsBuilder.buildALL} removes all the NEWS fragments after the build process, using the C{svn} C{rm} command. """ builder = NewsBuilder() project = self.createFakeTwistedProject() self.svnCommit(project) builder.buildAll(project) self.assertEqual(5, len(project.children())) output = runCommand(["svn", "status", project.path]) removed = [ line for line in output.splitlines() if line.startswith("D ") ] self.assertEqual(3, len(removed)) def test_checkSVN(self): """ L{NewsBuilder.buildAll} raises L{NotWorkingDirectory} when the given path is not a SVN checkout. """ self.assertRaises(NotWorkingDirectory, self.builder.buildAll, self.project)