#!/usr/bin/python """Automated tests for MergeBot """ import os import unittest import time import shutil from subprocess import call, Popen #, PIPE, STDOUT from twill.errors import TwillAssertionError from trac.tests.functional import FunctionalTestSuite, FunctionalTestEnvironment, FunctionalTester, TwillTest, tc, b, logfile from trac.tests.contentgen import random_page #, random_sentence, random_word class MergeBotTestEnvironment(FunctionalTestEnvironment): """Slight change to FunctionalTestEnvironment to keep the PYTHONPATH from our environment. """ def start(self): """Starts the webserver""" server = Popen(["python", "./trac/web/standalone.py", "--port=%s" % self.port, "-s", "--basic-auth=trac,%s," % self.htpasswd, self.tracdir], #env={'PYTHONPATH':'.'}, stdout=logfile, stderr=logfile, ) self.pid = server.pid time.sleep(1) # Give the server time to come up def _tracadmin(self, *args): """Internal utility method for calling trac-admin""" if call(["python", "./trac/admin/console.py", self.tracdir] + list(args), #env={'PYTHONPATH':'.'}, stdout=logfile, stderr=logfile): raise Exception('Failed running trac-admin with %r' % (args, )) FunctionalTestEnvironment = MergeBotTestEnvironment class MergeBotFunctionalTester(FunctionalTester): """Adds some MergeBot functionality to the functional tester.""" # FIXME: the tc.find( ) checks are bogus: any ticket can # satisfy them, not just the one we're working on. def __init__(self, *args, **kwargs): FunctionalTester.__init__(self, *args, **kwargs) self.mergeboturl = self.url + '/mergebot' def wait_until_find(self, search, timeout=5): start = time.time() while time.time() - start < timeout: try: tc.reload() tc.find(search) return except TwillAssertionError: pass raise TwillAssertionError("Unable to find %r within %s seconds" % (search, timeout)) def wait_until_notfind(self, search, timeout=5): start = time.time() while time.time() - start < timeout: try: tc.reload() tc.notfind(search) return except TwillAssertionError: pass raise TwillAssertionError("Unable to notfind %r within %s seconds" % (search, timeout)) def go_to_mergebot(self): tc.go(self.mergeboturl) tc.url(self.mergeboturl) tc.notfind('No handler matched request to /mergebot') def branch(self, ticket_id, component, timeout=1): """timeout is in seconds.""" self.go_to_mergebot() tc.formvalue('ops-%s' % ticket_id, 'ticket', ticket_id) # Essentially a noop to select the right form tc.submit('Branch') self.wait_until_notfind('Doing branch', timeout) tc.find('Rebranch') tc.find('Merge') tc.find('CheckMerge') self.go_to_ticket(ticket_id) tc.find('Created branch from .* for .*') retval = call(['svn', 'ls', self.repo_url + '/' + component + '/branches/ticket-%s' % ticket_id], stdout=logfile, stderr=logfile) if retval: raise Exception('svn ls failed with exit code %s' % retval) def rebranch(self, ticket_id, component, timeout=5): """timeout is in seconds.""" self.go_to_mergebot() tc.formvalue('ops-%s' % ticket_id, 'ticket', ticket_id) # Essentially a noop to select the right form tc.submit('Rebranch') self.wait_until_notfind('Doing rebranch', timeout) tc.find('Rebranch') tc.find('Merge') tc.find('CheckMerge') self.go_to_ticket(ticket_id) tc.find('Rebranched from .* for .*') retval = call(['svn', 'ls', self.repo_url + '/' + component + '/branches/ticket-%s' % ticket_id], stdout=logfile, stderr=logfile) if retval: raise Exception('svn ls failed with exit code %s' % retval) def merge(self, ticket_id, component, timeout=5): """timeout is in seconds.""" self.go_to_mergebot() tc.formvalue('ops-%s' % ticket_id, 'ticket', ticket_id) # Essentially a noop to select the right form tc.submit('Merge') self.wait_until_notfind('Doing merge', timeout) tc.find('Branch') self.go_to_ticket(ticket_id) tc.find('Merged .* to .* for') # TODO: We may want to change this to remove the "dead" branch retval = call(['svn', 'ls', self.repo_url + '/' + component + '/branches/ticket-%s' % ticket_id], stdout=logfile, stderr=logfile) if retval: raise Exception('svn ls failed with exit code %s' % retval) def checkmerge(self, ticket_id, component, timeout=5): """timeout is in seconds.""" self.go_to_mergebot() tc.formvalue('ops-%s' % ticket_id, 'ticket', ticket_id) # Essentially a noop to select the right form tc.submit('CheckMerge') self.wait_until_notfind('Doing checkmerge', timeout) tc.find('Rebranch') tc.find('Merge') tc.find('CheckMerge') self.go_to_ticket(ticket_id) tc.find('while checking merge of') # TODO: We may want to change this to remove the "dead" branch retval = call(['svn', 'ls', self.repo_url + '/' + component + '/branches/ticket-%s' % ticket_id], stdout=logfile, stderr=logfile) if retval: raise Exception('svn ls failed with exit code %s' % retval) class MergeBotTestSuite(FunctionalTestSuite): def setUp(self): port = 8889 baseurl = "http://localhost:%s" % port self._testenv = FunctionalTestEnvironment("testenv%s" % port, port) # Configure mergebot env = self._testenv.get_trac_environment() env.config.set('components', 'mergebot.web_ui.mergebotmodule', 'enabled') env.config.set('mergebot', 'repository_url', self._testenv.repo_url()) env.config.set('mergebot', 'work_dir', self._testenv.repodir + '/mergebot') env.config.set('ticket-custom', 'mergebotstate', 'select') env.config.set('ticket-custom', 'mergebotstate.editable', '0') env.config.set('ticket-custom', 'mergebotstate.label', 'MergeBotState') env.config.set('ticket-custom', 'mergebotstate.options', '| tomerge | merged | tobranch | branched | conflicts') env.config.set('ticket-custom', 'mergebotstate.order', '2') env.config.set('ticket-custom', 'mergebotstate.value', '') env.config.set('logging', 'log_type', 'file') env.config.save() env.config.parse_if_needed() self._testenv.start() self._tester = MergeBotFunctionalTester(baseurl, self._testenv.repo_url()) # Setup some common component stuff for MergeBot's use: svnurl = self._testenv.repo_url() for component in ['stuff', 'flagship', 'submarine']: self._tester.create_component(component) if call(['svn', '-m', 'Create tree for "%s".' % component, 'mkdir', svnurl + '/' + component, svnurl + '/' + component + '/trunk', svnurl + '/' + component + '/tags', svnurl + '/' + component + '/branches'], stdout=logfile, stderr=logfile): raise Exception("svn mkdir failed") self._tester.create_version('trunk') class MergeBotTestEnabled(TwillTest): def runTest(self): self._tester.logout() tc.go(self._tester.url) self._tester.login('admin') tc.follow('MergeBot') mergeboturl = self._tester.url + '/mergebot' tc.url(mergeboturl) tc.notfind('No handler matched request to /mergebot') class MergeBotTestQueueList(TwillTest): def runTest(self): tc.follow('MergeBot') for queue in 'branch', 'rebranch', 'checkmerge', 'merge': tc.find('%s Queue' % queue) class MergeBotTestNoVersion(TwillTest): """Verify that if a ticket does not have the version field set, it will not appear in the MergeBot list. """ def runTest(self): ticket_id = self._tester.create_ticket(summary=self.__class__.__name__, info={'component':'stuff', 'version':''}) tc.follow('MergeBot') tc.notfind(self.__class__.__name__) class MergeBotTestBranch(TwillTest): def runTest(self): """Verify that the 'branch' button works""" ticket_id = self._tester.create_ticket(summary=self.__class__.__name__, info={'component':'stuff', 'version':'trunk'}) self._tester.branch(ticket_id, 'stuff') class MergeBotTestRebranch(TwillTest): def runTest(self): """Verify that the 'rebranch' button works""" ticket_id = self._tester.create_ticket(summary=self.__class__.__name__, info={'component':'stuff', 'version':'trunk'}) self._tester.branch(ticket_id, 'stuff') self._tester.rebranch(ticket_id, 'stuff') class MergeBotTestMerge(TwillTest): def runTest(self): """Verify that the 'merge' button works""" ticket_id = self._tester.create_ticket(summary=self.__class__.__name__, info={'component':'stuff', 'version':'trunk'}) self._tester.branch(ticket_id, 'stuff') self._tester.merge(ticket_id, 'stuff') class MergeBotTestCheckMerge(TwillTest): def runTest(self): """Verify that the 'checkmerge' button works""" ticket_id = self._tester.create_ticket(summary=self.__class__.__name__, info={'component':'stuff', 'version':'trunk'}) self._tester.branch(ticket_id, 'stuff') self._tester.checkmerge(ticket_id, 'stuff') class MergeBotTestSingleUseCase(TwillTest): def runTest(self): """Create a branch, make a change, checkmerge, and merge it.""" ticket_id = self._tester.create_ticket(summary=self.__class__.__name__, info={'component':'stuff', 'version':'trunk'}) self._tester.branch(ticket_id, 'stuff') # checkout a working copy & make a change svnurl = self._testenv.repo_url() workdir = os.path.join(self._testenv.dirname, self.__class__.__name__) retval = call(['svn', 'checkout', svnurl + '/stuff/branches/ticket-%s' % ticket_id, workdir], stdout=logfile, stderr=logfile) self.assertEqual(retval, 0, "svn checkout failed with error %s" % (retval)) # Create & add a new file newfile = os.path.join(workdir, self.__class__.__name__) open(newfile, 'w').write(random_page()) retval = call(['svn', 'add', self.__class__.__name__], cwd=workdir, stdout=logfile, stderr=logfile) self.assertEqual(retval, 0, "svn add failed with error %s" % (retval)) retval = call(['svn', 'commit', '-m', 'Add a new file', self.__class__.__name__], cwd=workdir, stdout=logfile, stderr=logfile) self.assertEqual(retval, 0, "svn commit failed with error %s" % (retval)) self._tester.checkmerge(ticket_id, 'stuff') self._tester.merge(ticket_id, 'stuff') shutil.rmtree(workdir) # cleanup working copy def suite(): suite = MergeBotTestSuite() suite.addTest(MergeBotTestEnabled()) suite.addTest(MergeBotTestQueueList()) suite.addTest(MergeBotTestNoVersion()) suite.addTest(MergeBotTestBranch()) suite.addTest(MergeBotTestRebranch()) suite.addTest(MergeBotTestMerge()) suite.addTest(MergeBotTestCheckMerge()) suite.addTest(MergeBotTestSingleUseCase()) return suite if __name__ == '__main__': unittest.main(defaultTest='suite')