logging working in NewParallel, but changed to be default. Need to figure out how...
[scons.git] / SCons / SubstTests.py
blob1a203a0b11d69523ecf3484a408fb964a65c0690
1 # MIT License
3 # Copyright The SCons Foundation
5 # Permission is hereby granted, free of charge, to any person obtaining
6 # a copy of this software and associated documentation files (the
7 # "Software"), to deal in the Software without restriction, including
8 # without limitation the rights to use, copy, modify, merge, publish,
9 # distribute, sublicense, and/or sell copies of the Software, and to
10 # permit persons to whom the Software is furnished to do so, subject to
11 # the following conditions:
13 # The above copyright notice and this permission notice shall be included
14 # in all copies or substantial portions of the Software.
16 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
17 # KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
18 # WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20 # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21 # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22 # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
24 import SCons.compat
26 import os
27 import unittest
28 from functools import partial
31 import SCons.Errors
33 from SCons.Subst import (Literal, SUBST_CMD, SUBST_RAW, SUBST_SIG, SpecialAttrWrapper, collections,
34 escape_list, quote_spaces, scons_subst, scons_subst_list, scons_subst_once,
35 subst_dict)
37 class DummyNode:
38 """Simple node work-alike."""
39 def __init__(self, name):
40 self.name = os.path.normpath(name)
41 def __str__(self):
42 return self.name
43 def is_literal(self):
44 return 1
45 def rfile(self):
46 return self
47 def get_subst_proxy(self):
48 return self
50 class DummyEnv:
51 def __init__(self, dict={}):
52 self.dict = dict
54 def Dictionary(self, key = None):
55 if not key:
56 return self.dict
57 return self.dict[key]
59 def __getitem__(self, key):
60 return self.dict[key]
62 def get(self, key, default):
63 return self.dict.get(key, default)
65 def sig_dict(self):
66 dict = self.dict.copy()
67 dict["TARGETS"] = 'tsig'
68 dict["SOURCES"] = 'ssig'
69 return dict
71 def cs(target=None, source=None, env=None, for_signature=None):
72 return 'cs'
74 def cl(target=None, source=None, env=None, for_signature=None):
75 return ['cl']
77 def CmdGen1(target, source, env, for_signature):
78 # Nifty trick...since Environment references are interpolated,
79 # instantiate an instance of a callable class with this one,
80 # which will then get evaluated.
81 assert str(target) == 't', target
82 assert str(source) == 's', source
83 return "${CMDGEN2('foo', %d)}" % for_signature
85 class CmdGen2:
86 def __init__(self, mystr, forsig):
87 self.mystr = mystr
88 self.expect_for_signature = forsig
90 def __call__(self, target, source, env, for_signature):
91 assert str(target) == 't', target
92 assert str(source) == 's', source
93 assert for_signature == self.expect_for_signature, for_signature
94 return [ self.mystr, env.Dictionary('BAR') ]
97 def CallableWithDefault(target, source, env, for_signature, other_value="default"):
98 assert str(target) == 't', target
99 assert str(source) == 's', source
100 return "CallableWithDefault: %s"%other_value
102 PartialCallable = partial(CallableWithDefault, other_value="partial")
104 def CallableWithNoDefault(target, source, env, for_signature, other_value):
105 assert str(target) == 't', target
106 assert str(source) == 's', source
107 return "CallableWithNoDefault: %s"%other_value
109 PartialCallableNoDefault = partial(CallableWithNoDefault, other_value="partialNoDefault")
113 if os.sep == '/':
114 def cvt(str):
115 return str
116 else:
117 def cvt(str):
118 return str.replace('/', os.sep)
120 class SubstTestCase(unittest.TestCase):
121 class MyNode(DummyNode):
122 """Simple node work-alike with some extra stuff for testing."""
123 def __init__(self, name):
124 super().__init__(name)
125 class Attribute:
126 pass
127 self.attribute = Attribute()
128 self.attribute.attr1 = 'attr$1-' + os.path.basename(name)
129 self.attribute.attr2 = 'attr$2-' + os.path.basename(name)
130 def get_stuff(self, extra):
131 return self.name + extra
132 foo = 1
134 class TestLiteral:
135 def __init__(self, literal):
136 self.literal = literal
137 def __str__(self):
138 return self.literal
139 def is_literal(self):
140 return 1
142 class TestCallable:
143 def __init__(self, value):
144 self.value = value
145 def __call__(self):
146 pass
147 def __str__(self):
148 return self.value
150 # only use of this is currently commented out below
151 #def function_foo(arg):
152 # pass
154 target = [ MyNode("./foo/bar.exe"),
155 MyNode("/bar/baz with spaces.obj"),
156 MyNode("../foo/baz.obj") ]
157 source = [ MyNode("./foo/blah with spaces.cpp"),
158 MyNode("/bar/ack.cpp"),
159 MyNode("../foo/ack.c") ]
161 callable_object_1 = TestCallable('callable-1')
162 callable_object_2 = TestCallable('callable-2')
164 def _defines(defs):
165 l = []
166 for d in defs:
167 if SCons.Util.is_List(d) or isinstance(d, tuple):
168 l.append(str(d[0]) + '=' + str(d[1]))
169 else:
170 l.append(str(d))
171 return l
173 loc = {
174 'xxx' : None,
175 'NEWLINE' : 'before\nafter',
177 'null' : '',
178 'zero' : 0,
179 'one' : 1,
180 'BAZ' : 'baz',
181 'ONE' : '$TWO',
182 'TWO' : '$THREE',
183 'THREE' : 'four',
185 'AAA' : 'a',
186 'BBB' : 'b',
187 'CCC' : 'c',
189 'DO' : DummyNode('do something'),
190 'FOO' : DummyNode('foo.in'),
191 'BAR' : DummyNode('bar with spaces.out'),
192 'CRAZY' : DummyNode('crazy\nfile.in'),
194 # $XXX$HHH should expand to GGGIII, not BADNEWS.
195 'XXX' : '$FFF',
196 'FFF' : 'GGG',
197 'HHH' : 'III',
198 'FFFIII' : 'BADNEWS',
200 'THING1' : "$(STUFF$)",
201 'THING2' : "$THING1",
203 'LITERAL' : TestLiteral("$XXX"),
205 # Test that we can expand to and return a function.
206 #'FUNCTION' : function_foo,
208 'CMDGEN1' : CmdGen1,
209 'CMDGEN2' : CmdGen2,
211 'CallableWithDefault': CallableWithDefault,
212 'PartialCallable' : PartialCallable,
213 'PartialCallableNoDefault' : PartialCallableNoDefault,
215 'LITERALS' : [ Literal('foo\nwith\nnewlines'),
216 Literal('bar\nwith\nnewlines') ],
218 'NOTHING' : "",
219 'NONE' : None,
221 # Test various combinations of strings, lists and functions.
222 'N' : None,
223 'X' : 'x',
224 'Y' : '$X',
225 'R' : '$R',
226 'S' : 'x y',
227 'LS' : ['x y'],
228 'L' : ['x', 'y'],
229 'TS' : ('x y',),
230 'T' : ('x', 'y'),
231 'CS' : cs,
232 'CL' : cl,
233 'US' : collections.UserString('us'),
235 # Test function calls within ${}.
236 'FUNCCALL' : '${FUNC1("$AAA $FUNC2 $BBB")}',
237 'FUNC1' : lambda x: x,
238 'FUNC2' : lambda target, source, env, for_signature: ['x$CCC'],
240 # Various tests refactored from ActionTests.py.
241 'LIST' : [["This", "is", "$(", "$a", "$)", "test"]],
243 # Test recursion.
244 'RECURSE' : 'foo $RECURSE bar',
245 'RRR' : 'foo $SSS bar',
246 'SSS' : '$RRR',
248 # Test callables that don't match the calling arguments.
249 'CALLABLE1' : callable_object_1,
250 'CALLABLE2' : callable_object_2,
252 '_defines' : _defines,
253 'DEFS' : [ ('Q1', '"q1"'), ('Q2', '"$AAA"') ],
256 def basic_comparisons(self, function, convert):
257 env = DummyEnv(self.loc)
258 cases = self.basic_cases[:]
259 kwargs = {'target' : self.target, 'source' : self.source,
260 'gvars' : env.Dictionary()}
262 failed = 0
263 case_count = 0
264 while cases:
265 input, expect = cases[:2]
266 expect = convert(expect)
267 try:
268 result = function(input, env, **kwargs)
269 except Exception as e:
270 fmt = " input %s generated %s (%s)"
271 print(fmt % (repr(input), e.__class__.__name__, repr(e)))
272 failed = failed + 1
273 else:
274 if result != expect:
275 if failed == 0: print()
276 print("[%4d] input %s => \n%s did not match \n%s" % (case_count, repr(input), repr(result), repr(expect)))
277 failed = failed + 1
278 del cases[:2]
279 case_count += 1
280 fmt = "%d %s() cases failed"
281 assert failed == 0, fmt % (failed, function.__name__)
284 class scons_subst_TestCase(SubstTestCase):
286 # Basic tests of substitution functionality.
287 basic_cases = [
288 # Basics: strings without expansions are left alone, and
289 # the simplest possible expansion to a null-string value.
290 "test", "test",
291 "$null", "",
293 # Test expansion of integer values.
294 "test $zero", "test 0",
295 "test $one", "test 1",
297 # Test multiple re-expansion of values.
298 "test $ONE", "test four",
300 # Test a whole bunch of $TARGET[S] and $SOURCE[S] expansions.
301 "test $TARGETS $SOURCES",
302 "test foo/bar.exe /bar/baz with spaces.obj ../foo/baz.obj foo/blah with spaces.cpp /bar/ack.cpp ../foo/ack.c",
304 "test ${TARGETS[:]} ${SOURCES[0]}",
305 "test foo/bar.exe /bar/baz with spaces.obj ../foo/baz.obj foo/blah with spaces.cpp",
307 "test ${TARGETS[1:]}v",
308 "test /bar/baz with spaces.obj ../foo/baz.objv",
310 "test $TARGET",
311 "test foo/bar.exe",
313 "test $TARGET$NO_SUCH_VAR[0]",
314 "test foo/bar.exe[0]",
316 "test $TARGETS.foo",
317 "test 1 1 1",
319 "test ${SOURCES[0:2].foo}",
320 "test 1 1",
322 "test $SOURCE.foo",
323 "test 1",
325 "test ${TARGET.get_stuff('blah')}",
326 "test foo/bar.exeblah",
328 "test ${SOURCES.get_stuff('blah')}",
329 "test foo/blah with spaces.cppblah /bar/ack.cppblah ../foo/ack.cblah",
331 "test ${SOURCES[0:2].get_stuff('blah')}",
332 "test foo/blah with spaces.cppblah /bar/ack.cppblah",
334 "test ${SOURCES[0:2].get_stuff('blah')}",
335 "test foo/blah with spaces.cppblah /bar/ack.cppblah",
337 "test ${SOURCES.attribute.attr1}",
338 "test attr$1-blah with spaces.cpp attr$1-ack.cpp attr$1-ack.c",
340 "test ${SOURCES.attribute.attr2}",
341 "test attr$2-blah with spaces.cpp attr$2-ack.cpp attr$2-ack.c",
343 # Test adjacent expansions.
344 "foo$BAZ",
345 "foobaz",
347 "foo${BAZ}",
348 "foobaz",
350 # Test that adjacent expansions don't get re-interpreted
351 # together. The correct disambiguated expansion should be:
352 # $XXX$HHH => ${FFF}III => GGGIII
353 # not:
354 # $XXX$HHH => ${FFFIII} => BADNEWS
355 "$XXX$HHH", "GGGIII",
357 # Test double-dollar-sign behavior.
358 "$$FFF$HHH", "$FFFIII",
360 # Test double-dollar-sign before open paren. It's not meant
361 # to be signature escaping
362 'echo $$(pwd) > XYZ', 'echo $(pwd) > XYZ',
364 # Test that a Literal will stop dollar-sign substitution.
365 "$XXX $LITERAL $FFF", "GGG $XXX GGG",
367 # Test that we don't blow up even if they subscript
368 # something in ways they "can't."
369 "${FFF[0]}", "G",
370 "${FFF[7]}", "",
371 "${NOTHING[1]}", "",
373 # Test various combinations of strings and lists.
374 #None, '',
375 '', '',
376 'x', 'x',
377 'x y', 'x y',
378 '$N', '',
379 '$X', 'x',
380 '$Y', 'x',
381 '$R', '',
382 '$S', 'x y',
383 '$LS', 'x y',
384 '$L', 'x y',
385 '$TS', 'x y',
386 '$T', 'x y',
387 '$S z', 'x y z',
388 '$LS z', 'x y z',
389 '$L z', 'x y z',
390 '$TS z', 'x y z',
391 '$T z', 'x y z',
392 #cs, 'cs',
393 #cl, 'cl',
394 '$CS', 'cs',
395 '$CL', 'cl',
397 # Various uses of UserString.
398 collections.UserString('x'), 'x',
399 collections.UserString('$X'), 'x',
400 collections.UserString('$US'), 'us',
401 '$US', 'us',
403 # Test function calls within ${}.
404 '$FUNCCALL', 'a xc b',
406 # Bug reported by Christoph Wiedemann.
407 cvt('$xxx/bin'), '/bin',
409 # Tests callables that don't match our calling arguments.
410 '$CALLABLE1', 'callable-1',
412 # Test handling of quotes.
413 'aaa "bbb ccc" ddd', 'aaa "bbb ccc" ddd',
416 def test_scons_subst(self):
417 """Test scons_subst(): basic substitution"""
418 return self.basic_comparisons(scons_subst, cvt)
420 subst_cases = [
421 "test $xxx",
422 "test ",
423 "test",
424 "test",
426 "test $($xxx$)",
427 "test $($)",
428 "test",
429 "test",
431 "test $( $xxx $)",
432 "test $( $)",
433 "test",
434 "test",
436 "test $( $THING2 $)",
437 "test $( $(STUFF$) $)",
438 "test STUFF",
439 "test",
441 "$AAA ${AAA}A $BBBB $BBB",
442 "a aA b",
443 "a aA b",
444 "a aA b",
446 "$RECURSE",
447 "foo bar",
448 "foo bar",
449 "foo bar",
451 "$RRR",
452 "foo bar",
453 "foo bar",
454 "foo bar",
456 # Verify what happens with no target or source nodes.
457 "$TARGET $SOURCES",
458 " ",
462 "$TARGETS $SOURCE",
463 " ",
467 # Various tests refactored from ActionTests.py.
468 "${LIST}",
469 "This is $( $) test",
470 "This is test",
471 "This is test",
473 ["|", "$(", "$AAA", "|", "$BBB", "$)", "|", "$CCC", 1],
474 ["|", "$(", "a", "|", "b", "$)", "|", "c", "1"],
475 ["|", "a", "|", "b", "|", "c", "1"],
476 ["|", "|", "c", "1"],
479 def test_subst_env(self):
480 """Test scons_subst(): expansion dictionary"""
481 # The expansion dictionary no longer comes from the construction
482 # environment automatically.
483 env = DummyEnv(self.loc)
484 s = scons_subst('$AAA', env)
485 assert s == '', s
487 def test_subst_SUBST_modes(self):
488 """Test scons_subst(): SUBST_* modes"""
489 env = DummyEnv(self.loc)
490 subst_cases = self.subst_cases[:]
492 gvars = env.Dictionary()
494 failed = 0
495 while subst_cases:
496 input, eraw, ecmd, esig = subst_cases[:4]
497 result = scons_subst(input, env, mode=SUBST_RAW, gvars=gvars)
498 if result != eraw:
499 if failed == 0: print()
500 print(" input %s => RAW %s did not match %s" % (repr(input), repr(result), repr(eraw)))
501 failed = failed + 1
502 result = scons_subst(input, env, mode=SUBST_CMD, gvars=gvars)
503 if result != ecmd:
504 if failed == 0: print()
505 print(" input %s => CMD %s did not match %s" % (repr(input), repr(result), repr(ecmd)))
506 failed = failed + 1
507 result = scons_subst(input, env, mode=SUBST_SIG, gvars=gvars)
508 if result != esig:
509 if failed == 0: print()
510 print(" input %s => SIG %s did not match %s" % (repr(input), repr(result), repr(esig)))
511 failed = failed + 1
512 del subst_cases[:4]
513 assert failed == 0, "%d subst() mode cases failed" % failed
515 def test_subst_target_source(self):
516 """Test scons_subst(): target= and source= arguments"""
517 env = DummyEnv(self.loc)
518 t1 = self.MyNode('t1')
519 t2 = self.MyNode('t2')
520 s1 = self.MyNode('s1')
521 s2 = self.MyNode('s2')
522 result = scons_subst("$TARGET $SOURCES", env,
523 target=[t1, t2],
524 source=[s1, s2])
525 assert result == "t1 s1 s2", result
526 result = scons_subst("$TARGET $SOURCES", env,
527 target=[t1, t2],
528 source=[s1, s2],
529 gvars={})
530 assert result == "t1 s1 s2", result
532 result = scons_subst("$TARGET $SOURCES", env, target=[], source=[])
533 assert result == " ", result
534 result = scons_subst("$TARGETS $SOURCE", env, target=[], source=[])
535 assert result == " ", result
537 def test_subst_callable_expansion(self):
538 """Test scons_subst(): expanding a callable"""
539 env = DummyEnv(self.loc)
540 gvars = env.Dictionary()
541 newcom = scons_subst("test $CMDGEN1 $SOURCES $TARGETS", env,
542 target=self.MyNode('t'), source=self.MyNode('s'),
543 gvars=gvars)
544 assert newcom == "test foo bar with spaces.out s t", newcom
546 def test_subst_callable_with_default_expansion(self):
547 """Test scons_subst(): expanding a callable with a default value arg"""
548 env = DummyEnv(self.loc)
549 gvars = env.Dictionary()
550 newcom = scons_subst("test $CallableWithDefault $SOURCES $TARGETS", env,
551 target=self.MyNode('t'), source=self.MyNode('s'),
552 gvars=gvars)
553 assert newcom == "test CallableWithDefault: default s t", newcom
555 def test_subst_partial_callable_with_default_expansion(self):
556 """Test scons_subst(): expanding a functools.partial callable which sets
557 the default value in the callable"""
558 env = DummyEnv(self.loc)
559 gvars = env.Dictionary()
560 newcom = scons_subst("test $PartialCallable $SOURCES $TARGETS", env,
561 target=self.MyNode('t'), source=self.MyNode('s'),
562 gvars=gvars)
563 assert newcom == "test CallableWithDefault: partial s t", newcom
565 def test_subst_partial_callable_with_no_default_expansion(self):
566 """Test scons_subst(): expanding a functools.partial callable which sets
567 the value for extraneous function argument"""
568 env = DummyEnv(self.loc)
569 gvars = env.Dictionary()
570 newcom = scons_subst("test $PartialCallableNoDefault $SOURCES $TARGETS", env,
571 target=self.MyNode('t'), source=self.MyNode('s'),
572 gvars=gvars)
573 assert newcom == "test CallableWithNoDefault: partialNoDefault s t", newcom
575 def test_subst_attribute_errors(self):
576 """Test scons_subst(): handling attribute errors"""
577 env = DummyEnv(self.loc)
578 try:
579 class Foo:
580 pass
581 scons_subst('${foo.bar}', env, gvars={'foo':Foo()})
582 except SCons.Errors.UserError as e:
583 expect = [
584 "AttributeError `bar' trying to evaluate `${foo.bar}'",
585 "AttributeError `Foo instance has no attribute 'bar'' trying to evaluate `${foo.bar}'",
586 "AttributeError `'Foo' instance has no attribute 'bar'' trying to evaluate `${foo.bar}'",
587 "AttributeError `'Foo' object has no attribute 'bar'' trying to evaluate `${foo.bar}'",
589 assert str(e) in expect, e
590 else:
591 raise AssertionError("did not catch expected UserError")
593 def test_subst_syntax_errors(self):
594 """Test scons_subst(): handling syntax errors"""
595 env = DummyEnv(self.loc)
596 try:
597 scons_subst('$foo.bar.3.0', env)
598 except SCons.Errors.UserError as e:
599 expect = [
600 # Python 2.5 to 3.9
601 "SyntaxError `invalid syntax (<string>, line 1)' trying to evaluate `$foo.bar.3.0'",
602 # Python 3.10 and later
603 "SyntaxError `invalid syntax. Perhaps you forgot a comma? (<string>, line 1)' trying to evaluate `$foo.bar.3.0'",
605 assert str(e) in expect, e
606 else:
607 raise AssertionError("did not catch expected UserError")
609 def test_subst_balance_errors(self):
610 """Test scons_subst(): handling syntax errors"""
611 env = DummyEnv(self.loc)
612 try:
613 scons_subst('$(', env, mode=SUBST_SIG)
614 except SCons.Errors.UserError as e:
615 assert str(e) == "Unbalanced $(/$) in: $(", str(e)
616 else:
617 raise AssertionError("did not catch expected UserError")
619 try:
620 scons_subst('$)', env, mode=SUBST_SIG)
621 except SCons.Errors.UserError as e:
622 assert str(e) == "Unbalanced $(/$) in: $)", str(e)
623 else:
624 raise AssertionError("did not catch expected UserError")
626 def test_subst_type_errors(self):
627 """Test scons_subst(): handling type errors"""
628 env = DummyEnv(self.loc)
629 try:
630 scons_subst("${NONE[2]}", env, gvars={'NONE':None})
631 except SCons.Errors.UserError as e:
632 expect = [
633 # Python 2.7 and later
634 "TypeError `'NoneType' object is not subscriptable' trying to evaluate `${NONE[2]}'",
635 # Python 2.7 and later under Fedora
636 "TypeError `'NoneType' object has no attribute '__getitem__'' trying to evaluate `${NONE[2]}'",
638 assert str(e) in expect, e
639 else:
640 raise AssertionError("did not catch expected UserError")
642 try:
643 def func(a, b, c):
644 pass
645 scons_subst("${func(1)}", env, gvars={'func':func})
646 except SCons.Errors.UserError as e:
647 expect = [
648 # Python 3.5 (and 3.x?)
649 "TypeError `func() missing 2 required positional arguments: 'b' and 'c'' trying to evaluate `${func(1)}'",
650 # Python 3.10
651 "TypeError `scons_subst_TestCase.test_subst_type_errors.<locals>.func() missing 2 required positional arguments: 'b' and 'c'' trying to evaluate `${func(1)}'",
653 assert str(e) in expect, repr(str(e))
654 else:
655 raise AssertionError("did not catch expected UserError")
657 def test_subst_raw_function(self):
658 """Test scons_subst(): fetch function with SUBST_RAW plus conv"""
659 # Test that the combination of SUBST_RAW plus a pass-through
660 # conversion routine allows us to fetch a function through the
661 # dictionary. CommandAction uses this to allow delayed evaluation
662 # of $SPAWN variables.
663 env = DummyEnv(self.loc)
664 gvars = env.Dictionary()
665 x = lambda x: x
666 r = scons_subst("$CALLABLE1", env, mode=SUBST_RAW, conv=x, gvars=gvars)
667 assert r is self.callable_object_1, repr(r)
668 r = scons_subst("$CALLABLE1", env, mode=SUBST_RAW, gvars=gvars)
669 assert r == 'callable-1', repr(r)
671 # Test how we handle overriding the internal conversion routines.
672 def s(obj):
673 return obj
675 n1 = self.MyNode('n1')
676 env = DummyEnv({'NODE' : n1})
677 gvars = env.Dictionary()
678 node = scons_subst("$NODE", env, mode=SUBST_RAW, conv=s, gvars=gvars)
679 assert node is n1, node
680 node = scons_subst("$NODE", env, mode=SUBST_CMD, conv=s, gvars=gvars)
681 assert node is n1, node
682 node = scons_subst("$NODE", env, mode=SUBST_SIG, conv=s, gvars=gvars)
683 assert node is n1, node
685 def test_subst_overriding_gvars(self):
686 """Test scons_subst(): supplying an overriding gvars dictionary"""
687 env = DummyEnv({'XXX' : 'xxx'})
688 result = scons_subst('$XXX', env, gvars=env.Dictionary())
689 assert result == 'xxx', result
690 result = scons_subst('$XXX', env, gvars={'XXX' : 'yyy'})
691 assert result == 'yyy', result
693 class CLVar_TestCase(unittest.TestCase):
694 def test_CLVar(self):
695 """Test scons_subst() and scons_subst_list() with CLVar objects"""
697 loc = {}
698 loc['FOO'] = 'foo'
699 loc['BAR'] = SCons.Util.CLVar('bar')
700 loc['CALL'] = lambda target, source, env, for_signature: 'call'
701 env = DummyEnv(loc)
703 cmd = SCons.Util.CLVar("test $FOO $BAR $CALL test")
705 newcmd = scons_subst(cmd, env, gvars=env.Dictionary())
706 assert newcmd == ['test', 'foo', 'bar', 'call', 'test'], newcmd
708 cmd_list = scons_subst_list(cmd, env, gvars=env.Dictionary())
709 assert len(cmd_list) == 1, cmd_list
710 assert cmd_list[0][0] == "test", cmd_list[0][0]
711 assert cmd_list[0][1] == "foo", cmd_list[0][1]
712 assert cmd_list[0][2] == "bar", cmd_list[0][2]
713 assert cmd_list[0][3] == "call", cmd_list[0][3]
714 assert cmd_list[0][4] == "test", cmd_list[0][4]
716 class scons_subst_list_TestCase(SubstTestCase):
718 basic_cases = [
719 "$TARGETS",
721 ["foo/bar.exe", "/bar/baz with spaces.obj", "../foo/baz.obj"],
724 "$SOURCES $NEWLINE $TARGETS",
726 ["foo/blah with spaces.cpp", "/bar/ack.cpp", "../foo/ack.c", "before"],
727 ["after", "foo/bar.exe", "/bar/baz with spaces.obj", "../foo/baz.obj"],
730 "$SOURCES$NEWLINE",
732 ["foo/blah with spaces.cpp", "/bar/ack.cpp", "../foo/ack.cbefore"],
733 ["after"],
736 "foo$FFF",
738 ["fooGGG"],
741 "foo${FFF}",
743 ["fooGGG"],
746 "test ${SOURCES.attribute.attr1}",
748 ["test", "attr$1-blah with spaces.cpp", "attr$1-ack.cpp", "attr$1-ack.c"],
751 "test ${SOURCES.attribute.attr2}",
753 ["test", "attr$2-blah with spaces.cpp", "attr$2-ack.cpp", "attr$2-ack.c"],
756 "$DO --in=$FOO --out=$BAR",
758 ["do something", "--in=foo.in", "--out=bar with spaces.out"],
761 # This test is now fixed, and works like it should.
762 "$DO --in=$CRAZY --out=$BAR",
764 ["do something", "--in=crazy\nfile.in", "--out=bar with spaces.out"],
767 # Try passing a list to scons_subst_list().
768 [ "$SOURCES$NEWLINE", "$TARGETS", "This is a test"],
770 ["foo/blah with spaces.cpp", "/bar/ack.cpp", "../foo/ack.cbefore"],
771 ["after", "foo/bar.exe", "/bar/baz with spaces.obj", "../foo/baz.obj", "This is a test"],
774 # Test against a former bug in scons_subst_list().
775 "$XXX$HHH",
777 ["GGGIII"],
780 # Test double-dollar-sign behavior.
781 "$$FFF$HHH",
783 ["$FFFIII"],
786 # Test various combinations of strings, lists and functions.
787 None, [[]],
788 [None], [[]],
789 '', [[]],
790 [''], [[]],
791 'x', [['x']],
792 ['x'], [['x']],
793 'x y', [['x', 'y']],
794 ['x y'], [['x y']],
795 ['x', 'y'], [['x', 'y']],
796 '$N', [[]],
797 ['$N'], [[]],
798 '$X', [['x']],
799 ['$X'], [['x']],
800 '$Y', [['x']],
801 ['$Y'], [['x']],
802 #'$R', [[]],
803 #['$R'], [[]],
804 '$S', [['x', 'y']],
805 '$S z', [['x', 'y', 'z']],
806 ['$S'], [['x', 'y']],
807 ['$S z'], [['x', 'y z']], # XXX - IS THIS BEST?
808 ['$S', 'z'], [['x', 'y', 'z']],
809 '$LS', [['x y']],
810 '$LS z', [['x y', 'z']],
811 ['$LS'], [['x y']],
812 ['$LS z'], [['x y z']],
813 ['$LS', 'z'], [['x y', 'z']],
814 '$L', [['x', 'y']],
815 '$L z', [['x', 'y', 'z']],
816 ['$L'], [['x', 'y']],
817 ['$L z'], [['x', 'y z']], # XXX - IS THIS BEST?
818 ['$L', 'z'], [['x', 'y', 'z']],
819 cs, [['cs']],
820 [cs], [['cs']],
821 cl, [['cl']],
822 [cl], [['cl']],
823 '$CS', [['cs']],
824 ['$CS'], [['cs']],
825 '$CL', [['cl']],
826 ['$CL'], [['cl']],
828 # Various uses of UserString.
829 collections.UserString('x'), [['x']],
830 [collections.UserString('x')], [['x']],
831 collections.UserString('$X'), [['x']],
832 [collections.UserString('$X')], [['x']],
833 collections.UserString('$US'), [['us']],
834 [collections.UserString('$US')], [['us']],
835 '$US', [['us']],
836 ['$US'], [['us']],
838 # Test function calls within ${}.
839 '$FUNCCALL', [['a', 'xc', 'b']],
841 # Test handling of newlines in white space.
842 'foo\nbar', [['foo'], ['bar']],
843 'foo\n\nbar', [['foo'], ['bar']],
844 'foo \n \n bar', [['foo'], ['bar']],
845 'foo \nmiddle\n bar', [['foo'], ['middle'], ['bar']],
847 # Bug reported by Christoph Wiedemann.
848 cvt('$xxx/bin'), [['/bin']],
850 # Test variables smooshed together with different prefixes.
851 'foo$AAA', [['fooa']],
852 '<$AAA', [['<', 'a']],
853 '>$AAA', [['>', 'a']],
854 '|$AAA', [['|', 'a']],
856 # Test callables that don't match our calling arguments.
857 '$CALLABLE2', [['callable-2']],
859 # Test handling of quotes.
860 # XXX Find a way to handle this in the future.
861 #'aaa "bbb ccc" ddd', [['aaa', 'bbb ccc', 'ddd']],
863 '${_defines(DEFS)}', [['Q1="q1"', 'Q2="a"']],
866 def test_scons_subst_list(self):
867 """Test scons_subst_list(): basic substitution"""
868 def convert_lists(expect):
869 return [list(map(cvt, l)) for l in expect]
870 return self.basic_comparisons(scons_subst_list, convert_lists)
872 subst_list_cases = [
873 "test $xxx",
874 [["test"]],
875 [["test"]],
876 [["test"]],
878 "test $($xxx$)",
879 [["test", "$($)"]],
880 [["test"]],
881 [["test"]],
883 "test $( $xxx $)",
884 [["test", "$(", "$)"]],
885 [["test"]],
886 [["test"]],
888 "$AAA ${AAA}A $BBBB $BBB",
889 [["a", "aA", "b"]],
890 [["a", "aA", "b"]],
891 [["a", "aA", "b"]],
893 "$RECURSE",
894 [["foo", "bar"]],
895 [["foo", "bar"]],
896 [["foo", "bar"]],
898 "$RRR",
899 [["foo", "bar"]],
900 [["foo", "bar"]],
901 [["foo", "bar"]],
903 # Verify what happens with no target or source nodes.
904 "$TARGET $SOURCES",
905 [[]],
906 [[]],
907 [[]],
909 "$TARGETS $SOURCE",
910 [[]],
911 [[]],
912 [[]],
914 # Various test refactored from ActionTests.py
915 "${LIST}",
916 [['This', 'is', '$(', '$)', 'test']],
917 [['This', 'is', 'test']],
918 [['This', 'is', 'test']],
920 ["|", "$(", "$AAA", "|", "$BBB", "$)", "|", "$CCC", 1],
921 [["|", "$(", "a", "|", "b", "$)", "|", "c", "1"]],
922 [["|", "a", "|", "b", "|", "c", "1"]],
923 [["|", "|", "c", "1"]],
926 def test_subst_env(self):
927 """Test scons_subst_list(): expansion dictionary"""
928 # The expansion dictionary no longer comes from the construction
929 # environment automatically.
930 env = DummyEnv()
931 s = scons_subst_list('$AAA', env)
932 assert s == [[]], s
934 def test_subst_target_source(self):
935 """Test scons_subst_list(): target= and source= arguments"""
936 env = DummyEnv(self.loc)
937 gvars = env.Dictionary()
938 t1 = self.MyNode('t1')
939 t2 = self.MyNode('t2')
940 s1 = self.MyNode('s1')
941 s2 = self.MyNode('s2')
942 result = scons_subst_list("$TARGET $SOURCES", env,
943 target=[t1, t2],
944 source=[s1, s2],
945 gvars=gvars)
946 assert result == [['t1', 's1', 's2']], result
947 result = scons_subst_list("$TARGET $SOURCES", env,
948 target=[t1, t2],
949 source=[s1, s2],
950 gvars={})
951 assert result == [['t1', 's1', 's2']], result
953 # Test interpolating a callable.
954 _t = DummyNode('t')
955 _s = DummyNode('s')
956 cmd_list = scons_subst_list("testing $CMDGEN1 $TARGETS $SOURCES",
957 env, target=_t, source=_s,
958 gvars=gvars)
959 assert cmd_list == [['testing', 'foo', 'bar with spaces.out', 't', 's']], cmd_list
961 def test_subst_escape(self):
962 """Test scons_subst_list(): escape functionality"""
963 env = DummyEnv(self.loc)
964 gvars = env.Dictionary()
965 def escape_func(foo):
966 return '**' + foo + '**'
967 cmd_list = scons_subst_list("abc $LITERALS xyz", env, gvars=gvars)
968 assert cmd_list == [['abc',
969 'foo\nwith\nnewlines',
970 'bar\nwith\nnewlines',
971 'xyz']], cmd_list
972 c = cmd_list[0][0].escape(escape_func)
973 assert c == 'abc', c
974 c = cmd_list[0][1].escape(escape_func)
975 assert c == '**foo\nwith\nnewlines**', c
976 c = cmd_list[0][2].escape(escape_func)
977 assert c == '**bar\nwith\nnewlines**', c
978 c = cmd_list[0][3].escape(escape_func)
979 assert c == 'xyz', c
981 # We used to treat literals smooshed together like the whole
982 # thing was literal and escape it as a unit. The commented-out
983 # asserts below are in case we ever have to find a way to
984 # resurrect that functionality in some way.
985 cmd_list = scons_subst_list("abc${LITERALS}xyz", env, gvars=gvars)
986 c = cmd_list[0][0].escape(escape_func)
987 #assert c == '**abcfoo\nwith\nnewlines**', c
988 assert c == 'abcfoo\nwith\nnewlines', c
989 c = cmd_list[0][1].escape(escape_func)
990 #assert c == '**bar\nwith\nnewlinesxyz**', c
991 assert c == 'bar\nwith\nnewlinesxyz', c
993 _t = DummyNode('t')
995 cmd_list = scons_subst_list('echo "target: $TARGET"', env,
996 target=_t, gvars=gvars)
997 c = cmd_list[0][0].escape(escape_func)
998 assert c == 'echo', c
999 c = cmd_list[0][1].escape(escape_func)
1000 assert c == '"target:', c
1001 c = cmd_list[0][2].escape(escape_func)
1002 assert c == 't"', c
1004 def test_subst_SUBST_modes(self):
1005 """Test scons_subst_list(): SUBST_* modes"""
1006 env = DummyEnv(self.loc)
1007 subst_list_cases = self.subst_list_cases[:]
1008 gvars = env.Dictionary()
1010 r = scons_subst_list("$TARGET $SOURCES", env, mode=SUBST_RAW, gvars=gvars)
1011 assert r == [[]], r
1013 failed = 0
1014 while subst_list_cases:
1015 input, eraw, ecmd, esig = subst_list_cases[:4]
1016 result = scons_subst_list(input, env, mode=SUBST_RAW, gvars=gvars)
1017 if result != eraw:
1018 if failed == 0: print()
1019 print(" input %s => RAW %s did not match %s" % (repr(input), repr(result), repr(eraw)))
1020 failed = failed + 1
1021 result = scons_subst_list(input, env, mode=SUBST_CMD, gvars=gvars)
1022 if result != ecmd:
1023 if failed == 0: print()
1024 print(" input %s => CMD %s did not match %s" % (repr(input), repr(result), repr(ecmd)))
1025 failed = failed + 1
1026 result = scons_subst_list(input, env, mode=SUBST_SIG, gvars=gvars)
1027 if result != esig:
1028 if failed == 0: print()
1029 print(" input %s => SIG %s did not match %s" % (repr(input), repr(result), repr(esig)))
1030 failed = failed + 1
1031 del subst_list_cases[:4]
1032 assert failed == 0, "%d subst() mode cases failed" % failed
1034 def test_subst_attribute_errors(self):
1035 """Test scons_subst_list(): handling attribute errors"""
1036 env = DummyEnv()
1037 try:
1038 class Foo:
1039 pass
1040 scons_subst_list('${foo.bar}', env, gvars={'foo':Foo()})
1041 except SCons.Errors.UserError as e:
1042 expect = [
1043 "AttributeError `bar' trying to evaluate `${foo.bar}'",
1044 "AttributeError `Foo instance has no attribute 'bar'' trying to evaluate `${foo.bar}'",
1045 "AttributeError `'Foo' instance has no attribute 'bar'' trying to evaluate `${foo.bar}'",
1046 "AttributeError `'Foo' object has no attribute 'bar'' trying to evaluate `${foo.bar}'",
1048 assert str(e) in expect, e
1049 else:
1050 raise AssertionError("did not catch expected UserError")
1052 def test_subst_syntax_errors(self):
1053 """Test scons_subst_list(): handling syntax errors"""
1054 env = DummyEnv()
1055 try:
1056 scons_subst_list('$foo.bar.3.0', env)
1057 except SCons.Errors.UserError as e:
1058 expect = [
1059 # Python 2.5 to 3.9
1060 "SyntaxError `invalid syntax (<string>, line 1)' trying to evaluate `$foo.bar.3.0'",
1061 # Python 3.10 and later
1062 "SyntaxError `invalid syntax. Perhaps you forgot a comma? (<string>, line 1)' trying to evaluate `$foo.bar.3.0'",
1064 assert str(e) in expect, e
1065 else:
1066 raise AssertionError("did not catch expected SyntaxError")
1068 def test_subst_raw_function(self):
1069 """Test scons_subst_list(): fetch function with SUBST_RAW plus conv"""
1070 # Test that the combination of SUBST_RAW plus a pass-through
1071 # conversion routine allows us to fetch a function through the
1072 # dictionary.
1073 env = DummyEnv(self.loc)
1074 gvars = env.Dictionary()
1075 x = lambda x: x
1076 r = scons_subst_list("$CALLABLE2", env, mode=SUBST_RAW, conv=x, gvars=gvars)
1077 assert r == [[self.callable_object_2]], repr(r)
1078 r = scons_subst_list("$CALLABLE2", env, mode=SUBST_RAW, gvars=gvars)
1079 assert r == [['callable-2']], repr(r)
1081 def test_subst_list_overriding_gvars(self):
1082 """Test scons_subst_list(): overriding conv()"""
1083 env = DummyEnv()
1084 def s(obj):
1085 return obj
1087 n1 = self.MyNode('n1')
1088 env = DummyEnv({'NODE' : n1})
1089 gvars=env.Dictionary()
1090 node = scons_subst_list("$NODE", env, mode=SUBST_RAW, conv=s, gvars=gvars)
1091 assert node == [[n1]], node
1092 node = scons_subst_list("$NODE", env, mode=SUBST_CMD, conv=s, gvars=gvars)
1093 assert node == [[n1]], node
1094 node = scons_subst_list("$NODE", env, mode=SUBST_SIG, conv=s, gvars=gvars)
1095 assert node == [[n1]], node
1097 def test_subst_list_overriding_gvars2(self):
1098 """Test scons_subst_list(): supplying an overriding gvars dictionary"""
1099 env = DummyEnv({'XXX' : 'xxx'})
1100 result = scons_subst_list('$XXX', env, gvars=env.Dictionary())
1101 assert result == [['xxx']], result
1102 result = scons_subst_list('$XXX', env, gvars={'XXX' : 'yyy'})
1103 assert result == [['yyy']], result
1105 class scons_subst_once_TestCase(unittest.TestCase):
1107 loc = {
1108 'CCFLAGS' : '-DFOO',
1109 'ONE' : 1,
1110 'RECURSE' : 'r $RECURSE r',
1111 'LIST' : ['a', 'b', 'c'],
1114 basic_cases = [
1115 '$CCFLAGS -DBAR',
1116 'OTHER_KEY',
1117 '$CCFLAGS -DBAR',
1119 '$CCFLAGS -DBAR',
1120 'CCFLAGS',
1121 '-DFOO -DBAR',
1123 'x $ONE y',
1124 'ONE',
1125 'x 1 y',
1127 'x $RECURSE y',
1128 'RECURSE',
1129 'x r $RECURSE r y',
1131 '$LIST',
1132 'LIST',
1133 'a b c',
1135 ['$LIST'],
1136 'LIST',
1137 ['a', 'b', 'c'],
1139 ['x', '$LIST', 'y'],
1140 'LIST',
1141 ['x', 'a', 'b', 'c', 'y'],
1143 ['x', 'x $LIST y', 'y'],
1144 'LIST',
1145 ['x', 'x a b c y', 'y'],
1147 ['x', 'x $CCFLAGS y', 'y'],
1148 'LIST',
1149 ['x', 'x $CCFLAGS y', 'y'],
1151 ['x', 'x $RECURSE y', 'y'],
1152 'LIST',
1153 ['x', 'x $RECURSE y', 'y'],
1156 def test_subst_once(self):
1157 """Test the scons_subst_once() function"""
1158 env = DummyEnv(self.loc)
1159 cases = self.basic_cases[:]
1161 failed = 0
1162 while cases:
1163 input, key, expect = cases[:3]
1164 result = scons_subst_once(input, env, key)
1165 if result != expect:
1166 if failed == 0: print()
1167 print(" input %s (%s) => %s did not match %s" % (repr(input), repr(key), repr(result), repr(expect)))
1168 failed = failed + 1
1169 del cases[:3]
1170 assert failed == 0, "%d subst() cases failed" % failed
1172 class quote_spaces_TestCase(unittest.TestCase):
1173 def test_quote_spaces(self):
1174 """Test the quote_spaces() method..."""
1175 q = quote_spaces('x')
1176 assert q == 'x', q
1178 q = quote_spaces('x x')
1179 assert q == '"x x"', q
1181 q = quote_spaces('x\tx')
1182 assert q == '"x\tx"', q
1184 class Node:
1185 def __init__(self, name, children=[]):
1186 self.children = children
1187 self.name = name
1188 def __str__(self):
1189 return self.name
1190 def exists(self):
1191 return 1
1192 def rexists(self):
1193 return 1
1194 def has_builder(self):
1195 return 1
1196 def has_explicit_builder(self):
1197 return 1
1198 def side_effect(self):
1199 return 1
1200 def precious(self):
1201 return 1
1202 def always_build(self):
1203 return 1
1204 def current(self):
1205 return 1
1207 class LiteralTestCase(unittest.TestCase):
1208 def test_Literal(self):
1209 """Test the Literal() function."""
1210 input_list = [ '$FOO', Literal('$BAR') ]
1211 gvars = { 'FOO' : 'BAZ', 'BAR' : 'BLAT' }
1213 def escape_func(cmd):
1214 return '**' + cmd + '**'
1216 cmd_list = scons_subst_list(input_list, None, gvars=gvars)
1217 cmd_list = escape_list(cmd_list[0], escape_func)
1218 assert cmd_list == ['BAZ', '**$BAR**'], cmd_list
1220 def test_LiteralEqualsTest(self):
1221 """Test that Literals compare for equality properly"""
1222 assert Literal('a literal') == Literal('a literal')
1223 assert Literal('a literal') != Literal('b literal')
1225 class SpecialAttrWrapperTestCase(unittest.TestCase):
1226 def test_SpecialAttrWrapper(self):
1227 """Test the SpecialAttrWrapper() function."""
1228 input_list = [ '$FOO', SpecialAttrWrapper('$BAR', 'BLEH') ]
1229 gvars = { 'FOO' : 'BAZ', 'BAR' : 'BLAT' }
1231 def escape_func(cmd):
1232 return '**' + cmd + '**'
1234 cmd_list = scons_subst_list(input_list, None, gvars=gvars)
1235 cmd_list = escape_list(cmd_list[0], escape_func)
1236 assert cmd_list == ['BAZ', '**$BAR**'], cmd_list
1238 cmd_list = scons_subst_list(input_list, None, mode=SUBST_SIG, gvars=gvars)
1239 cmd_list = escape_list(cmd_list[0], escape_func)
1240 assert cmd_list == ['BAZ', '**BLEH**'], cmd_list
1242 class subst_dict_TestCase(unittest.TestCase):
1243 def test_subst_dict(self):
1244 """Test substituting dictionary values in an Action
1246 t = DummyNode('t')
1247 s = DummyNode('s')
1248 d = subst_dict(target=t, source=s)
1249 assert str(d['TARGETS'][0]) == 't', d['TARGETS']
1250 assert str(d['TARGET']) == 't', d['TARGET']
1251 assert str(d['SOURCES'][0]) == 's', d['SOURCES']
1252 assert str(d['SOURCE']) == 's', d['SOURCE']
1254 t1 = DummyNode('t1')
1255 t2 = DummyNode('t2')
1256 s1 = DummyNode('s1')
1257 s2 = DummyNode('s2')
1258 d = subst_dict(target=[t1, t2], source=[s1, s2])
1259 TARGETS = sorted([str(x) for x in d['TARGETS']])
1260 assert TARGETS == ['t1', 't2'], d['TARGETS']
1261 assert str(d['TARGET']) == 't1', d['TARGET']
1262 SOURCES = sorted([str(x) for x in d['SOURCES']])
1263 assert SOURCES == ['s1', 's2'], d['SOURCES']
1264 assert str(d['SOURCE']) == 's1', d['SOURCE']
1266 class V:
1267 # Fake Value node with no rfile() method.
1268 def __init__(self, name):
1269 self.name = name
1270 def __str__(self):
1271 return 'v-'+self.name
1272 def get_subst_proxy(self):
1273 return self
1275 class N(V):
1276 def rfile(self):
1277 return self.__class__('rstr-' + self.name)
1279 t3 = N('t3')
1280 t4 = DummyNode('t4')
1281 t5 = V('t5')
1282 s3 = DummyNode('s3')
1283 s4 = N('s4')
1284 s5 = V('s5')
1285 d = subst_dict(target=[t3, t4, t5], source=[s3, s4, s5])
1286 TARGETS = sorted([str(x) for x in d['TARGETS']])
1287 assert TARGETS == ['t4', 'v-t3', 'v-t5'], TARGETS
1288 SOURCES = sorted([str(x) for x in d['SOURCES']])
1289 assert SOURCES == ['s3', 'v-rstr-s4', 'v-s5'], SOURCES
1291 if __name__ == "__main__":
1292 unittest.main()
1294 # Local Variables:
1295 # tab-width:4
1296 # indent-tabs-mode:nil
1297 # End:
1298 # vim: set expandtab tabstop=4 shiftwidth=4: