1.. _rfc-72: 2 3=========================================== 4RFC 72: Update autotest suite to use pytest 5=========================================== 6 7======== =============================== 8Author: Craig de Stigter 9Contact: craig.destigter@koordinates.com 10Started: 2018-Sep-27 11Status: *Implemented in GDAL 2.4* 12======== =============================== 13 14Summary 15------- 16 17The document proposes and describes conversion of the existing Python 18autotest suite to use the `pytest 19framework <https://docs.pytest.org/en/latest/>`__. 20 21Using pytest provides significant productivity gains for writing, 22reading and debugging python tests, compared with the current home-grown 23approach. 24 25Motivation 26---------- 27 28The current autotest framework dates back to 2007 (at least), and while 29reasonably comprehensive (and 186,000 lines of Python) is difficult for 30developers to use and extend. 31 32- As a homegrown framework it'll never get any better than the effort 33 GDAL developers put in. For example: reporting, test coverage, 34 parallelisation, resumption, log/output handling, parameterisation. 35- Test failures are typically only as descriptive as "fail", 36 determining the cause requires editing the tests. 37- It is difficult to run/rerun individual tests 38- The tests often assume a set of compile options that may not be valid 39 for the local build. 40- Tests are patched/disabled in various CI environments by scripts 41 outside the test tree. This is opaque to developers working locally. 42- Some tests depend on each other and a specific execution order, 43 making it difficult to debug and extend. 44- Shared functionality is repeated across tests and modules 45- Tests are typically only written for new functionality, not 46 regressions. (Crudely, from the 2663 commits in the last year only 47 725 touched the autotest tree) 48 49By adopting an OSS test framework in widespread use we can leverage the 50ecosystem to provide GDAL with benefits and improvements going forward. 51The utility of automated testing has been proven for GDAL, and we need 52to make test writing as easy as possible. 53 54Proposal 55-------- 56 57Port the existing Python autotest suite to use the `pytest 58framework <https://docs.pytest.org/en/latest/>`__. Why pytest? It's in 59widespread use, has a wide set of features, is extensible via plugins, 60and focuses on making writing and debugging tests as easy as possible - 61minimising boilerplate code and maximising reuse. `This 62presentation <http://thesoftjaguar.com/pres_pytest.html>`__ (despite 63dating back to 2014) gives a brief overview of the key benefits. 64 65Do the bulk of this port using automated code refactoring tools so the 66autotest suite matches the preferred pytest approach. While pytest does 67support all sorts of custom test collection and execution methods, in 68order to increase the benefits to developers going forward we should do 69a proper conversion. Initial goal is to get the tests ported, remove as 70much boilerplate as feasible, all while keeping the existing CI green. 71Future goals are to continue to reduce boilerplate code and increase 72isolation between tests. 73 74At a minimum we still need to preserve the existing ability to: 75 76- Run all existing CI tests in all environments using the existing 77 configuration 78- Run individual test modules 79- Support existing subprocess/multiprocess tests 80- Support testing under Python 2.7 & Python 3 81- Stacktraces for assertion failures 82 83The new test suite will be in place for the GDAL 2.4.0 release in 84December 2018. Changes will not be backported to the 2.3.x or earlier 85release branches. 86 87References: 88 89- `issue #949 <https://github.com/OSGeo/gdal/issues/949>`__. 90- `gdal-dev 91 post <https://lists.osgeo.org/pipermail/gdal-dev/2018-October/049081.html>`__, 92 Oct 2018 93 94Example 95~~~~~~~ 96 97A typical existing GDAL python unit test: 98 99:: 100 101 def test_gdaladdo_1(): 102 if test_cli_utilities.get_gdaladdo_path() is None: 103 return 'skip' 104 105 shutil.copy('../gcore/data/mfloat32.vrt', 'tmp/mfloat32.vrt') 106 shutil.copy('../gcore/data/float32.tif', 'tmp/float32.tif') 107 108 (_, err) = gdaltest.runexternal_out_and_err(test_cli_utilities.get_gdaladdo_path() + ' tmp/mfloat32.vrt 2 4') 109 if not (err is None or err == ''): 110 gdaltest.post_reason('got error/warning') 111 print(err) 112 return 'fail' 113 114 ds = gdal.Open('tmp/mfloat32.vrt') 115 ret = tiff_ovr.tiff_ovr_check(ds) 116 ds = None 117 118 os.remove('tmp/mfloat32.vrt') 119 os.remove('tmp/mfloat32.vrt.ovr') 120 os.remove('tmp/float32.tif') 121 122 return ret 123 124Could *eventually* become something like this 125 126:: 127 128 @pytest.mark.require_files('gcore/data/mfloat32.vrt', 'gcore/data/float32.tif') 129 def test_gdaladdo_1(gdaladdo): 130 gdaladdo('gcore/data/mfloat32.vrt 2 4') 131 assert os.path.exists('gcore/data/mfloat32.vrt.ovr') 132 133 tiff_ovr.tiff_ovr_check(gdal.Open('mfloat32.vrt')) 134 135It's a lot clearer what it is actually testing, and all support 136functionality is handled by shared-use fixtures (``gdaladdo`` & 137``require_files``), including cleanup and conditional-skipping. 138 139Test output 140~~~~~~~~~~~ 141 142Pytest out-of-the-box produces readable output, and is augmented by the 143``pytest-sugar`` plugin which makes it even nicer: 144 145- Successful tests don't produce much output (a single ``.`` or ``✓`` 146 per test, by default) 147- Failed tests produce a traceback. Any logs, stdout and stderr 148 produced by the failing tests are printed too. This is a great start 149 for debugging the cause of the failure. 150- Any expressions used in failing asserts are printed. 151- Test output is clearly colourised (red/green) if the terminal 152 supports it. 153 154![](pytest-output-example.png, 626px, center) 155 156Plan Phase 1 157------------ 158 159Progress at `pull request 160963 <https://github.com/OSGeo/gdal/pull/963>`__. 161 162- Using code automation, convert the existing Python autotest suite to 163 use pytest-style assertions. 164 165- rename all tests to ``test_*()``. Pytest finds tests by matching 166 names against a regex and this is the default regex. 167 168- generate assertions from ``post_reason()``/``return 'fail'`` calls 169 where possible 170 171- replace all ``skip``/``fail``/``success`` return values 172 173- remove extra ``../pymod`` entries from ``sys.path``. All tests now 174 run in a single process 175 176- remove ``__main__`` block and ``gdaltest_list`` from test files 177 178- these collectively achieve better test collection/selection, output 179 capturing, and improved assertions and reporting 180 181- Manually convert the dynamically-generated tests to use 182 `parametrization <https://docs.pytest.org/en/latest/parametrize.html>`__ 183 184- Ensure the slow/internet tests are still marked as such and skipped 185 by default. 186 187- Use `pytest-sugar <https://pivotfinland.com/pytest-sugar/>`__ to make 188 test output pretty. Disable it in CI since it doesn't work well with 189 travis CI's output buffering. 190 191- Move environment-specific test-skipping from CI to the test suite, 192 possibly with additional tag/marks. 193 194- Ensure the existing CI tests pass & debug any failures 195 196- Add documentation and a straightforward install process for pytest 197 itself 198 199Notable changes and their implications 200~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 201 202- tests are now run with ``cd autotest ; pytest``. (The first time you 203 may need to ``pip install -r requirements.txt`` to install pytest) 204- All tests now run in a single process (they were previously forked 205 for each test module). This means that: 206 207 - errors during test collection are now loud, and immediately fail 208 the entire test run with a traceback. Previously things like 209 syntax errors in files and errors at module level were easy to 210 miss. 211 - a single segfault will kill the entire test run dead. 212 213- It's now possible to run individual tests, instead of just entire 214 files. However, tests are *not yet independent of each other*. So 215 that might cause the tests to behave differently than if you ran the 216 whole module. 217- ``test_py_scripts.run_py_script`` was modified to always run the 218 script as a subprocess. The stdout capturing of the original method 219 did strange things with pytest. This change broke some tests that 220 relied on passing files in the ``/vsimem/`` root to scripts, so those 221 have been changed to use the ``tmp/`` root instead. 222- no test suite support for Python <2.7 223 224.. _plan-phase-2--future-work: 225 226Plan Phase 2 / Future Work 227-------------------------- 228 229- Improving test isolation, so running an entire module at a time isn't 230 required. 231- Removing the global ``gdaltest.<drivername>_drv`` variables and 232 replace them with pytest fixtures. 233- Use fixtures for temporary file handling and cleanup 234- More automated test skipping based on what's actually compiled. 235- Automated style cleanup using 236 `Black <https://github.com/ambv/black>`__. 237- Consider parallelising test runs by default (there are several 238 `plugins available <https://github.com/pytest-dev/pytest-xdist>`__ 239 for this) 240 241Voting history 242-------------- 243 244Adopted with the following votes from PSC members: 245 246- +1 from EvenR, DanielM, HowardB and KurtS 247- +0 from JukkaR 248