├── .github └── workflows │ └── pythonpackage.yml ├── .gitignore ├── .hgignore ├── CHANGELOG.rst ├── LICENSE ├── MANIFEST.in ├── README.rst ├── pytest.ini ├── pytest_quickcheck ├── __init__.py ├── data.py ├── generator.py └── plugin.py ├── setup.cfg ├── setup.py ├── tests ├── python3 │ ├── test_plugin_annotation.py │ └── test_regression.py ├── test_list_generators.py ├── test_listof_realworld.py ├── test_misc.py ├── test_plugin_basic.py ├── test_plugin_basic_with_substitution.py ├── test_plugin_basic_with_type.py └── test_plugin_extend.py └── tox.ini /.github/workflows/pythonpackage.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: Python package 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | strategy: 17 | matrix: 18 | python-version: [3.7, 3.8, 3.9, '3.10'] 19 | 20 | steps: 21 | - uses: actions/checkout@v2 22 | - name: Set up Python ${{ matrix.python-version }} 23 | uses: actions/setup-python@v1 24 | with: 25 | python-version: ${{ matrix.python-version }} 26 | - name: Install dependencies 27 | run: | 28 | python -m pip install --upgrade pip 29 | python setup.py develop 30 | - name: Test with pytest 31 | run: | 32 | pip install pytest pytest-pycodestyle pytest-flakes 33 | py.test -v --pycodestyle --flakes pytest_quickcheck tests 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | -------------------------------------------------------------------------------- /.hgignore: -------------------------------------------------------------------------------- 1 | .cache 2 | .coverage 3 | .installed.cfg 4 | .pyc 5 | .pyo 6 | .swp 7 | .tox 8 | build 9 | htmlcov 10 | develop-eggs 11 | dist 12 | eggs 13 | parts 14 | .*.egg-info 15 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | 2 | ChangeLog 3 | ========= 4 | 5 | 0.9.0 (2022-11-06) 6 | ------------------ 7 | 8 | * support pytest > 6.0 9 | * drop supporting python 3.6 10 | 11 | 0.8.6 (2020-11-15) 12 | ------------------ 13 | 14 | * fix ignored ncalls parameter when a function annotation is used 15 | * change to be able to use the same argument in randomize marker and function annotation 16 | 17 | 0.8.5 (2020-09-19) 18 | ------------------ 19 | 20 | * fix a critical issue pytest cannot detect randomize marker 21 | * drop supporting pytest < 4.0.0 22 | * drop supporting python 3.5 23 | 24 | 0.8.4 (2020-03-06) 25 | ------------------ 26 | 27 | * fix an issue related to pytest-4.x/5.x 28 | * drop supporting python 3.3 and 3.4 29 | 30 | 0.8.3 (2017-05-27) 31 | ------------------ 32 | 33 | * fix an issue related to pytest-3.1.0 34 | * drop supporting python 2.6 and 3.2 35 | 36 | 0.8.2 (2015-03-02) 37 | ------------------ 38 | 39 | * transfer the code repository to pytest-dev 40 | 41 | 0.8.1 (2014-12-25) 42 | ------------------ 43 | 44 | * support min_length for str data type 45 | * removed distribute dependency 46 | * add pytest-flakes testing 47 | 48 | 0.8 (2013-12-08) 49 | ---------------- 50 | 51 | * fix use the parameter length for string generator even if the set of 52 | available characters is less than it (#2) 53 | 54 | * support new feature: Generating Collections from sonoflilit 55 | 56 | 0.7 (2012-10-20) 57 | ---------------- 58 | 59 | * the types in the arguments are specified by the types themselves (#1) 60 | 61 | 0.6 (2012-03-29) 62 | ---------------- 63 | * add generating data feature from function annotation 64 | 65 | 0.5 (2012-03-18) 66 | ---------------- 67 | * first release 68 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.ini 2 | include *.rst 3 | include LICENSE 4 | include MANIFEST.in 5 | recursive-include pytest_quickcheck *.py 6 | recursive-include tests *.py 7 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Requirements 2 | ------------ 3 | 4 | * Python 2.7 or 3.7 and later 5 | 6 | Features 7 | -------- 8 | 9 | * Provide **pytest.mark.randomize** function for generating random test data 10 | 11 | Installation 12 | ============ 13 | 14 | :: 15 | 16 | $ pip install pytest-quickcheck 17 | 18 | Quick Start 19 | =========== 20 | 21 | Just pass the signature of function to *randomize* marker. 22 | The signature is represented a tuple consist of argument name and its type. 23 | 24 | :: 25 | 26 | @pytest.mark.randomize(i1=int, i2=int, ncalls=1) 27 | def test_generate_ints(i1, i2): 28 | pass 29 | 30 | More complex data structure:: 31 | 32 | @pytest.mark.randomize( 33 | d1={'x': int, 'y': [str, (int, int)], 'z': {'x': str}} 34 | ) 35 | def test_generate_dict(d1): 36 | pass 37 | 38 | The *randomize* marker is able to use with *parametrize* marker. 39 | 40 | :: 41 | 42 | @pytest.mark.parametrize("prime", [2, 3, 5]) 43 | @pytest.mark.randomize(i1=int, f1=float, ncalls=1) 44 | def test_gen_parametrize_with_randomize_int_float(prime, i1, f1): 45 | pass 46 | 47 | Using command line option ``--randomize`` restricts only the *randomize* test. 48 | 49 | :: 50 | 51 | $ py.test -v --randomize test_option.py 52 | ========================================================================================== 53 | test session starts 54 | ========================================================================================== 55 | test_option.py:5: test_normal SKIPPED 56 | test_option.py:8: test_generate_ints[74-22] PASSED 57 | 58 | Usage 59 | ===== 60 | 61 | There some options for each data type:: 62 | 63 | $ py.test --markers 64 | @pytest.mark.randomize(argname=type, **options): mark the test function with 65 | random data generating any data type. 66 | There are options for each data type: (see doc for details) 67 | int: ['min_num', 'max_num'] 68 | float: ['min_num', 'max_num', 'positive'] 69 | str: ['encoding', 'fixed_length', 'min_length', 'max_length', 'str_attrs'] 70 | list_of, nonempty_list_of, dict_of: ['items', 'min_items', 'max_items'] 71 | 72 | * common option 73 | 74 | | **ncalls**: set the number of calls. Defaults to 3. (e.g. ncalls=5) 75 | | **choices**: choose from given sequence. (e.g. choices=[3, 5, 7]) 76 | 77 | * int 78 | 79 | | **min_num**: lower limit for generating integer number. (e.g. min_num=0) 80 | | **max_num**: upper limit for generating integer number. (e.g. max_num=10) 81 | 82 | * float 83 | 84 | | **min_num**: lower limit for generating real number. (e.g. min_num=0.0) 85 | | **max_num**: upper limit for generating real number. (e.g. max_num=1.0) 86 | | **positive**: generate only positive real number if set to `True`. 87 | Defaults to `False`. (e.g. positive=True) 88 | 89 | * str 90 | 91 | | **encoding**: generate unicode string encoded given character code. 92 | (e.g. encoding="utf-8") # for Python 2.x only 93 | | **fixed_length**: generate fixed length string. (e.g. fixed_length=8) 94 | | **max_length**: generate the string less than or equal to max length 95 | (e.g. max_length=32) 96 | | **str_attrs**: generate the string in given letters. 97 | set a tuple consist of attribute names in the `string module`_. 98 | (e.g. str_attrs=("digits", "punctuation") 99 | 100 | * list_of, nonempty_list_of, dict_of 101 | 102 | | **items**: number of items. 103 | | **min_items**: lower limit on number of items. 104 | | **max_items**: upper limit on number of items. 105 | 106 | Probably, `tests/test_plugin_basic.py` is useful for 107 | learning how to use these options. 108 | 109 | .. _string module: http://docs.python.org/library/string.html 110 | 111 | Generating Collections 112 | ====================== 113 | 114 | To generate a variable length list of items:: 115 | 116 | from pytest import list_of 117 | 118 | @pytest.mark.randomize(l=list_of(int)) 119 | def test_list_of(l): 120 | pass 121 | 122 | You can control its size with the ``items``, ``min_items`` and 123 | ``max_items`` options, or use the ``nonempty_list_of`` shortcut. 124 | 125 | :: 126 | 127 | @pytest.mark.randomize(l=list_of(int, num_items=10)) 128 | def test_list_of_length(l): 129 | assert len(l) == 10 130 | 131 | @pytest.mark.randomize(l=list_of(int, min_items=10, max_items=100)) 132 | def test_list_of_minimum_length(l): 133 | assert len(l) >= 10 134 | 135 | from pytest import nonempty_list_of 136 | 137 | @pytest.mark.randomize(l=nonempty_list_of(int) 138 | def test_list_of_minimum_length(l): 139 | assert len(l) >= 1 140 | 141 | Options for data types work as usual:: 142 | 143 | @pytest.mark.randomize(l=list_of(str, num_items=10), choices=["a", "b", "c"]) 144 | def test_list_of(l): 145 | assert l[0] in ["a", "b", "c"] 146 | 147 | (Note what goes into the ``list_of()`` call and what goes outside.) 148 | 149 | You can also generate a dict:: 150 | 151 | from pytest import dict_of 152 | @pytest.mark.randomize(d=dict_of(str, int)) 153 | def test_list_of(l): 154 | pass 155 | 156 | 157 | Python 3 158 | ======== 159 | 160 | For Python 3, the signature of function is given as function annotation. 161 | 162 | :: 163 | 164 | @pytest.mark.randomize(min_num=0, max_num=2, ncalls=5) 165 | def test_generate_int_anns(i1: int): 166 | pass 167 | 168 | Mixed representation is also OK, but it might not be useful. 169 | 170 | :: 171 | 172 | @pytest.mark.randomize(i1=int, fixed_length=8) 173 | def test_generate_arg_anns_mixed(i1, s1: str): 174 | pass 175 | 176 | See also: `PEP 3107 -- Function Annotations`_ 177 | 178 | .. _PEP 3107 -- Function Annotations: http://www.python.org/dev/peps/pep-3107/ 179 | 180 | Backward Compatibility 181 | ====================== 182 | 183 | Under 0.6 version, types were specified by strings containing the name 184 | of the type. It's still supported if you like. 185 | 186 | :: 187 | 188 | @pytest.mark.randomize(("i1", "int"), ("i2", "int"), ncalls=1) 189 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | flakes-ignore = 3 | plugin.py UnusedImport 4 | test_plugin_annotation.py UndefinedName 5 | -------------------------------------------------------------------------------- /pytest_quickcheck/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/t2y/pytest-quickcheck/48e853e61f03a08d76f47eee16fb44ab24f3fc93/pytest_quickcheck/__init__.py -------------------------------------------------------------------------------- /pytest_quickcheck/data.py: -------------------------------------------------------------------------------- 1 | from pytest_quickcheck.generator import Generator, get_int 2 | 3 | 4 | class listof(Generator): 5 | def __init__(self, data, **options): 6 | self.data = data 7 | self.min_num = options.pop("min_num", 0) 8 | assert self.min_num >= 0 9 | self.max_num = options.pop("max_num", 20) 10 | self.options = options 11 | 12 | def generate(self, **kwargs): 13 | kwargs.update(self.options) 14 | k = get_int(self.min_num, self.max_num) 15 | return [self.generate_data(self.data, **kwargs) for _ in range(k)] 16 | 17 | 18 | def listof1(data, **options): 19 | options.setdefault("min_num", 1) 20 | return listof(data, **options) 21 | -------------------------------------------------------------------------------- /pytest_quickcheck/generator.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import random 3 | import sys 4 | import string 5 | from functools import wraps 6 | from itertools import cycle 7 | from math import log, exp 8 | from operator import getitem 9 | 10 | IS_PY3 = sys.version_info[0] == 3 11 | if IS_PY3: 12 | unicode = str # FIXME: consider later 13 | from string import ascii_letters 14 | _MIN_INT = -sys.maxsize - 1 15 | _MAX_INT = sys.maxsize 16 | else: 17 | from string import letters as ascii_letters 18 | _MIN_INT = -sys.maxint - 1 19 | _MAX_INT = sys.maxint 20 | 21 | DATA_TYPE_OPTION = { 22 | "common": ["ncalls", "choices"], 23 | "int": ["min_num", "max_num"], 24 | "float": ["min_num", "max_num", "positive"], 25 | "str": [ 26 | "encoding", "fixed_length", "min_length", "max_length", "str_attrs" 27 | ], 28 | "*_of": ["items", "min_items", "max_items"], 29 | } 30 | 31 | DATA_TYPE_OPTIONS = set() 32 | DATA_TYPE_OPTIONS.update(*DATA_TYPE_OPTION.values()) 33 | 34 | _MIN_FLOAT = -1e7 35 | _MAX_FLOAT = 1e7 36 | _MIN_FLOAT_MAG = 1e-7 37 | _MAX_FLOAT_MAG = 1e+7 38 | 39 | _ASCII = ascii_letters 40 | _ASCII_LEN = len(_ASCII) - 1 41 | 42 | _BOOL_CYCLE = cycle([True, False]) 43 | 44 | 45 | def choice_data(func): 46 | def _choice_data(*args, **kwargs): 47 | choices = kwargs.get("choices") 48 | if choices: 49 | return random.choice(choices) 50 | else: 51 | return func(*args, **kwargs) 52 | return _choice_data 53 | 54 | def sanitize_option(data_def): 55 | def _sanitize_option(func): 56 | @wraps(func) 57 | def __sanitize_option(*args, **kwargs): 58 | sanitized_kwargs = {} 59 | for key in DATA_TYPE_OPTION[data_def]: 60 | value = kwargs.get(key) 61 | if value is not None: 62 | sanitized_kwargs[key] = value 63 | return func(*args, **sanitized_kwargs) 64 | return __sanitize_option 65 | return _sanitize_option 66 | 67 | @choice_data 68 | @sanitize_option("int") 69 | def get_int(min_num=_MIN_INT, max_num=_MAX_INT): 70 | return random.randint(min_num, max_num) 71 | 72 | @choice_data 73 | @sanitize_option("float") 74 | def get_float(min_num=_MIN_FLOAT, max_num=_MAX_FLOAT, 75 | min_mag=_MIN_FLOAT_MAG, max_mag=_MAX_FLOAT_MAG, positive=False): 76 | if positive: 77 | min_mag, max_mag = log(min_mag), log(max_mag) 78 | scale_range = max_mag - min_mag 79 | return exp(random.random() * scale_range + min_mag) 80 | else: 81 | length = max_num - min_num 82 | return random.random() * length + min_num 83 | 84 | @choice_data 85 | @sanitize_option("str") 86 | def get_str(encoding=None, fixed_length=None, min_length=0, 87 | max_length=32, str_attrs=None): 88 | base, end = str_attrs if str_attrs else (_ASCII, _ASCII_LEN) 89 | length = fixed_length if fixed_length else random.randint( 90 | min_length, max_length 91 | ) 92 | s = "".join(getitem(base, random.randint(0, end)) for _ in range(length)) 93 | if encoding and not IS_PY3: 94 | s = unicode(s, encoding) 95 | return s 96 | 97 | def get_unicode(**kwargs): 98 | if not kwargs.get("encoding"): 99 | kwargs["encoding"] = "utf-8" 100 | return get_str(**kwargs) 101 | 102 | def get_bool(): 103 | return next(_BOOL_CYCLE) 104 | 105 | 106 | class OptionOptimizer(object): 107 | 108 | def __init__(self, func): 109 | self.func = func 110 | self.cache = {} 111 | self.need_optimize = { 112 | "str_attrs": self.optimize_str_attrs, 113 | } 114 | 115 | def __call__(self, *args, **kwargs): 116 | for key in self.need_optimize: 117 | value = kwargs.get(key) 118 | if value: 119 | try: 120 | kwargs[key] = self.cache[value] 121 | except KeyError: 122 | optimized_value = self.need_optimize[key](value) 123 | kwargs[key] = self.cache[value] = optimized_value 124 | return self.func(*args, **kwargs) 125 | 126 | def optimize_str_attrs(self, attrs): 127 | base = "".join(getattr(string, attr) for attr in attrs) 128 | return base, len(base) - 1 129 | 130 | @OptionOptimizer 131 | def generate(data, **kwargs): 132 | if data is int: 133 | yield get_int(**kwargs) 134 | elif data is float: 135 | yield get_float(**kwargs) 136 | elif data is str: 137 | yield get_str(**kwargs) 138 | elif data is unicode: 139 | yield get_unicode(**kwargs) 140 | elif data is bool: 141 | yield get_bool() 142 | elif isinstance(data, (list, set, tuple)): 143 | for value in data: 144 | yield retrieve_func(value)(generate(value, **kwargs)) 145 | elif isinstance(data, dict): 146 | _dict = data.copy() 147 | for key, value in data.items(): 148 | _dict[key] = retrieve_func(value)(generate(value, **kwargs)) 149 | yield _dict 150 | elif isinstance(data, Generator): 151 | yield data.generate(**kwargs) 152 | elif data is None: 153 | yield None 154 | else: 155 | raise NotImplementedError("Unknown data type: %s" % data) 156 | 157 | class Generator(object): 158 | 159 | def __init__(self, **options): 160 | self.options = options 161 | 162 | def generate(self, **kwargs): 163 | raise NotImplementedError() 164 | 165 | @staticmethod 166 | def generate_data(data, **kwargs): 167 | data_type, retrieve = parse(data) 168 | return retrieve(generate(data_type, **kwargs)) 169 | 170 | class list_of(Generator): 171 | def __init__(self, data, **kwargs): 172 | self.data = data 173 | super(list_of, self).__init__(**kwargs) 174 | 175 | def generate(self, **kwargs): 176 | k = _options_to_num_items(self.options) 177 | 178 | return [self.generate_data(self.data, **kwargs) for _ in range(k)] 179 | 180 | def _options_to_num_items(options): 181 | options = dict(options) 182 | min_items = options.pop("min_items", 0) 183 | max_items = options.pop("max_items", 20) 184 | if "items" in options: 185 | min_items = max_items = options.pop("items") 186 | assert min_items >= 0 187 | assert max_items >= min_items 188 | if options: 189 | raise NotImplementedError( 190 | "generator does not take an option '%s', " 191 | "try giving it as a keyword argument to " 192 | "the randomize() function" % 193 | next(iter(options))) 194 | return get_int(min_items, max_items) 195 | 196 | def nonempty_list_of(data, **options): 197 | options.setdefault("min_items", 1) 198 | return list_of(data, **options) 199 | 200 | class dict_of(Generator): 201 | def __init__(self, keys, values, **kwargs): 202 | self.keys = keys 203 | self.values = values 204 | super(dict_of, self).__init__(**kwargs) 205 | 206 | def generate(self, **kwargs): 207 | k = _options_to_num_items(self.options) 208 | 209 | def gen(data, n): 210 | return (self.generate_data(data, **kwargs) for _ in range(n)) 211 | 212 | # keys need to be unique 213 | keys = set() 214 | while len(keys) < k: 215 | keys.update(gen(self.keys, k - len(keys))) 216 | return dict(zip(keys, gen(self.values, k))) 217 | 218 | def retrieve_func(data): 219 | if isinstance(data, (list, set, tuple)): 220 | return type(data) 221 | else: 222 | return next 223 | 224 | def parse(data_def): 225 | data_type = eval(data_def) if isinstance(data_def, str) else data_def 226 | return data_type, retrieve_func(data_type) 227 | -------------------------------------------------------------------------------- /pytest_quickcheck/plugin.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from _pytest.mark import Mark 3 | 4 | from pytest_quickcheck.generator import Generator 5 | from pytest_quickcheck.generator import list_of, nonempty_list_of, dict_of 6 | 7 | DEFAULT_NCALLS = 3 8 | 9 | def pytest_addoption(parser): 10 | parser.addoption("--randomize", action="store_true", 11 | help="random data test") 12 | 13 | def pytest_configure(config): 14 | from pytest_quickcheck.generator import DATA_TYPE_OPTION as opt 15 | config.addinivalue_line( 16 | "markers", 17 | "randomize(argname=type, **options): mark the test function with " 18 | "random data generating any data type.\n" 19 | " There are options for each data type: (see doc for details)\n " + 20 | "\n ".join("{0}: {1}".format(i, opt[i]) for i in opt)) 21 | 22 | pytest.list_of = globals()["list_of"] 23 | pytest.nonempty_list_of = globals()["nonempty_list_of"] 24 | pytest.dict_of = globals()["dict_of"] 25 | pytest.Generator = globals()["Generator"] 26 | 27 | 28 | def pytest_runtest_setup(item): 29 | if not isinstance(item, pytest.Function): 30 | return 31 | if item.config.option.randomize and not hasattr(item.obj, 'randomize'): 32 | pytest.skip("test with randomize only") 33 | 34 | def _has_pytestmark(metafunc): 35 | if not hasattr(metafunc.function, "pytestmark"): 36 | return False 37 | pytestmark = metafunc.function.pytestmark 38 | if len(pytestmark) == 0: 39 | return False 40 | return True 41 | 42 | 43 | def _get_ncalls(randomize, data_option): 44 | randomize_kwargs_ncalls = randomize.kwargs.pop("ncalls", None) 45 | if randomize_kwargs_ncalls is not None: 46 | return randomize_kwargs_ncalls 47 | 48 | data_option_ncalls = data_option.pop("ncalls", None) 49 | if data_option_ncalls is not None: 50 | return data_option_ncalls 51 | 52 | return DEFAULT_NCALLS 53 | 54 | 55 | def _set_parameterize(metafunc, randomize, data_option): 56 | from pytest_quickcheck.generator import generate, parse 57 | 58 | ncalls = _get_ncalls(randomize, data_option) 59 | for argname, data_def in randomize.args: 60 | data_type, retrieve = parse(data_def) 61 | values = [retrieve(generate(data_type, **data_option)) 62 | for _ in range(ncalls)] 63 | metafunc.parametrize(argname, values) 64 | 65 | 66 | def exclude_randomize_args_in_annotations(randomize, anns): 67 | randomize_args = {i[0] for i in randomize.args} 68 | for ann in anns: 69 | variable_name = ann[0] 70 | if variable_name in randomize_args: 71 | continue 72 | if variable_name == "return": 73 | continue 74 | yield ann 75 | 76 | 77 | def get_randomize_args(randomize, anns): 78 | if randomize is None: 79 | return tuple(i for i in anns if i[0] != "return") 80 | return tuple(exclude_randomize_args_in_annotations(randomize, anns)) 81 | 82 | 83 | def pytest_generate_tests(metafunc): 84 | from pytest_quickcheck.generator import DATA_TYPE_OPTIONS, IS_PY3 85 | 86 | if not _has_pytestmark(metafunc): 87 | return 88 | 89 | randomize = None 90 | ann_data_option = {} 91 | 92 | for i, mark in enumerate(metafunc.function.pytestmark): 93 | if mark.name == "randomize": 94 | randomize = mark 95 | data_option = {} 96 | for opt in DATA_TYPE_OPTIONS: 97 | if opt in randomize.kwargs: 98 | data_option[opt] = randomize.kwargs.pop(opt) 99 | ann_data_option.update(data_option) 100 | 101 | args = tuple(i for i in randomize.kwargs.items()) 102 | if args: 103 | randomize = Mark(randomize.name, args, {}) 104 | metafunc.function.pytestmark[i] = randomize 105 | 106 | _set_parameterize(metafunc, randomize, data_option) 107 | 108 | if IS_PY3 and hasattr(metafunc.function, "__annotations__"): 109 | anns = metafunc.function.__annotations__.items() 110 | args = get_randomize_args(randomize, anns) 111 | if args: 112 | _randomize = Mark("randomize", args, {}) 113 | _set_parameterize(metafunc, _randomize, ann_data_option) 114 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [pycodestyle] 2 | ignore = W504, E128, E302, E741 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | VERSION = "0.9.0" 4 | REQUIRES = ["pytest>=4.0"] 5 | 6 | try: 7 | LONG_DESCRIPTION = "".join([ 8 | open("README.rst").read(), 9 | open("CHANGELOG.rst").read(), 10 | ]) 11 | except: 12 | LONG_DESCRIPTION = "" 13 | 14 | CLASSIFIERS = [ 15 | "Development Status :: 4 - Beta", 16 | "Intended Audience :: Developers", 17 | "Operating System :: MacOS :: MacOS X", 18 | "Operating System :: Microsoft :: Windows", 19 | "Operating System :: POSIX", 20 | "Programming Language :: Python", 21 | "Programming Language :: Python :: 2.7", 22 | "Programming Language :: Python :: 3", 23 | "Programming Language :: Python :: 3.7", 24 | "Programming Language :: Python :: 3.8", 25 | "Programming Language :: Python :: 3.9", 26 | "Programming Language :: Python :: 3.10", 27 | "Topic :: Utilities", 28 | "Topic :: Software Development :: Testing", 29 | "Topic :: Software Development :: Libraries", 30 | "Topic :: Software Development :: Quality Assurance", 31 | ] 32 | 33 | setup( 34 | name="pytest-quickcheck", 35 | version=VERSION, 36 | description="pytest plugin to generate random data inspired by QuickCheck", 37 | license="Apache License 2.0", 38 | long_description=LONG_DESCRIPTION, 39 | long_description_content_type="text/x-rst", 40 | classifiers=CLASSIFIERS, 41 | keywords=["test", "pytest", "quickcheck"], 42 | author="Tetsuya Morimoto", 43 | author_email="tetsuya.morimoto@gmail.com", 44 | url="https://github.com/t2y/pytest-quickcheck", 45 | platforms=["linux", "osx", "unix", "win32"], 46 | packages=["pytest_quickcheck"], 47 | entry_points={"pytest11": ["quickcheck = pytest_quickcheck.plugin"]}, 48 | install_requires=REQUIRES, 49 | tests_require=["tox", "pytest", "pytest-pycodestyle", "pytest-flakes"], 50 | ) 51 | -------------------------------------------------------------------------------- /tests/python3/test_plugin_annotation.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import pytest 3 | 4 | @pytest.mark.randomize(ncalls=1) 5 | def test_generate_int_anns(i1: int): 6 | assert isinstance(i1, int) 7 | 8 | @pytest.mark.randomize(min_num=0, max_num=2, ncalls=5) 9 | def test_generate_int_with_option_anns(i1: int): 10 | assert isinstance(i1, int) 11 | assert 0 <= i1 <= 2 12 | 13 | @pytest.mark.randomize(ncalls=1) 14 | def test_generate_ints_anns(i1: int, i2: int): 15 | assert isinstance(i1, int) 16 | assert isinstance(i2, int) 17 | 18 | @pytest.mark.randomize(choices=[0, 1]) 19 | def test_generate_int_with_choices_anns(i1: int): 20 | assert isinstance(i1, int) 21 | assert i1 in [0, 1] 22 | 23 | @pytest.mark.randomize(ncalls=1) 24 | def test_generate_list_anns(l1: [int, str]): 25 | assert isinstance(l1, list) 26 | assert isinstance(l1[0], int) 27 | assert isinstance(l1[1], str) 28 | 29 | @pytest.mark.randomize(ncalls=1) 30 | def test_generate_set_anns(s1: set([int, str])): 31 | assert isinstance(s1, set) 32 | result = list(map(lambda x: isinstance(x, int) or isinstance(x, str), s1)) 33 | assert result[0] and result[1] 34 | 35 | @pytest.mark.randomize(ncalls=1) 36 | def test_generate_tuple_anns(t1: (int, str)): 37 | assert isinstance(t1, tuple) 38 | assert isinstance(t1[0], int) 39 | assert isinstance(t1[1], str) 40 | 41 | @pytest.mark.randomize(ncalls=1) 42 | def test_generate_dict_anns(d1: {'x': int, 'y': str}): 43 | assert isinstance(d1, dict) 44 | assert isinstance(d1["x"], int) 45 | assert isinstance(d1["y"], str) 46 | 47 | @pytest.mark.randomize(ncalls=1) 48 | def test_generate_none_anns(n1: None): 49 | assert n1 is None 50 | 51 | @pytest.mark.randomize(("i1", "int"), fixed_length=8) 52 | def test_generate_arg_anns_mixed(i1, s1: str): 53 | assert isinstance(i1, int) 54 | assert isinstance(s1, str) 55 | assert len(s1) == 8 56 | 57 | @pytest.mark.parametrize("prime", [3, 5]) 58 | @pytest.mark.randomize(("f1", "float"), min_num=-1.0, max_num=1.0) 59 | def test_generate_arg_anns_param_mixed1(prime, f1, b1: bool): 60 | assert prime in [3, 5] 61 | assert isinstance(f1, float) 62 | assert -1.0 <= f1 <= 1.0 63 | assert isinstance(b1, bool) 64 | 65 | @pytest.mark.parametrize("prime", [3, 5]) 66 | @pytest.mark.randomize(("f1", "float"), min_num=-1.0, max_num=1.0) 67 | @pytest.mark.randomize(t1=(str, str), max_length=3) 68 | def test_generate_arg_anns_param_mixed2(prime, f1, b1: bool, t1): 69 | assert prime in [3, 5] 70 | assert isinstance(f1, float) 71 | assert -1.0 <= f1 <= 1.0 72 | assert isinstance(b1, bool) 73 | assert len(t1[0]) <= 3 74 | assert len(t1[1]) <= 3 75 | assert isinstance(t1[0], str) 76 | assert isinstance(t1[1], str) 77 | assert isinstance(t1, tuple) 78 | -------------------------------------------------------------------------------- /tests/python3/test_regression.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.mark.randomize(total=int, min_num=0, max_num=100, ncalls=10) 5 | def test_regression1(total: int) -> None: 6 | assert isinstance(total, int) 7 | -------------------------------------------------------------------------------- /tests/test_list_generators.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from pytest import list_of, nonempty_list_of, dict_of, Generator 3 | 4 | @pytest.mark.randomize(l=list_of(int)) 5 | def test_list_of(l): 6 | assert isinstance(l, list) 7 | assert all(isinstance(i, int) for i in l), l 8 | 9 | @pytest.mark.randomize(l=nonempty_list_of(int), ncalls=50) 10 | def test_nonempty_list_of(l): 11 | assert isinstance(l, list), l 12 | assert len(l) >= 1 13 | 14 | @pytest.mark.randomize(l=list_of(str, min_items=10, max_items=12), 15 | fixed_length=5) 16 | def test_list_of_options(l): 17 | assert isinstance(l, list) 18 | assert 10 <= len(l) <= 12 19 | assert all(isinstance(s, str) and len(s) == 5 for s in l), l 20 | 21 | @pytest.mark.randomize(l=list_of(str, items=15)) 22 | def test_list_of_num_items(l): 23 | assert len(l) == 15 24 | 25 | @pytest.mark.randomize(l=list_of(str, items=15, min_items=1000)) 26 | def test_list_of_items_precedes_over_min_items(l): 27 | assert len(l) == 15 28 | 29 | @pytest.mark.randomize(l=list_of(int), min_num=-10, max_num=-1) 30 | def test_int_options_dont_affect_list_of(l): 31 | # if min_items, max_items would affect list_of it would raise an error 32 | # because min_items must be nonnegative 33 | 34 | assert isinstance(l, list) 35 | assert all(x < 0 for x in l), l 36 | 37 | def test_list_of_negative_size(): 38 | with pytest.raises(AssertionError): 39 | list_of(str, min_items=-1, max_items=-1).generate() 40 | 41 | def test_list_of_illogical_size(): 42 | with pytest.raises(AssertionError): 43 | list_of(str, min_items=2, max_items=1).generate() 44 | 45 | def test_list_of_unsupported_options(): 46 | with pytest.raises(NotImplementedError): 47 | list_of(str, choices="something").generate() 48 | 49 | @pytest.mark.randomize(l=nonempty_list_of(str), choices=["hodor"]) 50 | def test_list_of_global_options(l): 51 | assert list(set(l)) == ["hodor"] 52 | 53 | @pytest.mark.randomize(d=dict_of(int, str, items=5)) 54 | def test_dict_of(d): 55 | assert len(d) == 5 56 | assert all(isinstance(k, int) and isinstance(v, str) for k, v in d.items()) 57 | 58 | @pytest.mark.randomize(d=dict_of(int, str, items=100), 59 | min_num=1, max_num=100, ncalls=20) 60 | def test_dict_of_unique_keys(d): 61 | assert len(d) == 100 62 | 63 | class pair(Generator): 64 | def __init__(self, data, **options): 65 | self.data = data 66 | self.options = options 67 | 68 | def generate(self, **kwargs): 69 | options = dict(self.options) 70 | options.update(kwargs) 71 | return (self.generate_data(self.data, **options), 72 | self.generate_data(self.data, **options)) 73 | 74 | @pytest.mark.randomize(x=pair(str, fixed_length=8)) 75 | def test_custom_generator(x): 76 | a, b = x 77 | assert len(a) == len(b) == 8 78 | -------------------------------------------------------------------------------- /tests/test_listof_realworld.py: -------------------------------------------------------------------------------- 1 | """An attempt at 2 | http://www.chiark.greenend.org.uk/~sgtatham/algorithms/equivalence.html: 3 | 4 | In the general case, I do not believe there is a better solution than 5 | Mr. Tatham's. 6 | 7 | In the specific case where you may assume that the "universe" is huge 8 | relative to the set of all disconnected values, there may be something 9 | we can do... 10 | 11 | From the page, (c) Simon Tatham: 12 | 13 | Introduction 14 | 15 | An equivalence relation on a set is a partition of the set into 16 | classes. Two elements are considered equivalent if they are in the 17 | same class, and not if they are not. 18 | 19 | In some situations, you might find yourself dealing with a set of 20 | elements and gradually discover ways in which they behave differently; 21 | so you might want to keep track of which ones are different and which 22 | ones are not. 23 | 24 | (In other situations, you might find yourself dealing with a set of 25 | distinct elements and gradually discover that some are equivalent to 26 | others; but there's a known algorithm for this. It's made easier by 27 | the fact that if all elements start off distinct, you're unlikely ever 28 | to be dealing with too many of them to enumerate individually.) 29 | Desired Properties 30 | 31 | So I'm looking for data structures with these operations: 32 | 33 | Canonify. Given an element of the set, return a canonical element 34 | of the equivalence class containing that element. Any two 35 | equivalent elements should return the same element when 36 | canonified. Any two non-equivalent elements should return 37 | different elements. 38 | 39 | Enumerate. Run through all the equivalence classes one by one, 40 | probably by means of their canonical elements. 41 | 42 | Disconnect. Given a subset of the set, arrange that every 43 | equivalence class is either totally inside the subset or totally 44 | outside it, by means of splitting any class that crosses the 45 | boundary into two. 46 | 47 | Best Known Approximations 48 | 49 | For a small and dense set, where it's feasible to use the set elements 50 | as array indices, there's a reasonable implementation of all this: 51 | have an array A with one element for each set element. Then, for each 52 | set element e, let A[e] contain the canonical element of the class 53 | containing e. The canonical element of any class is defined to be the 54 | smallest-value element of that class. 55 | 56 | Then the "canonify" operation is a simple array lookup, and the 57 | "enumerate" operation consists of going through the array looking for 58 | any e satisfying A[e] = e. Disconnection is O(N), and connection is 59 | also O(N); but by assumption N is small, so that isn't too big a 60 | problem. 61 | 62 | For a sparse set - perhaps the set of all strings, or the set of basic 63 | blocks in a compilation process - I have no answer. 64 | 65 | Applications 66 | 67 | One clear application for equivalence classes with a disconnect 68 | operation is the algorithm that constructs a deterministic finite 69 | state machine from a nondeterministic one (used in regular expression 70 | processing). Most of the character set can be treated as equivalent: 71 | any character not mentioned explicitly in the regular expression 72 | behaves just the same as any other, and any two characters that are 73 | always used as part of the same character class are equivalent. For 74 | example, in the regular expression 75 | (0[xX][0-9A-Fa-f]+|[1-9][0-9]*|0[0-7]*), there is no need to treat all 76 | ASCII characters differently. The equivalence classes are [0], [Xx], 77 | [A-Fa-f], [1-7], [8-9], and everything else. So we only need to 78 | compute six transitions for each state, instead of 256. (back to 79 | algorithms index). 80 | 81 | """ 82 | 83 | import pytest 84 | from pytest import list_of, nonempty_list_of 85 | 86 | 87 | class Equivalence(object): 88 | """Keeps track of splitting a universe set into equivalence sets. 89 | Performs well when universe is huge or infinite and all other 90 | equivalence sets are relatively small. 91 | 92 | Assumes that values are ordered, although having infinite ordered 93 | values is enough (what we really need is the ability to generate a 94 | "bigger" value for any value). 95 | 96 | The important idea behind the algorithm is that of maintaining a 97 | separate "rest" equivalence partition, outside of the data 98 | structures for keeping track of everything, 99 | 100 | """ 101 | def __init__(self, valid_item=0): 102 | """ 103 | valid_item can be any valid item in the universal set. 104 | """ 105 | self._subsets = [] 106 | self._item_to_subset = {} 107 | self._rest_canonical = valid_item 108 | 109 | def canonify(self, item): 110 | # O(1) amortized 111 | s = self._item_to_subset.get(item, None) 112 | if s is None: 113 | return self._rest_canonical 114 | return self._arbitrary(s) 115 | 116 | def enumerate(self): 117 | # O(n) amortized, where n is number of equivalence partitions 118 | for s in self._subsets: 119 | yield self._arbitrary(s) 120 | yield self._rest_canonical 121 | 122 | def disconnect(self, items): 123 | # O(n) amortized where n is number of items 124 | subset = set(items) 125 | if len(subset) == 0: 126 | return 127 | if self._item_to_subset.get(self._arbitrary(subset), None) == subset: 128 | # we already know this equivalence 129 | return 130 | virgin = [] 131 | touching = {} 132 | for item in subset: 133 | s = self._item_to_subset.get(item, None) 134 | if s is None: 135 | virgin.append(item) 136 | else: 137 | touching.setdefault(id(s), []).append(item) 138 | # This looks like O(n) amortized: if there are k parts, on 139 | # average each has n/k items, so it is O(k * n/k) == O(n). 140 | # This calculation works for the edge cases where k is 1, 141 | # sqrt(n), and n, so I trust it. 142 | for part in touching.values(): 143 | whole = self._item_to_subset[part[0]] 144 | if len(whole) != len(part): 145 | # len(part) != 0, so some are in and some are out. Must split 146 | whole.difference_update(part) # a.difference_update(b) is O(b) 147 | self._create(part) 148 | if len(virgin) > 0: 149 | self._create(virgin) 150 | self._update_rest(virgin) 151 | 152 | def _update_rest(self, subset): 153 | """subset should contain at least all "virgin" items (items 154 | that used to be in "rest" and are now in a specified 155 | equivalence). Chooses a new canonical for the "rest" set. 156 | 157 | """ 158 | self._rest_canonical = max(self._rest_canonical, max(subset) + 1) 159 | 160 | def _create(self, items): 161 | subset = set(items) 162 | for item in items: 163 | self._item_to_subset[item] = subset 164 | self._subsets.append(subset) 165 | 166 | @staticmethod 167 | def _arbitrary(s): 168 | """Returns an arbitrary item from set s. Consistently returns 169 | the same item as long as s remains the same. 170 | 171 | """ 172 | return next(iter(s)) 173 | 174 | @pytest.mark.randomize(equivalence=nonempty_list_of(int), 175 | unrelateds=nonempty_list_of(list_of(int)), 176 | min_num=-10, max_num=100) 177 | def test_canonify(equivalence, unrelateds, n_calls=1000): 178 | e = Equivalence() 179 | e.disconnect(equivalence) 180 | canon = e.canonify(equivalence[0]) 181 | 182 | equivalence = set(equivalence) 183 | assert canon in equivalence 184 | for unrelated in unrelateds: 185 | unrelated = list(set(unrelated).difference(equivalence)) 186 | e.disconnect(unrelated) 187 | for item in equivalence: 188 | assert e.canonify(item) == canon 189 | 190 | @pytest.mark.randomize(equivalences=list_of(list_of(int))) 191 | def test_enumerate(equivalences, n_calls=100): 192 | e = Equivalence() 193 | for equivalence in equivalences: 194 | e.disconnect(equivalence) 195 | canons = list(e.enumerate()) 196 | # change e without changing any equivalence 197 | new = e._rest_canonical 198 | e.disconnect([new]) 199 | canons2 = list(e.enumerate()) 200 | assert len(canons2) == len(canons) + 1 201 | # the only different item is the new rest canon 202 | assert len(set(canons2).difference(set(canons))) == 1 203 | 204 | 205 | if __name__ == '__main__': 206 | from traceback import print_exc 207 | 208 | e = Equivalence() 209 | while True: 210 | try: 211 | print("c number - canonify") 212 | print("e - enumerate") 213 | print("d number1 number2 ... - disconnect") 214 | print("q - quit") 215 | command = raw_input(":").split() # pragma: no flakes 216 | if command[0] == "q": 217 | break 218 | elif command[0] == "c": 219 | assert len(command) == 2 220 | print(e.canonify(int(command[1]))) 221 | elif command[0] == "e": 222 | print(list(e.enumerate())) 223 | elif command[0] == "d": 224 | e.disconnect(map(int, command[1:])) 225 | else: 226 | print("usage error") 227 | except Exception: 228 | print_exc() 229 | -------------------------------------------------------------------------------- /tests/test_misc.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import pytest 3 | 4 | @pytest.mark.parametrize("prime", [3, 5, 7]) 5 | def test_is_prime(prime): 6 | assert prime in [3, 5, 7] 7 | 8 | def test_create_file(tmpdir): 9 | p = tmpdir.mkdir("sub").join("hello.txt") 10 | p.write("content") 11 | assert p.read() == "content" 12 | assert len(tmpdir.listdir()) == 1 13 | -------------------------------------------------------------------------------- /tests/test_plugin_basic.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import string 3 | 4 | import pytest 5 | 6 | from pytest_quickcheck.generator import IS_PY3 7 | if IS_PY3: 8 | unicode = str 9 | 10 | @pytest.mark.randomize(("i1", "int"), ncalls=1) 11 | def test_generate_int(i1): 12 | assert isinstance(i1, int) 13 | 14 | @pytest.mark.randomize(("i1", "int"), min_num=0, max_num=2, ncalls=5) 15 | def test_generate_int_with_option(i1): 16 | assert isinstance(i1, int) 17 | assert 0 <= i1 <= 2 18 | 19 | @pytest.mark.randomize(("i1", "int"), ("i2", "int"), ncalls=1) 20 | def test_generate_ints(i1, i2): 21 | assert isinstance(i1, int) 22 | assert isinstance(i2, int) 23 | 24 | @pytest.mark.randomize(("i1", "int"), choices=[0, 1]) 25 | def test_generate_int_with_choices(i1): 26 | assert isinstance(i1, int) 27 | assert i1 in [0, 1] 28 | 29 | @pytest.mark.randomize(("s1", "str"), ncalls=1) 30 | def test_generate_str(s1): 31 | assert isinstance(s1, str) 32 | 33 | @pytest.mark.randomize(("s1", "str"), fixed_length=2) 34 | def test_generate_str_with_fixed_length(s1): 35 | assert isinstance(s1, str) 36 | assert len(s1) == 2 37 | 38 | @pytest.mark.randomize(("s1", "str"), max_length=3, ncalls=5) 39 | def test_generate_str_with_max_length(s1): 40 | assert isinstance(s1, str) 41 | assert len(s1) <= 3 42 | 43 | @pytest.mark.randomize(("s1", "str"), min_length=3, ncalls=5) 44 | def test_generate_str_with_min_length(s1): 45 | assert isinstance(s1, str) 46 | assert len(s1) >= 3 47 | 48 | @pytest.mark.randomize(("s1", "str"), str_attrs=("octdigits",)) 49 | def test_generate_str_with_octdigits(s1): 50 | assert isinstance(s1, str) 51 | assert s1 == "".join(i for i in s1 if string.octdigits) 52 | 53 | @pytest.mark.randomize(("s1", "str"), 54 | str_attrs=("octdigits",), fixed_length=10 55 | ) 56 | def test_generate_str_with_octdigits_and_fixed_length(s1): 57 | assert isinstance(s1, str) 58 | assert len(s1) == 10 59 | assert s1 == "".join(i for i in s1 if string.octdigits) 60 | 61 | @pytest.mark.randomize(("s1", "str"), str_attrs=("digits", "punctuation")) 62 | def test_generate_str_with_attrs(s1): 63 | assert isinstance(s1, str) 64 | assert s1 == "".join(i for i in s1 if string.digits or string.punctuation) 65 | 66 | @pytest.mark.randomize(("s1", "str"), encoding="utf-8", ncalls=1) 67 | def test_generate_str_with_decoded(s1): 68 | assert isinstance(s1, unicode) 69 | assert isinstance(s1.encode("utf-8").decode("utf-8"), unicode) 70 | 71 | @pytest.mark.randomize(("u1", "unicode"), ncalls=1) 72 | def test_generate_unicode(u1): 73 | assert isinstance(u1, unicode) 74 | 75 | @pytest.mark.randomize(("s1", "str"), ("s2", "str"), ncalls=1) 76 | def test_generate_strs(s1, s2): 77 | assert isinstance(s1, str) 78 | assert isinstance(s2, str) 79 | 80 | @pytest.mark.randomize(("s1", "str"), ("s2", "str"), choices=["hello", "bye"]) 81 | def test_generate_strs_with_choices(s1, s2): 82 | assert isinstance(s1, str) 83 | assert isinstance(s2, str) 84 | assert s1 in ("hello", "bye") 85 | assert s2 in ("hello", "bye") 86 | 87 | @pytest.mark.randomize(("f1", "float"), ncalls=1) 88 | def test_generate_float(f1): 89 | assert isinstance(f1, float) 90 | 91 | @pytest.mark.randomize(("f1", "float"), min_num=-1.0, max_num=1.0, ncalls=5) 92 | def test_generate_float_with_min_max(f1): 93 | assert isinstance(f1, float) 94 | assert -1.0 <= f1 <= 1.0 95 | 96 | @pytest.mark.randomize(("f1", "float"), positive=True, ncalls=5) 97 | def test_generate_positive_float(f1): 98 | assert isinstance(f1, float) 99 | assert 0.0 <= f1 100 | 101 | @pytest.mark.randomize(("f1", "float"), choices=[0.0, 0.1]) 102 | def test_generate_float_with_choices(f1): 103 | assert isinstance(f1, float) 104 | assert f1 in [0.0, 0.1] 105 | 106 | @pytest.mark.randomize(("f1", "float"), ("f2", "float"), ncalls=1) 107 | def test_generate_floats(f1, f2): 108 | assert isinstance(f1, float) 109 | assert isinstance(f2, float) 110 | 111 | @pytest.mark.randomize(("b1", "bool"), ncalls=1) 112 | def test_generate_bool(b1): 113 | assert isinstance(b1, bool) 114 | 115 | @pytest.mark.randomize(("b1", "bool"), ("b2", "bool"), ncalls=1) 116 | def test_generate_bools(b1, b2): 117 | assert isinstance(b1, bool) 118 | assert isinstance(b2, bool) 119 | 120 | @pytest.mark.randomize(("l1", "[int, str]"), ncalls=1) 121 | def test_generate_list(l1): 122 | assert isinstance(l1, list) 123 | assert isinstance(l1[0], int) 124 | assert isinstance(l1[1], str) 125 | 126 | @pytest.mark.randomize(("s1", "set([int, str])"), ncalls=1) 127 | def test_generate_set(s1): 128 | assert isinstance(s1, set) 129 | result = list(map(lambda x: isinstance(x, int) or isinstance(x, str), s1)) 130 | assert result[0] and result[1] 131 | 132 | @pytest.mark.randomize(("t1", "(int, str)"), ncalls=1) 133 | def test_generate_tuple(t1): 134 | assert isinstance(t1, tuple) 135 | assert isinstance(t1[0], int) 136 | assert isinstance(t1[1], str) 137 | 138 | @pytest.mark.randomize(("d1", "{'x': int, 'y': str}"), ncalls=1) 139 | def test_generate_dict(d1): 140 | assert isinstance(d1, dict) 141 | assert isinstance(d1["x"], int) 142 | assert isinstance(d1["y"], str) 143 | 144 | @pytest.mark.randomize(("n1", "None"), ncalls=1) 145 | def test_generate_none(n1): 146 | assert n1 is None 147 | -------------------------------------------------------------------------------- /tests/test_plugin_basic_with_substitution.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import string 3 | 4 | import pytest 5 | 6 | from pytest_quickcheck.generator import IS_PY3 7 | if IS_PY3: 8 | unicode = str 9 | 10 | @pytest.mark.randomize(i1=int, ncalls=1) 11 | def test_generate_int_subs(i1): 12 | assert isinstance(i1, int) 13 | 14 | @pytest.mark.randomize(i1=int, min_num=0, max_num=2, ncalls=5) 15 | def test_generate_int_with_option_subs(i1): 16 | assert isinstance(i1, int) 17 | assert 0 <= i1 <= 2 18 | 19 | @pytest.mark.randomize(i1=int, i2=int, ncalls=1) 20 | def test_generate_ints_subs(i1, i2): 21 | assert isinstance(i1, int) 22 | assert isinstance(i2, int) 23 | 24 | @pytest.mark.randomize(i1=int, choices=[0, 1]) 25 | def test_generate_int_with_choices_subs(i1): 26 | assert isinstance(i1, int) 27 | assert i1 in [0, 1] 28 | 29 | @pytest.mark.randomize(s1=str, ncalls=1) 30 | def test_generate_str_subs(s1): 31 | assert isinstance(s1, str) 32 | 33 | @pytest.mark.randomize(s1=str, fixed_length=2) 34 | def test_generate_str_with_fixed_length_subs(s1): 35 | assert isinstance(s1, str) 36 | assert len(s1) == 2 37 | 38 | @pytest.mark.randomize(s1=str, max_length=3, ncalls=5) 39 | def test_generate_str_with_max_length_subs(s1): 40 | assert isinstance(s1, str) 41 | assert len(s1) <= 3 42 | 43 | @pytest.mark.randomize(s1=str, str_attrs=("octdigits",)) 44 | def test_generate_str_with_octdigits_subs(s1): 45 | assert isinstance(s1, str) 46 | assert s1 == "".join(i for i in s1 if string.octdigits) 47 | 48 | @pytest.mark.randomize(s1=str, str_attrs=("digits", "punctuation")) 49 | def test_generate_str_with_attrs_subs(s1): 50 | assert isinstance(s1, str) 51 | assert s1 == "".join(i for i in s1 if string.digits or string.punctuation) 52 | 53 | @pytest.mark.randomize(s1=str, encoding="utf-8", ncalls=1) 54 | def test_generate_str_with_decoded_subs(s1): 55 | assert isinstance(s1, unicode) 56 | assert isinstance(s1.encode("utf-8").decode("utf-8"), unicode) 57 | 58 | @pytest.mark.randomize(u1=unicode, ncalls=1) 59 | def test_generate_unicode_subs(u1): 60 | assert isinstance(u1, unicode) 61 | 62 | @pytest.mark.randomize(s1=str, s2=str, ncalls=1) 63 | def test_generate_strs_subs(s1, s2): 64 | assert isinstance(s1, str) 65 | assert isinstance(s2, str) 66 | 67 | @pytest.mark.randomize(s1=str, s2=str, choices=["hello", "bye"]) 68 | def test_generate_strs_subs_with_choices(s1, s2): 69 | assert isinstance(s1, str) 70 | assert isinstance(s2, str) 71 | assert s1 in ("hello", "bye") 72 | assert s2 in ("hello", "bye") 73 | 74 | @pytest.mark.randomize(f1=float, ncalls=1) 75 | def test_generate_float_subs(f1): 76 | assert isinstance(f1, float) 77 | 78 | @pytest.mark.randomize(f1=float, min_num=-1.0, max_num=1.0, ncalls=5) 79 | def test_generate_float_with_min_max_subs(f1): 80 | assert isinstance(f1, float) 81 | assert -1.0 <= f1 <= 1.0 82 | 83 | @pytest.mark.randomize(f1=float, positive=True, ncalls=5) 84 | def test_generate_positive_float_subs(f1): 85 | assert isinstance(f1, float) 86 | assert 0.0 <= f1 87 | 88 | @pytest.mark.randomize(f1=float, choices=[0.0, 0.1]) 89 | def test_generate_float_with_choices_subs(f1): 90 | assert isinstance(f1, float) 91 | assert f1 in [0.0, 0.1] 92 | 93 | @pytest.mark.randomize(f1=float, f2=float, ncalls=1) 94 | def test_generate_floats_subs(f1, f2): 95 | assert isinstance(f1, float) 96 | assert isinstance(f2, float) 97 | 98 | @pytest.mark.randomize(b1=bool, ncalls=1) 99 | def test_generate_bool_subs(b1): 100 | assert isinstance(b1, bool) 101 | 102 | @pytest.mark.randomize(b1=bool, b2=bool, ncalls=1) 103 | def test_generate_bools_subs(b1, b2): 104 | assert isinstance(b1, bool) 105 | assert isinstance(b2, bool) 106 | 107 | @pytest.mark.randomize(l1=[int, str], ncalls=1) 108 | def test_generate_list_subs(l1): 109 | assert isinstance(l1, list) 110 | assert isinstance(l1[0], int) 111 | assert isinstance(l1[1], str) 112 | 113 | @pytest.mark.randomize(s1=set([int, str]), ncalls=1) 114 | def test_generate_set_subs(s1): 115 | assert isinstance(s1, set) 116 | result = list(map(lambda x: isinstance(x, int) or isinstance(x, str), s1)) 117 | assert result[0] and result[1] 118 | 119 | @pytest.mark.randomize(t1=(int, str), ncalls=1) 120 | def test_generate_tuple_subs(t1): 121 | assert isinstance(t1, tuple) 122 | assert isinstance(t1[0], int) 123 | assert isinstance(t1[1], str) 124 | 125 | @pytest.mark.randomize(d1={'x': int, 'y': str}, ncalls=1) 126 | def test_generate_dict_subs(d1): 127 | assert isinstance(d1, dict) 128 | assert isinstance(d1["x"], int) 129 | assert isinstance(d1["y"], str) 130 | 131 | @pytest.mark.randomize(n1=None, ncalls=1) 132 | def test_generate_none_subs(n1): 133 | assert n1 is None 134 | -------------------------------------------------------------------------------- /tests/test_plugin_basic_with_type.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import pytest 3 | 4 | from pytest_quickcheck.generator import IS_PY3 5 | if IS_PY3: 6 | unicode = str 7 | 8 | @pytest.mark.randomize(("i1", int), ncalls=1) 9 | def test_generate_int_type(i1): 10 | assert isinstance(i1, int) 11 | 12 | @pytest.mark.randomize(("i1", int), ("i2", int), ncalls=1) 13 | def test_generate_ints_type(i1, i2): 14 | assert isinstance(i1, int) 15 | assert isinstance(i2, int) 16 | 17 | @pytest.mark.randomize(("s1", str), ncalls=1) 18 | def test_generate_str_type(s1): 19 | assert isinstance(s1, str) 20 | 21 | @pytest.mark.randomize(("u1", unicode), ncalls=1) 22 | def test_generate_unicode_type(u1): 23 | assert isinstance(u1, unicode) 24 | 25 | @pytest.mark.randomize(("f1", float), ncalls=1) 26 | def test_generate_float_type(f1): 27 | assert isinstance(f1, float) 28 | 29 | @pytest.mark.randomize(("b1", bool), ncalls=1) 30 | def test_generate_bool_type(b1): 31 | assert isinstance(b1, bool) 32 | 33 | @pytest.mark.randomize(("l1", [int, str]), ncalls=1) 34 | def test_generate_list_type(l1): 35 | assert isinstance(l1, list) 36 | assert isinstance(l1[0], int) 37 | assert isinstance(l1[1], str) 38 | 39 | @pytest.mark.randomize(("s1", set([int, str])), ncalls=1) 40 | def test_generate_set_type(s1): 41 | assert isinstance(s1, set) 42 | result = list(map(lambda x: isinstance(x, int) or isinstance(x, str), s1)) 43 | assert result[0] and result[1] 44 | 45 | @pytest.mark.randomize(("t1", (int, str)), ncalls=1) 46 | def test_generate_tuple_type(t1): 47 | assert isinstance(t1, tuple) 48 | assert isinstance(t1[0], int) 49 | assert isinstance(t1[1], str) 50 | 51 | @pytest.mark.randomize(("d1", {'x': int, 'y': str}), ncalls=1) 52 | def test_generate_dict_type(d1): 53 | assert isinstance(d1, dict) 54 | assert isinstance(d1["x"], int) 55 | assert isinstance(d1["y"], str) 56 | 57 | @pytest.mark.randomize(("n1", None), ncalls=1) 58 | def test_generate_none(n1): 59 | assert n1 is None 60 | -------------------------------------------------------------------------------- /tests/test_plugin_extend.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import pytest 3 | 4 | @pytest.mark.randomize(("s1", "str"), fixed_length=32, ncalls=1) 5 | def test_create_file_with_randomize(tmpdir, s1): 6 | p = tmpdir.mkdir("sub").join("tmp.txt") 7 | p.write(s1) 8 | assert len(s1) == 32 9 | assert p.read() == s1 10 | assert len(tmpdir.listdir()) == 1 11 | 12 | @pytest.mark.parametrize("prime", [3, 5, 7]) 13 | @pytest.mark.randomize(("i1", "int"), ("f1", "float"), max_num=100, ncalls=2) 14 | def test_gen_parametrize_with_randomize_int_float(prime, i1, f1): 15 | assert prime in [3, 5, 7] 16 | assert isinstance(i1, int) 17 | assert i1 < 100 18 | assert isinstance(f1, float) 19 | 20 | @pytest.mark.parametrize("prime", [3, 5, 7]) 21 | @pytest.mark.randomize( 22 | ("d1", "{'x': int, 'y': [str, (int, int)], 'z': {'x': str}}"), 23 | max_num=100, max_length=5, ncalls=1 24 | ) 25 | def test_gen_parametrize_with_randomize_dict(prime, d1): 26 | assert prime in [3, 5, 7] 27 | assert isinstance(d1, dict) 28 | assert isinstance(d1["x"], int) 29 | assert isinstance(d1["y"], list) 30 | assert isinstance(d1["y"][0], str) 31 | assert isinstance(d1["y"][1], tuple) 32 | assert isinstance(d1["y"][1][0], int) 33 | assert isinstance(d1["y"][1][1], int) 34 | assert isinstance(d1["z"], dict) 35 | assert isinstance(d1["z"]["x"], str) 36 | 37 | @pytest.mark.parametrize("prime", [3, 5, 7]) 38 | @pytest.mark.randomize(s1=str, choices=["hello", "bye"]) 39 | def test_gen_parametrize_with_randomize_str_substitution(prime, s1): 40 | assert prime in [3, 5, 7] 41 | assert isinstance(s1, str) 42 | assert s1 in ("hello", "bye") 43 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py27, py37, py38, py39, py310, pypy3 3 | 4 | [testenv:py27] 5 | commands = py.test -v --flakes --ignore=tests/python3 pytest_quickcheck tests 6 | 7 | [testenv] 8 | deps = 9 | pytest>=4.0 10 | pytest-pycodestyle 11 | pytest-flakes 12 | 13 | commands = py.test -v --pycodestyle --flakes pytest_quickcheck tests 14 | --------------------------------------------------------------------------------