1# 2# 3# Licensed to the Apache Software Foundation (ASF) under one 4# or more contributor license agreements. See the NOTICE file 5# distributed with this work for additional information 6# regarding copyright ownership. The ASF licenses this file 7# to you under the Apache License, Version 2.0 (the 8# "License"); you may not use this file except in compliance 9# with the License. You may obtain a copy of the License at 10# 11# http://www.apache.org/licenses/LICENSE-2.0 12# 13# Unless required by applicable law or agreed to in writing, 14# software distributed under the License is distributed on an 15# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16# KIND, either express or implied. See the License for the 17# specific language governing permissions and limitations 18# under the License. 19# 20# 21import unittest, setup_path, os, sys 22from sys import version_info # For Python version check 23from io import BytesIO 24from svn import core, repos, fs, delta 25from svn.core import SubversionException 26import utils 27 28class ChangeReceiver(delta.Editor): 29 """A delta editor which saves textdeltas for later use""" 30 31 def __init__(self, src_root, tgt_root): 32 self.src_root = src_root 33 self.tgt_root = tgt_root 34 self.textdeltas = [] 35 36 def apply_textdelta(self, file_baton, base_checksum, pool=None): 37 def textdelta_handler(textdelta): 38 if textdelta is not None: 39 self.textdeltas.append(textdelta) 40 return textdelta_handler 41 42class DumpStreamParser(repos.ParseFns3): 43 def __init__(self): 44 repos.ParseFns3.__init__(self) 45 self.ops = [] 46 def magic_header_record(self, version, pool=None): 47 self.ops.append((b"magic-header", version)) 48 def uuid_record(self, uuid, pool=None): 49 self.ops.append((b"uuid", uuid)) 50 def new_revision_record(self, headers, pool=None): 51 rev = int(headers[repos.DUMPFILE_REVISION_NUMBER]) 52 self.ops.append((b"new-revision", rev)) 53 return rev 54 def close_revision(self, revision_baton): 55 self.ops.append((b"close-revision", revision_baton)) 56 def new_node_record(self, headers, revision_baton, pool=None): 57 node = headers[repos.DUMPFILE_NODE_PATH] 58 self.ops.append((b"new-node", revision_baton, node)) 59 return (revision_baton, node) 60 def close_node(self, node_baton): 61 self.ops.append((b"close-node", node_baton[0], node_baton[1])) 62 def set_revision_property(self, revision_baton, name, value): 63 self.ops.append((b"set-revision-prop", revision_baton, name, value)) 64 def set_node_property(self, node_baton, name, value): 65 self.ops.append((b"set-node-prop", node_baton[0], node_baton[1], name, value)) 66 def remove_node_props(self, node_baton): 67 self.ops.append((b"remove-node-props", node_baton[0], node_baton[1])) 68 def delete_node_property(self, node_baton, name): 69 self.ops.append((b"delete-node-prop", node_baton[0], node_baton[1], name)) 70 def apply_textdelta(self, node_baton): 71 self.ops.append((b"apply-textdelta", node_baton[0], node_baton[1])) 72 return None 73 def set_fulltext(self, node_baton): 74 self.ops.append((b"set-fulltext", node_baton[0], node_baton[1])) 75 return None 76 77 78def _authz_callback(root, path, pool): 79 "A dummy authz callback which always returns success." 80 return 1 81 82class SubversionRepositoryTestCase(unittest.TestCase): 83 """Test cases for the Subversion repository layer""" 84 85 def setUp(self): 86 """Load a Subversion repository""" 87 self.temper = utils.Temper() 88 (self.repos, self.repos_path, _) = self.temper.alloc_known_repo( 89 'trac/versioncontrol/tests/svnrepos.dump', suffix='-repository') 90 self.fs = repos.fs(self.repos) 91 self.rev = fs.youngest_rev(self.fs) 92 93 def tearDown(self): 94 self.fs = None 95 self.repos = None 96 self.temper.cleanup() 97 98 def test_cease_invocation(self): 99 """Test returning SVN_ERR_CEASE_INVOCATION from a callback""" 100 101 revs = [] 102 def history_lookup(path, rev, pool): 103 revs.append(rev) 104 raise core.SubversionException(apr_err=core.SVN_ERR_CEASE_INVOCATION, 105 message="Hi from history_lookup") 106 107 repos.history2(self.fs, b'/trunk/README2.txt', history_lookup, None, 0, 108 self.rev, True) 109 self.assertEqual(len(revs), 1) 110 111 def test_create(self): 112 """Make sure that repos.create doesn't segfault when we set fs-type 113 using a config hash""" 114 fs_config = { b"fs-type": b"fsfs" } 115 for i in range(5): 116 path = self.temper.alloc_empty_dir(suffix='-repository-create%d' % i) 117 repos.create(path, b"", b"", None, fs_config) 118 119 def test_dump_fs2(self): 120 """Test the dump_fs2 function""" 121 122 self.callback_calls = 0 123 124 def is_cancelled(): 125 self.callback_calls += 1 126 return None 127 128 dumpstream = BytesIO() 129 feedbackstream = BytesIO() 130 repos.dump_fs2(self.repos, dumpstream, feedbackstream, 0, self.rev, 0, 0, 131 is_cancelled) 132 133 # Check that we can dump stuff 134 dump = dumpstream.getvalue() 135 feedback = feedbackstream.getvalue() 136 expected_feedback = b"* Dumped revision " + str(self.rev).encode('utf-8') 137 self.assertEqual(dump.count(b"Node-path: trunk/README.txt"), 2) 138 self.assertEqual(feedback.count(expected_feedback), 1) 139 self.assertEqual(self.callback_calls, 13) 140 141 # Check that the dump can be cancelled 142 self.assertRaises(SubversionException, repos.dump_fs2, 143 self.repos, dumpstream, feedbackstream, 0, self.rev, 0, 0, lambda: 1) 144 145 dumpstream.close() 146 feedbackstream.close() 147 148 # Check that the dump fails when the dumpstream is closed 149 self.assertRaises(ValueError, repos.dump_fs2, 150 self.repos, dumpstream, feedbackstream, 0, self.rev, 0, 0, None) 151 152 dumpstream = BytesIO() 153 feedbackstream = BytesIO() 154 155 # Check that we can grab the feedback stream, but not the dumpstream 156 repos.dump_fs2(self.repos, None, feedbackstream, 0, self.rev, 0, 0, None) 157 feedback = feedbackstream.getvalue() 158 self.assertEqual(feedback.count(expected_feedback), 1) 159 160 # Check that we can grab the dumpstream, but not the feedbackstream 161 repos.dump_fs2(self.repos, dumpstream, None, 0, self.rev, 0, 0, None) 162 dump = dumpstream.getvalue() 163 self.assertEqual(dump.count(b"Node-path: trunk/README.txt"), 2) 164 165 # Check that we can ignore both the dumpstream and the feedbackstream 166 repos.dump_fs2(self.repos, dumpstream, None, 0, self.rev, 0, 0, None) 167 self.assertEqual(feedback.count(expected_feedback), 1) 168 169 # FIXME: The Python bindings don't check for 'NULL' values for 170 # svn_repos_t objects, so the following call segfaults 171 #repos.dump_fs2(None, None, None, 0, self.rev, 0, 0, None) 172 173 def test_parse_fns3(self): 174 self.cancel_calls = 0 175 def is_cancelled(): 176 self.cancel_calls += 1 177 return None 178 dump_path = os.path.join(os.path.dirname(sys.argv[0]), 179 "trac/versioncontrol/tests/svnrepos.dump") 180 stream = open(dump_path, 'rb') 181 dsp = DumpStreamParser() 182 ptr, baton = repos.make_parse_fns3(dsp) 183 repos.parse_dumpstream3(stream, ptr, baton, False, is_cancelled) 184 stream.close() 185 self.assertEqual(self.cancel_calls, 76) 186 expected_list = [ 187 (b"magic-header", 2), 188 (b'uuid', b'92ea810a-adf3-0310-b540-bef912dcf5ba'), 189 (b'new-revision', 0), 190 (b'set-revision-prop', 0, b'svn:date', b'2005-04-01T09:57:41.312767Z'), 191 (b'close-revision', 0), 192 (b'new-revision', 1), 193 (b'set-revision-prop', 1, b'svn:log', b'Initial directory layout.'), 194 (b'set-revision-prop', 1, b'svn:author', b'john'), 195 (b'set-revision-prop', 1, b'svn:date', b'2005-04-01T10:00:52.353248Z'), 196 (b'new-node', 1, b'branches'), 197 (b'remove-node-props', 1, b'branches'), 198 (b'close-node', 1, b'branches'), 199 (b'new-node', 1, b'tags'), 200 (b'remove-node-props', 1, b'tags'), 201 (b'close-node', 1, b'tags'), 202 (b'new-node', 1, b'trunk'), 203 (b'remove-node-props', 1, b'trunk'), 204 (b'close-node', 1, b'trunk'), 205 (b'close-revision', 1), 206 (b'new-revision', 2), 207 (b'set-revision-prop', 2, b'svn:log', b'Added README.'), 208 (b'set-revision-prop', 2, b'svn:author', b'john'), 209 (b'set-revision-prop', 2, b'svn:date', b'2005-04-01T13:12:18.216267Z'), 210 (b'new-node', 2, b'trunk/README.txt'), 211 (b'remove-node-props', 2, b'trunk/README.txt'), 212 (b'set-fulltext', 2, b'trunk/README.txt'), 213 (b'close-node', 2, b'trunk/README.txt'), 214 (b'close-revision', 2), (b'new-revision', 3), 215 (b'set-revision-prop', 3, b'svn:log', b'Fixed README.\n'), 216 (b'set-revision-prop', 3, b'svn:author', b'kate'), 217 (b'set-revision-prop', 3, b'svn:date', b'2005-04-01T13:24:58.234643Z'), 218 (b'new-node', 3, b'trunk/README.txt'), 219 (b'remove-node-props', 3, b'trunk/README.txt'), 220 (b'set-node-prop', 3, b'trunk/README.txt', b'svn:mime-type', b'text/plain'), 221 (b'set-node-prop', 3, b'trunk/README.txt', b'svn:eol-style', b'native'), 222 (b'set-fulltext', 3, b'trunk/README.txt'), 223 (b'close-node', 3, b'trunk/README.txt'), (b'close-revision', 3), 224 ] 225 # Compare only the first X nodes described in the expected list - otherwise 226 # the comparison list gets too long. 227 self.assertEqual(dsp.ops[:len(expected_list)], expected_list) 228 229 def test_parse_fns3_invalid_set_fulltext(self): 230 class DumpStreamParserSubclass(DumpStreamParser): 231 def set_fulltext(self, node_baton): 232 DumpStreamParser.set_fulltext(self, node_baton) 233 return 42 234 stream = open(os.path.join(os.path.dirname(sys.argv[0]), 235 "trac/versioncontrol/tests/svnrepos.dump"), "rb") 236 try: 237 dsp = DumpStreamParserSubclass() 238 ptr, baton = repos.make_parse_fns3(dsp) 239 self.assertRaises(TypeError, repos.parse_dumpstream3, 240 stream, ptr, baton, False, None) 241 finally: 242 stream.close() 243 244 def test_get_logs(self): 245 """Test scope of get_logs callbacks""" 246 logs = [] 247 def addLog(paths, revision, author, date, message, pool): 248 if paths is not None: 249 logs.append(paths) 250 251 # Run get_logs 252 repos.get_logs(self.repos, [b'/'], self.rev, 0, True, 0, addLog) 253 254 # Count and verify changes 255 change_count = 0 256 for log in logs: 257 for path_changed in core._as_list(log.values()): 258 change_count += 1 259 path_changed.assert_valid() 260 self.assertEqual(logs[2][b"/tags/v1.1"].action, b"A") 261 self.assertEqual(logs[2][b"/tags/v1.1"].copyfrom_path, b"/branches/v1x") 262 self.assertEqual(len(logs), 12) 263 self.assertEqual(change_count, 19) 264 265 def test_dir_delta(self): 266 """Test scope of dir_delta callbacks""" 267 # Run dir_delta 268 this_root = fs.revision_root(self.fs, self.rev) 269 prev_root = fs.revision_root(self.fs, self.rev-1) 270 editor = ChangeReceiver(this_root, prev_root) 271 e_ptr, e_baton = delta.make_editor(editor) 272 repos.dir_delta(prev_root, b'', b'', this_root, b'', e_ptr, e_baton, 273 _authz_callback, 1, 1, 0, 0) 274 275 # Check results. 276 # Ignore the order in which the editor delivers the two sibling files. 277 self.assertEqual(set([editor.textdeltas[0].new_data, 278 editor.textdeltas[1].new_data]), 279 set([b"This is a test.\n", b"A test.\n"])) 280 self.assertEqual(len(editor.textdeltas), 2) 281 282 def test_unnamed_editor(self): 283 """Test editor object without reference from interpreter""" 284 # Check that the delta.Editor object has proper lifetime. Without 285 # increment of the refcount in make_baton, the object was destroyed 286 # immediately because the interpreter does not hold a reference to it. 287 this_root = fs.revision_root(self.fs, self.rev) 288 prev_root = fs.revision_root(self.fs, self.rev-1) 289 e_ptr, e_baton = delta.make_editor(ChangeReceiver(this_root, prev_root)) 290 repos.dir_delta(prev_root, b'', b'', this_root, b'', e_ptr, e_baton, 291 _authz_callback, 1, 1, 0, 0) 292 293 def test_retrieve_and_change_rev_prop(self): 294 """Test playing with revprops""" 295 self.assertEqual(repos.fs_revision_prop(self.repos, self.rev, b"svn:log", 296 _authz_callback), 297 b"''(a few years later)'' Argh... v1.1 was buggy, " 298 b"after all") 299 300 # We expect this to complain because we have no pre-revprop-change 301 # hook script for the repository. 302 self.assertRaises(SubversionException, repos.fs_change_rev_prop3, 303 self.repos, self.rev, b"jrandom", b"svn:log", 304 b"Youngest revision", True, True, _authz_callback) 305 306 repos.fs_change_rev_prop3(self.repos, self.rev, b"jrandom", b"svn:log", 307 b"Youngest revision", False, False, 308 _authz_callback) 309 310 self.assertEqual(repos.fs_revision_prop(self.repos, self.rev, b"svn:log", 311 _authz_callback), 312 b"Youngest revision") 313 314 def freeze_body(self, pool): 315 self.freeze_invoked += 1 316 317 def test_freeze(self): 318 """Test repository freeze""" 319 320 self.freeze_invoked = 0 321 repos.freeze([self.repos_path], self.freeze_body) 322 self.assertEqual(self.freeze_invoked, 1) 323 324 def test_lock_unlock(self): 325 """Basic lock/unlock""" 326 327 access = fs.create_access(b'jrandom') 328 fs.set_access(self.fs, access) 329 fs.lock(self.fs, b'/trunk/README.txt', None, None, 0, 0, self.rev, False) 330 try: 331 fs.lock(self.fs, b'/trunk/README.txt', None, None, 0, 0, self.rev, False) 332 except core.SubversionException as exc: 333 self.assertEqual(exc.apr_err, core.SVN_ERR_FS_PATH_ALREADY_LOCKED) 334 fs.lock(self.fs, b'/trunk/README.txt', None, None, 0, 0, self.rev, True) 335 336 self.calls = 0 337 self.errors = 0 338 def unlock_callback(path, lock, err, pool): 339 self.assertEqual(path, b'/trunk/README.txt') 340 self.assertEqual(lock, None) 341 self.calls += 1 342 if err != None: 343 self.assertEqual(err.apr_err, core.SVN_ERR_FS_NO_SUCH_LOCK) 344 self.errors += 1 345 346 the_lock = fs.get_lock(self.fs, b'/trunk/README.txt') 347 fs.unlock_many(self.fs, {b'/trunk/README.txt':the_lock.token}, False, 348 unlock_callback) 349 self.assertEqual(self.calls, 1) 350 self.assertEqual(self.errors, 0) 351 352 self.calls = 0 353 fs.unlock_many(self.fs, {b'/trunk/README.txt':the_lock.token}, False, 354 unlock_callback) 355 self.assertEqual(self.calls, 1) 356 self.assertEqual(self.errors, 1) 357 358 self.locks = 0 359 def lock_callback(path, lock, err, pool): 360 self.assertEqual(path, b'/trunk/README.txt') 361 if lock != None: 362 self.assertEqual(lock.owner, b'jrandom') 363 self.locks += 1 364 self.calls += 1 365 if err != None: 366 self.assertEqual(err.apr_err, core.SVN_ERR_FS_PATH_ALREADY_LOCKED) 367 self.errors += 1 368 369 self.calls = 0 370 self.errors = 0 371 target = fs.lock_target_create(None, self.rev) 372 fs.lock_many(self.fs, {b'trunk/README.txt':target}, 373 None, False, 0, False, lock_callback) 374 self.assertEqual(self.calls, 1) 375 self.assertEqual(self.locks, 1) 376 self.assertEqual(self.errors, 0) 377 378 self.calls = 0 379 self.locks = 0 380 fs.lock_many(self.fs, {b'trunk/README.txt':target}, 381 None, False, 0, False, lock_callback) 382 self.assertEqual(self.calls, 1) 383 self.assertEqual(self.locks, 0) 384 self.assertEqual(self.errors, 1) 385 386 self.calls = 0 387 self.errors = 0 388 the_lock = fs.get_lock(self.fs, b'/trunk/README.txt') 389 repos.fs_unlock_many(self.repos, {b'trunk/README.txt':the_lock.token}, 390 False, unlock_callback) 391 self.assertEqual(self.calls, 1) 392 self.assertEqual(self.errors, 0) 393 394 self.calls = 0 395 repos.fs_unlock_many(self.repos, {b'trunk/README.txt':the_lock.token}, 396 False, unlock_callback) 397 self.assertEqual(self.calls, 1) 398 self.assertEqual(self.errors, 1) 399 400 self.calls = 0 401 self.errors = 0 402 repos.fs_lock_many(self.repos, {b'trunk/README.txt':target}, 403 None, False, 0, False, lock_callback) 404 self.assertEqual(self.calls, 1) 405 self.assertEqual(self.locks, 1) 406 self.assertEqual(self.errors, 0) 407 408 self.calls = 0 409 self.locks = 0 410 repos.fs_lock_many(self.repos, {b'trunk/README.txt':target}, 411 None, False, 0, False, lock_callback) 412 self.assertEqual(self.calls, 1) 413 self.assertEqual(self.locks, 0) 414 self.assertEqual(self.errors, 1) 415 416def suite(): 417 return unittest.defaultTestLoader.loadTestsFromTestCase( 418 SubversionRepositoryTestCase) 419 420if __name__ == '__main__': 421 runner = unittest.TextTestRunner() 422 runner.run(suite()) 423