test-server-0.0.27/0000755000175000017500000000000013061166440014666 5ustar lorienlorien00000000000000test-server-0.0.27/setup.cfg0000644000175000017500000000004613061166440016507 0ustar lorienlorien00000000000000[egg_info] tag_build = tag_date = 0 test-server-0.0.27/MANIFEST.in0000644000175000017500000000032713054726014016427 0ustar lorienlorien00000000000000include 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 test *.py include tox.ini test-server-0.0.27/Makefile0000644000175000017500000000035213061165754016335 0ustar lorienlorien00000000000000.PHONY: clean upload viewdoc clean: find -name '*.pyc' -delete find -name '*.swp' -delete find -name __pycache__ -delete upload: git push --tags; python setup.py sdist upload viewdoc: x-www-browser docs/_build/html/index.html test-server-0.0.27/pytest.ini0000644000175000017500000000021713061165754016726 0ustar lorienlorien00000000000000[pytest] testpaths = test/ python_files = *.py python_classes= addopts=--tb=short -m 'not bug' markers = bug: run test that possibly fails test-server-0.0.27/test_server.egg-info/0000755000175000017500000000000013061166440020725 5ustar lorienlorien00000000000000test-server-0.0.27/test_server.egg-info/top_level.txt0000644000175000017500000000001413061166440023452 0ustar lorienlorien00000000000000test_server test-server-0.0.27/test_server.egg-info/requires.txt0000644000175000017500000000003413061166440023322 0ustar lorienlorien00000000000000tornado six psutil filelock test-server-0.0.27/test_server.egg-info/dependency_links.txt0000644000175000017500000000000113061166440024773 0ustar lorienlorien00000000000000 test-server-0.0.27/test_server.egg-info/entry_points.txt0000644000175000017500000000010713061166440024221 0ustar lorienlorien00000000000000[console_scripts] test_server = test_server.server:script_test_server test-server-0.0.27/test_server.egg-info/SOURCES.txt0000644000175000017500000000071213061166440022611 0ustar lorienlorien00000000000000.flake8 CHANGELOG.md MANIFEST.in Makefile README.rst conftest.py pylintrc pytest.ini requirements_dev.txt setup.py tox.ini test/__init__.py test/server.py test_server/__init__.py test_server/container.py test_server/error.py test_server/server.py test_server.egg-info/PKG-INFO test_server.egg-info/SOURCES.txt test_server.egg-info/dependency_links.txt test_server.egg-info/entry_points.txt test_server.egg-info/requires.txt test_server.egg-info/top_level.txttest-server-0.0.27/test_server.egg-info/PKG-INFO0000644000175000017500000000557713061166440022040 0ustar lorienlorien00000000000000Metadata-Version: 1.1 Name: test-server Version: 0.0.27 Summary: Server to test HTTP clients Home-page: https://github.com/lorien/test_server Author: Gregory Petukhov Author-email: lorien@lorien.name License: MIT License Download-URL: https://pypi.python.org/pypi/test-server Description: =========== Test-server =========== .. image:: https://travis-ci.org/lorien/test_server.png?branch=master :target: https://travis-ci.org/lorien/test_server .. image:: https://ci.appveyor.com/api/projects/status/o3qhdh1gprcu1x1x :target: https://ci.appveyor.com/project/lorien/test-server .. image:: https://coveralls.io/repos/lorien/test_server/badge.svg?branch=master :target: https://coveralls.io/r/lorien/test_server?branch=master .. image:: https://api.codacy.com/project/badge/Grade/3ff9f3ebf06d4b7f8809b264837eac43 :target: https://www.codacy.com/app/lorien/test_server?utm_source=github.com&utm_medium=referral&utm_content=lorien/test_server&utm_campaign=badger HTTP Server to test HTTP clients. Installation ============ .. code:: bash pip install test-server Usage Example ============= Example: .. code:: python from unittest import TestCase try: from urllib import urlopen except ImportError: from urllib.request import urlopen from test_server import TestServer 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): token = b'zorro' self.server.response['data'] = token data = urlopen(self.server.base_url).read() self.assertEqual(data, token) Keywords: test testing server http-server Platform: UNKNOWN Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: License :: OSI Approved :: MIT License Classifier: Topic :: Software Development :: Libraries :: Application Frameworks Classifier: Topic :: Software Development :: Libraries :: Python Modules test-server-0.0.27/conftest.py0000644000175000017500000000042613054213401017057 0ustar lorienlorien00000000000000import pytest def pytest_addoption(parser): parser.addoption('--engine', type=str, default='thread', help='Method of running test HTTP server') @pytest.fixture(scope='session') def opt_engine(request): return request.config.getoption('--engine') test-server-0.0.27/setup.py0000644000175000017500000000266013061165754016413 0ustar lorienlorien00000000000000import os from setuptools import setup ROOT = os.path.dirname(os.path.realpath(__file__)) setup( # Meta data name='test-server', version='0.0.27', 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 to test HTTP clients', long_description=open(os.path.join(ROOT, 'README.rst')).read(), download_url='https://pypi.python.org/pypi/test-server', keywords='test testing server http-server', license='MIT License', # Package files packages=['test_server'], install_requires=['tornado', 'six', 'psutil', 'filelock'], entry_points={ 'console_scripts': [ 'test_server = test_server.server:script_test_server', ], }, # Topics classifiers=[ 'Programming Language :: Python', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: Implementation :: CPython', 'License :: OSI Approved :: MIT License', 'Topic :: Software Development :: Libraries :: Application Frameworks', 'Topic :: Software Development :: Libraries :: Python Modules', ], ) test-server-0.0.27/tox.ini0000644000175000017500000000155713061166422016211 0ustar lorienlorien00000000000000[tox] envlist = py2,py3,py2-engine-subprocess,py3-engine-subprocess,qa [testenv] commands = python setup.py check -s pytest --cov test_server \ engine-thread: --engine=thread \ engine-subprocess: --engine=subprocess \ --tb=short {posargs} deps = -rrequirements_dev.txt [testenv:py3-bug] commands=pytest -m bug --tb=short {posargs} [testenv:py-appveyor] passenv = DISTUTILS_USE_SDK MSSdk INCLUDE LIB commands = pytest {posargs} [testenv:pylint-all] commands = pylint test_server test --enable=all --disable=missing-docstring,locally-disabled,locally-enabled,suppressed-message {posargs} [testenv:qa] commands = flake8 setup.py useragent test pylint setup test_server test [testenv:pylint-debug] commands = pylint {posargs} [testenv:doc] whitelist_externals=make basepython=python3 changedir = docs commands = make html deps = sphinx test-server-0.0.27/.flake80000644000175000017500000000047413061165754016055 0ustar lorienlorien00000000000000[flake8] # https://flake8.readthedocs.io/en/2.0/warnings.html # E261 - at least two spaces before inline comment # E265 - block comment should start with '#' # W503 - line break occured before a binary operator # F811 - redefinition of unused name # F401 - name imported but unused ignore = E261,E265,W503,F811,F401 test-server-0.0.27/test/0000755000175000017500000000000013061166440015645 5ustar lorienlorien00000000000000test-server-0.0.27/test/server.py0000644000175000017500000002054013061165754017535 0ustar lorienlorien00000000000000# coding: utf-8 # pylint: disable=redefined-outer-name import time from threading import Thread import os from six.moves.urllib.request import urlopen, Request from six.moves.urllib.error import HTTPError, URLError import pytest from test_server import TestServer, WaitTimeoutError import test_server @pytest.fixture(scope='session') def global_server(opt_engine): server = TestServer(engine=opt_engine) server.start() yield server server.stop() @pytest.fixture(scope='function') def server(global_server): global_server.reset() return global_server @pytest.fixture(autouse=True) def skip_by_engine(request, opt_engine): if request.node.get_marker('skip_engine'): if request.node.get_marker('skip_engine').args[0] == opt_engine: pytest.skip('Skipped on engine %s' % opt_engine) def test_get(server): valid_data = b'zorro' server.response['data'] = valid_data data = urlopen(server.get_url()).read() assert data == valid_data def test_non_utf_request_data(server): server.request['charset'] = 'cp1251' server.response['data'] = 'abc' req = Request(url=server.get_url(), data=u'конь'.encode('cp1251')) assert urlopen(req).read() == b'abc' assert server.request['data'] == u'конь'.encode('cp1251') def test_request_client_ip(server): urlopen(server.get_url()).read() assert server.address == server.request['client_ip'] def test_path(server): urlopen(server.get_url('/foo?bar=1')).read() assert server.request['path'] == '/foo' assert server.request['args']['bar'] == '1' def test_post(server): server.response['post.data'] = b'resp-data' data = urlopen(server.get_url(), b'req-data').read() assert data == b'resp-data' assert server.request['data'] == b'req-data' def test_response_once_get(server): server.response['data'] = b'base' assert urlopen(server.get_url()).read() == b'base' server.response_once['data'] = b'tmp' assert urlopen(server.get_url()).read() == b'tmp' assert urlopen(server.get_url()).read() == b'base' def test_response_once_headers(server): server.response['headers'] = [('foo', 'bar')] info = urlopen(server.get_url()) assert info.headers['foo'] == 'bar' server.response_once['headers'] = [('baz', 'gaz')] info = urlopen(server.get_url()) assert info.headers['baz'] == 'gaz' assert 'foo' not in info.headers info = urlopen(server.get_url()) assert 'baz' not in info.headers assert info.headers['foo'] == 'bar' def test_request_headers(server): req = Request(server.get_url(), headers={'Foo': 'Bar'}) urlopen(req).read() assert server.request['headers']['foo'] == 'Bar' def test_response_once_reset_headers(server): server.response_once['headers'] = [('foo', 'bar')] server.reset() info = urlopen(server.get_url()) assert 'foo' not in info.headers def test_method_sleep(server): delay = 0.3 start = time.time() urlopen(server.get_url()) elapsed = time.time() - start assert elapsed <= delay server.response['sleep'] = delay start = time.time() urlopen(server.get_url()) elapsed = time.time() - start assert elapsed > delay def test_response_once_code(server): info = urlopen(server.get_url()) assert info.getcode() == 200 server.response_once['code'] = 403 with pytest.raises(HTTPError): urlopen(server.get_url()) info = urlopen(server.get_url()) assert info.getcode() == 200 def test_request_done_after_start(server): server = TestServer() server.start() assert server.request['done'] is False def test_request_done(server): assert server.request['done'] is False urlopen(server.get_url()).read() assert server.request['done'] is True def test_wait_request(server): server.response['data'] = b'foo' #print('.test_wait_request(): started') def worker(): time.sleep(1) urlopen(server.get_url() + '?method=test-wait-request').read() #print('.test_wait_request(): end of thread') th = Thread(target=worker) th.start() with pytest.raises(WaitTimeoutError): server.wait_request(0.5) server.wait_request(2) #res = result.get() #assert res == b'foo' th.join() #print('.test_wait_request(): last line') def test_wait_timeout_error(server): """Need many iterations to be sure""" #print('.test_wait_timeout_error(): started') with pytest.raises(WaitTimeoutError): server.wait_request(0.01) def test_request_cookies(server): req = Request(url=server.get_url()) req.add_header('Cookie', 'foo=bar') urlopen(req) assert server.request['cookies']['foo']['value'] == 'bar' def test_response_once_cookies(server): server.response['cookies'] = [('foo', 'bar')] info = urlopen(server.get_url()) assert 'foo=bar' in info.headers['Set-Cookie'] server.response_once['cookies'] = [('baz', 'gaz')] info = urlopen(server.get_url()) assert 'foo=bar' not in info.headers['Set-Cookie'] assert 'baz=gaz' in info.headers['Set-Cookie'] info = urlopen(server.get_url()) assert 'foo=bar' in info.headers['Set-Cookie'] assert 'baz=gaz' not in info.headers['Set-Cookie'] def test_default_header_content_type(server): info = urlopen(server.get_url()) assert info.headers['content-type'] == 'text/html; charset=utf-8' # FIXME: FIX IT # FAILS WITH # > assert server.request['args']['who'] == u'конь' # E assert 'êîíü' == 'конь' # E - êîíü # E + конь #def test_non_utf_request_charset(server): # server.request['charset'] = 'cp1251' # server.response['data'] = 'abc' # req = Request(url=server.get_url() + u'?who=конь'.encode('cp1251')) # assert urlopen(req).read() == b'abc' # assert server.request['args']['who'] == u'конь' def test_custom_header_content_type(server): server.response['headers'] = ( ('Content-Type', 'text/html; charset=koi8-r'), ) info = urlopen(server.get_url()) assert info.headers['content-type'] == 'text/html; charset=koi8-r' def test_default_header_server(server): info = urlopen(server.get_url()) assert (info.headers['server'] == ('TestServer/%s' % test_server.__version__)) def test_custom_header_server(server): server.response['headers'] = ( ('Server', 'Google'), ) info = urlopen(server.get_url()) assert info.headers['server'] == 'Google' def test_options_method(server): server.response['data'] = b'abc' class RequestWithMethod(Request): def __init__(self, method, *args, **kwargs): self._method = method Request.__init__(self, *args, **kwargs) def get_method(self): return self._method req = RequestWithMethod(url=server.get_url(), method='OPTIONS') info = urlopen(req) assert server.request['method'] == 'OPTIONS' assert info.read() == b'abc' def test_multiple_start_stop_cycles(): for _ in range(30): server2 = TestServer() server2.start() try: server2.response['data'] = b'zorro' for _ in range(10): data = urlopen(server2.get_url()).read() assert data == b'zorro' finally: server2.stop() @pytest.mark.skip_engine('thread') def test_temp_files_are_removed(): server2 = TestServer(engine='subprocess') server2.start() files = [ server2.request_file, server2.response_file, server2.response_once_file, server2.request_lock_file, server2.response_lock_file, server2.response_once_lock_file, ] server2.stop() assert all(not os.path.exists(x) for x in files) @pytest.mark.skip_engine('subprocess') def test_data_generator(server): def data(): yield b'foo' yield b'bar' server.response['data'] = data() data1 = urlopen(server.get_url()).read() assert data1 == b'foo' data2 = urlopen(server.get_url()).read() assert data2 == b'bar' with pytest.raises(URLError) as ex: urlopen(server.get_url()) assert ex.value.code == 405 assert ex.value.read() == b'data generator has no more data' def test_specific_port(): server = TestServer(address='localhost', port=9876) server.start() server.response['data'] = b'abc' data = urlopen(server.get_url()).read() assert data == b'abc' test-server-0.0.27/test/__init__.py0000644000175000017500000000000013035055750017746 0ustar lorienlorien00000000000000test-server-0.0.27/CHANGELOG.md0000644000175000017500000000425413061165754016513 0ustar lorienlorien00000000000000# Change Log of test-server Library ## [0.0.28] - Unreleased ### Changed ## [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.27/README.rst0000644000175000017500000000300713054324100016344 0ustar lorienlorien00000000000000=========== Test-server =========== .. image:: https://travis-ci.org/lorien/test_server.png?branch=master :target: https://travis-ci.org/lorien/test_server .. image:: https://ci.appveyor.com/api/projects/status/o3qhdh1gprcu1x1x :target: https://ci.appveyor.com/project/lorien/test-server .. image:: https://coveralls.io/repos/lorien/test_server/badge.svg?branch=master :target: https://coveralls.io/r/lorien/test_server?branch=master .. image:: https://api.codacy.com/project/badge/Grade/3ff9f3ebf06d4b7f8809b264837eac43 :target: https://www.codacy.com/app/lorien/test_server?utm_source=github.com&utm_medium=referral&utm_content=lorien/test_server&utm_campaign=badger HTTP Server to test HTTP clients. Installation ============ .. code:: bash pip install test-server Usage Example ============= Example: .. code:: python from unittest import TestCase try: from urllib import urlopen except ImportError: from urllib.request import urlopen from test_server import TestServer 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): token = b'zorro' self.server.response['data'] = token data = urlopen(self.server.base_url).read() self.assertEqual(data, token) test-server-0.0.27/test_server/0000755000175000017500000000000013061166440017233 5ustar lorienlorien00000000000000test-server-0.0.27/test_server/server.py0000644000175000017500000005765313061165754021142 0ustar lorienlorien00000000000000from threading import Thread import time import collections import logging import types import os import tempfile import json from subprocess import Popen import signal import atexit from socket import AF_INET from six.moves.urllib.parse import urljoin import six from six.moves.urllib.request import urlopen from filelock import FileLock import psutil import tornado.web from tornado.locks import Semaphore import tornado.gen from tornado.httpserver import HTTPServer from tornado.httputil import HTTPHeaders from tornado.ioloop import IOLoop from tornado.netutil import bind_sockets from test_server.error import TestServerError from test_server.container import CallbackDict __all__ = ('TestServer', 'WaitTimeoutError') logger = logging.getLogger('test_server.server') # pylint: disable=invalid-name class WaitTimeoutError(Exception): pass def kill_process(pid): try: proc = psutil.Process(pid) except psutil.NoSuchProcess: pass else: os.kill(pid, signal.SIGINT) try: proc.wait(timeout=1) except psutil.TimeoutExpired: os.kill(pid, signal.SIGTERM) try: proc.wait(timeout=2) except psutil.TimeoutExpired: raise WaitTimeoutError('Could not kill subprocess running' ' test_server') def bytes_to_unicode(obj, charset): if isinstance(obj, six.text_type): return obj elif isinstance(obj, six.binary_type): return obj.decode(charset) elif isinstance(obj, list): return [bytes_to_unicode(x, charset) for x in obj] elif isinstance(obj, tuple): return tuple(bytes_to_unicode(x, charset) for x in obj) elif isinstance(obj, dict): return dict(bytes_to_unicode(x, charset) for x in obj.items()) else: return obj def prepare_loaded_state(state_key, state, charset, fix_headers=True): """ Fix state loaded from JSON-serialized data: * all values of data keys have to be converted to strings * headers should be converted to tornado.httputil.HTTPHeaders """ if 'data' in state: if state['data'] is not None: state['data'] = state['data'].encode(charset) for key in list(state.keys()): if key.endswith('.data'): if state[key] is not None: state[key] = state[key].encode(charset) if state_key == 'request': if fix_headers: hdr = HTTPHeaders() if state['headers']: for key, val in state['headers']: hdr.add(key, val) state['headers'] = hdr return state class TestServerRequestHandler(tornado.web.RequestHandler): # pylint: disable=abstract-method,protected-access def initialize(self, test_server): # pylint: disable=arguments-differ self._server = test_server def get_param(self, key, method='get', clear_once=True): method_key = '%s.%s' % (method, key) if method_key in self._server.response_once: value = self._server.response_once[method_key] if clear_once: del self._server.response_once[method_key] self._server.save_state(['response_once']) return value elif key in self._server.response_once: value = self._server.response_once[key] if clear_once: del self._server.response_once[key] self._server.save_state(['response_once']) return value elif method_key in self._server.response: return self._server.response[method_key] elif key in self._server.response: return self._server.response[key] else: raise TestServerError('Parameter %s does not exists in ' 'server response data' % key) def decode_argument(self, value, **kwargs): # pylint: disable=unused-argument return value.decode(self._server.request['charset']) @tornado.web.asynchronous @tornado.gen.engine def request_handler(self): from test_server import __version__ # load request state # required to track request['charset'] if it is set self._server.request.read_callback() # FIXME # load response state?? could be same issue as with # request['charset'] with (yield self._server._locks['request_handler'].acquire()): # Remove some standard tornado headers for key in ('Content-Type', 'Server'): if key in self._headers: del self._headers[key] method = self.request.method.lower() sleep = self.get_param('sleep', method) if sleep: yield tornado.gen.Task(self._server.ioloop.add_timeout, time.time() + sleep) self._server.request['client_ip'] = self.request.remote_ip self._server.request['args'] = {} for key in self.request.arguments.keys(): self._server.request['args'][key] = self.get_argument(key) if self._server._engine == 'subprocess': self._server.request['headers'] = ( list(self.request.headers.get_all()) ) else: self._server.request['headers'] = self.request.headers self._server.request['path'] = self.request.path self._server.request['method'] = self.request.method cookies = {} for key, cookie in self.request.cookies.items(): cookies[key] = dict(cookie) cookies[key]['name'] = cookie.key cookies[key]['value'] = cookie.value self._server.request['cookies'] = cookies self._server.request['data'] = self.request.body self._server.request['files'] = self.request.files callback = self.get_param('callback', method) if callback: call = callback(self) if isinstance(call, types.GeneratorType): for item in call: if isinstance(item, dict): assert 'type' in item assert item['type'] in ('sleep',) if item['type'] == 'sleep': yield tornado.gen.Task( self._server.ioloop.add_timeout, time.time() + item['time'], ) else: yield item else: response = { 'code': None, 'headers': [], 'data': None, } response['code'] = self.get_param('code', method) for key, val in self.get_param('cookies', method): # Set-Cookie: name=newvalue; expires=date; # path=/; domain=.example.org. response['headers'].append( ('Set-Cookie', '%s=%s' % (key, val))) for key, value in self.get_param('headers', method): response['headers'].append((key, value)) response['headers'].append( ('Listen-Port', str(self._server.port))) data = self.get_param('data', method) if isinstance(data, six.string_types): response['data'] = data elif isinstance(data, six.binary_type): response['data'] = data elif isinstance(data, collections.Iterable): try: response['data'] = next(data) except StopIteration: response['code'] = 405 response['data'] = b'data generator has no more data' else: raise TestServerError('Data parameter should ' 'be string or iterable ' 'object') header_keys = [x[0].lower() for x in response['headers']] if 'content-type' not in header_keys: response['headers'].append( ('Content-Type', 'text/html; charset=%s' % self._server.response['charset']) ) if 'server' not in header_keys: response['headers'].append( ('Server', 'TestServer/%s' % __version__)) self.set_status(response['code']) for key, val in response['headers']: self.add_header(key, val) self.write(response['data']) self._server.request['done'] = True if not callback: self.finish() get = post = put = patch = delete = options = request_handler # pylint: disable=abstract-method class StateCallbackDict(CallbackDict): def __init__(self, server, state): self.server = server self.state = state super(StateCallbackDict, self).__init__() def write_callback(self): self.server.save_state([self.state]) def read_callback(self): self.server.load_state([self.state]) # pylint: enable=abstract-method class TestServer(object): def __init__(self, port=0, address='127.0.0.1', engine='thread', role='master', **kwargs): assert engine in ('thread', 'subprocess') self.request = StateCallbackDict(self, 'request') self.response = StateCallbackDict(self, 'response') self.response_once = StateCallbackDict(self, 'response_once') self.port = port self.address = address self._handler = None self._thread = None # thread instance if thread engine self._proc = None # Process instance if subprocess engine self._engine = engine self._role = role self.request_file = None self.response_file = None self.response_once_file = None self.config_file = None self.request_lock_file = None self.response_lock_file = None self.response_once_lock_file = None if (role == 'master' and engine == 'thread') or role == 'server': self.ioloop = IOLoop() self.ioloop.make_current() # Restrict any activity untill the reset method # will setup initial content of request/respone files if role == 'master' and engine == 'subprocess': hdl, self.request_file = tempfile.mkstemp() os.close(hdl) hdl, self.response_file = tempfile.mkstemp() os.close(hdl) hdl, self.response_once_file = tempfile.mkstemp() os.close(hdl) hdl, self.config_file = tempfile.mkstemp() os.close(hdl) #print('Request file: %s' % self.request_file) #print('Response file: %s' % self.response_file) #print('Response_once file: %s' # % self.response_once_file) #print('config file: %s' % self.config_file) if role == 'server' and engine == 'subprocess': self.request_file = kwargs['request_file'] self.response_file = kwargs['response_file'] self.response_once_file = kwargs['response_once_file'] self.config_file = kwargs['config_file'] if engine == 'subprocess': self.request_lock_file = self.request_file + '.lock' self.response_lock_file = self.response_file + '.lock' self.response_once_lock_file = self.response_once_file + '.lock' self.config_lock_file = self.config_file + '.lock' self._locks = { 'request_handler': Semaphore(), 'request_file': (FileLock(self.request_lock_file) if self.request_file else None), 'response_file': (FileLock(self.response_lock_file) if self.response_file else None), 'response_once_file': (FileLock(self.response_once_lock_file) if self.response_once_file else None), 'config_file': (FileLock(self.config_lock_file) if self.config_file else None), } self.config = StateCallbackDict(self, 'config') self.config.update({ 'port': self.port, }) self.reset() def save_state(self, keys=None): if self._engine == 'subprocess': if keys is None: keys = ('request', 'response', 'response_once') for key in keys: attr = '%s_file' % key with self._locks[attr].acquire(timeout=-1): obj = getattr(self, key) with obj.disable_callbacks(): state = obj.get_dict() if key == 'request': charset_state = 'request' else: charset_state = 'response' charset_obj = getattr(self, charset_state) with charset_obj.disable_callbacks(): try: charset = state['charset'] except KeyError: try: charset = charset_obj['charset'] except KeyError: charset = 'utf-8' #if key == 'request': # print('----- save begins -------------------------') # print('STATE', state) # print('CHARSET', charset) # traceback.print_stack() # print('----- save ends -------------------------') with obj.disable_callbacks(): state_prep = bytes_to_unicode(state, charset) with open(getattr(self, attr), 'w') as out: json.dump(state_prep, out) def load_state(self, keys=None): if self._engine == 'subprocess': if keys is None: keys = ('request', 'response', 'response_once') for key in keys: attr = '%s_file' % key with self._locks[attr].acquire(timeout=-1): with open(getattr(self, attr)) as inp: raw_content = inp.read() fix_headers = (self._role == 'master') state = json.loads(raw_content) if key == 'request': charset_state = 'request' else: charset_state = 'response' charset_obj = getattr(self, charset_state) with charset_obj.disable_callbacks(): try: charset = state['charset'] except KeyError: try: charset = charset_obj['charset'] except KeyError: charset = 'utf-8' state_prep = prepare_loaded_state(key, state, charset, fix_headers=fix_headers) #if key == 'request': # print('----- load begins -------------------------') # print('STATE', state_prep) # print('CHARSET', charset) # traceback.print_stack() # print('----- load ends -------------------------') obj = getattr(self, key) with obj.disable_callbacks(): obj.clear() obj.update(state_prep) def reset(self): self.request.clear() self.request.update({ 'args': {}, 'headers': {}, 'cookies': None, 'path': None, 'method': None, 'data': None, 'files': {}, 'client_ip': None, 'done': False, 'charset': 'utf-8', }) #print('.reset(): just reset the request!') #print('.reset(): content of request: %s' % self.request.get_dict()) #print('.reset(): content of request file: %s' # % open(self.request_file).read()) self.response.clear() self.response.update({ 'code': 200, 'data': '', 'headers': [], 'cookies': [], 'callback': None, 'sleep': None, 'charset': 'utf-8', }) self.response_once.clear() def _build_web_app(self): """Build tornado web application that is served by HTTP server""" return tornado.web.Application([ (r"^.*", TestServerRequestHandler, {'test_server': self}), ]) def main_loop_function(self, keep_alive=False): """ Ask HTTP server start processing requests. This is function that is executed in separate thread: * start HTTP server * start tornado loop """ self.ioloop.make_current() if self.port == 0: socket = bind_sockets(0, self.address, family=AF_INET)[0] self.port = int(socket.getsockname()[1]) self.config['port'] = self.port else: socket = bind_sockets(self.port, self.address, family=AF_INET)[0] app = self._build_web_app() server = HTTPServer(app, no_keep_alive=not keep_alive) try_limit = 10 try_pause = 1 / float(try_limit) for count in range(try_limit): try: server.add_sockets([socket]) except OSError: if count == (try_limit - 1): raise else: logging.debug('Socket %s:%d is busy, ' 'waiting %.2f seconds.', self.address, self.port, try_pause) time.sleep(0.1) else: break logger.debug('Listening on port %d', self.port) try: self.ioloop.start() finally: # manually close sockets to be able to create # other HTTP servers on same sockets server.stop() def start(self, keep_alive=False, daemon=True): """Start the HTTP server.""" if self._engine == 'thread' or self._role == 'server': self._thread = Thread(target=self.main_loop_function, args=[keep_alive]) self._thread.daemon = daemon self._thread.start() elif self._engine == 'subprocess' and self._role == 'master': self._proc = Popen([ 'test_server', '%s:%d' % (self.address, self.port), '--req', self.request_file, '--resp', self.response_file, '--resp-once', self.response_once_file, '--config', self.config_file, ]) def kill_child(): try: os.kill(self._proc.pid, signal.SIGINT) except OSError: pass atexit.register(kill_child) atexit.register(self.remove_temp_files) else: raise Exception('Should not be raised ever') if self._role == 'master': if self._engine == 'subprocess': config_loaded = False try_limit = 50 try_pause = 1 / float(try_limit) for count in range(try_limit): try: self.config['port'] except ValueError: time.sleep(try_pause) else: if self.config['port'] != 0: config_loaded = True break else: time.sleep(try_pause) if not config_loaded: raise TestServerError( 'Could not load from master process the config file' ' saved by server process' ) self.port = self.config['port'] try_limit = 10 try_pause = 1 / float(try_limit) for count in range(try_limit): try: urlopen(self.get_url() + '?method=start').read() except Exception: # pylint: disable=broad-except if count == (try_limit - 1): raise else: time.sleep(try_pause) else: break self.reset() def stop(self): """Stop tornado loop and wait for thread finished it work.""" if ((self._role == 'master' and self._engine == 'thread') or self._role == 'server'): self.ioloop.stop() self._thread.join() if self._role == 'master' and self._engine == 'subprocess': kill_process(self._proc.pid) self.remove_temp_files() def remove_temp_files(self): files = ( self.request_file, self.response_file, self.response_once_file, self.config_file, self.request_lock_file, self.response_lock_file, self.response_once_lock_file, self.config_lock_file, ) for file_ in files: try: os.unlink(file_) except OSError: pass def get_url(self, path='', port=None): """Build URL that is served by HTTP server.""" if port is None: port = self.port return urljoin('http://%s:%d/' % (self.address, port), path) def wait_request(self, timeout): """Stupid implementation that eats CPU.""" start = time.time() while True: #req = self.request.get_dict() if self.request['done']: #if req['done']: #print('wait_request [timeout=%s]: req is done: %s' # % (timeout, req)) break time.sleep(0.01) if time.time() - start > timeout: raise WaitTimeoutError('No request processed in %d seconds' % timeout) def script_test_server(): try: from argparse import ArgumentParser import sys parser = ArgumentParser() parser.add_argument('address') parser.add_argument('--req') parser.add_argument('--resp') parser.add_argument('--resp-once') parser.add_argument('--config') opts = parser.parse_args() if opts.req is None: sys.stderr.write('Option --req is not specified\n') sys.exit(1) if opts.resp is None: sys.stderr.write('Option --resp is not specified\n') sys.exit(1) if opts.resp_once is None: sys.stderr.write('Option --resp-once is not specified\n') sys.exit(1) if opts.config is None: sys.stderr.write('Option --config is not specified\n') sys.exit(1) host, port = opts.address.split(':') port = int(port) server = TestServer(address=host, port=port, request_file=opts.req, response_file=opts.resp, response_once_file=opts.resp_once, config_file=opts.config, engine='subprocess', role='server') server.start() server.reset() while True: time.sleep(1) except KeyboardInterrupt: # Do not throw exception to console becuase # it could came as standard shutdown signal # from master process sys.exit(1) test-server-0.0.27/test_server/container.py0000644000175000017500000000331013061165754021573 0ustar lorienlorien00000000000000from contextlib import contextmanager from copy import deepcopy class CallbackDict(object): """ Dict-like class calling callbacks on data read/write. When data is reading the `.read_callback()` is called. When data is writing the `.write_callback()` is called. """ def __init__(self, data=None): if data is None: self._reg = {} else: self._reg = deepcopy(data) self.callbacks_enabled = True def write_callback(self): pass def read_callback(self): pass def __getitem__(self, key): if self.callbacks_enabled: self.read_callback() return self._reg[key] def __setitem__(self, key, val): self._reg[key] = val if self.callbacks_enabled: self.write_callback() def __delitem__(self, key): del self._reg[key] if self.callbacks_enabled: self.write_callback() def update(self, data): self._reg.update(data) if self.callbacks_enabled: self.write_callback() def clear(self): self._reg.clear() if self.callbacks_enabled: self.write_callback() def _not_implemented_mock(self, *args, **kwargs): raise NotImplementedError get = set = keys = items = _not_implemented_mock @contextmanager def disable_callbacks(self): self.callbacks_enabled = False yield self.callbacks_enabled = True def get_dict(self): if self.callbacks_enabled: self.read_callback() return self._reg def __contains__(self, key): if self.callbacks_enabled: self.read_callback() return key in self._reg test-server-0.0.27/test_server/__init__.py0000644000175000017500000000025113061165754021351 0ustar lorienlorien00000000000000from test_server.server import * # noqa pylint: disable=wildcard-import from test_server.error import * # noqa pylint: disable=wildcard-import __version__ = '0.0.27' test-server-0.0.27/test_server/error.py0000644000175000017500000000011413061165754020741 0ustar lorienlorien00000000000000__all__ = ('TestServerError',) class TestServerError(Exception): pass test-server-0.0.27/requirements_dev.txt0000644000175000017500000000011213055011463020777 0ustar lorienlorien00000000000000coverage coveralls flake8 bumpversion six pytest pytest-cov pylint sphinx test-server-0.0.27/PKG-INFO0000644000175000017500000000557713061166440016001 0ustar lorienlorien00000000000000Metadata-Version: 1.1 Name: test-server Version: 0.0.27 Summary: Server to test HTTP clients Home-page: https://github.com/lorien/test_server Author: Gregory Petukhov Author-email: lorien@lorien.name License: MIT License Download-URL: https://pypi.python.org/pypi/test-server Description: =========== Test-server =========== .. image:: https://travis-ci.org/lorien/test_server.png?branch=master :target: https://travis-ci.org/lorien/test_server .. image:: https://ci.appveyor.com/api/projects/status/o3qhdh1gprcu1x1x :target: https://ci.appveyor.com/project/lorien/test-server .. image:: https://coveralls.io/repos/lorien/test_server/badge.svg?branch=master :target: https://coveralls.io/r/lorien/test_server?branch=master .. image:: https://api.codacy.com/project/badge/Grade/3ff9f3ebf06d4b7f8809b264837eac43 :target: https://www.codacy.com/app/lorien/test_server?utm_source=github.com&utm_medium=referral&utm_content=lorien/test_server&utm_campaign=badger HTTP Server to test HTTP clients. Installation ============ .. code:: bash pip install test-server Usage Example ============= Example: .. code:: python from unittest import TestCase try: from urllib import urlopen except ImportError: from urllib.request import urlopen from test_server import TestServer 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): token = b'zorro' self.server.response['data'] = token data = urlopen(self.server.base_url).read() self.assertEqual(data, token) Keywords: test testing server http-server Platform: UNKNOWN Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: License :: OSI Approved :: MIT License Classifier: Topic :: Software Development :: Libraries :: Application Frameworks Classifier: Topic :: Software Development :: Libraries :: Python Modules test-server-0.0.27/pylintrc0000644000175000017500000000063013061165754016463 0ustar lorienlorien00000000000000[MASTER] jobs=4 extension-pkg-whitelist=pytest [TYPECHEK] ignored-modules=six.moves.urllib [MESSAGES CONTROL] disable=R,I,fixme,missing-docstring [REPORTS] reports=no [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}$ [FORMAT] max-line-length=79