summaryrefslogtreecommitdiff
path: root/testing/framework/test-framework.rst
diff options
context:
space:
mode:
Diffstat (limited to 'testing/framework/test-framework.rst')
-rw-r--r--testing/framework/test-framework.rst523
1 files changed, 523 insertions, 0 deletions
diff --git a/testing/framework/test-framework.rst b/testing/framework/test-framework.rst
new file mode 100644
index 0000000..cb6b8e1
--- /dev/null
+++ b/testing/framework/test-framework.rst
@@ -0,0 +1,523 @@
+=======================
+SCons Testing Framework
+=======================
+
+SCons uses extensive automated tests to ensure quality. The primary goal
+is that users be able to upgrade from version to version without
+any surprise changes in behavior.
+
+In general, no change goes into SCons unless it has one or more new
+or modified tests that demonstrably exercise the bug being fixed or
+the feature being added. There are exceptions to this guideline, but
+they should be just that, ''exceptions''. When in doubt, make sure
+it's tested.
+
+Test Organization
+=================
+
+There are three types of SCons tests:
+
+*End-to-End Tests*
+ End-to-end tests of SCons are Python scripts (``*.py``) underneath the
+ ``test/`` subdirectory. They use the test infrastructure modules in
+ the ``testing/framework`` subdirectory. They build set up complete
+ projects and call scons to execute them, checking that the behavior is
+ as expected.
+
+*Unit Tests*
+ Unit tests for individual SCons modules live underneath the
+ ``src/engine/`` subdirectory and are the same base name as the module
+ to be tests, with ``Tests`` appended before the ``.py``. For example,
+ the unit tests for the ``Builder.py`` module are in the
+ ``BuilderTests.py`` script. Unit tests tend to be based on assertions.
+
+*External Tests*
+ For the support of external Tools (in the form of packages, preferably),
+ the testing framework is extended so it can run in standalone mode.
+ You can start it from the top-level folder of your Tool's source tree,
+ where it then finds all Python scripts (``*.py``) underneath the local
+ ``test/`` directory. This implies that Tool tests have to be kept in
+ a folder named ``test``, like for the SCons core.
+
+
+Contrasting End-to-End and Unit Tests
+#####################################
+
+In general, functionality with end-to-end tests
+should be considered a hardened part of the public interface (that is,
+something that a user might do) and should not be broken. Unit tests
+are now considered more malleable, more for testing internal interfaces
+that can change so long as we don't break users' ``SConscript`` files.
+(This wasn't always the case, and there's a lot of meaty code in many
+of the unit test scripts that does, in fact, capture external interface
+behavior. In general, we should try to move those things to end-to-end
+scripts as we find them.)
+
+End-to-end tests are by their nature harder to debug.
+You can drop straight into the Python debugger on the unit test
+scripts by using the ``runtest.py --pdb`` option, but the end-to-end
+tests treat an SCons invocation as a "black box" and just look for
+external effects; simple methods like inserting ``print`` statements
+in the SCons code itself can disrupt those external effects.
+See `Debugging End-to-End Tests`_ for some more thoughts.
+
+Naming Conventions
+##################
+
+The end-to-end tests, more or less, stick to the following naming
+conventions:
+
+#. All tests end with a .py suffix.
+
+#. In the *General* form we use
+
+ ``Feature.py``
+ for the test of a specified feature; try to keep this description
+ reasonably short
+
+ ``Feature-x.py``
+ for the test of a specified feature using option ``x``
+#. The *command line option* tests take the form
+
+ ``option-x.py``
+ for a lower-case single-letter option
+
+ ``option--X.py``
+ upper-case single-letter option (with an extra hyphen, so the
+ file names will be unique on case-insensitive systems)
+
+ ``option--lo.py``
+ long option; abbreviate the long option name to a few characters
+
+
+Running Tests
+=============
+
+The standard set of SCons tests are run from the top-level source
+directory by the ``runtest.py`` script.
+
+Help is available through the ``-h`` option::
+
+ $ python runtest.py -h
+
+To simply run all the tests, use the ``-a`` option::
+
+ $ python runtest.py -a
+
+By default, ``runtest.py`` prints a count and percentage message for each
+test case, along with the name of the test file. If you need the output
+to be more silent, have a look at the ``-q``, ``-s`` and ``-k`` options.
+
+You may specifically list one or more tests to be run::
+
+ $ python runtest.py src/engine/SCons/BuilderTests.py
+ $ python runtest.py test/option-j.py test/Program.py
+
+Folder names are allowed arguments as well, so you can do::
+
+ $ python runtest.py test/SWIG
+
+to run all SWIG tests only.
+
+You can also use the ``-f`` option to execute just the tests listed in
+a specified text file::
+
+ $ cat testlist.txt
+ test/option-j.py
+ test/Program.py
+ $ python runtest.py -f testlist.txt
+
+One test must be listed per line, and any lines that begin with '#'
+will be ignored (the intent being to allow you, for example, to comment
+out tests that are currently passing and then uncomment all of the tests
+in the file for a final validation run).
+
+If more than one test is run, the ``runtest.py`` script prints a summary
+of how many tests passed, failed, or yielded no result, and lists any
+unsuccessful tests.
+
+The above invocations all test directly the files underneath the ``src/``
+subdirectory, and do not require that a packaging build be performed
+first. The ``runtest.py`` script supports additional options to run
+tests against unpacked packages in the ``build/test-*/`` subdirectories.
+
+If you are testing a separate Tool outside of the SCons source tree, you
+have to call the ``runtest.py`` script in *external* (stand-alone) mode::
+
+ $ python ~/scons/runtest.py -e -a
+
+This ensures that the testing framework doesn't try to access SCons
+classes needed for some of the *internal* test cases.
+
+Note, that the actual tests are carried out in a temporary folder each,
+which gets deleted afterwards. This ensures that your source directories
+don't get clobbered with temporary files from the test runs. It also
+means that you can't simply change into a folder to "debug things" after
+a test has gone wrong. For a way around this, check out the ``PRESERVE``
+environment variable. It can be seen in action in
+`How to convert old tests`_ below.
+
+Not Running Tests
+=================
+
+If you simply want to check which tests would get executed, you can call
+the ``runtest.py`` script with the ``-l`` option::
+
+ $ python runtest.py -l
+
+Then there is also the ``-n`` option, which prints the command line for
+each single test, but doesn't actually execute them::
+
+ $ python runtest.py -n
+
+Finding Tests
+=============
+
+When started in *standard* mode::
+
+ $ python runtest.py -a
+
+``runtest.py`` assumes that it is run from the SCons top-level source
+directory. It then dives into the ``src`` and ``test`` folders, where
+it tries to find filenames
+
+``*Test.py``
+ for the ``src`` directory
+
+``*.py``
+ for the ``test`` folder
+
+When using fixtures, you may quickly end up in a position where you have
+supporting Python script files in a subfolder, but they shouldn't get
+picked up as test scripts. In this case you have two options:
+
+#. Add a file with the name ``sconstest.skip`` to your subfolder. This
+ lets ``runtest.py`` skip the contents of the directory completely.
+#. Create a file ``.exclude_tests`` in each folder in question, and in
+ it list line-by-line the files to get excluded from testing.
+
+The same rules apply when testing external Tools by using the ``-e``
+option.
+
+
+Example End-to-End Test Script
+==============================
+
+To illustrate how the end-to-end test scripts work, let's walk through
+a simple "Hello, world!" example::
+
+ #!python
+ import TestSCons
+
+ test = TestSCons.TestSCons()
+
+ test.write('SConstruct', """\
+ Program('hello.c')
+ """)
+
+ test.write('hello.c', """\
+ int
+ main(int argc, char *argv[])
+ {
+ printf("Hello, world!\\n");
+ exit (0);
+ }
+ """)
+
+ test.run()
+
+ test.run(program='./hello', stdout="Hello, world!\n")
+
+ test.pass_test()
+
+
+``import TestSCons``
+ Imports the main infrastructure for writing SCons tests. This is
+ normally the only part of the infrastructure that needs importing.
+ Sometimes other Python modules are necessary or helpful, and get
+ imported before this line.
+
+``test = TestSCons.TestSCons()``
+ This initializes an object for testing. A fair amount happens under
+ the covers when the object is created, including:
+
+ * A temporary directory is created for all the in-line files that will
+ get created.
+
+ * The temporary directory's removal is arranged for when
+ the test is finished.
+
+ * The test does ``os.chdir()`` to the temporary directory.
+
+``test.write('SConstruct', ...)``
+ This line creates an ``SConstruct`` file in the temporary directory,
+ to be used as input to the ``scons`` run(s) that we're testing.
+ Note the use of the Python triple-quote syntax for the contents
+ of the ``SConstruct`` file. Because input files for tests are all
+ created from in-line data like this, the tests can sometimes get
+ a little confusing to read, because some of the Python code is found
+
+``test.write('hello.c', ...)``
+ This lines creates an ``hello.c`` file in the temporary directory.
+ Note that we have to escape the ``\\n`` in the
+ ``"Hello, world!\\n"`` string so that it ends up as a single
+ backslash in the ``hello.c`` file on disk.
+
+``test.run()``
+ This actually runs SCons. Like the object initialization, things
+ happen under the covers:
+
+ * The exit status is verified; the test exits with a failure if
+ the exit status is not zero.
+ * The error output is examined, and the test exits with a failure
+ if there is any.
+
+``test.run(program='./hello', stdout="Hello, world!\n")``
+ This shows use of the ``TestSCons.run()`` method to execute a program
+ other than ``scons``, in this case the ``hello`` program we just
+ presumably built. The ``stdout=`` keyword argument also tells the
+ ``TestSCons.run()`` method to fail if the program output does not
+ match the expected string ``"Hello, world!\n"``. Like the previous
+ ``test.run()`` line, it will also fail the test if the exit status is
+ non-zero, or there is any error output.
+
+``test.pass_test()``
+ This is always the last line in a test script. It prints ``PASSED``
+ on the screen and makes sure we exit with a ``0`` status to indicate
+ the test passed. As a side effect of destroying the ``test`` object,
+ the created temporary directory will be removed.
+
+Working with Fixtures
+=====================
+
+In the simple example above, the files to set up the test are created
+on the fly by the test program. We give a filename to the ``TestSCons.write()``
+method, and a string holding its contents, and it gets written to the test
+folder right before starting..
+
+This technique can still be seen throughout most of the end-to-end tests,
+but there is a better way. To create a test, you need to create the
+files that will be used, then when they work reasonably, they need to
+be pasted into the script. The process repeats for maintenance. Once
+a test gets more complex and/or grows many steps, the test script gets
+harder to read. Why not keep the files as is?
+
+In testing parlance, a fixture is a repeatable test setup. The scons
+test harness allows the use of saved files or directories to be used
+in that sense: "the fixture for this test is foo", instead of writing
+a whole bunch of strings to create files. Since these setups can be
+reusable across multiple tests, the *fixture* terminology applies well.
+
+Directory Fixtures
+##################
+
+The function ``dir_fixture(self, srcdir, dstdir=None)`` in the ``TestCmd``
+class copies the contents of the specified folder ``srcdir`` from
+the directory of the called test script to the current temporary test
+directory. The ``srcdir`` name may be a list, in which case the elements
+are concatenated with the ``os.path.join()`` method. The ``dstdir``
+is assumed to be under the temporary working directory, it gets created
+automatically, if it does not already exist.
+
+A short syntax example::
+
+ test = TestSCons.TestSCons()
+ test.dir_fixture('image')
+ test.run()
+
+would copy all files and subfolders from the local ``image`` folder,
+to the temporary directory for the current test.
+
+To see a real example for this in action, refer to the test named
+``test/packaging/convenience-functions/convenience-functions.py``.
+
+File Fixtures
+#############
+
+Like for directory fixtures, ``file_fixture(self, srcfile, dstfile=None)``
+copies the file ``srcfile`` from the directory of the called script,
+to the temporary test directory. The ``dstfile`` is assumed to be
+under the temporary working directory, unless it is an absolute path
+name. If ``dstfile`` is specified, its target directory gets created
+automatically if it doesn't already exist.
+
+With the following code::
+
+ test = TestSCons.TestSCons()
+ test.file_fixture('SConstruct')
+ test.file_fixture(['src','main.cpp'],['src','main.cpp'])
+ test.run()
+
+The files ``SConstruct`` and ``src/main.cpp`` are copied to the
+temporary test directory. Notice the second ``file_fixture`` line
+preserves the path of the original, otherwise ``main.cpp``
+would have landed in the top level of the test directory.
+
+Again, a reference example can be found in the current revision
+of SCons, it is ``test/packaging/sandbox-test/sandbox-test.py``.
+
+For even more examples you should check out
+one of the external Tools, e.g. the *Qt4* Tool at
+https://bitbucket.org/dirkbaechle/scons_qt4. Also visit the SCons Tools
+Index at https://github.com/SCons/scons/wiki/ToolsIndex for a complete
+list of available Tools, though not all may have tests yet.
+
+How to Convert Old Tests to Use Fixures
+#######################################
+
+Tests using the inline ``TestSCons.write()`` method can easily be
+converted to the fixture based approach. For this, we need to get at the
+files as they are written to each temporary test folder.
+
+``runtest.py`` checks for the existence of an environment
+variable named ``PRESERVE``. If it is set to a non-zero value, the testing
+framework preserves the test folder instead of deleting it, and prints
+its name to the screen.
+
+So, you should be able to give the commands::
+
+ $ PRESERVE=1 python runtest.py test/packaging/sandbox-test.py
+
+assuming Linux and a bash-like shell. For a Windows ``cmd`` shell, use
+``set PRESERVE=1`` (that will leave it set for the duration of the
+``cmd`` session, unless manually deleted).
+
+The output should then look something like this::
+
+ 1/1 (100.00%) /usr/bin/python -tt test/packaging/sandbox-test.py
+ PASSED
+ Preserved directory /tmp/testcmd.4060.twlYNI
+
+You can now copy the files from that folder to your new
+*fixture* folder. Then, in the test script you simply remove all the
+tedious ``TestSCons.write()`` statements and replace them by a single
+``TestSCons.dir_fixture()``.
+
+Finally, don't forget to clean up and remove the temporary test
+directory. ``;)``
+
+When Not to Use a Fixture
+#########################
+
+Note that some files are not appropriate for use in a fixture as-is:
+fixture files should be static. If the creation of the file involves
+interpolating data discovered during the run of the test script,
+that process should stay in the script. Here is an example of this
+kind of usage that does not lend itself to a fixture::
+
+ import TestSCons
+ _python_ = TestSCons._python_
+
+ test.write('SConstruct', """
+ cc = Environment().Dictionary('CC')
+ env = Environment(LINK = r'%(_python_)s mylink.py',
+ LINKFLAGS = [],
+ CC = r'%(_python_)s mycc.py',
+ CXX = cc,
+ CXXFLAGS = [])
+ env.Program(target = 'test1', source = 'test1.c')
+ """ % locals())
+
+Here the value of ``_python_`` is picked out of the script's
+``locals`` dictionary and interpolated into the string that
+will be written to ``SConstruct``.
+
+The other files created in this test may still be candidates for
+use in a fixture, however.
+
+Debugging End-to-End Tests
+==========================
+
+Most of the end to end tests have expectations for standard output
+and error from the test runs. The expectation could be either
+that there is nothing on that stream, or that it will contain
+very specific text which the test matches against. So adding
+``print()`` calls, or ``sys,stderr.write()`` or similar will
+emit data that the tests do not expect, and cause further failures.
+Say you have three different tests in a script, and the third
+one is unexpectedly failing. You add some debug prints to the
+part of scons that is involved, and now the first test of the
+three starts failing, aborting the test run before it gets
+to the third test you were trying to debug.
+
+Still, there are some techniques to help debugging.
+
+Probably the most effective technique is to use the internal
+``SCons.Debug.Trace()`` function, which prints output to
+``/dev/tty`` on Linux/UNIX systems and ``con`` on Windows systems,
+so you can see what's going on.
+
+If you do need to add informational messages in scons code
+to debug a problem, you can use logging and send the messages
+to a file instead, so they don't interrupt the test expectations.
+
+Part of the technique discussed in the section
+`How to Convert Old Tests to Use Fixures`_ can also be helpful
+for debugging purposes. If you have a failing test, try::
+
+ $ PRESERVE=1 python runtest.py test/failing-test.py
+
+You can now go to the save directory reported from this run
+and invoke the test manually to see what it is doing, without
+the presence of the test infrastructure which would otherwise
+"swallow" output you may be interested in. In this case,
+adding debug prints may be more useful.
+
+
+Test Infrastructure
+===================
+
+The main test API in the ``TestSCons.py`` class. ``TestSCons``
+is a subclass of ``TestCommon``, which is a subclass of ``TestCmd``.
+All those classes are defined in python files of the same name
+in ``testing/framework``. Start in
+``testing/framework/TestCmd.py`` for the base API definitions, like how
+to create files (``test.write()``) and run commands (``test.run()``).
+
+Use ``TestSCons`` for the end-to-end tests in ``test``, but use
+``TestCmd`` for the unit tests in the ``src`` folder.
+
+The match functions work like this:
+
+``TestSCons.match_re``
+ match each line with a RE
+
+ * Splits the lines into a list (unless they already are)
+ * splits the REs at newlines (unless already a list) and puts ^..$ around each
+ * then each RE must match each line. This means there must be as many
+ REs as lines.
+
+``TestSCons.match_re_dotall``
+ match all the lines against a single RE
+
+ * Joins the lines with newline (unless already a string)
+ * joins the REs with newline (unless it's a string) and puts ``^..$``
+ around the whole thing
+ * then whole thing must match with python re.DOTALL.
+
+Use them in a test like this::
+
+ test.run(..., match=TestSCons.match_re, ...)
+
+or::
+
+ test.must_match(..., match=TestSCons.match_re, ...)
+
+Avoiding Tests Based on Tool Existence
+======================================
+
+Here's a simple example::
+
+ #!python
+ intelc = test.detect_tool('intelc', prog='icpc')
+ if not intelc:
+ test.skip_test("Could not load 'intelc' Tool; skipping test(s).\n")
+
+See ``testing/framework/TestSCons.py`` for the ``detect_tool`` method.
+It calls the tool's ``generate()`` method, and then looks for the given
+program (tool name by default) in ``env['ENV']['PATH']``.
+
+The ``where_is`` method can be used to look for programs that
+are do not have tool specifications. The existing test code
+will have many samples of using either or both of these to detect
+if it is worth even proceeding with a test.