summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Construct112
-rw-r--r--TestCmd.py551
-rw-r--r--config264
-rw-r--r--runtest.py51
-rw-r--r--src/.aeignore4
-rw-r--r--src/MANIFEST12
-rw-r--r--src/scons.py54
-rw-r--r--src/scons/.aeignore4
-rw-r--r--src/scons/Builder.py58
-rw-r--r--src/scons/BuilderTests.py111
-rw-r--r--src/scons/Defaults.py20
-rw-r--r--src/scons/Environment.py114
-rw-r--r--src/scons/EnvironmentTests.py129
-rw-r--r--src/scons/Node/.aeignore4
-rw-r--r--src/scons/Node/FS.py139
-rw-r--r--src/scons/Node/FS/.aeignore4
-rw-r--r--src/scons/Node/FSTests.py107
-rw-r--r--src/scons/Node/NodeTests.py43
-rw-r--r--src/scons/Node/__init__.py19
-rw-r--r--src/scons/Sig/.aeignore4
-rw-r--r--src/scons/Sig/MD5.py70
-rw-r--r--src/scons/Sig/MD5Tests.py76
-rw-r--r--src/scons/Sig/TimeStamp.py49
-rw-r--r--src/scons/Sig/TimeStampTests.py73
-rw-r--r--src/scons/Sig/__init__.py7
-rw-r--r--src/scons/__init__.py9
-rw-r--r--src/setup.py14
-rw-r--r--template/.aeignore2
-rw-r--r--template/__init__.py9
-rw-r--r--template/file.py11
-rw-r--r--template/test.py3
-rw-r--r--test/.aeignore3
-rw-r--r--test/t0001.t30
-rw-r--r--test/t0010.py29
34 files changed, 2189 insertions, 0 deletions
diff --git a/Construct b/Construct
new file mode 100644
index 00000000..bf77bb75
--- /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
index 00000000..438bbedc
--- /dev/null
+++ b/TestCmd.py
@@ -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
index 00000000..c36eaf0a
--- /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
index 00000000..5c7b8de8
--- /dev/null
+++ b/runtest.py
@@ -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
index 00000000..43fe8513
--- /dev/null
+++ b/src/.aeignore
@@ -0,0 +1,4 @@
+*,D
+*.pyc
+.*.swp
+.consign
diff --git a/src/MANIFEST b/src/MANIFEST
new file mode 100644
index 00000000..508f1984
--- /dev/null
+++ b/src/MANIFEST
@@ -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
index 00000000..c700f779
--- /dev/null
+++ b/src/scons.py
@@ -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
index 00000000..43fe8513
--- /dev/null
+++ b/src/scons/.aeignore
@@ -0,0 +1,4 @@
+*,D
+*.pyc
+.*.swp
+.consign
diff --git a/src/scons/Builder.py b/src/scons/Builder.py
new file mode 100644
index 00000000..76c5512f
--- /dev/null
+++ b/src/scons/Builder.py
@@ -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
index 00000000..a749bf22
--- /dev/null
+++ b/src/scons/BuilderTests.py
@@ -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
index 00000000..0aa2b82e
--- /dev/null
+++ b/src/scons/Defaults.py
@@ -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
index 00000000..c410162b
--- /dev/null
+++ b/src/scons/Environment.py
@@ -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
index 00000000..5c6c151f
--- /dev/null
+++ b/src/scons/EnvironmentTests.py
@@ -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
index 00000000..43fe8513
--- /dev/null
+++ b/src/scons/Node/.aeignore
@@ -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
index 00000000..7640a7a5
--- /dev/null
+++ b/src/scons/Node/FS.py
@@ -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
index 00000000..43fe8513
--- /dev/null
+++ b/src/scons/Node/FS/.aeignore
@@ -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
index 00000000..afa4340a
--- /dev/null
+++ b/src/scons/Node/FSTests.py
@@ -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
index 00000000..92bc1958
--- /dev/null
+++ b/src/scons/Node/NodeTests.py
@@ -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
index 00000000..767f2972
--- /dev/null
+++ b/src/scons/Node/__init__.py
@@ -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
index 00000000..43fe8513
--- /dev/null
+++ b/src/scons/Sig/.aeignore
@@ -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
index 00000000..36e4230e
--- /dev/null
+++ b/src/scons/Sig/MD5.py
@@ -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
index 00000000..ac43f1ba
--- /dev/null
+++ b/src/scons/Sig/MD5Tests.py
@@ -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
index 00000000..cab44bf2
--- /dev/null
+++ b/src/scons/Sig/TimeStamp.py
@@ -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
index 00000000..aa61af80
--- /dev/null
+++ b/src/scons/Sig/TimeStampTests.py
@@ -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
index 00000000..411a94b2
--- /dev/null
+++ b/src/scons/Sig/__init__.py
@@ -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
index 00000000..9e279c29
--- /dev/null
+++ b/src/scons/__init__.py
@@ -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
index 00000000..ad93cacc
--- /dev/null
+++ b/src/setup.py
@@ -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
index 00000000..06d4a253
--- /dev/null
+++ b/template/.aeignore
@@ -0,0 +1,2 @@
+*,D
+.consign
diff --git a/template/__init__.py b/template/__init__.py
new file mode 100644
index 00000000..69c58b86
--- /dev/null
+++ b/template/__init__.py
@@ -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
index 00000000..0fb2a740
--- /dev/null
+++ b/template/file.py
@@ -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
index 00000000..b43a73fd
--- /dev/null
+++ b/template/test.py
@@ -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
index 00000000..872e8be6
--- /dev/null
+++ b/test/.aeignore
@@ -0,0 +1,3 @@
+*,D
+.*.swp
+.consign
diff --git a/test/t0001.t b/test/t0001.t
new file mode 100644
index 00000000..8f9bed3e
--- /dev/null
+++ b/test/t0001.t
@@ -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
index 00000000..9d00a7fd
--- /dev/null
+++ b/test/t0010.py
@@ -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()