From 766ed3f84f36ffffb0991ecb736beea28f05e3e7 Mon Sep 17 00:00:00 2001 From: ROMAIN BARTH Date: Thu, 17 Jul 2025 10:47:12 +0100 Subject: [PATCH] testcase class and sample --- elmclient/examples/etm_scenario1.py | 124 +++++++++++ elmclient/examples/etm_scenario2.py | 191 +++++++++++++++++ elmclient/examples/etm_scenario3.py | 183 ++++++++++++++++ elmclient/oslcqueryapi.py | 2 +- elmclient/rdfxml.py | 3 +- elmclient/resource.py | 2 +- elmclient/testcase.py | 317 ++++++++++++++++++++++++++++ 7 files changed, 819 insertions(+), 3 deletions(-) create mode 100644 elmclient/examples/etm_scenario1.py create mode 100644 elmclient/examples/etm_scenario2.py create mode 100644 elmclient/examples/etm_scenario3.py create mode 100644 elmclient/testcase.py diff --git a/elmclient/examples/etm_scenario1.py b/elmclient/examples/etm_scenario1.py new file mode 100644 index 0000000..5099371 --- /dev/null +++ b/elmclient/examples/etm_scenario1.py @@ -0,0 +1,124 @@ +## +## Copyright 2023- IBM Inc. All rights reserved +# SPDX-License-Identifier: MIT +## + +####################################################################################################### +# +# elmclient sample for TSE + +#ETM scenario1: Run a query for all Test Case modified since 01/01/2025 -> Display their URLs, identifier and title +# + +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]) ) + +#parameters +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" + + +# 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 = 2 + +##################################################################################################### +# 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 ETM application interface +qmapp = theserver.find_app( qmappdomain, ok_to_create=True ) +if not qmapp: + raise Exception( "Problem while creating the ETM application interface" ) + +##################################################################################################### +# find the project +p = qmapp.find_project( proj ) +if not p: + raise Exception( f"Project {proj} not found !!!" ) +pa_u = p.project_uri +#print( f"{pa_u=}" ) +#print( f"{p.get_alias()=}" ) + +# find the component +c = p.find_local_component( comp ) +if not c: + raise Exception( f"Component {comp} not found !!!" ) + +comp_u = c.project_uri +#print( f"{comp_u=}" ) + +# find the config +local_config_u = c.get_local_config( conf ) +if not local_config_u: + raise Exception( f"Configuration {conf} not found !!!" ) + +# select the configuration - from now on use c for all operations in the local config +c.set_local_config(local_config_u) + +##################################################################################################### +#SCENARIO 1 +# find the test cases with dcterms modified > 2025-01-01 +tcquerybase = c.get_query_capability_uri("oslc_qm:TestCaseQuery") +if not tcquerybase: + raise Exception( "TestCaseQueryBase not found !!!" ) + +tcs = c.execute_oslc_query( + tcquerybase, + whereterms=[['dcterms:modified','>','"2025-01-01T00:00:00.000Z"^^xsd:dateTime']], + select=['dcterms:identifier,dcterms:title,rqm_qm:shortIdentifier'], + prefixes={rdfxml.RDF_DEFAULT_PREFIX["dcterms"]:'dcterms',rdfxml.RDF_DEFAULT_PREFIX["rqm_qm"]:'rqm_qm'} # note this is reversed - url to prefix + ) + +nbTC = len(tcs) #count the number of Test case returned by the query +print(f"The query returned {nbTC} Test Cases") +print("----------------------------------------------------------") +count = 0 +for TCurl in tcs: + count+=1 + print(f"Test case #{count}") + print(TCurl) + print("Title: " + tcs[TCurl]['dcterms:title']) + print("Identifier: " + tcs[TCurl]['rqm_qm:shortIdentifier']) + print("----------------------------------------------------------") + +##################################################################################################### + + +print( "Finished" ) diff --git a/elmclient/examples/etm_scenario2.py b/elmclient/examples/etm_scenario2.py new file mode 100644 index 0000000..cd265c8 --- /dev/null +++ b/elmclient/examples/etm_scenario2.py @@ -0,0 +1,191 @@ +## +## Copyright 2023- IBM Inc. All rights reserved +# SPDX-License-Identifier: MIT +## + +####################################################################################################### +# +# elmclient sample for TSE + +#ETM scenario2: Get the test case with identifier = xxx +#• Modify its title and description +#• Delete an existing validates Requirement link if it exists +#• Add 2 Validated By links, 1 to a DNG requirement – 1 to a DWA requirement + +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 +from elmclient.testcase import TestCase, TestCaseLink +import lxml.etree as ET + +# 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]) ) + +#parameters +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" #conf="" if project is optout + + +# 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 = 2 + +##################################################################################################### +# 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 ETM application interface +qmapp = theserver.find_app( qmappdomain, ok_to_create=True ) +if not qmapp: + raise Exception( "Problem while creating the ETM application interface" ) + +##################################################################################################### +# find the project +p = qmapp.find_project( proj ) +if not p: + raise Exception( f"Project {proj} not found !!!" ) +pa_u = p.project_uri +#print( f"{pa_u=}" ) +#print( f"{p.get_alias()=}" ) + +# find the component +c = p.find_local_component( comp ) +if not c: + raise Exception( f"Component {comp} not found !!!" ) + +comp_u = c.project_uri +#print( f"{comp_u=}" ) + + +# if project is optin -> find the config +if conf!="": + local_config_u = c.get_local_config( conf ) + if not local_config_u: + raise Exception( f"Configuration {conf} not found !!!" ) + + # select the configuration - from now on use c for all operations in the local config + c.set_local_config(local_config_u) +#print(f"{local_config_u=}") +##################################################################################################### +#SCENARIO 2 +#Get the test case with identifier = xxx +#• Modify its title and description +#• Delete an existing validates Requirement link if it exists +#• Add 2 Validated By links, 1 to a DNG requirement – 1 to a DWA requirement + +tcquerybase = c.get_query_capability_uri("oslc_qm:TestCaseQuery") +if not tcquerybase: + raise Exception( "TestCaseQueryBase not found !!!" ) +tcid = 53 +print(f"Querying test case with identifier = {tcid}") +tcs = c.execute_oslc_query( + tcquerybase, + whereterms=[['rqm_qm:shortIdentifier','=',f'"{tcid}"']], + select=['*'], + prefixes={rdfxml.RDF_DEFAULT_PREFIX["rqm_qm"]:'rqm_qm'} # note this is reversed - url to prefix + ) + +if len(tcs.items())==1: + + tc_u = list(tcs.keys())[0] + print(f"Found Test Case URL: {tc_u}") + print("Doing a Get on test case url") + xml_data,etag = c.execute_get_rdf_xml( tc_u, return_etag=True) + #print(ET.tostring(xml_data)) + + print("Etag:" + etag) + #put the TC data in a test case object + tcObject = TestCase.from_etree(xml_data) + + #get the title and description + print(f"Test case title: {tcObject.title}") + print(f"Test case description: {tcObject.description}") + print("----------------------------------------------------------") + #displaying the links details + print("Links details:") + for link in tcObject.links: + print(f" - {link.predicate} -> {link.target} (title: {link.title})") + + print("----------------------------------------------------------") + + #modifying title and description + tcObject.title += " added by Python" + if tcObject.description is None: + tcObject.description = " added by Python" + else: + tcObject.description += " added by Python" + + + #delete an existing link + for link in tcObject.links: + tcObject.delete_validatesRequirementLink(link.target) + print(f"Deleting the link to {link.target}") + break + + #adding a link to a DWA requirement, provide URL and link title + tcObject.add_validatesRequirementLink("https://dwa9729rom1.fyre.ibm.com:8443/dwa/rm/urn:rational::1-66cdc1432a885b81-O-2-00000040","Module1 (2)") + + #adding a link to a DNG requirement, provide URL and link title + tcObject.add_validatesRequirementLink("https://jazz.ibm.com:9443/rm/resources/BI_kC8csQ_WEfCjT5cep7iZxA","req3") + + print("we have updated the test case with the following:") + + print(f"Test case title: {tcObject.title}") + print(f"Test case description: {tcObject.description}") + print("----------------------------------------------------------") + #displaying the links details + print("Links details:") + for link in tcObject.links: + print(f" - {link.predicate} -> {link.target} (title: {link.title})") + + print("----------------------------------------------------------") + + #get the data from the test case object + xml_data = tcObject.to_etree() + #print(ET.tostring(xml_data)) + print("sending the PUT request to update the test case") + response = c.execute_post_rdf_xml(tc_u, data=xml_data, put=True, cacheable=False, headers={'If-Match':etag,'Content-Type':'application/rdf+xml'}, intent="Update the test case" ) + if response.status_code==200: + print("Update succesfull") + else: + print("Update failed") +##################################################################################################### + +elif len(tcs.items())==0: + print("No test case found") + +else: + print(f"We found more than one test case with identififer {tcid} !!!???") +print( "Finished" ) diff --git a/elmclient/examples/etm_scenario3.py b/elmclient/examples/etm_scenario3.py new file mode 100644 index 0000000..0d5eaa9 --- /dev/null +++ b/elmclient/examples/etm_scenario3.py @@ -0,0 +1,183 @@ +## +## Copyright 2023- IBM Inc. All rights reserved +# SPDX-License-Identifier: MIT +## + +####################################################################################################### +# +# elmclient sample for TSE + +#ETM scenario3: Create a new test case +#• Set the title and description +#• Save the test case +#• Add 2 Validated By links, 1 to a DNG requirement – 1 to a DWA requirement + +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 +from elmclient.testcase import TestCase, TestCaseLink +import lxml.etree as ET + +# 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]) ) + +#parameters +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" #conf="" if project is optout + + +# 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 = 2 + +##################################################################################################### +# 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 ETM application interface +qmapp = theserver.find_app( qmappdomain, ok_to_create=True ) +if not qmapp: + raise Exception( "Problem while creating the ETM application interface" ) + +##################################################################################################### +# find the project +p = qmapp.find_project( proj ) +if not p: + raise Exception( f"Project {proj} not found !!!" ) +pa_u = p.project_uri +#print( f"{pa_u=}" ) +#print( f"{p.get_alias()=}" ) + +# find the component +c = p.find_local_component( comp ) +if not c: + raise Exception( f"Component {comp} not found !!!" ) + +comp_u = c.project_uri +#print( f"{comp_u=}" ) + + +# if project is optin -> find the config +if conf!="": + local_config_u = c.get_local_config( conf ) + if not local_config_u: + raise Exception( f"Configuration {conf} not found !!!" ) + + # select the configuration - from now on use c for all operations in the local config + c.set_local_config(local_config_u) +#print(f"{local_config_u=}") +##################################################################################################### +#SCENARIO 3 +#Create a new test case +#• Set the title and description +#• Save the test case +#• Add 2 Validated By links, 1 to a DNG requirement – 1 to a DWA requirement + +#get the factory URL +tc_factory_u = c.get_factory_uri(resource_type='TestCase',context=None, return_shapes=False) + +#variable for our tc title and description +tc_title = "my new TC created by Python ELMclient" +tc_description = "description from the new TC created by Python ELMclient" + +#creating a new object TestCase with a single argument: title +newTC = TestCase.create_minimal(tc_title) +#add the description to our test case +newTC.description = tc_description + +#get the XML representation of the new Test Case +xml_data = newTC.to_etree() + +#get the ELM cookie id, needed for the POST request +jsessionid = httpops.getcookievalue( p.app.server._session.cookies, 'JSESSIONID',None) +if not jsessionid: + raise Exception( "JSESSIONID not found!" ) + +#POST request to create the new test case +response = c.execute_post_rdf_xml( tc_factory_u, data=xml_data, intent="Create a test case", headers={'Referer': 'https://jazz.ibm.com:9443/qm', 'X-Jazz-CSRF-Prevent': jsessionid }, remove_parameters=['oslc_config.context'] ) + +if response.status_code==201: + print("Test Case created succesfully") + #Get the url of the new Test case created + tcquerybase = c.get_query_capability_uri("oslc_qm:TestCaseQuery") + if not tcquerybase: + raise Exception( "TestCaseQueryBase not found !!!" ) + + print(f"Querying test case with title = {tc_title}") + tcs = c.execute_oslc_query( + tcquerybase, + whereterms=[['dcterms:title','=',f'"{tc_title}"']], + select=['*'], + prefixes={rdfxml.RDF_DEFAULT_PREFIX["dcterms"]:'dcterms'} # note this is reversed - url to prefix + ) + if len(tcs.items())==1: + + tc_u = list(tcs.keys())[0] + print(f"Found Test Case URL: {tc_u}") + print("Doing a Get on test case url") + xml_data,etag = c.execute_get_rdf_xml( tc_u, return_etag=True) + + print("Etag:" + etag) + #put the TC data in a test case object + newTC = TestCase.from_etree(xml_data) + + #adding a link to a DWA requirement, provide URL and link title + newTC.add_validatesRequirementLink("https://dwa9729rom1.fyre.ibm.com:8443/dwa/rm/urn:rational::1-66cdc1432a885b81-O-2-00000040","Module1 (2)") + + #adding a link to a DNG requirement, provide URL and link title + newTC.add_validatesRequirementLink("https://jazz.ibm.com:9443/rm/resources/BI_kC8csQ_WEfCjT5cep7iZxA","req3") + + #get the data from the test case object + xml_data = newTC.to_etree() + #print(ET.tostring(xml_data)) + print("sending the PUT request to update the test case") + response = c.execute_post_rdf_xml(tc_u, data=xml_data, put=True, cacheable=False, headers={'If-Match':etag,'Content-Type':'application/rdf+xml'}, intent="Update the test case" ) + if response.status_code==200: + print("Update succesfull") + else: + print("Update failed") + + elif len(tcs.items())==0: + print("No test case found") + + else: + print(f"We found more than one test case with title {tc_title} !!!???") + #print(ET.tostring(xml_data)) +else: + print(f"Can not create test case: {response.status_code}") + +#################################################################################################### + +print( "Finished" ) diff --git a/elmclient/oslcqueryapi.py b/elmclient/oslcqueryapi.py index c0a83eb..8e0a19d 100644 --- a/elmclient/oslcqueryapi.py +++ b/elmclient/oslcqueryapi.py @@ -783,7 +783,7 @@ def _execute_vanilla_oslc_query(self, querycapabilityuri, query_params, orderby= elif gcmode: rdfs_member_es = rdfxml.xml_find_elements( result_xml, './/ldp:contains') elif qmmode: - rdfs_member_es = rdfxml.xml_find_elements( result_xml, './/rdf:Description[@rdf:about]/qm_rqm:orderIndex/..') + rdfs_member_es = rdfxml.xml_find_elements( result_xml, './/rdf:Description[@rdf:about]/rqm_qm:orderIndex/..') # print( f"1 {rdfs_member_es=}" ) if len(rdfs_member_es)==0: rdfs_member_es = rdfxml.xml_find_elements( result_xml, './/rdf:Description[@rdf:about]/dcterms:title/..') diff --git a/elmclient/rdfxml.py b/elmclient/rdfxml.py index 18e68b8..0e2e519 100644 --- a/elmclient/rdfxml.py +++ b/elmclient/rdfxml.py @@ -48,7 +48,7 @@ 'process': 'http://jazz.net/ns/process#', '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_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#', @@ -61,6 +61,7 @@ 'rm_text': 'http://jazz.net/xmlns/alm/rm/text/v0.1', # for RR 'rm_view': 'http://jazz.net/ns/rm/dng/view#', 'rqm': 'http://jazz.net/xmlns/prod/jazz/rqm/qm/1.0/', + 'rqm_qm': 'http://jazz.net/ns/qm/rqm#', 'rrm': 'http://www.ibm.com/xmlns/rrm/1.0/', # For RR 'rtc_cm': "http://jazz.net/xmlns/prod/jazz/rtc/cm/1.0/", 'trs': "http://open-services.net/ns/core/trs#", # for trs diff --git a/elmclient/resource.py b/elmclient/resource.py index 70f5f0f..1953a8b 100644 --- a/elmclient/resource.py +++ b/elmclient/resource.py @@ -147,7 +147,7 @@ def __setattr__( self, name, value ): if not self._force and name not in self._prefixes: raise Exception( "No property {name}!" ) if hasattr( self, "_prefixes" ): - taggedname = f"{{{self._prefixes.get(name,"UNDEFINED")}}}:{name}" + taggedname = f"{{{self._prefixes.get(name,'UNDEFINED')}}}:{name}" else: taggedname = f"{{NONE}}:{name}" diff --git a/elmclient/testcase.py b/elmclient/testcase.py new file mode 100644 index 0000000..0a6db08 --- /dev/null +++ b/elmclient/testcase.py @@ -0,0 +1,317 @@ +from dataclasses import dataclass, field +from typing import List, Optional, Dict, Tuple +import lxml.etree as ET + +@dataclass +class TestCaseLink: + node_id: Optional[str] = None + subject: Optional[str] = None + predicate: str = "" + target: str = "" + title: Optional[str] = None + +@dataclass +class TestCase: + @classmethod + def create_minimal(cls, title: str) -> 'TestCase': + """ + Create a new minimal TestCase instance with just a title. + The generated XML will include only the required namespaces, type, and title. + :param title: Title of the new Test Case + :return: A minimal TestCase object + """ + namespaces = { + 'rdf': 'http://www.w3.org/1999/02/22-rdf-syntax-ns#', + 'dcterms': 'http://purl.org/dc/terms/', + 'oslc_qm': 'http://open-services.net/ns/qm#', + 'rqm_auto': 'http://jazz.net/ns/auto/rqm#', + 'acp': 'http://jazz.net/ns/acp#', + 'calm': 'http://jazz.net/xmlns/prod/jazz/calm/1.0/', + 'acc': 'http://open-services.net/ns/core/acc#', + 'process': 'http://jazz.net/ns/process#', + 'skos': 'http://www.w3.org/2004/02/skos/core#', + 'jrs': 'http://jazz.net/ns/jrs#', + 'oslc_auto': 'http://open-services.net/ns/auto#', + 'xsd': 'http://www.w3.org/2001/XMLSchema#', + 'bp': 'http://open-services.net/ns/basicProfile#', + 'cmx': 'http://open-services.net/ns/cm-x#', + 'rdfs': 'http://www.w3.org/2000/01/rdf-schema#', + 'rqm_lm': 'http://jazz.net/ns/qm/rqm/labmanagement#', + 'oslc': 'http://open-services.net/ns/core#', + 'owl': 'http://www.w3.org/2002/07/owl#', + 'rqm_process': 'http://jazz.net/xmlns/prod/jazz/rqm/process/1.0/', + 'jazz': 'http://jazz.net/ns/jazz#', + 'oslc_config': 'http://open-services.net/ns/config#', + 'oslc_cm': 'http://open-services.net/ns/cm#', + 'rqm_qm': 'http://jazz.net/ns/qm/rqm#', + 'oslc_rm': 'http://open-services.net/ns/rm#', + 'foaf': 'http://xmlns.com/foaf/0.1/' + } + + tc = cls( + uri="", + title=title, + type="http://open-services.net/ns/qm#TestCase", + namespaces=namespaces + ) + + # Add to elements for proper serialization + tc.elements.append(( + '{http://purl.org/dc/terms/}title', + {'{http://www.w3.org/2001/XMLSchema#}datatype': 'http://www.w3.org/2001/XMLSchema#string'}, + title + )) + tc.elements.append(( + '{http://www.w3.org/1999/02/22-rdf-syntax-ns#}type', + {'{http://www.w3.org/1999/02/22-rdf-syntax-ns#}resource': 'http://open-services.net/ns/qm#TestCase'}, + None + )) + + return tc + + uri: str="" + title: Optional[str] = None + description: Optional[str] = None + identifier: Optional[str] = None + created: Optional[str] = None + modified: Optional[str] = None + creator: Optional[str] = None + contributor: Optional[str] = None + type: Optional[str] = None + relation: Optional[str] = None + short_id: Optional[str] = None + short_identifier: Optional[str] = None + script_step_count: Optional[str] = None + weight: Optional[str] = None + is_locked: Optional[str] = None + links: List[TestCaseLink] = field(default_factory=list) + namespaces: Dict[str, str] = field(default_factory=dict) + elements: List[Tuple[str, Dict[str, str], Optional[str]]] = field(default_factory=list) + + + + def add_link(self, predicate: str, target: str, title: Optional[str] = None): + """ + Add a generic link to the test case. + :param predicate: Predicate URI of the link + :param target: Target URI of the linked resource + :param title: Optional title describing the link + """ + self.links.append(TestCaseLink(predicate=predicate, target=target, title=title)) + + def add_validatesRequirementLink(self, target: str, title: Optional[str] = None): + """ + Add a validatesRequirement link to the test case and include a corresponding RDF property. + :param target: The target requirement URI + :param title: Optional title for the link + """ + self.links.append(TestCaseLink( + subject=self.uri, + predicate="http://open-services.net/ns/qm#validatesRequirement", + target=target, + title=title + )) + # Add corresponding property to the TestCase element list + tag = '{' + self.namespaces.get('oslc_qm', 'http://open-services.net/ns/qm#') + '}validatesRequirement' + attrib = {'{' + self.namespaces['rdf'] + '}resource': target} + self.elements.append((tag, attrib, None)) + + def delete_link(self, target: str) -> bool: + """ + Delete a generic link based on its target URI. + :param target: The target URI to remove + :return: True if a link was removed, False otherwise + """ + initial_length = len(self.links) + self.links = [link for link in self.links if link.target != target] + return len(self.links) < initial_length + + def delete_validatesRequirementLink(self, target: str) -> bool: + """ + Delete validatesRequirement links matching the target URL from both the links list and the elements list. + """ + initial_links = len(self.links) + self.links = [link for link in self.links if not ( + link.predicate == "http://open-services.net/ns/qm#validatesRequirement" and link.target == target) + ] + + uri = self.namespaces.get('rdf', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#') + oslc_qm_ns = self.namespaces.get('oslc_qm', 'http://open-services.net/ns/qm#') + validate_tag = '{' + oslc_qm_ns + '}validatesRequirement' + self.elements = [e for e in self.elements if not ( + e[0] == validate_tag and e[1].get('{' + uri + '}resource') == target) + ] + + return len(self.links) < initial_links + + @staticmethod + def from_etree(etree: ET._ElementTree) -> 'TestCase': + """ + Parse a TestCase instance from an RDF XML ElementTree. + :param etree: lxml.etree ElementTree representing the RDF + :return: TestCase instance with parsed data + """ + root = etree.getroot() + namespaces = {k if k is not None else '': v for k, v in root.nsmap.items()} + ns = namespaces.copy() + + main_elem = root.find(".//rdf:Description[@rdf:about]", ns) + if main_elem is None: + raise ValueError("No rdf:Description with rdf:about found") + + uri = main_elem.attrib[f'{{{ns["rdf"]}}}about'] + testcase = TestCase(uri=uri, namespaces=namespaces) + + for elem in main_elem: + tag = elem.tag + text = elem.text.strip() if elem.text else "" + attrib = dict(elem.attrib) + short_tag = ET.QName(tag).localname + testcase.elements.append((tag, attrib, text)) + + if short_tag == 'title' and tag.startswith('{http://purl.org/dc/terms/}'): + testcase.title = text + elif short_tag == 'identifier': + testcase.identifier = text + elif short_tag == 'description': + testcase.description = text + elif short_tag == 'created': + testcase.created = text + elif short_tag == 'modified': + testcase.modified = text + elif short_tag == 'creator': + testcase.creator = attrib.get(f'{{{ns["rdf"]}}}resource') + elif short_tag == 'contributor': + testcase.contributor = attrib.get(f'{{{ns["rdf"]}}}resource') + elif short_tag == 'type' and f'{{{ns["rdf"]}}}resource' in attrib: + testcase.type = attrib[f'{{{ns["rdf"]}}}resource'] + elif short_tag == 'relation': + testcase.relation = attrib.get(f'{{{ns["rdf"]}}}resource') + elif short_tag == 'shortId': + testcase.short_id = text + elif short_tag == 'shortIdentifier': + testcase.short_identifier = text + elif short_tag == 'scriptStepCount': + testcase.script_step_count = text + elif short_tag == 'weight': + testcase.weight = text + elif short_tag == 'isLocked': + testcase.is_locked = text + + for stmt in root.findall('.//rdf:Description[@rdf:nodeID]', ns): + node_id = stmt.attrib.get(f'{{{ns["rdf"]}}}nodeID') + subject_elem = stmt.find('rdf:subject', ns) + predicate_elem = stmt.find('rdf:predicate', ns) + object_elem = stmt.find('rdf:object', ns) + title_elem = stmt.find('dcterms:title', ns) + + if subject_elem is not None and predicate_elem is not None and object_elem is not None: + testcase.links.append(TestCaseLink( + node_id=node_id, + subject=subject_elem.attrib.get(f'{{{ns["rdf"]}}}resource'), + predicate=predicate_elem.attrib.get(f'{{{ns["rdf"]}}}resource'), + target=object_elem.attrib.get(f'{{{ns["rdf"]}}}resource'), + title=title_elem.text if title_elem is not None else None + )) + + return testcase + + def to_etree(self) -> ET._ElementTree: + """ + Serialize the TestCase instance to an RDF XML ElementTree. + :return: lxml.etree ElementTree + """ + NSMAP = self.namespaces or {'rdf': "http://www.w3.org/1999/02/22-rdf-syntax-ns#"} + rdf = ET.Element(ET.QName(NSMAP['rdf'], 'RDF'), nsmap=NSMAP) + if self.uri!="": + desc = ET.SubElement(rdf, ET.QName(NSMAP['rdf'], 'Description'), { + ET.QName(NSMAP['rdf'], 'about'): self.uri + }) + else: + desc = ET.SubElement(rdf, ET.QName(NSMAP['rdf'], 'Description')) + + # Emit known fields first + def add(tag_ns: str, tag: str, text=None, attrib=None): + el = ET.SubElement(desc, ET.QName(NSMAP[tag_ns], tag), attrib or {}) + if text: + el.text = text + + if self.title is not None: + add('dcterms', 'title', self.title, {f'{{{NSMAP["rdf"]}}}datatype': 'http://www.w3.org/2001/XMLSchema#string'}) + if self.identifier is not None: + add('dcterms', 'identifier', self.identifier, {f'{{{NSMAP["rdf"]}}}datatype': 'http://www.w3.org/2001/XMLSchema#string'}) + if self.description is not None: + add('dcterms', 'description', self.description, {f'{{{NSMAP["rdf"]}}}datatype': 'http://www.w3.org/2001/XMLSchema#string'}) + if self.created is not None: + add('dcterms', 'created', self.created, {f'{{{NSMAP["rdf"]}}}datatype': 'http://www.w3.org/2001/XMLSchema#dateTime'}) + if self.modified is not None: + add('dcterms', 'modified', self.modified, {f'{{{NSMAP["rdf"]}}}datatype': 'http://www.w3.org/2001/XMLSchema#dateTime'}) + if self.creator: + add('dcterms', 'creator', None, {f'{{{NSMAP["rdf"]}}}resource': self.creator}) + if self.contributor: + add('dcterms', 'contributor', None, {f'{{{NSMAP["rdf"]}}}resource': self.contributor}) + if self.type: + add('rdf', 'type', None, {f'{{{NSMAP["rdf"]}}}resource': self.type}) + if self.relation: + add('dcterms', 'relation', None, {f'{{{NSMAP["rdf"]}}}resource': self.relation}) + if self.short_id: + add('oslc', 'shortId', self.short_id, {f'{{{NSMAP["rdf"]}}}datatype': 'http://www.w3.org/2001/XMLSchema#int'}) + if self.short_identifier: + add('rqm_qm', 'shortIdentifier', self.short_identifier, {f'{{{NSMAP["rdf"]}}}datatype': 'http://www.w3.org/2001/XMLSchema#string'}) + if self.script_step_count: + add('rqm_qm', 'scriptStepCount', self.script_step_count, {f'{{{NSMAP["rdf"]}}}datatype': 'http://www.w3.org/2001/XMLSchema#long'}) + if self.weight: + add('rqm_qm', 'weight', self.weight, {f'{{{NSMAP["rdf"]}}}datatype': 'http://www.w3.org/2001/XMLSchema#int'}) + if self.is_locked: + add('rqm_qm', 'isLocked', self.is_locked, {f'{{{NSMAP["rdf"]}}}datatype': 'http://www.w3.org/2001/XMLSchema#boolean'}) + + known_tags = { + 'title', 'description', 'identifier', 'created', 'modified', 'creator', 'contributor', + 'type', 'relation', 'shortId', 'shortIdentifier', 'scriptStepCount', 'weight', 'isLocked' + } + + for tag, attrib, text in self.elements: + short_tag = ET.QName(tag).localname + if short_tag in known_tags: + continue # already added from field values + el = ET.SubElement(desc, ET.QName(tag), { + ET.QName(k) if isinstance(k, str) and ':' in k else k: v for k, v in attrib.items() + }) + if text: + el.text = text + + for i, link in enumerate(self.links): + attribs = {} + if link.node_id: + attribs[ET.QName(NSMAP['rdf'], 'nodeID')] = link.node_id + stmt = ET.SubElement(rdf, ET.QName(NSMAP['rdf'], 'Description'), attribs) + ET.SubElement(stmt, ET.QName(NSMAP['rdf'], 'subject'), { + ET.QName(NSMAP['rdf'], 'resource'): link.subject or self.uri + }) + ET.SubElement(stmt, ET.QName(NSMAP['rdf'], 'predicate'), { + ET.QName(NSMAP['rdf'], 'resource'): link.predicate + }) + ET.SubElement(stmt, ET.QName(NSMAP['rdf'], 'object'), { + ET.QName(NSMAP['rdf'], 'resource'): link.target + }) + ET.SubElement(stmt, ET.QName(NSMAP['rdf'], 'type'), { + ET.QName(NSMAP['rdf'], 'resource'): NSMAP['rdf'] + 'Statement' + }) + if link.title: + ET.SubElement(stmt, ET.QName(NSMAP['dcterms'], 'title')).text = link.title + + return ET.ElementTree(rdf) + + def is_xml_equal(self, other: 'TestCase') -> bool: + """ + Compare two TestCase instances for XML structural equality. + :param other: Another TestCase instance + :return: True if both XML trees are semantically identical + """ + """ + Compare two TestCase instances for XML structural equality. + """ + def clean(xml: ET._ElementTree) -> bytes: + return ET.tostring(xml.getroot(), encoding='utf-8', method='c14n') + + return clean(self.to_etree()) == clean(other.to_etree())