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: 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 toTrue
except when the mode contains'b'
Parameters:
-
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:
-
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 toTrue
to overwrite an existing directory by first removing it and then copying the temp project.Parameters:
-
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:
-
TemporaryProjectMixin.
assert_temp_path_exists
(path='.', msg='')[source] Asserts that the path given exists relative to the root of the temp project.
Parameters:
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’stempfile.mkdtemp
to make the temp directory.
-
TemporaryProjectMixin.
PRESERVE_DIR
= None Sets where the preserved path should be dumped too. This overrides the
TMP_DIR
whenENABLE_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 subclassTempProject
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.