| OLD | NEW |
| 1 # Copyright 2013 The Chromium Authors. All rights reserved. | 1 # Copyright 2013 The Chromium Authors. All rights reserved. |
| 2 # Use of this source code is governed by a BSD-style license that can be | 2 # Use of this source code is governed by a BSD-style license that can be |
| 3 # found in the LICENSE file. | 3 # found in the LICENSE file. |
| 4 | 4 |
| 5 import atexit | 5 import atexit |
| 6 import collections | 6 import collections |
| 7 import copy | 7 import copy |
| 8 import datetime | 8 import datetime |
| 9 import hashlib | 9 import hashlib |
| 10 import os | 10 import os |
| 11 import shutil | 11 import shutil |
| 12 import subprocess | 12 import subprocess |
| 13 import sys |
| 13 import tempfile | 14 import tempfile |
| 14 import unittest | 15 import unittest |
| 15 | 16 |
| 17 from cStringIO import StringIO |
| 18 |
| 16 | 19 |
| 17 def git_hash_data(data, typ='blob'): | 20 def git_hash_data(data, typ='blob'): |
| 18 """Calculate the git-style SHA1 for some data. | 21 """Calculate the git-style SHA1 for some data. |
| 19 | 22 |
| 20 Only supports 'blob' type data at the moment. | 23 Only supports 'blob' type data at the moment. |
| 21 """ | 24 """ |
| 22 assert typ == 'blob', 'Only support blobs for now' | 25 assert typ == 'blob', 'Only support blobs for now' |
| 23 return hashlib.sha1('blob %s\0%s' % (len(data), data)).hexdigest() | 26 return hashlib.sha1('blob %s\0%s' % (len(data), data)).hexdigest() |
| 24 | 27 |
| 25 | 28 |
| (...skipping 221 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 247 program (assuming a normal exit like with sys.exit) | 250 program (assuming a normal exit like with sys.exit) |
| 248 | 251 |
| 249 Args: | 252 Args: |
| 250 schema - An instance of GitRepoSchema | 253 schema - An instance of GitRepoSchema |
| 251 """ | 254 """ |
| 252 self.repo_path = tempfile.mkdtemp(dir=self.BASE_TEMP_DIR) | 255 self.repo_path = tempfile.mkdtemp(dir=self.BASE_TEMP_DIR) |
| 253 self.commit_map = {} | 256 self.commit_map = {} |
| 254 self._date = datetime.datetime(1970, 1, 1) | 257 self._date = datetime.datetime(1970, 1, 1) |
| 255 | 258 |
| 256 self.git('init') | 259 self.git('init') |
| 260 self.git('config', 'user.name', 'testcase') |
| 261 self.git('config', 'user.email', 'testcase@example.com') |
| 257 for commit in schema.walk(): | 262 for commit in schema.walk(): |
| 258 self._add_schema_commit(commit, schema.data_for(commit.name)) | 263 self._add_schema_commit(commit, schema.data_for(commit.name)) |
| 259 self.last_commit = self[commit.name] | 264 self.last_commit = self[commit.name] |
| 260 if schema.master: | 265 if schema.master: |
| 261 self.git('update-ref', 'master', self[schema.master]) | 266 self.git('update-ref', 'refs/heads/master', self[schema.master]) |
| 262 | 267 |
| 263 def __getitem__(self, commit_name): | 268 def __getitem__(self, commit_name): |
| 264 """Gets the hash of a commit by its schema name. | 269 """Gets the hash of a commit by its schema name. |
| 265 | 270 |
| 266 >>> r = GitRepo(GitRepoSchema('A B C')) | 271 >>> r = GitRepo(GitRepoSchema('A B C')) |
| 267 >>> r['B'] | 272 >>> r['B'] |
| 268 '7381febe1da03b09da47f009963ab7998a974935' | 273 '7381febe1da03b09da47f009963ab7998a974935' |
| 269 """ | 274 """ |
| 270 return self.commit_map[commit_name] | 275 return self.commit_map[commit_name] |
| 271 | 276 |
| 272 def _add_schema_commit(self, commit, data): | 277 def _add_schema_commit(self, commit, commit_data): |
| 273 data = data or {} | 278 commit_data = commit_data or {} |
| 274 | 279 |
| 275 if commit.parents: | 280 if commit.parents: |
| 276 parents = list(commit.parents) | 281 parents = list(commit.parents) |
| 277 self.git('checkout', '--detach', '-q', self[parents[0]]) | 282 self.git('checkout', '--detach', '-q', self[parents[0]]) |
| 278 if len(parents) > 1: | 283 if len(parents) > 1: |
| 279 self.git('merge', '--no-commit', '-q', *[self[x] for x in parents[1:]]) | 284 self.git('merge', '--no-commit', '-q', *[self[x] for x in parents[1:]]) |
| 280 else: | 285 else: |
| 281 self.git('checkout', '--orphan', 'root_%s' % commit.name) | 286 self.git('checkout', '--orphan', 'root_%s' % commit.name) |
| 282 self.git('rm', '-rf', '.') | 287 self.git('rm', '-rf', '.') |
| 283 | 288 |
| 284 env = {} | 289 env = self.get_git_commit_env(commit_data) |
| 285 for prefix in ('AUTHOR', 'COMMITTER'): | |
| 286 for suffix in ('NAME', 'EMAIL', 'DATE'): | |
| 287 singleton = '%s_%s' % (prefix, suffix) | |
| 288 key = getattr(self, singleton) | |
| 289 if key in data: | |
| 290 val = data[key] | |
| 291 else: | |
| 292 if suffix == 'DATE': | |
| 293 val = self._date | |
| 294 self._date += datetime.timedelta(days=1) | |
| 295 else: | |
| 296 val = getattr(self, 'DEFAULT_%s' % singleton) | |
| 297 env['GIT_%s' % singleton] = str(val) | |
| 298 | 290 |
| 299 for fname, file_data in data.iteritems(): | 291 for fname, file_data in commit_data.iteritems(): |
| 300 deleted = False | 292 deleted = False |
| 301 if 'data' in file_data: | 293 if 'data' in file_data: |
| 302 data = file_data.get('data') | 294 data = file_data.get('data') |
| 303 if data is None: | 295 if data is None: |
| 304 deleted = True | 296 deleted = True |
| 305 self.git('rm', fname) | 297 self.git('rm', fname) |
| 306 else: | 298 else: |
| 307 path = os.path.join(self.repo_path, fname) | 299 path = os.path.join(self.repo_path, fname) |
| 308 pardir = os.path.dirname(path) | 300 pardir = os.path.dirname(path) |
| 309 if not os.path.exists(pardir): | 301 if not os.path.exists(pardir): |
| 310 os.makedirs(pardir) | 302 os.makedirs(pardir) |
| 311 with open(path, 'wb') as f: | 303 with open(path, 'wb') as f: |
| 312 f.write(data) | 304 f.write(data) |
| 313 | 305 |
| 314 mode = file_data.get('mode') | 306 mode = file_data.get('mode') |
| 315 if mode and not deleted: | 307 if mode and not deleted: |
| 316 os.chmod(path, mode) | 308 os.chmod(path, mode) |
| 317 | 309 |
| 318 self.git('add', fname) | 310 self.git('add', fname) |
| 319 | 311 |
| 320 rslt = self.git('commit', '--allow-empty', '-m', commit.name, env=env) | 312 rslt = self.git('commit', '--allow-empty', '-m', commit.name, env=env) |
| 321 assert rslt.retcode == 0, 'Failed to commit %s' % str(commit) | 313 assert rslt.retcode == 0, 'Failed to commit %s' % str(commit) |
| 322 self.commit_map[commit.name] = self.git('rev-parse', 'HEAD').stdout.strip() | 314 self.commit_map[commit.name] = self.git('rev-parse', 'HEAD').stdout.strip() |
| 323 self.git('tag', 'tag_%s' % commit.name, self[commit.name]) | 315 self.git('tag', 'tag_%s' % commit.name, self[commit.name]) |
| 324 if commit.is_branch: | 316 if commit.is_branch: |
| 325 self.git('branch', '-f', 'branch_%s' % commit.name, self[commit.name]) | 317 self.git('branch', '-f', 'branch_%s' % commit.name, self[commit.name]) |
| 326 | 318 |
| 319 def get_git_commit_env(self, commit_data=None): |
| 320 commit_data = commit_data or {} |
| 321 env = {} |
| 322 for prefix in ('AUTHOR', 'COMMITTER'): |
| 323 for suffix in ('NAME', 'EMAIL', 'DATE'): |
| 324 singleton = '%s_%s' % (prefix, suffix) |
| 325 key = getattr(self, singleton) |
| 326 if key in commit_data: |
| 327 val = commit_data[key] |
| 328 else: |
| 329 if suffix == 'DATE': |
| 330 val = self._date |
| 331 self._date += datetime.timedelta(days=1) |
| 332 else: |
| 333 val = getattr(self, 'DEFAULT_%s' % singleton) |
| 334 env['GIT_%s' % singleton] = str(val) |
| 335 return env |
| 336 |
| 337 |
| 327 def git(self, *args, **kwargs): | 338 def git(self, *args, **kwargs): |
| 328 """Runs a git command specified by |args| in this repo.""" | 339 """Runs a git command specified by |args| in this repo.""" |
| 329 assert self.repo_path is not None | 340 assert self.repo_path is not None |
| 330 try: | 341 try: |
| 331 with open(os.devnull, 'wb') as devnull: | 342 with open(os.devnull, 'wb') as devnull: |
| 332 output = subprocess.check_output( | 343 output = subprocess.check_output( |
| 333 ('git',) + args, cwd=self.repo_path, stderr=devnull, **kwargs) | 344 ('git',) + args, cwd=self.repo_path, stderr=devnull, **kwargs) |
| 334 return self.COMMAND_OUTPUT(0, output) | 345 return self.COMMAND_OUTPUT(0, output) |
| 335 except subprocess.CalledProcessError as e: | 346 except subprocess.CalledProcessError as e: |
| 336 return self.COMMAND_OUTPUT(e.returncode, e.output) | 347 return self.COMMAND_OUTPUT(e.returncode, e.output) |
| 337 | 348 |
| 349 def git_commit(self, message): |
| 350 return self.git('commit', '-am', message, env=self.get_git_commit_env()) |
| 351 |
| 338 def nuke(self): | 352 def nuke(self): |
| 339 """Obliterates the git repo on disk. | 353 """Obliterates the git repo on disk. |
| 340 | 354 |
| 341 Causes this GitRepo to be unusable. | 355 Causes this GitRepo to be unusable. |
| 342 """ | 356 """ |
| 343 shutil.rmtree(self.repo_path) | 357 shutil.rmtree(self.repo_path) |
| 344 self.repo_path = None | 358 self.repo_path = None |
| 345 | 359 |
| 346 def run(self, fn, *args, **kwargs): | 360 def run(self, fn, *args, **kwargs): |
| 347 """Run a python function with the given args and kwargs with the cwd set to | 361 """Run a python function with the given args and kwargs with the cwd set to |
| 348 the git repo.""" | 362 the git repo.""" |
| 349 assert self.repo_path is not None | 363 assert self.repo_path is not None |
| 350 curdir = os.getcwd() | 364 curdir = os.getcwd() |
| 351 try: | 365 try: |
| 352 os.chdir(self.repo_path) | 366 os.chdir(self.repo_path) |
| 353 return fn(*args, **kwargs) | 367 return fn(*args, **kwargs) |
| 354 finally: | 368 finally: |
| 355 os.chdir(curdir) | 369 os.chdir(curdir) |
| 356 | 370 |
| 371 def capture_stdio(self, fn, *args, **kwargs): |
| 372 """Run a python function with the given args and kwargs with the cwd set to |
| 373 the git repo. |
| 374 |
| 375 Returns the (stdout, stderr) of whatever ran, instead of the what |fn| |
| 376 returned. |
| 377 """ |
| 378 stdout = sys.stdout |
| 379 stderr = sys.stderr |
| 380 try: |
| 381 sys.stdout = StringIO() |
| 382 sys.stderr = StringIO() |
| 383 try: |
| 384 self.run(fn, *args, **kwargs) |
| 385 except SystemExit: |
| 386 pass |
| 387 return sys.stdout.getvalue(), sys.stderr.getvalue() |
| 388 finally: |
| 389 sys.stdout = stdout |
| 390 sys.stderr = stderr |
| 391 |
| 392 def open(self, path, mode='rb'): |
| 393 return open(os.path.join(self.repo_path, path), mode) |
| 394 |
| 357 | 395 |
| 358 class GitRepoSchemaTestBase(unittest.TestCase): | 396 class GitRepoSchemaTestBase(unittest.TestCase): |
| 359 """A TestCase with a built-in GitRepoSchema. | 397 """A TestCase with a built-in GitRepoSchema. |
| 360 | 398 |
| 361 Expects a class variable REPO to be a GitRepoSchema string in the form | 399 Expects a class variable REPO_SCHEMA to be a GitRepoSchema string in the form |
| 362 described by that class. | 400 described by that class. |
| 363 | 401 |
| 364 You may also set class variables in the form COMMIT_%(commit_name)s, which | 402 You may also set class variables in the form COMMIT_%(commit_name)s, which |
| 365 provide the content for the given commit_name commits. | 403 provide the content for the given commit_name commits. |
| 366 | 404 |
| 367 You probably will end up using either GitRepoReadOnlyTestBase or | 405 You probably will end up using either GitRepoReadOnlyTestBase or |
| 368 GitRepoReadWriteTestBase for real tests. | 406 GitRepoReadWriteTestBase for real tests. |
| 369 """ | 407 """ |
| 370 REPO = None | 408 REPO_SCHEMA = None |
| 371 | 409 |
| 372 @classmethod | 410 @classmethod |
| 373 def getRepoContent(cls, commit): | 411 def getRepoContent(cls, commit): |
| 374 return getattr(cls, 'COMMIT_%s' % commit, None) | 412 return getattr(cls, 'COMMIT_%s' % commit, None) |
| 375 | 413 |
| 376 @classmethod | 414 @classmethod |
| 377 def setUpClass(cls): | 415 def setUpClass(cls): |
| 378 super(GitRepoSchemaTestBase, cls).setUpClass() | 416 super(GitRepoSchemaTestBase, cls).setUpClass() |
| 379 assert cls.REPO is not None | 417 assert cls.REPO_SCHEMA is not None |
| 380 cls.r_schema = GitRepoSchema(cls.REPO, cls.getRepoContent) | 418 cls.r_schema = GitRepoSchema(cls.REPO_SCHEMA, cls.getRepoContent) |
| 381 | 419 |
| 382 | 420 |
| 383 class GitRepoReadOnlyTestBase(GitRepoSchemaTestBase): | 421 class GitRepoReadOnlyTestBase(GitRepoSchemaTestBase): |
| 384 """Injects a GitRepo object given the schema and content from | 422 """Injects a GitRepo object given the schema and content from |
| 385 GitRepoSchemaTestBase into TestCase classes which subclass this. | 423 GitRepoSchemaTestBase into TestCase classes which subclass this. |
| 386 | 424 |
| 387 This GitRepo will appear as self.repo, and will be deleted and recreated once | 425 This GitRepo will appear as self.repo, and will be deleted and recreated once |
| 388 for the duration of all the tests in the subclass. | 426 for the duration of all the tests in the subclass. |
| 389 """ | 427 """ |
| 390 REPO = None | 428 REPO_SCHEMA = None |
| 391 | 429 |
| 392 @classmethod | 430 @classmethod |
| 393 def setUpClass(cls): | 431 def setUpClass(cls): |
| 394 super(GitRepoReadOnlyTestBase, cls).setUpClass() | 432 super(GitRepoReadOnlyTestBase, cls).setUpClass() |
| 395 assert cls.REPO is not None | 433 assert cls.REPO_SCHEMA is not None |
| 396 cls.repo = cls.r_schema.reify() | 434 cls.repo = cls.r_schema.reify() |
| 397 | 435 |
| 398 def setUp(self): | 436 def setUp(self): |
| 399 self.repo.git('checkout', '-f', self.repo.last_commit) | 437 self.repo.git('checkout', '-f', self.repo.last_commit) |
| 400 | 438 |
| 401 @classmethod | 439 @classmethod |
| 402 def tearDownClass(cls): | 440 def tearDownClass(cls): |
| 403 cls.repo.nuke() | 441 cls.repo.nuke() |
| 404 super(GitRepoReadOnlyTestBase, cls).tearDownClass() | 442 super(GitRepoReadOnlyTestBase, cls).tearDownClass() |
| 405 | 443 |
| 406 | 444 |
| 407 class GitRepoReadWriteTestBase(GitRepoSchemaTestBase): | 445 class GitRepoReadWriteTestBase(GitRepoSchemaTestBase): |
| 408 """Injects a GitRepo object given the schema and content from | 446 """Injects a GitRepo object given the schema and content from |
| 409 GitRepoSchemaTestBase into TestCase classes which subclass this. | 447 GitRepoSchemaTestBase into TestCase classes which subclass this. |
| 410 | 448 |
| 411 This GitRepo will appear as self.repo, and will be deleted and recreated for | 449 This GitRepo will appear as self.repo, and will be deleted and recreated for |
| 412 each test function in the subclass. | 450 each test function in the subclass. |
| 413 """ | 451 """ |
| 414 REPO = None | 452 REPO_SCHEMA = None |
| 415 | 453 |
| 416 def setUp(self): | 454 def setUp(self): |
| 417 super(GitRepoReadWriteTestBase, self).setUp() | 455 super(GitRepoReadWriteTestBase, self).setUp() |
| 418 self.repo = self.r_schema.reify() | 456 self.repo = self.r_schema.reify() |
| 419 | 457 |
| 420 def tearDown(self): | 458 def tearDown(self): |
| 421 self.repo.nuke() | 459 self.repo.nuke() |
| 422 super(GitRepoReadWriteTestBase, self).tearDown() | 460 super(GitRepoReadWriteTestBase, self).tearDown() |
| OLD | NEW |