molotov-1.6/0000755000076500000240000000000013261446760013321 5ustar tarekstaff00000000000000molotov-1.6/PKG-INFO0000644000076500000240000000701213261446760014416 0ustar tarekstaff00000000000000Metadata-Version: 1.1 Name: molotov Version: 1.6 Summary: Spiffy load testing tool. Home-page: https://molotov.readthedocs.io Author: Tarek Ziade Author-email: tarek@ziade.org License: UNKNOWN Description: ======= molotov ======= .. image:: http://coveralls.io/repos/github/loads/molotov/badge.svg?branch=master :target: https://coveralls.io/github/loads/molotov?branch=master .. image:: http://travis-ci.org/loads/molotov.svg?branch=master :target: https://travis-ci.org/loads/molotov .. image:: http://readthedocs.org/projects/molotov/badge/?version=latest :target: https://molotov.readthedocs.io .. image:: https://img.shields.io/pypi/pyversions/molotov.svg :target: https://molotov.readthedocs.io Simple Python 3.5+ tool to write load tests. Based on `asyncio `_, Built with `aiohttp `_ 2.x or 3.x `Full Documentation `_ CHANGELOG ========= 1.6 - 2018-04-05 ---------------- - works with aiohttp 2.x or 3.x so Python 3.5.1 can be used (#114) 1.5 - 2018-04-03 ---------------- - now runs on aiohttp 3.x (#109) - make sure we run a proper Python version (#9) - each process needs to have its own statsd client (#98) - fixed _run_in_fresh_loop and setup_session() error handling (#100) - Adde --fail (#105) - Added --force-shutdown (#107) - Make internet-based tests optional (#104) 1.4 - 2017-09-26 ---------------- - statsd: moved from aiostatsd to aiomeasures - Added --sizing and --sizing-tolerance (#72) - Refactored shared counters - Implemented a shared console (#42) - Improved shutdown process (#67) - Refactored fmwk.py (#25) - Add a way to record requests and responses (#80) - added --use-extension - added events - published tests/examples*.py to the docs (#90) 1.3 - 2017-07-28 ---------------- - fixed file-based requests with sessions -vvv option (#73) - proper managment of the verbose option in moloslave - added uvloop support (#68) - added initial PyPy support (#47) - Added name & @scenario_picker() options (#65) 1.2 - 2017-06-15 ---------------- - improved docs - added delay options (#48) - added --ramp-up option (#61) - fix a bug on response display (#62) 1.1 - 2017-06-09 ---------------- - added request and json_request helpers (#50) - added session setup and teardown fixtures (#52) - added set_var & get_var helpers (#54) - fixed thhe code generated by molostart (#55) 1.0 - 2017-03-23 ---------------- - Initial stable release Platform: UNKNOWN Classifier: Programming Language :: Python Classifier: License :: OSI Approved :: Apache Software License Classifier: Development Status :: 5 - Production/Stable Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 molotov-1.6/LICENSE0000644000076500000240000002613613036435530014327 0ustar tarekstaff00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "{}" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright {yyyy} {name of copyright owner} Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. molotov-1.6/requirements.txt0000644000076500000240000000004313261372407016576 0ustar tarekstaff00000000000000aiohttp==3.1.1 aiomeasures==0.5.14 molotov-1.6/Makefile0000644000076500000240000000065013260626031014750 0ustar tarekstaff00000000000000HERE = $(shell pwd) BIN = $(HERE)/bin PYTHON = $(BIN)/python INSTALL = $(BIN)/pip install --no-deps BUILD_DIRS = bin build include lib lib64 man share VIRTUALENV = virtualenv .PHONY: all test build clean docs all: build $(PYTHON): $(VIRTUALENV) $(VTENV_OPTS) . build: $(PYTHON) $(PYTHON) setup.py develop $(BIN)/pip install tox clean: rm -rf $(BUILD_DIRS) test: build $(BIN)/tox docs: build $(BIN)/tox -e docs molotov-1.6/tox-requirements.txt0000644000076500000240000000004413142023776017407 0ustar tarekstaff00000000000000-r tox-pypy-requirements.txt uvloop molotov-1.6/molotov.egg-info/0000755000076500000240000000000013261446760016512 5ustar tarekstaff00000000000000molotov-1.6/molotov.egg-info/PKG-INFO0000644000076500000240000000701213261446760017607 0ustar tarekstaff00000000000000Metadata-Version: 1.1 Name: molotov Version: 1.6 Summary: Spiffy load testing tool. Home-page: https://molotov.readthedocs.io Author: Tarek Ziade Author-email: tarek@ziade.org License: UNKNOWN Description: ======= molotov ======= .. image:: http://coveralls.io/repos/github/loads/molotov/badge.svg?branch=master :target: https://coveralls.io/github/loads/molotov?branch=master .. image:: http://travis-ci.org/loads/molotov.svg?branch=master :target: https://travis-ci.org/loads/molotov .. image:: http://readthedocs.org/projects/molotov/badge/?version=latest :target: https://molotov.readthedocs.io .. image:: https://img.shields.io/pypi/pyversions/molotov.svg :target: https://molotov.readthedocs.io Simple Python 3.5+ tool to write load tests. Based on `asyncio `_, Built with `aiohttp `_ 2.x or 3.x `Full Documentation `_ CHANGELOG ========= 1.6 - 2018-04-05 ---------------- - works with aiohttp 2.x or 3.x so Python 3.5.1 can be used (#114) 1.5 - 2018-04-03 ---------------- - now runs on aiohttp 3.x (#109) - make sure we run a proper Python version (#9) - each process needs to have its own statsd client (#98) - fixed _run_in_fresh_loop and setup_session() error handling (#100) - Adde --fail (#105) - Added --force-shutdown (#107) - Make internet-based tests optional (#104) 1.4 - 2017-09-26 ---------------- - statsd: moved from aiostatsd to aiomeasures - Added --sizing and --sizing-tolerance (#72) - Refactored shared counters - Implemented a shared console (#42) - Improved shutdown process (#67) - Refactored fmwk.py (#25) - Add a way to record requests and responses (#80) - added --use-extension - added events - published tests/examples*.py to the docs (#90) 1.3 - 2017-07-28 ---------------- - fixed file-based requests with sessions -vvv option (#73) - proper managment of the verbose option in moloslave - added uvloop support (#68) - added initial PyPy support (#47) - Added name & @scenario_picker() options (#65) 1.2 - 2017-06-15 ---------------- - improved docs - added delay options (#48) - added --ramp-up option (#61) - fix a bug on response display (#62) 1.1 - 2017-06-09 ---------------- - added request and json_request helpers (#50) - added session setup and teardown fixtures (#52) - added set_var & get_var helpers (#54) - fixed thhe code generated by molostart (#55) 1.0 - 2017-03-23 ---------------- - Initial stable release Platform: UNKNOWN Classifier: Programming Language :: Python Classifier: License :: OSI Approved :: Apache Software License Classifier: Development Status :: 5 - Production/Stable Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 molotov-1.6/molotov.egg-info/not-zip-safe0000644000076500000240000000000113036435775020744 0ustar tarekstaff00000000000000 molotov-1.6/molotov.egg-info/SOURCES.txt0000644000076500000240000000246313261446760020403 0ustar tarekstaff00000000000000.travis.yml CHANGELOG.rst LICENSE MANIFEST.in Makefile README.rst docs-requirements.txt molotov.json requirements.txt setup.py tox-requirements.txt tox.ini molotov/__init__.py molotov/api.py molotov/listeners.py molotov/run.py molotov/runner.py molotov/session.py molotov/sharedconsole.py molotov/sharedcounter.py molotov/slave.py molotov/stats.py molotov/util.py molotov/worker.py molotov.egg-info/PKG-INFO molotov.egg-info/SOURCES.txt molotov.egg-info/dependency_links.txt molotov.egg-info/entry_points.txt molotov.egg-info/not-zip-safe molotov.egg-info/requires.txt molotov.egg-info/top_level.txt molotov/quickstart/Makefile molotov/quickstart/__init__.py molotov/quickstart/loadtest.py molotov/quickstart/molotov.json molotov/tests/__init__.py molotov/tests/example.py molotov/tests/example2.py molotov/tests/example3.py molotov/tests/example4.py molotov/tests/example5.py molotov/tests/example6.py molotov/tests/example7.py molotov/tests/example8.py molotov/tests/molotov.json molotov/tests/statsd.py molotov/tests/support.py molotov/tests/test_api.py molotov/tests/test_fmwk.py molotov/tests/test_listeners.py molotov/tests/test_quickstart.py molotov/tests/test_run.py molotov/tests/test_session.py molotov/tests/test_sharedconsole.py molotov/tests/test_sharedcounter.py molotov/tests/test_slave.py molotov/tests/test_util.pymolotov-1.6/molotov.egg-info/entry_points.txt0000644000076500000240000000021713261446760022010 0ustar tarekstaff00000000000000 [console_scripts] molotov = molotov.run:main moloslave = molotov.slave:main molostart = molotov.quickstart:main molotov-1.6/molotov.egg-info/requires.txt0000644000076500000240000000002413261446760021106 0ustar tarekstaff00000000000000aiohttp aiomeasures molotov-1.6/molotov.egg-info/top_level.txt0000644000076500000240000000001013261446760021233 0ustar tarekstaff00000000000000molotov molotov-1.6/molotov.egg-info/dependency_links.txt0000644000076500000240000000000113261446760022560 0ustar tarekstaff00000000000000 molotov-1.6/MANIFEST.in0000644000076500000240000000053213064432740015051 0ustar tarekstaff00000000000000include CHANGELOG.rst include README.rst include LICENSE include CHANGES.rst include requirements.txt include tox-requirements.txt include docs-requirements.txt include tox.ini include .travis.yml include molotov.json include Makefile include molotov/tests/molotov.json include molotov/quickstart/Makefile include molotov/quickstart/molotov.json molotov-1.6/molotov/0000755000076500000240000000000013261446760015020 5ustar tarekstaff00000000000000molotov-1.6/molotov/worker.py0000644000076500000240000001541713256670024016707 0ustar tarekstaff00000000000000import asyncio import time from molotov.listeners import EventSender from molotov.session import LoggedClientSession as Session from molotov.api import get_fixture, pick_scenario, get_scenario from molotov.util import (cancellable_sleep, is_stopped, set_timer, get_timer, stop) class FixtureError(Exception): pass def _now(): return int(time.time()) class Worker(object): """"The Worker class creates a Session and runs scenario. """ def __init__(self, wid, results, console, args, statsd=None, delay=0, loop=None): self.wid = wid self.results = results self.console = console self.loop = loop or asyncio.get_event_loop() self.args = args self.statsd = statsd self.delay = delay self.count = 0 self.worker_start = 0 self.eventer = EventSender(console) # fixtures self._session_setup = get_fixture('setup_session') self._session_teardown = get_fixture('teardown_session') self._setup = get_fixture('setup') self._teardown = get_fixture('teardown') async def send_event(self, event, **options): await self.eventer.send_event(event, wid=self.wid, **options) async def run(self): if self.delay > 0.: await cancellable_sleep(self.delay) if is_stopped(): return self.results['WORKER'] += 1 try: res = await self._run() finally: self.teardown() self.results['WORKER'] -= 1 return res def _may_run(self): if is_stopped(): return False if _now() - self.worker_start > self.args.duration: return False if self.results['REACHED'] == 1: return False if self.args.max_runs and self.count > self.args.max_runs: return False return True async def setup(self): if self._setup is None: return {} try: options = await self._setup(self.wid, self.args) except Exception as e: self.console.print_error(e) raise FixtureError(str(e)) if options is None: options = {} elif not isinstance(options, dict): msg = 'The setup function needs to return a dict' self.console.print(msg) raise FixtureError(msg) return options async def session_setup(self, session): if self._session_setup is None: return try: await self._session_setup(self.wid, session) except Exception as e: self.console.print_error(e) raise FixtureError(str(e)) async def session_teardown(self, session): if self._session_teardown is None: return try: await self._session_teardown(self.wid, session) except Exception as e: # we can't stop the teardown process self.console.print_error(e) async def _run(self): verbose = self.args.verbose exception = self.args.exception if self.args.single_mode: single = get_scenario(self.args.single_mode) else: single = None self.count = 1 self.worker_start = _now() try: options = await self.setup() except FixtureError: stop() return async with Session(self.loop, self.console, verbose, self.statsd, **options) as session: session.args = self.args session.worker_id = self.wid try: await self.session_setup(session) except FixtureError: stop() return while self._may_run(): step_start = _now() session.step = self.count result = await self.step(self.count, session, scenario=single) if result == 1: self.results['OK'] += 1 self.results['MINUTE_OK'] += 1 elif result == -1: self.results['FAILED'] += 1 self.results['MINUTE_FAILED'] += 1 if exception: stop() if not is_stopped() and self._reached_tolerance(step_start): stop() cancellable_sleep.cancel_all() break self.count += 1 if self.args.delay > 0.: await cancellable_sleep(self.args.delay) else: # forces a context switch await asyncio.sleep(0) await self.session_teardown(session) def teardown(self): if self._teardown is None: return try: self._teardown(self.wid) except Exception as e: # we can't stop the teardown process self.console.print_error(e) def _reached_tolerance(self, current_time): if not self.args.sizing: return False if current_time - get_timer() > 60: # we need to reset the tolerance counters set_timer(current_time) self.results['MINUTE_OK'].value = 0 self.results['MINUTE_FAILED'].value = 0 return False OK = self.results['MINUTE_OK'].value FAILED = self.results['MINUTE_FAILED'].value if OK + FAILED < 100: # we don't have enough samples return False current_ratio = float(FAILED) / float(OK) * 100. reached = current_ratio > self.args.sizing_tolerance if reached: self.results['REACHED'].value = 1 self.results['RATIO'].value = int(current_ratio * 100) return reached async def step(self, step_id, session, scenario=None): """ single scenario call. When it returns 1, it works. -1 the script failed, 0 the test is stopping or needs to stop. """ if scenario is None: scenario = pick_scenario(self.wid, step_id) try: await self.send_event('scenario_start', scenario=scenario) await scenario['func'](session, *scenario['args'], **scenario['kw']) await self.send_event('scenario_success', scenario=scenario) if scenario['delay'] > 0.: await cancellable_sleep(scenario['delay']) return 1 except Exception as exc: await self.send_event('scenario_failure', scenario=scenario, exception=exc) if self.args.verbose > 0: self.console.print_error(exc) await self.console.flush() return -1 molotov-1.6/molotov/sharedcounter.py0000644000076500000240000000520513153510133020224 0ustar tarekstaff00000000000000import multiprocessing class SharedCounter(object): """A multi-process compatible counter. """ def __init__(self, name): self._val = multiprocessing.Value('i', 0) self._name = name def __eq__(self, other): return self.__cmp__(other) == 0 def __ne__(self, other): return self.__cmp__(other) != 0 def __gt__(self, other): return self.__cmp__(other) > 0 def __ge__(self, other): return self.__cmp__(other) >= 0 def __lt__(self, other): return self.__cmp__(other) < 0 def __le__(self, other): return self.__cmp__(other) <= 0 def __cmp__(self, other): if isinstance(other, SharedCounter): other = other.value if not isinstance(other, int): raise TypeError(other) if self._val.value == other: return 0 elif self._val.value > other: return 1 return -1 def __repr__(self): return '' % self._val.value def __iadd__(self, other): self.__add__(other) return self def __isub__(self, other): self.__sub__(other) return self def __add__(self, other): with self._val.get_lock(): if isinstance(other, SharedCounter): other = other.value if not isinstance(other, int): raise NotImplementedError() self._val.value += other def __sub__(self, other): self.__add__(-other) @property def value(self): return self._val.value @value.setter def value(self, _value): with self._val.get_lock(): if isinstance(_value, SharedCounter): _value = _value.value if not isinstance(_value, int): raise TypeError(_value) self._val.value = _value class SharedCounters(object): """Mapping of SharedCounter items. """ def __init__(self, *keys): self._counters = {} for key in keys: self._counters[key] = SharedCounter(key) def items(self): return self._counters.items() def values(self): return self._counters.values() def __iter__(self): return self._counters.__iter__() def keys(self): return self._counters.keys() def __contains__(self, key): return key in self._counters def __repr__(self): return repr(self._counters) def __setitem__(self, key, value): if key not in self._counters: raise KeyError(key) self._counters[key].value = value def __getitem__(self, key): return self._counters[key] molotov-1.6/molotov/run.py0000644000076500000240000001713013256670123016174 0ustar tarekstaff00000000000000import os import sys import argparse import platform from importlib import import_module from importlib.util import spec_from_file_location, module_from_spec from molotov.runner import Runner from molotov.api import get_scenarios, get_scenario from molotov import __version__ from molotov.util import expand_options, OptionError, printable_error from molotov.sharedconsole import SharedConsole PYPY = platform.python_implementation() == 'PyPy' def _parser(): parser = argparse.ArgumentParser(description='Load test.') parser.add_argument('scenario', default="loadtest.py", help="path or module name that contains scenarii", nargs="?") parser.add_argument('-s', '--single-mode', default=None, type=str, help="Name of a single scenario to run once.") parser.add_argument('--config', default=None, type=str, help='Point to a JSON config file.') parser.add_argument('--version', action='store_true', default=False, help='Displays version and exits.') parser.add_argument('--debug', action='store_true', default=False, help='Run the event loop in debug mode.') parser.add_argument('-v', '--verbose', action='count', default=0, help=('Verbosity level. -v will display ' 'tracebacks. -vv requests and responses.')) parser.add_argument('-w', '--workers', help='Number of workers', type=int, default=1) parser.add_argument('--ramp-up', help='Ramp-up time in seconds', type=float, default=0.) parser.add_argument('--sizing', help='Autosizing', action='store_true', default=False) parser.add_argument('--sizing-tolerance', help='Sizing tolerance', type=float, default=5.) parser.add_argument('--delay', help='Delay between each worker run', type=float, default=0.) parser.add_argument('--console-update', help='Delay between each console update', type=float, default=0.2) parser.add_argument('-p', '--processes', help='Number of processes', type=int, default=1) parser.add_argument('-d', '--duration', help='Duration in seconds', type=int, default=86400) parser.add_argument('-r', '--max-runs', help='Maximum runs per worker', type=int, default=None) parser.add_argument('-q', '--quiet', action='store_true', default=False, help='Quiet') parser.add_argument('-x', '--exception', action='store_true', default=False, help='Stop on first failure.') parser.add_argument('-f', '--fail', type=int, default=None, help='Number of failures required to fail') parser.add_argument('-c', '--console', action='store_true', default=True, help='Use simple console for feedback') parser.add_argument('--statsd', help='Activates statsd', action='store_true', default=False) parser.add_argument('--statsd-address', help='Statsd Address', type=str, default="udp://127.0.0.1:8125") parser.add_argument('--uvloop', help='Use uvloop', default=False, action='store_true') parser.add_argument('--use-extension', help='Imports a module containing Molotov extensions', default=None, type=str, nargs='+') parser.add_argument('--force-shutdown', help='Cancel all pending workers on shutdown', default=False, action='store_true') return parser def main(args=None): if args is None: parser = _parser() args = parser.parse_args() if args.version: print(__version__) sys.exit(0) if args.config: if args.scenario == 'loadtest.py': args.scenario = 'test' try: expand_options(args.config, args.scenario, args) except OptionError as e: print(str(e)) sys.exit(0) if args.uvloop: if PYPY: print("You can't use uvloop with PyPy") # pragma: no cover sys.exit(0) # pragma: no cover try: import uvloop except ImportError: print('You need to install uvloop when using --uvloop') sys.exit(0) import asyncio asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) if args.sizing: # sizing is just ramping up workers indefinitely until # something things break. If the user has not set the values, # we do it here with 5 minutes and 500 workers if args.ramp_up == 0.: args.ramp_up = 300 if args.workers == 1: args.workers = 500 return run(args) _SIZING = """\ Sizing is over! Error Ratio %(RATIO).2f %% obtained with %(WORKER)d workers. OVERALL: SUCCESSES: %(OK)d | FAILURES: %(FAILED)d LAST MINUTE: SUCCESSES: %(MINUTE_OK)d | FAILURES: %(MINUTE_FAILED)d """ HELLO = '**** Molotov v%s. Happy breaking! ****' % __version__ def run(args): args.shared_console = SharedConsole(interval=args.console_update) if not args.quiet: print(HELLO) if args.use_extension: for extension in args.use_extension: if not args.quiet: print("Loading extension %r" % extension) if os.path.exists(extension): spec = spec_from_file_location("extension", extension) module = module_from_spec(spec) spec.loader.exec_module(module) else: try: import_module(extension) except (ImportError, ValueError) as e: print('Cannot import %r' % extension) print('\n'.join(printable_error(e))) sys.exit(1) if os.path.exists(args.scenario): spec = spec_from_file_location("loadtest", args.scenario) module = module_from_spec(spec) spec.loader.exec_module(module) else: try: import_module(args.scenario) except (ImportError, ValueError) as e: print('Cannot import %r' % args.scenario) print('\n'.join(printable_error(e))) sys.exit(1) if len(get_scenarios()) == 0: print('You need at least one scenario. No scenario was found.') print('A scenario with a weight of 0 is ignored') sys.exit(1) if args.verbose > 0 and args.quiet: print("You can't use -q and -v at the same time") sys.exit(1) if args.single_mode: if get_scenario(args.single_mode) is None: print("Can't find %r in registered scenarii" % args.single_mode) sys.exit(1) res = Runner(args)() def _dict(counters): res = {} for k, v in counters.items(): if k == 'RATIO': res[k] = float(v.value) / 100. else: res[k] = v.value return res res = _dict(res) if not args.quiet: if args.sizing: if res['REACHED'] == 1: print(_SIZING % res) else: print('Sizing was not finished. (interrupted)') else: print('SUCCESSES: %(OK)d | FAILURES: %(FAILED)d\r' % res) print('*** Bye ***') if args.fail is not None and res['FAILED'] >= args.fail: sys.exit(1) molotov-1.6/molotov/runner.py0000644000076500000240000001731513256136531016706 0ustar tarekstaff00000000000000from contextlib import suppress import signal import multiprocessing import asyncio import os from molotov.api import get_fixture from molotov.listeners import EventSender from molotov.stats import get_statsd_client from molotov.sharedcounter import SharedCounters from molotov.util import cancellable_sleep, stop, is_stopped, set_timer from molotov.worker import Worker class Runner(object): """Manages processes & workers and grabs results. """ def __init__(self, args, loop=None): self.args = args self.console = self.args.shared_console if loop is None: loop = asyncio.get_event_loop() self.loop = loop # the stastd client gets initialized after we fork # processes in case -p was used self.statsd = None self._tasks = [] self._procs = [] self._results = SharedCounters('WORKER', 'REACHED', 'RATIO', 'OK', 'FAILED', 'MINUTE_OK', 'MINUTE_FAILED') self.eventer = EventSender(self.console) def _set_statsd(self): if self.args.statsd: self.statsd = get_statsd_client(self.args.statsd_address, loop=self.loop) else: self.statsd = None def gather(self, *futures): return asyncio.gather(*futures, loop=self.loop, return_exceptions=True) def ensure_future(self, coro): return asyncio.ensure_future(coro, loop=self.loop) def __call__(self): global_setup = get_fixture('global_setup') if global_setup is not None: try: global_setup(self.args) except Exception as e: self.console.print("The global_setup() fixture failed") self.console.print_error(e) raise try: return self._launch_processes() finally: global_teardown = get_fixture('global_teardown') if global_teardown is not None: try: global_teardown() except Exception as e: # we can't stop the teardown process self.console.print_error(e) def _launch_processes(self): args = self.args signal.signal(signal.SIGINT, self._shutdown) signal.signal(signal.SIGTERM, self._shutdown) args.original_pid = os.getpid() if args.processes > 1: if not args.quiet: self.console.print('Forking %d processes' % args.processes) jobs = [] for i in range(args.processes): p = multiprocessing.Process(target=self._process) jobs.append(p) p.start() for job in jobs: self._procs.append(job) async def run(quiet, console): while len(self._procs) > 0: if not quiet: console.print(self.display_results(), end='\r') for job in jobs: if job.exitcode is not None and job in self._procs: self._procs.remove(job) await cancellable_sleep(args.console_update) await self.console.stop() await self.eventer.stop() tasks = [self.ensure_future(self.console.display()), self.ensure_future(self._send_workers_event(1)), self.ensure_future(run(args.quiet, self.console))] self.loop.run_until_complete(self.gather(*tasks)) else: self._process() return self._results def _shutdown(self, signal, frame): stop() self._kill_tasks() # send sigterms for proc in self._procs: proc.terminate() def _runner(self): args = self.args def _prepare(): tasks = [] delay = 0 if args.ramp_up > 0.: step = args.ramp_up / args.workers else: step = 0. for i in range(self.args.workers): worker = Worker(i, self._results, self.console, self.args, self.statsd, delay, self.loop) f = self.ensure_future(worker.run()) tasks.append(f) delay += step return tasks if self.args.quiet: return _prepare() else: msg = 'Preparing {} worker{}' msg = msg.format(args.workers, 's' if args.workers > 1 else '') return self.console.print_block(msg, _prepare) def _process(self): set_timer() # coroutine that will kill everything when duration is up if self.args.duration and self.args.force_shutdown: async def _duration_killer(): cancelled = object() res = await cancellable_sleep(self.args.duration, result=cancelled) if res is cancelled or (res and not res.canceled()): self._shutdown(None, None) await asyncio.sleep(0) _duration_killer = self.ensure_future(_duration_killer()) else: _duration_killer = None if self.args.processes > 1: signal.signal(signal.SIGINT, self._shutdown) signal.signal(signal.SIGTERM, self._shutdown) self.loop = asyncio.new_event_loop() asyncio.set_event_loop(self.loop) if self.args.debug: self.console.print('**** RUNNING IN DEBUG MODE == SLOW ****') self.loop.set_debug(True) self._set_statsd() if self.args.original_pid == os.getpid(): self._tasks.append(self.ensure_future(self._send_workers_event(1))) if not self.args.quiet: fut = self._display_results(self.args.console_update) update = self.ensure_future(fut) display = self.ensure_future(self.console.display()) display = self.gather(update, display) self._tasks.append(display) workers = self.gather(*self._runner()) def _stop(cb): if _duration_killer is not None: if not _duration_killer.done(): _duration_killer.cancel() stop() workers.add_done_callback(_stop) self._tasks.append(workers) try: self.loop.run_until_complete(self.gather(*self._tasks)) finally: self._kill_tasks() if self.statsd is not None: self.statsd.close() self.loop.run_until_complete(self.ensure_future(asyncio.sleep(0))) self.loop.close() def _kill_tasks(self): cancellable_sleep.cancel_all() for task in reversed(self._tasks): with suppress(asyncio.CancelledError): task.cancel() for task in self._tasks: del task self._tasks[:] = [] def display_results(self): ok, fail = self._results['OK'].value, self._results['FAILED'].value workers = self._results['WORKER'].value pat = 'SUCCESSES: %s | FAILURES: %s | WORKERS: %s' return pat % (ok, fail, workers) async def _display_results(self, update_interval): while not is_stopped(): self.console.print(self.display_results(), end='\r') await cancellable_sleep(update_interval) await self.console.stop() async def _send_workers_event(self, update_interval): while not self.eventer.stopped() and not is_stopped(): workers = self._results['WORKER'].value await self.eventer.send_event('current_workers', workers=workers) await cancellable_sleep(update_interval) molotov-1.6/molotov/listeners.py0000644000076500000240000000722513157010511017371 0ustar tarekstaff00000000000000import io from molotov.api import get_fixture _UNREADABLE = "***WARNING: Molotov can't display this body***" _BINARY = "**** Binary content ****" _FILE = "**** File content ****" _COMPRESSED = ('gzip', 'compress', 'deflate', 'identity', 'br') class BaseListener(object): async def __call__(self, event, **options): attr = getattr(self, 'on_' + event, None) if attr is not None: await attr(**options) class StdoutListener(BaseListener): def __init__(self, **options): self.verbose = options.get('verbose', 0) self.console = options['console'] def _body2str(self, body): try: from aiohttp.payload import Payload except ImportError: Payload = None if Payload is not None and isinstance(body, Payload): body = body._value if isinstance(body, io.IOBase): return _FILE if not isinstance(body, str): try: body = str(body, 'utf8') except UnicodeDecodeError: return _UNREADABLE return body async def on_sending_request(self, session, request): if self.verbose < 2: return raw = '>' * 45 raw += '\n' + request.method + ' ' + str(request.url) if len(request.headers) > 0: headers = '\n'.join('%s: %s' % (k, v) for k, v in request.headers.items()) raw += '\n' + headers if request.headers.get('Content-Encoding') in _COMPRESSED: raw += '\n\n' + _BINARY + '\n' elif request.body: raw += '\n\n' + self._body2str(request.body) + '\n' self.console.print(raw) async def on_response_received(self, session, response, request): if self.verbose < 2: return raw = '\n' + '=' * 45 + '\n' raw += 'HTTP/1.1 %d %s\n' % (response.status, response.reason) items = response.headers.items() headers = '\n'.join('{}: {}'.format(k, v) for k, v in items) raw += headers if response.headers.get('Content-Encoding') in _COMPRESSED: raw += '\n\n' + _BINARY elif response.content: content = await response.content.read() if len(content) > 0: # put back the data in the content response.content.unread_data(content) try: raw += '\n\n' + content.decode() except UnicodeDecodeError: raw += '\n\n' + _UNREADABLE else: raw += '\n\n' raw += '\n' + '<' * 45 + '\n' self.console.print(raw) class CustomListener(object): def __init__(self, fixture): self.fixture = fixture async def __call__(self, event, **options): await self.fixture(event, **options) class EventSender(object): def __init__(self, console, listeners=None): self.console = console if listeners is None: listeners = [] self._listeners = listeners self._stopped = False fixture_listeners = get_fixture('events') if fixture_listeners is not None: for listener in fixture_listeners: self.add_listener(CustomListener(listener)) def add_listener(self, listener): self._listeners.append(listener) async def stop(self): self._stopped = True def stopped(self): return self._stopped async def send_event(self, event, **options): for listener in self._listeners: try: await listener(event, **options) except Exception as e: self.console.print_error(e) molotov-1.6/molotov/util.py0000644000076500000240000001531613261443307016347 0ustar tarekstaff00000000000000from io import StringIO import traceback import sys import functools import json import socket import os import asyncio import time import threading from urllib.parse import urlparse, urlunparse from socket import gethostbyname from aiohttp import ClientSession, __version__ _DNS_CACHE = {} _STOP = False _TIMER = None IS_AIOHTTP2 = __version__[0] == '2' def get_timer(): return _TIMER def set_timer(value=None): global _TIMER if value is None: value = int(time.time()) _TIMER = value def stop(): global _STOP _STOP = True def is_stopped(): return _STOP def resolve(url): parts = urlparse(url) if '@' in parts.netloc: username, password = parts.username, parts.password netloc = parts.netloc.split('@', 1)[1] else: username, password = None, None netloc = parts.netloc if ':' in netloc: host = netloc.split(':')[0] else: host = netloc port_provided = False if not parts.port and parts.scheme == 'https': port = 443 elif not parts.port and parts.scheme == 'http': port = 80 else: port = parts.port port_provided = True original = host resolved = None if host in _DNS_CACHE: resolved = _DNS_CACHE[host] else: try: resolved = gethostbyname(host) _DNS_CACHE[host] = resolved except socket.gaierror: return url, original, host # Don't use a resolved hostname for SSL requests otherwise the # certificate will not match the IP address (resolved) host = resolved if parts.scheme != 'https' else host netloc = host if port_provided: netloc += ':%d' % port if username is not None: if password is not None: netloc = '%s:%s@%s' % (username, password, netloc) else: netloc = '%s@%s' % (username, netloc) if port not in (443, 80): host += ':%d' % port original += ':%d' % port new = urlunparse((parts.scheme, netloc, parts.path or '', '', parts.query or '', parts.fragment or '')) return new, original, host class OptionError(Exception): pass def _expand_args(args, options): for key, val in options.items(): setattr(args, key, val) def expand_options(config, scenario, args): if not isinstance(config, str): try: config = json.loads(config.read()) except Exception: raise OptionError("Can't parse %r" % config) else: if not os.path.exists(config): raise OptionError("Can't find %r" % config) with open(config) as f: try: config = json.loads(f.read()) except ValueError: raise OptionError("Can't parse %r" % config) if 'molotov' not in config: raise OptionError("Bad config -- no molotov key") if 'tests' not in config['molotov']: raise OptionError("Bad config -- no molotov/tests key") if scenario not in config['molotov']['tests']: raise OptionError("Can't find %r in the config" % scenario) _expand_args(args, config['molotov']['tests'][scenario]) def _run_in_fresh_loop(coro, timeout=30): thres = [] thexc = [] def run(): loop = asyncio.new_event_loop() try: task = loop.create_task(coro(loop=loop)) thres.append(loop.run_until_complete(task)) except Exception as e: thexc.append(e) finally: loop.close() th = threading.Thread(target=run) th.start() th.join(timeout=timeout) # re-raise a thread exception if len(thexc) > 0: raise thexc[0] return thres[0] async def _request(endpoint, verb='GET', session_options=None, json=False, loop=None, **options): if session_options is None: session_options = {} async with ClientSession(loop=loop, **session_options) as session: meth = getattr(session, verb.lower()) result = {} async with meth(endpoint, **options) as resp: if json: result['content'] = await resp.json() else: result['content'] = await resp.text() result['status'] = resp.status result['headers'] = resp.headers return result def request(endpoint, verb='GET', session_options=None, **options): """Performs a synchronous request. Uses a dedicated event loop and aiohttp.ClientSession object. Options: - endpoint: the endpoint to call - verb: the HTTP verb to use (defaults: GET) - session_options: a dict containing options to initialize the session (defaults: None) - options: extra options for the request (defaults: None) Returns a dict object with the following keys: - content: the content of the response - status: the status - headers: a dict with all the response headers """ req = functools.partial(_request, endpoint, verb, session_options, **options) return _run_in_fresh_loop(req) def json_request(endpoint, verb='GET', session_options=None, **options): """Like :func:`molotov.request` but extracts json from the response. """ req = functools.partial(_request, endpoint, verb, session_options, json=True, **options) return _run_in_fresh_loop(req) _VARS = {} def set_var(name, value): """Sets a global variable. Options: - name: name of the variable - value: object to set """ _VARS[name] = value def get_var(name, factory=None): """Gets a global variable given its name. If factory is not None and the variable is not set, factory is a callable that will set the variable. If not set, returns None. """ if name not in _VARS and factory is not None: _VARS[name] = factory() return _VARS.get(name) # taken from https://stackoverflow.com/a/37211337 def _make_sleep(): async def sleep(delay, result=None, *, loop=None): coro = asyncio.sleep(delay, result=result, loop=loop) task = asyncio.ensure_future(coro, loop=loop) sleep.tasks.add(task) try: return await task except asyncio.CancelledError: return result finally: sleep.tasks.remove(task) sleep.tasks = set() sleep.cancel_all = lambda: sum(task.cancel() for task in sleep.tasks) return sleep cancellable_sleep = _make_sleep() def printable_error(error, tb=None): printable = [repr(error)] if tb is None: tb = sys.exc_info()[2] printed = StringIO() traceback.print_tb(tb, file=printed) printed.seek(0) for line in printed.readlines(): printable.append(line.rstrip('\n')) return printable molotov-1.6/molotov/tests/0000755000076500000240000000000013261446760016162 5ustar tarekstaff00000000000000molotov-1.6/molotov/tests/example5.py0000644000076500000240000000102613214303573020243 0ustar tarekstaff00000000000000""" This Molotov script demonstrates how to hook events. """ import molotov @molotov.events() async def print_request(event, **info): if event == 'sending_request': print("=>") @molotov.events() async def print_response(event, **info): if event == 'response_received': print("<=") @molotov.scenario(100) async def scenario_one(session): async with session.get('http://localhost:8080') as resp: res = await resp.json() assert res['result'] == 'OK' assert resp.status == 200 molotov-1.6/molotov/tests/support.py0000644000076500000240000002103313261443745020247 0ustar tarekstaff00000000000000import sys import signal import os import asyncio import unittest import multiprocessing import time from contextlib import contextmanager import functools from collections import namedtuple from http.client import HTTPConnection from io import StringIO import http.server import socketserver import pytest from queue import Empty from unittest.mock import patch from aiohttp.client_reqrep import URL from multidict import CIMultiDict from molotov.api import _SCENARIO, _FIXTURES from molotov import util from molotov.run import PYPY from molotov.session import LoggedClientRequest, LoggedClientResponse from molotov.sharedconsole import SharedConsole from molotov.sharedcounter import SharedCounters HERE = os.path.dirname(__file__) skip_pypy = pytest.mark.skipif(PYPY, reason='could not make work on pypy') only_pypy = pytest.mark.skipif(not PYPY, reason='only pypy') if os.environ.get('HAS_JOSH_K_SEAL_OF_APPROVAL', False): _TIMEOUT = 1. else: _TIMEOUT = .2 async def serialize(console): res = [] while True: try: res.append(console._stream.get(block=True, timeout=_TIMEOUT)) except Empty: break return ''.join(res) class HandlerRedirect(http.server.SimpleHTTPRequestHandler): def do_GET(self): if self.path == "/redirect": self.send_response(302) self.send_header('Location', '/') self.end_headers() return if self.path == "/slow": try: time.sleep(5) self.send_response(200) self.end_headers() except SystemExit: pass return return super(HandlerRedirect, self).do_GET() def run_server(port=8888): """Running in a subprocess to avoid any interference """ def _run(): os.chdir(HERE) socketserver.TCPServer.allow_reuse_address = True attempts = 0 httpd = None while attempts < 3: try: httpd = socketserver.TCPServer(("", port), HandlerRedirect) break except Exception: attempts += 1 time.sleep(.1) if httpd is None: raise OSError("Could not start the coserver") def _shutdown(*args, **kw): httpd.server_close() sys.exit(0) signal.signal(signal.SIGTERM, _shutdown) signal.signal(signal.SIGINT, _shutdown) httpd.serve_forever() p = multiprocessing.Process(target=_run) p.start() start = time.time() connected = False while time.time() - start < 5 and not connected: try: conn = HTTPConnection('localhost', 8888) conn.request("GET", "/") conn.getresponse() connected = True except Exception: time.sleep(.1) if not connected: os.kill(p.pid, signal.SIGTERM) p.join(timeout=1.) raise OSError('Could not connect to coserver') return p _CO = {'clients': 0, 'server': None} @contextmanager def coserver(port=8888): if _CO['clients'] == 0: _CO['server'] = run_server(port) _CO['clients'] += 1 try: yield finally: _CO['clients'] -= 1 if _CO['clients'] == 0: os.kill(_CO['server'].pid, signal.SIGTERM) _CO['server'].join(timeout=1.) _CO['server'].terminate() _CO['server'] = None def _respkw(): from aiohttp.helpers import TimerNoop return {'request_info': None, 'writer': None, 'continue100': None, 'timer': TimerNoop(), 'auto_decompress': True, 'traces': [], 'loop': asyncio.get_event_loop(), 'session': None} def Response(method='GET', status=200, body=b'***'): if util.IS_AIOHTTP2: response = LoggedClientResponse(method, URL('/')) else: response = LoggedClientResponse(method, URL('/'), **_respkw()) response.status = status response.reason = '' response.code = status response.should_close = False response.headers = CIMultiDict({}) response.raw_headers = [] class Body: async def read(self): return body def unread_data(self, data): if body == b'': err = AttributeError("'EmptyStreamReader' object has no " "attribute 'unread_data'") raise err pass response.content = Body() response._content = body return response def Request(url="http://127.0.0.1/", method='GET', body=b'***'): request = LoggedClientRequest(method, URL(url)) request.body = body return request class TestLoop(unittest.TestCase): def setUp(self): self.old = dict(_SCENARIO) self.oldsetup = dict(_FIXTURES) util._STOP = False util._TIMER = None self.policy = asyncio.get_event_loop_policy() def tearDown(self): _SCENARIO.clear() _FIXTURES.clear() _FIXTURES.update(self.oldsetup) asyncio.set_event_loop_policy(self.policy) def get_args(self, console=None): args = namedtuple('args', 'verbose quiet duration exception') args.force_shutdown = False args.ramp_up = .0 args.verbose = 1 args.quiet = False args.duration = 5 args.exception = True args.processes = 1 args.debug = True args.workers = 1 args.console = True args.statsd = False args.single_mode = None args.max_runs = None args.delay = .0 args.sizing = False args.sizing_tolerance = .0 args.console_update = 0 args.use_extension = [] args.fail = None args.force_reconnection = False if console is None: console = SharedConsole(interval=0) args.shared_console = console return args def async_test(func): @functools.wraps(func) def _async_test(*args, **kw): cofunc = asyncio.coroutine(func) oldloop = asyncio.get_event_loop() loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) loop.set_debug(True) console = SharedConsole(interval=0) results = SharedCounters('WORKER', 'REACHED', 'RATIO', 'OK', 'FAILED', 'MINUTE_OK', 'MINUTE_FAILED') kw['loop'] = loop kw['console'] = console kw['results'] = results try: loop.run_until_complete(cofunc(*args, **kw)) finally: loop.stop() loop.close() asyncio.set_event_loop(oldloop) return _async_test def dedicatedloop(func): @functools.wraps(func) def _loop(*args, **kw): old_loop = asyncio.get_event_loop() loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) try: return func(*args, **kw) finally: if not loop.is_closed(): loop.stop() loop.close() asyncio.set_event_loop(old_loop) return _loop def dedicatedloop_noclose(func): @functools.wraps(func) def _loop(*args, **kw): old_loop = asyncio.get_event_loop() loop = asyncio.new_event_loop() loop.set_debug(True) loop._close = loop.close loop.close = lambda: None asyncio.set_event_loop(loop) try: return func(*args, **kw) finally: loop._close() asyncio.set_event_loop(old_loop) return _loop @contextmanager def catch_output(): oldout, olderr = sys.stdout, sys.stderr sys.stdout, sys.stderr = StringIO(), StringIO() try: yield sys.stdout, sys.stderr finally: sys.stdout.seek(0) sys.stderr.seek(0) sys.stdout, sys.stderr = oldout, olderr @contextmanager def set_args(*args): old = list(sys.argv) sys.argv[:] = args oldout, olderr = sys.stdout, sys.stderr sys.stdout, sys.stderr = StringIO(), StringIO() try: yield sys.stdout, sys.stderr finally: sys.stdout.seek(0) sys.stderr.seek(0) sys.argv[:] = old sys.stdout, sys.stderr = oldout, olderr @contextmanager def catch_sleep(calls=None): original = asyncio.sleep if calls is None: calls = [] async def _slept(delay, result=None, *, loop=None): # 86400 is the duration timer if delay not in (0, 86400): calls.append(delay) # forces a context switch await original(0) with patch('asyncio.sleep', _slept): yield calls molotov-1.6/molotov/tests/test_quickstart.py0000644000076500000240000000377613142054667022001 0ustar tarekstaff00000000000000import tempfile import shutil import os from molotov import quickstart, __version__, run from molotov.tests.support import set_args, TestLoop, dedicatedloop class TestQuickStart(TestLoop): def setUp(self): super(TestQuickStart, self).setUp() self._curdir = os.getcwd() self.tempdir = tempfile.mkdtemp() self.location = os.path.join(self.tempdir, 'new') self._answers = ['', 'y', self.location] def tearDown(self): os.chdir(self._curdir) shutil.rmtree(self.tempdir) super(TestQuickStart, self).tearDown() def _input(self, text): if self._answers == []: self._answers = ['', 'y', self.location] answer = self._answers.pop() return answer def test_version(self): quickstart._input = self._input with set_args('molostart', '--version') as out: try: quickstart.main() except SystemExit: pass output = out[0].read().strip() self.assertEqual(output, __version__) def test_generate(self): quickstart._input = self._input with set_args('molostart'): quickstart.main() result = os.listdir(self.location) result.sort() self.assertEqual(result, ['Makefile', 'loadtest.py', 'molotov.json']) # second runs stops with set_args('molostart'): try: quickstart.main() raise AssertionError() except SystemExit: pass @dedicatedloop def test_codeworks(self): quickstart._input = self._input with set_args('molostart'): quickstart.main() result = os.listdir(self.location) result.sort() self.assertEqual(result, ['Makefile', 'loadtest.py', 'molotov.json']) os.chdir(self.location) with set_args('molotov', '-cxv', '--max-runs', '1'): try: run.main() except SystemExit: pass molotov-1.6/molotov/tests/test_slave.py0000644000076500000240000000214313254442176020704 0ustar tarekstaff00000000000000import os import pytest from molotov import __version__ from molotov.slave import main from molotov.tests.support import TestLoop, dedicatedloop, set_args _REPO = 'https://github.com/loads/molotov' NO_INTERNET = os.environ.get("NO_INTERNET") is not None @pytest.mark.skipif(NO_INTERNET, reason="This test requires internet access") class TestSlave(TestLoop): @dedicatedloop def test_main(self): with set_args('moloslave', _REPO, 'test') as out: main() if os.environ.get("TRAVIS") is not None: return output = out[0].read() self.assertTrue('Preparing 1 worker...' in output, output) self.assertTrue('OK' in output, output) @dedicatedloop def test_fail(self): with set_args('moloslave', _REPO, 'fail'): self.assertRaises(Exception, main) @dedicatedloop def test_version(self): with set_args('moloslave', '--version') as out: try: main() except SystemExit: pass version = out[0].read().strip() self.assertTrue(version, __version__) molotov-1.6/molotov/tests/example4.py0000644000076500000240000000262413157012711020244 0ustar tarekstaff00000000000000""" This Molotov script has: - a global setup fixture that sets variables - an init worker fixture that sets the session headers - an init session that attachs an object to the current session - 1 scenario - 2 tear downs fixtures """ import molotov class SomeObject(object): """Does something smart in real life with the async loop. """ def __init__(self, loop): self.loop = loop def cleanup(self): pass @molotov.global_setup() def init_test(args): molotov.set_var('SomeHeader', '1') molotov.set_var('endpoint', 'http://localhost:8080') @molotov.setup() async def init_worker(worker_num, args): headers = {'AnotherHeader': '1', 'SomeHeader': molotov.get_var('SomeHeader')} return {'headers': headers} @molotov.setup_session() async def init_session(worker_num, session): session.ob = SomeObject(loop=session.loop) @molotov.scenario(100) async def scenario_one(session): endpoint = molotov.get_var('endpoint') async with session.get(endpoint) as resp: res = await resp.json() assert res['result'] == 'OK' assert resp.status == 200 @molotov.teardown_session() async def end_session(worker_num, session): session.ob.cleanup() @molotov.teardown() def end_worker(worker_num): print("This is the end for %d" % worker_num) @molotov.global_teardown() def end_test(): print("This is the end of the test.") molotov-1.6/molotov/tests/test_sharedcounter.py0000644000076500000240000000423713142053477022444 0ustar tarekstaff00000000000000import unittest import multiprocessing from molotov.sharedcounter import SharedCounters, SharedCounter # pre-forked variable _DATA = SharedCounters('test') def run_worker(value): _DATA['test'] += value _DATA['test'] -= value _DATA['test'] += value class TestSharedCounters(unittest.TestCase): def test_operators(self): c1 = SharedCounter('ok') c2 = SharedCounter('ok') c1.value = 4 c2.value = 5 self.assertTrue(c1 <= c2) self.assertTrue(c1 < c2) self.assertTrue(c1 >= 2) self.assertTrue(c1 > 2) self.assertTrue(c1 == 4) self.assertTrue(c1 != 5) c2.value = 4 c2 += SharedCounter('ok') self.assertTrue(c1 == c2) repr(c1) str(c1) def _t(): c = SharedCounter('ok') c += 6.2 self.assertRaises(NotImplementedError, _t) def _c(): SharedCounter('ok') != 6.3 self.assertRaises(TypeError, _c) def test_interface(self): data = SharedCounters('one', 'two') self.assertTrue('one' in data) self.assertEqual(len(data.keys()), 2) for key in data: data[key] = 0 self.assertTrue(data[key], 0) for key, value in data.items(): data[key] = value self.assertTrue(data[key], value) data.values() repr(data) str(data) self.assertRaises(KeyError, data.__setitem__, 'meh', 1) self.assertRaises(TypeError, data.__setitem__, 'one', '1') def test_mapping(self): # making sure it works like a defaultdict(int) data = SharedCounters('one', 'two') self.assertTrue(data['one'].value == 0) data['one'] += 10 data['one'] -= 1 data['two'] = 4 self.assertTrue(data['one'].value, 9) self.assertTrue(data['two'].value, 4) def test_multiprocess(self): # now let's try with several processes pool = multiprocessing.Pool(10) try: inputs = [1] * 3000 pool.map(run_worker, inputs) self.assertEqual(_DATA['test'].value, 3000) finally: pool.close() molotov-1.6/molotov/tests/test_session.py0000644000076500000240000001620213176044126021252 0ustar tarekstaff00000000000000import gzip from aiohttp.client_reqrep import ClientRequest from yarl import URL from unittest.mock import patch from molotov.listeners import BaseListener import molotov.session from molotov.tests.support import coserver, Response, Request from molotov.tests.support import TestLoop, async_test, serialize class TestLoggedClientSession(TestLoop): def _get_session(self, *args, **kw): return molotov.session.LoggedClientSession(*args, **kw) @async_test async def test_add_listener(self, loop, console, results): class MyListener(BaseListener): def __init__(self): self.responses = [] def on_response_received(self, **options): self.responses.append(options['response']) lis = MyListener() async with self._get_session(loop, console, verbose=2) as session: session.eventer.add_listener(lis) request = Request() binary_body = b'' response = Response(body=binary_body) await session.send_event('response_received', response=response, request=request) await serialize(console) self.assertEqual(lis.responses, [response]) @async_test async def test_empty_response(self, loop, console, results): async with self._get_session(loop, console, verbose=2) as session: request = Request() binary_body = b'' response = Response(body=binary_body) await session.send_event('response_received', response=response, request=request) await serialize(console) @async_test async def test_encoding(self, loop, console, results): async with self._get_session(loop, console, verbose=2) as session: request = Request() binary_body = b'MZ\x90\x00\x03\x00\x00\x00\x04\x00' response = Response(body=binary_body) await session.send_event('response_received', response=response, request=request) res = await serialize(console) wanted = "can't display this body" self.assertTrue(wanted in res) @async_test async def test_request(self, loop, console, results): with coserver(): async with self._get_session(loop, console, verbose=2) as session: async with session.get('http://localhost:8888') as resp: self.assertEqual(resp.status, 200) res = await serialize(console) self.assertTrue('GET http://127.0.0.1:8888' in res) @async_test async def test_not_verbose(self, loop, console, results): async with self._get_session(loop, console, verbose=1) as session: req = ClientRequest('GET', URL('http://example.com')) await session.send_event('sending_request', request=req) response = Response(body='') request = Request() await session.send_event('response_received', response=response, request=request) res = await serialize(console) self.assertEqual(res, '') @async_test async def test_gzipped_request(self, loop, console, results): async with self._get_session(loop, console, verbose=2) as session: binary_body = gzip.compress(b'some gzipped data') req = ClientRequest('GET', URL('http://example.com'), data=binary_body) req.headers['Content-Encoding'] = 'gzip' await session.send_event('sending_request', request=req) res = await serialize(console) self.assertTrue("Binary" in res, res) @async_test async def test_file_request(self, loop, console, results): async with self._get_session(loop, console, verbose=2) as session: with open(__file__) as f: req = ClientRequest('POST', URL('http://example.com'), data=f) req.headers['Content-Encoding'] = 'something/bin' await session.send_event('sending_request', request=req) res = await serialize(console) self.assertTrue("File" in res, res) @async_test async def test_binary_file_request(self, loop, console, results): async with self._get_session(loop, console, verbose=2) as session: with open(__file__, 'rb') as f: req = ClientRequest('POST', URL('http://example.com'), data=f) req.headers['Content-Encoding'] = 'something/bin' await session.send_event('sending_request', request=req) res = await serialize(console) self.assertTrue("File" in res, res) @async_test async def test_gzipped_response(self, loop, console, results): async with self._get_session(loop, console, verbose=2) as session: request = Request() binary_body = gzip.compress(b'some gzipped data') response = Response(body=binary_body) response.headers['Content-Encoding'] = 'gzip' await session.send_event('response_received', response=response, request=request) res = await serialize(console) self.assertTrue("Binary" in res, res) @async_test async def test_cantread_request(self, loop, console, results): async with self._get_session(loop, console, verbose=2) as session: binary_body = gzip.compress(b'some gzipped data') req = ClientRequest('GET', URL('http://example.com'), data=binary_body) await session.send_event('sending_request', request=req) res = await serialize(console) self.assertTrue("display this body" in res, res) @async_test async def test_old_request_version(self, loop, console, results): orig_import = __import__ def import_mock(name, *args, **kw): if name == 'aiohttp.payload': raise ImportError() return orig_import(name, *args, **kw) with patch('builtins.__import__', side_effect=import_mock): async with self._get_session(loop, console, verbose=2) as session: body = "ok man" req = ClientRequest('GET', URL('http://example.com'), data=body) req.body = req.body._value await session.send_event('sending_request', request=req) res = await serialize(console) self.assertTrue("ok man" in res, res) molotov-1.6/molotov/tests/test_run.py0000644000076500000240000005237613260626031020402 0ustar tarekstaff00000000000000import time import random import os import signal import asyncio from unittest.mock import patch from molotov.api import scenario, global_setup from molotov.tests.support import (TestLoop, coserver, dedicatedloop, set_args, skip_pypy, only_pypy, catch_sleep, dedicatedloop_noclose) from molotov.tests.statsd import UDPServer from molotov.run import run, main from molotov.sharedcounter import SharedCounters from molotov.util import request, json_request, set_timer from molotov import __version__ _HERE = os.path.dirname(__file__) _CONFIG = os.path.join(_HERE, 'molotov.json') _RES = [] _RES2 = {} class TestRunner(TestLoop): def setUp(self): super(TestRunner, self).setUp() _RES[:] = [] _RES2.clear() def _get_args(self): args = self.get_args() args.statsd = True args.statsd_address = 'udp://127.0.0.1:9999' args.scenario = 'molotov.tests.test_run' return args @dedicatedloop_noclose def test_redirect(self): @scenario(weight=10) async def _one(session): # redirected async with session.get('http://localhost:8888/redirect') as resp: redirect = resp.history assert redirect[0].status == 302 assert resp.status == 200 # not redirected async with session.get('http://localhost:8888/redirect', allow_redirects=False) as resp: redirect = resp.history assert len(redirect) == 0 assert resp.status == 302 content = await resp.text() assert content == '' _RES.append(1) args = self._get_args() args.verbose = 2 args.max_runs = 2 with coserver(): run(args) self.assertTrue(len(_RES) > 0) @dedicatedloop_noclose def test_runner(self): test_loop = asyncio.get_event_loop() @global_setup() def something_sync(args): grab = request('http://localhost:8888') self.assertEqual(grab['status'], 200) grab_json = json_request('http://localhost:8888/molotov.json') self.assertTrue('molotov' in grab_json['content']) @scenario(weight=10) async def here_one(session): async with session.get('http://localhost:8888') as resp: await resp.text() _RES.append(1) @scenario(weight=90) async def here_two(session): if session.statsd is not None: session.statsd.incr('yopla') _RES.append(2) args = self._get_args() server = UDPServer('127.0.0.1', 9999, loop=test_loop) _stop = asyncio.Future() async def stop(): await _stop await server.stop() server_task = asyncio.ensure_future(server.run()) stop_task = asyncio.ensure_future(stop()) args.max_runs = 3 with coserver(): run(args) _stop.set_result(True) test_loop.run_until_complete(asyncio.gather(server_task, stop_task)) self.assertTrue(len(_RES) > 0) udp = server.flush() self.assertTrue(len(udp) > 0) @dedicatedloop def test_main(self): with set_args('molotov', '-cq', '-d', '1', 'molotov/tests/example.py'): main() def _test_molotov(self, *args): if '--duration' not in args and '-d' not in args: args = list(args) + ['--duration', '10'] rc = 0 with set_args('molotov', *args) as (stdout, stderr): try: main() except SystemExit as e: rc = e.code return stdout.read().strip(), stderr.read().strip(), rc @dedicatedloop def test_version(self): stdout, stderr, rc = self._test_molotov('--version') self.assertEqual(stdout, __version__) @dedicatedloop def test_empty_scenario(self): stdout, stderr, rc = self._test_molotov('') self.assertTrue('Cannot import' in stdout) @dedicatedloop def test_no_scenario(self): stdout, stderr, rc = self._test_molotov() self.assertTrue('Cannot import' in stdout) @dedicatedloop def test_config_no_scenario(self): stdout, stderr, rc = self._test_molotov('-c', '--config', _CONFIG, 'DONTEXIST') wanted = "Can't find 'DONTEXIST' in the config" self.assertTrue(wanted in stdout) @dedicatedloop def test_config_verbose_quiet(self): stdout, stderr, rc = self._test_molotov( '-qv', '--config', _CONFIG) wanted = "You can't" self.assertTrue(wanted in stdout) @dedicatedloop def test_config_no_scenario_found(self): stdout, stderr, rc = self._test_molotov( '-c', 'molotov.tests.test_run') wanted = "No scenario was found" self.assertTrue(wanted in stdout) @dedicatedloop def test_config_no_single_mode_found(self): @scenario(weight=10) async def not_me(session): _RES.append(3) stdout, stderr, rc = self._test_molotov('-c', '-s', 'blah', 'molotov.tests.test_run') wanted = "Can't find" self.assertTrue(wanted in stdout) @dedicatedloop def test_name(self): @scenario(weight=10) async def here_three(session): _RES.append(3) @scenario(weight=30, name='me') async def here_four(session): _RES.append(4) stdout, stderr, rc = self._test_molotov('-cx', '--max-runs', '2', '-s', 'me', 'molotov.tests.test_run') wanted = "SUCCESSES: 2" self.assertTrue(wanted in stdout) self.assertTrue(_RES, [4, 4]) @dedicatedloop def test_single_mode(self): @scenario(weight=10) async def here_three(session): _RES.append(3) stdout, stderr, rc = self._test_molotov('-cx', '--max-runs', '2', '-s', 'here_three', 'molotov.tests.test_run') wanted = "SUCCESSES: 2" self.assertTrue(wanted in stdout) @dedicatedloop def test_fail_mode_pass(self): @scenario(weight=10) async def here_three(session): _RES.append(3) stdout, stderr, rc = self._test_molotov('-cx', '--max-runs', '2', '--fail', '1', '-s', 'here_three', 'molotov.tests.test_run') wanted = "SUCCESSES: 2" self.assertTrue(wanted in stdout) self.assertEqual(rc, 0) @dedicatedloop def test_fail_mode_fail(self): @scenario(weight=10) async def here_three(session): assert False stdout, stderr, rc = self._test_molotov('-cx', '--max-runs', '2', '--fail', '1', '-s', 'here_three', 'molotov.tests.test_run') self.assertEqual(rc, 1) @only_pypy @dedicatedloop def test_uvloop_pypy(self): @scenario(weight=10) async def here_three(session): _RES.append(3) orig_import = __import__ def import_mock(name, *args): if name == 'uvloop': raise ImportError() return orig_import(name, *args) with patch('builtins.__import__', side_effect=import_mock): stdout, stderr, rc = self._test_molotov('-cx', '--max-runs', '2', '-s', 'here_three', '--uvloop', 'molotov.tests.test_run') wanted = "You can't use uvloop" self.assertTrue(wanted in stdout) @skip_pypy @dedicatedloop def test_uvloop_import_error(self): @scenario(weight=10) async def here_three(session): _RES.append(3) orig_import = __import__ def import_mock(name, *args): if name == 'uvloop': raise ImportError() return orig_import(name, *args) with patch('builtins.__import__', side_effect=import_mock): stdout, stderr, rc = self._test_molotov('-cx', '--max-runs', '2', '--console-update', '0', '-s', 'here_three', '--uvloop', 'molotov.tests.test_run') wanted = "You need to install uvloop" self.assertTrue(wanted in stdout) @skip_pypy @dedicatedloop def test_uvloop(self): @scenario(weight=10) async def here_three(session): _RES.append(3) stdout, stderr, rc = self._test_molotov('-cx', '--max-runs', '2', '-s', 'here_three', '--uvloop', 'molotov.tests.test_run') wanted = "SUCCESSES: 2" self.assertTrue(wanted in stdout, stdout) @dedicatedloop def test_delay(self): with catch_sleep() as delay: @scenario(weight=10, delay=.1) async def here_three(session): _RES.append(3) stdout, stderr, rc = self._test_molotov('--delay', '.6', '--console-update', '0', '-cx', '--max-runs', '2', '-s', 'here_three', 'molotov.tests.test_run') wanted = "SUCCESSES: 2" self.assertTrue(wanted in stdout, stdout) self.assertEqual(delay, [1, .1, 1, .6, 1, .1, 1, .6, 1]) @dedicatedloop def test_rampup(self): with catch_sleep() as delay: @scenario(weight=10) async def here_three(session): _RES.append(3) stdout, stderr, rc = self._test_molotov('--ramp-up', '10', '--workers', '5', '--console-update', '0', '-cx', '--max-runs', '2', '-s', 'here_three', 'molotov.tests.test_run') # workers should start every 2 seconds since # we have 5 workers and a ramp-up # the first one starts immediatly, then each worker # sleeps 2 seconds more. delay = [d for d in delay if d != 0] self.assertEqual(delay, [1, 2.0, 4.0, 6.0, 8.0, 1, 1]) wanted = "SUCCESSES: 10" self.assertTrue(wanted in stdout, stdout) @dedicatedloop def test_sizing(self): _RES2['fail'] = 0 _RES2['succ'] = 0 with catch_sleep(): @scenario() async def sizer(session): if random.randint(0, 20) == 1: _RES2['fail'] += 1 raise AssertionError() else: _RES2['succ'] += 1 stdout, stderr, rc = self._test_molotov('--sizing', '--console-update', '0', '--sizing-tolerance', '5', '-s', 'sizer', 'molotov.tests.test_run') ratio = float(_RES2['fail']) / float(_RES2['succ']) * 100. self.assertTrue(ratio < 15. and ratio >= 5., ratio) @dedicatedloop def test_sizing_multiprocess(self): counters = SharedCounters('OK', 'FAILED') with catch_sleep(): @scenario() async def sizer(session): if random.randint(0, 10) == 1: counters['FAILED'] += 1 raise AssertionError() else: counters['OK'] += 1 with set_args('molotov', '--sizing', '-p', '2', '--sizing-tolerance', '5', '--console-update', '0', '-s', 'sizer', 'molotov.tests.test_run') as (stdout, stderr): try: main() except SystemExit: pass stdout, stderr = stdout.read().strip(), stderr.read().strip() # stdout, stderr, rc = self._test_molotov() ratio = (float(counters['FAILED'].value) / float(counters['OK'].value) * 100.) self.assertTrue(ratio >= 5., ratio) @dedicatedloop_noclose def test_statsd_multiprocess(self): test_loop = asyncio.get_event_loop() @scenario() async def staty(session): session.statsd.incr('yopla') server = UDPServer('127.0.0.1', 9999, loop=test_loop) _stop = asyncio.Future() async def stop(): await _stop await server.stop() server_task = asyncio.ensure_future(server.run()) stop_task = asyncio.ensure_future(stop()) args = self._get_args() args.verbose = 2 args.processes = 2 args.max_runs = 5 args.duration = 1000 args.statsd = True args.statsd_address = 'udp://127.0.0.1:9999' args.single_mode = 'staty' args.scenario = 'molotov.tests.test_run' run(args) _stop.set_result(True) test_loop.run_until_complete(asyncio.gather(server_task, stop_task)) udp = server.flush() incrs = 0 for line in udp: for el in line.split(b'\n'): if el.strip() == b'': continue incrs += 1 # two processes making 5 run each self.assertEqual(incrs, 10) @dedicatedloop def test_timed_sizing(self): _RES2['fail'] = 0 _RES2['succ'] = 0 _RES2['messed'] = False with catch_sleep(): @scenario() async def sizer(session): if session.worker_id == 200 and not _RES2['messed']: # worker 2 will mess with the timer # since we're faking all timers, the current # time in the test is always around 0 # so to have now() - get_timer() > 60 # we need to set a negative value here # to trick it set_timer(-61) _RES2['messed'] = True _RES2['fail'] = _RES2['succ'] = 0 if session.worker_id > 100: # starting to introduce errors passed the 100th if random.randint(0, 10) == 1: _RES2['fail'] += 1 raise AssertionError() else: _RES2['succ'] += 1 # forces a switch await asyncio.sleep(0) stdout, stderr, rc = self._test_molotov('--sizing', '--sizing-tolerance', '5', '--console-update', '0', '-cs', 'sizer', 'molotov.tests.test_run') ratio = float(_RES2['fail']) / float(_RES2['succ']) * 100. self.assertTrue(ratio < 20. and ratio > 5., ratio) @dedicatedloop def test_sizing_multiprocess_interrupted(self): counters = SharedCounters('OK', 'FAILED') @scenario() async def sizer(session): if random.randint(0, 10) == 1: counters['FAILED'] += 1 raise AssertionError() else: counters['OK'] += 1 async def _stop(): await asyncio.sleep(2.) os.kill(os.getpid(), signal.SIGINT) asyncio.ensure_future(_stop()) stdout, stderr, rc = self._test_molotov('--sizing', '-p', '3', '--sizing-tolerance', '90', '--console-update', '0', '-s', 'sizer', 'molotov.tests.test_run') self.assertTrue("Sizing was not finished" in stdout) @dedicatedloop def test_use_extension(self): ext = os.path.join(_HERE, 'example5.py') @scenario(weight=10) async def simpletest(session): async with session.get('http://localhost:8888') as resp: assert resp.status == 200 with coserver(): stdout, stderr, rc = self._test_molotov('-cx', '--max-runs', '1', '--use-extension=' + ext, '-s', 'simpletest', 'molotov.tests.test_run') self.assertTrue("=>" in stdout) self.assertTrue("<=" in stdout) @dedicatedloop def test_use_extension_fail(self): ext = os.path.join(_HERE, 'exampleIDONTEXIST.py') @scenario(weight=10) async def simpletest(session): async with session.get('http://localhost:8888') as resp: assert resp.status == 200 with coserver(): stdout, stderr, rc = self._test_molotov('-cx', '--max-runs', '1', '--use-extension=' + ext, '-s', 'simpletest', 'molotov.tests.test_run') self.assertTrue("Cannot import" in stdout) @dedicatedloop def test_use_extension_module_name(self): ext = 'molotov.tests.example5' @scenario(weight=10) async def simpletest(session): async with session.get('http://localhost:8888') as resp: assert resp.status == 200 with coserver(): stdout, stderr, rc = self._test_molotov('-cx', '--max-runs', '1', '--use-extension=' + ext, '-s', 'simpletest', 'molotov.tests.test_run') self.assertTrue("=>" in stdout) self.assertTrue("<=" in stdout) @dedicatedloop def test_use_extension_module_name_fail(self): ext = 'IDONTEXTSIST' @scenario(weight=10) async def simpletest(session): async with session.get('http://localhost:8888') as resp: assert resp.status == 200 with coserver(): stdout, stderr, rc = self._test_molotov('-cx', '--max-runs', '1', '--use-extension=' + ext, '-s', 'simpletest', 'molotov.tests.test_run') self.assertTrue("Cannot import" in stdout) @dedicatedloop def test_quiet(self): @scenario(weight=10) async def here_three(session): _RES.append(3) stdout, stderr, rc = self._test_molotov('-cx', '--max-runs', '1', '-q', '-s', 'here_three', 'molotov.tests.test_run') self.assertEqual(stdout, '') self.assertEqual(stderr, '') @dedicatedloop_noclose def test_slow_server_force_shutdown(self): @scenario(weight=10) async def _one(session): async with session.get('http://localhost:8888/slow') as resp: assert resp.status == 200 _RES.append(1) args = self._get_args() args.duration = 2 args.verbose = 2 args.max_runs = 1 args.force_shutdown = True start = time.time() with coserver(): run(args) # makes sure the test is stopped even if the server # hangs a socket self.assertTrue(time.time() - start < 4) self.assertTrue(len(_RES) == 0) @dedicatedloop_noclose def test_slow_server_graceful(self): @scenario(weight=10) async def _one(session): async with session.get('http://localhost:8888/slow') as resp: assert resp.status == 200 _RES.append(1) args = self._get_args() args.duration = 2 args.verbose = 2 args.max_runs = 1 # graceful shutdown on the other hand will wait # for the worker completion args.graceful_shutdown = True start = time.time() with coserver(): run(args) # makes sure the test finishes self.assertTrue(time.time() - start > 5) self.assertTrue(len(_RES) == 1) molotov-1.6/molotov/tests/__init__.py0000644000076500000240000000000013036435530020252 0ustar tarekstaff00000000000000molotov-1.6/molotov/tests/test_util.py0000644000076500000240000000463613142053477020556 0ustar tarekstaff00000000000000from io import StringIO from tempfile import mkstemp import json import unittest import os from molotov.util import (resolve, expand_options, OptionError, set_var, get_var, _VARS) _HERE = os.path.dirname(__file__) config = os.path.join(_HERE, '..', '..', 'molotov.json') class Args: pass class TestUtil(unittest.TestCase): def setUp(self): super(TestUtil, self).setUp() _VARS.clear() def test_resolve(self): urls = [('http://localhost:80/blah', 'http://127.0.0.1:80/blah'), ('https://localhost', 'https://localhost'), ('http://cantfind', 'http://cantfind'), ('https://google.com', 'https://google.com'), ('http://user:pass@localhost/blah?yeah=1#ok', 'http://user:pass@127.0.0.1/blah?yeah=1#ok'), ('http://tarek@localhost/blah', 'http://tarek@127.0.0.1/blah')] for url, wanted in urls: changed, original, resolved = resolve(url) self.assertEqual(changed, wanted, '%s vs %s' % (original, resolved)) def test_config(self): args = Args() expand_options(config, "test", args) self.assertEqual(args.duration, 1) def _get_config(self, data): data = json.dumps(data) data = StringIO(data) data.seek(0) return data def test_bad_config(self): args = Args() fd, badfile = mkstemp() os.close(fd) with open(badfile, 'w') as f: f.write("'1") try: self.assertRaises(OptionError, expand_options, badfile, '', args) finally: os.remove(badfile) self.assertRaises(OptionError, expand_options, 1, '', args) self.assertRaises(OptionError, expand_options, '', '', args) bad_data = [({}, 'test'), ({'molotov': {}}, 'test'), ({'molotov': {'tests': {}}}, 'test')] for data, scenario in bad_data: self.assertRaises(OptionError, expand_options, self._get_config(data), scenario, args) def test_setget_var(self): me = object() set_var('me', me) self.assertTrue(get_var('me') is me) def test_get_var_factory(self): me = object() def factory(): return me self.assertTrue(get_var('me', factory) is me) molotov-1.6/molotov/tests/test_listeners.py0000644000076500000240000000231513157010511021565 0ustar tarekstaff00000000000000from molotov.listeners import BaseListener, EventSender from molotov.tests.support import TestLoop, async_test, serialize class TestListeners(TestLoop): @async_test async def test_add_listener(self, loop, console, results): class MyListener(BaseListener): def __init__(self): self.fired = False self.value = None def on_my_event(self, **options): self.fired = True self.value = options["value"] listener = MyListener() eventer = EventSender(console) eventer.add_listener(listener) await eventer.send_event("my_event", value=42) await serialize(console) self.assertTrue(listener.fired) self.assertEqual(listener.value, 42) @async_test async def test_buggy_listener(self, loop, console, results): class MyListener(BaseListener): def on_my_event(self, **options): raise Exception("Bam") listener = MyListener() eventer = EventSender(console) eventer.add_listener(listener) await eventer.send_event("my_event") resp = await serialize(console) self.assertTrue("Bam" in resp) molotov-1.6/molotov/tests/example.py0000644000076500000240000000253313157012522020157 0ustar tarekstaff00000000000000""" This Molotov script has: - a global setup fixture that sets a global headers dict - an init worker fixture that sets the session headers - 3 scenario - 2 tear downs fixtures """ import json from molotov import scenario, setup, global_setup, global_teardown, teardown _API = 'http://localhost:8080' _HEADERS = {} # notice that the global setup, global teardown and teardown # are not a coroutine. @global_setup() def init_test(args): _HEADERS['SomeHeader'] = '1' @global_teardown() def end_test(): print("This is the end") @setup() async def init_worker(worker_num, args): headers = {'AnotherHeader': '1'} headers.update(_HEADERS) return {'headers': headers} @teardown() def end_worker(worker_num): print("This is the end for %d" % worker_num) @scenario(weight=40) async def scenario_one(session): async with session.get(_API) as resp: if session.statsd: session.statsd.incr('BLEH') res = await resp.json() assert res['result'] == 'OK' assert resp.status == 200 @scenario(weight=30) async def scenario_two(session): async with session.get(_API) as resp: assert resp.status == 200 @scenario(weight=30) async def scenario_three(session): somedata = json.dumps({'OK': 1}) async with session.post(_API, data=somedata) as resp: assert resp.status == 200 molotov-1.6/molotov/tests/test_fmwk.py0000644000076500000240000002534513260626031020536 0ustar tarekstaff00000000000000import os import signal from molotov.session import LoggedClientSession from molotov.runner import Runner from molotov.worker import Worker from molotov.util import json_request, request from molotov.api import (scenario, setup, global_setup, teardown, global_teardown, setup_session, teardown_session, scenario_picker, events) from molotov.tests.support import (TestLoop, async_test, dedicatedloop, serialize, catch_sleep, coserver) class TestFmwk(TestLoop): def get_worker(self, console, results, loop=None, args=None): statsd = None delay = 0 if args is None: args = self.get_args(console=console) return Worker(1, results, console, args, statsd=statsd, delay=delay, loop=loop) @async_test async def test_step(self, loop, console, results): res = [] @scenario(weight=0) async def test_one(session): res.append('1') @scenario(weight=100, delay=1.5) async def test_two(session): res.append('2') async def _slept(time): res.append(time) w = self.get_worker(console, results, loop=loop) with catch_sleep(res): async with LoggedClientSession(loop, console) as session: result = await w.step(0, session) self.assertTrue(result, 1) self.assertEqual(len(res), 2) self.assertEqual(res[1], 1.5) @async_test async def test_picker(self, loop, console, results): res = [] @scenario_picker() def picker(wid, sid): series = '_one', '_two', '_two', '_one' return series[sid] @scenario() async def _one(session): res.append('1') @scenario() async def _two(session): res.append('2') w = self.get_worker(console, results, loop=loop) for i in range(4): async with LoggedClientSession(loop, console) as session: await w.step(i, session) self.assertEqual(res, ['1', '2', '2', '1']) @async_test async def test_failing_step(self, loop, console, results): @scenario(weight=100) async def test_two(session): raise ValueError() w = self.get_worker(console, results, loop=loop) async with LoggedClientSession(loop, console) as session: result = await w.step(0, session) self.assertTrue(result, -1) @async_test async def test_aworker(self, loop, console, results): res = [] @setup() async def setuptest(num, args): res.append('0') @scenario(weight=50) async def test_one(session): pass @scenario(weight=100) async def test_two(session): pass args = self.get_args(console=console) w = self.get_worker(console, results, loop=loop, args=args) await w.run() self.assertTrue(results['OK'] > 0) self.assertEqual(results['FAILED'], 0) self.assertEqual(len(res), 1) def _runner(self, console, screen=None): res = [] _events = [] @events() async def _event(event, **data): _events.append(event) @global_setup() def init(args): res.append('SETUP') @setup_session() async def _session(wid, session): session.some = 1 res.append('SESSION') @setup() async def setuptest(num, args): res.append('0') @scenario(weight=50) async def test_one(session): async with session.get('http://localhost:8888') as resp: await resp.text() @scenario(weight=100) async def test_two(session): async with session.get('http://localhost:8888') as resp: await resp.text() @teardown_session() async def _teardown_session(wid, session): self.assertEqual(session.some, 1) res.append('SESSION_TEARDOWN') args = self.get_args() args.console = console args.verbose = 1 if not args.sizing: args.max_runs = 5 results = Runner(args)() self.assertTrue(results['OK'] > 0) self.assertEqual(results['FAILED'], 0) self.assertEqual(res, ['SETUP', '0', 'SESSION', 'SESSION_TEARDOWN']) self.assertTrue(len(_events) > 0) @dedicatedloop def test_runner(self): with coserver(): return self._runner(console=False) @dedicatedloop def test_runner_console(self): with coserver(): return self._runner(console=True) @dedicatedloop def _multiprocess(self, console, nosetup=False): res = [] if not nosetup: @setup() async def setuptest(num, args): res.append('0') @scenario(weight=50) async def test_one(session): pass @scenario(weight=100) async def test_two(session): pass args = self.get_args() args.processes = 2 args.workers = 5 args.console = console results = Runner(args)() self.assertTrue(results['OK'] > 0) self.assertEqual(results['FAILED'], 0) @dedicatedloop def test_runner_multiprocess_console(self): self._multiprocess(console=True) self._multiprocess(console=False, nosetup=True) @async_test async def test_aworker_noexc(self, loop, console, results): res = [] @setup() async def setuptest(num, args): res.append('0') @scenario(weight=50) async def test_one(session): pass @scenario(weight=100) async def test_two(session): pass args = self.get_args(console=console) args.exception = False w = self.get_worker(console, results, loop=loop, args=args) await w.run() self.assertTrue(results['OK'] > 0) self.assertEqual(results['FAILED'], 0) self.assertEqual(len(res), 1) @async_test async def test_setup_session_failure(self, loop, console, results): @setup_session() async def _setup_session(wid, session): json_request("http://NOPE") @scenario(weight=100) async def test_working(session): pass args = self.get_args(console=console) w = self.get_worker(console, results, loop=loop, args=args) await w.run() output = await serialize(console) expected = ("Name or service not known" in output or "nodename nor servname provided" in output) self.assertTrue(expected, output) @async_test async def test_setup_session_fresh_loop(self, loop, console, results): content = [] @setup_session() async def _setup_session(wid, session): with coserver(): html = str(request("http://localhost:8888")) content.append(html) @scenario(weight=100) async def test_working(session): pass args = self.get_args(console=console) w = self.get_worker(console, results, loop=loop, args=args) await w.run() self.assertTrue("Directory listing" in content[0]) @async_test async def test_failure(self, loop, console, results): @scenario(weight=100) async def test_failing(session): raise ValueError() args = self.get_args(console=console) w = self.get_worker(console, results, loop=loop, args=args) await w.run() self.assertTrue(results['OK'] == 0) self.assertTrue(results['FAILED'] > 0) @dedicatedloop def test_shutdown(self): res = [] @teardown() def _worker_teardown(num): res.append('BYE WORKER') @global_teardown() def _teardown(): res.append('BYE') @scenario(weight=100) async def test_two(session): os.kill(os.getpid(), signal.SIGTERM) args = self.get_args() results = Runner(args)() self.assertEqual(results['OK'], 1) self.assertEqual(results['FAILED'], 0) self.assertEqual(res, ['BYE WORKER', 'BYE']) @dedicatedloop def test_shutdown_exception(self): @teardown() def _worker_teardown(num): raise Exception('bleh') @global_teardown() def _teardown(): raise Exception('bleh') @scenario(weight=100) async def test_two(session): os.kill(os.getpid(), signal.SIGTERM) args = self.get_args() results = Runner(args)() self.assertEqual(results['OK'], 1) @async_test async def test_session_shutdown_exception(self, loop, console, results): @teardown_session() async def _teardown_session(wid, session): raise Exception('bleh') @scenario(weight=100) async def test_tds(session): pass args = self.get_args(console=console) w = self.get_worker(console, results, loop=loop, args=args) await w.run() output = await serialize(console) self.assertTrue("Exception" in output, output) self.assertEqual(results['FAILED'], 0) @dedicatedloop def test_setup_exception(self): @setup() async def _worker_setup(num, args): raise Exception('bleh') @scenario(weight=100) async def test_two(session): os.kill(os.getpid(), signal.SIGTERM) args = self.get_args() results = Runner(args)() self.assertEqual(results['OK'], 0) @dedicatedloop def test_global_setup_exception(self): @global_setup() def _setup(args): raise Exception('bleh') @scenario(weight=100) async def test_two(session): os.kill(os.getpid(), signal.SIGTERM) args = self.get_args() runner = Runner(args) self.assertRaises(Exception, runner) @dedicatedloop def test_teardown_exception(self): @teardown() def _teardown(args): raise Exception('bleh') @scenario(weight=100) async def test_two(session): os.kill(os.getpid(), signal.SIGTERM) args = self.get_args() results = Runner(args)() self.assertEqual(results['FAILED'], 0) @dedicatedloop def test_setup_not_dict(self): @setup() async def _worker_setup(num, args): return 1 @scenario(weight=100) async def test_two(session): os.kill(os.getpid(), signal.SIGTERM) args = self.get_args() results = Runner(args)() self.assertEqual(results['OK'], 0) molotov-1.6/molotov/tests/molotov.json0000644000076500000240000000150313136566742020557 0ustar tarekstaff00000000000000{ "molotov": { "tests": { "big": { "duration": 10, "env": { "SERVER_URL": "http://aserver.net" }, "exception": true, "processes": 10, "requirements": "requirements.txt", "scenario": "molotov/tests/example.py", "workers": 100 }, "scenario_two_once": { "exception": true, "max_runs": 1, "scenario": "molotov/tests/example.py", "single_mode": "scenario_two" }, "test": { "duration": 1, "exception": true, "scenario": "molotov/tests/example.py", "verbose": 1 } } } } molotov-1.6/molotov/tests/example8.py0000644000076500000240000000260413160162314020245 0ustar tarekstaff00000000000000""" This Molotov script uses events to generate a success/failure output """ import json import molotov import time _T = {} def _now(): return time.time() * 1000 @molotov.events() async def record_time(event, **info): if event == 'scenario_start': scenario = info['scenario'] index = (info['wid'], scenario['name']) _T[index] = _now() if event == 'scenario_success': scenario = info['scenario'] index = (info['wid'], scenario['name']) start_time = _T.pop(index, None) duration = int(_now() - start_time) if start_time is not None: print(json.dumps({ "ts": time.time(), "type": "scenario_success", "name": scenario['name'], "duration": duration, })) elif event == 'scenario_failure': scenario = info['scenario'] exception = info['exception'] index = (info['wid'], scenario['name']) start_time = _T.pop(index, None) duration = int(_now() - start_time) if start_time is not None: print(json.dumps({ "ts": time.time(), "type": "scenario_failure", "name": scenario['name'], "exception": exception.__class__.__name__, "errorMessage": str(exception), "duration": duration })) molotov-1.6/molotov/tests/statsd.py0000644000076500000240000000322413142071747020034 0ustar tarekstaff00000000000000import asyncio # taken from aiostatsd.tests.test_client class ServerProto: def __init__(self, received_queue): self.received_queue = received_queue self.transport = None def connection_made(self, transport): self.transport = transport def datagram_received(self, data, addr): self.received_queue.put_nowait(data) def disconnect(self): self.transport.close() def error_received(self, exc): raise Exception(exc) def connection_lost(self, exc): print(exc) class UDPServer(object): def __init__(self, host, port, loop=None): self.host = host self.port = port if loop is None: self.loop = asyncio.get_event_loop() else: self.loop = loop self._stop = asyncio.Future(loop=self.loop) self._done = asyncio.Future(loop=self.loop) self.incoming = asyncio.Queue(loop=self.loop) async def run(self): ctx = {} def make_proto(): proto = ServerProto(self.incoming) ctx['proto'] = proto return proto conn = self.loop.create_datagram_endpoint( make_proto, local_addr=(self.host, self.port) ) async def listen_for_stop(): await self._stop ctx['proto'].disconnect() await asyncio.gather(conn, listen_for_stop(), loop=self.loop) self._done.set_result(True) def flush(self): out = [] while not self.incoming.empty(): out.append(self.incoming.get_nowait()) return out async def stop(self): self._stop.set_result(True) await self._done molotov-1.6/molotov/tests/test_sharedconsole.py0000644000076500000240000000504713153510133022414 0ustar tarekstaff00000000000000import unittest import asyncio import sys import os import re import multiprocessing from molotov.sharedconsole import SharedConsole from molotov.tests.support import dedicatedloop, catch_output OUTPUT = """\ one two 3 TypeError\("unsupported operand type(.*)? TypeError\("unsupported operand type.*""" # pre-forked variable _CONSOLE = SharedConsole(interval=0.) _PROC = [] def run_worker(input): if os.getpid() not in _PROC: _PROC.append(os.getpid()) _CONSOLE.print("hello") try: 3 + "" except Exception: _CONSOLE.print_error("meh") with catch_output() as (stdout, stderr): loop = asyncio.new_event_loop() fut = asyncio.ensure_future(_CONSOLE.display(), loop=loop) loop.run_until_complete(fut) loop.close() stdout = stdout.read() assert stdout == '', stdout class TestSharedConsole(unittest.TestCase): @dedicatedloop def test_simple_usage(self): test_loop = asyncio.get_event_loop() console = SharedConsole(interval=0.) async def add_lines(): console.print("one") console.print("two") console.print("3") try: 1 + 'e' except Exception as e: console.print_error(e) console.print_error(e, sys.exc_info()[2]) await asyncio.sleep(.2) await console.stop() with catch_output() as (stdout, stderr): adder = asyncio.ensure_future(add_lines()) displayer = asyncio.ensure_future(console.display()) test_loop.run_until_complete(asyncio.gather(adder, displayer)) output = stdout.read() test_loop.close() self.assertTrue(re.match(OUTPUT, output, re.S | re.M) is not None, output) @dedicatedloop def test_multiprocess(self): test_loop = asyncio.get_event_loop() # now let's try with several processes pool = multiprocessing.Pool(3) try: inputs = [1] * 3 pool.map(run_worker, inputs) finally: pool.close() async def stop(): await asyncio.sleep(1) await _CONSOLE.stop() with catch_output() as (stdout, stderr): stop = asyncio.ensure_future(stop()) display = asyncio.ensure_future(_CONSOLE.display()) test_loop.run_until_complete(asyncio.gather(stop, display)) output = stdout.read() for pid in _PROC: self.assertTrue("[%d]" % pid in output) test_loop.close() molotov-1.6/molotov/tests/example3.py0000644000076500000240000000030213117752465020247 0ustar tarekstaff00000000000000from molotov import scenario, global_setup @global_setup() def init_test(args): raise Exception("BAM") @scenario(weight=100) async def fail(session): raise Exception("I am failing") molotov-1.6/molotov/tests/test_api.py0000644000076500000240000000422613153510133020332 0ustar tarekstaff00000000000000from molotov.api import pick_scenario, scenario, get_scenarios, setup from molotov.tests.support import TestLoop, async_test class TestUtil(TestLoop): def test_pick_scenario(self): @scenario(weight=10) async def _one(self): pass @scenario(weight=90) async def _two(self): pass picked = [pick_scenario()['name'] for i in range(100)] ones = len([f for f in picked if f == '_one']) self.assertTrue(ones < 20) @async_test async def test_can_call(self, loop, console, results): @setup() async def _setup(self): pass @scenario(weight=10) async def _one(self): pass # can still be called await _one(self) # same for fixtures await _setup(self) def test_default_weight(self): @scenario() async def _default_weight(self): pass self.assertEqual(len(get_scenarios()), 1) self.assertEqual(get_scenarios()[0]['weight'], 1) def test_no_scenario(self): @scenario(weight=0) async def _one(self): pass @scenario(weight=0) async def _two(self): pass self.assertEqual(get_scenarios(), []) def test_scenario_not_coroutine(self): try: @scenario(weight=1) def _one(self): pass except TypeError: return raise AssertionError("Should raise") def test_setup_not_coroutine(self): try: @setup() def _setup(self): pass @scenario(weight=90) async def _two(self): pass except TypeError: return raise AssertionError("Should raise") def test_two_fixtures(self): try: @setup() async def _setup(self): pass @setup() async def _setup2(self): pass @scenario(weight=90) async def _two(self): pass except ValueError: return raise AssertionError("Should raise") molotov-1.6/molotov/tests/example7.py0000644000076500000240000000115013157013122020235 0ustar tarekstaff00000000000000""" This Molotov script uses events to display concurrency info """ import molotov import time concurs = [] # [(timestamp, worker count)] def _now(): return time.time() * 1000 @molotov.events() async def record_time(event, **info): if event == 'current_workers': concurs.append((_now(), info['workers'])) @molotov.global_teardown() def display_average(): print("\nconcurrencies: %s", concurs) delta = max(ts for ts, _ in concurs) - min(ts for ts, _ in concurs) average = sum(value for _, value in concurs) * 1000 / delta print("\nAverage concurrency: %.2f VU/s" % average) molotov-1.6/molotov/tests/example6.py0000644000076500000240000000102013157013017020233 0ustar tarekstaff00000000000000""" This Molotov script show how you can print the average response time. """ import molotov import time _T = {} def _now(): return time.time() * 1000 @molotov.events() async def record_time(event, **info): req = info.get('request') if event == 'sending_request': _T[req] = _now() elif event == 'response_received': _T[req] = _now() - _T[req] @molotov.global_teardown() def display_average(): average = sum(_T.values()) / len(_T) print("\nAverage response time %dms" % average) molotov-1.6/molotov/tests/example2.py0000644000076500000240000000067013252157216020247 0ustar tarekstaff00000000000000""" This Molotov script has 2 scenario """ from molotov import scenario _API = 'http://localhost:8080' @scenario(weight=40) async def scenario_one(session): async with session.get(_API) as resp: res = await resp.json() assert res['result'] == 'OK' assert resp.status == 200 @scenario(weight=60) async def scenario_two(session): async with session.get(_API) as resp: assert resp.status == 200 molotov-1.6/molotov/session.py0000644000076500000240000000701013261445173017050 0ustar tarekstaff00000000000000import socket from urllib.parse import urlparse import asyncio from aiohttp.client import ClientSession, ClientRequest, ClientResponse from aiohttp import TCPConnector from molotov.util import resolve, IS_AIOHTTP2 from molotov.listeners import StdoutListener, EventSender _HOST = socket.gethostname() class LoggedClientRequest(ClientRequest): """Printable Request. """ session = None if IS_AIOHTTP2: def send(self, *args, **kw): if self.session: event = self.session.send_event('sending_request', request=self) asyncio.ensure_future(event) response = super(LoggedClientRequest, self).send(*args, **kw) response.request = self return response else: async def send(self, *args, **kw): if self.session: event = self.session.send_event('sending_request', request=self) asyncio.ensure_future(event) response = await super(LoggedClientRequest, self).send(*args, **kw) response.request = self return response class LoggedClientResponse(ClientResponse): request = None class LoggedClientSession(ClientSession): """Session with printable requests and responses. """ def __init__(self, loop, console, verbose=0, statsd=None, **kw): connector = kw.pop('connector', None) if connector is None: connector = TCPConnector(loop=loop, limit=None) super(LoggedClientSession, self).__init__(loop=loop, request_class=LoggedClientRequest, response_class=LoggedClientResponse, connector=connector, **kw) self.console = console self.request_class = LoggedClientRequest self.request_class.verbose = verbose self.verbose = verbose self.request_class.session = self self.request_class.response_class = LoggedClientResponse self.statsd = statsd self.eventer = EventSender(console, [StdoutListener(verbose=self.verbose, console=self.console)]) async def send_event(self, event, **options): await self.eventer.send_event(event, session=self, **options) def _dns_lookup(self, url): return resolve(url)[0] async def _request(self, *args, **kw): args = list(args) args[1] = self._dns_lookup(args[1]) args = tuple(args) req = super(LoggedClientSession, self)._request if self.statsd: prefix = 'molotov.%(hostname)s.%(method)s.%(host)s.%(path)s' meth, url = args[:2] url = urlparse(url) path = url.path != '' and url.path or '/' data = {'method': meth, 'hostname': _HOST, 'host': url.netloc.split(":")[0], 'path': path} label = prefix % data @self.statsd.timer(label) async def request(): resp = await req(*args, **kw) self.statsd.incr(label + '.' + str(resp.status)) return resp resp = await request() else: resp = await req(*args, **kw) await self.send_event('response_received', response=resp, request=resp.request) return resp molotov-1.6/molotov/__init__.py0000644000076500000240000000103413260630241017113 0ustar tarekstaff00000000000000try: from molotov.api import (scenario, setup, global_setup, teardown, # NOQA global_teardown, setup_session, # NOQA teardown_session, scenario_picker, # NOQA events) # NOQA from molotov.util import request, json_request # NOQA from molotov.util import set_var, get_var # NOQA except ImportError: pass # first import __version__ = '1.6' molotov-1.6/molotov/quickstart/0000755000076500000240000000000013261446760017212 5ustar tarekstaff00000000000000molotov-1.6/molotov/quickstart/loadtest.py0000644000076500000240000000374513142100245021373 0ustar tarekstaff00000000000000""" Molotov-based test. """ import json from molotov import scenario, setup, global_setup, teardown, global_teardown # This is the service you want to load test _API = 'http://localhost:8080' @global_setup() def test_starts(args): """ This functions is called before anything starts. Notice that it's not a coroutine. """ pass @setup() async def worker_starts(worker_id, args): """ This function is called once per worker. If it returns a mapping, it will be used with all requests. You can add things like Authorization headers for instance, by setting a "headers" key. """ headers = {'SomeHeader': '1'} return {'headers': headers} @teardown() def worker_ends(worker_id): """ This functions is called when the worker is done. Notice that it's not a coroutine. """ pass @global_teardown() def test_ends(): """ This functions is called when everything is done. Notice that it's not a coroutine. """ pass # each scenario has a weight. Molotov uses it to determine # how often the scenario is picked. @scenario(weight=40) async def scenario_one(session): async with session.get(_API) as resp: # if Molotov is called with --statsd # you will have a statsd client set into the session # you can use to add metrics if session.statsd: session.statsd.incr('BLEH') # when you read the body, don't forget to use await res = await resp.json() assert res['result'] == 'OK' assert resp.status == 200 # all scenarii are coroutines @scenario(weight=30) async def scenario_two(session): # a call to one of the session method should be awaited # see aiohttp.Client docs for more info on this async with session.get(_API) as resp: assert resp.status == 200 @scenario(weight=30) async def scenario_three(session): somedata = json.dumps({'OK': 1}) async with session.post(_API, data=somedata) as resp: assert resp.status == 200 molotov-1.6/molotov/quickstart/Makefile0000644000076500000240000000064013115767113020646 0ustar tarekstaff00000000000000HERE = $(shell pwd) PYTHON = python3 VTENV_OPTS = --python $(PYTHON) TESTNAME = smoke BIN = $(HERE)/venv/bin VENV_PIP = $(BIN)/pip3 VENV_PYTHON = $(BIN)/python INSTALL = $(VENV_PIP) install .PHONY: build install test $(VENV_PYTHON): virtualenv $(VTENV_OPTS) venv build: $(VENV_PYTHON) $(INSTALL) --upgrade git+https://github.com/loads/molotov.git test: build $(BIN)/molotov --config molotov.json $(TESTNAME) molotov-1.6/molotov/quickstart/__init__.py0000644000076500000240000000413713153510133021312 0ustar tarekstaff00000000000000import sys import shutil import os import argparse from molotov import __version__ _DEFAULTS = {'target_dir': '.'} _PREFIX = '> ' _HERE = os.path.dirname(__file__) class ValidationError(Exception): pass def _input(msg): return input(msg) # pragma: no cover def _prompt(text, default, validator=None): while True: try: res = _input(_PREFIX + '%s [%s]: ' % (text, default)) if not res and default: res = default if validator: res = validator(res) return res except ValidationError as e: print(e) def _yes(x): if x.upper() not in ('Y', 'YES', 'N', 'NO'): raise ValidationError("Please enter either 'y' or 'n'.") return x.upper() in ('Y', 'YES') def _parser(): parser = argparse.ArgumentParser(description='Quickstart') parser.add_argument('--version', action='store_true', default=False, help='Displays version and exits.') return parser def _copy_file(name, target_dir): print("…copying %r in %r" % (name, target_dir)) target = os.path.join(target_dir, name) if os.path.exists(target): print("%r already exists. Cowardly stopping here" % target) sys.exit(1) shutil.copyfile(os.path.join(_HERE, name), target) def main(): parser = _parser() args = parser.parse_args() if args.version: print(__version__) sys.exit(0) # XXX print('**** Molotov Quickstart ****') print('') print('Answer to a few questions to get started...') target_dir = _prompt("Target directory", '.') create_makefile = _prompt("Create Makefile", 'y', validator=_yes) print('Generating Molotov test...') if not os.path.exists(target_dir): os.makedirs(target_dir) if create_makefile: _copy_file('Makefile', target_dir) _copy_file('loadtest.py', target_dir) _copy_file('molotov.json', target_dir) print("") print("All done. Happy Breaking!") print("Go in %r" % target_dir) if create_makefile: print("Run 'make build' to get started...") molotov-1.6/molotov/quickstart/molotov.json0000644000076500000240000000070513051261173021574 0ustar tarekstaff00000000000000{ "molotov": { "tests": { "big": { "duration": 60, "processes": 10, "scenario": "loadtest.py", "workers": 100 }, "small": { "duration": 60, "scenario": "loadtest.py" }, "smoke": { "duration": 1, "scenario": "loadtest.py" } } } } molotov-1.6/molotov/api.py0000644000076500000240000001365213256125541016145 0ustar tarekstaff00000000000000import random import functools import asyncio _SCENARIO = {} def get_scenarios(): scenarios = list(_SCENARIO.items()) scenarios.sort() return [scenario for (name, scenario) in scenarios] def get_scenario(name): return _SCENARIO.get(name) def _check_coroutine(func): if not asyncio.iscoroutinefunction(func): raise TypeError('%s needs to be a coroutine' % str(func)) def scenario(weight=1, delay=0.0, name=None): """Decorator to register a function as a Molotov test. Options: - **weight** used by Molotov when the scenarii are randomly picked. The functions with the highest values are more likely to be picked. Integer, defaults to 1. This value is ignored when the *scenario_picker* decorator is used. - **delay** once the scenario is done, the worker will sleep *delay* seconds. Float, defaults to 0. The general --delay argument you can pass to Molotov will be summed with this delay. - **name** name of the scenario. If not provided, will use the function __name___ attribute. The decorated function receives an :class:`aiohttp.ClientSession` instance. """ def _scenario(func, *args, **kw): _check_coroutine(func) if weight > 0: sname = name or func.__name__ data = {'name': sname, 'weight': weight, 'delay': delay, 'func': func, 'args': args, 'kw': kw} _SCENARIO[sname] = data @functools.wraps(func) def __scenario(*args, **kw): return func(*args, **kw) return __scenario return _scenario def pick_scenario(worker_id=0, step_id=0): custom_picker = get_fixture('scenario_picker') if custom_picker is not None: name = custom_picker(worker_id, step_id) return get_scenario(name) scenarios = get_scenarios() total = sum(item['weight'] for item in scenarios) selection = random.uniform(0, total) upto = 0 for item in scenarios: weight = item['weight'] if upto + weight > selection: return item upto += weight def scenario_picker(): """Called to chose a scenario. Arguments received by the decorated function: - **worker_id** the worker number - **step_id** the loop counter The decorated function should return the name of the scenario the worker should execute next. When used, the weights are ignored. *The decorated function should not be a coroutine.* """ return _fixture('scenario_picker', coroutine=False) _FIXTURES = {} def get_fixture(name): return _FIXTURES.get(name) def _fixture(name, coroutine=True, multiple=False): def __fixture(func, *args, **kw): if coroutine: _check_coroutine(func) if name in _FIXTURES and not multiple: raise ValueError("You can't have two %r functions" % name) if multiple: if name in _FIXTURES: _FIXTURES[name].append(func) else: _FIXTURES[name] = [func] else: _FIXTURES[name] = func @functools.wraps(func) def ___fixture(*args, **kw): return func(*args, **kw) return ___fixture return __fixture def setup(): """Called once per worker startup. Arguments received by the decorated function: - **worker_id** the worker number - **args** arguments used to start Molotov. The decorated function can send back a dict. This dict will be passed to the :class:`aiohttp.ClientSession` class as keywords when it's created. This is useful when you need to set up session-wide options like Authorization headers, or do whatever you need on startup. *The decorated function should be a coroutine.* """ return _fixture('setup') def global_setup(): """Called once when the test starts. The decorated function is called before processes and workers are created. Arguments received by the decorated function: - **args** arguments used to start Molotov. This decorator is useful if you need to set up some fixtures that are shared by all workers. *The decorated function should not be a coroutine.* """ return _fixture('global_setup', coroutine=False) def teardown(): """Called when a worker is done. Arguments received by the decorated function: - **worker_id** the worker number *The decorated function should not be a coroutine.* """ return _fixture('teardown', coroutine=False) def global_teardown(): """Called when everything is done. *The decorated function should not be a coroutine.* """ return _fixture('global_teardown', coroutine=False) def setup_session(): """Called once per worker startup. Arguments received by the decorated function: - **worker_id** the worker number - **session** the :class:`aiohttp.ClientSession` instance created The function can attach extra attributes to the session and use **session.loop** if needed. It's a good place to attache an object that interacts with the event loop, so you are sure to use the same one that the session's. *The decorated function should be a coroutine.* """ return _fixture('setup_session') def teardown_session(): """Called once per worker when the session is closing. Arguments received by the decorated function: - **worker_id** the worker number - **session** the :class:`aiohttp.ClientSession` instance *The decorated function should be a coroutine.* """ return _fixture('teardown_session') def events(): """Called everytime Molotov sends an event Arguments received by the decorated function: - **event** Name of the event - extra argument(s) specific to the event *The decorated function should be a coroutine.* *IMPORTANT This function will directly impact the load test performances* """ return _fixture('events', multiple=True) molotov-1.6/molotov/slave.py0000644000076500000240000000705513162714137016507 0ustar tarekstaff00000000000000import json import os import sys import argparse import subprocess import tempfile import shutil import site import pkg_resources from molotov import __version__ from molotov.run import main as run, _parser def clone_repo(github): # XXX security subprocess.check_call('git clone %s .' % github, shell=True) def create_virtualenv(virtualenv, python): # XXX security subprocess.check_call('%s --python %s venv' % (virtualenv, python), shell=True) def install_reqs(reqfile): subprocess.check_call('./venv/bin/pip install -r %s' % reqfile, shell=True) def run_test(**options): """Runs a molotov test. """ parser = _parser() fields = {} cli = [] for action in parser._actions: if action.dest in ('help', 'scenario'): continue op_str = action.option_strings[0] fields[action.dest] = op_str, action.const, type(action) for key, value in options.items(): if key in fields: opt, const, type_ = fields[key] is_count = type_ is argparse._CountAction if const or is_count: if is_count: cli += [opt] * value else: cli.append(opt) else: cli.append(opt) cli.append(str(value)) cli.append(options.pop('scenario', 'loadtest.py')) args = parser.parse_args(args=cli) print('Running: molotov %s' % ' '.join(cli)) return run(args) def main(): """Moloslave clones a git repo and runs a molotov test """ parser = argparse.ArgumentParser(description='Github-based load test') parser.add_argument('--version', action='store_true', default=False, help='Displays version and exits.') parser.add_argument('--virtualenv', type=str, default='virtualenv', help='Virtualenv executable.') parser.add_argument('--python', type=str, default=sys.executable, help='Python executable.') parser.add_argument('--config', type=str, default='molotov.json', help='Path of the configuration file.') parser.add_argument('repo', help='Github repo', type=str, nargs="?") parser.add_argument('run', help='Test to run', nargs="?") args = parser.parse_args() if args.version: print(__version__) sys.exit(0) tempdir = tempfile.mkdtemp() curdir = os.getcwd() os.chdir(tempdir) print('Working directory is %s' % tempdir) try: clone_repo(args.repo) config_file = os.path.join(tempdir, args.config) with open(config_file) as f: config = json.loads(f.read()) # creating the virtualenv create_virtualenv(args.virtualenv, args.python) # install deps if 'requirements' in config['molotov']: install_reqs(config['molotov']['requirements']) # load deps into sys.path pyver = '%d.%d' % (sys.version_info.major, sys.version_info.minor) site_pkg = os.path.join(tempdir, 'venv', 'lib', 'python' + pyver, 'site-packages') site.addsitedir(site_pkg) pkg_resources.working_set.add_entry(site_pkg) # environment if 'env' in config['molotov']: for key, value in config['molotov']['env'].items(): os.environ[key] = value run_test(**config['molotov']['tests'][args.run]) except Exception: os.chdir(curdir) shutil.rmtree(tempdir, ignore_errors=True) raise molotov-1.6/molotov/stats.py0000644000076500000240000000017713140143117016517 0ustar tarekstaff00000000000000from aiomeasures import StatsD def get_statsd_client(address="udp://127.0.0.1:8125", **kw): return StatsD(address, **kw) molotov-1.6/molotov/sharedconsole.py0000644000076500000240000000412213153510133020204 0ustar tarekstaff00000000000000import sys import asyncio import multiprocessing import os from queue import Empty from molotov.util import cancellable_sleep, printable_error class SharedConsole(object): """Multi-process compatible stdout console. """ def __init__(self, interval=.1, max_lines_displayed=20): self._stream = multiprocessing.Queue() self._interval = interval self._stop = True self._creator = os.getpid() self._stop = False self._max_lines_displayed = max_lines_displayed async def stop(self): self._stop = True while True: try: sys.stdout.write(self._stream.get_nowait()) except Empty: break sys.stdout.flush() async def flush(self): sys.stdout.flush() await asyncio.sleep(0) async def display(self): if os.getpid() != self._creator: return while not self._stop: lines_displayed = 0 while True: try: line = self._stream.get_nowait() sys.stdout.write(line) lines_displayed += 1 except Empty: break if self._stop or lines_displayed > self._max_lines_displayed: break else: await asyncio.sleep(0) sys.stdout.flush() if not self._stop: await cancellable_sleep(self._interval) def print(self, line, end='\n'): if os.getpid() != self._creator: line = "[%d] %s" % (os.getpid(), line) line += end self._stream.put_nowait(line) def print_error(self, error, tb=None): for line in printable_error(error, tb): self.print(line) def print_block(self, start, callable, end='OK'): if os.getpid() != self._creator: prefix = "[%d] " % os.getpid() else: prefix = '' self._stream.put(prefix + start + '...\n') res = callable() self._stream.put(prefix + 'OK\n') return res molotov-1.6/setup.py0000644000076500000240000000243313261444314015026 0ustar tarekstaff00000000000000import sys from setuptools import setup, find_packages if sys.version_info < (3, 5): raise ValueError("Requires Python 3.5 or superior") from molotov import __version__ # NOQA install_requires = ['aiohttp', 'aiomeasures'] description = '' for file_ in ('README', 'CHANGELOG'): with open('%s.rst' % file_) as f: description += f.read() + '\n\n' classifiers = ["Programming Language :: Python", "License :: OSI Approved :: Apache Software License", "Development Status :: 5 - Production/Stable", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6"] setup(name='molotov', version=__version__, url='https://molotov.readthedocs.io', packages=find_packages(), long_description=description.strip(), description=("Spiffy load testing tool."), author="Tarek Ziade", author_email="tarek@ziade.org", include_package_data=True, zip_safe=False, classifiers=classifiers, install_requires=install_requires, entry_points=""" [console_scripts] molotov = molotov.run:main moloslave = molotov.slave:main molostart = molotov.quickstart:main """) molotov-1.6/docs-requirements.txt0000644000076500000240000000002013046124031017504 0ustar tarekstaff00000000000000sphinx-argparse molotov-1.6/molotov.json0000644000076500000240000000211213142055567015706 0ustar tarekstaff00000000000000{ "molotov": { "env": { "SERVER_URL": "http://aserver.net" }, "requirements": "requirements.txt", "tests": { "big": { "console": true, "duration": 10, "exception": true, "processes": 10, "scenario": "molotov/tests/example.py", "workers": 100 }, "fail": { "exception": true, "max_runs": 1, "scenario": "molotov/tests/example3.py" }, "scenario_two_once": { "console": true, "exception": true, "max_runs": 1, "scenario": "molotov/tests/example.py", "single_mode": "scenario_two" }, "test": { "console": true, "duration": 1, "exception": true, "verbose": 1, "console_update": 0, "scenario": "molotov/tests/example.py" } } } } molotov-1.6/tox.ini0000644000076500000240000000231513261372731014631 0ustar tarekstaff00000000000000[tox] downloadcache = {toxworkdir}/cache/ envlist = py36,py35,flake8,docs,pypy3,aiohttp2 [testenv] passenv = TRAVIS TRAVIS_JOB_ID TRAVIS_BRANCH deps = -rtox-requirements.txt -rrequirements.txt commands = pytest --random-order-bucket=global -sv --cov-report= --cov-config .coveragerc --cov molotov molotov/tests coverage combine coverage report -m - coveralls [testenv:py35] passenv = TRAVIS TRAVIS_JOB_ID TRAVIS_BRANCH deps = -rtox-requirements.txt -rrequirements.txt commands = pytest --random-order-bucket=global -sv molotov/tests [testenv:aiohttp2] basepython = python3.5 passenv = TRAVIS TRAVIS_JOB_ID TRAVIS_BRANCH deps = -rtox-requirements.txt -r351-requirements.txt commands = pytest --random-order-bucket=global -sv molotov/tests [testenv:pypy3] passenv = TRAVIS TRAVIS_JOB_ID TRAVIS_BRANCH deps = -rtox-pypy-requirements.txt -rrequirements.txt commands = pytest --random-order-bucket=global -sv molotov/tests [testenv:flake8] commands = flake8 molotov deps = flake8 [testenv:docs] basepython=python3.5 deps = -rrequirements.txt sphinx -rdocs-requirements.txt commands= sphinx-build -W -b html docs/source docs/build molotov-1.6/setup.cfg0000644000076500000240000000007313261446760015142 0ustar tarekstaff00000000000000[egg_info] tag_build = tag_date = 0 tag_svn_revision = 0 molotov-1.6/README.rst0000644000076500000240000000136013261444367015011 0ustar tarekstaff00000000000000======= molotov ======= .. image:: http://coveralls.io/repos/github/loads/molotov/badge.svg?branch=master :target: https://coveralls.io/github/loads/molotov?branch=master .. image:: http://travis-ci.org/loads/molotov.svg?branch=master :target: https://travis-ci.org/loads/molotov .. image:: http://readthedocs.org/projects/molotov/badge/?version=latest :target: https://molotov.readthedocs.io .. image:: https://img.shields.io/pypi/pyversions/molotov.svg :target: https://molotov.readthedocs.io Simple Python 3.5+ tool to write load tests. Based on `asyncio `_, Built with `aiohttp `_ 2.x or 3.x `Full Documentation `_ molotov-1.6/CHANGELOG.rst0000644000076500000240000000302213261446656015343 0ustar tarekstaff00000000000000CHANGELOG ========= 1.6 - 2018-04-05 ---------------- - works with aiohttp 2.x or 3.x so Python 3.5.1 can be used (#114) 1.5 - 2018-04-03 ---------------- - now runs on aiohttp 3.x (#109) - make sure we run a proper Python version (#9) - each process needs to have its own statsd client (#98) - fixed _run_in_fresh_loop and setup_session() error handling (#100) - Adde --fail (#105) - Added --force-shutdown (#107) - Make internet-based tests optional (#104) 1.4 - 2017-09-26 ---------------- - statsd: moved from aiostatsd to aiomeasures - Added --sizing and --sizing-tolerance (#72) - Refactored shared counters - Implemented a shared console (#42) - Improved shutdown process (#67) - Refactored fmwk.py (#25) - Add a way to record requests and responses (#80) - added --use-extension - added events - published tests/examples*.py to the docs (#90) 1.3 - 2017-07-28 ---------------- - fixed file-based requests with sessions -vvv option (#73) - proper managment of the verbose option in moloslave - added uvloop support (#68) - added initial PyPy support (#47) - Added name & @scenario_picker() options (#65) 1.2 - 2017-06-15 ---------------- - improved docs - added delay options (#48) - added --ramp-up option (#61) - fix a bug on response display (#62) 1.1 - 2017-06-09 ---------------- - added request and json_request helpers (#50) - added session setup and teardown fixtures (#52) - added set_var & get_var helpers (#54) - fixed thhe code generated by molostart (#55) 1.0 - 2017-03-23 ---------------- - Initial stable release molotov-1.6/.travis.yml0000644000076500000240000000122113153510133015410 0ustar tarekstaff00000000000000language: python python: 3.5 compiler: gcc matrix: include: - python: 3.6 env: - TOX_ENV=py36 addons: apt: packages: - pypy-dev - liblapack-dev env: - TOX_ENV=py35 - TOX_ENV=docs - TOX_ENV=flake8 - TOX_ENV=pypy3 install: - | if [[ "${TOX_ENV}" = pypy3 ]]; then rm -rf ~/.pyenv git clone https://github.com/yyuu/pyenv.git ~/.pyenv PYENV_ROOT="$HOME/.pyenv" PATH="$PYENV_ROOT/bin:$PATH" eval "$(pyenv init -)" pyenv install pypy3.5-5.8.0 pyenv global pypy3.5-5.8.0 fi - pip install tox script: - tox -e $TOX_ENV