-
Notifications
You must be signed in to change notification settings - Fork 20
Add Programs load test #28
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
[pep8] | ||
# E501: Line too long. | ||
# See: http://pep8.readthedocs.org/en/latest/intro.html#error-codes | ||
ignore=E501 | ||
exclude=.git,.pycharm_helpers |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,28 +1,55 @@ | ||
This project is the single source for public load tests for edX software component. New tests should be developed here unless there is a very good reason why that cannot be the case. Old tests should be scrubbed and moved here over time. | ||
edX Load Tests |Travis|_ | ||
========================= | ||
.. |Travis| image:: https://travis-ci.org/edx/edx-load-tests.svg?branch=master | ||
.. _Travis: https://travis-ci.org/edx/edx-load-tests | ||
|
||
This repository is home to public load tests for edX software components. New tests should be developed here. Old tests should be scrubbed and moved here over time. | ||
|
||
Installation | ||
------------ | ||
Getting Started | ||
--------------- | ||
|
||
mkvirtualenv edx-load-tests | ||
pip install -r locust/requirements.txt | ||
cd locust/$TEST_DIR | ||
locust --host="http://localhost" -f $csm | ||
If you have not already done so, create and activate a `virtualenv <https://virtualenvwrapper.readthedocs.org/en/latest/>`_. Unless otherwise stated, assume all commands below are executed within said virtualenv. | ||
|
||
Layout | ||
------ | ||
Next, install load testing requirements. | ||
|
||
Each set of tasks should be captured as a top-level locustfile named | ||
for the particular set of endpoints being tested. This toplevel file | ||
can be a flat python file (lms.py) or a directory (csm/). In the case | ||
of a directory, the __init__.py file should have (or import) the Locust | ||
subclass that defines the test. | ||
.. code-block:: bash | ||
|
||
$ pip install -r requirements.txt | ||
|
||
Start Locust by providing the Locust CLI with a target host and pointing it to the location of your desired locustfile. For example, | ||
|
||
.. code-block:: bash | ||
|
||
$ locust --host=http://localhost:8009 -f programs | ||
|
||
Repository Structure | ||
-------------------- | ||
|
||
Tests are organized into top-level packages. For examples, see ``csm`` and ``programs``. A module called ``locustfile.py`` is included inside each test package, within which a subclass of the `Locust class <http://docs.locust.io/en/latest/writing-a-locustfile.html#the-locust-class>`_ is defined. This subclass is imported into the test package's ``__init__.py`` to facilitate discovery at runtime. | ||
|
||
License | ||
------- | ||
|
||
The code in this repository is licensed under version 3 of the AGPL | ||
unless otherwise noted. Please see the `LICENSE`_ file for details. | ||
The code in this repository is licensed under the AGPLv3 unless otherwise noted. Please see `LICENSE.txt <https://github.com/edx/edx-load-tests/blob/master/LICENSE.txt>`_ for details. | ||
|
||
How To Contribute | ||
----------------- | ||
|
||
Contributions are very welcome. | ||
|
||
Please read `How To Contribute <https://github.com/edx/edx-platform/blob/master/CONTRIBUTING.rst>`_ for details. | ||
|
||
Even though they were written with ``edx-platform`` in mind, the guidelines | ||
should be followed for Open edX code in general. | ||
|
||
Reporting Security Issues | ||
------------------------- | ||
|
||
Please do not report security issues in public. Please email security@edx.org. | ||
|
||
Mailing List and IRC Channel | ||
---------------------------- | ||
|
||
.. _LICENSE: https://github.com/edx/edx-load-tests/blob/master/LICENSE | ||
You can discuss this code in the `edx-code Google Group`__ or in the ``#edx-code`` IRC channel on Freenode. | ||
|
||
__ https://groups.google.com/forum/#!forum/edx-code |
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
"""Pair Locust and Slumber to allow easier load testing of REST APIs. | ||
|
||
See: http://www.renzolucioni.com/pairing-locust-and-slumber/. | ||
""" | ||
from edx_rest_api_client import exceptions | ||
from edx_rest_api_client.client import EdxRestApiClient | ||
import requests | ||
import slumber | ||
|
||
|
||
class LocustResource(slumber.Resource): | ||
"""Custom Slumber Resource which takes advantage of Locust's extended HttpSession.""" | ||
def _request(self, method, data=None, files=None, params=None): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. optional: add a **kwargs to the signature, to insulate from errors in case an update of slumber modifies the signature of this non-public method. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @jimabramson instead of silencing that kind of error, wouldn't we prefer this to fail loudly so we can investigate the change to Slumber and modify this accordingly? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @rlucioni actually since this isn't a user-facing system I agree, that makes more sense. |
||
serializer = self._store['serializer'] | ||
url = self.url() | ||
|
||
headers = {'accept': serializer.get_content_type()} | ||
|
||
if not files: | ||
headers['content-type'] = serializer.get_content_type() | ||
if data is not None: | ||
data = serializer.dumps(data) | ||
|
||
# An optional argument that can be used to specify a label to use in Locust's statistics | ||
# instead of the actual URL. Can be used to group requests to the same API endpoint that vary | ||
# only by resource ID included in the URL into a single entry in Locust's statistics. For example, | ||
# requests to | ||
# 'http://localhost:8002/api/v1/resource/1/' | ||
# and | ||
# 'http://localhost:8002/api/v1/resource/2/' | ||
# might be grouped under the name: | ||
# '/api/v1/resource/:id/' | ||
# See: http://docs.locust.io/en/latest/api.html#httpsession-class/. | ||
name = params.pop('name', None) | ||
|
||
resp = self._store['session'].request( | ||
method, | ||
url, | ||
data=data, | ||
params=params, | ||
files=files, | ||
headers=headers, | ||
name=name | ||
) | ||
|
||
if 400 <= resp.status_code <= 499: | ||
exception_class = exceptions.HttpNotFoundError if resp.status_code == 404 else exceptions.HttpClientError | ||
raise exception_class('Client Error %s: %s' % (resp.status_code, url), response=resp, content=resp.content) | ||
elif 500 <= resp.status_code <= 599: | ||
raise exceptions.HttpServerError('Server Error %s: %s' % (resp.status_code, url), response=resp, content=resp.content) | ||
|
||
self._ = resp | ||
|
||
return resp | ||
|
||
|
||
class LocustEdxRestApiClient(EdxRestApiClient): | ||
resource_class = LocustResource |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
Programs Load Testing | ||
===================== | ||
|
||
This directory contains Locust tasks designed to exercise the `edX Programs Service <https://github.com/edx/programs>`_. | ||
|
||
Getting Started | ||
--------------- | ||
|
||
At this point, these load tests only require a running instance of the Programs service. | ||
|
||
Configuration | ||
------------- | ||
|
||
The load tests rely on configuration which can be specified using environment variables. | ||
|
||
==================== ========= =========================================== | ||
Variable Required? Description | ||
==================== ========= =========================================== | ||
PROGRAMS_SERVICE_URL No URL root for the Programs service | ||
PROGRAMS_API_URL No URL root for the Programs API | ||
JWT_AUDIENCE No JWT audience claim (aud) | ||
JWT_ISSUER No JWT issuer claim (iss) | ||
JWT_EXPIRATION_DELTA No Number of days before generated JWTs expire | ||
JWT_SECRET_KEY Yes Secret key used to sign JWTs | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. in practice this will be the oauth2 client secret key There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. True, but the Programs settings still require setting a value for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ok. |
||
==================== ========= =========================================== | ||
|
||
If you want to use the defaults provided for these variables, make sure that these defaults are configured for your instance of the Programs service. | ||
|
||
Running | ||
------- | ||
|
||
You can run the Programs load tests from the top level ``edx-load-tests`` directory by executing something like the following: | ||
|
||
.. code-block:: bash | ||
|
||
$ JWT_SECRET_KEY=replace-me locust --host=http://localhost:8009/ -f programs | ||
|
||
As of this writing, there is a bug in Locust preventing tests from accessing hosts using SSL. This is attributable to an `issue with gevent <https://github.com/gevent/gevent/issues/477>`_ that appears to have been fixed in 1.0.2. However, Locust still `requires <https://github.com/locustio/locust/blob/master/setup.py#L50>`_ the broken 1.0.1. To get around this, use ``http`` as the protocol in the host URL, not ``https``. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
from programs.locustfile import ProgramsUser |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
"""Configuration for Programs load testing.""" | ||
import os | ||
|
||
|
||
# URL CONFIGURATION | ||
PROGRAMS_SERVICE_URL = os.environ.get( | ||
'PROGRAMS_SERVICE_URL', | ||
'http://localhost:8004' | ||
).strip('/') | ||
|
||
PROGRAMS_API_URL = os.environ.get( | ||
'PROGRAMS_API_URL', | ||
'{}/api/v1/'.format(PROGRAMS_SERVICE_URL) | ||
) | ||
# END URL CONFIGURATION | ||
|
||
|
||
# JWT CONFIGURATION | ||
JWT_AUDIENCE = os.environ.get('JWT_AUDIENCE', 'replace-me') | ||
JWT_ISSUER = os.environ.get('JWT_ISSUER', 'http://127.0.0.1:8000/oauth2') | ||
JWT_EXPIRATION_DELTA = int(os.environ.get('JWT_EXPIRATION_DELTA', 1)) | ||
JWT_SECRET_KEY = os.environ.get('JWT_SECRET_KEY') | ||
|
||
if not JWT_SECRET_KEY: | ||
raise RuntimeError('A JWT secret key is required to run Programs load tests.') | ||
# END JWT CONFIGURATION |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
import datetime | ||
import uuid | ||
|
||
import jwt | ||
from locust import TaskSet, task, HttpLocust | ||
from locust.clients import HttpSession | ||
from locust.exception import LocustError | ||
|
||
from helpers.api import LocustEdxRestApiClient | ||
from programs.config import PROGRAMS_API_URL, JWT_AUDIENCE, JWT_ISSUER, JWT_EXPIRATION_DELTA, JWT_SECRET_KEY | ||
|
||
|
||
class ProgramsTaskSet(TaskSet): | ||
"""Tasks exercising Programs functionality.""" | ||
@task | ||
def list_programs(self): | ||
self.client.programs.get() | ||
|
||
|
||
class ProgramsUser(HttpLocust): | ||
"""Representation of an HTTP "user" to be hatched. | ||
|
||
Hatched users will be used to attack the system being load tested. This class | ||
defines which TaskSet class should define the user's behavior and how long a simulated | ||
user should wait between executing tasks. This class also provides a custom client used | ||
to interface with edX REST APIs. | ||
""" | ||
USERNAME_PREFIX = 'load-test-' | ||
|
||
task_set = ProgramsTaskSet | ||
min_wait = 3 * 1000 | ||
max_wait = 5 * 1000 | ||
|
||
def __init__(self): | ||
super(ProgramsUser, self).__init__() | ||
|
||
if not self.host: | ||
raise LocustError( | ||
'You must specify a base host, either in the host attribute in the Locust class, ' | ||
'or on the command line using the --host option.' | ||
) | ||
|
||
self.client = LocustEdxRestApiClient( | ||
PROGRAMS_API_URL, | ||
session=HttpSession(base_url=self.host), | ||
jwt=self._get_token() | ||
) | ||
|
||
def _get_token(self): | ||
payload = { | ||
'preferred_username': self.USERNAME_PREFIX + str(uuid.uuid4()), | ||
'iss': JWT_ISSUER, | ||
'aud': JWT_AUDIENCE, | ||
'exp': datetime.datetime.utcnow() + datetime.timedelta(days=JWT_EXPIRATION_DELTA), | ||
} | ||
|
||
return jwt.encode(payload, JWT_SECRET_KEY) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1 @@ | ||
# Packages needed to run any loadtest in this repo | ||
|
||
-e git+https://github.com/edx/locust.git@edx#egg=locustio[scipy] | ||
-e . | ||
jupyter | ||
runipy | ||
seaborn | ||
-r requirements/dev.txt |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
# Packages required to run load tests | ||
-e git+https://github.com/edx/locust.git@edx#egg=locustio[scipy] | ||
edx-rest-api-client==1.2.1 | ||
jupyter | ||
runipy | ||
seaborn |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
# Packages required to develop load tests | ||
-r base.txt | ||
|
||
pep8==1.6.2 |
This file was deleted.
This file was deleted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for doing this cleanup.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No problem.