Author: philip
Date: Wed Feb 22 21:13:50 2012
New Revision: 1292507
URL: http://svn.apache.org/viewvc?rev=1292507&view=rev
Log:
Use a simple dump file parser to compare dumps, rather than simply
comparing lines, to remove a dependency on APR hash order from the
testsuite.
* subversion/tests/cmdline/svnrdump_tests.py
(compare_repos_dumps): Use verify.compare_dump_files.
* subversion/tests/cmdline/svnsync_tests.py
(verify_mirror): Use verify.compare_dump_files.
* subversion/tests/cmdline/svntest/verify.py
(SVNDumpParseError, DumpParser): New classes.
(compare_dump_files): New fuction.
Modified:
subversion/trunk/subversion/tests/cmdline/svnrdump_tests.py
subversion/trunk/subversion/tests/cmdline/svnsync_tests.py
subversion/trunk/subversion/tests/cmdline/svntest/verify.py
Modified: subversion/trunk/subversion/tests/cmdline/svnrdump_tests.py
URL:
http://svn.apache.org/viewvc/subversion/trunk/subversion/tests/cmdline/svnrdump_tests.py?rev=1292507&r1=1292506&r2=1292507&view=diff
==============================================================================
--- subversion/trunk/subversion/tests/cmdline/svnrdump_tests.py (original)
+++ subversion/trunk/subversion/tests/cmdline/svnrdump_tests.py Wed Feb 22
21:13:50 2012
@@ -89,7 +89,7 @@ def compare_repos_dumps(svnrdump_sbox, s
svnadmin_contents = svntest.actions.run_and_verify_dump(
svnadmin_sbox.repo_dir)
- svntest.verify.compare_and_display_lines(
+ svntest.verify.compare_dump_files(
"Dump files", "DUMP", svnadmin_contents, svnrdump_contents)
def run_dump_test(sbox, dumpfile_name, expected_dumpfile_name = None,
Modified: subversion/trunk/subversion/tests/cmdline/svnsync_tests.py
URL:
http://svn.apache.org/viewvc/subversion/trunk/subversion/tests/cmdline/svnsync_tests.py?rev=1292507&r1=1292506&r2=1292507&view=diff
==============================================================================
--- subversion/trunk/subversion/tests/cmdline/svnsync_tests.py (original)
+++ subversion/trunk/subversion/tests/cmdline/svnsync_tests.py Wed Feb 22
21:13:50 2012
@@ -223,7 +223,7 @@ def verify_mirror(dest_sbox, src_sbox):
dest_dump = svntest.actions.run_and_verify_dump(dest_sbox.repo_dir)
src_dump = svntest.actions.run_and_verify_dump(src_sbox.repo_dir)
- svntest.verify.compare_and_display_lines(
+ svntest.verify.compare_dump_files(
"Dump files", "DUMP", src_dump, dest_dump)
def run_test(sbox, dump_file_name, subdir=None, exp_dump_file_name=None,
Modified: subversion/trunk/subversion/tests/cmdline/svntest/verify.py
URL:
http://svn.apache.org/viewvc/subversion/trunk/subversion/tests/cmdline/svntest/verify.py?rev=1292507&r1=1292506&r2=1292507&view=diff
==============================================================================
--- subversion/trunk/subversion/tests/cmdline/svntest/verify.py (original)
+++ subversion/trunk/subversion/tests/cmdline/svntest/verify.py Wed Feb 22
21:13:50 2012
@@ -25,7 +25,8 @@
######################################################################
import re, sys
-from difflib import unified_diff
+from difflib import unified_diff, ndiff
+import pprint
import svntest
@@ -68,6 +69,10 @@ class SVNIncorrectDatatype(SVNUnexpected
run_and_verify_* API"""
pass
+class SVNDumpParseError(svntest.Failure):
+ """Exception raised if parsing a dump file fails"""
+ pass
+
######################################################################
# Comparison of expected vs. actual output
@@ -397,3 +402,193 @@ def verify_exit_code(message, actual, ex
display_lines(message, "Exit Code",
str(expected) + '\n', str(actual) + '\n')
raise raisable
+
+# A simple dump file parser. While sufficient for the current
+# testsuite it doesn't cope with all valid dump files.
+class DumpParser:
+ def __init__(self, lines):
+ self.current = 0
+ self.lines = lines
+ self.parsed = {}
+
+ def parse_line(self, regex, required=True):
+ m = re.match(regex, self.lines[self.current])
+ if not m:
+ if required:
+ raise SVNDumpParseError("expected '%s' at line %d\n%s"
+ % (regex, self.current,
+ self.lines[self.current]))
+ else:
+ return None
+ self.current += 1
+ return m.group(1)
+
+ def parse_blank(self, required=True):
+ if self.lines[self.current] != '\n': # Windows?
+ if required:
+ raise SVNDumpParseError("expected blank at line %d\n%s"
+ % (self.current, self.lines[self.current]))
+ else:
+ return False
+ self.current += 1
+ return True
+
+ def parse_format(self):
+ return self.parse_line('SVN-fs-dump-format-version: ([0-9]+)$')
+
+ def parse_uuid(self):
+ return self.parse_line('UUID: ([0-9a-z-]+)$')
+
+ def parse_revision(self):
+ return self.parse_line('Revision-number: ([0-9]+)$')
+
+ def parse_prop_length(self, required=True):
+ return self.parse_line('Prop-content-length: ([0-9]+)$', required)
+
+ def parse_content_length(self, required=True):
+ return self.parse_line('Content-length: ([0-9]+)$', required)
+
+ def parse_path(self):
+ path = self.parse_line('Node-path: (.+)$', required=False)
+ if not path and self.lines[self.current] == 'Node-path: \n':
+ self.current += 1
+ path = ''
+ return path
+
+ def parse_kind(self):
+ return self.parse_line('Node-kind: (.+)$', required=False)
+
+ def parse_action(self):
+ return self.parse_line('Node-action: ([0-9a-z-]+)$')
+
+ def parse_copyfrom_rev(self):
+ return self.parse_line('Node-copyfrom-rev: ([0-9]+)$', required=False)
+
+ def parse_copyfrom_path(self):
+ path = self.parse_line('Node-copyfrom-path: (.+)$', required=False)
+ if not path and self.lines[self.current] == 'Node-copyfrom-path: \n':
+ self.current += 1
+ path = ''
+ return path
+
+ def parse_copy_md5(self):
+ return self.parse_line('Text-copy-source-md5: ([0-9a-z]+)$',
required=False)
+
+ def parse_copy_sha1(self):
+ return self.parse_line('Text-copy-source-sha1: ([0-9a-z]+)$',
required=False)
+
+ def parse_text_md5(self):
+ return self.parse_line('Text-content-md5: ([0-9a-z]+)$', required=False)
+
+ def parse_text_sha1(self):
+ return self.parse_line('Text-content-sha1: ([0-9a-z]+)$', required=False)
+
+ def parse_text_length(self):
+ return self.parse_line('Text-content-length: ([0-9]+)$', required=False)
+
+ # One day we may need to parse individual property name/values into a map
+ def get_props(self):
+ props = []
+ while not re.match('PROPS-END$', self.lines[self.current]):
+ props.append(self.lines[self.current])
+ self.current += 1
+ self.current += 1
+ return props
+
+ def get_content(self, length):
+ content = ''
+ while len(content) < length:
+ content += self.lines[self.current]
+ self.current += 1
+ if len(content) == length + 1:
+ content = content[:-1]
+ elif len(content) != length:
+ raise SVNDumpParseError("content length expected %d actual %d at line %d"
+ % (length, len(content), self.current))
+ return content
+
+ def parse_one_node(self):
+ node = {}
+ node['kind'] = self.parse_kind()
+ action = self.parse_action()
+ node['copyfrom_rev'] = self.parse_copyfrom_rev()
+ node['copyfrom_path'] = self.parse_copyfrom_path()
+ node['copy_md5'] = self.parse_copy_md5()
+ node['copy_sha1'] = self.parse_copy_sha1()
+ node['prop_length'] = self.parse_prop_length(required=False)
+ node['text_length'] = self.parse_text_length()
+ node['text_md5'] = self.parse_text_md5()
+ node['text_sha1'] = self.parse_text_sha1()
+ node['content_length'] = self.parse_content_length(required=False)
+ self.parse_blank()
+ if node['prop_length']:
+ node['props'] = self.get_props()
+ if node['text_length']:
+ node['content'] = self.get_content(int(node['text_length']))
+ # Hard to determine how may blanks is 'correct' (a delete that is
+ # followed by an add that is a replace and a copy has one fewer
+ # than expected but that can't be predicted until seeing the add)
+ # so allow arbitrary number
+ blanks = 0
+ while self.current < len(self.lines) and self.parse_blank(required=False):
+ blanks += 1
+ node['blanks'] = blanks
+ return action, node
+
+ def parse_all_nodes(self):
+ nodes = {}
+ while True:
+ if self.current >= len(self.lines):
+ break
+ path = self.parse_path()
+ if not path and not path is '':
+ break
+ if not nodes.get(path):
+ nodes[path] = {}
+ action, node = self.parse_one_node()
+ if nodes[path].get(action):
+ raise SVNDumpParseError("duplicate action '%s' for node '%s' at line
%d"
+ % (action, path, self.current))
+ nodes[path][action] = node
+ return nodes
+
+ def parse_one_revision(self):
+ revision = {}
+ number = self.parse_revision()
+ revision['prop_length'] = self.parse_prop_length()
+ revision['content_length'] = self.parse_content_length()
+ self.parse_blank()
+ revision['props'] = self.get_props()
+ self.parse_blank()
+ revision['nodes'] = self.parse_all_nodes()
+ return number, revision
+
+ def parse_all_revisions(self):
+ while self.current < len(self.lines):
+ number, revision = self.parse_one_revision()
+ if self.parsed.get(number):
+ raise SVNDumpParseError("duplicate revision %d at line %d"
+ % (number, self.current))
+ self.parsed[number] = revision
+
+ def parse(self):
+ self.parsed['format'] = self.parse_format()
+ self.parse_blank()
+ self.parsed['uuid'] = self.parse_uuid()
+ self.parse_blank()
+ self.parse_all_revisions()
+ return self.parsed
+
+def compare_dump_files(message, label, expected, actual):
+ """Parse two dump files EXPECTED and ACTUAL, both of which are lists
+ of lines as returned by run_and_verify_dump, and check that the same
+ revisions, nodes, properties, etc. are present in both dumps.
+ """
+
+ parsed_expected = DumpParser(expected).parse()
+ parsed_actual = DumpParser(actual).parse()
+
+ if parsed_expected != parsed_actual:
+ raise svntest.Failure('\n' + '\n'.join(ndiff(
+ pprint.pformat(parsed_expected).splitlines(),
+ pprint.pformat(parsed_actual).splitlines())))