pylero.test_run — pylero documentation

# Source code for pylero.test_run

    # -*- coding: utf8 -*-
    from __future__ import absolute_import
    from __future__ import division
    from __future__ import print_function
    from __future__ import unicode_literals

    import importlib
    import os

    import suds
    from pylero._compatible import basestring
    from pylero._compatible import classmethod
    from pylero._compatible import object  # noqa
    from pylero._compatible import range
    from pylero.base_polarion import BasePolarion
    from pylero.base_polarion import tx_wrapper
    from pylero.build import Build  # noqa: F401
    from pylero.custom import ArrayOfCustom
    from pylero.custom import Custom
    from pylero.custom_field_type import CustomFieldType
    from pylero.document import Document
    from pylero.enum_custom_field_type import EnumCustomFieldType
    from pylero.enum_option_id import ArrayOfEnumOptionId
    from pylero.enum_option_id import EnumOptionId
    from pylero.exceptions import PyleroLibException
    from pylero.plan import Plan  # NOQA
    from pylero.project import Project
    from pylero.test_record import ArrayOfTestRecord
    from pylero.test_record import TestRecord
    from pylero.test_run_attachment import ArrayOfTestRunAttachment
    from pylero.test_run_attachment import TestRunAttachment
    from pylero.text import Text
    from pylero.user import User
    from pylero.work_item import _WorkItem

    # Build is used in custom fields.
    # Plan is used in custom fields.

    class TestRun(BasePolarion):
        """Object to manage the Polarion Test Management WS tns3:TestRun

        Attributes:
            attachments (list of TestRunAttachments)
            author (User): user object of the Test Run Author
            created (datetime)
            document (Module)
            finished_on (datetime)
            group_id
            is_template (bool): indicates if the TestRun object is a Template
            keep_in_history (bool)
            location
            project (Project)
            query (str): The Polarion query that the TestRun objects are based on.
            records (list of TestRecord objects)
            select_test_cases_by (EnumOptionId):
                The test cases can be:
                    AUTOMATED_PROCESS
                    DYNAMIC_QUERY
                    DYNAMIC_LIVEDOC
                    MANUAL_SELECTION
                    STATIC_QUERY
                    STATIC_LIVEDOC

            status
            summary_defect (_WorkItem)
            template (TestRun): template that the TestRun is based on.
            test_run_id (str): Unique identifier of the test run within the
                               project
            type
            updated
            custom_fields
        """

        _cls_suds_map = {
            "attachments": {
                "field_name": "attachments",
                "is_array": True,
                "cls": TestRunAttachment,
                "arr_cls": ArrayOfTestRunAttachment,
                "inner_field_name": "TestRunAttachment",
            },
            "author": {
                "field_name": "authorURI",
                "cls": User,
                "named_arg": "uri",
                "sync_field": "uri",
            },
            "created": "created",
            "document": {"field_name": "document", "cls": Document},
            "finished_on": "finishedOn",
            "group_id": "groupId",
            "test_run_id": "id",
            "is_template": "isTemplate",
            "keep_in_history": "keepInHistory",
            "location": "location",
            "project_id": {
                "field_name": "projectURI",
                "cls": Project,
                "named_arg": "uri",
                "sync_field": "uri",
            },
            "query": "query",
            "_records": {
                "field_name": "records",
                "is_array": True,
                "cls": TestRecord,
                "arr_cls": ArrayOfTestRecord,
                "inner_field_name": "TestRecord",
            },
            "select_test_cases_by": {
                "field_name": "selectTestCasesBy",
                "cls": EnumOptionId,
                "enum_id": "testrun-selectTestCasesBy",
            },
            "status": {
                "field_name": "status",
                "cls": EnumOptionId,
                "enum_id": "testing/testrun-status",
            },
            "summary_defect": {
                "field_name": "summaryDefectURI",
                "cls": _WorkItem,
                "named_arg": "uri",
                "sync_field": "uri",
            },
            "title": "title",
            "template": {
                "field_name": "templateURI",
                "named_arg": "uri",
                "sync_field": "uri",
            },
            "type": {
                "field_name": "type",
                "cls": EnumOptionId,
                "enum_id": "testing/testrun-type",
            },
            "updated": "updated",
            # the custom field attribute has been changed to be a protected attr.
            # All interaction with custom fields should be done directly with the
            # derived attribute.
            "_custom_fields": {
                "field_name": "customFields",
                "is_array": True,
                "cls": Custom,
                "arr_cls": ArrayOfCustom,
                "inner_field_name": "Custom",
            },
            "uri": "_uri",
            "_unresolvable": "_unresolvable",
        }
        _id_field = "test_run_id"
        _obj_client = "test_management_client"
        _obj_struct = "tns3:TestRun"
        _custom_field_cache = {}

        @property
        def records(self):
            """function to return all the test records of a TestRun.
            The records array for dynamic queries/documents only includes executed
            records. This returns the unexecuted ones as well.

            Args:
                None

            Returns:
                list of TestRecords
            """
            self._verify_obj()
            # if the type is not dynamic then all the cases are in the _records
            # attribute. If they are dynamic, they have to be gotten
            if "dynamic" not in self.select_test_cases_by:
                return self._records
            if "Doc" in self.select_test_cases_by:
                cases = self.document.get_work_items(None, True)
            elif "Query" in self.select_test_cases_by:
                cases = _WorkItem.query(self.query + " AND project.id:" + self.project_id)
            else:
                raise PyleroLibException(
                    "Only Test Runs based on Docs or" " Queries can be dynamic"
                )
            executed_ids = [rec.test_case_id for rec in self._records]
            test_recs = self._records
            for case in cases:
                if case.work_item_id not in executed_ids and case.type != "heading":
                    test_recs.append(TestRecord(self.project_id, case.work_item_id))
            return test_recs

        @records.setter
        def records(self, val):
            self._records = val

        @classmethod
        @tx_wrapper
        def create(cls, project_id, test_run_id=None, template=None, title=None, **kwargs):
            """class method create for creating a new test run in Polarion

            Args:
                project_id (string): the Polarion project to create the test run
                                     in
                test_run_id (string): the unique identifier for the test run,
                                      If the Enable Generated Test Run IDs option
                                      is marked in the configuration, it will
                                      ignore this field. If it is None, the title
                                      will be used.
                template (string): the id of the template to base the test run on.
                                   cannot be None. Because of the existing order of
                                   params before adding title, it is given a
                                   default value of None, which will cause an error
                                   if it is not populated
                title (string): To create a test run with a title. In this case,
                                if the id is not populated it will be autogen.
                                If it is populated, it will use that.
                **kwargs: keyword arguments. Test run attributes can be passed in
                          to be set upon creation. Required fields must be passed
                          in

            Returns:
                The created TestRun object

            References:
                test_management.createTestRun
            """
            if not template:
                raise PyleroLibException("Template is required")
            if title:
                uri = cls.session.test_management_client.service.createTestRunWithTitle(
                    project_id, test_run_id or title, title, template
                )
            else:
                uri = cls.session.test_management_client.service.createTestRun(
                    project_id, test_run_id, template
                )
            if uri:
                run = cls(uri=uri)
                run.verify_params(**kwargs)
                for field in kwargs:
                    setattr(run, field, kwargs[field])
                run.update()
                return run
            else:
                raise PyleroLibException("Test Run was not created")

        @classmethod
        @tx_wrapper
        def create_template(
            cls,
            project_id,
            template_id,
            parent_template_id="Empty",
            select_test_cases_by=None,
            query=None,
            doc_with_space=None,
            **kwargs,
        ):  # , test_case_ids=[]):
            # see comment below regarding test_case)ids.
            """class method create_template for creating a new template in Polarion

            Args:
                project_id (string): the Polarion project to create the test run
                                     in
                template_id (string): the unique identifier for the template
                parent_template_id: the template that this is based on
                                    Default: "Empty"
                select_test_cases_by: the method used to choose test cases
                                      NOTE: It is currently not possible to select
                                      test cases manually via the API.
                                      Default: None
                query: the Lucene query, for query methods, default None
                doc_with_space: the space/doc_name, for document methods
                                default: None
                **kwargs: keyword arguments. Test run attributes can be passed in
                          to be set upon creation. Required fields must be passed
                          in

            Returns:
                The created TestRun Template object

            References:
                test_management.createTestRun
            """
            tr = cls.create(project_id, template_id, parent_template_id, **kwargs)
            tr.is_template = True
            if select_test_cases_by:
                tr.select_test_cases_by = select_test_cases_by
            elif doc_with_space:
                tr.select_test_cases_by = "dynamicLiveDoc"
            elif query:
                tr.select_test_cases_by = "dynamicQueryResult"

            # you can have both a query and a document, through which the query
            # qualifies the doc.
            if query:
                tr.query = query
            if doc_with_space:
                tr.document = Document(project_id, doc_with_space)
            # TODO: This should work as soon as Polarion implements Change Request REQ-6334
            #        if test_case_ids:
            #            for test_case_id in test_case_ids:
            #                tr.add_test_record_by_object(testrec.TestRecord(
            #                                            test_case_id,
            #                                            project_id))

            tr.update()
            return TestRun(tr.test_run_id, project_id=project_id)

        @classmethod
        def search(
            cls,
            query,
            fields=["test_run_id"],
            sort="test_run_id",
            limit=None,
            search_templates=False,
            project_id=None,
        ):
            """class method search executes the given query and returns the results

            Args:
                query: the Polarion query used to find test runs
                fields:  test run fields that should be initialized,
                         all other fields will be null.
                         Field names are from the object's attributes and not
                         the Polarion field names, default ["test_run_id"]. If you
                         want all of the fields to be returned, pass [] for this
                         parameter.
                sort: the field used to sort results, default is test_run_id
                limit (int): the maximum number of records to be returned, -1
                             for no limit, default None.
                search_templates (bool): if set, searches the templates
                                         instead of the test runs, default False
                project_id: if set, searches the project id, else default project
            Returns:
                list of TestRun objects

            References:
                test_management.searchTestRunTemplates
                test_management.searchTestRunTemplatesLimited
                test_management.searchTestRunTemplatesWithFields
                test_management.searchTestRunTemplatesWithFieldsLimited
                test_management.searchTestRuns
                test_management.searchTestRunLimited
                test_management.searchTestRunsWithFields
                test_management.searchTestRunsWithFieldsLimited
            """

            # The Polarion functions with limited seem to be the same as without limited
            #    when -1 is passed in as limit. Because of this, the wrapper will not
            #    implement the functions without limited.
            project_id = project_id or cls.default_project
            query += " AND project.id:%s" % (project_id)

            # The following line one purpose is to instantiate a TestRun and by
            # doing so setting all the class attribute (including 'customFields').
            TestRun(project_id=project_id)
            function_name = "search"
            p_sort = (
                cls._cls_suds_map[sort]
                if not isinstance(cls._cls_suds_map[sort], dict)
                else cls._cls_suds_map[sort]["field_name"]
            )
            parms = [query, p_sort]
            if search_templates:
                function_name += "TestRunTemplates"
            else:
                function_name += "TestRuns"
            p_fields = cls._convert_obj_fields_to_polarion(fields)
            if p_fields:
                function_name += "WithFields"
                parms.append(p_fields)
            if limit:
                function_name += "Limited"
                parms.append(limit)
            test_runs = []
            results = getattr(cls.session.test_management_client.service, function_name)(
                *parms
            )
            for suds_obj in results:
                tr = TestRun(suds_object=suds_obj)
                test_runs.append(tr)
            return test_runs

        def __init__(self, test_run_id=None, suds_object=None, project_id=None, uri=None):
            """TestRun constructor.

            Args:
                test_run_id: when given, the object is populated with the
                             TestRuns. Requires project_id parameter
                suds_object: Polarion TestRun object. When given, the object
                             is populated by object data.
                project_id: the Polarion project that the Test Run is located
                            in. Required if test_run_id is passed in
                uri: the uri that references the Polarion TestRun

            Notes:
                Either test_run_id and project or suds_object or uri can be passed
                in or none of them. If none of the identifying parameters are
                passed in an empty object is created

            References:
                test_management.getTestRunById
                test_management.getTestRunByUri
            """
            self._add_custom_fields(project_id)
            super(self.__class__, self).__init__(test_run_id, suds_object)
            if test_run_id:
                if not project_id:
                    raise PyleroLibException(
                        "When test_run_id is passed in, " "project_id is required"
                    )
                self._suds_object = (
                    self.session.test_management_client.service.getTestRunById(
                        project_id, test_run_id
                    )
                )
            elif uri:
                self._suds_object = (
                    self.session.test_management_client.service.getTestRunByUri(uri)
                )
            if test_run_id or uri:
                if getattr(self._suds_object, "_unresolvable", True):
                    raise PyleroLibException(
                        "The Test Run {0} was not found.".format(test_run_id)
                    )

        def _fix_circular_refs(self):
            # a class can't reference itself as a class attribute so it is
            # defined after instatiation
            self._cls_suds_map["template"]["cls"] = self.__class__

        def _custom_field_types(self, field_type):
            """There are 4 types of custom fields in test runs:
            * built-in types (string, boolean, ...)
            * Pylero Text object (text)
            * Enum of existing type (@ prefix, i.e. enum:@user = Pylero User)
            * Enum, based on lookup table(i.e. enum:arch)
            The basic enums can get their valid values using the BasePolarion
            get_valid_field_values function.
            The existing type Enums, must validate by instantiating the object.

            Args;
                field_type - the field type passed in custom field enum_id
            Returns:
                None for base types, Text class for text types, the object to
                validate the enum for Enum objects or the key to validate the enum
                for basic enums
            """
            if field_type == "text":
                return Text
            # some custom types have a [] segment. Still unsure of how to handle
            # those specific attributes, but for now, this will ignore them
            field_type = field_type.split("[")[0]
            if field_type.startswith("@"):
                # an enum based on an object
                return [
                    globals()[x] for x in globals() if x.lower() == field_type[1:].lower()
                ][0]
            else:
                # a regular enum
                return field_type

        @classmethod
        def get_defined_custom_field_types(cls, project_id):
            """Gets all the custom fields defined for the specified project.
            the custom fields are all either of type CustomFieldType or
            EnumCustomFieldType in the case where the field is an enumeration.
            These 2 classes are mostly interchangeable.

            Args:
                project_id: the project to get the custom fields from

            Returns:
                list of all the custom fields

            References:
                testmanagement.getDefinedTestRunCustomFieldTypes
            """
            if not cls._custom_field_cache[project_id]:
                cfts = cls.session.test_management_client.service.getDefinedTestRunCustomFieldTypes(
                    project_id
                )
            else:
                cfts = cls._custom_field_cache[project_id]
            results = [
                (
                    CustomFieldType(suds_object=item)
                    if isinstance(item, CustomFieldType()._suds_object.__class__)
                    else EnumCustomFieldType(suds_object=item)
                )
                for item in cfts
            ]
            return results

        def _cache_custom_fields(self, project_id):
            """Polarion API provides the custom fields of a TestRun.
            Get all custom fields and save in the dict self._custom_field_cache

            Args:
                project_id
            Returns
                None
            """
            self._custom_field_cache[project_id] = {}
            results = self.get_defined_custom_field_types(project_id)
            for result in results:
                f = getattr(result, "enum_id", None)
                key = result.cft_id
                f_type = None
                if f:
                    f_type = self._custom_field_types(f)
                self._custom_field_cache[project_id][key] = {}
                self._custom_field_cache[project_id][key]["type"] = f_type
                self._custom_field_cache[project_id][key]["required"] = getattr(
                    result, "required", False
                )
                self._custom_field_cache[project_id][key]["multi"] = getattr(
                    result, "multi", False
                )

        def _add_custom_fields(self, project_id):
            """This generates object attributes, with validation, so that custom
            fields can be related to as regular attributes. It takes the custom
            fields from the cache and calls the cache function if the values are
            not already there.
            Args:
                project_id - Each project can have its own custom fields. This
                    function takes the custom fields for the specific project and
                    adds them to the _cls_suds_map so they will be built into
                    object attributes.
            Returns:
                None
            """
            self._changed_fields = {}
            # force the session to initialize. This is needed here because the
            # system has not yet been initialized.
            self.session
            if not project_id:
                project_id = self.default_project
            if project_id not in self._custom_field_cache:
                self._cache_custom_fields(project_id)
            cache = self._custom_field_cache[project_id]
            self._required_fields = []
            for field in cache:
                if cache[field]["required"]:
                    self._required_fields.append(field)
                self._cls_suds_map[field] = {}
                self._cls_suds_map[field]["field_name"] = field
                self._cls_suds_map[field]["is_custom"] = True
                if cache[field]["type"] == Text:
                    self._cls_suds_map[field]["cls"] = Text
                elif cache[field]["type"]:
                    if cache[field]["multi"]:
                        self._cls_suds_map[field]["cls"] = ArrayOfEnumOptionId
                        self._cls_suds_map[field]["is_array"] = True
                    else:
                        self._cls_suds_map[field]["cls"] = EnumOptionId
                    self._cls_suds_map[field]["enum_id"] = cache[field]["type"]
                    if (
                        isinstance(cache[field]["type"], type)
                        and "project_id"
                        in cache[field]["type"].__init__.__code__.co_varnames[
                            : cache[field]["type"].__init__.__code__.co_argcount
                        ]
                    ):
                        self._cls_suds_map[field]["additional_parms"] = {
                            "project_id": project_id
                        }

        def _get_index_of_test_record(self, test_case_id):
            # specific functions request the index of the test record within the
            # test run. However, the user doesn't know what the index is.
            # Therefore the user passes in the test case id and this function
            # figures out the index.
            # However, this function does not work for update_test_record_by_object
            # as that function requires the actual index of the record and not the
            # index of only executed records.
            index = -1
            for test_record in self._records:
                if test_record.executed:
                    index += 1
                if test_case_id in test_record._suds_object.testCaseURI:
                    return index
            raise PyleroLibException(
                "The Test Case is either not part of "
                "this TestRun or has not been executed"
            )

        def _status_change(self):
            """Change status if necessary and possible."""
            # load a new object to test if the status should be changed.
            # can't use existing object because it doesn't include the new test rec
            # if the status needs changing, change it in the new object, so it
            # doesn't update any user made changes in the existing object.
            check_tr = TestRun(uri=self.uri)
            results = [rec.result for rec in check_tr.records if rec.result]
            if not results:
                status = "notrun"
                check_tr.finished_on = None
            elif len(results) == len(check_tr.records):
                status = "finished"
                # Do not touch finished_on because it can not be reverted
                # DPP-171495 Web services: You cannot reset finishedOn in TestRun
                # check_tr.finished_on = datetime.datetime.now()
            else:
                status = "inprogress"
                check_tr.finished_on = None
            # Before setting the status, check if the new value is valid
            status_id = self._cls_suds_map["status"]["enum_id"]
            valid_status_values = self.get_valid_field_values(status_id, None)
            if status != check_tr.status and status in valid_status_values:
                check_tr.status = status
                check_tr.update()

        def _verify_record_count(self, record_index):
            # verifies the number of records is not less then the index given.
            self._verify_obj()
            if record_index > (len(self.records) - 1):
                raise PyleroLibException(
                    "There are only {0} test records".format(len(self.records))
                )

        def _verify_test_step_count(self, record_index, test_step_index):
            # verifies the number of test steps is not less then the index given.
            self._verify_record_count(record_index)
            if test_step_index > (len(self.records[record_index].test_step_results) - 1):
                raise PyleroLibException(
                    "There are only {0} test records".format(len(self.records))
                )

        def add_attachment_to_test_record(self, test_case_id, path, title):
            """method add_attachment_to_test_record, adds the given attachment to
            the specified test record

            Args:
                test_case_id (str): The id of the test case
                path: file path to upload
                title: u.User friendly name of the file

            Returns:
                None

            Notes:
                Raises an error if the test case given is not in the TestRun or has
                not been executed yet.

            References:
                test_management.addAttachmentToTestRecord
            """
            record_index = self._get_index_of_test_record(test_case_id)
            self._verify_record_count(record_index)
            data = self._get_file_data(path)
            filename = os.path.basename(path)
            self.session.test_management_client.service.addAttachmentToTestRecord(
                self.uri, record_index, filename, title, data
            )

        def add_attachment(self, path, title):
            """method add_attachment adds the given attachment to the current
            test run

            Args:
                path: file path to upload
                title: u.User friendly name of the file

            Returns:
                None

            Notes:
                Raises an error if the test run object is not populated

            References:
                test_management.addAttachmentToTestRun
            """
            self._verify_obj()
            data = self._get_file_data(path)
            filename = os.path.basename(path)
            self.session.test_management_client.service.addAttachmentToTestRun(
                self.uri, filename, title, data
            )

        def add_attachment_to_test_step(self, test_case_id, test_step_index, path, title):
            """method add_attachment_to_test_step, adds the given attachment to
            the specified test step of the specified test record

            Args:
                test_case_id (str): The id of the test case to the step is in
                test_step_index (int): The 0 based index of the test step
                path: file path to upload
                title: u.User friendly name of the file

            Returns:
                none

            Notes:
                Raises an error if the record_index given is higher then the number
                of test records. or if the test_step_index is higher then the
                number of steps.

            References:
                test_management.addAttachmentToTestStep
            """
            record_index = self._get_index_of_test_record(test_case_id)
            self._verify_test_step_count(record_index, test_step_index)
            data = self._get_file_data(path)
            filename = os.path.basename(path)
            self.session.test_management_client.service.addAttachmentToTestStep(
                self.uri, record_index, test_step_index, filename, title, data
            )

        def _check_test_record_exists(self, test_case_id):
            """Searches the test run to see if the case is already a member of it
            and if so raises an exception. It should only receive one result set
            and if it returns more then that it raises a different exception.
            It searches the server for the test record in case, the record was
            added by another process.

            Notes:
                This is the best that can be done and there is no method of knowing
                if another process added the same record in between this check and
                the actual add.

            Args:
                test_case_id (str): the id of the test case to check

            Returns:
                None
            """
            check_tr = TestRun.search(
                'project.id:%s AND id:"%s" AND %s'
                % (self.project_id, self.test_run_id, test_case_id)
            )
            if len(check_tr) not in [0, 1]:
                raise PyleroLibException(
                    "The search function did not work as expected. Please report."
                )
            elif len(check_tr) == 1:
                raise PyleroLibException("This test case is already part of the test run")
            else:
                return None

        @tx_wrapper
        def add_test_record_by_fields(
            self,
            test_case_id,
            test_result,
            test_comment,
            executed_by,
            executed,
            duration,
            defect_work_item_id=None,
            create_incident=True,
        ):
            """method add_test_record_by_fields, adds a test record for the given
            test case based on the result fields passed in.
            When a test record is added and the valid options for status contain
            "inprogress" and "finished", it changes the test run status to
            "inprogress" and when the last test record is run, it changes the
            status to "finished".

            Args:
                test_case_id (str): The id of the test case that was executed
                test_result (str): Must be one of the following values:
                                      passed
                                      failed
                                      blocked
                test_comment (str or Text object): may be None
                executed_by (str): user id
                executed (datetime):
                duration (float):
                defect_work_item_id (str): _WorkItem id of defect, default: None

            Returns:
                None

            References:
                test_management.addTestRecordToTestRun
            """
            self._verify_obj()
            self.check_valid_field_values(test_result, "result", {})
            if not executed or not test_result:
                raise PyleroLibException("executed and test_result require values")
            testrec = TestRecord(self.project_id, test_case_id)
            testrec.result = test_result
            testrec.comment = test_comment
            testrec.executed_by = executed_by
            testrec.executed = executed
            testrec.duration = duration
            if defect_work_item_id:
                testrec.defect_case_id = defect_work_item_id
            self.add_test_record_by_object(testrec, create_incident=create_incident)

        def __generate_description(
            self, test_case: _WorkItem, test_record: TestRecord
        ) -> str:
            """Generates the description for the incident report.

            If the test case has test steps that match the steps of the test record,
            the description will include a table with the test steps and their results.

            Args:
                test_case (_WorkItem): Test case as a work item.
                test_record (TestRecord): The test record contains the results of the
                    test execution.

            Returns:
                str: The incident report in HTML format.
            """

            tr_html = (
                '<b>Test Run:</b> <span id="link" class=    '
                '"polarion-rte-link" data-type="testRun" '
                'data-item-id="{0}" data-option-id="long">'
                "</span><br/>".format(self.test_run_id)
            )
            tc_html = (
                '<b>Test Case:</b> <span id="link" class='
                '"polarion-rte-link" data-type="workItem" '
                'data-item-id="{0}" data-option-id="long"></span>'
                "<br/>".format(test_case.work_item_id)
            )
            table_cell_style = (
                'style="text-align: left; padding: 10px; '
                'vertical-align: top; background-color: #ffffff;"'
            )
            table_row_style = 'style="border-bottom: 1px solid #f0f0f0;"'
            columns = [
                "",
                "#",
                '<span title="Step">Step</span>',
                '<span title="Expected Result">Expected Result</span>',
                "Actual Result",
            ]
            test_step_results = {
                "passed": '<span title="Results met expected results'
                '"><span style="white-space:nowrap;"><im'
                'g src="/polarion/icons/default/enums/test'
                'run_status_passed.png" style="vertical-a'
                "lign:text-bottom;border:0px;margin-right:2"
                'px;" class="polarion-no-style-cleanup"/'
                "></span></span>",
                "failed": '<span title="Results did not meet expecte'
                'd results"><span style="white-space:nowr'
                'ap;"><img src="/polarion/icons/default/e'
                'nums/testrun_status_failed.png" style="v'
                "ertical-align:text-bottom;border:0px;margi"
                'n-right:2px;" class="polarion-no-style-c'
                'leanup"/></span></span>',
                "blocked": '<span title="Errors in the product preve'
                'nted test from being executed"><span sty'
                'le="white-space:nowrap;"><img src="/po'
                "larion/icons/default/enums/testrun_status"
                '_blocked.png" style="vertical-align:tex'
                't-bottom;border:0px;margin-right:2px;" c'
                'lass="polarion-no-style-cleanup"/>'
                "</span></span>",
            }
            table_header = (
                '<table class="polarion-no-style-cleanup" style="border'
                '-collapse: collapse;"><tr style="text-align: left; '
                "white-space: nowrap; color: #757575; border-bottom: 1px "
                'solid #d2d7da; background-color: #ffffff;">{0}</tr>'.format(
                    "".join(
                        [
                            "<th {0}>{1}</th>".format(table_cell_style, column)
                            for column in columns
                        ]
                    )
                )
            )
            verdict = (
                '</table><table style="margin-bottom: 15px; ;border-collapse: '
                'collapse; width:100%; ;margin-top: 13px;" class="polarion-no'
                '-style-cleanup"><tr><th style="width: 80%; text-align: left;'
                ' background-color: #ffffff;">Test Case Verdict:</th></tr><tr>'
                '<td style="vertical-align: top;"><span style="font-weight: '
                'bold;"><span style="color: #C30000;"><span title="Results '
                'did not meet expected results"><span style="white-space:'
                'nowrap;"><img src="/polarion/icons/default/enums/testrun_'
                'status_failed.png" style="vertical-align:text-bottom;border:'
                '0px;margin-right:2px;" class="polarion-no-style-cleanup"/>'
                "</span>Failed</span></span></span><span> {0}</span></td></tr>"
                "</table>".format(test_record.comment)
            )
            try:
                table_rows = ""
                test_steps = test_case.get_test_steps()
                for step in range(len(test_record.test_step_results)):
                    table_rows += (
                        "<tr {0}>"
                        "<td {1}>{2}</td>"
                        "<td {1}>{3}</td>"
                        "<td {1}>{4}</td>"
                        "<td {1}>{5}</td>"
                        "<td {1}>{6}</td>"
                        "</tr>".format(
                            table_row_style,
                            table_cell_style,
                            test_step_results.get(
                                test_record.test_step_results[step].result
                            ),
                            step + 1,
                            test_steps.steps[step].values[0].content,
                            test_steps.steps[step].values[1].content,
                            test_record.test_step_results[step].comment,
                        )
                    )
            except (suds.WebFault, AttributeError):
                # test_case.get_test_steps() can raise a webfault
                # test_record.test_step_results cab raise an attribute error
                # other errors should be passed up the call stack
                table_rows = ""
            if table_rows:
                table = table_header + table_rows
            else:
                table = ""
            content = tr_html + tc_html + table + verdict
            return content

        def __create_incident_report(
            self, test_case_id: str, test_record: TestRecord
        ) -> str:
            """Create and populate an incident work item.

            The work_item type of the incident and the fields to copy from the test
            case and test run to the incident are defined in the tests configuration
            of the project.
            Throws an exception if the test case does not exists or if the incident
            could not be created.

            Args:
                test_case_id (str): The ID of the test case.
                test_record (TestRecord): The test record contains the results of the
                    test execution.
                    executed.

            Returns:
                str: work_item_id of the created incident.
            """
            project_id = self.project_id
            status = "open"
            project = Project(project_id)
            tconf = project.get_tests_configuration()
            defect_wi_type = tconf.defect_work_item_type
            test_case_wi_type = tconf.test_case_work_item_type

            try:
                work_item_module = importlib.import_module("pylero.work_item")
                wi_class_names = getattr(work_item_module, "workitems")
                test_case_class_name = wi_class_names[test_case_wi_type]
                test_case_class = getattr(work_item_module, test_case_class_name)
                test_case = test_case_class(
                    project_id=self.project_id, work_item_id=test_case_id
                )
            except (AttributeError, PyleroLibException):
                test_case = _WorkItem(project_id=self.project_id, work_item_id=test_case_id)

            title = "Failed: " + test_case.title
            description = self.__generate_description(test_case, test_record)
            kwarg_dict = {}
            for prop in tconf.fields_to_copy_from_test_case_to_defect.property:
                if hasattr(test_case, prop.key):
                    kwarg_dict[prop.value] = getattr(test_case, prop.key)
            for prop in tconf.fields_to_copy_from_test_run_to_linked_defect.property:
                if hasattr(self, prop.key):
                    kwarg_dict[prop.value] = getattr(self, prop.key)

            try:
                incident_report = _WorkItem.create(
                    project_id, defect_wi_type, title, description, status, **kwarg_dict
                )
            except PyleroLibException as e:
                msg = "\n".join(
                    [
                        (
                            f"Failed to create the work item {title} in"
                            f"project {project_id} of type {defect_wi_type}."
                        ),
                        f"Error: {e}",
                    ]
                )
                raise PyleroLibException(msg)
            incident_report.add_linked_item(test_case.work_item_id, "triggered_by")
            return incident_report.work_item_id

        @tx_wrapper
        def add_test_record_by_object(
            self, test_record, manual_state_change=False, create_incident=True
        ):
            """method add_test_record_by_object, adds a test record for the given
            test case based on the TestRecord object passed in.
            In addition, the test run is checked for completeness and the test
            run state will change accordingly.
            If possible, the status is set to one of
            ["notrun", "finished", "inprogress"].

            Args:
                test_record (TestRecord or Polarion TestRecord):
                manual_state_change (boolean): Change test run state
                                               automatically or manual.
                create_incident (boolean): Create an incident report if the test
                                            case fails and no incident is linked.

            Returns:
                None

            References:
                test_management.addTestRecordToTestRun
            """
            self._verify_obj()
            test_case_id = test_record.test_case_id
            self._check_test_record_exists(test_case_id)
            if isinstance(test_record, TestRecord):
                suds_object = test_record._suds_object
            elif isinstance(test_record, TestRecord()._suds_object.__class__):
                suds_object = test_record
            if (
                test_record.result == "failed"
                and not test_record.defect_case_id
                and create_incident
            ):
                test_record.defect_case_id = self.__create_incident_report(
                    test_case_id,
                    test_record,
                )
            self.session.test_management_client.service.addTestRecordToTestRun(
                self.uri, suds_object
            )

            if manual_state_change:
                self.update()
            else:
                self._status_change()

        def create_summary_defect(self, defect_template_id=None):
            """method create_summary_defect, adds a new summary _WorkItem for the
            test case based on the _WorkItem template id passed in. If not template
            is passed in, it creates it based on the default template.

            Args:
                defect_template_id (str): the _WorkItem template id to base the
                new summary defect. can be null. default: None

            Returns:
                the created _WorkItem

            References:
                test_management.createSummaryDefect
            """

            self._verify_obj()
            defect_template_uri = None
            if defect_template_id:
                suds_defect_template = _WorkItem(
                    work_item_id=defect_template_id, project_id=self.project_id
                )
                defect_template_uri = suds_defect_template._uri
            wi_uri = self.session.test_management_client.service.createSummaryDefect(
                self.uri, defect_template_uri
            )
            return _WorkItem(uri=wi_uri)

        def delete_attachment_from_test_record(self, test_case_id, filename):
            """Deletes Test Record Attachment of specified record and
            attachment's file name.

            Args:
                test_case_id: The test case to delete the attachment from
                filename: name of the file to delete

            Returns:
                None

            References:
                test_management.deleteAttachmentFromTestRecord
            """
            record_index = self._get_index_of_test_record(test_case_id)
            self._verify_record_count(record_index)
            self.session.test_management_client.service.deleteAttachmentFromTestRecord(
                self.uri, record_index, filename
            )

        def delete_attachment_from_test_step(self, test_case_id, test_step_index, filename):
            """Deletes Test Step Attachment of the specified step in the specified
            test record.

            Args:
                test_case_id: The test case to delete the attachment from
                filename: name of the file to delete

            Returns:
                None

            References:
                test_management.deleteAttachmentFromTestRecord
            """
            record_index = self._get_index_of_test_record(test_case_id)
            self._verify_test_step_count(record_index, test_step_index)
            self.session.test_management_client.service.deleteAttachmentFromTestStep(
                self.uri, record_index, test_step_index, filename
            )

        def delete_attachment(self, filename):
            """Deletes Test Run Attachment specified by attachment's
            file name. Method is applicable also on Test Run Template.

            Args:
                filename: filename to delete

            Returns:
                None

            References:
                test_management.deleteTestRunAttachment
            """
            self._verify_obj()
            self.session.test_management_client.service.deleteTestRunAttachment(
                self.uri, filename
            )

        def get_attachment(self, filename):
            """Gets Test Run Attachment specified by attachment's
            file name. Method is applicable also on Test Run Template.

            Args:
                filename: filename to delete

            Returns:
                TestRunAttachment object

            References:
                test_management.getTestRunAttachment
            """
            self._verify_obj()
            suds_attach = self.session.test_management_client.service.getTestRunAttachment(
                self.uri, filename
            )
            return TestRunAttachment(suds_object=suds_attach)

        def get_attachments(self):
            """method get_attachments returns all the attachments for the TestRun

            Args:
                None

            Returns:
                ArrayOfTestRunAttachments object

            References:
                test_management.getTestRunAttachments
            """
            self._verify_obj()
            lst_suds_attach = (
                self.session.test_management_client.service.getTestRunAttachments(self.uri)
            )
            lst_attach = [
                TestRunAttachment(suds_object=suds_attach)
                for suds_attach in lst_suds_attach
            ]
            return lst_attach

        def get_custom_field(self, field_name):
            """gets custom field values.

            Args:
                field_name: name of the custom field

            Returns:
                value of the custom field.

            Note: Polarion WSDL currently does not publish the list of custom
                  fields, so this function cannot do any verification if the field
                  is valid.
            """
            self._verify_obj()
            cf = self._custom_fields
            match = [x for x in cf if x.key == field_name]
            if match:
                return match[0].value
            else:
                return Custom(field_name, None)

        def get_wiki_content(self):
            """method get_wiki_content returns the wiki content for the Test Run

            Args:
                None

            Returns:
                Text object containing the wiki content

            References:
                test_management.getWikiContentForTestRun
            """
            self._verify_obj()
            suds_wiki = (
                self.session.test_management_client.service.getWikiContentForTestRun(
                    self.uri
                )
            )
            return Text(suds_object=suds_wiki)

        def _set_custom_field(self, field_name, value):
            """sets custom field values.

            Args:
                field_name: name of the custom field

            Returns:
                value of the custom field.

            Note: Polarion WSDL currently does not publish the list of custom
                  fields, so this function cannot do any verification if the field
                  or value is valid.
            """
            self._verify_obj()
            cf = self._custom_fields
            cust = Custom()
            cust.key = field_name
            cust.value = value
            if cf:
                # check if the custom field already exists and if so, modify it.
                match = [x for x in cf if x.key == field_name]
                if match:
                    match[0].value = value
                else:
                    cf.append(cust)
            else:
                cf = [cust]
            self._custom_fields = cf

        def update(self):
            """method update updates the testRun object with the attribute values
            currently in the object.

            Args:
                None

            Returns
                None

            References:
                test_management.updateTestRun
            """
            self._verify_obj()
            self.verify_required()
            for field in self._changed_fields:
                self._set_custom_field(field, self._changed_fields[field])
            self._changed_fields = {}
            self.session.test_management_client.service.updateTestRun(self._suds_object)

        def update_attachment(self, path, original_filename, title):
            """method update_attachment updates the specified attachment to the
            current test run

            Args:
                path: file path to upload
                original_filename: The file that we want to overwrite with the new
                                   file
                title: u.User friendly name of the file

            Returns:
                None

            Notes:
                Raises an error if the test run object is not populated.

            References:
                test_management.updateTestRunAttachment
            """
            self._verify_obj()
            data = self._get_file_data(path)
            filename = os.path.basename(original_filename)
            self.session.test_management_client.service.updateTestRunAttachment(
                self.uri, filename, title, data
            )

        def update_summary_defect(
            self, source, total_failures, total_errors, total_tests, defect_template_id
        ):
            """method update-summary_defect creates or updates the summary defect
            Work Item of a test run.

            Args:
                source (string): source of the summary defect, used to generate
                                  the description content.
                total_failures (int): amount of total failures in the test run,
                                      used to generate the description content.
                total_errors (int): amount of total errors in the test run,
                                    used to generate the description content.
                total_tests (int): amount of total tests in the test run,
                                   used to generate the description content.
                defect_template_id: ID of the defect template Work Item to be
                                    used, the configured template will be used if
                                    None.

            Returns:
                the created or updated _WorkItem

            References:
                test_management.updateSummaryDefect
            """
            self._verify_obj()
            suds_defect_template = _WorkItem(
                work_item_id=defect_template_id, project_id=self.project_id
            )
            defect_template_uri = suds_defect_template._uri
            wi_uri = self.session.test_management_client.service.updateSummaryDefect(
                self.uri,
                source,
                total_failures,
                total_errors,
                total_tests,
                defect_template_uri,
            )
            return _WorkItem(uri=wi_uri)

        @tx_wrapper
        def update_test_record_by_fields(
            self,
            test_case_id,
            test_result,
            test_comment,
            executed_by,
            executed,
            duration,
            defect_work_item_id=None,
            create_incident=True,
        ):
            """method update_test_record_by_fields updates a test record.

            Args:
                test_case_id: id of the test case to update.
                test_result: Must be one of the following values:
                                       passed
                                       failed
                                       blocked
                test_comment: (str or Text object) - may be None
                executed_by (str): user id
                executed: date when the test case has been executed
                duration: duration of the test case execution, any negative value
                          is treated as None.
                defect_work_item_id: _WorkItem id of defect, can be None
                                    Default: None

            Returns:
                None

            Notes:
                Only a test case that has already been executed may be updated
                using this function. To execute a test record use the
                add_test_record_by_fields function

            References:
                test_management.updateTestRecord
            """
            self._verify_obj()
            testrec = TestRecord(self.project_id, test_case_id)
            testrec.result = test_result
            testrec.comment = test_comment
            testrec.executed_by = executed_by
            testrec.executed = executed
            testrec.duration = duration
            if defect_work_item_id:
                testrec.defect_case_id = defect_work_item_id
            self.update_test_record_by_object(
                test_case_id, testrec, create_incident=create_incident
            )

        @tx_wrapper
        def update_test_record_by_object(
            self,
            test_case_id,
            test_record,
            manual_state_change=False,
            create_incident=True,
        ):
            """method update_test_record_by_object, adds a test record for the
            given test case based on the TestRecord object passed in.
            In addition, the test run is checked for completeness and the
            test run state will change accordingly.
            If possible, the status is set to one of
            ["notrun", "finished", "inprogress"].

            Args:
                test_case_id (str): the test case id that the record is related to.
                test_record (TestRecord or Polarion TestRecord)
                manual_state_change (boolean): Change the test
                                               run result automatically or manual.
                create_incident (boolean): Create an incident report if the test
                                            record result is failed and no defect
                                            case id is set.

            Returns:
                None

            References:
                test_management.updateTestRecordAtIndex
            """
            self._verify_obj()
            # this function cannot use the _get_index_of_test_record function
            # because this function (specifically and not documented) uses the
            # actual index of the test records and not the index of all
            # executed records.
            test_case_ids = [rec.test_case_id for rec in self._records]
            if test_case_id not in test_case_ids:
                self.add_test_record_by_object(test_record, create_incident=create_incident)
            else:
                if (
                    test_record.result == "failed"
                    and not test_record.defect_case_id
                    and create_incident
                ):
                    test_record.defect_case_id = self.__create_incident_report(
                        test_case_id, test_record
                    )
                index = test_case_ids.index(test_case_id)
                if isinstance(test_record, TestRecord):
                    suds_object = test_record._suds_object
                elif isinstance(test_record, TestRecord()._suds_object.__class__):
                    suds_object = test_record
                self.session.test_management_client.service.updateTestRecordAtIndex(
                    self.uri, index, suds_object
                )
                if manual_state_change:
                    self.update()
                else:
                    self._status_change()

        def update_wiki_content(self, content):
            """method update_wiki_content updates the wiki for the current TestRun

            Args:
                Content (str or Text object)

            Returns:
                None

            References:
                test_management.updateWikiContentForTestRun
            """
            self._verify_obj()
            if content:
                if isinstance(content, basestring):
                    obj_content = Text(content=content)
                    suds_content = obj_content._suds_object
                elif isinstance(content, Text):
                    suds_content = content._suds_object
                else:  # is a suds object
                    suds_content = content
            else:
                suds_content = suds.null()
            self.session.test_management_client.service.updateWikiContentForTestRun(
                self.uri, suds_content
            )

        def verify_required(self):
            """function that checks if all required fields are passed in

            Exceptions:
                PyleroLibException - if required params are not passed in
            """
            fields = ""
            for req in self._required_fields:
                if not getattr(self, req):
                    fields += (", " if fields else "") + req
            if fields:
                raise PyleroLibException(
                    "These parameters are required: {0}".format(fields)
                )

        def verify_params(self, **kwargs):
            """function that checks if all the kwargs are valid attributes.

            Args:
                **kwargs: keyword arguments. Test run attributes can be passed in
                          to be set upon creation. Required fields must be passed
                          in
            Exceptions:
                PyleroLibException - if params are unknown
            """
            params = ""
            for param in kwargs:
                if param not in self._cls_suds_map:
                    params += (", " if params else "") + param

            if params:
                raise PyleroLibException("These parameters are unknown: {0}".format(params))