├── .gitignore ├── LICENSE.txt ├── README.md ├── _github └── workflows │ ├── reveal-secret.yml │ ├── run-tests-matrix.yml │ └── run-tests.yml ├── continuous_integration.pdf ├── continuous_integration.pptx ├── debugging.pdf ├── debugging.pptx ├── extra_slides ├── code_organization_slides.pptx ├── mocking.pptx ├── mocking │ ├── code.py │ ├── demo_Mock.py │ ├── report.py │ └── telescope │ │ ├── telescope_driver.py │ │ ├── telescope_model.py │ │ ├── test_telescope_model.py │ │ └── test_telescope_model_with_mock.py ├── packaging.pptx ├── packaging │ └── noiser_project_final │ │ ├── noiser │ │ ├── __init__.py │ │ ├── images │ │ │ └── baboon_kandinsky.png │ │ ├── main.py │ │ ├── noise.py │ │ ├── setup.py │ │ ├── tests │ │ │ ├── __init__.py │ │ │ ├── test_noise.py │ │ │ └── test_utils.py │ │ └── utils.pyx │ │ └── setup.py └── profiling │ ├── residuals.py │ └── residuals_profile.py ├── hands_on ├── bug_hunt │ ├── datastore │ │ └── README │ └── file_datastore.py ├── debugger │ ├── analyze_sums_and_differences.ipynb │ └── debugger_example.py ├── factorial │ ├── factorial.py │ ├── numbers.txt │ └── test_factorial.py ├── first │ ├── first.py │ └── test_first.py ├── first_teacher │ └── first.py ├── local_maxima │ └── local_maxima.py ├── local_maxima_part2 │ ├── local_maxima.py │ └── test_local_maxima.py ├── logistic_fun │ └── readme.md └── numpy_equality │ └── test_numpy_equality.py ├── hands_on_solutions ├── first │ ├── first.py │ └── test_first.py ├── local_maxima │ ├── local_maxima.py │ └── test_local_maxima.py ├── logistic_fun │ ├── bifurcation.png │ ├── conftest.py │ ├── conftest2.py │ ├── logistic.py │ ├── plot_logfun.py │ ├── plot_script.py │ ├── readme.md │ ├── single_trajectory.png │ └── test_logistic.py ├── massmail_solution │ └── massmail ├── numerical_fuzzing │ └── test_var.py └── numpy_equality │ └── test_numpy_equality.py ├── how_I_compiled_qcachegrind_on_osx.txt ├── material ├── code.py └── logistic_map_plots.ipynb ├── profiling.pptx ├── testing_part1.pptx └── testing_part2.pptx /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | .pytest_cache 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | 47 | # Translations 48 | *.mo 49 | *.pot 50 | 51 | # Django stuff: 52 | *.log 53 | 54 | # Sphinx documentation 55 | docs/_build/ 56 | 57 | # PyBuilder 58 | target/ 59 | 60 | #macos stuff 61 | .DS_Store 62 | 63 | # PyCharm 64 | .idea/ 65 | 66 | # Jupyter 67 | .ipynb_checkpoints/ 68 | 69 | # general 70 | _archive/ 71 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The material in this repository is released under the 2 | CC Attribution-Share Alike 4.0 International 3 | license. 4 | 5 | Full license text available at 6 | https://creativecommons.org/licenses/by-sa/4.0/ 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # testing_debugging_profiling 2 | Material for the class "Testing, debugging, profiling -- Python tools for building software" 3 | -------------------------------------------------------------------------------- /_github/workflows/reveal-secret.yml: -------------------------------------------------------------------------------- 1 | name: Reveal a secret when the repository is tagged as something starting by secret 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'secret*' 7 | 8 | jobs: 9 | reveal-secret: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - shell: bash 14 | env: 15 | SECRET_MSG: ${{ secrets.TOP_SECRET }} 16 | run: | 17 | echo The secret is "$SECRET_MSG" 18 | if [ "$SECRET_MSG" = 'do not tell anyone' ]; then 19 | echo matches 20 | fi 21 | -------------------------------------------------------------------------------- /_github/workflows/run-tests-matrix.yml: -------------------------------------------------------------------------------- 1 | name: Run all the tests for PRs, with OS/Python matrix 2 | 3 | on: 4 | [push, pull_request] 5 | 6 | jobs: 7 | run-tests: 8 | runs-on: ${{ matrix.os }} 9 | 10 | strategy: 11 | matrix: 12 | os: [ubuntu-latest, macos-latest] 13 | python-version: [3.8, 3.9] 14 | 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Set up Python ${{ matrix.python-version }} 18 | uses: actions/setup-python@v2 19 | with: 20 | python-version: ${{ matrix.python-version }} 21 | - name: Install dependencies 22 | run: 23 | python -m pip install pytest numpy 24 | - name: Test with pytest 25 | run: 26 | pytest -sv hands_on/pyanno_voting 27 | -------------------------------------------------------------------------------- /_github/workflows/run-tests.yml: -------------------------------------------------------------------------------- 1 | name: Run all the tests for PRs 2 | 3 | on: 4 | [push, pull_request] 5 | 6 | jobs: 7 | run-tests: 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v2 12 | - name: Set up Python 13 | uses: actions/setup-python@v2 14 | with: 15 | python-version: 3.9 16 | - name: Install dependencies 17 | run: 18 | python -m pip install pytest numpy 19 | - name: Test with pytest 20 | run: 21 | pytest -sv hands_on/pyanno_voting 22 | 23 | -------------------------------------------------------------------------------- /continuous_integration.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ASPP/testing_debugging_profiling/a6585b649b3cb0230d256bc70cc86d58ac6507d4/continuous_integration.pdf -------------------------------------------------------------------------------- /continuous_integration.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ASPP/testing_debugging_profiling/a6585b649b3cb0230d256bc70cc86d58ac6507d4/continuous_integration.pptx -------------------------------------------------------------------------------- /debugging.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ASPP/testing_debugging_profiling/a6585b649b3cb0230d256bc70cc86d58ac6507d4/debugging.pdf -------------------------------------------------------------------------------- /debugging.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ASPP/testing_debugging_profiling/a6585b649b3cb0230d256bc70cc86d58ac6507d4/debugging.pptx -------------------------------------------------------------------------------- /extra_slides/code_organization_slides.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ASPP/testing_debugging_profiling/a6585b649b3cb0230d256bc70cc86d58ac6507d4/extra_slides/code_organization_slides.pptx -------------------------------------------------------------------------------- /extra_slides/mocking.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ASPP/testing_debugging_profiling/a6585b649b3cb0230d256bc70cc86d58ac6507d4/extra_slides/mocking.pptx -------------------------------------------------------------------------------- /extra_slides/mocking/code.py: -------------------------------------------------------------------------------- 1 | 2 | >>> from smtplib import SMTP 3 | >>> mock_smtp = Mock(spec=SMTP) 4 | 5 | >>> isinstance(mock_smtp, SMTP) 6 | True 7 | 8 | >>> mock_smtp. 9 | mock_smtp.assert_any_call mock_smtp.attach_mock mock_smtp.call_args 10 | mock_smtp.assert_called_once_with mock_smtp.auth mock_smtp.call_args_list 11 | mock_smtp.assert_called_with mock_smtp.auth_cram_md5 mock_smtp.call_count > 12 | mock_smtp.assert_has_calls mock_smtp.auth_login mock_smtp.called 13 | mock_smtp.assert_not_called mock_smtp.auth_plain mock_smtp.close 14 | 15 | >>> mock_smtp.bogus 16 | --------------------------------------------------------------------------- 17 | AttributeError Traceback (most recent call last) 18 | in () 19 | ----> 1 mock_smtp.bogus 20 | 21 | /Users/pberkes/miniconda3/envs/gnode/lib/python3.5/unittest/mock.py in __getattr__(self, name) 22 | 576 elif self._mock_methods is not None: 23 | 577 if name not in self._mock_methods or name in _all_magics: 24 | --> 578 raise AttributeError("Mock object has no attribute %r" % name) 25 | 579 elif _is_magic(name): 26 | 580 raise AttributeError(name) 27 | 28 | AttributeError: Mock object has no attribute 'bogus' 29 | 30 | 31 | -------------------------------------------------------------------------------- /extra_slides/mocking/demo_Mock.py: -------------------------------------------------------------------------------- 1 | ##### Mock basic 2 | m = Mock() 3 | m.x = 3 4 | m.x 5 | m.f(1,2,3) 6 | m.whatever(3, key=2) 7 | m 8 | m.f 9 | m.g 10 | 11 | ##### special attributes and assert methods 12 | mock=Mock() 13 | mock.f(2,3) 14 | mock.f('a') 15 | 16 | mock.f.called 17 | mock.add.called 18 | mock.f.called 19 | mock.f.call_args 20 | mock.f.call_count 21 | mock.f.call_args_list 22 | mock.f.assert_called_with('a') 23 | mock.f.assert_called_once_with('a') 24 | mock.f.assert_called_with(2, 3) 25 | mock.f.assert_any_call(2, 3) 26 | mock.f.assert_has_calls(['a', (2,3)]) 27 | 28 | #### return_value and side_effect 29 | mock.g.return_value = 7 30 | mock.g(32) 31 | mock.g('r') 32 | 33 | # useful to simulate file errors or server errors 34 | mock.g.side_effect = Exception('Noooo') 35 | mock.g(2) 36 | mock.g.side_effect = lambda x: x.append(2) 37 | a=[1] 38 | mock.g(a) 39 | a 40 | 41 | mock.g.side_effect = [1, 4, 5] 42 | mock.g() 43 | mock.g() 44 | mock.g() 45 | mock.g() 46 | 47 | ##### 48 | mock = Mock() 49 | mock.f(3,4) 50 | mock.g('a') 51 | mock.f.a() 52 | mock.method_calls 53 | 54 | result = m.h(32) 55 | result(1) 56 | m.mock_calls 57 | 58 | ##### spec 59 | from chaco.api import Plot 60 | m2 = Mock(spec=Plot) 61 | isinstance(m2, Plot) 62 | m2.add 63 | m2.add(12,'asdfasd') 64 | m2.aaa 65 | -------------------------------------------------------------------------------- /extra_slides/mocking/report.py: -------------------------------------------------------------------------------- 1 | report_template = """ 2 | Report 3 | ====== 4 | 5 | The experiment was a {judgment}! 6 | Let's do this again, with a bigger budget. 7 | """ 8 | 9 | 10 | def send_report(result, smtp): 11 | if result > 0.5: 12 | judgment = 'big success' 13 | else: 14 | judgment = 'total failure' 15 | report = report_template.format(judgment=judgment) 16 | smtp.send_message( 17 | report, 18 | from_addr='pony@magicpony.com', 19 | to_addrs=['ferenc@magicpony.com'], 20 | ) 21 | 22 | 23 | from unittest.mock import Mock 24 | 25 | def test_send_report_success(): 26 | smtp = Mock() 27 | 28 | send_report(0.6, smtp) 29 | assert smtp.send_message.call_count == 1 30 | pos_args, kw_args = smtp.send_message.call_args 31 | message = pos_args[0] 32 | assert 'success' in message 33 | 34 | smtp.reset_mock() 35 | 36 | send_report(0.4, smtp) 37 | assert smtp.send_message.call_count == 1 38 | args, kwargs = smtp.send_message.call_args 39 | message = args[0] 40 | assert 'failure' in message 41 | 42 | -------------------------------------------------------------------------------- /extra_slides/mocking/telescope/telescope_driver.py: -------------------------------------------------------------------------------- 1 | 2 | def connect(address): 3 | import time 4 | time.sleep(5) 5 | return '1' 6 | 7 | def get_angle(address): 8 | return 0.0 9 | 10 | def set_angle(address, angle): 11 | if angle < 0 or angle > 1.40: 12 | raise IOError('Telescope jammed -- please call technical support') 13 | return True 14 | -------------------------------------------------------------------------------- /extra_slides/mocking/telescope/telescope_model.py: -------------------------------------------------------------------------------- 1 | import telescope_driver 2 | 3 | 4 | class TelescopeModel(object): 5 | 6 | # Minimum safe elevation angle (see handbook). 7 | MIN_ANGLE = 0.0 8 | 9 | # Maximum safe elevation angle (see handbook). 10 | MAX_ANGLE = 80.0 11 | 12 | def __init__(self, address): 13 | self.address = address 14 | # Connect to telescope 15 | self.connection = telescope_driver.connect(address) 16 | # Get initial state of telescope. 17 | self.current_angle = telescope_driver.get_angle(self.connection) 18 | 19 | def set_elevation_angle(self, angle): 20 | """ Set the elevation angle of the telescope (in rad). 21 | 22 | If the angle is outside the range allowed by the manufacturer, 23 | raise a ValueError. 24 | """ 25 | 26 | if angle < self.MIN_ANGLE or angle > self.MAX_ANGLE: 27 | raise ValueError('Unsafe elevation angle: {}'.format(angle)) 28 | 29 | telescope_driver.set_angle(self.connection, angle) 30 | self.current_angle = angle 31 | -------------------------------------------------------------------------------- /extra_slides/mocking/telescope/test_telescope_model.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from py.test import raises 3 | 4 | from telescope_model import TelescopeModel 5 | 6 | 7 | def test_unsafe_elevation_angle(): 8 | telescope = TelescopeModel(address='10.2.1.1') 9 | elevation_angle = np.pi / 2.0 10 | 11 | with raises(ValueError): 12 | telescope.set_elevation_angle(elevation_angle) 13 | -------------------------------------------------------------------------------- /extra_slides/mocking/telescope/test_telescope_model_with_mock.py: -------------------------------------------------------------------------------- 1 | from unittest import mock 2 | 3 | import numpy as np 4 | from py.test import raises 5 | 6 | from telescope_model import TelescopeModel 7 | 8 | 9 | def test_unsafe_elevation_angle(): 10 | with mock.patch('telescope_model.telescope_driver'): 11 | telescope = TelescopeModel(address='10.2.1.1') 12 | elevation_angle = np.pi / 2.0 13 | with raises(ValueError): 14 | telescope.set_elevation_angle(elevation_angle) 15 | 16 | 17 | def test_model_initialization(): 18 | connection_id = 'bogus_connection' 19 | initial_angle = 1.23 20 | 21 | with mock.patch('telescope_model.telescope_driver') as driver: 22 | driver.connect.return_value = connection_id 23 | driver.get_angle.return_value = initial_angle 24 | 25 | telescope = TelescopeModel(address='10.2.1.1') 26 | assert telescope.connection == connection_id 27 | assert driver.connect.call_count == 1 28 | assert telescope.current_angle == initial_angle 29 | 30 | -------------------------------------------------------------------------------- /extra_slides/packaging.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ASPP/testing_debugging_profiling/a6585b649b3cb0230d256bc70cc86d58ac6507d4/extra_slides/packaging.pptx -------------------------------------------------------------------------------- /extra_slides/packaging/noiser_project_final/noiser/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ASPP/testing_debugging_profiling/a6585b649b3cb0230d256bc70cc86d58ac6507d4/extra_slides/packaging/noiser_project_final/noiser/__init__.py -------------------------------------------------------------------------------- /extra_slides/packaging/noiser_project_final/noiser/images/baboon_kandinsky.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ASPP/testing_debugging_profiling/a6585b649b3cb0230d256bc70cc86d58ac6507d4/extra_slides/packaging/noiser_project_final/noiser/images/baboon_kandinsky.png -------------------------------------------------------------------------------- /extra_slides/packaging/noiser_project_final/noiser/main.py: -------------------------------------------------------------------------------- 1 | import os.path 2 | 3 | import matplotlib.pyplot as plt 4 | from scipy.ndimage import imread 5 | from pkg_resources import resource_filename 6 | 7 | from noiser.noise import white_noise 8 | from noiser.utils import copy_image 9 | 10 | 11 | def main(): 12 | path = resource_filename('noiser', os.path.join('images', 'baboon_kandinsky.png')) 13 | print(path) 14 | img = imread(path) 15 | noisy = copy_image(white_noise(img, 20)) 16 | plt.imshow(noisy) 17 | plt.draw() 18 | plt.show() 19 | 20 | 21 | if __name__ == '__main__': 22 | main() 23 | -------------------------------------------------------------------------------- /extra_slides/packaging/noiser_project_final/noiser/noise.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | 4 | def white_noise(image, std): 5 | noise = np.random.normal(scale=std, size=image.shape).astype(image.dtype) 6 | noisy = image + noise 7 | return noisy 8 | -------------------------------------------------------------------------------- /extra_slides/packaging/noiser_project_final/noiser/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | setup( 4 | name='Noiser', 5 | version='1.0', 6 | packages=find_packages(), 7 | ) 8 | 9 | -------------------------------------------------------------------------------- /extra_slides/packaging/noiser_project_final/noiser/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ASPP/testing_debugging_profiling/a6585b649b3cb0230d256bc70cc86d58ac6507d4/extra_slides/packaging/noiser_project_final/noiser/tests/__init__.py -------------------------------------------------------------------------------- /extra_slides/packaging/noiser_project_final/noiser/tests/test_noise.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from numpy.testing import assert_allclose 3 | 4 | from noiser.noise import white_noise 5 | 6 | 7 | def test_white_noise(): 8 | n_images, height, width = 201, 101, 102 9 | dtype = np.float32 10 | 11 | # Create ``n_images`` identical image. 12 | base_image = np.random.rand(1, height, width, 3).astype(dtype) - 0.5 13 | images = np.repeat(base_image, n_images, axis=0) 14 | 15 | std = 0.13 16 | noisy = white_noise(images, std=std) 17 | 18 | # dtype and shape are preserved. 19 | assert noisy.dtype == dtype 20 | assert noisy.shape == images.shape 21 | 22 | # Mean and std of noisy image match expectations. 23 | assert_allclose(images.mean(0), base_image[0], atol=1e-4) 24 | assert np.isclose((noisy - images).std(), std, atol=1e-4) 25 | -------------------------------------------------------------------------------- /extra_slides/packaging/noiser_project_final/noiser/tests/test_utils.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from numpy.testing import assert_array_equal 3 | 4 | from noiser.utils import copy_image 5 | 6 | 7 | def test_copy_image(): 8 | height, width = 101, 102 9 | dtype = np.float32 10 | 11 | image = np.random.rand(height, width, 3).astype(dtype) 12 | copy = copy_image(image) 13 | assert_array_equal(copy, image) 14 | 15 | -------------------------------------------------------------------------------- /extra_slides/packaging/noiser_project_final/noiser/utils.pyx: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | cimport numpy as np 3 | 4 | 5 | def copy_image(np.ndarray img): 6 | cdef int h = img.shape[0] 7 | cdef int w = img.shape[1] 8 | cdef int c = img.shape[2] 9 | cdef np.ndarray copy = np.empty([h, w, c], dtype=img.dtype) 10 | 11 | for i in range(h): 12 | for j in range(w): 13 | for k in range(c): 14 | copy[i, j, k] = img[i, j, k] 15 | return copy 16 | -------------------------------------------------------------------------------- /extra_slides/packaging/noiser_project_final/setup.py: -------------------------------------------------------------------------------- 1 | from Cython.Build import cythonize 2 | import numpy 3 | from setuptools import setup, find_packages 4 | from setuptools.extension import Extension 5 | 6 | 7 | extensions = [ 8 | Extension( 9 | 'noiser.utils', 10 | ["noiser/utils.pyx"], 11 | include_dirs=[numpy.get_include()], 12 | ) 13 | ] 14 | 15 | 16 | setup( 17 | name='Noiser', 18 | version='1.0', 19 | packages=find_packages(), 20 | ext_modules=cythonize(extensions), 21 | entry_points={ 22 | 'console_scripts': [ 23 | 'baboon=noiser.main:main', 24 | ] 25 | }, 26 | package_data={ 27 | 'noiser': ['images/*.png'], 28 | } 29 | ) 30 | -------------------------------------------------------------------------------- /extra_slides/profiling/residuals.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import theano 3 | from theano import tensor as T 4 | 5 | 6 | SLOPE = 3.1 7 | INTERCEPT = -1.2 8 | 9 | 10 | def residual_stats_theano(x, y): 11 | expected = SLOPE * x + INTERCEPT 12 | residuals = y - expected 13 | return residuals.mean(), residuals.std() 14 | 15 | 16 | x_var = T.vector() 17 | y_var = T.vector() 18 | 19 | residual_stats = theano.function( 20 | inputs=[x_var, y_var], 21 | outputs=residual_stats_theano(x_var, y_var), 22 | allow_input_downcast=True, 23 | ) 24 | 25 | 26 | if __name__ == '__main__': 27 | x = np.linspace(-10, 10, 1000) 28 | y = SLOPE * x + INTERCEPT 29 | y += np.random.normal(loc=0.1, scale=0.5, size=x.shape) 30 | mn, std = residual_stats(x, y) 31 | print('Residual mean=', mn, ', std=', std) 32 | -------------------------------------------------------------------------------- /extra_slides/profiling/residuals_profile.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import theano 3 | from theano import tensor as T 4 | 5 | theano.config.profile_memory = True 6 | theano.config.profile = True 7 | 8 | 9 | SLOPE = 3.1 10 | INTERCEPT = -1.2 11 | 12 | 13 | def residual_stats_theano(x, y): 14 | expected = SLOPE * x + INTERCEPT 15 | residuals = y - expected 16 | return residuals.mean(), residuals.std() 17 | 18 | 19 | x_var = T.vector() 20 | y_var = T.vector() 21 | 22 | residual_stats = theano.function( 23 | inputs=[x_var, y_var], 24 | outputs=residual_stats_theano(x_var, y_var), 25 | allow_input_downcast=True, 26 | profile=True, 27 | ) 28 | 29 | 30 | if __name__ == '__main__': 31 | x = np.linspace(-10, 10, 1000) 32 | y = SLOPE * x + INTERCEPT 33 | y += np.random.normal(loc=0.1, scale=0.5, size=x.shape) 34 | mn, std = residual_stats(x, y) 35 | print('Residual mean=', mn, ', std=', std) 36 | -------------------------------------------------------------------------------- /hands_on/bug_hunt/datastore/README: -------------------------------------------------------------------------------- 1 | This is the directory used as a datastore by `file_datastore.py`. 2 | -------------------------------------------------------------------------------- /hands_on/bug_hunt/file_datastore.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | class FileDatastore: 5 | """Datastore based on a regular file system. 6 | 7 | Parameters 8 | ---------- 9 | base_path: str 10 | Filesystem path at which the data store is based. 11 | """ 12 | 13 | def __init__(self, base_path): 14 | if not os.path.exists(base_path): 15 | raise FileNotFoundError(f'Base path {base_path} does not exist') 16 | if not os.path.isdir(base_path): 17 | raise NotADirectoryError(f'Base path {base_path} exists but is not a directory') 18 | 19 | self.base_path = base_path 20 | 21 | def open(self, path, mode): 22 | """ Open a file-like object 23 | 24 | Parameters 25 | ---------- 26 | path: str 27 | Path relative to the root of the data store. 28 | mode: str 29 | Specifies the mode in which the file is opened. 30 | 31 | Returns 32 | ------- 33 | IO[Any] 34 | An open file-like object. 35 | """ 36 | path = os.path.join(self.base_path, path) 37 | return open(path, mode) 38 | 39 | def read(self, path): 40 | """ Read a sequence of bytes from the data store. 41 | 42 | Parameters 43 | ---------- 44 | path: str 45 | Path relative to the root of the data store. 46 | 47 | Returns 48 | ------- 49 | bytes 50 | The sequence of bytes read from `path`. 51 | """ 52 | with self.open(path, 'rb') as f: 53 | data = f.read() 54 | return data 55 | 56 | def write(self, path, data) -> None: 57 | """ Write a sequence of bytes to the data store. 58 | 59 | `path` contains the path relative to the root of the data store, including the name 60 | of the file to be created. If a file already exists at `path`, it is overwritten. 61 | 62 | Intermediate directories that do not exist will be created. 63 | 64 | Parameters 65 | ---------- 66 | path: str 67 | Path relative to the root of the data store. 68 | data: bytes 69 | Sequence of bytes to write at `path`. 70 | """ 71 | path = os.path.join(self.base_path, path) 72 | self.makedirs(os.path.dirname(path)) 73 | with self.open(path, 'wb') as f: 74 | f.write(data) 75 | 76 | def exists(self, path): 77 | """ Returns True if the file exists. 78 | 79 | Parameters 80 | ---------- 81 | path: str 82 | Path relative to the root of the data store. 83 | 84 | Returns 85 | ------- 86 | bool 87 | True if the file exists, false otherwise 88 | """ 89 | complete_path = os.path.join(self.base_path, path) 90 | return os.path.exists(complete_path) 91 | 92 | def makedirs(self, path): 93 | """ Creates the specified directory if needed. 94 | 95 | If the directories already exist, the method does not do anything. 96 | 97 | Parameters 98 | ---------- 99 | path: str 100 | Path relative to the root of the data store. 101 | """ 102 | complete_path = os.path.join(self.base_path, path) 103 | os.makedirs(complete_path, exist_ok=True) 104 | 105 | 106 | if __name__ == '__main__': 107 | data = b'A test! 012' 108 | datastore = FileDatastore(base_path='./datastore') 109 | datastore.write('a/mydata.bin', data) 110 | 111 | # This should pass! 112 | # The code works correctly if `base_path` is an absolute path :-( 113 | assert os.path.exists('./datastore/a/mydata.bin') 114 | -------------------------------------------------------------------------------- /hands_on/debugger/analyze_sums_and_differences.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "id": "30f3f04f", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "from debugger_example import sum_over_difference" 11 | ] 12 | }, 13 | { 14 | "cell_type": "code", 15 | "execution_count": null, 16 | "id": "3632cacb", 17 | "metadata": {}, 18 | "outputs": [], 19 | "source": [ 20 | "result = sum_over_difference(7, 5)\n", 21 | "print(result)" 22 | ] 23 | }, 24 | { 25 | "cell_type": "code", 26 | "execution_count": null, 27 | "id": "02577028", 28 | "metadata": {}, 29 | "outputs": [], 30 | "source": [ 31 | "result = sum_over_difference(12, 12)\n", 32 | "print(result)" 33 | ] 34 | }, 35 | { 36 | "cell_type": "code", 37 | "execution_count": null, 38 | "id": "58fc58c0", 39 | "metadata": {}, 40 | "outputs": [], 41 | "source": [] 42 | } 43 | ], 44 | "metadata": { 45 | "kernelspec": { 46 | "display_name": "Python 3 (ipykernel)", 47 | "language": "python", 48 | "name": "python3" 49 | }, 50 | "language_info": { 51 | "codemirror_mode": { 52 | "name": "ipython", 53 | "version": 3 54 | }, 55 | "file_extension": ".py", 56 | "mimetype": "text/x-python", 57 | "name": "python", 58 | "nbconvert_exporter": "python", 59 | "pygments_lexer": "ipython3", 60 | "version": "3.11.3" 61 | } 62 | }, 63 | "nbformat": 4, 64 | "nbformat_minor": 5 65 | } 66 | -------------------------------------------------------------------------------- /hands_on/debugger/debugger_example.py: -------------------------------------------------------------------------------- 1 | def add(arg1, arg2): 2 | return arg1 + arg2 3 | 4 | 5 | def sub(arg1, arg2): 6 | return arg1 - arg2 7 | 8 | 9 | def div(arg1, arg2): 10 | return arg1 / arg2 11 | 12 | 13 | def sum_over_difference(arg1, arg2): 14 | """Compute sum of arguments over difference of arguments.""" 15 | arg_sum = add(arg1, arg2) 16 | arg_diff = sub(arg1, arg2) 17 | sum_over_diff = div(arg_sum, arg_diff) 18 | return sum_over_diff 19 | 20 | 21 | if __name__ == '__main__': 22 | result = sum_over_difference(7, 5) 23 | print(result) 24 | -------------------------------------------------------------------------------- /hands_on/factorial/factorial.py: -------------------------------------------------------------------------------- 1 | """ Compute the factorial of a set of numbers stored in a file. """ 2 | 3 | def factorial(n): 4 | if n == 0: 5 | return 1 6 | else: 7 | return factorial(n-1) * n 8 | 9 | 10 | def read_data(filename): 11 | numbers = [] 12 | with open(filename, 'r') as f: 13 | for line in f: 14 | number = int(line) 15 | numbers.append(number) 16 | return numbers 17 | 18 | 19 | def compute_factorials_for_list(numbers): 20 | factorials = [] 21 | for number in numbers: 22 | result = factorial(number) 23 | factorials.append(result) 24 | return factorials 25 | 26 | 27 | def main(): 28 | numbers = read_data('numbers.txt') 29 | factorials = compute_factorials_for_list(numbers) 30 | 31 | 32 | if __name__ == '__main__': 33 | main() 34 | -------------------------------------------------------------------------------- /hands_on/factorial/test_factorial.py: -------------------------------------------------------------------------------- 1 | """ Tests for the factorial function. """ 2 | 3 | from factorial import factorial 4 | 5 | 6 | def test_factorial(): 7 | factorial_cases = [(1, 1), 8 | (0, 1), 9 | (5, 2*3*4*5), 10 | (30, 265252859812191058636308480000000)] 11 | 12 | for n, fact_n in factorial_cases: 13 | assert factorial(n) == fact_n 14 | -------------------------------------------------------------------------------- /hands_on/first/first.py: -------------------------------------------------------------------------------- 1 | def times_3(x): 2 | """ Multiply x by 3. 3 | 4 | Parameters 5 | ---------- 6 | x : The item to multiply by 3. 7 | """ 8 | return x * 3 9 | -------------------------------------------------------------------------------- /hands_on/first/test_first.py: -------------------------------------------------------------------------------- 1 | from first import times_3 2 | 3 | 4 | def test_times_3_integer(): 5 | value = 7 6 | expected = 21 7 | 8 | result = times_3(value) 9 | 10 | assert result == expected 11 | 12 | 13 | def test_times_3_string(): 14 | value = 'wow' 15 | expected = 'wowwowwow' 16 | 17 | result = times_3(value) 18 | 19 | assert result == expected 20 | -------------------------------------------------------------------------------- /hands_on/first_teacher/first.py: -------------------------------------------------------------------------------- 1 | def times_3(x): 2 | """ Multiply x by 3. 3 | 4 | Parameters 5 | ---------- 6 | x : The item to multiply by 3. 7 | """ 8 | return x * 3 9 | -------------------------------------------------------------------------------- /hands_on/local_maxima/local_maxima.py: -------------------------------------------------------------------------------- 1 | def find_maxima(x): 2 | """Find local maxima of x. 3 | 4 | Input arguments: 5 | x -- 1D list of real numbers 6 | 7 | Output: 8 | idx -- list of indices of the local maxima in x 9 | """ 10 | return [] 11 | -------------------------------------------------------------------------------- /hands_on/local_maxima_part2/local_maxima.py: -------------------------------------------------------------------------------- 1 | def find_maxima(x): 2 | """Find local maxima of x. 3 | 4 | Input arguments: 5 | x -- 1D list of real numbers 6 | 7 | Output: 8 | idx -- list of indices of the local maxima in x 9 | """ 10 | return [] 11 | -------------------------------------------------------------------------------- /hands_on/local_maxima_part2/test_local_maxima.py: -------------------------------------------------------------------------------- 1 | from local_maxima import find_maxima 2 | 3 | 4 | def test_find_maxima(): 5 | values = [1, 3, -2, 0, 2, 1] 6 | expected = [1, 4] 7 | maxima = find_maxima(values) 8 | assert maxima == expected 9 | 10 | 11 | def test_find_maxima_edges(): 12 | values = [4, 2, 1, 3, 1, 5] 13 | expected = [0, 3, 5] 14 | maxima = find_maxima(values) 15 | assert maxima == expected 16 | 17 | 18 | def test_find_maxima_empty(): 19 | values = [] 20 | expected = [] 21 | maxima = find_maxima(values) 22 | assert maxima == expected 23 | 24 | 25 | def test_find_maxima_plateau(): 26 | raise Exception('not yet implemented') 27 | 28 | 29 | def test_find_maxima_not_a_plateau(): 30 | raise Exception('not yet implemented') 31 | -------------------------------------------------------------------------------- /hands_on/logistic_fun/readme.md: -------------------------------------------------------------------------------- 1 | This exercise is in a separate repo! 2 | -------------------------------------------------------------------------------- /hands_on/numpy_equality/test_numpy_equality.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | 4 | def test_equality(): 5 | x = np.array([1, 1]) 6 | y = np.array([2, 2]) 7 | z = np.array([3, 3]) 8 | assert x + y == z 9 | 10 | 11 | def test_equality_with_nan(): 12 | x = np.array([1, np.nan]) 13 | y = np.array([2, np.nan]) 14 | z = np.array([3, np.nan]) 15 | assert x + y == z 16 | 17 | 18 | def test_allclose_with_nan(): 19 | x = np.array([1.1, np.nan]) 20 | y = np.array([2.2, np.nan]) 21 | z = np.array([3.3, np.nan]) 22 | assert x + y == z 23 | -------------------------------------------------------------------------------- /hands_on_solutions/first/first.py: -------------------------------------------------------------------------------- 1 | def times_3(x): 2 | """ Multiply x by 3. 3 | 4 | Parameters 5 | ---------- 6 | x : The item to multiply by 3. 7 | """ 8 | return x * 3 9 | -------------------------------------------------------------------------------- /hands_on_solutions/first/test_first.py: -------------------------------------------------------------------------------- 1 | from first import times_3 2 | 3 | 4 | def test_times_3_integer(): 5 | value = 7 6 | expected = 21 7 | 8 | result = times_3(value) 9 | 10 | assert result == expected 11 | 12 | 13 | def test_times_3_string(): 14 | value = 'wow' 15 | expected = 'wowwowwow' 16 | 17 | result = times_3(value) 18 | 19 | assert result == expected 20 | 21 | 22 | def test_times_3_list(): 23 | value = [1] 24 | expected = [1, 1, 1] 25 | 26 | result = times_3(value) 27 | 28 | assert result == expected 29 | -------------------------------------------------------------------------------- /hands_on_solutions/local_maxima/local_maxima.py: -------------------------------------------------------------------------------- 1 | def find_maxima(x): 2 | """Find local maxima of x. 3 | 4 | Example: 5 | >>> x = [1, 3, -2, 0, 2, 1] 6 | >>> find_maxima(x) 7 | [1, 4] 8 | 9 | If in a local maximum several elements have the same value, 10 | return the left-most index. 11 | Example: 12 | >>> x = [1, 2, 2, 1] 13 | >>> find_maxima(x) 14 | [1] 15 | 16 | Input arguments: 17 | x -- 1D list of real numbers 18 | 19 | Output: 20 | idx -- list of indices of the local maxima in x 21 | """ 22 | 23 | idx = [] 24 | up = False 25 | down = False 26 | for i in range(len(x)): 27 | if i == 0 or x[i-1] < x[i]: 28 | up = True 29 | up_idx = i 30 | elif x[i-1] > x[i]: 31 | up = False 32 | 33 | # if x[i-1] == x[i], no change 34 | 35 | if i+1 == len(x) or x[i+1] < x[i]: 36 | down = True 37 | elif x[i+1] > x[i]: 38 | down = False 39 | 40 | if up and down: 41 | idx.append(up_idx) 42 | 43 | return idx 44 | 45 | -------------------------------------------------------------------------------- /hands_on_solutions/local_maxima/test_local_maxima.py: -------------------------------------------------------------------------------- 1 | from local_maxima import find_maxima 2 | 3 | 4 | def test_find_maxima(): 5 | values = [1, 3, -2, 0, 2, 1] 6 | expected = [1, 4] 7 | maxima = find_maxima(values) 8 | assert maxima == expected 9 | 10 | 11 | def test_find_maxima_edges(): 12 | values = [4, 2, 1, 3, 1, 5] 13 | expected = [0, 3, 5] 14 | maxima = find_maxima(values) 15 | assert maxima == expected 16 | 17 | 18 | def test_find_maxima_empty(): 19 | values = [] 20 | expected = [] 21 | maxima = find_maxima(values) 22 | assert maxima == expected 23 | 24 | 25 | def test_find_maxima_plateau(): 26 | values = [1, 2, 2, 1] 27 | expected = [1] 28 | maxima = find_maxima(values) 29 | assert maxima == expected 30 | 31 | 32 | def test_find_maxima_not_a_plateau(): 33 | values = [1, 2, 2, 3, 1] 34 | expected = [3] 35 | maxima = find_maxima(values) 36 | assert maxima == expected 37 | -------------------------------------------------------------------------------- /hands_on_solutions/logistic_fun/bifurcation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ASPP/testing_debugging_profiling/a6585b649b3cb0230d256bc70cc86d58ac6507d4/hands_on_solutions/logistic_fun/bifurcation.png -------------------------------------------------------------------------------- /hands_on_solutions/logistic_fun/conftest.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pytest 3 | 4 | 5 | # add a commandline option to pytest 6 | def pytest_addoption(parser): 7 | """Add random seed option to py.test. 8 | """ 9 | parser.addoption('--seed', dest='seed', type=int, action='store', 10 | help='set random seed') 11 | 12 | 13 | # configure pytest to automatically set the rnd seed if not passed on CLI 14 | def pytest_configure(config): 15 | seed = config.getvalue("seed") 16 | # if seed was not set by the user, we set one now 17 | if seed is None or seed == ('NO', 'DEFAULT'): 18 | config.option.seed = int(np.random.randint(2**31-1)) 19 | 20 | 21 | def pytest_report_header(config): 22 | return f'Using random seed: {config.option.seed}' 23 | 24 | 25 | @pytest.fixture 26 | def random_state(request): 27 | random_state = np.random.RandomState(request.config.option.seed) 28 | return random_state 29 | -------------------------------------------------------------------------------- /hands_on_solutions/logistic_fun/conftest2.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pytest 3 | 4 | SEED = 42 5 | 6 | @pytest.fixture 7 | def random_state(): 8 | print(f'Seed: {SEED}') 9 | random_state = np.random.RandomState(SEED) 10 | return random_state 11 | -------------------------------------------------------------------------------- /hands_on_solutions/logistic_fun/logistic.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | 4 | def f(x, r): 5 | """ 6 | takes r and x as input and returns r*x*(1-x) 7 | """ 8 | return r * x * (1 - x) 9 | 10 | 11 | def iterate_f(it, xi, r): 12 | """ 13 | takes a number of iterations `it`, a starting value, 14 | and a parameter value for r. It should execute f repeatedly (it times), 15 | each time using the last result of f as the new input to f. Append each 16 | iteration's result to a list l. Finally, convert the list into a numpy 17 | array and return it. 18 | """ 19 | x = xi 20 | xs = [] 21 | for _ in range(it): 22 | x = f(x, r) 23 | xs.append(x) 24 | 25 | return np.array(xs) 26 | -------------------------------------------------------------------------------- /hands_on_solutions/logistic_fun/plot_logfun.py: -------------------------------------------------------------------------------- 1 | """Usage: 2 | ``` 3 | plot_trajectory(100, 3.6, 0.1) 4 | plot_bifurcation(2.5, 4.2, 0.001) 5 | ``` 6 | """ 7 | import numpy as np 8 | from matplotlib import pyplot as plt 9 | 10 | from logistic import iterate_f 11 | 12 | 13 | def plot_trajectory(n, r, x0, fname="single_trajectory.png"): 14 | """ 15 | Saves a plot of a single trajectory of the logistic function 16 | 17 | inputs 18 | n: int (number of iterations) 19 | r: float (r value for the logistic function) 20 | x0: float (between 0 and 1, starting point for the iteration) 21 | fname: str (filename to which to save the image) 22 | 23 | returns 24 | fig, ax (matplotlib objects) 25 | """ 26 | l = iterate_f(n, x0, r) 27 | fig, ax = plt.subplots(figsize=(10, 5)) 28 | ax.plot(list(range(n)), l) 29 | fig.suptitle('Logistic Function') 30 | 31 | fig.savefig(fname) 32 | return fig, ax 33 | 34 | 35 | def plot_bifurcation(start, end, step, fname="bifurcation.png", it=100000, 36 | last=300): 37 | """ 38 | Saves a plot of the bifurcation diagram of the logistic function. The 39 | `start`, `end`, and `step` parameters define for which r values to 40 | calculate the logistic function. If you space them too closely, it might 41 | take a very long time, if you dont plot enough, your bifurcation diagram 42 | won't be informative. Choose wisely! 43 | 44 | inputs 45 | start, end, step: float (which r values to calculate the logistic 46 | function for) 47 | fname: str (filename to which to save the image) 48 | it: int (how many iterations to run for each r value) 49 | last: int (how many of the last iterates to plot) 50 | 51 | 52 | returns 53 | fig, ax (matplotlib objects) 54 | """ 55 | r_range = np.arange(start, end, step) 56 | x = [] 57 | y = [] 58 | 59 | for r in r_range: 60 | l = iterate_f(it, 0.1, r) 61 | ll = l[len(l) - last::].copy() 62 | lll = np.unique(ll) 63 | y.extend(lll) 64 | x.extend(np.ones(len(lll)) * r) 65 | 66 | fig, ax = plt.subplots(figsize=(20, 10)) 67 | ax.scatter(x, y, s=0.1, color='k') 68 | ax.set_xlabel("r") 69 | fig.savefig(fname) 70 | return fig, ax 71 | -------------------------------------------------------------------------------- /hands_on_solutions/logistic_fun/plot_script.py: -------------------------------------------------------------------------------- 1 | from plot_logfun import plot_trajectory 2 | 3 | plot_trajectory(100, 3.4, 0.1) -------------------------------------------------------------------------------- /hands_on_solutions/logistic_fun/readme.md: -------------------------------------------------------------------------------- 1 | # Testing Project for ASPP 2023 Mexico 2 | 3 | ## Exercise 1 -- @parametrize and the logistic map 4 | 5 | Make a file `logistic.py` and `test_logistic.py` in the same folder as this 6 | readme and the `plot_logfun.py` file. Implement the code for the logistic map 7 | in the `logistic.py` file: 8 | 9 | a) Implement the logistic map f(𝑥)=𝑟∗𝑥∗(1−𝑥) . Use `@parametrize` 10 | to test the function for the following cases: 11 | ``` 12 | x=0.1, r=2.2 => f(x, r)=0.198 13 | x=0.2, r=3.4 => f(x, r)=0.544 14 | x=0.75, r=1.7 => f(x, r)=0.31875 15 | ``` 16 | 17 | b) Implement the function `iterate_f` that runs `f` for `it` 18 | iterations, each time passing the result back into f. 19 | Use `@parametrize` to test the function for the following cases: 20 | ``` 21 | x=0.1, r=2.2, it=1 => iterate_f(it, x, r)=[0.198] 22 | x=0.2, r=3.4, it=4 => f(x, r)=[0.544, 0.843418, 0.449019, 0.841163] 23 | x=0.75, r=1.7, it=2 => f(x, r)=[0.31875, 0.369152] 24 | ``` 25 | 26 | c) Import and call the `plot_trajectory` function from the `plot_logfun` 27 | module to look at the trajectories generated by your code. The `plot_logfun` 28 | imports and uses your `logistic.py` code. Import the module 29 | and call the function in a new `plot_script.py` file. 30 | 31 | Try with values `r<3`, `r>4` and `3`f` is a function and `x0` and `y0` are two possible seeds. 75 | >If `f` has SDIC then: 76 | >there is a number `delta` such that for any `x0` there is a `y0` that is not 77 | >more than `init_error` away from `x0`, where the initial condition `y0` has 78 | >the property that there is some integer n such that after n iterations, the 79 | >orbit is more than `delta` away from the orbit of `x0`. That is 80 | >|xn-yn| > delta 81 | 82 | -------------------------------------------------------------------------------- /hands_on_solutions/logistic_fun/single_trajectory.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ASPP/testing_debugging_profiling/a6585b649b3cb0230d256bc70cc86d58ac6507d4/hands_on_solutions/logistic_fun/single_trajectory.png -------------------------------------------------------------------------------- /hands_on_solutions/logistic_fun/test_logistic.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from numpy.testing import assert_allclose 3 | import pytest 4 | 5 | from logistic import f, iterate_f 6 | 7 | # set the random seed for once here 8 | SEED = np.random.randint(0, 2**31) 9 | 10 | 11 | @pytest.fixture 12 | def random_state(): 13 | print(f'Using seed {SEED}') 14 | random_state = np.random.RandomState(SEED) 15 | return random_state 16 | 17 | 18 | 19 | @pytest.mark.parametrize('a', [1, 2, 3]) 20 | @pytest.mark.parametrize('b', [5, 6, 7]) 21 | def test_addition_increases(a, b): 22 | print(a, b) 23 | assert b + a > a 24 | 25 | 26 | 27 | @pytest.mark.parametrize( 28 | 'x, r, expected', 29 | [ 30 | (0.1, 2.2, 0.198), 31 | (0.2, 3.4, 0.544), 32 | (0.75, 1.7, 0.31875), 33 | ] 34 | ) 35 | def test_f(x, r, expected): 36 | result = f(x, r) 37 | assert_allclose(result, expected) 38 | 39 | 40 | @pytest.mark.parametrize( 41 | 'x, r, it, expected', 42 | [ 43 | (0.1, 2.2, 1, [0.198]), 44 | (0.2, 3.4, 4, [0.544, 0.843418, 0.449019, 0.841163]), 45 | (0.75, 1.7, 2, [0.31875, 0.369152]), 46 | ] 47 | ) 48 | def test_iterate_f(x, r, it, expected): 49 | result = iterate_f(it, x, r) 50 | assert_allclose(result, expected, rtol=1e-5) 51 | 52 | 53 | def test_attractor_converges(): 54 | SEED = 42 55 | random_state = np.random.RandomState(SEED) 56 | 57 | for _ in range(100): 58 | x = random_state.uniform(0, 1) 59 | result = iterate_f(100, x, 1.5) 60 | assert_allclose(result[-1], 1 / 3) 61 | 62 | #################################################################### 63 | # These only work after adding the fixture 64 | #################################################################### 65 | 66 | 67 | @pytest.mark.xfail 68 | def test_attractor_converges2(random_state): 69 | for _ in range(100): 70 | x = random_state.uniform(0, 1) 71 | result = iterate_f(100, x, 1.5) 72 | assert_allclose(result[-1], 1 / 3) 73 | 74 | 75 | @pytest.mark.xfail 76 | def test_chaotic_behavior(random_state): 77 | r = 3.8 78 | for _ in range(10): 79 | x = random_state.uniform(0, 1) 80 | result = iterate_f(100000, x, r) 81 | assert np.all(result >= 0.0) 82 | assert np.all(result <= 1.0) 83 | assert min(np.abs(np.diff(result[-1000:]))) > 1e-6 84 | 85 | 86 | @pytest.mark.xfail 87 | def test_sensitivity_to_initial_conditions(random_state): 88 | """ 89 | `f` is a function and `x0` and `y0` are two possible seeds. 90 | If `f` has SDIC then: 91 | there is a number `delta` such that for any `x0` there is a `y0` that is 92 | not more than `init_error` away from `x0`, where the initial condition `y0` 93 | has the property that there is some integer n such that after n iterations, 94 | the orbit is more than `delta` away from the orbit of `x0`. That is 95 | |xn-yn| > delta 96 | """ 97 | delta = 0.1 98 | n = 10000 99 | x0 = random_state.rand() 100 | x0_diffs = random_state.rand(100) * 0.001 - 0.0005 101 | 102 | result_list = [] 103 | for x0_diff in x0_diffs: 104 | x1 = x0 + x0_diff 105 | l_x = iterate_f(n, x0, 3.8) 106 | l_y = iterate_f(n, x1, 3.8) 107 | result_list.append(any(abs(l_x - l_y) > delta)) 108 | assert any(result_list) 109 | -------------------------------------------------------------------------------- /hands_on_solutions/massmail_solution/massmail: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Script to send mass email 3 | # 4 | # Copyright (C) 2003-2017 Tiziano Zito , Jakob Jordan 5 | # 6 | # This script is free software and comes without any warranty, to 7 | # the extent permitted by applicable law. You can redistribute it 8 | # and/or modify it under the terms of the Do What The Fuck You Want 9 | # To Public License, Version 2, as published by Sam Hocevar. 10 | # http://www.wtfpl.net 11 | # 12 | # Full license text: 13 | # 14 | # DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 15 | # Version 2, December 2004. 16 | # 17 | # Everyone is permitted to copy and distribute verbatim or modified 18 | # copies of this license document, and changing it is allowed as long 19 | # as the name is changed. 20 | # 21 | # DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 22 | # TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 23 | # 24 | # 0. You just DO WHAT THE FUCK YOU WANT TO. 25 | 26 | import smtplib, getopt, sys, os, email, getpass 27 | import email.header 28 | import email.mime.text 29 | import tempfile 30 | 31 | from py.test import raises 32 | 33 | 34 | PROGNAME = os.path.basename(sys.argv[0]) 35 | USAGE = """Send mass mail 36 | Usage: 37 | %s [...] PARAMETER_FILE < BODY 38 | 39 | Options: 40 | -F FROM set the From: header for all messages. 41 | Must be ASCII. This argument is required 42 | 43 | -S SUBJECT set the Subject: header for all messages 44 | 45 | -B BCC set the Bcc: header for all messages. 46 | Must be ASCII 47 | 48 | -s SEPARATOR set field separator in parameter file, 49 | default: ";" 50 | 51 | -e ENCODING set PARAMETER_FILE *and* BODY character set 52 | encoding, default: "UTF-8". Note that if you fuck 53 | up this one, your email will be full of rubbish: 54 | You have been warned! 55 | 56 | -f fake run: don't really send emails, just print to 57 | standard output what would be done. Don't be scared 58 | if you can not read the body: it is base64 encoded 59 | UTF8 text 60 | 61 | -z SERVER the SMTP server to use. This argument is required 62 | 63 | -P PORT the SMTP port to use. 64 | 65 | -u SMTP user name. If not set, use anonymous SMTP 66 | connection 67 | 68 | -p SMTP password. If not set you will be prompted for one 69 | 70 | -h show this usage message 71 | 72 | Notes: 73 | The message body is read from standard input or 74 | typed in interactively (exit with ^D) and keywords are subsituted with values 75 | read from a parameter file. The first line of the parameter file defines 76 | the keywords. The keyword $EMAIL$ must always be present and contains a comma 77 | separated list of email addresses. 78 | Keep in mind shell escaping when setting headers with white spaces or special 79 | characters. 80 | Character set encodings are those supported by python. 81 | 82 | Examples: 83 | 84 | * Example of a parameter file: 85 | 86 | $NAME$; $SURNAME$; $EMAIL$ 87 | John; Smith; j@guys.com 88 | Anne; Joyce; a@girls.com 89 | 90 | * Example of body: 91 | 92 | Dear $NAME$ $SURNAME$, 93 | 94 | I think you are a great guy/girl! 95 | 96 | Cheers, 97 | 98 | My self. 99 | 100 | * Example usage: 101 | 102 | %s -F "Great Guy " -S "You are a great guy" -B "Not so great Guy " parameter-file < body 103 | 104 | """%(PROGNAME, PROGNAME) 105 | 106 | def error(s): 107 | sys.stderr.write(PROGNAME+': ') 108 | sys.stderr.write(s+'\n') 109 | sys.stderr.flush() 110 | sys.exit(-1) 111 | 112 | def parse_command_line_options(arguments): 113 | # parse options 114 | try: 115 | opts, args = getopt.getopt(arguments, "hfs:F:S:B:R:e:u:p:P:z:") 116 | except getopt.GetoptError, err: 117 | error(str(err)+USAGE) 118 | 119 | # set default options 120 | options = { 121 | 'sep': u';', 122 | 'fake': False, 123 | 'from': '', 124 | 'subject': '', 125 | 'bcc': '', 126 | 'encoding': 'utf-8', 127 | 'smtpuser': None, 128 | 'smtppassword': None, 129 | 'server': None, 130 | 'port': 0, 131 | 'in_reply_to': '', 132 | } 133 | 134 | for option, value in opts: 135 | if option == "-e": 136 | options['encoding'] = value 137 | if option == "-s": 138 | options['sep'] = value 139 | elif option == "-F": 140 | options['from'] = value 141 | elif option == "-S": 142 | options['subject'] = value 143 | elif option == "-B": 144 | options['bcc'] = value 145 | elif option == "-R": 146 | options['in_reply_to'] = value 147 | elif option == "-h": 148 | error(USAGE) 149 | elif option == "-f": 150 | options['fake'] = True 151 | elif option == "-u": 152 | options['smtpuser'] = value 153 | elif option == "-p": 154 | options['smtppassword'] = value 155 | elif option == "-P": 156 | options['port'] = int(value) 157 | elif option == "-z": 158 | options['server'] = value 159 | 160 | if len(args) == 0: 161 | error('You must specify a parameter file') 162 | 163 | if len(options['from']) == 0: 164 | error('You must set a from address with option -F') 165 | 166 | if options['server'] is None: 167 | error('You must set a SMTP server with option -z') 168 | 169 | if options['sep'] == ",": 170 | error('Separator can not be a comma') 171 | 172 | # get password if needed 173 | if options['smtpuser'] is not None and options['smtppassword'] is None: 174 | options['smtppassword'] = getpass.getpass('Enter password for %s: '%options['smtpuser']) 175 | 176 | # set filenames of parameter and mail body 177 | options['fn_parameters'] = args[0] 178 | 179 | return options 180 | 181 | def parse_parameter_file(options): 182 | pars_fh = open(options['fn_parameters'], 'rbU') 183 | pars = pars_fh.read() 184 | pars_fh.close() 185 | 186 | try: 187 | pars = unicode(pars, encoding=options['encoding']) 188 | except UnicodeDecodeError, e: 189 | error('Error decoding "'+options['fn_parameters']+'": '+str(e)) 190 | 191 | try: 192 | options['subject'] = unicode(options['subject'], encoding=options['encoding']) 193 | except UnicodeDecodeError, e: 194 | error('Error decoding SUBJECT: '+str(e)) 195 | 196 | try: 197 | options['from'] = unicode(options['from'], encoding=options['encoding']) 198 | except UnicodeDecodeError, e: 199 | error('Error decoding FROM: '+str(e)) 200 | 201 | if options['in_reply_to'] and not options['in_reply_to'].startswith('<'): 202 | options['in_reply_to'] = '<{}>'.format(options['in_reply_to']) 203 | 204 | # split lines 205 | pars = pars.splitlines() 206 | 207 | # get keywords from first line 208 | keywords_list = [key.strip() for key in pars[0].split(options['sep'])] 209 | 210 | # fail immediately if no EMAIL keyword is found 211 | if '$EMAIL$' not in keywords_list: 212 | error('No $EMAIL$ keyword found in %s'%options['fn_parameters']) 213 | 214 | # check that all keywords start and finish with a '$' character 215 | for key in keywords_list: 216 | if not key.startswith('$') or not key.endswith('$'): 217 | error(('Keyword "%s" malformed: should be $KEYWORD$'%key).encode(options['encoding'])) 218 | 219 | # gather all values 220 | email_count = 0 221 | keywords = dict([(key, []) for key in keywords_list]) 222 | for count, line in enumerate(pars[1:]): 223 | # ignore empty lines 224 | if len(line) == 0: 225 | continue 226 | values = [key.strip() for key in line.split(options['sep'])] 227 | if len(values) != len(keywords_list): 228 | error(('Line %d in "%s" malformed: %d values found instead of' 229 | ' %d: %s'%(count+1,options['fn_parameters'],len(values),len(keywords_list),line)).encode(options['encoding'])) 230 | for i, key in enumerate(keywords_list): 231 | keywords[key].append(values[i]) 232 | email_count += 1 233 | 234 | return keywords, email_count 235 | 236 | def create_email_bodies(options, keywords, email_count, body): 237 | try: 238 | body = unicode(body, encoding=options['encoding']) 239 | except UnicodeDecodeError, e: 240 | error('Error decoding email body: '+str(e)) 241 | 242 | # find keywords and substitute with values 243 | # we need to create email_count bodies 244 | msgs = {} 245 | 246 | for i in range(email_count): 247 | lbody = body 248 | for key in keywords: 249 | lbody = lbody.replace(key, keywords[key][i]) 250 | 251 | # Any single dollar left? That means that the body was malformed 252 | single_dollar_exists = lbody.count('$') != 2 * lbody.count('$$') 253 | if single_dollar_exists: 254 | raise ValueError('Malformed email body: unclosed placeholder or non-escaped dollar sign.') 255 | 256 | # Replace double dollars with single dollars 257 | lbody = lbody.replace('$$', '$') 258 | 259 | # encode body 260 | lbody = email.mime.text.MIMEText(lbody.encode(options['encoding']), 'plain', options['encoding']) 261 | msgs[keywords['$EMAIL$'][i]] = lbody 262 | 263 | return msgs 264 | 265 | def add_email_headers(options, msgs): 266 | # msgs is now a dictionary with {emailaddr:body} 267 | # we need to add the headers 268 | 269 | for emailaddr in msgs: 270 | msg = msgs[emailaddr] 271 | msg['To'] = str(emailaddr) 272 | msg['From'] = email.header.Header(options['from']) 273 | if options['subject']: 274 | msg['Subject'] = email.header.Header(options['subject'].encode(options['encoding']), options['encoding']) 275 | if options['in_reply_to']: 276 | msg['In-Reply-To'] = email.header.Header(options['in_reply_to']) 277 | msgs[emailaddr] = msg 278 | 279 | return None 280 | 281 | def send_messages(options, msgs): 282 | server = smtplib.SMTP(options['server'], port=options['port']) 283 | 284 | if options['smtpuser'] is not None: 285 | server.starttls() 286 | server.login(options['smtpuser'], options['smtppassword']) 287 | 288 | for emailaddr in msgs: 289 | print 'Sending email to:', emailaddr 290 | emails = [e.strip() for e in emailaddr.split(',')] 291 | if len(options['bcc']) > 0: 292 | emails.append(options['bcc']) 293 | if options['fake']: 294 | print msgs[emailaddr].as_string() 295 | else: 296 | try: 297 | out = server.sendmail(options['from'], emails, msgs[emailaddr].as_string()) 298 | except Exception, err: 299 | error(str(err)) 300 | 301 | if len(out) != 0: 302 | error(str(out)) 303 | 304 | server.close() 305 | 306 | def test_dummy(): 307 | pass 308 | 309 | def test_parse_parameter_file(): 310 | expected_keywords = {u'$VALUE$': [u'this is a test'], u'$EMAIL$': [u'testrecv@test']} 311 | with tempfile.NamedTemporaryFile() as f: 312 | f.write('$EMAIL$;$VALUE$\ntestrecv@test;this is a test') 313 | f.flush() 314 | cmd_options = [ 315 | '-F', 'testfrom@test', 316 | '-z', 'localhost', 317 | f.name, 318 | ] 319 | options = parse_command_line_options(cmd_options) 320 | keywords, email_count = parse_parameter_file(options) 321 | assert keywords == expected_keywords 322 | 323 | def test_local_sending(): 324 | parameter_string = '$EMAIL$;$NAME$;$VALUE$\ntestrecv@test.org;TestName;531' 325 | email_body = 'Dear $NAME$,\nthis is a test: $VALUE$\nBest regards' 326 | email_to = 'testrecv@test.org' 327 | email_from = 'testfrom@test.org' 328 | email_subject = 'Test Subject' 329 | email_encoding = 'utf-8' 330 | 331 | expected_email = email.mime.text.MIMEText('Dear TestName,\nthis is a test: 531\nBest regards'.encode(email_encoding), 'plain', email_encoding) 332 | expected_email['To'] = email_to 333 | expected_email['From'] = email_from 334 | expected_email['Subject'] = email.header.Header(email_subject.encode(email_encoding), email_encoding) 335 | 336 | with tempfile.NamedTemporaryFile() as f: 337 | f.write(parameter_string) 338 | f.flush() 339 | cmd_options = [ 340 | '-F', email_from, 341 | '-S', email_subject, 342 | '-z', 'localhost', 343 | '-e', email_encoding, 344 | f.name 345 | ] 346 | options = parse_command_line_options(cmd_options) 347 | keywords, email_count = parse_parameter_file(options) 348 | msgs = create_email_bodies(options, keywords, email_count, email_body) 349 | add_email_headers(options, msgs) 350 | assert msgs['testrecv@test.org'].as_string() == expected_email.as_string() 351 | 352 | def test_malformed_body(): 353 | parameter_string = '$EMAIL$;$NAME$;$VALUE$\ntestrecv@test.org;TestName;531' 354 | email_body = '$NAME$VALUE$' 355 | email_from = 'testfrom@test.org' 356 | email_subject = 'Test Subject' 357 | email_encoding = 'utf-8' 358 | 359 | with tempfile.NamedTemporaryFile() as f: 360 | f.write(parameter_string) 361 | f.flush() 362 | cmd_options = [ 363 | '-F', email_from, 364 | '-S', email_subject, 365 | '-z', 'localhost', 366 | '-e', email_encoding, 367 | f.name 368 | ] 369 | options = parse_command_line_options(cmd_options) 370 | keywords, email_count = parse_parameter_file(options) 371 | with raises(ValueError): 372 | msgs = create_email_bodies(options, keywords, email_count, email_body) 373 | 374 | def test_double_dollar(): 375 | parameter_string = '$EMAIL$;$NAME$;$VALUE$\ntestrecv@test.org;TestName;531' 376 | email_body = 'Dear $NAME$,\nyou owe us 254$$' 377 | email_to = 'testrecv@test.org' 378 | email_from = 'testfrom@test.org' 379 | email_subject = 'Test Subject' 380 | email_encoding = 'utf-8' 381 | 382 | expected_email = email.mime.text.MIMEText('Dear TestName,\nyou owe us 254$'.encode(email_encoding), 'plain', email_encoding) 383 | expected_email['To'] = email_to 384 | expected_email['From'] = email_from 385 | expected_email['Subject'] = email.header.Header(email_subject.encode(email_encoding), email_encoding) 386 | 387 | with tempfile.NamedTemporaryFile() as f: 388 | f.write(parameter_string) 389 | f.flush() 390 | cmd_options = [ 391 | '-F', email_from, 392 | '-S', email_subject, 393 | '-z', 'localhost', 394 | '-e', email_encoding, 395 | f.name 396 | ] 397 | options = parse_command_line_options(cmd_options) 398 | keywords, email_count = parse_parameter_file(options) 399 | msgs = create_email_bodies(options, keywords, email_count, email_body) 400 | assert msgs['testrecv@test.org'].get_payload(decode=email_encoding) == expected_email.get_payload(decode=email_encoding) 401 | 402 | 403 | if __name__ == '__main__': 404 | options = parse_command_line_options(sys.argv[1:]) 405 | keywords, email_count = parse_parameter_file(options) 406 | msgs = create_email_bodies(options, keywords, email_count, sys.stdin.read()) 407 | add_email_headers(options, msgs) 408 | send_messages(options, msgs) 409 | -------------------------------------------------------------------------------- /hands_on_solutions/numerical_fuzzing/test_var.py: -------------------------------------------------------------------------------- 1 | from math import isclose 2 | 3 | import numpy 4 | 5 | 6 | def test_var_deterministic(): 7 | x = numpy.array([-2.0, 2.0]) 8 | expected = 4.0 9 | assert isclose(numpy.var(x), expected) 10 | 11 | 12 | def test_var_fuzzing(): 13 | rand_state = numpy.random.RandomState(8393) 14 | 15 | N, D = 100000, 5 16 | # Goal variances: [0.1 , 0.45, 0.8 , 1.15, 1.5] 17 | expected = numpy.linspace(0.1, 1.5, D) 18 | 19 | # Generate random, D-dimensional data 20 | x = rand_state.randn(N, D) * numpy.sqrt(expected) 21 | variance = numpy.var(x, axis=0) 22 | numpy.testing.assert_allclose(variance, expected, rtol=1e-2) 23 | -------------------------------------------------------------------------------- /hands_on_solutions/numpy_equality/test_numpy_equality.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from numpy.testing import assert_allclose, assert_equal 3 | 4 | 5 | def test_equality(): 6 | x = np.array([1, 1]) 7 | y = np.array([2, 2]) 8 | z = np.array([3, 3]) 9 | assert_equal(x + y, z) 10 | 11 | 12 | def test_equality_with_nan(): 13 | x = np.array([1, np.nan]) 14 | y = np.array([2, np.nan]) 15 | z = np.array([3, np.nan]) 16 | assert_equal(x + y, z) 17 | 18 | 19 | def test_allclose_with_nan(): 20 | x = np.array([1.1, np.nan]) 21 | y = np.array([2.2, np.nan]) 22 | z = np.array([3.3, np.nan]) 23 | assert_allclose(x + y, z) 24 | -------------------------------------------------------------------------------- /how_I_compiled_qcachegrind_on_osx.txt: -------------------------------------------------------------------------------- 1 | 1) Install dependencies with MacPorts: 2 | sudo port install qt4-mac graphviz 3 | 4 | Get source code for qcachegrind: 5 | 6 | 2) Get kcachegrind-0.7.4.tar.gz from http://kcachegrind.sourceforge.net/html/Download.html 7 | 8 | 3) Unzip it: 9 | tar xfz ~/Downloads/kcachegrind-0.7.4.tar.gz 10 | 11 | We need "qmake" to find the right libraries 12 | 13 | 4) Set PATH to find the right "qmake": 14 | export PATH=/opt/local/libexec/qt4/bin:/opt/local/bin:/opt/local/sbin:$PATH 15 | 16 | 5) Set library path: 17 | export DYLD_LIBRARY_PATH=/opt/local/libexec/qt4/Library/Frameworks/ 18 | 19 | Compile: 20 | 21 | 6) Compile: 22 | cd kcachegrind-0.7.4/ 23 | cd qcachegrind/ 24 | qmake -nocache 25 | make 26 | mv qcachegrind.app /Applications/ 27 | 28 | Done! 29 | 30 | See also http://blogs.perl.org/users/rurban/2013/04/install-kachegrind-on-macosx-with-ports.html 31 | -------------------------------------------------------------------------------- /material/code.py: -------------------------------------------------------------------------------- 1 | """ 2 | This is code that is copy-and-pasted in the slides. 3 | """ 4 | 5 | 6 | def test_arithmetic(): 7 | assert 1 == 1 8 | assert 2 * 3 == 6 9 | 10 | def test_len_list(): 11 | lst = ['a', 'b', 'c'] 12 | assert len(lst) == 3 13 | 14 | def test_various(): 15 | assert 'Hi'.islower() 16 | assert 2 + 1 == 3 17 | assert [2] + [1] == [2, 1] 18 | assert 'a' + 'b' != 'ab' 19 | 20 | 21 | def test_1_and_2(): 22 | assert 1 + 2 == 3 23 | 24 | def test_1_and_2_float(): 25 | assert 1.1 + 2.2 == 3.3 26 | 27 | 28 | from math import isclose 29 | 30 | def test_floating_point_math(): 31 | assert isclose(1.1 + 2.2, 3.3) 32 | 33 | def test_floating_point_math2(): 34 | assert isclose(1.121, 1.2, abs_tol=1e-1) 35 | assert isclose(1.121, 1.2, abs_tol=1e-2) 36 | 37 | def test_floating_point_math3(): 38 | assert isclose(120.1, 121.4, rel_tol=1e-1) 39 | assert isclose(120.4, 121.4, rel_tol=1e-2) 40 | 41 | import numpy 42 | 43 | def test_numpy_equality(): 44 | x = numpy.array([1, 1]) 45 | y = numpy.array([2, 2]) 46 | z = numpy.array([3, 3]) 47 | assert x + y == z 48 | 49 | 50 | from py.test import raises 51 | 52 | class SomeException(Exception): 53 | pass 54 | 55 | 56 | do_something = do_something_else = lambda : 1 57 | 58 | 59 | def test_raises(): 60 | with raises(SomeException): 61 | do_something() 62 | do_something_else() 63 | 64 | 65 | def test_raises2(): 66 | with raises(ValueError): 67 | int('XYZ') 68 | 69 | 70 | def test_lower(): 71 | # Given 72 | string = 'HeLlO wOrld' 73 | expected = 'hello world' 74 | 75 | # When 76 | output = string.lower() 77 | 78 | # Then 79 | assert output == expected 80 | 81 | 82 | def test_lower_empty_string(): 83 | # Given 84 | string = '' 85 | expected = '' 86 | 87 | # When 88 | output = string.lower() 89 | 90 | # Then 91 | assert output == expected 92 | 93 | 94 | def test_lower1(): 95 | # Given 96 | # Each test case is a tuple of (input, expected_result) 97 | test_cases = [('HeLlO wOrld', 'hello world'), 98 | ('hi', 'hi'), 99 | ('123 ([?', '123 ([?'), 100 | ('', '')] 101 | 102 | for string, expected in test_cases: 103 | # When 104 | output = string.lower() 105 | # Then 106 | assert output == expected 107 | -------------------------------------------------------------------------------- /profiling.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ASPP/testing_debugging_profiling/a6585b649b3cb0230d256bc70cc86d58ac6507d4/profiling.pptx -------------------------------------------------------------------------------- /testing_part1.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ASPP/testing_debugging_profiling/a6585b649b3cb0230d256bc70cc86d58ac6507d4/testing_part1.pptx -------------------------------------------------------------------------------- /testing_part2.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ASPP/testing_debugging_profiling/a6585b649b3cb0230d256bc70cc86d58ac6507d4/testing_part2.pptx --------------------------------------------------------------------------------