http://scons.tigris.org/issues/show_bug.cgi?id=2329
[scons.git] / src / engine / SCons / BuilderTests.py
index c69aaa612c42bc6a204cdd370d05cab73a321710..e4ddaec5d89783498446a395d0ec5cc917ae1044 100644 (file)
@@ -1,5 +1,5 @@
 #
-# Copyright (c) 2001, 2002 Steven Knight
+# __COPYRIGHT__
 #
 # Permission is hereby granted, free of charge, to any person obtaining
 # a copy of this software and associated documentation files (the
@@ -23,6 +23,8 @@
 
 __revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
 
+import SCons.compat
+
 # Define a null function for use as a builder action.
 # Where this is defined in the file seems to affect its
 # byte-code contents, so try to minimize changes by
@@ -31,15 +33,22 @@ def Func():
     pass
 
 import os.path
+import re
 import sys
-import types
+import StringIO
 import unittest
+import UserList
 
 import TestCmd
+
+import SCons.Action
 import SCons.Builder
+import SCons.Environment
 import SCons.Errors
-import SCons.Node.FS
-import SCons.Warnings
+import SCons.Subst
+import SCons.Util
+
+sys.stdout = StringIO.StringIO()
 
 # Initial setup of the common environment for all tests,
 # a temporary working directory containing a
@@ -50,141 +59,343 @@ import SCons.Warnings
 # for each test, they can just use the one.
 test = TestCmd.TestCmd(workdir = '')
 
-test.write('act.py', """import os, string, sys
-f = open(sys.argv[1], 'w')
-f.write("act.py: '" + string.join(sys.argv[2:], "' '") + "'\\n")
-try:
-    if sys.argv[3]:
-        f.write("act.py: '" + os.environ[sys.argv[3]] + "'\\n")
-except:
-    pass
-f.close()
-sys.exit(0)
-""")
-
-act_py = test.workpath('act.py')
 outfile = test.workpath('outfile')
 outfile2 = test.workpath('outfile2')
 
+infile = test.workpath('infile')
+test.write(infile, "infile\n")
+
 show_string = None
-env_scanner = None
-count = 0
+
+scons_env = SCons.Environment.Environment()
+
+env_arg2nodes_called = None
 
 class Environment:
     def __init__(self, **kw):
         self.d = {}
+        self.d['SHELL'] = scons_env['SHELL']
+        self.d['SPAWN'] = scons_env['SPAWN']
+        self.d['ESCAPE'] = scons_env['ESCAPE']
         for k, v in kw.items():
             self.d[k] = v
+        global env_arg2nodes_called
+        env_arg2nodes_called = None
+        self.scanner = None
+        self.fs = SCons.Node.FS.FS()
     def subst(self, s):
-        try:
-            if s[0] == '$':
-                return self.d.get(s[1:], '')
-        except IndexError:
-            pass
-        return self.d.get(s, s)
+        if not SCons.Util.is_String(s):
+            return s
+        def substitute(m, d=self.d):
+            return d.get(m.group(1), '')
+        return re.sub(r'\$(\w+)', substitute, s)
+    def subst_target_source(self, string, raw=0, target=None,
+                            source=None, dict=None, conv=None):
+        return SCons.Subst.scons_subst(string, self, raw, target,
+                                       source, dict, conv)
+    def subst_list(self, string, raw=0, target=None, source=None, conv=None):
+        return SCons.Subst.scons_subst_list(string, self, raw, target,
+                                            source, {}, {}, conv)
+    def arg2nodes(self, args, factory, **kw):
+        global env_arg2nodes_called
+        env_arg2nodes_called = 1
+        if not SCons.Util.is_List(args):
+            args = [args]
+        list = []
+        for a in args:
+            if SCons.Util.is_String(a):
+                a = factory(self.subst(a))
+            list.append(a)
+        return list
+    def get_factory(self, factory):
+        return factory or self.fs.File
     def get_scanner(self, ext):
-        return env_scanner
+        return self.scanner
     def Dictionary(self):
         return {}
     def autogenerate(self, dir=''):
         return {}
+    def __setitem__(self, item, var):
+        self.d[item] = var
     def __getitem__(self, item):
         return self.d[item]
+    def __contains__(self, item):
+        return self.d.__contains__(item)
     def has_key(self, item):
-        return self.d.has_key(item)
-    
-env = Environment()
+        return item in self.d
+    def keys(self):
+        return self.d.keys()
+    def get(self, key, value=None):
+        return self.d.get(key, value)
+    def Override(self, overrides):
+        env = Environment(**self.d)
+        env.d.update(overrides)
+        env.scanner = self.scanner
+        return env
+    def _update(self, dict):
+        self.d.update(dict)
+    def items(self):
+        return self.d.items()
+    def sig_dict(self):
+        d = {}
+        for k,v in self.items(): d[k] = v
+        d['TARGETS'] = ['__t1__', '__t2__', '__t3__', '__t4__', '__t5__', '__t6__']
+        d['TARGET'] = d['TARGETS'][0]
+        d['SOURCES'] = ['__s1__', '__s2__', '__s3__', '__s4__', '__s5__', '__s6__']
+        d['SOURCE'] = d['SOURCES'][0]
+        return d
+    def __cmp__(self, other):
+        return cmp(self.scanner, other.scanner) or cmp(self.d, other.d)
+
+class MyAction:
+    def __init__(self, action):
+        self.action = action
+    def __call__(self, *args, **kw):
+        pass
+    def get_executor(self, env, overrides, tlist, slist, executor_kw):
+        return ['executor'] + [self.action]
+
+class MyNode_without_target_from_source:
+    def __init__(self, name):
+        self.name = name
+        self.sources = []
+        self.builder = None
+        self.is_explicit = None
+        self.side_effect = 0
+        self.suffix = os.path.splitext(name)[1]
+    def disambiguate(self):
+        return self
+    def __str__(self):
+        return self.name
+    def builder_set(self, builder):
+        self.builder = builder
+    def has_builder(self):
+        return not self.builder is None
+    def set_explicit(self, is_explicit):
+        self.is_explicit = is_explicit
+    def has_explicit_builder(self):
+        return self.is_explicit
+    def env_set(self, env, safe=0):
+        self.env = env
+    def add_source(self, source):
+        self.sources.extend(source)
+    def scanner_key(self):
+        return self.name
+    def is_derived(self):
+        return self.has_builder()
+    def generate_build_env(self, env):
+        return env
+    def get_build_env(self):
+        return self.executor.get_build_env()
+    def set_executor(self, executor):
+        self.executor = executor
+    def get_executor(self, create=1):
+        return self.executor
+
+class MyNode(MyNode_without_target_from_source):
+    def target_from_source(self, prefix, suffix, stripext):
+        return MyNode(prefix + stripext(str(self))[0] + suffix)
 
 class BuilderTestCase(unittest.TestCase):
 
+    def test__init__(self):
+        """Test simple Builder creation
+        """
+        builder = SCons.Builder.Builder(action="foo")
+        assert not builder is None, builder
+        builder = SCons.Builder.Builder(action="foo", OVERRIDE='x')
+        x = builder.overrides['OVERRIDE']
+        assert x == 'x', x
+
+    def test__nonzero__(self):
+        """Test a builder raising an exception when __nonzero__ is called
+        """
+        builder = SCons.Builder.Builder(action="foo")
+        exc_caught = None
+        try:
+            builder.__nonzero__()
+        except SCons.Errors.InternalError:
+            exc_caught = 1
+        assert exc_caught, "did not catch expected InternalError exception"
+
+        class Node:
+             pass
+
+        n = Node()
+        n.builder = builder
+        exc_caught = None
+        try:
+            if n.builder:
+                pass
+        except SCons.Errors.InternalError:
+            exc_caught = 1
+        assert exc_caught, "did not catch expected InternalError exception"
+
     def test__call__(self):
         """Test calling a builder to establish source dependencies
         """
-        class Node:
-            def __init__(self, name):
-                self.name = name
-                self.sources = []
-                self.builder = None
-            def __str__(self):
-                return self.name
-            def builder_set(self, builder):
-                self.builder = builder
-            def env_set(self, env, safe=0):
-                self.env = env
-            def add_source(self, source):
-                self.sources.extend(source)
-            def scanner_key(self):
-                return self.name
-        builder = SCons.Builder.Builder(name="builder", action="foo", node_factory=Node)
+        env = Environment()
+        builder = SCons.Builder.Builder(action="foo",
+                                        target_factory=MyNode,
+                                        source_factory=MyNode)
 
-        n1 = Node("n1");
-        n2 = Node("n2");
+        tgt = builder(env, source=[])
+        assert tgt == [], tgt
+
+        n1 = MyNode("n1")
+        n2 = MyNode("n2")
         builder(env, target = n1, source = n2)
-        assert n1.env == env
-        assert n1.builder == builder
-        assert n1.sources == [n2]
+        assert env_arg2nodes_called
+        assert n1.env == env, n1.env
+        assert n1.builder == builder, n1.builder
+        assert n1.sources == [n2], n1.sources
+        assert n1.executor, "no executor found"
         assert not hasattr(n2, 'env')
 
-        target = builder(env, target = 'n3', source = 'n4')
+        l = [1]
+        ul = UserList.UserList([2])
+        try:
+            l.extend(ul)
+        except TypeError:
+            def mystr(l):
+                return str(list(map(str, l)))
+        else:
+            mystr = str
+
+        nnn1 = MyNode("nnn1")
+        nnn2 = MyNode("nnn2")
+        tlist = builder(env, target = [nnn1, nnn2], source = [])
+        s = mystr(tlist)
+        assert s == "['nnn1', 'nnn2']", s
+        l = list(map(str, tlist))
+        assert l == ['nnn1', 'nnn2'], l
+
+        tlist = builder(env, target = 'n3', source = 'n4')
+        s = mystr(tlist)
+        assert s == "['n3']", s
+        target = tlist[0]
+        l = list(map(str, tlist))
+        assert l == ['n3'], l
         assert target.name == 'n3'
         assert target.sources[0].name == 'n4'
 
-        targets = builder(env, target = 'n4 n5', source = ['n6 n7'])
-        assert targets[0].name == 'n4'
-        assert targets[0].sources[0].name == 'n6 n7'
-        assert targets[1].name == 'n5'
-        assert targets[1].sources[0].name == 'n6 n7'
-
-        target = builder(env, target = ['n8 n9'], source = 'n10 n11')
+        tlist = builder(env, target = 'n4 n5', source = ['n6 n7'])
+        s = mystr(tlist)
+        assert s == "['n4 n5']", s
+        l = list(map(str, tlist))
+        assert l == ['n4 n5'], l
+        target = tlist[0]
+        assert target.name == 'n4 n5'
+        assert target.sources[0].name == 'n6 n7'
+
+        tlist = builder(env, target = ['n8 n9'], source = 'n10 n11')
+        s = mystr(tlist)
+        assert s == "['n8 n9']", s
+        l = list(map(str, tlist))
+        assert l == ['n8 n9'], l
+        target = tlist[0]
         assert target.name == 'n8 n9'
-        assert target.sources[0].name == 'n10'
-        assert target.sources[1].name == 'n11'
-
-        if not hasattr(types, 'UnicodeType'):
+        assert target.sources[0].name == 'n10 n11'
+
+        # A test to be uncommented when we freeze the environment
+        # as part of calling the builder.
+        #env1 = Environment(VAR='foo')
+        #target = builder(env1, target = 'n12', source = 'n13')
+        #env1['VAR'] = 'bar'
+        #be = target.get_build_env()
+        #assert be['VAR'] == 'foo', be['VAR']
+
+        try: unicode
+        except NameError:
             uni = str
         else:
             uni = unicode
 
-        targets = builder(env, target = uni('n12 n13'),
-                          source = [uni('n14 n15')])
-        assert targets[0].name == uni('n12')
-        assert targets[0].sources[0].name == uni('n14 n15')
-        assert targets[1].name == uni('n13')
-        assert targets[1].sources[0].name == uni('n14 n15')
+        target = builder(env, target = uni('n12 n13'),
+                          source = [uni('n14 n15')])[0]
+        assert target.name == uni('n12 n13')
+        assert target.sources[0].name == uni('n14 n15')
 
         target = builder(env, target = [uni('n16 n17')],
-                         source = uni('n18 n19'))
+                         source = uni('n18 n19'))[0]
         assert target.name == uni('n16 n17')
-        assert target.sources[0].name == uni('n18')
-        assert target.sources[1].name == uni('n19')
+        assert target.sources[0].name == uni('n18 n19')
 
-    def test_noname(self):
-        """Test deprecated warning for Builder name.
+        n20 = MyNode_without_target_from_source('n20')
+        flag = 0
+        try:
+            target = builder(env, None, source=n20)
+        except SCons.Errors.UserError, e:
+            flag = 1
+        assert flag, "UserError should be thrown if a source node can't create a target."
+
+        builder = SCons.Builder.Builder(action="foo",
+                                        target_factory=MyNode,
+                                        source_factory=MyNode,
+                                        prefix='p-',
+                                        suffix='.s')
+        target = builder(env, None, source='n21')[0]
+        assert target.name == 'p-n21.s', target
+
+        builder = SCons.Builder.Builder(misspelled_action="foo",
+                                        suffix = '.s')
+        try:
+            builder(env, target = 'n22', source = 'n22')
+        except SCons.Errors.UserError, e:
+            pass
+        else:
+            raise "Did not catch expected UserError."
 
-        Using the name argument for Builder() is deprectaed and the
-        user should receive a warning.
+        builder = SCons.Builder.Builder(action="foo")
+        target = builder(env, None, source='n22', srcdir='src_dir')[0]
+        p = target.sources[0].path
+        assert p == os.path.join('src_dir', 'n22'), p
+
+    def test_mistaken_variables(self):
+        """Test keyword arguments that are often mistakes
         """
-        SCons.Warnings.enableWarningClass(SCons.Warnings.DeprecatedWarning)
-        SCons.Warnings.warningAsException(1)
+        import SCons.Warnings
+        env = Environment()
+        builder = SCons.Builder.Builder(action="foo")
+
+        save_warn = SCons.Warnings.warn
+        warned = []
+        def my_warn(exception, warning, warned=warned):
+            warned.append(warning)
+        SCons.Warnings.warn = my_warn
 
         try:
-            try:
-                b = SCons.Builder.Builder(name='foo')
-            except SCons.Warnings.DeprecatedWarning:
-                pass
-            else:
-                assert 0
+            target = builder(env, 'mistaken1', sources='mistaken1.c')
+            assert warned == ["Did you mean to use `source' instead of `sources'?"], warned
+            del warned[:]
+
+            target = builder(env, 'mistaken2', targets='mistaken2.c')
+            assert warned == ["Did you mean to use `target' instead of `targets'?"], warned
+            del warned[:]
+
+            target = builder(env, 'mistaken3', targets='mistaken3', sources='mistaken3.c')
+            assert "Did you mean to use `source' instead of `sources'?" in warned, warned
+            assert "Did you mean to use `target' instead of `targets'?" in warned, warned
+            del warned[:]
         finally:
-            SCons.Warnings.suppressWarningClass(SCons.Warnings.DeprecatedWarning)
-            SCons.Warnings.warningAsException(0)
+            SCons.Warnings.warn = save_warn
 
     def test_action(self):
         """Test Builder creation
 
         Verify that we can retrieve the supplied action attribute.
         """
-        builder = SCons.Builder.Builder(name="builder", action="foo")
-        assert builder.action.cmd_list == ["foo"]
+        builder = SCons.Builder.Builder(action="foo")
+        assert builder.action.cmd_list == "foo"
+
+        def func():
+            pass
+        builder = SCons.Builder.Builder(action=func)
+        assert isinstance(builder.action, SCons.Action.FunctionAction)
+        # Preserve the following so that the baseline test will fail.
+        # Remove it in favor of the previous test at some convenient
+        # point in the future.
+        assert builder.action.execfunction == func
 
     def test_generator(self):
         """Test Builder creation given a generator function."""
@@ -192,236 +403,23 @@ class BuilderTestCase(unittest.TestCase):
         def generator():
             pass
 
-        builder = SCons.Builder.Builder(name="builder", generator=generator)
+        builder = SCons.Builder.Builder(generator=generator)
         assert builder.action.generator == generator
 
+    def test_get_name(self):
+        """Test the get_name() method
+        """
+
     def test_cmp(self):
         """Test simple comparisons of Builder objects
         """
-        b1 = SCons.Builder.Builder(name="b1", src_suffix = '.o')
-        b2 = SCons.Builder.Builder(name="b1", src_suffix = '.o')
+        b1 = SCons.Builder.Builder(src_suffix = '.o')
+        b2 = SCons.Builder.Builder(src_suffix = '.o')
         assert b1 == b2
-        b3 = SCons.Builder.Builder(name="b3", src_suffix = '.x')
+        b3 = SCons.Builder.Builder(src_suffix = '.x')
         assert b1 != b3
         assert b2 != b3
 
-    def test_execute(self):
-        """Test execution of simple Builder objects
-        
-        One Builder is a string that executes an external command,
-        one is an internal Python function, one is a list
-        containing one of each.
-        """
-
-        def MyBuilder(**kw):
-            builder = apply(SCons.Builder.Builder, (), kw)
-            def no_show(str):
-                pass
-            builder.action.show = no_show
-            return builder
-
-        python = sys.executable
-
-        cmd1 = r'%s %s %s xyzzy' % (python, act_py, outfile)
-
-        builder = MyBuilder(action = cmd1, name = "cmd1")
-        r = builder.execute()
-        assert r == 0
-        c = test.read(outfile, 'r')
-        assert c == "act.py: 'xyzzy'\n", c
-
-        cmd2 = r'%s %s %s $TARGET' % (python, act_py, outfile)
-
-        builder = MyBuilder(action = cmd2, name = "cmd2")
-        r = builder.execute(target = 'foo')
-        assert r == 0
-        c = test.read(outfile, 'r')
-        assert c == "act.py: 'foo'\n", c
-
-        cmd3 = r'%s %s %s ${TARGETS}' % (python, act_py, outfile)
-
-        builder = MyBuilder(action = cmd3, name = "cmd3")
-        r = builder.execute(target = ['aaa', 'bbb'])
-        assert r == 0
-        c = test.read(outfile, 'r')
-        assert c == "act.py: 'aaa' 'bbb'\n", c
-
-        cmd4 = r'%s %s %s $SOURCES' % (python, act_py, outfile)
-
-        builder = MyBuilder(action = cmd4, name = "cmd4")
-        r = builder.execute(source = ['one', 'two'])
-        assert r == 0
-        c = test.read(outfile, 'r')
-        assert c == "act.py: 'one' 'two'\n", c
-
-        cmd4 = r'%s %s %s ${SOURCES[:2]}' % (python, act_py, outfile)
-
-        builder = MyBuilder(action = cmd4, name = "cmd4")
-        r = builder.execute(source = ['three', 'four', 'five'])
-        assert r == 0
-        c = test.read(outfile, 'r')
-        assert c == "act.py: 'three' 'four'\n", c
-
-        cmd5 = r'%s %s %s $TARGET XYZZY' % (python, act_py, outfile)
-
-        builder = MyBuilder(action = cmd5, name = "cmd5")
-        r = builder.execute(target = 'out5', env = {'ENV' : {'XYZZY' : 'xyzzy'}})
-        assert r == 0
-        c = test.read(outfile, 'r')
-        assert c == "act.py: 'out5' 'XYZZY'\nact.py: 'xyzzy'\n", c
-
-        class Obj:
-            def __init__(self, str):
-                self._str = str
-            def __str__(self):
-                return self._str
-
-        cmd6 = r'%s %s %s ${TARGETS[1]} $TARGET ${SOURCES[:2]}' % (python, act_py, outfile)
-
-        builder = MyBuilder(action = cmd6, name = "cmd6")
-        r = builder.execute(target = [Obj('111'), Obj('222')],
-                            source = [Obj('333'), Obj('444'), Obj('555')])
-        assert r == 0
-        c = test.read(outfile, 'r')
-        assert c == "act.py: '222' '111' '333' '444'\n", c
-
-        cmd7 = '%s %s %s one\n\n%s %s %s two' % (python, act_py, outfile,
-                                                 python, act_py, outfile)
-        expect7 = '%s %s %s one\n%s %s %s two\n' % (python, act_py, outfile,
-                                                    python, act_py, outfile)
-
-        builder = MyBuilder(action = cmd7, name = "cmd7")
-
-        global show_string
-        show_string = ""
-        def my_show(string):
-            global show_string
-            show_string = show_string + string + "\n"
-        for action in builder.action.list:
-            action.show = my_show
-
-        r = builder.execute()
-        assert r == 0
-        assert show_string == expect7, show_string
-
-        global count
-        count = 0
-        def function1(**kw):
-            global count
-            count = count + 1
-            if not type(kw['target']) is type([]):
-                kw['target'] = [ kw['target'] ]
-            for t in kw['target']:
-                open(t, 'w').write("function1\n")
-            return 1
-
-        builder = MyBuilder(action = function1, name = "function1")
-        try:
-            r = builder.execute(target = [outfile, outfile2])
-        except SCons.Errors.BuildError:
-            pass
-        assert r == 1
-        assert count == 1
-        c = test.read(outfile, 'r')
-        assert c == "function1\n", c
-        c = test.read(outfile2, 'r')
-        assert c == "function1\n", c
-
-        class class1a:
-            def __init__(self, **kw):
-                open(kw['out'], 'w').write("class1a\n")
-
-        builder = MyBuilder(action = class1a, name = "class1a")
-        r = builder.execute(out = outfile)
-        assert r.__class__ == class1a
-        c = test.read(outfile, 'r')
-        assert c == "class1a\n", c
-
-        class class1b:
-            def __call__(self, **kw):
-                open(kw['out'], 'w').write("class1b\n")
-                return 2
-
-        builder = MyBuilder(action = class1b(), name = "class1b")
-        r = builder.execute(out = outfile)
-        assert r == 2
-        c = test.read(outfile, 'r')
-        assert c == "class1b\n", c
-
-        cmd2 = r'%s %s %s syzygy' % (python, act_py, outfile)
-
-        def function2(**kw):
-            open(kw['out'], 'a').write("function2\n")
-            return 0
-
-        class class2a:
-            def __call__(self, **kw):
-                open(kw['out'], 'a').write("class2a\n")
-                return 0
-
-        class class2b:
-            def __init__(self, **kw):
-                open(kw['out'], 'a').write("class2b\n")
-
-        builder = MyBuilder(action = SCons.Action.ListAction([cmd2, function2, class2a(), class2b]), name = "clist")
-        r = builder.execute(out = outfile)
-        assert r.__class__ == class2b
-        c = test.read(outfile, 'r')
-        assert c == "act.py: 'syzygy'\nfunction2\nclass2a\nclass2b\n", c
-
-        if os.name == 'nt':
-            # NT treats execs of directories and non-executable files
-            # as "file not found" errors
-            expect_nonexistent = 1
-            expect_nonexecutable = 1
-        else:
-            expect_nonexistent = 127
-            expect_nonexecutable = 126
-
-        # Test that a nonexistent command returns 127
-        builder = MyBuilder(action = python + "_XyZzY_", name="badcmd")
-        r = builder.execute(out = outfile)
-        assert r == expect_nonexistent, "r == %d" % r
-
-        # Test that trying to execute a directory returns 126
-        dir, tail = os.path.split(python)
-        builder = MyBuilder(action = dir, name = "dir")
-        r = builder.execute(out = outfile)
-        assert r == expect_nonexecutable, "r == %d" % r
-
-        # Test that trying to execute a non-executable file returns 126
-        builder = MyBuilder(action = outfile, name = "badfile")
-        r = builder.execute(out = outfile)
-        assert r == expect_nonexecutable, "r == %d" % r
-
-    def test_get_contents(self):
-        """Test returning the signature contents of a Builder
-        """
-
-        b1 = SCons.Builder.Builder(name = "b1", action = "foo")
-        contents = b1.get_contents()
-        assert contents == "foo", contents
-
-        b2 = SCons.Builder.Builder(name = "b2", action = Func)
-        contents = b2.get_contents()
-        assert contents == "\177\036\000\177\037\000d\000\000S", repr(contents)
-
-        b3 = SCons.Builder.Builder(name = "b3", action = SCons.Action.ListAction(["foo", Func, "bar"]))
-        contents = b3.get_contents()
-        assert contents == "foo\177\036\000\177\037\000d\000\000Sbar", repr(contents)
-
-    def test_node_factory(self):
-        """Test a Builder that creates nodes of a specified class
-        """
-        class Foo:
-            pass
-        def FooFactory(target):
-            global Foo
-            return Foo(target)
-        builder = SCons.Builder.Builder(name = "builder", node_factory = FooFactory)
-        assert builder.target_factory is FooFactory
-        assert builder.source_factory is FooFactory
-
     def test_target_factory(self):
         """Test a Builder that creates target nodes of a specified class
         """
@@ -430,7 +428,7 @@ class BuilderTestCase(unittest.TestCase):
         def FooFactory(target):
             global Foo
             return Foo(target)
-        builder = SCons.Builder.Builder(name = "builder", target_factory = FooFactory)
+        builder = SCons.Builder.Builder(target_factory = FooFactory)
         assert builder.target_factory is FooFactory
         assert not builder.source_factory is FooFactory
 
@@ -442,27 +440,112 @@ class BuilderTestCase(unittest.TestCase):
         def FooFactory(source):
             global Foo
             return Foo(source)
-        builder = SCons.Builder.Builder(name = "builder", source_factory = FooFactory)
+        builder = SCons.Builder.Builder(source_factory = FooFactory)
         assert not builder.target_factory is FooFactory
         assert builder.source_factory is FooFactory
 
+    def test_splitext(self):
+        """Test the splitext() method attached to a Builder."""
+        b = SCons.Builder.Builder()
+        assert b.splitext('foo') == ('foo','')
+        assert b.splitext('foo.bar') == ('foo','.bar')
+        assert b.splitext(os.path.join('foo.bar', 'blat')) == (os.path.join('foo.bar', 'blat'),'')
+
+        class MyBuilder(SCons.Builder.BuilderBase):
+            def splitext(self, path):
+                return "called splitext()"
+
+        b = MyBuilder()
+        ret = b.splitext('xyz.c')
+        assert ret == "called splitext()", ret
+
+    def test_adjust_suffix(self):
+        """Test how a Builder adjusts file suffixes
+        """
+        b = SCons.Builder.Builder()
+        assert b.adjust_suffix('.foo') == '.foo'
+        assert b.adjust_suffix('foo') == '.foo'
+        assert b.adjust_suffix('$foo') == '$foo'
+
+        class MyBuilder(SCons.Builder.BuilderBase):
+            def adjust_suffix(self, suff):
+                return "called adjust_suffix()"
+
+        b = MyBuilder()
+        ret = b.adjust_suffix('.foo')
+        assert ret == "called adjust_suffix()", ret
+
     def test_prefix(self):
         """Test Builder creation with a specified target prefix
 
         Make sure that there is no '.' separator appended.
         """
-        builder = SCons.Builder.Builder(name = "builder", prefix = 'lib.')
-        assert builder.get_prefix(env,{}) == 'lib.'
-        builder = SCons.Builder.Builder(name = "builder", prefix = 'lib')
-        assert builder.get_prefix(env,{}) == 'lib'
-        tgt = builder(env, target = 'tgt1', source = 'src1')
+        env = Environment()
+        builder = SCons.Builder.Builder(prefix = 'lib.')
+        assert builder.get_prefix(env) == 'lib.'
+        builder = SCons.Builder.Builder(prefix = 'lib', action='')
+        assert builder.get_prefix(env) == 'lib'
+        tgt = builder(env, target = 'tgt1', source = 'src1')[0]
         assert tgt.path == 'libtgt1', \
                 "Target has unexpected name: %s" % tgt.path
-        tgts = builder(env, target = 'tgt2a tgt2b', source = 'src2')
-        assert tgts[0].path == 'libtgt2a', \
-                "Target has unexpected name: %s" % tgts[0].path
-        assert tgts[1].path == 'libtgt2b', \
-                "Target has unexpected name: %s" % tgts[1].path
+        tgt = builder(env, target = 'tgt2a tgt2b', source = 'src2')[0]
+        assert tgt.path == 'libtgt2a tgt2b', \
+                "Target has unexpected name: %s" % tgt.path
+        tgt = builder(env, target = None, source = 'src3')[0]
+        assert tgt.path == 'libsrc3', \
+                "Target has unexpected name: %s" % tgt.path
+        tgt = builder(env, target = None, source = 'lib/src4')[0]
+        assert tgt.path == os.path.join('lib', 'libsrc4'), \
+                "Target has unexpected name: %s" % tgt.path
+        tgt = builder(env, target = 'lib/tgt5', source = 'lib/src5')[0]
+        assert tgt.path == os.path.join('lib', 'libtgt5'), \
+                "Target has unexpected name: %s" % tgt.path
+
+        def gen_prefix(env, sources):
+            return "gen_prefix() says " + env['FOO']
+        my_env = Environment(FOO = 'xyzzy')
+        builder = SCons.Builder.Builder(prefix = gen_prefix)
+        assert builder.get_prefix(my_env) == "gen_prefix() says xyzzy"
+        my_env['FOO'] = 'abracadabra'
+        assert builder.get_prefix(my_env) == "gen_prefix() says abracadabra"
+
+        def my_emit(env, sources):
+            return env.subst('$EMIT')
+        my_env = Environment(FOO = '.foo', EMIT = 'emit-')
+        builder = SCons.Builder.Builder(prefix = {None   : 'default-',
+                                                  '.in'  : 'out-',
+                                                  '.x'   : 'y-',
+                                                  '$FOO' : 'foo-',
+                                                  '.zzz' : my_emit},
+                                        action = '')
+        tgt = builder(my_env, target = None, source = 'f1')[0]
+        assert tgt.path == 'default-f1', tgt.path
+        tgt = builder(my_env, target = None, source = 'f2.c')[0]
+        assert tgt.path == 'default-f2', tgt.path
+        tgt = builder(my_env, target = None, source = 'f3.in')[0]
+        assert tgt.path == 'out-f3', tgt.path
+        tgt = builder(my_env, target = None, source = 'f4.x')[0]
+        assert tgt.path == 'y-f4', tgt.path
+        tgt = builder(my_env, target = None, source = 'f5.foo')[0]
+        assert tgt.path == 'foo-f5', tgt.path
+        tgt = builder(my_env, target = None, source = 'f6.zzz')[0]
+        assert tgt.path == 'emit-f6', tgt.path
+
+    def test_set_suffix(self):
+        """Test the set_suffix() method"""
+        b = SCons.Builder.Builder(action='')
+        env = Environment(XSUFFIX = '.x')
+
+        s = b.get_suffix(env)
+        assert s == '', s
+
+        b.set_suffix('.foo')
+        s = b.get_suffix(env)
+        assert s == '.foo', s
+
+        b.set_suffix('$XSUFFIX')
+        s = b.get_suffix(env)
+        assert s == '.x', s
 
     def test_src_suffix(self):
         """Test Builder creation with a specified source file suffix
@@ -472,35 +555,73 @@ class BuilderTestCase(unittest.TestCase):
         """
         env = Environment(XSUFFIX = '.x', YSUFFIX = '.y')
 
-        b1 = SCons.Builder.Builder(name = "builder", src_suffix = '.c')
-        assert b1.src_suffixes(env,{}) == ['.c'], b1.src_suffixes(env,{})
+        b1 = SCons.Builder.Builder(src_suffix = '.c', action='')
+        assert b1.src_suffixes(env) == ['.c'], b1.src_suffixes(env)
 
-        tgt = b1(env, target = 'tgt2', source = 'src2')
+        tgt = b1(env, target = 'tgt2', source = 'src2')[0]
         assert tgt.sources[0].path == 'src2.c', \
                 "Source has unexpected name: %s" % tgt.sources[0].path
 
-        tgt = b1(env, target = 'tgt3', source = 'src3a src3b')
-        assert tgt.sources[0].path == 'src3a.c', \
+        tgt = b1(env, target = 'tgt3', source = 'src3a src3b')[0]
+        assert len(tgt.sources) == 1
+        assert tgt.sources[0].path == 'src3a src3b.c', \
                 "Unexpected tgt.sources[0] name: %s" % tgt.sources[0].path
-        assert tgt.sources[1].path == 'src3b.c', \
-                "Unexpected tgt.sources[1] name: %s" % tgt.sources[1].path
-
-        b2 = SCons.Builder.Builder(name = "b2",
-                                   src_suffix = '.2',
-                                   src_builder = b1)
-        assert b2.src_suffixes(env,{}) == ['.2', '.c'], b2.src_suffixes(env,{})
-
-        b3 = SCons.Builder.Builder(name = "b3",
-                                   action = {'.3a' : '', '.3b' : ''})
-        s = b3.src_suffixes(env,{})
-        s.sort()
-        assert s == ['.3a', '.3b'], s
 
-        b4 = SCons.Builder.Builder(name = "b4", src_suffix = '$XSUFFIX')
-        assert b4.src_suffixes(env,{}) == ['.x'], b4.src_suffixes(env,{})
+        b2 = SCons.Builder.Builder(src_suffix = '.2', src_builder = b1)
+        r = sorted(b2.src_suffixes(env))
+        assert r == ['.2', '.c'], r
+
+        b3 = SCons.Builder.Builder(action = {'.3a' : '', '.3b' : ''})
+        s = sorted(b3.src_suffixes(env))
+        assert s == ['.3a', '.3b'], s
 
-        b5 = SCons.Builder.Builder(name = "b5", action = {'$YSUFFIX' : ''})
-        assert b5.src_suffixes(env,{}) == ['.y'], b5.src_suffixes(env,{})
+        b4 = SCons.Builder.Builder(src_suffix = '$XSUFFIX')
+        assert b4.src_suffixes(env) == ['.x'], b4.src_suffixes(env)
+
+        b5 = SCons.Builder.Builder(action = { '.y' : ''})
+        assert b5.src_suffixes(env) == ['.y'], b5.src_suffixes(env)
+
+    def test_srcsuffix_nonext(self):
+        "Test target generation from non-extension source suffixes"
+        env = Environment()
+        b6 = SCons.Builder.Builder(action = '',
+                                   src_suffix='_src.a',
+                                   suffix='.b')
+        tgt = b6(env, target=None, source='foo_src.a')
+        assert str(tgt[0]) == 'foo.b', str(tgt[0])
+
+        b7 = SCons.Builder.Builder(action = '',
+                                   src_suffix='_source.a',
+                                   suffix='_obj.b')
+        b8 = SCons.Builder.Builder(action = '',
+                                   src_builder=b7,
+                                   suffix='.c')
+        tgt = b8(env, target=None, source='foo_source.a')
+        assert str(tgt[0]) == 'foo_obj.c', str(tgt[0])
+        src = env.fs.File('foo_source.a')
+        tgt = b8(env, target=None, source=src)
+        assert str(tgt[0]) == 'foo_obj.c', str(tgt[0])
+
+        b9 = SCons.Builder.Builder(action={'_src.a' : 'srcaction'},
+                                   suffix='.c')
+        b9.add_action('_altsrc.b', 'altaction')
+        tgt = b9(env, target=None, source='foo_altsrc.b')
+        assert str(tgt[0]) == 'foo.c', str(tgt[0])
+
+    def test_src_suffix_expansion(self):
+        """Test handling source suffixes when an expansion is involved"""
+        env = Environment(OBJSUFFIX = '.obj')
+
+        b1 = SCons.Builder.Builder(action = '',
+                                   src_suffix='.c',
+                                   suffix='.obj')
+        b2 = SCons.Builder.Builder(action = '',
+                                   src_builder=b1,
+                                   src_suffix='.obj',
+                                   suffix='.exe')
+        tgt = b2(env, target=None, source=['foo$OBJSUFFIX'])
+        s = list(map(str, tgt[0].sources))
+        assert s == ['foo.obj'], s
 
     def test_suffix(self):
         """Test Builder creation with a specified target suffix
@@ -508,69 +629,164 @@ class BuilderTestCase(unittest.TestCase):
         Make sure that the '.' separator is appended to the
         beginning if it isn't already present.
         """
-        builder = SCons.Builder.Builder(name = "builder", suffix = '.o')
-        assert builder.get_suffix(env,{}) == '.o', builder.get_suffix(env,{})
-        builder = SCons.Builder.Builder(name = "builder", suffix = 'o')
-        assert builder.get_suffix(env,{}) == '.o', builder.get_suffix(env,{})
-        tgt = builder(env, target = 'tgt3', source = 'src3')
+        env = Environment()
+        builder = SCons.Builder.Builder(suffix = '.o')
+        assert builder.get_suffix(env) == '.o', builder.get_suffix(env)
+        builder = SCons.Builder.Builder(suffix = 'o', action='')
+        assert builder.get_suffix(env) == '.o', builder.get_suffix(env)
+        tgt = builder(env, target = 'tgt3', source = 'src3')[0]
         assert tgt.path == 'tgt3.o', \
-                "Target has unexpected name: %s" % tgt[0].path
-        tgts = builder(env, target = 'tgt4a tgt4b', source = 'src4')
-        assert tgts[0].path == 'tgt4a.o', \
-                "Target has unexpected name: %s" % tgts[0].path
-        tgts = builder(env, target = 'tgt4a tgt4b', source = 'src4')
-        assert tgts[1].path == 'tgt4b.o', \
-                "Target has unexpected name: %s" % tgts[1].path
-
-    def test_ListBuilder(self):
-        """Testing ListBuilder class."""
-        global count
-        count = 0
-        def function2(tlist = [outfile, outfile2], **kw):
-            global count
-            count = count + 1
-            for t in kw['target']:
+                "Target has unexpected name: %s" % tgt.path
+        tgt = builder(env, target = 'tgt4a tgt4b', source = 'src4')[0]
+        assert tgt.path == 'tgt4a tgt4b.o', \
+                "Target has unexpected name: %s" % tgt.path
+        tgt = builder(env, target = None, source = 'src5')[0]
+        assert tgt.path == 'src5.o', \
+                "Target has unexpected name: %s" % tgt.path
+
+        def gen_suffix(env, sources):
+            return "gen_suffix() says " + env['BAR']
+        my_env = Environment(BAR = 'hocus pocus')
+        builder = SCons.Builder.Builder(suffix = gen_suffix)
+        assert builder.get_suffix(my_env) == "gen_suffix() says hocus pocus", builder.get_suffix(my_env)
+        my_env['BAR'] = 'presto chango'
+        assert builder.get_suffix(my_env) == "gen_suffix() says presto chango"
+
+        def my_emit(env, sources):
+            return env.subst('$EMIT')
+        my_env = Environment(BAR = '.bar', EMIT = '.emit')
+        builder = SCons.Builder.Builder(suffix = {None   : '.default',
+                                                  '.in'  : '.out',
+                                                  '.x'   : '.y',
+                                                  '$BAR' : '.new',
+                                                  '.zzz' : my_emit},
+                                        action='')
+        tgt = builder(my_env, target = None, source = 'f1')[0]
+        assert tgt.path == 'f1.default', tgt.path
+        tgt = builder(my_env, target = None, source = 'f2.c')[0]
+        assert tgt.path == 'f2.default', tgt.path
+        tgt = builder(my_env, target = None, source = 'f3.in')[0]
+        assert tgt.path == 'f3.out', tgt.path
+        tgt = builder(my_env, target = None, source = 'f4.x')[0]
+        assert tgt.path == 'f4.y', tgt.path
+        tgt = builder(my_env, target = None, source = 'f5.bar')[0]
+        assert tgt.path == 'f5.new', tgt.path
+        tgt = builder(my_env, target = None, source = 'f6.zzz')[0]
+        assert tgt.path == 'f6.emit', tgt.path
+
+    def test_single_source(self):
+        """Test Builder with single_source flag set"""
+        def func(target, source, env):
+            open(str(target[0]), "w")
+            if (len(source) == 1 and len(target) == 1):
+                env['CNT'][0] = env['CNT'][0] + 1
+                
+        env = Environment()
+        infiles = []
+        outfiles = []
+        for i in range(10):
+            infiles.append(test.workpath('%d.in' % i))
+            outfiles.append(test.workpath('%d.out' % i))
+            test.write(infiles[-1], "\n")
+        builder = SCons.Builder.Builder(action=SCons.Action.Action(func,None),
+                                        single_source = 1, suffix='.out')
+        env['CNT'] = [0]
+        tgt = builder(env, target=outfiles[0], source=infiles[0])[0]
+        s = str(tgt)
+        t = os.path.normcase(test.workpath('0.out'))
+        assert os.path.normcase(s) == t, s
+        tgt.prepare()
+        tgt.build()
+        assert env['CNT'][0] == 1, env['CNT'][0]
+        tgt = builder(env, outfiles[1], infiles[1])[0]
+        s = str(tgt)
+        t = os.path.normcase(test.workpath('1.out'))
+        assert os.path.normcase(s) == t, s
+        tgt.prepare()
+        tgt.build()
+        assert env['CNT'][0] == 2
+        tgts = builder(env, None, infiles[2:4])
+        try:
+            [].extend(UserList.UserList())
+        except TypeError:
+            # Old Python version (1.5.2) that can't handle extending
+            # a list with list-like objects.  That means the return
+            # value from the builder call is a real list with Nodes,
+            # and doesn't have a __str__() method that stringifies
+            # the individual elements.  Since we're gong to drop 1.5.2
+            # support anyway, don't bother trying to test for it.
+            pass
+        else:
+            s = list(map(str, tgts))
+            expect = [test.workpath('2.out'), test.workpath('3.out')]
+            expect = list(map(os.path.normcase, expect))
+            assert list(map(os.path.normcase, s)) == expect, s
+        for t in tgts: t.prepare()
+        tgts[0].build()
+        tgts[1].build()
+        assert env['CNT'][0] == 4
+        try:
+            tgt = builder(env, outfiles[4], infiles[4:6])
+        except SCons.Errors.UserError:
+            pass
+        else:
+            assert 0
+        try:
+            # The builder may output more than one target per input file.
+            tgt = builder(env, outfiles[4:6], infiles[4:6])
+        except SCons.Errors.UserError:
+            pass
+        else:
+            assert 0
+        
+        
+    def test_lists(self):
+        """Testing handling lists of targets and source"""
+        def function2(target, source, env, tlist = [outfile, outfile2], **kw):
+            for t in target:
                 open(str(t), 'w').write("function2\n")
             for t in tlist:
-                if not t in map(str, kw['target']):
+                if not t in list(map(str, target)):
                     open(t, 'w').write("function2\n")
             return 1
 
-        builder = SCons.Builder.Builder(action = function2, name = "function2")
-        tgts = builder(env, target = [outfile, outfile2], source = 'foo')
+        env = Environment()
+        builder = SCons.Builder.Builder(action = function2)
+
+        tgts = builder(env, source=[])
+        assert tgts == [], tgts
+
+        tgts = builder(env, target = [outfile, outfile2], source = infile)
+        for t in tgts:
+            t.prepare()
         try:
-            r = tgts[0].builder.execute(target = tgts)
+            tgts[0].build()
         except SCons.Errors.BuildError:
             pass
         c = test.read(outfile, 'r')
         assert c == "function2\n", c
         c = test.read(outfile2, 'r')
         assert c == "function2\n", c
-        r = tgts[1].builder.execute(target = tgts[1])
-        assert r == 1, r
-        assert count == 1, count
 
         sub1_out = test.workpath('sub1', 'out')
         sub2_out = test.workpath('sub2', 'out')
 
-        count = 0
-        def function3(tlist = [sub1_out, sub2_out], **kw):
-            global count
-            count = count + 1
-            for t in kw['target']:
+        def function3(target, source, env, tlist = [sub1_out, sub2_out]):
+            for t in target:
                 open(str(t), 'w').write("function3\n")
             for t in tlist:
-                if not t in map(str, kw['target']):
+                if not t in list(map(str, target)):
                     open(t, 'w').write("function3\n")
             return 1
 
-        builder = SCons.Builder.Builder(action = function3, name = "function3")
-        tgts = builder(env, target = [sub1_out, sub2_out], source = 'foo')
+        builder = SCons.Builder.Builder(action = function3)
+        tgts = builder(env, target = [sub1_out, sub2_out], source = infile)
+        for t in tgts:
+            t.prepare()
         try:
-            r = tgts[0].builder.execute(target = tgts)
-        except:
+            tgts[0].build()
+        except SCons.Errors.BuildError:
             pass
-        assert r == 1, r
         c = test.read(sub1_out, 'r')
         assert c == "function3\n", c
         c = test.read(sub2_out, 'r')
@@ -578,204 +794,860 @@ class BuilderTestCase(unittest.TestCase):
         assert os.path.exists(test.workpath('sub1'))
         assert os.path.exists(test.workpath('sub2'))
 
-    def test_MultiStepBuilder(self):
-        """Testing MultiStepBuilder class."""
-        builder1 = SCons.Builder.Builder(name = "builder1",
-                                         action='foo',
+    def test_src_builder(self):
+        """Testing Builders with src_builder"""
+        # These used to be MultiStepBuilder objects until we
+        # eliminated it as a separate class
+        env = Environment()
+        builder1 = SCons.Builder.Builder(action='foo',
                                          src_suffix='.bar',
                                          suffix='.foo')
-        builder2 = SCons.Builder.MultiStepBuilder(name = "builder2",
-                                                  action='bar',
-                                                  src_builder = builder1,
-                                                  src_suffix = '.foo')
-        tgt = builder2(env, target='baz', source='test.bar test2.foo test3.txt')
-        assert str(tgt.sources[0]) == 'test.foo', str(tgt.sources[0])
-        assert str(tgt.sources[0].sources[0]) == 'test.bar', \
-               str(tgt.sources[0].sources[0])
-        assert str(tgt.sources[1]) == 'test2.foo', str(tgt.sources[1])
-        assert str(tgt.sources[2]) == 'test3.txt', str(tgt.sources[2])
-        
-    def test_CompositeBuilder(self):
-        """Testing CompositeBuilder class."""
-        def func_action(target, source, env):
-            return 0
-        
-        builder = SCons.Builder.Builder(name = "builder",
-                                        action={ '.foo' : func_action,
-                                                 '.bar' : func_action })
-        
-        assert isinstance(builder, SCons.Builder.BuilderBase)
-        assert isinstance(builder.action, SCons.Action.CommandGeneratorAction)
-        tgt = builder(env, target='test1', source='test1.foo')
-        assert isinstance(tgt.builder, SCons.Builder.BuilderBase)
-        assert isinstance(tgt.builder.action.generator, SCons.Builder.DictCmdGenerator)
-        flag = 0
-        tgt = builder(env, target='test2', source='test2.bar test1.foo')
-        try:
-            tgt.build()
-        except SCons.Errors.BuildError, e:
-            assert e.args[0] == SCons.Errors.UserError
-            flag = 1
-        assert flag, "UserError should be thrown when we build targets with files of different suffixes."
-
-        foo_bld = SCons.Builder.Builder(name = "foo_bld",
-                                        action = 'a-foo',
-                                        src_suffix = '.ina',
-                                        suffix = '.foo')
-        assert isinstance(foo_bld, SCons.Builder.BuilderBase)
-        builder = SCons.Builder.Builder(name = "builder",
-                                        action = { '.foo' : 'foo',
-                                                   '.bar' : 'bar' },
-                                        src_builder = foo_bld)
-        assert isinstance(builder, SCons.Builder.MultiStepBuilder)
-        assert isinstance(builder.action, SCons.Action.CommandGeneratorAction)
-
-        tgt = builder(env, target='t1', source='t1a.ina t1b.ina')
-        assert isinstance(tgt.builder, SCons.Builder.BuilderBase)
-
-        tgt = builder(env, target='t2', source='t2a.foo t2b.ina')
-        assert isinstance(tgt.builder, SCons.Builder.MultiStepBuilder), tgt.builder.__dict__
-
-        bar_bld = SCons.Builder.Builder(name = "bar_bld",
-                                        action = 'a-bar',
-                                        src_suffix = '.inb',
-                                        suffix = '.bar')
-        assert isinstance(bar_bld, SCons.Builder.BuilderBase)
-        builder = SCons.Builder.Builder(name = "builder",
-                                        action = { '.foo' : 'foo',
-                                                   '.bar' : 'bar' },
-                                        src_builder = [foo_bld, bar_bld])
-        assert isinstance(builder, SCons.Builder.MultiStepBuilder)
-        assert isinstance(builder.action, SCons.Action.CommandGeneratorAction)
-
-        tgt = builder(env, target='t3-foo', source='t3a.foo t3b.ina')
-        assert isinstance(tgt.builder, SCons.Builder.MultiStepBuilder)
-
-        tgt = builder(env, target='t3-bar', source='t3a.bar t3b.inb')
-        assert isinstance(tgt.builder, SCons.Builder.MultiStepBuilder)
-
-        flag = 0
-        tgt = builder(env, target='t5', source='test5a.foo test5b.inb')
-        try:
-            tgt.build()
-        except SCons.Errors.BuildError, e:
-            assert e.args[0] == SCons.Errors.UserError
-            flag = 1
-        assert flag, "UserError should be thrown when we build targets with files of different suffixes."
-
-        flag = 0
-        tgt = builder(env, target='t6', source='test6a.bar test6b.ina')
-        try:
-            tgt.build()
-        except SCons.Errors.BuildError, e:
-            assert e.args[0] == SCons.Errors.UserError
-            flag = 1
-        assert flag, "UserError should be thrown when we build targets with files of different suffixes."
-
-        flag = 0
-        tgt = builder(env, target='t4', source='test4a.ina test4b.inb')
-        try:
-            tgt.build()
-        except SCons.Errors.BuildError, e:
-            assert e.args[0] == SCons.Errors.UserError
-            flag = 1
-        assert flag, "UserError should be thrown when we build targets with files of different suffixes."
-
-
-    def test_build_scanner(self):
-        """Testing ability to set a target scanner through a builder."""
+        builder2 = SCons.Builder.Builder(action=MyAction('act'),
+                                         src_builder = builder1,
+                                         src_suffix = '.foo')
+
+        tgt = builder2(env, source=[])
+        assert tgt == [], tgt
+
+        sources = ['test.bar', 'test2.foo', 'test3.txt', 'test4']
+        tgt = builder2(env, target='baz', source=sources)[0]
+        s = str(tgt)
+        assert s == 'baz', s
+        s = list(map(str, tgt.sources))
+        assert s == ['test.foo', 'test2.foo', 'test3.txt', 'test4.foo'], s
+        s = list(map(str, tgt.sources[0].sources))
+        assert s == ['test.bar'], s
+
+        tgt = builder2(env, None, 'aaa.bar')[0]
+        s = str(tgt)
+        assert s == 'aaa', s
+        s = list(map(str, tgt.sources))
+        assert s == ['aaa.foo'], s
+        s = list(map(str, tgt.sources[0].sources))
+        assert s == ['aaa.bar'], s
+
+        builder3 = SCons.Builder.Builder(action='bld3')
+        assert not builder3.src_builder is builder1.src_builder
+
+        builder4 = SCons.Builder.Builder(action='bld4',
+                                         src_suffix='.i',
+                                         suffix='_wrap.c')
+        builder5 = SCons.Builder.Builder(action=MyAction('act'),
+                                         src_builder=builder4,
+                                         suffix='.obj',
+                                         src_suffix='.c')
+        builder6 = SCons.Builder.Builder(action=MyAction('act'),
+                                         src_builder=builder5,
+                                         suffix='.exe',
+                                         src_suffix='.obj')
+        tgt = builder6(env, 'test', 'test.i')[0]
+        s = str(tgt)
+        assert s == 'test.exe', s
+        s = list(map(str, tgt.sources))
+        assert s == ['test_wrap.obj'], s
+        s = list(map(str, tgt.sources[0].sources))
+        assert s == ['test_wrap.c'], s
+        s = list(map(str, tgt.sources[0].sources[0].sources))
+        assert s == ['test.i'], s
+
+    def test_target_scanner(self):
+        """Testing ability to set target and source scanners through a builder."""
         global instanced
         class TestScanner:
             pass
-        scn = TestScanner()
-        builder = SCons.Builder.Builder(name = "builder", scanner=scn)
-        tgt = builder(env, target='foo2', source='bar')
-        assert tgt.target_scanner == scn, tgt.target_scanner
-
-        builder1 = SCons.Builder.Builder(name = "builder1",
-                                         action='foo',
+        tscan = TestScanner()
+        sscan = TestScanner()
+        env = Environment()
+        builder = SCons.Builder.Builder(target_scanner=tscan,
+                                        source_scanner=sscan,
+                                        action='')
+        tgt = builder(env, target='foo2', source='bar')[0]
+        assert tgt.builder.target_scanner == tscan, tgt.builder.target_scanner
+        assert tgt.builder.source_scanner == sscan, tgt.builder.source_scanner
+
+        builder1 = SCons.Builder.Builder(action='foo',
                                          src_suffix='.bar',
                                          suffix='.foo')
-        builder2 = SCons.Builder.Builder(name = "builder2",
-                                         action='foo',
+        builder2 = SCons.Builder.Builder(action='foo',
                                          src_builder = builder1,
-                                         scanner = scn)
-        tgt = builder2(env, target='baz2', source='test.bar test2.foo test3.txt')
-        assert tgt.target_scanner == scn, tgt.target_scanner
+                                         target_scanner = tscan,
+                                         source_scanner = tscan)
+        tgt = builder2(env, target='baz2', source='test.bar test2.foo test3.txt')[0]
+        assert tgt.builder.target_scanner == tscan, tgt.builder.target_scanner
+        assert tgt.builder.source_scanner == tscan, tgt.builder.source_scanner
 
+    def test_actual_scanner(self):
+        """Test usage of actual Scanner objects."""
+
+        import SCons.Scanner
+
+        def func(self):
+            pass
+        
+        scanner = SCons.Scanner.Base(func, name='fooscan')
+
+        b1 = SCons.Builder.Builder(action='bld', target_scanner=scanner)
+        b2 = SCons.Builder.Builder(action='bld', target_scanner=scanner)
+        b3 = SCons.Builder.Builder(action='bld')
+
+        assert b1 == b2
+        assert b1 != b3
+        
     def test_src_scanner(slf):
         """Testing ability to set a source file scanner through a builder."""
-        global env_scanner
         class TestScanner:
             def key(self, env):
                  return 'TestScannerkey'
             def instance(self, env):
                  return self
-        env_scanner = TestScanner()
-        builder = SCons.Builder.Builder(name = "builder", action='action')
-        tgt = builder(env, target='foo.x', source='bar')
+            def select(self, node):
+                 return self
+            name = 'TestScanner'
+            def __str__(self):
+                return self.name
+
+        scanner = TestScanner()
+        builder = SCons.Builder.Builder(action='action')
+
+        # With no scanner specified, source_scanner and
+        # backup_source_scanner are None.
+        bar_y = MyNode('bar.y')
+        env1 = Environment()
+        tgt = builder(env1, target='foo1.x', source='bar.y')[0]
+        src = tgt.sources[0]
+        assert tgt.builder.target_scanner != scanner, tgt.builder.target_scanner
+        assert tgt.builder.source_scanner is None, tgt.builder.source_scanner
+        assert tgt.get_source_scanner(bar_y) is None, tgt.get_source_scanner(bar_y)
+        assert not src.has_builder(), src.has_builder()
+        s = src.get_source_scanner(bar_y)
+        assert isinstance(s, SCons.Util.Null), repr(s)
+
+        # An Environment that has suffix-specified SCANNERS should
+        # provide a source scanner to the target.
+        class EnvTestScanner:
+            def key(self, env):
+                 return '.y'
+            def instance(self, env):
+                 return self
+            name = 'EnvTestScanner'
+            def __str__(self):
+                return self.name
+            def select(self, node):
+                return self
+            def path(self, env, dir=None):
+                return ()
+            def __call__(self, node, env, path):
+                return []
+        env3 = Environment(SCANNERS = [EnvTestScanner()])
+        env3.scanner = EnvTestScanner() # test env's version of SCANNERS
+        tgt = builder(env3, target='foo2.x', source='bar.y')[0]
+        src = tgt.sources[0]
+        assert tgt.builder.target_scanner != scanner, tgt.builder.target_scanner
+        assert not tgt.builder.source_scanner, tgt.builder.source_scanner
+        assert tgt.get_source_scanner(bar_y), tgt.get_source_scanner(bar_y)
+        assert str(tgt.get_source_scanner(bar_y)) == 'EnvTestScanner', tgt.get_source_scanner(bar_y)
+        assert not src.has_builder(), src.has_builder()
+        s = src.get_source_scanner(bar_y)
+        assert isinstance(s, SCons.Util.Null), repr(s)
+
+        # Can't simply specify the scanner as a builder argument; it's
+        # global to all invocations of this builder.
+        tgt = builder(env3, target='foo3.x', source='bar.y', source_scanner = scanner)[0]
         src = tgt.sources[0]
-        assert tgt.target_scanner != env_scanner, tgt.target_scanner
-        assert src.source_scanner == env_scanner
+        assert tgt.builder.target_scanner != scanner, tgt.builder.target_scanner
+        assert not tgt.builder.source_scanner, tgt.builder.source_scanner
+        assert tgt.get_source_scanner(bar_y), tgt.get_source_scanner(bar_y)
+        assert str(tgt.get_source_scanner(bar_y)) == 'EnvTestScanner', tgt.get_source_scanner(bar_y)
+        assert not src.has_builder(), src.has_builder()
+        s = src.get_source_scanner(bar_y)
+        assert isinstance(s, SCons.Util.Null), s
+
+        # Now use a builder that actually has scanners and ensure that
+        # the target is set accordingly (using the specified scanner
+        # instead of the Environment's scanner)
+        builder = SCons.Builder.Builder(action='action',
+                                        source_scanner=scanner,
+                                        target_scanner=scanner)
+        tgt = builder(env3, target='foo4.x', source='bar.y')[0]
+        src = tgt.sources[0]
+        assert tgt.builder.target_scanner == scanner, tgt.builder.target_scanner
+        assert tgt.builder.source_scanner, tgt.builder.source_scanner
+        assert tgt.builder.source_scanner == scanner, tgt.builder.source_scanner
+        assert str(tgt.builder.source_scanner) == 'TestScanner', str(tgt.builder.source_scanner)
+        assert tgt.get_source_scanner(bar_y), tgt.get_source_scanner(bar_y)
+        assert tgt.get_source_scanner(bar_y) == scanner, tgt.get_source_scanner(bar_y)
+        assert str(tgt.get_source_scanner(bar_y)) == 'TestScanner', tgt.get_source_scanner(bar_y)
+        assert not src.has_builder(), src.has_builder()
+        s = src.get_source_scanner(bar_y)
+        assert isinstance(s, SCons.Util.Null), s
+
+
+
+    def test_Builder_API(self):
+        """Test Builder interface.
+
+        Some of this is tested elsewhere in this file, but this is a
+        quick collection of common operations on builders with various
+        forms of component specifications."""
+
+        builder = SCons.Builder.Builder()
+        env = Environment(BUILDERS={'Bld':builder})
+
+        r = builder.get_name(env)
+        assert r == 'Bld', r
+        r = builder.get_prefix(env)
+        assert r == '', r
+        r = builder.get_suffix(env)
+        assert r == '', r
+        r = builder.get_src_suffix(env)
+        assert r == '', r
+        r = builder.src_suffixes(env)
+        assert r == [], r
+
+        # src_suffix can be a single string or a list of strings
+        # src_suffixes() caches its return value, so we use a new
+        # Builder each time we do any of these tests
+
+        bld = SCons.Builder.Builder()
+        env = Environment(BUILDERS={'Bld':bld})
+
+        bld.set_src_suffix('.foo')
+        r = bld.get_src_suffix(env)
+        assert r == '.foo', r
+        r = bld.src_suffixes(env)
+        assert r == ['.foo'], r
+
+        bld = SCons.Builder.Builder()
+        env = Environment(BUILDERS={'Bld':bld})
+
+        bld.set_src_suffix(['.foo', '.bar'])
+        r = bld.get_src_suffix(env)
+        assert r == '.foo', r
+        r = bld.src_suffixes(env)
+        assert r == ['.foo', '.bar'], r
+
+        bld = SCons.Builder.Builder()
+        env = Environment(BUILDERS={'Bld':bld})
+
+        bld.set_src_suffix(['.bar', '.foo'])
+        r = bld.get_src_suffix(env)
+        assert r == '.bar', r
+        r = sorted(bld.src_suffixes(env))
+        assert r == ['.bar', '.foo'], r
+
+        # adjust_suffix normalizes the suffix, adding a `.' if needed
+
+        r = builder.adjust_suffix('.foo')
+        assert r == '.foo', r
+        r = builder.adjust_suffix('_foo')
+        assert r == '_foo', r
+        r = builder.adjust_suffix('$foo')
+        assert r == '$foo', r
+        r = builder.adjust_suffix('foo')
+        assert r == '.foo', r
+        r = builder.adjust_suffix('f._$oo')
+        assert r == '.f._$oo', r
+
+        # prefix and suffix can be one of:
+        #   1. a string (adjusted and env variables substituted),
+        #   2. a function (passed (env,sources), returns suffix string)
+        #   3. a dict of src_suffix:suffix settings, key==None is
+        #      default suffix (special case of #2, so adjust_suffix
+        #      not applied)
+
+        builder = SCons.Builder.Builder(prefix='lib', suffix='foo')
+
+        env = Environment(BUILDERS={'Bld':builder})
+        r = builder.get_name(env)
+        assert r == 'Bld', r
+        r = builder.get_prefix(env)
+        assert r == 'lib', r
+        r = builder.get_suffix(env)
+        assert r == '.foo', r
+
+        mkpref = lambda env,sources: 'Lib'
+        mksuff = lambda env,sources: '.Foo'
+        builder = SCons.Builder.Builder(prefix=mkpref, suffix=mksuff)
+
+        env = Environment(BUILDERS={'Bld':builder})
+        r = builder.get_name(env)
+        assert r == 'Bld', r
+        r = builder.get_prefix(env)
+        assert r == 'Lib', r
+        r = builder.get_suffix(env)
+        assert r == '.Foo', r
+
+        builder = SCons.Builder.Builder(prefix='$PREF', suffix='$SUFF')
+
+        env = Environment(BUILDERS={'Bld':builder},PREF="LIB",SUFF=".FOO")
+        r = builder.get_name(env)
+        assert r == 'Bld', r
+        r = builder.get_prefix(env)
+        assert r == 'LIB', r
+        r = builder.get_suffix(env)
+        assert r == '.FOO', r
+
+        builder = SCons.Builder.Builder(prefix={None:'A_',
+                                                '.C':'E_'},
+                                        suffix={None:'.B',
+                                                '.C':'.D'})
+
+        env = Environment(BUILDERS={'Bld':builder})
+        r = builder.get_name(env)
+        assert r == 'Bld', r
+        r = builder.get_prefix(env)
+        assert r == 'A_', r
+        r = builder.get_suffix(env)
+        assert r == '.B', r
+        r = builder.get_prefix(env, [MyNode('X.C')])
+        assert r == 'E_', r
+        r = builder.get_suffix(env, [MyNode('X.C')])
+        assert r == '.D', r
+
+        builder = SCons.Builder.Builder(prefix='A_', suffix={}, action={})
+        env = Environment(BUILDERS={'Bld':builder})
+
+        r = builder.get_name(env)
+        assert r == 'Bld', r
+        r = builder.get_prefix(env)
+        assert r == 'A_', r
+        r = builder.get_suffix(env)
+        assert r is None, r
+        r = builder.get_src_suffix(env)
+        assert r == '', r
+        r = builder.src_suffixes(env)
+        assert r == [], r
+
+        # Builder actions can be a string, a list, or a dictionary
+        # whose keys are the source suffix.  The add_action()
+        # specifies a new source suffix/action binding.
+
+        builder = SCons.Builder.Builder(prefix='A_', suffix={}, action={})
+        env = Environment(BUILDERS={'Bld':builder})
+        builder.add_action('.src_sfx1', 'FOO')
+
+        r = builder.get_name(env)
+        assert r == 'Bld', r
+        r = builder.get_prefix(env)
+        assert r == 'A_', r
+        r = builder.get_suffix(env)
+        assert r is None, r
+        r = builder.get_suffix(env, [MyNode('X.src_sfx1')])
+        assert r is None, r
+        r = builder.get_src_suffix(env)
+        assert r == '.src_sfx1', r
+        r = builder.src_suffixes(env)
+        assert r == ['.src_sfx1'], r
+
+        builder = SCons.Builder.Builder(prefix='A_', suffix={}, action={})
+        env = Environment(BUILDERS={'Bld':builder})
+        builder.add_action('.src_sfx1', 'FOO')
+        builder.add_action('.src_sfx2', 'BAR')
+
+        r = builder.get_name(env)
+        assert r == 'Bld', r
+        r = builder.get_prefix(env)
+        assert r == 'A_', r
+        r = builder.get_suffix(env)
+        assert r is None, r
+        r = builder.get_src_suffix(env)
+        assert r == '.src_sfx1', r
+        r = sorted(builder.src_suffixes(env))
+        assert r == ['.src_sfx1', '.src_sfx2'], r
+
 
     def test_Builder_Args(self):
-        """Testing passing extra agrs to a builder."""
-        def buildFunc(target, source, env, foo, bar, s=self):
-            s.foo=foo
-            s.bar=bar
+        """Testing passing extra args to a builder."""
+        def buildFunc(target, source, env, s=self):
+            s.foo=env['foo']
+            s.bar=env['bar']
+            assert env['CC'] == 'mycc'
 
-        builder = SCons.Builder.Builder(name="builder", action=buildFunc)
-        tgt = builder(env, target='foo', source='bar', foo=1, bar=2)
+        env=Environment(CC='cc')
+
+        builder = SCons.Builder.Builder(action=buildFunc)
+        tgt = builder(env, target='foo', source='bar', foo=1, bar=2, CC='mycc')[0]
         tgt.build()
         assert self.foo == 1, self.foo
         assert self.bar == 2, self.bar
 
     def test_emitter(self):
         """Test emitter functions."""
-        def emit(target, source, env, foo=0, bar=0):
+        def emit(target, source, env):
+            foo = env.get('foo', 0)
+            bar = env.get('bar', 0)
+            for t in target:
+                assert isinstance(t, MyNode)
+                assert t.has_builder()
+            for s in source:
+                assert isinstance(s, MyNode)
             if foo:
                 target.append("bar%d"%foo)
             if bar:
                 source.append("baz")
             return ( target, source )
 
-        builder = SCons.Builder.Builder(name="builder", action='foo',
-                                        emitter=emit)
-        tgt = builder(env, target='foo2', source='bar')
+        env = Environment()
+        builder = SCons.Builder.Builder(action='foo',
+                                        emitter=emit,
+                                        target_factory=MyNode,
+                                        source_factory=MyNode)
+        tgt = builder(env, target='foo2', source='bar')[0]
         assert str(tgt) == 'foo2', str(tgt)
         assert str(tgt.sources[0]) == 'bar', str(tgt.sources[0])
 
         tgt = builder(env, target='foo3', source='bar', foo=1)
         assert len(tgt) == 2, len(tgt)
-        assert 'foo3' in map(str, tgt), map(str, tgt)
-        assert 'bar1' in map(str, tgt), map(str, tgt)
+        assert 'foo3' in list(map(str, tgt)), list(map(str, tgt))
+        assert 'bar1' in list(map(str, tgt)), list(map(str, tgt))
 
-        tgt = builder(env, target='foo4', source='bar', bar=1)
+        tgt = builder(env, target='foo4', source='bar', bar=1)[0]
         assert str(tgt) == 'foo4', str(tgt)
         assert len(tgt.sources) == 2, len(tgt.sources)
-        assert 'baz' in map(str, tgt.sources), map(str, tgt.sources)
-        assert 'bar' in map(str, tgt.sources), map(str, tgt.sources)
+        assert 'baz' in list(map(str, tgt.sources)), list(map(str, tgt.sources))
+        assert 'bar' in list(map(str, tgt.sources)), list(map(str, tgt.sources))
 
         env2=Environment(FOO=emit)
-        builder2=SCons.Builder.Builder(name="builder2", action='foo',
-                                       emitter="$FOO")
+        builder2=SCons.Builder.Builder(action='foo',
+                                       emitter="$FOO",
+                                       target_factory=MyNode,
+                                       source_factory=MyNode)
+
+        builder2a=SCons.Builder.Builder(action='foo',
+                                        emitter="$FOO",
+                                        target_factory=MyNode,
+                                        source_factory=MyNode)
 
-        tgt = builder2(env2, target='foo5', source='bar')
+        assert builder2 == builder2a, repr(builder2.__dict__) + "\n" + repr(builder2a.__dict__)
+
+        tgt = builder2(env2, target='foo5', source='bar')[0]
         assert str(tgt) == 'foo5', str(tgt)
         assert str(tgt.sources[0]) == 'bar', str(tgt.sources[0])
 
         tgt = builder2(env2, target='foo6', source='bar', foo=2)
         assert len(tgt) == 2, len(tgt)
-        assert 'foo6' in map(str, tgt), map(str, tgt)
-        assert 'bar2' in map(str, tgt), map(str, tgt)
+        assert 'foo6' in list(map(str, tgt)), list(map(str, tgt))
+        assert 'bar2' in list(map(str, tgt)), list(map(str, tgt))
 
-        tgt = builder2(env2, target='foo7', source='bar', bar=1)
+        tgt = builder2(env2, target='foo7', source='bar', bar=1)[0]
         assert str(tgt) == 'foo7', str(tgt)
         assert len(tgt.sources) == 2, len(tgt.sources)
-        assert 'baz' in map(str, tgt.sources), map(str, tgt.sources)
-        assert 'bar' in map(str, tgt.sources), map(str, tgt.sources)
+        assert 'baz' in list(map(str, tgt.sources)), list(map(str, tgt.sources))
+        assert 'bar' in list(map(str, tgt.sources)), list(map(str, tgt.sources))
+
+    def test_emitter_preserve_builder(self):
+        """Test an emitter not overwriting a newly-set builder"""
+        env = Environment()
+
+        new_builder = SCons.Builder.Builder(action='new')
+        node = MyNode('foo8')
+        new_node = MyNode('foo8.new')
+
+        def emit(target, source, env, nb=new_builder, nn=new_node):
+            for t in target:
+                t.builder = nb
+            return [nn], source
+            
+        builder=SCons.Builder.Builder(action='foo',
+                                      emitter=emit,
+                                      target_factory=MyNode,
+                                      source_factory=MyNode)
+        tgt = builder(env, target=node, source='bar')[0]
+        assert tgt is new_node, tgt
+        assert tgt.builder is builder, tgt.builder
+        assert node.builder is new_builder, node.builder
+
+    def test_emitter_suffix_map(self):
+        """Test mapping file suffixes to emitter functions"""
+        env = Environment()
+
+        def emit4a(target, source, env):
+            source = list(map(str, source))
+            target = ['emit4a-' + x[:-3] for x in source]
+            return (target, source)
+        def emit4b(target, source, env):
+            source = list(map(str, source))
+            target = ['emit4b-' + x[:-3] for x in source]
+            return (target, source)
+
+        builder = SCons.Builder.Builder(action='foo',
+                                        emitter={'.4a':emit4a,
+                                                 '.4b':emit4b},
+                                        target_factory=MyNode,
+                                        source_factory=MyNode)
+        tgt = builder(env, None, source='aaa.4a')[0]
+        assert str(tgt) == 'emit4a-aaa', str(tgt)
+        tgt = builder(env, None, source='bbb.4b')[0]
+        assert str(tgt) == 'emit4b-bbb', str(tgt)
+        tgt = builder(env, None, source='ccc.4c')[0]
+        assert str(tgt) == 'ccc', str(tgt)
+
+        def emit4c(target, source, env):
+            source = list(map(str, source))
+            target = ['emit4c-' + x[:-3] for x in source]
+            return (target, source)
+
+        builder.add_emitter('.4c', emit4c)
+        tgt = builder(env, None, source='ccc.4c')[0]
+        assert str(tgt) == 'emit4c-ccc', str(tgt)
+
+    def test_emitter_function_list(self):
+        """Test lists of emitter functions"""
+        env = Environment()
+
+        def emit1a(target, source, env):
+            source = list(map(str, source))
+            target = target + ['emit1a-' + x[:-2] for x in source]
+            return (target, source)
+        def emit1b(target, source, env):
+            source = list(map(str, source))
+            target = target + ['emit1b-' + x[:-2] for x in source]
+            return (target, source)
+        builder1 = SCons.Builder.Builder(action='foo',
+                                         emitter=[emit1a, emit1b],
+                                         node_factory=MyNode)
+
+        tgts = builder1(env, target='target-1', source='aaa.1')
+        tgts = list(map(str, tgts))
+        assert tgts == ['target-1', 'emit1a-aaa', 'emit1b-aaa'], tgts
+
+        # Test a list of emitter functions through the environment.
+        def emit2a(target, source, env):
+            source = list(map(str, source))
+            target = target + ['emit2a-' + x[:-2] for x in source]
+            return (target, source)
+        def emit2b(target, source, env):
+            source = list(map(str, source))
+            target = target + ['emit2b-' + x[:-2] for x in source]
+            return (target, source)
+        builder2 = SCons.Builder.Builder(action='foo',
+                                         emitter='$EMITTERLIST',
+                                         node_factory=MyNode)
+                                         
+        env = Environment(EMITTERLIST = [emit2a, emit2b])
+
+        tgts = builder2(env, target='target-2', source='aaa.2')
+        tgts = list(map(str, tgts))
+        assert tgts == ['target-2', 'emit2a-aaa', 'emit2b-aaa'], tgts
+
+    def test_emitter_TARGET_SOURCE(self):
+        """Test use of $TARGET and $SOURCE in emitter results"""
+
+        env = SCons.Environment.Environment()
+
+        def emit(target, source, env):
+            return (target + ['${SOURCE}.s1', '${TARGET}.t1'],
+                    source + ['${TARGET}.t2', '${SOURCE}.s2'])
+
+        builder = SCons.Builder.Builder(action='foo',
+                                        emitter = emit,
+                                        node_factory = MyNode)
+
+        targets = builder(env, target = 'TTT', source ='SSS')
+        sources = targets[0].sources
+        targets = list(map(str, targets))
+        sources = list(map(str, sources))
+        assert targets == ['TTT', 'SSS.s1', 'TTT.t1'], targets
+        assert sources == ['SSS', 'TTT.t2', 'SSS.s2'], targets
+
+    def test_no_target(self):
+        """Test deducing the target from the source."""
+
+        env = Environment()
+        b = SCons.Builder.Builder(action='foo', suffix='.o')
+
+        tgt = b(env, None, 'aaa')[0]
+        assert str(tgt) == 'aaa.o', str(tgt)
+        assert len(tgt.sources) == 1, list(map(str, tgt.sources))
+        assert str(tgt.sources[0]) == 'aaa', list(map(str, tgt.sources))
+
+        tgt = b(env, None, 'bbb.c')[0]
+        assert str(tgt) == 'bbb.o', str(tgt)
+        assert len(tgt.sources) == 1, list(map(str, tgt.sources))
+        assert str(tgt.sources[0]) == 'bbb.c', list(map(str, tgt.sources))
+
+        tgt = b(env, None, 'ccc.x.c')[0]
+        assert str(tgt) == 'ccc.x.o', str(tgt)
+        assert len(tgt.sources) == 1, list(map(str, tgt.sources))
+        assert str(tgt.sources[0]) == 'ccc.x.c', list(map(str, tgt.sources))
+
+        tgt = b(env, None, ['d0.c', 'd1.c'])[0]
+        assert str(tgt) == 'd0.o', str(tgt)
+        assert len(tgt.sources) == 2,  list(map(str, tgt.sources))
+        assert str(tgt.sources[0]) == 'd0.c', list(map(str, tgt.sources))
+        assert str(tgt.sources[1]) == 'd1.c', list(map(str, tgt.sources))
+
+        tgt = b(env, target = None, source='eee')[0]
+        assert str(tgt) == 'eee.o', str(tgt)
+        assert len(tgt.sources) == 1, list(map(str, tgt.sources))
+        assert str(tgt.sources[0]) == 'eee', list(map(str, tgt.sources))
+
+        tgt = b(env, target = None, source='fff.c')[0]
+        assert str(tgt) == 'fff.o', str(tgt)
+        assert len(tgt.sources) == 1, list(map(str, tgt.sources))
+        assert str(tgt.sources[0]) == 'fff.c', list(map(str, tgt.sources))
+
+        tgt = b(env, target = None, source='ggg.x.c')[0]
+        assert str(tgt) == 'ggg.x.o', str(tgt)
+        assert len(tgt.sources) == 1, list(map(str, tgt.sources))
+        assert str(tgt.sources[0]) == 'ggg.x.c', list(map(str, tgt.sources))
+
+        tgt = b(env, target = None, source=['h0.c', 'h1.c'])[0]
+        assert str(tgt) == 'h0.o', str(tgt)
+        assert len(tgt.sources) == 2,  list(map(str, tgt.sources))
+        assert str(tgt.sources[0]) == 'h0.c', list(map(str, tgt.sources))
+        assert str(tgt.sources[1]) == 'h1.c', list(map(str, tgt.sources))
+
+        w = b(env, target='i0.w', source=['i0.x'])[0]
+        y = b(env, target='i1.y', source=['i1.z'])[0]
+        tgt = b(env, None, source=[w, y])[0]
+        assert str(tgt) == 'i0.o', str(tgt)
+        assert len(tgt.sources) == 2, list(map(str, tgt.sources))
+        assert str(tgt.sources[0]) == 'i0.w', list(map(str, tgt.sources))
+        assert str(tgt.sources[1]) == 'i1.y', list(map(str, tgt.sources))
+
+    def test_get_name(self):
+        """Test getting name of builder.
+
+        Each type of builder should return its environment-specific
+        name when queried appropriately.  """
+
+        b1 = SCons.Builder.Builder(action='foo', suffix='.o')
+        b2 = SCons.Builder.Builder(action='foo', suffix='.c')
+        b3 = SCons.Builder.Builder(action='bar', src_suffix = '.foo',
+                                                 src_builder = b1)
+        b4 = SCons.Builder.Builder(action={})
+        b5 = SCons.Builder.Builder(action='foo', name='builder5')
+        b6 = SCons.Builder.Builder(action='foo')
+        assert isinstance(b4, SCons.Builder.CompositeBuilder)
+        assert isinstance(b4.action, SCons.Action.CommandGeneratorAction)
+        
+        env = Environment(BUILDERS={'bldr1': b1,
+                                    'bldr2': b2,
+                                    'bldr3': b3,
+                                    'bldr4': b4})
+        env2 = Environment(BUILDERS={'B1': b1,
+                                     'B2': b2,
+                                     'B3': b3,
+                                     'B4': b4})
+        # With no name, get_name will return the class.  Allow
+        # for caching...
+        b6_names = [
+            'SCons.Builder.BuilderBase',
+            "<class 'SCons.Builder.BuilderBase'>",
+            'SCons.Memoize.BuilderBase',
+            "<class 'SCons.Memoize.BuilderBase'>",
+        ]
+
+        assert b1.get_name(env) == 'bldr1', b1.get_name(env)
+        assert b2.get_name(env) == 'bldr2', b2.get_name(env)
+        assert b3.get_name(env) == 'bldr3', b3.get_name(env)
+        assert b4.get_name(env) == 'bldr4', b4.get_name(env)
+        assert b5.get_name(env) == 'builder5', b5.get_name(env)
+        assert b6.get_name(env) in b6_names, b6.get_name(env)
+
+        assert b1.get_name(env2) == 'B1', b1.get_name(env2)
+        assert b2.get_name(env2) == 'B2', b2.get_name(env2)
+        assert b3.get_name(env2) == 'B3', b3.get_name(env2)
+        assert b4.get_name(env2) == 'B4', b4.get_name(env2)
+        assert b5.get_name(env2) == 'builder5', b5.get_name(env2)
+        assert b6.get_name(env2) in b6_names, b6.get_name(env2)
+
+        assert b5.get_name(None) == 'builder5', b5.get_name(None)
+        assert b6.get_name(None) in b6_names, b6.get_name(None)
+
+        # This test worked before adding batch builders, but we must now
+        # be able to disambiguate a CompositeAction into a more specific
+        # action based on file suffix at call time.  Leave this commented
+        # out (for now) in case this reflects a real-world use case that
+        # we must accomodate and we want to resurrect this test.
+        #tgt = b4(env, target = 'moo', source='cow')
+        #assert tgt[0].builder.get_name(env) == 'bldr4'
+
+class CompositeBuilderTestCase(unittest.TestCase):
+
+    def setUp(self):
+        def func_action(target, source, env):
+            return 0
+
+        builder = SCons.Builder.Builder(action={ '.foo' : func_action,
+                                                 '.bar' : func_action})
+
+        self.func_action = func_action
+        self.builder = builder
+
+    def test___init__(self):
+        """Test CompositeBuilder creation"""
+        env = Environment()
+        builder = SCons.Builder.Builder(action={})
+
+        tgt = builder(env, source=[])
+        assert tgt == [], tgt
+        
+        assert isinstance(builder, SCons.Builder.CompositeBuilder)
+        assert isinstance(builder.action, SCons.Action.CommandGeneratorAction)
+
+    def test_target_action(self):
+        """Test CompositeBuilder setting of target builder actions"""
+        env = Environment()
+        builder = self.builder
+
+        tgt = builder(env, target='test1', source='test1.foo')[0]
+        assert isinstance(tgt.builder, SCons.Builder.BuilderBase)
+        assert tgt.builder.action is builder.action
+
+        tgt = builder(env, target='test2', source='test1.bar')[0]
+        assert isinstance(tgt.builder, SCons.Builder.BuilderBase)
+        assert tgt.builder.action is builder.action
+
+    def test_multiple_suffix_error(self):
+        """Test the CompositeBuilder multiple-source-suffix error"""
+        env = Environment()
+        builder = self.builder
+
+        flag = 0
+        try:
+            builder(env, target='test3', source=['test2.bar', 'test1.foo'])[0]
+        except SCons.Errors.UserError, e:
+            flag = 1
+        assert flag, "UserError should be thrown when we call a builder with files of different suffixes."
+        expect = "While building `['test3']' from `test1.foo': Cannot build multiple sources with different extensions: .bar, .foo"
+        assert str(e) == expect, e
+
+    def test_source_ext_match(self):
+        """Test the CompositeBuilder source_ext_match argument"""
+        env = Environment()
+        func_action = self.func_action
+        builder = SCons.Builder.Builder(action={ '.foo' : func_action,
+                                                 '.bar' : func_action},
+                                        source_ext_match = None)
+
+        tgt = builder(env, target='test3', source=['test2.bar', 'test1.foo'])[0]
+        tgt.build()
+
+    def test_suffix_variable(self):
+        """Test CompositeBuilder defining action suffixes through a variable"""
+        env = Environment(BAR_SUFFIX = '.BAR2', FOO_SUFFIX = '.FOO2')
+        func_action = self.func_action
+        builder = SCons.Builder.Builder(action={ '.foo' : func_action,
+                                                 '.bar' : func_action,
+                                                 '$BAR_SUFFIX' : func_action,
+                                                 '$FOO_SUFFIX' : func_action })
+
+        tgt = builder(env, target='test4', source=['test4.BAR2'])[0]
+        assert isinstance(tgt.builder, SCons.Builder.BuilderBase)
+        try:
+            tgt.build()
+            flag = 1
+        except SCons.Errors.UserError, e:
+            print e
+            flag = 0
+        assert flag, "It should be possible to define actions in composite builders using variables."
+        env['FOO_SUFFIX'] = '.BAR2'
+        builder.add_action('$NEW_SUFFIX', func_action)
+        flag = 0
+        try:
+            builder(env, target='test5', source=['test5.BAR2'])[0]
+        except SCons.Errors.UserError:
+            flag = 1
+        assert flag, "UserError should be thrown when we call a builder with ambigous suffixes."
+
+    def test_src_builder(self):
+        """Test CompositeBuilder's use of a src_builder"""
+        env = Environment()
+
+        foo_bld = SCons.Builder.Builder(action = 'a-foo',
+                                        src_suffix = '.ina',
+                                        suffix = '.foo')
+        assert isinstance(foo_bld, SCons.Builder.BuilderBase)
+        builder = SCons.Builder.Builder(action = { '.foo' : 'foo',
+                                                   '.bar' : 'bar' },
+                                        src_builder = foo_bld)
+        assert isinstance(builder, SCons.Builder.CompositeBuilder)
+        assert isinstance(builder.action, SCons.Action.CommandGeneratorAction)
+
+        tgt = builder(env, target='t1', source='t1a.ina t1b.ina')[0]
+        assert isinstance(tgt.builder, SCons.Builder.BuilderBase)
+
+        tgt = builder(env, target='t2', source='t2a.foo t2b.ina')[0]
+        assert isinstance(tgt.builder, SCons.Builder.BuilderBase), tgt.builder.__dict__
+
+        bar_bld = SCons.Builder.Builder(action = 'a-bar',
+                                        src_suffix = '.inb',
+                                        suffix = '.bar')
+        assert isinstance(bar_bld, SCons.Builder.BuilderBase)
+        builder = SCons.Builder.Builder(action = { '.foo' : 'foo'},
+                                        src_builder = [foo_bld, bar_bld])
+        assert isinstance(builder, SCons.Builder.CompositeBuilder)
+        assert isinstance(builder.action, SCons.Action.CommandGeneratorAction)
+
+        builder.add_action('.bar', 'bar')
+
+        tgt = builder(env, target='t3-foo', source='t3a.foo t3b.ina')[0]
+        assert isinstance(tgt.builder, SCons.Builder.BuilderBase)
+
+        tgt = builder(env, target='t3-bar', source='t3a.bar t3b.inb')[0]
+        assert isinstance(tgt.builder, SCons.Builder.BuilderBase)
+
+        flag = 0
+        try:
+            builder(env, target='t5', source=['test5a.foo', 'test5b.inb'])[0]
+        except SCons.Errors.UserError, e:
+            flag = 1
+        assert flag, "UserError should be thrown when we call a builder with files of different suffixes."
+        expect = "While building `['t5']' from `test5b.bar': Cannot build multiple sources with different extensions: .foo, .bar"
+        assert str(e) == expect, e
+
+        flag = 0
+        try:
+            builder(env, target='t6', source=['test6a.bar', 'test6b.ina'])[0]
+        except SCons.Errors.UserError, e:
+            flag = 1
+        assert flag, "UserError should be thrown when we call a builder with files of different suffixes."
+        expect = "While building `['t6']' from `test6b.foo': Cannot build multiple sources with different extensions: .bar, .foo"
+        assert str(e) == expect, e
+
+        flag = 0
+        try:
+            builder(env, target='t4', source=['test4a.ina', 'test4b.inb'])[0]
+        except SCons.Errors.UserError, e:
+            flag = 1
+        assert flag, "UserError should be thrown when we call a builder with files of different suffixes."
+        expect = "While building `['t4']' from `test4b.bar': Cannot build multiple sources with different extensions: .foo, .bar"
+        assert str(e) == expect, e
+
+        flag = 0
+        try:
+            builder(env, target='t7', source=[env.fs.File('test7')])[0]
+        except SCons.Errors.UserError, e:
+            flag = 1
+        assert flag, "UserError should be thrown when we call a builder with files of different suffixes."
+        expect = "While building `['t7']': Cannot deduce file extension from source files: ['test7']"
+        assert str(e) == expect, e
+
+        flag = 0
+        try:
+            builder(env, target='t8', source=['test8.unknown'])[0]
+        except SCons.Errors.UserError, e:
+            flag = 1
+        assert flag, "UserError should be thrown when we call a builder target with an unknown suffix."
+        expect = "While building `['t8']' from `['test8.unknown']': Don't know how to build from a source file with suffix `.unknown'.  Expected a suffix in this list: ['.foo', '.bar']."
+        assert str(e) == expect, e
 
 if __name__ == "__main__":
-    suite = unittest.makeSuite(BuilderTestCase, 'test_')
+    suite = unittest.TestSuite()
+    tclasses = [
+        BuilderTestCase,
+        CompositeBuilderTestCase
+    ]
+    for tclass in tclasses:
+        names = unittest.getTestCaseNames(tclass, 'test_')
+        suite.addTests(list(map(tclass, names)))
     if not unittest.TextTestRunner().run(suite).wasSuccessful():
         sys.exit(1)
+
+# Local Variables:
+# tab-width:4
+# indent-tabs-mode:nil
+# End:
+# vim: set expandtab tabstop=4 shiftwidth=4: