Installing callable scripts

Some functionality one might want to test makes use of external programs such as a pager or a text editor. The tl.testing.script module provides utilities that install simple mock scripts in places where the code to be tested will find them. They take a string of Python code and create a wrapper script that sets the Python path to match that of the test and runs the code.

The script’s location

Without further arguments, the install function installs the mock script to the temporary directory, makes it executable and returns its absolute path:

>>> from tl.testing.script import install
>>> script_simple = install("print 'A simple script.'")
>>> print open(script_simple).read()

print 'A simple script.'
>>> import tempfile
>>> import os.path
>>> os.path.dirname(script_simple) == tempfile.gettempdir()

We can now call the script. In order to make this file easier to read, let’s define a helper function call first:

>>> import subprocess
>>> def call(script):
...     sub = subprocess.Popen(script, shell=True, stdout=subprocess.PIPE)
...     stdout, stderr = sub.communicate()
...     print stdout
>>> call(script_simple)
A simple script.

We can also influence the installation path of the script. To show this, we create a temporary directory and request that the script be installed into it and given the name ‘script’:

>>> location = tempfile.mkdtemp()
>>> path = os.path.join(location, 'script')
>>> script_at_path = install("print 'A script at a path.'", path=path)
>>> script_at_path == path
>>> call(script_at_path)
A script at a path.

Alternatively, only the script’s base name may be specified. The install function will then create a temporary directory to install the script into:

>>> script_named = install("print 'A named script.'", name='script')
>>> os.path.basename(script_named)
>>> os.path.dirname(os.path.dirname(script_named)) == tempfile.gettempdir()
>>> call(script_named)
A named script.

Making the script available via the environment

If the script’s base name is known, it makes sense to put its directory to the front of the system’s binary path, for instance in order for the script to be found instead of a program with a well-known name. We remember the current value of the PATH variable for use in the clean-up demonstrations:

>>> original_path = os.environ['PATH']
>>> script_on_path_foo = install("print 'A script on PATH: foo.'",
...                              name='tl.testing-foo', on_path=True)
>>> first_path = os.environ['PATH'].split(':')[0]
>>> os.path.dirname(script_on_path_foo) == first_path
>>> call('tl.testing-foo')
A script on PATH: foo.

After installing a second script this way, both are on the binary path:

>>> script_on_path_bar = install("print 'A script on PATH: bar.'",
...                              name='tl.testing-bar', on_path=True)
>>> call('tl.testing-foo')
A script on PATH: foo.
>>> call('tl.testing-bar')
A script on PATH: bar.

Also, installing a script on the path using the same name as an existing program will override that program (since the new path entry is prepended to the binary path list):

>>> script_on_path_bar2 = install("print 'A script on PATH: bar 2.'",
...                               name='tl.testing-bar', on_path=True)
>>> call('tl.testing-bar')
A script on PATH: bar 2.

Finally, install can be told to store the file path of the installed script in the environment of the process since it is a common pattern for applications to determine which external programs to run by examining a environment variable such as PAGER or EDITOR. This works both if the variable already existed, and if it is not yet used:

>>> used_variable = 'tl.testing-%s' % os.getpid()
>>> assert used_variable not in os.environ
>>> os.environ[used_variable] = 'foo'
>>> script_foo = install("print 'A mock foo.'", env=used_variable)
>>> import os
>>> call(os.environ[used_variable])
A mock foo.
>>> unused_variable = 'tl.testing-%s' % (os.getpid() + 1)
>>> assert unused_variable not in os.environ
>>> script_bar = install("print 'A mock baz.'", env=unused_variable)
>>> import os
>>> call(os.environ[unused_variable])
A mock baz.

Python environment inside the script

In addition to the Python source code passed to install, the generated script will contain a few lines in the beginning that make it use the same Python interpreter and module search path as the tests themselves. We demonstrate this by preparing a script that compares its executable and Python path with our own and imports and accesses a module that is not part of the standard library. (We let it access tl.testing.script as we’re sure we have can import it ourselves.)

>>> import sys
>>> script_python = install("""\
... import sys
... print 'Executable:', sys.executable == %r
... print 'Path:', sys.path == %r
... import tl.testing.script
... print tl.testing.script.install
... """ % (sys.executable, sys.path))
>>> print open(script_python).read()

import sys
sys.path[:] = [...]

import sys
print 'Executable:', sys.executable == '...'
print 'Path:', sys.path == [...]
import tl.testing.script
print tl.testing.script.install
>>> call(script_python)
Executable: True
Path: True
<function install at 0x...>

Cleaning up

The tl.testing.script module defines a function teardown_scripts that cleans up after previous install calls. When a script is installed, files and directories to be cleaned up later are stored in a module-global list:

>>> import tl.testing.script
>>> tmp_paths = [script_simple, script_at_path, os.path.dirname(script_named),
...              os.path.dirname(script_on_path_foo),
...              os.path.dirname(script_on_path_bar),
...              os.path.dirname(script_on_path_bar2),
...              script_foo, script_bar, script_python]
>>> tl.testing.script.tmp_paths == tmp_paths
>>> all(os.path.exists(path) for path in tmp_paths)

Also, the original values of any modified environment variables are remembered, with None signalling that the variable had not been in the environment before:

>>> original_environ = {'PATH': original_path,
...                     used_variable: 'foo',
...                     unused_variable: None}
>>> tl.testing.script.original_environ == original_environ
>>> all(os.environ[key] != value
...     for key, value in tl.testing.script.original_environ.items())

Running teardown_scripts removes the stored temporary paths and resets or deletes the modified environment variables:

>>> from tl.testing.script import teardown_scripts
>>> teardown_scripts()
>>> tl.testing.script.tmp_paths
>>> any(os.path.exists(path) for path in tmp_paths)
>>> tl.testing.script.original_environ
>>> os.environ['PATH'] == original_path
>>> os.environ[used_variable]
>>> unused_variable in os.environ

teardown_scripts may take one argument that will be ignored so the function can be used as a test suite’s tearDown callback. As we demonstrate this, we’ll see that teardown_scripts may also be called if no scripts were installed previously:

>>> teardown_scripts(object())
>>> tl.testing.script.tmp_paths
>>> tl.testing.script.original_environ