summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSteven Knight <knight@baldmt.com>2006-12-16 01:43:01 (GMT)
committerSteven Knight <knight@baldmt.com>2006-12-16 01:43:01 (GMT)
commita4fa510f9025ccb4ffeddefe26ce17e01c6a2b8f (patch)
tree8a0d07c078ac21bf1ab689eacf06577069bb9231
parentcb98bc6c10eb4cc1d29babc180d1dbc74fafe508 (diff)
downloadSCons-a4fa510f9025ccb4ffeddefe26ce17e01c6a2b8f.zip
SCons-a4fa510f9025ccb4ffeddefe26ce17e01c6a2b8f.tar.gz
SCons-a4fa510f9025ccb4ffeddefe26ce17e01c6a2b8f.tar.bz2
Merged revisions 1675-1736 via svnmerge from
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. ........
-rw-r--r--HOWTO/release.txt556
-rw-r--r--HOWTO/subrelease.txt124
-rw-r--r--QMTest/SConscript2
-rw-r--r--QMTest/TestCmd.py43
-rw-r--r--QMTest/TestCommon.py4
-rw-r--r--QMTest/TestSCons.py24
-rw-r--r--QMTest/TestSCons_time.py365
-rw-r--r--QMTest/TestSConsign.py74
-rw-r--r--README44
-rw-r--r--SConstruct11
-rwxr-xr-xbin/ae-cvs-ci22
-rwxr-xr-xbin/ae-svn-ci22
-rw-r--r--debian/changelog4
-rw-r--r--doc/SConscript2
-rw-r--r--doc/man/scons-time.11005
-rw-r--r--doc/man/scons.184
-rw-r--r--rpm/scons.spec.in3
-rw-r--r--src/CHANGES.txt120
-rw-r--r--src/RELEASE.txt16
-rw-r--r--src/engine/MANIFEST.in1
-rw-r--r--src/engine/SCons/Action.py23
-rw-r--r--src/engine/SCons/Builder.py109
-rw-r--r--src/engine/SCons/BuilderTests.py20
-rw-r--r--src/engine/SCons/Defaults.py8
-rw-r--r--src/engine/SCons/Environment.py114
-rw-r--r--src/engine/SCons/EnvironmentTests.py13
-rw-r--r--src/engine/SCons/Executor.py88
-rw-r--r--src/engine/SCons/JobTests.py14
-rw-r--r--src/engine/SCons/Memoize.py948
-rw-r--r--src/engine/SCons/MemoizeTests.py192
-rw-r--r--src/engine/SCons/Node/FS.py554
-rw-r--r--src/engine/SCons/Node/FSTests.py83
-rw-r--r--src/engine/SCons/Node/__init__.py54
-rw-r--r--src/engine/SCons/Options/__init__.py5
-rw-r--r--src/engine/SCons/PathList.py217
-rw-r--r--src/engine/SCons/PathListTests.py145
-rw-r--r--src/engine/SCons/Scanner/CTests.py15
-rw-r--r--src/engine/SCons/Scanner/D.py1
-rw-r--r--src/engine/SCons/Scanner/Fortran.py3
-rw-r--r--src/engine/SCons/Scanner/FortranTests.py13
-rw-r--r--src/engine/SCons/Scanner/IDLTests.py13
-rw-r--r--src/engine/SCons/Scanner/LaTeX.py72
-rw-r--r--src/engine/SCons/Scanner/LaTeXTests.py6
-rw-r--r--src/engine/SCons/Scanner/Prog.py5
-rw-r--r--src/engine/SCons/Scanner/ProgTests.py10
-rw-r--r--src/engine/SCons/Scanner/ScannerTests.py29
-rw-r--r--src/engine/SCons/Scanner/__init__.py54
-rw-r--r--src/engine/SCons/Script/Main.py36
-rw-r--r--src/engine/SCons/Script/SConscript.py10
-rw-r--r--src/engine/SCons/Script/__init__.py40
-rw-r--r--src/engine/SCons/Subst.py54
-rw-r--r--src/engine/SCons/SubstTests.py65
-rw-r--r--src/engine/SCons/Taskmaster.py17
-rw-r--r--src/engine/SCons/TaskmasterTests.py2
-rw-r--r--src/engine/SCons/Tool/386asm.py4
-rw-r--r--src/engine/SCons/Tool/dvipdf.py2
-rw-r--r--src/engine/SCons/Tool/dvips.py4
-rw-r--r--src/engine/SCons/Tool/gas.py4
-rw-r--r--src/engine/SCons/Tool/latex.py2
-rw-r--r--src/engine/SCons/Tool/mslink.py8
-rw-r--r--src/engine/SCons/Tool/msvc.xml2
-rw-r--r--src/engine/SCons/Tool/pdflatex.py2
-rw-r--r--src/engine/SCons/Tool/pdftex.py8
-rw-r--r--src/engine/SCons/Tool/swig.py11
-rw-r--r--src/engine/SCons/Tool/tex.py42
-rw-r--r--src/engine/SCons/Warnings.py3
-rw-r--r--src/script/MANIFEST.in1
-rw-r--r--src/script/scons-time.py1437
-rw-r--r--src/script/scons.py2
-rw-r--r--src/script/sconsign.py4
-rw-r--r--src/setup.py13
-rw-r--r--src/test_copyrights.py3
-rw-r--r--test/Alias/srcdir.py100
-rw-r--r--test/BadBuilder.py11
-rw-r--r--test/BuildDir/errors.py3
-rw-r--r--test/Builder/srcdir.py76
-rw-r--r--test/Errors/AttributeError.py47
-rw-r--r--test/Errors/Exception.py83
-rw-r--r--test/Errors/InternalError.py53
-rw-r--r--test/Errors/NameError.py47
-rw-r--r--test/Errors/SyntaxError.py51
-rw-r--r--test/Errors/TypeError.py47
-rw-r--r--test/Errors/UserError.py50
-rw-r--r--test/Errors/exit-status.py54
-rw-r--r--test/Options/Options.py28
-rw-r--r--test/SConscript/src_dir.py2
-rw-r--r--test/SWIG/SWIG.py2
-rw-r--r--test/Scanner/generated.py2
-rw-r--r--test/SideEffect.py211
-rw-r--r--test/SideEffect/basic.py108
-rw-r--r--test/SideEffect/build_dir.py77
-rw-r--r--test/SideEffect/directory.py76
-rw-r--r--test/SideEffect/parallel.py126
-rw-r--r--test/Subst/AllowSubstExceptions.py93
-rw-r--r--test/Subst/SyntaxError.py74
-rw-r--r--test/Subst/TypeError.py92
-rw-r--r--test/TEX/TEX.py16
-rw-r--r--test/TEX/build_dir.py256
-rw-r--r--test/TEX/subdir-input.py80
-rw-r--r--test/errors.py224
-rw-r--r--test/import.py106
-rw-r--r--test/option-c.py21
-rw-r--r--test/option-v.py29
-rw-r--r--test/option/debug-memoizer.py80
-rw-r--r--test/option/debug-nomemoizer.py34
-rw-r--r--test/option/profile.py6
-rw-r--r--test/scons-time/func/basic.py49
-rw-r--r--test/scons-time/func/chdir.py64
-rw-r--r--test/scons-time/func/file.py80
-rw-r--r--test/scons-time/func/format-gnuplot.py82
-rw-r--r--test/scons-time/func/function.py58
-rw-r--r--test/scons-time/func/glob.py57
-rw-r--r--test/scons-time/func/help.py57
-rw-r--r--test/scons-time/func/no-args.py43
-rw-r--r--test/scons-time/func/prefix.py65
-rw-r--r--test/scons-time/func/tail.py56
-rw-r--r--test/scons-time/help/all-subcommands.py58
-rw-r--r--test/scons-time/help/options.py59
-rw-r--r--test/scons-time/mem/chdir.py57
-rw-r--r--test/scons-time/mem/file.py71
-rw-r--r--test/scons-time/mem/format-gnuplot.py72
-rw-r--r--test/scons-time/mem/glob.py52
-rw-r--r--test/scons-time/mem/help.py57
-rw-r--r--test/scons-time/mem/no-args.py43
-rw-r--r--test/scons-time/mem/prefix.py62
-rw-r--r--test/scons-time/mem/stage.py83
-rw-r--r--test/scons-time/mem/tail.py55
-rw-r--r--test/scons-time/no-args.py42
-rw-r--r--test/scons-time/obj/chdir.py57
-rw-r--r--test/scons-time/obj/file.py71
-rw-r--r--test/scons-time/obj/format-gnuplot.py76
-rw-r--r--test/scons-time/obj/glob.py52
-rw-r--r--test/scons-time/obj/help.py57
-rw-r--r--test/scons-time/obj/no-args.py42
-rw-r--r--test/scons-time/obj/no-files.py44
-rw-r--r--test/scons-time/obj/prefix.py62
-rw-r--r--test/scons-time/obj/stage.py88
-rw-r--r--test/scons-time/obj/tail.py55
-rw-r--r--test/scons-time/run/aegis.py84
-rw-r--r--test/scons-time/run/archive/dir.py49
-rw-r--r--test/scons-time/run/archive/tar-gz.py49
-rw-r--r--test/scons-time/run/archive/tar.py49
-rw-r--r--test/scons-time/run/archive/tgz.py49
-rw-r--r--test/scons-time/run/archive/zip.py49
-rw-r--r--test/scons-time/run/config/archive_list.py73
-rw-r--r--test/scons-time/run/config/initial_commands.py56
-rw-r--r--test/scons-time/run/config/prefix.py52
-rw-r--r--test/scons-time/run/config/python.py68
-rw-r--r--test/scons-time/run/config/scons.py65
-rw-r--r--test/scons-time/run/config/subdir.py63
-rw-r--r--test/scons-time/run/config/targets.py86
-rw-r--r--test/scons-time/run/option/help.py59
-rw-r--r--test/scons-time/run/option/next-run.py99
-rw-r--r--test/scons-time/run/option/no-args.py42
-rw-r--r--test/scons-time/run/option/no-exec.py49
-rw-r--r--test/scons-time/run/option/number.py48
-rw-r--r--test/scons-time/run/option/outdir.py51
-rw-r--r--test/scons-time/run/option/prefix.py48
-rw-r--r--test/scons-time/run/option/python.py64
-rw-r--r--test/scons-time/run/option/quiet.py96
-rw-r--r--test/scons-time/run/option/scons.py59
-rw-r--r--test/scons-time/run/option/subdir.py59
-rw-r--r--test/scons-time/run/option/verbose.py161
-rw-r--r--test/scons-time/run/subversion.py85
-rw-r--r--test/scons-time/time/chdir.py57
-rw-r--r--test/scons-time/time/file.py71
-rw-r--r--test/scons-time/time/format-gnuplot.py72
-rw-r--r--test/scons-time/time/glob.py52
-rw-r--r--test/scons-time/time/help.py57
-rw-r--r--test/scons-time/time/no-args.py43
-rw-r--r--test/scons-time/time/prefix.py62
-rw-r--r--test/scons-time/time/tail.py55
-rw-r--r--test/scons-time/time/which.py84
-rw-r--r--test/scons-time/unknown.py45
-rw-r--r--test/sconsign/script.py602
-rw-r--r--test/sconsign/script/SConsignFile.py260
-rw-r--r--test/sconsign/script/Signatures.py109
-rw-r--r--test/sconsign/script/bad.py61
-rw-r--r--test/sconsign/script/dblite.py122
-rw-r--r--test/sconsign/script/no-SConsignFile.py205
-rw-r--r--test/symlink/BuildDir.py4
181 files changed, 12200 insertions, 2874 deletions
diff --git a/HOWTO/release.txt b/HOWTO/release.txt
index 88f9352..57f38b7 100644
--- a/HOWTO/release.txt
+++ b/HOWTO/release.txt
@@ -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
diff --git a/HOWTO/subrelease.txt b/HOWTO/subrelease.txt
index 6826c81..fbba52a 100644
--- a/HOWTO/subrelease.txt
+++ b/HOWTO/subrelease.txt
@@ -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.
diff --git a/QMTest/SConscript b/QMTest/SConscript
index e016dc4..309f0dc 100644
--- a/QMTest/SConscript
+++ b/QMTest/SConscript
@@ -37,6 +37,8 @@ files = [
'TestCommon.py',
'TestRuntest.py',
'TestSCons.py',
+ 'TestSConsign.py',
+ 'TestSCons_time.py',
'unittest.py',
]
diff --git a/QMTest/TestCmd.py b/QMTest/TestCmd.py
index 9b3e7a2..7a668e8 100644
--- a/QMTest/TestCmd.py
+++ b/QMTest/TestCmd.py
@@ -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')
diff --git a/QMTest/TestCommon.py b/QMTest/TestCommon.py
index b30b75c..b215a26 100644
--- a/QMTest/TestCommon.py
+++ b/QMTest/TestCommon.py
@@ -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
diff --git a/QMTest/TestSCons.py b/QMTest/TestSCons.py
index de15526..091c29f 100644
--- a/QMTest/TestSCons.py
+++ b/QMTest/TestSCons.py
@@ -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
index 0000000..90eb5a8
--- /dev/null
+++ b/QMTest/TestSCons_time.py
@@ -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
index 0000000..d144040
--- /dev/null
+++ b/QMTest/TestSConsign.py
@@ -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 c0c013e..7d5c84b 100644
--- 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.
diff --git a/SConstruct b/SConstruct
index 41708d6..811ed4a 100644
--- a/SConstruct
+++ b/SConstruct
@@ -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 ],
diff --git a/bin/ae-cvs-ci b/bin/ae-cvs-ci
index 3dcc287..47c8073 100755
--- a/bin/ae-cvs-ci
+++ b/bin/ae-cvs-ci
@@ -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
@@ -177,9 +177,23 @@ do
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\"
#
diff --git a/bin/ae-svn-ci b/bin/ae-svn-ci
index e5b81a4..301d890 100755
--- a/bin/ae-svn-ci
+++ b/bin/ae-svn-ci
@@ -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
@@ -213,9 +213,23 @@ do
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\"
#
diff --git a/debian/changelog b/debian/changelog
index f205e25..dc5945b 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -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
diff --git a/doc/SConscript b/doc/SConscript
index b0a4ba1..43d3403 100644
--- a/doc/SConscript
+++ b/doc/SConscript
@@ -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
index 0000000..50f490f
--- /dev/null
+++ b/doc/man/scons-time.1
@@ -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>
diff --git a/doc/man/scons.1 b/doc/man/scons.1
index 1b30e6a..d45333b 100644
--- a/doc/man/scons.1
+++ b/doc/man/scons.1
@@ -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
@@ -1816,6 +1827,41 @@ env.Alias('update', ['file1', 'file2'], "update_database $SOURCES")
'\"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
.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 ", ...)"
.TP
.RI env.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,
diff --git a/rpm/scons.spec.in b/rpm/scons.spec.in
index 92330d5..50bae78 100644
--- a/rpm/scons.spec.in
+++ b/rpm/scons.spec.in
@@ -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*
diff --git a/src/CHANGES.txt b/src/CHANGES.txt
index 778825e..e7f4937 100644
--- a/src/CHANGES.txt
+++ b/src/CHANGES.txt
@@ -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.
diff --git a/src/RELEASE.txt b/src/RELEASE.txt
index 0c5f587..e84a079 100644
--- a/src/RELEASE.txt
+++ b/src/RELEASE.txt
@@ -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
diff --git a/src/engine/MANIFEST.in b/src/engine/MANIFEST.in
index c762f1c..7be1c16 100644
--- a/src/engine/MANIFEST.in
+++ b/src/engine/MANIFEST.in
@@ -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
diff --git a/src/engine/SCons/Action.py b/src/engine/SCons/Action.py
index 4576164..503dc9f 100644
--- a/src/engine/SCons/Action.py
+++ b/src/engine/SCons/Action.py
@@ -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):
diff --git a/src/engine/SCons/Builder.py b/src/engine/SCons/Builder.py
index 16f1191..d5f566a 100644
--- a/src/engine/SCons/Builder.py
+++ b/src/engine/SCons/Builder.py
@@ -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):
diff --git a/src/engine/SCons/BuilderTests.py b/src/engine/SCons/BuilderTests.py
index c5b428c..4e196e2 100644
--- a/src/engine/SCons/BuilderTests.py
+++ b/src/engine/SCons/BuilderTests.py
@@ -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:
diff --git a/src/engine/SCons/Defaults.py b/src/engine/SCons/Defaults.py
index 7513c0d..96c3cf8 100644
--- a/src/engine/SCons/Defaults.py
+++ b/src/engine/SCons/Defaults.py
@@ -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 = []
diff --git a/src/engine/SCons/Environment.py b/src/engine/SCons/Environment.py
index a5bffc8..4761ea0 100644
--- a/src/engine/SCons/Environment.py
+++ b/src/engine/SCons/Environment.py
@@ -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
-
diff --git a/src/engine/SCons/EnvironmentTests.py b/src/engine/SCons/EnvironmentTests.py
index edf3740..f0f73da 100644
--- a/src/engine/SCons/EnvironmentTests.py
+++ b/src/engine/SCons/EnvironmentTests.py
@@ -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
diff --git a/src/engine/SCons/Executor.py b/src/engine/SCons/Executor.py
index ffc1ba3..4b15010 100644
--- a/src/engine/SCons/Executor.py
+++ b/src/engine/SCons/Executor.py
@@ -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)
-
diff --git a/src/engine/SCons/JobTests.py b/src/engine/SCons/JobTests.py
index d2c019f..e38e251 100644
--- a/src/engine/SCons/JobTests.py
+++ b/src/engine/SCons/JobTests.py
@@ -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
diff --git a/src/engine/SCons/Memoize.py b/src/engine/SCons/Memoize.py
index c2a3027..6a46350 100644
--- a/src/engine/SCons/Memoize.py
+++ b/src/engine/SCons/Memoize.py
@@ -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
index 0000000..7102f30
--- /dev/null
+++ b/src/engine/SCons/MemoizeTests.py
@@ -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)
diff --git a/src/engine/SCons/Node/FS.py b/src/engine/SCons/Node/FS.py
index 382bca3..08b8d7d 100644
--- a/src/engine/SCons/Node/FS.py
+++ b/src/engine/SCons/Node/FS.py
@@ -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
diff --git a/src/engine/SCons/Node/FSTests.py b/src/engine/SCons/Node/FSTests.py
index 1b38ffe..434709c 100644
--- a/src/engine/SCons/Node/FSTests.py
+++ b/src/engine/SCons/Node/FSTests.py
@@ -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."""
diff --git a/src/engine/SCons/Node/__init__.py b/src/engine/SCons/Node/__init__.py
index 42be5b1..e5d064e 100644
--- a/src/engine/SCons/Node/__init__.py
+++ b/src/engine/SCons/Node/__init__.py
@@ -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
diff --git a/src/engine/SCons/Options/__init__.py b/src/engine/SCons/Options/__init__.py
index 83798b3..5c30be6 100644
--- a/src/engine/SCons/Options/__init__.py
+++ b/src/engine/SCons/Options/__init__.py
@@ -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
index 0000000..b757bd3
--- /dev/null
+++ b/src/engine/SCons/PathList.py
@@ -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
index 0000000..d6fae0e
--- /dev/null
+++ b/src/engine/SCons/PathListTests.py
@@ -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)
diff --git a/src/engine/SCons/Scanner/CTests.py b/src/engine/SCons/Scanner/CTests.py
index 2ca522b..5d6765d 100644
--- a/src/engine/SCons/Scanner/CTests.py
+++ b/src/engine/SCons/Scanner/CTests.py
@@ -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)
diff --git a/src/engine/SCons/Scanner/D.py b/src/engine/SCons/Scanner/D.py
index 2ea2614..5a0b383 100644
--- a/src/engine/SCons/Scanner/D.py
+++ b/src/engine/SCons/Scanner/D.py
@@ -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, '.', '/')
diff --git a/src/engine/SCons/Scanner/Fortran.py b/src/engine/SCons/Scanner/Fortran.py
index 8f7a6ce..31a1e16 100644
--- a/src/engine/SCons/Scanner/Fortran.py
+++ b/src/engine/SCons/Scanner/Fortran.py
@@ -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)
diff --git a/src/engine/SCons/Scanner/FortranTests.py b/src/engine/SCons/Scanner/FortranTests.py
index da4a023..82db694 100644
--- a/src/engine/SCons/Scanner/FortranTests.py
+++ b/src/engine/SCons/Scanner/FortranTests.py
@@ -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)
diff --git a/src/engine/SCons/Scanner/IDLTests.py b/src/engine/SCons/Scanner/IDLTests.py
index 153951d..2332a57 100644
--- a/src/engine/SCons/Scanner/IDLTests.py
+++ b/src/engine/SCons/Scanner/IDLTests.py
@@ -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)
diff --git a/src/engine/SCons/Scanner/LaTeX.py b/src/engine/SCons/Scanner/LaTeX.py
index 6451a58..d875e6e 100644
--- a/src/engine/SCons/Scanner/LaTeX.py
+++ b/src/engine/SCons/Scanner/LaTeX.py
@@ -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
diff --git a/src/engine/SCons/Scanner/LaTeXTests.py b/src/engine/SCons/Scanner/LaTeXTests.py
index 45a387a..9cfe3ca 100644
--- a/src/engine/SCons/Scanner/LaTeXTests.py
+++ b/src/engine/SCons/Scanner/LaTeXTests.py
@@ -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)
diff --git a/src/engine/SCons/Scanner/Prog.py b/src/engine/SCons/Scanner/Prog.py
index 54db9a8..e8d1669 100644
--- a/src/engine/SCons/Scanner/Prog.py
+++ b/src/engine/SCons/Scanner/Prog.py
@@ -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:
diff --git a/src/engine/SCons/Scanner/ProgTests.py b/src/engine/SCons/Scanner/ProgTests.py
index bac10b7..47d4afd 100644
--- a/src/engine/SCons/Scanner/ProgTests.py
+++ b/src/engine/SCons/Scanner/ProgTests.py
@@ -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)
diff --git a/src/engine/SCons/Scanner/ScannerTests.py b/src/engine/SCons/Scanner/ScannerTests.py
index 29ca063..bd8546f 100644
--- a/src/engine/SCons/Scanner/ScannerTests.py
+++ b/src/engine/SCons/Scanner/ScannerTests.py
@@ -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
diff --git a/src/engine/SCons/Scanner/__init__.py b/src/engine/SCons/Scanner/__init__.py
index 1fd77e5..679efca 100644
--- a/src/engine/SCons/Scanner/__init__.py
+++ b/src/engine/SCons/Scanner/__init__.py
@@ -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)
diff --git a/src/engine/SCons/Script/Main.py b/src/engine/SCons/Script/Main.py
index 2c18112..6eedbab 100644
--- a/src/engine/SCons/Script/Main.py
+++ b/src/engine/SCons/Script/Main.py
@@ -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
diff --git a/src/engine/SCons/Script/SConscript.py b/src/engine/SCons/Script/SConscript.py
index dc896a0..749be6d 100644
--- a/src/engine/SCons/Script/SConscript.py
+++ b/src/engine/SCons/Script/SConscript.py
@@ -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))
diff --git a/src/engine/SCons/Script/__init__.py b/src/engine/SCons/Script/__init__.py
index ce96867..55797df 100644
--- a/src/engine/SCons/Script/__init__.py
+++ b/src/engine/SCons/Script/__init__.py
@@ -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
diff --git a/src/engine/SCons/Subst.py b/src/engine/SCons/Subst.py
index b100473..115f7db 100644
--- a/src/engine/SCons/Subst.py
+++ b/src/engine/SCons/Subst.py
@@ -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
diff --git a/src/engine/SCons/SubstTests.py b/src/engine/SCons/SubstTests.py
index 4de3348..e8419f1 100644
--- a/src/engine/SCons/SubstTests.py
+++ b/src/engine/SCons/SubstTests.py
@@ -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"
diff --git a/src/engine/SCons/Taskmaster.py b/src/engine/SCons/Taskmaster.py
index 7cdecf3..2ea3f0d 100644
--- a/src/engine/SCons/Taskmaster.py
+++ b/src/engine/SCons/Taskmaster.py
@@ -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
diff --git a/src/engine/SCons/TaskmasterTests.py b/src/engine/SCons/TaskmasterTests.py
index 8d71d71..4fefb9d 100644
--- a/src/engine/SCons/TaskmasterTests.py
+++ b/src/engine/SCons/TaskmasterTests.py
@@ -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()
diff --git a/src/engine/SCons/Tool/386asm.py b/src/engine/SCons/Tool/386asm.py
index f2a221b..1a59525 100644
--- a/src/engine/SCons/Tool/386asm.py
+++ b/src/engine/SCons/Tool/386asm.py
@@ -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('')
diff --git a/src/engine/SCons/Tool/dvipdf.py b/src/engine/SCons/Tool/dvipdf.py
index 51dfae1..179159e 100644
--- a/src/engine/SCons/Tool/dvipdf.py
+++ b/src/engine/SCons/Tool/dvipdf.py
@@ -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']
diff --git a/src/engine/SCons/Tool/dvips.py b/src/engine/SCons/Tool/dvips.py
index b987ea1..9996fc2 100644
--- a/src/engine/SCons/Tool/dvips.py
+++ b/src/engine/SCons/Tool/dvips.py
@@ -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'
diff --git a/src/engine/SCons/Tool/gas.py b/src/engine/SCons/Tool/gas.py
index 3b35424..e44a28d 100644
--- a/src/engine/SCons/Tool/gas.py
+++ b/src/engine/SCons/Tool/gas.py
@@ -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'
diff --git a/src/engine/SCons/Tool/latex.py b/src/engine/SCons/Tool/latex.py
index 72371b3..c4934c3 100644
--- a/src/engine/SCons/Tool/latex.py
+++ b/src/engine/SCons/Tool/latex.py
@@ -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):
diff --git a/src/engine/SCons/Tool/mslink.py b/src/engine/SCons/Tool/mslink.py
index 4458183..4b90e3d 100644
--- a/src/engine/SCons/Tool/mslink.py
+++ b/src/engine/SCons/Tool/mslink.py
@@ -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
diff --git a/src/engine/SCons/Tool/msvc.xml b/src/engine/SCons/Tool/msvc.xml
index 1c0f3fa..be155bd 100644
--- a/src/engine/SCons/Tool/msvc.xml
+++ b/src/engine/SCons/Tool/msvc.xml
@@ -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>
diff --git a/src/engine/SCons/Tool/pdflatex.py b/src/engine/SCons/Tool/pdflatex.py
index acf67b2..97420a8 100644
--- a/src/engine/SCons/Tool/pdflatex.py
+++ b/src/engine/SCons/Tool/pdflatex.py
@@ -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):
diff --git a/src/engine/SCons/Tool/pdftex.py b/src/engine/SCons/Tool/pdftex.py
index ddf5a23..e740fac 100644
--- a/src/engine/SCons/Tool/pdftex.py
+++ b/src/engine/SCons/Tool/pdftex.py
@@ -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')
diff --git a/src/engine/SCons/Tool/swig.py b/src/engine/SCons/Tool/swig.py
index ed066a7..a8e12a2 100644
--- a/src/engine/SCons/Tool/swig.py
+++ b/src/engine/SCons/Tool/swig.py
@@ -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:
diff --git a/src/engine/SCons/Tool/tex.py b/src/engine/SCons/Tool/tex.py
index d613958..0329667 100644
--- a/src/engine/SCons/Tool/tex.py
+++ b/src/engine/SCons/Tool/tex.py
@@ -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')
diff --git a/src/engine/SCons/Warnings.py b/src/engine/SCons/Warnings.py
index 123f36b..27614bf 100644
--- a/src/engine/SCons/Warnings.py
+++ b/src/engine/SCons/Warnings.py
@@ -54,6 +54,9 @@ class DuplicateEnvironmentWarning(Warning):
class MissingSConscriptWarning(Warning):
pass
+class NoMetaclassSupportWarning(Warning):
+ pass
+
class NoParallelSupportWarning(Warning):
pass
diff --git a/src/script/MANIFEST.in b/src/script/MANIFEST.in
index 9783f23..f324ed4 100644
--- a/src/script/MANIFEST.in
+++ b/src/script/MANIFEST.in
@@ -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
index 0000000..1867d44
--- /dev/null
+++ b/src/script/scons-time.py
@@ -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)
diff --git a/src/script/scons.py b/src/script/scons.py
index 4852d01..fbffe68 100644
--- a/src/script/scons.py
+++ b/src/script/scons.py
@@ -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__
diff --git a/src/script/sconsign.py b/src/script/sconsign.py
index 6f19d56..66f8887 100644
--- a/src/script/sconsign.py
+++ b/src/script/sconsign.py
@@ -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()
diff --git a/src/setup.py b/src/setup.py
index 68b46ab..f06e717 100644
--- a/src/setup.py
+++ b/src/setup.py
@@ -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,
diff --git a/src/test_copyrights.py b/src/test_copyrights.py
index 3f7d1c9..ce78a8b 100644
--- a/src/test_copyrights.py
+++ b/src/test_copyrights.py
@@ -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
index 0000000..2251165
--- /dev/null
+++ b/test/Alias/srcdir.py
@@ -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()
diff --git a/test/BadBuilder.py b/test/BadBuilder.py
index 8c28ea5..c7c9c95 100644
--- a/test/BadBuilder.py
+++ b/test/BadBuilder.py
@@ -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)
diff --git a/test/BuildDir/errors.py b/test/BuildDir/errors.py
index 4fcd625..44096a1 100644
--- a/test/BuildDir/errors.py
+++ b/test/BuildDir/errors.py
@@ -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
index 0000000..4f378a7
--- /dev/null
+++ b/test/Builder/srcdir.py
@@ -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
index 0000000..637cccf
--- /dev/null
+++ b/test/Errors/AttributeError.py
@@ -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
index 0000000..710c819
--- /dev/null
+++ b/test/Errors/Exception.py
@@ -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
index 0000000..e90d402
--- /dev/null
+++ b/test/Errors/InternalError.py
@@ -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
index 0000000..be69b50
--- /dev/null
+++ b/test/Errors/NameError.py
@@ -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
index 0000000..b9ff1ff
--- /dev/null
+++ b/test/Errors/SyntaxError.py
@@ -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
index 0000000..1ec14dd
--- /dev/null
+++ b/test/Errors/TypeError.py
@@ -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
index 0000000..69ed7d4
--- /dev/null
+++ b/test/Errors/UserError.py
@@ -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
index 0000000..946c1c4
--- /dev/null
+++ b/test/Errors/exit-status.py
@@ -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()
diff --git a/test/Options/Options.py b/test/Options/Options.py
index bd827d1..11452b1 100644
--- a/test/Options/Options.py
+++ b/test/Options/Options.py
@@ -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
diff --git a/test/SConscript/src_dir.py b/test/SConscript/src_dir.py
index f18066b..aaf3bcb 100644
--- a/test/SConscript/src_dir.py
+++ b/test/SConscript/src_dir.py
@@ -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[])
{
diff --git a/test/SWIG/SWIG.py b/test/SWIG/SWIG.py
index ed995a9..791d0e0 100644
--- a/test/SWIG/SWIG.py
+++ b/test/SWIG/SWIG.py
@@ -182,7 +182,7 @@ bar_string()
""")
test.write("bar.i", """\
-%module bar
+%module \t bar
%{
/* Put header files here (optional) */
%}
diff --git a/test/Scanner/generated.py b/test/Scanner/generated.py
index a88303f..99b5787 100644
--- a/test/Scanner/generated.py
+++ b/test/Scanner/generated.py
@@ -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
index f7533b5..0000000
--- a/test/SideEffect.py
+++ /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
index 0000000..b01020b
--- /dev/null
+++ b/test/SideEffect/basic.py
@@ -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
index 0000000..d33e3d4
--- /dev/null
+++ b/test/SideEffect/build_dir.py
@@ -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
index 0000000..5ede853
--- /dev/null
+++ b/test/SideEffect/directory.py
@@ -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
index 0000000..7c9bc27
--- /dev/null
+++ b/test/SideEffect/parallel.py
@@ -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
index 0000000..5709428
--- /dev/null
+++ b/test/Subst/AllowSubstExceptions.py
@@ -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
index 0000000..b37cdf4
--- /dev/null
+++ b/test/Subst/SyntaxError.py
@@ -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
index 0000000..d042b6d
--- /dev/null
+++ b/test/Subst/TypeError.py
@@ -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()
diff --git a/test/TEX/TEX.py b/test/TEX/TEX.py
index 7ea359b..eaea49a 100644
--- a/test/TEX/TEX.py
+++ b/test/TEX/TEX.py
@@ -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
index 0000000..146f6e1
--- /dev/null
+++ b/test/TEX/build_dir.py
@@ -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
index 0000000..b92ab73
--- /dev/null
+++ b/test/TEX/subdir-input.py
@@ -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
index 6e0a05f..0000000
--- a/test/errors.py
+++ /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()
diff --git a/test/import.py b/test/import.py
index bb070ab..92797df 100644
--- a/test/import.py
+++ b/test/import.py
@@ -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))
diff --git a/test/option-c.py b/test/option-c.py
index 7ab4129..e709709 100644
--- a/test/option-c.py
+++ b/test/option-c.py
@@ -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)
diff --git a/test/option-v.py b/test/option-v.py
index 28fe8ca..0626106 100644
--- a/test/option-v.py
+++ b/test/option-v.py
@@ -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()
diff --git a/test/option/debug-memoizer.py b/test/option/debug-memoizer.py
index c9f001c..33f0f4d 100644
--- a/test/option/debug-memoizer.py
+++ b/test/option/debug-memoizer.py
@@ -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")
diff --git a/test/option/debug-nomemoizer.py b/test/option/debug-nomemoizer.py
index 633a46d..3a927e5 100644
--- a/test/option/debug-nomemoizer.py
+++ b/test/option/debug-nomemoizer.py
@@ -25,19 +25,12 @@
__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()
diff --git a/test/option/profile.py b/test/option/profile.py
index 9207066..eb6b394 100644
--- a/test/option/profile.py
+++ b/test/option/profile.py
@@ -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
index 0000000..66c5853
--- /dev/null
+++ b/test/scons-time/func/basic.py
@@ -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
index 0000000..80ec93e
--- /dev/null
+++ b/test/scons-time/func/chdir.py
@@ -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
index 0000000..c9486c4
--- /dev/null
+++ b/test/scons-time/func/file.py
@@ -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
index 0000000..92eb837
--- /dev/null
+++ b/test/scons-time/func/format-gnuplot.py
@@ -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
index 0000000..27bb94d
--- /dev/null
+++ b/test/scons-time/func/function.py
@@ -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
index 0000000..4a629ed
--- /dev/null
+++ b/test/scons-time/func/glob.py
@@ -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
index 0000000..7341ade
--- /dev/null
+++ b/test/scons-time/func/help.py
@@ -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
index 0000000..0767ae6
--- /dev/null
+++ b/test/scons-time/func/no-args.py
@@ -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
index 0000000..53dfea6
--- /dev/null
+++ b/test/scons-time/func/prefix.py
@@ -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
index 0000000..816a7a1
--- /dev/null
+++ b/test/scons-time/func/tail.py
@@ -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
index 0000000..6fa0fac
--- /dev/null
+++ b/test/scons-time/help/all-subcommands.py
@@ -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
index 0000000..942dfa6
--- /dev/null
+++ b/test/scons-time/help/options.py
@@ -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
index 0000000..a97f756
--- /dev/null
+++ b/test/scons-time/mem/chdir.py
@@ -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
index 0000000..a236df7
--- /dev/null
+++ b/test/scons-time/mem/file.py
@@ -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
index 0000000..a4e0f19
--- /dev/null
+++ b/test/scons-time/mem/format-gnuplot.py
@@ -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
index 0000000..6f5174f
--- /dev/null
+++ b/test/scons-time/mem/glob.py
@@ -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
index 0000000..0658d5b
--- /dev/null
+++ b/test/scons-time/mem/help.py
@@ -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
index 0000000..cb47c9b
--- /dev/null
+++ b/test/scons-time/mem/no-args.py
@@ -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
index 0000000..41eb4e4
--- /dev/null
+++ b/test/scons-time/mem/prefix.py
@@ -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
index 0000000..88b21e2
--- /dev/null
+++ b/test/scons-time/mem/stage.py
@@ -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
index 0000000..e2c7ede
--- /dev/null
+++ b/test/scons-time/mem/tail.py
@@ -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
index 0000000..ba98da2
--- /dev/null
+++ b/test/scons-time/no-args.py
@@ -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
index 0000000..5e7df89
--- /dev/null
+++ b/test/scons-time/obj/chdir.py
@@ -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
index 0000000..c881397
--- /dev/null
+++ b/test/scons-time/obj/file.py
@@ -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
index 0000000..b47d843
--- /dev/null
+++ b/test/scons-time/obj/format-gnuplot.py
@@ -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
index 0000000..4af2854
--- /dev/null
+++ b/test/scons-time/obj/glob.py
@@ -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
index 0000000..95abad6
--- /dev/null
+++ b/test/scons-time/obj/help.py
@@ -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
index 0000000..a4d3f84
--- /dev/null
+++ b/test/scons-time/obj/no-args.py
@@ -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
index 0000000..cd91ceb
--- /dev/null
+++ b/test/scons-time/obj/no-files.py
@@ -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
index 0000000..8005dc4
--- /dev/null
+++ b/test/scons-time/obj/prefix.py
@@ -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
index 0000000..5dbd15b
--- /dev/null
+++ b/test/scons-time/obj/stage.py
@@ -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
index 0000000..0483d89
--- /dev/null
+++ b/test/scons-time/obj/tail.py
@@ -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
index 0000000..8f378cb
--- /dev/null
+++ b/test/scons-time/run/aegis.py
@@ -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
index 0000000..a6c48ec
--- /dev/null
+++ b/test/scons-time/run/archive/dir.py
@@ -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
index 0000000..ac69e29
--- /dev/null
+++ b/test/scons-time/run/archive/tar-gz.py
@@ -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
index 0000000..18823d7
--- /dev/null
+++ b/test/scons-time/run/archive/tar.py
@@ -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
index 0000000..f7df9a0
--- /dev/null
+++ b/test/scons-time/run/archive/tgz.py
@@ -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
index 0000000..67cfc3a
--- /dev/null
+++ b/test/scons-time/run/archive/zip.py
@@ -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
index 0000000..8d48d26
--- /dev/null
+++ b/test/scons-time/run/config/archive_list.py
@@ -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
index 0000000..73328ef
--- /dev/null
+++ b/test/scons-time/run/config/initial_commands.py
@@ -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
index 0000000..c04c9e7
--- /dev/null
+++ b/test/scons-time/run/config/prefix.py
@@ -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
index 0000000..4aa85b4
--- /dev/null
+++ b/test/scons-time/run/config/python.py
@@ -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
index 0000000..45ca369
--- /dev/null
+++ b/test/scons-time/run/config/scons.py
@@ -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
index 0000000..4d21318
--- /dev/null
+++ b/test/scons-time/run/config/subdir.py
@@ -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
index 0000000..8d2abd1
--- /dev/null
+++ b/test/scons-time/run/config/targets.py
@@ -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
index 0000000..304992f
--- /dev/null
+++ b/test/scons-time/run/option/help.py
@@ -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
index 0000000..8a23939
--- /dev/null
+++ b/test/scons-time/run/option/next-run.py
@@ -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
index 0000000..520b27d
--- /dev/null
+++ b/test/scons-time/run/option/no-args.py
@@ -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
index 0000000..a2eab74
--- /dev/null
+++ b/test/scons-time/run/option/no-exec.py
@@ -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
index 0000000..ddf2d86
--- /dev/null
+++ b/test/scons-time/run/option/number.py
@@ -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
index 0000000..f60eba0
--- /dev/null
+++ b/test/scons-time/run/option/outdir.py
@@ -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
index 0000000..6bc9619
--- /dev/null
+++ b/test/scons-time/run/option/prefix.py
@@ -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
index 0000000..bd12efe
--- /dev/null
+++ b/test/scons-time/run/option/python.py
@@ -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
index 0000000..5d5d7cf
--- /dev/null
+++ b/test/scons-time/run/option/quiet.py
@@ -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
index 0000000..14ac18d
--- /dev/null
+++ b/test/scons-time/run/option/scons.py
@@ -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
index 0000000..b96d0e6
--- /dev/null
+++ b/test/scons-time/run/option/subdir.py
@@ -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
index 0000000..cdf5b5a
--- /dev/null
+++ b/test/scons-time/run/option/verbose.py
@@ -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
index 0000000..3839999
--- /dev/null
+++ b/test/scons-time/run/subversion.py
@@ -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
index 0000000..b2a4d07
--- /dev/null
+++ b/test/scons-time/time/chdir.py
@@ -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
index 0000000..f4046c9
--- /dev/null
+++ b/test/scons-time/time/file.py
@@ -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
index 0000000..8a599a4
--- /dev/null
+++ b/test/scons-time/time/format-gnuplot.py
@@ -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
index 0000000..5bf2ccd
--- /dev/null
+++ b/test/scons-time/time/glob.py
@@ -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
index 0000000..2f7cb67
--- /dev/null
+++ b/test/scons-time/time/help.py
@@ -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
index 0000000..510b1b3
--- /dev/null
+++ b/test/scons-time/time/no-args.py
@@ -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
index 0000000..cf0462a
--- /dev/null
+++ b/test/scons-time/time/prefix.py
@@ -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
index 0000000..600bbc8
--- /dev/null
+++ b/test/scons-time/time/tail.py
@@ -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
index 0000000..30ce3a6
--- /dev/null
+++ b/test/scons-time/time/which.py
@@ -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
index 0000000..2fb673f
--- /dev/null
+++ b/test/scons-time/unknown.py
@@ -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
index 9612560..0000000
--- a/test/sconsign/script.py
+++ /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
index 0000000..2f5ddf3
--- /dev/null
+++ b/test/sconsign/script/SConsignFile.py
@@ -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
index 0000000..9a3ce62
--- /dev/null
+++ b/test/sconsign/script/Signatures.py
@@ -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
index 0000000..3ada44c
--- /dev/null
+++ b/test/sconsign/script/bad.py
@@ -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
index 0000000..fe49df6
--- /dev/null
+++ b/test/sconsign/script/dblite.py
@@ -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
index 0000000..b860770
--- /dev/null
+++ b/test/sconsign/script/no-SConsignFile.py
@@ -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()
diff --git a/test/symlink/BuildDir.py b/test/symlink/BuildDir.py
index eee0e4d..6394e0a 100644
--- a/test/symlink/BuildDir.py
+++ b/test/symlink/BuildDir.py
@@ -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);
}
""")