pax_global_header00006660000000000000000000000064142071524550014517gustar00rootroot0000000000000052 comment=263aa23a1233eb74ff05fafe2870bb0b1d9bc1da test_server-0.0.40/000077500000000000000000000000001420715245500141455ustar00rootroot00000000000000test_server-0.0.40/.bumpversion.cfg000066400000000000000000000001651420715245500172570ustar00rootroot00000000000000[bumpversion] current_version = 0.0.40 files = setup.py test_server/version.py docs/conf.py commit = True tag = True test_server-0.0.40/.flake8000066400000000000000000000006461420715245500153260ustar00rootroot00000000000000[flake8] # E261 at least two spaces before inline comment # E265 block comment should start with '# ' # E121 continuation line under-indented for hanging indent # E125 continuation line with same indent as next logical line # F401 'pprint.pprint' imported but unused, CHECKED BY PYLINT # F841 local variable 'suffix' is assigned to but never used, CHECKED BY PYLINT ignore=E261,E265,E121,E125,F401,F841 max-line-length=88 test_server-0.0.40/.github/000077500000000000000000000000001420715245500155055ustar00rootroot00000000000000test_server-0.0.40/.github/workflows/000077500000000000000000000000001420715245500175425ustar00rootroot00000000000000test_server-0.0.40/.github/workflows/test.yml000066400000000000000000000013261420715245500212460ustar00rootroot00000000000000name: test on: [push] jobs: build: runs-on: ubuntu-latest strategy: matrix: python-version: ['3.7', '3.8', '3.9', '3.10'] steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install -U pip pip install -U -r requirements_dev.txt pip install -U -e . - name: Run tests run: | coverage run -m pytest #- name: Upload coverage results # env: # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # run: | # coveralls --service=github test_server-0.0.40/.gitignore000066400000000000000000000003241420715245500161340ustar00rootroot00000000000000# Common *.pyc *.pyo *.swp *.swo *.orig *.egg-info # Project specific /.env /pip-log.txt /var/ /.tox/ /.coverage /dist/ /coverage.xml /.cache/ /build/ /.coverage* /docs/_build /.pytest_cache/ /test.py /.pytype/ test_server-0.0.40/.travis.yml000066400000000000000000000034321420715245500162600ustar00rootroot00000000000000python: 2.7 matrix: include: # code quality - os: linux language: python python: 3.4 env: TOX_ENV=qa - os: osx language: generic env: - TOX_ENV=py27 - PYENV_VERSION="2.7.13" - os: osx language: generic env: - TOX_ENV=py36 - PYENV_VERSION="3.6.0" - os: linux language: python env: TOX_ENV=py27 - os: linux language: python env: TOX_ENV=py34 - os: linux language: python python: 3.5 env: TOX_ENV=py35 - os: linux language: python python: 3.6 env: TOX_ENV=py36 exclude: - python: 2.7 # hack to exclude default no-env travis job install: # Deal with issue on Travis builders re: multiprocessing.Queue :( #- "sudo rm -rf /dev/shm && sudo ln -s /run/shm /dev/shm" #- | # if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then # brew update # brew unlink python # brew unlink python3 # if [[ TOX_ENV == py2* ]]; then # brew install python # else # brew install python3 # fi # fi #- | # if [[ TOX_ENV == py2* ]]; then # pip install -U pip setuptools tox # else # pip3 install -U pip setuptools tox # fi - | if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew update brew upgrade pyenv fi before_script: # Install custom pyton version on OSX - | if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then if [[ ! -z "$VIRTUAL_ENV" ]]; then deactivate; fi eval "$(pyenv init -)" pyenv install --skip-existing "$PYENV_VERSION" pyenv global "$PYENV_VERSION" fi - python -m pip install -U pip setuptools tox script: - tox -e $TOX_ENV after_success: - | if [[ "$TOX_ENV" == "py27" && "$TRAVIS_OS_NAME" == "linux" ]]; then coveralls; fi; test_server-0.0.40/CHANGELOG.md000066400000000000000000000051141420715245500157570ustar00rootroot00000000000000# Change Log of test_server Library ## [0.0.32] - unreleased ### Changed ## [0.0.31] - 2018-05-05 ### Added * Add support for non-ascii headers ## [0.0.30] - 2018-05-01 ### Changed * Migrate to bottle/webtest/weitress from tornado * Items of `request['files'][key]` are dicts now ### Removed * Removed subprocess mode ## [0.0.29] - 2018-04-17 ### Changed * Allow to use null bytes in response headers ## [0.0.28] - 2018-04-08 ### Changed * Restrict tornado dependency version: <5.0.0 ## [0.0.27] - 2017-03-12 ### Added * Add partial support for requests in non-UTF-8 encoding ### Fixed * Fix bug: test server fails to start on non-zero port ### Changed * Change request/response access method: use direct access * If the port is zero, then it is select automatically from free ports ## [0.0.26] - 2017-02-26 ### Fixed - Fix missing filelock dependency in setup.py ## [0.0.25] - 2017-02-25 ### Added - Option to run the server in subprocess ## [0.0.24] - 2017-01-29 ### Added - Add `keep_alive` option to start method ### Changed - Disable keep-alive by default. ## [0.0.23] - 2017-01-20 ### Fixed - Fix bug: incorrect processing the request['done'] ## [0.0.22] - 2017-01-20 ### Fixed - Fix bug: incorrect processing the request['done'] ## [0.0.21] - 2017-01-20 ### Added - Add feature: request['done'] - Add method: wait_request - Add exception: WaitTimeoutError ## [0.0.20] - 2017-01-20 ### Changed - Internfal refactoring ## [0.0.19] - 2017-01-20 ### Removed - Remove timeout_iterator feature ## [0.0.18] - 2017-01-19 ### Added - Add feature: request['client_ip'] ## [0.0.17] - 2017-01-19 ### Added - Set server thread daemon=True by default ## [0.0.16] - 2017-01-11 ### Fixed - Fix setup.py ## [0.0.15] - 2017-01-11 ### Added - Add sleep command yielded from callback ## [0.0.14] - 2015-09-10 ### Added - Add support for OPTIONS requests ## [0.0.13] - 2015-06-14 ### Fixed - Fix py3 issue - Fixed other errors ## [0.0.12] - 2015-06-08 ### Fixed - Fix issue wit closing socket ## [0.0.11] - 2015-04-10 ### Added - Add support for files ## [0.0.10] - 2015-02-22 ### Fixed - Fix Server/Content-Type header issues ## [0.0.9] - 2015-02-19 ### Added - Support iterator in response['data'] ## [0.0.8] - 2015-02-19 ### Fixed - Fix bug in processing the callback ## [0.0.7] - 2015-02-19 ### Changed - Change method of setting/getting response parameters ## [0.0.6] - 2015-02-18 ### Changed - Refactoring ## [0.0.5] - 2015-02-17 ### Fixed - Fix socket bug ## [0.0.4] - 2015-02-17 ### Fixed - Fix py3 issues ## [0.0.3] - 2015-02-17 ### Changed - More tests ## [0.0.2] - 2015-02-17 ### Added - Basic features test_server-0.0.40/LICENSE000066400000000000000000000021011420715245500151440ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2015-2017, Gregory Petukhov Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. test_server-0.0.40/MANIFEST.in000066400000000000000000000004051420715245500157020ustar00rootroot00000000000000include LICENSE include CHANGELOG.md include conftest.py include docs include .flake8 include Makefile include pylintrc include pytest.ini include README.rst include requirements_dev.txt recursive-include tests *.py include tox.ini include test_server/py.typed test_server-0.0.40/Makefile000066400000000000000000000012471420715245500156110ustar00rootroot00000000000000.PHONY: build venv deps clean release check test docs build: venv deps venv: virtualenv -p python3 .env deps: .env/bin/pip install -r requirements_dev.txt clean: find -name '*.pyc' -delete find -name '*.swp' -delete find -name __pycache__ -delete release: git push; git push --tags; rm dist/*; python3 setup.py clean sdist; twine upload dist/* check: python setup.py check -s \ && pylint setup.py test_server tests \ && flake8 setup.py test_server tests \ && pytype setup.py test_server tests \ && mypy setup.py test_server tests test: coverage run -m pytest \ && coverage report -m docs: rm -r docs/_build \ && sphinx-build -b html docs docs/_build test_server-0.0.40/README.rst000066400000000000000000000025361420715245500156420ustar00rootroot00000000000000=========== Test-server =========== .. image:: https://travis-ci.org/lorien/test_server.png?branch=master :target: https://travis-ci.org/lorien/test_server Simple HTTP Server for testing HTTP clients. Installation ============ .. code:: bash pip install test_server Usage Example ============= Example: .. code:: python from unittest import TestCase import unittest from urllib.request import urlopen from test_server import TestServer, Response, HttpHeaderStorage class UrllibTestCase(TestCase): @classmethod def setUpClass(cls): cls.server = TestServer() cls.server.start() @classmethod def tearDownClass(cls): cls.server.stop() def setUp(self): self.server.reset() def test_get(self): self.server.add_response( Response( data=b"hello", headers={"foo": "bar"}, ) ) self.server.add_response(Response(data=b"zzz")) url = self.server.get_url() info = urlopen(url) self.assertEqual(b"hello", info.read()) self.assertEqual("bar", info.headers["foo"]) info = urlopen(url) self.assertEqual(b"zzz", info.read()) self.assertTrue("bar" not in info.headers) unittest.main() test_server-0.0.40/appveyor.yml000066400000000000000000000007161420715245500165410ustar00rootroot00000000000000environment: matrix: - PYTHON: "C:\\Python27" - PYTHON: "C:\\Python27-x64" - PYTHON: "C:\\Python33" - PYTHON: "C:\\Python33-x64" - PYTHON: "C:\\Python34" - PYTHON: "C:\\Python34-x64" - PYTHON: "C:\\Python35" - PYTHON: "C:\\Python35-x64" - PYTHON: "C:\\Python36" - PYTHON: "C:\\Python36-x64" install: - "%PYTHON%/python.exe -m pip install tox" build: off test_script: - "%PYTHON%/python.exe -m tox -e py-appveyor" test_server-0.0.40/docs/000077500000000000000000000000001420715245500150755ustar00rootroot00000000000000test_server-0.0.40/docs/api_error.rst000066400000000000000000000002071420715245500176100ustar00rootroot00000000000000.. _api_error: Module test_server.error ======================== .. automodule:: test_server.error :members: :undoc-members: test_server-0.0.40/docs/api_server.rst000066400000000000000000000002131420715245500177620ustar00rootroot00000000000000.. _api_server: Module test_server.server ========================= .. automodule:: test_server.server :members: :undoc-members: test_server-0.0.40/docs/conf.py000066400000000000000000000234011420715245500163740ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # test_server documentation build configuration file, created by # sphinx-quickstart on Sat Feb 25 22:55:14 2017. # # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # import os import sys sys.path.insert(0, os.path.abspath("..")) # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. # # needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ "sphinx.ext.autodoc", "sphinx.ext.viewcode", ] # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # # source_suffix = ['.rst', '.md'] source_suffix = ".rst" # The encoding of source files. # # source_encoding = 'utf-8-sig' # The master toctree document. master_doc = "index" # General information about the project. project = u"test_server" copyright = u"2017-2022, Gregory Petukhov" author = u"Gregory Petukhov" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = u".".join(u"0.0.40".split(".")[:2]) # The full version, including alpha/beta/rc tags. release = u"0.0.40" # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: # # today = '' # # Else, today_fmt is used as the format for a strftime call. # # today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This patterns also effect to html_static_path and html_extra_path exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] # The reST default role (used for this markup: `text`) to use for all # documents. # # default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. # # add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). # # add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. # # show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. # keep_warnings = False # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = False # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # html_theme = "alabaster" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. # # html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. # html_theme_path = [] # The name for this set of Sphinx documents. # " v documentation" by default. # # html_title = u'test_server v0.0.25' # A shorter title for the navigation bar. Default is the same as html_title. # # html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. # # html_logo = None # The name of an image file (relative to this directory) to use as a favicon of # the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. # # html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". # html_static_path = ["_static"] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. # # html_extra_path = [] # If not None, a 'Last updated on:' timestamp is inserted at every page # bottom, using the given strftime format. # The empty string is equivalent to '%b %d, %Y'. # # html_last_updated_fmt = None # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. # # html_use_smartypants = True # Custom sidebar templates, maps document names to template names. # # html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. # # html_additional_pages = {} # If false, no module index is generated. # # html_domain_indices = True # If false, no index is generated. # # html_use_index = True # If true, the index is split into individual pages for each letter. # # html_split_index = False # If true, links to the reST sources are added to the pages. # # html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. # # html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. # # html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. # # html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). # html_file_suffix = None # Language to be used for generating the HTML full-text search index. # Sphinx supports the following languages: # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr', 'zh' # # html_search_language = 'en' # A dictionary with options for the search language support, empty by default. # 'ja' uses this config value. # 'zh' user can custom change `jieba` dictionary path. # # html_search_options = {'type': 'default'} # The name of a javascript file (relative to the configuration directory) that # implements a search results scorer. If empty, the default will be used. # # html_search_scorer = 'scorer.js' # Output file base name for HTML help builder. htmlhelp_basename = "test_serverdoc" # -- Options for LaTeX output --------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # # 'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). # # 'pointsize': '10pt', # Additional stuff for the LaTeX preamble. # # 'preamble': '', # Latex figure (float) alignment # # 'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ ( master_doc, "test_server.tex", u"test\\_server Documentation", u"Gregory Petukhov", "manual", ), ] # The name of an image file (relative to this directory) to place at the top of # the title page. # # latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. # # latex_use_parts = False # If true, show page references after internal links. # # latex_show_pagerefs = False # If true, show URL addresses after external links. # # latex_show_urls = False # Documents to append as an appendix to all manuals. # # latex_appendices = [] # It false, will not define \strong, \code, itleref, \crossref ... but only # \sphinxstrong, ..., \sphinxtitleref, ... To help avoid clash with user added # packages. # # latex_keep_old_macro_names = True # If false, no module index is generated. # # latex_domain_indices = True # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [(master_doc, "test_server", u"test_server Documentation", [author], 1)] # If true, show URL addresses after external links. # # man_show_urls = False # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ( master_doc, "test_server", u"test_server Documentation", author, "test_server", "One line description of project.", "Miscellaneous", ), ] # Documents to append as an appendix to all manuals. # # texinfo_appendices = [] # If false, no module index is generated. # # texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. # # texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. # # texinfo_no_detailmenu = False test_server-0.0.40/docs/index.rst000066400000000000000000000032761420715245500167460ustar00rootroot00000000000000test_server documentation ========================= Package test_servers helps you to test HTTP clients: * see details of HTTP request made by client * serve to client custom HTTP response Basic example: .. code:: python from test_server import TestServer, Response from urllib.request import urlopen server = TestServer() server.start() server.add_response(Response(data=b'response-data')) req = urlopen(server.get_url(), b'request-data') assert req.read() == b'response-data' assert server.get_request().data == b'request-data' Request object -------------- The request object contains information about HTTP request sent by HTTP client to test_server. The request object has these attrirubtes: :args: query string arguments :headers: HTTP headers :cookies: cookies :path: the path fragmet of requested URL :method: HTTP method :data: body of request :files: files sent with the request :client_ip: IP address the request has been sent from :charset: the character set which data of request are encoded with Response object --------------- The response object controls the data which the HTTP client would received in response from test server. Available keys are: :callback: function that builds completely custom request :raw_callback: function that returns complete HTTP response as bytes blob :cookies: cookies :data: body of HTTP response :headers: HTTP headers :sleep: amount of time to wait before send response data :status: HTTP status code API --- .. toctree:: :maxdepth: 2 api_server api_error Indices and tables ------------------ * :ref:`genindex` * :ref:`modindex` * :ref:`search` test_server-0.0.40/pylintrc000066400000000000000000000005421420715245500157350ustar00rootroot00000000000000[MASTER] jobs=4 [MESSAGES CONTROL] disable=R,missing-docstring,no-member,fixme [BASIC] method-rgx=([a-z_][a-z0-9_]{2,30}|test_[a-z_][a-z0-9_]{2,50})$ function-rgx=([a-z_][a-z0-9_]{2,30}|test_[a-z_][a-z0-9_]{2,50})$ variable-rgx=[a-z_][a-z0-9_]{1,30}$ argument-rgx=[a-z_][a-z0-9_]{1,30}$ [LOGGING] logging-format-style=new [FORMAT] max-line-length=88 test_server-0.0.40/pytest.ini000066400000000000000000000002201420715245500161700ustar00rootroot00000000000000[pytest] testpaths = tests/ python_files = *.py python_classes= addopts=--tb=short -m 'not bug' markers = bug: run test that possibly fails test_server-0.0.40/requirements_dev.txt000066400000000000000000000001341420715245500202650ustar00rootroot00000000000000coverage flake8 bumpversion pytest pylint sphinx pytype mypy types-urllib3 types-setuptools test_server-0.0.40/setup.py000066400000000000000000000026341420715245500156640ustar00rootroot00000000000000import os from setuptools import setup ROOT = os.path.dirname(os.path.realpath(__file__)) setup( # Meta data name="test_server", version="0.0.40", author="Gregory Petukhov", author_email="lorien@lorien.name", maintainer="Gregory Petukhov", maintainer_email="lorien@lorien.name", url="https://github.com/lorien/test_server", description="Server for testing HTTP clients", long_description=open(os.path.join(ROOT, "README.rst"), encoding="utf-8").read(), download_url="https://pypi.python.org/pypi/test_server", keywords="test testing server http-server", license="MIT License", # Package files packages=["test_server"], package_data={"test_server": ["py.typed"]}, zip_safe=False, install_requires=[], # Topics classifiers=[ "Development Status :: 4 - Beta", "Programming Language :: Python", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: Implementation :: CPython", "License :: OSI Approved :: MIT License", "Topic :: Software Development :: Libraries :: Python Modules", ], ) test_server-0.0.40/test_server/000077500000000000000000000000001420715245500165125ustar00rootroot00000000000000test_server-0.0.40/test_server/__init__.py000066400000000000000000000004641420715245500206270ustar00rootroot00000000000000from test_server.server import * # noqa pylint: disable=wildcard-import from test_server.structure import * # noqa pylint: disable=wildcard-import from test_server.error import * # noqa pylint: disable=wildcard-import from test_server.version import TEST_SERVER_VERSION __version__ = TEST_SERVER_VERSION test_server-0.0.40/test_server/error.py000066400000000000000000000013451420715245500202200ustar00rootroot00000000000000__all__ = [ "TestServerError", "WaitTimeoutError", "InternalError", "RequestNotProcessed", "NoResponse", ] class TestServerError(Exception): """Base class for all errrors which belogns to test_server package""" class WaitTimeoutError(TestServerError): """Raised by wait_request method if it timed out waiting a request done""" class InternalError(TestServerError): """Raised when exception happens during the processing request sent by client""" class RequestNotProcessed(TestServerError): """Raised by get_request method when no request has been processed""" class NoResponse(TestServerError): """Raised by get_response method when no response data is available to hande the request""" test_server-0.0.40/test_server/py.typed000066400000000000000000000000001420715245500201770ustar00rootroot00000000000000test_server-0.0.40/test_server/server.py000066400000000000000000000325351420715245500204020ustar00rootroot00000000000000# pylint: disable=consider-using-f-string from pprint import pprint # pylint: disable=unused-import import time from collections import defaultdict from threading import Thread, Event import cgi from io import BytesIO import logging from typing import Optional, Callable, List, MutableMapping, cast from socketserver import ThreadingMixIn, TCPServer from http.server import BaseHTTPRequestHandler from http.cookies import SimpleCookie from urllib.parse import urljoin, parse_qsl from .version import TEST_SERVER_VERSION from .error import ( TestServerError, WaitTimeoutError, InternalError, RequestNotProcessed, NoResponse, ) from .structure import HttpHeaderStorage, HttpHeaderStream __all__: list = ["TestServer", "WaitTimeoutError", "Response", "Request"] INTERNAL_ERROR_RESPONSE_STATUS: int = 555 class HandlerResult: __slots__ = ["status", "headers", "data"] def __init__( self, status: Optional[int] = None, headers: Optional[HttpHeaderStorage] = None, data: Optional[bytes] = None, ) -> None: self.status = status if status is not None else 200 self.headers = headers if headers else HttpHeaderStorage() self.data = data if data else b"" class Response(object): def __init__( self, callback: Optional[Callable] = None, raw_callback: Optional[Callable] = None, data: Optional[bytes] = None, headers: Optional[HttpHeaderStream] = None, sleep: Optional[float] = None, status: Optional[int] = None, ) -> None: self.callback = callback self.raw_callback = raw_callback self.data = b"" if data is None else data self.headers = HttpHeaderStorage(headers) self.sleep = sleep self.status = 200 if status is None else status class Request(object): def __init__( self, args: Optional[dict] = None, client_ip: Optional[str] = None, cookies: Optional[SimpleCookie] = None, data: Optional[bytes] = None, files: Optional[dict] = None, headers: Optional[HttpHeaderStream] = None, method: Optional[str] = None, path: Optional[str] = None, ): self.args = {} if args is None else args self.client_ip = {} if client_ip is None else client_ip self.cookies: SimpleCookie = SimpleCookie() if cookies is None else cookies self.data = None if data is None else data self.files = {} if files is None else files self.headers = HttpHeaderStorage(headers) self.method = None if method is None else method self.path: str = "" if path is None else path VALID_METHODS: List[str] = ["get", "post", "put", "delete", "options", "patch"] class ThreadingTCPServer(ThreadingMixIn, TCPServer): allow_reuse_address: bool = True started: bool = False def __init__( self, server_address, RequestHandlerClass, test_server=None, **kwargs ) -> None: super().__init__(server_address, RequestHandlerClass, **kwargs) self.test_server = test_server self.test_server.server_started.set() class TestServerHandler(BaseHTTPRequestHandler): server: ThreadingTCPServer def _collect_request_data(self, method: str) -> Request: data: MutableMapping = { "args": {}, "headers": [], "files": defaultdict(list), } data["client_ip"] = self.client_address[0] try: qs = self.path.split("?")[1] except IndexError: qs = "" params = dict(parse_qsl(qs)) for key, val in params.items(): data["args"][key] = val for key, val in self.headers.items(): data["headers"].append((key, val)) path = self.path data["path"] = path.split("?")[0] data["method"] = method.upper() data["cookies"] = SimpleCookie(self.headers["Cookie"]) clen = int(self.headers["Content-Length"] or "0") request_data = self.rfile.read(clen) data["data"] = request_data ctype = self.headers["Content-Type"] if ctype and ctype.split(";")[0] == "multipart/form-data": form = cgi.FieldStorage( fp=BytesIO(request_data), headers=cast(MutableMapping, self.headers), environ={ "REQUEST_METHOD": "POST", "CONTENT_TYPE": self.headers["Content-Type"], }, ) for field_key in form.keys(): # pylint: disable=consider-using-dict-items box = form[field_key] for field in box if isinstance(box, list) else [box]: data["files"].setdefault(field_key, []).append( { "name": field_key, # "raw_filename": None, "content_type": field.type, "filename": field.filename, "content": field.file.read(), } ) return Request(**data) def _request_handler(self) -> None: try: test_srv = self.server.test_server # pytype: disable=attribute-error method = self.command.lower() resp = test_srv.get_response(method) if resp.sleep: time.sleep(resp.sleep) test_srv.add_request(self._collect_request_data(method)) result = HandlerResult() if resp.raw_callback: data = resp.raw_callback() if isinstance(data, bytes): self.write_raw_response_data(data) return else: raise InternalError("Raw callback must return bytes data") if resp.callback: cb_res = resp.callback() if not isinstance(cb_res, dict): raise InternalError("Callback response is not a dict") elif cb_res.get("type") == "response": for key in cb_res: if key not in ( "type", "status", "headers", "data", ): raise InternalError( "Callback response contains invalid key: %s" % key ) if "status" in cb_res: result.status = cb_res["status"] if "headers" in cb_res: result.headers.extend(cb_res["headers"]) if "data" in cb_res: if isinstance(cb_res["data"], bytes): result.data = cb_res["data"] else: raise InternalError( 'Callback repsponse field "data" must be bytes' ) else: raise InternalError( "Callback response has invalid type key: %s" % cb_res.get("type", "NA") ) else: result.status = resp.status result.headers.extend(resp.headers.items()) data = resp.data if isinstance(data, bytes): result.data = data else: raise InternalError('Response parameter "data" must be bytes') port = self.server.test_server.port # pytype: disable=attribute-error result.headers.set("Listen-Port", str(port)) if "content-type" not in result.headers: result.headers.set("Content-Type", "text/html; charset=utf-8") if "server" not in result.headers: result.headers.set("Server", "TestServer/%s" % TEST_SERVER_VERSION) self.write_response_data(result.status, result.headers, result.data) except Exception as ex: # pylint: disable=broad-except logging.exception("Unexpected error happend in test server request handler") self.write_response_data( INTERNAL_ERROR_RESPONSE_STATUS, HttpHeaderStorage(), str(ex).encode("utf-8"), ) finally: test_srv.num_req_processed += 1 def write_response_data( self, status: int, headers: HttpHeaderStorage, data: bytes ) -> None: self.send_response(status) for key, val in headers.items(): self.send_header(key, val) self.end_headers() self.wfile.write(data) def write_raw_response_data(self, data: bytes) -> None: self.wfile.write(data) # pylint: disable=attribute-defined-outside-init self._headers_buffer: List[str] = [] # https://github.com/python/cpython/blob/main/Lib/http/server.py def send_response(self, code: int, message: Optional[str] = None) -> None: """ Custom method which does not send Server and Date headers This method overrides standard method from super class. """ self.log_request(code) self.send_response_only(code, message) do_GET = _request_handler do_POST = _request_handler do_PUT = _request_handler do_DELETE = _request_handler do_OPTIONS = _request_handler do_PATCH = _request_handler class TestServer(object): def __init__(self, address="127.0.0.1", port=0) -> None: self.server_started: Event = Event() self._requests: List = [] self._responses: MutableMapping = defaultdict(list) self.port: Optional[int] = None self._config_port: int = port self.address: str = address self._thread: Optional[Thread] = None self._server: Optional[ThreadingTCPServer] = None self._started: Event = Event() self.num_req_processed: int = 0 self.reset() def _thread_server(self) -> None: """Ask HTTP server start processing requests This function is supposed to be run in separate thread. """ self._server = ThreadingTCPServer( (self.address, self._config_port), TestServerHandler, test_server=self ) self._server.serve_forever(poll_interval=0.1) # **************** # Public Interface # **************** def add_request(self, req: Request) -> None: self._requests.append(req) def reset(self) -> None: self.num_req_processed = 0 self._requests.clear() self._responses.clear() def start(self, daemon: bool = True) -> None: """Start the HTTP server.""" self._thread = Thread( target=self._thread_server, ) self._thread.daemon = daemon self._thread.start() self.wait_server_started() self.port = cast(ThreadingTCPServer, self._server).socket.getsockname()[1] def wait_server_started(self) -> None: # I could not foind another way # to handle multiple socket issues # other than taking some sleep time.sleep(0.01) self.server_started.wait() def stop(self) -> None: if self._server: self._server.shutdown() self._server.server_close() def get_url(self, path: str = "", port: Optional[int] = None) -> str: """Build URL that is served by HTTP server.""" if port is None: port = cast(int, self.port) return urljoin("http://%s:%d" % (self.address, port), path) def wait_request(self, timeout: float) -> None: """Stupid implementation that eats CPU.""" start: float = time.time() while True: if self.num_req_processed: break time.sleep(0.01) if time.time() - start > timeout: raise WaitTimeoutError("No request processed in %d seconds" % timeout) def request_is_done(self) -> bool: return self.num_req_processed > 0 def get_request(self) -> Request: try: return self._requests[-1] except IndexError as ex: raise RequestNotProcessed("Request has not been processed") from ex @property def request(self) -> Request: return self.get_request() def add_response( self, resp: Response, count: int = 1, method: Optional[str] = None ) -> None: assert method is None or isinstance(method, str) assert count < 0 or count > 0 if method and method not in VALID_METHODS: raise TestServerError("Invalid method: %s" % method) self._responses[method].append( { "count": count, "response": resp, }, ) def get_response(self, method: str) -> Response: while True: item = None scope = None try: scope = self._responses[method] item = scope[0] except IndexError: try: scope = self._responses[None] item = scope[0] except IndexError as ex: raise NoResponse("No response available") from ex if item["count"] == -1: return item["response"] else: item["count"] -= 1 if item["count"] < 1: scope.pop(0) return item["response"] test_server-0.0.40/test_server/structure.py000066400000000000000000000041421420715245500211250ustar00rootroot00000000000000from pprint import pprint # pylint: disable=unused-import from collections import OrderedDict from typing import ( Union, Optional, Tuple, Mapping, MutableMapping, List, cast, Iterable, ) __all__ = ["HttpHeaderStorage"] HttpHeaderStream = Union[ Mapping[str, str], Iterable[Tuple[str, str]], ] class HttpHeaderStorage(object): """Storage for HTTP Headers. The storage maps string keys to one or multiple string values. Keys are case insensitive though the original case is stored. """ def __init__( self, data: Optional[HttpHeaderStream] = None, charset: str = "utf-8" ) -> None: self._store: MutableMapping[str, List[str]] = OrderedDict() self._charset = charset if data is not None: self.extend(data) # Public Interface def set(self, key: str, value: str) -> None: # Store original case of key self._store[key.lower()] = [key, value] def get(self, key: str) -> str: return self._store[key.lower()][1] def getlist(self, key: str) -> List[str]: return self._store[key.lower()][1:] def remove(self, key: str): del self._store[key.lower()] def add(self, key: str, value: str): box = self._store.setdefault(key.lower(), [key]) box.append(value) def extend(self, data: HttpHeaderStream) -> None: seq = ( data.items() if isinstance(data, MutableMapping) else cast(Iterable[Tuple[str, str]], data) ) for key, val in seq: self.add(key, val) def __contains__(self, key: str) -> bool: return key.lower() in self._store def count_keys(self) -> int: return len(self._store.keys()) def count_items(self) -> int: return sum(1 for _ in self.items()) def items(self): for items in self._store.values(): original_key = items[0] for idx, item in enumerate(items): if idx > 0: yield original_key, item def __repr__(self) -> str: return str(list(self.items())) test_server-0.0.40/test_server/version.py000066400000000000000000000000371420715245500205510ustar00rootroot00000000000000TEST_SERVER_VERSION = "0.0.40" test_server-0.0.40/tests/000077500000000000000000000000001420715245500153075ustar00rootroot00000000000000test_server-0.0.40/tests/__init__.py000066400000000000000000000000001420715245500174060ustar00rootroot00000000000000test_server-0.0.40/tests/test_httpheaderstorage.py000066400000000000000000000025751420715245500224460ustar00rootroot00000000000000import pytest from test_server.structure import HttpHeaderStorage def test_constructor_no_data(): HttpHeaderStorage() def test_constructor_dict(): HttpHeaderStorage({"foo": "bar"}) def test_constructor_list(): HttpHeaderStorage([("foo", "bar")]) def test_set_get_simple_value(): obj = HttpHeaderStorage() obj.set("foo", "bar") assert obj.get("foo") == "bar" def test_set_get_multi_value(): obj = HttpHeaderStorage() obj.add("foo", "bar") obj.add("foo", "baz") assert obj.getlist("foo") == ["bar", "baz"] def test_delitem(): obj = HttpHeaderStorage() with pytest.raises(KeyError): obj.remove("foo") obj.set("foo", "bar") obj.remove("foo") with pytest.raises(KeyError): obj.remove("foo") def test_repr(): obj = HttpHeaderStorage() obj.add("foo", "bar") obj.add("foo", "baz") assert repr(obj) == "[('foo', 'bar'), ('foo', 'baz')]" def test_constructor_key_multivalue(): obj = HttpHeaderStorage([("set-cookie", "foo=bar"), ("set-cookie", "baz=gaz")]) assert obj.getlist("set-cookie") == ["foo=bar", "baz=gaz"] def test_count_keys(): obj = HttpHeaderStorage() obj.add("foo", "bar") obj.add("foo", "baz") assert 1 == obj.count_keys() def test_count_items(): obj = HttpHeaderStorage() obj.add("foo", "bar") obj.add("foo", "baz") assert 2 == obj.count_items() test_server-0.0.40/tests/test_server.py000066400000000000000000000303471420715245500202350ustar00rootroot00000000000000# pylint: disable=consider-using-f-string from pprint import pprint # pylint: disable=unused-import from threading import Thread import time from urllib.parse import unquote, quote from urllib3 import PoolManager from urllib3.util.retry import Retry from urllib3.response import HTTPResponse import pytest from test_server import ( TestServer, WaitTimeoutError, TestServerError, Response, Request, RequestNotProcessed, ) import test_server from .util import fixture_global_server, fixture_server # pylint: disable=unused-import NETWORK_TIMEOUT = 1 SPECIFIC_TEST_PORT = 10100 pool = PoolManager() def request( url, data=None, method=None, headers=None, fields=None, retries_redirect=10 ) -> HTTPResponse: params = { "headers": headers, "timeout": NETWORK_TIMEOUT, "retries": Retry( total=None, connect=0, read=0, redirect=retries_redirect, other=0, ), "fields": fields, } if data: assert isinstance(data, bytes) params["body"] = data if not method: method = "POST" if (data or fields) else "GET" return pool.request(method, url, **params) # WTF: urllib3 makes TWO requests :-/ # def test_non_ascii_header(server: TestServer) -> None: # server.add_response(Response(headers=[("z", server.get_url() + "фыва")])) # res = request(server.get_url(), retries_redirect=False) # print(res.headers) def test_non_ascii_header(server: TestServer) -> None: server.add_response( Response(status=301, headers=[("Location", server.get_url(quote("фыва")))]) ) server.add_response(Response()) request(server.get_url()) assert quote("фыва") in server.get_request().path def test_get(server: TestServer) -> None: valid_data = b"zorro" server.add_response(Response(data=valid_data)) res = request(server.get_url()) assert res.data == valid_data def test_non_utf_request_data(server: TestServer) -> None: server.add_response(Response(data=b"abc")) res = request(url=server.get_url(), data="конь".encode("cp1251")) assert res.data == b"abc" assert server.get_request().data == "конь".encode("cp1251") def test_request_client_ip(server: TestServer) -> None: server.add_response(Response()) request(server.get_url()) assert server.address == server.get_request().client_ip def test_path(server: TestServer) -> None: server.add_response(Response()) request(server.get_url("/foo?bar=1")) assert server.get_request().path == "/foo" assert server.get_request().args["bar"] == "1" def test_post(server: TestServer) -> None: server.add_response(Response(data=b"abc"), method="post") res = request(server.get_url(), b"req-data") assert res.data == b"abc" assert server.get_request().data == b"req-data" def test_response_once_specific_method(server: TestServer) -> None: server.add_response(Response(data=b"bar"), method="get") server.add_response(Response(data=b"foo")) assert request(server.get_url()).data == b"bar" def test_request_headers(server: TestServer) -> None: server.add_response(Response()) request(server.get_url(), headers={"Foo": "Bar"}) assert server.get_request().headers.get("foo") == "Bar" def test_response_once_reset_headers(server: TestServer) -> None: server.add_response(Response(headers=[("foo", "bar")])) server.reset() res = request(server.get_url()) assert res.status == 555 assert b"No response" in res.data def test_method_sleep(server: TestServer) -> None: server.add_response(Response()) delay = 0.3 start = time.time() request(server.get_url()) elapsed = time.time() - start assert elapsed <= delay server.add_response(Response(sleep=delay)) start = time.time() request(server.get_url()) elapsed = time.time() - start assert elapsed > delay def test_request_done_after_start(server: TestServer) -> None: server = TestServer() try: server.start() assert not server.request_is_done() finally: server.stop() def test_request_done(server: TestServer) -> None: assert not server.request_is_done() server.add_response(Response()) request(server.get_url()) assert server.request_is_done() def test_wait_request(server: TestServer) -> None: server.add_response(Response(data=b"foo")) def worker(): time.sleep(1) request(server.get_url("?method=test-wait-request")) th = Thread(target=worker) th.start() with pytest.raises(WaitTimeoutError): server.wait_request(0.5) server.wait_request(2) th.join() def test_request_cookies(server: TestServer) -> None: server.add_response(Response()) request(url=server.get_url(), headers={"Cookie": "foo=bar"}) assert server.get_request().cookies["foo"].value == "bar" def test_default_header_content_type(server: TestServer) -> None: server.add_response(Response()) info = request(server.get_url()) assert info.headers["content-type"] == "text/html; charset=utf-8" def test_custom_header_content_type(server: TestServer) -> None: server.add_response( Response(headers=[("Content-Type", "text/html; charset=koi8-r")]) ) info = request(server.get_url()) assert info.headers["content-type"] == "text/html; charset=koi8-r" def test_default_header_server(server: TestServer) -> None: server.add_response(Response()) info = request(server.get_url()) assert info.headers["server"] == ("TestServer/%s" % test_server.__version__) def test_custom_header_server(server: TestServer) -> None: server.add_response(Response(headers=[("Server", "Google")])) info = request(server.get_url()) assert info.headers["server"] == "Google" def test_options_method(server: TestServer) -> None: server.add_response(Response(data=b"abc")) res = request(url=server.get_url(), method="OPTIONS") assert server.get_request().method == "OPTIONS" assert res.data == b"abc" def test_multiple_start_stop_cycles() -> None: for _ in range(30): server = TestServer() server.start() try: server.add_response(Response(data=b"zorro"), count=10) for _ in range(10): res = request(server.get_url()) assert res.data == b"zorro" finally: server.stop() def test_specific_port() -> None: server = TestServer(address="localhost", port=SPECIFIC_TEST_PORT) try: server.start() server.add_response(Response(data=b"abc")) data = request(server.get_url()).data assert data == b"abc" finally: server.stop() def test_null_bytes(server: TestServer) -> None: server.add_response( Response( status=302, headers=[ ("Location", server.get_url().rstrip("/") + "/\x00/"), ], ) ) server.add_response(Response(data=b"zzz")) res = request(server.get_url()) assert res.data == b"zzz" assert unquote(server.get_request().path) == "/\x00/" def test_callback(server: TestServer) -> None: def get_callback(): return { "type": "response", "data": b"Hello", "headers": [ ("method", "get"), ], } def post_callback(): return { "type": "response", "status": 201, "data": b"hey", "headers": [ ("method", "post"), ("set-cookie", "foo=bar"), ], } server.add_response(Response(callback=get_callback)) server.add_response(Response(callback=post_callback), method="post") info = request(server.get_url()) assert info.headers.get("method") == "get" assert info.data == b"Hello" info = request(server.get_url(), b"key=val") assert info.headers["method"] == "post" assert info.headers["set-cookie"] == "foo=bar" assert info.data == b"hey" assert info.status == 201 def test_response_data_invalid_type(server: TestServer) -> None: server.add_response(Response(data=1)) # type: ignore res = request(server.get_url()) assert res.status == 555 assert b"must be bytes" in res.data def test_stop_not_started_server() -> None: server = TestServer() server.stop() def test_start_request_stop_same_port() -> None: server = TestServer() for _ in range(10): try: server.start() server.add_response(Response()) request(server.get_url()) finally: server.stop() def test_file_uploading(server: TestServer) -> None: server.add_response(Response()) request( server.get_url(), fields={ "image": ("emoji.png", b"zzz"), }, ) assert server.get_request().files["image"][0]["name"] == "image" def test_callback_response_not_dict(server: TestServer) -> None: def callback(): return ["foo", "bar"] server.add_response(Response(callback=callback)) res = request(server.get_url()) assert res.status == 555 assert b"is not a dict" in res.data def test_callback_response_invalid_type(server: TestServer) -> None: def callback(): return { "foo": "bar", } server.add_response(Response(callback=callback)) res = request(server.get_url()) assert res.status == 555 assert b"invalid type key" in res.data def test_callback_response_invalid_key(server: TestServer) -> None: def callback(): return { "type": "response", "foo": "bar", } server.add_response(Response(callback=callback)) res = request(server.get_url()) assert res.status == 555 assert b"contains invalid key" in res.data def test_callback_data_non_bytes(server: TestServer) -> None: def callback(): return { "type": "response", "data": "bar", } server.add_response(Response(callback=callback)) res = request(server.get_url()) assert res.status == 555 assert b"must be bytes" in res.data def test_invalid_response_key() -> None: with pytest.raises(TypeError) as ex: # pylint: disable=unexpected-keyword-arg Response(foo="bar") # type: ignore assert "unexpected keyword argument" in str(ex.value) def test_get_request_no_request(server: TestServer) -> None: with pytest.raises(RequestNotProcessed): server.get_request() def test_add_response_invalid_method(server: TestServer) -> None: with pytest.raises(TestServerError) as ex: server.add_response(Response(), method="foo") assert "Invalid method" in str(ex.value) def test_add_response_count_minus_one(server: TestServer) -> None: server.add_response(Response(), count=-1) for _ in range(3): assert 200 == request(server.get_url()).status def test_add_response_count_one_default(server: TestServer) -> None: server.add_response(Response()) assert 200 == request(server.get_url()).status assert b"No response" in request(server.get_url()).data server.add_response(Response(), count=1) assert 200 == request(server.get_url()).status assert b"No response" in request(server.get_url()).data def test_add_response_count_two(server: TestServer) -> None: server.add_response(Response(), count=2) assert 200 == request(server.get_url()).status assert 200 == request(server.get_url()).status assert b"No response" in request(server.get_url()).data def test_raw_callback(server): def callback(): return b"HTTP/1.0 200 OK\nFoo: Bar\nGaz: Baz\nContent-Length: 5\n\nhello" server.add_response(Response(raw_callback=callback)) res = request(server.get_url()) assert "foo" in res.headers assert b"hello" == res.data def test_raw_callback_invalid_type(server): def callback(): return "hey" server.add_response(Response(raw_callback=callback)) res = request(server.get_url()) assert b"must return bytes" in res.data def test_request_property(server): server.add_response(Response()) request(server.get_url()) assert isinstance(server.request, Request) def test_put_request(server): server.add_response(Response()) request(server.get_url(), data=b"foo", method="put") assert server.request.method == "PUT" test_server-0.0.40/tests/test_types.py000066400000000000000000000013561420715245500200710ustar00rootroot00000000000000from typing import Mapping, MutableMapping, Dict def test_dict(): box = {} assert isinstance(box, Mapping) assert isinstance(box, MutableMapping) assert isinstance(box, Dict) assert isinstance(box, dict) def test_mapping_instance(): class Box(MutableMapping): # pragma: no cover def __delitem__(self, key): pass def __getitem__(self, key): pass def __setitem__(self, key, val): pass def __iter__(self): return iter([]) def __len__(self): return 0 box = Box() assert isinstance(box, Mapping) assert isinstance(box, MutableMapping) assert not isinstance(box, Dict) assert not isinstance(box, dict) test_server-0.0.40/tests/util.py000066400000000000000000000010041420715245500166310ustar00rootroot00000000000000import pytest from test_server import TestServer STATE = {"server": None} @pytest.fixture(scope="session", name="global_server") def fixture_global_server(): if not STATE["server"]: srv = TestServer() srv.start() STATE["server"] = srv yield STATE["server"] if STATE["server"]: STATE["server"].stop() STATE["server"] = None @pytest.fixture(scope="function", name="server") def fixture_server(global_server): global_server.reset() return global_server