summaryrefslogtreecommitdiff
path: root/subversion/tests/cmdline/svntest/verify.py
diff options
context:
space:
mode:
Diffstat (limited to 'subversion/tests/cmdline/svntest/verify.py')
-rw-r--r--subversion/tests/cmdline/svntest/verify.py349
1 files changed, 329 insertions, 20 deletions
diff --git a/subversion/tests/cmdline/svntest/verify.py b/subversion/tests/cmdline/svntest/verify.py
index 1c0ae2e..e0da27f 100644
--- a/subversion/tests/cmdline/svntest/verify.py
+++ b/subversion/tests/cmdline/svntest/verify.py
@@ -212,6 +212,9 @@ class RegexOutput(ExpectedOutput):
def display_differences(self, message, label, actual):
display_lines(message, self.expected, actual, label + ' (regexp)', label)
+ def insert(self, index, line):
+ self.expected.insert(index, line)
+ self.expected_re = re.compile(self.expected)
class RegexListOutput(ExpectedOutput):
"""Matches an ordered list of regular expressions.
@@ -227,7 +230,7 @@ class RegexListOutput(ExpectedOutput):
def __init__(self, expected, match_all=True):
"EXPECTED is a list of regular expression strings."
- assert isinstance(expected, list) and expected != []
+ assert isinstance(expected, list)
ExpectedOutput.__init__(self, expected, match_all)
self.expected_res = [re.compile(e) for e in expected]
@@ -251,6 +254,10 @@ class RegexListOutput(ExpectedOutput):
def display_differences(self, message, label, actual):
display_lines(message, self.expected, actual, label + ' (regexp)', label)
+ def insert(self, index, line):
+ self.expected.insert(index, line)
+ self.expected_res = [re.compile(e) for e in self.expected]
+
class UnorderedOutput(ExpectedOutput):
"""Matches an unordered list of lines.
@@ -467,8 +474,10 @@ class DumpParser:
if not m:
if required:
raise SVNDumpParseError("expected '%s' at line %d\n%s"
+ "\nPrevious lines:\n%s"
% (regex, self.current,
- self.lines[self.current]))
+ self.lines[self.current],
+ ''.join(self.lines[max(0,self.current - 10):self.current])))
else:
return None
self.current += 1
@@ -484,6 +493,26 @@ class DumpParser:
self.current += 1
return True
+ def parse_header(self, header):
+ regex = '([^:]*): (.*)$'
+ m = re.match(regex, self.lines[self.current])
+ if not m:
+ raise SVNDumpParseError("expected a header at line %d, but found:\n%s"
+ % (self.current, self.lines[self.current]))
+ self.current += 1
+ return m.groups()
+
+ def parse_headers(self):
+ headers = []
+ while self.lines[self.current] != '\n':
+ key, val = self.parse_header(self)
+ headers.append((key, val))
+ return headers
+
+
+ def parse_boolean(self, header, required):
+ return self.parse_line(header + ': (false|true)$', required)
+
def parse_format(self):
return self.parse_line('SVN-fs-dump-format-version: ([0-9]+)$')
@@ -493,6 +522,9 @@ class DumpParser:
def parse_revision(self):
return self.parse_line('Revision-number: ([0-9]+)$')
+ def parse_prop_delta(self):
+ return self.parse_line('Prop-delta: (false|true)$', required=False)
+
def parse_prop_length(self, required=True):
return self.parse_line('Prop-content-length: ([0-9]+)$', required)
@@ -500,10 +532,7 @@ class DumpParser:
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 = ''
+ path = self.parse_line('Node-path: (.*)$', required=False)
return path
def parse_kind(self):
@@ -534,17 +563,55 @@ class DumpParser:
def parse_text_sha1(self):
return self.parse_line('Text-content-sha1: ([0-9a-z]+)$', required=False)
+ def parse_text_delta(self):
+ return self.parse_line('Text-delta: (false|true)$', required=False)
+
+ def parse_text_delta_base_md5(self):
+ return self.parse_line('Text-delta-base-md5: ([0-9a-f]+)$', required=False)
+
+ def parse_text_delta_base_sha1(self):
+ return self.parse_line('Text-delta-base-sha1: ([0-9a-f]+)$', 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
+
+ # Split into key/value pairs to do an unordered comparison.
+ # This parses the serialized hash under the assumption that it is valid.
+ prophash = {}
+ curprop = [0]
+ while curprop[0] < len(props):
+ def read_key_or_value(curprop):
+ # klen / vlen
+ klen = int(props[curprop[0]].split()[1])
+ curprop[0] += 1
+
+ # key / value
+ key = ''
+ while len(key) != klen + 1:
+ key += props[curprop[0]]
+ curprop[0] += 1
+ key = key[:-1]
+
+ return key
+
+ if props[curprop[0]].startswith('K'):
+ key = read_key_or_value(curprop)
+ value = read_key_or_value(curprop)
+ elif props[curprop[0]].startswith('D'):
+ key = read_key_or_value(curprop)
+ value = None
+ else:
+ raise
+ prophash[key] = value
+
+ return prophash
def get_content(self, length):
content = ''
@@ -560,17 +627,43 @@ class DumpParser:
def parse_one_node(self):
node = {}
+
+ # optional 'kind' and required 'action' must be next
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)
+
+ # read any remaining headers
+ headers_list = self.parse_headers()
+ headers = dict(headers_list)
+
+ # Content-length must be last, if present
+ if 'Content-length' in headers and headers_list[-1][0] != 'Content-length':
+ raise SVNDumpParseError("'Content-length' header is not last, "
+ "in header block ending at line %d"
+ % (self.current,))
+
+ # parse the remaining optional headers and store in specific keys in NODE
+ for key, header, regex in [
+ ('copyfrom_rev', 'Node-copyfrom-rev', '([0-9]+)$'),
+ ('copyfrom_path', 'Node-copyfrom-path', '(.*)$'),
+ ('copy_md5', 'Text-copy-source-md5', '([0-9a-z]+)$'),
+ ('copy_sha1', 'Text-copy-source-sha1','([0-9a-z]+)$'),
+ ('prop_length', 'Prop-content-length', '([0-9]+)$'),
+ ('text_length', 'Text-content-length', '([0-9]+)$'),
+ ('text_md5', 'Text-content-md5', '([0-9a-z]+)$'),
+ ('text_sha1', 'Text-content-sha1', '([0-9a-z]+)$'),
+ ('content_length', 'Content-length', '([0-9]+)$'),
+ ]:
+ if not header in headers:
+ node[key] = None
+ continue
+ m = re.match(regex, headers[header])
+ if not m:
+ raise SVNDumpParseError("expected '%s' at line %d\n%s"
+ % (regex, self.current,
+ self.lines[self.current]))
+ node[key] = m.group(1)
+
self.parse_blank()
if node['prop_length']:
node['props'] = self.get_props()
@@ -592,7 +685,7 @@ class DumpParser:
if self.current >= len(self.lines):
break
path = self.parse_path()
- if not path and not path is '':
+ if path is None:
break
if not nodes.get(path):
nodes[path] = {}
@@ -630,7 +723,11 @@ class DumpParser:
self.parse_all_revisions()
return self.parsed
-def compare_dump_files(message, label, expected, actual):
+def compare_dump_files(message, label, expected, actual,
+ ignore_uuid=False,
+ expect_content_length_always=False,
+ ignore_empty_prop_sections=False,
+ ignore_number_of_blank_lines=False):
"""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.
@@ -639,7 +736,219 @@ def compare_dump_files(message, label, expected, actual):
parsed_expected = DumpParser(expected).parse()
parsed_actual = DumpParser(actual).parse()
+ if ignore_uuid:
+ parsed_expected['uuid'] = '<ignored>'
+ parsed_actual['uuid'] = '<ignored>'
+
+ for parsed in [parsed_expected, parsed_actual]:
+ for rev_name, rev_record in parsed.items():
+ #print "Found %s" % (rev_name,)
+ if 'nodes' in rev_record:
+ #print "Found %s.%s" % (rev_name, 'nodes')
+ for path_name, path_record in rev_record['nodes'].items():
+ #print "Found %s.%s.%s" % (rev_name, 'nodes', path_name)
+ for action_name, action_record in path_record.items():
+ #print "Found %s.%s.%s.%s" % (rev_name, 'nodes', path_name, action_name)
+
+ if expect_content_length_always:
+ if action_record.get('content_length') == None:
+ #print 'Adding: %s.%s.%s.%s.%s' % (rev_name, 'nodes', path_name, action_name, 'content_length=0')
+ action_record['content_length'] = '0'
+ if ignore_empty_prop_sections:
+ if action_record.get('prop_length') == '10':
+ #print 'Removing: %s.%s.%s.%s.%s' % (rev_name, 'nodes', path_name, action_name, 'prop_length')
+ action_record['prop_length'] = None
+ del action_record['props']
+ old_content_length = int(action_record['content_length'])
+ action_record['content_length'] = str(old_content_length - 10)
+ if ignore_number_of_blank_lines:
+ action_record['blanks'] = 0
+
if parsed_expected != parsed_actual:
- raise svntest.Failure('\n' + '\n'.join(ndiff(
+ print 'DIFF of raw dumpfiles (including expected differences)'
+ print ''.join(ndiff(expected, actual))
+ raise svntest.Failure('DIFF of parsed dumpfiles (ignoring expected differences)\n'
+ + '\n'.join(ndiff(
pprint.pformat(parsed_expected).splitlines(),
pprint.pformat(parsed_actual).splitlines())))
+
+##########################################################################################
+## diff verifications
+def is_absolute_url(target):
+ return (target.startswith('file://')
+ or target.startswith('http://')
+ or target.startswith('https://')
+ or target.startswith('svn://')
+ or target.startswith('svn+ssh://'))
+
+def make_diff_header(path, old_tag, new_tag, src_label=None, dst_label=None):
+ """Generate the expected diff header for file PATH, with its old and new
+ versions described in parentheses by OLD_TAG and NEW_TAG. SRC_LABEL and
+ DST_LABEL are paths or urls that are added to the diff labels if we're
+ diffing against the repository or diffing two arbitrary paths.
+ Return the header as an array of newline-terminated strings."""
+ if src_label:
+ src_label = src_label.replace('\\', '/')
+ if not is_absolute_url(src_label):
+ src_label = '.../' + src_label
+ src_label = '\t(' + src_label + ')'
+ else:
+ src_label = ''
+ if dst_label:
+ dst_label = dst_label.replace('\\', '/')
+ if not is_absolute_url(dst_label):
+ dst_label = '.../' + dst_label
+ dst_label = '\t(' + dst_label + ')'
+ else:
+ dst_label = ''
+ path_as_shown = path.replace('\\', '/')
+ return [
+ "Index: " + path_as_shown + "\n",
+ "===================================================================\n",
+ "--- " + path_as_shown + src_label + "\t(" + old_tag + ")\n",
+ "+++ " + path_as_shown + dst_label + "\t(" + new_tag + ")\n",
+ ]
+
+def make_no_diff_deleted_header(path, old_tag, new_tag):
+ """Generate the expected diff header for a deleted file PATH when in
+ 'no-diff-deleted' mode. (In that mode, no further details appear after the
+ header.) Return the header as an array of newline-terminated strings."""
+ path_as_shown = path.replace('\\', '/')
+ return [
+ "Index: " + path_as_shown + " (deleted)\n",
+ "===================================================================\n",
+ ]
+
+def make_git_diff_header(target_path, repos_relpath,
+ old_tag, new_tag, add=False, src_label=None,
+ dst_label=None, delete=False, text_changes=True,
+ cp=False, mv=False, copyfrom_path=None,
+ copyfrom_rev=None):
+ """ Generate the expected 'git diff' header for file TARGET_PATH.
+ REPOS_RELPATH is the location of the path relative to the repository root.
+ The old and new versions ("revision X", or "working copy") must be
+ specified in OLD_TAG and NEW_TAG.
+ SRC_LABEL and DST_LABEL are paths or urls that are added to the diff
+ labels if we're diffing against the repository. ADD, DELETE, CP and MV
+ denotes the operations performed on the file. COPYFROM_PATH is the source
+ of a copy or move. Return the header as an array of newline-terminated
+ strings."""
+
+ path_as_shown = target_path.replace('\\', '/')
+ if src_label:
+ src_label = src_label.replace('\\', '/')
+ src_label = '\t(.../' + src_label + ')'
+ else:
+ src_label = ''
+ if dst_label:
+ dst_label = dst_label.replace('\\', '/')
+ dst_label = '\t(.../' + dst_label + ')'
+ else:
+ dst_label = ''
+
+ output = [
+ "Index: " + path_as_shown + "\n",
+ "===================================================================\n"
+ ]
+ if add:
+ output.extend([
+ "diff --git a/" + repos_relpath + " b/" + repos_relpath + "\n",
+ "new file mode 10644\n",
+ ])
+ if text_changes:
+ output.extend([
+ "--- /dev/null\t(" + old_tag + ")\n",
+ "+++ b/" + repos_relpath + dst_label + "\t(" + new_tag + ")\n"
+ ])
+ elif delete:
+ output.extend([
+ "diff --git a/" + repos_relpath + " b/" + repos_relpath + "\n",
+ "deleted file mode 10644\n",
+ ])
+ if text_changes:
+ output.extend([
+ "--- a/" + repos_relpath + src_label + "\t(" + old_tag + ")\n",
+ "+++ /dev/null\t(" + new_tag + ")\n"
+ ])
+ elif cp:
+ if copyfrom_rev:
+ copyfrom_rev = '@' + copyfrom_rev
+ else:
+ copyfrom_rev = ''
+ output.extend([
+ "diff --git a/" + copyfrom_path + " b/" + repos_relpath + "\n",
+ "copy from " + copyfrom_path + copyfrom_rev + "\n",
+ "copy to " + repos_relpath + "\n",
+ ])
+ if text_changes:
+ output.extend([
+ "--- a/" + copyfrom_path + src_label + "\t(" + old_tag + ")\n",
+ "+++ b/" + repos_relpath + "\t(" + new_tag + ")\n"
+ ])
+ elif mv:
+ output.extend([
+ "diff --git a/" + copyfrom_path + " b/" + path_as_shown + "\n",
+ "rename from " + copyfrom_path + "\n",
+ "rename to " + repos_relpath + "\n",
+ ])
+ if text_changes:
+ output.extend([
+ "--- a/" + copyfrom_path + src_label + "\t(" + old_tag + ")\n",
+ "+++ b/" + repos_relpath + "\t(" + new_tag + ")\n"
+ ])
+ else:
+ output.extend([
+ "diff --git a/" + repos_relpath + " b/" + repos_relpath + "\n",
+ "--- a/" + repos_relpath + src_label + "\t(" + old_tag + ")\n",
+ "+++ b/" + repos_relpath + dst_label + "\t(" + new_tag + ")\n",
+ ])
+ return output
+
+def make_diff_prop_header(path):
+ """Return a property diff sub-header, as a list of newline-terminated
+ strings."""
+ return [
+ "\n",
+ "Property changes on: " + path.replace('\\', '/') + "\n",
+ "___________________________________________________________________\n"
+ ]
+
+def make_diff_prop_val(plus_minus, pval):
+ "Return diff for prop value PVAL, with leading PLUS_MINUS (+ or -)."
+ if len(pval) > 0 and pval[-1] != '\n':
+ return [plus_minus + pval + "\n","\\ No newline at end of property\n"]
+ return [plus_minus + pval]
+
+def make_diff_prop_deleted(pname, pval):
+ """Return a property diff for deletion of property PNAME, old value PVAL.
+ PVAL is a single string with no embedded newlines. Return the result
+ as a list of newline-terminated strings."""
+ return [
+ "Deleted: " + pname + "\n",
+ "## -1 +0,0 ##\n"
+ ] + make_diff_prop_val("-", pval)
+
+def make_diff_prop_added(pname, pval):
+ """Return a property diff for addition of property PNAME, new value PVAL.
+ PVAL is a single string with no embedded newlines. Return the result
+ as a list of newline-terminated strings."""
+ return [
+ "Added: " + pname + "\n",
+ "## -0,0 +1 ##\n",
+ ] + make_diff_prop_val("+", pval)
+
+def make_diff_prop_modified(pname, pval1, pval2):
+ """Return a property diff for modification of property PNAME, old value
+ PVAL1, new value PVAL2.
+
+ PVAL is a single string with no embedded newlines. A newline at the
+ end is significant: without it, we add an extra line saying '\ No
+ newline at end of property'.
+
+ Return the result as a list of newline-terminated strings.
+ """
+ return [
+ "Modified: " + pname + "\n",
+ "## -1 +1 ##\n",
+ ] + make_diff_prop_val("-", pval1) + make_diff_prop_val("+", pval2)
+