From 6456df4e9d103a75231d0ea43bb87250ad8745a6 Mon Sep 17 00:00:00 2001 From: Zac Hatfield-Dodds Date: Tue, 20 Mar 2018 23:40:11 +1100 Subject: [PATCH] Starter property-based test suite (#1972) --- .gitignore | 3 ++ .travis.yml | 4 +++ ci/requirements-py36-hypothesis.yml | 27 +++++++++++++++++ properties/README.md | 22 ++++++++++++++ properties/test_encode_decode.py | 46 +++++++++++++++++++++++++++++ 5 files changed, 102 insertions(+) create mode 100644 ci/requirements-py36-hypothesis.yml create mode 100644 properties/README.md create mode 100644 properties/test_encode_decode.py diff --git a/.gitignore b/.gitignore index b573471940d..9069cbebacc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ *.py[cod] __pycache__ +# example caches from Hypothesis +.hypothesis/ + # temp files from docs build doc/auto_gallery doc/example.nc diff --git a/.travis.yml b/.travis.yml index cee21bd87c6..5a9bea81e4c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -47,6 +47,8 @@ matrix: env: CONDA_ENV=py36-zarr-dev - python: 3.5 env: CONDA_ENV=docs + - python: 3.6 + env: CONDA_ENV=py36-hypothesis allow_failures: - python: 3.6 env: @@ -104,6 +106,8 @@ script: - if [[ "$CONDA_ENV" == "docs" ]]; then conda install -c conda-forge sphinx sphinx_rtd_theme sphinx-gallery numpydoc; sphinx-build -n -j auto -b html -d _build/doctrees doc _build/html; + elif [[ "$CONDA_ENV" == "py36-hypothesis" ]]; then + pytest properties ; else py.test xarray --cov=xarray --cov-config ci/.coveragerc --cov-report term-missing --verbose $EXTRA_FLAGS; fi diff --git a/ci/requirements-py36-hypothesis.yml b/ci/requirements-py36-hypothesis.yml new file mode 100644 index 00000000000..29f4ae33538 --- /dev/null +++ b/ci/requirements-py36-hypothesis.yml @@ -0,0 +1,27 @@ +name: test_env +channels: + - conda-forge +dependencies: + - python=3.6 + - dask + - distributed + - h5py + - h5netcdf + - matplotlib + - netcdf4 + - pytest + - flake8 + - numpy + - pandas + - scipy + - seaborn + - toolz + - rasterio + - bottleneck + - zarr + - pip: + - coveralls + - pytest-cov + - pydap + - lxml + - hypothesis diff --git a/properties/README.md b/properties/README.md new file mode 100644 index 00000000000..711062a2473 --- /dev/null +++ b/properties/README.md @@ -0,0 +1,22 @@ +# Property-based tests using Hypothesis + +This directory contains property-based tests using a library +called [Hypothesis](https://github.com/HypothesisWorks/hypothesis-python). + +The property tests for Xarray are a work in progress - more are always welcome. +They are stored in a separate directory because they tend to run more examples +and thus take longer, and so that local development can run a test suite +without needing to `pip install hypothesis`. + +## Hang on, "property-based" tests? + +Instead of making assertions about operations on a particular piece of +data, you use Hypothesis to describe a *kind* of data, then make assertions +that should hold for *any* example of this kind. + +For example: "given a 2d ndarray of dtype uint8 `arr`, +`xr.DataArray(arr).plot.imshow()` never raises an exception". + +Hypothesis will then try many random examples, and report a minimised +failing input for each error it finds. +[See the docs for more info.](https://hypothesis.readthedocs.io/en/master/) diff --git a/properties/test_encode_decode.py b/properties/test_encode_decode.py new file mode 100644 index 00000000000..8d84c0f6815 --- /dev/null +++ b/properties/test_encode_decode.py @@ -0,0 +1,46 @@ +""" +Property-based tests for encoding/decoding methods. + +These ones pass, just as you'd hope! + +""" +from __future__ import absolute_import, division, print_function + +from hypothesis import given, settings +import hypothesis.strategies as st +import hypothesis.extra.numpy as npst + +import xarray as xr + +# Run for a while - arrays are a bigger search space than usual +settings.deadline = None + + +an_array = npst.arrays( + dtype=st.one_of( + npst.unsigned_integer_dtypes(), + npst.integer_dtypes(), + npst.floating_dtypes(), + ), + shape=npst.array_shapes(max_side=3), # max_side specified for performance +) + + +@given(st.data(), an_array) +def test_CFMask_coder_roundtrip(data, arr): + names = data.draw(st.lists(st.text(), min_size=arr.ndim, + max_size=arr.ndim, unique=True).map(tuple)) + original = xr.Variable(names, arr) + coder = xr.coding.variables.CFMaskCoder() + roundtripped = coder.decode(coder.encode(original)) + xr.testing.assert_identical(original, roundtripped) + + +@given(st.data(), an_array) +def test_CFScaleOffset_coder_roundtrip(data, arr): + names = data.draw(st.lists(st.text(), min_size=arr.ndim, + max_size=arr.ndim, unique=True).map(tuple)) + original = xr.Variable(names, arr) + coder = xr.coding.variables.CFScaleOffsetCoder() + roundtripped = coder.decode(coder.encode(original)) + xr.testing.assert_identical(original, roundtripped)