Commits

Steven Knight  committed 5bea93b

rebuild in response to changed .c file

  • Participants
  • Parent commits 8425c00

Comments (0)

Files changed (12)

File src/engine/SCons/Builder.py

 
 import os
 import SCons.Node.FS
+import string
 import types
 
 
 	node = self.node_factory(target)
 	node.builder_set(self)
 	node.env_set(self)
-	node.sources = source	# XXX REACHING INTO ANOTHER OBJECT
+
+        # XXX REACHING INTO ANOTHER OBJECT (this is only temporary):
+        assert type(source) is type("")
+        node.sources = source
+        node.derived = 1
+        sources = string.split(source, " ")
+        sources = filter(lambda x: x, sources)
+        source_nodes = []
+        for source in sources:
+            source_node = self.node_factory(source)
+            source_node.derived = 0
+            source_node.source_nodes = []
+            source_nodes.append(source_node)
+        node.source_nodes = source_nodes
+        
 	return node
 
     def execute(self, **kw):

File src/engine/SCons/Node/FS.py

     def root(self):
         return self.parent.root()
 
+    def get_contents(self):
+        return open(self.path, "r").read()
+
+    def get_timestamp(self):
+        return os.path.getmtime(self.path)
+
+    def exists(self):
+        return os.path.exists(self.path)
 
 
 default_fs = FS()

File src/engine/SCons/Node/__init__.py

 
     def env_set(self, env):
 	self.env = env
+
+    def has_signature(self):
+        return hasattr(self, "signature")
+
+    def set_signature(self, signature):
+        self.signature = signature
+
+    def get_signature(self):
+        return self.signature
+

File src/engine/SCons/Sig/MD5.py

 import md5
 import string
 
-
+def current(obj, sig):
+    """Return whether a given object is up-to-date with the
+    specified signature.
+    """
+    return obj.get_signature() == sig
 
 def hexdigest(s):
     """Return a signature as a string of hex characters.
 	r = r + h[(i >> 4) & 0xF] + h[i & 0xF]
     return r
 
+def collect(signatures):
+    """
+    Collect a list of signatures into an aggregate signature.
 
+    signatures - a list of signatures
+    returns - the aggregate signature
+    """
+    if len(signatures) == 1:
+	return signatures[0]
+    else:
+        contents = string.join(signatures, ', ')
+	return hexdigest(md5.new(contents).digest())
 
-def _init():
-    pass	# XXX
+def signature(obj):
+    """Generate a signature for an object
+    """
+    return hexdigest(md5.new(obj.get_contents()).digest())
 
-def _end():
-    pass	# XXX
+def to_string(signature):
+    """Convert a signature to a string"""
+    return signature
 
-def current(obj, sig):
-    """Return whether a given object is up-to-date with the
-    specified signature.
-    """
-    return obj.signature() == sig
-
-def set():
-    pass	# XXX
-
-def invalidate():
-    pass	# XXX
-
-def collect(*objects):
-    """Collect signatures from a list of objects, returning the
-    aggregate signature of the list.
-    """
-    if len(objects) == 1:
-	sig = objects[0].signature()
-    else:
-	contents = string.join(map(lambda o: o.signature(), objects), ', ')
-	sig = signature(contents)
-#    if debug:
-#	pass
-    return sig
-
-def signature(contents):
-    """Generate a signature for a byte string.
-    """
-    return hexdigest(md5.new(contents).digest())
-
-def cmdsig():
-    pass	# XXX
-
-def srcsig():
-    pass	# XXX
+def from_string(string):
+    """Convert a string to a signature"""
+    return string

File src/engine/SCons/Sig/MD5Tests.py

 import sys
 import unittest
 
-import SCons.Sig.MD5
+from SCons.Sig.MD5 import current, collect, signature, to_string, from_string
+
 
 
 
 
     def __init__(self, value = ""):
 	self.value = value
-	self.sig = None
-
-    def signature(self):
-	if not self.sig:
-	    self.sig = SCons.Sig.MD5.signature(self.value)
+        
+    def get_signature(self):
+        if not hasattr(self, "sig"):
+	    self.sig = signature(self)
 	return self.sig
 
-    def current(self, sig):
-	return SCons.Sig.MD5.current(self, sig)
+    def get_contents(self):
+	return self.value
 
 
 
 class MD5TestCase(unittest.TestCase):
 
-    def test__init(self):
-	pass	# XXX
-
-    def test__end(self):
-	pass	# XXX
-
     def test_current(self):
 	"""Test deciding if an object is up-to-date
 
 	Simple comparison of different "signature" values.
 	"""
-	o111 = my_obj(value = '111')
-	assert not o111.current(SCons.Sig.MD5.signature('110'))
-	assert     o111.current(SCons.Sig.MD5.signature('111'))
-	assert not o111.current(SCons.Sig.MD5.signature('112'))
-
-    def test_set(self):
-	pass	# XXX
-
-    def test_invalidate(self):
-	pass	# XXX
+	obj = my_obj('111')
+	assert not current(obj, signature(my_obj('110')))
+	assert     current(obj, signature(my_obj('111')))
+	assert not current(obj, signature(my_obj('112')))
 
     def test_collect(self):
 	"""Test collecting a list of signatures into a new signature value
 	"""
-	o1 = my_obj(value = '111')
-	o2 = my_obj(value = '222')
-	o3 = my_obj(value = '333')
-	assert '698d51a19d8a121ce581499d7b701668' == SCons.Sig.MD5.collect(o1)
-	assert '8980c988edc2c78cc43ccb718c06efd5' == SCons.Sig.MD5.collect(o1, o2)
-	assert '53fd88c84ff8a285eb6e0a687e55b8c7' == SCons.Sig.MD5.collect(o1, o2, o3)
+        s = map(signature, map(my_obj, ('111', '222', '333')))
+        
+        assert '698d51a19d8a121ce581499d7b701668' == collect(s[0:1])
+        assert '8980c988edc2c78cc43ccb718c06efd5' == collect(s[0:2])
+	assert '53fd88c84ff8a285eb6e0a687e55b8c7' == collect(s)
 
     def test_signature(self):
-	pass	# XXX
+        """Test generating a signature"""
+	o1 = my_obj(value = '111')
+        assert '698d51a19d8a121ce581499d7b701668' == signature(o1)
 
-    def test_cmdsig(self):
-	pass	# XXX
+    def test_to_string(self):
+        assert '698d51a19d8a121ce581499d7b701668' == to_string('698d51a19d8a121ce581499d7b701668')
 
-    def test_srcsig(self):
-	pass	# XXX
-
+    def test_from_string(self):
+        assert '698d51a19d8a121ce581499d7b701668' == from_string('698d51a19d8a121ce581499d7b701668')
 
 if __name__ == "__main__":
     suite = unittest.makeSuite(MD5TestCase, 'test_')

File src/engine/SCons/Sig/SigTests.py

+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+import unittest
+import TestCmd
+import SCons.Sig
+import SCons.Sig.MD5
+import SCons.Sig.TimeStamp
+import sys
+
+
+class DummyFile:
+    """A class that simulates a file for testing purposes"""
+    def __init__(self, path, contents, timestamp, derived):
+        self.path = path
+        self.contents = contents
+        self.timestamp = timestamp
+        self.derived = derived
+
+    def modify(self, contents, timestamp):
+        self.contents = contents
+        self.timestamp = timestamp
+
+class DummyNode:
+    """A node workalike for testing purposes"""
+
+    def __init__(self, file):
+        self.file = file
+        self.path = file.path
+        self.derived = file.derived
+        
+    def get_contents(self):
+        # a file that doesn't exist has no contents:
+        assert self.exists()
+        
+        return self.file.contents
+
+    def get_timestamp(self):
+        # a file that doesn't exist has no timestamp:
+        assert self.exists()
+
+        return self.file.timestamp
+
+    def exists(self):
+        return not self.file.contents is None
+        
+    def has_signature(self):
+        return hasattr(self, "sig")
+
+    def set_signature(self, sig):
+        self.sig = sig
+
+    def get_signature(self):
+        return self.sig
+
+
+def create_files(test):
+    args  = [(test.workpath('f1.c'), 'blah blah', 111, 0),     #0
+             (test.workpath('f1'), None, 0, 1),                #1
+             (test.workpath('d1/f1.c'), 'blah blah', 111, 0),  #2
+             (test.workpath('d1/f1.h'), 'blah blah', 111, 0),  #3
+             (test.workpath('d1/f2.c'), 'blah blah', 111, 0),  #4
+             (test.workpath('d1/f3.h'), 'blah blah', 111, 0),  #5
+             (test.workpath('d1/f4.h'), 'blah blah', 111, 0),  #6
+             (test.workpath('d1/f1'), None, 0, 1),             #7
+             (test.workpath('d1/test.c'), 'blah blah', 111, 0),#8
+             (test.workpath('d1/test.o'), None, 0, 1),         #9
+             (test.workpath('d1/test'), None, 0, 1)]           #10
+    
+    files = map(lambda x: apply(DummyFile, x), args)
+
+    return files
+
+def create_nodes(files):
+    nodes = map(DummyNode, files)
+
+    nodes[0].source_nodes = []
+    nodes[1].source_nodes = [nodes[0]]
+    nodes[2].source_nodes = [nodes[3]]
+    nodes[3].source_nodes = []
+    nodes[4].source_nodes = [nodes[5]]
+    nodes[5].source_nodes = [nodes[6]]
+    nodes[6].source_nodes = [nodes[5]]
+    nodes[7].source_nodes = [nodes[2], nodes[4]]
+    nodes[8].source_nodes = []
+    nodes[9].source_nodes = [nodes[8]]
+    nodes[10].source_nodes = [nodes[9]]
+
+    return nodes
+        
+
+class SigTestBase:
+    
+    def runTest(self):
+
+        test = TestCmd.TestCmd(workdir = '')
+        test.subdir('d1')
+        
+        self.files = create_files(test)
+        self.test_initial()
+        self.test_built()
+        self.test_modify()
+        self.test_delete()
+        
+    def test_initial(self):
+        
+        nodes = create_nodes(self.files)
+        calc = SCons.Sig.Calculator(self.module)
+
+        for node in nodes:
+            self.failUnless(not calc.current(node), "none of the nodes should be current")
+
+        # simulate a build:
+        self.files[1].modify('built', 222)
+        self.files[7].modify('built', 222)
+        self.files[9].modify('built', 222)
+        self.files[10].modify('built', 222)
+        
+        calc.write(nodes)
+
+    def test_built(self):
+
+        nodes = create_nodes(self.files)
+
+        calc = SCons.Sig.Calculator(self.module)
+        
+        for node in nodes:
+            self.failUnless(calc.current(node), "all of the nodes should be current")
+
+        calc.write(nodes)
+
+    def test_modify(self):
+
+        nodes = create_nodes(self.files)
+
+        #simulate a modification of some files
+        self.files[0].modify('blah blah blah', 333)
+        self.files[3].modify('blah blah blah', 333)
+        self.files[6].modify('blah blah blah', 333)
+        self.files[8].modify('blah blah blah', 333)
+
+        calc = SCons.Sig.Calculator(self.module)
+
+        self.failUnless(not calc.current(nodes[0]), "modified directly")
+        self.failUnless(not calc.current(nodes[1]), "direct source modified")
+        self.failUnless(calc.current(nodes[2]))
+        self.failUnless(not calc.current(nodes[3]), "modified directly")
+        self.failUnless(calc.current(nodes[4]))
+        self.failUnless(calc.current(nodes[5]))
+        self.failUnless(not calc.current(nodes[6]), "modified directly")
+        self.failUnless(not calc.current(nodes[7]), "indirect source modified")
+        self.failUnless(not calc.current(nodes[8]), "modified directory")
+        self.failUnless(not calc.current(nodes[9]), "direct source modified")
+        self.failUnless(not calc.current(nodes[10]), "indirect source modified")
+
+        calc.write(nodes)
+
+    def test_delete(self):
+        
+        nodes = create_nodes(self.files)
+
+        #simulate the deletion of some files
+        self.files[1].modify(None, 0)
+        self.files[7].modify(None, 0)
+        self.files[9].modify(None, 0)
+        
+        calc = SCons.Sig.Calculator(self.module)
+
+        self.failUnless(calc.current(nodes[0]))
+        self.failUnless(not calc.current(nodes[1]), "deleted")
+        self.failUnless(calc.current(nodes[2]))
+        self.failUnless(calc.current(nodes[3]))
+        self.failUnless(calc.current(nodes[4]))
+        self.failUnless(calc.current(nodes[5]))
+        self.failUnless(calc.current(nodes[6]))
+        self.failUnless(not calc.current(nodes[7]), "deleted")
+        self.failUnless(calc.current(nodes[8]))
+        self.failUnless(not calc.current(nodes[9]), "deleted")
+        self.failUnless(calc.current(nodes[10]),
+                        "current even though it's source was deleted") 
+
+        calc.write(nodes)
+
+class MD5TestCase(unittest.TestCase, SigTestBase):
+    """Test MD5 signatures"""
+
+    module = SCons.Sig.MD5
+
+class TimeStampTestCase(unittest.TestCase, SigTestBase):
+    """Test timestamp signatures"""
+
+    module = SCons.Sig.TimeStamp
+
+def suite():
+    suite = unittest.TestSuite()
+    suite.addTest(MD5TestCase())
+    suite.addTest(TimeStampTestCase())
+    return suite
+
+if __name__ == "__main__":
+    runner = unittest.TextTestRunner()
+    result = runner.run(suite())
+    if not result.wasSuccessful():
+        sys.exit(1)
+    

File src/engine/SCons/Sig/TimeStamp.py

 
 __revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
 
-def _init():
-    pass	# XXX
+def current(obj, sig):
+    """Return whether the objects timestamp is up-to-date.
+    """
+    return obj.get_signature() >= sig
 
-def _end():
-    pass	# XXX
+def collect(signatures):
+    """
+    Collect a list of timestamps, returning
+    the most-recent timestamp from the list 
 
-def current(obj, sig):
-    """Return whether the object's timestamp is up-to-date.
+    signatures - a list of timestamps
+    returns - the most recent timestamp
     """
-    return obj.signature() >= sig
 
-def set():
-    pass	# XXX
+    if len(signatures) == 0:
+        return 0
+    elif len(signatures) == 1:
+        return signatures[0]
+    else:
+        return max(signatures)
 
-def invalidate():
-    pass	# XXX
-
-def collect(*objects):
-    """Collect timestamps from a list of objects, returning
-    the most-recent timestamp from the list.
-    """
-    r = 0
-    for obj in objects:
-	s = obj.signature()
-	if s > r:
-	    r = s
-    return r
-
-def signature(contents):
+def signature(obj):
     """Generate a timestamp.
     """
-    pass	# XXX
-#    return md5.new(contents).hexdigest()	# 2.0
-    return hexdigest(md5.new(contents).digest())
+    return obj.get_timestamp()
 
-def cmdsig():
-    pass	# XXX
+def to_string(signature):
+    """Convert a timestamp to a string"""
+    return str(signature)
 
-def srcsig():
-    pass	# XXX
+def from_string(string):
+    """Convert a string to a timestamp"""
+    return int(string)
+
+

File src/engine/SCons/Sig/TimeStampTests.py

 import sys
 import unittest
 
-import SCons.Sig.TimeStamp
+from SCons.Sig.TimeStamp import current, collect, signature, to_string, from_string
 
 
 
     requirements of the TimeStamp class.
     """
 
-    def __init__(self, value = ""):
+    def __init__(self, value = 0):
 	self.value = value
 
-    def signature(self):
+    def get_signature(self):
 	return self.value
 
+    def get_timestamp(self):
+        return self.value
 
 
 class TimeStampTestCase(unittest.TestCase):
 
-    def test__init(self):
-	pass	# XXX
-
-    def test__init(self):
-	pass	# XXX
-
-    def test__end(self):
-	pass	# XXX
-
     def test_current(self):
 	"""Test deciding if an object is up-to-date
 
 	Simple comparison of different timestamp values.
 	"""
 	o1 = my_obj(value = 111)
-	assert SCons.Sig.TimeStamp.current(o1, 110)
-	assert SCons.Sig.TimeStamp.current(o1, 111)
-	assert not SCons.Sig.TimeStamp.current(o1, 112)
-
-    def test_set(self):
-	pass	# XXX
-
-    def test_invalidate(self):
-	pass	# XXX
+	assert current(o1, 110)
+	assert current(o1, 111)
+	assert not current(o1, 112)
 
     def test_collect(self):
 	"""Test collecting a list of signatures into a new signature value
 	into a new timestamp value.
 	"""
-	o1 = my_obj(value = 111)
-	o2 = my_obj(value = 222)
-	o3 = my_obj(value = 333)
-	assert 111 == SCons.Sig.TimeStamp.collect(o1)
-	assert 222 == SCons.Sig.TimeStamp.collect(o1, o2)
-	assert 333 == SCons.Sig.TimeStamp.collect(o1, o2, o3)
+        
+	assert 111 == collect((111,))
+	assert 222 == collect((111, 222))
+	assert 333 == collect((333, 222, 111))
 
     def test_signature(self):
-	pass	# XXX
+        """Test generating a signature"""
+        o1 = my_obj(value = 111)
+        assert 111 == signature(o1)
 
-    def test_cmdsig(self):
-	pass	# XXX
+    def test_to_string(self):
+        assert '111' == to_string(111)
 
-    def test_srcsig(self):
-	pass	# XXX
+    def test_from_string(self):
+        assert 111 == from_string('111')
 
 
 if __name__ == "__main__":

File src/engine/SCons/Sig/__init__.py

 """SCons.Sig
 
-The Signature package for the SCons software construction utility.
+The Signature package for the scons software construction utility.
 
 """
 
 __revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+import os.path
+import string
+
+class SConsignFile:
+    """
+    Encapsulates reading and writing a .sconsign file.
+    """
+
+    def __init__(self, dir, module):
+        """
+        dir - the directory for the file
+        module - the signature module being used
+        """
+        
+        self.path = os.path.join(dir, '.sconsign')
+        self.entries = {}
+                    
+        try:
+            file = open(self.path, 'rt')
+        except:
+            pass
+        else:
+            for line in file.readlines():
+                filename, rest = map(string.strip, string.split(line, ":"))
+                time, signature = map(string.strip, string.split(rest, " "))
+                self.entries[filename] = (int(time), module.from_string(signature))
+
+    def get(self, filename):
+        """
+        Get the signature for a file
+
+        filename - the filename whose signature will be returned
+        returns - (timestamp, signature)
+        """
+        
+        try:
+            return self.entries[filename]
+        except KeyError:
+            return (0, None)
+
+    def set(self, filename, timestamp, signature, module):
+        """
+        Set the signature for a file
+
+        filename - the filename whose signature will be set
+        timestamp - the timestamp
+        signature - the signature
+        module - the signature module being used
+        """
+        self.entries[filename] = (timestamp, module.to_string(signature))
+
+    def write(self):
+        """
+        Write the .sconsign file to disk.
+        """
+        
+        file = open(self.path, 'wt')
+        for item in self.entries.items():
+            file.write("%s: %d %s\n" % (item[0], item[1][0], item[1][1]))
+
+
+class Calculator:
+    """
+    Encapsulates signature calculations and .sconsign file generating
+    for the build engine.
+    """
+
+    def __init__(self, module):
+        """
+        Initialize the calculator.
+
+        module - the signature module to use for signature calculations
+        """
+        self.module = module
+        self.sig_files = {}
+
+    
+    def collect(self, node, signatures):
+        """
+        Collect the signatures of the node's sources.
+
+        node - the node whose sources will be collected
+        signatures - the dictionary that the signatures will be
+        gathered into.
+        """
+        for source_node in node.source_nodes:
+            if not signatures.has_key(source_node):
+                signature = self.signature(source_node)
+                signatures[source_node] = signature
+                self.collect(source_node, signatures)
+
+    def get_sig_file(self, dir):
+        """
+        Get a sconsign file from the cache, or add it to the cache.
+
+        dir - the dir for the sconsign file
+        returns - the sconsign file
+        """
+        if self.sig_files.has_key(dir):
+            return self.sig_files[dir]
+        else:
+            self.sig_files[dir] = SConsignFile(dir, self.module)
+            return self.sig_files[dir]
+
+    def signature(self, node):
+        """
+        Get the signature for a node.
+
+        node - the node
+        returns - the signature or None if the signature could not
+        be computed.
+
+        This method also stores the signature in the node and
+        in the .sconsign file.
+        """
+
+        if node.has_signature():
+            sig = node.get_signature()
+        elif node.derived:
+            signatures = {}
+            self.collect(node, signatures)
+            signatures = filter(lambda x: not x is None, signatures.values())
+            sig = self.module.collect(signatures)
+        else:
+            if not node.exists():
+                return None
+            
+            # XXX handle nodes that are not under the source root
+            sig = self.module.signature(node)
+
+        node.set_signature(sig)
+
+        dir, filename = os.path.split(node.path)
+        if node.exists():
+            timestamp = node.get_timestamp()
+        else:
+            timestamp = 0
+            
+        self.get_sig_file(dir).set(filename,
+                                   timestamp,
+                                   sig,
+                                   self.module)
+
+        return sig
+
+    def current(self, node):
+        """
+        Check if a node is up to date.
+
+        node - the node whose signature will be checked
+
+        returns - 0 if the signature has changed since the last invocation,
+        and 1 if it hasn't
+        """
+
+        if not node.exists():
+            return 0
+
+        dir, filename = os.path.split(node.path)
+        oldtime, oldsig = self.get_sig_file(dir).get(filename)
+
+        newtime = node.get_timestamp()
+
+        if not node.derived and newtime == oldtime:
+            newsig = oldsig
+        else:
+            newsig = self.signature(node)
+        
+        return newsig == oldsig
+
+    def write(self, nodes):
+        """
+        Write out all of the signature files.
+        
+        nodes - the nodes whose signatures may have changed durring
+        the build
+        """
+
+        # make sure all the signatures have been calculated:
+        for node in nodes:
+            self.signature(node)
+            
+        for sig_file in self.sig_files.values():
+            sig_file.write()
+        

File src/script/scons.py

 import SCons.Node.FS
 import SCons.Job
 from SCons.Errors import *
+import SCons.Sig
+import SCons.Sig.MD5
 
 #
 # Modules and classes that we don't use directly in this script, but
     if not targets:
 	targets = default_targets
 
-    taskmaster = Taskmaster(map(
-    			lambda x: SCons.Node.FS.default_fs.File(x),
-			targets))
+    calc = SCons.Sig.Calculator(SCons.Sig.MD5)
+    nodes = map(lambda x: SCons.Node.FS.default_fs.File(x), targets)
+    nodes = filter(lambda x, calc=calc: not calc.current(x), nodes)
+    
+    taskmaster = Taskmaster(nodes)
 
     jobs = SCons.Job.Jobs(num_jobs, taskmaster)
     jobs.start()
     jobs.wait()
 
+    calc.write(nodes)
+
 if __name__ == "__main__":
     try:
         main()

File test/Program.py

 __revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
 
 import TestSCons
+import os.path
+import time
 
 #XXX Future:  be able to interpolate
 
 
 #XXXtest.up_to_date(arguments = '.')
 
+# make sure the programs don't get rebuilt, because nothing changed:
+oldtime1 = os.path.getmtime(test.workpath('foo1'))
+oldtime2 = os.path.getmtime(test.workpath('foo2'))
+time.sleep(1) # introduce a small delay, to make the test valid
+test.run(arguments = 'foo1 foo2')
+test.fail_test(not (oldtime1 == os.path.getmtime(test.workpath('foo1'))))
+test.fail_test(not (oldtime2 == os.path.getmtime(test.workpath('foo2'))))
+
 test.pass_test()

File test/option-j.py

 env.MyBuild(target = 'f2', source = 'f2.in')
 """)
 
-def RunTest(args):
-    test.write('f1.in', 'f1.in')
-    test.write('f2.in', 'f2.in')
+def RunTest(args, extra):
+    """extra is used to make scons rebuild the output file"""
+    test.write('f1.in', 'f1.in'+extra)
+    test.write('f2.in', 'f2.in'+extra)
 
     test.run(arguments = args)
 
 
     return start2, finish1
 
-start2, finish1 = RunTest('-j 2 f1 f2')
+start2, finish1 = RunTest('-j 2 f1 f2', "first")
 
 # fail if the second file was not started
 # before the first one was finished
 test.fail_test(not (start2 < finish1))
 
-start2, finish1 = RunTest('f1 f2')
+s2, f1 = RunTest('-j 2 f1 f2', "first")
+
+# re-run the test with the same input, fail if we don't
+# get back the same times, which would indicate that
+# SCons rebuilt the files even though nothing changed
+test.fail_test(start2 != s2)
+test.fail_test(finish1 != f1)
+
+start2, finish1 = RunTest('f1 f2', "second")
 
 # fail if the second file was started
 # before the first one was finished