Planet
navi homePPSaboutscreenshotsdownloaddevelopmentforum

source: downloads/boost_1_34_1/tools/build/v2/test/svn_tree.py @ 32

Last change on this file since 32 was 29, checked in by landauf, 17 years ago

updated boost from 1_33_1 to 1_34_1

File size: 21.3 KB
Line 
1#!/usr/bin/env python
2#
3#  tree.py: tools for comparing directory trees
4#
5#  Subversion is a tool for revision control.
6#  See http://subversion.tigris.org for more information.
7#
8# ====================================================================
9# Copyright (c) 2001 Sam Tobin-Hochstadt.  All rights reserved.
10#
11# This software is licensed as described in the file COPYING, which
12# you should have received as part of this distribution.  The terms
13# are also available at http://subversion.tigris.org/license-1.html.
14# If newer versions of this license are posted there, you may use a
15# newer version instead, at your option.
16#
17######################################################################
18
19# This file was modified by Vladimir Prus to store modification times in
20# tree nodes.
21
22import re
23import string
24import os.path
25import os
26import stat
27
28
29
30#========================================================================
31
32# ===>  Overview of our Datastructures  <===
33
34# The general idea here is that many, many things can be represented by
35# a tree structure:
36
37#   - a working copy's structure and contents
38#   - the output of 'svn status'
39#   - the output of 'svn checkout/update'
40#   - the output of 'svn commit'
41
42# The idea is that a test function creates a "expected" tree of some
43# kind, and is then able to compare it to an "actual" tree that comes
44# from running the Subversion client.  This is what makes a test
45# automated; if an actual and expected tree match exactly, then the test
46# has passed.  (See compare_trees() below.)
47
48# The SVNTreeNode class is the fundamental data type used to build tree
49# structures.  The class contains a method for "dropping" a new node
50# into an ever-growing tree structure. (See also create_from_path()).
51
52# We have four parsers in this file for the four use cases listed above:
53# each parser examines some kind of input and returns a tree of
54# SVNTreeNode objects.  (See build_tree_from_checkout(),
55# build_tree_from_commit(), build_tree_from_status(), and
56# build_tree_from_wc()).  These trees are the "actual" trees that result
57# from running the Subversion client.
58
59# Also necessary, of course, is a convenient way for a test to create an
60# "expected" tree.  The test *could* manually construct and link a bunch
61# of SVNTreeNodes, certainly.  But instead, all the tests are using the
62# build_generic_tree() routine instead.
63
64# build_generic_tree() takes a specially-formatted list of lists as
65# input, and returns a tree of SVNTreeNodes.  The list of lists has this
66# structure:
67
68#   [ ['/full/path/to/item', 'text contents', {prop-hash}, {att-hash}],
69#     [...],
70#     [...],
71#     ...   ]
72
73# You can see that each item in the list essentially defines an
74# SVNTreeNode.  build_generic_tree() instantiates a SVNTreeNode for each
75# item, and then drops it into a tree by parsing each item's full path.
76
77# So a typical test routine spends most of its time preparing lists of
78# this format and sending them to build_generic_tree(), rather than
79# building the "expected" trees directly.
80
81#   ### Note: in the future, we'd like to remove this extra layer of
82#   ### abstraction.  We'd like the SVNTreeNode class to be more
83#   ### directly programmer-friendly, providing a number of accessor
84#   ### routines, so that tests can construct trees directly.
85
86# The first three fields of each list-item are self-explanatory.  It's
87# the fourth field, the "attribute" hash, that needs some explanation.
88# The att-hash is used to place extra information about the node itself,
89# depending on the parsing context:
90
91#   - in the 'svn co/up' use-case, each line of output starts with two
92#     characters from the set of (A, D, G, U, C, _).  This status code
93#     is stored in a attribute named 'status'.
94
95#   - in the 'svn ci/im' use-case, each line of output starts with one
96#      of the words (Adding, Deleting, Sending).  This verb is stored in
97#      an attribute named 'verb'.
98
99#   - in the 'svn status' use-case (which is always run with the -v
100#     (--verbose) flag), each line of output contains a working revision
101#     number and a two-letter status code similar to the 'svn co/up'
102#     case.  The repository revision is also printed.  All of this
103#     information is stored in attributes named 'wc_rev', 'status', and
104#     'repos_rev', respectively.
105
106#   - in the working-copy use-case, the att-hash is ignored.
107
108
109# Finally, one last explanation: the file 'actions.py' contain a number
110# of helper routines named 'run_and_verify_FOO'.  These routines take
111# one or more "expected" trees as input, then run some svn subcommand,
112# then push the output through an appropriate parser to derive an
113# "actual" tree.  Then it runs compare_trees() and returns the result.
114# This is why most tests typically end with a call to
115# run_and_verify_FOO().
116
117
118
119
120# A node in a tree.
121#
122# If CHILDREN is None, then the node is a file.  Otherwise, CHILDREN
123# is a list of the nodes making up that directory's children.
124#
125# NAME is simply the name of the file or directory.  CONTENTS is a
126# string that contains the file's contents (if a file), PROPS are
127# properties attached to files or dirs, and ATTS is a dictionary of
128# other metadata attached to the node.
129
130class SVNTreeNode:
131
132  def __init__(self, name, children=None, contents=None, props={}, atts={}):
133    self.name = name
134    self.mtime = 0
135    self.children = children
136    self.contents = contents
137    self.props = props
138    self.atts = atts
139    self.path = name
140
141# TODO: Check to make sure contents and children are mutually exclusive
142
143  def add_child(self, newchild):
144    if self.children is None:  # if you're a file,
145      self.children = []     # become an empty dir.
146    child_already_exists = 0
147    for a in self.children:
148      if a.name == newchild.name:
149        child_already_exists = 1
150        break
151    if child_already_exists == 0:
152      self.children.append(newchild)
153      newchild.path = os.path.join (self.path, newchild.name)     
154
155    # If you already have the node,
156    else:     
157      if newchild.children is None:
158        # this is the 'end' of the chain, so copy any content here.
159        a.contents = newchild.contents
160        a.props = newchild.props
161        a.atts = newchild.atts
162        a.path = os.path.join (self.path, newchild.name)
163      else:
164        # try to add dangling children to your matching node
165        for i in newchild.children:
166          a.add_child(i)
167
168
169  def pprint(self):
170    print " * Node name:  ", self.name
171    print "    Path:      ", self.path
172    print "    Contents:  ", self.contents
173    print "    Properties:", self.props
174    print "    Attributes:", self.atts
175    ### FIXME: I'd like to be able to tell the difference between
176    ### self.children is None (file) and self.children == [] (empty
177    ### diretory), but it seems that most places that construct
178    ### SVNTreeNode objects don't even try to do that.  --xbc
179    if self.children is not None:
180      print "    Children:  ", len(self.children)
181    else:
182      print "    Children: is a file."
183
184# reserved name of the root of the tree
185
186root_node_name = "__SVN_ROOT_NODE"
187
188# Exception raised if you screw up in this module.
189
190class SVNTreeError(Exception): pass
191
192# Exception raised if two trees are unequal
193
194class SVNTreeUnequal(Exception): pass
195
196# Exception raised if one node is file and other is dir
197
198class SVNTypeMismatch(Exception): pass
199
200# Exception raised if get_child is passed a file.
201
202class SVNTreeIsNotDirectory(Exception): pass
203
204
205# Some attributes 'stack' on each other if the same node is added
206# twice to a tree.  Place all such special cases in here.
207def attribute_merge(orighash, newhash):
208  "Merge the attributes in NEWHASH into ORIGHASH."
209
210  if orighash.has_key('verb') and newhash.has_key('verb'):
211    # Special case: if a commit reports a node as "deleted", then
212    # "added", it's a replacment.
213    if orighash['verb'] == "Deleting":
214      if newhash['verb'] == "Adding":
215        orighash['verb'] = "Replacing"
216
217  # Add future stackable attributes here...
218
219  return orighash
220
221
222# helper func
223def add_elements_as_path(top_node, element_list):
224  """Add the elements in ELEMENT_LIST as if they were a single path
225  below TOP_NODE."""
226
227  # The idea of this function is to take a list like so:
228  # ['A', 'B', 'C'] and a top node, say 'Z', and generate a tree
229  # like this:
230  #
231  #             Z -> A -> B -> C
232  #
233  # where 1 -> 2 means 2 is a child of 1.
234  #
235
236  prev_node = top_node
237  for i in element_list:
238    new_node = SVNTreeNode(i, None)
239    prev_node.add_child(new_node)
240    prev_node = new_node
241
242
243# Sorting function -- sort 2 nodes by their names.
244def node_is_greater(a, b):
245  "Sort the names of two nodes."
246  # Interal use only
247  if a.name == b.name:
248    return 0
249  if a.name > b.name:
250    return 1
251  else:
252    return -1
253
254
255# Helper for compare_trees
256def compare_file_nodes(a, b):
257  """Compare two nodes' names, contents, and properties, ignoring
258  children.  Return 0 if the same, 1 otherwise."""
259  if a.name != b.name:
260    return 1
261  if a.contents != b.contents:
262    return 1
263  if a.props != b.props:
264    return 1
265  if a.atts != b.atts:
266    return 1
267
268
269# Internal utility used by most build_tree_from_foo() routines.
270#
271# (Take the output and .add_child() it to a root node.)
272
273def create_from_path(path, contents=None, props={}, atts={}):
274  """Create and return a linked list of treenodes, given a PATH
275  representing a single entry into that tree.  CONTENTS and PROPS are
276  optional arguments that will be deposited in the tail node."""
277
278  # get a list of all the names in the path
279  # each of these will be a child of the former
280  elements = path.split("/")
281  if len(elements) == 0:
282    raise SVNTreeError
283
284  root_node = SVNTreeNode(elements[0], None)
285
286  add_elements_as_path(root_node, elements[1:])
287
288  # deposit contents in the very last node.
289  node = root_node
290  while 1:
291    if node.children is None:
292      node.contents = contents
293      node.props = props
294      node.atts = atts
295      break
296    node = node.children[0]
297
298  return root_node
299
300
301# helper for handle_dir(), which is a helper for build_tree_from_wc()
302def get_props(path):
303  "Return a hash of props for PATH, using the svn client."
304
305  # It's not kosher to look inside SVN/ and try to read the internal
306  # property storage format.  Instead, we use 'svn proplist'.  After
307  # all, this is the only way the user can retrieve them, so we're
308  # respecting the black-box paradigm.
309
310  props = {}
311  output, errput = main.run_svn(1, "proplist", path, "--verbose")
312
313  for line in output:
314    name, value = line.split(' : ')
315    name = string.strip(name)
316    value = string.strip(value)
317    props[name] = value
318
319  return props
320
321
322# helper for handle_dir(), which helps build_tree_from_wc()
323def get_text(path):
324  "Return a string with the textual contents of a file at PATH."
325
326  # sanity check
327  if not os.path.isfile(path):
328    return None
329
330  fp = open(path, 'r')
331  contents = fp.read()
332  fp.close()
333  return contents
334
335
336# main recursive helper for build_tree_from_wc()
337def handle_dir(path, current_parent, load_props, ignore_svn):
338
339  # get a list of all the files
340  all_files = os.listdir(path)
341  files = []
342  dirs = []
343
344  # put dirs and files in their own lists, and remove SVN dirs
345  for f in all_files:
346    f = os.path.join(path, f)
347    if (os.path.isdir(f) and os.path.basename(f) != 'SVN'):
348      dirs.append(f)
349    elif os.path.isfile(f):
350      files.append(f)
351
352  # add each file as a child of CURRENT_PARENT
353  for f in files:
354    fcontents = get_text(f)
355    if load_props:
356      fprops = get_props(f)
357    else:
358      fprops = {}
359    c = SVNTreeNode(os.path.basename(f), None,
360                                         fcontents, fprops)
361    c.mtime = os.stat(f)[stat.ST_MTIME]
362    current_parent.add_child(c)
363
364  # for each subdir, create a node, walk its tree, add it as a child
365  for d in dirs:
366    if load_props:
367      dprops = get_props(d)
368    else:
369      dprops = {}
370    new_dir_node = SVNTreeNode(os.path.basename(d), [], None, dprops)
371    handle_dir(d, new_dir_node, load_props, ignore_svn)
372    new_dir_node.mtime = os.stat(f)[stat.ST_MTIME]
373    current_parent.add_child(new_dir_node)
374
375def get_child(node, name):
376  """If SVNTreeNode NODE contains a child named NAME, return child;
377  else, return None. If SVNTreeNode is not a directory, raise a
378  SVNTreeIsNotDirectory exception"""
379  if node.children == None:
380    raise SVNTreeIsNotDirectory
381  for n in node.children:
382    if (name == n.name):
383      return n
384  return None
385
386
387# Helper for compare_trees
388def default_singleton_handler(a, baton):
389  "Printing SVNTreeNode A's name, then raise SVNTreeUnequal."
390  print "Got singleton", a.name
391  a.pprint()
392  raise SVNTreeUnequal
393
394
395###########################################################################
396###########################################################################
397# EXPORTED ROUTINES ARE BELOW
398
399
400# Main tree comparison routine!
401
402def compare_trees(a, b,
403                  singleton_handler_a = None,
404                  a_baton = None,
405                  singleton_handler_b = None,
406                  b_baton = None):
407  """Compare SVNTreeNodes A and B, expressing differences using FUNC_A
408  and FUNC_B.  FUNC_A and FUNC_B are functions of two arguments (a
409  SVNTreeNode and a context baton), and may raise exception
410  SVNTreeUnequal.  Their return value is ignored.
411
412  If A and B are both files, then return 0 if their contents,
413  properties, and names are all the same; else raise a SVNTreeUnequal.
414  If A is a file and B is a directory, raise a SVNTypeMismatch; same
415  vice-versa.  If both are directories, then for each entry that
416  exists in both, call compare_trees on the two entries; otherwise, if
417  the entry exists only in A, invoke FUNC_A on it, and likewise for
418  B with FUNC_B."""
419
420  def display_nodes(a, b):
421    'Display two nodes, expected and actual.'
422    print "============================================================="
423    print "Expected", b.name, "and actual", a.name, "are different!"
424    print "============================================================="
425    print "EXPECTED NODE TO BE:"
426    print "============================================================="
427    b.pprint()
428    print "============================================================="
429    print "ACTUAL NODE FOUND:"
430    print "============================================================="
431    a.pprint()
432
433  # Setup singleton handlers
434  if (singleton_handler_a is None):
435    singleton_handler_a = default_singleton_handler
436  if (singleton_handler_b is None):
437    singleton_handler_b = default_singleton_handler
438
439  try:
440    # A and B are both files.
441    if ((a.children is None) and (b.children is None)):
442      if compare_file_nodes(a, b):
443        display_nodes(a, b)
444        raise main.SVNTreeUnequal
445    # One is a file, one is a directory.
446    elif (((a.children is None) and (b.children is not None))
447          or ((a.children is not None) and (b.children is None))):
448      display_nodes(a, b)
449      raise main.SVNTypeMismatch
450    # They're both directories.
451    else:
452      # First, compare the directories' two hashes.
453      if (a.props != b.props) or (a.atts != b.atts):
454        display_nodes(a, b)
455        raise main.SVNTreeUnequal
456
457      accounted_for = []
458      # For each child of A, check and see if it's in B.  If so, run
459      # compare_trees on the two children and add b's child to
460      # accounted_for.  If not, run FUNC_A on the child.  Next, for each
461      # child of B, check and see if it's in accounted_for.  If it is,
462      # do nothing. If not, run FUNC_B on it.
463      for a_child in a.children:
464        b_child = get_child(b, a_child.name)
465        if b_child:
466          accounted_for.append(b_child)
467          compare_trees(a_child, b_child,
468                        singleton_handler_a, a_baton,
469                        singleton_handler_b, b_baton)
470        else:
471          singleton_handler_a(a_child, a_baton)
472      for b_child in b.children:
473        if (b_child not in accounted_for):
474          singleton_handler_b(b_child, b_baton)
475      return 0
476  except SVNTypeMismatch:
477    print 'Unequal Types: one Node is a file, the other is a directory'
478    raise SVNTreeUnequal
479  except SVNTreeIsNotDirectory:
480    print "Error: Foolish call to get_child."
481    sys.exit(1)
482  except IndexError:
483    print "Error: unequal number of children"
484    raise SVNTreeUnequal
485  except SVNTreeUnequal:
486    if a.name == root_node_name:
487      return 1
488    else:
489      print "Unequal at node %s" % a.name
490      raise SVNTreeUnequal
491  return 0
492
493
494
495
496# Visually show a tree's structure
497
498def dump_tree(n,indent=""):
499  "Print out a nice representation of the tree's structure."
500
501  # Code partially stolen from Dave Beazley
502  if n.children is None:
503    tmp_children = []
504  else:
505    tmp_children = n.children
506
507  if n.name == root_node_name:
508    print "%s%s" % (indent, "ROOT")
509  else:
510    print "%s%s" % (indent, n.name)
511
512  indent = indent.replace("-"," ")
513  indent = indent.replace("+"," ")
514  for i in range(len(tmp_children)):
515    c = tmp_children[i]
516    if i == len(tmp_children
517                )-1:
518      dump_tree(c,indent + "  +-- ")
519    else:
520      dump_tree(c,indent + "  |-- ")
521
522
523###################################################################
524###################################################################
525# PARSERS that return trees made of SVNTreeNodes....
526
527
528###################################################################
529# Build an "expected" static tree from a list of lists
530
531
532# Create a list of lists, of the form:
533#
534#  [ [path, contents, props, atts], ... ]
535#
536#  and run it through this parser.  PATH is a string, a path to the
537#  object.  CONTENTS is either a string or None, and PROPS and ATTS are
538#  populated dictionaries or {}.  Each CONTENTS/PROPS/ATTS will be
539#  attached to the basename-node of the associated PATH.
540
541def build_generic_tree(nodelist):
542  "Given a list of lists of a specific format, return a tree."
543
544  root = SVNTreeNode(root_node_name)
545
546  for list in nodelist:
547    new_branch = create_from_path(list[0], list[1], list[2], list[3])
548    root.add_child(new_branch)
549
550  return root
551
552
553####################################################################
554# Build trees from different kinds of subcommand output.
555
556
557# Parse co/up output into a tree.
558#
559#   Tree nodes will contain no contents, and only one 'status' att.
560
561def build_tree_from_checkout(lines):
562  "Return a tree derived by parsing the output LINES from 'co' or 'up'."
563
564  root = SVNTreeNode(root_node_name)
565  rm = re.compile ('^([MAGCUD_ ][MAGCUD_ ]) (.+)')
566 
567  for line in lines:
568    match = rm.search(line)
569    if match and match.groups():
570      new_branch = create_from_path(match.group(2), None, {},
571                                    {'status' : match.group(1)})
572      root.add_child(new_branch)
573
574  return root
575
576
577# Parse ci/im output into a tree.
578#
579#   Tree nodes will contain no contents, and only one 'verb' att.
580
581def build_tree_from_commit(lines):
582  "Return a tree derived by parsing the output LINES from 'ci' or 'im'."
583
584  # Lines typically have a verb followed by whitespace then a path.
585  root = SVNTreeNode(root_node_name)
586  rm1 = re.compile ('^(\w+)\s+(.+)')
587  rm2 = re.compile ('^Transmitting')
588 
589  for line in lines:
590    match = rm2.search(line)
591    if not match:
592      match = rm1.search(line)
593      if match and match.groups():
594        new_branch = create_from_path(match.group(2), None, {},
595                                      {'verb' : match.group(1)})
596        root.add_child(new_branch)
597
598  return root
599
600
601# Parse status output into a tree.
602#
603#   Tree nodes will contain no contents, and these atts:
604#
605#          'status', 'wc_rev', 'repos_rev'
606#             ... and possibly 'locked', 'copied', IFF columns non-empty.
607#
608
609def build_tree_from_status(lines):
610  "Return a tree derived by parsing the output LINES from 'st'."
611
612  root = SVNTreeNode(root_node_name)
613  rm = re.compile ('^.+\:.+(\d+)')
614  lastline = string.strip(lines.pop())
615  match = rm.search(lastline)
616  if match and match.groups():
617    repos_rev = match.group(1)
618  else:
619    repos_rev = '?'
620   
621  # Try http://www.wordsmith.org/anagram/anagram.cgi?anagram=ACDRMGU
622  rm = re.compile ('^([MACDRUG_ ][MACDRUG_ ])(.)(.)   .   [^0-9-]+(\d+|-)(.{23})(.+)')
623  for line in lines:
624    match = rm.search(line)
625    if match and match.groups():
626      if match.group(5) != '-': # ignore items that only exist on repos
627        atthash = {'status' : match.group(1),
628                   'wc_rev' : match.group(4),
629                   'repos_rev' : repos_rev}
630        if match.group(2) != ' ':
631          atthash['locked'] = match.group(2)
632        if match.group(3) != ' ':
633          atthash['copied'] = match.group(3)
634        new_branch = create_from_path(match.group(6), None, {}, atthash)
635
636      root.add_child(new_branch)
637
638  return root
639
640
641####################################################################
642# Build trees by looking at the working copy
643
644
645#   The reason the 'load_props' flag is off by default is because it
646#   creates a drastic slowdown -- we spawn a new 'svn proplist'
647#   process for every file and dir in the working copy!
648
649
650def build_tree_from_wc(wc_path, load_props=0, ignore_svn=1):
651    """Takes WC_PATH as the path to a working copy.  Walks the tree below
652    that path, and creates the tree based on the actual found
653    files.  If IGNORE_SVN is true, then exclude SVN dirs from the tree.
654    If LOAD_PROPS is true, the props will be added to the tree."""
655
656    root = SVNTreeNode(root_node_name, None)
657
658    # if necessary, store the root dir's props in the root node.
659    if load_props:
660      root.props = get_props(wc_path)
661
662    # Walk the tree recursively
663    handle_dir(os.path.normpath(wc_path), root, load_props, ignore_svn)
664
665    return root
666
667### End of file.
668# local variables:
669# eval: (load-file "../../../../../tools/dev/svn-dev.el")
670# end:
Note: See TracBrowser for help on using the repository browser.