diff examples/trac/trac/versioncontrol/tests/svn_fs.py @ 39:93b4dcbafd7b trunk

Copy Trac to main branch.
author cmlenz
date Mon, 03 Jul 2006 18:53:27 +0000
parents
children
line wrap: on
line diff
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/versioncontrol/tests/svn_fs.py
@@ -0,0 +1,713 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2005 Edgewall Software
+# Copyright (C) 2005 Christopher Lenz <cmlenz@gmx.de>
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+#
+# Author: Christopher Lenz <cmlenz@gmx.de>
+
+import os.path
+import stat
+import shutil
+import sys
+import tempfile
+import unittest
+
+from StringIO import StringIO
+
+try:
+    from svn import core, repos
+    has_svn = True
+except:
+    has_svn = False
+
+from trac.log import logger_factory
+from trac.test import TestSetup
+from trac.core import TracError
+from trac.versioncontrol import Changeset, Node
+from trac.versioncontrol.svn_fs import SubversionRepository
+
+REPOS_PATH = os.path.join(tempfile.gettempdir(), 'trac-svnrepos')
+
+
+class SubversionRepositoryTestSetup(TestSetup):
+
+    def setUp(self):
+        dumpfile = open(os.path.join(os.path.split(__file__)[0],
+                                     'svnrepos.dump'))
+
+        core.apr_initialize()
+        pool = core.svn_pool_create(None)
+        dumpstream = None
+        try:
+            r = repos.svn_repos_create(REPOS_PATH, '', '', None, None, pool)
+            if hasattr(repos, 'svn_repos_load_fs2'):
+                repos.svn_repos_load_fs2(r, dumpfile, StringIO(),
+                                        repos.svn_repos_load_uuid_default, '',
+                                        0, 0, None, pool)
+            else:
+                dumpstream = core.svn_stream_from_aprfile(dumpfile, pool)
+                repos.svn_repos_load_fs(r, dumpstream, None,
+                                        repos.svn_repos_load_uuid_default, '',
+                                        None, None, pool)
+        finally:
+            if dumpstream:
+                core.svn_stream_close(dumpstream)
+            core.svn_pool_destroy(pool)
+            core.apr_terminate()
+
+    def tearDown(self):
+        if os.name == 'nt':
+            # The Windows version of 'shutil.rmtree' doesn't override the
+            # permissions of read-only files, so we have to do it ourselves:
+            format_file = os.path.join(REPOS_PATH, 'db', 'format')
+            if os.path.isfile(format_file):
+                os.chmod(format_file, stat.S_IRWXU)
+            os.chmod(os.path.join(REPOS_PATH, 'format'), stat.S_IRWXU)
+        shutil.rmtree(REPOS_PATH)
+
+
+class SubversionRepositoryTestCase(unittest.TestCase):
+
+    def setUp(self):
+        self.repos = SubversionRepository(REPOS_PATH, None,
+                                          logger_factory('test'))
+
+    def tearDown(self):
+        self.repos = None
+
+    def test_repos_normalize_path(self):
+        self.assertEqual('/', self.repos.normalize_path('/'))
+        self.assertEqual('/', self.repos.normalize_path(''))
+        self.assertEqual('/', self.repos.normalize_path(None))
+        self.assertEqual('trunk', self.repos.normalize_path('trunk'))
+        self.assertEqual('trunk', self.repos.normalize_path('/trunk'))
+        self.assertEqual('trunk', self.repos.normalize_path('trunk/'))
+        self.assertEqual('trunk', self.repos.normalize_path('/trunk/'))
+
+    def test_repos_normalize_rev(self):
+        self.assertEqual(17, self.repos.normalize_rev('latest'))
+        self.assertEqual(17, self.repos.normalize_rev('head'))
+        self.assertEqual(17, self.repos.normalize_rev(''))
+        self.assertEqual(17, self.repos.normalize_rev(None))
+        self.assertEqual(11, self.repos.normalize_rev('11'))
+        self.assertEqual(11, self.repos.normalize_rev(11))
+
+    def test_rev_navigation(self):
+        self.assertEqual(1, self.repos.oldest_rev)
+        self.assertEqual(None, self.repos.previous_rev(0))
+        self.assertEqual(None, self.repos.previous_rev(1))
+        self.assertEqual(17, self.repos.youngest_rev)
+        self.assertEqual(6, self.repos.next_rev(5))
+        self.assertEqual(7, self.repos.next_rev(6))
+        # ...
+        self.assertEqual(None, self.repos.next_rev(17))
+
+    def test_rev_path_navigation(self):
+        self.assertEqual(1, self.repos.oldest_rev)
+        self.assertEqual(None, self.repos.previous_rev(0, 'trunk'))
+        self.assertEqual(None, self.repos.previous_rev(1, 'trunk'))
+        self.assertEqual(17, self.repos.youngest_rev)
+        self.assertEqual(6, self.repos.next_rev(5, 'trunk'))
+        self.assertEqual(13, self.repos.next_rev(6, 'trunk'))
+        # ...
+        self.assertEqual(None, self.repos.next_rev(17, 'trunk'))
+        # test accentuated characters
+        self.assertEqual(None, self.repos.previous_rev(17, u'trunk/R\xe9sum\xe9.txt'))
+        self.assertEqual(17, self.repos.next_rev(16, u'trunk/R\xe9sum\xe9.txt'))
+
+    def test_has_node(self):
+        self.assertEqual(False, self.repos.has_node('/trunk/dir1', 3))
+        self.assertEqual(True, self.repos.has_node('/trunk/dir1', 4))
+        
+    def test_get_node(self):
+        node = self.repos.get_node('/trunk')
+        self.assertEqual('trunk', node.name)
+        self.assertEqual('/trunk', node.path)
+        self.assertEqual(Node.DIRECTORY, node.kind)
+        self.assertEqual(17, node.rev)
+        self.assertEqual(1143808225L, node.last_modified)
+        node = self.repos.get_node('/trunk/README.txt')
+        self.assertEqual('README.txt', node.name)
+        self.assertEqual('/trunk/README.txt', node.path)
+        self.assertEqual(Node.FILE, node.kind)
+        self.assertEqual(3, node.rev)
+        self.assertEqual(1112361898, node.last_modified)
+
+    def test_get_node_specific_rev(self):
+        node = self.repos.get_node('/trunk', 1)
+        self.assertEqual('trunk', node.name)
+        self.assertEqual('/trunk', node.path)
+        self.assertEqual(Node.DIRECTORY, node.kind)
+        self.assertEqual(1, node.rev)
+        self.assertEqual(1112349652, node.last_modified)
+        node = self.repos.get_node('/trunk/README.txt', 2)
+        self.assertEqual('README.txt', node.name)
+        self.assertEqual('/trunk/README.txt', node.path)
+        self.assertEqual(Node.FILE, node.kind)
+        self.assertEqual(2, node.rev)
+        self.assertEqual(1112361138, node.last_modified)
+
+    def test_get_dir_entries(self):
+        node = self.repos.get_node('/trunk')
+        entries = node.get_entries()
+        self.assertEqual(u'R\xe9sum\xe9.txt', entries.next().name)
+        self.assertEqual('dir1', entries.next().name)
+        self.assertEqual('README3.txt', entries.next().name)
+        self.assertEqual('README.txt', entries.next().name)
+        self.assertRaises(StopIteration, entries.next)
+
+    def test_get_file_entries(self):
+        node = self.repos.get_node('/trunk/README.txt')
+        entries = node.get_entries()
+        self.assertRaises(StopIteration, entries.next)
+
+    def test_get_dir_content(self):
+        node = self.repos.get_node('/trunk')
+        self.assertEqual(None, node.content_length)
+        self.assertEqual(None, node.content_type)
+        self.assertEqual(None, node.get_content())
+
+    def test_get_file_content(self):
+        node = self.repos.get_node('/trunk/README.txt')
+        self.assertEqual(8, node.content_length)
+        self.assertEqual('text/plain', node.content_type)
+        self.assertEqual('A test.\n', node.get_content().read())
+
+    def test_get_dir_properties(self):
+        f = self.repos.get_node('/trunk')
+        props = f.get_properties()
+        self.assertEqual(1, len(props))
+
+    def test_get_file_properties(self):
+        f = self.repos.get_node('/trunk/README.txt')
+        props = f.get_properties()
+        self.assertEqual('native', props['svn:eol-style'])
+        self.assertEqual('text/plain', props['svn:mime-type'])
+
+    def test_created_path_rev(self):
+        node = self.repos.get_node('/trunk/README3.txt', 15)
+        self.assertEqual(14, node.rev)
+        self.assertEqual('/trunk/README3.txt', node.path)
+        self.assertEqual(14, node.created_rev)
+        self.assertEqual('trunk/README3.txt', node.created_path)
+
+    def test_created_path_rev_parent_copy(self):
+        node = self.repos.get_node('/tags/v1/README.txt', 15)
+        self.assertEqual(3, node.rev)
+        self.assertEqual('/tags/v1/README.txt', node.path)
+        self.assertEqual(3, node.created_rev)
+        self.assertEqual('trunk/README.txt', node.created_path)
+
+    # Revision Log / node history 
+
+    def test_get_node_history(self):
+        node = self.repos.get_node('/trunk/README3.txt')
+        history = node.get_history()
+        self.assertEqual(('trunk/README3.txt', 14, 'copy'), history.next())
+        self.assertEqual(('trunk/README2.txt', 6, 'copy'), history.next())
+        self.assertEqual(('trunk/README.txt', 3, 'edit'), history.next())
+        self.assertEqual(('trunk/README.txt', 2, 'add'), history.next())
+        self.assertRaises(StopIteration, history.next)
+
+    def test_get_node_history_follow_copy(self):
+        node = self.repos.get_node('/tags/v1/README.txt')
+        history = node.get_history()
+        self.assertEqual(('tags/v1/README.txt', 7, 'copy'), history.next())
+        self.assertEqual(('trunk/README.txt', 3, 'edit'), history.next())
+        self.assertEqual(('trunk/README.txt', 2, 'add'), history.next())
+        self.assertRaises(StopIteration, history.next)
+
+    # Revision Log / path history 
+
+    def test_get_path_history(self):
+        history = self.repos.get_path_history('/trunk/README2.txt', None)
+        self.assertEqual(('trunk/README2.txt', 14, 'delete'), history.next())
+        self.assertEqual(('trunk/README2.txt', 6, 'copy'), history.next())
+        self.assertEqual(('trunk/README.txt', 3, 'unknown'), history.next())
+        self.assertRaises(StopIteration, history.next)
+
+    def test_get_path_history_copied_file(self):
+        history = self.repos.get_path_history('/tags/v1/README.txt', None)
+        self.assertEqual(('tags/v1/README.txt', 7, 'copy'), history.next())
+        self.assertEqual(('trunk/README.txt', 3, 'unknown'), history.next())
+        self.assertRaises(StopIteration, history.next)
+        
+    def test_get_path_history_copied_dir(self):
+        history = self.repos.get_path_history('/branches/v1x', None)
+        self.assertEqual(('branches/v1x', 12, 'copy'), history.next())
+        self.assertEqual(('tags/v1.1', 10, 'unknown'), history.next())
+        self.assertEqual(('branches/v1x', 11, 'delete'), history.next())
+        self.assertEqual(('branches/v1x', 9, 'edit'), history.next())
+        self.assertEqual(('branches/v1x', 8, 'copy'), history.next())
+        self.assertEqual(('tags/v1', 7, 'unknown'), history.next())
+        self.assertRaises(StopIteration, history.next)
+
+    # Diffs
+
+    def _cmp_diff(self, expected, got):
+        if expected[0]:
+            old = self.repos.get_node(*expected[0])
+            self.assertEqual((old.path, old.rev), (got[0].path, got[0].rev))
+        if expected[1]:
+            new = self.repos.get_node(*expected[1])
+            self.assertEqual((new.path, new.rev), (got[1].path, got[1].rev))
+        self.assertEqual(expected[2], (got[2], got[3]))
+        
+    def test_diff_file_different_revs(self):
+        diffs = self.repos.get_changes('trunk/README.txt', 2, 'trunk/README.txt', 3)
+        self._cmp_diff((('trunk/README.txt', 2),
+                        ('trunk/README.txt', 3),
+                        (Node.FILE, Changeset.EDIT)), diffs.next())
+        self.assertRaises(StopIteration, diffs.next)
+
+    def test_diff_file_different_files(self):
+        diffs = self.repos.get_changes('branches/v1x/README.txt', 12,
+                                      'branches/v1x/README2.txt', 12)
+        self._cmp_diff((('branches/v1x/README.txt', 12),
+                        ('branches/v1x/README2.txt', 12),
+                        (Node.FILE, Changeset.EDIT)), diffs.next())
+        self.assertRaises(StopIteration, diffs.next)
+
+    def test_diff_file_no_change(self):
+        diffs = self.repos.get_changes('trunk/README.txt', 7,
+                                      'tags/v1/README.txt', 7)
+        self.assertRaises(StopIteration, diffs.next)
+ 
+    def test_diff_dir_different_revs(self):
+        diffs = self.repos.get_changes('trunk', 4, 'trunk', 8)
+        self._cmp_diff((None, ('trunk/dir1/dir2', 8),
+                        (Node.DIRECTORY, Changeset.ADD)), diffs.next())
+        self._cmp_diff((None, ('trunk/dir1/dir3', 8),
+                        (Node.DIRECTORY, Changeset.ADD)), diffs.next())
+        self._cmp_diff((None, ('trunk/README2.txt', 6),
+                        (Node.FILE, Changeset.ADD)), diffs.next())
+        self._cmp_diff((('trunk/dir2', 4), None,
+                        (Node.DIRECTORY, Changeset.DELETE)), diffs.next())
+        self._cmp_diff((('trunk/dir3', 4), None,
+                        (Node.DIRECTORY, Changeset.DELETE)), diffs.next())
+        self.assertRaises(StopIteration, diffs.next)
+
+    def test_diff_dir_different_dirs(self):
+        diffs = self.repos.get_changes('trunk', 1, 'branches/v1x', 12)
+        self._cmp_diff((None, ('branches/v1x/dir1', 12),
+                        (Node.DIRECTORY, Changeset.ADD)), diffs.next())
+        self._cmp_diff((None, ('branches/v1x/dir1/dir2', 12),
+                        (Node.DIRECTORY, Changeset.ADD)), diffs.next())
+        self._cmp_diff((None, ('branches/v1x/dir1/dir3', 12),
+                        (Node.DIRECTORY, Changeset.ADD)), diffs.next())
+        self._cmp_diff((None, ('branches/v1x/README.txt', 12),
+                        (Node.FILE, Changeset.ADD)), diffs.next())
+        self._cmp_diff((None, ('branches/v1x/README2.txt', 12),
+                        (Node.FILE, Changeset.ADD)), diffs.next())
+        self.assertRaises(StopIteration, diffs.next)
+
+    def test_diff_dir_no_change(self):
+        diffs = self.repos.get_changes('trunk', 7,
+                                      'tags/v1', 7)
+        self.assertRaises(StopIteration, diffs.next)
+        
+    # Changesets
+
+    def test_changeset_repos_creation(self):
+        chgset = self.repos.get_changeset(0)
+        self.assertEqual(0, chgset.rev)
+        self.assertEqual(None, chgset.message)
+        self.assertEqual(None, chgset.author)
+        self.assertEqual(1112349461, chgset.date)
+        self.assertRaises(StopIteration, chgset.get_changes().next)
+
+    def test_changeset_added_dirs(self):
+        chgset = self.repos.get_changeset(1)
+        self.assertEqual(1, chgset.rev)
+        self.assertEqual('Initial directory layout.', chgset.message)
+        self.assertEqual('john', chgset.author)
+        self.assertEqual(1112349652, chgset.date)
+
+        changes = chgset.get_changes()
+        self.assertEqual(('branches', Node.DIRECTORY, Changeset.ADD, None, -1),
+                         changes.next())
+        self.assertEqual(('tags', Node.DIRECTORY, Changeset.ADD, None, -1),
+                         changes.next())
+        self.assertEqual(('trunk', Node.DIRECTORY, Changeset.ADD, None, -1),
+                         changes.next())
+        self.assertRaises(StopIteration, changes.next)
+
+    def test_changeset_file_edit(self):
+        chgset = self.repos.get_changeset(3)
+        self.assertEqual(3, chgset.rev)
+        self.assertEqual('Fixed README.\n', chgset.message)
+        self.assertEqual('kate', chgset.author)
+        self.assertEqual(1112361898, chgset.date)
+
+        changes = chgset.get_changes()
+        self.assertEqual(('trunk/README.txt', Node.FILE, Changeset.EDIT,
+                          'trunk/README.txt', 2), changes.next())
+        self.assertRaises(StopIteration, changes.next)
+
+    def test_changeset_dir_moves(self):
+        chgset = self.repos.get_changeset(5)
+        self.assertEqual(5, chgset.rev)
+        self.assertEqual('Moved directories.', chgset.message)
+        self.assertEqual('kate', chgset.author)
+        self.assertEqual(1112372739, chgset.date)
+
+        changes = chgset.get_changes()
+        self.assertEqual(('trunk/dir1/dir2', Node.DIRECTORY, Changeset.MOVE,
+                          'trunk/dir2', 4), changes.next())
+        self.assertEqual(('trunk/dir1/dir3', Node.DIRECTORY, Changeset.MOVE,
+                          'trunk/dir3', 4), changes.next())
+        self.assertRaises(StopIteration, changes.next)
+
+    def test_changeset_file_copy(self):
+        chgset = self.repos.get_changeset(6)
+        self.assertEqual(6, chgset.rev)
+        self.assertEqual('More things to read', chgset.message)
+        self.assertEqual('john', chgset.author)
+        self.assertEqual(1112381806, chgset.date)
+
+        changes = chgset.get_changes()
+        self.assertEqual(('trunk/README2.txt', Node.FILE, Changeset.COPY,
+                          'trunk/README.txt', 3), changes.next())
+        self.assertRaises(StopIteration, changes.next)
+
+    def test_changeset_root_propset(self):
+        chgset = self.repos.get_changeset(13)
+        self.assertEqual(13, chgset.rev)
+        self.assertEqual('Setting property on the repository_dir root',
+                         chgset.message)
+        changes = chgset.get_changes()
+        self.assertEqual(('/', Node.DIRECTORY, Changeset.EDIT, '/', 12),
+                         changes.next())
+        self.assertEqual(('trunk', Node.DIRECTORY, Changeset.EDIT, 'trunk', 6),
+                         changes.next())
+        self.assertRaises(StopIteration, changes.next)
+
+    def test_changeset_base_path_rev(self):
+        chgset = self.repos.get_changeset(9)
+        self.assertEqual(9, chgset.rev)
+        changes = chgset.get_changes()
+        self.assertEqual(('branches/v1x/README.txt', Node.FILE,
+                          Changeset.EDIT, 'trunk/README.txt', 3),
+                         changes.next())
+        self.assertRaises(StopIteration, changes.next)
+
+    def test_changeset_rename_and_edit(self):
+        chgset = self.repos.get_changeset(14)
+        self.assertEqual(14, chgset.rev)
+        changes = chgset.get_changes()
+        self.assertEqual(('trunk/README3.txt', Node.FILE,
+                          Changeset.MOVE, 'trunk/README2.txt', 13),
+                         changes.next())
+        self.assertRaises(StopIteration, changes.next)
+
+    def test_changeset_edit_after_wc2wc_copy__original_deleted(self):
+        chgset = self.repos.get_changeset(16)
+        self.assertEqual(16, chgset.rev)
+        changes = chgset.get_changes()
+        self.assertEqual(('branches/v2', Node.DIRECTORY, Changeset.COPY,
+                          'tags/v1.1', 14),
+                         changes.next())
+        self.assertEqual(('branches/v2/README2.txt', Node.FILE,
+                          Changeset.EDIT, 'trunk/README2.txt', 6),
+                         changes.next())
+        self.assertRaises(StopIteration, changes.next)
+
+
+class ScopedSubversionRepositoryTestCase(unittest.TestCase):
+
+    def setUp(self):
+        self.repos = SubversionRepository(REPOS_PATH + '/trunk', None,
+                                          logger_factory('test'))
+
+    def tearDown(self):
+        self.repos = None
+
+    def test_repos_normalize_path(self):
+        self.assertEqual('/', self.repos.normalize_path('/'))
+        self.assertEqual('/', self.repos.normalize_path(''))
+        self.assertEqual('/', self.repos.normalize_path(None))
+        self.assertEqual('dir1', self.repos.normalize_path('dir1'))
+        self.assertEqual('dir1', self.repos.normalize_path('/dir1'))
+        self.assertEqual('dir1', self.repos.normalize_path('dir1/'))
+        self.assertEqual('dir1', self.repos.normalize_path('/dir1/'))
+
+    def test_repos_normalize_rev(self):
+        self.assertEqual(17, self.repos.normalize_rev('latest'))
+        self.assertEqual(17, self.repos.normalize_rev('head'))
+        self.assertEqual(17, self.repos.normalize_rev(''))
+        self.assertEqual(17, self.repos.normalize_rev(None))
+        self.assertEqual(5, self.repos.normalize_rev('5'))
+        self.assertEqual(5, self.repos.normalize_rev(5))
+
+    def test_rev_navigation(self):
+        self.assertEqual(1, self.repos.oldest_rev)
+        self.assertEqual(None, self.repos.previous_rev(0))
+        self.assertEqual(1, self.repos.previous_rev(2))
+        self.assertEqual(17, self.repos.youngest_rev)
+        self.assertEqual(2, self.repos.next_rev(1))
+        self.assertEqual(3, self.repos.next_rev(2))
+        # ...
+        self.assertEqual(None, self.repos.next_rev(17))
+
+    def test_has_node(self):
+        self.assertEqual(False, self.repos.has_node('/dir1', 3))
+        self.assertEqual(True, self.repos.has_node('/dir1', 4))
+
+    def test_get_node(self):
+        node = self.repos.get_node('/dir1')
+        self.assertEqual('dir1', node.name)
+        self.assertEqual('/dir1', node.path)
+        self.assertEqual(Node.DIRECTORY, node.kind)
+        self.assertEqual(5, node.rev)
+        self.assertEqual(1112372739, node.last_modified)
+        node = self.repos.get_node('/README.txt')
+        self.assertEqual('README.txt', node.name)
+        self.assertEqual('/README.txt', node.path)
+        self.assertEqual(Node.FILE, node.kind)
+        self.assertEqual(3, node.rev)
+        self.assertEqual(1112361898, node.last_modified)
+
+    def test_get_node_specific_rev(self):
+        node = self.repos.get_node('/dir1', 4)
+        self.assertEqual('dir1', node.name)
+        self.assertEqual('/dir1', node.path)
+        self.assertEqual(Node.DIRECTORY, node.kind)
+        self.assertEqual(4, node.rev)
+        self.assertEqual(1112370155, node.last_modified)
+        node = self.repos.get_node('/README.txt', 2)
+        self.assertEqual('README.txt', node.name)
+        self.assertEqual('/README.txt', node.path)
+        self.assertEqual(Node.FILE, node.kind)
+        self.assertEqual(2, node.rev)
+        self.assertEqual(1112361138, node.last_modified)
+
+    def test_get_dir_entries(self):
+        node = self.repos.get_node('/')
+        entries = node.get_entries()
+        self.assertEqual(u'R\xe9sum\xe9.txt', entries.next().name)
+        self.assertEqual('dir1', entries.next().name)
+        self.assertEqual('README3.txt', entries.next().name)
+        self.assertEqual('README.txt', entries.next().name)
+        self.assertRaises(StopIteration, entries.next)
+
+    def test_get_file_entries(self):
+        node = self.repos.get_node('/README.txt')
+        entries = node.get_entries()
+        self.assertRaises(StopIteration, entries.next)
+
+    def test_get_dir_content(self):
+        node = self.repos.get_node('/dir1')
+        self.assertEqual(None, node.content_length)
+        self.assertEqual(None, node.content_type)
+        self.assertEqual(None, node.get_content())
+
+    def test_get_file_content(self):
+        node = self.repos.get_node('/README.txt')
+        self.assertEqual(8, node.content_length)
+        self.assertEqual('text/plain', node.content_type)
+        self.assertEqual('A test.\n', node.get_content().read())
+
+    def test_get_dir_properties(self):
+        f = self.repos.get_node('/dir1')
+        props = f.get_properties()
+        self.assertEqual(0, len(props))
+
+    def test_get_file_properties(self):
+        f = self.repos.get_node('/README.txt')
+        props = f.get_properties()
+        self.assertEqual('native', props['svn:eol-style'])
+        self.assertEqual('text/plain', props['svn:mime-type'])
+
+    # Revision Log / node history 
+
+    def test_get_node_history(self):
+        node = self.repos.get_node('/README3.txt')
+        history = node.get_history()
+        self.assertEqual(('README3.txt', 14, 'copy'), history.next())
+        self.assertEqual(('README2.txt', 6, 'copy'), history.next())
+        self.assertEqual(('README.txt', 3, 'edit'), history.next())
+        self.assertEqual(('README.txt', 2, 'add'), history.next())
+        self.assertRaises(StopIteration, history.next)
+
+    def test_get_node_history_follow_copy(self):
+        node = self.repos.get_node('dir1/dir3', )
+        history = node.get_history()
+        self.assertEqual(('dir1/dir3', 5, 'copy'), history.next())
+        self.assertEqual(('dir3', 4, 'add'), history.next())
+        self.assertRaises(StopIteration, history.next)
+
+    # Revision Log / path history 
+
+    def test_get_path_history(self):
+        history = self.repos.get_path_history('dir3', None)
+        self.assertEqual(('dir3', 5, 'delete'), history.next())
+        self.assertEqual(('dir3', 4, 'add'), history.next())
+        self.assertRaises(StopIteration, history.next)
+
+    def test_get_path_history_copied_file(self):
+        history = self.repos.get_path_history('README3.txt', None)
+        self.assertEqual(('README3.txt', 14, 'copy'), history.next())
+        self.assertEqual(('README2.txt', 6, 'unknown'), history.next())
+        self.assertRaises(StopIteration, history.next)
+        
+    def test_get_path_history_copied_dir(self):
+        history = self.repos.get_path_history('dir1/dir3', None)
+        self.assertEqual(('dir1/dir3', 5, 'copy'), history.next())
+        self.assertEqual(('dir3', 4, 'unknown'), history.next())
+        self.assertRaises(StopIteration, history.next)
+
+    def test_changeset_repos_creation(self):
+        chgset = self.repos.get_changeset(0)
+        self.assertEqual(0, chgset.rev)
+        self.assertEqual(None, chgset.message)
+        self.assertEqual(None, chgset.author)
+        self.assertEqual(1112349461, chgset.date)
+        self.assertRaises(StopIteration, chgset.get_changes().next)
+
+    def test_changeset_added_dirs(self):
+        chgset = self.repos.get_changeset(4)
+        self.assertEqual(4, chgset.rev)
+        self.assertEqual('More directories.', chgset.message)
+        self.assertEqual('john', chgset.author)
+        self.assertEqual(1112370155, chgset.date)
+
+        changes = chgset.get_changes()
+        self.assertEqual(('dir1', Node.DIRECTORY, 'add', None, -1),
+                         changes.next())
+        self.assertEqual(('dir2', Node.DIRECTORY, 'add', None, -1),
+                         changes.next())
+        self.assertEqual(('dir3', Node.DIRECTORY, 'add', None, -1),
+                         changes.next())
+        self.assertRaises(StopIteration, changes.next)
+
+    def test_changeset_file_edit(self):
+        chgset = self.repos.get_changeset(3)
+        self.assertEqual(3, chgset.rev)
+        self.assertEqual('Fixed README.\n', chgset.message)
+        self.assertEqual('kate', chgset.author)
+        self.assertEqual(1112361898, chgset.date)
+
+        changes = chgset.get_changes()
+        self.assertEqual(('README.txt', Node.FILE, Changeset.EDIT,
+                          'README.txt', 2), changes.next())
+        self.assertRaises(StopIteration, changes.next)
+
+    def test_changeset_dir_moves(self):
+        chgset = self.repos.get_changeset(5)
+        self.assertEqual(5, chgset.rev)
+        self.assertEqual('Moved directories.', chgset.message)
+        self.assertEqual('kate', chgset.author)
+        self.assertEqual(1112372739, chgset.date)
+
+        changes = chgset.get_changes()
+        self.assertEqual(('dir1/dir2', Node.DIRECTORY, Changeset.MOVE,
+                          'dir2', 4), changes.next())
+        self.assertEqual(('dir1/dir3', Node.DIRECTORY, Changeset.MOVE,
+                          'dir3', 4), changes.next())
+        self.assertRaises(StopIteration, changes.next)
+
+    def test_changeset_file_copy(self):
+        chgset = self.repos.get_changeset(6)
+        self.assertEqual(6, chgset.rev)
+        self.assertEqual('More things to read', chgset.message)
+        self.assertEqual('john', chgset.author)
+        self.assertEqual(1112381806, chgset.date)
+
+        changes = chgset.get_changes()
+        self.assertEqual(('README2.txt', Node.FILE, Changeset.COPY,
+                          'README.txt', 3), changes.next())
+        self.assertRaises(StopIteration, changes.next)
+
+    def test_changeset_root_propset(self):
+        chgset = self.repos.get_changeset(13)
+        self.assertEqual(13, chgset.rev)
+        self.assertEqual('Setting property on the repository_dir root',
+                         chgset.message)
+        changes = chgset.get_changes()
+        self.assertEqual(('/', Node.DIRECTORY, Changeset.EDIT, '/', 6),
+                         changes.next())
+        self.assertRaises(StopIteration, changes.next)
+
+
+class RecentPathScopedRepositoryTestCase(unittest.TestCase):
+
+    def setUp(self):
+        self.repos = SubversionRepository(REPOS_PATH + '/trunk/dir1', None,
+                                          logger_factory('test'))
+
+    def tearDown(self):
+        self.repos = None
+
+    def test_rev_navigation(self):
+        self.assertEqual(False, self.repos.has_node('/', 1))
+        self.assertEqual(False, self.repos.has_node('/', 2))
+        self.assertEqual(False, self.repos.has_node('/', 3))
+        self.assertEqual(True, self.repos.has_node('/', 4))
+        self.assertEqual(4, self.repos.oldest_rev)
+        self.assertEqual(None, self.repos.previous_rev(4))
+
+
+class NonSelfContainedScopedTestCase(unittest.TestCase):
+
+    def setUp(self):
+        self.repos = SubversionRepository(REPOS_PATH + '/tags/v1', None,
+                                          logger_factory('test'))
+
+    def tearDown(self):
+        self.repos = None
+
+    def test_mixed_changeset(self):
+        chgset = self.repos.get_changeset(7)
+        self.assertEqual(7, chgset.rev)
+        changes = chgset.get_changes()
+        self.assertEqual(('/', Node.DIRECTORY, Changeset.ADD, None, -1),
+                         changes.next())
+        self.assertRaises(TracError, lambda: self.repos.get_node(None, 6))
+
+
+class AnotherNonSelfContainedScopedTestCase(unittest.TestCase):
+
+    def setUp(self):
+        self.repos = SubversionRepository(REPOS_PATH + '/branches', None,
+                                          logger_factory('test'))
+
+    def tearDown(self):
+        self.repos = None
+
+    def test_mixed_changeset_with_edit(self):
+        chgset = self.repos.get_changeset(9)
+        self.assertEqual(9, chgset.rev)
+        changes = chgset.get_changes()
+        self.assertEqual(('v1x/README.txt', Node.FILE, Changeset.EDIT,
+                          'v1x/README.txt', 8),
+                         changes.next())
+
+
+def suite():
+    global has_svn
+    suite = unittest.TestSuite()
+    if has_svn:
+        suite.addTest(unittest.makeSuite(SubversionRepositoryTestCase,
+            'test', suiteClass=SubversionRepositoryTestSetup))
+        suite.addTest(unittest.makeSuite(ScopedSubversionRepositoryTestCase,
+            'test', suiteClass=SubversionRepositoryTestSetup))
+        suite.addTest(unittest.makeSuite(RecentPathScopedRepositoryTestCase,
+            'test', suiteClass=SubversionRepositoryTestSetup))
+        suite.addTest(unittest.makeSuite(NonSelfContainedScopedTestCase,
+            'test', suiteClass=SubversionRepositoryTestSetup))
+        suite.addTest(unittest.makeSuite(AnotherNonSelfContainedScopedTestCase,
+            'test', suiteClass=SubversionRepositoryTestSetup))
+    return suite
+
+if __name__ == '__main__':
+    runner = unittest.TextTestRunner()
+    runner.run(suite())
Copyright (C) 2012-2017 Edgewall Software