From 11ad88ce6d9165bebc6752a120bce4d962368bbf Mon Sep 17 00:00:00 2001 From: Steven Knight Date: Fri, 6 Jul 2001 11:46:17 +0000 Subject: Initial revision --- Construct | 112 ++++++++ TestCmd.py | 551 ++++++++++++++++++++++++++++++++++++++++ config | 264 +++++++++++++++++++ runtest.py | 51 ++++ src/.aeignore | 4 + src/MANIFEST | 12 + src/scons.py | 54 ++++ src/scons/.aeignore | 4 + src/scons/Builder.py | 58 +++++ src/scons/BuilderTests.py | 111 ++++++++ src/scons/Defaults.py | 20 ++ src/scons/Environment.py | 114 +++++++++ src/scons/EnvironmentTests.py | 129 ++++++++++ src/scons/Node/.aeignore | 4 + src/scons/Node/FS.py | 139 ++++++++++ src/scons/Node/FS/.aeignore | 4 + src/scons/Node/FSTests.py | 107 ++++++++ src/scons/Node/NodeTests.py | 43 ++++ src/scons/Node/__init__.py | 19 ++ src/scons/Sig/.aeignore | 4 + src/scons/Sig/MD5.py | 70 +++++ src/scons/Sig/MD5Tests.py | 76 ++++++ src/scons/Sig/TimeStamp.py | 49 ++++ src/scons/Sig/TimeStampTests.py | 73 ++++++ src/scons/Sig/__init__.py | 7 + src/scons/__init__.py | 9 + src/setup.py | 14 + template/.aeignore | 2 + template/__init__.py | 9 + template/file.py | 11 + template/test.py | 3 + test/.aeignore | 3 + test/t0001.t | 30 +++ test/t0010.py | 29 +++ 34 files changed, 2189 insertions(+) create mode 100644 Construct create mode 100644 TestCmd.py create mode 100644 config create mode 100644 runtest.py create mode 100644 src/.aeignore create mode 100644 src/MANIFEST create mode 100644 src/scons.py create mode 100644 src/scons/.aeignore create mode 100644 src/scons/Builder.py create mode 100644 src/scons/BuilderTests.py create mode 100644 src/scons/Defaults.py create mode 100644 src/scons/Environment.py create mode 100644 src/scons/EnvironmentTests.py create mode 100644 src/scons/Node/.aeignore create mode 100644 src/scons/Node/FS.py create mode 100644 src/scons/Node/FS/.aeignore create mode 100644 src/scons/Node/FSTests.py create mode 100644 src/scons/Node/NodeTests.py create mode 100644 src/scons/Node/__init__.py create mode 100644 src/scons/Sig/.aeignore create mode 100644 src/scons/Sig/MD5.py create mode 100644 src/scons/Sig/MD5Tests.py create mode 100644 src/scons/Sig/TimeStamp.py create mode 100644 src/scons/Sig/TimeStampTests.py create mode 100644 src/scons/Sig/__init__.py create mode 100644 src/scons/__init__.py create mode 100644 src/setup.py create mode 100644 template/.aeignore create mode 100644 template/__init__.py create mode 100644 template/file.py create mode 100644 template/test.py create mode 100644 test/.aeignore create mode 100644 test/t0001.t create mode 100644 test/t0010.py diff --git a/Construct b/Construct new file mode 100644 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 index 0000000..438bbed --- /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 " +__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 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 index 0000000..5c7b8de --- /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 0000000..43fe851 --- /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 0000000..508f198 --- /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 0000000..c700f77 --- /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 0000000..43fe851 --- /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 0000000..76c5512 --- /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 0000000..a749bf2 --- /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 0000000..0aa2b82 --- /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 0000000..c410162 --- /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 0000000..5c6c151 --- /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 0000000..43fe851 --- /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 0000000..7640a7a --- /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 0000000..43fe851 --- /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 0000000..afa4340 --- /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 0000000..92bc195 --- /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 0000000..767f297 --- /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 0000000..43fe851 --- /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 0000000..36e4230 --- /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 0000000..ac43f1b --- /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 0000000..cab44bf --- /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 0000000..aa61af8 --- /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 0000000..411a94b --- /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 0000000..9e279c2 --- /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 0000000..ad93cac --- /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 0000000..06d4a25 --- /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 0000000..69c58b8 --- /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 0000000..0fb2a74 --- /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 0000000..b43a73f --- /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 0000000..872e8be --- /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 0000000..8f9bed3 --- /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 0000000..9d00a7f --- /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() -- cgit v0.12