Don't eat scanner exceptions.
authorstevenknight <stevenknight@fdb21ef1-2011-0410-befe-b5e4ea1792b1>
Mon, 3 Mar 2003 06:28:49 +0000 (06:28 +0000)
committerstevenknight <stevenknight@fdb21ef1-2011-0410-befe-b5e4ea1792b1>
Mon, 3 Mar 2003 06:28:49 +0000 (06:28 +0000)
git-svn-id: http://scons.tigris.org/svn/scons/trunk@605 fdb21ef1-2011-0410-befe-b5e4ea1792b1

src/CHANGES.txt
src/engine/SCons/Script/__init__.py
src/engine/SCons/Taskmaster.py
src/engine/SCons/TaskmasterTests.py
test/Scanner-exception.py [new file with mode: 0644]

index 22f0bdce11ccb2acac042010d7ed6b3c8c428191..f4fa6acc3a18f0d8236a51ee5b3ed11fe130ab59 100644 (file)
@@ -51,6 +51,9 @@ RELEASE 0.12 - XXX
     (e.g. ${SOURCES[2]}) when calculating the build signature of a
     command.
 
+  - Don't silently swallow exceptions thrown by Scanners (or other
+    exceptions while finding a node's dependent children).
+
   From Lachlan O'Dea:
 
   - Add SharedObject() support to the masm tool.
index 3716d2ee3d6b42b2c5b096f917adbd1c0d642009..b14e50d0e818f9114837de47cfa7f5788a190fca 100644 (file)
@@ -143,6 +143,8 @@ class BuildTask(SCons.Taskmaster.Task):
                 s = s + '  Stop.'
             sys.stderr.write("scons: *** %s\n" % s)
         else:
+            if e is None:
+                e = sys.exc_type
             sys.stderr.write("scons: *** %s\n" % e)
 
         self.do_failed()
index 521dc19f22e81e96dee4d943a95c0e2d6cf95f3a..68e251c506ce9524dcd37670da65e4f3d0016689 100644 (file)
@@ -73,13 +73,12 @@ class Task:
         This method is called from multiple threads in a parallel build,
         so only do thread safe stuff here.  Do thread unsafe stuff in
         prepare(), executed() or failed()."""
-        try:
-            # We recorded an exception while getting this Task ready
-            # for execution.  Raise it now.
-            raise self.node.exc_type, self.node.exc_value
-        except AttributeError:
-            # The normal case:  no exception to raise.
-            pass
+
+        # Now that it's the appropriate time, give the TaskMaster a
+        # chance to raise any exceptions it encountered while preparing
+        # this task.
+        self.tm.exception_raise()
+
         try:
             self.targets[0].build()
         except KeyboardInterrupt:
@@ -194,6 +193,7 @@ class Taskmaster:
         self.ready = None # the next task that is ready to be executed
         self.calc = calc
         self.order = order
+        self.exception_set(None, None)
 
     def _find_next_ready_node(self):
         """Find the next node that is ready to be built"""
@@ -218,10 +218,9 @@ class Taskmaster:
             except:
                 # We had a problem just trying to figure out the
                 # children (like a child couldn't be linked in to a
-                # BuildDir).  Arrange to raise the exception when the
-                # Task is "executed."
-                node.exc_type = sys.exc_type
-                node.exc_value = sys.exc_value
+                # BuildDir, or a Scanner threw something).  Arrange to
+                # raise the exception when the Task is "executed."
+                self.exception_set(sys.exc_type, sys.exc_value)
                 self.candidates.pop()
                 self.ready = node
                 break
@@ -296,8 +295,7 @@ class Taskmaster:
             # a child couldn't be linked in to a BuildDir when deciding
             # whether this node is current).  Arrange to raise the
             # exception when the Task is "executed."
-            node.exc_type = sys.exc_type
-            node.exc_value = sys.exc_value
+            self.exception_set(sys.exc_type, sys.exc_value)
         self.ready = None
 
         return task
@@ -332,3 +330,18 @@ class Taskmaster:
         self.candidates.extend(self.pending)
         self.pending = []
 
+    def exception_set(self, type, value):
+        """Record an exception type and value to raise later, at an
+        appropriate time."""
+        self.exc_type = type
+        self.exc_value = value
+
+    def exception_raise(self):
+        """Raise any pending exception that was recorded while
+        getting a Task ready for execution."""
+        if self.exc_type:
+            try:
+                raise self.exc_type, self.exc_value
+            finally:
+                self.exception_set(None, None)
+
index 5a19c56c37cc3ff9943d2efa677e36d33add995f..2f9383a5a97f7106ad7ac4e86cc5f9935fa0e5ae 100644 (file)
@@ -385,8 +385,8 @@ class TaskmasterTestCase(unittest.TestCase):
         n1 = Node("n1")
         tm = SCons.Taskmaster.Taskmaster(targets = [n1], tasker = MyTask)
         t = tm.next_task()
-        assert n1.exc_type == MyException, n1.exc_type
-        assert str(n1.exc_value) == "from make_ready()", n1.exc_value
+        assert tm.exc_type == MyException, tm.exc_type
+        assert str(tm.exc_value) == "from make_ready()", tm.exc_value
 
 
     def test_children_errors(self):
@@ -398,8 +398,8 @@ class TaskmasterTestCase(unittest.TestCase):
         n1 = MyNode("n1")
         tm = SCons.Taskmaster.Taskmaster([n1])
         t = tm.next_task()
-        assert n1.exc_type == SCons.Errors.StopError, "Did not record StopError on node"
-        assert str(n1.exc_value) == "stop!", "Unexpected exc_value `%s'" % n1.exc_value
+        assert tm.exc_type == SCons.Errors.StopError, "Did not record StopError on node"
+        assert str(tm.exc_value) == "stop!", "Unexpected exc_value `%s'" % tm.exc_value
 
     def test_cycle_detection(self):
         """Test detecting dependency cycles
@@ -590,9 +590,9 @@ class TaskmasterTestCase(unittest.TestCase):
 
         built_text = None
         n5 = Node("n5")
-        n5.exc_type = MyException
-        n5.exc_value = "exception value"
         tm = SCons.Taskmaster.Taskmaster([n5])
+        tm.exc_type = MyException
+        tm.exc_value = "exception value"
         t = tm.next_task()
         exc_caught = None
         try:
@@ -603,6 +603,39 @@ class TaskmasterTestCase(unittest.TestCase):
         assert exc_caught, "did not catch expected MyException"
         assert built_text is None, built_text
 
+    def test_exception(self):
+        """Test generic Taskmaster exception handling
+
+        """
+        n1 = Node("n1")
+        tm = SCons.Taskmaster.Taskmaster([n1])
+
+        tm.exception_set(1, 2)
+        assert tm.exc_type == 1, tm.exc_type
+        assert tm.exc_value == 2, tm.exc_value
+
+        tm.exception_set(None, None)
+        assert tm.exc_type is None, tm.exc_type
+        assert tm.exc_value is None, tm.exc_value
+
+        tm.exception_set("exception 1", None)
+        try:
+            tm.exception_raise()
+        except:
+            assert sys.exc_type == "exception 1", sys.exc_type
+            assert sys.exc_value is None, sys.exc_type
+        else:
+            assert 0, "did not catch expected exception"
+
+        tm.exception_set("exception 2", "xyzzy")
+        try:
+            tm.exception_raise()
+        except:
+            assert sys.exc_type == "exception 2", sys.exc_type
+            assert sys.exc_value == "xyzzy", sys.exc_type
+        else:
+            assert 0, "did not catch expected exception"
+
 
 
 if __name__ == "__main__":
diff --git a/test/Scanner-exception.py b/test/Scanner-exception.py
new file mode 100644 (file)
index 0000000..8fe7875
--- /dev/null
@@ -0,0 +1,112 @@
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+import sys
+import TestSCons
+
+python = TestSCons.python
+
+test = TestSCons.TestSCons()
+
+# Execute a subsidiary SConscript just to make sure we can
+# get at the SCanners keyword from there.
+
+test.write('SConstruct', """
+SConscript('SConscript')
+""")
+
+test.write('SConscript', """
+import re
+
+include_re = re.compile(r'^include\s+(\S+)$', re.M)
+exception_re = re.compile(r'^exception\s+(.+)$', re.M)
+
+def kfile_scan(node, env, target, arg):
+    contents = node.get_contents()
+    exceptions = exception_re.findall(contents)
+    if exceptions:
+        raise "kfile_scan error:  %s" % exceptions[0]
+    includes = include_re.findall(contents)
+    return includes
+
+kscan = Scanner(name = 'kfile',
+                function = kfile_scan,
+                argument = None,
+                recursive = 1,
+                skeys = ['.k'])
+
+def process(outf, inf):
+    for line in inf.readlines():
+        if line[:8] == 'include ':
+            file = line[8:-1]
+            process(outf, open(file, 'rb'))
+        else:
+            outf.write(line)
+
+def cat(env, source, target):
+    target = str(target[0])
+    source = map(str, source)
+
+    outf = open(target, 'wb')
+    for src in source:
+        process(outf, open(src, 'rb'))
+    outf.close()
+
+env = Environment(BUILDERS={'Cat':Builder(action=cat)})
+env.Append(SCANNERS = [kscan])
+
+env.Cat('foo', 'foo.k')
+
+bar_in = File('bar.in')
+env.Cat('bar', bar_in)
+bar_in.source_scanner = kscan
+""")
+
+test.write('foo.k', 
+"""foo.k 1 line 1
+include xxx
+include yyy
+foo.k 1 line 4
+""")
+
+test.write('bar.in', 
+"""include yyy
+bar.in 1 line 2
+bar.in 1 line 3
+include zzz
+""")
+
+test.write('xxx', "xxx 1\n")
+
+test.write('yyy', "exception yyy 1\n")
+
+test.write('zzz', "zzz 1\n")
+
+test.run(arguments = '.',
+         status = 2,
+         stderr = "scons: *** kfile_scan error:  yyy 1\n")
+
+test.pass_test()