Skip to content

Commit

Permalink
First commit, add cache option to zeep and create a basic test case w… (
Browse files Browse the repository at this point in the history
#141)

* First commit, add cache option to zeep and create a basic test case with no properties
* #140 add traceability import through properties
* add documentation and example
  • Loading branch information
TristanFAURE authored Oct 22, 2023
1 parent 2528f7c commit 7155a2a
Show file tree
Hide file tree
Showing 8 changed files with 528 additions and 29 deletions.
20 changes: 19 additions & 1 deletion docs/xml.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -30,13 +30,31 @@ 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)
# if want to save the test_run as json, add:
ResultExporter.save_json("result.json", testrun)
For traceability:

.. code-block:: XML
<testcase name="testCase8" classname="Tests.Registration" assertions="4"
time="1.625275" file="tests/registration.code" line="302">
<!-- <properties> Some tools also support properties for test cases. -->
<properties>
<property name="verifies" value="REQ-001" />
<property name="verifies" value="POLARION-ID" />
</properties>
</testcase>
<testcase name="testCase10" classname="Tests.Registration" assertions="4">
<system-out>
[[PROPERTY|verifies=ISSUE-011]]
</system-out>
</testcase>
Export a test run as json
^^^^^^^^^^^^^^^^^^^^^^^^^
Expand Down
35 changes: 19 additions & 16 deletions polarion/polarion.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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
Expand All @@ -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):
"""
Expand All @@ -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:
Expand Down Expand Up @@ -189,14 +200,6 @@ def PdfProperties(self):
raise Exception(f'PDF not supported in this Polarion version')
return self._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
Expand All @@ -207,7 +210,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:
Expand Down
97 changes: 85 additions & 12 deletions polarion/xml.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from .record import Record
from datetime import datetime
import logging, json
import re
logger = logging.getLogger(__name__)

class Config:
Expand All @@ -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):
Expand All @@ -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
Expand All @@ -61,6 +69,7 @@ def from_dict(cls, data):
"""
return Config(data)


def __init__(self, data):
"""
Init from existing data
Expand All @@ -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:
"""
Expand All @@ -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):
Expand Down Expand Up @@ -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:
Expand All @@ -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"]}')
Expand All @@ -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'
Expand All @@ -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
Expand All @@ -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)
Expand All @@ -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
Expand Down
28 changes: 28 additions & 0 deletions tests/junit/no_properties.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<testsuites name="Test run" tests="4" failures="1" errors="1" skipped="1"
assertions="20" time="16.082687" timestamp="2021-04-02T15:48:23">

<testsuite name="Tests.Registration" tests="4" failures="1" errors="1" skipped="1"
assertions="20" time="16.082687" timestamp="2021-04-02T15:48:23"
file="tests/registration.code">

<testcase name="testCase8" classname="Tests.Registration" assertions="4"
time="1.625275" file="tests/registration.code" line="302">
<skipped type="skipped" message="Test was skipped." />
</testcase>
<testcase name="testCase9" classname="Tests.Registration" assertions="4"
time="1.625275" file="tests/registration.code" line="302">
<failure message="Expected value did not match." type="AssertionError">
<!-- Failure description or stack trace -->
</failure>
</testcase>
<testcase name="testCase10" classname="Tests.Registration" assertions="4"
time="1.625275" file="tests/registration.code" line="302">
<error message="Division by zero." type="ArithmeticError">
<!-- Error description or stack trace -->
</error>
</testcase>
<testcase name="testCase11" classname="Tests.Registration" assertions="4"
time="1.625275" file="tests/registration.code" line="302">
</testcase>
</testsuite>
</testsuites>
Loading

0 comments on commit 7155a2a

Please sign in to comment.