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

Unified Diff: testing_support/git_test_utils.py

Issue 26109002: Add git-number script to calculate generation numbers for commits. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/tools/depot_tools
Patch Set: Add version checking for coverage module Created 7 years, 1 month 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 side-by-side diff with in-line comments
Download patch
« no previous file with comments | « testing_support/coverage_utils.py ('k') | tests/git_common_test.py » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
Index: testing_support/git_test_utils.py
diff --git a/testing_support/git_test_utils.py b/testing_support/git_test_utils.py
new file mode 100644
index 0000000000000000000000000000000000000000..2c7c4d6675f29887e65bc053ef00ee60b8917e76
--- /dev/null
+++ b/testing_support/git_test_utils.py
@@ -0,0 +1,418 @@
+# Copyright (c) 2013 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import atexit
+import collections
+import copy
+import datetime
+import hashlib
+import os
+import shutil
+import subprocess
+import tempfile
+import unittest
+
+
+def git_hash_data(data, typ='blob'):
+ """Calculate the git-style SHA1 for some data.
+
+ Only supports 'blob' type data at the moment.
+ """
+ assert typ == 'blob', 'Only support blobs for now'
+ return hashlib.sha1('blob %s\0%s' % (len(data), data)).hexdigest()
+
+
+class OrderedSet(collections.MutableSet):
+ # from http://code.activestate.com/recipes/576694/
+ def __init__(self, iterable=None):
+ self.end = end = []
+ end += [None, end, end] # sentinel node for doubly linked list
+ self.data = {} # key --> [key, prev, next]
+ if iterable is not None:
+ self |= iterable
+
+ def __contains__(self, key):
+ return key in self.data
+
+ def __eq__(self, other):
+ if isinstance(other, OrderedSet):
+ return len(self) == len(other) and list(self) == list(other)
+ return set(self) == set(other)
+
+ def __ne__(self, other):
+ if isinstance(other, OrderedSet):
+ return len(self) != len(other) or list(self) != list(other)
+ return set(self) != set(other)
+
+ def __len__(self):
+ return len(self.data)
+
+ def __iter__(self):
+ end = self.end
+ curr = end[2]
+ while curr is not end:
+ yield curr[0]
+ curr = curr[2]
+
+ def __repr__(self):
+ if not self:
+ return '%s()' % (self.__class__.__name__,)
+ return '%s(%r)' % (self.__class__.__name__, list(self))
+
+ def __reversed__(self):
+ end = self.end
+ curr = end[1]
+ while curr is not end:
+ yield curr[0]
+ curr = curr[1]
+
+ def add(self, key):
+ if key not in self.data:
+ end = self.end
+ curr = end[1]
+ curr[2] = end[1] = self.data[key] = [key, curr, end]
+
+ def difference_update(self, *others):
+ for other in others:
+ for i in other:
+ self.discard(i)
+
+ def discard(self, key):
+ if key in self.data:
+ key, prev, nxt = self.data.pop(key)
+ prev[2] = nxt
+ nxt[1] = prev
+
+ def pop(self, last=True): # pylint: disable=W0221
+ if not self:
+ raise KeyError('set is empty')
+ key = self.end[1][0] if last else self.end[2][0]
+ self.discard(key)
+ return key
+
+
+class GitRepoSchema(object):
+ """A declarative git testing repo.
+
+ Pass a schema to __init__ in the form of:
+ A B C D
+ B E D
+
+ This is the repo
+
+ A - B - C - D
+ \ E /
+
+ Whitespace doesn't matter. Each line is a declaration of which commits come
+ before which other commits.
+
+ Every commit gets a tag 'tag_%(commit)s'
+ Every unique terminal commit gets a branch 'branch_%(commit)s'
+ Last commit in First line is the branch 'master'
+ Root commits get a ref 'root_%(commit)s'
+
+ Timestamps are in topo order, earlier commits (as indicated by their presence
+ in the schema) get earlier timestamps. Stamps start at the Unix Epoch, and
+ increment by 1 day each.
+ """
+ COMMIT = collections.namedtuple('COMMIT', 'name parents is_branch is_root')
+
+ def __init__(self, repo_schema='',
+ content_fn=lambda v: {v: {'data': v}}):
+ """Builds a new GitRepoSchema.
+
+ Args:
+ repo_schema (str) - Initial schema for this repo. See class docstring for
+ info on the schema format.
+ content_fn ((commit_name) -> commit_data) - A function which will be
+ lazily called to obtain data for each commit. The results of this
+ function are cached (i.e. it will never be called twice for the same
+ commit_name). See the docstring on the GitRepo class for the format of
+ the data returned by this function.
+ """
+ self.master = None
+ self.par_map = {}
+ self.data_cache = {}
+ self.content_fn = content_fn
+ self.add_commits(repo_schema)
+
+ def walk(self):
+ """(Generator) Walks the repo schema from roots to tips.
+
+ Generates GitRepoSchema.COMMIT objects for each commit.
+
+ Throws an AssertionError if it detects a cycle.
+ """
+ is_root = True
+ par_map = copy.deepcopy(self.par_map)
+ while par_map:
+ empty_keys = set(k for k, v in par_map.iteritems() if not v)
+ assert empty_keys, 'Cycle detected! %s' % par_map
+
+ for k in sorted(empty_keys):
+ yield self.COMMIT(k, self.par_map[k],
+ not any(k in v for v in self.par_map.itervalues()),
+ is_root)
+ del par_map[k]
+ for v in par_map.itervalues():
+ v.difference_update(empty_keys)
+ is_root = False
+
+ def add_commits(self, schema):
+ """Adds more commits from a schema into the existing Schema.
+
+ Args:
+ schema (str) - See class docstring for info on schema format.
+
+ Throws an AssertionError if it detects a cycle.
+ """
+ for commits in (l.split() for l in schema.splitlines() if l.strip()):
+ parent = None
+ for commit in commits:
+ if commit not in self.par_map:
+ self.par_map[commit] = OrderedSet()
+ if parent is not None:
+ self.par_map[commit].add(parent)
+ parent = commit
+ if parent and not self.master:
+ self.master = parent
+ for _ in self.walk(): # This will throw if there are any cycles.
+ pass
+
+ def reify(self):
+ """Returns a real GitRepo for this GitRepoSchema"""
+ return GitRepo(self)
+
+ def data_for(self, commit):
+ """Obtains the data for |commit|.
+
+ See the docstring on the GitRepo class for the format of the returned data.
+
+ Caches the result on this GitRepoSchema instance.
+ """
+ if commit not in self.data_cache:
+ self.data_cache[commit] = self.content_fn(commit)
+ return self.data_cache[commit]
+
+
+class GitRepo(object):
+ """Creates a real git repo for a GitRepoSchema.
+
+ Obtains schema and content information from the GitRepoSchema.
+
+ The format for the commit data supplied by GitRepoSchema.data_for is:
+ {
+ SPECIAL_KEY: special_value,
+ ...
+ "path/to/some/file": { 'data': "some data content for this file",
+ 'mode': 0755 },
+ ...
+ }
+
+ The SPECIAL_KEYs are the following attribues of the GitRepo class:
+ * AUTHOR_NAME
+ * AUTHOR_EMAIL
+ * AUTHOR_DATE - must be a datetime.datetime instance
+ * COMMITTER_NAME
+ * COMMITTER_EMAIL
+ * COMMITTER_DATE - must be a datetime.datetime instance
+
+ For file content, if 'data' is None, then this commit will `git rm` that file.
+ """
+ BASE_TEMP_DIR = tempfile.mkdtemp(suffix='base', prefix='git_repo')
+ atexit.register(shutil.rmtree, BASE_TEMP_DIR)
+
+ # Singleton objects to specify specific data in a commit dictionary.
+ AUTHOR_NAME = object()
+ AUTHOR_EMAIL = object()
+ AUTHOR_DATE = object()
+ COMMITTER_NAME = object()
+ COMMITTER_EMAIL = object()
+ COMMITTER_DATE = object()
+
+ DEFAULT_AUTHOR_NAME = 'Author McAuthorly'
+ DEFAULT_AUTHOR_EMAIL = 'author@example.com'
+ DEFAULT_COMMITTER_NAME = 'Charles Committish'
+ DEFAULT_COMMITTER_EMAIL = 'commitish@example.com'
+
+ COMMAND_OUTPUT = collections.namedtuple('COMMAND_OUTPUT', 'retcode stdout')
+
+ def __init__(self, schema):
+ """Makes new GitRepo.
+
+ Automatically creates a temp folder under GitRepo.BASE_TEMP_DIR. It's
+ recommended that you clean this repo up by calling nuke() on it, but if not,
+ GitRepo will automatically clean up all allocated repos at the exit of the
+ program (assuming a normal exit like with sys.exit)
+
+ Args:
+ schema - An instance of GitRepoSchema
+ """
+ self.repo_path = tempfile.mkdtemp(dir=self.BASE_TEMP_DIR)
+ self.commit_map = {}
+ self._date = datetime.datetime(1970, 1, 1)
+
+ self.git('init')
+ for commit in schema.walk():
+ self._add_schema_commit(commit, schema.data_for(commit.name))
+ if schema.master:
+ self.git('update-ref', 'master', self[schema.master])
+
+ def __getitem__(self, commit_name):
+ """Gets the hash of a commit by its schema name.
+
+ >>> r = GitRepo(GitRepoSchema('A B C'))
+ >>> r['B']
+ '7381febe1da03b09da47f009963ab7998a974935'
+ """
+ return self.commit_map[commit_name]
+
+ def _add_schema_commit(self, commit, data):
+ data = data or {}
+
+ if commit.parents:
+ parents = list(commit.parents)
+ self.git('checkout', '--detach', '-q', self[parents[0]])
+ if len(parents) > 1:
+ self.git('merge', '--no-commit', '-q', *[self[x] for x in parents[1:]])
+ else:
+ self.git('checkout', '--orphan', 'root_%s' % commit.name)
+ self.git('rm', '-rf', '.')
+
+ env = {}
+ for prefix in ('AUTHOR', 'COMMITTER'):
+ for suffix in ('NAME', 'EMAIL', 'DATE'):
+ singleton = '%s_%s' % (prefix, suffix)
+ key = getattr(self, singleton)
+ if key in data:
+ val = data[key]
+ else:
+ if suffix == 'DATE':
+ val = self._date
+ self._date += datetime.timedelta(days=1)
+ else:
+ val = getattr(self, 'DEFAULT_%s' % singleton)
+ env['GIT_%s' % singleton] = str(val)
+
+ for fname, file_data in data.iteritems():
+ deleted = False
+ if 'data' in file_data:
+ data = file_data.get('data')
+ if data is None:
+ deleted = True
+ self.git('rm', fname)
+ else:
+ path = os.path.join(self.repo_path, fname)
+ pardir = os.path.dirname(path)
+ if not os.path.exists(pardir):
+ os.makedirs(pardir)
+ with open(path, 'wb') as f:
+ f.write(data)
+
+ mode = file_data.get('mode')
+ if mode and not deleted:
+ os.chmod(path, mode)
+
+ self.git('add', fname)
+
+ rslt = self.git('commit', '--allow-empty', '-m', commit.name, env=env)
+ assert rslt.retcode == 0, 'Failed to commit %s' % str(commit)
+ self.commit_map[commit.name] = self.git('rev-parse', 'HEAD').stdout.strip()
+ self.git('tag', 'tag_%s' % commit.name, self[commit.name])
+ if commit.is_branch:
+ self.git('update-ref', 'branch_%s' % commit.name, self[commit.name])
+
+ def git(self, *args, **kwargs):
+ """Runs a git command specified by |args| in this repo."""
+ assert self.repo_path is not None
+ try:
+ with open(os.devnull, 'wb') as devnull:
+ output = subprocess.check_output(
+ ('git',) + args, cwd=self.repo_path, stderr=devnull, **kwargs)
+ return self.COMMAND_OUTPUT(0, output)
+ except subprocess.CalledProcessError as e:
+ return self.COMMAND_OUTPUT(e.returncode, e.output)
+
+ def nuke(self):
+ """Obliterates the git repo on disk.
+
+ Causes this GitRepo to be unusable.
+ """
+ shutil.rmtree(self.repo_path)
+ self.repo_path = None
+
+ def run(self, fn, *args, **kwargs):
+ """Run a python function with the given args and kwargs with the cwd set to
+ the git repo."""
+ assert self.repo_path is not None
+ curdir = os.getcwd()
+ try:
+ os.chdir(self.repo_path)
+ return fn(*args, **kwargs)
+ finally:
+ os.chdir(curdir)
+
+
+class GitRepoSchemaTestBase(unittest.TestCase):
+ """A TestCase with a built-in GitRepoSchema.
+
+ Expects a class variable REPO to be a GitRepoSchema string in the form
+ described by that class.
+
+ You may also set class variables in the form COMMIT_%(commit_name)s, which
+ provide the content for the given commit_name commits.
+
+ You probably will end up using either GitRepoReadOnlyTestBase or
+ GitRepoReadWriteTestBase for real tests.
+ """
+ REPO = None
+
+ @classmethod
+ def getRepoContent(cls, commit):
+ return getattr(cls, 'COMMIT_%s' % commit, None)
+
+ @classmethod
+ def setUpClass(cls):
+ super(GitRepoSchemaTestBase, cls).setUpClass()
+ assert cls.REPO is not None
+ cls.r_schema = GitRepoSchema(cls.REPO, cls.getRepoContent)
+
+
+class GitRepoReadOnlyTestBase(GitRepoSchemaTestBase):
+ """Injects a GitRepo object given the schema and content from
+ GitRepoSchemaTestBase into TestCase classes which subclass this.
+
+ This GitRepo will appear as self.repo, and will be deleted and recreated once
+ for the duration of all the tests in the subclass.
+ """
+ REPO = None
+
+ @classmethod
+ def setUpClass(cls):
+ super(GitRepoReadOnlyTestBase, cls).setUpClass()
+ assert cls.REPO is not None
+ cls.repo = cls.r_schema.reify()
+
+ @classmethod
+ def tearDownClass(cls):
+ cls.repo.nuke()
+ super(GitRepoReadOnlyTestBase, cls).tearDownClass()
+
+
+class GitRepoReadWriteTestBase(GitRepoSchemaTestBase):
+ """Injects a GitRepo object given the schema and content from
+ GitRepoSchemaTestBase into TestCase classes which subclass this.
+
+ This GitRepo will appear as self.repo, and will be deleted and recreated for
+ each test function in the subclass.
+ """
+ REPO = None
+
+ def setUp(self):
+ super(GitRepoReadWriteTestBase, self).setUp()
+ self.repo = self.r_schema.reify()
+
+ def tearDown(self):
+ self.repo.nuke()
+ super(GitRepoReadWriteTestBase, self).tearDown()
« no previous file with comments | « testing_support/coverage_utils.py ('k') | tests/git_common_test.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698