~ubuntu-branches/debian/sid/subversion/sid

« back to all changes in this revision

Viewing changes to subversion/tests/cmdline/svntest/verify.py

  • Committer: Package Import Robot
  • Author(s): James McCoy
  • Date: 2015-08-07 21:32:47 UTC
  • mfrom: (0.2.15) (4.1.7 experimental)
  • Revision ID: package-import@ubuntu.com-20150807213247-ozyewtmgsr6tkewl
Tags: 1.9.0-1
* Upload to unstable
* New upstream release.
  + Security fixes
    - CVE-2015-3184: Mixed anonymous/authenticated path-based authz with
      httpd 2.4
    - CVE-2015-3187: svn_repos_trace_node_locations() reveals paths hidden
      by authz
* Add >= 2.7 requirement for python-all-dev Build-Depends, needed to run
  tests.
* Remove Build-Conflicts against ruby-test-unit.  (Closes: #791844)
* Remove patches/apache_module_dependency in favor of expressing the
  dependencies in authz_svn.load/dav_svn.load.
* Build-Depend on apache2-dev (>= 2.4.16) to ensure ap_some_authn_required()
  is available when building mod_authz_svn and Depend on apache2-bin (>=
  2.4.16) for runtime support.

Show diffs side-by-side

added added

removed removed

Lines of Context:
212
212
  def display_differences(self, message, label, actual):
213
213
    display_lines(message, self.expected, actual, label + ' (regexp)', label)
214
214
 
 
215
  def insert(self, index, line):
 
216
    self.expected.insert(index, line)
 
217
    self.expected_re = re.compile(self.expected)
215
218
 
216
219
class RegexListOutput(ExpectedOutput):
217
220
  """Matches an ordered list of regular expressions.
227
230
 
228
231
  def __init__(self, expected, match_all=True):
229
232
    "EXPECTED is a list of regular expression strings."
230
 
    assert isinstance(expected, list) and expected != []
 
233
    assert isinstance(expected, list)
231
234
    ExpectedOutput.__init__(self, expected, match_all)
232
235
    self.expected_res = [re.compile(e) for e in expected]
233
236
 
251
254
  def display_differences(self, message, label, actual):
252
255
    display_lines(message, self.expected, actual, label + ' (regexp)', label)
253
256
 
 
257
  def insert(self, index, line):
 
258
    self.expected.insert(index, line)
 
259
    self.expected_res = [re.compile(e) for e in self.expected]
 
260
 
254
261
 
255
262
class UnorderedOutput(ExpectedOutput):
256
263
  """Matches an unordered list of lines.
467
474
    if not m:
468
475
      if required:
469
476
        raise SVNDumpParseError("expected '%s' at line %d\n%s"
 
477
                                "\nPrevious lines:\n%s"
470
478
                                % (regex, self.current,
471
 
                                   self.lines[self.current]))
 
479
                                   self.lines[self.current],
 
480
                                   ''.join(self.lines[max(0,self.current - 10):self.current])))
472
481
      else:
473
482
        return None
474
483
    self.current += 1
484
493
    self.current += 1
485
494
    return True
486
495
 
 
496
  def parse_header(self, header):
 
497
    regex = '([^:]*): (.*)$'
 
498
    m = re.match(regex, self.lines[self.current])
 
499
    if not m:
 
500
      raise SVNDumpParseError("expected a header at line %d, but found:\n%s"
 
501
                              % (self.current, self.lines[self.current]))
 
502
    self.current += 1
 
503
    return m.groups()
 
504
 
 
505
  def parse_headers(self):
 
506
    headers = []
 
507
    while self.lines[self.current] != '\n':
 
508
      key, val = self.parse_header(self)
 
509
      headers.append((key, val))
 
510
    return headers
 
511
 
 
512
 
 
513
  def parse_boolean(self, header, required):
 
514
    return self.parse_line(header + ': (false|true)$', required)
 
515
 
487
516
  def parse_format(self):
488
517
    return self.parse_line('SVN-fs-dump-format-version: ([0-9]+)$')
489
518
 
493
522
  def parse_revision(self):
494
523
    return self.parse_line('Revision-number: ([0-9]+)$')
495
524
 
 
525
  def parse_prop_delta(self):
 
526
    return self.parse_line('Prop-delta: (false|true)$', required=False)
 
527
 
496
528
  def parse_prop_length(self, required=True):
497
529
    return self.parse_line('Prop-content-length: ([0-9]+)$', required)
498
530
 
500
532
    return self.parse_line('Content-length: ([0-9]+)$', required)
501
533
 
502
534
  def parse_path(self):
503
 
    path = self.parse_line('Node-path: (.+)$', required=False)
504
 
    if not path and self.lines[self.current] == 'Node-path: \n':
505
 
      self.current += 1
506
 
      path = ''
 
535
    path = self.parse_line('Node-path: (.*)$', required=False)
507
536
    return path
508
537
 
509
538
  def parse_kind(self):
534
563
  def parse_text_sha1(self):
535
564
    return self.parse_line('Text-content-sha1: ([0-9a-z]+)$', required=False)
536
565
 
 
566
  def parse_text_delta(self):
 
567
    return self.parse_line('Text-delta: (false|true)$', required=False)
 
568
 
 
569
  def parse_text_delta_base_md5(self):
 
570
    return self.parse_line('Text-delta-base-md5: ([0-9a-f]+)$', required=False)
 
571
 
 
572
  def parse_text_delta_base_sha1(self):
 
573
    return self.parse_line('Text-delta-base-sha1: ([0-9a-f]+)$', required=False)
 
574
 
537
575
  def parse_text_length(self):
538
576
    return self.parse_line('Text-content-length: ([0-9]+)$', required=False)
539
577
 
540
 
  # One day we may need to parse individual property name/values into a map
541
578
  def get_props(self):
542
579
    props = []
543
580
    while not re.match('PROPS-END$', self.lines[self.current]):
544
581
      props.append(self.lines[self.current])
545
582
      self.current += 1
546
583
    self.current += 1
547
 
    return props
 
584
 
 
585
    # Split into key/value pairs to do an unordered comparison.
 
586
    # This parses the serialized hash under the assumption that it is valid.
 
587
    prophash = {}
 
588
    curprop = [0]
 
589
    while curprop[0] < len(props):
 
590
      def read_key_or_value(curprop):
 
591
        # klen / vlen
 
592
        klen = int(props[curprop[0]].split()[1])
 
593
        curprop[0] += 1
 
594
 
 
595
        # key / value
 
596
        key = ''
 
597
        while len(key) != klen + 1:
 
598
          key += props[curprop[0]]
 
599
          curprop[0] += 1
 
600
        key = key[:-1]
 
601
 
 
602
        return key
 
603
 
 
604
      if props[curprop[0]].startswith('K'):
 
605
        key = read_key_or_value(curprop)
 
606
        value = read_key_or_value(curprop)
 
607
      elif props[curprop[0]].startswith('D'):
 
608
        key = read_key_or_value(curprop)
 
609
        value = None
 
610
      else:
 
611
        raise
 
612
      prophash[key] = value
 
613
 
 
614
    return prophash
548
615
 
549
616
  def get_content(self, length):
550
617
    content = ''
560
627
 
561
628
  def parse_one_node(self):
562
629
    node = {}
 
630
 
 
631
    # optional 'kind' and required 'action' must be next
563
632
    node['kind'] = self.parse_kind()
564
633
    action = self.parse_action()
565
 
    node['copyfrom_rev'] = self.parse_copyfrom_rev()
566
 
    node['copyfrom_path'] = self.parse_copyfrom_path()
567
 
    node['copy_md5'] = self.parse_copy_md5()
568
 
    node['copy_sha1'] = self.parse_copy_sha1()
569
 
    node['prop_length'] = self.parse_prop_length(required=False)
570
 
    node['text_length'] = self.parse_text_length()
571
 
    node['text_md5'] = self.parse_text_md5()
572
 
    node['text_sha1'] = self.parse_text_sha1()
573
 
    node['content_length'] = self.parse_content_length(required=False)
 
634
 
 
635
    # read any remaining headers
 
636
    headers_list = self.parse_headers()
 
637
    headers = dict(headers_list)
 
638
 
 
639
    # Content-length must be last, if present
 
640
    if 'Content-length' in headers and headers_list[-1][0] != 'Content-length':
 
641
      raise SVNDumpParseError("'Content-length' header is not last, "
 
642
                              "in header block ending at line %d"
 
643
                              % (self.current,))
 
644
 
 
645
    # parse the remaining optional headers and store in specific keys in NODE
 
646
    for key, header, regex in [
 
647
        ('copyfrom_rev',    'Node-copyfrom-rev',    '([0-9]+)$'),
 
648
        ('copyfrom_path',   'Node-copyfrom-path',   '(.*)$'),
 
649
        ('copy_md5',        'Text-copy-source-md5', '([0-9a-z]+)$'),
 
650
        ('copy_sha1',       'Text-copy-source-sha1','([0-9a-z]+)$'),
 
651
        ('prop_length',     'Prop-content-length',  '([0-9]+)$'),
 
652
        ('text_length',     'Text-content-length',  '([0-9]+)$'),
 
653
        ('text_md5',        'Text-content-md5',     '([0-9a-z]+)$'),
 
654
        ('text_sha1',       'Text-content-sha1',    '([0-9a-z]+)$'),
 
655
        ('content_length',  'Content-length',       '([0-9]+)$'),
 
656
        ]:
 
657
      if not header in headers:
 
658
        node[key] = None
 
659
        continue
 
660
      m = re.match(regex, headers[header])
 
661
      if not m:
 
662
        raise SVNDumpParseError("expected '%s' at line %d\n%s"
 
663
                                % (regex, self.current,
 
664
                                   self.lines[self.current]))
 
665
      node[key] = m.group(1)
 
666
 
574
667
    self.parse_blank()
575
668
    if node['prop_length']:
576
669
      node['props'] = self.get_props()
592
685
      if self.current >= len(self.lines):
593
686
        break
594
687
      path = self.parse_path()
595
 
      if not path and not path is '':
 
688
      if path is None:
596
689
        break
597
690
      if not nodes.get(path):
598
691
        nodes[path] = {}
630
723
    self.parse_all_revisions()
631
724
    return self.parsed
632
725
 
633
 
def compare_dump_files(message, label, expected, actual):
 
726
def compare_dump_files(message, label, expected, actual,
 
727
                       ignore_uuid=False,
 
728
                       expect_content_length_always=False,
 
729
                       ignore_empty_prop_sections=False,
 
730
                       ignore_number_of_blank_lines=False):
634
731
  """Parse two dump files EXPECTED and ACTUAL, both of which are lists
635
732
  of lines as returned by run_and_verify_dump, and check that the same
636
733
  revisions, nodes, properties, etc. are present in both dumps.
639
736
  parsed_expected = DumpParser(expected).parse()
640
737
  parsed_actual = DumpParser(actual).parse()
641
738
 
 
739
  if ignore_uuid:
 
740
    parsed_expected['uuid'] = '<ignored>'
 
741
    parsed_actual['uuid'] = '<ignored>'
 
742
 
 
743
  for parsed in [parsed_expected, parsed_actual]:
 
744
    for rev_name, rev_record in parsed.items():
 
745
      #print "Found %s" % (rev_name,)
 
746
      if 'nodes' in rev_record:
 
747
        #print "Found %s.%s" % (rev_name, 'nodes')
 
748
        for path_name, path_record in rev_record['nodes'].items():
 
749
          #print "Found %s.%s.%s" % (rev_name, 'nodes', path_name)
 
750
          for action_name, action_record in path_record.items():
 
751
            #print "Found %s.%s.%s.%s" % (rev_name, 'nodes', path_name, action_name)
 
752
 
 
753
            if expect_content_length_always:
 
754
              if action_record.get('content_length') == None:
 
755
                #print 'Adding: %s.%s.%s.%s.%s' % (rev_name, 'nodes', path_name, action_name, 'content_length=0')
 
756
                action_record['content_length'] = '0'
 
757
            if ignore_empty_prop_sections:
 
758
              if action_record.get('prop_length') == '10':
 
759
                #print 'Removing: %s.%s.%s.%s.%s' % (rev_name, 'nodes', path_name, action_name, 'prop_length')
 
760
                action_record['prop_length'] = None
 
761
                del action_record['props']
 
762
                old_content_length = int(action_record['content_length'])
 
763
                action_record['content_length'] = str(old_content_length - 10)
 
764
            if ignore_number_of_blank_lines:
 
765
              action_record['blanks'] = 0
 
766
 
642
767
  if parsed_expected != parsed_actual:
643
 
    raise svntest.Failure('\n' + '\n'.join(ndiff(
 
768
    print 'DIFF of raw dumpfiles (including expected differences)'
 
769
    print ''.join(ndiff(expected, actual))
 
770
    raise svntest.Failure('DIFF of parsed dumpfiles (ignoring expected differences)\n'
 
771
                          + '\n'.join(ndiff(
644
772
          pprint.pformat(parsed_expected).splitlines(),
645
773
          pprint.pformat(parsed_actual).splitlines())))
 
774
 
 
775
##########################################################################################
 
776
## diff verifications
 
777
def is_absolute_url(target):
 
778
  return (target.startswith('file://')
 
779
          or target.startswith('http://')
 
780
          or target.startswith('https://')
 
781
          or target.startswith('svn://')
 
782
          or target.startswith('svn+ssh://'))
 
783
 
 
784
def make_diff_header(path, old_tag, new_tag, src_label=None, dst_label=None):
 
785
  """Generate the expected diff header for file PATH, with its old and new
 
786
  versions described in parentheses by OLD_TAG and NEW_TAG. SRC_LABEL and
 
787
  DST_LABEL are paths or urls that are added to the diff labels if we're
 
788
  diffing against the repository or diffing two arbitrary paths.
 
789
  Return the header as an array of newline-terminated strings."""
 
790
  if src_label:
 
791
    src_label = src_label.replace('\\', '/')
 
792
    if not is_absolute_url(src_label):
 
793
      src_label = '.../' + src_label
 
794
    src_label = '\t(' + src_label + ')'
 
795
  else:
 
796
    src_label = ''
 
797
  if dst_label:
 
798
    dst_label = dst_label.replace('\\', '/')
 
799
    if not is_absolute_url(dst_label):
 
800
      dst_label = '.../' + dst_label
 
801
    dst_label = '\t(' + dst_label + ')'
 
802
  else:
 
803
    dst_label = ''
 
804
  path_as_shown = path.replace('\\', '/')
 
805
  return [
 
806
    "Index: " + path_as_shown + "\n",
 
807
    "===================================================================\n",
 
808
    "--- " + path_as_shown + src_label + "\t(" + old_tag + ")\n",
 
809
    "+++ " + path_as_shown + dst_label + "\t(" + new_tag + ")\n",
 
810
    ]
 
811
 
 
812
def make_no_diff_deleted_header(path, old_tag, new_tag):
 
813
  """Generate the expected diff header for a deleted file PATH when in
 
814
  'no-diff-deleted' mode. (In that mode, no further details appear after the
 
815
  header.) Return the header as an array of newline-terminated strings."""
 
816
  path_as_shown = path.replace('\\', '/')
 
817
  return [
 
818
    "Index: " + path_as_shown + " (deleted)\n",
 
819
    "===================================================================\n",
 
820
    ]
 
821
 
 
822
def make_git_diff_header(target_path, repos_relpath,
 
823
                         old_tag, new_tag, add=False, src_label=None,
 
824
                         dst_label=None, delete=False, text_changes=True,
 
825
                         cp=False, mv=False, copyfrom_path=None,
 
826
                         copyfrom_rev=None):
 
827
  """ Generate the expected 'git diff' header for file TARGET_PATH.
 
828
  REPOS_RELPATH is the location of the path relative to the repository root.
 
829
  The old and new versions ("revision X", or "working copy") must be
 
830
  specified in OLD_TAG and NEW_TAG.
 
831
  SRC_LABEL and DST_LABEL are paths or urls that are added to the diff
 
832
  labels if we're diffing against the repository. ADD, DELETE, CP and MV
 
833
  denotes the operations performed on the file. COPYFROM_PATH is the source
 
834
  of a copy or move.  Return the header as an array of newline-terminated
 
835
  strings."""
 
836
 
 
837
  path_as_shown = target_path.replace('\\', '/')
 
838
  if src_label:
 
839
    src_label = src_label.replace('\\', '/')
 
840
    src_label = '\t(.../' + src_label + ')'
 
841
  else:
 
842
    src_label = ''
 
843
  if dst_label:
 
844
    dst_label = dst_label.replace('\\', '/')
 
845
    dst_label = '\t(.../' + dst_label + ')'
 
846
  else:
 
847
    dst_label = ''
 
848
 
 
849
  output = [
 
850
    "Index: " + path_as_shown + "\n",
 
851
    "===================================================================\n"
 
852
  ]
 
853
  if add:
 
854
    output.extend([
 
855
      "diff --git a/" + repos_relpath + " b/" + repos_relpath + "\n",
 
856
      "new file mode 10644\n",
 
857
    ])
 
858
    if text_changes:
 
859
      output.extend([
 
860
        "--- /dev/null\t(" + old_tag + ")\n",
 
861
        "+++ b/" + repos_relpath + dst_label + "\t(" + new_tag + ")\n"
 
862
      ])
 
863
  elif delete:
 
864
    output.extend([
 
865
      "diff --git a/" + repos_relpath + " b/" + repos_relpath + "\n",
 
866
      "deleted file mode 10644\n",
 
867
    ])
 
868
    if text_changes:
 
869
      output.extend([
 
870
        "--- a/" + repos_relpath + src_label + "\t(" + old_tag + ")\n",
 
871
        "+++ /dev/null\t(" + new_tag + ")\n"
 
872
      ])
 
873
  elif cp:
 
874
    if copyfrom_rev:
 
875
      copyfrom_rev = '@' + copyfrom_rev
 
876
    else:
 
877
      copyfrom_rev = ''
 
878
    output.extend([
 
879
      "diff --git a/" + copyfrom_path + " b/" + repos_relpath + "\n",
 
880
      "copy from " + copyfrom_path + copyfrom_rev + "\n",
 
881
      "copy to " + repos_relpath + "\n",
 
882
    ])
 
883
    if text_changes:
 
884
      output.extend([
 
885
        "--- a/" + copyfrom_path + src_label + "\t(" + old_tag + ")\n",
 
886
        "+++ b/" + repos_relpath + "\t(" + new_tag + ")\n"
 
887
      ])
 
888
  elif mv:
 
889
    output.extend([
 
890
      "diff --git a/" + copyfrom_path + " b/" + path_as_shown + "\n",
 
891
      "rename from " + copyfrom_path + "\n",
 
892
      "rename to " + repos_relpath + "\n",
 
893
    ])
 
894
    if text_changes:
 
895
      output.extend([
 
896
        "--- a/" + copyfrom_path + src_label + "\t(" + old_tag + ")\n",
 
897
        "+++ b/" + repos_relpath + "\t(" + new_tag + ")\n"
 
898
      ])
 
899
  else:
 
900
    output.extend([
 
901
      "diff --git a/" + repos_relpath + " b/" + repos_relpath + "\n",
 
902
      "--- a/" + repos_relpath + src_label + "\t(" + old_tag + ")\n",
 
903
      "+++ b/" + repos_relpath + dst_label + "\t(" + new_tag + ")\n",
 
904
    ])
 
905
  return output
 
906
 
 
907
def make_diff_prop_header(path):
 
908
  """Return a property diff sub-header, as a list of newline-terminated
 
909
     strings."""
 
910
  return [
 
911
    "\n",
 
912
    "Property changes on: " + path.replace('\\', '/') + "\n",
 
913
    "___________________________________________________________________\n"
 
914
  ]
 
915
 
 
916
def make_diff_prop_val(plus_minus, pval):
 
917
  "Return diff for prop value PVAL, with leading PLUS_MINUS (+ or -)."
 
918
  if len(pval) > 0 and pval[-1] != '\n':
 
919
    return [plus_minus + pval + "\n","\\ No newline at end of property\n"]
 
920
  return [plus_minus + pval]
 
921
 
 
922
def make_diff_prop_deleted(pname, pval):
 
923
  """Return a property diff for deletion of property PNAME, old value PVAL.
 
924
     PVAL is a single string with no embedded newlines.  Return the result
 
925
     as a list of newline-terminated strings."""
 
926
  return [
 
927
    "Deleted: " + pname + "\n",
 
928
    "## -1 +0,0 ##\n"
 
929
  ] + make_diff_prop_val("-", pval)
 
930
 
 
931
def make_diff_prop_added(pname, pval):
 
932
  """Return a property diff for addition of property PNAME, new value PVAL.
 
933
     PVAL is a single string with no embedded newlines.  Return the result
 
934
     as a list of newline-terminated strings."""
 
935
  return [
 
936
    "Added: " + pname + "\n",
 
937
    "## -0,0 +1 ##\n",
 
938
  ] + make_diff_prop_val("+", pval)
 
939
 
 
940
def make_diff_prop_modified(pname, pval1, pval2):
 
941
  """Return a property diff for modification of property PNAME, old value
 
942
     PVAL1, new value PVAL2.
 
943
 
 
944
     PVAL is a single string with no embedded newlines.  A newline at the
 
945
     end is significant: without it, we add an extra line saying '\ No
 
946
     newline at end of property'.
 
947
 
 
948
     Return the result as a list of newline-terminated strings.
 
949
  """
 
950
  return [
 
951
    "Modified: " + pname + "\n",
 
952
    "## -1 +1 ##\n",
 
953
  ] + make_diff_prop_val("-", pval1) + make_diff_prop_val("+", pval2)
 
954