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