files: 
  """ Base Metadata Classes """
  
  import copy
  import dataclasses
  import enum
  import functools
  import os
  import re
  import shutil
  import subprocess
  import sys
  import time
  from typing import Any, Dict, Generator, List, Optional, Tuple, Union, cast
  
  import click
  import fmf
  import fmf.base
  from click import confirm, echo, style
  from fmf.utils import listed
  from ruamel.yaml.error import MarkedYAMLError
  
  import tmt.export
  import tmt.identifier
  import tmt.steps
  import tmt.steps.discover
  import tmt.steps.execute
  import tmt.steps.finish
  import tmt.steps.prepare
  import tmt.steps.provision
  import tmt.steps.report
  import tmt.templates
  import tmt.utils
  from tmt.utils import FmfContextType, verdict
  
  if sys.version_info >= (3, 8):
      from typing import Literal, TypedDict
  else:
      from typing_extensions import Literal, TypedDict
  
  # Default test duration is 5m for individual tests discovered from L1
  # metadata and 1h for scripts defined directly in plans (L2 metadata).
  DEFAULT_TEST_DURATION_L1 = '5m'
  DEFAULT_TEST_DURATION_L2 = '1h'
  DEFAULT_ORDER = 50
  
  # How many already existing lines should tmt run --follow show
  FOLLOW_LINES = 10
  
  # Obsoleted test keys
  OBSOLETED_TEST_KEYS = "relevancy coverage".split()
  
  # Unofficial temporary test keys
  EXTRA_TEST_KEYS = (
      "extra-nitrate extra-hardware extra-pepa "
      "extra-summary extra-task id".split())
  
  # Unofficial temporary story keys
  EXTRA_STORY_KEYS = ("id".split())
  
  SECTIONS_HEADINGS = {
      'Setup': ['<h1>Setup</h1>'],
      'Test': ['<h1>Test</h1>',
               '<h1>Test .*</h1>'],
      'Step': ['<h2>Step</h2>',
               '<h2>Test Step</h2>'],
      'Expect': ['<h2>Expect</h2>',
                 '<h2>Result</h2>',
                 '<h2>Expected Result</h2>'],
      'Cleanup': ['<h1>Cleanup</h1>']
      }
  
  # Extra keys used for identification in Result class
  EXTRA_RESULT_IDENTIFICATION_KEYS = ['extra-nitrate', 'extra-task']
  
  
  #
  # fmf id types
  #
  # See https://fmf.readthedocs.io/en/latest/concept.html#identifiers for
  # formal specification.
  #
  
  # A "raw" fmf id as stored in fmf node data, i.e. as a mapping with various keys.
  # Used for a brief moment, to annotate raw fmf data before they are converted
  # into FmfId instances.
  _RawFmfId = TypedDict('_RawFmfId', {
      'url': Optional[str],
      'ref': Optional[str],
      'path': Optional[str],
      'name': Optional[str]
  })
  
  
  # An internal fmf id representation.
  @dataclasses.dataclass
  class FmfId:
      url: Optional[str] = None
      ref: Optional[str] = None
      path: Optional[str] = None
      name: Optional[str] = None
  
      def validate(self) -> Tuple[bool, str]:
          """
          Validate fmf id and return a human readable error
  
          Return a tuple (boolean, message) as the result of validation.
          The boolean specifies the validation result and the message
          the validation error. In case the FMF id is valid, return an empty
          string as the message.
          """
          # Validate remote id and translate to human readable errors
          try:
              # Simple asdict() is not good enough, fmf does not like keys that exist but are `None`.
              # Don't include those.
              fmf.base.Tree.node({
                  key: value for key, value in dataclasses.asdict(self).items()
                  if value is not None
                  })
          except fmf.utils.GeneralError as error:
              # Map fmf errors to more user friendly alternatives
              error_map: List[Tuple[str, str]] = [
                  ('git clone', f"repo '{self.url}' cannot be cloned"),
                  ('git checkout', f"git ref '{self.ref}' is invalid"),
                  ('directory path', f"path '{self.path}' is invalid"),
                  ('tree root', f"No tree found in repo '{self.url}', missing an '.fmf' directory?")
                  ]
  
              stringified_error = str(error)
  
              errors = [message for needle, message in error_map if needle in stringified_error]
              return (False, errors[0] if errors else stringified_error)
  
          return (True, '')
  
  #
  # Various types describing constructs as stored in "raw" fmf node data.
  # Used for a brief moment, to annotate raw fmf data before they are converted
  # into their internal representations.
  #
  
  
  # A type describing the raw form of the core `link` attribute. See
  # https://tmt.readthedocs.io/en/stable/spec/core.html#link for its
  # formal specification. Internally, the data is wrapped with `Link`
  # instances, the type below describes the raw data coming from Fmf
  # nodes.
  # TODO: `*Link._relations` would be much better, DRY, but that's allowed
  # since Python 3.11.
  _RawLinkRelation = Literal[
      'verifies', 'verified-by',
      'implements', 'implemented-by',
      'documents', 'documented-by',
      'blocks', 'blocked-by',
      'duplicates', 'duplicated-by',
      'parent', 'child',
      'relates',
      # Special case: not a relation, but it can appear where relations appear in
      # link data structures.
      'note'
  ]
  
  # A "relation": "link" subtype.
  #
  # An example from
  
  [... the rest is omitted ...]
files: tmt/base.py
files: 
  """ Base Metadata Classes """
  
  import copy
  import dataclasses
  import enum
  import functools
  import os
  import re
  import shutil
  import subprocess
  import sys
  import time
  from typing import Any, Dict, Generator, List, Optional, Tuple, Union, cast
  
  import click
  import fmf
  import fmf.base
  from click import confirm, echo, style
  from fmf.utils import listed
  from ruamel.yaml.error import MarkedYAMLError
  
  import tmt.export
  import tmt.identifier
  import tmt.steps
  import tmt.steps.discover
  import tmt.steps.execute
  import tmt.steps.finish
  import tmt.steps.prepare
  import tmt.steps.provision
  import tmt.steps.report
  import tmt.templates
  import tmt.utils
  from tmt.utils import FmfContextType, verdict
  
  if sys.version_info >= (3, 8):
      from typing import Literal, TypedDict
  else:
      from typing_extensions import Literal, TypedDict
  
  # Default test duration is 5m for individual tests discovered from L1
  # metadata and 1h for scripts defined directly in plans (L2 metadata).
  DEFAULT_TEST_DURATION_L1 = '5m'
  DEFAULT_TEST_DURATION_L2 = '1h'
  DEFAULT_ORDER = 50
  
  # How many already existing lines should tmt run --follow show
  FOLLOW_LINES = 10
  
  # Obsoleted test keys
  OBSOLETED_TEST_KEYS = "relevancy coverage".split()
  
  # Unofficial temporary test keys
  EXTRA_TEST_KEYS = (
      "extra-nitrate extra-hardware extra-pepa "
      "extra-summary extra-task id".split())
  
  # Unofficial temporary story keys
  EXTRA_STORY_KEYS = ("id".split())
  
  SECTIONS_HEADINGS = {
      'Setup': ['<h1>Setup</h1>'],
      'Test': ['<h1>Test</h1>',
               '<h1>Test .*</h1>'],
      'Step': ['<h2>Step</h2>',
               '<h2>Test Step</h2>'],
      'Expect': ['<h2>Expect</h2>',
                 '<h2>Result</h2>',
                 '<h2>Expected Result</h2>'],
      'Cleanup': ['<h1>Cleanup</h1>']
      }
  
  
  #
  # fmf id types
  #
  # See https://fmf.readthedocs.io/en/latest/concept.html#identifiers for
  # formal specification.
  #
  
  # A "raw" fmf id as stored in fmf node data, i.e. as a mapping with various keys.
  # Used for a brief moment, to annotate raw fmf data before they are converted
  # into FmfId instances.
  _RawFmfId = TypedDict('_RawFmfId', {
      'url': Optional[str],
      'ref': Optional[str],
      'path': Optional[str],
      'name': Optional[str]
  })
  
  
  # An internal fmf id representation.
  @dataclasses.dataclass
  class FmfId:
      url: Optional[str] = None
      ref: Optional[str] = None
      path: Optional[str] = None
      name: Optional[str] = None
  
      def validate(self) -> Tuple[bool, str]:
          """
          Validate fmf id and return a human readable error
  
          Return a tuple (boolean, message) as the result of validation.
          The boolean specifies the validation result and the message
          the validation error. In case the FMF id is valid, return an empty
          string as the message.
          """
          # Validate remote id and translate to human readable errors
          try:
              # Simple asdict() is not good enough, fmf does not like keys that exist but are `None`.
              # Don't include those.
              fmf.base.Tree.node({
                  key: value for key, value in dataclasses.asdict(self).items()
                  if value is not None
                  })
          except fmf.utils.GeneralError as error:
              # Map fmf errors to more user friendly alternatives
              error_map: List[Tuple[str, str]] = [
                  ('git clone', f"repo '{self.url}' cannot be cloned"),
                  ('git checkout', f"git ref '{self.ref}' is invalid"),
                  ('directory path', f"path '{self.path}' is invalid"),
                  ('tree root', f"No tree found in repo '{self.url}', missing an '.fmf' directory?")
                  ]
  
              stringified_error = str(error)
  
              errors = [message for needle, message in error_map if needle in stringified_error]
              return (False, errors[0] if errors else stringified_error)
  
          return (True, '')
  
  #
  # Various types describing constructs as stored in "raw" fmf node data.
  # Used for a brief moment, to annotate raw fmf data before they are converted
  # into their internal representations.
  #
  
  
  # A type describing the raw form of the core `link` attribute. See
  # https://tmt.readthedocs.io/en/stable/spec/core.html#link for its
  # formal specification. Internally, the data is wrapped with `Link`
  # instances, the type below describes the raw data coming from Fmf
  # nodes.
  # TODO: `*Link._relations` would be much better, DRY, but that's allowed
  # since Python 3.11.
  _RawLinkRelation = Literal[
      'verifies', 'verified-by',
      'implements', 'implemented-by',
      'documents', 'documented-by',
      'blocks', 'blocked-by',
      'duplicates', 'duplicated-by',
      'parent', 'child',
      'relates',
      # Special case: not a relation, but it can appear where relations appear in
      # link data structures.
      'note'
  ]
  
  # A "relation": "link" subtype.
  #
  # An example from TMT docs says:
  #
  # link:
  #   verifies: /stories/cli/init/base
  #
  # link:
  #     blocked-by
  
  [... the rest is omitted ...]
files: tmt/base.py
files: # coding: utf-8
  
  """ Export metadata into nitrate """
  
  
  import email.utils
  import os
  import re
  import traceback
  import types
  import xmlrpc.client
  from functools import lru_cache
  from typing import Any, Dict, Generator, List, Optional, Tuple, Union, cast
  
  import fmf
  from click import echo, secho, style
  
  import tmt
  import tmt.utils
  from tmt.convert import SYSTEM_OTHER, add_link
  from tmt.identifier import ID_KEY, add_uuid_if_not_defined
  from tmt.utils import ConvertError, check_git_url, markdown_to_html
  
  bugzilla: Optional[types.ModuleType] = None
  gssapi: Optional[types.ModuleType] = None
  nitrate: Optional[types.ModuleType] = None
  
  PolarionException: Any = None
  PolarionTestCase: Any = None
  PolarionWorkItem: Any = None
  
  DEFAULT_PRODUCT: Any = None
  
  SectionsReturnType = Tuple[str, str, str, str]
  HeadingsType = List[List[Union[int, str]]]
  SectionsHeadingsType = Dict[str, HeadingsType]
  
  log = fmf.utils.Logging('tmt').logger
  
  WARNING = """
  Test case has been migrated to git. Any changes made here might be overwritten.
  See: https://tmt.readthedocs.io/en/latest/questions.html#nitrate-migration
  """.lstrip()
  
  # For linking bugs
  BUGZILLA_XMLRPC_URL = "https://bugzilla.redhat.com/xmlrpc.cgi"
  NITRATE_TRACKER_ID = 69  # ID of nitrate in RH's bugzilla
  POLARION_TRACKER_ID = 117  # ID of polarion in RH's bugzilla
  RE_BUGZILLA_URL = r'bugzilla.redhat.com/show_bug.cgi\?id=(\d+)'
  RE_POLARION_URL = r'.*/polarion/#/project/.*/workitem\?id=(.*)'
  LEGACY_POLARION_PROJECTS = set(['RedHatEnterpriseLinux7'])
  
  
  def import_nitrate() -> Any:
      """ Conditionally import the nitrate module """
      # Need to import nitrate only when really needed. Otherwise we get
      # traceback when nitrate not installed or config file not available.
      # And we want to keep the core tmt package with minimal dependencies.
      try:
          global nitrate, DEFAULT_PRODUCT, gssapi
          import gssapi
          import nitrate
          assert nitrate
          DEFAULT_PRODUCT = nitrate.Product(name='RHEL Tests')
          return nitrate
      except ImportError:
          raise ConvertError(
              "Install tmt-test-convert to export tests to nitrate.")
      except nitrate.NitrateError as error:  # type: ignore
          raise ConvertError(error)
  
  
  def import_polarion() -> None:
      """ Import polarion python api - pylero """
      try:
          global PolarionException, PolarionTestCase, PolarionWorkItem
          from pylero.exceptions import PyleroLibException as PolarionException
          from pylero.work_item import TestCase as PolarionTestCase
          from pylero.work_item import _WorkItem as PolarionWorkItem
      except ImportError:
          raise ConvertError(
              "Run 'pip install tmt[export-polarion]' so pylero is installed.")
      except PolarionException as exc:
          log.debug(traceback.format_exc())
          raise ConvertError("Failed to login with pylero", original=exc)
  
  
  def get_bz_instance() -> Any:
      """ Import the bugzilla module and return BZ instance """
      try:
          import bugzilla
      except ImportError:
          raise ConvertError(
              "Install 'tmt-test-convert' to link test to the bugzilla.")
  
      try:
          bz_instance = bugzilla.Bugzilla(url=BUGZILLA_XMLRPC_URL)
      except Exception as exc:
          log.debug(traceback.format_exc())
          raise ConvertError(
              "Couldn't initialize the Bugzilla client.", original=exc)
  
      if not bz_instance.logged_in:
          raise ConvertError(
              "Not logged to Bugzilla, check 'man bugzilla' section "
              "'AUTHENTICATION CACHE AND API KEYS'.")
      return bz_instance
  
  
  def _nitrate_find_fmf_testcases(test: 'tmt.Test') -> Generator[Any, None, None]:
      """
      Find all Nitrate test cases with the same fmf identifier
  
      All component general plans are explored for possible duplicates.
      """
      assert nitrate
      for component in test.component:
          try:
              for testcase in find_general_plan(component).testcases:
                  struct_field = tmt.utils.StructuredField(testcase.notes)
                  try:
                      fmf_id = tmt.utils.yaml_to_dict(struct_field.get('fmf'))
                      if fmf_id == test.fmf_id:
                          echo(style(
                              f"Existing test case '{testcase.identifier}' "
                              f"found for given fmf id.", fg='magenta'))
                          yield testcase
                  except tmt.utils.StructuredFieldError:
                      pass
          except nitrate.NitrateError:
              pass
  
  
  def convert_manual_to_nitrate(test_md: str) -> SectionsReturnType:
      """
      Convert Markdown document to html sections.
  
      These sections can be exported to nitrate.
      Expects: Markdown document as a file.
      Returns: tuple of (step, expect, setup, cleanup) sections
      as html strings.
      """
  
      sections_headings: SectionsHeadingsType = {
          heading: []
          for heading_list in tmt.base.SECTIONS_HEADINGS.values()
          for heading in heading_list
          }
  
      html = markdown_to_html(test_md)
      html_splitlines = html.splitlines()
  
      for key in sections_headings.keys():
          result: HeadingsType = []
          i = 0
          while html_splitlines:
              try:
                  if re.search("^" + key + "$", html_splitlines[i]):
                      html_content = str()
                      if key.startswith('<h1>
  
  [... the rest is omitted ...]
files: tmt/export.py
files: # coding: utf-8
  
  """ Export metadata into nitrate """
  
  
  import email.utils
  import os
  import re
  import traceback
  import types
  import xmlrpc.client
  from functools import lru_cache
  from typing import Any, Dict, Generator, List, Optional, Tuple, Union, cast
  
  import fmf
  from click import echo, secho, style
  
  import tmt
  import tmt.utils
  from tmt.convert import SYSTEM_OTHER, add_link
  from tmt.identifier import ID_KEY, add_uuid_if_not_defined
  from tmt.utils import ConvertError, check_git_url, markdown_to_html
  
  bugzilla: Optional[types.ModuleType] = None
  gssapi: Optional[types.ModuleType] = None
  nitrate: Optional[types.ModuleType] = None
  
  PolarionException: Any = None
  PolarionTestCase: Any = None
  PolarionWorkItem: Any = None
  
  DEFAULT_PRODUCT: Any = None
  
  SectionsReturnType = Tuple[str, str, str, str]
  HeadingsType = List[List[Union[int, str]]]
  SectionsHeadingsType = Dict[str, HeadingsType]
  
  log = fmf.utils.Logging('tmt').logger
  
  WARNING = """
  Test case has been migrated to git. Any changes made here might be overwritten.
  See: https://tmt.readthedocs.io/en/latest/questions.html#nitrate-migration
  """.lstrip()
  
  # For linking bugs
  BUGZILLA_XMLRPC_URL = "https://bugzilla.redhat.com/xmlrpc.cgi"
  NITRATE_TRACKER_ID = 69  # ID of nitrate in RH's bugzilla
  POLARION_TRACKER_ID = 117  # ID of polarion in RH's bugzilla
  RE_BUGZILLA_URL = r'bugzilla.redhat.com/show_bug.cgi\?id=(\d+)'
  RE_POLARION_URL = r'.*/polarion/#/project/.*/workitem\?id=(.*)'
  LEGACY_POLARION_PROJECTS = set(['RedHatEnterpriseLinux7'])
  
  
  def import_nitrate() -> Any:
      """ Conditionally import the nitrate module """
      # Need to import nitrate only when really needed. Otherwise we get
      # traceback when nitrate not installed or config file not available.
      # And we want to keep the core tmt package with minimal dependencies.
      try:
          global nitrate, DEFAULT_PRODUCT, gssapi
          import gssapi
          import nitrate
          assert nitrate
          DEFAULT_PRODUCT = nitrate.Product(name='RHEL Tests')
          return nitrate
      except ImportError:
          raise ConvertError(
              "Install tmt-test-convert to export tests to nitrate.")
      except nitrate.NitrateError as error:  # type: ignore
          raise ConvertError(error)
  
  
  def import_polarion() -> None:
      """ Import polarion python api - pylero """
      try:
          global PolarionException, PolarionTestCase, PolarionWorkItem
          from pylero.exceptions import PyleroLibException as PolarionException
          from pylero.work_item import TestCase as PolarionTestCase
          from pylero.work_item import _WorkItem as PolarionWorkItem
      except ImportError:
          raise ConvertError(
              "Run 'pip install tmt[export-polarion]' so pylero is installed.")
      except PolarionException as exc:
          log.debug(traceback.format_exc())
          raise ConvertError("Failed to login with pylero", original=exc)
  
  
  def get_bz_instance() -> Any:
      """ Import the bugzilla module and return BZ instance """
      try:
          import bugzilla
      except ImportError:
          raise ConvertError(
              "Install 'tmt-test-convert' to link test to the bugzilla.")
  
      try:
          bz_instance = bugzilla.Bugzilla(url=BUGZILLA_XMLRPC_URL)
      except Exception as exc:
          log.debug(traceback.format_exc())
          raise ConvertError(
              "Couldn't initialize the Bugzilla client.", original=exc)
  
      if not bz_instance.logged_in:
          raise ConvertError(
              "Not logged to Bugzilla, check 'man bugzilla' section "
              "'AUTHENTICATION CACHE AND API KEYS'.")
      return bz_instance
  
  
  def _nitrate_find_fmf_testcases(test: 'tmt.Test') -> Generator[Any, None, None]:
      """
      Find all Nitrate test cases with the same fmf identifier
  
      All component general plans are explored for possible duplicates.
      """
      assert nitrate
      for component in test.component:
          try:
              for testcase in find_general_plan(component).testcases:
                  struct_field = tmt.utils.StructuredField(testcase.notes)
                  try:
                      fmf_id = tmt.utils.yaml_to_dict(struct_field.get('fmf'))
                      if fmf_id == test.fmf_id:
                          echo(style(
                              f"Existing test case '{testcase.identifier}' "
                              f"found for given fmf id.", fg='magenta'))
                          yield testcase
                  except tmt.utils.StructuredFieldError:
                      pass
          except nitrate.NitrateError:
              pass
  
  
  def convert_manual_to_nitrate(test_md: str) -> SectionsReturnType:
      """
      Convert Markdown document to html sections.
  
      These sections can be exported to nitrate.
      Expects: Markdown document as a file.
      Returns: tuple of (step, expect, setup, cleanup) sections
      as html strings.
      """
  
      sections_headings: SectionsHeadingsType = {
          heading: []
          for heading_list in tmt.base.SECTIONS_HEADINGS.values()
          for heading in heading_list
          }
  
      html = markdown_to_html(test_md)
      html_splitlines = html.splitlines()
  
      for key in sections_headings.keys():
          result: HeadingsType = []
          i = 0
          while html_splitlines:
              try:
                  if re.search("^" + key + "$", html_splitlines[i]):
                      html_content = str()
                      if key.startswith('<h1>
  
  [... the rest is omitted ...]
files: tmt/export.py
files: import datetime
  import os
  import xml.etree.ElementTree as ET
  from typing import Any, List, Optional
  
  import click
  from requests import post
  
  import tmt
  import tmt.steps
  
  from .junit import make_junit_xml
  
  DEFAULT_NAME = 'xunit.xml'
  
  
  @tmt.steps.provides_method('polarion')
  class ReportPolarion(tmt.steps.report.ReportPlugin):
      """
      Write test results into a xUnit file and upload to Polarion
      """
      _keys = ['file', 'no-upload', 'project-id', 'testrun-title']
  
      @classmethod
      def options(cls, how: Optional[str] = None) -> List[tmt.options.ClickOptionDecoratorType]:
          """ Prepare command line options """
          options = super().options(how)
          options[:0] = [
              click.option(
                  '--file', metavar='FILE', help='Path to the file to store xUnit in'),
              click.option(
                  '--upload / --no-upload', default=True, show_default=True,
                  help="Whether to upload results to Polarion"),
              click.option(
                  '--project-id', required=True, help='Use specific Polarion project ID'),
              click.option(
                  '--testrun-title', help='Use specific TestRun title')
              ]
          return options
  
      def go(self, *args: Any, **kwargs: Any) -> None:
          """ Go through executed tests and report into Polarion """
          super().go()
  
          from tmt.export import find_polarion_case_ids, import_polarion
          import_polarion()
          from tmt.export import PolarionWorkItem
          assert PolarionWorkItem
  
          title = self.get(
              'testrun_title',
              self.step.plan.name.rsplit('/', 1)[1] +
              datetime.datetime.now().strftime("%Y%m%d%H%M%S"))
          title = title.replace('-', '_')
          project_id = self.get('project-id')
          upload = self.get('upload')
  
          junit_suite = make_junit_xml(self)
          xml_tree = ET.fromstring(junit_suite.to_xml_string([junit_suite]))
  
          properties = {
              'polarion-project-id': project_id,
              'polarion-user-id': PolarionWorkItem._session.user_id,
              'polarion-testrun-id': title,
              'polarion-project-span-ids': project_id}
          testsuites_properties = ET.SubElement(xml_tree, 'properties')
          for name, value in properties.items():
              ET.SubElement(testsuites_properties, 'property', attrib={
                  'name': name, 'value': value})
  
          testsuite = xml_tree.find('testsuite')
          project_span_ids = xml_tree.find(
              '*property[@name="polarion-project-span-ids"]')
  
          for result in self.step.plan.execute.results():
              if not result.ids or not any(result.ids.values()):
                  raise tmt.utils.ReportError(
                      f"Test Case {result.name} is not exported to Polarion, "
                      "please run 'tmt tests export --how polarion' on it")
              work_item_id, test_project_id = find_polarion_case_ids(result.ids)
  
              if test_project_id is None:
                  raise tmt.utils.ReportError("Test case missing or not found in Polarion")
  
              assert work_item_id is not None
              assert project_span_ids is not None
  
              if test_project_id not in project_span_ids.attrib['value']:
                  project_span_ids.attrib['value'] += f',{test_project_id}'
  
              test_properties = {
                  'polarion-testcase-id': work_item_id,
                  'polarion-testcase-project-id': test_project_id}
  
              assert testsuite is not None
              test_case = testsuite.find(f"*[@name='{result.name}']")
              assert test_case is not None
              properties_elem = ET.SubElement(test_case, 'properties')
              for name, value in test_properties.items():
                  ET.SubElement(properties_elem, 'property', attrib={
                      'name': name, 'value': value})
  
          assert self.workdir is not None
  
          f_path = self.get("file", os.path.join(self.workdir, DEFAULT_NAME))
          with open(f_path, 'wb') as fw:
              ET.ElementTree(xml_tree).write(fw)
  
          if upload:
              server_url = str(PolarionWorkItem._session._server.url)
              polarion_import_url = (
                  f'{server_url}{"" if server_url.endswith("/") else "/"}'
                  'import/xunit')
              auth = (
                  PolarionWorkItem._session.user_id,
                  PolarionWorkItem._session.password)
  
              response = post(
                  polarion_import_url, auth=auth,
                  files={'file': ('xunit.xml', ET.tostring(xml_tree))})
              self.info(
                  f'Response code is {response.status_code} with text: {response.text}')
          else:
              self.info(f"xUnit file saved at: {f_path}")
              self.info("Polarion upload can be done manually using command:")
              self.info(
                  "curl -k -u <USER>:<PASSWORD> -X POST -F file=@<XUNIT_XML_FILE_PATH> "
                  "<POLARION_URL>/polarion/import/xunit")
  
  
  [... the rest is omitted ...]
files: tmt/steps/report/polarion.py
files: import datetime
  import os
  import xml.etree.ElementTree as ET
  from typing import Any, List, Optional
  
  import click
  from requests import post
  
  import tmt
  import tmt.steps
  
  from .junit import make_junit_xml
  
  DEFAULT_NAME = 'xunit.xml'
  
  
  @tmt.steps.provides_method('polarion')
  class ReportPolarion(tmt.steps.report.ReportPlugin):
      """
      Write test results into a xUnit file and upload to Polarion
      """
      _keys = ['file', 'no-upload', 'project-id', 'testrun-title']
  
      @classmethod
      def options(cls, how: Optional[str] = None) -> List[tmt.options.ClickOptionDecoratorType]:
          """ Prepare command line options """
          options = super().options(how)
          options[:0] = [
              click.option(
                  '--file', metavar='FILE', help='Path to the file to store xUnit in'),
              click.option(
                  '--upload / --no-upload', default=True, show_default=True,
                  help="Whether to upload results to Polarion"),
              click.option(
                  '--project-id', required=True, help='Use specific Polarion project ID'),
              click.option(
                  '--testrun-title', help='Use specific TestRun title')
              ]
          return options
  
      def go(self, *args: Any, **kwargs: Any) -> None:
          """ Go through executed tests and report into Polarion """
          super().go()
  
          from tmt.export import get_polarion_ids, import_polarion
          import_polarion()
          from tmt.export import PolarionWorkItem
          assert PolarionWorkItem
  
          title = self.get(
              'testrun_title',
              self.step.plan.name.rsplit('/', 1)[1] +
              datetime.datetime.now().strftime("%Y%m%d%H%M%S"))
          title = title.replace('-', '_')
          project_id = self.get('project-id')
          upload = self.get('upload')
  
          junit_suite = make_junit_xml(self)
          xml_tree = ET.fromstring(junit_suite.to_xml_string([junit_suite]))
  
          properties = {
              'polarion-project-id': project_id,
              'polarion-user-id': PolarionWorkItem._session.user_id,
              'polarion-testrun-id': title,
              'polarion-project-span-ids': project_id}
          testsuites_properties = ET.SubElement(xml_tree, 'properties')
          for name, value in properties.items():
              ET.SubElement(testsuites_properties, 'property', attrib={
                  'name': name, 'value': value})
  
          testsuite = xml_tree.find('testsuite')
          project_span_ids = xml_tree.find(
              '*property[@name="polarion-project-span-ids"]')
  
          for result in self.step.plan.execute.results():
              if not result.id:
                  raise tmt.utils.ReportError(
                      f"Test Case {result.name} is not exported to Polarion, "
                      "please run 'tmt tests export --how polarion' on it")
              work_item_id, test_project_id = get_polarion_ids(
                  PolarionWorkItem.query(
                      result.id, fields=['work_item_id', 'project_id']))
  
              assert work_item_id is not None
              assert test_project_id is not None
              assert project_span_ids is not None
  
              if test_project_id not in project_span_ids.attrib['value']:
                  project_span_ids.attrib['value'] += f',{test_project_id}'
  
              test_properties = {
                  'polarion-testcase-id': work_item_id,
                  'polarion-testcase-project-id': test_project_id}
  
              assert testsuite is not None
              test_case = testsuite.find(f"*[@name='{result.name}']")
              assert test_case is not None
              properties_elem = ET.SubElement(test_case, 'properties')
              for name, value in test_properties.items():
                  ET.SubElement(properties_elem, 'property', attrib={
                      'name': name, 'value': value})
  
          assert self.workdir is not None
  
          f_path = self.get("file", os.path.join(self.workdir, DEFAULT_NAME))
          with open(f_path, 'wb') as fw:
              ET.ElementTree(xml_tree).write(fw)
  
          if upload:
              server_url = str(PolarionWorkItem._session._server.url)
              polarion_import_url = (
                  f'{server_url}{"" if server_url.endswith("/") else "/"}'
                  'import/xunit')
              auth = (
                  PolarionWorkItem._session.user_id,
                  PolarionWorkItem._session.password)
  
              response = post(
                  polarion_import_url, auth=auth,
                  files={'file': ('xunit.xml', ET.tostring(xml_tree))})
              self.info(
                  f'Response code is {response.status_code} with text: {response.text}')
          else:
              self.info(f"xUnit file saved at: {f_path}")
              self.info("Polarion upload can be done manually using command:")
              self.info(
                  "curl -k -u <USER>:<PASSWORD> -X POST -F file=@<XUNIT_XML_FILE_PATH> "
                  "<POLARION_URL>/polarion/import/xunit")
  
  
  [... the rest is omitted ...]
files: tmt/steps/report/polarion.py