Skip to content

Commit 8bf1561

Browse files
author
Luiko Czub
committed
server call methods shifted to generic API lczub#7
uploadExecutionAttachment
1 parent a24177a commit 8bf1561

10 files changed

+170
-73
lines changed

MANIFEST.in

+1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
include MANIFEST.in LICENSE-2.0.txt README.md
22
recursive-include src *.py
33
recursive-include example *.py
4+
recursive-include example *.png
45
recursive-include test *.py

example/PyGreat.png

2.2 KB
Loading

example/TestLinkExample.py

+23-4
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
#! /usr/bin/python
22
# -*- coding: UTF-8 -*-
3-
from compiler.pycodegen import TRY_FINALLY
4-
from symbol import except_clause
53

64
# Copyright 2011-2013 Olivier Renault, TestLink-API-Python-client developers
75
#
@@ -57,7 +55,7 @@
5755
"""
5856
from testlink import TestlinkAPIClient, TestLinkHelper
5957
from testlink.testlinkerrors import TLResponseError
60-
import sys
58+
import sys, os.path
6159

6260
# precondition a)
6361
# SERVER_URL and KEY are defined in environment
@@ -92,6 +90,10 @@
9290
NEWTESTCASE_B="TESTCASE_B"
9391
NEWBUILD="Build v0.4.5"
9492

93+
NEWATTACHMENT_PY= os.path.realpath(__file__)
94+
this_file_dirname=os.path.dirname(NEWATTACHMENT_PY)
95+
NEWATTACHMENT_PNG=os.path.join(this_file_dirname, 'PyGreat.png')
96+
9597
# -- Start CHANGE v0.4.5 --
9698
# if myTestLink.checkDevKey() != True:
9799
# print "Error with the devKey."
@@ -286,10 +288,27 @@
286288
newResult = myTestLink.reportTCResult(None, newTestPlanID, None, 'f', '', guess=True,
287289
testcaseexternalid=tc_aa_full_ext_id)
288290
print newResult
291+
newResultID_AA = newResult[0]['id']
289292
# TC_B passed, explicit build and some notes , TC identified with internal id
290293
newResult = myTestLink.reportTCResult(newTestCaseID_B, newTestPlanID, NEWBUILD,
291294
'p', 'first try')
292-
print newResult
295+
print newResult
296+
newResultID_B = newResult[0]['id']
297+
298+
# add this (text) file as Attachemnt to last execution of TC_B with
299+
# different filename 'MyPyExampleApiClient.py'
300+
a_file=open(NEWATTACHMENT_PY)
301+
newAttachment = myTestLink.uploadExecutionAttachment(a_file, newResultID_B,
302+
'Textfile Example', 'Text Attachment Example for a TestCase',
303+
filename='MyPyExampleApiClient.py')
304+
print newAttachment
305+
# add png file as Attachemnt to last execution of TC_AA
306+
# !Attention - on WINDOWS use binary mode for none text file
307+
# see http://docs.python.org/2/tutorial/inputoutput.html#reading-and-writing-files
308+
a_file=open(NEWATTACHMENT_PNG, mode='rb')
309+
newAttachment = myTestLink.uploadExecutionAttachment(a_file, newResultID_AA,
310+
'PNG Example', 'PNG Attachment Example for a TestCase')
311+
print newAttachment
293312

294313
print ""
295314
print "Number of Projects in TestLink: %s " % (myTestLink.countProjects(),)

example/TestLinkExampleGenericApi.py

+24-1
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@
4444
--- 5 automated test steps
4545
"""
4646
from testlink import TestlinkAPIGeneric, TestLinkHelper
47-
import sys
47+
from testlink.testlinkerrors import TLResponseError
48+
import sys, os.path
4849

4950
# precondition a)
5051
# SERVER_URL and KEY are defined in environment
@@ -79,6 +80,10 @@
7980
NEWTESTCASE_B="TESTCASE_B"
8081
NEWBUILD="Build v0.4.5"
8182

83+
NEWATTACHMENT_PY= os.path.realpath(__file__)
84+
this_file_dirname=os.path.dirname(NEWATTACHMENT_PY)
85+
NEWATTACHMENT_PNG=os.path.join(this_file_dirname, 'PyGreat.png')
86+
8287
id_cache={}
8388

8489
# example handling Response Error Codes
@@ -205,11 +210,29 @@
205210
newResult = myTestLink.reportTCResult(id_cache[NEWTESTPLAN], 'f', guess=True,
206211
testcaseexternalid=tc_aa_full_ext_id)
207212
print newResult
213+
newResultID_AA = newResult[0]['id']
208214
# TC_B passed, explicit build and some notes , TC identified with internal id
209215
newResult = myTestLink.reportTCResult(id_cache[NEWTESTPLAN], 'p',
210216
buildid=id_cache[NEWBUILD], testcaseid=id_cache[NEWTESTCASE_B],
211217
notes="first try")
212218
print newResult
219+
newResultID_B = newResult[0]['id']
220+
221+
# add this python file as Attachemnt to last execution of TC_B with
222+
# different filename 'MyPyExampleApiGeneric.py'
223+
a_file=open(NEWATTACHMENT_PY)
224+
newAttachment = myTestLink.uploadExecutionAttachment(a_file, newResultID_B,
225+
title='Textfile Example', description='Text Attachment Example for a TestCase',
226+
filename='MyPyExampleApiGeneric.py')
227+
print newAttachment
228+
# add png file as Attachemnt to last execution of TC_AA
229+
# !Attention - on WINDOWS use binary mode for none text file
230+
# see http://docs.python.org/2/tutorial/inputoutput.html#reading-and-writing-files
231+
a_file=open(NEWATTACHMENT_PNG, mode='rb')
232+
newAttachment = myTestLink.uploadExecutionAttachment(a_file, newResultID_AA,
233+
title='PNG Example', description='PNG Attachment Example for a TestCase')
234+
print newAttachment
235+
213236

214237
print ""
215238
print "Number of Projects in TestLink: %i " % len(myTestLink.getProjects())

src/testlink/testlinkapi.py

+35-23
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,17 @@ class TestlinkAPIClient(TestlinkAPIGeneric):
2828
__slots__ = ['stepsList']
2929

3030
def __init__(self, server_url, devKey):
31-
super(TestlinkAPIClient, self).__init__(server_url, devKey,
32-
transport=None, encoding=None, verbose=0, allow_none=1)
31+
""" call super for init generell slots, init sepcial slots for teststeps
32+
and define special positional arg settings """
33+
super(TestlinkAPIClient, self).__init__(server_url, devKey,
34+
allow_none=1)
35+
# allow_none is an argument from xmlrpclib.Server()
36+
# with set to True, it is possible to set postional args to None, so
37+
# alternative optional arguments could be set
38+
# example - testcaseid is set :
39+
# reportTCResult(None, newTestPlanID, None, 'f', '', guess=True,
40+
# testcaseexternalid=tc_aa_full_ext_id)
41+
# otherwise xmlrpclib raise an error, that None values are not allowed
3342
self.stepsList = []
3443
self._changePositionalArgConfig()
3544

@@ -49,6 +58,9 @@ def _changePositionalArgConfig(self):
4958
# reportTCResult
5059
pos_arg_config['reportTCResult'] = ['testcaseid', 'testplanid',
5160
'buildname', 'status', 'notes']
61+
# uploadExecutionAttachment
62+
pos_arg_config['uploadExecutionAttachment'] = ['executionid', 'title', 'description']
63+
5264
#
5365
# BUILT-IN API CALLS
5466
#
@@ -418,27 +430,27 @@ def createTestCase(self, *argsPositional, **argsOptional):
418430

419431

420432

421-
def uploadExecutionAttachment(self,attachmentfile,executionid,title,description):
422-
"""
423-
Attach a file to a test execution
424-
attachmentfile: python file descriptor pointing to the file
425-
name : name of the file
426-
title : title of the attachment
427-
description : description of the attachment
428-
content type : mimetype of the file
429-
"""
430-
import mimetypes
431-
import base64
432-
import os.path
433-
argsAPI={'devKey' : self.devKey,
434-
'executionid':executionid,
435-
'title':title,
436-
'filename':os.path.basename(attachmentfile.name),
437-
'description':description,
438-
'filetype':mimetypes.guess_type(attachmentfile.name)[0],
439-
'content':base64.encodestring(attachmentfile.read())
440-
}
441-
return self._callServer('uploadExecutionAttachment', argsAPI)
433+
# def uploadExecutionAttachment(self,attachmentfile,executionid,title,description):
434+
# """
435+
# Attach a file to a test execution
436+
# attachmentfile: python file descriptor pointing to the file
437+
# name : name of the file
438+
# title : title of the attachment
439+
# description : description of the attachment
440+
# content type : mimetype of the file
441+
# """
442+
# import mimetypes
443+
# import base64
444+
# import os.path
445+
# argsAPI={'devKey' : self.devKey,
446+
# 'executionid':executionid,
447+
# 'title':title,
448+
# 'filename':os.path.basename(attachmentfile.name),
449+
# 'description':description,
450+
# 'filetype':mimetypes.guess_type(attachmentfile.name)[0],
451+
# 'content':base64.encodestring(attachmentfile.read())
452+
# }
453+
# return self._callServer('uploadExecutionAttachment', argsAPI)
442454

443455
#
444456
# ADDITIONNAL FUNCTIONS

src/testlink/testlinkapigeneric.py

+62-28
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121

2222
from testlinkhelper import TestLinkHelper, VERSION
2323
import testlinkerrors
24-
from compiler.ast import TryExcept
24+
2525

2626

2727
# Default Definition which (python) API-Method expects which postional arguments
@@ -40,7 +40,8 @@
4040
'createTestSuite' : ['testprojectid', 'testsuitename', 'details'],
4141
'doesUserExist' : ['user'],
4242
'repeat' : ['str'],
43-
'reportTCResult' : ['testplanid', 'status']
43+
'reportTCResult' : ['testplanid', 'status'],
44+
'uploadExecutionAttachment' : ['executionid']
4445
}
4546

4647
# decorators for generic api calls
@@ -69,15 +70,33 @@ def wrapper(self, *argsPositional, **argsOptional):
6970
return methodAPI(self, *argsPositional, **argsOptional)
7071
return wrapper
7172

73+
def decoApiCallAddAttachment(methodAPI):
74+
""" Decorator to expand parameter list with devKey and attachmentfile
75+
attachmentfile is a python file descriptor pointing to the file
76+
"""
77+
def wrapper(self, attachmentfile, *argsPositional, **argsOptional):
78+
if not ('devKey' in argsOptional):
79+
argsOptional['devKey'] = self.devKey
80+
argsAttachment = self._getAttachmentArgs(attachmentfile)
81+
# add additional key/value pairs from argsOptional
82+
# although overwrites filename, filetype, content with user definition
83+
# if they exist
84+
argsAttachment.update(argsOptional)
85+
return methodAPI(self, *argsPositional, **argsAttachment)
86+
return wrapper
87+
7288

7389
class TestlinkAPIGeneric(object):
7490

7591
__slots__ = ['server', 'devKey', '_server_url', '_positionalArgNames']
7692

7793
__VERSION__ = VERSION
7894

79-
def __init__(self, server_url, devKey,
80-
transport=None, encoding=None, verbose=0, allow_none=0):
95+
def __init__(self, server_url, devKey, **args):
96+
transport=args.get('transport')
97+
encoding=args.get('encoding')
98+
verbose=args.get('verbose',0)
99+
allow_none=args.get('allow_none',0)
81100
self.server = xmlrpclib.Server(server_url, transport, encoding,
82101
verbose, allow_none)
83102
self.devKey = devKey
@@ -692,26 +711,24 @@ def checkDevKey(self):
692711
# */
693712
# public function uploadTestCaseAttachment($args)
694713

695-
# /**
696-
# * Uploads an attachment for an execution.
697-
# *
698-
# * The attachment content must be Base64 encoded by the client before sending it.
699-
# *
700-
# * @param struct $args
701-
# * @param string $args["devKey"] Developer key
702-
# * @param int $args["executionid"] execution ID
703-
# * @param string $args["title"] (Optional) The title of the Attachment
704-
# * @param string $args["description"] (Optional) The description of the Attachment
705-
# * @param string $args["filename"] The file name of the Attachment (e.g.:notes.txt)
706-
# * @param string $args["filetype"] The file type of the Attachment (e.g.: text/plain)
707-
# * @param string $args["content"] The content (Base64 encoded) of the Attachment
708-
# *
709-
# * @since 1.9beta6
710-
# * @return mixed $resultInfo an array containing the fk_id, fk_table, title,
711-
# * description, file_name, file_size and file_type. If any errors occur it
712-
# * returns the erros map.
713-
# */
714-
# public function uploadExecutionAttachment($args)
714+
715+
@decoApiCallAddAttachment
716+
@decoApiCallWithArgs
717+
def uploadExecutionAttachment(self):
718+
""" uploadExecutionAttachment: Uploads an attachment for an execution
719+
mandatory args: attachmentfile
720+
positional args: executionid
721+
optional args : title, description, filename, filetype, content
722+
723+
attachmentfile: python file descriptor pointing to the file
724+
!Attention - on WINDOWS use binary mode for none text file
725+
see http://docs.python.org/2/tutorial/inputoutput.html#reading-and-writing-files
726+
727+
default values for filename, filetype, content are determine from
728+
ATTACHMENTFILE, but user could overwrite it, if they want to store the
729+
attachment with a different name
730+
"""
731+
715732

716733
# /**
717734
# * Uploads an attachment for specified table. You must specify the table that
@@ -1152,6 +1169,19 @@ def _convertPostionalArgs(self, methodName, valueList):
11521169
raise testlinkerrors.TLArgError(new_msg)
11531170
return {nameList[x] : valueList[x] for x in range(len(nameList)) }
11541171

1172+
def _getAttachmentArgs(self, attachmentfile):
1173+
""" returns dictionary with key/value pairs needed, to transfer
1174+
ATTACHMENTFILE via the api to into TL
1175+
ATTACHMENTFILE: python file descriptor pointing to the file """
1176+
import mimetypes
1177+
import base64
1178+
import os.path
1179+
return {'filename':os.path.basename(attachmentfile.name),
1180+
'filetype':mimetypes.guess_type(attachmentfile.name)[0],
1181+
'content':base64.encodestring(attachmentfile.read())
1182+
}
1183+
1184+
11551185
def _checkResponse(self, response, methodNameAPI, argsOptional):
11561186
""" Checks if RESPONSE is empty or includes Error Messages
11571187
Will raise TLRepsonseError in this case """
@@ -1161,10 +1191,14 @@ def _checkResponse(self, response, methodNameAPI, argsOptional):
11611191
raise testlinkerrors.TLResponseError(
11621192
methodNameAPI, argsOptional,
11631193
response[0]['message'], response[0]['code'])
1164-
except TypeError:
1165-
# some Response like doesUserExist returns boolean
1166-
# they are not iterable an will raise an TypeError
1167-
# - this reponses are ok
1194+
except (TypeError, KeyError):
1195+
# if the reponse has not a [{..}] structure, the check
1196+
# 'code' in response[0]
1197+
# raise an error. Following causes are ok
1198+
# TypeError: raised from doesUserExist, cause the postiv
1199+
# response is simply 'True'
1200+
# KeyError: raise from uploadExecutionAttachment, cause the
1201+
# positiv response is directly a dictionary
11681202
pass
11691203
else:
11701204
raise testlinkerrors.TLResponseError(methodNameAPI, argsOptional,

test/utest/testlinkapi_offline_test.py

+1
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ def test_countTestCasesTS(self):
163163
response = self.api.countTestCasesTS()
164164
self.assertEqual(0, response)
165165

166+
@unittest.expectedFailure
166167
def test_countPlatforms(self):
167168
self.api.loadScenario(SCENARIO_A)
168169
response = self.api.countPlatforms()

test/utest/testlinkapi_online_test.py

+6-5
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
# ok to check every implemented server call one time but not
2828
# to cover all possible responses or argument combinations
2929

30-
import unittest
30+
import unittest, os.path
3131
from testlink import TestlinkAPIClient, TestLinkHelper
3232
from testlink.testlinkerrors import TLResponseError
3333

@@ -196,10 +196,11 @@ def test_reportTCResult_unknownID(self):
196196
self.client.reportTCResult(4711, 4712, 'build 4713', 'p',
197197
'note 4714')
198198

199-
# def test_uploadExecutionAttachment_unknownID(self):
200-
# response = self.client.uploadExecutionAttachment('file 4711', 4712,
201-
# 'title 4713', 'descr. 4714')
202-
# self.assertIn('4711', response[0]['message'])
199+
def test_uploadExecutionAttachment_unknownID(self):
200+
attachemantFile = open(os.path.realpath(__file__), 'r')
201+
with self.assertRaisesRegexp(TLResponseError, '6004.*4712'):
202+
self.client.uploadExecutionAttachment(attachemantFile, 4712,
203+
'title 4713', 'descr. 4714')
203204

204205
def test_getProjectIDByName_unknownID(self):
205206
response = self.client.getProjectIDByName('project 4711')

0 commit comments

Comments
 (0)