Temporary Projects

When you need to be able to create a small environment to read files from or to write files to, you need to create a Temporary Project that only lasts as long as the test needs it. The TemporaryProjectMixin does just this.

Basic Setup

To setup, add the TemporaryProjectMixin to the list of base classes on your TestCase class:

# test/test_foo.py
from granite.testcase import TestCase, TemporaryProjectMixin

class TestFoo(TemporaryProjectMixin, TestCase):
    # ...

On test setUp(), the TemporaryProjectMixin will create a temporary directory and on test tearDown() that temporary directory will be destroyed.

Interacting with the Temporary Project

The TemporaryProjectMixin adds a new attribute to the TestCase instance: temp_project. This attribute is an instance of the TemporaryProject class and provides accessor methods for interacting with the temporary directory.

TemporaryProject.abspath(filename)[source]

Get the absolute path to the filename found in the temp dir.

Notes

  • Always use forward slashes in paths.
  • This method does not check if the path is valid. If the filename given doesn’t exist, an exception is not raised.
Parameters:filename (str) – the relative path to the file within this temp directory
Returns:the absolute path to the file within the temp directory.
Return type:str
TemporaryProject.read(filename, mode='r')[source]

Read the contents of the file found in the temp directory.

Parameters:
  • filename (str) – the path to the file in the temp dir.
  • mode (str) – a valid mode to open(). defaults to ‘r’
Returns:

The contents of the file.

TemporaryProject.write(filename, contents='', mode='w', dedent=True)[source]

Write the given contents to the file in the temp dir.

If the file or the directories to the file do not exist, they will be created.

If the file already exists, its contents will be overwritten with the new contents unless mode is set to some variant of append: (a, ab).

Specify the dedent flag to automatically call textwrap.dedent on the contents before writing. This is especially useful when writing contents that depend on whitespace being exact (e.g. writing a Python script). This defaults to True except when the mode contains 'b'

Parameters:
  • filename (str) – the relative path to a file in the temp dir
  • contents (any) – any data to write to the file
  • mode (str) – a valid open() mode
  • dedent (bool) – automatically dedent the contents (default: True)
TemporaryProject.remove(filename)[source]

Removes the filename found in the temp dir.

Parameters:filename (str) – the relative path to the file
TemporaryProject.touch(filename)[source]

Creates or updates timestamp on file given by filename.

Parameters:filename (str) – the filename to touch
TemporaryProject.glob(pattern, start='', absolute=False)[source]

Recursively searches through the temp dir for a filename that matches the given pattern and returns the first one that matches.

Parameters:
  • pattern (str) – the glob pattern to match the filenames against. Uses the fnmatch module’s fnmatch() function to determine matches.
  • start (str) – a directory relative to the root of the temp dir to start searching from.
  • absolute (bool) – whether the returned path should be an absolute path; defaults to being relative to the temp project.
Returns:

the relative path to the first filename that matches pattern

unless the absolute flag is given. If a match is not found None is returned.

Return type:

str

TemporaryProject.snapshot()[source]

Creates a snapshot of the current state of this temp dir.

Returns:the snapshot.
Return type:Snapshot
TemporaryProject.copy_project(dest, overwrite=False, symlinks=False, ignore=None)[source]

Allows for a copying the temp project to the destination given.

This provides test authors with the ability to preserve a tes environment at any point during a test.

By default, if the given destination is a directory that already exists, an error will be raised (shutil.copytree’s error). Set the overwrite flag to True to overwrite an existing directory by first removing it and then copying the temp project.

Parameters:
  • dest (str) – the destination directory
  • overwrite (bool) – if the directory exists, this will remove it first before copying
  • symlinks (bool) – passed to shutil.copytree: should symlinks be traversed?
  • ignore (bool) – ignore errors during copy?
TemporaryProject.teardown()[source]

Provides a public way to delete the directory that this temp project manages.

This allows for the temporary directory to be cleaned up on demand.

Ignores all errors.

Note

The temp_project attribute has a public .path attribute which holds the absolute path to the temporary directory.

Example of writing a file

When a file needs to be written to disk (a templated file, etc.) use the write method of the TemporaryProject to write that file to the temporary project:

# tests/test_foo.py

def test_that_file_is_written(self):
    # note that the path to the file should use forward
    # slashes (even on Windows!). The directories will be
    # created automatically.
    self.temp_project.write('path/to/some_file.txt', 'contents to write')

    # assert that the new file exists.
    # note: this uses the .path attribute of the `temp_project` in order
    #       to get the absolute path to the temporary directory
    self.assertTrue(
        os.path.exist(
            os.path.join(self.temp_project.path, 'path', 'to', 'some_file.txt')))

    # we can also assert that the new file exists by using the
    # TemporaryProjectMixin.assert_temp_path_exists() assert method.
    self.assert_temp_path_exists('path/to/some_file.txt')

TemporaryProjectMixin Assert Methods

The TemporaryProjectMixin provides additional assert methods useful for asserting conditions on the temporary project.

TemporaryProjectMixin.assert_in_temp_file(substring, filename, msg='', mode='r', not_in=False)[source]

Asserts that the given contents are found in the file in the temp project.

Parameters:
  • substring (str) – the substring to look for in the file’s contents
  • filename (str) – the name of the file relative to the temp project
  • msg (str) – the message to output in the event of a failure.
  • mode (str) – the mode to open the file with. defaults to ‘r’
  • not_in (bool) – asserts that the contents are not in the file
TemporaryProjectMixin.assert_not_in_temp_file(substring, filename, msg='', mode='r')[source]

Asserts that the given contents are not found in the file in the temp project.

Parameters:
  • substring (str) – the substring to look for in the file’s contents
  • filename (str) – the name of the file relative to the temp project
  • msg (str) – the message to output in the event of a failure.
  • mode (str) – the mode to open the file with. defaults to ‘r’
TemporaryProjectMixin.assert_temp_path_exists(path='.', msg='')[source]

Asserts that the path given exists relative to the root of the temp project.

Parameters:
  • path (str) – the string of the path relative to the root of the temp directory.
  • msg (str) – a custom string to show in the event that this assertion fails.

Taking A Snapshot of the Temporary Project

Sometimes it’s necessary to know what changed within a temporary project. Use the snapshot method on the self.temp_project in order to record a Snapshot of the complete state of all files and directories within the temp project. A snapshot by itself is somewhat useless, but with two snapshots, you can create a diff of the state of the temp project. A SnapshotDiff contains lists of added, removed, modified, and touched files. Note that a touched file is one whose timestamp has changed, but its contents have not. A modified file has had its contents change.

Example:

# tests/test_change_in_dir.py

class TestChangeInDir(TemporaryProjectMixin, BaseTestCase):
    def test_that_dir_changed(self):
        start = self.temp_project.snapshot()
        self.temp_project.write('hello.txt')
        end = self.temp_project.snapshot()
        diff = end - start
        self.assertIn('hello.txt', diff.added)

Advanced Setup

The TemporaryProjectMixin class allows for supplying some class-level attributes in order to configured the TestCase class.

TemporaryProjectMixin.TMP_DIR = None

Allows for setting the temp directory. Defaults to None which will use Python’s tempfile.mkdtemp to make the temp directory.

TemporaryProjectMixin.PRESERVE_DIR = None

Sets where the preserved path should be dumped too. This overrides the TMP_DIR when ENABLE_PRESERVE is set to True.

TemporaryProjectMixin.ENABLE_PRESERVE = False

A flag indicating whether the temp project should be preserved after the temp project object is destroyed. If True, the directory will still exist allowing a user to view the state of the directory after a test has run. This works in tandem with the PRESERVE_DIR class attribute.

TemporaryProjectMixin.TemporaryProjectClass = <class 'granite.environment.TemporaryProject'>

Set this attribute to a class that implements the interface of TemporaryProject. This allows for creating a custom temporary project manager. A typical use case would be to subclass TempProject and override specific functionality then specify that new class here.

Setting a custom temp directory

Sometimes it’s desirable to be in control of the temp directory. In order to change the location of the temporary directory, set the TMP_DIR attribute at the class level:

# tests/test_foo.py
import os

from granite.testcase import TestCase, TemporaryProjectMixin

THIS_DIR = os.path.dirname(os.path.abspath(__file__))

class TestFoo(TemporaryProjectMixin, TestCase):
    # set to be a directory named '.tmp' at the root of the project
    TMP_DIR = os.path.join(THIS_DIR, '..', '.tmp')

Note

There probably isn’t a good reason to change this. Hard-coding a single path will make running tests in parallel impossible, so it’s probably best to stick to the default. The only reason that a deterministic temporary path may be desirable is to inspect the contents of the temporary directory before, during, or after a test run in order to assert that tests are running as expected or to debug a test. For this case see below for setting a preserve path.

Setting a preserve path for temporary project debugging

During testing with temporary projects, inevitably it becomes desirable to preserve the temporary directory in order to debug its contents. The TemporaryProjectMixin provides an option for enabling the preservation of the temp project and setting a known location in order to view that temporary directory:

# tests/test_foo.py
import os

from granite.testcase import TestCase, TemporaryProjectMixin

THIS_DIR = os.path.dirname(os.path.abspath(__file__))

class TestFoo(TemporaryProjectMixin, TestCase):
    # set to be a directory named '.tmp' at the root of the project
    PRESERVE_DIR = os.path.join(THIS_DIR, '..', '.tmp')
    # without this, the TMP_DIR option will still be used
    ENABLE_PRESERVE = True

    def test_foo(self):
        # ...

Setting the PRESERVE_DIR sets the root of all preserved directories. All temporary projects will be created underneath this directory using the TestCase’s class name for the first directory and the resultant temp project will be created under another directory named after the test method.

For example, when the above test_foo is run, a temporary project will be created at .tmp/TestFoo/test_foo (relative to the project root). This allows for inspection of the temp project contents after the test has run at a pre-determined path.

Note that the ENABLE_PRESERVE attribute can be parameterized based on command line arguments. For this reason, it’s a good idea to provide a concrete TemporaryProjectTestCase class or to make some base class inherit the TemporaryProjectMixin so that the logic of enabling or disabling the preservation of temp projects is all in one place:

# tests/__init__.py
import sys

from granite.testcase import TestCase, TemporaryProjectMixin

# either make *all* tests use the TemporaryProjectMixin
class BaseTestCase(TemporaryProjectMixin, TestCase):
    # enable or disable preserve based on a command line flag
    ENABLE_PRESERVE = '--preserve' in sys.argv
    # set to be a directory named '.tmp' at the root of the project
    PRESERVE_DIR = os.path.join(THIS_DIR, '..', '.tmp')


# or provide a concrete temp project test case class only for
# tests that require a temp project
class TempProjectTestCase(TemporaryProjectMixin, TestCase):
    # enable or disable preserve based on a command line flag
    ENABLE_PRESERVE = '--preserve' in sys.argv
    # set to be a directory named '.tmp' at the root of the project
    PRESERVE_DIR = os.path.join(THIS_DIR, '..', '.tmp')

Doing the above provides the preservation configuration in one spot and all later tests can benefit by inheriting.