Script for updating test results

Change-Id: I986be872ca22444532e66075b100076d7ec5989d
Related-prod: PROD-32075
diff --git a/parcing_testrail_results/README.md b/parcing_testrail_results/README.md
new file mode 100644
index 0000000..cda86d3
--- /dev/null
+++ b/parcing_testrail_results/README.md
@@ -0,0 +1,68 @@
+# Parcing testrail results
+
+## Preparing the environment
+
+1. Clone the repository `parcing_testrail_results`:
+* Clone with SSH
+    ~~~~
+    git clone ssh://sturivnyi@gerrit.mcp.mirantis.com:29418/mcp/osccore-qa-testing-tools
+    ~~~~
+    
+2. Install python 3.6 virtualenv:
+    ~~~~
+    apt install python3.6-venv
+    ~~~~
+
+3. Navigate to the `parcing_testrail_results/` folder:
+   ~~~
+   cd parcing_testrail_results/
+   ~~~
+
+4. Create virtualenv and activate it:
+    ~~~~
+    virtualenv --python=python3.6 .venv
+    . .venv/bin/activate
+    ~~~~
+
+5. In the `parcing_testrail_results/` folder install requirements:
+    ~~~~
+    pip3 install -r requirements.txt
+    ~~~~
+
+6. Export environment variables:
+   ~~~
+   export TESTRAIL_USER="your_email@mirantis.com"
+   export TESTRAIL_PASSWORD="testrail_super_secret_password"
+   ~~~
+
+## Preparing `config` file
+
+1. Edit `config.py` file. Provide it with `TESTRAIL_TOKEN` and `TESTRAIL_COOKIES`
+
+2. Getting `TESTRAIL_TOKEN` and `TESTRAIL_COOKIES`
+* Use `Chromium` web browser
+* Navigate to the any test-case in tetrail. For example, `https://mirantis.testrail.com/index.php?/tests/view/59746877`
+* Open Chrome `DevTools` (press F12)
+* In the `DevTools` window select `Network` tab
+* In the browser click on the `History $ Context` tab
+* In the `DevTools` window select `index.php?/tests/ajax_render_history` request
+* In the `Headers` tab get `_token` and `cookie: tr_session` values
+* Change `TESTRAIL_TOKEN` and `TESTRAIL_COOKIES` in the `config.py` file
+
+## Running the script
+ 
+1. Getting help:
+   ~~~
+   python html_testrail.py --help
+   ~~~
+   
+2. Running the script:
+   ~~~
+   python html_testrail.py --run_id=63355
+   ~~~
+   
+   Where:
+   ```
+    --run_id TEXT  Testrail run_id. For example, https://mirantis.testrail.com/index.php?/runs/view/63288 
+   So run_id will be 63288
+   ```
diff --git a/parcing_testrail_results/config.py b/parcing_testrail_results/config.py
new file mode 100644
index 0000000..ede2f55
--- /dev/null
+++ b/parcing_testrail_results/config.py
@@ -0,0 +1,11 @@
+import os
+
+TESTRAIL_USER = os.environ.get('TESTRAIL_USER')
+TESTRAIL_PASSWORD = os.environ.get('TESTRAIL_PASSWORD')
+
+TESTRAIL_URL = 'https://mirantis.testrail.com'
+TESTRAIL_TOKEN = '0YGnO1TC5NCCQFwgxmsW'
+TESTRAIL_COOKIES = "9adbe251-4ef1-474c-8ca6-9aaa1fbc5e76"
+
+LOGGIGNG_FOLDER = '/tmp/'
+LOGGIGNG_UTILS = 'testrail.log'
diff --git a/parcing_testrail_results/gitignore b/parcing_testrail_results/gitignore
new file mode 100644
index 0000000..80b987d
--- /dev/null
+++ b/parcing_testrail_results/gitignore
@@ -0,0 +1,3 @@
+.*
+__*
+*.ipynb
diff --git a/parcing_testrail_results/html_testrail.py b/parcing_testrail_results/html_testrail.py
new file mode 100644
index 0000000..182299d
--- /dev/null
+++ b/parcing_testrail_results/html_testrail.py
@@ -0,0 +1,285 @@
+import click
+import config
+import logging
+import re
+import requests
+from bs4 import BeautifulSoup
+from difflib import SequenceMatcher
+
+from testrail import *
+
+client = APIClient(config.TESTRAIL_URL)
+client.user = config.TESTRAIL_USER
+client.password = config.TESTRAIL_PASSWORD
+
+
+logging.basicConfig(format='[%(asctime)s][%(name)s][%(levelname)s] %(message)s',
+                    datefmt='%d-%m-%Y %H:%M:%S',
+                    handlers=[
+                        logging.FileHandler('{}{}'.format(config.LOGGIGNG_FOLDER, config.LOGGIGNG_UTILS)),
+                        logging.StreamHandler()],
+                    level=logging.INFO)
+logger = logging.getLogger('testrail')
+
+
+class GetTestHistory:
+    def __init__(self, test_id):
+        self.test_id = test_id
+
+    def get_html(self):
+
+        token = config.TESTRAIL_TOKEN
+        post_url = "https://mirantis.testrail.com/index.php?/tests/ajax_render_history"
+
+        headers = {
+            "Accept": "text/plain, */*; q=0.01",
+            "Accept-Encoding": "gzip, deflate, br",
+            "Accept-Language": "en-US,en;q=0.9,es-AR;q=0.8,es;q=0.7,fr-DZ;q=0.6,fr;q=0.5,de-BE;q=0.4,de;q=0.3,ru-UA;q=0.2,ru;q=0.1,uk;q=0.1",
+            "Connection": "keep-alive",
+            "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
+            "Host": "mirantis.testrail.com",
+            "Origin": "https://mirantis.testrail.com",
+            "Proxy-Authorization": "Basic VVZQTnYxLXAybjJlbXhldzB6Z2RkcndwM25vZ2JiaHJ0Zm9ib3pjJmpvaG5kb2VAdXZwbi5tZTpvN3I3cDA4Mml6cHNoZHp6eDBjeHNsZGVudmUzYmNyZg ==",
+            "Referer": "https://mirantis.testrail.com/index.php?/tests/view/{}".format(self.test_id),
+            "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Ubuntu Chromium/73.0.3683.86 Chrome/73.0.3683.86 Safari/537.36",
+            "X-Requested-With": "XMLHttpRequest"
+        }
+
+        cookies = {"tr_session": config.TESTRAIL_COOKIES}
+
+        r = requests.post(post_url, data='test_id={}&limit=50&_token={}'.format(self.test_id, token),
+                          headers=headers,
+                          cookies=cookies)
+        html_page = r.text
+
+        return html_page
+
+    def get_old_test_results(self):
+        logger.info('Getting old test results from html page')
+        html_page = self.get_html()
+
+        soup = BeautifulSoup(html_page, 'html.parser')
+        page_div = soup.div
+
+        tests_history = []
+        for tag in page_div.find_all('td', {'class': 'id'}):
+            tag_parent = tag.parent
+
+            test_status = tag_parent.find_all('span', {'class', 'status'})[0].string
+            test_id = tag_parent.find_all('a', {'class', 'link-noline'})[0].string[1:]
+
+            test_data = {'test_status': test_status, 'test_id': test_id}
+
+            if test_status == 'TestFailed':
+                tests_history.append(test_data)
+
+        return tests_history
+
+
+class GetFailedTests:
+    def __init__(self, plan_id):
+        self.plan_id = plan_id
+
+    def get_plan(self):
+        logger.info('Getting plan: {}'.format(self.plan_id))
+        return client.send_get('get_plan/{}'.format(self.plan_id))
+
+    def get_suites(self):
+        logger.info('Getting suites')
+        plan = self.get_plan()
+        all_suites_ids = []
+
+        for suite in plan['entries']:
+            siute_id = suite['runs'][0]['id']
+            all_suites_ids.append(siute_id)
+            logger.info('Suite: {}'.format(siute_id))
+        return all_suites_ids
+
+    def get_test(self, test_id):
+        logger.info('Getting test: {}'.format(test_id))
+        return client.send_get('get_test/{}'.format(test_id))
+
+    def get_tests_results_by_suite(self, suite_id):
+        logger.info('Getting tests results by suite (suite_id): {}'.format(suite_id))
+        return client.send_get('get_tests/{}'.format(suite_id))
+
+    def get_all_tests_results(self):
+        logger.info('Getting all tests results')
+        all_suites = self.get_suites()
+
+        all_tests = []
+        for suite in all_suites:
+            test_results = self.get_tests_results_by_suite(suite)
+            all_tests.append(test_results)
+
+        return all_tests
+
+    def get_all_failed_tests(self, test_status=5):
+        logger.info('Getting failed tests')
+        # test['status_id'] == 5 failed
+        # test['status_id'] == 9 test failed
+        # test['status_id'] == 10 infra failed
+        all_tests_in_all_suites = self.get_all_tests_results()
+
+        failed_tests = []
+        for tests_in_suite in all_tests_in_all_suites:
+            for test in tests_in_suite:
+                if test['status_id'] == test_status:
+                   failed_tests.append(test)
+
+        return failed_tests
+
+    def get_test_result(self, test_id):
+        logger.info('Getting test result: {}'.format(test_id))
+        test = self.get_test(test_id)
+        return client.send_get('get_results_for_case/{}/{}'.format(test['run_id'], test['case_id']))
+
+    def update_test_results(self, test_id, defects):
+        """
+        Updates test results:
+        status_id: 9: Test Failed
+        comment: will be added to the test result
+
+        :param test_id: id of updated test
+        :param defects: defect to update
+        :return:
+        """
+        logger.info('Updating test results test_id: {} with defect: {}'.format(test_id, defects))
+        test = self.get_test(test_id)
+
+        return client.send_post('add_result_for_case/{}/{}'.format(test['run_id'], test['case_id']),
+                                {'status_id': 9, 'comment': 'Updated by R2D2', 'defects': defects})
+
+
+def get_current_test_comment(current_test_results):
+    logger.info('Getting current test comment')
+    for test_results in current_test_results:
+        if 'comment' in test_results:
+            if test_results['comment']:
+                if len(test_results['comment']) > 50:
+                    return test_results['comment']
+
+
+def get_old_tests_comments_ids(old_test_results, failed_tests):
+    logger.info('Getting old tests comments ids')
+    old_tests_comments_ids = []
+
+    for test in old_test_results:
+
+        test_result = failed_tests.get_test_result(test['test_id'])
+        old_tests_comments_ids.append(test_result)
+    return old_tests_comments_ids
+
+
+def update_test_comment(test_comment):
+    """
+    Get reed from the extra symbols and spaces in test comment (trace)
+    :param test_comment: string
+    :return: string
+    """
+    logger.info('Updating current test comment')
+    format_date = r"\d{4}-\d\d-\d\dT\d\d:\d\d:\d\dZ"
+    format_uuid_a = r"[0-9a-fA-F]{8}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{12}"
+    format_uuid_b = r"[0-9a-fA-F]{32}"
+    format_space = r" "
+    format_new_line = r"\n"
+
+    for item in format_date, format_uuid_a, format_uuid_b, format_space, format_new_line:
+        test_comment = re.sub(item, '', test_comment)
+    return test_comment
+
+
+def update_old_comments(defects_and_comments_from_old_tests):
+    logger.info('Updating old test comment')
+    for item in defects_and_comments_from_old_tests:
+        item['old_test_comment'] = update_test_comment(item['old_test_comment'])
+    return defects_and_comments_from_old_tests
+
+
+def get_defects_and_comments_from_old_tests(old_tests_comments_ids):
+    logger.info('Getting defects and comments from old tests')
+    data_from_old_tests = []
+
+    old_test_comment = ''
+    old_test_defect = ''
+
+    for old_test_list in old_tests_comments_ids:
+        for old_test in old_test_list:
+
+            if old_test['comment']:
+                if len(old_test['comment']) > 50:
+                    old_test_comment = old_test['comment']
+
+            if old_test['defects']:
+                old_test_defect = old_test['defects']
+
+        if old_test_comment and old_test_defect:
+            data_from_old_tests.append({'old_test_comment': old_test_comment, 'old_test_defect': old_test_defect})
+
+    return data_from_old_tests
+
+
+def compare_comments(current_test_comment, defects_and_comments_from_old_tests, desired_ratio=0.7, test_id=''):
+    logger.info('Comparing comments')
+
+    if not desired_ratio:
+        desired_ratio = 0.75
+
+    defect_for_update = ''
+    for item in defects_and_comments_from_old_tests:
+        m = SequenceMatcher(None, current_test_comment, item['old_test_comment'])
+        my_ratio = m.ratio()
+        logger.info('Low ratio: {}, Desired ratio {} Test https://mirantis.testrail.com/index.php?/tests/view/{} '
+                    'will NOT be updated with issue {}'.format(my_ratio,
+                                                               desired_ratio,
+                                                               test_id,
+                                                               item['old_test_defect']))
+
+        if my_ratio > desired_ratio:
+            logger.info('!!!!! Desired ratio {}, Test Ratio: {} Jira issue: {}'.format(desired_ratio,
+                                                                                       my_ratio,
+                                                                                       item['old_test_defect']))
+
+            defect_for_update = item['old_test_defect']
+            return defect_for_update
+
+
+@click.command()
+@click.option('--run_id', default=1, type=click.STRING, help='Testrail run_id. For example, '
+                                          'https://mirantis.testrail.com/index.php?/runs/view/63288 '
+                                          'So run_id will be 63288')
+@click.option('--ratio', type=click.FLOAT, help='The ratio to comapare current console output and old one.')
+def get_failed_tests_history(run_id, ratio):
+
+    failed_tests = GetFailedTests(run_id)
+    all_failed_tests = failed_tests.get_all_failed_tests()
+
+    for test in all_failed_tests:
+
+        test_history = GetTestHistory(test['id'])
+        old_test_results = test_history.get_old_test_results()
+
+        curr_tst_res = failed_tests.get_test_result(test['id'])
+
+        current_test_comment = get_current_test_comment(curr_tst_res)
+        current_test_comment = update_test_comment(current_test_comment)
+
+        old_tests_comments_ids = get_old_tests_comments_ids(old_test_results, failed_tests)
+
+        defects_and_comments_from_old_tests = get_defects_and_comments_from_old_tests(old_tests_comments_ids)
+        defects_and_comments_from_old_tests = update_old_comments(defects_and_comments_from_old_tests)
+
+        if defects_and_comments_from_old_tests:
+            defect_for_update = compare_comments(current_test_comment,
+                                                 defects_and_comments_from_old_tests,
+                                                 desired_ratio=ratio,
+                                                 test_id=test['id'])
+
+            if defect_for_update:
+                logger.info('!!!!! Updating test-case: https://mirantis.testrail.com/index.php?/tests/view/{} '
+                            'with Jira issue {}'.format(test['id'], defect_for_update))
+                failed_tests.update_test_results(test_id=test['id'], defects=defect_for_update)
+
+
+if __name__ == '__main__':
+    get_failed_tests_history()
diff --git a/parcing_testrail_results/requirements.txt b/parcing_testrail_results/requirements.txt
new file mode 100644
index 0000000..b0cfd8e
--- /dev/null
+++ b/parcing_testrail_results/requirements.txt
@@ -0,0 +1,7 @@
+ipython>=0.12.1
+requests>=2.20.0
+ipdb>=0.11
+tinydb>=3.11.1
+beautifulsoup4>=1
+atlassian-python-api>=1.11.17
+click>=7.0
\ No newline at end of file
diff --git a/parcing_testrail_results/testrail.py b/parcing_testrail_results/testrail.py
new file mode 100644
index 0000000..7ed5900
--- /dev/null
+++ b/parcing_testrail_results/testrail.py
@@ -0,0 +1,97 @@
+#
+# TestRail API binding for Python 3.x (API v2, available since
+# TestRail 3.0)
+#
+# Learn more:
+#
+# http://docs.gurock.com/testrail-api2/start
+# http://docs.gurock.com/testrail-api2/accessing
+#
+# Copyright Gurock Software GmbH. See license.md for details.
+#
+
+import urllib.request, urllib.error
+import json, base64
+import time
+
+class APIClient:
+	def __init__(self, base_url):
+		self.user = ''
+		self.password = ''
+		if not base_url.endswith('/'):
+			base_url += '/'
+		self.__url = base_url + 'index.php?/api/v2/'
+
+	#
+	# Send Get
+	#
+	# Issues a GET request (read) against the API and returns the result
+	# (as Python dict).
+	#
+	# Arguments:
+	#
+	# uri                 The API method to call including parameters
+	#                     (e.g. get_case/1)
+	#
+	def send_get(self, uri):
+		try:
+			return self.__send_request('GET', uri, None)
+		except APIError:
+			print("Got an API Exception. Waiting  30 sec.")
+			time.sleep(30)
+			return self.__send_request('GET', uri, None)
+
+	#
+	# Send POST
+	#
+	# Issues a POST request (write) against the API and returns the result
+	# (as Python dict).
+	#
+	# Arguments:
+	#
+	# uri                 The API method to call including parameters
+	#                     (e.g. add_case/1)
+	# data                The data to submit as part of the request (as
+	#                     Python dict, strings must be UTF-8 encoded)
+	#
+	def send_post(self, uri, data):
+		return self.__send_request('POST', uri, data)
+
+	def __send_request(self, method, uri, data):
+		url = self.__url + uri
+		request = urllib.request.Request(url)
+		if (method == 'POST'):
+			request.data = bytes(json.dumps(data), 'utf-8')
+		auth = str(
+			base64.b64encode(
+				bytes('%s:%s' % (self.user, self.password), 'utf-8')
+			),
+			'ascii'
+		).strip()
+		request.add_header('Authorization', 'Basic %s' % auth)
+		request.add_header('Content-Type', 'application/json')
+
+		e = None
+		try:
+			response = urllib.request.urlopen(request).read()
+		except urllib.error.HTTPError as ex:
+			response = ex.read()
+			e = ex
+
+		if response:
+			result = json.loads(response.decode())
+		else:
+			result = {}
+
+		if e != None:
+			if result and 'error' in result:
+				error = '"' + result['error'] + '"'
+			else:
+				error = 'No additional error message received'
+			raise APIError('TestRail API returned HTTP %s (%s)' %
+				(e.code, error))
+
+		return result
+
+class APIError(Exception):
+	pass
\ No newline at end of file