diff --git a/.gitignore b/.gitignore index 8838c61200a2..8e8d09618cb9 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,5 @@ startover.sh celery.db *.sql /resource_versions.py +/loadtest/results/* +/loadtest/localsettings.py \ No newline at end of file diff --git a/loadtest/README.mkd b/loadtest/README.mkd new file mode 100644 index 000000000000..8332a9942f1f --- /dev/null +++ b/loadtest/README.mkd @@ -0,0 +1,29 @@ +##CommCare HQ loadtest +multi-mechanize load tests for CommCare HQ + +Work in progress + +###Running +Requires [multi-mechanize](http://testutils.org/multi-mechanize/). + +From the root commcare-hq directory run + + $ multimech-run loadtest + +Currently has the following user profiles: + +* login - logs a user in and grabs an empty report page +* submit_form - submits a simple form with a single case +* ota_restore - ota restores a mobile user +* public_landingpage - hits the public HQ landing page + +Can tweak server, domain and user credentials by adding a localsettings.py file and overriding the following: + + BASE_URL = 'https://staging.commcarehq.org' + DOMAIN = "demo" + USERNAME = "changeme@dimagi.com" + PASSWORD = "***" + MOBILE_USERNAME = "user@demo.commcarehq.org" + MOBILE_PASSWORD = "***" + +Edit config.cfg to tweak number of threads per user profile and length of test. See multimechanize docs for more info. \ No newline at end of file diff --git a/loadtest/config.cfg b/loadtest/config.cfg new file mode 100644 index 000000000000..cc72486d062e --- /dev/null +++ b/loadtest/config.cfg @@ -0,0 +1,19 @@ +[global] +run_time = 300 +rampup = 200 +results_ts_interval = 10 +progress_bar = on +console_logging = off +xml_report = off + +[user_group-1] +threads: 5 +script: login.py + +[user_group-2] +threads: 10 +script: submit_form.py + +[user_group-3] +threads: 5 +script: ota_restore.py \ No newline at end of file diff --git a/loadtest/test_scripts/hq_settings.py b/loadtest/test_scripts/hq_settings.py new file mode 100644 index 000000000000..dcb89d368880 --- /dev/null +++ b/loadtest/test_scripts/hq_settings.py @@ -0,0 +1,75 @@ +import mechanize +import cookielib + + +BASE_URL = 'https://staging.commcarehq.org' +DOMAIN = "demo" +USERNAME = "changeme@dimagi.com" +PASSWORD = "***" +MOBILE_USERNAME = "user@demo.commcarehq.org" +MOBILE_PASSWORD = "***" + +try: + from localsettings import * +except ImportError: + pass + +def login_url(): + return "%s%s" % (BASE_URL, "/accounts/login/") + +# this is necessary to make the runner happy +class Transaction(object): + def run(self): return + +class HQTransaction(object): + """ + Stick some shared stuff in here so we can use it across tests + and keep most of the config in one place. + """ + + def __init__(self): + self.custom_timers = {} + self.base_url = BASE_URL + self.domain = DOMAIN + self.username = USERNAME + self.password = PASSWORD + self.mobile_username = MOBILE_USERNAME + self.mobile_password = MOBILE_PASSWORD + +class User(object): + def __init__(self, username, password, browser): + self.username = username + self.password = password + self.browser = browser + self.logged_in = False + + def ensure_logged_in(self): + if not self.logged_in: + _login(self.browser, self.username, self.password) + self.logged_in = True + + def __str__(self): + return "User" % (self.username, self.logged_in) + +# utility functions +def init_browser(): + """Returns an initialized browser and associated cookie jar.""" + br = mechanize.Browser() + cj = cookielib.LWPCookieJar() + br.set_cookiejar(cj) + + br.set_handle_equiv(True) + br.set_handle_gzip(True) + br.set_handle_redirect(True) + br.set_handle_referer(True) + br.set_handle_robots(False) + + br.set_handle_refresh(mechanize._http.HTTPRefreshProcessor(), max_time=1) + return br + +def _login(browser, username, password): + _ = browser.open(login_url()) + browser.select_form(name="form") + browser.form['username'] = username + browser.form['password'] = password + browser.submit() diff --git a/loadtest/test_scripts/login.py b/loadtest/test_scripts/login.py new file mode 100644 index 000000000000..18fcf63a88eb --- /dev/null +++ b/loadtest/test_scripts/login.py @@ -0,0 +1,24 @@ +import mechanize +import time +from hq_settings import init_browser, User, HQTransaction + +class Transaction(HQTransaction): + + def run(self): + br = init_browser() + start_timer = time.time() + user = User(self.username, self.password, br) + user.ensure_logged_in() + latency = time.time() - start_timer + self.custom_timers['Login'] = latency + + resp = br.open('%s/a/%s/reports/' % (self.base_url, self.domain)) + body = resp.read() + assert resp.code == 200, 'Bad HTTP Response' + assert "Case Activity" in body, "Couldn't find report list" + + +if __name__ == '__main__': + trans = Transaction() + trans.run() + print trans.custom_timers diff --git a/loadtest/test_scripts/ota_restore.py b/loadtest/test_scripts/ota_restore.py new file mode 100644 index 000000000000..7a6f4eb24bd2 --- /dev/null +++ b/loadtest/test_scripts/ota_restore.py @@ -0,0 +1,25 @@ +import mechanize +import time +from hq_settings import init_browser +import hq_settings +from hq_settings import HQTransaction + +class Transaction(HQTransaction): + + def run(self): + br = init_browser() + url = "%s/a/%s/phone/restore/" % (self.base_url, self.domain) + start_timer = time.time() + br.add_password(url, self.mobile_username, self.mobile_password) + resp = br.open(url) + latency = time.time() - start_timer + self.custom_timers['ota-restore'] = latency + body = resp.read() + assert resp.code == 200, 'Bad HTTP Response' + assert "Successfully restored" in body + + +if __name__ == '__main__': + trans = Transaction() + trans.run() + print trans.custom_timers diff --git a/loadtest/test_scripts/public_landingpage.py b/loadtest/test_scripts/public_landingpage.py new file mode 100644 index 000000000000..fdd0d74beae2 --- /dev/null +++ b/loadtest/test_scripts/public_landingpage.py @@ -0,0 +1,21 @@ +import mechanize +import time +from hq_settings import HQTransaction + +class Transaction(HQTransaction): + + def run(self): + br = mechanize.Browser() + br.set_handle_robots(False) + start_timer = time.time() + resp = br.open(self.base_url + '/home/') + resp.read() + latency = time.time() - start_timer + self.custom_timers['Public_Landing_Page'] = latency + assert (resp.code == 200), 'Bad HTTP Response' + + +if __name__ == '__main__': + trans = Transaction() + trans.run() + print trans.custom_timers diff --git a/loadtest/test_scripts/submit_form.py b/loadtest/test_scripts/submit_form.py new file mode 100644 index 000000000000..2d9ef1355938 --- /dev/null +++ b/loadtest/test_scripts/submit_form.py @@ -0,0 +1,77 @@ +import mechanize +import time +from hq_settings import HQTransaction +from datetime import datetime +from urlparse import urlparse +import httplib +import uuid + +# ghetto +SUBMIT_TEMPLATE = """ + + + multimechanize + %(timestart)s + %(timeend)s + multimechanize + multimechanize + %(instanceid)s + + %(extras)s +""" + +CASE_TEMPLATE = """ + + + loadtest + load test case + + + val1 + val2 + + +""" + +ISO_FORMAT = '%Y-%m-%dT%H:%M:%SZ' +def _format_datetime(time): + return time.strftime(ISO_FORMAT) + +def _submission(extras=""): + return SUBMIT_TEMPLATE % {"timestart": _format_datetime(datetime.utcnow()), + "timeend": _format_datetime(datetime.utcnow()), + "instanceid": uuid.uuid4().hex, + "extras": extras } +def _case_submission(): + caseblock = CASE_TEMPLATE % {"moddate": _format_datetime(datetime.utcnow()), + "caseid": uuid.uuid4().hex } + return _submission(extras=caseblock) + +def _post(data, url, content_type="text/xml"): + headers = {"content-type": content_type, + "content-length": len(data), + } + + up = urlparse(url) + conn = httplib.HTTPSConnection(up.netloc) if url.startswith("https") else httplib.HTTPConnection(up.netloc) + conn.request('POST', up.path, data, headers) + return conn.getresponse() + +class Transaction(HQTransaction): + + def run(self): + submit = _case_submission() + start_timer = time.time() + url = "%s%s" % (self.base_url, "/a/cory/receiver") + resp = _post(submit, url) + latency = time.time() - start_timer + self.custom_timers['submission'] = latency + responsetext = resp.read() + assert resp.status == 201, 'Bad HTTP Response' + assert "Thanks for submitting" in responsetext, "Bad response text" + +if __name__ == '__main__': + trans = Transaction() + trans.run() + print trans.custom_timers