# -*- 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 .
#
# 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'', 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'',
content)