├── setup.cfg ├── testrail ├── __init__.py ├── base.py ├── template.py ├── casetype.py ├── user.py ├── priority.py ├── entry.py ├── status.py ├── configuration.py ├── test.py ├── suite.py ├── section.py ├── helper.py ├── milestone.py ├── project.py ├── case.py ├── result.py ├── plan.py ├── run.py ├── client.py └── api.py ├── tests ├── testrail.conf-noemail ├── testrail.conf-nokey ├── testrail.conf-nourl ├── .testrail.conf ├── testrail.conf ├── testrail.conf-nosslcert ├── util.py ├── test_base.py ├── test_casetype.py ├── test_user.py ├── test_priority.py ├── test_status.py ├── test_updatecache.py ├── test_project.py ├── test_suite.py ├── test_milestone.py ├── test_section.py ├── test_test.py ├── test_plan.py ├── test_result.py └── test_run.py ├── nose.cfg ├── .travis.yml ├── tox.ini ├── .gitignore ├── LICENSE.txt ├── setup.py ├── README.md └── examples └── end_to_end_example.py /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md 3 | -------------------------------------------------------------------------------- /testrail/__init__.py: -------------------------------------------------------------------------------- 1 | from testrail.client import TestRail 2 | -------------------------------------------------------------------------------- /tests/testrail.conf-noemail: -------------------------------------------------------------------------------- 1 | testrail: 2 | user_key: 'your_api_key' 3 | url: 'https://' 4 | -------------------------------------------------------------------------------- /tests/testrail.conf-nokey: -------------------------------------------------------------------------------- 1 | testrail: 2 | user_email: 'user@yourdomain.com' 3 | url: 'https://' 4 | -------------------------------------------------------------------------------- /tests/testrail.conf-nourl: -------------------------------------------------------------------------------- 1 | testrail: 2 | user_email: 'user@yourdomain.com' 3 | user_key: 'your_api_key' 4 | -------------------------------------------------------------------------------- /tests/.testrail.conf: -------------------------------------------------------------------------------- 1 | testrail: 2 | user_email: 'user@yourdomain.com' 3 | user_key: 'your_api_key' 4 | url: 'https://' 5 | -------------------------------------------------------------------------------- /tests/testrail.conf: -------------------------------------------------------------------------------- 1 | testrail: 2 | user_email: 'user@yourdomain.com' 3 | user_key: 'your_api_key' 4 | url: 'https://' 5 | -------------------------------------------------------------------------------- /nose.cfg: -------------------------------------------------------------------------------- 1 | [nosetests] 2 | detailed-errors=1 3 | rednose=1 4 | verbosity=3 5 | where=tests 6 | with-coverage=1 7 | cover-erase=1 8 | cover-package=testrail 9 | -------------------------------------------------------------------------------- /tests/testrail.conf-nosslcert: -------------------------------------------------------------------------------- 1 | testrail: 2 | user_email: 'user@yourdomain.com' 3 | user_key: 'your_api_key' 4 | url: 'https://' 5 | verify_ssl: 'false' -------------------------------------------------------------------------------- /testrail/base.py: -------------------------------------------------------------------------------- 1 | class TestRailBase(object): 2 | """ Base class for all TestRail objects with a TestRail ID 3 | """ 4 | def __str__(self): 5 | class_name = self.__class__.__name__ 6 | return "{0}-{1}".format(class_name, self.id) 7 | 8 | def __repr__(self): 9 | return str(self) 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | sudo: false 3 | os: 4 | - linux 5 | python: 6 | - '2.7' 7 | - '3.6' 8 | - '3.7' 9 | - '3.8' 10 | - '3.9' 11 | install: 12 | - pip install -U pip 13 | - pip install -U tox-travis 14 | - pip install coveralls 15 | - pip install coverage 16 | - cp tests/testrail.conf ~/.testrail.conf 17 | script: tox -v --recreate 18 | after_success: 19 | - coveralls 20 | -------------------------------------------------------------------------------- /testrail/template.py: -------------------------------------------------------------------------------- 1 | class Template(object): 2 | def __init__(self, content): 3 | self._content = content 4 | 5 | @property 6 | def id(self): 7 | return self._content.get('id') 8 | 9 | @property 10 | def name(self): 11 | return self._content.get('name') 12 | 13 | @property 14 | def is_default(self): 15 | return self._content.get('is_default') 16 | -------------------------------------------------------------------------------- /tests/util.py: -------------------------------------------------------------------------------- 1 | import collections 2 | nested_dict = lambda: collections.defaultdict(nested_dict) 3 | 4 | 5 | def reset_shared_state(cls): 6 | for key in cls._shared_state.keys(): 7 | if key == '_timeout': 8 | cls._shared_state[key] = 30 9 | elif key == '_project_id': 10 | cls._shared_state[key] = None 11 | else: 12 | cls._shared_state[key] = nested_dict() 13 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | skipdist = True 3 | envlist = py{27,34,35,36} 4 | skip_missing_interpreters = True 5 | 6 | [tox:travis] 7 | 2.7 = py27 8 | 3.4 = py34 9 | 3.5 = py35 10 | 3.6 = py36 11 | 12 | [testenv] 13 | usedevelop = True 14 | envdir = {toxworkdir}/tox 15 | deps = 16 | nose 17 | nose-cov 18 | rednose 19 | mock 20 | 21 | commands = 22 | nosetests -c nose.cfg 23 | 24 | [testenv:env] 25 | envdir = {toxinidir}/env 26 | -------------------------------------------------------------------------------- /testrail/casetype.py: -------------------------------------------------------------------------------- 1 | from testrail.base import TestRailBase 2 | 3 | 4 | class CaseType(TestRailBase): 5 | def __init__(self, content): 6 | self._content = content 7 | 8 | @property 9 | def id(self): 10 | return self._content.get('id') 11 | 12 | @property 13 | def is_default(self): 14 | return self._content.get('is_default') 15 | 16 | @property 17 | def name(self): 18 | return self._content.get('name') 19 | -------------------------------------------------------------------------------- /tests/test_base.py: -------------------------------------------------------------------------------- 1 | import mock 2 | try: 3 | import unittest2 as unittest 4 | except ImportError: 5 | import unittest 6 | 7 | from testrail.base import TestRailBase 8 | 9 | 10 | class TestBase(unittest.TestCase): 11 | def setUp(self): 12 | class Foo(TestRailBase): 13 | def __init__(self, id): 14 | self.id = id 15 | 16 | self.base = Foo(123) 17 | 18 | def test___str__(self): 19 | self.assertEqual(str(self.base), "Foo-123") 20 | 21 | def test___repr__(self): 22 | self.assertEqual(repr(self.base), "Foo-123") 23 | -------------------------------------------------------------------------------- /testrail/user.py: -------------------------------------------------------------------------------- 1 | from testrail.base import TestRailBase 2 | 3 | 4 | class User(TestRailBase): 5 | def __init__(self, content=None): 6 | self._content = content or dict() 7 | 8 | def __str__(self): 9 | return '%s <%s>' % (self.name, self.id) 10 | 11 | @property 12 | def email(self): 13 | return self._content.get('email') 14 | 15 | @property 16 | def id(self): 17 | return self._content.get('id') 18 | 19 | @property 20 | def is_active(self): 21 | return self._content.get('is_active') 22 | 23 | @property 24 | def name(self): 25 | return self._content.get('name') 26 | -------------------------------------------------------------------------------- /testrail/priority.py: -------------------------------------------------------------------------------- 1 | from testrail.base import TestRailBase 2 | 3 | 4 | class Priority(TestRailBase): 5 | def __init__(self, content): 6 | self._content = content 7 | 8 | def __str__(self): 9 | return self.name 10 | 11 | @property 12 | def id(self): 13 | return self._content.get('id') 14 | 15 | @property 16 | def name(self): 17 | return self._content.get('name') 18 | 19 | @property 20 | def level(self): 21 | return self._content.get('priority') 22 | 23 | @property 24 | def short_name(self): 25 | return self._content.get('short_name') 26 | 27 | @property 28 | def is_default(self): 29 | return bool(self._content.get('is_default')) 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .coverage.* 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | *,cover 45 | 46 | # Translations 47 | *.mo 48 | *.pot 49 | 50 | # Django stuff: 51 | *.log 52 | 53 | # Sphinx documentation 54 | docs/_build/ 55 | 56 | # PyBuilder 57 | target/ 58 | -------------------------------------------------------------------------------- /tests/test_casetype.py: -------------------------------------------------------------------------------- 1 | try: 2 | import unittest2 as unittest 3 | except ImportError: 4 | import unittest 5 | 6 | from testrail.casetype import CaseType 7 | 8 | 9 | class TestCaseType(unittest.TestCase): 10 | def setUp(self): 11 | self.casetype = CaseType( 12 | { 13 | "id": 1, 14 | "is_default": False, 15 | "name": "Automated" 16 | } 17 | ) 18 | 19 | def test_get_id_type(self): 20 | self.assertEqual(type(self.casetype.id), int) 21 | 22 | def test_get_id(self): 23 | self.assertEqual(self.casetype.id, 1) 24 | 25 | def test_get_is_default_type(self): 26 | self.assertEqual(type(self.casetype.is_default), bool) 27 | 28 | def test_get_is_default(self): 29 | self.assertEqual(self.casetype.is_default, False) 30 | 31 | def test_get_name_type(self): 32 | self.assertEqual(type(self.casetype.name), str) 33 | 34 | def test_get_name(self): 35 | self.assertEqual(self.casetype.name, 'Automated') 36 | -------------------------------------------------------------------------------- /testrail/entry.py: -------------------------------------------------------------------------------- 1 | from testrail.base import TestRailBase 2 | from testrail.api import API 3 | from testrail.run import Run 4 | from testrail.suite import Suite 5 | 6 | 7 | class EntryRun(Run): 8 | def __init__(self, content): 9 | super(EntryRun, self).__init__(content) 10 | 11 | @property 12 | def entry_id(self): 13 | return self._content.get('entry_id') 14 | 15 | @property 16 | def entry_index(self): 17 | return self._content.get('entry_index') 18 | 19 | 20 | class Entry(TestRailBase): 21 | def __init__(self, content): 22 | self._content = content 23 | self._api = API() 24 | 25 | @property 26 | def id(self): 27 | return self._content.get('id') 28 | 29 | @property 30 | def name(self): 31 | return self._content.get('name') 32 | 33 | @property 34 | def runs(self): 35 | return list(map(EntryRun, self._content.get('runs'))) 36 | 37 | @property 38 | def suite(self): 39 | return Suite(self._api.suite_with_id(self._content.get('suite_id'))) 40 | -------------------------------------------------------------------------------- /testrail/status.py: -------------------------------------------------------------------------------- 1 | from testrail.base import TestRailBase 2 | 3 | 4 | class Status(TestRailBase): 5 | def __init__(self, content): 6 | self._content = content 7 | 8 | @property 9 | def id(self): 10 | return self._content.get('id') 11 | 12 | @property 13 | def name(self): 14 | return self._content.get('name') 15 | 16 | @property 17 | def label(self): 18 | return self._content.get('label') 19 | 20 | @property 21 | def is_untested(self): 22 | return self._content.get('is_untested') 23 | 24 | @property 25 | def is_system(self): 26 | return self._content.get('is_system') 27 | 28 | @property 29 | def is_final(self): 30 | return self._content.get('is_final') 31 | 32 | @property 33 | def color_medium(self): 34 | return self._content.get('color_medium') 35 | 36 | @property 37 | def color_dark(self): 38 | return self._content.get('color_dark') 39 | 40 | @property 41 | def color_bright(self): 42 | return self._content.get('color_bright') 43 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Travis Pavek 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /tests/test_user.py: -------------------------------------------------------------------------------- 1 | try: 2 | import unittest2 as unittest 3 | except ImportError: 4 | import unittest 5 | 6 | from testrail.user import User 7 | 8 | 9 | class TestUser(unittest.TestCase): 10 | def setUp(self): 11 | self.mock_user_data = { 12 | "email": "han@example.com", 13 | "id": 1, 14 | "is_active": True, 15 | "name": "Han Solo"} 16 | 17 | self.user = User(self.mock_user_data) 18 | 19 | def test_get_email(self): 20 | self.assertEqual(self.user.email, 'han@example.com') 21 | 22 | def test_get_email_type(self): 23 | self.assertEqual(str, type(self.user.email)) 24 | 25 | def test_get_id(self): 26 | self.assertEqual(self.user.id, 1) 27 | 28 | def test_get_id_type(self): 29 | self.assertEqual(int, type(self.user.id)) 30 | 31 | def test_get_is_active(self): 32 | self.assertEqual(self.user.is_active, True) 33 | 34 | def test_get_is_active_type(self): 35 | self.assertEqual(bool, type(self.user.is_active)) 36 | 37 | def test_get_name(self): 38 | self.assertEqual(self.user.name, 'Han Solo') 39 | 40 | def test_get_name_type(self): 41 | self.assertEqual(str, type(self.user.name)) 42 | -------------------------------------------------------------------------------- /tests/test_priority.py: -------------------------------------------------------------------------------- 1 | try: 2 | import unittest2 as unittest 3 | except ImportError: 4 | import unittest 5 | 6 | from testrail.priority import Priority 7 | 8 | 9 | class TestCaseType(unittest.TestCase): 10 | def setUp(self): 11 | self.priority = Priority( 12 | { 13 | "id": 4, 14 | "is_default": True, 15 | "name": "4 - Must Test", 16 | "priority": 4, 17 | "short_name": "4 - Must" 18 | } 19 | ) 20 | 21 | def test_get_id_type(self): 22 | self.assertEqual(type(self.priority.id), int) 23 | 24 | def test_get_id(self): 25 | self.assertEqual(self.priority.id, 4) 26 | 27 | def test_get_is_default_type(self): 28 | self.assertEqual(type(self.priority.is_default), bool) 29 | 30 | def test_get_is_default(self): 31 | self.assertEqual(self.priority.is_default, True) 32 | 33 | def test_get_name_type(self): 34 | self.assertEqual(type(self.priority.name), str) 35 | 36 | def test_get_name(self): 37 | self.assertEqual(self.priority.name, '4 - Must Test') 38 | 39 | def test_get_short_name_type(self): 40 | self.assertEqual(type(self.priority.short_name), str) 41 | 42 | def test_get_short_name(self): 43 | self.assertEqual(self.priority.short_name, '4 - Must') 44 | 45 | def test_get_level_type(self): 46 | self.assertEqual(type(self.priority.level), int) 47 | 48 | def test_get_level(self): 49 | self.assertEqual(self.priority.level, 4) 50 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from setuptools import setup 3 | 4 | install_requires = [ 5 | 'requests>=2.6.0', 6 | 'singledispatch>=3.4.0', 7 | 'pyyaml>=3.1.1', 8 | 'future', 9 | 'retry', 10 | ] 11 | 12 | if sys.version_info[:3] < (2, 7, 0): 13 | install_requires.append('ordereddict') 14 | 15 | setup( 16 | name='testrail', 17 | packages=['testrail'], 18 | version='0.3.15', 19 | description='Python library for interacting with TestRail via REST APIs.', 20 | author='Travis Pavek', 21 | author_email='travis.pavek@gmail.com', 22 | url='https://github.com/travispavek/testrail-python', 23 | download_url='https://github.com/travispavek/testrail-python/tarball/0.3.15', 24 | keywords=['testrail', 'api', 'client', 'library', 'rest'], 25 | install_requires=install_requires, 26 | classifiers=[ 27 | 'Development Status :: 4 - Beta', 28 | 'Intended Audience :: Developers', 29 | 'Natural Language :: English', 30 | 'License :: OSI Approved :: MIT License', 31 | 'Operating System :: OS Independent', 32 | 'Topic :: Software Development :: Libraries :: Python Modules', 33 | 'Topic :: Internet :: WWW/HTTP', 34 | 'Topic :: Software Development :: Quality Assurance', 35 | 'Topic :: Software Development :: Testing', 36 | 'Programming Language :: Python', 37 | 'Programming Language :: Python :: 2', 38 | 'Programming Language :: Python :: 2.7', 39 | 'Programming Language :: Python :: 3', 40 | 'Programming Language :: Python :: 3.6', 41 | 'Programming Language :: Python :: 3.7', 42 | 'Programming Language :: Python :: 3.8', 43 | 'Programming Language :: Python :: 3.9', 44 | ], 45 | ) 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TestRail Python Library 2 | [![Build Status](https://travis-ci.org/travispavek/testrail-python.svg?branch=master)](https://travis-ci.org/travispavek/testrail-python) 3 | [![Coverage Status](https://coveralls.io/repos/github/travispavek/testrail-python/badge.svg?branch=master)](https://coveralls.io/github/travispavek/testrail-python?branch=master) [![PyPI version](https://badge.fury.io/py/testrail.svg)](https://badge.fury.io/py/testrail) 4 | 5 | This Python Library allows you to easily publish results and manage your TestRail instance. 6 | 7 | ### Warning 8 | This library is still in beta. This means little to no testing and future releases may break compatibility. Please evaluate and report bugs/enhancements. 9 | 10 | ## Quick Start 11 | ```python 12 | from testrail import TestRail 13 | 14 | testrail = TestRail(project_id=1) 15 | milestone = testrail.milestone('rel-2.3') 16 | milestone.is_completed = True 17 | testrail.update(milestone) 18 | ``` 19 | 20 | For a more indepth example, see the [examples folder](examples/) 21 | 22 | 23 | #### Configuration 24 | Create '.testrail.conf' in your home directory with the following: 25 | ``` 26 | testrail: 27 | user_email: 'your email address' 28 | user_key: 'your API key or password' 29 | url: 'domain for TestRail instance' 30 | ``` 31 | 32 | You can override the config file with the following environment variables: 33 | 34 | * TESTRAIL_USER_EMAIL 35 | * TESTRAIL_USER_KEY 36 | * TESTRAIL_URL 37 | 38 | ## Installation 39 | The easiest and recommended way to install testrail is through [pip](https://pip.pypa.io): 40 | ``` 41 | $ pip install testrail 42 | ``` 43 | 44 | This will handle the client itself as well as any requirements. 45 | 46 | ## Usage 47 | Full documentation will hopefully be available soon. In the mean time, skimming over client.py should give you a good idea of how things work. 48 | 49 | **Important:** For performance reasons, response content is cached for 30 seconds. This can be adjusted by changing the timeout in api.py. Setting it to zero is not recommended and will probably annoy you to no end! 50 | -------------------------------------------------------------------------------- /testrail/configuration.py: -------------------------------------------------------------------------------- 1 | from testrail.base import TestRailBase 2 | from testrail.helper import ContainerIter, methdispatch, singleresult 3 | from testrail.project import Project 4 | 5 | 6 | class _InnerConfig(TestRailBase): 7 | def __init__(self, content=None): 8 | self._content = content or dict() 9 | 10 | @property 11 | def id(self): 12 | return self._content.get('id') 13 | 14 | @property 15 | def group_id(self): 16 | return self._content.get('group_id') 17 | 18 | @property 19 | def name(self): 20 | return self._content.get('name') 21 | 22 | 23 | class Config(TestRailBase): 24 | def __init__(self, content=None): 25 | self._content = content or dict() 26 | 27 | @property 28 | def id(self): 29 | return self._content.get('id') 30 | 31 | @property 32 | def name(self): 33 | return self._content.get('name') 34 | 35 | @property 36 | def project(self): 37 | return Project(self._content.get('project_id')) 38 | 39 | @property 40 | def configs(self): 41 | return _InnerConfigContainer( 42 | map(_InnerConfig, self._content.get('configs'))) 43 | 44 | 45 | class ConfigContainer(ContainerIter): 46 | def __init__(self, configs): 47 | super(ConfigContainer, self).__init__(configs) 48 | self._configs = configs 49 | 50 | @methdispatch 51 | @singleresult 52 | def group(self, gid): 53 | return filter(lambda g: g.id == gid, self._configs) 54 | 55 | @group.register(str) 56 | @singleresult 57 | def _group_by_name(self, gname): 58 | return filter(lambda g: g.name == gname, self._configs) 59 | 60 | 61 | class _InnerConfigContainer(ContainerIter): 62 | def __init__(self, configs): 63 | super(_InnerConfigContainer, self).__init__(configs) 64 | self._configs = configs 65 | 66 | @singleresult 67 | def id(self, config_id): 68 | return filter(lambda c: c.id == config_id, self._configs) 69 | 70 | @singleresult 71 | def name(self, name): 72 | if name is None: 73 | return list() 74 | return filter(lambda c: c.name.lower() == name.lower(), self._configs) 75 | -------------------------------------------------------------------------------- /testrail/test.py: -------------------------------------------------------------------------------- 1 | from testrail.base import TestRailBase 2 | from testrail import api 3 | from testrail.case import Case 4 | from testrail.casetype import CaseType 5 | from testrail.milestone import Milestone 6 | from testrail.project import Project 7 | from testrail.run import Run 8 | from testrail.status import Status 9 | from testrail.user import User 10 | from testrail.helper import testrail_duration_to_timedelta 11 | 12 | 13 | class Test(TestRailBase): 14 | def __init__(self, content=None): 15 | self._content = content or dict() 16 | self.api = api.API() 17 | 18 | def __str__(self): 19 | return self.title 20 | 21 | @property 22 | def assigned_to(self): 23 | return User(self.api.user_with_id(self._content.get('assignedto_id'))) 24 | 25 | @property 26 | def case(self): 27 | return Case(self.api.case_with_id(self._content.get('case_id'), suite_id=self.run.suite.id)) 28 | 29 | @property 30 | def estimate(self): 31 | duration = self._content.get('estimate') 32 | if duration is None: 33 | return None 34 | return testrail_duration_to_timedelta(duration) 35 | 36 | @property 37 | def estimate_forecast(self): 38 | duration = self._content.get('estimate_forecast') 39 | if duration is None: 40 | return None 41 | return testrail_duration_to_timedelta(duration) 42 | 43 | @property 44 | def id(self): 45 | return self._content.get('id') 46 | 47 | @property 48 | def milestone(self): 49 | project_id = self._content.get('project_id') 50 | milestone_id = self._content.get('milestone_id') 51 | if milestone_id is None: 52 | return None 53 | return Milestone(self.api.milestone_with_id(milestone_id, project_id)) 54 | 55 | @property 56 | def refs(self): 57 | return self._content.get('refs') 58 | 59 | @property 60 | def run(self): 61 | return Run(self.api.run_with_id(self._content.get('run_id'))) 62 | 63 | @property 64 | def status(self): 65 | return Status(self.api.status_with_id(self._content.get('status_id'))) 66 | 67 | @property 68 | def title(self): 69 | return self._content.get('title') 70 | 71 | def raw_data(self): 72 | return self._content 73 | -------------------------------------------------------------------------------- /testrail/suite.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from testrail.base import TestRailBase 4 | from testrail import api 5 | from testrail.helper import TestRailError 6 | from testrail.project import Project 7 | 8 | 9 | class Suite(TestRailBase): 10 | def __init__(self, content=None): 11 | self._content = content or dict() 12 | self.api = api.API() 13 | 14 | def __str__(self): 15 | return self.name 16 | 17 | @property 18 | def id(self): 19 | return self._content.get('id') 20 | 21 | @property 22 | def completed_on(self): 23 | try: 24 | return datetime.fromtimestamp( 25 | int(self._content.get('completed_on'))) 26 | except TypeError: 27 | return None 28 | 29 | @property 30 | def description(self): 31 | return self._content.get('description') 32 | 33 | @description.setter 34 | def description(self, value): 35 | if not isinstance(value, str): 36 | raise TestRailError('input must be a string') 37 | self._content['description'] = value 38 | 39 | @property 40 | def is_baseline(self): 41 | return self._content.get('is_baseline') 42 | 43 | @property 44 | def is_completed(self): 45 | return self._content.get('is_completed') 46 | 47 | @property 48 | def is_master(self): 49 | return self._content.get('is_master') 50 | 51 | @property 52 | def name(self): 53 | return self._content.get('name') 54 | 55 | @name.setter 56 | def name(self, value): 57 | if not isinstance(value, str): 58 | raise TestRailError('input must be a string') 59 | self._content['name'] = value 60 | 61 | @property 62 | def project(self): 63 | return Project( 64 | self.api.project_with_id(self._content.get('project_id'))) 65 | 66 | @project.setter 67 | def project(self, value): 68 | if not isinstance(value, Project): 69 | raise TestRailError('input must be a Project') 70 | self.api.project_with_id(value.id) # verify project is valid 71 | self._content['project_id'] = value.id 72 | 73 | @property 74 | def url(self): 75 | return self._content.get('url') 76 | 77 | def raw_data(self): 78 | return self._content 79 | -------------------------------------------------------------------------------- /testrail/section.py: -------------------------------------------------------------------------------- 1 | from testrail.base import TestRailBase 2 | from testrail import api 3 | from testrail.helper import TestRailError 4 | from testrail.suite import Suite 5 | 6 | 7 | class Section(TestRailBase): 8 | def __init__(self, content=None): 9 | self._content = content or dict() 10 | self.api = api.API() 11 | 12 | def __str__(self): 13 | return self.name 14 | 15 | @property 16 | def id(self): 17 | return self._content.get('id') 18 | 19 | @property 20 | def depth(self): 21 | return self._content.get('depth') 22 | 23 | @property 24 | def display_order(self): 25 | return self._content.get('display_order') 26 | 27 | @property 28 | def description(self): 29 | return self._content.get('description') 30 | 31 | @description.setter 32 | def description(self, value): 33 | if not isinstance(value, str): 34 | raise TestRailError('input must be a string') 35 | self._content['description'] = value 36 | 37 | @property 38 | def parent(self): 39 | return Section( 40 | self.api.section_with_id(self._content.get('parent_id'))) 41 | 42 | @parent.setter 43 | def parent(self, section): 44 | if not isinstance(section, Section): 45 | raise TestRailError('input must be a Section') 46 | self.api.section_with_id(section.id) # verify section is valid 47 | self._content['parent_id'] = section.id 48 | 49 | @property 50 | def name(self): 51 | return self._content.get('name') 52 | 53 | @name.setter 54 | def name(self, value): 55 | if not isinstance(value, str): 56 | raise TestRailError('input must be a string') 57 | self._content['name'] = value 58 | 59 | @property 60 | def suite(self): 61 | if self._content.get('suite_id') is None: 62 | return Suite() 63 | return Suite(self.api.suite_with_id(self._content.get('suite_id'))) 64 | 65 | @suite.setter 66 | def suite(self, suite_obj): 67 | if not isinstance(suite_obj, Suite): 68 | raise TestRailError('input must be a Suite') 69 | self.api.suite_with_id(suite_obj.id) # verify suite is valid 70 | self._content['suite_id'] = suite_obj.id 71 | 72 | def raw_data(self): 73 | return self._content 74 | -------------------------------------------------------------------------------- /testrail/helper.py: -------------------------------------------------------------------------------- 1 | import re 2 | import inspect 3 | from datetime import timedelta 4 | from functools import update_wrapper 5 | 6 | from singledispatch import singledispatch 7 | 8 | 9 | class TestRailError(Exception): 10 | pass 11 | 12 | 13 | class TooManyRequestsError(TestRailError): 14 | pass 15 | 16 | 17 | class ServiceUnavailableError(TestRailError): 18 | pass 19 | 20 | 21 | def methdispatch(func): 22 | dispatcher = singledispatch(func) 23 | 24 | def wrapper(*args, **kw): 25 | try: 26 | return dispatcher.dispatch(args[1].__class__)(*args, **kw) 27 | except IndexError: 28 | return dispatcher.dispatch(args[0].__class__)(*args, **kw) 29 | wrapper.register = dispatcher.register 30 | update_wrapper(wrapper, func) 31 | return wrapper 32 | 33 | 34 | def class_name(meth): 35 | for cls in inspect.getmro(meth.im_class): 36 | if meth.__name__ in cls.__dict__: 37 | return cls 38 | 39 | 40 | def testrail_duration_to_timedelta(duration): 41 | span = lambda x: int(x.group(0)[:-1]) if x else 0 42 | timedelta_map = { 43 | 'weeks': span(re.search('\d+w', duration)), 44 | 'days': span(re.search('\d+d', duration)), 45 | 'hours': span(re.search('\d+h', duration)), 46 | 'minutes': span(re.search('\d+m', duration)), 47 | 'seconds': span(re.search('\d+s', duration)) 48 | } 49 | return timedelta(**timedelta_map) 50 | 51 | 52 | def singleresult(func): 53 | def func_wrapper(*args, **kw): 54 | items = func(*args) 55 | if hasattr(items, '__iter__'): 56 | items = list(items) 57 | if len(items) > 1: 58 | raise TestRailError( 59 | 'identifier "%s" returned multiple results' % args[1]) 60 | elif len(items) == 0: 61 | return None 62 | #raise TestRailError('identifier "%s" returned no result' % args[1]) 63 | return items[0] 64 | return func_wrapper 65 | 66 | 67 | class ContainerIter(object): 68 | def __init__(self, objs): 69 | self._objs = list(objs) 70 | 71 | def __len__(self): 72 | return len(self._objs) 73 | 74 | def __getitem__(self, index): 75 | return self._objs[index] 76 | 77 | 78 | custom_methods_re = re.compile(r'^custom_(\w+)') 79 | 80 | 81 | def custom_methods(content): 82 | matches = [custom_methods_re.match(method) for method in content] 83 | return dict({match.group(1): match.group(0) for match in matches if match}) 84 | 85 | -------------------------------------------------------------------------------- /tests/test_status.py: -------------------------------------------------------------------------------- 1 | import mock 2 | try: 3 | import unittest2 as unittest 4 | except ImportError: 5 | import unittest 6 | 7 | from testrail.status import Status 8 | 9 | class TestStatus(unittest.TestCase): 10 | def setUp(self): 11 | self.mock_status_data = { 12 | "color_bright": 13684944, 13 | "color_dark": 0, 14 | "color_medium": 10526880, 15 | "id": 6, 16 | "is_final": False, 17 | "is_system": True, 18 | "is_untested": False, 19 | "label": "Mock Custom", 20 | "name": "mock_custom_status1" 21 | } 22 | self.status = Status(self.mock_status_data) 23 | 24 | def test_get_id_type(self): 25 | self.assertTrue(isinstance(self.status.id, int)) 26 | 27 | def test_get_id(self): 28 | self.assertEqual(self.status.id, 6) 29 | 30 | def test_get_name_type(self): 31 | self.assertTrue(isinstance(self.status.name, str)) 32 | 33 | def test_get_name(self): 34 | self.assertEqual(self.status.name, 'mock_custom_status1') 35 | 36 | def test_get_label_type(self): 37 | self.assertTrue(isinstance(self.status.label, str)) 38 | 39 | def test_get_label(self): 40 | self.assertEqual(self.status.label, 'Mock Custom') 41 | 42 | def test_get_is_untested_type(self): 43 | self.assertTrue(isinstance(self.status.is_untested, bool)) 44 | 45 | def test_get_is_untested(self): 46 | self.assertFalse(self.status.is_untested) 47 | 48 | def test_get_is_system_type(self): 49 | self.assertTrue(isinstance(self.status.is_system, bool)) 50 | 51 | def test_get_is_system(self): 52 | self.assertTrue(self.status.is_system) 53 | 54 | def test_get_is_final_type(self): 55 | self.assertTrue(isinstance(self.status.is_final, bool)) 56 | 57 | def test_get_is_final(self): 58 | self.assertFalse(self.status.is_final) 59 | 60 | def test_get_color_medium_type(self): 61 | self.assertTrue(isinstance(self.status.color_medium, int)) 62 | 63 | def test_get_color_medium(self): 64 | self.assertEqual(self.status.color_medium, 10526880) 65 | 66 | def test_get_color_dark_type(self): 67 | self.assertTrue(isinstance(self.status.color_dark, int)) 68 | 69 | def test_get_color_medium(self): 70 | self.assertEqual(self.status.color_dark, 0) 71 | 72 | def test_get_color_bright_type(self): 73 | self.assertTrue(isinstance(self.status.color_bright, int)) 74 | 75 | def test_get_color_bright(self): 76 | self.assertEqual(self.status.color_bright, 13684944) 77 | -------------------------------------------------------------------------------- /testrail/milestone.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | import time 3 | 4 | from testrail.base import TestRailBase 5 | from testrail import api 6 | from testrail.project import Project 7 | from testrail.helper import TestRailError 8 | 9 | 10 | class Milestone(TestRailBase): 11 | def __init__(self, content=None): 12 | self._content = content or dict() 13 | self.api = api.API() 14 | 15 | def __str__(self): 16 | return self.name 17 | 18 | @property 19 | def completed_on(self): 20 | try: 21 | return datetime.fromtimestamp( 22 | int(self._content.get('completed_on'))) 23 | except TypeError: 24 | return None 25 | 26 | @property 27 | def description(self): 28 | return self._content.get('description') 29 | 30 | @description.setter 31 | def description(self, value): 32 | if value is not None: 33 | if not isinstance(value, str): 34 | raise TestRailError('input must be string or None') 35 | self._content['description'] = value 36 | 37 | @property 38 | def due_on(self): 39 | try: 40 | return datetime.fromtimestamp(int(self._content.get('due_on'))) 41 | except TypeError: 42 | return None 43 | 44 | @due_on.setter 45 | def due_on(self, value): 46 | if value is None: 47 | due = None 48 | else: 49 | if not isinstance(value, datetime): 50 | raise TestRailError('input must be a datetime or None') 51 | due = int(time.mktime(value.timetuple())) 52 | self._content['due_on'] = due 53 | 54 | @property 55 | def id(self): 56 | return self._content.get('id') 57 | 58 | @property 59 | def is_completed(self): 60 | return bool(self._content.get('is_completed')) 61 | 62 | @is_completed.setter 63 | def is_completed(self, value): 64 | if not isinstance(value, bool): 65 | raise TestRailError('input must be a boolean') 66 | self._content['is_completed'] = value 67 | 68 | @property 69 | def name(self): 70 | return self._content.get('name') 71 | 72 | @name.setter 73 | def name(self, value): 74 | if not isinstance(value, str): 75 | raise TestRailError('input must be a string') 76 | self._content['name'] = value 77 | 78 | @property 79 | def project(self): 80 | return Project( 81 | self.api.project_with_id(self._content.get('project_id'))) 82 | 83 | @project.setter 84 | def project(self, value): 85 | if not isinstance(value, Project): 86 | raise TestRailError('input must be a Project') 87 | self.api.project_with_id(value.id) # verify project is valid 88 | self._content['project_id'] = value.id 89 | 90 | @property 91 | def url(self): 92 | return self._content.get('url') 93 | 94 | def raw_data(self): 95 | return self._content 96 | -------------------------------------------------------------------------------- /examples/end_to_end_example.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | import random 4 | import argparse 5 | 6 | from testrail import TestRail 7 | 8 | # Dictionary mapping of your project IDs to identifiable names 9 | project_dict = { 10 | 'project1': 1, 11 | 'project2': 2, 12 | } 13 | 14 | 15 | def get_args(): 16 | project_choices = sorted(project_dict.keys()) 17 | 18 | parser = argparse.ArgumentParser() 19 | 20 | parser.add_argument( 21 | 'project', type=str, choices=project_choices, 22 | help="Project to test") 23 | 24 | return parser.parse_args() 25 | 26 | 27 | def main(): 28 | """ This will offer a step by step guide to create a new run in TestRail, 29 | update tests in the run with results, and close the run 30 | """ 31 | # Parse command line arguments 32 | args = get_args() 33 | 34 | # Instantiate the TestRail client 35 | # Use the CLI argument to identify which project to work with 36 | tr = TestRail(project_dict[args.project]) 37 | 38 | # Get a reference to the current project 39 | project = tr.project(project_dict[args.project]) 40 | 41 | # To create a new run in TestRail, first create a new, blank run 42 | # Update the new run with a name and project reference 43 | new_run = tr.run() 44 | new_run.name = "Creating a new Run through the API" 45 | new_run.project = project 46 | new_run.include_all = True # All Cases in the Suite will be added as Tests 47 | 48 | # Add the run in TestRail. This creates the run, and returns a run object 49 | run = tr.add(new_run) 50 | 51 | print("Created new run: {0}".format(run.name)) 52 | 53 | # Before starting the tests, lets pull in the Status objects for later 54 | PASSED = tr.status('passed') 55 | FAILED = tr.status('failed') 56 | BLOCKED = tr.status('blocked') 57 | 58 | # Get a list of tests associated with the new run 59 | tests = list(tr.tests(run)) 60 | 61 | print("Found {0} tests".format(len(tests))) 62 | 63 | # Execute the tests, marking as passed, failed, or blocked 64 | for test_num, test in enumerate(tests): 65 | print("Executing test #{0}".format(test_num)) 66 | # Run your tests here, reaching some sort of pass/fail criteria 67 | # This example will pick results at random and update the tests as such 68 | 69 | test_status = random.choice([PASSED, FAILED, BLOCKED]) 70 | 71 | print("Updating test #{0} with a status of {1}".format(test_num, 72 | test_status.name)) 73 | 74 | # Create a blank result, and associate it with a test and a status 75 | result = tr.result() 76 | result.test = test 77 | result.status = test_status 78 | result.comment = "The test case was udpated via a script" 79 | 80 | # Add the result to TestRail 81 | tr.add(result) 82 | 83 | # All tests have been executed. Close this test run 84 | print("Finished, closing the run") 85 | tr.close(run) 86 | 87 | 88 | if __name__ == "__main__": 89 | main() 90 | -------------------------------------------------------------------------------- /testrail/project.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from testrail.base import TestRailBase 4 | from testrail.helper import ContainerIter, TestRailError 5 | 6 | 7 | class Project(TestRailBase): 8 | def __init__(self, response=None): 9 | self._content = response or dict() 10 | 11 | def __str__(self): 12 | return self.name 13 | 14 | @property 15 | def announcement(self): 16 | """The description/announcement of the project""" 17 | return self._content.get('announcement') 18 | 19 | @announcement.setter 20 | def announcement(self, msg): 21 | if not isinstance(msg, str): 22 | raise TestRailError('input must be a string') 23 | self._content['announcement'] = msg 24 | 25 | @property 26 | def completed_on(self): 27 | """The date/time when the project was marked as completed""" 28 | if self.is_completed: 29 | return datetime.fromtimestamp(self._content.get('completed_on')) 30 | return None 31 | 32 | @property 33 | def id(self): 34 | """The unique ID of the project""" 35 | return self._content.get('id') 36 | 37 | @property 38 | def is_completed(self): 39 | """True if the project is marked as completed and false otherwise""" 40 | return self._content.get('is_completed', False) 41 | 42 | @is_completed.setter 43 | def is_completed(self, value): 44 | if not isinstance(value, bool): 45 | raise TestRailError('input must be a boolean') 46 | self._content['is_completed'] = value 47 | 48 | @property 49 | def name(self): 50 | """The name of the project""" 51 | return self._content.get('name') 52 | 53 | @name.setter 54 | def name(self, value): 55 | if not isinstance(value, str): 56 | raise TestRailError('input must be a string') 57 | self._content['name'] = value 58 | 59 | @property 60 | def show_announcement(self): 61 | """True to show the announcement/description and false otherwise""" 62 | return self._content.get('show_announcement', False) 63 | 64 | @show_announcement.setter 65 | def show_announcement(self, value): 66 | if not isinstance(value, bool): 67 | raise TestRailError('input must be a boolean') 68 | self._content['show_announcement'] = value 69 | 70 | @property 71 | def suite_mode(self): 72 | """The suite mode of the project (1 for single suite mode, 73 | 2 for single suite + baselines, 3 for multiple suites) 74 | (added with TestRail 4.0) 75 | """ 76 | return self._content.get('suite_mode') 77 | 78 | @suite_mode.setter 79 | def suite_mode(self, mode): 80 | if not isinstance(mode, int): 81 | raise TestRailError('input must be an integer') 82 | if mode not in [1, 2, 3]: 83 | raise TestRailError('input must be a 1, 2, or 3') 84 | self._content['suite_mode'] = mode 85 | 86 | @property 87 | def url(self): 88 | """The address/URL of the project in the user interface""" 89 | return self._content.get('url') 90 | 91 | 92 | class ProjectContainer(ContainerIter): 93 | def __init__(self, projects): 94 | super(ProjectContainer, self).__init__(projects) 95 | self._projects = projects 96 | 97 | def __iter__(self): 98 | return iter(self._projects) 99 | 100 | def __len__(self): 101 | return len(self._projects) 102 | 103 | def __getitem__(self, i): 104 | return self._projects[i] 105 | 106 | def completed(self): 107 | return filter(lambda p: p.is_completed is True, self._projects) 108 | 109 | def active(self): 110 | return filter(lambda p: p.is_completed is False, self._projects) 111 | -------------------------------------------------------------------------------- /tests/test_updatecache.py: -------------------------------------------------------------------------------- 1 | from copy import deepcopy 2 | from builtins import dict 3 | from datetime import datetime as dt 4 | 5 | try: 6 | import unittest2 as unittest 7 | except ImportError: 8 | import unittest 9 | 10 | import mock 11 | 12 | from testrail.api import UpdateCache 13 | from testrail.helper import TestRailError 14 | 15 | 16 | class TestUpdateCache(unittest.TestCase): 17 | def setUp(self): 18 | self.mock_cache = { 19 | 0: { 20 | 'ts': dt.now(), 21 | 'value': [ 22 | {'id': 'id00', 'val': 'oldval'}, 23 | {'id': 'id01', 'val': 'oldval'}, 24 | {'id': 'id02', 'val': 'oldval'}, 25 | {'id': 'id03', 'val': 'oldval'}, 26 | {'id': 'id04', 'val': 'oldval'} 27 | ] 28 | }, 29 | 1: { 30 | 'ts': dt.now(), 31 | 'value': [ 32 | {'id': 'id10', 'val': 'oldval'}, 33 | {'id': 'id11', 'val': 'oldval'}, 34 | {'id': 'id12', 'val': 'oldval'}, 35 | {'id': 'id13', 'val': 'oldval'}, 36 | {'id': 'id14', 'val': 'oldval'} 37 | ] 38 | } 39 | } 40 | 41 | def tearDown(self): 42 | pass 43 | 44 | def test_cache_add(self,): 45 | add_cache = deepcopy(self.mock_cache) 46 | add_obj = {'id': 'id15', 'val': 'test_cache_add', 'project_id': 1} 47 | 48 | @UpdateCache(add_cache) 49 | def cache_add_func(): 50 | return add_obj 51 | 52 | cache_add_func() 53 | 54 | self.assertEqual(len(add_cache[0]['value']), 5) 55 | self.assertEqual(len(add_cache[1]['value']), 6) 56 | self.assertEqual(add_cache[1]['value'][-1], add_obj) 57 | 58 | def test_cache_add_when_no_timestamp(self,): 59 | no_ts_cache = deepcopy(self.mock_cache) 60 | no_ts_cache[1]['ts'] = None 61 | no_ts_obj = {'id': 'id11', 'val': 'test_cache_update', 'project_id': 1} 62 | 63 | @UpdateCache(no_ts_cache) 64 | def cache_update_func(): 65 | return no_ts_obj 66 | 67 | cache_update_func() 68 | 69 | self.assertEqual(len(no_ts_cache[0]['value']), 5) 70 | self.assertEqual(len(no_ts_cache[1]['value']), 5) 71 | self.assertEqual(no_ts_cache[1]['value'][1]['val'], 'oldval') 72 | 73 | def test_cache_update(self,): 74 | update_cache = deepcopy(self.mock_cache) 75 | update_obj = {'id': 'id12', 'val': 'test_cache_update', 'project_id': 1} 76 | 77 | @UpdateCache(update_cache) 78 | def cache_update_func(): 79 | return update_obj 80 | 81 | cache_update_func() 82 | 83 | self.assertEqual(len(update_cache[0]['value']), 5) 84 | self.assertEqual(len(update_cache[1]['value']), 5) 85 | self.assertEqual(update_cache[1]['value'][2], update_obj) 86 | 87 | def test_cache_update_when_no_timestamp(self,): 88 | no_ts_cache = deepcopy(self.mock_cache) 89 | no_ts_cache[1]['ts'] = None 90 | no_ts_obj = {'id': 'id15', 'val': 'test_cache_update', 'project_id': 1} 91 | 92 | @UpdateCache(no_ts_cache) 93 | def cache_update_func(): 94 | return no_ts_obj 95 | 96 | cache_update_func() 97 | 98 | self.assertEqual(len(no_ts_cache[0]['value']), 5) 99 | self.assertEqual(len(no_ts_cache[1]['value']), 5) 100 | 101 | def test_cache_delete(self,): 102 | delete_cache = deepcopy(self.mock_cache) 103 | delete_obj = {} 104 | id_to_delete = 'id13' 105 | 106 | @UpdateCache(delete_cache) 107 | def cache_delete_func(val): 108 | return delete_obj 109 | 110 | cache_delete_func(id_to_delete) 111 | 112 | self.assertEqual(len(delete_cache[0]['value']), 5) 113 | self.assertEqual(len(delete_cache[1]['value']), 4) 114 | for project in delete_cache.values(): 115 | for obj in project['value']: 116 | self.assertNotEqual(id_to_delete, obj['id']) 117 | 118 | def test_cache_refresh(self,): 119 | refresh_cache = deepcopy(self.mock_cache) 120 | force_refresh_obj = {} 121 | id_to_delete = 'id23' 122 | 123 | @UpdateCache(refresh_cache) 124 | def cache_refresh_func(val): 125 | return force_refresh_obj 126 | 127 | cache_refresh_func(id_to_delete) 128 | 129 | self.assertEqual(len(refresh_cache[0]['value']), 5) 130 | self.assertEqual(len(refresh_cache[1]['value']), 5) 131 | self.assertTrue(all([x['ts'] is None for x in refresh_cache.values()])) 132 | -------------------------------------------------------------------------------- /testrail/case.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | import sys 3 | 4 | from testrail.base import TestRailBase 5 | from testrail.api import API 6 | from testrail.casetype import CaseType 7 | from testrail.helper import custom_methods, TestRailError 8 | from testrail.milestone import Milestone 9 | from testrail.priority import Priority 10 | from testrail.section import Section 11 | from testrail.suite import Suite 12 | from testrail.template import Template 13 | from testrail.user import User 14 | 15 | if sys.version_info >= (3,0): 16 | unicode = str 17 | 18 | class Case(TestRailBase): 19 | def __init__(self, content=None): 20 | self._content = content or dict() 21 | self.api = API() 22 | self._custom_methods = custom_methods(self._content) 23 | 24 | def __getattr__(self, attr): 25 | if attr in self._custom_methods: 26 | return self._content.get(self._custom_methods[attr]) 27 | raise AttributeError("'{}' object has no attribute '{}'".format( 28 | self.__class__.__name__, attr)) 29 | 30 | def __str__(self): 31 | return self.title 32 | 33 | @property 34 | def created_by(self): 35 | user_id = self._content.get('created_by') 36 | return User(self.api.user_by_id(user_id)) 37 | 38 | @property 39 | def created_on(self): 40 | return datetime.fromtimestamp(int(self._content.get('created_on'))) 41 | 42 | @property 43 | def estimate(self): 44 | return self._content.get('estimate') 45 | 46 | @estimate.setter 47 | def estimate(self, value): 48 | #TODO should have some logic to validate format of timespa 49 | if not isinstance(value, (str, unicode)): 50 | raise TestRailError('input must be a string') 51 | self._content['estimate'] = value 52 | 53 | @property 54 | def estimated_forecast(self): 55 | return self._content.get('estimated_forecast') 56 | 57 | @property 58 | def id(self): 59 | return self._content.get('id') 60 | 61 | @property 62 | def milestone(self): 63 | m = self.api.milestone_with_id(self._content.get('milestone_id')) 64 | return Milestone(m) if m else Milestone() 65 | 66 | @milestone.setter 67 | def milestone(self, value): 68 | if not isinstance(value, Milestone): 69 | raise TestRailError('input must be a Milestone') 70 | self._content['milestone_id'] = value.id 71 | 72 | 73 | @property 74 | def priority(self): 75 | p = self.api.priority_with_id(self._content.get('priority_id')) 76 | return Priority(p) if p else Priority() 77 | 78 | @priority.setter 79 | def priority(self, value): 80 | if not isinstance(value, Priority): 81 | raise TestRailError('input must be a Priority') 82 | self._content['priority_id'] = value.id 83 | 84 | @property 85 | def refs(self): 86 | refs = self._content.get('refs') 87 | return refs.split(',') if refs else list() 88 | 89 | @refs.setter 90 | def refs(self, value): 91 | if not isinstance(value, list): 92 | raise TestRailError('input must be a list') 93 | self._content['refs'] = ','.join(value) 94 | 95 | 96 | @property 97 | def section(self): 98 | s = self.api.section_with_id(self._content.get('section_id')) 99 | return Section(s) if s else Section() 100 | 101 | @section.setter 102 | def section(self, value): 103 | if not isinstance(value, Section): 104 | raise TestRailError('input must be a Section') 105 | self._content['section_id'] = value.id 106 | 107 | @property 108 | def suite(self): 109 | s = self.api.suite_with_id(self._content.get('suite_id')) 110 | return Suite(s) if s else Suite() 111 | 112 | @suite.setter 113 | def suite(self, value): 114 | if not isinstance(value, Suite): 115 | raise TestRailError('input must be a Suite') 116 | self._content['suite_id'] = value.id 117 | 118 | @property 119 | def title(self): 120 | return self._content.get('title') 121 | 122 | @title.setter 123 | def title(self, value): 124 | if not isinstance(value, (str, unicode)): 125 | raise TestRailError('input must be a string') 126 | self._content['title'] = value 127 | 128 | @property 129 | def type(self): 130 | t = self.api.case_type_with_id(self._content.get('type_id')) 131 | return CaseType(t) if t else CaseType() 132 | 133 | @type.setter 134 | def type(self, value): 135 | if not isinstance(value, CaseType): 136 | raise TestRailError('input must be a CaseType') 137 | self._content['type_id'] = value.id 138 | 139 | 140 | @property 141 | def updated_by(self): 142 | user_id = self._content.get('updated_by') 143 | return User(self.api.user_by_id(user_id)) 144 | 145 | @property 146 | def updated_on(self): 147 | return datetime.fromtimestamp(int(self._content.get('updated_on'))) 148 | 149 | @property 150 | def template(self): 151 | # we don't get the template, on needed as a setter 152 | raise NotImplementedError 153 | 154 | @template.setter 155 | def template(self, value): 156 | if not isinstance(value, Template): 157 | raise TestRailError('input must be a Template') 158 | self._content['template_id'] = value.id 159 | 160 | def raw_data(self): 161 | return self._content 162 | -------------------------------------------------------------------------------- /testrail/result.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta 2 | import re 3 | 4 | from testrail.base import TestRailBase 5 | from testrail import api 6 | from testrail.helper import custom_methods, ContainerIter, TestRailError 7 | from testrail.status import Status 8 | from testrail.test import Test 9 | from testrail.user import User 10 | from testrail.helper import testrail_duration_to_timedelta 11 | 12 | 13 | class Result(TestRailBase): 14 | def __init__(self, content=None): 15 | self._content = content or dict() 16 | self.api = api.API() 17 | self._custom_methods = custom_methods(self._content) 18 | 19 | def __getattr__(self, attr): 20 | if attr in self._custom_methods: 21 | return self._content.get(self._custom_methods[attr]) 22 | raise AttributeError("'{}' object has no attribute '{}'".format( 23 | self.__class__.__name__, attr)) 24 | 25 | @property 26 | def assigned_to(self): 27 | user_id = self._content.get('assignedto_id') 28 | return User(self.api.user_with_id(user_id)) if user_id else User() 29 | 30 | @assigned_to.setter 31 | def assigned_to(self, user): 32 | if not isinstance(user, User): 33 | raise TestRailError('input must be a User object') 34 | try: 35 | self.api.user_with_id(user.id) 36 | except TestRailError: 37 | raise TestRailError("User with ID '%s' is not valid" % user.id) 38 | self._content['assignedto_id'] = user.id 39 | 40 | @property 41 | def comment(self): 42 | return self._content.get('comment') 43 | 44 | @comment.setter 45 | def comment(self, value): 46 | if not isinstance(value, str): 47 | raise TestRailError('input must be a string') 48 | self._content['comment'] = value 49 | 50 | @property 51 | def created_by(self): 52 | return User(self.api.user_with_id(self._content.get('created_by'))) 53 | 54 | @property 55 | def created_on(self): 56 | try: 57 | return datetime.fromtimestamp(int(self._content.get('created_on'))) 58 | except TypeError: 59 | return None 60 | 61 | @property 62 | def defects(self): 63 | defects = self._content.get('defects') 64 | return defects.split(',') if defects else list() 65 | 66 | @defects.setter 67 | def defects(self, values): 68 | if not isinstance(values, list): 69 | raise TestRailError('input must be a list of strings') 70 | if not all(map(lambda x, : isinstance(x, str), values)): 71 | raise TestRailError('input must be a list of strings') 72 | if len(values) > 0: 73 | self._content['defects'] = ','.join(values) 74 | else: 75 | self._content['defects'] = None 76 | 77 | @property 78 | def elapsed(self): 79 | duration = self._content.get('elapsed') 80 | if duration is None: 81 | return None 82 | 83 | if isinstance(duration, int): 84 | return timedelta(seconds=duration) 85 | return testrail_duration_to_timedelta(duration) 86 | 87 | @elapsed.setter 88 | def elapsed(self, td): 89 | if not isinstance(td, timedelta): 90 | raise TestRailError('input must be a timedelta') 91 | if td > timedelta(weeks=10): 92 | raise TestRailError('maximum elapsed time is 10 weeks') 93 | self._content['elapsed'] = td.seconds 94 | 95 | @property 96 | def id(self): 97 | return self._content.get('id') 98 | 99 | @property 100 | def status(self): 101 | return Status(self.api.status_with_id(self._content.get('status_id'))) 102 | 103 | @status.setter 104 | def status(self, status_obj): 105 | # TODO: Should I accept string name as well? 106 | if not isinstance(status_obj, Status): 107 | raise TestRailError('input must be a Status') 108 | # verify id is valid 109 | self.api.status_with_id(status_obj.id) 110 | self._content['status_id'] = status_obj.id 111 | 112 | @property 113 | def test(self): 114 | test_id = self._content.get('test_id') 115 | return Test(self.api.test_with_id(test_id)) if test_id else Test() 116 | 117 | @test.setter 118 | def test(self, test_obj): 119 | if not isinstance(test_obj, Test): 120 | raise TestRailError('input must be a Test') 121 | # verify id is valid 122 | self.api.test_with_id( 123 | test_obj._content['id'], test_obj._content['run_id']) 124 | self._content['test_id'] = test_obj.id 125 | 126 | @property 127 | def version(self): 128 | return self._content.get('version') 129 | 130 | @version.setter 131 | def version(self, ver): 132 | if not isinstance(ver, str): 133 | raise TestRailError('input must be a string') 134 | self._content['version'] = ver 135 | 136 | def raw_data(self): 137 | return self._content 138 | 139 | 140 | class ResultContainer(ContainerIter): 141 | def __init__(self, results): 142 | super(ResultContainer, self).__init__(results) 143 | self._results = results 144 | 145 | def blocked(self): 146 | return list(filter(lambda r: r.status.name == "blocked", self._results)) 147 | 148 | def failed(self): 149 | return list(filter(lambda r: r.status.name == "failed", self._results)) 150 | 151 | def latest(self): 152 | return sorted(self._results, key=lambda r: r.created_on)[-1] 153 | 154 | def oldest(self): 155 | return sorted(self._results, key=lambda r: r.created_on)[0] 156 | 157 | def passed(self): 158 | return list(filter(lambda r: r.status.name == "passed", self._results)) 159 | 160 | def retest(self): 161 | return list(filter(lambda r: r.status.name == "retest", self._results)) 162 | 163 | def untested(self): 164 | return list(filter(lambda r: r.status.name == "untested", self._results)) 165 | -------------------------------------------------------------------------------- /testrail/plan.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from testrail.base import TestRailBase 4 | import testrail.entry 5 | from testrail.api import API 6 | from testrail.user import User 7 | from testrail.project import Project 8 | from testrail.milestone import Milestone 9 | from testrail.helper import ContainerIter, TestRailError 10 | 11 | 12 | class Plan(TestRailBase): 13 | def __init__(self, content=None): 14 | self._content = content or dict() 15 | self.api = API() 16 | 17 | @property 18 | def assigned_to(self): 19 | return User(self.api.user_with_id(self._content.get('assignedto_id'))) 20 | 21 | @property 22 | def blocked_count(self): 23 | return self._content.get('blocked_count') 24 | 25 | @property 26 | def completed_on(self): 27 | try: 28 | return datetime.fromtimestamp(int( 29 | self._content.get('completed_on'))) 30 | except TypeError: 31 | return None 32 | 33 | @property 34 | def created_on(self): 35 | try: 36 | return datetime.fromtimestamp(int(self._content.get('created_on'))) 37 | except TypeError: 38 | return None 39 | 40 | @property 41 | def created_by(self): 42 | return User(self.api.user_with_id(self._content.get('created_by'))) 43 | 44 | @property 45 | def custom_status_count(self): 46 | return self._content.get('custom_status_count') 47 | 48 | @property 49 | def description(self): 50 | return self._content.get('description') 51 | 52 | @description.setter 53 | def description(self, value): 54 | if not isinstance(value, str): 55 | raise TestRailError('input must be a string') 56 | self._content['description'] = value 57 | 58 | @property 59 | def entries(self): 60 | # ToDo convert entries to run objects 61 | if not self._content.get('entries'): 62 | self._content['entries'] = self.api.plan_with_id( 63 | self.id, with_entries=True).get('entries') 64 | return list(map(testrail.entry.Entry, self._content.get('entries'))) 65 | 66 | @property 67 | def failed_count(self): 68 | return self._content.get('failed_count') 69 | 70 | @property 71 | def id(self): 72 | return self._content.get('id') 73 | 74 | @property 75 | def is_completed(self): 76 | return self._content.get('is_completed') 77 | 78 | @property 79 | def milestone(self): 80 | milestone_id = self._content.get('milestone_id') 81 | project_id = self._content.get('project_id') 82 | if milestone_id is None: 83 | return Milestone() 84 | return Milestone(self.api.milestone_with_id(milestone_id, project_id)) 85 | 86 | @milestone.setter 87 | def milestone(self, v): 88 | if not isinstance(v, Milestone): 89 | raise TestRailError('input must be a Milestone') 90 | self._content['milestone_id'] = v.id 91 | 92 | @property 93 | def name(self): 94 | return self._content.get('name') 95 | 96 | @name.setter 97 | def name(self, value): 98 | if not isinstance(value, str): 99 | raise TestRailError('input must be a string') 100 | self._content['name'] = value 101 | 102 | @property 103 | def passed_count(self): 104 | return self._content.get('passed_count') 105 | 106 | @property 107 | def project(self): 108 | return Project( 109 | self.api.project_with_id(self._content.get('project_id'))) 110 | 111 | @project.setter 112 | def project(self, value): 113 | if not isinstance(value, Project): 114 | raise TestRailError('input must be a Project') 115 | self.api.project_with_id(value.id) # verify project is valid 116 | self._content['project_id'] = value.id 117 | 118 | @property 119 | def project_id(self): 120 | return self._content.get('project_id') 121 | 122 | @property 123 | def retest_count(self): 124 | return self._content.get('retest_count') 125 | 126 | @property 127 | def untested_count(self): 128 | return self._content.get('untested_count') 129 | 130 | @property 131 | def url(self): 132 | return self._content.get('url') 133 | 134 | def raw_data(self): 135 | return self._content 136 | 137 | 138 | class PlanContainer(ContainerIter): 139 | def __init__(self, plans): 140 | super(PlanContainer, self).__init__(plans) 141 | self._plans = plans 142 | 143 | def completed(self): 144 | return list(filter(lambda p: p.is_completed is True, self._plans)) 145 | 146 | def active(self): 147 | return list(filter(lambda p: p.is_completed is False, self._plans)) 148 | 149 | def created_after(self, dt): 150 | if not isinstance(dt, datetime): 151 | raise TestRailError("Must pass in a datetime object") 152 | return list(filter(lambda p: p.created_on > dt, self._plans)) 153 | 154 | def created_before(self, dt): 155 | if not isinstance(dt, datetime): 156 | raise TestRailError("Must pass in a datetime object") 157 | return list(filter(lambda p: p.created_on < dt, self._plans)) 158 | 159 | def created_by(self, user): 160 | if not isinstance(user, User): 161 | raise TestRailError("Must pass in a User object") 162 | return list(filter(lambda p: p.created_by.id == user.id, self._plans)) 163 | 164 | def latest(self): 165 | self._plans.sort(key=lambda x: x.created_on) 166 | return self._plans[-1] 167 | 168 | def oldest(self): 169 | self._plans.sort(key=lambda x: x.created_on) 170 | return self._plans[0] 171 | 172 | def name(self, name): 173 | if not isinstance(name, str): 174 | raise TestRailError("Must pass in a string") 175 | 176 | def comp_func(p): 177 | return p.name.lower() == name.lower() 178 | 179 | try: 180 | return list(filter(comp_func, self._plans)).pop(0) 181 | except IndexError: 182 | raise TestRailError("Plan with name '%s' was not found" % name) 183 | -------------------------------------------------------------------------------- /testrail/run.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from testrail.base import TestRailBase 4 | from testrail.api import API 5 | from testrail.helper import ContainerIter, TestRailError 6 | from testrail.milestone import Milestone 7 | import testrail.plan 8 | from testrail.project import Project 9 | from testrail.user import User 10 | from testrail.case import Case 11 | from testrail.suite import Suite 12 | from testrail.helper import TestRailError 13 | 14 | 15 | class Run(TestRailBase): 16 | def __init__(self, content=None): 17 | self._content = content or dict() 18 | self.api = API() 19 | 20 | @property 21 | def assigned_to(self): 22 | return User(self.api.user_with_id(self._content.get('assignedto_id'))) 23 | 24 | @property 25 | def blocked_count(self): 26 | return self._content.get('blocked_count') 27 | 28 | @property 29 | def cases(self): 30 | if self._content.get('case_ids'): 31 | cases = list(map(self.api.case_with_id, self._content.get('case_ids'))) 32 | return list(map(Case, cases)) 33 | else: 34 | return list() 35 | 36 | 37 | @cases.setter 38 | def cases(self, cases): 39 | exc_msg = 'cases must be set to None or a container of Case objects' 40 | 41 | if cases is None: 42 | self._content['case_ids'] = None 43 | 44 | elif not isinstance(cases, (list, tuple)): 45 | raise TestRailError(exc_msg) 46 | 47 | elif not all([isinstance(case, Case) for case in cases]): 48 | raise TestRailError(exc_msg) 49 | 50 | else: 51 | self._content['case_ids'] = [case.id for case in cases] 52 | 53 | @property 54 | def completed_on(self): 55 | try: 56 | return datetime.fromtimestamp( 57 | int(self._content.get('completed_on'))) 58 | except TypeError: 59 | return None 60 | 61 | @property 62 | def config(self): 63 | return self._content.get('config') 64 | 65 | @property 66 | def config_ids(self): 67 | return self._content.get('config_ids') 68 | 69 | @property 70 | def created_by(self): 71 | return User(self.api.user_with_id(self._content.get('created_by'))) 72 | 73 | @property 74 | def created_on(self): 75 | try: 76 | return datetime.fromtimestamp(int(self._content.get('created_on'))) 77 | except TypeError: 78 | return None 79 | 80 | @property 81 | def custom_status_count(self): 82 | return self._content.get('custom_status_count') 83 | 84 | @property 85 | def description(self): 86 | return self._content.get('description') 87 | 88 | @property 89 | def failed_count(self): 90 | return self._content.get('failed_count') 91 | 92 | @property 93 | def id(self): 94 | return self._content.get('id') 95 | 96 | @property 97 | def include_all(self): 98 | return self._content.get('include_all') 99 | 100 | @include_all.setter 101 | def include_all(self, value): 102 | if not isinstance(value, bool): 103 | raise TestRailError('include_all must be a boolean') 104 | self._content['include_all'] = value 105 | 106 | @property 107 | def is_completed(self): 108 | return self._content.get('is_completed') 109 | 110 | @property 111 | def milestone(self): 112 | milestone_id = self._content.get('milestone_id') 113 | if milestone_id is None: 114 | return Milestone() 115 | return Milestone(self.api.milestone_with_id(milestone_id, 116 | self._content.get('project_id'))) 117 | 118 | @milestone.setter 119 | def milestone(self, value): 120 | if not isinstance(value, Milestone): 121 | raise TestRailError('input must be a Milestone') 122 | self.api.milestone_with_id(value.id) # verify milestone is valid 123 | self._content['milestone_id'] = value.id 124 | 125 | @property 126 | def name(self): 127 | return self._content.get('name') 128 | 129 | @name.setter 130 | def name(self, value): 131 | if not isinstance(value, str): 132 | raise TestRailError('input must be a string') 133 | self._content['name'] = value 134 | 135 | @property 136 | def passed_count(self): 137 | return self._content.get('passed_count') 138 | 139 | @property 140 | def plan(self): 141 | return testrail.plan.Plan( 142 | self.api.plan_with_id(self._content.get('plan_id'))) 143 | 144 | @property 145 | def project(self): 146 | return Project( 147 | self.api.project_with_id(self._content.get('project_id'))) 148 | 149 | @project.setter 150 | def project(self, value): 151 | if not isinstance(value, Project): 152 | raise TestRailError('input must be a Project') 153 | self.api.project_with_id(value.id) # verify project is valid 154 | self._content['project_id'] = value.id 155 | 156 | @property 157 | def project_id(self): 158 | return self._content.get('project_id') 159 | 160 | @property 161 | def retest_count(self): 162 | return self._content.get('retest_count') 163 | 164 | @property 165 | def suite(self): 166 | return Suite( 167 | self.api.suite_with_id(self._content.get('suite_id'))) 168 | 169 | @suite.setter 170 | def suite(self, value): 171 | if not isinstance(value, Suite): 172 | raise TestRailError('input must be a Suite') 173 | self.api.suite_with_id(value.id) # verify suite is valid 174 | self._content['suite_id'] = value.id 175 | 176 | @property 177 | def untested_count(self): 178 | return self._content.get('untested_count') 179 | 180 | @property 181 | def url(self): 182 | return self._content.get('url') 183 | 184 | def raw_data(self): 185 | return self._content 186 | 187 | 188 | class RunContainer(ContainerIter): 189 | def __init__(self, runs): 190 | super(RunContainer, self).__init__(runs) 191 | self._runs = runs 192 | 193 | def latest(self): 194 | self._runs.sort(key=lambda x: x.created_on) 195 | return self._runs[-1] 196 | 197 | def oldest(self): 198 | self._runs.sort(key=lambda x: x.created_on) 199 | return self._runs[0] 200 | 201 | def completed(self): 202 | return list(filter(lambda m: m.is_completed is True, self._runs)) 203 | 204 | def active(self): 205 | return list(filter(lambda m: m.is_completed is False, self._runs)) 206 | -------------------------------------------------------------------------------- /tests/test_project.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | try: 3 | import unittest2 as unittest 4 | except ImportError: 5 | import unittest 6 | 7 | from testrail.helper import TestRailError 8 | from testrail.project import Project, ProjectContainer 9 | 10 | 11 | class TestProject(unittest.TestCase): 12 | def setUp(self): 13 | self.mock_data_completed = { 14 | "announcement": "..", 15 | "completed_on": 1453504099, 16 | "id": 1, 17 | "is_completed": True, 18 | "name": "Project1", 19 | "show_announcement": True, 20 | "url": "http:///index.php?/projects/overview/1", 21 | "suite_mode": 3 22 | } 23 | 24 | self.mock_data_incomplete = { 25 | "announcement": "..", 26 | "completed_on": None, 27 | "id": 1, 28 | "is_completed": False, 29 | "name": "Project1", 30 | "show_announcement": True, 31 | "url": "http:///index.php?/projects/overview/1", 32 | "suite_mode": 3 33 | } 34 | self.project = Project(self.mock_data_completed) 35 | 36 | def test_get_announcement_type(self): 37 | self.assertEqual(type(self.project.announcement), str) 38 | 39 | def test_get_announcement(self): 40 | self.assertEqual(self.project.announcement, '..') 41 | 42 | def test_set_announcement(self): 43 | msg = 'test announcement' 44 | self.project.announcement = msg 45 | self.assertEqual(self.project.announcement, msg) 46 | 47 | def test_set_announcement_invalid(self): 48 | with self.assertRaises(TestRailError) as e: 49 | self.project.announcement = 23 50 | self.assertEqual(str(e.exception), 'input must be a string') 51 | 52 | def test_get_completed_on_type(self): 53 | self.assertEqual(type(self.project.completed_on), datetime) 54 | 55 | def test_get_completed_on(self): 56 | date_obj = datetime.fromtimestamp(1453504099) 57 | self.assertEqual(self.project.completed_on, date_obj) 58 | 59 | def test_get_completed_on_not_completed(self): 60 | project = Project(self.mock_data_incomplete) 61 | self.assertEqual(project.completed_on, None) 62 | 63 | def test_get_id_type(self): 64 | self.assertEqual(type(self.project.id), int) 65 | 66 | def test_get_id(self): 67 | self.assertEqual(self.project.id, 1) 68 | 69 | def test_get_is_completed_type(self): 70 | self.assertEqual(type(self.project.is_completed), bool) 71 | 72 | def test_get_is_completed(self): 73 | self.assertEqual(self.project.is_completed, True) 74 | 75 | def test_set_is_completed_type(self): 76 | with self.assertRaises(TestRailError) as e: 77 | self.project.is_completed = 1 78 | self.assertEqual(str(e.exception), 'input must be a boolean') 79 | 80 | def test_set_is_completed(self): 81 | self.project.is_completed = False 82 | self.assertEqual(self.project.is_completed, False) 83 | self.assertEqual(self.project._content['is_completed'], False) 84 | 85 | def test_get_name_type(self): 86 | self.assertEqual(type(self.project.name), str) 87 | 88 | def test_get_name(self): 89 | self.assertEqual(self.project.name, 'Project1') 90 | 91 | def test_set_name(self): 92 | name = 'my new project' 93 | self.project.name = name 94 | self.assertEqual(self.project.name, name) 95 | self.assertEqual(self.project._content['name'], name) 96 | 97 | def test_set_name_invalid(self): 98 | with self.assertRaises(TestRailError) as e: 99 | self.project.name = 394 100 | self.assertEqual(str(e.exception), 'input must be a string') 101 | 102 | def test_get_show_announcement(self): 103 | self.assertEqual(self.project.show_announcement, True) 104 | 105 | def test_set_show_announcement(self): 106 | self.project.show_announcement = False 107 | self.assertEqual(self.project.show_announcement, False) 108 | self.assertEqual(self.project._content['show_announcement'], False) 109 | 110 | def test_set_show_announcement_invalid(self): 111 | with self.assertRaises(TestRailError) as e: 112 | self.project.show_announcement = 1 113 | self.assertEqual(str(e.exception), 'input must be a boolean') 114 | 115 | def test_get_suite_mode(self): 116 | self.assertEqual(self.project.suite_mode, 3) 117 | 118 | def test_set_suite_mode(self): 119 | for i in [1, 2, 3]: 120 | self.project.suite_mode = i 121 | self.assertEqual(self.project.suite_mode, i) 122 | self.assertEqual(self.project._content['suite_mode'], i) 123 | 124 | def test_set_suite_mode_invalid_type(self): 125 | with self.assertRaises(TestRailError) as e: 126 | self.project.suite_mode = '2' 127 | self.assertEqual(str(e.exception), 'input must be an integer') 128 | 129 | def test_suite_mode_invalid_num(self): 130 | for i in [0, 4]: 131 | with self.assertRaises(TestRailError) as e: 132 | self.project.suite_mode = i 133 | self.assertEqual(str(e.exception), 'input must be a 1, 2, or 3') 134 | 135 | def test_get_url(self): 136 | self.assertEqual( 137 | self.project.url, 'http:///index.php?/projects/overview/1') 138 | 139 | 140 | class TestProjectContainer(unittest.TestCase): 141 | def setUp(self): 142 | self.mock_data = [ 143 | { 144 | "announcement": "..", 145 | "completed_on": "1453504099", 146 | "id": 1, 147 | "is_completed": True, 148 | "name": "Project1", 149 | "show_announcement": True, 150 | "url": "http:///index.php?/projects/overview/1", 151 | "suite_mode": 3 152 | }, 153 | { 154 | "announcement": "..", 155 | "completed_on": None, 156 | "id": 2, 157 | "is_completed": False, 158 | "name": "Project1", 159 | "show_announcement": True, 160 | "url": "http:///index.php?/projects/overview/2", 161 | "suite_mode": 3 162 | } 163 | ] 164 | 165 | ptemp = list() 166 | for pdata in self.mock_data: 167 | ptemp.append(Project(pdata)) 168 | self.projects = ProjectContainer(ptemp) 169 | 170 | def test_len(self): 171 | self.assertEqual(len(self.projects), 2) 172 | 173 | def test_completed(self): 174 | self.assertEqual(len(list(self.projects.completed())), 1) 175 | self.assertEqual(list(self.projects.completed())[0].id, 1) 176 | 177 | def test_active(self): 178 | self.assertEqual(len(list(self.projects.active())), 1) 179 | self.assertEqual(list(self.projects.active())[0].id, 2) 180 | 181 | def test_getitem(self): 182 | self.assertEqual(self.projects[0].id, 1) 183 | -------------------------------------------------------------------------------- /tests/test_suite.py: -------------------------------------------------------------------------------- 1 | import copy 2 | from datetime import datetime 3 | import mock 4 | try: 5 | import unittest2 as unittest 6 | except ImportError: 7 | import unittest 8 | 9 | from testrail.helper import TestRailError 10 | from testrail.project import Project 11 | from testrail.suite import Suite 12 | 13 | 14 | class TestSuite(unittest.TestCase): 15 | def setUp(self): 16 | 17 | self.mock_suite_data = { 18 | "description": "suite description", 19 | "id": 1, 20 | "name": "Setup & Installation", 21 | "project_id": 1, 22 | "url": "http:///index.php?/suites/view/1", 23 | "is_baseline": False, 24 | "is_completed": True, 25 | "is_master": True, 26 | "completed_on": 1453504099 27 | } 28 | 29 | self.mock_project_data = [ 30 | { 31 | "announcement": "..", 32 | "completed_on": 1653504099, 33 | "id": 1, 34 | "is_completed": False, 35 | "name": "Project1", 36 | "show_announcement": True, 37 | "url": "http:///index.php?/projects/overview/1", 38 | "suite_mode": 3 39 | }, 40 | { 41 | "announcement": "..", 42 | "completed_on": 1453504099, 43 | "id": 2, 44 | "is_completed": True, 45 | "name": "Project2", 46 | "show_announcement": True, 47 | "url": "http:///index.php?/projects/overview/1", 48 | "suite_mode": 3 49 | } 50 | ] 51 | 52 | self.suite = Suite(self.mock_suite_data) 53 | 54 | def test_get_completed_on_type(self): 55 | self.assertEqual(type(self.suite.completed_on), datetime) 56 | 57 | def test_get_completed_on(self): 58 | date_obj = datetime.fromtimestamp(1453504099) 59 | self.assertEqual(self.suite.completed_on, date_obj) 60 | 61 | def test_get_completed_on_not_completed(self): 62 | self.suite._content['completed_on'] = None 63 | self.suite._content['is_completed'] = False 64 | self.assertEqual(self.suite.completed_on, None) 65 | 66 | def test_get_id_type(self): 67 | self.assertEqual(type(self.suite.id), int) 68 | 69 | def test_get_id(self): 70 | self.assertEqual(self.suite.id, 1) 71 | 72 | def test_get_is_baseline_type(self): 73 | self.assertEqual(type(self.suite.is_baseline), bool) 74 | 75 | def test_get_is_baseline(self): 76 | self.assertEqual(self.suite.is_baseline, False) 77 | 78 | def test_get_is_completed_type(self): 79 | self.assertEqual(type(self.suite.is_completed), bool) 80 | 81 | def test_get_is_completed(self): 82 | self.assertEqual(self.suite.is_completed, True) 83 | 84 | def test_get_is_master_type(self): 85 | self.assertEqual(type(self.suite.is_master), bool) 86 | 87 | def test_get_is_master(self): 88 | self.assertEqual(self.suite.is_master, True) 89 | 90 | def test_get_description_type(self): 91 | self.assertEqual(type(self.suite.description), str) 92 | 93 | def test_get_description(self): 94 | self.assertEqual(self.suite.description, 'suite description') 95 | 96 | def test_set_description(self): 97 | description = 'new description' 98 | self.suite.description = description 99 | self.assertEqual(self.suite.description, description) 100 | self.assertEqual(self.suite._content['description'], description) 101 | 102 | def test_set_description_invalid_type(self): 103 | with self.assertRaises(TestRailError) as e: 104 | self.suite.description = 394 105 | self.assertEqual(str(e.exception), 'input must be a string') 106 | 107 | def test_get_name_type(self): 108 | self.assertEqual(type(self.suite.name), str) 109 | 110 | def test_get_name(self): 111 | self.assertEqual(self.suite.name, 'Setup & Installation') 112 | 113 | def test_set_name(self): 114 | name = 'my new suite' 115 | self.suite.name = name 116 | self.assertEqual(self.suite.name, name) 117 | self.assertEqual(self.suite._content['name'], name) 118 | 119 | def test_set_name_invalid_type(self): 120 | with self.assertRaises(TestRailError) as e: 121 | self.suite.name = 394 122 | self.assertEqual(str(e.exception), 'input must be a string') 123 | 124 | def test_get_url_type(self): 125 | self.assertEqual(type(self.suite.url), str) 126 | 127 | def test_get_url(self): 128 | self.assertEqual( 129 | self.suite.url, 'http:///index.php?/suites/view/1') 130 | 131 | @mock.patch('testrail.api.requests.get') 132 | def test_get_project_type(self, mock_get): 133 | mock_response = mock.Mock() 134 | mock_response.json.return_value = copy.deepcopy(self.mock_project_data) 135 | mock_response.status_code = 200 136 | mock_get.return_value = mock_response 137 | self.assertEqual(type(self.suite.project), Project) 138 | 139 | @mock.patch('testrail.api.requests.get') 140 | def test_get_project(self, mock_get): 141 | mock_response = mock.Mock() 142 | mock_response.json.return_value = copy.deepcopy(self.mock_project_data) 143 | mock_response.status_code = 200 144 | mock_get.return_value = mock_response 145 | self.assertEqual(self.suite.project.id, 1) 146 | 147 | @mock.patch('testrail.api.requests.get') 148 | def test_get_project_invalid_id(self, mock_get): 149 | mock_response = mock.Mock() 150 | mock_response.json.return_value = copy.deepcopy(self.mock_project_data) 151 | mock_response.status_code = 200 152 | mock_get.return_value = mock_response 153 | self.suite._content['project_id'] = 200 154 | with self.assertRaises(TestRailError) as e: 155 | self.suite.project 156 | self.assertEqual(str(e.exception), "Project ID '200' was not found") 157 | 158 | @mock.patch('testrail.api.API._refresh') 159 | @mock.patch('testrail.api.requests.get') 160 | def test_set_project(self, mock_get, refresh_mock): 161 | refresh_mock.return_value = True 162 | mock_response = mock.Mock() 163 | mock_response.json.return_value = copy.deepcopy(self.mock_project_data) 164 | mock_response.status_code = 200 165 | mock_get.return_value = mock_response 166 | self.assertEqual(self.suite.project.id, 1) 167 | self.suite.project = Project(self.mock_project_data[1]) 168 | self.assertEqual(self.suite._content['project_id'], 2) 169 | self.assertEqual(self.suite.project.id, 2) 170 | 171 | def test_set_project_invalid_type(self): 172 | with self.assertRaises(TestRailError) as e: 173 | self.suite.project = 2 174 | self.assertEqual(str(e.exception), 'input must be a Project') 175 | 176 | @mock.patch('testrail.api.requests.get') 177 | def test_set_project_invalid_project(self, mock_get): 178 | mock_response = mock.Mock() 179 | mock_response.json.return_value = copy.deepcopy(self.mock_project_data) 180 | mock_response.status_code = 200 181 | mock_get.return_value = mock_response 182 | project = Project() 183 | project._content['id'] = 5 184 | with self.assertRaises(TestRailError) as e: 185 | self.suite.project = project 186 | self.assertEqual(str(e.exception), 187 | "Project ID '5' was not found") 188 | 189 | @mock.patch('testrail.api.requests.get') 190 | def test_set_project_empty_project(self, mock_get): 191 | mock_response = mock.Mock() 192 | mock_response.json.return_value = copy.deepcopy(self.mock_project_data) 193 | mock_response.status_code = 200 194 | mock_get.return_value = mock_response 195 | with self.assertRaises(TestRailError) as e: 196 | self.suite.project = Project() 197 | self.assertEqual(str(e.exception), 198 | "Project ID 'None' was not found") 199 | 200 | def test_raw_data(self): 201 | self.assertEqual(self.suite.raw_data(), self.mock_suite_data) 202 | 203 | def test_raw_data_type(self): 204 | self.assertEqual(type(self.suite.raw_data()), dict) 205 | -------------------------------------------------------------------------------- /tests/test_milestone.py: -------------------------------------------------------------------------------- 1 | import copy 2 | from datetime import datetime 3 | import mock 4 | try: 5 | import unittest2 as unittest 6 | except ImportError: 7 | import unittest 8 | 9 | from testrail.helper import TestRailError 10 | from testrail.milestone import Milestone 11 | from testrail.project import Project 12 | 13 | 14 | class TestCaseType(unittest.TestCase): 15 | def setUp(self): 16 | self.mock_milestone_data = { 17 | "completed_on": 1389968184, 18 | "description": "foo", 19 | "due_on": 1391968184, 20 | "id": 1, 21 | "is_completed": True, 22 | "name": "Release 1.5", 23 | "project_id": 1, 24 | "url": "http:///index.php?/milestones/view/1" 25 | } 26 | 27 | self.mock_project_data = [ 28 | { 29 | "announcement": "..", 30 | "completed_on": 1453504099, 31 | "id": 1, 32 | "is_completed": True, 33 | "name": "Project1", 34 | "show_announcement": True, 35 | "url": "http:///index.php?/projects/overview/1", 36 | "suite_mode": 3 37 | }, 38 | { 39 | "announcement": "..", 40 | "completed_on": 1453504099, 41 | "id": 2, 42 | "is_completed": True, 43 | "name": "Project2", 44 | "show_announcement": True, 45 | "url": "http:///index.php?/projects/overview/1", 46 | "suite_mode": 3 47 | } 48 | ] 49 | 50 | self.milestone = Milestone(self.mock_milestone_data) 51 | 52 | def test_get_completed_on_type(self): 53 | self.assertEqual(type(self.milestone.completed_on), datetime) 54 | 55 | def test_get_completed_on(self): 56 | date_obj = datetime.fromtimestamp(1389968184) 57 | self.assertEqual(self.milestone.completed_on, date_obj) 58 | 59 | def test_get_completed_on_not_completed(self): 60 | milestone = Milestone({}) 61 | self.assertEqual(milestone.completed_on, None) 62 | 63 | def test_get_description_type(self): 64 | self.assertEqual(type(self.milestone.description), str) 65 | 66 | def test_get_description(self): 67 | self.assertEqual(self.milestone.description, 'foo') 68 | 69 | def test_get_description_none(self): 70 | self.milestone._content['description'] = None 71 | self.assertEqual(self.milestone.description, None) 72 | 73 | def test_set_description_invalid_type(self): 74 | with self.assertRaises(TestRailError) as e: 75 | self.milestone.description = 2 76 | self.assertEqual(str(e.exception), 'input must be string or None') 77 | 78 | def test_set_description(self): 79 | self.milestone.description = 'bar' 80 | self.assertEqual(self.milestone._content['description'], 'bar') 81 | 82 | def test_set_description_none(self): 83 | self.milestone.description = None 84 | self.assertEqual(self.milestone._content['description'], None) 85 | 86 | def test_get_due_on_type(self): 87 | self.assertEqual(type(self.milestone.due_on), datetime) 88 | 89 | def test_get_due_on(self): 90 | date_obj = datetime.fromtimestamp(1391968184) 91 | self.assertEqual(self.milestone.due_on, date_obj) 92 | 93 | def test_get_due_on_none(self): 94 | milestone = Milestone({}) 95 | self.assertEqual(milestone.due_on, None) 96 | 97 | def test_set_due_on_invalid_type(self): 98 | with self.assertRaises(TestRailError) as e: 99 | self.milestone.due_on = 'friday' 100 | self.assertEqual(str(e.exception), 'input must be a datetime or None') 101 | 102 | def test_set_due_on(self): 103 | ts = 1392468184 104 | self.milestone.due_on = datetime.fromtimestamp(ts) 105 | self.assertEqual(self.milestone._content['due_on'], ts) 106 | 107 | def test_set_due_on_none(self): 108 | self.milestone.due_on = None 109 | self.assertEqual(self.milestone._content['due_on'], None) 110 | 111 | def test_get_id_type(self): 112 | self.assertEqual(type(self.milestone.id), int) 113 | 114 | def test_get_id(self): 115 | self.assertEqual(self.milestone.id, 1) 116 | 117 | def test_get_is_completed_type(self): 118 | self.assertEqual(type(self.milestone.is_completed), bool) 119 | 120 | def test_get_is_completed(self): 121 | self.assertEqual(self.milestone.is_completed, True) 122 | 123 | def test_set_is_completed_type(self): 124 | with self.assertRaises(TestRailError) as e: 125 | self.milestone.is_completed = 1 126 | self.assertEqual(str(e.exception), 'input must be a boolean') 127 | 128 | def test_set_is_completed(self): 129 | self.milestone.is_completed = False 130 | self.assertEqual(self.milestone._content['is_completed'], False) 131 | 132 | def test_get_name_type(self): 133 | self.assertEqual(type(self.milestone.name), str) 134 | 135 | def test_get_name(self): 136 | self.assertEqual(self.milestone.name, 'Release 1.5') 137 | 138 | def test_set_name(self): 139 | name = 'new milestone' 140 | self.milestone.name = name 141 | self.assertEqual(self.milestone._content['name'], name) 142 | 143 | def test_set_name_invalid(self): 144 | with self.assertRaises(TestRailError) as e: 145 | self.milestone.name = 394 146 | self.assertEqual(str(e.exception), 'input must be a string') 147 | 148 | @mock.patch('testrail.api.requests.get') 149 | def test_get_project_type(self, mock_get): 150 | mock_response = mock.Mock() 151 | mock_response.json.return_value = copy.deepcopy(self.mock_project_data) 152 | mock_response.status_code = 200 153 | mock_get.return_value = mock_response 154 | self.assertEqual(type(self.milestone.project), Project) 155 | 156 | @mock.patch('testrail.api.requests.get') 157 | def test_get_project(self, mock_get): 158 | mock_response = mock.Mock() 159 | mock_response.json.return_value = copy.deepcopy(self.mock_project_data) 160 | mock_response.status_code = 200 161 | mock_get.return_value = mock_response 162 | self.assertEqual(self.milestone.project.id, 1) 163 | 164 | @mock.patch('testrail.api.requests.get') 165 | def test_get_project_invalid_id(self, mock_get): 166 | mock_response = mock.Mock() 167 | mock_response.json.return_value = copy.deepcopy(self.mock_project_data) 168 | mock_response.status_code = 200 169 | mock_get.return_value = mock_response 170 | self.milestone._content['project_id'] = 200 171 | with self.assertRaises(TestRailError) as e: 172 | self.milestone.project 173 | self.assertEqual(str(e.exception), "Project ID '200' was not found") 174 | 175 | @mock.patch('testrail.api.requests.get') 176 | def test_set_project(self, mock_get): 177 | mock_response = mock.Mock() 178 | mock_response.json.return_value = copy.deepcopy(self.mock_project_data) 179 | mock_response.status_code = 200 180 | mock_get.return_value = mock_response 181 | self.assertEqual(self.milestone.project.id, 1) 182 | self.milestone.project = Project(self.mock_project_data[1]) 183 | self.assertEqual(self.milestone._content['project_id'], 2) 184 | self.assertEqual(self.milestone.project.id, 2) 185 | 186 | def test_set_project_invalid_type(self): 187 | with self.assertRaises(TestRailError) as e: 188 | self.milestone.project = 2 189 | self.assertEqual(str(e.exception), 'input must be a Project') 190 | 191 | @mock.patch('testrail.api.requests.get') 192 | def test_set_project_invalid_project(self, mock_get): 193 | mock_response = mock.Mock() 194 | mock_response.json.return_value = copy.deepcopy(self.mock_project_data) 195 | mock_response.status_code = 200 196 | mock_get.return_value = mock_response 197 | project = Project() 198 | project._content['id'] = 5 199 | with self.assertRaises(TestRailError) as e: 200 | self.milestone.project = project 201 | self.assertEqual(str(e.exception), 202 | "Project ID '5' was not found") 203 | 204 | @mock.patch('testrail.api.requests.get') 205 | def test_set_project_empty_project(self, mock_get): 206 | mock_response = mock.Mock() 207 | mock_response.json.return_value = copy.deepcopy(self.mock_project_data) 208 | mock_response.status_code = 200 209 | mock_get.return_value = mock_response 210 | with self.assertRaises(TestRailError) as e: 211 | self.milestone.project = Project() 212 | self.assertEqual(str(e.exception), 213 | "Project ID 'None' was not found") 214 | 215 | def test_get_url_type(self): 216 | self.assertEqual(type(self.milestone.url), str) 217 | 218 | def test_get_url(self): 219 | self.assertEqual(self.milestone.url, 220 | 'http:///index.php?/milestones/view/1') 221 | 222 | def test_raw_data(self): 223 | self.assertEqual(self.milestone.raw_data(), self.mock_milestone_data) 224 | -------------------------------------------------------------------------------- /tests/test_section.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import mock 3 | try: 4 | import unittest2 as unittest 5 | except ImportError: 6 | import unittest 7 | 8 | from testrail.helper import TestRailError 9 | from testrail.section import Section 10 | from testrail.suite import Suite 11 | 12 | 13 | class TestSuite(unittest.TestCase): 14 | def setUp(self): 15 | self.mock_suite_data = [ 16 | { 17 | "description": "suite description", 18 | "id": 1, 19 | "name": "Setup & Installation", 20 | "project_id": 1, 21 | "url": "http:///index.php?/suites/view/1", 22 | "is_baseline": False, 23 | "is_completed": True, 24 | "is_master": True, 25 | "completed_on": 1453504099 26 | }, 27 | { 28 | "description": "suite description 2", 29 | "id": 2, 30 | "name": "Setup & Installation", 31 | "project_id": 1, 32 | "url": "http:///index.php?/suites/view/1", 33 | "is_baseline": False, 34 | "is_completed": False, 35 | "is_master": True, 36 | "completed_on": None 37 | }, 38 | ] 39 | self.mock_section_data = [ 40 | { 41 | "depth": 0, 42 | "description": 'Some description', 43 | "display_order": 1, 44 | "id": 1, 45 | "name": "Prerequisites", 46 | "parent_id": None, 47 | "suite_id": 1 48 | }, 49 | { 50 | "depth": 1, 51 | "description": 'some words', 52 | "display_order": 1, 53 | "id": 2, 54 | "name": "Prerequisites2", 55 | "parent_id": 1, 56 | "suite_id": 1 57 | } 58 | ] 59 | 60 | self.mock_project_data = [ 61 | { 62 | "announcement": "..", 63 | "completed_on": 1653504099, 64 | "id": 1, 65 | "is_completed": False, 66 | "name": "Project1", 67 | "show_announcement": True, 68 | "url": "http:///index.php?/projects/overview/1", 69 | "suite_mode": 3 70 | }, 71 | { 72 | "announcement": "..", 73 | "completed_on": 1453504099, 74 | "id": 2, 75 | "is_completed": True, 76 | "name": "Project2", 77 | "show_announcement": True, 78 | "url": "http:///index.php?/projects/overview/1", 79 | "suite_mode": 3 80 | } 81 | ] 82 | self.section = Section(self.mock_section_data[1]) 83 | 84 | def test_get_id_type(self): 85 | self.assertEqual(type(self.section.id), int) 86 | 87 | def test_get_id(self): 88 | self.assertEqual(self.section.id, 2) 89 | 90 | def test_get_depth_type(self): 91 | self.assertEqual(type(self.section.depth), int) 92 | 93 | def test_get_depth(self): 94 | self.assertEqual(self.section.depth, 1) 95 | 96 | def test_get_display_order_type(self): 97 | self.assertEqual(type(self.section.display_order), int) 98 | 99 | def test_get_display_order(self): 100 | self.assertEqual(self.section.display_order, 1) 101 | 102 | def test_get_description_type(self): 103 | self.assertEqual(type(self.section.description), str) 104 | 105 | def test_get_description(self): 106 | self.assertEqual(self.section.description, 'some words') 107 | 108 | def test_set_description(self): 109 | description = 'new description' 110 | self.section.description = description 111 | self.assertEqual(self.section.description, description) 112 | self.assertEqual(self.section._content['description'], description) 113 | 114 | def test_set_description_invalid_type(self): 115 | with self.assertRaises(TestRailError) as e: 116 | self.section.description = 394 117 | self.assertEqual(str(e.exception), 'input must be a string') 118 | 119 | def test_get_name_type(self): 120 | self.assertEqual(type(self.section.name), str) 121 | 122 | def test_get_name(self): 123 | self.assertEqual(self.section.name, 'Prerequisites2') 124 | 125 | def test_set_name(self): 126 | name = 'my new suite' 127 | self.section.name = name 128 | self.assertEqual(self.section.name, name) 129 | self.assertEqual(self.section._content['name'], name) 130 | 131 | def test_set_name_invalid_type(self): 132 | with self.assertRaises(TestRailError) as e: 133 | self.section.name = 394 134 | self.assertEqual(str(e.exception), 'input must be a string') 135 | 136 | def test_raw_data(self): 137 | self.assertEqual(self.section.raw_data(), self.mock_section_data[1]) 138 | 139 | def test_raw_data_type(self): 140 | self.assertEqual(type(self.section.raw_data()), dict) 141 | 142 | @mock.patch('testrail.api.requests.get') 143 | def test_get_suite_type(self, mock_get): 144 | mock_response = mock.Mock() 145 | mock_response.json.return_value = copy.deepcopy(self.mock_suite_data) 146 | mock_response.status_code = 200 147 | mock_get.return_value = mock_response 148 | self.assertEqual(type(self.section.suite), Suite) 149 | 150 | @mock.patch('testrail.api.requests.get') 151 | def test_get_suite(self, mock_get): 152 | mock_response = mock.Mock() 153 | mock_response.json.return_value = copy.deepcopy(self.mock_suite_data) 154 | mock_response.status_code = 200 155 | mock_get.return_value = mock_response 156 | self.assertEqual(self.section.suite.id, 1) 157 | 158 | @mock.patch('testrail.api.requests.get') 159 | def test_get_suite_invalid_id(self, mock_get): 160 | mock_response = mock.Mock() 161 | mock_response.json.return_value = copy.deepcopy(self.mock_suite_data) 162 | mock_response.status_code = 200 163 | mock_get.return_value = mock_response 164 | self.section._content['suite_id'] = 200 165 | with self.assertRaises(TestRailError) as e: 166 | self.section.suite 167 | self.assertEqual(str(e.exception), "Suite ID '200' was not found") 168 | 169 | @mock.patch('testrail.api.requests.get') 170 | def test_set_suite(self, mock_get): 171 | mock_response = mock.Mock() 172 | mock_response.json.return_value = copy.deepcopy(self.mock_suite_data) 173 | mock_response.status_code = 200 174 | mock_get.return_value = mock_response 175 | self.assertEqual(self.section.suite.id, 1) 176 | self.section.suite = Suite(self.mock_suite_data[1]) 177 | self.assertEqual(self.section._content['suite_id'], 2) 178 | self.assertEqual(self.section.suite.id, 2) 179 | 180 | def test_set_suite_invalid_type(self): 181 | with self.assertRaises(TestRailError) as e: 182 | self.section.suite = 2 183 | self.assertEqual(str(e.exception), 'input must be a Suite') 184 | 185 | @mock.patch('testrail.api.requests.get') 186 | def test_set_suite_invalid_suite(self, mock_get): 187 | mock_response = mock.Mock() 188 | mock_response.json.return_value = copy.deepcopy(self.mock_suite_data) 189 | mock_response.status_code = 200 190 | mock_get.return_value = mock_response 191 | suite = Suite() 192 | suite._content['id'] = 5 193 | with self.assertRaises(TestRailError) as e: 194 | self.section.suite = suite 195 | self.assertEqual(str(e.exception), 196 | "Suite ID '5' was not found") 197 | 198 | def test_set_suite_empty_suite(self): 199 | s = Section({}) 200 | self.assertEqual(s.suite.id, None) 201 | self.assertEqual(type(s.suite), Suite) 202 | 203 | @mock.patch('testrail.api.requests.get') 204 | def test_get_parent_type(self, mock_get): 205 | mock_response = mock.Mock() 206 | mock_response.json.return_value = copy.deepcopy(self.mock_section_data) 207 | mock_response.status_code = 200 208 | mock_get.return_value = mock_response 209 | self.assertEqual(type(self.section.parent), Section) 210 | 211 | @mock.patch('testrail.api.requests.get') 212 | def test_get_parent(self, mock_get): 213 | mock_response = mock.Mock() 214 | mock_response.json.return_value = copy.deepcopy(self.mock_section_data) 215 | mock_response.status_code = 200 216 | mock_get.return_value = mock_response 217 | self.assertEqual(self.section.parent.id, 1) 218 | 219 | @mock.patch('testrail.api.requests.get') 220 | def test_get_parent_invalid_id(self, mock_get): 221 | mock_response = mock.Mock() 222 | mock_response.json.return_value = copy.deepcopy(self.mock_section_data) 223 | mock_response.status_code = 200 224 | mock_get.return_value = mock_response 225 | self.section._content['parent_id'] = 200 226 | with self.assertRaises(TestRailError) as e: 227 | self.section.parent 228 | self.assertEqual(str(e.exception), "Section ID '200' was not found") 229 | 230 | @mock.patch('testrail.api.requests.get') 231 | def test_set_parent(self, mock_get): 232 | mock_response = mock.Mock() 233 | mock_response.json.return_value = copy.deepcopy(self.mock_section_data) 234 | mock_response.status_code = 200 235 | mock_get.return_value = mock_response 236 | self.assertEqual(self.section.parent.id, 1) 237 | self.section.parent = Section(self.mock_section_data[1]) 238 | self.assertEqual(self.section._content['parent_id'], 2) 239 | self.assertEqual(self.section.parent.id, 2) 240 | 241 | def test_set_parent_invalid_type(self): 242 | with self.assertRaises(TestRailError) as e: 243 | self.section.parent = 2 244 | self.assertEqual(str(e.exception), 'input must be a Section') 245 | 246 | @mock.patch('testrail.api.requests.get') 247 | def test_set_parent_invalid_section(self, mock_get): 248 | mock_response = mock.Mock() 249 | mock_response.json.return_value = copy.deepcopy(self.mock_section_data) 250 | mock_response.status_code = 200 251 | mock_get.return_value = mock_response 252 | section = Section({}) 253 | section._content['id'] = 5 254 | with self.assertRaises(TestRailError) as e: 255 | self.section.parent = section 256 | self.assertEqual(str(e.exception), 257 | "Section ID '5' was not found") 258 | 259 | @mock.patch('testrail.api.requests.get') 260 | def test_set_parent_empty_section(self, mock_get): 261 | mock_response = mock.Mock() 262 | mock_response.json.return_value = copy.deepcopy(self.mock_section_data) 263 | mock_response.status_code = 200 264 | mock_get.return_value = mock_response 265 | with self.assertRaises(TestRailError) as e: 266 | self.section.parent = Section({}) 267 | self.assertEqual(str(e.exception), 268 | "Section ID 'None' was not found") 269 | -------------------------------------------------------------------------------- /tests/test_test.py: -------------------------------------------------------------------------------- 1 | import mock 2 | import datetime 3 | try: 4 | import unittest2 as unittest 5 | except ImportError: 6 | import unittest 7 | 8 | from testrail.api import API 9 | from testrail.run import Run 10 | from testrail.case import Case 11 | from testrail.test import Test 12 | from testrail.user import User 13 | from testrail.status import Status 14 | from testrail.milestone import Milestone 15 | from testrail.suite import Suite 16 | 17 | 18 | class TestTest(unittest.TestCase): 19 | def setUp(self): 20 | self.mock_run_data = [ 21 | { 22 | "assignedto_id": 6, 23 | "blocked_count": 1, 24 | "case_ids": [ 25 | 8, 26 | 9 27 | ], 28 | "completed_on": None, 29 | "config": "Mock Config", 30 | "config_ids": [ 31 | 2, 32 | 6 33 | ], 34 | "created_by": 5, 35 | "created_on": 1393845644, 36 | "custom_status1_count": 0, 37 | "custom_status2_count": 0, 38 | "custom_status3_count": 0, 39 | "custom_status4_count": 0, 40 | "custom_status5_count": 0, 41 | "custom_status6_count": 0, 42 | "custom_status7_count": 0, 43 | "description": "Mock description", 44 | "failed_count": 2, 45 | "id": 81, 46 | "include_all": False, 47 | "is_completed": False, 48 | "milestone_id": 9, 49 | "name": "Mock Name", 50 | "passed_count": 3, 51 | "plan_id": 80, 52 | "project_id": 1, 53 | "retest_count": 7, 54 | "suite_id": 4, 55 | "untested_count": 17, 56 | "url": "http://mock_server/testrail/index.php?/runs/view/81" 57 | }, 58 | ] 59 | 60 | self.mock_status_data = [ 61 | { 62 | "color_bright": 13684944, 63 | "color_dark": 0, 64 | "color_medium": 10526880, 65 | "id": 5, 66 | "is_final": False, 67 | "is_system": True, 68 | "is_untested": False, 69 | "label": "Mock Custom", 70 | "name": "mock_custom_status1" 71 | }, 72 | ] 73 | 74 | self.mock_user_data = [ 75 | { 76 | "email": "mock1@email.com", 77 | "id": 5, 78 | "is_active": True, 79 | "name": "Mock Name 1" 80 | }, 81 | { 82 | "email": "mock2@email.com", 83 | "id": 6, 84 | "is_active": True, 85 | "name": "Mock Name 2" 86 | } 87 | ] 88 | 89 | self.mock_case_data = [ 90 | { 91 | "created_by": 5, 92 | "created_on": 1392300984, 93 | 'estimate': '1w 3d 6h 2m 30s', 94 | "estimate_forecast": None, 95 | "id": 8, 96 | "milestone_id": 9, 97 | "priority_id": 2, 98 | "refs": "RF-1, RF-2", 99 | "section_id": 1, 100 | "suite_id": 1, 101 | "title": "Change document attributes (author, title, organization)", 102 | "type_id": 4, 103 | "updated_by": 6, 104 | "updated_on": 1393586511 105 | }, 106 | ] 107 | 108 | self.mock_mstone_data = [ 109 | { 110 | "completed_on": 1389968184, 111 | "description": "Mock milestone description", 112 | "due_on": 1391968184, 113 | "id": 9, 114 | "is_completed": True, 115 | "name": "Release 1.5", 116 | "project_id": 1, 117 | "url": "http:///testrail/index.php?/milestones/view/1" 118 | } 119 | 120 | ] 121 | 122 | self.mock_test_data = [ 123 | { 124 | "assignedto_id": 5, 125 | "case_id": 8, 126 | 'estimate': '1w 1d 1h 1m 1s', 127 | "estimate_forecast": '2w 2d 2h 2m 2s', 128 | "id": 100, 129 | "milestone_id": 9, 130 | "priority_id": 2, 131 | "refs": "REF1,REF2,REF3", 132 | "run_id": 81, 133 | "status_id": 5, 134 | "title": "Mock Test Title 1", 135 | "type_id": 4 136 | }, 137 | { 138 | "assignedto_id": 5, 139 | "case_id": 8, 140 | 'estimate': None, 141 | "estimate_forecast": None, 142 | "id": 200, 143 | "milestone_id": None, 144 | "priority_id": 2, 145 | "run_id": 1, 146 | "status_id": 5, 147 | "title": "Mock Test Title 2", 148 | "type_id": 4 149 | }, 150 | ] 151 | 152 | self.test = Test(self.mock_test_data[0]) 153 | self.test2 = Test(self.mock_test_data[1]) 154 | 155 | @mock.patch('testrail.api.API._refresh') 156 | @mock.patch('testrail.api.requests.get') 157 | def test_get_test_assigned_to_type(self, mock_get, refresh_mock): 158 | refresh_mock.return_value = True 159 | mock_response = mock.Mock() 160 | mock_response.json.return_value = self.mock_user_data 161 | mock_response.status_code = 200 162 | mock_get.return_value = mock_response 163 | self.assertTrue(isinstance(self.test.assigned_to, User)) 164 | 165 | @mock.patch('testrail.api.API._refresh') 166 | @mock.patch('testrail.api.requests.get') 167 | def test_get_test_assigned_to(self, mock_get, refresh_mock): 168 | refresh_mock.return_value = True 169 | mock_response = mock.Mock() 170 | mock_response.json.return_value = self.mock_user_data 171 | mock_response.status_code = 200 172 | mock_get.return_value = mock_response 173 | self.assertEqual(self.test.assigned_to.id, 5) 174 | 175 | @mock.patch('testrail.test.Test.run') 176 | @mock.patch('testrail.api.API._refresh') 177 | @mock.patch('testrail.api.requests.get') 178 | def test_get_test_case_type(self, mock_get, refresh_mock, _): 179 | refresh_mock.return_value = True 180 | mock_response = mock.Mock() 181 | mock_response.json.return_value = self.mock_case_data 182 | mock_response.status_code = 200 183 | mock_get.return_value = mock_response 184 | self.assertTrue(isinstance(self.test.case, Case)) 185 | 186 | @mock.patch('testrail.test.Test.run') 187 | @mock.patch('testrail.api.API._refresh') 188 | @mock.patch('testrail.api.requests.get') 189 | def test_get_test_case(self, mock_get, refresh_mock, _): 190 | refresh_mock.return_value = True 191 | mock_response = mock.Mock() 192 | mock_response.json.return_value = self.mock_case_data 193 | mock_response.status_code = 200 194 | mock_get.return_value = mock_response 195 | self.assertEqual(self.test.case.id, 8) 196 | 197 | def test_get_test_estimate_type(self): 198 | self.assertTrue(isinstance(self.test.estimate, datetime.timedelta)) 199 | 200 | def test_get_test_estimate_null(self): 201 | self.assertEqual(self.test2.estimate, None) 202 | 203 | def test_get_test_estimate(self): 204 | expected_timedelta = datetime.timedelta( 205 | weeks=1, days=1, hours=1, minutes=1, seconds=1) 206 | 207 | self.assertEqual(self.test.estimate, expected_timedelta) 208 | 209 | def test_get_test_estimate_forecast_type(self): 210 | self.assertTrue(isinstance(self.test.estimate_forecast, datetime.timedelta)) 211 | 212 | def test_get_test_estimate_forecast_null(self): 213 | self.assertEqual(self.test2.estimate_forecast, None) 214 | 215 | def test_get_test_estimate_forecast(self): 216 | expected_timedelta = datetime.timedelta( 217 | weeks=2, days=2, hours=2, minutes=2, seconds=2) 218 | 219 | self.assertEqual(self.test.estimate_forecast, expected_timedelta) 220 | 221 | def test_get_test_id_type(self): 222 | self.assertTrue(isinstance(self.test.id, int)) 223 | 224 | def test_get_test_id(self): 225 | self.assertEqual(self.test.id, 100) 226 | 227 | @mock.patch('testrail.api.API._refresh') 228 | @mock.patch('testrail.api.requests.get') 229 | def test_get_test_milestone_type(self, mock_get, refresh_mock): 230 | refresh_mock.return_value = True 231 | mock_response = mock.Mock() 232 | mock_response.json.return_value = self.mock_mstone_data[0] 233 | mock_response.status_code = 200 234 | mock_get.return_value = mock_response 235 | self.assertTrue(isinstance(self.test.milestone, Milestone)) 236 | 237 | def test_get_test_milestone_is_null(self): 238 | self.assertEqual(self.test2.milestone, None) 239 | 240 | @mock.patch('testrail.api.API._refresh') 241 | @mock.patch('testrail.api.requests.get') 242 | def test_get_test_milestone(self, mock_get, refresh_mock): 243 | refresh_mock.return_value = True 244 | mock_response = mock.Mock() 245 | mock_response.json.return_value = self.mock_mstone_data[0] 246 | mock_response.status_code = 200 247 | mock_get.return_value = mock_response 248 | self.assertEqual(self.test.milestone.id, 9) 249 | 250 | def test_get_test_refs_type(self): 251 | self.assertTrue(isinstance(self.test.refs, str)) 252 | 253 | def test_get_test_refs_is_null(self): 254 | self.assertEqual(self.test2.refs, None) 255 | 256 | def test_get_test_refs(self): 257 | self.assertIn("REF1", self.test.refs) 258 | 259 | @mock.patch('testrail.api.API._refresh') 260 | @mock.patch('testrail.api.requests.get') 261 | def test_get_test_run_type(self, mock_get, refresh_mock): 262 | refresh_mock.return_value = True 263 | mock_response = mock.Mock() 264 | mock_response.json.return_value = self.mock_run_data 265 | mock_response.status_code = 200 266 | mock_get.return_value = mock_response 267 | self.assertTrue(isinstance(self.test.run, Run)) 268 | 269 | @mock.patch('testrail.api.API._refresh') 270 | @mock.patch('testrail.api.requests.get') 271 | def test_get_test_run(self, mock_get, refresh_mock): 272 | refresh_mock.return_value = True 273 | mock_response = mock.Mock() 274 | mock_response.json.return_value = self.mock_run_data 275 | mock_response.status_code = 200 276 | mock_get.return_value = mock_response 277 | self.assertEqual(self.test.run.id, 81) 278 | 279 | @mock.patch('testrail.api.API._refresh') 280 | @mock.patch('testrail.api.requests.get') 281 | def test_get_test_status_type(self, mock_get, refresh_mock): 282 | refresh_mock.return_value = True 283 | mock_response = mock.Mock() 284 | mock_response.json.return_value = self.mock_status_data 285 | mock_response.status_code = 200 286 | mock_get.return_value = mock_response 287 | self.assertTrue(isinstance(self.test.status, Status)) 288 | 289 | @mock.patch('testrail.api.API._refresh') 290 | @mock.patch('testrail.api.requests.get') 291 | def test_get_test_status(self, mock_get, refresh_mock): 292 | refresh_mock.return_value = True 293 | mock_response = mock.Mock() 294 | mock_response.json.return_value = self.mock_status_data 295 | mock_response.status_code = 200 296 | mock_get.return_value = mock_response 297 | self.assertEqual(self.test.status.id, 5) 298 | 299 | def test_get_test_title_type(self): 300 | self.assertTrue(isinstance(self.test.title, str)) 301 | 302 | def test_get_test_refs(self): 303 | self.assertIn("Mock Test Title 1", self.test.title) 304 | 305 | def test_get_raw_data_type(self): 306 | self.assertTrue(isinstance(self.test.raw_data(), dict)) 307 | 308 | def test_raw_data(self): 309 | self.assertEqual(self.test.raw_data(), self.mock_test_data[0]) 310 | -------------------------------------------------------------------------------- /testrail/client.py: -------------------------------------------------------------------------------- 1 | import re 2 | import sys 3 | 4 | from testrail.api import API 5 | from testrail.case import Case 6 | from testrail.configuration import Config, ConfigContainer 7 | from testrail.helper import methdispatch, singleresult, TestRailError 8 | from testrail.milestone import Milestone 9 | from testrail.plan import Plan, PlanContainer 10 | from testrail.project import Project, ProjectContainer 11 | from testrail.result import Result, ResultContainer 12 | from testrail.run import Run, RunContainer 13 | from testrail.status import Status 14 | from testrail.suite import Suite 15 | from testrail.section import Section 16 | from testrail.test import Test 17 | from testrail.user import User 18 | 19 | if sys.version_info >= (3,0): 20 | unicode = str 21 | 22 | class TestRail(object): 23 | def __init__(self, project_id=0, email=None, key=None, url=None): 24 | self.api = API(email=email, key=key, url=url) 25 | self.api.set_project_id(project_id) 26 | self._project_id = project_id 27 | 28 | def set_project_id(self, project_id): 29 | self._project_id = project_id 30 | self.api.set_project_id(project_id) 31 | 32 | # Post generics 33 | @methdispatch 34 | def add(self, obj): 35 | raise NotImplementedError 36 | 37 | @methdispatch 38 | def update(self, obj): 39 | raise NotImplementedError 40 | 41 | @methdispatch 42 | def close(self, obj): 43 | raise NotImplementedError 44 | 45 | @methdispatch 46 | def delete(self, obj): 47 | raise NotImplementedError 48 | 49 | # Project Methods 50 | def projects(self): 51 | return ProjectContainer(list(map(Project, self.api.projects()))) 52 | 53 | @methdispatch 54 | def project(self): 55 | return Project() 56 | 57 | @project.register(str) 58 | @project.register(unicode) 59 | @singleresult 60 | def _project_by_name(self, name): 61 | return filter(lambda p: p.name == name, self.projects()) 62 | 63 | @project.register(int) 64 | @singleresult 65 | def _project_by_id(self, project_id): 66 | return filter(lambda p: p.id == project_id, self.projects()) 67 | 68 | # User Methods 69 | def users(self): 70 | return list(map(User, self.api.users())) 71 | 72 | @methdispatch 73 | def user(self): 74 | return User() 75 | 76 | @user.register(int) 77 | @singleresult 78 | def _user_by_id(self, identifier): 79 | return filter(lambda u: u.id == identifier, self.users()) 80 | 81 | @user.register(str) 82 | @user.register(unicode) 83 | @singleresult 84 | def _user_by_email_name(self, identifier): 85 | by_email = lambda u: u.email == identifier 86 | by_name = lambda u: u.name == identifier 87 | f = by_email if re.match('[^@]+@[^@]+\.[^@]+', identifier) else by_name 88 | return filter(f, self.users()) 89 | 90 | def active_users(self): 91 | return list(filter(lambda u: u.is_active is True, self.users())) 92 | 93 | def inactive_users(self): 94 | return list(filter(lambda u: u.is_active is False, self.users())) 95 | 96 | # Suite Methods 97 | def suites(self): 98 | return list(map(Suite, self.api.suites(self._project_id))) 99 | 100 | @methdispatch 101 | def suite(self): 102 | return Suite() 103 | 104 | @suite.register(str) 105 | @suite.register(unicode) 106 | @singleresult 107 | def _suite_by_name(self, name): 108 | return filter(lambda s: s.name.lower() == name.lower(), self.suites()) 109 | 110 | @suite.register(int) 111 | @singleresult 112 | def _suite_by_id(self, suite_id): 113 | return filter(lambda s: s.id == suite_id, self.suites()) 114 | 115 | def active_suites(self): 116 | return filter(lambda s: s.is_completed is False, self.suites()) 117 | 118 | def completed_suites(self): 119 | return filter(lambda s: s.is_completed is True, self.suites()) 120 | 121 | @add.register(Suite) 122 | def _add_suite(self, obj): 123 | obj.project = obj.project or self.project(self._project_id) 124 | return Suite(self.api.add_suite(obj.raw_data())) 125 | 126 | @update.register(Suite) 127 | def _update_suite(self, obj): 128 | return Suite(self.api.update_suite(obj.raw_data())) 129 | 130 | @delete.register(Suite) 131 | def _delete_suite(self, obj): 132 | return self.api.delete_suite(obj.id) 133 | 134 | # Milestone Methods 135 | def milestones(self): 136 | return list(map(Milestone, self.api.milestones(self._project_id))) 137 | 138 | @methdispatch 139 | def milestone(self): 140 | return Milestone() 141 | 142 | @milestone.register(str) 143 | @milestone.register(unicode) 144 | @singleresult 145 | def _milestone_by_name(self, name): 146 | return filter( 147 | lambda m: m.name.lower() == name.lower(), self.milestones()) 148 | 149 | @milestone.register(int) 150 | @singleresult 151 | def _milestone_by_id(self, milestone_id): 152 | return filter(lambda s: s.id == milestone_id, self.milestones()) 153 | 154 | @add.register(Milestone) 155 | def _add_milestone(self, obj): 156 | obj.project = obj.project or self.project(self._project_id) 157 | return Milestone(self.api.add_milestone(obj.raw_data())) 158 | 159 | @update.register(Milestone) 160 | def _update_milestone(self, obj): 161 | return Milestone(self.api.update_milestone(obj.raw_data())) 162 | 163 | @delete.register(Milestone) 164 | def _delete_milestone(self, obj): 165 | return self.api.delete_milestone(obj.id) 166 | 167 | # Plan Methods 168 | @methdispatch 169 | def plans(self): 170 | return PlanContainer(list(map(Plan, self.api.plans(self._project_id)))) 171 | 172 | @plans.register(Milestone) 173 | def _plans_for_milestone(self, obj): 174 | plans = filter(lambda p: p.milestone is not None, self.plans()) 175 | return PlanContainer(filter(lambda p: p.milestone.id == obj.id, plans)) 176 | 177 | @methdispatch 178 | def plan(self): 179 | return Plan() 180 | 181 | @plan.register(str) 182 | @plan.register(unicode) 183 | @singleresult 184 | def _plan_by_name(self, name): 185 | return filter(lambda p: p.name.lower() == name.lower(), self.plans()) 186 | 187 | @plan.register(int) 188 | @singleresult 189 | def _plan_by_id(self, plan_id): 190 | return filter(lambda p: p.id == plan_id, self.plans()) 191 | 192 | def completed_plans(self): 193 | return filter(lambda p: p.is_completed is True, self.plans()) 194 | 195 | def active_plans(self): 196 | return filter(lambda p: p.is_completed is False, self.plans()) 197 | 198 | @add.register(Plan) 199 | def _add_plan(self, obj, milestone=None): 200 | obj.project = obj.project or self.project(self._project_id) 201 | obj.milestone = milestone or obj.milestone 202 | return Plan(self.api.add_plan(obj.raw_data())) 203 | 204 | @update.register(Plan) 205 | def _update_plan(self, obj): 206 | return Plan(self.api.update_plan(obj.raw_data())) 207 | 208 | @close.register(Plan) 209 | def _close_plan(self, obj): 210 | return Plan(self.api.close_plan(obj.id)) 211 | 212 | @delete.register(Plan) 213 | def _delete_plan(self, obj): 214 | return self.api.delete_plan(obj.id) 215 | 216 | # Run Methods 217 | @methdispatch 218 | def runs(self): 219 | return RunContainer(list(map(Run, self.api.runs(self._project_id)))) 220 | 221 | @runs.register(Milestone) 222 | def _runs_for_milestone(self, obj): 223 | return RunContainer(filter( 224 | lambda r: r.milestone.id == obj.id, self.runs())) 225 | 226 | @runs.register(str) 227 | @runs.register(unicode) 228 | def _runs_by_name(self, name): 229 | # Returns all Runs that match :name, in descending order by ID 230 | runs = list(filter(lambda r: r.name.lower() == name.lower(), self.runs())) 231 | return sorted(runs, key=lambda r: r.id) 232 | 233 | @methdispatch 234 | def run(self): 235 | return Run() 236 | 237 | @run.register(str) 238 | @run.register(unicode) 239 | @singleresult 240 | def _run_by_name(self, name): 241 | # Returns the most recently created Run that matches :name 242 | runs = list(filter(lambda r: r.name.lower() == name.lower(), self.runs())) 243 | return sorted(runs, key=lambda r: r.id)[:1] 244 | 245 | @run.register(int) 246 | @singleresult 247 | def _run_by_id(self, run_id): 248 | return filter(lambda p: p.id == run_id, self.runs()) 249 | 250 | @add.register(Run) 251 | def _add_run(self, obj): 252 | obj.project = obj.project or self.project(self._project_id) 253 | return Run(self.api.add_run(obj.raw_data())) 254 | 255 | @update.register(Run) 256 | def _update_run(self, obj): 257 | return Run(self.api.update_run(obj.raw_data())) 258 | 259 | @close.register(Run) 260 | def _close_run(self, obj): 261 | return Run(self.api.close_run(obj.id)) 262 | 263 | @delete.register(Run) 264 | def _delete_run(self, obj): 265 | return self.api.delete_run(obj.id) 266 | 267 | # Case Methods 268 | def cases(self, suite): 269 | return list(map(Case, self.api.cases(self._project_id, suite.id))) 270 | 271 | @methdispatch 272 | def case(self): 273 | return Case() 274 | 275 | @case.register(str) 276 | @case.register(unicode) 277 | @singleresult 278 | def _case_by_title(self, title, suite): 279 | return filter( 280 | lambda c: c.title.lower() == title.lower(), self.cases(suite)) 281 | 282 | @case.register(int) 283 | @singleresult 284 | def _case_by_id(self, case_id, suite=None): 285 | if suite is None: 286 | pass 287 | else: 288 | return filter(lambda c: c.id == case_id, self.cases(suite)) 289 | 290 | @add.register(Case) 291 | def _add_case(self, obj): 292 | return Case(self.api.add_case(obj.raw_data())) 293 | 294 | @update.register(Case) 295 | def _update_case(self, obj): 296 | return Case(self.api.update_case(obj.raw_data())) 297 | 298 | # Test Methods 299 | def tests(self, run): 300 | return list(map(Test, self.api.tests(run.id))) 301 | 302 | @methdispatch 303 | def test(self): 304 | return Test() 305 | 306 | @test.register(str) 307 | @test.register(unicode) 308 | @singleresult 309 | def _test_by_name(self, name, run): 310 | return filter(lambda t: t.title.lower() == name.lower(), self.tests(run)) 311 | 312 | @test.register(int) 313 | @singleresult 314 | def _test_by_id(self, test_id, run): 315 | return filter( 316 | lambda t: t.raw_data()['id'] == test_id, self.tests(run)) 317 | 318 | # Result Methods 319 | @methdispatch 320 | def results(self): 321 | raise TestRailError("Must request results by Run or Test") 322 | 323 | @results.register(Run) 324 | def _results_for_run(self, run): 325 | return ResultContainer(list(map(Result, self.api.results_by_run(run.id)))) 326 | 327 | @results.register(Test) 328 | def _results_for_test(self, test): 329 | return ResultContainer(list(map(Result, self.api.results_by_test(test.id)))) 330 | 331 | @methdispatch 332 | def result(self): 333 | return Result() 334 | 335 | @add.register(Result) 336 | def _add_result(self, obj): 337 | self.api.add_result(obj.raw_data()) 338 | 339 | @add.register(tuple) 340 | def _add_results(self, results): 341 | obj, value = results 342 | if isinstance(obj, Run): 343 | self.api.add_results(list(map(lambda x: x.raw_data(), value)), obj.id) 344 | 345 | # Section Methods 346 | def sections(self, suite=None): 347 | return list(map(Section, self.api.sections(suite_id=suite.id))) 348 | 349 | @methdispatch 350 | def section(self): 351 | return Section() 352 | 353 | @section.register(int) 354 | def _section_by_id(self, section_id): 355 | return Section(self.api.section_with_id(section_id)) 356 | 357 | @section.register(unicode) 358 | @section.register(str) 359 | @singleresult 360 | def _section_by_name(self, name, suite=None): 361 | return filter(lambda s: s.name == name, self.sections(suite)) 362 | 363 | @add.register(Section) 364 | def _add_section(self, section): 365 | return Section(self.api.add_section(section.raw_data())) 366 | 367 | # Status Methods 368 | def statuses(self): 369 | return list(map(Status, self.api.statuses())) 370 | 371 | @methdispatch 372 | def status(self): 373 | return Status() 374 | 375 | @status.register(str) 376 | @status.register(unicode) 377 | @singleresult 378 | def _status_by_name(self, name): 379 | return filter(lambda s: s.name == name.lower(), self.statuses()) 380 | 381 | @status.register(int) 382 | @singleresult 383 | def _status_by_id(self, status_id): 384 | return filter(lambda s: s.id == status_id, self.statuses()) 385 | 386 | def configs(self): 387 | return ConfigContainer(list(map(Config, self.api.configs()))) 388 | -------------------------------------------------------------------------------- /tests/test_plan.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import mock 3 | import datetime 4 | try: 5 | import unittest2 as unittest 6 | except ImportError: 7 | import unittest 8 | 9 | from testrail.api import API 10 | from testrail.run import Run 11 | from testrail.user import User 12 | from testrail.plan import Plan 13 | from testrail.entry import Entry 14 | from testrail.project import Project 15 | from testrail.milestone import Milestone 16 | from testrail.helper import TestRailError 17 | 18 | 19 | class TestPlan(unittest.TestCase): 20 | def setUp(self): 21 | API.flush_cache() 22 | 23 | self.mock_run_data = [ 24 | { 25 | "assignedto_id": 6, 26 | "blocked_count": 1, 27 | "completed_on": None, 28 | "config": "Mock Config", 29 | "config_ids": [ 30 | 2, 31 | 6 32 | ], 33 | "created_by": 5, 34 | "created_on": 1393845644, 35 | "custom_status1_count": 0, 36 | "custom_status2_count": 0, 37 | "custom_status3_count": 0, 38 | "custom_status4_count": 0, 39 | "custom_status5_count": 0, 40 | "custom_status6_count": 0, 41 | "custom_status7_count": 0, 42 | "description": "Mock description", 43 | "failed_count": 2, 44 | "id": 81, 45 | "include_all": False, 46 | "is_completed": False, 47 | "milestone_id": 9, 48 | "name": "Mock Name", 49 | "passed_count": 3, 50 | "plan_id": 80, 51 | "project_id": 1, 52 | "retest_count": 7, 53 | "suite_id": 4, 54 | "untested_count": 17, 55 | "url": "http://mock_server/testrail/index.php?/runs/view/81" 56 | }, 57 | { 58 | "assignedto_id": 7, 59 | "blocked_count": 1, 60 | "completed_on": 100000, 61 | "config": "Mock Config", 62 | "config_ids": [ 63 | 2, 64 | 6 65 | ], 66 | "created_by": 1, 67 | "created_on": None, 68 | "custom_status1_count": 0, 69 | "custom_status2_count": 0, 70 | "custom_status3_count": 0, 71 | "custom_status4_count": 0, 72 | "custom_status5_count": 0, 73 | "custom_status6_count": 0, 74 | "custom_status7_count": 0, 75 | "description": None, 76 | "failed_count": 2, 77 | "id": 81, 78 | "include_all": False, 79 | "is_completed": False, 80 | "milestone_id": 7, 81 | "name": "Mock Name", 82 | "passed_count": 2, 83 | "plan_id": 80, 84 | "project_id": 1, 85 | "retest_count": 1, 86 | "suite_id": 4, 87 | "untested_count": 3, 88 | "url": "http://mock_server/testrail/index.php?/runs/view/81" 89 | } 90 | ] 91 | 92 | self.mock_entries = [ 93 | { 94 | "id": "mock-id-1", 95 | "name": "Mock entry", 96 | "runs": self.mock_run_data, 97 | "suite_id": 4 98 | }, 99 | { 100 | "id": "mock-id-2", 101 | "name": "Mock entry 2", 102 | "runs": self.mock_run_data, 103 | "suite_id": 4 104 | } 105 | ] 106 | 107 | self.mock_entries2 = copy.deepcopy(self.mock_entries) 108 | self.mock_entries2[0]['id'] = "new-mock-id-1" 109 | self.mock_entries2[1]['id'] = "new-mock-id-1" 110 | 111 | mock_plan1 = { 112 | "assignedto_id": 6, 113 | "blocked_count": 2, 114 | "completed_on": None, 115 | "created_by": 6, 116 | "created_on": None, 117 | "custom_status1_count": 0, 118 | "custom_status2_count": 0, 119 | "custom_status3_count": 0, 120 | "custom_status4_count": 0, 121 | "custom_status5_count": 0, 122 | "custom_status6_count": 0, 123 | "custom_status7_count": 0, 124 | "description": "Mock plan description", 125 | "entries": self.mock_entries, 126 | "failed_count": 4, 127 | "id": 88, 128 | "is_completed": False, 129 | "milestone_id": 7, 130 | "name": "Mock Plan Name", 131 | "passed_count": 5, 132 | "project_id": 1, 133 | "retest_count": 20, 134 | "untested_count": 63, 135 | "url": "http:///testrail/index.php?/plans/view/80" 136 | } 137 | 138 | mock_plan2 = copy.deepcopy(mock_plan1) 139 | mock_plan2.update({ 140 | 'completed_on': 20000, 141 | "created_on": 30000, 142 | 'entries': list(), 143 | 'id': 999, 144 | "milestone_id": None, 145 | }) 146 | self.mock_plan_data = [mock_plan1, mock_plan2] 147 | 148 | self.mock_users = [ 149 | { 150 | "email": "mock1@email.com", 151 | "id": 5, 152 | "is_active": True, 153 | "name": "Mock Name 1" 154 | }, 155 | { 156 | "email": "mock2@email.com", 157 | "id": 6, 158 | "is_active": True, 159 | "name": "Mock Name 2" 160 | } 161 | ] 162 | 163 | self.mock_mstone_data = [ 164 | { 165 | "completed_on": 1389968888, 166 | "description": "Mock milestone1 description", 167 | "due_on": 1391968184, 168 | "id": 9, 169 | "is_completed": True, 170 | "name": "Release 1.5", 171 | "project_id": 1, 172 | "url": "http:///testrail/index.php?/milestones/view/1" 173 | }, 174 | { 175 | "completed_on": 1389969999, 176 | "description": "Mock milestone2 description", 177 | "due_on": 1391968184, 178 | "id": 7, 179 | "is_completed": False, 180 | "name": "Release 1.5", 181 | "project_id": 1, 182 | "url": "http:///testrail/index.php?/milestones/view/1" 183 | } 184 | 185 | ] 186 | 187 | self.mock_project_data = [{"id": 1, }, {"id": 99, }] 188 | 189 | self.plan = Plan(self.mock_plan_data[0]) 190 | self.plan2 = Plan(self.mock_plan_data[1]) 191 | 192 | @mock.patch('testrail.api.requests.get') 193 | def test_get_plan_assigned_to_type(self, mock_get): 194 | mock_response = mock.Mock() 195 | mock_response.json.return_value = copy.deepcopy(self.mock_users) 196 | mock_response.status_code = 200 197 | mock_get.return_value = mock_response 198 | self.assertTrue(isinstance(self.plan.assigned_to, User)) 199 | 200 | @mock.patch('testrail.api.requests.get') 201 | def test_run_assigned_to(self, mock_get): 202 | mock_response = mock.Mock() 203 | mock_response.json.return_value = copy.deepcopy(self.mock_users) 204 | mock_response.status_code = 200 205 | mock_get.return_value = mock_response 206 | self.assertEqual(self.plan.assigned_to.id, 6) 207 | 208 | def test_get_blocked_count_type(self): 209 | self.assertTrue(isinstance(self.plan.blocked_count, int)) 210 | 211 | def test_get_blocked_count(self): 212 | self.assertEqual(self.plan.blocked_count, 2) 213 | 214 | def test_get_completed_on_no_ts_type(self): 215 | self.assertEqual(self.plan.completed_on, None) 216 | 217 | def test_get_completed_on_with_ts_type(self): 218 | self.assertTrue(isinstance(self.plan2.completed_on, datetime.datetime)) 219 | 220 | def test_get_completed_on_with_ts(self): 221 | self.assertEqual( 222 | self.plan2.completed_on, datetime.datetime.fromtimestamp(20000)) 223 | 224 | def test_get_created_on_no_ts_type(self): 225 | self.assertEqual(self.plan.created_on, None) 226 | 227 | def test_get_created_on_with_ts_type(self): 228 | self.assertTrue(isinstance(self.plan2.created_on, datetime.datetime)) 229 | 230 | def test_get_created_on_with_ts(self): 231 | self.assertEqual( 232 | self.plan2.created_on, datetime.datetime.fromtimestamp(30000)) 233 | 234 | @mock.patch('testrail.api.requests.get') 235 | def test_get_created_by_type(self, mock_get): 236 | mock_response = mock.Mock() 237 | mock_response.json.return_value = copy.deepcopy(self.mock_users) 238 | mock_response.status_code = 200 239 | mock_get.return_value = mock_response 240 | self.assertTrue(isinstance(self.plan.created_by, User)) 241 | 242 | @mock.patch('testrail.api.requests.get') 243 | def test_created_by(self, mock_get2): 244 | mock_response = mock.Mock() 245 | mock_response.json.return_value = copy.deepcopy(self.mock_users) 246 | mock_response.status_code = 200 247 | mock_get2.return_value = mock_response 248 | self.assertEqual(self.plan.created_by.id, 6) 249 | 250 | def test_get_description_type(self): 251 | self.assertTrue(isinstance(self.plan.description, str)) 252 | 253 | def test_description(self): 254 | self.assertEqual(self.plan.description, "Mock plan description") 255 | 256 | def test_set_description(self): 257 | self.plan.description = "New plan description" 258 | self.assertEqual(self.plan.description, "New plan description") 259 | 260 | def test_set_description_invalid_type(self): 261 | with self.assertRaises(TestRailError) as e: 262 | self.plan.description = 194 263 | self.assertEqual(str(e.exception), 'input must be a string') 264 | 265 | def test_get_entries_type(self): 266 | self.assertTrue( 267 | all([lambda e: isinstance(e, Entry) for e in self.plan.entries])) 268 | 269 | def test_get_entries_type(self): 270 | def entry_checker(e): 271 | return e.id.startswith("mock-id") 272 | self.assertTrue(all([entry_checker(e) for e in self.plan.entries])) 273 | """ 274 | @mock.patch('testrail.api.requests.get') 275 | def test_get_entries_if_none(self, mock_get): 276 | mock_response = mock.Mock() 277 | mock_response.json.return_value = copy.deepcopy(self.mock_plan_data) 278 | mock_response.status_code = 200 279 | mock_get.return_value = mock_response 280 | self.assertTrue( 281 | all([lambda e: isinstance(e, Entry) for e in self.plan2.entries])) 282 | """ 283 | 284 | def test_get_failed_count_type(self): 285 | self.assertTrue(isinstance(self.plan.failed_count, int)) 286 | 287 | def test_failed_count(self): 288 | self.assertEqual(self.plan.failed_count, 4) 289 | 290 | def test_get_id_type(self): 291 | self.assertTrue(isinstance(self.plan.id, int)) 292 | 293 | def test_get_id(self): 294 | self.assertEqual(self.plan.id, 88) 295 | 296 | def test_get_is_completed_type(self): 297 | self.assertTrue(isinstance(self.plan.is_completed, bool)) 298 | 299 | def test_is_completed(self): 300 | self.assertEqual(self.plan.is_completed, False) 301 | 302 | @mock.patch('testrail.api.requests.get') 303 | def test_no_milestone(self, mock_get): 304 | mock_response = mock.Mock() 305 | mock_response.json.return_value = copy.deepcopy(self.mock_mstone_data) 306 | mock_response.status_code = 200 307 | mock_get.return_value = mock_response 308 | self.assertEqual(self.plan2.milestone.id, None) 309 | self.assertEqual(type(self.plan2.milestone), Milestone) 310 | 311 | @mock.patch('testrail.api.requests.get') 312 | def test_get_milestone_type(self, mock_get): 313 | mock_response = mock.Mock() 314 | mock_response.json.return_value = copy.deepcopy(self.mock_mstone_data) 315 | mock_response.status_code = 200 316 | mock_get.return_value = mock_response 317 | self.assertTrue(isinstance(self.plan.milestone, Milestone)) 318 | 319 | @mock.patch('testrail.api.requests.get') 320 | def test_milestone(self, mock_get): 321 | mock_response = mock.Mock() 322 | mock_response.json.return_value = copy.deepcopy(self.mock_mstone_data) 323 | mock_response.status_code = 200 324 | mock_get.return_value = mock_response 325 | self.assertEqual(self.plan.milestone.id, 7) 326 | 327 | @mock.patch('testrail.api.requests.get') 328 | def test_set_milestone(self, mock_get): 329 | mock_response = mock.Mock() 330 | mock_response.json.return_value = copy.deepcopy(self.mock_project_data) 331 | mock_response.status_code = 200 332 | mock_get.return_value = mock_response 333 | 334 | milestone = Milestone(self.mock_mstone_data[1]) 335 | self.plan.milestone = milestone 336 | self.assertEqual(self.plan._content['milestone_id'], 7) 337 | 338 | def test_set_milestone_invalid_type(self): 339 | with self.assertRaises(TestRailError) as e: 340 | self.plan.milestone = 994 341 | self.assertEqual(str(e.exception), 'input must be a Milestone') 342 | 343 | def test_get_name_type(self): 344 | self.assertTrue(isinstance(self.plan.name, str)) 345 | 346 | def test_get_name(self): 347 | self.assertEqual(self.plan.name, "Mock Plan Name") 348 | 349 | def test_set_name(self): 350 | name = "Mock New Name" 351 | self.plan.name = name 352 | self.assertEqual(self.plan.name, name) 353 | 354 | def test_set_name_invalid_type(self): 355 | with self.assertRaises(TestRailError) as e: 356 | self.plan.name = 394 357 | self.assertEqual(str(e.exception), 'input must be a string') 358 | 359 | def test_get_passed_count_type(self): 360 | self.assertTrue(isinstance(self.plan.passed_count, int)) 361 | 362 | def test_passed_count(self): 363 | self.assertEqual(self.plan.passed_count, 5) 364 | 365 | @mock.patch('testrail.api.requests.get') 366 | def test_get_project_type(self, mock_get): 367 | mock_response = mock.Mock() 368 | mock_response.json.return_value = copy.deepcopy(self.mock_project_data) 369 | mock_response.status_code = 200 370 | mock_get.return_value = mock_response 371 | self.assertTrue(isinstance(self.plan.project, Project)) 372 | 373 | @mock.patch('testrail.api.requests.get') 374 | def test_project(self, mock_get): 375 | mock_response = mock.Mock() 376 | mock_response.json.return_value = copy.deepcopy(self.mock_project_data) 377 | mock_response.status_code = 200 378 | mock_get.return_value = mock_response 379 | self.assertEqual(self.plan.project.id, 1) 380 | 381 | @mock.patch('testrail.api.requests.get') 382 | def test_set_project(self, mock_get): 383 | mock_response = mock.Mock() 384 | mock_response.json.return_value = copy.deepcopy(self.mock_project_data) 385 | mock_response.status_code = 200 386 | mock_get.return_value = mock_response 387 | 388 | project = Project(self.mock_project_data[1]) 389 | self.plan.project = project 390 | self.assertEqual(self.plan.project.id, 99) 391 | 392 | def test_set_project_invalid_type(self): 393 | with self.assertRaises(TestRailError) as e: 394 | self.plan.project = 394 395 | self.assertEqual(str(e.exception), 'input must be a Project') 396 | 397 | def test_get_project_id_type(self): 398 | self.assertTrue(isinstance(self.plan.project_id, int)) 399 | 400 | def test_project_id(self): 401 | self.assertEqual(self.plan.project_id, 1) 402 | 403 | def test_get_retest_count_type(self): 404 | self.assertTrue(isinstance(self.plan.retest_count, int)) 405 | 406 | def test_retest_count(self): 407 | self.assertEqual(self.plan.retest_count, 20) 408 | 409 | def test_get_untested_count_type(self): 410 | self.assertTrue(isinstance(self.plan.untested_count, int)) 411 | 412 | def test_untested_count(self): 413 | self.assertEqual(self.plan.untested_count, 63) 414 | 415 | def test_get_url_type(self): 416 | self.assertTrue(isinstance(self.plan.url, str)) 417 | 418 | def test_url(self): 419 | self.assertTrue(self.plan.url.startswith("http://")) 420 | 421 | def test_get_raw_data_type(self): 422 | self.assertTrue(isinstance(self.plan.raw_data(), dict)) 423 | 424 | def test_raw_data(self): 425 | self.assertEqual(self.plan.raw_data(), self.mock_plan_data[0]) 426 | -------------------------------------------------------------------------------- /tests/test_result.py: -------------------------------------------------------------------------------- 1 | import copy 2 | from datetime import datetime, timedelta 3 | import mock 4 | try: 5 | import unittest2 as unittest 6 | except ImportError: 7 | import unittest 8 | 9 | from testrail.api import API 10 | from testrail.test import Test 11 | from testrail.user import User 12 | from testrail.result import Result 13 | from testrail.status import Status 14 | from testrail.helper import TestRailError 15 | 16 | 17 | class TestUser(unittest.TestCase): 18 | 19 | def setUp(self): 20 | API.flush_cache() 21 | 22 | self.mock_result_data = { 23 | 'assignedto_id': 1, 24 | 'comment': 'All steps passed', 25 | 'created_by': 2, 26 | 'created_on': 1453504099, 27 | 'defects': 'def1, def2, def3', 28 | 'elapsed': '1w 3d 6h 2m 30s', 29 | 'id': 3, 30 | 'status_id': 1, 31 | 'test_id': 5, 32 | 'version': '1.0RC'} 33 | 34 | self.mock_user_data = [ 35 | { 36 | "email": "han@example.com", 37 | "id": 1, 38 | "is_active": True, 39 | "name": "Han Solo" 40 | }, 41 | { 42 | "email": "jabba@example.com", 43 | "id": 2, 44 | "is_active": True, 45 | "name": "Jabba the Hutt" 46 | } 47 | ] 48 | 49 | self.mock_status_data = [ 50 | { 51 | "color_bright": 12709313, 52 | "color_dark": 6667107, 53 | "color_medium": 9820525, 54 | "id": 1, 55 | "is_final": True, 56 | "is_system": True, 57 | "is_untested": True, 58 | "label": "Passed", 59 | "name": "passed" 60 | } 61 | ] 62 | 63 | self.mock_test_data = [ 64 | { 65 | "assignedto_id": 1, 66 | "case_id": 1, 67 | "estimate": "1m 5s", 68 | "estimate_forecast": None, 69 | "id": 5, 70 | "priority_id": 2, 71 | "run_id": 1, 72 | "status_id": 5, 73 | "title": "Verify line spacing on multi-page document", 74 | "type_id": 4 75 | } 76 | ] 77 | 78 | self.result = Result(self.mock_result_data) 79 | 80 | @mock.patch('testrail.api.requests.get') 81 | def test_get_assigned_to_type(self, mock_get): 82 | mock_response = mock.Mock() 83 | mock_response.json.return_value = copy.deepcopy(self.mock_user_data) 84 | mock_response.status_code = 200 85 | mock_get.return_value = mock_response 86 | user = self.result.assigned_to 87 | self.assertEqual(type(user), User) 88 | 89 | @mock.patch('testrail.api.requests.get') 90 | def test_get_assigned_to(self, mock_get): 91 | mock_response = mock.Mock() 92 | mock_response.json.return_value = copy.deepcopy(self.mock_user_data) 93 | mock_response.status_code = 200 94 | mock_get.return_value = mock_response 95 | user = self.result.assigned_to 96 | self.assertEqual(user._content, self.mock_user_data[0]) 97 | 98 | def test_get_assigned_to_null(self): 99 | self.result._content['assignedto_id'] = None 100 | self.assertEqual(type(self.result.assigned_to), User) 101 | self.assertEqual(self.result.assigned_to.id, None) 102 | 103 | @mock.patch('testrail.api.requests.get') 104 | def test_set_assigned_to(self, mock_get): 105 | mock_response = mock.Mock() 106 | mock_response.json.return_value = copy.deepcopy(self.mock_user_data) 107 | mock_response.status_code = 200 108 | mock_get.return_value = mock_response 109 | user = User(self.mock_user_data[1]) 110 | self.assertEqual(self.result.assigned_to.id, 1) 111 | self.result.assigned_to = user 112 | self.assertEqual(self.result._content['assignedto_id'], 2) 113 | self.assertEqual(self.result.assigned_to.id, 2) 114 | 115 | def test_set_assigned_to_invalid_type(self): 116 | with self.assertRaises(TestRailError) as e: 117 | self.result.assigned_to = 2 118 | self.assertEqual(str(e.exception), 'input must be a User object') 119 | 120 | @mock.patch('testrail.api.requests.get') 121 | def test_set_assigned_to_invalid_user(self, mock_get): 122 | mock_response = mock.Mock() 123 | mock_response.json.return_value = copy.deepcopy(self.mock_user_data) 124 | mock_response.status_code = 200 125 | mock_get.return_value = mock_response 126 | user = User() 127 | user._content['id'] = 5 128 | with self.assertRaises(TestRailError) as e: 129 | self.result.assigned_to = user 130 | self.assertEqual(str(e.exception), 131 | "User with ID '%s' is not valid" % user.id) 132 | 133 | @mock.patch('testrail.api.requests.get') 134 | def test_set_assigned_to_empty_user(self, mock_get): 135 | mock_response = mock.Mock() 136 | mock_response.json.return_value = copy.deepcopy(self.mock_user_data) 137 | mock_response.status_code = 200 138 | mock_get.return_value = mock_response 139 | user = User() 140 | with self.assertRaises(TestRailError) as e: 141 | self.result.assigned_to = user 142 | self.assertEqual(str(e.exception), 143 | "User with ID '%s' is not valid" % user.id) 144 | 145 | def test_get_comment_type(self): 146 | self.assertEqual(type(self.result.comment), str) 147 | 148 | def test_get_comment(self): 149 | self.assertEqual(self.result.comment, 'All steps passed') 150 | 151 | def test_get_comment_null(self): 152 | self.result._content['comment'] = None 153 | self.assertEqual(self.result.comment, None) 154 | 155 | def test_set_comment(self): 156 | self.assertEqual(self.result.comment, 'All steps passed') 157 | self.result.comment = 'tests failed' 158 | self.assertEqual(self.result._content['comment'], 'tests failed') 159 | self.assertEqual(self.result.comment, 'tests failed') 160 | 161 | def test_set_comment_invalid_type(self): 162 | with self.assertRaises(TestRailError) as e: 163 | self.result.comment = True 164 | self.assertEqual(str(e.exception), 'input must be a string') 165 | 166 | @mock.patch('testrail.api.requests.get') 167 | def test_get_created_type(self, mock_get): 168 | mock_response = mock.Mock() 169 | mock_response.json.return_value = copy.deepcopy(self.mock_user_data) 170 | mock_response.status_code = 200 171 | mock_get.return_value = mock_response 172 | self.assertEqual(type(self.result.created_by), User) 173 | 174 | @mock.patch('testrail.api.requests.get') 175 | def test_get_created_by(self, mock_get): 176 | mock_response = mock.Mock() 177 | mock_response.json.return_value = copy.deepcopy(self.mock_user_data) 178 | mock_response.status_code = 200 179 | mock_get.return_value = mock_response 180 | user = self.result.created_by 181 | self.assertEqual(user._content, self.mock_user_data[1]) 182 | 183 | @mock.patch('testrail.api.requests.get') 184 | def test_get_created_by_no_id(self, mock_get): 185 | mock_response = mock.Mock() 186 | mock_response.json.return_value = copy.deepcopy(self.mock_user_data) 187 | mock_response.status_code = 200 188 | mock_get.return_value = mock_response 189 | result = Result() 190 | with self.assertRaises(TestRailError) as e: 191 | result.created_by 192 | self.assertEqual(str(e.exception), "User ID 'None' was not found") 193 | 194 | @mock.patch('testrail.api.requests.get') 195 | def test_get_created_by_invalid_id(self, mock_get): 196 | mock_response = mock.Mock() 197 | mock_response.json.return_value = copy.deepcopy(self.mock_user_data) 198 | mock_response.status_code = 200 199 | mock_get.return_value = mock_response 200 | result = Result() 201 | result._content['created_by'] = 900 202 | with self.assertRaises(TestRailError) as e: 203 | result.created_by 204 | self.assertEqual(str(e.exception), "User ID '900' was not found") 205 | 206 | def test_get_created_on_type(self): 207 | self.assertEqual(type(self.result.created_on), datetime) 208 | 209 | def test_get_created_on(self): 210 | date_obj = datetime.fromtimestamp(1453504099) 211 | self.assertEqual(self.result.created_on, date_obj) 212 | 213 | def test_get_created_on_no_ts(self): 214 | self.assertEqual(Result().created_on, None) 215 | 216 | def test_get_defects_type(self): 217 | self.assertEqual(type(self.result.defects), list) 218 | 219 | def test_get_defects(self): 220 | self.assertEqual( 221 | self.result.defects, self.mock_result_data['defects'].split(',')) 222 | 223 | def test_get_defects_empty(self): 224 | self.result._content['defects'] = None 225 | self.assertEqual(self.result.defects, list()) 226 | 227 | def test_set_defects_invalid_type(self): 228 | with self.assertRaises(TestRailError) as e: 229 | self.result.defects = 'one, two' 230 | self.assertEqual(str(e.exception), 'input must be a list of strings') 231 | 232 | def test_set_defects_invalid_input(self): 233 | with self.assertRaises(TestRailError) as e: 234 | self.result.defects = ['one', 4] 235 | self.assertEqual(str(e.exception), 'input must be a list of strings') 236 | 237 | def test_set_defects(self): 238 | self.result.defects = ['1', 'b', '43'] 239 | self.assertEqual(self.result._content['defects'], '1,b,43') 240 | 241 | def test_set_defects_empty_list(self): 242 | self.result.defects = list() 243 | self.assertEqual(self.result._content['defects'], None) 244 | 245 | def test_get_elapsed_type(self): 246 | self.assertEqual(type(self.result.elapsed), timedelta) 247 | 248 | def test_get_elapsed_int_type(self): 249 | self.result._content['elapsed'] = 1 250 | self.assertEqual(type(self.result.elapsed), timedelta) 251 | 252 | def test_get_elapsed_int_seconds(self): 253 | self.result._content['elapsed'] = 60 254 | td = timedelta(seconds=60) 255 | self.assertEqual(self.result.elapsed, td) 256 | 257 | def test_get_elapsed_null(self): 258 | self.result._content['elapsed'] = None 259 | self.assertEqual(self.result.elapsed, None) 260 | 261 | def test_get_elapsed_weeks(self): 262 | self.result._content['elapsed'] = '10w' 263 | td = timedelta(weeks=10) 264 | self.assertEqual(self.result.elapsed, td) 265 | 266 | def test_get_elapsed_days(self): 267 | self.result._content['elapsed'] = '4d' 268 | td = timedelta(days=4) 269 | self.assertEqual(self.result.elapsed, td) 270 | 271 | def test_get_elapsed_hours(self): 272 | self.result._content['elapsed'] = '10h' 273 | td = timedelta(hours=10) 274 | self.assertEqual(self.result.elapsed, td) 275 | 276 | def test_get_elapsed_minutes(self): 277 | self.result._content['elapsed'] = '10m' 278 | td = timedelta(minutes=10) 279 | self.assertEqual(self.result.elapsed, td) 280 | 281 | def test_get_elapsed_seconds(self): 282 | self.result._content['elapsed'] = '120s' 283 | td = timedelta(minutes=2) 284 | self.assertEqual(self.result.elapsed, td) 285 | 286 | def test_get_elapsed_all(self): 287 | td = timedelta(weeks=1, days=3, hours=6, minutes=2, seconds=30) 288 | self.assertEqual(self.result.elapsed, td) 289 | 290 | def test_set_elapsed_invalid_type(self): 291 | with self.assertRaises(TestRailError) as e: 292 | self.result.elapsed = '5m' 293 | self.assertEqual(str(e.exception), 'input must be a timedelta') 294 | 295 | def test_set_elasped_invalid_value(self): 296 | with self.assertRaises(TestRailError) as e: 297 | self.result.elapsed = timedelta(weeks=10, seconds=1) 298 | self.assertEqual(str(e.exception), 'maximum elapsed time is 10 weeks') 299 | 300 | def test_set_elapsed(self): 301 | self.result.elapsed = timedelta(hours=2, seconds=30) 302 | self.assertEqual(self.result._content['elapsed'], 7230) 303 | 304 | def test_get_id_type(self): 305 | self.assertEqual(type(self.result.id), int) 306 | 307 | def test_get_id(self): 308 | self.assertEqual(self.result.id, 3) 309 | 310 | @mock.patch('testrail.api.requests.get') 311 | def test_get_status_type(self, mock_get): 312 | mock_response = mock.Mock() 313 | mock_response.json.return_value = copy.deepcopy(self.mock_status_data) 314 | mock_response.status_code = 200 315 | mock_get.return_value = mock_response 316 | self.assertEqual(type(self.result.status), Status) 317 | 318 | @mock.patch('testrail.api.requests.get') 319 | def test_get_status(self, mock_get): 320 | mock_response = mock.Mock() 321 | mock_response.json.return_value = copy.deepcopy(self.mock_status_data) 322 | mock_response.status_code = 200 323 | mock_get.return_value = mock_response 324 | self.assertEqual(self.result.status.label, 'Passed') 325 | 326 | @mock.patch('testrail.api.requests.get') 327 | def test_get_status_invalid_id(self, mock_get): 328 | mock_response = mock.Mock() 329 | mock_response.json.return_value = copy.deepcopy(self.mock_status_data) 330 | mock_response.status_code = 200 331 | mock_get.return_value = mock_response 332 | self.result._content['status_id'] = 0 333 | with self.assertRaises(TestRailError) as e: 334 | self.result.status 335 | self.assertEqual(str(e.exception), "Status ID '0' was not found") 336 | 337 | def test_set_status_invalid_type(self): 338 | with self.assertRaises(TestRailError) as e: 339 | self.result.status = 'passed' 340 | self.assertEqual(str(e.exception), 'input must be a Status') 341 | 342 | @mock.patch('testrail.api.requests.get') 343 | def test_set_status(self, mock_get): 344 | mock_response = mock.Mock() 345 | mock_response.json.return_value = copy.deepcopy(self.mock_status_data) 346 | mock_response.status_code = 200 347 | mock_get.return_value = mock_response 348 | self.result.status = Status({'id': 1}) 349 | self.assertEqual(self.result._content['status_id'], 1) 350 | 351 | @mock.patch('testrail.api.requests.get') 352 | def test_set_status_invalid_id(self, mock_get): 353 | mock_response = mock.Mock() 354 | mock_response.json.return_value = copy.deepcopy(self.mock_status_data) 355 | mock_response.status_code = 200 356 | mock_get.return_value = mock_response 357 | with self.assertRaises(TestRailError) as e: 358 | self.result.status = Status({'id': 0}) 359 | self.assertEqual(str(e.exception), "Status ID '0' was not found") 360 | 361 | def test_get_test_null(self): 362 | result_data = copy.deepcopy(self.mock_result_data) 363 | result_data['test_id'] = None 364 | result = Result(result_data) 365 | self.assertEqual(result.test.id, None) 366 | 367 | @mock.patch('testrail.api.requests.get') 368 | def test_get_test_type(self, mock_get): 369 | mock_response = mock.Mock() 370 | mock_response.json.return_value = copy.deepcopy(self.mock_test_data) 371 | mock_response.status_code = 200 372 | mock_get.return_value = mock_response 373 | self.assertEqual(type(self.result.test), Test) 374 | 375 | @mock.patch('testrail.api.requests.get') 376 | def test_get_test(self, mock_get): 377 | mock_response = mock.Mock() 378 | mock_response.json.return_value = copy.deepcopy(self.mock_test_data[0]) 379 | mock_response.status_code = 200 380 | mock_get.return_value = mock_response 381 | self.assertEqual(self.result.test.id, 5) 382 | 383 | @mock.patch('testrail.api.requests.get') 384 | def test_get_test_invalid_id(self, mock_get): 385 | mock_response = mock.Mock() 386 | mock_response.json.return_value = {u'error': u'Field :test_id is not a valid test.'} 387 | mock_response.status_code = 400 388 | mock_get.return_value = mock_response 389 | self.result._content['test_id'] = 100 390 | with self.assertRaises(TestRailError) as e: 391 | self.result.test 392 | self.assertEqual(str(e.exception), "Test ID '100' was not found") 393 | 394 | def test_set_test_invalid_type(self): 395 | with self.assertRaises(TestRailError) as e: 396 | self.result.test = 5 397 | self.assertEqual(str(e.exception), 'input must be a Test') 398 | 399 | @mock.patch('testrail.api.requests.get') 400 | def test_set_test(self, mock_get): 401 | mock_response = mock.Mock() 402 | mock_response.json.return_value = copy.deepcopy(self.mock_test_data) 403 | mock_response.status_code = 200 404 | mock_get.return_value = mock_response 405 | self.result.test = Test({'id': 5, 'run_id': 1}) 406 | self.assertEqual(self.result._content['test_id'], 5) 407 | 408 | @mock.patch('testrail.api.requests.get') 409 | def test_set_test_invalid_id(self, mock_get): 410 | mock_response = mock.Mock() 411 | mock_response.json.return_value = copy.deepcopy(self.mock_test_data) 412 | mock_response.status_code = 200 413 | mock_get.return_value = mock_response 414 | with self.assertRaises(TestRailError) as e: 415 | self.result.test = Test({'id': 100, 'run_id': 1}) 416 | self.assertEqual(str(e.exception), "Test ID '100' was not found") 417 | 418 | def test_get_version_type(self): 419 | self.assertEqual(type(self.result.version), str) 420 | 421 | def test_get_version_null(self): 422 | self.result._content['version'] = None 423 | self.assertEqual(self.result.version, None) 424 | 425 | def test_get_version(self): 426 | self.assertEqual(self.result.version, '1.0RC') 427 | 428 | def test_set_version_invalid_type(self): 429 | with self.assertRaises(TestRailError) as e: 430 | self.result.version = 1.0 431 | self.assertEqual(str(e.exception), 'input must be a string') 432 | 433 | def test_set_version(self): 434 | self.result.version = '2.0' 435 | self.assertEqual(self.result._content['version'], '2.0') 436 | 437 | def test_raw_data(self): 438 | self.assertEqual(self.result.raw_data(), self.mock_result_data) 439 | -------------------------------------------------------------------------------- /tests/test_run.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import mock 3 | import datetime 4 | try: 5 | import unittest2 as unittest 6 | except ImportError: 7 | import unittest 8 | 9 | from testrail.api import API 10 | from testrail.run import Run 11 | from testrail.user import User 12 | from testrail.plan import Plan 13 | from testrail.case import Case 14 | from testrail.suite import Suite 15 | from testrail.project import Project 16 | from testrail.milestone import Milestone 17 | from testrail.helper import TestRailError 18 | 19 | 20 | class TestRun(unittest.TestCase): 21 | def setUp(self): 22 | self.mock_run_data = [ 23 | { 24 | "assignedto_id": 6, 25 | "blocked_count": 1, 26 | "case_ids": [ 27 | 8, 28 | 9 29 | ], 30 | "completed_on": None, 31 | "config": "Mock Config", 32 | "config_ids": [ 33 | 2, 34 | 6 35 | ], 36 | "created_by": 5, 37 | "created_on": 1393845644, 38 | "custom_status1_count": 0, 39 | "custom_status2_count": 0, 40 | "custom_status3_count": 0, 41 | "custom_status4_count": 0, 42 | "custom_status5_count": 0, 43 | "custom_status6_count": 0, 44 | "custom_status7_count": 0, 45 | "description": "Mock description", 46 | "failed_count": 2, 47 | "id": 81, 48 | "include_all": False, 49 | "is_completed": False, 50 | "milestone_id": 9, 51 | "name": "Mock Name", 52 | "passed_count": 3, 53 | "plan_id": 80, 54 | "project_id": 1, 55 | "retest_count": 7, 56 | "suite_id": 1, 57 | "untested_count": 17, 58 | "url": "http://mock_server/testrail/index.php?/runs/view/81" 59 | }, 60 | { 61 | "assignedto_id": 7, 62 | "blocked_count": 1, 63 | "completed_on": 100000, 64 | "config": "Mock Config", 65 | "config_ids": [ 66 | 2, 67 | 6 68 | ], 69 | "created_by": 1, 70 | "created_on": None, 71 | "custom_status1_count": 0, 72 | "custom_status2_count": 0, 73 | "custom_status3_count": 0, 74 | "custom_status4_count": 0, 75 | "custom_status5_count": 0, 76 | "custom_status6_count": 0, 77 | "custom_status7_count": 0, 78 | "description": None, 79 | "failed_count": 2, 80 | "id": 81, 81 | "include_all": False, 82 | "is_completed": False, 83 | "milestone_id": None, 84 | "name": "Mock Name", 85 | "passed_count": 2, 86 | "plan_id": 80, 87 | "project_id": 1, 88 | "retest_count": 1, 89 | "suite_id": 4, 90 | "untested_count": 3, 91 | "url": "http://mock_server/testrail/index.php?/runs/view/81" 92 | } 93 | ] 94 | 95 | self.mock_run_user = [ 96 | { 97 | "email": "mock1@email.com", 98 | "id": 5, 99 | "is_active": True, 100 | "name": "Mock Name 1" 101 | }, 102 | { 103 | "email": "mock2@email.com", 104 | "id": 6, 105 | "is_active": True, 106 | "name": "Mock Name 2" 107 | } 108 | ] 109 | 110 | self.mock_run_cases = [ 111 | { 112 | "created_by": 5, 113 | "created_on": 1392300984, 114 | "estimate": "1m 5s", 115 | "estimate_forecast": None, 116 | "id": 8, 117 | "milestone_id": 9, 118 | "priority_id": 2, 119 | "refs": "RF-1, RF-2", 120 | "section_id": 1, 121 | "suite_id": 1, 122 | "title": "Change document attributes (author, title, organization)", 123 | "type_id": 4, 124 | "updated_by": 6, 125 | "updated_on": 1393586511 126 | }, 127 | { 128 | "created_by": 5, 129 | "created_on": 1392300984, 130 | "estimate": "1m 5s", 131 | "estimate_forecast": None, 132 | "id": 9, 133 | "milestone_id": 9, 134 | "priority_id": 2, 135 | "refs": "RF-1, RF-2", 136 | "section_id": 1, 137 | "suite_id": 2, 138 | "title": "Change document attributes (author, title, organization)", 139 | "type_id": 4, 140 | "updated_by": 6, 141 | "updated_on": 1393586511 142 | }, 143 | ] 144 | 145 | self.mock_mstone_data = [ 146 | { 147 | "completed_on": 1389968184, 148 | "description": "Mock milestone description", 149 | "due_on": 1391968184, 150 | "id": 9, 151 | "is_completed": True, 152 | "name": "Release 1.5", 153 | "project_id": 1, 154 | "url": "http:///testrail/index.php?/milestones/view/1" 155 | } 156 | ] 157 | 158 | self.mock_suite_data = [ 159 | { 160 | "description": "suite description", 161 | "id": 1, 162 | "name": "Setup & Installation", 163 | "project_id": 1, 164 | "url": "http:///index.php?/suites/view/1", 165 | "is_baseline": False, 166 | "is_completed": True, 167 | "is_master": True, 168 | "completed_on": 1453504099 169 | }, 170 | { 171 | "description": "suite description 2", 172 | "id": 2, 173 | "name": "Setup & Installation", 174 | "project_id": 1, 175 | "url": "http:///index.php?/suites/view/1", 176 | "is_baseline": False, 177 | "is_completed": False, 178 | "is_master": True, 179 | "completed_on": None 180 | }, 181 | ] 182 | self.mock_plan_data = [{"id": 80, }, ] 183 | self.mock_project_data = [{"id": 1, }, {"id": 99, }] 184 | 185 | self.run = Run(self.mock_run_data[0]) 186 | self.run2 = Run(self.mock_run_data[1]) 187 | 188 | @mock.patch('testrail.api.API._refresh') 189 | @mock.patch('testrail.api.requests.get') 190 | def test_get_run_assigned_to_type(self, mock_get, refresh_mock): 191 | refresh_mock.return_value = True 192 | mock_response = mock.Mock() 193 | mock_response.json.return_value = copy.deepcopy(self.mock_run_user) 194 | mock_response.status_code = 200 195 | mock_get.return_value = mock_response 196 | self.assertTrue(isinstance(self.run.assigned_to, User)) 197 | 198 | @mock.patch('testrail.api.API._refresh') 199 | @mock.patch('testrail.api.requests.get') 200 | def test_run_assigned_to(self, mock_get, refresh_mock): 201 | refresh_mock.return_value = True 202 | mock_response = mock.Mock() 203 | mock_response.json.return_value = copy.deepcopy(self.mock_run_user) 204 | mock_response.status_code = 200 205 | mock_get.return_value = mock_response 206 | self.assertEqual(self.run.assigned_to.id, 6) 207 | 208 | def test_get_blocked_count_type(self): 209 | self.assertTrue(isinstance(self.run.blocked_count, int)) 210 | 211 | def test_get_blocked_count(self): 212 | self.assertEqual(self.run.blocked_count, 1) 213 | 214 | @mock.patch('testrail.api.API._refresh') 215 | @mock.patch('testrail.api.requests.get') 216 | def test_get_cases_container_type(self, mock_get, refresh_mock): 217 | refresh_mock.return_value = True 218 | mock_response = mock.Mock() 219 | mock_response.json.return_value = copy.deepcopy(self.mock_run_cases) 220 | mock_response.status_code = 200 221 | mock_get.return_value = mock_response 222 | 223 | self.assertTrue(isinstance(self.run.cases, list)) 224 | 225 | @mock.patch('testrail.api.API._refresh') 226 | @mock.patch('testrail.api.requests.get') 227 | def test_get_cases_type(self, mock_get, refresh_mock): 228 | refresh_mock.return_value = True 229 | mock_response = mock.Mock() 230 | mock_response.json.return_value = copy.deepcopy(self.mock_run_cases) 231 | mock_response.status_code = 200 232 | mock_get.return_value = mock_response 233 | 234 | case_check = lambda val: isinstance(val, Case) 235 | self.assertTrue(all(map(case_check, self.run.cases))) 236 | 237 | @mock.patch('testrail.api.API._refresh') 238 | @mock.patch('testrail.api.requests.get') 239 | def test_set_cases(self, mock_get, refresh_mock): 240 | refresh_mock.return_value = True 241 | mock_response = mock.Mock() 242 | mock_response.json.return_value = copy.deepcopy(self.mock_run_cases) 243 | mock_response.status_code = 200 244 | mock_get.return_value = mock_response 245 | 246 | new_cases = [Case(case) for case in self.mock_run_cases] 247 | self.run.cases = new_cases 248 | 249 | cases_from_run = [case.id for case in self.run.cases] 250 | self.assertEqual(cases_from_run, [8, 9]) 251 | 252 | @mock.patch('testrail.api.API._refresh') 253 | @mock.patch('testrail.api.requests.get') 254 | def test_set_cases_to_none(self, mock_get, refresh_mock): 255 | refresh_mock.return_value = True 256 | mock_response = mock.Mock() 257 | mock_response.json.return_value = copy.deepcopy(self.mock_run_cases) 258 | mock_response.status_code = 200 259 | mock_get.return_value = mock_response 260 | 261 | # Make sure they are set to something other than None 262 | new_cases = [Case(case) for case in self.mock_run_cases] 263 | self.run.cases = new_cases 264 | 265 | # Set to None and verify it stuck 266 | self.run.cases = None 267 | self.assertEqual(type(self.run.cases), list) 268 | self.assertEqual(len(self.run.cases), 0) 269 | 270 | def test_set_cases_invalid_container_type(self): 271 | with self.assertRaises(TestRailError) as e: 272 | self.run.cases = "asdf" 273 | self.assertEqual(str(e.exception), 'cases must be set to None or a container of Case objects') 274 | 275 | def test_set_cases_invalid_value_type(self): 276 | with self.assertRaises(TestRailError) as e: 277 | self.run.cases = [1, 2, 'gg'] 278 | self.assertEqual(str(e.exception), 'cases must be set to None or a container of Case objects') 279 | 280 | def test_get_completed_on_no_ts_type(self): 281 | self.assertEqual(self.run.completed_on, None) 282 | 283 | def test_get_completed_on_with_ts_type(self): 284 | self.assertTrue(isinstance(self.run2.completed_on, datetime.datetime)) 285 | 286 | def test_get_completed_on_with_ts(self): 287 | self.assertEqual( 288 | self.run2.completed_on, datetime.datetime.fromtimestamp(100000)) 289 | 290 | def test_get_config_type(self): 291 | self.assertTrue(isinstance(self.run.config, str)) 292 | 293 | def test_get_config(self): 294 | self.assertEqual(self.run.config, "Mock Config") 295 | 296 | def test_get_config_ids_type(self): 297 | self.assertTrue(isinstance(self.run.config_ids, list)) 298 | 299 | def test_get_config_ids(self): 300 | self.assertEqual(self.run.config_ids, [2, 6]) 301 | 302 | def test_get_created_on_no_ts_type(self): 303 | self.assertEqual(self.run2.created_on, None) 304 | 305 | def test_get_created_on_with_ts_type(self): 306 | self.assertTrue(isinstance(self.run.created_on, datetime.datetime)) 307 | 308 | def test_get_created_on_with_ts(self): 309 | self.assertEqual( 310 | self.run.created_on, datetime.datetime.fromtimestamp(1393845644)) 311 | 312 | @mock.patch('testrail.api.API._refresh') 313 | @mock.patch('testrail.api.requests.get') 314 | def test_get_created_by_type(self, mock_get, refresh_mock): 315 | refresh_mock.return_value = True 316 | mock_response = mock.Mock() 317 | mock_response.json.return_value = copy.deepcopy(self.mock_run_user) 318 | mock_response.status_code = 200 319 | mock_get.return_value = mock_response 320 | self.assertTrue(isinstance(self.run.created_by, User)) 321 | 322 | @mock.patch('testrail.api.API._refresh') 323 | @mock.patch('testrail.api.requests.get') 324 | def test_created_by(self, mock_get2, refresh_mock): 325 | refresh_mock.return_value = True 326 | mock_response = mock.Mock() 327 | mock_response.json.return_value = copy.deepcopy(self.mock_run_user) 328 | mock_response.status_code = 200 329 | mock_get2.return_value = mock_response 330 | self.assertEqual(self.run.created_by.id, 5) 331 | 332 | def test_get_description_type(self): 333 | self.assertTrue(isinstance(self.run.description, str)) 334 | 335 | def test_description(self): 336 | self.assertEqual(self.run.description, "Mock description") 337 | 338 | def test_get_failed_count_type(self): 339 | self.assertTrue(isinstance(self.run.failed_count, int)) 340 | 341 | def test_failed_count(self): 342 | self.assertEqual(self.run.failed_count, 2) 343 | 344 | def test_get_id_type(self): 345 | self.assertTrue(isinstance(self.run.id, int)) 346 | 347 | def test_get_id(self): 348 | self.assertEqual(self.run.id, 81) 349 | 350 | def test_get_include_all_type(self): 351 | self.assertTrue(isinstance(self.run.include_all, bool)) 352 | 353 | def test_get_include_all(self): 354 | self.assertEqual(self.run.include_all, False) 355 | 356 | def test_set_include_all(self): 357 | self.run.include_all = True 358 | self.assertTrue(self.run.include_all) 359 | 360 | def test_set_include_all_invalid_type(self): 361 | with self.assertRaises(TestRailError) as e: 362 | self.run.include_all = "asdf" 363 | self.assertEqual(str(e.exception), 'include_all must be a boolean') 364 | 365 | def test_get_is_completed_type(self): 366 | self.assertTrue(isinstance(self.run.is_completed, bool)) 367 | 368 | def test_is_completed(self): 369 | self.assertEqual(self.run.is_completed, False) 370 | 371 | @mock.patch('testrail.api.API._refresh') 372 | @mock.patch('testrail.api.requests.get') 373 | def test_get_milestone_type(self, mock_get, refresh_mock): 374 | refresh_mock.return_value = True 375 | mock_response = mock.Mock() 376 | mock_response.json.return_value = copy.deepcopy(self.mock_mstone_data) 377 | mock_response.status_code = 200 378 | mock_get.return_value = mock_response 379 | self.assertTrue(isinstance(self.run.milestone, Milestone)) 380 | 381 | @mock.patch('testrail.api.API._refresh') 382 | @mock.patch('testrail.api.requests.get') 383 | def test_milestone(self, mock_get, refresh_mock): 384 | refresh_mock.return_value = True 385 | mock_response = mock.Mock() 386 | mock_response.json.return_value = copy.deepcopy(self.mock_mstone_data) 387 | mock_response.status_code = 200 388 | mock_get.return_value = mock_response 389 | self.assertEqual(self.run.milestone.id, 9) 390 | 391 | @mock.patch('testrail.api.API._refresh') 392 | @mock.patch('testrail.api.requests.get') 393 | def test_set_milestone(self, mock_get, refresh_mock): 394 | refresh_mock.return_value = True 395 | mock_response = mock.Mock() 396 | mock_response.json.return_value = copy.deepcopy(self.mock_mstone_data) 397 | mock_response.status_code = 200 398 | mock_get.return_value = mock_response 399 | 400 | mstone = Milestone(self.mock_mstone_data[0]) 401 | self.run.milestone = mstone 402 | self.assertEqual(self.run.milestone.id, 9) 403 | 404 | @mock.patch('testrail.api.API._refresh') 405 | @mock.patch('testrail.api.requests.get') 406 | def test_milestone_with_no_id(self, mock_get, refresh_mock): 407 | refresh_mock.return_value = True 408 | mock_response = mock.Mock() 409 | mock_response.json.return_value = copy.deepcopy(self.mock_mstone_data) 410 | mock_response.status_code = 200 411 | mock_get.return_value = mock_response 412 | self.assertEqual(self.run2.milestone.id, None) 413 | 414 | @mock.patch('testrail.api.API._refresh') 415 | @mock.patch('testrail.api.requests.get') 416 | def test_get_plan_type(self, mock_get, refresh_mock): 417 | refresh_mock.return_value = True 418 | mock_response = mock.Mock() 419 | mock_response.json.return_value = copy.deepcopy(self.mock_plan_data) 420 | mock_response.status_code = 200 421 | mock_get.return_value = mock_response 422 | self.assertTrue(isinstance(self.run.plan, Plan)) 423 | 424 | @mock.patch('testrail.api.API._refresh') 425 | @mock.patch('testrail.api.requests.get') 426 | def test_plan(self, mock_get, refresh_mock): 427 | refresh_mock.return_value = True 428 | mock_response = mock.Mock() 429 | mock_response.json.return_value = copy.deepcopy(self.mock_plan_data) 430 | mock_response.status_code = 200 431 | mock_get.return_value = mock_response 432 | self.assertEqual(self.run.plan.id, 80) 433 | 434 | def test_get_name_type(self): 435 | self.assertTrue(isinstance(self.run.name, str)) 436 | 437 | def test_get_name(self): 438 | self.assertEqual(self.run.name, "Mock Name") 439 | 440 | def test_set_name(self): 441 | name = "Mock New Name" 442 | self.run.name = name 443 | self.assertEqual(self.run.name, name) 444 | 445 | def test_set_name_invalid_type(self): 446 | with self.assertRaises(TestRailError) as e: 447 | self.run.name = 394 448 | self.assertEqual(str(e.exception), 'input must be a string') 449 | 450 | def test_get_passed_count_type(self): 451 | self.assertTrue(isinstance(self.run.passed_count, int)) 452 | 453 | def test_passed_count(self): 454 | self.assertEqual(self.run.passed_count, 3) 455 | 456 | @mock.patch('testrail.api.API._refresh') 457 | @mock.patch('testrail.api.requests.get') 458 | def test_get_project_type(self, mock_get, refresh_mock): 459 | refresh_mock.return_value = True 460 | mock_response = mock.Mock() 461 | mock_response.json.return_value = copy.deepcopy(self.mock_project_data) 462 | mock_response.status_code = 200 463 | mock_get.return_value = mock_response 464 | self.assertTrue(isinstance(self.run.project, Project)) 465 | 466 | @mock.patch('testrail.api.API._refresh') 467 | @mock.patch('testrail.api.requests.get') 468 | def test_project(self, mock_get, refresh_mock): 469 | refresh_mock.return_value = True 470 | mock_response = mock.Mock() 471 | mock_response.json.return_value = copy.deepcopy(self.mock_project_data) 472 | mock_response.status_code = 200 473 | mock_get.return_value = mock_response 474 | self.assertEqual(self.run.project.id, 1) 475 | 476 | @mock.patch('testrail.api.API._refresh') 477 | @mock.patch('testrail.api.requests.get') 478 | def test_set_project(self, mock_get, refresh_mock): 479 | refresh_mock.return_value = True 480 | mock_response = mock.Mock() 481 | mock_response.json.return_value = copy.deepcopy(self.mock_project_data) 482 | mock_response.status_code = 200 483 | mock_get.return_value = mock_response 484 | 485 | project = Project(self.mock_project_data[1]) 486 | self.run.project = project 487 | self.assertEqual(self.run.project.id, 99) 488 | 489 | def test_set_project_invalid_type(self): 490 | with self.assertRaises(TestRailError) as e: 491 | self.run.project = 394 492 | self.assertEqual(str(e.exception), 'input must be a Project') 493 | 494 | def test_get_project_id_type(self): 495 | self.assertTrue(isinstance(self.run.project_id, int)) 496 | 497 | def test_project_id(self): 498 | self.assertEqual(self.run.project_id, 1) 499 | 500 | def test_get_retest_count_type(self): 501 | self.assertTrue(isinstance(self.run.retest_count, int)) 502 | 503 | def test_retest_count(self): 504 | self.assertEqual(self.run.retest_count, 7) 505 | 506 | @mock.patch('testrail.api.API._refresh') 507 | @mock.patch('testrail.api.requests.get') 508 | def test_get_suite_type(self, mock_get, refresh_mock): 509 | refresh_mock.return_value = True 510 | mock_response = mock.Mock() 511 | mock_response.json.return_value = copy.deepcopy(self.mock_suite_data) 512 | mock_response.status_code = 200 513 | mock_get.return_value = mock_response 514 | self.assertTrue(isinstance(self.run.suite, Suite)) 515 | 516 | @mock.patch('testrail.api.API._refresh') 517 | @mock.patch('testrail.api.requests.get') 518 | def test_get_suite(self, mock_get, refresh_mock): 519 | refresh_mock.return_value = True 520 | mock_response = mock.Mock() 521 | mock_response.json.return_value = copy.deepcopy(self.mock_suite_data) 522 | mock_response.status_code = 200 523 | mock_get.return_value = mock_response 524 | self.assertEqual(self.run.suite.id, 1) 525 | 526 | @mock.patch('testrail.api.API._refresh') 527 | @mock.patch('testrail.api.requests.get') 528 | def test_set_suite(self, mock_get, refresh_mock): 529 | refresh_mock.return_value = True 530 | mock_response = mock.Mock() 531 | mock_response.json.return_value = copy.deepcopy(self.mock_suite_data) 532 | mock_response.status_code = 200 533 | mock_get.return_value = mock_response 534 | 535 | suite = Suite(self.mock_suite_data[1]) 536 | self.run.suite = suite 537 | self.assertEqual(self.run.suite.id, 2) 538 | 539 | def test_set_suite_invalid_type(self): 540 | with self.assertRaises(TestRailError) as e: 541 | self.run.suite = 394 542 | self.assertEqual(str(e.exception), 'input must be a Suite') 543 | 544 | def test_get_untested_count_type(self): 545 | self.assertTrue(isinstance(self.run.untested_count, int)) 546 | 547 | def test_untested_count(self): 548 | self.assertEqual(self.run.untested_count, 17) 549 | 550 | def test_get_url_type(self): 551 | self.assertTrue(isinstance(self.run.url, str)) 552 | 553 | def test_url(self): 554 | self.assertTrue(self.run.url.startswith("http://")) 555 | 556 | def test_get_raw_data_type(self): 557 | self.assertTrue(isinstance(self.run.raw_data(), dict)) 558 | 559 | def test_raw_data(self): 560 | self.assertEqual(self.run.raw_data(), self.mock_run_data[0]) 561 | -------------------------------------------------------------------------------- /testrail/api.py: -------------------------------------------------------------------------------- 1 | from __future__ import division 2 | 3 | import os 4 | import collections 5 | from time import sleep 6 | from builtins import dict 7 | from datetime import datetime, timedelta 8 | 9 | import yaml 10 | import requests 11 | from retry import retry 12 | 13 | from testrail.helper import TestRailError, TooManyRequestsError, ServiceUnavailableError 14 | 15 | nested_dict = lambda: collections.defaultdict(nested_dict) 16 | 17 | 18 | class UpdateCache(object): 19 | """ Decorator class for updating API cache 20 | """ 21 | def __init__(self, cache): 22 | self.cache = cache 23 | 24 | def __call__(self, f): 25 | def wrapped_f(*args, **kwargs): 26 | api_resp = f(*args, **kwargs) 27 | if isinstance(api_resp, dict) and not api_resp: 28 | # Empty dict, indicating something at args[-1] was deleted. 29 | self._delete_from_cache(args[-1]) 30 | else: 31 | # Something must have been added or updated 32 | self._update_cache(api_resp) 33 | 34 | return api_resp 35 | return wrapped_f 36 | 37 | def _delete_from_cache(self, delete_id): 38 | ''' Check every dict inside of self.cache for an object with a matching 39 | ID 40 | ''' 41 | for project in self.cache.values(): 42 | obj_list = project['value'] 43 | for index, obj in enumerate(obj_list): 44 | if obj['id'] == delete_id: 45 | obj_list.pop(index) 46 | return 47 | else: 48 | # If we hit this, it means we looked at every object in every cache 49 | # and didn't find a match. Set the cache to refresh on the next call 50 | for project in self.cache.values(): 51 | project['ts'] = None 52 | 53 | return 54 | 55 | def _update_cache(self, update_obj): 56 | ''' Update the cache using update_obj. 57 | 58 | If a matching object is found in the cache, replace it with update_obj. 59 | If no matching object is found, append it to the cache 60 | ''' 61 | # Make update_obj a list if it isn't already 62 | update_list = update_obj if isinstance(update_obj, list) else [update_obj, ] 63 | 64 | for update_obj in update_list: 65 | if 'project_id' in update_obj: 66 | # Most response objects have a project_id 67 | obj_key = update_obj['project_id'] 68 | elif 'test_id' in update_obj: 69 | # Results have no project_id and are cached based on test_id 70 | obj_key = update_obj['test_id'] 71 | else: 72 | raise TestRailError("Unknown object type; can't update cache") 73 | 74 | if not self.cache[obj_key]['ts']: 75 | # The cache will clear on the next read, so no reason to add/update 76 | continue 77 | 78 | obj_list = self.cache[obj_key]['value'] 79 | for index, obj in enumerate(obj_list): 80 | if obj['id'] == update_obj['id']: 81 | obj_list[index] = update_obj 82 | break 83 | else: 84 | # If we get this far, it means we searched all objects without 85 | # finding a match. Add the object 86 | obj_list.append(update_obj) 87 | obj_list.sort(key=lambda x: x['id']) 88 | 89 | 90 | class API(object): 91 | _config = None 92 | _ts = datetime.now() - timedelta(days=1) 93 | _shared_state = {'_case_types': nested_dict(), 94 | '_cases': nested_dict(), 95 | '_configs': nested_dict(), 96 | '_milestones': nested_dict(), 97 | '_plans': nested_dict(), 98 | '_priorities': nested_dict(), 99 | '_projects': nested_dict(), 100 | '_results': nested_dict(), 101 | '_runs': nested_dict(), 102 | '_sections': nested_dict(), 103 | '_statuses': nested_dict(), 104 | '_suites': nested_dict(), 105 | '_tests': nested_dict(), 106 | '_users': nested_dict(), 107 | '_timeout': 30, 108 | '_project_id': None} 109 | 110 | def __init__(self, email=None, key=None, url=None): 111 | self.__dict__ = self._shared_state 112 | if email is not None and key is not None and url is not None: 113 | config = dict(email=email, key=key, url=url) 114 | self._config = config 115 | elif self._config is not None: 116 | config = self._config 117 | else: 118 | config = self._conf() 119 | 120 | self._auth = (config['email'], config['key']) 121 | self._url = config['url'] 122 | self.headers = {'Content-Type': 'application/json'} 123 | self.verify_ssl = config.get('verify_ssl', True) 124 | 125 | def _conf(self): 126 | TR_EMAIL = 'TESTRAIL_USER_EMAIL' 127 | TR_KEY = 'TESTRAIL_USER_KEY' 128 | TR_URL = 'TESTRAIL_URL' 129 | 130 | conf_path = '%s/.testrail.conf' % os.path.expanduser('~') 131 | 132 | if os.path.isfile(conf_path): 133 | with open(conf_path, 'r') as f: 134 | config = yaml.load(f, Loader=yaml.BaseLoader) 135 | else: 136 | config = { 137 | 'testrail': { 138 | 'user_email': None, 'user_key': None, 'url': None 139 | } 140 | } 141 | 142 | _email = os.environ.get(TR_EMAIL) or config['testrail'].get('user_email') 143 | _key = os.environ.get(TR_KEY) or config['testrail'].get('user_key') 144 | _url = os.environ.get(TR_URL) or config['testrail'].get('url') 145 | 146 | if _email is None: 147 | raise TestRailError('A user email must be set in environment ' + 148 | 'variable %s or in ' % TR_EMAIL + 149 | '~/.testrail.conf') 150 | if _key is None: 151 | raise TestRailError('A password or API key must be set in ' + 152 | 'environment variable %s or ' % TR_KEY + 153 | 'in ~/.testrail.conf') 154 | if _url is None: 155 | raise TestRailError('A URL must be set in environment variable ' + 156 | '%s or in ~/.testrail.conf' % TR_URL) 157 | 158 | if os.environ.get('TESTRAIL_VERIFY_SSL') is not None: 159 | verify_ssl = os.environ.get('TESTRAIL_VERIFY_SSL').lower() == 'true' 160 | elif config['testrail'].get('verify_ssl') is not None: 161 | verify_ssl = config['testrail'].get('verify_ssl').lower() == 'true' 162 | else: 163 | verify_ssl = True 164 | 165 | return {'email': _email, 'key': _key, 'url': _url, 'verify_ssl': verify_ssl} 166 | 167 | def _paginate_request(self, end_point, params, field): 168 | params["offset"] = 0 169 | params["limit"] = 250 170 | values = [] 171 | while True: 172 | items = self._get(end_point, params=params) 173 | if items["size"] == 0: 174 | return values 175 | values.extend(items[field]) 176 | params["offset"] += params["limit"] 177 | 178 | @staticmethod 179 | def _raise_on_429_or_503_status(resp): 180 | """ 429 is TestRail's status for too many API requests 181 | Use the 'Retry-After' key in the response header to sleep for the 182 | specified amount of time, then raise an exception to trigger the 183 | retry 184 | """ 185 | if resp.status_code == 429: 186 | wait_amount = int(resp.headers['Retry-After']) 187 | sleep(wait_amount) 188 | raise TooManyRequestsError("Too many API requests") 189 | if resp.status_code == 503: 190 | raise ServiceUnavailableError("Service Temporarily Unavailable") 191 | else: 192 | return 193 | 194 | def _refresh(self, ts): 195 | if not ts: 196 | return True 197 | 198 | td = (datetime.now() - ts) 199 | since_last = (td.microseconds + (td.seconds + td.days * 24 * 3600) * 10**6) / 10**6 200 | 201 | return since_last > self._timeout 202 | 203 | @classmethod 204 | def flush_cache(cls): 205 | """ Set all cache objects to refresh the next time they are accessed 206 | """ 207 | def clear_ts(cache): 208 | if 'ts' in cache: 209 | cache['ts'] = None 210 | for val in cache.values(): 211 | if isinstance(val, dict): 212 | clear_ts(val) 213 | 214 | 215 | for cache in cls._shared_state.values(): 216 | if not isinstance(cache, dict): 217 | continue 218 | else: 219 | clear_ts(cache) 220 | 221 | def set_project_id(self, project_id): 222 | self._project_id = project_id 223 | 224 | # User Requests 225 | def users(self): 226 | if self._refresh(self._users['ts']): 227 | # get new value, if request is good update value with new ts. 228 | self._users['value'] = self._get('get_users') 229 | self._users['ts'] = datetime.now() 230 | return self._users['value'] 231 | 232 | def user_with_id(self, user_id): 233 | try: 234 | return list(filter(lambda x: x['id'] == user_id, self.users()))[0] 235 | except IndexError: 236 | raise TestRailError("User ID '%s' was not found" % user_id) 237 | 238 | def user_with_email(self, user_email): 239 | try: 240 | return list(filter(lambda x: x['email'] == user_email, self.users()))[0] 241 | except IndexError: 242 | raise TestRailError("User email '%s' was not found" % user_email) 243 | 244 | # Project Requests 245 | def projects(self): 246 | if self._refresh(self._projects['ts']): 247 | # get new value, if request is good update value with new ts. 248 | self._projects['value'] = self._paginate_request("get_projects", {}, "projects") 249 | self._projects['ts'] = datetime.now() 250 | return self._projects['value'] 251 | 252 | def project_with_id(self, project_id): 253 | try: 254 | return list(filter(lambda x: x['id'] == project_id, self.projects()))[0] 255 | except IndexError: 256 | raise TestRailError("Project ID '%s' was not found" % project_id) 257 | 258 | # Suite Requests 259 | def suites(self, project_id=None): 260 | project_id = project_id or self._project_id 261 | if self._refresh(self._suites[project_id]['ts']): 262 | # get new value, if request is good update value with new ts. 263 | _suites = self._get('get_suites/%s' % project_id) 264 | self._suites[project_id]['value'] = _suites 265 | self._suites[project_id]['ts'] = datetime.now() 266 | return self._suites[project_id]['value'] 267 | 268 | def suite_with_id(self, suite_id): 269 | try: 270 | return list(filter(lambda x: x['id'] == suite_id, self.suites()))[0] 271 | except IndexError: 272 | raise TestRailError("Suite ID '%s' was not found" % suite_id) 273 | 274 | @UpdateCache(_shared_state['_suites']) 275 | def add_suite(self, suite): 276 | fields = ['name', 'description'] 277 | fields.extend(self._custom_field_discover(suite)) 278 | 279 | project_id = suite.get('project_id') 280 | payload = self._payload_gen(fields, suite) 281 | return self._post('add_suite/%s' % project_id, payload) 282 | 283 | # Case Requests 284 | def cases(self, project_id=None, suite_id=-1): 285 | project_id = project_id or self._project_id 286 | if self._refresh(self._cases[project_id][suite_id]['ts']): 287 | # get new value, if request is good update value with new ts. 288 | endpoint = 'get_cases/%s' % project_id 289 | params = {'suite_id': suite_id} if suite_id != -1 else {} 290 | self._cases[project_id][suite_id]["value"] = self._paginate_request(endpoint, params, "cases") 291 | self._cases[project_id][suite_id]['ts'] = datetime.now() 292 | return self._cases[project_id][suite_id]['value'] 293 | 294 | def case_with_id(self, case_id, suite_id=None): 295 | try: 296 | return list(filter(lambda x: x['id'] == case_id, self.cases(suite_id=suite_id)))[0] 297 | except IndexError: 298 | raise TestRailError("Case ID '%s' was not found" % case_id) 299 | 300 | def add_case(self, case): 301 | fields = ['title', 'template_id', 'type_id', 'priority_id', 'estimate', 302 | 'milestone_id', 'refs'] 303 | section_id = case.get('section_id') 304 | payload = self._payload_gen(fields, case) 305 | #TODO get update cache working for now reset cache 306 | self.flush_cache() 307 | return self._post('add_case/%s' % section_id, payload) 308 | 309 | def update_case(self, case): 310 | fields = ['title', 'template_id', 'type_id', 'priority_id', 'estimate', 311 | 'milestone_id', 'refs'] 312 | fields.extend(self._custom_field_discover(case)) 313 | 314 | data = self._payload_gen(fields, case) 315 | #TODO get update cache working for now reset cache 316 | self.flush_cache() 317 | return self._post('update_case/%s' % case.get('id'), data) 318 | 319 | 320 | def case_types(self): 321 | if self._refresh(self._case_types['ts']): 322 | # get new value, if request is good update value with new ts. 323 | _case_types = self._get('get_case_types') 324 | self._case_types['value'] = _case_types 325 | self._case_types['ts'] = datetime.now() 326 | return self._case_types['value'] 327 | 328 | def case_type_with_id(self, case_type_id): 329 | try: 330 | return list(filter( 331 | lambda x: x['id'] == case_type_id, self.case_types()))[0] 332 | except IndexError: 333 | return TestRailError( 334 | "Case Type ID '%s' was not found" % case_type_id) 335 | 336 | # Milestone Requests 337 | def milestones(self, project_id): 338 | if self._refresh(self._milestones[project_id]['ts']): 339 | # get new value, if request is good update value with new ts. 340 | endpoint = 'get_milestones/%s' % project_id 341 | self._milestones[project_id]['value'] = self._paginate_request(endpoint, {}, "milestones") 342 | self._milestones[project_id]['ts'] = datetime.now() 343 | return self._milestones[project_id]['value'] 344 | 345 | def milestone_with_id(self, milestone_id, project_id=None): 346 | if project_id is None: 347 | return self._get('get_milestone/%s' % milestone_id) 348 | else: 349 | try: 350 | return list(filter(lambda x: x['id'] == milestone_id, 351 | self.milestones(project_id)))[0] 352 | except IndexError: 353 | raise TestRailError( 354 | "Milestone ID '%s' was not found" % milestone_id) 355 | 356 | @UpdateCache(_shared_state['_milestones']) 357 | def add_milestone(self, milestone): 358 | fields = ['name', 'description', 'due_on'] 359 | fields.extend(self._custom_field_discover(milestone)) 360 | 361 | project_id = milestone.get('project_id') 362 | payload = self._payload_gen(fields, milestone) 363 | return self._post('add_milestone/%s' % project_id, payload) 364 | 365 | @UpdateCache(_shared_state['_milestones']) 366 | def update_milestone(self, milestone): 367 | fields = ['name', 'description', 'due_on', 'is_completed'] 368 | fields.extend(self._custom_field_discover(milestone)) 369 | 370 | data = self._payload_gen(fields, milestone) 371 | return self._post('update_milestone/%s' % milestone.get('id'), data) 372 | 373 | @UpdateCache(_shared_state['_milestones']) 374 | def delete_milestone(self, milestone_id): 375 | return self._post('delete_milestone/%s' % milestone_id) 376 | 377 | # Priority Requests 378 | def priorities(self): 379 | if self._refresh(self._priorities['ts']): 380 | # get new value, if request is good update value with new ts. 381 | _priorities = self._get('get_priorities') 382 | self._priorities['value'] = _priorities 383 | self._priorities['ts'] = datetime.now() 384 | return self._priorities['value'] 385 | 386 | def priority_with_id(self, priority_id): 387 | try: 388 | return list(filter( 389 | lambda x: x['id'] == priority_id, self.priorities()))[0] 390 | except IndexError: 391 | raise TestRailError("Priority ID '%s' was not found") 392 | 393 | # Section Requests 394 | def sections(self, project_id=None, suite_id=-1): 395 | project_id = project_id or self._project_id 396 | if self._refresh(self._sections[project_id][suite_id]['ts']): 397 | params = {'suite_id': suite_id} if suite_id != -1 else None 398 | endpoint = 'get_sections/%s' % project_id 399 | self._sections[project_id][suite_id]['value'] = self._paginate_request(endpoint, params, "sections") 400 | self._sections[project_id][suite_id]['ts'] = datetime.now() 401 | return self._sections[project_id][suite_id]['value'] 402 | 403 | def section_with_id(self, section_id): 404 | try: 405 | return list(filter(lambda x: x['id'] == section_id, self.sections()))[0] 406 | except IndexError: 407 | raise TestRailError("Section ID '%s' was not found" % section_id) 408 | except TestRailError: 409 | # project must not be in single suite mode 410 | return self._get('get_section/%s' % section_id) 411 | 412 | def add_section(self, section): 413 | fields = ['description', 'suite_id', 'parent_id', 'name'] 414 | fields.extend(self._custom_field_discover(section)) 415 | 416 | project_id = section.get('project_id') or self._project_id 417 | payload = self._payload_gen(fields, section) 418 | #TODO get update cache working for now reset cache 419 | self.flush_cache() 420 | return self._post('add_section/%s' % project_id, payload) 421 | 422 | # Plan Requests 423 | def plans(self, project_id=None): 424 | project_id = project_id or self._project_id 425 | if self._refresh(self._plans[project_id]['ts']): 426 | # get new value, if request is good update value with new ts. 427 | endpoint = 'get_plans/%s' % project_id 428 | self._plans[project_id]['value'] = self._paginate_request(endpoint, {}, "plans") 429 | self._plans[project_id]['ts'] = datetime.now() 430 | return self._plans[project_id]['value'] 431 | 432 | def plan_with_id(self, plan_id, with_entries=False): 433 | #TODO consider checking if plan already has entries and if not add it 434 | if with_entries: 435 | return self._get('get_plan/%s' % plan_id) 436 | try: 437 | return list(filter(lambda x: x['id'] == plan_id, self.plans()))[0] 438 | except IndexError: 439 | raise TestRailError("Plan ID '%s' was not found" % plan_id) 440 | 441 | @UpdateCache(_shared_state['_plans']) 442 | def add_plan(self, plan): 443 | fields = ['name', 'description', 'milestone_id', 'entries'] 444 | fields.extend(self._custom_field_discover(plan)) 445 | 446 | project_id = plan.get('project_id') 447 | payload = self._payload_gen(fields, plan) 448 | return self._post('add_plan/%s' % project_id, payload) 449 | 450 | # can't @UpdateCache b/c it doesn't include project_id 451 | def add_plan_entry(self, plan_entry): 452 | fields = ['suite_id', 'name', 'description', 'assignedto_id', 453 | 'include_all', 'case_ids', 'config_ids', 'runs'] 454 | fields.extend(self._custom_field_discover(plan_entry)) 455 | 456 | plan_id = plan_entry.get('plan_id') 457 | payload = self._payload_gen(fields, plan_entry) 458 | return self._post('add_plan_entry/%s' % plan_id, payload) 459 | 460 | # Run Requests 461 | def runs(self, project_id=None, completed=None): 462 | project_id = project_id or self._project_id 463 | if self._refresh(self._runs[project_id]['ts']): 464 | # get new value, if request is good update value with new ts. 465 | endpoint = 'get_runs/%s' % project_id 466 | if completed is not None: 467 | endpoint += '&is_completed=%s' % str(int(completed)) 468 | self._runs[project_id]['value'] = self._paginate_request(endpoint, {}, "runs") 469 | self._runs[project_id]['ts'] = datetime.now() 470 | return self._runs[project_id]['value'] 471 | 472 | def run_with_id(self, run_id): 473 | try: 474 | return list(filter(lambda x: x['id'] == run_id, self.runs()))[0] 475 | except IndexError: 476 | raise TestRailError("Run ID '%s' was not found" % run_id) 477 | 478 | @UpdateCache(_shared_state['_runs']) 479 | def add_run(self, run): 480 | fields = ['name', 'description', 'suite_id', 'milestone_id', 481 | 'assignedto_id', 'include_all', 'case_ids'] 482 | fields.extend(self._custom_field_discover(run)) 483 | 484 | project_id = run.get('project_id') 485 | payload = self._payload_gen(fields, run) 486 | return self._post('add_run/%s' % project_id, payload) 487 | 488 | @UpdateCache(_shared_state['_runs']) 489 | def update_run(self, run): 490 | fields = [ 491 | 'name', 'description', 'milestone_id', 'include_all', 'case_ids'] 492 | fields.extend(self._custom_field_discover(run)) 493 | 494 | data = self._payload_gen(fields, run) 495 | return self._post('update_run/%s' % run.get('id'), data) 496 | 497 | @UpdateCache(_shared_state['_runs']) 498 | def close_run(self, run_id): 499 | return self._post('close_run/%s' % run_id) 500 | 501 | @UpdateCache(_shared_state['_runs']) 502 | def delete_run(self, run_id): 503 | return self._post('delete_run/%s' % run_id) 504 | 505 | @UpdateCache(_shared_state['_plans']) 506 | def add_plan(self, plan): 507 | fields = ['name', 'description', 'milestone_id'] 508 | fields.extend(self._custom_field_discover(plan)) 509 | 510 | project_id = plan.get('project_id') 511 | payload = self._payload_gen(fields, plan) 512 | return self._post('add_plan/%s' % project_id, payload) 513 | 514 | @UpdateCache(_shared_state['_plans']) 515 | def update_plan(self, plan): 516 | fields = ['name', 'description', 'milestone_id'] 517 | fields.extend(self._custom_field_discover(plan)) 518 | 519 | data = self._payload_gen(fields, plan) 520 | return self._post('update_plan/%s' % plan.get('id'), data) 521 | 522 | @UpdateCache(_shared_state['_plans']) 523 | def close_plan(self, plan_id): 524 | return self._post('close_plan/%s' % plan_id) 525 | 526 | @UpdateCache(_shared_state['_plans']) 527 | def delete_plan(self, plan_id): 528 | return self._post('delete_plan/%s' % plan_id) 529 | 530 | # Test Requests 531 | def tests(self, run_id): 532 | if self._refresh(self._tests[run_id]['ts']): 533 | endpoint = 'get_tests/%s' % run_id 534 | self._tests[run_id]['value'] = self._paginate_request(endpoint, {}, "tests") 535 | self._tests[run_id]['ts'] = datetime.now() 536 | return self._tests[run_id]['value'] 537 | 538 | def test_with_id(self, test_id, run_id=None): 539 | if run_id is not None: 540 | try: 541 | return list(filter(lambda x: x['id'] == test_id, self.tests(run_id)))[0] 542 | except IndexError: 543 | raise TestRailError("Test ID '%s' was not found" % test_id) 544 | else: 545 | try: 546 | return self._get('get_test/%s' % test_id) 547 | except TestRailError: 548 | raise TestRailError("Test ID '%s' was not found" % test_id) 549 | 550 | # Result Requests 551 | def results_by_run(self, run_id): 552 | if self._refresh(self._results[run_id]['ts']): 553 | endpoint = 'get_results_for_run/%s' % run_id 554 | self._results[run_id]['value'] = self._paginate_request(endpoint, {}, "results") 555 | self._results[run_id]['ts'] = datetime.now() 556 | return self._results[run_id]['value'] 557 | 558 | def results_by_test(self, test_id): 559 | if self._refresh(self._results[test_id]['ts']): 560 | endpoint = 'get_results/%s' % test_id 561 | self._results[test_id]['value'] = self._paginate_request(endpoint, {}, "results") 562 | self._results[test_id]['ts'] = datetime.now() 563 | return self._results[test_id]['value'] 564 | 565 | @UpdateCache(_shared_state['_results']) 566 | def add_result(self, data): 567 | fields = ['status_id', 568 | 'comment', 569 | 'version', 570 | 'elapsed', 571 | 'defects', 572 | 'assignedto_id'] 573 | fields.extend(self._custom_field_discover(data)) 574 | 575 | payload = self._payload_gen(fields, data) 576 | payload['elapsed'] = str(payload['elapsed']) + 's' 577 | result = self._post('add_result/%s' % data['test_id'], payload) 578 | 579 | # Need to update the _tests cache to mark the run for refresh 580 | for run in self._tests: 581 | for test in self._tests[run]['value']: 582 | if test['id'] == data['test_id']: 583 | self._tests[run]['ts'] = None 584 | return result 585 | else: 586 | raise TestRailError("Could not find test '%s' in cache to update" % data['test_id']) 587 | 588 | @UpdateCache(_shared_state['_results']) 589 | def add_results(self, results, run_id): 590 | fields = ['status_id', 591 | 'test_id', 592 | 'comment', 593 | 'version', 594 | 'elapsed', 595 | 'defects', 596 | 'assignedto_id'] 597 | 598 | payload = {'results': list()} 599 | for result in results: 600 | custom_field = fields + self._custom_field_discover(result) 601 | result = self._payload_gen(custom_field + fields, result) 602 | result['elapsed'] = str(result['elapsed']) + 's' 603 | payload['results'].append(result) 604 | 605 | response = self._post('add_results/%s' % run_id, payload) 606 | 607 | # Need to update the _tests cache to mark the run for refresh 608 | self._tests[run_id]['ts'] = None 609 | 610 | return response 611 | 612 | def _custom_field_discover(self, entity): 613 | return [field for field in entity.keys() if field.startswith('custom_')] 614 | 615 | # Status Requests 616 | def statuses(self): 617 | if self._refresh(self._statuses['ts']): 618 | _statuses = self._get('get_statuses') 619 | self._statuses['value'] = _statuses 620 | self._statuses['ts'] = datetime.now() 621 | return self._statuses['value'] 622 | 623 | def status_with_id(self, status_id): 624 | try: 625 | return list(filter(lambda x: x['id'] == status_id, self.statuses()))[0] 626 | except IndexError: 627 | raise TestRailError("Status ID '%s' was not found" % status_id) 628 | 629 | def configs(self): 630 | if self._refresh(self._configs['ts']): 631 | _configs = self._get('get_configs/%s' % self._project_id) 632 | self._configs['value'] = _configs 633 | self._configs['ts'] = datetime.now() 634 | return self._configs['value'] 635 | 636 | @retry(ServiceUnavailableError, tries=30, delay=10) 637 | @retry((TooManyRequestsError, ValueError), tries=3, delay=1, backoff=2) 638 | def _get(self, uri, params=None): 639 | uri = '/index.php?/api/v2/%s' % uri 640 | r = requests.get(self._url+uri, params=params, auth=self._auth, 641 | headers=self.headers, verify=self.verify_ssl) 642 | 643 | self._raise_on_429_or_503_status(r) 644 | 645 | if r.status_code == 200: 646 | return r.json() 647 | else: 648 | try: 649 | response = r.json() 650 | except ValueError: 651 | response = dict() 652 | 653 | response.update({'response_headers': str(r.headers), 654 | 'payload': params, 655 | 'url': r.url, 656 | 'status_code': r.status_code, 657 | 'error': response.get('error', None)}) 658 | raise TestRailError(response) 659 | 660 | @retry(ServiceUnavailableError, tries=30, delay=10) 661 | @retry(TooManyRequestsError, tries=3, delay=1, backoff=2) 662 | def _post(self, uri, data={}): 663 | uri = '/index.php?/api/v2/%s' % uri 664 | r = requests.post(self._url+uri, json=data, auth=self._auth, 665 | verify=self.verify_ssl) 666 | 667 | self._raise_on_429_or_503_status(r) 668 | 669 | if r.status_code == 200: 670 | try: 671 | return r.json() 672 | except ValueError: 673 | return dict() 674 | else: 675 | try: 676 | response = r.json() 677 | except ValueError: 678 | response = dict() 679 | 680 | response.update({'post_data': data, 681 | 'response_headers': str(r.headers), 682 | 'url': r.url, 683 | 'status_code': r.status_code, 684 | 'error': response.get('error', None)}) 685 | raise TestRailError(response) 686 | 687 | def _payload_gen(self, fields, data): 688 | payload = dict() 689 | for field in fields: 690 | if data.get(field) is not None: 691 | payload[field] = data.get(field) 692 | return payload 693 | --------------------------------------------------------------------------------