From 51d2dd6f1121ff80ddaa0c46905657a3333ee722 Mon Sep 17 00:00:00 2001 From: Baptiste Mispelon Date: Sat, 27 Jan 2024 13:32:23 +0100 Subject: [PATCH 1/9] [runtests] Use argparse for runtests.py script --- runtests.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/runtests.py b/runtests.py index 62f5127..ac927f6 100755 --- a/runtests.py +++ b/runtests.py @@ -9,6 +9,7 @@ Trac's testing framework isn't well suited for plugins, so we NIH'd a bit. """ +import argparse import BaseHTTPServer import ConfigParser import glob @@ -2120,15 +2121,22 @@ def apiMockServer(port, mockdata): httpd.mockdata = mockdata httpd.serve_forever() + +def get_parser(): + parser = argparse.ArgumentParser("Run the test suite for trac-github") + parser.add_argument('--with-coverage', action='store_true', help="Enable test coverage") + parser.add_argument('--with-trac-log', action='store_true', help="Display logs of test trac instances") + return parser + + if __name__ == '__main__': if glob.glob('test-*'): print "Test data remains from previous runs, aborting." print "Run `rm -rf test-*` and retry." sys.exit(1) - if '--with-coverage' in sys.argv: - COVERAGE = True - sys.argv.remove('--with-coverage') - if '--with-trac-log' in sys.argv: - SHOW_LOG = True - sys.argv.remove('--with-trac-log') - unittest.main() + + options, unittest_argv = get_parser().parse_known_args() + COVERAGE = options.with_coverage + SHOW_LOG = options.with_trac_log + + unittest.main(argv=[sys.argv[0]] + unittest_argv, exit=True) From 4e7c88efedc3f18b25ad7462849a65e15e7f8b44 Mon Sep 17 00:00:00 2001 From: Baptiste Mispelon Date: Sat, 27 Jan 2024 13:58:00 +0100 Subject: [PATCH 2/9] [runtests] Add support for running tests in virtualenv --- runtests.py | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/runtests.py b/runtests.py index ac927f6..085b33b 100755 --- a/runtests.py +++ b/runtests.py @@ -49,8 +49,12 @@ HEADERS = {'Content-Type': 'application/json', 'X-GitHub-Event': 'push'} UPDATEHOOK = '%s-mirror/hooks/trac-github-update' % GIT +# Global variables overriden when running the module (see very bottom of file) COVERAGE = False SHOW_LOG = False +TRAC_ADMIN_BIN = 'trac-admin' +TRACD_BIN = 'tracd' +COVERAGE_BIN = 'coverage' class HttpNoRedirectHandler(urllib2.HTTPRedirectHandler): @@ -100,9 +104,9 @@ def removeGitRepositories(cls): @classmethod def createTracEnvironment(cls, **kwargs): - subprocess.check_output(['trac-admin', ENV, 'initenv', + subprocess.check_output([TRAC_ADMIN_BIN, ENV, 'initenv', 'Trac - GitHub tests', 'sqlite:db/trac.db']) - subprocess.check_output(['trac-admin', ENV, 'permission', + subprocess.check_output([TRAC_ADMIN_BIN, ENV, 'permission', 'add', 'anonymous', 'TRAC_ADMIN']) conf = ConfigParser.ConfigParser() @@ -189,9 +193,9 @@ def createTracEnvironment(cls, **kwargs): run_resync = kwargs['resync'] if 'resync' in kwargs else True if run_resync: # Allow skipping resync for perfomance reasons if not required - subprocess.check_output(['trac-admin', ENV, 'repository', 'resync', '']) - subprocess.check_output(['trac-admin', ENV, 'repository', 'resync', 'alt']) - subprocess.check_output(['trac-admin', ENV, 'repository', 'resync', 'nogh']) + subprocess.check_output([TRAC_ADMIN_BIN, ENV, 'repository', 'resync', '']) + subprocess.check_output([TRAC_ADMIN_BIN, ENV, 'repository', 'resync', 'alt']) + subprocess.check_output([TRAC_ADMIN_BIN, ENV, 'repository', 'resync', 'nogh']) @classmethod def removeTracEnvironment(cls): @@ -200,12 +204,11 @@ def removeTracEnvironment(cls): @classmethod def startTracd(cls, **kwargs): if COVERAGE: - tracd = ['coverage', 'run', '--append', '--branch', - '--source=tracext.github', - subprocess.check_output(['which', 'tracd']).strip()] + tracd = [COVERAGE_BIN, 'run', '--append', '--branch', + '--source=tracext.github', TRACD_BIN] else: - tracd = ['tracd'] + tracd = [TRACD_BIN] if SHOW_LOG: kwargs['stdout'] = sys.stdout kwargs['stderr'] = sys.stderr @@ -295,7 +298,7 @@ def testAlternateLinkToChangeset(self): def testNonGitHubLinkToChangeset(self): changeset = self.makeGitCommit(NOGHGIT, 'myfile', 'for browser tests') - subprocess.check_output(['trac-admin', ENV, 'changeset', 'added', 'nogh', changeset]) + subprocess.check_output([TRAC_ADMIN_BIN, ENV, 'changeset', 'added', 'nogh', changeset]) response = requests.get(URL + '/changeset/' + changeset + '/nogh', allow_redirects=False) self.assertEqual(response.status_code, 200) @@ -325,7 +328,7 @@ def testAlternateLinkToPath(self): def testNonGitHubLinkToPath(self): changeset = self.makeGitCommit(NOGHGIT, 'myfile', 'for more browser tests') - subprocess.check_output(['trac-admin', ENV, 'changeset', 'added', 'nogh', changeset]) + subprocess.check_output([TRAC_ADMIN_BIN, ENV, 'changeset', 'added', 'nogh', changeset]) response = requests.get(URL + '/changeset/' + changeset + '/nogh/myfile', allow_redirects=False) self.assertEqual(response.status_code, 200) @@ -2126,6 +2129,7 @@ def get_parser(): parser = argparse.ArgumentParser("Run the test suite for trac-github") parser.add_argument('--with-coverage', action='store_true', help="Enable test coverage") parser.add_argument('--with-trac-log', action='store_true', help="Display logs of test trac instances") + parser.add_argument('--virtualenv', help="Path to the virtualenv where Trac is installed") return parser @@ -2136,7 +2140,13 @@ def get_parser(): sys.exit(1) options, unittest_argv = get_parser().parse_known_args() + COVERAGE = options.with_coverage SHOW_LOG = options.with_trac_log + if options.virtualenv: + TRAC_ADMIN_BIN = os.path.join(options.virtualenv, 'bin', TRAC_ADMIN_BIN) + TRACD_BIN = os.path.join(options.virtualenv, 'bin', TRACD_BIN) + COVERAGE_BIN = os.path.join(options.virtualenv, 'bin', COVERAGE_BIN) + unittest.main(argv=[sys.argv[0]] + unittest_argv, exit=True) From 1bec077391ca5df606895be3a00746283a38abed Mon Sep 17 00:00:00 2001 From: Baptiste Mispelon Date: Sat, 27 Jan 2024 14:00:36 +0100 Subject: [PATCH 3/9] [runtests] Add new argument to customize default git branch This helps running the tests on machines where git is configured with a different defaut branch name. --- runtests.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/runtests.py b/runtests.py index 085b33b..bb6bb58 100755 --- a/runtests.py +++ b/runtests.py @@ -55,6 +55,7 @@ TRAC_ADMIN_BIN = 'trac-admin' TRACD_BIN = 'tracd' COVERAGE_BIN = 'coverage' +GIT_DEFAULT_BRANCH = 'main' class HttpNoRedirectHandler(urllib2.HTTPRedirectHandler): @@ -143,7 +144,7 @@ def createTracEnvironment(cls, **kwargs): conf.set('github', 'client_secret', client_secret) conf.set('github', 'repository', 'aaugustin/trac-github') conf.set('github', 'alt.repository', 'follower/trac-github') - conf.set('github', 'alt.branches', 'master stable/*') + conf.set('github', 'alt.branches', '%s stable/*' % GIT_DEFAULT_BRANCH) if 'request_email' in kwargs: conf.set('github', 'request_email', kwargs['request_email']) if 'preferred_email_domain' in kwargs: @@ -232,16 +233,19 @@ def makeGitBranch(repo, branch): subprocess.check_output(['git', '-C', repo, 'branch', branch]) @staticmethod - def makeGitCommit(repo, path, content, message='edit', branch='master'): - if branch != 'master': + def makeGitCommit(repo, path, content, message='edit', branch=None): + if branch is None: + branch = GIT_DEFAULT_BRANCH + + if branch != GIT_DEFAULT_BRANCH: subprocess.check_output(['git', '-C', repo, 'checkout', branch], stderr=subprocess.PIPE) with open(os.path.join(repo, path), 'wb') as fp: fp.write(content) subprocess.check_output(['git', '-C', repo, 'add', path]) subprocess.check_output(['git', '-C', repo, 'commit', '-m', message]) - if branch != 'master': - subprocess.check_output(['git', '-C', repo, 'checkout', 'master'], + if branch != GIT_DEFAULT_BRANCH: + subprocess.check_output(['git', '-C', repo, 'checkout', GIT_DEFAULT_BRANCH], stderr=subprocess.PIPE) changeset = subprocess.check_output(['git', '-C', repo, 'rev-parse', 'HEAD']) return changeset.strip() @@ -2130,6 +2134,7 @@ def get_parser(): parser.add_argument('--with-coverage', action='store_true', help="Enable test coverage") parser.add_argument('--with-trac-log', action='store_true', help="Display logs of test trac instances") parser.add_argument('--virtualenv', help="Path to the virtualenv where Trac is installed") + parser.add_argument('--git-default-branch', default="main", help="The default branch used in the test repositories") return parser @@ -2143,6 +2148,7 @@ def get_parser(): COVERAGE = options.with_coverage SHOW_LOG = options.with_trac_log + GIT_DEFAULT_BRANCH = options.git_default_branch if options.virtualenv: TRAC_ADMIN_BIN = os.path.join(options.virtualenv, 'bin', TRAC_ADMIN_BIN) From 3c9a827b4fcce46d8d6067cb55976c8df533d952 Mon Sep 17 00:00:00 2001 From: Baptiste Mispelon Date: Sat, 27 Jan 2024 14:33:56 +0100 Subject: [PATCH 4/9] [runtests] Stop using subprocess.check_output(..., stderr=PIPE) The Python documentation says not to do that, and the pipe doesn't seem to be used here anyway. --- runtests.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/runtests.py b/runtests.py index bb6bb58..e532aab 100755 --- a/runtests.py +++ b/runtests.py @@ -238,15 +238,13 @@ def makeGitCommit(repo, path, content, message='edit', branch=None): branch = GIT_DEFAULT_BRANCH if branch != GIT_DEFAULT_BRANCH: - subprocess.check_output(['git', '-C', repo, 'checkout', branch], - stderr=subprocess.PIPE) + subprocess.check_output(['git', '-C', repo, 'checkout', branch]) with open(os.path.join(repo, path), 'wb') as fp: fp.write(content) subprocess.check_output(['git', '-C', repo, 'add', path]) subprocess.check_output(['git', '-C', repo, 'commit', '-m', message]) if branch != GIT_DEFAULT_BRANCH: - subprocess.check_output(['git', '-C', repo, 'checkout', GIT_DEFAULT_BRANCH], - stderr=subprocess.PIPE) + subprocess.check_output(['git', '-C', repo, 'checkout', GIT_DEFAULT_BRANCH]) changeset = subprocess.check_output(['git', '-C', repo, 'rev-parse', 'HEAD']) return changeset.strip() From 9f28640d130b7b757088565b4c48e509ebf19c49 Mon Sep 17 00:00:00 2001 From: Baptiste Mispelon Date: Sat, 27 Jan 2024 15:38:50 +0100 Subject: [PATCH 5/9] [runtests] Centralize interaction with git repos into one function This makes the code a tiny bit more readable, and could make mocking/stubbing the actual git respository easier down the line. --- runtests.py | 48 +++++++++++++++++++++++++++++++++++------------- 1 file changed, 35 insertions(+), 13 deletions(-) diff --git a/runtests.py b/runtests.py index e532aab..1bfdfde 100755 --- a/runtests.py +++ b/runtests.py @@ -66,6 +66,22 @@ def redirect_request(self, req, fp, code, msg, headers, newurl): urllib2.install_opener(urllib2.build_opener(HttpNoRedirectHandler())) +def git_check_output(*args, **kwargs): + """ + Run the given git command (`*args`), optionally on the given + repository (`kwargs['repo']`), return the output of that command + as a string. + """ + repo = kwargs.pop('repo', None) + + if repo is None: + cmdargs = ["git"] + list(args) + else: + cmdargs = ["git", "-C", repo] + list(args) + + return subprocess.check_output(cmdargs, **kwargs) + + class TracGitHubTests(unittest.TestCase): cached_git = False @@ -86,14 +102,14 @@ def tearDownClass(cls): @classmethod def createGitRepositories(cls): - subprocess.check_output(['git', 'init', GIT]) - subprocess.check_output(['git', 'init', ALTGIT]) - subprocess.check_output(['git', 'init', NOGHGIT]) + git_check_output('init', GIT) + git_check_output('init', ALTGIT) + git_check_output('init', NOGHGIT) cls.makeGitCommit(GIT, 'README', 'default git repository\n') cls.makeGitCommit(ALTGIT, 'README', 'alternative git repository\n') cls.makeGitCommit(NOGHGIT, 'README', 'git repository not on GitHub\n') - subprocess.check_output(['git', 'clone', '--quiet', '--mirror', GIT, '%s-mirror' % GIT]) - subprocess.check_output(['git', 'clone', '--quiet', '--mirror', ALTGIT, '%s-mirror' % ALTGIT]) + git_check_output('clone', '--quiet', '--mirror', GIT, '%s-mirror' % GIT) + git_check_output('clone', '--quiet', '--mirror', ALTGIT, '%s-mirror' % ALTGIT) @classmethod def removeGitRepositories(cls): @@ -230,7 +246,7 @@ def stopTracd(cls): @staticmethod def makeGitBranch(repo, branch): - subprocess.check_output(['git', '-C', repo, 'branch', branch]) + git_check_output('branch', branch, repo=repo) @staticmethod def makeGitCommit(repo, path, content, message='edit', branch=None): @@ -238,14 +254,14 @@ def makeGitCommit(repo, path, content, message='edit', branch=None): branch = GIT_DEFAULT_BRANCH if branch != GIT_DEFAULT_BRANCH: - subprocess.check_output(['git', '-C', repo, 'checkout', branch]) + git_check_output('checkout', branch, repo=repo) with open(os.path.join(repo, path), 'wb') as fp: fp.write(content) - subprocess.check_output(['git', '-C', repo, 'add', path]) - subprocess.check_output(['git', '-C', repo, 'commit', '-m', message]) + git_check_output('add', path, repo=repo) + git_check_output('commit', '-m', message, repo=repo) if branch != GIT_DEFAULT_BRANCH: - subprocess.check_output(['git', '-C', repo, 'checkout', GIT_DEFAULT_BRANCH]) - changeset = subprocess.check_output(['git', '-C', repo, 'rev-parse', 'HEAD']) + git_check_output('checkout', GIT_DEFAULT_BRANCH, repo=repo) + changeset = git_check_output('rev-parse', 'HEAD', repo=repo) return changeset.strip() @staticmethod @@ -255,8 +271,14 @@ def makeGitHubHookPayload(n=1, reponame=''): repo = {'': GIT, 'alt': ALTGIT}[reponame] commits = [] - log = subprocess.check_output(['git', '--git-dir=%s/.git' % repo, 'log', - '-%d' % n, '--branches', '--format=oneline', '--topo-order']) + log = git_check_output( + 'log', + '-%d' % n, + '--branches', + '--format=oneline', + '--topo-order', + repo=repo + ) for line in log.splitlines(): id, _, message = line.partition(' ') commits.append({'id': id, 'message': message, 'distinct': True}) From bd7a47ccf8c74f4e3ebdb0888ac2808782430d5d Mon Sep 17 00:00:00 2001 From: Baptiste Mispelon Date: Sat, 27 Jan 2024 17:03:27 +0100 Subject: [PATCH 6/9] [runtests] Use a temporary directory instead of the current one --- runtests.py | 105 ++++++++++++++++++++++++++++++---------------------- 1 file changed, 60 insertions(+), 45 deletions(-) diff --git a/runtests.py b/runtests.py index 1bfdfde..968f357 100755 --- a/runtests.py +++ b/runtests.py @@ -12,7 +12,6 @@ import argparse import BaseHTTPServer import ConfigParser -import glob import json import os import random @@ -21,6 +20,7 @@ import signal import subprocess import sys +import tempfile import threading import time import traceback @@ -40,6 +40,7 @@ GIT = 'test-git-foo' ALTGIT = 'test-git-bar' NOGHGIT = 'test-git-nogithub' +TESTDIR = '.' ENV = 'test-trac-github' CONF = '%s/conf/trac.ini' % ENV @@ -66,6 +67,14 @@ def redirect_request(self, req, fp, code, msg, headers, newurl): urllib2.install_opener(urllib2.build_opener(HttpNoRedirectHandler())) +def d(*args): + """ + Return an absolute path where the given arguments are joined and + prepended with the TESTDIR. + """ + return os.path.join(TESTDIR, *args) + + def git_check_output(*args, **kwargs): """ Run the given git command (`*args`), optionally on the given @@ -77,7 +86,7 @@ def git_check_output(*args, **kwargs): if repo is None: cmdargs = ["git"] + list(args) else: - cmdargs = ["git", "-C", repo] + list(args) + cmdargs = ["git", "-C", d(repo)] + list(args) return subprocess.check_output(cmdargs, **kwargs) @@ -91,7 +100,7 @@ def setUpClass(cls): cls.createGitRepositories() cls.createTracEnvironment() cls.startTracd() - cls.env = Environment(ENV) + cls.env = Environment(d(ENV)) @classmethod def tearDownClass(cls): @@ -102,32 +111,32 @@ def tearDownClass(cls): @classmethod def createGitRepositories(cls): - git_check_output('init', GIT) - git_check_output('init', ALTGIT) - git_check_output('init', NOGHGIT) + git_check_output('init', d(GIT)) + git_check_output('init', d(ALTGIT)) + git_check_output('init', d(NOGHGIT)) cls.makeGitCommit(GIT, 'README', 'default git repository\n') cls.makeGitCommit(ALTGIT, 'README', 'alternative git repository\n') cls.makeGitCommit(NOGHGIT, 'README', 'git repository not on GitHub\n') - git_check_output('clone', '--quiet', '--mirror', GIT, '%s-mirror' % GIT) - git_check_output('clone', '--quiet', '--mirror', ALTGIT, '%s-mirror' % ALTGIT) + git_check_output('clone', '--quiet', '--mirror', d(GIT), d('%s-mirror' % GIT)) + git_check_output('clone', '--quiet', '--mirror', d(ALTGIT), d('%s-mirror' % ALTGIT)) @classmethod def removeGitRepositories(cls): - shutil.rmtree(GIT) - shutil.rmtree(ALTGIT) - shutil.rmtree(NOGHGIT) - shutil.rmtree('%s-mirror' % GIT) - shutil.rmtree('%s-mirror' % ALTGIT) + shutil.rmtree(d(GIT)) + shutil.rmtree(d(ALTGIT)) + shutil.rmtree(d(NOGHGIT)) + shutil.rmtree(d('%s-mirror' % GIT)) + shutil.rmtree(d('%s-mirror' % ALTGIT)) @classmethod def createTracEnvironment(cls, **kwargs): - subprocess.check_output([TRAC_ADMIN_BIN, ENV, 'initenv', + subprocess.check_output([TRAC_ADMIN_BIN, d(ENV), 'initenv', 'Trac - GitHub tests', 'sqlite:db/trac.db']) - subprocess.check_output([TRAC_ADMIN_BIN, ENV, 'permission', + subprocess.check_output([TRAC_ADMIN_BIN, d(ENV), 'permission', 'add', 'anonymous', 'TRAC_ADMIN']) conf = ConfigParser.ConfigParser() - with open(CONF, 'rb') as fp: + with open(d(CONF), 'rb') as fp: conf.readfp(fp) conf.add_section('components') @@ -185,11 +194,11 @@ def createTracEnvironment(cls, **kwargs): conf.set('logging', 'log_level', 'DEBUG') conf.add_section('repositories') - conf.set('repositories', '.dir', os.path.realpath('%s-mirror' % GIT)) + conf.set('repositories', '.dir', d('%s-mirror' % GIT)) conf.set('repositories', '.type', 'git') - conf.set('repositories', 'alt.dir', os.path.realpath('%s-mirror' % ALTGIT)) + conf.set('repositories', 'alt.dir', d('%s-mirror' % ALTGIT)) conf.set('repositories', 'alt.type', 'git') - conf.set('repositories', 'nogh.dir', os.path.realpath('%s/.git' % NOGHGIT)) + conf.set('repositories', 'nogh.dir', d(NOGHGIT, '.git')) conf.set('repositories', 'nogh.type', 'git') # Show changed files in timeline, which will trigger the @@ -200,23 +209,23 @@ def createTracEnvironment(cls, **kwargs): conf.set('trac', 'permission_policies', 'GitHubPolicy, %s' % old_permission_policies) - with open(CONF, 'wb') as fp: + with open(d(CONF), 'wb') as fp: conf.write(fp) - with open(HTDIGEST, 'w') as fp: + with open(d(HTDIGEST), 'w') as fp: # user: user, pass: pass, realm: realm fp.write("user:realm:8493fbc53ba582fb4c044c456bdc40eb\n") run_resync = kwargs['resync'] if 'resync' in kwargs else True if run_resync: # Allow skipping resync for perfomance reasons if not required - subprocess.check_output([TRAC_ADMIN_BIN, ENV, 'repository', 'resync', '']) - subprocess.check_output([TRAC_ADMIN_BIN, ENV, 'repository', 'resync', 'alt']) - subprocess.check_output([TRAC_ADMIN_BIN, ENV, 'repository', 'resync', 'nogh']) + subprocess.check_output([TRAC_ADMIN_BIN, d(ENV), 'repository', 'resync', '']) + subprocess.check_output([TRAC_ADMIN_BIN, d(ENV), 'repository', 'resync', 'alt']) + subprocess.check_output([TRAC_ADMIN_BIN, d(ENV), 'repository', 'resync', 'nogh']) @classmethod def removeTracEnvironment(cls): - shutil.rmtree(ENV) + shutil.rmtree(d(ENV)) @classmethod def startTracd(cls, **kwargs): @@ -229,7 +238,7 @@ def startTracd(cls, **kwargs): if SHOW_LOG: kwargs['stdout'] = sys.stdout kwargs['stderr'] = sys.stderr - cls.tracd = subprocess.Popen(tracd + ['--port', '8765', '--auth=*,%s,realm' % HTDIGEST, ENV], **kwargs) + cls.tracd = subprocess.Popen(tracd + ['--port', '8765', '--auth=*,%s,realm' % d(HTDIGEST), d(ENV)], **kwargs) while True: try: @@ -255,7 +264,7 @@ def makeGitCommit(repo, path, content, message='edit', branch=None): if branch != GIT_DEFAULT_BRANCH: git_check_output('checkout', branch, repo=repo) - with open(os.path.join(repo, path), 'wb') as fp: + with open(d(repo, path), 'wb') as fp: fp.write(content) git_check_output('add', path, repo=repo) git_check_output('commit', '-m', message, repo=repo) @@ -322,7 +331,7 @@ def testAlternateLinkToChangeset(self): def testNonGitHubLinkToChangeset(self): changeset = self.makeGitCommit(NOGHGIT, 'myfile', 'for browser tests') - subprocess.check_output([TRAC_ADMIN_BIN, ENV, 'changeset', 'added', 'nogh', changeset]) + subprocess.check_output([TRAC_ADMIN_BIN, d(ENV), 'changeset', 'added', 'nogh', changeset]) response = requests.get(URL + '/changeset/' + changeset + '/nogh', allow_redirects=False) self.assertEqual(response.status_code, 200) @@ -352,7 +361,7 @@ def testAlternateLinkToPath(self): def testNonGitHubLinkToPath(self): changeset = self.makeGitCommit(NOGHGIT, 'myfile', 'for more browser tests') - subprocess.check_output([TRAC_ADMIN_BIN, ENV, 'changeset', 'added', 'nogh', changeset]) + subprocess.check_output([TRAC_ADMIN_BIN, d(ENV), 'changeset', 'added', 'nogh', changeset]) response = requests.get(URL + '/changeset/' + changeset + '/nogh/myfile', allow_redirects=False) self.assertEqual(response.status_code, 200) @@ -482,14 +491,14 @@ def setUpClass(cls): cls.trac_env_broken = trac_env_broken cls.trac_env_broken_api = trac_env_broken_api - with open(SECRET, 'wb') as fp: + with open(d(SECRET), 'wb') as fp: fp.write('98765432109876543210') @classmethod def tearDownClass(cls): cls.removeGitRepositories() - os.remove(SECRET) + os.remove(d(SECRET)) def testLoginWithReqEmail(self): """Test that configuring request_email = true requests the user:email scope from GitHub""" @@ -545,7 +554,7 @@ def testLoginWithSecretInEnvironment(self): def testLoginWithSecretInFile(self): """Test that passing client_id in absolute path works""" - path = os.path.join(os.getcwd(), SECRET) + path = d(SECRET) with TracContext(self, client_id=path): self.loginAndVerifyClientId('98765432109876543210') @@ -553,7 +562,7 @@ def testLoginWithSecretInFile(self): def testLoginWithSecretInRelativeFile(self): """Test that passing client_id in relative path works""" - path = os.path.join('.', SECRET) + path = './' + os.path.relpath(d(SECRET)) with TracContext(self, client_id=path): self.loginAndVerifyClientId('98765432109876543210') @@ -1069,7 +1078,7 @@ def setUpClass(cls): cls.createGitRepositories() cls.createTracEnvironment(webhook_secret='6c12713595df9247974fa0f2f99b94c815f242035c49c7f009892bfd7d9f0f98') cls.startTracd() - cls.env = Environment(ENV) + cls.env = Environment(d(ENV)) def testUnsignedPing(self): payload = {'zen': "Readability counts."} @@ -1095,19 +1104,19 @@ class GitHubPostCommitHookWithUpdateHookTests(TracGitHubTests): @classmethod def createUpdateHook(cls): - with open(UPDATEHOOK, 'wb') as fp: + with open(d(UPDATEHOOK), 'wb') as fp: # simple shell script to echo back all input fp.write("""#!/bin/sh\nexec cat""") os.fchmod(fp.fileno(), 0o755) def createFailingUpdateHook(cls): - with open(UPDATEHOOK, 'wb') as fp: + with open(d(UPDATEHOOK), 'wb') as fp: fp.write("""#!/bin/sh\nexit 1""") os.fchmod(fp.fileno(), 0o755) @classmethod def removeUpdateHook(cls): - os.remove(UPDATEHOOK) + os.remove(d(UPDATEHOOK)) @classmethod def setUpClass(cls): @@ -1131,7 +1140,7 @@ def testUpdateHook(self): self.assertEqual(output.split('\n')[-1], json.dumps(payload)) def testUpdateHookExecFailure(self): - os.chmod(UPDATEHOOK, 0o644) + os.chmod(d(UPDATEHOOK), 0o644) self.makeGitCommit(GIT, 'bar', 'bar content\n') payload = self.makeGitHubHookPayload() with self.assertRaisesRegexp(urllib2.HTTPError, r'^HTTP Error 500: Internal Server Error$'): @@ -1328,7 +1337,7 @@ def __enter__(self): if hasattr(self, attr): kwargs[attr] = getattr(self, attr) self._testobj.createTracEnvironment(**kwargs) - self._tracenv = Environment(ENV) + self._tracenv = Environment(d(ENV)) # Start tracd self._testobj.startTracd(env=self._env) @@ -2159,11 +2168,6 @@ def get_parser(): if __name__ == '__main__': - if glob.glob('test-*'): - print "Test data remains from previous runs, aborting." - print "Run `rm -rf test-*` and retry." - sys.exit(1) - options, unittest_argv = get_parser().parse_known_args() COVERAGE = options.with_coverage @@ -2175,4 +2179,15 @@ def get_parser(): TRACD_BIN = os.path.join(options.virtualenv, 'bin', TRACD_BIN) COVERAGE_BIN = os.path.join(options.virtualenv, 'bin', COVERAGE_BIN) - unittest.main(argv=[sys.argv[0]] + unittest_argv, exit=True) + TESTDIR = tempfile.mkdtemp(prefix='trac-github-test-') + print "Starting tests using temporary directory %r" % TESTDIR + + try: + test_program = unittest.main(argv=[sys.argv[0]] + unittest_argv, exit=False) + finally: + shutil.rmtree(TESTDIR) + + if not test_program.result.wasSuccessful(): + sys.exit(1) + else: + sys.exit(0) From a0e135731d6717b371be36ebf49ed097201c479e Mon Sep 17 00:00:00 2001 From: Baptiste Mispelon Date: Sat, 27 Jan 2024 18:50:27 +0100 Subject: [PATCH 7/9] [runtests] Make sure repos contain a hooks directory Some git configurations (like mine, as it turns out) don't include that directory when using git init. --- runtests.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/runtests.py b/runtests.py index 968f357..fec7b28 100755 --- a/runtests.py +++ b/runtests.py @@ -1121,6 +1121,11 @@ def removeUpdateHook(cls): @classmethod def setUpClass(cls): super(GitHubPostCommitHookWithUpdateHookTests, cls).setUpClass() + # Make sure the hooks directory exists in the repo (can be disabled in some git configs) + try: + os.mkdir(d('%s-mirror' % GIT, 'hooks')) + except OSError: + pass cls.createUpdateHook() @classmethod From c4d181277e1dc2bb8216bc39069716025a7e1f45 Mon Sep 17 00:00:00 2001 From: Baptiste Mispelon Date: Sat, 27 Jan 2024 19:03:09 +0100 Subject: [PATCH 8/9] [runtests] Error out early if tracd is non-responsive --- runtests.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/runtests.py b/runtests.py index fec7b28..697e411 100755 --- a/runtests.py +++ b/runtests.py @@ -240,13 +240,17 @@ def startTracd(cls, **kwargs): kwargs['stderr'] = sys.stderr cls.tracd = subprocess.Popen(tracd + ['--port', '8765', '--auth=*,%s,realm' % d(HTDIGEST), d(ENV)], **kwargs) - while True: + waittime = 0.1 + for _ in range(5): try: urllib2.urlopen(URL) except urllib2.URLError: - time.sleep(0.1) + time.sleep(waittime) + waittime *= 2 else: break + else: + raise RuntimeError("Can't communicate with tracd running on port 8765") @classmethod def stopTracd(cls): From a887dfe0aa99244f2d6df860b007ec034d6def31 Mon Sep 17 00:00:00 2001 From: Baptiste Mispelon Date: Sat, 27 Jan 2024 20:00:41 +0100 Subject: [PATCH 9/9] Add CI with github actions and nox --- .github/workflows/runtests.yml | 27 +++++++++++++++++++++++++++ .travis.yml | 12 ------------ noxfile.py | 16 ++++++++++++++++ requirements_test.txt | 5 +++++ runtests.py | 1 + 5 files changed, 49 insertions(+), 12 deletions(-) create mode 100644 .github/workflows/runtests.yml delete mode 100644 .travis.yml create mode 100644 noxfile.py create mode 100644 requirements_test.txt diff --git a/.github/workflows/runtests.yml b/.github/workflows/runtests.yml new file mode 100644 index 0000000..8a7446f --- /dev/null +++ b/.github/workflows/runtests.yml @@ -0,0 +1,27 @@ +name: runtests +run-name: Run test suite for trac-github +on: [pull_request] +jobs: + runtests-py2: + runs-on: ubuntu-20.04 + container: + image: python:2.7.18-buster + + steps: + - uses: actions/checkout@v4 + - run: pip install nox-py2 + - run: git config --global user.name runtest + - run: git config --global user.email runtest@localhost + - run: nox --non-interactive --error-on-missing-interpreter --session runtests -- --git-default-branch=master + +# runtests-py3: +# runs-on: ubuntu-latest +# steps: +# - uses: wntrblm/nox@2022.8.7 +# with: +# python-versions: "3.7" +# - uses: actions/checkout@v4 +# - run: git config --global user.name runtest +# - run: git config --global user.email runtest@localhost +# - run: git config --global init.defaultBranch main +# - run: nox --non-interactive --error-on-missing-interpreter --session runtests diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 6a2961f..0000000 --- a/.travis.yml +++ /dev/null @@ -1,12 +0,0 @@ -language: python -python: - - "2.7" -install: - - pip install coverage lxml requests requests-oauthlib python-coveralls Trac - - pip install -e . - - pip freeze -script: - - coverage erase - - ./runtests.py --with-coverage --with-trac-log -after_success: - - coveralls diff --git a/noxfile.py b/noxfile.py new file mode 100644 index 0000000..d6156fb --- /dev/null +++ b/noxfile.py @@ -0,0 +1,16 @@ +import sys + +import nox + +if sys.version_info.major == 2: + TRAC_VERSIONS = ["1.4.4", "1.2.6"] +else: + TRAC_VERSIONS = ["1.6"] + + +@nox.session +@nox.parametrize("trac", TRAC_VERSIONS) +def runtests(session, trac): + session.install("-r", "requirements_test.txt") + session.install("Trac==%s" % trac) + session.run("python", "runtests.py", *session.posargs) diff --git a/requirements_test.txt b/requirements_test.txt new file mode 100644 index 0000000..4239008 --- /dev/null +++ b/requirements_test.txt @@ -0,0 +1,5 @@ +-e . +requests-oauthlib==1.3.1 +lxml==5.0.1 +# Obviously Trac is also needed, but because we want to test several versions +# then we install it manually diff --git a/runtests.py b/runtests.py index 697e411..0c35226 100755 --- a/runtests.py +++ b/runtests.py @@ -2190,6 +2190,7 @@ def get_parser(): TESTDIR = tempfile.mkdtemp(prefix='trac-github-test-') print "Starting tests using temporary directory %r" % TESTDIR + print "Using git version %s" % git_check_output('--version').strip() try: test_program = unittest.main(argv=[sys.argv[0]] + unittest_argv, exit=False)