WIP: Cleanup locustio based performance / smoke tests, add more entities (#8358)
* Port mozilla/amo-loadtest to FXA authentication and WebExtensions. This is the first part of many to improve our load-tests and allow them to be run regularly as part of a regression test suite. This commit primarily ports the existing tests over to our repository, updates them to use FxA authentication and uses a WebExtension for the upload test. This only implements the baseline for more work to come in #7744 but it's important to have authentication and the basic infrastructure working correctly. It's also still using the legacy add-on detail and listing pages, that'll obviously change - or be only true for SeaMonkey and Thunderbird related tests since we still have to support them (very low priority though) It also adds a first step of a summary report that links to new-relic. That'll need a bit more tooling and testing but it worked quite well in first tests. Refs #7744 Next steps: * Implement most of the read-only tests from #7744 * Check what needs auth in #7744, implement it properly * Implement database and cache query logging * Implement a unified merged test-report that uses the database and cache query logging and merges it with locust logs (using our new unique request id) * Fix makefile, show dummy-usage for now. * Delete generate-summary script for now * Pin and cleanup dependencies * Reverse all urls * Use passed in account for login * split things up * Use gevent for waiting, some cleanups, add first version of Dockerfile, running script, disable developer stuff for now * Add browsing collections and browsing categories * Add loads more variation, add support for thunderbird, seamonkey, multiple languages * Decrease size of docker images for local testing * Get installation and docker image mostly to work, docs, cleanups * Allow fxa environment variables be overwritten * Hit legacy site of every variation * More explicit theme testing * Add browsing for app-versions and various rss feeds * More variation in rss feeds, add featured and search tools * Add a few more response.success() calls * Test user profile pages, warn for empty collection pages * More docs * Add review browsing
This commit is contained in:
Родитель
1edad2d1c7
Коммит
c1112d89a1
|
@ -1,3 +1,5 @@
|
|||
.git
|
||||
.cache
|
||||
.tox
|
||||
node_modules/
|
||||
logs/*
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
FROM python:2.7-alpine
|
||||
|
||||
COPY . /code
|
||||
WORKDIR /code
|
||||
|
||||
RUN apk --no-cache add --virtual=.build-dep build-base git \
|
||||
&& pip install --no-cache-dir -r /code/requirements/perftests.txt \
|
||||
&& apk del .build-dep \
|
||||
&& rm -f /tmp/* /etc/apk/cache/*
|
||||
|
||||
EXPOSE 8089 5557 5558
|
|
@ -1,29 +1,7 @@
|
|||
# Required by locust
|
||||
pyzmq==17.0.0 \
|
||||
--hash=sha256:0145ae59139b41f65e047a3a9ed11bbc36e37d5e96c64382fcdff911c4d8c3f0 \
|
||||
--hash=sha256:18de8a02768b1c0b3495ac635b24bd902fafc08befb70a6e68c4d343ccbd6cbd \
|
||||
--hash=sha256:2fb4d745ffe0a65ebf8fd29df093bb5c0ac96a506cb05b9a7b7c94b2524ae7f6 \
|
||||
--hash=sha256:4193cc666591495ab7fe8d24fa8374a35f9775f16dc7c46e03615559e1fc1855 \
|
||||
--hash=sha256:445fed4d71ac48da258ba38f2e29c88c5091124212a4004a0a6a42e6586a7de1 \
|
||||
--hash=sha256:538dfdd9542cf9ff37cd958da03b58d56b53b90800159ea07adc51a8ec7ffcb8 \
|
||||
--hash=sha256:613ac1fc4591b1c6a0a52ce3ed17dbffd6a17e985df504e8b4cdb987f97285b1 \
|
||||
--hash=sha256:630fb21f7474eb9e409a1ad476bf1ec489a69eb021172d422f2485cc3a44cd79 \
|
||||
--hash=sha256:6c3632d2c17cf03ce728ffaa328d45bb053623b3a0aa9747adcde81778d5a4d5 \
|
||||
--hash=sha256:767e1d0b1f7fff1950127abc08c5a5af2754987bc6480c6d641bed6971278a7a \
|
||||
--hash=sha256:863ec1bfa52da6eaa5c4aa59143eeaeb4ef7a076862407a548ec645f25e6d6df \
|
||||
--hash=sha256:a0ecf4c3eccd92f030a4e3e334b9da6fa3ee86be00249343c74e476d70567d0f \
|
||||
--hash=sha256:ad5a8b19b6671b52d30ccfc3a0f4c600e49c4e2dcc88caf4106ed5958dec8d5e \
|
||||
--hash=sha256:b31f2b50ad2920f21b904f5edf66bee324e42bb978df1407ecf381b210d4678e \
|
||||
--hash=sha256:b328c538061757f627d32f7f8885c16f1d2f59f5374e057822f3c8e6cd94c41b \
|
||||
--hash=sha256:b89268020a843d4c3cc04180577ec061fe96d35f267b0b672cb006e4d70560da \
|
||||
--hash=sha256:ba0b43aebf856e5e249250d74c1232d6600b6859328920d12e2ba72a565ab1b1 \
|
||||
--hash=sha256:bdb12b485b3440b5193cd337d27cc126cdfc54ea9f38df237e1ead6216435cbe \
|
||||
--hash=sha256:c30d27c9b35285597b8ef3019f97b9b98457b053f65dcc87a90dfdd4db09ca78 \
|
||||
--hash=sha256:d51eb3902d27d691483243707bfa67972167a70269bbbc172b74eeac4f780a1d \
|
||||
--hash=sha256:e5578ae84bb94e97adadfcb00106a1cb161cb8017f89b01f6c3737f356257811 \
|
||||
--hash=sha256:f35b4cdeffff79357a9d929daa2a8620fb362b2cbeebdc5dd2cf9fcd27c44821 \
|
||||
--hash=sha256:fb983aec4bddee3680a0b7395f99e4595d70d81841370da736c5dc642bad4cd2
|
||||
# Required by locust, not using hashes for now. We'll use them once we can
|
||||
# use a tagged locust release
|
||||
pyzmq==17.0.0
|
||||
|
||||
# We need this specific commit until there is a new locust release. Once this happens
|
||||
# This can be pinned to a specific version.
|
||||
-e git+https://github.com/locustio/locust@524ab5203ebc7c4c5c108b641773262ae53fbc40#egg=locust
|
||||
-e git+https://github.com/locustio/locust@524ab5203ebc7c4c5c108b641773262ae53fbc40#egg=locustio
|
||||
|
|
21
settings.py
21
settings.py
|
@ -98,9 +98,10 @@ CORS_ENDPOINT_OVERRIDES = cors_endpoint_overrides(
|
|||
# FxA config for local development only.
|
||||
FXA_CONFIG = {
|
||||
'default': {
|
||||
'client_id': 'f336377c014eacf0',
|
||||
'client_secret':
|
||||
'5a36054059674b09ea56709c85b862c388f2d493d735070868ae8f476e16a80d',
|
||||
'client_id': env('FXA_CLIENT_ID', default='f336377c014eacf0'),
|
||||
'client_secret': env(
|
||||
'FXA_CLIENT_SECRET',
|
||||
default='5a36054059674b09ea56709c85b862c388f2d493d735070868ae8f476e16a80d'), # noqa
|
||||
'content_host': 'https://stable.dev.lcip.org',
|
||||
'oauth_host': 'https://oauth-stable.dev.lcip.org/v1',
|
||||
'profile_host': 'https://stable.dev.lcip.org/profile/v1',
|
||||
|
@ -108,9 +109,10 @@ FXA_CONFIG = {
|
|||
'scope': 'profile',
|
||||
},
|
||||
'amo': {
|
||||
'client_id': '0f95f6474c24c1dc',
|
||||
'client_secret':
|
||||
'ca45e503a1b4ec9e2a3d4855d79849e098da18b7dfe42b6bc76dfed420fc1d38',
|
||||
'client_id': env('FXA_CLIENT_ID', default='0f95f6474c24c1dc'),
|
||||
'client_secret': env(
|
||||
'FXA_CLIENT_SECRET',
|
||||
default='ca45e503a1b4ec9e2a3d4855d79849e098da18b7dfe42b6bc76dfed420fc1d38'), # noqa
|
||||
'content_host': 'https://stable.dev.lcip.org',
|
||||
'oauth_host': 'https://oauth-stable.dev.lcip.org/v1',
|
||||
'profile_host': 'https://stable.dev.lcip.org/profile/v1',
|
||||
|
@ -118,9 +120,10 @@ FXA_CONFIG = {
|
|||
'scope': 'profile',
|
||||
},
|
||||
'local': {
|
||||
'client_id': '1778aef72d1adfb3',
|
||||
'client_secret':
|
||||
'3feebe3c009c1a0acdedd009f3530eae2b88859f430fa8bb951ea41f2f859b18',
|
||||
'client_id': env('FXA_CLIENT_ID', default='1778aef72d1adfb3'),
|
||||
'client_secret': env(
|
||||
'FXA_CLIENT_SECRET',
|
||||
default='3feebe3c009c1a0acdedd009f3530eae2b88859f430fa8bb951ea41f2f859b18'), # noqa
|
||||
'content_host': 'https://stable.dev.lcip.org',
|
||||
'oauth_host': 'https://oauth-stable.dev.lcip.org/v1',
|
||||
'profile_host': 'https://stable.dev.lcip.org/profile/v1',
|
||||
|
|
|
@ -29,7 +29,7 @@ def get_random():
|
|||
|
||||
|
||||
def submit_url(step):
|
||||
return '/en-US/developers/addon/submit/{step}'.format(step=step)
|
||||
return '/en-US/developers/addon/submit/{step}/'.format(step=step)
|
||||
|
||||
|
||||
def get_xpi():
|
||||
|
|
|
@ -2,211 +2,33 @@ import logging
|
|||
import os
|
||||
import sys
|
||||
import time
|
||||
import urlparse
|
||||
import random
|
||||
|
||||
# due to locust sys.path manipulation, we need to re-add the project root.
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(__file__))))
|
||||
|
||||
# Now we can load django settings
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'settings')
|
||||
import olympia # noqa
|
||||
from django.conf import settings # noqa
|
||||
from olympia.amo.urlresolvers import reverse
|
||||
|
||||
from locust import HttpLocust, TaskSet, task # noqa
|
||||
import lxml.html # noqa
|
||||
from lxml.html import submit_form # noqa
|
||||
from fxa import oauth as fxa_oauth # noqa
|
||||
from locust import HttpLocust # noqa
|
||||
|
||||
import tasks.user # noqa
|
||||
import helpers # noqa
|
||||
|
||||
from . import helpers # noqa
|
||||
|
||||
logging.Formatter.converter = time.gmtime
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
helpers.install_event_markers()
|
||||
|
||||
MAX_UPLOAD_POLL_ATTEMPTS = 200
|
||||
FXA_CONFIG = settings.FXA_CONFIG[settings.DEFAULT_FXA_CONFIG_NAME]
|
||||
|
||||
|
||||
class UserBehavior(TaskSet):
|
||||
|
||||
def on_start(self):
|
||||
self.fxa_account, self.email_account = helpers.get_fxa_account()
|
||||
|
||||
log.info(
|
||||
'Created {account} for load-tests'
|
||||
.format(account=self.fxa_account))
|
||||
|
||||
def on_stop(self):
|
||||
log.info(
|
||||
'Cleaning up and destroying {account}'
|
||||
.format(account=self.fxa_account))
|
||||
helpers.destroy_fxa_account(self.fxa_account, self.email_account)
|
||||
|
||||
def submit_form(self, form=None, url=None, extra_values=None):
|
||||
if form is None:
|
||||
raise ValueError('form cannot be None; url={}'.format(url))
|
||||
|
||||
def submit(method, form_action_url, values):
|
||||
values = dict(values)
|
||||
if 'csrfmiddlewaretoken' not in values:
|
||||
raise ValueError(
|
||||
'Possibly the wrong form. Could not find '
|
||||
'csrfmiddlewaretoken: {}'.format(repr(values)))
|
||||
|
||||
response = self.client.post(
|
||||
url or form_action_url, values,
|
||||
allow_redirects=False, catch_response=True)
|
||||
|
||||
if response.status_code not in (301, 302):
|
||||
# This probably means the form failed and is displaying
|
||||
# errors.
|
||||
response.failure(
|
||||
'Form submission did not redirect; status={}'
|
||||
.format(response.status_code))
|
||||
|
||||
return submit_form(form, open_http=submit, extra_values=extra_values)
|
||||
|
||||
def login(self, account):
|
||||
log.debug('creating fxa account')
|
||||
fxa_account, email_account = helpers.get_fxa_account()
|
||||
|
||||
log.debug('calling login/start to generate fxa_state')
|
||||
response = self.client.get(
|
||||
reverse('accounts.login_start'),
|
||||
allow_redirects=False)
|
||||
|
||||
params = dict(urlparse.parse_qsl(response.headers['Location']))
|
||||
fxa_state = params['state']
|
||||
|
||||
log.debug('Get browser id session token')
|
||||
fxa_session = helpers.get_fxa_client().login(
|
||||
email=fxa_account.email,
|
||||
password=fxa_account.password)
|
||||
|
||||
oauth_client = fxa_oauth.Client(
|
||||
client_id=FXA_CONFIG['client_id'],
|
||||
client_secret=FXA_CONFIG['client_secret'],
|
||||
server_url=FXA_CONFIG['oauth_host'])
|
||||
|
||||
log.debug('convert browser id session token into oauth code')
|
||||
oauth_code = oauth_client.authorize_code(fxa_session, scope='profile')
|
||||
|
||||
# Now authenticate the user, this will verify the user on the
|
||||
response = self.client.get(
|
||||
reverse('accounts.authenticate'),
|
||||
params={
|
||||
'state': fxa_state,
|
||||
'code': oauth_code,
|
||||
}
|
||||
)
|
||||
|
||||
def logout(self, account):
|
||||
log.debug('Logging out {}'.format(account))
|
||||
self.client.get(reverse('users.logout'))
|
||||
|
||||
def load_upload_form(self):
|
||||
url = helpers.submit_url('upload-unlisted')
|
||||
response = self.client.get(
|
||||
url, allow_redirects=False, catch_response=True)
|
||||
|
||||
if response.status_code == 200:
|
||||
html = lxml.html.fromstring(response.content)
|
||||
return html.get_element_by_id('create-addon')
|
||||
else:
|
||||
more_info = ''
|
||||
if response.status_code in (301, 302):
|
||||
more_info = ('Location: {}'
|
||||
.format(response.headers['Location']))
|
||||
response.failure('Unexpected status: {}; {}'
|
||||
.format(response.status_code, more_info))
|
||||
|
||||
def upload_addon(self, form):
|
||||
url = helpers.submit_url('upload-unlisted')
|
||||
csrfmiddlewaretoken = form.fields['csrfmiddlewaretoken']
|
||||
|
||||
with helpers.get_xpi() as addon_file:
|
||||
response = self.client.post(
|
||||
reverse('devhub.upload'),
|
||||
{'csrfmiddlewaretoken': csrfmiddlewaretoken},
|
||||
files={'upload': addon_file},
|
||||
name='devhub.upload {}'.format(
|
||||
os.path.basename(addon_file.name)),
|
||||
allow_redirects=False,
|
||||
catch_response=True)
|
||||
|
||||
if response.status_code == 302:
|
||||
poll_url = response.headers['location']
|
||||
upload_uuid = self.poll_upload_until_ready(poll_url)
|
||||
if upload_uuid:
|
||||
form.fields['upload'] = upload_uuid
|
||||
self.submit_form(form=form, url=url)
|
||||
else:
|
||||
response.failure('Unexpected status: {}'.format(
|
||||
response.status_code))
|
||||
|
||||
@task(1)
|
||||
def upload(self):
|
||||
self.login(self.fxa_account)
|
||||
|
||||
form = self.load_upload_form()
|
||||
if form:
|
||||
self.upload_addon(form)
|
||||
|
||||
self.logout(self.fxa_account)
|
||||
|
||||
@task(5)
|
||||
def browse(self):
|
||||
self.client.get(reverse('home'))
|
||||
|
||||
response = self.client.get(
|
||||
reverse('browse.extensions'),
|
||||
allow_redirects=False, catch_response=True)
|
||||
|
||||
if response.status_code == 200:
|
||||
html = lxml.html.fromstring(response.content)
|
||||
addon_links = html.cssselect('.item.addon h3 a')
|
||||
url = random.choice(addon_links).get('href')
|
||||
self.client.get(
|
||||
url,
|
||||
name=reverse('addons.detail', kwargs={'addon_id': ':slug'}))
|
||||
else:
|
||||
response.failure('Unexpected status code {}'.format(
|
||||
response.status_code))
|
||||
|
||||
def poll_upload_until_ready(self, url):
|
||||
for i in xrange(MAX_UPLOAD_POLL_ATTEMPTS):
|
||||
response = self.client.get(
|
||||
url, allow_redirects=False,
|
||||
name=reverse('devhub.upload_detail', args=(':uuid',)),
|
||||
catch_response=True)
|
||||
|
||||
try:
|
||||
data = response.json()
|
||||
except ValueError:
|
||||
return response.failure(
|
||||
'Failed to parse JSON when polling. '
|
||||
'Status: {} content: {}'.format(
|
||||
response.status_code, response.content))
|
||||
|
||||
if response.status_code == 200:
|
||||
if data['error']:
|
||||
return response.failure('Unexpected error: {}'.format(
|
||||
data['error']))
|
||||
elif data['validation']:
|
||||
return data['upload']
|
||||
else:
|
||||
return response.failure('Unexpected status: {}'.format(
|
||||
response.status_code))
|
||||
time.sleep(1)
|
||||
else:
|
||||
response.failure('Upload did not complete in {} tries'.format(
|
||||
MAX_UPLOAD_POLL_ATTEMPTS))
|
||||
|
||||
|
||||
class WebsiteUser(HttpLocust):
|
||||
task_set = UserBehavior
|
||||
min_wait = 5000
|
||||
max_wait = 9000
|
||||
weight = 1
|
||||
task_set = tasks.user.UserTaskSet
|
||||
min_wait = 120
|
||||
max_wait = 240
|
||||
|
||||
|
||||
# class Developer(HttpLocust):
|
||||
# weight = 10
|
||||
# task_set = tasks.developer.DeveloperTaskSet
|
||||
# min_wait = 120
|
||||
# max_wait = 240
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
#!/bin/sh
|
||||
# Example usage:
|
||||
#
|
||||
# $ docker build -t amoloadtests:latest -f Dockerfile.perftests
|
||||
# $ docker run -ti -e LOCUST_OPTS="-c 1 --no-web" \
|
||||
# -e ATTACKED_HOST="https://addons.allizom.org" \
|
||||
# amoloadtests:latest
|
||||
# /code/tests/performance/run-locust.sh
|
||||
#
|
||||
# If you're running this against addons.allizom.org make sure to configure
|
||||
# the correct FxA environment variables:
|
||||
#
|
||||
# -e FXA_CLIENT_ID="..." -e FXA_CLIENT_SECRET="..."
|
||||
#
|
||||
# To run this locally, use
|
||||
# $ LOCUST_OPTS="-c 3 --no-web" ATTACKED_HOST="https://addons.allizom.org" tests/performance/run-locust.sh
|
||||
|
||||
set -e
|
||||
LOCUST_MODE=${LOCUST_MODE:-standalone}
|
||||
LOCUST_MASTER_BIND_PORT=${LOCUST_MASTER_BIND_PORT:-5557}
|
||||
CURRENT_FOLDER=$(dirname $(realpath $0))
|
||||
DEFAULT_LOCUST_FILE="$CURRENT_FOLDER/locustfile.py"
|
||||
LOCUST_FILE=${LOCUST_FILE:-$DEFAULT_LOCUST_FILE}
|
||||
|
||||
if [ -z ${ATTACKED_HOST+x} ] ; then
|
||||
echo "You need to set the URL of the host to be tested (ATTACKED_HOST)."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
LOCUST_OPTS="-f ${LOCUST_FILE} --host=${ATTACKED_HOST} --no-reset-stats $LOCUST_OPTS"
|
||||
|
||||
case `echo ${LOCUST_MODE} | tr 'a-z' 'A-Z'` in
|
||||
"MASTER")
|
||||
LOCUST_OPTS="--master --master-bind-port=${LOCUST_MASTER_BIND_PORT} $LOCUST_OPTS"
|
||||
;;
|
||||
"SLAVE")
|
||||
LOCUST_OPTS="--slave --master-host=${LOCUST_MASTER} --master-port=${LOCUST_MASTER_BIND_PORT} $LOCUST_OPTS"
|
||||
if [ -z ${LOCUST_MASTER+x} ] ; then
|
||||
echo "You need to set LOCUST_MASTER."
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
locust ${LOCUST_OPTS}
|
|
@ -0,0 +1 @@
|
|||
from . import developer, user # noqa
|
|
@ -0,0 +1,126 @@
|
|||
import os
|
||||
import logging
|
||||
|
||||
import gevent
|
||||
from django.conf import settings
|
||||
from locust import task
|
||||
import lxml.html
|
||||
from lxml.html import submit_form
|
||||
|
||||
import helpers
|
||||
from .user import BaseUserTaskSet
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
MAX_UPLOAD_POLL_ATTEMPTS = 200
|
||||
FXA_CONFIG = settings.FXA_CONFIG[settings.DEFAULT_FXA_CONFIG_NAME]
|
||||
|
||||
|
||||
class DeveloperTaskSet(BaseUserTaskSet):
|
||||
|
||||
def submit_form(self, form=None, url=None, extra_values=None):
|
||||
if form is None:
|
||||
raise ValueError('form cannot be None; url={}'.format(url))
|
||||
|
||||
def submit(method, form_action_url, values):
|
||||
values = dict(values)
|
||||
if 'csrfmiddlewaretoken' not in values:
|
||||
raise ValueError(
|
||||
'Possibly the wrong form. Could not find '
|
||||
'csrfmiddlewaretoken: {}'.format(repr(values)))
|
||||
|
||||
response = self.client.post(
|
||||
url or form_action_url, values,
|
||||
allow_redirects=False, catch_response=True)
|
||||
|
||||
if response.status_code not in (301, 302):
|
||||
# This probably means the form failed and is displaying
|
||||
# errors.
|
||||
response.failure(
|
||||
'Form submission did not redirect; status={}'
|
||||
.format(response.status_code))
|
||||
|
||||
return submit_form(form, open_http=submit, extra_values=extra_values)
|
||||
|
||||
def load_upload_form(self):
|
||||
url = helpers.submit_url('upload-unlisted')
|
||||
response = self.client.get(
|
||||
url, allow_redirects=False, catch_response=True)
|
||||
|
||||
if response.status_code == 200:
|
||||
response.success()
|
||||
html = lxml.html.fromstring(response.content)
|
||||
return html.get_element_by_id('create-addon')
|
||||
else:
|
||||
more_info = ''
|
||||
if response.status_code in (301, 302):
|
||||
more_info = ('Location: {}'
|
||||
.format(response.headers['Location']))
|
||||
response.failure('Unexpected status: {}; {}'
|
||||
.format(response.status_code, more_info))
|
||||
|
||||
def upload_addon(self, form):
|
||||
url = helpers.submit_url('upload-unlisted')
|
||||
csrfmiddlewaretoken = form.fields['csrfmiddlewaretoken']
|
||||
|
||||
with helpers.get_xpi() as addon_file:
|
||||
response = self.client.post(
|
||||
'/en-US/developers/upload/',
|
||||
{'csrfmiddlewaretoken': csrfmiddlewaretoken},
|
||||
files={'upload': addon_file},
|
||||
name='devhub.upload {}'.format(
|
||||
os.path.basename(addon_file.name)),
|
||||
allow_redirects=False,
|
||||
catch_response=True)
|
||||
|
||||
if response.status_code == 302:
|
||||
poll_url = response.headers['location']
|
||||
upload_uuid = gevent.spawn(
|
||||
self.poll_upload_until_ready, poll_url
|
||||
).get()
|
||||
if upload_uuid:
|
||||
form.fields['upload'] = upload_uuid
|
||||
self.submit_form(form=form, url=url)
|
||||
else:
|
||||
response.failure('Unexpected status: {}'.format(
|
||||
response.status_code))
|
||||
|
||||
@task(1)
|
||||
def upload(self):
|
||||
self.login(self.fxa_account)
|
||||
|
||||
form = self.load_upload_form()
|
||||
if form:
|
||||
self.upload_addon(form)
|
||||
|
||||
self.logout(self.fxa_account)
|
||||
|
||||
def poll_upload_until_ready(self, url):
|
||||
for i in range(MAX_UPLOAD_POLL_ATTEMPTS):
|
||||
response = self.client.get(
|
||||
url, allow_redirects=False,
|
||||
name='/en-US/developers/upload/:uuid',
|
||||
catch_response=True)
|
||||
|
||||
try:
|
||||
data = response.json()
|
||||
except ValueError:
|
||||
return response.failure(
|
||||
'Failed to parse JSON when polling. '
|
||||
'Status: {} content: {}'.format(
|
||||
response.status_code, response.content))
|
||||
|
||||
if response.status_code == 200:
|
||||
if data['error']:
|
||||
return response.failure('Unexpected error: {}'.format(
|
||||
data['error']))
|
||||
elif data['validation']:
|
||||
response.success()
|
||||
return data['upload']
|
||||
else:
|
||||
return response.failure('Unexpected status: {}'.format(
|
||||
response.status_code))
|
||||
gevent.sleep(1)
|
||||
else:
|
||||
response.failure('Upload did not complete in {} tries'.format(
|
||||
MAX_UPLOAD_POLL_ATTEMPTS))
|
|
@ -0,0 +1,240 @@
|
|||
import logging
|
||||
import urlparse
|
||||
import random
|
||||
|
||||
from django.conf import settings
|
||||
from locust import TaskSet, task
|
||||
import lxml.html
|
||||
from fxa import oauth as fxa_oauth
|
||||
|
||||
import helpers
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
MAX_UPLOAD_POLL_ATTEMPTS = 200
|
||||
FXA_CONFIG = settings.FXA_CONFIG[settings.DEFAULT_FXA_CONFIG_NAME]
|
||||
|
||||
|
||||
class BaseUserTaskSet(TaskSet):
|
||||
|
||||
def on_start(self):
|
||||
self.fxa_account, self.email_account = helpers.get_fxa_account()
|
||||
|
||||
log.info(
|
||||
'Created {account} for load-tests'
|
||||
.format(account=self.fxa_account))
|
||||
|
||||
# Slightly weighted
|
||||
self.app = random.choice(
|
||||
['firefox'] * 20 +
|
||||
['thunderbird'] * 5 +
|
||||
['seamonkey'] * 1)
|
||||
|
||||
# Only take a sub-set of languages, doesn't really matter only
|
||||
# increases variance and may circumvent some caches here and there
|
||||
self.user_language = random.choice((
|
||||
'af', 'de', 'dsb', 'en-US', 'hsb', 'ru', 'tr', 'zh-CN', 'zh-TW'
|
||||
))
|
||||
|
||||
self.is_legacy_page = self.app in ('thunderbird', 'seamonkey')
|
||||
|
||||
def get_url(self, url):
|
||||
return url.format(app=self.app, language=self.user_language)
|
||||
|
||||
def on_stop(self):
|
||||
log.info(
|
||||
'Cleaning up and destroying {account}'
|
||||
.format(account=self.fxa_account))
|
||||
helpers.destroy_fxa_account(self.fxa_account, self.email_account)
|
||||
|
||||
def login(self, fxa_account):
|
||||
log.debug('calling login/start to generate fxa_state')
|
||||
response = self.client.get(
|
||||
'/api/v3/accounts/login/start/',
|
||||
allow_redirects=True)
|
||||
|
||||
params = dict(urlparse.parse_qsl(response.url))
|
||||
fxa_state = params['state']
|
||||
|
||||
log.debug('Get browser id session token')
|
||||
fxa_session = helpers.get_fxa_client().login(
|
||||
email=fxa_account.email,
|
||||
password=fxa_account.password)
|
||||
|
||||
oauth_client = fxa_oauth.Client(
|
||||
client_id=FXA_CONFIG['client_id'],
|
||||
client_secret=FXA_CONFIG['client_secret'],
|
||||
server_url=FXA_CONFIG['oauth_host'])
|
||||
|
||||
log.debug('convert browser id session token into oauth code')
|
||||
oauth_code = oauth_client.authorize_code(fxa_session, scope='profile')
|
||||
|
||||
# Now authenticate the user, this will verify the user on the server
|
||||
response = self.client.get(
|
||||
'/api/v3/accounts/authenticate/',
|
||||
params={
|
||||
'state': fxa_state,
|
||||
'code': oauth_code,
|
||||
},
|
||||
name='/api/v3/accounts/authenticate/?state=:state'
|
||||
)
|
||||
|
||||
def logout(self, account):
|
||||
log.debug('Logging out {}'.format(account))
|
||||
self.client.get('/en-US/firefox/users/logout/')
|
||||
|
||||
|
||||
class UserTaskSet(BaseUserTaskSet):
|
||||
def _browse_listing_and_click_detail(self, listing_url, detail_selector,
|
||||
legacy_selector=None, name=None,
|
||||
force_legacy=False):
|
||||
# TODO: This should hit pagination automatically if there is any
|
||||
response = self.client.get(
|
||||
self.get_url(listing_url),
|
||||
allow_redirects=False, catch_response=True)
|
||||
|
||||
if (self.is_legacy_page or force_legacy) and not legacy_selector:
|
||||
log.warn(
|
||||
'Received legacy url without legacy selector. {} :: {}'
|
||||
.format(listing_url, detail_selector))
|
||||
return
|
||||
|
||||
if response.status_code == 200:
|
||||
html = lxml.html.fromstring(response.content)
|
||||
selector = (
|
||||
detail_selector
|
||||
if not (self.is_legacy_page or force_legacy) else
|
||||
legacy_selector)
|
||||
collection_links = html.cssselect(selector)
|
||||
|
||||
if not collection_links:
|
||||
log.warn(
|
||||
'No selectable links on page. {} :: {}'
|
||||
.format(listing_url, selector))
|
||||
|
||||
url = random.choice(collection_links).get('href')
|
||||
|
||||
kwargs = {}
|
||||
if name is not None:
|
||||
if self.is_legacy_page or force_legacy:
|
||||
name = name.replace(':app', ':legacy_app')
|
||||
kwargs['name'] = name
|
||||
|
||||
self.client.get(url, **kwargs)
|
||||
response.success()
|
||||
else:
|
||||
response.failure('Unexpected status code {}'.format(
|
||||
response.status_code))
|
||||
|
||||
@task(8)
|
||||
def browse(self):
|
||||
self.client.get(self.get_url('/{language}/{app}/'))
|
||||
|
||||
self._browse_listing_and_click_detail(
|
||||
listing_url='/{language}/{app}/extensions/',
|
||||
detail_selector='a.SearchResult-link',
|
||||
legacy_selector='.items .item.addon a',
|
||||
name='/:lang/:app/addon/:slug')
|
||||
|
||||
@task(10)
|
||||
def search(self):
|
||||
term_choices = ('Spam', 'Privacy', 'Download')
|
||||
term = random.choice(term_choices)
|
||||
self.client.get(
|
||||
self.get_url(
|
||||
'/{language}/{app}/search/?platform=linux&q=' + term))
|
||||
|
||||
@task(6)
|
||||
def browse_and_download_addon(self):
|
||||
self._browse_listing_and_click_detail(
|
||||
listing_url='/{language}/{app}/extensions/',
|
||||
detail_selector='a.SearchResult-link',
|
||||
legacy_selector='.items .item.addon a',
|
||||
name='/:lang/:app/downloads/:file_id/')
|
||||
|
||||
@task(5)
|
||||
def browse_collections(self):
|
||||
# detail and legacy selector match both, themes and regular add-ons
|
||||
self._browse_listing_and_click_detail(
|
||||
listing_url='/{language}/{app}/',
|
||||
detail_selector='a.Home-SubjectShelf-link',
|
||||
legacy_selector='.listing-grid .hovercard .summary>a',
|
||||
name='/:lang/:app/addon/:slug')
|
||||
|
||||
@task(4)
|
||||
def browse_categories(self):
|
||||
self._browse_listing_and_click_detail(
|
||||
'/{language}/{app}/extensions/',
|
||||
detail_selector='a.Categories-link',
|
||||
legacy_selector='ul#side-categories li a',
|
||||
name='/:lang/:app/:extensions/:category_slug/')
|
||||
|
||||
@task(4)
|
||||
def browse_reviews(self):
|
||||
# TODO: Get add-ons more generalized by looking at collections
|
||||
# pages but for now that'll suffice.
|
||||
addons = (
|
||||
'grammarly-spell-checker', 'clip-to-onenote',
|
||||
'evernote-web-clipper', 'reader', 'fractal-summer-colors',
|
||||
'abstract-splash', 'colorful-fractal', 'tab-mix-plus')
|
||||
|
||||
for addon in addons:
|
||||
self.client.get(self.get_url(
|
||||
'/{language}/{app}/addon/%s/reviews/' % addon))
|
||||
|
||||
@task(4)
|
||||
def browse_theme_categories(self):
|
||||
self._browse_listing_and_click_detail(
|
||||
'/{language}/{app}/complete-themes/',
|
||||
detail_selector=None,
|
||||
legacy_selector='.listing-grid .hovercard>a',
|
||||
name='/:lang/:app/complete-themes/:slug/',
|
||||
force_legacy=True)
|
||||
|
||||
self._browse_listing_and_click_detail(
|
||||
'/{language}/{app}/themes/',
|
||||
detail_selector='a.SearchResult-link',
|
||||
legacy_selector='ul#side-categories li a',
|
||||
name='/:lang/:app/themes/:slug/')
|
||||
|
||||
@task(3)
|
||||
def test_user_profile(self):
|
||||
# TODO: Generalize by actually creating a user-profile and uploading
|
||||
# some data.
|
||||
usernames = (
|
||||
'giorgio-maone', 'wot-services', 'onemen', 'gary-reyes',
|
||||
'mozilla-labs5133025', 'gregglind',
|
||||
# Has many ratings
|
||||
'daveg')
|
||||
|
||||
for user in usernames:
|
||||
self.client.get(self.get_url(
|
||||
'/{language}/{app}/user/%s/' % user))
|
||||
|
||||
@task(2)
|
||||
def test_rss_feeds(self):
|
||||
urls = (
|
||||
# Add-on Category RSS Feed
|
||||
'/{language}/firefox/extensions/alerts-updates/format:rss',
|
||||
'/{language}/firefox/extensions/appearance/format:rss',
|
||||
'/{language}/firefox/extensions/bookmarks/format:rss',
|
||||
'/{language}/firefox/extensions/language-support/format:rss',
|
||||
|
||||
# App Version RSS Feed
|
||||
'/{language}/{app}/pages/appversions/format:rss',
|
||||
|
||||
# Collection RSS Feed
|
||||
'/{language}/firefox/collections/Vivre/ploaia/format:rss',
|
||||
|
||||
# Featured Add-ons
|
||||
'/{language}/{app}/featured/format:rss',
|
||||
|
||||
# Search tools RSS Feed
|
||||
'/{language}/{app}/search-tools/format:rss',
|
||||
)
|
||||
|
||||
self.client.get(self.get_url(random.choice(urls)))
|
||||
|
||||
@task(1)
|
||||
def test_browse_appversions(self):
|
||||
self.client.get(self.get_url('/{language}/{app}/pages/appversions/'))
|
Загрузка…
Ссылка в новой задаче