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:
Christopher Grebs 2018-05-30 16:46:16 +02:00 коммит произвёл GitHub
Родитель 1edad2d1c7
Коммит c1112d89a1
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
10 изменённых файлов: 457 добавлений и 229 удалений

Просмотреть файл

@ -1,3 +1,5 @@
.git
.cache
.tox
node_modules/
logs/*

11
Dockerfile.perftests Normal file
Просмотреть файл

@ -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

Просмотреть файл

@ -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

45
tests/performance/run-locust.sh Executable file
Просмотреть файл

@ -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/'))