diff options
Diffstat (limited to 'tools/build/v2/test/svn_tree.py')
-rw-r--r-- | tools/build/v2/test/svn_tree.py | 668 |
1 files changed, 668 insertions, 0 deletions
diff --git a/tools/build/v2/test/svn_tree.py b/tools/build/v2/test/svn_tree.py new file mode 100644 index 0000000000..74bceb7f57 --- /dev/null +++ b/tools/build/v2/test/svn_tree.py @@ -0,0 +1,668 @@ +#!/usr/bin/env python +# +# tree.py: tools for comparing directory trees +# +# Subversion is a tool for revision control. +# See http://subversion.tigris.org for more information. +# +# ==================================================================== +# Copyright (c) 2001 Sam Tobin-Hochstadt. 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://subversion.tigris.org/license-1.html. +# If newer versions of this license are posted there, you may use a +# newer version instead, at your option. +# +###################################################################### + +# This file was modified by Vladimir Prus to store modification times in tree +# nodes. + +import re +import string +import os.path +import os +import stat + + +#======================================================================== + +# ===> Overview of our Datastructures <=== + +# The general idea here is that many, many things can be represented by +# a tree structure: + +# - a working copy's structure and contents +# - the output of 'svn status' +# - the output of 'svn checkout/update' +# - the output of 'svn commit' + +# The idea is that a test function creates a "expected" tree of some +# kind, and is then able to compare it to an "actual" tree that comes +# from running the Subversion client. This is what makes a test +# automated; if an actual and expected tree match exactly, then the test +# has passed. (See compare_trees() below.) + +# The SVNTreeNode class is the fundamental data type used to build tree +# structures. The class contains a method for "dropping" a new node +# into an ever-growing tree structure. (See also create_from_path()). + +# We have four parsers in this file for the four use cases listed above: +# each parser examines some kind of input and returns a tree of +# SVNTreeNode objects. (See build_tree_from_checkout(), +# build_tree_from_commit(), build_tree_from_status(), and +# build_tree_from_wc()). These trees are the "actual" trees that result +# from running the Subversion client. + +# Also necessary, of course, is a convenient way for a test to create an +# "expected" tree. The test *could* manually construct and link a bunch +# of SVNTreeNodes, certainly. But instead, all the tests are using the +# build_generic_tree() routine instead. + +# build_generic_tree() takes a specially-formatted list of lists as +# input, and returns a tree of SVNTreeNodes. The list of lists has this +# structure: + +# [ ['/full/path/to/item', 'text contents', {prop-hash}, {att-hash}], +# [...], +# [...], +# ... ] + +# You can see that each item in the list essentially defines an +# SVNTreeNode. build_generic_tree() instantiates a SVNTreeNode for each +# item, and then drops it into a tree by parsing each item's full path. + +# So a typical test routine spends most of its time preparing lists of +# this format and sending them to build_generic_tree(), rather than +# building the "expected" trees directly. + +# ### Note: in the future, we'd like to remove this extra layer of +# ### abstraction. We'd like the SVNTreeNode class to be more +# ### directly programmer-friendly, providing a number of accessor +# ### routines, so that tests can construct trees directly. + +# The first three fields of each list-item are self-explanatory. It's +# the fourth field, the "attribute" hash, that needs some explanation. +# The att-hash is used to place extra information about the node itself, +# depending on the parsing context: + +# - in the 'svn co/up' use-case, each line of output starts with two +# characters from the set of (A, D, G, U, C, _). This status code +# is stored in a attribute named 'status'. + +# - in the 'svn ci/im' use-case, each line of output starts with one +# of the words (Adding, Deleting, Sending). This verb is stored in +# an attribute named 'verb'. + +# - in the 'svn status' use-case (which is always run with the -v +# (--verbose) flag), each line of output contains a working revision +# number and a two-letter status code similar to the 'svn co/up' +# case. The repository revision is also printed. All of this +# information is stored in attributes named 'wc_rev', 'status', and +# 'repos_rev', respectively. + +# - in the working-copy use-case, the att-hash is ignored. + + +# Finally, one last explanation: the file 'actions.py' contain a number +# of helper routines named 'run_and_verify_FOO'. These routines take +# one or more "expected" trees as input, then run some svn subcommand, +# then push the output through an appropriate parser to derive an +# "actual" tree. Then it runs compare_trees() and returns the result. +# This is why most tests typically end with a call to +# run_and_verify_FOO(). + + + + +# A node in a tree. +# +# If CHILDREN is None, then the node is a file. Otherwise, CHILDREN +# is a list of the nodes making up that directory's children. +# +# NAME is simply the name of the file or directory. CONTENTS is a +# string that contains the file's contents (if a file), PROPS are +# properties attached to files or dirs, and ATTS is a dictionary of +# other metadata attached to the node. + +class SVNTreeNode: + + def __init__(self, name, children=None, contents=None, props={}, atts={}): + self.name = name + self.mtime = 0 + self.children = children + self.contents = contents + self.props = props + self.atts = atts + self.path = name + +# TODO: Check to make sure contents and children are mutually exclusive + + def add_child(self, newchild): + if self.children is None: # if you're a file, + self.children = [] # become an empty dir. + child_already_exists = 0 + for a in self.children: + if a.name == newchild.name: + child_already_exists = 1 + break + if child_already_exists == 0: + self.children.append(newchild) + newchild.path = os.path.join (self.path, newchild.name) + + # If you already have the node, + else: + if newchild.children is None: + # this is the 'end' of the chain, so copy any content here. + a.contents = newchild.contents + a.props = newchild.props + a.atts = newchild.atts + a.path = os.path.join (self.path, newchild.name) + else: + # try to add dangling children to your matching node + for i in newchild.children: + a.add_child(i) + + + def pprint(self): + print " * Node name: ", self.name + print " Path: ", self.path + print " Contents: ", self.contents + print " Properties:", self.props + print " Attributes:", self.atts + ### FIXME: I'd like to be able to tell the difference between + ### self.children is None (file) and self.children == [] (empty + ### diretory), but it seems that most places that construct + ### SVNTreeNode objects don't even try to do that. --xbc + if self.children is not None: + print " Children: ", len(self.children) + else: + print " Children: is a file." + +# reserved name of the root of the tree + +root_node_name = "__SVN_ROOT_NODE" + +# Exception raised if you screw up in this module. + +class SVNTreeError(Exception): pass + +# Exception raised if two trees are unequal + +class SVNTreeUnequal(Exception): pass + +# Exception raised if one node is file and other is dir + +class SVNTypeMismatch(Exception): pass + +# Exception raised if get_child is passed a file. + +class SVNTreeIsNotDirectory(Exception): pass + + +# Some attributes 'stack' on each other if the same node is added +# twice to a tree. Place all such special cases in here. +def attribute_merge(orighash, newhash): + "Merge the attributes in NEWHASH into ORIGHASH." + + if orighash.has_key('verb') and newhash.has_key('verb'): + # Special case: if a commit reports a node as "deleted", then + # "added", it's a replacment. + if orighash['verb'] == "Deleting": + if newhash['verb'] == "Adding": + orighash['verb'] = "Replacing" + + # Add future stackable attributes here... + + return orighash + + +# helper func +def add_elements_as_path(top_node, element_list): + """Add the elements in ELEMENT_LIST as if they were a single path + below TOP_NODE.""" + + # The idea of this function is to take a list like so: + # ['A', 'B', 'C'] and a top node, say 'Z', and generate a tree + # like this: + # + # Z -> A -> B -> C + # + # where 1 -> 2 means 2 is a child of 1. + # + + prev_node = top_node + for i in element_list: + new_node = SVNTreeNode(i, None) + prev_node.add_child(new_node) + prev_node = new_node + + +# Sorting function -- sort 2 nodes by their names. +def node_is_greater(a, b): + "Sort the names of two nodes." + # Interal use only + if a.name == b.name: + return 0 + if a.name > b.name: + return 1 + else: + return -1 + + +# Helper for compare_trees +def compare_file_nodes(a, b): + """Compare two nodes' names, contents, and properties, ignoring + children. Return 0 if the same, 1 otherwise.""" + if a.name != b.name: + return 1 + if a.contents != b.contents: + return 1 + if a.props != b.props: + return 1 + if a.atts != b.atts: + return 1 + + +# Internal utility used by most build_tree_from_foo() routines. +# +# (Take the output and .add_child() it to a root node.) + +def create_from_path(path, contents=None, props={}, atts={}): + """Create and return a linked list of treenodes, given a PATH + representing a single entry into that tree. CONTENTS and PROPS are + optional arguments that will be deposited in the tail node.""" + + # get a list of all the names in the path + # each of these will be a child of the former + elements = path.split("/") + if len(elements) == 0: + raise SVNTreeError + + root_node = SVNTreeNode(elements[0], None) + + add_elements_as_path(root_node, elements[1:]) + + # deposit contents in the very last node. + node = root_node + while 1: + if node.children is None: + node.contents = contents + node.props = props + node.atts = atts + break + node = node.children[0] + + return root_node + + +# helper for handle_dir(), which is a helper for build_tree_from_wc() +def get_props(path): + "Return a hash of props for PATH, using the svn client." + + # It's not kosher to look inside SVN/ and try to read the internal + # property storage format. Instead, we use 'svn proplist'. After + # all, this is the only way the user can retrieve them, so we're + # respecting the black-box paradigm. + + props = {} + output, errput = main.run_svn(1, "proplist", path, "--verbose") + + for line in output: + name, value = line.split(' : ') + name = string.strip(name) + value = string.strip(value) + props[name] = value + + return props + + +# helper for handle_dir(), which helps build_tree_from_wc() +def get_text(path): + "Return a string with the textual contents of a file at PATH." + + # sanity check + if not os.path.isfile(path): + return None + + fp = open(path, 'r') + contents = fp.read() + fp.close() + return contents + + +# main recursive helper for build_tree_from_wc() +def handle_dir(path, current_parent, load_props, ignore_svn): + + # get a list of all the files + all_files = os.listdir(path) + files = [] + dirs = [] + + # put dirs and files in their own lists, and remove SVN dirs + for f in all_files: + f = os.path.join(path, f) + if (os.path.isdir(f) and os.path.basename(f) != 'SVN'): + dirs.append(f) + elif os.path.isfile(f): + files.append(f) + + # add each file as a child of CURRENT_PARENT + for f in files: + fcontents = get_text(f) + if load_props: + fprops = get_props(f) + else: + fprops = {} + c = SVNTreeNode(os.path.basename(f), None, + fcontents, fprops) + c.mtime = os.stat(f)[stat.ST_MTIME] + current_parent.add_child(c) + + # for each subdir, create a node, walk its tree, add it as a child + for d in dirs: + if load_props: + dprops = get_props(d) + else: + dprops = {} + new_dir_node = SVNTreeNode(os.path.basename(d), [], None, dprops) + handle_dir(d, new_dir_node, load_props, ignore_svn) + new_dir_node.mtime = os.stat(f)[stat.ST_MTIME] + current_parent.add_child(new_dir_node) + +def get_child(node, name): + """If SVNTreeNode NODE contains a child named NAME, return child; + else, return None. If SVNTreeNode is not a directory, raise a + SVNTreeIsNotDirectory exception""" + if node.children == None: + raise SVNTreeIsNotDirectory + for n in node.children: + if (name == n.name): + return n + return None + + +# Helper for compare_trees +def default_singleton_handler(a, baton): + "Printing SVNTreeNode A's name, then raise SVNTreeUnequal." + print "Got singleton", a.name + a.pprint() + raise SVNTreeUnequal + + +########################################################################### +########################################################################### +# EXPORTED ROUTINES ARE BELOW + + +# Main tree comparison routine! + +def compare_trees(a, b, + singleton_handler_a = None, + a_baton = None, + singleton_handler_b = None, + b_baton = None): + """Compare SVNTreeNodes A and B, expressing differences using FUNC_A + and FUNC_B. FUNC_A and FUNC_B are functions of two arguments (a + SVNTreeNode and a context baton), and may raise exception + SVNTreeUnequal. Their return value is ignored. + + If A and B are both files, then return 0 if their contents, + properties, and names are all the same; else raise a SVNTreeUnequal. + If A is a file and B is a directory, raise a SVNTypeMismatch; same + vice-versa. If both are directories, then for each entry that + exists in both, call compare_trees on the two entries; otherwise, if + the entry exists only in A, invoke FUNC_A on it, and likewise for + B with FUNC_B.""" + + def display_nodes(a, b): + 'Display two nodes, expected and actual.' + print "=============================================================" + print "Expected", b.name, "and actual", a.name, "are different!" + print "=============================================================" + print "EXPECTED NODE TO BE:" + print "=============================================================" + b.pprint() + print "=============================================================" + print "ACTUAL NODE FOUND:" + print "=============================================================" + a.pprint() + + # Setup singleton handlers + if (singleton_handler_a is None): + singleton_handler_a = default_singleton_handler + if (singleton_handler_b is None): + singleton_handler_b = default_singleton_handler + + try: + # A and B are both files. + if ((a.children is None) and (b.children is None)): + if compare_file_nodes(a, b): + display_nodes(a, b) + raise main.SVNTreeUnequal + # One is a file, one is a directory. + elif (((a.children is None) and (b.children is not None)) + or ((a.children is not None) and (b.children is None))): + display_nodes(a, b) + raise main.SVNTypeMismatch + # They're both directories. + else: + # First, compare the directories' two hashes. + if (a.props != b.props) or (a.atts != b.atts): + display_nodes(a, b) + raise main.SVNTreeUnequal + + accounted_for = [] + # For each child of A, check and see if it's in B. If so, run + # compare_trees on the two children and add b's child to + # accounted_for. If not, run FUNC_A on the child. Next, for each + # child of B, check and see if it's in accounted_for. If it is, + # do nothing. If not, run FUNC_B on it. + for a_child in a.children: + b_child = get_child(b, a_child.name) + if b_child: + accounted_for.append(b_child) + compare_trees(a_child, b_child, + singleton_handler_a, a_baton, + singleton_handler_b, b_baton) + else: + singleton_handler_a(a_child, a_baton) + for b_child in b.children: + if (b_child not in accounted_for): + singleton_handler_b(b_child, b_baton) + return 0 + except SVNTypeMismatch: + print 'Unequal Types: one Node is a file, the other is a directory' + raise SVNTreeUnequal + except SVNTreeIsNotDirectory: + print "Error: Foolish call to get_child." + sys.exit(1) + except IndexError: + print "Error: unequal number of children" + raise SVNTreeUnequal + except SVNTreeUnequal: + if a.name == root_node_name: + return 1 + else: + print "Unequal at node %s" % a.name + raise SVNTreeUnequal + return 0 + + + + +# Visually show a tree's structure + +def dump_tree(n,indent=""): + "Print out a nice representation of the tree's structure." + + # Code partially stolen from Dave Beazley. + if n.children is None: + tmp_children = [] + else: + tmp_children = n.children + + if n.name == root_node_name: + print "%s%s" % (indent, "ROOT") + else: + print "%s%s" % (indent, n.name) + + indent = indent.replace("-", " ") + indent = indent.replace("+", " ") + for i in range(len(tmp_children)): + c = tmp_children[i] + if i == len(tmp_children) - 1: + dump_tree(c,indent + " +-- ") + else: + dump_tree(c,indent + " |-- ") + + +################################################################### +################################################################### +# PARSERS that return trees made of SVNTreeNodes.... + + +################################################################### +# Build an "expected" static tree from a list of lists + + +# Create a list of lists, of the form: +# +# [ [path, contents, props, atts], ... ] +# +# and run it through this parser. PATH is a string, a path to the +# object. CONTENTS is either a string or None, and PROPS and ATTS are +# populated dictionaries or {}. Each CONTENTS/PROPS/ATTS will be +# attached to the basename-node of the associated PATH. + +def build_generic_tree(nodelist): + "Given a list of lists of a specific format, return a tree." + + root = SVNTreeNode(root_node_name) + + for list in nodelist: + new_branch = create_from_path(list[0], list[1], list[2], list[3]) + root.add_child(new_branch) + + return root + + +#################################################################### +# Build trees from different kinds of subcommand output. + + +# Parse co/up output into a tree. +# +# Tree nodes will contain no contents, and only one 'status' att. + +def build_tree_from_checkout(lines): + "Return a tree derived by parsing the output LINES from 'co' or 'up'." + + root = SVNTreeNode(root_node_name) + rm = re.compile ('^([MAGCUD_ ][MAGCUD_ ]) (.+)') + + for line in lines: + match = rm.search(line) + if match and match.groups(): + new_branch = create_from_path(match.group(2), None, {}, + {'status' : match.group(1)}) + root.add_child(new_branch) + + return root + + +# Parse ci/im output into a tree. +# +# Tree nodes will contain no contents, and only one 'verb' att. + +def build_tree_from_commit(lines): + "Return a tree derived by parsing the output LINES from 'ci' or 'im'." + + # Lines typically have a verb followed by whitespace then a path. + root = SVNTreeNode(root_node_name) + rm1 = re.compile ('^(\w+)\s+(.+)') + rm2 = re.compile ('^Transmitting') + + for line in lines: + match = rm2.search(line) + if not match: + match = rm1.search(line) + if match and match.groups(): + new_branch = create_from_path(match.group(2), None, {}, + {'verb' : match.group(1)}) + root.add_child(new_branch) + + return root + + +# Parse status output into a tree. +# +# Tree nodes will contain no contents, and these atts: +# +# 'status', 'wc_rev', 'repos_rev' +# ... and possibly 'locked', 'copied', IFF columns non-empty. +# + +def build_tree_from_status(lines): + "Return a tree derived by parsing the output LINES from 'st'." + + root = SVNTreeNode(root_node_name) + rm = re.compile ('^.+\:.+(\d+)') + lastline = string.strip(lines.pop()) + match = rm.search(lastline) + if match and match.groups(): + repos_rev = match.group(1) + else: + repos_rev = '?' + + # Try http://www.wordsmith.org/anagram/anagram.cgi?anagram=ACDRMGU + rm = re.compile ('^([MACDRUG_ ][MACDRUG_ ])(.)(.) . [^0-9-]+(\d+|-)(.{23})(.+)') + for line in lines: + match = rm.search(line) + if match and match.groups(): + if match.group(5) != '-': # ignore items that only exist on repos + atthash = {'status' : match.group(1), + 'wc_rev' : match.group(4), + 'repos_rev' : repos_rev} + if match.group(2) != ' ': + atthash['locked'] = match.group(2) + if match.group(3) != ' ': + atthash['copied'] = match.group(3) + new_branch = create_from_path(match.group(6), None, {}, atthash) + + root.add_child(new_branch) + + return root + + +#################################################################### +# Build trees by looking at the working copy + + +# The reason the 'load_props' flag is off by default is because it +# creates a drastic slowdown -- we spawn a new 'svn proplist' +# process for every file and dir in the working copy! + + +def build_tree_from_wc(wc_path, load_props=0, ignore_svn=1): + """Takes WC_PATH as the path to a working copy. Walks the tree below + that path, and creates the tree based on the actual found + files. If IGNORE_SVN is true, then exclude SVN dirs from the tree. + If LOAD_PROPS is true, the props will be added to the tree.""" + + root = SVNTreeNode(root_node_name, None) + + # if necessary, store the root dir's props in the root node. + if load_props: + root.props = get_props(wc_path) + + # Walk the tree recursively + handle_dir(os.path.normpath(wc_path), root, load_props, ignore_svn) + + return root + +### End of file. +# local variables: +# eval: (load-file "../../../../../tools/dev/svn-dev.el") +# end: |