diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4caf723 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +.project +.pydevproject +.settings/ +build/ +*.pyc +dist/ +MANIFEST diff --git a/LICENSE-2.0.txt b/LICENSE-2.0.txt new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/LICENSE-2.0.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..fb3515b --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,3 @@ +include MANIFEST.in LICENSE-2.0.txt README.md +recursive-include src *.py +recursive-include example *.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..f8fd9c6 --- /dev/null +++ b/README.md @@ -0,0 +1,95 @@ +TestLink API Python Client +========================== + +Copyright 2011-2012 +James Stock, Olivier Renault, TestLink-API-Python-client developers + +License [Apache License 2.0] + +Introduction +------------ + +TestLink-API-Python-client is a Python XMLRPC client for the [TestLink API]. + +It's a brick of Olivier Renault [JinFeng] idea - an interaction of [TestLink], + [Robot Framework] and [Jenkins]. + +Initially based on the James Stock testlink-api-python-client R7. + + +Directory Layout +---------------- + +src/ +* Source for TestLink API Python Client + +tests/ +* Unit Tests for TestLink API Python Client + +examples/ +* Examples, how to use the TestLink API Python Client + +Installation +------------ + +### Install TestLinkAPI into a virtualenv environment + +``` +[PYTHON27]\Scripts\virtualenv [PYENV]\testlink +[PYENV]\testlink\Scripts\activate +python setup.py install +``` + +### Run example with command line arguments + +``` +[PYENV]\testlink\Scripts\activate +python example\TestLinkExample.py + --server_url http://[YOURSERVER]/testlink/lib/api/xmlrpc.php + --devKey [Users devKey generated by TestLink] +``` + +### Run unittests with TestLink Server interaction + +``` +[PYENV]\testlink\Scripts\activate +set TESTLINK_API_PYTHON_SERVER_URL=http://[YOURSERVER]/testlink/lib/api/xmlrpc.php +set TESTLINK_API_PYTHON_DEVKEY=[Users devKey generated by TestLink] +cd test\utest +python -m unittest testlinkapicallservertest testlinkapi_online_test +``` + +### Run unittests without TestLink Server interaction + +``` +[PYENV]\testlink\Scripts\activate +cd test\utest +python -m unittest testlinkhelpertest testlinkapi_offline_test +``` + + +Download +-------- + +see [downloads] + + +TestLink-API-Python-client developers +------------------------------------- +* [James Stock], [Olivier Renault], kereval.com +* [g4l4drim], [pade], [lczub] +* anyone forgotten? + + +[Apache License 2.0]: http://www.apache.org/licenses/LICENSE-2.0 +[JinFeng]: http://www.sqaopen.net/blog/en/?p=63 +[TestLink API]: http://www.teamst.org/_tldoc/1.8/phpdoc_api/TestlinkAPI/TestlinkXMLRPCServer.html +[TestLink]: http://www.teamst.org/ +[Robot Framework]: http://code.google.com/p/robotframework +[Jenkins]: http://jenkins-ci.org/ +[downloads]: https://github.com/lczub/TestLink-API-Python-client/downloads +[Olivier Renault]: https://github.com/orenault/TestLink-API-Python-client +[pade]: https://github.com/pade/TestLink-API-Python-client +[g4l4drim]: https://github.com/g4l4drim/TestLink-API-Python-client +[James Stock]: https://code.google.com/p/testlink-api-python-client/ +[lczub]: https://github.com/lczub/TestLink-API-Python-client diff --git a/example/Last7DaysTestCases.py b/example/Last7DaysTestCases.py new file mode 100644 index 0000000..b53a2a3 --- /dev/null +++ b/example/Last7DaysTestCases.py @@ -0,0 +1,52 @@ +#! /usr/bin/python +# -*- coding: UTF-8 -*- + +# Copyright 2011-2012 Olivier Renault, James Stock, TestLink-API-Python-client developers +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------ + +# This example shows, how the API could be used to list all test cases, +# which have been created during the last 7 days + + +from testlink import TestlinkAPIClient, TestLinkHelper +import time + + +def iterTCasesfromTProject(api, TProjName, date1, date2): + """ returns as iterator all test cases of project TPROJTNAME, which are + created between DATE1 and DATE2 + DATE1 and DATE2 must be of type time.struct_time """ + TProjId = api.getTestProjectByName(TProjName)[0]['id'] + for TSinfo in api.getFirstLevelTestSuitesForTestProject(TProjId): + TSuiteId = TSinfo['id'] + for TCid in api.getTestCasesForTestSuite(TSuiteId, deep=1,details='only_id'): + TCdata = api.getTestCase(TCid)[0] #really only one TC? + dateTC=time.strptime(TCdata['creation_ts'][:10], '%Y-%m-%d') + if (date1 <= dateTC) and (dateTC <= date2): + yield TCdata + + +if __name__ == '__main__': + tlapi = TestLinkHelper().connect(TestlinkAPIClient) + projName = 'NEW_PROJECT_API' + currentTime = time.localtime() + oldTime = time.localtime(time.time() - 3600 * 24 * 7) + + print '%s test cases created between %s and %s' % \ + (projName, time.strftime('%Y-%m-%d', oldTime), + time.strftime('%Y-%m-%d', currentTime)) + for TCdata in iterTCasesfromTProject(tlapi, projName, oldTime, currentTime): + print ' %(name)s %(version)s %(creation_ts)s' % TCdata diff --git a/TestLinkExample.py b/example/TestLinkExample.py similarity index 76% rename from TestLinkExample.py rename to example/TestLinkExample.py index caf2c2b..88d651e 100644 --- a/TestLinkExample.py +++ b/example/TestLinkExample.py @@ -1,3 +1,23 @@ +#! /usr/bin/python +# -*- coding: UTF-8 -*- + +# Copyright 2011-2012 Olivier Renault, TestLink-API-Python-client developers +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------ + + """ TestLinkExample - v0.20 @@ -26,13 +46,23 @@ | --- 5 automated test steps """ -import TestLinkAPI +from testlink import TestlinkAPIClient, TestLinkHelper import sys -myTestLinkServer = "http://YOURSERVER/testlink/lib/api/xmlrpc.php" # YOURSERVER -myDevKey = "" # Put here your devKey - -myTestLink = TestLinkAPI.TestlinkAPIClient(myTestLinkServer, myDevKey) +# precondition a) +# SERVEUR_URL and KEY are defined in environment +# TESTLINK_API_PYTHON_SERVER_URL=http://YOURSERVER/testlink/lib/api/xmlrpc.php +# TESTLINK_API_PYTHON_DEVKEY=7ec252ab966ce88fd92c25d08635672b +# +# alternative precondition b) +# SERVEUR_URL and KEY are defined as command line arguments +# python TestLinkExample.py --server_url http://YOURSERVER/testlink/lib/api/xmlrpc.php +# --devKey 7ec252ab966ce88fd92c25d08635672b +tl_helper = TestLinkHelper() +tl_helper.setParamsFromArgs('''Shows how to use the TestLinkAPI. +=> Counts and lists the Projects +=> Create a new Project with the following structure:''') +myTestLink = tl_helper.connect(TestlinkAPIClient) NEWPROJECT="NEW_PROJECT_API" @@ -141,7 +171,8 @@ newTestCase = myTestLink.createTestCase(NEWTESTCASE_B, TestSuiteID_B, newProjectID, "admin", "This is the summary of the Test Case B", - "preconditions=these are the preconditions") + "preconditions=these are the preconditions", + "executiontype=%i" % AUTOMATED) isOk = newTestCase[0]['message'] if isOk=="Success!": newTestCaseID = newTestCase[0]['id'] diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..c5c25df --- /dev/null +++ b/setup.py @@ -0,0 +1,24 @@ +#! /usr/bin/python +# -*- coding: UTF-8 -*- + +# Copyright 2012 ??? , ??? +# +# Licensed under ??? +# + +from os.path import join, dirname +from distutils.core import setup + +execfile(join(dirname(__file__), 'src', 'testlink', 'version.py')) + +setup(name='TestLink', + version=VERSION, + description='Python XMLRPC client for the TestLink API', + author='James Stock, Olivier Renault, TestLink-API-Python-client developers', + author_email='???, ???, ???', + url='https://github.com/lczub/TestLink-API-Python-client', + license = 'unknown', + package_dir = {'': 'src'}, + packages=['testlink'], + ) + diff --git a/src/testlink/__init__.py b/src/testlink/__init__.py new file mode 100644 index 0000000..f5d1028 --- /dev/null +++ b/src/testlink/__init__.py @@ -0,0 +1,24 @@ +#! /usr/bin/python +# -*- coding: UTF-8 -*- + +# Copyright 2012 TestLink-API-Python-client developers +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------ + + +from .testlinkerrors import TestLinkError +from .testlinkapi import TestlinkAPIClient +from .testlink import TestLink +from .testlinkhelper import TestLinkHelper \ No newline at end of file diff --git a/src/testlink/testlink.py b/src/testlink/testlink.py new file mode 100644 index 0000000..28a2243 --- /dev/null +++ b/src/testlink/testlink.py @@ -0,0 +1,170 @@ +#! /usr/bin/python +# -*- coding: UTF-8 -*- + +# Copyright 2012 pade (Patrick Dassier), TestLink-API-Python-client developers +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------ + +from testlinkapi import TestlinkAPIClient, TestLinkHelper +from testlinkerrors import TestLinkError +from datetime import date + + +class TestLink(TestlinkAPIClient): + """ + TestLink API library + provide a user friendly library, with more robustness and error management + """ + + def __init__(self, server_url, key): + """ + Class initialisation + """ + super(TestLink, self).__init__(server_url, key) + + def getTestCaseIDByName(self, testCaseName, testSuiteName, testProjectName): + """ + Find a test case by its name, by its suite and its project + Suite name must not be duplicate, so only one test case must be found + Return test case id if success + or raise TestLinkError exception with error message in case of error + """ + results = super(TestLink, self).getTestCaseIDByName(testCaseName, testSuiteName, testProjectName) + if results[0].has_key("message"): + raise TestLinkError(results[0]["message"]) + elif len(results) > 1: + raise TestLinkError("(getTestCaseIDByName) - Several case test found. Suite name must not be duplicate for the same project") + else: + if results[0]["name"] == testCaseName: + return results[0]["id"] + raise TestLinkError("(getTestCaseIDByName) - Internal server error. Return value is not expected one!") + + + def reportResult(self, testResult, testCaseName, testSuiteName, testNotes="", **kwargs): + """ + Report results for test case + Arguments are: + - testResult: "p" for passed, "b" for blocked, "f" for failed + - testCaseName: the test case name to report + - testSuiteName: the test suite name that support the test case + - testNotes: optional, if empty will be replace by a default string. To let it blank, just set testNotes to " " characters + - an anonymous dictionnary with followings keys: + - testProjectName: the project to fill + - testPlanName: the active test plan + - buildName: the active build. + Raise a TestLinkError error with the error message in case of trouble + Return the execution id needs to attach files to test execution + """ + + # Check parameters + for data in ["testProjectName", "testPlanName", "buildName"]: + if not kwargs.has_key(data): + raise TestLinkError("(reportResult) - Missing key %s in anonymous dictionnary" % data) + + # Get project id + project = self.getTestProjectByName(kwargs["testProjectName"]) + + # Check if project is active + if project['active'] != '1': + raise TestLinkError("(reportResult) - Test project %s is not active" % kwargs["testProjectName"]) + + # Check test plan name + plan = self.getTestPlanByName(kwargs["testProjectName"], kwargs["testPlanName"]) + + # Check is test plan is open and active + if plan['is_open'] != '1' or plan['active'] != '1': + raise TestLinkError("(reportResult) - Test plan %s is not active or not open" % kwargs["testPlanName"]) + # Memorise test plan id + planId = plan['id'] + + # Check build name + build = self.getBuildByName(kwargs["testProjectName"], kwargs["testPlanName"], kwargs["buildName"]) + + # Check if build is open and active + if build['is_open'] != '1' or build['active'] != '1': + raise TestLinkError("(reportResult) - Build %s in not active or not open" % kwargs["buildName"]) + + # Get test case id + caseId = self.getTestCaseIDByName(testCaseName, testSuiteName, kwargs["testProjectName"]) + + # Check results parameters + if testResult not in "pbf": + raise TestLinkError("(reportResult) - Test result must be 'p', 'f' or 'b'") + + if testNotes == "": + # Builds testNotes if empty + today = date.today() + testNotes = "%s - Test performed automatically" % today.strftime("%c") + elif testNotes == " ": + #No notes + testNotes = "" + + print "testNotes: %s" % testNotes + # Now report results + results = self.reportTCResult(caseId, planId, kwargs["buildName"], testResult, testNotes) + # Check errors + if results[0]["message"] != "Success!": + raise TestLinkError(results[0]["message"]) + + return results[0]['id'] + + def getTestProjectByName(self, testProjectName): + """ + Return project + A TestLinkError is raised in case of error + """ + results = super(TestLink, self).getTestProjectByName(testProjectName) + if results[0].has_key("message"): + raise TestLinkError(results[0]["message"]) + + return results[0] + + def getTestPlanByName(self, testProjectName, testPlanName): + """ + Return test plan + A TestLinkError is raised in case of error + """ + results = super(TestLink, self).getTestPlanByName(testProjectName, testPlanName) + if results[0].has_key("message"): + raise TestLinkError(results[0]["message"]) + + return results[0] + + def getBuildByName(self, testProjectName, testPlanName, buildName): + """ + Return build corresponding to buildName + A TestLinkError is raised in case of error + """ + plan = self.getTestPlanByName(testProjectName, testPlanName) + builds = self.getBuildsForTestPlan(plan['id']) + + # Check if a builds exists + if builds == '': + raise TestLinkError("(getBuildByName) - Builds %s does not exists for test plan %s" % (buildName, testPlanName)) + + # Search the correct build name in the return builds list + for build in builds: + if build['name'] == buildName: + return build + + # No build found with builName name + raise TestLinkError("(getBuildByName) - Builds %s does not exists for test plan %s" % (buildName, testPlanName)) + +if __name__ == "__main__": + tl_helper = TestLinkHelper() + tl_helper.setParamsFromArgs() + myTestLink = tl_helper.connect(TestLink) + print myTestLink + print myTestLink.about() diff --git a/TestLinkAPI.py b/src/testlink/testlinkapi.py similarity index 63% rename from TestLinkAPI.py rename to src/testlink/testlinkapi.py index 2d08ba1..580f6c8 100644 --- a/TestLinkAPI.py +++ b/src/testlink/testlinkapi.py @@ -1,23 +1,61 @@ -""" - -TestLinkAPI - v0.20 -Created on 5 nov. 2011 -@author: Olivier Renault (admin@sqaopen.net) -@author: kereval.com -Initialy based on the James Stock testlink-api-python-client R7. +#! /usr/bin/python +# -*- coding: UTF-8 -*- -Updated by Kereval to support testCase Reporting and File attachment to test execution +# Copyright 2011-2012 Olivier Renault, James Stock, TestLink-API-Python-client developers +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------ -""" import xmlrpclib -class TestlinkAPIClient: - +from testlinkhelper import TestLinkHelper, VERSION +import testlinkerrors + + +class TestlinkAPIClient(object): + + __slots__ = ['server', 'devKey', 'stepsList', '_server_url'] + + __VERSION__ = VERSION + def __init__(self, server_url, devKey): self.server = xmlrpclib.Server(server_url) self.devKey = devKey self.stepsList = [] + self._server_url = server_url + + def _callServer(self, methodAPI, argsAPI=None): + """ call server method METHODAPI with error handling and returns the + responds """ + + response = None + try: + if argsAPI is None: + response = getattr(self.server.tl, methodAPI)() + else: + response = getattr(self.server.tl, methodAPI)(argsAPI) + except (IOError, xmlrpclib.ProtocolError), msg: + new_msg = 'problems connecting the TestLink Server %s\n%s' %\ + (self._server_url, msg) + raise testlinkerrors.TLConnectionError(new_msg) + except xmlrpclib.Fault, msg: + new_msg = 'problems calling the API method %s\n%s' %\ + (methodAPI, msg) + raise testlinkerrors.TLAPIError(new_msg) + return response + # # BUILT-IN API CALLS # @@ -27,21 +65,21 @@ def checkDevKey(self): check if Developer Key exists """ argsAPI = {'devKey' : self.devKey} - return self.server.tl.checkDevKey(argsAPI) + return self._callServer('checkDevKey', argsAPI) def about(self): """ about : Gives basic information about the API """ - return self.server.tl.about() + return self._callServer('about') def ping(self): """ ping : """ - return self.server.tl.ping() + return self._callServer('ping') def echo(self, message): - return self.server.tl.repeat({'str': message}) + return self._callServer('repeat', {'str': message}) def doesUserExist(self, user): """ doesUserExist : @@ -49,7 +87,7 @@ def doesUserExist(self, user): """ argsAPI = {'devKey' : self.devKey, 'user':str(user)} - return self.server.tl.doesUserExist(argsAPI) + return self._callServer('doesUserExist', argsAPI) def getBuildsForTestPlan(self, testplanid): """ getBuildsForTestPlan : @@ -57,15 +95,15 @@ def getBuildsForTestPlan(self, testplanid): """ argsAPI = {'devKey' : self.devKey, 'testplanid':str(testplanid)} - return self.server.tl.getBuildsForTestPlan(argsAPI) - + return self._callServer('getBuildsForTestPlan', argsAPI) + def getFirstLevelTestSuitesForTestProject(self,testprojectid): """ getFirstLevelTestSuitesForTestProject : Get set of test suites AT TOP LEVEL of tree on a Test Project """ argsAPI = {'devKey' : self.devKey, 'testprojectid':str(testprojectid)} - return self.server.tl.getFirstLevelTestSuitesForTestProject(argsAPI) + return self._callServer('getFirstLevelTestSuitesForTestProject', argsAPI) def getFullPath(self,nodeid): """ getFullPath : @@ -74,7 +112,7 @@ def getFullPath(self,nodeid): """ argsAPI = {'devKey' : self.devKey, 'nodeid':str(nodeid)} - return self.server.tl.getFullPath(argsAPI) + return self._callServer('getFullPath', argsAPI) def getLastExecutionResult(self, testplanid, testcaseid): """ getLastExecutionResult : @@ -84,7 +122,7 @@ def getLastExecutionResult(self, testplanid, testcaseid): argsAPI = {'devKey' : self.devKey, 'testplanid' : str(testplanid), 'testcaseid' : str(testcaseid)} - return self.server.tl.getLastExecutionResult(argsAPI) + return self._callServer('getLastExecutionResult', argsAPI) def getLatestBuildForTestPlan(self, testplanid): """ getLastExecutionResult : @@ -93,14 +131,14 @@ def getLatestBuildForTestPlan(self, testplanid): """ argsAPI = {'devKey' : self.devKey, 'testplanid':str(testplanid)} - return self.server.tl.getLatestBuildForTestPlan(argsAPI) + return self._callServer('getLatestBuildForTestPlan', argsAPI) def getProjects(self): """ getProjects: Gets a list of all projects """ argsAPI = {'devKey' : self.devKey} - return self.server.tl.getProjects(argsAPI) + return self._callServer('getProjects', argsAPI) def getProjectTestPlans(self, testprojectid): """ getLastExecutionResult : @@ -108,7 +146,7 @@ def getProjectTestPlans(self, testprojectid): """ argsAPI = {'devKey' : self.devKey, 'testprojectid':str(testprojectid)} - return self.server.tl.getProjectTestPlans(argsAPI) + return self._callServer('getProjectTestPlans', argsAPI) def getTestCase(self, testcaseid): """ getTestCase : @@ -116,7 +154,7 @@ def getTestCase(self, testcaseid): """ argsAPI = {'devKey' : self.devKey, 'testcaseid' : str(testcaseid)} - return self.server.tl.getTestCase(argsAPI) + return self._callServer('getTestCase', argsAPI) def getTestCaseAttachments(self, testcaseid): """ getTestCaseAttachments : @@ -124,7 +162,7 @@ def getTestCaseAttachments(self, testcaseid): """ argsAPI = {'devKey' : self.devKey, 'testcaseid':str(testcaseid)} - return self.server.tl.getTestCaseAttachments(argsAPI) + return self._callServer('getTestCaseAttachments', argsAPI) def getTestCaseCustomFieldDesignValue(self, testcaseexternalid, version, testprojectid, customfieldname, details): @@ -137,16 +175,34 @@ def getTestCaseCustomFieldDesignValue(self, testcaseexternalid, version, 'testprojectid' : str(testprojectid), 'customfieldname' : str(customfieldname), 'details' : str(details)} - return self.server.tl.getTestCaseCustomFieldDesignValue(argsAPI) + return self._callServer('getTestCaseCustomFieldDesignValue', argsAPI) - def getTestCaseIDByName(self, testCaseName): - """ getTestCaseIDByName : - Find a test case by its name - """ + def getTestCaseIDByName(self, testCaseName, testSuiteName=None, testProjectName=None): + """ + Find a test case by its name + testSuiteName and testProjectName are optionals arguments + This function return a list of tests cases + """ argsAPI = {'devKey' : self.devKey, 'testcasename':str(testCaseName)} - return self.server.tl.getTestCaseIDByName(argsAPI) - + + if testSuiteName is not None: + argsAPI.update({'testsuitename':str(testSuiteName)}) + + if testProjectName is not None: + argsAPI.update({'testprojectname':str(testProjectName)}) + + # Server return can be a list or a dictionnary ! + # This function always return a list + ret_srv = self._callServer('getTestCaseIDByName', argsAPI) + if type(ret_srv) == dict: + retval = [] + for value in ret_srv.values(): + retval.append(value) + return retval + else: + return ret_srv + def getTestCasesForTestPlan(self, *args): """ getTestCasesForTestPlan : List test cases linked to a test plan @@ -160,9 +216,9 @@ def getTestCasesForTestPlan(self, *args): if len(args)>1: params = args[1:] for param in params: - paramlist = param.split("=") - argsAPI[paramlist[0]] = paramlist[1] - return self.server.tl.getTestCasesForTestPlan(argsAPI) + paramlist = param.split("=") + argsAPI[paramlist[0]] = paramlist[1] + return self._callServer('getTestCasesForTestPlan', argsAPI) def getTestCasesForTestSuite(self, testsuiteid, deep, details): """ getTestCasesForTestSuite : @@ -172,7 +228,7 @@ def getTestCasesForTestSuite(self, testsuiteid, deep, details): 'testsuiteid' : str(testsuiteid), 'deep' : str(deep), 'details' : str(details)} - return self.server.tl.getTestCasesForTestSuite(argsAPI) + return self._callServer('getTestCasesForTestSuite', argsAPI) def getTestPlanByName(self, testprojectname, testplanname): """ getTestPlanByName : @@ -181,7 +237,7 @@ def getTestPlanByName(self, testprojectname, testplanname): argsAPI = {'devKey' : self.devKey, 'testprojectname' : str(testprojectname), 'testplanname' : str(testplanname)} - return self.server.tl.getTestPlanByName(argsAPI) + return self._callServer('getTestPlanByName', argsAPI) def getTestPlanPlatforms(self, testplanid): """ getTestPlanPlatforms : @@ -189,7 +245,7 @@ def getTestPlanPlatforms(self, testplanid): """ argsAPI = {'devKey' : self.devKey, 'testplanid' : str(testplanid)} - return self.server.tl.getTestPlanPlatforms(argsAPI) + return self._callServer('getTestPlanPlatforms', argsAPI) def getTestProjectByName(self, testprojectname): """ getTestProjectByName : @@ -197,7 +253,7 @@ def getTestProjectByName(self, testprojectname): """ argsAPI = {'devKey' : self.devKey, 'testprojectname' : str(testprojectname)} - return self.server.tl.getTestProjectByName(argsAPI) + return self._callServer('getTestProjectByName', argsAPI) def getTestSuiteByID(self, testsuiteid): """ getTestSuiteByID : @@ -205,7 +261,7 @@ def getTestSuiteByID(self, testsuiteid): """ argsAPI = {'devKey' : self.devKey, 'testsuiteid' : str(testsuiteid)} - return self.server.tl.getTestSuiteByID(argsAPI) + return self._callServer('getTestSuiteByID', argsAPI) def getTestSuitesForTestPlan(self, testplanid): """ getTestSuitesForTestPlan : @@ -213,7 +269,7 @@ def getTestSuitesForTestPlan(self, testplanid): """ argsAPI = {'devKey' : self.devKey, 'testplanid' : str(testplanid)} - return self.server.tl.getTestSuitesForTestPlan(argsAPI) + return self._callServer('getTestSuitesForTestPlan', argsAPI) def getTestSuitesForTestSuite(self, testsuiteid): """ getTestSuitesForTestSuite : @@ -221,7 +277,7 @@ def getTestSuitesForTestSuite(self, testsuiteid): """ argsAPI = {'devKey' : self.devKey, 'testsuiteid' : str(testsuiteid)} - return self.server.tl.getTestSuitesForTestSuite(argsAPI) + return self._callServer('getTestSuitesForTestSuite', argsAPI) def getTotalsForTestPlan(self, testplanid): """ getTotalsForTestPlan : @@ -229,7 +285,7 @@ def getTotalsForTestPlan(self, testplanid): """ argsAPI = {'devKey' : self.devKey, 'testplanid' : str(testplanid)} - return self.server.tl.getTotalsForTestPlan(argsAPI) + return self._callServer('getTotalsForTestPlan', argsAPI) def createTestProject(self, *args): """ createTestProject : @@ -248,16 +304,16 @@ def createTestProject(self, *args): if len(args)>2: params = args[2:] for param in params: - paramlist = param.split("=") - if paramlist[0] == "options": - optionlist = paramlist[1].split(",") - for option in optionlist: - optiontuple = option.split(":") - options[optiontuple[0]] = optiontuple[1] - argsAPI[paramlist[0]] = options - else: - argsAPI[paramlist[0]] = paramlist[1] - return self.server.tl.createTestProject(argsAPI) + paramlist = param.split("=") + if paramlist[0] == "options": + optionlist = paramlist[1].split(",") + for option in optionlist: + optiontuple = option.split(":") + options[optiontuple[0]] = optiontuple[1] + argsAPI[paramlist[0]] = options + else: + argsAPI[paramlist[0]] = paramlist[1] + return self._callServer('createTestProject', argsAPI) def createBuild(self, testplanid, buildname, buildnotes): """ createBuild : @@ -267,7 +323,7 @@ def createBuild(self, testplanid, buildname, buildnotes): 'testplanid' : str(testplanid), 'buildname' : str(buildname), 'buildnotes' : str(buildnotes)} - return self.server.tl.createBuild(argsAPI) + return self._callServer('createBuild', argsAPI) def createTestPlan(self, *args): """ createTestPlan : @@ -283,9 +339,9 @@ def createTestPlan(self, *args): if len(args)>2: params = args[2:] for param in params: - paramlist = param.split("=") - argsAPI[paramlist[0]] = paramlist[1] - return self.server.tl.createTestPlan(argsAPI) + paramlist = param.split("=") + argsAPI[paramlist[0]] = paramlist[1] + return self._callServer('createTestPlan', argsAPI) def createTestSuite(self, *args): """ createTestSuite : @@ -301,16 +357,16 @@ def createTestSuite(self, *args): if len(args)>3: params = args[3:] for param in params: - paramlist = param.split("=") - argsAPI[paramlist[0]] = paramlist[1] - return self.server.tl.createTestSuite(argsAPI) + paramlist = param.split("=") + argsAPI[paramlist[0]] = paramlist[1] + return self._callServer('createTestSuite', argsAPI) def createTestCase(self, *args): """ createTestCase : Create a test case Mandatory parameters : testcasename, testsuiteid, testprojectid, authorlogin, summary, steps - Optional parameters : preconditions, importance, execution, order, + Optional parameters : preconditions, importance, executiontype, order, internalid, checkduplicatedname, actiononduplicatedname """ argsAPI = {'devKey' : self.devKey, @@ -323,18 +379,18 @@ def createTestCase(self, *args): if len(args)>5: params = args[5:] for param in params: - paramlist = param.split("=") - argsAPI[paramlist[0]] = paramlist[1] - ret = self.server.tl.createTestCase(argsAPI) + paramlist = param.split("=") + argsAPI[paramlist[0]] = paramlist[1] + ret = self._callServer('createTestCase', argsAPI) self.stepsList = [] return ret - def reportTCResult(self, testcaseid, testplanid, buildid, status, notes ): - """ + def reportTCResult(self, testcaseid, testplanid, buildname, status, notes ): + """ Report execution result testcaseid: internal testlink id of the test case testplanid: testplan associated with the test case - buildid: build version of the test case + buildname: build name of the test case status: test verdict ('p': pass,'f': fail,'b': blocked) Return : [{'status': True, 'operation': 'reportTCResult', 'message': 'Success!', 'overwrite': False, 'id': '37'}] @@ -343,11 +399,13 @@ def reportTCResult(self, testcaseid, testplanid, buildid, status, notes ): argsAPI = {'devKey' : self.devKey, 'testcaseid' : testcaseid, 'testplanid' : testplanid, - 'buildid':buildid, 'status': status, - 'notes' : notes + 'buildname': buildname, + 'notes': str(notes) } - return self.server.tl.reportTCResult(argsAPI) + return self._callServer('reportTCResult', argsAPI) + + def uploadExecutionAttachment(self,attachmentfile,executionid,title,description): """ @@ -369,7 +427,7 @@ def uploadExecutionAttachment(self,attachmentfile,executionid,title,description) 'filetype':mimetypes.guess_type(attachmentfile.name)[0], 'content':base64.encodestring(attachmentfile.read()) } - return self.server.tl.uploadExecutionAttachment(argsAPI) + return self._callServer('uploadExecutionAttachment', argsAPI) # # ADDITIONNAL FUNCTIONS @@ -379,17 +437,17 @@ def countProjects(self): """ countProjects : Count all the test project """ - projects=TestlinkAPIClient.getProjects(self) + projects=self.getProjects() return len(projects) def countTestPlans(self): """ countProjects : Count all the test plans """ - projects=TestlinkAPIClient.getProjects(self) + projects=self.getProjects() nbTP = 0 for project in projects: - ret = TestlinkAPIClient.getProjectTestPlans(self,project['id']) + ret = self.getProjectTestPlans(project['id']) nbTP += len(ret) return nbTP @@ -397,14 +455,12 @@ def countTestSuites(self): """ countProjects : Count all the test suites """ - projects=TestlinkAPIClient.getProjects(self) + projects=self.getProjects() nbTS = 0 for project in projects: - TestPlans = TestlinkAPIClient.getProjectTestPlans(self, - project['id']) + TestPlans = self.getProjectTestPlans(project['id']) for TestPlan in TestPlans: - TestSuites = TestlinkAPIClient.getTestSuitesForTestPlan(self, - TestPlan['id']) + TestSuites = self.getTestSuitesForTestPlan(TestPlan['id']) nbTS += len(TestSuites) return nbTS @@ -412,14 +468,12 @@ def countTestCasesTP(self): """ countProjects : Count all the test cases linked to a Test Plan """ - projects=TestlinkAPIClient.getProjects(self) + projects=self.getProjects() nbTC = 0 for project in projects: - TestPlans = TestlinkAPIClient.getProjectTestPlans(self, - project['id']) + TestPlans = self.getProjectTestPlans(project['id']) for TestPlan in TestPlans: - TestCases = TestlinkAPIClient.getTestCasesForTestPlan(self, - TestPlan['id']) + TestCases = self.getTestCasesForTestPlan(TestPlan['id']) nbTC += len(TestCases) return nbTC @@ -427,16 +481,14 @@ def countTestCasesTS(self): """ countProjects : Count all the test cases linked to a Test Suite """ - projects=TestlinkAPIClient.getProjects(self) + projects=self.getProjects() nbTC = 0 for project in projects: - TestPlans = TestlinkAPIClient.getProjectTestPlans(self, - project['id']) + TestPlans = self.getProjectTestPlans(project['id']) for TestPlan in TestPlans: - TestSuites = TestlinkAPIClient.getTestSuitesForTestPlan(self, - TestPlan['id']) + TestSuites = self.getTestSuitesForTestPlan(TestPlan['id']) for TestSuite in TestSuites: - TestCases = TestlinkAPIClient.getTestCasesForTestSuite(self, + TestCases = self.getTestCasesForTestSuite( TestSuite['id'],'true','full') for TestCase in TestCases: nbTC += len(TestCases) @@ -446,14 +498,12 @@ def countPlatforms(self): """ countPlatforms : Count all the Platforms """ - projects=TestlinkAPIClient.getProjects(self) + projects=self.getProjects() nbPlatforms = 0 for project in projects: - TestPlans = TestlinkAPIClient.getProjectTestPlans(self, - project['id']) + TestPlans = self.getProjectTestPlans(project['id']) for TestPlan in TestPlans: - Platforms = TestlinkAPIClient.getTestPlanPlatforms(self, - TestPlan['id']) + Platforms = self.getTestPlanPlatforms(TestPlan['id']) nbPlatforms += len(Platforms) return nbPlatforms @@ -461,14 +511,12 @@ def countBuilds(self): """ countBuilds : Count all the Builds """ - projects=TestlinkAPIClient.getProjects(self) + projects=self.getProjects() nbBuilds = 0 for project in projects: - TestPlans = TestlinkAPIClient.getProjectTestPlans(self, - project['id']) + TestPlans = self.getProjectTestPlans(project['id']) for TestPlan in TestPlans: - Builds = TestlinkAPIClient.getBuildsForTestPlan(self, - TestPlan['id']) + Builds = self.getBuildsForTestPlan(TestPlan['id']) nbBuilds += len(Builds) return nbBuilds @@ -476,9 +524,9 @@ def listProjects(self): """ listProjects : Lists the Projects (display Name & ID) """ - projects=TestlinkAPIClient.getProjects(self) + projects=self.getProjects() for project in projects: - print "Name: %s ID: %s " % (project['name'], project['id']) + print "Name: %s ID: %s " % (project['name'], project['id']) def initStep(self, actions, expected_results, execution_type): @@ -486,47 +534,49 @@ def initStep(self, actions, expected_results, execution_type): Initializes the list which stores the Steps of a Test Case to create """ self.stepsList = [] - list = {} - list['step_number'] = '1' - list['actions'] = actions - list['expected_results'] = expected_results - list['execution_type'] = str(execution_type) - self.stepsList.append(list) + lst = {} + lst['step_number'] = '1' + lst['actions'] = actions + lst['expected_results'] = expected_results + lst['execution_type'] = str(execution_type) + self.stepsList.append(lst) return True def appendStep(self, actions, expected_results, execution_type): """ appendStep : Appends a step to the steps list """ - list = {} - list['step_number'] = str(len(self.stepsList)+1) - list['actions'] = actions - list['expected_results'] = expected_results - list['execution_type'] = str(execution_type) - self.stepsList.append(list) + lst = {} + lst['step_number'] = str(len(self.stepsList)+1) + lst['actions'] = actions + lst['expected_results'] = expected_results + lst['execution_type'] = str(execution_type) + self.stepsList.append(lst) return True def getProjectIDByName(self, projectName): - projects=self.server.tl.getProjects({'devKey' : self.devKey}) - for project in projects: - if (project['name'] == projectName): - result = project['id'] - else: - result = -1 - return result - -if __name__ == "__main__": - myTestLinkServer = "http://YOURSERVER/testlink/lib/api/xmlrpc.php" #change - myDevKey = "" # Put here your devKey - myTestLink = TestlinkAPIClient(myTestLinkServer, myDevKey) - print "TestLinkAPIClient - v0.2" - print "@author: Olivier Renault (admin@sqaopen.net)" - print "" - if myTestLink.checkDevKey() == True: - methodList = [method for method in TestlinkAPIClient.__dict__] - for method in methodList: - if method[0:2] != "__": - print method - print "" - else: - print "Incorrect DevKey." + projects=self.getProjects() + result=-1 + for project in projects: + if (project['name'] == projectName): + result = project['id'] + break + return result + + def __str__(self): + message = """ +TestlinkAPIClient - class %s - version %s +@author: Olivier Renault, James Stock, TestLink-API-Python-client developers +""" + return message % (self.__class__.__name__, self.__VERSION__) + + +if __name__ == "__main__": + tl_helper = TestLinkHelper() + tl_helper.setParamsFromArgs() + myTestLink = tl_helper.connect(TestlinkAPIClient) + print myTestLink + print myTestLink.about() + + + diff --git a/src/testlink/testlinkerrors.py b/src/testlink/testlinkerrors.py new file mode 100644 index 0000000..ae75d23 --- /dev/null +++ b/src/testlink/testlinkerrors.py @@ -0,0 +1,38 @@ +#! /usr/bin/python +# -*- coding: UTF-8 -*- + +# Copyright 2012 Patrick Dassier, Luiko Czub, TestLink-API-Python-client developers +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------ + +class TestLinkError(Exception): + """ Basic error + Return message pass as argument + """ +# def __init__(self, msg): +# self.__msg = msg +# +# def __str__(self): +# return self.__msg + +class TLConnectionError(TestLinkError): + """ Connection error + - wrong url? - server not reachable? """ + +class TLAPIError(TestLinkError): + """ API error + - wrong method name ? - misssing required args? """ + + \ No newline at end of file diff --git a/src/testlink/testlinkhelper.py b/src/testlink/testlinkhelper.py new file mode 100644 index 0000000..1601b03 --- /dev/null +++ b/src/testlink/testlinkhelper.py @@ -0,0 +1,114 @@ +#! /usr/bin/python +# -*- coding: UTF-8 -*- + +# Copyright 2012 Luiko Czub, TestLink-API-Python-client developers +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------ + +import os +from argparse import ArgumentParser +from version import VERSION + + +class TestLinkHelper(object): + """ Helper Class to find out the TestLink connection parameters. + a) TestLink Server URL of XMLRPC + environment variable - TESTLINK_API_PYTHON_SERVER_URL + default value - http://localhost/testlink/lib/api/xmlrpc.php + command line arg - server_url + b) Users devKey generated by TestLink + environment variable - TESTLINK_API_PYTHON_DEVKEY + default value - 42 + command line arg - devKey + + Examples 1 - init TestlinkAPIClient with environment variables + - define connection parameters in environment variables + TESTLINK_API_PYTHON_DEVKEY and TESTLINK_API_PYTHON_DEVKEY + - TestLinkHelper().connect(TestlinkAPIClient) + -> returns a TestlinkAPIClient instance + + Examples 2 - init TestLink with command line arguments + - call python module with command line arguments --server_url and --devKey + TESTLINK_API_PYTHON_DEVKEY and TESTLINK_API_PYTHON_DEVKEY + - tl_helper = TestLinkHelper() + tl_helper.setParamsFromArgs() + tl_helper.connect(TestLink) + -> returns a TestLink instance + + """ + + __slots__ = ['_server_url', '_devkey'] + + ENVNAME_SERVER_URL = 'TESTLINK_API_PYTHON_SERVER_URL' + ENVNAME_DEVKEY = 'TESTLINK_API_PYTHON_DEVKEY' + DEFAULT_SERVER_URL = 'http://localhost/testlink/lib/api/xmlrpc.php' + DEFAULT_DEVKEY = '42' + DEFAULT_DESCRIPTION = 'Python XMLRPC client for the TestLink API v%s' \ + % VERSION + + def __init__(self, server_url=None, devkey=None): + """ fill slots _server_url and _devkey + Priority: + 1. init args + 2. environment variables + 3. default values + """ + self._server_url = server_url + self._devkey = devkey + self._setParamsFromEnv() + + def _setParamsFromEnv(self): + """ fill empty slots _server_url and _devkey from environment variables + _server_url <- TESTLINK_API_PYTHON_SERVER_URL + _devkey <- TESTLINK_API_PYTHON_DEVKEY + + If environment variables are not defined, defaults values are set. + """ + if self._server_url == None: + self._server_url = os.getenv(self.ENVNAME_SERVER_URL, + self.DEFAULT_SERVER_URL) + if self._devkey == None: + self._devkey = os.getenv(self.ENVNAME_DEVKEY, self.DEFAULT_DEVKEY) + + def _createArgparser(self, usage): + """ returns a parser for command line arguments """ + + a_parser = ArgumentParser( description=usage) + # optional command line parameters + a_parser.add_argument('--server_url', default=self._server_url, + help='TestLink Server URL of XMLRPC (default: %(default)s) ') + # pseudo optional command line parameters, + # must be set individual for each user + a_parser.add_argument('--devKey', default=self._devkey, + help='Users devKey generated by TestLink (default: %(default)s) ') + return a_parser + + def setParamsFromArgs(self, usage=DEFAULT_DESCRIPTION, args=None): + """ fill slots _server_url and _devkey from command line arguments + _server_url <- --server_url + _devkey <- --devKey + + uses current values of these slots as default values + """ + a_parser = self._createArgparser(usage) + args = a_parser.parse_args(args) + self._server_url = args.server_url + self._devkey = args.devKey + + + def connect(self, tl_api_class): + """ returns a new instance of TL_API_CLASS """ + return tl_api_class(self._server_url, self._devkey) + \ No newline at end of file diff --git a/src/testlink/version.py b/src/testlink/version.py new file mode 100644 index 0000000..6ceccdb --- /dev/null +++ b/src/testlink/version.py @@ -0,0 +1,20 @@ +#! /usr/bin/python +# -*- coding: UTF-8 -*- + +# Copyright 2012 TestLink-API-Python-client developers +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------ + +VERSION = '0.4.0-RC1' \ No newline at end of file diff --git a/test/test.py b/test/test.py new file mode 100644 index 0000000..9984b75 --- /dev/null +++ b/test/test.py @@ -0,0 +1,82 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright 2011-2012 pade (Patrick Dassier), TestLink-API-Python-client developers +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------ + +''' + Source file + File name: test.py + Creation date: 04-10-2012 + Author: dassier + +''' + +''' +Fichier de test pour le module "TestLinkAPI.py" +''' + +import re +from testlink import TestLink, TestLinkError, TestLinkHelper +from nose.tools import * + +class TestClass(): + def setUp(self): + """Initialisation + """ + + # precondition - SERVEUR_URL and KEY are defined in environment + # TESTLINK_API_PYTHON_SERVER_URL=http://localhost/testlink/lib/api/xmlrpc.php + # TESTLINK_API_PYTHON_DEVKEY=7ec252ab966ce88fd92c25d08635672b + self.client = TestLinkHelper().connect(TestLink) + + def test_getTestCaseIDByName(self): + """ getTestCaseIDByName test + """ + val = self.client.getTestCaseIDByName("Fin de programme", "Séquence 2", "Test 2") + # 31 is test case id + assert_equal(val, '31' ) + + # Check if an error is raised in case of bad parameters + assert_raises(TestLinkError, self.client.getTestCaseIDByName, "Initialisation", "Séquence 1", "Test 2") + + def test_getTestProjectByName(self): + project = self.client.getTestProjectByName("Test 2") + assert_equals(type(project), dict) + # Check if an error is raised in case of bad parameters + assert_raises(TestLinkError, self.client.getTestProjectByName, "Unknown project") + + def test_getTestPlanByName(self): + plan_ok = self.client.getTestPlanByName("Test 2", "Full") + + # Assume that plan id is 33 + assert_equal(plan_ok['id'], '33') + + assert_raises(TestLinkError, self.client.getTestPlanByName, "Test 2", "Name Error") + + def test_getBuildByName(self): + pass + + def test_reportResult(self): + dico = {'testProjectName': 'Automatique', + 'testPlanName': 'FullAuto', + 'buildName': 'V0.1'} + execid = self.client.reportResult("p", "test1", "S1", "An example of note", **dico) + assert_equal(type(execid), str) + + execid = self.client.reportResult("f", "test2", "S1", **dico) + assert_equal(type(execid), str) + diff --git a/test/utest/testlinkapi_offline_test.py b/test/utest/testlinkapi_offline_test.py new file mode 100644 index 0000000..a424a19 --- /dev/null +++ b/test/utest/testlinkapi_offline_test.py @@ -0,0 +1,179 @@ +#! /usr/bin/python +# -*- coding: UTF-8 -*- + +# Copyright 2012 Luiko Czub, TestLink-API-Python-client developers +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------ + +# this test works WITHOUT an online TestLink Server +# no calls are send to a TestLink Server + +import unittest +from testlink import TestlinkAPIClient, TestLinkHelper + +# scenario_a includes response from a testlink 1.9.3 server +SCENARIO_A = {'getProjects' : [ + {'opt': {'requirementsEnabled': 0, 'testPriorityEnabled': 1, + 'automationEnabled': 1, 'inventoryEnabled': 0}, + 'prefix': 'NPROAPI', 'name': 'NEW_PROJECT_API', 'color': '', + 'notes': 'This is a Project created with the API', + 'option_priority': '0', + 'options': 'O:8:"stdClass":4:{s:19:"requirementsEnabled";i:0;s:19:"testPriorityEnabled";i:1;s:17:"automationEnabled";i:1;s:16:"inventoryEnabled";i:0;}', + 'tc_counter': '2', 'option_reqs': '0', 'active': '1', + 'is_public': '1', 'id': '21', 'option_automation': '0'}, + {'opt': {'requirementsEnabled': 1, 'testPriorityEnabled': 1, + 'automationEnabled': 1, 'inventoryEnabled': 1}, + 'prefix': 'TP', 'name': 'TestProject', 'color': '', + 'notes': '

Initiales TestProject, um  TestLink kennen zu lernen

', + 'option_priority': '0', + 'options': 'O:8:"stdClass":4:{s:19:"requirementsEnabled";i:1;s:19:"testPriorityEnabled";i:1;s:17:"automationEnabled";i:1;s:16:"inventoryEnabled";i:1;}', + 'tc_counter': '0', 'option_reqs': '0', 'active': '1', + 'is_public': '1', 'id': '1', 'option_automation': '0'}], + 'getProjectTestPlans' : { + '21' : [{'name': 'TestPlan_API', + 'notes': 'New TestPlan created with the API', + 'active': '1', 'is_public': '1', + 'testproject_id': '21', 'id': '22'}] , + '1' : '' }, + 'getFirstLevelTestSuitesForTestProject' : { + '21' : [{'node_type_id': '2', 'name': 'A - First Level', + 'parent_id': '21', 'node_order': '0', + 'node_table': 'testsuites', 'id': '23'}, + {'node_type_id': '2', 'name': 'B - First Level', + 'parent_id': '21', 'node_order': '0', + 'node_table': 'testsuites', 'id': '24'}], + '1' : [{'message': '(getFirstLevelTestSuitesForTestProject) - Test Project (TestProject) is empty.', + 'code': 7008}] }, + 'getTestSuitesForTestPlan' : {'22' : ''}, + 'getTestCasesForTestPlan' : {'22' : ''}, + # TL(1.9.3)->getTestSuitesForTestSuite really returns {...} and not [{....}] !!! + 'getTestSuitesForTestSuite' : { + '23' : {'node_type_id': '2', 'name': 'AA - Second Level', + 'parent_id': '23', 'node_order': '0', + 'details': 'Details of the Test Suite AA', 'id': '25'}, + '24' : ''}, + 'getTestCasesForTestSuite' : { + '23' : [{'node_type_id': '3', 'tcversion_id': '25', + 'name': 'TESTCASE_AA', 'parent_id': '25', + 'node_order': '0', 'node_table': 'testcases', + 'external_id': 'NPROAPI-1', 'id': '26'}], + '24' : [{'node_type_id': '3', 'tcversion_id': '24', + 'name': 'TESTCASE_B', 'parent_id': '24', + 'node_order': '0', 'node_table': 'testcases', + 'external_id': 'NPROAPI-2', 'id': '33'}], + '25' : [{'node_type_id': '3', 'tcversion_id': '25', + 'name': 'TESTCASE_AA', 'parent_id': '25', + 'node_order': '0', 'node_table': 'testcases', + 'external_id': 'NPROAPI-1', 'id': '26'}] + }, + 'getTestPlanPlatforms' : { + '22' : [{'message': 'Test plan (name:TestPlan_API) has no platforms linked', + 'code': 3041}]}, + 'getBuildsForTestPlan' : {'22' : ''} + } + +class DummyAPIClient(TestlinkAPIClient): + """ Dummy for Simulation TestLinkAPICLient. + Overrides _callServer() Method to return test scenarios + """ + + def __init__(self, server_url, devKey): + super(DummyAPIClient, self).__init__(server_url, devKey) + self.scenario_data = {} + + def loadScenario(self, a_scenario): + self.scenario_data = a_scenario + + def _callServer(self, methodAPI, argsAPI=None): + data = self.scenario_data[methodAPI] + response = None + if methodAPI in ['getProjectTestPlans', + 'getFirstLevelTestSuitesForTestProject']: + response = data[argsAPI['testprojectid']] + elif methodAPI in ['getTestSuitesForTestPlan', + 'getTestCasesForTestPlan', 'getTestPlanPlatforms', + 'getBuildsForTestPlan']: + response = data[argsAPI['testplanid']] + elif methodAPI in ['getTestCasesForTestSuite', + 'getTestSuitesForTestSuite']: + response = data[argsAPI['testsuiteid']] + else: + response = data + return response + + +class TestLinkAPIOfflineTestCase(unittest.TestCase): + """ TestCases for TestlinkAPIClient - does not interacts with a TestLink Server. + works with DummyAPIClientm which returns special test data + """ + + def setUp(self): + self.api = TestLinkHelper().connect(DummyAPIClient) + +# def tearDown(self): +# pass + + + def test_countProjects(self): + self.api.loadScenario(SCENARIO_A) + response = self.api.countProjects() + self.assertEqual(2, response) + + def test_countTestPlans(self): + self.api.loadScenario(SCENARIO_A) + response = self.api.countTestPlans() + self.assertEqual(1, response) + + def test_countTestSuites(self): + self.api.loadScenario(SCENARIO_A) + response = self.api.countTestSuites() + self.assertEqual(0, response) + + def test_countTestCasesTP(self): + self.api.loadScenario(SCENARIO_A) + response = self.api.countTestCasesTP() + self.assertEqual(0, response) + + def test_countTestCasesTS(self): + self.api.loadScenario(SCENARIO_A) + response = self.api.countTestCasesTS() + self.assertEqual(0, response) + + def test_countPlatforms(self): + self.api.loadScenario(SCENARIO_A) + response = self.api.countPlatforms() + self.assertEqual(0, response) + + def test_countBuilds(self): + self.api.loadScenario(SCENARIO_A) + response = self.api.countBuilds() + self.assertEqual(0, response) + +# def test_listProjects(self): +# self.api.loadScenario(SCENARIO_A) +# self.api.listProjects() +# no assert check cause method returns nothing +# 'just' prints to stdout + + def test_getProjectIDByName(self): + self.api.loadScenario(SCENARIO_A) + response = self.api.getProjectIDByName('NEW_PROJECT_API') + self.assertEqual('21', response) + response = self.api.getProjectIDByName('UNKNOWN_PROJECT') + self.assertEqual(-1, response) + +if __name__ == "__main__": + #import sys;sys.argv = ['', 'Test.testName'] + unittest.main() \ No newline at end of file diff --git a/test/utest/testlinkapi_online_test.py b/test/utest/testlinkapi_online_test.py new file mode 100644 index 0000000..0899ef5 --- /dev/null +++ b/test/utest/testlinkapi_online_test.py @@ -0,0 +1,218 @@ +#! /usr/bin/python +# -*- coding: UTF-8 -*- + +# Copyright 2012 Luiko Czub, TestLink-API-Python-client developers +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------ + +# this test requires an online TestLink Server, which connection parameters +# are defined in environment variables +# TESTLINK_API_PYTHON_DEVKEY and TESTLINK_API_PYTHON_DEVKEY +# +# works with the example project NEW_PROJECT_API (see TestLinkExample.py) +# FIME LC 29.10.29: test does not really interacts with test link +# only negative test with none existing IDs implemented +# ok to check every implemented server call one time but not +# to cover all possible responses or argument combinations + +import unittest +from testlink import TestlinkAPIClient, TestLinkHelper + + +class TestLinkAPIOnlineTestCase(unittest.TestCase): + """ TestCases for TestlinkAPIClient - interacts with a TestLink Server. + works with the example project NEW_PROJECT_API (see TestLinkExample.py) + """ + + def setUp(self): + self.client = TestLinkHelper().connect(TestlinkAPIClient) + + +# def tearDown(self): +# pass + + + def test_checkDevKey(self): + response = self.client.checkDevKey() + self.assertEqual(True, response) + + def test_about(self): + response = self.client.about() + self.assertIn('Testlink API', response) + + def test_ping(self): + response = self.client.ping() + self.assertEqual('Hello!', response) + + def test_echo(self): + response = self.client.echo('Yellow Submarine') + self.assertEqual('You said: Yellow Submarine', response) + + def test_doesUserExist_unknownID(self): + response = self.client.doesUserExist('Big Bird') + self.assertIn('Big Bird', response[0]['message']) + self.assertEqual(10000, response[0]['code']) + + def test_getBuildsForTestPlan_unknownID(self): + response = self.client.getBuildsForTestPlan(4711) + self.assertIn('4711', response[0]['message']) + self.assertEqual(3000, response[0]['code']) + + def test_getFirstLevelTestSuitesForTestProject_unknownID(self): + response = self.client.getFirstLevelTestSuitesForTestProject(4711) + self.assertIn('4711', response[0]['message']) + self.assertEqual(7000, response[0]['code']) + + def test_getFullPath_unknownID(self): + response = self.client.getFullPath(4711) + self.assertIn('getFullPath', response[0]['message']) + self.assertEqual(234, response[0]['code']) + + def test_getLastExecutionResult_unknownID(self): + response = self.client.getLastExecutionResult(4711, 4712) + self.assertIn('4711', response[0]['message']) + self.assertEqual(3000, response[0]['code']) + + def test_getLatestBuildForTestPlan_unknownID(self): + response = self.client.getLatestBuildForTestPlan(4711) + self.assertIn('4711', response[0]['message']) + self.assertEqual(3000, response[0]['code']) + + def test_getProjects(self): + response = self.client.getProjects() + self.assertIsNotNone(response) + + def test_getProjectTestPlans_unknownID(self): + response = self.client.getProjectTestPlans(4711) + self.assertIn('4711', response[0]['message']) + self.assertEqual(7000, response[0]['code']) + + def test_getTestCase_unknownID(self): + response = self.client.getTestCase(4711) + # FAILURE in 1.9.3 API message: replacement does not work + # The Test Case ID (testcaseid: %s) provided does not exist! + #self.assertIn('4711', response[0]['message']) + self.assertEqual(5000, response[0]['code']) + + def test_getTestCaseAttachments_unknownID(self): + response = self.client.getTestCaseAttachments(4711) + # FAILURE in 1.9.3 API message: replacement does not work + # The Test Case ID (testcaseid: %s) provided does not exist! + #self.assertIn('4711', response[0]['message']) + self.assertEqual(5000, response[0]['code']) + + def test_getTestCaseCustomFieldDesignValue_unknownID(self): + response = self.client.getTestCaseCustomFieldDesignValue( + 4712, 1, 4711, 'a_field', 'a_detail') + self.assertIn('4711', response[0]['message']) + self.assertEqual(7000, response[0]['code']) + + def test_getTestCaseIDByName_unknownID(self): + response = self.client.getTestCaseIDByName('Big Bird') + self.assertIn('getTestCaseIDByName', response[0]['message']) + self.assertEqual(5030, response[0]['code']) + + def test_getTestCasesForTestPlan_unknownID(self): + response = self.client.getTestCasesForTestPlan(4711) + self.assertIn('4711', response[0]['message']) + self.assertEqual(3000, response[0]['code']) + + def test_getTestCasesForTestSuite_unknownID(self): + response = self.client.getTestCasesForTestSuite(4711, 2, 'a_detail') + self.assertIn('4711', response[0]['message']) + self.assertEqual(8000, response[0]['code']) + + def test_getTestPlanByName_unknownID(self): + response = self.client.getTestPlanByName('project 4711', 'plan 4712') + self.assertIn('4711', response[0]['message']) + self.assertEqual(7011, response[0]['code']) + + def test_getTestPlanPlatforms_unknownID(self): + response = self.client.getTestPlanPlatforms(4711) + self.assertIn('4711', response[0]['message']) + self.assertEqual(3000, response[0]['code']) + + def test_getTestProjectByName_unknownID(self): + response = self.client.getTestProjectByName('project 4711') + self.assertIn('4711', response[0]['message']) + self.assertEqual(7011, response[0]['code']) + + def test_getTestSuiteByID_unknownID(self): + response = self.client.getTestSuiteByID(4711) + self.assertIn('4711', response[0]['message']) + self.assertEqual(8000, response[0]['code']) + + def test_getTestSuitesForTestPlan_unknownID(self): + response = self.client.getTestSuitesForTestPlan(4711) + self.assertIn('4711', response[0]['message']) + self.assertEqual(3000, response[0]['code']) + + def test_getTestSuitesForTestSuite_unknownID(self): + response = self.client.getTestSuitesForTestSuite(4711) + self.assertIn('4711', response[0]['message']) + self.assertEqual(8000, response[0]['code']) + + def test_getTotalsForTestPlan_unknownID(self): + response = self.client.getTotalsForTestPlan(4711) + self.assertIn('4711', response[0]['message']) + self.assertEqual(3000, response[0]['code']) + + def test_createTestProject_unknownID(self): + response = self.client.createTestProject('', 'P4711') + self.assertIn('Empty name', response[0]['message']) + self.assertEqual(7001, response[0]['code']) + + def test_createBuild_unknownID(self): + response = self.client.createBuild(4711, 'Build 4712', 'note 4713') + self.assertIn('4711', response[0]['message']) + self.assertEqual(3000, response[0]['code']) + + def test_createTestPlan_unknownID(self): + response = self.client.createTestPlan('plan 4711', 'project 4712') + self.assertIn('4712', response[0]['message']) + self.assertEqual(7011, response[0]['code']) + + def test_createTestSuite_unknownID(self): + response = self.client.createTestSuite( 4711, 'suite 4712', 'detail 4713') + self.assertIn('4711', response[0]['message']) + self.assertEqual(7000, response[0]['code']) + + def test_createTestCase_unknownID(self): + response = self.client.createTestCase('case 4711', 4712, 4713, + 'Big Bird', 'summary 4714') + self.assertIn('4713', response[0]['message']) + self.assertEqual(7000, response[0]['code']) + + def test_reportTCResult_unknownID(self): + response = self.client.reportTCResult(4711, 4712, 'build 4713', 'p', + 'note 4714') + # FAILURE in 1.9.3 API message: replacement does not work + # The Test Case ID (testcaseid: %s) provided does not exist! + #self.assertIn('4711', response[0]['message']) + self.assertEqual(5000, response[0]['code']) + +# def test_uploadExecutionAttachment_unknownID(self): +# response = self.client.uploadExecutionAttachment('file 4711', 4712, +# 'title 4713', 'descr. 4714') +# self.assertIn('4711', response[0]['message']) + + def test_getProjectIDByName_unknownID(self): + response = self.client.getProjectIDByName('project 4711') + self.assertEqual(-1, response) + + +if __name__ == "__main__": + #import sys;sys.argv = ['', 'Test.testName'] + unittest.main() \ No newline at end of file diff --git a/test/utest/testlinkapicallservertest.py b/test/utest/testlinkapicallservertest.py new file mode 100644 index 0000000..1b432ce --- /dev/null +++ b/test/utest/testlinkapicallservertest.py @@ -0,0 +1,73 @@ +#! /usr/bin/python +# -*- coding: UTF-8 -*- + +# Copyright 2012 Luiko Czub, TestLink-API-Python-client developers +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------ + +# this test requires an online TestLink Server, which connection parameters +# are defined in environment variables +# TESTLINK_API_PYTHON_DEVKEY and TESTLINK_API_PYTHON_DEVKEY + +import unittest +from testlink import TestlinkAPIClient, TestLinkHelper +from testlink import testlinkerrors + + +class TestLinkAPIcallServerTestCase(unittest.TestCase): + """ TestCases for TestLinkAPICleint._callServer() """ + + + def test_callServer_noArgs(self): + """ test _callServer() - calling method with no args """ + + client = TestLinkHelper().connect(TestlinkAPIClient) + response = client._callServer('sayHello') + self.assertEqual('Hello!', response) + + def test_callServer_withArgs(self): + """ test _callServer() - calling method with args """ + + client = TestLinkHelper().connect(TestlinkAPIClient) + response = client._callServer('repeat', {'str' : 'some arg'}) + self.assertEqual('You said: some arg', response) + + def test_callServer_ProtocollError(self): + """ test _callServer() - Server raises ProtocollError """ + + server_url = TestLinkHelper()._server_url + bad_server_url = server_url.split('xmlrpc.php')[0] + client = TestLinkHelper(bad_server_url).connect(TestlinkAPIClient) + def a_func(api_client): api_client._callServer('sayHello') + self.assertRaises(testlinkerrors.TLConnectionError, a_func, client) + + def test_callServer_socketError(self): + """ test _callServer() - Server raises a socket Error (IOError) """ + + bad_server_url = 'http://111.222.333.4/testlink/lib/api/xmlrpc.php' + client = TestLinkHelper(bad_server_url).connect(TestlinkAPIClient) + def a_func(api_client): api_client._callServer('sayHello') + self.assertRaises(testlinkerrors.TLConnectionError, a_func, client) + + def test_callServer_FaultError(self): + """ test _callServer() - Server raises Fault Error """ + + client = TestLinkHelper().connect(TestlinkAPIClient) + def a_func(api_client): api_client._callServer('sayGoodBye') + self.assertRaises(testlinkerrors.TLAPIError, a_func, client) + +if __name__ == "__main__": + #import sys;sys.argv = ['', 'Test.testName'] + unittest.main() \ No newline at end of file diff --git a/test/utest/testlinkhelpertest.py b/test/utest/testlinkhelpertest.py new file mode 100644 index 0000000..71cb2c8 --- /dev/null +++ b/test/utest/testlinkhelpertest.py @@ -0,0 +1,138 @@ +#! /usr/bin/python +# -*- coding: UTF-8 -*- + +# Copyright 2012 Luiko Czub, TestLink-API-Python-client developers +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------ + +# this test works WITHOUT an online TestLink Server +# no calls are send to a TestLink Server + +import unittest, os +from testlink import TestLinkHelper + + +class DummyTestLinkAPI(object): + """ Dummy for Simulation TestLinkAPICLient. + Used for init() tests with TestLinkHelper.connect(api_class) + """ + + def __init__(self, server_url, devKey): + self.server = server_url + self.devKey = devKey + +class TestLinkHelperTestCase(unittest.TestCase): + """ TestCases for TestLinkHelper """ + + CLASSUNDERTEST = TestLinkHelper + ENVNAMES = ['TESTLINK_API_PYTHON_SERVER_URL', 'TESTLINK_API_PYTHON_DEVKEY'] + EXPECTED_DEFAULTS = ['http://localhost/testlink/lib/api/xmlrpc.php', '42'] + + def setEnviron(self, envname, envvalue ): + """ manipulates os.environ - stores os.environ[envname] = envvalue """ + if envvalue is None: + # UNSET environment variable + if os.environ.has_key(envname): + os.environ.pop(envname) + else: + os.environ[envname] = envvalue + + def setUp(self): + """ backup TestLinkHelper related environment variables """ + self.backup = {} + for envname in self.ENVNAMES: + self.backup[envname] = os.getenv(envname) + + def tearDown(self): + """ restore TestLinkHelper related environment variables """ + for envname in self.ENVNAMES: + self.setEnviron(envname, self.backup[envname]) + + def test_init_Env(self): + """ init TestLinkHelper with environment variables """ + self.check_init_env((None, None), self.EXPECTED_DEFAULTS) + self.check_init_env(('SERVER-URL-1', None), + ('SERVER-URL-1', self.EXPECTED_DEFAULTS[1])) + self.check_init_env((None, 'DEVKEY-2'), + (self.EXPECTED_DEFAULTS[0], 'DEVKEY-2')) + self.check_init_env(('SERVER-URL-3', 'DEVKEY-3'), + ('SERVER-URL-3', 'DEVKEY-3')) + + def check_init_env(self, env_values, expectations ): + # set TestLinkHelper related environment variables + self.setEnviron(self.ENVNAMES[0], env_values[0]) + self.setEnviron(self.ENVNAMES[1], env_values[1]) + # init helper without method params + a_helper = self.CLASSUNDERTEST() + self.assertEqual(expectations[0], a_helper._server_url) + self.assertEqual(expectations[1], a_helper._devkey) + + def test_init_params(self): + """ init TestLinkHelper with method parameter and no env variables """ + self.check_init_params(('SERVER-URL-11', None), + ('SERVER-URL-11', self.EXPECTED_DEFAULTS[1])) + self.check_init_params((None, 'DEVKEY-12'), + (self.EXPECTED_DEFAULTS[0], 'DEVKEY-12')) + self.check_init_params(('SERVER-URL-13', 'DEVKEY-13'), + ('SERVER-URL-13', 'DEVKEY-13')) + + def check_init_params(self, param_values, expectations ): + # unset TestLinkHelper related environment variables + self.setEnviron(self.ENVNAMES[0], None) + self.setEnviron(self.ENVNAMES[1], None) + # init helper with method params + a_helper = self.CLASSUNDERTEST(param_values[0], param_values[1]) + self.assertEqual(expectations[0], a_helper._server_url) + self.assertEqual(expectations[1], a_helper._devkey) + + def test_init_env_params(self): + """ init TestLinkHelper with mixed method parameter and env variables """ + # set TestLinkHelper related environment variables + self.setEnviron(self.ENVNAMES[0], 'SERVER-URL-21') + self.setEnviron(self.ENVNAMES[1], 'DEVKEY-21') + # init helper with method params + a_helper = self.CLASSUNDERTEST('SERVER-URL-22', 'DEVKEY-22') + # the method params have a high priority than the environment variables + self.assertEqual('SERVER-URL-22', a_helper._server_url) + self.assertEqual('DEVKEY-22', a_helper._devkey) + + + def test_createArgparser(self): + """ create TestLinkHelper command line argument parser """ + a_helper = self.CLASSUNDERTEST('SERVER-URL-31', 'DEVKEY-31') + a_parser = a_helper._createArgparser('DESCRIPTION-31') + self.assertEqual('DESCRIPTION-31', a_parser.description) + default_args=a_parser.parse_args('') + self.assertEqual('SERVER-URL-31', default_args.server_url) + self.assertEqual('DEVKEY-31', default_args.devKey) + + def test_setParamsFromArgs(self): + """ set TestLinkHelper params from command line arguments """ + a_helper = self.CLASSUNDERTEST() + a_helper.setParamsFromArgs(None, ['--server_url', 'SERVER-URL-41', + '--devKey' , 'DEVKEY-41']) + self.assertEqual('SERVER-URL-41', a_helper._server_url) + self.assertEqual('DEVKEY-41', a_helper._devkey) + + def test_connect(self): + """ create a TestLink API dummy """ + a_helper = self.CLASSUNDERTEST('SERVER-URL-51', 'DEVKEY-51') + a_tl_api = a_helper.connect(DummyTestLinkAPI) + self.assertEqual('SERVER-URL-51', a_tl_api.server) + self.assertEqual('DEVKEY-51', a_tl_api.devKey) + +if __name__ == "__main__": + #import sys;sys.argv = ['', 'Test.testName'] + unittest.main() \ No newline at end of file