# MIT License # # Copyright The SCons Foundation # # 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. import SCons.compat import os import unittest from functools import partial import SCons.Errors from SCons.Subst import (Literal, SUBST_CMD, SUBST_RAW, SUBST_SIG, SpecialAttrWrapper, collections, escape_list, quote_spaces, scons_subst, scons_subst_list, scons_subst_once, subst_dict) 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') ] def CallableWithDefault(target, source, env, for_signature, other_value="default"): assert str(target) == 't', target assert str(source) == 's', source return "CallableWithDefault: %s"%other_value PartialCallable = partial(CallableWithDefault, other_value="partial") def CallableWithNoDefault(target, source, env, for_signature, other_value): assert str(target) == 't', target assert str(source) == 's', source return "CallableWithNoDefault: %s"%other_value PartialCallableNoDefault = partial(CallableWithNoDefault, other_value="partialNoDefault") if os.sep == '/': def cvt(str): return str else: def cvt(str): return str.replace('/', os.sep) class SubstTestCase(unittest.TestCase): class MyNode(DummyNode): """Simple node work-alike with some extra stuff for testing.""" def __init__(self, name): super().__init__(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 # only use of this is currently commented out below #def function_foo(arg): # pass 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") ] callable_object_1 = TestCallable('callable-1') callable_object_2 = TestCallable('callable-2') def _defines(defs): l = [] for d in defs: if SCons.Util.is_List(d) or isinstance(d, tuple): l.append(str(d[0]) + '=' + str(d[1])) else: l.append(str(d)) return l loc = { 'xxx' : None, 'NEWLINE' : 'before\nafter', 'null' : '', 'zero' : 0, 'one' : 1, 'BAZ' : 'baz', 'ONE' : '$TWO', 'TWO' : '$THREE', 'THREE' : 'four', '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', 'THING1' : "$(STUFF$)", 'THING2' : "$THING1", 'LITERAL' : TestLiteral("$XXX"), # Test that we can expand to and return a function. #'FUNCTION' : function_foo, 'CMDGEN1' : CmdGen1, 'CMDGEN2' : CmdGen2, 'CallableWithDefault': CallableWithDefault, 'PartialCallable' : PartialCallable, 'PartialCallableNoDefault' : PartialCallableNoDefault, 'LITERALS' : [ Literal('foo\nwith\nnewlines'), Literal('bar\nwith\nnewlines') ], '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'], 'TS' : ('x y',), 'T' : ('x', 'y'), 'CS' : cs, 'CL' : cl, 'US' : collections.UserString('us'), # 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. 'CALLABLE1' : callable_object_1, 'CALLABLE2' : callable_object_2, '_defines' : _defines, 'DEFS' : [ ('Q1', '"q1"'), ('Q2', '"$AAA"') ], } def basic_comparisons(self, function, convert): env = DummyEnv(self.loc) cases = self.basic_cases[:] kwargs = {'target' : self.target, 'source' : self.source, 'gvars' : env.Dictionary()} failed = 0 case_count = 0 while cases: input, expect = cases[:2] expect = convert(expect) try: result = function(input, env, **kwargs) except Exception as e: fmt = " input %s generated %s (%s)" print(fmt % (repr(input), e.__class__.__name__, repr(e))) failed = failed + 1 else: if result != expect: if failed == 0: print() print("[%4d] input %s => \n%s did not match \n%s" % (case_count, repr(input), repr(result), repr(expect))) failed = failed + 1 del cases[:2] case_count += 1 fmt = "%d %s() cases failed" assert failed == 0, fmt % (failed, function.__name__) class scons_subst_TestCase(SubstTestCase): # Basic tests of substitution functionality. basic_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 with spaces.obj ../foo/baz.obj foo/blah with spaces.cpp /bar/ack.cpp ../foo/ack.c", "test ${TARGETS[:]} ${SOURCES[0]}", "test foo/bar.exe /bar/baz with spaces.obj ../foo/baz.obj foo/blah with spaces.cpp", "test ${TARGETS[1:]}v", "test /bar/baz with spaces.obj ../foo/baz.objv", "test $TARGET", "test foo/bar.exe", "test $TARGET$NO_SUCH_VAR[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 with spaces.cppblah /bar/ack.cppblah ../foo/ack.cblah", "test ${SOURCES[0:2].get_stuff('blah')}", "test foo/blah with spaces.cppblah /bar/ack.cppblah", "test ${SOURCES[0:2].get_stuff('blah')}", "test foo/blah with spaces.cppblah /bar/ack.cppblah", "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", # Test adjacent expansions. "foo$BAZ", "foobaz", "foo${BAZ}", "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 double-dollar-sign before open paren. It's not meant # to be signature escaping 'echo $$(pwd) > XYZ', 'echo $(pwd) > XYZ', # 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]}", "", # 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', '$TS', 'x y', '$T', 'x y', '$S z', 'x y z', '$LS z', 'x y z', '$L z', 'x y z', '$TS z', 'x y z', '$T z', 'x y z', #cs, 'cs', #cl, 'cl', '$CS', 'cs', '$CL', 'cl', # Various uses of UserString. collections.UserString('x'), 'x', collections.UserString('$X'), 'x', collections.UserString('$US'), 'us', '$US', 'us', # 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. '$CALLABLE1', 'callable-1', # Test handling of quotes. 'aaa "bbb ccc" ddd', 'aaa "bbb ccc" ddd', ] def test_scons_subst(self): """Test scons_subst(): basic substitution""" return self.basic_comparisons(scons_subst, cvt) subst_cases = [ "test $xxx", "test ", "test", "test", "test $($xxx$)", "test $($)", "test", "test", "test $( $xxx $)", "test $( $)", "test", "test", "test $( $THING2 $)", "test $( $(STUFF$) $)", "test STUFF", "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"], ] def test_subst_env(self): """Test scons_subst(): expansion dictionary""" # The expansion dictionary no longer comes from the construction # environment automatically. env = DummyEnv(self.loc) s = scons_subst('$AAA', env) assert s == '', s def test_subst_SUBST_modes(self): """Test scons_subst(): SUBST_* modes""" env = DummyEnv(self.loc) subst_cases = self.subst_cases[:] 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 def test_subst_target_source(self): """Test scons_subst(): target= and source= arguments""" env = DummyEnv(self.loc) t1 = self.MyNode('t1') t2 = self.MyNode('t2') s1 = self.MyNode('s1') s2 = self.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 def test_subst_callable_expansion(self): """Test scons_subst(): expanding a callable""" env = DummyEnv(self.loc) gvars = env.Dictionary() newcom = scons_subst("test $CMDGEN1 $SOURCES $TARGETS", env, target=self.MyNode('t'), source=self.MyNode('s'), gvars=gvars) assert newcom == "test foo bar with spaces.out s t", newcom def test_subst_callable_with_default_expansion(self): """Test scons_subst(): expanding a callable with a default value arg""" env = DummyEnv(self.loc) gvars = env.Dictionary() newcom = scons_subst("test $CallableWithDefault $SOURCES $TARGETS", env, target=self.MyNode('t'), source=self.MyNode('s'), gvars=gvars) assert newcom == "test CallableWithDefault: default s t", newcom def test_subst_partial_callable_with_default_expansion(self): """Test scons_subst(): expanding a functools.partial callable which sets the default value in the callable""" env = DummyEnv(self.loc) gvars = env.Dictionary() newcom = scons_subst("test $PartialCallable $SOURCES $TARGETS", env, target=self.MyNode('t'), source=self.MyNode('s'), gvars=gvars) assert newcom == "test CallableWithDefault: partial s t", newcom def test_subst_partial_callable_with_no_default_expansion(self): """Test scons_subst(): expanding a functools.partial callable which sets the value for extraneous function argument""" env = DummyEnv(self.loc) gvars = env.Dictionary() newcom = scons_subst("test $PartialCallableNoDefault $SOURCES $TARGETS", env, target=self.MyNode('t'), source=self.MyNode('s'), gvars=gvars) assert newcom == "test CallableWithNoDefault: partialNoDefault s t", newcom def test_subst_attribute_errors(self): """Test scons_subst(): handling attribute errors""" env = DummyEnv(self.loc) try: class Foo: pass scons_subst('${foo.bar}', env, gvars={'foo':Foo()}) except SCons.Errors.UserError as e: expect = [ "AttributeError `bar' trying to evaluate `${foo.bar}'", "AttributeError `Foo instance has no attribute 'bar'' trying to evaluate `${foo.bar}'", "AttributeError `'Foo' instance has no attribute 'bar'' trying to evaluate `${foo.bar}'", "AttributeError `'Foo' object has no attribute 'bar'' trying to evaluate `${foo.bar}'", ] assert str(e) in expect, e else: raise AssertionError("did not catch expected UserError") def test_subst_syntax_errors(self): """Test scons_subst(): handling syntax errors""" env = DummyEnv(self.loc) try: scons_subst('$foo.bar.3.0', env) except SCons.Errors.UserError as e: expect = [ # Python 2.5 to 3.9 "SyntaxError `invalid syntax (, line 1)' trying to evaluate `$foo.bar.3.0'", # Python 3.10 and later "SyntaxError `invalid syntax. Perhaps you forgot a comma? (, line 1)' trying to evaluate `$foo.bar.3.0'", ] assert str(e) in expect, e else: raise AssertionError("did not catch expected UserError") def test_subst_balance_errors(self): """Test scons_subst(): handling syntax errors""" env = DummyEnv(self.loc) try: scons_subst('$(', env, mode=SUBST_SIG) except SCons.Errors.UserError as e: assert str(e) == "Unbalanced $(/$) in: $(", str(e) else: raise AssertionError("did not catch expected UserError") try: scons_subst('$)', env, mode=SUBST_SIG) except SCons.Errors.UserError as e: assert str(e) == "Unbalanced $(/$) in: $)", str(e) else: raise AssertionError("did not catch expected UserError") def test_subst_type_errors(self): """Test scons_subst(): handling type errors""" env = DummyEnv(self.loc) try: scons_subst("${NONE[2]}", env, gvars={'NONE':None}) except SCons.Errors.UserError as e: expect = [ # Python 2.7 and later "TypeError `'NoneType' object is not subscriptable' trying to evaluate `${NONE[2]}'", # Python 2.7 and later under Fedora "TypeError `'NoneType' object has no attribute '__getitem__'' trying to evaluate `${NONE[2]}'", ] assert str(e) in expect, e else: raise AssertionError("did not catch expected UserError") try: def func(a, b, c): pass scons_subst("${func(1)}", env, gvars={'func':func}) except SCons.Errors.UserError as e: expect = [ # Python 3.5 (and 3.x?) "TypeError `func() missing 2 required positional arguments: 'b' and 'c'' trying to evaluate `${func(1)}'", # Python 3.10 "TypeError `scons_subst_TestCase.test_subst_type_errors..func() missing 2 required positional arguments: 'b' and 'c'' trying to evaluate `${func(1)}'", ] assert str(e) in expect, repr(str(e)) else: raise AssertionError("did not catch expected UserError") def test_subst_raw_function(self): """Test scons_subst(): fetch function with SUBST_RAW plus conv""" # Test that the combination of SUBST_RAW plus a pass-through # conversion routine allows us to fetch a function through the # dictionary. CommandAction uses this to allow delayed evaluation # of $SPAWN variables. env = DummyEnv(self.loc) gvars = env.Dictionary() x = lambda x: x r = scons_subst("$CALLABLE1", env, mode=SUBST_RAW, conv=x, gvars=gvars) assert r is self.callable_object_1, repr(r) r = scons_subst("$CALLABLE1", env, mode=SUBST_RAW, gvars=gvars) assert r == 'callable-1', repr(r) # Test how we handle overriding the internal conversion routines. def s(obj): return obj n1 = self.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 def test_subst_overriding_gvars(self): """Test scons_subst(): 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 class CLVar_TestCase(unittest.TestCase): 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] class scons_subst_list_TestCase(SubstTestCase): basic_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']], # Various uses of UserString. collections.UserString('x'), [['x']], [collections.UserString('x')], [['x']], collections.UserString('$X'), [['x']], [collections.UserString('$X')], [['x']], collections.UserString('$US'), [['us']], [collections.UserString('$US')], [['us']], '$US', [['us']], ['$US'], [['us']], # 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. '$CALLABLE2', [['callable-2']], # 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"']], ] def test_scons_subst_list(self): """Test scons_subst_list(): basic substitution""" def convert_lists(expect): return [list(map(cvt, l)) for l in expect] return self.basic_comparisons(scons_subst_list, convert_lists) 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"]], ] def test_subst_env(self): """Test scons_subst_list(): expansion dictionary""" # The expansion dictionary no longer comes from the construction # environment automatically. env = DummyEnv() s = scons_subst_list('$AAA', env) assert s == [[]], s def test_subst_target_source(self): """Test scons_subst_list(): target= and source= arguments""" env = DummyEnv(self.loc) gvars = env.Dictionary() t1 = self.MyNode('t1') t2 = self.MyNode('t2') s1 = self.MyNode('s1') s2 = self.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 def test_subst_escape(self): """Test scons_subst_list(): escape functionality""" env = DummyEnv(self.loc) gvars = env.Dictionary() 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 # We used to treat literals smooshed together like the whole # thing was literal and escape it as a unit. The commented-out # asserts below are in case we ever have to find a way to # resurrect that functionality in some way. 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 assert c == 'abcfoo\nwith\nnewlines', c c = cmd_list[0][1].escape(escape_func) #assert c == '**bar\nwith\nnewlinesxyz**', c assert c == 'bar\nwith\nnewlinesxyz', c _t = DummyNode('t') cmd_list = scons_subst_list('echo "target: $TARGET"', env, target=_t, gvars=gvars) c = cmd_list[0][0].escape(escape_func) assert c == 'echo', c c = cmd_list[0][1].escape(escape_func) assert c == '"target:', c c = cmd_list[0][2].escape(escape_func) assert c == 't"', c def test_subst_SUBST_modes(self): """Test scons_subst_list(): SUBST_* modes""" env = DummyEnv(self.loc) subst_list_cases = self.subst_list_cases[:] 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 def test_subst_attribute_errors(self): """Test scons_subst_list(): handling attribute errors""" env = DummyEnv() try: class Foo: pass scons_subst_list('${foo.bar}', env, gvars={'foo':Foo()}) except SCons.Errors.UserError as e: expect = [ "AttributeError `bar' trying to evaluate `${foo.bar}'", "AttributeError `Foo instance has no attribute 'bar'' trying to evaluate `${foo.bar}'", "AttributeError `'Foo' instance has no attribute 'bar'' trying to evaluate `${foo.bar}'", "AttributeError `'Foo' object has no attribute 'bar'' trying to evaluate `${foo.bar}'", ] assert str(e) in expect, e else: raise AssertionError("did not catch expected UserError") def test_subst_syntax_errors(self): """Test scons_subst_list(): handling syntax errors""" env = DummyEnv() try: scons_subst_list('$foo.bar.3.0', env) except SCons.Errors.UserError as e: expect = [ # Python 2.5 to 3.9 "SyntaxError `invalid syntax (, line 1)' trying to evaluate `$foo.bar.3.0'", # Python 3.10 and later "SyntaxError `invalid syntax. Perhaps you forgot a comma? (, line 1)' trying to evaluate `$foo.bar.3.0'", ] assert str(e) in expect, e else: raise AssertionError("did not catch expected SyntaxError") def test_subst_raw_function(self): """Test scons_subst_list(): fetch function with SUBST_RAW plus conv""" # Test that the combination of SUBST_RAW plus a pass-through # conversion routine allows us to fetch a function through the # dictionary. env = DummyEnv(self.loc) gvars = env.Dictionary() x = lambda x: x r = scons_subst_list("$CALLABLE2", env, mode=SUBST_RAW, conv=x, gvars=gvars) assert r == [[self.callable_object_2]], repr(r) r = scons_subst_list("$CALLABLE2", env, mode=SUBST_RAW, gvars=gvars) assert r == [['callable-2']], repr(r) def test_subst_list_overriding_gvars(self): """Test scons_subst_list(): overriding conv()""" env = DummyEnv() def s(obj): return obj n1 = self.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 def test_subst_list_overriding_gvars2(self): """Test scons_subst_list(): 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 class scons_subst_once_TestCase(unittest.TestCase): loc = { 'CCFLAGS' : '-DFOO', 'ONE' : 1, 'RECURSE' : 'r $RECURSE r', 'LIST' : ['a', 'b', 'c'], } basic_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'], ] def test_subst_once(self): """Test the scons_subst_once() function""" env = DummyEnv(self.loc) cases = self.basic_cases[:] 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 class quote_spaces_TestCase(unittest.TestCase): def test_quote_spaces(self): """Test 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 class LiteralTestCase(unittest.TestCase): 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_LiteralEqualsTest(self): """Test that Literals compare for equality properly""" assert Literal('a literal') == Literal('a literal') assert Literal('a literal') != Literal('b literal') class SpecialAttrWrapperTestCase(unittest.TestCase): 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 class subst_dict_TestCase(unittest.TestCase): 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 = sorted([str(x) for x in d['TARGETS']]) assert TARGETS == ['t1', 't2'], d['TARGETS'] assert str(d['TARGET']) == 't1', d['TARGET'] SOURCES = sorted([str(x) for x in d['SOURCES']]) 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 = sorted([str(x) for x in d['TARGETS']]) assert TARGETS == ['t4', 'v-t3', 'v-t5'], TARGETS SOURCES = sorted([str(x) for x in d['SOURCES']]) assert SOURCES == ['s3', 'v-rstr-s4', 'v-s5'], SOURCES if __name__ == "__main__": unittest.main() # Local Variables: # tab-width:4 # indent-tabs-mode:nil # End: # vim: set expandtab tabstop=4 shiftwidth=4: