robotframework-robotlog2rqm 1.4.2__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 +169 -10
- RobotLog2RQM/RobotLog2RQM.pdf +0 -0
- RobotLog2RQM/logger.py +180 -0
- RobotLog2RQM/robotlog2rqm.py +3 -158
- RobotLog2RQM/rqmtool.py +392 -0
- RobotLog2RQM/version.py +2 -2
- {robotframework_robotlog2rqm-1.4.2.dist-info → robotframework_robotlog2rqm-1.6.0.dist-info}/METADATA +75 -17
- robotframework_robotlog2rqm-1.6.0.dist-info/RECORD +22 -0
- {robotframework_robotlog2rqm-1.4.2.dist-info → robotframework_robotlog2rqm-1.6.0.dist-info}/WHEEL +1 -1
- {robotframework_robotlog2rqm-1.4.2.dist-info → robotframework_robotlog2rqm-1.6.0.dist-info}/entry_points.txt +1 -1
- robotframework_robotlog2rqm-1.4.2.dist-info/RECORD +0 -20
- {robotframework_robotlog2rqm-1.4.2.dist-info → robotframework_robotlog2rqm-1.6.0.dist-info/licenses}/LICENSE +0 -0
- {robotframework_robotlog2rqm-1.4.2.dist-info → robotframework_robotlog2rqm-1.6.0.dist-info}/top_level.txt +0 -0
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*:
|
|
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
|
|
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
|
-
|
|
415
|
-
|
|
416
|
-
|
|
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
|
-
|
|
419
|
-
|
|
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
|
-
|
|
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,6 +923,91 @@ Example:
|
|
|
849
923
|
else:
|
|
850
924
|
raise Exception(f"Could not get 'team-areas' of project '{self.projectname}'.")
|
|
851
925
|
|
|
926
|
+
def getTestArtifactsFromResource(self, resource_type, resource_id, artifact_types):
|
|
927
|
+
"""
|
|
928
|
+
Get all test cases and test suites associated with a given test plan.
|
|
929
|
+
|
|
930
|
+
**Arguments:**
|
|
931
|
+
|
|
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``
|
|
939
|
+
|
|
940
|
+
/ *Condition*: required / *Type*: str /
|
|
941
|
+
|
|
942
|
+
The RQM test plan/suite to get test artifact(s).
|
|
943
|
+
|
|
944
|
+
* ``artifact_types``
|
|
945
|
+
|
|
946
|
+
/ *Condition*: required / *Type*: list /
|
|
947
|
+
|
|
948
|
+
List of artifact types (`testcase`, `testsuite`) for fetching.
|
|
949
|
+
|
|
950
|
+
**Returns:**
|
|
951
|
+
|
|
952
|
+
* / *Type*: dict /
|
|
953
|
+
|
|
954
|
+
A dictionary containing fetched artifacts:
|
|
955
|
+
|
|
956
|
+
.. code:: python
|
|
957
|
+
|
|
958
|
+
{
|
|
959
|
+
'testcase': [{'id': ..., 'name': ..., 'url': ...}, ...],
|
|
960
|
+
'testsuite': [{'id': ..., 'name': ..., 'url': ...}, ...]
|
|
961
|
+
}
|
|
962
|
+
"""
|
|
963
|
+
ALLOW_RESOURCE_TYPES = ['testplan', 'testsuite']
|
|
964
|
+
ALLOW_ARTIFACT_TYPES = ['testcase', 'testsuite']
|
|
965
|
+
result = {}
|
|
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
|
+
|
|
973
|
+
if isinstance(artifact_types, str):
|
|
974
|
+
artifact_types = [artifact_types]
|
|
975
|
+
elif not isinstance(artifact_types, (list, tuple)):
|
|
976
|
+
raise TypeError(
|
|
977
|
+
f"Invalid type for 'artifact_types': expected str or list, got {type(artifact_types).__name__}."
|
|
978
|
+
)
|
|
979
|
+
|
|
980
|
+
for item in artifact_types:
|
|
981
|
+
artifact_type = item.lower()
|
|
982
|
+
if artifact_type not in ALLOW_ARTIFACT_TYPES:
|
|
983
|
+
raise ValueError(
|
|
984
|
+
f"Unsupported artifact type '{artifact_type}'. "
|
|
985
|
+
f"Please use one of the following: {', '.join(ALLOW_ARTIFACT_TYPES)}."
|
|
986
|
+
)
|
|
987
|
+
result[artifact_type] = []
|
|
988
|
+
|
|
989
|
+
res = self.getResourceByID(resource_type, resource_id)
|
|
990
|
+
if res.status_code != 200:
|
|
991
|
+
raise Exception(f"Failed to get {resource_type} {resource_id}: {res.reason}")
|
|
992
|
+
|
|
993
|
+
oTree = get_xml_tree(BytesIO(str(res.text).encode()), bdtd_validation=False)
|
|
994
|
+
root = oTree.getroot()
|
|
995
|
+
nsmap = root.nsmap
|
|
996
|
+
|
|
997
|
+
for item in artifact_types:
|
|
998
|
+
artifact_type = item.lower()
|
|
999
|
+
# Find all linked test artifact
|
|
1000
|
+
for oTest in root.findall(f'.//ns2:{artifact_type}', nsmap):
|
|
1001
|
+
href = oTest.attrib.get('href')
|
|
1002
|
+
if href:
|
|
1003
|
+
test_id = href.split('/')[-1]
|
|
1004
|
+
# Get testcase/testsuite by ID
|
|
1005
|
+
test_res = self.getResourceByID(artifact_type, test_id)
|
|
1006
|
+
if test_res.status_code == 200:
|
|
1007
|
+
test_data = self.__parse_test_artifact(test_res.text)
|
|
1008
|
+
result[artifact_type].append(test_data)
|
|
1009
|
+
|
|
1010
|
+
return result
|
|
852
1011
|
|
|
853
1012
|
#
|
|
854
1013
|
# Methods to create XML template for resources
|
RobotLog2RQM/RobotLog2RQM.pdf
CHANGED
|
Binary file
|
RobotLog2RQM/logger.py
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# Copyright 2020-2023 Robert Bosch GmbH
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
# ******************************************************************************
|
|
15
|
+
#
|
|
16
|
+
# File: logging.py
|
|
17
|
+
#
|
|
18
|
+
# Initially created by Tran Duy Ngoan(RBVH/EMC51) / October 2025
|
|
19
|
+
#
|
|
20
|
+
# Logging class
|
|
21
|
+
#
|
|
22
|
+
# History:
|
|
23
|
+
#
|
|
24
|
+
# 2025-10-20:
|
|
25
|
+
# - initial version
|
|
26
|
+
#
|
|
27
|
+
# ******************************************************************************
|
|
28
|
+
import sys
|
|
29
|
+
import colorama as col
|
|
30
|
+
import os
|
|
31
|
+
|
|
32
|
+
class Logger():
|
|
33
|
+
"""
|
|
34
|
+
Logger class for logging message.
|
|
35
|
+
"""
|
|
36
|
+
output_logfile = None
|
|
37
|
+
output_console = True
|
|
38
|
+
color_normal = col.Fore.WHITE + col.Style.NORMAL
|
|
39
|
+
color_error = col.Fore.RED + col.Style.BRIGHT
|
|
40
|
+
color_warn = col.Fore.YELLOW + col.Style.BRIGHT
|
|
41
|
+
color_reset = col.Style.RESET_ALL + col.Fore.RESET + col.Back.RESET
|
|
42
|
+
prefix_warn = "WARN: "
|
|
43
|
+
prefix_error = "ERROR: "
|
|
44
|
+
prefix_fatalerror = "FATAL ERROR: "
|
|
45
|
+
prefix_all = ""
|
|
46
|
+
dryrun = False
|
|
47
|
+
|
|
48
|
+
@classmethod
|
|
49
|
+
def config(cls, output_console=True, output_logfile=None, dryrun=False):
|
|
50
|
+
"""
|
|
51
|
+
Configure Logger class.
|
|
52
|
+
|
|
53
|
+
**Arguments:**
|
|
54
|
+
|
|
55
|
+
* ``output_console``
|
|
56
|
+
|
|
57
|
+
/ *Condition*: optional / *Type*: bool / *Default*: True /
|
|
58
|
+
|
|
59
|
+
Write message to console output.
|
|
60
|
+
|
|
61
|
+
* ``output_logfile``
|
|
62
|
+
|
|
63
|
+
/ *Condition*: optional / *Type*: str / *Default*: None /
|
|
64
|
+
|
|
65
|
+
Path to log file output.
|
|
66
|
+
|
|
67
|
+
* ``dryrun``
|
|
68
|
+
|
|
69
|
+
/ *Condition*: optional / *Type*: bool / *Default*: True /
|
|
70
|
+
|
|
71
|
+
If set, a prefix as 'dryrun' is added for all messages.
|
|
72
|
+
|
|
73
|
+
**Returns:**
|
|
74
|
+
|
|
75
|
+
(*no returns*)
|
|
76
|
+
"""
|
|
77
|
+
cls.output_console = output_console
|
|
78
|
+
cls.output_logfile = output_logfile
|
|
79
|
+
cls.dryrun = dryrun
|
|
80
|
+
if cls.dryrun:
|
|
81
|
+
cls.prefix_all = cls.color_warn + "DRYRUN " + cls.color_reset
|
|
82
|
+
|
|
83
|
+
@classmethod
|
|
84
|
+
def log(cls, msg='', color=None, indent=0):
|
|
85
|
+
"""
|
|
86
|
+
Write log message to console/file output.
|
|
87
|
+
|
|
88
|
+
**Arguments:**
|
|
89
|
+
|
|
90
|
+
* ``msg``
|
|
91
|
+
|
|
92
|
+
/ *Condition*: optional / *Type*: str / *Default*: '' /
|
|
93
|
+
|
|
94
|
+
Message which is written to output.
|
|
95
|
+
|
|
96
|
+
* ``color``
|
|
97
|
+
|
|
98
|
+
/ *Condition*: optional / *Type*: str / *Default*: None /
|
|
99
|
+
|
|
100
|
+
Color style for the message.
|
|
101
|
+
|
|
102
|
+
* ``indent``
|
|
103
|
+
|
|
104
|
+
/ *Condition*: optional / *Type*: int / *Default*: 0 /
|
|
105
|
+
|
|
106
|
+
Offset indent.
|
|
107
|
+
|
|
108
|
+
**Returns:**
|
|
109
|
+
|
|
110
|
+
(*no returns*)
|
|
111
|
+
"""
|
|
112
|
+
if color is None:
|
|
113
|
+
color = cls.color_normal
|
|
114
|
+
if cls.output_console:
|
|
115
|
+
print(cls.prefix_all + cls.color_reset + color + " "*indent + msg + cls.color_reset)
|
|
116
|
+
if cls.output_logfile is not None and os.path.isfile(cls.output_logfile):
|
|
117
|
+
with open(cls.output_logfile, 'a') as f:
|
|
118
|
+
f.write(" "*indent + msg)
|
|
119
|
+
return
|
|
120
|
+
|
|
121
|
+
@classmethod
|
|
122
|
+
def log_warning(cls, msg, indent=0):
|
|
123
|
+
"""
|
|
124
|
+
Write warning message to console/file output.
|
|
125
|
+
|
|
126
|
+
**Arguments:**
|
|
127
|
+
|
|
128
|
+
* ``msg``
|
|
129
|
+
|
|
130
|
+
/ *Condition*: required / *Type*: str /
|
|
131
|
+
|
|
132
|
+
Warning message which is written to output.
|
|
133
|
+
|
|
134
|
+
* ``indent``
|
|
135
|
+
|
|
136
|
+
/ *Condition*: optional / *Type*: int / *Default*: 0 /
|
|
137
|
+
|
|
138
|
+
Offset indent.
|
|
139
|
+
|
|
140
|
+
**Returns:**
|
|
141
|
+
|
|
142
|
+
(*no returns*)
|
|
143
|
+
"""
|
|
144
|
+
cls.log(cls.prefix_warn+str(msg), cls.color_warn, indent)
|
|
145
|
+
|
|
146
|
+
@classmethod
|
|
147
|
+
def log_error(cls, msg, fatal_error=False, indent=0):
|
|
148
|
+
"""
|
|
149
|
+
Write error message to console/file output.
|
|
150
|
+
|
|
151
|
+
* ``msg``
|
|
152
|
+
|
|
153
|
+
/ *Condition*: required / *Type*: str /
|
|
154
|
+
|
|
155
|
+
Error message which is written to output.
|
|
156
|
+
|
|
157
|
+
* ``fatal_error``
|
|
158
|
+
|
|
159
|
+
/ *Condition*: optional / *Type*: bool / *Default*: False /
|
|
160
|
+
|
|
161
|
+
If set, tool will terminate after logging error message.
|
|
162
|
+
|
|
163
|
+
* ``indent``
|
|
164
|
+
|
|
165
|
+
/ *Condition*: optional / *Type*: int / *Default*: 0 /
|
|
166
|
+
|
|
167
|
+
Offset indent.
|
|
168
|
+
|
|
169
|
+
**Returns:**
|
|
170
|
+
|
|
171
|
+
(*no returns*)
|
|
172
|
+
"""
|
|
173
|
+
prefix = cls.prefix_error
|
|
174
|
+
if fatal_error:
|
|
175
|
+
prefix = cls.prefix_fatalerror
|
|
176
|
+
|
|
177
|
+
cls.log(prefix+str(msg), cls.color_error, indent)
|
|
178
|
+
if fatal_error:
|
|
179
|
+
cls.log(f"{sys.argv[0]} has been stopped!", cls.color_error)
|
|
180
|
+
exit(1)
|
RobotLog2RQM/robotlog2rqm.py
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
#
|
|
16
16
|
# File: robotlog2rqm.py
|
|
17
17
|
#
|
|
18
|
-
#
|
|
18
|
+
# Initially created by Tran Duy Ngoan(RBVH/ECM11) / January 2021
|
|
19
19
|
#
|
|
20
20
|
# This tool is used to parse the robot framework results output.xml
|
|
21
21
|
# then import them into RQM - IBM Rational Quality Manager
|
|
@@ -31,13 +31,12 @@ import json
|
|
|
31
31
|
import re
|
|
32
32
|
import argparse
|
|
33
33
|
import os
|
|
34
|
-
import sys
|
|
35
34
|
import datetime
|
|
36
|
-
import colorama as col
|
|
37
35
|
|
|
38
36
|
from robot.api import ExecutionResult
|
|
39
37
|
from RobotLog2RQM.CRQM import CRQMClient
|
|
40
38
|
from RobotLog2RQM.version import VERSION, VERSION_DATE
|
|
39
|
+
from RobotLog2RQM.logger import Logger
|
|
41
40
|
|
|
42
41
|
DRESULT_MAPPING = {
|
|
43
42
|
"PASS": "Passed",
|
|
@@ -74,160 +73,6 @@ NAMING_CONVENTION_SCHEMA = {
|
|
|
74
73
|
"suiteresult" : str
|
|
75
74
|
}
|
|
76
75
|
|
|
77
|
-
#
|
|
78
|
-
# Logger class
|
|
79
|
-
#
|
|
80
|
-
########################################################################
|
|
81
|
-
class Logger():
|
|
82
|
-
"""
|
|
83
|
-
Logger class for logging message.
|
|
84
|
-
"""
|
|
85
|
-
output_logfile = None
|
|
86
|
-
output_console = True
|
|
87
|
-
color_normal = col.Fore.WHITE + col.Style.NORMAL
|
|
88
|
-
color_error = col.Fore.RED + col.Style.BRIGHT
|
|
89
|
-
color_warn = col.Fore.YELLOW + col.Style.BRIGHT
|
|
90
|
-
color_reset = col.Style.RESET_ALL + col.Fore.RESET + col.Back.RESET
|
|
91
|
-
prefix_warn = "WARN: "
|
|
92
|
-
prefix_error = "ERROR: "
|
|
93
|
-
prefix_fatalerror = "FATAL ERROR: "
|
|
94
|
-
prefix_all = ""
|
|
95
|
-
dryrun = False
|
|
96
|
-
|
|
97
|
-
@classmethod
|
|
98
|
-
def config(cls, output_console=True, output_logfile=None, dryrun=False):
|
|
99
|
-
"""
|
|
100
|
-
Configure Logger class.
|
|
101
|
-
|
|
102
|
-
**Arguments:**
|
|
103
|
-
|
|
104
|
-
* ``output_console``
|
|
105
|
-
|
|
106
|
-
/ *Condition*: optional / *Type*: bool / *Default*: True /
|
|
107
|
-
|
|
108
|
-
Write message to console output.
|
|
109
|
-
|
|
110
|
-
* ``output_logfile``
|
|
111
|
-
|
|
112
|
-
/ *Condition*: optional / *Type*: str / *Default*: None /
|
|
113
|
-
|
|
114
|
-
Path to log file output.
|
|
115
|
-
|
|
116
|
-
* ``dryrun``
|
|
117
|
-
|
|
118
|
-
/ *Condition*: optional / *Type*: bool / *Default*: True /
|
|
119
|
-
|
|
120
|
-
If set, a prefix as 'dryrun' is added for all messages.
|
|
121
|
-
|
|
122
|
-
**Returns:**
|
|
123
|
-
|
|
124
|
-
(*no returns*)
|
|
125
|
-
"""
|
|
126
|
-
cls.output_console = output_console
|
|
127
|
-
cls.output_logfile = output_logfile
|
|
128
|
-
cls.dryrun = dryrun
|
|
129
|
-
if cls.dryrun:
|
|
130
|
-
cls.prefix_all = cls.color_warn + "DRYRUN " + cls.color_reset
|
|
131
|
-
|
|
132
|
-
@classmethod
|
|
133
|
-
def log(cls, msg='', color=None, indent=0):
|
|
134
|
-
"""
|
|
135
|
-
Write log message to console/file output.
|
|
136
|
-
|
|
137
|
-
**Arguments:**
|
|
138
|
-
|
|
139
|
-
* ``msg``
|
|
140
|
-
|
|
141
|
-
/ *Condition*: optional / *Type*: str / *Default*: '' /
|
|
142
|
-
|
|
143
|
-
Message which is written to output.
|
|
144
|
-
|
|
145
|
-
* ``color``
|
|
146
|
-
|
|
147
|
-
/ *Condition*: optional / *Type*: str / *Default*: None /
|
|
148
|
-
|
|
149
|
-
Color style for the message.
|
|
150
|
-
|
|
151
|
-
* ``indent``
|
|
152
|
-
|
|
153
|
-
/ *Condition*: optional / *Type*: int / *Default*: 0 /
|
|
154
|
-
|
|
155
|
-
Offset indent.
|
|
156
|
-
|
|
157
|
-
**Returns:**
|
|
158
|
-
|
|
159
|
-
(*no returns*)
|
|
160
|
-
"""
|
|
161
|
-
if color==None:
|
|
162
|
-
color = cls.color_normal
|
|
163
|
-
if cls.output_console:
|
|
164
|
-
print(cls.prefix_all + cls.color_reset + color + " "*indent + msg + cls.color_reset)
|
|
165
|
-
if cls.output_logfile!=None and os.path.isfile(cls.output_logfile):
|
|
166
|
-
with open(cls.output_logfile, 'a') as f:
|
|
167
|
-
f.write(" "*indent + msg)
|
|
168
|
-
return
|
|
169
|
-
|
|
170
|
-
@classmethod
|
|
171
|
-
def log_warning(cls, msg, indent=0):
|
|
172
|
-
"""
|
|
173
|
-
Write warning message to console/file output.
|
|
174
|
-
|
|
175
|
-
**Arguments:**
|
|
176
|
-
|
|
177
|
-
* ``msg``
|
|
178
|
-
|
|
179
|
-
/ *Condition*: required / *Type*: str /
|
|
180
|
-
|
|
181
|
-
Warning message which is written to output.
|
|
182
|
-
|
|
183
|
-
* ``indent``
|
|
184
|
-
|
|
185
|
-
/ *Condition*: optional / *Type*: int / *Default*: 0 /
|
|
186
|
-
|
|
187
|
-
Offset indent.
|
|
188
|
-
|
|
189
|
-
**Returns:**
|
|
190
|
-
|
|
191
|
-
(*no returns*)
|
|
192
|
-
"""
|
|
193
|
-
cls.log(cls.prefix_warn+str(msg), cls.color_warn, indent)
|
|
194
|
-
|
|
195
|
-
@classmethod
|
|
196
|
-
def log_error(cls, msg, fatal_error=False, indent=0):
|
|
197
|
-
"""
|
|
198
|
-
Write error message to console/file output.
|
|
199
|
-
|
|
200
|
-
* ``msg``
|
|
201
|
-
|
|
202
|
-
/ *Condition*: required / *Type*: str /
|
|
203
|
-
|
|
204
|
-
Error message which is written to output.
|
|
205
|
-
|
|
206
|
-
* ``fatal_error``
|
|
207
|
-
|
|
208
|
-
/ *Condition*: optional / *Type*: bool / *Default*: False /
|
|
209
|
-
|
|
210
|
-
If set, tool will terminate after logging error message.
|
|
211
|
-
|
|
212
|
-
* ``indent``
|
|
213
|
-
|
|
214
|
-
/ *Condition*: optional / *Type*: int / *Default*: 0 /
|
|
215
|
-
|
|
216
|
-
Offset indent.
|
|
217
|
-
|
|
218
|
-
**Returns:**
|
|
219
|
-
|
|
220
|
-
(*no returns*)
|
|
221
|
-
"""
|
|
222
|
-
prefix = cls.prefix_error
|
|
223
|
-
if fatal_error:
|
|
224
|
-
prefix = cls.prefix_fatalerror
|
|
225
|
-
|
|
226
|
-
cls.log(prefix+str(msg), cls.color_error, indent)
|
|
227
|
-
if fatal_error:
|
|
228
|
-
cls.log(f"{sys.argv[0]} has been stopped!", cls.color_error)
|
|
229
|
-
exit(1)
|
|
230
|
-
|
|
231
76
|
|
|
232
77
|
def get_from_tags(lTags, reInfo):
|
|
233
78
|
"""
|
|
@@ -380,7 +225,7 @@ Default schema supports below information:
|
|
|
380
225
|
# TESTCASE_NAME is not available for non-testcase relevant resources
|
|
381
226
|
# TESTSUITE_NAME is not available for `buildrecord` and `configuration` resources
|
|
382
227
|
# Warning user for using wrong place holders
|
|
383
|
-
oMatch = re.search(".*\{(.*)\}.*", dConfig[key])
|
|
228
|
+
oMatch = re.search(r".*\{(.*)\}.*", dConfig[key])
|
|
384
229
|
if oMatch:
|
|
385
230
|
if oMatch.group(1) not in CRQMClient.SUPPORTED_PLACEHOLDER:
|
|
386
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
ADDED
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
# Copyright 2020-2023 Robert Bosch GmbH
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
# ******************************************************************************
|
|
15
|
+
#
|
|
16
|
+
# File: rqmtool.py
|
|
17
|
+
#
|
|
18
|
+
# Initially created by Tran Duy Ngoan(RBVH/EMC51) / October 2025
|
|
19
|
+
#
|
|
20
|
+
# This tool is used to fetch RQM resources
|
|
21
|
+
#
|
|
22
|
+
# History:
|
|
23
|
+
#
|
|
24
|
+
# 2025-10-20:
|
|
25
|
+
# - initial version
|
|
26
|
+
#
|
|
27
|
+
# ******************************************************************************
|
|
28
|
+
|
|
29
|
+
import argparse
|
|
30
|
+
import os
|
|
31
|
+
import csv
|
|
32
|
+
import json
|
|
33
|
+
|
|
34
|
+
from RobotLog2RQM.CRQM import CRQMClient
|
|
35
|
+
from RobotLog2RQM.logger import Logger
|
|
36
|
+
from RobotLog2RQM.version import VERSION, VERSION_DATE
|
|
37
|
+
|
|
38
|
+
OUTPUT_FORMATS = ['json', 'csv']
|
|
39
|
+
ARTIFACT_TYPES = ['testcase', 'testsuite']
|
|
40
|
+
|
|
41
|
+
def __process_commandline():
|
|
42
|
+
"""
|
|
43
|
+
Process provided argument(s) from command line.
|
|
44
|
+
|
|
45
|
+
**Arguments:**
|
|
46
|
+
|
|
47
|
+
(*no arguments*)
|
|
48
|
+
|
|
49
|
+
**Returns:**
|
|
50
|
+
|
|
51
|
+
/ *Type*: `ArgumentParser` object /
|
|
52
|
+
|
|
53
|
+
ArgumentParser object.
|
|
54
|
+
"""
|
|
55
|
+
parser = argparse.ArgumentParser(
|
|
56
|
+
prog="RQMTool",
|
|
57
|
+
description="Fetch RQM resources."
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
parser.add_argument(
|
|
61
|
+
'-v', '--version',
|
|
62
|
+
action='version',
|
|
63
|
+
version=f'v{VERSION} ({VERSION_DATE})',
|
|
64
|
+
help='Version of the RQMTool.'
|
|
65
|
+
)
|
|
66
|
+
parser.add_argument(
|
|
67
|
+
"--host",
|
|
68
|
+
required=True,
|
|
69
|
+
help="RQM server URL."
|
|
70
|
+
)
|
|
71
|
+
parser.add_argument(
|
|
72
|
+
"--project",
|
|
73
|
+
required=True,
|
|
74
|
+
help="RQM project."
|
|
75
|
+
)
|
|
76
|
+
parser.add_argument(
|
|
77
|
+
"--user",
|
|
78
|
+
required=True,
|
|
79
|
+
help="RQM username."
|
|
80
|
+
)
|
|
81
|
+
parser.add_argument(
|
|
82
|
+
"--password",
|
|
83
|
+
required=True,
|
|
84
|
+
help="RQM password."
|
|
85
|
+
)
|
|
86
|
+
# exclusive group to provdve only --testsuite or --testplan
|
|
87
|
+
group = parser.add_mutually_exclusive_group(required=True)
|
|
88
|
+
group.add_argument(
|
|
89
|
+
"--testplan",
|
|
90
|
+
help="RQM testplan ID."
|
|
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
|
+
)
|
|
106
|
+
parser.add_argument(
|
|
107
|
+
"--dryrun",
|
|
108
|
+
action="store_true",
|
|
109
|
+
help='if set, then verify all input arguments (includes RQM authentication) and show what would be done.')
|
|
110
|
+
parser.add_argument(
|
|
111
|
+
"--format",
|
|
112
|
+
default="csv",
|
|
113
|
+
choices=OUTPUT_FORMATS,
|
|
114
|
+
help="Output format (csv or json). Default is csv.")
|
|
115
|
+
parser.add_argument(
|
|
116
|
+
"--types",
|
|
117
|
+
default="testcase,testsuite",
|
|
118
|
+
help="Comma-separated list of artifact types to fetch. Allowed: testcase, testsuite.")
|
|
119
|
+
parser.add_argument(
|
|
120
|
+
"--output-dir",
|
|
121
|
+
default=".",
|
|
122
|
+
help="Directory to save output files.")
|
|
123
|
+
parser.add_argument(
|
|
124
|
+
"--basename",
|
|
125
|
+
default="testplan_export",
|
|
126
|
+
help="Base name for output files.")
|
|
127
|
+
return parser.parse_args()
|
|
128
|
+
|
|
129
|
+
def __validate_arguments(arguments):
|
|
130
|
+
"""
|
|
131
|
+
Validate and normalize command line arguments.
|
|
132
|
+
|
|
133
|
+
**Arguments:**
|
|
134
|
+
|
|
135
|
+
* ``arguments``
|
|
136
|
+
|
|
137
|
+
/ *Condition*: required / *Type*: `ArgumentParser` object /
|
|
138
|
+
|
|
139
|
+
Parsed arguments from __process_commandline().
|
|
140
|
+
|
|
141
|
+
**Returns:**
|
|
142
|
+
|
|
143
|
+
* ``arguments``
|
|
144
|
+
|
|
145
|
+
/ *Type*: `ArgumentParser` object /
|
|
146
|
+
|
|
147
|
+
ArgumentParser object.
|
|
148
|
+
"""
|
|
149
|
+
artifact_types = arguments.types
|
|
150
|
+
if isinstance(artifact_types, str):
|
|
151
|
+
artifact_types = [x.strip().lower() for x in artifact_types.split(',')]
|
|
152
|
+
elif isinstance(artifact_types, (list, tuple)):
|
|
153
|
+
artifact_types = [x.lower() for x in artifact_types]
|
|
154
|
+
else:
|
|
155
|
+
raise TypeError(f"Invalid type for '--types': {type(artifact_types).__name__}, expected str or list.")
|
|
156
|
+
|
|
157
|
+
for t in artifact_types:
|
|
158
|
+
if t not in ARTIFACT_TYPES:
|
|
159
|
+
raise ValueError(
|
|
160
|
+
f"Invalid artifact type '{t}'. Allowed: {', '.join(ARTIFACT_TYPES)}."
|
|
161
|
+
)
|
|
162
|
+
arguments.types = artifact_types
|
|
163
|
+
|
|
164
|
+
return arguments
|
|
165
|
+
|
|
166
|
+
def write_json_file(file_name, data):
|
|
167
|
+
"""
|
|
168
|
+
Write data to a JSON file.
|
|
169
|
+
|
|
170
|
+
**Arguments:**
|
|
171
|
+
|
|
172
|
+
* ``file_name``
|
|
173
|
+
|
|
174
|
+
/ *Condition*: required / *Type*: str /
|
|
175
|
+
|
|
176
|
+
Path of the JSON file to write.
|
|
177
|
+
|
|
178
|
+
* ``data``
|
|
179
|
+
|
|
180
|
+
/ *Condition*: required / *Type*: dict /
|
|
181
|
+
|
|
182
|
+
Data to export.
|
|
183
|
+
|
|
184
|
+
**Returns:**
|
|
185
|
+
|
|
186
|
+
(*no returns*)
|
|
187
|
+
"""
|
|
188
|
+
with open(file_name, 'w', encoding='utf-8') as f:
|
|
189
|
+
json.dump(data, f, indent=2, ensure_ascii=False)
|
|
190
|
+
Logger.log(f"Exported data to: {file_name}")
|
|
191
|
+
|
|
192
|
+
def write_csv_file(file_name, data, artifact_type):
|
|
193
|
+
"""
|
|
194
|
+
Write data to a CSV file for a specific artifact type.
|
|
195
|
+
|
|
196
|
+
**Arguments:**
|
|
197
|
+
|
|
198
|
+
* ``file_name``
|
|
199
|
+
|
|
200
|
+
/ *Condition*: required / *Type*: str /
|
|
201
|
+
|
|
202
|
+
Path of the CSV file to write.
|
|
203
|
+
|
|
204
|
+
* ``data``
|
|
205
|
+
|
|
206
|
+
/ *Condition*: required / *Type*: dict /
|
|
207
|
+
|
|
208
|
+
Data dictionary containing artifacts.
|
|
209
|
+
|
|
210
|
+
* ``artifact_type``
|
|
211
|
+
|
|
212
|
+
/ *Condition*: required / *Type*: str /
|
|
213
|
+
|
|
214
|
+
Artifact type (`testcase` or `testsuite`) to export.
|
|
215
|
+
|
|
216
|
+
**Returns:**
|
|
217
|
+
|
|
218
|
+
(*no returns*)
|
|
219
|
+
"""
|
|
220
|
+
artifact_data = data.get(artifact_type, [])
|
|
221
|
+
if not artifact_data:
|
|
222
|
+
Logger.log_warning(f"No data for '{artifact_type}', skipping CSV export.")
|
|
223
|
+
return
|
|
224
|
+
|
|
225
|
+
fieldnames = list(data[artifact_type][0].keys()) if data[artifact_type] else ["id", "name"]
|
|
226
|
+
with open(file_name, mode="w", newline='', encoding="utf-8") as f:
|
|
227
|
+
writer = csv.DictWriter(f, fieldnames=fieldnames)
|
|
228
|
+
writer.writeheader()
|
|
229
|
+
for row in data[artifact_type]:
|
|
230
|
+
writer.writerow(row)
|
|
231
|
+
Logger.log(f"Exported {artifact_type} to: {file_name}")
|
|
232
|
+
|
|
233
|
+
def write_output_file(data, output_dir=".", basename="testplan_export", extension="csv", artifact_types=None):
|
|
234
|
+
"""
|
|
235
|
+
Write data to output files (JSON or CSV) according to specified options.
|
|
236
|
+
|
|
237
|
+
**Arguments:**
|
|
238
|
+
|
|
239
|
+
* ``data``
|
|
240
|
+
|
|
241
|
+
/ *Condition*: required / *Type*: dict /
|
|
242
|
+
|
|
243
|
+
Data dictionary containing artifacts.
|
|
244
|
+
|
|
245
|
+
* ``output_dir``
|
|
246
|
+
|
|
247
|
+
/ *Condition*: optional / *Type*: str /
|
|
248
|
+
|
|
249
|
+
Directory to save output files. Default is current directory.
|
|
250
|
+
|
|
251
|
+
* ``basename``
|
|
252
|
+
|
|
253
|
+
/ *Condition*: optional / *Type*: str /
|
|
254
|
+
|
|
255
|
+
Base name for output files. Default is "testplan_export".
|
|
256
|
+
|
|
257
|
+
* ``extension``
|
|
258
|
+
|
|
259
|
+
/ *Condition*: optional / *Type*: str /
|
|
260
|
+
|
|
261
|
+
Output format: `json` or `csv`. Default is `csv`.
|
|
262
|
+
|
|
263
|
+
* ``artifact_types``
|
|
264
|
+
|
|
265
|
+
/ *Condition*: optional / *Type*: list /
|
|
266
|
+
|
|
267
|
+
Artifact types to export. Default is all supported types.
|
|
268
|
+
|
|
269
|
+
**Returns:**
|
|
270
|
+
|
|
271
|
+
(*no returns*)
|
|
272
|
+
"""
|
|
273
|
+
if extension == 'json':
|
|
274
|
+
file_name = os.path.join(output_dir, f"{basename}.json")
|
|
275
|
+
write_json_file(file_name, data)
|
|
276
|
+
else:
|
|
277
|
+
# CSV export: one file per artifact_type
|
|
278
|
+
for artifact_type in (artifact_types or ARTIFACT_TYPES):
|
|
279
|
+
file_name = os.path.join(output_dir, f"{basename}_{artifact_type}s.csv")
|
|
280
|
+
write_csv_file(file_name, data, artifact_type)
|
|
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
|
+
|
|
332
|
+
def RQMTool():
|
|
333
|
+
"""
|
|
334
|
+
Main entry point for RQMTool CLI.
|
|
335
|
+
|
|
336
|
+
**Arguments:**
|
|
337
|
+
|
|
338
|
+
(*no arguments*)
|
|
339
|
+
|
|
340
|
+
**Returns:**
|
|
341
|
+
|
|
342
|
+
(*no returns*)
|
|
343
|
+
"""
|
|
344
|
+
args = __process_commandline()
|
|
345
|
+
__validate_arguments(args)
|
|
346
|
+
Logger.config(dryrun=args.dryrun)
|
|
347
|
+
|
|
348
|
+
RQMClient = CRQMClient(args.user, args.password, args.project, args.host)
|
|
349
|
+
try:
|
|
350
|
+
bSuccess = RQMClient.login()
|
|
351
|
+
RQMClient.config(stream=args.stream, baseline=args.baseline)
|
|
352
|
+
if bSuccess:
|
|
353
|
+
Logger.log()
|
|
354
|
+
Logger.log(f"Login RQM as user '{args.user}' successfully!")
|
|
355
|
+
else:
|
|
356
|
+
Logger.log_error("Could not login to RQM: 'Unknown reason'.")
|
|
357
|
+
except Exception as reason:
|
|
358
|
+
Logger.log_error(f"Could not login to RQM: '{str(reason)}'.")
|
|
359
|
+
|
|
360
|
+
if not args.dryrun:
|
|
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)
|
|
374
|
+
|
|
375
|
+
write_output_file(
|
|
376
|
+
test_data,
|
|
377
|
+
output_dir=args.output_dir,
|
|
378
|
+
basename=basename_with_id,
|
|
379
|
+
extension=args.format,
|
|
380
|
+
artifact_types=artifact_types
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
for artifact_type in artifact_types:
|
|
384
|
+
items = test_data.get(artifact_type, [])
|
|
385
|
+
Logger.log(f"Found {len(items)} {artifact_type}(s)")
|
|
386
|
+
cnt = 1
|
|
387
|
+
for item in items:
|
|
388
|
+
Logger.log(f"{cnt:>3}. {item['id']} - {item['name']}", indent=2)
|
|
389
|
+
cnt += 1
|
|
390
|
+
|
|
391
|
+
if __name__ == "__main__":
|
|
392
|
+
RQMTool()
|
RobotLog2RQM/version.py
CHANGED
{robotframework_robotlog2rqm-1.4.2.dist-info → robotframework_robotlog2rqm-1.6.0.dist-info}/METADATA
RENAMED
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: robotframework-robotlog2rqm
|
|
3
|
-
Version: 1.
|
|
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
|
|
|
@@ -129,15 +137,15 @@ Use below command to get tools\'s usage:
|
|
|
129
137
|
|
|
130
138
|
The usage should be showed as below:
|
|
131
139
|
|
|
132
|
-
usage: RobotLog2RQM (RobotXMLResult to RQM importer) [-h] [-v] [--testsuite TESTSUITE] [--recursive]
|
|
133
|
-
[--createmissing] [--updatetestcase] [--dryrun] [--stream STREAM] [--baseline BASELINE]
|
|
140
|
+
usage: RobotLog2RQM (RobotXMLResult to RQM importer) [-h] [-v] [--testsuite TESTSUITE] [--recursive]
|
|
141
|
+
[--createmissing] [--updatetestcase] [--dryrun] [--stream STREAM] [--baseline BASELINE]
|
|
134
142
|
resultxmlfile host project user password testplan
|
|
135
143
|
|
|
136
|
-
RobotLog2RQM imports XML result files (default: output.xml) generated by the
|
|
144
|
+
RobotLog2RQM imports XML result files (default: output.xml) generated by the
|
|
137
145
|
Robot Framework into an IBM Rational Quality Manager.
|
|
138
146
|
|
|
139
147
|
positional arguments:
|
|
140
|
-
resultxmlfile absolute or relative path to the xml result file
|
|
148
|
+
resultxmlfile absolute or relative path to the xml result file
|
|
141
149
|
or directory of result files to be imported.
|
|
142
150
|
host RQM host url.
|
|
143
151
|
project project on RQM.
|
|
@@ -149,19 +157,19 @@ The usage should be showed as below:
|
|
|
149
157
|
-h, --help show this help message and exit
|
|
150
158
|
-v, --version Version of the RobotLog2RQM importer.
|
|
151
159
|
--testsuite TESTSUITE
|
|
152
|
-
testsuite ID for this execution. If 'new', then create a new
|
|
160
|
+
testsuite ID for this execution. If 'new', then create a new
|
|
153
161
|
testsuite for this execution.
|
|
154
|
-
--recursive if set, then the path is searched recursively for
|
|
162
|
+
--recursive if set, then the path is searched recursively for
|
|
155
163
|
log files to be imported.
|
|
156
|
-
--createmissing if set, then all testcases without tcid are created
|
|
164
|
+
--createmissing if set, then all testcases without tcid are created
|
|
157
165
|
when importing.
|
|
158
|
-
--updatetestcase if set, then testcase information on RQM will be updated
|
|
166
|
+
--updatetestcase if set, then testcase information on RQM will be updated
|
|
159
167
|
bases on robot testfile.
|
|
160
|
-
--dryrun if set, then verify all input arguments
|
|
168
|
+
--dryrun if set, then verify all input arguments
|
|
161
169
|
(includes RQM authentication) and show what would be done.
|
|
162
|
-
--stream STREAM project stream. Note, requires Configuration Management (CM)
|
|
170
|
+
--stream STREAM project stream. Note, requires Configuration Management (CM)
|
|
163
171
|
to be enabled for the project area.
|
|
164
|
-
--baseline BASELINE project baseline. Note, requires Configuration Management (CM),
|
|
172
|
+
--baseline BASELINE project baseline. Note, requires Configuration Management (CM),
|
|
165
173
|
or Baselines Only to be enabled for the project area.
|
|
166
174
|
|
|
167
175
|
The below command is simple usage witth all required arguments to import
|
|
@@ -213,6 +221,58 @@ Then, open RQM with your favourite browser and you will see that the
|
|
|
213
221
|
test case execution records and their results are imported in the given
|
|
214
222
|
testplan ID.
|
|
215
223
|
|
|
224
|
+
# RQMTool Submodule
|
|
225
|
+
|
|
226
|
+
The package now includes a submodule **RQMTool** which provides a
|
|
227
|
+
standalone CLI to fetch test cases and test suites from IBM RQM test
|
|
228
|
+
plans.
|
|
229
|
+
|
|
230
|
+
RQMTool is accessible as a Python module:
|
|
231
|
+
|
|
232
|
+
python -m RobotLog2RQM.rqmtool --host <RQM_SERVER_URL> --project <PROJECT_AREA> \
|
|
233
|
+
--user <USERNAME> --password <PASSWORD> \
|
|
234
|
+
--testplan <TESTPLAN_ID> [--types <artifact_types>] \
|
|
235
|
+
[--format <csv|json>] [--output-dir <DIR>] \
|
|
236
|
+
[--basename <BASENAME>] [--dryrun]
|
|
237
|
+
|
|
238
|
+
Main features:
|
|
239
|
+
|
|
240
|
+
- Fetches selected artifact types ([testcase]{.title-ref} or
|
|
241
|
+
[testsuite]{.title-ref}) from a given test plan.
|
|
242
|
+
- Supports CSV or JSON export, with automatic filenames including the
|
|
243
|
+
test plan ID.
|
|
244
|
+
|
|
245
|
+
## How to use RQMTool
|
|
246
|
+
|
|
247
|
+
RQMTool requires RQM information (host URL, project, credentials) and a
|
|
248
|
+
test plan ID. Use the [-h]{.title-ref} argument to see full usage:
|
|
249
|
+
|
|
250
|
+
python -m RobotLog2RQM.rqmtool -h
|
|
251
|
+
|
|
252
|
+
This will display all available command line options for RQMTool, such
|
|
253
|
+
as:
|
|
254
|
+
|
|
255
|
+
- [\--types]{.title-ref} : artifact types to fetch
|
|
256
|
+
([testcase]{.title-ref}, [testsuite]{.title-ref}). Default: both.
|
|
257
|
+
- \`\--format\`: output format, either [csv]{.title-ref} or
|
|
258
|
+
[json]{.title-ref}.
|
|
259
|
+
- \`\--output-dir\`: directory to save exported files.
|
|
260
|
+
- \`\--basename\`: base name for output files.
|
|
261
|
+
- \`\--dryrun\`: validate inputs and RQM login without fetching data.
|
|
262
|
+
|
|
263
|
+
## Example
|
|
264
|
+
|
|
265
|
+
Fetch all test cases and test suites from test plan ID 720 and export to
|
|
266
|
+
CSV:
|
|
267
|
+
|
|
268
|
+
python -m RobotLog2RQM.rqmtool --host https://sample-rqm-host.com \
|
|
269
|
+
--project ROBFW-AIO \
|
|
270
|
+
--user test_user \
|
|
271
|
+
--password test_pw \
|
|
272
|
+
--testplan 720 \
|
|
273
|
+
--types testcase,testsuite \
|
|
274
|
+
--format csv
|
|
275
|
+
|
|
216
276
|
### Sourcecode Documentation
|
|
217
277
|
|
|
218
278
|
To understand more detail about the tool\'s features and how Robot test
|
|
@@ -260,5 +320,3 @@ distributed under the License is distributed on an \"AS IS\" BASIS,
|
|
|
260
320
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
261
321
|
See the License for the specific language governing permissions and
|
|
262
322
|
limitations under the License.
|
|
263
|
-
|
|
264
|
-
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
RobotLog2RQM/CRQM.py,sha256=NxFbIKvTOepkdNmv0V9aQCaU2jSGhuHO707XTglMF_A,69357
|
|
2
|
+
RobotLog2RQM/RobotLog2RQM.pdf,sha256=--VWUBLqZOtfQXy9O8N28MvOT7VMRPg9fSAQXl8coK0,358016
|
|
3
|
+
RobotLog2RQM/__init__.py,sha256=YKDTJjDsnQkr5X-gjjO8opwKUVKm6kc8sIUpURYMk48,596
|
|
4
|
+
RobotLog2RQM/__main__.py,sha256=JabttEncy80antJWeGVmjoXyiF1DyXxkxdW4xLuHzT0,681
|
|
5
|
+
RobotLog2RQM/logger.py,sha256=FnZZqssOcrF5yK6wxkgifScoQcFGrqrvzLvXWY3V-9A,4252
|
|
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
|
+
RobotLog2RQM/RQM_templates/buildrecord.xml,sha256=uGot7pNOjPR8do0JsJi0Lz3OCU9NMhODRd428QgvHh4,1498
|
|
10
|
+
RobotLog2RQM/RQM_templates/configuration.xml,sha256=NrFDv51fuGhgeMiZuRhQ5q_UJ0u_pWzdxisIF5AJs74,1378
|
|
11
|
+
RobotLog2RQM/RQM_templates/executionresult.xml,sha256=WTp4qDk29peBc0ll6GHVXX_kF_YBsOVjy9vBzoz7_2k,2160
|
|
12
|
+
RobotLog2RQM/RQM_templates/executionworkitem.xml,sha256=3GYO-nvXcG-HDQZnDzGwYZSQOWAUNckT3_GUa8Ze2Eo,1511
|
|
13
|
+
RobotLog2RQM/RQM_templates/suiteexecutionrecord.xml,sha256=9GAs2WqZMkFJSNEZULm9BJvQy02dl_2JMecpQPHGTPA,1389
|
|
14
|
+
RobotLog2RQM/RQM_templates/testcase.xml,sha256=zovFKj-37QHn2S8mMA_9RnAJ3zBmDJkJj31yelsnFFI,2167
|
|
15
|
+
RobotLog2RQM/RQM_templates/testsuite.xml,sha256=r2ijEsyPoE7qzCtUxgIHDOEcqUveDN4SMf9HSE9b0ZU,1326
|
|
16
|
+
RobotLog2RQM/RQM_templates/testsuitelog.xml,sha256=l-NlaCyk6Ben76PElXKOHfMlEvyQ-e9MOZ6-F9HvwDQ,1920
|
|
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,20 +0,0 @@
|
|
|
1
|
-
RobotLog2RQM/CRQM.py,sha256=sL7IsFOhzRVvV2n_g2PFDjL3Ej8YChUl3PDTv3LCIN0,64363
|
|
2
|
-
RobotLog2RQM/RobotLog2RQM.pdf,sha256=utfSCdhRtw6mtljc-WMaG4GqRn7yggyZNcOeCota40Y,337295
|
|
3
|
-
RobotLog2RQM/__init__.py,sha256=YKDTJjDsnQkr5X-gjjO8opwKUVKm6kc8sIUpURYMk48,596
|
|
4
|
-
RobotLog2RQM/__main__.py,sha256=JabttEncy80antJWeGVmjoXyiF1DyXxkxdW4xLuHzT0,681
|
|
5
|
-
RobotLog2RQM/robotlog2rqm.py,sha256=Q35lrrDDv3skXJO6IH9hRRoU0Ua0kSKOuBdpNeF_v5s,33531
|
|
6
|
-
RobotLog2RQM/version.py,sha256=XS3UlCLUKUcaTFjKhJ6Pj4bN04WACmWEz7cmWTadZ1A,916
|
|
7
|
-
RobotLog2RQM/RQM_templates/buildrecord.xml,sha256=uGot7pNOjPR8do0JsJi0Lz3OCU9NMhODRd428QgvHh4,1498
|
|
8
|
-
RobotLog2RQM/RQM_templates/configuration.xml,sha256=NrFDv51fuGhgeMiZuRhQ5q_UJ0u_pWzdxisIF5AJs74,1378
|
|
9
|
-
RobotLog2RQM/RQM_templates/executionresult.xml,sha256=WTp4qDk29peBc0ll6GHVXX_kF_YBsOVjy9vBzoz7_2k,2160
|
|
10
|
-
RobotLog2RQM/RQM_templates/executionworkitem.xml,sha256=3GYO-nvXcG-HDQZnDzGwYZSQOWAUNckT3_GUa8Ze2Eo,1511
|
|
11
|
-
RobotLog2RQM/RQM_templates/suiteexecutionrecord.xml,sha256=9GAs2WqZMkFJSNEZULm9BJvQy02dl_2JMecpQPHGTPA,1389
|
|
12
|
-
RobotLog2RQM/RQM_templates/testcase.xml,sha256=zovFKj-37QHn2S8mMA_9RnAJ3zBmDJkJj31yelsnFFI,2167
|
|
13
|
-
RobotLog2RQM/RQM_templates/testsuite.xml,sha256=r2ijEsyPoE7qzCtUxgIHDOEcqUveDN4SMf9HSE9b0ZU,1326
|
|
14
|
-
RobotLog2RQM/RQM_templates/testsuitelog.xml,sha256=l-NlaCyk6Ben76PElXKOHfMlEvyQ-e9MOZ6-F9HvwDQ,1920
|
|
15
|
-
robotframework_robotlog2rqm-1.4.2.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
16
|
-
robotframework_robotlog2rqm-1.4.2.dist-info/METADATA,sha256=JEeig_C3ceOdSoP9UauelzVEHm6l6S7gvd8S6Us8zO4,9863
|
|
17
|
-
robotframework_robotlog2rqm-1.4.2.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
|
|
18
|
-
robotframework_robotlog2rqm-1.4.2.dist-info/entry_points.txt,sha256=-Xug2kDJW2LtcSADEVPtCwa337twCy2iGh5aK7xApHA,73
|
|
19
|
-
robotframework_robotlog2rqm-1.4.2.dist-info/top_level.txt,sha256=jb_Gt6W44FoOLtGfBe7RzqCLaquhihkEWvSI1zjXDHc,13
|
|
20
|
-
robotframework_robotlog2rqm-1.4.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|