diff --git a/docs/xml.rst b/docs/xml.rst
index 38fa5b5..8713152 100644
--- a/docs/xml.rst
+++ b/docs/xml.rst
@@ -11,7 +11,7 @@ Import an xml file of results
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Import an xml file of results (see xml_junit.xsd for the standard format) into a new or an existing test run in a project
-
+Import traceability to Workitems using their ids or their titles
.. code:: python
@@ -30,6 +30,7 @@ Import an xml file of results (see xml_junit.xsd for the standard format) into a
Config.PROJECT_ID: 'project_id_in_polarion',
Config.TESTRUN_ID: 'testrun_id_in_polarion', # if not set, create a new test run
Config.TESTRUN_COMMENT: 'comment to add in the test run' # as an option.
+ Config.USE_CACHE : 'True or False (Default) use the zeep cache' # as an option
})
testrun=Importer.from_xml(config)
@@ -37,6 +38,23 @@ Import an xml file of results (see xml_junit.xsd for the standard format) into a
# if want to save the test_run as json, add:
ResultExporter.save_json("result.json", testrun)
+For traceability:
+
+.. code-block:: XML
+
+
+
+
+
+
+
+
+
+
+ [[PROPERTY|verifies=ISSUE-011]]
+
+
Export a test run as json
^^^^^^^^^^^^^^^^^^^^^^^^^
diff --git a/polarion/polarion.py b/polarion/polarion.py
index f1028fc..fedc9fc 100644
--- a/polarion/polarion.py
+++ b/polarion/polarion.py
@@ -2,7 +2,9 @@
import re
from urllib.parse import urljoin, urlparse
import requests
-from zeep import Client, Transport
+import tempfile
+import os
+from zeep import Client, CachingClient
from zeep.plugins import HistoryPlugin
from .project import Project
@@ -27,7 +29,7 @@ class Polarion(object):
"""
def __init__(self, polarion_url, user, password=None, token=None, static_service_list=False, verify_certificate=True,
- svn_repo_url=None, proxy=None, request_session=None):
+ svn_repo_url=None, proxy=None, request_session=None, cache=False):
self.user = user
self.password = password
self.token = token
@@ -36,6 +38,8 @@ def __init__(self, polarion_url, user, password=None, token=None, static_service
self.svn_repo_url = svn_repo_url
self.proxy = None
self.request_session = request_session
+ self.cache = cache
+ self.transport = None
if proxy is not None:
self.proxy = {
'http': proxy,
@@ -90,10 +94,9 @@ def _createSession(self):
"""
if 'Session' in self.services:
self.history = HistoryPlugin()
- self.services['Session']['client'] = Client(
- self.services['Session']['url'] + '?wsdl', plugins=[self.history], transport=self._getTransport())
+ self.services['Session']['client'] = self.get_client('Session',[self.history])
if self.proxy is not None:
- self.services['Session']['client'] .transport.session.proxies = self.proxy
+ self.services['Session']['client'].transport.session.proxies = self.proxy
try:
self.sessionHeaderElement = None
self.sessionCookieJar = None
@@ -116,6 +119,15 @@ def _createSession(self):
else:
raise Exception(
'Cannot login because WSDL has no SessionWebService')
+
+ def get_client(self,service,plugins=[]):
+ client = None
+ if self.cache:
+ client = CachingClient(self.services[service]['url'] + '?wsdl', plugins=plugins)
+ else:
+ client = Client(self.services[service]['url'] + '?wsdl', plugins=plugins)
+ client.transport.session.verify = self.verify_certificate
+ return client
def _updateServices(self):
"""
@@ -126,8 +138,7 @@ def _updateServices(self):
for service in self.services:
if service != 'Session':
if 'client' not in service:
- self.services[service]['client'] = Client(
- self.services[service]['url'] + '?wsdl', transport=self._getTransport())
+ self.services[service]['client'] = self.get_client(service)
self.services[service]['client'].set_default_soapheaders(
[self.sessionHeaderElement])
if self.proxy is not None:
@@ -173,14 +184,6 @@ def _getTypes(self):
self.ArrayOfSubterraURIType = self.getTypeFromService('Tracker', 'ns1:ArrayOfSubterraURI')
self.PdfProperties = self.getTypeFromService('Tracker', 'ns2:PdfProperties')
- def _getTransport(self):
- """
- Gets the zeep transport object
- """
- transport = Transport(session=self.request_session)
- transport.session.verify = self.verify_certificate
- return transport
-
def hasService(self, name: str):
"""
Checks if a WSDL service is available
@@ -191,7 +194,7 @@ def hasService(self, name: str):
def getService(self, name: str):
"""
- Get a WSDL service client. The name can be 'Trakcer' or 'Session'
+ Get a WSDL service client. The name can be 'Tracker' or 'Session'
"""
# request user info to see if we're still logged in
try:
diff --git a/polarion/xml.py b/polarion/xml.py
index 4aa0a65..fc83019 100644
--- a/polarion/xml.py
+++ b/polarion/xml.py
@@ -3,6 +3,7 @@
from .record import Record
from datetime import datetime
import logging, json
+import re
logger = logging.getLogger(__name__)
class Config:
@@ -21,11 +22,14 @@ class Config:
TESTRUN_TYPE='testrun_type' # type of the test run if testrun is created. If not set 'xUnit Test Manual Upload'
TESTRUN_COMMENT='testrun_comment' # test run comment to add if set.
SKIP_MISSING_TESTCASE='skip_missing_testcase' # if set to True, skip result on unknown test cases
+ VERIFY_CERT ='verify_cert' # verify or not the cert
+ USE_CACHE ='use_cache' # verify or not the cert
ATTRIBUTES = [
XML_FILE, URL, USERNAME, PASSWORD, TOKEN,
PROJECT_ID, TESTRUN_ID, TESTRUN_ID_GENERATOR,
- TESTRUN_TITLE, TESTRUN_TYPE, TESTRUN_COMMENT, SKIP_MISSING_TESTCASE]
+ TESTRUN_TITLE, TESTRUN_TYPE, TESTRUN_COMMENT, SKIP_MISSING_TESTCASE, VERIFY_CERT, USE_CACHE]
MANDATORY = [XML_FILE, URL, PROJECT_ID] # and also either user/password or token
+
_classinitialised = False
def __new__(cls, *args, **kwargs):
@@ -44,6 +48,10 @@ def _default_value(cls, attribute_name):
return 'xUnit Test Manual Upload'
elif attribute_name==Config.SKIP_MISSING_TESTCASE:
return False
+ elif attribute_name==Config.VERIFY_CERT:
+ return True
+ elif attribute_name==Config.USE_CACHE:
+ return False
return None
@classmethod
@@ -61,6 +69,7 @@ def from_dict(cls, data):
"""
return Config(data)
+
def __init__(self, data):
"""
Init from existing data
@@ -84,6 +93,7 @@ def generate_test_run_id(self):
else:
self._data[Config.TESTRUN_ID]=f'unit-{datetime.now().strftime("%Y-%m-%d-%H-%M-%S-%f")}'
return getattr(self, Config.TESTRUN_ID)
+
class XmlParser:
"""
@@ -92,6 +102,9 @@ class XmlParser:
TEST_SUITES='testsuites'
TEST_SUITE='testsuite'
TEST_CASE='testcase'
+ PROPERTIES='properties'
+ PROPERTY='property'
+ SYSOUT='system-out'
@classmethod
def parse_root(cls, xml_file):
@@ -131,6 +144,21 @@ def _parse_suite(cls, test_suite, parent, returned_cases):
else:
raise Exception(f'Unmanaged {XmlParser.TEST_SUITE} {test_suite.tag} in {parent["path"]}')
+ # matches expressions like [[PROPERTY|verifies=REQ-001]]
+ RE_PATTTERN = re.compile("\\[\\[PROPERTY\\|(.*)\\=(.*)\\]\\]")
+
+ @classmethod
+ def tranform_string_properties(cls, value):
+ result = []
+ tmp = XmlParser.RE_PATTTERN.findall(value)
+ for res in tmp:
+ if len(res) == 2:
+ result.append({
+ "name":res[0],
+ "value":res[1]
+ })
+ return result
+
@classmethod
def _parse_case(cls, test_case, parent, returned_cases):
if test_case.tag == XmlParser.TEST_CASE:
@@ -146,14 +174,26 @@ def _parse_case(cls, test_case, parent, returned_cases):
if 'time' in test_case.attrib.keys():
case.update({ 'time': test_case.attrib['time'] })
- # error & failure
+ # error & failure & properties
for elem in test_case:
- if elem.tag in ['error', 'failure']:
+ if elem.tag in ['error', 'failure','skipped']:
text = []
for attrib in ['type', 'message']:
if attrib in elem.attrib.keys(): text.append(elem.attrib[attrib])
- text.append(elem.text)
+ if elem.text is not None:
+ text.append(elem.text)
case.update({ elem.tag: '\n'.join(text)})
+ elif elem.tag == XmlParser.PROPERTIES:
+ if "properties" not in case:
+ case.update({'properties':[]})
+ for property in elem:
+ if XmlParser.PROPERTY == property.tag and 'name' in property.attrib.keys() and 'value' in property.attrib.keys():
+ case['properties'].append({property.get('name') : property.get('value')})
+ elif elem.tag == XmlParser.SYSOUT:
+ if "properties" not in case:
+ case.update({'properties':[]})
+ for property in XmlParser.tranform_string_properties(elem.text):
+ case['properties'].append({property['name'] : property['value']})
returned_cases.append(case)
else:
raise Exception(f'Unmanaged {XmlParser.TEST_CASE} {test_case.tag} in {parent["path"]}')
@@ -166,12 +206,12 @@ def _xmlnode_name(cls, node):
if 'name' in node.attrib.keys():
return f'{node.tag}[name={node.attrib["name"]}]'
return node.tag
-
+
class Importer:
"""
Import xml file to polarion using a config
"""
- TEST_CASE_ID_CUSTOM_FILED='testCaseID'
+ TEST_CASE_ID_CUSTOM_FIELD='testCaseID'
TEST_CASE_TYPE='type:testcase'
TEST_CASE_WI_TYPE='testcase'
TEST_CASE_WI_TITLE='title'
@@ -186,16 +226,17 @@ def from_xml(cls, config):
cases=XmlParser.parse_root(config.xml_file)
logger.info(f'Connection to polarion {config.url} on project {config.project_id}')
- polarion=Polarion(polarion_url=config.url, user=config.username, password=config.password, token=config.token)
+
+ polarion=Polarion(polarion_url=config.url, user=config.username, password=config.password, token=config.token, verify_certificate=config.verify_cert, cache=config.use_cache)
project=polarion.getProject(config.project_id)
# Indexing existing cases with custom field '
- test_cases=project.searchWorkitem(Importer.TEST_CASE_TYPE, field_list=['id', f'customFields.{Importer.TEST_CASE_ID_CUSTOM_FILED}'])
+ test_cases=project.searchWorkitem(Importer.TEST_CASE_TYPE, field_list=['id', f'customFields.{Importer.TEST_CASE_ID_CUSTOM_FIELD}'])
test_cases_from_id={}
for test_case in test_cases:
if hasattr(test_case,'customFields') and hasattr(test_case.customFields,'Custom'):
for custom in test_case.customFields.Custom:
- if getattr(custom, 'key', None) == Importer.TEST_CASE_ID_CUSTOM_FILED and hasattr(custom,'value'):
+ if getattr(custom, 'key', None) == Importer.TEST_CASE_ID_CUSTOM_FIELD and hasattr(custom,'value'):
test_cases_from_id[custom.value]=test_case.id
# Getting or creating test run
@@ -222,16 +263,19 @@ def from_xml(cls, config):
test_run.setCustomField(Importer.TEST_RUN_COMMENT_CUSTOM_FIELD, test_run._polarion.TextType(
content=comment, type='text/html', contentLossy=False))
+ # cache for work items traced
+ cache_for_workitems = {}
+
# Filling
logger.info('Saving results')
for case in cases:
if case['id'] not in test_cases_from_id.keys():
if config.skip_missing_testcase:
- logger.warn(f'Skipping case with {Importer.TEST_CASE_ID_CUSTOM_FILED} {case["id"]}')
+ logger.warn(f'Skipping case with {Importer.TEST_CASE_ID_CUSTOM_FIELD} {case["id"]}')
continue
- print(f'Creating case with {Importer.TEST_CASE_ID_CUSTOM_FILED} {case["id"]}')
+ print(f'Creating case with {Importer.TEST_CASE_ID_CUSTOM_FIELD} {case["id"]}')
wi_case=project.createWorkitem(workitem_type=Importer.TEST_CASE_WI_TYPE, new_workitem_fields={Importer.TEST_CASE_WI_TITLE: case['id']})
- wi_case.setCustomField(key=Importer.TEST_CASE_ID_CUSTOM_FILED, value=case['id'])
+ wi_case.setCustomField(key=Importer.TEST_CASE_ID_CUSTOM_FIELD, value=case['id'])
else:
wi_case=project.getWorkitem(test_cases_from_id[case['id']])
test_run.addTestcase(wi_case)
@@ -246,8 +290,37 @@ def from_xml(cls, config):
test_run.records[-1].setResult(Record.ResultType.FAILED, case['failure'])
elif 'error' in case.keys():
test_run.records[-1].setResult(Record.ResultType.BLOCKED, case['error'])
+ elif 'skipped' in case.keys():
+ test_run.records[-1].setResult(Record.ResultType.NOTTESTED, case['skipped'])
else:
test_run.records[-1].setResult(Record.ResultType.PASSED)
+
+ # handle traceability, because of API, traqceability must use default traceability role
+ # and not the opposite one.
+ # traceability links are made using IDs or titles
+ # this implementation does not allow traceability between test cases
+ if 'properties' in case.keys():
+ for property in case['properties']:
+ for key in property.keys():
+ linked_item = None
+ title=property.get(key)
+ if title in cache_for_workitems:
+ linked_item = cache_for_workitems[title]
+ else:
+ try:
+ linked_item=project.getWorkitem(property.get(key))
+ except Exception:
+ linked_items=project.searchWorkitem(query=f'title:{title}', field_list=['id','title'])
+ if len(linked_items) > 0 and linked_items[0]['title']==title:
+ linked_item=linked_items[0]
+ # work item is reload to avoid isues of class not correctly loaded by search work item
+ linked_item=project.getWorkitem(linked_item['id'])
+ else:
+ logger.error(f'impossible to link{wi_case.id} to {title}')
+ cache_for_workitems[title] = linked_item
+ if linked_item is not None:
+ wi_case.addLinkedItem(linked_item, key)
+
logger.info(f'Results saved in {config.url}/#/project/{config.project_id}/testrun?id={config.testrun_id}')
return test_run
diff --git a/tests/junit/no_properties.xml b/tests/junit/no_properties.xml
new file mode 100644
index 0000000..1d9bffb
--- /dev/null
+++ b/tests/junit/no_properties.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/junit/properties-errors.xml b/tests/junit/properties-errors.xml
new file mode 100644
index 0000000..8c5326d
--- /dev/null
+++ b/tests/junit/properties-errors.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/junit/properties-trace-sys-out.xml b/tests/junit/properties-trace-sys-out.xml
new file mode 100644
index 0000000..a33bf3f
--- /dev/null
+++ b/tests/junit/properties-trace-sys-out.xml
@@ -0,0 +1,58 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+Output line #1
+Output line #2
+
+[[PROPERTY|verifies=REQ-011]]
+[[PROPERTY|verifies=REQ-012]]
+
+
+
+
+
+
+
+
+
+Output line #1
+Output line #2
+
+[[PROPERTY|relates_to=REQ-012]]
+
+Output line #1
+Output line #3
+
+
+
+
+
+
+
+
+[[PROPERTY|verifies=ISSUE-011]]
+
+
+
+
\ No newline at end of file
diff --git a/tests/junit/properties.xml b/tests/junit/properties.xml
new file mode 100644
index 0000000..10de645
--- /dev/null
+++ b/tests/junit/properties.xml
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/test_junit.py b/tests/test_junit.py
new file mode 100644
index 0000000..6bed90a
--- /dev/null
+++ b/tests/test_junit.py
@@ -0,0 +1,246 @@
+import unittest
+
+import zeep
+from polarion.polarion import Polarion
+from polarion.xml import Config, Importer, ResultExporter, XmlParser
+from polarion.record import Record
+# from keys import polarion_url, polarion_project_id, polarion_user, polarion_password, polarion_token
+from keys import polarion_url, polarion_project_id, polarion_user, polarion_password
+
+from unittest import mock
+
+REQ_TYPE = 'requirement'
+ISSUE_TYPE = 'issue'
+
+class TestPolarionJunit(unittest.TestCase):
+
+ def get_config (self, file):
+ # return Config.from_dict({
+ # Config.XML_FILE: file,
+ # Config.URL: polarion_url,
+ # Config.USERNAME: polarion_user,
+ # Config.TOKEN: polarion_token,
+ # Config.PROJECT_ID: polarion_project_id,
+ # Config.TESTRUN_COMMENT: '',
+ # Config.USE_CACHE : True
+ # })
+ return Config.from_dict({
+ Config.XML_FILE: file,
+ Config.URL: polarion_url,
+ Config.USERNAME: polarion_user,
+ Config.PASSWORD: polarion_password,
+ Config.PROJECT_ID: polarion_project_id,
+ Config.TESTRUN_COMMENT: '',
+ Config.USE_CACHE : True
+ })
+
+ def import_xml (self, file):
+ return Importer.from_xml(self.get_config(file))
+
+ def get_project(self):
+ polarion=Polarion(polarion_url=polarion_url, user=polarion_user, password=polarion_password, cache=True)
+ # polarion=Polarion(polarion_url=polarion_url, user=polarion_user, token=polarion_token, cache=True)
+ project=polarion.getProject(polarion_project_id)
+ return project
+
+ def search_wi(self,project, wi):
+ tmp = None
+ try:
+ tmp = project.getWorkitem(wi)
+ except Exception:
+ result = project.searchWorkitem(query=f'title:{wi}',field_list=['id','title'])
+ if len(result) > 0 and result[0]['title'] == wi:
+ tmp = project.getWorkitem(result[0]['id'])
+ return tmp
+
+ def create_wi (self, project, type, delete_before=False, *wis):
+ result = {}
+ for wi in wis:
+ tmp = self.search_wi(project, wi)
+ if tmp is None:
+ result[wi] = project.createWorkitem(type, new_workitem_fields={'title': wi})
+ else:
+ if delete_before:
+ tmp.delete()
+ result[wi] = project.createWorkitem(type, new_workitem_fields={'title': wi})
+ else:
+ result[wi] = tmp
+ return result
+
+ def create_reqs (self, project, delete_before=False, *reqs):
+ return self.create_wi(project, REQ_TYPE, delete_before, *reqs)
+
+ def create_issue (self, project, delete_before=False, *crs):
+ return self.create_wi(project, ISSUE_TYPE, delete_before, *crs)
+
+ def test_regular_expression(self):
+ test1 = {
+ "value":"""
+ Output line #1
+ Output line #2
+
+ [[PROPERTY|verifies=REQ-001]]
+ [[PROPERTY|verifies=REQ-002]]
+ """,
+ "result":[{
+ "name":"verifies",
+ "value":"REQ-001"
+ },
+ {
+ "name":"verifies",
+ "value":"REQ-002"
+ }
+ ]
+ }
+ test2={
+ "value":"""
+ Output line #1
+ Output line #2
+
+ [[PROPERTY|relates_to=REQ-002]]
+
+ Output line #1
+ Output line #3
+ """,
+ "result":[{
+ "name":"relates_to",
+ "value":"REQ-002"
+ }
+ ]
+ }
+
+ test3={
+ "value":""""
+ [[PROPERTY|verifies=ISSUE-001]]
+ """,
+ "result":[{
+ "name":"verifies",
+ "value":"ISSUE-001"
+ }
+ ]
+ }
+ # intended error in the syntax
+ test4={
+ "value":""""
+ [PROPERTY|verifies=ISSUE-001]]
+ """,
+ "result":[]
+ }
+ test5={
+ "value":"",
+ "result":[]
+ }
+ self.assertEqual(XmlParser.tranform_string_properties(test1["value"]),test1["result"])
+ self.assertEqual(XmlParser.tranform_string_properties(test2["value"]),test2["result"])
+ self.assertEqual(XmlParser.tranform_string_properties(test3["value"]),test3["result"])
+ self.assertEqual(XmlParser.tranform_string_properties(test4["value"]),test4["result"])
+ self.assertEqual(XmlParser.tranform_string_properties(test5["value"]),test5["result"])
+
+ def test_import_xml_basic(self):
+ testrun=self.import_xml('junit/no_properties.xml')
+ self.assertIsNotNone(testrun)
+ self.assertEqual(len(testrun.records),4)
+ # impossible with the current api to map (without server calls) in the test to ID in the file
+ nbFailed = 1
+ nbBlocked = 1
+ nbPassed = 1
+ nbNotTested = 1
+ for rec in testrun.records:
+ if rec.getResult() == Record.ResultType.NOTTESTED:
+ nbNotTested = nbNotTested - 1
+ elif rec.getResult() == Record.ResultType.FAILED:
+ nbFailed = nbFailed - 1
+ elif rec.getResult() == Record.ResultType.PASSED:
+ nbPassed = nbPassed - 1
+ elif rec.getResult() == Record.ResultType.BLOCKED:
+ nbBlocked = nbBlocked - 1
+ self.assertEqual(nbFailed,0)
+ self.assertEqual(nbPassed,0)
+ self.assertEqual(nbNotTested,0)
+ self.assertEqual(nbBlocked,0)
+
+ def test_import_xml_properties(self):
+ # create work items for testing purpose
+ project = self.get_project()
+ self.create_reqs(project, True, "REQ-001","REQ-002")
+ self.create_issue(project, True, "ISSUE-001")
+
+ testrun=self.import_xml('junit/properties.xml')
+ self.assertIsNotNone(testrun)
+ self.assertEqual(len(testrun.records),3)
+
+ # verify REQ-001 traceability
+ req001 = self.search_wi(project, "REQ-001")
+ links = req001.getLinkedItemWithRoles()
+ self.assertEqual(len(links), 1)
+ self.assertEqual(links[0][0],"verifies")
+ self.assertEqual(links[0][1].title, "Tests.Registration.testCase8")
+
+ # verify REQ-002 traceability
+ req002 = self.search_wi(project, "REQ-002")
+ links_req002=req002.getLinkedItemWithRoles()
+ self.assertEqual(len(links_req002), 2)
+ for link in links_req002:
+ if link[0] == "verifies":
+ self.assertEqual(link[1].title, "Tests.Registration.testCase8")
+ elif link[0] == "relates_to":
+ self.assertEqual(link[1].title, "Tests.Registration.testCase9")
+
+ # verify ISSUE-001 traceability
+ issue001 = self.search_wi(project, "ISSUE-001")
+ links_issue = issue001.getLinkedItemWithRoles()
+ self.assertEqual(len(links_issue), 1)
+ self.assertEqual(links_issue[0][0],"verifies")
+ self.assertEqual(links_issue[0][1].title, "Tests.Registration.testCase10")
+
+ def test_import_xml_with_bad_link(self):
+ testrun=self.import_xml('junit/properties-errors.xml')
+ self.assertIsNotNone(testrun)
+ self.assertEqual(len(testrun.records),1)
+
+ project = self.get_project()
+ req_does_not_exist =self.search_wi(project, "REQ-THAT-DOES-NOT-EXIST")
+ self.assertIsNone(req_does_not_exist)
+
+
+ def test_import_xml_properties_sys_out(self):
+ # create work items for testing purpose
+ project = self.get_project()
+ self.create_reqs(project, True,"REQ-011","REQ-012")
+ self.create_issue(project, True,"ISSUE-011","ISSUE-012")
+
+ testrun=self.import_xml('junit/properties-trace-sys-out.xml')
+ self.assertIsNotNone(testrun)
+ self.assertEqual(len(testrun.records),3)
+
+ # verify REQ-011 traceability
+ req011 = self.search_wi(project, "REQ-011")
+ links = req011.getLinkedItemWithRoles()
+ self.assertEqual(len(links), 1)
+ self.assertEqual(links[0][0],"verifies")
+ self.assertEqual(links[0][1].title, "Tests.Registration.testCase8")
+
+ # verify REQ-012 traceability
+ req012 = self.search_wi(project, "REQ-012")
+ links_req012=req012.getLinkedItemWithRoles()
+ self.assertEqual(len(links_req012), 2)
+ for link in links_req012:
+ if link[0] == "verifies":
+ self.assertEqual(link[1].title, "Tests.Registration.testCase8")
+ elif link[0] == "relates_to":
+ self.assertEqual(link[1].title, "Tests.Registration.testCase9")
+
+ # test case 10 has two links one with tag and one in sys out
+ # verify ISSUE-011 traceability
+ issue011 = self.search_wi(project, "ISSUE-011")
+ links_issue = issue011.getLinkedItemWithRoles()
+ self.assertEqual(len(links_issue), 1)
+ self.assertEqual(links_issue[0][0],"verifies")
+ self.assertEqual(links_issue[0][1].title, "Tests.Registration.testCase10")
+
+ # verify ISSUE-012 traceability
+ issue012 = self.search_wi(project, "ISSUE-012")
+ links_issue12 = issue012.getLinkedItemWithRoles()
+ self.assertEqual(len(links_issue12), 1)
+ self.assertEqual(links_issue12[0][0],"verifies")
+ self.assertEqual(links_issue12[0][1].title, "Tests.Registration.testCase10")