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

Side by Side Diff: tests/fake_repos.py

Issue 6627013: Correctly kill 'git daemon' child process, fixing a lot of testing issues. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/tools/depot_tools
Patch Set: Fixed race condition Created 9 years, 9 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 | « no previous file | tests/gclient_smoketest.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 #!/usr/bin/python 1 #!/usr/bin/python
2 # Copyright (c) 2010 The Chromium Authors. All rights reserved. 2 # Copyright (c) 2010 The Chromium Authors. All rights reserved.
3 # Use of this source code is governed by a BSD-style license that can be 3 # Use of this source code is governed by a BSD-style license that can be
4 # found in the LICENSE file. 4 # found in the LICENSE file.
5 5
6 """Generate fake repositories for testing.""" 6 """Generate fake repositories for testing."""
7 7
8 import atexit 8 import atexit
9 import datetime
9 import errno 10 import errno
10 import logging 11 import logging
11 import os 12 import os
12 import pprint 13 import pprint
13 import re 14 import re
15 import socket
14 import stat 16 import stat
15 import subprocess 17 import subprocess
16 import sys 18 import sys
19 import tempfile
17 import time 20 import time
18 import unittest 21 import unittest
19 22
20 sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 23 sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
21 24
22 import scm 25 import scm
23 26
24 ## Utility functions 27 ## Utility functions
25 28
26 29
27 def addKill(): 30 def kill_pid(pid):
28 """Add kill() method to subprocess.Popen for python <2.6""" 31 """Kills a process by its process id."""
29 if getattr(subprocess.Popen, 'kill', None): 32 try:
33 # Unable to import 'module'
34 # pylint: disable=F0401
35 import signal
36 return os.kill(pid, signal.SIGKILL)
37 except ImportError:
38 pass
39
40
41 def kill_win(process):
42 """Kills a process with its windows handle.
43
44 Has no effect on other platforms.
45 """
46 try:
47 # Unable to import 'module'
48 # pylint: disable=F0401
49 import win32process
50 # Access to a protected member _handle of a client class
51 # pylint: disable=W0212
52 return win32process.TerminateProcess(process._handle, -1)
53 except ImportError:
54 pass
55
56
57 def add_kill():
58 """Adds kill() method to subprocess.Popen for python <2.6"""
59 if hasattr(subprocess.Popen, 'kill'):
30 return 60 return
31 # Unable to import 'module' 61
32 # pylint: disable=F0401
33 if sys.platform == 'win32': 62 if sys.platform == 'win32':
34 def kill_win(process):
35 import win32process
36 # Access to a protected member _handle of a client class
37 # pylint: disable=W0212
38 return win32process.TerminateProcess(process._handle, -1)
39 subprocess.Popen.kill = kill_win 63 subprocess.Popen.kill = kill_win
40 else: 64 else:
41 def kill_nix(process): 65 def kill_nix(process):
42 import signal 66 return kill_pid(process.pid)
Dirk Pranke 2011/03/04 21:15:17 I'd likely just change kill_pid to take process as
M-A Ruel 2011/03/04 21:18:21 I can't because I need to kill 'git-daemon' child
43 return os.kill(process.pid, signal.SIGKILL)
44 subprocess.Popen.kill = kill_nix 67 subprocess.Popen.kill = kill_nix
45 68
46 69
47 def rmtree(*path): 70 def rmtree(*path):
48 """Recursively removes a directory, even if it's marked read-only. 71 """Recursively removes a directory, even if it's marked read-only.
49 72
50 Remove the directory located at *path, if it exists. 73 Remove the directory located at *path, if it exists.
51 74
52 shutil.rmtree() doesn't work on Windows if any of the files or directories 75 shutil.rmtree() doesn't work on Windows if any of the files or directories
53 are read-only, which svn repositories and some .svn files are. We need to 76 are read-only, which svn repositories and some .svn files are. We need to
(...skipping 189 matching lines...) Expand 10 before | Expand all | Expand 10 after
243 self.svn_revs = [None] 266 self.svn_revs = [None]
244 # Format is { repo: [ None, (hash, tree), (hash, tree), ... ], ... } 267 # Format is { repo: [ None, (hash, tree), (hash, tree), ... ], ... }
245 # so reference looks like self.git_hashes[repo][rev][0] for hash and 268 # so reference looks like self.git_hashes[repo][rev][0] for hash and
246 # self.git_hashes[repo][rev][1] for it's tree snapshot. 269 # self.git_hashes[repo][rev][1] for it's tree snapshot.
247 # For consistency with self.svn_revs, it is 1-based too. 270 # For consistency with self.svn_revs, it is 1-based too.
248 self.git_hashes = {} 271 self.git_hashes = {}
249 self.svnserve = None 272 self.svnserve = None
250 self.gitdaemon = None 273 self.gitdaemon = None
251 self.common_init = False 274 self.common_init = False
252 self.repos_dir = None 275 self.repos_dir = None
276 self.git_pid_file = None
253 self.git_root = None 277 self.git_root = None
254 self.svn_checkout = None 278 self.svn_checkout = None
255 self.svn_repo = None 279 self.svn_repo = None
256 self.git_dirty = False 280 self.git_dirty = False
257 self.svn_dirty = False 281 self.svn_dirty = False
282 self.svn_port = 3690
283 self.git_port = 9418
258 284
259 def trial_dir(self): 285 def trial_dir(self):
260 if not self.TRIAL_DIR: 286 if not self.TRIAL_DIR:
261 self.TRIAL_DIR = os.path.join( 287 self.TRIAL_DIR = os.path.join(
262 os.path.dirname(os.path.abspath(__file__)), '_trial') 288 os.path.dirname(os.path.abspath(__file__)), '_trial')
263 return self.TRIAL_DIR 289 return self.TRIAL_DIR
264 290
265 def setUp(self): 291 def set_up(self):
266 """All late initialization comes here. 292 """All late initialization comes here.
267 293
268 Note that it deletes all trial_dir() and not only repos_dir. 294 Note that it deletes all trial_dir() and not only repos_dir.
269 """ 295 """
270 self.cleanup_dirt() 296 self.cleanup_dirt()
271 if not self.common_init: 297 if not self.common_init:
272 self.common_init = True 298 self.common_init = True
273 self.repos_dir = os.path.join(self.trial_dir(), 'repos') 299 self.repos_dir = os.path.join(self.trial_dir(), 'repos')
274 self.git_root = join(self.repos_dir, 'git') 300 self.git_root = join(self.repos_dir, 'git')
275 self.svn_checkout = join(self.repos_dir, 'svn_checkout') 301 self.svn_checkout = join(self.repos_dir, 'svn_checkout')
276 self.svn_repo = join(self.repos_dir, 'svn') 302 self.svn_repo = join(self.repos_dir, 'svn')
277 addKill() 303 add_kill()
278 rmtree(self.trial_dir()) 304 rmtree(self.trial_dir())
279 os.makedirs(self.repos_dir) 305 os.makedirs(self.repos_dir)
280 atexit.register(self.tearDown) 306 atexit.register(self.tear_down)
281 307
282 def cleanup_dirt(self): 308 def cleanup_dirt(self):
283 """For each dirty repository, regenerate it.""" 309 """For each dirty repository, regenerate it."""
284 if self.svnserve and self.svn_dirty: 310 if self.svn_dirty:
285 logging.debug('Killing svnserve pid %s' % self.svnserve.pid) 311 if not self.tear_down_svn():
286 self.svnserve.kill() 312 logging.warning('Using both leaking checkout and svn dirty checkout')
287 self.svnserve = None 313 if self.git_dirty:
288 if not self.SHOULD_LEAK: 314 if not self.tear_down_git():
289 logging.debug('Removing dirty %s' % self.svn_repo) 315 logging.warning('Using both leaking checkout and git dirty checkout')
290 rmtree(self.svn_repo)
291 logging.debug('Removing dirty %s' % self.svn_checkout)
292 rmtree(self.svn_checkout)
293 else:
294 logging.warning('Using both leaking checkout and dirty checkout')
295 if self.gitdaemon and self.git_dirty:
296 logging.debug('Killing git-daemon pid %s' % self.gitdaemon.pid)
297 self.gitdaemon.kill()
298 self.gitdaemon = None
299 if not self.SHOULD_LEAK:
300 logging.debug('Removing dirty %s' % self.git_root)
301 rmtree(self.git_root)
302 else:
303 logging.warning('Using both leaking checkout and dirty checkout')
304 316
305 def tearDown(self): 317 def tear_down(self):
318 self.tear_down_svn()
319 self.tear_down_git()
320 if not self.SHOULD_LEAK:
321 logging.debug('Removing %s' % self.trial_dir())
322 rmtree(self.trial_dir())
323
324 def tear_down_svn(self):
306 if self.svnserve: 325 if self.svnserve:
307 logging.debug('Killing svnserve pid %s' % self.svnserve.pid) 326 logging.debug('Killing svnserve pid %s' % self.svnserve.pid)
308 self.svnserve.kill() 327 self.svnserve.kill()
328 self.wait_for_port_to_free(self.svn_port)
309 self.svnserve = None 329 self.svnserve = None
330 if not self.SHOULD_LEAK:
331 logging.debug('Removing %s' % self.svn_repo)
332 rmtree(self.svn_repo)
333 logging.debug('Removing %s' % self.svn_checkout)
334 rmtree(self.svn_checkout)
335 else:
336 return False
337 return True
338
339 def tear_down_git(self):
310 if self.gitdaemon: 340 if self.gitdaemon:
311 logging.debug('Killing git-daemon pid %s' % self.gitdaemon.pid) 341 logging.debug('Killing git-daemon pid %s' % self.gitdaemon.pid)
312 self.gitdaemon.kill() 342 self.gitdaemon.kill()
313 self.gitdaemon = None 343 self.gitdaemon = None
314 if not self.SHOULD_LEAK: 344 if self.git_pid_file:
315 logging.debug('Removing %s' % self.trial_dir()) 345 pid = int(self.git_pid_file.read())
316 rmtree(self.trial_dir()) 346 self.git_pid_file.close()
347 kill_pid(pid)
348 self.git_pid_file = None
349 self.wait_for_port_to_free(self.git_port)
350 if not self.SHOULD_LEAK:
351 logging.debug('Removing %s' % self.git_root)
352 rmtree(self.git_root)
353 else:
354 return False
355 return True
317 356
318 @staticmethod 357 @staticmethod
319 def _genTree(root, tree_dict): 358 def _genTree(root, tree_dict):
320 """For a dictionary of file contents, generate a filesystem.""" 359 """For a dictionary of file contents, generate a filesystem."""
321 if not os.path.isdir(root): 360 if not os.path.isdir(root):
322 os.makedirs(root) 361 os.makedirs(root)
323 for (k, v) in tree_dict.iteritems(): 362 for (k, v) in tree_dict.iteritems():
324 k_os = k.replace('/', os.sep) 363 k_os = k.replace('/', os.sep)
325 k_arr = k_os.split(os.sep) 364 k_arr = k_os.split(os.sep)
326 if len(k_arr) > 1: 365 if len(k_arr) > 1:
327 p = os.sep.join([root] + k_arr[:-1]) 366 p = os.sep.join([root] + k_arr[:-1])
328 if not os.path.isdir(p): 367 if not os.path.isdir(p):
329 os.makedirs(p) 368 os.makedirs(p)
330 if v is None: 369 if v is None:
331 os.remove(join(root, k)) 370 os.remove(join(root, k))
332 else: 371 else:
333 write(join(root, k), v) 372 write(join(root, k), v)
334 373
335 def setUpSVN(self): 374 def set_up_svn(self):
336 """Creates subversion repositories and start the servers.""" 375 """Creates subversion repositories and start the servers."""
337 self.setUp() 376 self.set_up()
338 if self.svnserve: 377 if self.svnserve:
339 return True 378 return True
340 try: 379 try:
341 check_call(['svnadmin', 'create', self.svn_repo]) 380 check_call(['svnadmin', 'create', self.svn_repo])
342 except OSError: 381 except OSError:
343 return False 382 return False
344 write(join(self.svn_repo, 'conf', 'svnserve.conf'), 383 write(join(self.svn_repo, 'conf', 'svnserve.conf'),
345 '[general]\n' 384 '[general]\n'
346 'anon-access = read\n' 385 'anon-access = read\n'
347 'auth-access = write\n' 386 'auth-access = write\n'
348 'password-db = passwd\n') 387 'password-db = passwd\n')
349 text = '[users]\n' 388 text = '[users]\n'
350 text += ''.join('%s = %s\n' % (usr, pwd) for usr, pwd in self.USERS) 389 text += ''.join('%s = %s\n' % (usr, pwd) for usr, pwd in self.USERS)
351 write(join(self.svn_repo, 'conf', 'passwd'), text) 390 write(join(self.svn_repo, 'conf', 'passwd'), text)
352 391
353 # Start the daemon. 392 # Start the daemon.
354 cmd = ['svnserve', '-d', '--foreground', '-r', self.repos_dir] 393 cmd = ['svnserve', '-d', '--foreground', '-r', self.repos_dir]
355 if self.HOST == '127.0.0.1': 394 if self.HOST == '127.0.0.1':
356 cmd.append('--listen-host=127.0.0.1') 395 cmd.append('--listen-host=127.0.0.1')
396 self.check_port_is_free(self.svn_port)
357 self.svnserve = Popen(cmd, cwd=self.svn_repo) 397 self.svnserve = Popen(cmd, cwd=self.svn_repo)
398 self.wait_for_port_to_bind(self.svn_port, self.svnserve)
358 self.populateSvn() 399 self.populateSvn()
359 self.svn_dirty = False 400 self.svn_dirty = False
360 return True 401 return True
361 402
362 def setUpGIT(self): 403 def set_up_git(self):
363 """Creates git repositories and start the servers.""" 404 """Creates git repositories and start the servers."""
364 self.setUp() 405 self.set_up()
365 if self.gitdaemon: 406 if self.gitdaemon:
366 return True 407 return True
367 if sys.platform == 'win32': 408 if sys.platform == 'win32':
368 return False 409 return False
410 assert self.git_pid_file == None
369 for repo in ['repo_%d' % r for r in range(1, self.NB_GIT_REPOS + 1)]: 411 for repo in ['repo_%d' % r for r in range(1, self.NB_GIT_REPOS + 1)]:
370 check_call(['git', 'init', '-q', join(self.git_root, repo)]) 412 check_call(['git', 'init', '-q', join(self.git_root, repo)])
371 self.git_hashes[repo] = [None] 413 self.git_hashes[repo] = [None]
414 # Unlike svn, populate git before starting the server.
372 self.populateGit() 415 self.populateGit()
373 # Start the daemon. 416 # Start the daemon.
374 cmd = ['git', 'daemon', '--export-all', '--base-path=' + self.repos_dir] 417 self.git_pid_file = tempfile.NamedTemporaryFile()
418 cmd = ['git', 'daemon',
419 '--export-all',
420 '--reuseaddr',
421 '--base-path=' + self.repos_dir,
422 '--pid-file=' + self.git_pid_file.name]
375 if self.HOST == '127.0.0.1': 423 if self.HOST == '127.0.0.1':
376 cmd.append('--listen=127.0.0.1') 424 cmd.append('--listen=127.0.0.1')
377 logging.debug(cmd) 425 self.check_port_is_free(self.git_port)
378 self.gitdaemon = Popen(cmd, cwd=self.repos_dir) 426 self.gitdaemon = Popen(cmd, cwd=self.repos_dir)
427 self.wait_for_port_to_bind(self.git_port, self.gitdaemon)
379 self.git_dirty = False 428 self.git_dirty = False
380 return True 429 return True
381 430
382 def _commit_svn(self, tree): 431 def _commit_svn(self, tree):
383 self._genTree(self.svn_checkout, tree) 432 self._genTree(self.svn_checkout, tree)
384 commit_svn(self.svn_checkout, self.USERS[0][0], self.USERS[0][1]) 433 commit_svn(self.svn_checkout, self.USERS[0][0], self.USERS[0][1])
385 if self.svn_revs and self.svn_revs[-1]: 434 if self.svn_revs and self.svn_revs[-1]:
386 new_tree = self.svn_revs[-1].copy() 435 new_tree = self.svn_revs[-1].copy()
387 new_tree.update(tree) 436 new_tree.update(tree)
388 else: 437 else:
389 new_tree = tree.copy() 438 new_tree = tree.copy()
390 self.svn_revs.append(new_tree) 439 self.svn_revs.append(new_tree)
391 440
392 def _commit_git(self, repo, tree): 441 def _commit_git(self, repo, tree):
393 repo_root = join(self.git_root, repo) 442 repo_root = join(self.git_root, repo)
394 self._genTree(repo_root, tree) 443 self._genTree(repo_root, tree)
395 commit_hash = commit_git(repo_root) 444 commit_hash = commit_git(repo_root)
396 if self.git_hashes[repo][-1]: 445 if self.git_hashes[repo][-1]:
397 new_tree = self.git_hashes[repo][-1][1].copy() 446 new_tree = self.git_hashes[repo][-1][1].copy()
398 new_tree.update(tree) 447 new_tree.update(tree)
399 else: 448 else:
400 new_tree = tree.copy() 449 new_tree = tree.copy()
401 self.git_hashes[repo].append((commit_hash, new_tree)) 450 self.git_hashes[repo].append((commit_hash, new_tree))
402 451
452 def check_port_is_free(self, port):
453 sock = socket.socket()
454 try:
455 sock.connect((self.HOST, port))
456 # It worked, throw.
457 assert False, '%d shouldn\'t be bound' % port
458 except EnvironmentError:
459 pass
460 finally:
461 sock.close()
462
463 def wait_for_port_to_bind(self, port, process):
464 sock = socket.socket()
465 try:
466 start = datetime.datetime.utcnow()
Dirk Pranke 2011/03/04 21:15:17 Curious why you're using datetime instead of just
M-A Ruel 2011/03/04 21:18:21 Habit, no real reason.
467 maxdelay = datetime.timedelta(seconds=30)
468 while (datetime.datetime.utcnow() - start) < maxdelay:
469 try:
470 sock.connect((self.HOST, port))
471 logging.debug('%d is now bound' % port)
472 return
473 except EnvironmentError:
474 pass
475 logging.debug('%d is still not bound' % port)
476 finally:
477 sock.close()
478 # The process failed to bind. Kill it and dump its ouput.
479 process.kill()
480 logging.error('%s' % process.communicate()[0])
481 assert False, '%d is still not bound' % port
482
483 def wait_for_port_to_free(self, port):
484 start = datetime.datetime.utcnow()
485 maxdelay = datetime.timedelta(seconds=30)
486 while (datetime.datetime.utcnow() - start) < maxdelay:
487 try:
488 sock = socket.socket()
489 sock.connect((self.HOST, port))
490 logging.debug('%d was bound, waiting to free' % port)
491 except EnvironmentError:
492 logging.debug('%d now free' % port)
493 return
494 finally:
495 sock.close()
496 assert False, '%d is still bound' % port
497
403 def populateSvn(self): 498 def populateSvn(self):
404 raise NotImplementedError() 499 raise NotImplementedError()
405 500
406 def populateGit(self): 501 def populateGit(self):
407 raise NotImplementedError() 502 raise NotImplementedError()
408 503
409 504
410 class FakeRepos(FakeReposBase): 505 class FakeRepos(FakeReposBase):
411 """Implements populateSvn() and populateGit().""" 506 """Implements populateSvn() and populateGit()."""
412 NB_GIT_REPOS = 4 507 NB_GIT_REPOS = 4
(...skipping 215 matching lines...) Expand 10 before | Expand all | Expand 10 after
628 # Override if necessary. 723 # Override if necessary.
629 FAKE_REPOS_CLASS = FakeRepos 724 FAKE_REPOS_CLASS = FakeRepos
630 725
631 def __init__(self, *args, **kwargs): 726 def __init__(self, *args, **kwargs):
632 unittest.TestCase.__init__(self, *args, **kwargs) 727 unittest.TestCase.__init__(self, *args, **kwargs)
633 if not FakeReposTestBase.FAKE_REPOS: 728 if not FakeReposTestBase.FAKE_REPOS:
634 FakeReposTestBase.FAKE_REPOS = self.FAKE_REPOS_CLASS() 729 FakeReposTestBase.FAKE_REPOS = self.FAKE_REPOS_CLASS()
635 730
636 def setUp(self): 731 def setUp(self):
637 unittest.TestCase.setUp(self) 732 unittest.TestCase.setUp(self)
638 self.FAKE_REPOS.setUp() 733 self.FAKE_REPOS.set_up()
639 734
640 # Remove left overs and start fresh. 735 # Remove left overs and start fresh.
641 if not self.CLASS_ROOT_DIR: 736 if not self.CLASS_ROOT_DIR:
642 self.CLASS_ROOT_DIR = join(self.FAKE_REPOS.trial_dir(), 'smoke') 737 self.CLASS_ROOT_DIR = join(self.FAKE_REPOS.trial_dir(), 'smoke')
643 self.root_dir = join(self.CLASS_ROOT_DIR, self.id()) 738 self.root_dir = join(self.CLASS_ROOT_DIR, self.id())
644 rmtree(self.root_dir) 739 rmtree(self.root_dir)
645 os.makedirs(self.root_dir) 740 os.makedirs(self.root_dir)
646 self.svn_base = 'svn://%s/svn/' % self.FAKE_REPOS.HOST 741 self.svn_base = 'svn://%s/svn/' % self.FAKE_REPOS.HOST
647 self.git_base = 'git://%s/git/' % self.FAKE_REPOS.HOST 742 self.git_base = 'git://%s/git/' % self.FAKE_REPOS.HOST
648 743
(...skipping 64 matching lines...) Expand 10 before | Expand all | Expand 10 after
713 808
714 def gittree(self, repo, rev): 809 def gittree(self, repo, rev):
715 """Sort-hand: returns the directory tree for a git 'revision'.""" 810 """Sort-hand: returns the directory tree for a git 'revision'."""
716 return self.FAKE_REPOS.git_hashes[repo][int(rev)][1] 811 return self.FAKE_REPOS.git_hashes[repo][int(rev)][1]
717 812
718 813
719 def main(argv): 814 def main(argv):
720 fake = FakeRepos() 815 fake = FakeRepos()
721 print 'Using %s' % fake.trial_dir() 816 print 'Using %s' % fake.trial_dir()
722 try: 817 try:
723 fake.setUp() 818 fake.set_up_svn()
819 fake.set_up_git()
724 print('Fake setup, press enter to quit or Ctrl-C to keep the checkouts.') 820 print('Fake setup, press enter to quit or Ctrl-C to keep the checkouts.')
725 sys.stdin.readline() 821 sys.stdin.readline()
726 except KeyboardInterrupt: 822 except KeyboardInterrupt:
727 fake.SHOULD_LEAK = True 823 fake.SHOULD_LEAK = True
728 return 0 824 return 0
729 825
730 826
731 if '-l' in sys.argv: 827 if '-l' in sys.argv:
732 # See SHOULD_LEAK definition in FakeReposBase for its purpose. 828 # See SHOULD_LEAK definition in FakeReposBase for its purpose.
733 FakeReposBase.SHOULD_LEAK = True 829 FakeReposBase.SHOULD_LEAK = True
734 print 'Leaking!' 830 print 'Leaking!'
735 sys.argv.remove('-l') 831 sys.argv.remove('-l')
736 832
737 833
738 if __name__ == '__main__': 834 if __name__ == '__main__':
739 sys.exit(main(sys.argv)) 835 sys.exit(main(sys.argv))
OLDNEW
« no previous file with comments | « no previous file | tests/gclient_smoketest.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698