├── sure ├── version.py ├── registry.py ├── reporters │ ├── __init__.py │ └── test.py ├── doubles │ ├── mocks.py │ ├── fakes.py │ ├── stubs.py │ ├── __init__.py │ └── dummies.py ├── types.py ├── meta.py ├── terminal.py ├── astuneval.py ├── loader │ └── astutil.py ├── special.py ├── runner.py └── cli.py ├── .coveragerc ├── renovate.json ├── MANIFEST.in ├── .flake8 ├── CHANGELOG.md ├── .readthedocs.yaml ├── setup.cfg ├── .github ├── PULL_REQUEST_TEMPLATE.md ├── ISSUE_TEMPLATE.md └── workflows │ └── ci.yml ├── .gitignore ├── docs ├── source │ ├── index.rst │ ├── getting-started.rst │ ├── definitions.rst │ ├── intro.rst │ ├── conf.py │ ├── api-reference.rst │ ├── guide.rst.pending │ └── changelog.rst └── Makefile ├── examples ├── changelog.md ├── unit-tests │ ├── setup_and_teardown_with_behavior.py │ └── behavior_definition_simplify_mock.py └── functional-tests │ └── behavior_definition_with_live_network_servers.py ├── tests ├── issues │ ├── __init__.py │ ├── test_issue_48.py │ ├── test_issue_104.py │ ├── test_issue_139.py │ ├── test_issue_136.py │ ├── test_issue_148.py │ ├── test_issue_19.py │ └── test_issue_134.py ├── unit │ ├── __init__.py │ ├── reporters │ │ ├── __init__.py │ │ └── test_test_reporter.py │ ├── test_object_name.py │ ├── test_runtime.py │ ├── test_astuneval.py │ ├── test_special.py │ ├── test_terminal.py │ ├── test_doubles.py │ └── test_reporter.py ├── functional │ ├── __init__.py │ ├── loader │ │ ├── __init__.py │ │ └── fake_packages │ │ │ └── unsure │ │ │ ├── gawk │ │ │ ├── a.py │ │ │ ├── b.py │ │ │ ├── data.txt │ │ │ ├── clanging │ │ │ │ ├── __init__.py │ │ │ │ └── symptoms.py │ │ │ └── clanging.py │ │ │ ├── __init__.py │ │ │ └── grasp │ │ │ └── understand.py │ └── modules │ │ ├── __init__.py │ │ ├── error │ │ └── __init__.py │ │ ├── failure │ │ └── __init__.py │ │ └── success │ │ ├── __init__.py │ │ ├── module_with_function_members.py │ │ ├── module_with_members.py │ │ ├── module_with_nonunittest_test_cases.py │ │ └── module_with_unittest_test_cases.py ├── test_runtime │ ├── __init__.py │ ├── test_runtime_options.py │ ├── test_heuristics.py │ ├── test_scenario_result.py │ ├── test_container.py │ ├── test_runtime_context.py │ ├── test_base_result.py │ ├── test_feature.py │ ├── test_scenario_result_set.py │ ├── test_feature_result.py │ └── test_scenario.py ├── __init__.py ├── crashes │ └── test_special_syntax_disabled.py ├── test_assertion_builder_assertion_properties.py ├── runner │ └── test_eins.py ├── test_ensure_context_manager.py ├── test_loader_astutil.py ├── test_cpython_patches.py ├── test_custom_assertions.py └── test_assertion_builder_assertion_methods.py ├── TODO.rst ├── pyproject.toml ├── Makefile ├── .release ├── README.rst └── setup.py /sure/version.py: -------------------------------------------------------------------------------- 1 | version = "3.0a2" 2 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [report] 2 | show_missing = True -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include requirements.txt 2 | include COPYING 3 | include README.rst 4 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = E501,E731,F401,F821,E901 3 | max-line-length = 120 4 | exclude=patterns = .git,__pycache__,.eggs,*.egg 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | The changelog of the Sure project has been moved to the documentation page: [https://sure.readthedocs.io](https://sure.readthedocs.io) 4 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | 4 | build: 5 | os: ubuntu-22.04 6 | tools: 7 | python: "3.9" 8 | 9 | sphinx: 10 | builder: html 11 | configuration: docs/source/conf.py 12 | 13 | python: 14 | install: 15 | - requirements: development.txt 16 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [tool:pytest] 2 | addopts = --cov=sure --ignore tests/crashes -v --capture=no --disable-warnings --maxfail=1 3 | testpaths = 4 | tests 5 | filterwarnings = 6 | ignore::RuntimeWarning 7 | ignore::DeprecationWarning 8 | ignore::pytest.PytestCollectionWarning 9 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Pull Request Type 2 | 3 | Please specify the type of the pull request you want to submit: 4 | 5 | - [ ] Bugfix 6 | - [ ] New Feature 7 | - [ ] Documentation Only 8 | - [ ] Testing Only 9 | - [ ] ... 10 | 11 | ## Summary 12 | 13 | Please provide a short summary about your Pull Request. 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # rust 2 | target/ 3 | 4 | # python bytecode 5 | *.pyc 6 | __pycache__ 7 | 8 | # pip and setuptools 9 | docs/_build 10 | build/ 11 | dist/ 12 | *.egg-info/ 13 | -*.tar.gz 14 | .venv/ 15 | /.python-version 16 | 17 | # temporary test files and dirs 18 | .coverage 19 | 20 | # temporary editor files 21 | *.sublime-project 22 | *.sublime-workspace 23 | *.swp 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Issue Type 2 | 3 | Please specify the type of the issue you want to submit: 4 | 5 | - [ ] Bug Report 6 | - [ ] Feature Request 7 | - [ ] Documentation Report 8 | - [ ] General Enhancement Idea 9 | - [ ] ... 10 | 11 | ## Versions & Configuration 12 | 13 | Please specify the following things: 14 | 15 | - version of `sure` 16 | - implementation and version of python 17 | - operating system 18 | 19 | ## Steps to reproduce (Expected and Actual Results) 20 | 21 | Please specify the steps to reproduce your issue including the expected and the actual results. 22 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. Sure documentation master file, created by 2 | sphinx-quickstart on Tue Aug 11 13:58:30 2015. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Sure |version| - Documentation 7 | ================================ 8 | 9 | Contents: 10 | 11 | .. toctree:: 12 | :maxdepth: 1 13 | 14 | intro 15 | getting-started 16 | definitions 17 | assertion-reference 18 | how-it-works 19 | api-reference 20 | changelog 21 | 22 | Indices and tables 23 | ================== 24 | 25 | * :ref:`genindex` 26 | * :ref:`modindex` 27 | * :ref:`search` 28 | -------------------------------------------------------------------------------- /examples/changelog.md: -------------------------------------------------------------------------------- 1 | [draft] Changes in version 1.5.0 2 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 3 | 4 | * Introducing the concept of BehaviorDefinition: a clean and 5 | decoupled way to reutilize setup/teardown behaviors. So instead of 6 | the classic massive setup/teardown methods and/or chaotic 7 | ``unittest.TestCase`` subclass inheritance every test can be 8 | decorated with @apply_behavior(CustomBehaviorDefinitionTypeObject) 9 | 10 | * Avoid using the word "test" in your "Behavior Definitions" so that 11 | nose will not mistake your BehaviorDefinition with an actual test 12 | case class and thus execute .setup() and .teardown() in an 13 | undesired manner. 14 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= uv run sphinx-build 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | 3 | on: 4 | push: 5 | branches: 6 | - runner 7 | - master 8 | 9 | jobs: 10 | python: 11 | name: "Python" 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | python: 16 | - "3.11" 17 | - "3.12" 18 | - "3.13" 19 | # IMPORTANT: update the documentation file ../../docs/source/getting-started.rst when adding more python versions to the list above 20 | 21 | steps: 22 | - uses: actions/checkout@v3 23 | - name: Install uv 24 | uses: astral-sh/setup-uv@v5 25 | with: 26 | python-version: ${{ matrix.python }} 27 | 28 | - name: Test with PyTest 29 | run: make tests 30 | 31 | - name: Test with Sure Runner 32 | run: make run 33 | -------------------------------------------------------------------------------- /tests/issues/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) <2010-2024> Gabriel Falcão 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | -------------------------------------------------------------------------------- /tests/unit/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) <2010-2024> Gabriel Falcão 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | -------------------------------------------------------------------------------- /tests/functional/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) <2010-2024> Gabriel Falcão 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | -------------------------------------------------------------------------------- /tests/test_runtime/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) <2010-2024> Gabriel Falcão 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | -------------------------------------------------------------------------------- /tests/unit/reporters/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) <2010-2024> Gabriel Falcão 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | -------------------------------------------------------------------------------- /tests/functional/loader/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) <2010-2024> Gabriel Falcão 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | -------------------------------------------------------------------------------- /tests/functional/modules/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) <2010-2024> Gabriel Falcão 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | -------------------------------------------------------------------------------- /tests/functional/modules/error/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) <2010-2024> Gabriel Falcão 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | -------------------------------------------------------------------------------- /tests/functional/modules/failure/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) <2010-2024> Gabriel Falcão 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | -------------------------------------------------------------------------------- /tests/functional/modules/success/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) <2010-2024> Gabriel Falcão 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | -------------------------------------------------------------------------------- /tests/functional/loader/fake_packages/unsure/gawk/a.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) <2010-2024> Gabriel Falcão 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | -------------------------------------------------------------------------------- /tests/functional/loader/fake_packages/unsure/gawk/b.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) <2010-2024> Gabriel Falcão 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | -------------------------------------------------------------------------------- /tests/functional/loader/fake_packages/unsure/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) <2010-2024> Gabriel Falcão 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | -------------------------------------------------------------------------------- /tests/functional/loader/fake_packages/unsure/gawk/data.txt: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) <2010-2024> Gabriel Falcão 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | -------------------------------------------------------------------------------- /tests/functional/loader/fake_packages/unsure/grasp/understand.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) <2010-2024> Gabriel Falcão 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) <2010-2024> Gabriel Falcão 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | import sure 19 | sure.enable_special_syntax() 20 | -------------------------------------------------------------------------------- /tests/functional/loader/fake_packages/unsure/gawk/clanging/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) <2010-2024> Gabriel Falcão 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | -------------------------------------------------------------------------------- /tests/functional/loader/fake_packages/unsure/gawk/clanging/symptoms.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) <2010-2024> Gabriel Falcão 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | -------------------------------------------------------------------------------- /tests/functional/loader/fake_packages/unsure/gawk/clanging.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) <2010-2024> Gabriel Falcão 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | import this 17 | im port sure == apophasis 18 | -------------------------------------------------------------------------------- /sure/registry.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) <2010-2024> Gabriel Falcão 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | KNOWN_ASSERTIONS = [] 18 | 19 | context = { 20 | 'is_running': False, 21 | 'skips': set(), 22 | } 23 | -------------------------------------------------------------------------------- /sure/reporters/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) <2010-2024> Gabriel Falcão 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | from ..meta import get_reporter, gather_reporter_names 19 | from .feature import FeatureReporter 20 | from .test import TestReporter 21 | -------------------------------------------------------------------------------- /tests/issues/test_issue_48.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) <2010-2024> Gabriel Falcão 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | from sure import expect 19 | 20 | 21 | def raise_err(foobar): 22 | raise ValueError() 23 | 24 | 25 | def test_issue_48(): 26 | expect(raise_err).when.called_with('asdf').should.throw(ValueError) 27 | -------------------------------------------------------------------------------- /tests/unit/test_object_name.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) <2010-2024> Gabriel Falcão 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | "sure.runtime" 19 | 20 | from collections.abc import Awaitable 21 | from sure.runtime import object_name 22 | 23 | 24 | def test_object_name_type(): 25 | "calling ``sure.runtime.object_name(X)`` where X is a ``type``" 26 | assert object_name(Awaitable) == "collections.abc.Awaitable" 27 | -------------------------------------------------------------------------------- /tests/issues/test_issue_104.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) <2010-2024> Gabriel Falcão 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | from sure import expect 18 | 19 | 20 | def test_issue_104(): 21 | try: 22 | expect("hello").to.contain("world") 23 | except Exception: # just to prevent syntax error because try/else does not exist 24 | pass 25 | else: 26 | raise SystemExit("Oops") 27 | 28 | expect("hello world").to.contain("world") 29 | -------------------------------------------------------------------------------- /tests/functional/modules/success/module_with_function_members.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) <2010-2024> Gabriel Falcão 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | import unittest 18 | 19 | 20 | def test_function_A(): 21 | pass 22 | 23 | 24 | def test_function_B(): 25 | pass 26 | 27 | 28 | def test_function_C(): 29 | pass 30 | 31 | 32 | def test_function_X(): 33 | pass 34 | 35 | 36 | def test_function_Y(): 37 | pass 38 | 39 | 40 | def test_function_Z(): 41 | pass 42 | -------------------------------------------------------------------------------- /tests/functional/modules/success/module_with_members.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) <2010-2024> Gabriel Falcão 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | import unittest 18 | 19 | 20 | class TestCase(object): 21 | def test_case_member(self): 22 | pass 23 | 24 | 25 | class UnitCase(unittest.TestCase): 26 | def test_case_member(self): 27 | pass 28 | 29 | 30 | def test_function_A(): 31 | pass 32 | 33 | 34 | def test_function_B(): 35 | pass 36 | 37 | 38 | def test_function_C(): 39 | pass 40 | -------------------------------------------------------------------------------- /tests/unit/test_runtime.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) <2010-2024> Gabriel Falcão 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | "unit tests for :mod:`sure.runtime`" 19 | 20 | from collections.abc import Awaitable 21 | from sure.runtime import object_name 22 | 23 | 24 | def test_object_name_type(): 25 | "calling ``sure.runtime.object_name(X)`` where X is a ``type``" 26 | assert object_name(Awaitable).should_not.equal("collections.abc.Awaitablea") 27 | assert object_name(Awaitable).should.equal("collections.abc.Awaitable") 28 | -------------------------------------------------------------------------------- /tests/functional/modules/success/module_with_nonunittest_test_cases.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) <2010-2024> Gabriel Falcão 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | 19 | class TestCaseA(object): 20 | def test_case_A_member_A(self): 21 | pass 22 | 23 | def test_case_A_member_B(self): 24 | pass 25 | 26 | def test_case_A_member_C(self): 27 | pass 28 | 29 | 30 | class TestCaseB: 31 | def test_case_B_member_X(self): 32 | pass 33 | 34 | def test_case_B_member_Y(self): 35 | pass 36 | 37 | def test_case_B_member_Z(self): 38 | pass 39 | -------------------------------------------------------------------------------- /tests/issues/test_issue_139.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) <2010-2024> Gabriel Falcão 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | """ 19 | Test fix of bug described in GitHub Issue #139. 20 | """ 21 | 22 | from sure import expect 23 | 24 | 25 | def test_issue_139(): 26 | "Test for GitHub Issue #139" 27 | # test with big epsilon 28 | expect(1.).should.equal(5., 4.) 29 | 30 | # shouldn't raise IndexError: tuple index out of range 31 | try: 32 | expect(1.).should.equal(5., 3.) 33 | except AssertionError: 34 | pass 35 | else: 36 | raise RuntimeError('should not be equal') 37 | -------------------------------------------------------------------------------- /tests/issues/test_issue_136.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) <2010-2024> Gabriel Falcão 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | 19 | """ 20 | Test fix of bug described in GitHub Issue #19. 21 | """ 22 | 23 | import base64 24 | 25 | from sure import expect 26 | 27 | 28 | def test_issue_136(): 29 | "Test for unicode error when comparing bytes" 30 | data_b64 = ( 31 | 'MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg11zwkcKSsSppm8Du13' 32 | 'je6lmwR7hEVeKMw5L8NQEN/CehRANCAAT9RzcGN/S9yN7mWP+xfLGEuw/TyHRBiW4c' 33 | 'GE6AczRgske/P8eq8trs8unSJPCp0YPKrmCEcuotL/8BHQ4Y1AVK' 34 | ) 35 | 36 | data = base64.b64decode(data_b64) 37 | expect(data).should.be.equal(data) 38 | -------------------------------------------------------------------------------- /tests/crashes/test_special_syntax_disabled.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) <2010-2024> Gabriel Falcão 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | """this module tests some aspects of attempting to use the 18 | :ref:`Special Syntax` of :mod:`sure` with that feature disabled 19 | """ 20 | 21 | from sure import expects, SpecialSyntaxDisabledError 22 | 23 | description = "Special Syntax Disabled" 24 | 25 | 26 | def try_special_syntax(): 27 | "shouldnot".should_not.equal("should_not") 28 | 29 | 30 | def test_report_special_syntax_disabled(): 31 | "SpecialSyntaxDisabledError should be raised when its use is incorrect" 32 | 33 | expects(try_special_syntax).when.called.to.have.raised( 34 | SpecialSyntaxDisabledError, 35 | "test_special_syntax_disabled.py:33" 36 | ) 37 | -------------------------------------------------------------------------------- /tests/functional/modules/success/module_with_unittest_test_cases.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) <2010-2024> Gabriel Falcão 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | import unittest 18 | from unittest import TestCase 19 | 20 | description = "Module with :class:`unittest.TestCase` subclasses" 21 | 22 | 23 | class TestCaseA(unittest.TestCase): 24 | """Description of TestCaseA""" 25 | def test_case_A_member_A(self): 26 | pass 27 | 28 | def test_case_A_member_B(self): 29 | pass 30 | 31 | def test_case_A_member_C(self): 32 | pass 33 | 34 | 35 | class TestCaseB(TestCase): 36 | def test_case_B_member_X(self): 37 | pass 38 | 39 | def test_case_B_member_Y(self): 40 | pass 41 | 42 | def test_case_B_member_Z(self): 43 | pass 44 | -------------------------------------------------------------------------------- /tests/test_runtime/test_runtime_options.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) <2010-2024> Gabriel Falcão 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | from sure import expects 18 | from sure.runtime import RuntimeOptions 19 | 20 | 21 | description = "tests for :class:`sure.runtime.RuntimeOptions`" 22 | 23 | 24 | def test_runtime_options(): 25 | """sure.runtime.RuntimeOptions""" 26 | 27 | expects(RuntimeOptions(0).immediate).to.be.false 28 | expects(RuntimeOptions(1).immediate).to.be.true 29 | expects(repr(RuntimeOptions(1))).to.equal( 30 | "" 31 | ) 32 | expects(repr(RuntimeOptions(0))).to.equal( 33 | "" 34 | ) 35 | -------------------------------------------------------------------------------- /tests/test_assertion_builder_assertion_properties.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) <2010-2024> Gabriel Falcão 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | """tests for :class:`sure.AssertionBuilder` properties defined with the 18 | decorator :func:`sure.assertionproperty`""" 19 | 20 | from sure import expects 21 | from sure.doubles import anything_of_type 22 | 23 | 24 | def test_not_have(): 25 | "expects().to.not_have" 26 | 27 | class WaveFunctionParameters: 28 | period = anything_of_type(float) 29 | amplitude = anything_of_type(float) 30 | frequency = anything_of_type(float) 31 | 32 | expects(WaveFunctionParameters).to.not_have.property("unrequested_phase_change") 33 | expects(WaveFunctionParameters).to.have.property("frequency").which.should.equal(anything_of_type(float)) 34 | -------------------------------------------------------------------------------- /TODO.rst: -------------------------------------------------------------------------------- 1 | TODO 2 | ---- 3 | 4 | 5 | Test Runner 6 | ~~~~~~~~~~~ 7 | 8 | Pytest is unnecessarily verbose, showing full tracebacks when sure 9 | already performs that at the lowest level rather than hijacking some 10 | features of the python runtime. 11 | 12 | Sure's prerrogative is that it's designed to empower engineers to 13 | write adequate code rather than play with the language so as to 14 | deliver "production" code without worrying so much about the test 15 | runtime being diffrent than the production runtime. 16 | 17 | The test runner should be simple enough like nosetests was, but don't 18 | try to share too much information with the developer that might only 19 | slow down one's developer project. 20 | 21 | 22 | ``.when.py2.should.`` and ``.should.when.py2.`` 23 | ``.when.py3.should.`` and ``.should.when.py3.`` 24 | 25 | 26 | Mock and Stubbing support 27 | ~~~~~~~~~~~~~~~~~~~~~~~~~ 28 | 29 | New way for adding behavior to scenarios 30 | 31 | 32 | .. code:: python 33 | 34 | import sure 35 | from sure.scenario import BehaviorDefinition 36 | 37 | class Example1(BehaviorDefinition): 38 | context_namespace = 'example1' 39 | 40 | def setup(self, argument1): 41 | self.data = { 42 | 'parameter': argument1 43 | } 44 | 45 | def teardown(self): 46 | self.data = {} 47 | 48 | 49 | @apply_behavior(Example1, argument1='hello-world') 50 | def test_example_1(context): 51 | context.example1.data.should.equal({ 52 | 'parameter': 'hello-world', 53 | }) 54 | -------------------------------------------------------------------------------- /tests/runner/test_eins.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) <2010-2024> Gabriel Falcão 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | from unittest import TestCase 18 | 19 | feature = "test sure runner" 20 | 21 | 22 | def test_function_ok(): 23 | "testing successful function with sure runner" 24 | assert True 25 | 26 | 27 | class TestClass(TestCase): 28 | "`sure' should work seamlessly with a :class:`unittest.TestCase`" 29 | 30 | def setUp(self): 31 | self.one_attribute = { 32 | 'question': 'does it work for us?' 33 | } 34 | 35 | def tearDown(self): 36 | self.one_attribute.pop('question') 37 | 38 | def test_expected_attribute_exists(self): 39 | "the setUp should work in our favor or else everything is unambiguously lost" 40 | assert hasattr(self, 'one_attribute'), f'{self} should have one_attribute but does not appear so' 41 | -------------------------------------------------------------------------------- /tests/test_runtime/test_heuristics.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) <2010-2024> Gabriel Falcão 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | from sure import expects 18 | 19 | from sure.loader import collapse_path 20 | from sure.runtime import is_class_initializable_without_params 21 | 22 | 23 | description = "tests generally heuristic functions within :mod:`sure.runtime`" 24 | 25 | 26 | def test_is_class_initializable_without_params(): 27 | class ParamFreeClass(object): 28 | def __init__(self): 29 | pass 30 | 31 | class ParamClass(object): 32 | def __init__(self, param: object): 33 | self.__param__ = param 34 | 35 | expects(is_class_initializable_without_params(ParamFreeClass)).to.not_be.false 36 | expects(is_class_initializable_without_params(ParamClass)).to.not_be.true 37 | expects(is_class_initializable_without_params({})).to.not_be.true 38 | -------------------------------------------------------------------------------- /docs/source/getting-started.rst: -------------------------------------------------------------------------------- 1 | .. _Getting Started: 2 | 3 | Getting Started 4 | =============== 5 | 6 | Installing 7 | ---------- 8 | 9 | It is available in PyPi, so you can install through pip: 10 | 11 | .. code:: bash 12 | 13 | pip install sure 14 | 15 | 16 | Python version compatibility 17 | ---------------------------- 18 | 19 | :ref:`sure` is `continuously tested against 20 | `__ 21 | python versions 3.11 and above of the `cpython 22 | `_ implementation. 23 | 24 | Sure is not unlikely to work with other Python implementations such as 25 | `PyPy `_ or `Jython `_ 26 | with the added caveat that its :ref:`Special Syntax` is most likely to 27 | **not work** in any implementations other than `cpython 28 | `_ while the :ref:`Standard 29 | Behavior` is likely to work well. 30 | 31 | Finally, Sure is no longer tested in older python versions such as 32 | 3.6, 3.7, 3.9, 3.10. 33 | 34 | :ref:`Standard Behavior` Example 35 | -------------------------------- 36 | 37 | .. code:: python 38 | 39 | from sure import expect 40 | 41 | expect("this".replace("is", "at")).to.equal("that") 42 | 43 | 44 | :ref:`Special Syntax` Example 45 | ----------------------------- 46 | 47 | .. code:: python 48 | 49 | "this".replace("is", "at").should.equal("that") 50 | 51 | .. note:: 52 | The :ref:`Special Syntax` can be enabled via command-line with the 53 | ``--special-syntax`` flag or programmatically with the statement 54 | ``sure.enable_special_syntax()`` 55 | -------------------------------------------------------------------------------- /sure/doubles/mocks.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) <2010-2024> Gabriel Falcão 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | '''The :mod:`sure.doubles.mocks` module currently does not provide 19 | "Mocks" per se, it nevertheless serves as a containment module to 20 | hermetically isolate the types :class:`unittest.mock._CallList` and, 21 | if available within the target Python runtime, the type 22 | :class:`mock.mock._CallList` in a tuple that 23 | :class:`sure.core.DeepComparison` uses for comparing lists of 24 | :class:`unittest.mock.call` or :class:`mock.mock.call` somewhat 25 | interchangeably 26 | ''' 27 | 28 | 29 | from unittest.mock import _CallList as UnitTestMockCallList 30 | 31 | try: # TODO: document the coupling with :mod:`mock` or :mod:`unittest.mock` 32 | from mock.mock import _CallList as MockCallList 33 | except ImportError: # pragma: no cover 34 | MockCallList = None 35 | 36 | MockCallListType = tuple(filter(bool, (UnitTestMockCallList, MockCallList))) 37 | 38 | 39 | __all__ = ['MockCallListType'] 40 | -------------------------------------------------------------------------------- /sure/types.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) <2010-2024> Gabriel Falcão 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | from typing import TypeVar 18 | 19 | 20 | Runner = TypeVar('sure.runner.Runner') 21 | BaseContainer = TypeVar('sure.runtime.BaseContainer') 22 | RuntimeRole = TypeVar('sure.runtime.RuntimeRole') 23 | TestLocation = TypeVar('sure.runtime.TestLocation') 24 | ErrorStack = TypeVar('sure.runtime.ErrorStack') 25 | RuntimeOptions = TypeVar('sure.runtime.RuntimeOptions') 26 | RuntimeContext = TypeVar('sure.runtime.RuntimeContext') 27 | BaseResult = TypeVar('sure.runtime.BaseResult') 28 | Container = TypeVar('sure.runtime.Container') 29 | ScenarioArrangement = TypeVar('sure.runtime.ScenarioArrangement') 30 | Feature = TypeVar('sure.runtime.Feature') 31 | Scenario = TypeVar('sure.runtime.Scenario') 32 | ExceptionManager = TypeVar('sure.runtime.ExceptionManager') 33 | ScenarioResult = TypeVar('sure.runtime.ScenarioResult') 34 | ScenarioResultSet = TypeVar('sure.runtime.ScenarioResultSet') 35 | FeatureResult = TypeVar('sure.runtime.FeatureResult') 36 | FeatureResultSet = TypeVar('sure.runtime.FeatureResultSet') 37 | -------------------------------------------------------------------------------- /sure/meta.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) <2010-2024> Gabriel Falcão 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | from typing import List 18 | from pathlib import Path 19 | from sure.loader import loader 20 | 21 | REPORTERS = {} 22 | 23 | 24 | def register_class(cls, identifier): 25 | cls.kind = identifier 26 | cls.loader = loader 27 | if len(cls.__mro__) > 2: 28 | register = MODULE_REGISTERS[identifier] 29 | return register(cls) 30 | else: 31 | return cls 32 | 33 | 34 | def add_reporter(reporter: type) -> type: 35 | REPORTERS[reporter.name] = reporter 36 | return reporter 37 | 38 | 39 | def get_reporter(name: str) -> type: 40 | return REPORTERS.get(name) 41 | 42 | 43 | def gather_reporter_names() -> List[str]: 44 | return list(filter(bool, REPORTERS.keys())) 45 | 46 | 47 | class MetaReporter(type): 48 | def __init__(cls, name, bases, attrs): 49 | if cls.__module__ != __name__: 50 | cls = register_class(cls, "reporter") 51 | super(MetaReporter, cls).__init__(name, bases, attrs) 52 | 53 | 54 | MODULE_REGISTERS = dict((("reporter", add_reporter),)) 55 | -------------------------------------------------------------------------------- /sure/doubles/fakes.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) <2010-2024> Gabriel Falcão 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | '''The :mod:`sure.doubles.fakes` module provides test-doubles of the type "Fake" 19 | 20 | **"Fake objects actually have working implementations, but usually take some shortcut which makes them not suitable for production..."** 21 | ''' 22 | from collections import OrderedDict 23 | 24 | 25 | class FakeOrderedDict(OrderedDict): 26 | """Subclass of :class:`collections.OrderedDict` which overrides 27 | the methods :meth:`~collections.OrderedDict.__str__` and 28 | :meth:`~collections.OrderedDict.__repr__` to present an output 29 | similar to that of of a regular :class:`dict` instances. 30 | """ 31 | def __str__(self): 32 | if len(self) == 0: 33 | return '{}' 34 | 35 | key_values = [] 36 | for key, value in self.items(): 37 | key, value = repr(key), repr(value) 38 | key_values.append("{0}: {1}".format(key, value)) 39 | 40 | res = "{{{}}}".format(", ".join(key_values)) 41 | return res 42 | 43 | def __repr__(self): 44 | return self.__str__() 45 | -------------------------------------------------------------------------------- /tests/issues/test_issue_148.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) <2010-2024> Gabriel Falcão 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | """ 19 | Regression test for GitHub Issue #148 20 | """ 21 | 22 | 23 | def test_should_compare_dict_with_non_orderable_key_types(): 24 | # given 25 | class Foo(object): 26 | def __eq__(self, other): 27 | return isinstance(other, Foo) 28 | 29 | def __hash__(self): 30 | return hash("Foo") 31 | 32 | class Bar(object): 33 | def __eq__(self, other): 34 | return isinstance(other, Bar) 35 | 36 | def __hash__(self): 37 | return hash("Bar") 38 | 39 | # when 40 | foo = Foo() 41 | bar = Bar() 42 | 43 | # then 44 | {foo: 0, bar: 1}.should.equal({foo: 0, bar: 1}) 45 | 46 | 47 | def test_should_compare_dict_with_enum_keys(): 48 | try: 49 | from enum import Enum 50 | except ImportError: # Python 2 environment 51 | # skip this test 52 | return 53 | 54 | # given 55 | class SomeEnum(Enum): 56 | A = 'A' 57 | B = 'B' 58 | 59 | # when & then 60 | {SomeEnum.A: 0, SomeEnum.B: 1}.should.equal({SomeEnum.A: 0, SomeEnum.B: 1}) 61 | -------------------------------------------------------------------------------- /tests/test_runtime/test_scenario_result.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) <2010-2024> Gabriel Falcão 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | """tests for :class:`sure.runtime.ScenarioResult`""" 18 | 19 | import sys 20 | from sure import expects 21 | from sure.doubles import stub 22 | from sure.loader import collapse_path 23 | from sure.runtime import ( 24 | ScenarioResult, 25 | Scenario, 26 | TestLocation, 27 | RuntimeContext, 28 | ErrorStack, 29 | ) 30 | 31 | 32 | description = "tests for :class:`sure.runtime.ScenarioResult`" 33 | 34 | 35 | def test_scenario_result_printable(): 36 | "meth:`ScenarioResult.printable` returns its location as string" 37 | 38 | location = TestLocation(test_scenario_result_printable) 39 | scenario = stub(Scenario) 40 | context = stub(RuntimeContext) 41 | scenario_result = ScenarioResult( 42 | scenario=scenario, location=location, context=context, error=None 43 | ) 44 | 45 | expects(scenario_result.printable()).to.equal( 46 | ( 47 | 'scenario "meth:`ScenarioResult.printable` returns its location as string" \n' 48 | "defined at " 49 | f"{collapse_path(__file__)}:35" 50 | ) 51 | ) 52 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "sure" 3 | version = "3.0a2" 4 | description = "sophisticated automated test library and runner" 5 | readme = "README.rst" 6 | authors = [ 7 | {name = "Gabriel Falcao", email = "gabriel@nacaolivre.org"}, 8 | ] 9 | maintainers = [ 10 | {name = "Gabriel Falcao", email = "gabrielteratos@gmail.com"}, 11 | ] 12 | classifiers = [ 13 | "Development Status :: 1 - Planning", 14 | "Environment :: Console", 15 | "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)", 16 | "Operating System :: MacOS :: MacOS X", 17 | "Operating System :: POSIX", 18 | "Operating System :: POSIX :: Linux", 19 | "Programming Language :: Python", 20 | "Programming Language :: Python :: 3", 21 | "Programming Language :: Python :: 3.11", 22 | "Programming Language :: Python :: 3.12", 23 | "Programming Language :: Python :: Implementation", 24 | "Programming Language :: Python :: Implementation :: CPython", 25 | "Programming Language :: Python :: Implementation :: PyPy", 26 | "Topic :: Software Development :: Testing", 27 | ] 28 | dependencies = [ 29 | "couleur>=0.7.4", 30 | ] 31 | requires-python = ">=3.11" 32 | 33 | [project.urls] 34 | Homepage = "http://github.com/gabrielfalcao/sure" 35 | Documentation = "https://sure.readthedocs.io/en/latest/" 36 | "Source Code" = "https://github.com/gabrielfalcao/sure" 37 | "Issue Tracker" = "https://github.com/gabrielfalcao/sure/issues" 38 | "Continuous Integration" = "https://github.com/gabrielfalcao/sure/actions/workflows/ci.yml" 39 | "Test Coverage" = "https://codecov.io/gh/gabrielfalcao/sure" 40 | 41 | 42 | [dependency-groups] 43 | dev = [ 44 | "coverage>=7.4.0", 45 | "mock>=5.1.0", 46 | "pytest-cov>=6.0.0", 47 | "pytest>=8.3.4", 48 | "black>=25.1.0", 49 | "isort>=6.0.1", 50 | "flake8>=7.1.2", 51 | "twine>=6.1.0", 52 | "sphinx>=8.2.1", 53 | "sphinx-rtd-theme>=3.0.2", 54 | ] 55 | 56 | [tool.uv] 57 | package = true 58 | -------------------------------------------------------------------------------- /tests/test_runtime/test_container.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) <2010-2024> Gabriel Falcão 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | from sure import expects 18 | from collections import OrderedDict 19 | from sure.loader import collapse_path 20 | from sure.runtime import Container, TestLocation, Scenario 21 | from sure.doubles import Dummy, stub 22 | 23 | 24 | description = "tests for :class:`sure.runtime.Container`" 25 | 26 | 27 | def test_container_unit(): 28 | "sure.runtime.Container.unit returns the given runnable" 29 | 30 | def dynamic(): 31 | return "balance" 32 | 33 | module_dummy = Dummy("module_or_instance") 34 | scenario_stub = stub(Scenario, name="Scenario Stub") 35 | container = Container( 36 | "test", dynamic, scenario=scenario_stub, module_or_instance=module_dummy 37 | ) 38 | 39 | expects(container.unit()).to.equal("balance") 40 | expects(container.name).to.equal("test") 41 | expects(container.runnable).to.equal(dynamic) 42 | expects(container.module_or_instance).to.equal(module_dummy) 43 | expects(container.location).to.be.a(TestLocation) 44 | expects(repr(container)).to.equal( 45 | f"" 46 | ) 47 | -------------------------------------------------------------------------------- /sure/terminal.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) <2010-2024> Gabriel Falcão 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | import os 18 | import sys 19 | import platform 20 | from functools import cache 21 | 22 | 23 | @cache 24 | def has_ansi_support(os=os, sys=sys, platform=platform): 25 | if os.getenv("SURE_NO_COLORS"): 26 | return False 27 | 28 | for handle in [sys.stdout, sys.stderr]: 29 | if (hasattr(handle, "isatty") and handle.isatty()) or ( 30 | "TERM" in os.environ and os.environ["TERM"] == "ANSI" 31 | ): 32 | if platform.system() != "Windows" and ( 33 | "TERM" in os.environ and os.environ["TERM"] == "ANSI" 34 | ): 35 | return True 36 | 37 | return False 38 | 39 | 40 | def white(msg): 41 | if not has_ansi_support(): 42 | return msg 43 | return r"\033[1;37m{0}\033[0m".format(msg) 44 | 45 | 46 | def yellow(msg): 47 | if not has_ansi_support(): 48 | return msg 49 | return r"\033[1;33m{0}\033[0m".format(msg) 50 | 51 | 52 | def red(msg): 53 | if not has_ansi_support(): 54 | return msg 55 | return r"\033[1;31m{0}\033[0m".format(msg) 56 | 57 | 58 | def green(msg): 59 | if not has_ansi_support(): 60 | return msg 61 | return r"\033[1;32m{0}\033[0m".format(msg) 62 | -------------------------------------------------------------------------------- /sure/doubles/stubs.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) <2010-2024> Gabriel Falcão 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | '''The :mod:`sure.doubles.stubs` module provides test-doubles of the type "Stub" 19 | 20 | **Stubs provide canned answers to calls made during the test, usually not responding at all to anything outside what's programmed in for the test.** 21 | ''' 22 | 23 | 24 | def stub(base_class=None, **attributes): 25 | """creates a python class "on-the-fly" with the given keyword-arguments 26 | as class-attributes accessible with .attrname. 27 | 28 | The new class inherits from ``base_class`` and defaults to ``object`` 29 | Use this to mock rather than stub in instances where such approach seems reasonable. 30 | """ 31 | if not isinstance(base_class, type): 32 | attributes['base_class'] = base_class 33 | base_class = object 34 | 35 | stub_name = attributes.get('__name__', f"{base_class.__name__}Stub") 36 | 37 | members = { 38 | "__init__": lambda self: None, 39 | "__new__": lambda *args, **kw: base_class.__new__( 40 | *args, *kw 41 | ), 42 | } 43 | if base_class.__repr__ == object.__repr__: 44 | members["__repr__"] = lambda self: f"<{stub_name}>" 45 | 46 | members.update(attributes) 47 | return type(stub_name, (base_class,), members)() 48 | -------------------------------------------------------------------------------- /tests/test_ensure_context_manager.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) <2010-2024> Gabriel Falcão 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | from sure import ensure, expects 19 | 20 | 21 | def test_ensure_simple_assertion(): 22 | """:func:`~sure.ensure` should capture :exc:`AssertionError` instances other than :exc:`Exception`""" 23 | 24 | def __test_something(): 25 | # same calculated value 26 | name = "Hodor" 27 | with ensure("the return value actually looks like: {0}", name): 28 | expects(name).should.contain("whatever") 29 | 30 | # check if the test function raises the custom AssertionError 31 | expects(__test_something).when.called_with().should.throw( 32 | AssertionError, "the return value actually looks like: Hodor" 33 | ) 34 | 35 | 36 | def test_ensure_just_assertion_error(): 37 | """:class:`~sure.ensure` should not capture :exc:`Exception` instances other than :exc:`AssertionError`""" 38 | 39 | def __test_something(): 40 | # same calculated value 41 | with ensure("neverused"): 42 | raise Exception("This is not an AssertionError") 43 | 44 | # check if the test function does not override the original exception 45 | # if it is not an AssertionError exception 46 | expects(__test_something).when.called_with().should.throw( 47 | Exception, "This is not an AssertionError" 48 | ) 49 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | MAKEFILE_PATH := $(realpath $(firstword $(MAKEFILE_LIST))) 2 | GIT_ROOT := $(shell dirname $(MAKEFILE_PATH)) 3 | VENV_ROOT := $(GIT_ROOT)/.venv 4 | 5 | PACKAGE_NAME := sure 6 | LIBEXEC_NAME := sure 7 | 8 | PACKAGE_PATH := $(GIT_ROOT)/$(PACKAGE_NAME) 9 | LIBEXEC_PATH := $(VENV_ROOT)/bin/$(LIBEXEC_NAME) 10 | export VENV ?= $(VENV_ROOT) 11 | OSNAME := $(shell uname) 12 | ifeq ($(OSNAME), Linux) 13 | OPEN_COMMAND := gnome-open 14 | else 15 | OPEN_COMMAND := open 16 | endif 17 | export SURE_NO_COLORS := true 18 | export SURE_LOG_FILE := $(GIT_ROOT)/sure-$(date +"%Y-%m-%d-%H:%M:%S").log 19 | export PYTHONPATH := $(GIT_ROOT) 20 | AUTO_STYLE_TARGETS := sure/runtime.py sure/runner.py sure/meta.py sure/meta.py sure/reporter.py sure/reporters 21 | 22 | 23 | all: tests html-docs autostyle build-release 24 | 25 | clean-docs: 26 | @rm -rf ./docs/build 27 | 28 | html-docs: clean-docs 29 | @cd ./docs && make html 30 | 31 | docs: html-docs 32 | $(OPEN_COMMAND) docs/build/html/index.html 33 | 34 | test: 35 | @uv run pytest --cov=sure tests 36 | 37 | tests: clean test run 38 | 39 | run: 40 | uv run sure --reap-warnings tests/crashes 41 | uv run sure --reap-warnings --special-syntax --with-coverage --cover-branches --cover-erase --cover-module=sure --immediate --cover-module=sure --ignore tests/crashes tests 42 | 43 | push-release: dist 44 | uv build 45 | uv run twine upload dist/*.tar.gz 46 | 47 | build-release: 48 | uv build 49 | uv run twine check dist/*.tar.gz 50 | 51 | release: tests 52 | @./.release 53 | $(MAKE) build-release 54 | $(MAKE) push-release 55 | 56 | clean: 57 | @rm -rf .coverage 58 | 59 | flake8: 60 | @uv run flake8 --statistics --max-complexity 17 --exclude=$(VENV) $(AUTO_STYLE_TARGETS) 61 | 62 | black: 63 | @uv run black -l 80 $(AUTO_STYLE_TARGETS) 64 | 65 | isort: 66 | @uv run isort --overwrite-in-place --profile=black --ls --srx --cs --ca -n --ot --tc --color --star-first --virtual-env $(VENV) --py auto $(AUTO_STYLE_TARGETS) 67 | 68 | 69 | autostyle: isort black flake8 70 | 71 | 72 | .PHONY: \ 73 | all \ 74 | autostyle \ 75 | black \ 76 | build-release \ 77 | clean \ 78 | clean-docs \ 79 | dependencies \ 80 | develop \ 81 | docs \ 82 | flake8 \ 83 | html-docs \ 84 | isort \ 85 | push-release \ 86 | release \ 87 | run \ 88 | test \ 89 | tests 90 | -------------------------------------------------------------------------------- /tests/issues/test_issue_19.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) <2010-2024> Gabriel Falcão 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | """ 19 | Test fix of bug described in GitHub Issue #19. 20 | """ 21 | 22 | from sure import expect, AssertionBuilder 23 | from sure.special import is_cpython 24 | 25 | 26 | def test_issue_19(): 27 | "Allow monkey-patching of methods already implemented by sure." 28 | 29 | class Foo(object): 30 | pass 31 | 32 | @property 33 | def should(self): 34 | return 42 35 | 36 | instance = Foo() 37 | instance.do = "anything" 38 | instance.doesnt = "foo" 39 | 40 | expect(instance.do).should.be.equal("anything") 41 | expect(instance.doesnt).should.be.equal("foo") 42 | 43 | if is_cpython: 44 | instance2 = Foo() 45 | instance2.do.shouldnt.be.equal("anything") 46 | instance.does.__class__.should.be.equal(AssertionBuilder) 47 | 48 | # remove attribute 49 | del instance.do 50 | 51 | if is_cpython: 52 | instance.do.shouldnt.be.equal("anything") 53 | else: 54 | expect(instance).shouldnt.have.property("do") 55 | 56 | if is_cpython: 57 | Foo.shouldnt.__class__.should.be.equal(AssertionBuilder) 58 | 59 | Foo.shouldnt = "bar" 60 | expect(Foo.shouldnt).should.be.equal("bar") 61 | del Foo.shouldnt 62 | 63 | if is_cpython: 64 | Foo.shouldnt.__class__.should.be.equal(AssertionBuilder) 65 | else: 66 | expect(Foo).shouldnt.have.property("shouldnt") 67 | 68 | 69 | expect(instance.should).should.be.equal(42) 70 | -------------------------------------------------------------------------------- /docs/source/definitions.rst: -------------------------------------------------------------------------------- 1 | .. _Definitions: 2 | 3 | Definitions 4 | =========== 5 | 6 | This section defines terms used across :ref:`Sure`'s documentation, 7 | code and run-time error messages and must be interpreted accordingly 8 | as to prevent confusion, misunderstanding and abuse of any manner, 9 | form or kind be it real or virtual. 10 | 11 | Each term is defined in the subsections below which are titled by the 12 | term itself and followed by the actual definition of the term in case 13 | which shall be entirely comprehended as they appear typographically 14 | written regardless of capitalization. Synonyms may be presented within 15 | each of those sections. 16 | 17 | These terms might be updated in the event of emerging incorrectness or 18 | general evolution of the :ref:`Sure` project. 19 | 20 | 21 | .. _truthy: 22 | 23 | truthy 24 | ------ 25 | 26 | Defines Python objects whose logical value is equivalent to the 27 | boolean value of ``True``. 28 | 29 | More specifically, any valid Python code evaluted by :class:`bool` as 30 | ``True`` might appear written as ``truthy`` within the scope of 31 | :ref:`Sure`. 32 | 33 | Synonyms: ``true``, ``truthy``, ``ok`` 34 | 35 | 36 | .. _falsy: 37 | 38 | falsy 39 | ----- 40 | 41 | Defines Python objects whose logical value is equivalent to the 42 | boolean value of ``False``. 43 | 44 | More specifically, any valid Python code evaluted by :class:`bool` as 45 | ``False`` might appear written as ``falsy`` within the scope of 46 | :ref:`Sure`. 47 | 48 | Synonyms: ``false``, ``falsy``, ``not_ok`` 49 | 50 | 51 | .. _none: 52 | 53 | none 54 | ---- 55 | 56 | Defines Python objects whose logical value is equivalent to the 57 | boolean value of ``False``. 58 | 59 | Synonyms: ``none``, ``None`` 60 | 61 | 62 | .. _special syntax definition: 63 | 64 | special syntax 65 | -------------- 66 | 67 | :ref:`Special Syntax` refers to the unique feature of giving *special 68 | properties* to every in-memory Python :class:`python:object` from 69 | which to build assertions. 70 | 71 | Such special properties are semantically divided in two categories: 72 | :ref:`positive ` (``do``, ``does``, ``must``, ``should``, ``when``) and :ref:`negative 73 | ` (``do_not``, ``dont``, ``does_not``, ``doesnt``, ``must_not``, ``mustnt``, ``should_not``, ``shouldnt``). 74 | -------------------------------------------------------------------------------- /tests/test_runtime/test_runtime_context.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) <2010-2024> Gabriel Falcão 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | "tests for :class:`sure.runtime.RuntimeContext`" 18 | 19 | from mock import patch 20 | from sure import expects 21 | from sure.doubles import stub 22 | from sure.runtime import RuntimeContext, RuntimeOptions 23 | from sure.reporter import Reporter 24 | 25 | 26 | description = "tests for :class:`sure.runtime.RuntimeContext`" 27 | 28 | 29 | @patch('sure.runtime.WarningReaper') 30 | def test_runtime_context(WarningReaper): 31 | """sure.runtime.RuntimeContext""" 32 | 33 | warning_reaper = WarningReaper.return_value 34 | warning_reaper.warnings = ['dummy-warning-a', 'dummy-warning-b'] 35 | options_dummy = RuntimeOptions(immediate=False, reap_warnings=True) 36 | reporter_stub = stub(Reporter) 37 | 38 | context = RuntimeContext(reporter_stub, options_dummy, "dummy_test_name") 39 | 40 | expects(context).to.have.property("reporter").being.equal(reporter_stub) 41 | expects(context).to.have.property("options").being.equal(options_dummy) 42 | expects(context).to.have.property("unittest_testcase_method_name").being.equal( 43 | "dummy_test_name" 44 | ) 45 | 46 | expects(repr(context)).to.equal( 47 | " options=>" 48 | ) 49 | WarningReaper.assert_called_once_with() 50 | warning_reaper.enable_capture.assert_called_once_with() 51 | 52 | expects(context).to.have.property('warnings').being.equal([ 53 | 'dummy-warning-a', 54 | 'dummy-warning-b', 55 | ]) 56 | -------------------------------------------------------------------------------- /tests/issues/test_issue_134.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) <2010-2024> Gabriel Falcão 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | 19 | """ 20 | Test fix of bug described in GitHub Issue #134. 21 | """ 22 | 23 | from sure import expect 24 | 25 | 26 | def test_issue_132(): 27 | "Correctly handle {} characters in matcher string" 28 | 29 | def __great_test(): 30 | expect('hello%world').should.be.equal('hello%other') 31 | 32 | expect(__great_test).when.called.to.throw(AssertionError, "X is 'hello%world' whereas Y is 'hello%other'") 33 | 34 | def __great_test_2(): 35 | expect('hello{42}world').should.be.equal('hello{42}foo') 36 | 37 | expect(__great_test_2).when.called.to.throw(AssertionError, "X is 'hello{42}world' whereas Y is 'hello{42}foo'") 38 | 39 | def __great_test_3(): 40 | expect('hello{42world }').should.be.equal('hello{42foo }') 41 | 42 | expect(__great_test_3).when.called.to.throw(AssertionError, "X is 'hello{42world }' whereas Y is 'hello{42foo }'") 43 | 44 | def __great_test_4(): 45 | expect('hello{42world }}').should.be.equal('hello{42foo }}') 46 | 47 | expect(__great_test_4).when.called.to.throw(AssertionError, "X is 'hello{42world }}' whereas Y is 'hello{42foo }}'") 48 | 49 | def __great_test_bytes(): 50 | expect(b'hello{42world }}').should.be.equal(b'hello{42foo }}') 51 | 52 | error_msg = "X is b'hello{42world }}' whereas Y is b'hello{42foo }}'" 53 | 54 | expect(__great_test_bytes).when.called.to.throw(AssertionError, error_msg) 55 | 56 | def __great_test_unicode(): 57 | expect(u'hello{42world }}').should.be.equal(u'hello{42foo }}') 58 | 59 | error_msg = "X is 'hello{42world }}' whereas Y is 'hello{42foo }}'" 60 | 61 | expect(__great_test_unicode).when.called.to.throw(AssertionError, error_msg) 62 | -------------------------------------------------------------------------------- /.release: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | declare newversion 4 | declare current_version 5 | declare sure 6 | 7 | 8 | current_version="$(grep -E version ./sure/version.py | sed 's,version *= *.\(\([0-9]*[.]*\)\{3\,4\}\(dev\|a\|b\)[0-9]*\).,\1,g')" 9 | echo -en "The current version is \033[1;33m$current_version\033[0m, type a new one\n\033[1;32mnew version:\033[0m " 10 | read -r newversion 11 | 12 | 13 | function find_files () { 14 | for name in $(find . -name 'README.rst' -or -name pyproject.toml -or -name version.py -or -name conf.py | grep -v '\(\.venv\|build\)[/]'); do 15 | echo "${name}" 16 | done 17 | } 18 | 19 | function update_files (){ 20 | find_files | xargs gsed -i "s,$current_version,$newversion,g" 21 | } 22 | function revert_files (){ 23 | find_files | xargs gsed -i "s,$newversion,$current_version,g" 24 | } 25 | 26 | echo -en "\033[A\033[A\rI will make a new commit named \033[1;33m'New release $newversion'\033[0m\n" 27 | echo -en "Are you sure? [\033[1;32myes\033[0m or \033[1;31mno\033[0m]\n" 28 | read -r sure 29 | 30 | 31 | if [ "${sure}" == "yes" ]; then 32 | echo "updating relevant files with new version..." 33 | if update_files; then 34 | echo "committing and pushing changes..." 35 | echo -en "New release: \033[1;32m$newversion\033[0m\n" 36 | if git add -f $(find_files); then 37 | if git commit $(find_files) -m "New release: $newversion"; then 38 | if git push; then 39 | echo "creating tag ${newversion}..." 40 | if git tag "v${newversion}"; then 41 | echo "pushing tag ${newversion}..." 42 | git push --tags 43 | else 44 | echo "failed to create tag ${newversion}" 45 | echo "you might want to revert the last commit and check what happened" 46 | exit 1 47 | fi 48 | else 49 | echo "failed to push, skipping release and reverting changes" 50 | revert_files 51 | exit 1 52 | fi 53 | else 54 | echo "failed to commit, skipping release and reverting changes" 55 | revert_files 56 | exit 1 57 | fi 58 | else 59 | echo "no files to git add, skipping release" 60 | exit 1 61 | fi; 62 | else 63 | echo "no files were updated, skipping release" 64 | exit 1 65 | fi 66 | else 67 | echo "kthankxbye" 68 | fi 69 | -------------------------------------------------------------------------------- /docs/source/intro.rst: -------------------------------------------------------------------------------- 1 | .. _sure: 2 | 3 | Sure 4 | ==== 5 | .. index:: sure 6 | 7 | .. _Introduction: 8 | 9 | Introduction 10 | ------------ 11 | 12 | Sure is a both a library and a test-runner for for the Python Programming Languages, featuring a DSL for writing 13 | assertions. Sure's original author is `Gabriel Falcão `_. 14 | 15 | Sure provides a :ref:`special syntax definition` for writing tests in a 16 | human-friendly, fluent and easy-to-use manner, In the context of the 17 | Python Programming language, Sure is a pioneer at extending every 18 | object with test-specific methods at test-runtime. This feature is 19 | disabled by default starting on version 3.0.0 and MAY be optionally 20 | enabled programmatically or via command-line. Read the section 21 | :ref:`special syntax definition` for more information. 22 | 23 | Whether the :ref:`Special Syntax` is enabled or not, :ref:`sure` 24 | generally aims at enabling software developers to writing tests in a 25 | human-friendly, fluent and hopefully fun way. 26 | 27 | 28 | Quick Examples 29 | -------------- 30 | 31 | :ref:`Standard Behavior` Example 32 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 33 | 34 | .. code:: python 35 | 36 | from sure import expect 37 | 38 | def printing_money_indiscriminately(amount): 39 | raise ValueError(f"Inflation! Printing {amount} amounts of money is likely increase inflation!") 40 | 41 | expect(printing_money_indiscriminately.when.called_with(88888888).should.throw( 42 | ValueError, 43 | "Inflation! Printing 88888888 amounts of money is likely increase inflation!" 44 | ) 45 | 46 | 47 | :ref:`Special Syntax` Example 48 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 49 | 50 | .. code:: python 51 | 52 | import sure 53 | sure.enable_special_syntax() 54 | 55 | def superpowers(mode): 56 | if mode in ("ignorance", "selfishness"): 57 | raise SyntaxError( 58 | f"superpowers cannot, must not and shall not be used in the name of {mode}!" 59 | ) 60 | raise NotImplementedError( 61 | f"{mode} entirely not allowed" 62 | ) 63 | 64 | superpowers.when.called_with("ignorance").should.have.raised( 65 | SyntaxError, 66 | "superpowers cannot, must not and shall not be used in the name of ignorance!" 67 | ) 68 | 69 | superpowers.when.called_with("selfishness").should.have.raised( 70 | SyntaxError, 71 | "superpowers cannot, must not and shall not be used in the name of selfishness!" 72 | ) 73 | 74 | superpowers.when.called_with("out thinking").should.have.raised( 75 | NotImplementedError, 76 | "out thinking entirely not allowed" 77 | ) 78 | -------------------------------------------------------------------------------- /tests/test_runtime/test_base_result.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) <2010-2024> Gabriel Falcão 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | """tests for :class:`sure.runtime.BaseResult`""" 19 | from sure import expects 20 | from sure.runtime import BaseResult 21 | 22 | 23 | description = "tests for :class:`sure.runtime.BaseResult`" 24 | 25 | 26 | def test_base_result___repr___not_implemented_error_missing_label_property(): 27 | "BaseResult.__repr__ should raise :exc:`NotImplementederror` when missing a `label' property" 28 | 29 | expects(repr).when.called_with(BaseResult()).to.have.raised( 30 | NotImplementedError, 31 | " MUST define a `label' property or attribute which must be a string" 32 | ) 33 | 34 | 35 | def test_base_result___repr___not_implemented_error_nonstring_label_property(): 36 | "calling :func:`repr` on a subclass of :class:`sure.runtime.BaseResult` whose label property returns something other than a :class:`str` instance should raise :exc:`NotImplementedError`" 37 | 38 | class FakeResultDummyLabelNonString(BaseResult): 39 | @property 40 | def label(self): 41 | return () 42 | 43 | expects(repr).when.called_with(FakeResultDummyLabelNonString()).to.have.raised( 44 | NotImplementedError, 45 | ".FakeResultDummyLabelNonString'>.label must be a string but is a instead" 46 | ) 47 | 48 | 49 | def test_base_result___repr___returns_lowercase_label(): 50 | "the builtin implementation of :meth:`sure.runtime.BaseResult.__repr__` should return the value of its `label' property as a lower-case string" 51 | 52 | class FakeResultDummyLabel(BaseResult): 53 | @property 54 | def label(self): 55 | return "LABEL" 56 | 57 | repr(FakeResultDummyLabel()).should.equal("'label'") 58 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | sure 2 | ==== 3 | 4 | .. image:: https://img.shields.io/pypi/dm/sure 5 | :target: https://pypi.org/project/sure 6 | 7 | .. image:: https://github.com/gabrielfalcao/sure/workflows/Sure%20Tests/badge.svg 8 | :target: https://github.com/gabrielfalcao/sure/actions?query=workflow%3A%22Sure+Tests%22 9 | 10 | .. image:: https://img.shields.io/readthedocs/sure 11 | :target: https://sure.readthedocs.io/ 12 | 13 | .. image:: https://img.shields.io/github/license/gabrielfalcao/sure?label=Github%20License 14 | :target: https://github.com/gabrielfalcao/sure/blob/master/LICENSE 15 | 16 | .. image:: https://img.shields.io/pypi/v/sure 17 | :target: https://pypi.org/project/sure 18 | 19 | .. image:: https://img.shields.io/pypi/l/sure?label=PyPi%20License 20 | :target: https://pypi.org/project/sure 21 | 22 | .. image:: https://img.shields.io/pypi/format/sure 23 | :target: https://pypi.org/project/sure 24 | 25 | .. image:: https://img.shields.io/pypi/status/sure 26 | :target: https://pypi.org/project/sure 27 | 28 | .. image:: https://img.shields.io/pypi/pyversions/sure 29 | :target: https://pypi.org/project/sure 30 | 31 | .. image:: https://img.shields.io/pypi/implementation/sure 32 | :target: https://pypi.org/project/sure 33 | 34 | .. image:: https://img.shields.io/github/v/tag/gabrielfalcao/sure 35 | :target: https://github.com/gabrielfalcao/sure/releases 36 | 37 | The sophisticated automated test tool for Python, featuring a test 38 | runner and a library with powerful and flexible assertions. 39 | 40 | Originally authored by `Gabriel Falcão `_. 41 | 42 | 43 | Installing 44 | ---------- 45 | 46 | .. code:: bash 47 | 48 | pip install sure 49 | 50 | 51 | Running tests 52 | ------------- 53 | 54 | .. code:: bash 55 | 56 | sure tests 57 | 58 | 59 | For More Information: 60 | 61 | .. code:: bash 62 | 63 | sure --help 64 | 65 | 66 | Documentation 67 | ------------- 68 | 69 | Available on `sure.readthedocs.io `_. 70 | 71 | To build locally run: 72 | 73 | .. code:: bash 74 | 75 | make docs 76 | 77 | 78 | Quick Library Showcase 79 | ---------------------- 80 | 81 | .. code:: python 82 | 83 | from sure import expects 84 | 85 | expects(4).to.be.equal(2 + 2) 86 | expects(7.5).to.be.eql(3.5 + 4) 87 | 88 | expects(3).to.not_be.equal(5) 89 | expects(9).to_not.be.equal(11) 90 | 91 | .. code:: python 92 | 93 | from sure import expects 94 | 95 | expects({'foo': 'bar'}).to.equal({'foo': 'bar'}) 96 | expects({'foo': 'bar'}).to.have.key('foo').being.equal('bar') 97 | 98 | .. code:: python 99 | 100 | "Awesome ASSERTIONS".lower().split().should.equal(['awesome', 'assertions']) 101 | -------------------------------------------------------------------------------- /tests/test_loader_astutil.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) <2010-2024> Gabriel Falcão 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | import unittest 18 | from unittest import TestCase 19 | from unittest.mock import patch 20 | from sure import expects 21 | from sure.loader.astutil import gather_class_definitions_from_module_path, gather_class_definitions_node 22 | 23 | 24 | class TestLoaderAstUtilBaseClassName(TestCase): 25 | def test_gather_class_definitions_from_module_path(self): 26 | classes = gather_class_definitions_from_module_path(__file__) 27 | expects(classes).to.equal( 28 | {'TestLoaderAstUtilBaseClassName': (24, ('TestCase',)), 'TestLoaderAstUtilBaseClassAttributeAndName': (32, ('unittest.TestCase',))} 29 | ) 30 | 31 | 32 | class TestLoaderAstUtilBaseClassAttributeAndName(unittest.TestCase): 33 | def test_gather_class_definitions_from_module_path(self): 34 | classes = gather_class_definitions_from_module_path(__file__) 35 | expects(classes).to.equal( 36 | {'TestLoaderAstUtilBaseClassName': (24, ('TestCase',)), 'TestLoaderAstUtilBaseClassAttributeAndName': (32, ('unittest.TestCase',))} 37 | ) 38 | 39 | 40 | def test_gather_class_definitions_node_with_string(): 41 | "sure.laoder.astutil.gather_class_definitions_node() with a string" 42 | 43 | expects(gather_class_definitions_node("string", classes={})).to.equal({}) 44 | 45 | 46 | @patch('sure.loader.astutil.send_runtime_warning') 47 | @patch('sure.loader.astutil.Path') 48 | def test_gather_class_definitions_from_module_path_symlink(Path, send_runtime_warning): 49 | "sure.laoder.astutil.gather_class_definitions_from_module_path() with a symlink" 50 | 51 | path = Path.return_value 52 | path.is_symlink.return_value = True 53 | path.resolve.return_value.exists.return_value = False 54 | path.absolute.return_value = "absolute-path-dummy" 55 | expects(gather_class_definitions_from_module_path("path")).to.equal({}) 56 | Path.assert_called_once_with("path") 57 | send_runtime_warning.assert_called_once_with("parsing skipped of irregular file `absolute-path-dummy'") 58 | -------------------------------------------------------------------------------- /sure/reporters/test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) <2010-2024> Gabriel Falcão 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | import time 19 | from collections import defaultdict 20 | from typing import Union 21 | from sure.reporter import Reporter 22 | from sure.runtime import ( 23 | Feature, 24 | FeatureResult, 25 | Scenario, 26 | ScenarioResult, 27 | ScenarioResultSet, 28 | TestLocation, 29 | ErrorStack, 30 | RuntimeContext, 31 | ) 32 | 33 | 34 | events = defaultdict(list) 35 | 36 | 37 | class TestReporter(Reporter): 38 | """Reporter intented exclusively for testing sure itself""" 39 | 40 | name = "test" 41 | 42 | def on_start(self): 43 | events["on_start"].append((time.time(),)) 44 | 45 | def on_feature(self, feature: Feature): 46 | events["on_feature"].append((time.time(), feature.title)) 47 | 48 | def on_feature_done(self, feature: Feature, result: FeatureResult): 49 | events["on_feature_done"].append( 50 | (time.time(), feature.title, result.label.lower()) 51 | ) 52 | 53 | def on_scenario(self, scenario: Scenario): 54 | events["on_scenario"].append((time.time(), scenario.name)) 55 | 56 | def on_scenario_done( 57 | self, scenario: Scenario, result: Union[ScenarioResult, ScenarioResultSet] 58 | ): 59 | events["on_scenario_done"].append( 60 | (time.time(), scenario.name, result.label.lower()) 61 | ) 62 | 63 | def on_failure(self, test: Scenario, result: ScenarioResult): 64 | events["on_failure"].append((time.time(), test.name, result.label.lower())) 65 | 66 | def on_success(self, test: Scenario): 67 | events["on_success"].append((time.time(), test.name)) 68 | 69 | def on_error(self, test: Scenario, result: ScenarioResult): 70 | events["on_error"].append((time.time(), test.name, result.label.lower())) 71 | 72 | def on_internal_runtime_error(self, context: RuntimeContext, error: ErrorStack): 73 | events["on_internal_runtime_error"].append((time.time(), context, error)) 74 | 75 | def on_finish(self, context: RuntimeContext): 76 | events["on_finish"].append((time.time(), context)) 77 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) <2010-2024> Gabriel Falcão 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | import sys 18 | import os 19 | import sphinx_rtd_theme 20 | from pathlib import Path 21 | try: 22 | import sure 23 | import sure.original 24 | import sure.core 25 | import sure.runtime 26 | import sure.runner 27 | import sure.doubles 28 | import sure.meta 29 | import sure.reporter 30 | import sure.reporters.feature 31 | except ImportError: 32 | sys.path.insert(0, Path(__file__).parent.parent.parent) 33 | 34 | from sure import version 35 | extensions = [ 36 | "sphinx.ext.autodoc", 37 | "sphinx.ext.doctest", 38 | "sphinx.ext.intersphinx", 39 | "sphinx.ext.todo", 40 | "sphinx.ext.coverage", 41 | "sphinx.ext.imgmath", 42 | "sphinx.ext.ifconfig", 43 | "sphinx.ext.viewcode", 44 | ] 45 | 46 | source_suffix = ".rst" 47 | master_doc = "index" 48 | project = "sure" 49 | copyright = "2010-2024, Gabriel Falcão" 50 | author = "Gabriel Falcão" 51 | release = version 52 | language = 'en' 53 | exclude_patterns = [] 54 | pygments_style = "sphinx" 55 | todo_include_todos = True 56 | 57 | html_theme = "sphinx_rtd_theme" 58 | # html_static_path = ["_static"] 59 | htmlhelp_basename = "sure_" 60 | latex_elements = {} 61 | latex_documents = [ 62 | (master_doc, "Sure.tex", "Sure Documentation", "Gabriel Falcão", "manual"), 63 | ] 64 | man_pages = [(master_doc, "sure", "Sure Documentation", [author], 1)] 65 | texinfo_documents = [ 66 | ( 67 | master_doc, 68 | "Sure", 69 | "Sure Documentation", 70 | author, 71 | "Sure", 72 | "sophisticated automated test library and runner for python.", 73 | "Automated Testing", 74 | ), 75 | ] 76 | epub_title = project 77 | epub_author = author 78 | epub_publisher = author 79 | epub_copyright = copyright 80 | epub_exclude_files = ["search.html"] 81 | intersphinx_disabled_reftypes = [] 82 | intersphinx_mapping = { 83 | "python": ("https://docs.python.org/3", None), 84 | "mock": ("https://mock.readthedocs.io/en/latest", None), 85 | "psycopg2": ("https://www.psycopg.org/docs", None), 86 | "coverage": ("https://coverage.readthedocs.io/en/7.6.12", None), 87 | } 88 | pygments_style = 'xcode' 89 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright (C) <2010-2024> Gabriel Falcão 5 | # 6 | # This program is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program. If not, see . 18 | 19 | """sophisticated automated test library and runner""" 20 | 21 | import os 22 | import ast 23 | import sys 24 | import codecs 25 | from setuptools import setup, find_packages 26 | 27 | # These python versions are explicitly not supported 28 | # by sure. This is mostly because of the incompatiblities 29 | # with unicode strings. If there is an urgent reason why 30 | # to support it after all or if you have a quick fix 31 | # please open an issue on GitHub. 32 | EXPL_NOT_SUPPORTED_VERSIONS = ((3, 0), (3, 1), (3, 2), (3, 3), (3, 4)) 33 | 34 | if sys.version_info[0:2] in EXPL_NOT_SUPPORTED_VERSIONS: 35 | raise SystemExit( 36 | "Sure does explicitly not support the following python versions " 37 | "due to big incompatibilities: {0}".format(EXPL_NOT_SUPPORTED_VERSIONS) 38 | ) 39 | 40 | if sys.version_info[0] < 3: 41 | raise SystemExit( 42 | "Sure no longer supports Python 2" 43 | ) 44 | 45 | 46 | PROJECT_ROOT = os.path.dirname(__file__) 47 | 48 | 49 | def read_version(): 50 | mod = ast.parse(local_text_file("sure", "version.py")) 51 | exp = mod.body[0] 52 | tgt = exp.targets[0] 53 | cst = exp.value 54 | assert tgt.id == "version" 55 | return cst.value 56 | 57 | 58 | def local_text_file(*f): 59 | path = os.path.join(PROJECT_ROOT, *f) 60 | with open(path, "rt") as fp: 61 | file_data = fp.read() 62 | 63 | return file_data 64 | 65 | 66 | def read_readme(): 67 | """Read README content. 68 | If the README.rst file does not exist yet 69 | (this is the case when not releasing) 70 | only the short description is returned. 71 | """ 72 | try: 73 | return local_text_file("README.rst") 74 | except IOError: 75 | return __doc__ 76 | 77 | 78 | version = read_version() 79 | packages = find_packages(exclude=[ 80 | "*docs*", 81 | "*e2e*", 82 | "*examples*", 83 | "*tests*", 84 | ]) 85 | 86 | if __name__ == "__main__": 87 | setup( 88 | name="sure", 89 | version=version, 90 | description=__doc__, 91 | long_description=read_readme(), 92 | include_package_data=True, 93 | packages=packages, 94 | long_description_content_type='text/x-rst', 95 | ) 96 | -------------------------------------------------------------------------------- /sure/astuneval.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) <2010-2024> Gabriel Falcão 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | """astuneval (Abstract Syntax-Tree Unevaluation) - safe substitution for unsafe :func:`eval` 18 | """ 19 | import ast 20 | 21 | 22 | class Accessor(object): 23 | """base class for object element accessors""" 24 | 25 | def __init__(self, astbody): 26 | self.body = astbody 27 | 28 | def __call__(self, object: object, *args, **kw) -> object: 29 | return self.access(object, *args, **kw) 30 | 31 | def access(self, object: object) -> object: 32 | raise NotImplementedError(f"support to {type(self.body)} is not implemented") 33 | 34 | 35 | class NameAccessor(Accessor): 36 | """Accesses an object's attributes through name""" 37 | 38 | def access(self, object: object) -> object: 39 | return getattr(object, self.body.id) 40 | 41 | 42 | class SliceAccessor(Accessor): 43 | """Accesses an object's attributes through slice""" 44 | 45 | def access(self, object: object) -> object: 46 | return object[self.body.value] 47 | 48 | 49 | class SubsAccessor(Accessor): 50 | """Accesses an object's attributes through subscript""" 51 | 52 | def access(self, object: object) -> object: 53 | get_value = NameAccessor(self.body.value) 54 | get_slice = SliceAccessor(self.body.slice) 55 | return get_slice(get_value(object)) 56 | 57 | 58 | class AttributeAccessor(Accessor): 59 | """Accesses an object's attributes through chained attribute""" 60 | 61 | def access(self, object: object) -> object: 62 | attr_name = self.body.attr 63 | access = resolve_accessor(self.body.value) 64 | value = access(object) 65 | return getattr(value, attr_name) 66 | 67 | 68 | def resolve_accessor(body): 69 | return { 70 | ast.Name: NameAccessor, 71 | ast.Subscript: SubsAccessor, 72 | ast.Attribute: AttributeAccessor, 73 | }.get(type(body), Accessor)(body) 74 | 75 | 76 | def parse_accessor(value: str) -> Accessor: 77 | body = parse_body(value) 78 | return resolve_accessor(body) 79 | 80 | 81 | def parse_body(value: str) -> ast.stmt: 82 | bodies = ast.parse(value).body 83 | if len(bodies) > int(True): 84 | raise SyntaxError(f"{repr(value)} exceeds the maximum body count for ast nodes") 85 | 86 | return bodies[0].value 87 | -------------------------------------------------------------------------------- /docs/source/api-reference.rst: -------------------------------------------------------------------------------- 1 | .. _API-Reference: 2 | 3 | API Reference 4 | ============= 5 | 6 | ``sure`` 7 | -------- 8 | 9 | .. automodule:: sure 10 | .. autofunction:: sure.enable_special_syntax 11 | .. autoclass:: sure.StagingArea 12 | .. autoclass:: sure.CallBack 13 | .. autofunction:: sure.scenario 14 | .. autofunction:: sure.within 15 | .. autofunction:: sure.word_to_number 16 | .. autofunction:: sure.assertionmethod 17 | .. autofunction:: sure.assertionproperty 18 | .. autoclass:: sure.ObjectIdentityAssertion 19 | .. autoclass:: sure.AssertionBuilder 20 | .. autofunction:: sure.assertion 21 | .. autofunction:: sure.chain 22 | .. autofunction:: sure.chainproperty 23 | .. autoclass:: sure.ensure 24 | 25 | 26 | ``sure.core`` 27 | ------------- 28 | 29 | .. automodule:: sure.core 30 | .. autoclass:: sure.core.Explanation 31 | .. _deep comparison: 32 | .. autoclass:: sure.core.DeepComparison 33 | .. autofunction:: sure.core.itemize_length 34 | 35 | 36 | ``sure.runner`` 37 | --------------- 38 | 39 | .. automodule:: sure.runner 40 | .. autoclass:: sure.runner.Runner 41 | 42 | 43 | ``sure.loader`` 44 | --------------- 45 | 46 | .. automodule:: sure.loader 47 | .. autoclass:: sure.loader.loader 48 | .. autofunction:: sure.loader.resolve_path 49 | .. autofunction:: sure.loader.get_package 50 | .. autofunction:: sure.loader.get_type_definition_filename_and_firstlineno 51 | 52 | 53 | ``sure.loader.astutil`` 54 | ----------------------- 55 | 56 | .. automodule:: sure.loader.astutil 57 | .. autofunction:: sure.loader.astutil.is_classdef 58 | .. autofunction:: sure.loader.astutil.resolve_base_names 59 | .. autofunction:: sure.loader.astutil.gather_class_definitions_node 60 | .. autofunction:: sure.loader.astutil.gather_class_definitions_from_module_path 61 | 62 | 63 | ``sure.reporter`` 64 | ----------------- 65 | 66 | .. py:module:: sure.reporter 67 | .. autoclass:: sure.reporter.Reporter 68 | 69 | ``sure.reporters`` 70 | ------------------ 71 | 72 | .. py:module:: sure.reporters 73 | .. autoclass:: sure.reporters.feature.FeatureReporter 74 | 75 | 76 | ``sure.original`` 77 | ----------------- 78 | 79 | .. automodule:: sure.original 80 | .. autofunction:: sure.original.identify_caller_location 81 | .. autofunction:: sure.original.is_iterable 82 | .. autofunction:: sure.original.all_integers 83 | .. autofunction:: sure.original.Explanation 84 | 85 | ``sure.doubles`` 86 | ---------------- 87 | 88 | .. automodule:: sure.doubles 89 | .. autofunction:: sure.doubles.stub 90 | .. autoclass:: sure.doubles.FakeOrderedDict 91 | 92 | 93 | ``sure.doubles.dummies`` 94 | ------------------------ 95 | 96 | .. autoclass:: sure.doubles.dummies.Anything 97 | .. autoclass:: sure.doubles.dummies.AnythingOfType 98 | .. autoattribute:: sure.doubles.dummies.anything 99 | .. autofunction:: sure.doubles.dummies.anything_of_type 100 | 101 | 102 | ``sure.doubles.fakes`` 103 | ---------------------- 104 | 105 | .. automodule:: sure.doubles.fakes 106 | .. autoclass:: sure.doubles.fakes.FakeOrderedDict 107 | 108 | ``sure.doubles.stubs`` 109 | ---------------------- 110 | 111 | .. automodule:: sure.doubles.stubs 112 | .. autoclass:: sure.doubles.stubs.stub 113 | -------------------------------------------------------------------------------- /sure/doubles/__init__.py: -------------------------------------------------------------------------------- 1 | """This module provides a limited set of "test-doubles" 2 | 3 | The concept of "test-double" employed in the module :mod:`sure.doubles` is imperative to, inspired or derived from the Martin Fowler's great article `"Mocks Aren't Stubs" `_. 4 | 5 | To that extent, it currently presents the following sub-modules: 6 | 7 | - :mod:`sure.doubles.dummies` 8 | - :mod:`sure.doubles.fakes` 9 | - :mod:`sure.doubles.stubs` 10 | 11 | Considering the aforementioned `article `_, there are two types of 12 | test-doubles missing from the list above: "spies" and "mocks". That is 13 | because neither of those are presently provided in :mod:`sure` as 14 | test-doubles. 15 | 16 | In the very specific case of the type of test-doubles referred to as 17 | "Mocks", :mod:`sure` loosely recommends the usage of Python's builtin 18 | :mod:`mock` and its underlying components with the added caveat that, 19 | at the time of this writing, :class:`~mock.Mock` and its related 20 | module components provide types of features that seem to reasonably 21 | cover all or almost all the features expected from all distinct types 22 | of test-doubles while not making a reasonably clear distinction or 23 | articulation between the peculiarities that bring value to that 24 | otherwise clear distinction. 25 | 26 | The value in case lies - primarily but not limited to - somewhere 27 | within the principles, related disciplines or practices of "Separation 28 | of concerns" and "Didactics". This is because an unclear distinction 29 | between the aforementioned types of test-doubles leaves room for 30 | misinterpretation of rather important concepts underlying each type, 31 | and because it is apparent that students of the discipline of 32 | Automated Software Testing in Python facing the sorts of obstacles 33 | that eventually lead to the discovery of the concept of "Mocks" and 34 | consequently stumble upon the :mod:`mock` module often fail to realize 35 | the values of a thorough and clear understanding of the concept of 36 | "test-doubles" both in terms of theory and practice, as well as the 37 | surmounting costs of writing inconcise, liable tests that generate 38 | unwarranted tech-debt. 39 | 40 | To conclude, the :mod:`mock` and its components have, in some sense, 41 | quite intelligent internal mechanisms that can prove quite powerful at 42 | virtually every occasion. Nevertheless wielding intelligent tools 43 | should ideally require a level of care for knowledge that, more often 44 | than not, can only be achieved by those who take the enterprise of 45 | intellectual pursuit seriously and not insincerely, and continuously 46 | seek to hone their software-craftsmanship skills. 47 | 48 | `Fowler's article 49 | `_ is great 50 | source of knowledge to dispel the sorts of harmful misconceptions made 51 | salient in this section. 52 | """ 53 | 54 | from sure.doubles.fakes import FakeOrderedDict 55 | from sure.doubles.stubs import stub 56 | from sure.doubles.dummies import anything, Dummy, AnythingOfType, anything_of_type 57 | 58 | __all__ = ['FakeOrderedDict', 'stub', 'anything', 'Dummy', 'AnythingOfType', 'anything_of_type'] 59 | -------------------------------------------------------------------------------- /tests/unit/test_astuneval.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) <2010-2024> Gabriel Falcão 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | import ast 18 | from sure import expects 19 | from sure.astuneval import parse_body 20 | from sure.astuneval import parse_accessor 21 | from sure.astuneval import Accessor, NameAccessor, SubsAccessor, AttributeAccessor 22 | 23 | 24 | def test_parse_body_against_several_kinds(): 25 | expects(parse_body("atomic_bonds[3:7]")).to.be.an(ast.Subscript) 26 | expects(parse_body("children[6]")).to.be.an(ast.Subscript) 27 | expects(parse_body("hippolytus")).to.be.an(ast.Name) 28 | expects(parse_body("zone[4].damage")).to.be.an(ast.Attribute) 29 | 30 | 31 | def test_parse_accessor_name_accessor(): 32 | class Tragedy: 33 | telemachus = "♒️" 34 | 35 | expects(parse_accessor("telemachus")).to.be.a(NameAccessor) 36 | get_character = parse_accessor("telemachus") 37 | expects(get_character(Tragedy)).to.equal('♒️') 38 | 39 | 40 | def test_parse_accessor_subscript_accessor(): 41 | class MonacoGrandPrix1990: 42 | classification = [ 43 | "Ayrton Senna", 44 | "Alain Prost", 45 | "Jean Alesi", 46 | ] 47 | expects(parse_accessor("classification[2]")).to.be.a(SubsAccessor) 48 | get_position = parse_accessor("classification[2]") 49 | expects(get_position(MonacoGrandPrix1990)).to.equal("Jean Alesi") 50 | 51 | 52 | def test_parse_accessor_attr_accessor(): 53 | class Event: 54 | def __init__(self, description: str): 55 | self.tag = description 56 | 57 | class LogBook: 58 | events = [ 59 | Event("occurrenceA"), 60 | Event("occurrenceB"), 61 | Event("occurrenceC"), 62 | Event("occurrenceD"), 63 | Event("occurrenceE"), 64 | Event("occurrenceF"), 65 | ] 66 | 67 | expects(parse_accessor("events[3].description")).to.be.a(AttributeAccessor) 68 | 69 | access_description = parse_accessor("events[3].tag") 70 | expects(access_description(LogBook)).to.equal("occurrenceD") 71 | 72 | 73 | def test_accessor_access_not_implemented(): 74 | accessor = Accessor(parse_body("attribute")) 75 | expects(accessor.access).when.called_with(object).to.throw( 76 | NotImplementedError 77 | ) 78 | 79 | 80 | def test_parse_body_syntax_error(): 81 | parse_body.when.called_with("substance = collect()\nsubstance.reuse()").to.throw( 82 | SyntaxError, 83 | "'substance = collect()\\nsubstance.reuse()' exceeds the maximum body count for ast nodes" 84 | ) 85 | -------------------------------------------------------------------------------- /tests/unit/test_special.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) <2010-2024> Gabriel Falcão 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | import io 18 | import sys 19 | import warnings 20 | from datetime import datetime 21 | from unittest.mock import patch 22 | from sure import expects 23 | from sure.loader import collapse_path 24 | from sure.doubles.dummies import anything, Dummy, anything_of_type 25 | from sure.special import ( 26 | determine_python_implementation, 27 | runtime_is_cpython, 28 | get_py_ssize_t, 29 | noop_patchable_builtin, 30 | craft_patchable_builtin, 31 | load_ctypes, 32 | WarningReaper, 33 | ) 34 | description = "tests for :class:`sure.special`" 35 | 36 | 37 | @patch("sure.special.load_ctypes") 38 | def test_get_py_ssize_t_64(load_ctypes_mock): 39 | "sure.special.get_py_ssize_t() in 64 bits platform" 40 | 41 | ctypes = load_ctypes_mock.return_value 42 | ctypes.pythonapi.Py_InitModule4_64 = anything 43 | ctypes.c_int64 = Dummy("ctypes.c_int64") 44 | expects(get_py_ssize_t()).to.equal(ctypes.c_int64) 45 | 46 | 47 | @patch("sure.special.load_ctypes") 48 | def test_runtime_is_cpython_without_ctypes(load_ctypes_mock): 49 | "sure.special.runtime_is_cpython() returns False when :mod:`ctypes` is not available" 50 | 51 | load_ctypes_mock.return_value = None 52 | expects(runtime_is_cpython()).to.equal(False) 53 | 54 | 55 | def test_noop_patchable_builtin(): 56 | "sure.special.noop_patchable_builtin() returns performs no action" 57 | expects(noop_patchable_builtin()).to.be.none 58 | 59 | 60 | @patch("sure.special.runtime_is_cpython") 61 | def test_craft_patchable_builtin_noop(runtime_is_cpython): 62 | "sure.special.craft_patchable_builtin() returns noop_patchable_builtin when runtime is not cpython" 63 | 64 | runtime_is_cpython.return_value = False 65 | expects(craft_patchable_builtin()).to.equal(noop_patchable_builtin) 66 | 67 | 68 | @patch.dict(sys.modules, ctypes=None) 69 | def test_load_ctypes_unavailable(): 70 | "sure.special.load_ctypes() returns None when ctypes cannot be imported" 71 | 72 | expects(load_ctypes()).to.equal(None) 73 | 74 | 75 | def test_warning_reaper(): 76 | "sure.special.WarningReaper should (toggle-) feature an interface to capture warnings" 77 | 78 | warning_reaper = WarningReaper().enable_capture() 79 | warning_reaper.clear() 80 | warnings.showwarning("test", ResourceWarning, filename=__file__, lineno=81) 81 | 82 | warning_reaper.warnings.should.equal([{"message": "test", "category": ResourceWarning, "filename": __file__, "lineno": 81, "occurrence": anything_of_type(datetime), "line": None, "file": None}]) 83 | -------------------------------------------------------------------------------- /tests/test_cpython_patches.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) <2010-2024> Gabriel Falcão 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | import sure 18 | from sure import expect, is_special_syntax_enabled 19 | from sure.special import is_cpython 20 | 21 | if not is_special_syntax_enabled(): 22 | def test_it_works_with_objects(): 23 | ("anything that inherits from object should be patched") 24 | 25 | (4).should.equal(2 + 2) 26 | "foo".should.equal("f" + ("o" * 2)) 27 | {}.should.be.empty 28 | 29 | def test_shouldnt_overwrite_class_attributes(): 30 | """do not patch already existing class attributes with same name""" 31 | 32 | class Foo(object): 33 | when = 42 34 | shouldnt = 43 35 | bar = "bar" 36 | 37 | Foo.when.should.be.equal(42) 38 | Foo.shouldnt.should.be.equal(43) 39 | Foo.bar.should.be.equal("bar") 40 | 41 | Foo.__dict__.should.contain("when") 42 | Foo.__dict__.should.contain("shouldnt") 43 | Foo.__dict__.should.contain("bar") 44 | 45 | dir(Foo).should.contain("when") 46 | dir(Foo).should.contain("shouldnt") 47 | dir(Foo).should.contain("bar") 48 | dir(Foo).shouldnt.contain("should") 49 | 50 | def test_shouldnt_overwrite_instance_attributes(): 51 | """do not patch already existing instance attributes with same name""" 52 | 53 | class Foo(object): 54 | def __init__(self, when, shouldnt, bar): 55 | self.when = when 56 | self.shouldnt = shouldnt 57 | self.bar = bar 58 | 59 | f = Foo(42, 43, "bar") 60 | 61 | f.when.should.be.equal(42) 62 | f.shouldnt.should.be.equal(43) 63 | f.bar.should.be.equal("bar") 64 | 65 | f.__dict__.should.contain("when") 66 | f.__dict__.should.contain("shouldnt") 67 | f.__dict__.should.contain("bar") 68 | 69 | dir(f).should.contain("when") 70 | dir(f).should.contain("shouldnt") 71 | dir(f).should.contain("bar") 72 | dir(f).shouldnt.contain("should") 73 | 74 | def test_dir_conceals_sure_specific_attributes(): 75 | ("dir(obj) should conceal names of methods that were grafted by sure") 76 | 77 | x = 123 78 | 79 | expect(set(dir(x)).intersection(set(sure.POSITIVES))).to.be.empty 80 | expect(set(dir(x)).intersection(set(sure.NEGATIVES))).to.be.empty 81 | 82 | 83 | # TODO 84 | # def test_it_works_with_non_objects(): 85 | # ("anything that inherits from non-object should also be patched") 86 | 87 | # class Foo: 88 | # pass 89 | 90 | # f = Foo() 91 | 92 | # f.should.be.a(Foo) 93 | 94 | # def test_can_override_properties(): 95 | # x =1 96 | # x.should = 2 97 | # assert x.should == 2 98 | -------------------------------------------------------------------------------- /sure/loader/astutil.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) <2010-2024> Gabriel Falcão 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | """astutils (Abstract Syntax-Tree Utils)""" 18 | import ast 19 | 20 | from typing import Dict, List, Optional, Tuple, Union 21 | from pathlib import Path 22 | from sure.errors import send_runtime_warning 23 | 24 | 25 | def is_classdef(node: ast.stmt) -> bool: 26 | """ 27 | :param node: a :class:`node ` instance 28 | :returns: ``True`` if the given :class:`node ` is a :class:`ast.ClassDef` 29 | """ 30 | return isinstance(node, ast.ClassDef) 31 | 32 | 33 | def resolve_base_names(bases: List[ast.stmt]) -> Tuple[str]: 34 | """returns a tuple with the names of base classes of an :class:`node `""" 35 | names = [] 36 | for base in bases: 37 | if isinstance(base, ast.Name): 38 | names.append(base.id) 39 | continue 40 | if isinstance(base, ast.Attribute): 41 | names.append(f"{base.value.id}.{base.attr}") 42 | continue 43 | 44 | return tuple(names) 45 | 46 | 47 | def gather_class_definitions_node( 48 | node: Union[ast.stmt, str], classes: dict, nearest_line: Optional[int] = None 49 | ) -> Dict[str, Tuple[int, Tuple[str]]]: 50 | """Recursively scans all class definitions of an :class:`node ` 51 | 52 | Primarily designed to find nested :class:`unittest.TestCase` classes. 53 | 54 | :returns: :class:`dict` containing a 2-item tuple: (line number, tuple of base class names), keyed with the class name 55 | """ 56 | classes = dict(classes) 57 | 58 | if is_classdef(node): 59 | classes[node.name] = (node.lineno, resolve_base_names(node.bases)) 60 | elif isinstance(node, str): 61 | return classes 62 | 63 | for name, subnode in ast.iter_fields(node): 64 | if isinstance(subnode, list): 65 | for subnode in subnode: 66 | classes.update(gather_class_definitions_node(subnode, classes)) 67 | 68 | return classes 69 | 70 | 71 | def gather_class_definitions_from_module_path( 72 | path: Path, nearest_line: Optional[int] = None 73 | ) -> Dict[str, Tuple[int, Tuple[str]]]: 74 | """parses the Python file at the given path and returns a mapping 75 | of class names to tuples indicating the line number in which the 76 | class is defined and a tuple with the names of its base classes. 77 | """ 78 | 79 | path = Path(path) 80 | 81 | if path.is_symlink() and not path.resolve().exists(): # avoid loading broken symlinks 82 | send_runtime_warning(f"parsing skipped of irregular file `{path.absolute()}'") 83 | return {} 84 | 85 | with path.open() as f: 86 | node = ast.parse(f.read()) 87 | 88 | return gather_class_definitions_node(node, {}, nearest_line=nearest_line) 89 | -------------------------------------------------------------------------------- /sure/doubles/dummies.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) <2010-2024> Gabriel Falcão 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | '''The :mod:`sure.doubles.dummies` module provides test-doubles of the type "Dummy" 19 | 20 | "**Dummy objects are passed around but never actually used. Usually they are just used to fill parameter lists.**" 21 | ''' 22 | __dummy_id_registry__ = {} 23 | 24 | 25 | class Dummy(object): 26 | """class for creating Dummy objects with a string identity. 27 | 28 | When compared with ``==`` (:func:`operator.eq`) :class:`~sure.doubles.dummes.Dummy` objects are matched at the ``dummy_id`` 29 | """ 30 | 31 | def __init__(self, dummy_id: str): 32 | if not isinstance(dummy_id, str): 33 | raise TypeError( 34 | f'{__name__}.Dummy() takes string as argument, received {dummy_id} ({type(dummy_id)}) instead' 35 | ) 36 | 37 | self.__dummy_id__ = dummy_id 38 | __dummy_id_registry__[dummy_id] = self 39 | 40 | @property 41 | def id(self): 42 | return self.__dummy_id__ 43 | 44 | def __eq__(self, dummy) -> bool: 45 | return isinstance(dummy, Dummy) and self.id == dummy.id 46 | 47 | def __repr__(self): 48 | return f'' 49 | 50 | def __str__(self): 51 | return f'' 52 | 53 | 54 | class Anything(Dummy): 55 | """Dummy class whose entire purpose is to serve as sentinel in assertion 56 | statements where the :meth:`operator.__eq__` is employed under the 57 | specific circumstance of expecting the :class:`bool` value ``True`` 58 | """ 59 | def __eq__(self, _): 60 | return True 61 | 62 | 63 | class AnythingOfType(Anything): 64 | """Dummy class bound to a :class:`type` in terms of employing the :meth:`operator.__eq__` 65 | """ 66 | def __init__(self, expected_type: type): 67 | if not isinstance(expected_type, type): 68 | raise TypeError(f'{repr(expected_type)} should be a class but is a {type(expected_type)} instead') 69 | 70 | module_name = expected_type.__module__ 71 | type_name = expected_type.__name__ 72 | self.__expected_type__ = expected_type 73 | self.__type_fqdn__ = f"{module_name}.{type_name}" 74 | super().__init__(f"anything_of_type({self.__type_fqdn__})") 75 | 76 | def __eq__(self, given: object): 77 | given_type = type(given) 78 | module_name = given_type.__module__ 79 | type_name = given_type.__name__ 80 | return isinstance(given, (self.__expected_type__, self.__class__)) and super().__eq__(f"typed:{module_name}.{type_name}") 81 | 82 | def __repr__(self): 83 | return f'' 84 | 85 | def __str__(self): 86 | return f'' 87 | 88 | 89 | anything = Anything('sure.doubles.dummies.Anything') 90 | 91 | 92 | def anything_of_type(expected_type: type) -> AnythingOfType: 93 | return AnythingOfType(expected_type) 94 | -------------------------------------------------------------------------------- /tests/test_custom_assertions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) <2010-2024> Gabriel Falcão 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | """ 19 | Test custom assertions. 20 | """ 21 | 22 | from sure import expect, assertion, chain, chainproperty 23 | from sure.special import is_cpython 24 | 25 | 26 | def test_custom_assertion(): 27 | "test extending sure with a custom assertion." 28 | 29 | class Response(object): 30 | def __init__(self, return_code): 31 | self.return_code = return_code 32 | 33 | 34 | @assertion 35 | def return_code(self, return_code): 36 | if self.negative: 37 | assert return_code != self.actual.return_code, "Expected was a return code different from {0}.".format(return_code) 38 | else: 39 | assert return_code == self.actual.return_code, "Expected return code is: {0}\nGiven return code was: {1}".format( 40 | return_code, self.actual.return_code) 41 | 42 | return True 43 | 44 | 45 | expect(Response(200)).should.have.return_code(200) 46 | expect(Response(200)).shouldnt.have.return_code(201) 47 | 48 | 49 | def test_custom_chain_method(): 50 | "test extending sure with a custom chain method." 51 | 52 | class Response(object): 53 | def __init__(self, headers, return_code): 54 | self.headers = headers 55 | self.return_code = return_code 56 | 57 | 58 | @chain 59 | def header(self, header_name): 60 | expect(self.actual.headers).should.have.key(header_name) 61 | return self.actual.headers[header_name] 62 | 63 | 64 | # FIXME(TF): 'must' does not sound right in this method chain. 65 | # it should rather be ...header("foo").which.equals("bar") 66 | # however, which is an assertionproperty in AssertionBuilder 67 | # and is not a monkey patched property. 68 | if is_cpython: 69 | Response({"foo": "bar", "bar": "foo"}, 200).should.have.header("foo").must.be.equal("bar") 70 | else: 71 | expect(expect(Response({"foo": "bar", "bar": "foo"}, 200)).should.have.header("foo")).must.be.equal("bar") 72 | 73 | 74 | def test_custom_chain_property(): 75 | "test extending sure with a custom chain property." 76 | 77 | class Response(object): 78 | special = 41 79 | 80 | @chainproperty 81 | def having(self): 82 | return self 83 | 84 | @chainproperty 85 | def implement(self): 86 | return self 87 | 88 | 89 | @assertion 90 | def attribute(self, name): 91 | has_it = hasattr(self.actual, name) 92 | if self.negative: 93 | assert not has_it, "Expected was that object {0} does not have attribute {1}".format( 94 | self.actual, name) 95 | else: 96 | assert has_it, "Expected was that object {0} has attribute {1}".format( 97 | self.actual, name) 98 | 99 | return True 100 | 101 | 102 | expect(Response).having.attribute("special") 103 | expect(Response).doesnt.implement.attribute("nospecial") 104 | -------------------------------------------------------------------------------- /sure/special.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) <2012-2024> Gabriel Falcão 4 | # Copyright (C) <2012> Lincoln Clarete 5 | # 6 | # This program is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program. If not, see . 18 | import io 19 | import platform 20 | import warnings 21 | 22 | from datetime import datetime 23 | from typing import Dict, List, Optional 24 | 25 | 26 | __captured_warnings__ = [] 27 | 28 | 29 | class WarningReaper(object): 30 | """Captures warnings for posterior analysis""" 31 | 32 | builtin_showwarning = warnings.showwarning 33 | 34 | @property 35 | def warnings(self) -> List[Dict[str, object]]: 36 | return list(__captured_warnings__) 37 | 38 | def showwarning( 39 | self, 40 | message, 41 | category: Warning, 42 | filename: str, 43 | lineno: int, 44 | file: Optional[io.IOBase] = None, 45 | line: Optional[str] = None, 46 | ): 47 | occurrence = datetime.utcnow() 48 | info = locals() 49 | info.pop('self') 50 | __captured_warnings__.append(info) 51 | 52 | def enable_capture(self): 53 | warnings.showwarning = self.showwarning 54 | return self 55 | 56 | def clear(self): 57 | __captured_warnings__.clear() 58 | 59 | 60 | def load_ctypes(): 61 | try: 62 | import ctypes 63 | except (ImportError, ModuleNotFoundError): 64 | ctypes = None 65 | return ctypes 66 | 67 | 68 | DictProxyType = type(object.__dict__) 69 | 70 | 71 | def determine_python_implementation(): 72 | return getattr(platform, "python_implementation", lambda: "")().lower() 73 | 74 | 75 | def runtime_is_cpython(): 76 | if not load_ctypes(): 77 | return False 78 | 79 | return determine_python_implementation() == "cpython" 80 | 81 | 82 | def get_py_ssize_t(): 83 | ctypes = load_ctypes() 84 | pythonapi = getattr(ctypes, "pythonapi", None) 85 | if hasattr(pythonapi, "Py_InitModule4_64"): 86 | return ctypes.c_int64 87 | else: 88 | return ctypes.c_int 89 | 90 | 91 | def noop_patchable_builtin(*args, **kw): 92 | pass 93 | 94 | 95 | def craft_patchable_builtin(): 96 | ctypes = load_ctypes() 97 | if not runtime_is_cpython(): 98 | return noop_patchable_builtin 99 | 100 | class PyObject(ctypes.Structure): 101 | pass 102 | 103 | PyObject._fields_ = [ 104 | ("ob_refcnt", get_py_ssize_t()), 105 | ("ob_type", ctypes.POINTER(PyObject)), 106 | ] 107 | 108 | def patchable_builtin(klass): 109 | name = klass.__name__ 110 | target = getattr(klass, "__dict__", name) 111 | 112 | class SlotsProxy(PyObject): 113 | _fields_ = [("dict", ctypes.POINTER(PyObject))] 114 | 115 | proxy_dict = SlotsProxy.from_address(id(target)) 116 | namespace = {} 117 | 118 | ctypes.pythonapi.PyDict_SetItem( 119 | ctypes.py_object(namespace), 120 | ctypes.py_object(name), 121 | proxy_dict.dict, 122 | ) 123 | 124 | return namespace[name] 125 | 126 | return patchable_builtin 127 | 128 | 129 | patchable_builtin = craft_patchable_builtin() 130 | is_cpython = runtime_is_cpython() 131 | -------------------------------------------------------------------------------- /tests/test_runtime/test_feature.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) <2010-2024> Gabriel Falcão 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | """tests for :class:`sure.runtime.Feature`""" 19 | 20 | from unittest.mock import patch, call 21 | from unittest.mock import Mock as Spy 22 | from sure import expects 23 | from sure.runtime import Feature, RuntimeOptions, ScenarioResult, Scenario 24 | from sure.doubles import stub 25 | from sure.errors import ExitFailure, ExitError 26 | 27 | 28 | description = "tests for :class:`sure.runtime.Feature`" 29 | 30 | 31 | def test_feature_with_description(): 32 | "repr(sure.runtime.Feature) with description" 33 | 34 | feature = stub(Feature, title="title", description="description") 35 | 36 | expects(repr(feature)).to.equal('') 37 | 38 | 39 | def test_feature_without_description(): 40 | "repr(sure.runtime.Feature) with description" 41 | 42 | feature = stub(Feature, title="title", description=None) 43 | 44 | expects(repr(feature)).to.equal('') 45 | 46 | 47 | @patch('sure.errors.sys.exit') 48 | @patch('sure.runtime.RuntimeContext') 49 | def test_feature_run_is_failure(RuntimeContext, exit): 50 | 'Feature.run() should raise :class:`sure.errors.ExitFailure` at the occurrence of failure within an "immediate" failure context' 51 | 52 | reporter_spy = Spy(name='Reporter') 53 | scenario_result = stub(ScenarioResult, is_failure=True, __failure__=AssertionError('contrived failure'), __error__=None) 54 | scenario_run_spy = Spy(name="Scenario.run", return_value=scenario_result) 55 | 56 | scenario_stub = stub(Scenario, run=scenario_run_spy) 57 | feature_stub = stub( 58 | Feature, 59 | title="failure feature test", 60 | description=None, 61 | scenarios=[scenario_stub] 62 | ) 63 | 64 | expects(feature_stub.run).when.called_with(reporter=reporter_spy, runtime=RuntimeOptions(immediate=True)).to.have.raised( 65 | ExitFailure, 66 | 'ExitFailure' 67 | ) 68 | expects(reporter_spy.mock_calls).to.equal([ 69 | call.on_failure(scenario_stub, scenario_result) 70 | ]) 71 | 72 | 73 | @patch('sure.errors.sys.exit') 74 | @patch('sure.runtime.RuntimeContext') 75 | def test_feature_run_is_error(RuntimeContext, exit): 76 | 'Feature.run() should raise :class:`sure.errors.ExitError` at the occurrence of error within an "immediate" error context' 77 | 78 | reporter_spy = Spy(name='Reporter') 79 | scenario_run_spy = Spy(name="Scenario.run") 80 | scenario_stub = stub(Scenario, run=scenario_run_spy) 81 | feature_stub = stub( 82 | Feature, 83 | title="error feature test", 84 | description=None, 85 | scenarios=[scenario_stub] 86 | ) 87 | scenario_result = stub(ScenarioResult, is_error=True, __error__=ValueError('contrived error'), __failure__=None, is_failure=False, scenario=scenario_stub) 88 | scenario_run_spy.return_value = scenario_result 89 | 90 | expects(feature_stub.run).when.called_with(reporter=reporter_spy, runtime=RuntimeOptions(immediate=True)).to.have.raised( 91 | ExitError, 92 | 'ExitError' 93 | ) 94 | expects(reporter_spy.mock_calls).to.equal([ 95 | call.on_error(scenario_stub, scenario_result) 96 | ]) 97 | -------------------------------------------------------------------------------- /docs/source/guide.rst.pending: -------------------------------------------------------------------------------- 1 | .. _Guide: 2 | 3 | Guide 4 | ===== 5 | 6 | .. note:: The sections in this guide make use of the :ref:`Special Syntax` 7 | 8 | 9 | Setup/Teardown 10 | -------------- 11 | 12 | It might not be uncommon for developers to be familiar with to how the :mod:`unittest` module 13 | suggests to `implement setup and teardown callbacks `_ 14 | for your tests. 15 | 16 | But if you prefer to define test cases as functions and use a runner 17 | like `nose `_ then *sure* can 18 | help you define and activate modular fixtures. 19 | 20 | In *sure's* parlance, we call it a *Scenario* 21 | 22 | 23 | Example: Setup a Flask app for testing 24 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 25 | 26 | 27 | ``my_flask_app.py`` 28 | ................... 29 | 30 | .. code:: 31 | 32 | import json 33 | from flask import Response, Flask 34 | 35 | webapp = Flask(__name__) 36 | 37 | @webapp.route('/') 38 | def index(): 39 | data = json.dumps({'hello': 'world'}} 40 | return Response(data, headers={'Content-Type': 'application/json'}) 41 | 42 | 43 | ``tests/scenarios.py`` 44 | ...................... 45 | 46 | .. code:: python 47 | 48 | from sure import scenario 49 | from my_flask_app import webapp 50 | 51 | def prepare_webapp(context): 52 | context.server = webapp.test_client() 53 | 54 | web_scenario = scenario(prepare_webapp) 55 | 56 | 57 | ``tests/test_webapp.py`` 58 | ........................ 59 | 60 | .. code:: python 61 | 62 | import json 63 | from sure import scenario 64 | from tests.scenarios import web_scenario 65 | 66 | @web_scenario 67 | def test_hello_world(context): 68 | # Given that I GET / 69 | response = context.server.get('/') 70 | 71 | # Then it should have returned a successful json response 72 | response.headers.should.have.key('Content-Type').being.equal('application/json') 73 | response.status_code.should.equal(200) 74 | 75 | json.loads(response.data).should.equal({'hello': 'world'}) 76 | 77 | 78 | Example: Multiple Setup and Teardown functions 79 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 80 | 81 | 82 | ``tests/scenarios.py`` 83 | ...................... 84 | 85 | .. code:: python 86 | 87 | import os 88 | import shutil 89 | from sure import scenario 90 | 91 | def prepare_directories(context): 92 | context.root = os.path.dirname(os.path.abspath(__file__)) 93 | context.fixture_path = os.path.join(context.root, 'input_data') 94 | context.result_path = os.path.join(context.root, 'output_data') 95 | context.directories = [ 96 | context.fixture_path, 97 | context.result_path, 98 | ] 99 | 100 | for path in context.directories: 101 | if os.path.isdir(path): 102 | shutil.rmtree(path) 103 | 104 | os.makedirs(path) 105 | 106 | 107 | def cleanup_directories(context): 108 | for path in context.directories: 109 | if os.path.isdir(path): 110 | shutil.rmtree(path) 111 | 112 | 113 | def create_10_dummy_hex_files(context): 114 | for index in range(10): 115 | filename = os.path.join(context.fixture_path, 'dummy-{}.hex'.format(index)) 116 | open(filename, 'wb').write(os.urandom(32).encode('hex')) 117 | 118 | 119 | dummy_files_scenario = scenario([create_directories, create_10_dummy_hex_files], [cleanup_directories]) 120 | 121 | 122 | ``tests/test_filesystem.py`` 123 | ............................ 124 | 125 | .. code:: python 126 | 127 | import os 128 | from tests.scenarios import dummy_files_scenario 129 | 130 | @dummy_files_scenario 131 | def test_files_exist(context): 132 | os.listdir(context.fixture_path).should.equal([ 133 | 'dummy-0.hex', 134 | 'dummy-1.hex', 135 | 'dummy-2.hex', 136 | 'dummy-3.hex', 137 | 'dummy-4.hex', 138 | 'dummy-5.hex', 139 | 'dummy-6.hex', 140 | 'dummy-7.hex', 141 | 'dummy-8.hex', 142 | 'dummy-9.hex', 143 | ]) 144 | -------------------------------------------------------------------------------- /tests/test_runtime/test_scenario_result_set.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) <2010-2024> Gabriel Falcão 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | """tests for :class:`sure.runtime.ScenarioResultSet`""" 18 | 19 | import sys 20 | from sure import expects 21 | from sure.doubles import stub 22 | from sure.loader import collapse_path 23 | from sure.runtime import ( 24 | ErrorStack, 25 | RuntimeContext, 26 | Scenario, 27 | ScenarioResult, 28 | ScenarioResultSet, 29 | TestLocation, 30 | ) 31 | 32 | description = "tests for :class:`sure.runtime.ScenarioResultSet`" 33 | 34 | 35 | def test_scenario_result_set(): 36 | "ScenarioResultSet discerns types of :class:`sure.runtime.ScenarioResult` instances" 37 | 38 | scenario_results = [ 39 | stub(ScenarioResult, __error__=None, __failure__=None), 40 | stub(ScenarioResult, __error__=None, __failure__=AssertionError('y')), 41 | stub(ScenarioResult, __error__=InterruptedError('x'), __failure__=None), 42 | stub(ScenarioResult, __error__=None, __failure__=AssertionError('Y')), 43 | stub(ScenarioResult, __error__=InterruptedError('X'), __failure__=None), 44 | stub(ScenarioResult, __error__=None, __failure__=None), 45 | ] 46 | 47 | scenario_result_set = ScenarioResultSet(scenario_results, context=stub(RuntimeContext)) 48 | 49 | expects(scenario_result_set).to.have.property('failed_scenarios').being.length_of(2) 50 | expects(scenario_result_set).to.have.property('errored_scenarios').being.length_of(2) 51 | expects(scenario_result_set).to.have.property('scenario_results').being.length_of(6) 52 | 53 | 54 | def test_scenario_result_set_printable_error(): 55 | "ScenarioResultSet.printable presents reference to first error occurrence" 56 | 57 | scenario_results = [ 58 | stub(ScenarioResult, __error__=None, __failure__=None), 59 | stub(ScenarioResult, __error__=InterruptedError('x'), __failure__=None), 60 | stub(ScenarioResult, __error__=InterruptedError('X'), __failure__=None), 61 | stub(ScenarioResult, __error__=None, __failure__=None), 62 | ] 63 | 64 | scenario_result_set = ScenarioResultSet(scenario_results, context=stub(RuntimeContext)) 65 | 66 | expects(scenario_result_set.printable()).to.equal("InterruptedError: x") 67 | 68 | 69 | def test_scenario_result_set_printable_failure(): 70 | "ScenarioResultSet.printable presents reference to first failure occurrence" 71 | 72 | scenario_results = [ 73 | stub(ScenarioResult, __error__=None, __failure__=None), 74 | stub(ScenarioResult, __error__=None, __failure__=AssertionError('Y')), 75 | stub(ScenarioResult, __error__=InterruptedError('x'), __failure__=None), 76 | stub(ScenarioResult, __error__=None, __failure__=AssertionError('y')), 77 | stub(ScenarioResult, __error__=InterruptedError('X'), __failure__=None), 78 | stub(ScenarioResult, __error__=None, __failure__=None), 79 | ] 80 | 81 | scenario_result_set = ScenarioResultSet(scenario_results, context=stub(RuntimeContext)) 82 | 83 | expects(scenario_result_set.printable()).to.equal("AssertionError: Y") 84 | 85 | 86 | def test_scenario_result_set_printable_no_errors_or_failures(): 87 | "ScenarioResultSet.printable presents empty string when there are no errors or failures" 88 | 89 | scenario_results = [ 90 | stub(ScenarioResult, __error__=None, __failure__=None), 91 | ] 92 | 93 | scenario_result_set = ScenarioResultSet(scenario_results, context=stub(RuntimeContext)) 94 | 95 | expects(scenario_result_set.printable()).to.be.a(str) 96 | expects(scenario_result_set.printable()).to.be.empty 97 | -------------------------------------------------------------------------------- /tests/unit/reporters/test_test_reporter.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) <2010-2024> Gabriel Falcão 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | "unit tests for :mod:`sure.reporters.test`" 18 | 19 | from unittest.mock import patch, call 20 | from sure import expects 21 | from sure.runner import Runner 22 | from sure.runtime import Feature, Scenario, TestLocation, ScenarioResult, RuntimeContext 23 | from sure.reporters.test import TestReporter 24 | from sure.doubles import stub 25 | from sure.errors import InternalRuntimeError 26 | 27 | 28 | @patch("sure.reporters.test.events") 29 | @patch("sure.reporters.test.time") 30 | def test_test_reporter_on_failure(time, events): 31 | "TestReporter.on_failure()" 32 | 33 | time.time.return_value = "on_failure" 34 | reporter = TestReporter(stub(Runner)) 35 | scenario = stub(Scenario, name="scenario-stub-name") 36 | scenario_result = stub( 37 | ScenarioResult, __error__=None, __failure__=AssertionError("test") 38 | ) 39 | reporter.on_failure(scenario, scenario_result) 40 | 41 | expects(events.mock_calls).to.equal( 42 | [ 43 | call.__getitem__("on_failure"), 44 | call.__getitem__().append(("on_failure", "scenario-stub-name", "failure")), 45 | ] 46 | ) 47 | 48 | 49 | @patch("sure.reporters.test.events") 50 | @patch("sure.reporters.test.time") 51 | def test_test_reporter_on_success(time, events): 52 | "TestReporter.on_success()" 53 | 54 | time.time.return_value = "on_success" 55 | reporter = TestReporter(stub(Runner)) 56 | scenario = stub(Scenario, name="scenario-stub-name") 57 | reporter.on_success(scenario) 58 | 59 | expects(events.mock_calls).to.equal( 60 | [ 61 | call.__getitem__("on_success"), 62 | call.__getitem__().append(("on_success", "scenario-stub-name")), 63 | ] 64 | ) 65 | 66 | 67 | @patch("sure.reporters.test.events") 68 | @patch("sure.reporters.test.time") 69 | def test_test_reporter_on_error(time, events): 70 | "TestReporter.on_error()" 71 | 72 | time.time.return_value = "on_error" 73 | reporter = TestReporter(stub(Runner)) 74 | scenario = stub(Scenario, name="scenario-stub-name") 75 | scenario_result = stub( 76 | ScenarioResult, __failure__=None, __error__=RuntimeError("test") 77 | ) 78 | reporter.on_error(scenario, scenario_result) 79 | 80 | expects(events.mock_calls).to.equal( 81 | [ 82 | call.__getitem__("on_error"), 83 | call.__getitem__().append(("on_error", "scenario-stub-name", "error")), 84 | ] 85 | ) 86 | 87 | 88 | @patch("sure.reporters.test.events") 89 | @patch("sure.reporters.test.time") 90 | def test_test_reporter_on_internal_runtime_error(time, events): 91 | "TestReporter.on_internal_runtime_error()" 92 | 93 | time.time.return_value = "on_internal_runtime_error" 94 | reporter = TestReporter(stub(Runner)) 95 | context = stub(RuntimeContext, name="scenario-stub-name", reporter=reporter) 96 | error = InternalRuntimeError(context, TypeError("test")) 97 | scenario_result = stub(ScenarioResult, __failure__=None, __error__=error) 98 | reporter.on_internal_runtime_error(context, scenario_result) 99 | 100 | expects(events.mock_calls).to.equal( 101 | [ 102 | call.__getitem__("on_internal_runtime_error"), 103 | call.__getitem__().append(("on_internal_runtime_error", context, error)), 104 | call.__getitem__("on_internal_runtime_error"), 105 | call.__getitem__().append( 106 | ("on_internal_runtime_error", context, scenario_result) 107 | ), 108 | ] 109 | ) 110 | -------------------------------------------------------------------------------- /tests/test_runtime/test_feature_result.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) <2010-2024> Gabriel Falcão 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | """tests for :class:`sure.runtime.FeatureResult`""" 18 | 19 | import sys 20 | from sure import expects 21 | from sure.doubles import stub 22 | from sure.loader import collapse_path 23 | from sure.runtime import ( 24 | ErrorStack, 25 | RuntimeContext, 26 | Scenario, 27 | ScenarioResult, 28 | ScenarioResultSet, 29 | FeatureResult, 30 | TestLocation, 31 | ) 32 | 33 | description = "tests for :class:`sure.runtime.FeatureResult`" 34 | 35 | 36 | def test_feature_result(): 37 | "FeatureResult discerns types of :class:`sure.runtime.ScenarioResult` instances" 38 | 39 | context = stub(RuntimeContext) 40 | scenario_result_sets = [ 41 | ScenarioResultSet([stub(ScenarioResult, __error__=None, __failure__=None)], context=context), 42 | ScenarioResultSet([stub(ScenarioResult, __error__=None, __failure__=AssertionError('dummy'))], context=context), 43 | ScenarioResultSet([stub(ScenarioResult, __error__=ValueError('dummy'), __failure__=None)], context=context), 44 | ScenarioResultSet([stub(ScenarioResult, __error__=None, __failure__=AssertionError('dummy'))], context=context), 45 | ScenarioResultSet([stub(ScenarioResult, __error__=ValueError('dummy'), __failure__=None)], context=context), 46 | ScenarioResultSet([stub(ScenarioResult, __error__=None, __failure__=AssertionError('dummy'))], context=context), 47 | ScenarioResultSet([stub(ScenarioResult, __error__=ValueError('dummy'), __failure__=None)], context=context), 48 | ] 49 | 50 | feature_result = FeatureResult(scenario_result_sets) 51 | 52 | expects(feature_result).to.have.property('failed_scenarios').being.length_of(3) 53 | expects(feature_result).to.have.property('errored_scenarios').being.length_of(3) 54 | expects(feature_result).to.have.property('scenario_results').being.length_of(7) 55 | 56 | 57 | def test_feature_result_printable_with_failure(): 58 | "Feature.printable presents reference to first failure occurrence" 59 | 60 | context = stub(RuntimeContext) 61 | scenario_result_sets = [ 62 | ScenarioResultSet([stub(ScenarioResult, __error__=None, __failure__=None)], context=context), 63 | ScenarioResultSet([stub(ScenarioResult, __error__=None, __failure__=AssertionError('dummy'))], context=context), 64 | ScenarioResultSet([stub(ScenarioResult, __error__=ValueError('dummy'), __failure__=None)], context=context), 65 | ScenarioResultSet([stub(ScenarioResult, __error__=None, __failure__=AssertionError('dummy'))], context=context), 66 | ScenarioResultSet([stub(ScenarioResult, __error__=ValueError('dummy'), __failure__=None)], context=context), 67 | ScenarioResultSet([stub(ScenarioResult, __error__=None, __failure__=AssertionError('dummy'))], context=context), 68 | ScenarioResultSet([stub(ScenarioResult, __error__=ValueError('dummy'), __failure__=None)], context=context), 69 | ] 70 | 71 | feature_result = FeatureResult(scenario_result_sets) 72 | 73 | expects(feature_result.printable()).to.equal("AssertionError: dummy") 74 | 75 | 76 | def test_feature_result_printable_with_error(): 77 | "Feature.printable presents reference to first error occurrence" 78 | 79 | context = stub(RuntimeContext) 80 | scenario_result_sets = [ 81 | ScenarioResultSet([stub(ScenarioResult, __error__=None, __failure__=None)], context=context), 82 | ScenarioResultSet([stub(ScenarioResult, __error__=ValueError('dummy'), __failure__=None)], context=context), 83 | ] 84 | 85 | feature_result = FeatureResult(scenario_result_sets) 86 | 87 | expects(feature_result.printable()).to.equal("ValueError: dummy") 88 | -------------------------------------------------------------------------------- /examples/unit-tests/setup_and_teardown_with_behavior.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import json 4 | import requests 5 | from gevent.pool import Pool 6 | from flask import Flask, Response 7 | from sqlalchemy import create_engine 8 | from sqlalchemy import MetaData 9 | 10 | from myapp.db import sqlalchemy_metadata 11 | from sure.scenario import BehaviorDefinition, apply_behavior 12 | 13 | 14 | # Changes in version 1.5.0 [draft] 15 | # ~~~~~~~~~~~~~~~~~~~~~~~~ 16 | # 17 | # * Introducing the concept of BehaviorDefinition: a clean and 18 | # decoupled way to reutilize setup/teardown behaviors. So instead of 19 | # the classic massive setup/teardown methods and/or chaotic 20 | # ``unittest.TestCase`` subclass inheritance every test can be 21 | # decorated with @apply_behavior(CustomBehaviorDefinitionTypeObject) 22 | # 23 | # * Avoid using the word "test" in your "Behavior Definitions" so that 24 | # nose will not mistake your BehaviorDefinition with an actual test 25 | # case class and thus execute .setup() and .teardown() in an 26 | # undesired manner. 27 | 28 | 29 | 30 | def get_json_request(self): 31 | """parses the request body as JSON without running any sort of validation""" 32 | return json.loads(request.data) 33 | 34 | def json_response(response_body, status=200, headers=None): 35 | """utility that automatically serializes the provided payload in JSON 36 | and generates :py:`flask.Response` with the ``application/json`` 37 | content-type. 38 | 39 | :param response_body: a python dictionary or any JSON-serializable python object. 40 | 41 | """ 42 | headers = headers or {} 43 | serialized = json.dumps(response_body, indent=2) 44 | headers[b'Content-Type'] = 'application/json' 45 | return Response(serialized, status=code, headers=headers) 46 | 47 | 48 | class GreenConcurrencyBehaviorDefinition(BehaviorDefinition): 49 | # NOTE: 50 | # ---- 51 | # 52 | # * Sure uses ``context_namespace`` internally to namespace the 53 | # self-assigned attributes into the context in order to prevent 54 | # attribute name collision. 55 | 56 | context_namespace = 'green' 57 | 58 | def setup(self, pool_size=1): 59 | self.pool = Pool(pool_size) 60 | 61 | 62 | class UseFakeHTTPAPIServer(GreenConcurrencyBehaviorDefinition): 63 | context_namespace = 'fake_api' 64 | 65 | def setup(self, http_port): 66 | # NOTES: 67 | # ~~~~~~ 68 | # 69 | # * GreenConcurrencyBehaviorDefinition.setup() is automatically called by 70 | # * sure in the correct order 71 | # 72 | # * Sure automatically takes care of performing top-down calls 73 | # to every parent of your behavior. 74 | # 75 | # * In simple words, this UseFakeHTTPAPIServer behavior will automatically call GreenConcurrencyBehaviorDefinition 76 | 77 | # 1. Create a simple Flask server 78 | self.server = Flask('fake.http.api.server') 79 | # 2. Setup fake routes 80 | self.server.add_url_rule('/auth', view_func=self.fake_auth_endpoint) 81 | self.server.add_url_rule('/item/', view_func=self.fake_get_item_endpoint) 82 | # 3. Run the server 83 | self.pool.spawn(self..server.run, port=http_port) 84 | 85 | def teardown(self): 86 | self.server.stop() 87 | 88 | def fake_auth_endpoint(self): 89 | data = get_json_request() 90 | data['token'] = '$2b$12$heKIpWg0wJQ6BeKxPSJCP.7Vf9hn8s6yFs8yGWnWdPZ48toXbjK9W', 91 | return json_response(data) 92 | 93 | def fake_get_item_endpoint(self, uid): 94 | return json_response({ 95 | 'uid': uid, 96 | 'title': 'fake item', 97 | }) 98 | 99 | 100 | class CleanSlateDatabase(BehaviorDefinition): 101 | # Sure uses ``context_namespace`` internally to namespace the 102 | # self-assigned attributes into the context in order to prevent 103 | # attribute name collision 104 | context_namespace = 'db' 105 | 106 | def setup(self, sqlalchemy_database_uri='mysql://root@localhost/test-database'): 107 | self.engine = create_engine(sqlalchemy_database_uri) 108 | self.metadata = sqlalchemy_metadata 109 | # Dropping the whole schema just in case a previous test 110 | # execution fails and leaves the database dirty before having 111 | # the chance to run .teardown() 112 | self.metadata.drop_all(engine) 113 | self.metadata.create_all(engine) 114 | 115 | 116 | @apply_behavior(UseFakeHTTPAPIServer, http_port=5001) 117 | def test_with_real_network_io(context): 118 | response = requests.post('http://localhosst:5001/auth', data=json.dumps({'username': 'foobar'})) 119 | response.headers.should.have.key('Content-Type').being.equal('application/json') 120 | response.json().should.equal({ 121 | 'token': '$2b$12$heKIpWg0wJQ6BeKxPSJCP.7Vf9hn8s6yFs8yGWnWdPZ48toXbjK9W', 122 | 'username': 'foobar', 123 | }) 124 | -------------------------------------------------------------------------------- /examples/functional-tests/behavior_definition_with_live_network_servers.py: -------------------------------------------------------------------------------- 1 | # # -*- coding: utf-8 -*- 2 | # 3 | # import json 4 | # import requests 5 | # from gevent.pool import Pool 6 | # from flask import Flask, Response 7 | # from sqlalchemy import create_engine 8 | # from sqlalchemy import MetaData 9 | # 10 | # from myapp.db import sqlalchemy_metadata 11 | # from sure.scenario import BehaviorDefinition, apply_behavior 12 | # 13 | # 14 | # # Changes in version 1.5.0 [draft] 15 | # # ~~~~~~~~~~~~~~~~~~~~~~~~ 16 | # # 17 | # # * Introducing the concept of BehaviorDefinition: a clean and 18 | # # decoupled way to reutilize setup/teardown behaviors. So instead of 19 | # # the classic massive setup/teardown methods and/or chaotic 20 | # # ``unittest.TestCase`` subclass inheritance every test can be 21 | # # decorated with @apply_behavior(CustomBehaviorDefinitionTypeObject) 22 | # # 23 | # # * Avoid using the word "test" in your "Behavior Definitions" so that 24 | # # nose will not mistake your BehaviorDefinition with an actual test 25 | # # case class and thus execute .setup() and .teardown() in an 26 | # # undesired manner. 27 | # 28 | # 29 | # def get_json_request(self): 30 | # """parses the request body as JSON without running any sort of validation""" 31 | # return json.loads(request.data) 32 | # 33 | # 34 | # def json_response(response_body, status=200, headers=None): 35 | # """utility that automatically serializes the provided payload in JSON 36 | # and generates :py:class:`flask.Response` with the ``application/json`` 37 | # content-type. 38 | # 39 | # :param response_body: a python dictionary or any JSON-serializable python object. 40 | # 41 | # """ 42 | # headers = headers or {} 43 | # serialized = json.dumps(response_body, indent=2) 44 | # headers[b'Content-Type'] = 'application/json' 45 | # return Response(serialized, status=code, headers=headers) 46 | # 47 | # 48 | # class GreenConcurrencyBehaviorDefinition(BehaviorDefinition): 49 | # # NOTE: 50 | # # ---- 51 | # # 52 | # # * Sure uses ``context_namespace`` internally to namespace the 53 | # # self-assigned attributes into the context in order to prevent 54 | # # attribute name collision. 55 | # 56 | # context_namespace = 'green' 57 | # 58 | # def setup(self, pool_size=1): 59 | # self.pool = Pool(pool_size) 60 | # 61 | # 62 | # class UseFakeHTTPAPIServer(GreenConcurrencyBehaviorDefinition): 63 | # context_namespace = 'fake_api' 64 | # 65 | # def setup(self, http_port): 66 | # # NOTES: 67 | # # ~~~~~~ 68 | # # 69 | # # * GreenConcurrencyBehaviorDefinition.setup() is automatically called by 70 | # # * sure in the correct order 71 | # # 72 | # # * Sure automatically takes care of performing top-down calls 73 | # # to every parent of your behavior. 74 | # # 75 | # # * In simple words, this UseFakeHTTPAPIServer behavior will automatically call GreenConcurrencyBehaviorDefinition 76 | # 77 | # # 1. Create a simple Flask server 78 | # self.server = Flask('fake.http.api.server') 79 | # # 2. Setup fake routes 80 | # self.server.add_url_rule('/auth', view_func=self.fake_auth_endpoint) 81 | # self.server.add_url_rule('/item/', view_func=self.fake_get_item_endpoint) 82 | # # 3. Run the server 83 | # self.pool.spawn(self.server.run, port=http_port) 84 | # 85 | # def teardown(self): 86 | # self.server.stop() 87 | # 88 | # def fake_auth_endpoint(self): 89 | # data = get_json_request() 90 | # data['token'] = '$2b$12$heKIpWg0wJQ6BeKxPSJCP.7Vf9hn8s6yFs8yGWnWdPZ48toXbjK9W', 91 | # return json_response(data) 92 | # 93 | # def fake_get_item_endpoint(self, uid): 94 | # return json_response({ 95 | # 'uid': uid, 96 | # 'title': 'fake item', 97 | # }) 98 | # 99 | # 100 | # class CleanSlateDatabase(BehaviorDefinition): 101 | # # Sure uses ``context_namespace`` internally to namespace the 102 | # # self-assigned attributes into the context in order to prevent 103 | # # attribute name collision 104 | # context_namespace = 'db' 105 | # 106 | # def setup(self, sqlalchemy_database_uri='mysql://root@localhost/test-database'): 107 | # self.engine = create_engine(sqlalchemy_database_uri) 108 | # self.metadata = sqlalchemy_metadata 109 | # # Dropping the whole schema just in case a previous test 110 | # # execution fails and leaves the database dirty before having 111 | # # the chance to run .teardown() 112 | # self.metadata.drop_all(engine) 113 | # self.metadata.create_all(engine) 114 | # 115 | # 116 | # @apply_behavior(UseFakeHTTPAPIServer, http_port=5001) 117 | # def test_with_real_network_io(context): 118 | # response = requests.post('http://localhosst:5001/auth', data=json.dumps({'username': 'foobar'})) 119 | # response.headers.should.have.key('Content-Type').being.equal('application/json') 120 | # response.json().should.equal({ 121 | # 'token': '$2b$12$heKIpWg0wJQ6BeKxPSJCP.7Vf9hn8s6yFs8yGWnWdPZ48toXbjK9W', 122 | # 'username': 'foobar', 123 | # }) 124 | # 125 | -------------------------------------------------------------------------------- /tests/unit/test_terminal.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) <2010-2024> Gabriel Falcão 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | from sure import expects 18 | from sure import terminal 19 | from mock import patch 20 | 21 | 22 | description = "tests for :class:`sure.terminal`" 23 | 24 | 25 | @patch('sure.terminal.os') 26 | def test_has_ansi_support_disabled_env_var_set(os): 27 | "sure.terminal.has_ansi_support() returns False when environment variable SURE_NO_COLORS is set" 28 | 29 | os.getenv.return_value = 'true' 30 | 31 | expects(terminal.has_ansi_support(os=os)).to.equal(False) 32 | 33 | 34 | @patch('sure.terminal.platform') 35 | @patch('sure.terminal.sys') 36 | @patch('sure.terminal.os') 37 | def test_has_ansi_support_enabled(os, sys, platform): 38 | "sure.terminal.has_ansi_support() returns True" 39 | 40 | platform.system.return_value = "Unix" 41 | sys.stdout.isatty.return_value = True 42 | sys.stderr.isatty.return_value = True 43 | os.getenv.return_value = False 44 | os.environ = { 45 | 'TERM': 'ANSI' 46 | } 47 | 48 | expects(terminal.has_ansi_support(os, sys, platform)).to.equal(True) 49 | 50 | 51 | @patch('sure.terminal.platform') 52 | @patch('sure.terminal.sys') 53 | @patch('sure.terminal.os') 54 | def test_has_ansi_support_disabled_for_windows(os, sys, platform): 55 | "sure.terminal.has_ansi_support() returns False when on Windows™ platform" 56 | 57 | platform.system.return_value = "Windows" 58 | sys.stdout.isatty.return_value = True 59 | sys.stderr.isatty.return_value = True 60 | os.getenv.return_value = False 61 | os.environ = { 62 | 'TERM': 'ANSI' 63 | } 64 | 65 | expects(terminal.has_ansi_support(os, sys, platform)).to.equal(False) 66 | 67 | 68 | @patch('sure.terminal.platform') 69 | @patch('sure.terminal.sys') 70 | @patch('sure.terminal.os') 71 | def test_has_ansi_support_disabled_with_term_env_var_non_ansi(os, sys, platform): 72 | "sure.terminal.has_ansi_support() returns True" 73 | 74 | platform.system.return_value = "Unix" 75 | sys.stdout.isatty.return_value = True 76 | sys.stderr.isatty.return_value = True 77 | os.getenv.return_value = False 78 | os.environ = { 79 | 'TERM': '' 80 | } 81 | 82 | expects(terminal.has_ansi_support(os, sys, platform)).to.equal(False) 83 | 84 | 85 | @patch('sure.terminal.has_ansi_support') 86 | def test_red_with_ansi_support(has_ansi_support): 87 | "sure.terminal.red() with ANSI support" 88 | 89 | has_ansi_support.return_value = True 90 | 91 | expects(terminal.red("blue")).to.equal(r"\033[1;31mblue\033[0m") 92 | 93 | 94 | @patch('sure.terminal.has_ansi_support') 95 | def test_red_without_ansi_support(has_ansi_support): 96 | "sure.terminal.red() without ANSI support" 97 | 98 | has_ansi_support.return_value = False 99 | 100 | expects(terminal.red("blue")).to.equal(r"blue") 101 | 102 | 103 | @patch('sure.terminal.has_ansi_support') 104 | def test_white_with_ansi_support(has_ansi_support): 105 | "sure.terminal.white() with ANSI support" 106 | 107 | has_ansi_support.return_value = True 108 | 109 | expects(terminal.white("green")).to.equal(r"\033[1;37mgreen\033[0m") 110 | 111 | 112 | @patch('sure.terminal.has_ansi_support') 113 | def test_white_without_ansi_support(has_ansi_support): 114 | "sure.terminal.white() without ANSI support" 115 | 116 | has_ansi_support.return_value = False 117 | 118 | expects(terminal.white("green")).to.equal(r"green") 119 | 120 | 121 | @patch('sure.terminal.has_ansi_support') 122 | def test_yellow_with_ansi_support(has_ansi_support): 123 | "sure.terminal.yellow() with ANSI support" 124 | 125 | has_ansi_support.return_value = True 126 | 127 | expects(terminal.yellow("blue")).to.equal(r"\033[1;33mblue\033[0m") 128 | 129 | 130 | @patch('sure.terminal.has_ansi_support') 131 | def test_yellow_without_ansi_support(has_ansi_support): 132 | "sure.terminal.yellow() without ANSI support" 133 | 134 | has_ansi_support.return_value = False 135 | 136 | expects(terminal.yellow("blue")).to.equal(r"blue") 137 | 138 | 139 | @patch('sure.terminal.has_ansi_support') 140 | def test_green_with_ansi_support(has_ansi_support): 141 | "sure.terminal.green() with ANSI support" 142 | 143 | has_ansi_support.return_value = True 144 | 145 | expects(terminal.green("blue")).to.equal(r"\033[1;32mblue\033[0m") 146 | 147 | 148 | @patch('sure.terminal.has_ansi_support') 149 | def test_green_without_ansi_support(has_ansi_support): 150 | "sure.terminal.green() without ANSI support" 151 | 152 | has_ansi_support.return_value = False 153 | 154 | expects(terminal.green("blue")).to.equal(r"blue") 155 | -------------------------------------------------------------------------------- /tests/unit/test_doubles.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) <2010-2024> Gabriel Falcão 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | "unit tests for :mod:`sure.doubles`" 19 | 20 | from sure import expects 21 | from sure.doubles.dummies import Dummy, anything_of_type, AnythingOfType 22 | from sure.doubles.fakes import FakeOrderedDict 23 | from sure.doubles.stubs import stub 24 | 25 | 26 | class AutoMobile(object): 27 | def __init__(self, model: str, manufacturer: str, year: int, color: str, features: dict): 28 | self.model = model 29 | self.year = year 30 | self.color = color 31 | 32 | 33 | def test_stub(): 34 | ":func:`sure.doubles.stubs.stub` should create a stub with the given type and keyword-args" 35 | 36 | veraneio = stub( 37 | AutoMobile, 38 | model="Omega", 39 | manufacturer="Chevrolet", 40 | year=1993, 41 | color="Graphite", 42 | ) 43 | expects(veraneio).to.be.an(AutoMobile) 44 | expects(veraneio).to.have.property("year").being.equal(1993) 45 | expects(veraneio).to.have.property("model").being.equal("Omega") 46 | expects(veraneio).to.have.property("color").being.equal("Graphite") 47 | expects(veraneio).to.have.property("manufacturer").being.equal("Chevrolet") 48 | 49 | 50 | def test_stub_without_base_class(): 51 | ":func:`sure.doubles.stubs.stub` should create an opaque object when not providing a `base_class' param" 52 | 53 | auto = stub( 54 | model="Opala", 55 | manufacturer="Chevrolet", 56 | year=71, 57 | color="White", 58 | features={"vinyl_roof": "black"}, 59 | ) 60 | expects(auto).to.have.property("model").being.equal("Opala") 61 | expects(auto).to.have.property("manufacturer").being.equal("Chevrolet") 62 | expects(auto).to.have.property("color").being.equal("White") 63 | 64 | 65 | def test_fake_ordered_dict_str(): 66 | ":meth:`sure.doubles.fakes.FakeOrderedDict.__str__` should be similar to that of :meth:`dict.__str__`" 67 | 68 | fake_ordered_dict = FakeOrderedDict([("a", "A"), ("z", "Z")]) 69 | expects(str(fake_ordered_dict)).to.equal("{'a': 'A', 'z': 'Z'}") 70 | expects(str(FakeOrderedDict())).to.equal("{}") 71 | 72 | 73 | def test_fake_ordered_dict_repr(): 74 | ":meth:`sure.doubles.fakes.FakeOrderedDict.__repr__` should be similar to that of :meth:`dict.__repr__`" 75 | 76 | fake_ordered_dict = FakeOrderedDict([("a", "A"), ("z", "Z")]) 77 | expects(repr(fake_ordered_dict)).to.equal("{'a': 'A', 'z': 'Z'}") 78 | expects(repr(FakeOrderedDict())).to.equal("{}") 79 | 80 | 81 | def test_dummy(): 82 | "Dummy() should return the dummy_id" 83 | 84 | dummy = Dummy("some dummy id") 85 | expects(dummy).to.have.property("__dummy_id__").being.equal("some dummy id") 86 | 87 | expects(str(dummy)).to.equal("") 88 | expects(repr(dummy)).to.equal("") 89 | 90 | 91 | def test_dummy_takes_exclusively_string_as_id(): 92 | "Dummy() should throw exception when receiving a non-string param" 93 | 94 | expects(Dummy).when.called_with(299).should.throw( 95 | TypeError, 96 | "sure.doubles.dummies.Dummy() takes string as argument, received 299 () instead", 97 | ) 98 | 99 | 100 | def test_anything_of_type_should_return_an_instance_of_the_anythingoftype_class(): 101 | "anything_of_type() should return an instance of the AnythingOfType class" 102 | 103 | expects(anything_of_type(str)).to.be.an(AnythingOfType) 104 | expects(str(anything_of_type(str))).to.equal( 105 | "" 106 | ) 107 | expects(repr(anything_of_type(str))).to.equal( 108 | "" 109 | ) 110 | 111 | 112 | def test_anything_of_type_should_equal_any_python_object_of_the_given_type(): 113 | "anything_of_type() should return ``True`` for the :func:`operator.eq`" 114 | 115 | expects(anything_of_type(str) == "anything_of_type(str)").to.not_be.false 116 | expects(anything_of_type(str)).to.equal("anything_of_type(str)") 117 | 118 | expects(anything_of_type(str) == b"anything_of_type(str)").to.be.false 119 | expects(anything_of_type(str)).to_not.equal(b"anything_of_type(str)") 120 | 121 | 122 | def test_anything_of_type_should_raise_type_error_when_receiving_a_nontype_type_instance(): 123 | 'anything_of_type() should raise :exc:`TypeError` when receiving a non-type type "instance"' 124 | 125 | expects(anything_of_type).when.called_with("anything_of_type(str)").to.have.raised( 126 | TypeError, 127 | "'anything_of_type(str)' should be a class but is a instead", 128 | ) 129 | -------------------------------------------------------------------------------- /examples/unit-tests/behavior_definition_simplify_mock.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | # -*- coding: utf-8 -*- 3 | 4 | # =========================================== 5 | # Behavior definition examples in pseudo code 6 | # =========================================== 7 | 8 | 9 | #################################################################################### 10 | 11 | # UNIT TESTS 12 | # --------- 13 | 14 | # Goals: 15 | # ~~~~~ 16 | # 17 | # * Provide an API that leverages mocking ``sure.scenario.MockfulBehaviorDefinition`` 18 | # * Provide a didactic and fun way to keep the mocks organized 19 | # * Make monkey-patching of modules more fun: 20 | # * Always forward the mocked objects as keyword-arguments as 21 | # opposed to the default behavior of ``@patch()`` decorator stack 22 | # which uses positional arguments, polute test declarations and 23 | # demands menial, extrenuous work in case of test refactoring. 24 | # * ``self.mock.module(members={})`` is a shorthand for creating a dummy module object that contains the given members as properties 25 | 26 | # Notes: 27 | # ~~~~~ 28 | # 29 | # * the following methods require exactly one positional argument: ``mock_name``, otherwise: ``raise AssertionError("self.mock.simple() and self.mock.special() require one positional argument: its name.")`` 30 | # * ``self.mock.simple()`` 31 | # * ``self.mock.special()`` 32 | # * self.mock.install() forwards keyword-arguments to ``mock.patch``, for example: 33 | # * ``self.mock.install('myapp.api.http.db.engine.connect', return_value='DUMMY_CONNECTION') 34 | # * ``self.mock.install('myapp.api.http.db.engine.connect', return_value='DUMMY_CONNECTION') 35 | # * self.mock.install() always generates MagicMocks 36 | # * self.mock.install() accepts the parameter ``argname='some_identifier`` to be passed the keyword-argument that contains the ``Mock`` instance returned by ``.install()`` 37 | # * When ``argname=None`` the mock instance will be passed with a keyword-arg whose value is derived from the last __name__ found in the patch target, that is: ``self.mock.install('myapp.core.BaseServer')`` will change the test signature to: ``def test_something(context, BaseServer)``. 38 | # * self.mock.install() will automatically name the mock when the ``name=""`` keyword-arg is not provided 39 | # * self.mock.uninstall() accepts one positional argument: identified and then automatically match it against: 40 | # * self.scenario.forward_argument() allows for arbitrary parameters in the test function, that is: ``self.scenario.forward_argument(connection=self.connection)`` will change the test signature to: ``def test_something(context, connection)``. 41 | # * Developers do *NOT* need to manually uninstall mocks, but that is 42 | # still permitted to encompass the cases where it has to be 43 | # accomplished in mid-test, for example: 44 | # 45 | # @apply_behavior(FakeSettings) 46 | # @apply_behavior(PreventRedisIO) 47 | # def test_something(context): 48 | # # perform some action that requires a stubbed setting 49 | # context.redis.mock.uninstall(name=' 50 | # # peform 51 | # 52 | 53 | # 54 | from mock import Mock 55 | from myapp import settings 56 | from myapp.api.http import APIServer 57 | from sure.scenario import MockfulBehaviorDefinition, apply_behavior, require_behavior 58 | 59 | 60 | class FakeSettings(MockfulBehaviorDefinition): 61 | """automatically patches myapp.settings and overriding its keys with 62 | the provided keyword-args.""" 63 | 64 | context_namespace = 'settings' 65 | 66 | def setup(self, **kw): 67 | # 1. make 2 copies: 68 | # * one original copy for backup 69 | # * one containing the overrides 70 | self.original_state = settings.to_dict() 71 | cloned_state = settings.to_dict() 72 | 73 | # 2. Apply the overrides in the cloned state 74 | cloned_state.update(kw) 75 | 76 | # 3. Create a module mock containing the members 77 | fake_settings = self.mock.module(members=fake_state) 78 | 79 | # 4. Install the mocked settings 80 | self.mock.install('myapp.api.http.settings', fake_settings) 81 | 82 | 83 | class PreventRedisIO(MockfulBehaviorDefinition): 84 | context_namespace = 'redis' 85 | 86 | def setup(self): 87 | self.connection = self.mock.simple('redis-connection-instance') 88 | self.mock.install('myapp.api.http.RedisConnection', argname='connection', return_value=self.connection) 89 | 90 | class PreventSQLIO(MockfulBehaviorDefinition): 91 | context_namespace = 'sql' 92 | 93 | def setup(self): 94 | self.engine = self.mock.simple('sqlalchemy.engine') 95 | self.connection = self.engine.return_value 96 | self.mock.install('myapp.api.http.sqlengine', return_value=self.engine) 97 | self.scenario.forward_argument(connection=self.connection) 98 | 99 | 100 | class IOSafeAPIServer(BehaviorGroup): 101 | layers = [ 102 | FakeSettings(SESSION_SECRET='dummy'), 103 | PreventRedisIO(), 104 | PreventSQLIO() 105 | ] 106 | 107 | 108 | @apply_behavior(IOSafeAPIServer) 109 | def test_api_get_user(context, connection, ): 110 | ('APIServer.handle_get_user() should try to retrieve from the redis cache first') 111 | 112 | # Given a server 113 | api = APIServer() 114 | 115 | # When I call .handle_get_user() 116 | response = api.handle_get_user(user_uuid='b1i6c00010ff1ceb00dab00b') 117 | 118 | # Then it should have returned a response 119 | response.should.be.a('flask.Response') 120 | -------------------------------------------------------------------------------- /sure/runner.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) <2010-2024> Gabriel Falcão 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | import os 18 | import re 19 | import sys 20 | import types 21 | import inspect 22 | import unittest 23 | import traceback 24 | 25 | from pathlib import Path 26 | from typing import ( 27 | List, 28 | Optional, 29 | Tuple, 30 | TypeVar, 31 | Union, 32 | Iterable, 33 | ) 34 | from functools import lru_cache, cached_property 35 | 36 | from sure.errors import ExitError, ExitFailure, ImmediateError, ImmediateFailure 37 | from sure.runtime import ( 38 | Feature, 39 | Scenario, 40 | BaseResult, 41 | ErrorStack, 42 | RuntimeContext, 43 | FeatureResult, 44 | RuntimeOptions, 45 | ScenarioResult, 46 | FeatureResultSet, 47 | ScenarioResultSet, 48 | stripped, 49 | seem_to_indicate_test, 50 | ) 51 | from sure.loader import ( 52 | loader, 53 | object_belongs_to_sure, 54 | ) 55 | from sure.reporter import Reporter 56 | 57 | 58 | Candidate = TypeVar("Candidate") 59 | 60 | 61 | class Runner(object): 62 | """Manages I/O operations in regards to finding tests and executing them""" 63 | 64 | def __init__(self, base_path: Path, reporter: str, options: RuntimeOptions, **kwds): 65 | self.base_path = base_path 66 | self.reporter = self.get_reporter(reporter) 67 | self.options = options 68 | self.kwds = kwds 69 | 70 | def __repr__(self): 71 | return f"" 72 | 73 | def get_reporter(self, name) -> Reporter: 74 | return Reporter.from_name_and_runner(name, self) 75 | 76 | def find_candidates( 77 | self, lookup_paths: Iterable[Union[str, Path]] 78 | ) -> List[types.ModuleType]: 79 | candidate_modules = [] 80 | for path in lookup_paths: 81 | modules = loader.load_recursive( 82 | path, 83 | glob_pattern=self.options.glob_pattern, 84 | excludes=self.options.ignore, 85 | ) 86 | candidate_modules.extend(modules) 87 | 88 | return candidate_modules 89 | 90 | def is_runnable_test(self, item) -> bool: 91 | if object_belongs_to_sure(item): 92 | return False 93 | 94 | name = getattr(item, "__name__", None) 95 | if isinstance(item, type): 96 | if not issubclass(item, unittest.TestCase): 97 | return seem_to_indicate_test(name) 98 | elif item == unittest.TestCase: 99 | return False 100 | else: 101 | return True 102 | 103 | elif not isinstance(item, types.FunctionType): 104 | return False 105 | 106 | return seem_to_indicate_test(name) 107 | 108 | def extract_members( 109 | self, candidate: Candidate 110 | ) -> Tuple[ 111 | Candidate, 112 | Iterable[Union[types.MethodType, types.FunctionType, unittest.TestCase, type]], 113 | ]: 114 | all_members = [m[1] for m in inspect.getmembers(candidate)] 115 | members = list(filter(self.is_runnable_test, all_members)) 116 | return candidate, members 117 | 118 | def load_features(self, lookup_paths: List[Union[Path, str]]) -> List[Feature]: 119 | features = [] 120 | candidates = self.find_candidates(lookup_paths) 121 | for module, executables in map(self.extract_members, candidates): 122 | feature = Feature(module) 123 | feature.read_scenarios(executables) 124 | features.append(feature) 125 | 126 | return features 127 | 128 | def execute(self, lookup_paths=Iterable[Union[Path, str]]) -> FeatureResultSet: 129 | results = [] 130 | self.reporter.on_start() 131 | lookup_paths = list(lookup_paths) 132 | 133 | for feature in self.load_features(lookup_paths): 134 | self.reporter.on_feature(feature) 135 | 136 | result = feature.run(self.reporter, runtime=self.options) 137 | if self.options.immediate: 138 | if result.is_failure: 139 | raise ExitFailure(self.context, result) 140 | 141 | if result.is_error: 142 | raise ExitError(self.context, result) 143 | 144 | results.append(result) 145 | 146 | self.reporter.on_feature_done(feature, result) 147 | 148 | self.reporter.on_finish(self.context) 149 | return FeatureResultSet(results) 150 | 151 | def run(self, *args, **kws): 152 | try: 153 | return self.execute(*args, **kws) 154 | except ImmediateFailure as failure: 155 | self.reporter.on_failure(failure.scenario, failure.result) 156 | return failure.result 157 | 158 | except ImmediateError as error: 159 | self.reporter.on_error(error.scenario, error.result) 160 | return error.result 161 | 162 | @cached_property 163 | def context(self): 164 | return RuntimeContext(self.reporter, self.options) 165 | -------------------------------------------------------------------------------- /docs/source/changelog.rst: -------------------------------------------------------------------------------- 1 | Change Log 2 | ========== 3 | 4 | All notable changes to this project will be documented in this file. 5 | This project adheres to `Semantic Versioning `__. 6 | 7 | v3.0.0 8 | ------ 9 | 10 | - Pervasive test-coverage 11 | - Presents better documentation, refactoring and bugfixes 12 | - Drops support to Python 2 obliterates the ``sure.compat`` module 13 | - Introduces the modules: 14 | - :mod:`sure.doubles` 15 | - :mod:`sure.doubles.fakes` 16 | - :mod:`sure.doubles.stubs` 17 | - :mod:`sure.doubles.dummies` 18 | - Introduces the classes: 19 | - :class:`sure.doubles.dummies.Anything` (moved from ``sure.Anything``) 20 | - :class:`sure.doubles.dummies.AnythingOfType` 21 | - Sure’s featured synctactic-sugar of injecting/monkey-patching 22 | ``.should``, ``.should_not``, et cetera methods into 23 | :class:``object`` and its subclasses is disabled by default and 24 | needs to be enabled explicitly, programmatically via 25 | ``sure.enable_special_syntax()`` or via command-line with the flags: 26 | ``-s`` or ``--special-syntax`` 27 | - Moves :class:``sure.original.that`` to :attr:``sure.that`` as 28 | an instance of :class:``sure.original.AssertionHelper`` rather 29 | than an alias to the class. 30 | - ``AssertionHelper.every_one_is()`` renamed to ``AssertionHelper.every_item_is()`` 31 | - Renames :class:`sure.AssertionBuilder` constructor parameters: 32 | - ``with_kwargs`` to ``with_kws`` 33 | - ``and_kwargs`` to ``and_kws`` 34 | - Functions or methods decorated with the :func:`sure.within` 35 | decorator no longer receive a :class:`datetime.datetime` object as 36 | first argument. 37 | 38 | - Removes methods from :class:`sure.original.AssertionHelper`: 39 | - :meth:`sure.original.AssertionHelper.differs` 40 | - :meth:`sure.original.AssertionHelper.has` 41 | - :meth:`sure.original.AssertionHelper.is_a` 42 | - :meth:`sure.original.AssertionHelper.every_item_is` 43 | - :meth:`sure.original.AssertionHelper.at` 44 | - :meth:`sure.original.AssertionHelper.like` 45 | 46 | - No longer (officially) supports python versions lower than 3.11 47 | 48 | [v2.0.0] 49 | -------- 50 | 51 | Fixed 52 | ~~~~~ 53 | 54 | - No longer patch the builtin ``dir()`` function, which fixes pytest in 55 | some cases such as projects using gevent. 56 | 57 | [v1.4.11] 58 | --------- 59 | 60 | .. _fixed-1: 61 | 62 | Fixed 63 | ~~~~~ 64 | 65 | - Reading the version dynamically was causing import errors that caused 66 | error when installing package. Refs #144 67 | 68 | `v1.4.7 `__ 69 | ------------------------------------------------------------------------- 70 | 71 | .. _fixed-2: 72 | 73 | Fixed 74 | ~~~~~ 75 | 76 | - Remove wrong parens for format call. Refs #139 77 | 78 | `v1.4.6 `__ 79 | ------------------------------------------------------------------------- 80 | 81 | Added 82 | ~~~~~ 83 | 84 | - Support and test against PyPy 3 85 | 86 | .. _fixed-3: 87 | 88 | Fixed 89 | ~~~~~ 90 | 91 | - Fix safe representation in exception messages for bytes and unicode 92 | objects. Refs #136 93 | 94 | `v1.4.5 `__ 95 | ------------------------------------------------------------------------- 96 | 97 | .. _fixed-4: 98 | 99 | Fixed 100 | ~~~~~ 101 | 102 | - Correctly escape special character for ``str.format()`` for assertion 103 | messages. Refs #134 104 | 105 | `v1.4.4 `__ 106 | ------------------------------------------------------------------------- 107 | 108 | *Nothing to mention here.* 109 | 110 | `v1.4.3 `__ 111 | ------------------------------------------------------------------------- 112 | 113 | .. _fixed-5: 114 | 115 | Fixed 116 | ~~~~~ 117 | 118 | - Bug in setup.py that would break in python > 2 119 | 120 | `v1.4.2 `__ 121 | ------------------------------------------------------------------------- 122 | 123 | .. _added-1: 124 | 125 | Added 126 | ~~~~~ 127 | 128 | - ``ensure`` context manager to provide custom assertion messages. Refs 129 | #125 130 | 131 | `v1.4.1 `__ 132 | ------------------------------------------------------------------------- 133 | 134 | .. _added-2: 135 | 136 | Added 137 | ~~~~~ 138 | 139 | - Python 3.6 support 140 | - Python 3.7-dev support (allowed to fail) 141 | 142 | .. _fixed-6: 143 | 144 | Fixed 145 | ~~~~~ 146 | 147 | - Do not overwrite existing class and instance attributes with sure 148 | properties (when. should, …). Refs #127, #129 149 | - Fix patched built-in ``dir()`` method. Refs #124, #128 150 | 151 | `v1.4.0 `__ 152 | ------------------------------------------------------------------------- 153 | 154 | .. _added-3: 155 | 156 | Added 157 | ~~~~~ 158 | 159 | - anything object which is accessible with ``sure.anything`` 160 | - interface to extend sure. Refs #31 161 | 162 | Removed 163 | ~~~~~~~ 164 | 165 | - Last traces of Python 2.6 support 166 | 167 | .. _fixed-7: 168 | 169 | Fixed 170 | ~~~~~ 171 | 172 | - Allow overwriting of monkey-patched properties by sure. Refs #19 173 | - Assertions for raises 174 | 175 | `v1.3.0 `__ 176 | ------------------------------------------------------------------------- 177 | 178 | .. _added-4: 179 | 180 | Added 181 | ~~~~~ 182 | 183 | - Python 3.3, 3.4 and 3.5 support 184 | - pypy support 185 | - Support comparison of OrderedDict. Refs #55 186 | 187 | .. _fixed-8: 188 | 189 | Fixed 190 | ~~~~~ 191 | 192 | - ``contain`` assertion. Refs #104 193 | -------------------------------------------------------------------------------- /tests/test_runtime/test_scenario.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) <2010-2024> Gabriel Falcão 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | """tests for :class:`sure.runtime.Scenario`""" 19 | 20 | 21 | from unittest.mock import patch, call 22 | from unittest.mock import Mock as Spy 23 | from sure import expects 24 | from sure.runtime import ( 25 | Scenario, 26 | RuntimeOptions, 27 | ScenarioResult, 28 | RuntimeRole, 29 | RuntimeContext, 30 | ScenarioResultSet, 31 | ) 32 | from sure.doubles import stub, Dummy, anything 33 | from sure.errors import ExitFailure, ExitError 34 | 35 | 36 | description = "tests for :class:`sure.runtime.Scenario`" 37 | 38 | 39 | @patch("sure.errors.sys.exit") 40 | @patch("sure.runtime.ScenarioArrangement") 41 | def test_scenario_run_when_result_is_failure(ScenarioArrangement, exit): 42 | "Scenario.run() should raise :class:`sure.errors.ExitError` when a failure occurs" 43 | 44 | scenario_stub = stub( 45 | Scenario, 46 | object=Dummy("scenario.object"), 47 | ) 48 | reporter_spy = Spy(name="Reporter") 49 | scenario_arrangement = ScenarioArrangement.return_value 50 | scenario_arrangement.scenario = scenario_stub 51 | ScenarioArrangement.from_generic_object.return_value = scenario_arrangement 52 | scenario_arrangement.uncollapse_nested.return_value = [scenario_arrangement] 53 | scenario_result = stub( 54 | ScenarioResult, 55 | is_failure=True, 56 | is_success=False, 57 | is_error=False, 58 | scenario=scenario_stub, 59 | ) 60 | scenario_arrangement.run.return_value = [(scenario_result, RuntimeRole.Unit)] 61 | 62 | context = stub( 63 | RuntimeContext, options=RuntimeOptions(immediate=False), reporter=reporter_spy 64 | ) 65 | scenario_result_set = scenario_stub.run(context) 66 | 67 | expects(scenario_result_set).to.be.a(ScenarioResultSet) 68 | expects(reporter_spy.mock_calls).to.equal( 69 | [ 70 | call.on_scenario(scenario_arrangement.scenario), 71 | call.on_failure(scenario_stub, scenario_result), 72 | call.on_scenario_done(scenario_arrangement.scenario, scenario_result), 73 | ] 74 | ) 75 | 76 | 77 | @patch("sure.errors.sys.exit") 78 | @patch("sure.runtime.ScenarioArrangement") 79 | def test_scenario_run_when_result_is_failure_and_runtime_options_immediate( 80 | ScenarioArrangement, exit 81 | ): 82 | 'Scenario.run() should raise :class:`sure.errors.ExitError` when a failure occurs and the runtime context is configured to "fail immediately"' 83 | 84 | reporter_spy = Spy(name="Reporter") 85 | scenario_arrangement = ScenarioArrangement.return_value 86 | ScenarioArrangement.from_generic_object.return_value = scenario_arrangement 87 | scenario_arrangement.uncollapse_nested.return_value = [scenario_arrangement] 88 | scenario_stub = stub( 89 | Scenario, 90 | object=Dummy("scenario.object"), 91 | ) 92 | scenario_result = stub( 93 | ScenarioResult, 94 | is_failure=True, 95 | is_success=False, 96 | is_error=False, 97 | scenario=scenario_stub, 98 | ) 99 | scenario_arrangement.run.return_value = [(scenario_result, RuntimeRole.Unit)] 100 | 101 | context = stub( 102 | RuntimeContext, options=RuntimeOptions(immediate=True), reporter=reporter_spy 103 | ) 104 | expects(scenario_stub.run).when.called_with(context).should.have.raised(ExitFailure) 105 | 106 | expects(reporter_spy.mock_calls).to.equal( 107 | [ 108 | call.on_scenario(scenario_arrangement.scenario), 109 | call.on_failure(scenario_stub, scenario_result), 110 | ] 111 | ) 112 | 113 | 114 | @patch("sure.errors.sys.exit") 115 | @patch("sure.runtime.ScenarioArrangement") 116 | def test_scenario_run_when_result_is_error_and_runtime_options_immediate( 117 | ScenarioArrangement, exit 118 | ): 119 | 'Scenario.run() should raise :class:`sure.errors.ExitError` when an error occurs and the runtime context is configured to "error immediately"' 120 | 121 | reporter_spy = Spy(name="Reporter") 122 | scenario_arrangement = ScenarioArrangement.return_value 123 | ScenarioArrangement.from_generic_object.return_value = scenario_arrangement 124 | scenario_arrangement.uncollapse_nested.return_value = [scenario_arrangement] 125 | scenario_stub = stub( 126 | Scenario, 127 | object=Dummy("scenario.object"), 128 | ) 129 | scenario_result = stub( 130 | ScenarioResult, 131 | is_error=True, 132 | is_success=False, 133 | is_failure=False, 134 | scenario=scenario_stub, 135 | ) 136 | scenario_arrangement.run.return_value = [(scenario_result, RuntimeRole.Unit)] 137 | 138 | context = stub( 139 | RuntimeContext, options=RuntimeOptions(immediate=True), reporter=reporter_spy 140 | ) 141 | expects(scenario_stub.run).when.called_with(context).should.have.raised(ExitError) 142 | 143 | expects(reporter_spy.mock_calls).to.equal( 144 | [ 145 | call.on_scenario(scenario_arrangement.scenario), 146 | call.on_error(scenario_stub, scenario_result), 147 | ] 148 | ) 149 | -------------------------------------------------------------------------------- /tests/unit/test_reporter.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) <2010-2024> Gabriel Falcão 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | from sure import expects 18 | from sure.reporter import Reporter 19 | from sure.reporters import FeatureReporter 20 | from sure.runner import Runner 21 | from sure.doubles import stub, anything 22 | from unittest.mock import patch 23 | 24 | description = "tests for :class:`sure.reporter`" 25 | 26 | 27 | def test_reporter_from_name(): 28 | 'sure.reporter.Reporter.from_name should return presently existing "reporters"' 29 | expects(Reporter.from_name('feature')).to.be(FeatureReporter) 30 | expects(Reporter.from_name).when.called_with('dummy').to.throw( 31 | RuntimeError, 32 | "no reporter found with name `dummy', options are: feature" 33 | ) 34 | 35 | 36 | def test_name_and_runner(): 37 | 'sure.reporter.Reporter.from_name_and_runner should return an instance of the given reporter type' 38 | 39 | runner_stub = stub(Runner) 40 | reporter = Reporter.from_name_and_runner('feature', runner_stub) 41 | expects(reporter).to.be.a(FeatureReporter) 42 | 43 | 44 | def test_reporter_from_name_nonstring(): 45 | 'sure.reporter.Reporter.from_name raises TypeError when not receiving a string as argument' 46 | expects(Reporter.from_name).when.called_with(['not', 'string']).to.throw( 47 | TypeError, 48 | "name should be a str but got the list ['not', 'string'] instead" 49 | ) 50 | 51 | 52 | def test_reporter_instance_methods(): 53 | "sure.reporter.Reporter() instance methods" 54 | runner_stub = stub(Runner) 55 | 56 | with patch('sure.reporter.Reporter.initialize') as initialize: 57 | reporter = Reporter(runner_stub, 'foo', bar='bar') 58 | initialize.assert_called_with('foo', bar='bar') 59 | 60 | reporter = Reporter(runner_stub) 61 | expects(repr(reporter)).to.equal('') 62 | 63 | expects(reporter.on_start).when.called.to.have.raised( 64 | NotImplementedError 65 | ) 66 | 67 | expects(reporter.on_feature).when.called_with(feature=anything).to.have.raised( 68 | NotImplementedError 69 | ) 70 | 71 | expects(reporter.on_feature_done).when.called_with(feature=anything, result=anything).to.have.raised( 72 | NotImplementedError 73 | ) 74 | 75 | expects(reporter.on_scenario).when.called_with(scenario=anything).to.have.raised( 76 | NotImplementedError 77 | ) 78 | 79 | expects(reporter.on_scenario_done).when.called_with(scenario=anything, result=anything).to.have.raised( 80 | NotImplementedError 81 | ) 82 | 83 | expects(reporter.on_failure).when.called_with(scenario_result=anything, error=anything).to.have.raised( 84 | NotImplementedError 85 | ) 86 | 87 | expects(reporter.on_success).when.called_with(scenario=anything).to.have.raised( 88 | NotImplementedError 89 | ) 90 | 91 | expects(reporter.on_internal_runtime_error).when.called_with(context=anything, exception=anything).to.have.raised( 92 | NotImplementedError 93 | ) 94 | 95 | expects(reporter.on_error).when.called_with(scenario_result=anything, error=anything).to.have.raised( 96 | NotImplementedError 97 | ) 98 | 99 | expects(reporter.on_finish).when.called_with(anything).to.have.raised( 100 | NotImplementedError 101 | ) 102 | 103 | 104 | @patch('sure.reporter.Reporter.initialize') 105 | def test_from_name_and_runner(initialize): 106 | "sure.reporter.Reporter() instance methods" 107 | runner_stub = stub(Runner) 108 | 109 | reporter = Reporter(runner_stub, 'foo', bar='bar') 110 | 111 | expects(repr(reporter)).to.equal('') 112 | initialize.assert_called_with('foo', bar='bar') 113 | 114 | expects(reporter.on_start).when.called.to.have.raised( 115 | NotImplementedError 116 | ) 117 | 118 | expects(reporter.on_feature).when.called_with(feature=anything).to.have.raised( 119 | NotImplementedError 120 | ) 121 | 122 | expects(reporter.on_feature_done).when.called_with(feature=anything, result=anything).to.have.raised( 123 | NotImplementedError 124 | ) 125 | 126 | expects(reporter.on_scenario).when.called_with(scenario=anything).to.have.raised( 127 | NotImplementedError 128 | ) 129 | 130 | expects(reporter.on_scenario_done).when.called_with(scenario=anything, result=anything).to.have.raised( 131 | NotImplementedError 132 | ) 133 | 134 | expects(reporter.on_failure).when.called_with(scenario_result=anything, error=anything).to.have.raised( 135 | NotImplementedError 136 | ) 137 | 138 | expects(reporter.on_success).when.called_with(scenario=anything).to.have.raised( 139 | NotImplementedError 140 | ) 141 | 142 | expects(reporter.on_internal_runtime_error).when.called_with(context=anything, exception=anything).to.have.raised( 143 | NotImplementedError 144 | ) 145 | 146 | expects(reporter.on_error).when.called_with(scenario_result=anything, error=anything).to.have.raised( 147 | NotImplementedError 148 | ) 149 | 150 | expects(reporter.on_finish).when.called_with(anything).to.have.raised( 151 | NotImplementedError 152 | ) 153 | -------------------------------------------------------------------------------- /sure/cli.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright (C) <2010-2024> Gabriel Falcão 5 | # 6 | # This program is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program. If not, see . 18 | import os # pragma: no cover 19 | import sys # pragma: no cover 20 | from glob import glob # pragma: no cover 21 | from itertools import chain as flatten # pragma: no cover 22 | from functools import reduce # pragma: no cover 23 | from pathlib import Path # pragma: no cover 24 | 25 | import click # pragma: no cover 26 | import coverage # pragma: no cover 27 | import threading # pragma: no cover 28 | import sure.reporters # pragma: no cover 29 | 30 | from sure.loader import resolve_path # pragma: no cover 31 | from sure.runner import Runner # pragma: no cover 32 | from sure.runtime import RuntimeOptions # pragma: no cover 33 | from sure.reporters import gather_reporter_names # pragma: no cover 34 | from sure.errors import ExitError, ExitFailure, InternalRuntimeError, treat_error # pragma: no cover 35 | 36 | 37 | @click.command(no_args_is_help=True) # pragma: no cover 38 | @click.argument("paths", nargs=-1) 39 | @click.option("-c", "--with-coverage", is_flag=True) 40 | @click.option("-s", "--special-syntax", is_flag=True) 41 | @click.option("-f", "--log-file", help="path to a log file. Default to SURE_LOG_FILE") 42 | @click.option( 43 | "-l", 44 | "--log-level", 45 | type=click.Choice(["debug", "info", "warning", "error"]), 46 | help="default='info'", 47 | ) 48 | @click.option( 49 | "-x", 50 | "--immediate", 51 | is_flag=True, 52 | help="quit test execution immediately at first failure", 53 | ) 54 | @click.option("-i", "--ignore", help="paths to ignore", multiple=True) 55 | @click.option( 56 | "-r", 57 | "--reporter", 58 | default="feature", 59 | help="default=feature", 60 | type=click.Choice(gather_reporter_names()), 61 | ) 62 | @click.option("--cover-branches", is_flag=True) 63 | @click.option("--cover-include", multiple=True, help="includes paths or patterns in the coverage") 64 | @click.option("--cover-omit", multiple=True, help="omits paths or patterns from the coverage") 65 | @click.option("--cover-module", multiple=True, help="specify module names to cover") 66 | @click.option("--cover-erase", is_flag=True, help="erases coverage data prior to running tests") 67 | @click.option("--cover-concurrency", help="indicates the concurrency library used in measured code", type=click.Choice(["greenlet", "eventlet", "gevent", "multiprocessing", "thread"]), default="thread") 68 | @click.option("--reap-warnings", is_flag=True, help="reaps warnings during runtime and report only at the end of test session") 69 | def entrypoint( 70 | paths, 71 | reporter, 72 | immediate, 73 | ignore, 74 | log_level, 75 | log_file, 76 | special_syntax, 77 | with_coverage, 78 | cover_branches, 79 | cover_include, 80 | cover_omit, 81 | cover_module, 82 | cover_erase, 83 | cover_concurrency, 84 | reap_warnings, 85 | ): 86 | if not paths: 87 | paths = glob("test*/**") 88 | else: 89 | paths = flatten(*list(map(glob, paths))) 90 | 91 | coverageopts = { 92 | "auto_data": not False, 93 | "branch": cover_branches, 94 | "include": cover_include, 95 | "concurrency": cover_concurrency, 96 | "omit": cover_omit, 97 | "config_file": not False, 98 | "cover_pylib": not False, 99 | "source": cover_module, 100 | } 101 | 102 | options = RuntimeOptions(immediate=immediate, ignore=ignore, reap_warnings=reap_warnings) 103 | runner = Runner(resolve_path(os.getcwd()), reporter, options) 104 | 105 | cov = with_coverage and coverage.Coverage(**coverageopts) or None 106 | if cov: 107 | cover_erase and cov.erase() 108 | cov.load() 109 | cov.start() 110 | 111 | if special_syntax: 112 | sure.enable_special_syntax() 113 | 114 | try: 115 | result = runner.run(paths) 116 | except Exception as e: 117 | raise InternalRuntimeError(runner.context, treat_error(e)) 118 | 119 | if result: 120 | if result.is_failure: 121 | raise ExitFailure(runner.context, result) 122 | 123 | elif result.is_error: 124 | raise ExitError(runner.context, result) 125 | 126 | elif cov: 127 | sys.stdout = sys.__stdout__ 128 | sys.stderr = sys.__stderr__ 129 | cov.stop() 130 | cov.save() 131 | cov.report() 132 | -------------------------------------------------------------------------------- /tests/test_assertion_builder_assertion_methods.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) <2010-2024> Gabriel Falcão 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | """tests for :class:`sure.AssertionBuilder` properties defined with the 18 | decorator :func:`sure.assertionmethod`""" 19 | import re 20 | import sys 21 | from sure import expects 22 | from sure.doubles import anything_of_type 23 | 24 | 25 | def test_contains_and_to_contain(): 26 | "expects.that().contains and expects().to_contain" 27 | 28 | expects.that(set(range(8))).contains(7) 29 | expects(set(range(13))).to.contain(8) 30 | expects(set(range(33))).to_contain(3) 31 | expects(set()).to_contain.when.called_with("art").should.have.raised("`art' should be in `set()'") 32 | 33 | 34 | def test_does_not_contain_and_to_not_contain(): 35 | "expects().contains and expects().to_not_contain" 36 | 37 | expects(list()).to_not_contain("speculations") 38 | expects(set(range(33))).to_not_contain.when.called_with(3).should.have.raised("`3' should not be in `{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32}'") 39 | 40 | 41 | def test_within(): 42 | "expects().to.be.within()" 43 | 44 | expects(3).to.be.within(set(range(33))) 45 | expects(7).to.be.within.when.called_with(0, 0, 6).should.have.raised( 46 | "(7).should.be.within(0, 0, 6) must be called with either an iterable:\n" 47 | "(7).should.be.within([1, 2, 3, 4])\n" 48 | "or with a range of numbers, i.e.: `(7).should.be.within(1, 3000)'" 49 | ) 50 | expects(3).to.be.within(0, 7) 51 | expects(1).to_not.be.within.when.called_with(0, 0, 7).should.have.raised( 52 | "(1).should_not.be.within(0, 0, 7) must be called with either an iterable:\n" 53 | ) 54 | expects(3).to.not_be.within.when.called_with(set(range(33))).should.have.raised( 55 | "`3' should not be in `{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32}'" 56 | ) 57 | expects("art").to.be.within.when.called_with({"set"}).should.have.raised( 58 | "`art' should be in `{'set'}'" 59 | ) 60 | 61 | 62 | def test_different_of(): 63 | "expects().to.be.different_of()" 64 | 65 | expects("").to.be.different_of.when.called_with([]).should.have.raised( 66 | ".different_of only works for string comparison but in this case is expecting [] () instead" 67 | ) 68 | 69 | expects([]).to.be.different_of.when.called_with("").should.have.raised( 70 | ".different_of only works for string comparison but in this case the actual source comparison object is [] () instead" 71 | ) 72 | 73 | 74 | def test_is_a(): 75 | "expects().to.be.a()" 76 | 77 | expects(b"a").to.be.a(bytes) 78 | expects(sys.stdout).to.be.a.when.called_with("io.StringIO").to.have.raised( 79 | "expects(sys.stdout).to.be.a.when.called_with(\"io.StringIO\").to.have.raised( expects `<_io.TextIOWrapper name='' mode='w' encoding='utf-8'>' to be an `io.StringIO'" 80 | ) 81 | expects(sys.stdout).to.not_be.a.when.called_with("io.TextIOWrapper").to.have.raised( 82 | "expects(sys.stdout).to.not_be.a.when.called_with(\"io.TextIOWrapper\").to.have.raised( expects `<_io.TextIOWrapper name='' mode='w' encoding='utf-8'>' to not be an `io.TextIOWrapper'" 83 | ) 84 | 85 | 86 | def test_to_be_below(): 87 | "expects().to.be.below()" 88 | 89 | expects(b"A").to.be.below(b"a") 90 | expects(b"a").to.below.when.called_with(b"A").should.have.raised( 91 | "b'a' should be below b'A'" 92 | ) 93 | expects(70).to_not.be.below.when.called_with(83).should.have.raised( 94 | "70 should not be below 83" 95 | ) 96 | expects(b"a").to.below.when.called_with(b"A").should_not.have.raised.when.called_with( 97 | "b'a' should be below b'A'" 98 | ).should.have.thrown.when.called_with("`below' called with args (b'A',) and keyword-args {} should not raise b'a' should be below b'A' but raised b'a' should be below b'A'").should.return_value(not False) 99 | 100 | 101 | def test_to_be_above(): 102 | "expects().to.be.above()" 103 | 104 | expects(b"S").to.be.above(b"B") 105 | expects(b"D").to.be.above.when.called_with(b"S").should.have.raised( 106 | "b'D' should be above b'S'" 107 | ) 108 | expects(115).to.not_be.above.when.called_with(102).to.have.raised( 109 | "115 should not be above 102" 110 | ) 111 | 112 | 113 | def test_to_match(): 114 | "expects().to.match() REGEX" 115 | 116 | expects("ROBSON").to.match(r"(^RO|.OB.{3})") 117 | expects("robson").to.match(r"(^RO|.OB.{3})", re.I) 118 | expects("OM").to.match(r"S?[OU][NM]") 119 | expects("ON").to.match(r"S?[OU][NM]") 120 | expects("SOM").to.match(r"S?[OU][NM]") 121 | expects("SON").to.match(r"S?[OU][NM]") 122 | expects("SUM").to.match(r"S?[OU][MN]") 123 | expects("SUN").to.match(r"S?[OU][MN]") 124 | expects("UM").to.match(r"S?[OU][MN]") 125 | expects("UN").to.match(r"S?[OU][MN]") 126 | expects("NOS").to_not.match(r"S?[OU][MN]") 127 | expects(list("OHMS")).to.match.when.called_with("Ω").should.have.raised( 128 | "['O', 'H', 'M', 'S'] should be a string in order to compare using .match()" 129 | ) 130 | --------------------------------------------------------------------------------