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

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