Skip to content

Commit f2348e9

Browse files
authored
Merge pull request #23 from Brian-Williams/add-service-methods
Add service methods and TestReporter
2 parents 0b843e3 + 9470568 commit f2348e9

File tree

6 files changed

+261
-6
lines changed

6 files changed

+261
-6
lines changed

Diff for: README.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ how existing code can be adapted
9292
TestLink-API-Python-client developers
9393
-------------------------------------
9494
* `James Stock`_, `Olivier Renault`_, `lczub`_, `manojklm`_ (PY3)
95-
* `g4l4drim`_, `pade`_, `anton-matosov`_, `citizen-stig`_, `charz`_, `Maberi`_
95+
* `g4l4drim`_, `pade`_, `anton-matosov`_, `citizen-stig`_, `charz`_, `Maberi`_, `Brian-Williams`_
9696
* anyone forgotten?
9797

9898
.. _Apache License 2.0: http://www.apache.org/licenses/LICENSE-2.0

Diff for: setup.py

-1
Original file line numberDiff line numberDiff line change
@@ -87,4 +87,3 @@
8787
keywords = ['testing', 'testlink', 'xml-rpc', 'testautomation']
8888

8989
)
90-

Diff for: src/testlink/__init__.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,5 @@
2121
from .testlinkerrors import TestLinkError
2222
from .testlinkapigeneric import TestlinkAPIGeneric
2323
from .testlinkapi import TestlinkAPIClient
24-
from .testlinkhelper import TestLinkHelper
24+
from .testlinkhelper import TestLinkHelper
25+
from .testreporter import TestGenReporter

Diff for: src/testlink/testlinkapi.py

+18-2
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,23 @@ def copyTCnewTestCase(self, origTestCaseId, origVersion=None, **changedAttribute
212212
return self._copyTC(origTestCaseId, changedAttributes, origVersion,
213213
duplicateaction = 'generate_new')
214214

215-
215+
def getTestCaseByVersion(self, testCaseID, version=None):
216+
"""
217+
Gets testcase information based on the version.
218+
219+
:param testCaseID: test case to search for
220+
:param version: version to search for defaults to None. None searches for latest
221+
:return: test case info dictionary
222+
"""
223+
testcases = self.getTestCase(testCaseID, version=version)
224+
if version is None:
225+
return testcases[0]
226+
for testcase in testcases:
227+
if str(testcase['version']) == str(version):
228+
return testcase
229+
else:
230+
raise RuntimeError("Testcase {} doesn't have version {}.".format(testCaseID, version))
231+
216232
def _copyTC(self, origTestCaseId, changedArgs, origVersion=None, **options):
217233
""" creates a copy of test case with id ORIGTESTCASEID
218234
@@ -237,7 +253,7 @@ def _copyTC(self, origTestCaseId, changedArgs, origVersion=None, **options):
237253
"""
238254

239255
# get orig test case content
240-
origArgItems = self.getTestCase(origTestCaseId, version=origVersion)[0]
256+
origArgItems = self.getTestCaseByVersion(origTestCaseId, version=origVersion)
241257
# get orig test case project id
242258
origArgItems['testprojectid'] = self.getProjectIDByNode(origTestCaseId)
243259

Diff for: src/testlink/testreporter.py

+240
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
from testlink.testlinkerrors import TLResponseError
2+
3+
4+
class TestReporter(dict):
5+
def __init__(self, tls, testcases, *args, **kwargs):
6+
"""This can be given one or more testcases, but they all must have the same project, plan, and platform."""
7+
super(TestReporter, self).__init__(*args, **kwargs)
8+
self.tls = tls
9+
# handle single testcase
10+
self.testcases = testcases if isinstance(testcases, list) else [testcases]
11+
self._plan_testcases = None
12+
self.remove_non_report_kwargs()
13+
self._platformname_generated = False
14+
15+
def remove_non_report_kwargs(self):
16+
self.buildname = self.pop('buildname')
17+
self.buildnotes = self.pop('buildnotes', "Created with automation.")
18+
19+
def setup_testlink(self):
20+
"""Call properties that may set report kwarg values."""
21+
self.testprojectname
22+
self.testprojectid
23+
self.testplanid
24+
self.testplanname
25+
self.platformname
26+
self.platformid
27+
self.buildid
28+
29+
def _get_project_name_by_id(self):
30+
if self.testprojectid:
31+
for project in self.tls.getProjects():
32+
if project['id'] == self.testprojectid:
33+
return project['name']
34+
35+
def _projectname_getter(self):
36+
if not self.get('testprojectname') and self.testprojectid:
37+
self['testprojectname'] = self._get_project_name_by_id()
38+
return self.get('testprojectname')
39+
40+
@property
41+
def testprojectname(self):
42+
return self._projectname_getter()
43+
44+
def _get_project_id(self):
45+
tpid = self.get('testprojectid')
46+
if not tpid and self.testprojectname:
47+
self['testprojectid'] = self.tls.getProjectIDByName(self['testprojectname'])
48+
return self['testprojectid']
49+
return tpid
50+
51+
def _get_project_id_or_none(self):
52+
project_id = self._get_project_id()
53+
# If not found the id will return as -1
54+
if project_id == -1:
55+
project_id = None
56+
return project_id
57+
58+
@property
59+
def testprojectid(self):
60+
self['testprojectid'] = self._get_project_id_or_none()
61+
return self.get('testprojectid')
62+
63+
@property
64+
def testplanid(self):
65+
return self.get('testplanid')
66+
67+
@property
68+
def testplanname(self):
69+
return self.get('testplanname')
70+
71+
@property
72+
def platformname(self):
73+
"""Return a platformname added to the testplan if there is one."""
74+
return self.get('platformname')
75+
76+
@property
77+
def platformid(self):
78+
return self.get('platformid')
79+
80+
@property
81+
def buildid(self):
82+
return self.get('buildid')
83+
84+
@property
85+
def plan_tcids(self):
86+
if not self._plan_testcases:
87+
self._plan_testcases = set()
88+
tc_dict = self.tls.getTestCasesForTestPlan(self.testplanid)
89+
try:
90+
for _, platform in tc_dict.items():
91+
for k, v in platform.items():
92+
self._plan_testcases.add(v['full_external_id'])
93+
except AttributeError:
94+
# getTestCasesForTestPlan returns an empty list instead of an empty dict
95+
pass
96+
return self._plan_testcases
97+
98+
def reportgen(self):
99+
"""For use if you need to look at the status returns of individual reporting."""
100+
self.setup_testlink()
101+
for testcase in self.testcases:
102+
yield self.tls.reportTCResult(testcaseexternalid=testcase, **self)
103+
104+
def report(self):
105+
for _ in self.reportgen():
106+
pass
107+
108+
109+
class AddTestReporter(TestReporter):
110+
"""Add testcase to testplan if not added."""
111+
def setup_testlink(self):
112+
super(AddTestReporter, self).setup_testlink()
113+
self.ensure_testcases_in_plan()
114+
115+
def ensure_testcases_in_plan(self):
116+
# Get the platformid if possible or else addition will fail
117+
self.platformid
118+
for testcase in self.testcases:
119+
# Can't check if testcase is in plan_tcids, because that won't work if it's there, but of the wrong platform
120+
try:
121+
self.tls.addTestCaseToTestPlan(
122+
self.testprojectid, self.testplanid, testcase, self.get_latest_tc_version(testcase),
123+
platformid=self.platformid
124+
)
125+
except TLResponseError as e:
126+
# Test Case version is already linked to Test Plan
127+
if e.code == 3045:
128+
pass
129+
else:
130+
raise
131+
132+
def get_latest_tc_version(self, testcaseexternalid):
133+
return int(self.tls.getTestCase(None, testcaseexternalid=testcaseexternalid)[0]['version'])
134+
135+
136+
class AddTestPlanReporter(TestReporter):
137+
@property
138+
def testplanid(self):
139+
if not self.get('testplanid'):
140+
try:
141+
self['testplanid'] = self.tls.getTestPlanByName(self.testprojectname, self.testplanname)[0]['id']
142+
except TLResponseError as e:
143+
# Name does not exist
144+
if e.code == 3033:
145+
self['testplanid'] = self.generate_testplanid()
146+
else:
147+
raise
148+
except TypeError:
149+
self['testplanid'] = self.generate_testplanid()
150+
return self['testplanid']
151+
152+
def generate_testplanid(self):
153+
"""This won't necessarily be able to create a testplanid. It requires a planname and projectname."""
154+
if 'testplanname' not in self:
155+
raise RuntimeError("Need testplanname to generate a testplan for results.")
156+
157+
tp = self.tls.createTestPlan(self['testplanname'], self.testprojectname)
158+
self['testplanid'] = tp[0]['id']
159+
return self['testplanid']
160+
161+
162+
class AddPlatformReporter(TestReporter):
163+
@property
164+
def platformname(self):
165+
"""Return a platformname added to the testplan if there is one."""
166+
pn_kwarg = self.get('platformname')
167+
if pn_kwarg and self._platformname_generated is False:
168+
# If we try to create platform and catch platform already exists error (12000) it sometimes duplicates a
169+
# platformname
170+
try:
171+
self.tls.addPlatformToTestPlan(self.testplanid, pn_kwarg)
172+
except TLResponseError as e:
173+
if int(e.code) == 235:
174+
self.tls.createPlatform(self.testprojectname, pn_kwarg)
175+
self.tls.addPlatformToTestPlan(self.testplanid, pn_kwarg)
176+
else:
177+
raise
178+
self._platformname_generated = True
179+
return pn_kwarg
180+
181+
@property
182+
def platformid(self):
183+
if not self.get('platformid'):
184+
self['platformid'] = self.getPlatformID(self.platformname)
185+
# This action is idempotent
186+
self.tls.addPlatformToTestPlan(self.testplanid, self.platformname)
187+
return self['platformid']
188+
189+
def getPlatformID(self, platformname, _firstrun=True):
190+
"""
191+
This is hardcoded for platformname to always be self.platformname
192+
"""
193+
platforms = self.tls.getTestPlanPlatforms(self.testplanid)
194+
for platform in platforms:
195+
if platform['name'] == platformname:
196+
return platform['id']
197+
# Platformname houses platform creation as platform creation w/o a name isn't possible
198+
if not self.platformname:
199+
raise RuntimeError(
200+
"Couldn't find platformid for {}.{}, "
201+
"please provide a platformname to generate.".format(self.testplanid, platformname)
202+
)
203+
if _firstrun is True:
204+
return self.getPlatformID(self.platformname, _firstrun=False)
205+
else:
206+
raise RuntimeError("PlatformID not found after generated from platformname '{}' "
207+
"in test plan {}.".format(self.platformname, self.testplanid))
208+
209+
210+
class AddBuildReporter(TestReporter):
211+
@property
212+
def buildid(self):
213+
bid = self.get('buildid')
214+
if not bid or bid not in self.tls.getBuildsForTestPlan(self.testplanid):
215+
self['buildid'] = self._generate_buildid()
216+
return self.get('buildid')
217+
218+
def _generate_buildid(self):
219+
r = self.tls.createBuild(self.testplanid, self.buildname, self.buildnotes)
220+
return r[0]['id']
221+
222+
223+
class TestGenReporter(AddTestReporter, AddBuildReporter, AddTestPlanReporter, AddPlatformReporter, TestReporter):
224+
"""This is the default generate everything it can version of test reporting.
225+
226+
If you don't want to generate one of these values you can 'roll your own' version of this class with only the
227+
needed features that you want to generate.
228+
229+
For example if you wanted to add platforms and/or tests to testplans, but didn't want to ever make a new testplan
230+
you could use a class like:
231+
`type('MyOrgTestGenReporter', (AddTestReporter, AddPlatformReporter, TestReporter), {})`
232+
233+
Example usage with fake testlink server test and a manual project.
234+
```
235+
tls = testlink.TestLinkHelper('https://testlink.corp.com/testlink/lib/api/xmlrpc/v1/xmlrpc.php',
236+
'devkeyabc123').connect(testlink.TestlinkAPIClient)
237+
tgr = TestGenReporter(tls, ['TEST-123'], testprojectname='MANUALLY_MADE_PROJECT', testplanname='generated',
238+
platformname='gend', buildname='8.fake', status='p')
239+
```
240+
"""

Diff for: src/testlink/version.py

-1
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,3 @@
1919

2020
VERSION = '0.6.4'
2121
TL_RELEASE = '1.9.16'
22-

0 commit comments

Comments
 (0)