Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(201)

Side by Side Diff: testing_support/gerrit_test_case.py

Issue 26399002: Add git/gerrit-on-borg utilities. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/tools/depot_tools
Patch Set: Simplify gerrit_util import Created 7 years, 2 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « testing_support/gerrit-init.sh ('k') | no next file » | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
1 # Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
2 # Use of this source code is governed by a BSD-style license that can be
3 # found in the LICENSE file.
4
5 """Test framework for code that interacts with gerrit.
6
7 class GerritTestCase
8 --------------------------------------------------------------------------------
9 This class initializes and runs an a gerrit instance on localhost. To use the
10 framework, define a class that extends GerritTestCase, and then do standard
11 python unittest development as described here:
12
13 http://docs.python.org/2.7/library/unittest.html#basic-example
14
15 When your test code runs, the framework will:
16
17 - Download the latest stable(-ish) binary release of the gerrit code.
18 - Start up a live gerrit instance running in a temp directory on the localhost.
19 - Set up a single gerrit user account with admin priveleges.
20 - Supply credential helpers for interacting with the gerrit instance via http
21 or ssh.
22
23 Refer to depot_tools/testing_support/gerrit-init.sh for details about how the
24 gerrit instance is set up, and refer to helper methods defined below
25 (createProject, cloneProject, uploadChange, etc.) for ways to interact with the
26 gerrit instance from your test methods.
27
28
29 class RepoTestCase
30 --------------------------------------------------------------------------------
31 This class extends GerritTestCase, and creates a set of project repositories
32 and a manifest repository that can be used in conjunction with the 'repo' tool.
33
34 Each test method will initialize and sync a brand-new repo working directory.
35 The 'repo' command may be invoked in a subprocess as part of your tests.
36
37 One gotcha: 'repo upload' will always attempt to use the ssh interface to talk
38 to gerrit.
39 """
40
41 import collections
42 from cStringIO import StringIO
43 import errno
44 import httplib
45 import imp
46 import json
47 import netrc
48 import os
49 import re
50 import shutil
51 import signal
52 import socket
53 import stat
54 import subprocess
55 import sys
56 import tempfile
57 import unittest
58 import urllib
59
60 import gerrit_util
61
62
63 DEPOT_TOOLS_DIR = os.path.normpath(os.path.join(
64 os.path.realpath(__file__), '..', '..'))
65
66
67 # When debugging test code, it's sometimes helpful to leave the test gerrit
68 # instance intact and running after the test code exits. Setting TEARDOWN
69 # to False will do that.
70 TEARDOWN = True
71
72 class GerritTestCase(unittest.TestCase):
73 """Test class for tests that interact with a gerrit server.
74
75 The class setup creates and launches a stand-alone gerrit instance running on
76 localhost, for test methods to interact with. Class teardown stops and
77 deletes the gerrit instance.
78
79 Note that there is a single gerrit instance for ALL test methods in a
80 GerritTestCase sub-class.
81 """
82
83 COMMIT_RE = re.compile(r'^commit ([0-9a-fA-F]{40})$')
84 CHANGEID_RE = re.compile('^\s+Change-Id:\s*(\S+)$')
85 DEVNULL = open(os.devnull, 'w')
86 TEST_USERNAME = 'test-username'
87 TEST_EMAIL = 'test-username@test.org'
88
89 GerritInstance = collections.namedtuple('GerritInstance', [
90 'credential_file',
91 'gerrit_dir',
92 'gerrit_exe',
93 'gerrit_host',
94 'gerrit_pid',
95 'gerrit_url',
96 'git_dir',
97 'git_host',
98 'git_url',
99 'http_port',
100 'netrc_file',
101 'ssh_ident',
102 'ssh_port',
103 ])
104
105 @classmethod
106 def check_call(cls, *args, **kwargs):
107 kwargs.setdefault('stdout', cls.DEVNULL)
108 kwargs.setdefault('stderr', cls.DEVNULL)
109 subprocess.check_call(*args, **kwargs)
110
111 @classmethod
112 def check_output(cls, *args, **kwargs):
113 kwargs.setdefault('stderr', cls.DEVNULL)
114 return subprocess.check_output(*args, **kwargs)
115
116 @classmethod
117 def _create_gerrit_instance(cls, gerrit_dir):
118 gerrit_init_script = os.path.join(
119 DEPOT_TOOLS_DIR, 'testing_support', 'gerrit-init.sh')
120 http_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
121 http_sock.bind(('', 0))
122 http_port = str(http_sock.getsockname()[1])
123 ssh_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
124 ssh_sock.bind(('', 0))
125 ssh_port = str(ssh_sock.getsockname()[1])
126
127 # NOTE: this is not completely safe. These port numbers could be
128 # re-assigned by the OS between the calls to socket.close() and gerrit
129 # starting up. The only safe way to do this would be to pass file
130 # descriptors down to the gerrit process, which is not even remotely
131 # supported. Alas.
132 http_sock.close()
133 ssh_sock.close()
134 cls.check_call(['bash', gerrit_init_script, '--http-port', http_port,
135 '--ssh-port', ssh_port, gerrit_dir])
136
137 gerrit_exe = os.path.join(gerrit_dir, 'bin', 'gerrit.sh')
138 cls.check_call(['bash', gerrit_exe, 'start'])
139 with open(os.path.join(gerrit_dir, 'logs', 'gerrit.pid')) as fh:
140 gerrit_pid = int(fh.read().rstrip())
141
142 return cls.GerritInstance(
143 credential_file=os.path.join(gerrit_dir, 'tmp', '.git-credentials'),
144 gerrit_dir=gerrit_dir,
145 gerrit_exe=gerrit_exe,
146 gerrit_host='localhost:%s' % http_port,
147 gerrit_pid=gerrit_pid,
148 gerrit_url='http://localhost:%s' % http_port,
149 git_dir=os.path.join(gerrit_dir, 'git'),
150 git_host='%s/git' % gerrit_dir,
151 git_url='file://%s/git' % gerrit_dir,
152 http_port=http_port,
153 netrc_file=os.path.join(gerrit_dir, 'tmp', '.netrc'),
154 ssh_ident=os.path.join(gerrit_dir, 'tmp', 'id_rsa'),
155 ssh_port=ssh_port,)
156
157 @classmethod
158 def setUpClass(cls):
159 """Sets up the gerrit instances in a class-specific temp dir."""
160 # Create gerrit instance.
161 gerrit_dir = tempfile.mkdtemp()
162 os.chmod(gerrit_dir, 0700)
163 gi = cls.gerrit_instance = cls._create_gerrit_instance(gerrit_dir)
164
165 # Set netrc file for http authentication.
166 cls.gerrit_util_netrc_orig = gerrit_util.NETRC
167 gerrit_util.NETRC = netrc.netrc(gi.netrc_file)
168
169 # gerrit_util.py defaults to using https, but for testing, it's much
170 # simpler to use http connections.
171 cls.gerrit_util_protocol_orig = gerrit_util.GERRIT_PROTOCOL
172 gerrit_util.GERRIT_PROTOCOL = 'http'
173
174 # Because we communicate with the test server via http, rather than https,
175 # libcurl won't add authentication headers to raw git requests unless the
176 # gerrit server returns 401. That works for pushes, but for read operations
177 # (like git-ls-remote), gerrit will simply omit any ref that requires
178 # authentication. By default gerrit doesn't permit anonymous read access to
179 # refs/meta/config. Override that behavior so tests can access
180 # refs/meta/config if necessary.
181 clone_path = os.path.join(gi.gerrit_dir, 'tmp', 'All-Projects')
182 cls._CloneProject('All-Projects', clone_path)
183 project_config = os.path.join(clone_path, 'project.config')
184 cls.check_call(['git', 'config', '--file', project_config, '--add',
185 'access.refs/meta/config.read', 'group Anonymous Users'])
186 cls.check_call(['git', 'add', project_config], cwd=clone_path)
187 cls.check_call(
188 ['git', 'commit', '-m', 'Anonyous read for refs/meta/config'],
189 cwd=clone_path)
190 cls.check_call(['git', 'push', 'origin', 'HEAD:refs/meta/config'],
191 cwd=clone_path)
192
193 def setUp(self):
194 self.tempdir = tempfile.mkdtemp()
195 os.chmod(self.tempdir, 0700)
196
197 def tearDown(self):
198 if TEARDOWN:
199 shutil.rmtree(self.tempdir)
200
201 @classmethod
202 def createProject(cls, name, description='Test project', owners=None,
203 submit_type='CHERRY_PICK'):
204 """Create a project on the test gerrit server."""
205 if owners is None:
206 owners = ['Administrators']
207 body = {
208 'description': description,
209 'submit_type': submit_type,
210 'owners': owners,
211 }
212 path = 'projects/%s' % urllib.quote(name, '')
213 conn = gerrit_util.CreateHttpConn(
214 cls.gerrit_instance.gerrit_host, path, reqtype='PUT', body=body)
215 jmsg = gerrit_util.ReadHttpJsonResponse(conn, expect_status=201)
216 assert jmsg['name'] == name
217
218 @classmethod
219 def _post_clone_bookkeeping(cls, clone_path):
220 config_path = os.path.join(clone_path, '.git', 'config')
221 cls.check_call(
222 ['git', 'config', '--file', config_path, 'user.email', cls.TEST_EMAIL])
223 cls.check_call(
224 ['git', 'config', '--file', config_path, 'credential.helper',
225 'store --file=%s' % cls.gerrit_instance.credential_file])
226
227 @classmethod
228 def _CloneProject(cls, name, path):
229 """Clone a project from the test gerrit server."""
230 gi = cls.gerrit_instance
231 parent_dir = os.path.dirname(path)
232 if not os.path.exists(parent_dir):
233 os.makedirs(parent_dir)
234 url = '/'.join((gi.gerrit_url, name))
235 cls.check_call(['git', 'clone', url, path])
236 cls._post_clone_bookkeeping(path)
237 # Install commit-msg hook to add Change-Id lines.
238 hook_path = os.path.join(path, '.git', 'hooks', 'commit-msg')
239 cls.check_call(['curl', '-o', hook_path,
240 '/'.join((gi.gerrit_url, 'tools/hooks/commit-msg'))])
241 os.chmod(hook_path, stat.S_IRWXU)
242 return path
243
244 def cloneProject(self, name, path=None):
245 """Clone a project from the test gerrit server."""
246 if path is None:
247 path = os.path.basename(name)
248 if path.endswith('.git'):
249 path = path[:-4]
250 path = os.path.join(self.tempdir, path)
251 return self._CloneProject(name, path)
252
253 @classmethod
254 def _CreateCommit(cls, clone_path, fn=None, msg=None, text=None):
255 """Create a commit in the given git checkout."""
256 if not fn:
257 fn = 'test-file.txt'
258 if not msg:
259 msg = 'Test Message'
260 if not text:
261 text = 'Another day, another dollar.'
262 fpath = os.path.join(clone_path, fn)
263 with open(fpath, 'a') as fh:
264 fh.write('%s\n' % text)
265 cls.check_call(['git', 'add', fn], cwd=clone_path)
266 cls.check_call(['git', 'commit', '-m', msg], cwd=clone_path)
267 return cls._GetCommit(clone_path)
268
269 def createCommit(self, clone_path, fn=None, msg=None, text=None):
270 """Create a commit in the given git checkout."""
271 clone_path = os.path.join(self.tempdir, clone_path)
272 return self._CreateCommit(clone_path, fn, msg, text)
273
274 @classmethod
275 def _GetCommit(cls, clone_path, ref='HEAD'):
276 """Get the sha1 and change-id for a ref in the git checkout."""
277 log_proc = cls.check_output(['git', 'log', '-n', '1', ref], cwd=clone_path)
278 sha1 = None
279 change_id = None
280 for line in log_proc.splitlines():
281 match = cls.COMMIT_RE.match(line)
282 if match:
283 sha1 = match.group(1)
284 continue
285 match = cls.CHANGEID_RE.match(line)
286 if match:
287 change_id = match.group(1)
288 continue
289 assert sha1
290 assert change_id
291 return (sha1, change_id)
292
293 def getCommit(self, clone_path, ref='HEAD'):
294 """Get the sha1 and change-id for a ref in the git checkout."""
295 clone_path = os.path.join(self.tempdir, clone_path)
296 return self._GetCommit(clone_path, ref)
297
298 @classmethod
299 def _UploadChange(cls, clone_path, branch='master', remote='origin'):
300 """Create a gerrit CL from the HEAD of a git checkout."""
301 cls.check_call(
302 ['git', 'push', remote, 'HEAD:refs/for/%s' % branch], cwd=clone_path)
303
304 def uploadChange(self, clone_path, branch='master', remote='origin'):
305 """Create a gerrit CL from the HEAD of a git checkout."""
306 clone_path = os.path.join(self.tempdir, clone_path)
307 self._UploadChange(clone_path, branch, remote)
308
309 @classmethod
310 def _PushBranch(cls, clone_path, branch='master'):
311 """Push a branch directly to gerrit, bypassing code review."""
312 cls.check_call(
313 ['git', 'push', 'origin', 'HEAD:refs/heads/%s' % branch],
314 cwd=clone_path)
315
316 def pushBranch(self, clone_path, branch='master'):
317 """Push a branch directly to gerrit, bypassing code review."""
318 clone_path = os.path.join(self.tempdir, clone_path)
319 self._PushBranch(clone_path, branch)
320
321 @classmethod
322 def createAccount(cls, name='Test User', email='test-user@test.org',
323 password=None, groups=None):
324 """Create a new user account on gerrit."""
325 username = email.partition('@')[0]
326 gerrit_cmd = 'gerrit create-account %s --full-name "%s" --email %s' % (
327 username, name, email)
328 if password:
329 gerrit_cmd += ' --http-password "%s"' % password
330 if groups:
331 gerrit_cmd += ' '.join(['--group %s' % x for x in groups])
332 ssh_cmd = ['ssh', '-p', cls.gerrit_instance.ssh_port,
333 '-i', cls.gerrit_instance.ssh_ident,
334 '-o', 'NoHostAuthenticationForLocalhost=yes',
335 '-o', 'StrictHostKeyChecking=no',
336 '%s@localhost' % cls.TEST_USERNAME, gerrit_cmd]
337 cls.check_call(ssh_cmd)
338
339 @classmethod
340 def _stop_gerrit(cls, gerrit_instance):
341 """Stops the running gerrit instance and deletes it."""
342 try:
343 # This should terminate the gerrit process.
344 cls.check_call(['bash', gerrit_instance.gerrit_exe, 'stop'])
345 finally:
346 try:
347 # cls.gerrit_pid should have already terminated. If it did, then
348 # os.waitpid will raise OSError.
349 os.waitpid(gerrit_instance.gerrit_pid, os.WNOHANG)
350 except OSError as e:
351 if e.errno == errno.ECHILD:
352 # If gerrit shut down cleanly, os.waitpid will land here.
353 # pylint: disable=W0150
354 return
355
356 # If we get here, the gerrit process is still alive. Send the process
357 # SIGKILL for good measure.
358 try:
359 os.kill(gerrit_instance.gerrit_pid, signal.SIGKILL)
360 except OSError:
361 if e.errno == errno.ESRCH:
362 # os.kill raised an error because the process doesn't exist. Maybe
363 # gerrit shut down cleanly after all.
364 # pylint: disable=W0150
365 return
366
367 # Announce that gerrit didn't shut down cleanly.
368 msg = 'Test gerrit server (pid=%d) did not shut down cleanly.' % (
369 gerrit_instance.gerrit_pid)
370 print >> sys.stderr, msg
371
372 @classmethod
373 def tearDownClass(cls):
374 gerrit_util.NETRC = cls.gerrit_util_netrc_orig
375 gerrit_util.GERRIT_PROTOCOL = cls.gerrit_util_protocol_orig
376 if TEARDOWN:
377 cls._stop_gerrit(cls.gerrit_instance)
378 shutil.rmtree(cls.gerrit_instance.gerrit_dir)
379
380
381 class RepoTestCase(GerritTestCase):
382 """Test class which runs in a repo checkout."""
383
384 REPO_URL = 'https://chromium.googlesource.com/external/repo'
385 MANIFEST_PROJECT = 'remotepath/manifest'
386 MANIFEST_TEMPLATE = """<?xml version="1.0" encoding="UTF-8"?>
387 <manifest>
388 <remote name="remote1"
389 fetch="%(gerrit_url)s"
390 review="%(gerrit_host)s" />
391 <remote name="remote2"
392 fetch="%(gerrit_url)s"
393 review="%(gerrit_host)s" />
394 <default revision="refs/heads/master" remote="remote1" sync-j="1" />
395 <project remote="remote1" path="localpath/testproj1" name="remotepath/testproj 1" />
396 <project remote="remote1" path="localpath/testproj2" name="remotepath/testproj 2" />
397 <project remote="remote2" path="localpath/testproj3" name="remotepath/testproj 3" />
398 <project remote="remote2" path="localpath/testproj4" name="remotepath/testproj 4" />
399 </manifest>
400 """
401
402 @classmethod
403 def setUpClass(cls):
404 GerritTestCase.setUpClass()
405 gi = cls.gerrit_instance
406
407 # Create local mirror of repo tool repository.
408 repo_mirror_path = os.path.join(gi.git_dir, 'repo.git')
409 cls.check_call(
410 ['git', 'clone', '--mirror', cls.REPO_URL, repo_mirror_path])
411
412 # Check out the top-level repo script; it will be used for invocation.
413 repo_clone_path = os.path.join(gi.gerrit_dir, 'tmp', 'repo')
414 cls.check_call(['git', 'clone', '-n', repo_mirror_path, repo_clone_path])
415 cls.check_call(
416 ['git', 'checkout', 'origin/stable', 'repo'], cwd=repo_clone_path)
417 shutil.rmtree(os.path.join(repo_clone_path, '.git'))
418 cls.repo_exe = os.path.join(repo_clone_path, 'repo')
419
420 # Create manifest repository.
421 cls.createProject(cls.MANIFEST_PROJECT)
422 clone_path = os.path.join(gi.gerrit_dir, 'tmp', 'manifest')
423 cls._CloneProject(cls.MANIFEST_PROJECT, clone_path)
424 manifest_path = os.path.join(clone_path, 'default.xml')
425 with open(manifest_path, 'w') as fh:
426 fh.write(cls.MANIFEST_TEMPLATE % gi.__dict__)
427 cls.check_call(['git', 'add', 'default.xml'], cwd=clone_path)
428 cls.check_call(['git', 'commit', '-m', 'Test manifest.'], cwd=clone_path)
429 cls._PushBranch(clone_path)
430
431 # Create project repositories.
432 for i in xrange(1, 5):
433 proj = 'testproj%d' % i
434 cls.createProject('remotepath/%s' % proj)
435 clone_path = os.path.join(gi.gerrit_dir, 'tmp', proj)
436 cls._CloneProject('remotepath/%s' % proj, clone_path)
437 cls._CreateCommit(clone_path)
438 cls._PushBranch(clone_path, 'master')
439
440 def setUp(self):
441 super(RepoTestCase, self).setUp()
442 manifest_url = '/'.join((self.gerrit_instance.gerrit_url,
443 self.MANIFEST_PROJECT))
444 repo_url = '/'.join((self.gerrit_instance.gerrit_url, 'repo'))
445 self.check_call(
446 [self.repo_exe, 'init', '-u', manifest_url, '--repo-url',
447 repo_url, '--no-repo-verify'], cwd=self.tempdir)
448 self.check_call([self.repo_exe, 'sync'], cwd=self.tempdir)
449 for i in xrange(1, 5):
450 clone_path = os.path.join(self.tempdir, 'localpath', 'testproj%d' % i)
451 self._post_clone_bookkeeping(clone_path)
452 # Tell 'repo upload' to upload this project without prompting.
453 config_path = os.path.join(clone_path, '.git', 'config')
454 self.check_call(
455 ['git', 'config', '--file', config_path, 'review.%s.upload' %
456 self.gerrit_instance.gerrit_host, 'true'])
457
458 @classmethod
459 def runRepo(cls, *args, **kwargs):
460 # Unfortunately, munging $HOME appears to be the only way to control the
461 # netrc file used by repo.
462 munged_home = os.path.join(cls.gerrit_instance.gerrit_dir, 'tmp')
463 if 'env' not in kwargs:
464 env = kwargs['env'] = os.environ.copy()
465 env['HOME'] = munged_home
466 else:
467 env.setdefault('HOME', munged_home)
468 args[0].insert(0, cls.repo_exe)
469 cls.check_call(*args, **kwargs)
470
471 def uploadChange(self, clone_path, branch='master', remote='origin'):
472 review_host = self.check_output(
473 ['git', 'config', 'remote.%s.review' % remote],
474 cwd=clone_path).strip()
475 assert(review_host)
476 projectname = self.check_output(
477 ['git', 'config', 'remote.%s.projectname' % remote],
478 cwd=clone_path).strip()
479 assert(projectname)
480 GerritTestCase._UploadChange(
481 clone_path, branch=branch, remote='%s://%s/%s' % (
482 gerrit_util.GERRIT_PROTOCOL, review_host, projectname))
OLDNEW
« no previous file with comments | « testing_support/gerrit-init.sh ('k') | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698