From 69fc740c985b2b879bb0a919fb54788e8149e58e Mon Sep 17 00:00:00 2001 From: Ian Barnard Date: Mon, 9 Oct 2023 17:28:19 +0100 Subject: [PATCH] add etm_simple_updatetestresult.py showing using the OSLC APIs to create a new test result --- elmclient/_project.py | 17 + elmclient/_qm.py | 3 +- elmclient/_qmrestapi.py | 42 +++ .../examples/etm_simple_updatetestresult.py | 342 ++++++++++++++++++ elmclient/httpops.py | 6 +- elmclient/rdfxml.py | 2 + 6 files changed, 408 insertions(+), 4 deletions(-) create mode 100644 elmclient/_qmrestapi.py create mode 100644 elmclient/examples/etm_simple_updatetestresult.py diff --git a/elmclient/_project.py b/elmclient/_project.py index bf00e9c..8a20810 100644 --- a/elmclient/_project.py +++ b/elmclient/_project.py @@ -103,7 +103,19 @@ def report_type_system( self ): rows.append( [shortname,k,qcdetails[k]]) # print in a nice table with equal length columns report += utils.print_in_html(rows,['Short Name', 'URI', 'Query Capability URI']) + + report += "

Project Factory Resource Types, short name and URI

\n" + factorydetails = self.get_factory_uris() + rows = [] + for k in sorted(factorydetails.keys()): + shortname = k.split('#')[-1] + shortname += " (default)" if self.default_query_resource is not None and k==rdfxml.tag_to_uri(self.default_query_resource) else "" + rows.append( [shortname,k,factorydetails[k]]) + # print in a nice table with equal length columns + report += utils.print_in_html(rows,['Short Name', 'URI', 'Query Capability URI']) + report += self.textreport() + rows = [] for prefix in sorted(rdfxml.RDF_DEFAULT_PREFIX.keys()): rows.append([prefix,rdfxml.RDF_DEFAULT_PREFIX[prefix]] ) @@ -139,6 +151,11 @@ def get_factory_uri(self,resource_type=None,context=None, return_shapes=False): resource_type = resource_type or context.default_query_resource return self.app.get_factory_uri_from_xml(factoriesxml=context.get_services_xml(), resource_type=resource_type,context=context, return_shapes=return_shapes) + def get_factory_uris(self,resource_type=None,context=None): + context = context or self + resource_type = resource_type or context.default_query_resource + return self.app.get_factory_uris_from_xml(factoriesxml=context.get_services_xml(),context=context) + def load_type_from_resource_shape(self, el): raise Exception( "This must be provided by the inheriting class!" ) diff --git a/elmclient/_qm.py b/elmclient/_qm.py index 40e0c22..3b1824d 100644 --- a/elmclient/_qm.py +++ b/elmclient/_qm.py @@ -21,6 +21,7 @@ from . import rdfxml from . import server from . import utils +from . import _qmrestapi ################################################################################################# @@ -29,7 +30,7 @@ ################################################################################################# -class _QMProject(_project._Project): +class _QMProject(_project._Project, _qmrestapi.QM_REST_API_Mixin): # A QM project def __init__(self, name, project_uri, app, is_optin=False, singlemode=False,defaultinit=True): super().__init__(name, project_uri, app, is_optin,singlemode,defaultinit=defaultinit) diff --git a/elmclient/_qmrestapi.py b/elmclient/_qmrestapi.py new file mode 100644 index 0000000..eca5f9f --- /dev/null +++ b/elmclient/_qmrestapi.py @@ -0,0 +1,42 @@ +## +## © Copyright 2021- IBM Inc. All rights reserved +# SPDX-License-Identifier: MIT +## + +# this is a start of implementing the ETM REST API - it's very incomplete! + +import logging + +import lxml.etree as ET + +from . import rdfxml +from . import utils + +logger = logging.getLogger(__name__) + +################################################################################################# + +class QM_REST_API_Mixin(): + def __init__(self,*args,**kwargs): + self.typesystem_loaded = False + self.has_typesystem=True + self.clear_typesystem() + self.alias = None + + def get_alias( self ): + if not self.alias: + # GET the alias + projects_x = self.execute_get_rdf_xml( f"service/com.ibm.rqm.integration.service.IIntegrationService/projects" ) + self.alias = rdfxml.xmlrdf_get_resource_text( projects_x, f".//atom:entry/atom:title[.='{self.name}']/../atom:content/qm_ns2:project/qm_ns2:alias" ) +# print( f"{self.alias=}" ) + return self.alias + + def find_testplan( self, name ): + pass + + def find_testcase( self, name ): + pass + + def find_testexecturionrecord( self, name ): + pass + diff --git a/elmclient/examples/etm_simple_updatetestresult.py b/elmclient/examples/etm_simple_updatetestresult.py new file mode 100644 index 0000000..6ed6e57 --- /dev/null +++ b/elmclient/examples/etm_simple_updatetestresult.py @@ -0,0 +1,342 @@ +## +## Copyright 2023- IBM Inc. All rights reserved +# SPDX-License-Identifier: MIT +## + +####################################################################################################### +# +# elmclient ETM simple example of adding a new test result to a test execution record +# + +import sys +import os +import csv +import logging +import urllib.parse + +import elmclient.server as elmserver +import elmclient.utils as utils +import elmclient.rdfxml as rdfxml +import elmclient.httpops as httpops + +# setup logging - see levels in utils.py +#loglevel = "INFO,INFO" +loglevel = "TRACE,OFF" +levels = [utils.loglevels.get(l,-1) for l in loglevel.split(",",1)] +if len(levels)<2: + # assert console logging level OFF if not provided + levels.append(None) +if -1 in levels: + raise Exception( f'Logging level {loglevel} not valid - should be comma-separated one or two values from DEBUG, INFO, WARNING, ERROR, CRITICAL, OFF' ) +utils.setup_logging( filelevel=levels[0], consolelevel=levels[1] ) + +logger = logging.getLogger(__name__) + +utils.log_commandline( os.path.basename(sys.argv[0]) ) + +jazzhost = 'https://jazz.ibm.com:9443' + +username = 'ibm' +password = 'ibm' + +jtscontext = 'jts' +qmappdomain = 'qm' + +# the project+component+config that will be queried +proj = "SGC Quality Management" +comp = "SGC MTM" +conf = "SGC MTM Production stream" + +outfile = "etm_test results.csv" + +# caching control +# 0=fully cached (but code below specifies queries aren't cached) - if you need to clear the cache, delet efolder .web_cache +# 1=clear cache initially then continue with cache enabled +# 2=clear cache and disable caching +caching = 1 + +##################################################################################################### + +def writeresultstocsv( outfile, queryresults ): + # want to find all the headers + allcolumns = {} + for uri,row in queryresults.items(): + for k in row.keys(): + allcolumns[k]=True + + def safeint(s): + try: + return int(s.get('dcterms:identifier',0)) + except: + pass + return 0 + + # sort by dcterms.identifier + uris = sorted(queryresults, key=lambda s: safeint(queryresults[s]) ) + + print( f"Writing results to CSV {outfile}" ) + + with open( outfile, "w", newline='' ) as csvfile: + csvwriter = csv.DictWriter(csvfile,fieldnames=sorted(allcolumns.keys())) + csvwriter.writeheader() + for uri in uris: + row = queryresults[uri] + csvwriter.writerow(row) + +##################################################################################################### +# get the commandline args +if len(sys.argv) != 5: + raise Exception("need exactly four arguments - the testplan name, the testcase name and the tcer name, and the verdict (passed or failed) - BE CAREFUL WITH EXACT SPELLING!" ) + +tpname = sys.argv[1] +tcname = sys.argv[2] +tcername = sys.argv[3] +verdict = sys.argv[4] + +if verdict not in ['passed', 'failed']: + raise Exception( "Only verdicts pass or failed are accepted!" ) + +##################################################################################################### +# create our "server" which is how we connect to ETM +# first enable the proxy so if a proxy is running it can monitor the communication with server (this is ignored if proxy isn't running) +elmserver.setupproxy(jazzhost,proxyport=8888) +theserver = elmserver.JazzTeamServer(jazzhost, username, password, verifysslcerts=False, jtsappstring=f"jts:{jtscontext}", appstring=qmappdomain, cachingcontrol=caching) + +##################################################################################################### +# create the RM application interface +qmapp = theserver.find_app( qmappdomain, ok_to_create=True ) +if not qmapp: + raise Exception( "Something serious went wrong" ) + +##################################################################################################### +# find the project/component/config +p = qmapp.find_project( proj ) +if not p: + raise Exception( "Something serious went wrong" ) +pa_u = p.project_uri +print( f"{pa_u=}" ) +print( f"{p.get_alias()=}" ) + +c = p.find_local_component( comp ) +if not c: + raise Exception( "Something serious went wrong" ) + +comp_u = c.project_uri +print( f"{comp_u=}" ) + +local_config_u = c.get_local_config( conf ) +if not local_config_u: + raise Exception( "Something serious went wrong" ) + +# select the configuration - from now on use c for all operations in the local config +c.set_local_config(local_config_u) + +##################################################################################################### +# find the test plan + +tpquerybase = c.get_query_capability_uri("oslc_qm:TestPlanQuery") +if not tpquerybase: + raise Exception( "Something serious went wrong" ) + +# query for Test Case +tps = c.execute_oslc_query( + tpquerybase, + whereterms=[['dcterms:title','=',f'"{tpname}"']], + select=['*'], +# prefixes={rdfxml.RDF_DEFAULT_PREFIX["dcterms"]:'dcterms'} # note this is reversed - url to prefix + ) +if len(tps.items())!=1: + raise Exception( "Something serious went wrong" ) +#print( f"\n{tcs=}" ) +#print( f"\n{tcs.items()=}" ) +#print( f"\n{list(tcs.keys())[0]=}" ) + +# the testcase URL is the only key as exactly one result :-) +tp_u = list(tps.keys())[0] +print( f"{tp_u=}" ) + +writeresultstocsv( "01_testplans.csv", tps ) + + +##################################################################################################### +# find the test case +tcquerybase = c.get_query_capability_uri("oslc_qm:TestCaseQuery") +if not tcquerybase: + raise Exception( "Something serious went wrong" ) + +# query for Test Case +tcs = c.execute_oslc_query( + tcquerybase, + whereterms=[['dcterms:title','=',f'"{tcname}"']], + select=['*'], +# prefixes={rdfxml.RDF_DEFAULT_PREFIX["dcterms"]:'dcterms'} # note this is reversed - url to prefix + ) +if len(tcs.items())!=1: + raise Exception( "Something serious went wrong" ) +#print( f"\n{tcs=}" ) +#print( f"\n{tcs.items()=}" ) +#print( f"\n{list(tcs.keys())[0]=}" ) + +# the testcase URL is the only key as exactly one result :-) +tc_u = list(tcs.keys())[0] +print( f"{tc_u=}" ) + +writeresultstocsv( "02_testcases.csv", tcs ) + +##################################################################################################### +# Now find the test execution record which refers to the tc and has the title=tcername +terquerybase = c.get_query_capability_uri("oslc_qm:TestExecutionRecordQuery") +if not terquerybase: + raise Exception( "Something serious went wrong" ) + +tcers = c.execute_oslc_query( + terquerybase, + whereterms=[['and',['oslc_qm:runsTestCase','=',f'<{tc_u}>'],['dcterms:title','=',f'"{tcername}"']]], + select=['*'], +# prefixes={rdfxml.RDF_DEFAULT_PREFIX["dcterms"]:'dcterms'} # note this is reversed - url to prefix + ) + +print( f"TERs: {len(tcers.items())}" ) + +writeresultstocsv( "03_testcaseexecutionrecords.csv", tcers ) + +if len(tcers)>1: + raise Exception( "Too many tcers!" ) +# if len(tcers)==0 we need to create a TER +if len(tcers)==0: + print( "Need to create a TER)" ) + tcer_factory_u = c.get_factory_uri(resource_type='TestExecutionRecord',context=None, return_shapes=False) + if not tcer_factory_u: + raise Exception( "Something serious went wrong" ) + print( f"{tcer_factory_u=}" ) + + tcer_x = f""" + + + + + {tcername} + + +""" + + jsessionid = httpops.getcookievalue( p.app.server._session.cookies, 'JSESSIONID',None) + if not jsessionid: + raise Exception( "JSESSIONID not found!" ) + + response = c.execute_post_rdf_xml( tcer_factory_u, data=tcer_x, intent="Create the tcer for the test plan and the test case", headers={'Referer': 'https://jazz.ibm.com:9443/qm', 'X-Jazz-CSRF-Prevent': jsessionid }, remove_parameters=['oslc_config.context'] ) + print( f"{response=}" ) + tcer_u = response.headers['Location'] +else: + tcer_u = list(tcers.keys())[0] + result = c.execute_get_rdf_xml( tcer_u, headers={ 'Accept': 'application/rdf+xml' } ) + +print( f"{tcer_u=}" ) +##################################################################################################### +# now we have a ter, we can create a new test result + +trquerybase = c.get_query_capability_uri("oslc_qm:TestResultQuery") +if not trquerybase: + raise Exception( "Something serious went wrong" ) + +trs = c.execute_oslc_query( + trquerybase, + whereterms=[['oslc_qm:producedByTestExecutionRecord','=',f'<{tcer_u}>']], + select=['*'], + prefixes={rdfxml.RDF_DEFAULT_PREFIX["oslc_qm"]:'oslc_qm'} # note this is reversed - url to prefix + ) + +print( f"Test results: {len(trs.items())}" ) + +writeresultstocsv( "04_testresults.csv", trs ) + +print( "Need to create a TR" ) +tr_factory_u = c.get_factory_uri(resource_type='TestResult',context=None, return_shapes=False) +if not tr_factory_u: + raise Exception( "Something serious went wrong" ) +print( f"{tr_factory_u=}" ) + +#oslc_qm:producedByTestExecutionRecord + +tr_x = f""" + + + + + Allocate_Dividends_by_Percentage_Firefox_DB2_Tomcat_Windows_S12 + 17 + + + + com.ibm.rqm.execution.common.state.{verdict} + + +""" + +jsessionid = httpops.getcookievalue( p.app.server._session.cookies, 'JSESSIONID',None) +if not jsessionid: + raise Exception( "JSESSIONID not found!" ) + +response = c.execute_post_rdf_xml( tr_factory_u, data=tr_x, intent="Create the test result for ter", headers={'Referer': 'https://jazz.ibm.com:9443/qm', 'X-Jazz-CSRF-Prevent': jsessionid }, remove_parameters=['oslc_config.context'] ) +print( f"{response=}" ) +tr_u = response.headers['Location'] +print( f"{tr_u=}" ) + + + +##################################################################################################### + + + +##################################################################################################### + + +print( "Finished" ) diff --git a/elmclient/httpops.py b/elmclient/httpops.py index cfd3182..e84b190 100644 --- a/elmclient/httpops.py +++ b/elmclient/httpops.py @@ -94,10 +94,9 @@ def to_binary(text, encoding=None, errors='strict'): ################################################################################################# def getcookievalue( cookies, cookiename, defaultvalue=None): - print( f"gcv {cookies=} {cookiename=} {defaultvalue=}" ) for c in cookies: if c.name == cookiename: - print( f"Found {cookiename} {c.value}" ) +# print( f"Found {cookiename} {c.value}" ) return c.value print( f"Not found {cookiename}" ) return defaultvalue @@ -167,6 +166,7 @@ def execute_put_rdf_xml(self, reluri, *, data=None, params=None, headers=None, * return response def execute_post_rdf_xml(self, reluri, *, data=None, params=None, headers=None, put=False, **kwargs): + print( f"EPRX {params=}" ) reqheaders = {'Accept': 'application/xml', 'Content-Type': 'application/rdf+xml'} if headers is not None: reqheaders.update(headers) @@ -290,6 +290,7 @@ def _get_get_request(self, reluri='', *, params=None, headers=None): return self._get_request('GET', reluri, params=params, headers=headers) def _get_post_request(self, reluri='', *, params=None, headers=None, data=None, put=False ): + print( f"GPR {params=}" ) if put: return self._get_request('PUT', reluri, params=params, headers=headers, data=data) return self._get_request('POST', reluri, params=params, headers=headers, data=data) @@ -305,7 +306,6 @@ def __init__(self, session, verb, uri, *, params=None, headers=None, data=None): paramstring = f"?{urllib.parse.urlencode( params, quote_via=urllib.parse.quote, safe='/')}" else: paramstring = "" - # self._req = requests.Request( verb,uri, params=params, headers=headers, data=data ) self._req = requests.Request( verb,uri+paramstring, headers=headers, data=data ) self._session = session diff --git a/elmclient/rdfxml.py b/elmclient/rdfxml.py index 84af2b6..c89693c 100644 --- a/elmclient/rdfxml.py +++ b/elmclient/rdfxml.py @@ -14,6 +14,7 @@ RDF_DEFAULT_PREFIX = { 'acc': 'http://open-services.net/ns/core/acc#', # added for GCM 'acp': 'http://jazz.net/ns/acp#', + 'atom': "http://www.w3.org/2005/Atom", 'config_ext': 'http://jazz.net/ns/config_ext#', 'dc': 'http://purl.org/dc/elements/1.1/', 'dcterms': 'http://purl.org/dc/terms/', @@ -44,6 +45,7 @@ 'public_rm_10': 'http://www.ibm.com/xmlns/rm/public/1.0/', 'prov': 'http://www.w3.org/ns/prov#', # added for GCM 'qm_rqm': "http://jazz.net/ns/qm/rqm#", + 'qm_ns2': "http://jazz.net/xmlns/alm/qm/v0.1/", 'rdf': 'http://www.w3.org/1999/02/22-rdf-syntax-ns#', 'rdfs': 'http://www.w3.org/2000/01/rdf-schema#', 'rdm_types': 'http://www.ibm.com/xmlns/rdm/types/',