Blame tests/doc.md

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.