http://scons.tigris.org/issues/show_bug.cgi?id=2345
[scons.git] / src / engine / SCons / Node / NodeTests.py
index 2d6e0bce293c32c3a41e76042a9ab0b06b8bd113..6de6d386da5dd9da6b266837ef88ca1ada93da54 100644 (file)
 # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
 # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 #
+from __future__ import generators  ### KEEP FOR COMPATIBILITY FIXERS
 
 __revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
 
+import SCons.compat
+
+import collections
 import os
+import re
 import sys
-import types
 import unittest
 
 import SCons.Errors
 import SCons.Node
+import SCons.Util
 
 
 
@@ -39,12 +44,35 @@ built_source =  None
 cycle_detected = None
 built_order = 0
 
-class MyAction:
+def _actionAppend(a1, a2):
+    all = []
+    for curr_a in [a1, a2]:
+        if isinstance(curr_a, MyAction):
+            all.append(curr_a)
+        elif isinstance(curr_a, MyListAction):
+            all.extend(curr_a.list)
+        elif isinstance(curr_a, list):
+            all.extend(curr_a)
+        else:
+            raise Exception('Cannot Combine Actions')
+    return MyListAction(all)
+
+class MyActionBase:
+    def __add__(self, other):
+        return _actionAppend(self, other)
+
+    def __radd__(self, other):
+        return _actionAppend(other, self)
+
+class MyAction(MyActionBase):
     def __init__(self):
         self.order = 0
-        
-    def __call__(self, target, source, env):
+
+    def __call__(self, target, source, env, executor=None):
         global built_it, built_target, built_source, built_args, built_order
+        if executor:
+            target = executor.get_all_targets()
+            source = executor.get_all_sources()
         built_it = 1
         built_target = target
         built_source = source
@@ -53,41 +81,69 @@ class MyAction:
         self.order = built_order
         return 0
 
-    def get_actions(self):
-        return [self]
-
-class MyNonGlobalAction:
-    def __init__(self):
-        self.order = 0
-        self.built_it = None
-        self.built_target =  None
-        self.built_source =  None
-
+    def get_implicit_deps(self, target, source, env):
+        return []
+
+class MyExecutor:
+    def __init__(self, env=None, targets=[], sources=[]):
+        self.env = env
+        self.targets = targets
+        self.sources = sources
+    def get_build_env(self):
+        return self.env
+    def get_build_scanner_path(self, scanner):
+        return 'executor would call %s' % scanner
+    def cleanup(self):
+        self.cleaned_up = 1
+    def scan_targets(self, scanner):
+        if not scanner:
+            return
+        d = scanner(self.targets)
+        for t in self.targets:
+            t.implicit.extend(d)
+    def scan_sources(self, scanner):
+        if not scanner:
+            return
+        d = scanner(self.sources)
+        for t in self.targets:
+            t.implicit.extend(d)
+
+class MyListAction(MyActionBase):
+    def __init__(self, list):
+        self.list = list
     def __call__(self, target, source, env):
-        # Okay, so not ENTIRELY non-global...
-        global built_order
-        self.built_it = 1
-        self.built_target = target
-        self.built_source = source
-        self.built_args = env
-        built_order = built_order + 1
-        self.order = built_order
-        return 0
-
-    def get_actions(self):
-        return [self]
+        for A in self.list:
+            A(target, source, env)
 
 class Environment:
+    def __init__(self, **kw):
+        self._dict = {}
+        self._dict.update(kw)
+    def __getitem__(self, key):
+        return self._dict[key]
     def Dictionary(self, *args):
         return {}
     def Override(self, overrides):
-        return overrides
+        d = self._dict.copy()
+        d.update(overrides)
+        return Environment(**d)
+    def _update(self, dict):
+        self._dict.update(dict)
+    def get_factory(self, factory):
+        return factory or MyNode
+    def get_scanner(self, scanner_key):
+        return self._dict['SCANNERS'][0]
 
 class Builder:
-    def __init__(self):
-        self.env = Environment()
+    def __init__(self, env=None, is_explicit=1):
+        if env is None: env = Environment()
+        self.env = env
         self.overrides = {}
         self.action = MyAction()
+        self.source_factory = MyNode
+        self.is_explicit = is_explicit
+        self.target_scanner = None
+        self.source_scanner = None
     def targets(self, t):
         return [t]
     def get_actions(self):
@@ -122,13 +178,19 @@ class ExceptBuilder:
 
 class ExceptBuilder2:
     def execute(self, target, source, env):
-        raise "foo"
+        raise Exception("foo")
 
 class Scanner:
     called = None
     def __call__(self, node):
         self.called = 1
         return node.found_includes
+    def path(self, env, dir, target=None, source=None):
+        return ()
+    def select(self, node):
+        return self
+    def recurse_nodes(self, nodes):
+        return nodes
 
 class MyNode(SCons.Node.Node):
     """The base Node class contains a number of do-nothing methods that
@@ -144,6 +206,84 @@ class MyNode(SCons.Node.Node):
     def get_found_includes(self, env, scanner, target):
         return scanner(self)
 
+class Calculator:
+    def __init__(self, val):
+        self.max_drift = 0
+        class M:
+            def __init__(self, val):
+                self.val = val
+            def signature(self, args):
+                return self.val
+            def collect(self, args):
+                return reduce(lambda x, y: x+y, args, self.val)
+        self.module = M(val)
+
+
+
+class NodeInfoBaseTestCase(unittest.TestCase):
+
+    def test_merge(self):
+        """Test merging NodeInfoBase attributes"""
+        ni1 = SCons.Node.NodeInfoBase(SCons.Node.Node())
+        ni2 = SCons.Node.NodeInfoBase(SCons.Node.Node())
+
+        ni1.a1 = 1
+        ni1.a2 = 2
+
+        ni2.a2 = 222
+        ni2.a3 = 333
+
+        ni1.merge(ni2)
+        expect = {'a1':1, 'a2':222, 'a3':333, '_version_id':1}
+        assert ni1.__dict__ == expect, ni1.__dict__
+
+    def test_update(self):
+        """Test the update() method"""
+        ni = SCons.Node.NodeInfoBase(SCons.Node.Node())
+        ni.update(SCons.Node.Node())
+
+    def test_format(self):
+        """Test the NodeInfoBase.format() method"""
+        ni1 = SCons.Node.NodeInfoBase(SCons.Node.Node())
+        ni1.xxx = 'x'
+        ni1.yyy = 'y'
+        ni1.zzz = 'z'
+
+        f = ni1.format()
+        assert f == ['1', 'x', 'y', 'z'], f
+
+        ni1.field_list = ['xxx', 'zzz', 'aaa']
+
+        f = ni1.format()
+        assert f == ['x', 'z', 'None'], f
+
+
+
+class BuildInfoBaseTestCase(unittest.TestCase):
+
+    def test___init__(self):
+        """Test BuildInfoBase initialization"""
+        n = SCons.Node.Node()
+        bi = SCons.Node.BuildInfoBase(n)
+        assert bi
+
+    def test_merge(self):
+        """Test merging BuildInfoBase attributes"""
+        n1 = SCons.Node.Node()
+        bi1 = SCons.Node.BuildInfoBase(n1)
+        n2 = SCons.Node.Node()
+        bi2 = SCons.Node.BuildInfoBase(n2)
+
+        bi1.a1 = 1
+        bi1.a2 = 2
+
+        bi2.a2 = 222
+        bi2.a3 = 333
+
+        bi1.merge(bi2)
+        assert bi1.a1 == 1, bi1.a1
+        assert bi1.a2 == 222, bi1.a2
+        assert bi1.a3 == 333, bi1.a3
 
 
 class NodeTestCase(unittest.TestCase):
@@ -156,7 +296,9 @@ class NodeTestCase(unittest.TestCase):
         # Make sure it doesn't blow up if no builder is set.
         node = MyNode("www")
         node.build()
-        assert built_it == None
+        assert built_it is None
+        node.build(extra_kw_argument = 1)
+        assert built_it is None
 
         node = MyNode("xxx")
         node.builder_set(Builder())
@@ -222,36 +364,89 @@ class NodeTestCase(unittest.TestCase):
         assert built_args["on"] == 3, built_args
         assert built_args["off"] == 4, built_args
 
-        built_it = None
-        built_order = 0
-        node = MyNode("xxx")
-        node.builder_set(Builder())
-        node.env_set(Environment())
-        node.sources = ["yyy", "zzz"]
-        pre1 = MyNonGlobalAction()
-        pre2 = MyNonGlobalAction()
-        post1 = MyNonGlobalAction()
-        post2 = MyNonGlobalAction()
-        node.add_pre_action(pre1)
-        node.add_pre_action(pre2)
-        node.add_post_action(post1)
-        node.add_post_action(post2)
-        node.build()
-        assert built_it
-        assert pre1.built_it
-        assert pre2.built_it
-        assert post1.built_it
-        assert post2.built_it
-        assert pre1.order == 1, pre1.order
-        assert pre2.order == 2, pre1.order
-        # The action of the builder itself is order 3...
-        assert post1.order == 4, pre1.order
-        assert post2.order == 5, pre1.order
-
-        for act in [ pre1, pre2, post1, post2 ]:
-            assert type(act.built_target[0]) == type(MyNode("bar")), type(act.built_target[0])
-            assert str(act.built_target[0]) == "xxx", str(act.built_target[0])
-            assert act.built_source == ["yyy", "zzz"], act.built_source
+    def test_get_build_scanner_path(self):
+        """Test the get_build_scanner_path() method"""
+        n = SCons.Node.Node()
+        x = MyExecutor()
+        n.set_executor(x)
+        p = n.get_build_scanner_path('fake_scanner')
+        assert p == "executor would call fake_scanner", p
+
+    def test_get_executor(self):
+        """Test the get_executor() method"""
+        n = SCons.Node.Node()
+
+        try:
+            n.get_executor(0)
+        except AttributeError:
+            pass
+        else:
+            self.fail("did not catch expected AttributeError")
+
+        class Builder:
+            action = 'act'
+            env = 'env1'
+            overrides = {}
+
+        n = SCons.Node.Node()
+        n.builder_set(Builder())
+        x = n.get_executor()
+        assert x.env == 'env1', x.env
+
+        n = SCons.Node.Node()
+        n.builder_set(Builder())
+        n.env_set('env2')
+        x = n.get_executor()
+        assert x.env == 'env2', x.env
+
+    def test_set_executor(self):
+        """Test the set_executor() method"""
+        n = SCons.Node.Node()
+        n.set_executor(1)
+        assert n.executor == 1, n.executor
+
+    def test_executor_cleanup(self):
+        """Test letting the executor cleanup its cache"""
+        n = SCons.Node.Node()
+        x = MyExecutor()
+        n.set_executor(x)
+        n.executor_cleanup()
+        assert x.cleaned_up
+
+    def test_reset_executor(self):
+        """Test the reset_executor() method"""
+        n = SCons.Node.Node()
+        n.set_executor(1)
+        assert n.executor == 1, n.executor
+        n.reset_executor()
+        assert not hasattr(n, 'executor'), "unexpected executor attribute"
+
+    def test_built(self):
+        """Test the built() method"""
+        class SubNodeInfo(SCons.Node.NodeInfoBase):
+            def update(self, node):
+                self.updated = 1
+        class SubNode(SCons.Node.Node):
+            def clear(self):
+                self.cleared = 1
+
+        n = SubNode()
+        n.ninfo = SubNodeInfo(n)
+        n.built()
+        assert n.cleared, n.cleared
+        assert n.ninfo.updated, n.ninfo.cleared
+
+    def test_push_to_cache(self):
+        """Test the base push_to_cache() method"""
+        n = SCons.Node.Node()
+        r = n.push_to_cache()
+        assert r is None, r
+
+    def test_retrieve_from_cache(self):
+        """Test the base retrieve_from_cache() method"""
+        n = SCons.Node.Node()
+        r = n.retrieve_from_cache()
+        assert r == 0, r
 
     def test_visited(self):
         """Test the base visited() method
@@ -260,15 +455,7 @@ class NodeTestCase(unittest.TestCase):
         """
         n = SCons.Node.Node()
         n.visited()
-            
-    def test_depends_on(self):
-        """Test the depends_on() method
-        """
-        parent = SCons.Node.Node()
-        child = SCons.Node.Node()
-        parent.add_dependency([child])
-        assert parent.depends_on([child])
-        
+
     def test_builder_set(self):
         """Test setting a Node's Builder
         """
@@ -285,6 +472,37 @@ class NodeTestCase(unittest.TestCase):
         n1.builder_set(Builder())
         assert n1.has_builder() == 1
 
+    def test_has_explicit_builder(self):
+        """Test the has_explicit_builder() method
+        """
+        n1 = SCons.Node.Node()
+        assert not n1.has_explicit_builder()
+        n1.set_explicit(1)
+        assert n1.has_explicit_builder()
+        n1.set_explicit(None)
+        assert not n1.has_explicit_builder()
+
+    def test_get_builder(self):
+        """Test the get_builder() method"""
+        n1 = SCons.Node.Node()
+        b = n1.get_builder()
+        assert b is None, b
+        b = n1.get_builder(777)
+        assert b == 777, b
+        n1.builder_set(888)
+        b = n1.get_builder()
+        assert b == 888, b
+        b = n1.get_builder(999)
+        assert b == 888, b
+
+    def test_multiple_side_effect_has_builder(self):
+        """Test the multiple_side_effect_has_builder() method
+        """
+        n1 = SCons.Node.Node()
+        assert n1.multiple_side_effect_has_builder() == 0
+        n1.builder_set(Builder())
+        assert n1.multiple_side_effect_has_builder() == 1
+
     def test_is_derived(self):
         """Test the is_derived() method
         """
@@ -305,13 +523,28 @@ class NodeTestCase(unittest.TestCase):
         n = SCons.Node.Node()
         t, m = n.alter_targets()
         assert t == [], t
-        assert m == None, m
+        assert m is None, m
 
-    def test_current(self):
-        """Test the default current() method
+    def test_is_up_to_date(self):
+        """Test the default is_up_to_date() method
         """
         node = SCons.Node.Node()
-        assert node.current() is None
+        assert node.is_up_to_date() is None
+
+    def test_children_are_up_to_date(self):
+        """Test the children_are_up_to_date() method used by subclasses
+        """
+        n1 = SCons.Node.Node()
+        n2 = SCons.Node.Node()
+
+        n1.add_source([n2])
+        assert n1.children_are_up_to_date(), "expected up to date"
+        n2.set_state(SCons.Node.executed)
+        assert not n1.children_are_up_to_date(), "expected not up to date"
+        n2.set_state(SCons.Node.up_to_date)
+        assert n1.children_are_up_to_date(), "expected up to date"
+        n1.always_build = 1
+        assert not n1.children_are_up_to_date(), "expected not up to date"
 
     def test_env_set(self):
         """Test setting a Node's Environment
@@ -329,57 +562,104 @@ class NodeTestCase(unittest.TestCase):
         a = node.builder.get_actions()
         assert isinstance(a[0], MyAction), a[0]
 
-    def test_set_bsig(self):
-        """Test setting a Node's signature
+    def test_get_csig(self):
+        """Test generic content signature calculation
         """
         node = SCons.Node.Node()
-        node.set_bsig('www')
-        assert node.bsig == 'www'
+        node.get_contents = lambda: 444
+        result = node.get_csig()
+        assert result == '550a141f12de6341fba65b0ad0433500', result
 
-    def test_get_bsig(self):
-        """Test fetching a Node's signature
+    def test_get_cachedir_csig(self):
+        """Test content signature calculation for CacheDir
         """
         node = SCons.Node.Node()
-        node.set_bsig('xxx')
-        assert node.get_bsig() == 'xxx'
+        node.get_contents = lambda: 555
+        result = node.get_cachedir_csig()
+        assert result == '15de21c670ae7c3f6f3f1f37029303c9', result
 
-    def test_set_csig(self):
-        """Test setting a Node's signature
+    def test_get_binfo(self):
+        """Test fetching/creating a build information structure
         """
         node = SCons.Node.Node()
-        node.set_csig('yyy')
-        assert node.csig == 'yyy'
 
-    def test_get_csig(self):
-        """Test fetching a Node's signature
-        """
-        node = SCons.Node.Node()
-        node.set_csig('zzz')
-        assert node.get_csig() == 'zzz'
+        binfo = node.get_binfo()
+        assert isinstance(binfo, SCons.Node.BuildInfoBase), binfo
 
-    def test_store_bsig(self):
-        """Test calling the method to store a build signature
-        """
         node = SCons.Node.Node()
-        node.store_bsig()
-
-    def test_store_csig(self):
-        """Test calling the method to store a content signature
+        d = SCons.Node.Node()
+        d.get_ninfo().csig = 777
+        i = SCons.Node.Node()
+        i.get_ninfo().csig = 888
+        node.depends = [d]
+        node.implicit = [i]
+
+        binfo = node.get_binfo()
+        assert isinstance(binfo, SCons.Node.BuildInfoBase), binfo
+        assert hasattr(binfo, 'bsources')
+        assert hasattr(binfo, 'bsourcesigs')
+        assert binfo.bdepends == [d]
+        assert hasattr(binfo, 'bdependsigs')
+        assert binfo.bimplicit == [i]
+        assert hasattr(binfo, 'bimplicitsigs')
+
+    def test_explain(self):
+        """Test explaining why a Node must be rebuilt
         """
-        node = SCons.Node.Node()
-        node.store_csig()
-
-    def test_get_timestamp(self):
-        """Test calling the method to fetch a Node's timestamp
+        class testNode(SCons.Node.Node):
+            def __str__(self): return 'xyzzy'
+        node = testNode()
+        node.exists = lambda: None
+        # Can't do this with new-style classes (python bug #1066490)
+        #node.__str__ = lambda: 'xyzzy'
+        result = node.explain()
+        assert result == "building `xyzzy' because it doesn't exist\n", result
+
+        class testNode2(SCons.Node.Node):
+            def __str__(self): return 'null_binfo'
+        class FS:
+            pass
+        node = testNode2()
+        node.fs = FS()
+        node.fs.Top = SCons.Node.Node()
+        result = node.explain()
+        assert result is None, result
+
+        def get_null_info():
+            class Null_SConsignEntry:
+                class Null_BuildInfo:
+                    def prepare_dependencies(self):
+                        pass
+                binfo = Null_BuildInfo()
+            return Null_SConsignEntry()
+
+        node.get_stored_info = get_null_info
+        #see above: node.__str__ = lambda: 'null_binfo'
+        result = node.explain()
+        assert result == "Cannot explain why `null_binfo' is being rebuilt: No previous build information found\n", result
+
+        # XXX additional tests for the guts of the functionality some day
+
+    #def test_del_binfo(self):
+    #    """Test deleting the build information from a Node
+    #    """
+    #    node = SCons.Node.Node()
+    #    node.binfo = None
+    #    node.del_binfo()
+    #    assert not hasattr(node, 'binfo'), node
+
+    def test_store_info(self):
+        """Test calling the method to store build information
         """
         node = SCons.Node.Node()
-        assert node.get_timestamp() == 0
+        node.store_info()
 
-    def test_store_timestamp(self):
-        """Test calling the method to store a timestamp
+    def test_get_stored_info(self):
+        """Test calling the method to fetch stored build information
         """
         node = SCons.Node.Node()
-        node.store_timestamp()
+        result = node.get_stored_info()
+        assert result is None, result
 
     def test_set_always_build(self):
         """Test setting a Node's always_build value
@@ -390,6 +670,19 @@ class NodeTestCase(unittest.TestCase):
         node.set_always_build(3)
         assert node.always_build == 3
 
+    def test_set_noclean(self):
+        """Test setting a Node's noclean value
+        """
+        node = SCons.Node.Node()
+        node.set_noclean()
+        assert node.noclean == 1, node.noclean
+        node.set_noclean(7)
+        assert node.noclean == 1, node.noclean
+        node.set_noclean(0)
+        assert node.noclean == 0, node.noclean
+        node.set_noclean(None)
+        assert node.noclean == 0, node.noclean
+
     def test_set_precious(self):
         """Test setting a Node's precious value
         """
@@ -423,29 +716,31 @@ class NodeTestCase(unittest.TestCase):
 
     def test_prepare(self):
         """Test preparing a node to be built
+
+        By extension, this also tests the missing() method.
         """
         node = SCons.Node.Node()
 
         n1 = SCons.Node.Node()
         n1.builder_set(Builder())
         node.implicit = []
-        node.implicit_dict = {}
-        node._add_child(node.implicit, node.implicit_dict, [n1])
+        node.implicit_set = set()
+        node._add_child(node.implicit, node.implicit_set, [n1])
 
         node.prepare()  # should not throw an exception
 
         n2 = SCons.Node.Node()
         n2.linked = 1
         node.implicit = []
-        node.implicit_dict = {}
-        node._add_child(node.implicit, node.implicit_dict, [n2])
+        node.implicit_set = set()
+        node._add_child(node.implicit, node.implicit_set, [n2])
 
         node.prepare()  # should not throw an exception
 
         n3 = SCons.Node.Node()
         node.implicit = []
-        node.implicit_dict = {}
-        node._add_child(node.implicit, node.implicit_dict, [n3])
+        node.implicit_set = set()
+        node._add_child(node.implicit, node.implicit_set, [n3])
 
         node.prepare()  # should not throw an exception
 
@@ -454,8 +749,8 @@ class NodeTestCase(unittest.TestCase):
                 return None
         n4 = MyNode()
         node.implicit = []
-        node.implicit_dict = {}
-        node._add_child(node.implicit, node.implicit_dict, [n4]) 
+        node.implicit_set = set()
+        node._add_child(node.implicit, node.implicit_set, [n4])
         exc_caught = 0
         try:
             node.prepare()
@@ -470,30 +765,30 @@ class NodeTestCase(unittest.TestCase):
         assert node.depends == []
 
         zero = SCons.Node.Node()
-        try:
-            node.add_dependency(zero)
-        except TypeError:
-            pass
-        else:
-            assert 0
 
         one = SCons.Node.Node()
         two = SCons.Node.Node()
         three = SCons.Node.Node()
         four = SCons.Node.Node()
+        five = SCons.Node.Node()
+        six = SCons.Node.Node()
 
+        node.add_dependency([zero])
+        assert node.depends == [zero]
         node.add_dependency([one])
-        assert node.depends == [one]
+        assert node.depends == [zero, one]
         node.add_dependency([two, three])
-        assert node.depends == [one, two, three]
+        assert node.depends == [zero, one, two, three]
         node.add_dependency([three, four, one])
-        assert node.depends == [one, two, three, four]
+        assert node.depends == [zero, one, two, three, four]
 
-        assert zero.get_parents() == []
-        assert one.get_parents() == [node]
-        assert two.get_parents() == [node]
-        assert three.get_parents() == [node]
-        assert four.get_parents() == [node]
+        try:
+            node.add_depends([[five, six]])
+        except:
+            pass
+        else:
+            raise Exception("did not catch expected exception")
+        assert node.depends == [zero, one, two, three, four]
 
 
     def test_add_source(self):
@@ -503,30 +798,29 @@ class NodeTestCase(unittest.TestCase):
         assert node.sources == []
 
         zero = SCons.Node.Node()
-        try:
-            node.add_source(zero)
-        except TypeError:
-            pass
-        else:
-            assert 0
-
         one = SCons.Node.Node()
         two = SCons.Node.Node()
         three = SCons.Node.Node()
         four = SCons.Node.Node()
+        five = SCons.Node.Node()
+        six = SCons.Node.Node()
 
+        node.add_source([zero])
+        assert node.sources == [zero]
         node.add_source([one])
-        assert node.sources == [one]
+        assert node.sources == [zero, one]
         node.add_source([two, three])
-        assert node.sources == [one, two, three]
+        assert node.sources == [zero, one, two, three]
         node.add_source([three, four, one])
-        assert node.sources == [one, two, three, four]
+        assert node.sources == [zero, one, two, three, four]
 
-        assert zero.get_parents() == []
-        assert one.get_parents() == [node]
-        assert two.get_parents() == [node]
-        assert three.get_parents() == [node]
-        assert four.get_parents() == [node]
+        try:
+            node.add_source([[five, six]])
+        except:
+            pass
+        else:
+            raise Exception("did not catch expected exception")
+        assert node.sources == [zero, one, two, three, four], node.sources
 
     def test_add_ignore(self):
         """Test adding files whose dependencies should be ignored.
@@ -535,30 +829,29 @@ class NodeTestCase(unittest.TestCase):
         assert node.ignore == []
 
         zero = SCons.Node.Node()
-        try:
-            node.add_ignore(zero)
-        except TypeError:
-            pass
-        else:
-            assert 0
-
         one = SCons.Node.Node()
         two = SCons.Node.Node()
         three = SCons.Node.Node()
         four = SCons.Node.Node()
+        five = SCons.Node.Node()
+        six = SCons.Node.Node()
 
+        node.add_ignore([zero])
+        assert node.ignore == [zero]
         node.add_ignore([one])
-        assert node.ignore == [one]
+        assert node.ignore == [zero, one]
         node.add_ignore([two, three])
-        assert node.ignore == [one, two, three]
+        assert node.ignore == [zero, one, two, three]
         node.add_ignore([three, four, one])
-        assert node.ignore == [one, two, three, four]
+        assert node.ignore == [zero, one, two, three, four]
 
-        assert zero.get_parents() == []
-        assert one.get_parents() == [node]
-        assert two.get_parents() == [node]
-        assert three.get_parents() == [node]
-        assert four.get_parents() == [node]
+        try:
+            node.add_ignore([[five, six]])
+        except:
+            pass
+        else:
+            raise Exception("did not catch expected exception")
+        assert node.ignore == [zero, one, two, three, four]
 
     def test_get_found_includes(self):
         """Test the default get_found_includes() method
@@ -581,58 +874,154 @@ class NodeTestCase(unittest.TestCase):
         assert deps == [], deps
 
         s = Scanner()
-        d = MyNode("ddd")
-        node.found_includes = [d]
+        d1 = MyNode("d1")
+        d2 = MyNode("d2")
+        node.found_includes = [d1, d2]
 
         # Simple return of the found includes
         deps = node.get_implicit_deps(env, s, target)
-        assert deps == [d], deps
+        assert deps == [d1, d2], deps
 
-        # No "recursive" attribute on scanner doesn't recurse
+        # By default, our fake scanner recurses
         e = MyNode("eee")
-        d.found_includes = [e]
+        f = MyNode("fff")
+        g = MyNode("ggg")
+        d1.found_includes = [e, f]
+        d2.found_includes = [e, f]
+        f.found_includes = [g]
         deps = node.get_implicit_deps(env, s, target)
-        assert deps == [d], map(str, deps)
+        assert deps == [d1, d2, e, f, g], list(map(str, deps))
 
-        # Explicit "recursive" attribute on scanner doesn't recurse
-        s.recursive = None
+        # Recursive scanning eliminates duplicates
+        e.found_includes = [f]
         deps = node.get_implicit_deps(env, s, target)
-        assert deps == [d], map(str, deps)
+        assert deps == [d1, d2, e, f, g], list(map(str, deps))
 
-        # Explicit "recursive" attribute on scanner which does recurse
-        s.recursive = 1
+        # Scanner method can select specific nodes to recurse
+        def no_fff(nodes):
+            return [n for n in nodes if str(n)[0] != 'f']
+        s.recurse_nodes = no_fff
         deps = node.get_implicit_deps(env, s, target)
-        assert deps == [d, e], map(str, deps)
+        assert deps == [d1, d2, e, f], list(map(str, deps))
 
-        # Recursive scanning eliminates duplicates
-        f = MyNode("fff")
-        d.found_includes = [e, f]
-        e.found_includes = [f]
+        # Scanner method can short-circuit recursing entirely
+        s.recurse_nodes = lambda nodes: []
         deps = node.get_implicit_deps(env, s, target)
-        assert deps == [d, e, f], map(str, deps)
+        assert deps == [d1, d2], list(map(str, deps))
+
+    def test_get_env_scanner(self):
+        """Test fetching the environment scanner for a Node
+        """
+        node = SCons.Node.Node()
+        scanner = Scanner()
+        env = Environment(SCANNERS = [scanner])
+        s = node.get_env_scanner(env)
+        assert s == scanner, s
+        s = node.get_env_scanner(env, {'X':1})
+        assert s == scanner, s
+
+    def test_get_target_scanner(self):
+        """Test fetching the target scanner for a Node
+        """
+        s = Scanner()
+        b = Builder()
+        b.target_scanner = s
+        n = SCons.Node.Node()
+        n.builder = b
+        x = n.get_target_scanner()
+        assert x is s, x
+
+    def test_get_source_scanner(self):
+        """Test fetching the source scanner for a Node
+        """
+        target = SCons.Node.Node()
+        source = SCons.Node.Node()
+        s = target.get_source_scanner(source)
+        assert isinstance(s, SCons.Util.Null), s
+
+        ts1 = Scanner()
+        ts2 = Scanner()
+        ts3 = Scanner()
+
+        class Builder1(Builder):
+            def __call__(self, source):
+                r = SCons.Node.Node()
+                r.builder = self
+                return [r]
+        class Builder2(Builder1):
+            def __init__(self, scanner):
+                self.source_scanner = scanner
+
+        builder = Builder2(ts1)
+            
+        targets = builder([source])
+        s = targets[0].get_source_scanner(source)
+        assert s is ts1, s
+
+        target.builder_set(Builder2(ts1))
+        target.builder.source_scanner = ts2
+        s = target.get_source_scanner(source)
+        assert s is ts2, s
+
+        builder = Builder1(env=Environment(SCANNERS = [ts3]))
+
+        targets = builder([source])
+        
+        s = targets[0].get_source_scanner(source)
+        assert s is ts3, s
+
 
     def test_scan(self):
         """Test Scanner functionality
         """
+        env = Environment()
         node = MyNode("nnn")
         node.builder = Builder()
-        node.env_set(Environment())
-        s = Scanner()
+        node.env_set(env)
+        x = MyExecutor(env, [node])
 
+        s = Scanner()
         d = MyNode("ddd")
         node.found_includes = [d]
 
-        assert node.target_scanner == None, node.target_scanner
-        node.target_scanner = s
+        node.builder.target_scanner = s
         assert node.implicit is None
 
         node.scan()
         assert s.called
         assert node.implicit == [d], node.implicit
 
+        # Check that scanning a node with some stored implicit
+        # dependencies resets internal attributes appropriately
+        # if the stored dependencies need recalculation.
+        class StoredNode(MyNode):
+            def get_stored_implicit(self):
+                return [MyNode('implicit1'), MyNode('implicit2')]
+
+        save_implicit_cache = SCons.Node.implicit_cache
+        save_implicit_deps_changed = SCons.Node.implicit_deps_changed
+        save_implicit_deps_unchanged = SCons.Node.implicit_deps_unchanged
+        SCons.Node.implicit_cache = 1
+        SCons.Node.implicit_deps_changed = None
+        SCons.Node.implicit_deps_unchanged = None
+        try:
+            sn = StoredNode("eee")
+            sn.builder_set(Builder())
+            sn.builder.target_scanner = s
+
+            sn.scan()
+
+            assert sn.implicit == [], sn.implicit
+            assert sn.children() == [], sn.children()
+
+        finally:
+            SCons.Node.implicit_cache = save_implicit_cache
+            SCons.Node.implicit_deps_changed = save_implicit_deps_changed
+            SCons.Node.implicit_deps_unchanged = save_implicit_deps_unchanged
+
     def test_scanner_key(self):
         """Test that a scanner_key() method exists"""
-        assert SCons.Node.Node().scanner_key() == None
+        assert SCons.Node.Node().scanner_key() is None
 
     def test_children(self):
         """Test fetching the non-ignored "children" of a Node.
@@ -654,9 +1043,9 @@ class NodeTestCase(unittest.TestCase):
         node.add_source([n1, n2, n3])
         node.add_dependency([n4, n5, n6])
         node.implicit = []
-        node.implicit_dict = {}
-        node._add_child(node.implicit, node.implicit_dict, [n7, n8, n9])
-        node._add_child(node.implicit, node.implicit_dict, [n10, n11, n12])
+        node.implicit_set = set()
+        node._add_child(node.implicit, node.implicit_set, [n7, n8, n9])
+        node._add_child(node.implicit, node.implicit_set, [n10, n11, n12])
         node.add_ignore([n2, n5, n8, n11])
 
         kids = node.children()
@@ -685,9 +1074,9 @@ class NodeTestCase(unittest.TestCase):
         node.add_source([n1, n2, n3])
         node.add_dependency([n4, n5, n6])
         node.implicit = []
-        node.implicit_dict = {}
-        node._add_child(node.implicit, node.implicit_dict, [n7, n8, n9])
-        node._add_child(node.implicit, node.implicit_dict, [n10, n11, n12])
+        node.implicit_set = set()
+        node._add_child(node.implicit, node.implicit_set, [n7, n8, n9])
+        node._add_child(node.implicit, node.implicit_set, [n10, n11, n12])
         node.add_ignore([n2, n5, n8, n11])
 
         kids = node.all_children()
@@ -698,7 +1087,7 @@ class NodeTestCase(unittest.TestCase):
         """Test setting and getting the state of a node
         """
         node = SCons.Node.Node()
-        assert node.get_state() == None
+        assert node.get_state() == SCons.Node.no_state
         node.set_state(SCons.Node.executing)
         assert node.get_state() == SCons.Node.executing
         assert SCons.Node.pending < SCons.Node.executing
@@ -714,23 +1103,23 @@ class NodeTestCase(unittest.TestCase):
 
         nw = SCons.Node.Walker(n1)
         assert not nw.is_done()
-        assert nw.next().name ==  "n1"
+        assert nw.get_next().name ==  "n1"
         assert nw.is_done()
-        assert nw.next() == None
+        assert nw.get_next() is None
 
         n2 = MyNode("n2")
         n3 = MyNode("n3")
         n1.add_source([n2, n3])
 
         nw = SCons.Node.Walker(n1)
-        n = nw.next()
+        n = nw.get_next()
         assert n.name ==  "n2", n.name
-        n = nw.next()
+        n = nw.get_next()
         assert n.name ==  "n3", n.name
-        n = nw.next()
+        n = nw.get_next()
         assert n.name ==  "n1", n.name
-        n = nw.next()
-        assert n == None, n
+        n = nw.get_next()
+        assert n is None, n
 
         n4 = MyNode("n4")
         n5 = MyNode("n5")
@@ -740,17 +1129,17 @@ class NodeTestCase(unittest.TestCase):
         n3.add_dependency([n6, n7])
 
         nw = SCons.Node.Walker(n1)
-        assert nw.next().name ==  "n4"
-        assert nw.next().name ==  "n5"
-        assert nw.history.has_key(n2)
-        assert nw.next().name ==  "n2"
-        assert nw.next().name ==  "n6"
-        assert nw.next().name ==  "n7"
-        assert nw.history.has_key(n3)
-        assert nw.next().name ==  "n3"
-        assert nw.history.has_key(n1)
-        assert nw.next().name ==  "n1"
-        assert nw.next() == None
+        assert nw.get_next().name ==  "n4"
+        assert nw.get_next().name ==  "n5"
+        assert n2 in nw.history
+        assert nw.get_next().name ==  "n2"
+        assert nw.get_next().name ==  "n6"
+        assert nw.get_next().name ==  "n7"
+        assert n3 in nw.history
+        assert nw.get_next().name ==  "n3"
+        assert n1 in nw.history
+        assert nw.get_next().name ==  "n1"
+        assert nw.get_next() is None
 
         n8 = MyNode("n8")
         n8.add_dependency([n3])
@@ -763,21 +1152,16 @@ class NodeTestCase(unittest.TestCase):
         global cycle_detected
 
         nw = SCons.Node.Walker(n3, cycle_func = cycle)
-        n = nw.next()
+        n = nw.get_next()
         assert n.name == "n6", n.name
-        n = nw.next()
+        n = nw.get_next()
         assert n.name == "n8", n.name
         assert cycle_detected
         cycle_detected = None
-        n = nw.next()
+        n = nw.get_next()
         assert n.name == "n7", n.name
-        n = nw.next()
-        assert nw.next() == None
-
-    def test_rstr(self):
-        """Test the rstr() method."""
-        n1 = MyNode("n1")
-        assert n1.rstr() == 'n1', n1.rstr()
+        n = nw.get_next()
+        assert nw.get_next() is None
 
     def test_abspath(self):
         """Test the get_abspath() method."""
@@ -798,134 +1182,16 @@ class NodeTestCase(unittest.TestCase):
 
             def for_signature(self):
                 return self.sig
-            
+
         n = TestNode("foo", "bar")
         assert n.get_string(0) == "foo", n.get_string(0)
         assert n.get_string(1) == "bar", n.get_string(1)
 
-    def test_arg2nodes(self):
-        """Test the arg2nodes function."""
-        dict = {}
-        class X(SCons.Node.Node):
-            pass
-        def Factory(name, directory = None, create = 1, dict=dict, X=X):
-            if not dict.has_key(name):
-                dict[name] = X()
-                dict[name].name = name
-            return dict[name]
-
-        nodes = SCons.Node.arg2nodes("Util.py UtilTests.py", Factory)
-        assert len(nodes) == 1, nodes
-        assert isinstance(nodes[0], X)
-        assert nodes[0].name == "Util.py UtilTests.py"
-
-        if hasattr(types, 'UnicodeType'):
-            code = """if 1:
-                nodes = SCons.Node.arg2nodes(u"Util.py UtilTests.py", Factory)
-                assert len(nodes) == 1, nodes
-                assert isinstance(nodes[0], X)
-                assert nodes[0].name == u"Util.py UtilTests.py"
-                \n"""
-            exec code in globals(), locals()
-
-        nodes = SCons.Node.arg2nodes(["Util.py", "UtilTests.py"], Factory)
-        assert len(nodes) == 2, nodes
-        assert isinstance(nodes[0], X)
-        assert isinstance(nodes[1], X)
-        assert nodes[0].name == "Util.py"
-        assert nodes[1].name == "UtilTests.py"
-
-        n1 = Factory("Util.py")
-        nodes = SCons.Node.arg2nodes([n1, "UtilTests.py"], Factory)
-        assert len(nodes) == 2, nodes
-        assert isinstance(nodes[0], X)
-        assert isinstance(nodes[1], X)
-        assert nodes[0].name == "Util.py"
-        assert nodes[1].name == "UtilTests.py"
-
-        class SConsNode(SCons.Node.Node):
-            pass
-        nodes = SCons.Node.arg2nodes(SConsNode())
-        assert len(nodes) == 1, nodes
-        assert isinstance(nodes[0], SConsNode), node
-
-        class OtherNode:
-            pass
-        nodes = SCons.Node.arg2nodes(OtherNode())
-        assert len(nodes) == 1, nodes
-        assert isinstance(nodes[0], OtherNode), node
-
-        def lookup_a(str, F=Factory):
-            if str[0] == 'a':
-                n = F(str)
-                n.a = 1
-                return n
-            else:
-                return None
-
-        def lookup_b(str, F=Factory):
-            if str[0] == 'b':
-                n = F(str)
-                n.b = 1
-                return n
-            else:
-                return None
-
-        SCons.Node.arg2nodes_lookups.append(lookup_a)
-        SCons.Node.arg2nodes_lookups.append(lookup_b)
-
-        nodes = SCons.Node.arg2nodes(['aaa', 'bbb', 'ccc'], Factory)
-        assert len(nodes) == 3, nodes
-
-        assert nodes[0].name == 'aaa', nodes[0]
-        assert nodes[0].a == 1, nodes[0]
-        assert not hasattr(nodes[0], 'b'), nodes[0]
-
-        assert nodes[1].name == 'bbb'
-        assert not hasattr(nodes[1], 'a'), nodes[1]
-        assert nodes[1].b == 1, nodes[1]
-
-        assert nodes[2].name == 'ccc'
-        assert not hasattr(nodes[2], 'a'), nodes[1]
-        assert not hasattr(nodes[2], 'b'), nodes[1]
-
-        def lookup_bbbb(str, F=Factory):
-            if str == 'bbbb':
-                n = F(str)
-                n.bbbb = 1
-                return n
-            else:
-                return None
-
-        def lookup_c(str, F=Factory):
-            if str[0] == 'c':
-                n = F(str)
-                n.c = 1
-                return n
-            else:
-                return None
-
-        nodes = SCons.Node.arg2nodes(['bbbb', 'ccc'], Factory,
-                                     [lookup_c, lookup_bbbb, lookup_b])
-        assert len(nodes) == 2, nodes
-
-        assert nodes[0].name == 'bbbb'
-        assert not hasattr(nodes[0], 'a'), nodes[1]
-        assert not hasattr(nodes[0], 'b'), nodes[1]
-        assert nodes[0].bbbb == 1, nodes[1]
-        assert not hasattr(nodes[0], 'c'), nodes[0]
-
-        assert nodes[1].name == 'ccc'
-        assert not hasattr(nodes[1], 'a'), nodes[1]
-        assert not hasattr(nodes[1], 'b'), nodes[1]
-        assert not hasattr(nodes[1], 'bbbb'), nodes[0]
-        assert nodes[1].c == 1, nodes[1]
-        
     def test_literal(self):
         """Test the is_literal() function."""
         n=SCons.Node.Node()
         assert n.is_literal()
-        
+
     def test_Annotate(self):
         """Test using an interface-specific Annotate function."""
         def my_annotate(node, self=self):
@@ -950,20 +1216,18 @@ class NodeTestCase(unittest.TestCase):
         n = SCons.Node.Node()
 
         n.set_state(3)
-        n.set_bsig('bsig')
-        n.set_csig('csig')
+        n.binfo = 'xyz'
         n.includes = 'testincludes'
         n.found_include = {'testkey':'testvalue'}
         n.implicit = 'testimplicit'
 
+        x = MyExecutor()
+        n.set_executor(x)
+
         n.clear()
 
-        assert n.get_state() is None, n.get_state()
-        assert not hasattr(n, 'bsig'), n.bsig
-        assert not hasattr(n, 'csig'), n.csig
         assert n.includes is None, n.includes
-        assert n.found_includes == {}, n.found_includes
-        assert n.implicit is None, n.implicit
+        assert x.cleaned_up
 
     def test_get_subst_proxy(self):
         """Test the get_subst_proxy method."""
@@ -971,15 +1235,84 @@ class NodeTestCase(unittest.TestCase):
 
         assert n.get_subst_proxy() == n, n.get_subst_proxy()
 
-    def test_get_prevsiginfo(self):
-        """Test the base Node get_prevsiginfo() method"""
+    def test_new_binfo(self):
+        """Test the new_binfo() method"""
         n = SCons.Node.Node()
-        siginfo = n.get_prevsiginfo()
-        assert siginfo == (None, None, None), siginfo
+        result = n.new_binfo()
+        assert isinstance(result, SCons.Node.BuildInfoBase), result
+
+    def test_get_suffix(self):
+        """Test the base Node get_suffix() method"""
+        n = SCons.Node.Node()
+        s = n.get_suffix()
+        assert s == '', s
+
+    def test_postprocess(self):
+        """Test calling the base Node postprocess() method"""
+        n = SCons.Node.Node()
+        n.waiting_parents = set( ['foo','bar'] )
+
+        n.postprocess()
+        assert n.waiting_parents == set(), n.waiting_parents
+
+    def test_add_to_waiting_parents(self):
+        """Test the add_to_waiting_parents() method"""
+        n1 = SCons.Node.Node()
+        n2 = SCons.Node.Node()
+        assert n1.waiting_parents == set(), n1.waiting_parents
+        r = n1.add_to_waiting_parents(n2)
+        assert r == 1, r
+        assert n1.waiting_parents == set((n2,)), n1.waiting_parents
+        r = n1.add_to_waiting_parents(n2)
+        assert r == 0, r
+
+
+class NodeListTestCase(unittest.TestCase):
+    def test___str__(self):
+        """Test"""
+        n1 = MyNode("n1")
+        n2 = MyNode("n2")
+        n3 = MyNode("n3")
+        nl = SCons.Node.NodeList([n3, n2, n1])
+
+        l = [1]
+        ul = collections.UserList([2])
+        try:
+            l.extend(ul)
+        except TypeError:
+            # An older version of Python (*cough* 1.5.2 *cough*)
+            # that doesn't allow UserList objects to extend lists.
+            pass
+        else:
+            s = str(nl)
+            assert s == "['n3', 'n2', 'n1']", s
+
+        r = repr(nl)
+        r = re.sub('at (0[xX])?[0-9a-fA-F]+', 'at 0x', r)
+        # Don't care about ancestry: just leaf value of MyNode
+        r = re.sub('<.*?\.MyNode', '<MyNode', r)
+        # New-style classes report as "object"; classic classes report
+        # as "instance"...
+        r = re.sub("object", "instance", r)
+        l = ", ".join(["<MyNode instance at 0x>"]*3)
+        assert r == '[%s]' % l, r
 
 
 
 if __name__ == "__main__":
-    suite = unittest.makeSuite(NodeTestCase, 'test_')
+    suite = unittest.TestSuite()
+    tclasses = [ BuildInfoBaseTestCase,
+                 NodeInfoBaseTestCase,
+                 NodeTestCase,
+                 NodeListTestCase ]
+    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: