diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 000000000..473bdf647 --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1,2 @@ +# Default code owners for master branch - Atlassian Data Center App Performance Toolkit +* @benmagro @ometelytsia @jkim2-atlassian @SergeyMoroz0703 @astashys @mmizin \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 695330e30..45babb92e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -10,7 +10,7 @@ All the changes are welcome. Please help us to improve code, examples and docume Pull requests, issues and comments are welcome. For pull requests: - - Create your own fork of the repository and raise a pull request targeting master branch in the main repository + - Create your own fork of the repository and raise a pull request targeting `dev` branch in the main repository - Separate unrelated changes into multiple pull requests See the [existing issues](https://ecosystem.atlassian.net/projects/DAPT/issues) for things to start contributing. @@ -31,5 +31,5 @@ link below to digitally sign the CLA. The Corporate CLA is for those who are contributing as a member of an organization and the individual CLA is for those contributing as an individual. -* [CLA for corporate contributors](https://na2.docusign.net/Member/PowerFormSigning.aspx?PowerFormId=e1c17c66-ca4d-4aab-a953-2c231af4a20b) -* [CLA for individuals](https://na2.docusign.net/Member/PowerFormSigning.aspx?PowerFormId=3f94fbdc-2fbe-46ac-b14c-5d152700ae5d) \ No newline at end of file +* [CLA for corporate contributors](https://opensource.atlassian.com/corporate) +* [CLA for individuals](https://opensource.atlassian.com/individual) \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..cbaf2913e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,25 @@ +# name: atlassain/dcapt +# working dir: dc-app-performance-toolkit +# build: docker build -t atlassain/dcapt . +# bzt run: docker run --shm-size=4g -v "$PWD:/dc-app-performance-toolkit" atlassian/dcapt jira.yml +# interactive run: docker run -it --entrypoint="/bin/bash" -v "$PWD:/dc-app-performance-toolkit" atlassian/dcapt + +FROM blazemeter/taurus + +ENV APT_INSTALL="apt-get -y install --no-install-recommends" + +RUN apt-get -y update \ + && $APT_INSTALL vim git openssh-server python3.8-dev python3-pip google-chrome-stable \ + && update-alternatives --install /usr/bin/python python /usr/bin/python3.8 1 \ + && python -m pip install --upgrade pip \ + && python -m pip install --upgrade setuptools \ + && apt-get clean + +COPY requirements.txt /tmp/requirements.txt +RUN pip install -r /tmp/requirements.txt + +RUN rm -rf /root/.bzt/jmeter-taurus/ + +WORKDIR /dc-app-performance-toolkit/app + +ENTRYPOINT ["bzt", "-o", "modules.console.disable=true"] \ No newline at end of file diff --git a/app/bitbucket.yml b/app/bitbucket.yml index 345689729..66421a9ed 100644 --- a/app/bitbucket.yml +++ b/app/bitbucket.yml @@ -6,32 +6,35 @@ settings: env: application_hostname: test_bitbucket_instance.atlassian.com # Bitbucket DC hostname without protocol and port e.g. test-bitbucket.atlassian.com or localhost application_protocol: http # http or https - application_port: 80 # 80, 443, 8080, 2990, etc - application_postfix: # e.g. /bitbucket in case of url like http://localhost:2990/bitbucket + application_port: 80 # 80, 443, 8080, 7990 etc + application_postfix: # e.g. /bitbucket in case of url like http://localhost:7990/bitbucket admin_login: admin admin_password: admin - concurrency: 20 + load_executor: jmeter # only jmeter executor is supported + concurrency: 20 # number of concurrent virtual users for jmeter scenario test_duration: 50m + ramp-up: 10m # time to spin all concurrent users + total_actions_per_hour: 32700 WEBDRIVER_VISIBLE: False JMETER_VERSION: 5.2.1 allow_analytics: Yes # Allow sending basic run analytics to Atlassian. These analytics help us to understand how the tool is being used and help us to continue to invest in this tooling. For more details please see our README. services: - module: shellexec prepare: - - python util/environment_checker.py - - python util/git_client_check.py - - python util/data_preparation/bitbucket/prepare-data.py + - python util/pre_run/environment_checker.py + - python util/pre_run/git_client_check.py + - python util/data_preparation/bitbucket_prepare_data.py shutdown: - - python util/jmeter_post_check.py + - python util/post_run/jmeter_post_check.py - python util/jtl_convertor/jtls-to-csv.py kpi.jtl selenium.jtl post-process: - - python util/analytics.py bitbucket - - python util/cleanup_results_dir.py + - python util/analytics/analytics.py bitbucket + - python util/post_run/cleanup_results_dir.py execution: - - scenario: jmeter + - scenario: ${load_executor} concurrency: ${concurrency} hold-for: ${test_duration} - ramp-up: 10m + ramp-up: ${ramp-up} - scenario: selenium executor: selenium runner: pytest @@ -48,7 +51,7 @@ scenarios: application_protocol: ${application_protocol} application_port: ${application_port} application_postfix: ${application_postfix} - total_actions_per_hr: 32700 + total_actions_per_hr: ${total_actions_per_hour} tmp_dir: /tmp ssh_key_url: https://centaurus-datasets.s3.us-east-2.amazonaws.com/bitbucket/ssh/id_rsa modules: @@ -73,7 +76,6 @@ modules: - jpgc-tst=2.4 - jpgc-wsc=0.3 - tilln-sshmon=1.0 - - jpgc-cmd=2.2 - jpgc-synthesis=2.2 system-properties: server.rmi.ssl.disable: true @@ -81,7 +83,7 @@ modules: httpsampler.ignore_failed_embedded_resources: "true" selenium: chromedriver: - version: "80.0.3987.106" # Supports Chrome version 80. You can refer to http://chromedriver.chromium.org/downloads + version: "83.0.4103.39" # Supports Chrome version 83. You can refer to http://chromedriver.chromium.org/downloads reporting: - data-source: sample-labels module: junit-xml diff --git a/app/confluence.yml b/app/confluence.yml index e86ef9538..c72f83f08 100644 --- a/app/confluence.yml +++ b/app/confluence.yml @@ -6,38 +6,56 @@ settings: env: application_hostname: test_confluence_instance.atlassian.com # Confluence DC hostname without protocol and port e.g. test-jira.atlassian.com or localhost application_protocol: http # http or https - application_port: 80 # 80, 443, 8080, 2990, etc - application_postfix: # e.g. /jira in case of url like http://localhost:2990/jira + application_port: 80 # 80, 443, 8080, 1990, etc + application_postfix: # e.g. /confluence in case of url like http://localhost:1990/confluence admin_login: admin admin_password: admin - concurrency: 200 + load_executor: jmeter # jmeter and locust are supported. jmeter by default. + concurrency: 200 # number of concurrent virtual users for jmeter or locust scenario test_duration: 45m + ramp-up: 3m # time to spin all concurrent users + total_actions_per_hour: 20000 WEBDRIVER_VISIBLE: False JMETER_VERSION: 5.2.1 allow_analytics: Yes # Allow sending basic run analytics to Atlassian. These analytics help us to understand how the tool is being used and help us to continue to invest in this tooling. For more details please see our README. + # Action percentage for Jmeter and Locust load executors + view_page: 54 + view_dashboard: 6 + view_blog: 8 + search_cql: 7 + create_blog: 3 + create_and_edit_page: 6 + comment_page: 5 + view_attachment: 3 + upload_attachment: 5 + like_page: 3 + standalone_extension: 0 # By default disabled services: - module: shellexec prepare: - - python util/environment_checker.py - - python util/data_preparation/confluence/prepare-data.py + - python util/pre_run/environment_checker.py + - python util/data_preparation/confluence_prepare_data.py shutdown: - - python util/jmeter_post_check.py + - python util/post_run/jmeter_post_check.py - python util/jtl_convertor/jtls-to-csv.py kpi.jtl selenium.jtl post-process: - - python util/analytics.py confluence - - python util/cleanup_results_dir.py + - python util/analytics/analytics.py confluence + - python util/post_run/cleanup_results_dir.py execution: - - scenario: jmeter + - scenario: ${load_executor} + executor: ${load_executor} concurrency: ${concurrency} hold-for: ${test_duration} - ramp-up: 3m + ramp-up: ${ramp-up} - scenario: selenium executor: selenium runner: pytest hold-for: ${test_duration} scenarios: selenium: - script: selenium_ui/confluence-ui.py + script: selenium_ui/confluence_ui.py + locust: + script: locustio/confluence/locustfile.py jmeter: script: jmeter/confluence.jmx properties: @@ -46,18 +64,18 @@ scenarios: application_port: ${application_port} application_postfix: ${application_postfix} # Workload model - total_actions_per_hr: 20000 - perc_view_page: 54 - perc_view_dashboard: 6 - perc_view_blog: 8 - perc_search_cql: 7 - perc_create_blog: 3 - perc_create_and_edit_page: 6 - perc_comment_page: 5 - perc_view_attachment: 3 - perc_upload_attachment: 5 - perc_like_page: 3 - perc_standalone_extension: 0 # By default disabled + total_actions_per_hr: ${total_actions_per_hour} + perc_view_page: ${view_page} + perc_view_dashboard: ${view_dashboard} + perc_view_blog: ${view_blog} + perc_search_cql: ${search_cql} + perc_create_blog: ${create_blog} + perc_create_and_edit_page: ${create_and_edit_page} + perc_comment_page: ${comment_page} + perc_view_attachment: ${view_attachment} + perc_upload_attachment: ${upload_attachment} + perc_like_page: ${like_page} + perc_standalone_extension: ${standalone_extension} modules: consolidator: rtimes-len: 0 # CONFSRVDEV-7631 reduce sampling @@ -80,7 +98,6 @@ modules: - jpgc-tst=2.4 - jpgc-wsc=0.3 - tilln-sshmon=1.0 - - jpgc-cmd=2.2 - jpgc-synthesis=2.2 system-properties: server.rmi.ssl.disable: true @@ -88,7 +105,7 @@ modules: httpsampler.ignore_failed_embedded_resources: "true" selenium: chromedriver: - version: "80.0.3987.106" # Supports Chrome version 80. You can refer to http://chromedriver.chromium.org/downloads + version: "83.0.4103.39" # Supports Chrome version 83. You can refer to http://chromedriver.chromium.org/downloads reporting: - data-source: sample-labels module: junit-xml diff --git a/app/datasets/jira/examples/projects.csv b/app/datasets/jira/examples/projects.csv new file mode 100644 index 000000000..faa18fea3 --- /dev/null +++ b/app/datasets/jira/examples/projects.csv @@ -0,0 +1 @@ +ABC,10000 \ No newline at end of file diff --git a/app/extension/bitbucket/extension_ui.py b/app/extension/bitbucket/extension_ui.py index 4bff60291..92d3f5a04 100644 --- a/app/extension/bitbucket/extension_ui.py +++ b/app/extension/bitbucket/extension_ui.py @@ -5,21 +5,21 @@ from selenium_ui.base_page import BasePage -def custom_action(webdriver, datasets): +def app_specific_action(webdriver, datasets): page = BasePage(webdriver) - @print_timing - def measure(webdriver, interaction): - @print_timing - def measure(webdriver, interaction): - page.go_to_url(f"{BITBUCKET_SETTINGS.server_url}/plugin/report") - page.wait_until_visible((By.ID, 'report_app_element_id'), interaction) - measure(webdriver, 'selenium_app_custom_action:view_report') + @print_timing("selenium_app_custom_action") + def measure(): - @print_timing - def measure(webdriver, interaction): - page.go_to_url(f"{BITBUCKET_SETTINGS.server_url}/plugin/dashboard") - page.wait_until_visible((By.ID, 'dashboard_app_element_id'), interaction) + @print_timing("selenium_app_custom_action:view_report") + def sub_measure(): + page.go_to_url(f"{BITBUCKET_SETTINGS.server_url}/plugin/report") + page.wait_until_visible((By.ID, 'report_app_element_id')) + sub_measure() - measure(webdriver, 'selenium_app_custom_action:view_dashboard') - measure(webdriver, 'selenium_app_custom_action') + @print_timing("selenium_app_custom_action:view_dashboard") + def sub_measure(): + page.go_to_url(f"{BITBUCKET_SETTINGS.server_url}/plugin/dashboard") + page.wait_until_visible((By.ID, 'dashboard_app_element_id')) + sub_measure() + measure() diff --git a/app/extension/confluence/extension_locust.py b/app/extension/confluence/extension_locust.py new file mode 100644 index 000000000..ec19a5ff2 --- /dev/null +++ b/app/extension/confluence/extension_locust.py @@ -0,0 +1,28 @@ +import re +from locustio.common_utils import init_logger, confluence_measure + +logger = init_logger(app_type='confluence') + + +@confluence_measure +def app_specific_action(locust): + + r = locust.client.get('/plugin/report') # navigate to page + + content = r.content.decode('utf-8') # parse page content + token_pattern_example = '"token":"(.+?)"' + id_pattern_example = '"id":"(.+?)"' + token = re.findall(token_pattern_example, content) # parse variables from response using regexp + id = re.findall(id_pattern_example, content) + logger.locust_info(f'token: {token}, id: {id}') # logger for debug when verbose is true in confluence.yml file + if 'assertion string' not in content: + logger.error(f"'assertion string' was not found in {content}") + assert 'assertion string' in content # assertion after GET request + + body = {"id": id, "token": token} # include parsed variables to POST body + headers = {'content-type': 'application/json'} + r = locust.client.post('/plugin/post/endpoint', body, headers) # send some POST request + content = r.content.decode('utf-8') + if 'assertion string after successful post request' not in content: + logger.error(f"'assertion string after successful post request' was not found in {content}") + assert 'assertion string after successful post request' in content # assertion after POST request diff --git a/app/extension/confluence/extension_ui.py b/app/extension/confluence/extension_ui.py index 404e695e8..db24ea6a4 100644 --- a/app/extension/confluence/extension_ui.py +++ b/app/extension/confluence/extension_ui.py @@ -5,21 +5,21 @@ from selenium_ui.base_page import BasePage -def custom_action(webdriver, datasets): +def app_specific_action(webdriver, datasets): page = BasePage(webdriver) - @print_timing - def measure(webdriver, interaction): - @print_timing - def measure(webdriver, interaction): - page.go_to_url(f"{CONFLUENCE_SETTINGS.server_url}/plugin/report") - page.wait_until_visible((By.ID, 'report_app_element_id'), interaction) - measure(webdriver, 'selenium_app_custom_action:view_report') + @print_timing("selenium_app_custom_action") + def measure(): - @print_timing - def measure(webdriver, interaction): - page.go_to_url(f"{CONFLUENCE_SETTINGS.server_url}/plugin/dashboard") - page.wait_until_visible((By.ID, 'dashboard_app_element_id'), interaction) + @print_timing("selenium_app_custom_action:view_report") + def sub_measure(): + page.go_to_url(f"{CONFLUENCE_SETTINGS.server_url}/plugin/report") + page.wait_until_visible((By.ID, 'report_app_element_id')) + sub_measure() - measure(webdriver, 'selenium_app_custom_action:view_dashboard') - measure(webdriver, 'selenium_app_custom_action') + @print_timing("selenium_app_custom_action:view_dashboard") + def sub_measure(): + page.go_to_url(f"{CONFLUENCE_SETTINGS.server_url}/plugin/dashboard") + page.wait_until_visible((By.ID, 'dashboard_app_element_id')) + sub_measure() + measure() diff --git a/app/extension/jira/examples/drawio/extension_ui.py b/app/extension/jira/examples/drawio/extension_ui.py index 348f54652..e4a553357 100644 --- a/app/extension/jira/examples/drawio/extension_ui.py +++ b/app/extension/jira/examples/drawio/extension_ui.py @@ -13,9 +13,9 @@ def custom_action(webdriver, datasets): # Click more webdriver.find_element_by_id('opsbar-operations_more').click() - @print_timing - def measure(webdriver, interaction): + @print_timing("selenium_open_drawio_editor") + def measure(): # Click to add a diagram (opens the drawio editor) webdriver.find_element_by_id('drawio-add-menu-item').click() WebDriverWait(webdriver, timeout).until(ec.frame_to_be_available_and_switch_to_it((By.ID, 'drawioEditor'))) - measure(webdriver, 'selenium_open_drawio_editor') + measure() diff --git a/app/extension/jira/extension_locust.py b/app/extension/jira/extension_locust.py new file mode 100644 index 000000000..f521bb823 --- /dev/null +++ b/app/extension/jira/extension_locust.py @@ -0,0 +1,27 @@ +import re +from locustio.common_utils import init_logger, jira_measure + +logger = init_logger(app_type='jira') + + +@jira_measure +def app_specific_action(locust): + r = locust.client.get('/plugin/report') # navigate to page + + content = r.content.decode('utf-8') # parse page content + token_pattern_example = '"token":"(.+?)"' + id_pattern_example = '"id":"(.+?)"' + token = re.findall(token_pattern_example, content) # parse variables from response using regexp + id = re.findall(id_pattern_example, content) + logger.locust_info(f'token: {token}, id: {id}') # logger for debug when verbose is true in jira.yml file + if 'assertion string' not in content: + logger.error(f"'assertion string' was not found in {content}") + assert 'assertion string' in content # assertion after GET request + + body = {"id": id, "token": token} # include parsed variables to POST body + headers = {'content-type': 'application/json'} + r = locust.client.post('/plugin/post/endpoint', body, headers) # send some POST request + content = r.content.decode('utf-8') + if 'assertion string after successful post request' not in content: + logger.error(f"'assertion string after successful post request' was not found in {content}") + assert 'assertion string after successful post request' in content # assertion after POST request diff --git a/app/extension/jira/extension_ui.py b/app/extension/jira/extension_ui.py index 92a987380..27a69b6fd 100644 --- a/app/extension/jira/extension_ui.py +++ b/app/extension/jira/extension_ui.py @@ -5,21 +5,21 @@ from selenium_ui.base_page import BasePage -def custom_action(webdriver, datasets): +def app_specific_action(webdriver, datasets): page = BasePage(webdriver) - @print_timing - def measure(webdriver, interaction): - @print_timing - def measure(webdriver, interaction): - page.go_to_url(f"{JIRA_SETTINGS.server_url}/plugin/report") - page.wait_until_visible((By.ID, 'report_app_element_id'), interaction) - measure(webdriver, 'selenium_app_custom_action:view_report') + @print_timing("selenium_app_custom_action") + def measure(): - @print_timing - def measure(webdriver, interaction): - page.go_to_url(f"{JIRA_SETTINGS.server_url}/plugin/dashboard") - page.wait_until_visible((By.ID, 'dashboard_app_element_id'), interaction) + @print_timing("selenium_app_custom_action:view_report") + def sub_measure(): + page.go_to_url(f"{JIRA_SETTINGS.server_url}/plugin/report") + page.wait_until_visible((By.ID, 'report_app_element_id')) + sub_measure() - measure(webdriver, 'selenium_app_custom_action:view_dashboard') - measure(webdriver, 'selenium_app_custom_action') + @print_timing("selenium_app_custom_action:view_dashboard") + def sub_measure(): + page.go_to_url(f"{JIRA_SETTINGS.server_url}/plugin/dashboard") + page.wait_until_visible((By.ID, 'dashboard_app_element_id')) + sub_measure() + measure() diff --git a/app/jira.yml b/app/jira.yml index b84bd6aaa..19fd14b8c 100644 --- a/app/jira.yml +++ b/app/jira.yml @@ -10,27 +10,45 @@ settings: application_postfix: # e.g. /jira in case of url like http://localhost:2990/jira admin_login: admin admin_password: admin - concurrency: 200 + load_executor: jmeter # jmeter and locust are supported. jmeter by default. + concurrency: 200 # number of concurrent virtual users for jmeter or locust scenario test_duration: 45m + ramp-up: 3m # time to spin all concurrent users + total_actions_per_hour: 54500 WEBDRIVER_VISIBLE: False JMETER_VERSION: 5.2.1 allow_analytics: Yes # Allow sending basic run analytics to Atlassian. These analytics help us to understand how the tool is being used and help us to continue to invest in this tooling. For more details please see our README. + # Action percentage for Jmeter and Locust load executors + create_issue: 4 + search_jql: 13 + view_issue: 43 + view_project_summary: 4 + view_dashboard: 12 + edit_issue: 4 + add_comment: 2 + browse_projects: 4 + view_scrum_board: 3 + view_kanban_board: 3 + view_backlog: 6 + browse_boards: 2 + standalone_extension: 0 # By default disabled services: - module: shellexec prepare: - - python util/environment_checker.py - - python util/data_preparation/jira/prepare-data.py + - python util/pre_run/environment_checker.py + - python util/data_preparation/jira_prepare_data.py shutdown: - - python util/jmeter_post_check.py + - python util/post_run/jmeter_post_check.py - python util/jtl_convertor/jtls-to-csv.py kpi.jtl selenium.jtl post-process: - - python util/analytics.py jira - - python util/cleanup_results_dir.py + - python util/analytics/analytics.py jira + - python util/post_run/cleanup_results_dir.py execution: - - scenario: jmeter + - scenario: ${load_executor} + executor: ${load_executor} concurrency: ${concurrency} hold-for: ${test_duration} - ramp-up: 3m + ramp-up: ${ramp-up} - scenario: selenium executor: selenium runner: pytest @@ -38,6 +56,8 @@ execution: scenarios: selenium: script: selenium_ui/jira_ui.py + locust: + script: locustio/jira/locustfile.py jmeter: script: jmeter/jira.jmx properties: @@ -46,20 +66,20 @@ scenarios: application_port: ${application_port} application_postfix: ${application_postfix} # Workload model - total_actions_per_hr: 54500 - perc_create_issue: 4 - perc_search_jql: 13 - perc_view_issue: 43 - perc_view_project_summary: 4 - perc_view_dashboard: 12 - perc_edit_issue: 4 - perc_add_comment: 2 - perc_browse_projects: 4 - perc_view_scrum_board: 3 - perc_view_kanban_board: 3 - perc_view_backlog: 6 - perc_browse_boards: 2 - perc_standalone_extension: 0 # By default disabled + total_actions_per_hr: ${total_actions_per_hour} + perc_create_issue: ${create_issue} + perc_search_jql: ${search_jql} + perc_view_issue: ${view_issue} + perc_view_project_summary: ${view_project_summary} + perc_view_dashboard: ${view_dashboard} + perc_edit_issue: ${edit_issue} + perc_add_comment: ${add_comment} + perc_browse_projects: ${browse_projects} + perc_view_scrum_board: ${view_scrum_board} + perc_view_kanban_board: ${view_kanban_board} + perc_view_backlog: ${view_backlog} + perc_browse_boards: ${browse_boards} + perc_standalone_extension: ${standalone_extension} modules: consolidator: rtimes-len: 0 # CONFSRVDEV-7631 reduce sampling @@ -82,7 +102,6 @@ modules: - jpgc-tst=2.4 - jpgc-wsc=0.3 - tilln-sshmon=1.0 - - jpgc-cmd=2.2 - jpgc-synthesis=2.2 system-properties: server.rmi.ssl.disable: true @@ -90,7 +109,7 @@ modules: httpsampler.ignore_failed_embedded_resources: "true" selenium: chromedriver: - version: "80.0.3987.106" # Supports Chrome version 80. You can refer to http://chromedriver.chromium.org/downloads + version: "83.0.4103.39" # Supports Chrome version 83. You can refer to http://chromedriver.chromium.org/downloads reporting: - data-source: sample-labels module: junit-xml diff --git a/app/jmeter/bitbucket.jmx b/app/jmeter/bitbucket.jmx index a64112263..51ec7c45c 100644 --- a/app/jmeter/bitbucket.jmx +++ b/app/jmeter/bitbucket.jmx @@ -1,5 +1,5 @@ - + This test plan was created by the BlazeMeter converter v.2.3.14. Please contact support@blazemeter.com for further support. @@ -109,6 +109,7 @@ true + false @@ -123,6 +124,7 @@ true false + false @@ -136,6 +138,7 @@ false + true @@ -331,7 +334,7 @@ else{ - /login + ${application.postfix}/login POST true false @@ -361,7 +364,7 @@ else{ - /rest/ssh/1.0/keys + ${application.postfix}/rest/ssh/1.0/keys GET true false @@ -437,7 +440,7 @@ vars.put("USER_SSH_KEY", new File('${__P(PRIVATE_SSH_KEY_LOCATION - /rest/ssh/latest/keys?user=${admin_login} + ${application.postfix}/rest/ssh/latest/keys?user=${admin_login} POST true false @@ -479,7 +482,7 @@ vars.put("USER_SSH_KEY", new File('${__P(PRIVATE_SSH_KEY_LOCATION - /j_atl_security_logout + ${application.postfix}/j_atl_security_logout POST true false @@ -520,6 +523,7 @@ else{ false + true @@ -575,7 +579,7 @@ if (randomNum <= gitProtocolSshPercentage) { vars.put("REPO_URL", "ssh://git@${application.ssh_hostname}:${application.ssh_port}/" + projectKey + "/" + repoSlug + ".git") } else { vars.put("GIT_PROTOCOL", "http") - vars.put("REPO_URL", "${application.protocol}://" + username + ":" + password + "@${application.hostname}:${application.port}/scm/" + projectKey + "/" + repoSlug + ".git") + vars.put("REPO_URL", "${application.protocol}://" + username + ":" + password + "@${application.hostname}:${application.port}${application.postfix}/scm/" + projectKey + "/" + repoSlug + ".git") } @@ -1004,6 +1008,7 @@ if (localWorkingCopy.exists()) { false + true diff --git a/app/jmeter/confluence.jmx b/app/jmeter/confluence.jmx index 00daebb3c..48f04a0b4 100644 --- a/app/jmeter/confluence.jmx +++ b/app/jmeter/confluence.jmx @@ -1,5 +1,5 @@ - + @@ -55,6 +55,7 @@ true + false @@ -115,6 +116,7 @@ false 3600 + true @@ -395,9 +397,8 @@ 1 - + - quick-search Log Out @@ -406,12 +407,6 @@ 16 - - Adding this sampler after the login operation to stop and start next thread if fails. - - 1 - - true @@ -775,6 +770,20 @@ + + ${__jexl3(!${JMeterThread.last_sample_ok},)} + false + true + Check if login action is success + + + + 5 + 0 + 0 + + + true -1 @@ -2902,7 +2911,6 @@ vars.put("extend_action", extend_action); Accept-Encoding gzip, deflate - Accept */* @@ -2971,7 +2979,6 @@ vars.put("extend_action", extend_action); Accept-Encoding gzip, deflate - Accept application/json, text/javascript, */*; q=0.01 @@ -3024,7 +3031,6 @@ vars.put("extend_action", extend_action); Accept-Encoding gzip, deflate - Accept application/json, text/javascript, */*; q=0.01 @@ -3077,7 +3083,6 @@ vars.put("extend_action", extend_action); Accept-Encoding gzip, deflate - Accept application/json, text/javascript, */*; q=0.01 @@ -3130,7 +3135,6 @@ vars.put("extend_action", extend_action); Accept-Encoding gzip, deflate - Accept application/json, text/javascript, */*; q=0.01 @@ -3183,7 +3187,6 @@ vars.put("extend_action", extend_action); Accept-Encoding gzip, deflate - Accept application/json, text/javascript, */*; q=0.01 @@ -3240,7 +3243,6 @@ vars.put("extend_action", extend_action); Accept-Encoding gzip, deflate - Accept application/json, text/javascript, */*; q=0.01 @@ -3325,7 +3327,6 @@ vars.put("extend_action", extend_action); Accept-Encoding gzip, deflate - Accept application/json, text/javascript, */*; q=0.01 @@ -3395,7 +3396,6 @@ vars.put("extend_action", extend_action); Accept-Encoding gzip, deflate - Accept application/json, text/javascript, */*; q=0.01 @@ -3456,7 +3456,6 @@ vars.put("extend_action", extend_action); Accept-Encoding gzip, deflate - Accept application/json, text/javascript, */*; q=0.01 @@ -3541,7 +3540,6 @@ vars.put("extend_action", extend_action); Accept-Encoding gzip, deflate - Accept text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 @@ -3633,7 +3631,6 @@ vars.put("extend_action", extend_action); Accept-Encoding gzip, deflate - Accept application/json, text/javascript, */*; q=0.01 @@ -3683,7 +3680,6 @@ vars.put("extend_action", extend_action); Accept-Encoding gzip, deflate - Accept application/json, text/javascript, */*; q=0.01 @@ -3725,7 +3721,6 @@ vars.put("extend_action", extend_action); Accept-Encoding gzip, deflate - Accept text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 @@ -3778,7 +3773,6 @@ vars.put("extend_action", extend_action); Accept-Encoding gzip, deflate - Accept application/json, text/javascript, */*; q=0.01 @@ -3831,7 +3825,6 @@ vars.put("extend_action", extend_action); Accept-Encoding gzip, deflate - Accept application/json, text/javascript, */*; q=0.01 @@ -3881,7 +3874,6 @@ vars.put("extend_action", extend_action); Accept-Encoding gzip, deflate - Accept application/json, text/javascript, */*; q=0.01 @@ -3932,7 +3924,6 @@ vars.put("extend_action", extend_action); Accept-Encoding gzip, deflate - Accept application/json, text/javascript, */*; q=0.01 @@ -4014,7 +4005,6 @@ vars.put("extend_action", extend_action); Accept-Encoding gzip, deflate - Accept application/json, text/javascript, */*; q=0.01 @@ -4073,7 +4063,6 @@ vars.put("extend_action", extend_action); Accept-Encoding gzip, deflate - Accept */* @@ -4126,7 +4115,6 @@ vars.put("extend_action", extend_action); Accept-Encoding gzip, deflate - Accept text/plain, */*; q=0.01 @@ -4207,7 +4195,6 @@ vars.put("p_new_file_name", (new Random().with {(1..9).collect {((&apo Accept-Encoding gzip, deflate - Accept application/json, text/javascript, */*; q=0.01 @@ -4270,7 +4257,6 @@ vars.put("p_new_file_name", (new Random().with {(1..9).collect {((&apo Accept-Encoding gzip, deflate - Accept text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 @@ -4333,7 +4319,6 @@ vars.put("p_new_file_name", (new Random().with {(1..9).collect {((&apo Accept-Encoding gzip, deflate - Accept application/json, text/javascript, */*; q=0.01 @@ -4386,7 +4371,6 @@ vars.put("p_new_file_name", (new Random().with {(1..9).collect {((&apo Accept-Encoding gzip, deflate - Accept application/json, text/javascript, */*; q=0.01 @@ -4428,7 +4412,6 @@ vars.put("p_new_file_name", (new Random().with {(1..9).collect {((&apo Accept-Encoding gzip, deflate - Accept text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 @@ -4479,7 +4462,6 @@ vars.put("p_new_file_name", (new Random().with {(1..9).collect {((&apo Accept-Encoding gzip, deflate - Accept application/json, text/javascript, */*; q=0.01 @@ -4536,7 +4518,6 @@ vars.put("p_new_file_name", (new Random().with {(1..9).collect {((&apo Accept-Encoding gzip, deflate - Accept application/json, text/javascript, */*; q=0.01 @@ -4589,7 +4570,6 @@ vars.put("p_new_file_name", (new Random().with {(1..9).collect {((&apo Accept-Encoding gzip, deflate - Accept application/json, text/javascript, */*; q=0.01 @@ -4646,7 +4626,6 @@ vars.put("p_new_file_name", (new Random().with {(1..9).collect {((&apo Accept-Encoding gzip, deflate - Accept */* @@ -4703,7 +4682,6 @@ vars.put("p_new_file_name", (new Random().with {(1..9).collect {((&apo Accept-Encoding gzip, deflate - Accept application/json, text/javascript, */*; q=0.01 @@ -4741,7 +4719,6 @@ vars.put("p_new_file_name", (new Random().with {(1..9).collect {((&apo Accept-Encoding gzip, deflate - Accept */* @@ -4787,7 +4764,6 @@ vars.put("p_new_file_name", (new Random().with {(1..9).collect {((&apo Accept-Encoding gzip, deflate - Accept application/json, text/javascript, */*; q=0.01 @@ -4844,7 +4820,6 @@ vars.put("p_new_file_name", (new Random().with {(1..9).collect {((&apo Accept-Encoding gzip, deflate - Accept application/json, text/javascript, */*; q=0.01 @@ -4929,7 +4904,6 @@ vars.put("p_new_file_name", (new Random().with {(1..9).collect {((&apo Accept-Encoding gzip, deflate - Accept */* @@ -4982,7 +4956,6 @@ vars.put("p_new_file_name", (new Random().with {(1..9).collect {((&apo Accept-Encoding gzip, deflate - Accept application/json, text/javascript, */*; q=0.01 @@ -5035,7 +5008,6 @@ vars.put("p_new_file_name", (new Random().with {(1..9).collect {((&apo Accept-Encoding gzip, deflate - Accept application/json, text/javascript, */*; q=0.01 @@ -5124,7 +5096,6 @@ vars.put("p_new_file_name", (new Random().with {(1..9).collect {((&apo Accept-Encoding gzip, deflate - Accept application/json, text/javascript, */*; q=0.01 @@ -5223,7 +5194,6 @@ vars.put("extend_action", extend_action); Accept-Encoding gzip, deflate - Accept text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 @@ -5315,7 +5285,6 @@ vars.put("extend_action", extend_action); Accept-Encoding gzip, deflate - Accept application/json, text/javascript, */*; q=0.01 @@ -5368,7 +5337,6 @@ vars.put("extend_action", extend_action); Accept-Encoding gzip, deflate - Accept application/json, text/javascript, */*; q=0.01 @@ -5421,7 +5389,6 @@ vars.put("extend_action", extend_action); Accept-Encoding gzip, deflate - Accept application/json, text/javascript, */*; q=0.01 @@ -5463,7 +5430,6 @@ vars.put("extend_action", extend_action); Accept-Encoding gzip, deflate - Accept application/json, text/javascript, */*; q=0.01 @@ -5513,7 +5479,6 @@ vars.put("extend_action", extend_action); Accept-Encoding gzip, deflate - Accept application/json, text/javascript, */*; q=0.01 @@ -5563,7 +5528,6 @@ vars.put("extend_action", extend_action); Accept-Encoding gzip, deflate - Accept */* @@ -5613,7 +5577,6 @@ vars.put("extend_action", extend_action); Accept-Encoding gzip, deflate - Accept application/json, text/javascript, */*; q=0.01 @@ -5663,7 +5626,6 @@ vars.put("extend_action", extend_action); Accept-Encoding gzip, deflate - Accept application/json, text/javascript, */*; q=0.01 @@ -5716,7 +5678,6 @@ vars.put("extend_action", extend_action); Accept-Encoding gzip, deflate - Accept application/json, text/javascript, */*; q=0.01 @@ -5798,7 +5759,6 @@ vars.put("extend_action", extend_action); Accept-Encoding gzip, deflate - Accept application/json, text/javascript, */*; q=0.01 @@ -5892,7 +5852,6 @@ vars.put("extend_action", extend_action); Accept-Encoding gzip, deflate - Accept application/json, text/javascript, */*; q=0.01 @@ -5954,7 +5913,6 @@ vars.put("extend_action", extend_action); Accept-Encoding gzip, deflate - Accept text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 @@ -6068,7 +6026,6 @@ vars.put("extend_action", extend_action); Accept-Encoding gzip, deflate - Accept application/json, text/javascript, */*; q=0.01 @@ -6114,7 +6071,6 @@ vars.put("extend_action", extend_action); Accept-Encoding gzip, deflate - Accept application/json, text/javascript, */*; q=0.01 @@ -6167,7 +6123,6 @@ vars.put("extend_action", extend_action); Accept-Encoding gzip, deflate - Accept application/json, text/javascript, */*; q=0.01 @@ -6228,7 +6183,6 @@ vars.put("extend_action", extend_action); Accept-Encoding gzip, deflate - Accept application/json, text/javascript, */*; q=0.01 @@ -6285,7 +6239,6 @@ vars.put("extend_action", extend_action); Accept-Encoding gzip, deflate - Accept application/json, text/javascript, */*; q=0.01 @@ -6342,7 +6295,6 @@ vars.put("extend_action", extend_action); Accept-Encoding gzip, deflate - Accept application/json, text/javascript, */*; q=0.01 @@ -6399,7 +6351,6 @@ vars.put("extend_action", extend_action); Accept-Encoding gzip, deflate - Accept application/json, text/javascript, */*; q=0.01 @@ -6456,7 +6407,6 @@ vars.put("extend_action", extend_action); Accept-Encoding gzip, deflate - Accept */* @@ -6506,7 +6456,6 @@ vars.put("extend_action", extend_action); Accept-Encoding gzip, deflate - Accept application/json, text/javascript, */*; q=0.01 @@ -6559,7 +6508,6 @@ vars.put("extend_action", extend_action); Accept-Encoding gzip, deflate - Accept application/json, text/javascript, */*; q=0.01 @@ -6612,7 +6560,6 @@ vars.put("extend_action", extend_action); Accept-Encoding gzip, deflate - Accept application/json, text/javascript, */*; q=0.01 @@ -6697,7 +6644,6 @@ vars.put("extend_action", extend_action); Accept-Encoding gzip, deflate - Accept */* @@ -6750,7 +6696,6 @@ vars.put("extend_action", extend_action); Accept-Encoding gzip, deflate - Accept application/json, text/javascript, */*; q=0.01 @@ -6803,7 +6748,6 @@ vars.put("extend_action", extend_action); Accept-Encoding gzip, deflate - Accept application/json, text/javascript, */*; q=0.01 @@ -6856,7 +6800,6 @@ vars.put("extend_action", extend_action); Accept-Encoding gzip, deflate - Accept application/json, text/javascript, */*; q=0.01 @@ -6912,7 +6855,6 @@ vars.put("extend_action", extend_action); Accept-Encoding gzip, deflate - Accept text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 @@ -7052,7 +6994,6 @@ vars.put("extend_action", extend_action); Accept-Encoding gzip, deflate - Accept */* @@ -7134,7 +7075,6 @@ vars.put("extend_action", extend_action); Accept-Encoding gzip, deflate - Accept application/json, text/javascript, */*; q=0.01 @@ -7191,7 +7131,6 @@ vars.put("extend_action", extend_action); Accept-Encoding gzip, deflate - Accept application/json, text/javascript, */*; q=0.01 @@ -7241,7 +7180,6 @@ vars.put("extend_action", extend_action); Accept-Encoding gzip, deflate - Accept application/json, text/javascript, */*; q=0.01 @@ -7323,7 +7261,6 @@ vars.put("extend_action", extend_action); Accept-Encoding gzip, deflate - Accept application/json, text/javascript, */*; q=0.01 @@ -7373,7 +7310,6 @@ vars.put("extend_action", extend_action); Accept-Encoding gzip, deflate - Accept */* @@ -7423,7 +7359,6 @@ vars.put("extend_action", extend_action); Accept-Encoding gzip, deflate - Accept application/json, text/javascript, */*; q=0.01 @@ -7505,7 +7440,6 @@ vars.put("extend_action", extend_action); Accept-Encoding gzip, deflate - Accept application/json, text/javascript, */*; q=0.01 @@ -7783,7 +7717,6 @@ vars.put("p_page_version", page_version.toString()); Accept-Encoding gzip, deflate - Accept text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 @@ -7880,7 +7813,6 @@ if (totalAncestorID > 0) Accept-Encoding gzip, deflate - Accept application/json, text/javascript, */*; q=0.01 @@ -7955,7 +7887,6 @@ if (totalAncestorID > 0) Accept-Encoding gzip, deflate - Accept application/json, text/javascript, */*; q=0.01 @@ -8001,7 +7932,6 @@ if (totalAncestorID > 0) Accept-Encoding gzip, deflate - Accept application/json, text/javascript, */*; q=0.01 @@ -8054,7 +7984,6 @@ if (totalAncestorID > 0) Accept-Encoding gzip, deflate - Accept application/json, text/javascript, */*; q=0.01 @@ -8115,7 +8044,6 @@ if (totalAncestorID > 0) Accept-Encoding gzip, deflate - Accept application/json, text/javascript, */*; q=0.01 @@ -8172,7 +8100,6 @@ if (totalAncestorID > 0) Accept-Encoding gzip, deflate - Accept application/json, text/javascript, */*; q=0.01 @@ -8229,7 +8156,6 @@ if (totalAncestorID > 0) Accept-Encoding gzip, deflate - Accept */* @@ -8286,7 +8212,6 @@ if (totalAncestorID > 0) Accept-Encoding gzip, deflate - Accept application/json, text/javascript, */*; q=0.01 @@ -8371,7 +8296,6 @@ if (totalAncestorID > 0) Accept-Encoding gzip, deflate - Accept */* @@ -8428,7 +8352,6 @@ if (totalAncestorID > 0) Accept-Encoding gzip, deflate - Accept application/json, text/javascript, */*; q=0.01 @@ -8478,7 +8401,6 @@ if (totalAncestorID > 0) Accept-Encoding gzip, deflate - Accept application/json, text/javascript, */*; q=0.01 @@ -8531,7 +8453,6 @@ if (totalAncestorID > 0) Accept-Encoding gzip, deflate - Accept application/json, text/javascript, */*; q=0.01 @@ -8584,7 +8505,6 @@ if (totalAncestorID > 0) Accept-Encoding gzip, deflate - Accept application/json, text/javascript, */*; q=0.01 @@ -8637,7 +8557,6 @@ if (totalAncestorID > 0) Accept-Encoding gzip, deflate - Accept application/json, text/javascript, */*; q=0.01 @@ -8690,7 +8609,6 @@ if (totalAncestorID > 0) Accept-Encoding gzip, deflate - Accept application/json, text/javascript, */*; q=0.01 @@ -8797,7 +8715,6 @@ vars.put("extend_action", extend_action); Accept-Encoding gzip, deflate - Accept application/json, text/javascript, */*; q=0.01 @@ -8905,7 +8822,6 @@ vars.put("extend_action", extend_action); Accept-Encoding gzip, deflate - Accept text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 diff --git a/app/jmeter/jira.jmx b/app/jmeter/jira.jmx index 002b29b44..b808fbfaf 100644 --- a/app/jmeter/jira.jmx +++ b/app/jmeter/jira.jmx @@ -1,5 +1,5 @@ - + @@ -56,6 +56,7 @@ true + false @@ -150,6 +151,7 @@ false + true @@ -165,7 +167,7 @@ import org.apache.commons.io.FileUtils; // TODO Rework hard-codded path -int projects = FileUtils.readLines(new File("datasets/jira/project_keys.csv")).size(); +int projects = FileUtils.readLines(new File("datasets/jira/projects.csv")).size(); int page_size = 25; int pages = (projects % page_size == 0) ? projects / page_size : projects / page_size + 1; props.put("project_pages", String.valueOf(pages)); @@ -186,6 +188,7 @@ props.put("project_pages", String.valueOf(pages)); false 0 + true @@ -1087,6 +1090,20 @@ JMeterUtils.setProperty("c_AtlToken" + user_counter, atl_token) + + ${__jexl3(!${JMeterThread.last_sample_ok},)} + false + true + Check if login action is success + + + + 5 + 0 + 0 + + + true -1 @@ -1096,7 +1113,7 @@ JMeterUtils.setProperty("c_AtlToken" + user_counter, atl_token) datasets/jira/issues.csv UTF-8 , - issue_key,issue_id,project_key + issue_key,issue_id,issue_project_key false false true @@ -1137,6 +1154,17 @@ JMeterUtils.setProperty("c_AtlToken" + user_counter, atl_token) shareMode.all + + datasets/jira/projects.csv + UTF-8 + , + project_key,project_id + true + false + true + false + + 1 0 @@ -1263,24 +1291,6 @@ vars.put("extend_action", extend_action); 1 - - false - x_issue_type - {&quot;label&quot;:&quot;Bug&quot;,&quot;value&quot;:&quot;([0-9]*)&quot; - $1$ - NOT FOUND - 1 - - - - false - x_project_id - class=\\"project-field\\" value=\\"(.+?)\\" - $1$ - NOT FOUND - 1 - - false x_resolution_done @@ -1311,15 +1321,6 @@ vars.put("extend_action", extend_action); true - - false - x_fields_to_retain - {"id":"(.+?)" - $1$ - NOT FOUND - -1 - - "id":"project","label":"Project" @@ -1460,7 +1461,7 @@ vars.put("extend_action", extend_action); } // static -def pid = vars.get("x_project_id") +def pid = vars.get("project_id") def issuetype = vars.get("x_issue_type") def atl_token = vars.get("x_atl_token") def formToken = vars.get("x_form_token") @@ -1505,73 +1506,6 @@ if (custom_fields_to_retain_total > 0) { request_body = request_body + "&${custom_fields_to_retain}&${fields_to_retain}" vars.put("p_request_body", request_body.toString()) - - - - groovy - - - true - def generator = { String alphabet, int n -> - new Random().with { - (1..n).collect { alphabet[ nextInt( alphabet.length() ) ] }.join() - } - -} - -// static -def pid = vars.get("x_project_id") -def issuetype = vars.get("x_issue_type") -def atl_token = vars.get("x_atl_token") -def formToken = vars.get("x_form_token") -def summary = "Summary - " + generator((('a'..'z')+ ' '*5).join(), 20) -def duedate = "" -def reporter = vars.get("username") -def environment = "Environment - " + generator((('a'..'z')+ ' '*5).join(), 20) -def description = "Description - " + generator((('a'..'z') + ' '*10).join(), 500); -def timetracking_originalestimate = "" -def timetracking_remainingestimate = "" -def isCreateIssue = "true" -def hasWorkStarted = "" -def resolution = vars.get("x_resolution_done") -def request_body = "pid=${pid}&issuetype=${issuetype}&atl_token=${atl_token}&formToken=${formToken}&summary=${summary}&duedate=${duedate}&reporter=${reporter}&environment=${environment}&description=${description}&timetracking_originalestimate=${timetracking_originalestimate}&timetracking_remainingestimate=${timetracking_remainingestimate}&isCreateIssue=${isCreateIssue}&hasWorkStarted=${hasWorkStarted}&resolution=${resolution}" - -//if (vars.get("x_fields_to_retain") == "none" || vars.get("x_custom_fields_to_retain") == "none") { -// vars.put("p_request_body", request_body.toString()) -//} else { - // dynamic (generic fields) - def fields_to_retain = "" - def fields_to_retain_total = vars.get("x_fields_to_retain_matchNr").toInteger(); - counter = 1; - if (fields_to_retain_total >= 0) - { - while (counter < fields_to_retain_total) - { - fields_to_retain = fields_to_retain + "fieldsToRetain=" + vars.get("x_fields_to_retain_" + counter) + "&"; - counter = counter + 1; - } - fields_to_retain = fields_to_retain.substring(0,fields_to_retain.length() - 1) - } - - // dynamic (custom fields) - def custom_fields_to_retain = "" - def custom_fields_to_retain_total = vars.get("x_custom_fields_to_retain_matchNr").toInteger(); - counter = 1; - if (custom_fields_to_retain_total >= 0) - { - while (counter < custom_fields_to_retain_total) - { - custom_fields_to_retain = custom_fields_to_retain + "fieldsToRetain=customfield_" + vars.get("x_custom_fields_to_retain_" + counter) + "&"; - counter = counter + 1; - } - custom_fields_to_retain = custom_fields_to_retain.substring(0,custom_fields_to_retain.length() - 1) - } - - - // request - request_body = request_body + "&${custom_fields_to_retain}&${fields_to_retain}" - vars.put("p_request_body", request_body.toString()) -//} @@ -3032,7 +2966,7 @@ vars.put("extend_action", extend_action); - ${application.postfix}/rest/projects/1.0/project/${project_key}/lastVisited + ${application.postfix}/rest/projects/1.0/project/${issue_project_key}/lastVisited PUT true false @@ -5227,7 +5161,7 @@ vars.put("extend_action", extend_action); false projectKey - ${project_key} + ${issue_project_key} = true @@ -5754,7 +5688,7 @@ vars.put("random_string_long", random_string_long) - ${application.postfix}/rest/bamboo/latest/deploy/${project_key}/${issue_key} + ${application.postfix}/rest/bamboo/latest/deploy/${issue_project_key}/${issue_key} GET true false @@ -5973,7 +5907,7 @@ vars.put("random_string_long", random_string_long) - ${application.postfix}/rest/projects/1.0/project/${project_key}/lastVisited + ${application.postfix}/rest/projects/1.0/project/${issue_project_key}/lastVisited PUT true false @@ -6340,7 +6274,7 @@ vars.put("extend_action", extend_action); false projectKey - ${project_key} + ${issue_project_key} = true diff --git a/app/locustio/common_utils.py b/app/locustio/common_utils.py new file mode 100644 index 000000000..9285eaac5 --- /dev/null +++ b/app/locustio/common_utils.py @@ -0,0 +1,197 @@ +from locust import events +import time +import csv +import re +import logging +import random +import string +import json +import socket +from logging.handlers import RotatingFileHandler +from datetime import datetime +from util.conf import JIRA_SETTINGS, CONFLUENCE_SETTINGS, AppSettingsExtLoadExecutor +from util.project_paths import ENV_TAURUS_ARTIFACT_DIR +import inspect + +TEXT_HEADERS = { + 'Accept-Language': 'en-US,en;q=0.5', + 'Content-Type': 'application/x-www-form-urlencoded', + 'Accept-Encoding': 'gzip, deflate', + 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', + } +ADMIN_HEADERS = { + 'Accept-Language': 'en-US,en;q=0.5', + 'X-AUSERNAME': 'admin', + 'X-Requested-With': 'XMLHttpRequest', + 'Accept-Encoding': 'gzip, deflate', + 'Accept': '*/*' + } +NO_TOKEN_HEADERS = { + "Accept-Language": "en-US,en;q=0.5", + "X-Requested-With": "XMLHttpRequest", + "__amdModuleName": "jira/issue/utils/xsrf-token-header", + "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", + "Accept-Encoding": "gzip, deflate", + "Accept": "application/json, text/javascript, */*; q=0.01", + "X-Atlassian-Token": "no-check" +} +JSON_HEADERS = { + "Accept-Language": "en-US,en;q=0.5", + "X-Requested-With": "XMLHttpRequest", + "Content-Type": "application/json", + "Accept-Encoding": "gzip, deflate", + "Accept": "application/json, text/javascript, */*; q=0.01" +} + +jira_action_time = 3600 / int((JIRA_SETTINGS.total_actions_per_hour) / int(JIRA_SETTINGS.concurrency)) +confluence_action_time = 3600 / int((CONFLUENCE_SETTINGS.total_actions_per_hour) / int(CONFLUENCE_SETTINGS.concurrency)) + + +class ActionPercentage: + + def __init__(self, config_yml: AppSettingsExtLoadExecutor): + self.env = config_yml.env + + def percentage(self, action_name: str): + if action_name in self.env: + return int(self.env[action_name]) + else: + raise Exception(f'Action percentage for {action_name} does not set in yml configuration file') + + +class Logger(logging.Logger): + + def __init__(self, name, level, app_type): + super().__init__(name=name, level=level) + self.type = app_type + + def locust_info(self, msg, *args, **kwargs): + is_verbose = False + if self.type.lower() == 'confluence': + is_verbose = CONFLUENCE_SETTINGS.verbose + elif self.type.lower() == 'jira': + is_verbose = JIRA_SETTINGS.verbose + if is_verbose or not self.type: + if self.isEnabledFor(logging.INFO): + self._log(logging.INFO, msg, args, **kwargs) + + +def jira_measure(func): + def wrapper(*args, **kwargs): + start = time.time() + result = global_measure(func, start, *args, **kwargs) + total = (time.time() - start) + if total < jira_action_time: + sleep = (jira_action_time - total) + print(f'action: {func.__name__}, action_execution_time: {total}, sleep {sleep}') + time.sleep(sleep) + return result + return wrapper + + +def confluence_measure(func): + def wrapper(*args, **kwargs): + start = time.time() + result = global_measure(func, start, *args, **kwargs) + total = (time.time() - start) + if total < confluence_action_time: + sleep = (confluence_action_time - total) + logger.info(f'action: {func.__name__}, action_execution_time: {total}, sleep {sleep}') + time.sleep(sleep) + return result + return wrapper + + +def global_measure(func, start_time, *args, **kwargs): + result = None + try: + result = func(*args, **kwargs) + except Exception as e: + total = int((time.time() - start_time) * 1000) + + # Delete this workaround after fix from Taurus. + for handler in events.request_failure._handlers: + argspec = inspect.getfullargspec(handler) + if argspec.varkw is None and 'self' in argspec.args: + # Special case for incompatible Taurus handler + handler(request_type='Action', + name=f"locust_{func.__name__}", + response_time=total, + exception=e) + else: + handler( + request_type='Action', + name=f"locust_{func.__name__}", + response_time=total, + exception=e, + response_length=0, + ) + + # Uncomment with fix of __on_failure() function from Taurus. Expected Taurus version with the fix is 1.14.3 + # events.request_failure.fire(request_type="Action", + # name=f"locust_{func.__name__}", + # response_time=total, + # response_length=0, + # exception=e) + logger.error(f'{func.__name__} action failed. Reason: {e}') + else: + total = int((time.time() - start_time) * 1000) + events.request_success.fire(request_type="Action", + name=f"locust_{func.__name__}", + response_time=total, + response_length=0) + logger.info(f'{func.__name__} is finished successfully') + return result + + +def read_input_file(file_path): + with open(file_path, 'r') as fs: + reader = csv.reader(fs) + return list(reader) + + +def fetch_by_re(pattern, text, group_no=1, default_value=None): + search = re.search(pattern, text) + if search: + return search.group(group_no) + else: + return default_value + + +def read_json(file_json): + with open(file_json) as f: + return json.load(f) + + +def init_logger(app_type=None): + logfile_path = ENV_TAURUS_ARTIFACT_DIR / 'locust.log' + root_logger = Logger(name='locust', level=logging.INFO, app_type=app_type) + log_format = f"[%(asctime)s.%(msecs)03d] [%(levelname)s] {socket.gethostname()}/%(name)s : %(message)s" + formatter = logging.Formatter(log_format, '%Y-%m-%d %H:%M:%S') + file_handler = RotatingFileHandler(logfile_path, maxBytes=5 * 1024 * 1024, backupCount=3) + file_handler.setFormatter(formatter) + file_handler.setLevel(logging.INFO) + root_logger.addHandler(file_handler) + return root_logger + + +def timestamp_int(): + now = datetime.now() + return int(datetime.timestamp(now)) + + +def generate_random_string(length, only_letters=False): + if not only_letters: + return "".join([random.choice(string.digits + string.ascii_letters + ' ') for _ in range(length)]) + else: + return "".join([random.choice(string.ascii_lowercase + ' ') for _ in range(length)]) + + +def get_first_index(from_list: list, err): + if len(from_list) > 0: + return from_list[0] + else: + raise IndexError(err) + + +logger = init_logger() diff --git a/app/locustio/confluence/http_actions.py b/app/locustio/confluence/http_actions.py new file mode 100644 index 000000000..74e1dc17e --- /dev/null +++ b/app/locustio/confluence/http_actions.py @@ -0,0 +1,783 @@ +import random +import re +import uuid + +from locustio.common_utils import confluence_measure, fetch_by_re, timestamp_int,\ + TEXT_HEADERS, NO_TOKEN_HEADERS, JSON_HEADERS, generate_random_string, init_logger +from locustio.confluence.requests_params import confluence_datasets, Login, ViewPage, ViewDashboard, ViewBlog, \ + CreateBlog, CreateEditPage, UploadAttachments, LikePage +from util.conf import CONFLUENCE_SETTINGS + +logger = init_logger(app_type='confluence') +confluence_dataset = confluence_datasets() + + +@confluence_measure +def login_and_view_dashboard(locust): + params = Login() + + user = random.choice(confluence_dataset["users"]) + username = user[0] + password = user[1] + + login_body = params.login_body + login_body['os_username'] = username + login_body['os_password'] = password + locust.client.post('/dologin.action', login_body, TEXT_HEADERS, catch_response=True) + r = locust.client.get('/', catch_response=True) + content = r.content.decode('utf-8') + if 'Log Out' not in content: + logger.error(f'Login with {username}, {password} failed: {content}') + assert 'Log Out' in content, 'User authentication failed.' + logger.locust_info(f'User {username} is successfully logged in') + keyboard_hash = fetch_by_re(params.keyboard_hash_re, content) + build_number = fetch_by_re(params.build_number_re, content) + locust.client.post('/rest/webResources/1.0/resources', params.resources_body.get("010"), + TEXT_HEADERS, catch_response=True) + locust.client.get('/rest/mywork/latest/status/notification/count', catch_response=True) + locust.client.get(f'/rest/shortcuts/latest/shortcuts/{build_number}/{keyboard_hash}', catch_response=True) + locust.client.post('/rest/webResources/1.0/resources', params.resources_body.get("025"), + TEXT_HEADERS, catch_response=True) + locust.client.get(f'/rest/experimental/search?cql=type=space%20and%20space.type=favourite%20order%20by%20favourite' + f'%20desc&expand=space.icon&limit=100&_={timestamp_int()}', catch_response=True) + locust.client.get('/rest/dashboardmacros/1.0/updates?maxResults=40&tab=all&showProfilePic=true&labels=' + '&spaces=&users=&types=&category=&spaceKey=', catch_response=True) + locust.storage = dict() # Define locust storage dict for getting cross-functional variables access + locust.storage['build_number'] = build_number + locust.storage['keyboard_hash'] = keyboard_hash + locust.user = username + + +def view_page_and_tree(locust): + params = ViewPage() + page = random.choice(confluence_dataset["pages"]) + page_id = page[0] + + @confluence_measure + def view_page(): + r = locust.client.get(f'/pages/viewpage.action?pageId={page_id}', catch_response=True) + content = r.content.decode('utf-8') + if 'Created by' not in content or 'Save for later' not in content: + logger.error(f'Fail to open page {page_id}: {content}') + assert 'Created by' in content and 'Save for later' in content, 'Could not open page.' + parent_page_id = fetch_by_re(params.parent_page_id_re, content) + parsed_page_id = fetch_by_re(params.page_id_re, content) + space_key = fetch_by_re(params.space_key_re, content) + tree_request_id = fetch_by_re(params.tree_result_id_re, content) + has_no_root = fetch_by_re(params.has_no_root_re, content) + root_page_id = fetch_by_re(params.root_page_id_re, content) + atl_token_view_issue = fetch_by_re(params.atl_token_view_issue_re, content) + editable = fetch_by_re(params.editable_re, content) + ancestor_ids = re.findall(params.ancestor_ids_re, content) + + ancestor_str = 'ancestors=' + for ancestor in ancestor_ids: + ancestor_str = ancestor_str + str(ancestor) + '&' + + locust.storage['page_id'] = parsed_page_id + locust.storage['has_no_root'] = has_no_root + locust.storage['tree_request_id'] = tree_request_id + locust.storage['root_page_id'] = root_page_id + locust.storage['ancestors'] = ancestor_str + locust.storage['space_key'] = space_key + locust.storage['editable'] = editable + locust.storage['atl_token_view_issue'] = atl_token_view_issue + + locust.client.get('/rest/helptips/1.0/tips', catch_response=True) + locust.client.post('/rest/webResources/1.0/resources', params.resources_body.get("110"), + TEXT_HEADERS, catch_response=True) + locust.client.get(f'/rest/likes/1.0/content/{parsed_page_id}/likes?commentLikes=true&_={timestamp_int()}', + catch_response=True) + locust.client.get(f'/rest/highlighting/1.0/panel-items?pageId={parsed_page_id}&_={timestamp_int()}', + catch_response=True) + locust.client.get(f'/rest/mywork/latest/status/notification/count?pageId={parsed_page_id}&_={timestamp_int()}', + catch_response=True) + r = locust.client.get(f'/rest/inlinecomments/1.0/comments?containerId={parsed_page_id}&_={timestamp_int()}', + catch_response=True) + content = r.content.decode('utf-8') + if 'authorDisplayName' not in content and '[]' not in content: + logger.error(f'Could not open comments for page {parsed_page_id}: {content}') + assert 'authorDisplayName' in content or '[]' in content, 'Could not open comments for page.' + locust.client.get(f'/plugins/editor-loader/editor.action?parentPageId={parent_page_id}&pageId={parsed_page_id}' + f'&spaceKey={space_key}&atl_after_login_redirect=/pages/viewpage.action' + f'&timeout=12000&_={timestamp_int()}', catch_response=True) + locust.client.get(f'/rest/watch-button/1.0/watchState/{parsed_page_id}?_={timestamp_int()}', + catch_response=True) + locust.client.post('/rest/webResources/1.0/resources', params.resources_body.get("145"), + TEXT_HEADERS, catch_response=True) + locust.client.post('/rest/webResources/1.0/resources', params.resources_body.get("150"), + TEXT_HEADERS, catch_response=True) + locust.client.post('/rest/webResources/1.0/resources', params.resources_body.get("155"), + TEXT_HEADERS, catch_response=True) + locust.client.post('/rest/webResources/1.0/resources', params.resources_body.get("160"), + TEXT_HEADERS, catch_response=True) + + @confluence_measure + def view_page_tree(): + tree_request_id = locust.storage['tree_request_id'].replace('&', '&') + ancestors = locust.storage['ancestors'] + root_page_id = locust.storage['root_page_id'] + viewed_page_id = locust.storage['page_id'] + space_key = locust.storage['space_key'] + r = '' + # Page has parent + if locust.storage['has_no_root'] == 'false': + request = f"{tree_request_id}&hasRoot=true&pageId={root_page_id}&treeId=0&startDepth=0&mobile=false" \ + f"&{ancestors}treePageId={viewed_page_id}&_={timestamp_int()}" + r = locust.client.get(f'{request}', catch_response=True) + # Page does not have parent + elif locust.storage['has_no_root'] == 'true': + request = f"{tree_request_id}&hasRoot=false&spaceKey={space_key}&treeId=0&startDepth=0&mobile=false" \ + f"&{ancestors}treePageId={viewed_page_id}&_={timestamp_int()}" + r = locust.client.get(f'{request}', catch_response=True) + content = r.content.decode('utf-8') + if 'plugin_pagetree_children_span' not in content or 'plugin_pagetree_children_list' not in content: + logger.error(f'Could not view page tree: {content}') + assert 'plugin_pagetree_children_span' in content and 'plugin_pagetree_children_list' in content, \ + 'Could not view page tree.' + + view_page() + view_page_tree() + + +@confluence_measure +def view_dashboard(locust): + params = ViewDashboard() + + r = locust.client.get('/index.action', catch_response=True) + content = r.content.decode('utf-8') + keyboard_hash = fetch_by_re(params.keyboard_hash_re, content) + build_number = fetch_by_re(params.build_number_re, content) + if 'quick-search' not in content or 'Log Out' not in content: + logger.error(f'Could not view dashboard: {content}') + assert 'quick-search' in content and 'Log Out' in content, 'Could not view dashboard.' + + locust.client.post('/rest/webResources/1.0/resources', params.resources_body.get("205"), + TEXT_HEADERS, catch_response=True) + locust.client.get('/rest/mywork/latest/status/notification/count', catch_response=True) + locust.client.get(f'/rest/shortcuts/latest/shortcuts/{build_number}/{keyboard_hash}', catch_response=True) + locust.client.get(f'/rest/experimental/search?cql=type=space%20and%20space.type=favourite%20order%20by%20' + f'favourite%20desc&expand=space.icon&limit=100&_={timestamp_int()}', catch_response=True) + r = locust.client.get('/rest/dashboardmacros/1.0/updates?maxResults=40&tab=all&showProfilePic=true&labels=' + '&spaces=&users=&types=&category=&spaceKey=', catch_response=True) + content = r.content.decode('utf-8') + if 'changeSets' not in content: + logger.error(f'Could not view dashboard macros: {content}') + assert 'changeSets' in content, 'Could not view dashboard macros.' + + +@confluence_measure +def view_blog(locust): + params = ViewBlog() + blog = random.choice(confluence_dataset["blogs"]) + blog_id = blog[0] + + r = locust.client.get(f'/pages/viewpage.action?pageId={blog_id}', catch_response=True) + content = r.content.decode('utf-8') + if 'Created by' not in content or 'Save for later' not in content: + logger.error(f'Fail to open blog {blog_id}: {content}') + assert 'Created by' in content and 'Save for later' in content, 'Could not view blog.' + + parent_page_id = fetch_by_re(params.parent_page_id_re, content) + parsed_blog_id = fetch_by_re(params.page_id_re, content) + space_key = fetch_by_re(params.space_key_re, content) + + locust.client.get('/rest/helptips/1.0/tips', catch_response=True) + locust.client.post('/rest/webResources/1.0/resources', params.resources_body.get("310"), + TEXT_HEADERS, catch_response=True) + locust.client.get(f'/rest/likes/1.0/content/{parsed_blog_id}/likes?commentLikes=true&_={timestamp_int()}', + catch_response=True) + locust.client.get(f'/rest/highlighting/1.0/panel-items?pageId={parsed_blog_id}&_={timestamp_int()}', + catch_response=True) + locust.client.get(f'/rest/mywork/latest/status/notification/count?pageId={parsed_blog_id}&_={timestamp_int()}', + catch_response=True) + r = locust.client.get(f'/rest/inlinecomments/1.0/comments?containerId={parsed_blog_id}&_={timestamp_int()}', + catch_response=True) + content = r.content.decode('utf-8') + if 'authorDisplayName' not in content and '[]' not in content: + logger.error(f'Could not open comments for page {parsed_blog_id}: {content}') + assert 'authorDisplayName' in content or '[]' in content, 'Could not open comments for page.' + + r = locust.client.get(f'/plugins/editor-loader/editor.action?parentPageId={parent_page_id}&pageId={parsed_blog_id}' + f'&spaceKey={space_key}&atl_after_login_redirect=/pages/viewpage.action' + f'&timeout=12000&_={timestamp_int()}', catch_response=True) + content = r.content.decode('utf-8') + if 'draftId' not in content: + logger.error(f'Could not open editor for blog {parsed_blog_id}: {content}') + assert 'draftId' in content, 'Could not open editor for blog.' + + locust.client.get(f'/rest/watch-button/1.0/watchState/{parsed_blog_id}?_={timestamp_int()}', catch_response=True) + locust.client.post('/rest/webResources/1.0/resources', params.resources_body.get("345"), + TEXT_HEADERS, catch_response=True) + locust.client.post('/rest/webResources/1.0/resources', params.resources_body.get("350"), + TEXT_HEADERS, catch_response=True) + locust.client.post('/rest/webResources/1.0/resources', params.resources_body.get("360"), + TEXT_HEADERS, catch_response=True) + locust.client.post('/rest/webResources/1.0/resources', params.resources_body.get("365"), + TEXT_HEADERS, catch_response=True) + locust.client.get(f'/rest/quickreload/latest/{parsed_blog_id}?since={timestamp_int()}&_={timestamp_int()}', + catch_response=True) + + +def search_cql_and_view_results(locust): + + @confluence_measure + def search_recently_viewed(): + locust.client.get('/rest/recentlyviewed/1.0/recent?limit=8', catch_response=True) + + @confluence_measure + def search_cql(): + r = locust.client.get(f"/rest/api/search?cql=siteSearch~'{generate_random_string(3, only_letters=True)}'" + f"&start=0&limit=20", catch_response=True) + if '{"results":[' not in r.content.decode('utf-8'): + logger.locust_info(r.content.decode('utf-8')) + content = r.content.decode('utf-8') + if 'results' not in content: + logger.error(f"Search cql failed: {content}") + assert 'results' in content, "Search cql failed." + locust.client.get('/rest/mywork/latest/status/notification/count', catch_response=True) + + search_recently_viewed() + search_cql() + + +def open_editor_and_create_blog(locust): + params = CreateBlog() + blog = random.choice(confluence_dataset["blogs"]) + blog_space_key = blog[1] + build_number = locust.storage.get('build_number', '') + keyboard_hash = locust.storage.get('keyboard_hash', '') + + @confluence_measure + def create_blog_editor(): + r = locust.client.get(f'/pages/createblogpost.action?spaceKey={blog_space_key}', catch_response=True) + content = r.content.decode('utf-8') + if 'Blog post title' not in content: + logger.error(f'Could not open editor for {blog_space_key}: {content}') + assert 'Blog post title' in content, 'Could not open editor for blog.' + + atl_token = fetch_by_re(params.atl_token_re, content) + content_id = fetch_by_re(params.content_id_re, content) + parsed_space_key = fetch_by_re(params.space_key, content) + + locust.client.post('/rest/webResources/1.0/resources', params.resources_body.get("910"), + TEXT_HEADERS, catch_response=True) + locust.client.get('/rest/mywork/latest/status/notification/count?pageId=0', catch_response=True) + locust.client.get('/plugins/servlet/notifications-miniview', catch_response=True) + locust.client.post('/rest/webResources/1.0/resources', params.resources_body.get("925"), + TEXT_HEADERS, catch_response=True) + locust.client.post('/rest/webResources/1.0/resources', params.resources_body.get("930"), + TEXT_HEADERS, catch_response=True) + locust.client.get(f'/rest/emoticons/1.0/_={timestamp_int()}', catch_response=True) + locust.client.get(f'/rest/shortcuts/latest/shortcuts/{build_number}/{keyboard_hash}?_={timestamp_int()}', + catch_response=True) + + heartbeat_activity_body = {"dataType": "json", + "contentId": content_id, + "draftType": "blogpost", + "spaceKey": parsed_space_key, + "atl_token": atl_token + } + r = locust.client.post('/json/startheartbeatactivity.action', heartbeat_activity_body, + TEXT_HEADERS, catch_response=True) + content = r.content.decode('utf-8') + if atl_token not in content: + logger.error(f'Token {atl_token} not found in content: {content}') + assert atl_token in content, 'Token not found in content.' + + contributor_hash = fetch_by_re(params.contribution_hash, content) + locust.storage['contributor_hash'] = contributor_hash + + r = locust.client.get(f'/rest/ui/1.0/content/{content_id}/labels', catch_response=True) + content = r.content.decode('utf-8') + if '"success":true' not in content: + logger.error(f'Could not get labels for content {content_id}: {content}') + assert '"success":true' in content, 'Could not get labels for content in blog editor.' + + draft_name = f"Performance Blog - {generate_random_string(10, only_letters=True)}" + locust.storage['draft_name'] = draft_name + locust.storage['parsed_space_key'] = parsed_space_key + locust.storage['content_id'] = content_id + locust.storage['atl_token'] = atl_token + + draft_body = {"draftId": content_id, + "pageId": "0", + "type": "blogpost", + "title": draft_name, + "spaceKey": parsed_space_key, + "content": "

test blog draft

", + "syncRev": "0.mcPCPtDvwoayMR7zvuQSbf8.27"} + + TEXT_HEADERS['Content-Type'] = 'application/json' + r = locust.client.post('/rest/tinymce/1/drafts', json=draft_body, headers=TEXT_HEADERS, catch_response=True) + content = r.content.decode('utf-8') + if 'draftId' not in content: + logger.error(f'Could not create blog post draft in space {parsed_space_key}: {content}') + assert 'draftId' in content, 'Could not create blog post draft.' + + @confluence_measure + def create_blog(): + draft_name = locust.storage['draft_name'] + parsed_space_key = locust.storage['parsed_space_key'] + content_id = locust.storage['content_id'] + atl_token = locust.storage['atl_token'] + + draft_body = {"status": "current", "title": draft_name, "space": {"key": f"{parsed_space_key}"}, + "body": {"editor": {"value": f"Test Performance Blog Page Content {draft_name}", + "representation": "editor", "content": {"id": f"{content_id}"}}}, + "id": f"{content_id}", "type": "blogpost", + "version": {"number": 1, "minorEdit": True, "syncRev": "0.mcPCPtDvwoayMR7zvuQSbf8.30"}} + TEXT_HEADERS['Content-Type'] = 'application/json' + r = locust.client.put(f'/rest/api/content/{content_id}?status=draft', json=draft_body, + headers=TEXT_HEADERS, catch_response=True) + content = r.content.decode('utf-8') + if 'current' not in content or 'title' not in content: + logger.error(f'Could not open draft {draft_name}: {content}') + assert 'current' in content and 'title' in content, 'Could not open blog draft.' + created_blog_title = fetch_by_re(params.created_blog_title_re, content) + logger.locust_info(f'Blog {created_blog_title} created') + + r = locust.client.get(f'/{created_blog_title}', catch_response=True) + content = r.content.decode('utf-8') + if 'Created by' not in content: + logger.error(f'Could not open created blog {created_blog_title}: {content}') + assert 'Created by' in content, 'Could not open created blog.' + + locust.client.post('/rest/webResources/1.0/resources', params.resources_body.get("970"), + TEXT_HEADERS, catch_response=True) + locust.client.post('/rest/webResources/1.0/resources', params.resources_body.get("975"), + TEXT_HEADERS, catch_response=True) + locust.client.get('/plugins/servlet/notifications-miniview', catch_response=True) + locust.client.get(f'/rest/watch-button/1.0/watchState/{content_id}?_={timestamp_int()}', catch_response=True) + locust.client.get(f'/rest/likes/1.0/content/{content_id}/likes?commentLikes=true&_={timestamp_int()}', + catch_response=True) + locust.client.post('/rest/webResources/1.0/resources', params.resources_body.get("995"), + TEXT_HEADERS, catch_response=True) + locust.client.get(f'/rest/highlighting/1.0/panel-items?pageId={content_id}&_={timestamp_int()}', + catch_response=True) + locust.client.get(f'/rest/inlinecomments/1.0/comments?containerId={content_id}&_={timestamp_int()}', + catch_response=True) + locust.client.get(f'/s/en_GB/{build_number}/{keyboard_hash}/_/images/icons/profilepics/add_profile_pic.svg', + catch_response=True) + locust.client.get('/rest/helptips/1.0/tips', catch_response=True) + locust.client.get(f'/rest/mywork/latest/status/notification/count?pageid={content_id}&_={timestamp_int()}', + catch_response=True) + locust.client.get(f'/plugins/editor-loader/editor.action?parentPageId=&pageId={content_id}' + f'&spaceKey={parsed_space_key}&atl_after_login_redirect={created_blog_title}' + f'&timeout=12000&_={timestamp_int()}', catch_response=True) + locust.client.post('/rest/webResources/1.0/resources', params.resources_body.get("1030"), + TEXT_HEADERS, catch_response=True) + locust.client.post('/rest/webResources/1.0/resources', params.resources_body.get("1035"), + TEXT_HEADERS, catch_response=True) + + heartbeat_activity_body = {"dataType": "json", + "contentId": content_id, + "draftType": "blogpost", + "spaceKey": parsed_space_key, + "atl_token": atl_token + } + r = locust.client.post('/json/startheartbeatactivity.action', heartbeat_activity_body, + TEXT_HEADERS, catch_response=True) + content = r.content.decode('utf-8') + if atl_token not in content: + logger.error(f'Token {atl_token} not found in content: {content}') + assert atl_token in content, 'Token not found in content.' + + create_blog_editor() + create_blog() + + +def create_and_edit_page(locust): + params = CreateEditPage() + page = random.choice(confluence_dataset["pages"]) + page_id = page[0] + space_key = page[1] + build_number = locust.storage.get('build_number', '') + keyboard_hash = locust.storage.get('keyboard_hash', '') + + @confluence_measure + def create_page_editor(): + r = locust.client.get(f'/pages/createpage.action?spaceKey={space_key}&fromPageId={page_id}&src=quick-create', + catch_response=True) + content = r.content.decode('utf-8') + if 'Page Title' not in content: + logger.error(f'Could not open page editor: {content}') + assert 'Page Title' in content, 'Could not open page editor.' + + parsed_space_key = fetch_by_re(params.space_key_re, content) + atl_token = fetch_by_re(params.atl_token_re, content) + content_id = fetch_by_re(params.content_id_re, content) + locust.storage['content_id'] = content_id + locust.storage['atl_token'] = atl_token + + locust.client.post('/rest/webResources/1.0/resources', params.resources_body.get("705"), + TEXT_HEADERS, catch_response=True) + locust.client.post('/rest/webResources/1.0/resources', params.resources_body.get("710"), + TEXT_HEADERS, catch_response=True) + locust.client.post('/rest/webResources/1.0/resources', params.resources_body.get("715"), + TEXT_HEADERS, catch_response=True) + locust.client.get('/rest/create-dialog/1.0/storage/quick-create', catch_response=True) + locust.client.get(f'/rest/mywork/latest/status/notification/count?pageid=0&_={timestamp_int()}', + catch_response=True) + locust.client.get(f'/rest/jiraanywhere/1.0/servers?_={timestamp_int()}', catch_response=True) + locust.client.get(f'/rest/shortcuts/latest/shortcuts/{build_number}/{keyboard_hash}', catch_response=True) + locust.client.get(f'/rest/emoticons/1.0/?_={timestamp_int()}', catch_response=True) + locust.client.post('/rest/webResources/1.0/resources', params.resources_body.get("750"), + TEXT_HEADERS, catch_response=True) + + heartbeat_activity_body = {"dataType": "json", + "contentId": content_id, + "draftType": "page", + "spaceKey": parsed_space_key, + "atl_token": atl_token + } + r = locust.client.post('/json/startheartbeatactivity.action', heartbeat_activity_body, + TEXT_HEADERS, catch_response=True) + content = r.content.decode('utf-8') + if atl_token not in content: + logger.error(f'Token {atl_token} not found in content: {content}') + assert atl_token in content, 'Token not found in content.' + + @confluence_measure + def create_page(): + draft_name = f"{generate_random_string(10, only_letters=True)}" + content_id = locust.storage['content_id'] + atl_token = locust.storage['atl_token'] + create_page_body = { + "status": "current", + "title": f"Test Performance JMeter {draft_name}", + "space": {"key": f"{space_key}"}, + "body": { + "storage": { + "value": f"Test Performance Create Page Content {draft_name}", + "representation": "storage", + "content": { + "id": f"{content_id}" + } + } + }, + "id": f"{content_id}", + "type": "page", + "version": { + "number": 1 + }, + "ancestors": [ + { + "id": f"{page_id}", + "type": "page" + } + ] + } + + TEXT_HEADERS['Content-Type'] = 'application/json' + TEXT_HEADERS['X-Requested-With'] = 'XMLHttpRequest' + r = locust.client.put(f'/rest/api/content/{content_id}?status=draft', json=create_page_body, + headers=TEXT_HEADERS, catch_response=True) + content = r.content.decode('utf-8') + if 'draftId' not in content: + logger.error(f'Could not create PAGE draft: {content}') + assert 'draftId' in content, 'Could not create PAGE draft.' + page_title = fetch_by_re(params.page_title_re, content) + + r = locust.client.get(f'{page_title}', catch_response=True) + content = r.content.decode('utf-8') + if 'Created by' not in content: + logger.error(f'Page {page_title} was not created: {content}') + assert 'Created by' in content, 'Page was not created.' + + parent_page_id = fetch_by_re(params.parent_page_id, content) + create_page_id = fetch_by_re(params.create_page_id, content) + locust.storage['create_page_id'] = create_page_id + locust.storage['parent_page_id'] = parent_page_id + + heartbeat_activity_body = {"dataType": "json", + "contentId": content_id, + "space_key": space_key, + "draftType": "page", + "atl_token": atl_token + } + locust.client.post('/json/stopheartbeatactivity.action', params=heartbeat_activity_body, + headers=TEXT_HEADERS, catch_response=True) + + locust.client.get('/rest/helptips/1.0/tips', catch_response=True) + locust.client.post('/rest/webResources/1.0/resources', params.resources_body.get("795"), + TEXT_HEADERS, catch_response=True) + locust.client.get(f'/rest/jira-metadata/1.0/metadata/aggregate?pageId={create_page_id}&_={timestamp_int()}', + catch_response=True) + locust.client.get(f'/rest/likes/1.0/content/{create_page_id}/likes?commentLikes=true&_={timestamp_int()}', + catch_response=True) + locust.client.get(f'/rest/inlinecomments/1.0/comments?containerId={create_page_id}&_={timestamp_int()}', + catch_response=True) + locust.client.get(f'/rest/mywork/latest/status/notification/count?pageid={create_page_id}&_={timestamp_int()}', + catch_response=True) + locust.client.get(f'/rest/highlighting/1.0/panel-items?pageId={create_page_id}&_={timestamp_int()}', + catch_response=True) + locust.client.get(f'/rest/watch-button/1.0/watchState/{create_page_id}?_={timestamp_int()}', + catch_response=True) + locust.client.post('/rest/webResources/1.0/resources', params.resources_body.get("830"), + TEXT_HEADERS, catch_response=True) + locust.client.post('/rest/webResources/1.0/resources', params.resources_body.get("835"), + TEXT_HEADERS, catch_response=True) + + r = locust.client.get(f'/plugins/editor-loader/editor.action?parentPageId={parent_page_id}' + f'&pageId={create_page_id}&spaceKey={space_key}' + f'&atl_after_login_redirect={page_title}&timeout=12000&_={timestamp_int()}', + catch_response=True) + content = r.content.decode('utf-8') + if page_title not in content: + logger.error(f'Page editor load failed for page {page_title}: {content}') + assert page_title in content, 'Page editor load failed for page.' + + locust.client.post('/rest/webResources/1.0/resources', params.resources_body.get("845"), + TEXT_HEADERS, catch_response=True) + locust.client.post('/rest/webResources/1.0/resources', params.resources_body.get("850"), + TEXT_HEADERS, catch_response=True) + locust.client.post('/rest/webResources/1.0/resources', params.resources_body.get("855"), + TEXT_HEADERS, catch_response=True) + + @confluence_measure + def open_editor(): + create_page_id = locust.storage['create_page_id'] + + r = locust.client.get(f'/pages/editpage.action?pageId={create_page_id}', catch_response=True) + content = r.content.decode('utf-8') + if 'Edit' not in content or 'Update</button>' not in content: + logger.error(f'Could not open PAGE {create_page_id} to edit: {content}') + assert '<title>Edit' in content and 'Update</button>' in content, \ + 'Could not open PAGE to edit.' + + edit_page_version = fetch_by_re(params.editor_page_version_re, content) + edit_atl_token = fetch_by_re(params.atl_token_re, content) + edit_space_key = fetch_by_re(params.space_key_re, content) + edit_content_id = fetch_by_re(params.content_id_re, content) + edit_page_id = fetch_by_re(params.page_id_re, content) + edit_parent_page_id = fetch_by_re(params.parent_page_id, content) + + locust.storage['edit_parent_page_id'] = edit_parent_page_id + locust.storage['edit_page_version'] = edit_page_version + locust.storage['edit_page_id'] = edit_page_id + locust.storage['atl_token'] = edit_atl_token + locust.storage['edit_content_id'] = edit_content_id + + locust.client.get(f'/rest/jiraanywhere/1.0/servers?_={timestamp_int()}', catch_response=True) + heartbeat_activity_body = {"dataType": "json", + "contentId": edit_content_id, + "draftType": "page", + "spaceKey": edit_space_key, + "atl_token": edit_atl_token + } + locust.client.post('/json/startheartbeatactivity.action', heartbeat_activity_body, + TEXT_HEADERS, catch_response=True) + expand = 'history.createdBy.status%2Chistory.contributors.publishers.users.status' \ + '%2Cchildren.comment.version.by.status' + locust.client.get(f'/rest/api/content/{edit_page_id}?expand={expand}&_={timestamp_int()}', + catch_response=True) + locust.client.get(f'/rest/emoticons/1.0/_={timestamp_int()}', catch_response=True) + locust.client.post('/json/startheartbeatactivity.action', heartbeat_activity_body, + TEXT_HEADERS, catch_response=True) + locust.client.get(f'/rest/ui/1.0/content/{edit_page_id}/labels?_={timestamp_int()}', catch_response=True) + locust.client.get('/rest/mywork/latest/status/notification/count', catch_response=True) + locust.client.post('/json/startheartbeatactivity.action', heartbeat_activity_body, + TEXT_HEADERS, catch_response=True) + + @confluence_measure + def edit_page(): + locust.storage['draft_name'] = f"{generate_random_string(10, only_letters=True)}" + edit_parent_page_id = locust.storage['edit_parent_page_id'] + edit_page_id = locust.storage['edit_page_id'] + content_id = locust.storage['edit_content_id'] + edit_page_version = int(locust.storage['edit_page_version']) + 1 + edit_atl_token = locust.storage['atl_token'] + edit_page_body = dict() + + if edit_parent_page_id: + edit_page_body = { + "status": "current", + "title": f"Test Performance Edit with locust {locust.storage['draft_name']}", + "space": { + "key": f"{space_key}" + }, + "body": { + "storage": { + "value": f"Page edit with locust {locust.storage['draft_name']}", + "representation": "storage", + "content": { + "id": f"{content_id}" + } + } + }, + "id": f"{content_id}", + "type": "page", + "version": { + "number": f"{edit_page_version}" + }, + "ancestors": [ + { + "id": f"{edit_parent_page_id}", + "type": "page" + } + ] + } + + if not edit_parent_page_id: + edit_page_body = { + "status": "current", + "title": f"Test Performance Edit with locust {locust.storage['draft_name']}", + "space": { + "key": f"{space_key}" + }, + "body": { + "storage": { + "value": f"Page edit with locust {locust.storage['draft_name']}", + "representation": "storage", + "content": { + "id": f"{content_id}" + } + } + }, + "id": f"{content_id}", + "type": "page", + "version": { + "number": f"{edit_page_version}" + } + } + TEXT_HEADERS['Content-Type'] = 'application/json' + TEXT_HEADERS['X-Requested-With'] = 'XMLHttpRequest' + r = locust.client.put(f'/rest/api/content/{content_id}?status=draft', json=edit_page_body, + headers=TEXT_HEADERS, catch_response=True) + content = r.content.decode('utf-8') + + if 'history' not in content: + logger.info(f'Could not edit page. Response content: {content}') + if 'history' not in content: + logger.error(f'User {locust.user} could not edit page {content_id}, ' + f'parent page id: {edit_parent_page_id}: {content}') + assert 'history' in content, \ + 'User could not edit page.' + + r = locust.client.get(f'/pages/viewpage.action?pageId={edit_page_id}', catch_response=True) + content = r.content.decode('utf-8') + if not('last-modified' in content and 'Created by' in content): + logger.error(f"Could not open page {edit_page_id}: {content}") + assert 'last-modified' in content and 'Created by' in content, "Could not open page to edit." + + locust.client.get('/rest/mywork/latest/status/notification/count', catch_response=True) + heartbeat_activity_body = {"dataType": "json", + "contentId": content_id, + "space_key": space_key, + "draftType": "page", + "atl_token": edit_atl_token + } + locust.client.post('/json/stopheartbeatactivity.action', params=heartbeat_activity_body, + headers=TEXT_HEADERS, catch_response=True) + locust.client.get('/rest/helptips/1.0/tips', catch_response=True) + locust.client.post('/rest/webResources/1.0/resources', params.resources_body.get("1175"), + TEXT_HEADERS, catch_response=True) + locust.client.get(f'/rest/jira-metadata/1.0/metadata/aggregate?pageId={edit_page_id}&_={timestamp_int()}', + catch_response=True) + locust.client.get(f'/rest/likes/1.0/content/{edit_page_id}/likes?commentLikes=true&_={timestamp_int()}', + catch_response=True) + locust.client.get(f'/rest/highlighting/1.0/panel-items?pageId={edit_page_id}&_={timestamp_int()}', + catch_response=True) + locust.client.get(f'/rest/mywork/latest/status/notification/count?pageId={edit_page_id}&_={timestamp_int()}', + catch_response=True) + locust.client.get(f'/plugins/editor-loader/editor.action?parentPageId={edit_parent_page_id}' + f'&pageId={edit_page_id}&spaceKey={space_key}&atl_after_login_redirect=/pages/viewpage.action' + f'&timeout=12000&_={timestamp_int()}', catch_response=True) + locust.client.get(f'/rest/inlinecomments/1.0/comments?containerId={content_id}&_={timestamp_int()}', + catch_response=True) + locust.client.get(f'/rest/watch-button/1.0/watchState/{edit_page_id}?_={timestamp_int()}', + catch_response=True) + locust.client.post('/rest/webResources/1.0/resources', params.resources_body.get("1215"), + TEXT_HEADERS, catch_response=True) + locust.client.post('/rest/webResources/1.0/resources', params.resources_body.get("1220"), + TEXT_HEADERS, catch_response=True) + locust.client.post('/rest/webResources/1.0/resources', params.resources_body.get("1225"), + TEXT_HEADERS, catch_response=True) + locust.client.post('/rest/webResources/1.0/resources', params.resources_body.get("1230"), + TEXT_HEADERS, catch_response=True) + + create_page_editor() + create_page() + open_editor() + edit_page() + + +@confluence_measure +def comment_page(locust): + page = random.choice(confluence_dataset["pages"]) + page_id = page[0] + comment = f'<p>{generate_random_string(length=15, only_letters=True)}</p>' + uid = str(uuid.uuid4()) + r = locust.client.post(f'/rest/tinymce/1/content/{page_id}/comment?actions=true', + params={'html': comment, 'watch': True, 'uuid': uid}, headers=NO_TOKEN_HEADERS, + catch_response=True) + content = r.content.decode('utf-8') + if not('reply-comment' in content and 'edit-comment' in content): + logger.error(f'Could not add comment: {content}') + assert 'reply-comment' in content and 'edit-comment' in content, 'Could not add comment.' + + +@confluence_measure +def view_attachments(locust): + page = random.choice(confluence_dataset["pages"]) + page_id = page[0] + r = locust.client.get(f'/pages/viewpageattachments.action?pageId={page_id}', catch_response=True) + content = r.content.decode('utf-8') + if not('Upload file' in content and 'Attach more files' in content or 'currently no attachments' in content): + logger.error(f'View attachments failed: {content}') + assert 'Upload file' in content and 'Attach more files' in content \ + or 'currently no attachments' in content, 'View attachments failed.' + + +@confluence_measure +def upload_attachments(locust): + params = UploadAttachments() + page = random.choice(confluence_dataset["pages"]) + static_content = random.choice(confluence_dataset["static-content"]) + file_path = static_content[0] + file_name = static_content[2] + file_extension = static_content[1] + page_id = page[0] + + r = locust.client.get(f'/pages/viewpage.action?pageId={page_id}', catch_response=True) + content = r.content.decode('utf-8') + if not('Created by' in content and 'Save for later' in content): + logger.error(f'Failed to open page {page_id}: {content}') + assert 'Created by' in content and 'Save for later' in content, 'Failed to open page to upload attachments.' + atl_token_view_issue = fetch_by_re(params.atl_token_view_issue_re, content) + + multipart_form_data = { + "file": (file_name, open(file_path, 'rb'), file_extension) + } + + r = locust.client.post(f'/pages/doattachfile.action?pageId={page_id}', + params={"atl_token": atl_token_view_issue, "comment_0": "", "comment_1": "", "comment_2": "", + "comment_3": "", "comment_4": "0", "confirm": "Attach"}, files=multipart_form_data, + catch_response=True) + content = r.content.decode('utf-8') + if not('Upload file' in content and 'Attach more files' in content): + logger.error(f'Could not upload attachments: {content}') + assert 'Upload file' in content and 'Attach more files' in content, 'Could not upload attachments.' + + +@confluence_measure +def like_page(locust): + params = LikePage() + page = random.choice(confluence_dataset["pages"]) + page_id = page[0] + + JSON_HEADERS['Origin'] = CONFLUENCE_SETTINGS.server_url + r = locust.client.get(f'/rest/likes/1.0/content/{page_id}/likes', headers=JSON_HEADERS, catch_response=True) + content = r.content.decode('utf-8') + like = fetch_by_re(params.like_re, content) + + if like is None: + r = locust.client.post(f'/rest/likes/1.0/content/{page_id}/likes', headers=JSON_HEADERS, catch_response=True) + else: + r = locust.client.delete(f'/rest/likes/1.0/content/{page_id}/likes', catch_response=True) + + content = r.content.decode('utf-8') + if 'likes' not in content: + logger.error(f"Could not set like to the page {page_id}: {content}") + assert 'likes' in r.content.decode('utf-8'), 'Could not set like to the page.' diff --git a/app/locustio/confluence/locustfile.py b/app/locustio/confluence/locustfile.py new file mode 100644 index 000000000..b45ccff51 --- /dev/null +++ b/app/locustio/confluence/locustfile.py @@ -0,0 +1,65 @@ +from locust import HttpLocust, TaskSet, task, between +from locustio.confluence.http_actions import login_and_view_dashboard, view_page_and_tree, view_dashboard, view_blog, \ + search_cql_and_view_results, open_editor_and_create_blog, create_and_edit_page, comment_page, view_attachments, \ + upload_attachments, like_page +from locustio.common_utils import ActionPercentage +from util.conf import CONFLUENCE_SETTINGS +from extension.confluence.extension_locust import app_specific_action + +action = ActionPercentage(config_yml=CONFLUENCE_SETTINGS) + + +class ConfluenceBehavior(TaskSet): + + def on_start(self): + login_and_view_dashboard(self) + + @task(action.percentage('view_page')) + def view_page_action(self): + view_page_and_tree(self) + + @task(action.percentage('view_dashboard')) + def view_dashboard_action(self): + view_dashboard(self) + + @task(action.percentage('view_blog')) + def view_blog_action(self): + view_blog(self) + + @task(action.percentage('search_cql')) + def search_cql_action(self): + search_cql_and_view_results(self) + + @task(action.percentage('create_blog')) + def create_blog_action(self): + open_editor_and_create_blog(self) + + @task(action.percentage('create_and_edit_page')) + def create_and_edit_page_action(self): + create_and_edit_page(self) + + @task(action.percentage('comment_page')) + def comment_page_action(self): + comment_page(self) + + @task(action.percentage('view_attachment')) + def view_attachments_action(self): + view_attachments(self) + + @task(action.percentage('upload_attachment')) + def upload_attachments_action(self): + upload_attachments(self) + + @task(action.percentage('like_page')) + def like_page_action(self): + like_page(self) + + @task(action.percentage('standalone_extension')) + def custom_action(self): + app_specific_action(self) + + +class ConfluenceUser(HttpLocust): + host = CONFLUENCE_SETTINGS.server_url + task_set = ConfluenceBehavior + wait_time = between(0, 0) diff --git a/app/locustio/confluence/requests_params.py b/app/locustio/confluence/requests_params.py new file mode 100644 index 000000000..d8b2c5b78 --- /dev/null +++ b/app/locustio/confluence/requests_params.py @@ -0,0 +1,116 @@ +# flake8: noqa +from locustio.common_utils import generate_random_string, read_input_file +from util.project_paths import CONFLUENCE_PAGES, CONFLUENCE_BLOGS, CONFLUENCE_USERS, CONFLUENCE_STATIC_CONTENT +import json + + +def confluence_datasets(): + data_sets = dict() + data_sets["pages"] = read_input_file(CONFLUENCE_PAGES) + data_sets["blogs"] = read_input_file(CONFLUENCE_BLOGS) + data_sets["users"] = read_input_file(CONFLUENCE_USERS) + data_sets['static-content'] = read_input_file(CONFLUENCE_STATIC_CONTENT) + + return data_sets + + +class BaseResource: + resources_file = 'locustio/confluence/resources.json' + action_name = '' + storage = dict() + + def __init__(self): + self.resources_json = self.read_json() + self.resources_body = self.action_resources() + + def read_json(self): + with open(self.resources_file) as f: + return json.load(f) + + def action_resources(self): + return self.resources_json[self.action_name] if self.action_name in self.resources_json else dict() + + +class Login(BaseResource): + action_name = 'login_and_view_dashboard' + login_body = { + 'os_username': '', + 'os_password': '', + 'os_cookie': True, + 'os_destination': '', + 'login': 'Log in' + } + keyboard_hash_re = 'name=\"ajs-keyboardshortcut-hash\" content=\"(.*?)\">' + static_resource_url_re = 'meta name=\"ajs-static-resource-url-prefix\" content=\"(.*?)/_\">' + version_number_re = 'meta name=\"ajs-version-number\" content=\"(.*?)\">' + build_number_re = 'meta name=\"ajs-build-number\" content=\"(.*?)\"' + + +class ViewPage(BaseResource): + action_name = 'view_page' + parent_page_id_re = 'meta name=\"ajs-parent-page-id\" content=\"(.*?)\"' + page_id_re = 'meta name=\"ajs-page-id\" content=\"(.*?)\">' + space_key_re = 'meta id=\"confluence-space-key\" name=\"confluence-space-key\" content=\"(.*?)\"' + ancestor_ids_re = 'name=\"ancestorId\" value=\"(.*?)\"' + tree_result_id_re = 'name="treeRequestId" value="(.+?)"' + has_no_root_re = '"noRoot" value="(.+?)"' + root_page_id_re = 'name="rootPageId" value="(.+?)"' + atl_token_view_issue_re = '"ajs-atl-token" content="(.+?)"' + editable_re = 'id=\"editPageLink\" href="(.+?)\?pageId=(.+?)\"' + inline_comment_re = '\"id\":(.+?)\,\"' + + +class ViewDashboard(BaseResource): + action_name = 'view_dashboard' + keyboard_hash_re = 'name=\"ajs-keyboardshortcut-hash\" content=\"(.*?)\">' + static_resource_url_re = 'meta name=\"ajs-static-resource-url-prefix\" content=\"(.*?)/_\">' + version_number_re = 'meta name=\"ajs-version-number\" content=\"(.*?)\">' + build_number_re = 'meta name=\"ajs-build-number\" content=\"(.*?)\"' + + +class ViewBlog(BaseResource): + action_name = 'view_blog' + parent_page_id_re = 'meta name=\"ajs-parent-page-id\" content=\"(.*?)\"' + page_id_re = 'meta name=\"ajs-page-id\" content=\"(.*?)\">' + space_key_re = 'meta id=\"confluence-space-key\" name=\"confluence-space-key\" content=\"(.*?)\"' + atl_token_re = '"ajs-atl-token" content="(.+?)"' + inline_comment_re = '\"id\":(.+?)\,\"' + + +class CreateBlog(BaseResource): + action_name = 'create_blog' + atl_token_re = 'name=\"ajs-atl-token\" content=\"(.*?)\">' + content_id_re = 'name=\"ajs-content-id\" content=\"(.*?)\">' + space_key = 'createpage.action\?spaceKey=(.+?)\&' + contribution_hash = '\"contributorsHash\":\"\"' + + created_blog_title_re = 'anonymous_export_view.*?\"webui\":\"(.*?)\"' + + +class CreateEditPage(BaseResource): + content_id_re = 'meta name=\"ajs-content-id\" content=\"(.*?)\">' + atl_token_re = 'meta name=\"ajs-atl-token\" content=\"(.*?)\">' + space_key_re = 'createpage.action\?spaceKey=(.+?)\&' + page_title_re = 'anonymous_export_view.*?\"webui\":\"(.*?)\"' + page_id_re = 'meta name=\"ajs-page-id\" content=\"(.*?)\">' + parent_page_id = 'meta name=\"ajs-parent-page-id\" content=\"(.*?)\"' + create_page_id = 'meta name=\"ajs-page-id\" content=\"(.*?)\">' + + editor_page_title_re = 'name=\"ajs-page-title\" content=\"(.*?)\"' + editor_page_version_re = 'name=\"ajs-page-version\" content=\"(.*?)\">' + editor_page_content_re = 'id=\"wysiwygTextarea\" name=\"wysiwygContent\" class=\ + "hidden tinymce-editor\">([\w\W]*?)</textarea>' + + +class CommentPage(BaseResource): + action_name = 'comment_page' + + +class UploadAttachments(BaseResource): + action_name = 'upload_attachments' + atl_token_view_issue_re = '"ajs-atl-token" content="(.+?)"' + + +class LikePage(BaseResource): + action_name = 'like_page' + like_re = '\{\"likes\":\[\{"user":\{"name\"\:\"(.+?)",' diff --git a/app/locustio/confluence/resources.json b/app/locustio/confluence/resources.json new file mode 100644 index 000000000..517dcc566 --- /dev/null +++ b/app/locustio/confluence/resources.json @@ -0,0 +1,51 @@ +{ + "login_and_view_dashboard": { + "010": {"r":[],"c":["browser-metrics-plugin.contrib"],"xc":["_super","baseurl-checker-resource","dashboard","backbone-dashboard","atl.general","main"],"xr":["confluence.macros.advanced:blogpost-resources"]}, + "025": {"r":[],"c":["com.atlassian.confluence.extra.team-calendars.resources-batch"],"xc":["_super","baseurl-checker-resource","dashboard","backbone-dashboard","atl.general","main"],"xr":["confluence.macros.advanced:blogpost-resources"]} + }, + "view_page": { + "110": {"r":[],"c":["browser-metrics-plugin.contrib"],"xc":["_super","baseurl-checker-resource","atl.confluence.plugins.pagetree-desktop","main","viewcontent","page","atl.general","atl.comments"],"xr":[]}, + "145": {"r":[],"c":["request-access-plugin"],"xc":["_super","baseurl-checker-resource","atl.confluence.plugins.pagetree-desktop","main","viewcontent","page","atl.general","atl.comments"],"xr":[]}, + "150": {"r":[],"c":["editor-v4","editor","macro-browser","fullpage-editor"],"xc":["_super","baseurl-checker-resource","atl.confluence.plugins.pagetree-desktop","main","viewcontent","page","atl.general","atl.comments"],"xr":[]}, + "155": {"r":["confluence.web.resources.colors:colors","com.atlassian.confluence.keyboardshortcuts:confluence-keyboard-shortcuts","com.atlassian.confluence.editor:editor-resources-conf-frontend","com.atlassian.confluence.plugins.confluence-browser-metrics:editor","confluence.web.resources:quicksearchdropdown","com.atlassian.confluence.editor:property-panel-image-link-macro-conf-frontend","confluence.web.resources:legacy-editor-global-AVOID-IF-POSSIBLE","confluence.web.resources:hint-manager","confluence.web.resources:raphael","com.atlassian.confluence.plugins.confluence-link-browser:link-object","confluence.web.resources:core","com.atlassian.confluence.plugins.confluence-page-layout:pagelayout-frontend","com.atlassian.confluence.plugins.confluence-link-browser:link-browser-conf-frontend","confluence.extra.jira:macro-browser-resources","confluence.extra.jira:dialogsJs-for-conf-frontend","confluence.extra.jira:jirachart-macro","confluence.web.resources:navigator-context","com.atlassian.plugins.atlassian-connect-plugin:confluence-atlassian-connect-resources-v5","com.atlassian.plugins.atlassian-connect-plugin:dynamic-css-resources","com.atlassian.plugins.atlassian-connect-plugin:featured-macro-css-resources","com.atlassian.plugins.atlassian-connect-plugin:confluence-atlassian-connect-autoconvert-resources-v5","com.atlassian.plugins.atlassian-connect-plugin:macro-editor-resources-v5","com.atlassian.confluence.plugins.confluence-inline-tasks:editor-autocomplete-date-conf-frontend","confluence.web.resources:breadcrumbs","com.atlassian.confluence.plugins.confluence-collaborative-editor-plugin:confluence-collaborative-editor-plugin-resources","com.atlassian.confluence.keyboardshortcuts:confluence-keyboard-shortcuts","com.atlassian.confluence.plugins.confluence-labels:labels-editor","confluence.web.resources:ajs","com.atlassian.auiplugin:aui-inline-dialog2","confluence.web.resources:legacy-editor-global-AVOID-IF-POSSIBLE","com.atlassian.confluence.plugins.drag-and-drop:drag-and-drop-for-editor-conf-frontend","com.atlassian.confluence.plugins.gadgets:macro-browser-for-gadgetsplugin","com.atlassian.confluence.plugins.confluence-hipchat-emoticons-plugin:hipchat-emoticons-conf-frontend","com.atlassian.confluence.plugins.confluence-invite-to-edit:edit-resources","confluence.macros.multimedia:macro-browser-smart-fields","com.atlassian.confluence.editor:property-panel-image-link-macro-conf-frontend","com.atlassian.confluence.editor:property-panel-image-link-macro-conf-frontend","com.atlassian.confluence.image.effects.ImageEffectsPlugin:propertiespanel","com.atlassian.confluence.plugins.confluence-roadmap-plugin:roadmap-editor-resources-conf-frontend","com.atlassian.confluence.plugins.confluence-view-file-macro:view-file-macro-editor-resources","confluence.macros.advanced:editor_includemacro-conf-frontend","com.atlassian.confluence.plugins.confluence-request-access-plugin:confluence-request-access-plugin-resources","confluence.web.resources:deferred-dialog-loader","com.atlassian.confluence.plugins.confluence-create-content-plugin:editor-resources","com.atlassian.confluence.plugins.confluence-create-content-plugin:create-from-template-macro-browser-resources","com.atlassian.confluence.plugins.confluence-create-content-plugin:create-from-template-resources","com.atlassian.confluence.plugins.confluence-inline-tasks:inline-tasks-macro-browser","com.atlassian.confluence.extra.team-calendars:amd","com.atlassian.confluence.extra.team-calendars:user-timezone-setup","com.atlassian.confluence.extra.team-calendars:calendar-init-editor","com.atlassian.confluence.extra.team-calendars:macro-browser-web-resources","confluence.extra.jira:text-placeholders-jira"],"c":[],"xc":["_super","baseurl-checker-resource","atl.confluence.plugins.pagetree-desktop","main","viewcontent","page","atl.general","atl.comments"],"xr":[]}, + "160": {"r":[],"c":["com.atlassian.confluence.extra.team-calendars.resources-batch"],"xc":["_super","baseurl-checker-resource","atl.confluence.plugins.pagetree-desktop","main","viewcontent","page","atl.general","atl.comments"],"xr":[]} + }, + "view_dashboard": { + "205": {"r":[],"c":["browser-metrics-plugin.contrib"],"xc":["_super","baseurl-checker-resource","dashboard","backbone-dashboard","atl.general","main"],"xr":["confluence.macros.advanced:blogpost-resources"]}, + "220": {"r":[],"c":["com.atlassian.confluence.extra.team-calendars.resources-batch"],"xc":["_super","baseurl-checker-resource","dashboard","backbone-dashboard","atl.general","main"],"xr":["confluence.macros.advanced:blogpost-resources"]} + }, + "view_blog": { + "310": {"r":[],"c":["browser-metrics-plugin.contrib"],"xc":["_super","baseurl-checker-resource","atl.confluence.plugins.pagetree-desktop","main","viewcontent","page","atl.general","atl.comments"],"xr":[]}, + "345": {"r":[],"c":["request-access-plugin"],"xc":["_super","baseurl-checker-resource","atl.confluence.plugins.pagetree-desktop","main","viewcontent","page","atl.general","atl.comments"],"xr":[]}, + "350": {"r":[],"c":["editor-v4","editor","macro-browser","fullpage-editor"],"xc":["_super","baseurl-checker-resource","atl.confluence.plugins.pagetree-desktop","main","viewcontent","page","atl.general","atl.comments"],"xr":[]}, + "355": {"r":["confluence.web.resources.colors:colors","com.atlassian.confluence.keyboardshortcuts:confluence-keyboard-shortcuts","com.atlassian.confluence.editor:editor-resources-conf-frontend","com.atlassian.confluence.plugins.confluence-browser-metrics:editor","confluence.web.resources:quicksearchdropdown","com.atlassian.confluence.editor:property-panel-image-link-macro-conf-frontend","confluence.web.resources:legacy-editor-global-AVOID-IF-POSSIBLE","confluence.web.resources:hint-manager","confluence.web.resources:raphael","com.atlassian.confluence.plugins.confluence-link-browser:link-object","confluence.web.resources:core","com.atlassian.confluence.plugins.confluence-page-layout:pagelayout-frontend","com.atlassian.confluence.plugins.confluence-link-browser:link-browser-conf-frontend","confluence.extra.jira:macro-browser-resources","confluence.extra.jira:dialogsJs-for-conf-frontend","confluence.extra.jira:jirachart-macro","confluence.web.resources:navigator-context","com.atlassian.plugins.atlassian-connect-plugin:confluence-atlassian-connect-resources-v5","com.atlassian.plugins.atlassian-connect-plugin:dynamic-css-resources","com.atlassian.plugins.atlassian-connect-plugin:featured-macro-css-resources","com.atlassian.plugins.atlassian-connect-plugin:confluence-atlassian-connect-autoconvert-resources-v5","com.atlassian.plugins.atlassian-connect-plugin:macro-editor-resources-v5","com.atlassian.confluence.plugins.confluence-inline-tasks:editor-autocomplete-date-conf-frontend","confluence.web.resources:breadcrumbs","com.atlassian.confluence.plugins.confluence-collaborative-editor-plugin:confluence-collaborative-editor-plugin-resources","com.atlassian.confluence.keyboardshortcuts:confluence-keyboard-shortcuts","com.atlassian.confluence.plugins.confluence-labels:labels-editor","confluence.web.resources:ajs","com.atlassian.auiplugin:aui-inline-dialog2","confluence.web.resources:legacy-editor-global-AVOID-IF-POSSIBLE","com.atlassian.confluence.plugins.drag-and-drop:drag-and-drop-for-editor-conf-frontend","com.atlassian.confluence.plugins.gadgets:macro-browser-for-gadgetsplugin","com.atlassian.confluence.plugins.confluence-hipchat-emoticons-plugin:hipchat-emoticons-conf-frontend","com.atlassian.confluence.plugins.confluence-invite-to-edit:edit-resources","confluence.macros.multimedia:macro-browser-smart-fields","com.atlassian.confluence.editor:property-panel-image-link-macro-conf-frontend","com.atlassian.confluence.editor:property-panel-image-link-macro-conf-frontend","com.atlassian.confluence.image.effects.ImageEffectsPlugin:propertiespanel","com.atlassian.confluence.plugins.confluence-roadmap-plugin:roadmap-editor-resources-conf-frontend","com.atlassian.confluence.plugins.confluence-view-file-macro:view-file-macro-editor-resources","confluence.macros.advanced:editor_includemacro-conf-frontend","com.atlassian.confluence.plugins.confluence-request-access-plugin:confluence-request-access-plugin-resources","confluence.web.resources:deferred-dialog-loader","com.atlassian.confluence.plugins.confluence-create-content-plugin:editor-resources","com.atlassian.confluence.plugins.confluence-create-content-plugin:create-from-template-macro-browser-resources","com.atlassian.confluence.plugins.confluence-create-content-plugin:create-from-template-resources","com.atlassian.confluence.plugins.confluence-inline-tasks:inline-tasks-macro-browser","com.atlassian.confluence.extra.team-calendars:amd","com.atlassian.confluence.extra.team-calendars:user-timezone-setup","com.atlassian.confluence.extra.team-calendars:calendar-init-editor","com.atlassian.confluence.extra.team-calendars:macro-browser-web-resources","confluence.extra.jira:text-placeholders-jira"],"c":[],"xc":["_super","baseurl-checker-resource","atl.confluence.plugins.pagetree-desktop","main","viewcontent","page","atl.general","atl.comments"],"xr":[]}, + "360": {"r":[],"c":["com.atlassian.confluence.extra.team-calendars.resources-batch"],"xc":["_super","baseurl-checker-resource","atl.confluence.plugins.pagetree-desktop","main","viewcontent","page","atl.general","atl.comments"],"xr":[]} + }, + "create_blog": { + "910": {"r":[],"c":["browser-metrics-plugin.contrib"],"xc":["_super","baseurl-checker-resource","editor-v4","atl.general","editor","macro-browser","main","blogpost"],"xr":["com.atlassian.confluence.plugins.confluence-space-ia:spacesidebar"]}, + "925": {"r":[],"c":["com.atlassian.confluence.extra.team-calendars.resources-batch"],"xc":["_super","baseurl-checker-resource","editor-v4","atl.general","editor","macro-browser","main","blogpost"],"xr":["com.atlassian.confluence.plugins.confluence-space-ia:spacesidebar"]}, + "930": {"r":[],"c":["request-access-plugin"],"xc":["_super","baseurl-checker-resource","editor-v4","atl.general","editor","macro-browser","main","blogpost"],"xr":["com.atlassian.confluence.plugins.confluence-space-ia:spacesidebar"]}, + "970": {"r":[],"c":["browser-metrics-plugin.contrib"],"xc":["_super","baseurl-checker-resource","blogpost","viewcontent","main","atl.general","atl.comments"],"xr":[]}, + "975": {"r":[],"c":["sortable-tables-resources"],"xc":["_super","baseurl-checker-resource","blogpost","viewcontent","main","atl.general","atl.comments"],"xr":[]}, + "995": {"r":[],"c":["request-access-plugin"],"xc":["_super","baseurl-checker-resource","blogpost","viewcontent","main","atl.general","atl.comments"],"xr":[]}, + "1030": {"r":[],"c":["editor-v4","editor","macro-browser","fullpage-editor"],"xc":["_super","baseurl-checker-resource","blogpost","viewcontent","main","atl.general","atl.comments"],"xr":[]}, + "1035": {"r":[],"c":["com.atlassian.confluence.extra.team-calendars.resources-batch"],"xc":["_super","baseurl-checker-resource","blogpost","viewcontent","main","atl.general","atl.comments"],"xr":[]} + }, + "create_and_edit_page": { + "705": {"r":["confluence.web.resources.colors:colors","com.atlassian.confluence.keyboardshortcuts:confluence-keyboard-shortcuts","com.atlassian.confluence.editor:editor-resources-conf-frontend","com.atlassian.confluence.plugins.confluence-browser-metrics:editor","confluence.web.resources:quicksearchdropdown","com.atlassian.confluence.editor:property-panel-image-link-macro-conf-frontend","confluence.web.resources:legacy-editor-global-AVOID-IF-POSSIBLE","confluence.web.resources:hint-manager","confluence.web.resources:raphael","com.atlassian.confluence.plugins.confluence-link-browser:link-object","confluence.web.resources:core","com.atlassian.confluence.plugins.confluence-page-layout:pagelayout-frontend","com.atlassian.confluence.plugins.confluence-link-browser:link-browser-conf-frontend","confluence.extra.jira:macro-browser-resources","confluence.extra.jira:dialogsJs-for-conf-frontend","confluence.extra.jira:jirachart-macro","confluence.web.resources:navigator-context","com.atlassian.plugins.atlassian-connect-plugin:confluence-atlassian-connect-resources-v5","com.atlassian.plugins.atlassian-connect-plugin:dynamic-css-resources","com.atlassian.plugins.atlassian-connect-plugin:featured-macro-css-resources","com.atlassian.plugins.atlassian-connect-plugin:confluence-atlassian-connect-autoconvert-resources-v5","com.atlassian.plugins.atlassian-connect-plugin:macro-editor-resources-v5","com.atlassian.confluence.plugins.confluence-inline-tasks:editor-autocomplete-date-conf-frontend","confluence.web.resources:breadcrumbs","com.atlassian.confluence.plugins.confluence-collaborative-editor-plugin:confluence-collaborative-editor-plugin-resources","com.atlassian.confluence.keyboardshortcuts:confluence-keyboard-shortcuts","com.atlassian.confluence.plugins.confluence-labels:labels-editor","confluence.web.resources:ajs","com.atlassian.auiplugin:aui-inline-dialog2","confluence.web.resources:legacy-editor-global-AVOID-IF-POSSIBLE","com.atlassian.confluence.plugins.drag-and-drop:drag-and-drop-for-editor-conf-frontend","com.atlassian.confluence.plugins.gadgets:macro-browser-for-gadgetsplugin","com.atlassian.confluence.plugins.confluence-hipchat-emoticons-plugin:hipchat-emoticons-conf-frontend","com.atlassian.confluence.plugins.confluence-invite-to-edit:edit-resources","confluence.macros.multimedia:macro-browser-smart-fields","com.atlassian.confluence.editor:property-panel-image-link-macro-conf-frontend","com.atlassian.confluence.editor:property-panel-image-link-macro-conf-frontend","com.atlassian.confluence.image.effects.ImageEffectsPlugin:propertiespanel","com.atlassian.confluence.plugins.confluence-roadmap-plugin:roadmap-editor-resources-conf-frontend","com.atlassian.confluence.plugins.confluence-view-file-macro:view-file-macro-editor-resources","confluence.macros.advanced:editor_includemacro-conf-frontend","com.atlassian.confluence.plugins.confluence-request-access-plugin:confluence-request-access-plugin-resources","confluence.web.resources:deferred-dialog-loader","com.atlassian.confluence.plugins.confluence-create-content-plugin:editor-resources","com.atlassian.confluence.plugins.confluence-create-content-plugin:create-from-template-macro-browser-resources","com.atlassian.confluence.plugins.confluence-create-content-plugin:create-from-template-resources","com.atlassian.confluence.plugins.confluence-inline-tasks:inline-tasks-macro-browser","com.atlassian.confluence.extra.team-calendars:amd","com.atlassian.confluence.extra.team-calendars:user-timezone-setup","com.atlassian.confluence.extra.team-calendars:calendar-init-editor","com.atlassian.confluence.extra.team-calendars:macro-browser-web-resources","confluence.extra.jira:text-placeholders-jira"],"c":[],"xc":["_super","baseurl-checker-resource","editor-v4","editor","macro-browser","atl.general","main","page"],"xr":["com.atlassian.confluence.plugins.confluence-space-ia:spacesidebar"]}, + "710": {"r":[],"c":["browser-metrics-plugin.contrib"],"xc":["_super","baseurl-checker-resource","editor-v4","editor","macro-browser","atl.general","main","page"],"xr":["com.atlassian.confluence.plugins.confluence-space-ia:spacesidebar"]}, + "715": {"r":[],"c":["com.atlassian.confluence.extra.team-calendars.resources-batch"],"xc":["_super","baseurl-checker-resource","editor-v4","editor","macro-browser","atl.general","main","page"],"xr":["com.atlassian.confluence.plugins.confluence-space-ia:spacesidebar"]}, + "750": {"r":[],"c":["request-access-plugin"],"xc":["_super","baseurl-checker-resource","editor-v4","editor","macro-browser","atl.general","main","page"],"xr":["com.atlassian.confluence.plugins.confluence-space-ia:spacesidebar"]}, + "795": {"r":[],"c":["browser-metrics-plugin.contrib"],"xc":["_super","baseurl-checker-resource","atl.confluence.plugins.pagetree-desktop","main","viewcontent","page","atl.general","atl.comments"],"xr":[]}, + "830": {"r":[],"c":["sortable-tables-resources"],"xc":["_super","baseurl-checker-resource","atl.confluence.plugins.pagetree-desktop","main","viewcontent","page","atl.general","atl.comments"],"xr":[]}, + "835": {"r":[],"c":["request-access-plugin"],"xc":["_super","baseurl-checker-resource","atl.confluence.plugins.pagetree-desktop","main","viewcontent","page","atl.general","atl.comments"],"xr":[]}, + "845": {"r":[],"c":["editor-v4","editor","macro-browser","fullpage-editor"],"xc":["_super","baseurl-checker-resource","atl.confluence.plugins.pagetree-desktop","main","viewcontent","page","atl.general","atl.comments"],"xr":[]}, + "850": {"r":["confluence.web.resources.colors:colors","com.atlassian.confluence.keyboardshortcuts:confluence-keyboard-shortcuts","com.atlassian.confluence.editor:editor-resources-conf-frontend","com.atlassian.confluence.plugins.confluence-browser-metrics:editor","confluence.web.resources:quicksearchdropdown","com.atlassian.confluence.editor:property-panel-image-link-macro-conf-frontend","confluence.web.resources:legacy-editor-global-AVOID-IF-POSSIBLE","confluence.web.resources:hint-manager","confluence.web.resources:raphael","com.atlassian.confluence.plugins.confluence-link-browser:link-object","confluence.web.resources:core","com.atlassian.confluence.plugins.confluence-page-layout:pagelayout-frontend","com.atlassian.confluence.plugins.confluence-link-browser:link-browser-conf-frontend","confluence.extra.jira:macro-browser-resources","confluence.extra.jira:dialogsJs-for-conf-frontend","confluence.extra.jira:jirachart-macro","confluence.web.resources:navigator-context","com.atlassian.plugins.atlassian-connect-plugin:confluence-atlassian-connect-resources-v5","com.atlassian.plugins.atlassian-connect-plugin:dynamic-css-resources","com.atlassian.plugins.atlassian-connect-plugin:featured-macro-css-resources","com.atlassian.plugins.atlassian-connect-plugin:confluence-atlassian-connect-autoconvert-resources-v5","com.atlassian.plugins.atlassian-connect-plugin:macro-editor-resources-v5","com.atlassian.confluence.plugins.confluence-inline-tasks:editor-autocomplete-date-conf-frontend","confluence.web.resources:breadcrumbs","com.atlassian.confluence.plugins.confluence-collaborative-editor-plugin:confluence-collaborative-editor-plugin-resources","com.atlassian.confluence.keyboardshortcuts:confluence-keyboard-shortcuts","com.atlassian.confluence.plugins.confluence-labels:labels-editor","confluence.web.resources:ajs","com.atlassian.auiplugin:aui-inline-dialog2","confluence.web.resources:legacy-editor-global-AVOID-IF-POSSIBLE","com.atlassian.confluence.plugins.drag-and-drop:drag-and-drop-for-editor-conf-frontend","com.atlassian.confluence.plugins.gadgets:macro-browser-for-gadgetsplugin","com.atlassian.confluence.plugins.confluence-hipchat-emoticons-plugin:hipchat-emoticons-conf-frontend","com.atlassian.confluence.plugins.confluence-invite-to-edit:edit-resources","confluence.macros.multimedia:macro-browser-smart-fields","com.atlassian.confluence.editor:property-panel-image-link-macro-conf-frontend","com.atlassian.confluence.editor:property-panel-image-link-macro-conf-frontend","com.atlassian.confluence.image.effects.ImageEffectsPlugin:propertiespanel","com.atlassian.confluence.plugins.confluence-roadmap-plugin:roadmap-editor-resources-conf-frontend","com.atlassian.confluence.plugins.confluence-view-file-macro:view-file-macro-editor-resources","confluence.macros.advanced:editor_includemacro-conf-frontend","com.atlassian.confluence.plugins.confluence-request-access-plugin:confluence-request-access-plugin-resources","confluence.web.resources:deferred-dialog-loader","com.atlassian.confluence.plugins.confluence-create-content-plugin:editor-resources","com.atlassian.confluence.plugins.confluence-create-content-plugin:create-from-template-macro-browser-resources","com.atlassian.confluence.plugins.confluence-create-content-plugin:create-from-template-resources","com.atlassian.confluence.plugins.confluence-inline-tasks:inline-tasks-macro-browser","com.atlassian.confluence.extra.team-calendars:amd","com.atlassian.confluence.extra.team-calendars:user-timezone-setup","com.atlassian.confluence.extra.team-calendars:calendar-init-editor","com.atlassian.confluence.extra.team-calendars:macro-browser-web-resources","confluence.extra.jira:text-placeholders-jira"],"c":[],"xc":["_super","baseurl-checker-resource","atl.confluence.plugins.pagetree-desktop","main","viewcontent","page","atl.general","atl.comments"],"xr":[]}, + "855": {"r":[],"c":["com.atlassian.confluence.extra.team-calendars.resources-batch"],"xc":["_super","baseurl-checker-resource","atl.confluence.plugins.pagetree-desktop","main","viewcontent","page","atl.general","atl.comments"],"xr":[]}, + "1175": {"r":[],"c":["browser-metrics-plugin.contrib"],"xc":["_super","baseurl-checker-resource","atl.confluence.plugins.pagetree-desktop","main","viewcontent","page","atl.general","atl.comments"],"xr":[]}, + "1215": {"r":[],"c":["request-access-plugin"],"xc":["_super","baseurl-checker-resource","atl.confluence.plugins.pagetree-desktop","main","viewcontent","page","atl.general","atl.comments"],"xr":[]}, + "1220": {"r":[],"c":["editor-v4","editor","macro-browser","fullpage-editor"],"xc":["_super","baseurl-checker-resource","atl.confluence.plugins.pagetree-desktop","main","viewcontent","page","atl.general","atl.comments"],"xr":[]}, + "1225": {"r":["confluence.web.resources.colors:colors","com.atlassian.confluence.keyboardshortcuts:confluence-keyboard-shortcuts","com.atlassian.confluence.editor:editor-resources-conf-frontend","com.atlassian.confluence.plugins.confluence-browser-metrics:editor","confluence.web.resources:quicksearchdropdown","com.atlassian.confluence.editor:property-panel-image-link-macro-conf-frontend","confluence.web.resources:legacy-editor-global-AVOID-IF-POSSIBLE","confluence.web.resources:hint-manager","confluence.web.resources:raphael","com.atlassian.confluence.plugins.confluence-link-browser:link-object","confluence.web.resources:core","com.atlassian.confluence.plugins.confluence-page-layout:pagelayout-frontend","com.atlassian.confluence.plugins.confluence-link-browser:link-browser-conf-frontend","confluence.extra.jira:macro-browser-resources","confluence.extra.jira:dialogsJs-for-conf-frontend","confluence.extra.jira:jirachart-macro","confluence.web.resources:navigator-context","com.atlassian.plugins.atlassian-connect-plugin:confluence-atlassian-connect-resources-v5","com.atlassian.plugins.atlassian-connect-plugin:dynamic-css-resources","com.atlassian.plugins.atlassian-connect-plugin:featured-macro-css-resources","com.atlassian.plugins.atlassian-connect-plugin:confluence-atlassian-connect-autoconvert-resources-v5","com.atlassian.plugins.atlassian-connect-plugin:macro-editor-resources-v5","com.atlassian.confluence.plugins.confluence-inline-tasks:editor-autocomplete-date-conf-frontend","confluence.web.resources:breadcrumbs","com.atlassian.confluence.plugins.confluence-collaborative-editor-plugin:confluence-collaborative-editor-plugin-resources","com.atlassian.confluence.keyboardshortcuts:confluence-keyboard-shortcuts","com.atlassian.confluence.plugins.confluence-labels:labels-editor","confluence.web.resources:ajs","com.atlassian.auiplugin:aui-inline-dialog2","confluence.web.resources:legacy-editor-global-AVOID-IF-POSSIBLE","com.atlassian.confluence.plugins.drag-and-drop:drag-and-drop-for-editor-conf-frontend","com.atlassian.confluence.plugins.gadgets:macro-browser-for-gadgetsplugin","com.atlassian.confluence.plugins.confluence-hipchat-emoticons-plugin:hipchat-emoticons-conf-frontend","com.atlassian.confluence.plugins.confluence-invite-to-edit:edit-resources","confluence.macros.multimedia:macro-browser-smart-fields","com.atlassian.confluence.editor:property-panel-image-link-macro-conf-frontend","com.atlassian.confluence.editor:property-panel-image-link-macro-conf-frontend","com.atlassian.confluence.image.effects.ImageEffectsPlugin:propertiespanel","com.atlassian.confluence.plugins.confluence-roadmap-plugin:roadmap-editor-resources-conf-frontend","com.atlassian.confluence.plugins.confluence-view-file-macro:view-file-macro-editor-resources","confluence.macros.advanced:editor_includemacro-conf-frontend","com.atlassian.confluence.plugins.confluence-request-access-plugin:confluence-request-access-plugin-resources","confluence.web.resources:deferred-dialog-loader","com.atlassian.confluence.plugins.confluence-create-content-plugin:editor-resources","com.atlassian.confluence.plugins.confluence-create-content-plugin:create-from-template-macro-browser-resources","com.atlassian.confluence.plugins.confluence-create-content-plugin:create-from-template-resources","com.atlassian.confluence.plugins.confluence-inline-tasks:inline-tasks-macro-browser","com.atlassian.confluence.extra.team-calendars:amd","com.atlassian.confluence.extra.team-calendars:user-timezone-setup","com.atlassian.confluence.extra.team-calendars:calendar-init-editor","com.atlassian.confluence.extra.team-calendars:macro-browser-web-resources","confluence.extra.jira:text-placeholders-jira"],"c":[],"xc":["_super","baseurl-checker-resource","atl.confluence.plugins.pagetree-desktop","main","viewcontent","page","atl.general","atl.comments"],"xr":[]}, + "1230": {"r":[],"c":["com.atlassian.confluence.extra.team-calendars.resources-batch"],"xc":["_super","baseurl-checker-resource","atl.confluence.plugins.pagetree-desktop","main","viewcontent","page","atl.general","atl.comments"],"xr":[]} + } +} \ No newline at end of file diff --git a/app/locustio/jira/http_actions.py b/app/locustio/jira/http_actions.py new file mode 100644 index 000000000..5afb0a759 --- /dev/null +++ b/app/locustio/jira/http_actions.py @@ -0,0 +1,476 @@ +import random +import re +from locust.exception import ResponseError +from locustio.jira.requests_params import Login, BrowseIssue, CreateIssue, SearchJql, ViewBoard, BrowseBoards, \ + BrowseProjects, AddComment, ViewDashboard, EditIssue, ViewProjectSummary, jira_datasets +from locustio.common_utils import jira_measure, fetch_by_re, timestamp_int, generate_random_string, TEXT_HEADERS, \ + ADMIN_HEADERS, NO_TOKEN_HEADERS, init_logger + +from util.conf import JIRA_SETTINGS + +logger = init_logger(app_type='jira') +jira_dataset = jira_datasets() + + +@jira_measure +def login_and_view_dashboard(locust): + params = Login() + + user = random.choice(jira_dataset["users"]) + body = params.login_body + body['os_username'] = user[0] + body['os_password'] = user[1] + + locust.client.post('/login.jsp', body, TEXT_HEADERS, catch_response=True) + r = locust.client.get('/', catch_response=True) + if not r.content: + raise Exception('Please check server hostname in jira.yml file') + content = r.content.decode('utf-8') + locust.client.post('/rest/webResources/1.0/resources', params.resources_body.get("110"), + TEXT_HEADERS, catch_response=True) + locust.client.post("/plugins/servlet/gadgets/dashboard-diagnostics", + {"uri": f"{locust.client.base_url.lower()}/secure/Dashboard.jspa"}, + TEXT_HEADERS, catch_response=True) + locust.client.post('/rest/webResources/1.0/resources', params.resources_body.get("120"), + TEXT_HEADERS, catch_response=True) + locust.client.post('/rest/webResources/1.0/resources', params.resources_body.get("125"), + TEXT_HEADERS, catch_response=True) + locust.client.post('/rest/webResources/1.0/resources', params.resources_body.get("130"), + TEXT_HEADERS, catch_response=True) + + locust.client.get(f'/rest/activity-stream/1.0/preferences?_={timestamp_int()}', catch_response=True) + locust.client.get(f'/rest/gadget/1.0/issueTable/jql?num=10&tableContext=jira.table.cols.dashboard' + f'&addDefault=true&enableSorting=true&paging=true&showActions=true' + f'&jql=assignee+%3D+currentUser()+AND' + f'+resolution+%3D+unresolved+ORDER+BY+priority+DESC%2C+created+ASC' + f'&sortBy=&startIndex=0&_={timestamp_int()}', catch_response=True) + locust.client.get(f'/plugins/servlet/streams?maxResults=5&relativeLinks=true&_={timestamp_int()}', + catch_response=True) + # Assertions + token = fetch_by_re(params.atl_token_pattern, content) + if not (f'title="loggedInUser" value="{user[0]}">' in content): + logger.error(f'User {user[0]} authentication failed: {content}') + assert f'title="loggedInUser" value="{user[0]}">' in content, 'User authentication failed' + locust.user = user[0] + locust.atl_token = token + locust.storage = dict() # Define locust storage dict for getting cross-functional variables access + logger.locust_info(f"{params.action_name}: User {user[0]} logged in with atl_token: {token}") + + +@jira_measure +def view_issue(locust): + params = BrowseIssue() + issue_key = random.choice(jira_dataset['issues'])[0] + project_key = random.choice(jira_dataset['issues'])[2] + + r = locust.client.get(f'/browse/{issue_key}', catch_response=True) + content = r.content.decode('utf-8') + issue_id = fetch_by_re(params.issue_id_pattern, content) + project_avatar_id = fetch_by_re(params.project_avatar_id_pattern, content) + edit_allowed = fetch_by_re(params.edit_allow_pattern, content, group_no=0) + locust.client.get(f'/secure/projectavatar?avatarId={project_avatar_id}', catch_response=True) + # Assertions + if not(f'<meta name="ajs-issue-key" content="{issue_key}">' in content): + logger.error(f'Issue {issue_key} not found: {content}') + assert f'<meta name="ajs-issue-key" content="{issue_key}">' in content, 'Issue not found' + logger.locust_info(f"{params.action_name}: Issue {issue_key} is opened successfully") + + logger.locust_info(f'{params.action_name}: Issue key - {issue_key}, issue_id - {issue_id}') + if edit_allowed: + url = f'/secure/AjaxIssueEditAction!default.jspa?decorator=none&issueId={issue_id}&_={timestamp_int()}' + locust.client.get(url, catch_response=True) + locust.client.put(f'/rest/projects/1.0/project/{project_key}/lastVisited', params.browse_project_payload, + catch_response=True) + + +def create_issue(locust): + params = CreateIssue() + project = random.choice(jira_dataset['projects']) + project_id = project[1] + + @jira_measure + def create_issue_open_quick_create(): + r = locust.client.post('/secure/QuickCreateIssue!default.jspa?decorator=none', + ADMIN_HEADERS, catch_response=True) + content = r.content.decode('utf-8') + atl_token = fetch_by_re(params.atl_token_pattern, content) + form_token = fetch_by_re(params.form_token_pattern, content) + issue_type = fetch_by_re(params.issue_type_pattern, content) + resolution_done = fetch_by_re(params.resolution_done_pattern, content) + fields_to_retain = re.findall(params.fields_to_retain_pattern, content) + custom_fields_to_retain = re.findall(params.custom_fields_to_retain_pattern, content) + + issue_body_params_dict = {'atl_token': atl_token, + 'form_token': form_token, + 'issue_type': issue_type, + 'project_id': project_id, + 'resolution_done': resolution_done, + 'fields_to_retain': fields_to_retain, + 'custom_fields_to_retain': custom_fields_to_retain + } + if not ('"id":"project","label":"Project"' in content): + logger.error(f'{params.err_message_create_issue}: {content}') + assert '"id":"project","label":"Project"' in content, params.err_message_create_issue + locust.client.post('/rest/quickedit/1.0/userpreferences/create', params.user_preferences_payload, + ADMIN_HEADERS, catch_response=True) + locust.storage['issue_body_params_dict'] = issue_body_params_dict + create_issue_open_quick_create() + + @jira_measure + def create_issue_submit_form(): + issue_body = params.prepare_issue_body(locust.storage['issue_body_params_dict'], user=locust.user) + r = locust.client.post('/secure/QuickCreateIssue.jspa?decorator=none', params=issue_body, + headers=ADMIN_HEADERS, catch_response=True) + content = r.content.decode('utf-8') + if not ('"id":"project","label":"Project"') in content: + logger.error(f'{params.err_message_create_issue}: {content}') + assert '"id":"project","label":"Project"' in content, params.err_message_create_issue + issue_key = fetch_by_re(params.create_issue_key_pattern, content) + logger.locust_info(f"{params.action_name}: Issue {issue_key} was successfully created") + create_issue_submit_form() + locust.storage.clear() + + +@jira_measure +def search_jql(locust): + params = SearchJql() + jql = random.choice(jira_dataset['jqls'])[0] + + r = locust.client.get(f'/issues/?jql={jql}', catch_response=True) + content = r.content.decode('utf-8') + if not (locust.atl_token in content): + logger.error(f'Can not search by {jql}: {content}') + assert locust.atl_token in content, 'Can not search by jql' + + locust.client.post('/rest/webResources/1.0/resources', params.resources_body.get("305"), + TEXT_HEADERS, catch_response=True) + + locust.client.get(f'/rest/api/2/filter/favourite?expand=subscriptions[-5:]&_={timestamp_int()}', + catch_response=True) + locust.client.post('/rest/issueNav/latest/preferredSearchLayout', params={'layoutKey': 'split-view'}, + headers=NO_TOKEN_HEADERS, catch_response=True) + + locust.client.post('/rest/webResources/1.0/resources', params.resources_body.get("320"), + TEXT_HEADERS, catch_response=True) + r = locust.client.post('/rest/issueNav/1/issueTable', data=params.issue_table_payload, + headers=NO_TOKEN_HEADERS, catch_response=True) + content = r.content.decode('utf-8') + issue_ids = re.findall(params.ids_pattern, content) + + locust.client.post('/rest/webResources/1.0/resources', params.resources_body.get("330"), + TEXT_HEADERS, catch_response=True) + if issue_ids: + body = params.prepare_jql_body(issue_ids) + r = locust.client.post('/rest/issueNav/1/issueTable/stable', data=body, + headers=NO_TOKEN_HEADERS, catch_response=True) + content = r.content.decode('utf-8') + issue_key = fetch_by_re(params.issue_key_pattern, content) + issue_id = fetch_by_re(params.issue_id_pattern, content) + locust.client.post('/secure/QueryComponent!Jql.jspa', params={'jql': 'order by created DESC', + 'decorator': None}, headers=TEXT_HEADERS, + catch_response=True) + locust.client.post('/rest/orderbycomponent/latest/orderByOptions/primary', + data={"jql": "order by created DESC"}, headers=TEXT_HEADERS, catch_response=True) + if issue_ids: + r = locust.client.post('/secure/AjaxIssueAction!default.jspa', params={"decorator": None, + "issueKey": issue_key, + "prefetch": False, + "shouldUpdateCurrentProject": False, + "loadFields": False, + "_": timestamp_int()}, + headers=TEXT_HEADERS, catch_response=True) + if params.edit_allow_string in r.content.decode('utf-8'): + locust.client.get(f'/secure/AjaxIssueEditAction!default.jspa?' + f'decorator=none&issueId={issue_id}&_={timestamp_int()}', catch_response=True) + + +@jira_measure +def view_project_summary(locust): + params = ViewProjectSummary() + project = random.choice(jira_dataset['projects']) + project_key = project[0] + + r = locust.client.get(f'/projects/{project_key}/summary', catch_response=True) + content = r.content.decode('utf-8') + logger.locust_info(f"{params.action_name}. View project {project_key}: {content}") + + assert_string = f'["project-key"]="\\"{project_key}\\"' + if not (assert_string in content): + logger.error(f'{params.err_message} {project_key}') + assert assert_string in content, params.err_message + + locust.client.post('/rest/webResources/1.0/resources', params.resources_body.get("505"), + TEXT_HEADERS, catch_response=True) + locust.client.post('/rest/webResources/1.0/resources', params.resources_body.get("510"), + TEXT_HEADERS, catch_response=True) + locust.client.get(f'/rest/activity-stream/1.0/preferences?_={timestamp_int()}', catch_response=True) + locust.client.post('/rest/webResources/1.0/resources', params.resources_body.get("520"), + TEXT_HEADERS, catch_response=True) + locust.client.get(f'/plugins/servlet/streams?maxResults=10&relativeLinks=true&streams=key+IS+{project_key}' + f'&providers=thirdparty+dvcs-streams-provider+issues&_={timestamp_int()}', + catch_response=True) + locust.client.post('/rest/webResources/1.0/resources', params.resources_body.get("530"), + TEXT_HEADERS, catch_response=True) + locust.client.get(f'/projects/{project_key}?selectedItem=com.atlassian.jira.jira-projects-plugin:' + f'project-activity-summary&decorator=none&contentOnly=true&_={timestamp_int()}', + catch_response=True) + locust.client.post('/rest/webResources/1.0/resources', params.resources_body.get("545"), + TEXT_HEADERS, catch_response=True) + locust.client.put(f'/rest/api/2/user/properties/lastViewedVignette?username={locust.user}', data={"id": "priority"}, + headers=TEXT_HEADERS, catch_response=True) + locust.client.post('/rest/webResources/1.0/resources', params.resources_body.get("555"), + TEXT_HEADERS, catch_response=True) + locust.client.get(f'/rest/activity-stream/1.0/preferences?_={timestamp_int()}', catch_response=True) + locust.client.get(f'/plugins/servlet/streams?maxResults=10&relativeLinks=true&streams=key+IS+{project_key}' + f'&providers=thirdparty+dvcs-streams-provider+issues&_={timestamp_int()}', + catch_response=True) + + +def edit_issue(locust): + params = EditIssue() + issue = random.choice(jira_dataset['issues']) + issue_id = issue[1] + issue_key = issue[0] + project_key = issue[2] + + @jira_measure + def edit_issue_open_editor(): + r = locust.client.get(f'/secure/EditIssue!default.jspa?id={issue_id}', catch_response=True) + content = r.content.decode('utf-8') + + issue_type = fetch_by_re(params.issue_type_pattern, content) + atl_token = fetch_by_re(params.atl_token_pattern, content) + priority = fetch_by_re(params.issue_priority_pattern, content, group_no=2) + assignee = fetch_by_re(params.issue_assigneee_reporter_pattern, content, group_no=2) + reporter = fetch_by_re(params.issue_reporter_pattern, content) + + if not (f' Edit Issue: [{issue_key}]' in content): + logger.error(f'{params.err_message_issue_not_found} - {issue_id}, {issue_key}: {content}') + assert f' Edit Issue: [{issue_key}]' in content, \ + params.err_message_issue_not_found + logger.locust_info(f"{params.action_name}: Editing issue {issue_key}") + + locust.client.post('/rest/webResources/1.0/resources', params.resources_body.get("705"), + TEXT_HEADERS, catch_response=True) + locust.client.post('/rest/webResources/1.0/resources', params.resources_body.get("710"), + TEXT_HEADERS, catch_response=True) + locust.client.post('/rest/webResources/1.0/resources', params.resources_body.get("720"), + TEXT_HEADERS, catch_response=True) + locust.client.get(f'/rest/internal/2/user/mention/search?issueKey={issue_key}' + f'&projectKey={project_key}&maxResults=10&_={timestamp_int()}', catch_response=True) + + edit_body = f'id={issue_id}&summary={generate_random_string(15)}&issueType={issue_type}&priority={priority}' \ + f'&dueDate=""&assignee={assignee}&reporter={reporter}&environment=""' \ + f'&description={generate_random_string(500)}&timetracking_originalestimate=""' \ + f'&timetracking_remainingestimate=""&isCreateIssue=""&hasWorkStarted=""&dnd-dropzone=""' \ + f'&comment=""&commentLevel=""&atl_token={atl_token}&Update=Update' + locust.storage['edit_issue_body'] = edit_body + locust.storage['atl_token'] = atl_token + edit_issue_open_editor() + + @jira_measure + def edit_issue_save_edit(): + r = locust.client.post(f'/secure/EditIssue.jspa?atl_token={locust.storage["atl_token"]}', + params=locust.storage['edit_issue_body'], + headers=TEXT_HEADERS, catch_response=True) + content = r.content.decode('utf-8') + if not (f'[{issue_key}]' in content): + logger.error(f'Could not save edited page: {content}') + assert f'[{issue_key}]' in content, 'Could not save edited page' + + locust.client.get(f'/browse/{issue_key}', catch_response=True) + locust.client.post('/rest/webResources/1.0/resources', params.resources_body.get("740"), + TEXT_HEADERS, catch_response=True) + locust.client.post('/rest/webResources/1.0/resources', params.resources_body.get("745"), + TEXT_HEADERS, catch_response=True) + locust.client.post('/rest/webResources/1.0/resources', params.resources_body.get("765"), + TEXT_HEADERS, catch_response=True) + locust.client.get(f'/secure/AjaxIssueEditAction!default.jspa?decorator=none&issueId=' + f'{issue_id}&_={timestamp_int()}', catch_response=True) + locust.client.post('/rest/webResources/1.0/resources', params.resources_body.get("775"), + TEXT_HEADERS, catch_response=True) + locust.client.put(f'/rest/projects/1.0/project/{project_key}/lastVisited', params.last_visited_body, + catch_response=True) + edit_issue_save_edit() + locust.storage.clear() + + +@jira_measure +def view_dashboard(locust): + params = ViewDashboard() + + r = locust.client.get('/secure/Dashboard.jspa', catch_response=True) + content = r.content.decode('utf-8') + if not (f'title="loggedInUser" value="{locust.user}">' in content): + logger.error(f'User {locust.user} authentication failed: {content}') + assert f'title="loggedInUser" value="{locust.user}">' in content, 'User authentication failed' + locust.client.post('/rest/webResources/1.0/resources', params.resources_body.get("605"), + TEXT_HEADERS, catch_response=True) + r = locust.client.post('/plugins/servlet/gadgets/dashboard-diagnostics', + params={'uri': f'{JIRA_SETTINGS.server_url.lower()}//secure/Dashboard.jspa'}, + headers=TEXT_HEADERS, catch_response=True) + content = r.content.decode('utf-8') + if not ('Dashboard Diagnostics: OK' in content): + logger.error(f'view_dashboard dashboard-diagnostics failed: {content}') + assert 'Dashboard Diagnostics: OK' in content, 'view_dashboard dashboard-diagnostics failed' + locust.client.post('/rest/webResources/1.0/resources', params.resources_body.get("620"), + TEXT_HEADERS, catch_response=True) + locust.client.get(f'/rest/activity-stream/1.0/preferences?_={timestamp_int()}', catch_response=True) + locust.client.get(f'/rest/gadget/1.0/issueTable/jql?num=10&tableContext=jira.table.cols.dashboard&addDefault=true' + f'&enableSorting=true&paging=true&showActions=true' + f'&jql=assignee+%3D+currentUser()+AND+resolution+%3D+unresolved+ORDER+BY+priority+' + f'DESC%2C+created+ASC&sortBy=&startIndex=0&_=1588507042019', catch_response=True) + locust.client.get(f'/plugins/servlet/streams?maxResults=5&relativeLinks=true&_={timestamp_int()}', + catch_response=True) + + +def add_comment(locust): + params = AddComment() + issue = random.choice(jira_dataset['issues']) + issue_id = issue[1] + issue_key = issue[0] + project_key = issue[2] + + @jira_measure + def add_comment_open_comment(): + r = locust.client.get(f'/secure/AddComment!default.jspa?id={issue_id}', catch_response=True) + content = r.content.decode('utf-8') + token = fetch_by_re(params.atl_token_pattern, content) + form_token = fetch_by_re(params.form_token_pattern, content) + if not (f'Add Comment: {issue_key}' in content): + logger.error(f'Could not open comment in the {issue_key} issue: {content}') + assert f'Add Comment: {issue_key}' in content, 'Could not open comment in the issue' + + locust.client.post('/rest/webResources/1.0/resources', params.resources_body.get("805"), + TEXT_HEADERS, catch_response=True) + locust.client.post('/rest/webResources/1.0/resources', params.resources_body.get("810"), + TEXT_HEADERS, catch_response=True) + locust.client.post('/rest/webResources/1.0/resources', params.resources_body.get("820"), + TEXT_HEADERS, catch_response=True) + locust.client.get(f'/rest/internal/2/user/mention/search?issueKey={issue_key}&projectKey={project_key}' + f'&maxResults=10&_={timestamp_int()}', catch_response=True) + locust.storage['token'] = token + locust.storage['form_token'] = form_token + add_comment_open_comment() + + @jira_measure + def add_comment_save_comment(): + r = locust.client.post(f'/secure/AddComment.jspa?atl_token={locust.storage["token"]}', + params={"id": {issue_id}, "formToken": locust.storage["form_token"], + "dnd-dropzone": None, "comment": generate_random_string(20), + "commentLevel": None, "atl_token": locust.storage["token"], + "Add": "Add"}, headers=TEXT_HEADERS, catch_response=True) + content = r.content.decode('utf-8') + if not (f'<meta name="ajs-issue-key" content="{issue_key}">' in content): + logger.error(f'Could not save comment: {content}') + assert f'<meta name="ajs-issue-key" content="{issue_key}">' in content, 'Could not save comment' + add_comment_save_comment() + locust.storage.clear() + + +@jira_measure +def browse_projects(locust): + params = BrowseProjects() + + page = random.randint(1, jira_dataset['pages']) + r = locust.client.get(f'/secure/BrowseProjects.jspa?selectedCategory=all&selectedProjectType=all&page={page}', + catch_response=True) + content = r.content.decode('utf-8') + if not ('WRM._unparsedData["com.atlassian.jira.project.browse:projects"]="' in content): + logger.error(f'Could not browse projects: {content}') + assert 'WRM._unparsedData["com.atlassian.jira.project.browse:projects"]="' in content, 'Could not browse projects' + locust.client.post('/rest/webResources/1.0/resources', params.resources_body.get("905"), + TEXT_HEADERS, catch_response=True) + locust.client.post('/rest/webResources/1.0/resources', params.resources_body.get("910"), + TEXT_HEADERS, catch_response=True) + locust.client.post('/rest/webResources/1.0/resources', params.resources_body.get("920"), + TEXT_HEADERS, catch_response=True) + + +@jira_measure +def view_kanban_board(locust): + kanban_board_id = random.choice(jira_dataset["kanban_boards"])[0] + view_board(locust, kanban_board_id) + + +@jira_measure +def view_scrum_board(locust): + scrum_board_id = random.choice(jira_dataset["scrum_boards"])[0] + view_board(locust, scrum_board_id) + + +@jira_measure +def view_backlog(locust): + scrum_board_id = random.choice(jira_dataset["scrum_boards"])[0] + view_board(locust, scrum_board_id, view_backlog=True) + + +@jira_measure +def browse_boards(locust): + params = BrowseBoards() + locust.client.get('/secure/ManageRapidViews.jspa', catch_response=True) + locust.client.post('/rest/webResources/1.0/resources', params.resources_body.get("1205"), + TEXT_HEADERS, catch_response=True) + locust.client.post('/rest/webResources/1.0/resources', params.resources_body.get("1210"), + TEXT_HEADERS, catch_response=True) + locust.client.post('/rest/webResources/1.0/resources', params.resources_body.get("1215"), + TEXT_HEADERS, catch_response=True) + locust.client.post('/rest/webResources/1.0/resources', params.resources_body.get("1225"), + TEXT_HEADERS, catch_response=True) + locust.client.get(f'/rest/greenhopper/1.0/rapidviews/viewsData?_{timestamp_int()}', catch_response=True) + + +def view_board(locust, board_id, view_backlog=False): + params = ViewBoard() + if view_backlog: + url = f'/secure/RapidBoard.jspa?rapidView={board_id}&view=planning' + else: + url = f'/secure/RapidBoard.jspa?rapidView={board_id}' + + r = locust.client.get(url, catch_response=True) + content = r.content.decode('utf-8') + project_key = fetch_by_re(params.project_key_pattern, content) + project_id = fetch_by_re(params.project_id_pattern, content) + project_plan = fetch_by_re(params.project_plan_pattern, content, group_no=2) + if project_plan: + project_plan = project_plan.replace('\\', '') + logger.locust_info(f"{params.action_name}: key = {project_key}, id = {project_id}, plan = {project_plan}") + assert f'currentViewConfig\"{{\"id\":{board_id}', 'Could not open board' + + locust.client.post('/rest/webResources/1.0/resources', params.resources_body.get("1000"), + TEXT_HEADERS, catch_response=True) + locust.client.post('/rest/webResources/1.0/resources', params.resources_body.get("1005"), + TEXT_HEADERS, catch_response=True) + locust.client.post('/rest/webResources/1.0/resources', params.resources_body.get("1010"), + TEXT_HEADERS, catch_response=True) + locust.client.post('/rest/webResources/1.0/resources', params.resources_body.get("1015"), + TEXT_HEADERS, catch_response=True) + locust.client.post('/rest/webResources/1.0/resources', params.resources_body.get("1020"), + TEXT_HEADERS, catch_response=True) + + if project_key: + locust.client.get(f'/rest/api/2/project/{project_key}?_={timestamp_int()}', catch_response=True) + locust.client.get(f'/rest/greenhopper/1.0/xboard/toolSections?mode=work&rapidViewId={board_id}' + f'&selectedProjectKey={project_key}&_={timestamp_int()}', catch_response=True) + locust.client.get(f'/rest/greenhopper/1.0/xboard/work/allData.json?rapidViewId={board_id}' + f'&selectedProjectKey={project_key}&_={timestamp_int()}', catch_response=True) + if view_backlog: + locust.client.get(f'/rest/inline-create/1.0/context/bootstrap?query=' + f'project%20%3D%20{project_key}%20ORDER%20BY%20Rank%20ASC&&_={timestamp_int()}', + catch_response=True) + else: + locust.client.get(f'/rest/greenhopper/1.0/xboard/toolSections?mode=work&rapidViewId={board_id}' + f'&_={timestamp_int()}', catch_response=True) + locust.client.get(f'/rest/greenhopper/1.0/xboard/work/allData.json?rapidViewId={board_id}' + f'&selectedProjectKey={project_key}', catch_response=True) + locust.client.post('/rest/webResources/1.0/resources', params.resources_body.get("1025"), + TEXT_HEADERS, catch_response=True) + locust.client.post('/rest/webResources/1.0/resources', params.resources_body.get("1030"), + TEXT_HEADERS, catch_response=True) + if view_backlog: + locust.client.get(f'/rest/greenhopper/1.0/rapidviewconfig/editmodel.json?rapidViewId={board_id}' + f'&_={timestamp_int()}', catch_response=True) + if project_key: + locust.client.put(f'/rest/projects/1.0/project/{project_key}/lastVisited', + {"id": f"com.pyxis.greenhopper.jira:project-sidebar-work-{project_plan}"}, + catch_response=True) diff --git a/app/locustio/jira/locustfile.py b/app/locustio/jira/locustfile.py new file mode 100644 index 000000000..425efc772 --- /dev/null +++ b/app/locustio/jira/locustfile.py @@ -0,0 +1,73 @@ +from locust import HttpLocust, TaskSet, task, between +from locustio.jira.http_actions import login_and_view_dashboard, create_issue, search_jql, view_issue, \ + view_project_summary, view_dashboard, edit_issue, add_comment, browse_boards, view_kanban_board, view_scrum_board, \ + view_backlog, browse_projects +from locustio.common_utils import ActionPercentage +from extension.jira.extension_locust import app_specific_action +from util.conf import JIRA_SETTINGS + +action = ActionPercentage(config_yml=JIRA_SETTINGS) + + +class JiraBehavior(TaskSet): + + def on_start(self): + login_and_view_dashboard(self) + + @task(action.percentage('create_issue')) + def create_issue_action(self): + create_issue(self) + + @task(action.percentage('search_jql')) + def search_jql_action(self): + search_jql(self) + + @task(action.percentage('view_issue')) + def view_issue_action(self): + view_issue(self) + + @task(action.percentage('view_project_summary')) + def view_project_summary_action(self): + view_project_summary(self) + + @task(action.percentage('view_dashboard')) + def view_dashboard_action(self): + view_dashboard(self) + + @task(action.percentage('edit_issue')) + def edit_issue_action(self): + edit_issue(self) + + @task(action.percentage('add_comment')) + def add_comment_action(self): + add_comment(self) + + @task(action.percentage('browse_projects')) + def browse_projects_action(self): + browse_projects(self) + + @task(action.percentage('view_kanban_board')) + def view_kanban_board_action(self): + view_kanban_board(self) + + @task(action.percentage('view_scrum_board')) + def view_scrum_board_action(self): + view_scrum_board(self) + + @task(action.percentage('view_backlog')) + def view_backlog_action(self): + view_backlog(self) + + @task(action.percentage('browse_boards')) + def browse_boards_action(self): + browse_boards(self) + + @task(action.percentage('standalone_extension')) # By default disabled + def custom_action(self): + app_specific_action(self) + + +class JiraUser(HttpLocust): + host = JIRA_SETTINGS.server_url + task_set = JiraBehavior + wait_time = between(0, 0) diff --git a/app/locustio/jira/requests_params.py b/app/locustio/jira/requests_params.py new file mode 100644 index 000000000..670913878 --- /dev/null +++ b/app/locustio/jira/requests_params.py @@ -0,0 +1,168 @@ +from locustio.common_utils import generate_random_string, read_input_file +from util.project_paths import JIRA_DATASET_ISSUES, JIRA_DATASET_JQLS, JIRA_DATASET_KANBAN_BOARDS, \ + JIRA_DATASET_PROJECTS, JIRA_DATASET_SCRUM_BOARDS, JIRA_DATASET_USERS +import json + + +def jira_datasets(): + data_sets = dict() + data_sets["issues"] = read_input_file(JIRA_DATASET_ISSUES) + data_sets["users"] = read_input_file(JIRA_DATASET_USERS) + data_sets["jqls"] = read_input_file(JIRA_DATASET_JQLS) + data_sets["scrum_boards"] = read_input_file(JIRA_DATASET_SCRUM_BOARDS) + data_sets["kanban_boards"] = read_input_file(JIRA_DATASET_KANBAN_BOARDS) + data_sets["projects"] = read_input_file(JIRA_DATASET_PROJECTS) + page_size = 25 + projects_count = len(data_sets['projects']) + data_sets['pages'] = projects_count // page_size if projects_count % page_size == 0 \ + else projects_count // page_size + 1 + return data_sets + + +class BaseResource: + resources_file = 'locustio/jira/resources.json' + action_name = '' + + def __init__(self): + self.resources_json = self.read_json() + self.resources_body = self.action_resources() + + def read_json(self): + with open(self.resources_file) as f: + return json.load(f) + + def action_resources(self): + return self.resources_json[self.action_name] if self.action_name in self.resources_json else dict() + + +class Login(BaseResource): + action_name = 'login_and_view_dashboard' + atl_token_pattern = r'name="atlassian-token" content="(.+?)">' + login_body = { + 'os_username': '', + 'os_password': '', + 'os_destination': '', + 'user_role': '', + 'atl_token': '', + 'login': 'Log in' + } + + +class BrowseIssue(BaseResource): + issue_id_pattern = r'id="key-val" rel="(.+?)">' + project_avatar_id_pattern = r'projectavatar\?avatarId\=(.+?)" ' + edit_allow_pattern = "secure\/EditLabels\!default" # noqa W605 + browse_project_payload = {"id": "com.atlassian.jira.jira-projects-issue-navigator:sidebar-issue-navigator"} + + +class ViewDashboard(BaseResource): + action_name = 'view_dashboard' + + +class CreateIssue(BaseResource): + atl_token_pattern = '"atl_token":"(.+?)"' + form_token_pattern = '"formToken":"(.+?)"' + issue_type_pattern = '\{"label":"Story","value":"([0-9]*)"' # noqa W605 + project_id_pattern = r'class=\\"project-field\\" value=\\"(.+?)\\"' + resolution_done_pattern = r'<option value=\\"([0-9]*)\\">\\n Done\\n' + fields_to_retain_pattern = '"id":"([a-z]*)","label":"[A-Za-z0-9\- ]*","required":(false|true),' # noqa W605 + custom_fields_to_retain_pattern = '"id":"customfield_([0-9]*)","label":"[A-Za-z0-9\- ]*","required":(false|true),' # noqa W605 + user_preferences_payload = {"useQuickForm": False, "fields": ["summary", "description", + "priority", "versions", "components"], + "showWelcomeScreen": True} + create_issue_key_pattern = '"issueKey":"(.+?)"' + err_message_create_issue = 'Issue was not created' + + @staticmethod + def prepare_issue_body(issue_body_dict: dict, user): + description = f"Locust description {generate_random_string(20)}" + summary = f"Locust summary {generate_random_string(10)}" + environment = f'Locust environment {generate_random_string(10)}' + duedate = "" + reporter = user + timetracking_originalestimate = "" + timetracking_remainingestimate = "" + is_create_issue = "true" + has_work_started = "" + project_id = issue_body_dict['project_id'] + atl_token = issue_body_dict['atl_token'] + form_token = issue_body_dict['form_token'] + issue_type = issue_body_dict['issue_type'] + resolution_done = issue_body_dict['resolution_done'] + fields_to_retain = issue_body_dict['fields_to_retain'] + custom_fields_to_retain = issue_body_dict['custom_fields_to_retain'] + + request_body = f"pid={project_id}&issuetype={issue_type}&atl_token={atl_token}&formToken={form_token}" \ + f"&summary={summary}&duedate={duedate}&reporter={reporter}&environment={environment}" \ + f"&description={description}&timetracking_originalestimate={timetracking_originalestimate}" \ + f"&timetracking_remainingestimate={timetracking_remainingestimate}" \ + f"&is_create_issue={is_create_issue}" \ + f"&hasWorkStarted={has_work_started}&resolution={resolution_done}" + fields_to_retain_body = '' + custom_fields_to_retain_body = '' + for field in fields_to_retain: + fields_to_retain_body = fields_to_retain_body + 'fieldsToRetain=' + field[0] + '&' + for custom_field in custom_fields_to_retain: + custom_fields_to_retain_body = custom_fields_to_retain_body + 'fieldsToRetain=customfield_' \ + + custom_field[0] + '&' + custom_fields_to_retain_body = custom_fields_to_retain_body[:-1] # remove last & + request_body = request_body + f"&{fields_to_retain_body}{custom_fields_to_retain_body}" + return request_body + + +class SearchJql(BaseResource): + action_name = 'search_jql' + issue_table_payload = {"startIndex": "0", + "jql": "order by created DESC", + "layoutKey": "split-view", + "filterId": "-4"} + ids_pattern = '"issueIds":\[([0-9\, ]*)\]' # noqa W605 + issue_key_pattern = '\"table\"\:\[\{\"id\"\:(.+?)\,\"key\"\:\"(.+?)\"' # noqa W605 + issue_id_pattern = '\"table\"\:\[\{\"id\"\:(.+?)\,' # noqa W605 + edit_allow_string = 'secure/EditLabels!default' + + @staticmethod + def prepare_jql_body(issue_ids): + request_body = "layoutKey=split-view" + issue_ids = issue_ids[0].split(',') + for issue_id in issue_ids: + request_body = request_body + '&id=' + issue_id + return request_body + + +class ViewProjectSummary(BaseResource): + action_name = 'view_project_summary' + err_message = 'Project not found' + + +class EditIssue(BaseResource): + action_name = 'edit_issue' + issue_type_pattern = 'name="issuetype" type="hidden" value="(.+?)"' + atl_token_pattern = 'atl_token=(.+?)"' + issue_priority_pattern = 'selected="selected" data-icon="(.+?)" value="(.+?)">' + issue_assigneee_reporter_pattern = '<select id="assignee" (.+?)Automatic</option><option value="(.+?)" ' \ + '(.+?)<option selected="selected" value="(.+?)"' + issue_reporter_pattern = 'assignee.*<option selected="selected" value="(.+?)"' + last_visited_body = {"id": "com.atlassian.jira.jira-projects-issue-navigator:sidebar-issue-navigator"} + err_message_issue_not_found = 'Issue not found' + + +class AddComment(BaseResource): + action_name = 'add_comment' + form_token_pattern = 'name="formToken"\s*type="hidden"\s*value="(.+?)"' # noqa W605 + atl_token_pattern = r'name="atlassian-token" content="(.+?)">' + + +class BrowseProjects(BaseResource): + action_name = 'browse_projects' + + +class ViewBoard(BaseResource): + action_name = 'view_kanban_board' + project_key_pattern = '\["project-key"\]=\"\\\\"(.+?)\\\\""' # noqa W605 + project_id_pattern = '\["project-id"\]=\"(.+?)\"' # noqa W605 + project_plan_pattern = 'com.pyxis.greenhopper.jira:project-sidebar-(.+?)-(.+?)"' + + +class BrowseBoards(BaseResource): + action_name = 'browse_boards' diff --git a/app/locustio/jira/resources.json b/app/locustio/jira/resources.json new file mode 100644 index 000000000..abb9118f7 --- /dev/null +++ b/app/locustio/jira/resources.json @@ -0,0 +1,698 @@ +{ + "login_and_view_dashboard": { + "110": { + "r": [], + "c": [ + "browser-metrics-plugin.contrib" + ], + "xc": [ + "_super", + "atl.dashboard", + "jira.global", + "atl.general", + "jira.general" + ], + "xr": [ + "com.atlassian.jira.jira-postsetup-announcements-plugin:post-setup-announcements", + "com.atlassian.gadgets.dashboard:gadgets-adgs", + "com.atlassian.jira.jira-issue-nav-components:adgs", + "com.atlassian.jira.jira-issue-nav-components:detailslayout-adgs", + "com.atlassian.jira.jira-issue-nav-components:simpleissuelist-adgs", + "com.atlassian.jira.jira-issue-nav-plugin:adgs-styles", + "com.atlassian.jira.jira-issue-nav-components:orderby-less-adgs", + "com.atlassian.jira.jira-issue-nav-components:pager-less-adgs", + "com.atlassian.jira.jira-issue-nav-components:issueviewer-adgs", + "com.atlassian.jira.gadgets:introduction-dashboard-item-resource-adgs" + ] + }, + "120": { + "r": [], + "c": [ + "jira.webresources:mentions-feature" + ], + "xc": [ + "_super", + "atl.dashboard", + "jira.global", + "atl.general", + "jira.general", + "browser-metrics-plugin.contrib", + "atl.global", + "jira.dashboard", + "jira.global.look-and-feel" + ], + "xr": [ + "com.atlassian.jira.jira-postsetup-announcements-plugin:post-setup-announcements", + "com.atlassian.gadgets.dashboard:gadgets-adgs", + "com.atlassian.jira.jira-issue-nav-components:adgs", + "com.atlassian.jira.jira-issue-nav-components:detailslayout-adgs", + "com.atlassian.jira.jira-issue-nav-components:simpleissuelist-adgs", + "com.atlassian.jira.jira-issue-nav-plugin:adgs-styles", + "com.atlassian.jira.jira-issue-nav-components:orderby-less-adgs", + "com.atlassian.jira.jira-issue-nav-components:pager-less-adgs", + "com.atlassian.jira.jira-issue-nav-components:issueviewer-adgs", + "com.atlassian.jira.gadgets:introduction-dashboard-item-resource-adgs", + "com.atlassian.jira.jira-tzdetect-plugin:tzdetect-banner-component", + "com.atlassian.jira.jira-tzdetect-plugin:tzdetect-lib", + "com.atlassian.jira.jira-postsetup-announcements-plugin:post-setup-announcements-lib", + "jira.webresources:calendar-en", + "jira.webresources:calendar-localisation-moment", + "jira.webresources:bigpipe-js", + "jira.webresources:bigpipe-init" + ] + }, + "125": { + "r": [ + "com.atlassian.jira.jira-header-plugin:newsletter-signup-tip" + ], + "c": [ + "com.atlassian.jira.plugins.jira-development-integration-plugin:0" + ], + "xc": [ + "_super", + "atl.dashboard", + "jira.global", + "atl.general", + "jira.general", + "browser-metrics-plugin.contrib", + "atl.global", + "jira.dashboard", + "jira.global.look-and-feel" + ], + "xr": [ + "com.atlassian.jira.jira-postsetup-announcements-plugin:post-setup-announcements", + "com.atlassian.gadgets.dashboard:gadgets-adgs", + "com.atlassian.jira.jira-issue-nav-components:adgs", + "com.atlassian.jira.jira-issue-nav-components:detailslayout-adgs", + "com.atlassian.jira.jira-issue-nav-components:simpleissuelist-adgs", + "com.atlassian.jira.jira-issue-nav-plugin:adgs-styles", + "com.atlassian.jira.jira-issue-nav-components:orderby-less-adgs", + "com.atlassian.jira.jira-issue-nav-components:pager-less-adgs", + "com.atlassian.jira.jira-issue-nav-components:issueviewer-adgs", + "com.atlassian.jira.gadgets:introduction-dashboard-item-resource-adgs", + "com.atlassian.jira.jira-tzdetect-plugin:tzdetect-banner-component", + "com.atlassian.jira.jira-tzdetect-plugin:tzdetect-lib", + "com.atlassian.jira.jira-postsetup-announcements-plugin:post-setup-announcements-lib", + "jira.webresources:calendar-en", + "jira.webresources:calendar-localisation-moment", + "jira.webresources:bigpipe-js", + "jira.webresources:bigpipe-init" + ] + }, + "130": { + "r": [], + "c": [ + "browser-metrics-plugin.contrib" + ], + "xc": [ + "jira.webresources:almond", + "jira.webresources:aui-core-amd-shim", + "jira.webresources:jira-metadata", + "jira.webresources:jquery-livestamp", + "com.atlassian.auiplugin:ajs-underscorejs", + "com.atlassian.analytics.analytics-client:js-events", + "com.atlassian.plugins.browser.metrics.browser-metrics-plugin:api", + "com.atlassian.gadgets.publisher:ajs-gadgets", + "com.atlassian.streams:streamsGadgetResources" + ], + "xr": [ + "jira.webresources:icons", + "jira.webresources:list-styles", + "jira.webresources:inline-layer", + "jira.webresources:dropdown", + "com.atlassian.auiplugin:aui-lozenge", + "com.atlassian.auiplugin:aui-tipsy", + "com.atlassian.auiplugin:aui-tooltips", + "com.atlassian.plugins.issue-status-plugin:issue-status-resources", + "jira.webresources:frother-queryable-dropdown-select", + "jira.webresources:frother-singleselect", + "jira.webresources:frother-multiselect", + "jira.webresources:frother-checkbox-multiselect", + "jira.webresources:select-pickers", + "jira.webresources:autocomplete", + "com.atlassian.jira.gadgets:core-gadget-resources" + ] + } + }, + "search_jql": { + "305": { + "r": [ + ], + "c": [ + "browser-metrics-plugin.contrib" + ], + "xc": [ + "_super", + "jira.view.issue", + "jira.navigator.kickass", + "viewissue.standalone", + "jira.navigator.simple", + "jira.navigator.advanced", + "atl.general", + "jira.general", + "jira.global" + ], + "xr": [ + "jira.filter.deletion.warning:styles" + ] + }, + "320": { + "r": [ + "com.atlassian.jira.plugins.jira-editor-plugin:api", + "com.atlassian.jira.plugins.jira-editor-plugin:resources", + "com.atlassian.jira.plugins.jira-editor-plugin:converter" + ], + "c": [ + "jira.webresources:mentions-feature", + "jira.rich.editor" + ], + "xc": [ + "_super", + "jira.view.issue", + "jira.navigator.kickass", + "viewissue.standalone", + "jira.navigator.simple", + "jira.navigator.advanced", + "atl.general", + "jira.general", + "jira.global", + "browser-metrics-plugin.contrib", + "atl.global", + "jira.global.look-and-feel" + ], + "xr": [ + "jira.filter.deletion.warning:styles", + "com.atlassian.jira.jira-tzdetect-plugin:tzdetect-banner-component", + "com.atlassian.jira.jira-tzdetect-plugin:tzdetect-lib", + "jira.webresources:calendar-en", + "jira.webresources:calendar-localisation-moment", + "jira.webresources:bigpipe-js", + "jira.webresources:bigpipe-init" + ] + }, + "330": { + "r": [ + "com.atlassian.jira.jira-header-plugin:newsletter-signup-tip" + ], + "c": [ + ], + "xc": [ + "_super", + "jira.view.issue", + "jira.navigator.kickass", + "viewissue.standalone", + "jira.navigator.simple", + "jira.navigator.advanced", + "atl.general", + "jira.general", + "jira.global", + "browser-metrics-plugin.contrib", + "atl.global", + "jira.global.look-and-feel", + "jira.rich.editor" + ], + "xr": [ + "jira.filter.deletion.warning:styles", + "com.atlassian.jira.jira-tzdetect-plugin:tzdetect-banner-component", + "com.atlassian.jira.jira-tzdetect-plugin:tzdetect-lib", + "jira.webresources:calendar-en", + "jira.webresources:calendar-localisation-moment", + "jira.webresources:bigpipe-js", + "jira.webresources:bigpipe-init", + "com.atlassian.jira.plugins.jira-editor-plugin:tinymce", + "com.atlassian.jira.plugins.jira-editor-plugin:schema-builder", + "com.atlassian.jira.plugins.jira-editor-plugin:schema", + "com.atlassian.jira.plugins.jira-editor-plugin:i18n", + "com.atlassian.jira.plugins.jira-editor-plugin:wrm", + "com.atlassian.jira.plugins.jira-editor-plugin:converter-util", + "com.atlassian.jira.plugins.jira-editor-plugin:context-detector", + "com.atlassian.jira.plugins.jira-editor-plugin:context-manager", + "com.atlassian.jira.plugins.jira-editor-plugin:selection", + "com.atlassian.jira.plugins.jira-editor-plugin:instance", + "com.atlassian.jira.plugins.jira-editor-plugin:api", + "com.atlassian.jira.plugins.jira-editor-plugin:renderer", + "com.atlassian.jira.plugins.jira-editor-plugin:mentions", + "com.atlassian.jira.plugins.jira-editor-plugin:mentions-plugin", + "com.atlassian.jira.plugins.jira-editor-plugin:polyfil-string-ends-with", + "com.atlassian.jira.plugins.jira-editor-plugin:resources", + "com.atlassian.jira.plugins.jira-editor-plugin:converter" + ] + } + }, + "view_project_summary": { + "505": { + "r": [ + ], + "c": [ + "browser-metrics-plugin.contrib" + ], + "xc": [ + "_super", + "com.atlassian.jira.project.summary.page", + "jira.project.sidebar", + "com.atlassian.jira.projects.sidebar.init", + "atl.general", + "jira.general", + "jira.global" + ], + "xr": [ + "jira.webresources:calendar-lib", + "jira.webresources:autocomplete", + "jira.webresources:groupbrowser", + "jira.webresources:group-pickers", + "com.atlassian.auiplugin:aui-labels", + "jira.webresources:global-static-adgs", + "jira.webresources:global-static" + ] + }, + "510": { + "r": [ + ], + "c": [ + "browser-metrics-plugin.contrib" + ], + "xc": [ + "jira.webresources:almond", + "jira.webresources:aui-core-amd-shim", + "jira.webresources:jira-metadata", + "jira.webresources:jquery-livestamp", + "com.atlassian.auiplugin:ajs-underscorejs", + "com.atlassian.analytics.analytics-client:js-events", + "com.atlassian.plugins.browser.metrics.browser-metrics-plugin:api", + "com.atlassian.gadgets.publisher:ajs-gadgets", + "com.atlassian.streams:streamsGadgetResources" + ], + "xr": [ + "jira.webresources:icons", + "jira.webresources:list-styles", + "jira.webresources:inline-layer", + "jira.webresources:dropdown", + "com.atlassian.auiplugin:aui-lozenge", + "com.atlassian.auiplugin:aui-tipsy", + "com.atlassian.auiplugin:aui-tooltips", + "com.atlassian.plugins.issue-status-plugin:issue-status-resources", + "jira.webresources:frother-queryable-dropdown-select", + "jira.webresources:frother-singleselect", + "jira.webresources:frother-multiselect", + "jira.webresources:frother-checkbox-multiselect", + "jira.webresources:select-pickers", + "jira.webresources:autocomplete", + "com.atlassian.jira.gadgets:core-gadget-resources" + ] + }, + "520": { + "r": [ + ], + "c": [ + "jira.webresources:mentions-feature" + ], + "xc": [ + "_super", + "com.atlassian.jira.project.summary.page", + "jira.project.sidebar", + "com.atlassian.jira.projects.sidebar.init", + "atl.general", + "jira.general", + "jira.global", + "browser-metrics-plugin.contrib", + "atl.global", + "jira.global.look-and-feel" + ], + "xr": [ + "jira.webresources:calendar-lib", + "jira.webresources:autocomplete", + "jira.webresources:groupbrowser", + "jira.webresources:group-pickers", + "com.atlassian.auiplugin:aui-labels", + "jira.webresources:global-static-adgs", + "jira.webresources:global-static", + "com.atlassian.jira.jira-tzdetect-plugin:tzdetect-banner-component", + "com.atlassian.jira.jira-tzdetect-plugin:tzdetect-lib", + "jira.webresources:calendar", + "jira.webresources:group-label-lozenge", + "jira.webresources:ie-imitation-placeholder", + "jira.webresources:jira-project-issuetype-fields", + "jira.webresources:jira-fields", + "jira.webresources:calendar-en", + "jira.webresources:calendar-localisation-moment", + "jira.webresources:bigpipe-js", + "jira.webresources:bigpipe-init" + ] + }, + "530": { + "r": [ + ], + "c": [ + "jira.project.sidebar", + "jira.project.sidebar.software" + ], + "xc": [ + "_super", + "com.atlassian.jira.project.summary.page", + "jira.project.sidebar", + "com.atlassian.jira.projects.sidebar.init", + "atl.general", + "jira.general", + "jira.global", + "browser-metrics-plugin.contrib", + "atl.global", + "jira.global.look-and-feel" + ], + "xr": [ + "jira.webresources:calendar-lib", + "jira.webresources:autocomplete", + "jira.webresources:groupbrowser", + "jira.webresources:group-pickers", + "com.atlassian.auiplugin:aui-labels", + "jira.webresources:global-static-adgs", + "jira.webresources:global-static", + "com.atlassian.jira.jira-tzdetect-plugin:tzdetect-banner-component", + "com.atlassian.jira.jira-tzdetect-plugin:tzdetect-lib", + "jira.webresources:calendar", + "jira.webresources:group-label-lozenge", + "jira.webresources:ie-imitation-placeholder", + "jira.webresources:jira-project-issuetype-fields", + "jira.webresources:jira-fields", + "jira.webresources:calendar-en", + "jira.webresources:calendar-localisation-moment", + "jira.webresources:bigpipe-js", + "jira.webresources:bigpipe-init" + ] + }, + "545": { + "r": [ + "com.atlassian.jira.jira-header-plugin:newsletter-signup-tip" + ], + "c": [ + ], + "xc": [ + "_super", + "com.atlassian.jira.project.summary.page", + "jira.project.sidebar", + "com.atlassian.jira.projects.sidebar.init", + "atl.general", + "jira.general", + "jira.global", + "browser-metrics-plugin.contrib", + "atl.global", + "jira.global.look-and-feel" + ], + "xr": [ + "jira.webresources:calendar-lib", + "jira.webresources:autocomplete", + "jira.webresources:groupbrowser", + "jira.webresources:group-pickers", + "com.atlassian.auiplugin:aui-labels", + "jira.webresources:global-static-adgs", + "jira.webresources:global-static", + "com.atlassian.jira.jira-tzdetect-plugin:tzdetect-banner-component", + "com.atlassian.jira.jira-tzdetect-plugin:tzdetect-lib", + "jira.webresources:calendar", + "jira.webresources:group-label-lozenge", + "jira.webresources:ie-imitation-placeholder", + "jira.webresources:jira-project-issuetype-fields", + "jira.webresources:jira-fields", + "jira.webresources:calendar-en", + "jira.webresources:calendar-localisation-moment", + "jira.webresources:bigpipe-js", + "jira.webresources:bigpipe-init" + ] + }, + "555": { + "r": [ + ], + "c": [ + "browser-metrics-plugin.contrib" + ], + "xc": [ + "jira.webresources:almond", + "jira.webresources:aui-core-amd-shim", + "jira.webresources:jira-metadata", + "jira.webresources:jquery-livestamp", + "com.atlassian.auiplugin:ajs-underscorejs", + "com.atlassian.analytics.analytics-client:js-events", + "com.atlassian.plugins.browser.metrics.browser-metrics-plugin:api", + "com.atlassian.gadgets.publisher:ajs-gadgets", + "com.atlassian.streams:streamsGadgetResources" + ], + "xr": [ + "jira.webresources:icons", + "jira.webresources:list-styles", + "jira.webresources:inline-layer", + "jira.webresources:dropdown", + "com.atlassian.auiplugin:aui-lozenge", + "com.atlassian.auiplugin:aui-tipsy", + "com.atlassian.auiplugin:aui-tooltips", + "com.atlassian.plugins.issue-status-plugin:issue-status-resources", + "jira.webresources:frother-queryable-dropdown-select", + "jira.webresources:frother-singleselect", + "jira.webresources:frother-multiselect", + "jira.webresources:frother-checkbox-multiselect", + "jira.webresources:select-pickers", + "jira.webresources:autocomplete", + "com.atlassian.jira.gadgets:core-gadget-resources" + ] + } + }, + "edit_issue": { + "705": { + "r": [ + ], + "c": [ + "browser-metrics-plugin.contrib" + ], + "xc": [ + "_super", + "atl.general", + "jira.edit.issue", + "jira.general", + "jira.global" + ], + "xr": [ + "com.atlassian.auiplugin:aui-labels", + "jira.webresources:global-static-adgs", + "jira.webresources:global-static" + ] + }, + "710": { + "r": [ + ], + "c": [ + "jira.webresources:mentions-feature" + ], + "xc": [ + "_super", + "atl.general", + "jira.edit.issue", + "jira.general", + "jira.global", + "browser-metrics-plugin.contrib", + "atl.global", + "jira.global.look-and-feel" + ], + "xr": [ + "com.atlassian.auiplugin:aui-labels", + "jira.webresources:global-static-adgs", + "jira.webresources:global-static", + "com.atlassian.jira.jira-tzdetect-plugin:tzdetect-banner-component", + "com.atlassian.jira.jira-tzdetect-plugin:tzdetect-lib", + "jira.webresources:calendar-en", + "jira.webresources:calendar-localisation-moment", + "jira.webresources:bigpipe-js", + "jira.webresources:bigpipe-init" + ] + }, + "720": { + "r": [ + "com.atlassian.jira.jira-header-plugin:newsletter-signup-tip", + "com.atlassian.jira.plugins.jira-editor-plugin:api", + "com.atlassian.jira.plugins.jira-editor-plugin:resources", + "com.atlassian.jira.plugins.jira-editor-plugin:converter" + ], + "c": [ + "jira.rich.editor" + ], + "xc": [ + "_super", + "atl.general", + "jira.edit.issue", + "jira.general", + "jira.global", + "browser-metrics-plugin.contrib", + "atl.global", + "jira.global.look-and-feel" + ], + "xr": [ + "com.atlassian.auiplugin:aui-labels", + "jira.webresources:global-static-adgs", + "jira.webresources:global-static", + "com.atlassian.jira.jira-tzdetect-plugin:tzdetect-banner-component", + "com.atlassian.jira.jira-tzdetect-plugin:tzdetect-lib", + "jira.webresources:calendar-en", + "jira.webresources:calendar-localisation-moment", + "jira.webresources:bigpipe-js", + "jira.webresources:bigpipe-init" + ] + }, + "740": { + "r": [ + ], + "c": [ + "browser-metrics-plugin.contrib" + ], + "xc": [ + "_super", + "project.issue.navigator", + "jira.view.issue", + "jira.global", + "atl.general", + "jira.general" + ], + "xr": [ + ] + }, + "745": { + "r": [ + ], + "c": [ + "com.atlassian.jira.plugins.jira-development-integration-plugin:0" + ], + "xc": [ + "_super", + "project.issue.navigator", + "jira.view.issue", + "jira.global", + "atl.general", + "jira.general", + "browser-metrics-plugin.contrib", + "atl.global", + "com.atlassian.jira.projects.sidebar.init", + "jira.global.look-and-feel" + ], + "xr": [ + "com.atlassian.plugins.atlassian-chaperone:hotspot-tour", + "com.atlassian.jira.jira-tzdetect-plugin:tzdetect-banner-component", + "com.atlassian.jira.jira-tzdetect-plugin:tzdetect-lib", + "jira.webresources:calendar-en", + "jira.webresources:calendar-localisation-moment", + "jira.webresources:bigpipe-js", + "jira.webresources:bigpipe-init" + ] + }, + "765": { + "r": [ + "com.atlassian.jira.plugins.jira-editor-plugin:api", + "com.atlassian.jira.plugins.jira-editor-plugin:resources", + "com.atlassian.jira.plugins.jira-editor-plugin:converter", + "com.atlassian.jira.jira-header-plugin:newsletter-signup-tip" + ], + "c": [ + "jira.webresources:mentions-feature", + "jira.rich.editor" + ], + "xc": [ + "_super", + "project.issue.navigator", + "jira.view.issue", + "jira.global", + "atl.general", + "jira.general", + "browser-metrics-plugin.contrib", + "atl.global", + "com.atlassian.jira.projects.sidebar.init", + "jira.global.look-and-feel", + "com.atlassian.jira.plugins.jira-development-integration-plugin:0" + ], + "xr": [ + "com.atlassian.plugins.atlassian-chaperone:hotspot-tour", + "com.atlassian.jira.jira-tzdetect-plugin:tzdetect-banner-component", + "com.atlassian.jira.jira-tzdetect-plugin:tzdetect-lib", + "jira.webresources:calendar-en", + "jira.webresources:calendar-localisation-moment", + "jira.webresources:bigpipe-js", + "jira.webresources:bigpipe-init" + ] + }, + "775": { + "r": [ + ], + "c": [ + "jira.project.sidebar", + "jira.project.sidebar.software" + ], + "xc": [ + "_super", + "project.issue.navigator", + "jira.view.issue", + "jira.global", + "atl.general", + "jira.general", + "browser-metrics-plugin.contrib", + "atl.global", + "com.atlassian.jira.projects.sidebar.init", + "jira.global.look-and-feel", + "com.atlassian.jira.plugins.jira-development-integration-plugin:0", + "jira.rich.editor" + ], + "xr": [ + "com.atlassian.plugins.atlassian-chaperone:hotspot-tour", + "com.atlassian.jira.jira-tzdetect-plugin:tzdetect-banner-component", + "com.atlassian.jira.jira-tzdetect-plugin:tzdetect-lib", + "jira.webresources:calendar-en", + "jira.webresources:calendar-localisation-moment", + "jira.webresources:bigpipe-js", + "jira.webresources:bigpipe-init", + "com.atlassian.jira.jira-header-plugin:newsletter-signup-tip", + "com.atlassian.jira.plugins.jira-editor-plugin:tinymce", + "com.atlassian.jira.plugins.jira-editor-plugin:schema-builder", + "com.atlassian.jira.plugins.jira-editor-plugin:schema", + "com.atlassian.jira.plugins.jira-editor-plugin:i18n", + "com.atlassian.jira.plugins.jira-editor-plugin:wrm", + "com.atlassian.jira.plugins.jira-editor-plugin:converter-util", + "com.atlassian.jira.plugins.jira-editor-plugin:context-detector", + "com.atlassian.jira.plugins.jira-editor-plugin:context-manager", + "com.atlassian.jira.plugins.jira-editor-plugin:selection", + "com.atlassian.jira.plugins.jira-editor-plugin:instance", + "com.atlassian.jira.plugins.jira-editor-plugin:api", + "com.atlassian.jira.plugins.jira-editor-plugin:renderer", + "com.atlassian.jira.plugins.jira-editor-plugin:mentions", + "com.atlassian.jira.plugins.jira-editor-plugin:mentions-plugin", + "com.atlassian.jira.plugins.jira-editor-plugin:polyfil-string-ends-with", + "com.atlassian.jira.plugins.jira-editor-plugin:resources", + "com.atlassian.jira.plugins.jira-editor-plugin:converter", + "com.atlassian.plugin.jslibs:underscore-1.8.3" + ] + } + }, + "view_dashboard": { + "605": {"r":[],"c":["browser-metrics-plugin.contrib"],"xc":["_super","atl.dashboard","jira.global","atl.general","jira.general"],"xr":[]}, + "620": {"r":["com.atlassian.jira.jira-header-plugin:newsletter-signup-tip"],"c":["jira.webresources:mentions-feature","com.atlassian.jira.plugins.jira-development-integration-plugin:0"],"xc":["_super","atl.dashboard","jira.global","atl.general","jira.general","browser-metrics-plugin.contrib","atl.global","jira.dashboard","jira.global.look-and-feel"],"xr":["com.atlassian.jira.jira-tzdetect-plugin:tzdetect-banner-component","com.atlassian.jira.jira-tzdetect-plugin:tzdetect-lib","jira.webresources:calendar-en","jira.webresources:calendar-localisation-moment","jira.webresources:bigpipe-js","jira.webresources:bigpipe-init"]} + + }, + "add_comment": { + "805": {"r":[],"c":["browser-metrics-plugin.contrib"],"xc":["_super","atl.general","jira.edit.issue","jira.general","jira.global"],"xr":["com.atlassian.auiplugin:aui-labels","jira.webresources:global-static-adgs","jira.webresources:global-static"]}, + "810": {"r":[],"c":["jira.webresources:mentions-feature"],"xc":["_super","atl.general","jira.edit.issue","jira.general","jira.global","browser-metrics-plugin.contrib","atl.global","jira.global.look-and-feel"],"xr":["com.atlassian.auiplugin:aui-labels","jira.webresources:global-static-adgs","jira.webresources:global-static","com.atlassian.jira.jira-tzdetect-plugin:tzdetect-banner-component","com.atlassian.jira.jira-tzdetect-plugin:tzdetect-lib","jira.webresources:bigpipe-js","jira.webresources:bigpipe-init"]}, + "820": {"r":["com.atlassian.jira.jira-header-plugin:newsletter-signup-tip","com.atlassian.jira.plugins.jira-editor-plugin:api","com.atlassian.jira.plugins.jira-editor-plugin:resources","com.atlassian.jira.plugins.jira-editor-plugin:converter"],"c":["jira.rich.editor"],"xc":["_super","atl.general","jira.edit.issue","jira.general","jira.global","browser-metrics-plugin.contrib","atl.global","jira.global.look-and-feel"],"xr":["com.atlassian.auiplugin:aui-labels","jira.webresources:global-static-adgs","jira.webresources:global-static","com.atlassian.jira.jira-tzdetect-plugin:tzdetect-banner-component","com.atlassian.jira.jira-tzdetect-plugin:tzdetect-lib","jira.webresources:bigpipe-js","jira.webresources:bigpipe-init"]} + }, + "browse_projects": { + "905": {"r":[],"c":["browser-metrics-plugin.contrib"],"xc":["_super","atl.general","jira.general","jira.global"],"xr":["jira.webresources:calendar-lib","jira.webresources:autocomplete","jira.webresources:groupbrowser","jira.webresources:group-pickers","com.atlassian.auiplugin:aui-labels","jira.webresources:global-static-adgs","jira.webresources:global-static"]}, + "910": {"r":[],"c":["jira.webresources:mentions-feature"],"xc":["_super","atl.general","jira.general","jira.global","browser-metrics-plugin.contrib","atl.global","jira.global.look-and-feel"],"xr":["jira.webresources:calendar-lib","jira.webresources:autocomplete","jira.webresources:groupbrowser","jira.webresources:group-pickers","com.atlassian.auiplugin:aui-labels","jira.webresources:global-static-adgs","jira.webresources:global-static","com.atlassian.jira.jira-tzdetect-plugin:tzdetect-banner-component","com.atlassian.jira.jira-tzdetect-plugin:tzdetect-lib","com.atlassian.plugin.jslibs:backbone.paginator-2.0.2-factory","jira.webresources:backbone-paginator","jira.webresources:backbone-queryparams","jira.webresources:project-type-keys","com.atlassian.plugin.jslibs:marionette-1.6.1-factory","jira.webresources:marionette","jira.webresources:navigation-utils","jira.webresources:pagination-view","jira.webresources:browseprojects","jira.webresources:calendar","jira.webresources:group-label-lozenge","jira.webresources:ie-imitation-placeholder","jira.webresources:jira-project-issuetype-fields","jira.webresources:jira-fields","jira.webresources:calendar-en","jira.webresources:calendar-localisation-moment","jira.webresources:bigpipe-js","jira.webresources:bigpipe-init"]}, + "920": {"r":["com.atlassian.jira.jira-header-plugin:newsletter-signup-tip"],"c":[],"xc":["_super","atl.general","jira.general","jira.global","browser-metrics-plugin.contrib","atl.global","jira.global.look-and-feel"],"xr":["jira.webresources:calendar-lib","jira.webresources:autocomplete","jira.webresources:groupbrowser","jira.webresources:group-pickers","com.atlassian.auiplugin:aui-labels","jira.webresources:global-static-adgs","jira.webresources:global-static","com.atlassian.jira.jira-tzdetect-plugin:tzdetect-banner-component","com.atlassian.jira.jira-tzdetect-plugin:tzdetect-lib","com.atlassian.plugin.jslibs:backbone.paginator-2.0.2-factory","jira.webresources:backbone-paginator","jira.webresources:backbone-queryparams","jira.webresources:project-type-keys","com.atlassian.plugin.jslibs:marionette-1.6.1-factory","jira.webresources:marionette","jira.webresources:navigation-utils","jira.webresources:pagination-view","jira.webresources:browseprojects","jira.webresources:calendar","jira.webresources:group-label-lozenge","jira.webresources:ie-imitation-placeholder","jira.webresources:jira-project-issuetype-fields","jira.webresources:jira-fields","jira.webresources:calendar-en","jira.webresources:calendar-localisation-moment","jira.webresources:bigpipe-js","jira.webresources:bigpipe-init"]} + }, + "view_kanban_board": { + "1000": {"r":[],"c":["browser-metrics-plugin.contrib"],"xc":["_super","gh-rapid-exception","greenhopper-rapid-non-gadget","atl.general","gh-rapid","jira.project.sidebar","com.atlassian.jira.projects.sidebar.init","jira.global","jira.general","gh-rapid-charts"],"xr":[]}, + "1005": {"r":[],"c":["com.pyxis.greenhopper.jira:gh-rapid-inline-editable"],"xc":["_super","gh-rapid-exception","greenhopper-rapid-non-gadget","atl.general","gh-rapid","jira.project.sidebar","com.atlassian.jira.projects.sidebar.init","jira.global","jira.general","gh-rapid-charts","browser-metrics-plugin.contrib","atl.global","jira.global.look-and-feel"],"xr":["com.atlassian.jira.jira-tzdetect-plugin:tzdetect-banner-component","com.atlassian.jira.jira-tzdetect-plugin:tzdetect-lib","jira.webresources:calendar-en","jira.webresources:calendar-localisation-moment"]}, + "1010": {"r":[],"c":["com.atlassian.jira.plugins.jira-development-integration-plugin:0"],"xc":["_super","gh-rapid-exception","greenhopper-rapid-non-gadget","atl.general","gh-rapid","jira.project.sidebar","com.atlassian.jira.projects.sidebar.init","jira.global","jira.general","gh-rapid-charts","browser-metrics-plugin.contrib","atl.global","jira.global.look-and-feel","com.pyxis.greenhopper.jira:gh-rapid-inline-editable"],"xr":["com.atlassian.jira.jira-tzdetect-plugin:tzdetect-banner-component","com.atlassian.jira.jira-tzdetect-plugin:tzdetect-lib","jira.webresources:calendar-en","jira.webresources:calendar-localisation-moment","jira.webresources:bigpipe-js","jira.webresources:bigpipe-init"]}, + "1015": {"r":[],"c":["jira.webresources:mentions-feature"],"xc":["_super","gh-rapid-exception","greenhopper-rapid-non-gadget","atl.general","gh-rapid","jira.project.sidebar","com.atlassian.jira.projects.sidebar.init","jira.global","jira.general","gh-rapid-charts","browser-metrics-plugin.contrib","atl.global","jira.global.look-and-feel","com.pyxis.greenhopper.jira:gh-rapid-inline-editable","com.atlassian.jira.plugins.jira-development-integration-plugin:0"],"xr":["com.atlassian.jira.jira-tzdetect-plugin:tzdetect-banner-component","com.atlassian.jira.jira-tzdetect-plugin:tzdetect-lib","jira.webresources:calendar-en","jira.webresources:calendar-localisation-moment","jira.webresources:bigpipe-js","jira.webresources:bigpipe-init"]}, + "1020": {"r":["com.atlassian.jira.jira-header-plugin:newsletter-signup-tip"],"c":[],"xc":["_super","gh-rapid-exception","greenhopper-rapid-non-gadget","atl.general","gh-rapid","jira.project.sidebar","com.atlassian.jira.projects.sidebar.init","jira.global","jira.general","gh-rapid-charts","browser-metrics-plugin.contrib","atl.global","jira.global.look-and-feel","com.pyxis.greenhopper.jira:gh-rapid-inline-editable","com.atlassian.jira.plugins.jira-development-integration-plugin:0"],"xr":["com.atlassian.jira.jira-tzdetect-plugin:tzdetect-banner-component","com.atlassian.jira.jira-tzdetect-plugin:tzdetect-lib","jira.webresources:calendar-en","jira.webresources:calendar-localisation-moment","jira.webresources:bigpipe-js","jira.webresources:bigpipe-init"]}, + "1025": {"r":["com.atlassian.jira.plugins.jira-editor-plugin:api","com.atlassian.jira.plugins.jira-editor-plugin:resources","com.atlassian.jira.plugins.jira-editor-plugin:converter"],"c":["jira.rich.editor"],"xc":["_super","gh-rapid-exception","greenhopper-rapid-non-gadget","atl.general","gh-rapid","jira.project.sidebar","com.atlassian.jira.projects.sidebar.init","jira.global","jira.general","gh-rapid-charts","browser-metrics-plugin.contrib","atl.global","jira.global.look-and-feel","com.pyxis.greenhopper.jira:gh-rapid-inline-editable","com.atlassian.jira.plugins.jira-development-integration-plugin:0"],"xr":["com.atlassian.jira.jira-tzdetect-plugin:tzdetect-banner-component","com.atlassian.jira.jira-tzdetect-plugin:tzdetect-lib","jira.webresources:calendar-en","jira.webresources:calendar-localisation-moment","jira.webresources:bigpipe-js","jira.webresources:bigpipe-init","com.atlassian.jira.jira-header-plugin:newsletter-signup-tip","com.atlassian.plugin.jslibs:underscore-1.8.3"]}, + "1030": {"r":[],"c":["jira.project.sidebar","jira.project.sidebar.software"],"xc":["_super","gh-rapid-exception","greenhopper-rapid-non-gadget","atl.general","gh-rapid","jira.project.sidebar","com.atlassian.jira.projects.sidebar.init","jira.global","jira.general","gh-rapid-charts","browser-metrics-plugin.contrib","atl.global","jira.global.look-and-feel","com.pyxis.greenhopper.jira:gh-rapid-inline-editable","com.atlassian.jira.plugins.jira-development-integration-plugin:0","jira.rich.editor"],"xr":["com.atlassian.jira.jira-tzdetect-plugin:tzdetect-banner-component","com.atlassian.jira.jira-tzdetect-plugin:tzdetect-lib","jira.webresources:calendar-en","jira.webresources:calendar-localisation-moment","jira.webresources:bigpipe-js","jira.webresources:bigpipe-init","com.atlassian.jira.jira-header-plugin:newsletter-signup-tip","com.atlassian.plugin.jslibs:underscore-1.8.3","com.atlassian.jira.plugins.jira-editor-plugin:tinymce","com.atlassian.jira.plugins.jira-editor-plugin:schema-builder","com.atlassian.jira.plugins.jira-editor-plugin:schema","com.atlassian.jira.plugins.jira-editor-plugin:i18n","com.atlassian.jira.plugins.jira-editor-plugin:wrm","com.atlassian.jira.plugins.jira-editor-plugin:converter-util","com.atlassian.jira.plugins.jira-editor-plugin:context-detector","com.atlassian.jira.plugins.jira-editor-plugin:context-manager","com.atlassian.jira.plugins.jira-editor-plugin:selection","com.atlassian.jira.plugins.jira-editor-plugin:instance","com.atlassian.jira.plugins.jira-editor-plugin:api","com.atlassian.jira.plugins.jira-editor-plugin:renderer","com.atlassian.jira.plugins.jira-editor-plugin:mentions","com.atlassian.jira.plugins.jira-editor-plugin:mentions-plugin","com.atlassian.jira.plugins.jira-editor-plugin:polyfil-string-ends-with","com.atlassian.jira.plugins.jira-editor-plugin:resources","com.atlassian.jira.plugins.jira-editor-plugin:converter"]} + }, + "browse_boards": { + "1205": {"r":[],"c":["com.pyxis.greenhopper.jira:gh-rapid-inline-editable"],"xc":["_super","gh-rapid-exception","gh-manage-boards","jira.global","atl.general","greenhopper-rapid-non-gadget","jira.general"],"xr":["com.atlassian.auiplugin:aui-labels","jira.webresources:global-static-adgs","jira.webresources:global-static"]}, + "1210": {"r":[],"c":["browser-metrics-plugin.contrib"],"xc":["_super","gh-rapid-exception","gh-manage-boards","jira.global","atl.general","greenhopper-rapid-non-gadget","jira.general","com.pyxis.greenhopper.jira:gh-rapid-inline-editable","atl.global","jira.global.look-and-feel"],"xr":["com.atlassian.auiplugin:aui-labels","jira.webresources:global-static-adgs","jira.webresources:global-static","com.atlassian.jira.jira-tzdetect-plugin:tzdetect-banner-component","com.atlassian.jira.jira-tzdetect-plugin:tzdetect-lib","jira.webresources:calendar-en","jira.webresources:calendar-localisation-moment"]}, + "1215": {"r":[],"c":["jira.webresources:mentions-feature"],"xc":["_super","gh-rapid-exception","gh-manage-boards","jira.global","atl.general","greenhopper-rapid-non-gadget","jira.general","com.pyxis.greenhopper.jira:gh-rapid-inline-editable","atl.global","jira.global.look-and-feel","browser-metrics-plugin.contrib"],"xr":["com.atlassian.auiplugin:aui-labels","jira.webresources:global-static-adgs","jira.webresources:global-static","com.atlassian.jira.jira-tzdetect-plugin:tzdetect-banner-component","com.atlassian.jira.jira-tzdetect-plugin:tzdetect-lib","jira.webresources:calendar-en","jira.webresources:calendar-localisation-moment","jira.webresources:bigpipe-js","jira.webresources:bigpipe-init"]}, + "1225": {"r":["com.atlassian.jira.jira-header-plugin:newsletter-signup-tip"],"c":[],"xc":["_super","gh-rapid-exception","gh-manage-boards","jira.global","atl.general","greenhopper-rapid-non-gadget","jira.general","com.pyxis.greenhopper.jira:gh-rapid-inline-editable","atl.global","jira.global.look-and-feel","browser-metrics-plugin.contrib"],"xr":["com.atlassian.auiplugin:aui-labels","jira.webresources:global-static-adgs","jira.webresources:global-static","com.atlassian.jira.jira-tzdetect-plugin:tzdetect-banner-component","com.atlassian.jira.jira-tzdetect-plugin:tzdetect-lib","jira.webresources:calendar-en","jira.webresources:calendar-localisation-moment","jira.webresources:bigpipe-js","jira.webresources:bigpipe-init"]} + } + +} \ No newline at end of file diff --git a/app/reports_generation/scripts/csv_aggregator.py b/app/reports_generation/scripts/csv_aggregator.py index 8d2ca8e1a..ff3acadc0 100644 --- a/app/reports_generation/scripts/csv_aggregator.py +++ b/app/reports_generation/scripts/csv_aggregator.py @@ -7,6 +7,13 @@ RESULTS_CSV_FILE_NAME = "results.csv" +class ResultsCSV: + + def __init__(self, absolute_file_path, actions: dict): + self.absolute_file_path = absolute_file_path + self.actions = actions + + def __validate_config(config: dict): validate_str_is_not_blank(config, 'column_name') validate_str_is_not_blank(config, 'profile') @@ -30,48 +37,35 @@ def __create_header(config) -> List[str]: return header -def __validate_count_of_actions(key_files: dict): - # TODO consider doing validation and reading file avoiding opening the same file two times (see __get_data_to_write) - counter = 0 - counter_dict = {} - for run in key_files['runs']: - filename = resolve_path(run['fullPath']) / RESULTS_CSV_FILE_NAME - with filename.open(mode='r') as f: - records = csv.DictReader(f) - row_count = sum(1 for _ in records) - counter_dict[filename] = row_count - if counter == 0: - counter = row_count - if row_count != counter: - for filename, actions in counter_dict.items(): - print(f'Result file {filename} has {actions} actions\n') - raise SystemExit('Incorrect number of actions. ' - 'The number of actions should be the same for each results.csv.') - - -def __get_data_to_write(config: dict) -> List[dict]: - __validate_count_of_actions(config) - - column_value_by_label_list = [] +def __validate_count_of_actions(tests_results: List[dict]): + if any(len(tests_results[0].actions) != len(actions_count.actions) for actions_count in tests_results): + for file in tests_results: + print(f'Result file {file.absolute_file_path} has {len(file.actions)} actions\n') + raise SystemExit('Incorrect number of actions. ' + 'The number of actions should be the same for each results.csv.') + + +def __get_tests_results(config: dict) -> List[dict]: + results_files_list = [] column_name = config['column_name'] for run in config['runs']: - column_value_by_label = {} - filename = resolve_path(run['fullPath']) / RESULTS_CSV_FILE_NAME - with filename.open(mode='r') as fs: + value_by_action = {} + absolute_file_path = resolve_path(run['fullPath']) / RESULTS_CSV_FILE_NAME + with absolute_file_path.open(mode='r') as fs: for row in csv.DictReader(fs): - column_value_by_label[row['Label']] = row[column_name] - column_value_by_label_list.append(column_value_by_label) + value_by_action[row['Label']] = row[column_name] + results_files_list.append(ResultsCSV(absolute_file_path=absolute_file_path, actions=value_by_action)) - return column_value_by_label_list + return results_files_list -def __write_list_to_csv(header: List[str], rows: List[dict], output_filename: Path): - first_file_labels = rows[0].keys() +def __write_list_to_csv(header: List[str], tests_results: List[dict], output_filename: Path): + actions = [action for action in tests_results[0].actions] with output_filename.open(mode='w', newline='') as file_stream: writer = csv.writer(file_stream) writer.writerow(header) - for label in first_file_labels: - row = [label] + [column_value_by_label[label] for column_value_by_label in rows] + for action in actions: + row = [action] + [value_by_action.actions[action] for value_by_action in tests_results] writer.writerow(row) @@ -81,10 +75,11 @@ def __get_output_file_path(config, results_dir) -> Path: def aggregate(config: dict, results_dir: Path) -> Path: __validate_config(config) + tests_results = __get_tests_results(config) + __validate_count_of_actions(tests_results) output_file_path = __get_output_file_path(config, results_dir) header = __create_header(config) - data = __get_data_to_write(config) - __write_list_to_csv(header, data, output_file_path) + __write_list_to_csv(header, tests_results, output_file_path) validate_file_exists(output_file_path, f"Result file {output_file_path} is not created") print(f'Results file {output_file_path.absolute()} is created') diff --git a/app/reports_generation/scripts/utils.py b/app/reports_generation/scripts/utils.py index b160055dd..a9da16fc7 100644 --- a/app/reports_generation/scripts/utils.py +++ b/app/reports_generation/scripts/utils.py @@ -1,5 +1,4 @@ import numbers -import sys from pathlib import Path @@ -26,6 +25,3 @@ def validate_is_number(config: dict, key: str): def validate_file_exists(file: Path, msg: str): if not file.exists(): raise SystemExit(msg) - - - diff --git a/app/selenium_ui/base_page.py b/app/selenium_ui/base_page.py index d0176bc63..6b1946e03 100644 --- a/app/selenium_ui/base_page.py +++ b/app/selenium_ui/base_page.py @@ -1,8 +1,8 @@ from selenium.webdriver.support.wait import WebDriverWait from selenium.webdriver.support import expected_conditions as ec from selenium.webdriver.common.action_chains import ActionChains +from selenium.common.exceptions import WebDriverException from selenium.webdriver.support.ui import Select -from selenium_ui.conftest import AnyEc import random import string @@ -19,12 +19,12 @@ def __init__(self, driver): def go_to(self): self.driver.get(self.page_url) - def wait_for_page_loaded(self, interaction): + def wait_for_page_loaded(self): if type(self.page_loaded_selector) == list: for selector in self.page_loaded_selector: - self.wait_until_visible(selector, interaction) + self.wait_until_visible(selector) else: - self.wait_until_visible(self.page_loaded_selector, interaction) + self.wait_until_visible(self.page_loaded_selector) def go_to_url(self, url): self.driver.get(url) @@ -39,45 +39,39 @@ def get_elements(self, selector): by, locator = selector_name[0], selector_name[1] return self.driver.find_elements(by, locator) - def wait_until_invisible(self, selector_name, interaction=None): + def wait_until_invisible(self, selector_name): selector = self.get_selector(selector_name) - return self.__wait_until(expected_condition=ec.invisibility_of_element_located(selector), - interaction=interaction) + return self.__wait_until(expected_condition=ec.invisibility_of_element_located(selector)) - def wait_until_visible(self, selector_name, interaction=None): + def wait_until_visible(self, selector_name): selector = self.get_selector(selector_name) - return self.__wait_until(expected_condition=ec.visibility_of_element_located(selector), - interaction=interaction) + return self.__wait_until(expected_condition=ec.visibility_of_element_located(selector)) - def wait_until_available_to_switch(self, selector_name, interaction=None): + def wait_until_available_to_switch(self, selector_name): selector = self.get_selector(selector_name) - return self.__wait_until(expected_condition=ec.frame_to_be_available_and_switch_to_it(selector), - interaction=interaction) + return self.__wait_until(expected_condition=ec.frame_to_be_available_and_switch_to_it(selector)) - def wait_until_present(self, selector_name, interaction=None, time_out=TIMEOUT): + def wait_until_present(self, selector_name, time_out=TIMEOUT): selector = self.get_selector(selector_name) - return self.__wait_until(expected_condition=ec.presence_of_element_located(selector), - interaction=interaction, time_out=time_out) + return self.__wait_until(expected_condition=ec.presence_of_element_located(selector), time_out=time_out) - def wait_until_clickable(self, selector_name, interaction=None): + def wait_until_clickable(self, selector_name): selector = self.get_selector(selector_name) - return self.__wait_until(expected_condition=ec.element_to_be_clickable(selector), - interaction=interaction) + return self.__wait_until(expected_condition=ec.element_to_be_clickable(selector)) - def wait_until_any_element_visible(self, selector_name, interaction=None): + def wait_until_any_element_visible(self, selector_name): selector = self.get_selector(selector_name) - return self.__wait_until(expected_condition=ec.visibility_of_any_elements_located(selector), - interaction=interaction) + return self.__wait_until(expected_condition=ec.visibility_of_any_elements_located(selector)) - def wait_until_any_ec_presented(self, selector_names, interaction): + def wait_until_any_ec_presented(self, selector_names): origin_selectors = [] for selector in selector_names: origin_selectors.append(self.get_selector(selector)) any_ec = AnyEc() any_ec.ecs = tuple(ec.presence_of_element_located(origin_selector) for origin_selector in origin_selectors) - return self.__wait_until(expected_condition=any_ec, interaction=interaction) + return self.__wait_until(expected_condition=any_ec) - def wait_until_any_ec_text_presented_in_el(self, selector_names, interaction): + def wait_until_any_ec_text_presented_in_el(self, selector_names): origin_selectors = [] for selector_text in selector_names: selector = self.get_selector(selector_text[0]) @@ -86,10 +80,10 @@ def wait_until_any_ec_text_presented_in_el(self, selector_names, interaction): any_ec = AnyEc() any_ec.ecs = tuple(ec.text_to_be_present_in_element(locator=origin_selector[0], text_=origin_selector[1]) for origin_selector in origin_selectors) - return self.__wait_until(expected_condition=any_ec, interaction=interaction) + return self.__wait_until(expected_condition=any_ec) - def __wait_until(self, expected_condition, interaction, time_out=TIMEOUT): - message = f"Interaction: {interaction}. " + def __wait_until(self, expected_condition, time_out=TIMEOUT): + message = f"Error in wait_until: " ec_type = type(expected_condition) if ec_type == AnyEc: conditions_text = "" @@ -116,7 +110,7 @@ def dismiss_popup(self, *args): for elem in args: try: self.driver.execute_script(f"document.querySelector(\'{elem}\').click()") - except: + except(WebDriverException, Exception): pass def return_to_parent_frame(self): @@ -143,4 +137,21 @@ def select(self, element): return Select(element) def action_chains(self): - return ActionChains(self.driver) \ No newline at end of file + return ActionChains(self.driver) + + +class AnyEc: + """ Use with WebDriverWait to combine expected_conditions + in an OR. + """ + + def __init__(self, *args): + self.ecs = args + + def __call__(self, w_driver): + for fn in self.ecs: + try: + if fn(w_driver): + return True + except(WebDriverException, Exception): + pass diff --git a/app/selenium_ui/bitbucket/modules.py b/app/selenium_ui/bitbucket/modules.py index 1fda10f23..336854a1b 100644 --- a/app/selenium_ui/bitbucket/modules.py +++ b/app/selenium_ui/bitbucket/modules.py @@ -20,50 +20,55 @@ def setup_run_data(datasets): def login(webdriver, datasets): setup_run_data(datasets) login_page = LoginPage(webdriver) - @print_timing - def measure(webdriver, interaction): - @print_timing - def measure(webdriver, interaction): + + @print_timing("selenium_login") + def measure(): + + @print_timing("selenium_login:open_login_page") + def sub_measure(): login_page.go_to() webdriver.app_version = login_page.get_app_version() - measure(webdriver, "selenium_login:open_login_page") + sub_measure() login_page.set_credentials(datasets['username'], datasets['password']) - @print_timing - def measure(webdriver, interaction): - login_page.submit_login(interaction) + @print_timing("selenium_login:login_get_started") + def sub_measure(): + login_page.submit_login() get_started_page = GetStarted(webdriver) - get_started_page.wait_for_page_loaded(interaction) - measure(webdriver, "selenium_login:login_get_started") - measure(webdriver, "selenium_login") + get_started_page.wait_for_page_loaded() + sub_measure() + measure() def view_dashboard(webdriver, datasets): - @print_timing - def measure(webdriver, interaction): + + @print_timing("selenium_view_dashboard") + def measure(): dashboard_page = Dashboard(webdriver) dashboard_page.go_to() - dashboard_page.wait_for_page_loaded(interaction) - measure(webdriver, "selenium_view_dashboard") + dashboard_page.wait_for_page_loaded() + measure() def view_projects(webdriver, datasets): - @print_timing - def measure(webdriver, interaction): + + @print_timing("selenium_view_projects") + def measure(): projects_page = Projects(webdriver) projects_page.go_to() - projects_page.wait_for_page_loaded(interaction) - measure(webdriver, "selenium_view_projects") + projects_page.wait_for_page_loaded() + measure() def view_project_repos(webdriver, datasets): - @print_timing - def measure(webdriver, interaction): + + @print_timing("selenium_view_project_repositories") + def measure(): project_page = Project(webdriver, project_key=datasets['project_key']) project_page.go_to() - project_page.wait_for_page_loaded(interaction) - measure(webdriver, "selenium_view_project_repositories") + project_page.wait_for_page_loaded() + measure() def view_repo(webdriver, datasets): @@ -71,13 +76,13 @@ def view_repo(webdriver, datasets): project_key=datasets['project_key'], repo_slug=datasets['repo_slug']) - @print_timing - def measure(webdriver, interaction): + @print_timing("selenium_view_repository") + def measure(): repository_page.go_to() nav_panel = RepoNavigationPanel(webdriver) - nav_panel.wait_for_page_loaded(interaction) + nav_panel.wait_for_page_loaded() PopupManager(webdriver).dismiss_default_popup() - measure(webdriver, "selenium_view_repository") + measure() def view_list_pull_requests(webdriver, datasets): @@ -85,11 +90,11 @@ def view_list_pull_requests(webdriver, datasets): project_key=datasets['project_key'], repo_slug=datasets['repo_slug']) - @print_timing - def measure(webdriver, interaction): + @print_timing("selenium_view_list_pull_requests") + def measure(): repo_pull_requests_page.go_to() - repo_pull_requests_page.wait_for_page_loaded(interaction) - measure(webdriver, 'selenium_view_list_pull_requests') + repo_pull_requests_page.wait_for_page_loaded() + measure() def view_pull_request_overview_tab(webdriver, datasets): @@ -97,12 +102,12 @@ def view_pull_request_overview_tab(webdriver, datasets): repo_slug=datasets['repo_slug'], pull_request_key=datasets['pull_request_id']) - @print_timing - def measure(webdriver, interaction): + @print_timing("selenium_view_pull_request_overview") + def measure(): pull_request_page.go_to_overview() - pull_request_page.wait_for_overview_tab(interaction) + pull_request_page.wait_for_overview_tab() PopupManager(webdriver).dismiss_default_popup() - measure(webdriver, 'selenium_view_pull_request_overview') + measure() def view_pull_request_diff_tab(webdriver, datasets): @@ -110,12 +115,12 @@ def view_pull_request_diff_tab(webdriver, datasets): repo_slug=datasets['repo_slug'], pull_request_key=datasets['pull_request_id']) - @print_timing - def measure(webdriver, interaction): + @print_timing("selenium_view_pull_request_diff") + def measure(): pull_request_page.go_to_diff() - pull_request_page.wait_for_diff_tab(interaction) + pull_request_page.wait_for_diff_tab() PopupManager(webdriver).dismiss_default_popup() - measure(webdriver, 'selenium_view_pull_request_diff') + measure() def view_pull_request_commits_tab(webdriver, datasets): @@ -123,12 +128,12 @@ def view_pull_request_commits_tab(webdriver, datasets): repo_slug=datasets['repo_slug'], pull_request_key=datasets['pull_request_id']) - @print_timing - def measure(webdriver, interaction): + @print_timing("selenium_view_pull_request_commits") + def measure(): pull_request_page.go_to_commits() - pull_request_page.wait_for_commits_tab(interaction) + pull_request_page.wait_for_commits_tab() PopupManager(webdriver).dismiss_default_popup() - measure(webdriver, 'selenium_view_pull_request_commits') + measure() def comment_pull_request_diff(webdriver, datasets): @@ -136,16 +141,17 @@ def comment_pull_request_diff(webdriver, datasets): repo_slug=datasets['repo_slug'], pull_request_key=datasets['pull_request_id']) pull_request_page.go_to_diff() - @print_timing - def measure(webdriver, interaction): + + @print_timing("selenium_comment_pull_request_file") + def measure(): PopupManager(webdriver).dismiss_default_popup() - pull_request_page.wait_for_diff_tab(interaction) + pull_request_page.wait_for_diff_tab() PopupManager(webdriver).dismiss_default_popup() - pull_request_page.wait_for_code_diff(interaction) + pull_request_page.wait_for_code_diff() PopupManager(webdriver).dismiss_default_popup() pull_request_page.click_inline_comment_button_js() - pull_request_page.add_code_comment(interaction) - measure(webdriver, 'selenium_comment_pull_request_file') + pull_request_page.add_code_comment() + measure() def comment_pull_request_overview(webdriver, datasets): @@ -153,26 +159,27 @@ def comment_pull_request_overview(webdriver, datasets): repo_slug=datasets['repo_slug'], pull_request_key=datasets['pull_request_id']) pull_request_page.go_to() - @print_timing - def measure(webdriver, interaction): + + @print_timing("selenium_comment_pull_request_overview") + def measure(): PopupManager(webdriver).dismiss_default_popup() - pull_request_page.wait_for_overview_tab(interaction) + pull_request_page.wait_for_overview_tab() PopupManager(webdriver).dismiss_default_popup() - pull_request_page.add_overview_comment(interaction) - pull_request_page.click_save_comment_button(interaction) - measure(webdriver, 'selenium_comment_pull_request_overview') + pull_request_page.add_overview_comment() + pull_request_page.click_save_comment_button() + measure() def view_branches(webdriver, datasets): branches_page = RepositoryBranches(webdriver, project_key=datasets['project_key'], repo_slug=datasets['repo_slug']) - @print_timing - def measure(webdriver, interaction): + @print_timing("selenium_view_branches") + def measure(): branches_page.go_to() - branches_page.wait_for_page_loaded(interaction) + branches_page.wait_for_page_loaded() PopupManager(webdriver).dismiss_default_popup() - measure(webdriver, 'selenium_view_branches') + measure() def create_pull_request(webdriver, datasets): @@ -186,59 +193,55 @@ def create_pull_request(webdriver, datasets): navigation_panel = RepoNavigationPanel(webdriver) PopupManager(webdriver).dismiss_default_popup() - @print_timing - def measure(webdriver, interaction): - @print_timing - def measure(webdriver, interaction): + @print_timing("selenium_create_pull_request") + def measure(): + + @print_timing("selenium_create_pull_request:create_pull_request") + def sub_measure(): branch_from = datasets['pull_request_branch_from'] branch_to = datasets['pull_request_branch_to'] - repository_branches_page.open_base_branch(interaction=interaction, - base_branch_name=branch_from) - fork_branch_from = repository_branches_page.create_branch_fork_rnd_name(interaction=interaction, - base_branch_name=branch_from) - navigation_panel.wait_for_navigation_panel(interaction) - repository_branches_page.open_base_branch(interaction=interaction, - base_branch_name=branch_to) - fork_branch_to = repository_branches_page.create_branch_fork_rnd_name(interaction=interaction, - base_branch_name=branch_to) + repository_branches_page.open_base_branch(base_branch_name=branch_from) + fork_branch_from = repository_branches_page.create_branch_fork_rnd_name(base_branch_name=branch_from) + navigation_panel.wait_for_navigation_panel() + repository_branches_page.open_base_branch(base_branch_name=branch_to) + fork_branch_to = repository_branches_page.create_branch_fork_rnd_name(base_branch_name=branch_to) datasets['pull_request_fork_branch_to'] = fork_branch_to - navigation_panel.wait_for_navigation_panel(interaction) - - repo_pull_requests_page.create_new_pull_request(interaction=interaction, from_branch=fork_branch_from, - to_branch=fork_branch_to) + navigation_panel.wait_for_navigation_panel() + repo_pull_requests_page.create_new_pull_request(from_branch=fork_branch_from, to_branch=fork_branch_to) PopupManager(webdriver).dismiss_default_popup() + sub_measure() - measure(webdriver, 'selenium_create_pull_request:create_pull_request') - - @print_timing - def measure(webdriver, interaction): + @print_timing("selenium_create_pull_request:merge_pull_request") + def sub_measure(): PopupManager(webdriver).dismiss_default_popup() pull_request_page = PullRequest(webdriver) - pull_request_page.wait_for_overview_tab(interaction) + pull_request_page.wait_for_overview_tab() PopupManager(webdriver).dismiss_default_popup() - pull_request_page.merge_pull_request(interaction) - measure(webdriver, 'selenium_create_pull_request:merge_pull_request') + pull_request_page.merge_pull_request() + sub_measure() + repository_branches_page.go_to() - repository_branches_page.wait_for_page_loaded(interaction) - repository_branches_page.delete_branch(interaction=interaction, - branch_name=datasets['pull_request_fork_branch_to']) - measure(webdriver, 'selenium_create_pull_request') + repository_branches_page.wait_for_page_loaded() + repository_branches_page.delete_branch(branch_name=datasets['pull_request_fork_branch_to']) + measure() def view_commits(webdriver, datasets): repo_commits_page = RepositoryCommits(webdriver, project_key=datasets['project_key'], repo_slug=datasets['repo_slug']) - @print_timing - def measure(webdriver, interaction): + + @print_timing("selenium_view_commits") + def measure(): repo_commits_page.go_to() - repo_commits_page.wait_for_page_loaded(interaction) + repo_commits_page.wait_for_page_loaded() PopupManager(webdriver).dismiss_default_popup() - measure(webdriver, 'selenium_view_commits') + measure() def logout(webdriver, datasets): - @print_timing - def measure(webdriver, interaction): + + @print_timing("selenium_log_out") + def measure(): logout_page_page = LogoutPage(webdriver) logout_page_page.go_to() - measure(webdriver, "selenium_log_out") + measure() diff --git a/app/selenium_ui/bitbucket/pages/pages.py b/app/selenium_ui/bitbucket/pages/pages.py index 71d288007..ab7a5ac4e 100644 --- a/app/selenium_ui/bitbucket/pages/pages.py +++ b/app/selenium_ui/bitbucket/pages/pages.py @@ -16,8 +16,8 @@ def fill_username(self, username): def fill_password(self, password): self.get_element(LoginPageLocators.password_textfield).send_keys(password) - def submit_login(self, interaction=None): - self.wait_until_visible(LoginPageLocators.submit_button, interaction).click() + def submit_login(self): + self.wait_until_visible(LoginPageLocators.submit_button).click() def set_credentials(self, username, password): self.fill_username(username) @@ -64,18 +64,18 @@ class RepoNavigationPanel(BasePage): def __clone_repo_button(self): return self.get_element(RepoNavigationPanelLocators.clone_repo_button) - def wait_for_navigation_panel(self, interaction): - return self.wait_until_present(RepoNavigationPanelLocators.navigation_panel, interaction) + def wait_for_navigation_panel(self): + return self.wait_until_present(RepoNavigationPanelLocators.navigation_panel) def clone_repo_click(self): self.__clone_repo_button().click() - def fork_repo(self, interaction): - return self.wait_until_visible(RepoNavigationPanelLocators.fork_repo_button, interaction) + def fork_repo(self): + return self.wait_until_visible(RepoNavigationPanelLocators.fork_repo_button) - def create_pull_request(self, interaction): - self.wait_until_visible(RepoNavigationPanelLocators.create_pull_request_button, interaction).click() - self.wait_until_visible(RepoLocators.pull_requests_list, interaction) + def create_pull_request(self): + self.wait_until_visible(RepoNavigationPanelLocators.create_pull_request_button).click() + self.wait_until_visible(RepoLocators.pull_requests_list) class PopupManager(BasePage): @@ -93,8 +93,8 @@ def __init__(self, driver, project_key, repo_slug): self.repo_slug = repo_slug self.project_key = project_key - def set_enable_fork_sync(self, interaction, value): - checkbox = self.wait_until_visible(RepoLocators.repo_fork_sync, interaction) + def set_enable_fork_sync(self, value): + checkbox = self.wait_until_visible(RepoLocators.repo_fork_sync) current_state = checkbox.is_selected() if (value and not current_state) or (not value and current_state): checkbox.click() @@ -118,44 +118,44 @@ def __init__(self, driver, project_key, repo_slug): self.url_manager = UrlManager(project_key=project_key, repo_slug=repo_slug) self.page_url = self.url_manager.repo_pull_requests() - def create_new_pull_request(self, from_branch, to_branch, interaction): + def create_new_pull_request(self, from_branch, to_branch): self.go_to_url(url=self.url_manager.create_pull_request_url(from_branch=from_branch, to_branch=to_branch)) - self.submit_pull_request(interaction) + self.submit_pull_request() - def set_pull_request_source_branch(self, interaction, source_branch): - self.wait_until_visible(RepoLocators.pr_source_branch_field, interaction).click() - self.wait_until_visible(RepoLocators.pr_branches_dropdown, interaction) + def set_pull_request_source_branch(self, source_branch): + self.wait_until_visible(RepoLocators.pr_source_branch_field).click() + self.wait_until_visible(RepoLocators.pr_branches_dropdown) source_branch_name_field = self.get_element(RepoLocators.pr_source_branch_name) source_branch_name_field.send_keys(source_branch) - self.wait_until_invisible(RepoLocators.pr_source_branch_spinner, interaction) + self.wait_until_invisible(RepoLocators.pr_source_branch_spinner) source_branch_name_field.send_keys(Keys.ENTER) - self.wait_until_invisible(RepoLocators.pr_branches_dropdown, interaction) + self.wait_until_invisible(RepoLocators.pr_branches_dropdown) - def set_pull_request_destination_repo(self, interaction): - self.wait_until_visible(RepoLocators.pr_destination_repo_field, interaction).click() - self.wait_until_visible(RepoLocators.pr_destination_first_repo_dropdown, interaction).click() + def set_pull_request_destination_repo(self): + self.wait_until_visible(RepoLocators.pr_destination_repo_field).click() + self.wait_until_visible(RepoLocators.pr_destination_first_repo_dropdown).click() - def set_pull_request_destination_branch(self, interaction, destination_branch): - self.wait_until_visible(RepoLocators.pr_destination_branch_field, interaction) + def set_pull_request_destination_branch(self, destination_branch): + self.wait_until_visible(RepoLocators.pr_destination_branch_field) self.execute_js("document.querySelector('#targetBranch').click()") - self.wait_until_visible(RepoLocators.pr_destination_branch_dropdown, interaction) + self.wait_until_visible(RepoLocators.pr_destination_branch_dropdown) destination_branch_name_field = self.get_element(RepoLocators.pr_destination_branch_name) destination_branch_name_field.send_keys(destination_branch) - self.wait_until_invisible(RepoLocators.pr_destination_branch_spinner, interaction) + self.wait_until_invisible(RepoLocators.pr_destination_branch_spinner) destination_branch_name_field.send_keys(Keys.ENTER) - self.wait_until_invisible(RepoLocators.pr_branches_dropdown, interaction) - self.wait_until_clickable(RepoLocators.pr_continue_button, interaction) - self.wait_until_visible(RepoLocators.pr_continue_button, interaction).click() + self.wait_until_invisible(RepoLocators.pr_branches_dropdown) + self.wait_until_clickable(RepoLocators.pr_continue_button) + self.wait_until_visible(RepoLocators.pr_continue_button).click() - def submit_pull_request(self, interaction): - self.wait_until_visible(RepoLocators.pr_description_field, interaction) + def submit_pull_request(self): + self.wait_until_visible(RepoLocators.pr_description_field) title = self.get_element(RepoLocators.pr_title_field) title.clear() title.send_keys('Selenium test pull request') - self.wait_until_visible(RepoLocators.pr_submit_button, interaction).click() - self.wait_until_visible(PullRequestLocator.pull_request_activity_content, interaction) - self.wait_until_clickable(PullRequestLocator.pull_request_page_merge_button, interaction) + self.wait_until_visible(RepoLocators.pr_submit_button).click() + self.wait_until_visible(PullRequestLocator.pull_request_activity_content) + self.wait_until_clickable(PullRequestLocator.pull_request_page_merge_button) class PullRequest(BasePage): @@ -168,8 +168,8 @@ def __init__(self, driver, project_key=None, repo_slug=None, pull_request_key=No self.diff_url = url_manager.pull_request_diff() self.commits_url = url_manager.pull_request_commits() - def wait_for_overview_tab(self, interaction): - return self.wait_until_visible(PullRequestLocator.pull_request_activity_content, interaction) + def wait_for_overview_tab(self): + return self.wait_until_visible(PullRequestLocator.pull_request_activity_content) def go_to_overview(self): return self.go_to() @@ -180,14 +180,14 @@ def go_to_diff(self): def go_to_commits(self): self.go_to_url(self.commits_url) - def wait_for_diff_tab(self, interaction): - return self.wait_until_any_element_visible(PullRequestLocator.commit_files, interaction) + def wait_for_diff_tab(self, ): + return self.wait_until_any_element_visible(PullRequestLocator.commit_files) - def wait_for_code_diff(self, interaction): - return self.wait_until_visible(PullRequestLocator.diff_code_lines, interaction) + def wait_for_code_diff(self, ): + return self.wait_until_visible(PullRequestLocator.diff_code_lines) - def wait_for_commits_tab(self, interaction): - self.wait_until_any_element_visible(PullRequestLocator.commit_message_label, interaction) + def wait_for_commits_tab(self, ): + self.wait_until_any_element_visible(PullRequestLocator.commit_message_label) def click_inline_comment_button_js(self): selector = self.get_selector(PullRequestLocator.inline_comment_button) @@ -196,45 +196,45 @@ def click_inline_comment_button_js(self): "item.scrollIntoView();" "item.click();") - def wait_for_comment_text_area(self, interaction): - return self.wait_until_visible(PullRequestLocator.comment_text_area, interaction) + def wait_for_comment_text_area(self): + return self.wait_until_visible(PullRequestLocator.comment_text_area) - def add_code_comment_v6(self, interaction): - self.wait_for_comment_text_area(interaction) + def add_code_comment_v6(self): + self.wait_for_comment_text_area() selector = self.get_selector(PullRequestLocator.comment_text_area) self.execute_js(f"document.querySelector('{selector[1]}').value = 'Comment from Selenium script';") - self.click_save_comment_button(interaction) + self.click_save_comment_button() - def add_code_comment_v7(self, interaction): - self.wait_until_visible(PullRequestLocator.text_area, interaction).send_keys('Comment from Selenium script') - self.click_save_comment_button(interaction) + def add_code_comment_v7(self): + self.wait_until_visible(PullRequestLocator.text_area).send_keys('Comment from Selenium script') + self.click_save_comment_button() - def add_code_comment(self, interaction): + def add_code_comment(self, ): if self.app_version == '6': - self.add_code_comment_v6(interaction) + self.add_code_comment_v6() elif self.app_version == '7': - self.add_code_comment_v7(interaction) + self.add_code_comment_v7() - def click_save_comment_button(self, interaction): - return self.wait_until_visible(PullRequestLocator.comment_button, interaction).click() + def click_save_comment_button(self): + return self.wait_until_visible(PullRequestLocator.comment_button).click() - def add_overview_comment(self, interaction): - self.wait_for_comment_text_area(interaction).click() - self.wait_until_clickable(PullRequestLocator.text_area, interaction).send_keys(self.generate_random_string(50)) + def add_overview_comment(self): + self.wait_for_comment_text_area().click() + self.wait_until_clickable(PullRequestLocator.text_area).send_keys(self.generate_random_string(50)) - def wait_merge_button_clickable(self, interaction): - self.wait_until_clickable(PullRequestLocator.pull_request_page_merge_button, interaction) + def wait_merge_button_clickable(self): + self.wait_until_clickable(PullRequestLocator.pull_request_page_merge_button) - def merge_pull_request(self, interaction): + def merge_pull_request(self): if self.driver.app_version == '6': if self.get_elements(PullRequestLocator.merge_spinner): - self.wait_until_invisible(PullRequestLocator.merge_spinner, interaction) + self.wait_until_invisible(PullRequestLocator.merge_spinner) self.wait_until_present(PullRequestLocator.pull_request_page_merge_button).click() PopupManager(self.driver).dismiss_default_popup() self.wait_until_visible(PullRequestLocator.diagram_selector) self.get_element(PullRequestLocator.delete_branch_per_merge_checkbox).click() - self.wait_until_clickable(PullRequestLocator.pull_request_modal_merge_button, interaction).click() - self.wait_until_invisible(PullRequestLocator.del_branch_checkbox_selector, interaction) + self.wait_until_clickable(PullRequestLocator.pull_request_modal_merge_button).click() + self.wait_until_invisible(PullRequestLocator.del_branch_checkbox_selector) class RepositoryBranches(BasePage): @@ -245,39 +245,38 @@ def __init__(self, driver, project_key, repo_slug): self.url_manager = UrlManager(project_key=project_key, repo_slug=repo_slug) self.page_url = self.url_manager.repo_branches() - def open_base_branch(self, base_branch_name, interaction): + def open_base_branch(self, base_branch_name): self.go_to_url(f"{self.url_manager.base_branch_url()}{base_branch_name}") - self.wait_until_visible(BranchesLocator.branches_name, interaction) + self.wait_until_visible(BranchesLocator.branches_name) - def create_branch_fork_rnd_name(self, base_branch_name, interaction): - self.wait_until_visible(BranchesLocator.branches_action, interaction).click() + def create_branch_fork_rnd_name(self, base_branch_name): + self.wait_until_visible(BranchesLocator.branches_action).click() self.get_element(BranchesLocator.branches_action_create_branch).click() - self.wait_until_visible(BranchesLocator.new_branch_name_textfield, interaction) + self.wait_until_visible(BranchesLocator.new_branch_name_textfield) branch_name = f"{base_branch_name}-{self.generate_random_string(5)}".replace(' ', '-') self.get_element(BranchesLocator.new_branch_name_textfield).send_keys(branch_name) - self.wait_until_clickable(BranchesLocator.new_branch_submit_button, interaction).click() + self.wait_until_clickable(BranchesLocator.new_branch_submit_button).click() return branch_name - def delete_branch(self, interaction, branch_name): - self.wait_until_visible(BranchesLocator.search_branch_textfield, interaction).send_keys(branch_name) - self.wait_until_visible(BranchesLocator.branches_name, interaction) - self.wait_until_visible(BranchesLocator.search_branch_action, interaction).click() + def delete_branch(self, branch_name): + self.wait_until_visible(BranchesLocator.search_branch_textfield).send_keys(branch_name) + self.wait_until_visible(BranchesLocator.branches_name) + self.wait_until_visible(BranchesLocator.search_branch_action).click() self.execute_js("document.querySelector('li>a.delete-branch').click()") - self.wait_until_clickable(BranchesLocator.delete_branch_diaglog_submit, interaction).click() + self.wait_until_clickable(BranchesLocator.delete_branch_diaglog_submit).click() class RepositorySettings(BasePage): - def wait_repository_settings(self, interaction): - self.wait_until_visible(RepositorySettingsLocator.repository_settings_menu, interaction) + def wait_repository_settings(self): + self.wait_until_visible(RepositorySettingsLocator.repository_settings_menu) - def delete_repository(self, interaction, repo_slug): - self.wait_repository_settings(interaction) - self.wait_until_visible(RepositorySettingsLocator.delete_repository_button, interaction).click() - self.wait_until_visible(RepositorySettingsLocator.delete_repository_modal_text_field, - interaction).send_keys(repo_slug) - self.wait_until_clickable(RepositorySettingsLocator.delete_repository_modal_submit_button, interaction) - self.wait_until_visible(RepositorySettingsLocator.delete_repository_modal_submit_button, interaction).click() + def delete_repository(self, repo_slug): + self.wait_repository_settings() + self.wait_until_visible(RepositorySettingsLocator.delete_repository_button).click() + self.wait_until_visible(RepositorySettingsLocator.delete_repository_modal_text_field,).send_keys(repo_slug) + self.wait_until_clickable(RepositorySettingsLocator.delete_repository_modal_submit_button) + self.wait_until_visible(RepositorySettingsLocator.delete_repository_modal_submit_button).click() class ForkRepositorySettings(RepositorySettings): @@ -294,8 +293,8 @@ def __init__(self, driver, user): url_manager = UrlManager(user=user) self.page_url = url_manager.user_settings_url() - def user_role_visible(self, interaction): - return self.wait_until_visible(UserSettingsLocator.user_role_label, interaction) + def user_role_visible(self): + return self.wait_until_visible(UserSettingsLocator.user_role_label) class RepositoryCommits(BasePage): diff --git a/app/selenium_ui/bitbucket/pages/selectors.py b/app/selenium_ui/bitbucket/pages/selectors.py index 8738c594d..993aefb66 100644 --- a/app/selenium_ui/bitbucket/pages/selectors.py +++ b/app/selenium_ui/bitbucket/pages/selectors.py @@ -183,7 +183,7 @@ class PullRequestLocator: '7': (By.CSS_SELECTOR, ".diff-line-comment-trigger")} comment_text_area = {'6': (By.CSS_SELECTOR, "textarea.text"), '7': (By.CLASS_NAME, "comment-editor-wrapper")} text_area = {'6': (By.CSS_SELECTOR, 'textarea.text'), '7': (By.CLASS_NAME, 'CodeMirror-code')} - comment_button = {'6': (By.CSS_SELECTOR, "div.buttons>button:nth-child(1)"), + comment_button = {'6': (By.CSS_SELECTOR, "div.comment-form-footer>div.buttons>button:nth-child(1)"), '7': (By.CSS_SELECTOR, "div.editor-controls>button:nth-child(1)")} pull_request_activity_content = {'6': (By.CSS_SELECTOR, ".pull-request-activity-content"), '7': (By.CSS_SELECTOR, ".pull-request-activities")} diff --git a/app/selenium_ui/bitbucket_ui.py b/app/selenium_ui/bitbucket_ui.py index 6363a726f..66f2a7657 100644 --- a/app/selenium_ui/bitbucket_ui.py +++ b/app/selenium_ui/bitbucket_ui.py @@ -1,5 +1,5 @@ from selenium_ui.bitbucket import modules -from extension.bitbucket import extension_ui +from extension.bitbucket import extension_ui # noqa F401 # this action should be the first one @@ -65,11 +65,11 @@ def test_13_selenium_view_commits(webdriver, bitbucket_datasets, bitbucket_scree """ Add custom actions anywhere between login and log out action. Move this to a different line as needed. -Write your custom selenium scripts in `app/extension/jira/extension_ui.py`. +Write your custom selenium scripts in `app/extension/jira/extension_ui.py`. Refer to `app/selenium_ui/jira/modules.py` for examples. """ # def test_1_selenium_custom_action(webdriver, bitbucket_datasets, bitbucket_screen_shots): -# extension_ui.custom_action(webdriver, bitbucket_datasets) +# extension_ui.app_specific_action(webdriver, bitbucket_datasets) def test_14_selenium_logout(webdriver, bitbucket_datasets, bitbucket_screen_shots): diff --git a/app/selenium_ui/confluence/modules.py b/app/selenium_ui/confluence/modules.py index 65e082565..f4ac5d145 100644 --- a/app/selenium_ui/confluence/modules.py +++ b/app/selenium_ui/confluence/modules.py @@ -18,120 +18,136 @@ def setup_run_data(datasets): def login(webdriver, datasets): setup_run_data(datasets) login_page = Login(webdriver) - @print_timing - def measure(webdriver, interaction): - @print_timing - def measure(webdriver, interaction): + + @print_timing("selenium_login") + def measure(): + + @print_timing("selenium_login:open_login_page") + def sub_measure(): login_page.go_to() - login_page.wait_for_page_loaded(interaction) - measure(webdriver, "selenium_login:open_login_page") + login_page.wait_for_page_loaded() + sub_measure() login_page.set_credentials(username=datasets['username'], password=datasets['password']) - @print_timing - def measure(webdriver, interaction): - login_page.click_login_button(interaction) + @print_timing("selenium_login:login_and_view_dashboard") + def sub_measure(): + login_page.click_login_button() if login_page.is_first_login(): - login_page.first_user_setup(interaction) + login_page.first_user_setup() all_updates_page = AllUpdates(webdriver) - all_updates_page.wait_for_page_loaded(interaction) - measure(webdriver, "selenium_login:login_and_view_dashboard") - measure(webdriver, 'selenium_login') + all_updates_page.wait_for_page_loaded() + sub_measure() + measure() PopupManager(webdriver).dismiss_default_popup() def view_page(webdriver, datasets): page = Page(webdriver, page_id=datasets['page_id']) - @print_timing - def measure(webdriver, interaction): + @print_timing("selenium_view_page") + def measure(): page.go_to() - page.wait_for_page_loaded(interaction) - measure(webdriver, "selenium_view_page") + page.wait_for_page_loaded() + measure() def view_blog(webdriver, datasets): blog = Page(webdriver, page_id=datasets['blog_id']) - @print_timing - def measure(webdriver, interaction): + @print_timing("selenium_view_blog") + def measure(): blog.go_to() - blog.wait_for_page_loaded(interaction) - measure(webdriver, "selenium_view_blog") + blog.wait_for_page_loaded() + measure() def view_dashboard(webdriver, datasets): dashboard_page = Dashboard(webdriver) - @print_timing - def measure(webdriver, interaction): + @print_timing("selenium_view_dashboard") + def measure(): dashboard_page.go_to() - dashboard_page.wait_for_page_loaded(interaction) - measure(webdriver, "selenium_view_dashboard") + dashboard_page.wait_for_page_loaded() + measure() -def create_page(webdriver, datasets): +def create_confluence_page(webdriver, datasets): nav_panel = TopNavPanel(webdriver) create_page = Editor(webdriver) - @print_timing - def measure(webdriver, interaction): - nav_panel.click_create(interaction) - PopupManager(webdriver).dismiss_default_popup() - create_page.wait_for_create_page_open(interaction) - measure(webdriver, "selenium_create_page:open_create_page_editor") + @print_timing("selenium_create_page") + def measure(): - PopupManager(webdriver).dismiss_default_popup() + @print_timing("selenium_create_page:open_create_page_editor") + def sub_measure(): + nav_panel.click_create() + PopupManager(webdriver).dismiss_default_popup() + create_page.wait_for_create_page_open() + sub_measure() + + PopupManager(webdriver).dismiss_default_popup() - create_page.write_title() - create_page.write_content(interaction='create page') + create_page.write_title() + create_page.write_content() - @print_timing - def measure(webdriver, interaction): - create_page.click_submit() - page = Page(webdriver) - page.wait_for_page_loaded(interaction) - measure(webdriver, "selenium_create_page:save_created_page") + @print_timing("selenium_create_page:save_created_page") + def sub_measure(): + create_page.click_submit() + page = Page(webdriver) + page.wait_for_page_loaded() + sub_measure() + measure() -def edit_page(webdriver, datasets): +def edit_confluence_page(webdriver, datasets): edit_page = Editor(webdriver, page_id=datasets['page_id']) - @print_timing - def measure(webdriver, interaction): - edit_page.go_to() - edit_page.wait_for_page_loaded(interaction) - measure(webdriver, "selenium_edit_page:open_create_page_editor") + @print_timing("selenium_edit_page") + def measure(): - edit_page.write_content(interaction='edit page') + @print_timing("selenium_edit_page:open_create_page_editor") + def sub_measure(): + edit_page.go_to() + edit_page.wait_for_page_loaded() + sub_measure() - @print_timing - def measure(webdriver, interaction): - edit_page.save_edited_page(interaction) - measure(webdriver, "selenium_edit_page:save_edited_page") + edit_page.write_content() + + @print_timing("selenium_edit_page:save_edited_page") + def sub_measure(): + edit_page.save_edited_page() + sub_measure() + measure() def create_comment(webdriver, datasets): page = Page(webdriver, page_id=datasets['page_id']) - page.go_to() - page.wait_for_page_loaded(interaction='create comment') - edit_comment = Editor(webdriver) - @print_timing - def measure(webdriver, interaction): - page.click_add_comment() - edit_comment.write_content(interaction=interaction, text='This is selenium comment') - measure(webdriver, 'selenium_create_comment:write_comment') - - @print_timing - def measure(webdriver, interaction): - edit_comment.click_submit() - page.wait_for_comment_field(interaction) - measure(webdriver, "selenium_create_comment:save_comment") + + @print_timing("selenium_create_comment") + def measure(): + page.go_to() + page.wait_for_page_loaded() + edit_comment = Editor(webdriver) + + @print_timing("selenium_create_comment:write_comment") + def sub_measure(): + page.click_add_comment() + edit_comment.write_content(text='This is selenium comment') + sub_measure() + + @print_timing("selenium_create_comment:save_comment") + def sub_measure(): + edit_comment.click_submit() + page.wait_for_comment_field() + sub_measure() + measure() def log_out(webdriver, datasets): - @print_timing - def measure(webdriver, interaction): + + @print_timing("selenium_log_out") + def measure(): logout_page = Logout(webdriver) logout_page.go_to() - measure(webdriver, "selenium_log_out") + measure() diff --git a/app/selenium_ui/confluence/pages/pages.py b/app/selenium_ui/confluence/pages/pages.py index 263e33ae7..85c0edebe 100644 --- a/app/selenium_ui/confluence/pages/pages.py +++ b/app/selenium_ui/confluence/pages/pages.py @@ -13,22 +13,22 @@ def set_credentials(self, username, password): self.get_element(LoginPageLocators.login_username_field).send_keys(username) self.get_element(LoginPageLocators.login_password_field).send_keys(password) - def click_login_button(self, interaction): - self.wait_until_visible(LoginPageLocators.login_button, interaction).click() - self.wait_until_invisible(LoginPageLocators.login_button, interaction) + def click_login_button(self): + self.wait_until_visible(LoginPageLocators.login_button).click() + self.wait_until_invisible(LoginPageLocators.login_button) def is_first_login(self): - elems = self.get_elements(LoginPageLocators.first_login_setup_page) - return True if elems else False + elements = self.get_elements(LoginPageLocators.first_login_setup_page) + return True if elements else False - def first_user_setup(self, interaction): + def first_user_setup(self): if self.get_element(LoginPageLocators.current_step_sel).text == 'Welcome': - self.wait_until_clickable(LoginPageLocators.skip_welcome_button, interaction).click() + self.wait_until_clickable(LoginPageLocators.skip_welcome_button).click() if self.get_element(LoginPageLocators.current_step_sel).text == 'Upload your photo': - self.wait_until_clickable(LoginPageLocators.skip_photo_upload, interaction).click() + self.wait_until_clickable(LoginPageLocators.skip_photo_upload).click() if self.get_element(LoginPageLocators.current_step_sel).text == 'Find content': - self.wait_until_any_element_visible(LoginPageLocators.skip_find_content, interaction)[0].click() - self.wait_until_clickable(LoginPageLocators.finish_setup, interaction).click() + self.wait_until_any_element_visible(LoginPageLocators.skip_find_content)[0].click() + self.wait_until_clickable(LoginPageLocators.finish_setup).click() class Logout(BasePage): @@ -59,8 +59,8 @@ def click_add_comment(self): css_selector = PageLocators.comment_text_field[1] self.execute_js(f"document.querySelector('{css_selector}').click()") - def wait_for_comment_field(self, interaction): - self.wait_until_visible(PageLocators.comment_text_field, interaction) + def wait_for_comment_field(self): + self.wait_until_visible(PageLocators.comment_text_field) class Dashboard(BasePage): @@ -70,8 +70,8 @@ class Dashboard(BasePage): class TopNavPanel(BasePage): - def click_create(self, interaction): - self.wait_until_clickable(TopPanelLocators.create_button, interaction).click() + def click_create(self): + self.wait_until_clickable(TopPanelLocators.create_button).click() class Editor(BasePage): @@ -81,21 +81,21 @@ def __init__(self, driver, page_id=None): url_manager = UrlManager(page_id=page_id) self.page_url = url_manager.edit_page_url() - def wait_for_create_page_open(self, interaction): - self.wait_until_clickable(EditorLocators.publish_button, interaction) + def wait_for_create_page_open(self): + self.wait_until_clickable(EditorLocators.publish_button) - def wait_for_page_loaded(self, interaction): - self.wait_for_editor_open(interaction) - self.wait_until_clickable(EditorLocators.publish_button, interaction) + def wait_for_page_loaded(self): + self.wait_for_editor_open() + self.wait_until_clickable(EditorLocators.publish_button) def write_title(self): - title_field = self.wait_until_visible(EditorLocators.title_field, interaction='page title') + title_field = self.wait_until_visible(EditorLocators.title_field) title = "Selenium - " + self.generate_random_string(10) title_field.clear() title_field.send_keys(title) - def write_content(self, interaction, text=None): - self.wait_until_available_to_switch(EditorLocators.page_content_field, interaction=interaction) + def write_content(self, text=None): + self.wait_until_available_to_switch(EditorLocators.page_content_field) text = self.generate_random_string(30) if not text else text tinymce_text_el = self.get_element(EditorLocators.tinymce_page_content_field) tinymce_text_el.find_element_by_tag_name('p').send_keys(text) @@ -104,17 +104,15 @@ def write_content(self, interaction, text=None): def click_submit(self): self.get_element(EditorLocators.publish_button).click() - def wait_for_editor_open(self, interaction): + def wait_for_editor_open(self): self.wait_until_any_ec_text_presented_in_el(selector_names=[(EditorLocators.status_indicator, 'Ready to go'), - (EditorLocators.status_indicator, 'Changes saved')], - interaction=interaction) + (EditorLocators.status_indicator, 'Changes saved')]) - def save_edited_page(self, interaction): + def save_edited_page(self): self.get_element(EditorLocators.publish_button).click() if self.get_elements(EditorLocators.confirm_publishing_button): if self.get_element(EditorLocators.confirm_publishing_button).is_displayed(): self.get_element(EditorLocators.confirm_publishing_button).click() - self.wait_until_invisible(EditorLocators.save_spinner, interaction) + self.wait_until_invisible(EditorLocators.save_spinner) self.wait_until_any_ec_presented(selector_names=[PageLocators.page_title, - EditorLocators.confirm_publishing_button], - interaction=interaction) + EditorLocators.confirm_publishing_button]) diff --git a/app/selenium_ui/confluence-ui.py b/app/selenium_ui/confluence_ui.py similarity index 84% rename from app/selenium_ui/confluence-ui.py rename to app/selenium_ui/confluence_ui.py index 64d0376bf..a6fab4647 100644 --- a/app/selenium_ui/confluence-ui.py +++ b/app/selenium_ui/confluence_ui.py @@ -1,5 +1,5 @@ from selenium_ui.confluence import modules -from extension.confluence import extension_ui +from extension.confluence import extension_ui # noqa F401 # this action should be the first one @@ -12,11 +12,11 @@ def test_1_selenium_view_page(webdriver, confluence_datasets, confluence_screen_ def test_1_selenium_create_page(webdriver, confluence_datasets, confluence_screen_shots): - modules.create_page(webdriver, confluence_datasets) + modules.create_confluence_page(webdriver, confluence_datasets) def test_1_selenium_edit_page(webdriver, confluence_datasets, confluence_screen_shots): - modules.edit_page(webdriver, confluence_datasets) + modules.edit_confluence_page(webdriver, confluence_datasets) def test_1_selenium_create_comment(webdriver, confluence_datasets, confluence_screen_shots): @@ -33,11 +33,11 @@ def test_1_selenium_view_dashboard(webdriver, confluence_datasets, confluence_sc """ Add custom actions anywhere between login and log out action. Move this to a different line as needed. -Write your custom selenium scripts in `app/extension/confluence/extension_ui.py`. +Write your custom selenium scripts in `app/extension/confluence/extension_ui.py`. Refer to `app/selenium_ui/confluence/modules.py` for examples. """ # def test_1_selenium_custom_action(webdriver, confluence_datasets, confluence_screen_shots): -# extension_ui.custom_action(webdriver, confluence_datasets) +# extension_ui.app_specific_action(webdriver, confluence_datasets) # this action should be the last one diff --git a/app/selenium_ui/conftest.py b/app/selenium_ui/conftest.py index 8de5ff5b6..e8e4f2716 100644 --- a/app/selenium_ui/conftest.py +++ b/app/selenium_ui/conftest.py @@ -1,45 +1,76 @@ import atexit import csv import datetime -import json import os -import random -import string import sys import time -from pathlib import Path +import functools import pytest +from selenium.common.exceptions import WebDriverException from selenium.webdriver import Chrome from selenium.webdriver.chrome.options import Options from util.conf import CONFLUENCE_SETTINGS, JIRA_SETTINGS, BITBUCKET_SETTINGS from util.project_paths import JIRA_DATASET_ISSUES, JIRA_DATASET_JQLS, JIRA_DATASET_KANBAN_BOARDS, \ - JIRA_DATASET_PROJECT_KEYS, JIRA_DATASET_SCRUM_BOARDS, JIRA_DATASET_USERS, BITBUCKET_USERS, BITBUCKET_PROJECTS, \ - BITBUCKET_REPOS, BITBUCKET_PRS, CONFLUENCE_BLOGS, CONFLUENCE_PAGES, CONFLUENCE_USERS + JIRA_DATASET_PROJECTS, JIRA_DATASET_SCRUM_BOARDS, JIRA_DATASET_USERS, BITBUCKET_USERS, BITBUCKET_PROJECTS, \ + BITBUCKET_REPOS, BITBUCKET_PRS, CONFLUENCE_BLOGS, CONFLUENCE_PAGES, CONFLUENCE_USERS, ENV_TAURUS_ARTIFACT_DIR SCREEN_WIDTH = 1920 SCREEN_HEIGHT = 1080 JTL_HEADER = "timeStamp,elapsed,label,responseCode,responseMessage,threadName,success,bytes,grpThreads,allThreads," \ "Latency,Hostname,Connect\n" +LOGIN_ACTION_NAME = 'login' -def __get_current_results_dir(): - if 'TAURUS_ARTIFACTS_DIR' in os.environ: - return os.environ.get('TAURUS_ARTIFACTS_DIR') - else: - # TODO we have error here if 'results' dir does not exist - results_dir_name = datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S") - pytest_run_results = f'results/{results_dir_name}_local' - os.mkdir(pytest_run_results) - return pytest_run_results # in case you just run pytest +class InitGlobals: + def __init__(self): + self.driver = False + self.driver_title = False + self.login_failed = False + + +class Dataset: + def __init__(self): + self.dataset = dict() + + def jira_dataset(self): + if not self.dataset: + self.dataset["issues"] = self.__read_input_file(JIRA_DATASET_ISSUES) + self.dataset["users"] = self.__read_input_file(JIRA_DATASET_USERS) + self.dataset["jqls"] = self.__read_input_file(JIRA_DATASET_JQLS) + self.dataset["scrum_boards"] = self.__read_input_file(JIRA_DATASET_SCRUM_BOARDS) + self.dataset["kanban_boards"] = self.__read_input_file(JIRA_DATASET_KANBAN_BOARDS) + self.dataset["projects"] = self.__read_input_file(JIRA_DATASET_PROJECTS) + return self.dataset + + def confluence_dataset(self): + if not self.dataset: + self.dataset["pages"] = self.__read_input_file(CONFLUENCE_PAGES) + self.dataset["blogs"] = self.__read_input_file(CONFLUENCE_BLOGS) + self.dataset["users"] = self.__read_input_file(CONFLUENCE_USERS) + return self.dataset + def bitbucket_dataset(self): + if not self.dataset: + self.dataset["projects"] = self.__read_input_file(BITBUCKET_PROJECTS) + self.dataset["users"] = self.__read_input_file(BITBUCKET_USERS) + self.dataset["repos"] = self.__read_input_file(BITBUCKET_REPOS) + self.dataset["pull_requests"] = self.__read_input_file(BITBUCKET_PRS) + return self.dataset -# create selenium output files -current_results_dir = __get_current_results_dir() -selenium_results_file = Path(current_results_dir + '/selenium.jtl') -selenium_error_file = Path(current_results_dir + '/selenium.err') + @staticmethod + def __read_input_file(file_path): + with open(file_path, 'r') as fs: + reader = csv.reader(fs) + return list(reader) + + +globals = InitGlobals() + +selenium_results_file = ENV_TAURUS_ARTIFACT_DIR / 'selenium.jtl' +selenium_error_file = ENV_TAURUS_ARTIFACT_DIR / 'selenium.err' if not selenium_results_file.exists(): with open(selenium_results_file, "w") as file: @@ -51,42 +82,48 @@ def datetime_now(prefix): return prefix + "-" + "".join(symbols) -def print_timing(func): - def wrapper(webdriver, interaction): - start = time.time() - error_msg = 'Success' - full_exception = '' - try: - func(webdriver, interaction) - success = True - except Exception: - success = False - # https://docs.python.org/2/library/sys.html#sys.exc_info - exc_type, full_exception = sys.exc_info()[:2] - error_msg = exc_type.__name__ - end = time.time() - timing = str(int((end - start) * 1000)) +def print_timing(interaction=None): + assert interaction is not None, "Interaction name is not passed to print_timing decorator" - with open(selenium_results_file, "a+") as file: - timestamp = round(time.time() * 1000) - file.write(f"{timestamp},{timing},{interaction},,{error_msg},,{success},0,0,0,0,,0\n") + def deco_wrapper(func): + @functools.wraps(func) + def wrapper(): + if LOGIN_ACTION_NAME in interaction: + globals.login_failed = False + if globals.login_failed: + pytest.skip(f"login is failed") + start = time.time() + error_msg = 'Success' + full_exception = '' + try: + func() + success = True + except Exception: + success = False + # https://docs.python.org/2/library/sys.html#sys.exc_info + exc_type, full_exception = sys.exc_info()[:2] + error_msg = f"Failed measure: {interaction} - {exc_type.__name__}" + end = time.time() + timing = str(int((end - start) * 1000)) + + with open(selenium_results_file, "a+") as jtl_file: + timestamp = round(time.time() * 1000) + jtl_file.write(f"{timestamp},{timing},{interaction},,{error_msg},,{success},0,0,0,0,,0\n") - print(f"{timestamp},{timing},{interaction},{error_msg},{success}") + print(f"{timestamp},{timing},{interaction},{error_msg},{success}") - if not success: - raise Exception(error_msg, full_exception) + if not success: + if LOGIN_ACTION_NAME in interaction: + globals.login_failed = True + raise Exception(error_msg, full_exception) - return wrapper + return wrapper + return deco_wrapper @pytest.fixture(scope="module") def webdriver(): - # TODO consider extract common logic with globals to separate function - global driver - if 'driver' in globals(): - driver = globals()['driver'] - return driver - else: + def driver_init(): chrome_options = Options() if os.getenv('WEBDRIVER_VISIBLE', 'False').lower() != 'true': chrome_options.add_argument("--headless") @@ -95,24 +132,27 @@ def webdriver(): chrome_options.add_argument("--disable-infobars") driver = Chrome(options=chrome_options) return driver - - -# Global instance driver quit -def driver_quit(): - driver.quit() - - -atexit.register(driver_quit) - - -def generate_random_string(length): - return "".join([random.choice(string.digits + string.ascii_letters + ' ') for _ in range(length)]) - - -def generate_jqls(max_length=3, count=100): - # Generate jqls like "abc*" - return ['text ~ "{}*" order by key'.format( - ''.join(random.choices(string.ascii_lowercase, k=random.randrange(1, max_length + 1)))) for _ in range(count)] + # First time driver init + if not globals.driver: + driver = driver_init() + print('first driver inits') + + def driver_quit(): + driver.quit() + globals.driver = driver + atexit.register(driver_quit) + return driver + else: + try: + # check if driver is not broken + globals.driver_title = globals.driver.title + print('get driver from global') + return globals.driver + except WebDriverException: + # re-init driver if it broken + globals.driver = driver_init() + print('reinit driver') + return globals.driver @pytest.hookimpl(tryfirst=True, hookwrapper=True) @@ -152,8 +192,9 @@ def get_screen_shots(request, webdriver, app_settings): with open(selenium_error_file, mode) as err_file: err_file.write(f"Action: {action_name}, Error: {error_text}\n") print(f"Action: {action_name}, Error: {error_text}\n") - os.makedirs(f"{current_results_dir}/errors_artifacts", exist_ok=True) - error_artifact_name = f'{current_results_dir}/errors_artifacts/{datetime_now(action_name)}' + errors_artifacts = ENV_TAURUS_ARTIFACT_DIR / 'errors_artifacts' + errors_artifacts.mkdir(parents=True, exist_ok=True) + error_artifact_name = errors_artifacts / datetime_now(action_name) webdriver.save_screenshot('{}.png'.format(error_artifact_name)) with open(f'{error_artifact_name}.html', 'wb') as html_file: html_file.write(webdriver.page_source.encode('utf-8')) @@ -161,63 +202,19 @@ def get_screen_shots(request, webdriver, app_settings): webdriver.get(app_settings.server_url) -@pytest.fixture(scope="module") -def jira_datasets(): - # TODO consider extract common logic with globals to separate function - global data_sets - if 'data_sets' in globals(): - data_sets = globals()['data_sets'] +application_dataset = Dataset() - return data_sets - else: - data_sets = dict() - data_sets["issues"] = __read_input_file(JIRA_DATASET_ISSUES) - data_sets["users"] = __read_input_file(JIRA_DATASET_USERS) - data_sets["jqls"] = __read_input_file(JIRA_DATASET_JQLS) - data_sets["scrum_boards"] = __read_input_file(JIRA_DATASET_SCRUM_BOARDS) - data_sets["kanban_boards"] = __read_input_file(JIRA_DATASET_KANBAN_BOARDS) - data_sets["project_keys"] = __read_input_file(JIRA_DATASET_PROJECT_KEYS) - return data_sets +@pytest.fixture(scope="module") +def jira_datasets(): + return application_dataset.jira_dataset() @pytest.fixture(scope="module") def confluence_datasets(): - datasets = dict() - datasets["pages"] = __read_input_file(CONFLUENCE_PAGES) - datasets["blogs"] = __read_input_file(CONFLUENCE_BLOGS) - datasets["users"] = __read_input_file(CONFLUENCE_USERS) - return datasets + return application_dataset.confluence_dataset() @pytest.fixture(scope="module") def bitbucket_datasets(): - datasets = dict() - datasets["projects"] = __read_input_file(BITBUCKET_PROJECTS) - datasets["users"] = __read_input_file(BITBUCKET_USERS) - datasets["repos"] = __read_input_file(BITBUCKET_REPOS) - datasets["pull_requests"] = __read_input_file(BITBUCKET_PRS) - return datasets - - -def __read_input_file(file_path): - with open(file_path, 'r') as fs: - reader = csv.reader(fs) - return list(reader) - - -class AnyEc: - """ Use with WebDriverWait to combine expected_conditions - in an OR. - """ - - def __init__(self, *args): - self.ecs = args - - def __call__(self, w_driver): - for fn in self.ecs: - try: - if fn(w_driver): - return True - except: - pass + return application_dataset.bitbucket_dataset() diff --git a/app/selenium_ui/jira/modules.py b/app/selenium_ui/jira/modules.py index e10946b84..8e15b64cd 100644 --- a/app/selenium_ui/jira/modules.py +++ b/app/selenium_ui/jira/modules.py @@ -5,204 +5,222 @@ from selenium_ui.jira.pages.pages import Login, PopupManager, Issue, Project, Search, ProjectsList, \ BoardsList, Board, Dashboard, Logout +KANBAN_BOARDS = "kanban_boards" +SCRUM_BOARDS = "scrum_boards" +USERS = "users" +ISSUES = "issues" +JQLS = "jqls" +PROJECTS = "projects" + def setup_run_data(datasets): page_size = 25 - projects_count = len(datasets['project_keys']) - user = random.choice(datasets["users"]) - issue = random.choice(datasets["issues"]) - scrum_boards = random.choice(datasets["scrum_boards"]) - kanban_boards = random.choice(datasets["kanban_boards"]) - project_key = random.choice(datasets["issues"])[2] + projects_count = len(datasets[PROJECTS]) + user = random.choice(datasets[USERS]) + issue = random.choice(datasets[ISSUES]) + scrum_boards = random.choice(datasets[SCRUM_BOARDS]) + kanban_boards = random.choice(datasets[KANBAN_BOARDS]) + projects = random.choice(datasets[PROJECTS]) datasets['username'] = user[0] datasets['password'] = user[1] datasets['issue_key'] = issue[0] datasets['issue_id'] = issue[1] - datasets['project_key'] = project_key + datasets['project_key'] = projects[0] datasets['scrum_board_id'] = scrum_boards[0] datasets['kanban_board_id'] = kanban_boards[0] - datasets['jql'] = urllib.parse.quote(random.choice(datasets["jqls"][0])) - datasets['pages'] = projects_count // page_size if projects_count % page_size == 0 \ + datasets['jql'] = urllib.parse.quote(random.choice(datasets[JQLS][0])) + datasets['project_pages_count'] = projects_count // page_size if projects_count % page_size == 0 \ else projects_count // page_size + 1 def login(webdriver, datasets): setup_run_data(datasets) - @print_timing - def measure(webdriver, interaction): + + @print_timing("selenium_login") + def measure(): login_page = Login(webdriver) - @print_timing - # TODO do we need this unused argument? Suggest rewriting without using the same function names and inner funcs - def measure(webdriver, interaction): + + @print_timing("selenium_login:open_login_page") + def sub_measure(): login_page.go_to() - measure(webdriver, "selenium_login:open_login_page") + sub_measure() - @print_timing - def measure(webdriver, interaction): + @print_timing("selenium_login:login_and_view_dashboard") + def sub_measure(): login_page.set_credentials(username=datasets['username'], password=datasets['password']) if login_page.is_first_login(): - login_page.first_login_setup(interaction) - login_page.wait_for_page_loaded(interaction) - measure(webdriver, "selenium_login:login_and_view_dashboard") - measure(webdriver, "selenium_login") + login_page.first_login_setup() + login_page.wait_for_page_loaded() + sub_measure() + measure() PopupManager(webdriver).dismiss_default_popup() def view_issue(webdriver, datasets): issue_page = Issue(webdriver, issue_key=datasets['issue_key']) - @print_timing - def measure(webdriver, interaction): - issue_page.go_to() - issue_page.wait_for_page_loaded(interaction) - measure(webdriver, "selenium_view_issue") - -def create_issue(webdriver, datasets): - issue_modal = Issue(webdriver) - @print_timing - def measure(webdriver, interaction): - @print_timing - def measure(webdriver, interaction): - issue_modal.open_create_issue_modal(interaction) - measure(webdriver, "selenium_create_issue:open_quick_create") - - @print_timing - def measure(webdriver, interaction): - issue_modal.fill_summary_create(interaction) # Fill summary field - issue_modal.fill_description_create(interaction) # Fill description field - issue_modal.assign_to_me() # Click assign to me - issue_modal.set_resolution() # Set resolution if there is such field - issue_modal.set_issue_type(interaction) # Set issue type, use non epic type - - @print_timing - def measure(webdriver, interaction): - issue_modal.submit_issue(interaction) - measure(webdriver, "selenium_create_issue:submit_issue_form") - measure(webdriver, "selenium_create_issue:fill_and_submit_issue_form") - measure(webdriver, "selenium_create_issue") - PopupManager(webdriver).dismiss_default_popup() + @print_timing("selenium_view_issue") + def measure(): + issue_page.go_to() + issue_page.wait_for_page_loaded() + measure() def view_project_summary(webdriver, datasets): project_page = Project(webdriver, project_key=datasets['project_key']) - @print_timing - def measure(webdriver, interaction): + @print_timing("selenium_project_summary") + def measure(): project_page.go_to() - project_page.wait_for_page_loaded(interaction) - measure(webdriver, "selenium_project_summary") + project_page.wait_for_page_loaded() + measure() + + +def create_issue(webdriver, dataset): + issue_modal = Issue(webdriver) + + @print_timing("selenium_create_issue") + def measure(): + + @print_timing("selenium_create_issue:open_quick_create") + def sub_measure(): + issue_modal.open_create_issue_modal() + sub_measure() + + @print_timing("selenium_create_issue:fill_and_submit_issue_form") + def sub_measure(): + issue_modal.fill_summary_create() # Fill summary field + issue_modal.fill_description_create() # Fill description field + issue_modal.assign_to_me() # Click assign to me + issue_modal.set_resolution() # Set resolution if there is such field + issue_modal.set_issue_type() # Set issue type, use non epic type + + @print_timing("selenium_create_issue:fill_and_submit_issue_form:submit_issue_form") + def sub_sub_measure(): + issue_modal.submit_issue() + sub_sub_measure() + sub_measure() + measure() + PopupManager(webdriver).dismiss_default_popup() def search_jql(webdriver, datasets): search_page = Search(webdriver, jql=datasets['jql']) - @print_timing - def measure(webdriver, interaction): + @print_timing("selenium_search_jql") + def measure(): search_page.go_to() - search_page.wait_for_page_loaded(interaction) - measure(webdriver, "selenium_search_jql") + search_page.wait_for_page_loaded() + measure() def edit_issue(webdriver, datasets): issue_page = Issue(webdriver, issue_id=datasets['issue_id']) - @print_timing - def measure(webdriver, interaction): - @print_timing - def measure(webdriver, interaction): - issue_page.go_to_edit_issue(interaction) # open editor - measure(webdriver, "selenium_edit_issue:open_edit_issue_form") + @print_timing("selenium_edit_issue") + def measure(): + + @print_timing("selenium_edit_issue:open_edit_issue_form") + def sub_measure(): + issue_page.go_to_edit_issue() # open editor + sub_measure() issue_page.fill_summary_edit() # edit summary - issue_page.fill_description_edit(interaction) # edit description + issue_page.fill_description_edit() # edit description - @print_timing - def measure(webdriver, interaction): + @print_timing("selenium_edit_issue:save_edit_issue_form") + def sub_measure(): issue_page.edit_issue_submit() # submit edit issue - issue_page.wait_for_issue_title(interaction) - measure(webdriver, "selenium_edit_issue:save_edit_issue_form") - measure(webdriver, "selenium_edit_issue") + issue_page.wait_for_issue_title() + sub_measure() + measure() def save_comment(webdriver, datasets): issue_page = Issue(webdriver, issue_id=datasets['issue_id']) - @print_timing - def measure(webdriver, interaction): - @print_timing - def measure(webdriver, interaction): - issue_page.go_to_edit_comment(interaction) # Open edit comment page - measure(webdriver, "selenium_save_comment:open_comment_form") - issue_page.fill_comment_edit(interaction) # Fill comment text field + @print_timing("selenium_save_comment") + def measure(): + + @print_timing("selenium_save_comment:open_comment_form") + def sub_measure(): + issue_page.go_to_edit_comment() # Open edit comment page + sub_measure() - @print_timing - def measure(webdriver, interaction): - issue_page.edit_comment_submit(interaction) # Submit comment - measure(webdriver, "selenium_save_comment:submit_form") - measure(webdriver, "selenium_save_comment") + issue_page.fill_comment_edit() # Fill comment text field + + @print_timing("selenium_save_comment:submit_form") + def sub_measure(): + issue_page.edit_comment_submit() # Submit comment + sub_measure() + measure() def browse_projects_list(webdriver, datasets): - @print_timing - def measure(webdriver, interaction): - projects_list_page = ProjectsList(webdriver, projects_list_pages=datasets['pages']) + @print_timing("selenium_browse_projects_list") + def measure(): + projects_list_page = ProjectsList(webdriver, projects_list_pages=datasets['project_pages_count']) projects_list_page.go_to() - projects_list_page.wait_for_page_loaded(interaction) - measure(webdriver, "selenium_browse_projects_list") + projects_list_page.wait_for_page_loaded() + measure() def browse_boards_list(webdriver, datasets): - @print_timing - def measure(webdriver, interaction): + @print_timing("selenium_browse_boards_list") + def measure(): boards_list_page = BoardsList(webdriver) boards_list_page.go_to() - boards_list_page.wait_for_page_loaded(interaction) - measure(webdriver, "selenium_browse_boards_list") + boards_list_page.wait_for_page_loaded() + measure() PopupManager(webdriver).dismiss_default_popup() def view_backlog_for_scrum_board(webdriver, datasets): scrum_board_page = Board(webdriver, board_id=datasets['scrum_board_id']) - @print_timing - def measure(webdriver, interaction): + + @print_timing("selenium_view_scrum_board_backlog") + def measure(): scrum_board_page.go_to_backlog() - scrum_board_page.wait_for_scrum_board_backlog(interaction) - measure(webdriver, "selenium_view_scrum_board_backlog") + scrum_board_page.wait_for_scrum_board_backlog() + measure() def view_scrum_board(webdriver, datasets): scrum_board_page = Board(webdriver, board_id=datasets['scrum_board_id']) - @print_timing - def measure(webdriver, interaction): + + @print_timing("selenium_view_scrum_board") + def measure(): scrum_board_page.go_to() - scrum_board_page.wait_for_page_loaded(interaction) - measure(webdriver, "selenium_view_scrum_board") + scrum_board_page.wait_for_page_loaded() + measure() def view_kanban_board(webdriver, datasets): kanban_board_page = Board(webdriver, board_id=datasets['kanban_board_id']) - @print_timing - def measure(webdriver, interaction): + + @print_timing("selenium_view_kanban_board") + def measure(): kanban_board_page.go_to() - kanban_board_page.wait_for_page_loaded(interaction) - measure(webdriver, "selenium_view_kanban_board") + kanban_board_page.wait_for_page_loaded() + measure() def view_dashboard(webdriver, datasets): dashboard_page = Dashboard(webdriver) - @print_timing - def measure(webdriver, interaction): + + @print_timing("selenium_view_dashboard") + def measure(): dashboard_page.go_to() - dashboard_page.wait_dashboard_presented(interaction) - measure(webdriver, "selenium_view_dashboard") + dashboard_page.wait_dashboard_presented() + measure() def log_out(webdriver, datasets): logout_page = Logout(webdriver) - @print_timing - def measure(webdriver, interaction): + + @print_timing("selenium_log_out") + def measure(): logout_page.go_to() logout_page.click_logout() - logout_page.wait_for_page_loaded(interaction) - measure(webdriver, "selenium_log_out") - + logout_page.wait_for_page_loaded() + measure() diff --git a/app/selenium_ui/jira/pages/pages.py b/app/selenium_ui/jira/pages/pages.py index b2e6119ca..d001860c6 100644 --- a/app/selenium_ui/jira/pages/pages.py +++ b/app/selenium_ui/jira/pages/pages.py @@ -20,13 +20,13 @@ class Login(BasePage): def is_first_login(self): return True if self.get_elements(LoginPageLocators.continue_button) else False - def first_login_setup(self, interaction): - self.wait_until_visible(LoginPageLocators.continue_button, interaction).send_keys(Keys.ESCAPE) + def first_login_setup(self): + self.wait_until_visible(LoginPageLocators.continue_button).send_keys(Keys.ESCAPE) self.get_element(LoginPageLocators.continue_button).click() - self.wait_until_visible(LoginPageLocators.avatar_page_next_button, interaction).click() - self.wait_until_visible(LoginPageLocators.explore_current_projects, interaction).click() + self.wait_until_visible(LoginPageLocators.avatar_page_next_button).click() + self.wait_until_visible(LoginPageLocators.explore_current_projects).click() self.go_to_url(DashboardLocators.dashboard_url) - self.wait_until_visible(DashboardLocators.dashboard_window, interaction) + self.wait_until_visible(DashboardLocators.dashboard_window) def set_credentials(self, username, password): self.get_element(LoginPageLocators.login_field).send_keys(username) @@ -40,15 +40,15 @@ class Logout(BasePage): def click_logout(self): self.get_element(LogoutLocators.logout_submit_button).click() - def wait_for_page_loaded(self, interaction): - self.wait_until_present(LogoutLocators.login_button_link, interaction) + def wait_for_page_loaded(self): + self.wait_until_present(LogoutLocators.login_button_link) class Dashboard(BasePage): page_url = DashboardLocators.dashboard_url - def wait_dashboard_presented(self, interaction): - self.wait_until_present(DashboardLocators.dashboard_window, interaction) + def wait_dashboard_presented(self): + self.wait_until_present(DashboardLocators.dashboard_window) class Issue(BasePage): @@ -62,44 +62,44 @@ def __init__(self, driver, issue_key=None, issue_id=None): self.page_url_edit_issue = url_manager_edit_page.edit_issue_url() self.page_url_edit_comment = url_manager_edit_page.edit_comments_url() - def wait_for_issue_title(self, interaction): - self.wait_until_visible(IssueLocators.issue_title, interaction) + def wait_for_issue_title(self): + self.wait_until_visible(IssueLocators.issue_title) - def go_to_edit_issue(self, interaction): + def go_to_edit_issue(self): self.go_to_url(self.page_url_edit_issue) - self.wait_until_visible(IssueLocators.edit_issue_page, interaction) + self.wait_until_visible(IssueLocators.edit_issue_page) - def go_to_edit_comment(self, interaction): + def go_to_edit_comment(self): self.go_to_url(self.page_url_edit_comment) - self.wait_until_visible(IssueLocators.edit_comment_add_comment_button, interaction) + self.wait_until_visible(IssueLocators.edit_comment_add_comment_button) def fill_summary_edit(self): text_summary = f"Edit summary form selenium - {self.generate_random_string(10)}" self.get_element(IssueLocators.issue_summary_field).send_keys(text_summary) - def __fill_rich_editor_textfield(self, text, interaction, selector): - self.wait_until_available_to_switch(selector, interaction) + def __fill_rich_editor_textfield(self, text, selector): + self.wait_until_available_to_switch(selector) self.get_element(IssueLocators.tinymce_description_field).send_keys(text) self.return_to_parent_frame() def edit_issue_submit(self): self.get_element(IssueLocators.edit_issue_submit).click() - def fill_description_edit(self, interaction): + def fill_description_edit(self): text_description = f"Edit description form selenium - {self.generate_random_string(30)}" - self.__fill_rich_editor_textfield(text_description, interaction, selector=IssueLocators.issue_description_field) + self.__fill_rich_editor_textfield(text_description, selector=IssueLocators.issue_description_field) - def open_create_issue_modal(self, interaction): - self.wait_until_clickable(IssueLocators.create_issue_button, interaction).click() - self.wait_until_visible(IssueLocators.issue_modal, interaction) + def open_create_issue_modal(self): + self.wait_until_clickable(IssueLocators.create_issue_button).click() + self.wait_until_visible(IssueLocators.issue_modal) - def fill_description_create(self, interaction): + def fill_description_create(self): text_description = f'Description: {self.generate_random_string(100)}' - self.__fill_rich_editor_textfield(text_description, interaction, selector=IssueLocators.issue_description_field) + self.__fill_rich_editor_textfield(text_description, selector=IssueLocators.issue_description_field) - def fill_summary_create(self, interaction): + def fill_summary_create(self): summary = f"Issue created date {time.time()}" - self.wait_until_clickable(IssueLocators.issue_summary_field, interaction).send_keys(summary) + self.wait_until_clickable(IssueLocators.issue_summary_field).send_keys(summary) def assign_to_me(self): assign_to_me_links = self.get_elements(IssueLocators.issue_assign_to_me_link) @@ -109,11 +109,11 @@ def assign_to_me(self): def set_resolution(self): resolution_field = self.get_elements(IssueLocators.issue_resolution_field) if resolution_field: - dropdown_length = len(self.select(resolution_field[0]).options) - random_resolution_id = random.randint(1, dropdown_length - 1) + drop_down_length = len(self.select(resolution_field[0]).options) + random_resolution_id = random.randint(1, drop_down_length - 1) self.select(resolution_field[0]).select_by_index(random_resolution_id) - def set_issue_type(self, interaction): + def set_issue_type(self): def __filer_epic(element): return "epic" not in element.get_attribute("class").lower() @@ -123,17 +123,17 @@ def __filer_epic(element): filtered_issue_elements = list(filter(__filer_epic, issue_dropdown_elements)) rnd_issue_type_el = random.choice(filtered_issue_elements) self.action_chains().move_to_element(rnd_issue_type_el).click(rnd_issue_type_el).perform() - self.wait_until_invisible(IssueLocators.issue_ready_to_save_spinner, interaction) + self.wait_until_invisible(IssueLocators.issue_ready_to_save_spinner) - def submit_issue(self, interaction): - self.wait_until_clickable(IssueLocators.issue_submit_button, interaction).click() + def submit_issue(self): + self.wait_until_clickable(IssueLocators.issue_submit_button).click() self.wait_until_invisible(IssueLocators.issue_modal) - def fill_comment_edit(self, interaction): + def fill_comment_edit(self): text = 'Comment from selenium' - self.__fill_rich_editor_textfield(text, interaction, selector=IssueLocators.edit_comment_text_field) + self.__fill_rich_editor_textfield(text, selector=IssueLocators.edit_comment_text_field) - def edit_comment_submit(self, interaction): + def edit_comment_submit(self): self.get_element(IssueLocators.edit_comment_add_comment_button).click() self.wait_until_visible(IssueLocators.issue_title) @@ -155,10 +155,9 @@ def __init__(self, driver, projects_list_pages): url_manager = UrlManager(projects_list_page=self.projects_list_page) self.page_url = url_manager.projects_list_page_url() - def wait_for_page_loaded(self, interaction): - self.wait_until_any_ec_presented(selector_names=[ProjectLocators.projects_list, - ProjectLocators.projects_not_found], - interaction=interaction) + def wait_for_page_loaded(self): + self.wait_until_any_ec_presented( + selector_names=[ProjectLocators.projects_list, ProjectLocators.projects_not_found]) class BoardsList(BasePage): @@ -173,11 +172,10 @@ def __init__(self, driver, jql): url_manager = UrlManager(jql=jql) self.page_url = url_manager.jql_search_url() - def wait_for_page_loaded(self, interaction): + def wait_for_page_loaded(self): self.wait_until_any_ec_presented(selector_names=[SearchLocators.search_issue_table, SearchLocators.search_issue_content, - SearchLocators.search_no_issue_found], - interaction=interaction) + SearchLocators.search_no_issue_found]) class Board(BasePage): @@ -192,5 +190,5 @@ def __init__(self, driver, board_id): def go_to_backlog(self): self.go_to_url(self.backlog_url) - def wait_for_scrum_board_backlog(self, interaction): - self.wait_until_present(BoardLocators.scrum_board_backlog_content, interaction) + def wait_for_scrum_board_backlog(self): + self.wait_until_present(BoardLocators.scrum_board_backlog_content) diff --git a/app/selenium_ui/jira/pages/selectors.py b/app/selenium_ui/jira/pages/selectors.py index 1247da305..fe62350be 100644 --- a/app/selenium_ui/jira/pages/selectors.py +++ b/app/selenium_ui/jira/pages/selectors.py @@ -146,4 +146,4 @@ class BoardsListLocators: class BoardLocators: # Scrum boards scrum_board_backlog_content = (By.CSS_SELECTOR, "#ghx-backlog[data-rendered]:not(.browser-metrics-stale)") - board_columns = (By.CSS_SELECTOR, ".ghx-column") \ No newline at end of file + board_columns = (By.CSS_SELECTOR, ".ghx-column") diff --git a/app/selenium_ui/jira_ui.py b/app/selenium_ui/jira_ui.py index 9e8772088..7deb4c199 100644 --- a/app/selenium_ui/jira_ui.py +++ b/app/selenium_ui/jira_ui.py @@ -1,5 +1,5 @@ from selenium_ui.jira import modules -from extension.jira import extension_ui +from extension.jira import extension_ui # noqa F401 # this action should be the first one @@ -57,11 +57,11 @@ def test_1_selenium_view_project_summary(webdriver, jira_datasets, jira_screen_s """ Add custom actions anywhere between login and log out action. Move this to a different line as needed. -Write your custom selenium scripts in `app/extension/jira/extension_ui.py`. +Write your custom selenium scripts in `app/extension/jira/extension_ui.py`. Refer to `app/selenium_ui/jira/modules.py` for examples. """ # def test_1_selenium_custom_action(webdriver, jira_datasets, jira_screen_shots): -# extension_ui.custom_action(webdriver, jira_datasets) +# extension_ui.app_specific_action(webdriver, jira_datasets) # this action should be the last one diff --git a/app/util/analytics.py b/app/util/analytics.py deleted file mode 100644 index 6f50fff7e..000000000 --- a/app/util/analytics.py +++ /dev/null @@ -1,398 +0,0 @@ -import sys -import os -import re -import requests -from datetime import datetime, timezone -import platform -import uuid -import getpass -import socket -import hashlib - -from util.conf import JIRA_SETTINGS, CONFLUENCE_SETTINGS, BITBUCKET_SETTINGS, TOOLKIT_VERSION -from util.data_preparation.api.jira_clients import JiraRestClient -from util.data_preparation.api.confluence_clients import ConfluenceRestClient -from util.data_preparation.api.bitbucket_clients import BitbucketRestClient - -JIRA = 'jira' -CONFLUENCE = 'confluence' -BITBUCKET = 'bitbucket' - -MIN_DEFAULTS = {JIRA: {'test_duration': 2700, 'concurrency': 200}, - CONFLUENCE: {'test_duration': 2700, 'concurrency': 200}, - BITBUCKET: {'test_duration': 3000, 'concurrency': 20, 'git_operations_per_hour': 14400} - } - -# List in value in case of specific output appears for some OS for command platform.system() -OS = {'macOS': ['Darwin'], 'Windows': ['Windows'], 'Linux': ['Linux']} -DT_REGEX = r'(\d{4}-\d{1,2}-\d{1,2}\s+\d{1,2}:\d{1,2}:\d{1,2})' -SUCCESS_TEST_RATE_REGX = r'(\d{1,3}.\d{1,2}%)' -JMETER_TEST_REGX = r'jmeter_\S*' -SELENIUM_TEST_REGX = r'selenium_\S*' -BASE_URL = 'https://s7hdm2mnj1.execute-api.us-east-2.amazonaws.com/default/analytics_collector' -SUCCESS_TEST_RATE = 95.00 -RESULTS_CSV = 'results.csv' -BZT_LOG = 'bzt.log' -LABEL_HEADER = 'Label' -LABEL_HEADER_INDEX = 0 -SAMPLES_HEADER = '# Samples' -SAMPLES_HEADER_INDEX = 1 -GIT_OPERATIONS = ['jmeter_clone_repo_via_http', 'jmeter_clone_repo_via_ssh', - 'jmeter_git_push_via_http', 'jmeter_git_fetch_via_http', - 'jmeter_git_push_via_ssh', 'jmeter_git_fetch_via_ssh'] - -APP_TYPE_MSG = ('ERROR: Please run util/analytics.py with application type as argument. ' - 'E.g. python util/analytics.py jira') - - -def __validate_app_type(): - try: - app_type = sys.argv[1] - if app_type.lower() not in [JIRA, CONFLUENCE, BITBUCKET]: - raise SystemExit(APP_TYPE_MSG) - except IndexError: - SystemExit(APP_TYPE_MSG) - - -def get_application_type(): - __validate_app_type() - return sys.argv[1] - - -class AnalyticsCollector: - - def __init__(self, application_type): - self.application_type = application_type - self.run_id = str(uuid.uuid1()) - self.tool_version = "" - self.os = "" - self.duration = 0 - self.concurrency = 0 - self.actual_duration = 0 - self.selenium_test_rates = dict() - self.jmeter_test_rates = dict() - self.time_stamp = "" - self.date = "" - self.application_version = "" - self.summary = [] - - @property - def config_yml(self): - if self.application_type.lower() == JIRA: - return JIRA_SETTINGS - if self.application_type.lower() == CONFLUENCE: - return CONFLUENCE_SETTINGS - if self.application_type.lower() == BITBUCKET: - return BITBUCKET_SETTINGS - - @property - def _log_dir(self): - if 'TAURUS_ARTIFACTS_DIR' in os.environ: - return os.environ.get('TAURUS_ARTIFACTS_DIR') - else: - raise SystemExit('ERROR: Taurus result directory could not be found') - - @property - def bzt_log_file(self): - with open(f'{self._log_dir}/{BZT_LOG}') as log_file: - log_file = log_file.readlines() - return log_file - - @staticmethod - def get_os(): - os_type = platform.system() - for key, value in OS.items(): - os_type = key if os_type in value else os_type - return os_type - - def is_analytics_enabled(self): - return str(self.config_yml.analytics_collector).lower() in ['yes', 'true', 'y'] - - def __validate_bzt_log_not_empty(self): - if len(self.bzt_log_file) == 0: - raise SystemExit(f'ERROR: {BZT_LOG} file in {self._log_dir} is empty') - - def get_duration_by_start_finish_strings(self): - first_string = self.bzt_log_file[0] - last_string = self.bzt_log_file[-1] - start_time = re.findall(DT_REGEX, first_string)[0] - start_datetime_obj = datetime.strptime(start_time, '%Y-%m-%d %H:%M:%S') - finish_time = re.findall(DT_REGEX, last_string)[0] - finish_datetime_obj = datetime.strptime(finish_time, '%Y-%m-%d %H:%M:%S') - duration = finish_datetime_obj - start_datetime_obj - return duration.seconds - - def get_duration_by_test_duration(self): - test_duration = None - for string in self.bzt_log_file: - if 'Test duration' in string: - str_duration = string.split('duration:')[1].replace('\n', '') - str_duration = str_duration.replace(' ', '') - duration_datetime_obj = datetime.strptime(str_duration, '%H:%M:%S') - test_duration = (duration_datetime_obj.hour*3600 + - duration_datetime_obj.minute*60 + duration_datetime_obj.second) - break - return test_duration - - def get_actual_run_time(self): - self.__validate_bzt_log_not_empty() - run_time_bzt = self.get_duration_by_test_duration() - run_time_start_finish = self.get_duration_by_start_finish_strings() - return run_time_bzt if run_time_bzt else run_time_start_finish - - @staticmethod - def get_test_count_by_type(tests_type, log): - trigger = f' {tests_type}_' - test_search_regx = "" - if tests_type == 'jmeter': - test_search_regx = JMETER_TEST_REGX - elif tests_type == 'selenium': - test_search_regx = SELENIUM_TEST_REGX - tests = {} - for line in log: - if trigger in line and ('FAIL' in line or 'OK' in line): - test_name = re.findall(test_search_regx, line)[0] - test_rate = float(''.join(re.findall(SUCCESS_TEST_RATE_REGX, line))[:-1]) - if test_name not in tests: - tests[test_name] = test_rate - return tests - - def set_actual_test_count(self): - test_result_string_trigger = 'Request label stats:' - res_string_idx = [index for index, value in enumerate(self.bzt_log_file) if test_result_string_trigger in value] - # Cut bzt.log from the 'Request label stats:' string to the end - if res_string_idx: - res_string_idx = res_string_idx[0] - results_bzt_run = self.bzt_log_file[res_string_idx:] - - self.selenium_test_rates = self.get_test_count_by_type(tests_type='selenium', log=results_bzt_run) - self.jmeter_test_rates = self.get_test_count_by_type(tests_type='jmeter', log=results_bzt_run) - - @staticmethod - def __convert_to_sec(duration): - seconds_per_unit = {"s": 1, "m": 60, "h": 3600, "d": 86400, "w": 604800} - duration = str(duration) - numbers = ''.join(filter(str.isdigit, duration)) - units = ''.join(filter(str.isalpha, duration)) - return int(numbers) * seconds_per_unit[units] if units in seconds_per_unit else int(numbers) - - def set_date_timestamp(self): - utc_now = datetime.utcnow() - self.time_stamp = int(round(utc_now.timestamp() * 1000)) - self.date = datetime.utcnow().replace(tzinfo=timezone.utc).isoformat('T', 'seconds') - - def get_jira_version(self): - client = JiraRestClient(host=self.config_yml.server_url, user=self.config_yml.admin_login, - password=self.config_yml.admin_password) - jira_server_info = client.get_server_info() - jira_server_version = jira_server_info.get('version', '') - return jira_server_version - - def get_confluence_version(self): - client = ConfluenceRestClient(host=self.config_yml.server_url, user=self.config_yml.admin_login, - password=self.config_yml.admin_password) - return client.get_confluence_version() - - def get_bitbucket_version(self): - client = BitbucketRestClient(host=self.config_yml.server_url, user=self.config_yml.admin_login, - password=self.config_yml.admin_password) - return client.get_bitbucket_version() - - def get_application_version(self): - if self.application_type.lower() == JIRA: - return self.get_jira_version() - if self.application_type.lower() == CONFLUENCE: - return self.get_confluence_version() - if self.application_type.lower() == BITBUCKET: - return self.get_bitbucket_version() - - @property - def uniq_user_id(self): - user_info = str(platform.node()) + str(getpass.getuser()) + str(socket.gethostname()) - uid = hashlib.pbkdf2_hmac('sha256', user_info.encode('utf-8'), - b"DCAPT Centaurus", - 100000).hex() - return uid - - def generate_analytics(self): - self.concurrency = self.config_yml.concurrency - self.duration = self.__convert_to_sec(self.config_yml.duration) - self.os = self.get_os() - self.actual_duration = self.get_actual_run_time() - self.tool_version = TOOLKIT_VERSION - self.set_actual_test_count() - self.set_date_timestamp() - self.application_version = self.get_application_version() - - @property - def actual_git_operations_count(self): - count = 0 - - if self.application_type != BITBUCKET: - raise Exception(f'ERROR: {self.application_type} is not {BITBUCKET}') - results_csv_file_path = f'{self._log_dir}/results.csv' - if not os.path.exists(results_csv_file_path): - raise SystemExit(f'ERROR: {results_csv_file_path} was not found.') - with open(results_csv_file_path) as res_file: - header = res_file.readline() - results = res_file.readlines() - - headers_list = header.split(',') - if headers_list[LABEL_HEADER_INDEX] != LABEL_HEADER: - raise SystemExit(f'ERROR: {results_csv_file_path} has unexpected header. ' - f'Actual: {headers_list[LABEL_HEADER_INDEX]}, Expected: {LABEL_HEADER}') - if headers_list[SAMPLES_HEADER_INDEX] != SAMPLES_HEADER: - raise SystemExit(f'ERROR: {results_csv_file_path} has unexpected header. ' - f'Actual: {headers_list[SAMPLES_HEADER_INDEX]}, Expected: {SAMPLES_HEADER}') - - for line in results: - if any(s in line for s in GIT_OPERATIONS): - count = count + int(line.split(',')[SAMPLES_HEADER_INDEX]) - - return count - - @staticmethod - def is_all_tests_successful(tests): - for success_rate in tests.values(): - if success_rate < SUCCESS_TEST_RATE: - return False - return True - - def __is_success(self): - message = 'OK' - if not self.jmeter_test_rates: - return False, f"JMeter test results was not found." - if not self.selenium_test_rates: - return False, f"Selenium test results was not found." - - success = (self.is_all_tests_successful(self.jmeter_test_rates) and - self.is_all_tests_successful(self.selenium_test_rates)) - - if not success: - message = f"One or more actions have success rate < {SUCCESS_TEST_RATE} %." - return success, message - - def __is_finished(self): - message = 'OK' - finished = self.actual_duration >= self.duration - if not finished: - message = (f"Actual test duration {self.actual_duration} sec " - f"< than expected test_duration {self.duration} sec.") - return finished, message - - def __is_compliant(self): - message = 'OK' - compliant = (self.actual_duration >= MIN_DEFAULTS[self.application_type]['test_duration'] and - self.concurrency >= MIN_DEFAULTS[self.application_type]['concurrency']) - if not compliant: - err_msg = [] - if self.actual_duration < MIN_DEFAULTS[self.application_type]['test_duration']: - err_msg.append(f"Test run duration {self.actual_duration} sec < than minimum test " - f"duration {MIN_DEFAULTS[self.application_type]['test_duration']} sec.") - if self.concurrency < MIN_DEFAULTS[self.application_type]['concurrency']: - err_msg.append(f"Test run concurrency {self.concurrency} < than minimum test " - f"concurrency {MIN_DEFAULTS[self.application_type]['concurrency']}.") - message = ' '.join(err_msg) - return compliant, message - - def __is_git_operations_compliant(self): - # calculate expected git operations for a particular test duration - message = 'OK' - expected_get_operations_count = int(MIN_DEFAULTS[BITBUCKET]['git_operations_per_hour'] / 3600 * self.duration) - git_operations_compliant = self.actual_git_operations_count >= expected_get_operations_count - if not git_operations_compliant: - message = (f"Total git operations {self.actual_git_operations_count} < than " - f"{expected_get_operations_count} - minimum for expected duration {self.duration} sec.") - return git_operations_compliant, message - - def generate_report_summary(self): - summary_report = [] - summary_report_file = f'{self._log_dir}/results_summary.log' - - finished = self.__is_finished() - compliant = self.__is_compliant() - success = self.__is_success() - - overall_status = 'OK' if finished[0] and success[0] and compliant[0] else 'FAIL' - - if self.application_type == BITBUCKET: - git_compliant = self.__is_git_operations_compliant() - overall_status = 'OK' if finished[0] and success[0] and compliant[0] and git_compliant[0] else 'FAIL' - - summary_report.append(f'Summary run status|{overall_status}\n') - summary_report.append(f'Artifacts dir|{os.path.basename(self._log_dir)}') - summary_report.append(f'OS|{self.os}') - summary_report.append(f'DC Apps Performance Toolkit version|{self.tool_version}') - summary_report.append(f'Application|{self.application_type} {self.application_version}') - summary_report.append(f'Concurrency|{self.concurrency}') - summary_report.append(f'Expected test run duration from yml file|{self.duration} sec') - summary_report.append(f'Actual test run duration|{self.actual_duration} sec') - - if self.application_type == BITBUCKET: - summary_report.append(f'Total Git operations count|{self.actual_git_operations_count}') - summary_report.append(f'Total Git operations compliant|{git_compliant}') - - summary_report.append(f'Finished|{finished}') - summary_report.append(f'Compliant|{compliant}') - summary_report.append(f'Success|{success}\n') - - summary_report.append(f'Action|Success Rate|Status') - - for key, value in {**self.jmeter_test_rates, **self.selenium_test_rates}.items(): - status = 'OK' if value >= SUCCESS_TEST_RATE else 'Fail' - summary_report.append(f'{key}|{value}|{status}') - - pretty_report = map(self.format_string, summary_report) - - self.__write_to_file(pretty_report, summary_report_file) - - @staticmethod - def __write_to_file(content, file): - with open(file, 'w') as f: - f.writelines(content) - - @staticmethod - def format_string(string_to_format, offset=50): - # format string with delimiter "|" - return ''.join([f'{item}{" "*(offset-len(str(item)))}' for item in string_to_format.split("|")]) + "\n" - - -class AnalyticsSender: - - def __init__(self, analytics_instance): - self.analytics = analytics_instance - - def send_request(self): - headers = {"Content-Type": "application/json"} - payload = {"run_id": self.analytics.run_id, - "user_id": self.analytics.uniq_user_id, - "app_version": self.analytics.application_version, - "date": self.analytics.date, - "time_stamp": self.analytics.time_stamp, - "app_type": self.analytics.application_type, - "os": self.analytics.os, - "tool_ver": self.analytics.tool_version, - "exp_dur": self.analytics.duration, - "act_dur": self.analytics.actual_duration, - "selenium_test_rates": self.analytics.selenium_test_rates, - "jmeter_test_rates": self.analytics.jmeter_test_rates, - "concurrency": self.analytics.concurrency - } - r = requests.post(url=f'{BASE_URL}', json=payload, headers=headers) - print(r.json()) - if r.status_code != 200: - print(f'Analytics data was send unsuccessfully, status code {r.status_code}') - - -def main(): - app_type = get_application_type() - collector = AnalyticsCollector(app_type) - collector.generate_analytics() - collector.generate_report_summary() - if collector.is_analytics_enabled(): - sender = AnalyticsSender(collector) - sender.send_request() - - -if __name__ == '__main__': - main() diff --git a/app/util/analytics/analytics.py b/app/util/analytics/analytics.py new file mode 100644 index 000000000..8cf69bce5 --- /dev/null +++ b/app/util/analytics/analytics.py @@ -0,0 +1,144 @@ +import sys +import requests +import uuid +from datetime import datetime, timezone + +from util.analytics.application_info import ApplicationSelector, BaseApplication +from util.analytics.log_reader import BztFileReader, ResultsFileReader +from util.conf import TOOLKIT_VERSION +from util.analytics.analytics_utils import get_os, convert_to_sec, get_timestamp, get_date, is_all_tests_successful, \ + uniq_user_id, generate_report_summary, get_first_elem + +JIRA = 'jira' +CONFLUENCE = 'confluence' +BITBUCKET = 'bitbucket' + +MIN_DEFAULTS = {JIRA: {'test_duration': 2700, 'concurrency': 200}, + CONFLUENCE: {'test_duration': 2700, 'concurrency': 200}, + BITBUCKET: {'test_duration': 3000, 'concurrency': 20, 'git_operations_per_hour': 14400} + } + +BASE_URL = 'https://s7hdm2mnj1.execute-api.us-east-2.amazonaws.com/default/analytics_collector' +SUCCESS_TEST_RATE = 95.00 + + +class AnalyticsCollector: + + def __init__(self, application: BaseApplication): + bzt_log = BztFileReader() + + self.log_dir = bzt_log.log_dir + self.conf = application.config + self.app_type = application.type + self.results_log = ResultsFileReader() + self.run_id = str(uuid.uuid1()) + self.tool_version = TOOLKIT_VERSION + self.os = get_os() + self.duration = convert_to_sec(self.conf.duration) + self.concurrency = self.conf.concurrency + self.actual_duration = bzt_log.actual_run_time + self.selenium_test_rates = bzt_log.selenium_test_rates + self.jmeter_test_rates = bzt_log.jmeter_test_rates if self.conf.load_executor == 'jmeter' else dict() + self.locust_test_rates = bzt_log.locust_test_rates if self.conf.load_executor == 'locust' else dict() + self.time_stamp = get_timestamp() + self.date = get_date() + self.application_version = application.version + self.nodes_count = application.nodes_count + self.dataset_information = application.dataset_information + + def is_analytics_enabled(self): + return str(self.conf.analytics_collector).lower() in ['yes', 'true', 'y'] + + def set_date_timestamp(self): + utc_now = datetime.utcnow() + self.time_stamp = int(round(utc_now.timestamp() * 1000)) + self.date = datetime.utcnow().replace(tzinfo=timezone.utc).isoformat('T', 'seconds') + + def is_success(self): + message = 'OK' + load_test_rates = dict() + if self.conf.load_executor == 'jmeter': + load_test_rates = self.jmeter_test_rates + elif self.conf.load_executor == 'locust': + load_test_rates = self.locust_test_rates + if not load_test_rates: + return False, f"Jmeter/Locust test results was not found." + + if not self.selenium_test_rates: + return False, f"Selenium test results was not found." + + success = (is_all_tests_successful(load_test_rates) and + is_all_tests_successful(self.selenium_test_rates)) + + if not success: + message = f"One or more actions have success rate < {SUCCESS_TEST_RATE} %." + return success, message + + def is_finished(self): + message = 'OK' + finished = self.actual_duration >= self.duration + if not finished: + message = (f"Actual test duration {self.actual_duration} sec " + f"< than expected test_duration {self.duration} sec.") + return finished, message + + def is_compliant(self): + message = 'OK' + compliant = (self.actual_duration >= MIN_DEFAULTS[self.app_type]['test_duration'] and + self.concurrency >= MIN_DEFAULTS[self.app_type]['concurrency']) + if not compliant: + err_msg = [] + if self.actual_duration < MIN_DEFAULTS[self.app_type]['test_duration']: + err_msg.append(f"Test run duration {self.actual_duration} sec < than minimum test " + f"duration {MIN_DEFAULTS[self.app_type]['test_duration']} sec.") + if self.concurrency < MIN_DEFAULTS[self.app_type]['concurrency']: + err_msg.append(f"Test run concurrency {self.concurrency} < than minimum test " + f"concurrency {MIN_DEFAULTS[self.app_type]['concurrency']}.") + message = ' '.join(err_msg) + return compliant, message + + def is_git_operations_compliant(self): + # calculate expected git operations for a particular test duration + message = 'OK' + expected_get_operations_count = int(MIN_DEFAULTS[BITBUCKET]['git_operations_per_hour'] / 3600 * self.duration) + git_operations_compliant = self.results_log.actual_git_operations_count >= expected_get_operations_count + if not git_operations_compliant: + message = (f"Total git operations {self.results_log.actual_git_operations_count} < than " + f"{expected_get_operations_count} - minimum for expected duration {self.duration} sec.") + return git_operations_compliant, message + + +def send_analytics(collector: AnalyticsCollector): + headers = {"Content-Type": "application/json"} + payload = {"run_id": collector.run_id, + "user_id": uniq_user_id(collector.conf.server_url), + "app_version": collector.application_version, + "date": collector.date, + "time_stamp": collector.time_stamp, + "app_type": collector.app_type, + "os": collector.os, + "tool_ver": collector.tool_version, + "exp_dur": collector.duration, + "act_dur": collector.actual_duration, + "selenium_test_rates": collector.selenium_test_rates, + "jmeter_test_rates": collector.jmeter_test_rates, + "locust_test_rates": collector.locust_test_rates, + "concurrency": collector.concurrency + } + r = requests.post(url=f'{BASE_URL}', json=payload, headers=headers) + print(r.json()) + if r.status_code != 200: + print(f'Analytics data was send unsuccessfully, status code {r.status_code}') + + +def main(): + application_name = get_first_elem(sys.argv) + application = ApplicationSelector(application_name).application + collector = AnalyticsCollector(application) + generate_report_summary(collector) + if collector.is_analytics_enabled(): + send_analytics(collector) + + +if __name__ == '__main__': + main() diff --git a/app/util/analytics/analytics_utils.py b/app/util/analytics/analytics_utils.py new file mode 100644 index 000000000..8b90f303b --- /dev/null +++ b/app/util/analytics/analytics_utils.py @@ -0,0 +1,130 @@ +import os +import platform +import hashlib +import getpass +import socket +from datetime import datetime, timezone + +SUCCESS_TEST_RATE = 95.00 +OS = {'macOS': ['Darwin'], 'Windows': ['Windows'], 'Linux': ['Linux']} + + +def is_docker(): + path = '/proc/self/cgroup' + return ( + os.path.exists('/.dockerenv') or + os.path.isfile(path) and any('docker' in line for line in open(path)) + ) + + +def format_string_summary_report(string_to_format, offset=50): + # format string with delimiter "|" + return ''.join([f'{item}{" " * (offset - len(str(item)))}' for item in string_to_format.split("|")]) + "\n" + + +def write_to_file(content, file): + with open(file, 'w') as f: + f.writelines(content) + + +def generate_report_summary(collector): + bitbucket = 'bitbucket' + git_compliant = None + + summary_report = [] + summary_report_file = f'{collector.log_dir}/results_summary.log' + + finished = collector.is_finished() + compliant = collector.is_compliant() + success = collector.is_success() + + overall_status = 'OK' if finished[0] and success[0] and compliant[0] else 'FAIL' + + if collector.app_type == bitbucket: + git_compliant = collector.is_git_operations_compliant() + overall_status = 'OK' if finished[0] and success[0] and compliant[0] and git_compliant[0] else 'FAIL' + + summary_report.append(f'Summary run status|{overall_status}\n') + summary_report.append(f'Artifacts dir|{os.path.basename(collector.log_dir)}') + summary_report.append(f'OS|{collector.os}') + summary_report.append(f'DC Apps Performance Toolkit version|{collector.tool_version}') + summary_report.append(f'Application|{collector.app_type} {collector.application_version}') + summary_report.append(f'Dataset info|{collector.dataset_information}') + summary_report.append(f'Application nodes count|{collector.nodes_count}') + summary_report.append(f'Concurrency|{collector.concurrency}') + summary_report.append(f'Expected test run duration from yml file|{collector.duration} sec') + summary_report.append(f'Actual test run duration|{collector.actual_duration} sec') + + if collector.app_type == bitbucket: + total_git_count = collector.results_log.actual_git_operations_count + summary_report.append(f'Total Git operations count|{total_git_count}') + summary_report.append(f'Total Git operations compliant|{git_compliant}') + + summary_report.append(f'Finished|{finished}') + summary_report.append(f'Compliant|{compliant}') + summary_report.append(f'Success|{success}\n') + + summary_report.append(f'Action|Success Rate|Status') + load_test_rates = {} + if collector.conf.load_executor == 'jmeter': + load_test_rates = collector.jmeter_test_rates + elif collector.conf.load_executor == 'locust': + load_test_rates = collector.locust_test_rates + + for key, value in {**load_test_rates, **collector.selenium_test_rates}.items(): + status = 'OK' if value >= SUCCESS_TEST_RATE else 'Fail' + summary_report.append(f'{key}|{value}|{status}') + + pretty_report = map(format_string_summary_report, summary_report) + write_to_file(pretty_report, summary_report_file) + + +def get_os(): + os_type = platform.system() + for key, value in OS.items(): + os_type = key if os_type in value else os_type + return os_type + + +def uniq_user_id(server_url: str): + if is_docker(): + user_info = server_url + else: + user_info = str(platform.node()) + str(getpass.getuser()) + str(socket.gethostname()) + uid = hashlib.pbkdf2_hmac('sha256', user_info.encode('utf-8'), + b"DCAPT Centaurus", + 100000).hex() + return uid + + +def convert_to_sec(duration): + seconds_per_unit = {"s": 1, "m": 60, "h": 3600, "d": 86400, "w": 604800} + duration = str(duration) + numbers = ''.join(filter(str.isdigit, duration)) + units = ''.join(filter(str.isalpha, duration)) + return int(numbers) * seconds_per_unit[units] if units in seconds_per_unit else int(numbers) + + +def is_all_tests_successful(tests: dict): + for success_rate in tests.values(): + if success_rate < SUCCESS_TEST_RATE: + return False + return True + + +def get_first_elem(elems: list): + try: + return elems[1] + except IndexError: + raise Exception('analytics.py expects application name as argument') + + +def get_date(): + date = datetime.utcnow().replace(tzinfo=timezone.utc).isoformat('T', 'seconds') + return date + + +def get_timestamp(): + utc_now = datetime.utcnow() + time_stamp = int(round(utc_now.timestamp() * 1000)) + return time_stamp diff --git a/app/util/analytics/application_info.py b/app/util/analytics/application_info.py new file mode 100644 index 000000000..7db18fca4 --- /dev/null +++ b/app/util/analytics/application_info.py @@ -0,0 +1,111 @@ +from util.conf import JIRA_SETTINGS, CONFLUENCE_SETTINGS, BITBUCKET_SETTINGS +from util.api.jira_clients import JiraRestClient +from util.api.confluence_clients import ConfluenceRestClient +from util.api.bitbucket_clients import BitbucketRestClient +from lxml import etree + +JIRA = 'jira' +CONFLUENCE = 'confluence' +BITBUCKET = 'bitbucket' + + +class BaseApplication: + type = None + version = None + nodes_count = None + dataset_information = None + + def __init__(self, api_client, config_yml): + self.client = api_client(host=config_yml.server_url, + user=config_yml.admin_login, password=config_yml.admin_password) + self.config = config_yml + + +class Jira(BaseApplication): + type = JIRA + + @property + def version(self): + jira_server_info = self.client.get_server_info() + jira_server_version = jira_server_info.get('version', '') + return jira_server_version + + @property + def nodes_count(self): + html_pattern = '<td><strong>Nodestate:</strong></td><td>Active</td>' + if self.version >= '8.1.0': + return len(self.client.get_nodes_info_via_rest()) + else: + jira_system_page = self.client.get_system_info_page() + node_count = jira_system_page.replace(' ', '').replace('\n', '').count(html_pattern) + return node_count + + def __issues_count(self): + return self.client.get_total_issues_count() + + @property + def dataset_information(self): + return f"{self.__issues_count()} issues" + + +class Confluence(BaseApplication): + type = CONFLUENCE + + @property + def version(self): + return self.client.get_confluence_version() + + @property + def nodes_count(self): + return len(self.client.get_confluence_nodes_count()) + + @property + def dataset_information(self): + return f"{self.client.get_total_pages_count()} pages" + + +class Bitbucket(BaseApplication): + type = BITBUCKET + bitbucket_repos_selector = "#content-bitbucket\.atst\.repositories-0>.field-group>.field-value" # noqa W605 + + @property + def version(self): + return self.client.get_bitbucket_version() + + @property + def nodes_count(self): + cluster_page = self.client.get_bitbucket_cluster_page() + nodes_count = cluster_page.count('class="cluster-node-id" headers="cluster-node-id"') + return nodes_count + + @property + def dataset_information(self): + system_page_html = self.client.get_bitbucket_system_page() + if 'Repositories' in system_page_html: + dom = etree.HTML(system_page_html) + repos_count = dom.cssselect(self.bitbucket_repos_selector)[0].text + return f'{repos_count} repositories' + else: + return 'Could not parse number of Bitbucket repositories' + + +class ApplicationSelector: + APP_TYPE_MSG = ('ERROR: Please run util/analytics.py with application type as argument. ' + 'E.g. python util/analytics.py jira') + + def __init__(self, app_name): + self.application_type = self.__get_application_type(app_name) + + def __get_application_type(self, app_name): + if app_name.lower() not in [JIRA, CONFLUENCE, BITBUCKET]: + raise SystemExit(self.APP_TYPE_MSG) + return app_name.lower() + + @property + def application(self): + if self.application_type == JIRA: + return Jira(api_client=JiraRestClient, config_yml=JIRA_SETTINGS) + if self.application_type == CONFLUENCE: + return Confluence(api_client=ConfluenceRestClient, config_yml=CONFLUENCE_SETTINGS) + if self.application_type == BITBUCKET: + return Bitbucket(api_client=BitbucketRestClient, config_yml=BITBUCKET_SETTINGS) diff --git a/app/util/analytics/log_reader.py b/app/util/analytics/log_reader.py new file mode 100644 index 000000000..5597dfc0f --- /dev/null +++ b/app/util/analytics/log_reader.py @@ -0,0 +1,146 @@ +import os +import re +from datetime import datetime +from util.project_paths import ENV_TAURUS_ARTIFACT_DIR + +GIT_OPERATIONS = ['jmeter_clone_repo_via_http', 'jmeter_clone_repo_via_ssh', + 'jmeter_git_push_via_http', 'jmeter_git_fetch_via_http', + 'jmeter_git_push_via_ssh', 'jmeter_git_fetch_via_ssh'] + + +class BaseFileReader: + + @staticmethod + def validate_file_exists(path): + if not os.path.exists(path): + raise Exception(f'{path} does not exist') + + @staticmethod + def validate_file_not_empty(file): + if len(file) == 0: + raise SystemExit(f'ERROR: {file} file in {file} is empty') + + @staticmethod + def validate_headers(headers_list, validation_dict): + for key, value in validation_dict.items(): + if headers_list[key] != value: + raise SystemExit(f'Header validation error. ' + f'Actual: {headers_list[key]}, Expected: {validation_dict[key]}') + + @property + def log_dir(self): + return ENV_TAURUS_ARTIFACT_DIR + + +class BztFileReader(BaseFileReader): + + bzt_log_name = 'bzt.log' + dt_regexp = r'(\d{4}-\d{1,2}-\d{1,2}\s+\d{1,2}:\d{1,2}:\d{1,2})' + jmeter_test_regexp = r'jmeter_\S*' + selenium_test_regexp = r'selenium_\S*' + locust_test_regexp = r'locust_\S*' + success_test_rate_regexp = r'(\d{1,3}.\d{1,2}%)' + + def __init__(self): + self.bzt_log = self.get_bzt_log() + self.bzt_log_results_part = self._get_results_bzt_log_part() + + def get_bzt_log(self): + bzt_log_path = f'{self.log_dir}/{self.bzt_log_name}' + self.validate_file_exists(bzt_log_path) + with open(bzt_log_path) as log_file: + log_file = log_file.readlines() + self.validate_file_not_empty(log_file) + return log_file + + def _get_duration_by_start_finish_strings(self): + first_string = self.bzt_log[0] + last_string = self.bzt_log[-1] + start_time = re.findall(self.dt_regexp, first_string)[0] + start_datetime_obj = datetime.strptime(start_time, '%Y-%m-%d %H:%M:%S') + finish_time = re.findall(self.dt_regexp, last_string)[0] + finish_datetime_obj = datetime.strptime(finish_time, '%Y-%m-%d %H:%M:%S') + duration = finish_datetime_obj - start_datetime_obj + return duration.seconds + + def _get_duration_by_test_duration(self): + test_duration = None + for string in self.bzt_log: + if 'Test duration' in string: + str_duration = string.split('duration:')[1].replace('\n', '') + str_duration = str_duration.replace(' ', '') + duration_datetime_obj = datetime.strptime(str_duration, '%H:%M:%S') + test_duration = (duration_datetime_obj.hour * 3600 + + duration_datetime_obj.minute * 60 + duration_datetime_obj.second) + break + return test_duration + + def _get_test_count_by_type(self, tests_type, log): + trigger = f' {tests_type}_' + test_search_regx = "" + if tests_type == 'jmeter': + test_search_regx = self.jmeter_test_regexp + elif tests_type == 'selenium': + test_search_regx = self.selenium_test_regexp + elif tests_type == 'locust': + test_search_regx = self.locust_test_regexp + tests = {} + for line in log: + if trigger in line and ('FAIL' in line or 'OK' in line): + test_name = re.findall(test_search_regx, line)[0] + test_rate = float(''.join(re.findall(self.success_test_rate_regexp, line))[:-1]) + if test_name not in tests: + tests[test_name] = test_rate + return tests + + def _get_results_bzt_log_part(self): + test_result_string_trigger = 'Request label stats:' + res_string_idx = [index for index, value in enumerate(self.bzt_log) if test_result_string_trigger in value] + # Cut bzt.log from the 'Request label stats:' string to the end + if res_string_idx: + res_string_idx = res_string_idx[0] + results_bzt_run = self.bzt_log[res_string_idx:] + return results_bzt_run + + @property + def selenium_test_rates(self): + return self._get_test_count_by_type(tests_type='selenium', log=self.bzt_log_results_part) + + @property + def jmeter_test_rates(self): + return self._get_test_count_by_type(tests_type='jmeter', log=self.bzt_log_results_part) + + @property + def locust_test_rates(self): + return self._get_test_count_by_type(tests_type='locust', log=self.bzt_log_results_part) + + @property + def actual_run_time(self): + run_time_bzt = self._get_duration_by_test_duration() + return run_time_bzt if run_time_bzt else self._get_duration_by_start_finish_strings() + + +class ResultsFileReader(BaseFileReader): + header_validation = {0: 'Label', 1: '# Samples'} + + def __init__(self): + self.results_log = self.get_results_log() + + def get_results_log(self): + results_log_path = f'{self.log_dir}/results.csv' + self.validate_file_exists(results_log_path) + with open(results_log_path) as res_file: + header = res_file.readline() + results = res_file.readlines() + self.validate_file_not_empty(results) + headers_list = header.split(',') + self.validate_headers(headers_list, self.header_validation) + return results + + @property + def actual_git_operations_count(self): + count = 0 + for line in self.results_log: + if any(s in line for s in GIT_OPERATIONS): + count = count + int(line.split(',')[1]) + return count diff --git a/app/util/data_preparation/api/__init__.py b/app/util/api/__init__.py similarity index 100% rename from app/util/data_preparation/api/__init__.py rename to app/util/api/__init__.py diff --git a/app/util/data_preparation/api/abstract_clients.py b/app/util/api/abstract_clients.py similarity index 82% rename from app/util/data_preparation/api/abstract_clients.py rename to app/util/api/abstract_clients.py index daca3cd14..329f9d6e0 100644 --- a/app/util/data_preparation/api/abstract_clients.py +++ b/app/util/api/abstract_clients.py @@ -29,6 +29,11 @@ class RestClient(Client): "Accept": "application/json", "Content-Type": "application/json" } + LOGIN_POST_HEADERS = { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,' + 'application/signed-exchange;v=b3;q=0.9' + } @staticmethod def to_json(obj: dict) -> str: @@ -81,5 +86,8 @@ def __verify_response(self, response: Response, error_msg: str): if denied_reason and denied_reason.startswith('CAPTCHA_CHALLENGE'): raise Exception(f"User name [{self.user}] is in Captcha Mode. " + "Please login via Web UI first and re-run tests.") - + elif status_code == 404: + raise Exception(f"The URL or content are not found for {response.url}. " + f"Please check environment variables in " + f"config.yml file (hostname, port, protocol, postfix).") raise Exception(f"{error_msg}. Response code:[{response.status_code}], response text:[{response.text}]") diff --git a/app/util/data_preparation/api/bitbucket_clients.py b/app/util/api/bitbucket_clients.py similarity index 68% rename from app/util/data_preparation/api/bitbucket_clients.py rename to app/util/api/bitbucket_clients.py index 43b66402d..e542a9fa1 100644 --- a/app/util/data_preparation/api/bitbucket_clients.py +++ b/app/util/api/bitbucket_clients.py @@ -1,7 +1,8 @@ import time from enum import Enum -from util.data_preparation.api.abstract_clients import RestClient +from util.api.abstract_clients import RestClient +import lxml.html as LH BATCH_SIZE_PROJECTS = 100 BATCH_SIZE_USERS = 100 @@ -45,16 +46,23 @@ def get_entities(self, entity_name, batch_size, filter_=None, max_results=500): return entities def get_non_fork_repos(self, max_results): - batch_size = 1000 + batch_size = None non_fork_repos = [] start_at = 0 while len(non_fork_repos) < max_results: - api_url = f'{self.host}/rest/api/1.0/repos?limit={batch_size}&start={start_at}' + api_url = f'{self.host}/rest/api/1.0/repos?limit={batch_size if batch_size else 1000}&start={start_at}' response = self.get(api_url, f'Could not retrieve entities list') - for repo in response.json()['values']: - if 'origin' not in repo and len(non_fork_repos) < max_results: + if not batch_size: + batch_size = response.json()['limit'] + repos = response.json()['values'] + for repo in repos: + if 'origin' not in repo: non_fork_repos.append(repo) - start_at = start_at + batch_size + if len(non_fork_repos) == max_results: + return non_fork_repos + if response.json()['isLastPage']: + break + start_at = response.json()['nextPageStart'] return non_fork_repos def get_projects(self, max_results=500): @@ -112,3 +120,40 @@ def apply_user_permissions(self, name: str, permission: BitbucketUserPermission) response = self.put(api_url, "Could not create user", params=params) print(f'Successfully applied user [{name}] permission [{permission.value}] in [{(time.time() - start_time)}]') return response + + def get_bitbucket_cluster_page(self): + session = self._session + url = f"{self.host}/admin/clustering" + body = { + '_atl_remember_me': 'on', + 'j_password': self.password, + 'j_username': self.user, + 'next': '/admin/clustering', + 'queryString': 'next=/admin/clustering', + 'submit': 'Log in' + } + headers = self.LOGIN_POST_HEADERS + headers['Origin'] = self.host + r = session.post(url, data=body, headers=headers) + cluster_html = r.content.decode("utf-8") + return cluster_html + + def get_bitbucket_system_page(self): + session = self._session + url = f"{self.host}/j_atl_security_check" + body = {'j_username': self.user, 'j_password': self.password, '_atl_remember_me': 'on', + 'next': f"{self.host}/plugins/servlet/troubleshooting/view/system-info/view", + 'submit': 'Log in'} + headers = self.LOGIN_POST_HEADERS + headers['Origin'] = self.host + session.post(url, data=body, headers=headers) + r = session.get(f"{self.host}/plugins/servlet/troubleshooting/view/system-info/view") + return r.content.decode('utf-8') + + def get_locale(self): + page = LH.parse(self.host) + try: + language = page.xpath('//html/@lang')[0] + except Exception: + raise Exception('Could not get user locale') + return language diff --git a/app/util/data_preparation/api/confluence_clients.py b/app/util/api/confluence_clients.py similarity index 84% rename from app/util/data_preparation/api/confluence_clients.py rename to app/util/api/confluence_clients.py index 0dc9610a9..fd6ac65ce 100644 --- a/app/util/data_preparation/api/confluence_clients.py +++ b/app/util/api/confluence_clients.py @@ -1,7 +1,8 @@ import xmlrpc.client -from util.data_preparation.api.abstract_clients import RestClient, Client +from util.api.abstract_clients import RestClient, Client import xml.etree.ElementTree as ET +import lxml.html as LH BATCH_SIZE_SEARCH = 500 @@ -129,6 +130,29 @@ def is_remote_api_enabled(self): 'General Configuration - Further Configuration - Remote API') return response.status_code == 200 + def get_confluence_nodes_count(self): + api_url = f"{self.host}/rest/atlassian-cluster-monitoring/cluster/nodes" + response = self.get(api_url, error_msg='Could not get Confluence nodes count via API') + return response.json() + + def get_total_pages_count(self): + api_url = f"{self.host}/rest/api/search?cql=type=page" + response = self.get(api_url, 'Could not get issues count') + return response.json().get('totalSize', 0) + + def get_collaborative_editing_status(self): + api_url = f'{self.host}/rest/synchrony-interop/status' + response = self.get(api_url, error_msg='Could not get collaborative editing status') + return response.json() + + def get_locale(self): + page = LH.parse(self.host) + try: + language = page.xpath('.//meta[@name="ajs-user-locale"]/@content')[0] + except Exception: + raise Exception('Could not get user locale') + return language + class ConfluenceRpcClient(Client): @@ -152,6 +176,6 @@ def create_user(self, username=None, password=None): } proxy.confluence2.addUser(token, user_definition, password) user_definition['password'] = password - return user_definition + return {'user': {'username': user_definition["name"], 'email': user_definition["email"]}} else: raise Exception(f"Can't create user {username}: user already exists.") diff --git a/app/util/data_preparation/api/jira_clients.py b/app/util/api/jira_clients.py similarity index 74% rename from app/util/data_preparation/api/jira_clients.py rename to app/util/api/jira_clients.py index 55d74a4b4..277eec853 100644 --- a/app/util/data_preparation/api/jira_clients.py +++ b/app/util/api/jira_clients.py @@ -1,4 +1,4 @@ -from util.data_preparation.api.abstract_clients import RestClient +from util.api.abstract_clients import RestClient BATCH_SIZE_BOARDS = 1000 BATCH_SIZE_USERS = 1000 @@ -76,13 +76,12 @@ def get_users(self, username='.', start_at=0, max_results=1000, include_active=T return users_list - def issues_search(self, jql='order by key', start_at=0, max_results=1000, validate_query=True, fields='id'): + def issues_search(self, jql='order by key', start_at=0, max_results=1000, fields=None): """ Searches for issues using JQL. :param jql: a JQL query string :param start_at: the index of the first issue to return (0-based) :param max_results: the maximum number of issues to return (defaults to 50). - :param validate_query: whether to validate the JQL query :param fields: the list of fields to return for each issue. By default, all navigable fields are returned. *all - include all fields *navigable - include just navigable fields @@ -95,11 +94,18 @@ def issues_search(self, jql='order by key', start_at=0, max_results=1000, valida loop_count = max_results // BATCH_SIZE_ISSUES + 1 issues = list() last_loop_remainder = max_results % BATCH_SIZE_ISSUES + api_url = f'{self.host}/rest/api/2/search' while loop_count > 0: - api_url = f'{self.host}/rest/api/2/search?jql={jql}&startAt={start_at}&maxResults={max_results}' \ - f'&validateQuery={validate_query}&fields={fields}' - response = self.get(api_url, "Could not retrieve issues") + + body = { + "jql": jql, + "startAt": start_at, + "maxResults": max_results, + "fields": ['id'] if fields is None else fields + } + + response = self.post(api_url, "Could not retrieve issues", body=body) current_issues = response.json()['issues'] issues.extend(current_issues) @@ -110,6 +116,12 @@ def issues_search(self, jql='order by key', start_at=0, max_results=1000, valida return issues + def get_total_issues_count(self): + api_url = f'{self.host}/rest/api/2/search' + body = {"jql": "order by key"} + response = self.post(api_url, "Could not retrieve issues", body=body) + return response.json().get('total', 0) + def create_user(self, display_name=None, email=None, name='', password=''): """ Creates a user. This resource is retained for legacy compatibility. @@ -145,3 +157,41 @@ def get_server_info(self): response = self.get(api_url, 'Could not get the server information') return response.json() + + def get_nodes_info_via_rest(self): + # Works for Jira version >= 8.1.0 + api_url = f'{self.host}/rest/api/2/cluster/nodes' + response = self.get(api_url, 'Could not get Jira nodes count') + + return response.json() + + def get_system_info_page(self): + session = self._session + login_url = f'{self.host}/login.jsp' + auth_url = f'{self.host}/secure/admin/WebSudoAuthenticate.jspa' + login_body = { + 'atl_token': '', + 'os_destination': '/secure/admin/ViewSystemInfo.jspa', + 'os_password': self.password, + 'os_username': self.user, + 'user_role': 'ADMIN' + } + auth_body = { + 'webSudoDestination': '/secure/admin/ViewSystemInfo.jspa', + 'webSudoIsPost': False, + 'webSudoPassword': self.password + } + headers = self.LOGIN_POST_HEADERS + headers['Origin'] = self.host + + session.post(url=login_url, data=login_body, headers=headers) + auth_request = session.post(url=auth_url, data=auth_body, headers=headers) + system_info_html = auth_request.content.decode("utf-8") + if 'Cluster nodes' not in system_info_html: + print('Could not get Jira nodes count via parse html page') + return system_info_html + + def get_locale(self): + api_url = f'{self.host}/rest/api/2/myself' + user_properties = self.get(api_url, "Could not retrieve user") + return user_properties.json()['locale'] diff --git a/app/util/bitbucket/populate_db.sh b/app/util/bitbucket/populate_db.sh index 0bd607153..03d3ffcd5 100644 --- a/app/util/bitbucket/populate_db.sh +++ b/app/util/bitbucket/populate_db.sh @@ -1,7 +1,7 @@ #!/bin/bash ################### Check if NFS exists ################### -pgrep nfsd > /dev/null && echo "NFS found" || (echo "NFS process was not found. This script is intended to run only on the Bitbucket NFS Server machine."; exit 1) +pgrep nfsd > /dev/null && echo "NFS found" || { echo NFS process was not found. This script is intended to run only on the Bitbucket NFS Server machine. && exit 1; } ################### Variables section ################### # Command to install psql client for Amazon Linux 2. @@ -12,7 +12,6 @@ INSTALL_PSQL_CMD="amazon-linux-extras install -y postgresql10" DB_CONFIG="/media/atl/bitbucket/shared/bitbucket.properties" # Depending on BITBUCKET installation directory -BITBUCKET_CURRENT_DIR="/opt/atlassian/bitbucket/current/" BITBUCKET_VERSION_FILE="/media/atl/bitbucket/shared/bitbucket.version" # DB admin user name, password and DB name @@ -23,6 +22,11 @@ BITBUCKET_DB_PASS="Password1!" # BITBUCKET version variables SUPPORTED_BITBUCKET_VERSIONS=(6.10.0 7.0.0) BITBUCKET_VERSION=$(sudo su bitbucket -c "cat ${BITBUCKET_VERSION_FILE}") +if [[ -z "$BITBUCKET_VERSION" ]]; then + echo The $BITBUCKET_VERSION_FILE file does not exists or emtpy. Please check if BITBUCKET_VERSION_FILE variable \ + has a valid file path of the Bitbucket version file or set your Cluster BITBUCKET_VERSION explicitly. + exit 1 +fi echo "Bitbucket version: ${BITBUCKET_VERSION}" # Datasets AWS bucket and db dump name @@ -33,7 +37,6 @@ DB_DUMP_URL="${DATASETS_AWS_BUCKET}/${BITBUCKET_VERSION}/${DATASETS_SIZE}/${DB_D ################### End of variables section ################### - # Check if Bitbucket version is supported if [[ ! "${SUPPORTED_BITBUCKET_VERSIONS[@]}" =~ "${BITBUCKET_VERSION}" ]]; then echo "Bitbucket Version: ${BITBUCKET_VERSION} is not officially supported by Data Center App Performance Toolkit." @@ -64,7 +67,6 @@ echo "This script restores Postgres DB from SQL DB dump for Bitbucket DC created echo "You can review or modify default variables in 'Variables section' of this script." echo # move to a new line echo "Variables:" -echo "BITBUCKET_CURRENT_DIR=${BITBUCKET_CURRENT_DIR}" echo "DB_CONFIG=${DB_CONFIG}" echo "BITBUCKET_DB_NAME=${BITBUCKET_DB_NAME}" echo "BITBUCKET_DB_USER=${BITBUCKET_DB_USER}" @@ -92,7 +94,53 @@ else echo "Postgres client is already installed" fi -echo "Step2: Download DB dump" +echo "Step2: Get DB Host and check DB connection" +DB_HOST=$(sudo su -c "cat ${DB_CONFIG} | grep 'jdbc:postgresql' | cut -d'/' -f3 | cut -d':' -f1") +if [[ -z ${DB_HOST} ]]; then + echo "DataBase URL was not found in ${DB_CONFIG}" + exit 1 +fi +echo "DB_HOST=${DB_HOST}" + +PGPASSWORD=${BITBUCKET_DB_PASS} pg_isready -U ${BITBUCKET_DB_USER} -h ${DB_HOST} +if [[ $? -ne 0 ]]; then + echo "Connection to DB failed. Please check correctness of following variables:" + echo "BITBUCKET_DB_NAME=${BITBUCKET_DB_NAME}" + echo "BITBUCKET_DB_USER=${BITBUCKET_DB_USER}" + echo "BITBUCKET_DB_PASS=${BITBUCKET_DB_PASS}" + echo "DB_HOST=${DB_HOST}" + exit 1 +fi + +echo "Step3: Write 'instance.url' property to file" +BITBUCKET_BASE_URL_FILE="base_url" +if [[ -s ${BITBUCKET_BASE_URL_FILE} ]]; then + echo "File ${BITBUCKET_BASE_URL_FILE} was found. Base url: $(cat ${BITBUCKET_BASE_URL_FILE})." +else + PGPASSWORD=${BITBUCKET_DB_PASS} psql -h ${DB_HOST} -d ${BITBUCKET_DB_NAME} -U ${BITBUCKET_DB_USER} -Atc \ + "select prop_value from app_property where prop_key='instance.url';" > ${BITBUCKET_BASE_URL_FILE} + if [[ ! -s ${BITBUCKET_BASE_URL_FILE} ]]; then + echo "Failed to get Base URL value from database. Check DB configuration variables." + exit 1 + fi + echo "$(cat ${BITBUCKET_BASE_URL_FILE}) was written to the ${BITBUCKET_BASE_URL_FILE} file." +fi + +echo "Step4: Write license to file" +BITBUCKET_LICENSE_FILE="license" +if [[ -s ${BITBUCKET_LICENSE_FILE} ]]; then + echo "File ${BITBUCKET_LICENSE_FILE} was found. License: $(cat ${BITBUCKET_LICENSE_FILE})." +else + PGPASSWORD=${BITBUCKET_DB_PASS} psql -h ${DB_HOST} -d ${BITBUCKET_DB_NAME} -U ${BITBUCKET_DB_USER} -tAc \ + "select prop_value from app_property where prop_key = 'license';" | sed "s/\r//g" > ${BITBUCKET_LICENSE_FILE} + if [[ ! -s ${BITBUCKET_LICENSE_FILE} ]]; then + echo "Failed to get bitbucket license from database. Check DB configuration variables." + exit 1 + fi + echo "$(cat ${BITBUCKET_LICENSE_FILE}) was written to the ${BITBUCKET_LICENSE_FILE} file." +fi + +echo "Step5: Download DB dump" DUMP_DIR='/media/atl/bitbucket/shared' if [[ $? -ne 0 ]]; then echo "Directory ${DUMP_DIR} does not exist" @@ -117,25 +165,8 @@ if [[ $? -ne 0 ]]; then exit 1 fi -echo "Step3: Get DB Host" -DB_HOST=$(sudo su -c "cat ${DB_CONFIG} | grep 'jdbc:postgresql' | cut -d'/' -f3 | cut -d':' -f1") -if [[ -z ${DB_HOST} ]]; then - echo "DataBase URL was not found in ${DB_CONFIG}" - exit 1 -fi -echo "DB_HOST=${DB_HOST}" - -echo "Step4: SQL Restore" +echo "Step6: SQL Restore" echo "Check DB connection" -PGPASSWORD=${BITBUCKET_DB_PASS} pg_isready -U ${BITBUCKET_DB_USER} -h ${DB_HOST} -if [[ $? -ne 0 ]]; then - echo "Connection to DB failed. Please check correctness of following variables:" - echo "BITBUCKET_DB_NAME=${BITBUCKET_DB_NAME}" - echo "BITBUCKET_DB_USER=${BITBUCKET_DB_USER}" - echo "BITBUCKET_DB_PASS=${BITBUCKET_DB_PASS}" - echo "DB_HOST=${DB_HOST}" - exit 1 -fi echo "Drop DB" sudo su -c "PGPASSWORD=${BITBUCKET_DB_PASS} dropdb -U ${BITBUCKET_DB_USER} -h ${DB_HOST} ${BITBUCKET_DB_NAME}" if [[ $? -ne 0 ]]; then @@ -158,10 +189,44 @@ if [[ $? -ne 0 ]]; then fi sudo su -c "rm -rf ${DUMP_DIR}/${DB_DUMP_NAME}" -echo "Finished" -echo # move to a new line +echo "Step7: Update 'instance.url' property in database" +if [[ -s ${BITBUCKET_BASE_URL_FILE} ]]; then + BASE_URL=$(cat ${BITBUCKET_BASE_URL_FILE}) + if [[ $(PGPASSWORD=${BITBUCKET_DB_PASS} psql -h ${DB_HOST} -d ${BITBUCKET_DB_NAME} -U ${BITBUCKET_DB_USER} -c \ + "UPDATE app_property SET prop_value = '${BASE_URL}' WHERE prop_key = 'instance.url';") != "UPDATE 1" ]]; then + echo "Couldn't update database 'instance.url' property. Please check your database connection." + exit 1 + else + echo "The database 'instance.url' property was updated with ${BASE_URL}" + fi +else + echo "The ${BITBUCKET_BASE_URL_FILE} file doesn't exist or empty. Please check file existence or 'instance.url' property in the database." + exit 1 +fi -echo "Important: new admin user credentials are admin/admin" -echo "Important: do not start Bitbucket until attachments restore is finished" +echo "Step8: Update license property in database" +if [[ -s ${BITBUCKET_LICENSE_FILE} ]]; then + LICENSE=$(cat ${BITBUCKET_LICENSE_FILE}) + if [[ $(PGPASSWORD=${BITBUCKET_DB_PASS} psql -h ${DB_HOST} -d ${BITBUCKET_DB_NAME} -U ${BITBUCKET_DB_USER} -c \ + "update app_property set prop_value = '${LICENSE}' where prop_key = 'license';") != "UPDATE 1" ]]; then + echo "Couldn't update database bitbucket license property. Please check your database connection." + exit 1 + else + echo "The database bitbucket license property was updated with ${LICENSE}" + fi +else + echo "The ${BITBUCKET_LICENSE_FILE} file doesn't exist or empty. Please check file existence or 'bitbucket license' property in the database." + exit 1 +fi + +echo "Step9: Remove ${BITBUCKET_BASE_URL_FILE} file" +sudo rm ${BITBUCKET_BASE_URL_FILE} +echo "Step10: Remove ${BITBUCKET_LICENSE_FILE} file" +sudo rm ${BITBUCKET_LICENSE_FILE} +echo "Finished" +echo # move to a new line + +echo "Important: new admin user credentials are admin/admin" +echo "Important: do not start Bitbucket until attachments restore is finished" \ No newline at end of file diff --git a/app/util/bitbucket/upload_attachments.sh b/app/util/bitbucket/upload_attachments.sh index c1241caf4..1d761bf3a 100644 --- a/app/util/bitbucket/upload_attachments.sh +++ b/app/util/bitbucket/upload_attachments.sh @@ -1,13 +1,18 @@ #!/bin/bash ################### Check if NFS exists ################### -pgrep nfsd > /dev/null && echo "NFS found" || (echo "NFS process was not found. This script is intended to run only on the Bitbucket NFS Server machine."; exit 1) +pgrep nfsd > /dev/null && echo "NFS found" || { echo NFS process was not found. This script is intended to run only on the Bitbucket NFS Server machine. && exit 1; } ################### Variables section ################### # Bitbucket version variables BITBUCKET_VERSION_FILE="/media/atl/bitbucket/shared/bitbucket.version" SUPPORTED_BITBUCKET_VERSIONS=(6.10.0 7.0.0) BITBUCKET_VERSION=$(sudo su bitbucket -c "cat ${BITBUCKET_VERSION_FILE}") +if [[ -z "$BITBUCKET_VERSION" ]]; then + echo The $BITBUCKET_VERSION_FILE file does not exists or emtpy. Please check if BITBUCKET_VERSION_FILE variable \ + has a valid file path of the Bitbucket version file or set your Cluster BITBUCKET_VERSION explicitly. + exit 1 +fi echo "Bitbucket Version: ${BITBUCKET_VERSION}" DATASETS_AWS_BUCKET="https://centaurus-datasets.s3.amazonaws.com/bitbucket" diff --git a/app/util/cleanup_results_dir.py b/app/util/cleanup_results_dir.py deleted file mode 100644 index 1511e28ca..000000000 --- a/app/util/cleanup_results_dir.py +++ /dev/null @@ -1,24 +0,0 @@ -from pathlib import Path -import os - -ENV_TAURUS_ARTIFACT_DIR = 'TAURUS_ARTIFACTS_DIR' -FILES_TO_REMOVE = ['jmeter.out', - 'jmeter-bzt.properties', - 'merged.json', - 'merged.yml', - 'PyTestExecutor.ldjson', - 'system.properties'] - -if ENV_TAURUS_ARTIFACT_DIR in os.environ: - artifacts_dir = os.environ.get(ENV_TAURUS_ARTIFACT_DIR) -else: - raise SystemExit(f'Error: env variable {ENV_TAURUS_ARTIFACT_DIR} is not set') - -for file in FILES_TO_REMOVE: - file_path = Path(f'{artifacts_dir}/{file}') - try: - os.remove(file_path) - print(f'The {file} was removed successfully') - except OSError as e: - print(f'Deleting of the {file} failed!\n' - f'Error: {file_path}: {e.strerror}') diff --git a/app/util/conf.py b/app/util/conf.py index b871fb7fa..4e5a081c2 100644 --- a/app/util/conf.py +++ b/app/util/conf.py @@ -2,7 +2,7 @@ from util.project_paths import JIRA_YML, CONFLUENCE_YML, BITBUCKET_YML -TOOLKIT_VERSION = '2.0.0' +TOOLKIT_VERSION = '3.0.0' def read_yml_file(file): @@ -24,12 +24,23 @@ def __init__(self, config_yml): self.concurrency = env_settings['concurrency'] self.duration = env_settings['test_duration'] self.analytics_collector = env_settings['allow_analytics'] + self.load_executor = env_settings['load_executor'] @property def server_url(self): return f'{self.protocol}://{self.hostname}:{self.port}{self.postfix}' -JIRA_SETTINGS = AppSettings(config_yml=JIRA_YML) -CONFLUENCE_SETTINGS = AppSettings(config_yml=CONFLUENCE_YML) +class AppSettingsExtLoadExecutor(AppSettings): + + def __init__(self, config_yml): + super().__init__(config_yml) + obj = read_yml_file(config_yml) + self.env = obj['settings']['env'] + self.verbose = obj['settings']['verbose'] + self.total_actions_per_hour = self.env['total_actions_per_hour'] + + +JIRA_SETTINGS = AppSettingsExtLoadExecutor(config_yml=JIRA_YML) +CONFLUENCE_SETTINGS = AppSettingsExtLoadExecutor(config_yml=CONFLUENCE_YML) BITBUCKET_SETTINGS = AppSettings(config_yml=BITBUCKET_YML) diff --git a/app/util/confluence/populate_db.sh b/app/util/confluence/populate_db.sh index ed6e917c3..0688dca1c 100644 --- a/app/util/confluence/populate_db.sh +++ b/app/util/confluence/populate_db.sh @@ -9,7 +9,6 @@ INSTALL_PSQL_CMD="amazon-linux-extras install -y postgresql10" DB_CONFIG="/var/atlassian/application-data/confluence/confluence.cfg.xml" # Depending on Confluence installation directory -CONFLUENCE_CURRENT_DIR="/opt/atlassian/confluence/current" CONFLUENCE_VERSION_FILE="/media/atl/confluence/shared-home/confluence.version" # DB admin user name, password and DB name @@ -17,9 +16,24 @@ CONFLUENCE_DB_NAME="confluence" CONFLUENCE_DB_USER="postgres" CONFLUENCE_DB_PASS="Password1!" +# Confluence DB requests +SELECT_CONFLUENCE_SETTING_SQL="select BANDANAVALUE from BANDANA where BANDANACONTEXT = '_GLOBAL' and BANDANAKEY = 'atlassian.confluence.settings';" + # Confluence version variables SUPPORTED_CONFLUENCE_VERSIONS=(6.13.8 7.0.4) + +if [[ ! $(systemctl status confluence) ]]; then + echo "The Confluence service was not found on this host." \ + "Please make sure you are running this script on a host that is running Confluence." + exit 1 +fi + CONFLUENCE_VERSION=$(sudo su confluence -c "cat ${CONFLUENCE_VERSION_FILE}") +if [[ -z "$CONFLUENCE_VERSION" ]]; then + echo The $CONFLUENCE_VERSION_FILE file does not exists or emtpy. Please check if CONFLUENCE_VERSION_FILE variable \ + has a valid file path of the Confluence version file or set your Cluster CONFLUENCE_VERSION explicitly. + exit 1 +fi echo "Confluence Version: ${CONFLUENCE_VERSION}" # Datasets AWS bucket and db dump name @@ -30,7 +44,6 @@ DB_DUMP_URL="${DATASETS_AWS_BUCKET}/${CONFLUENCE_VERSION}/${DATASETS_SIZE}/${DB_ ################### End of variables section ################### - # Check if Confluence version is supported if [[ ! "${SUPPORTED_CONFLUENCE_VERSIONS[@]}" =~ "${CONFLUENCE_VERSION}" ]]; then echo "Confluence Version: ${CONFLUENCE_VERSION} is not officially supported by Data Center App Performance Toolkit." @@ -61,7 +74,6 @@ echo "This script restores Postgres DB from SQL DB dump for Confluence DC create echo "You can review or modify default variables in 'Variables section' of this script." echo # move to a new line echo "Variables:" -echo "CONFLUENCE_CURRENT_DIR=${CONFLUENCE_CURRENT_DIR}" echo "DB_CONFIG=${DB_CONFIG}" echo "CONFLUENCE_DB_NAME=${CONFLUENCE_DB_NAME}" echo "CONFLUENCE_DB_USER=${CONFLUENCE_DB_USER}" @@ -89,14 +101,47 @@ else echo "Postgres client is already installed" fi -echo "Step2: Stop Confluence" +echo "Step2: Get DB Host and check DB connection" +DB_HOST=$(sudo su -c "cat ${DB_CONFIG} | grep 'jdbc:postgresql' | cut -d'/' -f3 | cut -d':' -f1") +if [[ -z ${DB_HOST} ]]; then + echo "DataBase URL was not found in ${DB_CONFIG}" + exit 1 +fi +echo "DB_HOST=${DB_HOST}" + +echo "Check DB connection" +PGPASSWORD=${CONFLUENCE_DB_PASS} pg_isready -U ${CONFLUENCE_DB_USER} -h ${DB_HOST} +if [[ $? -ne 0 ]]; then + echo "Connection to DB failed. Please check correctness of following variables:" + echo "CONFLUENCE_DB_NAME=${CONFLUENCE_DB_NAME}" + echo "CONFLUENCE_DB_USER=${CONFLUENCE_DB_USER}" + echo "CONFLUENCE_DB_PASS=${CONFLUENCE_DB_PASS}" + echo "DB_HOST=${DB_HOST}" + exit 1 +fi + +echo "Step3: Write confluence baseUrl to file" +CONFLUENCE_BASE_URL_FILE="base_url" +if [[ -s ${CONFLUENCE_BASE_URL_FILE} ]];then + echo "File ${CONFLUENCE_BASE_URL_FILE} was found. Base url: $(cat ${CONFLUENCE_BASE_URL_FILE})." +else + PGPASSWORD=${CONFLUENCE_DB_PASS} psql -h ${DB_HOST} -d ${CONFLUENCE_DB_NAME} -U ${CONFLUENCE_DB_USER} -Atc "${SELECT_CONFLUENCE_SETTING_SQL}" \ + | grep -i "<baseurl>" > ${CONFLUENCE_BASE_URL_FILE} + if [[ ! -s ${CONFLUENCE_BASE_URL_FILE} ]]; then + echo "Failed to get Base URL value from database. Check DB configuration variables." + exit 1 + fi + echo "$(cat ${CONFLUENCE_BASE_URL_FILE}) was written to the ${CONFLUENCE_BASE_URL_FILE} file." +fi + +echo "Step4: Stop Confluence" sudo systemctl stop confluence if [[ $? -ne 0 ]]; then echo "Confluence did not stop. Please try to rerun script." exit 1 fi -echo "Step3: Download DB dump" +echo "Step5: Download DB dump" rm -rf ${DB_DUMP_NAME} ARTIFACT_SIZE_BYTES=$(curl -sI ${DB_DUMP_URL} | grep "Content-Length" | awk {'print $2'} | tr -d '[:space:]') ARTIFACT_SIZE_GB=$((${ARTIFACT_SIZE_BYTES}/1024/1024/1024)) @@ -104,11 +149,11 @@ FREE_SPACE_KB=$(df -k --output=avail "$PWD" | tail -n1) FREE_SPACE_GB=$((${FREE_SPACE_KB}/1024/1024)) REQUIRED_SPACE_GB=$((5 + ${ARTIFACT_SIZE_GB})) if [[ ${FREE_SPACE_GB} -lt ${REQUIRED_SPACE_GB} ]]; then - echo "Not enough free space for download." - echo "Free space: ${FREE_SPACE_GB} GB" - echo "Required space: ${REQUIRED_SPACE_GB} GB" - exit 1 -fi; + echo "Not enough free space for download." + echo "Free space: ${FREE_SPACE_GB} GB" + echo "Required space: ${REQUIRED_SPACE_GB} GB" + exit 1 +fi # use computer style progress bar time wget --progress=dot:giga ${DB_DUMP_URL} if [[ $? -ne 0 ]]; then @@ -116,25 +161,7 @@ if [[ $? -ne 0 ]]; then exit 1 fi -echo "Step4: Get DB Host" -DB_HOST=$(sudo su -c "cat ${DB_CONFIG} | grep 'jdbc:postgresql' | cut -d'/' -f3 | cut -d':' -f1") -if [[ -z ${DB_HOST} ]]; then - echo "DataBase URL was not found in ${DB_CONFIG}" - exit 1 -fi -echo "DB_HOST=${DB_HOST}" - -echo "Step5: SQL Restore" -echo "Check DB connection" -PGPASSWORD=${CONFLUENCE_DB_PASS} pg_isready -U ${CONFLUENCE_DB_USER} -h ${DB_HOST} -if [[ $? -ne 0 ]]; then - echo "Connection to DB failed. Please check correctness of following variables:" - echo "CONFLUENCE_DB_NAME=${CONFLUENCE_DB_NAME}" - echo "CONFLUENCE_DB_USER=${CONFLUENCE_DB_USER}" - echo "CONFLUENCE_DB_PASS=${CONFLUENCE_DB_PASS}" - echo "DB_HOST=${DB_HOST}" - exit 1 -fi +echo "Step6: SQL Restore" echo "Drop DB" PGPASSWORD=${CONFLUENCE_DB_PASS} dropdb -U ${CONFLUENCE_DB_USER} -h ${DB_HOST} ${CONFLUENCE_DB_NAME} if [[ $? -ne 0 ]]; then @@ -156,12 +183,41 @@ if [[ $? -ne 0 ]]; then exit 1 fi -echo "Step6: Start Confluence" +echo "Step7: Update confluence baseUrl value in database" +BASE_URL_TO_REPLACE=$(PGPASSWORD=${CONFLUENCE_DB_PASS} psql -h ${DB_HOST} -d ${CONFLUENCE_DB_NAME} -U ${CONFLUENCE_DB_USER} -Atc \ +"${SELECT_CONFLUENCE_SETTING_SQL}" | grep -i "<baseurl>") + +if [[ -z "${BASE_URL_TO_REPLACE}" ]]; then + echo "The BASE_URL_TO_REPLACE variable is empty. Please check that the confluence baseUrl value is exist in the database." + exit 1 +fi + +if [[ -s ${CONFLUENCE_BASE_URL_FILE} ]]; then + BASE_URL=$(cat ${CONFLUENCE_BASE_URL_FILE}) + if [[ $(PGPASSWORD=${CONFLUENCE_DB_PASS} psql -h ${DB_HOST} -d ${CONFLUENCE_DB_NAME} -U ${CONFLUENCE_DB_USER} -c \ + "update BANDANA + set BANDANAVALUE = replace(BANDANAVALUE, '${BASE_URL_TO_REPLACE}', '${BASE_URL}') + where BANDANACONTEXT = '_GLOBAL' + and BANDANAKEY = 'atlassian.confluence.settings';") != "UPDATE 1" ]]; then + echo "Couldn't update database baseUrl value. Please check your DB configuration variables." + exit 1 + else + echo "The database baseUrl value was updated with ${BASE_URL}" + fi +else + echo "The ${CONFLUENCE_BASE_URL_FILE} file doesn't exist or empty. Check DB configuration variables." + exit 1 +fi + +echo "Step8: Start Confluence" sudo systemctl start confluence rm -rf ${DB_DUMP_NAME} +echo "Step9: Remove ${CONFLUENCE_BASE_URL_FILE} file" +sudo rm ${CONFLUENCE_BASE_URL_FILE} + echo "Finished" echo # move to a new line echo "Important: new admin user credentials are admin/admin" -echo "Wait a couple of minutes until Confluence is started." \ No newline at end of file +echo "Wait a couple of minutes until Confluence is started." diff --git a/app/util/confluence/upload_attachments.sh b/app/util/confluence/upload_attachments.sh index 2cd0bf03f..9a321f840 100644 --- a/app/util/confluence/upload_attachments.sh +++ b/app/util/confluence/upload_attachments.sh @@ -6,6 +6,11 @@ CONFLUENCE_VERSION_FILE="/media/atl/confluence/shared-home/confluence.version" SUPPORTED_CONFLUENCE_VERSIONS=(6.13.8 7.0.4) CONFLUENCE_VERSION=$(sudo su confluence -c "cat ${CONFLUENCE_VERSION_FILE}") +if [[ -z "$CONFLUENCE_VERSION" ]]; then + echo The $CONFLUENCE_VERSION_FILE file does not exists or emtpy. Please check if CONFLUENCE_VERSION_FILE variable \ + has a valid file path of the Confluence version file or set your Cluster CONFLUENCE_VERSION explicitly. + exit 1 +fi echo "Confluence Version: ${CONFLUENCE_VERSION}" DATASETS_AWS_BUCKET="https://centaurus-datasets.s3.amazonaws.com/confluence" @@ -17,6 +22,12 @@ TMP_DIR="/tmp" EFS_DIR="/media/atl/confluence/shared-home" ################### End of variables section ################### +if [[ ! `systemctl status confluence` ]]; then + echo "The Confluence service was not found on this host." \ + "Please make sure you are running this script on a host that is running Confluence." + exit 1 +fi + # Check if Confluence version is supported if [[ ! "${SUPPORTED_CONFLUENCE_VERSIONS[@]}" =~ "${CONFLUENCE_VERSION}" ]]; then echo "Confluence Version: ${CONFLUENCE_VERSION} is not officially supported by Data Center App Peformance Toolkit." @@ -100,4 +111,4 @@ sudo su confluence -c "time ./msrsync -P -p 100 -f 3000 ${ATTACHMENTS_DIR} ${EFS sudo su -c "rm -rf ${ATTACHMENTS_DIR}" echo "Finished" -echo # move to a new line \ No newline at end of file +echo # move to a new line diff --git a/app/util/data_preparation/bitbucket/prepare-data.py b/app/util/data_preparation/bitbucket_prepare_data.py similarity index 76% rename from app/util/data_preparation/bitbucket/prepare-data.py rename to app/util/data_preparation/bitbucket_prepare_data.py index e18ab89ec..60b377cdc 100644 --- a/app/util/data_preparation/bitbucket/prepare-data.py +++ b/app/util/data_preparation/bitbucket_prepare_data.py @@ -3,7 +3,7 @@ import time from util.conf import BITBUCKET_SETTINGS -from util.data_preparation.api.bitbucket_clients import BitbucketRestClient, BitbucketUserPermission +from util.api.bitbucket_clients import BitbucketRestClient, BitbucketUserPermission from util.project_paths import BITBUCKET_PROJECTS, BITBUCKET_USERS, BITBUCKET_REPOS, BITBUCKET_PRS DEFAULT_USER_PREFIX = 'dcapt-perf-user' @@ -15,6 +15,8 @@ FETCH_LIMIT_REPOS = 50 FETCH_LIMIT_PROJECTS = FETCH_LIMIT_REPOS +ENGLISH = 'en' + def generate_random_string(length=20): return "".join([random.choice(string.ascii_lowercase) for _ in range(length)]) @@ -51,10 +53,10 @@ def __get_repos(bitbucket_api): FETCH_LIMIT_REPOS if concurrency < FETCH_LIMIT_REPOS else concurrency ) print(f'Repos number to fetch via API is {FETCH_LIMIT_REPOS}') - repos_len = len(repos) - if repos_len < concurrency: + repos_count = len(repos) + if repos_count < concurrency: raise SystemExit(f'Required number of repositories based on concurrency was not found' - f' Found [{repos_len}] repos, needed at least [{concurrency}]') + f' Found [{repos_count}] repos, needed at least [{concurrency}]') return repos @@ -75,10 +77,13 @@ def __get_prs(bitbucket_api): if len(repos_prs) <= concurrency: prs = bitbucket_api.get_pull_request(project_key=repo['project']['key'], repo_key=repo['slug']) for pr in prs['values']: - repos_prs.append([repo['slug'], repo['project']['key'], pr['id'], pr['fromRef']['displayId'], pr['toRef']['displayId']]) + # filter PRs created by selenium and not merged + if 'Selenium' not in pr['title']: + repos_prs.append([repo['slug'], repo['project']['key'], pr['id'], + pr['fromRef']['displayId'], pr['toRef']['displayId']]) if len(repos_prs) < concurrency: - raise SystemExit(f'Repositories from list {[repo["project"]["key"] - repo["slug"] for repo in repos]} ' - f'do not contain {concurrency} pull requests') + repos_without_prs = [f'{repo["project"]["key"]}/{repo["slug"]}' for repo in repos] + raise SystemExit(f'Repositories {repos_without_prs} do not contain at least {concurrency} pull requests') print(f"Successfully fetched pull requests in [{(time.time() - start_time)}]") return repos_prs @@ -89,6 +94,10 @@ def __create_data_set(bitbucket_api): dataset[PROJECTS] = __get_projects(bitbucket_api) dataset[REPOS] = __get_repos(bitbucket_api) dataset[PULL_REQUESTS] = __get_prs(bitbucket_api) + print(f'Users count: {len(dataset[USERS])}') + print(f'Projects count: {len(dataset[PROJECTS])}') + print(f'Repos count: {len(dataset[REPOS])}') + print(f'Pull requests count: {len(dataset[PULL_REQUESTS])}') return dataset @@ -112,6 +121,13 @@ def write_test_data_to_files(datasets): __write_to_file(BITBUCKET_PRS, prs) +def __check_current_language(bitbucket_api): + language = bitbucket_api.get_locale() + if language != ENGLISH: + raise SystemExit(f'"{language}" language is not supported. ' + f'Please change your account language to "English (United States)"') + + def main(): print("Started preparing data") @@ -119,6 +135,9 @@ def main(): print("Server url: ", url) client = BitbucketRestClient(url, BITBUCKET_SETTINGS.admin_login, BITBUCKET_SETTINGS.admin_password) + + __check_current_language(client) + dataset = __create_data_set(client) write_test_data_to_files(dataset) diff --git a/app/util/data_preparation/confluence/prepare-data.py b/app/util/data_preparation/confluence_prepare_data.py similarity index 68% rename from app/util/data_preparation/confluence/prepare-data.py rename to app/util/data_preparation/confluence_prepare_data.py index eed357aa0..f1180c165 100644 --- a/app/util/data_preparation/confluence/prepare-data.py +++ b/app/util/data_preparation/confluence_prepare_data.py @@ -4,7 +4,7 @@ import urllib3 from util.conf import CONFLUENCE_SETTINGS -from util.data_preparation.api.confluence_clients import ConfluenceRpcClient, ConfluenceRestClient +from util.api.confluence_clients import ConfluenceRpcClient, ConfluenceRestClient from util.project_paths import CONFLUENCE_USERS, CONFLUENCE_PAGES, CONFLUENCE_BLOGS urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) @@ -14,6 +14,10 @@ BLOGS = "blogs" DEFAULT_USER_PREFIX = 'performance_' DEFAULT_USER_PASSWORD = 'password' +ERROR_LIMIT = 10 + +ENGLISH_US = 'en_US' +ENGLISH_GB = 'en_GB' def generate_random_string(length=20): @@ -25,24 +29,32 @@ def __create_data_set(rest_client, rpc_client): dataset[USERS] = __get_users(rest_client, rpc_client, CONFLUENCE_SETTINGS.concurrency) dataset[PAGES] = __get_pages(rest_client, 5000) dataset[BLOGS] = __get_blogs(rest_client, 5000) + print(f'Users count: {len(dataset[USERS])}') + print(f'Pages count: {len(dataset[PAGES])}') + print(f'Blogs count: {len(dataset[BLOGS])}') return dataset def __get_users(confluence_api, rpc_api, count): + errors_count = 0 cur_perf_users = confluence_api.get_users(DEFAULT_USER_PREFIX, count) if len(cur_perf_users) >= count: return cur_perf_users while len(cur_perf_users) < count: + if errors_count >= ERROR_LIMIT: + raise Exception(f'Maximum error limit reached {errors_count}/{ERROR_LIMIT}. ' + f'Please check the errors above') username = f"{DEFAULT_USER_PREFIX}{generate_random_string(10)}" try: user = rpc_api.create_user(username=username, password=DEFAULT_USER_PASSWORD) - print(f"User {user['name']} is created, number of users to create is " + print(f"User {user['user']['username']} is created, number of users to create is " f"{count - len(cur_perf_users)}") cur_perf_users.append(user) # To avoid rate limit error from server. Execution should not be stopped after catch error from server. except Exception as error: - print(error) + print(f"{error}. Error limits {errors_count}/{ERROR_LIMIT}") + errors_count = errors_count + 1 print('All performance test users were successfully created') return cur_perf_users @@ -52,9 +64,10 @@ def __get_pages(confluence_api, count): 0, count, cql='type=page' ' and title !~ JMeter' # filter out pages created by JMeter ' and title !~ Selenium' # filter out pages created by Selenium + ' and title !~ locust' # filter out pages created by locust ' and title !~ Home') # filter out space Home pages if not pages: - raise SystemExit(f"There are no Pages in Confluence") + raise SystemExit("There are no Pages in Confluence") return pages @@ -90,6 +103,20 @@ def write_test_data_to_files(dataset): __write_to_file(CONFLUENCE_USERS, users) +def __is_collaborative_editing_enabled(confluence_api): + status = confluence_api.get_collaborative_editing_status() + if not all(status.values()): + raise Exception('Please turn on collaborative editing in Confluence System Preferences page ' + 'in order to run DC Apps Performance Toolkit.') + + +def __check_current_language(confluence_api): + language = confluence_api.get_locale() + if language not in [ENGLISH_US, ENGLISH_GB]: + raise SystemExit(f'"{language}" language is not supported. ' + f'Please change your profile language to "English (US)"') + + def main(): print("Started preparing data") @@ -99,6 +126,10 @@ def main(): rest_client = ConfluenceRestClient(url, CONFLUENCE_SETTINGS.admin_login, CONFLUENCE_SETTINGS.admin_password) rpc_client = ConfluenceRpcClient(url, CONFLUENCE_SETTINGS.admin_login, CONFLUENCE_SETTINGS.admin_password) + __is_collaborative_editing_enabled(rest_client) + + __check_current_language(rest_client) + __is_remote_api_enabled(rest_client) dataset = __create_data_set(rest_client, rpc_client) diff --git a/app/util/data_preparation/jira/__init__.py b/app/util/data_preparation/jira/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/app/util/data_preparation/jira/prepare-data.py b/app/util/data_preparation/jira_prepare_data.py similarity index 70% rename from app/util/data_preparation/jira/prepare-data.py rename to app/util/data_preparation/jira_prepare_data.py index 4e0d24ec3..a9949770f 100644 --- a/app/util/data_preparation/jira/prepare-data.py +++ b/app/util/data_preparation/jira_prepare_data.py @@ -3,10 +3,11 @@ import urllib3 + from util.conf import JIRA_SETTINGS -from util.data_preparation.api.jira_clients import JiraRestClient +from util.api.jira_clients import JiraRestClient from util.project_paths import JIRA_DATASET_JQLS, JIRA_DATASET_SCRUM_BOARDS, JIRA_DATASET_KANBAN_BOARDS, \ - JIRA_DATASET_USERS, JIRA_DATASET_ISSUES, JIRA_DATASET_PROJECT_KEYS + JIRA_DATASET_USERS, JIRA_DATASET_ISSUES, JIRA_DATASET_PROJECTS urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) @@ -15,11 +16,13 @@ USERS = "users" ISSUES = "issues" JQLS = "jqls" -PROJECT_KEYS = "project_keys" -PROJECTS_COUNT_LIMIT = 1000 +PROJECTS = "projects" DEFAULT_USER_PASSWORD = 'password' DEFAULT_USER_PREFIX = 'performance_' +ERROR_LIMIT = 10 + +ENGLISH = 'en_US' def __generate_jqls(max_length=3, count=100): @@ -33,11 +36,15 @@ def __generate_jqls(max_length=3, count=100): def generate_perf_users(cur_perf_user, api): + errors_count = 0 config_perf_users_count = JIRA_SETTINGS.concurrency if len(cur_perf_user) >= config_perf_users_count: return cur_perf_user[:config_perf_users_count] else: while len(cur_perf_user) < config_perf_users_count: + if errors_count >= ERROR_LIMIT: + raise Exception(f'Maximum error limit reached {errors_count}/{ERROR_LIMIT}. ' + f'Please check the errors above') username = f"{DEFAULT_USER_PREFIX}{generate_random_string(10)}" try: user = api.create_user(name=username, password=DEFAULT_USER_PASSWORD) @@ -46,7 +53,8 @@ def generate_perf_users(cur_perf_user, api): cur_perf_user.append(user) # To avoid rate limit error from server. Execution should not be stopped after catch error from server. except Exception as error: - print(error) + print(f"{error}. Error limits {errors_count}/{ERROR_LIMIT}") + errors_count = errors_count + 1 print('All performance test users were successfully created') return cur_perf_user @@ -70,8 +78,8 @@ def write_test_data_to_files(datasets): issues = [f"{issue['key']},{issue['id']},{issue['key'].split('-')[0]}" for issue in datasets[ISSUES]] __write_to_file(JIRA_DATASET_ISSUES, issues) - keys = datasets[PROJECT_KEYS] - __write_to_file(JIRA_DATASET_PROJECT_KEYS, keys) + keys = datasets[PROJECTS] + __write_to_file(JIRA_DATASET_PROJECTS, keys) def __write_to_file(file_path, items): @@ -83,18 +91,25 @@ def __write_to_file(file_path, items): def __create_data_set(jira_api): dataset = dict() dataset[USERS] = __get_users(jira_api) - software_project_keys = __get_software_project_keys(jira_api, PROJECTS_COUNT_LIMIT) - dataset[PROJECT_KEYS] = software_project_keys - dataset[ISSUES] = __get_issues(jira_api, software_project_keys) + software_projects = __get_software_projects(jira_api) + dataset[PROJECTS] = software_projects + dataset[ISSUES] = __get_issues(jira_api, software_projects) dataset[SCRUM_BOARDS] = __get_boards(jira_api, 'scrum') dataset[KANBAN_BOARDS] = __get_boards(jira_api, 'kanban') dataset[JQLS] = __generate_jqls(count=150) + print(f'Users count: {len(dataset[USERS])}') + print(f'Projects: {len(dataset[PROJECTS])}') + print(f'Issues count: {len(dataset[ISSUES])}') + print(f'Scrum boards count: {len(dataset[SCRUM_BOARDS])}') + print(f'Kanban boards count: {len(dataset[KANBAN_BOARDS])}') + print(f'Jqls count: {len(dataset[JQLS])}') return dataset -def __get_issues(jira_api, software_project_keys): - jql_projects_str = ','.join(f'"{prj}"' for prj in software_project_keys) +def __get_issues(jira_api, software_projects): + project_keys = [f"{prj.split(',')[0]}" for prj in software_projects] + jql_projects_str = ','.join(f'"{prj_key}"' for prj_key in project_keys) issues = jira_api.issues_search( jql=f"project in ({jql_projects_str}) AND status != Closed order by key", max_results=8000 ) @@ -121,13 +136,21 @@ def __get_users(jira_api): return users -def __get_software_project_keys(jira_api, max_projects_count): +def __get_software_projects(jira_api): all_projects = jira_api.get_all_projects() - software_project_keys = [project['key'] for project in all_projects if 'software' == project.get('projectTypeKey')] - if not software_project_keys: + software_projects = \ + [f"{project['key']},{project['id']}" for project in all_projects if 'software' == project.get('projectTypeKey')] + if not software_projects: raise SystemExit("There are no software projects in Jira") # Limit number of projects to avoid "Request header is too large" for further requests. - return software_project_keys[:max_projects_count] + return software_projects + + +def __check_current_language(jira_api): + language = jira_api.get_locale() + if language != ENGLISH: + raise SystemExit(f'"{language}" language is not supported. ' + f'Please change your profile language to "English (United States) [Default]"') def main(): @@ -137,6 +160,9 @@ def main(): print("Server url: ", url) client = JiraRestClient(url, JIRA_SETTINGS.admin_login, JIRA_SETTINGS.admin_password) + + __check_current_language(client) + dataset = __create_data_set(client) write_test_data_to_files(dataset) diff --git a/app/util/jira/populate_db.sh b/app/util/jira/populate_db.sh index d078042b2..91e17c3fc 100644 --- a/app/util/jira/populate_db.sh +++ b/app/util/jira/populate_db.sh @@ -25,6 +25,11 @@ JIRA_DB_PASS="Password1!" # Jira version variables SUPPORTED_JIRA_VERSIONS=(8.0.3 7.13.6 8.5.0) JIRA_VERSION=$(sudo su jira -c "cat ${JIRA_VERSION_FILE}") +if [[ -z "$JIRA_VERSION" ]]; then + echo The $JIRA_VERSION_FILE file does not exists or emtpy. Please check if JIRA_VERSION_FILE variable \ + has a valid file path of the Jira version file or set your Cluster JIRA_VERSION explicitly. + exit 1 +fi echo "Jira Version: ${JIRA_VERSION}" # Datasets AWS bucket and db dump name @@ -35,7 +40,6 @@ DB_DUMP_URL="${DATASETS_AWS_BUCKET}/${JIRA_VERSION}/${DATASETS_SIZE}/${DB_DUMP_N ################### End of variables section ################### - # Check if Jira version is supported if [[ ! "${SUPPORTED_JIRA_VERSIONS[@]}" =~ "${JIRA_VERSION}" ]]; then echo "Jira Version: ${JIRA_VERSION} is not officially supported by Data Center App Performance Toolkit." @@ -100,7 +104,56 @@ else echo "Postgres client is already installed" fi -echo "Step2: Stop Jira" +echo "Step2: Get DB Host and check DB connection" +DB_HOST=$(sudo su -c "cat ${DB_CONFIG} | grep 'jdbc:postgresql' | cut -d'/' -f3 | cut -d':' -f1") +if [[ -z ${DB_HOST} ]]; then + echo "DataBase URL was not found in ${DB_CONFIG}" + exit 1 +fi +echo "DB_HOST=${DB_HOST}" + +echo "Check database connection" +PGPASSWORD=${JIRA_DB_PASS} pg_isready -U ${JIRA_DB_USER} -h ${DB_HOST} +if [[ $? -ne 0 ]]; then + echo "Connection to database failed. Please check correctness of following variables:" + echo "JIRA_DB_NAME=${JIRA_DB_NAME}" + echo "JIRA_DB_USER=${JIRA_DB_USER}" + echo "JIRA_DB_PASS=${JIRA_DB_PASS}" + echo "DB_HOST=${DB_HOST}" + exit 1 +fi + +echo "Step3: Write jira.baseurl property to file" +JIRA_BASE_URL_FILE="base_url" +if [[ -s ${JIRA_BASE_URL_FILE} ]]; then + echo "File ${JIRA_BASE_URL_FILE} was found. Base url: $(cat ${JIRA_BASE_URL_FILE})." +else + PGPASSWORD=${JIRA_DB_PASS} psql -h ${DB_HOST} -d ${JIRA_DB_NAME} -U ${JIRA_DB_USER} -Atc \ + "select propertyvalue from propertyentry PE + join propertystring PS on PE.id=PS.id + where PE.property_key = 'jira.baseurl';" > ${JIRA_BASE_URL_FILE} + if [[ ! -s ${JIRA_BASE_URL_FILE} ]]; then + echo "Failed to get Base URL value from database." + exit 1 + fi + echo "$(cat ${JIRA_BASE_URL_FILE}) was written to the ${JIRA_BASE_URL_FILE} file." +fi + +echo "Step4: Write jira license to file" +JIRA_LICENSE_FILE="license" +if [[ -s ${JIRA_LICENSE_FILE} ]]; then + echo "File ${JIRA_LICENSE_FILE} was found. License: $(cat ${JIRA_LICENSE_FILE})." + else + PGPASSWORD=${JIRA_DB_PASS} psql -h ${DB_HOST} -d ${JIRA_DB_NAME} -U ${JIRA_DB_USER} -Atc \ + "select license from productlicense;" > ${JIRA_LICENSE_FILE} + if [[ ! -s ${JIRA_LICENSE_FILE} ]]; then + echo "Failed to get jira license from database. Check DB configuration variables." + exit 1 + fi + echo "$(cat ${JIRA_LICENSE_FILE}) was written to the ${JIRA_LICENSE_FILE} file." +fi + +echo "Step5: Stop Jira" CATALINA_PID=$(pgrep -f "catalina") echo "CATALINA_PID=${CATALINA_PID}" if [[ -z ${CATALINA_PID} ]]; then @@ -132,7 +185,7 @@ else fi fi -echo "Step3: Download DB dump" +echo "Step6: Download database dump" rm -rf ${DB_DUMP_NAME} ARTIFACT_SIZE_BYTES=$(curl -sI ${DB_DUMP_URL} | grep "Content-Length" | awk {'print $2'} | tr -d '[:space:]') ARTIFACT_SIZE_GB=$((${ARTIFACT_SIZE_BYTES}/1024/1024/1024)) @@ -140,48 +193,30 @@ FREE_SPACE_KB=$(df -k --output=avail "$PWD" | tail -n1) FREE_SPACE_GB=$((${FREE_SPACE_KB}/1024/1024)) REQUIRED_SPACE_GB=$((5 + ${ARTIFACT_SIZE_GB})) if [[ ${FREE_SPACE_GB} -lt ${REQUIRED_SPACE_GB} ]]; then - echo "Not enough free space for download." - echo "Free space: ${FREE_SPACE_GB} GB" - echo "Required space: ${REQUIRED_SPACE_GB} GB" - exit 1 -fi; + echo "Not enough free space for download." + echo "Free space: ${FREE_SPACE_GB} GB" + echo "Required space: ${REQUIRED_SPACE_GB} GB" + exit 1 +fi # use computer style progress bar time wget --progress=dot:giga ${DB_DUMP_URL} if [[ $? -ne 0 ]]; then - echo "DB dump download failed! Pls check available disk space." + echo "Database dump download failed! Pls check available disk space." exit 1 fi -echo "Step4: Get DB Host" -DB_HOST=$(sudo su -c "cat ${DB_CONFIG} | grep 'jdbc:postgresql' | cut -d'/' -f3 | cut -d':' -f1") -if [[ -z ${DB_HOST} ]]; then - echo "DataBase URL was not found in ${DB_CONFIG}" - exit 1 -fi -echo "DB_HOST=${DB_HOST}" - -echo "Step5: SQL Restore" -echo "Check DB connection" -PGPASSWORD=${JIRA_DB_PASS} pg_isready -U ${JIRA_DB_USER} -h ${DB_HOST} -if [[ $? -ne 0 ]]; then - echo "Connection to DB failed. Please check correctness of following variables:" - echo "JIRA_DB_NAME=${JIRA_DB_NAME}" - echo "JIRA_DB_USER=${JIRA_DB_USER}" - echo "JIRA_DB_PASS=${JIRA_DB_PASS}" - echo "DB_HOST=${DB_HOST}" - exit 1 -fi -echo "Drop DB" +echo "Step7: SQL Restore" +echo "Drop database" PGPASSWORD=${JIRA_DB_PASS} dropdb -U ${JIRA_DB_USER} -h ${DB_HOST} ${JIRA_DB_NAME} if [[ $? -ne 0 ]]; then echo "Drop DB failed." exit 1 fi sleep 5 -echo "Create DB" +echo "Create database" PGPASSWORD=${JIRA_DB_PASS} createdb -U ${JIRA_DB_USER} -h ${DB_HOST} -T template0 -E "UNICODE" -l "C" ${JIRA_DB_NAME} if [[ $? -ne 0 ]]; then - echo "Create DB failed." + echo "Create database failed." exit 1 fi sleep 5 @@ -192,12 +227,60 @@ if [[ $? -ne 0 ]]; then exit 1 fi -echo "Step6: Start Jira" +echo "Step8: Update jira.baseurl property in database" +if [[ -s ${JIRA_BASE_URL_FILE} ]]; then + BASE_URL=$(cat $JIRA_BASE_URL_FILE) + if [[ $(PGPASSWORD=${JIRA_DB_PASS} psql -h ${DB_HOST} -d ${JIRA_DB_NAME} -U ${JIRA_DB_USER} -c \ + "update propertystring + set propertyvalue = '${BASE_URL}' + from propertyentry PE + where PE.id=propertystring.id + and PE.property_key = 'jira.baseurl';") != "UPDATE 1" ]]; then + echo "Couldn't update database jira.baseurl property. Please check your database connection." + exit 1 + else + echo "The database jira.baseurl property was updated with ${BASE_URL}" + fi +else + echo "The ${JIRA_BASE_URL_FILE} file doesn't exist or empty. Please check file existence or 'jira.baseurl' property in the database." + exit 1 +fi + +echo "Step9: Update jira license in database" +if [[ -s ${JIRA_LICENSE_FILE} ]]; then + LICENSE=$(cat ${JIRA_LICENSE_FILE}) + LICENSE_ID=$(PGPASSWORD=${JIRA_DB_PASS} psql -h ${DB_HOST} -d ${JIRA_DB_NAME} -U ${JIRA_DB_USER} -Atc \ + "select id from productlicense;") + if [[ -z "${LICENSE_ID}" ]]; then + echo "License update failed. License id value in the database is empty." + exit 1 + fi + if [[ $(PGPASSWORD=${JIRA_DB_PASS} psql -h ${DB_HOST} -d ${JIRA_DB_NAME} -U ${JIRA_DB_USER} -c \ + "update productlicense + set license = '${LICENSE}' + where id = '${LICENSE_ID}';") != "UPDATE 1" ]]; then + echo "Couldn't update database jira license. Please check your database connection." + exit 1 + else + echo "The database jira license was updated with ${LICENSE}" + fi +else + echo "The ${JIRA_LICENSE_FILE} file doesn't exist or empty. Please check file existence or jira license in the database." + exit 1 +fi + +echo "Step10: Start Jira" sudo su jira -c "${START_JIRA}" rm -rf ${DB_DUMP_NAME} +echo "Step11: Remove ${JIRA_BASE_URL_FILE} file" +sudo rm ${JIRA_BASE_URL_FILE} + +echo "Step12: Remove ${JIRA_LICENSE_FILE} file" +sudo rm ${JIRA_LICENSE_FILE} + echo "Finished" -echo # move to a new line +echo # move to a new line echo "Important: new admin user credentials are admin/admin" -echo "Wait a couple of minutes until Jira is started." \ No newline at end of file +echo "Wait a couple of minutes until Jira is started." diff --git a/app/util/jira/upload_attachments.sh b/app/util/jira/upload_attachments.sh index 4bcd04bef..56d7977b0 100644 --- a/app/util/jira/upload_attachments.sh +++ b/app/util/jira/upload_attachments.sh @@ -6,6 +6,11 @@ JIRA_VERSION_FILE="/media/atl/jira/shared/jira-software.version" SUPPORTED_JIRA_VERSIONS=(8.0.3 7.13.6 8.5.0) JIRA_VERSION=$(sudo su jira -c "cat ${JIRA_VERSION_FILE}") +if [[ -z "$JIRA_VERSION" ]]; then + echo The $JIRA_VERSION_FILE file does not exists or emtpy. Please check if JIRA_VERSION_FILE variable \ + has a valid file path of the Jira version file or set your Cluster JIRA_VERSION explicitly. + exit 1 +fi echo "Jira Version: ${JIRA_VERSION}" DATASETS_AWS_BUCKET="https://centaurus-datasets.s3.amazonaws.com/jira" @@ -17,6 +22,12 @@ TMP_DIR="/tmp" EFS_DIR="/media/atl/jira/shared/data" ################### End of variables section ################### +if [[ ! `systemctl status jira` ]]; then + echo "The Jira service was not found on this host." \ + "Please make sure you are running this script on a host that is running Jira." + exit 1 +fi + # Check if Jira version is supported if [[ ! "${SUPPORTED_JIRA_VERSIONS[@]}" =~ "${JIRA_VERSION}" ]]; then echo "Jira Version: ${JIRA_VERSION} is not officially supported by Data Center App Performance Toolkit." diff --git a/app/util/jtl_convertor/jtl_validator.py b/app/util/jtl_convertor/jtl_validator.py index 143f9a145..ebfbae40f 100644 --- a/app/util/jtl_convertor/jtl_validator.py +++ b/app/util/jtl_convertor/jtl_validator.py @@ -20,9 +20,9 @@ LABEL = 'label' ELAPSED = 'elapsed' TIME_STAMP = 'timeStamp' +METHOD = 'method' -SUPPORTED_JTL_HEADER: List[str] = [TIME_STAMP, ELAPSED, LABEL, RESPONSE_CODE, RESPONSE_MESSAGE, THREAD_NAME, - SUCCESS, BYTES, GRP_THREADS, ALL_THREADS, LATENCY, HOSTNAME, CONNECT] +SUPPORTED_JTL_HEADER: List[str] = [TIME_STAMP, ELAPSED, LABEL, SUCCESS] VALIDATION_FUNCS_BY_COLUMN: Dict[str, List[FunctionType]] = { TIME_STAMP: [is_not_none, is_number], @@ -38,6 +38,7 @@ LATENCY: [], HOSTNAME: [], CONNECT: [], + METHOD: [], } @@ -63,9 +64,11 @@ def __validate_row(jtl_row: Dict) -> None: __validate_value(column, str(value)) -def __validate_header(header: List) -> None: - if not (SUPPORTED_JTL_HEADER == header): - __raise_validation_error(f"Header is not correct. Supported header is {SUPPORTED_JTL_HEADER}") +def __validate_header(headers: List) -> None: + for header in SUPPORTED_JTL_HEADER: + if header not in headers: + __raise_validation_error(f"Headers is not correct. Required headers is {SUPPORTED_JTL_HEADER}. " + f"{header} is missed") def __raise_validation_error(error_msg: str) -> None: @@ -83,12 +86,12 @@ def __validate_rows(reader) -> None: def validate(file_path: Path) -> None: print(f'Started validating jtl file: {file_path}') start_time = time.time() - try: with file_path.open(mode='r') as f: reader: DictReader = DictReader(f) __validate_header(reader.fieldnames) __validate_rows(reader) + except (ValidationException, FileNotFoundError) as e: raise SystemExit(f"ERROR: Validation failed. File path: [{file_path}]. Validation details: {str(e)}") diff --git a/app/util/jtl_convertor/jtls-to-csv.py b/app/util/jtl_convertor/jtls-to-csv.py index fb8120ef1..54b48f3e4 100644 --- a/app/util/jtl_convertor/jtls-to-csv.py +++ b/app/util/jtl_convertor/jtls-to-csv.py @@ -4,19 +4,31 @@ import time from pathlib import Path from typing import IO, List, Set +import csv +import pandas from util.jtl_convertor import jtl_validator - +from util.project_paths import ENV_TAURUS_ARTIFACT_DIR + +LABEL = 'Label' +SAMPLES = '# Samples' +AVERAGE = 'Average' +MEDIAN = 'Median' +PERC_90 = '90% Line' +PERC_95 = '95% Line' +PERC_99 = '99% Line' +MIN = 'Min' +MAX = 'Max' +ERROR_RATE = 'Error %' +LABEL_JTL = 'label' +ELAPSED_JTL_TMP = 'elapsed_tmp' +ELAPSED_JTL = 'elapsed' +SUCCESS_JTL = 'success' +SUCCESS_JTL_TMP = 'success_tmp' +FALSE_JTL = 'false' + +CSV_HEADER = f'{LABEL},{SAMPLES},{AVERAGE},{MEDIAN},{PERC_90},{PERC_95},{PERC_99},{MIN},{MAX},{ERROR_RATE}\n' RESULTS_CSV_NAME = 'results.csv' -ENV_JMETER_VERSION = 'JMETER_VERSION' -ENV_TAURUS_ARTIFACT_DIR = 'TAURUS_ARTIFACTS_DIR' -TEMPLATE_PLUGIN_COMMAND = 'java -Djava.awt.headless=true -jar {libs_home}cmdrunner-2.2.jar ' \ - '--tool Reporter ' \ - '--tool Reporter --generate-csv {output_csv} ' \ - '--input-jtl "{input_jtl}" ' \ - '--plugin-type AggregateReport' -CSV_HEADER = 'Label,# Samples,Average,Median,90% Line,95% Line,99% Line,' \ - 'Min,Max,Error %,Throughput,Received KB/sec,Std. Dev.\n' def __count_file_lines(stream: IO) -> int: @@ -27,30 +39,15 @@ def __reset_file_stream(stream: IO) -> None: stream.seek(0) -def __convert_jtl_to_csv(input_file_path: Path, output_file_path: Path, jmeter_libs_home: Path) -> None: +def __convert_jtl_to_csv(input_file_path: Path, output_file_path: Path) -> None: if not input_file_path.exists(): raise SystemExit(f'Input file {output_file_path} does not exist') - - command = TEMPLATE_PLUGIN_COMMAND.format(libs_home=str(jmeter_libs_home) + os.path.sep, - output_csv=output_file_path, - input_jtl=input_file_path) - print(os.popen(command).read()) + start = time.time() + convert_to_csv(output_csv=output_file_path, input_jtl=input_file_path) if not output_file_path.exists(): raise SystemExit(f'Something went wrong. Output file {output_file_path} does not exist') - print(f'Created file {output_file_path}') - - -def __get_jmeter_home() -> Path: - jmeter_version = os.getenv(ENV_JMETER_VERSION) - if jmeter_version is None: - raise SystemExit(f'Error: env variable {ENV_JMETER_VERSION} is not set') - - return Path().home() / '.bzt' / 'jmeter-taurus' / jmeter_version - - -def __get_jmeter_lib_dir() -> Path: - return __get_jmeter_home() / 'lib' + print(f'Created file {output_file_path}. Converted from jtl to csv in {time.time() - start} ') def __change_file_extension(file_name: str, new_extension) -> str: @@ -61,13 +58,12 @@ def __get_file_name_without_extension(file_name): return os.path.splitext(file_name)[0] -def __read_csv_without_first_and_last_line(results_file_stream, input_csv): +def __read_csv_without_first_line(results_file_stream, input_csv): with input_csv.open(mode='r') as file_stream: - lines_number: int = __count_file_lines(file_stream) __reset_file_stream(file_stream) for cnt, line in enumerate(file_stream, 1): - if cnt != 1 and cnt != lines_number: + if cnt != 1: results_file_stream.write(line) print(f'File {input_csv} successfully read') @@ -77,7 +73,7 @@ def __create_results_csv(csv_list: List[Path], results_file_path: Path) -> None: results_file_stream.write(CSV_HEADER) for temp_csv_path in csv_list: - __read_csv_without_first_and_last_line(results_file_stream, temp_csv_path) + __read_csv_without_first_line(results_file_stream, temp_csv_path) if not results_file_path.exists(): raise SystemExit(f'Something went wrong. Output file {results_file_path} does not exist') @@ -98,25 +94,67 @@ def __validate_file_names(file_names: List[str]): file_names_set.add(file_name_without_extension) +def convert_to_csv(input_jtl: Path, output_csv: Path): + reader = csv.DictReader(input_jtl.open(mode='r')) + + jtl_list = [row for row in reader] + csv_list = [] + + for jtl_sample in jtl_list: + sample = {} + if jtl_sample[LABEL_JTL] not in [processed_sample[LABEL] for processed_sample in csv_list]: + sample[LABEL] = jtl_sample[LABEL_JTL] + sample[SAMPLES] = 1 + sample[ELAPSED_JTL_TMP] = [int(jtl_sample[ELAPSED_JTL])] # Temp list with 'elapsed' value for current label + sample[SUCCESS_JTL_TMP] = [jtl_sample[SUCCESS_JTL]] # Temp list with 'success' value for current label + csv_list.append(sample) + + else: + # Get and update processed row with current label + processed_sample = [row for row in csv_list if row[LABEL] == jtl_sample['label']][0] + processed_sample[SAMPLES] = processed_sample[SAMPLES] + 1 # Count samples + processed_sample[ELAPSED_JTL_TMP].append(int(jtl_sample[ELAPSED_JTL])) # list of elapsed values + processed_sample[SUCCESS_JTL_TMP].append(jtl_sample[SUCCESS_JTL]) # list of success values + + # Calculation after the last row in kpi.jtl is processed + if jtl_sample == jtl_list[-1]: + for processed_sample in csv_list: + elapsed_df = pandas.Series(processed_sample[ELAPSED_JTL_TMP]) + processed_sample[AVERAGE] = int(round(elapsed_df.mean())) + processed_sample[MEDIAN] = int(round(elapsed_df.quantile(0.5))) + processed_sample[PERC_90] = int(round(elapsed_df.quantile(0.9))) + processed_sample[PERC_95] = int(round(elapsed_df.quantile(0.95))) + processed_sample[PERC_99] = int(round(elapsed_df.quantile(0.99))) + processed_sample[MIN] = min(processed_sample[ELAPSED_JTL_TMP]) + processed_sample[MAX] = max(processed_sample[ELAPSED_JTL_TMP]) + + success_list = processed_sample[SUCCESS_JTL_TMP] + processed_sample[ERROR_RATE] = round(success_list.count(FALSE_JTL) / len(success_list), 2) * 100.00 + del processed_sample[SUCCESS_JTL_TMP] + del processed_sample[ELAPSED_JTL_TMP] + + headers = csv_list[0].keys() + with output_csv.open('w') as output_file: + dict_writer = csv.DictWriter(output_file, headers) + dict_writer.writeheader() + for row in csv_list: + dict_writer.writerow(row) + + def main(): file_names = sys.argv[1:] __validate_file_names(file_names) - artifacts_dir: str = os.getenv(ENV_TAURUS_ARTIFACT_DIR) - if artifacts_dir is None: - raise SystemExit(f'Error: env variable {ENV_TAURUS_ARTIFACT_DIR} is not set') - artifacts_dir_path = Path(artifacts_dir) with tempfile.TemporaryDirectory() as tmp_dir: - jmeter_lib_dir = __get_jmeter_lib_dir() temp_csv_list: List[Path] = [] for file_name in file_names: - jtl_file_path = artifacts_dir_path / file_name + jtl_file_path = ENV_TAURUS_ARTIFACT_DIR / file_name jtl_validator.validate(jtl_file_path) csv_file_path = Path(tmp_dir) / __change_file_extension(file_name, '.csv') - __convert_jtl_to_csv(jtl_file_path, csv_file_path, jmeter_lib_dir) + __convert_jtl_to_csv(jtl_file_path, csv_file_path) temp_csv_list.append(csv_file_path) - results_file_path = artifacts_dir_path / RESULTS_CSV_NAME + results_file_path = ENV_TAURUS_ARTIFACT_DIR / RESULTS_CSV_NAME __create_results_csv(temp_csv_list, results_file_path) diff --git a/app/util/data_preparation/bitbucket/__init__.py b/app/util/post_run/__init__.py similarity index 100% rename from app/util/data_preparation/bitbucket/__init__.py rename to app/util/post_run/__init__.py diff --git a/app/util/post_run/cleanup_results_dir.py b/app/util/post_run/cleanup_results_dir.py new file mode 100644 index 000000000..cfb5f653f --- /dev/null +++ b/app/util/post_run/cleanup_results_dir.py @@ -0,0 +1,20 @@ +import os +from util.project_paths import ENV_TAURUS_ARTIFACT_DIR + +FILES_TO_REMOVE = ['jmeter.out', + 'jmeter-bzt.properties', + 'merged.json', + 'merged.yml', + 'PyTestExecutor.ldjson', + 'system.properties', + 'locust.out'] + +for file in FILES_TO_REMOVE: + file_path = ENV_TAURUS_ARTIFACT_DIR / file + if file_path.exists(): + try: + os.remove(file_path) + print(f'The {file} was removed successfully') + except OSError as e: + print(f'Deleting of the {file} failed!\n' + f'Error: {file_path}: {e.strerror}') diff --git a/app/util/jmeter_post_check.py b/app/util/post_run/jmeter_post_check.py similarity index 69% rename from app/util/jmeter_post_check.py rename to app/util/post_run/jmeter_post_check.py index 6f1faa517..271652ef4 100644 --- a/app/util/jmeter_post_check.py +++ b/app/util/post_run/jmeter_post_check.py @@ -1,18 +1,13 @@ import os from pathlib import Path from shutil import rmtree +from util.project_paths import ENV_TAURUS_ARTIFACT_DIR -ENV_TAURUS_ARTIFACT_DIR = 'TAURUS_ARTIFACTS_DIR' JMETER_JTL_FILE_NAME = 'kpi.jtl' -artifacts_dir = os.getenv(ENV_TAURUS_ARTIFACT_DIR) -if artifacts_dir is None: - raise SystemExit(f'Error: env variable {ENV_TAURUS_ARTIFACT_DIR} is not set') - jmeter_home_path = Path().home() / '.bzt' / 'jmeter-taurus' - -jmeter_jtl_file = f"{artifacts_dir}/{JMETER_JTL_FILE_NAME}" +jmeter_jtl_file = ENV_TAURUS_ARTIFACT_DIR / JMETER_JTL_FILE_NAME if not os.path.exists(jmeter_jtl_file): if jmeter_home_path.exists(): diff --git a/app/util/data_preparation/confluence/__init__.py b/app/util/pre_run/__init__.py similarity index 100% rename from app/util/data_preparation/confluence/__init__.py rename to app/util/pre_run/__init__.py diff --git a/app/util/environment_checker.py b/app/util/pre_run/environment_checker.py similarity index 90% rename from app/util/environment_checker.py rename to app/util/pre_run/environment_checker.py index 8e19ac680..f8e444dd1 100644 --- a/app/util/environment_checker.py +++ b/app/util/pre_run/environment_checker.py @@ -11,6 +11,6 @@ # Print toolkit version after Python check -from util.conf import TOOLKIT_VERSION +from util.conf import TOOLKIT_VERSION # noqa E402 print("Data Center App Performance Toolkit version: {}".format(TOOLKIT_VERSION)) diff --git a/app/util/git_client_check.py b/app/util/pre_run/git_client_check.py similarity index 100% rename from app/util/git_client_check.py rename to app/util/pre_run/git_client_check.py diff --git a/app/util/project_paths.py b/app/util/project_paths.py index c9ff20db0..d1ea45557 100644 --- a/app/util/project_paths.py +++ b/app/util/project_paths.py @@ -1,3 +1,5 @@ +import datetime +import os from pathlib import Path @@ -41,6 +43,16 @@ def __get_bitbucket_dataset(file_name): return __get_bitbucket_datasets() / file_name +def __get_taurus_artifacts_dir(): + if 'TAURUS_ARTIFACTS_DIR' in os.environ: + return Path(os.environ.get('TAURUS_ARTIFACTS_DIR')) + else: + results_dir_name = datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S") + local_run_results = Path(f'results/{results_dir_name}_local') + local_run_results.mkdir(parents=True) + return local_run_results + + JIRA_YML = __get_jira_yml() JIRA_DATASETS = __get_jira_datasets() JIRA_DATASET_JQLS = __get_jira_dataset('jqls.csv') @@ -48,13 +60,14 @@ def __get_bitbucket_dataset(file_name): JIRA_DATASET_KANBAN_BOARDS = __get_jira_dataset('kanban-boards.csv') JIRA_DATASET_USERS = __get_jira_dataset('users.csv') JIRA_DATASET_ISSUES = __get_jira_dataset('issues.csv') -JIRA_DATASET_PROJECT_KEYS = __get_jira_dataset('project_keys.csv') +JIRA_DATASET_PROJECTS = __get_jira_dataset('projects.csv') CONFLUENCE_YML = __get_confluence_yml() CONFLUENCE_DATASETS = __get_confluence_datasets() CONFLUENCE_USERS = __get_confluence_dataset('users.csv') CONFLUENCE_PAGES = __get_confluence_dataset('pages.csv') CONFLUENCE_BLOGS = __get_confluence_dataset('blogs.csv') +CONFLUENCE_STATIC_CONTENT = __get_confluence_dataset('static-content/files_upload.csv') BITBUCKET_YML = __get_bitbucket_yml() BITBUCKET_DATASETS = __get_bitbucket_datasets() @@ -62,3 +75,5 @@ def __get_bitbucket_dataset(file_name): BITBUCKET_PROJECTS = __get_bitbucket_dataset('projects.csv') BITBUCKET_REPOS = __get_bitbucket_dataset('repos.csv') BITBUCKET_PRS = __get_bitbucket_dataset('pull_requests.csv') + +ENV_TAURUS_ARTIFACT_DIR = __get_taurus_artifacts_dir() diff --git a/docs/bitbucket/README.md b/docs/bitbucket/README.md index 7a6032a15..e05cc0ad3 100644 --- a/docs/bitbucket/README.md +++ b/docs/bitbucket/README.md @@ -51,15 +51,15 @@ Be sure to run this command inside the `app` directory. The main [bitbucket.jmx] ### Debugging JMeter scripts 1. Open JMeter GUI from `app` directory by running the `~/.bzt/jmeter-taurus/<jmeter_version>/bin/jmeter` command. -2. Right-click `Test Plan` > `Add` > `Listener` > `View Results Tree`. -3. On the `View Results Tree` page, click the `Browse` button and open `error.jtl` from `app/results/bitbucket/YY-MM-DD-hh-mm-ss` folder. +1. Right-click `Test Plan` > `Add` > `Listener` > `View Results Tree`. +1. On the `View Results Tree` page, click the `Browse` button and open `error.jtl` from `app/results/bitbucket/YY-MM-DD-hh-mm-ss` folder. From this view, you can click on any failed action and see the request and response data in appropriate tabs. In addition, you can run and monitor JMeter test real-time with GUI. 1. Launch the test with GUI by running `bzt bitbucket.yml -gui`. -2. Right-click `Test Plan` > `Add` > `Listener` > `View Results Tree`. -3. Click the start button to start running the test. +1. Right-click `Test Plan` > `Add` > `Listener` > `View Results Tree`. +1. Click the start button to start running the test. ## Selenium ### Debugging Selenium scripts @@ -70,15 +70,15 @@ Also, screenshots and HTMLs of Selenium fails are stared in the `results/bitbuck ### Running Selenium tests with Browser GUI There are two options of running Selenium tests with browser GUI: 1. In [bitbucket.yml](../../app/bitbucket.yml) file, set the `WEBDRIVER_VISIBLE: True`. -2. Set environment variable with the `export WEBDRIVER_VISIBLE=True` command. +1. Set environment variable with the `export WEBDRIVER_VISIBLE=True` command. ### Running Selenium tests locally without the Performance Toolkit 1. Activate virualenv for the Performance Toolkit. -2. Navigate to the selenium folder using the `cd app/selenium_ui` command. -3. Set browser visibility using the `export WEBDRIVER_VISIBLE=True` command. -4. Run all Selenium PyTest tests with the `pytest bitbucket-ui.py` command. -5. To run one Selenium PyTest test (e.g., `test_1_selenium_view_dashboard`), execute the first login test and the required one with this command: +1. Navigate to the selenium folder using the `cd app/selenium_ui` command. +1. Set browser visibility using the `export WEBDRIVER_VISIBLE=True` command. +1. Run all Selenium PyTest tests with the `pytest bitbucket-ui.py` command. +1. To run one Selenium PyTest test (e.g., `test_1_selenium_view_dashboard`), execute the first login test and the required one with this command: `pytest bitbucket-ui.py::test_0_selenium_a_login bitbucket-ui.py::test_1_selenium_view_dashboard`. diff --git a/docs/confluence/README.md b/docs/confluence/README.md index b35ce1dca..b374c5682 100644 --- a/docs/confluence/README.md +++ b/docs/confluence/README.md @@ -21,6 +21,8 @@ For spiking, testing, or developing, your local Confluence instance would work w * `concurrency`: number of concurrent users for JMeter scenario * `test_duration`: duration of test execution (45m is by default) * `WEBDRIVER_VISIBLE`: visibility of Chrome browser during selenium execution (False is by default) +* `load_executor`: `jmeter` or `locust` load executor. `jmeter` is using by default. + ## Step 2: Run tests Run Taurus. @@ -33,6 +35,7 @@ Results are located in the `resutls/confluence/YY-MM-DD-hh-mm-ss` directory: * `bzt.log` - log of bzt run * `error_artifacts` - folder with screenshots and HTMLs of Selenium fails * `jmeter.err` - JMeter errors log +* `locust.err` - Locust errors log * `kpi.jtl` - JMeter raw data * `pytest.out` - detailed log of Selenium execution, including stacktraces of Selenium fails * `selenium.jtl` - Selenium raw data @@ -43,41 +46,66 @@ next steps. # Useful information -## Jmeter -### Changing JMeter workload -The [confluence.yml](../../app/confluence.yml) has a workload section with `perc_action_name` fields. You can change values from 0 to 100 to increase/decrease execution frequency of certain actions. +## Changing performance workload for JMeter and Locust +The [confluence.yml](../../app/confluence.yml) has `action_name` field in `env` section with percentage for each action. You can change values from 0 to 100 to increase/decrease execution frequency of certain actions. The percentages must add up to 100, if you want to ensure the performance script maintains -throughput defined in `total_actions_per_hr`. The default load simulates an enterprise scale load of 54500 user transactions per hour at 200 concurrency. +throughput defined in `total_actions_per_hr`. The default load simulates an enterprise scale load of 20000 user transactions per hour at 200 concurrency. To simulate a load of medium-sized customers, `total_actions_per_hr` and `concurrency` can be reduced to 14000 transactions and 70 users. This can be further halved for a small customer. +## JMeter ### Opening JMeter scripts JMeter is written in XML and requires the JMeter GUI to view and make changes. You can launch JMeter GUI by running the `~/.bzt/jmeter-taurus/<jmeter_version>/bin/jmeter` command. Be sure to run this command inside the `app` directory. The main [confluence.jmx](../../app/jmeter/confluence.jmx) file contains the relative path to other scripts and will throw errors if run elsewhere. ### Debugging JMeter scripts 1. Open JMeter GUI from `app` directory by running the `~/.bzt/jmeter-taurus/<jmeter_version>/bin/jmeter` command. -2. Right-click `Test Plan` > `Add` > `Listener` > `View Results Tree`. -3. On the `View Results Tree` page, click the `Browse` button and open `error.jtl` from `app/results/confluence/YY-MM-DD-hh-mm-ss` folder. +1. Right-click `Test Plan` > `Add` > `Listener` > `View Results Tree`. +1. On the `View Results Tree` page, click the `Browse` button and open `error.jtl` from `app/results/confluence/YY-MM-DD-hh-mm-ss` folder. From this view, you can click on any failed action and see the request and response data in appropriate tabs. In addition, you can run and monitor JMeter test real-time with GUI. 1. Launch the test with GUI by running `bzt confluence.yml -gui`. -2. Right-click `Test Plan` > `Add` > `Listener` > `View Results Tree`. -3. Click the start button to start running the test. +1. Right-click `Test Plan` > `Add` > `Listener` > `View Results Tree`. +1. Click the start button to start running the test. ### Run one JMeter action ####Option 1: Run one JMeter action via GUI 1. Open JMeter GUI from `app` directory by running the `~/.bzt/jmeter-taurus/<jmeter_version>/bin/jmeter` command. -2. Go to `File` > `Open`, and then open `jmeter/confluence.jmx`. -2. In the`Global Variables` section, add correct confluence hostname, port, protocol, and postfix (if required). -3. In `confluence` > `load profile`, set `perc_desired_action` to 100. -4. Run JMeter. +1. Go to `File` > `Open`, and then open `jmeter/confluence.jmx`. +1. In the `Global Variables` section, add correct confluence hostname, port, protocol, and postfix (if required). +1. In `confluence` > `load profile`, set `perc_desired_action` to 100. +1. Enable `View Results Tree` controller. +1. Run JMeter. +1. `View Results Tree` controller will have all details for every request and corresponding response. ####Option 2: Run one JMeter action via bzt 1. In [confluence.yml](../../app/confluence.yml), set `perc_desired_action` to 100 and all other perc_* to 0. -2. Run `bzt confluence.yml`. +1. Run `bzt confluence.yml`. + +## Locust +### Debugging Locust scripts +Detailed log of Locust executor is located in the `results/confluence/YY-MM-DD-hh-mm-ss/locust.log` file. Locust errors and stacktrace are located in the `results/confluence/YY-MM-DD-hh-mm-ss/locust.err` file. + +Additional debug information could be enabled by setting `verbose` flag to `true` in `confluence.yml` configuration file. To add log message use `logger.locust_info('your INFO message')` string in the code. +### Running Locust tests locally without the Performance Toolkit +#### Start locust UI mode +1. Activate virualenv for the Performance Toolkit. +1. Navigate to `app` directory and execute command `locust --locustfile locustio/confluence/locustfile.py`. +1. Open your browser, navigate to `localhost:8089`. +1. Enter `Number of total users to simulate` (`1` is recommended value for debug purpose) +1. Enter `Hatch rate (users spawned/secods)` +1. Press `Start spawning` button. + +#### Start Locust console mode +1. Activate virualenv for the Performance Toolkit. +1. Navigate to `app` and execute command `locust --no-web --locustfile locustio/confluence/locustfile.py --clients N --hatch-rate R`, where `N` is the number of total users to simulate and `R` is the hatch rate. + +Full logs of local run you can find in the `results/confluence/YY-MM-DD-hh-mm-ss_local/` directory. + +To execute one locust action, navigate to `confluence.yml` and set percentage value `100` to the action you would like to run separately, set percentage value `0` to all other actions. + ## Selenium ### Debugging Selenium scripts @@ -88,17 +116,17 @@ Also, screenshots and HTMLs of Selenium fails are stared in the `results/conflue ### Running Selenium tests with Browser GUI There are two options of running Selenium tests with browser GUI: 1. In [confluence.yml](../../app/confluence.yml) file, set the `WEBDRIVER_VISIBLE: True`. -2. Set environment variable with the `export WEBDRIVER_VISIBLE=True` command. +1. Set environment variable with the `export WEBDRIVER_VISIBLE=True` command. ### Running Selenium tests locally without the Performance Toolkit 1. Activate virualenv for the Performance Toolkit. -2. Navigate to the selenium folder using the `cd app/selenium_ui` command. -3. Set browser visibility using the `export WEBDRIVER_VISIBLE=True` command. -4. Run all Selenium PyTest tests with the `pytest confluence-ui.py` command. -5. To run one Selenium PyTest test (e.g., `test_1_selenium_view_page`), execute the first login test and the required one with this command: +1. Navigate to the selenium folder using the `cd app/selenium_ui` command. +1. Set browser visibility using the `export WEBDRIVER_VISIBLE=True` command. +1. Run all Selenium PyTest tests with the `pytest confluence_ui.py` command. +1. To run one Selenium PyTest test (e.g., `test_1_selenium_view_page`), execute the first login test and the required one with this command: -`pytest confluence-ui.py::test_0_selenium_a_login confluence-ui.py::test_1_selenium_view_page`. +`pytest confluence_ui.py::test_0_selenium_a_login confluence_ui.py::test_1_selenium_view_page`. ### Comparing different runs diff --git a/docs/dc-apps-performance-toolkit-user-guide-bitbucket.md b/docs/dc-apps-performance-toolkit-user-guide-bitbucket.md index 84b266356..ca6354328 100644 --- a/docs/dc-apps-performance-toolkit-user-guide-bitbucket.md +++ b/docs/dc-apps-performance-toolkit-user-guide-bitbucket.md @@ -8,19 +8,20 @@ date: "2020-02-13" --- # Data Center App Performance Toolkit User Guide For Bitbucket -To use the Data Center App Performance Toolkit, you'll need to first clone its repo. +This document walks you through the process of testing your app on Bitbucket using the Data Center App Performance Toolkit. These instructions focus on producing the required [performance and scale benchmarks for your Data Center app](https://developer.atlassian.com/platform/marketplace/dc-apps-performance-and-scale-testing/). -``` bash -git clone git@github.com:atlassian/dc-app-performance-toolkit.git -``` +To use the Data Center App Performance Toolkit, you'll need to: -Follow installation instructions described in the `dc-app-performance-toolkit/README.md` file. +1. [Set up Bitbucket Data Center on AWS](#instancesetup). +1. [Load an enterprise-scale dataset on your Bitbucket Data Center deployment](#preloading). +1. [Set up an execution environment for the toolkit](#executionhost). +1. [Run all the testing scenarios in the toolkit](#testscenario). -If you need performance testing results at a production level, follow instructions in this chapter to set up Bitbucket Data Center with the corresponding dataset. - -For spiking, testing, or developing, your local Bitbucket instance would work well. Thus, you can skip this chapter and proceed with [Testing scenarios](/platform/marketplace/dc-apps-performance-toolkit-user-guide-bitbucket/#testing-scenarios). Still, script adjustments for your local dataset may be required. +{{% note %}} +For simple spikes or tests, you can skip steps 1-2 and target any Bitbucket test instance. When you [set up your execution environment](#executionhost), you may need to edit the scripts according to your test instance's data set. +{{% /note %}} -## Setting up Bitbucket Data Center +## <a id="instancesetup"></a> Setting up Bitbucket Data Center We recommend that you use the [AWS Quick Start for Bitbucket Data Center](https://aws.amazon.com/quickstart/architecture/bitbucket/) to deploy a Bitbucket Data Center testing environment. This Quick Start will allow you to deploy Bitbucket Data Center with a new [Atlassian Standard Infrastructure](https://aws.amazon.com/quickstart/architecture/atlassian-standard-infrastructure/) (ASI) or into an existing one. @@ -58,10 +59,11 @@ All important parameters are listed and described in this section. For all other | Parameter | Recommended Value | | --------- | ----------------- | -| Version | 6.10.0 | +| Version | 6.10.0 or 7.0.0 | The Data Center App Performance Toolkit officially supports: +- Bitbucket Platform Release version: 7.0.0 - Bitbucket [Enterprise Releases](https://confluence.atlassian.com/enterprise/atlassian-enterprise-releases-948227420.html): 6.10.0 **Cluster nodes** @@ -230,7 +232,7 @@ To populate the database with SQL: ``` bash INSTALL_PSQL_CMD="amazon-linux-extras install -y postgresql10" DB_CONFIG="/media/atl/bitbucket/shared/bitbucket.properties" - + # Depending on BITBUCKET installation directory BITBUCKET_CURRENT_DIR="/opt/atlassian/bitbucket/current/" BITBUCKET_VERSION_FILE="/media/atl/bitbucket/shared/bitbucket.version" @@ -239,7 +241,7 @@ To populate the database with SQL: BITBUCKET_DB_NAME="bitbucket" BITBUCKET_DB_USER="postgres" BITBUCKET_DB_PASS="Password1!" - + # Datasets AWS bucket and db dump name DATASETS_AWS_BUCKET="https://centaurus-datasets.s3.amazonaws.com/bitbucket" DATASETS_SIZE="large" @@ -296,7 +298,7 @@ After [Importing the main dataset](#importingdataset), you'll now have to pre-lo {{% note %}} Do not close or interrupt the session. It will take about two hours to upload attachments. {{% /note %}} - + ### Start Bitbucket Server 1. Using SSH, connect to the Bitbucket node via the Bastion instance: @@ -317,10 +319,6 @@ Do not close or interrupt the session. It will take about two hours to upload at sudo systemctl start bitbucket ``` 1. Wait 10-15 minutes until Bitbucket Server is started. -1. Open browser and navigate to **LoadBalancerURL**. -1. Login with admin user. -1. Go to **![cog icon](/platform/marketplace/images/cog.png) > Server settings**, set **Base URL** to **LoadBalancerURL** value and click **Save**. - ### Elasticsearch Index If your app does not use Bitbucket search functionality just **skip** this section. @@ -335,11 +333,40 @@ To check status of indexing: 1. Navigate to **LoadBalancerURL**/rest/indexing/latest/status page. {{% note %}} -If case of any difficulties with Index generation, contact us for support in the [community Slack](http://bit.ly/dcapt_slack) **#data-center-app-performance-toolkit** channel. +In case of any difficulties with Index generation, contact us for support in the [community Slack](http://bit.ly/dcapt_slack) **#data-center-app-performance-toolkit** channel. {{% /note %}} +## <a id="executionhost"></a> Setting up an execution environment + +{{% note %}} +For simple spikes or tests, you can set up an execution environment on your local machine. To do this, clone the [DC App Performance Toolkit repo](https://github.com/atlassian/dc-app-performance-toolkit) and follow the instructions on the `dc-app-performance-toolkit/README.md` file. Make sure your local machine has at least a 4-core CPU and 16GB of RAM. +{{% /note %}} + +If you're using the DC App Performance Toolkit to produce the required [performance and scale benchmarks for your Data Center app](https://developer.atlassian.com/platform/marketplace/dc-apps-performance-and-scale-testing/), we recommend that you set up your execution environment on AWS: -## Testing scenarios +1. [Launch AWS EC2 instance](https://docs.aws.amazon.com/quickstarts/latest/vmlaunch/step-1-launch-instance.html). Instance type: [`c5.2xlarge`](https://aws.amazon.com/ec2/instance-types/c5/), OS: select from Quick Start `Ubuntu Server 18.04 LTS`. +1. Connect to the instance using [SSH](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/AccessingInstancesLinux.html) or the [AWS Systems Manager Sessions Manager](https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager.html). + + ```bash + ssh -i path_to_pem_file ubuntu@INSTANCE_PUBLIC_IP + ``` + +1. Install [Docker](https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository). Setup manage Docker as a [non-root user](https://docs.docker.com/engine/install/linux-postinstall). +1. Go to GitHub and create a fork of [dc-app-performance-toolkit](https://github.com/atlassian/dc-app-performance-toolkit). +1. Clone the fork locally, then edit the `bitbucket.yml` configuration file and other files as needed. +1. Push your changes to the forked repository. +1. Connect to the AWS EC2 instance and clone forked repository. + +Once your environment is set up, you can run the DC App Performance Toolkit: + +``` bash +cd dc-app-performance-toolkit +docker run --shm-size=4g -v "$PWD:/dc-app-performance-toolkit" atlassian/dcapt bitbucket.yml +``` + +You'll need to run the toolkit for each [test scenario](#testscenario) in the next section. + +## <a id="testscenario"></a> Running the test scenarios on your execution environment Using the Data Center App Performance Toolkit for [Performance and scale testing your Data Center app](/platform/marketplace/developing-apps-for-atlassian-data-center-products/) involves two test scenarios: @@ -447,23 +474,12 @@ In the `bitbucket-ui.py` script, view the following block of code: ``` python # def test_1_selenium_custom_action(webdriver, datasets, screen_shots): -# custom_action(webdriver, datasets) +# app_specific_action(webdriver, datasets) ``` This is a placeholder to add an extension action. The custom action can be moved to a different line, depending on the required workflow, as long as it is between the login (`test_0_selenium_a_login`) and logout (`test_2_selenium_z_log_out`) actions. -To implement the custom_action function, modify the `extension_ui.py` file in the `extension/bitbucket/` directory. The following is an example of the `custom_action` function, where Selenium navigates to a URL, clicks on an element, and waits until an element is visible: - -``` python -def custom_action(webdriver, datasets): - @print_timing - def measure(webdriver, interaction): - @print_timing - def measure(webdriver, interaction): - webdriver.get(f'{APPLICATION_URL}/plugins/servlet/some-app/reporter') - WebDriverWait(webdriver, timeout).until(EC.visibility_of_element_located((By.ID, 'plugin-element'))) - measure(webdriver, 'selenium_app_custom_action:view_report') -``` +To implement the app_specific_action function, modify the `extension_ui.py` file in the `extension/bitbucket/` directory. The following is an example of the `app_specific_action` function, where Selenium navigates to a URL, clicks on an element, and waits until an element is visible. To view more examples, see the `modules.py` file in the `selenium_ui/bitbucket` directory. @@ -556,4 +572,4 @@ After completing all your tests, delete your Bitbucket Data Center stacks. ## Support -In case of technical questions, issues or problems with DC Apps Performance Toolkit, contact us for support in the [community Slack](http://bit.ly/dcapt_slack) **#data-center-app-performance-toolkit** channel. \ No newline at end of file +In case of technical questions, issues or problems with DC Apps Performance Toolkit, contact us for support in the [community Slack](http://bit.ly/dcapt_slack) **#data-center-app-performance-toolkit** channel. diff --git a/docs/dc-apps-performance-toolkit-user-guide-confluence.md b/docs/dc-apps-performance-toolkit-user-guide-confluence.md index a0d4fbcc1..796004edb 100644 --- a/docs/dc-apps-performance-toolkit-user-guide-confluence.md +++ b/docs/dc-apps-performance-toolkit-user-guide-confluence.md @@ -8,19 +8,20 @@ date: "2018-07-19" --- # Data Center App Performance Toolkit User Guide For Confluence -To use the Data Center App Performance Toolkit, you'll need to first clone its repo. +This document walks you through the process of testing your app on Confluence using the Data Center App Performance Toolkit. These instructions focus on producing the required [performance and scale benchmarks for your Data Center app](https://developer.atlassian.com/platform/marketplace/dc-apps-performance-and-scale-testing/). -``` bash -git clone git@github.com:atlassian/dc-app-performance-toolkit.git -``` +To use the Data Center App Performance Toolkit, you'll need to: -Follow installation instructions described in the `dc-app-performance-toolkit/README.md` file. +1. [Set up Confluence Data Center on AWS](#instancesetup). +1. [Load an enterprise-scale dataset on your Confluence Data Center deployment](#preloading). +1. [Set up an execution environment for the toolkit](#executionhost). +1. [Run all the testing scenarios in the toolkit](#testscenario). -If you need performance testing results at a production level, follow instructions in this chapter to set up Confluence Data Center with the corresponding dataset. - -For spiking, testing, or developing, your local Confluence instance would work well. Thus, you can skip this chapter and proceed with [Testing scenarios](/platform/marketplace/dc-apps-performance-toolkit-user-guide-confluence/#testing-scenarios). Still, script adjustments for your local dataset may be required. +{{% note %}} +For simple spikes or tests, you can skip steps 1-2 and target any Confluence test instance. When you [set up your execution environment](#executionhost), you may need to edit the scripts according to your test instance's data set. +{{% /note %}} -## Setting up Confluence Data Center +## <a id="instancesetup"></a> Setting up Confluence Data Center We recommend that you use the [AWS Quick Start for Confluence Data Center](https://aws.amazon.com/quickstart/architecture/confluence/) to deploy a Confluence Data Center testing environment. This Quick Start will allow you to deploy Confluence Data Center with a new [Atlassian Standard Infrastructure](https://aws.amazon.com/quickstart/architecture/atlassian-standard-infrastructure/) (ASI) or into an existing one. @@ -63,8 +64,8 @@ All important parameters are listed and described in this section. For all other The Data Center App Performance Toolkit officially supports: -- The latest Confluence Platform Release version: 7.0.4 -- The latest Confluence [Enterprise Release](https://confluence.atlassian.com/enterprise/atlassian-enterprise-releases-948227420.html): 6.13.8 +- Confluence Platform Release version: 7.0.4 +- Confluence [Enterprise Release](https://confluence.atlassian.com/enterprise/atlassian-enterprise-releases-948227420.html): 6.13.8 **Cluster nodes** @@ -266,14 +267,10 @@ Do not close or interrupt the session. It will take some time to upload attachme ### <a id="reindexing"></a> Re-indexing Confluence Data Center (~2-4 hours) -{{% note %}} -Before re-index, go to **![cog icon](/platform/marketplace/images/cog.png) > General configuration > General configuration**, click **Edit** for **Site Configuration** and set **Base URL** to **LoadBalancerURL** value. -{{% /note %}} - For more information, go to [Re-indexing Confluence](https://confluence.atlassian.com/doc/content-index-administration-148844.html). 1. Log in as a user with the **Confluence System Administrators** [global permission](https://confluence.atlassian.com/doc/global-permissions-overview-138709.html). -1. Go to **![cog icon](/platform/marketplace/images/cog.png) > General Configuration > Content Indexing**. +1. Go to **![cog icon](/platform/marketplace/images/cog.png) > General Configuration > Content Indexing**. 1. Click **Rebuild** and wait until re-indexing is completed. Confluence will be unavailable for some time during the re-indexing process. @@ -284,7 +281,7 @@ For more information, go to [Administer your Data Center search index](https://c 1. Log in as a user with the **Confluence System Administrators** [global permission](https://confluence.atlassian.com/doc/global-permissions-overview-138709.html). 1. Create any new page with a random content (without a new page index snapshot job will not be triggered). -1. Go to **![cog icon](/platform/marketplace/images/cog.png) > General Configuration > Scheduled Jobs**. +1. Go to **![cog icon](/platform/marketplace/images/cog.png) > General Configuration > Scheduled Jobs**. 1. Find **Clean Journal Entries** job and click **Run**. 1. Make sure that Confluence index snapshot was created. To do that, use SSH to connect to the Confluence node via Bastion (where `NODE_IP` is the IP of the node): @@ -306,7 +303,37 @@ For more information, go to [Administer your Data Center search index](https://c Snapshot was created successfully. ``` -## Testing scenarios +## <a id="executionhost"></a> Setting up an execution environment + +{{% note %}} +For simple spikes or tests, you can set up an execution environment on your local machine. To do this, clone the [DC App Performance Toolkit repo](https://github.com/atlassian/dc-app-performance-toolkit) and follow the instructions on the `dc-app-performance-toolkit/README.md` file. Make sure your local machine has at least a 4-core CPU and 16GB of RAM. +{{% /note %}} + +If you're using the DC App Performance Toolkit to produce the required [performance and scale benchmarks for your Data Center app](https://developer.atlassian.com/platform/marketplace/dc-apps-performance-and-scale-testing/), we recommend that you set up your execution environment on AWS: + +1. [Launch AWS EC2 instance](https://docs.aws.amazon.com/quickstarts/latest/vmlaunch/step-1-launch-instance.html). Instance type: [`c5.2xlarge`](https://aws.amazon.com/ec2/instance-types/c5/), OS: select from Quick Start `Ubuntu Server 18.04 LTS`. +1. Connect to the instance using [SSH](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/AccessingInstancesLinux.html) or the [AWS Systems Manager Sessions Manager](https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager.html). + + ```bash + ssh -i path_to_pem_file ubuntu@INSTANCE_PUBLIC_IP + ``` + +1. Install [Docker](https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository). Setup manage Docker as a [non-root user](https://docs.docker.com/engine/install/linux-postinstall). +1. Go to GitHub and create a fork of [dc-app-performance-toolkit](https://github.com/atlassian/dc-app-performance-toolkit). +1. Clone the fork locally, then edit the `confluence.yml` configuration file and other files as needed. +1. Push your changes to the forked repository. +1. Connect to the AWS EC2 instance and clone forked repository. + +Once your environment is set up, you can run the DC App Performance Toolkit: + +``` bash +cd dc-app-performance-toolkit +docker run --shm-size=4g -v "$PWD:/dc-app-performance-toolkit" atlassian/dcapt confluence.yml +``` + +You'll need to run the toolkit for each [test scenario](#testscenario) in the next section. + +## <a id="testscenario"></a> Running the test scenarios on your execution environment Using the Data Center App Performance Toolkit for [Performance and scale testing your Data Center app](/platform/marketplace/developing-apps-for-atlassian-data-center-products/) involves two test scenarios: @@ -330,6 +357,7 @@ To receive performance baseline results without an app installed: - `application_port`: for HTTP - 80, for HTTPS - 443, or your instance-specific port. The self-signed certificate is not supported. - `admin_login`: admin user username - `admin_password`: admin user password + - `load_executor`: executor for load tests. Valid options are [jmeter](https://jmeter.apache.org/) (default) or [locust](https://locust.io/). - `concurrency`: number of concurrent users for JMeter scenario - we recommend you use the defaults to generate full-scale results. - `test_duration`: duration of the performance run - we recommend you use the defaults to generate full-scale results. - `ramp-up`: amount of time it will take JMeter to add all test users to test execution - we recommend you use the defaults to generate full-scale results. @@ -402,7 +430,7 @@ For many apps and extensions to Atlassian products, there should not be a signif #### Extending the base action -Extension scripts, which extend the base JMeter (`confluence.jmx`) and Selenium (`confluence-ui.py`) scripts, are located in a separate folder (`dc-app-performance-toolkit/app/extension/confluence`). You can modify these scripts to include their app-specific actions. +Extension scripts, which extend the base JMeter (`confluence.jmx`), Selenium (`confluence_ui.py`) and Locust (`locustfile.py`) scripts, are located in a separate folder (`dc-app-performance-toolkit/app/extension/confluence`). You can modify these scripts to include their app-specific actions. ##### Modifying JMeter @@ -429,11 +457,11 @@ The controllers in the extension script, which are executed along with the base When debugging, if you want to only test transactions in the `extend_view_issue` action, you can comment out other transactions in the `confluence.yml` config file and set the percentage of the base execution to 100. Alternatively, you can change percentages of others to 0. ``` yml -# perc_create_issue: 4 -# perc_search_jql: 16 - perc_view_issue: 100 -# perc_view_project_summary: 4 -# perc_view_dashboard: 8 +# create_issue: 4 +# search_jql: 16 + view_issue: 100 +# view_project_summary: 4 +# view_dashboard: 8 ``` {{% note %}} @@ -449,7 +477,7 @@ In such a case, you extend the `extend_standalone_extension` controller, which i The following configuration ensures that extend_standalone_extension controller is executed 10% of the total transactions. ``` yml - perc_standalone_extension: 10 + standalone_extension: 10 ``` ##### Using JMeter variables from the base script @@ -469,33 +497,43 @@ Use or access the following variables of the extension script from the base scri If there are some additional variables from the base script required by the extension script, you can add variables to the base script using extractors. For more information, go to [Regular expression extractors](http://jmeter.apache.org/usermanual/component_reference.html#Regular_Expression_Extractor). {{% /note %}} +##### Modifying Locust + +The main Locust script for Confluence is `locustio/confluence/locustfile.py` which executes `HTTP` actions from `locustio/confluence/http_actions.py`. +To customize Locust with app-specific actions, edit the function `app_specific_action` in the `extension/confluence/extension_locust.py` script. To enable `app_specific_action`, set a non-zero percentage value for `standalone_extension` in `confluence.yml` configuration file. +```yaml + # Action percentage for Jmeter and Locust load executors + view_page: 54 + view_dashboard: 6 + view_blog: 8 + search_cql: 7 + create_blog: 3 + create_and_edit_page: 6 + comment_page: 5 + view_attachment: 3 + upload_attachment: 5 + like_page: 3 + standalone_extension: 0 # By default disabled +``` +Locust uses actions percentage as relative [weights](https://docs.locust.io/en/stable/writing-a-locustfile.html#weight-attribute). For example, setting `standalone_extension` to `100` means that `app_specific_action` will be executed 20 times more than `upload_attachments`. To run just your app-specific action, disable all other actions by setting their value to `0`. + + ##### Modifying Selenium -In addition to JMeter, you can extend Selenium scripts to measure the end-to-end browser timings. +In addition to JMeter or Locust, you can extend Selenium scripts to measure end-to-end browser timings. -We use **Pytest** to drive Selenium tests. The `confluence-ui.py` executor script is located in the `app/selenium_ui/` folder. This file contains all browser actions, defined by the `test_ functions`. These actions are executed one by one during the testing. +We use **Pytest** to drive Selenium tests. The `confluence_ui.py` executor script is located in the `app/selenium_ui/` folder. This file contains all browser actions, defined by the `test_ functions`. These actions are executed one by one during the testing. -In the `confluence-ui.py` script, view the following block of code: +In the `confluence_ui.py` script, view the following block of code: ``` python # def test_1_selenium_custom_action(webdriver, datasets, screen_shots): -# custom_action(webdriver, datasets) +# app_specific_action(webdriver, datasets) ``` This is a placeholder to add an extension action. The custom action can be moved to a different line, depending on the required workflow, as long as it is between the login (`test_0_selenium_a_login`) and logout (`test_2_selenium_z_log_out`) actions. -To implement the custom_action function, modify the `extension_ui.py` file in the `extension/confluence/` directory. The following is an example of the `custom_action` function, where Selenium navigates to a URL, clicks on an element, and waits until an element is visible: - -``` python -def custom_action(webdriver, datasets): - @print_timing - def measure(webdriver, interaction): - @print_timing - def measure(webdriver, interaction): - webdriver.get(f'{APPLICATION_URL}/plugins/servlet/some-app/reporter') - WebDriverWait(webdriver, timeout).until(EC.visibility_of_element_located((By.ID, 'plugin-element'))) - measure(webdriver, 'selenium_app_custom_action:view_report') -``` +To implement the app_specific_action function, modify the `extension_ui.py` file in the `extension/confluence/` directory. The following is an example of the `app_specific_action` function, where Selenium navigates to a URL, clicks on an element, and waits until an element is visible. To view more examples, see the `modules.py` file in the `selenium_ui/confluence` directory. @@ -549,7 +587,7 @@ To receive scalability benchmark results for two-node Confluence DC with app-spe Index recovery is required for main index, starting now main index recovered from shared home directory ``` - + 1. Run bzt. ``` bash @@ -610,4 +648,4 @@ Once completed, you will be able to review action timings on Confluence Data Cen After completing all your tests, delete your Confluence Data Center stacks. ## Support -In case of technical questions, issues or problems with DC Apps Performance Toolkit, contact us for support in the [community Slack](http://bit.ly/dcapt_slack) **#data-center-app-performance-toolkit** channel. \ No newline at end of file +In case of technical questions, issues or problems with DC Apps Performance Toolkit, contact us for support in the [community Slack](http://bit.ly/dcapt_slack) **#data-center-app-performance-toolkit** channel. diff --git a/docs/dc-apps-performance-toolkit-user-guide-jira.md b/docs/dc-apps-performance-toolkit-user-guide-jira.md index d948e8400..ce58afa5f 100644 --- a/docs/dc-apps-performance-toolkit-user-guide-jira.md +++ b/docs/dc-apps-performance-toolkit-user-guide-jira.md @@ -8,19 +8,20 @@ date: "2019-09-12" --- # Data Center App Performance Toolkit User Guide For Jira -To use the Data Center App Performance Toolkit, you'll need to first clone its repo. +This document walks you through the process of testing your app on Jira using the Data Center App Performance Toolkit. These instructions focus on producing the required [performance and scale benchmarks for your Data Center app](https://developer.atlassian.com/platform/marketplace/dc-apps-performance-and-scale-testing/). -``` bash -git clone git@github.com:atlassian/dc-app-performance-toolkit.git -``` +To use the Data Center App Performance Toolkit, you'll need to: -Follow installation instructions described in the `dc-app-performance-toolkit/README.md` file. +1. [Set up Jira Data Center on AWS](#instancesetup). +1. [Load an enterprise-scale dataset on your Jira Data Center deployment](#preloading). +1. [Set up an execution environment for the toolkit](#executionhost). +1. [Run all the testing scenarios in the toolkit](#testscenario). -If you need performance testing results at a production level, follow instructions in this chapter to set up Jira Data Center with the corresponding dataset. - -For spiking, testing, or developing, your local Jira instance would work well. Thus, you can skip this chapter and proceed with [Testing scenarios](/platform/marketplace/dc-apps-performance-toolkit-user-guide-jira/#testing-scenarios). Still, script adjustments for your local dataset may be required. +{{% note %}} +For simple spikes or tests, you can skip steps 1-2 and target any Jira test instance. When you [set up your execution environment](#executionhost), you may need to edit the scripts according to your test instance's data set. +{{% /note %}} -## Setting up Jira Data Center +## <a id="instancesetup"></a> Setting up Jira Data Center We recommend that you use the [AWS Quick Start for Jira Data Center](https://aws.amazon.com/quickstart/architecture/jira/) to deploy a Jira Data Center testing environment. This Quick Start will allow you to deploy Jira Data Center with a new [Atlassian Standard Infrastructure](https://aws.amazon.com/quickstart/architecture/atlassian-standard-infrastructure/) (ASI) or into an existing one. @@ -63,8 +64,8 @@ All important parameters are listed and described in this section. For all other The Data Center App Performance Toolkit officially supports: -- The latest Jira Platform Release version: 8.0.3 -- The following Jira [Enterprise Releases](https://confluence.atlassian.com/enterprise/atlassian-enterprise-releases-948227420.html): 7.13.6 and 8.5.0 +- Jira Platform Release version: 8.0.3 +- Jira [Enterprise Releases](https://confluence.atlassian.com/enterprise/atlassian-enterprise-releases-948227420.html): 7.13.6 and 8.5.0 **Cluster nodes** @@ -264,7 +265,7 @@ We recommend that you only use this method if you are having problems with the [ sudo su jira -c "wget https://centaurus-datasets.s3.amazonaws.com/jira/${JIRA_VERSION}/large/xml_backup.zip -O /media/atl/jira/shared/import/xml_backup.zip" ``` 1. From a different computer, log in as a user with the **Jira System Administrators** [global permission](https://confluence.atlassian.com/adminjiraserver/managing-global-permissions-938847142.html). -1. Go to **![cog icon](/platform/marketplace/images/cog.png) > System > Restore System.** from the menu. +1. Go to **![cog icon](/platform/marketplace/images/cog.png) > System > Restore System.** from the menu. 1. Populate the **File name** field with `xml_backup.zip`. 1. Click **Restore** and wait until the import is completed. @@ -313,15 +314,44 @@ Do not close or interrupt the session. It will take about two hours to upload at For more information, go to [Re-indexing Jira](https://confluence.atlassian.com/adminjiraserver/search-indexing-938847710.html). 1. Log in as a user with the **Jira System Administrators** [global permission](https://confluence.atlassian.com/adminjiraserver/managing-global-permissions-938847142.html). -1. Go to **![cog icon](/platform/marketplace/images/cog.png) > System > Indexing**. +1. Go to **![cog icon](/platform/marketplace/images/cog.png) > System > Indexing**. 1. Select the **Lock one Jira node and rebuild index** option. 1. Click **Re-Index** and wait until re-indexing is completed. Jira will be unavailable for some time during the re-indexing process. When finished, the **Acknowledge** button will be available on the re-indexing page. +## <a id="executionhost"></a> Setting up an execution environment + {{% note %}} -Go to **![cog icon](/platform/marketplace/images/cog.png) > System > General configuration**, click **Edit Settings** and set **Base URL** to **LoadBalancerURL** value. -{{% /note %}} +For simple spikes or tests, you can set up an execution environment on your local machine. To do this, clone the [DC App Performance Toolkit repo](https://github.com/atlassian/dc-app-performance-toolkit) and follow the instructions on the `dc-app-performance-toolkit/README.md` file. Make sure your local machine has at least a 4-core CPU and 16GB of RAM. +{{% /note %}} + +If you're using the DC App Performance Toolkit to produce the required [performance and scale benchmarks for your Data Center app](https://developer.atlassian.com/platform/marketplace/dc-apps-performance-and-scale-testing/), we recommend that you set up your execution environment on AWS: + +1. [Launch AWS EC2 instance](https://docs.aws.amazon.com/quickstarts/latest/vmlaunch/step-1-launch-instance.html). Instance type: [`c5.2xlarge`](https://aws.amazon.com/ec2/instance-types/c5/), OS: select from Quick Start `Ubuntu Server 18.04 LTS`. +1. Connect to the instance using [SSH](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/AccessingInstancesLinux.html) or the [AWS Systems Manager Sessions Manager](https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager.html). + + ```bash + ssh -i path_to_pem_file ubuntu@INSTANCE_PUBLIC_IP + ``` + +1. Install [Docker](https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository). Setup manage Docker as a [non-root user](https://docs.docker.com/engine/install/linux-postinstall). +1. Go to GitHub and create a fork of [dc-app-performance-toolkit](https://github.com/atlassian/dc-app-performance-toolkit). +1. Clone the fork locally, then edit the `jira.yml` configuration file and other files as needed. +1. Push your changes to the forked repository. +1. Connect to the AWS EC2 instance and clone forked repository. + +Once your environment is set up, you can run the DC App Performance Toolkit: + +``` bash +cd dc-app-performance-toolkit +docker run --shm-size=4g -v "$PWD:/dc-app-performance-toolkit" atlassian/dcapt jira.yml +``` + +You'll need to run the toolkit for each [test scenario](#testscenario) in the next section. + +## <a id="testscenario"></a> Running the test scenarios on your execution environment + ## Testing scenarios @@ -347,6 +377,7 @@ To receive performance baseline results without an app installed: - `application_port`: for HTTP - 80, for HTTPS - 443, or your instance-specific port. The self-signed certificate is not supported. - `admin_login`: admin user username - `admin_password`: admin user password + - `load_executor`: executor for load tests. Valid options are [jmeter](https://jmeter.apache.org/) (default) or [locust](https://locust.io/). - `concurrency`: number of concurrent users for JMeter scenario - we recommend you use the defaults to generate full-scale results. - `test_duration`: duration of the performance run - we recommend you use the defaults to generate full-scale results. - `ramp-up`: amount of time it will take JMeter to add all test users to test execution - we recommend you use the defaults to generate full-scale results. @@ -356,7 +387,7 @@ To receive performance baseline results without an app installed: ``` bash bzt jira.yml ``` - + 1. View the following main results of the run in the `dc-app-performance-toolkit/app/results/jira/YY-MM-DD-hh-mm-ss` folder: - `results_summary.log`: detailed run summary - `results.csv`: aggregated .csv file with all actions and timings @@ -374,7 +405,7 @@ Review `results_summary.log` file under artifacts dir location. Make sure that o #### <a id="regressionrun2"></a> Run 2 (~50 min + Lucene Index timing test) -If you are submitting a Jira app, you are required to conduct a Lucene Index timing test. This involves conducting a foreground re-index on a single-node Data Center deployment (without and with your app installed) and a dataset that has 1M issues. +If you are submitting a Jira app, you are required to conduct a Lucene Index timing test. This involves conducting a foreground re-index on a single-node Data Center deployment (without and with your app installed) and a dataset that has 1M issues. First, benchmark your re-index time without your app installed: @@ -390,7 +421,7 @@ Jira 7 index time for 1M issues on a User Guide [recommended configuration](#qui Next, benchmark your re-index time with your app installed: -1. Install the app you want to test. +1. Install the app you want to test. 1. Go to **![cog icon](/platform/marketplace/images/cog.png) > System > Indexing**. 1. Select the **Lock one Jira node and rebuild index** option. 1. Click **Re-Index** and wait until re-indexing is completed. @@ -402,7 +433,7 @@ After attaching both screenshots to your DC HELP ticket, move on to performance ``` bash bzt jira.yml ``` - + {{% note %}} When the execution is successfully completed, the `INFO: Artifacts dir:` line with the full path to results directory will be displayed in console output. Save this full path to the run results folder. Later you will have to insert it under `runName: "with app"` for report generation. {{% /note %}} @@ -440,7 +471,7 @@ For many apps and extensions to Atlassian products, there should not be a signif #### Extending the base action -Extension scripts, which extend the base JMeter (`jira.jmx`) and Selenium (`jira-ui.py`) scripts, are located in a separate folder (`dc-app-performance-toolkit/extension/jira`). You can modify these scripts to include their app-specific actions. +Extension scripts, which extend the base JMeter (`jira.jmx`), Selenium (`jira-ui.py`) and Locust (`locustfile.py`) scripts, are located in a separate folder (`dc-app-performance-toolkit/extension/jira`). You can modify these scripts to include their app-specific actions. As there are two options for load tests executor available for selection, you can modify either Locust or JMeter scripts. ##### Modifying JMeter @@ -467,11 +498,11 @@ The controllers in the extension script, which are executed along with the base When debugging, if you want to only test transactions in the `extend_view_issue` action, you can comment out other transactions in the `jira.yml` config file and set the percentage of the base execution to 100. Alternatively, you can change percentages of others to 0. ``` yml -# perc_create_issue: 4 -# perc_search_jql: 16 - perc_view_issue: 100 -# perc_view_project_summary: 4 -# perc_view_dashboard: 8 +# create_issue: 4 +# search_jql: 16 + view_issue: 100 +# view_project_summary: 4 +# view_dashboard: 8 ``` {{% note %}} @@ -487,7 +518,7 @@ In such a case, you extend the `extend_standalone_extension` controller, which i The following configuration ensures that extend_standalone_extension controller is executed 10% of the total transactions. ``` yml - perc_standalone_extension: 10 + standalone_extension: 10 ``` ##### Using JMeter variables from the base script @@ -507,9 +538,31 @@ Use or access the following variables of the extension script from the base scri If there are some additional variables from the base script required by the extension script, you can add variables to the base script using extractors. For more information, go to [Regular expression extractors](http://jmeter.apache.org/usermanual/component_reference.html#Regular_Expression_Extractor). {{% /note %}} +##### Modifying Locust + +The main Locust script for Jira is `locustio/jira/locustfile.py` which executes `HTTP` actions from `locustio/jira/http_actions.py`. +To customize Locust with app-specific actions, edit the function `app_specific_action` in the `extension/jira/extension_locust.py` script. To enable `app_specific_action`, set non-zero percentage value for `standalone_extension` in `jira.yml` configuration file. +```yaml + # Action percentage for Jmeter and Locust load executors + create_issue: 4 + search_jql: 13 + view_issue: 43 + view_project_summary: 4 + view_dashboard: 12 + edit_issue: 4 + add_comment: 2 + browse_projects: 4 + view_scrum_board: 3 + view_kanban_board: 3 + view_backlog: 6 + browse_boards: 2 + standalone_extension: 0 # By default disabled +``` +Locust uses actions percentage as relative [weights](https://docs.locust.io/en/stable/writing-a-locustfile.html#weight-attribute). For example, setting `standalone_extension` to `100` means that `app_specific_action` will be executed 50 times more than `browse_boards`. To run just your app-specific action, disable all other actions by setting their value to `0`. + ##### Modifying Selenium -In addition to JMeter, you can extend Selenium scripts to measure the end-to-end browser timings. +In addition to JMeter or Locust, you can extend Selenium scripts to measure end-to-end browser timings. We use **Pytest** to drive Selenium tests. The `jira-ui.py` executor script is located in the `app/selenium_ui/` folder. This file contains all browser actions, defined by the `test_ functions`. These actions are executed one by one during the testing. @@ -517,23 +570,12 @@ In the `jira-ui.py` script, view the following block of code: ``` python # def test_1_selenium_custom_action(webdriver, datasets, screen_shots): -# custom_action(webdriver, datasets) +# app_specific_action(webdriver, datasets) ``` This is a placeholder to add an extension action. The custom action can be moved to a different line, depending on the required workflow, as long as it is between the login (`test_0_selenium_a_login`) and logout (`test_2_selenium_z_log_out`) actions. -To implement the custom_action function, modify the `extension_ui.py` file in the `extension/jira/` directory. The following is an example of the `custom_action` function, where Selenium navigates to a URL, clicks on an element, and waits until an element is visible: - -``` python -def custom_action(webdriver, datasets): - @print_timing - def measure(webdriver, interaction): - @print_timing - def measure(webdriver, interaction): - webdriver.get(f'{APPLICATION_URL}/plugins/servlet/some-app/reporter') - WebDriverWait(webdriver, timeout).until(EC.visibility_of_element_located((By.ID, 'plugin-element'))) - measure(webdriver, 'selenium_app_custom_action:view_report') -``` +To implement the app_specific_action function, modify the `extension_ui.py` file in the `extension/jira/` directory. The following is an example of the `app_specific_action` function, where Selenium navigates to a URL, clicks on an element, and waits until an element is visible. To view more examples, see the `modules.py` file in the `selenium_ui/jira` directory. @@ -649,4 +691,4 @@ Once completed, you will be able to review action timings on Jira Data Center wi After completing all your tests, delete your Jira Data Center stacks. ## Support -In case of technical questions, issues or problems with DC Apps Performance Toolkit, contact us for support in the [community Slack](http://bit.ly/dcapt_slack) **#data-center-app-performance-toolkit** channel. \ No newline at end of file +In case of technical questions, issues or problems with DC Apps Performance Toolkit, contact us for support in the [community Slack](http://bit.ly/dcapt_slack) **#data-center-app-performance-toolkit** channel. diff --git a/docs/jira/README.md b/docs/jira/README.md index 647f48e17..d34651bea 100644 --- a/docs/jira/README.md +++ b/docs/jira/README.md @@ -21,6 +21,7 @@ For spiking, testing, or developing, your local Jira instance would work well. * `concurrency`: number of concurrent users for JMeter scenario * `test_duration`: duration of test execution (45m is by default) * `WEBDRIVER_VISIBLE`: visibility of Chrome browser during selenium execution (False is by default) +* `load_executor`: `jmeter` or `locust` load executor. `jmeter` is using by default. ## Step 2: Run tests Run Taurus. @@ -33,6 +34,7 @@ Results are located in the `resutls/jira/YY-MM-DD-hh-mm-ss` directory: * `bzt.log` - log of bzt run * `error_artifacts` - folder with screenshots and HTMLs of Selenium fails * `jmeter.err` - JMeter errors log +* `locust.err` - Locust errors log * `kpi.jtl` - JMeter raw data * `pytest.out` - detailed log of Selenium execution, including stacktraces of Selenium fails * `selenium.jtl` - Selenium raw data @@ -43,41 +45,67 @@ next steps. # Useful information -## Jmeter -### Changing JMeter workload -The [jira.yml](../../app/jira.yml) has a workload section with `perc_action_name` fields. You can change values from 0 to 100 to increase/decrease execution frequency of certain actions. +## Changing performance workload for JMeter and Locust +The [jira.yml](../../app/jira.yml) has a `action_name` fields in `env` section with percentage for each action. You can change values from 0 to 100 to increase/decrease execution frequency of certain actions. The percentages must add up to 100, if you want to ensure the performance script maintains throughput defined in `total_actions_per_hr`. The default load simulates an enterprise scale load of 54500 user transactions per hour at 200 concurrency. To simulate a load of medium-sized customers, `total_actions_per_hr` and `concurrency` can be reduced to 14000 transactions and 70 users. This can be further halved for a small customer. +## JMeter ### Opening JMeter scripts JMeter is written in XML and requires the JMeter GUI to view and make changes. You can launch JMeter GUI by running the `~/.bzt/jmeter-taurus/<jmeter_version>/bin/jmeter` command. Be sure to run this command inside the `app` directory. The main [jira.jmx](../../app/jmeter/jira.jmx) file contains the relative path to other scripts and will throw errors if run elsewhere. ### Debugging JMeter scripts 1. Open JMeter GUI from `jira` directory by running the `~/.bzt/jmeter-taurus/<jmeter_version>/bin/jmeter` command. -2. Right-click `Test Plan` > `Add` > `Listener` > `View Results Tree`. -3. On the `View Results Tree` page, click the `Browse` button and open `error.jtl` from `app/results/jira/YY-MM-DD-hh-mm-ss` folder. +1. Right-click `Test Plan` > `Add` > `Listener` > `View Results Tree`. +1. On the `View Results Tree` page, click the `Browse` button and open `error.jtl` from `app/results/jira/YY-MM-DD-hh-mm-ss` folder. From this view, you can click on any failed action and see the request and response data in appropriate tabs. In addition, you can run and monitor JMeter test real-time with GUI. 1. Launch the test with GUI by running `bzt jira.yml -gui`. -2. Right-click `Test Plan` > `Add` > `Listener` > `View Results Tree`. -3. Click the start button to start running the test. +1. Right-click `Test Plan` > `Add` > `Listener` > `View Results Tree`. +1. Click the start button to start running the test. ### Run one JMeter action #### Option 1: Run one JMeter action via GUI 1. Open JMeter GUI from `app` directory by running the `~/.bzt/jmeter-taurus/<jmeter_version>/bin/jmeter` command. -2. Go to `File` > `Open`, and then open `jmeter/jira.jmx`. -2. In the`Global Variables` section, add correct Jira hostname, port, protocol, and postfix (if required). -3. In `Jira` > `load profile`, set `perc_desired_action` to 100. -4. Run JMeter. +1. Go to `File` > `Open`, and then open `jmeter/jira.jmx`. +1. In the `Global Variables` section, add correct Jira hostname, port, protocol, and postfix (if required). +1. In `Jira` > `load profile`, set `perc_desired_action` to 100. +1. Enable `View Results Tree` controller. +1. Run JMeter. +1. `View Results Tree` controller will have all details for every request and corresponding response.. #### Option 2: Run one JMeter action via bzt 1. In [jira.yml](../../app/jira.yml), set `perc_desired_action` to 100 and all other perc_* to 0. -2. Run `bzt jira.yml`. +1. Run `bzt jira.yml`. + + +## Locust +### Debugging Locust scripts +Detailed log of Locust executor is located in the `results/jira/YY-MM-DD-hh-mm-ss/locust.log` file. Locust errors and stacktrace are located in the `results/jira/YY-MM-DD-hh-mm-ss/locust.err` file. + +Additional debug information could be enabled by setting `verbose` flag to `true` in `jira.yml` configuration file. To add log message use `logger.locust_info('your INFO message')` string in the code. +### Running Locust tests locally without the Performance Toolkit +#### Start locust UI mode +1. Activate virualenv for the Performance Toolkit. +1. Navigate to `app` directory and execute command `locust --locustfile locustio/jira/locustfile.py`. +1. Open your browser, navigate to `localhost:8089`. +1. Enter `Number of total users to simulate` (`1` is recommended value for debug purpose) +1. Enter `Hatch rate (users spawned/secods)` +1. Press `Start spawning` button. + +#### Start Locust console mode +1. Activate virualenv for the Performance Toolkit. +1. Navigate to `app` and execute command `locust --no-web --locustfile locustio/jira/locustfile.py --clients N --hatch-rate R`, where `N` is the number of total users to simulate and `R` is the hatch rate. + +Full logs of local run you can find in the `results/jira/YY-MM-DD-hh-mm-ss_local/` directory. + +To execute one locust action, navigate to `jira.yml` and set percentage value `100` to the action you would like to run separately, set percentage value `0` to all other actions. + ## Selenium ### Debugging Selenium scripts @@ -88,15 +116,15 @@ Also, screenshots and HTMLs of Selenium fails are stared in the `results/jira/YY ### Running Selenium tests with Browser GUI There are two options of running Selenium tests with browser GUI: 1. In [jira.yml](../../app/jira.yml) file, set the `WEBDRIVER_VISIBLE: True`. -2. Set environment variable with the `export WEBDRIVER_VISIBLE=True` command. +1. Set environment variable with the `export WEBDRIVER_VISIBLE=True` command. ### Running Selenium tests locally without the Performance Toolkit 1. Activate virualenv for the Performance Toolkit. -2. Navigate to the selenium folder using the `cd app/selenium_ui` command. -3. Set browser visibility using the `export WEBDRIVER_VISIBLE=True` command. -4. Run all Selenium PyTest tests with the `pytest jira-ui.py` command. -5. To run one Selenium PyTest test (e.g., `test_1_selenium_view_issue`), execute the first login test and the required one with this command: +1. Navigate to the selenium folder using the `cd app/selenium_ui` command. +1. Set browser visibility using the `export WEBDRIVER_VISIBLE=True` command. +1. Run all Selenium PyTest tests with the `pytest jira-ui.py` command. +1. To run one Selenium PyTest test (e.g., `test_1_selenium_view_issue`), execute the first login test and the required one with this command: `pytest jira-ui.py::test_0_selenium_a_login jira-ui.py::test_1_selenium_view_issue`. diff --git a/requirements.txt b/requirements.txt index e7e2a530f..22549867a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,6 @@ matplotlib==3.2.1 -pandas==1.0.3 -importlib-metadata==1.6.0 -zipp==2.2.0 # hard requirement of bzt 1.14.1 -bzt==1.14.1 \ No newline at end of file +pandas==1.0.4 +importlib-metadata==1.6.1 +zipp==2.2.0 # hard requirement of bzt 1.14.2 +bzt==1.14.2 +locustio==0.13.5 \ No newline at end of file