utils.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312
  1. # utils.py -- Test utilities for Dulwich.
  2. # Copyright (C) 2010 Google, Inc.
  3. #
  4. # This program is free software; you can redistribute it and/or
  5. # modify it under the terms of the GNU General Public License
  6. # as published by the Free Software Foundation; version 2
  7. # of the License or (at your option) any later version of
  8. # the License.
  9. #
  10. # This program is distributed in the hope that it will be useful,
  11. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. # GNU General Public License for more details.
  14. #
  15. # You should have received a copy of the GNU General Public License
  16. # along with this program; if not, write to the Free Software
  17. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
  18. # MA 02110-1301, USA.
  19. """Utility functions common to Dulwich tests."""
  20. import datetime
  21. import os
  22. import shutil
  23. import tempfile
  24. import time
  25. import types
  26. from dulwich.index import (
  27. commit_tree,
  28. )
  29. from dulwich.objects import (
  30. FixedSha,
  31. Commit,
  32. )
  33. from dulwich.pack import (
  34. OFS_DELTA,
  35. REF_DELTA,
  36. DELTA_TYPES,
  37. obj_sha,
  38. SHA1Writer,
  39. write_pack_header,
  40. write_pack_object,
  41. create_delta,
  42. )
  43. from dulwich.repo import Repo
  44. from dulwich.tests import (
  45. SkipTest,
  46. )
  47. # Plain files are very frequently used in tests, so let the mode be very short.
  48. F = 0100644 # Shorthand mode for Files.
  49. def open_repo(name):
  50. """Open a copy of a repo in a temporary directory.
  51. Use this function for accessing repos in dulwich/tests/data/repos to avoid
  52. accidentally or intentionally modifying those repos in place. Use
  53. tear_down_repo to delete any temp files created.
  54. :param name: The name of the repository, relative to
  55. dulwich/tests/data/repos
  56. :returns: An initialized Repo object that lives in a temporary directory.
  57. """
  58. temp_dir = tempfile.mkdtemp()
  59. repo_dir = os.path.join(os.path.dirname(__file__), 'data', 'repos', name)
  60. temp_repo_dir = os.path.join(temp_dir, name)
  61. shutil.copytree(repo_dir, temp_repo_dir, symlinks=True)
  62. return Repo(temp_repo_dir)
  63. def tear_down_repo(repo):
  64. """Tear down a test repository."""
  65. temp_dir = os.path.dirname(repo.path.rstrip(os.sep))
  66. shutil.rmtree(temp_dir)
  67. def make_object(cls, **attrs):
  68. """Make an object for testing and assign some members.
  69. This method creates a new subclass to allow arbitrary attribute
  70. reassignment, which is not otherwise possible with objects having __slots__.
  71. :param attrs: dict of attributes to set on the new object.
  72. :return: A newly initialized object of type cls.
  73. """
  74. class TestObject(cls):
  75. """Class that inherits from the given class, but without __slots__.
  76. Note that classes with __slots__ can't have arbitrary attributes monkey-
  77. patched in, so this is a class that is exactly the same only with a
  78. __dict__ instead of __slots__.
  79. """
  80. pass
  81. obj = TestObject()
  82. for name, value in attrs.iteritems():
  83. if name == 'id':
  84. # id property is read-only, so we overwrite sha instead.
  85. sha = FixedSha(value)
  86. obj.sha = lambda: sha
  87. else:
  88. setattr(obj, name, value)
  89. return obj
  90. def make_commit(**attrs):
  91. """Make a Commit object with a default set of members.
  92. :param attrs: dict of attributes to overwrite from the default values.
  93. :return: A newly initialized Commit object.
  94. """
  95. default_time = int(time.mktime(datetime.datetime(2010, 1, 1).timetuple()))
  96. all_attrs = {'author': 'Test Author <test@nodomain.com>',
  97. 'author_time': default_time,
  98. 'author_timezone': 0,
  99. 'committer': 'Test Committer <test@nodomain.com>',
  100. 'commit_time': default_time,
  101. 'commit_timezone': 0,
  102. 'message': 'Test message.',
  103. 'parents': [],
  104. 'tree': '0' * 40}
  105. all_attrs.update(attrs)
  106. return make_object(Commit, **all_attrs)
  107. def functest_builder(method, func):
  108. """Generate a test method that tests the given function."""
  109. def do_test(self):
  110. method(self, func)
  111. return do_test
  112. def ext_functest_builder(method, func):
  113. """Generate a test method that tests the given extension function.
  114. This is intended to generate test methods that test both a pure-Python
  115. version and an extension version using common test code. The extension test
  116. will raise SkipTest if the extension is not found.
  117. Sample usage:
  118. class MyTest(TestCase);
  119. def _do_some_test(self, func_impl):
  120. self.assertEqual('foo', func_impl())
  121. test_foo = functest_builder(_do_some_test, foo_py)
  122. test_foo_extension = ext_functest_builder(_do_some_test, _foo_c)
  123. :param method: The method to run. It must must two parameters, self and the
  124. function implementation to test.
  125. :param func: The function implementation to pass to method.
  126. """
  127. def do_test(self):
  128. if not isinstance(func, types.BuiltinFunctionType):
  129. raise SkipTest("%s extension not found" % func.func_name)
  130. method(self, func)
  131. return do_test
  132. def build_pack(f, objects_spec, store=None):
  133. """Write test pack data from a concise spec.
  134. :param f: A file-like object to write the pack to.
  135. :param objects_spec: A list of (type_num, obj). For non-delta types, obj
  136. is the string of that object's data.
  137. For delta types, obj is a tuple of (base, data), where:
  138. * base can be either an index in objects_spec of the base for that
  139. * delta; or for a ref delta, a SHA, in which case the resulting pack
  140. * will be thin and the base will be an external ref.
  141. * data is a string of the full, non-deltified data for that object.
  142. Note that offsets/refs and deltas are computed within this function.
  143. :param store: An optional ObjectStore for looking up external refs.
  144. :return: A list of tuples in the order specified by objects_spec:
  145. (offset, type num, data, sha, CRC32)
  146. """
  147. sf = SHA1Writer(f)
  148. num_objects = len(objects_spec)
  149. write_pack_header(sf, num_objects)
  150. full_objects = {}
  151. offsets = {}
  152. crc32s = {}
  153. while len(full_objects) < num_objects:
  154. for i, (type_num, data) in enumerate(objects_spec):
  155. if type_num not in DELTA_TYPES:
  156. full_objects[i] = (type_num, data,
  157. obj_sha(type_num, [data]))
  158. continue
  159. base, data = data
  160. if isinstance(base, int):
  161. if base not in full_objects:
  162. continue
  163. base_type_num, _, _ = full_objects[base]
  164. else:
  165. base_type_num, _ = store.get_raw(base)
  166. full_objects[i] = (base_type_num, data,
  167. obj_sha(base_type_num, [data]))
  168. for i, (type_num, obj) in enumerate(objects_spec):
  169. offset = f.tell()
  170. if type_num == OFS_DELTA:
  171. base_index, data = obj
  172. base = offset - offsets[base_index]
  173. _, base_data, _ = full_objects[base_index]
  174. obj = (base, create_delta(base_data, data))
  175. elif type_num == REF_DELTA:
  176. base_ref, data = obj
  177. if isinstance(base_ref, int):
  178. _, base_data, base = full_objects[base_ref]
  179. else:
  180. base_type_num, base_data = store.get_raw(base_ref)
  181. base = obj_sha(base_type_num, base_data)
  182. obj = (base, create_delta(base_data, data))
  183. crc32 = write_pack_object(sf, type_num, obj)
  184. offsets[i] = offset
  185. crc32s[i] = crc32
  186. expected = []
  187. for i in xrange(num_objects):
  188. type_num, data, sha = full_objects[i]
  189. assert len(sha) == 20
  190. expected.append((offsets[i], type_num, data, sha, crc32s[i]))
  191. sf.write_sha()
  192. f.seek(0)
  193. return expected
  194. def build_commit_graph(object_store, commit_spec, trees=None, attrs=None):
  195. """Build a commit graph from a concise specification.
  196. Sample usage:
  197. >>> c1, c2, c3 = build_commit_graph(store, [[1], [2, 1], [3, 1, 2]])
  198. >>> store[store[c3].parents[0]] == c1
  199. True
  200. >>> store[store[c3].parents[1]] == c2
  201. True
  202. If not otherwise specified, commits will refer to the empty tree and have
  203. commit times increasing in the same order as the commit spec.
  204. :param object_store: An ObjectStore to commit objects to.
  205. :param commit_spec: An iterable of iterables of ints defining the commit
  206. graph. Each entry defines one commit, and entries must be in topological
  207. order. The first element of each entry is a commit number, and the
  208. remaining elements are its parents. The commit numbers are only
  209. meaningful for the call to make_commits; since real commit objects are
  210. created, they will get created with real, opaque SHAs.
  211. :param trees: An optional dict of commit number -> tree spec for building
  212. trees for commits. The tree spec is an iterable of (path, blob, mode) or
  213. (path, blob) entries; if mode is omitted, it defaults to the normal file
  214. mode (0100644).
  215. :param attrs: A dict of commit number -> (dict of attribute -> value) for
  216. assigning additional values to the commits.
  217. :return: The list of commit objects created.
  218. :raise ValueError: If an undefined commit identifier is listed as a parent.
  219. """
  220. if trees is None:
  221. trees = {}
  222. if attrs is None:
  223. attrs = {}
  224. commit_time = 0
  225. nums = {}
  226. commits = []
  227. for commit in commit_spec:
  228. commit_num = commit[0]
  229. try:
  230. parent_ids = [nums[pn] for pn in commit[1:]]
  231. except KeyError, e:
  232. missing_parent, = e.args
  233. raise ValueError('Unknown parent %i' % missing_parent)
  234. blobs = []
  235. for entry in trees.get(commit_num, []):
  236. if len(entry) == 2:
  237. path, blob = entry
  238. entry = (path, blob, F)
  239. path, blob, mode = entry
  240. blobs.append((path, blob.id, mode))
  241. object_store.add_object(blob)
  242. tree_id = commit_tree(object_store, blobs)
  243. commit_attrs = {
  244. 'message': 'Commit %i' % commit_num,
  245. 'parents': parent_ids,
  246. 'tree': tree_id,
  247. 'commit_time': commit_time,
  248. }
  249. commit_attrs.update(attrs.get(commit_num, {}))
  250. commit_obj = make_commit(**commit_attrs)
  251. # By default, increment the time by a lot. Out-of-order commits should
  252. # be closer together than this because their main cause is clock skew.
  253. commit_time = commit_attrs['commit_time'] + 100
  254. nums[commit_num] = commit_obj.id
  255. object_store.add_object(commit_obj)
  256. commits.append(commit_obj)
  257. return commits