[rp-reporter] again fix downloading and checking files

Make it thread safe

Related-prod: PRODX-48948
Change-Id: I4a08c19665c2ce6dcf18ade8e4bbf9832f0f9315
diff --git a/rp_reporter/rp_reporter/batch_reporter.py b/rp_reporter/rp_reporter/batch_reporter.py
index 0ee2664..a38e1b7 100755
--- a/rp_reporter/rp_reporter/batch_reporter.py
+++ b/rp_reporter/rp_reporter/batch_reporter.py
@@ -1,18 +1,11 @@
 import click
-
-import ipdb
+import logging
 import itertools
-import sys, os
 
 import jenkins_jinny.main as jj
 from copy import deepcopy
-from pathlib import Path
+from .report_from_xml import timestamp, Reporter
 
-from .report_from_xml import report_xml, timestamp
-from .report_from_xml import client as rp_client
-import logging
-
-logging.basicConfig(level=logging.DEBUG)
 LOG = logging.getLogger("rp_reporter")
 
 def upload_job(job:str, suite_per_job=False, tags=None):
@@ -40,8 +33,9 @@
     # for file_location in deploy_job.get_artifacts("deployed.yaml"):
     #     osdpl_content = yaml.safe_load_all(open(file_location))
     #     yaml.safe_load_all
-    #     tags["rockoon"] = 
-
+    #     tags["rockoon"] =
+    reporter = Reporter()
+    rp_client = reporter.client
     launch_id = None
     if suite_per_job:
         tags["jenkins_job"] = job.number
@@ -52,8 +46,15 @@
             description=f"Deploy job {job.url}"
         )
         print(f"(^-^)_日 report will be here {rp_client.get_launch_ui_url()}")
-
+    rp_client.log(time=timestamp(),
+                  message=f"Job status: {job.status}",
+                  # item_id=launch_id
+                  )
     for child in itertools.chain([job], job.heirs):
+        rp_client.log(time=timestamp(),
+                      message=f"{child} {child.status} {child.url}",
+                      # item_id=launch_id
+                      )
         print(f"⫍‍⌕‍⫎ tests in {child}")
         test_tags = deepcopy(tags)
         test_results_files = None
@@ -99,6 +100,9 @@
         if not test_results_files:
             # We can iterate by child jobs which don't contain any reports
             continue
+        rp_client.log(time=timestamp(),
+                      message=f"Found file to upload: {test_results_files}",
+                      item_id=launch_id)
         report_path = test_results_files[0]
         LOG.info("=== report_xml {kwargs}".format(
             kwargs = dict(report_path=report_path,
@@ -109,7 +113,7 @@
         )
 
         print(f"(っ・-・)っ Uploading {report_path}")
-        report_xml(
+        reporter.report_xml(
             report_path=report_path,
             title = title,
             attributes=test_tags,
@@ -120,6 +124,10 @@
         # if suite_per_job:
         #
         #     LOG.info(rp_client.get_launch_info())
+    rp_client.log(time=timestamp(),
+                  message="Reporting completed",
+                  # item_id=launch_id
+                  )
     if suite_per_job:
         report_url = rp_client.get_launch_ui_url()
         rp_client.finish_launch(end_time=timestamp())
@@ -160,7 +168,7 @@
     :param report_path: Url or file location of xunit report
     """
     title = report_path.split("/")[-1]
-    report_xml(
+    Reporter().report_xml(
         report_path=report_path,
         title=title,
     )
diff --git a/rp_reporter/rp_reporter/report_from_xml.py b/rp_reporter/rp_reporter/report_from_xml.py
index 75bc891..d55171e 100644
--- a/rp_reporter/rp_reporter/report_from_xml.py
+++ b/rp_reporter/rp_reporter/report_from_xml.py
@@ -9,6 +9,8 @@
 import re
 import wget
 import time
+import uuid
+from typing import Optional
 from xml.etree import ElementTree
 
 from reportportal_client import RPClient
@@ -17,219 +19,246 @@
 from .settings import RP_ENDPOINT, RP_APIKEY, RP_PROJECT
 
 LOG = logging.getLogger("rp_reporter")
-scheduled = []
-uuid_by_subfolder = dict()
 
-client = RPClient(endpoint=RP_ENDPOINT,
-              project=RP_PROJECT,
-              api_key=RP_APIKEY,
-              is_skipped_an_issue=False,
-              truncate_attributes=False
-              )
 
-def schedule_finishing(item):
-    scheduled.append(item)
-
-def finish_all():
-    while scheduled:
-        finishing_item = scheduled.pop()
-        client.finish_test_item(item_id=finishing_item,
-                                end_time=timestamp()
-                                )
-
-def create_subfolders(list_of_subfolders:list, root_folder=None) -> str:
-    parent_folder = root_folder
-    for number, subfolder in enumerate(list_of_subfolders):
-        subfolder_fullpath = list_of_subfolders[:number]
-        subfolder_fullpath.append(subfolder)
-        fullpath = ".".join(subfolder_fullpath)
-        if fullpath not in uuid_by_subfolder.keys():
-            created_folder = client.start_test_item(
-                name=subfolder,
-                start_time=timestamp(),
-                item_type="suite",
-                parent_item_id=parent_folder
-            )
-            LOG.debug(f"start_test_item {subfolder=} "
-                      f"item_type=suite "
-                      f"name={subfolder} "
-                      f"parent_item_id={parent_folder}")
-
-            LOG.info(f"Started suite: uuid={created_folder} for suite {subfolder=} in {fullpath=} with {parent_folder=}")
-            schedule_finishing(created_folder)
-            uuid_by_subfolder[fullpath] = created_folder
-        folder_uuid = uuid_by_subfolder.get(fullpath)
-        parent_folder = folder_uuid
-    return parent_folder
-
-def report_xml(report_path, title,
-               attributes=None, link=None, to_specific_launch=None):
-    ts: xunitparser.TestSuite
-    tr: xunitparser.TestResult
-    tc: xunitparser.TestCase
-    if not attributes:
-        attributes = dict()
-    # FIXME rewrite it to not use globals
-    global uuid_by_subfolder
-    uuid_by_subfolder = dict()
-    global scheduled
-    scheduled = list()
-    _report_path = str(report_path)
+def obtain_file(report_path:str) -> Optional[str]:
+    """
+    Returns file's location in file system
+    If report_path is an URL script will download it to /tmp folder
+    :param report_path: str - file system or network location
+    :return:
+    """
     if report_path.startswith("http"):
         try:
-            report_path = wget.download(report_path, f"/tmp/")
+            downloaded_file = wget.download(report_path,
+                                        f"/tmp/{uuid.uuid4()}.xml")
+            return downloaded_file
         except urllib.error.HTTPError:
             print("File is absent")
-            return
-        try:
-            ElementTree.fromstring(open(report_path).read())
-        except ElementTree.ParseError:
-            print("File is not in XML format")
-            return
+            return None
     if pathlib.Path(report_path).exists():
-        report_file = report_path
+        return report_path
     else:
         raise FileNotFoundError(report_path)
-    ts, tr = xunitparser.parse(open(report_file))
-    # ipdb.set_trace()
 
-    # xunitparser can't provide start_time from xml
-    # so we can use only now()
-    # if tr.timestamp:
-    #     start_time = datetime.datetime.fromisoformat(tr.timestamp)
-    # ipdb.set_trace()
-    # if tr.time
-    start_time = int(os.stat(report_file).st_birthtime * 1000) # to cut float part
-    attributes.update(ts.properties)
 
-    last_subfolder = None
-    if to_specific_launch:
-        test_suite = client.start_test_item(
-            name=title,
-            start_time=timestamp(),
-            attributes=attributes or None,
-            item_type="suite",
-            description=f"{link} \n uploaded from report {_report_path} ",
-        )
-        LOG.debug(f"start_test_item {test_suite=} item_type=suite name={title}")
-        schedule_finishing(item=test_suite)
-        root_folder = test_suite
-    else:
-        launch_id = client.start_launch(name=title,
-                            start_time=timestamp(),
-                            attributes=attributes or None,
-                            description=f"{link} \n uploaded from report {_report_path} "
-                            )
-        LOG.debug(f"start_launch {launch_id=} name={title} ")
-        print(f"(^-^)_日 report will be here {client.get_launch_ui_url()}")
-        root_folder = None
-        if not launch_id:
-            # ipdb.set_trace()
-            raise Exception(f"Launch {title} is not published ")
+def is_xml(file_path:str) -> bool:
+    try:
+        ElementTree.fromstring(open(file_path).read())
+        return True
+    except ElementTree.ParseError:
+        print("File is not in XML format")
+        return False
 
-    LOG.info(f"Sending to RP")
 
-    ts_with_progress_bar = click.progressbar(ts, 
-                                             label='Sending to RP',
-                                            #  item_show_func=lambda a: a.methodname if a is not None,
-                                             length=tr.testsRun,
-                                             show_percent=True,
-                                             bar_template='[%(bar)s] %(info)s %(label)s'
-                                             )
-    with ts_with_progress_bar: 
-        started_at = time.time()
+class Reporter:
+    def __init__(self, client=None):
+        if not client:
+            self.client = RPClient(endpoint=RP_ENDPOINT,
+                  project=RP_PROJECT,
+                  api_key=RP_APIKEY,
+                  is_skipped_an_issue=False,
+                  truncate_attributes=False
+                  )
+        self.scheduled = list()
+        self.uuid_by_subfolder = dict()
+        self.attributes = dict()
 
-        for tc in ts_with_progress_bar:
-            if tc.classname:
-                last_subfolder = create_subfolders(
-                    list_of_subfolders=tc.classname.split("."),
-                    root_folder=root_folder
-                )
-            elif "setup" in tc.methodname.lower() or "teardown" in tc.methodname.lower():
-                # setup and teardown don't have classname but have path in their name like
-                # in tempest:
-                # setUpClass (tempest.api.compute.admin.test_create_server.WindowsServers11Test)
-                found_text: list = re.findall(r"\([\w.]+\)", tc.methodname)
-                if found_text:
-                    found_text: str = found_text[-1]
-                if found_text:
-                    found_text: str = found_text.strip("()")
-                last_subfolder = create_subfolders(
-                    list_of_subfolders=found_text.split("."),
-                    root_folder=root_folder)
-                # name = f"{tc.classname}.{tc.methodname}"
-            # else:
-            name = tc.methodname
-            elapsed = time.time() - started_at
-            ts_with_progress_bar.label = f"{elapsed:.2f}s {name}".replace("\n", " ")
+    def schedule_finishing(self, item) -> None:
+        self.scheduled.append(item)
 
-            # It's a bad way to detect setup\teardown because every testing
-            # framework has his own way to log setup\teardown
-            # Also test name can contain setup
-            # if "setup" in tc.methodname.lower():
-            #     item_type="setup"
-            # elif "teardown" in tc.methodname.lower():
-            #     item_type="teardown"
-            # else:
-            #     item_type = "STEP"
-            item_type = "STEP"
-
-            # ipdb.set_trace()
-            test_started_at = timestamp()
-            item_id = client.start_test_item(
-                name=name,
-                start_time=test_started_at,
-                item_type=item_type,
-                description=f"{tc.classname}.{tc.methodname}",
-                parent_item_id=last_subfolder
-            )
-            LOG.debug(f"start_test_item {item_id=} "
-                      f"{name=} "
-                      f"{item_type=} "
-                      f"parent_item_id={last_subfolder}")
-            if not item_id:
-                raise Exception(f"Failed to start test {name}")
-            match tc.result:
-                case "success":
-                    status = "PASSED"
-                case "skipped":
-                    status = "SKIPPED"
-                case "failure":
-                    status = "FAILED"
-                case "error":
-                    status = "FAILED"
-                case _:
-                    raise BaseException(f"Unknown {tc.result=} in xml")
-
-            # LOG.debug(f"Logging {tc.trace}")
-            # ipdb.set_trace()
-            if tc.message:
-                client.log(time=timestamp(),
-                        message=tc.message,
-                        item_id=item_id
-                        )
-            if tc.trace:
-                client.log(time=timestamp(),
-                        message=tc.trace,
-                        item_id=item_id
-                        )
-            
-            # timestamp() 1739905243451 in milliseconds
-            # tc.time datetime.timedelta(microseconds=259000)
-            end_time_with_duration = datetime.datetime.fromtimestamp(
-                int(test_started_at) / 1000) + tc.time
-            end_time_in_milliseconds = int(
-                end_time_with_duration.timestamp() * 1000)
-
-            client.finish_test_item(item_id=item_id,
-                                    end_time=str(end_time_in_milliseconds),
-                                    status=status,
+    def finish_all(self) -> None:
+        while self.scheduled:
+            finishing_item = self.scheduled.pop()
+            self.client.finish_test_item(item_id=finishing_item,
+                                    end_time=timestamp()
                                     )
-            finish_all()
+        self.reset_cache()
 
-    if not to_specific_launch:
-        client.finish_launch(end_time=timestamp())
-    LOG.info(client.get_launch_info())
+    def reset_cache(self) -> None:
+        self.scheduled = list()
+        self.uuid_by_subfolder = dict()
+
+    def create_subfolders(self,
+                          list_of_subfolders: list, root_folder=None) -> str:
+        parent_folder = root_folder
+        for number, subfolder in enumerate(list_of_subfolders):
+            subfolder_fullpath = list_of_subfolders[:number]
+            subfolder_fullpath.append(subfolder)
+            fullpath = ".".join(subfolder_fullpath)
+            if fullpath not in self.uuid_by_subfolder.keys():
+                created_folder = self.client.start_test_item(
+                    name=subfolder,
+                    start_time=timestamp(),
+                    item_type="suite",
+                    parent_item_id=parent_folder
+                )
+                LOG.debug(f"start_test_item {subfolder=} "
+                          f"item_type=suite "
+                          f"name={subfolder} "
+                          f"parent_item_id={parent_folder}")
+
+                LOG.info(
+                    f"Started suite: uuid={created_folder} for suite "
+                    f"{subfolder=} in {fullpath=} with {parent_folder=}")
+                self.schedule_finishing(created_folder)
+                self.uuid_by_subfolder[fullpath] = created_folder
+            folder_uuid = self.uuid_by_subfolder.get(fullpath)
+            parent_folder = folder_uuid
+        return parent_folder
+
+    def report_xml(self, report_path, title, attributes=None,
+                   link=None, to_specific_launch=None):
+        ts: xunitparser.TestSuite
+        tr: xunitparser.TestResult
+        tc: xunitparser.TestCase
+        report_file = obtain_file(report_path)
+        if not report_file:
+            print("Error occurred with file. Interrupting reporting")
+            return
+        ts, tr = xunitparser.parse(open(report_file))
+        if not ts:
+            print(f"{ts=} is empty in {report_file=}. Interrupting reporting")
+            return
+
+        # xunitparser can't provide start_time from xml
+        # so we can use only now()
+        # if tr.timestamp:
+        #     start_time = datetime.datetime.fromisoformat(tr.timestamp)
+        # ipdb.set_trace()
+        # if tr.time
+        # start_time = int(os.stat(report_file).st_birthtime * 1000) # to cut float part
+        attributes.update(ts.properties)
+
+        last_subfolder = None
+        if to_specific_launch:
+            test_suite = self.client.start_test_item(
+                name=title,
+                start_time=timestamp(),
+                attributes=attributes or None,
+                item_type="suite",
+                description=f"{link} \n uploaded from report {report_path} ",
+            )
+            LOG.debug(
+                f"start_test_item {test_suite=} item_type=suite name={title}")
+            self.schedule_finishing(item=test_suite)
+            root_folder = test_suite
+        else:
+            launch_id = self.client.start_launch(name=title,
+                                            start_time=timestamp(),
+                                            attributes=attributes or None,
+                                            description=f"{link} \n uploaded from report {report_path} "
+                                            )
+            LOG.debug(f"start_launch {launch_id=} name={title} ")
+            print(f"(^-^)_日 report will be here {self.client.get_launch_ui_url()}")
+            root_folder = None
+            if not launch_id:
+                # ipdb.set_trace()
+                raise Exception(f"Launch {title} is not published ")
+
+        LOG.info(f"Sending to RP")
+
+        ts_with_progress_bar = click.progressbar(ts,
+                                                 label='Sending to RP',
+                                                 #  item_show_func=lambda a: a.methodname if a is not None,
+                                                 length=tr.testsRun,
+                                                 show_percent=True,
+                                                 bar_template='[%(bar)s] %(info)s %(label)s'
+                                                 )
+        with ts_with_progress_bar:
+            started_at = time.time()
+
+            for tc in ts_with_progress_bar:
+                if tc.classname:
+                    last_subfolder = self.create_subfolders(
+                        list_of_subfolders=tc.classname.split("."),
+                        root_folder=root_folder
+                    )
+                elif "setup" in tc.methodname.lower() or "teardown" in tc.methodname.lower():
+                    # setup and teardown don't have classname but have path in their name like
+                    # in tempest:
+                    # setUpClass (tempest.api.compute.admin.test_create_server.WindowsServers11Test)
+                    found_text: list = re.findall(r"\([\w.]+\)", tc.methodname)
+                    if found_text:
+                        found_text: str = found_text[-1]
+                    if found_text:
+                        found_text: str = found_text.strip("()")
+                    last_subfolder = self.create_subfolders(
+                        list_of_subfolders=found_text.split("."),
+                        root_folder=root_folder)
+                    # name = f"{tc.classname}.{tc.methodname}"
+                # else:
+                name = tc.methodname
+                elapsed = time.time() - started_at
+                ts_with_progress_bar.label = f"{elapsed:.2f}s {name}".replace(
+                    "\n", " ")
+
+                # It's a bad way to detect setup\teardown because every testing
+                # framework has his own way to log setup\teardown
+                # Also test name can contain setup
+                # if "setup" in tc.methodname.lower():
+                #     item_type="setup"
+                # elif "teardown" in tc.methodname.lower():
+                #     item_type="teardown"
+                # else:
+                #     item_type = "STEP"
+                item_type = "STEP"
+
+                test_started_at = timestamp()
+                item_id = self.client.start_test_item(
+                    name=name,
+                    start_time=test_started_at,
+                    item_type=item_type,
+                    description=f"{tc.classname}.{tc.methodname}",
+                    parent_item_id=last_subfolder
+                )
+                LOG.debug(f"start_test_item {item_id=} "
+                          f"{name=} "
+                          f"{item_type=} "
+                          f"parent_item_id={last_subfolder}")
+                if not item_id:
+                    raise Exception(f"Failed to start test {name}")
+                match tc.result:
+                    case "success":
+                        status = "PASSED"
+                    case "skipped":
+                        status = "SKIPPED"
+                    case "failure":
+                        status = "FAILED"
+                    case "error":
+                        status = "FAILED"
+                    case _:
+                        raise BaseException(f"Unknown {tc.result=} in xml")
+
+                # LOG.debug(f"Logging {tc.trace}")
+                # ipdb.set_trace()
+                if tc.message:
+                    self.client.log(time=timestamp(),
+                               message=tc.message,
+                               item_id=item_id
+                               )
+                if tc.trace:
+                    self.client.log(time=timestamp(),
+                               message=tc.trace,
+                               item_id=item_id
+                               )
+
+                # timestamp() 1739905243451 in milliseconds
+                # tc.time datetime.timedelta(microseconds=259000)
+                end_time_with_duration = datetime.datetime.fromtimestamp(
+                    int(test_started_at) / 1000) + tc.time
+                end_time_in_milliseconds = int(
+                    end_time_with_duration.timestamp() * 1000)
+
+                self.client.finish_test_item(item_id=item_id,
+                                        end_time=str(end_time_in_milliseconds),
+                                        status=status,
+                                        )
+                self.finish_all()
+
+        if not to_specific_launch:
+            self.client.finish_launch(end_time=timestamp())
+        LOG.info(self.client.get_launch_info())
 
 
 if __name__ == "__main__":
@@ -242,11 +271,10 @@
     }
     link = ""
     
-    
-    report_xml(
+    reporter = Reporter()
+    reporter.report_xml(
         report_path=report_path,
         title = title,
-        attributes=attributes,
         link=link,
         # to_specific_launch="432ce97b-2727-4e4c-8303-9f1d966a184e"
         )
diff --git a/rp_reporter/rp_reporter/settings.py b/rp_reporter/rp_reporter/settings.py
index 773e6bc..3ca05bf 100644
--- a/rp_reporter/rp_reporter/settings.py
+++ b/rp_reporter/rp_reporter/settings.py
@@ -3,7 +3,6 @@
 from pathlib import Path
 import logging
 
-
 LOG = logging.getLogger("rp_reporter")
 
 RP_CONFIG_FILE = environ.get("RP_CONFIG_FILE") or (Path.home() / ".reportportal_config")
@@ -29,12 +28,12 @@
 RP_PROJECT = environ.get('RP_PROJECT') or from_conf('RP_PROJECT') or call_error("RP_PROJECT")
 
 RP_LOG_FILE = environ.get('RP_LOG_FILE') or from_conf('RP_LOG_FILE') or (Path.cwd() / "report.log")
-RP_LOGGING = environ.get('RP_LOGGING') or from_conf('RP_LOGGING') or logging.INFO
+RP_LOGGING = environ.get('RP_LOGGING') or from_conf('RP_LOGGING') or logging.WARNING
 
 logging.basicConfig(level=RP_LOGGING,
                     format='%(asctime)s %(levelname)s - %(filename)s:%(lineno)d (%(funcName)s) - %(message)s',
-                    filename=RP_LOG_FILE,
-                    filemode='w'
+                    # filename=RP_LOG_FILE,
+                    # filemode='w'
                     )
 
 if __name__ == "__main__":