gvsig-scripting / org.gvsig.scripting / trunk / org.gvsig.scripting / org.gvsig.scripting.app / org.gvsig.scripting.app.mainplugin / src / main / resources-plugin / scripting / lib / dulwich / tests / utils.py @ 959
History | View | Annotate | Download (12.3 KB)
1 | 959 | jjdelcerro | # utils.py -- Test utilities for Dulwich.
|
---|---|---|---|
2 | # Copyright (C) 2010 Google, Inc.
|
||
3 | #
|
||
4 | # Dulwich is dual-licensed under the Apache License, Version 2.0 and the GNU
|
||
5 | # General Public License as public by the Free Software Foundation; version 2.0
|
||
6 | # or (at your option) any later version. You can redistribute it and/or
|
||
7 | # modify it under the terms of either of these two licenses.
|
||
8 | #
|
||
9 | # Unless required by applicable law or agreed to in writing, software
|
||
10 | # distributed under the License is distributed on an "AS IS" BASIS,
|
||
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
12 | # See the License for the specific language governing permissions and
|
||
13 | # limitations under the License.
|
||
14 | #
|
||
15 | # You should have received a copy of the licenses; if not, see
|
||
16 | # <http://www.gnu.org/licenses/> for a copy of the GNU General Public License
|
||
17 | # and <http://www.apache.org/licenses/LICENSE-2.0> for a copy of the Apache
|
||
18 | # License, Version 2.0.
|
||
19 | #
|
||
20 | |||
21 | """Utility functions common to Dulwich tests."""
|
||
22 | |||
23 | |||
24 | import datetime |
||
25 | import os |
||
26 | import shutil |
||
27 | import tempfile |
||
28 | import time |
||
29 | import types |
||
30 | |||
31 | import warnings |
||
32 | |||
33 | from dulwich.index import ( |
||
34 | commit_tree, |
||
35 | ) |
||
36 | from dulwich.objects import ( |
||
37 | FixedSha, |
||
38 | Commit, |
||
39 | Tag, |
||
40 | object_class, |
||
41 | ) |
||
42 | from dulwich.pack import ( |
||
43 | OFS_DELTA, |
||
44 | REF_DELTA, |
||
45 | DELTA_TYPES, |
||
46 | obj_sha, |
||
47 | SHA1Writer, |
||
48 | write_pack_header, |
||
49 | write_pack_object, |
||
50 | create_delta, |
||
51 | ) |
||
52 | from dulwich.repo import Repo |
||
53 | from dulwich.tests import ( |
||
54 | SkipTest, |
||
55 | skipIf, |
||
56 | ) |
||
57 | |||
58 | |||
59 | # Plain files are very frequently used in tests, so let the mode be very short.
|
||
60 | F = 0o100644 # Shorthand mode for Files. |
||
61 | |||
62 | |||
63 | def open_repo(name, temp_dir=None): |
||
64 | """Open a copy of a repo in a temporary directory.
|
||
65 |
|
||
66 | Use this function for accessing repos in dulwich/tests/data/repos to avoid
|
||
67 | accidentally or intentionally modifying those repos in place. Use
|
||
68 | tear_down_repo to delete any temp files created.
|
||
69 |
|
||
70 | :param name: The name of the repository, relative to
|
||
71 | dulwich/tests/data/repos
|
||
72 | :param temp_dir: temporary directory to initialize to. If not provided, a
|
||
73 | temporary directory will be created.
|
||
74 | :returns: An initialized Repo object that lives in a temporary directory.
|
||
75 | """
|
||
76 | if temp_dir is None: |
||
77 | temp_dir = tempfile.mkdtemp() |
||
78 | repo_dir = os.path.join(os.path.dirname(__file__), 'data', 'repos', name) |
||
79 | temp_repo_dir = os.path.join(temp_dir, name) |
||
80 | shutil.copytree(repo_dir, temp_repo_dir, symlinks=True)
|
||
81 | return Repo(temp_repo_dir)
|
||
82 | |||
83 | |||
84 | def tear_down_repo(repo): |
||
85 | """Tear down a test repository."""
|
||
86 | repo.close() |
||
87 | temp_dir = os.path.dirname(repo.path.rstrip(os.sep)) |
||
88 | shutil.rmtree(temp_dir) |
||
89 | |||
90 | |||
91 | def make_object(cls, **attrs): |
||
92 | """Make an object for testing and assign some members.
|
||
93 |
|
||
94 | This method creates a new subclass to allow arbitrary attribute
|
||
95 | reassignment, which is not otherwise possible with objects having __slots__.
|
||
96 |
|
||
97 | :param attrs: dict of attributes to set on the new object.
|
||
98 | :return: A newly initialized object of type cls.
|
||
99 | """
|
||
100 | |||
101 | class TestObject(cls): |
||
102 | """Class that inherits from the given class, but without __slots__.
|
||
103 |
|
||
104 | Note that classes with __slots__ can't have arbitrary attributes monkey-
|
||
105 | patched in, so this is a class that is exactly the same only with a
|
||
106 | __dict__ instead of __slots__.
|
||
107 | """
|
||
108 | pass
|
||
109 | TestObject.__name__ = 'TestObject_' + cls.__name__
|
||
110 | |||
111 | obj = TestObject() |
||
112 | for name, value in attrs.items(): |
||
113 | if name == 'id': |
||
114 | # id property is read-only, so we overwrite sha instead.
|
||
115 | sha = FixedSha(value) |
||
116 | obj.sha = lambda: sha
|
||
117 | else:
|
||
118 | setattr(obj, name, value)
|
||
119 | return obj
|
||
120 | |||
121 | |||
122 | def make_commit(**attrs): |
||
123 | """Make a Commit object with a default set of members.
|
||
124 |
|
||
125 | :param attrs: dict of attributes to overwrite from the default values.
|
||
126 | :return: A newly initialized Commit object.
|
||
127 | """
|
||
128 | default_time = int(time.mktime(datetime.datetime(2010, 1, 1).timetuple())) |
||
129 | all_attrs = {'author': b'Test Author <test@nodomain.com>', |
||
130 | 'author_time': default_time,
|
||
131 | 'author_timezone': 0, |
||
132 | 'committer': b'Test Committer <test@nodomain.com>', |
||
133 | 'commit_time': default_time,
|
||
134 | 'commit_timezone': 0, |
||
135 | 'message': b'Test message.', |
||
136 | 'parents': [],
|
||
137 | 'tree': b'0' * 40} |
||
138 | all_attrs.update(attrs) |
||
139 | return make_object(Commit, **all_attrs)
|
||
140 | |||
141 | |||
142 | def make_tag(target, **attrs): |
||
143 | """Make a Tag object with a default set of values.
|
||
144 |
|
||
145 | :param target: object to be tagged (Commit, Blob, Tree, etc)
|
||
146 | :param attrs: dict of attributes to overwrite from the default values.
|
||
147 | :return: A newly initialized Tag object.
|
||
148 | """
|
||
149 | target_id = target.id |
||
150 | target_type = object_class(target.type_name) |
||
151 | default_time = int(time.mktime(datetime.datetime(2010, 1, 1).timetuple())) |
||
152 | all_attrs = {'tagger': b'Test Author <test@nodomain.com>', |
||
153 | 'tag_time': default_time,
|
||
154 | 'tag_timezone': 0, |
||
155 | 'message': b'Test message.', |
||
156 | 'object': (target_type, target_id),
|
||
157 | 'name': b'Test Tag', |
||
158 | } |
||
159 | all_attrs.update(attrs) |
||
160 | return make_object(Tag, **all_attrs)
|
||
161 | |||
162 | |||
163 | def functest_builder(method, func): |
||
164 | """Generate a test method that tests the given function."""
|
||
165 | |||
166 | def do_test(self): |
||
167 | method(self, func)
|
||
168 | |||
169 | return do_test
|
||
170 | |||
171 | |||
172 | def ext_functest_builder(method, func): |
||
173 | """Generate a test method that tests the given extension function.
|
||
174 |
|
||
175 | This is intended to generate test methods that test both a pure-Python
|
||
176 | version and an extension version using common test code. The extension test
|
||
177 | will raise SkipTest if the extension is not found.
|
||
178 |
|
||
179 | Sample usage:
|
||
180 |
|
||
181 | class MyTest(TestCase);
|
||
182 | def _do_some_test(self, func_impl):
|
||
183 | self.assertEqual('foo', func_impl())
|
||
184 |
|
||
185 | test_foo = functest_builder(_do_some_test, foo_py)
|
||
186 | test_foo_extension = ext_functest_builder(_do_some_test, _foo_c)
|
||
187 |
|
||
188 | :param method: The method to run. It must must two parameters, self and the
|
||
189 | function implementation to test.
|
||
190 | :param func: The function implementation to pass to method.
|
||
191 | """
|
||
192 | |||
193 | def do_test(self): |
||
194 | if not isinstance(func, types.BuiltinFunctionType): |
||
195 | raise SkipTest("%s extension not found" % func) |
||
196 | method(self, func)
|
||
197 | |||
198 | return do_test
|
||
199 | |||
200 | |||
201 | def build_pack(f, objects_spec, store=None): |
||
202 | """Write test pack data from a concise spec.
|
||
203 |
|
||
204 | :param f: A file-like object to write the pack to.
|
||
205 | :param objects_spec: A list of (type_num, obj). For non-delta types, obj
|
||
206 | is the string of that object's data.
|
||
207 | For delta types, obj is a tuple of (base, data), where:
|
||
208 |
|
||
209 | * base can be either an index in objects_spec of the base for that
|
||
210 | * delta; or for a ref delta, a SHA, in which case the resulting pack
|
||
211 | * will be thin and the base will be an external ref.
|
||
212 | * data is a string of the full, non-deltified data for that object.
|
||
213 |
|
||
214 | Note that offsets/refs and deltas are computed within this function.
|
||
215 | :param store: An optional ObjectStore for looking up external refs.
|
||
216 | :return: A list of tuples in the order specified by objects_spec:
|
||
217 | (offset, type num, data, sha, CRC32)
|
||
218 | """
|
||
219 | sf = SHA1Writer(f) |
||
220 | num_objects = len(objects_spec)
|
||
221 | write_pack_header(sf, num_objects) |
||
222 | |||
223 | full_objects = {} |
||
224 | offsets = {} |
||
225 | crc32s = {} |
||
226 | |||
227 | while len(full_objects) < num_objects: |
||
228 | for i, (type_num, data) in enumerate(objects_spec): |
||
229 | if type_num not in DELTA_TYPES: |
||
230 | full_objects[i] = (type_num, data, |
||
231 | obj_sha(type_num, [data])) |
||
232 | continue
|
||
233 | base, data = data |
||
234 | if isinstance(base, int): |
||
235 | if base not in full_objects: |
||
236 | continue
|
||
237 | base_type_num, _, _ = full_objects[base] |
||
238 | else:
|
||
239 | base_type_num, _ = store.get_raw(base) |
||
240 | full_objects[i] = (base_type_num, data, |
||
241 | obj_sha(base_type_num, [data])) |
||
242 | |||
243 | for i, (type_num, obj) in enumerate(objects_spec): |
||
244 | offset = f.tell() |
||
245 | if type_num == OFS_DELTA:
|
||
246 | base_index, data = obj |
||
247 | base = offset - offsets[base_index] |
||
248 | _, base_data, _ = full_objects[base_index] |
||
249 | obj = (base, create_delta(base_data, data)) |
||
250 | elif type_num == REF_DELTA:
|
||
251 | base_ref, data = obj |
||
252 | if isinstance(base_ref, int): |
||
253 | _, base_data, base = full_objects[base_ref] |
||
254 | else:
|
||
255 | base_type_num, base_data = store.get_raw(base_ref) |
||
256 | base = obj_sha(base_type_num, base_data) |
||
257 | obj = (base, create_delta(base_data, data)) |
||
258 | |||
259 | crc32 = write_pack_object(sf, type_num, obj) |
||
260 | offsets[i] = offset |
||
261 | crc32s[i] = crc32 |
||
262 | |||
263 | expected = [] |
||
264 | for i in range(num_objects): |
||
265 | type_num, data, sha = full_objects[i] |
||
266 | assert len(sha) == 20 |
||
267 | expected.append((offsets[i], type_num, data, sha, crc32s[i])) |
||
268 | |||
269 | sf.write_sha() |
||
270 | f.seek(0)
|
||
271 | return expected
|
||
272 | |||
273 | |||
274 | def build_commit_graph(object_store, commit_spec, trees=None, attrs=None): |
||
275 | """Build a commit graph from a concise specification.
|
||
276 |
|
||
277 | Sample usage:
|
||
278 | >>> c1, c2, c3 = build_commit_graph(store, [[1], [2, 1], [3, 1, 2]])
|
||
279 | >>> store[store[c3].parents[0]] == c1
|
||
280 | True
|
||
281 | >>> store[store[c3].parents[1]] == c2
|
||
282 | True
|
||
283 |
|
||
284 | If not otherwise specified, commits will refer to the empty tree and have
|
||
285 | commit times increasing in the same order as the commit spec.
|
||
286 |
|
||
287 | :param object_store: An ObjectStore to commit objects to.
|
||
288 | :param commit_spec: An iterable of iterables of ints defining the commit
|
||
289 | graph. Each entry defines one commit, and entries must be in topological
|
||
290 | order. The first element of each entry is a commit number, and the
|
||
291 | remaining elements are its parents. The commit numbers are only
|
||
292 | meaningful for the call to make_commits; since real commit objects are
|
||
293 | created, they will get created with real, opaque SHAs.
|
||
294 | :param trees: An optional dict of commit number -> tree spec for building
|
||
295 | trees for commits. The tree spec is an iterable of (path, blob, mode) or
|
||
296 | (path, blob) entries; if mode is omitted, it defaults to the normal file
|
||
297 | mode (0100644).
|
||
298 | :param attrs: A dict of commit number -> (dict of attribute -> value) for
|
||
299 | assigning additional values to the commits.
|
||
300 | :return: The list of commit objects created.
|
||
301 | :raise ValueError: If an undefined commit identifier is listed as a parent.
|
||
302 | """
|
||
303 | if trees is None: |
||
304 | trees = {} |
||
305 | if attrs is None: |
||
306 | attrs = {} |
||
307 | commit_time = 0
|
||
308 | nums = {} |
||
309 | commits = [] |
||
310 | |||
311 | for commit in commit_spec: |
||
312 | commit_num = commit[0]
|
||
313 | try:
|
||
314 | parent_ids = [nums[pn] for pn in commit[1:]] |
||
315 | except KeyError as e: |
||
316 | missing_parent, = e.args |
||
317 | raise ValueError('Unknown parent %i' % missing_parent) |
||
318 | |||
319 | blobs = [] |
||
320 | for entry in trees.get(commit_num, []): |
||
321 | if len(entry) == 2: |
||
322 | path, blob = entry |
||
323 | entry = (path, blob, F) |
||
324 | path, blob, mode = entry |
||
325 | blobs.append((path, blob.id, mode)) |
||
326 | object_store.add_object(blob) |
||
327 | tree_id = commit_tree(object_store, blobs) |
||
328 | |||
329 | commit_attrs = { |
||
330 | 'message': ('Commit %i' % commit_num).encode('ascii'), |
||
331 | 'parents': parent_ids,
|
||
332 | 'tree': tree_id,
|
||
333 | 'commit_time': commit_time,
|
||
334 | } |
||
335 | commit_attrs.update(attrs.get(commit_num, {})) |
||
336 | commit_obj = make_commit(**commit_attrs) |
||
337 | |||
338 | # By default, increment the time by a lot. Out-of-order commits should
|
||
339 | # be closer together than this because their main cause is clock skew.
|
||
340 | commit_time = commit_attrs['commit_time'] + 100 |
||
341 | nums[commit_num] = commit_obj.id |
||
342 | object_store.add_object(commit_obj) |
||
343 | commits.append(commit_obj) |
||
344 | |||
345 | return commits
|
||
346 | |||
347 | |||
348 | def setup_warning_catcher(): |
||
349 | """Wrap warnings.showwarning with code that records warnings."""
|
||
350 | |||
351 | caught_warnings = [] |
||
352 | original_showwarning = warnings.showwarning |
||
353 | |||
354 | def custom_showwarning(*args, **kwargs): |
||
355 | caught_warnings.append(args[0])
|
||
356 | |||
357 | warnings.showwarning = custom_showwarning |
||
358 | |||
359 | def restore_showwarning(): |
||
360 | warnings.showwarning = original_showwarning |
||
361 | |||
362 | return caught_warnings, restore_showwarning |