Initial revision
authorstevenknight <stevenknight@fdb21ef1-2011-0410-befe-b5e4ea1792b1>
Fri, 6 Jul 2001 11:46:17 +0000 (11:46 +0000)
committerstevenknight <stevenknight@fdb21ef1-2011-0410-befe-b5e4ea1792b1>
Fri, 6 Jul 2001 11:46:17 +0000 (11:46 +0000)
git-svn-id: http://scons.tigris.org/svn/scons/trunk@2 fdb21ef1-2011-0410-befe-b5e4ea1792b1

34 files changed:
Construct [new file with mode: 0644]
TestCmd.py [new file with mode: 0644]
config [new file with mode: 0644]
runtest.py [new file with mode: 0644]
src/.aeignore [new file with mode: 0644]
src/MANIFEST [new file with mode: 0644]
src/scons.py [new file with mode: 0644]
src/scons/.aeignore [new file with mode: 0644]
src/scons/Builder.py [new file with mode: 0644]
src/scons/BuilderTests.py [new file with mode: 0644]
src/scons/Defaults.py [new file with mode: 0644]
src/scons/Environment.py [new file with mode: 0644]
src/scons/EnvironmentTests.py [new file with mode: 0644]
src/scons/Node/.aeignore [new file with mode: 0644]
src/scons/Node/FS.py [new file with mode: 0644]
src/scons/Node/FS/.aeignore [new file with mode: 0644]
src/scons/Node/FSTests.py [new file with mode: 0644]
src/scons/Node/NodeTests.py [new file with mode: 0644]
src/scons/Node/__init__.py [new file with mode: 0644]
src/scons/Sig/.aeignore [new file with mode: 0644]
src/scons/Sig/MD5.py [new file with mode: 0644]
src/scons/Sig/MD5Tests.py [new file with mode: 0644]
src/scons/Sig/TimeStamp.py [new file with mode: 0644]
src/scons/Sig/TimeStampTests.py [new file with mode: 0644]
src/scons/Sig/__init__.py [new file with mode: 0644]
src/scons/__init__.py [new file with mode: 0644]
src/setup.py [new file with mode: 0644]
template/.aeignore [new file with mode: 0644]
template/__init__.py [new file with mode: 0644]
template/file.py [new file with mode: 0644]
template/test.py [new file with mode: 0644]
test/.aeignore [new file with mode: 0644]
test/t0001.t [new file with mode: 0644]
test/t0010.py [new file with mode: 0644]

diff --git a/Construct b/Construct
new file mode 100644 (file)
index 0000000..bf77bb7
--- /dev/null
+++ b/Construct
@@ -0,0 +1,112 @@
+#
+# Construct file to build scons during development.
+# (Kind of ironic that we're using the classic Perl Cons
+# to build its Python child...)
+#
+$project = 'scons';
+
+$env = new cons( ENV => {
+                         AEGIS_PROJECT => $ENV{AEGIS_PROJECT},
+                         PATH => $ENV{PATH},
+                       } );
+
+Default qw( . );
+
+#
+# Grab the information that we "build" into the files (using sed).
+#
+chomp($date = $ARG{date} || `date '+%Y/%m/%d %H:%M:%S'`);
+
+$developer = $ARG{developer} || '???';
+
+chomp($revision = $ARG{version} || `aesub '\$version' 2>/dev/null` || '0.01');
+
+@arr = split(/\./, $revision);
+@arr = ($arr[0], map {length($_) == 1 ? "0$_" : $_} @arr[1 .. $#arr]);
+$revision = join('.', @arr);
+pop @arr if $#arr >= 2;
+map {s/^[CD]//, s/^0*(\d\d)$/$1/} @arr;
+$version = join('.', @arr);
+
+#
+# We use %(-%) around the date so date changes don't cause rebuilds.
+#
+$sed_cmd = "sed" .
+       " %( -e 's+__DATE__+$date+' %)" .
+       " -e 's+__DEVELOPER__+$developer+'" .
+       " -e 's+__REVISION__+$revision+'" .
+       " -e 's+__VERSION__+$version+'" .
+       " %< > %>";
+
+#
+# Run everything in the MANIFEST through the sed command we concocted.
+#
+chomp(@files = `cat src/MANIFEST`);
+
+foreach $file (@files) {
+    Command $env "build/$file", "src/$file", $sed_cmd;
+}
+
+#
+# Use the Python distutils to generate the packages.
+#
+$tar_gz = "build/dist/$project-$version.tar.gz";
+
+@targets = (
+    "build/build/bdist.linux-i686/rpm/SOURCES/$project-$version.tar.gz",
+    "build/build/bdist.linux-i686/rpm/SPECS/$project.spec",
+    $tar_gz,
+    "build/dist/$project-$version-1.src.rpm",
+    "build/dist/$project-$version.linux-i686.tar.gz",
+    "build/dist/$project-$version-1.noarch.rpm",
+);
+
+@build_files = map("build/$_", @files);
+
+Command $env [@targets], @build_files, qq(
+    rm -rf build/build build/dist/*
+    cd build && python setup.py bdist bdist_rpm
+);
+
+Depends $env [@targets], 'build/MANIFEST';
+
+#
+# Unpack the .tar.gz created by the distutils into build/test, and
+# add the TestCmd.py module.  The runtest.py script will set PYTHONPATH
+# so that the tests only look under build/test.  This makes sure that
+# our tests pass with what we really packaged, not because of something
+# hanging around in the development directory.
+#
+$test_dir = "build/test";
+
+Command $env "$test_dir/$project-$version/$project/__init__.py", $tar_gz, qq(
+    rm -rf $test_dir/$project-$version
+    tar zxf %< -C $test_dir
+);
+
+Install $env $test_dir, "TestCmd.py";
+
+#
+# If we're running in the actual Aegis project, pack up a complete
+# source .tar.gz from the project files and files in the change,
+# so we can share it with helpful developers who don't use Aegis.
+#
+eval '@src_files = grep($_ !~ /\.(aeignore|consign)$/ && ! $seen{$_}++,
+                   `aegis -list -terse pf 2>/dev/null`,
+                   `aegis -list -terse cf 2>/dev/null`)';
+if (@src_files) {
+    chomp(@src_files);
+
+    foreach $file (@src_files) {
+       Command $env "build/$project-src/$file", $file, $sed_cmd;
+    }
+
+    Command $env "build/dist/$project-src-$version.tar.gz",
+               $tar_gz,
+               map("build/$project-src/$_", @src_files), qq(
+       rm -rf build/$project-src-$version
+       cp -r build/$project-src build/$project-src-$version
+       find build/$project-src-$version -name .consign -exec rm {} \\;
+       cd build && tar zcf dist/%>:f $project-src-$version
+    );
+}
diff --git a/TestCmd.py b/TestCmd.py
new file mode 100644 (file)
index 0000000..438bbed
--- /dev/null
@@ -0,0 +1,551 @@
+"""
+TestCmd.py:  a testing framework for commands and scripts.
+
+The TestCmd module provides a framework for portable automated testing
+of executable commands and scripts (in any language, not just Python),
+especially commands and scripts that require file system interaction.
+
+In addition to running tests and evaluating conditions, the TestCmd module
+manages and cleans up one or more temporary workspace directories, and
+provides methods for creating files and directories in those workspace
+directories from in-line data, here-documents), allowing tests to be
+completely self-contained.
+
+A TestCmd environment object is created via the usual invocation:
+
+    test = TestCmd()
+
+The TestCmd module provides pass_test(), fail_test(), and no_result()
+unbound methods that report test results for use with the Aegis change
+management system.  These methods terminate the test immediately,
+reporting PASSED, FAILED, or NO RESULT respectively, and exiting with
+status 0 (success), 1 or 2 respectively.  This allows for a distinction
+between an actual failed test and a test that could not be properly
+evaluated because of an external condition (such as a full file system
+or incorrect permissions).
+"""
+
+# Copyright 2000 Steven Knight
+# This module is free software, and you may redistribute it and/or modify
+# it under the same terms as Python itself, so long as this copyright message
+# and disclaimer are retained in their original form.
+#
+# IN NO EVENT SHALL THE AUTHOR BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT,
+# SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OF
+# THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
+# DAMAGE.
+#
+# THE AUTHOR SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+# PARTICULAR PURPOSE.  THE CODE PROVIDED HEREUNDER IS ON AN "AS IS" BASIS,
+# AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
+# SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
+
+from string import join, split
+
+__author__ = "Steven Knight <knight@baldmt.com>"
+__revision__ = "TestCmd.py 0.D001 2001/01/14 00:43:41 software"
+__version__ = "0.01"
+
+from types import *
+
+import FCNTL
+import os
+import os.path
+import popen2
+import re
+import shutil
+import stat
+import sys
+import tempfile
+import traceback
+
+tempfile.template = 'testcmd.'
+
+_Cleanup = []
+
+def _clean():
+    global _Cleanup
+    list = _Cleanup[:]
+    _Cleanup = []
+    list.reverse()
+    for test in list:
+       test.cleanup()
+
+sys.exitfunc = _clean
+
+def _caller(tblist, skip):
+    string = ""
+    arr = []
+    for file, line, name, text in tblist:
+       if file[-10:] == "TestCmd.py":
+               break
+       arr = [(file, line, name, text)] + arr
+    atfrom = "at"
+    for file, line, name, text in arr[skip:]:
+       if name == "?":
+           name = ""
+       else:
+           name = " (" + name + ")"
+       string = string + ("%s line %d of %s%s\n" % (atfrom, line, file, name))
+       atfrom = "\tfrom"
+    return string
+
+def fail_test(self = None, condition = 1, function = None, skip = 0):
+    """Cause the test to fail.
+
+    By default, the fail_test() method reports that the test FAILED
+    and exits with a status of 1.  If a condition argument is supplied,
+    the test fails only if the condition is true.
+    """
+    if not condition:
+       return
+    if not function is None:
+       function()
+    of = ""
+    desc = ""
+    sep = " "
+    if not self is None:
+       if self.program:
+           of = " of " + self.program
+           sep = "\n\t"
+       if self.description:
+           desc = " [" + self.description + "]"
+           sep = "\n\t"
+
+    at = _caller(traceback.extract_stack(), skip)
+    sys.stderr.write("FAILED test" + of + desc + sep + at)
+
+    sys.exit(1)
+
+def no_result(self = None, condition = 1, function = None, skip = 0):
+    """Causes a test to exit with no valid result.
+
+    By default, the no_result() method reports NO RESULT for the test
+    and exits with a status of 2.  If a condition argument is supplied,
+    the test fails only if the condition is true.
+    """
+    if not condition:
+       return
+    if not function is None:
+       function()
+    of = ""
+    desc = ""
+    sep = " "
+    if not self is None:
+       if self.program:
+           of = " of " + self.program
+           sep = "\n\t"
+       if self.description:
+           desc = " [" + self.description + "]"
+           sep = "\n\t"
+
+    at = _caller(traceback.extract_stack(), skip)
+    sys.stderr.write("NO RESULT for test" + of + desc + sep + at)
+
+    sys.exit(2)
+
+def pass_test(self = None, condition = 1, function = None):
+    """Causes a test to pass.
+
+    By default, the pass_test() method reports PASSED for the test
+    and exits with a status of 0.  If a condition argument is supplied,
+    the test passes only if the condition is true.
+    """
+    if not condition:
+       return
+    if not function is None:
+       function()
+    sys.stderr.write("PASSED\n")
+    sys.exit(0)
+
+def match_exact(lines = None, matches = None):
+    """
+    """
+    if not type(lines) is ListType:
+       lines = split(lines, "\n")
+    if not type(matches) is ListType:
+       matches = split(matches, "\n")
+    if len(lines) != len(matches):
+       return
+    for i in range(len(lines)):
+       if lines[i] != matches[i]:
+           return
+    return 1
+
+def match_re(lines = None, res = None):
+    """
+    """
+    if not type(lines) is ListType:
+       lines = split(lines, "\n")
+    if not type(res) is ListType:
+       res = split(res, "\n")
+    if len(lines) != len(res):
+       return
+    for i in range(len(lines)):
+       if not re.compile("^" + res[i] + "$").search(lines[i]):
+           return
+    return 1
+
+class TestCmd:
+    """Class TestCmd
+    """
+
+    def __init__(self, description = None,
+                       program = None,
+                       interpreter = None,
+                       workdir = None,
+                       subdir = None,
+                       verbose = 0,
+                       match = None):
+       self._cwd = os.getcwd()
+       self.description_set(description)
+       self.program_set(program)
+       self.interpreter_set(interpreter)
+       self.verbose_set(verbose)
+       if not match is None:
+           self.match_func = match
+       else:
+           self.match_func = match_re
+       self._dirlist = []
+       self._preserve = {'pass_test': 0, 'fail_test': 0, 'no_result': 0}
+       if os.environ.has_key('PRESERVE') and not os.environ['PRESERVE'] is '':
+           self._preserve['pass_test'] = os.environ['PRESERVE']
+           self._preserve['fail_test'] = os.environ['PRESERVE']
+           self._preserve['no_result'] = os.environ['PRESERVE']
+       else:
+           try:
+               self._preserve['pass_test'] = os.environ['PRESERVE_PASS']
+           except KeyError:
+               pass
+           try:
+               self._preserve['fail_test'] = os.environ['PRESERVE_FAIL']
+           except KeyError:
+               pass
+           try:
+               self._preserve['no_result'] = os.environ['PRESERVE_NO_RESULT']
+           except KeyError:
+               pass
+       self._stdout = []
+       self._stderr = []
+       self.status = None
+       self.condition = 'no_result'
+       self.workdir_set(workdir)
+       self.subdir(subdir)
+
+    def __del__(self):
+       self.cleanup()
+
+    def __repr__(self):
+       return "%x" % id(self)
+
+    def cleanup(self, condition = None):
+       """Removes any temporary working directories for the specified
+       TestCmd environment.  If the environment variable PRESERVE was
+       set when the TestCmd environment was created, temporary working
+       directories are not removed.  If any of the environment variables
+       PRESERVE_PASS, PRESERVE_FAIL, or PRESERVE_NO_RESULT were set
+       when the TestCmd environment was created, then temporary working
+       directories are not removed if the test passed, failed, or had
+       no result, respectively.  Temporary working directories are also
+       preserved for conditions specified via the preserve method.
+
+       Typically, this method is not called directly, but is used when
+       the script exits to clean up temporary working directories as
+       appropriate for the exit status.
+       """
+       if not self._dirlist:
+           return
+       if condition is None:
+           condition = self.condition
+       #print "cleanup(" + condition + "):  ", self._preserve
+       if self._preserve[condition]:
+           return
+       os.chdir(self._cwd)
+       self.workdir = None
+       list = self._dirlist[:]
+       self._dirlist = []
+       list.reverse()
+       for dir in list:
+           self.writable(dir, 1)
+           shutil.rmtree(dir, ignore_errors = 1)
+       try:
+           global _Cleanup
+           _Cleanup.remove(self)
+       except (AttributeError, ValueError):
+           pass
+
+    def description_set(self, description):
+       """Set the description of the functionality being tested.
+       """
+       self.description = description
+
+#    def diff(self):
+#      """Diff two arrays.
+#      """
+
+    def fail_test(self, condition = 1, function = None, skip = 0):
+       """Cause the test to fail.
+       """
+       if not condition:
+           return
+       self.condition = 'fail_test'
+       fail_test(self = self,
+                 condition = condition,
+                 function = function,
+                 skip = skip)
+
+    def interpreter_set(self, interpreter):
+       """Set the program to be used to interpret the program
+       under test as a script.
+       """
+       self.interpreter = interpreter
+
+    def match(self, lines, matches):
+       """Compare actual and expected file contents.
+       """
+       return self.match_func(lines, matches)
+
+    def match_exact(self, lines, matches):
+       """Compare actual and expected file contents.
+       """
+       return match_exact(lines, matches)
+
+    def match_re(self, lines, res):
+       """Compare actual and expected file contents.
+       """
+       return match_re(lines, res)
+
+    def no_result(self, condition = 1, function = None, skip = 0):
+       """Report that the test could not be run.
+       """
+       if not condition:
+           return
+       self.condition = 'no_result'
+       no_result(self = self,
+                 condition = condition,
+                 function = function,
+                 skip = skip)
+
+    def pass_test(self, condition = 1, function = None):
+       """Cause the test to pass.
+       """
+       if not condition:
+           return
+       self.condition = 'pass_test'
+       pass_test(self = self, condition = condition, function = function)
+
+    def preserve(self, *conditions):
+       """Arrange for the temporary working directories for the
+       specified TestCmd environment to be preserved for one or more
+       conditions.  If no conditions are specified, arranges for
+       the temporary working directories to be preserved for all
+       conditions.
+       """
+       if conditions is ():
+           conditions = ('pass_test', 'fail_test', 'no_result')
+       for cond in conditions:
+           self._preserve[cond] = 1
+
+    def program_set(self, program):
+       """Set the executable program or script to be tested.
+       """
+       if program and not os.path.isabs(program):
+           program = os.path.join(self._cwd, program)
+       self.program = program
+
+    def read(self, file):
+       """Reads and returns the contents of the specified file name.
+       The file name may be a list, in which case the elements are
+       concatenated with the os.path.join() method.  The file is
+       assumed to be under the temporary working directory unless it
+       is an absolute path name.
+       """
+       if type(file) is ListType:
+           file = apply(os.path.join, tuple(file))
+       if not os.path.isabs(file):
+           file = os.path.join(self.workdir, file)
+       f = os.fdopen(os.open(file, FCNTL.O_RDONLY))
+       contents = f.read()
+       f.close()
+       return contents
+
+    def run(self, program = None,
+                 interpreter = None,
+                 arguments = None,
+                 chdir = None,
+                 stdin = None):
+       """Runs a test of the program or script for the test
+       environment.  Standard output and error output are saved for
+       future retrieval via the stdout() and stderr() methods.
+       """
+       if chdir:
+           oldcwd = os.getcwd()
+           if not os.path.isabs(chdir):
+               chdir = os.path.join(self.workpath(chdir))
+           if self.verbose:
+               sys.stderr.write("chdir(" + chdir + ")\n")
+           os.chdir(chdir)
+       cmd = None
+       if program:
+           if not os.path.isabs(program):
+               program = os.path.join(self._cwd, program)
+           cmd = program
+           if interpreter:
+               cmd = interpreter + " " + cmd
+       else:
+           cmd = self.program
+           if self.interpreter:
+               cmd =  self.interpreter + " " + cmd
+       if arguments:
+           cmd = cmd + " " + arguments
+       if self.verbose:
+           sys.stderr.write(cmd + "\n")
+       p = popen2.Popen3(cmd, 1)
+       if stdin:
+           if type(stdin) is ListType:
+               for line in stdin:
+                   p.tochild.write(line)
+           else:
+               p.tochild.write(stdin)
+       p.tochild.close()
+       self._stdout.append(p.fromchild.read())
+       self._stderr.append(p.childerr.read())
+       self.status = p.wait()
+       if chdir:
+           os.chdir(oldcwd)
+
+    def stderr(self, run = None):
+       """Returns the error output from the specified run number.
+       If there is no specified run number, then returns the error
+       output of the last run.  If the run number is less than zero,
+       then returns the error output from that many runs back from the
+       current run.
+       """
+       if not run:
+           run = len(self._stderr)
+       elif run < 0:
+           run = len(self._stderr) + run
+       run = run - 1
+       return self._stderr[run]
+
+    def stdout(self, run = None):
+       """Returns the standard output from the specified run number.
+       If there is no specified run number, then returns the standard
+       output of the last run.  If the run number is less than zero,
+       then returns the standard output from that many runs back from
+       the current run.
+       """
+       if not run:
+           run = len(self._stdout)
+       elif run < 0:
+           run = len(self._stdout) + run
+       run = run - 1
+       return self._stdout[run]
+
+    def subdir(self, *subdirs):
+       """Create new subdirectories under the temporary working
+       directory, one for each argument.  An argument may be a list,
+       in which case the list elements are concatenated using the
+       os.path.join() method.  Subdirectories multiple levels deep
+       must be created using a separate argument for each level:
+
+               test.subdir('sub', ['sub', 'dir'], ['sub', 'dir', 'ectory'])
+
+       Returns the number of subdirectories actually created.
+       """
+       count = 0
+       for sub in subdirs:
+           if sub is None:
+               continue
+           if type(sub) is ListType:
+               sub = apply(os.path.join, tuple(sub))
+           new = os.path.join(self.workdir, sub)
+           try:
+               os.mkdir(new)
+           except:
+               pass
+           else:
+               count = count + 1
+       return count
+
+    def verbose_set(self, verbose):
+       """Set the verbose level.
+       """
+       self.verbose = verbose
+
+    def workdir_set(self, path):
+       """Creates a temporary working directory with the specified
+       path name.  If the path is a null string (''), a unique
+       directory name is created.
+       """
+       if (path != None):
+           if path == '':
+               path = tempfile.mktemp()
+           if path != None:
+               os.mkdir(path)
+           self._dirlist.append(path)
+           global _Cleanup
+           try:
+               _Cleanup.index(self)
+           except ValueError:
+               _Cleanup.append(self)
+           # We'd like to set self.workdir like this:
+           #   self.workdir = path
+           # But symlinks in the path will report things
+           # differently from os.getcwd(), so chdir there
+           # and back to fetch the canonical path.
+           cwd = os.getcwd()
+           os.chdir(path)
+           self.workdir = os.getcwd()
+           os.chdir(cwd)
+       else:
+           self.workdir = None
+
+    def workpath(self, *args):
+       """Returns the absolute path name to a subdirectory or file
+       within the current temporary working directory.  Concatenates
+       the temporary working directory name with the specified
+       arguments using the os.path.join() method.
+       """
+       return apply(os.path.join, (self.workdir,) + tuple(args))
+
+    def writable(self, top, write):
+       """Make the specified directory tree writable (write == 1)
+       or not (write == None).
+       """
+
+       def _walk_chmod(arg, dirname, names):
+           st = os.stat(dirname)
+           os.chmod(dirname, arg(st[stat.ST_MODE]))
+           for name in names:
+               n = os.path.join(dirname, name)
+               st = os.stat(n)
+               os.chmod(n, arg(st[stat.ST_MODE]))
+
+       def _mode_writable(mode):
+           return stat.S_IMODE(mode|0200)
+
+       def _mode_non_writable(mode):
+           return stat.S_IMODE(mode&~0200)
+
+       if write:
+           f = _mode_writable
+       else:
+           f = _mode_non_writable
+       os.path.walk(top, _walk_chmod, f)
+
+    def write(self, file, content):
+       """Writes the specified content text (second argument) to the
+       specified file name (first argument).  The file name may be
+       a list, in which case the elements are concatenated with the
+       os.path.join() method.  The file is created under the temporary
+       working directory.  Any subdirectories in the path must already
+       exist.  """
+       if type(file) is ListType:
+           file = apply(os.path.join, tuple(file))
+       if not os.path.isabs(file):
+           file = os.path.join(self.workdir, file)
+       fd = os.open(file, FCNTL.O_CREAT|FCNTL.O_WRONLY)
+       os.write(fd, content)
+       os.close(fd)
diff --git a/config b/config
new file mode 100644 (file)
index 0000000..c36eaf0
--- /dev/null
+++ b/config
@@ -0,0 +1,264 @@
+/*
+ *     aegis - project change supervisor
+ *     This file is in the Public Domain, 1995, Peter Miller.
+ *
+ * MANIFEST: example use of make in project config file
+ *
+ * The make(1) program exists in many forms, usually one is available with each
+ * UNIX version.  The one used in the writing of this section is GNU Make 3.70,
+ * avaiable by anonymous FTP from your nearest GNU archive site.  GNU Make was
+ * chosen because it was the most powerful, it is widely avaiable (usually for
+ * little or no cost) and discussion of the alternatives (SunOS make, BSD 4.3
+ * make, etc), would not be universally applicable.  "Plain vanilla" make
+ * (with no transitive closure, no pattern rules, no functions) is not
+ * sufficiently capable to satisfy the demands placed on it by aegis.
+ *
+ * As mentioned in the Dependency Maintenance Tool chapter of the User Guide,
+ * make is not really sufficient, because it lacks dynamic include dependencies.
+ * However, GNU Make has a form of dynamic include dependencies, and it has a
+ * few quirks, but mostly works well.
+ *
+ * The other feature lacking in make is a search path.  While GNU Make has
+ * functionality called VPATH, the implementation leaves something to be
+ * desired, and can't be used for the search path functionality required by
+ * aegis.  Because of this, the create_symlinks_before_build field of the
+ * project config file is set to true so that aegis will arrange for the
+ * development directory to be fiull of symbolic links, making it appear that
+ * the entire project is in each change's development directory.
+ */
+
+/*
+ * The build_command field of the project config file is used to invoke the
+ * relevant build command.  This command tells make where to find the rules.
+ * The ${s Makefile} expands to a path into the baseline during development
+ * if the file is not in the change.  Look in aesub(5) for more information
+ * about command substitutions.
+ */
+build_command = "cons date='${DAte %Y/%m/%d %H:%M:%S}' developer=${DEVeloper} version=${VERsion}";
+
+/*
+ * The rules used in the User Guide all remove their targets before
+ * constructing them, which qualifies them for the following entry in the
+ * config file.  The files must be removed first, otherwise the baseline would
+ * cease to be self-consistent.
+ */
+link_integration_directory = true;
+
+/*
+ * Another field to be set in this file is one which tells aegis to maintain
+ * symbolic links between the development directory and the basline.  This also
+ * requires that rules remove their targets before constructing them, to ensure
+ * that development builds do not attempt to write their results onto the
+ * read-only versions in the baseline.
+ */
+create_symlinks_before_build = true;
+
+/*
+ * NOT UNTIL AEGIS 3.23; we may not need it anyway.
+remove_symlinks_after_build = false;
+ */
+
+/*
+integrate_begin_command =
+       "";
+*/
+
+/*
+ *     aegis - project change supervisor
+ *     This file is in the Public Domain, 1995, 1998 Peter Miller.
+ *
+ * MANIFEST: example of using rcs in the project config file
+ *
+ * The entries for the commands are listed below.  RCS uses a slightly
+ * different model than aegis wants, so some maneuvering is required.
+ * The command strings in this section assume that the RCS commands ci and co
+ * and rcs and rlog are in the command search PATH, but you may like to
+ * hard-wire the paths, or set PATH at the start of each.  You should also note
+ * that the strings are always handed to the Bourne shell to be executed, and
+ * are set to exit with an error immediately a sub-command fails.
+ *
+ * In these commands, the RCS file is kept unlocked, since only the owner will
+ * be checking changes in.  The RCS functionality for coordinating shared
+ * access is not required.
+ *
+ * One advantage of using RCS version 5.6 or later is that binary files are
+ * supported, should you want to have binary files in the baseline.
+ *
+ * The ${quote ...} construct is used to quote filenames which contain
+ * shell special characters.  A minimum of quoting is performed, so if
+ * the filenames do not contail shell special characters, no quotes will
+ * be used.
+ */
+
+/*
+ * This command is used to create a new file history.
+ * This command is always executed as the project owner.
+ * The following substitutions are available:
+ *
+ * ${Input}
+ *     absolute path of the source file
+ * ${History}
+ *     absolute path of the history file
+ *
+ * The "ci -f" option is used to specify that a copy is to be checked-in even
+ *     if there are no changes.
+ * The "ci -u" option is used to specify that an unlocked copy will remain in
+ *     the baseline.
+ * The "ci -d" option is used to specify that the file time rather than the
+ *     current time is to be used for the new revision.
+ * The "ci -M" option is used to specify that the mode date on the original
+ *     file is not to be altered.
+ * The "ci -t" option is used to specify that there is to be no description
+ *     text for the new RCS file.
+ * The "ci -m" option is used to specify that the change number is to be stored
+ *     in the file log if this is actually an update (typically from aenf
+ *     after aerm on the same file name).
+ * The "rcs -U" option is used to specify that the new RCS file is to have
+ *     unstrict locking.
+ * The "rcs -kk" option is used to specify that keyword substitution is
+ *     disabled (only keyword names, not values, are substituted).
+ */
+history_create_command =
+       "ci -f -u -d -M -m$c -t/dev/null ${quote $input} ${quote $history,v}; \
+rcs -kk -U ${quote $history,v}";
+
+
+/*
+ * This command is used to get a specific edit back from history.
+ * This command is always executed as the project owner.
+ * The following substitutions are available:
+ *
+ * ${History}
+ *     absolute path of the history file
+ * ${Edit}
+ *     edit number, as given by history_\%query_\%command
+ * ${Output}
+ *     absolute path of the destination file
+ *
+ * The "co -r" option is used to specify the edit to be retrieved.
+ * The "co -p" option is used to specify that the results be printed on the
+ *     standard output; this is because the destination filename will never
+ *     look anything like the history source filename.
+ * The "rcs -kk" option is used to specify that keyword substitution is
+ *     disabled (only keyword names, not values, are substituted).
+ */
+history_get_command =
+       "co -kk -r${quote $edit} -p ${quote $history,v} > ${quote $output}";
+
+/*
+ * This command is used to add a new "top-most" entry to the history file.
+ * This command is always executed as the project owner.
+ * The following substitutions are available:
+ *
+ * ${Input}
+ *     absolute path of source file
+ * ${History}
+ *     absolute path of history file
+ *
+ * The "ci -f" option is used to specify that a copy is to be checked-in even
+ *     if there are no changes.
+ * The "ci -u" option is used to specify that an unlocked copy will remain in
+ *     the baseline.
+ * The "ci -d" option is used to specify that the file time rather than the
+ *     current time is to be used for the new revision.
+ * The "ci -M" option is used to specify that the mode date on the original
+ *     file is not to be altered.
+ * The "ci -m" option is used to specify that the change number is to be stored
+ *     in the file log, which allows rlog to be used to find the change
+ *     numbers to which each revision of the file corresponds.
+ *
+ * It is possible for a a very cautious approach has been taken, in which case
+ * the history_put_command may be set to the same string specified above for
+ * the history_create_command.
+ */
+history_put_command =
+       "ci -f -u -d -M -m$c ${quote $input} ${quote $history,v}";
+
+/*
+ * This command is used to query what the history mechanism calls the top-most
+ * edit of a history file.  The result may be any arbitrary string, it need not
+ * be anything like a number, just so long as it uniquely identifies the edit
+ * for use by the history_get_command at a later date.  The edit number is to
+ * be printed on the standard output.  This command is always executed as the
+ * project owner.
+ *
+ * The following substitutions are available:
+ *
+ * ${History}
+ *     absolute path of the history file
+ */
+history_query_command =
+       "rlog -r ${quote $history,v} | awk '/^head:/ {print $$2}'";
+
+/*
+ * RCS also provides a merge program, which can be used to provide a three-way
+ * merge.  It has an ouput format some sites prefer to the fmerge output.
+ *
+ * This command is used by aed(1) to produce a difference listing when a file
+ * in the development directory is out of date compared to the current version
+ * in the baseline.
+ *
+ * All of the command substitutions described in aesub(5) are available.
+ * In addition, the following substitutions are also available:
+ *
+ * ${ORiginal}
+ *     The absolute path name of a file containing the common ancestor
+ *     version of ${MostRecent} and {$Input}.  Usually the version originally
+ *     copied into the change.  Usually in a temporary file.
+ * ${Most_Recent}
+ *     The absolute path name of a file containing the most recent version.
+ *     Usually in the baseline.
+ * ${Input}
+ *     The absolute path name of the edited version of the file.  Usually in
+ *     the development directory.
+ * ${Output}
+ *     The absolute path name of the file in which to write the difference
+ *     listing.  Usually in the development directory.
+ *
+ * An exit status of 0 means successful, even of the files differ (and they
+ * usually do).  An exit status which is non-zero means something is wrong.
+ *
+ * The "merge -L" options are used to specify labels for the baseline and the
+ *     development directory, respecticvely, when conflict lines are inserted
+ *     into the result.
+ * The "merge -p" options is used to specify that the results are to be printed
+ *     on the standard output.
+ */
+
+diff3_command =
+       "set +e; \
+merge -p -L baseline -L C$c ${quote $mostrecent} ${quote $original} \
+${quote $input} > ${quote $output}; \
+test $? -le 1";
+
+diff_command =
+       "set +e; \
+       diff -c ${quote $original} ${quote $input} > ${quote $output}; \
+       test $? -le 1";
+
+/*
+ * We use an intermediary test.pl script to execute tests.
+ * This serves as glue between the tests themselves (which are
+ * written to conform to Perl conventions) and Aegis' expectations.
+ * See the comments in the test.pl script itself for details.
+ */
+test_command = "python runtest.py -v ${VERsion} ${File_Name}";
+
+/*
+ *
+ */
+file_template =
+[
+       {
+               pattern = [ "src/scons/*__init__.py" ];
+               body = "${read_file ${source template/__init__.py abs}}";
+       },
+       {
+               pattern = [ "src/scons/*Tests.py" ];
+               body = "${read_file ${source template/test.py abs}}";
+       },
+       {
+               pattern = [ "src/scons/*.py" ];
+               body = "${read_file ${source template/file.py abs}}";
+       },
+];
diff --git a/runtest.py b/runtest.py
new file mode 100644 (file)
index 0000000..5c7b8de
--- /dev/null
@@ -0,0 +1,51 @@
+#!/usr/bin/env python
+
+import getopt, os, os.path, re, string, sys
+
+opts, tests = getopt.getopt(sys.argv[1:], "dv:")
+
+debug = ''
+version = None
+
+for o, a in opts:
+    if o == '-d': debug = "/usr/lib/python1.5/pdb.py"
+    if o == '-v': version = a
+
+if not version:
+    version = os.popen("aesub '$version'").read()[:-1]
+
+match = re.compile(r'^[CD]0*')
+
+def aegis_to_version(aever):
+    arr = string.split(aever, '.')
+    end = max(len(arr) - 1, 2)
+    arr = map(lambda e: match.sub('', e), arr[:end])
+    def rep(e):
+       if len(e) == 1:
+           e = '0' + e
+       return e
+    arr[1:] = map(rep, arr[1:])
+    return string.join(arr, '.')
+
+version = aegis_to_version(version)
+
+cwd = os.getcwd()
+
+map(os.path.abspath, tests)
+
+build_test = os.path.join(cwd, "build", "test")
+scons_ver = os.path.join(build_test, "scons-" + version)
+
+os.chdir(scons_ver)
+
+os.environ['PYTHONPATH']  = scons_ver + ':' + build_test
+
+exit = 0
+
+for path in tests:
+    if not os.path.isabs(path):
+       path = os.path.join(cwd, path)
+    if os.system("python " + debug + " " + path):
+       exit = 1
+
+sys.exit(exit)
diff --git a/src/.aeignore b/src/.aeignore
new file mode 100644 (file)
index 0000000..43fe851
--- /dev/null
@@ -0,0 +1,4 @@
+*,D
+*.pyc
+.*.swp
+.consign
diff --git a/src/MANIFEST b/src/MANIFEST
new file mode 100644 (file)
index 0000000..508f198
--- /dev/null
@@ -0,0 +1,12 @@
+MANIFEST
+scons/__init__.py
+scons/Builder.py
+scons/Defaults.py
+scons/Environment.py
+scons/Node/__init__.py
+scons/Node/FS.py
+scons/Sig/__init__.py
+scons/Sig/MD5.py
+scons/Sig/TimeStamp.py
+scons.py
+setup.py
diff --git a/src/scons.py b/src/scons.py
new file mode 100644 (file)
index 0000000..c700f77
--- /dev/null
@@ -0,0 +1,54 @@
+#!/usr/bin/env python
+
+import getopt
+import os.path
+import string
+import sys
+
+opts, targets = getopt.getopt(sys.argv[1:], 'f:')
+
+Scripts = []
+
+for o, a in opts:
+    if o == '-f': Scripts.append(a)
+
+if not Scripts:
+    Scripts.append('SConstruct')
+
+
+# XXX The commented-out code here adds any "scons" subdirs in anything
+# along sys.path to sys.path.  This was an attempt at setting up things
+# so we can import "node.FS" instead of "scons.Node.FS".  This doesn't
+# quite fit our testing methodology, though, so save it for now until
+# the right solutions pops up.
+#
+#dirlist = []
+#for dir in sys.path:
+#    scons = os.path.join(dir, 'scons')
+#    if os.path.isdir(scons):
+#      dirlist = dirlist + [scons]
+#    dirlist = dirlist + [dir]
+#
+#sys.path = dirlist
+
+from scons.Node.FS import init, Dir, File, lookup
+from scons.Environment import Environment
+
+init()
+
+
+
+def Conscript(filename):
+    Scripts.append(filename)
+
+
+
+while Scripts:
+    file, Scripts = Scripts[0], Scripts[1:]
+    execfile(file)
+
+
+
+for path in targets:
+       target = lookup(File, path)
+       target.build()
diff --git a/src/scons/.aeignore b/src/scons/.aeignore
new file mode 100644 (file)
index 0000000..43fe851
--- /dev/null
@@ -0,0 +1,4 @@
+*,D
+*.pyc
+.*.swp
+.consign
diff --git a/src/scons/Builder.py b/src/scons/Builder.py
new file mode 100644 (file)
index 0000000..76c5512
--- /dev/null
@@ -0,0 +1,58 @@
+"""scons.Builder
+
+XXX
+
+"""
+
+__revision__ = "Builder.py __REVISION__ __DATE__ __DEVELOPER__"
+
+
+
+import os
+from types import *
+from scons.Node.FS import Dir, File, lookup
+
+
+
+class Builder:
+    """Base class for Builders, objects that create output
+    nodes (files) from input nodes (files).
+    """
+
+    def __init__(self, name = None,
+                       action = None,
+                       input_suffix = None,
+                       output_suffix = None,
+                       node_class = File):
+       self.name = name
+       self.action = action
+       self.insuffix = input_suffix
+       self.outsuffix = output_suffix
+       self.node_class = node_class
+       if not self.insuffix is None and self.insuffix[0] != '.':
+           self.insuffix = '.' + self.insuffix
+       if not self.outsuffix is None and self.outsuffix[0] != '.':
+           self.outsuffix = '.' + self.outsuffix
+
+    def __cmp__(self, other):
+       return cmp(self.__dict__, other.__dict__)
+
+    def __call__(self, target = None, source = None):
+       node = lookup(self.node_class, target)
+       node.builder_set(self)
+       node.sources = source   # XXX REACHING INTO ANOTHER OBJECT
+       return node
+
+    def execute(self, **kw):
+       """Execute a builder's action to create an output object.
+       """
+       # XXX THIS SHOULD BE DONE BY TURNING Builder INTO A FACTORY
+       # FOR SUBCLASSES FOR StringType AND FunctionType
+       t = type(self.action)
+       if t == StringType:
+           cmd = self.action % kw
+           print cmd
+           os.system(cmd)
+       elif t == FunctionType:
+           # XXX WHAT SHOULD WE PRINT HERE
+           self.action(kw)
diff --git a/src/scons/BuilderTests.py b/src/scons/BuilderTests.py
new file mode 100644 (file)
index 0000000..a749bf2
--- /dev/null
@@ -0,0 +1,111 @@
+__revision__ = "BuilderTests.py __REVISION__ __DATE__ __DEVELOPER__"
+
+import sys
+import unittest
+
+from scons.Builder import Builder
+from TestCmd import TestCmd
+
+
+# Initial setup of the common environment for all tests,
+# a temporary working directory containing a
+# script for writing arguments to an output file.
+#
+# We don't do this as a setUp() method because it's
+# unnecessary to create a separate directory and script
+# for each test, they can just use the one.
+test = 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")
+f.close()
+sys.exit(0)
+""")
+
+act_py = test.workpath('act.py')
+outfile = test.workpath('outfile')
+
+
+class BuilderTestCase(unittest.TestCase):
+
+    def test_action(self):
+       """Test the simple ability to create a Builder
+       and retrieve the supplied action attribute.
+       """
+       builder = Builder(action = "foo")
+       assert builder.action == "foo"
+
+    def test_cmp(self):
+       """Test simple comparisons of Builder objects.
+       """
+       b1 = Builder(input_suffix = '.o')
+       b2 = Builder(input_suffix = '.o')
+       assert b1 == b2
+       b3 = Builder(input_suffix = '.x')
+       assert b1 != b3
+       assert b2 != b3
+
+    def test_execute(self):
+       """Test the ability to execute simple Builders, one
+       a string that executes an external command, and one an
+       internal function.
+       """
+       cmd = "python %s %s xyzzy" % (act_py, outfile)
+       builder = Builder(action = cmd)
+       builder.execute()
+       assert test.read(outfile) == "act.py: xyzzy\n"
+
+       def function(kw):
+           import os, string, sys
+           f = open(kw['out'], 'w')
+           f.write("function\n")
+           f.close()
+           return not None
+
+       builder = Builder(action = function)
+       builder.execute(out = outfile)
+       assert test.read(outfile) == "function\n"
+
+    def test_insuffix(self):
+       """Test the ability to create a Builder with a specified
+       input suffix, making sure that the '.' separator is
+       appended to the beginning if it isn't already present.
+       """
+       builder = Builder(input_suffix = '.c')
+       assert builder.insuffix == '.c'
+       builder = Builder(input_suffix = 'c')
+       assert builder.insuffix == '.c'
+
+    def test_name(self):
+       """Test the ability to create a Builder with a specified
+       name.
+       """
+       builder = Builder(name = 'foo')
+       assert builder.name == 'foo'
+
+    def test_node_class(self):
+       """Test the ability to create a Builder that creates nodes
+       of the specified class.
+       """
+       class Foo:
+               pass
+       builder = Builder(node_class = Foo)
+       assert builder.node_class is Foo
+
+    def test_outsuffix(self):
+       """Test the ability to create a Builder with a specified
+       output suffix, making sure that the '.' separator is
+       appended to the beginning if it isn't already present.
+       """
+       builder = Builder(input_suffix = '.o')
+       assert builder.insuffix == '.o'
+       builder = Builder(input_suffix = 'o')
+       assert builder.insuffix == '.o'
+
+
+
+if __name__ == "__main__":
+    suite = unittest.makeSuite(BuilderTestCase, 'test_')
+    if not unittest.TextTestRunner().run(suite).wasSuccessful():
+       sys.exit(1)
diff --git a/src/scons/Defaults.py b/src/scons/Defaults.py
new file mode 100644 (file)
index 0000000..0aa2b82
--- /dev/null
@@ -0,0 +1,20 @@
+"""scons.Defaults
+
+Builders and other things for the local site.  Here's where we'll
+duplicate the functionality of autoconf until we move it into the
+installation procedure or use something like qmconf.
+
+"""
+
+__revision__ = "local.py __REVISION__ __DATE__ __DEVELOPER__"
+
+
+
+from scons.Builder import Builder
+
+
+
+Object = Builder(name = 'Object', action = 'cc -c -o %(target)s %(source)s')
+Program = Builder(name = 'Program', action = 'cc -o %(target)s %(source)s')
+
+Builders = [Object, Program]
diff --git a/src/scons/Environment.py b/src/scons/Environment.py
new file mode 100644 (file)
index 0000000..c410162
--- /dev/null
@@ -0,0 +1,114 @@
+"""scons.Environment
+
+XXX
+
+"""
+
+__revision__ = "Environment.py __REVISION__ __DATE__ __DEVELOPER__"
+
+
+
+import copy
+import re
+import types
+
+
+
+def Command():
+    pass       # XXX
+
+def Install():
+    pass       # XXX
+
+def InstallAs():
+    pass       # XXX
+
+
+
+_cv = re.compile(r'%([_a-zA-Z]\w*|{[_a-zA-Z]\w*})')
+_self = None
+
+
+
+def _deepcopy_atomic(x, memo):
+       return x
+copy._deepcopy_dispatch[types.ModuleType] = _deepcopy_atomic
+copy._deepcopy_dispatch[types.ClassType] = _deepcopy_atomic
+copy._deepcopy_dispatch[types.FunctionType] = _deepcopy_atomic
+copy._deepcopy_dispatch[types.MethodType] = _deepcopy_atomic
+copy._deepcopy_dispatch[types.TracebackType] = _deepcopy_atomic
+copy._deepcopy_dispatch[types.FrameType] = _deepcopy_atomic
+copy._deepcopy_dispatch[types.FileType] = _deepcopy_atomic
+
+
+
+class Environment:
+    """Base class for construction Environments.  These are
+    the primary objects used to communicate dependency and
+    construction information to the build engine.
+
+    Keyword arguments supplied when the construction Environment
+    is created are construction variables used to initialize the
+    Environment.
+    """
+
+    def __init__(self, **kw):
+       self.Dictionary = {}
+       if kw.has_key('BUILDERS'):
+           builders = kw['BUILDERS']
+           if not type(builders) is types.ListType:
+               kw['BUILDERS'] = [builders]
+       else:
+           import scons.Defaults
+           kw['BUILDERS'] = scons.Defaults.Builders[:]
+       self.Dictionary.update(copy.deepcopy(kw))
+       for b in kw['BUILDERS']:
+           setattr(self, b.name, b)
+
+    def __cmp__(self, other):
+       return cmp(self.Dictionary, other.Dictionary)
+
+    def Builders(self):
+       pass    # XXX
+
+    def Copy(self, **kw):
+       """Return a copy of a construction Environment.  The
+       copy is like a Python "deep copy"--that is, independent
+       copies are made recursively of each objects--except that
+       a reference is copied when an object is not deep-copyable
+       (like a function).  There are no references to any mutable
+       objects in the original Environment.
+       """
+       return copy.deepcopy(self)
+
+    def Scanners(self):
+       pass    # XXX
+
+    def        Update(self, **kw):
+       """Update an existing construction Environment with new
+       construction variables and/or values.
+       """
+       self.Dictionary.update(copy.deepcopy(kw))
+
+    def subst(self, string):
+       """Recursively interpolates construction variables from the
+       Environment into the specified string, returning the expanded
+       result.  Construction variables are specified by a % prefix
+       in the string and begin with an initial underscore or
+       alphabetic character followed by any number of underscores
+       or alphanumeric characters.  The construction variable names
+       may be surrounded by curly braces to separate the name from
+       trailing characters.
+       """
+       global _self
+       _self = self    # XXX NOT THREAD SAFE, BUT HOW ELSE DO WE DO THIS?
+       def repl(m):
+           key = m.group(1)
+           if key[:1] == '{' and key[-1:] == '}':
+               key = key[1:-1]
+           if _self.Dictionary.has_key(key): return _self.Dictionary[key]
+           else: return ''
+       n = 1
+       while n != 0:
+           string, n = _cv.subn(repl, string)
+       return string
diff --git a/src/scons/EnvironmentTests.py b/src/scons/EnvironmentTests.py
new file mode 100644 (file)
index 0000000..5c6c151
--- /dev/null
@@ -0,0 +1,129 @@
+__revision__ = "EnivronmentTests.py __REVISION__ __DATE__ __DEVELOPER__"
+
+import sys
+import unittest
+
+from scons.Environment import *
+
+
+
+built_it = {}
+
+class Builder:
+    """A dummy Builder class for testing purposes.  "Building"
+    a target is simply setting a value in the dictionary.
+    """
+    def __init__(self, name = None):
+       self.name = name
+
+    def execute(self, target = None, source = None):
+       built_it[target] = 1
+
+
+
+class EnvironmentTestCase(unittest.TestCase):
+
+    def test_Builders(self):
+       """Test the ability to execute simple builders through
+       different environment, one initialized with a single
+       Builder object, one with a list of a single Builder
+       object, and one with a list of two Builder objects.
+       """
+       global built_it
+
+       b1 = Builder(name = 'builder1')
+       b2 = Builder(name = 'builder2')
+
+       built_it = {}
+       env1 = Environment(BUILDERS = b1)
+       env1.builder1.execute(target = 'out1')
+       assert built_it['out1']
+
+       built_it = {}
+       env2 = Environment(BUILDERS = [b1])
+       env1.builder1.execute(target = 'out1')
+       assert built_it['out1']
+
+       built_it = {}
+       env3 = Environment(BUILDERS = [b1, b2])
+       env3.builder1.execute(target = 'out1')
+       env3.builder2.execute(target = 'out2')
+       env3.builder1.execute(target = 'out3')
+       assert built_it['out1']
+       assert built_it['out2']
+       assert built_it['out3']
+
+    def test_Command(self):
+       pass    # XXX
+
+    def test_Copy(self):
+       """Test the ability to copy a construction Environment.
+       Update the copy independently afterwards and check that
+       the original remains intact (that is, no dangling
+       references point to objects in the copied environment).
+       """
+       env1 = Environment(XXX = 'x', YYY = 'y')
+       env2 = env1.Copy()
+       env1copy = env1.Copy()
+       env2.Update(YYY = 'yyy')
+       assert env1 != env2
+       assert env1 == env1copy
+
+    def test_Dictionary(self):
+       """Test the simple ability to retrieve known construction
+       variables from the Dictionary and check for well-known
+       defaults that get inserted.
+       """
+       env = Environment(XXX = 'x', YYY = 'y')
+       assert env.Dictionary['XXX'] == 'x'
+       assert env.Dictionary['YYY'] == 'y'
+       assert env.Dictionary.has_key('BUILDERS')
+
+    def test_Environment(self):
+       """Test the simple ability to create construction
+       Environments.  Create two with identical arguments
+       and check that they compare the same.
+       """
+       env1 = Environment(XXX = 'x', YYY = 'y')
+       env2 = Environment(XXX = 'x', YYY = 'y')
+       assert env1 == env2
+
+    def test_Install(self):
+       pass    # XXX
+
+    def test_InstallAs(self):
+       pass    # XXX
+
+    def test_Scanners(self):
+       pass    # XXX
+
+    def test_Update(self):
+       """Test the ability to update a construction Environment
+       with new construction variables after it was first created.
+       """
+       env1 = Environment(AAA = 'a', BBB = 'b')
+       env1.Update(BBB = 'bbb', CCC = 'ccc')
+       env2 = Environment(AAA = 'a', BBB = 'bbb', CCC = 'c')
+       assert env1 != env2
+
+    def test_subst(self):
+       """Test the ability to substitute construction variables
+       into a string.  Check various combinations, including
+       recursive expansion of variables into other variables.
+       """
+       env = Environment(AAA = 'a', BBB = 'b')
+       str = env.subst("%AAA %{AAA}A %BBBB %BBB")
+       assert str == "a aA  b", str
+       env = Environment(AAA = '%BBB', BBB = 'b', BBBA = 'foo')
+       str = env.subst("%AAA %{AAA}A %{AAA}B %BBB")
+       assert str == "b foo  b", str
+       env = Environment(AAA = '%BBB', BBB = '%CCC', CCC = 'c')
+       str = env.subst("%AAA %{AAA}A %{AAA}B %BBB")
+       assert str == "c   c", str
+
+
+
+if __name__ == "__main__":
+    suite = unittest.makeSuite(EnvironmentTestCase, 'test_')
+    if not unittest.TextTestRunner().run(suite).wasSuccessful():
+       sys.exit(1)
diff --git a/src/scons/Node/.aeignore b/src/scons/Node/.aeignore
new file mode 100644 (file)
index 0000000..43fe851
--- /dev/null
@@ -0,0 +1,4 @@
+*,D
+*.pyc
+.*.swp
+.consign
diff --git a/src/scons/Node/FS.py b/src/scons/Node/FS.py
new file mode 100644 (file)
index 0000000..7640a7a
--- /dev/null
@@ -0,0 +1,139 @@
+"""scons.Node.FS
+
+File system nodes.
+
+"""
+
+__revision__ = "Node/FS.py __REVISION__ __DATE__ __DEVELOPER__"
+
+
+
+import os
+import os.path
+from scons.Node import Node
+
+
+
+Top = None
+Root = {}
+
+
+
+def init(path = None):
+    """Initialize the Node.FS subsystem.
+
+    The supplied path is the top of the source tree, where we
+    expect to find the top-level build file.  If no path is
+    supplied, the current directory is the default.
+    """
+    global Top
+    if path == None:
+       path = os.getcwd()
+    Top = lookup(Dir, path, directory = None)
+    Top.path = '.'
+
+def lookup(fsclass, name, directory = Top):
+    """Look up a file system node for a path name.  If the path
+    name is relative, it will be looked up relative to the
+    specified directory node, or to the top-level directory
+    if no node was specified.  An initial '#' specifies that
+    the name will be looked up relative to the top-level directory,
+    regardless of the specified directory argument.  Returns the
+    existing or newly-created node for the specified path name.
+    The node returned will be of the specified fsclass (Dir or
+    File).
+    """
+    global Top
+    head, tail = os.path.split(name)
+    if not tail:
+       drive, path = os.path.splitdrive(head)
+       if not Root.has_key(drive):
+           Root[drive] = Dir(head, None)
+           Root[drive].abspath = head
+           Root[drive].path = head
+       return Root[drive]
+    if tail[0] == '#':
+       directory = Top
+       tail = tail[1:]
+    elif directory is None:
+       directory = Top
+    if head:
+       directory = lookup(Dir, head, directory)
+    try:
+       self = directory.entries[tail]
+    except AttributeError:
+       # There was no "entries" attribute on the directory,
+       # which essentially implies that it was a file.
+       # Return it as a more descriptive exception.
+       raise TypeError, directory
+    except KeyError:
+       # There was to entry for "tail," so create the new
+       # node and link it in to the existing structure.
+       self = fsclass(tail, directory)
+       self.name = tail
+       if self.path[0:2] == "./":
+           self.path = self.path[2:]
+       directory.entries[tail] = self
+    except:
+       raise
+    if self.__class__.__name__ != fsclass.__name__:
+       # Here, we found an existing node for this path,
+       # but it was the wrong type (a File when we were
+       # looking for a Dir, or vice versa).
+       raise TypeError, self
+    return self
+
+
+
+# XXX TODO?
+# Annotate with the creator
+# is_under
+# rel_path
+# srcpath / srcdir
+# link / is_linked
+# linked_targets
+# is_accessible
+
+class Dir(Node):
+    """A class for directories in a file system.
+    """
+
+    def __init__(self, name, directory):
+       self.entries = {}
+       self.entries['.'] = self
+       self.entries['..'] = directory
+       if not directory is None:
+           self.abspath = os.path.join(directory.abspath, name, '')
+           self.path = os.path.join(directory.path, name, '')
+
+    def up(self):
+       return self.entries['..']
+
+
+# XXX TODO?
+# rfile
+# precious
+# no_rfile
+# rpath
+# rsrcpath
+# source_exists
+# derived_exists
+# is_on_rpath
+# local
+# base_suf
+# suffix
+# addsuffix
+# accessible
+# ignore
+# build
+# bind
+# is_under
+# relpath
+
+class File(Node):
+    """A class for files in a file system.
+    """
+
+    def __init__(self, name, directory):
+       self.abspath = os.path.join(directory.abspath, name)
+       self.path = os.path.join(directory.path, name)
diff --git a/src/scons/Node/FS/.aeignore b/src/scons/Node/FS/.aeignore
new file mode 100644 (file)
index 0000000..43fe851
--- /dev/null
@@ -0,0 +1,4 @@
+*,D
+*.pyc
+.*.swp
+.consign
diff --git a/src/scons/Node/FSTests.py b/src/scons/Node/FSTests.py
new file mode 100644 (file)
index 0000000..afa4340
--- /dev/null
@@ -0,0 +1,107 @@
+__revision__ = "Node/FSTests.py __REVISION__ __DATE__ __DEVELOPER__"
+
+import os
+import sys
+import unittest
+
+from scons.Node.FS import init, lookup, Dir, File
+
+
+
+built_it = None
+
+class Builder:
+    def execute(self, target = None, source = None):
+       global built_it
+       built_it = 1
+
+
+
+class FSTestCase(unittest.TestCase):
+    def runTest(self):
+       """This test case handles all of the file system node
+       tests in one environment, so we don't have to set up a
+       complicated directory structure for each test individually.
+       """
+       from TestCmd import TestCmd
+
+       test = TestCmd(workdir = '')
+       test.subdir('sub', ['sub', 'dir'])
+
+       wp = test.workpath('')
+       sub = test.workpath('sub', '')
+       sub_dir = test.workpath('sub', 'dir', '')
+       sub_dir_foo = test.workpath('sub', 'dir', 'foo', '')
+       sub_dir_foo_bar = test.workpath('sub', 'dir', 'foo', 'bar', '')
+       sub_foo = test.workpath('sub', 'foo', '')
+
+       os.chdir(sub_dir)
+
+       init()
+
+       def Dir_test(lpath, path, abspath, up_path):
+           dir = lookup(Dir, lpath)
+           assert(dir.path == path)
+           assert(dir.abspath == abspath)
+           assert(dir.up().path == up_path)
+
+       Dir_test('foo',         'foo/',         sub_dir_foo,            '.')
+       Dir_test('foo/bar',     'foo/bar/',     sub_dir_foo_bar,        'foo/')
+       Dir_test('/foo',        '/foo/',        '/foo/',                '/')
+       Dir_test('/foo/bar',    '/foo/bar/',    '/foo/bar/',            '/foo/')
+       Dir_test('..',          sub,            sub,                    wp)
+       Dir_test('foo/..',      '.',            sub_dir,                sub)
+       Dir_test('../foo',      sub_foo,        sub_foo,                sub)
+       Dir_test('.',           '.',            sub_dir,                sub)
+       Dir_test('./.',         '.',            sub_dir,                sub)
+       Dir_test('foo/./bar',   'foo/bar/',     sub_dir_foo_bar,        'foo/')
+
+       d1 = lookup(Dir, 'd1')
+
+       f1 = lookup(File, 'f1', directory = d1)
+
+       assert(f1.path == 'd1/f1')
+
+       try:
+           f2 = lookup(File, 'f1/f2', directory = d1)
+       except TypeError, x:
+           node = x.args[0]
+           assert(node.path == 'd1/f1')
+           assert(node.__class__.__name__ == 'File')
+       except:
+           raise
+
+       try:
+           dir = lookup(Dir, 'd1/f1')
+       except TypeError, x:
+           node = x.args[0]
+           assert(node.path == 'd1/f1')
+           assert(node.__class__.__name__ == 'File')
+       except:
+           raise
+
+       # Test for sub-classing of node building.
+       global built_it
+
+       built_it = None
+       assert not built_it
+       d1.path = "d"           # XXX FAKE SUBCLASS ATTRIBUTE
+       d1.sources = "d"        # XXX FAKE SUBCLASS ATTRIBUTE
+       d1.builder_set(Builder())
+       d1.build()
+       assert built_it
+
+       built_it = None
+       assert not built_it
+       f1.path = "f"           # XXX FAKE SUBCLASS ATTRIBUTE
+       f1.sources = "f"        # XXX FAKE SUBCLASS ATTRIBUTE
+       f1.builder_set(Builder())
+       f1.build()
+       assert built_it
+
+
+if __name__ == "__main__":
+    suite = unittest.TestSuite()
+    suite.addTest(FSTestCase())
+    if not unittest.TextTestRunner().run(suite).wasSuccessful():
+       sys.exit(1)
diff --git a/src/scons/Node/NodeTests.py b/src/scons/Node/NodeTests.py
new file mode 100644 (file)
index 0000000..92bc195
--- /dev/null
@@ -0,0 +1,43 @@
+__revision__ = "Node/NodeTests.py __REVISION__ __DATE__ __DEVELOPER__"
+
+import os
+import sys
+import unittest
+
+from scons.Node import Node
+
+
+
+built_it = None
+
+class Builder:
+    def execute(self, target = None, source = None):
+       global built_it
+       built_it = 1
+
+
+
+class NodeTestCase(unittest.TestCase):
+
+    def test_build(self):
+       """Test the ability to build a node.
+       """
+       node = Node()
+       node.builder_set(Builder())
+       node.path = "xxx"       # XXX FAKE SUBCLASS ATTRIBUTE
+       node.sources = "yyy"    # XXX FAKE SUBCLASS ATTRIBUTE
+       node.build()
+       assert built_it
+
+    def test_builder_set(self):
+       node = Node()
+       b = Builder()
+       node.builder_set(b)
+       assert node.builder == b
+
+
+
+if __name__ == "__main__":
+    suite = unittest.makeSuite(NodeTestCase, 'test_')
+    if not unittest.TextTestRunner().run(suite).wasSuccessful():
+       sys.exit(1)
diff --git a/src/scons/Node/__init__.py b/src/scons/Node/__init__.py
new file mode 100644 (file)
index 0000000..767f297
--- /dev/null
@@ -0,0 +1,19 @@
+"""scons.Node
+
+The Node package for the scons software construction utility.
+
+"""
+
+__revision__ = "Node/__init__.py __REVISION__ __DATE__ __DEVELOPER__"
+
+
+
+class Node:
+    """The base Node class, for entities that we know how to
+    build, or use to build other Nodes.
+    """
+    def build(self):
+       self.builder.execute(target = self.path, source = self.sources)
+
+    def builder_set(self, builder):
+       self.builder = builder
diff --git a/src/scons/Sig/.aeignore b/src/scons/Sig/.aeignore
new file mode 100644 (file)
index 0000000..43fe851
--- /dev/null
@@ -0,0 +1,4 @@
+*,D
+*.pyc
+.*.swp
+.consign
diff --git a/src/scons/Sig/MD5.py b/src/scons/Sig/MD5.py
new file mode 100644 (file)
index 0000000..36e4230
--- /dev/null
@@ -0,0 +1,70 @@
+"""scons.Sig.MD5
+
+The MD5 signature package for the scons software construction
+utility.
+
+"""
+
+__revision__ = "Sig/MD5.py __REVISION__ __DATE__ __DEVELOPER__"
+
+import md5
+import string
+
+
+
+def hexdigest(s):
+    """Return a signature as a string of hex characters.
+    """
+    # NOTE:  This routine is a method in the Python 2.0 interface
+    # of the native md5 module, but we want scons to operate all
+    # the way back to at least Python 1.5.2, which doesn't have it.
+    h = string.hexdigits
+    r = ''
+    for c in s:
+       i = ord(c)
+       r = r + h[(i >> 4) & 0xF] + h[i & 0xF]
+    return r
+
+
+
+def _init():
+    pass       # XXX
+
+def _end():
+    pass       # XXX
+
+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
diff --git a/src/scons/Sig/MD5Tests.py b/src/scons/Sig/MD5Tests.py
new file mode 100644 (file)
index 0000000..ac43f1b
--- /dev/null
@@ -0,0 +1,76 @@
+__revision__ = "Sig/MD5Tests.py __REVISION__ __DATE__ __DEVELOPER__"
+
+import sys
+import unittest
+
+import scons.Sig.MD5
+
+
+
+class my_obj:
+    """A dummy object class that satisfies the interface
+    requirements of the MD5 class.
+    """
+
+    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)
+       return self.sig
+
+    def current(self, sig):
+       return scons.Sig.MD5.current(self, sig)
+
+
+
+class MD5TestCase(unittest.TestCase):
+
+    def test__init(self):
+       pass    # XXX
+
+    def test__end(self):
+       pass    # XXX
+
+    def test_current(self):
+       """Test the ability to decide if an object is up-to-date
+       with 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
+
+    def test_collect(self):
+       """Test the ability to collect a sequence of object 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)
+
+    def test_signature(self):
+       pass    # XXX
+
+    def test_cmdsig(self):
+       pass    # XXX
+
+    def test_srcsig(self):
+       pass    # XXX
+
+
+if __name__ == "__main__":
+    suite = unittest.makeSuite(MD5TestCase, 'test_')
+    if not unittest.TextTestRunner().run(suite).wasSuccessful():
+       sys.exit(1)
diff --git a/src/scons/Sig/TimeStamp.py b/src/scons/Sig/TimeStamp.py
new file mode 100644 (file)
index 0000000..cab44bf
--- /dev/null
@@ -0,0 +1,49 @@
+"""scons.Sig.TimeStamp
+
+The TimeStamp signature package for the scons software construction
+utility.
+
+"""
+
+__revision__ = "Sig/TimeStamp.py __REVISION__ __DATE__ __DEVELOPER__"
+
+def _init():
+    pass       # XXX
+
+def _end():
+    pass       # XXX
+
+def current(obj, sig):
+    """Return whether the object's timestamp is up-to-date.
+    """
+    return obj.signature() >= sig
+
+def set():
+    pass       # XXX
+
+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):
+    """Generate a timestamp.
+    """
+    pass       # XXX
+#    return md5.new(contents).hexdigest()      # 2.0
+    return hexdigest(md5.new(contents).digest())
+
+def cmdsig():
+    pass       # XXX
+
+def srcsig():
+    pass       # XXX
diff --git a/src/scons/Sig/TimeStampTests.py b/src/scons/Sig/TimeStampTests.py
new file mode 100644 (file)
index 0000000..aa61af8
--- /dev/null
@@ -0,0 +1,73 @@
+__revision__ = "Sig/TimeStampTests.py __REVISION__ __DATE__ __DEVELOPER__"
+
+import sys
+import unittest
+
+import scons.Sig.TimeStamp
+
+
+
+class my_obj:
+    """A dummy object class that satisfies the interface
+    requirements of the TimeStamp class.
+    """
+
+    def __init__(self, value = ""):
+       self.value = value
+
+    def signature(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 the ability to decide if an object is up-to-date
+       with 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
+
+    def test_collect(self):
+       """Test the ability to collect a sequence of object timestamps
+       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)
+
+    def test_signature(self):
+       pass    # XXX
+
+    def test_cmdsig(self):
+       pass    # XXX
+
+    def test_srcsig(self):
+       pass    # XXX
+
+
+if __name__ == "__main__":
+    suite = unittest.makeSuite(TimeStampTestCase, 'test_')
+    if not unittest.TextTestRunner().run(suite).wasSuccessful():
+       sys.exit(1)
diff --git a/src/scons/Sig/__init__.py b/src/scons/Sig/__init__.py
new file mode 100644 (file)
index 0000000..411a94b
--- /dev/null
@@ -0,0 +1,7 @@
+"""scons.Sig
+
+The Signature package for the scons software construction utility.
+
+"""
+
+__revision__ = "Sig/__init__.py __REVISION__ __DATE__ __DEVELOPER__"
diff --git a/src/scons/__init__.py b/src/scons/__init__.py
new file mode 100644 (file)
index 0000000..9e279c2
--- /dev/null
@@ -0,0 +1,9 @@
+"""scons
+
+The main package for the scons software construction utility.
+
+"""
+
+__revision__ = "__init__.py __REVISION__ __DATE__ __DEVELOPER__"
+
+__version__ = "__VERSION__"
diff --git a/src/setup.py b/src/setup.py
new file mode 100644 (file)
index 0000000..ad93cac
--- /dev/null
@@ -0,0 +1,14 @@
+__revision__ = "setup.py __REVISION__ __DATE__ __DEVELOPER__"
+
+from string import join, split
+
+from distutils.core import setup
+
+setup(name = "scons",
+      version = "__VERSION__",
+      description = "scons",
+      author = "Steven Knight",
+      author_email = "knight@baldmt.com",
+      url = "http://www.baldmt.com/scons",
+      packages = ["scons"],
+      scripts = ["scons.py"])
diff --git a/template/.aeignore b/template/.aeignore
new file mode 100644 (file)
index 0000000..06d4a25
--- /dev/null
@@ -0,0 +1,2 @@
+*,D
+.consign
diff --git a/template/__init__.py b/template/__init__.py
new file mode 100644 (file)
index 0000000..69c58b8
--- /dev/null
@@ -0,0 +1,9 @@
+"""${subst '/' '.' ${subst '^src/' '' ${subst '/[^/]*$' '' $filename}}}
+
+XXX
+
+"""
+
+__revision__ = "${subst '^src/scons/' '' $filename} __REVISION__ __DATE__ __DEVELOPER__"
+
+__version__ = "__VERSION__"
diff --git a/template/file.py b/template/file.py
new file mode 100644 (file)
index 0000000..0fb2a74
--- /dev/null
@@ -0,0 +1,11 @@
+"""${subst '/' '.' ${subst '^src/' '' ${subst '\.py$' '' $filename}}}
+
+XXX
+
+"""
+
+__revision__ = "${subst '^src/scons/' '' $filename} __REVISION__ __DATE__ __DEVELOPER__"
+
+
+
+import XXX
diff --git a/template/test.py b/template/test.py
new file mode 100644 (file)
index 0000000..b43a73f
--- /dev/null
@@ -0,0 +1,3 @@
+__revision__ = "${subst '^src/scons/' '' $filename} __REVISION__ __DATE__ __DEVELOPER__"
+
+from TestCmd import TestCmd
diff --git a/test/.aeignore b/test/.aeignore
new file mode 100644 (file)
index 0000000..872e8be
--- /dev/null
@@ -0,0 +1,3 @@
+*,D
+.*.swp
+.consign
diff --git a/test/t0001.t b/test/t0001.t
new file mode 100644 (file)
index 0000000..8f9bed3
--- /dev/null
@@ -0,0 +1,30 @@
+#!/usr/bin/env python
+
+__revision__ = "test/t0001.t __REVISION__ __DATE__ __DEVELOPER__"
+
+from TestCmd import TestCmd
+
+test = TestCmd(program = 'scons.py', workdir = '', interpreter = 'python')
+
+test.write('SConstruct', """
+import os
+print "SConstruct", os.getcwd()
+Conscript('SConscript')
+""")
+
+# XXX I THINK THEY SHOULD HAVE TO RE-IMPORT OS HERE,
+# WHICH THEY DO FOR THE SECOND TEST BELOW, BUT NOT THE FIRST...
+test.write('SConscript', """
+import os
+print "SConscript " + os.getcwd()
+""")
+
+wpath = test.workpath()
+
+test.run(chdir = '.')
+test.fail_test(test.stdout() != ("SConstruct %s\nSConscript %s\n" % (wpath, wpath)))
+
+test.run(chdir = '.', arguments = '-f SConscript')
+test.fail_test(test.stdout() != ("SConscript %s\n" % wpath))
+
+test.pass_test()
diff --git a/test/t0010.py b/test/t0010.py
new file mode 100644 (file)
index 0000000..9d00a7f
--- /dev/null
@@ -0,0 +1,29 @@
+#!/usr/bin/env python
+
+__revision__ = "test/t0001.t __REVISION__ __DATE__ __DEVELOPER__"
+
+from TestCmd import TestCmd
+
+test = TestCmd(program = 'scons.py', workdir = '', interpreter = 'python')
+
+test.write('SConstruct', """
+env = Environment()
+env.Program(target = 'foo', source = 'foo.c')
+""")
+
+test.write('foo.c', """
+int
+main(int argc, char *argv[])
+{
+       printf("foo.c\n");
+       exit (0);
+}
+""")
+
+test.run(chdir = '.', arguments = 'foo')
+
+test.run(program = test.workpath('foo'))
+
+test.fail_test(test.stdout() != "foo.c\n")
+
+test.pass_test()