1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372
|
.. sidebar:: ToC
.. contents::
.. _infra-manage-write-app-tests:
Writing App Tests
=================
This HowTo describes the basics of writing your own app tests.
.. caution::
SeqAn does not accept new apps into the repository so this document is less relevant for the future.
Overview
--------
SeqAn application tests allow the simple testing of apps: the application is run several times, each with a different set of parameters and/or input files.
The output to STDOUT and STDERR is captured.
The captured output and all files written by the application are then compared against "gold standard" output files that were previously generated.
The general data flow for app tests is as follows (a simple working example are the app tests for the app **pair_align**).
.. image:: app_tests.png
There are two major steps involved: (A) Test data generation and (B) actually running the tests.
Test Data Generation
^^^^^^^^^^^^^^^^^^^^
This is done once when preparing the test.
The program is run with different parameters (in the case of pair_align with different algorithms, scores, alphabets etc) and input files (for pair_align, e.g. for proteins and DNA sequences). The output files and the output to stdout (logs) are collected. The output and logs are then stored as files in the SVN repository and serve as the reference for future comparisons.
The expected output files are mostly generated by running a "sane" version of the program and then being checked for problems.
Since there are a lot, they are usually generated by shell files, the *generate_outputs.sh* files.
These files also serve as documentation which settings were used for which output files.
Generally, they mirror the structure of the app test Python script (described below).
To reiterate, the shell script is usually only executed once when the tests are created.
The output needs to be regenerated only when the program output changes and this change is **deliberate**.
They are *not* regenerated on each test run.
Note that the path to the binary that is used to generate the output should be stored in a shell variable at the top of the file.
This allows for easily changing this.
These shell scripts should also be well-documented.
See the *generate_outputs.sh* file of pair_align or tree_recon for simple and mostly clean examples.
Running Tests
^^^^^^^^^^^^^
The app tests are then run in the nightly CMake builds and their results are submitted to CDash.
There are two steps involved here: (1) Executing the programs and (2) comparing their result with the expected ones.
There is a Python test driver program (called *run_tests.py* by convention) for each collection of app tests.
These programs us the Python module *seqan.app_tests* for running and usually mirror the corresponding *generate_outputs.sh* file.
Creating App Tests
------------------
We will create app tests for a small app that converts its argument to upper case and prints it to stdout.
Setup App "upcase"
^^^^^^^^^^^^^^^^^^
First, create the app using *skel.py*.
.. code-block:: console
$ ./util/bin/skel.py app upcase .
Then, edit *upcase.cpp* to look as follows:
.. code-block:: cpp
#include <iostream>
#include <cstring>
using namespace seqan;
int main(int argc, char const ** argv)
{
if (argc <= 1)
{
std::cerr << "No arguments given!" << std::endl;
return 1;
}
for (int i = 1; i < argc; ++i)
{
for (char const * ptr = &argv[i][0]; *ptr != '\0'; ++ptr)
std::cout << static_cast<char>(toupper(*ptr));
std::cout << std::endl;
}
return 0;
}
Then, go to your build directory (here: *build/Debug*), build the app, and make sure it works correctly.
.. code-block:: console
$ cd build/Debug
$ cmake .
$ cd apps/upcase
$ make
$ ./upcase This is a test
THIS
IS
A
TEST
Creating App Tests
^^^^^^^^^^^^^^^^^^
You can use the *skel.py* program to create the app tests.
.. code-block:: console
$ cd ../../../../../..
$ ./util/bin/skel.py app_tests apps/upcase/
As suggested by the output of *skel.py*, add the following to your *apps/upcase/CMakeLists.txt*:
.. code-block:: cmake
# Add app tests if Python interpreter could be found.
if(PYTHONINTERP_FOUND)
add_test(NAME app_test_upcase COMMAND ${PYTHON_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tests/run_tests.py ${CMAKE_SOURCE_DIR}
${CMAKE_BINARY_DIR})
endif(PYTHONINTERP_FOUND)
Now, open the file *apps/upcase/tests/generate_outputs.sh* and modify it as follows.
.. code-block:: bash
#!/bin/sh
#
# Output generation script for upcase
UPCASE=../../../../../build/Debug/apps/upcase/upcase
# ============================================================
# Generate Output
# ============================================================
${UPCASE} simple example > simple.stdout
${UPCASE} 'another()/' 'examplE!' > other.stdout
We now run the program two times with different arguments and stored the output in files *simple.stdout* and *other.stdout*.
These files are kept in the directory *apps/upcase/tests* and can now go into version control.
.. code-block:: console
$ cd apps/upcase/tests
$ ./generate_outputs.sh
$ head -1000 simple.stdout other.stdout
===> simple.stdout <===
SIMPLE
EXAMPLE
===> other.stdout <===
ANOTHER()/
EXAMPLE!
Now, we have the expected test output files.
We now have to modify the test driver script *run_tests.py*.
Open the file *apps/upcase/tests/run_tests.py*.
This file is a Python script that runs the programs, collects their output and compares the expected output prepared above with the actual one.
It should look like the following:
.. code-block:: python
#!/usr/bin/env python3
"""Execute the tests for upcase.
The golden test outputs are generated by the script generate_outputs.sh.
You have to give the root paths to the source and the binaries as arguments to
the program. These are the paths to the directory that contains the 'projects'
directory.
Usage: run_tests.py SOURCE_ROOT_PATH BINARY_ROOT_PATH
"""
import logging
import os.path
import sys
# Automagically add util/py_lib to PYTHONPATH environment variable.
path = os.path.abspath(os.path.join(os.path.dirname(<u>file</u>), '..', '..',
'..', '..', 'util', 'py_lib'))
sys.path.insert(0, path)
import seqan.app_tests as app_tests
def main(source_base, binary_base):
"""Main entry point of the script."""
print 'Executing test for upcase'
print '========================='
print
ph = app_tests.TestPathHelper(
source_base, binary_base,
'apps/upcase/tests') # tests dir
# ============================================================
# Auto-detect the binary path.
# ============================================================
path_to_program = app_tests.autolocateBinary(
binary_base, 'apps/upcase', 'upcase')
# ============================================================
# Built TestConf list.
# ============================================================
# Build list with TestConf objects, analogously to how the output
# was generated in generate_outputs.sh.
conf_list = []
# ============================================================
# First Section.
# ============================================================
# App TestConf objects to conf_list, just like this for each
# test you want to run.
conf = app_tests.TestConf(
program=path_to_program,
redir_stdout=ph.outFile('STDOUT_FILE'),
args=['ARGS', 'MUST', 'BE', 'STRINGS', str(1), str(99),
ph.inFile('INPUT_FILE1'),
ph.inFile('INPUT_FILE2')],
to_diff=[(ph.inFile('STDOUT_FILE'),
ph.outFile('STDOUT_FILE')),
(ph.inFile('INPUT_FILE1'),
ph.outFile('INPUT_FILE1'))])
conf_list.append(conf)
# ============================================================
# Execute the tests.
# ============================================================
failures = 0
for conf in conf_list:
res = app_tests.runTest(conf)
# Output to the user.
print ' '.join(['upcase'] + conf.args),
if res:
print 'OK'
else:
failures += 1
print 'FAILED'
print '=============================='
print ' total tests: %d' % len(conf_list)
print ' failed tests: %d' % failures
print 'successful tests: %d' % (len(conf_list) - failures)
print '=============================='
# Compute and return return code.
return failures != 0
if <u>name</u> == '<u>main</u>':
sys.exit(app_tests.main(main))
Here, we now mirror the *generate_outputs.sh* file by replacing the
section *First Section* with the following:
.. code-block:: python
# ============================================================
# Run Program upcase.
# ============================================================
# Simple Example.
conf = app_tests.TestConf(
program=path_to_program,
redir_stdout=ph.outFile('simple.stdout'),
args=['simple', 'example'],
to_diff=[(ph.inFile('simple.stdout'),
ph.outFile('simple.stdout'))])
conf_list.append(conf)
# Another Example.
conf = app_tests.TestConf(
program=path_to_program,
redir_stdout=ph.outFile('other.stdout'),
args=['another()/', 'examplE!'],
to_diff=[(ph.inFile('other.stdout'),
ph.outFile('other.stdout'))])
conf_list.append(conf)
Finally, we can run the program using ctest.
.. code-block:: console
$ cd ../../../../..
$ cd build/Debug/apps/upcase
$ ctest .
If everything goes well, the output will be as follows:
.. code-block:: console
$ ctest .
Test project ${PATH_TO_CHECKOUT}/build/Debug/apps/upcase
Start 1: app_test_upcase
1/1 Test #1: app_test_upcase .................. Passed 0.04 sec
100% tests passed, 0 tests failed out of 1
Total Test time (real) = 0.05 sec
In the case of failures, the output could be as follows.
.. code-block:: console
$ ctest .
Test project /home/holtgrew/Development/seqan-trunk/build/Debug/apps/upcase
Start 1: app_test_upcase
1/1 Test #1: app_test_upcase ..................***Failed 0.02 sec
0% tests passed, 1 tests failed out of 1
Total Test time (real) = 0.03 sec
The following tests FAILED:
1 - app_test_upcase (Failed)
Errors while running CTest
The ``ctest`` command has many options.
A useful one for debugging is ``--output-on-failure`` which will print the test result if the test does not succeed.
For example, the output could be as follows:
.. code-block:: console
$ ctest . --output-on-failure
Test project /home/holtgrew/Development/seqan-src/build/Debug/apps/upcase
Start 1: app_test_upcase
1/1 Test #1: app_test_upcase ..................***Failed 0.02 sec
Traceback (most recent call last):
File "/home/holtgrew/Development/seqan-trunk/apps/upcase/tests/run_tests.py", line 16, in <module>
import seqan.app_tests as app_tests
ImportError: No module named seqan.app_tests
0% tests passed, 1 tests failed out of 1
Total Test time (real) = 0.03 sec
The following tests FAILED:
1 - app_test_upcase (Failed)
Errors while running CTest
This is a common error that tells us that we have to appropriately set the environment variable *PYTHONPATH* so the module *seqan.app_tests* is available from within Python.
You have to add ``util/py_lib`` to your PYTHONPATH.
On Linux/Mac Os X, you can do this as follows (on Windows you have to set the environment variable, e.g. following `this guide <http://vlaurie.com/computers2/Articles/environment.htm>`_).
.. code-block:: console
$ export PYTHONPATH=${PYTHONPATH}:PATH_TO_SEQAN/util/py_lib
Now, you test should run through:
.. code-block:: console
$ ctest . --output-on-failure
...
|