robotframework-robotlog2rqm 1.5.0__py3-none-any.whl → 1.6.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
RobotLog2RQM/CRQM.py CHANGED
@@ -308,9 +308,9 @@ Disconnect from RQM.
308
308
  """
309
309
  self.session.close()
310
310
 
311
- def config(self, plan_id, build_name=None, config_name=None,
311
+ def config(self, plan_id=None, build_name=None, config_name=None,
312
312
  createmissing=False, updatetestcase=False, suite_id=None,
313
- stream=None, baseline=None, naming_convention=None):
313
+ stream=None, baseline=None, naming_convention=None, fetch_team_areas=True):
314
314
  """
315
315
  Configure RQMClient with testplan ID, build, configuration, createmissing, ...
316
316
 
@@ -322,7 +322,7 @@ Configure RQMClient with testplan ID, build, configuration, createmissing, ...
322
322
 
323
323
  * ``plan_id``
324
324
 
325
- / *Condition*: required / *Type*: str /
325
+ / *Condition*: optional / *Type*: str /
326
326
 
327
327
  Testplan ID of RQM project for importing result(s).
328
328
 
@@ -352,12 +352,36 @@ Configure RQMClient with testplan ID, build, configuration, createmissing, ...
352
352
 
353
353
  If `True`, the information of testcase on RQM will be updated bases on robot testfile.
354
354
 
355
- * ``suite_id (optional)``
355
+ * ``suite_id``
356
356
 
357
357
  / *Condition*: optional / *Type*: str / *Default*: None /
358
358
 
359
359
  Testsuite ID of RQM project for importing result(s).
360
360
 
361
+ * ``stream``
362
+
363
+ / *Condition*: optional / *Type*: str / *Default*: None /
364
+
365
+ Project stream which is enabled in Configuration Management (CM).
366
+
367
+ * ``baseline``
368
+
369
+ / *Condition*: optional / *Type*: str / *Default*: None /
370
+
371
+ Project baseline which is enabled in Configuration Management (CM).
372
+
373
+ * ``naming_convention``
374
+
375
+ / *Condition*: optional / *Type*: dict / *Default*: None /
376
+
377
+ Naming convention for the creating new resources (testcase, TCER, ...).
378
+
379
+ * ``fetch_team_areas``
380
+
381
+ / *Condition*: optional / *Type*: bool / *Default*: True /
382
+
383
+ If `True`, fetch team-areas information for further process.
384
+
361
385
  **Returns:**
362
386
 
363
387
  (*no returns*)
@@ -411,12 +435,13 @@ Configure RQMClient with testplan ID, build, configuration, createmissing, ...
411
435
  raise Exception("Get all baselines failed. Reason: %s"%res['message'])
412
436
 
413
437
  # Verify testplan ID
414
- res_plan = self.getResourceByID('testplan', plan_id)
415
- if res_plan.status_code != 200:
416
- raise Exception('Testplan with ID %s is not existing!'%str(plan_id))
438
+ if plan_id != None:
439
+ res_plan = self.getResourceByID('testplan', plan_id)
440
+ if res_plan.status_code != 200:
441
+ raise Exception('Testplan with ID %s is not existing!'%str(plan_id))
417
442
 
418
- oTestplan = get_xml_tree(BytesIO(str(res_plan.text).encode()), bdtd_validation=False)
419
- self.testplan.name = oTestplan.find('ns4:title', oTestplan.getroot().nsmap).text
443
+ oTestplan = get_xml_tree(BytesIO(str(res_plan.text).encode()), bdtd_validation=False)
444
+ self.testplan.name = oTestplan.find('ns4:title', oTestplan.getroot().nsmap).text
420
445
 
421
446
  # Verify and create build version if required
422
447
  if build_name != None:
@@ -453,7 +478,8 @@ Configure RQMClient with testplan ID, build, configuration, createmissing, ...
453
478
  self.testsuite.name = oTestsuite.find('ns4:title', oTestsuite.getroot().nsmap).text
454
479
 
455
480
  # get all team-areas for testcase template
456
- self.getAllTeamAreas()
481
+ if fetch_team_areas:
482
+ self.getAllTeamAreas()
457
483
 
458
484
 
459
485
  except Exception as error:
@@ -680,6 +706,54 @@ Return the name for given resource bases on the naming convention
680
706
  except:
681
707
  raise Exception(f"Failed to generate name for {resource} '{name}'")
682
708
 
709
+ def __parse_test_artifact(self, xml_data):
710
+ """
711
+ Parse XML test case response and extract its data.
712
+
713
+ **Arguments:**
714
+
715
+ * ``xml_data``
716
+
717
+ / *Condition*: required / *Type*: str /
718
+
719
+ The response text from request to get test case as xml string.
720
+
721
+ **Returns:**
722
+
723
+ * / *Type*: dict /
724
+
725
+ The dictionary which contains the parsed data from given xml.
726
+
727
+ .. code:: python
728
+
729
+ {
730
+ 'id': ...,
731
+ 'name': ...,
732
+ 'url': ...,
733
+ ...
734
+ }
735
+
736
+ """
737
+ test_tree = get_xml_tree(BytesIO(str(xml_data).encode()), bdtd_validation=False)
738
+ nsmap = test_tree.getroot().nsmap
739
+
740
+ test_name = test_tree.find('ns4:title', test_tree.getroot().nsmap)
741
+ test_web_id = test_tree.find('ns2:webId', test_tree.getroot().nsmap)
742
+ test_url = test_tree.find('ns4:identifier', test_tree.getroot().nsmap)
743
+ test_category = test_tree.findall('ns2:category', test_tree.getroot().nsmap)
744
+ test_custom_attr = test_tree.findall('.//ns2:customAttribute', test_tree.getroot().nsmap)
745
+ test_data = {'id': test_web_id.text,
746
+ 'name': test_name.text if test_name is not None else '',
747
+ 'url': test_url.text}
748
+ for item in test_category:
749
+ test_data[item.attrib.get('term')] = item.attrib.get('value')
750
+ for attr in test_custom_attr:
751
+ try:
752
+ test_data[attr.find("ns2:name", nsmap).text] = attr.find("ns2:value", nsmap).text
753
+ except:
754
+ pass
755
+
756
+ return test_data
683
757
  #
684
758
  # Methods to get resources
685
759
  #
@@ -849,17 +923,23 @@ Example:
849
923
  else:
850
924
  raise Exception(f"Could not get 'team-areas' of project '{self.projectname}'.")
851
925
 
852
- def getTestsFromTestplan(self, testplan_id, artifact_types):
926
+ def getTestArtifactsFromResource(self, resource_type, resource_id, artifact_types):
853
927
  """
854
928
  Get all test cases and test suites associated with a given test plan.
855
929
 
856
930
  **Arguments:**
857
931
 
858
- * ``testplan_id``
932
+ * ``resource_type``
933
+
934
+ / *Condition*: required / *Type*: str /
935
+
936
+ The RQM test resource (`testplan` or `testsuite`) to get test artifact(s).
937
+
938
+ * ``resource_id``
859
939
 
860
940
  / *Condition*: required / *Type*: str /
861
941
 
862
- The RQM test plan to get test artifact(s).
942
+ The RQM test plan/suite to get test artifact(s).
863
943
 
864
944
  * ``artifact_types``
865
945
 
@@ -880,9 +960,16 @@ Get all test cases and test suites associated with a given test plan.
880
960
  'testsuite': [{'id': ..., 'name': ..., 'url': ...}, ...]
881
961
  }
882
962
  """
963
+ ALLOW_RESOURCE_TYPES = ['testplan', 'testsuite']
883
964
  ALLOW_ARTIFACT_TYPES = ['testcase', 'testsuite']
884
965
  result = {}
885
966
 
967
+ if resource_type not in ALLOW_RESOURCE_TYPES:
968
+ raise ValueError(
969
+ f"Unsupported resource type '{resource_type}'. "
970
+ f"Please use one of the following: {', '.join(ALLOW_RESOURCE_TYPES)}."
971
+ )
972
+
886
973
  if isinstance(artifact_types, str):
887
974
  artifact_types = [artifact_types]
888
975
  elif not isinstance(artifact_types, (list, tuple)):
@@ -899,9 +986,9 @@ Get all test cases and test suites associated with a given test plan.
899
986
  )
900
987
  result[artifact_type] = []
901
988
 
902
- res = self.getResourceByID('testplan', testplan_id)
989
+ res = self.getResourceByID(resource_type, resource_id)
903
990
  if res.status_code != 200:
904
- raise Exception(f"Failed to get testplan {testplan_id}: {res.reason}")
991
+ raise Exception(f"Failed to get {resource_type} {resource_id}: {res.reason}")
905
992
 
906
993
  oTree = get_xml_tree(BytesIO(str(res.text).encode()), bdtd_validation=False)
907
994
  root = oTree.getroot()
@@ -914,16 +1001,11 @@ Get all test cases and test suites associated with a given test plan.
914
1001
  href = oTest.attrib.get('href')
915
1002
  if href:
916
1003
  test_id = href.split('/')[-1]
917
- # Get testcase name
1004
+ # Get testcase/testsuite by ID
918
1005
  test_res = self.getResourceByID(artifact_type, test_id)
919
1006
  if test_res.status_code == 200:
920
- test_tree = get_xml_tree(BytesIO(str(test_res.text).encode()), bdtd_validation=False)
921
- test_name = test_tree.find('ns4:title', test_tree.getroot().nsmap)
922
- test_web_id = test_tree.find('ns2:webId', test_tree.getroot().nsmap)
923
- test_url = test_tree.find('ns4:identifier', test_tree.getroot().nsmap)
924
- result[artifact_type].append({'id': test_web_id.text,
925
- 'name': test_name.text if test_name is not None else '',
926
- 'url': test_url.text})
1007
+ test_data = self.__parse_test_artifact(test_res.text)
1008
+ result[artifact_type].append(test_data)
927
1009
 
928
1010
  return result
929
1011
 
Binary file
@@ -225,7 +225,7 @@ Default schema supports below information:
225
225
  # TESTCASE_NAME is not available for non-testcase relevant resources
226
226
  # TESTSUITE_NAME is not available for `buildrecord` and `configuration` resources
227
227
  # Warning user for using wrong place holders
228
- oMatch = re.search(".*\{(.*)\}.*", dConfig[key])
228
+ oMatch = re.search(r".*\{(.*)\}.*", dConfig[key])
229
229
  if oMatch:
230
230
  if oMatch.group(1) not in CRQMClient.SUPPORTED_PLACEHOLDER:
231
231
  Logger.log_warning(f"Place holder '{{{oMatch.group(1)}}}' is not supported, it will not be replaced when generating {key} resource")
RobotLog2RQM/rqmtool.py CHANGED
@@ -83,11 +83,26 @@ Process provided argument(s) from command line.
83
83
  required=True,
84
84
  help="RQM password."
85
85
  )
86
- parser.add_argument(
86
+ # exclusive group to provdve only --testsuite or --testplan
87
+ group = parser.add_mutually_exclusive_group(required=True)
88
+ group.add_argument(
87
89
  "--testplan",
88
- required=True,
89
90
  help="RQM testplan ID."
90
91
  )
92
+ group.add_argument(
93
+ "--testsuite",
94
+ help="RQM testsuite ID."
95
+ )
96
+ parser.add_argument(
97
+ "--stream",
98
+ type=str,
99
+ help="project stream. Note, requires Configuration Management (CM) to be enabled for the project area."
100
+ )
101
+ parser.add_argument(
102
+ '--baseline',
103
+ type=str,
104
+ help='project baseline. Note, requires Configuration Management (CM), or Baselines Only to be enabled for the project area.'
105
+ )
91
106
  parser.add_argument(
92
107
  "--dryrun",
93
108
  action="store_true",
@@ -264,6 +279,56 @@ Write data to output files (JSON or CSV) according to specified options.
264
279
  file_name = os.path.join(output_dir, f"{basename}_{artifact_type}s.csv")
265
280
  write_csv_file(file_name, data, artifact_type)
266
281
 
282
+ def normalize_custom_attributes(data, artifact_types):
283
+ """
284
+ Ensure all artifacts across projects have consistent keys.
285
+ Missing custom attributes will be filled with empty strings.
286
+
287
+ **Arguments:**
288
+
289
+ * ``data``
290
+
291
+ / *Condition*: required / *Type*: dict /
292
+
293
+ Dictionary containing artifact data fetched from RQM, where each key represents
294
+ an artifact type (e.g., ``testcase``, ``testsuite``) and the value is a list
295
+ of dictionaries holding artifact details.
296
+
297
+ * ``artifact_types``
298
+
299
+ / *Condition*: required / *Type*: list /
300
+
301
+ List of artifact types to process (e.g., ``['testcase', 'testsuite']``).
302
+ Each type key in ``data`` will be normalized to ensure all items have the same set of attributes.
303
+
304
+ **Returns:**
305
+
306
+ * ``data``
307
+
308
+ / *Type*: dict /
309
+
310
+ Normalized dictionary with consistent keys across all artifacts.
311
+ Any missing custom attributes are added with an empty string ("") as value.
312
+ """
313
+ for artifact_type in artifact_types:
314
+ artifacts = data.get(artifact_type, [])
315
+ if not artifacts:
316
+ continue
317
+
318
+ # Collect all keys (standard + custom attributes)
319
+ all_keys = set()
320
+ for item in artifacts:
321
+ all_keys.update(item.keys())
322
+
323
+ # Fill missing attributes with empty string
324
+ for item in artifacts:
325
+ for key in all_keys:
326
+ if key not in item:
327
+ item[key] = ""
328
+
329
+ return data
330
+
331
+
267
332
  def RQMTool():
268
333
  """
269
334
  Main entry point for RQMTool CLI.
@@ -283,6 +348,7 @@ Main entry point for RQMTool CLI.
283
348
  RQMClient = CRQMClient(args.user, args.password, args.project, args.host)
284
349
  try:
285
350
  bSuccess = RQMClient.login()
351
+ RQMClient.config(stream=args.stream, baseline=args.baseline)
286
352
  if bSuccess:
287
353
  Logger.log()
288
354
  Logger.log(f"Login RQM as user '{args.user}' successfully!")
@@ -292,20 +358,30 @@ Main entry point for RQMTool CLI.
292
358
  Logger.log_error(f"Could not login to RQM: '{str(reason)}'.")
293
359
 
294
360
  if not args.dryrun:
295
- testplan_data = RQMClient.getTestsFromTestplan(args.testplan, args.types)
296
-
297
- basename_with_id = f"{args.basename}_{args.testplan}"
361
+ if args.testplan:
362
+ artifact_types = args.types
363
+ basename_with_id = f"{args.basename}_{args.testplan}"
364
+ test_data = RQMClient.getTestArtifactsFromResource('testplan', args.testplan, args.types)
365
+ test_data = normalize_custom_attributes(test_data, artifact_types)
366
+ elif args.testsuite:
367
+ artifact_types = ['testcase']
368
+ if args.basename == "testplan_export":
369
+ basename_with_id = f"testsuite_export_{args.testsuite}"
370
+ else:
371
+ basename_with_id = f"{args.basename}_{args.testsuite}"
372
+ test_data = RQMClient.getTestArtifactsFromResource('testsuite', args.testsuite, artifact_types)
373
+ test_data = normalize_custom_attributes(test_data, artifact_types)
298
374
 
299
375
  write_output_file(
300
- testplan_data,
376
+ test_data,
301
377
  output_dir=args.output_dir,
302
378
  basename=basename_with_id,
303
379
  extension=args.format,
304
- artifact_types=args.types
380
+ artifact_types=artifact_types
305
381
  )
306
382
 
307
- for artifact_type in args.types:
308
- items = testplan_data.get(artifact_type, [])
383
+ for artifact_type in artifact_types:
384
+ items = test_data.get(artifact_type, [])
309
385
  Logger.log(f"Found {len(items)} {artifact_type}(s)")
310
386
  cnt = 1
311
387
  for item in items:
RobotLog2RQM/version.py CHANGED
@@ -18,5 +18,5 @@
18
18
  #
19
19
  # Version and date of RobotLog2RQM
20
20
  #
21
- VERSION = "1.5.0"
22
- VERSION_DATE = "28.10.2025"
21
+ VERSION = "1.6.0"
22
+ VERSION_DATE = "10.11.2025"
@@ -1,12 +1,10 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: robotframework-robotlog2rqm
3
- Version: 1.5.0
3
+ Version: 1.6.0
4
4
  Summary: Imports robot result(s) to IBM Rational Quality Manager (RQM)
5
5
  Home-page: https://github.com/test-fullautomation/robotframework-robotlog2rqm
6
6
  Author: Tran Duy Ngoan
7
7
  Author-email: Ngoan.TranDuy@vn.bosch.com
8
- License: UNKNOWN
9
- Platform: UNKNOWN
10
8
  Classifier: Programming Language :: Python :: 3
11
9
  Classifier: License :: OSI Approved :: Apache Software License
12
10
  Classifier: Operating System :: OS Independent
@@ -20,6 +18,16 @@ Requires-Dist: lxml
20
18
  Requires-Dist: requests
21
19
  Requires-Dist: colorama
22
20
  Requires-Dist: robotframework
21
+ Dynamic: author
22
+ Dynamic: author-email
23
+ Dynamic: classifier
24
+ Dynamic: description
25
+ Dynamic: description-content-type
26
+ Dynamic: home-page
27
+ Dynamic: license-file
28
+ Dynamic: requires-dist
29
+ Dynamic: requires-python
30
+ Dynamic: summary
23
31
 
24
32
  # RobotLog2RQM Description
25
33
 
@@ -312,5 +320,3 @@ distributed under the License is distributed on an \"AS IS\" BASIS,
312
320
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
313
321
  See the License for the specific language governing permissions and
314
322
  limitations under the License.
315
-
316
-
@@ -1,11 +1,11 @@
1
- RobotLog2RQM/CRQM.py,sha256=TsgEXYu608vb-rjQ9qTbxzql_EPTgVHqnrdkDs3rAus,67139
2
- RobotLog2RQM/RobotLog2RQM.pdf,sha256=Ujot_99PiYu2s9hS_sQu3fP1occXcbs2WO9qiHSVx6M,349449
1
+ RobotLog2RQM/CRQM.py,sha256=NxFbIKvTOepkdNmv0V9aQCaU2jSGhuHO707XTglMF_A,69357
2
+ RobotLog2RQM/RobotLog2RQM.pdf,sha256=--VWUBLqZOtfQXy9O8N28MvOT7VMRPg9fSAQXl8coK0,358016
3
3
  RobotLog2RQM/__init__.py,sha256=YKDTJjDsnQkr5X-gjjO8opwKUVKm6kc8sIUpURYMk48,596
4
4
  RobotLog2RQM/__main__.py,sha256=JabttEncy80antJWeGVmjoXyiF1DyXxkxdW4xLuHzT0,681
5
5
  RobotLog2RQM/logger.py,sha256=FnZZqssOcrF5yK6wxkgifScoQcFGrqrvzLvXWY3V-9A,4252
6
- RobotLog2RQM/robotlog2rqm.py,sha256=FeMJsBuAu1CLBjDJsQQs7CjQifYgqyhaPMNabnEeSc0,30160
7
- RobotLog2RQM/rqmtool.py,sha256=wDagUGptbPwRYyn5jo1RA8HJmwHoDX_iBew40WFtWJ0,7957
8
- RobotLog2RQM/version.py,sha256=4VudYtnU16wL-FZAKungXo19Q5QacjNSAHMEWv-53JE,916
6
+ RobotLog2RQM/robotlog2rqm.py,sha256=zX8ibJqeQTPwtRF6zX-qHtT1d6jj1EI90vpaUL77FEM,30161
7
+ RobotLog2RQM/rqmtool.py,sha256=s-rTI1MBby0SfQIBP-WRx5_frdtQBss86I9i04MBuy4,10564
8
+ RobotLog2RQM/version.py,sha256=3E-s2UEJ2s5J5UVh1TPTW-T21mrSzBfY_KjCv-v7VQk,916
9
9
  RobotLog2RQM/RQM_templates/buildrecord.xml,sha256=uGot7pNOjPR8do0JsJi0Lz3OCU9NMhODRd428QgvHh4,1498
10
10
  RobotLog2RQM/RQM_templates/configuration.xml,sha256=NrFDv51fuGhgeMiZuRhQ5q_UJ0u_pWzdxisIF5AJs74,1378
11
11
  RobotLog2RQM/RQM_templates/executionresult.xml,sha256=WTp4qDk29peBc0ll6GHVXX_kF_YBsOVjy9vBzoz7_2k,2160
@@ -14,9 +14,9 @@ RobotLog2RQM/RQM_templates/suiteexecutionrecord.xml,sha256=9GAs2WqZMkFJSNEZULm9B
14
14
  RobotLog2RQM/RQM_templates/testcase.xml,sha256=zovFKj-37QHn2S8mMA_9RnAJ3zBmDJkJj31yelsnFFI,2167
15
15
  RobotLog2RQM/RQM_templates/testsuite.xml,sha256=r2ijEsyPoE7qzCtUxgIHDOEcqUveDN4SMf9HSE9b0ZU,1326
16
16
  RobotLog2RQM/RQM_templates/testsuitelog.xml,sha256=l-NlaCyk6Ben76PElXKOHfMlEvyQ-e9MOZ6-F9HvwDQ,1920
17
- robotframework_robotlog2rqm-1.5.0.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
18
- robotframework_robotlog2rqm-1.5.0.dist-info/METADATA,sha256=mBHsyTik_HMPC-SsoTjqph_S_3MQc_CLj3VViXfgXuI,11846
19
- robotframework_robotlog2rqm-1.5.0.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
20
- robotframework_robotlog2rqm-1.5.0.dist-info/entry_points.txt,sha256=94N661T4lHzLSey2WQ18OVduw9-Mf6Kh8HK7cBL2YPY,112
21
- robotframework_robotlog2rqm-1.5.0.dist-info/top_level.txt,sha256=jb_Gt6W44FoOLtGfBe7RzqCLaquhihkEWvSI1zjXDHc,13
22
- robotframework_robotlog2rqm-1.5.0.dist-info/RECORD,,
17
+ robotframework_robotlog2rqm-1.6.0.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
18
+ robotframework_robotlog2rqm-1.6.0.dist-info/METADATA,sha256=k834p4eH88hGgfxhwifUu7ms7TXY83tJCty1uIS8WCA,12028
19
+ robotframework_robotlog2rqm-1.6.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
20
+ robotframework_robotlog2rqm-1.6.0.dist-info/entry_points.txt,sha256=yLoNhxm2lfj3FxMx3Orct8db4qkPLII8u241GAH6BB0,111
21
+ robotframework_robotlog2rqm-1.6.0.dist-info/top_level.txt,sha256=jb_Gt6W44FoOLtGfBe7RzqCLaquhihkEWvSI1zjXDHc,13
22
+ robotframework_robotlog2rqm-1.6.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.45.1)
2
+ Generator: setuptools (80.9.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,4 +1,3 @@
1
1
  [console_scripts]
2
2
  RQMTool = RobotLog2RQM.rqmtool:RQMTool
3
3
  RobotLog2RQM = RobotLog2RQM.robotlog2rqm:RobotLog2RQM
4
-