Sandboxes of directories and files

When testing code that modifies directories and files, it is useful to be able to create and inspect a sample tree of directories and files easily. The tl.testing.fs module provides support for creating a tree from a textual description, listing it in the same format and clean up after itself.

While we use the term ‘sandbox’, this is not about security at all. Tests that use the functions described are not restricted to work within the sandbox directories; they may still happily wreak havoc anywhere on the file system.

Note that this implementation was not designed with threading in mind: it changes the working directory of the process and uses module-global variables.

Setup

The setup_sandboxes function is used to save the original working directory of the test runner. It may take one argument that will be ignored so it can be used as the setUp callback of a test suite:

>>> from tl.testing.fs import setup_sandboxes
>>> setup_sandboxes()
>>> setup_sandboxes(object())

The current working directory is now stored in a module-global variable. We also remember it for later comparisons:

>>> import tl.testing.fs
>>> import os
>>> tl.testing.fs.original_cwd == os.getcwd()
True
>>> original_cwd = os.getcwd()

Creating sandboxes

The new_sandbox function creates a directory and populates it with items described by a multi-line string, one line per item. Each line consists of a character specifying the type of item (directory, file, symbolic link) and the path relative to the sandbox. Depending on the type, a third field may appear. Fields are separated by whitespace:

>>> from tl.testing.fs import new_sandbox
>>> new_sandbox("""\
... d foo
... f foo/bar asdf
... l baz -> foo/bar
... """)

Our working directory has been changed to the new sandbox directory:

>>> sandbox = os.getcwd()
>>> sandbox != tl.testing.fs.original_cwd
True

In order to be able to clean up later, the path to the sandbox is stored in a module-global list:

>>> tl.testing.fs.sandboxes == [sandbox]
True

The sandbox is located in the temporary directory as used by the tempfile module:

>>> import os.path
>>> import tempfile
>>> os.path.dirname(sandbox) == tempfile.gettempdir()
True
>>> os.path.basename(sandbox) in os.listdir(tempfile.gettempdir())
True

The content of the sandbox has been created according to our three-line description. The third field of the file line is written to the file, the third file for the symbolic link must start with ‘-> ‘ and specifies the link target:

>>> sorted(os.listdir('.'))
['baz', 'foo']
>>> os.path.isdir('foo')
True
>>> os.path.islink('baz')
True
>>> os.readlink('baz')
'foo/bar'
>>> os.listdir('foo')
['bar']
>>> os.path.isfile('foo/bar')
True
>>> open('foo/bar').read()
'asdf'

It is not allowed to specify paths that point outside the sandbox:

>>> new_sandbox("f /tmp/tl.cli.rename-impossible")
Traceback (most recent call last):
ValueError: "/tmp/tl.cli.rename-impossible" points outside the sandbox.
>>> new_sandbox("f ../tl.cli.rename-impossible")
Traceback (most recent call last):
ValueError: "../tl.cli.rename-impossible" points outside the sandbox.

Note that the failed sandboxes will be appended in the sandbox list so they can be clean up at some point but our working directory has not been changed:

>>> len(tl.testing.fs.sandboxes)
3
>>> os.getcwd() == sandbox
True

If we create another sandbox, it is placed in the temporary directory under a different name and included in the sandbox list, our working directory changes to the new sandbox, and the first sandbox continues to exist:

>>> new_sandbox("f foobar")
>>> sandbox2 = os.getcwd()
>>> sandbox2 != sandbox
True
>>> tl.testing.fs.sandboxes[-1] == sandbox2
True
>>> os.path.basename(sandbox2) in os.listdir(tempfile.gettempdir())
True
>>> os.listdir('.')
['foobar']
>>> sorted(os.listdir(sandbox))
['baz', 'foo']

Listing the sandbox contents

The ls function creates a readable recursive listing of the sandbox (actually, the current working directory) in the same multi-line text format that is used when creating a sandbox, although sorted alphabetically. To demonstrate this, we switch back to the first sandbox:

>>> os.chdir(sandbox)
>>> from tl.testing.fs import ls
>>> ls()
l baz -> foo/bar
d foo
f foo/bar asdf

Cleaning up

The teardown_sandboxes function removes all sandboxes previously created. They will disappear both from the temporary directory and the sandbox list:

>>> from tl.testing.fs import teardown_sandboxes
>>> teardown_sandboxes()
>>> os.path.basename(sandbox) in os.listdir(tempfile.gettempdir())
False
>>> os.path.basename(sandbox2) in os.listdir(tempfile.gettempdir())
False
>>> tl.testing.fs.sandboxes
[]

Also, our working directory has been reset:

>>> os.getcwd() == original_cwd
True

Like setup_sandboxes, teardown_sandboxes 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_sandboxes may also be called if no current sandbox exists:

>>> teardown_sandboxes(object())
>>> tl.testing.fs.sandboxes
[]
>>> os.getcwd() == original_cwd
True