diff --git a/Dockerfile.perftests b/Dockerfile.perftests deleted file mode 100644 index 26850aa624..0000000000 --- a/Dockerfile.perftests +++ /dev/null @@ -1,11 +0,0 @@ -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 diff --git a/requirements/perftests.txt b/requirements/perftests.txt deleted file mode 100644 index d0d01e96a2..0000000000 --- a/requirements/perftests.txt +++ /dev/null @@ -1,7 +0,0 @@ -# Required by locust, not using hashes for now. We'll use them once we can -# use a tagged locust release -pyzmq==25.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=locustio diff --git a/tests/performance/__init__.py b/tests/performance/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/performance/fixtures/small_webextension.xpi b/tests/performance/fixtures/small_webextension.xpi deleted file mode 100644 index 7b4e39ddaf..0000000000 Binary files a/tests/performance/fixtures/small_webextension.xpi and /dev/null differ diff --git a/tests/performance/helpers.py b/tests/performance/helpers.py deleted file mode 100644 index c3ba2d4e17..0000000000 --- a/tests/performance/helpers.py +++ /dev/null @@ -1,146 +0,0 @@ -import logging -import random -import collections -import os -import string -import re -import tempfile -import uuid -from contextlib import contextmanager -from shutil import make_archive, rmtree -from zipfile import ZipFile - -import lxml - -from fxa.constants import ENVIRONMENT_URLS -from fxa.core import Client -from fxa.tests.utils import TestEmailAccount - -NAME_REGEX = re.compile('THIS_IS_THE_NAME') - -root_path = os.path.dirname(__file__) -data_dir = os.path.join(root_path, 'fixtures') -xpis = [os.path.join(data_dir, xpi) for xpi in os.listdir(data_dir)] -log = logging.getLogger(__name__) - - -def get_random(): - return str(uuid.uuid4()) - - -def submit_url(step): - return f'/en-US/developers/addon/submit/{step}/' - - -def get_xpi(): - return uniqueify_xpi(random.choice(xpis)) - - -@contextmanager -def uniqueify_xpi(path): - output_dir = tempfile.mkdtemp() - try: - data_dir = os.path.join(output_dir, 'xpi') - output_path = os.path.join(output_dir, 'addon') - xpi_name = os.path.basename(path) - xpi_path = os.path.join(output_dir, xpi_name) - - with ZipFile(path) as original: - original.extractall(data_dir) - - with open(os.path.join(data_dir, 'manifest.json')) as f: - manifest_json = f.read() - - manifest_json = NAME_REGEX.sub(get_random(), manifest_json) - - with open(os.path.join(data_dir, 'manifest.json'), 'w') as f: - f.write(manifest_json) - - archive_path = make_archive(output_path, 'zip', data_dir) - os.rename(archive_path, xpi_path) - with open(xpi_path) as f: - yield f - finally: - rmtree(output_dir) - - -class EventMarker: - """ - Simple event marker that logs on every call. - """ - - def __init__(self, name): - self.name = name - - def _generate_log_message(self): - log.info(f'locust event: {self.name}') - - def __call__(self, *args, **kwargs): - self._generate_log_message() - - -def install_event_markers(): - # "import locust" within this scope so that this module is importable by - # code running in environments which do not have locust installed. - import locust - - # The locust logging format is not necessarily stable, so we use the event - # hooks API to implement our own "stable" logging for later programmatic - # reference. - - # The events are: - - # * locust_start_hatching - # * master_start_hatching - # * quitting - # * hatch_complete - - # install simple event markers - locust.events.locust_start_hatching += EventMarker('locust_start_hatching') - locust.events.master_start_hatching += EventMarker('master_start_hatching') - locust.events.quitting += EventMarker('quitting') - locust.events.hatch_complete += EventMarker('hatch_complete') - - -def get_fxa_client(): - fxa_env = os.getenv('FXA_ENV', 'stable') - return Client(ENVIRONMENT_URLS[fxa_env]['authentication']) - - -def get_fxa_account(): - fxa_client = get_fxa_client() - - account = TestEmailAccount() - password = ''.join([random.choice(string.ascii_letters) for i in range(8)]) - FxAccount = collections.namedtuple('FxAccount', 'email password') - fxa_account = FxAccount(email=account.email, password=password) - session = fxa_client.create_account(fxa_account.email, fxa_account.password) - account.fetch() - message = account.wait_for_email(lambda m: 'x-verify-code' in m['headers']) - session.verify_email_code(message['headers']['x-verify-code']) - return fxa_account, account - - -def destroy_fxa_account(fxa_account, email_account): - email_account.clear() - get_fxa_client().destroy_account(fxa_account.email, fxa_account.password) - - -def get_the_only_form_without_id(response_content): - """ - Gets the only form on the page that doesn't have an ID. - - A lot of pages (login, registration) have a single form without an ID. - This is the one we want. The other forms on the page have IDs so we - can ignore them. I'm sure this will break one day. - """ - html = lxml.html.fromstring(response_content) - target_form = None - for form in html.forms: - if not form.attrib.get('id'): - target_form = form - if target_form is None: - raise ValueError( - f'Could not find only one form without an ID; found: {html.forms}' - ) - return target_form diff --git a/tests/performance/locustfile.py b/tests/performance/locustfile.py deleted file mode 100644 index c3df32322d..0000000000 --- a/tests/performance/locustfile.py +++ /dev/null @@ -1,34 +0,0 @@ -import logging -import os -import sys -import time - -# 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__)))) -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'settings') -import olympia # noqa - -from locust import HttpLocust # noqa - -import tasks.user # noqa -import helpers # noqa - - -logging.Formatter.converter = time.gmtime - -log = logging.getLogger(__name__) -helpers.install_event_markers() - - -class WebsiteUser(HttpLocust): - 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 diff --git a/tests/performance/run-locust.sh b/tests/performance/run-locust.sh deleted file mode 100755 index e9348294b7..0000000000 --- a/tests/performance/run-locust.sh +++ /dev/null @@ -1,45 +0,0 @@ -#!/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} diff --git a/tests/performance/tasks/__init__.py b/tests/performance/tasks/__init__.py deleted file mode 100644 index 2e447466bc..0000000000 --- a/tests/performance/tasks/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from . import developer, user # noqa diff --git a/tests/performance/tasks/developer.py b/tests/performance/tasks/developer.py deleted file mode 100644 index 665676d951..0000000000 --- a/tests/performance/tasks/developer.py +++ /dev/null @@ -1,130 +0,0 @@ -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(f'form cannot be None; url={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(f'Unexpected status: {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=f'devhub.upload {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(f'Unexpected status: {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(f'Unexpected status: {response.status_code}') - gevent.sleep(1) - else: - response.failure( - f'Upload did not complete in {MAX_UPLOAD_POLL_ATTEMPTS} tries' - ) diff --git a/tests/performance/tasks/user.py b/tests/performance/tasks/user.py deleted file mode 100644 index 682e603783..0000000000 --- a/tests/performance/tasks/user.py +++ /dev/null @@ -1,271 +0,0 @@ -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(f'Created {self.fxa_account} for load-tests') - - def is_legacy_page(self, app): - return app in ('thunderbird', 'seamonkey') - - def get_app(self): - # Slightly weighted - app = random.choice(['firefox'] * 20 + ['thunderbird'] * 5 + ['seamonkey'] * 1) - return app - - def get_url(self, url, app): - # Only take a sub-set of languages, doesn't really matter only - # increases variance and may circumvent some caches here and there - user_language = random.choice( - ('af', 'de', 'dsb', 'en-US', 'hsb', 'ru', 'tr', 'zh-CN', 'zh-TW') - ) - - return url.format(app=app, language=user_language) - - def on_stop(self): - log.info(f'Cleaning up and destroying {self.fxa_account}') - helpers.destroy_fxa_account(self.fxa_account, self.email_account) - - def login(self, fxa_account): - log.info('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.info('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.info('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.info(f'Logging out {account}') - self.client.get('/en-US/firefox/users/logout/') - - -class UserTaskSet(BaseUserTaskSet): - def _browse_listing_and_click_detail( - self, - listing_url, - app, - 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, app), allow_redirects=False, catch_response=True - ) - - if (self.is_legacy_page(app) or force_legacy) and not legacy_selector: - log.warning( - '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(app) or force_legacy) - else legacy_selector - ) - collection_links = html.cssselect(selector) - - if not collection_links: - log.warning( - '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(app) or force_legacy: - name = name.replace(':app', ':legacy_app') - kwargs['name'] = name - - self.client.get(url, **kwargs) - response.success() - else: - response.failure(f'Unexpected status code {response.status_code}') - - @task(1) - def browse(self): - app = self.get_app() - self.client.get(self.get_url('/{language}/{app}/', app)) - - self._browse_listing_and_click_detail( - listing_url='/{language}/{app}/extensions/', - app=app, - detail_selector='a.SearchResult-link', - legacy_selector='.items .item.addon a', - ) - - @task(10) - def search(self): - app = self.get_app() - - term_choices = ('Spam', 'Privacy', 'Download') - term = random.choice(term_choices) - self.client.get( - self.get_url('/{language}/{app}/search/?platform=linux&q=' + term, app) - ) - - @task(6) - def browse_and_download_addon(self): - app = self.get_app() - - self._browse_listing_and_click_detail( - listing_url='/{language}/{app}/extensions/', - app=app, - detail_selector='a.SearchResult-link', - legacy_selector='.items .item.addon a', - ) - - @task(5) - def browse_collections(self): - app = self.get_app() - - # detail and legacy selector match both, themes and regular add-ons - self._browse_listing_and_click_detail( - listing_url='/{language}/{app}/', - app=app, - detail_selector='a.Home-SubjectShelf-link', - legacy_selector='.listing-grid .hovercard .summary>a', - ) - - @task(4) - def browse_categories(self): - app = self.get_app() - - self._browse_listing_and_click_detail( - listing_url='/{language}/{app}/extensions/', - app=app, - detail_selector='a.Categories-link', - legacy_selector='ul#side-categories li a', - ) - - @task(4) - def browse_reviews(self): - app = self.get_app() - - # 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, app) - ) - - @task(4) - def browse_theme_categories(self): - app = self.get_app() - self._browse_listing_and_click_detail( - listing_url='/{language}/{app}/complete-themes/', - app=app, - detail_selector=None, - legacy_selector='.listing-grid .hovercard>a', - name='/:lang/:app/complete-themes/:slug/', - force_legacy=True, - ) - - self._browse_listing_and_click_detail( - listing_url='/{language}/{app}/themes/', - app=app, - detail_selector='a.SearchResult-link', - legacy_selector='ul#side-categories li a', - name='/:lang/:app/themes/:slug/', - ) - - @task(3) - def test_user_profile(self): - app = self.get_app() - - # 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, app)) - - @task(2) - def test_rss_feeds(self): - app = self.get_app() - - 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', - # 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), app))