#!/cad/perltools/ruby/ruby-x86-linux/bin/ruby -w
# p4protect -- process Tensilica-custom perforce protection description files
# Copyright (c) 2005, Tensilica Inc.
# All rights reserved.
#
# Redistribution and use, with or without modification, are permitted provided
# that the following conditions are met:
#
# - Redistributions must retain the above copyright notice, this list of
# conditions, and the following disclaimer.
#
# - Modified software must be plainly marked as such, so as not to be
# misrepresented as being the original software.
#
# - Neither the names of the copyright holders or their contributors, nor
# any of their trademarks, may be used to endorse or promote products or
# services derived from this software without specific prior written
# permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
# See `p4protect -h` for usage info.
#
# $Id: //depot/other/perforce/server/scripts/triggers/p4protect#34 $
"$Revision: #34 $" =~ /\d+/;
PROGVERS = $&;
PROGNAME = "p4protect";
UTNAME = "unit"; # magic unit test name
require 'pp' # Allows pretty-printing like this: pp my_object
require 'P4';
require File.dirname(File.expand_path($0)) + '/P4Triggers';
#require '../P4Triggers';
#require '/home/p4admin/bin/triggers/P4Triggers';
#$p4prog = "p4";
$p4 = nil;
$debug = false;
$errors_found = 0;
$warnings_found = 0;
$too_many_errors = false;
$run_tests = false;
$test_name = nil;
$displayproc = nil;
# These two are set by setup_users_groups():
$p4groups = {}; # hash of groupname => [list of users]
$p4users = {}; # hash of username => substring from 'p4 users' output
$meta_dirname = nil;
META_FILENAME = 'prot_meta';
PermValues = {'DENY'=>0, 'RO'=>1, 'LOCK'=>2, 'SUBMIT'=>3};
# Output a message.
#
def displaymsg(msg)
#$stderr.print "#{PROGNAME}: ";
if $displayproc
$displayproc.call(msg)
else
$stderr.print msg;
end
end
def error(msg)
return if $too_many_errors;
displaymsg "ERROR: #{msg}\n";
$errors_found += 1;
if $errors_found >= 20
displaymsg("ERROR: too many errors (#{$errors_found}), quitting");
$too_many_errors = true;
end
end
def warn(msg)
return if $too_many_errors;
$warnings_found += 1;
displaymsg "WARNING: #{msg}\n";
end
# Parse a single line into tokens.
# Handles double-quoted strings properly.
#
def parse_line(line, context)
tokens = [];
line.chomp!;
while true
line.sub!(/^\s+/, ''); # strip leading whitespace
line.sub!(/^\#.*$/, ''); # strip out comments
return tokens unless line.length > 0;
if line.sub!(/^"/, '') # double-quoted?
if ! line.sub!(/^([^"]*)"/, '')
error("#{context}: unmatched quote");
end
tokens.push($1);
if line =~ /^\S/
error("#{context}: unexpected characters following terminating quote");
end
else
if ! line.sub!(/^\S+/, '')
error("#{context}: internal error parsing '#{line}'");
end
tokens.push($&);
end
# $stderr.print "Parsing <<#{line}>>\n";
# return() unless line ne ''; # skip empty lines
# $stderr.print "Got <<#{line}>>\n";
# (line);
# line.split(' ');
end
end
# Append path to prefix, if path is a relative path.
# If path is a full path, just return path.
#
def pathcat(prefix, path, needfullpath, context)
return path if path =~ %r|^//|;
# print "Path is '#{path}'\n";
error("#{context}: full paths begin with two forward slashes, relative paths begin with none;\n unexpected path '#{path}'") \
if path =~ %r|^/|;
# Here, path is a relative path
return prefix if path.length == 0;
error("#{context}: full (rooted) pathname expected") \
if needfullpath and prefix !~ %r|^//|;
return prefix + ((prefix =~ %r|/$|) ? '' : '/') + path;
end
# Check that path makes sense, and globs are valid:
#
def validatepath(path, context)
# Catch a common error: FIXME could generalize this
error("#{context}: superfluous './' prefix in path") if path =~ /^\.\//;
# Verify that '...' is only at directory boundaries:
mypath = path.dup;
if mypath.gsub!(/\.\.\.([^\/])/, '.../*\1') or
mypath.gsub!(/([^\/])\.\.\./, '\1*/...')
# FIXME: make this an error:
warn( "#{context}: tree wildcard '...' not at directory boundary:\n" +
" '#{path}'\n" +
" Unlike Perforce, we restrict '...' to the boundaries of the path or '/' characters,\n" +
" for clarity. There is no loss of expressiveness. Consider rewriting your path as:\n" +
" '#{mypath}'\n" +
" (if that is what you intended)");
end
# Verify that '/' is only doubled at the beginning:
error("#{context}: separator '/' can only be doubled at start of path") if path =~ %r(.//);
# Verify path does not end in '/':
error("#{context}: path cannot end with '/' separator") if path =~ %r(/$);
# Verify path does not begin with single '/':
error("#{context}: path cannot begin with single '/' separator") if path =~ %r(^/[^/]) or path == '/';
# Verify '*' and '...' are not doubled (note: '*...' is useful and not flagged):
error("#{context}: glob '*' unnecessarily doubled") if path =~ /\*\*/;
error("#{context}: glob '...' unnecessarily doubled") if path =~ /\.\.\.\.\.\./;
error("#{context}: glob '...' unnecessarily followed by '*'") if path =~ /\.\.\.\*/;
# Verify only valid characters are present:
# (NOTE: this ensures '@' is not in path, which is required for path_compare())
error("#{context}: unexpected single quote (') in path: use double quotes around the path to quote spaces") if path =~ /\'/;
error("#{context}: unexpected character '#{$&}' in path") if path =~ /[^-a-zA-Z0-9_. \'\/*]/;
end
# Create (or modify) group 'g' to contain users/groups listed by name in 'list'.
#
def add_group(g, list, fromfile, context)
# Check for ambiguous group/user names:
if $p4users.detect {|u,v| $p4groups[u]}
error("#{context}: group and user have the same name '#{u}'");
end
# Test for invalid group members (Perforce does not check!):
list.find_all {|w| !($p4users[w] or $p4groups[w]) }.each {|w|
if fromfile # file-local g_xxx group?
ug = (w =~ /^G_/) ? "group" : "user"; # member assumed a user or a group?
error("#{context}: local group #{g}: unrecognized #{ug} '#{w}'");
list.delete(w);
else # Perforce-defined G_xxx group?
if w =~ /^G_/
# New Perforce groups can be created regardless of protections,
# so treat non-existent groups as errors:
error("#{context}: Perforce group #{g}: unrecognized group '#{w}'");
list.delete(w);
else
# Protections must specifically allow each new user before they can be created,
# so we have to allow new users here, and assume their existence
# for purposes of protection:
warn("#{context}: Perforce group #{g}: assuming '#{w}' is a new user");
$p4users[w] = true;
end
end
}
# Expand groups within group members:
seen = {}
users = []
while w = list.shift
next if seen[w];
seen[w] = 1;
if $p4users[w]
users.push(w)
else
#$stderr.print "SEE #{w} in #{g}\n";
list.concat($p4groups[w])
end
end
$p4groups[g] = users; # re-assign with expanded list
end
# Read-in user and group definitions from Perforce.
#
def setup_users_groups
# Read in existing users.
# (New users specified in groups will be added later.)
$p4.run_users.each { |user|
user.sub!(/^(\S+)\s.*$/, '\1');
if user =~ /^G_/
warn("#{PROGNAME}: ignoring user '#{user}' incorrectly prefixed with 'G_'"+
"\n (ask your Perforce administrator to fix this)");
next;
end
unless user =~ /^[a-zA-Z]\w*$/
warn("#{PROGNAME}: ignoring user '#{user}' not formed like an identifier"+
"\n (ask your Perforce administrator to fix this)");
next;
end
$p4users[user] = true;
#$stderr.print "USER #{user}\n";
}
# Read in existing groups.
$p4.run_groups.each { |g|
$p4groups[g] = [];
#$stderr.print "GROUP '#{g}'\n";
unless g =~ /^G_/
warn("#{PROGNAME}: ignoring group '#{g}' not prefixed with 'G_'"+
"\n (ask your Perforce administrator to fix this)");
next;
end
unless g =~ /^G_\w+$/
warn("#{PROGNAME}: ignoring group '#{g}' containing non-identifier characters"+
"\n (ask your Perforce administrator to fix this)");
next;
end
process_user_group = false
$p4.run_group('-o', g).join('').split("\n").each { |k|
if (process_user_group)
user_group = k.sub(/^\s+(\w+)/, '\1')
$p4groups[g].push(user_group) if ($1)
end
process_user_group = true if (k =~ /^(Users|Subgroups)/)
}
}
# Verify groups and expand subgroups within groups
# (new users specified in groups are added here):
$p4groups.each {|g,list| add_group(g, list, false, PROGNAME); }
# Add '*' group: # FIXME: can't use "user *" in generated protections, so can't allow this for now:
#add_group('*', $p4users.keys.sort, false, PROGNAME);
# FIXME TODO instead of this, automatically collapse user lists to groups for shorter protection outputs
#$p4groups.each {|g,list|
#$stderr.print "GROUP #{g} = ", list.join("+"), " .\n";
#}
#exit 0;
end
# Verify that Perforce account is logged in.
#
def verify_p4_login
login_string = $p4.run_login('-s')
#system("#{$p4prog} login -s > /dev/null 2>&1");
if (login_string.join("") !~ /ticket expires/)
error("internal error: Perforce administrator account requires login\n(message is #{login_string})");
return false;
end
return true; # all ok
end
# Verify that the specified comma-separated groups/users are valid.
# Remove redundant entries.
# Return array of groups/users.
#
def checkwho(who, context)
seen = {};
users = {};
list = [];
whos = who.split(',');
oopsie = false;
while w = whos.shift
next if seen[w];
seen[w] = 1;
unless $p4groups[w] or $p4users[w]
# oopsie:
if w =~ /^G_/i
error("#{context}: unrecognized group name '#{w}'\n" +
" Valid group names are: " + $p4groups.keys.sort.join(", ") + ".");
else
error("#{context}: unrecognized user name '#{w}'\n" +
" Valid user names are: " + $p4users.keys.sort.join(", ") + ".");
end
oopsie = true;
next
end
if w =~ /^g_/ # file-local group name?
whos = $p4groups[w] + whos; # expand to avoid non-Perforce names
next;
end
list.push(w);
if $p4users[w]
users[w] = 1;
else
$p4groups[w].each {|u| users[u] = 1 }
end
end
error("#{context}: empty list of users in #{context}") unless list.length > 0 and users.length > 0 and !oopsie;
return [list, users];
end
# Structures used to parse protections:
#
ProtEntry = Struct::new(
:perm, # 'DENY', 'RO', 'LOCK', or 'SUBMIT'
:dependent, # true (if '?' after perm keyword) or false
:who, # [array of user and group names, expanded array of users]
:path, # perforce *full* path name, possibly including globs ('...', '*')
:startline, # line at which protection was declared
:perm_overrides # privileges overriden by this entry (hash of perm => 1)
)
ProtLevel = Struct::new(
:prefix, # current 'with' prefix at this level
:prots, # array of ProtEntry and ProtLevel structures
:startline, # line at which scope started
:endline # line at which scope ended
);
# Remove identical (non-glob) characters from left of a and b.
# Return true if only identical characters found,
# false if different (non-glob) characters found.
# (@ considered a glob)
#
def match_trim_left(a, b)
while a != '' and b != '' and a !~ /^[*@]/ and b !~ /^[*@]/
#$stderr.print "a a=#{a} b=#{b} a0=#{a[0]} b0=#{b[0]}\n";
return false if a[0] != b[0]; # differ at start
a.slice!(0,1);
b.slice!(0,1);
end
return true;
end
# Same, for right side of a and b.
def match_trim_right(a, b)
while a != '' and b != '' and a !~ /[*@]$/ and b !~ /[*@]$/
#$stderr.print "b a=#{a} b=#{b}\n";
return false if a[-1] != b[-1]; # differ at end
a.slice!(-1,1);
b.slice!(-1,1);
end
return true;
end
# Given 'path' (Perforce path not containing globs)
# and 'pattern' (Perforce path possibly containing globs),
# return true if path matches pattern, false otherwise.
# Globs include '*' and '...' (and '@' as synonym for '...', for internal use).
#
def path_compare_glob(path, pattern)
pat = pattern.gsub(/\.\.\./, '@'); # normalize
pat.gsub!(/([-.])/, '\\\1'); # escape '.' and '-' characters
pat.gsub!(/\*/, '[^/]*'); # expand '*' globs
pat.gsub!(/\@/, '.*'); # expand '...' globs
re = Regexp.new(pat); # build regular expression
return path =~ re;
end
# Check for overlap between two path components, a and b.
# Neither has '...' nor '/'. But they can have '*'.
# Returns:
# nil no overlap
# 0 a equals b
# 1 a contains b
# 2 b contains a
# 3 partial overlap
#
def do_component_compare(a, b)
return 0 if a == b;
return nil if a !~ /[*@]/ and b !~ /[*@]/; # if no globs, they just differ
aa = a.dup; bb = b.dup;
# Compare non-glob characters at beginning and end:
return nil unless match_trim_left(aa, bb);
return nil unless match_trim_right(aa, bb);
return 1 if aa == '*';
return 2 if bb == '*';
return path_compare_glob(aa, bb) ? 2 : nil if aa !~ /[*@]/; # globs in bb but not aa ?
return path_compare_glob(bb, aa) ? 1 : nil if bb !~ /[*@]/; # globs in aa but not bb ?
return 1 if aa == '*' + bb or aa == bb + '*' or aa == '*' + bb + '*';
return 2 if bb == '*' + aa or bb == aa + '*' or bb == '*' + aa + '*';
# Remaining cases are rather odd. Assume partial overlap for now.
# This is a conservative, fail-safe return value: it may cause a valid
# protection specification to be rejected, but should not (at least with
# current code as of March 2005) cause an invalid / illegal protection
# specification to be accepted. (This statement NOT formally verified)
# FIXME/TODO: determine whether we can do any better.
return 3;
end
def component_compare(a, b)
x = do_component_compare(a,b);
#$stderr.print "'#{a}' <=> '#{b}' is #{x ? x : 'nil'}\n";
return x;
end
# Check for overlap between two Perforce paths, a and b.
# Each path may contain globs ('...' and '*').
# Returns:
# nil no overlap
# 0 a equals b
# 1 a contains b
# 2 b contains a
# 3 partial overlap
# In other words, if any overlap:
# bit 0 is set (1) if a covers more than the overlap
# bit 1 is set (2) if b covers more than the overlap
#
def do_path_compare(apath, bpath)
return 0 if apath == bpath; # paths are identical
aapath = apath.gsub(/\.\.\./, '@'); # convert '...' to '@'
bbpath = bpath.gsub(/\.\.\./, '@');
a = aapath.split(%r(/+)); a[0] = '/' if aapath =~ %r(^//);
b = bbpath.split(%r(/+)); b[0] = '/' if bbpath =~ %r(^//);
overlap = 0;
# Skip over identical initial entries (stop at any '...'):
while a.length > 0 and b.length > 0 and a[0] !~ /@/ and b[0] !~ /@/
cmp = component_compare(a[0], b[0]);
return nil unless cmp; # if different, whole path is different
overlap |= cmp; # else track overlaps
a.shift
b.shift
end
# Skip over identical ending entries (stop at any '...'):
while a.length > 0 and b.length > 0 and a[-1] !~ /@/ and b[-1] !~ /@/
cmp = component_compare(a[-1], b[-1]);
return nil unless cmp; # if different, whole path is different
overlap |= cmp; # else track overlaps
a.pop
b.pop
end
# Done if there are no '...':
return overlap if a.length == 0 and b.length == 0;
# If any one side is empty, paths are different,
# because even with '...' non-empty implies at least one more directory:
return nil if a.length == 0 or b.length == 0;
# Skip over identical starting/ending characters.
return nil unless match_trim_left(a[0], b[0]);
return nil unless match_trim_right(a[-1], b[-1]);
# Okay, now we get to some interesting / odd cases.
# At this point, both arrays are non-empty.
# At least one starts with a component containing '...', and
# at least one ends with a component containing '...'.
sega = a.join("/"); # full paths of what's left
segb = b.join("/");
return path_compare_glob(sega, segb) ? overlap|2 : nil if sega !~ /[*@]/; # globs in b but not a ?
return path_compare_glob(segb, sega) ? overlap|1 : nil if segb !~ /[*@]/; # globs in a but not b ?
return (overlap | 1) if sega=='@'+segb or sega==segb+'@' or sega=='@'+segb+'@';
return (overlap | 2) if segb=='@'+sega or segb==sega+'@' or segb=='@'+sega+'@';
# Compare what's before any '...':
aa = a[0].dup; a[0] = aa.sub!(/\**@.*$/, '*') ? $& : ''; # treat any '...' as '*'
bb = b[0].dup; b[0] = bb.sub!(/\**@.*$/, '*') ? $& : ''; # treat any '...' as '*'
cmp = component_compare(aa, bb);
return nil unless cmp; # if different, whole path is different
overlap |= cmp; # else track overlaps
# Compare what's after any '...':
aa = a[-1].dup; a[-1] = aa.sub!(/^.*@/, '*') ? $& : ''; # treat any '...' as '*'
bb = b[-1].dup; b[-1] = bb.sub!(/^.*@/, '*') ? $& : ''; # treat any '...' as '*'
cmp = component_compare(aa, bb);
return nil unless cmp; # if different, whole path is different
overlap |= cmp; # else track overlaps
#$stderr.print "LEFT: '#{a.join('/')}' and '#{b.join('/')}' (overlap is #{overlap})\n";
sega = a.join("/"); # full paths of what's left
segb = b.join("/");
return overlap if sega == segb; # if what remains is identical
return (overlap | 1) if sega == '@'; # if a is just '...' it's a superset
return (overlap | 2) if segb == '@'; # if b is just '...' it's a superset
return (overlap | 1) if sega=='@'+segb or sega==segb+'@' or sega=='@'+segb+'@';
return (overlap | 2) if segb=='@'+sega or segb==sega+'@' or segb=='@'+sega+'@';
return 3 if a[0] =~ /^@/ and b[0] =~ /^@/ and a[-1] =~ /@$/ and b[-1] =~ /@$/; # if '...' around everything
# Other cases are too complicated, just assume partial overlap.
# This is a conservative, fail-safe return value: it may cause a valid
# protection specification to be rejected, but should not (at least with
# current code as of March 2005) cause an invalid / illegal protection
# specification to be accepted. (This statement NOT formally verified)
# It may cause arcane NO-OP statements to slip through unnoticed.
# FIXME/TODO: determine whether we can do any better.
# (Here it's clear we can do better, it's just a question of how far we
# want to go. I know of patterns that fall through here [eg. see tests],
# but none of them seem highly likely or interesting. -Marc 3/24/2005)
warn("assuming partial overlap between '#{apath}' and '#{bpath}'") if $debug; # for debugging
return 3;
end
def path_compare(apath, bpath)
x = do_path_compare(apath, bpath);
#$stderr.print "'#{apath}' vs '#{bpath}': #{x ? x : 'nil'}\n";
return x;
end
# Check for overlap between two protection entries, a and b,
# both of type ProtEntry (see above).
# Returns:
# nil no overlap
# 0 a equals b
# 1 a contains b
# 2 b contains a
# 3 partial overlap
#
def prot_entry_compare(a, b)
# Compare user hashes:
a_users = a.who[1].keys.sort
b_users = b.who[1].keys.sort
common_users = a_users & b_users
return nil if common_users.length == 0;
woverlap = 0;
woverlap |= 1 if a_users.length > common_users.length;
woverlap |= 2 if b_users.length > common_users.length;
# Compare paths:
overlap = path_compare(a.path, b.path);
return nil unless overlap;
return (overlap | woverlap);
end
# Check for overlaps between protection entry 'prot' (declared in 'curlevel')
# and (previous) protection entries in 'level'.
# Called by parse_file() while parsing protection file.
#
# Returns true if check completed due to a superset entry found, false otherwise.
#
def level_overlap_check(prot, level, can_override, context)
# Traverse protections in reverse order, and stop when we encounter a subsuming entry
# (that completely supersets or equals 'prot').
#
level.prots.reverse.each do |prev|
if prev.class == ProtEntry
cmp = prot_entry_compare(prot, prev);
if cmp
cmpmsg = ["equals",
"is a subset of",
"is a superset of",
"intersects"] [cmp];
cmpdesc = "#{prev.path}(#{prev.who[0].join('+')}) " +
"#{cmpmsg} " +
"#{prot.path}(#{prot.who[0].join('+')})";
#" in scope that starts at line #{level.startline}"
# Keep track of what 'prot' overlaps (eg. overrides) to help reduce output protection list:
prot.perm_overrides[prev.perm] = cmp;
# For debugging:
#warn("#{context}: overlap with line #{prev.startline}:\n #{cmpdesc}");
# Check whether the overlap is okay, if not indicate why:
if ! prev.dependent and prev.perm != prot.perm
error( "#{context}: conflicting overlap with independent line #{prev.startline}:" +
"\n #{cmpdesc}");
elsif ! can_override # and prev.perm != prot.perm # FIXME: do we allow this 'and' clause or not?
error( "#{context}: overlap with line #{prev.startline} whose scope has ended:" +
"\n #{cmpdesc}");
elsif cmp == 0 or cmp == 1
error( "#{context}: overlap makes line #{prev.startline} a NO-OP:" +
"\n #{cmpdesc}");
elsif cmp == 2 and prev.perm == prot.perm
error( "#{context}: is a NO-OP subset of line #{prev.startline} with same permissions:" +
"\n #{cmpdesc}");
elsif cmp == 2
# Entry 'prev' is a superset of 'prot', with different permissions.
# This is normal and okay. At this point however, there is no need
# need to check 'prot' against entries earlier than 'prev' because we've
# already made relevant checks when checking 'prev'.
# Indeed, the search must stop here, otherwise we might hit an entry earlier
# than 'prev' that is a superset of 'prev' but with same permissions as 'prot'
# which is also normal but which the above check would flag as an error.
return true;
else
# case 3 (partial overlap / intersection) with different or same permissions,
# where 'prev' is dependent ('?' specified), are normal and okay.
end
end
else # prev.class == ProtLevel
#$stderr.print "class is #{prev.class}.\n";
# Recurse in closed scope. These cannot be overridden:
return true if level_overlap_check(prot, prev, false, context);
end
end
return false;
end
# Verify whether a protection entry complies with meta protections.
# Returns true if okay, false (with error already displayed) otherwise.
#
def validate_prot(prot, metalist, protfilename, context)
unless metalist.length > 0
error("#{context}: missing meta protection information");
return false;
end
metalist.each {|m|
next unless m.perm == protfilename;
cmp = prot_entry_compare(m, prot);
return true if cmp == 0 or cmp == 1; # okay if prot subset of meta entry
}
error("#{context}: entry not allowed by meta protection file:\n" +
" #{prot.perm} #{prot.who[0].join(',')} #{prot.path}");
return false;
end
# Return list of users/groups that correspond to '*' for given path and protection file.
#
def allowed_users(metalist, protfilename, path)
seen = {}
metalist.each {|m|
next unless m.perm == protfilename;
overlap = path_compare(m.path, path);
next unless overlap and (overlap == 0 or overlap == 1);
# Path is subset, add users/groups:
m.who[0].each {|w| seen[w] = 1;}
}
return seen.keys.sort;
end
# Parse a meta protection entry in the master protection file.
#
def parse_meta_file(file, filename, metalist, lineref)
line = lineref[0];
startline = line;
metalev = ProtLevel.new('', [], startline); # local use only
while file.length > 0
line += 1;
tokens = parse_line(file.shift, line);
next if tokens.length == 0; # skip empty lines
context = "#{filename} line #{line}";
case tokens[0]
when 'end'
lineref[0] = line;
return false; # no errors
when 'meta', 'with', '^', 'DENY', 'RO', 'LOCK', 'SUBMIT'
error("#{context}: unexpected token '#{tokens[0]}' within 'meta' block");
else # <prot_filename> <who> <path>
error("#{context}: expected exactly three parameters for meta protection entry:\n <prot_filename>, <who>, <depot_path>") \
unless tokens.length == 3;
prot = ProtEntry.new(tokens[0], false, tokens[1], tokens[2], line, {});
if prot.perm == '^' or prot.who == '^' or prot.path == '^'
error("#{context}: repeat character '^' not allowed in meta protection line");
end
validatepath(prot.path, context);
prot.path = pathcat('', prot.path, true, context);
#prot.who = $p4users.keys.join(",") if prot.who == '*';
prot.who = checkwho(prot.who, context);
# For debugging:
printf "%3d: meta %7s %16s %s\n", line, prot.perm, prot.who[0].join(","), prot.path if $debug;
metalev.prots = metalist;
level_overlap_check(prot, metalev, true, context);
metalist.push(prot);
end
end
error("#{context}: missing 'end' for 'meta' starting at line #{startline}");
lineref[0] = line;
return true; # unexpected end of file
end
# Parse a protection file.
# Returns parsed protection in the form of a ProtLevel structure,
# or nil on error.
#
def parse_file(file, protfilename, metalist, is_meta)
curlevel = ProtLevel.new('', [], 1);
levels = []; # entry for each level (depth) currently being parsed below curlevel
lastprot = [];
prefix = '';
line = 0; # line number
while file.length > 0
line += 1;
tokens = parse_line(file.shift, line);
next if tokens.length == 0; # skip empty lines
context = "#{protfilename} line #{line}";
# printf "%3d:", line; print " ", tokens.join(', '), ".\n";
case tokens[0]
when 'begin'
error("#{context}: unexpected text '#{tokens[1]}' after 'begin'") \
unless tokens.length == 1 or (tokens.length == 3 and tokens[1] == 'with');
levels.push(curlevel);
curlevel = ProtLevel.new(prefix, [], line, nil);
if tokens.length == 3
prefix = pathcat(prefix, tokens[2], true, context);
end
when 'meta'
unless is_meta
error("#{context}: meta keyword only allowed in meta (master) protection file");
return nil;
end
unless levels.length == 0
error("#{context}: meta keyword only allowed at top-level");
return nil;
end
last if parse_meta_file(file, protfilename, metalist, ref = [line]);
line = ref[0];
when 'end'
if levels.length == 0
error("#{context}: unmatched 'end'");
end
oldlevel = curlevel
curlevel = levels.pop;
oldlevel.endline = line
curlevel.prots.push(oldlevel);
prefix = oldlevel.prefix; # restore prefix
when /^(\^|DENY|RO|LOCK|SUBMIT)(\??)$/
perm = $1;
dependent = ($2 == '?');
error("#{context}: expected exactly two parameters after #{perm} keyword") \
unless tokens.length == 3;
prot = ProtEntry.new(perm, dependent, tokens[1], tokens[2], line, {});
prot.path = '' if prot.path == '.'; # this is what pathcat expects
# Deal with '^' character:
if lastprot.length == 0 and
(prot.perm == '^' or prot.who == '^' or prot.path == '^')
error("#{context}: repeat character '^' not allowed in first protection line");
end
prot.perm = lastprot.perm if prot.perm == '^';
prot.who = lastprot.who if prot.who == '^';
prot.path = lastprot.path if prot.path == '^';
lastprot = prot.dup;
# Other expansions:
validatepath(prot.path, context);
prot.path = pathcat(prefix, prot.path, true, context);
if prot.who == '*'
prot.who = allowed_users(metalist, protfilename, prot.path).join(",");
end
prot.who = checkwho(prot.who, context);
# Check against meta protections:
validate_prot(prot, metalist, protfilename, context) or return nil;
printf "%3d: %7s %16s %s\n", line, prot.perm, prot.who[0].join(","), prot.path if $debug;
# Check overlaps:
unless level_overlap_check(prot, curlevel, true, context)
levels.reverse.each {|lev| break if level_overlap_check(prot, lev, true, context); }
end
curlevel.prots.push(prot);
when 'group'
error("#{context}: expected exactly two parameters (group name, and comma-separate member list) after 'group' keyword") \
unless tokens.length == 3;
groupname,members = tokens[1],tokens[2];
error("#{context}: file-local group name '#{groupname}' invalid or does not start with 'g_' (lowercase)") unless groupname =~ /^g_\w+$/;
error("#{context}: file-local group name '#{groupname}' already exists") if $p4groups[groupname] or $p4users[groupname];
add_group(groupname, members.split(","), true, context);
else
error("#{context}: unrecognized token '#{tokens[0]}'");
end
end
$p4groups.delete_if {|g,m| g =~ /^g_/ }; # remove file-local groups
if levels.length > 0
error("#{context}: missing 'end' for 'begin' at line #{curlevel.startline}");
end
curlevel.endline = line;
if $errors_found > 0
error("#{protfilename}: #{$errors_found} error(s) found");
return nil;
end
return curlevel;
end
# Construct a single Perforce protection entry (line).
# 'who' is a single user or group (not a list).
#
def construct_protection_line(perm,who,path)
user_group = ($p4groups[who] && who != '*') ? "group" : "user";
line = " #{perm} #{user_group} #{who} * ";
# Only quote if needed:
if path =~ / /
line += "\"#{path}\""
else
line += path
end
return line + "\n"
end
# Construct new Perforce protection list from sub-parsetree
# (as returned by parse_file()).
#
def construct_protections_level(parsetree)
output = "";
parsetree.prots.each do |p|
if p.class == ProtEntry
p.who[0].each do |w|
case p.perm
when 'DENY' then p4perm = 'list'; prefix = '-'; predeny = false; review = false;
when 'RO' then p4perm = 'read'; prefix = ''; predeny = true; review = true;
when 'LOCK' then p4perm = 'open'; prefix = ''; predeny = true; review = true;
when 'SUBMIT' then p4perm = 'write'; prefix = ''; predeny = false; review = true;
else error("oopsie, unrecognized permission #{p.perm}.");
end
if predeny and p.perm_overrides.keys.find {|pp| PermValues[pp] > PermValues[p.perm] }
output += construct_protection_line("list", w, "-"+p.path);
else
predeny = false;
end
if review and (p.perm_overrides.keys.find {|pp| pp == 'DENY' } or
! p.perm_overrides.values.find {|pp| pp == 2 } or predeny)
output += construct_protection_line("review", w, prefix + p.path);
end
output += construct_protection_line(p4perm, w, prefix + p.path);
end
else
output += construct_protections_level(p);
end
end
return output;
end
# Construct new Perforce protection list from a single hierarchical parsetree.
#
def construct_protections(protfile, parsetree, metalist)
output = "";
# First, deny all that this file covers, per the meta file:
metalist.each do |m|
next unless m.perm == protfile;
m.who[0].each do |w|
output += construct_protection_line("list", w, "-" + m.path);
end
end
# Then go through each protection entry:
output += construct_protections_level(parsetree);
return output;
end
########################### TRIGGER CLASS
# The trigger class, built using Perforce's P4Trigger trigger-wrapper class.
# The main method in here is validate() which
# is invoked from the super-class' parse_change() method.
#
class MetaProtectTrigger < P4Trigger
# Constructor.
def initialize()
# @foo = ...
super()
end
# Get protection file contents
def populate(protfile,file)
if file
# It's a new/edited file.
if file.revisions[0].action == 'delete'
message("\n Meta protections refer to a file being deleted.\n");
return nil;
end
metachg = "@=#{change.change}";
else
# It's an existing file.
metachg = "#head";
end
# FIXME: Using P4 will provide better error handling here:
#message("*** retrieving #{protfile}#{metachg}.\n");
# FIXME - fix $p4 to handle -q.
#contents = `#{$p4prog} print -q #{$meta_dirname}/#{protfile}#{metachg}`;
contents = $p4.run_print('-q',
"#{$meta_dirname}/#{protfile}#{metachg}");
contents = contents.join("");
unless(contents)
message("\n Error #{$?} retrieving #{protfile}#{metachg}.\n");
return nil;
end
if contents == ''
message("\n Empty file or error retrieving #{protfile}#{metachg}.\n");
return nil;
end
return contents;
end
# Validate submission. This is the main trigger method.
#
def my_validate()
# Get a list of protection files in this submission:
@newfiles = {}
change.each_file do |file|
unless file.depot_file =~ %r{^#{$meta_dirname}/(\w+)$}
message( "\n Please restrict your submission to files in the"+
"\n #{$meta_dirname} directory. You included:"+
"\n #{file.depot_file}"+
"\n which is not allowed in a protection update."+
"\n" )
return false; # reject submission
end
protname = $1
@newfiles[protname] = file;
end
# Process meta protection file:
metafile = populate(META_FILENAME,@newfiles[META_FILENAME]); # get prot_meta file contents
return false unless metafile; # else reject submission
#$displayproc = proc {|msg| print "FOO: #{msg}";}
$displayproc = proc {|msg| message(msg)}
metalist = [];
parsetrees = {};
parselist = [];
unless parse = parse_file(metafile.chomp.split(/\n/), META_FILENAME, metalist, true)
return false; # reject submission
end
parsetrees[META_FILENAME] = parse;
parselist.push(META_FILENAME);
# Process all files referenced by the meta protection file:
metalist.each do |m|
protfile = m.perm;
next if parsetrees[protfile]; # skip if already parsed
contents = populate(protfile,@newfiles[protfile]); # get contents
#message("*** got #{protfile}\n");
return false unless contents; # else reject submission
unless parse = parse_file(contents.chomp.split(/\n/), protfile, metalist, false)
return false; # reject submission
end
parsetrees[protfile] = parse;
parselist.push(protfile);
end
# Now construct new Perforce protection list from parsetrees[]
protections = <<__ENDPROT__;
Protections:
super user p4admin * //comment/AUTOGENERATED_#{Time.now.asctime.gsub(/ /, '_')}
__ENDPROT__
# FIXME: used to have "list user * * -//..." in header but bug in p4 makes this enable arbitrary user creation!
parselist.each do |protfile|
protections += construct_protections(protfile, parsetrees[protfile], metalist);
end
protections += <<__ENDPROT__;
review user p4admin * //...
super user p4admin * //...
__ENDPROT__
if change.desc =~ /REJECTTEST/
message("Submission rejected by submitter request (REJECTTEST keyword found).\n");
return false; # submission rejected
end
if $warnings_found > 0 and change.desc !~ /IGNOREWARN/
message( "\n"+
"Submission rejected because of warnings (so that you can see them).\n"+
"Please insert the keyword IGNOREWARN anywhere in your change description\n"+
"to ignore these warnings and let your submission go through. E.g. do:\n"+
"\n"+
" p4 change #{change.change}\n"+
" (add 'IGNOREWARN' to the Description field)\n"+
" p4 submit -c #{change.change}\n" );
return false; # submission rejected
end
##################################################
# COMMIT POINT
##################################################
message("*** constructed protections\n");
$p4.input(protections)
$p4.run_protect('-i')
unless $run_tests
File.open("/home/p4admin/bin/triggers/last_p4protect_output", "w") do |f|
f.print protections;
end
end
message( "Submission rejected because... well, just because.\n" )
#return false; # submission rejected
return true; # submission accepted
end
# Invoke my_validate() above, and try to unlock files on failure:
def validate()
valid = my_validate();
unless valid
# TEST: when rejecting, unlock all files in the change
# to avoid content-trigger lock bug:
change.each_file do |file|
$p4.run_unlock('-f', file.depot_file);
# if file.depot_file =~ %r{^#{$meta_dirname}/(\w+)$};
end
end
return valid;
end
end
########################### ARGUMENT PARSING
def usage
print <<__USAGE__;
#{PROGNAME} version #{PROGVERS} - process custom protection description files
usage: #{PROGNAME} [options]
where options are:
-h display this message and exit
-t tname run test <tname>, "#{UTNAME}" means run unit tests
-d turn on debugging messages
__USAGE__
exit 0;
end
def process_cmd_args
require 'getoptlong';
opts = GetoptLong.new(
["--help", "-h", GetoptLong::NO_ARGUMENT ],
["--test", "-t", GetoptLong::REQUIRED_ARGUMENT ],
["--debug", "-d", GetoptLong::NO_ARGUMENT ]
);
opts.each do |opt,arg|
case opt
when '--help'
usage();
when '--debug'
$debug = true;
when '--test'
$run_tests = true;
$test_name = arg
end
end
ARGV.each do |arg|
# ... process filename argument ...
end
end
########################### MAIN PROGRAM ################################
def manual_main
verify_p4_login or exit 1;
setup_users_groups;
metafile = [
'meta',
' prot_dev G_allusers //depot/main/...',
' prot_dev G_allusers //depot/dev/...',
'end'
];
metalist = [];
parsetree1 = parse_file(metafile, META_FILENAME, metalist, true);
parsetree2 = parse_file($stdin.readlines, 'prot_dev', metalist, false);
#process_paths(parsetree1, parsetree2);
end
def trigger_main
$stderr = $stdout; # this really saves a lot of pain getting errors back to the submitter :-)
print "\n\n"; # for more nicely readable submission reject messages
$displayproc = proc {|msg| print "#{msg}";}
verify_p4_login or exit 1;
setup_users_groups;
trig = MetaProtectTrigger.new;
return( trig.parse_change( ARGV.shift ) )
end
########################### CALL MAIN ################################
# When called via trigger:
if ARGV.length == 1 and ARGV[0] =~ /^\d+$/
$meta_dirname = '//depot/meta/prot';
$p4 = P4.new
$p4.connect
exit(trigger_main())
end
process_cmd_args;
# When called from command line:
unless ($run_tests)
$meta_dirname = '//depot/meta/prot';
$p4 = P4.new
manual_main; # MAIN PROGRAM
exit 0; # done
end
########################### UNIT TESTS ################################
# Override methods in P4 wrapper to get values from the current test.
# Ruby rocks!
require 'P4'
require 'test/unit'
# FIXME - make sure these generate assertions
$saved_assertions = []
class P4
@saved_input = nil
def xt_unit_test_init(testdir)
@testdir = testdir
#p "unit_test_init called with #{@testdir}"
require @testdir + '/setup.rb'
end
def xt_strip_comments_and_whitespace(array_in, gold=false)
array_out = []
array_in.each { |line|
line.gsub!(/\s+/, ' ')
line.sub!(/^ *\#.*$/, '')if gold
line.gsub!(/\s+$/, '')
next if line =~ /^\s*$/
next if line =~ %r{//comment/}
array_out.push(line)
}
return array_out
end
def xt_write_file(fname, contents)
print("Writing out #{fname}\n")
File.open(fname, "w") { |fh|
fh.print contents
}
end
def xt_process_p4_protect
if (Setup::Ut_expected_test_return_status == 0)
goldfile = @testdir + '/gold_p4_protect'
goldfile_contents = nil
if File.readable?(goldfile)
fh = File.new(goldfile)
goldfile_contents =
xt_strip_comments_and_whitespace(fh.readlines, true).join("\n")
else
error("Could not read file #{goldfile}\n")
end
xt_write_file(@testdir + '/actual_p4_protect', @saved_input)
@stripped_saved_input =
xt_strip_comments_and_whitespace(@saved_input.split("\n")).
join("\n")
if (goldfile_contents == @stripped_saved_input)
print "Goldfile matches generated 'p4 protect -i'\n"
else
error("Goldfile does not match 'p4 protect -i'")
xt_write_file(@testdir + '/actual_p4_protect.strip',
@stripped_saved_input)
xt_write_file(@testdir + '/gold_p4_protect.strip',
goldfile_contents)
end
else
message = "protect -i unexpectedly executed in testcase"
error(message)
# FIXME - make sure this causes assertion in caller
$saved_assertions.push(message)
end
end
alias xt_original_input input
def input(*args)
if args.length == 1
@saved_input = args[0]
else
error("only support 'input(the_input)")
end
end
alias xt_original_run run
# This fake version of P4:run supports 'run' and 'run_XXX' syntax for P4
# commands, but not fetch_group.
def run(*args)
orig_args = args.clone
cmd = args.shift
# run_XXX form puts all args into an array...
args.flatten!
# FIXME - fail commands unless connect/login called first.
# FIXME test that disconnect called before script exits?
case (cmd)
when 'connect'
print "Connecting to nowhere...\n"
when 'login'
if (args.length == 1 and args[0] == '-s')
return ["User foobar ticket expires in 1 hour 3 minutes\n"]
else
error("only supporting 'login -s'")
end
when 'users'
error("users takes no args") unless args.length == 0
return Setup::Ut_p4_users
when 'groups'
error("groups takes no args") unless args.length == 0
return Setup::Ut_p4_group_hash.keys
when 'group'
if (args.length == 2 and args[0] == '-o')
group_form = Setup::Ut_p4_group_hash[args[1]]
error("non-existent group #{args[1]}") unless group_form
return [group_form]
else
error("only supporting 'group -o <G_name>'")
end
when 'print'
if (args.length == 2 and args[0] == '-q')
full_fname = args[1].sub(/#.*/, '')
#p full_fname
if File.readable?(full_fname)
fh = File.new(full_fname)
return [fh.read, ""]
else
error("Could not read file #{full_fname}\n")
return nil
end
else
error("only support 'print -q <full_fname>'")
return nil
end
when 'protect'
if (args.length == 1 and args[0] == '-i')
xt_process_p4_protect
else
error("only supporting 'protect -i <G_name>'")
end
when 'describe'
# FIXME - ignoring changenum OK?
if (args.length == 2 and args[0] == '-s')
return [Setup::Ut_p4_describe]
else
error("only supporting 'describe -s <changenum>'")
end
else
error("Fake P4 cmd #{orig_args.join(':')}, unimplemented\n")
end
end
end
# Run a single protection test using a set of protection files, a metafile, and
# a setup.rb file.
class Check_protection_files < Test::Unit::TestCase # :nodoc:
#P4_PROTECTION_TESTS_DIR = "p4protect_tests"
def test_protection_file_group
return if $test_name == UTNAME
$displayproc = proc {|msg| } # Disable error msg printing
# FIXME - support only one test, and get it from --test <testname>
# this is how the script is actually used
#prot_file_tests = Dir.glob(P4_PROTECTION_TESTS_DIR + "/*")
#if prot_file_tests.length == 0
# error("Could not find prot_file tests")
#end
print "p4protect test: #{$test_name}\n"
unless (test(?d, $test_name))
fail("Could not find test directory #{$test_name}")
end
$p4 = P4.new
$meta_dirname = $test_name
$p4.xt_unit_test_init($test_name)
# FIXME - pushing on bogus change number; instead, put in test
ARGV.push(1)
status = (trigger_main != 0 || $errors_found > 0) ? 1 : 0;
assert_equal(Setup::Ut_expected_test_return_status, status)
end
end
class Check_validatepath < Test::Unit::TestCase # :nodoc:
# Helper function for test_star_star
def star_star_helper(path, expected=1)
validatepath(path, path)
assert_equal(expected, $errors_found)
$errors_found = 0;
end
# Verify validatepath function
def test_star_star
return if $test_name != UTNAME
print "p4protect test: #{$test_name}\n"
$errors_found = 0;
save_displayproc = $displayproc;
$displayproc = proc {|msg| } # Disable error msg printing
star_star_helper('//this-is okay_/.*...txt', 0)
star_star_helper('relative too', 0)
star_star_helper('like/this', 0)
star_star_helper('//...a/***')
star_star_helper('//a**')
star_star_helper('//aa***bb')
star_star_helper('//a......b')
star_star_helper('//a...*...b')
star_star_helper('//a...*b')
star_star_helper('//a@b')
star_star_helper('//a**b')
star_star_helper('//a#b')
star_star_helper('//a+b')
star_star_helper('//a**b/cde/e**f')
star_star_helper('//a*b/cde/e********f')
star_star_helper('//donot//doubleslashes')
star_star_helper('//orend/withone/')
star_star_helper('/what_s_that_root')
star_star_helper("//what's this")
star_star_helper('//...**/c', 2)
$displayproc = save_displayproc; # restore printing
end
end
# Class for checking def path_compare(apath, bpath)
# FIXME - test punctuation more fully?
class Check_path_compare < Test::Unit::TestCase # :nodoc:
def test_no_overlaps # Paths have no overlap ==> nil
return if $test_name != UTNAME
assert_equal(nil,
path_compare( '//a',
'//b'))
assert_equal(nil,
path_compare( '//a',
'//A'))
assert_equal(nil,
path_compare( '//a*',
'//b*'))
assert_equal(nil,
path_compare( '//a...',
'//b...'))
assert_equal(nil,
path_compare( '//aa',
'//ab'))
assert_equal(nil,
path_compare( '//a*a',
'//a*b'))
assert_equal(nil,
path_compare( '//a...a',
'//a...b'))
assert_equal(nil,
path_compare( '//depot/...foo/o*k',
'//depot/...bar/o*k'))
assert_equal(nil,
path_compare( '//depot/.../foo',
'//depot/.../afoo'))
assert_equal(nil,
path_compare( '//depot/foo',
'//depot/*/foo'))
assert_equal(nil,
path_compare( '//depot/...*/foo',
'//depot/foo'))
assert_equal(nil,
path_compare( '//depot/*.../foo',
'//depot/foo'))
assert_equal(nil,
path_compare( '//depot/...*.../foo',
'//depot/foo'))
assert_equal(nil,
path_compare( '//depot/.../a*foo',
'//depot/.../b*foo'))
assert_equal(nil,
path_compare( '//depot/.../...done',
'//depot/welldone'))
assert_equal(nil,
path_compare( '//depot/welldone',
'//depot/.../...done'))
assert_equal(nil,
path_compare( '//depot/...xy...',
'//depot/axtz'))
assert_equal(nil,
path_compare( '//depot/axtz',
'//depot/...xy...'))
assert_equal(nil,
path_compare( '//depot/*xy*',
'//depot/axtz'))
assert_equal(nil,
path_compare( '//depot/axtz',
'//depot/*xy*'))
x = path_compare( '//depot/.../*xy*...',
'//depot/*t*/zyxel/foobar')
assert((x == nil or x == 3)) # must be nil, currently interpreted as 3 (tricky case)
x = path_compare( '//depot/...xz.../...',
'//depot/abxyz/*foo')
assert(x == nil || x == 3) # must be nil, currently interpreted as 3 (tricky case)
end
def test_paths_identical # Paths are identical ==> 0
return if $test_name != UTNAME
assert_equal(0,
path_compare( '//a',
'//a'))
assert_equal(0,
path_compare( '//xx',
'//xx'))
assert_equal(0,
path_compare( '//a/bb/ccc/dddd/eeeee/aoeuhts',
'//a/bb/ccc/dddd/eeeee/aoeuhts'))
assert_equal(0,
path_compare( '//1246eygcEOquReaRSwk-_<>',
'//1246eygcEOquReaRSwk-_<>'))
assert_equal(0,
path_compare( '//...foo*/.../a*b/....pdf',
'//...foo*/.../a*b/....pdf'))
end
def test_path_a_contains_b # Path a contains b ==> 1
return if $test_name != UTNAME
assert_equal(1,
path_compare( '//depot/...foo',
'//depot/*foo'))
assert_equal(1,
path_compare( '//depot/...foo',
'//depot/foo'))
assert_equal(1,
path_compare( '//depot/...foo',
'//depot/...afoo'))
assert_equal(1,
path_compare( '//depot/...foo',
'//depot/...bar...foo'))
assert_equal(1,
path_compare( '//depot/aa*bb/foo',
'//depot/aabb/foo'))
assert_equal(1,
path_compare( '//*a/.../b*c',
'//a/.../b*c'))
assert_equal(1,
path_compare( '//*a/.../b...c',
'//a/.../b...c'))
assert_equal(1,
path_compare( '//a/.../b*c*',
'//a/.../b*c'))
assert_equal(1,
path_compare( '//...a/.../b*c',
'//a/.../b*c'))
assert_equal(1,
path_compare( '//...a/.../b...c',
'//a/.../b...c'))
assert_equal(1,
path_compare( '//a/.../b*c...',
'//a/.../b*c'))
assert_equal(1,
path_compare( '//a/.../b...c...',
'//a/.../b...c'))
assert_equal(1,
path_compare( '//depot/...xy...',
'//depot/axyz'))
assert_equal(1,
path_compare( '//depot/*xy*',
'//depot/axyz'))
x = path_compare( '//depot/.../*yx*...',
'//depot/*t*/zyxel/foobar')
assert(x == 1 || x == 3) # must be 1, currently interpreted as 3 (tricky case)
x = path_compare( '//depot/...xy.../...',
'//depot/abxyz/*foo')
assert(x == 1 || x == 3) # must be 1, currently interpreted as 3 (tricky case)
end
def test_path_b_contains_a # Path b contains a ==> 2
return if $test_name != UTNAME
assert_equal(2,
path_compare( '//depot/foo',
'//depot/...foo'))
assert_equal(2,
path_compare( '//depot/...afoo',
'//depot/...foo'))
assert_equal(2,
path_compare( '//depot/...bar...foo',
'//depot/...foo'))
assert_equal(2,
path_compare( '//depot/aabb/foo',
'//depot/aa*bb/foo'))
assert_equal(2,
path_compare( '//a/.../b*c',
'//*a/.../b*c'))
assert_equal(2,
path_compare( '//a/.../b...c',
'//*a/.../b...c'))
assert_equal(2,
path_compare( '//a/.../b*c',
'//a/.../b*c*'))
assert_equal(2,
path_compare( '//a/.../b...c',
'//a/.../b...c*'))
assert_equal(2,
path_compare( '//a/.../b*c',
'//...a/.../b*c'))
assert_equal(2,
path_compare( '//a/.../b...c',
'//...a/.../b...c'))
assert_equal(2,
path_compare( '//a/.../b*c',
'//a/.../b*c...'))
assert_equal(2,
path_compare( '//a/.../b...c',
'//a/.../b...c...'))
assert_equal(2,
path_compare( '//depot/axyz',
'//depot/...xy...'))
assert_equal(2,
path_compare( '//depot/axyz',
'//depot/*xy*'))
x = path_compare( '//depot/*t*/zyxel/foobar',
'//depot/.../*yx*...')
assert(x == 2 || x == 3) # must be 2, currently interpreted as 3 (tricky case)
x = path_compare( '//depot/abxyz/*foo',
'//depot/...xy.../...')
assert(x == 2 || x == 3) # must be 2, currently interpreted as 3 (tricky case)
end
def test_paths_partially_overlap # Path a, b partially overlap ==> 3
return if $test_name != UTNAME
assert_equal(3,
path_compare( '//a/*',
'//*/c'))
assert_equal(3,
path_compare( '//a/b/*',
'//a/*/c'))
assert_equal(3,
path_compare( '//*/b/*',
'//*/*/c'))
assert_equal(3,
path_compare( '//a/...*',
'//*/...b'))
assert_equal(3,
path_compare( '//a/*...',
'//*/c...'))
assert_equal(3,
path_compare( '//a.../*',
'//*.../c'))
assert_equal(3,
path_compare( '//...a/*',
'//...*/c'))
assert_equal(3,
path_compare( '//depot/...abc...',
'//depot/...def...'))
assert_equal(3,
path_compare( '//depot/...abc*',
'//depot/...def...'))
assert_equal(3,
path_compare( '//depot/...def...',
'//depot/...abc*'))
assert_equal(3,
path_compare( '//depot/...abc*',
'//depot/*def...'))
assert_equal(3,
path_compare( '//depot/*def...',
'//depot/...abc*'))
assert_equal(3,
path_compare( '//depot/abc*',
'//depot/*def...'))
assert_equal(3,
path_compare( '//depot/*def...',
'//depot/abc*'))
assert_equal(3,
path_compare( '//depot/...xy.../zafoo',
'//depot/abxyz/*foo'))
assert_equal(3,
path_compare( '//depot/abxyz/*foo',
'//depot/...xy.../zafoo'))
assert_equal(3,
path_compare( '//depot/.../*yx*...foo*',
'//depot/*t*/zyxel/foobar...')) # currently with warning
end
end