performance.py
389 lines
| 10.8 KiB
| text/x-python
|
PythonLexer
r1 | # -*- coding: utf-8 -*- | |||
# Copyright (C) 2010-2016 RhodeCode GmbH | ||||
# | ||||
# This program is free software: you can redistribute it and/or modify | ||||
# it under the terms of the GNU Affero General Public License, version 3 | ||||
# (only), as published by the Free Software Foundation. | ||||
# | ||||
# This program is distributed in the hope that it will be useful, | ||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||||
# GNU General Public License for more details. | ||||
# | ||||
# You should have received a copy of the GNU Affero General Public License | ||||
# along with this program. If not, see <http://www.gnu.org/licenses/>. | ||||
# | ||||
# This program is dual-licensed. If you wish to learn more about the | ||||
# RhodeCode Enterprise Edition, including its added features, Support services, | ||||
# and proprietary license terms, please see https://rhodecode.com/licenses/ | ||||
""" | ||||
This is a locustio based test scenario simulating users on the web interface. | ||||
To run it:: | ||||
locust -f rhodecode/tests/load/performance.py --no-web -c 5 -r 5 | ||||
Discover details regarding the command with the ``--help`` parameter and the | ||||
documentation at http://docs.locust.io/en/latest/ . | ||||
With the environment variable `SEED` it is possible to create more or less | ||||
repeatable runs. They are not fully repeatable, since the response times from | ||||
the server vary and so two test runs with the same seed value will sooner or | ||||
later run out of sync. | ||||
""" | ||||
import logging | ||||
import os | ||||
import random | ||||
import re | ||||
from locust import HttpLocust, TaskSet, task | ||||
log = logging.getLogger('performance') | ||||
# List of repositories which shall be used during the test run | ||||
REPOSITORIES = [ | ||||
'vcs_test_git', | ||||
'vcs_test_hg', | ||||
'vcs_test_svn', | ||||
# 'linux', | ||||
# 'cpython', | ||||
# 'git', | ||||
] | ||||
HOST = 'http://localhost:5000' | ||||
# List of user accounts to login to the test system. | ||||
# Format: tuple(username, password) | ||||
USER_ACCOUNTS = [ | ||||
('test_admin', 'test12'), | ||||
] | ||||
# Toggle this to show and aggregate results on individual URLs | ||||
SHOW_DETAILS = False | ||||
# Used to initialize pseudo random generators which are used to select | ||||
# the next task, to take a decision etc. | ||||
_seed = os.environ.get('SEED', None) | ||||
try: | ||||
_seed = int(_seed) | ||||
except ValueError: | ||||
pass | ||||
if (isinstance(_seed, basestring) and | ||||
os.environ.get('PYTHONHASHSEED', None) == 'random'): | ||||
print("Setting a SEED does not work if PYTHONHASHSEED is set to random.") | ||||
print("Use a numeric value for SEED or unset PYTHONHASHSEED.") | ||||
exit(1) | ||||
log.info("Using SEED %s", _seed) | ||||
_seeder = random.Random(_seed) | ||||
class BaseTaskSet(TaskSet): | ||||
_decision = None | ||||
_random = None | ||||
def __init__(self, parent): | ||||
super(BaseTaskSet, self).__init__(parent) | ||||
log_name = "user.%s" % (id(self.locust), ) | ||||
self.log = log.getChild(log_name) | ||||
@property | ||||
def decision(self): | ||||
if self._decision: | ||||
return self._decision | ||||
return self.parent.decision | ||||
@property | ||||
def random(self): | ||||
if self._random: | ||||
return self._random | ||||
return self.parent.random | ||||
def get_next_task(self): | ||||
task = self.random.choice(self.tasks) | ||||
self.log.debug("next task %s", task.__name__) | ||||
return task | ||||
def wait(self): | ||||
millis = self.random.randint(self.min_wait, self.max_wait) | ||||
seconds = millis / 1000.0 | ||||
self.log.debug("sleeping %s", seconds) | ||||
self._sleep(seconds) | ||||
class BrowseLatestChanges(BaseTaskSet): | ||||
""" | ||||
User browsing through the latest changes form the summary page | ||||
""" | ||||
page_size = 10 | ||||
def on_start(self): | ||||
self.repo = self.decision.choose_repository() | ||||
self.page = 1 | ||||
self.response = self.client.get('/%s' % (self.repo, )) | ||||
@task(20) | ||||
def goto_next_page(self): | ||||
self.page += 1 | ||||
self.get_changelog_summary() | ||||
@task(5) | ||||
def goto_prev_page(self): | ||||
self.page -= 1 | ||||
if self.page < 0: | ||||
self.interrupt() | ||||
self.get_changelog_summary() | ||||
@task(20) | ||||
def open_commit(self): | ||||
commits = get_commit_links(self.response.content) | ||||
commit = self.decision.choose(commits, "commit") | ||||
if not commit: | ||||
return | ||||
self.client.get( | ||||
commit, | ||||
name=self._commit_name(commit)) | ||||
def get_changelog_summary(self): | ||||
summary = '/%s?size=%s&page=%s' % ( | ||||
self.repo, self.page_size, self.page) | ||||
self.response = self.client.get( | ||||
summary, | ||||
headers={'X-PARTIAL-XHR': 'true'}, | ||||
name=self._changelog_summary_name(summary)) | ||||
@task(1) | ||||
def stop(self): | ||||
self.interrupt() | ||||
def _commit_name(self, url): | ||||
if SHOW_DETAILS: | ||||
return url | ||||
return '/%s/changeset/{{commit_id}}' % (self.repo, ) | ||||
def _changelog_summary_name(self, url): | ||||
if SHOW_DETAILS: | ||||
return url | ||||
return '/%s?size={{size}}&page={{page}}' % (self.repo, ) | ||||
class BrowseChangelog(BaseTaskSet): | ||||
def on_start(self): | ||||
self.repo = self.decision.choose_repository() | ||||
self.page = 1 | ||||
self.open_changelog_page() | ||||
@task(20) | ||||
def goto_next_page(self): | ||||
self.page += 1 | ||||
self.open_changelog_page() | ||||
@task(5) | ||||
def goto_prev_page(self): | ||||
self.page -= 1 | ||||
self.open_changelog_page() | ||||
@task(20) | ||||
def open_commit(self): | ||||
commits = get_commit_links(self.response.content) | ||||
commit = self.decision.choose(commits, "commit") | ||||
if not commit: | ||||
return | ||||
self.client.get( | ||||
commit, | ||||
name=self._commit_name(commit)) | ||||
def open_changelog_page(self): | ||||
if self.page <= 0: | ||||
self.interrupt() | ||||
changelog = '/%s/changelog?page=%s' % (self.repo, self.page) | ||||
self.response = self.client.get( | ||||
changelog, | ||||
name=self._changelog_page_name(changelog)) | ||||
@task(1) | ||||
def stop(self): | ||||
self.interrupt() | ||||
def _commit_name(self, url): | ||||
if SHOW_DETAILS: | ||||
return url | ||||
return '/%s/changeset/{{commit_id}}' % (self.repo, ) | ||||
def _changelog_page_name(self, url): | ||||
if SHOW_DETAILS: | ||||
return url | ||||
return '/%s/changelog/?page={{page}}' % (self.repo, ) | ||||
class BrowseFiles(BaseTaskSet): | ||||
def on_start(self): | ||||
self.stack = [] | ||||
self.repo = self.decision.choose_repository() | ||||
self.fetch_directory() | ||||
def fetch_directory(self, url=None): | ||||
if not url: | ||||
url = '/%s/files/tip/' % (self.repo, ) | ||||
response = self.client.get(url, name=self._url_name('dir', url=url)) | ||||
self.stack.append({ | ||||
'directories': get_directory_links(response.content, self.repo), | ||||
'files': get_file_links(response.content, self.repo, 'files'), | ||||
'files_raw': get_file_links(response.content, self.repo, 'raw'), | ||||
'files_annotate': get_file_links(response.content, self.repo, 'annotate'), | ||||
}) | ||||
@task(10) | ||||
def browse_directory(self): | ||||
dirs = self.stack[-1]['directories'] | ||||
if not dirs: | ||||
return | ||||
url = self.decision.choose(dirs, "directory") | ||||
self.fetch_directory(url=url) | ||||
@task(10) | ||||
def browse_file(self): | ||||
files = self.stack[-1]['files'] | ||||
if not files: | ||||
return | ||||
file_url = self.decision.choose(files, "file") | ||||
self.client.get(file_url, name=self._url_name('file', file_url)) | ||||
@task(10) | ||||
def browse_raw_file(self): | ||||
files = self.stack[-1]['files_raw'] | ||||
if not files: | ||||
return | ||||
file_url = self.decision.choose(files, "file") | ||||
self.client.get(file_url, name=self._url_name('file_raw', file_url)) | ||||
@task(10) | ||||
def browse_annotate_file(self): | ||||
files = self.stack[-1]['files_annotate'] | ||||
if not files: | ||||
return | ||||
file_url = self.decision.choose(files, "file") | ||||
self.client.get( | ||||
file_url, name=self._url_name('file_annotate', file_url)) | ||||
@task(5) | ||||
def back_to_parent_directory(self): | ||||
if not len(self.stack) > 1: | ||||
return | ||||
self.stack.pop() | ||||
@task(1) | ||||
def stop(self): | ||||
self.interrupt() | ||||
def _url_name(self, kind, url): | ||||
if SHOW_DETAILS: | ||||
return url | ||||
return '/%s/%s/{{commit_id}}/{{path}}' % (self.repo, kind) | ||||
class UserBehavior(BaseTaskSet): | ||||
tasks = [ | ||||
(BrowseLatestChanges, 1), | ||||
(BrowseChangelog, 1), | ||||
(BrowseFiles, 2), | ||||
] | ||||
def on_start(self): | ||||
seed = getattr(self.locust, "seed", None) | ||||
self._decision = ChoiceMaker(random.Random(seed), log=self.log) | ||||
self._random = random.Random(seed) | ||||
user_account = self.decision.choose_user() | ||||
data = { | ||||
'username': user_account[0], | ||||
'password': user_account[1], | ||||
} | ||||
response = self.client.post( | ||||
'/_admin/login?came_from=/', data=data, allow_redirects=False) | ||||
# Sanity check that the login worked out. | ||||
# Successful login means we are redirected to "came_from". | ||||
assert response.status_code == 302 | ||||
assert response.headers['location'] == self.locust.host + '/' | ||||
@task(2) | ||||
def browse_index_page(self): | ||||
self.client.get('/') | ||||
class WebsiteUser(HttpLocust): | ||||
host = HOST | ||||
task_set = UserBehavior | ||||
min_wait = 1000 | ||||
max_wait = 1000 | ||||
def __init__(self): | ||||
super(WebsiteUser, self).__init__() | ||||
self.seed = _seeder.random() | ||||
log.info("WebsiteUser with seed %s", self.seed) | ||||
class ChoiceMaker(object): | ||||
def __init__(self, random, log=None): | ||||
self._random = random | ||||
self.log = log | ||||
def choose(self, choices, kind=None): | ||||
if len(choices) == 0: | ||||
self.log.debug("nothing to choose from") | ||||
return None | ||||
item = self._random.choice(choices) | ||||
self.log.debug("choosing %s %s", kind or "item", item) | ||||
return item | ||||
def choose_user(self): | ||||
return self.choose(USER_ACCOUNTS, "user") | ||||
def choose_repository(self): | ||||
return self.choose(REPOSITORIES, "repository") | ||||
def get_commit_links(content): | ||||
# TODO: find out a way to read the HTML and grab elements by selector | ||||
links = re.findall( | ||||
r'<a.*?class="message-link".*?href="(.*?)".*?/a>', content) | ||||
return links | ||||
def get_directory_links(content, repo_name): | ||||
return _get_links(content, 'browser-dir') | ||||
def get_file_links(content, repo_name, link_type): | ||||
def _process(lnk, repo_name, link_type): | ||||
if link_type == 'files': | ||||
return lnk | ||||
elif link_type == 'raw': | ||||
return re.sub(r'%s/files/' % repo_name, r'%s/raw/' % repo_name, lnk) | ||||
elif link_type == 'annotate': | ||||
return re.sub(r'%s/files/' % repo_name, r'%s/annotate/' % repo_name, lnk) | ||||
links = [] | ||||
for lnk in _get_links(content, 'browser-file'): | ||||
links.append(_process(lnk, repo_name, link_type)) | ||||
return links | ||||
def _get_links(content, class_name): | ||||
return re.findall( | ||||
r'<a.*?class="' + class_name + r'.*?href="(.*?)".*?/a>', | ||||
content) | ||||