Modified: bloodhound/branches/trac-1.0.2-integration/trac/trac/tests/functional/better_twill.py URL: http://svn.apache.org/viewvc/bloodhound/branches/trac-1.0.2-integration/trac/trac/tests/functional/better_twill.py?rev=1639823&r1=1639822&r2=1639823&view=diff ============================================================================== --- bloodhound/branches/trac-1.0.2-integration/trac/trac/tests/functional/better_twill.py (original) +++ bloodhound/branches/trac-1.0.2-integration/trac/trac/tests/functional/better_twill.py Sat Nov 15 01:14:46 2014 @@ -1,12 +1,26 @@ -#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright (C) 2008-2014 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://trac.edgewall.org/wiki/TracLicense. +# +# This software consists of voluntary contributions made by many +# individuals. For the exact contribution history, see the revision +# history and logs, available at http://trac.edgewall.org/log/. + """better_twill is a small wrapper around twill to set some sane defaults and monkey-patch some better versions of some of twill's methods. It also handles twill's absense. """ import os -from os.path import abspath, dirname, join import sys +import urllib +import urlparse +from os.path import abspath, dirname, join from pkg_resources import parse_version as pv try: from cStringIO import StringIO @@ -36,9 +50,9 @@ except ImportError: if twill: # We want Trac to generate valid html, and therefore want to test against - # the html as generated by Trac. "tidy" tries to clean up broken html, and - # is responsible for one difficult to track down testcase failure (for - # #5497). Therefore we turn it off here. + # the html as generated by Trac. "tidy" tries to clean up broken html, + # and is responsible for one difficult to track down testcase failure + # (for #5497). Therefore we turn it off here. twill.commands.config('use_tidy', '0') # We use a transparent proxy to access the global browser object through @@ -90,9 +104,9 @@ if twill: context = data.splitlines()[max(0, entry.line - 5): entry.line + 6] msg.append("\n# %s\n# URL: %s\n# Line %d, column %d\n\n%s\n" - % (entry.message, entry.filename, - entry.line, entry.column, - "\n".join([each.decode('utf-8') for each in context]))) + % (entry.message, entry.filename, entry.line, + entry.column, "\n".join([each.decode('utf-8') + for each in context]))) return "\n".join(msg).encode('ascii', 'xmlcharrefreplace') def _validate_xhtml(func_name, *args, **kwargs): @@ -120,6 +134,8 @@ if twill: """Write the current html to a file. Name the file based on the current testcase. """ + import unittest + frame = sys._getframe() while frame: if frame.f_code.co_name in ('runTest', 'setUp', 'tearDown'): @@ -127,19 +143,26 @@ if twill: testname = testcase.__class__.__name__ tracdir = testcase._testenv.tracdir break + elif isinstance(frame.f_locals.get('self'), unittest.TestCase): + testcase = frame.f_locals['self'] + testname = '%s.%s' % (testcase.__class__.__name__, + testcase._testMethodName) + tracdir = testcase._testenv.tracdir + break frame = frame.f_back else: # We didn't find a testcase in the stack, so we have no clue what's # going on. raise Exception("No testcase was found on the stack. This was " - "really not expected, and I don't know how to handle it.") + "really not expected, and I don't know how to " + "handle it.") filename = os.path.join(tracdir, 'log', "%s.html" % testname) html_file = open(filename, 'w') html_file.write(b.get_html()) html_file.close() - return filename + return urlparse.urljoin('file:', urllib.pathname2url(filename)) # Twill isn't as helpful with errors as I'd like it to be, so we replace # the formvalue function. This would be better done as a patch to Twill. @@ -149,8 +172,8 @@ if twill: except (twill.errors.TwillAssertionError, twill.utils.ClientForm.ItemNotFoundError), e: filename = twill_write_html() - args = e.args + (filename,) - raise twill.errors.TwillAssertionError(*args) + raise twill.errors.TwillAssertionError('%s at %s' % + (unicode(e), filename)) tc.formvalue = better_formvalue tc.fv = better_formvalue @@ -163,8 +186,8 @@ if twill: if formname is not None: # enhancement to directly specify the form browser._browser.form = browser.get_form(formname) old_submit(fieldname) - b.submit = better_browser_submit + def better_submit(fieldname=None, formname=None): b.submit(fieldname, formname) tc.submit = better_submit @@ -186,39 +209,41 @@ if twill: control = b.get_form_field(form, fieldname) if not control.is_of_kind('file'): - raise twill.errors.TwillException('ERROR: field is not a file ' - 'upload field!') + raise twill.errors.TwillException("ERROR: field is not a file " + "upload field!") b.clicked(form, control) control.add_file(fp, content_type, filename) tc.formfile = better_formfile - # Twill's tc.find() does not provide any guidance on what we got instead of - # what was expected. + # Twill's tc.find() does not provide any guidance on what we got + # instead of what was expected. def better_find(what, flags='', tcfind=tc.find): try: tcfind(what, flags) except twill.errors.TwillAssertionError, e: filename = twill_write_html() - args = e.args + (filename,) - raise twill.errors.TwillAssertionError(*args) + raise twill.errors.TwillAssertionError('%s at %s' % + (unicode(e), filename)) tc.find = better_find + def better_notfind(what, flags='', tcnotfind=tc.notfind): try: tcnotfind(what, flags) except twill.errors.TwillAssertionError, e: filename = twill_write_html() - args = e.args + (filename,) - raise twill.errors.TwillAssertionError(*args) + raise twill.errors.TwillAssertionError('%s at %s' % + (unicode(e), filename)) tc.notfind = better_notfind + # Same for tc.url - no hint about what went wrong! def better_url(should_be, tcurl=tc.url): try: tcurl(should_be) except twill.errors.TwillAssertionError, e: filename = twill_write_html() - args = e.args + (filename,) - raise twill.errors.TwillAssertionError(*args) + raise twill.errors.TwillAssertionError('%s at %s' % + (unicode(e), filename)) tc.url = better_url else: b = tc = None
Modified: bloodhound/branches/trac-1.0.2-integration/trac/trac/tests/functional/compat.py URL: http://svn.apache.org/viewvc/bloodhound/branches/trac-1.0.2-integration/trac/trac/tests/functional/compat.py?rev=1639823&r1=1639822&r2=1639823&view=diff ============================================================================== --- bloodhound/branches/trac-1.0.2-integration/trac/trac/tests/functional/compat.py (original) +++ bloodhound/branches/trac-1.0.2-integration/trac/trac/tests/functional/compat.py Sat Nov 15 01:14:46 2014 @@ -1,18 +1,15 @@ -#!/usr/bin/python -import os -import shutil +# -*- coding: utf-8 -*- +# +# Copyright (C) 2008-2013 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://trac.edgewall.org/wiki/TracLicense. +# +# This software consists of voluntary contributions made by many +# individuals. For the exact contribution history, see the revision +# history and logs, available at http://trac.edgewall.org/log/. +from trac.tests.compat import rmtree from trac.util.compat import close_fds - -# On Windows, shutil.rmtree doesn't remove files with the read-only -# attribute set, so this function explicitly removes it on every error -# before retrying. Even on Linux, shutil.rmtree chokes on read-only -# directories, so we use this version in all cases. -# Fix from http://bitten.edgewall.org/changeset/521 -def rmtree(root): - """Catch shutil.rmtree failures on Windows when files are read-only.""" - def _handle_error(fn, path, excinfo): - os.chmod(path, 0666) - fn(path) - return shutil.rmtree(root, onerror=_handle_error) - Modified: bloodhound/branches/trac-1.0.2-integration/trac/trac/tests/functional/svntestenv.py URL: http://svn.apache.org/viewvc/bloodhound/branches/trac-1.0.2-integration/trac/trac/tests/functional/svntestenv.py?rev=1639823&r1=1639822&r2=1639823&view=diff ============================================================================== --- bloodhound/branches/trac-1.0.2-integration/trac/trac/tests/functional/svntestenv.py (original) +++ bloodhound/branches/trac-1.0.2-integration/trac/trac/tests/functional/svntestenv.py Sat Nov 15 01:14:46 2014 @@ -1,31 +1,47 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2009-2014 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://trac.edgewall.org/wiki/TracLicense. +# +# This software consists of voluntary contributions made by many +# individuals. For the exact contribution history, see the revision +# history and logs, available at http://trac.edgewall.org/log/. + import os import re from subprocess import call from testenv import FunctionalTestEnvironment -from trac.tests.functional.compat import close_fds from trac.tests.functional import logfile +from trac.util.compat import close_fds + class SvnFunctionalTestEnvironment(FunctionalTestEnvironment): def work_dir(self): return os.path.join(self.dirname, 'workdir') + def repo_path(self, filename): + return os.path.join(self.dirname, filename) + def repo_path_for_initenv(self): - return os.path.join(self.dirname, 'repo') + return self.repo_path('repo') def create_repo(self): """ Initialize a repo of the type :attr:`self.repotype`. """ - if call(["svnadmin", "create", self.repo_path_for_initenv()], - stdout=logfile, stderr=logfile, close_fds=close_fds): - raise Exception('unable to create subversion repository') - if call(['svn', 'co', self.repo_url(), self.work_dir()], stdout=logfile, - stderr=logfile, close_fds=close_fds): + self.svnadmin_create() + if call(['svn', 'co', self.repo_url(), self.work_dir()], + stdout=logfile, stderr=logfile, close_fds=close_fds): raise Exception('Checkout from %s failed.' % self.repo_url()) def destroy_repo(self): - """The deletion of the testenvironment will remove the repo as well.""" + """The deletion of the test environment will remove the + repo as well.""" pass def repo_url(self): @@ -38,6 +54,18 @@ class SvnFunctionalTestEnvironment(Funct else: return 'file://' + repodir + def svnadmin_create(self, filename=None): + """Subversion helper to create a new repository.""" + if filename is None: + path = self.repo_path_for_initenv() + else: + path = self.repo_path(filename) + if call(["svnadmin", "create", path], + stdout=logfile, stderr=logfile, close_fds=close_fds): + raise Exception('unable to create subversion repository: %r' % + path) + return path + def svn_mkdir(self, paths, msg, username='admin'): """Subversion helper to create a new directory within the main repository. Operates directly on the repository url, so a working @@ -48,11 +76,12 @@ class SvnFunctionalTestEnvironment(Funct self._testenv.svn_mkdir(["abc", "def"], "Add dirs") """ - self.call_in_workdir(['svn', '--username=%s' % username, 'mkdir', '-m', msg] - + [self.repo_url() + '/' + d for d in paths]) + self.call_in_workdir(['svn', '--username=%s' % username, + 'mkdir', '-m', msg] + + [self.repo_url() + '/' + d for d in paths]) self.call_in_workdir(['svn', 'update']) - def svn_add(self, filename, data): + def svn_add(self, filename, data, msg=None, username='admin'): """Subversion helper to add a file to the given path within the main repository. @@ -67,8 +96,10 @@ class SvnFunctionalTestEnvironment(Funct self.call_in_workdir(['svn', 'add', filename]) environ = os.environ.copy() environ['LC_ALL'] = 'C' # Force English messages in svn - output = self.call_in_workdir(['svn', '--username=admin', 'commit', '-m', - 'Add %s' % filename, filename], environ=environ) + msg = 'Add %s' % filename if msg is None else msg + output = self.call_in_workdir(['svn', '--username=%s' % username, + 'commit', '-m', msg, filename], + environ=environ) try: revision = re.search(r'Committed revision ([0-9]+)\.', output).group(1) @@ -77,3 +108,5 @@ class SvnFunctionalTestEnvironment(Funct raise Exception(*args) return int(revision) + def call_in_workdir(self, args, environ=None): + return self.call_in_dir(self.work_dir(), args, environ) Modified: bloodhound/branches/trac-1.0.2-integration/trac/trac/tests/functional/testcases.py URL: http://svn.apache.org/viewvc/bloodhound/branches/trac-1.0.2-integration/trac/trac/tests/functional/testcases.py?rev=1639823&r1=1639822&r2=1639823&view=diff ============================================================================== --- bloodhound/branches/trac-1.0.2-integration/trac/trac/tests/functional/testcases.py (original) +++ bloodhound/branches/trac-1.0.2-integration/trac/trac/tests/functional/testcases.py Sat Nov 15 01:14:46 2014 @@ -1,17 +1,96 @@ -# -*- encoding: utf-8 -*- -#!/usr/bin/python +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright (C) 2008-2013 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://trac.edgewall.org/wiki/TracLicense. +# +# This software consists of voluntary contributions made by many +# individuals. For the exact contribution history, see the revision +# history and logs, available at http://trac.edgewall.org/log/. + import os + from trac.tests.functional import * +from trac.util import create_file + + +class TestAttachmentNonexistentParent(FunctionalTwillTestCaseSetup): + def runTest(self): + """TracError should be raised when navigating to the attachment + page for a nonexistent resource.""" + self._tester.go_to_wiki('NonexistentPage') + tc.find("The page NonexistentPage does not exist. " + "You can create it here.") + tc.find(r"\bCreate this page\b") + + tc.go(self._tester.url + '/attachment/wiki/NonexistentPage') + tc.find('<h1>Trac Error</h1>\s+<p class="message">' + 'Parent resource NonexistentPage doesn\'t exist</p>') + + +class TestErrorPage(FunctionalTwillTestCaseSetup): + """Validate the error page. + Defects reported to trac-hacks should use the Component defined in the + plugin's URL (#11434). + """ + def runTest(self): + env = self._testenv.get_trac_environment() + env.config.set('components', 'RaiseExceptionPlugin.*', 'enabled') + env.config.save() + create_file(os.path.join(env.path, 'plugins', + 'RaiseExceptionPlugin.py'), +"""\ +from trac.core import Component, implements +from trac.web.api import IRequestHandler + +url = None + +class RaiseExceptionPlugin(Component): + implements(IRequestHandler) + + def match_request(self, req): + if req.path_info.startswith('/raise-exception'): + return True + + def process_request(self, req): + print 'maybe?' + if req.args.get('report') == 'tho': + global url + url = 'http://trac-hacks.org/wiki/HelloWorldMacro' + raise Exception + +""") + self._testenv.restart() + + try: + tc.go(self._tester.url + '/raise-exception') + tc.find(internal_error) + tc.find('<form class="newticket" method="get" ' + 'action="http://trac.edgewall.org/newticket">') + + tc.go(self._tester.url + '/raise-exception?report=tho') + tc.find(internal_error) + tc.find('<form class="newticket" method="get" ' + 'action="http://trac-hacks.org/newticket">') + tc.find('<input type="hidden" name="component" ' + 'value="HelloWorldMacro" />') + finally: + env.config.set('components', 'RaiseExceptionPlugin.*', 'disabled') class RegressionTestRev6017(FunctionalTwillTestCaseSetup): def runTest(self): """Test for regression of the plugin reload fix in r6017""" # Setup the DeleteTicket plugin - plugin = open(os.path.join(self._testenv.command_cwd, 'sample-plugins', - 'workflow', 'DeleteTicket.py')).read() - open(os.path.join(self._testenv.tracdir, 'plugins', 'DeleteTicket.py'), - 'w').write(plugin) + plugin = open(os.path.join(self._testenv.trac_src, + 'sample-plugins', 'workflow', + 'DeleteTicket.py')).read() + open(os.path.join(self._testenv.tracdir, 'plugins', + 'DeleteTicket.py'), 'w').write(plugin) env = self._testenv.get_trac_environment() prevconfig = env.config.get('ticket', 'workflow') env.config.set('ticket', 'workflow', @@ -30,7 +109,6 @@ class RegressionTestRev6017(FunctionalTw # Remove the DeleteTicket plugin env.config.set('ticket', 'workflow', prevconfig) env.config.save() - self._testenv.restart() for ext in ('py', 'pyc', 'pyo'): filename = os.path.join(self._testenv.tracdir, 'plugins', 'DeleteTicket.%s' % ext) @@ -52,7 +130,8 @@ class RegressionTestTicket3833a(Function env.log.debug("RegressionTestTicket3833 debug1") debug1 = traclogfile.read() self.assertNotEqual(debug1.find("RegressionTestTicket3833 debug1"), -1, - 'Logging off when it should have been on.\n%r' % debug1) + 'Logging off when it should have been on.\n%r' + % debug1) class RegressionTestTicket3833b(FunctionalTestCaseSetup): @@ -75,7 +154,8 @@ class RegressionTestTicket3833b(Function self.assertNotEqual(debug2.find("RegressionTestTicket3833 info2"), -1, 'Logging at info failed.\n%r' % debug2) self.assertEqual(debug2.find("RegressionTestTicket3833 debug2"), -1, - 'Logging still on when it should have been off.\n%r' % debug2) + 'Logging still on when it should have been off.\n%r' + % debug2) class RegressionTestTicket3833c(FunctionalTestCaseSetup): @@ -103,12 +183,11 @@ class RegressionTestTicket3833c(Function success = debug3.find("RegressionTestTicket3833 debug3") != -1 if not success: # Ok, the testcase failed, but we really need logging enabled. - self._testenv.restart() env.log.debug("RegressionTestTicket3833 fixup3") fixup3 = traclogfile.read() message = 'Logging still off when it should have been on.\n' \ '%r\n%r' % (debug3, fixup3) - self.assert_(success, message) + self.assertTrue(success, message) class RegressionTestTicket5572(FunctionalTwillTestCaseSetup): @@ -122,29 +201,28 @@ class RegressionTestTicket5572(Functiona class RegressionTestTicket7209(FunctionalTwillTestCaseSetup): def runTest(self): """Test for regression of http://trac.edgewall.org/ticket/7209""" - summary = random_sentence(5) - ticketid = self._tester.create_ticket(summary) + ticketid = self._tester.create_ticket() self._tester.create_ticket() self._tester.add_comment(ticketid) - self._tester.attach_file_to_ticket(ticketid, tempfilename='hello.txt', + self._tester.attach_file_to_ticket(ticketid, filename='hello.txt', description='Preserved Descr') self._tester.go_to_ticket(ticketid) tc.find('Preserved Descr') # Now replace the existing attachment, and the description should come # through. - self._tester.attach_file_to_ticket(ticketid, tempfilename='hello.txt', + self._tester.attach_file_to_ticket(ticketid, filename='hello.txt', description='', replace=True) self._tester.go_to_ticket(ticketid) tc.find('Preserved Descr') - self._tester.attach_file_to_ticket(ticketid, tempfilename='blah.txt', + self._tester.attach_file_to_ticket(ticketid, filename='blah.txt', description='Second Attachment') self._tester.go_to_ticket(ticketid) tc.find('Second Attachment') # This one should get a new description when it's replaced # (Second->Other) - self._tester.attach_file_to_ticket(ticketid, tempfilename='blah.txt', + self._tester.attach_file_to_ticket(ticketid, filename='blah.txt', description='Other Attachment', replace=True) self._tester.go_to_ticket(ticketid) @@ -159,10 +237,9 @@ class RegressionTestTicket9880(Functiona Upload of a file which the browsers associates a Content-Type of multipart/related (e.g. an .mht file) should succeed. """ - summary = random_sentence(5) - ticketid = self._tester.create_ticket(summary) + ticketid = self._tester.create_ticket() self._tester.create_ticket() - self._tester.attach_file_to_ticket(ticketid, tempfilename='hello.mht', + self._tester.attach_file_to_ticket(ticketid, filename='hello.mht', content_type='multipart/related', data=""" Well, the actual content of the file doesn't matter, the problem is @@ -171,15 +248,6 @@ See also http://bugs.python.org/issue155 """) -class ErrorPageValidation(FunctionalTwillTestCaseSetup): - def runTest(self): - """Validate the error page""" - url = self._tester.url + '/wiki/WikiStart' - tc.go(url + '?version=bug') - tc.url(url) - tc.find(internal_error) - - class RegressionTestTicket3663(FunctionalTwillTestCaseSetup): def runTest(self): """Regression test for non-UTF-8 PATH_INFO (#3663) @@ -196,14 +264,79 @@ class RegressionTestTicket3663(Functiona tc.find('Invalid URL encoding') -def functionalSuite(): - suite = FunctionalTestSuite() - return suite +class RegressionTestTicket6318(FunctionalTwillTestCaseSetup): + def runTest(self): + """Regression test for non-ascii usernames (#6318) + """ + # first do a logout, otherwise we might end up logged in as + # admin again, as this is the first thing the tester does. + # ... but even before that we need to make sure we're coming + # from a valid URL, which is not the case if we're just coming + # from the above test! ('/wiki/\xE9t\xE9') + self._tester.go_to_front() + self._tester.logout() + try: + # also test a regular ascii user name + self._testenv.adduser(u'user') + self._tester.login(u'user') + self._tester.go_to_front() + self._tester.logout() + # now test utf-8 user name + self._testenv.adduser(u'joé') + self._tester.login(u'joé') + self._tester.go_to_front() + self._tester.logout() + # finally restore expected 'admin' login + self._tester.login('admin') + finally: + self._testenv.deluser(u'joé') + + +class RegressionTestTicket11434(FunctionalTwillTestCaseSetup): + """Test for regression of http://trac.edgewall.org/ticket/11434 + Defects reported to trac-hacks should use the Component defined in the + plugin's URL. + """ + def runTest(self): + env = self._testenv.get_trac_environment() + env.config.set('components', 'RaiseExceptionPlugin.*', 'enabled') + env.config.save() + create_file(os.path.join(env.path, 'plugins', 'RaiseExceptionPlugin.py'), +"""\ +from trac.core import Component, implements +from trac.web.api import IRequestHandler + +url = 'http://trac-hacks.org/wiki/HelloWorldMacro' + +class RaiseExceptionPlugin(Component): + implements(IRequestHandler) + + def match_request(self, req): + if req.path_info == '/raise-exception': + return True + + def process_request(self, req): + raise Exception + +""") + try: + tc.go(self._tester.url + '/raise-exception') + tc.find(internal_error) + tc.find('<form class="newticket" method="get" ' + 'action="http://trac-hacks.org/newticket">') + tc.find('<input type="hidden" name="component" ' + 'value="HelloWorldMacro" />') + finally: + env.config.set('components', 'RaiseExceptionPlugin.*', 'disabled') -def suite(): - suite = functionalSuite() +def functionalSuite(suite=None): + if not suite: + import trac.tests.functional + suite = trac.tests.functional.functionalSuite() + suite.addTest(TestAttachmentNonexistentParent()) + suite.addTest(TestErrorPage()) suite.addTest(RegressionTestRev6017()) suite.addTest(RegressionTestTicket3833a()) suite.addTest(RegressionTestTicket3833b()) @@ -211,27 +344,11 @@ def suite(): suite.addTest(RegressionTestTicket5572()) suite.addTest(RegressionTestTicket7209()) suite.addTest(RegressionTestTicket9880()) - suite.addTest(ErrorPageValidation()) suite.addTest(RegressionTestTicket3663()) - - import trac.versioncontrol.tests - trac.versioncontrol.tests.functionalSuite(suite) - import trac.ticket.tests - trac.ticket.tests.functionalSuite(suite) - import trac.prefs.tests - trac.prefs.tests.functionalSuite(suite) - import trac.wiki.tests - trac.wiki.tests.functionalSuite(suite) - import trac.timeline.tests - trac.timeline.tests.functionalSuite(suite) - import trac.admin.tests - trac.admin.tests.functionalSuite(suite) - # The db tests should be last since the backup test occurs there. - import trac.db.tests - trac.db.tests.functionalSuite(suite) - + suite.addTest(RegressionTestTicket6318()) + suite.addTest(RegressionTestTicket11434()) return suite if __name__ == '__main__': - unittest.main(defaultTest='suite') + unittest.main(defaultTest='functionalSuite') Modified: bloodhound/branches/trac-1.0.2-integration/trac/trac/tests/functional/testenv.py URL: http://svn.apache.org/viewvc/bloodhound/branches/trac-1.0.2-integration/trac/trac/tests/functional/testenv.py?rev=1639823&r1=1639822&r2=1639823&view=diff ============================================================================== --- bloodhound/branches/trac-1.0.2-integration/trac/trac/tests/functional/testenv.py (original) +++ bloodhound/branches/trac-1.0.2-integration/trac/trac/tests/functional/testenv.py Sat Nov 15 01:14:46 2014 @@ -1,24 +1,40 @@ -#!/usr/bin/python # -*- coding: utf-8 -*- # +# Copyright (C) 2008-2013 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://trac.edgewall.org/wiki/TracLicense. +# +# This software consists of voluntary contributions made by many +# individuals. For the exact contribution history, see the revision +# history and logs, available at http://trac.edgewall.org/log/. + """Object for creating and destroying a Trac environment for testing purposes. Provides some Trac environment-wide utility functions, and a way to call :command:`trac-admin` without it being on the path.""" +import locale import os -import time -import signal +import re import sys -import errno -import locale +import time from subprocess import call, Popen, PIPE, STDOUT from trac.env import open_environment from trac.test import EnvironmentStub, get_dburi -from trac.tests.functional.compat import rmtree -from trac.tests.functional import logfile +from trac.tests.compat import rmtree +from trac.tests.functional import logfile, trac_source_tree from trac.tests.functional.better_twill import tc, ConnectError -from trac.util.compat import close_fds +from trac.util import terminate +from trac.util.compat import close_fds, wait_for_file_mtime_change +from trac.util.text import to_utf8 + +try: + from configobj import ConfigObj +except ImportError: + ConfigObj = None # TODO: refactor to support testing multiple frontends, backends # (and maybe repositories and authentication). @@ -33,6 +49,7 @@ from trac.util.compat import close_fds # (those need to test search escaping, among many other things like long # paths in browser and unicode chars being allowed/translating...) + class FunctionalTestEnvironment(object): """Common location for convenience functions that work with the test environment on Trac. Subclass this and override some methods if you are @@ -47,6 +64,7 @@ class FunctionalTestEnvironment(object): def __init__(self, dirname, port, url): """Create a :class:`FunctionalTestEnvironment`, see the class itself for parameter information.""" + self.trac_src = trac_source_tree self.url = url self.command_cwd = os.path.normpath(os.path.join(dirname, '..')) self.dirname = os.path.abspath(dirname) @@ -60,8 +78,6 @@ class FunctionalTestEnvironment(object): self.create() locale.setlocale(locale.LC_ALL, '') - trac_src = '.' - @property def dburi(self): dburi = get_dburi() @@ -97,11 +113,13 @@ class FunctionalTestEnvironment(object): def post_create(self, env): """Hook for modifying the environment after creation. For example, to - set configuration like:: + set configuration like: + :: def post_create(self, env): env.config.set('git', 'path', '/usr/bin/git') env.config.save() + """ pass @@ -125,10 +143,11 @@ class FunctionalTestEnvironment(object): if call([sys.executable, os.path.join(self.trac_src, 'contrib', 'htpasswd.py'), "-c", "-b", self.htpasswd, "admin", "admin"], close_fds=close_fds, - cwd=self.command_cwd): + cwd=self.command_cwd): raise Exception('Unable to setup admin password') self.adduser('user') - self._tracadmin('permission', 'add', 'admin', 'TRAC_ADMIN') + self.adduser('joe') + self.grant_perm('admin', 'TRAC_ADMIN') # Setup Trac logging env = self.get_trac_environment() env.config.set('logging', 'log_type', 'file') @@ -140,23 +159,89 @@ class FunctionalTestEnvironment(object): def adduser(self, user): """Add a user to the environment. The password will be set to the same as username.""" + user = to_utf8(user) if call([sys.executable, os.path.join(self.trac_src, 'contrib', 'htpasswd.py'), '-b', self.htpasswd, user, user], close_fds=close_fds, cwd=self.command_cwd): raise Exception('Unable to setup password for user "%s"' % user) + def deluser(self, user): + """Delete a user from the environment.""" + user = to_utf8(user) + self._tracadmin('session', 'delete', user) + if call([sys.executable, os.path.join(self.trac_src, 'contrib', + 'htpasswd.py'), '-D', self.htpasswd, user], + close_fds=close_fds, cwd=self.command_cwd): + raise Exception('Unable to remove password for user "%s"' % user) + + def grant_perm(self, user, perm): + """Grant permission(s) to specified user. A single permission may + be specified as a string, or multiple permissions may be + specified as a list or tuple of strings.""" + if isinstance(perm, (list, tuple)): + self._tracadmin('permission', 'add', user, *perm) + else: + self._tracadmin('permission', 'add', user, perm) + # We need to force an environment reset, as this is necessary + # for the permission change to take effect: grant only + # invalidates the `DefaultPermissionStore._all_permissions` + # cache, but the `DefaultPermissionPolicy.permission_cache` is + # unaffected. + self.get_trac_environment().config.touch() + + def revoke_perm(self, user, perm): + """Revoke permission(s) from specified user. A single permission + may be specified as a string, or multiple permissions may be + specified as a list or tuple of strings.""" + if isinstance(perm, (list, tuple)): + self._tracadmin('permission', 'remove', user, *perm) + else: + self._tracadmin('permission', 'remove', user, perm) + # Force an environment reset (see grant_perm above) + self.get_trac_environment().config.touch() + + def set_config(self, *args): + """Calls trac-admin to get the value for the given option + in `trac.ini`.""" + self._tracadmin('config', 'set', *args) + + def get_config(self, *args): + """Calls trac-admin to set the value for the given option + in `trac.ini`.""" + return self._tracadmin('config', 'get', *args) + + def remove_config(self, *args): + """Calls trac-admin to remove the value for the given option + in `trac.ini`.""" + return self._tracadmin('config', 'remove', *args) + def _tracadmin(self, *args): """Internal utility method for calling trac-admin""" proc = Popen([sys.executable, os.path.join(self.trac_src, 'trac', - 'admin', 'console.py'), self.tracdir] - + list(args), stdout=PIPE, stderr=STDOUT, - close_fds=close_fds, cwd=self.command_cwd) - out = proc.communicate()[0] + 'admin', 'console.py'), self.tracdir], + stdin=PIPE, stdout=PIPE, stderr=STDOUT, + close_fds=close_fds, cwd=self.command_cwd) + if args: + if any('\n' in arg for arg in args): + raise Exception( + "trac-admin in interactive mode doesn't support " + "arguments with newline characters: %r" % (args,)) + # Don't quote first token which is sub-command name + input = ' '.join(('"%s"' % to_utf8(arg) if idx else arg) + for idx, arg in enumerate(args)) + else: + input = None + out = proc.communicate(input=input)[0] if proc.returncode: print(out) logfile.write(out) - raise Exception('Failed with exitcode %s running trac-admin ' \ - 'with %r' % (proc.returncode, args)) + raise Exception("Failed while running trac-admin with arguments %r.\n" + "Exitcode: %s \n%s" + % (args, proc.returncode, out)) + else: + # trac-admin is started in interactive mode, so we strip away + # everything up to the to the interactive prompt + return re.split(r'\r?\nTrac \[[^]]+\]> ', out, 2)[1] def start(self): """Starts the webserver, and waits for it to come up.""" @@ -177,8 +262,7 @@ class FunctionalTestEnvironment(object): server = Popen(args + options + [self.tracdir], stdout=logfile, stderr=logfile, close_fds=close_fds, - cwd=self.command_cwd, - ) + cwd=self.command_cwd) self.pid = server.pid # Verify that the url is ok timeout = 30 @@ -199,17 +283,7 @@ class FunctionalTestEnvironment(object): FIXME: probably needs a nicer way to exit for coverage to work """ if self.pid: - if os.name == 'nt': - # Untested - res = call(["taskkill", "/f", "/pid", str(self.pid)], - stdin=PIPE, stdout=PIPE, stderr=PIPE) - else: - os.kill(self.pid, signal.SIGTERM) - try: - os.waitpid(self.pid, 0) - except OSError, e: - if e.errno != errno.ESRCH: - raise + terminate(self) def restart(self): """Restarts the webserver""" @@ -224,13 +298,73 @@ class FunctionalTestEnvironment(object): """Default to no repository""" return "''" # needed for Python 2.3 and 2.4 on win32 - def call_in_workdir(self, args, environ=None): + def call_in_dir(self, dir, args, environ=None): proc = Popen(args, stdout=PIPE, stderr=logfile, - close_fds=close_fds, cwd=self.work_dir(), env=environ) + close_fds=close_fds, cwd=dir, env=environ) (data, _) = proc.communicate() if proc.wait(): raise Exception('Unable to run command %s in %s' % - (args, self.work_dir())) - + (args, dir)) logfile.write(data) return data + + def enable_authz_permpolicy(self, authz_content, filename=None): + """Enables the Authz permissions policy. The `authz_content` will + be written to `filename`, and may be specified in a triple-quoted + string.:: + + [wiki:WikiStart@*] + * = WIKI_VIEW + [wiki:PrivatePage@*] + john = WIKI_VIEW + * = !WIKI_VIEW + + `authz_content` may also be a dictionary of dictionaries specifying + the sections and key/value pairs of each section, however this form + should only be used when the order of the entries in the file is not + important, as the order cannot be known.:: + + { + 'wiki:WikiStart@*': {'*': 'WIKI_VIEW'}, + 'wiki:PrivatePage@*': {'john': 'WIKI_VIEW', '*': '!WIKI_VIEW'}, + } + + The `filename` parameter is optional, and if omitted a filename will + be generated by computing a hash of `authz_content`, prefixed with + "authz-". + """ + if not ConfigObj: + raise ImportError("Can't enable authz permissions policy. " + + "ConfigObj not installed.") + if filename is None: + from hashlib import md5 + filename = 'authz-' + md5(str(authz_content)).hexdigest()[0:9] + env = self.get_trac_environment() + permission_policies = env.config.get('trac', 'permission_policies') + env.config.set('trac', 'permission_policies', + 'AuthzPolicy, ' + permission_policies) + authz_file = self.tracdir + '/conf/' + filename + if isinstance(authz_content, basestring): + authz_content = [line.strip() for line in + authz_content.strip().splitlines()] + authz_config = ConfigObj(authz_content, encoding='utf8', + write_empty_values=True, indent_type='') + authz_config.filename = authz_file + wait_for_file_mtime_change(authz_file) + authz_config.write() + env.config.set('authz_policy', 'authz_file', authz_file) + env.config.set('components', 'tracopt.perm.authz_policy.*', 'enabled') + env.config.save() + + def disable_authz_permpolicy(self): + """Disables the Authz permission policy.""" + env = self.get_trac_environment() + permission_policies = env.config.get('trac', 'permission_policies') + pp_list = [p.strip() for p in permission_policies.split(',')] + if 'AuthzPolicy' in pp_list: + pp_list.remove('AuthzPolicy') + permission_policies = ', '.join(pp_list) + env.config.set('trac', 'permission_policies', permission_policies) + env.config.remove('authz_policy', 'authz_file') + env.config.remove('components', 'tracopt.perm.authz_policy.*') + env.config.save() Modified: bloodhound/branches/trac-1.0.2-integration/trac/trac/tests/functional/tester.py URL: http://svn.apache.org/viewvc/bloodhound/branches/trac-1.0.2-integration/trac/trac/tests/functional/tester.py?rev=1639823&r1=1639822&r2=1639823&view=diff ============================================================================== --- bloodhound/branches/trac-1.0.2-integration/trac/trac/tests/functional/tester.py (original) +++ bloodhound/branches/trac-1.0.2-integration/trac/trac/tests/functional/tester.py Sat Nov 15 01:14:46 2014 @@ -1,19 +1,34 @@ -#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright (C) 2008-2013 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://trac.edgewall.org/wiki/TracLicense. +# +# This software consists of voluntary contributions made by many +# individuals. For the exact contribution history, see the revision +# history and logs, available at http://trac.edgewall.org/log/. + """The :class:`FunctionalTester` object provides a higher-level interface to working with a Trac environment to make test cases more succinct. """ +import re + from trac.tests.functional import internal_error from trac.tests.functional.better_twill import tc, b from trac.tests.contentgen import random_page, random_sentence, random_word, \ - random_unique_camel -from trac.util.text import unicode_quote + random_unique_camel +from trac.util.text import to_utf8, unicode_quote try: from cStringIO import StringIO except ImportError: from StringIO import StringIO + class FunctionalTester(object): """Provides a library of higher-level operations for interacting with a test environment. @@ -34,10 +49,11 @@ class FunctionalTester(object): def login(self, username): """Login as the given user""" + username = to_utf8(username) tc.add_auth("", self.url, username, username) self.go_to_front() tc.find("Login") - tc.follow("Login") + tc.follow(r"\bLogin\b") # We've provided authentication info earlier, so this should # redirect back to the base url. tc.find("logged in as %s" % username) @@ -47,8 +63,9 @@ class FunctionalTester(object): def logout(self): """Logout""" - tc.follow("Logout") + tc.submit('logout', 'logout') tc.notfind(internal_error) + tc.notfind('logged in as') def create_ticket(self, summary=None, info=None): """Create a new (random) ticket in the test environment. Returns @@ -63,10 +80,10 @@ class FunctionalTester(object): `summary` and `description` default to randomly-generated values. """ self.go_to_front() - tc.follow('New Ticket') + tc.follow(r"\bNew Ticket\b") tc.notfind(internal_error) - if summary == None: - summary = random_sentence(4) + if summary is None: + summary = random_sentence(5) tc.formvalue('propertyform', 'field_summary', summary) tc.formvalue('propertyform', 'field_description', random_page()) if info: @@ -75,17 +92,12 @@ class FunctionalTester(object): tc.submit('submit') # we should be looking at the newly created ticket tc.url(self.url + '/ticket/%s' % (self.ticketcount + 1)) + tc.notfind(internal_error) # Increment self.ticketcount /after/ we've verified that the ticket # was created so a failure does not trigger spurious later # failures. self.ticketcount += 1 - # verify the ticket creation event shows up in the timeline - self.go_to_timeline() - tc.formvalue('prefs', 'ticket', True) - tc.submit() - tc.find('Ticket.*#%s.*created' % self.ticketcount) - return self.ticketcount def quickjump(self, search): @@ -94,51 +106,91 @@ class FunctionalTester(object): tc.submit() tc.notfind(internal_error) + def go_to_url(self, url): + tc.go(url) + tc.url(re.escape(url)) + tc.notfind(internal_error) + def go_to_front(self): """Go to the Trac front page""" - tc.go(self.url) - tc.url(self.url) - tc.notfind(internal_error) + self.go_to_url(self.url) - def go_to_ticket(self, ticketid): - """Surf to the page for the given ticket ID. Assumes ticket - exists.""" - ticket_url = self.url + "/ticket/%s" % ticketid - tc.go(ticket_url) - tc.url(ticket_url) + def go_to_ticket(self, ticketid=None): + """Surf to the page for the given ticket ID, or to the NewTicket page + if `ticketid` is not specified or is `None`. If `ticketid` is + specified, it assumes the ticket exists.""" + if ticketid is not None: + ticket_url = self.url + '/ticket/%s' % ticketid + else: + ticket_url = self.url + '/newticket' + self.go_to_url(ticket_url) + tc.url(ticket_url + '$') + + def go_to_wiki(self, name, version=None): + """Surf to the wiki page. By default this will be the latest version + of the page. - def go_to_wiki(self, name): - """Surf to the page for the given wiki page.""" + :param name: name of the wiki page. + :param version: version of the wiki page. + """ # Used to go based on a quickjump, but if the wiki pagename isn't # camel case, that won't work. wiki_url = self.url + '/wiki/%s' % name - tc.go(wiki_url) - tc.url(wiki_url) + if version: + wiki_url += '?version=%s' % version + self.go_to_url(wiki_url) def go_to_timeline(self): """Surf to the timeline page.""" self.go_to_front() - tc.follow('Timeline') + tc.follow(r"\bTimeline\b") tc.url(self.url + '/timeline') + def go_to_view_tickets(self, href='report'): + """Surf to the View Tickets page. By default this will be the Reports + page, but 'query' can be specified for the `href` argument to support + non-default configurations.""" + self.go_to_front() + tc.follow(r"\bView Tickets\b") + tc.url(self.url + '/' + href.lstrip('/')) + def go_to_query(self): """Surf to the custom query page.""" self.go_to_front() - tc.follow('View Tickets') - tc.follow('Custom Query') + tc.follow(r"\bView Tickets\b") + tc.follow(r"\bCustom Query\b") tc.url(self.url + '/query') - def go_to_admin(self): - """Surf to the webadmin page.""" + def go_to_admin(self, panel_label=None): + """Surf to the webadmin page. Continue surfing to a specific + admin page if `panel_label` is specified.""" self.go_to_front() - tc.follow('\\bAdmin\\b') + tc.follow(r"\bAdmin\b") + tc.url(self.url + '/admin') + if panel_label is not None: + tc.follow(r"\b%s\b" % panel_label) def go_to_roadmap(self): """Surf to the roadmap page.""" self.go_to_front() - tc.follow('\\bRoadmap\\b') + tc.follow(r"\bRoadmap\b") tc.url(self.url + '/roadmap') + def go_to_milestone(self, name): + """Surf to the specified milestone page. Assumes milestone exists.""" + self.go_to_roadmap() + tc.follow(r"\bMilestone: %s\b" % name) + tc.url(self.url + '/milestone/%s' % name) + + def go_to_preferences(self, panel_label=None): + """Surf to the preferences page. Continue surfing to a specific + preferences panel if `panel_label` is specified.""" + self.go_to_front() + tc.follow(r"\bPreferences\b") + tc.url(self.url + '/prefs') + if panel_label is not None: + tc.follow(r"\b%s\b" % panel_label) + def add_comment(self, ticketid, comment=None): """Adds a comment to the given ticket ID, assumes ticket exists.""" self.go_to_ticket(ticketid) @@ -152,35 +204,16 @@ class FunctionalTester(object): tc.url(self.url + '/ticket/%s(?:#comment:.*)?$' % ticketid) return comment - def attach_file_to_ticket(self, ticketid, data=None, tempfilename=None, + def attach_file_to_ticket(self, ticketid, data=None, filename=None, description=None, replace=False, content_type=None): """Attaches a file to the given ticket id, with random data if none is provided. Assumes the ticket exists. """ - if data is None: - data = random_page() - if description is None: - description = random_sentence() - if tempfilename is None: - tempfilename = random_word() - self.go_to_ticket(ticketid) - # set the value to what it already is, so that twill will know we - # want this form. - tc.formvalue('attachfile', 'action', 'new') - tc.submit() - tc.url(self.url + "/attachment/ticket/" \ - "%s/\\?action=new&attachfilebutton=Attach\\+file" % ticketid) - fp = StringIO(data) - tc.formfile('attachment', 'attachment', tempfilename, - content_type=content_type, fp=fp) - tc.formvalue('attachment', 'description', description) - if replace: - tc.formvalue('attachment', 'replace', True) - tc.submit() - tc.url(self.url + '/attachment/ticket/%s/$' % ticketid) - return tempfilename + return self._attach_file_to_resource('ticket', ticketid, data, + filename, description, + replace, content_type) def clone_ticket(self, ticketid): """Create a clone of the given ticket id using the clone button.""" @@ -194,57 +227,66 @@ class FunctionalTester(object): tc.url(self.url + "/ticket/%s" % self.ticketcount) return self.ticketcount - def create_wiki_page(self, page, content=None): - """Creates the specified wiki page, with random content if none is - provided. + def create_wiki_page(self, name=None, content=None, comment=None): + """Creates a wiki page, with a random unique CamelCase name if none + is provided, random content if none is provided and a random comment + if none is provided. Returns the name of the wiki page. """ - if content == None: + if name is None: + name = random_unique_camel() + if content is None: content = random_page() - page_url = self.url + "/wiki/" + page - tc.go(page_url) - tc.url(page_url) - tc.find("The page %s does not exist." % page) - tc.formvalue('modifypage', 'action', 'edit') - tc.submit() - tc.url(page_url + '\\?action=edit') + self.go_to_wiki(name) + tc.find("The page %s does not exist." % name) - tc.formvalue('edit', 'text', content) - tc.submit('save') - tc.url(page_url+'$') + self.edit_wiki_page(name, content, comment) # verify the event shows up in the timeline self.go_to_timeline() tc.formvalue('prefs', 'wiki', True) tc.submit() - tc.find(page + ".*created") + tc.find(name + ".*created") - def attach_file_to_wiki(self, name, data=None, tempfilename=None): + self.go_to_wiki(name) + + return name + + def edit_wiki_page(self, name, content=None, comment=None): + """Edits a wiki page, with random content is none is provided. + and a random comment if none is provided. Returns the content. + """ + if content is None: + content = random_page() + if comment is None: + comment = random_sentence() + self.go_to_wiki(name) + tc.formvalue('modifypage', 'action', 'edit') + tc.submit() + tc.formvalue('edit', 'text', content) + tc.formvalue('edit', 'comment', comment) + tc.submit('save') + page_url = self.url + '/wiki/%s' % name + tc.url(page_url+'$') + + return content + + def attach_file_to_wiki(self, name, data=None, filename=None, + description=None, replace=False, + content_type=None): """Attaches a file to the given wiki page, with random content if none is provided. Assumes the wiki page exists. """ - if data == None: - data = random_page() - if tempfilename is None: - tempfilename = random_word() + self.go_to_wiki(name) - # set the value to what it already is, so that twill will know we - # want this form. - tc.formvalue('attachfile', 'action', 'new') - tc.submit() - tc.url(self.url + "/attachment/wiki/" \ - "%s/\\?action=new&attachfilebutton=Attach\\+file" % name) - fp = StringIO(data) - tc.formfile('attachment', 'attachment', tempfilename, fp=fp) - tc.formvalue('attachment', 'description', random_sentence()) - tc.submit() - tc.url(self.url + '/attachment/wiki/%s/$' % name) - return tempfilename + return self._attach_file_to_resource('wiki', name, data, + filename, description, + replace, content_type) def create_milestone(self, name=None, due=None): """Creates the specified milestone, with a random name if none is provided. Returns the name of the milestone. """ - if name == None: + if name is None: name = random_unique_camel() milestone_url = self.url + "/admin/ticket/milestones" tc.go(milestone_url) @@ -260,32 +302,51 @@ class FunctionalTester(object): tc.find(name) # Make sure it's on the roadmap. - tc.follow('Roadmap') + tc.follow(r"\bRoadmap\b") tc.url(self.url + "/roadmap") tc.find('Milestone:.*%s' % name) - tc.follow(name) + tc.follow(r"\b%s\b" % name) tc.url('%s/milestone/%s' % (self.url, unicode_quote(name))) if not due: tc.find('No date set') return name - def create_component(self, name=None, user=None): + def attach_file_to_milestone(self, name, data=None, filename=None, + description=None, replace=False, + content_type=None): + """Attaches a file to the given milestone, with random content if none + is provided. Assumes the milestone exists. + """ + + self.go_to_milestone(name) + return self._attach_file_to_resource('milestone', name, data, + filename, description, + replace, content_type) + + def create_component(self, name=None, owner=None, description=None): """Creates the specified component, with a random camel-cased name if none is provided. Returns the name.""" - if name == None: + if name is None: name = random_unique_camel() component_url = self.url + "/admin/ticket/components" tc.go(component_url) tc.url(component_url) tc.formvalue('addcomponent', 'name', name) - if user != None: - tc.formvalue('addcomponent', 'owner', user) + if owner is not None: + tc.formvalue('addcomponent', 'owner', owner) tc.submit() # Verify the component appears in the component list tc.url(component_url) tc.find(name) tc.notfind(internal_error) + if description is not None: + tc.follow(r"\b%s\b" % name) + tc.formvalue('modcomp', 'description', description) + tc.submit('save') + tc.url(component_url) + tc.find("Your changes have been saved.") + tc.notfind(internal_error) # TODO: verify the component shows up in the newticket page return name @@ -294,7 +355,7 @@ class FunctionalTester(object): ``severity``, etc). If no name is given, a unique random word is used. The name is returned. """ - if name == None: + if name is None: name = random_unique_camel() priority_url = self.url + "/admin/ticket/" + kind tc.go(priority_url) @@ -326,12 +387,12 @@ class FunctionalTester(object): """Create a new version. The name defaults to a random camel-cased word if not provided.""" version_admin = self.url + "/admin/ticket/versions" - if name == None: + if name is None: name = random_unique_camel() tc.go(version_admin) tc.url(version_admin) tc.formvalue('addversion', 'name', name) - if releasetime != None: + if releasetime is not None: tc.formvalue('addversion', 'time', releasetime) tc.submit() tc.url(version_admin) @@ -342,7 +403,7 @@ class FunctionalTester(object): def create_report(self, title, query, description): """Create a new report with the given title, query, and description""" self.go_to_front() - tc.follow('View Tickets') + tc.follow(r"\bView Tickets\b") tc.formvalue('create_report', 'action', 'new') # select the right form tc.submit() tc.find('New Report') @@ -364,3 +425,29 @@ class FunctionalTester(object): tc.formvalue('propertyform', 'milestone', milestone) tc.submit('submit') # TODO: verify the change occurred. + + def _attach_file_to_resource(self, realm, name, data=None, + filename=None, description=None, + replace=False, content_type=None): + """Attaches a file to a resource. Assumes the resource exists and + has already been navigated to.""" + + if data is None: + data = random_page() + if description is None: + description = random_sentence() + if filename is None: + filename = random_word() + + tc.submit('attachfilebutton', 'attachfile') + tc.url(self.url + r'/attachment/%s/%s/\?action=new$' % (realm, name)) + fp = StringIO(data) + tc.formfile('attachment', 'attachment', filename, + content_type=content_type, fp=fp) + tc.formvalue('attachment', 'description', description) + if replace: + tc.formvalue('attachment', 'replace', True) + tc.submit() + tc.url(self.url + r'/attachment/%s/%s/$' % (realm, name)) + + return filename Modified: bloodhound/branches/trac-1.0.2-integration/trac/trac/tests/notification.py URL: http://svn.apache.org/viewvc/bloodhound/branches/trac-1.0.2-integration/trac/trac/tests/notification.py?rev=1639823&r1=1639822&r2=1639823&view=diff ============================================================================== --- bloodhound/branches/trac-1.0.2-integration/trac/trac/tests/notification.py (original) +++ bloodhound/branches/trac-1.0.2-integration/trac/trac/tests/notification.py Sat Nov 15 01:14:46 2014 @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2005-2009 Edgewall Software +# Copyright (C) 2005-2013 Edgewall Software # Copyright (C) 2005-2006 Emmanuel Blot <emmanuel.b...@free.fr> # All rights reserved. # @@ -19,24 +19,30 @@ # classes to run SMTP notification tests # +import base64 +import os +import quopri +import re import socket import string import threading -import re -import base64 -import quopri +import unittest +from trac.config import ConfigurationError +from trac.notification import SendmailEmailSender, SmtpEmailSender +from trac.test import EnvironmentStub LF = '\n' CR = '\r' -email_re = re.compile(r"([\w\d_\.\-])+\@(([\w\d\-])+\.)+([\w\d]{2,4})+") +SMTP_TEST_PORT = 7000 + os.getpid() % 1000 +email_re = re.compile(r'([\w\d_\.\-])+\@(([\w\d\-])+\.)+([\w\d]{2,4})+') header_re = re.compile(r'^=\?(?P<charset>[\w\d\-]+)\?(?P<code>[qb])\?(?P<value>.*)\?=$') class SMTPServerInterface: """ - A base class for the imlementation of an application specific SMTP - Server. Applications should subclass this and overide these + A base class for the implementation of an application specific SMTP + Server. Applications should subclass this and override these methods, which by default do nothing. A method is defined for each RFC821 command. For each of these @@ -44,7 +50,7 @@ class SMTPServerInterface: client. The 'data' method is called after all of the client DATA is received. - If a method returns 'None', then a '250 OK'message is + If a method returns 'None', then a '250 OK' message is automatically sent to the client. If a subclass returns a non-null string then it is returned instead. """ @@ -67,10 +73,10 @@ class SMTPServerInterface: def reset(self, args): return None + # # Some helper functions for manipulating from & to addresses etc. # - def strip_address(address): """ Strip the leading & trailing <> from an address. Handy for @@ -80,6 +86,7 @@ def strip_address(address): end = string.index(address, '>') return address[start:end] + def split_to(address): """ Return 'address' as undressed (host, fulladdress) tuple. @@ -88,7 +95,7 @@ def split_to(address): start = string.index(address, '<') + 1 sep = string.index(address, '@') + 1 end = string.index(address, '>') - return (address[sep:end], address[start:end],) + return address[sep:end], address[start:end] # @@ -133,13 +140,13 @@ class SMTPServerEngine: lump = self.socket.recv(1024) if len(lump): data += lump - if (len(data) >= 2) and data[-2:] == '\r\n': + if len(data) >= 2 and data[-2:] == '\r\n': completeLine = 1 if self.state != SMTPServerEngine.ST_DATA: rsp, keep = self.do_command(data) else: rsp = self.do_data(data) - if rsp == None: + if rsp is None: continue self.socket.send(rsp + "\r\n") if keep == 0: @@ -171,28 +178,28 @@ class SMTPServerEngine: keep = 0 elif cmd == "MAIL": if self.state != SMTPServerEngine.ST_HELO: - return ("503 Bad command sequence", 1) + return "503 Bad command sequence", 1 self.state = SMTPServerEngine.ST_MAIL rv = self.impl.mail_from(data[5:]) elif cmd == "RCPT": if (self.state != SMTPServerEngine.ST_MAIL) and \ (self.state != SMTPServerEngine.ST_RCPT): - return ("503 Bad command sequence", 1) + return "503 Bad command sequence", 1 self.state = SMTPServerEngine.ST_RCPT rv = self.impl.rcpt_to(data[5:]) elif cmd == "DATA": if self.state != SMTPServerEngine.ST_RCPT: - return ("503 Bad command sequence", 1) + return "503 Bad command sequence", 1 self.state = SMTPServerEngine.ST_DATA self.data_accum = "" - return ("354 OK, Enter data, terminated with a \\r\\n.\\r\\n", 1) + return "354 OK, Enter data, terminated with a \\r\\n.\\r\\n", 1 else: - return ("505 Eh? WTF was that?", 1) + return "505 Eh? WTF was that?", 1 if rv: - return (rv, keep) + return rv, keep else: - return("250 OK", keep) + return "250 OK", keep def do_data(self, data): """ @@ -227,7 +234,7 @@ class SMTPServer: self._socket_service = None def serve(self, impl): - while ( self._resume ): + while self._resume: try: nsd = self._socket.accept() except socket.error: @@ -273,11 +280,11 @@ class SMTPServerStore(SMTPServerInterfac def mail_from(self, args): if args.lower().startswith('from:'): - self.sender = strip_address(args[5:].replace('\r\n','').strip()) + self.sender = strip_address(args[5:].replace('\r\n', '').strip()) def rcpt_to(self, args): if args.lower().startswith('to:'): - rcpt = args[3:].replace('\r\n','').strip() + rcpt = args[3:].replace('\r\n', '').strip() self.recipients.append(strip_address(rcpt)) def data(self, args): @@ -300,12 +307,12 @@ class SMTPThreadedServer(threading.Threa def __init__(self, port): self.port = port self.server = SMTPServer(port) - self.store = SMTPServerStore() + self.store = SMTPServerStore() threading.Thread.__init__(self) def run(self): # run from within the SMTP server thread - self.server.serve(impl = self.store) + self.server.serve(impl=self.store) def start(self): # run from the main thread @@ -356,19 +363,19 @@ def decode_header(header): # header does not seem to be MIME-encoded if not mo: return header - # attempts to decode the hedear, - # following the specified MIME endoding and charset + # attempts to decode the header, + # following the specified MIME encoding and charset try: encoding = mo.group('code').lower() - if encoding == 'q': + if encoding == 'q': val = quopri.decodestring(mo.group('value'), header=True) elif encoding == 'b': val = base64.decodestring(mo.group('value')) else: - raise AssertionError, "unsupported encoding: %s" % encoding + raise AssertionError("unsupported encoding: %s" % encoding) header = unicode(val, mo.group('charset')) except Exception, e: - raise AssertionError, e + raise AssertionError(e) return header @@ -384,19 +391,19 @@ def parse_smtp_message(msg): # last line does not contain the final line ending msg += '\r\n' for line in msg.splitlines(True): - if body != None: + if body is not None: # append current line to the body if line[-2] == CR: body += line[0:-2] body += '\n' else: - raise AssertionError, "body misses CRLF: %s (0x%x)" \ - % (line, ord(line[-1])) + raise AssertionError("body misses CRLF: %s (0x%x)" + % (line, ord(line[-1]))) else: if line[-2] != CR: # RFC822 requires CRLF at end of field line - raise AssertionError, "header field misses CRLF: %s (0x%x)" \ - % (line, ord(line[-1])) + raise AssertionError("header field misses CRLF: %s (0x%x)" + % (line, ord(line[-1]))) # discards CR line = line[0:-2] if line.strip() == '': @@ -405,11 +412,11 @@ def parse_smtp_message(msg): else: val = None if line[0] in ' \t': - # continution of the previous line + # continuation of the previous line if not lh: # unexpected multiline - raise AssertionError, \ - "unexpected folded line: %s" % line + raise AssertionError("unexpected folded line: %s" + % line) val = decode_header(line.strip(' \t')) # appends the current line to the previous one if not isinstance(headers[lh], tuple): @@ -420,14 +427,52 @@ def parse_smtp_message(msg): # splits header name from value (h, v) = line.split(':', 1) val = decode_header(v.strip()) - if headers.has_key(h): + if h in headers: if isinstance(headers[h], tuple): headers[h] += val else: headers[h] = (headers[h], val) else: headers[h] = val - # stores the last header (for multilines headers) + # stores the last header (for multi-line headers) lh = h # returns the headers and the message body - return (headers, body) + return headers, body + + +class SendmailEmailSenderTestCase(unittest.TestCase): + + def setUp(self): + self.env = EnvironmentStub() + + def test_sendmail_path_not_found_raises(self): + sender = SendmailEmailSender(self.env) + self.env.config.set('notification', 'sendmail_path', + os.path.join(os.path.dirname(__file__), + 'sendmail')) + self.assertRaises(ConfigurationError, sender.send, + 'ad...@domain.com', ['f...@domain.com'], "") + + +class SmtpEmailSenderTestCase(unittest.TestCase): + + def setUp(self): + self.env = EnvironmentStub() + + def test_smtp_server_not_found_raises(self): + sender = SmtpEmailSender(self.env) + self.env.config.set('notification', 'smtp_server', 'localhost') + self.env.config.set('notification', 'smtp_port', '65536') + self.assertRaises(ConfigurationError, sender.send, + 'ad...@domain.com', ['f...@domain.com'], "") + + +def suite(): + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(SendmailEmailSenderTestCase)) + suite.addTest(unittest.makeSuite(SmtpEmailSenderTestCase)) + return suite + + +if __name__ == '__main__': + unittest.main(defaultTest='suite') Modified: bloodhound/branches/trac-1.0.2-integration/trac/trac/tests/perm.py URL: http://svn.apache.org/viewvc/bloodhound/branches/trac-1.0.2-integration/trac/trac/tests/perm.py?rev=1639823&r1=1639822&r2=1639823&view=diff ============================================================================== --- bloodhound/branches/trac-1.0.2-integration/trac/trac/tests/perm.py (original) +++ bloodhound/branches/trac-1.0.2-integration/trac/trac/tests/perm.py Sat Nov 15 01:14:46 2014 @@ -1,15 +1,30 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2004-2013 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://trac.edgewall.org/wiki/TracLicense. +# +# This software consists of voluntary contributions made by many +# individuals. For the exact contribution history, see the revision +# history and logs, available at http://trac.edgewall.org/log/. + +import unittest + from trac import perm from trac.core import * +from trac.resource import Resource from trac.test import EnvironmentStub -import unittest - class DefaultPermissionStoreTestCase(unittest.TestCase): def setUp(self): - self.env = EnvironmentStub(enable=[perm.DefaultPermissionStore, - perm.DefaultPermissionGroupProvider]) + self.env = \ + EnvironmentStub(enable=[perm.DefaultPermissionStore, + perm.DefaultPermissionGroupProvider]) self.store = perm.DefaultPermissionStore(self.env) def tearDown(self): @@ -21,10 +36,10 @@ class DefaultPermissionStoreTestCase(uni [('john', 'WIKI_MODIFY'), ('john', 'REPORT_ADMIN'), ('kate', 'TICKET_CREATE')]) - self.assertEquals(['REPORT_ADMIN', 'WIKI_MODIFY'], - sorted(self.store.get_user_permissions('john'))) - self.assertEquals(['TICKET_CREATE'], - self.store.get_user_permissions('kate')) + self.assertEqual(['REPORT_ADMIN', 'WIKI_MODIFY'], + sorted(self.store.get_user_permissions('john'))) + self.assertEqual(['TICKET_CREATE'], + self.store.get_user_permissions('kate')) def test_simple_group(self): self.env.db_transaction.executemany( @@ -32,8 +47,8 @@ class DefaultPermissionStoreTestCase(uni [('dev', 'WIKI_MODIFY'), ('dev', 'REPORT_ADMIN'), ('john', 'dev')]) - self.assertEquals(['REPORT_ADMIN', 'WIKI_MODIFY'], - sorted(self.store.get_user_permissions('john'))) + self.assertEqual(['REPORT_ADMIN', 'WIKI_MODIFY'], + sorted(self.store.get_user_permissions('john'))) def test_nested_groups(self): self.env.db_transaction.executemany( @@ -42,8 +57,8 @@ class DefaultPermissionStoreTestCase(uni ('dev', 'REPORT_ADMIN'), ('admin', 'dev'), ('john', 'admin')]) - self.assertEquals(['REPORT_ADMIN', 'WIKI_MODIFY'], - sorted(self.store.get_user_permissions('john'))) + self.assertEqual(['REPORT_ADMIN', 'WIKI_MODIFY'], + sorted(self.store.get_user_permissions('john'))) def test_mixed_case_group(self): self.env.db_transaction.executemany( @@ -52,8 +67,8 @@ class DefaultPermissionStoreTestCase(uni ('Dev', 'REPORT_ADMIN'), ('Admin', 'Dev'), ('john', 'Admin')]) - self.assertEquals(['REPORT_ADMIN', 'WIKI_MODIFY'], - sorted(self.store.get_user_permissions('john'))) + self.assertEqual(['REPORT_ADMIN', 'WIKI_MODIFY'], + sorted(self.store.get_user_permissions('john'))) def test_builtin_groups(self): self.env.db_transaction.executemany( @@ -61,10 +76,10 @@ class DefaultPermissionStoreTestCase(uni [('authenticated', 'WIKI_MODIFY'), ('authenticated', 'REPORT_ADMIN'), ('anonymous', 'TICKET_CREATE')]) - self.assertEquals(['REPORT_ADMIN', 'TICKET_CREATE', 'WIKI_MODIFY'], - sorted(self.store.get_user_permissions('john'))) - self.assertEquals(['TICKET_CREATE'], - self.store.get_user_permissions('anonymous')) + self.assertEqual(['REPORT_ADMIN', 'TICKET_CREATE', 'WIKI_MODIFY'], + sorted(self.store.get_user_permissions('john'))) + self.assertEqual(['TICKET_CREATE'], + self.store.get_user_permissions('anonymous')) def test_get_all_permissions(self): self.env.db_transaction.executemany( @@ -76,7 +91,7 @@ class DefaultPermissionStoreTestCase(uni ('dev', 'REPORT_ADMIN'), ('john', 'dev')] for res in self.store.get_all_permissions(): - self.failIf(res not in expected) + self.assertFalse(res not in expected) class TestPermissionRequestor(Component): @@ -89,6 +104,48 @@ class TestPermissionRequestor(Component) ('TEST_ADMIN', ['TEST_MODIFY'])] +class PermissionErrorTestCase(unittest.TestCase): + + def setUp(self): + self.env = EnvironmentStub() + + def test_default_message(self): + permission_error = perm.PermissionError() + self.assertEqual(None, permission_error.action) + self.assertEqual(None, permission_error.resource) + self.assertEqual(None, permission_error.env) + self.assertEqual("Insufficient privileges to perform this operation.", + unicode(permission_error)) + self.assertEqual("Forbidden", permission_error.title) + self.assertEqual(unicode(permission_error), permission_error.msg) + + def test_message_specified(self): + message = "The message." + permission_error = perm.PermissionError(msg=message) + self.assertEqual(message, unicode(permission_error)) + + def test_message_from_action(self): + action = 'WIKI_VIEW' + permission_error = perm.PermissionError(action) + self.assertEqual(action, permission_error.action) + self.assertEqual(None, permission_error.resource) + self.assertEqual(None, permission_error.env) + self.assertEqual("WIKI_VIEW privileges are required to perform this " + "operation. You don't have the required " + "permissions.", unicode(permission_error)) + + def test_message_from_action_and_resource(self): + action = 'WIKI_VIEW' + resource = Resource('wiki', 'WikiStart') + permission_error = perm.PermissionError(action, resource, self.env) + self.assertEqual(action, permission_error.action) + self.assertEqual(resource, permission_error.resource) + self.assertEqual(self.env, permission_error.env) + self.assertEqual("WIKI_VIEW privileges are required to perform this " + "operation on WikiStart. You don't have the " + "required permissions.", unicode(permission_error)) + + class PermissionSystemTestCase(unittest.TestCase): def setUp(self): @@ -130,7 +187,7 @@ class PermissionSystemTestCase(unittest. expected = [('bob', 'TEST_CREATE'), ('jane', 'TEST_ADMIN')] for res in self.perm.get_all_permissions(): - self.failIf(res not in expected) + self.assertFalse(res not in expected) def test_expand_actions_iter_7467(self): # Check that expand_actions works with iterators (#7467) @@ -146,6 +203,8 @@ class PermissionCacheTestCase(unittest.T self.env = EnvironmentStub(enable=[perm.DefaultPermissionStore, perm.DefaultPermissionPolicy, TestPermissionRequestor]) + self.env.config.set('trac', 'permission_policies', + 'DefaultPermissionPolicy') self.perm_system = perm.PermissionSystem(self.env) # by-pass DefaultPermissionPolicy cache: perm.DefaultPermissionPolicy.CACHE_EXPIRY = -1 @@ -157,19 +216,20 @@ class PermissionCacheTestCase(unittest.T self.env.reset_db() def test_contains(self): - self.assertEqual(True, 'TEST_MODIFY' in self.perm) - self.assertEqual(True, 'TEST_ADMIN' in self.perm) - self.assertEqual(False, 'TRAC_ADMIN' in self.perm) + self.assertTrue('TEST_MODIFY' in self.perm) + self.assertTrue('TEST_ADMIN' in self.perm) + self.assertFalse('TRAC_ADMIN' in self.perm) def test_has_permission(self): - self.assertEqual(True, self.perm.has_permission('TEST_MODIFY')) - self.assertEqual(True, self.perm.has_permission('TEST_ADMIN')) - self.assertEqual(False, self.perm.has_permission('TRAC_ADMIN')) + self.assertTrue(self.perm.has_permission('TEST_MODIFY')) + self.assertTrue(self.perm.has_permission('TEST_ADMIN')) + self.assertFalse(self.perm.has_permission('TRAC_ADMIN')) def test_require(self): self.perm.require('TEST_MODIFY') self.perm.require('TEST_ADMIN') - self.assertRaises(perm.PermissionError, self.perm.require, 'TRAC_ADMIN') + self.assertRaises(perm.PermissionError, + self.perm.require, 'TRAC_ADMIN') def test_assert_permission(self): self.perm.assert_permission('TEST_MODIFY') @@ -216,12 +276,14 @@ class TestPermissionPolicy(Component): class PermissionPolicyTestCase(unittest.TestCase): + def setUp(self): self.env = EnvironmentStub(enable=[perm.DefaultPermissionStore, perm.DefaultPermissionPolicy, TestPermissionPolicy, TestPermissionRequestor]) - self.env.config.set('trac', 'permission_policies', 'TestPermissionPolicy') + self.env.config.set('trac', 'permission_policies', + 'TestPermissionPolicy') self.policy = TestPermissionPolicy(self.env) self.perm = perm.PermissionCache(self.env, 'testuser') @@ -246,7 +308,8 @@ class PermissionPolicyTestCase(unittest. ('testuser', 'TEST_ADMIN'): True}) def test_policy_chaining(self): - self.env.config.set('trac', 'permission_policies', 'TestPermissionPolicy,DefaultPermissionPolicy') + self.env.config.set('trac', 'permission_policies', + 'TestPermissionPolicy,DefaultPermissionPolicy') self.policy.grant('testuser', ['TEST_MODIFY']) system = perm.PermissionSystem(self.env) system.grant_permission('testuser', 'TEST_ADMIN') @@ -259,13 +322,17 @@ class PermissionPolicyTestCase(unittest. self.assertEqual(self.policy.results, {('testuser', 'TEST_MODIFY'): True, ('testuser', 'TEST_ADMIN'): None}) + + def suite(): suite = unittest.TestSuite() - suite.addTest(unittest.makeSuite(DefaultPermissionStoreTestCase, 'test')) - suite.addTest(unittest.makeSuite(PermissionSystemTestCase, 'test')) - suite.addTest(unittest.makeSuite(PermissionCacheTestCase, 'test')) - suite.addTest(unittest.makeSuite(PermissionPolicyTestCase, 'test')) + suite.addTest(unittest.makeSuite(DefaultPermissionStoreTestCase)) + suite.addTest(unittest.makeSuite(PermissionErrorTestCase)) + suite.addTest(unittest.makeSuite(PermissionSystemTestCase)) + suite.addTest(unittest.makeSuite(PermissionCacheTestCase)) + suite.addTest(unittest.makeSuite(PermissionPolicyTestCase)) return suite + if __name__ == '__main__': - unittest.main() + unittest.main(defaultTest='suite') Modified: bloodhound/branches/trac-1.0.2-integration/trac/trac/tests/resource.py URL: http://svn.apache.org/viewvc/bloodhound/branches/trac-1.0.2-integration/trac/trac/tests/resource.py?rev=1639823&r1=1639822&r2=1639823&view=diff ============================================================================== --- bloodhound/branches/trac-1.0.2-integration/trac/trac/tests/resource.py (original) +++ bloodhound/branches/trac-1.0.2-integration/trac/trac/tests/resource.py Sat Nov 15 01:14:46 2014 @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2007-2009 Edgewall Software +# Copyright (C) 2007-2013 Edgewall Software # All rights reserved. # # This software is licensed as described in the file COPYING, which @@ -165,7 +165,7 @@ class TestResourceChangeListener(Compone def suite(): suite = unittest.TestSuite() suite.addTest(doctest.DocTestSuite(resource)) - suite.addTest(unittest.makeSuite(ResourceTestCase, 'test')) + suite.addTest(unittest.makeSuite(ResourceTestCase)) suite.addTest(unittest.makeSuite(NeighborhoodTestCase, 'test')) return suite Modified: bloodhound/branches/trac-1.0.2-integration/trac/trac/tests/wikisyntax.py URL: http://svn.apache.org/viewvc/bloodhound/branches/trac-1.0.2-integration/trac/trac/tests/wikisyntax.py?rev=1639823&r1=1639822&r2=1639823&view=diff ============================================================================== --- bloodhound/branches/trac-1.0.2-integration/trac/trac/tests/wikisyntax.py (original) +++ bloodhound/branches/trac-1.0.2-integration/trac/trac/tests/wikisyntax.py Sat Nov 15 01:14:46 2014 @@ -1,4 +1,18 @@ -import os +# -*- coding: utf-8 -*- +# +# Copyright (C) 2006-2013 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://trac.edgewall.org/wiki/TracLicense. +# +# This software consists of voluntary contributions made by many +# individuals. For the exact contribution history, see the revision +# history and logs, available at http://trac.edgewall.org/log/. + +from __future__ import with_statement + import shutil import tempfile import unittest @@ -11,6 +25,7 @@ from trac.test import MockPerm from trac.web.href import Href from trac.wiki.tests import formatter + SEARCH_TEST_CASES = u""" ============================== search: link resolver search:foo @@ -116,12 +131,14 @@ attachment:file.txt?format=raw def attachment_setup(tc): import trac.ticket.api import trac.wiki.api - tc.env.path = os.path.join(tempfile.gettempdir(), 'trac-tempenv') - os.mkdir(tc.env.path) - attachment = Attachment(tc.env, 'wiki', 'WikiStart') - attachment.insert('file.txt', tempfile.TemporaryFile(), 0) + tc.env.path = tempfile.mkdtemp(prefix='trac-tempenv-') + with tc.env.db_transaction as db: + db("INSERT INTO wiki (name,version) VALUES ('SomePage/SubPage',1)") + db("INSERT INTO ticket (id) VALUES (123)") attachment = Attachment(tc.env, 'ticket', 123) attachment.insert('file.txt', tempfile.TemporaryFile(), 0) + attachment = Attachment(tc.env, 'wiki', 'WikiStart') + attachment.insert('file.txt', tempfile.TemporaryFile(), 0) attachment = Attachment(tc.env, 'wiki', 'SomePage/SubPage') attachment.insert('foo.txt', tempfile.TemporaryFile(), 0) @@ -188,4 +205,3 @@ def suite(): if __name__ == '__main__': unittest.main(defaultTest='suite') - Modified: bloodhound/branches/trac-1.0.2-integration/trac/trac/ticket/__init__.py URL: http://svn.apache.org/viewvc/bloodhound/branches/trac-1.0.2-integration/trac/trac/ticket/__init__.py?rev=1639823&r1=1639822&r2=1639823&view=diff ============================================================================== --- bloodhound/branches/trac-1.0.2-integration/trac/trac/ticket/__init__.py (original) +++ bloodhound/branches/trac-1.0.2-integration/trac/trac/ticket/__init__.py Sat Nov 15 01:14:46 2014 @@ -1,3 +1,16 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2005-2013 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://trac.edgewall.org/wiki/TracLicense. +# +# This software consists of voluntary contributions made by many +# individuals. For the exact contribution history, see the revision +# history and logs, available at http://trac.edgewall.org/log/. + from trac.ticket.api import * from trac.ticket.default_workflow import * from trac.ticket.model import *