From 2edaeb4f22c8a3877dbb5278ab5c92add9d7c4f4 Mon Sep 17 00:00:00 2001 From: stevenknight Date: Mon, 19 Sep 2005 11:13:56 +0000 Subject: [PATCH] Give the subst logic its own SCons.Subst module. It's big enough. git-svn-id: http://scons.tigris.org/svn/scons/trunk@1346 fdb21ef1-2011-0410-befe-b5e4ea1792b1 --- src/RELEASE.txt | 25 + src/engine/MANIFEST.in | 1 + src/engine/SCons/Action.py | 6 +- src/engine/SCons/ActionTests.py | 4 +- src/engine/SCons/Defaults.py | 5 +- src/engine/SCons/Environment.py | 17 +- src/engine/SCons/Node/FS.py | 15 +- src/engine/SCons/Options/OptionsTests.py | 4 +- src/engine/SCons/Platform/__init__.py | 2 +- src/engine/SCons/Subst.py | 824 ++++++++++++++++ src/engine/SCons/SubstTests.py | 1135 ++++++++++++++++++++++ src/engine/SCons/Util.py | 803 +-------------- src/engine/SCons/UtilTests.py | 1069 +------------------- 13 files changed, 2026 insertions(+), 1884 deletions(-) create mode 100644 src/engine/SCons/Subst.py create mode 100644 src/engine/SCons/SubstTests.py diff --git a/src/RELEASE.txt b/src/RELEASE.txt index e8bc3fdc..a6b10c3c 100644 --- a/src/RELEASE.txt +++ b/src/RELEASE.txt @@ -256,6 +256,31 @@ RELEASE 0.97 - XXX The deprecated "validater" keyword to the Options.Add() method has been removed. + -- INTERNAL FUNCTIONS AND CLASSES HAVE MOVED FROM SCons.Util + + All internal functions and classes related to string substitution + have been moved out of the SCons.Util module into their own + SCons.Subst module. The following classes have been moved: + + Literal + SpecialAttrWrapper + NLWrapper + Targets_or_Sources + Target_or_Source + + And the following functions have moved: + + quote_spaces() + escape_list() + subst_dict() + scons_subst() + scons_subst_list() + scons_subst_once() + + If your SConscript files have been using any of these function + directly from the SCons.Util module (which they ultimately should + not be!), you will need to modify them. + Please note the following important changes since release 0.95: -- BUILDERS NOW ALWAYS RETURN A LIST OF TARGETS diff --git a/src/engine/MANIFEST.in b/src/engine/MANIFEST.in index 48887369..17839eaf 100644 --- a/src/engine/MANIFEST.in +++ b/src/engine/MANIFEST.in @@ -50,6 +50,7 @@ SCons/Script/__init__.py SCons/Sig/__init__.py SCons/Sig/MD5.py SCons/Sig/TimeStamp.py +SCons/Subst.py SCons/Taskmaster.py SCons/Tool/__init__.py SCons/Tool/386asm.py diff --git a/src/engine/SCons/Action.py b/src/engine/SCons/Action.py index 59ab2614..0bbcc3a5 100644 --- a/src/engine/SCons/Action.py +++ b/src/engine/SCons/Action.py @@ -422,7 +422,8 @@ class CommandAction(_ActionAction): handle lists of commands, even though that's not how we use it externally. """ - from SCons.Util import is_String, is_List, flatten, escape_list + from SCons.Subst import escape_list + from SCons.Util import is_String, is_List, flatten try: shell = env['SHELL'] @@ -478,12 +479,13 @@ class CommandAction(_ActionAction): This strips $(-$) and everything in between the string, since those parts don't affect signatures. """ + from SCons.Subst import SUBST_SIG cmd = self.cmd_list if SCons.Util.is_List(cmd): cmd = string.join(map(str, cmd)) else: cmd = str(cmd) - return env.subst_target_source(cmd, SCons.Util.SUBST_SIG, target, source) + return env.subst_target_source(cmd, SUBST_SIG, target, source) class CommandGeneratorAction(ActionBase): """Class for command-generator actions.""" diff --git a/src/engine/SCons/ActionTests.py b/src/engine/SCons/ActionTests.py index f5aa67a2..ff23cd77 100644 --- a/src/engine/SCons/ActionTests.py +++ b/src/engine/SCons/ActionTests.py @@ -136,10 +136,10 @@ class Environment: self.d[k] = v # Just use the underlying scons_subst*() utility methods. def subst(self, strSubst, raw=0, target=[], source=[]): - return SCons.Util.scons_subst(strSubst, self, raw, target, source, self.d) + return SCons.Subst.scons_subst(strSubst, self, raw, target, source, self.d) subst_target_source = subst def subst_list(self, strSubst, raw=0, target=[], source=[]): - return SCons.Util.scons_subst_list(strSubst, self, raw, target, source, self.d) + return SCons.Subst.scons_subst_list(strSubst, self, raw, target, source, self.d) def __getitem__(self, item): return self.d[item] def __setitem__(self, item, value): diff --git a/src/engine/SCons/Defaults.py b/src/engine/SCons/Defaults.py index 88a167a6..c1b9d3f4 100644 --- a/src/engine/SCons/Defaults.py +++ b/src/engine/SCons/Defaults.py @@ -50,6 +50,7 @@ import SCons.Builder import SCons.Environment import SCons.Tool import SCons.Sig +import SCons.Subst # A placeholder for a default Environment (for fetching source files # from source code management systems and the like). This must be @@ -211,8 +212,8 @@ def _concat(prefix, list, suffix, env, f=lambda x: x, target=None, source=None): result = [] # ensure that prefix and suffix are strings - prefix = str(env.subst(prefix, SCons.Util.SUBST_RAW)) - suffix = str(env.subst(suffix, SCons.Util.SUBST_RAW)) + prefix = str(env.subst(prefix, SCons.Subst.SUBST_RAW)) + suffix = str(env.subst(suffix, SCons.Subst.SUBST_RAW)) for x in list: if isinstance(x, SCons.Node.FS.File): diff --git a/src/engine/SCons/Environment.py b/src/engine/SCons/Environment.py index 57066c17..0833f5f1 100644 --- a/src/engine/SCons/Environment.py +++ b/src/engine/SCons/Environment.py @@ -55,6 +55,7 @@ import SCons.SConsign import SCons.Sig import SCons.Sig.MD5 import SCons.Sig.TimeStamp +import SCons.Subst import SCons.Tool import SCons.Util import SCons.Warnings @@ -374,7 +375,7 @@ class SubstitutionEnvironment: gvars = self.gvars() lvars = self.lvars() lvars['__env__'] = self - return SCons.Util.scons_subst(string, self, raw, target, source, gvars, lvars, conv) + return SCons.Subst.scons_subst(string, self, raw, target, source, gvars, lvars, conv) def subst_kw(self, kw, raw=0, target=None, source=None): nkw = {} @@ -386,12 +387,12 @@ class SubstitutionEnvironment: return nkw def subst_list(self, string, raw=0, target=None, source=None, conv=None): - """Calls through to SCons.Util.scons_subst_list(). See + """Calls through to SCons.Subst.scons_subst_list(). See the documentation for that function.""" gvars = self.gvars() lvars = self.lvars() lvars['__env__'] = self - return SCons.Util.scons_subst_list(string, self, raw, target, source, gvars, lvars, conv) + return SCons.Subst.scons_subst_list(string, self, raw, target, source, gvars, lvars, conv) def subst_path(self, path, target=None, source=None): """Substitute a path list, turning EntryProxies into Nodes @@ -450,7 +451,7 @@ class SubstitutionEnvironment: o = copy_non_reserved_keywords(overrides) overrides = {} for key, value in o.items(): - overrides[key] = SCons.Util.scons_subst_once(value, self, key) + overrides[key] = SCons.Subst.scons_subst_once(value, self, key) if overrides: env = OverrideEnvironment(self, overrides) return env @@ -774,7 +775,7 @@ class Base(SubstitutionEnvironment): kw = copy_non_reserved_keywords(kw) new = {} for key, value in kw.items(): - new[key] = SCons.Util.scons_subst_once(value, self, key) + new[key] = SCons.Subst.scons_subst_once(value, self, key) apply(clone.Replace, (), new) if __debug__: logInstanceCreation(self, 'Environment.EnvironmentCopy') return clone @@ -1351,7 +1352,7 @@ class Base(SubstitutionEnvironment): return result def Literal(self, string): - return SCons.Util.Literal(string) + return SCons.Subst.Literal(string) def Local(self, *targets): ret = [] @@ -1596,13 +1597,13 @@ def NoSubstitutionProxy(subject): nkw = kwargs.copy() nkw['gvars'] = {} self.raw_to_mode(nkw) - return apply(SCons.Util.scons_subst_list, nargs, nkw) + return apply(SCons.Subst.scons_subst_list, nargs, nkw) def subst_target_source(self, string, *args, **kwargs): nargs = (string, self,) + args nkw = kwargs.copy() nkw['gvars'] = {} self.raw_to_mode(nkw) - return apply(SCons.Util.scons_subst, nargs, nkw) + return apply(SCons.Subst.scons_subst, nargs, nkw) return _NoSubstitutionProxy(subject) if SCons.Memoize.use_old_memoization(): diff --git a/src/engine/SCons/Node/FS.py b/src/engine/SCons/Node/FS.py index 6eb61d99..9ecac89a 100644 --- a/src/engine/SCons/Node/FS.py +++ b/src/engine/SCons/Node/FS.py @@ -49,6 +49,7 @@ from SCons.Debug import logInstanceCreation import SCons.Errors import SCons.Node import SCons.Sig.MD5 +import SCons.Subst import SCons.Util import SCons.Warnings @@ -362,28 +363,28 @@ def diskcheck_types(): class EntryProxy(SCons.Util.Proxy): def __get_abspath(self): entry = self.get() - return SCons.Util.SpecialAttrWrapper(entry.get_abspath(), + return SCons.Subst.SpecialAttrWrapper(entry.get_abspath(), entry.name + "_abspath") def __get_filebase(self): name = self.get().name - return SCons.Util.SpecialAttrWrapper(SCons.Util.splitext(name)[0], + return SCons.Subst.SpecialAttrWrapper(SCons.Util.splitext(name)[0], name + "_filebase") def __get_suffix(self): name = self.get().name - return SCons.Util.SpecialAttrWrapper(SCons.Util.splitext(name)[1], + return SCons.Subst.SpecialAttrWrapper(SCons.Util.splitext(name)[1], name + "_suffix") def __get_file(self): name = self.get().name - return SCons.Util.SpecialAttrWrapper(name, name + "_file") + return SCons.Subst.SpecialAttrWrapper(name, name + "_file") def __get_base_path(self): """Return the file's directory and file name, with the suffix stripped.""" entry = self.get() - return SCons.Util.SpecialAttrWrapper(SCons.Util.splitext(entry.get_path())[0], + return SCons.Subst.SpecialAttrWrapper(SCons.Util.splitext(entry.get_path())[0], entry.name + "_base") def __get_posix_path(self): @@ -394,7 +395,7 @@ class EntryProxy(SCons.Util.Proxy): else: entry = self.get() r = string.replace(entry.get_path(), os.sep, '/') - return SCons.Util.SpecialAttrWrapper(r, entry.name + "_posix") + return SCons.Subst.SpecialAttrWrapper(r, entry.name + "_posix") def __get_win32_path(self): """Return the path with \ as the path separator, @@ -404,7 +405,7 @@ class EntryProxy(SCons.Util.Proxy): else: entry = self.get() r = string.replace(entry.get_path(), os.sep, '\\') - return SCons.Util.SpecialAttrWrapper(r, entry.name + "_win32") + return SCons.Subst.SpecialAttrWrapper(r, entry.name + "_win32") def __get_srcnode(self): return EntryProxy(self.get().srcnode()) diff --git a/src/engine/SCons/Options/OptionsTests.py b/src/engine/SCons/Options/OptionsTests.py index b908568d..aeffe2df 100644 --- a/src/engine/SCons/Options/OptionsTests.py +++ b/src/engine/SCons/Options/OptionsTests.py @@ -29,7 +29,7 @@ import unittest import TestSCons import SCons.Options -import SCons.Util +import SCons.Subst import SCons.Warnings @@ -37,7 +37,7 @@ class Environment: def __init__(self): self.dict = {} def subst(self, x): - return SCons.Util.scons_subst(x, self, gvars=self.dict) + return SCons.Subst.scons_subst(x, self, gvars=self.dict) def __setitem__(self, key, value): self.dict[key] = value def __getitem__(self, key): diff --git a/src/engine/SCons/Platform/__init__.py b/src/engine/SCons/Platform/__init__.py index 385f0c99..7a2f1c37 100644 --- a/src/engine/SCons/Platform/__init__.py +++ b/src/engine/SCons/Platform/__init__.py @@ -178,7 +178,7 @@ class TempFileMunge: if not prefix: prefix = '@' - args = map(SCons.Util.quote_spaces, cmd[1:]) + args = map(SCons.Subst.quote_spaces, cmd[1:]) open(tmp, 'w').write(string.join(args, " ") + "\n") # XXX Using the SCons.Action.print_actions value directly # like this is bogus, but expedient. This class should diff --git a/src/engine/SCons/Subst.py b/src/engine/SCons/Subst.py new file mode 100644 index 00000000..47674039 --- /dev/null +++ b/src/engine/SCons/Subst.py @@ -0,0 +1,824 @@ +"""SCons.Subst + +SCons string substitution. + +""" + +# +# __COPYRIGHT__ +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# + +__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" + +import __builtin__ +import re +import string +import types +import UserList + +import SCons.Errors + +from SCons.Util import is_String, is_List + +# Indexed by the SUBST_* constants below. +_strconv = [SCons.Util.to_String, + SCons.Util.to_String, + SCons.Util.to_String_for_signature] + +class Literal: + """A wrapper for a string. If you use this object wrapped + around a string, then it will be interpreted as literal. + When passed to the command interpreter, all special + characters will be escaped.""" + def __init__(self, lstr): + self.lstr = lstr + + def __str__(self): + return self.lstr + + def escape(self, escape_func): + return escape_func(self.lstr) + + def for_signature(self): + return self.lstr + + def is_literal(self): + return 1 + +class SpecialAttrWrapper: + """This is a wrapper for what we call a 'Node special attribute.' + This is any of the attributes of a Node that we can reference from + Environment variable substitution, such as $TARGET.abspath or + $SOURCES[1].filebase. We implement the same methods as Literal + so we can handle special characters, plus a for_signature method, + such that we can return some canonical string during signature + calculation to avoid unnecessary rebuilds.""" + + def __init__(self, lstr, for_signature=None): + """The for_signature parameter, if supplied, will be the + canonical string we return from for_signature(). Else + we will simply return lstr.""" + self.lstr = lstr + if for_signature: + self.forsig = for_signature + else: + self.forsig = lstr + + def __str__(self): + return self.lstr + + def escape(self, escape_func): + return escape_func(self.lstr) + + def for_signature(self): + return self.forsig + + def is_literal(self): + return 1 + +def quote_spaces(arg): + """Generic function for putting double quotes around any string that + has white space in it.""" + if ' ' in arg or '\t' in arg: + return '"%s"' % arg + else: + return str(arg) + +class CmdStringHolder(SCons.Util.UserString): + """This is a special class used to hold strings generated by + scons_subst() and scons_subst_list(). It defines a special method + escape(). When passed a function with an escape algorithm for a + particular platform, it will return the contained string with the + proper escape sequences inserted. + + This should really be a subclass of UserString, but that module + doesn't exist in Python 1.5.2.""" + def __init__(self, cmd, literal=None): + SCons.Util.UserString.__init__(self, cmd) + self.literal = literal + + def is_literal(self): + return self.literal + + def escape(self, escape_func, quote_func=quote_spaces): + """Escape the string with the supplied function. The + function is expected to take an arbitrary string, then + return it with all special characters escaped and ready + for passing to the command interpreter. + + After calling this function, the next call to str() will + return the escaped string. + """ + + if self.is_literal(): + return escape_func(self.data) + elif ' ' in self.data or '\t' in self.data: + return quote_func(self.data) + else: + return self.data + +def escape_list(list, escape_func): + """Escape a list of arguments by running the specified escape_func + on every object in the list that has an escape() method.""" + def escape(obj, escape_func=escape_func): + try: + e = obj.escape + except AttributeError: + return obj + else: + return e(escape_func) + return map(escape, list) + +class NLWrapper: + """A wrapper class that delays turning a list of sources or targets + into a NodeList until it's needed. The specified function supplied + when the object is initialized is responsible for turning raw nodes + into proxies that implement the special attributes like .abspath, + .source, etc. This way, we avoid creating those proxies just + "in case" someone is going to use $TARGET or the like, and only + go through the trouble if we really have to. + + In practice, this might be a wash performance-wise, but it's a little + cleaner conceptually... + """ + + def __init__(self, list, func): + self.list = list + self.func = func + def _return_nodelist(self): + return self.nodelist + def _gen_nodelist(self): + list = self.list + if list is None: + list = [] + elif not is_List(list): + list = [list] + # The map(self.func) call is what actually turns + # a list into appropriate proxies. + self.nodelist = SCons.Util.NodeList(map(self.func, list)) + self._create_nodelist = self._return_nodelist + return self.nodelist + _create_nodelist = _gen_nodelist + + +class Targets_or_Sources(UserList.UserList): + """A class that implements $TARGETS or $SOURCES expansions by in turn + wrapping a NLWrapper. This class handles the different methods used + to access the list, calling the NLWrapper to create proxies on demand. + + Note that we subclass UserList.UserList purely so that the is_List() + function will identify an object of this class as a list during + variable expansion. We're not really using any UserList.UserList + methods in practice. + """ + def __init__(self, nl): + self.nl = nl + def __getattr__(self, attr): + nl = self.nl._create_nodelist() + return getattr(nl, attr) + def __getitem__(self, i): + nl = self.nl._create_nodelist() + return nl[i] + def __getslice__(self, i, j): + nl = self.nl._create_nodelist() + i = max(i, 0); j = max(j, 0) + return nl[i:j] + def __str__(self): + nl = self.nl._create_nodelist() + return str(nl) + def __repr__(self): + nl = self.nl._create_nodelist() + return repr(nl) + +class Target_or_Source: + """A class that implements $TARGET or $SOURCE expansions by in turn + wrapping a NLWrapper. This class handles the different methods used + to access an individual proxy Node, calling the NLWrapper to create + a proxy on demand. + """ + def __init__(self, nl): + self.nl = nl + def __getattr__(self, attr): + nl = self.nl._create_nodelist() + try: + nl0 = nl[0] + except IndexError: + # If there is nothing in the list, then we have no attributes to + # pass through, so raise AttributeError for everything. + raise AttributeError, "NodeList has no attribute: %s" % attr + return getattr(nl0, attr) + def __str__(self): + nl = self.nl._create_nodelist() + if nl: + return str(nl[0]) + return '' + def __repr__(self): + nl = self.nl._create_nodelist() + if nl: + return repr(nl[0]) + return '' + +def subst_dict(target, source): + """Create a dictionary for substitution of special + construction variables. + + This translates the following special arguments: + + target - the target (object or array of objects), + used to generate the TARGET and TARGETS + construction variables + + source - the source (object or array of objects), + used to generate the SOURCES and SOURCE + construction variables + """ + dict = {} + + if target: + tnl = NLWrapper(target, lambda x: x.get_subst_proxy()) + dict['TARGETS'] = Targets_or_Sources(tnl) + dict['TARGET'] = Target_or_Source(tnl) + else: + dict['TARGETS'] = None + dict['TARGET'] = None + + if source: + def get_src_subst_proxy(node): + try: + rfile = node.rfile + except AttributeError: + pass + else: + node = rfile() + return node.get_subst_proxy() + snl = NLWrapper(source, get_src_subst_proxy) + dict['SOURCES'] = Targets_or_Sources(snl) + dict['SOURCE'] = Target_or_Source(snl) + else: + dict['SOURCES'] = None + dict['SOURCE'] = None + + return dict + +# Constants for the "mode" parameter to scons_subst_list() and +# scons_subst(). SUBST_RAW gives the raw command line. SUBST_CMD +# gives a command line suitable for passing to a shell. SUBST_SIG +# gives a command line appropriate for calculating the signature +# of a command line...if this changes, we should rebuild. +SUBST_CMD = 0 +SUBST_RAW = 1 +SUBST_SIG = 2 + +_rm = re.compile(r'\$[()]') +_remove = re.compile(r'\$\([^\$]*(\$[^\)][^\$]*)*\$\)') + +# Indexed by the SUBST_* constants above. +_regex_remove = [ _rm, None, _remove ] + +# Regular expressions for splitting strings and handling substitutions, +# for use by the scons_subst() and scons_subst_list() functions: +# +# The first expression compiled matches all of the $-introduced tokens +# that we need to process in some way, and is used for substitutions. +# The expressions it matches are: +# +# "$$" +# "$(" +# "$)" +# "$variable" [must begin with alphabetic or underscore] +# "${any stuff}" +# +# The second expression compiled is used for splitting strings into tokens +# to be processed, and it matches all of the tokens listed above, plus +# the following that affect how arguments do or don't get joined together: +# +# " " [white space] +# "non-white-space" [without any dollar signs] +# "$" [single dollar sign] +# +_dollar_exps_str = r'\$[\$\(\)]|\$[_a-zA-Z][\.\w]*|\${[^}]*}' +_dollar_exps = re.compile(r'(%s)' % _dollar_exps_str) +_separate_args = re.compile(r'(%s|\s+|[^\s\$]+|\$)' % _dollar_exps_str) + +# This regular expression is used to replace strings of multiple white +# space characters in the string result from the scons_subst() function. +_space_sep = re.compile(r'[\t ]+(?![^{]*})') + +def scons_subst(strSubst, env, mode=SUBST_RAW, target=None, source=None, gvars={}, lvars={}, conv=None): + """Expand a string containing construction variable substitutions. + + This is the work-horse function for substitutions in file names + and the like. The companion scons_subst_list() function (below) + handles separating command lines into lists of arguments, so see + that function if that's what you're looking for. + """ + if type(strSubst) == types.StringType and string.find(strSubst, '$') < 0: + return strSubst + + class StringSubber: + """A class to construct the results of a scons_subst() call. + + This binds a specific construction environment, mode, target and + source with two methods (substitute() and expand()) that handle + the expansion. + """ + def __init__(self, env, mode, target, source, conv, gvars): + self.env = env + self.mode = mode + self.target = target + self.source = source + self.conv = conv + self.gvars = gvars + + def expand(self, s, lvars): + """Expand a single "token" as necessary, returning an + appropriate string containing the expansion. + + This handles expanding different types of things (strings, + lists, callables) appropriately. It calls the wrapper + substitute() method to re-expand things as necessary, so that + the results of expansions of side-by-side strings still get + re-evaluated separately, not smushed together. + """ + if is_String(s): + try: + s0, s1 = s[:2] + except (IndexError, ValueError): + return s + if s0 != '$': + return s + if s1 == '$': + return '$' + elif s1 in '()': + return s + else: + key = s[1:] + if key[0] == '{' or string.find(key, '.') >= 0: + if key[0] == '{': + key = key[1:-1] + try: + s = eval(key, self.gvars, lvars) + except AttributeError, e: + raise SCons.Errors.UserError, \ + "Error trying to evaluate `%s': %s" % (s, e) + except (IndexError, NameError, TypeError): + return '' + except SyntaxError,e: + if self.target: + raise SCons.Errors.BuildError, (self.target[0], "Syntax error `%s' trying to evaluate `%s'" % (e,s)) + else: + raise SCons.Errors.UserError, "Syntax error `%s' trying to evaluate `%s'" % (e,s) + else: + if lvars.has_key(key): + s = lvars[key] + elif self.gvars.has_key(key): + s = self.gvars[key] + else: + return '' + + # Before re-expanding the result, handle + # recursive expansion by copying the local + # variable dictionary and overwriting a null + # string for the value of the variable name + # we just expanded. + # + # This could potentially be optimized by only + # copying lvars when s contains more expansions, + # but lvars is usually supposed to be pretty + # small, and deeply nested variable expansions + # are probably more the exception than the norm, + # so it should be tolerable for now. + lv = lvars.copy() + var = string.split(key, '.')[0] + lv[var] = '' + return self.substitute(s, lv) + elif is_List(s): + def func(l, conv=self.conv, substitute=self.substitute, lvars=lvars): + return conv(substitute(l, lvars)) + r = map(func, s) + return string.join(r) + elif callable(s): + try: + s = s(target=self.target, + source=self.source, + env=self.env, + for_signature=(self.mode != SUBST_CMD)) + except TypeError: + # This probably indicates that it's a callable + # object that doesn't match our calling arguments + # (like an Action). + s = str(s) + return self.substitute(s, lvars) + elif s is None: + return '' + else: + return s + + def substitute(self, args, lvars): + """Substitute expansions in an argument or list of arguments. + + This serves as a wrapper for splitting up a string into + separate tokens. + """ + if is_String(args) and not isinstance(args, CmdStringHolder): + try: + def sub_match(match, conv=self.conv, expand=self.expand, lvars=lvars): + return conv(expand(match.group(1), lvars)) + result = _dollar_exps.sub(sub_match, args) + except TypeError: + # If the internal conversion routine doesn't return + # strings (it could be overridden to return Nodes, for + # example), then the 1.5.2 re module will throw this + # exception. Back off to a slower, general-purpose + # algorithm that works for all data types. + args = _separate_args.findall(args) + result = [] + for a in args: + result.append(self.conv(self.expand(a, lvars))) + try: + result = string.join(result, '') + except TypeError: + if len(result) == 1: + result = result[0] + return result + else: + return self.expand(args, lvars) + + if conv is None: + conv = _strconv[mode] + + # Doing this every time is a bit of a waste, since the Executor + # has typically already populated the OverrideEnvironment with + # $TARGET/$SOURCE variables. We're keeping this (for now), though, + # because it supports existing behavior that allows us to call + # an Action directly with an arbitrary target+source pair, which + # we use in Tool/tex.py to handle calling $BIBTEX when necessary. + # If we dropped that behavior (or found another way to cover it), + # we could get rid of this call completely and just rely on the + # Executor setting the variables. + d = subst_dict(target, source) + if d: + lvars = lvars.copy() + lvars.update(d) + + # We're (most likely) going to eval() things. If Python doesn't + # find a __builtin__ value in the global dictionary used for eval(), + # it copies the current __builtin__ values for you. Avoid this by + # setting it explicitly and then deleting, so we don't pollute the + # construction environment Dictionary(ies) that are typically used + # for expansion. + gvars['__builtin__'] = __builtin__ + + ss = StringSubber(env, mode, target, source, conv, gvars) + result = ss.substitute(strSubst, lvars) + + try: + del gvars['__builtin__'] + except KeyError: + pass + + if is_String(result): + # Remove $(-$) pairs and any stuff in between, + # if that's appropriate. + remove = _regex_remove[mode] + if remove: + result = remove.sub('', result) + if mode != SUBST_RAW: + # Compress strings of white space characters into + # a single space. + result = string.strip(_space_sep.sub(' ', result)) + + return result + +#Subst_List_Strings = {} + +def scons_subst_list(strSubst, env, mode=SUBST_RAW, target=None, source=None, gvars={}, lvars={}, conv=None): + """Substitute construction variables in a string (or list or other + object) and separate the arguments into a command list. + + The companion scons_subst() function (above) handles basic + substitutions within strings, so see that function instead + if that's what you're looking for. + """ +# try: +# Subst_List_Strings[strSubst] = Subst_List_Strings[strSubst] + 1 +# except KeyError: +# Subst_List_Strings[strSubst] = 1 +# import SCons.Debug +# SCons.Debug.caller(1) + class ListSubber(UserList.UserList): + """A class to construct the results of a scons_subst_list() call. + + Like StringSubber, this class binds a specific construction + environment, mode, target and source with two methods + (substitute() and expand()) that handle the expansion. + + In addition, however, this class is used to track the state of + the result(s) we're gathering so we can do the appropriate thing + whenever we have to append another word to the result--start a new + line, start a new word, append to the current word, etc. We do + this by setting the "append" attribute to the right method so + that our wrapper methods only need ever call ListSubber.append(), + and the rest of the object takes care of doing the right thing + internally. + """ + def __init__(self, env, mode, target, source, conv, gvars): + UserList.UserList.__init__(self, []) + self.env = env + self.mode = mode + self.target = target + self.source = source + self.conv = conv + self.gvars = gvars + + if self.mode == SUBST_RAW: + self.add_strip = lambda x, s=self: s.append(x) + else: + self.add_strip = lambda x, s=self: None + self.in_strip = None + self.next_line() + + def expand(self, s, lvars, within_list): + """Expand a single "token" as necessary, appending the + expansion to the current result. + + This handles expanding different types of things (strings, + lists, callables) appropriately. It calls the wrapper + substitute() method to re-expand things as necessary, so that + the results of expansions of side-by-side strings still get + re-evaluated separately, not smushed together. + """ + + if is_String(s): + try: + s0, s1 = s[:2] + except (IndexError, ValueError): + self.append(s) + return + if s0 != '$': + self.append(s) + return + if s1 == '$': + self.append('$') + elif s1 == '(': + self.open_strip('$(') + elif s1 == ')': + self.close_strip('$)') + else: + key = s[1:] + if key[0] == '{' or string.find(key, '.') >= 0: + if key[0] == '{': + key = key[1:-1] + try: + s = eval(key, self.gvars, lvars) + except AttributeError, e: + raise SCons.Errors.UserError, \ + "Error trying to evaluate `%s': %s" % (s, e) + except (IndexError, NameError, TypeError): + return + except SyntaxError,e: + if self.target: + raise SCons.Errors.BuildError, (self.target[0], "Syntax error `%s' trying to evaluate `%s'" % (e,s)) + else: + raise SCons.Errors.UserError, "Syntax error `%s' trying to evaluate `%s'" % (e,s) + else: + if lvars.has_key(key): + s = lvars[key] + elif self.gvars.has_key(key): + s = self.gvars[key] + else: + return + + # Before re-expanding the result, handle + # recursive expansion by copying the local + # variable dictionary and overwriting a null + # string for the value of the variable name + # we just expanded. + lv = lvars.copy() + var = string.split(key, '.')[0] + lv[var] = '' + self.substitute(s, lv, 0) + self.this_word() + elif is_List(s): + for a in s: + self.substitute(a, lvars, 1) + self.next_word() + elif callable(s): + try: + s = s(target=self.target, + source=self.source, + env=self.env, + for_signature=(self.mode != SUBST_CMD)) + except TypeError: + # This probably indicates that it's a callable + # object that doesn't match our calling arguments + # (like an Action). + s = str(s) + self.substitute(s, lvars, within_list) + elif s is None: + self.this_word() + else: + self.append(s) + + def substitute(self, args, lvars, within_list): + """Substitute expansions in an argument or list of arguments. + + This serves as a wrapper for splitting up a string into + separate tokens. + """ + + if is_String(args) and not isinstance(args, CmdStringHolder): + args = _separate_args.findall(args) + for a in args: + if a[0] in ' \t\n\r\f\v': + if '\n' in a: + self.next_line() + elif within_list: + self.append(a) + else: + self.next_word() + else: + self.expand(a, lvars, within_list) + else: + self.expand(args, lvars, within_list) + + def next_line(self): + """Arrange for the next word to start a new line. This + is like starting a new word, except that we have to append + another line to the result.""" + UserList.UserList.append(self, []) + self.next_word() + + def this_word(self): + """Arrange for the next word to append to the end of the + current last word in the result.""" + self.append = self.add_to_current_word + + def next_word(self): + """Arrange for the next word to start a new word.""" + self.append = self.add_new_word + + def add_to_current_word(self, x): + """Append the string x to the end of the current last word + in the result. If that is not possible, then just add + it as a new word. Make sure the entire concatenated string + inherits the object attributes of x (in particular, the + escape function) by wrapping it as CmdStringHolder.""" + + if not self.in_strip or self.mode != SUBST_SIG: + try: + current_word = self[-1][-1] + except IndexError: + self.add_new_word(x) + else: + # All right, this is a hack and it should probably + # be refactored out of existence in the future. + # The issue is that we want to smoosh words together + # and make one file name that gets escaped if + # we're expanding something like foo$EXTENSION, + # but we don't want to smoosh them together if + # it's something like >$TARGET, because then we'll + # treat the '>' like it's part of the file name. + # So for now, just hard-code looking for the special + # command-line redirection characters... + try: + last_char = str(current_word)[-1] + except IndexError: + last_char = '\0' + if last_char in '<>|': + self.add_new_word(x) + else: + y = current_word + x + literal1 = self.literal(self[-1][-1]) + literal2 = self.literal(x) + y = self.conv(y) + if is_String(y): + y = CmdStringHolder(y, literal1 or literal2) + self[-1][-1] = y + + def add_new_word(self, x): + if not self.in_strip or self.mode != SUBST_SIG: + literal = self.literal(x) + x = self.conv(x) + if is_String(x): + x = CmdStringHolder(x, literal) + self[-1].append(x) + self.append = self.add_to_current_word + + def literal(self, x): + try: + l = x.is_literal + except AttributeError: + return None + else: + return l() + + def open_strip(self, x): + """Handle the "open strip" $( token.""" + self.add_strip(x) + self.in_strip = 1 + + def close_strip(self, x): + """Handle the "close strip" $) token.""" + self.add_strip(x) + self.in_strip = None + + if conv is None: + conv = _strconv[mode] + + # Doing this every time is a bit of a waste, since the Executor + # has typically already populated the OverrideEnvironment with + # $TARGET/$SOURCE variables. We're keeping this (for now), though, + # because it supports existing behavior that allows us to call + # an Action directly with an arbitrary target+source pair, which + # we use in Tool/tex.py to handle calling $BIBTEX when necessary. + # If we dropped that behavior (or found another way to cover it), + # we could get rid of this call completely and just rely on the + # Executor setting the variables. + d = subst_dict(target, source) + if d: + lvars = lvars.copy() + lvars.update(d) + + # We're (most likely) going to eval() things. If Python doesn't + # find a __builtin__ value in the global dictionary used for eval(), + # it copies the current __builtin__ values for you. Avoid this by + # setting it explicitly and then deleting, so we don't pollute the + # construction environment Dictionary(ies) that are typically used + # for expansion. + gvars['__builtins__'] = __builtins__ + + ls = ListSubber(env, mode, target, source, conv, gvars) + ls.substitute(strSubst, lvars, 0) + + try: + del gvars['__builtins__'] + except KeyError: + pass + + return ls.data + +def scons_subst_once(strSubst, env, key): + """Perform single (non-recursive) substitution of a single + construction variable keyword. + + This is used when setting a variable when copying or overriding values + in an Environment. We want to capture (expand) the old value before + we override it, so people can do things like: + + env2 = env.Copy(CCFLAGS = '$CCFLAGS -g') + + We do this with some straightforward, brute-force code here... + """ + if type(strSubst) == types.StringType and string.find(strSubst, '$') < 0: + return strSubst + + matchlist = ['$' + key, '${' + key + '}'] + val = env.get(key, '') + def sub_match(match, val=val, matchlist=matchlist): + a = match.group(1) + if a in matchlist: + a = val + if is_List(a): + return string.join(map(str, a)) + else: + return str(a) + + if is_List(strSubst): + result = [] + for arg in strSubst: + if is_String(arg): + if arg in matchlist: + arg = val + if is_List(arg): + result.extend(arg) + else: + result.append(arg) + else: + result.append(_dollar_exps.sub(sub_match, arg)) + else: + result.append(arg) + return result + elif is_String(strSubst): + return _dollar_exps.sub(sub_match, strSubst) + else: + return strSubst diff --git a/src/engine/SCons/SubstTests.py b/src/engine/SCons/SubstTests.py new file mode 100644 index 00000000..64a2fe5a --- /dev/null +++ b/src/engine/SCons/SubstTests.py @@ -0,0 +1,1135 @@ +# +# __COPYRIGHT__ +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# + +__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" + +import os +import os.path +import string +import StringIO +import sys +import types +import unittest + +from UserDict import UserDict + +import SCons.Errors + +from SCons.Subst import * + +class DummyNode: + """Simple node work-alike.""" + def __init__(self, name): + self.name = os.path.normpath(name) + def __str__(self): + return self.name + def is_literal(self): + return 1 + def rfile(self): + return self + def get_subst_proxy(self): + return self + +class DummyEnv: + def __init__(self, dict={}): + self.dict = dict + + def Dictionary(self, key = None): + if not key: + return self.dict + return self.dict[key] + + def __getitem__(self, key): + return self.dict[key] + + def get(self, key, default): + return self.dict.get(key, default) + + def sig_dict(self): + dict = self.dict.copy() + dict["TARGETS"] = 'tsig' + dict["SOURCES"] = 'ssig' + return dict + +def cs(target=None, source=None, env=None, for_signature=None): + return 'cs' + +def cl(target=None, source=None, env=None, for_signature=None): + return ['cl'] + +def CmdGen1(target, source, env, for_signature): + # Nifty trick...since Environment references are interpolated, + # instantiate an instance of a callable class with this one, + # which will then get evaluated. + assert str(target) == 't', target + assert str(source) == 's', source + return "${CMDGEN2('foo', %d)}" % for_signature + +class CmdGen2: + def __init__(self, mystr, forsig): + self.mystr = mystr + self.expect_for_signature = forsig + + def __call__(self, target, source, env, for_signature): + assert str(target) == 't', target + assert str(source) == 's', source + assert for_signature == self.expect_for_signature, for_signature + return [ self.mystr, env.Dictionary('BAR') ] + +if os.sep == '/': + def cvt(str): + return str +else: + def cvt(str): + return string.replace(str, '/', os.sep) + +class SubstTestCase(unittest.TestCase): + def test_subst(self): + """Test the subst() function""" + class MyNode(DummyNode): + """Simple node work-alike with some extra stuff for testing.""" + def __init__(self, name): + DummyNode.__init__(self, name) + class Attribute: + pass + self.attribute = Attribute() + self.attribute.attr1 = 'attr$1-' + os.path.basename(name) + self.attribute.attr2 = 'attr$2-' + os.path.basename(name) + def get_stuff(self, extra): + return self.name + extra + foo = 1 + + class TestLiteral: + def __init__(self, literal): + self.literal = literal + def __str__(self): + return self.literal + def is_literal(self): + return 1 + + class TestCallable: + def __init__(self, value): + self.value = value + def __call__(self): + pass + def __str__(self): + return self.value + + def function_foo(arg): + pass + + target = [ MyNode("./foo/bar.exe"), + MyNode("/bar/baz.obj"), + MyNode("../foo/baz.obj") ] + source = [ MyNode("./foo/blah.cpp"), + MyNode("/bar/ack.cpp"), + MyNode("../foo/ack.c") ] + + loc = { + 'xxx' : None, + 'null' : '', + 'zero' : 0, + 'one' : 1, + 'BAR' : 'baz', + 'ONE' : '$TWO', + 'TWO' : '$THREE', + 'THREE' : 'four', + + 'AAA' : 'a', + 'BBB' : 'b', + 'CCC' : 'c', + + # $XXX$HHH should expand to GGGIII, not BADNEWS. + 'XXX' : '$FFF', + 'FFF' : 'GGG', + 'HHH' : 'III', + 'FFFIII' : 'BADNEWS', + + 'LITERAL' : TestLiteral("$XXX"), + + # Test that we can expand to and return a function. + #'FUNCTION' : function_foo, + + 'CMDGEN1' : CmdGen1, + 'CMDGEN2' : CmdGen2, + + 'NOTHING' : "", + 'NONE' : None, + + # Test various combinations of strings, lists and functions. + 'N' : None, + 'X' : 'x', + 'Y' : '$X', + 'R' : '$R', + 'S' : 'x y', + 'LS' : ['x y'], + 'L' : ['x', 'y'], + 'CS' : cs, + 'CL' : cl, + + # Test function calls within ${}. + 'FUNCCALL' : '${FUNC1("$AAA $FUNC2 $BBB")}', + 'FUNC1' : lambda x: x, + 'FUNC2' : lambda target, source, env, for_signature: ['x$CCC'], + + # Various tests refactored from ActionTests.py. + 'LIST' : [["This", "is", "$(", "$a", "$)", "test"]], + + # Test recursion. + 'RECURSE' : 'foo $RECURSE bar', + 'RRR' : 'foo $SSS bar', + 'SSS' : '$RRR', + + # Test callables that don't match the calling arguments. + 'CALLABLE' : TestCallable('callable-1'), + } + + env = DummyEnv(loc) + + # Basic tests of substitution functionality. + cases = [ + # Basics: strings without expansions are left alone, and + # the simplest possible expansion to a null-string value. + "test", "test", + "$null", "", + + # Test expansion of integer values. + "test $zero", "test 0", + "test $one", "test 1", + + # Test multiple re-expansion of values. + "test $ONE", "test four", + + # Test a whole bunch of $TARGET[S] and $SOURCE[S] expansions. + "test $TARGETS $SOURCES", + "test foo/bar.exe /bar/baz.obj ../foo/baz.obj foo/blah.cpp /bar/ack.cpp ../foo/ack.c", + + "test ${TARGETS[:]} ${SOURCES[0]}", + "test foo/bar.exe /bar/baz.obj ../foo/baz.obj foo/blah.cpp", + + "test ${TARGETS[1:]}v", + "test /bar/baz.obj ../foo/baz.objv", + + "test $TARGET", + "test foo/bar.exe", + + "test $TARGET$FOO[0]", + "test foo/bar.exe[0]", + + "test $TARGETS.foo", + "test 1 1 1", + + "test ${SOURCES[0:2].foo}", + "test 1 1", + + "test $SOURCE.foo", + "test 1", + + "test ${TARGET.get_stuff('blah')}", + "test foo/bar.exeblah", + + "test ${SOURCES.get_stuff('blah')}", + "test foo/blah.cppblah /bar/ack.cppblah ../foo/ack.cblah", + + "test ${SOURCES[0:2].get_stuff('blah')}", + "test foo/blah.cppblah /bar/ack.cppblah", + + "test ${SOURCES[0:2].get_stuff('blah')}", + "test foo/blah.cppblah /bar/ack.cppblah", + + "test ${SOURCES.attribute.attr1}", + "test attr$1-blah.cpp attr$1-ack.cpp attr$1-ack.c", + + "test ${SOURCES.attribute.attr2}", + "test attr$2-blah.cpp attr$2-ack.cpp attr$2-ack.c", + + # Test adjacent expansions. + "foo$BAR", + "foobaz", + + "foo${BAR}", + "foobaz", + + # Test that adjacent expansions don't get re-interpreted + # together. The correct disambiguated expansion should be: + # $XXX$HHH => ${FFF}III => GGGIII + # not: + # $XXX$HHH => ${FFFIII} => BADNEWS + "$XXX$HHH", "GGGIII", + + # Test double-dollar-sign behavior. + "$$FFF$HHH", "$FFFIII", + + # Test that a Literal will stop dollar-sign substitution. + "$XXX $LITERAL $FFF", "GGG $XXX GGG", + + # Test that we don't blow up even if they subscript + # something in ways they "can't." + "${FFF[0]}", "G", + "${FFF[7]}", "", + "${NOTHING[1]}", "", + "${NONE[2]}", "", + + # Test various combinations of strings and lists. + #None, '', + '', '', + 'x', 'x', + 'x y', 'x y', + '$N', '', + '$X', 'x', + '$Y', 'x', + '$R', '', + '$S', 'x y', + '$LS', 'x y', + '$L', 'x y', + '$S z', 'x y z', + '$LS z', 'x y z', + '$L z', 'x y z', + #cs, 'cs', + #cl, 'cl', + '$CS', 'cs', + '$CL', 'cl', + + # Test function calls within ${}. + '$FUNCCALL', 'a xc b', + + # Bug reported by Christoph Wiedemann. + cvt('$xxx/bin'), '/bin', + + # Tests callables that don't match our calling arguments. + '$CALLABLE', 'callable-1', + + # Test handling of quotes. + 'aaa "bbb ccc" ddd', 'aaa "bbb ccc" ddd', + ] + + kwargs = {'target' : target, 'source' : source, + 'gvars' : env.Dictionary()} + + failed = 0 + while cases: + input, expect = cases[:2] + expect = cvt(expect) + result = apply(scons_subst, (input, env), kwargs) + if result != expect: + if failed == 0: print + print " input %s => %s did not match %s" % (repr(input), repr(result), repr(expect)) + failed = failed + 1 + del cases[:2] + assert failed == 0, "%d subst() cases failed" % failed + + # The expansion dictionary no longer comes from the construction + # environment automatically. + s = scons_subst('$AAA', env) + assert s == '', s + + # Tests of the various SUBST_* modes of substitution. + subst_cases = [ + "test $xxx", + "test ", + "test", + "test", + + "test $($xxx$)", + "test $($)", + "test", + "test", + + "test $( $xxx $)", + "test $( $)", + "test", + "test", + + "$AAA ${AAA}A $BBBB $BBB", + "a aA b", + "a aA b", + "a aA b", + + "$RECURSE", + "foo bar", + "foo bar", + "foo bar", + + "$RRR", + "foo bar", + "foo bar", + "foo bar", + + # Verify what happens with no target or source nodes. + "$TARGET $SOURCES", + " ", + "", + "", + + "$TARGETS $SOURCE", + " ", + "", + "", + + # Various tests refactored from ActionTests.py. + "${LIST}", + "This is $( $) test", + "This is test", + "This is test", + + ["|", "$(", "$AAA", "|", "$BBB", "$)", "|", "$CCC", 1], + "| $( a | b $) | c 1", + "| a | b | c 1", + "| | c 1", + ] + + gvars = env.Dictionary() + + failed = 0 + while subst_cases: + input, eraw, ecmd, esig = subst_cases[:4] + result = scons_subst(input, env, mode=SUBST_RAW, gvars=gvars) + if result != eraw: + if failed == 0: print + print " input %s => RAW %s did not match %s" % (repr(input), repr(result), repr(eraw)) + failed = failed + 1 + result = scons_subst(input, env, mode=SUBST_CMD, gvars=gvars) + if result != ecmd: + if failed == 0: print + print " input %s => CMD %s did not match %s" % (repr(input), repr(result), repr(ecmd)) + failed = failed + 1 + result = scons_subst(input, env, mode=SUBST_SIG, gvars=gvars) + if result != esig: + if failed == 0: print + print " input %s => SIG %s did not match %s" % (repr(input), repr(result), repr(esig)) + failed = failed + 1 + del subst_cases[:4] + assert failed == 0, "%d subst() mode cases failed" % failed + + t1 = MyNode('t1') + t2 = MyNode('t2') + s1 = MyNode('s1') + s2 = MyNode('s2') + result = scons_subst("$TARGET $SOURCES", env, + target=[t1, t2], + source=[s1, s2]) + assert result == "t1 s1 s2", result + result = scons_subst("$TARGET $SOURCES", env, + target=[t1, t2], + source=[s1, s2], + gvars={}) + assert result == "t1 s1 s2", result + + result = scons_subst("$TARGET $SOURCES", env, target=[], source=[]) + assert result == " ", result + result = scons_subst("$TARGETS $SOURCE", env, target=[], source=[]) + assert result == " ", result + + # Test interpolating a callable. + newcom = scons_subst("test $CMDGEN1 $SOURCES $TARGETS", + env, target=MyNode('t'), source=MyNode('s'), + gvars=gvars) + assert newcom == "test foo baz s t", newcom + + # Test that we handle attribute errors during expansion as expected. + try: + class Foo: + pass + scons_subst('${foo.bar}', env, gvars={'foo':Foo()}) + except SCons.Errors.UserError, e: + expect = [ + "Error trying to evaluate `${foo.bar}': bar", + "Error trying to evaluate `${foo.bar}': Foo instance has no attribute 'bar'", + ] + assert str(e) in expect, e + else: + raise AssertionError, "did not catch expected UserError" + + # Test that we handle syntax errors during expansion as expected. + try: + scons_subst('$foo.bar.3.0', env) + except SCons.Errors.UserError, e: + expect1 = "Syntax error `invalid syntax' trying to evaluate `$foo.bar.3.0'" + expect2 = "Syntax error `invalid syntax (line 1)' trying to evaluate `$foo.bar.3.0'" + assert str(e) in [expect1, expect2], e + else: + raise AssertionError, "did not catch expected UserError" + + # Test how we handle overriding the internal conversion routines. + def s(obj): + return obj + + n1 = MyNode('n1') + env = DummyEnv({'NODE' : n1}) + gvars = env.Dictionary() + node = scons_subst("$NODE", env, mode=SUBST_RAW, conv=s, gvars=gvars) + assert node is n1, node + node = scons_subst("$NODE", env, mode=SUBST_CMD, conv=s, gvars=gvars) + assert node is n1, node + node = scons_subst("$NODE", env, mode=SUBST_SIG, conv=s, gvars=gvars) + assert node is n1, node + + # Test returning a function. + #env = DummyEnv({'FUNCTION' : foo}) + #gvars = env.Dictionary() + #func = scons_subst("$FUNCTION", env, mode=SUBST_RAW, call=None, gvars=gvars) + #assert func is function_foo, func + #func = scons_subst("$FUNCTION", env, mode=SUBST_CMD, call=None, gvars=gvars) + #assert func is function_foo, func + #func = scons_subst("$FUNCTION", env, mode=SUBST_SIG, call=None, gvars=gvars) + #assert func is function_foo, func + + # Test supplying an overriding gvars dictionary. + env = DummyEnv({'XXX' : 'xxx'}) + result = scons_subst('$XXX', env, gvars=env.Dictionary()) + assert result == 'xxx', result + result = scons_subst('$XXX', env, gvars={'XXX' : 'yyy'}) + assert result == 'yyy', result + + def test_CLVar(self): + """Test scons_subst() and scons_subst_list() with CLVar objects""" + + loc = {} + loc['FOO'] = 'foo' + loc['BAR'] = SCons.Util.CLVar('bar') + loc['CALL'] = lambda target, source, env, for_signature: 'call' + env = DummyEnv(loc) + + cmd = SCons.Util.CLVar("test $FOO $BAR $CALL test") + + newcmd = scons_subst(cmd, env, gvars=env.Dictionary()) + assert newcmd == 'test foo bar call test', newcmd + + cmd_list = scons_subst_list(cmd, env, gvars=env.Dictionary()) + assert len(cmd_list) == 1, cmd_list + assert cmd_list[0][0] == "test", cmd_list[0][0] + assert cmd_list[0][1] == "foo", cmd_list[0][1] + assert cmd_list[0][2] == "bar", cmd_list[0][2] + assert cmd_list[0][3] == "call", cmd_list[0][3] + assert cmd_list[0][4] == "test", cmd_list[0][4] + + def test_subst_list(self): + """Testing the scons_subst_list() method...""" + class MyNode(DummyNode): + """Simple node work-alike with some extra stuff for testing.""" + def __init__(self, name): + DummyNode.__init__(self, name) + class Attribute: + pass + self.attribute = Attribute() + self.attribute.attr1 = 'attr$1-' + os.path.basename(name) + self.attribute.attr2 = 'attr$2-' + os.path.basename(name) + + class TestCallable: + def __init__(self, value): + self.value = value + def __call__(self): + pass + def __str__(self): + return self.value + + target = [ MyNode("./foo/bar.exe"), + MyNode("/bar/baz with spaces.obj"), + MyNode("../foo/baz.obj") ] + source = [ MyNode("./foo/blah with spaces.cpp"), + MyNode("/bar/ack.cpp"), + MyNode("../foo/ack.c") ] + + def _defines(defs): + l = [] + for d in defs: + if SCons.Util.is_List(d) or type(d) is types.TupleType: + l.append(str(d[0]) + '=' + str(d[1])) + else: + l.append(str(d)) + return l + + loc = { + 'xxx' : None, + 'NEWLINE' : 'before\nafter', + + 'AAA' : 'a', + 'BBB' : 'b', + 'CCC' : 'c', + + 'DO' : DummyNode('do something'), + 'FOO' : DummyNode('foo.in'), + 'BAR' : DummyNode('bar with spaces.out'), + 'CRAZY' : DummyNode('crazy\nfile.in'), + + # $XXX$HHH should expand to GGGIII, not BADNEWS. + 'XXX' : '$FFF', + 'FFF' : 'GGG', + 'HHH' : 'III', + 'FFFIII' : 'BADNEWS', + + 'CMDGEN1' : CmdGen1, + 'CMDGEN2' : CmdGen2, + + 'LITERALS' : [ Literal('foo\nwith\nnewlines'), + Literal('bar\nwith\nnewlines') ], + + # Test various combinations of strings, lists and functions. + 'N' : None, + 'X' : 'x', + 'Y' : '$X', + 'R' : '$R', + 'S' : 'x y', + 'LS' : ['x y'], + 'L' : ['x', 'y'], + 'CS' : cs, + 'CL' : cl, + + # Test function calls within ${}. + 'FUNCCALL' : '${FUNC1("$AAA $FUNC2 $BBB")}', + 'FUNC1' : lambda x: x, + 'FUNC2' : lambda target, source, env, for_signature: ['x$CCC'], + + # Various tests refactored from ActionTests.py. + 'LIST' : [["This", "is", "$(", "$a", "$)", "test"]], + + # Test recursion. + 'RECURSE' : 'foo $RECURSE bar', + 'RRR' : 'foo $SSS bar', + 'SSS' : '$RRR', + + # Test callable objects that don't match our calling arguments. + 'CALLABLE' : TestCallable('callable-2'), + + '_defines' : _defines, + 'DEFS' : [ ('Q1', '"q1"'), ('Q2', '"$AAA"') ], + } + + env = DummyEnv(loc) + + cases = [ + "$TARGETS", + [ + ["foo/bar.exe", "/bar/baz with spaces.obj", "../foo/baz.obj"], + ], + + "$SOURCES $NEWLINE $TARGETS", + [ + ["foo/blah with spaces.cpp", "/bar/ack.cpp", "../foo/ack.c", "before"], + ["after", "foo/bar.exe", "/bar/baz with spaces.obj", "../foo/baz.obj"], + ], + + "$SOURCES$NEWLINE", + [ + ["foo/blah with spaces.cpp", "/bar/ack.cpp", "../foo/ack.cbefore"], + ["after"], + ], + + "foo$FFF", + [ + ["fooGGG"], + ], + + "foo${FFF}", + [ + ["fooGGG"], + ], + + "test ${SOURCES.attribute.attr1}", + [ + ["test", "attr$1-blah with spaces.cpp", "attr$1-ack.cpp", "attr$1-ack.c"], + ], + + "test ${SOURCES.attribute.attr2}", + [ + ["test", "attr$2-blah with spaces.cpp", "attr$2-ack.cpp", "attr$2-ack.c"], + ], + + "$DO --in=$FOO --out=$BAR", + [ + ["do something", "--in=foo.in", "--out=bar with spaces.out"], + ], + + # This test is now fixed, and works like it should. + "$DO --in=$CRAZY --out=$BAR", + [ + ["do something", "--in=crazy\nfile.in", "--out=bar with spaces.out"], + ], + + # Try passing a list to scons_subst_list(). + [ "$SOURCES$NEWLINE", "$TARGETS", "This is a test"], + [ + ["foo/blah with spaces.cpp", "/bar/ack.cpp", "../foo/ack.cbefore"], + ["after", "foo/bar.exe", "/bar/baz with spaces.obj", "../foo/baz.obj", "This is a test"], + ], + + # Test against a former bug in scons_subst_list(). + "$XXX$HHH", + [ + ["GGGIII"], + ], + + # Test double-dollar-sign behavior. + "$$FFF$HHH", + [ + ["$FFFIII"], + ], + + # Test various combinations of strings, lists and functions. + None, [[]], + [None], [[]], + '', [[]], + [''], [[]], + 'x', [['x']], + ['x'], [['x']], + 'x y', [['x', 'y']], + ['x y'], [['x y']], + ['x', 'y'], [['x', 'y']], + '$N', [[]], + ['$N'], [[]], + '$X', [['x']], + ['$X'], [['x']], + '$Y', [['x']], + ['$Y'], [['x']], + #'$R', [[]], + #['$R'], [[]], + '$S', [['x', 'y']], + '$S z', [['x', 'y', 'z']], + ['$S'], [['x', 'y']], + ['$S z'], [['x', 'y z']], # XXX - IS THIS BEST? + ['$S', 'z'], [['x', 'y', 'z']], + '$LS', [['x y']], + '$LS z', [['x y', 'z']], + ['$LS'], [['x y']], + ['$LS z'], [['x y z']], + ['$LS', 'z'], [['x y', 'z']], + '$L', [['x', 'y']], + '$L z', [['x', 'y', 'z']], + ['$L'], [['x', 'y']], + ['$L z'], [['x', 'y z']], # XXX - IS THIS BEST? + ['$L', 'z'], [['x', 'y', 'z']], + cs, [['cs']], + [cs], [['cs']], + cl, [['cl']], + [cl], [['cl']], + '$CS', [['cs']], + ['$CS'], [['cs']], + '$CL', [['cl']], + ['$CL'], [['cl']], + + # Test function calls within ${}. + '$FUNCCALL', [['a', 'xc', 'b']], + + # Test handling of newlines in white space. + 'foo\nbar', [['foo'], ['bar']], + 'foo\n\nbar', [['foo'], ['bar']], + 'foo \n \n bar', [['foo'], ['bar']], + 'foo \nmiddle\n bar', [['foo'], ['middle'], ['bar']], + + # Bug reported by Christoph Wiedemann. + cvt('$xxx/bin'), [['/bin']], + + # Test variables smooshed together with different prefixes. + 'foo$AAA', [['fooa']], + '<$AAA', [['<', 'a']], + '>$AAA', [['>', 'a']], + '|$AAA', [['|', 'a']], + + # Test callables that don't match our calling arguments. + '$CALLABLE', [['callable-2']], + + # Test + + # Test handling of quotes. + # XXX Find a way to handle this in the future. + #'aaa "bbb ccc" ddd', [['aaa', 'bbb ccc', 'ddd']], + + '${_defines(DEFS)}', [['Q1="q1"', 'Q2="a"']], + ] + + gvars = env.Dictionary() + + kwargs = {'target' : target, 'source' : source, 'gvars' : gvars} + + failed = 0 + while cases: + input, expect = cases[:2] + expect = map(lambda l: map(cvt, l), expect) + result = apply(scons_subst_list, (input, env), kwargs) + if result != expect: + if failed == 0: print + print " input %s => %s did not match %s" % (repr(input), result, repr(expect)) + failed = failed + 1 + del cases[:2] + assert failed == 0, "%d subst_list() cases failed" % failed + + # The expansion dictionary no longer comes from the construction + # environment automatically. + s = scons_subst_list('$AAA', env) + assert s == [[]], s + + t1 = MyNode('t1') + t2 = MyNode('t2') + s1 = MyNode('s1') + s2 = MyNode('s2') + result = scons_subst_list("$TARGET $SOURCES", env, + target=[t1, t2], + source=[s1, s2], + gvars=gvars) + assert result == [['t1', 's1', 's2']], result + result = scons_subst_list("$TARGET $SOURCES", env, + target=[t1, t2], + source=[s1, s2], + gvars={}) + assert result == [['t1', 's1', 's2']], result + + # Test interpolating a callable. + _t = DummyNode('t') + _s = DummyNode('s') + cmd_list = scons_subst_list("testing $CMDGEN1 $TARGETS $SOURCES", + env, target=_t, source=_s, + gvars=gvars) + assert cmd_list == [['testing', 'foo', 'bar with spaces.out', 't', 's']], cmd_list + + # Test escape functionality. + def escape_func(foo): + return '**' + foo + '**' + cmd_list = scons_subst_list("abc $LITERALS xyz", env, gvars=gvars) + assert cmd_list == [['abc', + 'foo\nwith\nnewlines', + 'bar\nwith\nnewlines', + 'xyz']], cmd_list + c = cmd_list[0][0].escape(escape_func) + assert c == 'abc', c + c = cmd_list[0][1].escape(escape_func) + assert c == '**foo\nwith\nnewlines**', c + c = cmd_list[0][2].escape(escape_func) + assert c == '**bar\nwith\nnewlines**', c + c = cmd_list[0][3].escape(escape_func) + assert c == 'xyz', c + + cmd_list = scons_subst_list("abc${LITERALS}xyz", env, gvars=gvars) + c = cmd_list[0][0].escape(escape_func) + assert c == '**abcfoo\nwith\nnewlines**', c + c = cmd_list[0][1].escape(escape_func) + assert c == '**bar\nwith\nnewlinesxyz**', c + + # Tests of the various SUBST_* modes of substitution. + subst_list_cases = [ + "test $xxx", + [["test"]], + [["test"]], + [["test"]], + + "test $($xxx$)", + [["test", "$($)"]], + [["test"]], + [["test"]], + + "test $( $xxx $)", + [["test", "$(", "$)"]], + [["test"]], + [["test"]], + + "$AAA ${AAA}A $BBBB $BBB", + [["a", "aA", "b"]], + [["a", "aA", "b"]], + [["a", "aA", "b"]], + + "$RECURSE", + [["foo", "bar"]], + [["foo", "bar"]], + [["foo", "bar"]], + + "$RRR", + [["foo", "bar"]], + [["foo", "bar"]], + [["foo", "bar"]], + + # Verify what happens with no target or source nodes. + "$TARGET $SOURCES", + [[]], + [[]], + [[]], + + "$TARGETS $SOURCE", + [[]], + [[]], + [[]], + + # Various test refactored from ActionTests.py + "${LIST}", + [['This', 'is', '$(', '$)', 'test']], + [['This', 'is', 'test']], + [['This', 'is', 'test']], + + ["|", "$(", "$AAA", "|", "$BBB", "$)", "|", "$CCC", 1], + [["|", "$(", "a", "|", "b", "$)", "|", "c", "1"]], + [["|", "a", "|", "b", "|", "c", "1"]], + [["|", "|", "c", "1"]], + ] + + gvars = env.Dictionary() + + r = scons_subst_list("$TARGET $SOURCES", env, mode=SUBST_RAW, gvars=gvars) + assert r == [[]], r + + failed = 0 + while subst_list_cases: + input, eraw, ecmd, esig = subst_list_cases[:4] + result = scons_subst_list(input, env, mode=SUBST_RAW, gvars=gvars) + if result != eraw: + if failed == 0: print + print " input %s => RAW %s did not match %s" % (repr(input), repr(result), repr(eraw)) + failed = failed + 1 + result = scons_subst_list(input, env, mode=SUBST_CMD, gvars=gvars) + if result != ecmd: + if failed == 0: print + print " input %s => CMD %s did not match %s" % (repr(input), repr(result), repr(ecmd)) + failed = failed + 1 + result = scons_subst_list(input, env, mode=SUBST_SIG, gvars=gvars) + if result != esig: + if failed == 0: print + print " input %s => SIG %s did not match %s" % (repr(input), repr(result), repr(esig)) + failed = failed + 1 + del subst_list_cases[:4] + assert failed == 0, "%d subst() mode cases failed" % failed + + # Test that we handle attribute errors during expansion as expected. + try: + class Foo: + pass + scons_subst_list('${foo.bar}', env, gvars={'foo':Foo()}) + except SCons.Errors.UserError, e: + expect = [ + "Error trying to evaluate `${foo.bar}': bar", + "Error trying to evaluate `${foo.bar}': Foo instance has no attribute 'bar'", + ] + assert str(e) in expect, e + else: + raise AssertionError, "did not catch expected UserError" + + # Test that we handle syntax errors during expansion as expected. + try: + scons_subst_list('$foo.bar.3.0', env) + except SCons.Errors.UserError, e: + expect1 = "Syntax error `invalid syntax' trying to evaluate `$foo.bar.3.0'" + expect2 = "Syntax error `invalid syntax (line 1)' trying to evaluate `$foo.bar.3.0'" + assert str(e) in [expect1, expect2], e + else: + raise AssertionError, "did not catch expected SyntaxError" + + # Test we handle overriding the internal conversion routines. + def s(obj): + return obj + + n1 = MyNode('n1') + env = DummyEnv({'NODE' : n1}) + gvars=env.Dictionary() + node = scons_subst_list("$NODE", env, mode=SUBST_RAW, conv=s, gvars=gvars) + assert node == [[n1]], node + node = scons_subst_list("$NODE", env, mode=SUBST_CMD, conv=s, gvars=gvars) + assert node == [[n1]], node + node = scons_subst_list("$NODE", env, mode=SUBST_SIG, conv=s, gvars=gvars) + assert node == [[n1]], node + + # Test supplying an overriding gvars dictionary. + env = DummyEnv({'XXX' : 'xxx'}) + result = scons_subst_list('$XXX', env, gvars=env.Dictionary()) + assert result == [['xxx']], result + result = scons_subst_list('$XXX', env, gvars={'XXX' : 'yyy'}) + assert result == [['yyy']], result + + def test_subst_once(self): + """Testing the scons_subst_once() method""" + + loc = { + 'CCFLAGS' : '-DFOO', + 'ONE' : 1, + 'RECURSE' : 'r $RECURSE r', + 'LIST' : ['a', 'b', 'c'], + } + + env = DummyEnv(loc) + + cases = [ + '$CCFLAGS -DBAR', + 'OTHER_KEY', + '$CCFLAGS -DBAR', + + '$CCFLAGS -DBAR', + 'CCFLAGS', + '-DFOO -DBAR', + + 'x $ONE y', + 'ONE', + 'x 1 y', + + 'x $RECURSE y', + 'RECURSE', + 'x r $RECURSE r y', + + '$LIST', + 'LIST', + 'a b c', + + ['$LIST'], + 'LIST', + ['a', 'b', 'c'], + + ['x', '$LIST', 'y'], + 'LIST', + ['x', 'a', 'b', 'c', 'y'], + + ['x', 'x $LIST y', 'y'], + 'LIST', + ['x', 'x a b c y', 'y'], + + ['x', 'x $CCFLAGS y', 'y'], + 'LIST', + ['x', 'x $CCFLAGS y', 'y'], + + ['x', 'x $RECURSE y', 'y'], + 'LIST', + ['x', 'x $RECURSE y', 'y'], + ] + + failed = 0 + while cases: + input, key, expect = cases[:3] + result = scons_subst_once(input, env, key) + if result != expect: + if failed == 0: print + print " input %s (%s) => %s did not match %s" % (repr(input), repr(key), repr(result), repr(expect)) + failed = failed + 1 + del cases[:3] + assert failed == 0, "%d subst() cases failed" % failed + + def test_quote_spaces(self): + """Testing the quote_spaces() method...""" + q = quote_spaces('x') + assert q == 'x', q + + q = quote_spaces('x x') + assert q == '"x x"', q + + q = quote_spaces('x\tx') + assert q == '"x\tx"', q + + class Node: + def __init__(self, name, children=[]): + self.children = children + self.name = name + def __str__(self): + return self.name + def exists(self): + return 1 + def rexists(self): + return 1 + def has_builder(self): + return 1 + def has_explicit_builder(self): + return 1 + def side_effect(self): + return 1 + def precious(self): + return 1 + def always_build(self): + return 1 + def current(self): + return 1 + + def test_Literal(self): + """Test the Literal() function.""" + input_list = [ '$FOO', Literal('$BAR') ] + gvars = { 'FOO' : 'BAZ', 'BAR' : 'BLAT' } + + def escape_func(cmd): + return '**' + cmd + '**' + + cmd_list = scons_subst_list(input_list, None, gvars=gvars) + cmd_list = escape_list(cmd_list[0], escape_func) + assert cmd_list == ['BAZ', '**$BAR**'], cmd_list + + def test_SpecialAttrWrapper(self): + """Test the SpecialAttrWrapper() function.""" + input_list = [ '$FOO', SpecialAttrWrapper('$BAR', 'BLEH') ] + gvars = { 'FOO' : 'BAZ', 'BAR' : 'BLAT' } + + def escape_func(cmd): + return '**' + cmd + '**' + + cmd_list = scons_subst_list(input_list, None, gvars=gvars) + cmd_list = escape_list(cmd_list[0], escape_func) + assert cmd_list == ['BAZ', '**$BAR**'], cmd_list + + cmd_list = scons_subst_list(input_list, None, mode=SUBST_SIG, gvars=gvars) + cmd_list = escape_list(cmd_list[0], escape_func) + assert cmd_list == ['BAZ', '**BLEH**'], cmd_list + + def test_subst_dict(self): + """Test substituting dictionary values in an Action + """ + t = DummyNode('t') + s = DummyNode('s') + d = subst_dict(target=t, source=s) + assert str(d['TARGETS'][0]) == 't', d['TARGETS'] + assert str(d['TARGET']) == 't', d['TARGET'] + assert str(d['SOURCES'][0]) == 's', d['SOURCES'] + assert str(d['SOURCE']) == 's', d['SOURCE'] + + t1 = DummyNode('t1') + t2 = DummyNode('t2') + s1 = DummyNode('s1') + s2 = DummyNode('s2') + d = subst_dict(target=[t1, t2], source=[s1, s2]) + TARGETS = map(lambda x: str(x), d['TARGETS']) + TARGETS.sort() + assert TARGETS == ['t1', 't2'], d['TARGETS'] + assert str(d['TARGET']) == 't1', d['TARGET'] + SOURCES = map(lambda x: str(x), d['SOURCES']) + SOURCES.sort() + assert SOURCES == ['s1', 's2'], d['SOURCES'] + assert str(d['SOURCE']) == 's1', d['SOURCE'] + + class V: + # Fake Value node with no rfile() method. + def __init__(self, name): + self.name = name + def __str__(self): + return 'v-'+self.name + def get_subst_proxy(self): + return self + + class N(V): + def rfile(self): + return self.__class__('rstr-' + self.name) + + t3 = N('t3') + t4 = DummyNode('t4') + t5 = V('t5') + s3 = DummyNode('s3') + s4 = N('s4') + s5 = V('s5') + d = subst_dict(target=[t3, t4, t5], source=[s3, s4, s5]) + TARGETS = map(lambda x: str(x), d['TARGETS']) + TARGETS.sort() + assert TARGETS == ['t4', 'v-t3', 'v-t5'], TARGETS + SOURCES = map(lambda x: str(x), d['SOURCES']) + SOURCES.sort() + assert SOURCES == ['s3', 'v-rstr-s4', 'v-s5'], SOURCES + +if __name__ == "__main__": + suite = unittest.makeSuite(SubstTestCase, 'test_') + if not unittest.TextTestRunner().run(suite).wasSuccessful(): + sys.exit(1) diff --git a/src/engine/SCons/Util.py b/src/engine/SCons/Util.py index 17316550..90e5393d 100644 --- a/src/engine/SCons/Util.py +++ b/src/engine/SCons/Util.py @@ -29,20 +29,18 @@ Various utility functions go here. __revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" - +import __builtin__ import copy import os import os.path import re -import stat import string import sys +import stat import types -from UserDict import UserDict -import UserList - -import SCons.Errors +from UserDict import UserDict +from UserList import UserList try: from UserString import UserString @@ -96,7 +94,6 @@ except ImportError: __rmul__ = __mul__ # -import __builtin__ try: __builtin__.zip except AttributeError: @@ -187,61 +184,7 @@ def to_String_for_signature(obj): else: return f() -# Indexed by the SUBST_* constants below. -_strconv = [to_String, to_String, to_String_for_signature] - -class Literal: - """A wrapper for a string. If you use this object wrapped - around a string, then it will be interpreted as literal. - When passed to the command interpreter, all special - characters will be escaped.""" - def __init__(self, lstr): - self.lstr = lstr - - def __str__(self): - return self.lstr - - def escape(self, escape_func): - return escape_func(self.lstr) - - def for_signature(self): - return self.lstr - - def is_literal(self): - return 1 - -class SpecialAttrWrapper: - """This is a wrapper for what we call a 'Node special attribute.' - This is any of the attributes of a Node that we can reference from - Environment variable substitution, such as $TARGET.abspath or - $SOURCES[1].filebase. We implement the same methods as Literal - so we can handle special characters, plus a for_signature method, - such that we can return some canonical string during signature - calculation to avoid unnecessary rebuilds.""" - - def __init__(self, lstr, for_signature=None): - """The for_signature parameter, if supplied, will be the - canonical string we return from for_signature(). Else - we will simply return lstr.""" - self.lstr = lstr - if for_signature: - self.forsig = for_signature - else: - self.forsig = lstr - - def __str__(self): - return self.lstr - - def escape(self, escape_func): - return escape_func(self.lstr) - - def for_signature(self): - return self.forsig - - def is_literal(self): - return 1 - -class CallableComposite(UserList.UserList): +class CallableComposite(UserList): """A simple composite callable class that, when called, will invoke all of its contained callables with the same arguments.""" def __call__(self, *args, **kwargs): @@ -253,7 +196,7 @@ class CallableComposite(UserList.UserList): return self.__class__(retvals) return NodeList(retvals) -class NodeList(UserList.UserList): +class NodeList(UserList): """This class is almost exactly like a regular list of Nodes (actually it can hold any object), with one important difference. If you try to get an attribute from this list, it will return that @@ -311,47 +254,6 @@ def get_environment_var(varstr): else: return None -def quote_spaces(arg): - """Generic function for putting double quotes around any string that - has white space in it.""" - if ' ' in arg or '\t' in arg: - return '"%s"' % arg - else: - return str(arg) - -class CmdStringHolder(UserString): - """This is a special class used to hold strings generated by - scons_subst() and scons_subst_list(). It defines a special method - escape(). When passed a function with an escape algorithm for a - particular platform, it will return the contained string with the - proper escape sequences inserted. - - This should really be a subclass of UserString, but that module - doesn't exist in Python 1.5.2.""" - def __init__(self, cmd, literal=None): - UserString.__init__(self, cmd) - self.literal = literal - - def is_literal(self): - return self.literal - - def escape(self, escape_func, quote_func=quote_spaces): - """Escape the string with the supplied function. The - function is expected to take an arbitrary string, then - return it with all special characters escaped and ready - for passing to the command interpreter. - - After calling this function, the next call to str() will - return the escaped string. - """ - - if self.is_literal(): - return escape_func(self.data) - elif ' ' in self.data or '\t' in self.data: - return quote_func(self.data) - else: - return self.data - class DisplayEngine: def __init__(self): self.__call__ = self.print_it @@ -369,693 +271,6 @@ class DisplayEngine: else: self.__call__ = self.dont_print -def escape_list(list, escape_func): - """Escape a list of arguments by running the specified escape_func - on every object in the list that has an escape() method.""" - def escape(obj, escape_func=escape_func): - try: - e = obj.escape - except AttributeError: - return obj - else: - return e(escape_func) - return map(escape, list) - -class NLWrapper: - """A wrapper class that delays turning a list of sources or targets - into a NodeList until it's needed. The specified function supplied - when the object is initialized is responsible for turning raw nodes - into proxies that implement the special attributes like .abspath, - .source, etc. This way, we avoid creating those proxies just - "in case" someone is going to use $TARGET or the like, and only - go through the trouble if we really have to. - - In practice, this might be a wash performance-wise, but it's a little - cleaner conceptually... - """ - - def __init__(self, list, func): - self.list = list - self.func = func - def _return_nodelist(self): - return self.nodelist - def _gen_nodelist(self): - list = self.list - if list is None: - list = [] - elif not is_List(list): - list = [list] - # The map(self.func) call is what actually turns - # a list into appropriate proxies. - self.nodelist = NodeList(map(self.func, list)) - self._create_nodelist = self._return_nodelist - return self.nodelist - _create_nodelist = _gen_nodelist - - -class Targets_or_Sources(UserList.UserList): - """A class that implements $TARGETS or $SOURCES expansions by in turn - wrapping a NLWrapper. This class handles the different methods used - to access the list, calling the NLWrapper to create proxies on demand. - - Note that we subclass UserList.UserList purely so that the is_List() - function will identify an object of this class as a list during - variable expansion. We're not really using any UserList.UserList - methods in practice. - """ - def __init__(self, nl): - self.nl = nl - def __getattr__(self, attr): - nl = self.nl._create_nodelist() - return getattr(nl, attr) - def __getitem__(self, i): - nl = self.nl._create_nodelist() - return nl[i] - def __getslice__(self, i, j): - nl = self.nl._create_nodelist() - i = max(i, 0); j = max(j, 0) - return nl[i:j] - def __str__(self): - nl = self.nl._create_nodelist() - return str(nl) - def __repr__(self): - nl = self.nl._create_nodelist() - return repr(nl) - -class Target_or_Source: - """A class that implements $TARGET or $SOURCE expansions by in turn - wrapping a NLWrapper. This class handles the different methods used - to access an individual proxy Node, calling the NLWrapper to create - a proxy on demand. - """ - def __init__(self, nl): - self.nl = nl - def __getattr__(self, attr): - nl = self.nl._create_nodelist() - try: - nl0 = nl[0] - except IndexError: - # If there is nothing in the list, then we have no attributes to - # pass through, so raise AttributeError for everything. - raise AttributeError, "NodeList has no attribute: %s" % attr - return getattr(nl0, attr) - def __str__(self): - nl = self.nl._create_nodelist() - if nl: - return str(nl[0]) - return '' - def __repr__(self): - nl = self.nl._create_nodelist() - if nl: - return repr(nl[0]) - return '' - -def subst_dict(target, source): - """Create a dictionary for substitution of special - construction variables. - - This translates the following special arguments: - - target - the target (object or array of objects), - used to generate the TARGET and TARGETS - construction variables - - source - the source (object or array of objects), - used to generate the SOURCES and SOURCE - construction variables - """ - dict = {} - - if target: - tnl = NLWrapper(target, lambda x: x.get_subst_proxy()) - dict['TARGETS'] = Targets_or_Sources(tnl) - dict['TARGET'] = Target_or_Source(tnl) - else: - dict['TARGETS'] = None - dict['TARGET'] = None - - if source: - def get_src_subst_proxy(node): - try: - rfile = node.rfile - except AttributeError: - pass - else: - node = rfile() - return node.get_subst_proxy() - snl = NLWrapper(source, get_src_subst_proxy) - dict['SOURCES'] = Targets_or_Sources(snl) - dict['SOURCE'] = Target_or_Source(snl) - else: - dict['SOURCES'] = None - dict['SOURCE'] = None - - return dict - -# Constants for the "mode" parameter to scons_subst_list() and -# scons_subst(). SUBST_RAW gives the raw command line. SUBST_CMD -# gives a command line suitable for passing to a shell. SUBST_SIG -# gives a command line appropriate for calculating the signature -# of a command line...if this changes, we should rebuild. -SUBST_CMD = 0 -SUBST_RAW = 1 -SUBST_SIG = 2 - -_rm = re.compile(r'\$[()]') -_remove = re.compile(r'\$\([^\$]*(\$[^\)][^\$]*)*\$\)') - -# Indexed by the SUBST_* constants above. -_regex_remove = [ _rm, None, _remove ] - -# Regular expressions for splitting strings and handling substitutions, -# for use by the scons_subst() and scons_subst_list() functions: -# -# The first expression compiled matches all of the $-introduced tokens -# that we need to process in some way, and is used for substitutions. -# The expressions it matches are: -# -# "$$" -# "$(" -# "$)" -# "$variable" [must begin with alphabetic or underscore] -# "${any stuff}" -# -# The second expression compiled is used for splitting strings into tokens -# to be processed, and it matches all of the tokens listed above, plus -# the following that affect how arguments do or don't get joined together: -# -# " " [white space] -# "non-white-space" [without any dollar signs] -# "$" [single dollar sign] -# -_dollar_exps_str = r'\$[\$\(\)]|\$[_a-zA-Z][\.\w]*|\${[^}]*}' -_dollar_exps = re.compile(r'(%s)' % _dollar_exps_str) -_separate_args = re.compile(r'(%s|\s+|[^\s\$]+|\$)' % _dollar_exps_str) - -# This regular expression is used to replace strings of multiple white -# space characters in the string result from the scons_subst() function. -_space_sep = re.compile(r'[\t ]+(?![^{]*})') - -def scons_subst(strSubst, env, mode=SUBST_RAW, target=None, source=None, gvars={}, lvars={}, conv=None): - """Expand a string containing construction variable substitutions. - - This is the work-horse function for substitutions in file names - and the like. The companion scons_subst_list() function (below) - handles separating command lines into lists of arguments, so see - that function if that's what you're looking for. - """ - if type(strSubst) == types.StringType and string.find(strSubst, '$') < 0: - return strSubst - - class StringSubber: - """A class to construct the results of a scons_subst() call. - - This binds a specific construction environment, mode, target and - source with two methods (substitute() and expand()) that handle - the expansion. - """ - def __init__(self, env, mode, target, source, conv, gvars): - self.env = env - self.mode = mode - self.target = target - self.source = source - self.conv = conv - self.gvars = gvars - - def expand(self, s, lvars): - """Expand a single "token" as necessary, returning an - appropriate string containing the expansion. - - This handles expanding different types of things (strings, - lists, callables) appropriately. It calls the wrapper - substitute() method to re-expand things as necessary, so that - the results of expansions of side-by-side strings still get - re-evaluated separately, not smushed together. - """ - if is_String(s): - try: - s0, s1 = s[:2] - except (IndexError, ValueError): - return s - if s0 != '$': - return s - if s1 == '$': - return '$' - elif s1 in '()': - return s - else: - key = s[1:] - if key[0] == '{' or string.find(key, '.') >= 0: - if key[0] == '{': - key = key[1:-1] - try: - s = eval(key, self.gvars, lvars) - except AttributeError, e: - raise SCons.Errors.UserError, \ - "Error trying to evaluate `%s': %s" % (s, e) - except (IndexError, NameError, TypeError): - return '' - except SyntaxError,e: - if self.target: - raise SCons.Errors.BuildError, (self.target[0], "Syntax error `%s' trying to evaluate `%s'" % (e,s)) - else: - raise SCons.Errors.UserError, "Syntax error `%s' trying to evaluate `%s'" % (e,s) - else: - if lvars.has_key(key): - s = lvars[key] - elif self.gvars.has_key(key): - s = self.gvars[key] - else: - return '' - - # Before re-expanding the result, handle - # recursive expansion by copying the local - # variable dictionary and overwriting a null - # string for the value of the variable name - # we just expanded. - # - # This could potentially be optimized by only - # copying lvars when s contains more expansions, - # but lvars is usually supposed to be pretty - # small, and deeply nested variable expansions - # are probably more the exception than the norm, - # so it should be tolerable for now. - lv = lvars.copy() - var = string.split(key, '.')[0] - lv[var] = '' - return self.substitute(s, lv) - elif is_List(s): - def func(l, conv=self.conv, substitute=self.substitute, lvars=lvars): - return conv(substitute(l, lvars)) - r = map(func, s) - return string.join(r) - elif callable(s): - try: - s = s(target=self.target, - source=self.source, - env=self.env, - for_signature=(self.mode != SUBST_CMD)) - except TypeError: - # This probably indicates that it's a callable - # object that doesn't match our calling arguments - # (like an Action). - s = str(s) - return self.substitute(s, lvars) - elif s is None: - return '' - else: - return s - - def substitute(self, args, lvars): - """Substitute expansions in an argument or list of arguments. - - This serves as a wrapper for splitting up a string into - separate tokens. - """ - if is_String(args) and not isinstance(args, CmdStringHolder): - try: - def sub_match(match, conv=self.conv, expand=self.expand, lvars=lvars): - return conv(expand(match.group(1), lvars)) - result = _dollar_exps.sub(sub_match, args) - except TypeError: - # If the internal conversion routine doesn't return - # strings (it could be overridden to return Nodes, for - # example), then the 1.5.2 re module will throw this - # exception. Back off to a slower, general-purpose - # algorithm that works for all data types. - args = _separate_args.findall(args) - result = [] - for a in args: - result.append(self.conv(self.expand(a, lvars))) - try: - result = string.join(result, '') - except TypeError: - if len(result) == 1: - result = result[0] - return result - else: - return self.expand(args, lvars) - - if conv is None: - conv = _strconv[mode] - - # Doing this every time is a bit of a waste, since the Executor - # has typically already populated the OverrideEnvironment with - # $TARGET/$SOURCE variables. We're keeping this (for now), though, - # because it supports existing behavior that allows us to call - # an Action directly with an arbitrary target+source pair, which - # we use in Tool/tex.py to handle calling $BIBTEX when necessary. - # If we dropped that behavior (or found another way to cover it), - # we could get rid of this call completely and just rely on the - # Executor setting the variables. - d = subst_dict(target, source) - if d: - lvars = lvars.copy() - lvars.update(d) - - # We're (most likely) going to eval() things. If Python doesn't - # find a __builtin__ value in the global dictionary used for eval(), - # it copies the current __builtin__ values for you. Avoid this by - # setting it explicitly and then deleting, so we don't pollute the - # construction environment Dictionary(ies) that are typically used - # for expansion. - gvars['__builtin__'] = __builtin__ - - ss = StringSubber(env, mode, target, source, conv, gvars) - result = ss.substitute(strSubst, lvars) - - try: - del gvars['__builtin__'] - except KeyError: - pass - - if is_String(result): - # Remove $(-$) pairs and any stuff in between, - # if that's appropriate. - remove = _regex_remove[mode] - if remove: - result = remove.sub('', result) - if mode != SUBST_RAW: - # Compress strings of white space characters into - # a single space. - result = string.strip(_space_sep.sub(' ', result)) - - return result - -#Subst_List_Strings = {} - -def scons_subst_list(strSubst, env, mode=SUBST_RAW, target=None, source=None, gvars={}, lvars={}, conv=None): - """Substitute construction variables in a string (or list or other - object) and separate the arguments into a command list. - - The companion scons_subst() function (above) handles basic - substitutions within strings, so see that function instead - if that's what you're looking for. - """ -# try: -# Subst_List_Strings[strSubst] = Subst_List_Strings[strSubst] + 1 -# except KeyError: -# Subst_List_Strings[strSubst] = 1 -# import SCons.Debug -# SCons.Debug.caller(1) - class ListSubber(UserList.UserList): - """A class to construct the results of a scons_subst_list() call. - - Like StringSubber, this class binds a specific construction - environment, mode, target and source with two methods - (substitute() and expand()) that handle the expansion. - - In addition, however, this class is used to track the state of - the result(s) we're gathering so we can do the appropriate thing - whenever we have to append another word to the result--start a new - line, start a new word, append to the current word, etc. We do - this by setting the "append" attribute to the right method so - that our wrapper methods only need ever call ListSubber.append(), - and the rest of the object takes care of doing the right thing - internally. - """ - def __init__(self, env, mode, target, source, conv, gvars): - UserList.UserList.__init__(self, []) - self.env = env - self.mode = mode - self.target = target - self.source = source - self.conv = conv - self.gvars = gvars - - if self.mode == SUBST_RAW: - self.add_strip = lambda x, s=self: s.append(x) - else: - self.add_strip = lambda x, s=self: None - self.in_strip = None - self.next_line() - - def expand(self, s, lvars, within_list): - """Expand a single "token" as necessary, appending the - expansion to the current result. - - This handles expanding different types of things (strings, - lists, callables) appropriately. It calls the wrapper - substitute() method to re-expand things as necessary, so that - the results of expansions of side-by-side strings still get - re-evaluated separately, not smushed together. - """ - - if is_String(s): - try: - s0, s1 = s[:2] - except (IndexError, ValueError): - self.append(s) - return - if s0 != '$': - self.append(s) - return - if s1 == '$': - self.append('$') - elif s1 == '(': - self.open_strip('$(') - elif s1 == ')': - self.close_strip('$)') - else: - key = s[1:] - if key[0] == '{' or string.find(key, '.') >= 0: - if key[0] == '{': - key = key[1:-1] - try: - s = eval(key, self.gvars, lvars) - except AttributeError, e: - raise SCons.Errors.UserError, \ - "Error trying to evaluate `%s': %s" % (s, e) - except (IndexError, NameError, TypeError): - return - except SyntaxError,e: - if self.target: - raise SCons.Errors.BuildError, (self.target[0], "Syntax error `%s' trying to evaluate `%s'" % (e,s)) - else: - raise SCons.Errors.UserError, "Syntax error `%s' trying to evaluate `%s'" % (e,s) - else: - if lvars.has_key(key): - s = lvars[key] - elif self.gvars.has_key(key): - s = self.gvars[key] - else: - return - - # Before re-expanding the result, handle - # recursive expansion by copying the local - # variable dictionary and overwriting a null - # string for the value of the variable name - # we just expanded. - lv = lvars.copy() - var = string.split(key, '.')[0] - lv[var] = '' - self.substitute(s, lv, 0) - self.this_word() - elif is_List(s): - for a in s: - self.substitute(a, lvars, 1) - self.next_word() - elif callable(s): - try: - s = s(target=self.target, - source=self.source, - env=self.env, - for_signature=(self.mode != SUBST_CMD)) - except TypeError: - # This probably indicates that it's a callable - # object that doesn't match our calling arguments - # (like an Action). - s = str(s) - self.substitute(s, lvars, within_list) - elif s is None: - self.this_word() - else: - self.append(s) - - def substitute(self, args, lvars, within_list): - """Substitute expansions in an argument or list of arguments. - - This serves as a wrapper for splitting up a string into - separate tokens. - """ - - if is_String(args) and not isinstance(args, CmdStringHolder): - args = _separate_args.findall(args) - for a in args: - if a[0] in ' \t\n\r\f\v': - if '\n' in a: - self.next_line() - elif within_list: - self.append(a) - else: - self.next_word() - else: - self.expand(a, lvars, within_list) - else: - self.expand(args, lvars, within_list) - - def next_line(self): - """Arrange for the next word to start a new line. This - is like starting a new word, except that we have to append - another line to the result.""" - UserList.UserList.append(self, []) - self.next_word() - - def this_word(self): - """Arrange for the next word to append to the end of the - current last word in the result.""" - self.append = self.add_to_current_word - - def next_word(self): - """Arrange for the next word to start a new word.""" - self.append = self.add_new_word - - def add_to_current_word(self, x): - """Append the string x to the end of the current last word - in the result. If that is not possible, then just add - it as a new word. Make sure the entire concatenated string - inherits the object attributes of x (in particular, the - escape function) by wrapping it as CmdStringHolder.""" - - if not self.in_strip or self.mode != SUBST_SIG: - try: - current_word = self[-1][-1] - except IndexError: - self.add_new_word(x) - else: - # All right, this is a hack and it should probably - # be refactored out of existence in the future. - # The issue is that we want to smoosh words together - # and make one file name that gets escaped if - # we're expanding something like foo$EXTENSION, - # but we don't want to smoosh them together if - # it's something like >$TARGET, because then we'll - # treat the '>' like it's part of the file name. - # So for now, just hard-code looking for the special - # command-line redirection characters... - try: - last_char = str(current_word)[-1] - except IndexError: - last_char = '\0' - if last_char in '<>|': - self.add_new_word(x) - else: - y = current_word + x - literal1 = self.literal(self[-1][-1]) - literal2 = self.literal(x) - y = self.conv(y) - if is_String(y): - y = CmdStringHolder(y, literal1 or literal2) - self[-1][-1] = y - - def add_new_word(self, x): - if not self.in_strip or self.mode != SUBST_SIG: - literal = self.literal(x) - x = self.conv(x) - if is_String(x): - x = CmdStringHolder(x, literal) - self[-1].append(x) - self.append = self.add_to_current_word - - def literal(self, x): - try: - l = x.is_literal - except AttributeError: - return None - else: - return l() - - def open_strip(self, x): - """Handle the "open strip" $( token.""" - self.add_strip(x) - self.in_strip = 1 - - def close_strip(self, x): - """Handle the "close strip" $) token.""" - self.add_strip(x) - self.in_strip = None - - if conv is None: - conv = _strconv[mode] - - # Doing this every time is a bit of a waste, since the Executor - # has typically already populated the OverrideEnvironment with - # $TARGET/$SOURCE variables. We're keeping this (for now), though, - # because it supports existing behavior that allows us to call - # an Action directly with an arbitrary target+source pair, which - # we use in Tool/tex.py to handle calling $BIBTEX when necessary. - # If we dropped that behavior (or found another way to cover it), - # we could get rid of this call completely and just rely on the - # Executor setting the variables. - d = subst_dict(target, source) - if d: - lvars = lvars.copy() - lvars.update(d) - - # We're (most likely) going to eval() things. If Python doesn't - # find a __builtin__ value in the global dictionary used for eval(), - # it copies the current __builtin__ values for you. Avoid this by - # setting it explicitly and then deleting, so we don't pollute the - # construction environment Dictionary(ies) that are typically used - # for expansion. - gvars['__builtins__'] = __builtins__ - - ls = ListSubber(env, mode, target, source, conv, gvars) - ls.substitute(strSubst, lvars, 0) - - try: - del gvars['__builtins__'] - except KeyError: - pass - - return ls.data - -def scons_subst_once(strSubst, env, key): - """Perform single (non-recursive) substitution of a single - construction variable keyword. - - This is used when setting a variable when copying or overriding values - in an Environment. We want to capture (expand) the old value before - we override it, so people can do things like: - - env2 = env.Copy(CCFLAGS = '$CCFLAGS -g') - - We do this with some straightforward, brute-force code here... - """ - if type(strSubst) == types.StringType and string.find(strSubst, '$') < 0: - return strSubst - - matchlist = ['$' + key, '${' + key + '}'] - val = env.get(key, '') - def sub_match(match, val=val, matchlist=matchlist): - a = match.group(1) - if a in matchlist: - a = val - if is_List(a): - return string.join(map(str, a)) - else: - return str(a) - - if is_List(strSubst): - result = [] - for arg in strSubst: - if is_String(arg): - if arg in matchlist: - arg = val - if is_List(arg): - result.extend(arg) - else: - result.append(arg) - else: - result.append(_dollar_exps.sub(sub_match, arg)) - else: - result.append(arg) - return result - elif is_String(strSubst): - return _dollar_exps.sub(sub_match, strSubst) - else: - return strSubst - def render_tree(root, child_func, prune=0, margin=[0], visited={}): """ Render a tree of nodes into an ASCII tree view. @@ -1167,7 +382,7 @@ def is_Dict(e): return type(e) is types.DictType or isinstance(e, UserDict) def is_List(e): - return type(e) is types.ListType or isinstance(e, UserList.UserList) + return type(e) is types.ListType or isinstance(e, UserList) if hasattr(types, 'UnicodeType'): def is_String(e): @@ -1500,7 +715,7 @@ def Split(arg): else: return [arg] -class CLVar(UserList.UserList): +class CLVar(UserList): """A class for command-line construction variables. This is a list that uses Split() to split an initial string along @@ -1511,7 +726,7 @@ class CLVar(UserList.UserList): command-line construction variable. """ def __init__(self, seq = []): - UserList.UserList.__init__(self, Split(seq)) + UserList.__init__(self, Split(seq)) def __coerce__(self, other): return (self, CLVar(other)) def __str__(self): diff --git a/src/engine/SCons/UtilTests.py b/src/engine/SCons/UtilTests.py index b20ff624..724c51db 100644 --- a/src/engine/SCons/UtilTests.py +++ b/src/engine/SCons/UtilTests.py @@ -30,13 +30,15 @@ import StringIO import sys import types import unittest + from UserDict import UserDict -from SCons.Util import * import TestCmd import SCons.Errors +from SCons.Util import * + class OutBuffer: def __init__(self): self.buffer = "" @@ -44,978 +46,12 @@ class OutBuffer: def write(self, str): self.buffer = self.buffer + str -class DummyNode: - """Simple node work-alike.""" - def __init__(self, name): - self.name = os.path.normpath(name) - def __str__(self): - return self.name - def is_literal(self): - return 1 - def rfile(self): - return self - def get_subst_proxy(self): - return self - -class DummyEnv: - def __init__(self, dict={}): - self.dict = dict - - def Dictionary(self, key = None): - if not key: - return self.dict - return self.dict[key] - - def __getitem__(self, key): - return self.dict[key] - - def get(self, key, default): - return self.dict.get(key, default) - - def sig_dict(self): - dict = self.dict.copy() - dict["TARGETS"] = 'tsig' - dict["SOURCES"] = 'ssig' - return dict - -def cs(target=None, source=None, env=None, for_signature=None): - return 'cs' - -def cl(target=None, source=None, env=None, for_signature=None): - return ['cl'] - -def CmdGen1(target, source, env, for_signature): - # Nifty trick...since Environment references are interpolated, - # instantiate an instance of a callable class with this one, - # which will then get evaluated. - assert str(target) == 't', target - assert str(source) == 's', source - return "${CMDGEN2('foo', %d)}" % for_signature - -class CmdGen2: - def __init__(self, mystr, forsig): - self.mystr = mystr - self.expect_for_signature = forsig - - def __call__(self, target, source, env, for_signature): - assert str(target) == 't', target - assert str(source) == 's', source - assert for_signature == self.expect_for_signature, for_signature - return [ self.mystr, env.Dictionary('BAR') ] - -if os.sep == '/': - def cvt(str): - return str -else: - def cvt(str): - return string.replace(str, '/', os.sep) - class UtilTestCase(unittest.TestCase): - def test_subst(self): - """Test the subst() function""" - class MyNode(DummyNode): - """Simple node work-alike with some extra stuff for testing.""" - def __init__(self, name): - DummyNode.__init__(self, name) - class Attribute: - pass - self.attribute = Attribute() - self.attribute.attr1 = 'attr$1-' + os.path.basename(name) - self.attribute.attr2 = 'attr$2-' + os.path.basename(name) - def get_stuff(self, extra): - return self.name + extra - foo = 1 - - class TestLiteral: - def __init__(self, literal): - self.literal = literal - def __str__(self): - return self.literal - def is_literal(self): - return 1 - - class TestCallable: - def __init__(self, value): - self.value = value - def __call__(self): - pass - def __str__(self): - return self.value - - def function_foo(arg): - pass - - target = [ MyNode("./foo/bar.exe"), - MyNode("/bar/baz.obj"), - MyNode("../foo/baz.obj") ] - source = [ MyNode("./foo/blah.cpp"), - MyNode("/bar/ack.cpp"), - MyNode("../foo/ack.c") ] - - loc = { - 'xxx' : None, - 'null' : '', - 'zero' : 0, - 'one' : 1, - 'BAR' : 'baz', - 'ONE' : '$TWO', - 'TWO' : '$THREE', - 'THREE' : 'four', - - 'AAA' : 'a', - 'BBB' : 'b', - 'CCC' : 'c', - - # $XXX$HHH should expand to GGGIII, not BADNEWS. - 'XXX' : '$FFF', - 'FFF' : 'GGG', - 'HHH' : 'III', - 'FFFIII' : 'BADNEWS', - - 'LITERAL' : TestLiteral("$XXX"), - - # Test that we can expand to and return a function. - #'FUNCTION' : function_foo, - - 'CMDGEN1' : CmdGen1, - 'CMDGEN2' : CmdGen2, - - 'NOTHING' : "", - 'NONE' : None, - - # Test various combinations of strings, lists and functions. - 'N' : None, - 'X' : 'x', - 'Y' : '$X', - 'R' : '$R', - 'S' : 'x y', - 'LS' : ['x y'], - 'L' : ['x', 'y'], - 'CS' : cs, - 'CL' : cl, - - # Test function calls within ${}. - 'FUNCCALL' : '${FUNC1("$AAA $FUNC2 $BBB")}', - 'FUNC1' : lambda x: x, - 'FUNC2' : lambda target, source, env, for_signature: ['x$CCC'], - - # Various tests refactored from ActionTests.py. - 'LIST' : [["This", "is", "$(", "$a", "$)", "test"]], - - # Test recursion. - 'RECURSE' : 'foo $RECURSE bar', - 'RRR' : 'foo $SSS bar', - 'SSS' : '$RRR', - - # Test callables that don't match the calling arguments. - 'CALLABLE' : TestCallable('callable-1'), - } - - env = DummyEnv(loc) - - # Basic tests of substitution functionality. - cases = [ - # Basics: strings without expansions are left alone, and - # the simplest possible expansion to a null-string value. - "test", "test", - "$null", "", - - # Test expansion of integer values. - "test $zero", "test 0", - "test $one", "test 1", - - # Test multiple re-expansion of values. - "test $ONE", "test four", - - # Test a whole bunch of $TARGET[S] and $SOURCE[S] expansions. - "test $TARGETS $SOURCES", - "test foo/bar.exe /bar/baz.obj ../foo/baz.obj foo/blah.cpp /bar/ack.cpp ../foo/ack.c", - - "test ${TARGETS[:]} ${SOURCES[0]}", - "test foo/bar.exe /bar/baz.obj ../foo/baz.obj foo/blah.cpp", - - "test ${TARGETS[1:]}v", - "test /bar/baz.obj ../foo/baz.objv", - - "test $TARGET", - "test foo/bar.exe", - - "test $TARGET$FOO[0]", - "test foo/bar.exe[0]", - - "test $TARGETS.foo", - "test 1 1 1", - - "test ${SOURCES[0:2].foo}", - "test 1 1", - - "test $SOURCE.foo", - "test 1", - - "test ${TARGET.get_stuff('blah')}", - "test foo/bar.exeblah", - - "test ${SOURCES.get_stuff('blah')}", - "test foo/blah.cppblah /bar/ack.cppblah ../foo/ack.cblah", - - "test ${SOURCES[0:2].get_stuff('blah')}", - "test foo/blah.cppblah /bar/ack.cppblah", - - "test ${SOURCES[0:2].get_stuff('blah')}", - "test foo/blah.cppblah /bar/ack.cppblah", - - "test ${SOURCES.attribute.attr1}", - "test attr$1-blah.cpp attr$1-ack.cpp attr$1-ack.c", - - "test ${SOURCES.attribute.attr2}", - "test attr$2-blah.cpp attr$2-ack.cpp attr$2-ack.c", - - # Test adjacent expansions. - "foo$BAR", - "foobaz", - - "foo${BAR}", - "foobaz", - - # Test that adjacent expansions don't get re-interpreted - # together. The correct disambiguated expansion should be: - # $XXX$HHH => ${FFF}III => GGGIII - # not: - # $XXX$HHH => ${FFFIII} => BADNEWS - "$XXX$HHH", "GGGIII", - - # Test double-dollar-sign behavior. - "$$FFF$HHH", "$FFFIII", - - # Test that a Literal will stop dollar-sign substitution. - "$XXX $LITERAL $FFF", "GGG $XXX GGG", - - # Test that we don't blow up even if they subscript - # something in ways they "can't." - "${FFF[0]}", "G", - "${FFF[7]}", "", - "${NOTHING[1]}", "", - "${NONE[2]}", "", - - # Test various combinations of strings and lists. - #None, '', - '', '', - 'x', 'x', - 'x y', 'x y', - '$N', '', - '$X', 'x', - '$Y', 'x', - '$R', '', - '$S', 'x y', - '$LS', 'x y', - '$L', 'x y', - '$S z', 'x y z', - '$LS z', 'x y z', - '$L z', 'x y z', - #cs, 'cs', - #cl, 'cl', - '$CS', 'cs', - '$CL', 'cl', - - # Test function calls within ${}. - '$FUNCCALL', 'a xc b', - - # Bug reported by Christoph Wiedemann. - cvt('$xxx/bin'), '/bin', - - # Tests callables that don't match our calling arguments. - '$CALLABLE', 'callable-1', - - # Test handling of quotes. - 'aaa "bbb ccc" ddd', 'aaa "bbb ccc" ddd', - ] - - kwargs = {'target' : target, 'source' : source, - 'gvars' : env.Dictionary()} - - failed = 0 - while cases: - input, expect = cases[:2] - expect = cvt(expect) - result = apply(scons_subst, (input, env), kwargs) - if result != expect: - if failed == 0: print - print " input %s => %s did not match %s" % (repr(input), repr(result), repr(expect)) - failed = failed + 1 - del cases[:2] - assert failed == 0, "%d subst() cases failed" % failed - - # The expansion dictionary no longer comes from the construction - # environment automatically. - s = scons_subst('$AAA', env) - assert s == '', s - - # Tests of the various SUBST_* modes of substitution. - subst_cases = [ - "test $xxx", - "test ", - "test", - "test", - - "test $($xxx$)", - "test $($)", - "test", - "test", - - "test $( $xxx $)", - "test $( $)", - "test", - "test", - - "$AAA ${AAA}A $BBBB $BBB", - "a aA b", - "a aA b", - "a aA b", - - "$RECURSE", - "foo bar", - "foo bar", - "foo bar", - - "$RRR", - "foo bar", - "foo bar", - "foo bar", - - # Verify what happens with no target or source nodes. - "$TARGET $SOURCES", - " ", - "", - "", - - "$TARGETS $SOURCE", - " ", - "", - "", - - # Various tests refactored from ActionTests.py. - "${LIST}", - "This is $( $) test", - "This is test", - "This is test", - - ["|", "$(", "$AAA", "|", "$BBB", "$)", "|", "$CCC", 1], - "| $( a | b $) | c 1", - "| a | b | c 1", - "| | c 1", - ] - - gvars = env.Dictionary() - - failed = 0 - while subst_cases: - input, eraw, ecmd, esig = subst_cases[:4] - result = scons_subst(input, env, mode=SUBST_RAW, gvars=gvars) - if result != eraw: - if failed == 0: print - print " input %s => RAW %s did not match %s" % (repr(input), repr(result), repr(eraw)) - failed = failed + 1 - result = scons_subst(input, env, mode=SUBST_CMD, gvars=gvars) - if result != ecmd: - if failed == 0: print - print " input %s => CMD %s did not match %s" % (repr(input), repr(result), repr(ecmd)) - failed = failed + 1 - result = scons_subst(input, env, mode=SUBST_SIG, gvars=gvars) - if result != esig: - if failed == 0: print - print " input %s => SIG %s did not match %s" % (repr(input), repr(result), repr(esig)) - failed = failed + 1 - del subst_cases[:4] - assert failed == 0, "%d subst() mode cases failed" % failed - - t1 = MyNode('t1') - t2 = MyNode('t2') - s1 = MyNode('s1') - s2 = MyNode('s2') - result = scons_subst("$TARGET $SOURCES", env, - target=[t1, t2], - source=[s1, s2]) - assert result == "t1 s1 s2", result - result = scons_subst("$TARGET $SOURCES", env, - target=[t1, t2], - source=[s1, s2], - gvars={}) - assert result == "t1 s1 s2", result - - result = scons_subst("$TARGET $SOURCES", env, target=[], source=[]) - assert result == " ", result - result = scons_subst("$TARGETS $SOURCE", env, target=[], source=[]) - assert result == " ", result - - # Test interpolating a callable. - newcom = scons_subst("test $CMDGEN1 $SOURCES $TARGETS", - env, target=MyNode('t'), source=MyNode('s'), - gvars=gvars) - assert newcom == "test foo baz s t", newcom - - # Test that we handle attribute errors during expansion as expected. - try: - class Foo: - pass - scons_subst('${foo.bar}', env, gvars={'foo':Foo()}) - except SCons.Errors.UserError, e: - expect = [ - "Error trying to evaluate `${foo.bar}': bar", - "Error trying to evaluate `${foo.bar}': Foo instance has no attribute 'bar'", - ] - assert str(e) in expect, e - else: - raise AssertionError, "did not catch expected UserError" - - # Test that we handle syntax errors during expansion as expected. - try: - scons_subst('$foo.bar.3.0', env) - except SCons.Errors.UserError, e: - expect1 = "Syntax error `invalid syntax' trying to evaluate `$foo.bar.3.0'" - expect2 = "Syntax error `invalid syntax (line 1)' trying to evaluate `$foo.bar.3.0'" - assert str(e) in [expect1, expect2], e - else: - raise AssertionError, "did not catch expected UserError" - - # Test how we handle overriding the internal conversion routines. - def s(obj): - return obj - - n1 = MyNode('n1') - env = DummyEnv({'NODE' : n1}) - gvars = env.Dictionary() - node = scons_subst("$NODE", env, mode=SUBST_RAW, conv=s, gvars=gvars) - assert node is n1, node - node = scons_subst("$NODE", env, mode=SUBST_CMD, conv=s, gvars=gvars) - assert node is n1, node - node = scons_subst("$NODE", env, mode=SUBST_SIG, conv=s, gvars=gvars) - assert node is n1, node - - # Test returning a function. - #env = DummyEnv({'FUNCTION' : foo}) - #gvars = env.Dictionary() - #func = scons_subst("$FUNCTION", env, mode=SUBST_RAW, call=None, gvars=gvars) - #assert func is function_foo, func - #func = scons_subst("$FUNCTION", env, mode=SUBST_CMD, call=None, gvars=gvars) - #assert func is function_foo, func - #func = scons_subst("$FUNCTION", env, mode=SUBST_SIG, call=None, gvars=gvars) - #assert func is function_foo, func - - # Test supplying an overriding gvars dictionary. - env = DummyEnv({'XXX' : 'xxx'}) - result = scons_subst('$XXX', env, gvars=env.Dictionary()) - assert result == 'xxx', result - result = scons_subst('$XXX', env, gvars={'XXX' : 'yyy'}) - assert result == 'yyy', result - - def test_subst_list(self): - """Testing the scons_subst_list() method...""" - class MyNode(DummyNode): - """Simple node work-alike with some extra stuff for testing.""" - def __init__(self, name): - DummyNode.__init__(self, name) - class Attribute: - pass - self.attribute = Attribute() - self.attribute.attr1 = 'attr$1-' + os.path.basename(name) - self.attribute.attr2 = 'attr$2-' + os.path.basename(name) - - class TestCallable: - def __init__(self, value): - self.value = value - def __call__(self): - pass - def __str__(self): - return self.value - - target = [ MyNode("./foo/bar.exe"), - MyNode("/bar/baz with spaces.obj"), - MyNode("../foo/baz.obj") ] - source = [ MyNode("./foo/blah with spaces.cpp"), - MyNode("/bar/ack.cpp"), - MyNode("../foo/ack.c") ] - - def _defines(defs): - l = [] - for d in defs: - if SCons.Util.is_List(d) or type(d) is types.TupleType: - l.append(str(d[0]) + '=' + str(d[1])) - else: - l.append(str(d)) - return l - - loc = { - 'xxx' : None, - 'NEWLINE' : 'before\nafter', - - 'AAA' : 'a', - 'BBB' : 'b', - 'CCC' : 'c', - - 'DO' : DummyNode('do something'), - 'FOO' : DummyNode('foo.in'), - 'BAR' : DummyNode('bar with spaces.out'), - 'CRAZY' : DummyNode('crazy\nfile.in'), - - # $XXX$HHH should expand to GGGIII, not BADNEWS. - 'XXX' : '$FFF', - 'FFF' : 'GGG', - 'HHH' : 'III', - 'FFFIII' : 'BADNEWS', - - 'CMDGEN1' : CmdGen1, - 'CMDGEN2' : CmdGen2, - - 'LITERALS' : [ Literal('foo\nwith\nnewlines'), - Literal('bar\nwith\nnewlines') ], - - # Test various combinations of strings, lists and functions. - 'N' : None, - 'X' : 'x', - 'Y' : '$X', - 'R' : '$R', - 'S' : 'x y', - 'LS' : ['x y'], - 'L' : ['x', 'y'], - 'CS' : cs, - 'CL' : cl, - - # Test function calls within ${}. - 'FUNCCALL' : '${FUNC1("$AAA $FUNC2 $BBB")}', - 'FUNC1' : lambda x: x, - 'FUNC2' : lambda target, source, env, for_signature: ['x$CCC'], - - # Various tests refactored from ActionTests.py. - 'LIST' : [["This", "is", "$(", "$a", "$)", "test"]], - - # Test recursion. - 'RECURSE' : 'foo $RECURSE bar', - 'RRR' : 'foo $SSS bar', - 'SSS' : '$RRR', - - # Test callable objects that don't match our calling arguments. - 'CALLABLE' : TestCallable('callable-2'), - - '_defines' : _defines, - 'DEFS' : [ ('Q1', '"q1"'), ('Q2', '"$AAA"') ], - } - - env = DummyEnv(loc) - - cases = [ - "$TARGETS", - [ - ["foo/bar.exe", "/bar/baz with spaces.obj", "../foo/baz.obj"], - ], - - "$SOURCES $NEWLINE $TARGETS", - [ - ["foo/blah with spaces.cpp", "/bar/ack.cpp", "../foo/ack.c", "before"], - ["after", "foo/bar.exe", "/bar/baz with spaces.obj", "../foo/baz.obj"], - ], - - "$SOURCES$NEWLINE", - [ - ["foo/blah with spaces.cpp", "/bar/ack.cpp", "../foo/ack.cbefore"], - ["after"], - ], - - "foo$FFF", - [ - ["fooGGG"], - ], - - "foo${FFF}", - [ - ["fooGGG"], - ], - - "test ${SOURCES.attribute.attr1}", - [ - ["test", "attr$1-blah with spaces.cpp", "attr$1-ack.cpp", "attr$1-ack.c"], - ], - - "test ${SOURCES.attribute.attr2}", - [ - ["test", "attr$2-blah with spaces.cpp", "attr$2-ack.cpp", "attr$2-ack.c"], - ], - - "$DO --in=$FOO --out=$BAR", - [ - ["do something", "--in=foo.in", "--out=bar with spaces.out"], - ], - - # This test is now fixed, and works like it should. - "$DO --in=$CRAZY --out=$BAR", - [ - ["do something", "--in=crazy\nfile.in", "--out=bar with spaces.out"], - ], - - # Try passing a list to scons_subst_list(). - [ "$SOURCES$NEWLINE", "$TARGETS", "This is a test"], - [ - ["foo/blah with spaces.cpp", "/bar/ack.cpp", "../foo/ack.cbefore"], - ["after", "foo/bar.exe", "/bar/baz with spaces.obj", "../foo/baz.obj", "This is a test"], - ], - - # Test against a former bug in scons_subst_list(). - "$XXX$HHH", - [ - ["GGGIII"], - ], - - # Test double-dollar-sign behavior. - "$$FFF$HHH", - [ - ["$FFFIII"], - ], - - # Test various combinations of strings, lists and functions. - None, [[]], - [None], [[]], - '', [[]], - [''], [[]], - 'x', [['x']], - ['x'], [['x']], - 'x y', [['x', 'y']], - ['x y'], [['x y']], - ['x', 'y'], [['x', 'y']], - '$N', [[]], - ['$N'], [[]], - '$X', [['x']], - ['$X'], [['x']], - '$Y', [['x']], - ['$Y'], [['x']], - #'$R', [[]], - #['$R'], [[]], - '$S', [['x', 'y']], - '$S z', [['x', 'y', 'z']], - ['$S'], [['x', 'y']], - ['$S z'], [['x', 'y z']], # XXX - IS THIS BEST? - ['$S', 'z'], [['x', 'y', 'z']], - '$LS', [['x y']], - '$LS z', [['x y', 'z']], - ['$LS'], [['x y']], - ['$LS z'], [['x y z']], - ['$LS', 'z'], [['x y', 'z']], - '$L', [['x', 'y']], - '$L z', [['x', 'y', 'z']], - ['$L'], [['x', 'y']], - ['$L z'], [['x', 'y z']], # XXX - IS THIS BEST? - ['$L', 'z'], [['x', 'y', 'z']], - cs, [['cs']], - [cs], [['cs']], - cl, [['cl']], - [cl], [['cl']], - '$CS', [['cs']], - ['$CS'], [['cs']], - '$CL', [['cl']], - ['$CL'], [['cl']], - - # Test function calls within ${}. - '$FUNCCALL', [['a', 'xc', 'b']], - - # Test handling of newlines in white space. - 'foo\nbar', [['foo'], ['bar']], - 'foo\n\nbar', [['foo'], ['bar']], - 'foo \n \n bar', [['foo'], ['bar']], - 'foo \nmiddle\n bar', [['foo'], ['middle'], ['bar']], - - # Bug reported by Christoph Wiedemann. - cvt('$xxx/bin'), [['/bin']], - - # Test variables smooshed together with different prefixes. - 'foo$AAA', [['fooa']], - '<$AAA', [['<', 'a']], - '>$AAA', [['>', 'a']], - '|$AAA', [['|', 'a']], - - # Test callables that don't match our calling arguments. - '$CALLABLE', [['callable-2']], - - # Test - - # Test handling of quotes. - # XXX Find a way to handle this in the future. - #'aaa "bbb ccc" ddd', [['aaa', 'bbb ccc', 'ddd']], - - '${_defines(DEFS)}', [['Q1="q1"', 'Q2="a"']], - ] - - gvars = env.Dictionary() - - kwargs = {'target' : target, 'source' : source, 'gvars' : gvars} - - failed = 0 - while cases: - input, expect = cases[:2] - expect = map(lambda l: map(cvt, l), expect) - result = apply(scons_subst_list, (input, env), kwargs) - if result != expect: - if failed == 0: print - print " input %s => %s did not match %s" % (repr(input), result, repr(expect)) - failed = failed + 1 - del cases[:2] - assert failed == 0, "%d subst_list() cases failed" % failed - - # The expansion dictionary no longer comes from the construction - # environment automatically. - s = scons_subst_list('$AAA', env) - assert s == [[]], s - - t1 = MyNode('t1') - t2 = MyNode('t2') - s1 = MyNode('s1') - s2 = MyNode('s2') - result = scons_subst_list("$TARGET $SOURCES", env, - target=[t1, t2], - source=[s1, s2], - gvars=gvars) - assert result == [['t1', 's1', 's2']], result - result = scons_subst_list("$TARGET $SOURCES", env, - target=[t1, t2], - source=[s1, s2], - gvars={}) - assert result == [['t1', 's1', 's2']], result - - # Test interpolating a callable. - _t = DummyNode('t') - _s = DummyNode('s') - cmd_list = scons_subst_list("testing $CMDGEN1 $TARGETS $SOURCES", - env, target=_t, source=_s, - gvars=gvars) - assert cmd_list == [['testing', 'foo', 'bar with spaces.out', 't', 's']], cmd_list - - # Test escape functionality. - def escape_func(foo): - return '**' + foo + '**' - cmd_list = scons_subst_list("abc $LITERALS xyz", env, gvars=gvars) - assert cmd_list == [['abc', - 'foo\nwith\nnewlines', - 'bar\nwith\nnewlines', - 'xyz']], cmd_list - c = cmd_list[0][0].escape(escape_func) - assert c == 'abc', c - c = cmd_list[0][1].escape(escape_func) - assert c == '**foo\nwith\nnewlines**', c - c = cmd_list[0][2].escape(escape_func) - assert c == '**bar\nwith\nnewlines**', c - c = cmd_list[0][3].escape(escape_func) - assert c == 'xyz', c - - cmd_list = scons_subst_list("abc${LITERALS}xyz", env, gvars=gvars) - c = cmd_list[0][0].escape(escape_func) - assert c == '**abcfoo\nwith\nnewlines**', c - c = cmd_list[0][1].escape(escape_func) - assert c == '**bar\nwith\nnewlinesxyz**', c - - # Tests of the various SUBST_* modes of substitution. - subst_list_cases = [ - "test $xxx", - [["test"]], - [["test"]], - [["test"]], - - "test $($xxx$)", - [["test", "$($)"]], - [["test"]], - [["test"]], - - "test $( $xxx $)", - [["test", "$(", "$)"]], - [["test"]], - [["test"]], - - "$AAA ${AAA}A $BBBB $BBB", - [["a", "aA", "b"]], - [["a", "aA", "b"]], - [["a", "aA", "b"]], - - "$RECURSE", - [["foo", "bar"]], - [["foo", "bar"]], - [["foo", "bar"]], - - "$RRR", - [["foo", "bar"]], - [["foo", "bar"]], - [["foo", "bar"]], - - # Verify what happens with no target or source nodes. - "$TARGET $SOURCES", - [[]], - [[]], - [[]], - - "$TARGETS $SOURCE", - [[]], - [[]], - [[]], - - # Various test refactored from ActionTests.py - "${LIST}", - [['This', 'is', '$(', '$)', 'test']], - [['This', 'is', 'test']], - [['This', 'is', 'test']], - - ["|", "$(", "$AAA", "|", "$BBB", "$)", "|", "$CCC", 1], - [["|", "$(", "a", "|", "b", "$)", "|", "c", "1"]], - [["|", "a", "|", "b", "|", "c", "1"]], - [["|", "|", "c", "1"]], - ] - - gvars = env.Dictionary() - - r = scons_subst_list("$TARGET $SOURCES", env, mode=SUBST_RAW, gvars=gvars) - assert r == [[]], r - - failed = 0 - while subst_list_cases: - input, eraw, ecmd, esig = subst_list_cases[:4] - result = scons_subst_list(input, env, mode=SUBST_RAW, gvars=gvars) - if result != eraw: - if failed == 0: print - print " input %s => RAW %s did not match %s" % (repr(input), repr(result), repr(eraw)) - failed = failed + 1 - result = scons_subst_list(input, env, mode=SUBST_CMD, gvars=gvars) - if result != ecmd: - if failed == 0: print - print " input %s => CMD %s did not match %s" % (repr(input), repr(result), repr(ecmd)) - failed = failed + 1 - result = scons_subst_list(input, env, mode=SUBST_SIG, gvars=gvars) - if result != esig: - if failed == 0: print - print " input %s => SIG %s did not match %s" % (repr(input), repr(result), repr(esig)) - failed = failed + 1 - del subst_list_cases[:4] - assert failed == 0, "%d subst() mode cases failed" % failed - - # Test that we handle attribute errors during expansion as expected. - try: - class Foo: - pass - scons_subst_list('${foo.bar}', env, gvars={'foo':Foo()}) - except SCons.Errors.UserError, e: - expect = [ - "Error trying to evaluate `${foo.bar}': bar", - "Error trying to evaluate `${foo.bar}': Foo instance has no attribute 'bar'", - ] - assert str(e) in expect, e - else: - raise AssertionError, "did not catch expected UserError" - - # Test that we handle syntax errors during expansion as expected. - try: - scons_subst_list('$foo.bar.3.0', env) - except SCons.Errors.UserError, e: - expect1 = "Syntax error `invalid syntax' trying to evaluate `$foo.bar.3.0'" - expect2 = "Syntax error `invalid syntax (line 1)' trying to evaluate `$foo.bar.3.0'" - assert str(e) in [expect1, expect2], e - else: - raise AssertionError, "did not catch expected SyntaxError" - - # Test we handle overriding the internal conversion routines. - def s(obj): - return obj - - n1 = MyNode('n1') - env = DummyEnv({'NODE' : n1}) - gvars=env.Dictionary() - node = scons_subst_list("$NODE", env, mode=SUBST_RAW, conv=s, gvars=gvars) - assert node == [[n1]], node - node = scons_subst_list("$NODE", env, mode=SUBST_CMD, conv=s, gvars=gvars) - assert node == [[n1]], node - node = scons_subst_list("$NODE", env, mode=SUBST_SIG, conv=s, gvars=gvars) - assert node == [[n1]], node - - # Test supplying an overriding gvars dictionary. - env = DummyEnv({'XXX' : 'xxx'}) - result = scons_subst_list('$XXX', env, gvars=env.Dictionary()) - assert result == [['xxx']], result - result = scons_subst_list('$XXX', env, gvars={'XXX' : 'yyy'}) - assert result == [['yyy']], result - - def test_subst_once(self): - """Testing the scons_subst_once() method""" - - loc = { - 'CCFLAGS' : '-DFOO', - 'ONE' : 1, - 'RECURSE' : 'r $RECURSE r', - 'LIST' : ['a', 'b', 'c'], - } - - env = DummyEnv(loc) - - cases = [ - '$CCFLAGS -DBAR', - 'OTHER_KEY', - '$CCFLAGS -DBAR', - - '$CCFLAGS -DBAR', - 'CCFLAGS', - '-DFOO -DBAR', - - 'x $ONE y', - 'ONE', - 'x 1 y', - - 'x $RECURSE y', - 'RECURSE', - 'x r $RECURSE r y', - - '$LIST', - 'LIST', - 'a b c', - - ['$LIST'], - 'LIST', - ['a', 'b', 'c'], - - ['x', '$LIST', 'y'], - 'LIST', - ['x', 'a', 'b', 'c', 'y'], - - ['x', 'x $LIST y', 'y'], - 'LIST', - ['x', 'x a b c y', 'y'], - - ['x', 'x $CCFLAGS y', 'y'], - 'LIST', - ['x', 'x $CCFLAGS y', 'y'], - - ['x', 'x $RECURSE y', 'y'], - 'LIST', - ['x', 'x $RECURSE y', 'y'], - ] - - failed = 0 - while cases: - input, key, expect = cases[:3] - result = scons_subst_once(input, env, key) - if result != expect: - if failed == 0: print - print " input %s (%s) => %s did not match %s" % (repr(input), repr(key), repr(result), repr(expect)) - failed = failed + 1 - del cases[:3] - assert failed == 0, "%d subst() cases failed" % failed - def test_splitext(self): assert splitext('foo') == ('foo','') assert splitext('foo.bar') == ('foo','.bar') assert splitext(os.path.join('foo.bar', 'blat')) == (os.path.join('foo.bar', 'blat'),'') - def test_quote_spaces(self): - """Testing the quote_spaces() method...""" - q = quote_spaces('x') - assert q == 'x', q - - q = quote_spaces('x x') - assert q == '"x x"', q - - q = quote_spaces('x\tx') - assert q == '"x\tx"', q - class Node: def __init__(self, name, children=[]): self.children = children @@ -1361,34 +397,6 @@ class UtilTestCase(unittest.TestCase): assert p.baz == 5, p.baz assert p.get() == s, p.get() - def test_Literal(self): - """Test the Literal() function.""" - input_list = [ '$FOO', Literal('$BAR') ] - gvars = { 'FOO' : 'BAZ', 'BAR' : 'BLAT' } - - def escape_func(cmd): - return '**' + cmd + '**' - - cmd_list = scons_subst_list(input_list, None, gvars=gvars) - cmd_list = SCons.Util.escape_list(cmd_list[0], escape_func) - assert cmd_list == ['BAZ', '**$BAR**'], cmd_list - - def test_SpecialAttrWrapper(self): - """Test the SpecialAttrWrapper() function.""" - input_list = [ '$FOO', SpecialAttrWrapper('$BAR', 'BLEH') ] - gvars = { 'FOO' : 'BAZ', 'BAR' : 'BLAT' } - - def escape_func(cmd): - return '**' + cmd + '**' - - cmd_list = scons_subst_list(input_list, None, gvars=gvars) - cmd_list = SCons.Util.escape_list(cmd_list[0], escape_func) - assert cmd_list == ['BAZ', '**$BAR**'], cmd_list - - cmd_list = scons_subst_list(input_list, None, mode=SUBST_SIG, gvars=gvars) - cmd_list = SCons.Util.escape_list(cmd_list[0], escape_func) - assert cmd_list == ['BAZ', '**BLEH**'], cmd_list - def test_display(self): old_stdout = sys.stdout sys.stdout = OutBuffer() @@ -1419,58 +427,6 @@ class UtilTestCase(unittest.TestCase): except OSError: pass - def test_subst_dict(self): - """Test substituting dictionary values in an Action - """ - t = DummyNode('t') - s = DummyNode('s') - d = subst_dict(target=t, source=s) - assert str(d['TARGETS'][0]) == 't', d['TARGETS'] - assert str(d['TARGET']) == 't', d['TARGET'] - assert str(d['SOURCES'][0]) == 's', d['SOURCES'] - assert str(d['SOURCE']) == 's', d['SOURCE'] - - t1 = DummyNode('t1') - t2 = DummyNode('t2') - s1 = DummyNode('s1') - s2 = DummyNode('s2') - d = subst_dict(target=[t1, t2], source=[s1, s2]) - TARGETS = map(lambda x: str(x), d['TARGETS']) - TARGETS.sort() - assert TARGETS == ['t1', 't2'], d['TARGETS'] - assert str(d['TARGET']) == 't1', d['TARGET'] - SOURCES = map(lambda x: str(x), d['SOURCES']) - SOURCES.sort() - assert SOURCES == ['s1', 's2'], d['SOURCES'] - assert str(d['SOURCE']) == 's1', d['SOURCE'] - - class V: - # Fake Value node with no rfile() method. - def __init__(self, name): - self.name = name - def __str__(self): - return 'v-'+self.name - def get_subst_proxy(self): - return self - - class N(V): - def rfile(self): - return self.__class__('rstr-' + self.name) - - t3 = N('t3') - t4 = DummyNode('t4') - t5 = V('t5') - s3 = DummyNode('s3') - s4 = N('s4') - s5 = V('s5') - d = subst_dict(target=[t3, t4, t5], source=[s3, s4, s5]) - TARGETS = map(lambda x: str(x), d['TARGETS']) - TARGETS.sort() - assert TARGETS == ['t4', 'v-t3', 'v-t5'], TARGETS - SOURCES = map(lambda x: str(x), d['SOURCES']) - SOURCES.sort() - assert SOURCES == ['s3', 'v-rstr-s4', 'v-s5'], SOURCES - def test_PrependPath(self): """Test prepending to a path""" p1 = r'C:\dir\num\one;C:\dir\num\two' @@ -1617,25 +573,6 @@ class UtilTestCase(unittest.TestCase): assert r.data == ['a', 'b', ' c', 'd'], r.data assert str(r) == 'a b c d', str(r) - loc = {} - loc['FOO'] = 'foo' - loc['BAR'] = SCons.Util.CLVar('bar') - loc['CALL'] = lambda target, source, env, for_signature: 'call' - env = DummyEnv(loc) - - cmd = SCons.Util.CLVar("test $FOO $BAR $CALL test") - - newcmd = scons_subst(cmd, env, gvars=env.Dictionary()) - assert newcmd == 'test foo bar call test', newcmd - - cmd_list = scons_subst_list(cmd, env, gvars=env.Dictionary()) - assert len(cmd_list) == 1, cmd_list - assert cmd_list[0][0] == "test", cmd_list[0][0] - assert cmd_list[0][1] == "foo", cmd_list[0][1] - assert cmd_list[0][2] == "bar", cmd_list[0][2] - assert cmd_list[0][3] == "call", cmd_list[0][3] - assert cmd_list[0][4] == "test", cmd_list[0][4] - def test_Selector(self): """Test the Selector class""" -- 2.26.2