[ci skip] Add note that this change may break SetOption() + ninja usage with fix
[scons.git] / SCons / SubstTests.py
blob12929c6239c024d7c1fdc0c0f92ca562269cf8e4
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) -> None:
40 self.name = os.path.normpath(name)
41 def __str__(self) -> str:
42 return self.name
43 def is_literal(self) -> bool:
44 return True
45 def rfile(self):
46 return self
47 def get_subst_proxy(self):
48 return self
50 class DummyEnv:
51 def __init__(self, dict={}) -> None:
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) -> str:
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) -> str:
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) -> None:
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: str="default") -> str:
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) -> str:
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) -> None:
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) -> None:
136 self.literal = literal
137 def __str__(self) -> str:
138 return self.literal
139 def is_literal(self) -> bool:
140 return True
142 class TestCallable:
143 def __init__(self, value) -> None:
144 self.value = value
145 def __call__(self) -> None:
146 pass
147 def __str__(self) -> str:
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) -> None:
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) -> None:
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) -> None:
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) -> None:
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) -> None:
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) -> None:
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) -> None:
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) -> None:
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) -> None:
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) -> None:
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) -> None:
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) -> None:
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]
717 def test_subst_overriding_lvars_overrides(self) -> None:
718 """Test that optional passed arg overrides overrides gvars, and existing lvars."""
719 env=DummyEnv({'XXX' : 'xxx'})
720 result = scons_subst('$XXX', env, gvars=env.Dictionary(), overrides={'XXX': 'yyz'})
721 assert result == 'yyz', result
724 class scons_subst_list_TestCase(SubstTestCase):
726 basic_cases = [
727 "$TARGETS",
729 ["foo/bar.exe", "/bar/baz with spaces.obj", "../foo/baz.obj"],
732 "$SOURCES $NEWLINE $TARGETS",
734 ["foo/blah with spaces.cpp", "/bar/ack.cpp", "../foo/ack.c", "before"],
735 ["after", "foo/bar.exe", "/bar/baz with spaces.obj", "../foo/baz.obj"],
738 "$SOURCES$NEWLINE",
740 ["foo/blah with spaces.cpp", "/bar/ack.cpp", "../foo/ack.cbefore"],
741 ["after"],
744 "foo$FFF",
746 ["fooGGG"],
749 "foo${FFF}",
751 ["fooGGG"],
754 "test ${SOURCES.attribute.attr1}",
756 ["test", "attr$1-blah with spaces.cpp", "attr$1-ack.cpp", "attr$1-ack.c"],
759 "test ${SOURCES.attribute.attr2}",
761 ["test", "attr$2-blah with spaces.cpp", "attr$2-ack.cpp", "attr$2-ack.c"],
764 "$DO --in=$FOO --out=$BAR",
766 ["do something", "--in=foo.in", "--out=bar with spaces.out"],
769 # This test is now fixed, and works like it should.
770 "$DO --in=$CRAZY --out=$BAR",
772 ["do something", "--in=crazy\nfile.in", "--out=bar with spaces.out"],
775 # Try passing a list to scons_subst_list().
776 [ "$SOURCES$NEWLINE", "$TARGETS", "This is a test"],
778 ["foo/blah with spaces.cpp", "/bar/ack.cpp", "../foo/ack.cbefore"],
779 ["after", "foo/bar.exe", "/bar/baz with spaces.obj", "../foo/baz.obj", "This is a test"],
782 # Test against a former bug in scons_subst_list().
783 "$XXX$HHH",
785 ["GGGIII"],
788 # Test double-dollar-sign behavior.
789 "$$FFF$HHH",
791 ["$FFFIII"],
794 # Test various combinations of strings, lists and functions.
795 None, [[]],
796 [None], [[]],
797 '', [[]],
798 [''], [[]],
799 'x', [['x']],
800 ['x'], [['x']],
801 'x y', [['x', 'y']],
802 ['x y'], [['x y']],
803 ['x', 'y'], [['x', 'y']],
804 '$N', [[]],
805 ['$N'], [[]],
806 '$X', [['x']],
807 ['$X'], [['x']],
808 '$Y', [['x']],
809 ['$Y'], [['x']],
810 #'$R', [[]],
811 #['$R'], [[]],
812 '$S', [['x', 'y']],
813 '$S z', [['x', 'y', 'z']],
814 ['$S'], [['x', 'y']],
815 ['$S z'], [['x', 'y z']], # XXX - IS THIS BEST?
816 ['$S', 'z'], [['x', 'y', 'z']],
817 '$LS', [['x y']],
818 '$LS z', [['x y', 'z']],
819 ['$LS'], [['x y']],
820 ['$LS z'], [['x y z']],
821 ['$LS', 'z'], [['x y', 'z']],
822 '$L', [['x', 'y']],
823 '$L z', [['x', 'y', 'z']],
824 ['$L'], [['x', 'y']],
825 ['$L z'], [['x', 'y z']], # XXX - IS THIS BEST?
826 ['$L', 'z'], [['x', 'y', 'z']],
827 cs, [['cs']],
828 [cs], [['cs']],
829 cl, [['cl']],
830 [cl], [['cl']],
831 '$CS', [['cs']],
832 ['$CS'], [['cs']],
833 '$CL', [['cl']],
834 ['$CL'], [['cl']],
836 # Various uses of UserString.
837 collections.UserString('x'), [['x']],
838 [collections.UserString('x')], [['x']],
839 collections.UserString('$X'), [['x']],
840 [collections.UserString('$X')], [['x']],
841 collections.UserString('$US'), [['us']],
842 [collections.UserString('$US')], [['us']],
843 '$US', [['us']],
844 ['$US'], [['us']],
846 # Test function calls within ${}.
847 '$FUNCCALL', [['a', 'xc', 'b']],
849 # Test handling of newlines in white space.
850 'foo\nbar', [['foo'], ['bar']],
851 'foo\n\nbar', [['foo'], ['bar']],
852 'foo \n \n bar', [['foo'], ['bar']],
853 'foo \nmiddle\n bar', [['foo'], ['middle'], ['bar']],
855 # Bug reported by Christoph Wiedemann.
856 cvt('$xxx/bin'), [['/bin']],
858 # Test variables smooshed together with different prefixes.
859 'foo$AAA', [['fooa']],
860 '<$AAA', [['<', 'a']],
861 '>$AAA', [['>', 'a']],
862 '|$AAA', [['|', 'a']],
864 # Test callables that don't match our calling arguments.
865 '$CALLABLE2', [['callable-2']],
867 # Test handling of quotes.
868 # XXX Find a way to handle this in the future.
869 #'aaa "bbb ccc" ddd', [['aaa', 'bbb ccc', 'ddd']],
871 '${_defines(DEFS)}', [['Q1="q1"', 'Q2="a"']],
874 def test_scons_subst_list(self):
875 """Test scons_subst_list(): basic substitution"""
876 def convert_lists(expect):
877 return [list(map(cvt, l)) for l in expect]
878 return self.basic_comparisons(scons_subst_list, convert_lists)
880 subst_list_cases = [
881 "test $xxx",
882 [["test"]],
883 [["test"]],
884 [["test"]],
886 "test $($xxx$)",
887 [["test", "$($)"]],
888 [["test"]],
889 [["test"]],
891 "test $( $xxx $)",
892 [["test", "$(", "$)"]],
893 [["test"]],
894 [["test"]],
896 "$AAA ${AAA}A $BBBB $BBB",
897 [["a", "aA", "b"]],
898 [["a", "aA", "b"]],
899 [["a", "aA", "b"]],
901 "$RECURSE",
902 [["foo", "bar"]],
903 [["foo", "bar"]],
904 [["foo", "bar"]],
906 "$RRR",
907 [["foo", "bar"]],
908 [["foo", "bar"]],
909 [["foo", "bar"]],
911 # Verify what happens with no target or source nodes.
912 "$TARGET $SOURCES",
913 [[]],
914 [[]],
915 [[]],
917 "$TARGETS $SOURCE",
918 [[]],
919 [[]],
920 [[]],
922 # Various test refactored from ActionTests.py
923 "${LIST}",
924 [['This', 'is', '$(', '$)', 'test']],
925 [['This', 'is', 'test']],
926 [['This', 'is', 'test']],
928 ["|", "$(", "$AAA", "|", "$BBB", "$)", "|", "$CCC", 1],
929 [["|", "$(", "a", "|", "b", "$)", "|", "c", "1"]],
930 [["|", "a", "|", "b", "|", "c", "1"]],
931 [["|", "|", "c", "1"]],
934 def test_subst_env(self) -> None:
935 """Test scons_subst_list(): expansion dictionary"""
936 # The expansion dictionary no longer comes from the construction
937 # environment automatically.
938 env = DummyEnv()
939 s = scons_subst_list('$AAA', env)
940 assert s == [[]], s
942 def test_subst_target_source(self) -> None:
943 """Test scons_subst_list(): target= and source= arguments"""
944 env = DummyEnv(self.loc)
945 gvars = env.Dictionary()
946 t1 = self.MyNode('t1')
947 t2 = self.MyNode('t2')
948 s1 = self.MyNode('s1')
949 s2 = self.MyNode('s2')
950 result = scons_subst_list("$TARGET $SOURCES", env,
951 target=[t1, t2],
952 source=[s1, s2],
953 gvars=gvars)
954 assert result == [['t1', 's1', 's2']], result
955 result = scons_subst_list("$TARGET $SOURCES", env,
956 target=[t1, t2],
957 source=[s1, s2],
958 gvars={})
959 assert result == [['t1', 's1', 's2']], result
961 # Test interpolating a callable.
962 _t = DummyNode('t')
963 _s = DummyNode('s')
964 cmd_list = scons_subst_list("testing $CMDGEN1 $TARGETS $SOURCES",
965 env, target=_t, source=_s,
966 gvars=gvars)
967 assert cmd_list == [['testing', 'foo', 'bar with spaces.out', 't', 's']], cmd_list
969 def test_subst_escape(self) -> None:
970 """Test scons_subst_list(): escape functionality"""
971 env = DummyEnv(self.loc)
972 gvars = env.Dictionary()
973 def escape_func(foo):
974 return '**' + foo + '**'
975 cmd_list = scons_subst_list("abc $LITERALS xyz", env, gvars=gvars)
976 assert cmd_list == [['abc',
977 'foo\nwith\nnewlines',
978 'bar\nwith\nnewlines',
979 'xyz']], cmd_list
980 c = cmd_list[0][0].escape(escape_func)
981 assert c == 'abc', c
982 c = cmd_list[0][1].escape(escape_func)
983 assert c == '**foo\nwith\nnewlines**', c
984 c = cmd_list[0][2].escape(escape_func)
985 assert c == '**bar\nwith\nnewlines**', c
986 c = cmd_list[0][3].escape(escape_func)
987 assert c == 'xyz', c
989 # We used to treat literals smooshed together like the whole
990 # thing was literal and escape it as a unit. The commented-out
991 # asserts below are in case we ever have to find a way to
992 # resurrect that functionality in some way.
993 cmd_list = scons_subst_list("abc${LITERALS}xyz", env, gvars=gvars)
994 c = cmd_list[0][0].escape(escape_func)
995 #assert c == '**abcfoo\nwith\nnewlines**', c
996 assert c == 'abcfoo\nwith\nnewlines', c
997 c = cmd_list[0][1].escape(escape_func)
998 #assert c == '**bar\nwith\nnewlinesxyz**', c
999 assert c == 'bar\nwith\nnewlinesxyz', c
1001 _t = DummyNode('t')
1003 cmd_list = scons_subst_list('echo "target: $TARGET"', env,
1004 target=_t, gvars=gvars)
1005 c = cmd_list[0][0].escape(escape_func)
1006 assert c == 'echo', c
1007 c = cmd_list[0][1].escape(escape_func)
1008 assert c == '"target:', c
1009 c = cmd_list[0][2].escape(escape_func)
1010 assert c == 't"', c
1012 def test_subst_SUBST_modes(self) -> None:
1013 """Test scons_subst_list(): SUBST_* modes"""
1014 env = DummyEnv(self.loc)
1015 subst_list_cases = self.subst_list_cases[:]
1016 gvars = env.Dictionary()
1018 r = scons_subst_list("$TARGET $SOURCES", env, mode=SUBST_RAW, gvars=gvars)
1019 assert r == [[]], r
1021 failed = 0
1022 while subst_list_cases:
1023 input, eraw, ecmd, esig = subst_list_cases[:4]
1024 result = scons_subst_list(input, env, mode=SUBST_RAW, gvars=gvars)
1025 if result != eraw:
1026 if failed == 0: print()
1027 print(" input %s => RAW %s did not match %s" % (repr(input), repr(result), repr(eraw)))
1028 failed = failed + 1
1029 result = scons_subst_list(input, env, mode=SUBST_CMD, gvars=gvars)
1030 if result != ecmd:
1031 if failed == 0: print()
1032 print(" input %s => CMD %s did not match %s" % (repr(input), repr(result), repr(ecmd)))
1033 failed = failed + 1
1034 result = scons_subst_list(input, env, mode=SUBST_SIG, gvars=gvars)
1035 if result != esig:
1036 if failed == 0: print()
1037 print(" input %s => SIG %s did not match %s" % (repr(input), repr(result), repr(esig)))
1038 failed = failed + 1
1039 del subst_list_cases[:4]
1040 assert failed == 0, "%d subst() mode cases failed" % failed
1042 def test_subst_attribute_errors(self):
1043 """Test scons_subst_list(): handling attribute errors"""
1044 env = DummyEnv()
1045 try:
1046 class Foo:
1047 pass
1048 scons_subst_list('${foo.bar}', env, gvars={'foo':Foo()})
1049 except SCons.Errors.UserError as e:
1050 expect = [
1051 "AttributeError `bar' trying to evaluate `${foo.bar}'",
1052 "AttributeError `Foo instance has no attribute 'bar'' trying to evaluate `${foo.bar}'",
1053 "AttributeError `'Foo' instance has no attribute 'bar'' trying to evaluate `${foo.bar}'",
1054 "AttributeError `'Foo' object has no attribute 'bar'' trying to evaluate `${foo.bar}'",
1056 assert str(e) in expect, e
1057 else:
1058 raise AssertionError("did not catch expected UserError")
1060 def test_subst_syntax_errors(self):
1061 """Test scons_subst_list(): handling syntax errors"""
1062 env = DummyEnv()
1063 try:
1064 scons_subst_list('$foo.bar.3.0', env)
1065 except SCons.Errors.UserError as e:
1066 expect = [
1067 # Python 2.5 to 3.9
1068 "SyntaxError `invalid syntax (<string>, line 1)' trying to evaluate `$foo.bar.3.0'",
1069 # Python 3.10 and later
1070 "SyntaxError `invalid syntax. Perhaps you forgot a comma? (<string>, line 1)' trying to evaluate `$foo.bar.3.0'",
1072 assert str(e) in expect, e
1073 else:
1074 raise AssertionError("did not catch expected SyntaxError")
1076 def test_subst_raw_function(self) -> None:
1077 """Test scons_subst_list(): fetch function with SUBST_RAW plus conv"""
1078 # Test that the combination of SUBST_RAW plus a pass-through
1079 # conversion routine allows us to fetch a function through the
1080 # dictionary.
1081 env = DummyEnv(self.loc)
1082 gvars = env.Dictionary()
1083 x = lambda x: x
1084 r = scons_subst_list("$CALLABLE2", env, mode=SUBST_RAW, conv=x, gvars=gvars)
1085 assert r == [[self.callable_object_2]], repr(r)
1086 r = scons_subst_list("$CALLABLE2", env, mode=SUBST_RAW, gvars=gvars)
1087 assert r == [['callable-2']], repr(r)
1089 def test_subst_list_overriding_gvars(self) -> None:
1090 """Test scons_subst_list(): overriding conv()"""
1091 env = DummyEnv()
1092 def s(obj):
1093 return obj
1095 n1 = self.MyNode('n1')
1096 env = DummyEnv({'NODE' : n1})
1097 gvars=env.Dictionary()
1098 node = scons_subst_list("$NODE", env, mode=SUBST_RAW, conv=s, gvars=gvars)
1099 assert node == [[n1]], node
1100 node = scons_subst_list("$NODE", env, mode=SUBST_CMD, conv=s, gvars=gvars)
1101 assert node == [[n1]], node
1102 node = scons_subst_list("$NODE", env, mode=SUBST_SIG, conv=s, gvars=gvars)
1103 assert node == [[n1]], node
1105 def test_subst_list_overriding_gvars2(self) -> None:
1106 """Test scons_subst_list(): supplying an overriding gvars dictionary"""
1107 env = DummyEnv({'XXX' : 'xxx'})
1108 result = scons_subst_list('$XXX', env, gvars=env.Dictionary())
1109 assert result == [['xxx']], result
1110 result = scons_subst_list('$XXX', env, gvars={'XXX' : 'yyy'})
1111 assert result == [['yyy']], result
1113 def test_subst_list_overriding_lvars_overrides(self) -> None:
1114 """Test that optional passed arg overrides overrides gvars, and existing lvars."""
1115 env = DummyEnv({'XXX':'xxx'})
1116 result = scons_subst_list('$XXX', env, gvars=env.Dictionary(), overrides={'XXX': 'yyy'})
1117 assert result == [['yyy']], result
1120 class scons_subst_once_TestCase(unittest.TestCase):
1122 loc = {
1123 'CCFLAGS' : '-DFOO',
1124 'ONE' : 1,
1125 'RECURSE' : 'r $RECURSE r',
1126 'LIST' : ['a', 'b', 'c'],
1129 basic_cases = [
1130 '$CCFLAGS -DBAR',
1131 'OTHER_KEY',
1132 '$CCFLAGS -DBAR',
1134 '$CCFLAGS -DBAR',
1135 'CCFLAGS',
1136 '-DFOO -DBAR',
1138 'x $ONE y',
1139 'ONE',
1140 'x 1 y',
1142 'x $RECURSE y',
1143 'RECURSE',
1144 'x r $RECURSE r y',
1146 '$LIST',
1147 'LIST',
1148 'a b c',
1150 ['$LIST'],
1151 'LIST',
1152 ['a', 'b', 'c'],
1154 ['x', '$LIST', 'y'],
1155 'LIST',
1156 ['x', 'a', 'b', 'c', 'y'],
1158 ['x', 'x $LIST y', 'y'],
1159 'LIST',
1160 ['x', 'x a b c y', 'y'],
1162 ['x', 'x $CCFLAGS y', 'y'],
1163 'LIST',
1164 ['x', 'x $CCFLAGS y', 'y'],
1166 ['x', 'x $RECURSE y', 'y'],
1167 'LIST',
1168 ['x', 'x $RECURSE y', 'y'],
1171 def test_subst_once(self) -> None:
1172 """Test the scons_subst_once() function"""
1173 env = DummyEnv(self.loc)
1174 cases = self.basic_cases[:]
1176 failed = 0
1177 while cases:
1178 input, key, expect = cases[:3]
1179 result = scons_subst_once(input, env, key)
1180 if result != expect:
1181 if failed == 0: print()
1182 print(" input %s (%s) => %s did not match %s" % (repr(input), repr(key), repr(result), repr(expect)))
1183 failed = failed + 1
1184 del cases[:3]
1185 assert failed == 0, "%d subst() cases failed" % failed
1187 class quote_spaces_TestCase(unittest.TestCase):
1188 def test_quote_spaces(self) -> None:
1189 """Test the quote_spaces() method..."""
1190 q = quote_spaces('x')
1191 assert q == 'x', q
1193 q = quote_spaces('x x')
1194 assert q == '"x x"', q
1196 q = quote_spaces('x\tx')
1197 assert q == '"x\tx"', q
1199 class Node:
1200 def __init__(self, name, children=[]) -> None:
1201 self.children = children
1202 self.name = name
1204 def __str__(self) -> str:
1205 return self.name
1206 def exists(self) -> bool:
1207 return True
1208 def rexists(self) -> bool:
1209 return True
1210 def has_builder(self) -> bool:
1211 return True
1212 def has_explicit_builder(self) -> bool:
1213 return True
1214 def side_effect(self) -> bool:
1215 return True
1216 def precious(self) -> bool:
1217 return True
1218 def always_build(self) -> bool:
1219 return True
1220 def current(self) -> bool:
1221 return True
1223 class LiteralTestCase(unittest.TestCase):
1224 def test_Literal(self) -> None:
1225 """Test the Literal() function."""
1226 input_list = [ '$FOO', Literal('$BAR') ]
1227 gvars = { 'FOO' : 'BAZ', 'BAR' : 'BLAT' }
1229 def escape_func(cmd):
1230 return '**' + cmd + '**'
1232 cmd_list = scons_subst_list(input_list, None, gvars=gvars)
1233 cmd_list = escape_list(cmd_list[0], escape_func)
1234 assert cmd_list == ['BAZ', '**$BAR**'], cmd_list
1236 def test_LiteralEqualsTest(self) -> None:
1237 """Test that Literals compare for equality properly"""
1238 assert Literal('a literal') == Literal('a literal')
1239 assert Literal('a literal') != Literal('b literal')
1241 class SpecialAttrWrapperTestCase(unittest.TestCase):
1242 def test_SpecialAttrWrapper(self) -> None:
1243 """Test the SpecialAttrWrapper() function."""
1244 input_list = [ '$FOO', SpecialAttrWrapper('$BAR', 'BLEH') ]
1245 gvars = { 'FOO' : 'BAZ', 'BAR' : 'BLAT' }
1247 def escape_func(cmd):
1248 return '**' + cmd + '**'
1250 cmd_list = scons_subst_list(input_list, None, gvars=gvars)
1251 cmd_list = escape_list(cmd_list[0], escape_func)
1252 assert cmd_list == ['BAZ', '**$BAR**'], cmd_list
1254 cmd_list = scons_subst_list(input_list, None, mode=SUBST_SIG, gvars=gvars)
1255 cmd_list = escape_list(cmd_list[0], escape_func)
1256 assert cmd_list == ['BAZ', '**BLEH**'], cmd_list
1258 class subst_dict_TestCase(unittest.TestCase):
1259 def test_subst_dict(self) -> None:
1260 """Test substituting dictionary values in an Action
1262 t = DummyNode('t')
1263 s = DummyNode('s')
1264 d = subst_dict(target=t, source=s)
1265 assert str(d['TARGETS'][0]) == 't', d['TARGETS']
1266 assert str(d['TARGET']) == 't', d['TARGET']
1267 assert str(d['SOURCES'][0]) == 's', d['SOURCES']
1268 assert str(d['SOURCE']) == 's', d['SOURCE']
1270 t1 = DummyNode('t1')
1271 t2 = DummyNode('t2')
1272 s1 = DummyNode('s1')
1273 s2 = DummyNode('s2')
1274 d = subst_dict(target=[t1, t2], source=[s1, s2])
1275 TARGETS = sorted([str(x) for x in d['TARGETS']])
1276 assert TARGETS == ['t1', 't2'], d['TARGETS']
1277 assert str(d['TARGET']) == 't1', d['TARGET']
1278 SOURCES = sorted([str(x) for x in d['SOURCES']])
1279 assert SOURCES == ['s1', 's2'], d['SOURCES']
1280 assert str(d['SOURCE']) == 's1', d['SOURCE']
1282 class V:
1283 # Fake Value node with no rfile() method.
1284 def __init__(self, name) -> None:
1285 self.name = name
1286 def __str__(self) -> str:
1287 return 'v-'+self.name
1288 def get_subst_proxy(self):
1289 return self
1291 class N(V):
1292 def rfile(self):
1293 return self.__class__('rstr-' + self.name)
1295 t3 = N('t3')
1296 t4 = DummyNode('t4')
1297 t5 = V('t5')
1298 s3 = DummyNode('s3')
1299 s4 = N('s4')
1300 s5 = V('s5')
1301 d = subst_dict(target=[t3, t4, t5], source=[s3, s4, s5])
1302 TARGETS = sorted([str(x) for x in d['TARGETS']])
1303 assert TARGETS == ['t4', 'v-t3', 'v-t5'], TARGETS
1304 SOURCES = sorted([str(x) for x in d['SOURCES']])
1305 assert SOURCES == ['s3', 'v-rstr-s4', 'v-s5'], SOURCES
1307 if __name__ == "__main__":
1308 unittest.main()
1310 # Local Variables:
1311 # tab-width:4
1312 # indent-tabs-mode:nil
1313 # End:
1314 # vim: set expandtab tabstop=4 shiftwidth=4: