|
Packit |
01d647 |
# TL;DR
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
If you just want to write a simple test case, check out the file
|
|
Packit |
01d647 |
`writing_tests.md`.
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
# Introduction
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
This test suite is intended for system tests, i.e. for running a binary with
|
|
Packit |
01d647 |
certain parameters and comparing the output against an expected value. This is
|
|
Packit |
01d647 |
especially useful for a regression test suite, but can be also used for testing
|
|
Packit |
01d647 |
of new features where unit testing is not feasible, e.g. to test new command
|
|
Packit |
01d647 |
line parameters.
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
The test suite is configured via `INI` style files using Python's builtin
|
|
Packit |
01d647 |
[ConfigParser](https://docs.python.org/3/library/configparser.html)
|
|
Packit |
01d647 |
module. Such a configuration file looks roughly like this:
|
|
Packit |
01d647 |
``` ini
|
|
Packit |
01d647 |
[DEFAULT]
|
|
Packit |
01d647 |
some_var: some_val
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
[section 1]
|
|
Packit |
01d647 |
empty_var:
|
|
Packit |
01d647 |
multiline_var: this is a multiline string
|
|
Packit |
01d647 |
as long as the indentation
|
|
Packit |
01d647 |
is present
|
|
Packit |
01d647 |
# comments can be inserted
|
|
Packit |
01d647 |
# some_var is implicitly present in this section by the DEFAULT section
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
[section 2]
|
|
Packit |
01d647 |
# set some_var for this section to something else than the default
|
|
Packit |
01d647 |
some_var: some_other_val
|
|
Packit |
01d647 |
# values from other sections can be inserted
|
|
Packit |
01d647 |
vars can have whitespaces: ${some_var} ${section 1: multiline var}
|
|
Packit |
01d647 |
multiline var: multiline variables can have
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
empty lines too
|
|
Packit |
01d647 |
```
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
For further details concerning the syntax, please consult the official
|
|
Packit |
01d647 |
documentation. The `ConfigParser` module is used with the following defaults:
|
|
Packit |
01d647 |
- Comments are started by `#` only
|
|
Packit |
01d647 |
- The separator between a variable and the value is `:`
|
|
Packit |
01d647 |
- Multiline comments can have empty lines
|
|
Packit |
01d647 |
- Extended Interpolation is used (this allows to refer to other sections when
|
|
Packit |
01d647 |
inserting values using the `${section:variable}` syntax)
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
Please keep in mind that leading and trailing whitespaces are **stripped** from
|
|
Packit |
01d647 |
strings when extracting variable values. So this:
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
``` ini
|
|
Packit |
01d647 |
some_var: some value with whitespaces before and after
|
|
Packit |
01d647 |
```
|
|
Packit |
01d647 |
is equivalent to this:
|
|
Packit |
01d647 |
``` ini
|
|
Packit |
01d647 |
some_var:some value with whitespaces before and after
|
|
Packit |
01d647 |
```
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
The test suite itself uses the builtin `unittest` module of Python to discover
|
|
Packit |
01d647 |
and run the individual test cases. The test cases themselves are implemented in
|
|
Packit |
01d647 |
Python source files, but the required Python knowledge is minimal.
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
## Test suite
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
The test suite is configured via one configuration file whose location
|
|
Packit |
01d647 |
automatically sets the root directory of the test suite. The `unittest` module
|
|
Packit |
01d647 |
then recursively searches all sub-directories with a `__init__.py` file for
|
|
Packit |
01d647 |
files of the form `test_*.py`, which it automatically interprets as test cases
|
|
Packit |
01d647 |
(more about these in the next section). Python will automatically interpret each
|
|
Packit |
01d647 |
directory as a module and use this to format the output, e.g. the test case
|
|
Packit |
01d647 |
`regression/crashes/test_bug_15.py` will be interpreted as the module
|
|
Packit |
01d647 |
`regression.crashes.test_bug_15`. Thus one can use the directory structure to
|
|
Packit |
01d647 |
group test cases.
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
The test suite's configuration file should have the following form:
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
``` ini
|
|
Packit |
01d647 |
[General]
|
|
Packit |
01d647 |
timeout: 0.1
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
[paths]
|
|
Packit |
01d647 |
binary: ../build/bin/binary
|
|
Packit |
01d647 |
important_file: ../conf/main.cfg
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
[variables]
|
|
Packit |
01d647 |
abort_error: ERROR
|
|
Packit |
01d647 |
abort_exit value: 1
|
|
Packit |
01d647 |
```
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
The General section only contains the `timeout` parameter, which is actually
|
|
Packit |
01d647 |
optional (when left out 1.0 is assumed). The timeout sets the maximum time in
|
|
Packit |
01d647 |
seconds for each command that is run before it is aborted. This allows for test
|
|
Packit |
01d647 |
driven development with tests that cause infinite loops or similar hangs in the
|
|
Packit |
01d647 |
test suite.
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
The paths and variables sections define global variables for the system test
|
|
Packit |
01d647 |
suite, which every test case can read. Following the DRY principle, one can put
|
|
Packit |
01d647 |
common outputs of the tested binary in a variable, so that changing an error
|
|
Packit |
01d647 |
message does not result in an hour long update of the test suite. Both sections
|
|
Packit |
01d647 |
are merged together before being passed on to the test cases, thus they must not
|
|
Packit |
01d647 |
contain variables with the same name (doing so results in an error).
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
While the values in the variables section are simply passed on to the test cases
|
|
Packit |
01d647 |
the paths section is special as its contents are interpreted as relative paths
|
|
Packit |
01d647 |
(with respect to the test suite's root) and are expanded to absolute paths
|
|
Packit |
01d647 |
before being passed to the test cases. This can be used to inform each test case
|
|
Packit |
01d647 |
about the location of a built binary or a configuration file without having to
|
|
Packit |
01d647 |
rely on environment variables.
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
However, sometimes environment variables are very handy to implement variable
|
|
Packit |
01d647 |
paths or platform differences (like different build directories or file
|
|
Packit |
01d647 |
extensions). For this, the test suite supports the `ENV` and `ENV fallback`
|
|
Packit |
01d647 |
sections. In conjunction with the extended interpolation of the `ConfigParser`
|
|
Packit |
01d647 |
module, these can be quite useful. Consider the following example:
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
``` ini
|
|
Packit |
01d647 |
[General]
|
|
Packit |
01d647 |
timeout: 0.1
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
[ENV]
|
|
Packit |
01d647 |
variable_prefix: PREFIX
|
|
Packit |
01d647 |
file_extension: FILE_EXT
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
[ENV fallback]
|
|
Packit |
01d647 |
variable_prefix: ../build
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
[paths]
|
|
Packit |
01d647 |
binary: ${ENV:variable_prefix}/bin/binary${ENV:file_extension}
|
|
Packit |
01d647 |
important_file: ../conf/main.cfg
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
[variables]
|
|
Packit |
01d647 |
abort_error: ERROR
|
|
Packit |
01d647 |
abort_exit value: 1
|
|
Packit |
01d647 |
```
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
The `ENV` section is, similarly to the `paths` section, special insofar as the
|
|
Packit |
01d647 |
variables are extracted from the environment with the given name. E.g. the
|
|
Packit |
01d647 |
variable `file_extension` would be set to the value of the environment variable
|
|
Packit |
01d647 |
`FILE_EXT`. If the environment variable is not defined, then the test suite will
|
|
Packit |
01d647 |
look in the `ENV fallback` section for a fallback. E.g. in the above example
|
|
Packit |
01d647 |
`variable_prefix` has the fallback or default value of `../build` which will be
|
|
Packit |
01d647 |
used if the environment variable `PREFIX` is not set. If no fallback is provided
|
|
Packit |
01d647 |
then an empty string is used instead, which would happen to `file_extension` if
|
|
Packit |
01d647 |
`FILE_EXT` would be unset.
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
This can be combined with the extended interpolation of Python's `ConfigParser`,
|
|
Packit |
01d647 |
which allows to include variables from arbitrary sections into other variables
|
|
Packit |
01d647 |
using the `${sect:var_name}` syntax. This would be expanded to the value of
|
|
Packit |
01d647 |
`var_name` from the section `sect`. The above example only utilizes this in the
|
|
Packit |
01d647 |
`paths` section, but it can also be used in the `variables` section, if that
|
|
Packit |
01d647 |
makes sense for the use case.
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
Returning to the example config file, the path `binary` would be inferred in the
|
|
Packit |
01d647 |
following steps:
|
|
Packit |
01d647 |
1. extract `PREFIX` & `FILE_EXT` from the environment, if they don't exist use
|
|
Packit |
01d647 |
the default values from `ENV fallback` or ""
|
|
Packit |
01d647 |
2. substitute the strings `${ENV:variable_prefix}` and `${ENV:file_extension}`
|
|
Packit |
01d647 |
3. expand the relative path to an absolute path
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
Please note that while the `INI` file allows for variables with whitespaces or
|
|
Packit |
01d647 |
`-` in their names, such variables will cause errors as they are invalid
|
|
Packit |
01d647 |
variable names in Python.
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
## Test cases
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
The test cases are defined in Python source files utilizing the unittest module,
|
|
Packit |
01d647 |
thus every file must also be a valid Python file. Each file defining a test case
|
|
Packit |
01d647 |
must start with `test_` and have the file extension `py`. To be discovered by
|
|
Packit |
01d647 |
the unittest module it must reside in a directory with a (empty) `__init__.py`
|
|
Packit |
01d647 |
file.
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
A test case should test one logical unit, e.g. test for regressions of a certain
|
|
Packit |
01d647 |
bug or check if a command line option works. Each test case can run multiple
|
|
Packit |
01d647 |
commands which results are compared to an expected standard output, standard
|
|
Packit |
01d647 |
error and return value. Should differences arise or should one of the commands
|
|
Packit |
01d647 |
take too long, then an error message with the exact differences is shown to the
|
|
Packit |
01d647 |
user.
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
An example test case file would look like this:
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
``` python
|
|
Packit |
01d647 |
# -*- coding: utf-8 -*-
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
import system_tests
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
class AnInformativeName(metaclass=system_tests.CaseMeta):
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
filename = "invalid_input_file"
|
|
Packit |
01d647 |
commands = [
|
|
Packit |
01d647 |
"$binary -c $import_file -i $filename"
|
|
Packit |
01d647 |
]
|
|
Packit |
01d647 |
retval = ["$abort_exit_value"]
|
|
Packit |
01d647 |
stdout = ["Reading $filename"]
|
|
Packit |
01d647 |
stderr = [
|
|
Packit |
01d647 |
"""$abort_error
|
|
Packit |
01d647 |
error in $filename
|
|
Packit |
01d647 |
"""
|
|
Packit |
01d647 |
]
|
|
Packit |
01d647 |
```
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
The first 6 lines are necessary boilerplate to pull in the necessary routines to
|
|
Packit |
01d647 |
run the actual tests (these are implemented in the module `system_tests` with
|
|
Packit |
01d647 |
the meta-class `system_tests.CaseMeta` which performs the necessary preparations
|
|
Packit |
01d647 |
for the tests to run). When adding new tests one should choose a new class name
|
|
Packit |
01d647 |
that briefly summarizes the test. Note that the file name (without the
|
|
Packit |
01d647 |
extension) with the directory structure is interpreted as the module by Python
|
|
Packit |
01d647 |
and pre-pended to the class name when reporting about the tests. E.g. the file
|
|
Packit |
01d647 |
`regression/crashes/test_bug_15.py` with the class `OutOfBoundsRead` gets
|
|
Packit |
01d647 |
reported as `regression.crashes.test_bug_15.OutOfBoundsRead` already including
|
|
Packit |
01d647 |
a brief summary of this test.
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
In the following lines the lists `commands`, `retval`, `stdout` and `stderr`
|
|
Packit |
01d647 |
should be defined. These are lists of strings and must all have the same number
|
|
Packit |
01d647 |
of elements.
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
The test suite at first takes all these strings and substitutes all values
|
|
Packit |
01d647 |
following a `$` with variables either defined in this class alongside (like
|
|
Packit |
01d647 |
`filename` in the above example) or with the values defined in the test suite's
|
|
Packit |
01d647 |
configuration file. Please note that defining a variable with the same name as a
|
|
Packit |
01d647 |
variable in the suite's configuration file will result in an error (otherwise
|
|
Packit |
01d647 |
one of the variables would take precedence leading to unexpected results). The
|
|
Packit |
01d647 |
variables defined in the test suites configuration file are also available in
|
|
Packit |
01d647 |
the `system_tests` namespace. In the above example it would be therefore
|
|
Packit |
01d647 |
possible to access `abort_exit_value` via `system_tests.abort_exit_value`
|
|
Packit |
01d647 |
(please be aware that all values will be strings though).
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
The substitution of values is performed using the template module from Python's
|
|
Packit |
01d647 |
string library via `safe_substitute`. In the above example the command would
|
|
Packit |
01d647 |
thus expand to:
|
|
Packit |
01d647 |
``` shell
|
|
Packit |
01d647 |
/path/to/the/dir/build/bin/binary -c /path/to/the/dir/conf/main.cfg -i invalid_input_file
|
|
Packit |
01d647 |
```
|
|
Packit |
01d647 |
and similarly for `stdout` and `stderr`.
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
Once the substitution is performed, each command is run using Python's
|
|
Packit |
01d647 |
`subprocess` module, its output is compared to the values in `stdout` and
|
|
Packit |
01d647 |
`stderr` and its return value to `retval`. Please note that for portability
|
|
Packit |
01d647 |
reasons the subprocess module is run with `shell=False`, thus shell expansions,
|
|
Packit |
01d647 |
pipes and redirections into files will not work.
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
As the test cases are implemented in Python, one can take full advantage of
|
|
Packit |
01d647 |
Python for the construction of the necessary lists. For example when 10 commands
|
|
Packit |
01d647 |
should be run and all return 0, one can write `retval = 10 * [0]` instead of
|
|
Packit |
01d647 |
writing 0 ten times. The same is of course possible for strings.
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
### Multiline strings
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
It is generally recommended to use Python's multiline strings (strings starting
|
|
Packit |
01d647 |
and ending with three `"` instead of one `"`) for the elements of the `commands`
|
|
Packit |
01d647 |
list, especially when the commands include `"` or escape sequences. Proper
|
|
Packit |
01d647 |
escaping is tricky to get right in a platform independent way, as it depends on
|
|
Packit |
01d647 |
the terminal that is used. Using multiline strings circumvents this issue.
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
There are however some peculiarities with multiline strings in Python. Normal
|
|
Packit |
01d647 |
strings start and end with a single `"` but multiline strings start with three
|
|
Packit |
01d647 |
`"`. Also, while the variable names must be indented, new lines in multiline
|
|
Packit |
01d647 |
strings must not or additional whitespaces will be added. E.g.:
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
``` python
|
|
Packit |
01d647 |
stderr = [
|
|
Packit |
01d647 |
"""something
|
|
Packit |
01d647 |
else"""
|
|
Packit |
01d647 |
]
|
|
Packit |
01d647 |
```
|
|
Packit |
01d647 |
will actually result in the string:
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
```
|
|
Packit |
01d647 |
something
|
|
Packit |
01d647 |
else
|
|
Packit |
01d647 |
```
|
|
Packit |
01d647 |
and not:
|
|
Packit |
01d647 |
```
|
|
Packit |
01d647 |
something
|
|
Packit |
01d647 |
else
|
|
Packit |
01d647 |
```
|
|
Packit |
01d647 |
as the indentation might have suggested.
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
Also note that in this example the string will not be terminated with a newline
|
|
Packit |
01d647 |
character. To achieve that put the `"""` on the following line.
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
### Paths
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
Some test cases require the specification of paths (e.g. to the location of test
|
|
Packit |
01d647 |
cases). This can be problematic when working with the Windows operating system,
|
|
Packit |
01d647 |
as it sometimes exhibits problems with `/` as path separators instead of `\`,
|
|
Packit |
01d647 |
which cannot be used on every other platform.
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
This can be circumvented by creating the paths via `os.path.join`, but that is
|
|
Packit |
01d647 |
quite verbose. A slightly simpler alternative is the function `path` from
|
|
Packit |
01d647 |
`system_tests` which converts all `/` inside your string into the platform's
|
|
Packit |
01d647 |
default path separator:
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
``` python
|
|
Packit |
01d647 |
# -*- coding: utf-8 -*-
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
from system_tests import CaseMeta, path
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
class AnInformativeName(metaclass=CaseMeta):
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
filename = path("$path_to_test_files/invalid_input_file")
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
# the rest of your test case
|
|
Packit |
01d647 |
```
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
## Advanced test cases
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
This section describes more advanced features that are probably not necessary
|
|
Packit |
01d647 |
the "standard" usage of the test suite.
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
### Providing standard input to commands
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
The test suite supports providing a standard input to commands in a similar
|
|
Packit |
01d647 |
fashion as the standard output and error are specified: it expects a list (with
|
|
Packit |
01d647 |
the length equal to the number of commands) of standard inputs (either strings
|
|
Packit |
01d647 |
or `bytes`). For commands that expect no standard input, simply set the
|
|
Packit |
01d647 |
respective entry to `None`:
|
|
Packit |
01d647 |
``` python
|
|
Packit |
01d647 |
# -*- coding: utf-8 -*-
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
import system_tests
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
class AnInformativeName(metaclass=system_tests.CaseMeta):
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
commands = [
|
|
Packit |
01d647 |
"$binary -c $import_file --",
|
|
Packit |
01d647 |
"$binary -c $import_file --"
|
|
Packit |
01d647 |
]
|
|
Packit |
01d647 |
retval = [1, 1]
|
|
Packit |
01d647 |
stdin = [
|
|
Packit |
01d647 |
"read file a",
|
|
Packit |
01d647 |
None
|
|
Packit |
01d647 |
]
|
|
Packit |
01d647 |
stdout = [
|
|
Packit |
01d647 |
"Reading...",
|
|
Packit |
01d647 |
""
|
|
Packit |
01d647 |
]
|
|
Packit |
01d647 |
stderr = [
|
|
Packit |
01d647 |
"Error",
|
|
Packit |
01d647 |
"No input provided"
|
|
Packit |
01d647 |
]
|
|
Packit |
01d647 |
```
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
In this example, the command `$binary -c $import_file --` would be run twice,
|
|
Packit |
01d647 |
first with the standard input `read file a` and second without any input
|
|
Packit |
01d647 |
(resulting in the error `No input provided`).
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
If all commands don't expect any standard input, omit the attribute `stdin`, the
|
|
Packit |
01d647 |
test suite will implicitly assume `None` for every command.
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
### Using a different output encoding
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
The test suite will try to interpret the program's output as utf-8 encoded
|
|
Packit |
01d647 |
strings and if that fails it will try the `iso-8859-1` encoding (also know as
|
|
Packit |
01d647 |
`latin-1`).
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
If the tested program outputs characters in another encoding then it can be
|
|
Packit |
01d647 |
supplied as the `encodings` parameter in each test case:
|
|
Packit |
01d647 |
``` python
|
|
Packit |
01d647 |
# -*- coding: utf-8 -*-
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
import system_tests
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
class AnInformativeName(metaclass=system_tests.CaseMeta):
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
encodings = ['ascii']
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
filename = "invalid_input_file"
|
|
Packit |
01d647 |
commands = [
|
|
Packit |
01d647 |
"$binary -c $import_file -i $filename"
|
|
Packit |
01d647 |
]
|
|
Packit |
01d647 |
retval = ["$abort_exit_value"]
|
|
Packit |
01d647 |
stdout = ["Reading $filename"]
|
|
Packit |
01d647 |
stderr = [
|
|
Packit |
01d647 |
"""$abort_error
|
|
Packit |
01d647 |
error in $filename
|
|
Packit |
01d647 |
"""
|
|
Packit |
01d647 |
]
|
|
Packit |
01d647 |
```
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
The test suite will try to decode the program's output with the provided
|
|
Packit |
01d647 |
encodings in the order that they appear in the list. It will select the first
|
|
Packit |
01d647 |
encoding that can decode the output successfully. If no encoding is able to
|
|
Packit |
01d647 |
decode the program's output, then an error is raised. The list of all supported
|
|
Packit |
01d647 |
encodings can be found
|
|
Packit |
01d647 |
[here](https://docs.python.org/3/library/codecs.html#standard-encodings).
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
### Working with binary output
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
Some programs output binary data directly to stdout or stderr. Such programs can
|
|
Packit |
01d647 |
be also tested by specifying the type `bytes` as the only member in the
|
|
Packit |
01d647 |
`encodings` list and supplying `stdout` and/or `stderr` as `bytes` and not as a
|
|
Packit |
01d647 |
string.
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
An example test case would look like this:
|
|
Packit |
01d647 |
``` python
|
|
Packit |
01d647 |
# -*- coding: utf-8 -*-
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
import system_tests
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
class AnInformativeName(metaclass=system_tests.CaseMeta):
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
encodings = [bytes]
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
commands = ["$prog --dump-binary"]
|
|
Packit |
01d647 |
retval = [1]
|
|
Packit |
01d647 |
stdout = [bytes([1, 2, 3, 4, 16, 42])]
|
|
Packit |
01d647 |
stderr = [bytes()]
|
|
Packit |
01d647 |
```
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
Using the bytes encoding has the following limitations:
|
|
Packit |
01d647 |
- variables of the form `$some_var` cannot be expanded in `stdout` and `stderr`
|
|
Packit |
01d647 |
- if the `bytes` encoding is specified, then both `stderr` and `stdout` must be
|
|
Packit |
01d647 |
valid `bytes`
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
### Setting and modifying environment variables
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
The test suite supports setting or modifying environment variables for
|
|
Packit |
01d647 |
individual test cases. This can be accomplished by adding a member dictionary
|
|
Packit |
01d647 |
named `env` with the appropriate variable names and keys:
|
|
Packit |
01d647 |
``` python
|
|
Packit |
01d647 |
# -*- coding: utf-8 -*-
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
from system_tests import CaseMeta, path
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
class AnInformativeName(metaclass=CaseMeta):
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
env = {
|
|
Packit |
01d647 |
"MYVAR": 26,
|
|
Packit |
01d647 |
"USER": "foobar"
|
|
Packit |
01d647 |
}
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
# if you want a pristine environment, consisting only of MYVAR & USER,
|
|
Packit |
01d647 |
# uncomment the following line:
|
|
Packit |
01d647 |
# inherit_env = False
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
# the rest of the test case follows
|
|
Packit |
01d647 |
```
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
All commands belonging to this test case will be run with a modified environment
|
|
Packit |
01d647 |
where the variables `MYVAR` and `USER` will be set to the specified
|
|
Packit |
01d647 |
values. By default the environment is inherited from the user's environment and
|
|
Packit |
01d647 |
the specified variables in `env` take precedence over the variables in the
|
|
Packit |
01d647 |
user's environment (in the above example the variable `$USER` would be
|
|
Packit |
01d647 |
overridden). If no variables should be inherited set `inherit_env` to `False`
|
|
Packit |
01d647 |
and your test case will get only the specified environment variables.
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
### Creating file copies
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
For tests that modify their input file it is useful to run these with a
|
|
Packit |
01d647 |
disposable copy of the input file and not with the original. For this purpose
|
|
Packit |
01d647 |
the test suite features a decorator which creates a copy of the supplied files
|
|
Packit |
01d647 |
and deletes the copies after the test ran.
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
Example:
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
``` python
|
|
Packit |
01d647 |
# -*- coding: utf-8 -*-
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
import system_tests
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
@system_tests.CopyFiles("$filename", "$some_path/another_file.txt")
|
|
Packit |
01d647 |
class AnInformativeName(metaclass=system_tests.CaseMeta):
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
filename = "invalid_input_file"
|
|
Packit |
01d647 |
commands = [
|
|
Packit |
01d647 |
"$binary -c $import_file -i $filename"
|
|
Packit |
01d647 |
]
|
|
Packit |
01d647 |
retval = ["$abort_exit_value"]
|
|
Packit |
01d647 |
stdout = ["Reading $filename"]
|
|
Packit |
01d647 |
stderr = [
|
|
Packit |
01d647 |
"""$abort_error
|
|
Packit |
01d647 |
error in $filename
|
|
Packit |
01d647 |
"""
|
|
Packit |
01d647 |
]
|
|
Packit |
01d647 |
```
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
In this example, the test suite would automatically create a copy of the files
|
|
Packit |
01d647 |
`invalid_input_file` and `$some_path/another_file.txt` (`some_path` would be of
|
|
Packit |
01d647 |
course expanded too) named `invalid_input_file_copy` and
|
|
Packit |
01d647 |
`$some_path/another_file_copy.txt`. After the test ran, the copies are
|
|
Packit |
01d647 |
deleted. Please note that variable expansion in the filenames is possible.
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
### Customizing the output check
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
Some tests do not require a "brute-force" comparison of the whole output of a
|
|
Packit |
01d647 |
program but only a very simple check (e.g. that a string is present). For these
|
|
Packit |
01d647 |
cases, one can customize how stdout and stderr checked for errors.
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
The `system_tests.Case` class has two public functions for the check of stdout &
|
|
Packit |
01d647 |
stderr: `compare_stdout` & `compare_stderr`. They have the following interface:
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
``` python
|
|
Packit |
01d647 |
compare_stdout(self, i, command, got_stdout, expected_stdout)
|
|
Packit |
01d647 |
compare_stderr(self, i, command, got_stderr, expected_stderr)
|
|
Packit |
01d647 |
```
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
with the parameters:
|
|
Packit |
01d647 |
- i: index of the command in the `commands` list
|
|
Packit |
01d647 |
- command: a string of the actually invoked command
|
|
Packit |
01d647 |
- got_stdout/stderr: the obtained stdout, post-processed depending on the
|
|
Packit |
01d647 |
platform so that lines always end with `\n`
|
|
Packit |
01d647 |
- expected_stdout/stderr: the expected output extracted from
|
|
Packit |
01d647 |
`self.stdout`/`self.stderr`
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
These functions can be overridden in child classes to perform custom checks (or
|
|
Packit |
01d647 |
to omit them completely, too). Please however note, that it is not possible to
|
|
Packit |
01d647 |
customize how the return value is checked. This is indented, as the return value
|
|
Packit |
01d647 |
is often used by the OS to indicate segfaults and ignoring it (in combination
|
|
Packit |
01d647 |
with flawed checks of the output) could lead to crashes not being noticed.
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
A drop-in replacement for `compare_stderr` is provided by the `system_tests`
|
|
Packit |
01d647 |
module itself: `check_no_ASAN_UBSAN_errors`. This function only checks that
|
|
Packit |
01d647 |
errors from AddressSanitizer and undefined behavior sanitizer are not present in
|
|
Packit |
01d647 |
the obtained output to standard error **and nothing else**. This is useful for
|
|
Packit |
01d647 |
test cases where stderr is filled with warnings that are not worth being tracked
|
|
Packit |
01d647 |
by the test suite. It can be used in the following way:
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
``` python
|
|
Packit |
01d647 |
# -*- coding: utf-8 -*-
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
import system_tests
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
class AnInformativeName(metaclass=system_tests.CaseMeta):
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
filename = "invalid_input_file"
|
|
Packit |
01d647 |
commands = ["$binary -c $import_file -i $filename"]
|
|
Packit |
01d647 |
retval = ["$abort_exit_value"]
|
|
Packit |
01d647 |
stdout = ["Reading $filename"]
|
|
Packit |
01d647 |
stderr = ["""A huge amount of error messages would be here that we absolutely do not care about. Actually everything in this string gets ignored, so we can just leave it empty.
|
|
Packit |
01d647 |
"""
|
|
Packit |
01d647 |
]
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
compare_stderr = system_tests.check_no_ASAN_UBSAN_errors
|
|
Packit |
01d647 |
```
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
### Running all commands under valgrind
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
The test suite can run all commands under a memory checker like
|
|
Packit |
01d647 |
[valgrind](http://valgrind.org/) or [dr. memory](http://drmemory.org/). This
|
|
Packit |
01d647 |
option can be enabled by adding the entry `memcheck` in the `General` section of
|
|
Packit |
01d647 |
the configuration file, which specifies the command to invoke the memory
|
|
Packit |
01d647 |
checking tool. The test suite will then prefix **all** commands with the
|
|
Packit |
01d647 |
specified command.
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
For example this configuration file:
|
|
Packit |
01d647 |
``` ini
|
|
Packit |
01d647 |
[General]
|
|
Packit |
01d647 |
timeout: 0.1
|
|
Packit |
01d647 |
memcheck: valgrind --quiet
|
|
Packit |
01d647 |
```
|
|
Packit |
01d647 |
will result in every command specified in the test cases being run as `valgrind
|
|
Packit |
01d647 |
--quiet $command`.
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
When running your test cases under a memory checker, please take the following
|
|
Packit |
01d647 |
into account:
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
- valgrind and dr. memory slow the program execution down by a factor of
|
|
Packit |
01d647 |
10-20. Therefore the test suite will increase the timeout value by a factor of
|
|
Packit |
01d647 |
20 or by the value specified in the option `memcheck_timeout_penalty` in the
|
|
Packit |
01d647 |
`General` section.
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
- valgrind reports by default on success to stderr, be sure to run it with
|
|
Packit |
01d647 |
`--quiet`. Otherwise successful tests will fail under valgrind, as unexpected
|
|
Packit |
01d647 |
output is present on stderr
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
- valgrind and ASAN cannot be used together
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
- Although the option is called `memcheck`, it can be used to execute all
|
|
Packit |
01d647 |
commands via a wrapper that has a completely different purpose (e.g. to
|
|
Packit |
01d647 |
collect test coverage).
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
### Manually expanding variables in strings
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
In case completely custom checks have to be run but one still wants to access
|
|
Packit |
01d647 |
the variables from the test suite, the class `system_test.Case` provides the
|
|
Packit |
01d647 |
function `expand_variables(self, string)`. It performs the previously described
|
|
Packit |
01d647 |
variable substitution using the test suite's configuration file.
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
Unfortunately, it has to run in a class member function. The `setUp()` function
|
|
Packit |
01d647 |
can be used for this, as it is run before each test. For example like this:
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
``` python
|
|
Packit |
01d647 |
class SomeName(metaclass=system_tests.CaseMeta):
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
def setUp(self):
|
|
Packit |
01d647 |
self.commands = [self.expand_variables("$some_var/foo.txt")]
|
|
Packit |
01d647 |
self.stderr = [""]
|
|
Packit |
01d647 |
self.stdout = [self.expand_variables("$success_message")]
|
|
Packit |
01d647 |
self.retval = [0]
|
|
Packit |
01d647 |
```
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
This example will work, as the test runner reads the data for `commands`,
|
|
Packit |
01d647 |
`stderr`, `stdout` and `retval` from the class instance. What however will not
|
|
Packit |
01d647 |
work is creating a new member in `setUp()` and trying to use it as a variable
|
|
Packit |
01d647 |
for expansion, like this:
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
``` python
|
|
Packit |
01d647 |
class SomeName(metaclass=system_tests.CaseMeta):
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
def setUp(self):
|
|
Packit |
01d647 |
self.new_var = "foo"
|
|
Packit |
01d647 |
self.another_string = self.expand_variables("$new_var")
|
|
Packit |
01d647 |
```
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
This example fails in `self.expand_variables` because the expansion uses only
|
|
Packit |
01d647 |
static class members (which `new_var` is not). Also, if you modify a static
|
|
Packit |
01d647 |
class member in `setUp()` the changed version will **not** be used for variable
|
|
Packit |
01d647 |
expansion, as the variables are saved in a new dictionary **before** `setUp()`
|
|
Packit |
01d647 |
runs. Thus this:
|
|
Packit |
01d647 |
``` python
|
|
Packit |
01d647 |
class SomeName(metaclass=system_tests.CaseMeta):
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
new_var = "foo"
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
def setUp(self):
|
|
Packit |
01d647 |
self.new_var = "bar"
|
|
Packit |
01d647 |
self.another_string = self.expand_variables("$new_var")
|
|
Packit |
01d647 |
```
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
will result in `another_string` being "foo" and not "bar".
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
### Hooks
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
The `Case` class provides two hooks that are run after each command and after
|
|
Packit |
01d647 |
all commands, respectively. The hook which is run after each successful command
|
|
Packit |
01d647 |
has the following signature:
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
``` Python
|
|
Packit |
01d647 |
post_command_hook(self, i, command)
|
|
Packit |
01d647 |
```
|
|
Packit |
01d647 |
with the following parameters:
|
|
Packit |
01d647 |
- `i`: index of the command in the `commands` list
|
|
Packit |
01d647 |
- `command`: a string of the actually invoked command
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
The hook which is run after all test takes no parameters except `self`:
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
``` Python
|
|
Packit |
01d647 |
post_tests_hook(self)
|
|
Packit |
01d647 |
```
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
By default, these hooks do nothing. They can be used to implement custom checks
|
|
Packit |
01d647 |
after certain commands, e.g. to check if a file was created. Such a test can be
|
|
Packit |
01d647 |
implemented as follows:
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
``` Python
|
|
Packit |
01d647 |
# -*- coding: utf-8 -*-
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
import system_tests
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
class AnInformativeName(metaclass=system_tests.CaseMeta):
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
filename = "input_file"
|
|
Packit |
01d647 |
output = "out"
|
|
Packit |
01d647 |
commands = ["$binary -o output -i $filename"]
|
|
Packit |
01d647 |
retval = [0]
|
|
Packit |
01d647 |
stdout = [""]
|
|
Packit |
01d647 |
stderr = [""]
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
output_contents = """Hello World!
|
|
Packit |
01d647 |
"""
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
def post_tests_hook(self):
|
|
Packit |
01d647 |
with open(self.output, "r") as out:
|
|
Packit |
01d647 |
self.assertMultiLineEqual(self.output_contents, out.read(-1))
|
|
Packit |
01d647 |
```
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
### Possible pitfalls
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
- Do not provide a custom `setUpClass()` function for the test
|
|
Packit |
01d647 |
cases. `setUpClass()` is used by `system_tests.Case` to store the variables
|
|
Packit |
01d647 |
for expansion.
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
## Running the test suite
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
The test suite is written for Python 3 and is not compatible with Python 2, thus
|
|
Packit |
01d647 |
it must be run with `python3` and not with `python` (which is usually an alias
|
|
Packit |
01d647 |
for Python 2).
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
Then navigate to the `tests/` subdirectory and run:
|
|
Packit |
01d647 |
``` shell
|
|
Packit |
01d647 |
python3 runner.py
|
|
Packit |
01d647 |
```
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
One can supply the script with a directory where the suite should look for the
|
|
Packit |
01d647 |
tests (it will search the directory recursively). If omitted, the runner will
|
|
Packit |
01d647 |
look in the directory where the configuration file is located. It is also
|
|
Packit |
01d647 |
possible to instead pass a file as the parameter, the test suite will then only
|
|
Packit |
01d647 |
run the tests from this file.
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
The runner script also supports the optional arguments `--config_file` which
|
|
Packit |
01d647 |
allows to provide a different test suite configuration file than the default
|
|
Packit |
01d647 |
`suite.conf`. It also forwards the verbosity setting via the `-v`/`--verbose`
|
|
Packit |
01d647 |
flags to Python's unittest module.
|
|
Packit |
01d647 |
|
|
Packit |
01d647 |
Optionally one can provide the `--debug` flag which will instruct test suite to
|
|
Packit |
01d647 |
print all command invocations and all expected and obtained outputs to the
|
|
Packit |
01d647 |
standard output.
|