Merged revisions 1675-1736 via svnmerge from
authorstevenknight <stevenknight@fdb21ef1-2011-0410-befe-b5e4ea1792b1>
Sat, 16 Dec 2006 01:43:01 +0000 (01:43 +0000)
committerstevenknight <stevenknight@fdb21ef1-2011-0410-befe-b5e4ea1792b1>
Sat, 16 Dec 2006 01:43:01 +0000 (01:43 +0000)
http://scons.tigris.org/svn/scons/branches/core

........
  r1689 | stevenknight | 2006-11-06 20:56:29 -0600 (Mon, 06 Nov 2006) | 1 line

  0.96.D483 - Merge changes for 0.96.93 packaging from the subsidiary branch.
........
  r1690 | stevenknight | 2006-11-06 20:59:30 -0600 (Mon, 06 Nov 2006) | 1 line

  0.96.D484 - Update HOWTO for releases. Fix name type in src/CHANGES.txt.
........
  r1691 | stevenknight | 2006-11-08 13:55:36 -0600 (Wed, 08 Nov 2006) | 1 line

  0.96.D485 - Fix MergeFlags() handling of None values. (John Pye)
........
  r1692 | stevenknight | 2006-11-08 17:15:05 -0600 (Wed, 08 Nov 2006) | 1 line

  0.96.D486 - Directly execute commands on Windows when possible. (Jay Kint)
........
  r1693 | stevenknight | 2006-11-08 18:54:49 -0600 (Wed, 08 Nov 2006) | 1 line

  0.96.D487 - Remove the semi-colon from the list of characters that determine when we use cmd
........
  r1694 | stevenknight | 2006-11-09 01:34:06 -0600 (Thu, 09 Nov 2006) | 1 line

  0.96.D488 - Pick up latex/bibtex 'Rerun to get citations correct' messages. (Dmitry Mikhin)
........
  r1695 | stevenknight | 2006-11-11 08:36:33 -0600 (Sat, 11 Nov 2006) | 1 line

  0.96.D489 - Back out the direct-execution-on-Windows change until we solve a corner case.
........
  r1696 | stevenknight | 2006-11-15 10:33:10 -0600 (Wed, 15 Nov 2006) | 1 line

  0.96.D490 - Fix the sconsign script when the .sconsign.dblite file is specified with its suf
........
  r1697 | stevenknight | 2006-11-18 10:45:50 -0600 (Sat, 18 Nov 2006) | 4 lines

  Complete move of test/sconsign/script.py to underneath test/sconsign/script/.
  (This got left out of the previous checkin due to an error in the
  script that resubmits Aegis changes to Subversion.)
........
  r1698 | stevenknight | 2006-11-18 11:05:26 -0600 (Sat, 18 Nov 2006) | 1 line

  0.96.D491 - Allow an Options converter to take the construction environment as a parameter.
........
  r1699 | stevenknight | 2006-11-30 15:34:37 -0600 (Thu, 30 Nov 2006) | 1 line

  0.96.D492 - Reverse the order in which we try the arguments Options converters, first a sing
........
  r1700 | stevenknight | 2006-11-30 16:03:09 -0600 (Thu, 30 Nov 2006) | 1 line

  0.96.D493 - Speed up rel_path() by avoiding recomputation of intermediate directory relative
........
  r1701 | stevenknight | 2006-11-30 16:14:16 -0600 (Thu, 30 Nov 2006) | 1 line

  0.96.D494 - More efficient get_suffix(): compute it once when we set the name.
........
  r1702 | stevenknight | 2006-11-30 16:22:55 -0600 (Thu, 30 Nov 2006) | 1 line

  0.96.D495 - Fix missing XML end tags.
........
  r1703 | stevenknight | 2006-11-30 17:15:25 -0600 (Thu, 30 Nov 2006) | 1 line

  0.96.D496 - Turn Memoizer into a simple counter for --debug=memoizer, not something that doe
........
  r1704 | stevenknight | 2006-11-30 20:30:50 -0600 (Thu, 30 Nov 2006) | 1 line

  0.96.D497 - Add the scons-time script, with doc and tests.
........
  r1705 | stevenknight | 2006-11-30 23:28:20 -0600 (Thu, 30 Nov 2006) | 1 line

  0.96.D498 - Update the copyright years string.
........
  r1706 | stevenknight | 2006-12-01 11:54:22 -0600 (Fri, 01 Dec 2006) | 1 line

  0.96.D499 - Fix _do_Lookup => _doLookup value-caching misspellings. (Ben Leslie)
........
  r1707 | stevenknight | 2006-12-01 12:03:46 -0600 (Fri, 01 Dec 2006) | 1 line

  0.96.D500 - Fix copyright test against debian build. (Walter Franzini)
........
  r1708 | stevenknight | 2006-12-01 14:23:29 -0600 (Fri, 01 Dec 2006) | 1 line

  0.96.D501 - Add #include lines for test portability. (Gary Oberbrunner)
........
  r1709 | stevenknight | 2006-12-01 14:51:12 -0600 (Fri, 01 Dec 2006) | 1 line

  0.96.D502 - Fix tests under Python versions with no profiler (pstats module).
........
  r1710 | stevenknight | 2006-12-01 20:04:49 -0600 (Fri, 01 Dec 2006) | 1 line

  0.96.D503 - Remove unnecessary os.path.normpath() calls. (Gary Oberbrunner)
........
  r1711 | stevenknight | 2006-12-01 20:34:31 -0600 (Fri, 01 Dec 2006) | 1 line

  0.96.D504 - Accomodate arbitray white space after a SWIG %module keyword. (Anonymous)
........
  r1712 | stevenknight | 2006-12-05 14:49:54 -0600 (Tue, 05 Dec 2006) | 1 line

  0.96.D506 - Cache substitutions of of Builder source suffixes. Use a new PathList module, and a refactor Node.FS.Rfindalldirs() method, to cache calculations of values like CPPPATH.
........
  r1713 | stevenknight | 2006-12-05 18:43:36 -0600 (Tue, 05 Dec 2006) | 1 line

  0.96.D507 - Use cached stat() values in diskchecks.
........
  r1714 | stevenknight | 2006-12-05 21:11:24 -0600 (Tue, 05 Dec 2006) | 1 line

  0.96.D508 - Fix Memoizer hit counts for methods memoizing simple values. Clean up the code for memoizing return values in a dictionary. Fix comments.
........
  r1715 | stevenknight | 2006-12-06 07:23:18 -0600 (Wed, 06 Dec 2006) | 1 line

  0.96.D369 - More efficient Node.FS.Dir.current() check. Fix some Windows test portability issues.
........
  r1716 | stevenknight | 2006-12-06 12:24:32 -0600 (Wed, 06 Dec 2006) | 2 lines

  Undo previous checkin (distributed incorrect Aegis change number).
........
  r1717 | stevenknight | 2006-12-06 12:34:53 -0600 (Wed, 06 Dec 2006) | 1 line

  0.96.D505 - Update ae-{cvs,svn}-ci for newer versions of aetar, and to not truncate descriptions.
........
  r1718 | stevenknight | 2006-12-07 23:01:41 -0600 (Thu, 07 Dec 2006) | 1 line

  0.96.D509 - Only look for mslink on Windows systems. (Sohail Somani)
........
  r1719 | stevenknight | 2006-12-07 23:18:33 -0600 (Thu, 07 Dec 2006) | 1 line

  0.96.D510 - Have the D compiler Tool use the same  logic for shared libraries, too. (Paolo Invernizzi)
........
  r1720 | stevenknight | 2006-12-07 23:29:47 -0600 (Thu, 07 Dec 2006) | 1 line

  0.96.D511 - Generalize a JobTests.py test so it doesn't assume a specific order in which the operating system executes the threads.
........
  r1721 | stevenknight | 2006-12-07 23:39:37 -0600 (Thu, 07 Dec 2006) | 1 line

  0.96.D512 - Back out the Tool/dmd.py  change; it breaks shared library linking for other lanuages beside D in the construction environment.
........
  r1722 | stevenknight | 2006-12-07 23:47:11 -0600 (Thu, 07 Dec 2006) | 1 line

  0.96.D513 - Test fixes: Windows portability, handle changes to Python 2.5 messages.
........
  r1723 | stevenknight | 2006-12-08 00:00:13 -0600 (Fri, 08 Dec 2006) | 1 line

  0.96.D514 - Change how the 'as' Tool is imported to accomodate the Python 2.6 'as' keyword.
........
  r1724 | stevenknight | 2006-12-08 11:19:27 -0600 (Fri, 08 Dec 2006) | 1 line

  0.96.D515 - Cache both Node.FS.find_file() and Node.FS.Dri.srcdir_find_file().
........
  r1725 | stevenknight | 2006-12-08 17:27:35 -0600 (Fri, 08 Dec 2006) | 1 line

  0.96.D516 - Better error when we try to fetch contents from an Entry that doesn't exist. (Tom Parker)
........
  r1726 | stevenknight | 2006-12-08 23:28:55 -0600 (Fri, 08 Dec 2006) | 1 line

  0.96.D517 - Make sure we pick up the scons-local directory regardless of where we chdir internally.
........
  r1727 | stevenknight | 2006-12-11 16:25:53 -0600 (Mon, 11 Dec 2006) | 1 line

  0.96.D518 - Cache results of Executor.get_unignored_sources() and Executor.process_sources(). Eliminate some map() and disambiguate() calls when scanning for implicit dependencies.
........
  r1728 | stevenknight | 2006-12-12 14:32:22 -0600 (Tue, 12 Dec 2006) | 1 line

  0.96.D519 - Fix SideEffect() when -j is used.
........
  r1729 | stevenknight | 2006-12-12 16:58:15 -0600 (Tue, 12 Dec 2006) | 1 line

  0.96.D520 - Add a srcdir keyword to Builder calls.
........
  r1730 | stevenknight | 2006-12-12 21:40:59 -0600 (Tue, 12 Dec 2006) | 1 line

  0.96.D521 - TeX/LaTeX updates, including handling files in subdirectories. (Joel B. Mohler, Rob Managan, Dmitry Mikhin)
........
  r1731 | stevenknight | 2006-12-14 15:01:02 -0600 (Thu, 14 Dec 2006) | 1 line

  0.96.D522 - Propogate TypeErrors during variable substitution for display to the user.
........
  r1732 | stevenknight | 2006-12-14 20:01:49 -0600 (Thu, 14 Dec 2006) | 1 line

  0.96.D523 - Fix the os.path.join() calls in EnvironmentTests.py.
........
  r1733 | stevenknight | 2006-12-15 07:48:22 -0600 (Fri, 15 Dec 2006) | 1 line

  0.96.D524 - Fix source directories as dependencies of an Alias (0.96.93 problem found by LilyPond).
........
  r1735 | stevenknight | 2006-12-15 12:43:45 -0600 (Fri, 15 Dec 2006) | 1 line

  0.96.D525 - Allow printing Debug.caller() output (or other end-of-run debugging info) when using -h.
........
  r1736 | stevenknight | 2006-12-15 16:30:08 -0600 (Fri, 15 Dec 2006) | 1 line

  0.96.D526 - Add an option to debug IndexError and NameError exceptions during variable substitution.
........

git-svn-id: http://scons.tigris.org/svn/scons/trunk@1738 fdb21ef1-2011-0410-befe-b5e4ea1792b1

181 files changed:
HOWTO/release.txt
HOWTO/subrelease.txt
QMTest/SConscript
QMTest/TestCmd.py
QMTest/TestCommon.py
QMTest/TestSCons.py
QMTest/TestSCons_time.py [new file with mode: 0644]
QMTest/TestSConsign.py [new file with mode: 0644]
README
SConstruct
bin/ae-cvs-ci
bin/ae-svn-ci
debian/changelog
doc/SConscript
doc/man/scons-time.1 [new file with mode: 0644]
doc/man/scons.1
rpm/scons.spec.in
src/CHANGES.txt
src/RELEASE.txt
src/engine/MANIFEST.in
src/engine/SCons/Action.py
src/engine/SCons/Builder.py
src/engine/SCons/BuilderTests.py
src/engine/SCons/Defaults.py
src/engine/SCons/Environment.py
src/engine/SCons/EnvironmentTests.py
src/engine/SCons/Executor.py
src/engine/SCons/JobTests.py
src/engine/SCons/Memoize.py
src/engine/SCons/MemoizeTests.py [new file with mode: 0644]
src/engine/SCons/Node/FS.py
src/engine/SCons/Node/FSTests.py
src/engine/SCons/Node/__init__.py
src/engine/SCons/Options/__init__.py
src/engine/SCons/PathList.py [new file with mode: 0644]
src/engine/SCons/PathListTests.py [new file with mode: 0644]
src/engine/SCons/Scanner/CTests.py
src/engine/SCons/Scanner/D.py
src/engine/SCons/Scanner/Fortran.py
src/engine/SCons/Scanner/FortranTests.py
src/engine/SCons/Scanner/IDLTests.py
src/engine/SCons/Scanner/LaTeX.py
src/engine/SCons/Scanner/LaTeXTests.py
src/engine/SCons/Scanner/Prog.py
src/engine/SCons/Scanner/ProgTests.py
src/engine/SCons/Scanner/ScannerTests.py
src/engine/SCons/Scanner/__init__.py
src/engine/SCons/Script/Main.py
src/engine/SCons/Script/SConscript.py
src/engine/SCons/Script/__init__.py
src/engine/SCons/Subst.py
src/engine/SCons/SubstTests.py
src/engine/SCons/Taskmaster.py
src/engine/SCons/TaskmasterTests.py
src/engine/SCons/Tool/386asm.py
src/engine/SCons/Tool/dvipdf.py
src/engine/SCons/Tool/dvips.py
src/engine/SCons/Tool/gas.py
src/engine/SCons/Tool/latex.py
src/engine/SCons/Tool/mslink.py
src/engine/SCons/Tool/msvc.xml
src/engine/SCons/Tool/pdflatex.py
src/engine/SCons/Tool/pdftex.py
src/engine/SCons/Tool/swig.py
src/engine/SCons/Tool/tex.py
src/engine/SCons/Warnings.py
src/script/MANIFEST.in
src/script/scons-time.py [new file with mode: 0644]
src/script/scons.py
src/script/sconsign.py
src/setup.py
src/test_copyrights.py
test/Alias/srcdir.py [new file with mode: 0644]
test/BadBuilder.py
test/BuildDir/errors.py
test/Builder/srcdir.py [new file with mode: 0644]
test/Errors/AttributeError.py [new file with mode: 0644]
test/Errors/Exception.py [new file with mode: 0644]
test/Errors/InternalError.py [new file with mode: 0644]
test/Errors/NameError.py [new file with mode: 0644]
test/Errors/SyntaxError.py [new file with mode: 0644]
test/Errors/TypeError.py [new file with mode: 0644]
test/Errors/UserError.py [new file with mode: 0644]
test/Errors/exit-status.py [new file with mode: 0644]
test/Options/Options.py
test/SConscript/src_dir.py
test/SWIG/SWIG.py
test/Scanner/generated.py
test/SideEffect.py [deleted file]
test/SideEffect/basic.py [new file with mode: 0644]
test/SideEffect/build_dir.py [new file with mode: 0644]
test/SideEffect/directory.py [new file with mode: 0644]
test/SideEffect/parallel.py [new file with mode: 0644]
test/Subst/AllowSubstExceptions.py [new file with mode: 0644]
test/Subst/SyntaxError.py [new file with mode: 0644]
test/Subst/TypeError.py [new file with mode: 0644]
test/TEX/TEX.py
test/TEX/build_dir.py [new file with mode: 0644]
test/TEX/subdir-input.py [new file with mode: 0644]
test/errors.py [deleted file]
test/import.py
test/option-c.py
test/option-v.py
test/option/debug-memoizer.py
test/option/debug-nomemoizer.py
test/option/profile.py
test/scons-time/func/basic.py [new file with mode: 0644]
test/scons-time/func/chdir.py [new file with mode: 0644]
test/scons-time/func/file.py [new file with mode: 0644]
test/scons-time/func/format-gnuplot.py [new file with mode: 0644]
test/scons-time/func/function.py [new file with mode: 0644]
test/scons-time/func/glob.py [new file with mode: 0644]
test/scons-time/func/help.py [new file with mode: 0644]
test/scons-time/func/no-args.py [new file with mode: 0644]
test/scons-time/func/prefix.py [new file with mode: 0644]
test/scons-time/func/tail.py [new file with mode: 0644]
test/scons-time/help/all-subcommands.py [new file with mode: 0644]
test/scons-time/help/options.py [new file with mode: 0644]
test/scons-time/mem/chdir.py [new file with mode: 0644]
test/scons-time/mem/file.py [new file with mode: 0644]
test/scons-time/mem/format-gnuplot.py [new file with mode: 0644]
test/scons-time/mem/glob.py [new file with mode: 0644]
test/scons-time/mem/help.py [new file with mode: 0644]
test/scons-time/mem/no-args.py [new file with mode: 0644]
test/scons-time/mem/prefix.py [new file with mode: 0644]
test/scons-time/mem/stage.py [new file with mode: 0644]
test/scons-time/mem/tail.py [new file with mode: 0644]
test/scons-time/no-args.py [new file with mode: 0644]
test/scons-time/obj/chdir.py [new file with mode: 0644]
test/scons-time/obj/file.py [new file with mode: 0644]
test/scons-time/obj/format-gnuplot.py [new file with mode: 0644]
test/scons-time/obj/glob.py [new file with mode: 0644]
test/scons-time/obj/help.py [new file with mode: 0644]
test/scons-time/obj/no-args.py [new file with mode: 0644]
test/scons-time/obj/no-files.py [new file with mode: 0644]
test/scons-time/obj/prefix.py [new file with mode: 0644]
test/scons-time/obj/stage.py [new file with mode: 0644]
test/scons-time/obj/tail.py [new file with mode: 0644]
test/scons-time/run/aegis.py [new file with mode: 0644]
test/scons-time/run/archive/dir.py [new file with mode: 0644]
test/scons-time/run/archive/tar-gz.py [new file with mode: 0644]
test/scons-time/run/archive/tar.py [new file with mode: 0644]
test/scons-time/run/archive/tgz.py [new file with mode: 0644]
test/scons-time/run/archive/zip.py [new file with mode: 0644]
test/scons-time/run/config/archive_list.py [new file with mode: 0644]
test/scons-time/run/config/initial_commands.py [new file with mode: 0644]
test/scons-time/run/config/prefix.py [new file with mode: 0644]
test/scons-time/run/config/python.py [new file with mode: 0644]
test/scons-time/run/config/scons.py [new file with mode: 0644]
test/scons-time/run/config/subdir.py [new file with mode: 0644]
test/scons-time/run/config/targets.py [new file with mode: 0644]
test/scons-time/run/option/help.py [new file with mode: 0644]
test/scons-time/run/option/next-run.py [new file with mode: 0644]
test/scons-time/run/option/no-args.py [new file with mode: 0644]
test/scons-time/run/option/no-exec.py [new file with mode: 0644]
test/scons-time/run/option/number.py [new file with mode: 0644]
test/scons-time/run/option/outdir.py [new file with mode: 0644]
test/scons-time/run/option/prefix.py [new file with mode: 0644]
test/scons-time/run/option/python.py [new file with mode: 0644]
test/scons-time/run/option/quiet.py [new file with mode: 0644]
test/scons-time/run/option/scons.py [new file with mode: 0644]
test/scons-time/run/option/subdir.py [new file with mode: 0644]
test/scons-time/run/option/verbose.py [new file with mode: 0644]
test/scons-time/run/subversion.py [new file with mode: 0644]
test/scons-time/time/chdir.py [new file with mode: 0644]
test/scons-time/time/file.py [new file with mode: 0644]
test/scons-time/time/format-gnuplot.py [new file with mode: 0644]
test/scons-time/time/glob.py [new file with mode: 0644]
test/scons-time/time/help.py [new file with mode: 0644]
test/scons-time/time/no-args.py [new file with mode: 0644]
test/scons-time/time/prefix.py [new file with mode: 0644]
test/scons-time/time/tail.py [new file with mode: 0644]
test/scons-time/time/which.py [new file with mode: 0644]
test/scons-time/unknown.py [new file with mode: 0644]
test/sconsign/script.py [deleted file]
test/sconsign/script/SConsignFile.py [new file with mode: 0644]
test/sconsign/script/Signatures.py [new file with mode: 0644]
test/sconsign/script/bad.py [new file with mode: 0644]
test/sconsign/script/dblite.py [new file with mode: 0644]
test/sconsign/script/no-SConsignFile.py [new file with mode: 0644]
test/symlink/BuildDir.py

index 88f93521a42277a3cd474007cc36e5bd0b9db252..57f38b77e517b8d943ec19f46765d9f01b00b805 100644 (file)
@@ -8,427 +8,477 @@ then see the document HOWTO/subrelease.txt.
 
 Things to do to release a new X.Y version of SCons:
 
-       Prepare the describe-the-release section for the announcements
+    Prepare the describe-the-release section for the announcements
 
-               summarize changes from src/CHANGES.txt
+        summarize changes from src/CHANGES.txt
 
-               template is below, search for "describe-the-release"
+        template is below, search for "describe-the-release"
 
-               send this out for review while you get the rest of the
-               release ready!
+        send this out for review while you get the rest of the
+        release ready!
 
-       Build and test candidate packages
+    Build and test candidate packages
 
-               test on Linux
+        test on Linux
 
-               test on Windows NT
+        test on Windows NT
 
-                       1) tar zxf scons-src-{version}.tar.gz
-                          cd scons-src-{version}
-                          python runtest.py -a
+            1) tar zxf scons-src-{version}.tar.gz
+               cd scons-src-{version}
+               python runtest.py -a
 
-                       2) tar zxf scons-{version}.tar.gz
-                          cd scons-{version}
-                          python setup.py install
-                          cd scons-src-{version}
-                          python runtest.py -a -X -x C:\Python20\scons.bat
+            2) tar zxf scons-{version}.tar.gz
+               cd scons-{version}
+               python setup.py install
+               cd scons-src-{version}
+               python runtest.py -a -X -x C:\Python20\scons.bat
 
-                       3) scons-{verson}.win32.exe
-                          cd scons-src-{version}
-                          python runtest.py -a -X -x C:\Python20\scons.bat
+            3) scons-{verson}.win32.exe
+               cd scons-src-{version}
+               python runtest.py -a -X -x C:\Python20\scons.bat
 
-                       4) mkdir temporary_directory
-                          cd temporary_directory
-                          tar zxf scons-local-{version}.tar.gz
-                          cd scons-src-{version}
-                          python runtest.py -a -x C:\temporary_directory\scons.py
+            4) mkdir temporary_directory
+               cd temporary_directory
+               tar zxf scons-local-{version}.tar.gz
+               cd scons-src-{version}
+               python runtest.py -a -x C:\temporary_directory\scons.py
 
-                Checkin any changes necessary to make everything work
+        Check in any changes necessary to make everything work
 
-       END THE CURRENT DEVELOPMENT BRANCH
+    END THE CURRENT DEVELOPMENT BRANCH
 
-               ae_p scons.0
+        ae_p scons.0
 
-               aede {96}
+        aede {96}
 
-               aerpass {96}
+        aerpass {96}
 
-               aeib {96}
+        aeib {96}
 
-               aed
+        aed
 
-               aeb
+        aeb
 
-               aet
+        aet
 
-               aet -reg
+        aet -reg
 
-               aeipass
+        aeipass
 
-       START THE NEW BRANCH FOR RELEASE
+    START THE NEW BRANCH FOR RELEASE
 
-               aenbr -p scons.0 {97}
+        aenbr -p scons.0 {97}
 
-               aenc -p scons.0.{97}
+        aenc -p scons.0.{97}
 
-                        Call it something like,
-                        "Initialize the new branch for release."
-                        Cause = internal_enhancement.
-                        Exempt it from all tests (*_exempt = true).
+            Call it something like,
+            "Initialize the new branch for release."
+            Cause = internal_enhancement.
+            Exempt it from all tests (*_exempt = true).
 
-               ae_p scons.0.{97}
+        ae_p scons.0.{97}
 
-               aedb 100
+        aedb 100
 
-               aecd
+        aecd
 
-               # Change the hard-coded package version numbers
-               # in the following files.
-               aecp README
-               vi README
+        # Change the hard-coded package version numbers
+        # in the following files.
+        aecp README
+        vi README
 
-                aecp SConstruct
-                vi SConstruct
+        aecp SConstruct
+        vi SConstruct
 
-                aecp rpm/scons.spec.in
-                vi rpm/scons.spec.in
+        aecp rpm/scons.spec.in
+        vi rpm/scons.spec.in
 
-                aecp QMTest/TestSCons.py
-                vi QMTest/TestSCons.py
+        aecp QMTest/TestSCons.py
+        vi QMTest/TestSCons.py
 
-                # Read through and update the README files if necessary
-               [optional] aecp README
-               [optional] vi README
+        # Read through and update the README files if necessary
+        [optional] aecp README
+        [optional] vi README
 
-               [optional] aecp src/README.txt
-               [optional] vi src/README.txt
+        [optional] aecp src/README.txt
+        [optional] vi src/README.txt
 
-                # Prepare src/CHANGES.txt
-               aecp src/CHANGES.txt
-               vi src/CHANGES.txt
+        # Prepare src/CHANGES.txt
+        aecp src/CHANGES.txt
+        vi src/CHANGES.txt
 
-                       date -R the latest release
+            date -R the latest release
 
-                       should be current if this has been updated
-                        as each change went in.
+            should be current if this has been updated
+            as each change went in.
 
-               # Prepare src/RELEASE.txt
-                aecp src/RELEASE.txt
-                vi src/RELEASE.txt
+        # Prepare src/RELEASE.txt
+        aecp src/RELEASE.txt
+        vi src/RELEASE.txt
 
-                       date -R the latest release
+            date -R the latest release
 
-                       Read through and edit appropriately.
+            Read through and edit appropriately.
 
-                       Can probably keep most of the existing text
+            Can probably keep most of the existing text
 
-                       Add any new known problems
+            Add any new known problems
 
-               # Prepare debian/changelog
-                aecp debian/changelog
-                vi debian/changelog
+        # Prepare debian/changelog
+        aecp debian/changelog
+        vi debian/changelog
 
-                       date -R the latest release
+            date -R the latest release
 
-                # Now build and prepare the release itself.
-                aeb
+        # Now build and prepare the release itself.
+        aeb
 
-               aed
+        aed
 
-               aet -reg
+        aet -reg
 
-               aede
+        aede
 
-               etc.
+        etc.
 
 
 
-       Read through the FAQ for any updates
+    Read through the FAQ for any updates
 
 
 
-       Upload the packages to the SourceForge incoming FTP:
+    Upload the packages to the SourceForge incoming FTP:
 
-               ftp upload.sourceforge.net
-               anonymous
-               <your email>
-               cd incoming
-               bin
-               put scons-0.{97}-1.noarch.rpm
-               put scons-0.{97}-1.src.rpm
-               put scons-0.{97}.tar.gz
-               put scons-0.{97}.win32.exe
-               put scons-0.{97}.zip
-               put scons-local-0.{97}.tar.gz
-               put scons-local-0.{97}.zip
-               put scons-src-0.{97}.tar.gz
-               put scons-src-0.{97}.zip
-               put scons_0.{97}-1_all.deb
+        ftp upload.sourceforge.net
+        anonymous
+        <your email>
+        cd incoming
+        bin
+        put scons-0.{97}-1.noarch.rpm
+        put scons-0.{97}-1.src.rpm
+        put scons-0.{97}.tar.gz
+        put scons-0.{97}.win32.exe
+        put scons-0.{97}.zip
+        put scons-local-0.{97}.tar.gz
+        put scons-local-0.{97}.zip
+        put scons-src-0.{97}.tar.gz
+        put scons-src-0.{97}.zip
+        put scons_0.{97}-1_all.deb
 
-       Create the new release at the SourceForge project page:
+    Create the new release at the SourceForge project page:
 
-               Go to the File Release page
+        Pull down the "Admin" menu and select "File Releases"
 
-               Package Name:  scons
+        Package Name:  scons
 
-               => Add Release
+        => Add Release
 
-               New release name:  0.{97}
+        New release name:  0.{97}
 
-               Cut-and-paste or upload the RELEASE.txt file.
+        Upload the RELEASE.txt file.
 
-               Cut-and-paste or upload the CHANGES.txt file.
+        Upload the CHANGES.txt file.
 
-               (If you cut-and-paste, check the "Preserve my
-               pre-formatted text." box!)
+        Check the "Preserve my pre-formatted text." box  (IMPORTANT!)
 
-               Click "Submit/Refresh"  (IMPORTANT!)
+        Click "Submit/Refresh"  (IMPORTANT!)
 
-               Check the SCons files you uploaded
+        Check the SCons files you uploaded
 
-               Click "Add Files and/or Refresh View"
+        Click "Add Files and/or Refresh View"
 
-               Edit the file info:
+        Edit the file info:
 
-                       scons-0.{97}-1.noarch.rpm       Any     .rpm
-                       scons-0.{97}-1.src.rpm          Any     Source .rpm
-                       scons-0.{97}.tar.gz             Any     .gz
-                       scons-0.{97}.win32.exe          i386    .exe (32-bit Windows)
-                       scons-0.{97}.zip                Any     .zip
-                       scons_0.{97}-1_all.deb          Any     .deb
+            scons-0.{97}-1.noarch.rpm           Any     .rpm
+            scons-0.{97}-1.src.rpm              Any     Source .rpm
+            scons-0.{97}.tar.gz                 Any     .gz
+            scons-0.{97}.win32.exe              i386    .exe (32-bit Windows)
+            scons-0.{97}.zip                    Any     .zip
+            scons_0.{97}-1_all.deb              Any     .deb
 
-                Click "Update/Refresh" for each file; this must be done
-                one at a time.
+        Click "Update/Refresh" for each file; this must be done
+        one at a time.
 
-               Check "I'm sure." and click "Send Notice" in the Email
-               Release Notice section.
+        Check "I'm sure." and click "Send Notice" in the Email
+        Release Notice section.
 
 
-               Go to the File Release page
+        Pull down the "Admin" menu and select "File Releases"
 
-               Package Name:  scons-local
+        Package Name:  scons-local
 
-               => Add Release
+        => Add Release
 
-               New release name:  0.{97}
+        New release name:  0.{97}
 
-               Cut-and-paste or upload the RELEASE.txt file.
+        Upload the RELEASE.txt file.
 
-               Cut-and-paste or upload the CHANGES.txt file.
+        Upload the CHANGES.txt file.
 
-               (If you cut-and-paste, check the "Preserve my
-               pre-formatted text." box!)
+        Check the "Preserve my pre-formatted text." box  (IMPORTANT!)
 
-               Click "Submit/Refresh"  (IMPORTANT!)
+        Click "Submit/Refresh"  (IMPORTANT!)
 
-               Check the SCons files you uploaded
+        Check the SCons files you uploaded
 
-               Click "Add Files and/or Refresh View"
+        Click "Add Files and/or Refresh View"
 
-               Edit the file info:
+        Edit the file info:
 
-                       scons-local-0.{97}.tar.gz       Any     .gz
-                       scons-local-0.{97}.zip          Any     .zip
+            scons-local-0.{97}.tar.gz        Any        .gz
+            scons-local-0.{97}.zip           Any        .zip
 
-                Click "Update/Refresh" for each file; this must be done
-                one at a time.
+        Click "Update/Refresh" for each file; this must be done
+        one at a time.
 
-               Check "I'm sure." and click "Send Notice" in the Email
-               Release Notice section.
+        Check "I'm sure." and click "Send Notice" in the Email
+        Release Notice section.
 
 
-               Go to the File Release page
+        Pull down the "Admin" menu and select "File Releases"
 
-               Package Name:  scons-src
+        Package Name:  scons-src
 
-               => Add Release
+        => Add Release
 
-               New release name:  0.{97}
+        New release name:  0.{97}
 
-               Cut-and-paste or upload the RELEASE.txt file.
+        Upload the RELEASE.txt file.
 
-               Cut-and-paste or upload the CHANGES.txt file.
+        Upload the CHANGES.txt file.
 
-               (If you cut-and-paste, check the "Preserve my
-               pre-formatted text." box!)
+        Check the "Preserve my pre-formatted text." box  (IMPORTANT!)
 
-               Click "Submit/Refresh"  (IMPORTANT!)
+        Click "Submit/Refresh"  (IMPORTANT!)
 
-               Check the SCons files you uploaded
+        Check the SCons files you uploaded
 
-               Click "Add Files and/or Refresh View"
+        Click "Add Files and/or Refresh View"
 
-               Edit the file info:
+        Edit the file info:
 
-                       scons-src-0.{97}.tar.gz         Any     .gz
-                       scons-src-0.{97}.zip            Any     .zip
+            scons-src-0.{97}.tar.gz                Any        .gz
+            scons-src-0.{97}.zip                   Any        .zip
 
-                Click "Update/Refresh" for each file; this must be done
-                one at a time.
+        Click "Update/Refresh" for each file; this must be done
+        one at a time.
 
-               Check "I'm sure." and click "Send Notice" in the Email
-               Release Notice section.
+        Check "I'm sure." and click "Send Notice" in the Email
+        Release Notice section.
 
 
-       Hide release 0.{95} at the SourceForge download page:
+    Hide release 0.{95} at the SourceForge download page:
 
-               Go to the Admin page
+        Pull down the "Admin" menu and select "File Releases"
 
-               => Edit/Add File Releases
+        Package Name:  scons
 
-               Package Name:  scons
+        => Edit Releases
 
-               => Edit Releases
+        Release Name: 0.{95}
 
-               Release Name: 0.{95}
+        => Edit This Release
 
-               => Edit This Release
+        Status: => Hidden
 
-               Status: => Hidden
+        Click Submit/Refresh
 
-               Click Submit/Refresh
 
+        Pull down the "Admin" menu and select "File Releases"
 
-               Go to the Admin page
+        Package Name:  scons-local
 
-               => Edit/Add File Releases
+        => Edit Releases
 
-               Package Name:  scons-local
+        Release Name: 0.{95}
 
-               => Edit Releases
+        => Edit This Release
 
-               Release Name: 0.{95}
+        Status: => Hidden
 
-               => Edit This Release
+        Click Submit/Refresh
 
-               Status: => Hidden
 
-               Click Submit/Refresh
+        Pull down the "Admin" menu and select "File Releases"
 
+        Package Name:  scons-src
 
-               Go to the Admin page
+        => Edit Releases
 
-               => Edit/Add File Releases
+        Release Name: 0.{95}
 
-               Package Name:  scons-src
+        => Edit This Release
 
-               => Edit Releases
+        Status: => Hidden
 
-               Release Name: 0.{95}
+        Click Submit/Refresh
 
-               => Edit This Release
 
-               Status: => Hidden
 
-               Click Submit/Refresh
+    Add a new release for 0.{97} in the Issue Tracker at tigris.org:
 
+        Click "Issue Tracker" on the left-hand nav bar
 
+        Click "Configuration options"
 
-       In the Bugs Tracker, add a Group for the new release 0.{97}
+        Click "Add/edit components"
 
+        Under "scons"
+        To the right of "Add ..."
+        Click "Version"
 
+        At the bottom of the list click "Add"
 
-       Test downloading from the SourceForge project page
+        Fill in the "Version:" box with 0.{97}
 
-                You may need to wait a good bit; they seem to update
-                this on half-hour cycles.
+        Check "Add this version to *all* components."
 
+        Click the "Add" button
 
 
-       Update the web site:
 
-               template:  new version number
+    Update the scons.org web site:
 
-                src/doc.py:  new version number
+        svn co http://scons.tigris.org/svn/scons/scons.org
 
-                src/download.py:  new version number
+        cd scons.org
 
-               src/index.py:  announcement on the home page
+            CHANGES.txt:    copy new version from built source tree
 
-               src/scons-doc.tar.gz:  update
+            download.php:   new version number
 
-               src/CHANGES.txt:  update
+            versions.php:   add new version number do $docversions[],
+                            shift index numbers :-(
 
-       Test downloading from the web site download page
+            index.php:      announcement on the home page
+                            remove out-of-date announcements
 
+            news-raw.xhtml: add announcement to list (dup from home page)
 
+            RELEASE.txt:    copy new version from built source tree
 
-       Add news item to the SourceForge project page
+        mkdir doc/0.{97}
 
+        (cd doc/0.{97} && tar zxf scons-doc-0.{97}.tar.gz)
 
+        svn add doc/0.{97}
 
-       Announce to the following mailing lists (template below):
+        svn commit
 
-               scons-announce@lists.sourceforge.net
-               scons-users@lists.sourceforge.net
-               scons-devel@lists.sourceforge.net
+        ssh -l scons manam.pair.com
 
-                       [right away]
+            cd public_html
 
-               python-announce@python.org
+            mkdir new
 
-                       [right away, it's moderated and will take
-                       some time to get through]
+            svn co http://scons.tigris.org/svn/scons/scons.org new
 
-               linux-announce@news.ornl.gov
+            mv production previous && mv new production
 
-                       [right away, it's moderated and will take
-                       some time to get through]
+        [point your browser to http://www.scons.org/]
 
-               [optional] cons-discuss@gnu.org
 
-                       [only if it's a really big announcement,
-                       I said we wouldn't bug this list]
+    Update the tigris.org web site:
 
-               python-list@python.org
+        svn co http://scons.tigris.org/svn/scons/trunk
 
-                        [wait until business hours so the announcement
-                       hits mailboxes while U.S. workers are active]
+        cd trunk
 
-        Notify Gentoo Linux of the update
+            www/project_highlights.html
 
-                For now, we will do this by entering a bug report, and
-                attaching the files in build/gentoo to the report.  Go
-                to:
+            www/roadmap.html
 
-                        http://bugs.gentoo.org/
+        svn commit
 
-                This requires an account (based on your email address)
-                and a certain amount of Bugzilla-based navigation,
-                but nothing that's too difficult.
 
-                This is just my best stab at a process that will work
-                for Gentoo.  This process may change if the Gentoo
-                developers come back and want something submitted in
-                some other form.
 
-       Notify www.cmtoday.com/contribute.html
 
-                [This guy wants an announcement no more frequently than
-                once a month, so save it for a future release if it's
-                been too soon since the previous one.]
+    Test downloading from the SourceForge project page
 
-       Notify freshmeat.net
+        You may need to wait a good bit; they seem to update
+        this on half-hour cycles.
 
-                [Wait until the morning so the announcement hits the
-                main freshmeat.net page while people in the U.S. are
-                awake and working]
 
+    Test downloading from the web site download page
 
 
-       Checkin another change to prepare for development on this branch.
 
-               # Prep the following files to track the changes
-               # made during the next development cycle
-               aecp src/CHANGES.txt src/RELEASE.txt
-               vi src/CHANGES.txt src/RELEASE.txt
+    Add news item to the SourceForge project page
 
-               # Optionally, update release numbers in the following:
-               [optional] aecp HOWTO/change.txt
-               [optional] vi HOWTO/change.txt
 
-               [optional] aecp HOWTO/release.txt
-               [optional] vi HOWTO/release.txt
+
+    Announce to the following mailing lists (template below):
+
+        scons-announce@lists.sourceforge.net
+        scons-users@lists.sourceforge.net
+        scons-devel@lists.sourceforge.net
+
+            [right away]
+
+        python-announce@python.org
+
+            [right away, it's moderated and will take
+            some time to get through]
+
+        linux-announce@news.ornl.gov
+
+            [right away, it's moderated and will take
+            some time to get through]
+
+        [optional] cons-discuss@gnu.org
+
+            [only if it's a really big announcement,
+            I said we wouldn't bug this list]
+
+        python-list@python.org
+
+            [wait until business hours so the announcement
+            hits mailboxes while U.S. workers are active]
+
+    Notify Gentoo Linux of the update
+
+        For now, we will do this by entering a bug report, and
+        attaching the files in build/gentoo to the report.  Go
+        to:
+
+            http://bugs.gentoo.org/
+
+        This requires an account (based on your email address)
+        and a certain amount of Bugzilla-based navigation,
+        but nothing that's too difficult.
+
+        This is just my best stab at a process that will work
+        for Gentoo.  This process may change if the Gentoo
+        developers come back and want something submitted in
+        some other form.
+
+    Notify www.cmtoday.com/contribute.html
+
+        [This guy wants an announcement no more frequently than
+        once a month, so save it for a future release if it's
+        been too soon since the previous one.]
+
+    Notify freshmeat.net
+
+        [Wait until the morning so the announcement hits the
+        main freshmeat.net page while people in the U.S. are
+        awake and working]
+
+
+
+    Checkin another change to prepare for development on this branch.
+
+        # Prep the following files to track the changes
+        # made during the next development cycle
+        aecp src/CHANGES.txt src/RELEASE.txt
+        vi src/CHANGES.txt src/RELEASE.txt
+
+        # Optionally, update release numbers in the following:
+        [optional] aecp HOWTO/change.txt
+        [optional] vi HOWTO/change.txt
+
+        [optional] aecp HOWTO/release.txt
+        [optional] vi HOWTO/release.txt
 
 
 
@@ -477,18 +527,18 @@ XXX Template describe-the-release section goes here XXX
 Special thanks to XXX, XXX, and XXX for their contributions to this
 release.
 
-       --SK
+        --SK
 =======================
 Template scons-users + scons-announce announcement:
 
 Version 0.95 of SCons has been released and is available for download
 from the SCons web site:
 
-       http://www.scons.org/
+        http://www.scons.org/
 
 Or through the download link at the SCons project page at SourceForge:
 
-       http://sourceforge.net/projects/scons/
+        http://sourceforge.net/projects/scons/
 
 RPM and Debian packages and a Win32 installer are all available, in
 addition to the traditional .tar.gz and .zip files.
@@ -506,7 +556,7 @@ release.
 
 On behalf of the SCons team,
 
-       --SK
+        --SK
 =======================
 Template python-announce, linux-announce and python-list announcement:
 
@@ -517,11 +567,11 @@ build tool competition in August 2000.
 Version 0.95 of SCons has been released and is available for download
 from the SCons web site:
 
-       http://www.scons.org/
+        http://www.scons.org/
 
 Or through the download link at the SCons project page at SourceForge:
 
-       http://sourceforge.net/projects/scons/
+        http://sourceforge.net/projects/scons/
 
 RPM and Debian packages and a Win32 installer are all available, in
 addition to the traditional .tar.gz and .zip files.
@@ -553,13 +603,13 @@ Distinctive features of SCons include:
 An scons-users mailing list is available for those interested in getting
 started using SCons.  You can subscribe at:
 
-       http://lists.sourceforge.net/lists/listinfo/scons-users
+        http://lists.sourceforge.net/lists/listinfo/scons-users
 
 Alternatively, we invite you to subscribe to the low-volume
 scons-announce mailing list to receive notification when new versions of
 SCons become available:
 
-       http://lists.sourceforge.net/lists/listinfo/scons-announce
+        http://lists.sourceforge.net/lists/listinfo/scons-announce
 
 
 ACKNOWLEDGEMENTS
@@ -569,4 +619,4 @@ release.
 
 On behalf of the SCons team,
 
-       --SK
+        --SK
index 6826c81792da4ed59e4f1d95f88c0184b3ecc260..fbba52a29666fc957a2c3b5fde3bd7370a153ad3 100644 (file)
@@ -8,105 +8,105 @@ see the document HOWTO/release.txt.
 
 Things to do to release a new X.Y.Z version of SCons:
 
-       START THE NEW SUB-BRANCH FOR SUBRELEASE
+    START THE NEW SUB-BRANCH FOR SUBRELEASE
 
-               aenbr -p scons.0{94} {1}
+        aenbr -p scons.0.{94} {1}
 
-               aenc -p scons.0.{94}.{1}
+        aenc -p scons.0.{94}.{1}
 
-                        Call it something like,
-                        "Prepare a new sub-release for XYZ."
-                        Cause = internal_enhancement.
-                        Exempt it from all tests (*_exempt = true).
+            Call it something like,
+            "Prepare a new sub-release for XYZ."
+            Cause = internal_enhancement.
+            Exempt it from all tests (*_exempt = true).
 
-               ae_p scons.0.{94}.{1}
+        ae_p scons.0.{94}.{1}
 
-               aedb 100
+        aedb 100
 
-               aecd
+        aecd
 
-               # Change the hard-coded package version numbers
-               # in the following files.
-               aecp README
-               vi README
+        # Change the hard-coded package version numbers
+        # in the following files.
+        aecp README
+        vi README
 
-                aecp SConstruct
-                vi SConstruct
+        aecp SConstruct
+        vi SConstruct
 
-                aecp rpm/scons.spec.in
-                vi rpm/scons.spec.in
+        aecp rpm/scons.spec.in
+        vi rpm/scons.spec.in
 
-                aecp src/setup.py
-                vi src/setup.py
+        aecp src/setup.py
+        vi src/setup.py
 
-                aecp QMTest/TestSCons.py
-                vi QMTest/TestSCons.py
+        aecp QMTest/TestSCons.py
+        vi QMTest/TestSCons.py
 
-                # Read through and update the README files if necessary
-               [optional] aecp README
-               [optional] vi README
+        # Read through and update the README files if necessary
+        [optional] aecp README
+        [optional] vi README
 
-               [optional] aecp src/README.txt
-               [optional] vi src/README.txt
+        [optional] aecp src/README.txt
+        [optional] vi src/README.txt
 
-                # Prepare src/CHANGES.txt
-               aecp src/CHANGES.txt
-               vi src/CHANGES.txt
+        # Prepare src/CHANGES.txt
+        aecp src/CHANGES.txt
+        vi src/CHANGES.txt
 
-                        change the release line to reflect
-                        the new subrelease
+            change the release line to reflect
+            the new subrelease
 
-                       date -R the new subrelease
+            date -R the new subrelease
 
-                        add an explanatory not after the subrelease line:
+            add an explanatory not after the subrelease line:
 
-                                NOTE:  This is a pre-release of 0.{95}
-                                for testing purposes.  When 0.{95} is
-                                released, all these changes will show
-                                up as 0.95 changes.
+                    NOTE:  This is a pre-release of 0.{95}
+                    for testing purposes.  When 0.{95} is
+                    released, all these changes will show
+                    up as 0.95 changes.
 
-               # Prepare src/RELEASE.txt
-                aecp src/RELEASE.txt
-                vi src/RELEASE.txt
+        # Prepare src/RELEASE.txt
+        aecp src/RELEASE.txt
+        vi src/RELEASE.txt
 
-                       date -R the release only if necessary
+            date -R the release only if necessary
 
-                       Read through and edit appropriately.
+            Read through and edit appropriately.
 
-                       Can probably keep most of the existing text
+            Can probably keep most of the existing text
 
-                       Add any new known problems
+            Add any new known problems
 
-               # Prepare debian/changelog
-                aecp debian/changelog
-                vi debian/changelog
+        # Prepare debian/changelog
+        aecp debian/changelog
+        vi debian/changelog
 
-                        add the new subrelease
+            add the new subrelease
 
-                       date -R the new subrelease
+            date -R the new subrelease
 
-                # Now build and prepare the release itself.
-                aeb
+        # Now build and prepare the release itself.
+        aeb
 
-               aet -reg
+        aet -reg
 
-               aed
+        aed
 
-               aede
+        aede
 
-               etc.
+        etc.
 
 
 
-       Make the relevant packages available for by-hand pickup directly
-       off the web site:
+    Make the relevant packages available for by-hand pickup directly
+    off the web site:
 
-               scp scons-0.{94}.{1}.tar.gz stevenknight@scons.sourceforge.net:/home/groups/s/sc/scons/htdocs
-               scp scons-0.{94}.{1}.zip stevenknight@scons.sourceforge.net:/home/groups/s/sc/scons/htdocs
+        scp scons-0.{94}.{1}.tar.gz stevenknight@scons.sourceforge.net:/home/groups/s/sc/scons/htdocs
+        scp scons-0.{94}.{1}.zip stevenknight@scons.sourceforge.net:/home/groups/s/sc/scons/htdocs
 
 
-       Test downloading from the web site.
+    Test downloading from the web site.
 
 
 
-       Announce to dev@scons.tigris.org.
+    Announce to dev@scons.tigris.org.
index e016dc4d26a5f546862985214c44e27b0e82a862..309f0dc234a79d22e5a118c16dfec361f1dbf0b2 100644 (file)
@@ -37,6 +37,8 @@ files = [
     'TestCommon.py',
     'TestRuntest.py',
     'TestSCons.py',
+    'TestSConsign.py',
+    'TestSCons_time.py',
     'unittest.py',
 ]
 
index 9b3e7a225ea1144d0bec4276902eb11b295054bb..7a668e8e2f5c5e9a8ebc61163ab0049077c4384f 100644 (file)
@@ -176,8 +176,8 @@ version.
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 
 __author__ = "Steven Knight <knight at baldmt dot com>"
-__revision__ = "TestCmd.py 0.22.D001 2006/02/26 15:45:18 knight"
-__version__ = "0.22"
+__revision__ = "TestCmd.py 0.23.D001 2006/11/30 13:57:29 knight"
+__version__ = "0.23"
 
 import os
 import os.path
@@ -193,9 +193,17 @@ import traceback
 import types
 import UserList
 
-__all__ = [ 'fail_test', 'no_result', 'pass_test',
-            'match_exact', 'match_re', 'match_re_dotall',
-            'python_executable', 'TestCmd' ]
+__all__ = [
+    'diff_re',
+    'fail_test',
+    'no_result',
+    'pass_test',
+    'match_exact',
+    'match_re',
+    'match_re_dotall',
+    'python_executable',
+    'TestCmd'
+]
 
 def is_List(e):
     return type(e) is types.ListType \
@@ -362,6 +370,31 @@ def match_re_dotall(lines = None, res = None):
     if re.compile("^" + res + "$", re.DOTALL).match(lines):
         return 1
 
+def diff_re(a, b, fromfile='', tofile='',
+                fromfiledate='', tofiledate='', n=3, lineterm='\n'):
+    """
+    A simple "diff" of two sets of lines when the expected lines
+    are regular expressions.  This is a really dumb thing that
+    just compares each line in turn, so it doesn't look for
+    chunks of matching lines and the like--but at least it lets
+    you know exactly which line first didn't compare correctl...
+    """
+    result = []
+    diff = len(a) - len(b)
+    if diff < 0:
+        a = a + ['']*(-diff)
+    elif diff > 0:
+        b = b + ['']*diff
+    i = 0
+    for aline, bline in zip(a, b):
+        if not re.compile("^" + aline + "$").search(bline):
+            result.append("%sc%s" % (i+1, i+1))
+            result.append('< ' + repr(a[i]))
+            result.append('---')
+            result.append('> ' + repr(b[i]))
+        i = i+1
+    return result
+
 if os.name == 'java':
 
     python_executable = os.path.join(sys.prefix, 'jython')
index b30b75ca84985b035d60480b04f7a1a017d5b185..b215a265288bf4e872c3fa4ba1450a0a94d72197 100644 (file)
@@ -80,8 +80,8 @@ The TestCommon module also provides the following variables
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 
 __author__ = "Steven Knight <knight at baldmt dot com>"
-__revision__ = "TestCommon.py 0.22.D001 2006/02/26 15:45:18 knight"
-__version__ = "0.22"
+__revision__ = "TestCommon.py 0.23.D001 2006/11/30 13:57:29 knight"
+__version__ = "0.23"
 
 import os
 import os.path
index de15526431726f3eba0a3afef08de6cde65ef793..091c29f81e5efcf49d3c6bbd9a572b22b7f38c35 100644 (file)
@@ -30,7 +30,7 @@ from TestCommon import __all__
 # to what we expect.  (If we derived the version number from the same
 # data driving the build we might miss errors if the logic breaks.)
 
-SConsVersion = '0.96.92'
+SConsVersion = '0.96.93'
 
 __all__.extend([ 'TestSCons',
                  'python',
@@ -303,6 +303,28 @@ class TestSCons(TestCommon):
             i = i + 1
         return "Actual matched the expected output???"
 
+    def python_file_line(self, file, line):
+        """
+        Returns a Python error line for output comparisons.
+
+        The exec of the traceback line gives us the correct format for
+        this version of Python.  Before 2.5, this yielded:
+
+            File "<string>", line 1, ?
+
+        Python 2.5 changed this to:
+
+            File "<string>", line 1, <module>
+
+        We stick the requested file name and line number in the right
+        places, abstracting out the version difference.
+        """
+        exec 'import traceback; x = traceback.format_stack()[-1]'
+        x = string.lstrip(x)
+        x = string.replace(x, '<string>', file)
+        x = string.replace(x, 'line 1,', 'line %s,' % line)
+        return x
+
     def java_ENV(self):
         """
         Return a default external environment that uses a local Java SDK
diff --git a/QMTest/TestSCons_time.py b/QMTest/TestSCons_time.py
new file mode 100644 (file)
index 0000000..90eb5a8
--- /dev/null
@@ -0,0 +1,365 @@
+"""
+TestSCons_time.py:  a testing framework for the scons-test.py script
+
+A TestSCons_time environment object is created via the usual invocation:
+
+    test = TestSCons_time()
+
+TestSCons_time is a subclass of TestCommon, which is in turn is a subclass
+of TestCmd), and hence has available all of the methods and attributes
+from those classes, as well as any overridden or additional methods or
+attributes defined in this subclass.
+"""
+
+# Copyright (c) 2001, 2002, 2003, 2004 The SCons Foundation
+
+__revision__ = "QMTest/TestSCons_time.py 0.96.C629 2006/11/19 06:39:17 knight"
+
+import os
+import os.path
+import string
+import sys
+
+from TestCommon import *
+from TestCommon import __all__
+
+__all__.extend([ 'TestSCons',
+                 'python',
+                 '_exe',
+                 '_obj',
+                 '_shobj',
+                 'lib_',
+                 '_lib',
+                 'dll_',
+                 '_dll'
+               ])
+
+python = python_executable
+_python_ = '"' + python_executable + '"'
+
+SConstruct = """\
+import os
+print "SConstruct file directory:", os.getcwd()
+"""
+
+scons_py = """\
+#!/usr/bin/env python
+import os
+import sys
+def write_args(fp, args):
+    fp.write(args[0] + '\\n')
+    for arg in args[1:]:
+        fp.write('    ' + arg + '\\n')
+write_args(sys.stdout, sys.argv)
+for arg in sys.argv[1:]:
+    if arg[:10] == '--profile=':
+        profile = open(arg[10:], 'wb')
+        profile.write('--profile\\n')
+        write_args(profile, sys.argv)
+        break
+sys.stdout.write('SCONS_LIB_DIR = ' + os.environ['SCONS_LIB_DIR'] + '\\n')
+execfile('SConstruct')
+"""
+
+aegis_py = """\
+#!/usr/bin/env python
+import os
+import sys
+script_dir = 'src/script'
+if not os.path.exists(script_dir):
+    os.makedirs(script_dir)
+open(script_dir + '/scons.py', 'w').write(
+r'''%s''')
+""" % scons_py
+
+
+svn_py = """\
+#!/usr/bin/env python
+import os
+import sys
+dir = sys.argv[-1]
+script_dir = dir + '/src/script'
+os.makedirs(script_dir)
+open(script_dir + '/scons.py', 'w').write(
+r'''%s''')
+""" % scons_py
+
+
+logfile_contents = """\
+Memory before reading SConscript files:  100%(index)s
+Memory after reading SConscript files:  200%(index)s
+Memory before building targets:  300%(index)s
+Memory after building targets:  400%(index)s
+Object counts:
+       pre-   post-    pre-   post-   
+       read    read   build   build   Class
+       101%(index)s    102%(index)s    103%(index)s    104%(index)s   Action.CommandAction
+       201%(index)s    202%(index)s    203%(index)s    204%(index)s   Action.CommandGeneratorAction
+       301%(index)s    302%(index)s    303%(index)s    304%(index)s   Action.FunctionAction
+       401%(index)s    402%(index)s    403%(index)s    404%(index)s   Action.LazyAction
+       501%(index)s    502%(index)s    503%(index)s    504%(index)s   Action.ListAction
+       601%(index)s    602%(index)s    603%(index)s    604%(index)s   Builder.BuilderBase
+       701%(index)s    702%(index)s    703%(index)s    704%(index)s   Builder.CompositeBuilder
+       801%(index)s    802%(index)s    803%(index)s    804%(index)s   Builder.ListBuilder
+       901%(index)s    902%(index)s    903%(index)s    904%(index)s   Builder.MultiStepBuilder
+      1001%(index)s   1002%(index)s   1003%(index)s   1004%(index)s   Builder.OverrideWarner
+      1101%(index)s   1102%(index)s   1103%(index)s   1104%(index)s   Environment.Base
+      1201%(index)s   1202%(index)s   1203%(index)s   1204%(index)s   Environment.EnvironmentClone
+      1301%(index)s   1302%(index)s   1303%(index)s   1304%(index)s   Environment.OverrideEnvironment
+      1401%(index)s   1402%(index)s   1403%(index)s   1404%(index)s   Executor.Executor
+      1501%(index)s   1502%(index)s   1503%(index)s   1504%(index)s   Node.FS
+      1601%(index)s   1602%(index)s   1603%(index)s   1604%(index)s   Node.FS.Base
+      1701%(index)s   1702%(index)s   1703%(index)s   1704%(index)s   Node.FS.Dir
+      1801%(index)s   1802%(index)s   1803%(index)s   1804%(index)s   Node.FS.File
+      1901%(index)s   1902%(index)s   1904%(index)s   1904%(index)s   Node.FS.RootDir
+      2001%(index)s   2002%(index)s   2003%(index)s   2004%(index)s   Node.Node
+Total build time: 11.123456 seconds
+Total SConscript file execution time: 22.234567 seconds
+Total SCons execution time: 33.345678 seconds
+Total command execution time: 44.456789 seconds
+"""
+
+
+profile_py = """\
+%(body)s
+
+import profile
+
+try: dispatch = profile.Profile.dispatch
+except AttributeError: pass
+else: dispatch['c_exception'] = profile.Profile.trace_dispatch_return
+
+prof = profile.Profile()
+prof.runcall(%(call)s)
+prof.dump_stats(r'%(profile_name)s')
+"""
+
+
+class TestSCons_time(TestCommon):
+    """Class for testing the scons-time script.
+
+    This provides a common place for initializing scons-time tests,
+    eliminating the need to begin every test with the same repeated
+    initializations.
+    """
+
+    def __init__(self, **kw):
+        """Initialize an SCons_time testing object.
+
+        If they're not overridden by keyword arguments, this
+        initializes the object with the following default values:
+
+                program = 'scons-time'
+                interpreter = ['python', '-tt']
+                match = match_exact
+                workdir = ''
+
+        The workdir value means that, by default, a temporary workspace
+        directory is created for a TestSCons_time environment.
+        In addition, this method changes directory (chdir) to the
+        workspace directory, so an explicit "chdir = '.'" on all of the
+        run() method calls is not necessary.
+        """
+
+        self.orig_cwd = os.getcwd()
+        try:
+            script_dir = os.environ['SCONS_SCRIPT_DIR']
+        except KeyError:
+            pass
+        else:
+            os.chdir(script_dir)
+        if not kw.has_key('program'):
+            p = os.environ.get('SCONS_TIME')
+            if not p:
+                p = 'scons-time'
+                if not os.path.exists(p):
+                    p = 'scons-time.py'
+            kw['program'] = p
+
+        if not kw.has_key('interpreter'):
+            kw['interpreter'] = [python, '-tt']
+
+        if not kw.has_key('match'):
+            kw['match'] = match_exact
+
+        if not kw.has_key('workdir'):
+            kw['workdir'] = ''
+
+        apply(TestCommon.__init__, [self], kw)
+
+        try:
+            eval('[x for x in [1, 2]]')
+        except SyntaxError:
+            version = string.split(sys.version)[0]
+            msg = 'scons-time does not work on Python version %s\n' % version
+            self.skip_test(msg)
+
+    def archive_split(self, path):
+        if path[-7:] == '.tar.gz':
+            return path[:-7], path[-7:]
+        else:
+            return os.path.splitext(path)
+
+    def must_contain_all_lines(self, name, content, expected, exists=None):
+        missing_lines = []
+
+        if exists is None:
+            exists = lambda e, c: string.find(c, e) != -1
+
+        for e in expected:
+            if not exists(e, content):
+                missing_lines.append(e)
+
+        if missing_lines:
+            sys.stdout.write('%s is missing expected string(s):\n' % name)
+            for m in missing_lines:
+                sys.stdout.write('    ' + repr(m) + '\n')
+            sys.stdout.write('%s content:\n' % name)
+            sys.stdout.write(content)
+            self.fail_test()
+
+    def fake_logfile(self, logfile_name, index=0):
+        self.write(self.workpath(logfile_name), logfile_contents % locals())
+
+    def profile_data(self, profile_name, python_name, call, body):
+        profile_name = self.workpath(profile_name)
+        python_name = self.workpath(python_name)
+        d = {
+            'profile_name'  : profile_name,
+            'python_name'   : python_name,
+            'call'          : call,
+            'body'          : body,
+        }
+        self.write(python_name, profile_py % d)
+        self.run(program = python_name, interpreter = sys.executable)
+
+    def skip_test(self, message="Skipping test.\n"):
+        """Skips a test.
+
+        Proper test-skipping behavior is dependent on whether we're being
+        executed as part of development of a change under Aegis.
+
+        Technically, skipping a test is a NO RESULT, but Aegis will
+        treat that as a test failure and prevent the change from going
+        to the next step.  We don't want to force anyone using Aegis
+        to have to install absolutely every tool used by the tests,
+        so we actually report to Aegis that a skipped test has PASSED
+        so that the workflow isn't held up.
+        """
+        if message:
+            sys.stdout.write(message)
+            sys.stdout.flush()
+        devdir = os.popen("aesub '$dd' 2>/dev/null", "r").read()[:-1]
+        intdir = os.popen("aesub '$intd' 2>/dev/null", "r").read()[:-1]
+        if devdir and self._cwd[:len(devdir)] == devdir or \
+           intdir and self._cwd[:len(intdir)] == intdir:
+            # We're under the development directory for this change,
+            # so this is an Aegis invocation; pass the test (exit 0).
+            self.pass_test()
+        else:
+            # skip=1 means skip this function when showing where this
+            # result came from.  They only care about the line where the
+            # script called test.skip_test(), not the line number where
+            # we call test.no_result().
+            self.no_result(skip=1)
+
+    def write_fake_aegis_py(self, name):
+        name = self.workpath(name)
+        self.write(name, aegis_py)
+        os.chmod(name, 0755)
+        return name
+
+    def write_fake_scons_py(self):
+        self.subdir('src', ['src', 'script'])
+        self.write('src/script/scons.py', scons_py)
+
+    def write_fake_svn_py(self, name):
+        name = self.workpath(name)
+        self.write(name, svn_py)
+        os.chmod(name, 0755)
+        return name
+
+    def write_sample_directory(self, archive, dir, files):
+        dir = self.workpath(dir)
+        for name, content in files:
+            path = os.path.join(dir, name)
+            d, f = os.path.split(path)
+            if not os.path.isdir(d):
+                os.makedirs(d)
+            open(path, 'wb').write(content)
+        return dir
+
+    def write_sample_tarfile(self, archive, dir, files):
+        import shutil
+        try:
+            import tarfile
+
+        except ImportError:
+
+            self.skip_test('no tarfile module\n')
+
+        else:
+
+            base, suffix = self.archive_split(archive)
+
+            mode = {
+                '.tar'      : 'w',
+                '.tar.gz'   : 'w:gz',
+                '.tgz'      : 'w:gz',
+            }
+
+            tar = tarfile.open(archive, mode[suffix])
+            for name, content in files:
+                path = os.path.join(dir, name)
+                open(path, 'wb').write(content)
+                tarinfo = tar.gettarinfo(path, path)
+                tarinfo.uid = 111
+                tarinfo.gid = 111
+                tarinfo.uname = 'fake_user'
+                tarinfo.gname = 'fake_group'
+                tar.addfile(tarinfo, open(path, 'rb'))
+            tar.close()
+            shutil.rmtree(dir)
+            return self.workpath(archive)
+
+    def write_sample_zipfile(self, archive, dir, files):
+        import shutil
+        try:
+            import zipfile
+        except ImportError:
+
+            sys.stderr.write('no zipfile module\n')
+            self.no_result()
+
+        else:
+
+            zip = zipfile.ZipFile(archive, 'w')
+            for name, content in files:
+                path = os.path.join(dir, name)
+                open(path, 'wb').write(content)
+                zip.write(path)
+            zip.close()
+            shutil.rmtree(dir)
+            return self.workpath(archive)
+
+    sample_project_files = [
+        ('SConstruct',  SConstruct),
+    ]
+
+    def write_sample_project(self, archive, dir=None):
+        base, suffix = self.archive_split(archive)
+
+        write_sample = {
+            '.tar'      : self.write_sample_tarfile,
+            '.tar.gz'   : self.write_sample_tarfile,
+            '.tgz'      : self.write_sample_tarfile,
+            '.zip'      : self.write_sample_zipfile,
+        }.get(suffix, self.write_sample_directory)
+
+        if not dir:
+            dir = base
+
+        os.mkdir(dir)
+        path = write_sample(archive, dir, self.sample_project_files)
+
+        return path
diff --git a/QMTest/TestSConsign.py b/QMTest/TestSConsign.py
new file mode 100644 (file)
index 0000000..d144040
--- /dev/null
@@ -0,0 +1,74 @@
+# __COPYRIGHT__
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+__doc__ = """
+TestSConsign.py:  a testing framework for the "sconsign" script 
+tool.
+
+A TestSConsign environment object is created via the usual invocation:
+
+    test = TestSConsign()
+
+TestSconsign is a subclass of TestSCons, which is a subclass of
+TestCommon, which is in turn is a subclass of TestCmd), and hence
+has available all of the methods and attributes from those classes,
+as well as any overridden or additional methods or attributes defined
+in this subclass.
+"""
+
+import os
+import os.path
+import string
+import sys
+
+from TestSCons import *
+from TestSCons import __all__
+
+__all__.extend([ 'TestSConsign', ])
+
+class TestSConsign(TestSCons):
+    """Class for testing the sconsign.py script.
+
+    This provides a common place for initializing sconsign tests,
+    eliminating the need to begin every test with the same repeated
+    initializations.
+
+    This adds additional methods for running the sconsign script
+    without changing the basic ability of the run() method to run
+    "scons" itself, since we need to run scons to generate the
+    .sconsign files that we want the sconsign script to read.
+    """
+    def __init__(self, *args, **kw):
+        try:
+            script_dir = os.environ['SCONS_SCRIPT_DIR']
+        except KeyError:
+            pass
+        else:
+            os.chdir(script_dir)
+        self.script_dir = os.getcwd()
+
+        apply(TestSCons.__init__, (self,)+args, kw)
+
+        self.my_kw = {
+            'interpreter' : python,     # imported from TestSCons
+        }
+
+        if os.path.exists(self.script_path('sconsign.py')):
+            sconsign = 'sconsign.py'
+        elif os.path.exists(self.script_path('sconsign')):
+            sconsign = 'sconsign'
+        else:
+            print "Can find neither 'sconsign.py' nor 'sconsign' scripts."
+            self.no_result()
+        self.set_sconsign(sconsign)
+
+    def script_path(self, script):
+        return os.path.join(self.script_dir, script)
+
+    def set_sconsign(self, sconsign):
+        self.my_kw['program'] = sconsign
+
+    def run_sconsign(self, *args, **kw):
+        kw.update(self.my_kw)
+        return apply(self.run, args, kw)
diff --git a/README b/README
index c0c013e0b92089adace175dbf5c42e26aafb76ff..7d5c84b5252dd4c611ba3f7cfa346b61e089aa01 100644 (file)
--- a/README
+++ b/README
@@ -83,12 +83,12 @@ In this case, your options are:
     --  (Optional.)  Install from a pre-packaged SCons package that
         does not require distutils:
 
-            Red Hat Linux       scons-0.96.92.noarch.rpm
+            Red Hat Linux       scons-0.96.93.noarch.rpm
 
-            Debian GNU/Linux    scons_0.96.92_all.deb
+            Debian GNU/Linux    scons_0.96.93_all.deb
                                 (or use apt-get)
 
-            Windows             scons-0.96.92.win32.exe
+            Windows             scons-0.96.93.win32.exe
 
     --  (Recommended.)  Download the latest distutils package from the
         following URL:
@@ -152,7 +152,7 @@ And on Windows:
 
 By default, the above commands will do the following:
 
-    --  Install the version-numbered "scons-0.96.92" and "sconsign-0.96.92"
+    --  Install the version-numbered "scons-0.96.93" and "sconsign-0.96.93"
         scripts in the default system script directory (/usr/bin or
         C:\Python*\Scripts, for example).  This can be disabled by
         specifying the "--no-version-script" option on the command
@@ -165,15 +165,15 @@ By default, the above commands will do the following:
         if you want to install and experiment with a new version before
         making it the default on your system.  On UNIX or Linux systems,
         you can have the "scons" and "sconsign" scripts be hard links or
-        symbolic links to the "scons-0.96.92" and "sconsign-0.96.92" scripts
+        symbolic links to the "scons-0.96.93" and "sconsign-0.96.93" scripts
         by specifying the "--hardlink-scons" or "--symlink-scons"
         options on the command line.
 
-    --  Install "scons-0.96.92.bat" and "scons.bat" wrapper scripts in the
+    --  Install "scons-0.96.93.bat" and "scons.bat" wrapper scripts in the
         Python prefix directory on Windows (C:\Python*, for example).
         This can be disabled by specifying the "--no-install-bat" option
         on the command line.  On UNIX or Linux systems, the
-        "--install-bat" option may be specified to have "scons-0.96.92.bat"
+        "--install-bat" option may be specified to have "scons-0.96.93.bat"
         and "scons.bat" files installed in the default system script
         directory, which is useful if you want to install SCons in a
         shared file system directory that can be used to execute SCons
@@ -181,7 +181,7 @@ By default, the above commands will do the following:
 
     --  Install the SCons build engine (a Python module) in an
         appropriate version-numbered SCons library directory
-        (/usr/lib/scons-0.96.92 or C:\Python*\scons-0.96.92, for example).
+        (/usr/lib/scons-0.96.93 or C:\Python*\scons-0.96.93, for example).
         See below for more options related to installing the build
         engine library.
 
@@ -484,13 +484,13 @@ tests instead of running all of "runtest.py -a".
 BUILDING PACKAGES
 =================
 
-We use SCons (version 0.96 or later) to build its own packages.  If you
+We use SCons (version 0.96.93 later) to build its own packages.  If you
 already have an appropriate version of SCons installed on your system,
 you can build everything by simply running it:
 
         $ scons
 
-If you don't have SCons version 0.96 or later already installed on your
+If you don't have SCons version 0.96.93 later already installed on your
 system, you can build this version of SCons with itself with a little more
 typing.  On UNIX or Linux (using sh or a derivative like bash or ksh):
 
@@ -505,18 +505,18 @@ On Windows:
 Depending on the utilities installed on your system, any or all of the
 following packages will be built:
 
-        build/dist/scons-0.96.92-1.noarch.rpm
-        build/dist/scons-0.96.92-1.src.rpm
-        build/dist/scons-0.96.92.linux-i686.tar.gz
-        build/dist/scons-0.96.92.tar.gz
-        build/dist/scons-0.96.92.win32.exe
-        build/dist/scons-0.96.92.zip
-        build/dist/scons-doc-0.96.92.tar.gz
-        build/dist/scons-local-0.96.92.tar.gz
-        build/dist/scons-local-0.96.92.zip
-        build/dist/scons-src-0.96.92.tar.gz
-        build/dist/scons-src-0.96.92.zip
-        build/dist/scons_0.96.92-1_all.deb
+        build/dist/scons-0.96.93-1.noarch.rpm
+        build/dist/scons-0.96.93-1.src.rpm
+        build/dist/scons-0.96.93.linux-i686.tar.gz
+        build/dist/scons-0.96.93.tar.gz
+        build/dist/scons-0.96.93.win32.exe
+        build/dist/scons-0.96.93.zip
+        build/dist/scons-doc-0.96.93.tar.gz
+        build/dist/scons-local-0.96.93.tar.gz
+        build/dist/scons-local-0.96.93.zip
+        build/dist/scons-src-0.96.93.tar.gz
+        build/dist/scons-src-0.96.93.zip
+        build/dist/scons_0.96.93-1_all.deb
 
 The SConstruct file is supposed to be smart enough to avoid trying to
 build packages for which you don't have the proper utilities installed.
index 41708d695a899b8e10c0405c279227c44fd9dbe2..811ed4a1786afe7cabd288816528d6d978edb3f8 100644 (file)
@@ -4,7 +4,9 @@
 # See the README file for an overview of how SCons is built and tested.
 #
 
-copyright_years = '2001, 2002, 2003, 2004'
+# When this gets changed, you also need to change test/option-v.py
+# so it looks for the right string.
+copyright_years = '2001, 2002, 2003, 2004, 2005, 2006'
 
 #
 # __COPYRIGHT__
@@ -40,7 +42,7 @@ import sys
 import time
 
 project = 'scons'
-default_version = '0.96.92'
+default_version = '0.96.93'
 copyright = "Copyright (c) %s The SCons Foundation" % copyright_years
 
 Default('.')
@@ -453,6 +455,7 @@ scons_script = {
                             'LICENSE.txt'       : '../LICENSE.txt',
                             'scons'             : 'scons.py',
                             'sconsign'          : 'sconsign.py',
+                            'scons-time'        : 'scons-time.py',
                            },
 
         'buildermap'    : {},
@@ -460,6 +463,7 @@ scons_script = {
         'extra_rpm_files' : [
                             'scons-' + version,
                             'sconsign-' + version,
+                            'scons-time-' + version,
                           ],
 
         'explicit_deps' : {
@@ -490,6 +494,7 @@ scons = {
                             'os_spawnv_fix.diff',
                             'scons.1',
                             'sconsign.1',
+                            'scons-time.1',
                             'script/scons.bat',
                             'setup.cfg',
                             'setup.py',
@@ -498,11 +503,13 @@ scons = {
         'filemap'       : {
                             'scons.1' : '../build/doc/man/scons.1',
                             'sconsign.1' : '../build/doc/man/sconsign.1',
+                            'scons-time.1' : '../build/doc/man/scons-time.1',
                           },
 
         'buildermap'    : {
                             'scons.1' : env.SOElim,
                             'sconsign.1' : env.SOElim,
+                            'scons-time.1' : env.SOElim,
                           },
 
         'subpkgs'       : [ python_scons, scons_script ],
index 3dcc2870aad2564b6337aeab67c3d086b19e99e8..47c8073fc641450e530ee8525052dbce7199807a 100755 (executable)
@@ -130,7 +130,7 @@ ${EXECUTE} cd $module
 # and so will not be readily compilable.
 #
 # gunzip < $baseline/export/${project}.tar.gz | tardy -rp ${project} | tar xf -
-aetar -send -o - | tar xzf -
+aetar -send -comp-alg=gzip -o - | tar xzf -
 
 #
 # If any new directories have been created we will need to add them
@@ -145,9 +145,9 @@ then
     xargs --max-args=1 |
     while read dir
     do
-        if [ ! -d $dir/CVS ]
+        if [ ! -d "$dir/CVS" ]
         then
-           Command cvs add $dir
+           Command cvs add "$dir"
         fi
     done
 fi
@@ -176,10 +176,24 @@ do
     esac
 done
 
+#
+# Extract the brief description.  We'd like to do this using aesub
+# or something, like so:
+#
+#      message=`aesub '${version} - ${change description}'`
+#
+# but the expansion of ${change description} has a lame hard-coded max of
+# 80 characters, so we have to do this by hand.  (This has the slight
+# benefit of preserving backslashes in front of any double-quotes in
+# the text; that will have to be handled if we go back to using aesub.)
+#
+description=`aegis -ca -l | sed -n 's/brief_description = "\(.*\)";$/\1/p'`
+version=`aesub '${version}'`
+message="$version - $description"
+
 #
 # Now commit all the changes.
 #
-message=`aesub '${version} - ${change description}'`
 Command cvs -q commit -m \"$message\"
 
 #
index e5b81a4df46d474536eb9461719b01575d784f15..301d890161ba701f190b730c83456ec46f5d5e8a 100755 (executable)
@@ -130,7 +130,7 @@ ${EXECUTE} cd $module
 # and so will not be readily compilable.
 #
 # gunzip < $baseline/export/${project}.tar.gz | tardy -rp ${project} | tar xf -
-aetar -send -o - | tar xzf -
+aetar -send -comp-alg=gzip -o - | tar xzf -
 
 #
 # If any new directories have been created we will need to add them
@@ -145,9 +145,9 @@ then
     xargs --max-args=1 |
     while read dir
     do
-        if [ ! -d $dir/.svn ]
+        if [ ! -d "$dir/.svn" ]
         then
-            Command svn add -N $dir
+            Command svn add -N "$dir"
         fi
     done
 fi
@@ -212,10 +212,24 @@ do
     esac
 done
 
+#
+# Extract the brief description.  We'd like to do this using aesub
+# or something, like so:
+#
+#      message=`aesub '${version} - ${change description}'`
+#
+# but the expansion of ${change description} has a lame hard-coded max of
+# 80 characters, so we have to do this by hand.  (This has the slight
+# benefit of preserving backslashes in front of any double-quotes in
+# the text; that will have to be handled if we go back to using aesub.)
+#
+description=`aegis -ca -l | sed -n 's/brief_description = "\(.*\)";$/\1/p'`
+version=`aesub '${version}'`
+message="$version - $description"
+
 #
 # Now commit all the changes.
 #
-message=`aesub '${version} - ${change description}'`
 Command svn commit -m \"$message\"
 
 #
index f205e257fecdbb88aac48a906542a75bb57421d3..dc5945b464ce1f525568e7f2453e33f8dc39a1da 100644 (file)
@@ -1,8 +1,8 @@
-scons (0.96-92) unstable; urgency=low
+scons (0.96-93) unstable; urgency=low
 
   * Pre-release of eighth beta release.
 
- -- Steven Knight <knight@baldmt.com>  Mon, 10 Apr 2006 21:08:22 -0400
+ -- Steven Knight <knight@baldmt.com>  Mon, 06 Nov 2006 00:44:11 -0600
 
 
 scons (0.96-1) unstable; urgency=low
index b0a4ba11d12a7d6f152cbd950e7d72be3eb692bd..43d34036ad0c5ad8508f345f21faa451ba0d5e9a 100644 (file)
@@ -376,7 +376,7 @@ THIS IS AN AUTOMATICALLY-GENERATED FILE.  DO NOT EDIT.
 #
 # Man page(s), in good ol' troff format.
 #
-man_page_list = ['scons.1', 'sconsign.1']
+man_page_list = ['scons.1', 'sconsign.1', 'scons-time.1']
 
 for m in man_page_list:
     orig_env.SCons_revision(os.path.join(build, 'man', m),
diff --git a/doc/man/scons-time.1 b/doc/man/scons-time.1
new file mode 100644 (file)
index 0000000..50f490f
--- /dev/null
@@ -0,0 +1,1005 @@
+.\" __COPYRIGHT__
+.\"
+.\" Permission is hereby granted, free of charge, to any person obtaining
+.\" a copy of this software and associated documentation files (the
+.\" "Software"), to deal in the Software without restriction, including
+.\" without limitation the rights to use, copy, modify, merge, publish,
+.\" distribute, sublicense, and/or sell copies of the Software, and to
+.\" permit persons to whom the Software is furnished to do so, subject to
+.\" the following conditions:
+.\"
+.\" The above copyright notice and this permission notice shall be included
+.\" in all copies or substantial portions of the Software.
+.\"
+.\" THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+.\" KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+.\" WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+.\" NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+.\" LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+.\" OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+.\" WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+.\"
+.\" doc/man/scons-time.1 0.96.C629 2006/11/18 11:50:43 knight
+.\"
+.\" ES - Example Start - indents and turns off line fill
+.de ES
+.RS
+.nf
+..
+.\" EE - Example End - ends indent and turns line fill back on
+.de EE
+.RE
+.fi
+..
+'\"==========================================================================
+.de SF
+.B scons-time func
+[\fB-h\fR]
+[\fB--chdir=\fIDIR\fR]
+[\fB-f \fIFILE\fR]
+[\fB--fmt=\fIFORMAT\fR]
+[\fB--func=\fINAME\fR]
+[\fB-p \fISTRING\fR]
+[\fB-t \fINUMBER\fR]
+[\fB--title= TITLE\fR]
+[\fIARGUMENTS\fR]
+..
+'\"--------------------------------------------------------------------------
+.de SY
+.B scons-time mem
+[\fB-h\fR]
+[\fB--chdir=\fIDIR\fR]
+[\fB-f \fIFILE\fR]
+[\fB--fmt=\fIFORMAT\fR]
+[\fB-p \fISTRING\fR]
+[\fB--stage=\fISTAGE\fR]
+[\fB-t \fINUMBER\fR]
+[\fB--title=\fITITLE\fR]
+[\fIARGUMENTS\fR]
+..
+'\"--------------------------------------------------------------------------
+.de SO
+.B scons-time obj
+[\fB-h\fR]
+[\fB--chdir=\fIDIR\fR]
+[\fB-f \fIFILE\fR]
+[\fB--fmt=\fIFORMAT\fR]
+[\fB-p \fISTRING\fR]
+[\fB--stage=\fISTAGE\fR]
+[\fB-t \fINUMBER\fR]
+[\fB--title=\fITITLE\fR]
+[\fIARGUMENTS\fR]
+..
+'\"--------------------------------------------------------------------------
+.de SR
+.B scons-time run
+[\fB-hnqv\fR]
+[\fB--aegis=\fIPROJECT\fR]
+[\fB-f \fIFILE\fR]
+[\fB--number=\fINUMBER\fR]
+[\fB--outdir=\fIOUTDIR\fR]
+[\fB-p \fISTRING\fR]
+[\fB--python=\fIPYTHON\fR]
+[\fB-s \fIDIR\fR]
+[\fB--scons=\fISCONS\fR]
+[\fB--svn=\fIURL\fR]
+[\fIARGUMENTS\fR]
+..
+'\"--------------------------------------------------------------------------
+.de ST
+.B scons-time time
+[\fB-h\fR]
+[\fB--chdir=\fIDIR\fR]
+[\fB-f \fIFILE\fR]
+[\fB--fmt=\fIFORMAT\fR]
+[\fB-p \fISTRING\fR]
+[\fB-t \fINUMBER\fR]
+[\fB--title=\fITITLE\fR]
+[\fB--which=\fIWHICH\fR]
+[\fIARGUMENTS\fR]
+..
+.TH SCONS-TIME 1 "November 2006"
+.SH NAME
+scons-time \- generate and display SCons timing information
+'\"==========================================================================
+.SH SYNOPSIS
+.B scons-time
+.IR subcommand
+[
+.IR options ...
+]
+[
+.IR arguments ...
+]
+'\"--------------------------------------------------------------------------
+.SS "Generating Timing Information"
+.SR
+'\"--------------------------------------------------------------------------
+.SS "Extracting Function Timings"
+.SF
+'\"--------------------------------------------------------------------------
+.SS "Extracting Memory Statistics"
+.SY
+'\"--------------------------------------------------------------------------
+.SS "Extracting Object Counts"
+.SO
+'\"--------------------------------------------------------------------------
+.SS "Extracting Execution Times"
+.ST
+'\"--------------------------------------------------------------------------
+.SS "Help Text"
+.B scons-time help
+.I SUBCOMMAND
+[...]
+'\"==========================================================================
+.SH DESCRIPTION
+The 
+.B scons-time
+command runs an SCons configuration
+through a standard set of profiled timings
+and can extract and graph information from the
+resulting profiles and log files of those timings.
+The action to be performed by the
+.B scons-time
+script is specified
+by a subcommand, the first argument on the command line.
+See the
+.B SUBCOMMANDS
+section below for information about the operation
+of specific subcommands.
+.P
+The basic way to use
+.B scons-time
+is to run the
+.B scons-time run
+subcommand
+(possibly multiple times)
+to generate profile and log file output,
+and then use one of the other
+subcommands to display the results
+captured in the profiles and log files
+for a particular kind of information:
+function timings
+(the
+.B scons-time func
+subcommand),
+total memory used
+(the
+.B scons-time mem
+subcommand),
+object counts
+(the
+.B scons-time obj
+subcommand)
+and overall execution time
+(the
+.B scons-time time
+subcommand).
+Options exist to place and find the
+profiles and log files in separate directories,
+to generate the output in a format suitable
+for graphing with the
+.BR gnuplot (1)
+program,
+and so on.
+.P
+There are two basic ways the
+.B scons-time run
+subcommand
+is intended to be used
+to gather timing statistics
+for a configuration.
+One is to use the
+.B --svn=
+option to test a configuration against
+a list of revisions from the SCons Subversion repository.
+This will generate a profile and timing log file
+for every revision listed with the
+.B --number=
+option,
+and can be used to look at the
+impact of commited changes to the
+SCons code base on a particular
+configuration over time.
+.P
+The other way is to profile incremental changes to a
+local SCons code base during a development cycle--that is,
+to look at the performance impact of changes
+you're making in the local tree.
+In this mode,
+you run the
+.B scons-time run
+subcommand
+.I without
+the
+.B --svn=
+option,
+in which case it simply looks in the profile/log file output directory
+(the current directory by default)
+and automatically figures out the
+.I next
+run number for the output profile and log file.
+Used in this way,
+the development cycle goes something like:
+make a change to SCons;
+run
+.B scons-time run
+to profile it against a specific configuration;
+make another change to SCons;
+run
+.B scons-time run
+again to profile it;
+etc.
+'\"==========================================================================
+.SH OPTIONS
+The
+.B scons-time
+command only supports a few global options:
+.TP
+-h, --help
+Displays the global help text and exits,
+identical to the
+.B scons-time help
+subcommand.
+.TP
+-V, --version
+Displays the
+.B scons-time
+version and exits.
+.P
+Most functionality is controlled by options
+to the individual subcommands.
+See the next section for information
+about individual subcommand options.
+'\"==========================================================================
+.SH SUBCOMMANDS
+The
+.B scons-time
+command supports the following
+individual subcommands.
+'\"--------------------------------------------------------------------------
+.SS "The func Subcommand"
+.SF
+.P
+The
+.B scons-time func
+subcommand displays timing information
+for a specific Python function within SCons.
+By default, it extracts information about the
+.BR _main ()
+function,
+which includes the Python profiler timing
+for all of SCons.
+.P
+The
+.B scons-time func
+subcommand extracts function timing information
+from all the specified file arguments,
+which should be Python profiler output files.
+(Normally, these would be
+.B *.prof
+files generated by the
+.B scons-time run
+subcommand,
+but they can actually be generated
+by any Python profiler invocation.)
+All file name arguments will be
+globbed for on-disk files.
+.P
+If no arguments are specified,
+then function timing information
+will be extracted from all
+.B *.prof
+files,
+or the subset of them
+with a prefix specified by the
+.B -p
+option.
+.P
+Options include:
+.TP
+-C DIRECTORY, --chdir=DIRECTORY
+Changes to the specified
+.I DIRECTORY
+before looking for the specified files
+(or files that match the specified patterns).
+.TP
+-f FILE, --file=FILE
+Reads configuration information from the specified
+.IR FILE .
+.TP
+-fmt=FORMAT, --format=FORMAT
+Reports the output in the specified
+.IR FORMAT .
+The formats currently supported are
+.B ascii
+(the default)
+and
+.BR gnuplot .
+.TP
+--func=NAME
+Extracts timings for the specified function
+.IR NAME .
+The default is to report cumulative timings for the
+.BR _main ()
+function,
+which contains the entire SCons run.
+.TP
+-h, --help
+Displays help text for the
+.B scons-time func
+subcommand.
+.TP
+-p STRING, --prefix=STRING
+Specifies the prefix string for profiles
+from which to extract function timing information.
+This will be used to search for profiles
+if no arguments are specified on the command line.
+.TP
+-t NUMBER, --tail=NUMBER
+Only extracts function timings from the last
+.I NUMBER
+files.
+'\"--------------------------------------------------------------------------
+.SS "The help Subcommand"
+.B scons-time help
+.I SUBCOMMAND
+[...]
+The
+.B help
+subcommand prints help text for any
+other subcommands listed as later arguments on the command line.
+'\"--------------------------------------------------------------------------
+.SS "The mem Subcommand"
+.SY
+.P
+The
+.B scons-time mem
+subcommand displays how much memory SCons uses.
+.P
+The
+.B scons-time mem
+subcommand extracts memory use information
+from all the specified file arguments,
+which should be files containing output from
+running SCons with the
+.B --debug=memory
+option.
+(Normally, these would be
+.B *.log
+files generated by the
+.B scons-time run
+subcommand.)
+All file name arguments will be
+globbed for on-disk files.
+.P
+If no arguments are specified,
+then memory information
+will be extracted from all
+.B *.log
+files,
+or the subset of them
+with a prefix specified by the
+.B -p
+option.
+.P
+.TP
+-C DIR, --chdir=DIR
+Changes to the specified
+.I DIRECTORY
+before looking for the specified files
+(or files that match the specified patterns).
+.TP
+-f FILE, --file=FILE
+Reads configuration information from the specified
+.IR FILE .
+.TP
+-fmt=FORMAT, --format=FORMAT
+Reports the output in the specified
+.IR FORMAT .
+The formats currently supported are
+.B ascii
+(the default)
+and
+.BR gnuplot .
+.TP
+-h, --help
+Displays help text for the
+.B scons-time mem
+subcommand.
+.TP
+-p STRING, --prefix=STRING
+Specifies the prefix string for log files
+from which to extract memory usage information.
+This will be used to search for log files
+if no arguments are specified on the command line.
+.TP
+--stage=STAGE
+Prints the memory used at the end of the specified
+.IR STAGE :
+.B pre-read
+(before the SConscript files are read),
+.B post-read ,
+(after the SConscript files are read),
+.B pre-build
+(before any targets are built)
+or
+.B post-build
+(after any targets are built).
+If no
+.B --stage
+option is specified,
+the default behavior is
+.BR post-build ,
+which reports the final amount of memory
+used by SCons during each run.
+.TP
+-t NUMBER, --tail=NUMBER
+Only reports memory statistics from the last
+.I NUMBER
+files.
+'\"--------------------------------------------------------------------------
+.SS "The obj Subcommand"
+.SO
+.P
+The
+.B scons-time obj
+subcommand displays how many objects of a specific named type
+are created by SCons.
+.P
+The
+.B scons-time obj
+subcommand extracts object counts
+from all the specified file arguments,
+which should be files containing output from
+running SCons with the
+.B --debug=count
+option.
+(Normally, these would be
+.B *.log
+files generated by the
+.B scons-time run
+subcommand.)
+All file name arguments will be
+globbed for on-disk files.
+.P
+If no arguments are specified,
+then object counts
+will be extracted from all
+.B *.log
+files,
+or the subset of them
+with a prefix specified by the
+.B -p
+option.
+.TP
+-C DIR, --chdir=DIR
+Changes to the specified
+.I DIRECTORY
+before looking for the specified files
+(or files that match the specified patterns).
+.TP
+-f FILE, --file=FILE
+Reads configuration information from the specified
+.IR FILE .
+.TP
+-fmt=FORMAT, --format=FORMAT
+Reports the output in the specified
+.IR FORMAT .
+The formats currently supported are
+.B ascii
+(the default)
+and
+.BR gnuplot .
+.TP
+-h, --help
+Displays help text for the
+.B scons-time obj
+subcommand.
+.TP
+-p STRING, --prefix=STRING
+Specifies the prefix string for log files
+from which to extract object counts.
+This will be used to search for log files
+if no arguments are specified on the command line.
+.TP
+--stage=STAGE
+Prints the object count at the end of the specified
+.IR STAGE :
+.B pre-read
+(before the SConscript files are read),
+.B post-read ,
+(after the SConscript files are read),
+.B pre-build
+(before any targets are built)
+or
+.B post-build
+(after any targets are built).
+If no
+.B --stage
+option is specified,
+the default behavior is
+.BR post-build ,
+which reports the final object count during each run.
+.TP
+-t NUMBER, --tail=NUMBER
+Only reports object counts from the last
+.I NUMBER
+files.
+'\"--------------------------------------------------------------------------
+.SS "The run Subcommand"
+.SR
+The
+.B scons-time run
+subcommand is the basic subcommand
+for profiling a specific configuration
+against a version of SCons.
+.P
+The configuration to be tested
+is specified as a list of files
+or directories that will be unpacked or copied
+into a temporary directory
+in which SCons will be invoked.
+The
+.B scons-time run
+subcommand understands file suffixes like
+.BR .tar ,
+.BR .tar.gz ,
+.BR .tgz
+and
+.BR .zip
+and will unpack their contents into a temporary directory.
+If more than one argument is specified,
+each one will be unpacked or copied
+into the temporary directory "on top of"
+the previous archives or directories,
+so the expectation is that multiple
+specified archives share the same directory layout.
+.P
+Once the file or directory arguments are unpacked or
+copied to the temporary directory,
+the
+.B scons-time run
+subcommand runs the
+requested version of SCons
+against the configuration
+three times:
+.TP
+Startup
+SCons is run with the
+.B --help
+option so that just the SConscript files are read,
+and then the default help text is printed.
+This profiles just the perceived "overhead" of starting up SCons
+and processing the SConscript files.
+.TP
+Full build
+SCons is run to build everything specified in the configuration.
+Specific targets to be passed in on the command l ine
+may be specified by the
+.B targets
+keyword in a configuration file; see below for details.
+.TP
+Rebuild
+SCons is run again on the same just-built directory.
+If the dependencies in the SCons configuration are correct,
+this should be an up-to-date, "do nothing" rebuild.
+.P
+Each invocation captures the output log file and a profile.
+.P
+The
+.B scons-time run
+subcommand supports the following options:
+.TP
+--aegis=PROJECT
+Specifies the Aegis
+.I PROJECT
+from which the
+version(s) of
+.B scons
+being timed will be extracted.
+When
+.B --aegis
+is specified, the
+.BI --number= NUMBER
+option specifies delta numbers
+that will be tested.
+Output from each invocation run will be placed in file
+names that match the Aegis delta numbers.
+If the
+.B --number=
+option is not specified,
+then the default behavior is to time the
+tip of the specified
+.IR PROJECT .
+.TP
+-f FILE, --file=FILE
+Reads configuration information from the specified
+.IR FILE .
+This often provides a more convenient way to specify and
+collect parameters associated with a specific timing configuration
+than specifying them on the command line.
+See the
+.B CONFIGURATION FILE
+section below
+for information about the configuration file parameters.
+.TP
+-h, --help
+Displays help text for the
+.B scons-time run
+subcommand.
+.TP
+-n, --no-exec
+Do not execute commands,
+just printing the command-line equivalents of what would be executed.
+Note that the
+.B scons-time
+script actually executes its actions in Python,
+where possible,
+for portability.
+The commands displayed are UNIX
+.I equivalents
+of what it's doing.
+.TP
+--number=NUMBER
+Specifies the run number to be used in the names of
+the log files and profile outputs generated by this run.
+.IP
+When used in conjuction with the
+.BI --aegis= PROJECT
+option,
+.I NUMBER
+specifies one or more comma-separated Aegis delta numbers
+that will be retrieved automatically from the specified Aegis
+.IR PROJECT .
+.IP
+When used in conjuction with the
+.BI --svn= URL
+option,
+.I NUMBER
+specifies one or more comma-separated Subversion revision numbers
+that will be retrieved automatically from the Subversion
+repository at the specified
+.IR URL .
+Ranges of delta or revision numbers
+may be specified be separating two numbers
+with a hyphen
+.RB ( \- ).
+.P
+Example:
+.ES
+% scons-time run --svn=http://scons.tigris.org/svn/trunk --num=1247,1249-1252 .
+.EE
+.TP
+-p STRING, --prefix=STRING
+Specifies the prefix string to be used for all of the log files
+and profiles generated by this run.
+The default is derived from the first
+specified argument:
+if the first argument is a directory,
+the default prefix is the name of the directory;
+if the first argument is an archive
+(tar or zip file),
+the default prefix is the the base name of the archive,
+that is, what remains after stripping the archive suffix
+.RB ( .tgz ", " .tar.gz " or " .zip ).
+.TP
+--python=PYTHON
+Specifies a path to the Python executable to be used
+for the timing runs.
+The default is to use the same Python executable that
+is running the
+.B scons-time
+command itself.
+.TP
+-q, --quiet
+Suppresses display of the command lines being executed.
+.TP
+-s DIR, --subdir=DIR
+Specifies the name of directory or subdirectory
+from which the commands should be executed.
+The default is XXX
+.TP
+--scons=SCONS
+Specifies a path to the SCons script to be used
+for the timing runs.
+The default is XXX
+.TP
+--svn=URL, --subversion=URL
+Specifies the
+.I URL
+of the Subversion repository from which the
+version(s) of
+.B scons
+being timed will be extracted.
+When
+.B --svn
+is specified, the
+.BI --number= NUMBER
+option specifies revision numbers
+that will be tested.
+Output from each invocation run will be placed in file
+names that match the Subversion revision numbers.
+If the
+.B --number=
+option is not specified,
+then the default behavior is to time the
+.B HEAD
+of the specified
+.IR URL .
+.TP
+-v, --verbose
+Displays the output from individual commands to the screen
+(in addition to capturing the output in log files).
+'\"--------------------------------------------------------------------------
+.SS "The time Subcommand"
+.ST
+.P
+The
+.B scons-time time
+subcommand displays SCons execution times
+as reported by the
+.B scons --debug=time
+option.
+.P
+The
+.B scons-time time
+subcommand extracts SCons timing
+from all the specified file arguments,
+which should be files containing output from
+running SCons with the
+.B --debug=time
+option.
+(Normally, these would be
+.B *.log
+files generated by the
+.B scons-time run
+subcommand.)
+All file name arguments will be
+globbed for on-disk files.
+.P
+If no arguments are specified,
+then execution timings
+will be extracted from all
+.B *.log
+files,
+or the subset of them
+with a prefix specified by the
+.B -p
+option.
+.TP
+-C DIR, --chdir=DIR
+Changes to the specified
+.I DIRECTORY
+before looking for the specified files
+(or files that match the specified patterns).
+.TP
+-f FILE, --file=FILE
+Reads configuration information from the specified
+.IR FILE .
+.TP
+-fmt=FORMAT, --format=FORMAT
+Reports the output in the specified
+.IR FORMAT .
+The formats currently supported are
+.B ascii
+(the default)
+and
+.BR gnuplot .
+.TP
+-h, --help
+Displays help text for the
+.B scons-time time
+subcommand.
+.TP
+-p STRING, --prefix=STRING
+Specifies the prefix string for log files
+from which to extract execution timings.
+This will be used to search for log files
+if no arguments are specified on the command line.
+.TP
+-t NUMBER, --tail=NUMBER
+Only reports object counts from the last
+.I NUMBER
+files.
+.TP
+--which=WHICH
+Prints the execution time for the specified
+.IR WHICH
+value:
+.B total
+(the total execution time),
+.B SConscripts
+(total execution time for the SConscript files themselves),
+.B SCons
+(exectuion time in SCons code itself)
+or
+.B commands
+(execution time of the commands and other actions
+used to build targets).
+If no
+.B --which
+option is specified,
+the default behavior is
+.BR total ,
+which reports the total execution time for each run.
+'\"==========================================================================
+.SH CONFIGURATION FILE
+Various
+.B scons-time
+subcommands can read information from a specified
+configuration file when passed the
+.B \-f
+or
+.B \--file
+options.
+The configuration file is actually executed as a Python script.
+Setting Python variables in the configuration file
+controls the behavior of the
+.B scons-time
+script more conveniently than having to specify
+command-line options or arguments for every run,
+and provides a handy way to "shrink-wrap"
+the necessary information for producing (and reporting)
+consistent timing runs for a given configuration.
+.TP
+.B aegis
+The Aegis executable for extracting deltas.
+The default is simply
+.BR aegis .
+.TP
+.B aegis_project
+The Aegis project from which deltas should be extracted.
+The default is whatever is specified
+with the
+.B --aegis=
+command-line option.
+.TP
+.B initial_commands
+A list of commands that will be executed
+before the actual timed
+.B scons
+runs.
+This can be used for commands that are necessary
+to prepare the source tree\-for example,
+creating a configuration file
+that should not be part of the timed run.
+.TP
+.B key_location
+The location of the key on Gnuplot graphing information
+generated with the
+.BR --format=gnuplot
+option.
+The default is
+.BR "bottom left" .
+.TP
+.B prefix
+The file name prefix to be used when
+running or extracting timing for this configuration.
+.TP
+.B python
+The path name of the Python executable
+to be used when running or extracting information
+for this configuration.
+The default is the same version of Python
+used to run the SCons
+.TP
+.B scons
+The path name of the SCons script to be used
+when running or extracting information
+for this configuration.
+The default is simply
+.BR scons .
+.TP
+.B scons_flags
+The
+.B scons
+flags used when running SCons to collect timing information.
+The default value is
+.BR "--debug=count --debug=memory --debug=time --debug=memoizer" .
+.TP
+.B scons_lib_dir
+.TP
+.B scons_wrapper
+.TP
+.B startup_targets
+.TP
+.B subdir
+The subdirectory of the project into which the
+.B scons-time
+script should change
+before executing the SCons commands to time.
+.TP
+.B subversion_url
+The Subversion URL from
+.TP
+.B svn
+The subversion executable used to
+check out revisions of SCons to be timed.
+The default is simple
+.BR svn .
+.TP
+.B svn_co_flag
+.TP
+.B tar
+.TP
+.B targets
+A string containing the targets that should be added to
+the command line of every timed
+.B scons
+run.
+This can be used to restrict what's being timed to a
+subset of the full build for the configuration.
+.TP
+.B targets0
+.TP
+.B targets1
+.TP
+.B targets2
+.TP
+.B title
+.TP
+.B unzip
+.TP
+.B verbose
+.TP
+.B vertical_bars
+'\"--------------------------------------------------------------------------
+.SS Example
+Here is an example
+.B scons-time
+configuration file
+for a hypothetical sample project:
+.P
+.ES
+# The project doesn't use SCons natively (yet), so we're
+# timing a separate set of SConscript files that we lay
+# on top of the vanilla unpacked project tarball.
+arguments = ['project-1.2.tgz', 'project-SConscripts.tar']
+
+# The subdirectory name contains the project version number,
+# so tell scons-time to chdir there before building.
+subdir = 'project-1.2'
+
+# Set the prefix so output log files and profiles are named:
+#     project-000-[012].{log,prof}
+#     project-001-[012].{log,prof}
+# etc.
+prefix = 'project'
+
+# The SConscript files being tested don't do any SConf
+# configuration, so run their normal ./configure script
+# before we invoke SCons.
+initial_commands = [
+    './configure',
+]
+
+# Only time building the bin/project executable.
+targets = 'bin/project'
+
+# Time against SCons revisions of the branches/core branch
+subversion_url = 'http://scons.tigris.org/svn/scons/branches/core'
+.EE
+'\"==========================================================================
+.SH ENVIRONMENT
+The
+.B
+scons-time
+script uses the following environment variables:
+.TP
+.B PRESERVE
+If this value is set,
+the
+.B scons-time
+script will
+.I not
+remove the temporary directory or directories
+in which it builds the specified configuration
+or downloads a specific version of SCons.
+'\"==========================================================================
+.SH "SEE ALSO"
+.BR gnuplot (1),
+.BR scons (1)
+
+.SH AUTHORS
+Steven Knight <knight at baldmt dot com>
index 1b30e6ac8ad90dbf8294ddc7e72306f4c359dd73..d45333b1311fffcaf608904ff5d7a86a5c734cc4 100644 (file)
@@ -31,7 +31,7 @@
 .fi
 .RE
 ..
-.TH SCONS 1 "December 2005"
+.TH SCONS 1 "December 2006"
 .SH NAME
 scons \- a software construction tool
 .SH SYNOPSIS
@@ -602,10 +602,11 @@ $ scons --debug=includes foo.o
 
 .TP
 --debug=memoizer
-Prints a summary of hits and misses in the Memoizer,
-the internal SCons subsystem for caching
-various values in memory instead of
-recomputing them each time they're needed.
+Prints a summary of hits and misses using the Memoizer,
+an internal subsystem that counts
+how often SCons uses cached values in memory
+instead of recomputing them each time they're needed.
+Only available when using Python 2.2 or later.
 
 .TP
 --debug=memory
@@ -615,19 +616,7 @@ and before and after building targets.
 
 .TP
 --debug=nomemoizer
-Disables use of the Memoizer,
-the internal SCons subsystem for caching
-various values in memory instead of
-recomputing them each time they're needed.
-This provides more accurate counts of the
-underlying function calls in the 
-Python profiler output when using the
-.RI --profile=
-option.
-(When the Memoizer is used,
-the profiler counts all
-memoized functions as being executed
-by the Memoizer's wrapper calls.)
+A deprecated option preserved for backwards compatibility.
 
 .TP
 --debug=objects
@@ -1382,6 +1371,28 @@ env.Program(source = 'bar.c')
 env.Program('bar.c')
 .EE
 
+As a convenience, a
+.B srcdir
+keyword argument may be specified
+when calling a Builder.
+When specified,
+all source file strings that are not absolute paths
+will be interpreted relative to the specified
+.BR srcdir .
+The following example will build the
+.B build/prog
+(or
+.B build/prog.exe
+on Windows)
+program from the files
+.B src/f1.c
+and
+.BR src/f2.c :
+
+.ES
+env.Program('build/prog', ['f1.c', 'f2.c'], srcdir='src')
+.EE
+
 It is possible to override or add construction variables when calling a
 builder method by passing additional keyword arguments.
 These overridden or added
@@ -1814,6 +1825,41 @@ env.Alias('install', ['/usr/local/man'])
 env.Alias('update', ['file1', 'file2'], "update_database $SOURCES")
 .EE
 
+'\"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
+.TP
+.RI AllowSubstExceptions([ exception ", ...])"
+Specifies the exceptions that will be allowed
+when expanding construction variables.
+By default,
+any construction variable expansions that generate a
+.B NameError
+or
+.BR IndexError
+exception will expand to a
+.B ''
+(a null string) and not cause scons to fail.
+All exceptions not in the specified list
+will generate an error message
+and terminate processing.
+
+If
+.B AllowSubstExceptions
+is called multiple times,
+each call completely overwrites the previous list
+of allowed exceptions.
+Example:
+
+.ES
+# Requires that all construction variable names exist.
+# (You may wish to do this if you want to enforce strictly
+# that all construction variables must be defined before use.)
+AllowSubstExceptions()
+
+# Also allow a string containing a zero-division expansion
+# like '${1 / 0}' to evalute to ''.
+AllowSubstExceptions(IndexError, NameError, ZeroDivisionError)
+.EE
+
 '\"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
 .TP
 .RI AlwaysBuild( target ", ...)"
@@ -4925,7 +4971,7 @@ The recommended way to handle an invalid value is
 to raise an exception (see example below).
 .I converter
 is called to convert the value before putting it in the environment, and
-should take a single argument: value.
+should take either a value, or the value and environment, as parameters.
 The
 .I converter
 must return a value,
index 92330d587278508ef515ae0b0ae392e3e00f37b6..50bae78b575561f6d256665ccbbccc4db0b6ad04 100644 (file)
@@ -1,5 +1,5 @@
 %define name scons
-%define version 0.96.92
+%define version 0.96.93
 %define release 1
 
 Summary: an Open Source software construction tool
@@ -50,3 +50,4 @@ rm -rf $RPM_BUILD_ROOT
 __RPM_FILES__
 %doc %{_mandir}/man1/scons.1*
 %doc %{_mandir}/man1/sconsign.1*
+%doc %{_mandir}/man1/scons-time.1*
index 778825e7cd1031a72649e926618e9e23aa56d705..e7f49370921e86e83ff6e4985ecb6c54efdf04c4 100644 (file)
@@ -12,6 +12,124 @@ RELEASE 0.97 - XXX
 
   From Anonymous:
 
+  - Allow arbitrary white space after a SWIG %module declaration.
+
+  From Jay Kint:
+
+  - Alleviate long command line issues on Windows by executing command
+    lines directly via os.spawnv() if the command line doesn't need
+    shell interpretation (has no pipes, redirection, etc.).
+
+  From Walter Franzini:
+
+  - Exclude additional Debian packaging files from the copyright check.
+
+  From Fawad Halim:
+
+  - Handle the conflict between the impending Python 2.6 'as' keyword
+    and our Tool/as.py module name.
+
+  From Steven Knight:
+
+  - Speed up the Node.FS.Dir.rel_path() method used to generate path names
+    that get put into the .sconsign* file(s).
+
+  - Optimize Node.FS.Base.get_suffix() by computing the suffix once, up
+    front, when we set the Node's name.  (Duh...)
+
+  - Reduce the Memoizer's responsibilities to simply counting hits and
+    misses when the --debug=memoizer option is used, not to actually
+    handling the key calculation and memoization itself.  This speeds
+    up some configurations significantly, and should cause no functional
+    differences.
+
+  - Add a new scons-time script with subcommands for generating
+    consistent timing output from SCons configurations, extracting
+    various information from those timings, and displaying them in
+    different formats.
+
+  - Reduce some unnecessary stat() calls from on-disk entry type checks.
+
+  - Fix SideEffect() when used with -j, which was badly broken in 0.96.93.
+
+  - Propagate TypeError exceptions when evaluating construction variable
+    expansions up the stack, so users can see what's going on.
+
+  - When disambiguating a Node.FS.Entry into a Dir or File, don't look
+    in the on-disk source directory until we've confirmed there's no
+    on-disk entry locally and there *is* one in the srcdir.  This avoids
+    creating a phantom Node that can interfere with dependencies on
+    directory contents.
+
+  - Add an AllowSubstExceptions() function that gives the SConscript
+    files control over what exceptions cause a string to expand to ''
+    vs. terminating processing with an error.
+
+  From Ben Leslie:
+
+  - Fix post-Memoizer value caching misspellings in Node.FS._doLookup().
+
+  From Rob Managan, Dmitry Mikhin and Joel B. Mohler:
+
+  - Handle TeX/LaTeX files in subdirectories by changing directory
+    before invoking TeX/LaTeX.
+
+  - Scan LaTeX files for \bibliography lines.
+
+  - Support multiple file names in a "\bibliography{file1,file2}" string.
+
+  - Handle TeX warnings about undefined citations.
+
+  - Support re-running LaTeX if necessary due to a Table of Contents.
+
+  From Dmitry Mikhin:
+
+  - Return LaTeX if "Rerun to get citations correct" shows up on the next
+    line after the "Warning:" string.
+
+  From Gary Oberbrunner:
+
+  - Add #include lines to fix portability issues in two tests.
+
+  - Eliminate some unnecessary os.path.normpath() calls.
+
+  From Tom Parker:
+
+  - Have the error message print the missing file that Qt can't find.
+
+  From John Pye:
+
+  - Fix env.MergeFlags() appending to construction variable value of None.
+
+  From Steve Robbins:
+
+  - Fix the "sconsign" script when the .sconsign.dblite file is explicitly
+    specified on the command line (and not intuited from the old way of
+    calling it with just ".sconsign").
+
+  From Sohail Somain:
+
+  - Have the mslink.py Tool only look for a 'link' executable on Windows
+    systems.
+
+  From Vaclav Smilauer:
+
+  - Add support for a "srcdir" keyword argument when calling a Builder,
+    which will add a srcdir prefix to all non-relative string sources.
+
+  From Jonathan Ultis:
+
+  - Allow Options converters to take the construction environment as
+    an optional argument.
+
+
+
+RELEASE 0.96.93 - Mon, 06 Nov 2006 00:44:11 -0600
+
+  NOTE:  This is a pre-release of 0.97 for testing purposes.
+
+  From Anonymous:
+
   - Allow Python Value Nodes to be Builder targets.
 
   From Matthias:
@@ -173,7 +291,7 @@ RELEASE 0.97 - XXX
 
   - Initial infrastructure for running SCons tests under QMTest.
 
-  From Sohail Sumani:
+  From Sohail Somani:
 
   - Fix tests that fail due to gcc warnings.
 
index 0c5f5873fa5aaa1f47f566fa9989b7201a2f6d02..e84a07921e06f3bf3a549f0ff4c79df1fab53a9a 100644 (file)
@@ -20,12 +20,26 @@ more effectively, please sign up for the scons-users mailing list at:
 
 
 
-RELEASE 0.96.92 - Mon, 10 Apr 2006 21:08:22 -0400
+RELEASE 0.97 - XXX
 
   This is a pre-release for testing the eighth beta release of SCons.
   Please consult the CHANGES.txt file for a list of specific changes
   since last release.
 
+  Please note the following important changes since release 0.96.93:
+
+    --  THE --debug=memoizer OPTION NOW REQUIRES PYTHON 2.2 OR LATER
+
+        The --debug=memoizer option now prints a warning message and
+        does nothing if SCons is run on a version of Python that does
+        not support metaclasses (earlier than Python 2.2).
+
+    --  THE --debug=nomemoizer OPTION DOES NOTHING AND IS NOW DEPRECATED
+
+        The --debug=nomemoizer no longer does anything and instead
+        now generates a warning message about being deprecated.  The
+        --debug=nomemoizer will be removed completely in a future release.
+
   Please note the following important changes since release 0.96.91:
 
     --  /opt/bin AND /sw/bin ADDED TO DEFAULT EXECUTION PATH VARIABLES
index c762f1cd58973d61123ddf7b506e53a50af1dedd..7be1c168e6333042401f47ef84c40e0c42d99f12 100644 (file)
@@ -26,6 +26,7 @@ SCons/Options/EnumOption.py
 SCons/Options/ListOption.py
 SCons/Options/PackageOption.py
 SCons/Options/PathOption.py
+SCons/PathList.py
 SCons/Platform/__init__.py
 SCons/Platform/aix.py
 SCons/Platform/cygwin.py
index 4576164a082e45aa43a37fc6036e8e8e8458acd1..503dc9f7cb0f2259bf97f90e6f1528af6422c621 100644 (file)
@@ -233,9 +233,6 @@ class ActionBase:
     """Base class for all types of action objects that can be held by
     other objects (Builders, Executors, etc.)  This provides the
     common methods for manipulating and combining those actions."""
-    
-    if SCons.Memoize.use_memoizer:
-        __metaclass__ = SCons.Memoize.Memoized_Metaclass
 
     def __cmp__(self, other):
         return cmp(self.__dict__, other)
@@ -266,15 +263,6 @@ class ActionBase:
         return SCons.Executor.Executor(self, env, overrides,
                                        tlist, slist, executor_kw)
 
-if SCons.Memoize.use_old_memoization():
-    _Base = ActionBase
-    class ActionBase(SCons.Memoize.Memoizer, _Base):
-        "Cache-backed version of ActionBase"
-        def __init__(self, *args, **kw):
-            apply(_Base.__init__, (self,)+args, kw)
-            SCons.Memoize.Memoizer.__init__(self)
-
-
 class _ActionAction(ActionBase):
     """Base class for actions that create output objects."""
     def __init__(self, strfunction=_null, presub=_null, chdir=None, exitstatfunc=None, **kw):
@@ -563,9 +551,6 @@ class CommandGeneratorAction(ActionBase):
 
 class LazyAction(CommandGeneratorAction, CommandAction):
 
-    if SCons.Memoize.use_memoizer:
-        __metaclass__ = SCons.Memoize.Memoized_Metaclass
-
     def __init__(self, var, *args, **kw):
         if __debug__: logInstanceCreation(self, 'Action.LazyAction')
         apply(CommandAction.__init__, (self, '$'+var)+args, kw)
@@ -580,7 +565,6 @@ class LazyAction(CommandGeneratorAction, CommandAction):
         return CommandGeneratorAction
 
     def _generate_cache(self, env):
-        """__cacheable__"""
         c = env.get(self.var, '')
         gen_cmd = apply(Action, (c,)+self.gen_args, self.gen_kw)
         if not gen_cmd:
@@ -599,13 +583,6 @@ class LazyAction(CommandGeneratorAction, CommandAction):
         c = self.get_parent_class(env)
         return c.get_contents(self, target, source, env)
 
-if not SCons.Memoize.has_metaclass:
-    _Base = LazyAction
-    class LazyAction(SCons.Memoize.Memoizer, _Base):
-        def __init__(self, *args, **kw):
-            SCons.Memoize.Memoizer.__init__(self)
-            apply(_Base.__init__, (self,)+args, kw)
-
 
 
 class FunctionAction(_ActionAction):
index 16f1191535456dae1566d690febbb820db73a325..d5f566a325353c2c9b4eae05a31746dc9f6c0acb 100644 (file)
@@ -126,6 +126,7 @@ import SCons.Action
 from SCons.Debug import logInstanceCreation
 from SCons.Errors import InternalError, UserError
 import SCons.Executor
+import SCons.Memoize
 import SCons.Node
 import SCons.Node.FS
 import SCons.Util
@@ -370,6 +371,8 @@ class BuilderBase:
     if SCons.Memoize.use_memoizer:
         __metaclass__ = SCons.Memoize.Memoized_Metaclass
 
+    memoizer_counters = []
+
     def __init__(self,  action = None,
                         prefix = '',
                         suffix = '',
@@ -387,6 +390,7 @@ class BuilderBase:
                         is_explicit = 1,
                         **overrides):
         if __debug__: logInstanceCreation(self, 'Builder.BuilderBase')
+        self._memo = {}
         self.action = action
         self.multi = multi
         if SCons.Util.is_Dict(prefix):
@@ -604,6 +608,16 @@ class BuilderBase:
             ekw = self.executor_kw.copy()
             ekw['chdir'] = chdir
         if kw:
+            if kw.has_key('srcdir'):
+                def prependDirIfRelative(f, srcdir=kw['srcdir']):
+                    import os.path
+                    if SCons.Util.is_String(f) and not os.path.isabs(f):
+                        f = os.path.join(srcdir, f)
+                    return f
+                if not SCons.Util.is_List(source):
+                    source = [source]
+                source = map(prependDirIfRelative, source)
+                del kw['srcdir']
             if self.overrides:
                 env_kw = self.overrides.copy()
                 env_kw.update(kw)
@@ -636,9 +650,34 @@ class BuilderBase:
             suffix = suffix(env, sources)
         return env.subst(suffix)
 
+    def _src_suffixes_key(self, env):
+        return id(env)
+
+    memoizer_counters.append(SCons.Memoize.CountDict('src_suffixes', _src_suffixes_key))
+
     def src_suffixes(self, env):
-        "__cacheable__"
-        return map(lambda x, s=self, e=env: e.subst(x), self.src_suffix)
+        """
+        Returns the list of source suffixes for this Builder.
+
+        The suffix list may contain construction variable expansions,
+        so we have to evaluate the individual strings.  To avoid doing
+        this over and over, we memoize the results for each construction
+        environment.
+        """
+        memo_key = id(env)
+        try:
+            memo_dict = self._memo['src_suffixes']
+        except KeyError:
+            memo_dict = {}
+            self._memo['src_suffixes'] = memo_dict
+        else:
+            try:
+                return memo_dict[memo_key]
+            except KeyError:
+                pass
+        result = map(lambda x, s=self, e=env: e.subst(x), self.src_suffix)
+        memo_dict[memo_key] = result
+        return result
 
     def set_src_suffix(self, src_suffix):
         if not src_suffix:
@@ -673,13 +712,7 @@ class BuilderBase:
         """
         self.emitter[suffix] = emitter
 
-if SCons.Memoize.use_old_memoization():
-    _Base = BuilderBase
-    class BuilderBase(SCons.Memoize.Memoizer, _Base):
-        "Cache-backed version of BuilderBase"
-        def __init__(self, *args, **kw):
-            apply(_Base.__init__, (self,)+args, kw)
-            SCons.Memoize.Memoizer.__init__(self)
+
 
 class ListBuilder(SCons.Util.Proxy):
     """A Proxy to support building an array of targets (for example,
@@ -718,6 +751,9 @@ class MultiStepBuilder(BuilderBase):
     builder is NOT invoked if the suffix of a source file matches
     src_suffix.
     """
+
+    memoizer_counters = []
+
     def __init__(self,  src_builder,
                         action = None,
                         prefix = '',
@@ -738,8 +774,32 @@ class MultiStepBuilder(BuilderBase):
             src_builder = [ src_builder ]
         self.src_builder = src_builder
 
+    def _get_sdict_key(self, env):
+        return id(env)
+
+    memoizer_counters.append(SCons.Memoize.CountDict('_get_sdict', _get_sdict_key))
+
     def _get_sdict(self, env):
-        "__cacheable__"
+        """
+        Returns a dictionary mapping all of the source suffixes of all
+        src_builders of this Builder to the underlying Builder that
+        should be called first.
+
+        This dictionary is used for each target specified, so we save a
+        lot of extra computation by memoizing it for each construction
+        environment.
+        """
+        memo_key = id(env)
+        try:
+            memo_dict = self._memo['_get_sdict']
+        except KeyError:
+            memo_dict = {}
+            self._memo['_get_sdict'] = memo_dict
+        else:
+            try:
+                return memo_dict[memo_key]
+            except KeyError:
+                pass
         sdict = {}
         for bld in self.src_builder:
             if SCons.Util.is_String(bld):
@@ -749,6 +809,7 @@ class MultiStepBuilder(BuilderBase):
                     continue
             for suf in bld.src_suffixes(env):
                 sdict[suf] = bld
+        memo_dict[memo_key] = sdict
         return sdict
         
     def _execute(self, env, target, source, overwarn={}, executor_kw={}):
@@ -810,14 +871,36 @@ class MultiStepBuilder(BuilderBase):
             ret.append(bld)
         return ret
 
+    def _src_suffixes_key(self, env):
+        return id(env)
+
+    memoizer_counters.append(SCons.Memoize.CountDict('src_suffixes', _src_suffixes_key))
+
     def src_suffixes(self, env):
-        """Return a list of the src_suffix attributes for all
-        src_builders of this Builder.
-        __cacheable__
         """
+        Returns the list of source suffixes for all src_builders of this
+        Builder.
+
+        The suffix list may contain construction variable expansions,
+        so we have to evaluate the individual strings.  To avoid doing
+        this over and over, we memoize the results for each construction
+        environment.
+        """
+        memo_key = id(env)
+        try:
+            memo_dict = self._memo['src_suffixes']
+        except KeyError:
+            memo_dict = {}
+            self._memo['src_suffixes'] = memo_dict
+        else:
+            try:
+                return memo_dict[memo_key]
+            except KeyError:
+                pass
         suffixes = BuilderBase.src_suffixes(self, env)
         for builder in self.get_src_builders(env):
             suffixes.extend(builder.src_suffixes(env))
+        memo_dict[memo_key] = suffixes
         return suffixes
 
 class CompositeBuilder(SCons.Util.Proxy):
index c5b428c58178c9f4b637493d580dc269757f5132..4e196e269c764590d4613d915b11f25bda706b3e 100644 (file)
@@ -339,6 +339,11 @@ class BuilderTestCase(unittest.TestCase):
         else:
             raise "Did not catch expected UserError."
 
+        builder = SCons.Builder.Builder(action="foo")
+        target = builder(env, None, source='n22', srcdir='src_dir')[0]
+        p = target.sources[0].path
+        assert p == os.path.join('src_dir', 'n22'), p
+
     def test_mistaken_variables(self):
         """Test keyword arguments that are often mistakes
         """
@@ -1182,6 +1187,13 @@ class BuilderTestCase(unittest.TestCase):
                                        target_factory=MyNode,
                                        source_factory=MyNode)
 
+        builder2a=SCons.Builder.Builder(action='foo',
+                                        emitter="$FOO",
+                                        target_factory=MyNode,
+                                        source_factory=MyNode)
+
+        assert builder2 == builder2a, repr(builder2.__dict__) + "\n" + repr(builder2a.__dict__)
+
         tgt = builder2(env2, target='foo5', source='bar')[0]
         assert str(tgt) == 'foo5', str(tgt)
         assert str(tgt.sources[0]) == 'bar', str(tgt.sources[0])
@@ -1197,12 +1209,6 @@ class BuilderTestCase(unittest.TestCase):
         assert 'baz' in map(str, tgt.sources), map(str, tgt.sources)
         assert 'bar' in map(str, tgt.sources), map(str, tgt.sources)
 
-        builder2a=SCons.Builder.Builder(action='foo',
-                                        emitter="$FOO",
-                                        target_factory=MyNode,
-                                        source_factory=MyNode)
-        assert builder2 == builder2a, repr(builder2.__dict__) + "\n" + repr(builder2a.__dict__)
-
         # Test that, if an emitter sets a builder on the passed-in
         # targets and passes back new targets, the new builder doesn't
         # get overwritten.
@@ -1595,7 +1601,7 @@ class CompositeBuilderTestCase(unittest.TestCase):
 if __name__ == "__main__":
     suite = unittest.TestSuite()
     tclasses = [
-#        BuilderTestCase,
+        BuilderTestCase,
         CompositeBuilderTestCase
     ]
     for tclass in tclasses:
index 7513c0ddd47131a95eca134dd6e6762014f838f5..96c3cf84022e7c29f28e4c43baa5cb06c632cb4d 100644 (file)
@@ -48,9 +48,10 @@ import sys
 import SCons.Action
 import SCons.Builder
 import SCons.Environment
-import SCons.Tool
+import SCons.PathList
 import SCons.Sig
 import SCons.Subst
+import SCons.Tool
 
 # A placeholder for a default Environment (for fetching source files
 # from source code management systems and the like).  This must be
@@ -214,7 +215,10 @@ def _concat(prefix, list, suffix, env, f=lambda x: x, target=None, source=None):
 
     if SCons.Util.is_List(list):
         list = SCons.Util.flatten(list)
-    list = f(env.subst_path(list, target=target, source=source))
+
+    l = f(SCons.PathList.PathList(list).subst_path(env, target, source))
+    if not l is None:
+        list = l
 
     result = []
 
index a5bffc8d5704c2cdddf16e2b9b72b2e3912c7d3a..4761ea050e53862d1662a00ceac9c4fe123fe0eb 100644 (file)
@@ -166,6 +166,10 @@ def _set_BUILDERS(env, key, value):
         env._dict[key] = BuilderDict(kwbd, env)
     env._dict[key].update(value)
 
+def _del_SCANNERS(env, key):
+    del env._dict[key]
+    env.scanner_map_delete()
+
 def _set_SCANNERS(env, key, value):
     env._dict[key] = value
     env.scanner_map_delete()
@@ -279,29 +283,35 @@ class SubstitutionEnvironment:
         self.lookup_list = SCons.Node.arg2nodes_lookups
         self._dict = kw.copy()
         self._init_special()
+        #self._memo = {}
 
     def _init_special(self):
-        """Initial the dispatch table for special handling of
+        """Initial the dispatch tables for special handling of
         special construction variables."""
-        self._special = {}
+        self._special_del = {}
+        self._special_del['SCANNERS'] = _del_SCANNERS
+
+        self._special_set = {}
         for key in reserved_construction_var_names:
-            self._special[key] = _set_reserved
-        self._special['BUILDERS'] = _set_BUILDERS
-        self._special['SCANNERS'] = _set_SCANNERS
+            self._special_set[key] = _set_reserved
+        self._special_set['BUILDERS'] = _set_BUILDERS
+        self._special_set['SCANNERS'] = _set_SCANNERS
 
     def __cmp__(self, other):
         return cmp(self._dict, other._dict)
 
     def __delitem__(self, key):
-        "__cache_reset__"
-        del self._dict[key]
+        special = self._special_del.get(key)
+        if special:
+            special(self, key)
+        else:
+            del self._dict[key]
 
     def __getitem__(self, key):
         return self._dict[key]
 
     def __setitem__(self, key, value):
-        "__cache_reset__"
-        special = self._special.get(key)
+        special = self._special_set.get(key)
         if special:
             special(self, key, value)
         else:
@@ -663,8 +673,10 @@ class SubstitutionEnvironment:
             except KeyError:
                 orig = value
             else:
-                if len(orig) == 0: orig = []
-                elif not SCons.Util.is_List(orig): orig = [orig]
+                if not orig:
+                    orig = []
+                elif not SCons.Util.is_List(orig): 
+                    orig = [orig]
                 orig = orig + value
             t = []
             if key[-4:] == 'PATH':
@@ -694,6 +706,8 @@ class Base(SubstitutionEnvironment):
     if SCons.Memoize.use_memoizer:
         __metaclass__ = SCons.Memoize.Memoized_Metaclass
 
+    memoizer_counters = []
+
     #######################################################################
     # This is THE class for interacting with the SCons build engine,
     # and it contains a lot of stuff, so we're going to try to keep this
@@ -725,6 +739,7 @@ class Base(SubstitutionEnvironment):
         with the much simpler base class initialization.
         """
         if __debug__: logInstanceCreation(self, 'Environment.Base')
+        self._memo = {}
         self.fs = SCons.Node.FS.default_fs or SCons.Node.FS.FS()
         self.ans = SCons.Node.Alias.default_ans
         self.lookup_list = SCons.Node.arg2nodes_lookups
@@ -786,7 +801,6 @@ class Base(SubstitutionEnvironment):
             return None
 
     def get_calculator(self):
-        "__cacheable__"
         try:
             module = self._calc_module
             c = apply(SCons.Sig.Calculator, (module,), CalculatorArgs)
@@ -800,7 +814,6 @@ class Base(SubstitutionEnvironment):
     def get_factory(self, factory, default='File'):
         """Return a factory function for creating Nodes for this
         construction environment.
-        __cacheable__
         """
         name = default
         try:
@@ -827,50 +840,54 @@ class Base(SubstitutionEnvironment):
             factory = getattr(self.fs, name)
         return factory
 
+    memoizer_counters.append(SCons.Memoize.CountValue('_gsm'))
+
     def _gsm(self):
-        "__cacheable__"
         try:
-            scanners = self._dict['SCANNERS']
+            return self._memo['_gsm']
         except KeyError:
-            return None
+            pass
 
-        sm = {}
-        # Reverse the scanner list so that, if multiple scanners
-        # claim they can scan the same suffix, earlier scanners
-        # in the list will overwrite later scanners, so that
-        # the result looks like a "first match" to the user.
-        if not SCons.Util.is_List(scanners):
-            scanners = [scanners]
+        result = {}
+
+        try:
+            scanners = self._dict['SCANNERS']
+        except KeyError:
+            pass
         else:
-            scanners = scanners[:] # copy so reverse() doesn't mod original
-        scanners.reverse()
-        for scanner in scanners:
-            for k in scanner.get_skeys(self):
-                sm[k] = scanner
-        return sm
+            # Reverse the scanner list so that, if multiple scanners
+            # claim they can scan the same suffix, earlier scanners
+            # in the list will overwrite later scanners, so that
+            # the result looks like a "first match" to the user.
+            if not SCons.Util.is_List(scanners):
+                scanners = [scanners]
+            else:
+                scanners = scanners[:] # copy so reverse() doesn't mod original
+            scanners.reverse()
+            for scanner in scanners:
+                for k in scanner.get_skeys(self):
+                    result[k] = scanner
+
+        self._memo['_gsm'] = result
+
+        return result
         
     def get_scanner(self, skey):
         """Find the appropriate scanner given a key (usually a file suffix).
         """
-        sm = self._gsm()
-        try: return sm[skey]
-        except (KeyError, TypeError): return None
+        return self._gsm().get(skey)
 
-    def _smd(self):
-        "__reset_cache__"
-        pass
-    
     def scanner_map_delete(self, kw=None):
         """Delete the cached scanner map (if we need to).
         """
-        if not kw is None and not kw.has_key('SCANNERS'):
-            return
-        self._smd()
+        try:
+            del self._memo['_gsm']
+        except KeyError:
+            pass
 
     def _update(self, dict):
         """Update an environment's values directly, bypassing the normal
         checks that occur when users try to set items.
-        __cache_reset__
         """
         self._dict.update(dict)
 
@@ -1014,7 +1031,9 @@ class Base(SubstitutionEnvironment):
             clone._dict['BUILDERS'] = BuilderDict(cbd, clone)
         except KeyError:
             pass
-        
+
+        clone._memo = {}
+
         apply_tools(clone, tools, toolpath)
 
         # Apply passed-in variables after the new tools.
@@ -1030,7 +1049,7 @@ class Base(SubstitutionEnvironment):
         return apply(self.Clone, args, kw)
 
     def Detect(self, progs):
-        """Return the first available program in progs.  __cacheable__
+        """Return the first available program in progs.
         """
         if not SCons.Util.is_List(progs):
             progs = [ progs ]
@@ -1306,7 +1325,7 @@ class Base(SubstitutionEnvironment):
         tool(self)
 
     def WhereIs(self, prog, path=None, pathext=None, reject=[]):
-        """Find prog in the path.  __cacheable__
+        """Find prog in the path.
         """
         if path is None:
             try:
@@ -1841,12 +1860,3 @@ def NoSubstitutionProxy(subject):
             self.raw_to_mode(nkw)
             return apply(SCons.Subst.scons_subst, nargs, nkw)
     return _NoSubstitutionProxy(subject)
-
-if SCons.Memoize.use_old_memoization():
-    _Base = Base
-    class Base(SCons.Memoize.Memoizer, _Base):
-        def __init__(self, *args, **kw):
-            SCons.Memoize.Memoizer.__init__(self)
-            apply(_Base.__init__, (self,)+args, kw)
-    Environment = Base
-
index edf374084d021a8fb9cbec339b2f185b9ac85048..f0f73dac6cb1e58d1c795cdb99a93958a39f60f1 100644 (file)
@@ -728,6 +728,10 @@ sys.exit(1)
         env.MergeFlags('-X')
         assert env['CCFLAGS'] == ['-X'], env['CCFLAGS']
 
+        env = SubstitutionEnvironment(CCFLAGS=None)
+        env.MergeFlags('-Y')
+        assert env['CCFLAGS'] == ['-Y'], env['CCFLAGS']
+
         env = SubstitutionEnvironment()
         env.MergeFlags({'A':['aaa'], 'B':['bbb']})
         assert env['A'] == ['aaa'], env['A']
@@ -992,7 +996,7 @@ class BaseTestCase(unittest.TestCase,TestEnvironmentFixture):
                           LIBLINKSUFFIX = 'bar')
 
         def RDirs(pathlist, fs=env.fs):
-            return fs.Rfindalldirs(pathlist, fs.Dir('xx'))
+            return fs.Dir('xx').Rfindalldirs(pathlist)
 
         env['RDirs'] = RDirs
         flags = env.subst_list('$_LIBFLAGS', 1)[0]
@@ -2782,7 +2786,7 @@ def generate(env):
         tgt = env.Install('export', 'build')
         paths = map(str, tgt)
         paths.sort()
-        expect = ['export/build']
+        expect = [os.path.join('export', 'build')]
         assert paths == expect, paths
         for tnode in tgt:
             assert tnode.builder == InstallBuilder
@@ -2790,7 +2794,10 @@ def generate(env):
         tgt = env.Install('export', ['build', 'build/foo1'])
         paths = map(str, tgt)
         paths.sort()
-        expect = ['export/build', 'export/foo1']
+        expect = [
+            os.path.join('export', 'build'),
+            os.path.join('export', 'foo1'),
+        ]
         assert paths == expect, paths
         for tnode in tgt:
             assert tnode.builder == InstallBuilder
index ffc1ba3831cf7a29145a3b1e0872aeca180896c3..4b150102abe7041335b8d2114c3f8fe39d829eb8 100644 (file)
@@ -47,6 +47,8 @@ class Executor:
     if SCons.Memoize.use_memoizer:
         __metaclass__ = SCons.Memoize.Memoized_Metaclass
 
+    memoizer_counters = []
+
     def __init__(self, action, env=None, overridelist=[{}],
                  targets=[], sources=[], builder_kw={}):
         if __debug__: logInstanceCreation(self, 'Executor.Executor')
@@ -58,6 +60,7 @@ class Executor:
         self.targets = targets
         self.sources = sources[:]
         self.builder_kw = builder_kw
+        self._memo = {}
 
     def set_action_list(self, action):
         if not SCons.Util.is_List(action):
@@ -72,7 +75,6 @@ class Executor:
     def get_build_env(self):
         """Fetch or create the appropriate build Environment
         for this Executor.
-        __cacheable__
         """
         # Create the build environment instance with appropriate
         # overrides.  These get evaluated against the current
@@ -125,8 +127,7 @@ class Executor:
         self.do_execute(target, exitstatfunc, kw)
 
     def cleanup(self):
-        "__reset_cache__"
-        pass
+        self._memo = {}
 
     def add_sources(self, sources):
         """Add source files to this Executor's list.  This is necessary
@@ -151,25 +152,30 @@ class Executor:
 
 
     def __str__(self):
-        "__cacheable__"
         return self.my_str()
 
     def nullify(self):
-        "__reset_cache__"
+        self.cleanup()
         self.do_execute = self.do_nothing
         self.my_str     = lambda S=self: ''
 
+    memoizer_counters.append(SCons.Memoize.CountValue('get_contents'))
+
     def get_contents(self):
-        """Fetch the signature contents.  This, along with
-        get_raw_contents(), is the real reason this class exists, so we
-        can compute this once and cache it regardless of how many target
-        or source Nodes there are.
-        __cacheable__
+        """Fetch the signature contents.  This is the main reason this
+        class exists, so we can compute this once and cache it regardless
+        of how many target or source Nodes there are.
         """
+        try:
+            return self._memo['get_contents']
+        except KeyError:
+            pass
         env = self.get_build_env()
         get = lambda action, t=self.targets, s=self.sources, e=env: \
                      action.get_contents(t, s, e)
-        return string.join(map(get, self.get_action_list()), "")
+        result = string.join(map(get, self.get_action_list()), "")
+        self._memo['get_contents'] = result
+        return result
 
     def get_timestamp(self):
         """Fetch a time stamp for this Executor.  We don't have one, of
@@ -219,20 +225,58 @@ class Executor:
 
     def get_missing_sources(self):
         """
-        __cacheable__
         """
         return filter(lambda s: s.missing(), self.sources)
 
-    def get_unignored_sources(self, ignore):
-        """__cacheable__"""
+    def _get_unignored_sources_key(self, ignore=()):
+        return tuple(ignore)
+
+    memoizer_counters.append(SCons.Memoize.CountDict('get_unignored_sources', _get_unignored_sources_key))
+
+    def get_unignored_sources(self, ignore=()):
+        ignore = tuple(ignore)
+        try:
+            memo_dict = self._memo['get_unignored_sources']
+        except KeyError:
+            memo_dict = {}
+            self._memo['get_unignored_sources'] = memo_dict
+        else:
+            try:
+                return memo_dict[ignore]
+            except KeyError:
+                pass
+
         sourcelist = self.sources
         if ignore:
             sourcelist = filter(lambda s, i=ignore: not s in i, sourcelist)
+
+        memo_dict[ignore] = sourcelist
+
         return sourcelist
 
-    def process_sources(self, func, ignore=[]):
-        """__cacheable__"""
-        return map(func, self.get_unignored_sources(ignore))
+    def _process_sources_key(self, func, ignore=()):
+        return (func, tuple(ignore))
+
+    memoizer_counters.append(SCons.Memoize.CountDict('process_sources', _process_sources_key))
+
+    def process_sources(self, func, ignore=()):
+        memo_key = (func, tuple(ignore))
+        try:
+            memo_dict = self._memo['process_sources']
+        except KeyError:
+            memo_dict = {}
+            self._memo['process_sources'] = memo_dict
+        else:
+            try:
+                return memo_dict[memo_key]
+            except KeyError:
+                pass
+
+        result = map(func, self.get_unignored_sources(ignore))
+
+        memo_dict[memo_key] = result
+
+        return result
 
 
 _Executor = Executor
@@ -258,13 +302,3 @@ class Null(_Executor):
         return None
     def cleanup(self):
         pass
-
-
-
-if SCons.Memoize.use_old_memoization():
-    _Base = Executor
-    class Executor(SCons.Memoize.Memoizer, _Base):
-        def __init__(self, *args, **kw):
-            SCons.Memoize.Memoizer.__init__(self)
-            apply(_Base.__init__, (self,)+args, kw)
-
index d2c019f2050d2690d7560d7bd36d7c3952e9e776..e38e25111da3ecb45685596fad82cf825de210d6 100644 (file)
@@ -259,16 +259,14 @@ class ParallelTestCase(unittest.TestCase):
             jobs.run()
 
             # The key here is that we get(1) and get(2) from the
-            # resultsQueue before we put(3).
+            # resultsQueue before we put(3), but get(1) and get(2) can
+            # be in either order depending on how the first two parallel
+            # tasks get scheduled by the operating system.
             expect = [
-                'put(1)',
-                'put(2)',
-                'get(1)',
-                'get(2)',
-                'put(3)',
-                'get(3)',
+                ['put(1)', 'put(2)', 'get(1)', 'get(2)', 'put(3)', 'get(3)'],
+                ['put(1)', 'put(2)', 'get(2)', 'get(1)', 'put(3)', 'get(3)'],
             ]
-            assert ThreadPoolCallList == expect, ThreadPoolCallList
+            assert ThreadPoolCallList in expect, ThreadPoolCallList
 
         finally:
             SCons.Job.ThreadPool = SaveThreadPool
index c2a302725b6549f32f15d60af2607dcedb6e4122..6a463505936471e181a8e8cc052c0fd36d43bf15 100644 (file)
@@ -1,66 +1,3 @@
-"""Memoizer
-
-Memoizer -- base class to provide automatic, optimized caching of
-method return values for subclassed objects.  Caching is activated by
-the presence of "__cacheable__" in the doc of a method (acts like a
-decorator).  The presence of "__cache_reset__" or "__reset_cache__"
-in the doc string instead indicates a method that should reset the
-cache, discarding any currently cached values.
-
-Note: current implementation is optimized for speed, not space.  The
-cache reset operation does not actually discard older results, and in
-fact, all cached results (and keys) are held indefinitely.
-
-Most of the work for this is done by copying and modifying the class
-definition itself, rather than the object instances.  This will
-therefore allow all instances of a class to get caching activated
-without requiring lengthy initialization or other management of the
-instance.
-
-[This could also be done using metaclassing (which would require
-Python 2.2) and decorators (which would require Python 2.4).  Current
-implementation is used due to Python 1.5.2 compatability requirement
-contraint.]
-
-A few notes:
-
-    * All local methods/attributes use a prefix of "_MeMoIZeR" to avoid
-      namespace collisions with the attributes of the objects
-      being cached.
-
-    * Based on performance evaluations of dictionaries, caching is
-      done by providing each object with a unique key attribute and
-      using the value of that attribute as an index for dictionary
-      lookup.  If an object doesn't have one of these attributes,
-      fallbacks are utilized (although they will be somewhat slower).
-
-      * To support this unique-value attribute correctly, it must be
-        removed whenever a __cmp__ operation is performed, and it must
-        be updated whenever a copy.copy or copy.deepcopy is performed,
-        so appropriate manipulation is provided by the Caching code
-        below.
-
-    * Cached values are stored in the class (indexed by the caching
-      key attribute, then by the name of the method called and the
-      constructed key of the arguments passed).  By storing them here
-      rather than on the instance, the instance can be compared,
-      copied, and pickled much easier.
-
-Some advantages:
-
-    * The method by which caching is implemented can be changed in a
-      single location and it will apply globally.
-
-    * Greatly simplified client code: remove lots of try...except or
-      similar handling of cached lookup.  Also usually more correct in
-      that it based caching on all input arguments whereas many
-      hand-implemented caching operations often miss arguments that
-      might affect results.
-
-    * Caching can be globally disabled very easily (for testing, etc.)
-
-"""
-
 #
 # __COPYRIGHT__
 #
@@ -86,752 +23,247 @@ Some advantages:
 
 __revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
 
-#TBD: for pickling, should probably revert object to unclassed state...
+__doc__ = """Memoizer
 
-import copy
-import os
-import string
-import sys
+A metaclass implementation to count hits and misses of the computed
+values that various methods cache in memory.
 
-# A flag controlling whether or not we actually use memoization.
-use_memoizer = 1
+Use of this modules assumes that wrapped methods be coded to cache their
+values in a consistent way.  Here is an example of wrapping a method
+that returns a computed value, with no input parameters:
 
-#
-# Generate a key for an object that is to be used as the caching key
-# for that object.
-#
-# Current implementation: singleton generating a monotonically
-# increasing integer
+    memoizer_counters = []                                      # Memoization
 
-class MemoizerKey:
-    def __init__(self):
-        self._next_keyval = 0
-    def __call__(self):
-        r = self._next_keyval
-        self._next_keyval = self._next_keyval + 1
-        return str(r)
-Next_Memoize_Key = MemoizerKey()
+    memoizer_counters.append(SCons.Memoize.CountValue('foo'))   # Memoization
 
+    def foo(self):
 
-#
-# Memoized Class management.
-#
-# Classes can be manipulated just like object instances; we are going
-# to do some of that here, without the benefit of metaclassing
-# introduced in Python 2.2 (it would be nice to use that, but this
-# attempts to maintain backward compatibility to Python 1.5.2).
-#
-# The basic implementation therefore is to update the class definition
-# for any objects that we want to enable caching for.  The updated
-# definition performs caching activities for those methods
-# appropriately marked in the original class.
-#
-# When an object is created, its class is switched to this updated,
-# cache-enabled class definition, thereby enabling caching operations.
-#
-# To get an instance to used the updated, caching class, the instance
-# must declare the Memoizer as a base class and make sure to call the
-# Memoizer's __init__ during the instance's __init__.  The Memoizer's
-# __init__ will perform the class updating.
-
-# For Python 2.2 and later, where metaclassing is supported, it is
-# sufficient to provide a "__metaclass__ = Memoized_Metaclass" as part
-# of the class definition; the metaclassing will automatically invoke
-# the code herein properly.
-
-##import cPickle
-##def ALT0_MeMoIZeR_gen_key(argtuple, kwdict):
-##    return cPickle.dumps( (argtuple,kwdict) )
-
-def ALT1_MeMoIZeR_gen_key(argtuple, kwdict):
-    return repr(argtuple) + '|' + repr(kwdict)
-
-def ALT2_MeMoIZeR_gen_key(argtuple, kwdict):
-    return string.join(map(lambda A:
-                           getattr(A, '_MeMoIZeR_Key', str(A)),
-                           argtuple) + \
-                       map(lambda D:
-                           str(D[0])+
-                           getattr(D[1], '_MeMoIZeR_Key', str(D[1])),
-                           kwdict.items()),
-                       '|')
-
-def ALT3_MeMoIZeR_gen_key(argtuple, kwdict):
-    ret = []
-    for A in argtuple:
-        X = getattr(A, '_MeMoIZeR_Key', None)
-        if X:
-            ret.append(X)
-        else:
-            ret.append(str(A))
-    for K,V in kwdict.items():
-        ret.append(str(K))
-        X = getattr(V, '_MeMoIZeR_Key', None)
-        if X:
-            ret.append(X)
-        else:
-            ret.append(str(V))
-    return string.join(ret, '|')
-
-def ALT4_MeMoIZeR_gen_key(argtuple, kwdict):
-    if kwdict:
-        return string.join(map(lambda A:
-                               getattr(A, '_MeMoIZeR_Key', None) or str(A),
-                               argtuple) + \
-                           map(lambda D:
-                               str(D[0])+
-                               (getattr(D[1], '_MeMoIZeR_Key', None) or str(D[1])),
-                               kwdict.items()),
-                           '|')
-    return string.join(map(lambda A:
-                        getattr(A, '_MeMoIZeR_Key', None) or str(A),
-                        argtuple),
-                       '!')
-
-def ALT5_MeMoIZeR_gen_key(argtuple, kwdict):
-    A = string.join(map(str, argtuple), '|')
-    K = ''
-    if kwdict:
-        I = map(lambda K,D=kwdict: str(K)+'='+str(D[K]), kwdict.keys())
-        K = string.join(I, '|')
-    return string.join([A,K], '!')
-
-def ALT6_MeMoIZeR_gen_key(argtuple, kwdict):
-    A = string.join(map(str, map(id, argtuple)), '|')
-    K = ''
-    if kwdict:
-        I = map(lambda K,D=kwdict: str(K)+'='+str(id(D[K])), kwdict.keys())
-        K = string.join(I, '|')
-    return string.join([A,K], '!')
-
-def ALT7_MeMoIZeR_gen_key(argtuple, kwdict):
-    A = string.join(map(repr, argtuple), '|')
-    K = ''
-    if kwdict:
-        I = map(lambda K,D=kwdict: repr(K)+'='+repr(D[K]), kwdict.keys())
-        K = string.join(I, '|')
-    return string.join([A,K], '!')
-
-def ALT8_MeMoIZeR_gen_key(argtuple, kwdict):
-    ret = []
-    for A in argtuple:
-        X = getattr(A, '_MeMoIZeR_Key', None)
-        if X:
-            ret.append(X)
-        else:
-            ret.append(repr(A))
-    for K,V in kwdict.items():
-        ret.append(str(K))
-        X = getattr(V, '_MeMoIZeR_Key', None)
-        if X:
-            ret.append(X)
-        else:
-            ret.append(repr(V))
-    return string.join(ret, '|')
+        try:                                                    # Memoization
+            return self._memo['foo']                            # Memoization
+        except KeyError:                                        # Memoization
+            pass                                                # Memoization
 
-def ALT9_MeMoIZeR_gen_key(argtuple, kwdict):
-    ret = []
-    for A in argtuple:
-        try:
-            X = A.__dict__.get('_MeMoIZeR_Key', None) or repr(A)
-        except (AttributeError, KeyError):
-            X = repr(A)
-        ret.append(X)
-    for K,V in kwdict.items():
-        ret.append(str(K))
-        ret.append('=')
-        try:
-            X = V.__dict__.get('_MeMoIZeR_Key', None) or repr(V)
-        except (AttributeError, KeyError):
-            X = repr(V)
-        ret.append(X)
-    return string.join(ret, '|')
-
-#_MeMoIZeR_gen_key = ALT9_MeMoIZeR_gen_key    # 8.8, 0.20
-_MeMoIZeR_gen_key = ALT8_MeMoIZeR_gen_key    # 8.5, 0.18
-#_MeMoIZeR_gen_key = ALT7_MeMoIZeR_gen_key    # 8.7, 0.17
-#_MeMoIZeR_gen_key = ALT6_MeMoIZeR_gen_key    #
-#_MeMoIZeR_gen_key = ALT5_MeMoIZeR_gen_key    # 9.7, 0.20
-#_MeMoIZeR_gen_key = ALT4_MeMoIZeR_gen_key    # 8.6, 0.19
-#_MeMoIZeR_gen_key = ALT3_MeMoIZeR_gen_key    # 8.5, 0.20
-#_MeMoIZeR_gen_key = ALT2_MeMoIZeR_gen_key    # 10.1, 0.22
-#_MeMoIZeR_gen_key = ALT1_MeMoIZeR_gen_key    # 8.6 0.18
-
-
-
-## This is really the core worker of the Memoize module.  Any
-## __cacheable__ method ends up calling this function which tries to
-## return a previously cached value if it exists, and which calls the
-## actual function and caches the return value if it doesn't already
-## exist.
-##
-## This function should be VERY efficient: it will get called a lot
-## and its job is to be faster than what would be called.
-
-def Memoizer_cache_get(func, cdict, args, kw):
-    """Called instead of name to see if this method call's return
-    value has been cached.  If it has, just return the cached
-    value; if not, call the actual method and cache the return."""
-
-    obj = args[0]
-
-    ckey = obj._MeMoIZeR_Key + ':' + _MeMoIZeR_gen_key(args, kw)
-
-##    try:
-##        rval = cdict[ckey]
-##    except KeyError:
-##        rval = cdict[ckey] = apply(func, args, kw)
-
-    rval = cdict.get(ckey, "_MeMoIZeR")
-    if rval is "_MeMoIZeR":
-        rval = cdict[ckey] = apply(func, args, kw)
-
-##    rval = cdict.setdefault(ckey, apply(func, args, kw))
-
-##    if cdict.has_key(ckey):
-##        rval = cdict[ckey]
-##    else:
-##        rval = cdict[ckey] = apply(func, args, kw)
-
-    return rval
-
-def Memoizer_cache_get_self(func, cdict, self):
-    """Called instead of func(self) to see if this method call's
-    return value has been cached.  If it has, just return the cached
-    value; if not, call the actual method and cache the return.
-    Optimized version of Memoizer_cache_get for methods that take the
-    object instance as the only argument."""
-
-    ckey = self._MeMoIZeR_Key
-
-##    try:
-##        rval = cdict[ckey]
-##    except KeyError:
-##        rval = cdict[ckey] = func(self)
-
-    rval = cdict.get(ckey, "_MeMoIZeR")
-    if rval is "_MeMoIZeR":
-        rval = cdict[ckey] = func(self)
-
-##    rval = cdict.setdefault(ckey, func(self)))
-
-##    if cdict.has_key(ckey):
-##        rval = cdict[ckey]
-##    else:
-##        rval = cdict[ckey] = func(self)
-
-    return rval
-
-def Memoizer_cache_get_one(func, cdict, self, arg):
-    """Called instead of func(self, arg) to see if this method call's
-    return value has been cached.  If it has, just return the cached
-    value; if not, call the actual method and cache the return.
-    Optimized version of Memoizer_cache_get for methods that take the
-    object instance and one other argument only."""
-
-##    X = getattr(arg, "_MeMoIZeR_Key", None)
-##    if X:
-##        ckey = self._MeMoIZeR_Key +':'+ X
-##    else:
-##        ckey = self._MeMoIZeR_Key +':'+ str(arg)
-    ckey = self._MeMoIZeR_Key + ':' + \
-           (getattr(arg, "_MeMoIZeR_Key", None) or repr(arg))
-
-##    try:
-##        rval = cdict[ckey]
-##    except KeyError:
-##        rval = cdict[ckey] = func(self, arg)
-
-    rval = cdict.get(ckey, "_MeMoIZeR")
-    if rval is "_MeMoIZeR":
-        rval = cdict[ckey] = func(self, arg)
+        result = self.compute_foo_value()
 
-##    rval = cdict.setdefault(ckey, func(self, arg)))
-
-##    if cdict.has_key(ckey):
-##        rval = cdict[ckey]
-##    else:
-##        rval = cdict[ckey] = func(self, arg)
+        self._memo['foo'] = result                              # Memoization
 
-    return rval
+        return result
 
-#
-# Caching stuff is tricky, because the tradeoffs involved are often so
-# non-obvious, so we're going to support an alternate set of functions
-# that also count the hits and misses, to try to get a concrete idea of
-# which Memoizations seem to pay off.
-#
-# Because different configurations can have such radically different
-# performance tradeoffs, interpreting the hit/miss results will likely be
-# more of an art than a science.  In other words, don't assume that just
-# because you see no hits in one configuration that it's not worthwhile
-# Memoizing that method.
-#
-# Note that these are essentially cut-and-paste copies of the above
-# Memozer_cache_get*() implementations, with the addition of the
-# counting logic.  If the above implementations change, the
-# corresponding change should probably be made down below as well,
-# just to try to keep things in sync.
-#
+Here is an example of wrapping a method that will return different values
+based on one or more input arguments:
 
-class CounterEntry:
-    def __init__(self):
-        self.hit = 0
-        self.miss = 0
+    def _bar_key(self, argument):                               # Memoization
+        return argument                                         # Memoization
 
-import UserDict
-class Counter(UserDict.UserDict):
-    def __call__(self, obj, methname):
-        k = obj.__class__.__name__ + '.' + methname
-        try:
-            return self[k]
-        except KeyError:
-            c = self[k] = CounterEntry()
-            return c
-
-CacheCount = Counter()
-CacheCountSelf = Counter()
-CacheCountOne = Counter()
-
-def Dump():
-    items = CacheCount.items() + CacheCountSelf.items() + CacheCountOne.items()
-    items.sort()
-    for k, v in items:
-        print "    %7d hits %7d misses   %s()" % (v.hit, v.miss, k)
-
-def Count_cache_get(name, func, cdict, args, kw):
-    """Called instead of name to see if this method call's return
-    value has been cached.  If it has, just return the cached
-    value; if not, call the actual method and cache the return."""
-
-    obj = args[0]
-
-    ckey = obj._MeMoIZeR_Key + ':' + _MeMoIZeR_gen_key(args, kw)
-
-    c = CacheCount(obj, name)
-    rval = cdict.get(ckey, "_MeMoIZeR")
-    if rval is "_MeMoIZeR":
-        rval = cdict[ckey] = apply(func, args, kw)
-        c.miss = c.miss + 1
-    else:
-        c.hit = c.hit + 1
-
-    return rval
-
-def Count_cache_get_self(name, func, cdict, self):
-    """Called instead of func(self) to see if this method call's
-    return value has been cached.  If it has, just return the cached
-    value; if not, call the actual method and cache the return.
-    Optimized version of Memoizer_cache_get for methods that take the
-    object instance as the only argument."""
-
-    ckey = self._MeMoIZeR_Key
-
-    c = CacheCountSelf(self, name)
-    rval = cdict.get(ckey, "_MeMoIZeR")
-    if rval is "_MeMoIZeR":
-        rval = cdict[ckey] = func(self)
-        c.miss = c.miss + 1
-    else:
-        c.hit = c.hit + 1
-
-    return rval
-
-def Count_cache_get_one(name, func, cdict, self, arg):
-    """Called instead of func(self, arg) to see if this method call's
-    return value has been cached.  If it has, just return the cached
-    value; if not, call the actual method and cache the return.
-    Optimized version of Memoizer_cache_get for methods that take the
-    object instance and one other argument only."""
-
-    ckey = self._MeMoIZeR_Key + ':' + \
-           (getattr(arg, "_MeMoIZeR_Key", None) or repr(arg))
-
-    c = CacheCountOne(self, name)
-    rval = cdict.get(ckey, "_MeMoIZeR")
-    if rval is "_MeMoIZeR":
-        rval = cdict[ckey] = func(self, arg)
-        c.miss = c.miss + 1
-    else:
-        c.hit = c.hit + 1
-
-    return rval
-
-MCG_dict = {
-    'MCG'  : Memoizer_cache_get,
-    'MCGS' : Memoizer_cache_get_self,
-    'MCGO' : Memoizer_cache_get_one,
-}
-
-MCG_lambda = "lambda *args, **kw: MCG(methcode, methcached, args, kw)"
-MCGS_lambda = "lambda self: MCGS(methcode, methcached, self)"
-MCGO_lambda = "lambda self, arg: MCGO(methcode, methcached, self, arg)"
-
-def EnableCounting():
-    """Enable counting of Memoizer hits and misses by overriding the
-    globals that hold the non-counting versions of the functions and
-    lambdas we call with the counting versions.
-    """
-    global MCG_dict
-    global MCG_lambda
-    global MCGS_lambda
-    global MCGO_lambda
+    memoizer_counters.append(SCons.Memoize.CountDict('bar', _bar_key)) # Memoization
 
-    MCG_dict = {
-        'MCG'  : Count_cache_get,
-        'MCGS' : Count_cache_get_self,
-        'MCGO' : Count_cache_get_one,
-    }
+    def bar(self, argument):
 
-    MCG_lambda = "lambda *args, **kw: MCG(methname, methcode, methcached, args, kw)"
-    MCGS_lambda = "lambda self: MCGS(methname, methcode, methcached, self)"
-    MCGO_lambda = "lambda self, arg: MCGO(methname, methcode, methcached, self, arg)"
+        memo_key = argument                                     # Memoization
+        try:                                                    # Memoization
+            memo_dict = self._memo['bar']                       # Memoization
+        except KeyError:                                        # Memoization
+            memo_dict = {}                                      # Memoization
+            self._memo['dict'] = memo_dict                      # Memoization
+        else:                                                   # Memoization
+            try:                                                # Memoization
+                return memo_dict[memo_key]                      # Memoization
+            except KeyError:                                    # Memoization
+                pass                                            # Memoization
 
+        result = self.compute_bar_value(argument)
 
+        memo_dict[memo_key] = result                            # Memoization
 
-class _Memoizer_Simple:
+        return result
 
-    def __setstate__(self, state):
-        self.__dict__.update(state)
-        self.__dict__['_MeMoIZeR_Key'] = Next_Memoize_Key()
-        #kwq: need to call original's setstate if it had one...
+At one point we avoided replicating this sort of logic in all the methods
+by putting it right into this module, but we've moved away from that at
+present (see the "Historical Note," below.).
 
-    def _MeMoIZeR_reset(self):
-        self.__dict__['_MeMoIZeR_Key'] = Next_Memoize_Key()
-        return 1
+Deciding what to cache is tricky, because different configurations
+can have radically different performance tradeoffs, and because the
+tradeoffs involved are often so non-obvious.  Consequently, deciding
+whether or not to cache a given method will likely be more of an art than
+a science, but should still be based on available data from this module.
+Here are some VERY GENERAL guidelines about deciding whether or not to
+cache return values from a method that's being called a lot:
 
+    --  The first question to ask is, "Can we change the calling code
+        so this method isn't called so often?"  Sometimes this can be
+        done by changing the algorithm.  Sometimes the *caller* should
+        be memoized, not the method you're looking at.
 
-class _Memoizer_Comparable:
+    --  The memoized function should be timed with multiple configurations
+        to make sure it doesn't inadvertently slow down some other
+        configuration.
 
-    def __setstate__(self, state):
-        self.__dict__.update(state)
-        self.__dict__['_MeMoIZeR_Key'] = Next_Memoize_Key()
-        #kwq: need to call original's setstate if it had one...
+    --  When memoizing values based on a dictionary key composed of
+        input arguments, you don't need to use all of the arguments
+        if some of them don't affect the return values.
 
-    def _MeMoIZeR_reset(self):
-        self.__dict__['_MeMoIZeR_Key'] = Next_Memoize_Key()
-        return 1
+Historical Note:  The initial Memoizer implementation actually handled
+the caching of values for the wrapped methods, based on a set of generic
+algorithms for computing hashable values based on the method's arguments.
+This collected caching logic nicely, but had two drawbacks:
 
-    def __cmp__(self, other):
-        """A comparison might use the object dictionaries to
-        compare, so the dictionaries should contain caching
-        entries.  Make new dictionaries without those entries
-        to use with the underlying comparison."""
+    Running arguments through a generic key-conversion mechanism is slower
+    (and less flexible) than just coding these things directly.  Since the
+    methods that need memoized values are generally performance-critical,
+    slowing them down in order to collect the logic isn't the right
+    tradeoff.
 
-        if self is other:
-            return 0
+    Use of the memoizer really obscured what was being called, because
+    all the memoized methods were wrapped with re-used generic methods.
+    This made it more difficult, for example, to use the Python profiler
+    to figure out how to optimize the underlying methods.
+"""
 
-        # We are here as a cached object, but cmp will flip its
-        # arguments back and forth and recurse attempting to get base
-        # arguments for the comparison, so we might have already been
-        # stripped.
+import new
 
-        try:
-            saved_d1 = self.__dict__
-            d1 = copy.copy(saved_d1)
-            del d1['_MeMoIZeR_Key']
-        except KeyError:
-            return self._MeMoIZeR_cmp(other)
-        self.__dict__ = d1
+# A flag controlling whether or not we actually use memoization.
+use_memoizer = None
 
-        # Same thing for the other, but we should try to convert it
-        # here in case the _MeMoIZeR_cmp compares __dict__ objects
-        # directly.
+CounterList = []
 
-        saved_other = None
-        try:
-            if other.__dict__.has_key('_MeMoIZeR_Key'):
-                saved_other = other.__dict__
-                d2 = copy.copy(saved_other)
-                del d2['_MeMoIZeR_Key']
-                other.__dict__ = d2
-        except (AttributeError, KeyError):
-            pass
-
-        # Both self and other have been prepared: perform the test,
-        # then restore the original dictionaries and exit
-
-        rval = self._MeMoIZeR_cmp(other)
-
-        self.__dict__ = saved_d1
-        if saved_other:
-            other.__dict__ = saved_other
-
-        return rval
-
-
-def Analyze_Class(klass):
-    if klass.__dict__.has_key('_MeMoIZeR_converted'): return klass
-
-    original_name = str(klass)
-
-    D,R,C = _analyze_classmethods(klass.__dict__, klass.__bases__)
-
-    if C:
-        modelklass = _Memoizer_Comparable
-        lcldict = {'_MeMoIZeR_cmp':C}
-    else:
-        modelklass = _Memoizer_Simple
-        lcldict = {}
-
-    klass.__dict__.update(memoize_classdict(klass, modelklass, lcldict, D, R))
-
-    return klass
-
-
-# Note that each eval("lambda...") has a few \n's prepended to the
-# lambda, and furthermore that each of these evals has a different
-# number of \n's prepended.  This is to provide a little bit of info
-# for traceback or profile output, which generate things like 'File
-# "<string>", line X'.  X will be the number of \n's plus 1.
-
-# Also use the following routine to specify the "filename" portion so
-# that it provides useful information.  In addition, make sure it
-# contains 'os.sep + "SCons" + os.sep' for the
-# SCons.Script.find_deepest_user_frame operation.
-
-def whoami(memoizer_funcname, real_funcname):
-    return '...'+os.sep+'SCons'+os.sep+'Memoizer-'+ \
-           memoizer_funcname+'-lambda<'+real_funcname+'>'
-
-def memoize_classdict(klass, modelklass, new_klassdict, cacheable, resetting):
-    new_klassdict.update(modelklass.__dict__)
-    new_klassdict['_MeMoIZeR_converted'] = 1
-
-    for name,code in cacheable.items():
-        eval_dict = {
-            'methname' : name,
-            'methcode' : code,
-            'methcached' : {},
-        }
-        eval_dict.update(MCG_dict)
-        fc = code.func_code
-        if fc.co_argcount == 1 and not fc.co_flags & 0xC:
-            compiled = compile("\n"*1 + MCGS_lambda,
-                               whoami('cache_get_self', name),
-                               "eval")
-        elif fc.co_argcount == 2 and not fc.co_flags & 0xC:
-            compiled = compile("\n"*2 + MCGO_lambda,
-                               whoami('cache_get_one', name),
-                               "eval")
+class Counter:
+    """
+    Base class for counting memoization hits and misses.
+
+    We expect that the metaclass initialization will have filled in
+    the .name attribute that represents the name of the function
+    being counted.
+    """
+    def __init__(self, method_name):
+        """
+        """
+        self.method_name = method_name
+        self.hit = 0
+        self.miss = 0
+        CounterList.append(self)
+    def display(self):
+        fmt = "    %7d hits %7d misses    %s()"
+        print fmt % (self.hit, self.miss, self.name)
+    def __cmp__(self, other):
+        return cmp(self.name, other.name)
+
+class CountValue(Counter):
+    """
+    A counter class for simple, atomic memoized values.
+
+    A CountValue object should be instantiated in a class for each of
+    the class's methods that memoizes its return value by simply storing
+    the return value in its _memo dictionary.
+
+    We expect that the metaclass initialization will fill in the
+    .underlying_method attribute with the method that we're wrapping.
+    We then call the underlying_method method after counting whether
+    its memoized value has already been set (a hit) or not (a miss).
+    """
+    def __call__(self, *args, **kw):
+        obj = args[0]
+        if obj._memo.has_key(self.method_name):
+            self.hit = self.hit + 1
         else:
-            compiled = compile("\n"*3 + MCG_lambda,
-                               whoami('cache_get', name),
-                               "eval")
-        newmethod = eval(compiled, eval_dict, {})
-        new_klassdict[name] = newmethod
-
-    for name,code in resetting.items():
-        newmethod = eval(
-            compile(
-            "lambda obj_self, *args, **kw: (obj_self._MeMoIZeR_reset(), apply(rmethcode, (obj_self,)+args, kw))[1]",
-            whoami('cache_reset', name),
-            'eval'),
-            {'rmethcode':code}, {})
-        new_klassdict[name] = newmethod
-
-    return new_klassdict
-
-def _analyze_classmethods(klassdict, klassbases):
-    """Given a class, performs a scan of methods for that class and
-    all its base classes (recursively). Returns aggregated results of
-    _scan_classdict calls where subclass methods are superimposed over
-    base class methods of the same name (emulating instance->class
-    method lookup)."""
-
-    D = {}
-    R = {}
-    C = None
-
-    # Get cache/reset/cmp methods from subclasses
-
-    for K in klassbases:
-        if K.__dict__.has_key('_MeMoIZeR_converted'): continue
-        d,r,c = _analyze_classmethods(K.__dict__, K.__bases__)
-        D.update(d)
-        R.update(r)
-        C = c or C
-
-    # Delete base method info if current class has an override
-
-    for M in D.keys():
-        if M == '__cmp__': continue
-        if klassdict.has_key(M):
-            del D[M]
-    for M in R.keys():
-        if M == '__cmp__': continue
-        if klassdict.has_key(M):
-            del R[M]
-
-    # Get cache/reset/cmp from current class
-
-    d,r,c = _scan_classdict(klassdict)
-
-    # Update accumulated cache/reset/cmp methods
-
-    D.update(d)
-    R.update(r)
-    C = c or C
-
-    return D,R,C
-
-
-def _scan_classdict(klassdict):
-    """Scans the method dictionary of a class to find all methods
-    interesting to caching operations.  Returns a tuple of these
-    interesting methods:
-
-      ( dict-of-cachable-methods,
-        dict-of-cache-resetting-methods,
-        cmp_method_val or None)
-
-    Each dict has the name of the method as a key and the corresponding
-    value is the method body."""
-
-    cache_setters = {}
-    cache_resetters = {}
-    cmp_if_exists = None
-    already_cache_modified = 0
-
-    for attr,val in klassdict.items():
-        if not callable(val): continue
-        if attr == '__cmp__':
-            cmp_if_exists = val
-            continue  # cmp can't be cached and can't reset cache
-        if attr == '_MeMoIZeR_cmp':
-            already_cache_modified = 1
-            continue
-        if not val.__doc__: continue
-        if string.find(val.__doc__, '__cache_reset__') > -1:
-            cache_resetters[attr] = val
-            continue
-        if string.find(val.__doc__, '__reset_cache__') > -1:
-            cache_resetters[attr] = val
-            continue
-        if string.find(val.__doc__, '__cacheable__') > -1:
-            cache_setters[attr] = val
-            continue
-    if already_cache_modified: cmp_if_exists = 'already_cache_modified'
-    return cache_setters, cache_resetters, cmp_if_exists
+            self.miss = self.miss + 1
+        return apply(self.underlying_method, args, kw)
 
-#
-# Primary Memoizer class.  This should be a base-class for any class
-# that wants method call results to be cached.  The sub-class should
-# call this parent class's __init__ method, but no other requirements
-# are made on the subclass (other than appropriate decoration).
+class CountDict(Counter):
+    """
+    A counter class for memoized values stored in a dictionary, with
+    keys based on the method's input arguments.
+
+    A CountDict object is instantiated in a class for each of the
+    class's methods that memoizes its return value in a dictionary,
+    indexed by some key that can be computed from one or more of
+    its input arguments.
+
+    We expect that the metaclass initialization will fill in the
+    .underlying_method attribute with the method that we're wrapping.
+    We then call the underlying_method method after counting whether the
+    computed key value is already present in the memoization dictionary
+    (a hit) or not (a miss).
+    """
+    def __init__(self, method_name, keymaker):
+        """
+        """
+        Counter.__init__(self, method_name)
+        self.keymaker = keymaker
+    def __call__(self, *args, **kw):
+        obj = args[0]
+        try:
+            memo_dict = obj._memo[self.method_name]
+        except KeyError:
+            self.miss = self.miss + 1
+        else:
+            key = apply(self.keymaker, args, kw)
+            if memo_dict.has_key(key):
+                self.hit = self.hit + 1
+            else:
+                self.miss = self.miss + 1
+        return apply(self.underlying_method, args, kw)
 
 class Memoizer:
     """Object which performs caching of method calls for its 'primary'
     instance."""
 
     def __init__(self):
-        self.__class__ = Analyze_Class(self.__class__)
-        self._MeMoIZeR_Key =  Next_Memoize_Key()
+        pass
+
+# Find out if we support metaclasses (Python 2.2 and later).
 
-# Find out if we are pre-2.2
+class M:
+    def __init__(cls, name, bases, cls_dict):
+        cls.has_metaclass = 1
+
+class A:
+    __metaclass__ = M
 
 try:
-    vinfo = sys.version_info
+    has_metaclass = A.has_metaclass
 except AttributeError:
-    """Split an old-style version string into major and minor parts.  This
-    is complicated by the fact that a version string can be something
-    like 3.2b1."""
-    import re
-    version = string.split(string.split(sys.version, ' ')[0], '.')
-    vinfo = (int(version[0]), int(re.match('\d+', version[1]).group()))
-    del re
-
-need_version = (2, 2) # actual
-#need_version = (33, 0)  # always
-#need_version = (0, 0)  # never
-
-has_metaclass =  (vinfo[0] > need_version[0] or \
-                  (vinfo[0] == need_version[0] and
-                   vinfo[1] >= need_version[1]))
+    has_metaclass = None
+
+del M
+del A
 
 if not has_metaclass:
 
+    def Dump(title):
+        pass
+
     class Memoized_Metaclass:
         # Just a place-holder so pre-metaclass Python versions don't
         # have to have special code for the Memoized classes.
         pass
 
+    def EnableMemoization():
+        import SCons.Warnings
+        msg = 'memoization is not supported in this version of Python (no metaclasses)'
+        raise SCons.Warnings.NoMetaclassSupportWarning, msg
+
 else:
 
-    # Initialization is a wee bit of a hassle.  We want to do some of
-    # our own work for initialization, then pass on to the actual
-    # initialization function.  However, we have to be careful we
-    # don't interfere with (a) the super()'s initialization call of
-    # it's superclass's __init__, and (b) classes we are Memoizing
-    # that don't have their own __init__ but which have a super that
-    # has an __init__.  To do (a), we eval a lambda below where the
-    # actual init code is locally bound and the __init__ entry in the
-    # class's dictionary is replaced with the _MeMoIZeR_init call.  To
-    # do (b), we use _MeMoIZeR_superinit as a fallback if the class
-    # doesn't have it's own __init__.  Note that we don't use getattr
-    # to obtain the __init__ because we don't want to re-instrument
-    # parent-class __init__ operations (and we want to avoid the
-    # Object object's slot init if the class has no __init__).
-
-    def _MeMoIZeR_init(actual_init, self, args, kw):
-        self.__dict__['_MeMoIZeR_Key'] =  Next_Memoize_Key()
-        apply(actual_init, (self,)+args, kw)
-
-    def _MeMoIZeR_superinit(self, cls, args, kw):
-        apply(super(cls, self).__init__, args, kw)
+    def Dump(title=None):
+        if title:
+            print title
+        CounterList.sort()
+        for counter in CounterList:
+            counter.display()
 
     class Memoized_Metaclass(type):
         def __init__(cls, name, bases, cls_dict):
-            # Note that cls_dict apparently contains a *copy* of the
-            # attribute dictionary of the class; modifying cls_dict
-            # has no effect on the actual class itself.
-            D,R,C = _analyze_classmethods(cls_dict, bases)
-            if C:
-                modelklass = _Memoizer_Comparable
-                cls_dict['_MeMoIZeR_cmp'] = C
-            else:
-                modelklass = _Memoizer_Simple
-            klassdict = memoize_classdict(cls, modelklass, cls_dict, D, R)
-
-            init = klassdict.get('__init__', None)
-            if not init:
-                # Make sure filename has os.sep+'SCons'+os.sep so that
-                # SCons.Script.find_deepest_user_frame doesn't stop here
-                import inspect # It's OK, can't get here for Python < 2.1
-                filename = inspect.getsourcefile(_MeMoIZeR_superinit)
-                if not filename:
-                    # This file was compiled at a path name different from
-                    # how it's invoked now, so just make up something.
-                    filename = whoami('superinit', '???')
-                superinitcode = compile(
-                    "lambda self, *args, **kw: MPI(self, cls, args, kw)",
-                    filename,
-                    "eval")
-                superinit = eval(superinitcode,
-                                 {'cls':cls,
-                                  'MPI':_MeMoIZeR_superinit})
-                init = superinit
-
-            newinitcode = compile(
-                "\n"*(init.func_code.co_firstlineno-1) +
-                "lambda self, args, kw: _MeMoIZeR_init(real_init, self, args, kw)",
-                whoami('init', init.func_code.co_filename),
-                'eval')
-            newinit = eval(newinitcode,
-                           {'real_init':init,
-                            '_MeMoIZeR_init':_MeMoIZeR_init},
-                           {})
-            klassdict['__init__'] = lambda self, *args, **kw: newinit(self, args, kw)
-
-            super(Memoized_Metaclass, cls).__init__(name, bases, klassdict)
-            # Now, since klassdict doesn't seem to have affected the class
-            # definition itself, apply klassdict.
-            for attr in klassdict.keys():
-                setattr(cls, attr, klassdict[attr])
-
-def DisableMemoization():
-    global use_memoizer
-    use_memoizer = None
-
-def use_old_memoization():
-    return use_memoizer and not has_metaclass
+            super(Memoized_Metaclass, cls).__init__(name, bases, cls_dict)
+
+            for counter in cls_dict.get('memoizer_counters', []):
+                method_name = counter.method_name
+
+                counter.name = cls.__name__ + '.' + method_name
+                counter.underlying_method = cls_dict[method_name]
+
+                replacement_method = new.instancemethod(counter, None, cls)
+                setattr(cls, method_name, replacement_method)
+
+    def EnableMemoization():
+        global use_memoizer
+        use_memoizer = 1
diff --git a/src/engine/SCons/MemoizeTests.py b/src/engine/SCons/MemoizeTests.py
new file mode 100644 (file)
index 0000000..7102f30
--- /dev/null
@@ -0,0 +1,192 @@
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+import sys
+import unittest
+
+import SCons.Memoize
+
+
+
+class FakeObject:
+
+    __metaclass__ = SCons.Memoize.Memoized_Metaclass
+
+    memoizer_counters = []
+
+    def __init__(self):
+        self._memo = {}
+
+    def _dict_key(self, argument):
+        return argument
+
+    memoizer_counters.append(SCons.Memoize.CountDict('dict', _dict_key))
+
+    def dict(self, argument):
+
+        memo_key = argument
+        try:
+            memo_dict = self._memo['dict']
+        except KeyError:
+            memo_dict = {}
+            self._memo['dict'] = memo_dict
+        else:
+            try:
+                return memo_dict[memo_key]
+            except KeyError:
+                pass
+
+        result = self.compute_dict(argument)
+
+        memo_dict[memo_key] = result
+
+        return result
+
+    memoizer_counters.append(SCons.Memoize.CountValue('value'))
+
+    def value(self):
+
+        try:
+            return self._memo['value']
+        except KeyError:
+            pass
+
+        result = self.compute_value()
+
+        self._memo['value'] = result
+
+        return result
+
+    def get_memoizer_counter(self, name):
+        for mc in self.memoizer_counters:
+            if mc.method_name == name:
+                return mc
+        return  None
+
+class Returner:
+    def __init__(self, result):
+        self.result = result
+        self.calls = 0
+    def __call__(self, *args, **kw):
+        self.calls = self.calls + 1
+        return self.result
+
+
+class CountDictTestCase(unittest.TestCase):
+
+    def test___call__(self):
+        """Calling a Memoized dict method
+        """
+        obj = FakeObject()
+
+        called = []
+
+        fd1 = Returner(1)
+        fd2 = Returner(2)
+
+        obj.compute_dict = fd1
+
+        r = obj.dict(11)
+        assert r == 1, r
+
+        obj.compute_dict = fd2
+
+        r = obj.dict(12)
+        assert r == 2, r
+
+        r = obj.dict(11)
+        assert r == 1, r
+
+        obj.compute_dict = fd1
+
+        r = obj.dict(11)
+        assert r == 1, r
+
+        r = obj.dict(12)
+        assert r == 2, r
+
+        assert fd1.calls == 1, fd1.calls
+        assert fd2.calls == 1, fd2.calls
+
+        c = obj.get_memoizer_counter('dict')
+
+        if SCons.Memoize.has_metaclass:
+            assert c.hit == 3, c.hit
+            assert c.miss == 2, c.miss
+        else:
+            assert c.hit == 0, c.hit
+            assert c.miss == 0, c.miss
+
+
+class CountValueTestCase(unittest.TestCase):
+
+    def test___call__(self):
+        """Calling a Memoized value method
+        """
+        obj = FakeObject()
+
+        called = []
+
+        fv1 = Returner(1)
+        fv2 = Returner(2)
+
+        obj.compute_value = fv1
+
+        r = obj.value()
+        assert r == 1, r
+        r = obj.value()
+        assert r == 1, r
+
+        obj.compute_value = fv2
+
+        r = obj.value()
+        assert r == 1, r
+        r = obj.value()
+        assert r == 1, r
+
+        assert fv1.calls == 1, fv1.calls
+        assert fv2.calls == 0, fv2.calls
+
+        c = obj.get_memoizer_counter('value')
+
+        if SCons.Memoize.has_metaclass:
+            assert c.hit == 3, c.hit
+            assert c.miss == 1, c.miss
+        else:
+            assert c.hit == 0, c.hit
+            assert c.miss == 0, c.miss
+
+
+if __name__ == "__main__":
+    suite = unittest.TestSuite()
+    tclasses = [
+        CountDictTestCase,
+        CountValueTestCase,
+    ]
+    for tclass in tclasses:
+        names = unittest.getTestCaseNames(tclass, 'test_')
+        suite.addTests(map(tclass, names))
+    if not unittest.TextTestRunner().run(suite).wasSuccessful():
+        sys.exit(1)
index 382bca3a9baba76e2e46465cd70e548b96c32bca..08b8d7d813c81380d48ebdb2aa1b42b038a8fba6 100644 (file)
@@ -346,9 +346,20 @@ class DiskChecker:
             self.set_ignore()
 
 def do_diskcheck_match(node, predicate, errorfmt):
-    path = node.abspath
-    if predicate(path):
-        raise TypeError, errorfmt % path
+    result = predicate()
+    try:
+        # If calling the predicate() cached a None value from stat(),
+        # remove it so it doesn't interfere with later attempts to
+        # build this Node as we walk the DAG.  (This isn't a great way
+        # to do this, we're reaching into an interface that doesn't
+        # really belong to us, but it's all about performance, so
+        # for now we'll just document the dependency...)
+        if node._memo['stat'] is None:
+            del node._memo['stat']
+    except (AttributeError, KeyError):
+        pass
+    if result:
+        raise TypeError, errorfmt % node.abspath
 
 def ignore_diskcheck_match(node, predicate, errorfmt):
     pass
@@ -520,6 +531,8 @@ class Base(SCons.Node.Node):
     object identity comparisons.
     """
 
+    memoizer_counters = []
+
     def __init__(self, name, directory, fs):
         """Initialize a generic Node.FS.Base object.
         
@@ -531,6 +544,7 @@ class Base(SCons.Node.Node):
         SCons.Node.Node.__init__(self)
 
         self.name = name
+        self.suffix = SCons.Util.splitext(name)[1]
         self.fs = fs
 
         assert directory, "A directory must be provided"
@@ -550,20 +564,11 @@ class Base(SCons.Node.Node):
         self.cwd = None # will hold the SConscript directory for target nodes
         self.duplicate = directory.duplicate
 
-    def clear(self):
-        """Completely clear a Node.FS.Base object of all its cached
-        state (so that it can be re-evaluated by interfaces that do
-        continuous integration builds).
-        __cache_reset__
-        """
-        SCons.Node.Node.clear(self)
-
     def get_dir(self):
         return self.dir
 
     def get_suffix(self):
-        "__cacheable__"
-        return SCons.Util.splitext(self.name)[1]
+        return self.suffix
 
     def rfile(self):
         return self
@@ -576,9 +581,16 @@ class Base(SCons.Node.Node):
             return self._save_str()
         return self._get_str()
 
+    memoizer_counters.append(SCons.Memoize.CountValue('_save_str'))
+
     def _save_str(self):
-        "__cacheable__"
-        return self._get_str()
+        try:
+            return self._memo['_save_str']
+        except KeyError:
+            pass
+        result = self._get_str()
+        self._memo['_save_str'] = result
+        return result
 
     def _get_str(self):
         if self.duplicate or self.is_derived():
@@ -587,17 +599,20 @@ class Base(SCons.Node.Node):
 
     rstr = __str__
 
+    memoizer_counters.append(SCons.Memoize.CountValue('stat'))
+
     def stat(self):
-        "__cacheable__"
-        try: return self.fs.stat(self.abspath)
-        except os.error: return None
+        try: return self._memo['stat']
+        except KeyError: pass
+        try: result = self.fs.stat(self.abspath)
+        except os.error: result = None
+        self._memo['stat'] = result
+        return result
 
     def exists(self):
-        "__cacheable__"
         return not self.stat() is None
 
     def rexists(self):
-        "__cacheable__"
         return self.rfile().exists()
 
     def getmtime(self):
@@ -640,7 +655,7 @@ class Base(SCons.Node.Node):
         """If this node is in a build path, return the node
         corresponding to its source file.  Otherwise, return
         ourself.
-        __cacheable__"""
+        """
         dir=self.dir
         name=self.name
         while dir:
@@ -707,9 +722,48 @@ class Base(SCons.Node.Node):
     def target_from_source(self, prefix, suffix, splitext=SCons.Util.splitext):
         return self.dir.Entry(prefix + splitext(self.name)[0] + suffix)
 
+    def _Rfindalldirs_key(self, pathlist):
+        return pathlist
+
+    memoizer_counters.append(SCons.Memoize.CountDict('Rfindalldirs', _Rfindalldirs_key))
+
+    def Rfindalldirs(self, pathlist):
+        """
+        Return all of the directories for a given path list, including
+        corresponding "backing" directories in any repositories.
+
+        The Node lookups are relative to this Node (typically a
+        directory), so memoizing result saves cycles from looking
+        up the same path for each target in a given directory.
+        """
+        try:
+            memo_dict = self._memo['Rfindalldirs']
+        except KeyError:
+            memo_dict = {}
+            self._memo['Rfindalldirs'] = memo_dict
+        else:
+            try:
+                return memo_dict[pathlist]
+            except KeyError:
+                pass
+
+        create_dir_relative_to_self = self.Dir
+        result = []
+        for path in pathlist:
+            if isinstance(path, SCons.Node.Node):
+                result.append(path)
+            else:
+                dir = create_dir_relative_to_self(path)
+                result.extend(dir.get_all_rdirs())
+
+        memo_dict[pathlist] = result
+
+        return result
+
     def RDirs(self, pathlist):
         """Search for a list of directories in the Repository list."""
-        return self.fs.Rfindalldirs(pathlist, self.cwd)
+        cwd = self.cwd or self.fs._cwd
+        return cwd.Rfindalldirs(pathlist)
 
 class Entry(Base):
     """This is the class for generic Node.FS entries--that is, things
@@ -723,13 +777,35 @@ class Entry(Base):
         pass
 
     def disambiguate(self):
-        if self.isdir() or self.srcnode().isdir():
+        """
+        """
+        if self.isdir():
             self.__class__ = Dir
             self._morph()
-        else:
+        elif self.isfile():
             self.__class__ = File
             self._morph()
             self.clear()
+        else:
+            # There was nothing on-disk at this location, so look in
+            # the src directory.
+            #
+            # We can't just use self.srcnode() straight away because
+            # that would create an actual Node for this file in the src
+            # directory, and there might not be one.  Instead, use the
+            # dir_on_disk() method to see if there's something on-disk
+            # with that name, in which case we can go ahead and call
+            # self.srcnode() to create the right type of entry.
+            srcdir = self.dir.srcnode()
+            if srcdir != self.dir and \
+               srcdir.entry_exists_on_disk(self.name) and \
+               self.srcnode().isdir():
+                self.__class__ = Dir
+                self._morph()
+            else:
+                self.__class__ = File
+                self._morph()
+                self.clear()
         return self
 
     def rfile(self):
@@ -759,7 +835,8 @@ class Entry(Base):
             return self.get_contents()
         if self.islink():
             return ''             # avoid errors for dangling symlinks
-        raise AttributeError
+        msg = "No such file or directory: '%s'" % self.abspath
+        raise SCons.Errors.UserError, msg
 
     def must_be_a_Dir(self):
         """Called to make sure a Node is a Dir.  Since we're an
@@ -867,14 +944,6 @@ class LocalFS:
             return ''
 
 
-if SCons.Memoize.use_old_memoization():
-    _FSBase = LocalFS
-    class LocalFS(SCons.Memoize.Memoizer, _FSBase):
-        def __init__(self, *args, **kw):
-            apply(_FSBase.__init__, (self,)+args, kw)
-            SCons.Memoize.Memoizer.__init__(self)
-
-
 #class RemoteFS:
 #    # Skeleton for the obvious methods we might need from the
 #    # abstraction layer for a remote filesystem.
@@ -886,6 +955,8 @@ if SCons.Memoize.use_old_memoization():
 
 class FS(LocalFS):
 
+    memoizer_counters = []
+
     def __init__(self, path = None):
         """Initialize the Node.FS subsystem.
 
@@ -897,6 +968,8 @@ class FS(LocalFS):
         """
         if __debug__: logInstanceCreation(self, 'Node.FS')
 
+        self._memo = {}
+
         self.Root = {}
         self.SConstruct_dir = None
         self.CachePath = None
@@ -915,10 +988,6 @@ class FS(LocalFS):
         self.Top.path = '.'
         self.Top.tpath = '.'
         self._cwd = self.Top
-
-    def clear_cache(self):
-        "__cache_reset__"
-        pass
     
     def set_SConstruct_dir(self, dir):
         self.SConstruct_dir = dir
@@ -942,6 +1011,11 @@ class FS(LocalFS):
         raise TypeError, "Tried to lookup %s '%s' as a %s." % \
               (node.__class__.__name__, node.path, klass.__name__)
         
+    def _doLookup_key(self, fsclass, name, directory = None, create = 1):
+        return (fsclass, name, directory)
+
+    memoizer_counters.append(SCons.Memoize.CountDict('_doLookup', _doLookup_key))
+
     def _doLookup(self, fsclass, name, directory = None, create = 1):
         """This method differs from the File and Dir factory methods in
         one important way: the meaning of the directory parameter.
@@ -949,7 +1023,18 @@ class FS(LocalFS):
         name is expected to be an absolute path.  If you try to look up a
         relative path with directory=None, then an AssertionError will be
         raised.
-        __cacheable__"""
+        """
+        memo_key = (fsclass, name, directory)
+        try:
+            memo_dict = self._memo['_doLookup']
+        except KeyError:
+            memo_dict = {}
+            self._memo['_doLookup'] = memo_dict
+        else:
+            try:
+                return memo_dict[memo_key]
+            except KeyError:
+                pass
 
         if not name:
             # This is a stupid hack to compensate for the fact that the
@@ -990,6 +1075,7 @@ class FS(LocalFS):
                     self.Root[''] = directory
 
         if not path_orig:
+            memo_dict[memo_key] = directory
             return directory
 
         last_orig = path_orig.pop()     # strip last element
@@ -1040,6 +1126,9 @@ class FS(LocalFS):
             directory.add_wkid(result)
         else:
             result = self.__checkClass(e, fsclass)
+
+        memo_dict[memo_key] = result
+
         return result 
 
     def _transformPath(self, name, directory):
@@ -1067,7 +1156,8 @@ class FS(LocalFS):
                 # Correct such that '#/foo' is equivalent
                 # to '#foo'.
                 name = name[1:]
-            name = os.path.join('.', os.path.normpath(name))
+            name = os.path.normpath(os.path.join('.', name))
+            return (name, directory)
         elif not directory:
             directory = self._cwd
         return (os.path.normpath(name), directory)
@@ -1116,7 +1206,6 @@ class FS(LocalFS):
         This method will raise TypeError if a directory is found at the
         specified path.
         """
-
         return self.Entry(name, directory, create, File)
     
     def Dir(self, name, directory = None, create = 1):
@@ -1129,7 +1218,6 @@ class FS(LocalFS):
         This method will raise TypeError if a normal file is found at the
         specified path.
         """
-
         return self.Entry(name, directory, create, Dir)
     
     def BuildDir(self, build_dir, src_dir, duplicate=1):
@@ -1155,22 +1243,6 @@ class FS(LocalFS):
                 d = self.Dir(d)
             self.Top.addRepository(d)
 
-    def Rfindalldirs(self, pathlist, cwd):
-        """__cacheable__"""
-        if SCons.Util.is_String(pathlist):
-            pathlist = string.split(pathlist, os.pathsep)
-        if not SCons.Util.is_List(pathlist):
-            pathlist = [pathlist]
-        result = []
-        for path in filter(None, pathlist):
-            if isinstance(path, SCons.Node.Node):
-                result.append(path)
-                continue
-            path, dir = self._transformPath(path, cwd)
-            dir = dir.Dir(path)
-            result.extend(dir.get_all_rdirs())
-        return result
-
     def CacheDebugWrite(self, fmt, target, cachefile):
         self.CacheDebugFP.write(fmt % (target, os.path.split(cachefile)[1]))
 
@@ -1194,7 +1266,10 @@ class FS(LocalFS):
 
         Climb the directory tree, and look up path names
         relative to any linked build directories we find.
-        __cacheable__
+
+        Even though this loops and walks up the tree, we don't memoize
+        the return value because this is really only used to process
+        the command-line targets.
         """
         targets = []
         message = None
@@ -1223,6 +1298,8 @@ class Dir(Base):
     """A class for directories in a file system.
     """
 
+    memoizer_counters = []
+
     NodeInfo = DirNodeInfo
     BuildInfo = DirBuildInfo
 
@@ -1238,7 +1315,7 @@ class Dir(Base):
         Set up this directory's entries and hook it into the file
         system tree.  Specify that directories (this Node) don't use
         signatures for calculating whether they're current.
-        __cache_reset__"""
+        """
 
         self.repositories = []
         self.srcdir = None
@@ -1258,8 +1335,8 @@ class Dir(Base):
         self.get_executor().set_action_list(self.builder.action)
 
     def diskcheck_match(self):
-        diskcheck_match(self, self.fs.isfile,
-                           "File %s found where directory expected.")
+        diskcheck_match(self, self.isfile,
+                        "File %s found where directory expected.")
 
     def __clearRepositoryCache(self, duplicate=None):
         """Called when we change the repository(ies) for a directory.
@@ -1305,13 +1382,19 @@ class Dir(Base):
 
     def getRepositories(self):
         """Returns a list of repositories for this directory.
-        __cacheable__"""
+        """
         if self.srcdir and not self.duplicate:
             return self.srcdir.get_all_rdirs() + self.repositories
         return self.repositories
 
+    memoizer_counters.append(SCons.Memoize.CountValue('get_all_rdirs'))
+
     def get_all_rdirs(self):
-        """__cacheable__"""
+        try:
+            return self._memo['get_all_rdirs']
+        except KeyError:
+            pass
+
         result = [self]
         fname = '.'
         dir = self
@@ -1320,6 +1403,9 @@ class Dir(Base):
                 result.append(rep.Dir(fname))
             fname = dir.name + os.sep + fname
             dir = dir.up()
+
+        self._memo['get_all_rdirs'] = result
+
         return result
 
     def addRepository(self, dir):
@@ -1331,29 +1417,54 @@ class Dir(Base):
     def up(self):
         return self.entries['..']
 
+    def _rel_path_key(self, other):
+        return str(other)
+
+    memoizer_counters.append(SCons.Memoize.CountDict('rel_path', _rel_path_key))
+
     def rel_path(self, other):
         """Return a path to "other" relative to this directory.
-        __cacheable__"""
-        if isinstance(other, Dir):
-            name = []
+        """
+        try:
+            memo_dict = self._memo['rel_path']
+        except KeyError:
+            memo_dict = {}
+            self._memo['rel_path'] = memo_dict
         else:
             try:
-                name = [other.name]
-                other = other.dir
-            except AttributeError:
-                return str(other)
+                return memo_dict[other]
+            except KeyError:
+                pass
+
         if self is other:
-            return name and name[0] or '.'
-        i = 0
-        for x, y in map(None, self.path_elements, other.path_elements):
-            if not x is y:
-                break
-            i = i + 1
-        path_elems = ['..']*(len(self.path_elements)-i) \
-                   + map(lambda n: n.name, other.path_elements[i:]) \
-                   + name
+
+            result = '.'
+
+        elif not other in self.path_elements:
+
+            try:
+                other_dir = other.dir
+            except AttributeError:
+                result = str(other)
+            else:
+                dir_rel_path = self.rel_path(other_dir)
+                if dir_rel_path == '.':
+                    result = other.name
+                else:
+                    result = dir_rel_path + os.sep + other.name
+
+        else:
+
+            i = self.path_elements.index(other) + 1
+
+            path_elems = ['..'] * (len(self.path_elements) - i) \
+                         + map(lambda n: n.name, other.path_elements[i:])
              
-        return string.join(path_elems, os.sep)
+            result = string.join(path_elems, os.sep)
+
+        memo_dict[other] = result
+
+        return result
 
     def get_env_scanner(self, env, kw={}):
         return SCons.Defaults.DirEntryScanner
@@ -1362,10 +1473,13 @@ class Dir(Base):
         return SCons.Defaults.DirEntryScanner
 
     def get_found_includes(self, env, scanner, path):
-        """Return the included implicit dependencies in this file.
-        Cache results so we only scan the file once per path
-        regardless of how many times this information is requested.
-        __cacheable__"""
+        """Return this directory's implicit dependencies.
+
+        We don't bother caching the results because the scan typically
+        shouldn't be requested more than once (as opposed to scanning
+        .h file contents, which can be requested as many times as the
+        files is #included by other files).
+        """
         if not scanner:
             return []
         # Clear cached info for this Dir.  If we already visited this
@@ -1451,7 +1565,6 @@ class Dir(Base):
         return 1
 
     def rdir(self):
-        "__cacheable__"
         if not self.exists():
             norm_name = _my_normcase(self.name)
             for dir in self.dir.get_all_rdirs():
@@ -1500,7 +1613,6 @@ class Dir(Base):
         return self
 
     def entry_exists_on_disk(self, name):
-        """__cacheable__"""
         try:
             d = self.on_disk_entries
         except AttributeError:
@@ -1515,8 +1627,14 @@ class Dir(Base):
             self.on_disk_entries = d
         return d.has_key(_my_normcase(name))
 
+    memoizer_counters.append(SCons.Memoize.CountValue('srcdir_list'))
+
     def srcdir_list(self):
-        """__cacheable__"""
+        try:
+            return self._memo['srcdir_list']
+        except KeyError:
+            pass
+
         result = []
 
         dirname = '.'
@@ -1533,6 +1651,8 @@ class Dir(Base):
             dirname = dir.name + os.sep + dirname
             dir = dir.up()
 
+        self._memo['srcdir_list'] = result
+
         return result
 
     def srcdir_duplicate(self, name):
@@ -1547,8 +1667,23 @@ class Dir(Base):
                     return srcnode
         return None
 
+    def _srcdir_find_file_key(self, filename):
+        return filename
+
+    memoizer_counters.append(SCons.Memoize.CountDict('srcdir_find_file', _srcdir_find_file_key))
+
     def srcdir_find_file(self, filename):
-        """__cacheable__"""
+        try:
+            memo_dict = self._memo['srcdir_find_file']
+        except KeyError:
+            memo_dict = {}
+            self._memo['srcdir_find_file'] = memo_dict
+        else:
+            try:
+                return memo_dict[filename]
+            except KeyError:
+                pass
+
         def func(node):
             if (isinstance(node, File) or isinstance(node, Entry)) and \
                (node.is_derived() or node.is_pseudo_derived() or node.exists()):
@@ -1562,7 +1697,9 @@ class Dir(Base):
             except KeyError: node = rdir.file_on_disk(filename)
             else: node = func(node)
             if node:
-                return node, self
+                result = (node, self)
+                memo_dict[filename] = result
+                return result
 
         for srcdir in self.srcdir_list():
             for rdir in srcdir.get_all_rdirs():
@@ -1570,9 +1707,13 @@ class Dir(Base):
                 except KeyError: node = rdir.file_on_disk(filename)
                 else: node = func(node)
                 if node:
-                    return File(filename, self, self.fs), srcdir
+                    result = (File(filename, self, self.fs), srcdir)
+                    memo_dict[filename] = result
+                    return result
 
-        return None, None
+        result = (None, None)
+        memo_dict[filename] = result
+        return result
 
     def dir_on_disk(self, name):
         if self.entry_exists_on_disk(name):
@@ -1720,12 +1861,14 @@ class File(Base):
     """A class for files in a file system.
     """
 
+    memoizer_counters = []
+
     NodeInfo = FileNodeInfo
     BuildInfo = FileBuildInfo
 
     def diskcheck_match(self):
-        diskcheck_match(self, self.fs.isdir,
-                           "Directory %s found where file expected.")
+        diskcheck_match(self, self.isdir,
+                        "Directory %s found where file expected.")
 
     def __init__(self, name, directory, fs):
         if __debug__: logInstanceCreation(self, 'Node.FS.File')
@@ -1760,7 +1903,7 @@ class File(Base):
     #            'RDirs' : self.RDirs}
 
     def _morph(self):
-        """Turn a file system node into a File object.  __cache_reset__"""
+        """Turn a file system node into a File object."""
         self.scanner_paths = {}
         if not hasattr(self, '_local'):
             self._local = 0
@@ -1789,7 +1932,6 @@ class File(Base):
         self.dir.sconsign().set_entry(self.name, entry)
 
     def get_stored_info(self):
-        "__cacheable__"
         try:
             stored = self.dir.sconsign().get_entry(self.name)
         except (KeyError, OSError):
@@ -1816,14 +1958,37 @@ class File(Base):
     def rel_path(self, other):
         return self.dir.rel_path(other)
 
+    def _get_found_includes_key(self, env, scanner, path):
+        return (id(env), id(scanner), path)
+
+    memoizer_counters.append(SCons.Memoize.CountDict('get_found_includes', _get_found_includes_key))
+
     def get_found_includes(self, env, scanner, path):
         """Return the included implicit dependencies in this file.
         Cache results so we only scan the file once per path
         regardless of how many times this information is requested.
-        __cacheable__"""
-        if not scanner:
-            return []
-        return scanner(self, env, path)
+        """
+        memo_key = (id(env), id(scanner), path)
+        try:
+            memo_dict = self._memo['get_found_includes']
+        except KeyError:
+            memo_dict = {}
+            self._memo['get_found_includes'] = memo_dict
+        else:
+            try:
+                return memo_dict[memo_key]
+            except KeyError:
+                pass
+
+        if scanner:
+            result = scanner(self, env, path)
+            result = map(lambda N: N.disambiguate(), result)
+        else:
+            result = []
+
+        memo_dict[memo_key] = result
+
+        return result
 
     def _createDir(self):
         # ensure that the directories for this node are
@@ -1875,13 +2040,17 @@ class File(Base):
 
     def built(self):
         """Called just after this node is successfully built.
-        __cache_reset__"""
+        """
         # Push this file out to cache before the superclass Node.built()
         # method has a chance to clear the build signature, which it
         # will do if this file has a source scanner.
+        #
+        # We have to clear the memoized values *before* we push it to
+        # cache so that the memoization of the self.exists() return
+        # value doesn't interfere.
+        self.clear_memoized_values()
         if self.fs.CachePath and self.exists():
             CachePush(self, [], None)
-        self.fs.clear_cache()
         SCons.Node.Node.built(self)
 
     def visited(self):
@@ -1926,11 +2095,10 @@ class File(Base):
         return self.fs.build_dir_target_climb(self, self.dir, [self.name])
 
     def is_pseudo_derived(self):
-        "__cacheable__"
         return self.has_src_builder()
 
     def _rmv_existing(self):
-        '__cache_reset__'
+        self.clear_memoized_values()
         Unlink(self, [], None)
         
     def prepare(self):
@@ -1973,29 +2141,36 @@ class File(Base):
         # _rexists attributes so they can be reevaluated.
         self.clear()
 
+    memoizer_counters.append(SCons.Memoize.CountValue('exists'))
+
     def exists(self):
-        "__cacheable__"
+        try:
+            return self._memo['exists']
+        except KeyError:
+            pass
         # Duplicate from source path if we are set up to do this.
         if self.duplicate and not self.is_derived() and not self.linked:
             src = self.srcnode()
-            if src is self:
-                return Base.exists(self)
-            # At this point, src is meant to be copied in a build directory.
-            src = src.rfile()
-            if src.abspath != self.abspath:
-                if src.exists():
-                    self.do_duplicate(src)
-                    # Can't return 1 here because the duplication might
-                    # not actually occur if the -n option is being used.
-                else:
-                    # The source file does not exist.  Make sure no old
-                    # copy remains in the build directory.
-                    if Base.exists(self) or self.islink():
-                        self.fs.unlink(self.path)
-                    # Return None explicitly because the Base.exists() call
-                    # above will have cached its value if the file existed.
-                    return None
-        return Base.exists(self)
+            if not src is self:
+                # At this point, src is meant to be copied in a build directory.
+                src = src.rfile()
+                if src.abspath != self.abspath:
+                    if src.exists():
+                        self.do_duplicate(src)
+                        # Can't return 1 here because the duplication might
+                        # not actually occur if the -n option is being used.
+                    else:
+                        # The source file does not exist.  Make sure no old
+                        # copy remains in the build directory.
+                        if Base.exists(self) or self.islink():
+                            self.fs.unlink(self.path)
+                        # Return None explicitly because the Base.exists() call
+                        # above will have cached its value if the file existed.
+                        self._memo['exists'] = None
+                        return None
+        result = Base.exists(self)
+        self._memo['exists'] = result
+        return result
 
     #
     # SIGNATURE SUBSYSTEM
@@ -2063,7 +2238,6 @@ class File(Base):
         self.binfo = self.gen_binfo(calc)
         return self._cur2()
     def _cur2(self):
-        "__cacheable__"
         if self.always_build:
             return None
         if not self.exists():
@@ -2082,8 +2256,14 @@ class File(Base):
         else:
             return self.is_up_to_date()
 
+    memoizer_counters.append(SCons.Memoize.CountValue('rfile'))
+
     def rfile(self):
-        "__cacheable__"
+        try:
+            return self._memo['rfile']
+        except KeyError:
+            pass
+        result = self
         if not self.exists():
             norm_name = _my_normcase(self.name)
             for dir in self.dir.get_all_rdirs():
@@ -2092,8 +2272,10 @@ class File(Base):
                 if node and node.exists() and \
                    (isinstance(node, File) or isinstance(node, Entry) \
                     or not node.is_derived()):
-                        return node
-        return self
+                        result = node
+                        break
+        self._memo['rfile'] = result
+        return result
 
     def rstr(self):
         return str(self.rfile())
@@ -2121,72 +2303,82 @@ class File(Base):
 
 default_fs = None
 
-def find_file(filename, paths, verbose=None):
+class FileFinder:
+    """
     """
-    find_file(str, [Dir()]) -> [nodes]
+    if SCons.Memoize.use_memoizer:
+        __metaclass__ = SCons.Memoize.Memoized_Metaclass
 
-    filename - a filename to find
-    paths - a list of directory path *nodes* to search in.  Can be
-            represented as a list, a tuple, or a callable that is
-            called with no arguments and returns the list or tuple.
+    memoizer_counters = []
 
-    returns - the node created from the found file.
+    def __init__(self):
+        self._memo = {}
 
-    Find a node corresponding to either a derived file or a file
-    that exists already.
+    def _find_file_key(self, filename, paths, verbose=None):
+        return (filename, paths)
+        
+    memoizer_counters.append(SCons.Memoize.CountDict('find_file', _find_file_key))
 
-    Only the first file found is returned, and none is returned
-    if no file is found.
-    __cacheable__
-    """
-    if verbose:
-        if not SCons.Util.is_String(verbose):
-            verbose = "find_file"
-        if not callable(verbose):
-            verbose = '  %s: ' % verbose
-            verbose = lambda s, v=verbose: sys.stdout.write(v + s)
-    else:
-        verbose = lambda x: x
+    def find_file(self, filename, paths, verbose=None):
+        """
+        find_file(str, [Dir()]) -> [nodes]
+
+        filename - a filename to find
+        paths - a list of directory path *nodes* to search in.  Can be
+                represented as a list, a tuple, or a callable that is
+                called with no arguments and returns the list or tuple.
 
-    if callable(paths):
-        paths = paths()
+        returns - the node created from the found file.
 
-    # Give Entries a chance to morph into Dirs.
-    paths = map(lambda p: p.must_be_a_Dir(), paths)
+        Find a node corresponding to either a derived file or a file
+        that exists already.
 
-    filedir, filename = os.path.split(filename)
-    if filedir:
-        def filedir_lookup(p, fd=filedir):
+        Only the first file found is returned, and none is returned
+        if no file is found.
+        """
+        memo_key = self._find_file_key(filename, paths)
+        try:
+            memo_dict = self._memo['find_file']
+        except KeyError:
+            memo_dict = {}
+            self._memo['find_file'] = memo_dict
+        else:
             try:
-                return p.Dir(fd)
-            except TypeError:
-                # We tried to look up a Dir, but it seems there's already
-                # a File (or something else) there.  No big.
-                return None
-        paths = filter(None, map(filedir_lookup, paths))
-
-    for dir in paths:
-        verbose("looking for '%s' in '%s' ...\n" % (filename, dir))
-        node, d = dir.srcdir_find_file(filename)
-        if node:
-            verbose("... FOUND '%s' in '%s'\n" % (filename, d))
-            return node
-    return None
+                return memo_dict[memo_key]
+            except KeyError:
+                pass
 
-def find_files(filenames, paths):
-    """
-    find_files([str], [Dir()]) -> [nodes]
+        if verbose:
+            if not SCons.Util.is_String(verbose):
+                verbose = "find_file"
+            if not callable(verbose):
+                verbose = '  %s: ' % verbose
+                verbose = lambda s, v=verbose: sys.stdout.write(v + s)
+        else:
+            verbose = lambda x: x
 
-    filenames - a list of filenames to find
-    paths - a list of directory path *nodes* to search in
+        filedir, filename = os.path.split(filename)
+        if filedir:
+            def filedir_lookup(p, fd=filedir):
+                try:
+                    return p.Dir(fd)
+                except TypeError:
+                    # We tried to look up a Dir, but it seems there's
+                    # already a File (or something else) there.  No big.
+                    return None
+            paths = filter(None, map(filedir_lookup, paths))
 
-    returns - the nodes created from the found files.
+        result = None
+        for dir in paths:
+            verbose("looking for '%s' in '%s' ...\n" % (filename, dir))
+            node, d = dir.srcdir_find_file(filename)
+            if node:
+                verbose("... FOUND '%s' in '%s'\n" % (filename, d))
+                result = node
+                break
 
-    Finds nodes corresponding to either derived files or files
-    that exist already.
+        memo_dict[memo_key] = result
 
-    Only the first file found is returned for each filename,
-    and any files that aren't found are ignored.
-    """
-    nodes = map(lambda x, paths=paths: find_file(x, paths), filenames)
-    return filter(None, nodes)
+        return result
+
+find_file = FileFinder().find_file
index 1b38ffee84358642b936f8132cf1cc0417c217a6..434709c4be92ebed8a1f86f3788f839e87b1ee94 100644 (file)
@@ -740,14 +740,22 @@ class FileNodeInfoTestCase(_tempdirTestCase):
 
         test.write('fff', "fff\n")
 
-        assert ni.timestamp != os.path.getmtime('fff'), ni.timestamp
-        assert ni.size != os.path.getsize('fff'), ni.size
+        st = os.stat('fff')
+
+        mtime = st[stat.ST_MTIME]
+        assert ni.timestamp != mtime, (ni.timestamp, mtime)
+        size = st[stat.ST_SIZE]
+        assert ni.size != size, (ni.size, size)
 
         fff.clear()
         ni.update(fff)
 
-        assert ni.timestamp == os.path.getmtime('fff'), ni.timestamp
-        assert ni.size == os.path.getsize('fff'), ni.size
+        st = os.stat('fff')
+
+        mtime = st[stat.ST_MTIME]
+        assert ni.timestamp == mtime, (ni.timestamp, mtime)
+        size = st[stat.ST_SIZE]
+        assert ni.size == size, (ni.size, size)
 
 class FileBuildInfoTestCase(_tempdirTestCase):
     def test___init__(self):
@@ -1219,9 +1227,9 @@ class FSTestCase(_tempdirTestCase):
         exc_caught = 0
         try:
             e.get_contents()
-        except AttributeError:
+        except SCons.Errors.UserError:
             exc_caught = 1
-        assert exc_caught, "Should have caught an AttributError"
+        assert exc_caught, "Should have caught an IOError"
 
         test.write("file", "file\n")
         try:
@@ -1266,18 +1274,18 @@ class FSTestCase(_tempdirTestCase):
         assert t == 0, "expected 0, got %s" % str(t)
 
         test.subdir('tdir2')
-        d = fs.Dir('tdir2')
         f1 = test.workpath('tdir2', 'file1')
         f2 = test.workpath('tdir2', 'file2')
         test.write(f1, 'file1\n')
         test.write(f2, 'file2\n')
-        fs.File(f1)
-        fs.File(f2)
         current_time = float(int(time.time() / 2) * 2)
         t1 = current_time - 4.0
         t2 = current_time - 2.0
         os.utime(f1, (t1 - 2.0, t1))
         os.utime(f2, (t2 - 2.0, t2))
+        d = fs.Dir('tdir2')
+        fs.File(f1)
+        fs.File(f2)
         t = d.get_timestamp()
         assert t == t2, "expected %f, got %f" % (t2, t)
 
@@ -1861,9 +1869,9 @@ class EntryTestCase(_tempdirTestCase):
         exc_caught = None
         try:
             e3n.get_contents()
-        except AttributeError:
+        except SCons.Errors.UserError:
             exc_caught = 1
-        assert exc_caught, "did not catch expected AttributeError"
+        assert exc_caught, "did not catch expected SCons.Errors.UserError"
 
         test.subdir('e4d')
         test.write('e4f', "e4f\n")
@@ -2133,25 +2141,25 @@ class RepositoryTestCase(_tempdirTestCase):
         rep2_sub_d1 = fs.Dir(test.workpath('rep2', 'sub', 'd1'))
         rep3_sub_d1 = fs.Dir(test.workpath('rep3', 'sub', 'd1'))
 
-        r = fs.Rfindalldirs(d1, fs.Top)
+        r = fs.Top.Rfindalldirs((d1,))
         assert r == [d1], map(str, r)
 
-        r = fs.Rfindalldirs([d1, d2], fs.Top)
+        r = fs.Top.Rfindalldirs((d1, d2))
         assert r == [d1, d2], map(str, r)
 
-        r = fs.Rfindalldirs('d1', fs.Top)
+        r = fs.Top.Rfindalldirs(('d1',))
         assert r == [d1, rep1_d1, rep2_d1, rep3_d1], map(str, r)
 
-        r = fs.Rfindalldirs('#d1', fs.Top)
+        r = fs.Top.Rfindalldirs(('#d1',))
         assert r == [d1, rep1_d1, rep2_d1, rep3_d1], map(str, r)
 
-        r = fs.Rfindalldirs('d1', sub)
+        r = sub.Rfindalldirs(('d1',))
         assert r == [sub_d1, rep1_sub_d1, rep2_sub_d1, rep3_sub_d1], map(str, r)
 
-        r = fs.Rfindalldirs('#d1', sub)
+        r = sub.Rfindalldirs(('#d1',))
         assert r == [d1, rep1_d1, rep2_d1, rep3_d1], map(str, r)
 
-        r = fs.Rfindalldirs(['d1', d2], fs.Top)
+        r = fs.Top.Rfindalldirs(('d1', d2))
         assert r == [d1, rep1_d1, rep2_d1, rep3_d1, d2], map(str, r)
 
     def test_rexists(self):
@@ -2223,6 +2231,7 @@ class find_fileTestCase(unittest.TestCase):
         """Testing find_file function"""
         test = TestCmd(workdir = '')
         test.write('./foo', 'Some file\n')
+        test.write('./foo2', 'Another file\n')
         test.subdir('same')
         test.subdir('bar')
         test.write(['bar', 'on_disk'], 'Another file\n')
@@ -2237,7 +2246,7 @@ class find_fileTestCase(unittest.TestCase):
         node_pseudo = fs.File(test.workpath('pseudo'))
         node_pseudo.set_src_builder(1) # Any non-zero value.
 
-        paths = map(fs.Dir, ['.', 'same', './bar'])
+        paths = tuple(map(fs.Dir, ['.', 'same', './bar']))
         nodes = [SCons.Node.FS.find_file('foo', paths)]
         nodes.append(SCons.Node.FS.find_file('baz', paths))
         nodes.append(SCons.Node.FS.find_file('pseudo', paths))
@@ -2261,19 +2270,18 @@ class find_fileTestCase(unittest.TestCase):
         try:
             sio = StringIO.StringIO()
             sys.stdout = sio
-            SCons.Node.FS.find_file('foo', paths, verbose="xyz")
-            expect = "  xyz: looking for 'foo' in '.' ...\n" + \
-                     "  xyz: ... FOUND 'foo' in '.'\n"
+            SCons.Node.FS.find_file('foo2', paths, verbose="xyz")
+            expect = "  xyz: looking for 'foo2' in '.' ...\n" + \
+                     "  xyz: ... FOUND 'foo2' in '.'\n"
             c = sio.getvalue()
             assert c == expect, c
 
             sio = StringIO.StringIO()
             sys.stdout = sio
-            SCons.Node.FS.find_file('baz', paths, verbose=1)
-            expect = "  find_file: looking for 'baz' in '.' ...\n" + \
-                     "  find_file: looking for 'baz' in 'same' ...\n" + \
-                     "  find_file: looking for 'baz' in 'bar' ...\n" + \
-                     "  find_file: ... FOUND 'baz' in 'bar'\n"
+            SCons.Node.FS.find_file('baz2', paths, verbose=1)
+            expect = "  find_file: looking for 'baz2' in '.' ...\n" + \
+                     "  find_file: looking for 'baz2' in 'same' ...\n" + \
+                     "  find_file: looking for 'baz2' in 'bar' ...\n"
             c = sio.getvalue()
             assert c == expect, c
 
@@ -2717,12 +2725,29 @@ class disambiguateTestCase(unittest.TestCase):
         f = efile.disambiguate()
         assert f.__class__ is fff.__class__, f.__class__
 
+        test.subdir('build')
+        test.subdir(['build', 'bdir'])
+        test.write(['build', 'bfile'], "build/bfile\n")
+
         test.subdir('src')
+        test.write(['src', 'bdir'], "src/bdir\n")
+        test.subdir(['src', 'bfile'])
+
         test.subdir(['src', 'edir'])
         test.write(['src', 'efile'], "src/efile\n")
 
         fs.BuildDir(test.workpath('build'), test.workpath('src'))
 
+        build_bdir = fs.Entry(test.workpath('build/bdir'))
+        d = build_bdir.disambiguate()
+        assert d is build_bdir, d
+        assert d.__class__ is ddd.__class__, d.__class__
+
+        build_bfile = fs.Entry(test.workpath('build/bfile'))
+        f = build_bfile.disambiguate()
+        assert f is build_bfile, f
+        assert f.__class__ is fff.__class__, f.__class__
+
         build_edir = fs.Entry(test.workpath('build/edir'))
         d = build_edir.disambiguate()
         assert d.__class__ is ddd.__class__, d.__class__
@@ -2731,6 +2756,10 @@ class disambiguateTestCase(unittest.TestCase):
         f = build_efile.disambiguate()
         assert f.__class__ is fff.__class__, f.__class__
 
+        build_nonexistant = fs.Entry(test.workpath('build/nonexistant'))
+        f = build_nonexistant.disambiguate()
+        assert f.__class__ is fff.__class__, f.__class__
+
 class postprocessTestCase(unittest.TestCase):
     def runTest(self):
         """Test calling the postprocess() method."""
index 42be5b129b3f7f049ba6f53ee13739ac3b175e77..e5d064e581f07575629890ff28ca0dd88ed72386 100644 (file)
@@ -166,6 +166,8 @@ class Node:
     if SCons.Memoize.use_memoizer:
         __metaclass__ = SCons.Memoize.Memoized_Metaclass
 
+    memoizer_counters = []
+
     class Attrs:
         pass
 
@@ -210,6 +212,8 @@ class Node:
         self.post_actions = []
         self.linked = 0 # is this node linked to the build directory?
 
+        self.clear_memoized_values()
+
         # Let the interface in which the build engine is embedded
         # annotate this Node with its own info (like a description of
         # what line in what file created the node, for example).
@@ -223,7 +227,7 @@ class Node:
 
     def get_build_env(self):
         """Fetch the appropriate Environment to build this node.
-        __cacheable__"""
+        """
         return self.get_executor().get_build_env()
 
     def get_build_scanner_path(self, scanner):
@@ -349,8 +353,8 @@ class Node:
         """Completely clear a Node of all its cached state (so that it
         can be re-evaluated by interfaces that do continuous integration
         builds).
-        __reset_cache__
         """
+        self.clear_memoized_values()
         self.executor_cleanup()
         self.del_binfo()
         try:
@@ -361,13 +365,15 @@ class Node:
         self.found_includes = {}
         self.implicit = None
 
+    def clear_memoized_values(self):
+        self._memo = {}
+
     def visited(self):
         """Called just after this node has been visited
         without requiring a build.."""
         pass
 
     def builder_set(self, builder):
-        "__cache_reset__"
         self.builder = builder
 
     def has_builder(self):
@@ -424,7 +430,6 @@ class Node:
         signatures when they are used as source files to other derived files. For
         example: source with source builders are not derived in this sense,
         and hence should not return true.
-        __cacheable__
         """
         return self.has_builder() or self.side_effect
 
@@ -474,7 +479,6 @@ class Node:
             d = filter(lambda x, seen=seen: not seen.has_key(x),
                        n.get_found_includes(env, scanner, path))
             if d:
-                d = map(lambda N: N.disambiguate(), d)
                 deps.extend(d)
                 for n in d:
                     seen[n] = 1
@@ -609,24 +613,34 @@ class Node:
         env = self.env or SCons.Defaults.DefaultEnvironment()
         return env.get_calculator()
 
+    memoizer_counters.append(SCons.Memoize.CountValue('calc_signature'))
+
     def calc_signature(self, calc=None):
         """
         Select and calculate the appropriate build signature for a node.
-        __cacheable__
 
         self - the node
         calc - the signature calculation module
         returns - the signature
         """
+        try:
+            return self._memo['calc_signature']
+        except KeyError:
+            pass
         if self.is_derived():
             import SCons.Defaults
 
             env = self.env or SCons.Defaults.DefaultEnvironment()
             if env.use_build_signature():
-                return self.get_bsig(calc)
+                result = self.get_bsig(calc)
+            else:
+                result = self.get_csig(calc)
         elif not self.rexists():
-            return None
-        return self.get_csig(calc)
+            result = None
+        else:
+            result = self.get_csig(calc)
+        self._memo['calc_signature'] = result
+        return result
 
     def new_ninfo(self):
         return self.NodeInfo(self)
@@ -661,7 +675,6 @@ class Node:
         node's children's signatures.  We expect that they're
         already built and updated by someone else, if that's
         what's wanted.
-        __cacheable__
         """
 
         if calc is None:
@@ -676,7 +689,7 @@ class Node:
         def calc_signature(node, calc=calc):
             return node.calc_signature(calc)
 
-        sources = executor.process_sources(None, self.ignore)
+        sources = executor.get_unignored_sources(self.ignore)
         sourcesigs = executor.process_sources(calc_signature, self.ignore)
 
         depends = self.depends
@@ -767,7 +780,6 @@ class Node:
         return self.exists()
 
     def missing(self):
-        """__cacheable__"""
         return not self.is_derived() and \
                not self.is_pseudo_derived() and \
                not self.linked and \
@@ -850,7 +862,7 @@ class Node:
             self.wkids.append(wkid)
 
     def _children_reset(self):
-        "__cache_reset__"
+        self.clear_memoized_values()
         # We need to let the Executor clear out any calculated
         # bsig info that it's cached so we can re-calculate it.
         self.executor_cleanup()
@@ -881,11 +893,17 @@ class Node:
         else:
             return self.sources + self.depends + self.implicit
 
+    memoizer_counters.append(SCons.Memoize.CountValue('_children_get'))
+
     def _children_get(self):
-        "__cacheable__"
+        try:
+            return self._memo['children_get']
+        except KeyError:
+            pass
         children = self._all_children_get()
         if self.ignore:
             children = filter(self.do_not_ignore, children)
+        self._memo['children_get'] = children
         return children
 
     def all_children(self, scan=1):
@@ -1101,14 +1119,6 @@ else:
 del l
 del ul
 
-if SCons.Memoize.use_old_memoization():
-    _Base = Node
-    class Node(SCons.Memoize.Memoizer, _Base):
-        def __init__(self, *args, **kw):
-            apply(_Base.__init__, (self,)+args, kw)
-            SCons.Memoize.Memoizer.__init__(self)
-
-
 def get_children(node, parent): return node.children()
 def ignore_cycle(node, stack): pass
 def do_nothing(node, parent): pass
index 83798b3d600c660a2d4d59db7267c24ef4d34007..5c30be60194ea1a592c20c0c313e76af8f8d6b1d 100644 (file)
@@ -163,7 +163,10 @@ class Options:
             if option.converter and values.has_key(option.key):
                 value = env.subst('${%s}'%option.key)
                 try:
-                    env[option.key] = option.converter(value)
+                    try:
+                        env[option.key] = option.converter(value)
+                    except TypeError:
+                        env[option.key] = option.converter(value, env)
                 except ValueError, x:
                     raise SCons.Errors.UserError, 'Error converting option: %s\n%s'%(option.key, x)
 
diff --git a/src/engine/SCons/PathList.py b/src/engine/SCons/PathList.py
new file mode 100644 (file)
index 0000000..b757bd3
--- /dev/null
@@ -0,0 +1,217 @@
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+__doc__ = """SCons.PathList
+
+A module for handling lists of directory paths (the sort of things
+that get set as CPPPATH, LIBPATH, etc.) with as much caching of data and
+efficiency as we can while still keeping the evaluation delayed so that we
+Do the Right Thing (almost) regardless of how the variable is specified.
+
+"""
+
+import os
+import string
+
+import SCons.Util
+
+#
+# Variables to specify the different types of entries in a PathList object:
+#
+
+TYPE_STRING_NO_SUBST = 0        # string with no '$'
+TYPE_STRING_SUBST = 1           # string containing '$'
+TYPE_OBJECT = 2                 # other object
+
+def node_conv(obj):
+    """
+    This is the "string conversion" routine that we have our substitutions
+    use to return Nodes, not strings.  This relies on the fact that an
+    EntryProxy object has a get() method that returns the underlying
+    Node that it wraps, which is a bit of architectural dependence
+    that we might need to break or modify in the future in response to
+    additional requirements.
+    """
+    try:
+        get = obj.get
+    except AttributeError:
+        pass
+    else:
+        obj = get()
+    return obj
+
+class _PathList:
+    """
+    An actual PathList object.
+    """
+    def __init__(self, pathlist):
+        """
+        Initializes a PathList object, canonicalizing the input and
+        pre-processing it for quicker substitution later.
+
+        The stored representation of the PathList is a list of tuples
+        containing (type, value), where the "type" is one of the TYPE_*
+        variables defined above.  We distinguish between:
+
+            strings that contain no '$' and therefore need no
+            delayed-evaluation string substitution (we expect that there
+            will be many of these and that we therefore get a pretty
+            big win from avoiding string substitution)
+
+            strings that contain '$' and therefore need substitution
+            (the hard case is things like '${TARGET.dir}/include',
+            which require re-evaluation for every target + source)
+
+            other objects (which may be something like an EntryProxy
+            that needs a method called to return a Node)
+
+        Pre-identifying the type of each element in the PathList up-front
+        and storing the type in the list of tuples is intended to reduce
+        the amount of calculation when we actually do the substitution
+        over and over for each target.
+        """
+        if SCons.Util.is_String(pathlist):
+            pathlist = string.split(pathlist, os.pathsep)
+        elif SCons.Util.is_List(pathlist) or SCons.Util.is_Tuple(pathlist):
+            pathlist = SCons.Util.flatten(pathlist)
+        else:
+            pathlist = [pathlist]
+
+        pl = []
+        for p in pathlist:
+            try:
+                index = string.find(p, '$')
+            except (AttributeError, TypeError):
+                type = TYPE_OBJECT
+            else:
+                if index == -1:
+                    type = TYPE_STRING_NO_SUBST
+                else:
+                    type = TYPE_STRING_SUBST
+            pl.append((type, p))
+
+        self.pathlist = tuple(pl)
+
+    def __len__(self): return len(self.pathlist)
+
+    def __getitem__(self, i): return self.pathlist[i]
+
+    def subst_path(self, env, target, source):
+        """
+        Performs construction variable substitution on a pre-digested
+        PathList for a specific target and source.
+        """
+        result = []
+        for type, value in self.pathlist:
+            if type == TYPE_STRING_SUBST:
+                value = env.subst(value, target=target, source=source,
+                                  conv=node_conv)
+            elif type == TYPE_OBJECT:
+                value = node_conv(value)
+            result.append(value)
+        return tuple(result)
+
+
+class PathListCache:
+    """
+    A class to handle caching of PathList lookups.
+
+    This class gets instantiated once and then deleted from the namespace,
+    so it's used as a Singleton (although we don't enforce that in the
+    usual Pythonic ways).  We could have just made the cache a dictionary
+    in the module namespace, but putting it in this class allows us to
+    use the same Memoizer pattern that we use elsewhere to count cache
+    hits and misses, which is very valuable.
+
+    Lookup keys in the cache are computed by the _PathList_key() method.
+    Cache lookup should be quick, so we don't spend cycles canonicalizing
+    all forms of the same lookup key.  For example, 'x:y' and ['x',
+    'y'] logically represent the same list, but we don't bother to
+    split string representations and treat those two equivalently.
+    (Note, however, that we do, treat lists and tuples the same.)
+
+    The main type of duplication we're trying to catch will come from
+    looking up the same path list from two different clones of the
+    same construction environment.  That is, given
+    
+        env2 = env1.Clone()
+
+    both env1 and env2 will have the same CPPPATH value, and we can
+    cheaply avoid re-parsing both values of CPPPATH by using the
+    common value from this cache.
+    """
+    if SCons.Memoize.use_memoizer:
+        __metaclass__ = SCons.Memoize.Memoized_Metaclass
+
+    memoizer_counters = []
+
+    def __init__(self):
+        self._memo = {}
+
+    def _PathList_key(self, pathlist):
+        """
+        Returns the key for memoization of PathLists.
+
+        Note that we want this to be quick, so we don't canonicalize
+        all forms of the same list.  For example, 'x:y' and ['x', 'y']
+        logically represent the same list, but we're not going to bother
+        massaging strings into canonical lists here.
+
+        The reason
+
+        """
+        if SCons.Util.is_List(pathlist):
+            pathlist = tuple(pathlist)
+        return pathlist
+        
+    memoizer_counters.append(SCons.Memoize.CountDict('PathList', _PathList_key))
+
+    def PathList(self, pathlist):
+        """
+        Returns the cached _PathList object for the specified pathlist,
+        creating and caching a new object as necessary.
+        """
+        pathlist = self._PathList_key(pathlist)
+        try:
+            memo_dict = self._memo['PathList']
+        except KeyError:
+            memo_dict = {}
+            self._memo['PathList'] = memo_dict
+        else:
+            try:
+                return memo_dict[pathlist]
+            except KeyError:
+                pass
+
+        result = _PathList(pathlist)
+
+        memo_dict[pathlist] = result
+
+        return result
+
+PathList = PathListCache().PathList
+
+
+del PathListCache
diff --git a/src/engine/SCons/PathListTests.py b/src/engine/SCons/PathListTests.py
new file mode 100644 (file)
index 0000000..d6fae0e
--- /dev/null
@@ -0,0 +1,145 @@
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+import unittest
+
+import SCons.PathList
+
+
+class subst_pathTestCase(unittest.TestCase):
+
+    def setUp(self):
+
+        class FakeEnvironment:
+            def __init__(self, **kw):
+                self.kw = kw
+            def subst(self, s, target=None, source=None, conv=lambda x: x):
+                if s[0] == '$':
+                    s = s[1:]
+                    if s == 'target':
+                        s = target
+                    elif s == 'source':
+                        s = source
+                    else:
+                        s = self.kw[s]
+                return s
+
+        self.env = FakeEnvironment(AAA = 'aaa')
+
+    def test_object(self):
+        """Test the subst_path() method on an object
+        """
+
+        class A:
+            pass
+
+        a = A()
+
+        pl = SCons.PathList.PathList((a,))
+
+        result = pl.subst_path(self.env, 'y', 'z')
+
+        assert result == (a,), result
+
+    def test_object_get(self):
+        """Test the subst_path() method on an object with a get() method
+        """
+
+        class B:
+            def get(self):
+                return 'b'
+
+        b = B()
+
+        pl = SCons.PathList.PathList((b,))
+
+        result = pl.subst_path(self.env, 'y', 'z')
+
+        assert result == ('b',), result
+
+    def test_string(self):
+        """Test the subst_path() method on a non-substitution string
+        """
+
+        self.env.subst = lambda s, target, source, conv: 'NOT THIS STRING'
+
+        pl = SCons.PathList.PathList(('x'))
+
+        result = pl.subst_path(self.env, 'y', 'z')
+
+        assert result == ('x',), result
+
+    def test_subst(self):
+        """Test the subst_path() method on a substitution string
+        """
+
+        pl = SCons.PathList.PathList(('$AAA',))
+
+        result = pl.subst_path(self.env, 'y', 'z')
+
+        assert result == ('aaa',), result
+
+
+class PathListCacheTestCase(unittest.TestCase):
+
+    def test_no_PathListCache(self):
+        """Make sure the PathListCache class is not visible
+        """
+        try:
+            SCons.PathList.PathListCache
+        except AttributeError:
+            pass
+        else:
+            self.fail("Found PathListCache unexpectedly\n")
+
+
+class PathListTestCase(unittest.TestCase):
+
+    def test_PathList(self):
+        """Test the PathList() entry point
+        """
+
+        x1 = SCons.PathList.PathList(('x',))
+        x2 = SCons.PathList.PathList(['x',])
+
+        assert x1 is x2, (x1, x2)
+
+        x3 = SCons.PathList.PathList('x')
+
+        assert not x1 is x3, (x1, x3)
+
+
+if __name__ == "__main__":
+    suite = unittest.TestSuite()
+    tclasses = [
+        subst_pathTestCase,
+        PathListCacheTestCase,
+        PathListTestCase,
+    ]
+    for tclass in tclasses:
+        names = unittest.getTestCaseNames(tclass, 'test_')
+        suite.addTests(map(tclass, names))
+    if not unittest.TextTestRunner().run(suite).wasSuccessful():
+        sys.exit(1)
index 2ca522b82fd71e37c18153d8d88e0002c13412ef..5d6765d0a3454c5047b8e68b65a1b7fc186d82df 100644 (file)
@@ -179,17 +179,17 @@ class DummyEnvironment(UserDict.UserDict):
     def Dictionary(self, *args):
         return self.data
 
-    def subst(self, strSubst):
+    def subst(self, strSubst, target=None, source=None, conv=None):
         if strSubst[0] == '$':
             return self.data[strSubst[1:]]
         return strSubst
 
-    def subst_list(self, strSubst):
+    def subst_list(self, strSubst, target=None, source=None, conv=None):
         if strSubst[0] == '$':
             return [self.data[strSubst[1:]]]
         return [[strSubst]]
 
-    def subst_path(self, path, target=None, source=None):
+    def subst_path(self, path, target=None, source=None, conv=None):
         if type(path) != type([]):
             path = [path]
         return map(self.subst, path)
@@ -401,9 +401,12 @@ class CScannerTestCase13(unittest.TestCase):
     def runTest(self):
         """Find files in directories named in a substituted environment variable"""
         class SubstEnvironment(DummyEnvironment):
-            def subst(self, arg, test=test):
-                return test.workpath("d1")
-        env = SubstEnvironment(CPPPATH=["blah"])
+            def subst(self, arg, target=None, source=None, conv=None, test=test):
+                if arg == "$blah":
+                    return test.workpath("d1")
+                else:
+                    return arg
+        env = SubstEnvironment(CPPPATH=["$blah"])
         s = SCons.Scanner.C.CScanner()
         path = s.path(env)
         deps = s(env.File('f1.cpp'), env, path)
index 2ea261449d03e78b18ee403016a26e345090ba6e..5a0b3834bc50543cfbab6cf4c230ee473a4d8707 100644 (file)
@@ -46,7 +46,6 @@ def DScanner():
 
 class D(SCons.Scanner.Classic):
     def find_include(self, include, source_dir, path):
-        if callable(path): path=path()
         # translate dots (package separators) to slashes
         inc = string.replace(include, '.', '/')
 
index 8f7a6ce14712bc61161b487627719283faa12edc..31a1e169f86eca5cbec0d9238637831fb02f48d3 100644 (file)
@@ -78,7 +78,6 @@ class F90Scanner(SCons.Scanner.Classic):
         apply(SCons.Scanner.Current.__init__, (self,) + args, kw)
 
     def scan(self, node, env, path=()):
-        "__cacheable__"
 
         # cache the includes list in node so we only scan it once:
         if node.includes != None:
@@ -112,6 +111,8 @@ class F90Scanner(SCons.Scanner.Classic):
         # is actually found in a Repository or locally.
         nodes = []
         source_dir = node.get_dir()
+        if callable(path):
+            path = path()
         for dep in mods_and_includes:
             n, i = self.find_include(dep, source_dir, path)
 
index da4a0238b740e4d2cf0646ad9d7923640f831359..82db69455bcdb084ae05f5daa290de7d10a2a95d 100644 (file)
@@ -232,12 +232,12 @@ class DummyEnvironment:
     def __delitem__(self,key):
         del self.Dictionary()[key]
 
-    def subst(self, arg):
+    def subst(self, arg, target=None, source=None, conv=None):
         if arg[0] == '$':
             return self[arg[1:]]
         return arg
 
-    def subst_path(self, path, target=None, source=None):
+    def subst_path(self, path, target=None, source=None, conv=None):
         if type(path) != type([]):
             path = [path]
         return map(self.subst, path)
@@ -461,10 +461,13 @@ class FortranScannerTestCase14(unittest.TestCase):
 class FortranScannerTestCase15(unittest.TestCase):
     def runTest(self):
         class SubstEnvironment(DummyEnvironment):
-            def subst(self, arg, test=test):
-                return test.workpath("d1")
+            def subst(self, arg, target=None, source=None, conv=None, test=test):
+                if arg == "$junk":
+                    return test.workpath("d1")
+                else:
+                    return arg
         test.write(['d1', 'f2.f'], "      INCLUDE 'fi.f'\n")
-        env = SubstEnvironment(["junk"])
+        env = SubstEnvironment(["$junk"])
         s = SCons.Scanner.Fortran.FortranScan()
         path = s.path(env)
         deps = s(env.File('fff1.f'), env, path)
index 153951db7bada7f34ea456d8c1790fe995b246e5..2332a57f31eaa16e1f63af9fa785533575815dc1 100644 (file)
@@ -199,10 +199,10 @@ class DummyEnvironment:
         else:
             raise KeyError, "Dummy environment only has CPPPATH attribute."
 
-    def subst(self, arg):
+    def subst(self, arg, target=None, source=None, conv=None):
         return arg
 
-    def subst_path(self, path, target=None, source=None):
+    def subst_path(self, path, target=None, source=None, conv=None):
         if type(path) != type([]):
             path = [path]
         return map(self.subst, path)
@@ -411,9 +411,12 @@ class IDLScannerTestCase11(unittest.TestCase):
 class IDLScannerTestCase12(unittest.TestCase):
     def runTest(self):
         class SubstEnvironment(DummyEnvironment):
-            def subst(self, arg, test=test):
-                return test.workpath("d1")
-        env = SubstEnvironment(["blah"])
+            def subst(self, arg, target=None, source=None, conv=None, test=test):
+                if arg == "$blah":
+                    return test.workpath("d1")
+                else:
+                    return arg
+        env = SubstEnvironment(["$blah"])
         s = SCons.Scanner.IDL.IDLScan()
         path = s.path(env)
         deps = s(env.File('t1.idl'), env, path)
index 6451a58a593cfddf64b31a09f8e234c995cf3cdd..d875e6e8e6ce074943d8745ecf0cca0b19216bbe 100644 (file)
@@ -31,13 +31,15 @@ __revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
 
 
 import SCons.Scanner
+import string
+import os.path
 
 def LaTeXScanner(fs = SCons.Node.FS.default_fs):
     """Return a prototype Scanner instance for scanning LaTeX source files"""
     ds = LaTeX(name = "LaTeXScanner",
                suffixes =  '$LATEXSUFFIXES',
                path_variable = 'TEXINPUTS',
-               regex = '\\\\(include|includegraphics(?:\[[^\]]+\])?|input){([^}]*)}',
+               regex = '\\\\(include|includegraphics(?:\[[^\]]+\])?|input|bibliography){([^}]*)}',
                recursive = 0)
     return ds
 
@@ -46,21 +48,75 @@ class LaTeX(SCons.Scanner.Classic):
 
     Unlike most scanners, which use regular expressions that just
     return the included file name, this returns a tuple consisting
-    of the keyword for the inclusion ("include", "includegraphics" or
-    "input"), and then the file name itself.  Base on a quick look at
-    LaTeX documentation, it seems that we need a should append .tex
-    suffix for "include" and "input" keywords, but leave the file name
-    untouched for "includegraphics."
+    of the keyword for the inclusion ("include", "includegraphics",
+    "input", or "bibliography"), and then the file name itself.  
+    Based on a quick look at LaTeX documentation, it seems that we 
+    need a should append .tex suffix for the "include" keywords, 
+    append .tex if there is no extension for the "input" keyword, 
+    but leave the file name untouched for "includegraphics." For
+    the "bibliography" keyword we need to add .bib if there is
+    no extension. (This need to be revisited since if there
+    is no extension for an :includegraphics" keyword latex will 
+    append .ps or .eps to find the file; while pdftex will use 
+    other extensions.)
     """
     def latex_name(self, include):
         filename = include[1]
-        if include[0][:15] != 'includegraphics':
+        if include[0] == 'input':
+            base, ext = os.path.splitext( filename )
+            if ext == "":
+                filename = filename + '.tex'
+        if (include[0] == 'include'):
             filename = filename + '.tex'
+        if include[0] == 'bibliography':
+            base, ext = os.path.splitext( filename )
+            if ext == "":
+                filename = filename + '.bib'
         return filename
     def sort_key(self, include):
         return SCons.Node.FS._my_normcase(self.latex_name(include))
     def find_include(self, include, source_dir, path):
-        if callable(path): path=path()
         i = SCons.Node.FS.find_file(self.latex_name(include),
                                     (source_dir,) + path)
         return i, include
+
+    def scan(self, node, path=()):
+        #
+        # Modify the default scan function to allow for the regular
+        # expression to return a comma separated list of file names
+        # as can be the case with the bibliography keyword.
+        #
+        # cache the includes list in node so we only scan it once:
+        if node.includes != None:
+            includes = node.includes
+        else:
+            includes = self.cre.findall(node.get_contents())
+            node.includes = includes
+
+        # This is a hand-coded DSU (decorate-sort-undecorate, or
+        # Schwartzian transform) pattern.  The sort key is the raw name
+        # of the file as specifed on the #include line (including the
+        # " or <, since that may affect what file is found), which lets
+        # us keep the sort order constant regardless of whether the file
+        # is actually found in a Repository or locally.
+        nodes = []
+        source_dir = node.get_dir()
+        for include in includes:
+            #
+            # Handle multiple filenames in include[1]
+            #
+            inc_list = string.split(include[1],',')
+            for j in range(len(inc_list)):
+                include_local = [include[0],inc_list[j]]
+                n, i = self.find_include(include_local, source_dir, path)
+
+            if n is None:
+                SCons.Warnings.warn(SCons.Warnings.DependencyWarning,
+                                    "No dependency generated for file: %s (included from: %s) -- file not found" % (i, node))
+            else:
+                sortkey = self.sort_key(include)
+                nodes.append((sortkey, n))
+
+        nodes.sort()
+        nodes = map(lambda pair: pair[1], nodes)
+        return nodes
index 45a387a88df4a86a39676c34bd942fff76d4e900..9cfe3ca033aba2786a1bd0b430f7c936ad85417a 100644 (file)
@@ -70,17 +70,17 @@ class DummyEnvironment(UserDict.UserDict):
     def Dictionary(self, *args):
         return self.data
 
-    def subst(self, strSubst):
+    def subst(self, strSubst, target=None, source=None, conv=None):
         if strSubst[0] == '$':
             return self.data[strSubst[1:]]
         return strSubst
 
-    def subst_list(self, strSubst):
+    def subst_list(self, strSubst, target=None, source=None, conv=None):
         if strSubst[0] == '$':
             return [self.data[strSubst[1:]]]
         return [[strSubst]]
 
-    def subst_path(self, path, target=None, source=None):
+    def subst_path(self, path, target=None, source=None, conv=None):
         if type(path) != type([]):
             path = [path]
         return map(self.subst, path)
index 54db9a8b6a7782bddda483f15b1f9f4a443d8da4..e8d1669cd69be6562e201436c26141ade288b7bd 100644 (file)
@@ -80,8 +80,9 @@ def scan(node, env, libpath = ()):
 
     result = []
 
-    if callable(libpath): libpath = libpath()
-    
+    if callable(libpath):
+        libpath = libpath()
+
     find_file = SCons.Node.FS.find_file
     adjustixes = SCons.Util.adjustixes
     for lib in libs:
index bac10b732106962997e5b9bbc37fe98797c24760..47d4afdbcb6120858ea4e0896fa4f15c62deff5d 100644 (file)
@@ -71,7 +71,7 @@ class DummyEnvironment:
     def __delitem__(self,key):
         del self.Dictionary()[key]
 
-    def subst(self, s):
+    def subst(self, s, target=None, source=None, conv=None):
         try:
             if s[0] == '$':
                 return self._dict[s[1:]]
@@ -79,7 +79,7 @@ class DummyEnvironment:
             return ''
         return s
 
-    def subst_path(self, path, target=None, source=None):
+    def subst_path(self, path, target=None, source=None, conv=None):
         if type(path) != type([]):
             path = [path]
         return map(self.subst, path)
@@ -165,12 +165,12 @@ class ProgramScannerTestCase3(unittest.TestCase):
 class ProgramScannerTestCase5(unittest.TestCase):
     def runTest(self):
         class SubstEnvironment(DummyEnvironment):
-            def subst(self, arg, path=test.workpath("d1")):
-                if arg == "blah":
+            def subst(self, arg, target=None, source=None, conv=None, path=test.workpath("d1")):
+                if arg == "$blah":
                     return test.workpath("d1")
                 else:
                     return arg
-        env = SubstEnvironment(LIBPATH=[ "blah" ],
+        env = SubstEnvironment(LIBPATH=[ "$blah" ],
                                LIBS=string.split('l2 l3'))
         s = SCons.Scanner.Prog.ProgramScanner()
         path = s.path(env)
index 29ca0631d2971deba26a01710b4f6681c2bdba35..bd8546f933cb9ff00338be4538f184921bb433e3 100644 (file)
@@ -31,27 +31,23 @@ import SCons.Sig
 import SCons.Scanner
 
 class DummyFS:
-    def __init__(self, search_result=[]):
-        self.search_result = search_result
     def File(self, name):
         return DummyNode(name)
-    def Rfindalldirs(self, pathlist, cwd):
-        return self.search_result + pathlist
 
 class DummyEnvironment(UserDict.UserDict):
     def __init__(self, dict=None, **kw):
         UserDict.UserDict.__init__(self, dict)
         self.data.update(kw)
         self.fs = DummyFS()
-    def subst(self, strSubst):
+    def subst(self, strSubst, target=None, source=None, conv=None):
         if strSubst[0] == '$':
             return self.data[strSubst[1:]]
         return strSubst
-    def subst_list(self, strSubst):
+    def subst_list(self, strSubst, target=None, source=None, conv=None):
         if strSubst[0] == '$':
             return [self.data[strSubst[1:]]]
         return [[strSubst]]
-    def subst_path(self, path, target=None, source=None):
+    def subst_path(self, path, target=None, source=None, conv=None):
         if type(path) != type([]):
             path = [path]
         return map(self.subst, path)
@@ -61,20 +57,24 @@ class DummyEnvironment(UserDict.UserDict):
         return factory or self.fs.File
 
 class DummyNode:
-    def __init__(self, name):
+    def __init__(self, name, search_result=()):
         self.name = name
+        self.search_result = tuple(search_result)
     def rexists(self):
         return 1
     def __str__(self):
         return self.name
+    def Rfindalldirs(self, pathlist):
+        return self.search_result + pathlist
 
 class FindPathDirsTestCase(unittest.TestCase):
     def test_FindPathDirs(self):
         """Test the FindPathDirs callable class"""
 
         env = DummyEnvironment(LIBPATH = [ 'foo' ])
-        env.fs = DummyFS(['xxx'])
+        env.fs = DummyFS()
 
+        dir = DummyNode('dir', ['xxx'])
         fpd = SCons.Scanner.FindPathDirs('LIBPATH')
         result = fpd(env, dir)
         assert str(result) == "('xxx', 'foo')", result
@@ -473,10 +473,11 @@ class ClassicTestCase(unittest.TestCase):
         ret = s.function(n, env, ('foo3',))
         assert ret == ['def'], ret
 
-        # Verify that overall scan results are cached even if individual
-        # results are de-cached
-        ret = s.function(n, env, ('foo2',))
-        assert ret == ['abc'], 'caching inactive; got: %s'%ret
+        # We no longer cache overall scan results, which would be returned
+        # if individual results are de-cached.  If we ever restore that
+        # functionality, this test goes back here.
+        #ret = s.function(n, env, ('foo2',))
+        #assert ret == ['abc'], 'caching inactive; got: %s'%ret
 
         # Verify that it sorts what it finds.
         n.includes = ['xyz', 'uvw']
@@ -501,8 +502,6 @@ class ClassicCPPTestCase(unittest.TestCase):
         s = SCons.Scanner.ClassicCPP("Test", [], None, "")
 
         def _find_file(filename, paths):
-            if callable(paths):
-                paths = paths()
             return paths[0]+'/'+filename
 
         save = SCons.Node.FS.find_file
index 1fd77e525a3a51de834a4e2e36afcf9e09f37a66..679efca5a70d27fac6f96401642dacced1e31423 100644 (file)
@@ -54,25 +54,6 @@ def Scanner(function, *args, **kw):
         return apply(Base, (function,) + args, kw)
 
 
-class _Binder:
-    def __init__(self, bindval):
-        self._val = bindval
-    def __call__(self):
-        return self._val
-    def __str__(self):
-        return str(self._val)
-        #debug: return 'B<%s>'%str(self._val)
-
-BinderDict = {}
-
-def Binder(path):
-    try:
-        return BinderDict[path]
-    except KeyError:
-        b = _Binder(path)
-        BinderDict[path] = b
-        return b
-
 
 class FindPathDirs:
     """A class to bind a specific *PATH variable name to a function that
@@ -80,16 +61,17 @@ class FindPathDirs:
     def __init__(self, variable):
         self.variable = variable
     def __call__(self, env, dir, target=None, source=None, argument=None):
-        # The goal is that we've made caching this unnecessary
-        # because the caching takes place at higher layers.
+        import SCons.PathList
         try:
             path = env[self.variable]
         except KeyError:
             return ()
 
-        path = env.subst_path(path, target=target, source=source)
-        path_tuple = tuple(env.fs.Rfindalldirs(path, dir))
-        return Binder(path_tuple)
+        dir = dir or env.fs._cwd
+        path = SCons.PathList.PathList(path).subst_path(env, target, source)
+        return tuple(dir.Rfindalldirs(path))
+
+
 
 class Base:
     """
@@ -97,9 +79,6 @@ class Base:
     straightforward, single-pass scanning of a single file.
     """
 
-    if SCons.Memoize.use_memoizer:
-        __metaclass__ = SCons.Memoize.Memoized_Metaclass
-
     def __init__(self,
                  function,
                  name = "NONE",
@@ -257,14 +236,6 @@ class Base:
 
     recurse_nodes = _recurse_no_nodes
 
-if SCons.Memoize.use_old_memoization():
-    _Base = Base
-    class Base(SCons.Memoize.Memoizer, _Base):
-        "Cache-backed version of Scanner Base"
-        def __init__(self, *args, **kw):
-            apply(_Base.__init__, (self,)+args, kw)
-            SCons.Memoize.Memoizer.__init__(self)
-
 
 class Selector(Base):
     """
@@ -333,8 +304,6 @@ class Classic(Current):
         apply(Current.__init__, (self,) + args, kw)
 
     def find_include(self, include, source_dir, path):
-        "__cacheable__"
-        if callable(path): path = path()
         n = SCons.Node.FS.find_file(include, (source_dir,) + tuple(path))
         return n, include
 
@@ -342,7 +311,6 @@ class Classic(Current):
         return SCons.Node.FS._my_normcase(include)
 
     def scan(self, node, path=()):
-        "__cacheable__"
 
         # cache the includes list in node so we only scan it once:
         if node.includes != None:
@@ -359,6 +327,8 @@ class Classic(Current):
         # is actually found in a Repository or locally.
         nodes = []
         source_dir = node.get_dir()
+        if callable(path):
+            path = path()
         for include in includes:
             n, i = self.find_include(include, source_dir, path)
 
@@ -384,14 +354,10 @@ class ClassicCPP(Classic):
     the contained filename in group 1.
     """
     def find_include(self, include, source_dir, path):
-        "__cacheable__"
-        if callable(path):
-            path = path()   #kwq: extend callable to find_file...
-
         if include[0] == '"':
-            paths = Binder( (source_dir,) + tuple(path) )
+            paths = (source_dir,) + tuple(path)
         else:
-            paths = Binder( tuple(path) + (source_dir,) )
+            paths = tuple(path) + (source_dir,)
 
         n = SCons.Node.FS.find_file(include[1], paths)
 
index 2c1811280c428e76bff3bf26b091e7412e210a35..6eedbabf880ec8da3a58871a726a7e6550c100d2 100644 (file)
@@ -309,6 +309,7 @@ command_time = 0
 exit_status = 0 # exit status, assume success by default
 repositories = []
 num_jobs = 1 # this is modifed by SConscript.SetJobs()
+delayed_warnings = []
 
 diskcheck_all = SCons.Node.FS.diskcheck_types()
 diskcheck_option_set = None
@@ -671,12 +672,13 @@ class OptParser(OptionParser):
                              "build all Default() targets.")
 
         debug_options = ["count", "dtree", "explain", "findlibs",
-                         "includes", "memoizer", "memory",
-                         "nomemoizer", "objects",
+                         "includes", "memoizer", "memory", "objects",
                          "pdb", "presub", "stacktrace", "stree",
                          "time", "tree"]
 
-        def opt_debug(option, opt, value, parser, debug_options=debug_options):
+        deprecated_debug_options = [ "nomemoizer", ]
+
+        def opt_debug(option, opt, value, parser, debug_options=debug_options, deprecated_debug_options=deprecated_debug_options):
             if value in debug_options:
                 try:
                     if parser.values.debug is None:
@@ -684,6 +686,9 @@ class OptParser(OptionParser):
                 except AttributeError:
                     parser.values.debug = []
                 parser.values.debug.append(value)
+            elif value in deprecated_debug_options:
+                w = "The --debug=%s option is deprecated and has no effect." % value
+                delayed_warnings.append((SCons.Warnings.DeprecatedWarning, w))
             else:
                 raise OptionValueError("Warning:  %s is not a valid debug type" % value)
         self.add_option('--debug', action="callback", type="string",
@@ -945,6 +950,8 @@ class SConscriptSettableOptions:
     
 
 def _main(args, parser):
+    global exit_status
+
     # Here's where everything really happens.
 
     # First order of business:  set up default warnings and and then
@@ -954,6 +961,7 @@ def _main(args, parser):
                          SCons.Warnings.DeprecatedWarning,
                          SCons.Warnings.DuplicateEnvironmentWarning,
                          SCons.Warnings.MissingSConscriptWarning,
+                         SCons.Warnings.NoMetaclassSupportWarning,
                          SCons.Warnings.NoParallelSupportWarning,
                          SCons.Warnings.MisleadingKeywordsWarning, ]
     for warning in default_warnings:
@@ -962,6 +970,9 @@ def _main(args, parser):
     if options.warn:
         _setup_warn(options.warn)
 
+    for warning_type, message in delayed_warnings:
+        SCons.Warnings.warn(warning_type, message)
+
     # Next, we want to create the FS object that represents the outside
     # world's file system, as that's central to a lot of initialization.
     # To do this, however, we need to be in the directory from which we
@@ -1019,7 +1030,8 @@ def _main(args, parser):
             # Give them the options usage now, before we fail
             # trying to read a non-existent SConstruct file.
             parser.print_help()
-            sys.exit(0)
+            exit_status = 0
+            return
         raise SCons.Errors.UserError, "No SConstruct file found."
 
     if scripts[0] == "-":
@@ -1105,7 +1117,6 @@ def _main(args, parser):
         # reading SConscript files and haven't started building
         # things yet, stop regardless of whether they used -i or -k
         # or anything else.
-        global exit_status
         sys.stderr.write("scons: *** %s  Stop.\n" % e)
         exit_status = 2
         sys.exit(exit_status)
@@ -1134,7 +1145,8 @@ def _main(args, parser):
         else:
             print help_text
             print "Use scons -H for help about command-line options."
-        sys.exit(0)
+        exit_status = 0
+        return
 
     # Now that we've read the SConscripts we can set the options
     # that are SConscript settable:
@@ -1285,12 +1297,9 @@ def _main(args, parser):
     count_stats.append(('post-', 'build'))
 
 def _exec_main():
-    all_args = sys.argv[1:]
-    try:
-        all_args = string.split(os.environ['SCONSFLAGS']) + all_args
-    except KeyError:
-            # it's OK if there's no SCONSFLAGS
-            pass
+    sconsflags = os.environ.get('SCONSFLAGS', '')
+    all_args = string.split(sconsflags) + sys.argv[1:]
+
     parser = OptParser()
     global options
     options, args = parser.parse_args(all_args)
@@ -1353,8 +1362,7 @@ def main():
         #SCons.Debug.dumpLoggedInstances('*')
 
     if print_memoizer:
-        print "Memoizer (memory cache) hits and misses:"
-        SCons.Memoize.Dump()
+        SCons.Memoize.Dump("Memoizer (memory cache) hits and misses:")
 
     # Dump any development debug info that may have been enabled.
     # These are purely for internal debugging during development, so
index dc896a00eb0e50fe0a54a532e812ecff8e070028..749be6d789d4b2512b059c48b1e354c2e47d64c5 100644 (file)
@@ -183,6 +183,7 @@ def _SConscript(fs, *files, **kw):
                     # the builder so that it doesn't get built *again*
                     # during the actual build phase.
                     f.build()
+                    f.built()
                     f.builder_set(None)
                     if f.exists():
                         _file_ = open(f.get_abspath(), "r")
@@ -286,9 +287,12 @@ def SConscript_exception(file=sys.stderr):
         # in SCons itself.  Show the whole stack.
         tb = exc_tb
     stack = traceback.extract_tb(tb)
-    type = str(exc_type)
-    if type[:11] == "exceptions.":
-        type = type[11:]
+    try:
+        type = exc_type.__name__
+    except AttributeError:
+        type = str(exc_type)
+        if type[:11] == "exceptions.":
+            type = type[11:]
     file.write('%s: %s:\n' % (type, exc_value))
     for fname, line, func, text in stack:
         file.write('  File "%s", line %d:\n' % (fname, line))
index ce968670ac7136d9bd6badbe81193706319dcf8d..55797dfd73b1e31a15f64d9e353d17957becc3b6 100644 (file)
@@ -44,29 +44,33 @@ import string
 import sys
 import UserList
 
-# Special chicken-and-egg handling of the "--debug=memoizer"
-# and "--debug=nomemoizer" flags:
+# Special chicken-and-egg handling of the "--debug=memoizer" flag:
 #
 # SCons.Memoize contains a metaclass implementation that affects how
-# the other classes are instantiated.  The Memoizer handles optional
-# counting of the hits and misses by using a different, parallel set of
-# functions, so we don't slow down normal operation any more than we
-# have to.  We can also tell it disable memoization completely.
+# the other classes are instantiated.  The Memoizer may add shim methods
+# to classes that have methods that cache computed values in order to
+# count and report the hits and misses.
 #
-# If we wait to enable the counting or disable memoization completely
-# until we've parsed the command line options normally, it will be too
-# late, because the Memoizer will have already analyzed the classes
-# that it's Memoizing and bound the (non-counting) versions of the
-# functions.  So we have to use a special-case, up-front check for
-# the "--debug=memoizer" and "--debug=nomemoizer" flags and do what's
-# appropriate before we import any of the other modules that use it.
+# If we wait to enable the Memoization until after we've parsed the
+# command line options normally, it will be too late, because the Memoizer
+# will have already analyzed the classes that it's Memoizing and decided
+# to not add the shims.  So we use a special-case, up-front check for
+# the "--debug=memoizer" flag and enable Memoizer before we import any
+# of the other modules that use it.
+
 _args = sys.argv + string.split(os.environ.get('SCONSFLAGS', ''))
 if "--debug=memoizer" in _args:
     import SCons.Memoize
-    SCons.Memoize.EnableCounting()
-if "--debug=nomemoizer" in _args:
-    import SCons.Memoize
-    SCons.Memoize.DisableMemoization()
+    import SCons.Warnings
+    try:
+        SCons.Memoize.EnableMemoization()
+    except SCons.Warnings.Warning:
+        # Some warning was thrown (inability to --debug=memoizer on
+        # Python 1.5.2 because it doesn't have metaclasses).  Arrange
+        # for it to be displayed or not after warnings are configured.
+        import Main
+        exc_type, exc_value, tb = sys.exc_info()
+        Main.delayed_warnings.append(exc_type, exc_value)
 del _args
 
 import SCons.Action
@@ -77,6 +81,7 @@ import SCons.Options
 import SCons.Platform
 import SCons.Scanner
 import SCons.SConf
+import SCons.Subst
 import SCons.Tool
 import SCons.Util
 import SCons.Defaults
@@ -127,6 +132,7 @@ call_stack              = _SConscript.call_stack
 
 #
 Action                  = SCons.Action.Action
+AllowSubstExceptions    = SCons.Subst.SetAllowableExceptions
 BoolOption              = SCons.Options.BoolOption
 Builder                 = SCons.Builder.Builder
 Configure               = _SConscript.Configure
index b100473727e873f74ca87c20e86cfd871daa564e..115f7dba427ce17e84cf6d68cef29687e7109b2b 100644 (file)
@@ -44,6 +44,24 @@ _strconv = [SCons.Util.to_String,
             SCons.Util.to_String,
             SCons.Util.to_String_for_signature]
 
+
+
+AllowableExceptions = (IndexError, NameError)
+
+def SetAllowableExceptions(*excepts):
+    global AllowableExceptions
+    AllowableExceptions = filter(None, excepts)
+
+def raise_exception(exception, target, s):
+    name = exception.__class__.__name__
+    msg = "%s `%s' trying to evaluate `%s'" % (name, exception, s)
+    if target:
+        raise SCons.Errors.BuildError, (target[0], msg)
+    else:
+        raise SCons.Errors.UserError, msg
+
+
+
 class Literal:
     """A wrapper for a string.  If you use this object wrapped
     around a string, then it will be interpreted as literal.
@@ -377,21 +395,19 @@ def scons_subst(strSubst, env, mode=SUBST_RAW, target=None, source=None, gvars={
                             key = key[1:-1]
                         try:
                             s = eval(key, self.gvars, lvars)
-                        except AttributeError, e:
-                            raise SCons.Errors.UserError, \
-                                  "Error trying to evaluate `%s': %s" % (s, e)
-                        except (IndexError, NameError, TypeError):
-                            return ''
-                        except SyntaxError,e:
-                            if self.target:
-                                raise SCons.Errors.BuildError, (self.target[0], "Syntax error `%s' trying to evaluate `%s'" % (e,s))
-                            else:
-                                raise SCons.Errors.UserError, "Syntax error `%s' trying to evaluate `%s'" % (e,s)
+                        except KeyboardInterrupt:
+                            raise
+                        except Exception, e:
+                            if e.__class__ in AllowableExceptions:
+                                return ''
+                            raise_exception(e, self.target, s)
                     else:
                         if lvars.has_key(key):
                             s = lvars[key]
                         elif self.gvars.has_key(key):
                             s = self.gvars[key]
+                        elif not NameError in AllowableExceptions:
+                            raise_exception(NameError(key), self.target, s)
                         else:
                             return ''
     
@@ -590,21 +606,19 @@ def scons_subst_list(strSubst, env, mode=SUBST_RAW, target=None, source=None, gv
                             key = key[1:-1]
                         try:
                             s = eval(key, self.gvars, lvars)
-                        except AttributeError, e:
-                            raise SCons.Errors.UserError, \
-                                  "Error trying to evaluate `%s': %s" % (s, e)
-                        except (IndexError, NameError, TypeError):
-                            return
-                        except SyntaxError,e:
-                            if self.target:
-                                raise SCons.Errors.BuildError, (self.target[0], "Syntax error `%s' trying to evaluate `%s'" % (e,s))
-                            else:
-                                raise SCons.Errors.UserError, "Syntax error `%s' trying to evaluate `%s'" % (e,s)
+                        except KeyboardInterrupt:
+                            raise
+                        except Exception, e:
+                            if e.__class__ in AllowableExceptions:
+                                return
+                            raise_exception(e, self.target, s)
                     else:
                         if lvars.has_key(key):
                             s = lvars[key]
                         elif self.gvars.has_key(key):
                             s = self.gvars[key]
+                        elif not NameError in AllowableExceptions:
+                            raise_exception(NameError(), self.target, s)
                         else:
                             return
 
index 4de3348f4e818f9dd3cba0d25ca3297746eb80f0..e8419f1dbe0fbaa1676e67fdf6d726b14716f80b 100644 (file)
@@ -290,7 +290,6 @@ class SubstTestCase(unittest.TestCase):
             "${FFF[0]}",            "G",
             "${FFF[7]}",            "",
             "${NOTHING[1]}",        "",
-            "${NONE[2]}",           "",
 
             # Test various combinations of strings and lists.
             #None,                   '',
@@ -336,7 +335,11 @@ class SubstTestCase(unittest.TestCase):
         while cases:
             input, expect = cases[:2]
             expect = cvt(expect)
-            result = apply(scons_subst, (input, env), kwargs)
+            try:
+                result = apply(scons_subst, (input, env), kwargs)
+            except Exception, e:
+                print "    input %s generated %s %s" % (repr(input), e.__class__.__name__, str(e))
+                failed = failed + 1
             if result != expect:
                 if failed == 0: print
                 print "    input %s => %s did not match %s" % (repr(input), repr(result), repr(expect))
@@ -459,8 +462,8 @@ class SubstTestCase(unittest.TestCase):
             scons_subst('${foo.bar}', env, gvars={'foo':Foo()})
         except SCons.Errors.UserError, e:
             expect = [
-                "Error trying to evaluate `${foo.bar}': bar",
-                "Error trying to evaluate `${foo.bar}': Foo instance has no attribute 'bar'",
+                "AttributeError `bar' trying to evaluate `${foo.bar}'",
+                "AttributeError `Foo instance has no attribute 'bar'' trying to evaluate `${foo.bar}'",
             ]
             assert str(e) in expect, e
         else:
@@ -470,9 +473,44 @@ class SubstTestCase(unittest.TestCase):
         try:
             scons_subst('$foo.bar.3.0', env)
         except SCons.Errors.UserError, e:
-            expect1 = "Syntax error `invalid syntax' trying to evaluate `$foo.bar.3.0'"
-            expect2 = "Syntax error `invalid syntax (line 1)' trying to evaluate `$foo.bar.3.0'"
-            assert str(e) in [expect1, expect2], e
+            expect = [
+                # Python 1.5
+                "SyntaxError `invalid syntax' trying to evaluate `$foo.bar.3.0'",
+                # Python 2.2, 2.3, 2.4
+                "SyntaxError `invalid syntax (line 1)' trying to evaluate `$foo.bar.3.0'",
+                # Python 2.5
+                "SyntaxError `invalid syntax (<string>, line 1)' trying to evaluate `$foo.bar.3.0'",
+            ]
+            assert str(e) in expect, e
+        else:
+            raise AssertionError, "did not catch expected UserError"
+
+        # Test that we handle type errors 
+        try:
+            scons_subst("${NONE[2]}", env, gvars={'NONE':None})
+        except SCons.Errors.UserError, e:
+            expect = [
+                # Python 1.5, 2.2, 2.3, 2.4
+                "TypeError `unsubscriptable object' trying to evaluate `${NONE[2]}'",
+                # Python 2.5 and later
+                "TypeError `'NoneType' object is unsubscriptable' trying to evaluate `${NONE[2]}'",
+            ]
+            assert str(e) in expect, e
+        else:
+            raise AssertionError, "did not catch expected UserError"
+
+        try:
+            def func(a, b, c):
+                pass
+            scons_subst("${func(1)}", env, gvars={'func':func})
+        except SCons.Errors.UserError, e:
+            expect = [
+                # Python 1.5
+                "TypeError `not enough arguments; expected 3, got 1' trying to evaluate `${func(1)}'",
+                # Python 2.2, 2.3, 2.4, 2.5
+                "TypeError `func() takes exactly 3 arguments (1 given)' trying to evaluate `${func(1)}'"
+            ]
+            assert str(e) in expect, repr(str(e))
         else:
             raise AssertionError, "did not catch expected UserError"
 
@@ -933,8 +971,8 @@ class SubstTestCase(unittest.TestCase):
             scons_subst_list('${foo.bar}', env, gvars={'foo':Foo()})
         except SCons.Errors.UserError, e:
             expect = [
-                "Error trying to evaluate `${foo.bar}': bar",
-                "Error trying to evaluate `${foo.bar}': Foo instance has no attribute 'bar'",
+                "AttributeError `bar' trying to evaluate `${foo.bar}'",
+                "AttributeError `Foo instance has no attribute 'bar'' trying to evaluate `${foo.bar}'",
             ]
             assert str(e) in expect, e
         else:
@@ -944,9 +982,12 @@ class SubstTestCase(unittest.TestCase):
         try:
             scons_subst_list('$foo.bar.3.0', env)
         except SCons.Errors.UserError, e:
-            expect1 = "Syntax error `invalid syntax' trying to evaluate `$foo.bar.3.0'"
-            expect2 = "Syntax error `invalid syntax (line 1)' trying to evaluate `$foo.bar.3.0'"
-            assert str(e) in [expect1, expect2], e
+            expect = [
+                "SyntaxError `invalid syntax' trying to evaluate `$foo.bar.3.0'",
+                "SyntaxError `invalid syntax (line 1)' trying to evaluate `$foo.bar.3.0'",
+                "SyntaxError `invalid syntax (<string>, line 1)' trying to evaluate `$foo.bar.3.0'",
+            ]
+            assert str(e) in expect, e
         else:
             raise AssertionError, "did not catch expected SyntaxError"
 
index 7cdecf3bb2770041a3e18086e31a5cf1b2a24886..2ea3f0dad857037d58804b34dfa48ff7b7e92ded 100644 (file)
@@ -240,7 +240,7 @@ class Task:
         for t in self.targets:
             t.disambiguate().set_state(SCons.Node.executing)
             for s in t.side_effects:
-                s.set_state(SCons.Node.pending)
+                s.set_state(SCons.Node.executing)
 
     def make_ready_current(self):
         """Mark all targets in a task ready for execution if any target
@@ -256,7 +256,7 @@ class Task:
                 self.out_of_date.append(t)
                 t.set_state(SCons.Node.executing)
                 for s in t.side_effects:
-                    s.set_state(SCons.Node.pending)
+                    s.set_state(SCons.Node.executing)
 
     make_ready = make_ready_current
 
@@ -268,7 +268,7 @@ class Task:
                 parents[p] = parents.get(p, 0) + 1
         for t in self.targets:
             for s in t.side_effects:
-                if s.get_state() == SCons.Node.pending:
+                if s.get_state() == SCons.Node.executing:
                     s.set_state(SCons.Node.no_state)
                     for p in s.waiting_parents.keys():
                         if not parents.has_key(p):
@@ -515,12 +515,11 @@ class Taskmaster:
                     T.write(' waiting on unfinished children:\n    %s\n' % c)
                 continue
 
-            # Skip this node if it has side-effects that are
-            # currently being built:
-            side_effects = reduce(lambda E,N:
-                                  E or N.get_state() == SCons.Node.executing,
-                                  node.side_effects,
-                                  0)
+            # Skip this node if it has side-effects that are currently being
+            # built  themselves or waiting for something else being built.
+            side_effects = filter(lambda N:
+                                  N.get_state() == SCons.Node.executing,
+                                  node.side_effects)
             if side_effects:
                 map(lambda n, P=node: n.add_to_waiting_s_e(P), side_effects)
                 if S: S.side_effects = S.side_effects + 1
index 8d71d71f01a68109ed62b0350ab68385a7baad25..4fefb9d201a1085eeebd4b3bb76a3939131e4ae1 100644 (file)
@@ -374,7 +374,7 @@ class TaskmasterTestCase(unittest.TestCase):
         tm = SCons.Taskmaster.Taskmaster([n1,n2,n3,n4,n5])
         t = tm.next_task()
         assert t.get_target() == n1
-        assert n4.state == SCons.Node.pending, n4.state
+        assert n4.state == SCons.Node.executing, n4.state
         t.executed()
         t.postprocess()
         t = tm.next_task()
index f2a221b455514d3c34c8b313229d0ddd91c822b9..1a5952574188034408177c76411236718b0f9ece 100644 (file)
@@ -37,11 +37,11 @@ __revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
 from SCons.Tool.PharLapCommon import addPharLapPaths
 import SCons.Util
 
-import as
+as_module = __import__('as', globals(), locals(), [])
 
 def generate(env):
     """Add Builders and construction variables for ar to an Environment."""
-    as.generate(env)
+    as_module.generate(env)
 
     env['AS']        = '386asm'
     env['ASFLAGS']   = SCons.Util.CLVar('')
index 51dfae18cc1182140c651acd982caedd3a88ae1f..179159ed0a334c69b585769e6f2315e50347a192 100644 (file)
@@ -66,7 +66,7 @@ def generate(env):
 
     env['DVIPDF']      = 'dvipdf'
     env['DVIPDFFLAGS'] = SCons.Util.CLVar('')
-    env['DVIPDFCOM']   = '$DVIPDF $DVIPDFFLAGS $SOURCE $TARGET'
+    env['DVIPDFCOM']   = 'cd ${TARGET.dir} && $DVIPDF $DVIPDFFLAGS ${SOURCE.file} ${TARGET.file}'
 
     # Deprecated synonym.
     env['PDFCOM']      = ['$DVIPDFCOM']
index b987ea1219a8b3e14da002db7790c989572ee9bb..9996fc2dfed4f09edb4ebfa7bfbc06716b774fd2 100644 (file)
@@ -58,7 +58,9 @@ def generate(env):
     
     env['DVIPS']      = 'dvips'
     env['DVIPSFLAGS'] = SCons.Util.CLVar('')
-    env['PSCOM']      = '$DVIPS $DVIPSFLAGS -o $TARGET $SOURCE'
+    # I'm not quite sure I got the directories and filenames right for build_dir
+    # We need to be in the correct directory for the sake of latex \includegraphics eps included files.
+    env['PSCOM']      = 'cd ${TARGET.dir} && $DVIPS $DVIPSFLAGS -o ${TARGET.file} ${SOURCE.file}'
     env['PSPREFIX'] = ''
     env['PSSUFFIX'] = '.ps'
 
index 3b354240fe0fbe047a41f7902ff816f5a9a35486..e44a28dde829fe44f51c70bcdca32c42f809a0e3 100644 (file)
@@ -33,13 +33,13 @@ selection method.
 
 __revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
 
-import as
+as_module = __import__('as', globals(), locals(), [])
 
 assemblers = ['as', 'gas']
 
 def generate(env):
     """Add Builders and construction variables for as to an Environment."""
-    as.generate(env)
+    as_module.generate(env)
 
     env['AS']        = env.Detect(assemblers) or 'as'
 
index 72371b3a587c5ac2f4f5bf0dcc744975004489b3..c4934c321b30fb7353b9eb77e9e9cbba78fcfa23 100644 (file)
@@ -64,7 +64,7 @@ def generate(env):
 
     env['LATEX']        = 'latex'
     env['LATEXFLAGS']   = SCons.Util.CLVar('')
-    env['LATEXCOM']     = '$LATEX $LATEXFLAGS $SOURCE'
+    env['LATEXCOM']     = 'cd ${TARGET.dir} && $LATEX $LATEXFLAGS ${SOURCE.file}'
     env['LATEXRETRIES'] = 3
 
 def exists(env):
index 4458183c2c71c956591ee3a2bf7d67aa5b751a04..4b90e3dcb47e21138926c3d33ce4b1dc21a01b54 100644 (file)
@@ -222,8 +222,14 @@ def generate(env):
     env['LDMODULECOM'] = compositeLinkAction
 
 def exists(env):
+    platform = env.get('PLATFORM', '')
     if SCons.Tool.msvs.is_msvs_installed():
         # there's at least one version of MSVS installed.
         return 1
-    else:
+    elif platform in ('win32', 'cygwin'):
+        # Only explicitly search for a 'link' executable on Windows
+        # systems.  Some other systems (e.g. Ubuntu Linux) have an
+        # executable named 'link' and we don't want that to make SCons
+        # think Visual Studio is installed.
         return env.Detect('link')
+    return None
index 1c0f3fa236fdc937c4db00878d8b4f1a875e5219..be155bda096eb03d0d04d7e04f1f09bdcfc9b888 100644 (file)
@@ -55,6 +55,7 @@ The default value expands expands to the appropriate
 Microsoft Visual C++ command-line options
 when the &cv-PCH; construction variable is set.
 </summary>
+</cvar>
 
 <cvar name="CCPDBFLAGS">
 <summary>
@@ -93,6 +94,7 @@ the &cv-CCPDBFLAGS; variable as follows:
 env['CCPDBFLAGS'] = '/Zi /Fd${TARGET}.pdb'
 </example>
 </summary>
+</cvar>
 
 <cvar name="PCH">
 <summary>
index acf67b27bf0d3ea6fa3850efee6477004240f28d..97420a8ae035aff25b0ed545a44667d80027eb5c 100644 (file)
@@ -67,7 +67,7 @@ def generate(env):
 
     env['PDFLATEX']      = 'pdflatex'
     env['PDFLATEXFLAGS'] = SCons.Util.CLVar('')
-    env['PDFLATEXCOM']   = '$PDFLATEX $PDFLATEXFLAGS $SOURCE'
+    env['PDFLATEXCOM']   = 'cd ${TARGET.dir} && $PDFLATEX $PDFLATEXFLAGS ${SOURCE.file}'
     env['LATEXRETRIES']  = 3
 
 def exists(env):
index ddf5a23125afcdb25d8426171788a4d8bd9e18ae..e740fac22d48e281263f8b27cad80ef46ab71ad1 100644 (file)
@@ -82,17 +82,13 @@ def generate(env):
 
     env['PDFTEX']      = 'pdftex'
     env['PDFTEXFLAGS'] = SCons.Util.CLVar('')
-    env['PDFTEXCOM']   = '$PDFTEX $PDFTEXFLAGS $SOURCE'
+    env['PDFTEXCOM']   = 'cd ${TARGET.dir} && $PDFTEX $PDFTEXFLAGS ${SOURCE.file}'
 
     # Duplicate from latex.py.  If latex.py goes away, then this is still OK.
     env['PDFLATEX']      = 'pdflatex'
     env['PDFLATEXFLAGS'] = SCons.Util.CLVar('')
-    env['PDFLATEXCOM']   = '$PDFLATEX $PDFLATEXFLAGS $SOURCE'
+    env['PDFLATEXCOM']   = 'cd ${TARGET.dir} && $PDFLATEX $PDFLATEXFLAGS ${SOURCE.file}'
     env['LATEXRETRIES']  = 3
 
-    env['BIBTEX']      = 'bibtex'
-    env['BIBTEXFLAGS'] = SCons.Util.CLVar('')
-    env['BIBTEXCOM']   = '$BIBTEX $BIBTEXFLAGS ${SOURCE.base}'
-
 def exists(env):
     return env.Detect('pdftex')
index ed066a7cc6b57a5a7cf79c382bc617bea64ded46..a8e12a29fcb2c835b758f157633ecc1ed53b698f 100644 (file)
@@ -49,19 +49,20 @@ def swigSuffixEmitter(env, source):
     else:
         return '$SWIGCFILESUFFIX'
 
-_reSwig = re.compile(r"%include\s+(\S+)")
+_reInclude = re.compile(r'%include\s+(\S+)')
+_reModule = re.compile(r'%module\s+(.+)')
 
 def recurse(path, searchPath):
-    global _reSwig
+    global _reInclude
     f = open(path)
     try: contents = f.read()
     finally: f.close()
 
     found = []
     # Better code for when we drop Python 1.5.2.
-    #for m in _reSwig.finditer(contents):
+    #for m in _reInclude.finditer(contents):
     #    fname = m.group(1)
-    for fname in _reSwig.findall(contents):
+    for fname in _reInclude.findall(contents):
         for dpath in searchPath:
             absPath = os.path.join(dpath, fname)
             if os.path.isfile(absPath):
@@ -88,7 +89,7 @@ def _swigEmitter(target, source, env):
             f = open(src)
             try:
                 for l in f.readlines():
-                    m = re.match("%module (.+)", l)
+                    m = _reModule.match(l)
                     if m:
                         mname = m.group(1)
             finally:
index d61395881b66e65f90a7ef9bb7a3d9d8be4dc73d..0329667f257acabdee8ede01653b2f1c5fca315a 100644 (file)
@@ -43,7 +43,13 @@ import SCons.Node.FS
 import SCons.Util
 
 warning_rerun_re = re.compile("^LaTeX Warning:.*Rerun", re.MULTILINE)
-undefined_references_re = re.compile("^LaTeX Warning:.*undefined references", re.MULTILINE)
+
+rerun_citations_str = "^LaTeX Warning:.*\n.*Rerun to get citations correct"
+rerun_citations_re = re.compile(rerun_citations_str, re.MULTILINE)
+
+undefined_references_str = '(^LaTeX Warning:.*undefined references)|(^Package \w+ Warning:.*undefined citations)'
+undefined_references_re = re.compile(undefined_references_str, re.MULTILINE)
+
 openout_aux_re = re.compile(r"\\openout.*`(.*\.aux)'")
 
 # An Action sufficient to build any generic tex file.
@@ -63,7 +69,8 @@ def InternalLaTeXAuxAction(XXXLaTeXAction, target = None, source= None, env=None
     """A builder for LaTeX files that checks the output in the aux file
     and decides how many times to use LaTeXAction, and BibTeXAction."""
 
-    basename, ext = SCons.Util.splitext(str(target[0]))
+    basename = SCons.Util.splitext(str(source[0]))[0]
+    basedir = os.path.split(str(source[0]))[0]
 
     # Run LaTeX once to generate a new aux file.
     XXXLaTeXAction(target, source, env)
@@ -82,11 +89,11 @@ def InternalLaTeXAuxAction(XXXLaTeXAction, target = None, source= None, env=None
 
     # Now decide if bibtex will need to be run.
     for auxfilename in auxfiles:
-        if os.path.exists(auxfilename):
-            content = open(auxfilename, "rb").read()
+        if os.path.exists(os.path.join(basedir, auxfilename)):
+            content = open(os.path.join(basedir, auxfilename), "rb").read()
             if string.find(content, "bibdata") != -1:
                 bibfile = env.fs.File(basename)
-                BibTeXAction(None, bibfile, env)
+                BibTeXAction(bibfile, bibfile, env)
                 break
 
     # Now decide if makeindex will need to be run.
@@ -94,7 +101,13 @@ def InternalLaTeXAuxAction(XXXLaTeXAction, target = None, source= None, env=None
     if os.path.exists(idxfilename):
         idxfile = env.fs.File(basename)
         # TODO: if ( idxfile has changed) ...
-        MakeIndexAction(None, idxfile, env)
+        MakeIndexAction(idxfile, idxfile, env)
+        XXXLaTeXAction(target, source, env)
+
+    # Now decide if latex will need to be run again due to table of contents.
+    tocfilename = basename + '.toc'
+    if os.path.exists(tocfilename):
+        # TODO: if ( tocfilename has changed) ...
         XXXLaTeXAction(target, source, env)
 
     # Now decide if latex needs to be run yet again.
@@ -104,6 +117,7 @@ def InternalLaTeXAuxAction(XXXLaTeXAction, target = None, source= None, env=None
             break
         content = open(logfilename, "rb").read()
         if not warning_rerun_re.search(content) and \
+           not rerun_citations_re.search(content) and \
            not undefined_references_re.search(content):
             break
         XXXLaTeXAction(target, source, env)
@@ -135,9 +149,12 @@ def TeXLaTeXFunction(target = None, source= None, env=None):
 def tex_emitter(target, source, env):
     base = SCons.Util.splitext(str(source[0]))[0]
     target.append(base + '.aux')
+    env.Precious(base + '.aux')
     target.append(base + '.log')
     for f in source:
         content = f.get_contents()
+        if string.find(content, r'\tableofcontents') != -1:
+            target.append(base + '.toc')
         if string.find(content, r'\makeindex') != -1:
             target.append(base + '.ilg')
             target.append(base + '.ind')
@@ -151,7 +168,10 @@ def tex_emitter(target, source, env):
     if os.path.exists(logfilename):
         content = open(logfilename, "rb").read()
         aux_files = openout_aux_re.findall(content)
-        target.extend(filter(lambda f, b=base+'.aux': f != b, aux_files))
+        aux_files = filter(lambda f, b=base+'.aux': f != b, aux_files)
+        dir = os.path.split(base)[0]
+        aux_files = map(lambda f, d=dir: d+os.sep+f, aux_files)
+        target.extend(aux_files)
 
     return (target, source)
 
@@ -194,21 +214,21 @@ def generate(env):
 
     env['TEX']      = 'tex'
     env['TEXFLAGS'] = SCons.Util.CLVar('')
-    env['TEXCOM']   = '$TEX $TEXFLAGS $SOURCE'
+    env['TEXCOM']   = 'cd ${TARGET.dir} && $TEX $TEXFLAGS ${SOURCE.file}'
 
     # Duplicate from latex.py.  If latex.py goes away, then this is still OK.
     env['LATEX']        = 'latex'
     env['LATEXFLAGS']   = SCons.Util.CLVar('')
-    env['LATEXCOM']     = '$LATEX $LATEXFLAGS $SOURCE'
+    env['LATEXCOM']     = 'cd ${TARGET.dir} && $LATEX $LATEXFLAGS ${SOURCE.file}'
     env['LATEXRETRIES'] = 3
 
     env['BIBTEX']      = 'bibtex'
     env['BIBTEXFLAGS'] = SCons.Util.CLVar('')
-    env['BIBTEXCOM']   = '$BIBTEX $BIBTEXFLAGS ${SOURCE.base}'
+    env['BIBTEXCOM']   = 'cd ${TARGET.dir} && $BIBTEX $BIBTEXFLAGS ${SOURCE.filebase}'
 
     env['MAKEINDEX']      = 'makeindex'
     env['MAKEINDEXFLAGS'] = SCons.Util.CLVar('')
-    env['MAKEINDEXCOM']   = '$MAKEINDEX $MAKEINDEXFLAGS $SOURCES'
+    env['MAKEINDEXCOM']   = 'cd ${TARGET.dir} && $MAKEINDEX $MAKEINDEXFLAGS ${SOURCE.file}'
 
 def exists(env):
     return env.Detect('tex')
index 123f36b81c0effea572f4ac98bced2c9fdcc90b2..27614bf9a9d14fe5cf864b0102732f76595caa71 100644 (file)
@@ -54,6 +54,9 @@ class DuplicateEnvironmentWarning(Warning):
 class MissingSConscriptWarning(Warning):
     pass
 
+class NoMetaclassSupportWarning(Warning):
+    pass
+
 class NoParallelSupportWarning(Warning):
     pass
 
index 9783f2352a19607fe7dff9104e84a823b05e3c3a..f324ed45e851db352ffad7dcdef36d6aea0894ba 100644 (file)
@@ -1,2 +1,3 @@
 scons
 sconsign
+scons-time
diff --git a/src/script/scons-time.py b/src/script/scons-time.py
new file mode 100644 (file)
index 0000000..1867d44
--- /dev/null
@@ -0,0 +1,1437 @@
+#!/usr/bin/env python
+#
+# scons-time - run SCons timings and collect statistics
+#
+# A script for running a configuration through SCons with a standard
+# set of invocations to collect timing and memory statistics and to
+# capture the results in a consistent set of output files for display
+# and analysis.
+#
+
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+import getopt
+import glob
+import os
+import re
+import shutil
+import string
+import sys
+import tempfile
+import time
+
+class Plotter:
+    def increment_size(self, largest):
+        """
+        Return the size of each horizontal increment line for a specified
+        maximum value.  This returns a value that will provide somewhere
+        between 5 and 9 horizontal lines on the graph, on some set of
+        boundaries that are multiples of 10/100/1000/etc.
+        """
+        i = largest / 5
+        if not i:
+            return largest
+        multiplier = 1
+        while i >= 10:
+            i = i / 10
+            multiplier = multiplier * 10
+        return i * multiplier
+
+    def max_graph_value(self, largest):
+        # Round up to next integer.
+        largest = int(largest) + 1
+        increment = self.increment_size(largest)
+        return ((largest + increment - 1) / increment) * increment
+
+class Line:
+    def __init__(self, points, type, title, label, comment, fmt="%s %s"):
+        self.points = points
+        self.type = type
+        self.title = title
+        self.label = label
+        self.comment = comment
+        self.fmt = fmt
+
+    def print_label(self, inx, x, y):
+        if self.label:
+            print 'set label %s "%s" at %s,%s right' % (inx, self.label, x, y)
+
+    def plot_string(self):
+        if self.title:
+            title_string = 'title "%s"' % self.title
+        else:
+            title_string = 'notitle'
+        return "'-' %s with lines lt %s" % (title_string, self.type)
+
+    def print_points(self, fmt=None):
+        if fmt is None:
+            fmt = self.fmt
+        if self.comment:
+            print '# %s' % self.comment
+        for x, y in self.points:
+            print fmt % (x, y)
+        print 'e'
+
+    def get_x_values(self):
+        return [ p[0] for p in self.points ]
+
+    def get_y_values(self):
+        return [ p[1] for p in self.points ]
+
+class Gnuplotter(Plotter):
+
+    def __init__(self, title, key_location):
+        self.lines = []
+        self.title = title
+        self.key_location = key_location
+
+    def line(self, points, type, title=None, label=None, comment=None, fmt='%s %s'):
+        if points:
+            line = Line(points, type, title, label, comment, fmt)
+            self.lines.append(line)
+
+    def plot_string(self, line):
+        return line.plot_string()
+
+    def vertical_bar(self, x, type, label, comment):
+        if self.get_min_x() <= x and x <= self.get_max_x():
+            points = [(x, 0), (x, self.get_max_x())]
+            self.line(points, type, label, comment)
+
+    def get_all_x_values(self):
+        result = []
+        for line in self.lines:
+            result.extend(line.get_x_values())
+        return result
+
+    def get_all_y_values(self):
+        result = []
+        for line in self.lines:
+            result.extend(line.get_y_values())
+        return result
+
+    def get_min_x(self):
+        try:
+            return self.min_x
+        except AttributeError:
+            self.min_x = min(self.get_all_x_values())
+            return self.min_x
+
+    def get_max_x(self):
+        try:
+            return self.max_x
+        except AttributeError:
+            self.max_x = max(self.get_all_x_values())
+            return self.max_x
+
+    def get_min_y(self):
+        try:
+            return self.min_y
+        except AttributeError:
+            self.min_y = min(self.get_all_y_values())
+            return self.min_y
+
+    def get_max_y(self):
+        try:
+            return self.max_y
+        except AttributeError:
+            self.max_y = max(self.get_all_y_values())
+            return self.max_y
+
+    def draw(self):
+
+        if not self.lines:
+            return
+
+        if self.title:
+            print 'set title "%s"' % self.title
+        print 'set key %s' % self.key_location
+
+        inx = 1
+        max_y = self.max_graph_value(self.get_max_y())/2
+        for line in self.lines:
+            line.print_label(inx, line.points[0][0]-1, max_y)
+            inx += 1
+
+        plot_strings = [ self.plot_string(l) for l in self.lines ]
+        print 'plot ' + ', \\\n     '.join(plot_strings)
+
+        for line in self.lines:
+            line.print_points()
+
+
+
+def untar(fname):
+    import tarfile
+    tar = tarfile.open(name=fname, mode='r')
+    for tarinfo in tar:
+        tar.extract(tarinfo)
+    tar.close()
+
+def unzip(fname):
+    import zipfile
+    zf = zipfile.ZipFile(fname, 'r')
+    for name in zf.namelist():
+        dir = os.path.dirname(name)
+        try:
+            os.makedirs(dir)
+        except:
+            pass
+        open(name, 'w').write(zf.read(name))
+
+def read_tree(dir):
+    def read_files(arg, dirname, fnames):
+        for fn in fnames:
+            fn = os.path.join(dirname, fn)
+            if os.path.isfile(fn):
+                open(fn, 'rb').read()
+    os.path.walk('.', read_files, None)
+
+def redirect_to_file(command, log):
+    return '%s > %s 2>&1' % (command, log)
+
+def tee_to_file(command, log):
+    return '%s 2>&1 | tee %s' % (command, log)
+
+
+    
+class SConsTimer:
+    """
+    Usage: scons-time SUBCOMMAND [ARGUMENTS]
+    Type "scons-time help SUBCOMMAND" for help on a specific subcommand.
+
+    Available subcommands:
+        func            Extract test-run data for a function
+        help            Provides help
+        mem             Extract --debug=memory data from test runs
+        obj             Extract --debug=count data from test runs
+        time            Extract --debug=time data from test runs
+        run             Runs a test configuration
+    """
+
+    name = 'scons-time'
+    name_spaces = ' '*len(name)
+
+    def makedict(**kw):
+        return kw
+
+    default_settings = makedict(
+        aegis               = 'aegis',
+        aegis_project       = None,
+        chdir               = None,
+        config_file         = None,
+        initial_commands    = [],
+        key_location        = 'bottom left',
+        orig_cwd            = os.getcwd(),
+        outdir              = None,
+        prefix              = '',
+        python              = '"%s"' % sys.executable,
+        redirect            = redirect_to_file,
+        scons               = None,
+        scons_flags         = '--debug=count --debug=memory --debug=time --debug=memoizer',
+        scons_lib_dir       = None,
+        scons_wrapper       = None,
+        startup_targets     = '--help',
+        subdir              = None,
+        subversion_url      = None,
+        svn                 = 'svn',
+        svn_co_flag         = '-q',
+        tar                 = 'tar',
+        targets             = '',
+        targets0            = None,
+        targets1            = None,
+        targets2            = None,
+        title               = None,
+        unzip               = 'unzip',
+        verbose             = False,
+        vertical_bars       = [],
+
+        unpack_map = {
+            '.tar.gz'       : (untar,   '%(tar)s xzf %%s'),
+            '.tgz'          : (untar,   '%(tar)s xzf %%s'),
+            '.tar'          : (untar,   '%(tar)s xf %%s'),
+            '.zip'          : (unzip,   '%(unzip)s %%s'),
+        },
+    )
+
+    run_titles = [
+        'Startup',
+        'Full build',
+        'Up-to-date build',
+    ]
+
+    run_commands = [
+        '%(python)s %(scons_wrapper)s %(scons_flags)s --profile=%(prof0)s %(targets0)s',
+        '%(python)s %(scons_wrapper)s %(scons_flags)s --profile=%(prof1)s %(targets1)s',
+        '%(python)s %(scons_wrapper)s %(scons_flags)s --profile=%(prof2)s %(targets2)s',
+    ]
+
+    stages = [
+        'pre-read',
+        'post-read',
+        'pre-build',
+        'post-build',
+    ]
+
+    stage_strings = {
+        'pre-read'      : 'Memory before reading SConscript files:',
+        'post-read'     : 'Memory after reading SConscript files:',
+        'pre-build'     : 'Memory before building targets:',
+        'post-build'    : 'Memory after building targets:',
+    }
+
+    memory_string_all = 'Memory '
+
+    default_stage = stages[-1]
+
+    time_strings = {
+        'total'         : 'Total build time',
+        'SConscripts'   : 'Total SConscript file execution time',
+        'SCons'         : 'Total SCons execution time',
+        'commands'      : 'Total command execution time',
+    }
+    
+    time_string_all = 'Total .* time'
+
+    #
+
+    def __init__(self):
+        self.__dict__.update(self.default_settings)
+
+    # Functions for displaying and executing commands.
+
+    def subst(self, x, dictionary):
+        try:
+            return x % dictionary
+        except TypeError:
+            # x isn't a string (it's probably a Python function),
+            # so just return it.
+            return x
+
+    def subst_variables(self, command, dictionary):
+        """
+        Substitutes (via the format operator) the values in the specified
+        dictionary into the specified command.
+
+        The command can be an (action, string) tuple.  In all cases, we
+        perform substitution on strings and don't worry if something isn't
+        a string.  (It's probably a Python function to be executed.)
+        """
+        try:
+            command + ''
+        except TypeError:
+            action = command[0]
+            string = command[1]
+            args = command[2:]
+        else:
+            action = command
+            string = action
+            args = (())
+        action = self.subst(action, dictionary)
+        string = self.subst(string, dictionary)
+        return (action, string, args)
+
+    def _do_not_display(self, msg, *args):
+        pass
+
+    def display(self, msg, *args):
+        """
+        Displays the specified message.
+
+        Each message is prepended with a standard prefix of our name
+        plus the time.
+        """
+        if callable(msg):
+            msg = msg(*args)
+        else:
+            msg = msg % args
+        if msg is None:
+            return
+        fmt = '%s[%s]: %s\n'
+        sys.stdout.write(fmt % (self.name, time.strftime('%H:%M:%S'), msg))
+
+    def _do_not_execute(self, action, *args):
+        pass
+
+    def execute(self, action, *args):
+        """
+        Executes the specified action.
+
+        The action is called if it's a callable Python function, and
+        otherwise passed to os.system().
+        """
+        if callable(action):
+            action(*args)
+        else:
+            os.system(action % args)
+
+    def run_command_list(self, commands, dict):
+        """
+        Executes a list of commands, substituting values from the
+        specified dictionary.
+        """
+        commands = [ self.subst_variables(c, dict) for c in commands ]
+        for action, string, args in commands:
+            self.display(string, *args)
+            sys.stdout.flush()
+            status = self.execute(action, *args)
+            if status:
+                sys.exit(status)
+
+    def log_display(self, command, log):
+        command = self.subst(command, self.__dict__)
+        if log:
+            command = self.redirect(command, log)
+        return command
+
+    def log_execute(self, command, log):
+        command = self.subst(command, self.__dict__)
+        output = os.popen(command).read()
+        if self.verbose:
+            sys.stdout.write(output)
+        open(log, 'wb').write(output)
+
+    #
+
+    def archive_splitext(self, path):
+        """
+        Splits an archive name into a filename base and extension.
+
+        This is like os.path.splitext() (which it calls) except that it
+        also looks for '.tar.gz' and treats it as an atomic extensions.
+        """
+        if path.endswith('.tar.gz'):
+            return path[:-7], path[-7:]
+        else:
+            return os.path.splitext(path)
+
+    def args_to_files(self, args, tail=None):
+        """
+        Takes a list of arguments, expands any glob patterns, and
+        returns the last "tail" files from the list.
+        """
+        files = []
+        for a in args:
+            g = glob.glob(a)
+            g.sort()
+            files.extend(g)
+
+        if tail:
+            files = files[-tail:]
+
+        return files
+
+    def ascii_table(self, files, columns,
+                    line_function, file_function=lambda x: x,
+                    *args, **kw):
+
+        header_fmt = ' '.join(['%12s'] * len(columns))
+        line_fmt = header_fmt + '    %s'
+
+        print header_fmt % columns
+
+        for file in files:
+            t = line_function(file, *args, **kw)
+            diff = len(columns) - len(t)
+            if diff > 0:
+                t += [''] * diff
+            t.append(file_function(file))
+            print line_fmt % tuple(t)
+
+    def collect_results(self, files, function, *args, **kw):
+        results = {}
+
+        for file in files:
+            base = os.path.splitext(file)[0]
+            run, index = string.split(base, '-')[-2:]
+
+            run = int(run)
+            index = int(index)
+
+            value = function(file, *args, **kw)
+
+            try:
+                r = results[index]
+            except KeyError:
+                r = []
+                results[index] = r
+            r.append((run, value))
+
+        return results
+
+    def doc_to_help(self, obj):
+        """
+        Translates an object's __doc__ string into help text.
+
+        This strips a consistent number of spaces from each line in the
+        help text, essentially "outdenting" the text to the left-most
+        column.
+        """
+        doc = obj.__doc__
+        if doc is None:
+            return ''
+        return self.outdent(doc)
+
+    def find_next_run_number(self, dir, prefix):
+        """
+        Returns the next run number in a directory for the specified prefix.
+
+        Examines the contents the specified directory for files with the
+        specified prefix, extracts the run numbers from each file name,
+        and returns the next run number after the largest it finds.
+        """
+        x = re.compile(re.escape(prefix) + '-([0-9]+).*')
+        matches = map(lambda e, x=x: x.match(e), os.listdir(dir))
+        matches = filter(None, matches)
+        if not matches:
+            return 0
+        run_numbers = map(lambda m: int(m.group(1)), matches)
+        return int(max(run_numbers)) + 1
+
+    def gnuplot_results(self, results, fmt='%s %.3f'):
+        """
+        Prints out a set of results in Gnuplot format.
+        """
+        gp = Gnuplotter(self.title, self.key_location)
+
+        indices = results.keys()
+        indices.sort()
+
+        for i in indices:
+            try:
+                t = self.run_titles[i]
+            except IndexError:
+                t = '??? %s ???' % i
+            results[i].sort()
+            gp.line(results[i], i+1, t, None, t, fmt=fmt)
+
+        for bar_tuple in self.vertical_bars:
+            try:
+                x, type, label, comment = bar_tuple
+            except ValueError:
+                x, type, label = bar_tuple
+                comment = label
+            gp.vertical_bar(x, type, None, label, comment)
+
+        gp.draw()
+
+    def logfile_name(self, invocation):
+        """
+        Returns the absolute path of a log file for the specificed
+        invocation number.
+        """
+        name = self.prefix_run + '-%d.log' % invocation
+        return os.path.join(self.outdir, name)
+
+    def outdent(self, s):
+        """
+        Strip as many spaces from each line as are found at the beginning
+        of the first line in the list.
+        """
+        lines = s.split('\n')
+        if lines[0] == '':
+            lines = lines[1:]
+        spaces = re.match(' *', lines[0]).group(0)
+        def strip_initial_spaces(l, s=spaces):
+            if l.startswith(spaces):
+                l = l[len(spaces):]
+            return l
+        return '\n'.join([ strip_initial_spaces(l) for l in lines ]) + '\n'
+
+    def profile_name(self, invocation):
+        """
+        Returns the absolute path of a profile file for the specified
+        invocation number.
+        """
+        name = self.prefix_run + '-%d.prof' % invocation
+        return os.path.join(self.outdir, name)
+
+    def set_env(self, key, value):
+        os.environ[key] = value
+
+    #
+
+    def get_debug_times(self, file, time_string=None):
+        """
+        Fetch times from the --debug=time strings in the specified file.
+        """
+        if time_string is None:
+            search_string = self.time_string_all
+        else:
+            search_string = time_string
+        contents = open(file).read()
+        result = re.findall(r'%s: ([\d\.]*)' % search_string, contents)[-4:]
+        result = [ float(r) for r in result ]
+        if not time_string is None:
+            result = result[0]
+        return result
+
+    def get_function_profile(self, file, function):
+        """
+        Returns the file, line number, function name, and cumulative time.
+        """
+        try:
+            import pstats
+        except ImportError, e:
+            sys.stderr.write('%s: func: %s\n' % (self.name, e))
+            sys.stderr.write('%s  This version of Python is missing the profiler.\n' % self.name_spaces)
+            sys.stderr.write('%s  Cannot use the "func" subcommand.\n' % self.name_spaces)
+            sys.exit(1)
+        statistics = pstats.Stats(file).stats
+        matches = [ e for e in statistics.items() if e[0][2] == function ]
+        r = matches[0]
+        return r[0][0], r[0][1], r[0][2], r[1][3]
+
+    def get_function_time(self, file, function):
+        """
+        Returns just the cumulative time for the specified function.
+        """
+        return self.get_function_profile(file, function)[3]
+
+    def get_memory(self, file, memory_string=None):
+        """
+        Returns a list of integers of the amount of memory used.  The
+        default behavior is to return all the stages.
+        """
+        if memory_string is None:
+            search_string = self.memory_string_all
+        else:
+            search_string = memory_string
+        lines = open(file).readlines()
+        lines = [ l for l in lines if l.startswith(search_string) ][-4:]
+        result = [ int(l.split()[-1]) for l in lines[-4:] ]
+        if len(result) == 1:
+            result = result[0]
+        return result
+
+    def get_object_counts(self, file, object_name, index=None):
+        """
+        Returns the counts of the specified object_name.
+        """
+        object_string = ' ' + object_name + '\n'
+        lines = open(file).readlines()
+        line = [ l for l in lines if l.endswith(object_string) ][0]
+        result = [ int(field) for field in line.split()[:4] ]
+        if not index is None:
+            result = result[index]
+        return result
+
+    #
+
+    command_alias = {}
+
+    def execute_subcommand(self, argv):
+        """
+        Executes the do_*() function for the specified subcommand (argv[0]).
+        """
+        if not argv:
+            return
+        cmdName = self.command_alias.get(argv[0], argv[0])
+        try:
+            func = getattr(self, 'do_' + cmdName)
+        except AttributeError:
+            return self.default(argv)
+        try:
+            return func(argv)
+        except TypeError, e:
+            sys.stderr.write("%s %s: %s\n" % (self.name, cmdName, e))
+            import traceback
+            traceback.print_exc(file=sys.stderr)
+            sys.stderr.write("Try '%s help %s'\n" % (self.name, cmdName))
+
+    def default(self, argv):
+        """
+        The default behavior for an unknown subcommand.  Prints an
+        error message and exits.
+        """
+        sys.stderr.write('%s: Unknown subcommand "%s".\n' % (self.name, argv[0]))
+        sys.stderr.write('Type "%s help" for usage.\n' % self.name)
+        sys.exit(1)
+
+    #
+
+    def do_help(self, argv):
+        """
+        """
+        if argv[1:]:
+            for arg in argv[1:]:
+                try:
+                    func = getattr(self, 'do_' + arg)
+                except AttributeError:
+                    sys.stderr.write('%s: No help for "%s"\n' % (self.name, arg))
+                else:
+                    try:
+                        help = getattr(self, 'help_' + arg)
+                    except AttributeError:
+                        sys.stdout.write(self.doc_to_help(func))
+                        sys.stdout.flush()
+                    else:
+                        help()
+        else:
+            doc = self.doc_to_help(self.__class__)
+            if doc:
+                sys.stdout.write(doc)
+            sys.stdout.flush()
+            return None
+
+    #
+
+    def help_func(self):
+        help = """\
+        Usage: scons-time func [OPTIONS] FILE [...]
+
+          -C DIR, --chdir=DIR           Change to DIR before looking for files
+          -f FILE, --file=FILE          Read configuration from specified FILE
+          --fmt=FORMAT, --format=FORMAT Print data in specified FORMAT
+          --func=NAME, --function=NAME  Report time for function NAME
+          -h, --help                    Print this help and exit
+          -p STRING, --prefix=STRING    Use STRING as log file/profile prefix
+          -t NUMBER, --tail=NUMBER      Only report the last NUMBER files
+          --title=TITLE                 Specify the output plot TITLE
+        """
+        sys.stdout.write(self.outdent(help))
+        sys.stdout.flush()
+
+    def do_func(self, argv):
+        """
+        """
+        format = 'ascii'
+        function_name = '_main'
+        tail = None
+
+        short_opts = '?C:f:hp:t:'
+
+        long_opts = [
+            'chdir=',
+            'file=',
+            'fmt=',
+            'format=',
+            'func=',
+            'function=',
+            'help',
+            'prefix=',
+            'tail=',
+            'title=',
+        ]
+
+        opts, args = getopt.getopt(argv[1:], short_opts, long_opts)
+
+        for o, a in opts:
+            if o in ('-C', '--chdir'):
+                self.chdir = a
+            elif o in ('-f', '--file'):
+                self.config_file = a
+            elif o in ('--fmt', '--format'):
+                format = a
+            elif o in ('--func', '--function'):
+                function_name = a
+            elif o in ('-?', '-h', '--help'):
+                self.do_help(['help', 'func'])
+                sys.exit(0)
+            elif o in ('--max'):
+                max_time = int(a)
+            elif o in ('-p', '--prefix'):
+                self.prefix = a
+            elif o in ('-t', '--tail'):
+                tail = int(a)
+            elif o in ('--title'):
+                self.title = a
+
+        if self.config_file:
+            execfile(self.config_file, self.__dict__)
+
+        if self.chdir:
+            os.chdir(self.chdir)
+
+        if not args:
+
+            pattern = '%s*.prof' % self.prefix
+            args = self.args_to_files([pattern], tail)
+
+            if not args:
+                if self.chdir:
+                    directory = self.chdir
+                else:
+                    directory = os.getcwd()
+
+                sys.stderr.write('%s: func: No arguments specified.\n' % self.name)
+                sys.stderr.write('%s  No %s*.prof files found in "%s".\n' % (self.name_spaces, self.prefix, directory))
+                sys.stderr.write('%s  Type "%s help func" for help.\n' % (self.name_spaces, self.name))
+                sys.exit(1)
+
+        else:
+
+            args = self.args_to_files(args, tail)
+
+        cwd_ = os.getcwd() + os.sep
+
+        if format == 'ascii':
+
+            def print_function_timing(file, func):
+                try:
+                    f, line, func, time = self.get_function_profile(file, func)
+                except ValueError, e:
+                    sys.stderr.write("%s: func: %s: %s\n" % (self.name, file, e))
+                else:
+                    if f.startswith(cwd_):
+                        f = f[len(cwd_):]
+                    print "%.3f %s:%d(%s)" % (time, f, line, func)
+
+            for file in args:
+                print_function_timing(file, function_name)
+
+        elif format == 'gnuplot':
+
+            results = self.collect_results(args, self.get_function_time,
+                                           function_name)
+
+            self.gnuplot_results(results)
+
+        else:
+
+            sys.stderr.write('%s: func: Unknown format "%s".\n' % (self.name, format))
+            sys.exit(1)
+
+    #
+
+    def help_mem(self):
+        help = """\
+        Usage: scons-time mem [OPTIONS] FILE [...]
+
+          -C DIR, --chdir=DIR           Change to DIR before looking for files
+          -f FILE, --file=FILE          Read configuration from specified FILE
+          --fmt=FORMAT, --format=FORMAT Print data in specified FORMAT
+          -h, --help                    Print this help and exit
+          -p STRING, --prefix=STRING    Use STRING as log file/profile prefix
+          --stage=STAGE                 Plot memory at the specified stage:
+                                          pre-read, post-read, pre-build,
+                                          post-build (default: post-build)
+          -t NUMBER, --tail=NUMBER      Only report the last NUMBER files
+          --title=TITLE                 Specify the output plot TITLE
+        """
+        sys.stdout.write(self.outdent(help))
+        sys.stdout.flush()
+
+    def do_mem(self, argv):
+
+        format = 'ascii'
+        logfile_path = lambda x: x
+        stage = self.default_stage
+        tail = None
+
+        short_opts = '?C:f:hp:t:'
+
+        long_opts = [
+            'chdir=',
+            'file=',
+            'fmt=',
+            'format=',
+            'help',
+            'prefix=',
+            'stage=',
+            'tail=',
+            'title=',
+        ]
+
+        opts, args = getopt.getopt(argv[1:], short_opts, long_opts)
+
+        for o, a in opts:
+            if o in ('-C', '--chdir'):
+                self.chdir = a
+            elif o in ('-f', '--file'):
+                self.config_file = a
+            elif o in ('--fmt', '--format'):
+                format = a
+            elif o in ('-?', '-h', '--help'):
+                self.do_help(['help', 'mem'])
+                sys.exit(0)
+            elif o in ('-p', '--prefix'):
+                self.prefix = a
+            elif o in ('--stage'):
+                if not a in self.stages:
+                    sys.stderr.write('%s: mem: Unrecognized stage "%s".\n' % (self.name, a))
+                    sys.exit(1)
+                stage = a
+            elif o in ('-t', '--tail'):
+                tail = int(a)
+            elif o in ('--title'):
+                self.title = a
+
+        if self.config_file:
+            execfile(self.config_file, self.__dict__)
+
+        if self.chdir:
+            os.chdir(self.chdir)
+            logfile_path = lambda x, c=self.chdir: os.path.join(c, x)
+
+        if not args:
+
+            pattern = '%s*.log' % self.prefix
+            args = self.args_to_files([pattern], tail)
+
+            if not args:
+                if self.chdir:
+                    directory = self.chdir
+                else:
+                    directory = os.getcwd()
+
+                sys.stderr.write('%s: mem: No arguments specified.\n' % self.name)
+                sys.stderr.write('%s  No %s*.log files found in "%s".\n' % (self.name_spaces, self.prefix, directory))
+                sys.stderr.write('%s  Type "%s help mem" for help.\n' % (self.name_spaces, self.name))
+                sys.exit(1)
+
+        else:
+
+            args = self.args_to_files(args, tail)
+
+        cwd_ = os.getcwd() + os.sep
+
+        if format == 'ascii':
+
+            self.ascii_table(args, tuple(self.stages), self.get_memory, logfile_path)
+
+        elif format == 'gnuplot':
+
+            results = self.collect_results(args, self.get_memory,
+                                           self.stage_strings[stage])
+
+            self.gnuplot_results(results)
+
+        else:
+
+            sys.stderr.write('%s: mem: Unknown format "%s".\n' % (self.name, format))
+            sys.exit(1)
+
+        return 0
+
+    #
+
+    def help_obj(self):
+        help = """\
+        Usage: scons-time obj [OPTIONS] OBJECT FILE [...]
+
+          -C DIR, --chdir=DIR           Change to DIR before looking for files
+          -f FILE, --file=FILE          Read configuration from specified FILE
+          --fmt=FORMAT, --format=FORMAT Print data in specified FORMAT
+          -h, --help                    Print this help and exit
+          -p STRING, --prefix=STRING    Use STRING as log file/profile prefix
+          --stage=STAGE                 Plot memory at the specified stage:
+                                          pre-read, post-read, pre-build,
+                                          post-build (default: post-build)
+          -t NUMBER, --tail=NUMBER      Only report the last NUMBER files
+          --title=TITLE                 Specify the output plot TITLE
+        """
+        sys.stdout.write(self.outdent(help))
+        sys.stdout.flush()
+
+    def do_obj(self, argv):
+
+        format = 'ascii'
+        logfile_path = lambda x: x
+        stage = self.default_stage
+        tail = None
+
+        short_opts = '?C:f:hp:t:'
+
+        long_opts = [
+            'chdir=',
+            'file=',
+            'fmt=',
+            'format=',
+            'help',
+            'prefix=',
+            'stage=',
+            'tail=',
+            'title=',
+        ]
+
+        opts, args = getopt.getopt(argv[1:], short_opts, long_opts)
+
+        for o, a in opts:
+            if o in ('-C', '--chdir'):
+                self.chdir = a
+            elif o in ('-f', '--file'):
+                self.config_file = a
+            elif o in ('--fmt', '--format'):
+                format = a
+            elif o in ('-?', '-h', '--help'):
+                self.do_help(['help', 'obj'])
+                sys.exit(0)
+            elif o in ('-p', '--prefix'):
+                self.prefix = a
+            elif o in ('--stage'):
+                if not a in self.stages:
+                    sys.stderr.write('%s: obj: Unrecognized stage "%s".\n' % (self.name, a))
+                    sys.stderr.write('%s       Type "%s help obj" for help.\n' % (self.name_spaces, self.name))
+                    sys.exit(1)
+                stage = a
+            elif o in ('-t', '--tail'):
+                tail = int(a)
+            elif o in ('--title'):
+                self.title = a
+
+        if not args:
+            sys.stderr.write('%s: obj: Must specify an object name.\n' % self.name)
+            sys.stderr.write('%s       Type "%s help obj" for help.\n' % (self.name_spaces, self.name))
+            sys.exit(1)
+
+        object_name = args.pop(0)
+
+        if self.config_file:
+            execfile(self.config_file, self.__dict__)
+
+        if self.chdir:
+            os.chdir(self.chdir)
+            logfile_path = lambda x, c=self.chdir: os.path.join(c, x)
+
+        if not args:
+
+            pattern = '%s*.log' % self.prefix
+            args = self.args_to_files([pattern], tail)
+
+            if not args:
+                if self.chdir:
+                    directory = self.chdir
+                else:
+                    directory = os.getcwd()
+
+                sys.stderr.write('%s: obj: No arguments specified.\n' % self.name)
+                sys.stderr.write('%s  No %s*.log files found in "%s".\n' % (self.name_spaces, self.prefix, directory))
+                sys.stderr.write('%s  Type "%s help obj" for help.\n' % (self.name_spaces, self.name))
+                sys.exit(1)
+
+        else:
+
+            args = self.args_to_files(args, tail)
+
+        cwd_ = os.getcwd() + os.sep
+
+        if format == 'ascii':
+
+            self.ascii_table(args, tuple(self.stages), self.get_object_counts, logfile_path, object_name)
+
+        elif format == 'gnuplot':
+
+            stage_index = 0
+            for s in self.stages:
+                if stage == s:
+                    break
+                stage_index = stage_index + 1
+
+            results = self.collect_results(args, self.get_object_counts,
+                                           object_name, stage_index)
+
+            self.gnuplot_results(results)
+
+        else:
+
+            sys.stderr.write('%s: obj: Unknown format "%s".\n' % (self.name, format))
+            sys.exit(1)
+
+        return 0
+
+    #
+
+    def help_run(self):
+        help = """\
+        Usage: scons-time run [OPTIONS] [FILE ...]
+
+          --aegis=PROJECT               Use SCons from the Aegis PROJECT
+          --chdir=DIR                   Name of unpacked directory for chdir
+          -f FILE, --file=FILE          Read configuration from specified FILE
+          -h, --help                    Print this help and exit
+          -n, --no-exec                 No execute, just print command lines
+          --number=NUMBER               Put output in files for run NUMBER
+          --outdir=OUTDIR               Put output files in OUTDIR
+          -p STRING, --prefix=STRING    Use STRING as log file/profile prefix
+          --python=PYTHON               Time using the specified PYTHON
+          -q, --quiet                   Don't print command lines
+          --scons=SCONS                 Time using the specified SCONS
+          --svn=URL, --subversion=URL   Use SCons from Subversion URL
+          -v, --verbose                 Display output of commands
+        """
+        sys.stdout.write(self.outdent(help))
+        sys.stdout.flush()
+
+    def do_run(self, argv):
+        """
+        """
+        run_number_list = [None]
+
+        short_opts = '?f:hnp:qs:v'
+
+        long_opts = [
+            'aegis=',
+            'file=',
+            'help',
+            'no-exec',
+            'number=',
+            'outdir=',
+            'prefix=',
+            'python=',
+            'quiet',
+            'scons=',
+            'svn=',
+            'subdir=',
+            'subversion=',
+            'verbose',
+        ]
+
+        opts, args = getopt.getopt(argv[1:], short_opts, long_opts)
+
+        for o, a in opts:
+            if o in ('--aegis'):
+                self.aegis_project = a
+            elif o in ('-f', '--file'):
+                self.config_file = a
+            elif o in ('-?', '-h', '--help'):
+                self.do_help(['help', 'run'])
+                sys.exit(0)
+            elif o in ('-n', '--no-exec'):
+                self.execute = self._do_not_execute
+            elif o in ('--number'):
+                run_number_list = self.split_run_numbers(a)
+            elif o in ('--outdir'):
+                self.outdir = a
+            elif o in ('-p', '--prefix'):
+                self.prefix = a
+            elif o in ('--python'):
+                self.python = a
+            elif o in ('-q', '--quiet'):
+                self.display = self._do_not_display
+            elif o in ('-s', '--subdir'):
+                self.subdir = a
+            elif o in ('--scons'):
+                self.scons = a
+            elif o in ('--svn', '--subversion'):
+                self.subversion_url = a
+            elif o in ('-v', '--verbose'):
+                self.redirect = tee_to_file
+                self.verbose = True
+                self.svn_co_flag = ''
+
+        if not args and not self.config_file:
+            sys.stderr.write('%s: run: No arguments or -f config file specified.\n' % self.name)
+            sys.stderr.write('%s  Type "%s help run" for help.\n' % (self.name_spaces, self.name))
+            sys.exit(1)
+
+        if self.config_file:
+            execfile(self.config_file, self.__dict__)
+
+        if args:
+            self.archive_list = args
+
+        archive_file_name = os.path.split(self.archive_list[0])[1]
+
+        if not self.subdir:
+            self.subdir = self.archive_splitext(archive_file_name)[0]
+
+        if not self.prefix:
+            self.prefix = self.archive_splitext(archive_file_name)[0]
+
+        prepare = None
+        if self.subversion_url:
+            prepare = self.prep_subversion_run
+        elif self.aegis_project:
+            prepare = self.prep_aegis_run
+
+        for run_number in run_number_list:
+            self.individual_run(run_number, self.archive_list, prepare)
+
+    def split_run_numbers(self, s):
+        result = []
+        for n in s.split(','):
+            try:
+                x, y = n.split('-')
+            except ValueError:
+                result.append(int(n))
+            else:
+                result.extend(range(int(x), int(y)+1))
+        return result
+
+    def scons_path(self, dir):
+        return os.path.join(dir, 'src', 'script', 'scons.py')
+
+    def scons_lib_dir_path(self, dir):
+        return os.path.join(dir, 'src', 'engine')
+
+    def prep_aegis_run(self, commands, removals):
+        self.aegis_tmpdir = tempfile.mktemp(prefix = self.name + '-aegis-')
+        removals.append((shutil.rmtree, 'rm -rf %%s', self.aegis_tmpdir))
+
+        self.aegis_parent_project = os.path.splitext(self.aegis_project)[0]
+        self.scons = self.scons_path(self.aegis_tmpdir)
+        self.scons_lib_dir = self.scons_lib_dir_path(self.aegis_tmpdir)
+
+        commands.extend([
+            'mkdir %(aegis_tmpdir)s',
+            (lambda: os.chdir(self.aegis_tmpdir), 'cd %(aegis_tmpdir)s'),
+            '%(aegis)s -cp -ind -p %(aegis_parent_project)s .',
+            '%(aegis)s -cp -ind -p %(aegis_project)s -delta %(run_number)s .',
+        ])
+
+    def prep_subversion_run(self, commands, removals):
+        self.svn_tmpdir = tempfile.mktemp(prefix = self.name + '-svn-')
+        removals.append((shutil.rmtree, 'rm -rf %%s', self.svn_tmpdir))
+
+        self.scons = self.scons_path(self.svn_tmpdir)
+        self.scons_lib_dir = self.scons_lib_dir_path(self.svn_tmpdir)
+
+        commands.extend([
+            'mkdir %(svn_tmpdir)s',
+            '%(svn)s co %(svn_co_flag)s -r %(run_number)s %(subversion_url)s %(svn_tmpdir)s',
+        ])
+
+    def individual_run(self, run_number, archive_list, prepare=None):
+        """
+        Performs an individual run of the default SCons invocations.
+        """
+
+        commands = []
+        removals = []
+
+        if prepare:
+            prepare(commands, removals)
+
+        save_scons              = self.scons
+        save_scons_wrapper      = self.scons_wrapper
+        save_scons_lib_dir      = self.scons_lib_dir
+
+        if self.outdir is None:
+            self.outdir = self.orig_cwd
+        elif not os.path.isabs(self.outdir):
+            self.outdir = os.path.join(self.orig_cwd, self.outdir)
+
+        if self.scons is None:
+            self.scons = self.scons_path(self.orig_cwd)
+
+        if self.scons_lib_dir is None:
+            self.scons_lib_dir = self.scons_lib_dir_path(self.orig_cwd)
+
+        if self.scons_wrapper is None:
+            self.scons_wrapper = self.scons
+
+        if not run_number:
+            run_number = self.find_next_run_number(self.outdir, self.prefix)
+
+        self.run_number = str(run_number)
+
+        self.prefix_run = self.prefix + '-%03d' % run_number
+
+        if self.targets0 is None:
+            self.targets0 = self.startup_targets
+        if self.targets1 is None:
+            self.targets1 = self.targets
+        if self.targets2 is None:
+            self.targets2 = self.targets
+
+        self.tmpdir = tempfile.mktemp(prefix = self.name + '-')
+
+        commands.extend([
+            'mkdir %(tmpdir)s',
+
+            (os.chdir, 'cd %%s', self.tmpdir),
+        ])
+
+        for archive in archive_list:
+            if not os.path.isabs(archive):
+                archive = os.path.join(self.orig_cwd, archive)
+            if os.path.isdir(archive):
+                dest = os.path.split(archive)[1]
+                commands.append((shutil.copytree, 'cp -r %%s %%s', archive, dest))
+            else:
+                suffix = self.archive_splitext(archive)[1]
+                commands.append(self.unpack_map[suffix] + (archive,))
+
+        commands.extend([
+            (os.chdir, 'cd %%s', self.subdir),
+        ])
+
+        commands.extend(self.initial_commands)
+
+        commands.extend([
+            (lambda: read_tree('.'),
+            'find * -type f | xargs cat > /dev/null'),
+
+            (self.set_env, 'export %%s=%%s',
+             'SCONS_LIB_DIR', self.scons_lib_dir),
+
+            '%(python)s %(scons_wrapper)s --version',
+        ])
+
+        index = 0
+        for run_command in self.run_commands:
+            setattr(self, 'prof%d' % index, self.profile_name(index))
+            c = (
+                self.log_execute,
+                self.log_display,
+                run_command,
+                self.logfile_name(index),
+            )
+            commands.append(c)
+            index = index + 1
+
+        commands.extend([
+            (os.chdir, 'cd %%s', self.orig_cwd),
+        ])
+
+        if not os.environ.get('PRESERVE'):
+            commands.extend(removals)
+
+            commands.append((shutil.rmtree, 'rm -rf %%s', self.tmpdir))
+
+        self.run_command_list(commands, self.__dict__)
+
+        self.scons              = save_scons
+        self.scons_lib_dir      = save_scons_lib_dir
+        self.scons_wrapper      = save_scons_wrapper
+
+    #
+
+    def help_time(self):
+        help = """\
+        Usage: scons-time time [OPTIONS] FILE [...]
+
+          -C DIR, --chdir=DIR           Change to DIR before looking for files
+          -f FILE, --file=FILE          Read configuration from specified FILE
+          --fmt=FORMAT, --format=FORMAT Print data in specified FORMAT
+          -h, --help                    Print this help and exit
+          -p STRING, --prefix=STRING    Use STRING as log file/profile prefix
+          -t NUMBER, --tail=NUMBER      Only report the last NUMBER files
+          --which=TIMER                 Plot timings for TIMER:  total,
+                                          SConscripts, SCons, commands.
+        """
+        sys.stdout.write(self.outdent(help))
+        sys.stdout.flush()
+
+    def do_time(self, argv):
+
+        format = 'ascii'
+        logfile_path = lambda x: x
+        tail = None
+        which = 'total'
+
+        short_opts = '?C:f:hp:t:'
+
+        long_opts = [
+            'chdir=',
+            'file=',
+            'fmt=',
+            'format=',
+            'help',
+            'prefix=',
+            'tail=',
+            'title=',
+            'which=',
+        ]
+
+        opts, args = getopt.getopt(argv[1:], short_opts, long_opts)
+
+        for o, a in opts:
+            if o in ('-C', '--chdir'):
+                self.chdir = a
+            elif o in ('-f', '--file'):
+                self.config_file = a
+            elif o in ('--fmt', '--format'):
+                format = a
+            elif o in ('-?', '-h', '--help'):
+                self.do_help(['help', 'time'])
+                sys.exit(0)
+            elif o in ('-p', '--prefix'):
+                self.prefix = a
+            elif o in ('-t', '--tail'):
+                tail = int(a)
+            elif o in ('--title'):
+                self.title = a
+            elif o in ('--which'):
+                if not a in self.time_strings.keys():
+                    sys.stderr.write('%s: time: Unrecognized timer "%s".\n' % (self.name, a))
+                    sys.stderr.write('%s  Type "%s help time" for help.\n' % (self.name_spaces, self.name))
+                    sys.exit(1)
+                which = a
+
+        if self.config_file:
+            execfile(self.config_file, self.__dict__)
+
+        if self.chdir:
+            os.chdir(self.chdir)
+            logfile_path = lambda x, c=self.chdir: os.path.join(c, x)
+
+        if not args:
+
+            pattern = '%s*.log' % self.prefix
+            args = self.args_to_files([pattern], tail)
+
+            if not args:
+                if self.chdir:
+                    directory = self.chdir
+                else:
+                    directory = os.getcwd()
+
+                sys.stderr.write('%s: time: No arguments specified.\n' % self.name)
+                sys.stderr.write('%s  No %s*.log files found in "%s".\n' % (self.name_spaces, self.prefix, directory))
+                sys.stderr.write('%s  Type "%s help time" for help.\n' % (self.name_spaces, self.name))
+                sys.exit(1)
+
+        else:
+
+            args = self.args_to_files(args, tail)
+
+        cwd_ = os.getcwd() + os.sep
+
+        if format == 'ascii':
+
+            columns = ("Total", "SConscripts", "SCons", "commands")
+            self.ascii_table(args, columns, self.get_debug_times, logfile_path)
+
+        elif format == 'gnuplot':
+
+            results = self.collect_results(args, self.get_debug_times,
+                                           self.time_strings[which])
+
+            self.gnuplot_results(results, fmt='%s %.6f')
+
+        else:
+
+            sys.stderr.write('%s: time: Unknown format "%s".\n' % (self.name, format))
+            sys.exit(1)
+
+if __name__ == '__main__':
+    opts, args = getopt.getopt(sys.argv[1:], 'h?V', ['help', 'version'])
+
+    ST = SConsTimer()
+
+    for o, a in opts:
+        if o in ('-?', '-h', '--help'):
+            ST.do_help(['help'])
+            sys.exit(0)
+        elif o in ('-V', '--version'):
+            sys.stdout.write('scons-time version\n')
+            sys.exit(0)
+
+    if not args:
+        sys.stderr.write('Type "%s help" for usage.\n' % ST.name)
+        sys.exit(1)
+
+    ST.execute_subcommand(args)
index 4852d01a0962c0bce5f617c9b1f2a34fb985756c..fbffe68e74ca769bcf03d45bb7769ec45eef7b9a 100644 (file)
@@ -69,7 +69,7 @@ if os.environ.has_key("SCONS_LIB_DIR"):
 local = 'scons-local-' + __version__
 if script_dir:
     local = os.path.join(script_dir, local)
-libs.append(local)
+libs.append(os.path.abspath(local))
 
 scons_version = 'scons-%s' % __version__
 
index 6f19d568b8900e1fd42ddcfd42282772ec20a0b9..66f8887d42c20dff3d8d48bb4814c14574179567 100644 (file)
@@ -70,7 +70,7 @@ if os.environ.has_key("SCONS_LIB_DIR"):
 local = 'scons-local-' + __version__
 if script_dir:
     local = os.path.join(script_dir, local)
-libs.append(local)
+libs.append(os.path.abspath(local))
 
 scons_version = 'scons-%s' % __version__
 
@@ -166,6 +166,8 @@ import whichdb
 import SCons.SConsign
 
 def my_whichdb(filename):
+    if filename[-7:] == ".dblite":
+        return "SCons.dblite"
     try:
         f = open(filename + ".dblite", "rb")
         f.close()
index 68b46ab0ed5d06d7333707df9b0cdc0ecff7dd7a..f06e717ae9374b474190f667756e21175dbc5aa8 100644 (file)
@@ -29,7 +29,13 @@ import stat
 import string
 import sys
 
-Version = "0.96.92"
+Version = "0.96.93"
+
+man_pages = [
+    'scons.1',
+    'sconsign.1',
+    'scons-time.1',
+]
 
 (head, tail) = os.path.split(sys.argv[0])
 
@@ -331,7 +337,7 @@ class install_data(_install_data):
                 dir = 'Doc'
             else:
                 dir = os.path.join('man', 'man1')
-            self.data_files = [(dir, ["scons.1", "sconsign.1"])]
+            self.data_files = [(dir, man_pages)]
             man_dir = os.path.join(self.install_dir, dir)
             msg = "Installed SCons man pages into %s" % man_dir
             Installed.append(msg)
@@ -351,9 +357,10 @@ arguments = {
                           "SCons.Sig",
                           "SCons.Tool"],
     'package_dir'      : {'' : 'engine'},
-    'data_files'       : [('man/man1', ["scons.1", "sconsign.1"])],
+    'data_files'       : [('man/man1', man_pages)],
     'scripts'          : ['script/scons',
                           'script/sconsign',
+                          'script/scons-time',
                           'script/scons.bat'],
     'cmdclass'         : {'install'         : install,
                           'install_lib'     : install_lib,
index 3f7d1c9ad39741d4ffdbb377fb4242bfef038934..ce78a8bcc1e033f193e8b04d6a99de6310d34a8a 100644 (file)
@@ -78,6 +78,9 @@ def visit(collect, dirname, names):
 check = {
     build_scons : [
         'build',
+        'build-stamp',
+        'configure-stamp',
+        'debian',
         'dist',
         'engine/SCons/Conftest.py',
         'engine/SCons/dblite.py',
diff --git a/test/Alias/srcdir.py b/test/Alias/srcdir.py
new file mode 100644 (file)
index 0000000..2251165
--- /dev/null
@@ -0,0 +1,100 @@
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+"""
+Verify that an Alias for a BuildDir()'s source directory works as
+expected.
+
+This tests for a 0.96.93 bug uncovered by the LilyPond project's build.
+
+The specific problem is that, in 0.96.93, the simple act of trying to
+disambiguate a target file in the BuildDir() would call srcnode(), which
+would create a "phantom" Node for the target in the *source* directory:
+
+        +-minimal
+          +-python
+            +-foo               <= this doesn't belong!
+            +-foo.py
+            +-out-scons
+              +-foo             <= this is all right
+              +-foo.py
+
+As part of deciding if the 'minimal' Alias is up-to-date, the 'python'
+source directory would get scanned for files, including the "phantom"
+'python/foo' target Node.  Since this didn't exist, the build would die:
+
+    scons: *** Source `python/foo' not found, needed by target `minimal'.  Stop.
+
+The specific 0.96.94 solution was to make the Node.FS.Entry.disambiguate()
+smarter about looking on disk.  Future versions may solve this in other
+ways as the architecture evolves, of course, but this will still make
+for good test case regardless.
+"""
+
+import TestSCons
+
+test = TestSCons.TestSCons()
+
+test.subdir('python')
+
+test.write('SConstruct', """\
+import os.path
+
+env = Environment ()
+
+def at_copy_ext (target, source, env):
+    n = str (source[0])
+    s = open (n, 'rb').read ()
+    e = os.path.splitext (n)[1]
+    t = str (target[0]) + e
+    open (t, 'wb').write (s)
+
+AT_COPY_EXT = Builder (action = at_copy_ext, src_suffix=['.py', '.sh',])
+env.Append (BUILDERS = {'AT_COPY_EXT': AT_COPY_EXT})
+
+env.Alias ('minimal', ['python'])
+
+Export ('env')
+
+b = 'python/out-scons'
+
+env.BuildDir(b, 'python', duplicate=0)
+
+SConscript(b + '/SConscript')
+""")
+
+test.write(['python', 'SConscript'], """\
+Import ('env')
+env.AT_COPY_EXT('foo.py')
+""")
+
+test.write(['python', 'foo.py'], 'python/foo.py\n')
+
+test.run('minimal')
+
+test.must_match(['python', 'out-scons', 'foo.py'], "python/foo.py\n")
+
+test.pass_test()
index 8c28ea57e19e312249176edc734cb9614ebf7aa4..c7c9c95d36cfae0424ee9bafe0a7f40dac77be9f 100644 (file)
@@ -58,6 +58,8 @@ foo.b
 built
 """)
 
+python_file_line = test.python_file_line(SConstruct_path, 14)
+
 ### Gross mistake in Builder spec
 
 test.write(SConstruct_path, sconstruct % '\
@@ -66,8 +68,7 @@ b2 = Builder(act__ion=buildop, src_suffix=".b", suffix=".c")')
 expect_stderr = """\
 
 scons: *** Builder b2 must have an action to build ['foo.c'].
-File "%(SConstruct_path)s", line 14, in ?
-""" % locals()
+""" + python_file_line
 
 test.run(arguments='.', stderr=expect_stderr, status = 2)
 
@@ -79,8 +80,7 @@ b2 = Builder(actoin=buildop, src_suffix=".b", suffix=".c")')
 expect_stderr="""\
 
 scons: *** Builder b2 must have an action to build ['foo.c'].
-File "%(SConstruct_path)s", line 14, in ?
-""" % locals()
+""" + python_file_line
 
 test.run(arguments='test2', stderr=expect_stderr, status=2)
 
@@ -92,8 +92,7 @@ b2 = Builder(src_suffix=".b", suffix=".c")')
 expect_stderr = """\
 
 scons: *** Builder b2 must have an action to build ['foo.c'].
-File "%(SConstruct_path)s", line 14, in ?
-""" % locals()
+""" + python_file_line
 
 test.run(arguments='test2', stderr=expect_stderr, status = 2)
 
index 4fcd6257a7a05337d35832e364ae6a2e94755d9d..44096a1c46b97ea6274d34800099efbac7f38d38 100644 (file)
@@ -165,8 +165,7 @@ BuildDir('build', 'src2')
 
 expect_stderr = """
 scons: *** 'build' already has a source directory: 'src1'.
-File "%(duplicate_SConstruct_path)s", line 2, in ?
-""" % locals()
+""" + test.python_file_line(duplicate_SConstruct_path, 2)
 
 test.run(chdir = 'duplicate',
          arguments = ".",
diff --git a/test/Builder/srcdir.py b/test/Builder/srcdir.py
new file mode 100644 (file)
index 0000000..4f378a7
--- /dev/null
@@ -0,0 +1,76 @@
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+"""
+Verify that specifying a srcdir when calling a Builder correctly
+prefixes each relative-path string with the specified srcdir.
+"""
+
+import TestSCons
+
+_python_ = TestSCons._python_
+
+test = TestSCons.TestSCons()
+
+test.subdir('src', ['src', 'foo'])
+
+file3 = test.workpath('file3')
+
+test.write(['src', 'cat.py'], """\
+import sys
+o = open(sys.argv[1], 'wb')
+for f in sys.argv[2:]:
+    o.write(open(f, 'rb').read())
+o.close()
+""")
+
+test.write(['src', 'SConstruct'], """\
+Command('output',
+        ['file1', File('file2'), r'%(file3)s', 'file4'],
+        '%(_python_)s cat.py $TARGET $SOURCES',
+        srcdir='foo')
+""" % locals())
+
+test.write(['src', 'foo', 'file1'],     "file1\n")
+
+test.write(['src', 'file2'],            "file2\n")
+
+test.write(file3,                       "file3\n")
+
+test.write(['src', 'foo', 'file4'],     "file4\n")
+
+test.run(chdir = 'src', arguments = '.')
+
+expected = """\
+file1
+file2
+file3
+file4
+"""
+
+test.must_match(['src', 'output'],  expected)
+
+test.pass_test()
diff --git a/test/Errors/AttributeError.py b/test/Errors/AttributeError.py
new file mode 100644 (file)
index 0000000..637cccf
--- /dev/null
@@ -0,0 +1,47 @@
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+"""
+Verify the exit status and error output if an SConstruct file
+throws an AttributeError.
+"""
+
+import TestSCons
+
+test = TestSCons.TestSCons(match = TestSCons.match_re_dotall)
+
+test.write('SConstruct', """\
+a = 1
+a.append(2)
+""")
+
+test.run(status = 2, stderr = """\
+(AttributeError|<type 'exceptions\.AttributeError'>): 'int' object has no attribute 'append':
+  File ".+SConstruct", line 2:
+    a.append\(2\)
+""")
+
+test.pass_test()
diff --git a/test/Errors/Exception.py b/test/Errors/Exception.py
new file mode 100644 (file)
index 0000000..710c819
--- /dev/null
@@ -0,0 +1,83 @@
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+import string
+
+import TestSCons
+
+test = TestSCons.TestSCons(match = TestSCons.match_re_dotall)
+
+test.write('SConstruct', """\
+def foo(env, target, source):
+    print str(target[0])
+    open(str(target[0]), 'wt').write('foo')
+
+def exit(env, target, source):
+    raise Exception('exit')
+
+env = Environment(BUILDERS = { 'foo'  : Builder(action=foo),
+                               'exit' : Builder(action=exit) })
+
+env.foo('foo.out', 'foo.in')
+env.exit('exit.out', 'exit.in')
+""")
+
+test.write('foo.in', 'foo\n')
+
+test.write('exit.in', 'exit\n')
+
+# print_exception doesn't always show a source line if the source file
+# no longer exists or that line in the source file no longer exists,
+# so make sure the proper variations are supported in the following
+# regexp.
+expect = """scons: \*\*\* \[exit.out\] Exception
+Traceback \((most recent call|innermost) last\):
+(  File ".+", line \d+, in \S+
+    [^\n]+
+)*(  File ".+", line \d+, in \S+
+)*(  File ".+", line \d+, in \S+
+    [^\n]+
+)*\S.+
+"""
+
+# Build foo.out first, and expect an error when we try to build exit.out.
+test.run(arguments='foo.out exit.out', stderr=expect, status=2)
+
+# Rebuild.  foo.out should be up to date, and we should get the
+# expected error building exit.out.
+test.run(arguments='foo.out exit.out', stderr=expect, status=2)
+
+stdout = test.stdout()
+
+expect = "scons: `foo.out' is up to date."
+
+if string.find(stdout, expect) == -1:
+    print "Did not find expected string %s" % repr(expect)
+    print "STDOUT ======================================================="
+    print stdout
+    test.fail_test()
+
+test.pass_test()
diff --git a/test/Errors/InternalError.py b/test/Errors/InternalError.py
new file mode 100644 (file)
index 0000000..e90d402
--- /dev/null
@@ -0,0 +1,53 @@
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+"""
+Verify the exit status and error output if an SConstruct file
+throws an InternalError.
+"""
+
+import TestSCons
+
+test = TestSCons.TestSCons(match = TestSCons.match_re_dotall)
+
+# Test InternalError.
+test.write('SConstruct', """
+assert not globals().has_key("InternalError")
+from SCons.Errors import InternalError
+raise InternalError, 'error inside'
+""")
+
+test.run(stdout = "scons: Reading SConscript files ...\ninternal error\n",
+         stderr = r"""Traceback \((most recent call|innermost) last\):
+  File ".+", line \d+, in .+
+  File ".+", line \d+, in .+
+  File ".+", line \d+, in .+
+  File ".+SConstruct", line \d+, in .+
+    raise InternalError, 'error inside'
+InternalError: error inside
+""", status=2)
+
+test.pass_test()
diff --git a/test/Errors/NameError.py b/test/Errors/NameError.py
new file mode 100644 (file)
index 0000000..be69b50
--- /dev/null
@@ -0,0 +1,47 @@
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+"""
+Verify the exit status and error output if an SConstruct file
+throws a NameError (tries to reference a Python variable that
+doesn't exist).
+"""
+
+import TestSCons
+
+test = TestSCons.TestSCons(match = TestSCons.match_re_dotall)
+
+test.write('SConstruct', """\
+a == 1
+""")
+
+test.run(status = 2, stderr = """\
+NameError: [^\n]*
+  File ".+SConstruct", line 1:
+    a == 1
+""")
+
+test.pass_test()
diff --git a/test/Errors/SyntaxError.py b/test/Errors/SyntaxError.py
new file mode 100644 (file)
index 0000000..b9ff1ff
--- /dev/null
@@ -0,0 +1,51 @@
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+"""
+Verify the exit status and error output if an SConstruct file
+throws a SyntaxError (contains improper Python code).
+"""
+
+import TestSCons
+
+test = TestSCons.TestSCons(match = TestSCons.match_re_dotall)
+
+test.write('SConstruct', """
+a ! x
+""")
+
+test.run(stdout = "scons: Reading SConscript files ...\n",
+         stderr = """  File ".+SConstruct", line 2
+
+    a ! x
+
+      \^
+
+SyntaxError: invalid syntax
+
+""", status=2)
+
+test.pass_test()
diff --git a/test/Errors/TypeError.py b/test/Errors/TypeError.py
new file mode 100644 (file)
index 0000000..1ec14dd
--- /dev/null
@@ -0,0 +1,47 @@
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+"""
+Verify the exit status and error output if an SConstruct file
+throws a TypeError.
+"""
+
+import TestSCons
+
+test = TestSCons.TestSCons(match = TestSCons.match_re_dotall)
+
+test.write('SConstruct', """\
+a = 1
+a[2] = 3
+""")
+
+test.run(status = 2, stderr = """\
+TypeError:( 'int')? object does not support item assignment:
+  File ".+SConstruct", line 2:
+    a\[2\] = 3
+""")
+
+test.pass_test()
diff --git a/test/Errors/UserError.py b/test/Errors/UserError.py
new file mode 100644 (file)
index 0000000..69ed7d4
--- /dev/null
@@ -0,0 +1,50 @@
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+"""
+Verify the exit status and error output if an SConstruct file
+throws a UserError.
+"""
+
+import TestSCons
+
+test = TestSCons.TestSCons(match = TestSCons.match_re_dotall)
+
+test.write('SConstruct', """
+assert not globals().has_key("UserError")
+import SCons.Errors
+raise SCons.Errors.UserError, 'Depends() requires both sources and targets.'
+""")
+
+expect = """
+scons: \*\*\* Depends\(\) requires both sources and targets.
+""" + TestSCons.file_expr
+
+test.run(stdout = "scons: Reading SConscript files ...\n",
+         stderr = expect,
+         status=2)
+
+test.pass_test()
diff --git a/test/Errors/exit-status.py b/test/Errors/exit-status.py
new file mode 100644 (file)
index 0000000..946c1c4
--- /dev/null
@@ -0,0 +1,54 @@
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+"""
+Verify the exit status and error output if an external command throws
+a non-zero exit status.
+"""
+
+import TestSCons
+
+_python_ = TestSCons._python_
+
+test = TestSCons.TestSCons(match = TestSCons.match_re_dotall)
+
+test.write('build.py', '''
+import sys
+sys.exit(7)
+''')
+
+test.write('SConstruct', """
+env=Environment()
+Default(env.Command(['one.out', 'two.out'],
+                    ['foo.in'],
+                    action=r'%(_python_)s build.py'))
+""" % locals())
+
+test.write('foo.in', "foo.in\n")
+
+test.run(status=2, stderr="scons: \\*\\*\\* \\[one.out\\] Error 7\n")
+
+test.pass_test()
index bd827d1eb9186fcba65e629cba63cca3138005a5..11452b125437e1bc9e575e404f32efdc16360614 100644 (file)
@@ -47,6 +47,14 @@ def validator(key, value, environ):
     environ[key] = "v"
     environ["valid_key"] = "v"
 
+
+def old_converter (value):
+    return "old_converter"
+
+def new_converter (value, env):
+    return "new_converter"
+
+
 opts = Options('custom.py')
 opts.Add('RELEASE_BUILD',
          'Set to 1 to build a release build',
@@ -69,6 +77,18 @@ opts.Add('VALIDATE',
          validator,
          None)
 
+opts.Add('OLD_CONVERTER',
+         'An option for testing converters that take one parameter',
+         "foo",
+         None,
+         old_converter)
+
+opts.Add('NEW_CONVERTER',
+         'An option for testing converters that take two parameters',
+         "foo",
+         None,
+         new_converter)
+
 opts.Add('UNSPECIFIED',
          'An option with no value')
 
@@ -168,6 +188,14 @@ VALIDATE: An option for testing validation
     default: notset
     actual: v
 
+OLD_CONVERTER: An option for testing converters that take one parameter
+    default: foo
+    actual: old_converter
+
+NEW_CONVERTER: An option for testing converters that take two parameters
+    default: foo
+    actual: new_converter
+
 UNSPECIFIED: An option with no value
     default: None
     actual: None
index f18066b0bd29734e9389a2873bb41f249d555951..aaf3bcb6c2ba7e090ad0bc245617410f63a85bbe 100644 (file)
@@ -67,6 +67,7 @@ for source in sources:
 
 test.write(['samples', 'goodbye.c'], """\
 #include <stdio.h>
+#include <stdlib.h>
 
 int main(int argc, char *argv[])
 {
@@ -77,6 +78,7 @@ int main(int argc, char *argv[])
 
 test.write(['src', 'hello.c'], """\
 #include <stdio.h>
+#include <stdlib.h>
 
 int main(int argc, char *argv[])
 {
index ed995a93698aab55c8392990e33e2dbf54dc9b8f..791d0e0dc97031b80090f4174061a3980346132c 100644 (file)
@@ -182,7 +182,7 @@ bar_string()
 """)
 
     test.write("bar.i", """\
-%module bar
+%module \t bar
 %{
 /* Put header files here (optional) */
 %}
index a88303f576079052d396e62492e12a39a9a71160..99b578713cee709df5d7a663af5707ff3dbbd7ac 100644 (file)
@@ -28,7 +28,7 @@ __revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
 Verify that we only scan generated .h files once.
 
 This originated as a real-life bug report submitted by Scott Lystig
-Fritchie.  It's been left as-is, rather than stripped down to bear
+Fritchie.  It's been left as-is, rather than stripped down to bare
 minimum, partly because it wasn't completely clear what combination of
 factors triggered the bug Scott saw, and partly because the real-world
 complexity is valuable in its own right.
diff --git a/test/SideEffect.py b/test/SideEffect.py
deleted file mode 100644 (file)
index f7533b5..0000000
+++ /dev/null
@@ -1,211 +0,0 @@
-#!/usr/bin/env python
-#
-# __COPYRIGHT__
-#
-# Permission is hereby granted, free of charge, to any person obtaining
-# a copy of this software and associated documentation files (the
-# "Software"), to deal in the Software without restriction, including
-# without limitation the rights to use, copy, modify, merge, publish,
-# distribute, sublicense, and/or sell copies of the Software, and to
-# permit persons to whom the Software is furnished to do so, subject to
-# the following conditions:
-#
-# The above copyright notice and this permission notice shall be included
-# in all copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
-# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
-# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
-# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
-# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
-# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
-# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-#
-
-__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
-
-import os.path
-import string
-
-import TestSCons
-
-test = TestSCons.TestSCons()
-
-test.write('SConstruct', """\
-def copy(source, target):
-    open(target, "wb").write(open(source, "rb").read())
-
-def build(env, source, target):
-    copy(str(source[0]), str(target[0]))
-    if target[0].side_effects:
-        side_effect = open(str(target[0].side_effects[0]), "ab")
-        side_effect.write('%%s -> %%s\\n'%%(str(source[0]), str(target[0])))
-
-Build = Builder(action=build)
-env = Environment(BUILDERS={'Build':Build}, SUBDIR='subdir')
-env.Build('foo.out', 'foo.in')
-env.Build('bar.out', 'bar.in')
-env.Build('blat.out', 'blat.in')
-SideEffect('log.txt', ['foo.out', 'bar.out', 'blat.out'])
-env.Build('log.out', 'log.txt')
-env.Build('subdir/baz.out', 'baz.in')
-env.SideEffect(r'%s', ['blat.out', r'%s'])
-env.Build('subdir/out.out', 'subdir/out.txt')
-""" % (os.path.join('$SUBDIR', 'out.txt'),
-       os.path.join('$SUBDIR', 'baz.out')))
-
-test.write('foo.in', 'foo.in\n')
-test.write('bar.in', 'bar.in\n')
-test.write('blat.in', 'blat.in\n')
-test.write('baz.in', 'baz.in\n')
-
-test.run(arguments = 'foo.out bar.out', stdout=test.wrap_stdout("""\
-build(["foo.out"], ["foo.in"])
-build(["bar.out"], ["bar.in"])
-"""))
-
-expect = """\
-foo.in -> foo.out
-bar.in -> bar.out
-"""
-assert test.read('log.txt') == expect
-
-test.write('bar.in', 'bar.in 2 \n')
-
-test.run(arguments = 'log.txt', stdout=test.wrap_stdout("""\
-build(["bar.out"], ["bar.in"])
-build(["blat.out"], ["blat.in"])
-"""))
-
-expect = """\
-foo.in -> foo.out
-bar.in -> bar.out
-bar.in -> bar.out
-blat.in -> blat.out
-"""
-assert test.read('log.txt') == expect
-
-test.write('foo.in', 'foo.in 2 \n')
-
-test.run(arguments = ".", stdout=test.wrap_stdout("""\
-build(["foo.out"], ["foo.in"])
-build(["log.out"], ["log.txt"])
-build(["%s"], ["baz.in"])
-build(["%s"], ["%s"])
-""" % (os.path.join('subdir', 'baz.out'),
-       os.path.join('subdir', 'out.out'),
-       os.path.join('subdir', 'out.txt'))))
-
-expect = """\
-foo.in -> foo.out
-bar.in -> bar.out
-bar.in -> bar.out
-blat.in -> blat.out
-foo.in -> foo.out
-"""
-assert test.read('log.txt') == expect
-
-test.run(arguments = "-c .")
-
-test.must_not_exist(test.workpath('foo.out'))
-test.must_not_exist(test.workpath('bar.out'))
-test.must_not_exist(test.workpath('blat.out'))
-test.must_not_exist(test.workpath('log.txt'))
-
-build_lines =  [
-    'build(["bar.out"], ["bar.in"])', 
-    'build(["blat.out"], ["blat.in"])', 
-    'build(["foo.out"], ["foo.in"])', 
-    'build(["log.out"], ["log.txt"])', 
-    'build(["%s"], ["baz.in"])' % os.path.join('subdir', 'baz.out'),
-    'build(["%s"], ["%s"])' % (os.path.join('subdir', 'out.out'),
-                           os.path.join('subdir', 'out.txt')),
-]
-test.run(arguments = "-j 4 .")
-output = test.stdout()
-for line in build_lines:
-    test.fail_test(string.find(output, line) == -1)
-
-log_lines = [
-    'bar.in -> bar.out',
-    'blat.in -> blat.out',
-    'foo.in -> foo.out',
-]
-log = test.read('log.txt')
-for line in log_lines:
-    test.fail_test(string.find(log, line) == -1)
-
-test.write('SConstruct', 
-"""
-import os.path
-import os
-
-def copy(source, target):
-    open(target, "wb").write(open(source, "rb").read())
-
-def build(env, source, target):
-    copy(str(source[0]), str(target[0]))
-    if target[0].side_effects:
-        try: os.mkdir('log')
-        except: pass
-        copy(str(target[0]), os.path.join('log', str(target[0])))
-
-Build = Builder(action=build)
-env = Environment(BUILDERS={'Build':Build})
-env.Build('foo.out', 'foo.in')
-env.Build('bar.out', 'bar.in')
-env.Build('blat.out', 'blat.in')
-env.SideEffect(Dir('log'), ['foo.out', 'bar.out', 'blat.out'])
-""")
-
-test.run(arguments='foo.out')
-
-test.must_exist(test.workpath('foo.out'))
-test.must_exist(test.workpath('log/foo.out'))
-test.must_not_exist(test.workpath('log', 'bar.out'))
-test.must_not_exist(test.workpath('log', 'blat.out'))
-
-test.run(arguments='log')
-test.must_exist(test.workpath('log', 'bar.out'))
-test.must_exist(test.workpath('log', 'blat.out'))
-
-test.write('SConstruct', 
-"""
-def copy(source, target):
-    open(target, "wb").write(open(source, "rb").read())
-
-def build(env, source, target):
-    copy(str(source[0]), str(target[0]))
-    if target[0].side_effects:
-        side_effect = open(str(target[0].side_effects[0]), "ab")
-        side_effect.write('%s -> %s\\n'%(str(source[0]), str(target[0])))
-
-Build = Builder(action=build)
-env = Environment(BUILDERS={'Build':Build})
-Export('env')
-SConscript('SConscript', build_dir='build', duplicate=0)""")
-
-test.write('SConscript', """
-Import('env')
-env.Build('foo.out', 'foo.in')
-env.Build('bar.out', 'bar.in')
-env.Build('blat.out', 'blat.in')
-env.SideEffect('log.txt', ['foo.out', 'bar.out', 'blat.out'])
-""")
-
-test.write('foo.in', 'foo.in\n')
-test.write('bar.in', 'bar.in\n')
-
-build_foo_out = os.path.join('build', 'foo.out')
-build_bar_out = os.path.join('build', 'bar.out')
-
-test.run(arguments = '%s %s' % (build_foo_out, build_bar_out))
-
-expect = """\
-foo.in -> %s
-bar.in -> %s
-""" % (build_foo_out, build_bar_out)
-
-assert test.read('build/log.txt') == expect
-
-test.pass_test()
diff --git a/test/SideEffect/basic.py b/test/SideEffect/basic.py
new file mode 100644 (file)
index 0000000..b01020b
--- /dev/null
@@ -0,0 +1,108 @@
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+"""
+Verify basic operation of the SideEffect() method, using a "log
+file" as the side effect "target."
+"""
+
+import os.path
+import string
+
+import TestSCons
+
+test = TestSCons.TestSCons()
+
+test.write('SConstruct', """\
+def copy(source, target):
+    open(target, "wb").write(open(source, "rb").read())
+
+def build(env, source, target):
+    copy(str(source[0]), str(target[0]))
+    if target[0].side_effects:
+        side_effect = open(str(target[0].side_effects[0]), "ab")
+        side_effect.write('%%s -> %%s\\n'%%(str(source[0]), str(target[0])))
+
+Build = Builder(action=build)
+env = Environment(BUILDERS={'Build':Build}, SUBDIR='subdir')
+env.Build('foo.out', 'foo.in')
+env.Build('bar.out', 'bar.in')
+env.Build('blat.out', 'blat.in')
+SideEffect('log.txt', ['foo.out', 'bar.out', 'blat.out'])
+env.Build('log.out', 'log.txt')
+env.Build('subdir/baz.out', 'baz.in')
+env.SideEffect(r'%s', ['blat.out', r'%s'])
+env.Build('subdir/out.out', 'subdir/out.txt')
+""" % (os.path.join('$SUBDIR', 'out.txt'),
+       os.path.join('$SUBDIR', 'baz.out')))
+
+test.write('foo.in', 'foo.in\n')
+test.write('bar.in', 'bar.in\n')
+test.write('blat.in', 'blat.in\n')
+test.write('baz.in', 'baz.in\n')
+
+test.run(arguments = 'foo.out bar.out', stdout=test.wrap_stdout("""\
+build(["foo.out"], ["foo.in"])
+build(["bar.out"], ["bar.in"])
+"""))
+
+expect = """\
+foo.in -> foo.out
+bar.in -> bar.out
+"""
+test.must_match('log.txt', expect)
+
+test.write('bar.in', 'bar.in 2 \n')
+
+test.run(arguments = 'log.txt', stdout=test.wrap_stdout("""\
+build(["bar.out"], ["bar.in"])
+build(["blat.out"], ["blat.in"])
+"""))
+
+expect = expect + """\
+bar.in -> bar.out
+blat.in -> blat.out
+"""
+test.must_match('log.txt', expect)
+
+test.write('foo.in', 'foo.in 2 \n')
+
+test.run(arguments = ".", stdout=test.wrap_stdout("""\
+build(["foo.out"], ["foo.in"])
+build(["log.out"], ["log.txt"])
+build(["%s"], ["baz.in"])
+build(["%s"], ["%s"])
+""" % (os.path.join('subdir', 'baz.out'),
+       os.path.join('subdir', 'out.out'),
+       os.path.join('subdir', 'out.txt'))))
+
+expect = expect + """\
+foo.in -> foo.out
+"""
+
+test.must_match('log.txt', expect)
+
+test.pass_test()
diff --git a/test/SideEffect/build_dir.py b/test/SideEffect/build_dir.py
new file mode 100644 (file)
index 0000000..d33e3d4
--- /dev/null
@@ -0,0 +1,77 @@
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+"""
+Verify correct operation of SideEffect() when an SConscript()
+build_dir is used.
+"""
+
+import os.path
+
+import TestSCons
+
+test = TestSCons.TestSCons()
+
+test.write('SConstruct', 
+"""
+def copy(source, target):
+    open(target, "wb").write(open(source, "rb").read())
+
+def build(env, source, target):
+    copy(str(source[0]), str(target[0]))
+    if target[0].side_effects:
+        side_effect = open(str(target[0].side_effects[0]), "ab")
+        side_effect.write('%s -> %s\\n'%(str(source[0]), str(target[0])))
+
+Build = Builder(action=build)
+env = Environment(BUILDERS={'Build':Build})
+Export('env')
+SConscript('SConscript', build_dir='build', duplicate=0)""")
+
+test.write('SConscript', """
+Import('env')
+env.Build('foo.out', 'foo.in')
+env.Build('bar.out', 'bar.in')
+env.Build('blat.out', 'blat.in')
+env.SideEffect('log.txt', ['foo.out', 'bar.out', 'blat.out'])
+""")
+
+test.write('foo.in', 'foo.in\n')
+test.write('bar.in', 'bar.in\n')
+
+build_foo_out = os.path.join('build', 'foo.out')
+build_bar_out = os.path.join('build', 'bar.out')
+
+test.run(arguments = '%s %s' % (build_foo_out, build_bar_out))
+
+expect = """\
+foo.in -> %s
+bar.in -> %s
+""" % (build_foo_out, build_bar_out)
+
+test.must_match('build/log.txt', expect)
+
+test.pass_test()
diff --git a/test/SideEffect/directory.py b/test/SideEffect/directory.py
new file mode 100644 (file)
index 0000000..5ede853
--- /dev/null
@@ -0,0 +1,76 @@
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+"""
+Verify that a directory (Dir()) works as a SideEffect() "target."
+"""
+
+import os.path
+import string
+
+import TestSCons
+
+test = TestSCons.TestSCons()
+
+test.write('SConstruct', """\
+import os.path
+import os
+
+def copy(source, target):
+    open(target, "wb").write(open(source, "rb").read())
+
+def build(env, source, target):
+    copy(str(source[0]), str(target[0]))
+    if target[0].side_effects:
+        try: os.mkdir('log')
+        except: pass
+        copy(str(target[0]), os.path.join('log', str(target[0])))
+
+Build = Builder(action=build)
+env = Environment(BUILDERS={'Build':Build})
+env.Build('foo.out', 'foo.in')
+env.Build('bar.out', 'bar.in')
+env.Build('blat.out', 'blat.in')
+env.SideEffect(Dir('log'), ['foo.out', 'bar.out', 'blat.out'])
+""")
+
+test.write('foo.in', "foo.in\n")
+test.write('bar.in', "bar.in\n")
+test.write('blat.in', "blat.in\n")
+
+test.run(arguments='foo.out')
+
+test.must_exist(test.workpath('foo.out'))
+test.must_exist(test.workpath('log/foo.out'))
+test.must_not_exist(test.workpath('log', 'bar.out'))
+test.must_not_exist(test.workpath('log', 'blat.out'))
+
+test.run(arguments='log')
+
+test.must_exist(test.workpath('log', 'bar.out'))
+test.must_exist(test.workpath('log', 'blat.out'))
+
+test.pass_test()
diff --git a/test/SideEffect/parallel.py b/test/SideEffect/parallel.py
new file mode 100644 (file)
index 0000000..7c9bc27
--- /dev/null
@@ -0,0 +1,126 @@
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+"""
+Verify that targets with the same SideEffect are not built in parallel
+when the -j option is used.
+"""
+
+import string
+
+import TestSCons
+
+_python_ = TestSCons._python_
+
+test = TestSCons.TestSCons()
+
+test.write('build.py', """\
+import os
+import sys
+import time
+
+lockdir = 'build.lock'
+logfile = 'log.txt'
+
+try:
+    os.mkdir(lockdir)
+except OSError, e:
+    msg = 'could not create lock directory: %s\\n' % e
+    sys.stderr.write(msg)
+    sys.exit(1)
+
+src, target = sys.argv[1:]
+
+open(logfile, 'ab').write('%s -> %s\\n' % (src, target))
+
+# Give the other threads a chance to start.
+time.sleep(1)
+
+os.rmdir(lockdir)
+""")
+
+test.write('SConstruct', """\
+Build = Builder(action=r'%(_python_)s build.py $SOURCE $TARGET')
+env = Environment(BUILDERS={'Build':Build})
+env.Build('h1.out', 'h1.in')
+env.Build('g2.out', 'g2.in')
+env.Build('f3.out', 'f3.in')
+SideEffect('log.txt', ['h1.out', 'g2.out', 'f3.out'])
+env.Build('log.out', 'log.txt')
+""" % locals())
+
+test.write('h1.in', 'h1.in\n')
+test.write('g2.in', 'g2.in\n')
+test.write('f3.in', 'f3.in\n')
+test.write('baz.in', 'baz.in\n')
+
+
+test.run(arguments = "-j 4 .")
+
+stdout = test.stdout()
+
+
+build_lines =  [
+    'build.py h1.in h1.out', 
+    'build.py g2.in g2.out', 
+    'build.py f3.in f3.out', 
+]
+
+missing = []
+for line in build_lines:
+    if string.find(stdout, line) == -1:
+        missing.append(line)
+
+if missing:
+    print "===== standard output is missing the following lines:"
+    print string.join(missing, '\n')
+    print "===== STDOUT ========================================"
+    print stdout
+    test.fail_test()
+
+
+log = test.read('log.txt')
+
+log_lines = [
+    'f3.in -> f3.out',
+    'h1.in -> h1.out',
+    'g2.in -> g2.out',
+]
+
+missing = []
+for line in log_lines:
+    if string.find(log, line) == -1:
+        missing.append(line)
+
+if missing:
+    print "===== log file 'log.txt' is missing the following lines:"
+    print string.join(missing, '\n')
+    print "===== STDOUT ==========================================="
+    print log
+    test.fail_test()
+
+
+test.pass_test()
diff --git a/test/Subst/AllowSubstExceptions.py b/test/Subst/AllowSubstExceptions.py
new file mode 100644 (file)
index 0000000..5709428
--- /dev/null
@@ -0,0 +1,93 @@
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+"""
+XXX Put a description of the test here.
+"""
+
+import TestSCons
+
+test = TestSCons.TestSCons()
+
+test.write('SConstruct', """\
+import SCons.Errors
+
+env = Environment(INDEX = [0, 1])
+
+assert env.subst('$NAME') == ''
+assert env.subst('${NAME}') == ''
+assert env.subst('${INDEX[999]}') == ''
+
+assert env.subst_list('$NAME') == [[]]
+assert env.subst_list('${NAME}') == [[]]
+assert env.subst_list('${INDEX[999]}') == [[]]
+
+AllowSubstExceptions()
+
+try: env.subst('$NAME')
+except SCons.Errors.UserError, e: print e
+else: raise Exception, "did not catch expected SCons.Errors.UserError"
+
+try: env.subst('${NAME}')
+except SCons.Errors.UserError, e: print e
+else: raise Exception, "did not catch expected SCons.Errors.UserError"
+
+try: env.subst('${INDEX[999]}')
+except SCons.Errors.UserError, e: print e
+else: raise Exception, "did not catch expected SCons.Errors.UserError"
+
+try: env.subst_list('$NAME')
+except SCons.Errors.UserError, e: print e
+else: raise Exception, "did not catch expected SCons.Errors.UserError"
+
+try: env.subst_list('${NAME}')
+except SCons.Errors.UserError, e: print e
+else: raise Exception, "did not catch expected SCons.Errors.UserError"
+
+try: env.subst_list('${INDEX[999]}')
+except SCons.Errors.UserError, e: print e
+else: raise Exception, "did not catch expected SCons.Errors.UserError"
+
+
+
+try: env.subst('${1/0}')
+except SCons.Errors.UserError, e: print e
+else: raise Exception, "did not catch expected SCons.Errors.UserError"
+
+try: env.subst_list('${1/0}')
+except SCons.Errors.UserError, e: print e
+else: raise Exception, "did not catch expected SCons.Errors.UserError"
+
+AllowSubstExceptions(ZeroDivisionError)
+
+assert env.subst('${1/0}') == ''
+assert env.subst_list('${1/0}') == [[]]
+""")
+
+test.run()
+#print test.stdout()
+
+test.pass_test()
diff --git a/test/Subst/SyntaxError.py b/test/Subst/SyntaxError.py
new file mode 100644 (file)
index 0000000..b37cdf4
--- /dev/null
@@ -0,0 +1,74 @@
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+"""
+Verify the exit status and error output if variable expansion
+throws a SyntaxError.
+"""
+
+import TestSCons
+
+test = TestSCons.TestSCons(match = TestSCons.match_re_dotall)
+
+
+expect_build = r"""scons: \*\*\*%s SyntaxError `invalid syntax( \((<string>, )?line 1\))?' trying to evaluate `%s'
+"""
+
+expect_read = "\n" + expect_build + TestSCons.file_expr
+
+
+# Syntax errors at SConscript read time:
+test.write('SConstruct', """\
+env = Environment()
+env.subst('$foo.bar.3.0')
+""")
+
+test.run(status=2, stderr=expect_read % ('', r'\$foo\.bar\.3\.0'))
+
+
+
+test.write('SConstruct', """\
+env = Environment()
+env.subst('${x ! y}')
+""")
+
+test.run(status=2, stderr=expect_read % ('', r'\$\{x \! y\}'))
+
+
+
+# Syntax errors at build time:
+test.write('SConstruct', """\
+env = Environment()
+env.Command('foo.bar', [], '$foo.bar.3.0')
+""")
+
+expect = expect_build % (r' \[foo\.bar\]', r'\$foo\.bar\.3\.0')
+
+test.run(status=2, stderr=expect)
+
+
+
+test.pass_test()
diff --git a/test/Subst/TypeError.py b/test/Subst/TypeError.py
new file mode 100644 (file)
index 0000000..d042b6d
--- /dev/null
@@ -0,0 +1,92 @@
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+"""
+Verify the exit status and error output if variable expansion
+throws a TypeError.
+"""
+
+import TestSCons
+
+test = TestSCons.TestSCons(match = TestSCons.match_re_dotall)
+
+
+
+expect_build = r"""scons: \*\*\*%s TypeError `(unsubscriptable object|'NoneType' object is unsubscriptable)' trying to evaluate `%s'
+"""
+
+expect_read = "\n" + expect_build + TestSCons.file_expr
+
+# Type errors at SConscript read time:
+test.write('SConstruct', """\
+env = Environment(NONE = None)
+env.subst('${NONE[0]}')
+""")
+
+test.run(status=2, stderr=expect_read % ('', r'\$\{NONE\[0\]\}'))
+
+# Type errors at build time:
+test.write('SConstruct', """\
+env = Environment(NONE = None)
+env.Command('foo.bar', [], '${NONE[0]}')
+""")
+
+expect = expect_build % (r' \[foo\.bar\]', r'\$\{NONE\[0\]\}')
+
+test.run(status=2, stderr=expect)
+
+
+
+expect_build = r"""scons: \*\*\*%s TypeError `(not enough arguments; expected 3, got 1|func\(\) takes exactly 3 arguments \(1 given\))' trying to evaluate `%s'
+"""
+
+expect_read = "\n" + expect_build + TestSCons.file_expr
+
+# Type errors at SConscript read time:
+test.write('SConstruct', """\
+def func(a, b, c):
+    pass
+env = Environment(func = func)
+env.subst('${func(1)}')
+""")
+
+test.run(status=2, stderr=expect_read % ('', r'\$\{func\(1\)\}'))
+
+# Type errors at build time:
+test.write('SConstruct', """\
+def func(a, b, c):
+    pass
+env = Environment(func = func)
+env.Command('foo.bar', [], '${func(1)}')
+""")
+
+expect = expect_build % (r' \[foo\.bar\]', r'\$\{func\(1\)\}')
+
+test.run(status=2, stderr=expect)
+
+
+
+test.pass_test()
index 7ea359b4425c2e8d1359fdc2d14ec82a4bb1eebc..eaea49a0cdf316c9cc57b0e6d8acea580bbc8eb5 100644 (file)
@@ -174,9 +174,17 @@ Run \texttt{latex}, then \texttt{bibtex}, then \texttt{latex} twice again \cite{
 
     test.run(stderr = None)
     output_lines = string.split(test.stdout(), '\n')
-    reruns = filter(lambda x: x == 'latex rerun.tex', output_lines)
-    test.fail_test(len(reruns) != 2)
-    bibtex = filter(lambda x: x == 'bibtex bibtex-test', output_lines)
-    test.fail_test(len(bibtex) != 1)
+
+    reruns = filter(lambda x: string.find(x, 'latex rerun.tex') != -1, output_lines)
+    if len(reruns) != 2:
+        print "Expected 2 latex calls, got %s:" % len(reruns)
+        print string.join(reruns, '\n')
+        test.fail_test()
+
+    bibtex = filter(lambda x: string.find(x, 'bibtex bibtex-test') != -1, output_lines)
+    if len(bibtex) != 1:
+        print "Expected 1 bibtex call, got %s:" % len(bibtex)
+        print string.join(bibtex, '\n')
+        test.fail_test()
 
 test.pass_test()
diff --git a/test/TEX/build_dir.py b/test/TEX/build_dir.py
new file mode 100644 (file)
index 0000000..146f6e1
--- /dev/null
@@ -0,0 +1,256 @@
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+"""
+Test creation of a fully-featured TeX document (with bibliography
+and index) in a build_dir.
+
+Test courtesy Rob Managan.
+"""
+
+import TestSCons
+
+test = TestSCons.TestSCons()
+
+test.subdir(['docs'])
+
+
+test.write(['SConstruct'], """\
+import os
+
+env = Environment(ENV = { 'PATH' : os.environ['PATH'] })
+Export(['env'])
+
+SConscript(os.path.join('docs', 'SConscript'),
+           build_dir=os.path.join('mybuild','docs'),
+           duplicate=1)
+""")
+
+
+test.write(['docs', 'SConscript'], """\
+Import('env')
+
+test_dvi = env.DVI(source='test.tex')
+testpdf = env.PDF(source=test_dvi)
+""")
+
+
+test.write(['docs', 'Fig1.ps'], """\
+%!PS-Adobe-2.0 EPSF-2.0
+%%Title: Fig1.fig
+%%Creator: fig2dev Version 3.2 Patchlevel 4
+%%CreationDate: Tue Apr 25 09:56:11 2006
+%%For: managan@mangrove.llnl.gov (Rob Managan)
+%%BoundingBox: 0 0 98 98
+%%Magnification: 1.0000
+%%EndComments
+/$F2psDict 200 dict def
+$F2psDict begin
+$F2psDict /mtrx matrix put
+/col-1 {0 setgray} bind def
+/col0 {0.000 0.000 0.000 srgb} bind def
+
+end
+save
+newpath 0 98 moveto 0 0 lineto 98 0 lineto 98 98 lineto closepath clip newpath
+-24.9 108.2 translate
+1 -1 scale
+
+/cp {closepath} bind def
+/ef {eofill} bind def
+/gr {grestore} bind def
+/gs {gsave} bind def
+/rs {restore} bind def
+/l {lineto} bind def
+/m {moveto} bind def
+/rm {rmoveto} bind def
+/n {newpath} bind def
+/s {stroke} bind def
+/slc {setlinecap} bind def
+/slj {setlinejoin} bind def
+/slw {setlinewidth} bind def
+/srgb {setrgbcolor} bind def
+/sc {scale} bind def
+/sf {setfont} bind def
+/scf {scalefont} bind def
+/tr {translate} bind def
+ /DrawEllipse {
+       /endangle exch def
+       /startangle exch def
+       /yrad exch def
+       /xrad exch def
+       /y exch def
+       /x exch def
+       /savematrix mtrx currentmatrix def
+       x y tr xrad yrad sc 0 0 1 startangle endangle arc
+       closepath
+       savematrix setmatrix
+       } def
+
+/$F2psBegin {$F2psDict begin /$F2psEnteredState save def} def
+/$F2psEnd {$F2psEnteredState restore end} def
+
+$F2psBegin
+10 setmiterlimit
+ 0.06299 0.06299 sc
+%
+% Fig objects follow
+% 
+7.500 slw
+% Ellipse
+n 1170 945 766 766 0 360 DrawEllipse gs col0 s gr
+
+$F2psEnd
+rs
+""")
+
+
+test.write(['docs', 'Fig1.tex'],
+r"""\begin{picture}(0,0)%
+\includegraphics{Fig1.ps}%
+\end{picture}%
+\setlength{\unitlength}{4144sp}%
+%
+\begingroup\makeatletter\ifx\SetFigFont\undefined%
+\gdef\SetFigFont#1#2#3#4#5{%
+  \reset@font\fontsize{#1}{#2pt}%
+  \fontfamily{#3}\fontseries{#4}\fontshape{#5}%
+  \selectfont}%
+\fi\endgroup%
+\begin{picture}(1548,1546)(397,-879)
+\put(856,-196){\makebox(0,0)[lb]{\smash{\SetFigFont{12}{14.4}{\rmdefault}{\mddefault}{\updefault}{\color[rgb]{0,0,0}center $r_0$}%
+}}}
+\end{picture}
+""")
+
+
+test.write(['docs', 'test.bib'], """\
+%% This BibTeX bibliography file was created using BibDesk.
+%% http://bibdesk.sourceforge.net/
+
+
+%% Created for Rob Managan at 2006-11-15 12:53:16 -0800 
+
+
+%% Saved with string encoding Western (ASCII) 
+
+
+
+@techreport{Managan:2006fk,
+       Author = {Robert Managan},
+       Date-Added = {2006-11-15 12:51:30 -0800},
+       Date-Modified = {2006-11-15 12:52:35 -0800},
+       Institution = {none},
+       Month = {November},
+       Title = {A Test Paper},
+       Year = {2006}}
+""")
+
+
+test.write(['docs', 'test.tex'],
+r"""\documentclass{report}
+
+\usepackage{graphicx}
+\usepackage{epsfig,color} % for .tex version of figures if we go that way
+
+\usepackage{makeidx}
+\makeindex
+
+\begin{document}
+\title{Report Title}
+
+\author{A. N. Author}
+\maketitle 
+\begin{abstract}
+there is no abstract
+\end{abstract}
+
+\tableofcontents
+\listoffigures
+
+\chapter{Introduction}
+
+The introduction is short.
+
+\index{Acknowledgements}
+
+\section{Acknowledgements}
+
+The Acknowledgements are show as well \cite{Managan:2006fk}.  
+
+\index{Getting the Report}
+
+To get a hard copy of this report call me.
+
+\begin{figure}[htbp]
+\begin{center}
+\input{Fig1.tex} % testing figure variant that uses TeX labeling
+\caption{Zone and Node indexing}
+\label{fig1}
+\end{center}
+\end{figure}
+
+All done now.
+
+\bibliographystyle{unsrt}
+\bibliography{test}
+\newpage
+
+\printindex
+
+\end{document}
+""")
+
+
+# makeindex will write status messages to stderr (grrr...), so ignore it.
+test.run(arguments = '.', stderr=None)
+
+
+# All (?) the files we expect will get created in the build_dir
+# (mybuild/docs) and not in the srcdir (docs).
+files = [
+    'test.aux',
+    'test.bbl',
+    'test.blg',
+    'test.dvi',
+    'test.idx',
+    'test.ilg',
+    'test.ind',
+    'test.lof',
+    'test.log',
+    'test.pdf',
+    'test.toc',
+]
+
+for f in files:
+    test.must_exist(['mybuild', 'docs', f])
+    test.must_not_exist(['docs', f])
+
+
+test.pass_test()
diff --git a/test/TEX/subdir-input.py b/test/TEX/subdir-input.py
new file mode 100644 (file)
index 0000000..b92ab73
--- /dev/null
@@ -0,0 +1,80 @@
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+"""
+Verify that we execute TeX in a subdirectory (if that's where the document
+resides) by checking that all the auxiliary files get created there and
+not in the top-level directory.
+
+Also check that we find files
+
+Test case courtesy Joel B. Mohler.
+"""
+
+import TestSCons
+
+test = TestSCons.TestSCons()
+
+test.subdir('sub')
+
+test.write('SConstruct', """\
+PDF( 'sub/x.tex' )
+DVI( 'sub/x.tex' )
+""")
+
+test.write(['sub', 'x.tex'],
+r"""\documentclass{article}
+\begin{document}
+Hi there.
+\input{y}
+\end{document}
+""")
+
+test.write(['sub', 'y.tex'], """\
+Sub-document 1
+""")
+
+test.run(arguments = '.')
+
+test.must_exist(['sub', 'x.aux'])
+test.must_exist(['sub', 'x.dvi'])
+test.must_exist(['sub', 'x.log'])
+test.must_exist(['sub', 'x.pdf'])
+
+test.must_not_exist('x.aux')
+test.must_not_exist('x.dvi')
+test.must_not_exist('x.log')
+test.must_not_exist('x.pdf')
+
+test.up_to_date(arguments = '.')
+
+test.write(['sub', 'y.tex'], """\
+Sub-document 2
+""")
+
+test.not_up_to_date(arguments = '.')
+
+test.pass_test()
diff --git a/test/errors.py b/test/errors.py
deleted file mode 100644 (file)
index 6e0a05f..0000000
+++ /dev/null
@@ -1,224 +0,0 @@
-#!/usr/bin/env python
-#
-# __COPYRIGHT__
-#
-# Permission is hereby granted, free of charge, to any person obtaining
-# a copy of this software and associated documentation files (the
-# "Software"), to deal in the Software without restriction, including
-# without limitation the rights to use, copy, modify, merge, publish,
-# distribute, sublicense, and/or sell copies of the Software, and to
-# permit persons to whom the Software is furnished to do so, subject to
-# the following conditions:
-#
-# The above copyright notice and this permission notice shall be included
-# in all copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
-# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
-# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
-# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
-# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
-# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
-# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-#
-
-__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
-
-import TestCmd
-import TestSCons
-import string
-import sys
-
-_python_ = TestSCons._python_
-
-test = TestSCons.TestSCons(match = TestCmd.match_re_dotall)
-
-
-
-test.write('foo.in', 'foo\n')
-
-test.write('exit.in', 'exit\n')
-
-test.write('SConstruct', """\
-import sys
-
-def foo(env, target, source):
-    print str(target[0])
-    open(str(target[0]), 'wt').write('foo')
-
-def exit(env, target, source):
-    raise 'exit'
-
-env = Environment(BUILDERS = { 'foo'  : Builder(action=foo),
-                               'exit' : Builder(action=exit) })
-
-env.foo('foo.out', 'foo.in')
-env.exit('exit.out', 'exit.in')
-""")
-
-# print_exception doesn't always show a source line if the source file
-# no longer exists or that line in the source file no longer exists,
-# so make sure the proper variations are supported in the following
-# regexp.
-stderr = """scons: \*\*\* \[exit.out\] Exception
-Traceback \((most recent call|innermost) last\):
-(  File ".+", line \d+, in \S+
-    [^\n]+
-)*(  File ".+", line \d+, in \S+
-)*(  File ".+", line \d+, in \S+
-    [^\n]+
-)*\S.+
-"""
-
-test.run(arguments='foo.out exit.out', stderr=stderr, status=2)
-
-test.run(arguments='foo.out exit.out', stderr=stderr, status=2)
-assert string.find(test.stdout(), "scons: `foo.out' is up to date.") != -1, test.stdout()
-
-
-
-# Test AttributeError.
-test.write('SConstruct', """\
-a = 1
-a.append(2)
-""")
-
-test.run(status = 2, stderr = """\
-AttributeError: 'int' object has no attribute 'append':
-  File ".+SConstruct", line 2:
-    a.append\(2\)
-""")
-
-
-
-# Test NameError.
-test.write('SConstruct', """\
-a == 1
-""")
-
-test.run(status = 2, stderr = """\
-NameError: [^\n]*
-  File ".+SConstruct", line 1:
-    a == 1
-""")
-
-
-
-# Test SyntaxError.
-test.write('SConstruct', """
-a ! x
-""")
-
-test.run(stdout = "scons: Reading SConscript files ...\n",
-         stderr = """  File ".+SConstruct", line 2
-
-    a ! x
-
-      \^
-
-SyntaxError: invalid syntax
-
-""", status=2)
-
-
-
-# Test TypeError.
-test.write('SConstruct', """\
-a = 1
-a[2] = 3
-""")
-
-test.run(status = 2, stderr = """\
-TypeError: object does not support item assignment:
-  File ".+SConstruct", line 2:
-    a\[2\] = 3
-""")
-
-
-
-# Test UserError.
-test.write('SConstruct', """
-assert not globals().has_key("UserError")
-import SCons.Errors
-raise SCons.Errors.UserError, 'Depends() require both sources and targets.'
-""")
-
-test.run(stdout = "scons: Reading SConscript files ...\n",
-         stderr = """
-scons: \*\*\* Depends\(\) require both sources and targets.
-File ".+SConstruct", line 4, in \?
-""", status=2)
-
-
-
-# Test InternalError.
-test.write('SConstruct', """
-assert not globals().has_key("InternalError")
-from SCons.Errors import InternalError
-raise InternalError, 'error inside'
-""")
-
-test.run(stdout = "scons: Reading SConscript files ...\ninternal error\n",
-         stderr = r"""Traceback \((most recent call|innermost) last\):
-  File ".+", line \d+, in .+
-  File ".+", line \d+, in .+
-  File ".+", line \d+, in .+
-  File ".+SConstruct", line \d+, in \?
-    raise InternalError, 'error inside'
-InternalError: error inside
-""", status=2)
-
-test.write('build.py', '''
-import sys
-sys.exit(2)
-''')
-
-
-
-# Test ...
-test.write('SConstruct', """
-env=Environment()
-Default(env.Command(['one.out', 'two.out'],
-                    ['foo.in'],
-                    action=r'%(_python_)s build.py'))
-""" % locals())
-
-test.run(status=2, stderr="scons: \\*\\*\\* \\[one.out\\] Error 2\n")
-
-
-
-# Test syntax errors when trying to expand construction variables.
-test.write('SConstruct', """\
-env = Environment()
-env.subst('$foo.bar.3.0')
-""")
-
-test.run(status=2, stderr="""
-scons: \*\*\* Syntax error `invalid syntax( \(line 1\))?' trying to evaluate `\$foo\.bar\.3\.0'
-File "[^"]+", line \d+, in \S+
-""")
-
-test.write('SConstruct', """\
-env = Environment()
-env.subst_list('$foo.3.0.x')
-""")
-
-test.run(status=2, stderr="""
-scons: \*\*\* Syntax error `invalid syntax( \(line 1\))?' trying to evaluate `\$foo\.3\.0\.x'
-File "[^"]+", line \d+, in \S+
-""")
-
-#Test syntax errors when trying to expand construction variables at build time:
-test.write('SConstruct', """\
-env = Environment()
-env.Command('foo.bar', [], '$foo.bar.3.0')
-""")
-
-test.run(status=2, stderr=r"""scons: \*\*\* \[foo\.bar\] Syntax error `invalid syntax( \(line 1\))?' trying to evaluate `\$foo\.bar\.3\.0'
-""")
-
-
-
-
-
-test.pass_test()
index bb070ab3cd85f9c8a6ccce826ad32a9efa14e61d..92797df95e8ad9f461a39926f4b3067553fcca75 100644 (file)
@@ -29,6 +29,9 @@ Verify that we can import and use the contents of Platform and Tool
 modules directly.
 """
 
+import re
+import sys
+
 import TestSCons
 
 test = TestSCons.TestSCons()
@@ -49,10 +52,10 @@ platforms = [
 
 for platform in platforms:
     test.write('SConstruct', """
-env = Environment(platform = '%s')
-import SCons.Platform.%s
-x = SCons.Platform.%s.generate
-""" % (platform, platform, platform))
+env = Environment(platform = '%(platform)s')
+import SCons.Platform.%(platform)s
+x = SCons.Platform.%(platform)s.generate
+""" % locals())
     test.run()
 
 tools = [
@@ -139,46 +142,33 @@ tools = [
     'zip',
 ]
 
-# Intel no compiler warning..
-intel_no_compiler_fmt = """
-scons: warning: Failed to find Intel compiler for version='None', abi='%(abi)s'
-File "%(SConstruct_path)s", line 1, in ?
-"""
-
-abi = 'ia32'
-intel_no_compiler_32_warning = intel_no_compiler_fmt % locals()
+if sys.platform == 'win32':
+    tools.extend([
+        '386asm',
+        'linkloc',
+    ])
 
-abi = 'x86_64'
-intel_no_compiler_64_warning = intel_no_compiler_fmt % locals()
+# Intel no compiler warning..
+intel_no_compiler_warning = """
+scons: warning: Failed to find Intel compiler for version='None', abi='[^']*'
+""" + TestSCons.file_expr
 
 # Intel no top dir warning.
-intel_no_top_dir_fmt = """
-scons: warning: Can't find Intel compiler top dir for version='None', abi='%(abi)s'
-File "%(SConstruct_path)s", line 1, in ?
-""" % locals()
-
-abi = 'ia32'
-intel_no_top_dir_32_warning = intel_no_top_dir_fmt % locals()
-
-abi = 'x86_64'
-intel_no_top_dir_64_warning = intel_no_top_dir_fmt % locals()
+intel_no_top_dir_warning = """
+scons: warning: Can't find Intel compiler top dir for version='None', abi='[^']*'
+""" + TestSCons.file_expr
 
 # Intel no license directory warning
-intel_license_warning = """
+intel_license_warning = re.escape("""
 scons: warning: Intel license dir was not found.  Tried using the INTEL_LICENSE_FILE environment variable (), the registry () and the default path (C:\Program Files\Common Files\Intel\Licenses).  Using the default path as a last resort.
-File "%(SConstruct_path)s", line 1, in ?
-""" % locals()
+""") + TestSCons.file_expr
 
 intel_warnings = [
-    intel_license_warning,
-    intel_no_compiler_32_warning,
-    intel_no_compiler_32_warning + intel_license_warning,
-    intel_no_compiler_64_warning,
-    intel_no_compiler_64_warning + intel_license_warning,
-    intel_no_top_dir_32_warning,
-    intel_no_top_dir_32_warning + intel_license_warning,
-    intel_no_top_dir_64_warning,
-    intel_no_top_dir_64_warning + intel_license_warning,
+    re.compile(intel_license_warning),
+    re.compile(intel_no_compiler_warning),
+    re.compile(intel_no_compiler_warning + intel_license_warning),
+    re.compile(intel_no_top_dir_warning),
+    re.compile(intel_no_top_dir_warning + intel_license_warning),
 ]
 
 moc = test.where_is('moc')
@@ -189,50 +179,60 @@ if moc:
 
     qt_err = """
 scons: warning: Could not detect qt, using moc executable as a hint (QTDIR=%(qtdir)s)
-File "%(SConstruct_path)s", line 1, in ?
 """ % locals()
 
 else:
 
     qt_err = """
 scons: warning: Could not detect qt, using empty QTDIR
-File "%(SConstruct_path)s", line 1, in ?
-""" % locals()
+"""
+
+qt_warnings = [ re.compile(qt_err + TestSCons.file_expr) ]
 
 error_output = {
     'icl' : intel_warnings,
     'intelc' : intel_warnings,
-    'qt' : [qt_err],
+    'qt' : qt_warnings,
 }
 
 # An SConstruct for importing Tool names that have illegal characters
 # for Python variable names.
 indirect_import = """\
-env = Environment(tools = ['%s'])
-SCons = __import__('SCons.Tool.%s', globals(), locals(), [])
-m = getattr(SCons.Tool, '%s')
-x = m.generate
+env = Environment(tools = ['%(tool)s'])
+
+SCons = __import__('SCons.Tool.%(tool)s', globals(), locals(), [])
+m = getattr(SCons.Tool, '%(tool)s')
+env = Environment()
+m.generate(env)
 """
 
 # An SConstruct for importing Tool names "normally."
 direct_import = """\
-env = Environment(tools = ['%s'])
-import SCons.Tool.%s
-x = SCons.Tool.%s.generate
+env = Environment(tools = ['%(tool)s'])
+
+import SCons.Tool.%(tool)s
+env = Environment()
+SCons.Tool.%(tool)s.generate(env)
 """
 
 failures = []
 for tool in tools:
     if tool[0] in '0123456789' or '+' in tool:
-        test.write('SConstruct', indirect_import % (tool, tool, tool))
+        test.write('SConstruct', indirect_import % locals())
     else:
-        test.write('SConstruct', direct_import % (tool, tool, tool))
+        test.write('SConstruct', direct_import % locals())
     test.run(stderr=None)
     stderr = test.stderr()
-    if not stderr in [''] + error_output.get(tool, []):
-        print "Failed importing '%s', stderr:" % tool
-        print stderr
-        failures.append(tool)
+    if stderr:
+        matched = None
+        for expression in error_output.get(tool, []):
+            if expression.match(stderr):
+                matched = 1
+                break
+        if not matched:
+            print "Failed importing '%s', stderr:" % tool
+            print stderr
+            failures.append(tool)
 
 test.fail_test(len(failures))
 
index 7ab4129ae0d744fd7e0d1186a460280302be93e8..e709709f9513e833e09d9a910a698633be446a77 100644 (file)
@@ -177,10 +177,27 @@ test.must_match(test.workpath('foo4.out'), "foo4.in\n")
 test.must_exist(test.workpath('touch1.out'))
 test.must_exist(test.workpath('touch2.out'))
 
+
+expect1 = "scons: Could not remove 'foo1.out': Permission denied\n"
+expect2 = "scons: Could not remove 'foo1.out': The process cannot access the file because it is being used by another process\n"
+
+expect = [
+    test.wrap_stdout(expect1, cleaning=1),
+    test.wrap_stdout(expect2, cleaning=1),
+]
+
 test.writable('.', 0)
 f = open(test.workpath('foo1.out'))
-test.run(arguments = '-c foo1.out',
-         stdout = test.wrap_stdout("scons: Could not remove 'foo1.out': Permission denied\n", cleaning=1))
+test.run(arguments = '-c foo1.out')
+stdout = test.stdout()
+matched = None
+for e in expect:
+    if stdout == e:
+        matched = 1
+        break
+if not matched:
+    print stdout
+    test.fail_test()
 test.must_exist(test.workpath('foo1.out'))
 f.close()
 test.writable('.', 1)
index 28fe8ca6b95fa3d7d27a2cc273da8ee694ae8ae3..0626106d5c7ef5e1bdbd68ca8229a38d056975b5 100644 (file)
@@ -26,6 +26,7 @@ __revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
 
 import TestCmd
 import TestSCons
+import re
 import string
 import sys
 
@@ -33,26 +34,38 @@ test = TestSCons.TestSCons(match = TestCmd.match_re)
 
 test.write('SConstruct', "")
 
+# Construct the standard copyright marker so it doesn't get replaced
+# by the packaging build.
+copyright_marker = '__' + 'COPYRIGHT' + '__'
+
+copyright_years = '2001, 2002, 2003, 2004, 2005, 2006'
+
+fmt = '(%s|Copyright \\(c\\) %s The SCons Foundation)\n'
+
+copyright_line = fmt % (copyright_marker, copyright_years)
+
 # Windows may or may not print a line for the script version
 # depending on whether it's invoked through scons.py or scons.bat.
 expect1 = r"""SCons by Steven Knight et al.:
 \tengine: v\S+, [^,]*, by \S+ on \S+
-(__COPYRIGHT__|Copyright \(c\) 2001, 2002, 2003, 2004 The SCons Foundation)
-"""
+""" + copyright_line
 
 expect2 = r"""SCons by Steven Knight et al.:
 \tscript: v\S+, [^,]*, by \S+ on \S+
 \tengine: v\S+, [^,]*, by \S+ on \S+
-(__COPYRIGHT__|Copyright \(c\) 2001, 2002, 2003, 2004 The SCons Foundation)
-"""
+""" + copyright_line
 
 test.run(arguments = '-v')
-test.fail_test(not test.match_re(test.stdout(), expect1) and
-               not test.match_re(test.stdout(), expect2))
+stdout = test.stdout()
+if not test.match_re(stdout, expect1) and not test.match_re(stdout, expect2):
+    print stdout
+    test.fail_test()
 
 test.run(arguments = '--version')
-test.fail_test(not test.match_re(test.stdout(), expect1) and
-               not test.match_re(test.stdout(), expect2))
+stdout = test.stdout()
+if not test.match_re(stdout, expect1) and not test.match_re(stdout, expect2):
+    print stdout
+    test.fail_test()
 
 test.pass_test()
  
index c9f001cfcfb905c5d01ad1bfbd19f7111d2d1b82..33f0f4d11f475267a1edb92bf762af40c8b50650 100644 (file)
@@ -33,7 +33,23 @@ import string
 
 import TestSCons
 
-test = TestSCons.TestSCons()
+test = TestSCons.TestSCons(match = TestSCons.match_re)
+
+# Find out if we support metaclasses (Python 2.2 and later).
+
+class M:
+    def __init__(cls, name, bases, cls_dict):
+        cls.has_metaclass = 1
+
+class A:
+    __metaclass__ = M
+
+try:
+    has_metaclass = A.has_metaclass
+except AttributeError:
+    has_metaclass = None
+
+
 
 test.write('SConstruct', """
 def cat(target, source, env):
@@ -50,21 +66,47 @@ test.write('file.in', "file.in\n")
 # change this test...
 expect = [
     "Memoizer (memory cache) hits and misses",
-    "Dir.exists()",
+    "Base.stat()",
+    "Dir.srcdir_list()",
     "File.exists()",
-    "SConsEnvironment.Detect()",
+    "FS._doLookup()",
+    "Node._children_get()",
 ]
 
+expect_no_metaclasses = """
+scons: warning: memoization is not supported in this version of Python \\(no metaclasses\\)
+""" + TestSCons.file_expr
+
+
+if has_metaclass:
+
+    def run_and_check(test, args, desc):
+        test.run(arguments = args)
+        stdout = test.stdout()
+        missing = filter(lambda e, s=stdout: string.find(s, e) == -1, expect)
+        if missing:
+            print "Missing the following strings in the %s output:" % desc
+            print "    " + string.join(missing, "\n    ")
+            print "STDOUT ============"
+            print stdout
+            test.fail_test()
+
+else:
+
+    def run_and_check(test, args, desc):
+        test.run(arguments = args, stderr = expect_no_metaclasses)
+        stdout = test.stdout()
+        present = filter(lambda e, s=stdout: string.find(s, e) != -1, expect)
+        if present:
+            print "The following unexpected strings are present in the %s output:" % desc
+            print "    " + string.join(present, "\n    ")
+            print "STDOUT ============"
+            print stdout
+            test.fail_test()
+
+
 for args in ['-h --debug=memoizer', '--debug=memoizer']:
-    test.run(arguments = args)
-    stdout = test.stdout()
-    missing = filter(lambda e, s=stdout: string.find(s, e) == -1, expect)
-    if missing:
-        print "Missing the following strings in the command line '%s' output:" % args
-        print "    " + string.join(missing, "\n    ")
-        print "STDOUT ============"
-        print stdout
-        test.fail_test(1)
+    run_and_check(test, args, "command line '%s'" % args)
 
 test.must_match('file.out', "file.in\n")
 
@@ -72,17 +114,13 @@ test.must_match('file.out', "file.in\n")
 
 test.unlink("file.out")
 
+
+
 os.environ['SCONSFLAGS'] = '--debug=memoizer'
 
-test.run()
-stdout = test.stdout()
-missing = filter(lambda e, s=stdout: string.find(s, e) == -1, expect)
-if missing:
-    print "Missing the following strings in the SCONSFLAGS=--debug=memoizer output:"
-    print "    " + string.join(missing, "\n    ")
-    print "STDOUT ============"
-    print stdout
-    test.fail_test(1)
+run_and_check(test, '', 'SCONSFLAGS=--debug=memoizer')
+
+test.must_match('file.out', "file.in\n")
 
 
 
index 633a46d2930ead8907149c6af65ed72dbaa4599f..3a927e5a2528693de0105f32dd534c365189e25b 100644 (file)
 __revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
 
 """
-Test calling the --debug=nomemoizer option.
+Test calling the (deprecated) --debug=nomemoizer option.
 """
 
-import pstats
-import string
-import StringIO
-import sys
-
 import TestSCons
 
-test = TestSCons.TestSCons()
-
-scons_prof = test.workpath('scons.prof')
+test = TestSCons.TestSCons(match = TestSCons.match_re)
 
 test.write('SConstruct', """
 def cat(target, source, env):
@@ -48,25 +41,12 @@ env.Cat('file.out', 'file.in')
 
 test.write('file.in', "file.in\n")
 
-test.run(arguments = "--profile=%s --debug=nomemoizer " % scons_prof)
-
-stats = pstats.Stats(scons_prof)
-stats.sort_stats('time')
-
-try:
-    save_stdout = sys.stdout
-    sys.stdout = StringIO.StringIO()
-
-    stats.strip_dirs().print_stats()
+expect = """
+scons: warning: The --debug=nomemoizer option is deprecated and has no effect.
+""" + TestSCons.file_expr
 
-    s = sys.stdout.getvalue()
-finally:
-    sys.stdout = save_stdout
+test.run(arguments = "--debug=nomemoizer", stderr = expect)
 
-test.fail_test(string.find(s, '_MeMoIZeR_init') != -1)
-test.fail_test(string.find(s, '_MeMoIZeR_reset') != -1)
-test.fail_test(string.find(s, 'Count_cache_get') != -1)
-test.fail_test(string.find(s, 'Count_cache_get_self') != -1)
-test.fail_test(string.find(s, 'Count_cache_get_one') != -1)
+test.must_match('file.out', "file.in\n")
 
 test.pass_test()
index 920706671554a6cc1ccf9d8fc250fd289f2af9e7..eb6b394594c4454725bb5de7b282f51c3d902950 100644 (file)
@@ -24,7 +24,6 @@
 
 __revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
 
-import pstats
 import string
 import StringIO
 import sys
@@ -33,6 +32,11 @@ import TestSCons
 
 test = TestSCons.TestSCons()
 
+try:
+    import pstats
+except ImportError:
+    test.skip_test('No pstats module, skipping test.\n')
+
 test.write('SConstruct', """\
 Command('file.out', 'file.in', Copy("$TARGET", "$SOURCE"))
 """)
diff --git a/test/scons-time/func/basic.py b/test/scons-time/func/basic.py
new file mode 100644 (file)
index 0000000..66c5853
--- /dev/null
@@ -0,0 +1,49 @@
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+"""
+Verify basic operation of the "func" subcommand.
+"""
+
+import TestSCons_time
+
+test = TestSCons_time.TestSCons_time(match = TestSCons_time.match_re)
+
+try:
+    import pstats
+except ImportError:
+    test.skip_test('No pstats module, skipping test.\n')
+
+test.profile_data('foo.prof', 'prof.py', '_main', """\
+def _main():
+    pass
+""")
+
+expect = r'\d.\d\d\d prof\.py:1\(_main\)' + '\n'
+
+test.run(arguments = 'func foo.prof', stdout = expect)
+
+test.pass_test()
diff --git a/test/scons-time/func/chdir.py b/test/scons-time/func/chdir.py
new file mode 100644 (file)
index 0000000..80ec93e
--- /dev/null
@@ -0,0 +1,64 @@
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+"""
+Verify that the func -C and --chdir options change directory before
+globbing for files.
+"""
+
+import re
+
+import TestSCons_time
+
+test = TestSCons_time.TestSCons_time(match = TestSCons_time.match_re)
+
+try:
+    import pstats
+except ImportError:
+    test.skip_test('No pstats module, skipping test.\n')
+
+test.subdir('profs')
+
+input = """\
+def _main():
+    pass
+"""
+
+expect = []
+for i in xrange(9):
+    i = str(i)
+    test.subdir(i)
+    test.profile_data('profs/foo-%s.prof' % i, '%s/prof.py' % i, '_main', input)
+    s = r'\d.\d\d\d %s/prof\.py:1\(_main\)' % re.escape(test.workpath(i))
+    expect.append(s + '\n')
+
+expect = ''.join(expect)
+
+test.run(arguments = 'func -C profs foo-*.prof', stdout = expect)
+
+test.run(arguments = 'func --chdir profs foo-?.prof', stdout = expect)
+
+test.pass_test()
diff --git a/test/scons-time/func/file.py b/test/scons-time/func/file.py
new file mode 100644 (file)
index 0000000..c9486c4
--- /dev/null
@@ -0,0 +1,80 @@
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+"""
+Verify that config files specified with the -f and --file options
+affect how the func subcommand processes things.
+"""
+
+import TestSCons_time
+
+test = TestSCons_time.TestSCons_time(match = TestSCons_time.match_re)
+
+try:
+    import pstats
+except ImportError:
+    test.skip_test('No pstats module, skipping test.\n')
+
+test.profile_data('foo-001-0.prof', 'prof1.py', '_main', """\
+def _main():
+    pass
+""")
+
+test.profile_data('foo-002-0.prof', 'prof2.py', '_main', """\
+# line 1 (intentional comment to adjust starting line numbers)
+def _main():
+    pass
+""")
+
+
+test.write('st1.conf', """\
+prefix = 'foo-001'
+""")
+
+expect1 = r'\d.\d\d\d prof1\.py:1\(_main\)' + '\n'
+
+test.run(arguments = 'func -f st1.conf', stdout = expect1)
+
+
+test.write('st2.conf', """\
+prefix = 'foo'
+title = 'ST2.CONF TITLE'
+""")
+
+expect2 = \
+r"""set title "ST2.CONF TITLE"
+set key bottom left
+plot '-' title "Startup" with lines lt 1
+# Startup
+1 0.000
+2 0.000
+e
+"""
+
+test.run(arguments = 'func --file st2.conf --fmt gnuplot', stdout = expect2)
+
+
+test.pass_test()
diff --git a/test/scons-time/func/format-gnuplot.py b/test/scons-time/func/format-gnuplot.py
new file mode 100644 (file)
index 0000000..92eb837
--- /dev/null
@@ -0,0 +1,82 @@
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+"""
+Verify the func --format=gnuplot option.
+"""
+
+import TestSCons_time
+
+test = TestSCons_time.TestSCons_time()
+
+try:
+    import pstats
+except ImportError:
+    test.skip_test('No pstats module, skipping test.\n')
+
+content = """\
+def _main():
+    pass
+"""
+
+test.profile_data('foo-000-0.prof', 'prof.py', '_main', content)
+test.profile_data('foo-000-1.prof', 'prof.py', '_main', content)
+test.profile_data('foo-000-2.prof', 'prof.py', '_main', content)
+
+test.profile_data('foo-001-0.prof', 'prof.py', '_main', content)
+test.profile_data('foo-001-1.prof', 'prof.py', '_main', content)
+test.profile_data('foo-001-2.prof', 'prof.py', '_main', content)
+
+expect_notitle = """\
+set key bottom left
+plot '-' title "Startup" with lines lt 1, \\
+     '-' title "Full build" with lines lt 2, \\
+     '-' title "Up-to-date build" with lines lt 3
+# Startup
+0 0.000
+1 0.000
+e
+# Full build
+0 0.000
+1 0.000
+e
+# Up-to-date build
+0 0.000
+1 0.000
+e
+"""
+
+expect_title = 'set title "TITLE"\n' + expect_notitle
+
+test.run(arguments = 'func --fmt gnuplot', stdout=expect_notitle)
+
+test.run(arguments = 'func --fmt=gnuplot --title TITLE', stdout=expect_title)
+
+test.run(arguments = 'func --format gnuplot --title TITLE', stdout=expect_title)
+
+test.run(arguments = 'func --format=gnuplot', stdout=expect_notitle)
+
+test.pass_test()
diff --git a/test/scons-time/func/function.py b/test/scons-time/func/function.py
new file mode 100644 (file)
index 0000000..27bb94d
--- /dev/null
@@ -0,0 +1,58 @@
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+"""
+Verify use of the --func and --function options to select functions
+other than the default _main().
+"""
+
+import TestSCons_time
+
+test = TestSCons_time.TestSCons_time(match = TestSCons_time.match_re)
+
+try:
+    import pstats
+except ImportError:
+    test.skip_test('No pstats module, skipping test.\n')
+
+test.profile_data('foo.prof', 'prof.py', '_main', """\
+def f1():
+    pass
+def f3():
+    pass
+def _main():
+    f1()
+    f3()
+""")
+
+expect1 = r'\d.\d\d\d prof\.py:1\(f1\)' + '\n'
+expect3 = r'\d.\d\d\d prof\.py:3\(f3\)' + '\n'
+
+test.run(arguments = 'func --func f1 foo.prof', stdout = expect1)
+
+test.run(arguments = 'func --function f3 foo.prof', stdout = expect3)
+
+test.pass_test()
diff --git a/test/scons-time/func/glob.py b/test/scons-time/func/glob.py
new file mode 100644 (file)
index 0000000..4a629ed
--- /dev/null
@@ -0,0 +1,57 @@
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+"""
+Verify that the func subcommands globs for files.
+"""
+
+import TestSCons_time
+
+test = TestSCons_time.TestSCons_time(match = TestSCons_time.match_re)
+
+try:
+    import pstats
+except ImportError:
+    test.skip_test('No pstats module, skipping test.\n')
+
+input = """\
+def _main():
+    pass
+"""
+
+expect = []
+for i in xrange(9):
+    test.subdir(str(i))
+    test.profile_data('foo-%s.prof' % i, '%s/prof.py' % i, '_main', input)
+    expect.append((r'\d.\d\d\d %s/prof\.py:1\(_main\)' + '\n') % i)
+
+expect = ''.join(expect)
+
+test.run(arguments = 'func foo-*.prof', stdout = expect)
+
+test.run(arguments = 'func foo-?.prof', stdout = expect)
+
+test.pass_test()
diff --git a/test/scons-time/func/help.py b/test/scons-time/func/help.py
new file mode 100644 (file)
index 0000000..7341ade
--- /dev/null
@@ -0,0 +1,57 @@
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+"""
+Verify the func subcommand help.
+"""
+
+import TestSCons_time
+
+test = TestSCons_time.TestSCons_time()
+
+expect = [
+    "Usage: scons-time func [OPTIONS] FILE [...]\n",
+    "  -C DIR, --chdir=DIR           Change to DIR before looking for files\n",
+    "  -h, --help                    Print this help and exit\n",
+]
+
+test.run(arguments = 'func -h')
+
+test.must_contain_all_lines('Standard output', test.stdout(), expect)
+
+test.run(arguments = 'func -?')
+
+test.must_contain_all_lines('Standard output', test.stdout(), expect)
+
+test.run(arguments = 'func --help')
+
+test.must_contain_all_lines('Standard output', test.stdout(), expect)
+
+test.run(arguments = 'help func')
+
+test.must_contain_all_lines('Standard output', test.stdout(), expect)
+
+test.pass_test()
diff --git a/test/scons-time/func/no-args.py b/test/scons-time/func/no-args.py
new file mode 100644 (file)
index 0000000..0767ae6
--- /dev/null
@@ -0,0 +1,43 @@
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+"""
+Verify the error from the func subcommand when no arguments are specified.
+"""
+
+import TestSCons_time
+
+test = TestSCons_time.TestSCons_time()
+
+expect = """\
+scons-time: func: No arguments specified.
+            No *.prof files found in "%s".
+            Type "scons-time help func" for help.
+""" % test.workpath()
+
+test.run(arguments = 'func', status = 1, stderr = expect)
+
+test.pass_test()
diff --git a/test/scons-time/func/prefix.py b/test/scons-time/func/prefix.py
new file mode 100644 (file)
index 0000000..53dfea6
--- /dev/null
@@ -0,0 +1,65 @@
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+"""
+Verify that the func -p and --prefix options specify what log files to use.
+"""
+
+import os.path
+
+import TestSCons_time
+
+test = TestSCons_time.TestSCons_time(match = TestSCons_time.match_re)
+
+try:
+    import pstats
+except ImportError:
+    test.skip_test('No pstats module, skipping test.\n')
+
+input = """\
+def _main():
+    pass
+"""
+
+foo_lines = []
+bar_lines = []
+
+for i in xrange(2):
+    test.profile_data('foo-%s.prof' % i, 'prof.py', '_main', input)
+    foo_lines.append(r'\d.\d\d\d prof\.py:1\(_main\)' + '\n')
+
+for i in xrange(4):
+    test.profile_data('bar-%s.prof' % i, 'prof.py', '_main', input)
+    bar_lines.append(r'\d.\d\d\d prof\.py:1\(_main\)' + '\n')
+
+foo_expect = ''.join(foo_lines)
+bar_expect = ''.join(bar_lines)
+
+test.run(arguments = 'func -p bar', stdout = bar_expect)
+
+test.run(arguments = 'func --prefix=foo', stdout = foo_expect)
+
+test.pass_test()
diff --git a/test/scons-time/func/tail.py b/test/scons-time/func/tail.py
new file mode 100644 (file)
index 0000000..816a7a1
--- /dev/null
@@ -0,0 +1,56 @@
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+"""
+Verify that the func subcommand only prints results for the last number
+of files specified with the -t and --tail options.
+"""
+
+import TestSCons_time
+
+test = TestSCons_time.TestSCons_time(match = TestSCons_time.match_re)
+
+try:
+    import pstats
+except ImportError:
+    test.skip_test('No pstats module, skipping test.\n')
+
+input = """\
+def _main():
+    pass
+"""
+
+expect = []
+for i in xrange(9):
+    test.subdir(str(i))
+    test.profile_data('foo-%s.prof' % i, '%s/prof.py' % i, '_main', input)
+    expect.append((r'\d.\d\d\d %s/prof\.py:1\(_main\)' + '\n') % i)
+
+test.run(arguments = 'func -t 3 foo-*.prof', stdout = ''.join(expect[-3:]))
+
+test.run(arguments = 'func --tail 5 foo-*.prof', stdout = ''.join(expect[-5:]))
+
+test.pass_test()
diff --git a/test/scons-time/help/all-subcommands.py b/test/scons-time/help/all-subcommands.py
new file mode 100644 (file)
index 0000000..6fa0fac
--- /dev/null
@@ -0,0 +1,58 @@
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+"""
+Verify that all subcommands show up in the global help.
+
+This makes sure that each do_*() function attached to the SConsTimer
+class has a line in the help string.
+"""
+
+import TestSCons_time
+
+test = TestSCons_time.TestSCons_time()
+
+# Compile the scons-time script as a module.
+c = compile(test.read(test.program, mode='r'), test.program, 'exec')
+
+# Evaluate the module in a global name space so we can get at SConsTimer.
+globals = {}
+try: eval(c, globals)
+except: pass
+
+# Extract all subcommands from the the do_*() functions.
+functions = globals['SConsTimer'].__dict__.keys()
+do_funcs = filter(lambda x: x[:3] == 'do_', functions)
+
+subcommands = map(lambda x: x[3:], do_funcs)
+
+expect = map(lambda x: '    %s ' % x, subcommands)
+
+test.run(arguments = 'help')
+
+test.must_contain_all_lines('Standard output', test.stdout(), expect)
+
+test.pass_test()
diff --git a/test/scons-time/help/options.py b/test/scons-time/help/options.py
new file mode 100644 (file)
index 0000000..942dfa6
--- /dev/null
@@ -0,0 +1,59 @@
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+"""
+Verify that the 'help' subcommand and -h, -? and --help options print
+the default help.
+"""
+
+import TestSCons_time
+
+test = TestSCons_time.TestSCons_time()
+
+expect = [
+    'Usage: scons-time SUBCOMMAND [ARGUMENTS]\n',
+    'Type "scons-time help SUBCOMMAND" for help on a specific subcommand.\n',
+    'Available subcommands:\n',
+    '    help            Provides help\n',
+]
+
+test.run(arguments = 'help')
+
+test.must_contain_all_lines('Standard output', test.stdout(), expect)
+
+test.run(arguments = '-h')
+
+test.must_contain_all_lines('Standard output', test.stdout(), expect)
+
+test.run(arguments = '-?')
+
+test.must_contain_all_lines('Standard output', test.stdout(), expect)
+
+test.run(arguments = '--help')
+
+test.must_contain_all_lines('Standard output', test.stdout(), expect)
+
+test.pass_test()
diff --git a/test/scons-time/mem/chdir.py b/test/scons-time/mem/chdir.py
new file mode 100644 (file)
index 0000000..a97f756
--- /dev/null
@@ -0,0 +1,57 @@
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+"""
+Verify that the mem -C and --chdir options change directory before
+globbing for files.
+"""
+
+import os.path
+
+import TestSCons_time
+
+test = TestSCons_time.TestSCons_time()
+
+test.subdir('logs')
+
+lines = [
+    '    pre-read    post-read    pre-build   post-build\n'
+]
+
+line_fmt = '        1000         2000         3000         4000    %s\n'
+
+for i in xrange(9):
+    logfile_name = os.path.join('logs', 'foo-%s.log' % i)
+    test.fake_logfile(logfile_name)
+    lines.append(line_fmt % logfile_name)
+
+expect = ''.join(lines)
+
+test.run(arguments = 'mem -C logs foo-*.log', stdout = expect)
+
+test.run(arguments = 'mem --chdir logs foo-?.log', stdout = expect)
+
+test.pass_test()
diff --git a/test/scons-time/mem/file.py b/test/scons-time/mem/file.py
new file mode 100644 (file)
index 0000000..a236df7
--- /dev/null
@@ -0,0 +1,71 @@
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+"""
+Verify that config files specified with the -f and --file options
+affect how the mem subcommand processes things.
+"""
+
+import TestSCons_time
+
+test = TestSCons_time.TestSCons_time()
+
+test.fake_logfile('foo-001-0.log')
+
+test.fake_logfile('foo-002-0.log')
+
+
+test.write('st1.conf', """\
+prefix = 'foo-001'
+""")
+
+expect1 = """\
+    pre-read    post-read    pre-build   post-build
+        1000         2000         3000         4000    foo-001-0.log
+"""
+
+test.run(arguments = 'mem -f st1.conf', stdout = expect1)
+
+
+test.write('st2.conf', """\
+prefix = 'foo'
+title = 'ST2.CONF TITLE'
+""")
+
+expect2 = \
+r"""set title "ST2.CONF TITLE"
+set key bottom left
+plot '-' title "Startup" with lines lt 1
+# Startup
+1 4000.000
+2 4000.000
+e
+"""
+
+test.run(arguments = 'mem --file st2.conf --fmt gnuplot', stdout = expect2)
+
+
+test.pass_test()
diff --git a/test/scons-time/mem/format-gnuplot.py b/test/scons-time/mem/format-gnuplot.py
new file mode 100644 (file)
index 0000000..a4e0f19
--- /dev/null
@@ -0,0 +1,72 @@
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+"""
+Verify the mem --format=gnuplot option.
+"""
+
+import TestSCons_time
+
+test = TestSCons_time.TestSCons_time()
+
+test.fake_logfile('foo-000-0.log', 0)
+test.fake_logfile('foo-000-1.log', 0)
+test.fake_logfile('foo-000-2.log', 0)
+
+test.fake_logfile('foo-001-0.log', 1)
+test.fake_logfile('foo-001-1.log', 1)
+test.fake_logfile('foo-001-2.log', 1)
+
+expect_notitle = """\
+set key bottom left
+plot '-' title "Startup" with lines lt 1, \\
+     '-' title "Full build" with lines lt 2, \\
+     '-' title "Up-to-date build" with lines lt 3
+# Startup
+0 4000.000
+1 4001.000
+e
+# Full build
+0 4000.000
+1 4001.000
+e
+# Up-to-date build
+0 4000.000
+1 4001.000
+e
+"""
+
+expect_title = 'set title "TITLE"\n' + expect_notitle
+
+test.run(arguments = 'mem --fmt gnuplot', stdout=expect_notitle)
+
+test.run(arguments = 'mem --fmt=gnuplot --title TITLE', stdout=expect_title)
+
+test.run(arguments = 'mem --format gnuplot --title TITLE', stdout=expect_title)
+
+test.run(arguments = 'mem --format=gnuplot', stdout=expect_notitle)
+
+test.pass_test()
diff --git a/test/scons-time/mem/glob.py b/test/scons-time/mem/glob.py
new file mode 100644 (file)
index 0000000..6f5174f
--- /dev/null
@@ -0,0 +1,52 @@
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+"""
+Verify that the mem subommand globs for files.
+"""
+
+import TestSCons_time
+
+test = TestSCons_time.TestSCons_time()
+
+lines = [
+    '    pre-read    post-read    pre-build   post-build\n'
+]
+
+line_fmt = '        1000         2000         3000         4000    %s\n'
+
+for i in xrange(9):
+    logfile_name = 'foo-%s.log' % i
+    test.fake_logfile(logfile_name)
+    lines.append(line_fmt % logfile_name)
+
+expect = ''.join(lines)
+
+test.run(arguments = 'mem foo-*.log', stdout = expect)
+
+test.run(arguments = 'mem foo-?.log', stdout = expect)
+
+test.pass_test()
diff --git a/test/scons-time/mem/help.py b/test/scons-time/mem/help.py
new file mode 100644 (file)
index 0000000..0658d5b
--- /dev/null
@@ -0,0 +1,57 @@
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+"""
+Verify the "mem" subcommand help.
+"""
+
+import TestSCons_time
+
+test = TestSCons_time.TestSCons_time()
+
+expect = [
+    "Usage: scons-time mem [OPTIONS] FILE [...]\n",
+    "  -C DIR, --chdir=DIR           Change to DIR before looking for files\n",
+    "  -h, --help                    Print this help and exit\n",
+]
+
+test.run(arguments = 'mem -h')
+
+test.must_contain_all_lines('Standard output', test.stdout(), expect)
+
+test.run(arguments = 'mem -?')
+
+test.must_contain_all_lines('Standard output', test.stdout(), expect)
+
+test.run(arguments = 'mem --help')
+
+test.must_contain_all_lines('Standard output', test.stdout(), expect)
+
+test.run(arguments = 'help mem')
+
+test.must_contain_all_lines('Standard output', test.stdout(), expect)
+
+test.pass_test()
diff --git a/test/scons-time/mem/no-args.py b/test/scons-time/mem/no-args.py
new file mode 100644 (file)
index 0000000..cb47c9b
--- /dev/null
@@ -0,0 +1,43 @@
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+"""
+Verify the error when no arguments are specified to the mem subcommand.
+"""
+
+import TestSCons_time
+
+test = TestSCons_time.TestSCons_time()
+
+expect = """\
+scons-time: mem: No arguments specified.
+            No *.log files found in "%s".
+            Type "scons-time help mem" for help.
+""" % test.workpath()
+
+test.run(arguments = 'mem', status = 1, stderr = expect)
+
+test.pass_test()
diff --git a/test/scons-time/mem/prefix.py b/test/scons-time/mem/prefix.py
new file mode 100644 (file)
index 0000000..41eb4e4
--- /dev/null
@@ -0,0 +1,62 @@
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+"""
+Verify that the mem -p and --prefix options specify what log files to use.
+"""
+
+import os.path
+
+import TestSCons_time
+
+test = TestSCons_time.TestSCons_time()
+
+test.subdir('logs')
+
+header = '    pre-read    post-read    pre-build   post-build\n'
+
+line_fmt = '        1000         2000         3000         4000    %s\n'
+
+foo_lines = [ header ]
+bar_lines = [ header ]
+
+for i in xrange(3):
+    logfile_name = os.path.join('foo-%s.log' % i)
+    test.fake_logfile(logfile_name)
+    foo_lines.append(line_fmt % logfile_name)
+
+    logfile_name = os.path.join('bar-%s.log' % i)
+    test.fake_logfile(logfile_name)
+    bar_lines.append(line_fmt % logfile_name)
+
+foo_expect = ''.join(foo_lines)
+bar_expect = ''.join(bar_lines)
+
+test.run(arguments = 'mem -p bar', stdout = bar_expect)
+
+test.run(arguments = 'mem --prefix=foo', stdout = foo_expect)
+
+test.pass_test()
diff --git a/test/scons-time/mem/stage.py b/test/scons-time/mem/stage.py
new file mode 100644 (file)
index 0000000..88b21e2
--- /dev/null
@@ -0,0 +1,83 @@
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+"""
+Verify the mem --stage option.
+"""
+
+import TestSCons_time
+
+test = TestSCons_time.TestSCons_time()
+
+test.fake_logfile('foo-000-0.log', 0)
+test.fake_logfile('foo-000-1.log', 0)
+test.fake_logfile('foo-000-2.log', 0)
+
+test.fake_logfile('foo-001-0.log', 1)
+test.fake_logfile('foo-001-1.log', 1)
+test.fake_logfile('foo-001-2.log', 1)
+
+expect = """\
+set key bottom left
+plot '-' title "Startup" with lines lt 1, \\
+     '-' title "Full build" with lines lt 2, \\
+     '-' title "Up-to-date build" with lines lt 3
+# Startup
+0 %(index)s000.000
+1 %(index)s001.000
+e
+# Full build
+0 %(index)s000.000
+1 %(index)s001.000
+e
+# Up-to-date build
+0 %(index)s000.000
+1 %(index)s001.000
+e
+"""
+
+pre_read    = expect % {'index' : 1}
+post_read   = expect % {'index' : 2}
+pre_build   = expect % {'index' : 3}
+post_build  = expect % {'index' : 4}
+
+test.run(arguments = 'mem --fmt gnuplot --stage pre-read', stdout=pre_read)
+
+test.run(arguments = 'mem --fmt gnuplot --stage=post-read', stdout=post_read)
+
+test.run(arguments = 'mem --fmt gnuplot --stage=pre-build', stdout=pre_build)
+
+test.run(arguments = 'mem --fmt gnuplot --stage post-build', stdout=post_build)
+
+expect = """\
+scons-time: mem: Unrecognized stage "unknown".
+"""
+
+test.run(arguments = 'mem --fmt gnuplot --stage unknown',
+         status = 1,
+         stderr = expect)
+
+test.pass_test()
diff --git a/test/scons-time/mem/tail.py b/test/scons-time/mem/tail.py
new file mode 100644 (file)
index 0000000..e2c7ede
--- /dev/null
@@ -0,0 +1,55 @@
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+"""
+Verify that the mem subcommand only prints results for the last number
+of files specified with the -t and --tail options.
+"""
+
+import TestSCons_time
+
+test = TestSCons_time.TestSCons_time()
+
+
+header = '    pre-read    post-read    pre-build   post-build\n'
+
+lines = []
+
+line_fmt = '        1000         2000         3000         4000    %s\n'
+
+for i in xrange(9):
+    logfile_name = 'foo-%s.log' % i
+    test.fake_logfile(logfile_name)
+    lines.append(line_fmt % logfile_name)
+
+expect3 = [header] + lines[-3:]
+expect5 = [header] + lines[-5:]
+
+test.run(arguments = 'mem -t 3 foo-*.log', stdout = ''.join(expect3))
+
+test.run(arguments = 'mem --tail 5 foo-*.log', stdout = ''.join(expect5))
+
+test.pass_test()
diff --git a/test/scons-time/no-args.py b/test/scons-time/no-args.py
new file mode 100644 (file)
index 0000000..ba98da2
--- /dev/null
@@ -0,0 +1,42 @@
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+"""
+Verify that invoking scons-test with no arguments prints the
+fall-back message and exits non-zero.
+"""
+
+import TestSCons_time
+
+test = TestSCons_time.TestSCons_time()
+
+expect = """\
+Type "scons-time help" for usage.
+"""
+
+test.run(arguments = '', status=1, stderr=expect)
+
+test.pass_test()
diff --git a/test/scons-time/obj/chdir.py b/test/scons-time/obj/chdir.py
new file mode 100644 (file)
index 0000000..5e7df89
--- /dev/null
@@ -0,0 +1,57 @@
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+"""
+Verify that the obj -C and --chdir options change directory before
+globbing for files.
+"""
+
+import os.path
+
+import TestSCons_time
+
+test = TestSCons_time.TestSCons_time()
+
+test.subdir('logs')
+
+lines = [
+    '    pre-read    post-read    pre-build   post-build\n'
+]
+
+line_fmt = '       1101%(i)s        1102%(i)s        1103%(i)s        1104%(i)s    %(logfile_name)s\n'
+
+for i in xrange(9):
+    logfile_name = os.path.join('logs', 'foo-%s.log' % i)
+    test.fake_logfile(logfile_name, i)
+    lines.append(line_fmt % locals())
+
+expect = ''.join(lines)
+
+test.run(arguments = 'obj -C logs Environment.Base foo-*.log', stdout = expect)
+
+test.run(arguments = 'obj --chdir logs Environment.Base foo-?.log', stdout = expect)
+
+test.pass_test()
diff --git a/test/scons-time/obj/file.py b/test/scons-time/obj/file.py
new file mode 100644 (file)
index 0000000..c881397
--- /dev/null
@@ -0,0 +1,71 @@
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+"""
+Verify that config files specified with the -f and --file options
+affect how the obj subcommand processes things.
+"""
+
+import TestSCons_time
+
+test = TestSCons_time.TestSCons_time()
+
+test.fake_logfile('foo-001-0.log')
+
+test.fake_logfile('foo-002-0.log')
+
+
+test.write('st1.conf', """\
+prefix = 'foo-001'
+""")
+
+expect1 = """\
+    pre-read    post-read    pre-build   post-build
+       16010        16020        16030        16040    foo-001-0.log
+"""
+
+test.run(arguments = 'obj -f st1.conf Node.FS.Base', stdout = expect1)
+
+
+test.write('st2.conf', """\
+prefix = 'foo'
+title = 'ST2.CONF TITLE'
+""")
+
+expect2 = \
+r"""set title "ST2.CONF TITLE"
+set key bottom left
+plot '-' title "Startup" with lines lt 1
+# Startup
+1 16040.000
+2 16040.000
+e
+"""
+
+test.run(arguments = 'obj --file st2.conf --fmt gnuplot Node.FS.Base', stdout = expect2)
+
+
+test.pass_test()
diff --git a/test/scons-time/obj/format-gnuplot.py b/test/scons-time/obj/format-gnuplot.py
new file mode 100644 (file)
index 0000000..b47d843
--- /dev/null
@@ -0,0 +1,76 @@
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+"""
+Verify the obj --format=gnuplot option.
+"""
+
+import TestSCons_time
+
+test = TestSCons_time.TestSCons_time()
+
+test.fake_logfile('foo-000-0.log', 0)
+test.fake_logfile('foo-000-1.log', 0)
+test.fake_logfile('foo-000-2.log', 0)
+
+test.fake_logfile('foo-001-0.log', 1)
+test.fake_logfile('foo-001-1.log', 1)
+test.fake_logfile('foo-001-2.log', 1)
+
+expect_notitle = """\
+set key bottom left
+plot '-' title "Startup" with lines lt 1, \\
+     '-' title "Full build" with lines lt 2, \\
+     '-' title "Up-to-date build" with lines lt 3
+# Startup
+0 20040.000
+1 20041.000
+e
+# Full build
+0 20040.000
+1 20041.000
+e
+# Up-to-date build
+0 20040.000
+1 20041.000
+e
+"""
+
+expect_title = 'set title "TITLE"\n' + expect_notitle
+
+test.run(arguments = 'obj --fmt gnuplot Node.Node',
+         stdout=expect_notitle)
+
+test.run(arguments = 'obj --fmt=gnuplot --title TITLE Node.Node',
+         stdout=expect_title)
+
+test.run(arguments = 'obj --format gnuplot --title TITLE Node.Node',
+         stdout=expect_title)
+
+test.run(arguments = 'obj --format=gnuplot Node.Node',
+         stdout=expect_notitle)
+
+test.pass_test()
diff --git a/test/scons-time/obj/glob.py b/test/scons-time/obj/glob.py
new file mode 100644 (file)
index 0000000..4af2854
--- /dev/null
@@ -0,0 +1,52 @@
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+"""
+Verify that the obj subcommand globs for files.
+"""
+
+import TestSCons_time
+
+test = TestSCons_time.TestSCons_time()
+
+lines = [
+    '    pre-read    post-read    pre-build   post-build\n'
+]
+
+line_fmt = '        601%(i)s         602%(i)s         603%(i)s         604%(i)s    %(logfile_name)s\n'
+
+for i in xrange(9):
+    logfile_name = 'foo-%s.log' % i
+    test.fake_logfile(logfile_name, i)
+    lines.append(line_fmt % locals())
+
+expect = ''.join(lines)
+
+test.run(arguments = 'obj Builder.BuilderBase foo-*.log', stdout = expect)
+
+test.run(arguments = 'obj Builder.BuilderBase foo-?.log', stdout = expect)
+
+test.pass_test()
diff --git a/test/scons-time/obj/help.py b/test/scons-time/obj/help.py
new file mode 100644 (file)
index 0000000..95abad6
--- /dev/null
@@ -0,0 +1,57 @@
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+"""
+Verify the "obj" subcommand help.
+"""
+
+import TestSCons_time
+
+test = TestSCons_time.TestSCons_time()
+
+expect = [
+    "Usage: scons-time obj [OPTIONS] OBJECT FILE [...]\n",
+    "  -C DIR, --chdir=DIR           Change to DIR before looking for files\n",
+    "  -h, --help                    Print this help and exit\n",
+]
+
+test.run(arguments = 'obj -h')
+
+test.must_contain_all_lines('Standard output', test.stdout(), expect)
+
+test.run(arguments = 'obj -?')
+
+test.must_contain_all_lines('Standard output', test.stdout(), expect)
+
+test.run(arguments = 'obj --help')
+
+test.must_contain_all_lines('Standard output', test.stdout(), expect)
+
+test.run(arguments = 'help obj')
+
+test.must_contain_all_lines('Standard output', test.stdout(), expect)
+
+test.pass_test()
diff --git a/test/scons-time/obj/no-args.py b/test/scons-time/obj/no-args.py
new file mode 100644 (file)
index 0000000..a4d3f84
--- /dev/null
@@ -0,0 +1,42 @@
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+"""
+Verify the error when no arguments are specified to the obj subcommand.
+"""
+
+import TestSCons_time
+
+test = TestSCons_time.TestSCons_time()
+
+expect = """\
+scons-time: obj: Must specify an object name.
+                 Type "scons-time help obj" for help.
+"""
+
+test.run(arguments = 'obj', status = 1, stderr = expect)
+
+test.pass_test()
diff --git a/test/scons-time/obj/no-files.py b/test/scons-time/obj/no-files.py
new file mode 100644 (file)
index 0000000..cd91ceb
--- /dev/null
@@ -0,0 +1,44 @@
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+"""
+Verify the error when the obj subcommand is passed object argument but no
+file arguments.
+"""
+
+import TestSCons_time
+
+test = TestSCons_time.TestSCons_time()
+
+expect = """\
+scons-time: obj: No arguments specified.
+            No *.log files found in "%s".
+            Type "scons-time help obj" for help.
+""" % test.workpath()
+
+test.run(arguments = 'obj fake.object', status = 1, stderr = expect)
+
+test.pass_test()
diff --git a/test/scons-time/obj/prefix.py b/test/scons-time/obj/prefix.py
new file mode 100644 (file)
index 0000000..8005dc4
--- /dev/null
@@ -0,0 +1,62 @@
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+"""
+Verify that the obj -p and --prefix options specify what log files to use.
+"""
+
+import os.path
+
+import TestSCons_time
+
+test = TestSCons_time.TestSCons_time()
+
+test.subdir('logs')
+
+header = '    pre-read    post-read    pre-build   post-build\n'
+
+line_fmt = '       11010        11020        11030        11040    %s\n'
+
+foo_lines = [ header ]
+bar_lines = [ header ]
+
+for i in xrange(3):
+    logfile_name = os.path.join('foo-%s.log' % i)
+    test.fake_logfile(logfile_name)
+    foo_lines.append(line_fmt % logfile_name)
+
+    logfile_name = os.path.join('bar-%s.log' % i)
+    test.fake_logfile(logfile_name)
+    bar_lines.append(line_fmt % logfile_name)
+
+foo_expect = ''.join(foo_lines)
+bar_expect = ''.join(bar_lines)
+
+test.run(arguments = 'obj -p bar Environment.Base', stdout = bar_expect)
+
+test.run(arguments = 'obj --prefix=foo Environment.Base', stdout = foo_expect)
+
+test.pass_test()
diff --git a/test/scons-time/obj/stage.py b/test/scons-time/obj/stage.py
new file mode 100644 (file)
index 0000000..5dbd15b
--- /dev/null
@@ -0,0 +1,88 @@
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+"""
+Verify the obj --stage option.
+"""
+
+import TestSCons_time
+
+test = TestSCons_time.TestSCons_time()
+
+test.fake_logfile('foo-000-0.log', 0)
+test.fake_logfile('foo-000-1.log', 0)
+test.fake_logfile('foo-000-2.log', 0)
+
+test.fake_logfile('foo-001-0.log', 1)
+test.fake_logfile('foo-001-1.log', 1)
+test.fake_logfile('foo-001-2.log', 1)
+
+expect = """\
+set key bottom left
+plot '-' title "Startup" with lines lt 1, \\
+     '-' title "Full build" with lines lt 2, \\
+     '-' title "Up-to-date build" with lines lt 3
+# Startup
+0 50%(index)s0.000
+1 50%(index)s1.000
+e
+# Full build
+0 50%(index)s0.000
+1 50%(index)s1.000
+e
+# Up-to-date build
+0 50%(index)s0.000
+1 50%(index)s1.000
+e
+"""
+
+pre_read = expect % {'index' : 1}
+post_read = expect % {'index' : 2}
+pre_build = expect % {'index' : 3}
+post_build = expect % {'index' : 4}
+
+test.run(arguments = 'obj --fmt gnuplot --stage pre-read Action.ListAction',
+         stdout=pre_read)
+
+test.run(arguments = 'obj --fmt gnuplot --stage=post-read Action.ListAction',
+         stdout=post_read)
+
+test.run(arguments = 'obj --fmt gnuplot --stage=pre-build Action.ListAction',
+         stdout=pre_build)
+
+test.run(arguments = 'obj --fmt gnuplot --stage post-build Action.ListAction',
+         stdout=post_build)
+
+expect = """\
+scons-time: obj: Unrecognized stage "unknown".
+                 Type "scons-time help obj" for help.
+"""
+
+test.run(arguments = 'obj --fmt gnuplot --stage unknown',
+         status = 1,
+         stderr = expect)
+
+test.pass_test()
diff --git a/test/scons-time/obj/tail.py b/test/scons-time/obj/tail.py
new file mode 100644 (file)
index 0000000..0483d89
--- /dev/null
@@ -0,0 +1,55 @@
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+"""
+Verify that the obj subcommand only prints results for the last number
+of files specified with the -t and --tail options.
+"""
+
+import TestSCons_time
+
+test = TestSCons_time.TestSCons_time()
+
+
+header = '    pre-read    post-read    pre-build   post-build\n'
+
+lines = []
+
+line_fmt = '       1501%(i)s        1502%(i)s        1503%(i)s        1504%(i)s    %(logfile_name)s\n'
+
+for i in xrange(9):
+    logfile_name = 'foo-%s.log' % i
+    test.fake_logfile(logfile_name, i)
+    lines.append(line_fmt % locals())
+
+expect3 = ''.join([header] + lines[-3:])
+expect5 = ''.join([header] + lines[-5:])
+
+test.run(arguments = 'obj -t 3 Node.FS foo-*.log', stdout = expect3)
+
+test.run(arguments = 'obj --tail 5 Node.FS foo-*.log', stdout = expect5)
+
+test.pass_test()
diff --git a/test/scons-time/run/aegis.py b/test/scons-time/run/aegis.py
new file mode 100644 (file)
index 0000000..8f378cb
--- /dev/null
@@ -0,0 +1,84 @@
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+"""
+Verify the ability to "check out" an SCons delta from a fake
+Aegis utility.
+"""
+
+import re
+
+import TestSCons_time
+
+test = TestSCons_time.TestSCons_time()
+
+test.write_sample_project('foo.tar')
+
+my_aegis_py = test.write_fake_aegis_py('my_aegis.py')
+
+test.write('config', """\
+aegis = r'%(my_aegis_py)s'
+""" % locals())
+
+test.run(arguments = 'run -f config --aegis xyzzy.0.1 --number 321,329 foo.tar')
+
+test.must_exist('foo-321-0.log',
+                'foo-321-0.prof',
+                'foo-321-1.log',
+                'foo-321-1.prof',
+                'foo-321-2.log',
+                'foo-321-2.prof')
+
+test.must_exist('foo-329-0.log',
+                'foo-329-0.prof',
+                'foo-329-1.log',
+                'foo-329-1.prof',
+                'foo-329-2.log',
+                'foo-329-2.prof')
+
+def tempdir_re(*args):
+    import os
+    import os.path
+    import string
+    import tempfile
+
+    sep = re.escape(os.sep)
+    args = (tempfile.gettempdir(), 'scons-time-aegis-',) + args
+    x = apply(os.path.join, args)
+    x = re.escape(x)
+    x = string.replace(x, 'aegis\\-', 'aegis\\-[^%s]*' % sep)
+    return x
+
+expect = [
+    tempdir_re('src', 'script', 'scons.py'),
+    'SCONS_LIB_DIR = %s' % tempdir_re('src', 'engine'),
+]
+
+content = test.read(test.workpath('foo-321-2.log'))
+
+test.must_contain_all_lines('foo-617-2.log', content, expect, re.search)
+
+test.pass_test()
diff --git a/test/scons-time/run/archive/dir.py b/test/scons-time/run/archive/dir.py
new file mode 100644 (file)
index 0000000..a6c48ec
--- /dev/null
@@ -0,0 +1,49 @@
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+"""
+Verify basic generation of timing information from an input fake-project
+directory tree.
+"""
+
+import TestSCons_time
+
+test = TestSCons_time.TestSCons_time()
+
+test.write_fake_scons_py()
+
+test.write_sample_project('foo')
+
+test.run(arguments = 'run foo')
+
+test.must_exist('foo-000-0.log',
+                'foo-000-0.prof',
+                'foo-000-1.log',
+                'foo-000-1.prof',
+                'foo-000-2.log',
+                'foo-000-2.prof')
+
+test.pass_test()
diff --git a/test/scons-time/run/archive/tar-gz.py b/test/scons-time/run/archive/tar-gz.py
new file mode 100644 (file)
index 0000000..ac69e29
--- /dev/null
@@ -0,0 +1,49 @@
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+"""
+Verify basic generation of timing information from an input fake-project
+.tar.gz file.
+"""
+
+import TestSCons_time
+
+test = TestSCons_time.TestSCons_time()
+
+test.write_fake_scons_py()
+
+test.write_sample_project('foo.tar.gz')
+
+test.run(arguments = 'run foo.tar.gz')
+
+test.must_exist('foo-000-0.log',
+                'foo-000-0.prof',
+                'foo-000-1.log',
+                'foo-000-1.prof',
+                'foo-000-2.log',
+                'foo-000-2.prof')
+
+test.pass_test()
diff --git a/test/scons-time/run/archive/tar.py b/test/scons-time/run/archive/tar.py
new file mode 100644 (file)
index 0000000..18823d7
--- /dev/null
@@ -0,0 +1,49 @@
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+"""
+Verify basic generation of timing information from an input fake-project
+.tar file.
+"""
+
+import TestSCons_time
+
+test = TestSCons_time.TestSCons_time()
+
+test.write_fake_scons_py()
+
+test.write_sample_project('foo.tar')
+
+test.run(arguments = 'run foo.tar')
+
+test.must_exist('foo-000-0.log',
+                'foo-000-0.prof',
+                'foo-000-1.log',
+                'foo-000-1.prof',
+                'foo-000-2.log',
+                'foo-000-2.prof')
+
+test.pass_test()
diff --git a/test/scons-time/run/archive/tgz.py b/test/scons-time/run/archive/tgz.py
new file mode 100644 (file)
index 0000000..f7df9a0
--- /dev/null
@@ -0,0 +1,49 @@
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+"""
+Verify basic generation of timing information from an input fake-project
+.tgz file.
+"""
+
+import TestSCons_time
+
+test = TestSCons_time.TestSCons_time()
+
+test.write_fake_scons_py()
+
+test.write_sample_project('foo.tgz')
+
+test.run(arguments = 'run foo.tgz')
+
+test.must_exist('foo-000-0.log',
+                'foo-000-0.prof',
+                'foo-000-1.log',
+                'foo-000-1.prof',
+                'foo-000-2.log',
+                'foo-000-2.prof')
+
+test.pass_test()
diff --git a/test/scons-time/run/archive/zip.py b/test/scons-time/run/archive/zip.py
new file mode 100644 (file)
index 0000000..67cfc3a
--- /dev/null
@@ -0,0 +1,49 @@
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+"""
+Verify basic generation of timing information from an input fake-project
+.zip file.
+"""
+
+import TestSCons_time
+
+test = TestSCons_time.TestSCons_time()
+
+test.write_fake_scons_py()
+
+test.write_sample_project('foo.zip')
+
+test.run(arguments = 'run foo.zip')
+
+test.must_exist('foo-000-0.log',
+                'foo-000-0.prof',
+                'foo-000-1.log',
+                'foo-000-1.prof',
+                'foo-000-2.log',
+                'foo-000-2.prof')
+
+test.pass_test()
diff --git a/test/scons-time/run/config/archive_list.py b/test/scons-time/run/config/archive_list.py
new file mode 100644 (file)
index 0000000..8d48d26
--- /dev/null
@@ -0,0 +1,73 @@
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+"""
+Verify specifying a list of archives through the archive_list setting
+in a config file.
+"""
+
+import TestSCons_time
+
+test = TestSCons_time.TestSCons_time()
+
+test.write_fake_scons_py()
+
+test.write_sample_project('foo.tar.gz')
+
+test.write('config', """\
+archive_list = ['foo.tar.gz']
+""")
+
+test.run(arguments = 'run -f config')
+
+test.must_exist('foo-000-0.log',
+                'foo-000-0.prof',
+                'foo-000-1.log',
+                'foo-000-1.prof',
+                'foo-000-2.log',
+                'foo-000-2.prof')
+
+
+test.write_sample_project('bar.tar.gz')
+
+test.run(arguments = 'run -f config bar.tar.gz')
+
+test.must_not_exist('foo-001-0.log',
+                    'foo-001-0.prof',
+                    'foo-001-1.log',
+                    'foo-001-1.prof',
+                    'foo-001-2.log',
+                    'foo-001-2.prof')
+
+test.must_exist('bar-000-0.log',
+                'bar-000-0.prof',
+                'bar-000-1.log',
+                'bar-000-1.prof',
+                'bar-000-2.log',
+                'bar-000-2.prof')
+
+
+test.pass_test()
diff --git a/test/scons-time/run/config/initial_commands.py b/test/scons-time/run/config/initial_commands.py
new file mode 100644 (file)
index 0000000..73328ef
--- /dev/null
@@ -0,0 +1,56 @@
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+"""
+Verify specifying a list of initial commands through a config file.
+"""
+
+import TestSCons_time
+
+test = TestSCons_time.TestSCons_time()
+
+test.write_fake_scons_py()
+
+test.write_sample_project('foo.tar.gz')
+
+test.write('config', """\
+def touch(arg):
+    open(arg, 'w')
+initial_commands = [(touch, 'touch %%%%s', r'%s')]
+""" % test.workpath('INITIAL'))
+
+test.run(arguments = 'run -f config foo.tar.gz')
+
+test.must_exist('foo-000-0.log',
+                'foo-000-0.prof',
+                'foo-000-1.log',
+                'foo-000-1.prof',
+                'foo-000-2.log',
+                'foo-000-2.prof')
+
+test.must_exist('INITIAL')
+
+test.pass_test()
diff --git a/test/scons-time/run/config/prefix.py b/test/scons-time/run/config/prefix.py
new file mode 100644 (file)
index 0000000..c04c9e7
--- /dev/null
@@ -0,0 +1,52 @@
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+"""
+Verify specifying an alternate file prefix through a config file.
+"""
+
+import TestSCons_time
+
+test = TestSCons_time.TestSCons_time()
+
+test.write_fake_scons_py()
+
+test.write_sample_project('foo.tar.gz')
+
+test.write('config', """\
+prefix = 'bar'
+""")
+
+test.run(arguments = 'run -f config foo.tar.gz')
+
+test.must_exist('bar-000-0.log',
+                'bar-000-0.prof',
+                'bar-000-1.log',
+                'bar-000-1.prof',
+                'bar-000-2.log',
+                'bar-000-2.prof')
+
+test.pass_test()
diff --git a/test/scons-time/run/config/python.py b/test/scons-time/run/config/python.py
new file mode 100644 (file)
index 0000000..4aa85b4
--- /dev/null
@@ -0,0 +1,68 @@
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+"""
+Verify specifying an alternate Python executable in a config file.
+"""
+
+import os
+
+import TestSCons_time
+
+test = TestSCons_time.TestSCons_time()
+
+test.write_sample_project('foo.tar.gz')
+
+my_python_py = test.workpath('my_python.py')
+
+test.write('config', """\
+python = r'%(my_python_py)s'
+""" % locals())
+
+test.write(my_python_py, """\
+#!/usr/bin/env python
+import sys
+profile = ''
+for arg in sys.argv[1:]:
+    if arg.startswith('--profile='):
+        profile = arg[10:]
+        break
+print 'my_python.py: %s' % profile
+""")
+
+os.chmod(my_python_py, 0755)
+
+test.run(arguments = 'run -f config foo.tar.gz')
+
+prof0 = test.workpath('foo-000-0.prof')
+prof1 = test.workpath('foo-000-1.prof')
+prof2 = test.workpath('foo-000-2.prof')
+
+test.must_match('foo-000-0.log', "my_python.py: %s\n" % prof0)
+test.must_match('foo-000-1.log', "my_python.py: %s\n" % prof1)
+test.must_match('foo-000-2.log', "my_python.py: %s\n" % prof2)
+
+test.pass_test()
diff --git a/test/scons-time/run/config/scons.py b/test/scons-time/run/config/scons.py
new file mode 100644 (file)
index 0000000..45ca369
--- /dev/null
@@ -0,0 +1,65 @@
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+"""
+Verify specifying an alternate SCons through a config file.
+"""
+
+import os
+
+import TestSCons_time
+
+test = TestSCons_time.TestSCons_time()
+
+test.write_sample_project('foo.tar.gz')
+
+my_scons_py = test.workpath('my_scons.py')
+
+test.write('config', """\
+scons = r'%(my_scons_py)s'
+""" % locals())
+
+test.write(my_scons_py, """\
+import sys
+profile = ''
+for arg in sys.argv[1:]:
+    if arg.startswith('--profile='):
+        profile = arg[10:]
+        break
+print 'my_scons.py: %s' % profile
+""")
+
+test.run(arguments = 'run -f config foo.tar.gz')
+
+prof0 = test.workpath('foo-000-0.prof')
+prof1 = test.workpath('foo-000-1.prof')
+prof2 = test.workpath('foo-000-2.prof')
+
+test.must_match('foo-000-0.log', "my_scons.py: %s\n" % prof0)
+test.must_match('foo-000-1.log', "my_scons.py: %s\n" % prof1)
+test.must_match('foo-000-2.log', "my_scons.py: %s\n" % prof2)
+
+test.pass_test()
diff --git a/test/scons-time/run/config/subdir.py b/test/scons-time/run/config/subdir.py
new file mode 100644 (file)
index 0000000..4d21318
--- /dev/null
@@ -0,0 +1,63 @@
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+"""
+Verify picking up the subdir value from a config file.
+"""
+
+import os
+import re
+
+import TestSCons_time
+
+test = TestSCons_time.TestSCons_time()
+
+test.write_fake_scons_py()
+
+test.write_sample_project('foo.tar.gz', 'subdir')
+
+test.write('config', """\
+subdir = 'subdir'
+""")
+
+test.run(arguments = 'run -f config foo.tar.gz')
+
+test.must_exist('foo-000-0.log',
+                'foo-000-0.prof',
+                'foo-000-1.log',
+                'foo-000-1.prof',
+                'foo-000-2.log',
+                'foo-000-2.prof')
+
+content = test.read(test.workpath('foo-000-0.log'), mode='r')
+
+expect = [
+    'SConstruct file directory: .*%ssubdir$' % re.escape(os.sep),
+]
+
+test.must_contain_all_lines('foo-000-0.log', content, expect, re.search)
+
+test.pass_test()
diff --git a/test/scons-time/run/config/targets.py b/test/scons-time/run/config/targets.py
new file mode 100644 (file)
index 0000000..8d2abd1
--- /dev/null
@@ -0,0 +1,86 @@
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+"""
+Verify specifying a list of targets through a config file.
+"""
+
+import os
+import re
+
+import TestSCons_time
+
+test = TestSCons_time.TestSCons_time(match = TestSCons_time.match_re)
+
+test.write_fake_scons_py()
+
+test.write_sample_project('foo.tar.gz')
+
+test.write('config', """\
+targets = 'target1 target2'
+""")
+
+test.run(arguments = 'run -f config foo.tar.gz')
+
+scons_py    = re.escape(test.workpath('src', 'script', 'scons.py'))
+src_engine  = re.escape(test.workpath('src', 'engine'))
+
+prof1       = re.escape(test.workpath('foo-000-1.prof'))
+prof2       = re.escape(test.workpath('foo-000-2.prof'))
+
+sep         = re.escape(os.sep)
+
+expect = """\
+%(scons_py)s
+    --debug=count
+    --debug=memory
+    --debug=time
+    --debug=memoizer
+    --profile=%(prof1)s
+    target1
+    target2
+SCONS_LIB_DIR = %(src_engine)s
+SConstruct file directory: .*scons-time-.*%(sep)sfoo
+""" % locals()
+
+test.must_match('foo-000-1.log', expect, mode='r')
+
+expect = """\
+%(scons_py)s
+    --debug=count
+    --debug=memory
+    --debug=time
+    --debug=memoizer
+    --profile=%(prof2)s
+    target1
+    target2
+SCONS_LIB_DIR = %(src_engine)s
+SConstruct file directory: .*scons-time-.*%(sep)sfoo
+""" % locals()
+
+test.must_match('foo-000-2.log', expect, mode='r')
+
+test.pass_test()
diff --git a/test/scons-time/run/option/help.py b/test/scons-time/run/option/help.py
new file mode 100644 (file)
index 0000000..304992f
--- /dev/null
@@ -0,0 +1,59 @@
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+"""
+Verify that the run -h option (and variants) prints help.
+"""
+
+import TestSCons_time
+
+test = TestSCons_time.TestSCons_time()
+
+expect = [
+    "Usage: scons-time run [OPTIONS] [FILE ...]\n",
+    "  -h, --help                    Print this help and exit\n",
+    "  -n, --no-exec                 No execute, just print command lines\n",
+    "  -q, --quiet                   Don't print command lines\n",
+    "  -v, --verbose                 Display output of commands\n",
+]
+
+test.run(arguments = 'run -h')
+
+test.must_contain_all_lines('Standard output', test.stdout(), expect)
+
+test.run(arguments = 'run -?')
+
+test.must_contain_all_lines('Standard output', test.stdout(), expect)
+
+test.run(arguments = 'run --help')
+
+test.must_contain_all_lines('Standard output', test.stdout(), expect)
+
+test.run(arguments = 'help run')
+
+test.must_contain_all_lines('Standard output', test.stdout(), expect)
+
+test.pass_test()
diff --git a/test/scons-time/run/option/next-run.py b/test/scons-time/run/option/next-run.py
new file mode 100644 (file)
index 0000000..8a23939
--- /dev/null
@@ -0,0 +1,99 @@
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+"""
+Verify that the script finds the next run number based on the
+contents of the directory, even when specified with --outdir.
+"""
+
+import TestSCons_time
+
+test = TestSCons_time.TestSCons_time()
+
+test.subdir('sub')
+
+test.write_fake_scons_py()
+
+test.write_sample_project('foo.tar.gz')
+
+test.run(arguments = 'run foo.tar.gz')
+
+test.must_exist('foo-000-0.log',
+                'foo-000-0.prof',
+                'foo-000-1.log',
+                'foo-000-1.prof',
+                'foo-000-2.log',
+                'foo-000-2.prof')
+
+test.must_not_exist('foo-001-0.log',
+                    'foo-001-0.prof',
+                    'foo-001-1.log',
+                    'foo-001-1.prof',
+                    'foo-001-2.log',
+                    'foo-001-2.prof')
+
+test.run(arguments = 'run foo.tar.gz')
+
+test.must_exist('foo-001-0.log',
+                'foo-001-0.prof',
+                'foo-001-1.log',
+                'foo-001-1.prof',
+                'foo-001-2.log',
+                'foo-001-2.prof')
+
+test.must_not_exist('foo-002-0.log',
+                    'foo-002-0.prof',
+                    'foo-002-1.log',
+                    'foo-002-1.prof',
+                    'foo-002-2.log',
+                    'foo-002-2.prof')
+
+test.run(arguments = 'run foo.tar.gz')
+
+test.must_exist('foo-002-0.log',
+                'foo-002-0.prof',
+                'foo-002-1.log',
+                'foo-002-1.prof',
+                'foo-002-2.log',
+                'foo-002-2.prof')
+
+test.run(arguments = 'run --outdir sub foo.tar.gz')
+
+test.must_exist(['sub', 'foo-000-0.log'],
+                ['sub', 'foo-000-0.prof'],
+                ['sub', 'foo-000-1.log'],
+                ['sub', 'foo-000-1.prof'],
+                ['sub', 'foo-000-2.log'],
+                ['sub', 'foo-000-2.prof'])
+
+test.must_not_exist('foo-003-0.log',
+                    'foo-003-0.prof',
+                    'foo-003-1.log',
+                    'foo-003-1.prof',
+                    'foo-003-2.log',
+                    'foo-003-2.prof')
+
+test.pass_test()
diff --git a/test/scons-time/run/option/no-args.py b/test/scons-time/run/option/no-args.py
new file mode 100644 (file)
index 0000000..520b27d
--- /dev/null
@@ -0,0 +1,42 @@
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+"""
+Verify the error when the run command is passed no arguments.
+"""
+
+import TestSCons_time
+
+test = TestSCons_time.TestSCons_time()
+
+expect = """\
+scons-time: run: No arguments or -f config file specified.
+            Type "scons-time help run" for help.
+"""
+
+test.run(arguments = 'run', status = 1, stderr = expect)
+
+test.pass_test()
diff --git a/test/scons-time/run/option/no-exec.py b/test/scons-time/run/option/no-exec.py
new file mode 100644 (file)
index 0000000..a2eab74
--- /dev/null
@@ -0,0 +1,49 @@
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+"""
+Verify that the run -n and --no-exec options don't actually create the
+expected output files.
+"""
+
+import TestSCons_time
+
+test = TestSCons_time.TestSCons_time()
+
+test.write_sample_project('foo.tar.gz')
+
+test.run(arguments = 'run -n foo.tar.gz')
+
+test.run(arguments = 'run --no-exec foo.tar.gz')
+
+test.must_not_exist('foo-000-0.log',
+                    'foo-000-0.prof',
+                    'foo-000-1.log',
+                    'foo-000-1.prof',
+                    'foo-000-2.log',
+                    'foo-000-2.prof')
+
+test.pass_test()
diff --git a/test/scons-time/run/option/number.py b/test/scons-time/run/option/number.py
new file mode 100644 (file)
index 0000000..ddf2d86
--- /dev/null
@@ -0,0 +1,48 @@
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+"""
+Verify the run --number option specifies the run number.
+"""
+
+import TestSCons_time
+
+test = TestSCons_time.TestSCons_time()
+
+test.write_fake_scons_py()
+
+test.write_sample_project('foo.tar.gz')
+
+test.run(arguments = 'run --number 77 foo.tar.gz')
+
+test.must_exist('foo-077-0.log',
+                'foo-077-0.prof',
+                'foo-077-1.log',
+                'foo-077-1.prof',
+                'foo-077-2.log',
+                'foo-077-2.prof')
+
+test.pass_test()
diff --git a/test/scons-time/run/option/outdir.py b/test/scons-time/run/option/outdir.py
new file mode 100644 (file)
index 0000000..f60eba0
--- /dev/null
@@ -0,0 +1,51 @@
+
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+"""
+Verify specifying an alternate output directory with the --outdir option.
+"""
+
+import TestSCons_time
+
+test = TestSCons_time.TestSCons_time()
+
+test.subdir('sub')
+
+test.write_fake_scons_py()
+
+test.write_sample_project('foo.tar.gz')
+
+test.run(arguments = 'run --outdir sub foo.tar.gz')
+
+test.must_exist(['sub', 'foo-000-0.log'],
+                ['sub', 'foo-000-0.prof'],
+                ['sub', 'foo-000-1.log'],
+                ['sub', 'foo-000-1.prof'],
+                ['sub', 'foo-000-2.log'],
+                ['sub', 'foo-000-2.prof'])
+
+test.pass_test()
diff --git a/test/scons-time/run/option/prefix.py b/test/scons-time/run/option/prefix.py
new file mode 100644 (file)
index 0000000..6bc9619
--- /dev/null
@@ -0,0 +1,48 @@
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+"""
+Verify specifying an alternate file prefix with the --prefix option.
+"""
+
+import TestSCons_time
+
+test = TestSCons_time.TestSCons_time()
+
+test.write_fake_scons_py()
+
+test.write_sample_project('foo.tar.gz')
+
+test.run(arguments = 'run --prefix bar foo.tar.gz')
+
+test.must_exist('bar-000-0.log',
+                'bar-000-0.prof',
+                'bar-000-1.log',
+                'bar-000-1.prof',
+                'bar-000-2.log',
+                'bar-000-2.prof')
+
+test.pass_test()
diff --git a/test/scons-time/run/option/python.py b/test/scons-time/run/option/python.py
new file mode 100644 (file)
index 0000000..bd12efe
--- /dev/null
@@ -0,0 +1,64 @@
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+"""
+Verify the run --python option to specify an alternatie Python executable.
+"""
+
+import os
+
+import TestSCons_time
+
+test = TestSCons_time.TestSCons_time()
+
+test.write_sample_project('foo.tar.gz')
+
+my_python_py = test.workpath('my_python.py')
+
+test.write(my_python_py, """\
+#!/usr/bin/env python
+import sys
+profile = ''
+for arg in sys.argv[1:]:
+    if arg.startswith('--profile='):
+        profile = arg[10:]
+        break
+sys.stdout.write('my_python.py: %s\\n' % profile)
+""")
+
+os.chmod(my_python_py, 0755)
+
+test.run(arguments = 'run --python %s foo.tar.gz' % my_python_py)
+
+prof0 = test.workpath('foo-000-0.prof')
+prof1 = test.workpath('foo-000-1.prof')
+prof2 = test.workpath('foo-000-2.prof')
+
+test.must_match('foo-000-0.log', "my_python.py: %s\n" % prof0)
+test.must_match('foo-000-1.log', "my_python.py: %s\n" % prof1)
+test.must_match('foo-000-2.log', "my_python.py: %s\n" % prof2)
+
+test.pass_test()
diff --git a/test/scons-time/run/option/quiet.py b/test/scons-time/run/option/quiet.py
new file mode 100644 (file)
index 0000000..5d5d7cf
--- /dev/null
@@ -0,0 +1,96 @@
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+"""
+Verify that the run -q and --quiet options suppress build output.
+"""
+
+import re
+
+import TestSCons_time
+
+python = TestSCons_time.python
+
+test = TestSCons_time.TestSCons_time(match = TestSCons_time.match_re)
+test.diff_function = TestSCons_time.diff_re
+
+
+def tempdir_re(*args):
+    import os
+    import os.path
+    import string
+    import tempfile
+
+    sep = re.escape(os.sep)
+    args = (tempfile.gettempdir(), 'scons-time-',) + args
+    x = apply(os.path.join, args)
+    x = re.escape(x)
+    x = string.replace(x, 'time\\-', 'time\\-[^%s]*' % sep)
+    return x
+
+scons_py = re.escape(test.workpath('src', 'script', 'scons.py'))
+src_engine = re.escape(test.workpath('src', 'engine'))
+
+tmp_scons_time = tempdir_re()
+tmp_scons_time_foo = tempdir_re('foo')
+
+
+test.write_fake_scons_py()
+
+foo_tar_gz = test.write_sample_project('foo.tar.gz')
+
+expect = """\
+%(scons_py)s
+    --version
+SCONS_LIB_DIR = %(src_engine)s
+SConstruct file directory: %(tmp_scons_time_foo)s
+""" % locals()
+
+test.run(arguments = 'run -q foo.tar.gz', stdout = expect)
+
+test.must_exist('foo-000-0.log',
+                'foo-000-0.prof',
+                'foo-000-1.log',
+                'foo-000-1.prof',
+                'foo-000-2.log',
+                'foo-000-2.prof')
+
+scons_py = test.workpath('src/script/scons.py')
+
+src_engine = test.workpath('src/engine')
+
+test.run(arguments = 'run -q foo.tar.gz', stdout = expect)
+
+test.must_exist('foo-001-0.log',
+                'foo-001-0.prof',
+                'foo-001-1.log',
+                'foo-001-1.prof',
+                'foo-001-2.log',
+                'foo-001-2.prof')
+
+test.run(arguments = 'run --quiet foo.tar.gz', stdout = expect)
+
+test.pass_test()
diff --git a/test/scons-time/run/option/scons.py b/test/scons-time/run/option/scons.py
new file mode 100644 (file)
index 0000000..14ac18d
--- /dev/null
@@ -0,0 +1,59 @@
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+"""
+Verify the run --scons option to specify an alternatie SCons script.
+"""
+
+import TestSCons_time
+
+test = TestSCons_time.TestSCons_time()
+
+test.write_sample_project('foo.tar.gz')
+
+my_scons_py = test.workpath('my_scons.py')
+
+test.write(my_scons_py, """\
+import sys
+profile = ''
+for arg in sys.argv[1:]:
+    if arg.startswith('--profile='):
+        profile = arg[10:]
+        break
+print 'my_scons.py: %s' % profile
+""")
+
+test.run(arguments = 'run --scons %s foo.tar.gz' % my_scons_py)
+
+prof0 = test.workpath('foo-000-0.prof')
+prof1 = test.workpath('foo-000-1.prof')
+prof2 = test.workpath('foo-000-2.prof')
+
+test.must_match('foo-000-0.log', "my_scons.py: %s\n" % prof0)
+test.must_match('foo-000-1.log', "my_scons.py: %s\n" % prof1)
+test.must_match('foo-000-2.log', "my_scons.py: %s\n" % prof2)
+
+test.pass_test()
diff --git a/test/scons-time/run/option/subdir.py b/test/scons-time/run/option/subdir.py
new file mode 100644 (file)
index 0000000..b96d0e6
--- /dev/null
@@ -0,0 +1,59 @@
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+"""
+Verify that we change to a subdirectory before building if asked to do so.
+"""
+
+import os
+import re
+
+import TestSCons_time
+
+test = TestSCons_time.TestSCons_time()
+
+test.write_fake_scons_py()
+
+test.write_sample_project('foo.tar.gz', 'subdir')
+
+test.run(arguments = 'run --subdir subdir foo.tar.gz')
+
+test.must_exist('foo-000-0.log',
+                'foo-000-0.prof',
+                'foo-000-1.log',
+                'foo-000-1.prof',
+                'foo-000-2.log',
+                'foo-000-2.prof')
+
+expect = [
+    'SConstruct file directory: .*%ssubdir$' % re.escape(os.sep),
+]
+
+content = test.read(test.workpath('foo-000-0.log'), mode='r')
+
+test.must_contain_all_lines('foo-000-0.log', content, expect, re.search)
+
+test.pass_test()
diff --git a/test/scons-time/run/option/verbose.py b/test/scons-time/run/option/verbose.py
new file mode 100644 (file)
index 0000000..cdf5b5a
--- /dev/null
@@ -0,0 +1,161 @@
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+"""
+Verify that the run -v and --verbose options display command output.
+"""
+
+import re
+
+import TestSCons_time
+
+_python_ = re.escape(TestSCons_time._python_)
+
+
+test = TestSCons_time.TestSCons_time(match = TestSCons_time.match_re)
+test.diff_function = TestSCons_time.diff_re
+
+
+def tempdir_re(*args):
+    import os
+    import os.path
+    import string
+    import tempfile
+
+    sep = re.escape(os.sep)
+    args = (tempfile.gettempdir(), 'scons-time-',) + args
+    x = apply(os.path.join, args)
+    x = re.escape(x)
+    x = string.replace(x, 'time\\-', 'time\\-[^%s]*' % sep)
+    return x
+
+scons_py = re.escape(test.workpath('src', 'script', 'scons.py'))
+src_engine = re.escape(test.workpath('src', 'engine'))
+
+tmp_scons_time = tempdir_re()
+tmp_scons_time_foo = tempdir_re('foo')
+
+
+test.write_fake_scons_py()
+
+foo_tar_gz = test.write_sample_project('foo.tar.gz')
+
+expect = """\
+%(scons_py)s
+    --version
+SCONS_LIB_DIR = %(src_engine)s
+SConstruct file directory: %(tmp_scons_time_foo)s
+""" % locals()
+
+test.run(arguments = 'run -q foo.tar.gz', stdout = expect)
+
+test.must_exist('foo-000-0.log',
+                'foo-000-0.prof',
+                'foo-000-1.log',
+                'foo-000-1.prof',
+                'foo-000-2.log',
+                'foo-000-2.prof')
+
+time_re = r'\[\d\d:\d\d:\d\d\]'
+
+scons_flags = '--debug=count --debug=memory --debug=time --debug=memoizer'
+
+
+expect = """\
+scons-time%(time_re)s: mkdir %(tmp_scons_time)s
+scons-time%(time_re)s: cd %(tmp_scons_time)s
+scons-time%(time_re)s: tar xzf %(foo_tar_gz)s
+scons-time%(time_re)s: cd foo
+scons-time%(time_re)s: find \\* -type f | xargs cat > /dev/null
+scons-time%(time_re)s: export SCONS_LIB_DIR=%(src_engine)s
+scons-time%(time_re)s: %(_python_)s %(scons_py)s --version
+%(scons_py)s
+    --version
+SCONS_LIB_DIR = %(src_engine)s
+SConstruct file directory: %(tmp_scons_time_foo)s
+scons-time%(time_re)s: %(_python_)s %(scons_py)s %(scons_flags)s --profile=%(prof0)s --help 2>&1 \\| tee %(log0)s
+%(scons_py)s
+    --debug=count
+    --debug=memory
+    --debug=time
+    --debug=memoizer
+    --profile=%(prof0)s
+    --help
+SCONS_LIB_DIR = %(src_engine)s
+SConstruct file directory: %(tmp_scons_time_foo)s
+scons-time%(time_re)s: %(_python_)s %(scons_py)s %(scons_flags)s --profile=%(prof1)s  2>&1 \\| tee %(log1)s
+%(scons_py)s
+    --debug=count
+    --debug=memory
+    --debug=time
+    --debug=memoizer
+    --profile=%(prof1)s
+SCONS_LIB_DIR = %(src_engine)s
+SConstruct file directory: %(tmp_scons_time_foo)s
+scons-time%(time_re)s: %(_python_)s %(scons_py)s %(scons_flags)s --profile=%(prof2)s  2>&1 \\| tee %(log2)s
+%(scons_py)s
+    --debug=count
+    --debug=memory
+    --debug=time
+    --debug=memoizer
+    --profile=%(prof2)s
+SCONS_LIB_DIR = %(src_engine)s
+SConstruct file directory: %(tmp_scons_time_foo)s
+scons-time%(time_re)s: cd .*
+scons-time%(time_re)s: rm -rf %(tmp_scons_time)s
+"""
+
+foo_tar_gz = re.escape(foo_tar_gz)
+
+log0 = re.escape(test.workpath('foo-001-0.log'))
+log1 = re.escape(test.workpath('foo-001-1.log'))
+log2 = re.escape(test.workpath('foo-001-2.log'))
+
+prof0 = re.escape(test.workpath('foo-001-0.prof'))
+prof1 = re.escape(test.workpath('foo-001-1.prof'))
+prof2 = re.escape(test.workpath('foo-001-2.prof'))
+
+test.run(arguments = 'run -v foo.tar.gz', stdout = expect % locals())
+
+test.must_exist('foo-001-0.log',
+                'foo-001-0.prof',
+                'foo-001-1.log',
+                'foo-001-1.prof',
+                'foo-001-2.log',
+                'foo-001-2.prof')
+
+log0 = re.escape(test.workpath('foo-002-0.log'))
+log1 = re.escape(test.workpath('foo-002-1.log'))
+log2 = re.escape(test.workpath('foo-002-2.log'))
+
+prof0 = re.escape(test.workpath('foo-002-0.prof'))
+prof1 = re.escape(test.workpath('foo-002-1.prof'))
+prof2 = re.escape(test.workpath('foo-002-2.prof'))
+
+test.run(arguments = 'run --verbose foo.tar.gz', stdout = expect % locals())
+
+
+test.pass_test()
diff --git a/test/scons-time/run/subversion.py b/test/scons-time/run/subversion.py
new file mode 100644 (file)
index 0000000..3839999
--- /dev/null
@@ -0,0 +1,85 @@
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+"""
+Verify ability to "check out" an SCons revision from a fake
+Subversion utility.
+"""
+
+import re
+import tempfile
+
+import TestSCons_time
+
+test = TestSCons_time.TestSCons_time()
+
+test.write_sample_project('foo.tar')
+
+my_svn_py = test.write_fake_svn_py('my_svn.py')
+
+test.write('config', """\
+svn = r'%(my_svn_py)s'
+""" % locals())
+
+test.run(arguments = 'run -f config --svn http://xyzzy --number 617,716 foo.tar')
+
+test.must_exist('foo-617-0.log',
+                'foo-617-0.prof',
+                'foo-617-1.log',
+                'foo-617-1.prof',
+                'foo-617-2.log',
+                'foo-617-2.prof')
+
+test.must_exist('foo-716-0.log',
+                'foo-716-0.prof',
+                'foo-716-1.log',
+                'foo-716-1.prof',
+                'foo-716-2.log',
+                'foo-716-2.prof')
+
+def tempdir_re(*args):
+    import os
+    import os.path
+    import string
+    import tempfile
+
+    sep = re.escape(os.sep)
+    args = (tempfile.gettempdir(), 'scons-time-svn-',) + args
+    x = apply(os.path.join, args)
+    x = re.escape(x)
+    x = string.replace(x, 'svn\\-', 'svn\\-[^%s]*' % sep)
+    return x
+
+expect = [
+    tempdir_re('src', 'script', 'scons.py'),
+    'SCONS_LIB_DIR = %s' % tempdir_re('src', 'engine'),
+]
+
+content = test.read(test.workpath('foo-617-2.log'), mode='r')
+
+test.must_contain_all_lines('foo-617-2.log', content, expect, re.search)
+
+test.pass_test()
diff --git a/test/scons-time/time/chdir.py b/test/scons-time/time/chdir.py
new file mode 100644 (file)
index 0000000..b2a4d07
--- /dev/null
@@ -0,0 +1,57 @@
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+"""
+Verify that the time -C and --chdir options change directory before
+globbing for files.
+"""
+
+import os.path
+
+import TestSCons_time
+
+test = TestSCons_time.TestSCons_time()
+
+test.subdir('logs')
+
+lines = [
+    '       Total  SConscripts        SCons     commands\n'
+]
+
+line_fmt = '   11.123456    22.234567    33.345678    44.456789    %s\n'
+
+for i in xrange(9):
+    logfile_name = os.path.join('logs', 'foo-%s.log' % i)
+    test.fake_logfile(logfile_name)
+    lines.append(line_fmt % logfile_name)
+
+expect = ''.join(lines)
+
+test.run(arguments = 'time -C logs foo-*.log', stdout = expect)
+
+test.run(arguments = 'time --chdir logs foo-?.log', stdout = expect)
+
+test.pass_test()
diff --git a/test/scons-time/time/file.py b/test/scons-time/time/file.py
new file mode 100644 (file)
index 0000000..f4046c9
--- /dev/null
@@ -0,0 +1,71 @@
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+"""
+Verify that config files specified with the -f and --file options
+affect how the time subcommand processes things.
+"""
+
+import TestSCons_time
+
+test = TestSCons_time.TestSCons_time()
+
+test.fake_logfile('foo-001-0.log')
+
+test.fake_logfile('foo-002-0.log')
+
+
+test.write('st1.conf', """\
+prefix = 'foo-001'
+""")
+
+expect1 = """\
+       Total  SConscripts        SCons     commands
+   11.123456    22.234567    33.345678    44.456789    foo-001-0.log
+"""
+
+test.run(arguments = 'time -f st1.conf', stdout = expect1)
+
+
+test.write('st2.conf', """\
+prefix = 'foo'
+title = 'ST2.CONF TITLE'
+""")
+
+expect2 = \
+r"""set title "ST2.CONF TITLE"
+set key bottom left
+plot '-' title "Startup" with lines lt 1
+# Startup
+1 11.123456
+2 11.123456
+e
+"""
+
+test.run(arguments = 'time --file st2.conf --fmt gnuplot', stdout = expect2)
+
+
+test.pass_test()
diff --git a/test/scons-time/time/format-gnuplot.py b/test/scons-time/time/format-gnuplot.py
new file mode 100644 (file)
index 0000000..8a599a4
--- /dev/null
@@ -0,0 +1,72 @@
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+"""
+Verify the time --format=gnuplot option.
+"""
+
+import TestSCons_time
+
+test = TestSCons_time.TestSCons_time()
+
+test.fake_logfile('foo-000-0.log', 0)
+test.fake_logfile('foo-000-1.log', 0)
+test.fake_logfile('foo-000-2.log', 0)
+
+test.fake_logfile('foo-001-0.log', 1)
+test.fake_logfile('foo-001-1.log', 1)
+test.fake_logfile('foo-001-2.log', 1)
+
+expect_notitle = """\
+set key bottom left
+plot '-' title "Startup" with lines lt 1, \\
+     '-' title "Full build" with lines lt 2, \\
+     '-' title "Up-to-date build" with lines lt 3
+# Startup
+0 11.123456
+1 11.123456
+e
+# Full build
+0 11.123456
+1 11.123456
+e
+# Up-to-date build
+0 11.123456
+1 11.123456
+e
+"""
+
+expect_title = 'set title "TITLE"\n' + expect_notitle
+
+test.run(arguments = 'time --fmt gnuplot', stdout=expect_notitle)
+
+test.run(arguments = 'time --fmt=gnuplot --title TITLE', stdout=expect_title)
+
+test.run(arguments = 'time --format gnuplot --title TITLE', stdout=expect_title)
+
+test.run(arguments = 'time --format=gnuplot', stdout=expect_notitle)
+
+test.pass_test()
diff --git a/test/scons-time/time/glob.py b/test/scons-time/time/glob.py
new file mode 100644 (file)
index 0000000..5bf2ccd
--- /dev/null
@@ -0,0 +1,52 @@
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+"""
+Verify that the time subcommand globs for files.
+"""
+
+import TestSCons_time
+
+test = TestSCons_time.TestSCons_time()
+
+lines = [
+    '       Total  SConscripts        SCons     commands\n'
+]
+
+line_fmt = '   11.123456    22.234567    33.345678    44.456789    %s\n'
+
+for i in xrange(9):
+    logfile_name = 'foo-%s.log' % i
+    test.fake_logfile(logfile_name)
+    lines.append(line_fmt % logfile_name)
+
+expect = ''.join(lines)
+
+test.run(arguments = 'time foo-*.log', stdout = expect)
+
+test.run(arguments = 'time foo-?.log', stdout = expect)
+
+test.pass_test()
diff --git a/test/scons-time/time/help.py b/test/scons-time/time/help.py
new file mode 100644 (file)
index 0000000..2f7cb67
--- /dev/null
@@ -0,0 +1,57 @@
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+"""
+Verify the "time" subcommand help.
+"""
+
+import TestSCons_time
+
+test = TestSCons_time.TestSCons_time()
+
+expect = [
+    "Usage: scons-time time [OPTIONS] FILE [...]\n",
+    "  -C DIR, --chdir=DIR           Change to DIR before looking for files\n",
+    "  -h, --help                    Print this help and exit\n",
+]
+
+test.run(arguments = 'time -h')
+
+test.must_contain_all_lines('Standard output', test.stdout(), expect)
+
+test.run(arguments = 'time -?')
+
+test.must_contain_all_lines('Standard output', test.stdout(), expect)
+
+test.run(arguments = 'time --help')
+
+test.must_contain_all_lines('Standard output', test.stdout(), expect)
+
+test.run(arguments = 'help time')
+
+test.must_contain_all_lines('Standard output', test.stdout(), expect)
+
+test.pass_test()
diff --git a/test/scons-time/time/no-args.py b/test/scons-time/time/no-args.py
new file mode 100644 (file)
index 0000000..510b1b3
--- /dev/null
@@ -0,0 +1,43 @@
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+"""
+Verify the error when the time subcommand is passed no arguments.
+"""
+
+import TestSCons_time
+
+test = TestSCons_time.TestSCons_time()
+
+expect = """\
+scons-time: mem: No arguments specified.
+            No *.log files found in "%s".
+            Type "scons-time help mem" for help.
+""" % test.workpath()
+
+test.run(arguments = 'mem', status = 1, stderr = expect)
+
+test.pass_test()
diff --git a/test/scons-time/time/prefix.py b/test/scons-time/time/prefix.py
new file mode 100644 (file)
index 0000000..cf0462a
--- /dev/null
@@ -0,0 +1,62 @@
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+"""
+Verify that the time -p and --prefix options specify what log files to use.
+"""
+
+import os.path
+
+import TestSCons_time
+
+test = TestSCons_time.TestSCons_time()
+
+test.subdir('logs')
+
+header = '       Total  SConscripts        SCons     commands\n'
+
+line_fmt = '   11.123456    22.234567    33.345678    44.456789    %s\n'
+
+foo_lines = [ header ]
+bar_lines = [ header ]
+
+for i in xrange(3):
+    logfile_name = os.path.join('foo-%s.log' % i)
+    test.fake_logfile(logfile_name)
+    foo_lines.append(line_fmt % logfile_name)
+
+    logfile_name = os.path.join('bar-%s.log' % i)
+    test.fake_logfile(logfile_name)
+    bar_lines.append(line_fmt % logfile_name)
+
+foo_expect = ''.join(foo_lines)
+bar_expect = ''.join(bar_lines)
+
+test.run(arguments = 'time -p bar', stdout = bar_expect)
+
+test.run(arguments = 'time --prefix=foo', stdout = foo_expect)
+
+test.pass_test()
diff --git a/test/scons-time/time/tail.py b/test/scons-time/time/tail.py
new file mode 100644 (file)
index 0000000..600bbc8
--- /dev/null
@@ -0,0 +1,55 @@
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+"""
+Verify that the time subcommand only prints results for the last number
+of files specified with the -t and --tail options.
+"""
+
+import TestSCons_time
+
+test = TestSCons_time.TestSCons_time()
+
+
+header = '       Total  SConscripts        SCons     commands\n'
+
+lines = []
+
+line_fmt = '   11.123456    22.234567    33.345678    44.456789    %s\n'
+
+for i in xrange(9):
+    logfile_name = 'foo-%s.log' % i
+    test.fake_logfile(logfile_name)
+    lines.append(line_fmt % logfile_name)
+
+expect3 = [header] + lines[-3:]
+expect5 = [header] + lines[-5:]
+
+test.run(arguments = 'time -t 3 foo-*.log', stdout = ''.join(expect3))
+
+test.run(arguments = 'time --tail 5 foo-*.log', stdout = ''.join(expect5))
+
+test.pass_test()
diff --git a/test/scons-time/time/which.py b/test/scons-time/time/which.py
new file mode 100644 (file)
index 0000000..30ce3a6
--- /dev/null
@@ -0,0 +1,84 @@
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+"""
+Verify the time --which option.
+"""
+
+import TestSCons_time
+
+test = TestSCons_time.TestSCons_time()
+
+test.fake_logfile('foo-000-0.log', 0)
+test.fake_logfile('foo-000-1.log', 0)
+test.fake_logfile('foo-000-2.log', 0)
+
+test.fake_logfile('foo-001-0.log', 1)
+test.fake_logfile('foo-001-1.log', 1)
+test.fake_logfile('foo-001-2.log', 1)
+
+expect = """\
+set key bottom left
+plot '-' title "Startup" with lines lt 1, \\
+     '-' title "Full build" with lines lt 2, \\
+     '-' title "Up-to-date build" with lines lt 3
+# Startup
+0 %(time)s
+1 %(time)s
+e
+# Full build
+0 %(time)s
+1 %(time)s
+e
+# Up-to-date build
+0 %(time)s
+1 %(time)s
+e
+"""
+
+total       = expect % {'time' : 11.123456}
+SConscripts = expect % {'time' : 22.234567}
+SCons       = expect % {'time' : 33.345678}
+commands    = expect % {'time' : 44.456789}
+
+test.run(arguments = 'time --fmt gnuplot --which total', stdout=total)
+
+test.run(arguments = 'time --fmt gnuplot --which=SConscripts', stdout=SConscripts)
+
+test.run(arguments = 'time --fmt gnuplot --which=SCons', stdout=SCons)
+
+test.run(arguments = 'time --fmt gnuplot --which commands', stdout=commands)
+
+expect = """\
+scons-time: time: Unrecognized timer "unknown".
+            Type "scons-time help time" for help.
+"""
+
+test.run(arguments = 'time --fmt gnuplot --which unknown',
+         status = 1,
+         stderr = expect)
+
+test.pass_test()
diff --git a/test/scons-time/unknown.py b/test/scons-time/unknown.py
new file mode 100644 (file)
index 0000000..2fb673f
--- /dev/null
@@ -0,0 +1,45 @@
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+"""
+Verify that invoking scons-test with an unknown argument generates
+the appropriate error message and exits non-zero.
+"""
+
+import os
+
+import TestSCons_time
+
+test = TestSCons_time.TestSCons_time()
+
+expect = """\
+scons-time: Unknown subcommand "unknown".
+Type "scons-time help" for usage.
+"""
+
+test.run(arguments = 'unknown', status=1, stderr=expect)
+
+test.pass_test()
diff --git a/test/sconsign/script.py b/test/sconsign/script.py
deleted file mode 100644 (file)
index 9612560..0000000
+++ /dev/null
@@ -1,602 +0,0 @@
-#!/usr/bin/env python
-#
-# __COPYRIGHT__
-#
-# Permission is hereby granted, free of charge, to any person obtaining
-# a copy of this software and associated documentation files (the
-# "Software"), to deal in the Software without restriction, including
-# without limitation the rights to use, copy, modify, merge, publish,
-# distribute, sublicense, and/or sell copies of the Software, and to
-# permit persons to whom the Software is furnished to do so, subject to
-# the following conditions:
-#
-# The above copyright notice and this permission notice shall be included
-# in all copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
-# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
-# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
-# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
-# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
-# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
-# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-#
-
-__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
-
-import os.path
-import string
-import time
-
-import TestCmd
-import TestSCons
-
-class MyTestSCons(TestSCons.TestSCons):
-    # subclass with a method for running the sconsign script
-    def __init__(self, *args, **kw):
-        try:
-            script_dir = os.environ['SCONS_SCRIPT_DIR']
-        except KeyError:
-            pass
-        else:
-            os.chdir(script_dir)
-        self.script_dir = os.getcwd()
-        apply(TestSCons.TestSCons.__init__, (self,)+args, kw)
-        self.my_kw = {
-            'interpreter' : TestSCons.python,
-        }
-    def script_path(self, script):
-        return os.path.join(self.script_dir, script)
-    def set_sconsign(self, sconsign):
-        self.my_kw['program'] = sconsign
-    def run_sconsign(self, *args, **kw):
-        kw.update(self.my_kw)
-        return apply(self.run, args, kw)
-
-test = MyTestSCons(match = TestCmd.match_re)
-
-# Check for the sconsign script before we instantiate TestSCons(),
-# because that will change directory on us.
-if os.path.exists(test.script_path('sconsign.py')):
-    sconsign = 'sconsign.py'
-elif os.path.exists(test.script_path('sconsign')):
-    sconsign = 'sconsign'
-else:
-    print "Can find neither 'sconsign.py' nor 'sconsign' scripts."
-    test.no_result()
-
-test.set_sconsign(sconsign)
-
-def sort_match(test, lines, expect):
-    lines = string.split(lines, '\n')
-    lines.sort()
-    expect = string.split(expect, '\n')
-    expect.sort()
-    return test.match_re(lines, expect)
-
-def re_sep(*args):
-    return string.replace(apply(os.path.join, args), '\\', '\\\\')
-
-
-
-test.subdir('work1', ['work1', 'sub1'], ['work1', 'sub2'],
-            'work2', ['work2', 'sub1'], ['work2', 'sub2'])
-
-test.write(['work1', 'SConstruct'], """
-SConsignFile(None)
-env1 = Environment(PROGSUFFIX = '.exe', OBJSUFFIX = '.obj')
-env1.Program('sub1/hello.c')
-env2 = env1.Clone(CPPPATH = ['sub2'])
-env2.Program('sub2/hello.c')
-""")
-
-test.write(['work1', 'sub1', 'hello.c'], r"""\
-#include <stdio.h>
-#include <stdlib.h>
-int
-main(int argc, char *argv[])
-{
-        argv[argc++] = "--";
-        printf("sub1/hello.c\n");
-        exit (0);
-}
-""")
-
-test.write(['work1', 'sub2', 'hello.c'], r"""\
-#include <stdio.h>
-#include <stdlib.h>
-#include <inc1.h>
-#include <inc2.h>
-int
-main(int argc, char *argv[])
-{
-        argv[argc++] = "--";
-        printf("sub2/goodbye.c\n");
-        exit (0);
-}
-""")
-
-test.write(['work1', 'sub2', 'inc1.h'], r"""\
-#define STRING1 "inc1.h"
-""")
-
-test.write(['work1', 'sub2', 'inc2.h'], r"""\
-#define STRING2 "inc2.h"
-""")
-
-test.run(chdir = 'work1', arguments = '--implicit-cache .')
-
-test.run_sconsign(arguments = "work1/sub1/.sconsign",
-         stdout = """\
-hello.exe: \S+ None \d+ \d+
-        hello.obj: \S+
-hello.obj: \S+ None \d+ \d+
-        hello.c: \S+
-""")
-
-test.run_sconsign(arguments = "--raw work1/sub1/.sconsign",
-         stdout = """\
-hello.exe: {'bsig': '\S+', 'size': \d+L?, 'timestamp': \d+}
-        hello.obj: \S+
-hello.obj: {'bsig': '\S+', 'size': \d+L?, 'timestamp': \d+}
-        hello.c: \S+
-""")
-
-test.run_sconsign(arguments = "-v work1/sub1/.sconsign",
-         stdout = """\
-hello.exe:
-    bsig: \S+
-    csig: None
-    timestamp: \d+
-    size: \d+
-    implicit:
-        hello.obj: \S+
-hello.obj:
-    bsig: \S+
-    csig: None
-    timestamp: \d+
-    size: \d+
-    implicit:
-        hello.c: \S+
-""")
-
-test.run_sconsign(arguments = "-b -v work1/sub1/.sconsign",
-         stdout = """\
-hello.exe:
-    bsig: \S+
-hello.obj:
-    bsig: \S+
-""")
-
-test.run_sconsign(arguments = "-c -v work1/sub1/.sconsign",
-         stdout = """\
-hello.exe:
-    csig: None
-hello.obj:
-    csig: None
-""")
-
-test.run_sconsign(arguments = "-s -v work1/sub1/.sconsign",
-         stdout = """\
-hello.exe:
-    size: \d+
-hello.obj:
-    size: \d+
-""")
-
-test.run_sconsign(arguments = "-t -v work1/sub1/.sconsign",
-         stdout = """\
-hello.exe:
-    timestamp: \d+
-hello.obj:
-    timestamp: \d+
-""")
-
-test.run_sconsign(arguments = "-e hello.obj work1/sub1/.sconsign",
-         stdout = """\
-hello.obj: \S+ None \d+ \d+
-        hello.c: \S+
-""")
-
-test.run_sconsign(arguments = "-e hello.obj -e hello.exe -e hello.obj work1/sub1/.sconsign",
-         stdout = """\
-hello.obj: \S+ None \d+ \d+
-        hello.c: \S+
-hello.exe: \S+ None \d+ \d+
-        hello.obj: \S+
-hello.obj: \S+ None \d+ \d+
-        hello.c: \S+
-""")
-
-# XXX NOT SURE IF THIS IS RIGHT!
-sub2_inc1_h = re_sep('sub2', 'inc1.h')
-sub2_inc2_h = re_sep('sub2', 'inc2.h')
-
-test.run_sconsign(arguments = "work1/sub2/.sconsign",
-         stdout = """\
-hello.exe: \S+ None \d+ \d+
-        hello.obj: \S+
-hello.obj: \S+ None \d+ \d+
-        hello.c: \S+
-        inc1.h: \S+
-        inc2.h: \S+
-""")
-
-#test.run_sconsign(arguments = "-i -v work1/sub2/.sconsign",
-#         stdout = """\
-#hello.exe:
-#    implicit:
-#        hello.obj: \S+ None \d+ \d+
-#hello.obj:
-#    implicit:
-#        hello.c: None \S+ \d+ \d+
-#        inc1.h: None \S+ \d+ \d+
-#        inc2.h: None \S+ \d+ \d+
-#""")
-
-test.run_sconsign(arguments = "-e hello.obj work1/sub2/.sconsign work1/sub1/.sconsign",
-         stdout = """\
-hello.obj: \S+ None \d+ \d+
-        hello.c: \S+
-        inc1.h: \S+
-        inc2.h: \S+
-hello.obj: \S+ None \d+ \d+
-        hello.c: \S+
-""")
-
-test.run(chdir = 'work1', arguments = '--clean .')
-
-test.write(['work1', 'SConstruct'], """
-SourceSignatures('timestamp')
-TargetSignatures('content')
-env1 = Environment(PROGSUFFIX = '.exe', OBJSUFFIX = '.obj')
-env1.Program('sub1/hello.c')
-env2 = env1.Clone(CPPPATH = ['sub2'])
-env2.Program('sub2/hello.c')
-""")
-
-time.sleep(1)
-
-test.run(chdir = 'work1', arguments = '. --max-drift=1 --debug=stacktrace')
-
-test.run_sconsign(arguments = "-e hello.exe -e hello.obj work1/sub1/.sconsign",
-         stdout = """\
-hello.exe: \S+ None \d+ \d+
-        hello.obj: \S+
-hello.obj: \S+ None \d+ \d+
-        hello.c: \S+
-""")
-
-test.run_sconsign(arguments = "-e hello.exe -e hello.obj -r work1/sub1/.sconsign",
-         stdout = """\
-hello.exe: \S+ None '\S+ \S+ [ \d]\d \d\d:\d\d:\d\d \d\d\d\d' \d+
-        hello.obj: \S+
-hello.obj: \S+ None '\S+ \S+ [ \d]\d \d\d:\d\d:\d\d \d\d\d\d' \d+
-        hello.c: \S+
-""")
-
-
-##############################################################################
-
-test.write(['work2', 'SConstruct'], """
-SConsignFile()
-env1 = Environment(PROGSUFFIX = '.exe', OBJSUFFIX = '.obj')
-env1.Program('sub1/hello.c')
-env2 = env1.Clone(CPPPATH = ['sub2'])
-env2.Program('sub2/hello.c')
-""")
-
-test.write(['work2', 'sub1', 'hello.c'], r"""\
-#include <stdio.h>
-#include <stdlib.h>
-int
-main(int argc, char *argv[])
-{
-        argv[argc++] = "--";
-        printf("sub1/hello.c\n");
-        exit (0);
-}
-""")
-
-test.write(['work2', 'sub2', 'hello.c'], r"""\
-#include <stdio.h>
-#include <stdlib.h>
-#include <inc1.h>
-#include <inc2.h>
-int
-main(int argc, char *argv[])
-{
-        argv[argc++] = "--";
-        printf("sub2/goodbye.c\n");
-        exit (0);
-}
-""")
-
-test.write(['work2', 'sub2', 'inc1.h'], r"""\
-#define STRING1 "inc1.h"
-""")
-
-test.write(['work2', 'sub2', 'inc2.h'], r"""\
-#define STRING2 "inc2.h"
-""")
-
-test.run(chdir = 'work2', arguments = '--implicit-cache .')
-
-test.run_sconsign(arguments = "work2/.sconsign")
-
-test.run_sconsign(arguments = "work2/.sconsign",
-         stdout = """\
-=== sub1:
-hello.exe: \S+ None \d+ \d+
-        hello.obj: \S+
-hello.obj: \S+ None \d+ \d+
-        hello.c: \S+
-=== sub2:
-hello.exe: \S+ None \d+ \d+
-        hello.obj: \S+
-hello.obj: \S+ None \d+ \d+
-        hello.c: \S+
-        inc1.h: \S+
-        inc2.h: \S+
-""")
-
-test.run_sconsign(arguments = "--raw work2/.sconsign",
-         stdout = """\
-=== sub1:
-hello.exe: {'bsig': '\S+', 'size': \d+L?, 'timestamp': \d+}
-        hello.obj: \S+
-hello.obj: {'bsig': '\S+', 'size': \d+L?, 'timestamp': \d+}
-        hello.c: \S+
-=== sub2:
-hello.exe: {'bsig': '\S+', 'size': \d+L?, 'timestamp': \d+}
-        hello.obj: \S+
-hello.obj: {'bsig': '\S+', 'size': \d+L?, 'timestamp': \d+}
-        hello.c: \S+
-        inc1.h: \S+
-        inc2.h: \S+
-""")
-
-test.run_sconsign(arguments = "-v work2/.sconsign",
-         stdout = """\
-=== sub1:
-hello.exe:
-    bsig: \S+
-    csig: None
-    timestamp: \d+
-    size: \d+
-    implicit:
-        hello.obj: \S+
-hello.obj:
-    bsig: \S+
-    csig: None
-    timestamp: \d+
-    size: \d+
-    implicit:
-        hello.c: \S+
-=== sub2:
-hello.exe:
-    bsig: \S+
-    csig: None
-    timestamp: \d+
-    size: \d+
-    implicit:
-        hello.obj: \S+
-hello.obj:
-    bsig: \S+
-    csig: None
-    timestamp: \d+
-    size: \d+
-    implicit:
-        hello.c: \S+
-        inc1.h: \S+
-        inc2.h: \S+
-""")
-
-test.run_sconsign(arguments = "-b -v work2/.sconsign",
-         stdout = """\
-=== sub1:
-hello.exe:
-    bsig: \S+
-hello.obj:
-    bsig: \S+
-=== sub2:
-hello.exe:
-    bsig: \S+
-hello.obj:
-    bsig: \S+
-""")
-
-test.run_sconsign(arguments = "-c -v work2/.sconsign",
-         stdout = """\
-=== sub1:
-hello.exe:
-    csig: None
-hello.obj:
-    csig: None
-=== sub2:
-hello.exe:
-    csig: None
-hello.obj:
-    csig: None
-""")
-
-test.run_sconsign(arguments = "-s -v work2/.sconsign",
-         stdout = """\
-=== sub1:
-hello.exe:
-    size: \d+
-hello.obj:
-    size: \d+
-=== sub2:
-hello.exe:
-    size: \d+
-hello.obj:
-    size: \d+
-""")
-
-test.run_sconsign(arguments = "-t -v work2/.sconsign",
-         stdout = """\
-=== sub1:
-hello.exe:
-    timestamp: \d+
-hello.obj:
-    timestamp: \d+
-=== sub2:
-hello.exe:
-    timestamp: \d+
-hello.obj:
-    timestamp: \d+
-""")
-
-test.run_sconsign(arguments = "-e hello.obj work2/.sconsign",
-         stdout = """\
-=== sub1:
-hello.obj: \S+ None \d+ \d+
-        hello.c: \S+
-=== sub2:
-hello.obj: \S+ None \d+ \d+
-        hello.c: \S+
-        inc1.h: \S+
-        inc2.h: \S+
-""")
-
-test.run_sconsign(arguments = "-e hello.obj -e hello.exe -e hello.obj work2/.sconsign",
-         stdout = """\
-=== sub1:
-hello.obj: \S+ None \d+ \d+
-        hello.c: \S+
-hello.exe: \S+ None \d+ \d+
-        hello.obj: \S+
-hello.obj: \S+ None \d+ \d+
-        hello.c: \S+
-=== sub2:
-hello.obj: \S+ None \d+ \d+
-        hello.c: \S+
-        inc1.h: \S+
-        inc2.h: \S+
-hello.exe: \S+ None \d+ \d+
-        hello.obj: \S+
-hello.obj: \S+ None \d+ \d+
-        hello.c: \S+
-        inc1.h: \S+
-        inc2.h: \S+
-""")
-
-#test.run_sconsign(arguments = "-i -v work2/.sconsign",
-#         stdout = """\
-#=== sub1:
-#hello.exe:
-#    implicit:
-#        hello.obj: \S+
-#hello.obj:
-#    implicit:
-#        hello.c: \S+
-#=== sub2:
-#hello.exe:
-#    implicit:
-#        hello.obj: \S+
-#hello.obj:
-#    implicit:
-#        hello.c: \S+
-#        inc1.h: \S+
-#        inc2.h: \S+
-#""")
-
-test.run(chdir = 'work2', arguments = '--clean .')
-
-test.write(['work2','SConstruct'], """
-SConsignFile('my_sconsign')
-SourceSignatures('timestamp')
-TargetSignatures('content')
-env1 = Environment(PROGSUFFIX = '.exe', OBJSUFFIX = '.obj')
-env1.Program('sub1/hello.c')
-env2 = env1.Clone(CPPPATH = ['sub2'])
-env2.Program('sub2/hello.c')
-""")
-
-time.sleep(1)
-
-test.run(chdir = 'work2', arguments = '. --max-drift=1')
-
-test.run_sconsign(arguments = "-e hello.exe -e hello.obj -d sub1 -f dblite work2/my_sconsign",
-         stdout = """\
-=== sub1:
-hello.exe: \d+ None \d+ \d+
-        hello.obj: \d+
-hello.obj: \d+ None \d+ \d+
-        hello.c: \d+
-""")
-
-test.run_sconsign(arguments = "-e hello.exe -e hello.obj -d sub1 -f dblite work2/my_sconsign.dblite",
-         stdout = """\
-=== sub1:
-hello.exe: \d+ None \d+ \d+
-        hello.obj: \S+
-hello.obj: \d+ None \d+ \d+
-        hello.c: \S+
-""")
-
-test.run_sconsign(arguments = "-e hello.exe -e hello.obj -d sub1 -f dblite work2/my_sconsign",
-         stdout = """\
-=== sub1:
-hello.exe: \d+ None \d+ \d+
-        hello.obj: \d+
-hello.obj: \d+ None \d+ \d+
-        hello.c: \d+
-""")
-
-test.run_sconsign(arguments = "-e hello.exe -e hello.obj -d sub1 -f dblite work2/my_sconsign.dblite",
-         stdout = """\
-=== sub1:
-hello.exe: \S+ None \d+ \d+
-        hello.obj: \d+
-hello.obj: \S+ None \d+ \d+
-        hello.c: \d+
-""")
-
-test.run_sconsign(arguments = "-e hello.exe -e hello.obj -r -d sub1 -f dblite work2/my_sconsign",
-         stdout = """\
-=== sub1:
-hello.exe: \d+ None '\S+ \S+ [ \d]\d \d\d:\d\d:\d\d \d\d\d\d' \d+
-        hello.obj: \d+
-hello.obj: \d+ None '\S+ \S+ [ \d]\d \d\d:\d\d:\d\d \d\d\d\d' \d+
-        hello.c: \d+
-""")
-
-test.run_sconsign(arguments = "-e hello.exe -e hello.obj -r -d sub1 -f dblite work2/my_sconsign.dblite",
-         stdout = """\
-=== sub1:
-hello.exe: \d+ None '\S+ \S+ [ \d]\d \d\d:\d\d:\d\d \d\d\d\d' \d+
-        hello.obj: \d+
-hello.obj: \d+ None '\S+ \S+ [ \d]\d \d\d:\d\d:\d\d \d\d\d\d' \d+
-        hello.c: \d+
-""")
-
-##############################################################################
-
-test.write('bad1', "bad1\n")
-test.write('bad2.dblite', "bad2.dblite\n")
-test.write('bad3', "bad3\n")
-
-test.run_sconsign(arguments = "-f dblite no_sconsign",
-         stderr = "sconsign: \[Errno 2\] No such file or directory: 'no_sconsign'\n")
-
-test.run_sconsign(arguments = "-f dblite bad1",
-         stderr = "sconsign: \[Errno 2\] No such file or directory: 'bad1.dblite'\n")
-
-test.run_sconsign(arguments = "-f dblite bad1.dblite",
-         stderr = "sconsign: \[Errno 2\] No such file or directory: 'bad1.dblite'\n")
-
-test.run_sconsign(arguments = "-f dblite bad2",
-         stderr = "sconsign: ignoring invalid `dblite' file `bad2'\n")
-
-test.run_sconsign(arguments = "-f dblite bad2.dblite",
-         stderr = "sconsign: ignoring invalid `dblite' file `bad2.dblite'\n")
-
-test.run_sconsign(arguments = "-f sconsign no_sconsign",
-         stderr = "sconsign: \[Errno 2\] No such file or directory: 'no_sconsign'\n")
-
-test.run_sconsign(arguments = "-f sconsign bad3",
-         stderr = "sconsign: ignoring invalid .sconsign file `bad3'\n")
-
-test.pass_test()
diff --git a/test/sconsign/script/SConsignFile.py b/test/sconsign/script/SConsignFile.py
new file mode 100644 (file)
index 0000000..2f5ddf3
--- /dev/null
@@ -0,0 +1,260 @@
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+"""
+Verify that the sconsign script works with files generated when
+using the signatures in an SConsignFile().
+"""
+
+import TestSConsign
+
+test = TestSConsign.TestSConsign(match = TestSConsign.match_re)
+
+test.subdir('sub1', 'sub2')
+
+test.write(['SConstruct'], """\
+SConsignFile()
+env1 = Environment(PROGSUFFIX = '.exe', OBJSUFFIX = '.obj')
+env1.Program('sub1/hello.c')
+env2 = env1.Clone(CPPPATH = ['sub2'])
+env2.Program('sub2/hello.c')
+""")
+
+test.write(['sub1', 'hello.c'], r"""\
+#include <stdio.h>
+#include <stdlib.h>
+int
+main(int argc, char *argv[])
+{
+        argv[argc++] = "--";
+        printf("sub1/hello.c\n");
+        exit (0);
+}
+""")
+
+test.write(['sub2', 'hello.c'], r"""\
+#include <stdio.h>
+#include <stdlib.h>
+#include <inc1.h>
+#include <inc2.h>
+int
+main(int argc, char *argv[])
+{
+        argv[argc++] = "--";
+        printf("sub2/goodbye.c\n");
+        exit (0);
+}
+""")
+
+test.write(['sub2', 'inc1.h'], r"""\
+#define STRING1 "inc1.h"
+""")
+
+test.write(['sub2', 'inc2.h'], r"""\
+#define STRING2 "inc2.h"
+""")
+
+test.run(arguments = '--implicit-cache .')
+
+test.run_sconsign(arguments = ".sconsign",
+         stdout = """\
+=== sub1:
+hello.exe: \S+ None \d+ \d+
+        hello.obj: \S+
+hello.obj: \S+ None \d+ \d+
+        hello.c: \S+
+=== sub2:
+hello.exe: \S+ None \d+ \d+
+        hello.obj: \S+
+hello.obj: \S+ None \d+ \d+
+        hello.c: \S+
+        inc1.h: \S+
+        inc2.h: \S+
+""")
+
+test.run_sconsign(arguments = "--raw .sconsign",
+         stdout = """\
+=== sub1:
+hello.exe: {'bsig': '\S+', 'size': \d+L?, 'timestamp': \d+}
+        hello.obj: \S+
+hello.obj: {'bsig': '\S+', 'size': \d+L?, 'timestamp': \d+}
+        hello.c: \S+
+=== sub2:
+hello.exe: {'bsig': '\S+', 'size': \d+L?, 'timestamp': \d+}
+        hello.obj: \S+
+hello.obj: {'bsig': '\S+', 'size': \d+L?, 'timestamp': \d+}
+        hello.c: \S+
+        inc1.h: \S+
+        inc2.h: \S+
+""")
+
+test.run_sconsign(arguments = "-v .sconsign",
+         stdout = """\
+=== sub1:
+hello.exe:
+    bsig: \S+
+    csig: None
+    timestamp: \d+
+    size: \d+
+    implicit:
+        hello.obj: \S+
+hello.obj:
+    bsig: \S+
+    csig: None
+    timestamp: \d+
+    size: \d+
+    implicit:
+        hello.c: \S+
+=== sub2:
+hello.exe:
+    bsig: \S+
+    csig: None
+    timestamp: \d+
+    size: \d+
+    implicit:
+        hello.obj: \S+
+hello.obj:
+    bsig: \S+
+    csig: None
+    timestamp: \d+
+    size: \d+
+    implicit:
+        hello.c: \S+
+        inc1.h: \S+
+        inc2.h: \S+
+""")
+
+test.run_sconsign(arguments = "-b -v .sconsign",
+         stdout = """\
+=== sub1:
+hello.exe:
+    bsig: \S+
+hello.obj:
+    bsig: \S+
+=== sub2:
+hello.exe:
+    bsig: \S+
+hello.obj:
+    bsig: \S+
+""")
+
+test.run_sconsign(arguments = "-c -v .sconsign",
+         stdout = """\
+=== sub1:
+hello.exe:
+    csig: None
+hello.obj:
+    csig: None
+=== sub2:
+hello.exe:
+    csig: None
+hello.obj:
+    csig: None
+""")
+
+test.run_sconsign(arguments = "-s -v .sconsign",
+         stdout = """\
+=== sub1:
+hello.exe:
+    size: \d+
+hello.obj:
+    size: \d+
+=== sub2:
+hello.exe:
+    size: \d+
+hello.obj:
+    size: \d+
+""")
+
+test.run_sconsign(arguments = "-t -v .sconsign",
+         stdout = """\
+=== sub1:
+hello.exe:
+    timestamp: \d+
+hello.obj:
+    timestamp: \d+
+=== sub2:
+hello.exe:
+    timestamp: \d+
+hello.obj:
+    timestamp: \d+
+""")
+
+test.run_sconsign(arguments = "-e hello.obj .sconsign",
+         stdout = """\
+=== sub1:
+hello.obj: \S+ None \d+ \d+
+        hello.c: \S+
+=== sub2:
+hello.obj: \S+ None \d+ \d+
+        hello.c: \S+
+        inc1.h: \S+
+        inc2.h: \S+
+""")
+
+test.run_sconsign(arguments = "-e hello.obj -e hello.exe -e hello.obj .sconsign",
+         stdout = """\
+=== sub1:
+hello.obj: \S+ None \d+ \d+
+        hello.c: \S+
+hello.exe: \S+ None \d+ \d+
+        hello.obj: \S+
+hello.obj: \S+ None \d+ \d+
+        hello.c: \S+
+=== sub2:
+hello.obj: \S+ None \d+ \d+
+        hello.c: \S+
+        inc1.h: \S+
+        inc2.h: \S+
+hello.exe: \S+ None \d+ \d+
+        hello.obj: \S+
+hello.obj: \S+ None \d+ \d+
+        hello.c: \S+
+        inc1.h: \S+
+        inc2.h: \S+
+""")
+
+#test.run_sconsign(arguments = "-i -v .sconsign",
+#         stdout = """\
+#=== sub1:
+#hello.exe:
+#    implicit:
+#        hello.obj: \S+
+#hello.obj:
+#    implicit:
+#        hello.c: \S+
+#=== sub2:
+#hello.exe:
+#    implicit:
+#        hello.obj: \S+
+#hello.obj:
+#    implicit:
+#        hello.c: \S+
+#        inc1.h: \S+
+#        inc2.h: \S+
+#""")
+
+test.pass_test()
diff --git a/test/sconsign/script/Signatures.py b/test/sconsign/script/Signatures.py
new file mode 100644 (file)
index 0000000..9a3ce62
--- /dev/null
@@ -0,0 +1,109 @@
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+"""
+Verify that the sconsign script works when using a .sconsign file in
+each subdirectory (SConsignFile(None)) written with the non-default
+SourceSignatures() and TargetSignatures() values (timestamp and content,
+respectively).
+"""
+
+import TestSConsign
+
+test = TestSConsign.TestSConsign(match = TestSConsign.match_re)
+
+def re_sep(*args):
+    import os.path
+    import re
+    return re.escape(apply(os.path.join, args))
+
+test.subdir('sub1', 'sub2')
+
+test.write('SConstruct', """
+SConsignFile(None)
+SourceSignatures('timestamp')
+TargetSignatures('content')
+env1 = Environment(PROGSUFFIX = '.exe', OBJSUFFIX = '.obj')
+env1.Program('sub1/hello.c')
+env2 = env1.Clone(CPPPATH = ['sub2'])
+env2.Program('sub2/hello.c')
+""")
+
+test.write(['sub1', 'hello.c'], r"""\
+#include <stdio.h>
+#include <stdlib.h>
+int
+main(int argc, char *argv[])
+{
+        argv[argc++] = "--";
+        printf("sub1/hello.c\n");
+        exit (0);
+}
+""")
+
+test.write(['sub2', 'hello.c'], r"""\
+#include <stdio.h>
+#include <stdlib.h>
+#include <inc1.h>
+#include <inc2.h>
+int
+main(int argc, char *argv[])
+{
+        argv[argc++] = "--";
+        printf("sub2/goodbye.c\n");
+        exit (0);
+}
+""")
+
+test.write(['sub2', 'inc1.h'], r"""\
+#define STRING1 "inc1.h"
+""")
+
+test.write(['sub2', 'inc2.h'], r"""\
+#define STRING2 "inc2.h"
+""")
+
+test.sleep()
+
+test.run(arguments = '. --max-drift=1')
+
+test.run_sconsign(arguments = "-e hello.exe -e hello.obj sub1/.sconsign",
+         stdout = """\
+hello.exe: \S+ None \d+ \d+
+        hello.obj: \S+
+hello.obj: \S+ None \d+ \d+
+        hello.c: \S+
+""")
+
+test.run_sconsign(arguments = "-e hello.exe -e hello.obj -r sub1/.sconsign",
+         stdout = """\
+hello.exe: \S+ None '\S+ \S+ [ \d]\d \d\d:\d\d:\d\d \d\d\d\d' \d+
+        hello.obj: \S+
+hello.obj: \S+ None '\S+ \S+ [ \d]\d \d\d:\d\d:\d\d \d\d\d\d' \d+
+        hello.c: \S+
+""")
+
+test.pass_test()
diff --git a/test/sconsign/script/bad.py b/test/sconsign/script/bad.py
new file mode 100644 (file)
index 0000000..3ada44c
--- /dev/null
@@ -0,0 +1,61 @@
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+"""
+Verify that the sconsign script generates appropriate error messages when
+passed various non-existent or bad sconsign files as arguments.
+"""
+
+import TestSConsign
+
+test = TestSConsign.TestSConsign(match = TestSConsign.match_re)
+
+test.write('bad1', "bad1\n")
+test.write('bad2.dblite', "bad2.dblite\n")
+test.write('bad3', "bad3\n")
+
+test.run_sconsign(arguments = "-f dblite no_sconsign",
+         stderr = "sconsign: \[Errno 2\] No such file or directory: 'no_sconsign'\n")
+
+test.run_sconsign(arguments = "-f dblite bad1",
+         stderr = "sconsign: \[Errno 2\] No such file or directory: 'bad1.dblite'\n")
+
+test.run_sconsign(arguments = "-f dblite bad1.dblite",
+         stderr = "sconsign: \[Errno 2\] No such file or directory: 'bad1.dblite'\n")
+
+test.run_sconsign(arguments = "-f dblite bad2",
+         stderr = "sconsign: ignoring invalid `dblite' file `bad2'\n")
+
+test.run_sconsign(arguments = "-f dblite bad2.dblite",
+         stderr = "sconsign: ignoring invalid `dblite' file `bad2.dblite'\n")
+
+test.run_sconsign(arguments = "-f sconsign no_sconsign",
+         stderr = "sconsign: \[Errno 2\] No such file or directory: 'no_sconsign'\n")
+
+test.run_sconsign(arguments = "-f sconsign bad3",
+         stderr = "sconsign: ignoring invalid .sconsign file `bad3'\n")
+
+test.pass_test()
diff --git a/test/sconsign/script/dblite.py b/test/sconsign/script/dblite.py
new file mode 100644 (file)
index 0000000..fe49df6
--- /dev/null
@@ -0,0 +1,122 @@
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+"""
+Verify that various ways of getting at a an sconsign file written with
+the default dblite module and default .dblite suffix work correctly.
+"""
+
+import TestSConsign
+
+test = TestSConsign.TestSConsign(match = TestSConsign.match_re)
+
+test.subdir('sub1', 'sub2')
+
+test.write('SConstruct', """
+SConsignFile('my_sconsign')
+SourceSignatures('timestamp')
+TargetSignatures('content')
+env1 = Environment(PROGSUFFIX = '.exe', OBJSUFFIX = '.obj')
+env1.Program('sub1/hello.c')
+env2 = env1.Clone(CPPPATH = ['sub2'])
+env2.Program('sub2/hello.c')
+""")
+
+test.write(['sub1', 'hello.c'], r"""\
+#include <stdio.h>
+#include <stdlib.h>
+int
+main(int argc, char *argv[])
+{
+        argv[argc++] = "--";
+        printf("sub1/hello.c\n");
+        exit (0);
+}
+""")
+
+test.write(['sub2', 'hello.c'], r"""\
+#include <stdio.h>
+#include <stdlib.h>
+#include <inc1.h>
+#include <inc2.h>
+int
+main(int argc, char *argv[])
+{
+        argv[argc++] = "--";
+        printf("sub2/goodbye.c\n");
+        exit (0);
+}
+""")
+
+test.write(['sub2', 'inc1.h'], r"""\
+#define STRING1 "inc1.h"
+""")
+
+test.write(['sub2', 'inc2.h'], r"""\
+#define STRING2 "inc2.h"
+""")
+
+test.sleep()
+
+test.run(arguments = '. --max-drift=1')
+
+expect = """\
+=== sub1:
+hello.exe: \d+ None \d+ \d+
+        hello.obj: \d+
+hello.obj: \d+ None \d+ \d+
+        hello.c: \d+
+"""
+
+expect_r = """\
+=== sub1:
+hello.exe: \d+ None '\S+ \S+ [ \d]\d \d\d:\d\d:\d\d \d\d\d\d' \d+
+        hello.obj: \d+
+hello.obj: \d+ None '\S+ \S+ [ \d]\d \d\d:\d\d:\d\d \d\d\d\d' \d+
+        hello.c: \d+
+"""
+
+common_flags = '-e hello.exe -e hello.obj -d sub1'
+
+test.run_sconsign(arguments = "%s my_sconsign" % common_flags,
+                  stdout = expect)
+
+test.run_sconsign(arguments = "%s my_sconsign.dblite" % common_flags,
+                  stdout = expect)
+
+test.run_sconsign(arguments = "%s -f dblite my_sconsign" % common_flags,
+                  stdout = expect)
+
+test.run_sconsign(arguments = "%s -f dblite my_sconsign.dblite" % common_flags,
+                  stdout = expect)
+
+test.run_sconsign(arguments = "%s -r -f dblite my_sconsign" % common_flags,
+                  stdout = expect_r)
+
+test.run_sconsign(arguments = "%s -r -f dblite my_sconsign.dblite" % common_flags,
+                  stdout = expect_r)
+
+test.pass_test()
diff --git a/test/sconsign/script/no-SConsignFile.py b/test/sconsign/script/no-SConsignFile.py
new file mode 100644 (file)
index 0000000..b860770
--- /dev/null
@@ -0,0 +1,205 @@
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+"""
+Verify that the sconsign script works when using an individual
+.sconsign file in each directory (SConsignFile(None)).
+"""
+
+import TestSConsign
+
+test = TestSConsign.TestSConsign(match = TestSConsign.match_re)
+
+def re_sep(*args):
+    import os.path
+    import re
+    return re.escape(apply(os.path.join, args))
+
+test.subdir('sub1', 'sub2')
+
+test.write(['SConstruct'], """
+SConsignFile(None)
+env1 = Environment(PROGSUFFIX = '.exe', OBJSUFFIX = '.obj')
+env1.Program('sub1/hello.c')
+env2 = env1.Clone(CPPPATH = ['sub2'])
+env2.Program('sub2/hello.c')
+""")
+
+test.write(['sub1', 'hello.c'], r"""\
+#include <stdio.h>
+#include <stdlib.h>
+int
+main(int argc, char *argv[])
+{
+        argv[argc++] = "--";
+        printf("sub1/hello.c\n");
+        exit (0);
+}
+""")
+
+test.write(['sub2', 'hello.c'], r"""\
+#include <stdio.h>
+#include <stdlib.h>
+#include <inc1.h>
+#include <inc2.h>
+int
+main(int argc, char *argv[])
+{
+        argv[argc++] = "--";
+        printf("sub2/goodbye.c\n");
+        exit (0);
+}
+""")
+
+test.write(['sub2', 'inc1.h'], r"""\
+#define STRING1 "inc1.h"
+""")
+
+test.write(['sub2', 'inc2.h'], r"""\
+#define STRING2 "inc2.h"
+""")
+
+test.run(arguments = '--implicit-cache .')
+
+test.run_sconsign(arguments = "sub1/.sconsign",
+         stdout = """\
+hello.exe: \S+ None \d+ \d+
+        hello.obj: \S+
+hello.obj: \S+ None \d+ \d+
+        hello.c: \S+
+""")
+
+test.run_sconsign(arguments = "--raw sub1/.sconsign",
+         stdout = """\
+hello.exe: {'bsig': '\S+', 'size': \d+L?, 'timestamp': \d+}
+        hello.obj: \S+
+hello.obj: {'bsig': '\S+', 'size': \d+L?, 'timestamp': \d+}
+        hello.c: \S+
+""")
+
+test.run_sconsign(arguments = "-v sub1/.sconsign",
+         stdout = """\
+hello.exe:
+    bsig: \S+
+    csig: None
+    timestamp: \d+
+    size: \d+
+    implicit:
+        hello.obj: \S+
+hello.obj:
+    bsig: \S+
+    csig: None
+    timestamp: \d+
+    size: \d+
+    implicit:
+        hello.c: \S+
+""")
+
+test.run_sconsign(arguments = "-b -v sub1/.sconsign",
+         stdout = """\
+hello.exe:
+    bsig: \S+
+hello.obj:
+    bsig: \S+
+""")
+
+test.run_sconsign(arguments = "-c -v sub1/.sconsign",
+         stdout = """\
+hello.exe:
+    csig: None
+hello.obj:
+    csig: None
+""")
+
+test.run_sconsign(arguments = "-s -v sub1/.sconsign",
+         stdout = """\
+hello.exe:
+    size: \d+
+hello.obj:
+    size: \d+
+""")
+
+test.run_sconsign(arguments = "-t -v sub1/.sconsign",
+         stdout = """\
+hello.exe:
+    timestamp: \d+
+hello.obj:
+    timestamp: \d+
+""")
+
+test.run_sconsign(arguments = "-e hello.obj sub1/.sconsign",
+         stdout = """\
+hello.obj: \S+ None \d+ \d+
+        hello.c: \S+
+""")
+
+test.run_sconsign(arguments = "-e hello.obj -e hello.exe -e hello.obj sub1/.sconsign",
+         stdout = """\
+hello.obj: \S+ None \d+ \d+
+        hello.c: \S+
+hello.exe: \S+ None \d+ \d+
+        hello.obj: \S+
+hello.obj: \S+ None \d+ \d+
+        hello.c: \S+
+""")
+
+# XXX NOT SURE IF THIS IS RIGHT!
+sub2_inc1_h = re_sep('sub2', 'inc1.h')
+sub2_inc2_h = re_sep('sub2', 'inc2.h')
+
+test.run_sconsign(arguments = "sub2/.sconsign",
+         stdout = """\
+hello.exe: \S+ None \d+ \d+
+        hello.obj: \S+
+hello.obj: \S+ None \d+ \d+
+        hello.c: \S+
+        inc1.h: \S+
+        inc2.h: \S+
+""")
+
+#test.run_sconsign(arguments = "-i -v sub2/.sconsign",
+#         stdout = """\
+#hello.exe:
+#    implicit:
+#        hello.obj: \S+ None \d+ \d+
+#hello.obj:
+#    implicit:
+#        hello.c: None \S+ \d+ \d+
+#        inc1.h: None \S+ \d+ \d+
+#        inc2.h: None \S+ \d+ \d+
+#""")
+
+test.run_sconsign(arguments = "-e hello.obj sub2/.sconsign sub1/.sconsign",
+         stdout = """\
+hello.obj: \S+ None \d+ \d+
+        hello.c: \S+
+        inc1.h: \S+
+        inc2.h: \S+
+hello.obj: \S+ None \d+ \d+
+        hello.c: \S+
+""")
+
+test.pass_test()
index eee0e4d38306ab3f9adc5666290c838534c79ae7..6394e0a57902b0b6625b96a120c1e7b1ec3c3d71 100644 (file)
@@ -50,10 +50,14 @@ Program('hello', ['obj/subdir/main.c'])
 """)
 
 test.write(['srcdir', 'main.c'], r"""
+#include <stdio.h>
+#include <stdlib.h>
+
 int
 main(int ac, char *argv[])
 {
     printf("srcdir/main.c\n");
+    exit(0);
 }
 """)