├── pytest.ini ├── requirements.txt ├── binary-search ├── test_binarysearch.py └── binarysearch.py ├── cron ├── test_cron.py └── cron.py ├── Vagrantfile ├── runlengthencoding ├── runlengthencoding.py └── test_runlengthencoding.py ├── floodfill ├── test_floodfill.py ├── test_image.py └── floodfill.py ├── README.md └── .gitignore /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | addopts=--strict --tb=short -vv -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | hypothesis 2 | pytest 3 | ipython 4 | coverage 5 | flake8 -------------------------------------------------------------------------------- /binary-search/test_binarysearch.py: -------------------------------------------------------------------------------- 1 | from binarysearch import binary_search 2 | from hypothesis import given 3 | from hypothesis import strategies as st 4 | 5 | 6 | def test_inserts_at_end(): 7 | assert binary_search([1, 2, 3], 5) == 3 8 | 9 | 10 | def test_inserts_at_beginning(): 11 | assert binary_search([1, 2, 3], -1) == 0 12 | 13 | 14 | def test_inserts_at_smallest_point(): 15 | assert binary_search([1, 2, 2, 3], 2) == 1 16 | 17 | 18 | @given(st.lists(st.integers()).map(sorted), st.integers()) 19 | def test_binary_search_does_not_crash(ls, value): 20 | binary_search(ls, value) 21 | -------------------------------------------------------------------------------- /binary-search/binarysearch.py: -------------------------------------------------------------------------------- 1 | def binary_search(list, value): 2 | """If list is sorted, return the smallest index i such that 3 | ls.insert(value) would still be sorted. 4 | 5 | If list is not sorted, the result from this function may be any value i in 6 | the range 0 <= i <= len(list) 7 | 8 | """ 9 | if not list: 10 | return 0 11 | if value > list[-1]: 12 | return len(list) 13 | if value <= list[0]: 14 | return 0 15 | lo = 0 16 | hi = len(list) - 1 17 | while lo + 1 < hi: 18 | mid = (lo + hi) // 2 19 | pivot = list[mid] 20 | if value < pivot: 21 | hi = mid 22 | elif pivot == value: 23 | return mid 24 | else: 25 | lo = mid 26 | assert list[lo] < value 27 | assert list[hi] >= value 28 | return hi 29 | -------------------------------------------------------------------------------- /cron/test_cron.py: -------------------------------------------------------------------------------- 1 | from cron import cron 2 | from hypothesis import given 3 | from hypothesis import strategies as st 4 | 5 | 6 | def test_if_all_star_then_runs_now(): 7 | assert cron(None, None, 10, 10) == (False, 10, 10) 8 | 9 | 10 | def test_star_hour_can_fire_this_hour(): 11 | assert cron(10, None, 10, 30) == (False, 10, 30) 12 | 13 | 14 | def test_star_minute_can_fire_at_current_minute(): 15 | assert cron(2, None, 2, 10) == (False, 2, 10) 16 | 17 | 18 | @given( 19 | cron_hour=st.none() | st.integers(0, 23), 20 | cron_minute=st.none() | st.integers(0, 59), 21 | current_hour=st.integers(0, 23), 22 | current_minute=st.integers(0, 59), 23 | ) 24 | def test_cron_does_not_crash( 25 | cron_hour, cron_minute, current_hour, current_minute 26 | ): 27 | cron(cron_hour, cron_minute, current_hour, current_minute) 28 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | # This is a trivial Vagrantfile designed to simplify development of Hypothesis on Windows, 5 | # where the normal make based build system doesn't work, or anywhere else where you would 6 | # prefer a clean environment for Hypothesis development. It doesn't do anything more than spin 7 | # up a suitable local VM for use with vagrant ssh. You should then use the Makefile from within 8 | # that VM. 9 | 10 | PROVISION = < 0) 41 | result = Image(width, height) 42 | for i, j, v in draw( 43 | st.lists(st.tuples(Indices, Indices, Pixels), average_size=( 44 | width * height / 2 45 | )) 46 | ): 47 | if i < width and j < height: 48 | result[i, j] = v 49 | return result 50 | 51 | 52 | @given(images()) 53 | def test_images_agree_with_data(img): 54 | d = img.to_data() 55 | for x in range(img.width): 56 | for y in range(img.height): 57 | assert d[y][x] == img[x, y] 58 | -------------------------------------------------------------------------------- /floodfill/floodfill.py: -------------------------------------------------------------------------------- 1 | class Image(object): 2 | """A container class representing a two-dimensional 8-bit image""" 3 | 4 | def __init__(self, width, height, data=None): 5 | self.width = width 6 | self.height = height 7 | self.data = [0] * (width * height) 8 | if data is not None: 9 | for i, row in enumerate(data): 10 | for j, v in enumerate(row): 11 | self[j, i] = v 12 | 13 | @property 14 | def size(self): 15 | return (self.width, self.height) 16 | 17 | @classmethod 18 | def from_data(cls, data): 19 | if not data: 20 | return Image(0, 0) 21 | else: 22 | return Image(len(data[0]), len(data), data) 23 | 24 | def to_data(self): 25 | return [ 26 | [self[i, j] for i in range(self.width)] 27 | for j in range(self.height) 28 | ] 29 | 30 | def __getitem__(self, key): 31 | x, y = key 32 | return self.data[self.__index(x, y)] 33 | 34 | def __setitem__(self, key, value): 35 | if not (0 <= value < 256): 36 | raise ValueError('Colour out of range ') 37 | x, y = key 38 | self.data[self.__index(x, y)] = value 39 | 40 | def __index(self, x, y): 41 | if x < 0 or x >= self.width: 42 | raise IndexError('Index x out of range 0 <= %d < %d' % ( 43 | x, self.width 44 | )) 45 | if y < 0 or y >= self.height: 46 | raise IndexError('Index x out of range 0 <= %d < %d' % ( 47 | y, self.height 48 | )) 49 | return self.width * y + x 50 | 51 | def __eq__(self, other): 52 | return isinstance(other, Image) and self.to_data() == other.to_data() 53 | 54 | def __ne__(self, other): 55 | return not self.__eq__(other) 56 | 57 | def __repr__(self): 58 | return "Image(%r, %r, %r)" % ( 59 | self.width, self.height, self.to_data() 60 | ) 61 | 62 | def copy(self): 63 | return Image(self.width, self.height, self.to_data()) 64 | 65 | def flood_fill(image, x, y, replace): 66 | """Set image[x, y] to colour replace, along with any pixels of the same 67 | colour that are connected to it.""" 68 | xsize, ysize = image.size 69 | target_colour = image[x, y] 70 | queue = [(x, y)] 71 | while queue: 72 | i, j = queue.pop() 73 | if i < 0 or i >= xsize: 74 | continue 75 | if j < 0 or j >= xsize: 76 | continue 77 | if image[i, j] == target_colour: 78 | image[i, j] = replace 79 | queue.append((i - 1, j)) 80 | queue.append((i + 1, j)) 81 | queue.append((i, j - 1)) 82 | queue.append((i, j + 1)) 83 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hypothesis Training Examples 2 | 3 | ## What is this? 4 | 5 | If you've come here because I linked to this repo in a training course 6 | you can skip this section and move on to the next one. 7 | 8 | These are examples for the structured training course with Hypothesis 9 | for Python. 10 | 11 | They each consist of an implementation of some specification and a small 12 | set of tests for it. Each implementation is entirely flake8 clean, has 13 | 100% test coverage and is wrong (the tests are all right, they're just 14 | inadequate to the task). 15 | 16 | The goal of the training course is to write tests that can find the 17 | bugs in these implementations. 18 | 19 | Each of these does have some use of Hypothesis in the tests already to 20 | get you started, as the goal here is to learn more about what to test 21 | with Hypothesis rather than the comparatively easy but time consuming 22 | mechanical details of how to use the library. 23 | 24 | The order I currently use in courses is: 25 | 26 | 1. Run Length Encoding 27 | 2. Binary Search 28 | 3. Cron 29 | 4. Flood Fill (this is a bonus question for if we do well on time) 30 | 31 | The course is evolving fairly constantly, so this will change over time. 32 | As examples get cycled out they will remain in this repository even if 33 | they are not currently being used. 34 | 35 | ## Getting Set Up 36 | 37 | If you use Vagrant, the Vagrantfile at the root of this repository will 38 | give you an environment you can work in. This is not necessary and is 39 | merely a convenience for people who prefer to work that way. 40 | 41 | Otherwise, you will need to install Hypothesis and py.test. If you want 42 | to work in a virtualenv (which I'd encourage) the following will get you 43 | started (assuming you are on Linux or OSX and already have virtualenv 44 | installed): 45 | 46 | ``` 47 | python -m virtualenv training 48 | source training/bin/activate 49 | python -m pip install -r requirements.txt 50 | ``` 51 | 52 | If you are on Windows the equivalent invocation is: 53 | 54 | ``` 55 | python -m virtualenv training 56 | training\Scripts\activate 57 | python -m pip install -r requirements.txt 58 | ``` 59 | 60 | If you get an error like 'no module named virtualenv' you need to run 61 | `pip install virtualenv` (this may require sudo depending on your 62 | system). 63 | 64 | ## Running the tests 65 | 66 | You can run the tests for a particular example by cding to the directory 67 | for it and then running `python -m pytest`. This will run all the tests 68 | in Python files with names starting with test_ in the current directory. 69 | 70 | You can also run tests in a specific file with 71 | `python -m pytest some_file.py`, or tests of a specific name with 72 | `python -m pytest -ksome_test_jname` 73 | 74 | Other useful test flags: 75 | 76 | * --lf will run all tests that failed last time pytest was run (or all 77 | tests if none did) 78 | * --pdb will drop you into a pdb console (somewhere between a python 79 | console and a debugger) at the point of test failure. 80 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Vim template 3 | # swap 4 | [._]*.s[a-w][a-z] 5 | [._]s[a-w][a-z] 6 | # session 7 | Session.vim 8 | # temporary 9 | .netrwhist 10 | *~ 11 | # auto-generated tag files 12 | tags 13 | ### Python template 14 | # Byte-compiled / optimized / DLL files 15 | __pycache__/ 16 | *.py[cod] 17 | *$py.class 18 | 19 | # C extensions 20 | *.so 21 | 22 | # Distribution / packaging 23 | .Python 24 | env/ 25 | build/ 26 | develop-eggs/ 27 | dist/ 28 | downloads/ 29 | eggs/ 30 | .eggs/ 31 | lib/ 32 | lib64/ 33 | parts/ 34 | sdist/ 35 | var/ 36 | *.egg-info/ 37 | .installed.cfg 38 | *.egg 39 | 40 | # PyInstaller 41 | # Usually these files are written by a python script from a template 42 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 43 | *.manifest 44 | *.spec 45 | 46 | # Installer logs 47 | pip-log.txt 48 | pip-delete-this-directory.txt 49 | 50 | # Unit test / coverage reports 51 | htmlcov/ 52 | .tox/ 53 | .coverage 54 | .coverage.* 55 | .cache 56 | nosetests.xml 57 | coverage.xml 58 | *,cover 59 | .hypothesis/ 60 | 61 | # Translations 62 | *.mo 63 | *.pot 64 | 65 | # Django stuff: 66 | *.log 67 | local_settings.py 68 | 69 | # Flask stuff: 70 | instance/ 71 | .webassets-cache 72 | 73 | # Scrapy stuff: 74 | .scrapy 75 | 76 | # Sphinx documentation 77 | docs/_build/ 78 | 79 | # PyBuilder 80 | target/ 81 | 82 | # IPython Notebook 83 | .ipynb_checkpoints 84 | 85 | # pyenv 86 | .python-version 87 | 88 | # celery beat schedule file 89 | celerybeat-schedule 90 | 91 | # dotenv 92 | .env 93 | 94 | # virtualenv 95 | venv/ 96 | ENV/ 97 | 98 | # Spyder project settings 99 | .spyderproject 100 | 101 | # Rope project settings 102 | .ropeproject 103 | ### VirtualEnv template 104 | # Virtualenv 105 | # http://iamzed.com/2009/05/07/a-primer-on-virtualenv/ 106 | .Python 107 | [Bb]in 108 | [Ii]nclude 109 | [Ll]ib 110 | [Ll]ib64 111 | [Ll]ocal 112 | [Ss]cripts 113 | pyvenv.cfg 114 | .venv 115 | pip-selfcheck.json 116 | ### JetBrains template 117 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 118 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 119 | 120 | # User-specific stuff: 121 | .idea/workspace.xml 122 | .idea/tasks.xml 123 | .idea/dictionaries 124 | .idea/vcs.xml 125 | .idea/jsLibraryMappings.xml 126 | 127 | # Sensitive or high-churn files: 128 | .idea/dataSources.ids 129 | .idea/dataSources.xml 130 | .idea/dataSources.local.xml 131 | .idea/sqlDataSources.xml 132 | .idea/dynamic.xml 133 | .idea/uiDesigner.xml 134 | 135 | # Gradle: 136 | .idea/gradle.xml 137 | .idea/libraries 138 | 139 | # Mongo Explorer plugin: 140 | .idea/mongoSettings.xml 141 | 142 | ## File-based project format: 143 | *.iws 144 | 145 | ## Plugin-specific files: 146 | 147 | # IntelliJ 148 | /out/ 149 | 150 | # mpeltonen/sbt-idea plugin 151 | .idea_modules/ 152 | 153 | # JIRA plugin 154 | atlassian-ide-plugin.xml 155 | 156 | # Crashlytics plugin (for Android Studio and IntelliJ) 157 | com_crashlytics_export_strings.xml 158 | crashlytics.properties 159 | crashlytics-build.properties 160 | fabric.properties 161 | ### Vagrant template 162 | .vagrant/ 163 | 164 | .idea/ 165 | *.iml 166 | .hypothesis 167 | --------------------------------------------------------------------------------